Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e0dcd5374 |
@@ -46,13 +46,17 @@ Ensure after your edits are done, your final comments do not contain intermediat
|
|||||||
|
|
||||||
## Mode Selection
|
## Mode Selection
|
||||||
|
|
||||||
choose the appropriate mode based on the prompt payload:
|
Before starting any work, you must first determine which mode to use by examining the request and calling ${ghPullfrogMcpName}/select_mode.
|
||||||
|
|
||||||
|
Available modes:
|
||||||
|
|
||||||
${[...modes, ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
|
${[...modes, ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
|
||||||
|
|
||||||
## Modes
|
**IMPORTANT**: The first thing you must do is:
|
||||||
|
1. Examine the user's request/prompt carefully
|
||||||
${[...modes, ...payload.modes].map((w) => `### ${w.name}\n\n${w.prompt}`).join("\n\n")}
|
2. Determine which mode is most appropriate based on the mode descriptions above
|
||||||
|
3. Call ${ghPullfrogMcpName}/select_mode with the chosen mode name
|
||||||
|
4. The tool will return detailed instructions for that mode - follow those instructions exactly
|
||||||
|
|
||||||
************* USER PROMPT *************
|
************* USER PROMPT *************
|
||||||
|
|
||||||
|
|||||||
+3
-14
@@ -1,7 +1,5 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { chmodSync, createWriteStream, existsSync } from "node:fs";
|
import { chmodSync, createWriteStream, existsSync } from "node:fs";
|
||||||
import { mkdtemp } from "node:fs/promises";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { pipeline } from "node:stream/promises";
|
import { pipeline } from "node:stream/promises";
|
||||||
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||||
@@ -97,11 +95,7 @@ export async function installFromNpmTarball({
|
|||||||
|
|
||||||
log.info(`📦 Installing ${packageName}@${resolvedVersion}...`);
|
log.info(`📦 Installing ${packageName}@${resolvedVersion}...`);
|
||||||
|
|
||||||
// Derive temp directory prefix from package name (remove @, replace / with -, add trailing -)
|
const tempDir = process.env.PULLFROG_TEMP_DIR!;
|
||||||
const tempDirPrefix = packageName.replace("@", "").replace(/\//g, "-") + "-";
|
|
||||||
|
|
||||||
// Create temp directory
|
|
||||||
const tempDir = await mkdtemp(join(tmpdir(), tempDirPrefix));
|
|
||||||
const tarballPath = join(tempDir, "package.tgz");
|
const tarballPath = join(tempDir, "package.tgz");
|
||||||
|
|
||||||
// Download tarball from npm
|
// Download tarball from npm
|
||||||
@@ -183,12 +177,7 @@ export async function installFromCurl({
|
|||||||
}: InstallFromCurlParams): Promise<string> {
|
}: InstallFromCurlParams): Promise<string> {
|
||||||
log.info(`📦 Installing ${executableName}...`);
|
log.info(`📦 Installing ${executableName}...`);
|
||||||
|
|
||||||
// Derive temp directory prefix from executable name (sanitize similar to package name)
|
const tempDir = process.env.PULLFROG_TEMP_DIR!;
|
||||||
// Replace any special characters with - and ensure trailing -
|
|
||||||
const tempDirPrefix = executableName.replace(/[^a-zA-Z0-9]/g, "-") + "-";
|
|
||||||
|
|
||||||
// Create temp directory
|
|
||||||
const tempDir = await mkdtemp(join(tmpdir(), tempDirPrefix));
|
|
||||||
const installScriptPath = join(tempDir, "install.sh");
|
const installScriptPath = join(tempDir, "install.sh");
|
||||||
|
|
||||||
// Download the install script
|
// Download the install script
|
||||||
@@ -206,7 +195,7 @@ export async function installFromCurl({
|
|||||||
// Make install script executable
|
// Make install script executable
|
||||||
chmodSync(installScriptPath, 0o755);
|
chmodSync(installScriptPath, 0o755);
|
||||||
|
|
||||||
log.info("Installing to temp directory...");
|
log.info(`Installing to temp directory at ${tempDir}...`);
|
||||||
|
|
||||||
// Run the install script with HOME set to temp directory
|
// Run the install script with HOME set to temp directory
|
||||||
// The Cursor install script installs to $HOME/.local/bin/{executableName}
|
// The Cursor install script installs to $HOME/.local/bin/{executableName}
|
||||||
|
|||||||
@@ -18146,7 +18146,7 @@ var require_summary = __commonJS({
|
|||||||
exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
|
exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
|
||||||
var os_1 = __require("os");
|
var os_1 = __require("os");
|
||||||
var fs_1 = __require("fs");
|
var fs_1 = __require("fs");
|
||||||
var { access, appendFile, writeFile } = fs_1.promises;
|
var { access, appendFile, writeFile: writeFile2 } = fs_1.promises;
|
||||||
exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
|
exports.SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
|
||||||
exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
|
exports.SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
|
||||||
var Summary = class {
|
var Summary = class {
|
||||||
@@ -18204,7 +18204,7 @@ var require_summary = __commonJS({
|
|||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
|
const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
|
||||||
const filePath2 = yield this.filePath();
|
const filePath2 = yield this.filePath();
|
||||||
const writeFunc = overwrite ? writeFile : appendFile;
|
const writeFunc = overwrite ? writeFile2 : appendFile;
|
||||||
yield writeFunc(filePath2, this._buffer, { encoding: "utf8" });
|
yield writeFunc(filePath2, this._buffer, { encoding: "utf8" });
|
||||||
return this.emptyBuffer();
|
return this.emptyBuffer();
|
||||||
});
|
});
|
||||||
@@ -26991,13 +26991,13 @@ var ProcessTransport = class {
|
|||||||
this.logForDebugging(data.toString());
|
this.logForDebugging(data.toString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const cleanup = () => {
|
const cleanup2 = () => {
|
||||||
if (this.child && !this.child.killed) {
|
if (this.child && !this.child.killed) {
|
||||||
this.child.kill("SIGTERM");
|
this.child.kill("SIGTERM");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.processExitHandler = cleanup;
|
this.processExitHandler = cleanup2;
|
||||||
this.abortHandler = cleanup;
|
this.abortHandler = cleanup2;
|
||||||
process.on("exit", this.processExitHandler);
|
process.on("exit", this.processExitHandler);
|
||||||
this.abortController.signal.addEventListener("abort", this.abortHandler);
|
this.abortController.signal.addEventListener("abort", this.abortHandler);
|
||||||
this.child.on("error", (error2) => {
|
this.child.on("error", (error2) => {
|
||||||
@@ -28223,15 +28223,15 @@ var Query = class {
|
|||||||
const messageId = "id" in mcpRequest.message ? mcpRequest.message.id : null;
|
const messageId = "id" in mcpRequest.message ? mcpRequest.message.id : null;
|
||||||
const key = `${serverName}:${messageId}`;
|
const key = `${serverName}:${messageId}`;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const cleanup = () => {
|
const cleanup2 = () => {
|
||||||
this.pendingMcpResponses.delete(key);
|
this.pendingMcpResponses.delete(key);
|
||||||
};
|
};
|
||||||
const resolveAndCleanup = (response) => {
|
const resolveAndCleanup = (response) => {
|
||||||
cleanup();
|
cleanup2();
|
||||||
resolve(response);
|
resolve(response);
|
||||||
};
|
};
|
||||||
const rejectAndCleanup = (error2) => {
|
const rejectAndCleanup = (error2) => {
|
||||||
cleanup();
|
cleanup2();
|
||||||
reject(error2);
|
reject(error2);
|
||||||
};
|
};
|
||||||
this.pendingMcpResponses.set(key, {
|
this.pendingMcpResponses.set(key, {
|
||||||
@@ -28241,7 +28241,7 @@ var Query = class {
|
|||||||
if (transport.onmessage) {
|
if (transport.onmessage) {
|
||||||
transport.onmessage(mcpRequest.message);
|
transport.onmessage(mcpRequest.message);
|
||||||
} else {
|
} else {
|
||||||
cleanup();
|
cleanup2();
|
||||||
reject(new Error("No message handler registered"));
|
reject(new Error("No message handler registered"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -32888,7 +32888,7 @@ function query({
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/action",
|
name: "@pullfrog/action",
|
||||||
version: "0.0.107",
|
version: "0.0.108",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -32962,6 +32962,8 @@ var package_default = {
|
|||||||
|
|
||||||
// utils/cli.ts
|
// utils/cli.ts
|
||||||
var core = __toESM(require_core(), 1);
|
var core = __toESM(require_core(), 1);
|
||||||
|
import { appendFileSync as appendFileSync2, existsSync as existsSync2 } from "node:fs";
|
||||||
|
import { join as join4 } from "node:path";
|
||||||
var isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
var isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
||||||
var isDebugEnabled = process.env.LOG_LEVEL === "debug";
|
var isDebugEnabled = process.env.LOG_LEVEL === "debug";
|
||||||
function startGroup2(name) {
|
function startGroup2(name) {
|
||||||
@@ -33157,8 +33159,82 @@ var log = {
|
|||||||
/**
|
/**
|
||||||
* End a collapsed group
|
* End a collapsed group
|
||||||
*/
|
*/
|
||||||
endGroup: endGroup2
|
endGroup: endGroup2,
|
||||||
|
/**
|
||||||
|
* Log MCP tool call information to mcpLog.txt in the temp directory
|
||||||
|
*/
|
||||||
|
toolCall: ({
|
||||||
|
toolName,
|
||||||
|
request,
|
||||||
|
result,
|
||||||
|
error: error2
|
||||||
|
}) => {
|
||||||
|
const logPath = getMcpLogPath();
|
||||||
|
const params = { toolName, request };
|
||||||
|
if (error2) {
|
||||||
|
params.error = error2;
|
||||||
|
} else if (result) {
|
||||||
|
params.result = result;
|
||||||
|
}
|
||||||
|
const logEntry = formatToolCall(params);
|
||||||
|
appendFileSync2(logPath, logEntry, "utf-8");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
function getMcpLogPath() {
|
||||||
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
||||||
|
return join4(tempDir, "mcpLog.txt");
|
||||||
|
}
|
||||||
|
function formatJsonValue(value2) {
|
||||||
|
const compact = JSON.stringify(value2);
|
||||||
|
return compact.length > 80 || compact.includes("\n") ? JSON.stringify(value2, null, 2) : compact;
|
||||||
|
}
|
||||||
|
function formatIndentedField(label, content) {
|
||||||
|
if (!content.includes("\n")) {
|
||||||
|
return ` ${label}: ${content}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
const lines = content.split("\n");
|
||||||
|
let formatted = ` ${label}: ${lines[0]}
|
||||||
|
`;
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
formatted += ` ${lines[i]}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
function formatToolInput(request) {
|
||||||
|
const requestFormatted = formatJsonValue(request);
|
||||||
|
if (requestFormatted === "{}") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return formatIndentedField("input", requestFormatted);
|
||||||
|
}
|
||||||
|
function formatToolResult(result) {
|
||||||
|
try {
|
||||||
|
const parsed2 = JSON.parse(result);
|
||||||
|
const formatted = formatJsonValue(parsed2);
|
||||||
|
return formatIndentedField("result", formatted);
|
||||||
|
} catch {
|
||||||
|
return formatIndentedField("result", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function formatToolCall({
|
||||||
|
toolName,
|
||||||
|
request,
|
||||||
|
result,
|
||||||
|
error: error2
|
||||||
|
}) {
|
||||||
|
let logEntry = `\u2192 ${toolName}
|
||||||
|
`;
|
||||||
|
logEntry += formatToolInput(request);
|
||||||
|
if (error2) {
|
||||||
|
logEntry += formatIndentedField("error", error2);
|
||||||
|
} else if (result) {
|
||||||
|
logEntry += formatToolResult(result);
|
||||||
|
}
|
||||||
|
logEntry += "\n";
|
||||||
|
return logEntry;
|
||||||
|
}
|
||||||
|
|
||||||
// mcp/index.ts
|
// mcp/index.ts
|
||||||
var ghPullfrogMcpName = "gh-pullfrog";
|
var ghPullfrogMcpName = "gh-pullfrog";
|
||||||
@@ -33257,15 +33333,17 @@ Ensure after your edits are done, your final comments do not contain intermediat
|
|||||||
|
|
||||||
## Mode Selection
|
## Mode Selection
|
||||||
|
|
||||||
choose the appropriate mode based on the prompt payload:
|
Before starting any work, you must first determine which mode to use by examining the request and calling ${ghPullfrogMcpName}/select_mode.
|
||||||
|
|
||||||
|
Available modes:
|
||||||
|
|
||||||
${[...modes, ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
|
${[...modes, ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
|
||||||
|
|
||||||
## Modes
|
**IMPORTANT**: The first thing you must do is:
|
||||||
|
1. Examine the user's request/prompt carefully
|
||||||
${[...modes, ...payload.modes].map((w) => `### ${w.name}
|
2. Determine which mode is most appropriate based on the mode descriptions above
|
||||||
|
3. Call ${ghPullfrogMcpName}/select_mode with the chosen mode name
|
||||||
${w.prompt}`).join("\n\n")}
|
4. The tool will return detailed instructions for that mode - follow those instructions exactly
|
||||||
|
|
||||||
************* USER PROMPT *************
|
************* USER PROMPT *************
|
||||||
|
|
||||||
@@ -33275,10 +33353,8 @@ ${JSON.stringify(payload.event, null, 2)}`;
|
|||||||
|
|
||||||
// agents/shared.ts
|
// agents/shared.ts
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { chmodSync, createWriteStream as createWriteStream2, existsSync as existsSync2 } from "node:fs";
|
import { chmodSync, createWriteStream as createWriteStream2, existsSync as existsSync3 } from "node:fs";
|
||||||
import { mkdtemp } from "node:fs/promises";
|
import { join as join5 } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import { join as join4 } from "node:path";
|
|
||||||
import { pipeline } from "node:stream/promises";
|
import { pipeline } from "node:stream/promises";
|
||||||
async function installFromNpmTarball({
|
async function installFromNpmTarball({
|
||||||
packageName,
|
packageName,
|
||||||
@@ -33306,9 +33382,8 @@ async function installFromNpmTarball({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info(`\u{1F4E6} Installing ${packageName}@${resolvedVersion}...`);
|
log.info(`\u{1F4E6} Installing ${packageName}@${resolvedVersion}...`);
|
||||||
const tempDirPrefix = packageName.replace("@", "").replace(/\//g, "-") + "-";
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
||||||
const tempDir = await mkdtemp(join4(tmpdir(), tempDirPrefix));
|
const tarballPath = join5(tempDir, "package.tgz");
|
||||||
const tarballPath = join4(tempDir, "package.tgz");
|
|
||||||
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
|
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
|
||||||
let tarballUrl;
|
let tarballUrl;
|
||||||
if (packageName.startsWith("@")) {
|
if (packageName.startsWith("@")) {
|
||||||
@@ -33337,9 +33412,9 @@ async function installFromNpmTarball({
|
|||||||
`Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`
|
`Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const extractedDir = join4(tempDir, "package");
|
const extractedDir = join5(tempDir, "package");
|
||||||
const cliPath = join4(extractedDir, executablePath);
|
const cliPath = join5(extractedDir, executablePath);
|
||||||
if (!existsSync2(cliPath)) {
|
if (!existsSync3(cliPath)) {
|
||||||
throw new Error(`Executable not found in extracted package at ${cliPath}`);
|
throw new Error(`Executable not found in extracted package at ${cliPath}`);
|
||||||
}
|
}
|
||||||
if (installDependencies) {
|
if (installDependencies) {
|
||||||
@@ -33365,9 +33440,8 @@ async function installFromCurl({
|
|||||||
executableName
|
executableName
|
||||||
}) {
|
}) {
|
||||||
log.info(`\u{1F4E6} Installing ${executableName}...`);
|
log.info(`\u{1F4E6} Installing ${executableName}...`);
|
||||||
const tempDirPrefix = executableName.replace(/[^a-zA-Z0-9]/g, "-") + "-";
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
||||||
const tempDir = await mkdtemp(join4(tmpdir(), tempDirPrefix));
|
const installScriptPath = join5(tempDir, "install.sh");
|
||||||
const installScriptPath = join4(tempDir, "install.sh");
|
|
||||||
log.info(`Downloading install script from ${installUrl}...`);
|
log.info(`Downloading install script from ${installUrl}...`);
|
||||||
const installScriptResponse = await fetch(installUrl);
|
const installScriptResponse = await fetch(installUrl);
|
||||||
if (!installScriptResponse.ok) {
|
if (!installScriptResponse.ok) {
|
||||||
@@ -33378,7 +33452,7 @@ async function installFromCurl({
|
|||||||
await pipeline(installScriptResponse.body, fileStream);
|
await pipeline(installScriptResponse.body, fileStream);
|
||||||
log.info(`Downloaded install script to ${installScriptPath}`);
|
log.info(`Downloaded install script to ${installScriptPath}`);
|
||||||
chmodSync(installScriptPath, 493);
|
chmodSync(installScriptPath, 493);
|
||||||
log.info("Installing to temp directory...");
|
log.info(`Installing to temp directory at ${tempDir}...`);
|
||||||
const installResult = spawnSync("bash", [installScriptPath], {
|
const installResult = spawnSync("bash", [installScriptPath], {
|
||||||
cwd: tempDir,
|
cwd: tempDir,
|
||||||
env: {
|
env: {
|
||||||
@@ -33395,8 +33469,8 @@ async function installFromCurl({
|
|||||||
`Failed to install ${executableName}. Install script exited with code ${installResult.status}. Output: ${errorOutput}`
|
`Failed to install ${executableName}. Install script exited with code ${installResult.status}. Output: ${errorOutput}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const cliPath = join4(tempDir, ".local", "bin", executableName);
|
const cliPath = join5(tempDir, ".local", "bin", executableName);
|
||||||
if (!existsSync2(cliPath)) {
|
if (!existsSync3(cliPath)) {
|
||||||
throw new Error(`Executable not found at ${cliPath}`);
|
throw new Error(`Executable not found at ${cliPath}`);
|
||||||
}
|
}
|
||||||
chmodSync(cliPath, 493);
|
chmodSync(cliPath, 493);
|
||||||
@@ -33554,7 +33628,7 @@ async function createOutputSchemaFile(schema2) {
|
|||||||
}
|
}
|
||||||
const schemaDir = await fs2.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-"));
|
const schemaDir = await fs2.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-"));
|
||||||
const schemaPath = path.join(schemaDir, "schema.json");
|
const schemaPath = path.join(schemaDir, "schema.json");
|
||||||
const cleanup = async () => {
|
const cleanup2 = async () => {
|
||||||
try {
|
try {
|
||||||
await fs2.rm(schemaDir, { recursive: true, force: true });
|
await fs2.rm(schemaDir, { recursive: true, force: true });
|
||||||
} catch {
|
} catch {
|
||||||
@@ -33562,9 +33636,9 @@ async function createOutputSchemaFile(schema2) {
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await fs2.writeFile(schemaPath, JSON.stringify(schema2), "utf8");
|
await fs2.writeFile(schemaPath, JSON.stringify(schema2), "utf8");
|
||||||
return { schemaPath, cleanup };
|
return { schemaPath, cleanup: cleanup2 };
|
||||||
} catch (error2) {
|
} catch (error2) {
|
||||||
await cleanup();
|
await cleanup2();
|
||||||
throw error2;
|
throw error2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33592,7 +33666,7 @@ var Thread = class {
|
|||||||
return { events: this.runStreamedInternal(input, turnOptions) };
|
return { events: this.runStreamedInternal(input, turnOptions) };
|
||||||
}
|
}
|
||||||
async *runStreamedInternal(input, turnOptions = {}) {
|
async *runStreamedInternal(input, turnOptions = {}) {
|
||||||
const { schemaPath, cleanup } = await createOutputSchemaFile(turnOptions.outputSchema);
|
const { schemaPath, cleanup: cleanup2 } = await createOutputSchemaFile(turnOptions.outputSchema);
|
||||||
const options = this._threadOptions;
|
const options = this._threadOptions;
|
||||||
const { prompt, images } = normalizeInput(input);
|
const { prompt, images } = normalizeInput(input);
|
||||||
const generator = this._exec.run({
|
const generator = this._exec.run({
|
||||||
@@ -33625,7 +33699,7 @@ var Thread = class {
|
|||||||
yield parsed2;
|
yield parsed2;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await cleanup();
|
await cleanup2();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** Provides the input to the agent and returns the completed turn. */
|
/** Provides the input to the agent and returns the completed turn. */
|
||||||
@@ -34008,7 +34082,7 @@ function configureCodexMcpServers({ mcpServers, cliPath }) {
|
|||||||
// agents/cursor.ts
|
// agents/cursor.ts
|
||||||
import { spawn as spawn3 } from "node:child_process";
|
import { spawn as spawn3 } from "node:child_process";
|
||||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||||
import { join as join5 } from "node:path";
|
import { join as join6 } from "node:path";
|
||||||
var cursor = agent({
|
var cursor = agent({
|
||||||
name: "cursor",
|
name: "cursor",
|
||||||
inputKeys: ["cursor_api_key"],
|
inputKeys: ["cursor_api_key"],
|
||||||
@@ -34103,8 +34177,8 @@ var cursor = agent({
|
|||||||
});
|
});
|
||||||
function configureCursorMcpServers({ mcpServers, cliPath }) {
|
function configureCursorMcpServers({ mcpServers, cliPath }) {
|
||||||
const tempDir = cliPath.split("/.local/bin/")[0];
|
const tempDir = cliPath.split("/.local/bin/")[0];
|
||||||
const cursorConfigDir = join5(tempDir, ".cursor");
|
const cursorConfigDir = join6(tempDir, ".cursor");
|
||||||
const mcpConfigPath = join5(cursorConfigDir, "mcp.json");
|
const mcpConfigPath = join6(cursorConfigDir, "mcp.json");
|
||||||
mkdirSync2(cursorConfigDir, { recursive: true });
|
mkdirSync2(cursorConfigDir, { recursive: true });
|
||||||
writeFileSync2(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), "utf-8");
|
writeFileSync2(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), "utf-8");
|
||||||
}
|
}
|
||||||
@@ -34289,6 +34363,12 @@ var agents = {
|
|||||||
gemini
|
gemini
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// main.ts
|
||||||
|
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
|
||||||
|
import { mkdtemp, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join as join8 } from "node:path";
|
||||||
|
|
||||||
// node_modules/.pnpm/@ark+schema@0.53.0/node_modules/@ark/schema/out/shared/registry.js
|
// node_modules/.pnpm/@ark+schema@0.53.0/node_modules/@ark/schema/out/shared/registry.js
|
||||||
var _registryName = "$ark";
|
var _registryName = "$ark";
|
||||||
var suffix = 2;
|
var suffix = 2;
|
||||||
@@ -42016,7 +42096,7 @@ var caller = (options = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// node_modules/.pnpm/@ark+fs@0.53.0/node_modules/@ark/fs/out/fs.js
|
// node_modules/.pnpm/@ark+fs@0.53.0/node_modules/@ark/fs/out/fs.js
|
||||||
import { dirname as dirname2, join as join6, parse } from "node:path";
|
import { dirname as dirname2, join as join7, parse } from "node:path";
|
||||||
import * as process3 from "node:process";
|
import * as process3 from "node:process";
|
||||||
import { URL as URL2, fileURLToPath as fileURLToPath4 } from "node:url";
|
import { URL as URL2, fileURLToPath as fileURLToPath4 } from "node:url";
|
||||||
var filePath = (path4) => {
|
var filePath = (path4) => {
|
||||||
@@ -42030,7 +42110,7 @@ var filePath = (path4) => {
|
|||||||
return file;
|
return file;
|
||||||
};
|
};
|
||||||
var dirOfCaller = () => dirname2(filePath(caller({ methodName: "dirOfCaller", upStackBy: 1 }).file));
|
var dirOfCaller = () => dirname2(filePath(caller({ methodName: "dirOfCaller", upStackBy: 1 }).file));
|
||||||
var fromHere = (...joinWith) => join6(dirOfCaller(), ...joinWith);
|
var fromHere = (...joinWith) => join7(dirOfCaller(), ...joinWith);
|
||||||
var fsRoot = parse(process3.cwd()).root;
|
var fsRoot = parse(process3.cwd()).root;
|
||||||
|
|
||||||
// utils/github.ts
|
// utils/github.ts
|
||||||
@@ -42222,7 +42302,7 @@ function parseRepoContext() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mcp/config.ts
|
// mcp/config.ts
|
||||||
function createMcpConfigs(githubInstallationToken) {
|
function createMcpConfigs(githubInstallationToken, modes2) {
|
||||||
const repoContext = parseRepoContext();
|
const repoContext = parseRepoContext();
|
||||||
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
|
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
|
||||||
const serverPath = process.env.GITHUB_ACTIONS ? fromHere("mcp-server.js") : fromHere("server.ts");
|
const serverPath = process.env.GITHUB_ACTIONS ? fromHere("mcp-server.js") : fromHere("server.ts");
|
||||||
@@ -42232,7 +42312,8 @@ function createMcpConfigs(githubInstallationToken) {
|
|||||||
args: [serverPath],
|
args: [serverPath],
|
||||||
env: {
|
env: {
|
||||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||||
GITHUB_REPOSITORY: githubRepository
|
GITHUB_REPOSITORY: githubRepository,
|
||||||
|
PULLFROG_MODES: JSON.stringify(modes2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -42334,6 +42415,32 @@ var Inputs = type({
|
|||||||
...keyInputDefs,
|
...keyInputDefs,
|
||||||
"agent?": AgentName.or("undefined")
|
"agent?": AgentName.or("undefined")
|
||||||
});
|
});
|
||||||
|
async function main(inputs) {
|
||||||
|
const partialCtx = await initializeContext(inputs);
|
||||||
|
const ctx = partialCtx;
|
||||||
|
try {
|
||||||
|
await determineAgent(ctx);
|
||||||
|
setupGitAuth(ctx.githubInstallationToken, ctx.repoContext);
|
||||||
|
await setupTempDirectory(ctx);
|
||||||
|
setupMcpLogPolling(ctx);
|
||||||
|
ctx.payload = parsePayload(inputs);
|
||||||
|
setupMcpServers(ctx);
|
||||||
|
await installAgentCli(ctx);
|
||||||
|
validateApiKey(ctx);
|
||||||
|
const result = await runAgent(ctx);
|
||||||
|
return await handleAgentResult(result);
|
||||||
|
} catch (error2) {
|
||||||
|
const errorMessage = error2 instanceof Error ? error2.message : "Unknown error occurred";
|
||||||
|
log.error(errorMessage);
|
||||||
|
await log.writeSummary();
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await cleanup(partialCtx);
|
||||||
|
}
|
||||||
|
}
|
||||||
function throwMissingApiKeyError({
|
function throwMissingApiKeyError({
|
||||||
agentName,
|
agentName,
|
||||||
inputKeys,
|
inputKeys,
|
||||||
@@ -42367,87 +42474,124 @@ Alternatively, configure Pullfrog to use an agent with existing credentials in y
|
|||||||
log.error(message);
|
log.error(message);
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
async function main(inputs) {
|
async function initializeContext(inputs) {
|
||||||
let tokenToRevoke = null;
|
log.info(`\u{1F438} Running pullfrog/action@${package_default.version}...`);
|
||||||
try {
|
Inputs.assert(inputs);
|
||||||
log.info(`\u{1F438} Running pullfrog/action@${package_default.version}...`);
|
setupGitConfig();
|
||||||
Inputs.assert(inputs);
|
const { githubInstallationToken, wasAcquired } = await setupGitHubInstallationToken();
|
||||||
setupGitConfig();
|
const tokenToRevoke = wasAcquired ? githubInstallationToken : null;
|
||||||
const { githubInstallationToken, wasAcquired } = await setupGitHubInstallationToken();
|
const repoContext = parseRepoContext();
|
||||||
if (wasAcquired) {
|
return {
|
||||||
tokenToRevoke = githubInstallationToken;
|
inputs,
|
||||||
}
|
githubInstallationToken,
|
||||||
const repoContext = parseRepoContext();
|
tokenToRevoke,
|
||||||
const repoSettings = await fetchRepoSettings({
|
repoContext,
|
||||||
token: githubInstallationToken,
|
agentName: "claude",
|
||||||
repoContext
|
agent: agents.claude,
|
||||||
});
|
sharedTempDir: "",
|
||||||
const agentName = inputs.agent || repoSettings.defaultAgent || "claude";
|
mcpLogPath: "",
|
||||||
const agent2 = agents[agentName];
|
pollInterval: null
|
||||||
setupGitAuth(githubInstallationToken, repoContext);
|
};
|
||||||
const mcpServers = createMcpConfigs(githubInstallationToken);
|
}
|
||||||
log.debug(`\u{1F4CB} MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
|
async function determineAgent(ctx) {
|
||||||
const cliPath = await agent2.install();
|
const repoSettings = await fetchRepoSettings({
|
||||||
log.info(`Running ${agentName}...`);
|
token: ctx.githubInstallationToken,
|
||||||
let payload;
|
repoContext: ctx.repoContext
|
||||||
try {
|
});
|
||||||
const parsedPrompt = JSON.parse(inputs.prompt);
|
ctx.agentName = ctx.inputs.agent || repoSettings.defaultAgent || "claude";
|
||||||
if (!("~pullfrog" in parsedPrompt)) {
|
ctx.agent = agents[ctx.agentName];
|
||||||
throw new Error();
|
}
|
||||||
|
async function setupTempDirectory(ctx) {
|
||||||
|
ctx.sharedTempDir = await mkdtemp(join8(tmpdir(), "pullfrog-"));
|
||||||
|
process.env.PULLFROG_TEMP_DIR = ctx.sharedTempDir;
|
||||||
|
ctx.mcpLogPath = join8(ctx.sharedTempDir, "mcpLog.txt");
|
||||||
|
await writeFile(ctx.mcpLogPath, "", "utf-8");
|
||||||
|
log.info(`\u{1F4C2} PULLFROG_TEMP_DIR has been created at ${ctx.sharedTempDir}`);
|
||||||
|
}
|
||||||
|
function setupMcpLogPolling(ctx) {
|
||||||
|
let lastSize = 0;
|
||||||
|
ctx.pollInterval = setInterval(() => {
|
||||||
|
if (existsSync4(ctx.mcpLogPath)) {
|
||||||
|
const content = readFileSync2(ctx.mcpLogPath, "utf-8");
|
||||||
|
if (content.length > lastSize) {
|
||||||
|
const newContent = content.slice(lastSize);
|
||||||
|
process.stdout.write(newContent);
|
||||||
|
lastSize = content.length;
|
||||||
}
|
}
|
||||||
payload = parsedPrompt;
|
|
||||||
} catch {
|
|
||||||
payload = {
|
|
||||||
"~pullfrog": true,
|
|
||||||
agent: null,
|
|
||||||
prompt: inputs.prompt,
|
|
||||||
event: {},
|
|
||||||
modes
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
log.box(payload.prompt, { title: "Prompt" });
|
}, 100);
|
||||||
const matchingInputKey = agent2.inputKeys.find((inputKey) => inputs[inputKey]);
|
}
|
||||||
if (!matchingInputKey) {
|
function parsePayload(inputs) {
|
||||||
throwMissingApiKeyError({
|
try {
|
||||||
agentName,
|
const parsedPrompt = JSON.parse(inputs.prompt);
|
||||||
inputKeys: agent2.inputKeys,
|
if (!("~pullfrog" in parsedPrompt)) {
|
||||||
repoContext,
|
throw new Error();
|
||||||
inputs
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const apiKey = inputs[matchingInputKey];
|
return parsedPrompt;
|
||||||
const result = await agent2.run({
|
} catch {
|
||||||
payload,
|
|
||||||
mcpServers,
|
|
||||||
githubInstallationToken,
|
|
||||||
apiKey,
|
|
||||||
cliPath
|
|
||||||
});
|
|
||||||
if (!result.success) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: result.error || "Agent execution failed",
|
|
||||||
output: result.output
|
|
||||||
};
|
|
||||||
}
|
|
||||||
log.success("Task complete.");
|
|
||||||
await log.writeSummary();
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
"~pullfrog": true,
|
||||||
output: result.output || ""
|
agent: null,
|
||||||
|
prompt: inputs.prompt,
|
||||||
|
event: {},
|
||||||
|
modes
|
||||||
};
|
};
|
||||||
} catch (error2) {
|
}
|
||||||
const errorMessage = error2 instanceof Error ? error2.message : "Unknown error occurred";
|
}
|
||||||
log.error(errorMessage);
|
function setupMcpServers(ctx) {
|
||||||
await log.writeSummary();
|
const allModes = [...modes, ...ctx.payload.modes || []];
|
||||||
|
ctx.mcpServers = createMcpConfigs(ctx.githubInstallationToken, allModes);
|
||||||
|
log.debug(`\u{1F4CB} MCP Config: ${JSON.stringify(ctx.mcpServers, null, 2)}`);
|
||||||
|
}
|
||||||
|
async function installAgentCli(ctx) {
|
||||||
|
ctx.cliPath = await ctx.agent.install();
|
||||||
|
}
|
||||||
|
function validateApiKey(ctx) {
|
||||||
|
const matchingInputKey = ctx.agent.inputKeys.find(
|
||||||
|
(inputKey) => ctx.inputs[inputKey]
|
||||||
|
);
|
||||||
|
if (!matchingInputKey) {
|
||||||
|
throwMissingApiKeyError({
|
||||||
|
agentName: ctx.agentName,
|
||||||
|
inputKeys: ctx.agent.inputKeys,
|
||||||
|
repoContext: ctx.repoContext,
|
||||||
|
inputs: ctx.inputs
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ctx.apiKey = ctx.inputs[matchingInputKey];
|
||||||
|
}
|
||||||
|
async function runAgent(ctx) {
|
||||||
|
log.info(`Running ${ctx.agentName}...`);
|
||||||
|
log.box(ctx.payload.prompt, { title: "Prompt" });
|
||||||
|
return ctx.agent.run({
|
||||||
|
payload: ctx.payload,
|
||||||
|
mcpServers: ctx.mcpServers,
|
||||||
|
githubInstallationToken: ctx.githubInstallationToken,
|
||||||
|
apiKey: ctx.apiKey,
|
||||||
|
cliPath: ctx.cliPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function handleAgentResult(result) {
|
||||||
|
if (!result.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: errorMessage
|
error: result.error || "Agent execution failed",
|
||||||
|
output: result.output
|
||||||
};
|
};
|
||||||
} finally {
|
}
|
||||||
if (tokenToRevoke) {
|
log.success("Task complete.");
|
||||||
await revokeInstallationToken(tokenToRevoke);
|
await log.writeSummary();
|
||||||
}
|
return {
|
||||||
|
success: true,
|
||||||
|
output: result.output || ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function cleanup(ctx) {
|
||||||
|
if (ctx.pollInterval) {
|
||||||
|
clearInterval(ctx.pollInterval);
|
||||||
|
}
|
||||||
|
if (ctx.tokenToRevoke) {
|
||||||
|
await revokeInstallationToken(ctx.tokenToRevoke);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entry point for GitHub Action
|
* Entry point for GitHub Action
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { mkdtemp, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
import { flatMorph } from "@ark/util";
|
import { flatMorph } from "@ark/util";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { agents } from "./agents/index.ts";
|
import { agents } from "./agents/index.ts";
|
||||||
@@ -41,6 +45,36 @@ export interface MainResult {
|
|||||||
error?: string | undefined;
|
error?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function main(inputs: Inputs): Promise<MainResult> {
|
||||||
|
const partialCtx = await initializeContext(inputs);
|
||||||
|
const ctx = partialCtx as MainContext;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await determineAgent(ctx);
|
||||||
|
setupGitAuth(ctx.githubInstallationToken, ctx.repoContext);
|
||||||
|
await setupTempDirectory(ctx);
|
||||||
|
setupMcpLogPolling(ctx);
|
||||||
|
|
||||||
|
ctx.payload = parsePayload(inputs);
|
||||||
|
setupMcpServers(ctx);
|
||||||
|
await installAgentCli(ctx);
|
||||||
|
validateApiKey(ctx);
|
||||||
|
|
||||||
|
const result = await runAgent(ctx);
|
||||||
|
return await handleAgentResult(result);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||||
|
log.error(errorMessage);
|
||||||
|
await log.writeSummary();
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await cleanup(partialCtx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Throw an error for missing API key with helpful message linking to repo settings
|
* Throw an error for missing API key with helpful message linking to repo settings
|
||||||
*/
|
*/
|
||||||
@@ -90,110 +124,165 @@ To fix this, add the required secret to your GitHub repository:
|
|||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function main(inputs: Inputs): Promise<MainResult> {
|
interface MainContext {
|
||||||
let tokenToRevoke: string | null = null;
|
inputs: Inputs;
|
||||||
|
githubInstallationToken: string;
|
||||||
|
tokenToRevoke: string | null;
|
||||||
|
repoContext: RepoContext;
|
||||||
|
agentName: AgentName;
|
||||||
|
agent: (typeof agents)[AgentName];
|
||||||
|
sharedTempDir: string;
|
||||||
|
mcpLogPath: string;
|
||||||
|
pollInterval: NodeJS.Timeout | null;
|
||||||
|
payload: Payload;
|
||||||
|
mcpServers: ReturnType<typeof createMcpConfigs>;
|
||||||
|
cliPath: string;
|
||||||
|
apiKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
async function initializeContext(
|
||||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
inputs: Inputs
|
||||||
|
): Promise<Omit<MainContext, "payload" | "mcpServers" | "cliPath" | "apiKey">> {
|
||||||
|
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||||
|
Inputs.assert(inputs);
|
||||||
|
setupGitConfig();
|
||||||
|
|
||||||
Inputs.assert(inputs);
|
const { githubInstallationToken, wasAcquired } = await setupGitHubInstallationToken();
|
||||||
setupGitConfig();
|
const tokenToRevoke = wasAcquired ? githubInstallationToken : null;
|
||||||
|
const repoContext = parseRepoContext();
|
||||||
|
|
||||||
const { githubInstallationToken, wasAcquired } = await setupGitHubInstallationToken();
|
return {
|
||||||
if (wasAcquired) {
|
inputs,
|
||||||
tokenToRevoke = githubInstallationToken;
|
githubInstallationToken,
|
||||||
}
|
tokenToRevoke,
|
||||||
const repoContext = parseRepoContext();
|
repoContext,
|
||||||
|
agentName: "claude" as AgentName,
|
||||||
|
agent: agents.claude,
|
||||||
|
sharedTempDir: "",
|
||||||
|
mcpLogPath: "",
|
||||||
|
pollInterval: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const repoSettings = await fetchRepoSettings({
|
async function determineAgent(
|
||||||
token: githubInstallationToken,
|
ctx: Omit<MainContext, "payload" | "mcpServers" | "cliPath" | "apiKey">
|
||||||
repoContext,
|
): Promise<void> {
|
||||||
});
|
const repoSettings = await fetchRepoSettings({
|
||||||
|
token: ctx.githubInstallationToken,
|
||||||
|
repoContext: ctx.repoContext,
|
||||||
|
});
|
||||||
|
|
||||||
const agentName: AgentName = inputs.agent || repoSettings.defaultAgent || "claude";
|
ctx.agentName = ctx.inputs.agent || repoSettings.defaultAgent || "claude";
|
||||||
|
ctx.agent = agents[ctx.agentName];
|
||||||
|
}
|
||||||
|
|
||||||
const agent = agents[agentName];
|
async function setupTempDirectory(
|
||||||
|
ctx: Omit<MainContext, "payload" | "mcpServers" | "cliPath" | "apiKey">
|
||||||
|
): Promise<void> {
|
||||||
|
ctx.sharedTempDir = await mkdtemp(join(tmpdir(), "pullfrog-"));
|
||||||
|
process.env.PULLFROG_TEMP_DIR = ctx.sharedTempDir;
|
||||||
|
ctx.mcpLogPath = join(ctx.sharedTempDir, "mcpLog.txt");
|
||||||
|
await writeFile(ctx.mcpLogPath, "", "utf-8");
|
||||||
|
log.info(`📂 PULLFROG_TEMP_DIR has been created at ${ctx.sharedTempDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
setupGitAuth(githubInstallationToken, repoContext);
|
function setupMcpLogPolling(ctx: MainContext): void {
|
||||||
|
let lastSize = 0;
|
||||||
const mcpServers = createMcpConfigs(githubInstallationToken);
|
ctx.pollInterval = setInterval(() => {
|
||||||
|
if (existsSync(ctx.mcpLogPath)) {
|
||||||
log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
|
const content = readFileSync(ctx.mcpLogPath, "utf-8");
|
||||||
|
if (content.length > lastSize) {
|
||||||
// Install agent CLI before running
|
const newContent = content.slice(lastSize);
|
||||||
const cliPath = await agent.install();
|
process.stdout.write(newContent);
|
||||||
|
lastSize = content.length;
|
||||||
log.info(`Running ${agentName}...`);
|
|
||||||
|
|
||||||
let payload: Payload;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// attempt JSON parsing
|
|
||||||
const parsedPrompt = JSON.parse(inputs.prompt);
|
|
||||||
if (!("~pullfrog" in parsedPrompt)) {
|
|
||||||
// is non-pullfrog JSON (probably from a GitHub event), treat it as a plain text prompt
|
|
||||||
throw new Error();
|
|
||||||
}
|
}
|
||||||
payload = parsedPrompt as Payload;
|
|
||||||
} catch {
|
|
||||||
payload = {
|
|
||||||
"~pullfrog": true,
|
|
||||||
agent: null,
|
|
||||||
prompt: inputs.prompt,
|
|
||||||
event: {},
|
|
||||||
modes,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
log.box(payload.prompt, { title: "Prompt" });
|
function parsePayload(inputs: Inputs): Payload {
|
||||||
|
try {
|
||||||
const matchingInputKey = agent.inputKeys.find((inputKey) => inputs[inputKey]);
|
const parsedPrompt = JSON.parse(inputs.prompt);
|
||||||
|
if (!("~pullfrog" in parsedPrompt)) {
|
||||||
if (!matchingInputKey) {
|
throw new Error();
|
||||||
throwMissingApiKeyError({
|
|
||||||
agentName,
|
|
||||||
inputKeys: agent.inputKeys,
|
|
||||||
repoContext,
|
|
||||||
inputs,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return parsedPrompt as Payload;
|
||||||
const apiKey = inputs[matchingInputKey]!;
|
} catch {
|
||||||
|
|
||||||
const result = await agent.run({
|
|
||||||
payload,
|
|
||||||
mcpServers,
|
|
||||||
githubInstallationToken,
|
|
||||||
apiKey,
|
|
||||||
cliPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: result.error || "Agent execution failed",
|
|
||||||
output: result.output!,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
log.success("Task complete.");
|
|
||||||
await log.writeSummary();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
"~pullfrog": true,
|
||||||
output: result.output || "",
|
agent: null,
|
||||||
|
prompt: inputs.prompt,
|
||||||
|
event: {},
|
||||||
|
modes,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
}
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
}
|
||||||
log.error(errorMessage);
|
|
||||||
await log.writeSummary();
|
function setupMcpServers(ctx: MainContext): void {
|
||||||
return {
|
const allModes = [...modes, ...(ctx.payload.modes || [])];
|
||||||
success: false,
|
ctx.mcpServers = createMcpConfigs(ctx.githubInstallationToken, allModes);
|
||||||
error: errorMessage,
|
log.debug(`📋 MCP Config: ${JSON.stringify(ctx.mcpServers, null, 2)}`);
|
||||||
};
|
}
|
||||||
} finally {
|
|
||||||
if (tokenToRevoke) {
|
async function installAgentCli(ctx: MainContext): Promise<void> {
|
||||||
await revokeInstallationToken(tokenToRevoke);
|
ctx.cliPath = await ctx.agent.install();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateApiKey(ctx: MainContext): void {
|
||||||
|
const matchingInputKey = ctx.agent.inputKeys.find(
|
||||||
|
(inputKey) => ctx.inputs[inputKey as AgentInputKey]
|
||||||
|
);
|
||||||
|
if (!matchingInputKey) {
|
||||||
|
throwMissingApiKeyError({
|
||||||
|
agentName: ctx.agentName,
|
||||||
|
inputKeys: ctx.agent.inputKeys,
|
||||||
|
repoContext: ctx.repoContext,
|
||||||
|
inputs: ctx.inputs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ctx.apiKey = ctx.inputs[matchingInputKey as AgentInputKey]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAgent(ctx: MainContext): Promise<import("./agents/shared.ts").AgentResult> {
|
||||||
|
log.info(`Running ${ctx.agentName}...`);
|
||||||
|
log.box(ctx.payload.prompt, { title: "Prompt" });
|
||||||
|
|
||||||
|
return ctx.agent.run({
|
||||||
|
payload: ctx.payload,
|
||||||
|
mcpServers: ctx.mcpServers,
|
||||||
|
githubInstallationToken: ctx.githubInstallationToken,
|
||||||
|
apiKey: ctx.apiKey,
|
||||||
|
cliPath: ctx.cliPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAgentResult(
|
||||||
|
result: import("./agents/shared.ts").AgentResult
|
||||||
|
): Promise<MainResult> {
|
||||||
|
if (!result.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: result.error || "Agent execution failed",
|
||||||
|
output: result.output!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log.success("Task complete.");
|
||||||
|
await log.writeSummary();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: result.output || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanup(
|
||||||
|
ctx: Omit<MainContext, "payload" | "mcpServers" | "cliPath" | "apiKey">
|
||||||
|
): Promise<void> {
|
||||||
|
if (ctx.pollInterval) {
|
||||||
|
clearInterval(ctx.pollInterval);
|
||||||
|
}
|
||||||
|
if (ctx.tokenToRevoke) {
|
||||||
|
await revokeInstallationToken(ctx.tokenToRevoke);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+133
-46
@@ -97759,10 +97759,6 @@ var schema = ark.schema;
|
|||||||
var define2 = ark.define;
|
var define2 = ark.define;
|
||||||
var declare = ark.declare;
|
var declare = ark.declare;
|
||||||
|
|
||||||
// mcp/shared.ts
|
|
||||||
import { appendFileSync } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
|
||||||
|
|
||||||
// node_modules/.pnpm/universal-user-agent@7.0.3/node_modules/universal-user-agent/index.js
|
// node_modules/.pnpm/universal-user-agent@7.0.3/node_modules/universal-user-agent/index.js
|
||||||
function getUserAgent() {
|
function getUserAgent() {
|
||||||
if (typeof navigator === "object" && "userAgent" in navigator) {
|
if (typeof navigator === "object" && "userAgent" in navigator) {
|
||||||
@@ -101186,11 +101182,10 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// utils/github.ts
|
|
||||||
var core2 = __toESM(require_core(), 1);
|
|
||||||
|
|
||||||
// utils/cli.ts
|
// utils/cli.ts
|
||||||
var core = __toESM(require_core(), 1);
|
var core = __toESM(require_core(), 1);
|
||||||
|
import { appendFileSync, existsSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
var isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
var isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
||||||
var isDebugEnabled = process.env.LOG_LEVEL === "debug";
|
var isDebugEnabled = process.env.LOG_LEVEL === "debug";
|
||||||
function startGroup2(name) {
|
function startGroup2(name) {
|
||||||
@@ -101386,10 +101381,85 @@ var log = {
|
|||||||
/**
|
/**
|
||||||
* End a collapsed group
|
* End a collapsed group
|
||||||
*/
|
*/
|
||||||
endGroup: endGroup2
|
endGroup: endGroup2,
|
||||||
|
/**
|
||||||
|
* Log MCP tool call information to mcpLog.txt in the temp directory
|
||||||
|
*/
|
||||||
|
toolCall: ({
|
||||||
|
toolName,
|
||||||
|
request: request2,
|
||||||
|
result,
|
||||||
|
error: error41
|
||||||
|
}) => {
|
||||||
|
const logPath = getMcpLogPath();
|
||||||
|
const params = { toolName, request: request2 };
|
||||||
|
if (error41) {
|
||||||
|
params.error = error41;
|
||||||
|
} else if (result) {
|
||||||
|
params.result = result;
|
||||||
|
}
|
||||||
|
const logEntry = formatToolCall(params);
|
||||||
|
appendFileSync(logPath, logEntry, "utf-8");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
function getMcpLogPath() {
|
||||||
|
const tempDir = process.env.PULLFROG_TEMP_DIR;
|
||||||
|
return join(tempDir, "mcpLog.txt");
|
||||||
|
}
|
||||||
|
function formatJsonValue(value2) {
|
||||||
|
const compact = JSON.stringify(value2);
|
||||||
|
return compact.length > 80 || compact.includes("\n") ? JSON.stringify(value2, null, 2) : compact;
|
||||||
|
}
|
||||||
|
function formatIndentedField(label, content) {
|
||||||
|
if (!content.includes("\n")) {
|
||||||
|
return ` ${label}: ${content}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
const lines = content.split("\n");
|
||||||
|
let formatted = ` ${label}: ${lines[0]}
|
||||||
|
`;
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
formatted += ` ${lines[i]}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
function formatToolInput(request2) {
|
||||||
|
const requestFormatted = formatJsonValue(request2);
|
||||||
|
if (requestFormatted === "{}") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return formatIndentedField("input", requestFormatted);
|
||||||
|
}
|
||||||
|
function formatToolResult(result) {
|
||||||
|
try {
|
||||||
|
const parsed2 = JSON.parse(result);
|
||||||
|
const formatted = formatJsonValue(parsed2);
|
||||||
|
return formatIndentedField("result", formatted);
|
||||||
|
} catch {
|
||||||
|
return formatIndentedField("result", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function formatToolCall({
|
||||||
|
toolName,
|
||||||
|
request: request2,
|
||||||
|
result,
|
||||||
|
error: error41
|
||||||
|
}) {
|
||||||
|
let logEntry = `\u2192 ${toolName}
|
||||||
|
`;
|
||||||
|
logEntry += formatToolInput(request2);
|
||||||
|
if (error41) {
|
||||||
|
logEntry += formatIndentedField("error", error41);
|
||||||
|
} else if (result) {
|
||||||
|
logEntry += formatToolResult(result);
|
||||||
|
}
|
||||||
|
logEntry += "\n";
|
||||||
|
return logEntry;
|
||||||
|
}
|
||||||
|
|
||||||
// utils/github.ts
|
// utils/github.ts
|
||||||
|
var core2 = __toESM(require_core(), 1);
|
||||||
function parseRepoContext() {
|
function parseRepoContext() {
|
||||||
const githubRepo = process.env.GITHUB_REPOSITORY;
|
const githubRepo = process.env.GITHUB_REPOSITORY;
|
||||||
if (!githubRepo) {
|
if (!githubRepo) {
|
||||||
@@ -101415,53 +101485,25 @@ var getMcpContext = cached2(() => {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
function getLogPath() {
|
|
||||||
return join(process.cwd(), "log.txt");
|
|
||||||
}
|
|
||||||
function logToolCall({
|
|
||||||
toolName,
|
|
||||||
request: request2,
|
|
||||||
error: error41,
|
|
||||||
success
|
|
||||||
}) {
|
|
||||||
const logPath = getLogPath();
|
|
||||||
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
||||||
const requestStr = JSON.stringify(request2, null, 2);
|
|
||||||
let logEntry = `[${timestamp}] Tool: ${toolName}
|
|
||||||
Request: ${requestStr}
|
|
||||||
`;
|
|
||||||
if (error41) {
|
|
||||||
const errorMessage = error41 instanceof Error ? error41.message : String(error41);
|
|
||||||
const errorStack = error41 instanceof Error ? error41.stack : void 0;
|
|
||||||
logEntry += `Error: ${errorMessage}
|
|
||||||
`;
|
|
||||||
if (errorStack) {
|
|
||||||
logEntry += `Stack: ${errorStack}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
logEntry += `Status: FAILED
|
|
||||||
`;
|
|
||||||
} else if (success !== void 0) {
|
|
||||||
logEntry += `Status: ${success ? "SUCCESS" : "FAILED"}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
logEntry += `${"=".repeat(80)}
|
|
||||||
|
|
||||||
`;
|
|
||||||
appendFileSync(logPath, logEntry, "utf-8");
|
|
||||||
}
|
|
||||||
var tool = (toolDef) => {
|
var tool = (toolDef) => {
|
||||||
const toolName = toolDef.name;
|
const toolName = toolDef.name;
|
||||||
const originalExecute = toolDef.execute;
|
const originalExecute = toolDef.execute;
|
||||||
toolDef.execute = async (args2, context) => {
|
toolDef.execute = async (args2, context) => {
|
||||||
try {
|
try {
|
||||||
logToolCall({ toolName, request: args2 });
|
|
||||||
const result = await originalExecute(args2, context);
|
const result = await originalExecute(args2, context);
|
||||||
const isError = result && typeof result === "object" && "isError" in result && result.isError === true;
|
const isError = result && typeof result === "object" && "isError" in result && result.isError === true;
|
||||||
logToolCall({ toolName, request: args2, success: !isError });
|
const resultData = result && typeof result === "object" && "content" in result ? result.content[0]?.text : void 0;
|
||||||
|
if (isError && resultData) {
|
||||||
|
log.toolCall({ toolName, request: args2, error: resultData });
|
||||||
|
} else if (resultData) {
|
||||||
|
log.toolCall({ toolName, request: args2, result: resultData });
|
||||||
|
} else {
|
||||||
|
log.toolCall({ toolName, request: args2 });
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (error41) {
|
} catch (error41) {
|
||||||
logToolCall({ toolName, request: args2, error: error41 });
|
const errorMessage = error41 instanceof Error ? error41.message : String(error41);
|
||||||
|
log.toolCall({ toolName, request: args2, error: errorMessage });
|
||||||
throw error41;
|
throw error41;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -101786,12 +101828,57 @@ var ReviewTool = tool({
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// mcp/selectMode.ts
|
||||||
|
function getModes() {
|
||||||
|
const modesJson = process.env.PULLFROG_MODES;
|
||||||
|
if (modesJson) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(modesJson);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
var SelectMode = type({
|
||||||
|
modeName: type.string.describe(
|
||||||
|
"the name of the mode to select (e.g., 'Plan', 'Build', 'Review', 'Prompt')"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
var SelectModeTool = tool({
|
||||||
|
name: "select_mode",
|
||||||
|
description: "Select a mode and get its detailed prompt instructions. Call this first to determine which mode to use based on the request.",
|
||||||
|
parameters: SelectMode,
|
||||||
|
execute: contextualize(async ({ modeName }) => {
|
||||||
|
const allModes = getModes();
|
||||||
|
if (allModes.length === 0) {
|
||||||
|
return {
|
||||||
|
error: "No modes available. Modes must be provided via PULLFROG_MODES environment variable."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const selectedMode = allModes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
|
||||||
|
if (!selectedMode) {
|
||||||
|
const availableModes = allModes.map((m) => m.name).join(", ");
|
||||||
|
return {
|
||||||
|
error: `Mode "${modeName}" not found. Available modes: ${availableModes}`,
|
||||||
|
availableModes: allModes.map((m) => ({ name: m.name, description: m.description }))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
modeName: selectedMode.name,
|
||||||
|
description: selectedMode.description,
|
||||||
|
prompt: selectedMode.prompt
|
||||||
|
};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
// mcp/server.ts
|
// mcp/server.ts
|
||||||
var server = new FastMCP({
|
var server = new FastMCP({
|
||||||
name: "gh-pullfrog",
|
name: "gh-pullfrog",
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
});
|
});
|
||||||
addTools(server, [
|
addTools(server, [
|
||||||
|
SelectModeTool,
|
||||||
CreateCommentTool,
|
CreateCommentTool,
|
||||||
EditCommentTool,
|
EditCommentTool,
|
||||||
CreateWorkingCommentTool,
|
CreateWorkingCommentTool,
|
||||||
|
|||||||
+4
-21
@@ -2,9 +2,9 @@
|
|||||||
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { McpServerConfig, McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import { fromHere } from "@ark/fs";
|
import { fromHere } from "@ark/fs";
|
||||||
import { log } from "../utils/cli.ts";
|
import type { Mode } from "../modes.ts";
|
||||||
import { parseRepoContext } from "../utils/github.ts";
|
import { parseRepoContext } from "../utils/github.ts";
|
||||||
import { ghPullfrogMcpName } from "./index.ts";
|
import { ghPullfrogMcpName } from "./index.ts";
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export type McpName = typeof ghPullfrogMcpName;
|
|||||||
|
|
||||||
export type McpConfigs = Record<McpName, McpStdioServerConfig>;
|
export type McpConfigs = Record<McpName, McpStdioServerConfig>;
|
||||||
|
|
||||||
export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
|
export function createMcpConfigs(githubInstallationToken: string, modes: Mode[]): McpConfigs {
|
||||||
const repoContext = parseRepoContext();
|
const repoContext = parseRepoContext();
|
||||||
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
|
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
|
||||||
|
|
||||||
@@ -27,25 +27,8 @@ export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
|
|||||||
env: {
|
env: {
|
||||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||||
GITHUB_REPOSITORY: githubRepository,
|
GITHUB_REPOSITORY: githubRepository,
|
||||||
|
PULLFROG_MODES: JSON.stringify(modes),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterate through MCP servers and call the provided handler for each stdio server
|
|
||||||
* Shared logic to avoid duplication across agents
|
|
||||||
*/
|
|
||||||
export function forEachStdioMcpServer(
|
|
||||||
mcpServers: Record<string, McpServerConfig>,
|
|
||||||
handler: (serverName: string, serverConfig: McpStdioServerConfig) => void
|
|
||||||
): void {
|
|
||||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
||||||
// Only configure stdio servers (CLIs support stdio MCP servers)
|
|
||||||
if (!("command" in serverConfig)) {
|
|
||||||
log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
handler(serverName, serverConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { type } from "arktype";
|
||||||
|
import type { Mode } from "../modes.ts";
|
||||||
|
import { contextualize, tool } from "./shared.ts";
|
||||||
|
|
||||||
|
// Get modes from environment variable (set by createMcpConfigs)
|
||||||
|
function getModes(): Mode[] {
|
||||||
|
const modesJson = process.env.PULLFROG_MODES;
|
||||||
|
if (modesJson) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(modesJson);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectMode = type({
|
||||||
|
modeName: type.string.describe(
|
||||||
|
"the name of the mode to select (e.g., 'Plan', 'Build', 'Review', 'Prompt')"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SelectModeTool = tool({
|
||||||
|
name: "select_mode",
|
||||||
|
description:
|
||||||
|
"Select a mode and get its detailed prompt instructions. Call this first to determine which mode to use based on the request.",
|
||||||
|
parameters: SelectMode,
|
||||||
|
execute: contextualize(async ({ modeName }) => {
|
||||||
|
const allModes = getModes();
|
||||||
|
|
||||||
|
if (allModes.length === 0) {
|
||||||
|
return {
|
||||||
|
error:
|
||||||
|
"No modes available. Modes must be provided via PULLFROG_MODES environment variable.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedMode = allModes.find((m) => m.name.toLowerCase() === modeName.toLowerCase());
|
||||||
|
|
||||||
|
if (!selectedMode) {
|
||||||
|
const availableModes = allModes.map((m) => m.name).join(", ");
|
||||||
|
return {
|
||||||
|
error: `Mode "${modeName}" not found. Available modes: ${availableModes}`,
|
||||||
|
availableModes: allModes.map((m) => ({ name: m.name, description: m.description })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
modeName: selectedMode.name,
|
||||||
|
description: selectedMode.description,
|
||||||
|
prompt: selectedMode.prompt,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
+2
-2
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Minimal GitHub Issue Comment MCP Server
|
|
||||||
import { FastMCP } from "fastmcp";
|
import { FastMCP } from "fastmcp";
|
||||||
import {
|
import {
|
||||||
CreateCommentTool,
|
CreateCommentTool,
|
||||||
@@ -11,6 +9,7 @@ import { IssueTool } from "./issue.ts";
|
|||||||
import { PullRequestTool } from "./pr.ts";
|
import { PullRequestTool } from "./pr.ts";
|
||||||
import { PullRequestInfoTool } from "./prInfo.ts";
|
import { PullRequestInfoTool } from "./prInfo.ts";
|
||||||
import { ReviewTool } from "./review.ts";
|
import { ReviewTool } from "./review.ts";
|
||||||
|
import { SelectModeTool } from "./selectMode.ts";
|
||||||
import { addTools } from "./shared.ts";
|
import { addTools } from "./shared.ts";
|
||||||
|
|
||||||
const server = new FastMCP({
|
const server = new FastMCP({
|
||||||
@@ -19,6 +18,7 @@ const server = new FastMCP({
|
|||||||
});
|
});
|
||||||
|
|
||||||
addTools(server, [
|
addTools(server, [
|
||||||
|
SelectModeTool,
|
||||||
CreateCommentTool,
|
CreateCommentTool,
|
||||||
EditCommentTool,
|
EditCommentTool,
|
||||||
CreateWorkingCommentTool,
|
CreateWorkingCommentTool,
|
||||||
|
|||||||
+15
-48
@@ -1,9 +1,8 @@
|
|||||||
import { appendFileSync } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { cached } from "@ark/util";
|
import { cached } from "@ark/util";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
||||||
import type { FastMCP, Tool } from "fastmcp";
|
import type { FastMCP, Tool } from "fastmcp";
|
||||||
|
import { log } from "../utils/cli.ts";
|
||||||
import { parseRepoContext, type RepoContext } from "../utils/github.ts";
|
import { parseRepoContext, type RepoContext } from "../utils/github.ts";
|
||||||
|
|
||||||
export interface ToolResult {
|
export interface ToolResult {
|
||||||
@@ -32,49 +31,6 @@ export interface McpContext extends RepoContext {
|
|||||||
octokit: Octokit;
|
octokit: Octokit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the log file path
|
|
||||||
*/
|
|
||||||
function getLogPath(): string {
|
|
||||||
return join(process.cwd(), "log.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log MCP tool call information to log.txt
|
|
||||||
*/
|
|
||||||
function logToolCall({
|
|
||||||
toolName,
|
|
||||||
request,
|
|
||||||
error,
|
|
||||||
success,
|
|
||||||
}: {
|
|
||||||
toolName: string;
|
|
||||||
request: unknown;
|
|
||||||
error?: unknown;
|
|
||||||
success?: boolean;
|
|
||||||
}) {
|
|
||||||
const logPath = getLogPath();
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const requestStr = JSON.stringify(request, null, 2);
|
|
||||||
|
|
||||||
let logEntry = `[${timestamp}] Tool: ${toolName}\nRequest: ${requestStr}\n`;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
||||||
logEntry += `Error: ${errorMessage}\n`;
|
|
||||||
if (errorStack) {
|
|
||||||
logEntry += `Stack: ${errorStack}\n`;
|
|
||||||
}
|
|
||||||
logEntry += `Status: FAILED\n`;
|
|
||||||
} else if (success !== undefined) {
|
|
||||||
logEntry += `Status: ${success ? "SUCCESS" : "FAILED"}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
logEntry += `${"=".repeat(80)}\n\n`;
|
|
||||||
appendFileSync(logPath, logEntry, "utf-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => {
|
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => {
|
||||||
// Wrap the execute function to add logging with the tool name
|
// Wrap the execute function to add logging with the tool name
|
||||||
const toolName = toolDef.name;
|
const toolName = toolDef.name;
|
||||||
@@ -82,15 +38,26 @@ export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>)
|
|||||||
|
|
||||||
toolDef.execute = async (args: params, context: any) => {
|
toolDef.execute = async (args: params, context: any) => {
|
||||||
try {
|
try {
|
||||||
logToolCall({ toolName, request: args });
|
|
||||||
const result = await originalExecute(args, context);
|
const result = await originalExecute(args, context);
|
||||||
// Check if result is a ToolResult with isError property
|
// Check if result is a ToolResult with isError property
|
||||||
const isError =
|
const isError =
|
||||||
result && typeof result === "object" && "isError" in result && result.isError === true;
|
result && typeof result === "object" && "isError" in result && result.isError === true;
|
||||||
logToolCall({ toolName, request: args, success: !isError });
|
const resultData =
|
||||||
|
result && typeof result === "object" && "content" in result
|
||||||
|
? (result as ToolResult).content[0]?.text
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (isError && resultData) {
|
||||||
|
log.toolCall({ toolName, request: args, error: resultData });
|
||||||
|
} else if (resultData) {
|
||||||
|
log.toolCall({ toolName, request: args, result: resultData });
|
||||||
|
} else {
|
||||||
|
log.toolCall({ toolName, request: args });
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logToolCall({ toolName, request: args, error });
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
log.toolCall({ toolName, request: args, error: errorMessage });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.107",
|
"version": "0.0.108",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
[] gemini installation speed (bundle/esm.sh?) (TODO: SHAWN)
|
[] gemini installation speed (bundle/esm.sh?) (TODO: SHAWN)
|
||||||
[] handle defaulting agent name value (TODO: SHAWN)
|
[] handle defaulting agent name value (TODO: SHAWN)
|
||||||
|
|
||||||
[] split up prompts, load dynamically based on mode
|
|
||||||
[] log.txt to stdout
|
|
||||||
|
|
||||||
[] entry.js
|
|
||||||
[] test agent/mode combinations
|
[] test agent/mode combinations
|
||||||
[] test if home directory mcp.json works if mcp.json is specified in repo
|
[] test if home directory mcp.json works if mcp.json is specified in repo
|
||||||
[] add footer to the working comment ("executed by {agent}", link to pullfrog (homepage) w/ small logo?, feedback (create github issue), link to workflow run)- see https://github.com/colinhacks/zod/issues/5459#issuecomment-3548382991
|
[] add footer to the working comment ("executed by {agent}", link to pullfrog (homepage) w/ small logo?, feedback (create github issue), link to workflow run)- see https://github.com/colinhacks/zod/issues/5459#issuecomment-3548382991
|
||||||
@@ -33,3 +29,6 @@
|
|||||||
[x] handle progressive comment updating from pullfrog mcp
|
[x] handle progressive comment updating from pullfrog mcp
|
||||||
[x] jules/gemini support
|
[x] jules/gemini support
|
||||||
[x] standardize mcp server
|
[x] standardize mcp server
|
||||||
|
[x] entry.js
|
||||||
|
[x] split up prompts, load dynamically based on mode
|
||||||
|
[x] log.txt to stdout
|
||||||
+114
-2
@@ -2,9 +2,10 @@
|
|||||||
* CLI output utilities that work well in both local and GitHub Actions environments
|
* CLI output utilities that work well in both local and GitHub Actions environments
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { appendFileSync, existsSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { spawnSync } from "child_process";
|
|
||||||
import { existsSync } from "fs";
|
|
||||||
|
|
||||||
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
||||||
const isDebugEnabled = process.env.LOG_LEVEL === "debug";
|
const isDebugEnabled = process.env.LOG_LEVEL === "debug";
|
||||||
@@ -263,8 +264,119 @@ export const log = {
|
|||||||
* End a collapsed group
|
* End a collapsed group
|
||||||
*/
|
*/
|
||||||
endGroup,
|
endGroup,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log MCP tool call information to mcpLog.txt in the temp directory
|
||||||
|
*/
|
||||||
|
toolCall: ({
|
||||||
|
toolName,
|
||||||
|
request,
|
||||||
|
result,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
toolName: string;
|
||||||
|
request: unknown;
|
||||||
|
result?: string;
|
||||||
|
error?: string;
|
||||||
|
}): void => {
|
||||||
|
const logPath = getMcpLogPath();
|
||||||
|
const params: Parameters<typeof formatToolCall>[0] = { toolName, request };
|
||||||
|
if (error) {
|
||||||
|
params.error = error;
|
||||||
|
} else if (result) {
|
||||||
|
params.result = result;
|
||||||
|
}
|
||||||
|
const logEntry = formatToolCall(params);
|
||||||
|
appendFileSync(logPath, logEntry, "utf-8");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the MCP log file in the temp directory
|
||||||
|
*/
|
||||||
|
function getMcpLogPath(): string {
|
||||||
|
const tempDir = process.env.PULLFROG_TEMP_DIR!;
|
||||||
|
return join(tempDir, "mcpLog.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a value as JSON, using compact format for simple values and pretty-printed for complex ones
|
||||||
|
*/
|
||||||
|
function formatJsonValue(value: unknown): string {
|
||||||
|
const compact = JSON.stringify(value);
|
||||||
|
return compact.length > 80 || compact.includes("\n") ? JSON.stringify(value, null, 2) : compact;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a multi-line string with proper indentation for tool call output
|
||||||
|
* First line has the label, subsequent lines are indented 4 spaces
|
||||||
|
*/
|
||||||
|
function formatIndentedField(label: string, content: string): string {
|
||||||
|
if (!content.includes("\n")) {
|
||||||
|
return ` ${label}: ${content}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = content.split("\n");
|
||||||
|
let formatted = ` ${label}: ${lines[0]}\n`;
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
formatted += ` ${lines[i]}\n`;
|
||||||
|
}
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the input field for a tool call
|
||||||
|
*/
|
||||||
|
function formatToolInput(request: unknown): string {
|
||||||
|
const requestFormatted = formatJsonValue(request);
|
||||||
|
if (requestFormatted === "{}") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return formatIndentedField("input", requestFormatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the result field for a tool call, parsing JSON if possible
|
||||||
|
*/
|
||||||
|
function formatToolResult(result: string): string {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
const formatted = formatJsonValue(parsed);
|
||||||
|
return formatIndentedField("result", formatted);
|
||||||
|
} catch {
|
||||||
|
// Not JSON, display as-is
|
||||||
|
return formatIndentedField("result", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a complete tool call entry with tool name, input, result, and error
|
||||||
|
*/
|
||||||
|
function formatToolCall({
|
||||||
|
toolName,
|
||||||
|
request,
|
||||||
|
result,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
toolName: string;
|
||||||
|
request: unknown;
|
||||||
|
result?: string;
|
||||||
|
error?: string;
|
||||||
|
}): string {
|
||||||
|
let logEntry = `→ ${toolName}\n`;
|
||||||
|
|
||||||
|
logEntry += formatToolInput(request);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logEntry += formatIndentedField("error", error);
|
||||||
|
} else if (result) {
|
||||||
|
logEntry += formatToolResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logEntry += "\n";
|
||||||
|
return logEntry;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds a CLI executable path by checking if it's installed globally
|
* Finds a CLI executable path by checking if it's installed globally
|
||||||
* @param name The name of the CLI executable to find
|
* @param name The name of the CLI executable to find
|
||||||
|
|||||||
Reference in New Issue
Block a user