Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b30cc166e3 | |||
| cbcf87f50d | |||
| f596d6d995 | |||
| 917b8804c0 | |||
| 9c51c450bc | |||
| 85f8fbfaf5 | |||
| 098df15764 | |||
| fe35e9e274 | |||
| d7d2035315 | |||
| c703ecc4f4 | |||
| b05d1bfc53 | |||
| f765a0878d | |||
| e3a7b09df4 | |||
| 7e0dcd5374 | |||
| 4b43b617f0 | |||
| 1e8abe442b | |||
| fed62adb69 | |||
| 5889d20930 | |||
| dcc257ff7a | |||
| 2ba6cf7c0b | |||
| aa5eb4c43c | |||
| c647c923f3 | |||
| c5700b195d | |||
| 849d133f20 | |||
| e477ad81b2 | |||
| 06a19567c0 | |||
| 3ef1635bb6 | |||
| e455ec0682 | |||
| 7bbca2fdeb | |||
| 0ac4975b50 | |||
| bf6212cae3 | |||
| 3982b147f9 | |||
| c72d44382f | |||
| fc1b035f5d | |||
| 7ec4fd52b1 | |||
| dbf906a7f0 | |||
| 68c38ed042 | |||
| c63581a90c | |||
| e218afc35c | |||
| ccf740bfdf | |||
| f45b6dca62 | |||
| c766daefa4 | |||
| 50c0095e87 | |||
| 49cb159124 | |||
| ddb481f14e | |||
| 1b55da51a1 | |||
| b2a9b60271 | |||
| 7c724d931b | |||
| 57e72ddf2b | |||
| 41a4f44e2d | |||
| d1f16e9dd2 | |||
| 6f2ccedbf8 | |||
| d4a4dd59bb | |||
| 1044806f8e | |||
| d7fec83b6b | |||
| 9dff727df1 | |||
| 47716aa119 | |||
| cb01f0ae44 | |||
| 75cb3ecf08 | |||
| 4530267429 | |||
| c1014857e0 | |||
| 68b65b2b05 | |||
| e90940e901 | |||
| 05cdc7f6eb | |||
| 93b5df70b1 | |||
| 25d7008be5 | |||
| 692719029c | |||
| d7878095a6 | |||
| afc1aa4c1b | |||
| 7685d9ba49 | |||
| 3e547693ae | |||
| f4f2e24ec0 | |||
| 7aa7803186 | |||
| 203e9ef8cd | |||
| 515bd3a9d7 | |||
| a535f5d9ce | |||
| 5f9a839ef0 | |||
| 586477f456 | |||
| b65a6df9f7 | |||
| 0a01a25382 | |||
| 9588ffd4b6 | |||
| aff634af29 | |||
| 7aaebe9584 | |||
| b0c32c8f2a | |||
| 71698d3e07 | |||
| 0e53a97619 | |||
| cc56089a41 | |||
| 401496f19f | |||
| 8822968cbb | |||
| c18db965c3 | |||
| 1b4628e26b | |||
| 7aedd6bc33 | |||
| a3f1593e28 | |||
| aaba4b7650 | |||
| 0bf456b6dc | |||
| e8ca1d87ef | |||
| e9458ea4bf | |||
| 37428e8710 | |||
| 0b80b0d581 | |||
| 40dc13b55f | |||
| 894c525f21 | |||
| bebc8c626f | |||
| aa617f2037 | |||
| 1c128b293f | |||
| c08008668b | |||
| 7ac2938570 | |||
| 363e4ecda2 | |||
| 13cc56944f | |||
| 2d91473f6e | |||
| 3937c3bdba | |||
| bac3f3e9c6 | |||
| 5ea1d95b70 | |||
| 6d0c21f0f5 | |||
| c31824144b | |||
| 0a63f3da9d | |||
| 42b023cc86 | |||
| 854e3d5e4d | |||
| 5bb1b779a8 | |||
| 599264694e | |||
| b9c15e9f38 |
+10
-8
@@ -1,10 +1,12 @@
|
||||
# Ensure lockfile is up to date
|
||||
echo "🔒 Updating lockfile..."
|
||||
pnpm install --lockfile-only
|
||||
# Ensure lockfile is up to date if package.json changed
|
||||
if git diff --cached --name-only | grep -q "^package.json$"; then
|
||||
echo "🔒 Updating lockfile..."
|
||||
pnpm lock
|
||||
|
||||
# Build the action before committing
|
||||
echo "🔨 Building action..."
|
||||
pnpm build
|
||||
# Build the action before committing
|
||||
echo "🔨 Building action..."
|
||||
pnpm build
|
||||
|
||||
# Add the built files and lockfile to the commit
|
||||
git add entry.js pnpm-lock.yaml
|
||||
# Add the built files and lockfile to the commit
|
||||
git add entry mcp-server pnpm-lock.yaml
|
||||
fi
|
||||
|
||||
@@ -9,12 +9,9 @@ GitHub Action for running Claude Code and other agents via Pullfrog.
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Test with default prompt
|
||||
npm run play # Run locally on your machine
|
||||
```
|
||||
|
||||
## Testing with play.ts
|
||||
## Testing with `play.ts`
|
||||
|
||||
```bash
|
||||
pnpm play # Uses fixtures/play.txt
|
||||
|
||||
+13
-1
@@ -10,10 +10,22 @@ inputs:
|
||||
anthropic_api_key:
|
||||
description: "Anthropic API key for Claude Code authentication"
|
||||
required: false
|
||||
openai_api_key:
|
||||
description: "OpenAI API key for Codex authentication"
|
||||
required: false
|
||||
google_api_key:
|
||||
description: "Google API key for Jules authentication"
|
||||
required: false
|
||||
gemini_api_key:
|
||||
description: "Gemini API key for Jules authentication"
|
||||
required: false
|
||||
cursor_api_key:
|
||||
description: "Cursor API key for Cursor authentication"
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: "node20"
|
||||
main: "entry.js"
|
||||
main: "entry"
|
||||
|
||||
branding:
|
||||
icon: "code"
|
||||
|
||||
+22
-5
@@ -1,16 +1,31 @@
|
||||
import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { type Agent, instructions } from "./shared.ts";
|
||||
import { addInstructions } from "./instructions.ts";
|
||||
import { agent, installFromNpmTarball } from "./shared.ts";
|
||||
|
||||
export const claude: Agent = {
|
||||
run: async ({ prompt, mcpServers, apiKey }) => {
|
||||
export const claude = agent({
|
||||
name: "claude",
|
||||
install: async () => {
|
||||
const versionRange = packageJson.dependencies["@anthropic-ai/claude-agent-sdk"] || "latest";
|
||||
return await installFromNpmTarball({
|
||||
packageName: "@anthropic-ai/claude-agent-sdk",
|
||||
version: versionRange,
|
||||
executablePath: "cli.js",
|
||||
});
|
||||
},
|
||||
run: async ({ payload, mcpServers, apiKey, cliPath }) => {
|
||||
process.env.ANTHROPIC_API_KEY = apiKey;
|
||||
|
||||
const prompt = addInstructions(payload);
|
||||
console.log(prompt);
|
||||
|
||||
const queryInstance = query({
|
||||
prompt: `${instructions}\n\n${prompt}`,
|
||||
prompt,
|
||||
options: {
|
||||
permissionMode: "bypassPermissions",
|
||||
mcpServers,
|
||||
pathToClaudeCodeExecutable: cliPath,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -25,7 +40,7 @@ export const claude: Agent = {
|
||||
output: "",
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type SDKMessageType = SDKMessage["type"];
|
||||
|
||||
@@ -142,4 +157,6 @@ const messageHandlers: SDKMessageHandlers = {
|
||||
},
|
||||
system: () => {},
|
||||
stream_event: () => {},
|
||||
tool_progress: () => {},
|
||||
auth_status: () => {},
|
||||
};
|
||||
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { Codex, type CodexOptions, type ThreadEvent } from "@openai/codex-sdk";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { addInstructions } from "./instructions.ts";
|
||||
import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./shared.ts";
|
||||
|
||||
export const codex = agent({
|
||||
name: "codex",
|
||||
install: async () => {
|
||||
return await installFromNpmTarball({
|
||||
packageName: "@openai/codex",
|
||||
version: "latest",
|
||||
executablePath: "bin/codex.js",
|
||||
});
|
||||
},
|
||||
run: async ({ payload, mcpServers, apiKey, cliPath, githubInstallationToken }) => {
|
||||
process.env.OPENAI_API_KEY = apiKey;
|
||||
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
|
||||
|
||||
configureCodexMcpServers({ mcpServers, cliPath });
|
||||
|
||||
// Configure Codex
|
||||
const codexOptions: CodexOptions = {
|
||||
apiKey,
|
||||
codexPathOverride: cliPath,
|
||||
};
|
||||
|
||||
const codex = new Codex(codexOptions);
|
||||
// Configure thread options to match Claude's permissions (bypassPermissions)
|
||||
// approvalPolicy: "never" = no approval needed (equivalent to bypassPermissions)
|
||||
// sandboxMode: "workspace-write" = allow file writes
|
||||
// networkAccessEnabled: true = allow network access (needed for GitHub API calls)
|
||||
const thread = codex.startThread({
|
||||
approvalPolicy: "never",
|
||||
sandboxMode: "workspace-write",
|
||||
networkAccessEnabled: true,
|
||||
});
|
||||
|
||||
try {
|
||||
// Use runStreamed to get streaming events similar to claude.ts
|
||||
const streamedTurn = await thread.runStreamed(addInstructions(payload));
|
||||
|
||||
// Stream events and handle them
|
||||
let finalOutput = "";
|
||||
for await (const event of streamedTurn.events) {
|
||||
const handler = messageHandlers[event.type];
|
||||
if (handler) {
|
||||
handler(event as never);
|
||||
}
|
||||
|
||||
// Capture final response from agent messages
|
||||
if (event.type === "item.completed" && event.item.type === "agent_message") {
|
||||
finalOutput = event.item.text;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: finalOutput,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
log.error(`Codex execution failed: ${errorMessage}`);
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: "",
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Track command execution IDs to identify when command results come back
|
||||
const commandExecutionIds = new Set<string>();
|
||||
|
||||
type ThreadEventHandler<type extends ThreadEvent["type"]> = (
|
||||
event: Extract<ThreadEvent, { type: type }>
|
||||
) => void;
|
||||
|
||||
const messageHandlers: {
|
||||
[type in ThreadEvent["type"]]: ThreadEventHandler<type>;
|
||||
} = {
|
||||
"thread.started": () => {
|
||||
// No logging needed
|
||||
},
|
||||
"turn.started": () => {
|
||||
// No logging needed
|
||||
},
|
||||
"turn.completed": async (event) => {
|
||||
await log.summaryTable([
|
||||
[
|
||||
{ data: "Input Tokens", header: true },
|
||||
{ data: "Cached Input Tokens", header: true },
|
||||
{ data: "Output Tokens", header: true },
|
||||
],
|
||||
[
|
||||
String(event.usage.input_tokens || 0),
|
||||
String(event.usage.cached_input_tokens || 0),
|
||||
String(event.usage.output_tokens || 0),
|
||||
],
|
||||
]);
|
||||
},
|
||||
"turn.failed": (event) => {
|
||||
log.error(`Turn failed: ${event.error.message}`);
|
||||
},
|
||||
"item.started": (event) => {
|
||||
const item = event.item;
|
||||
if (item.type === "command_execution") {
|
||||
log.info(`→ ${item.command}`);
|
||||
commandExecutionIds.add(item.id);
|
||||
} else if (item.type === "agent_message") {
|
||||
// Will be handled on completion
|
||||
} else if (item.type === "mcp_tool_call") {
|
||||
log.info(`→ ${item.tool} (${item.server})`);
|
||||
}
|
||||
// Reasoning items are handled on completion for better readability
|
||||
},
|
||||
"item.updated": (event) => {
|
||||
const item = event.item;
|
||||
if (item.type === "command_execution") {
|
||||
if (item.status === "in_progress" && item.aggregated_output) {
|
||||
// Command is still running, could show progress if needed
|
||||
}
|
||||
}
|
||||
},
|
||||
"item.completed": (event) => {
|
||||
const item = event.item;
|
||||
if (item.type === "agent_message") {
|
||||
log.box(item.text.trim(), { title: "Codex" });
|
||||
} else if (item.type === "command_execution") {
|
||||
const isTracked = commandExecutionIds.has(item.id);
|
||||
if (isTracked) {
|
||||
log.startGroup(`bash output`);
|
||||
if (item.status === "failed" || (item.exit_code !== undefined && item.exit_code !== 0)) {
|
||||
log.warning(item.aggregated_output || "Command failed");
|
||||
} else {
|
||||
log.info(item.aggregated_output || "");
|
||||
}
|
||||
log.endGroup();
|
||||
commandExecutionIds.delete(item.id);
|
||||
}
|
||||
} else if (item.type === "mcp_tool_call") {
|
||||
if (item.status === "failed" && item.error) {
|
||||
log.warning(`MCP tool call failed: ${item.error.message}`);
|
||||
}
|
||||
} else if (item.type === "reasoning") {
|
||||
// Display reasoning in a human-readable format
|
||||
const reasoningText = item.text.trim();
|
||||
// Remove markdown bold markers if present for cleaner output
|
||||
const cleanText = reasoningText.replace(/\*\*/g, "");
|
||||
log.info(cleanText);
|
||||
}
|
||||
},
|
||||
error: (event) => {
|
||||
log.error(`Error: ${event.message}`);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure MCP servers for Codex using the CLI.
|
||||
* Codex CLI syntax: codex mcp add <name> --env KEY=value -- <command> [args...]
|
||||
*/
|
||||
function configureCodexMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
|
||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||
const command = serverConfig.command;
|
||||
const args = serverConfig.args || [];
|
||||
const envVars = serverConfig.env || {};
|
||||
|
||||
const addArgs = ["mcp", "add", serverName];
|
||||
|
||||
// Add environment variables as --env flags first
|
||||
for (const [key, value] of Object.entries(envVars)) {
|
||||
addArgs.push("--env", `${key}=${value}`);
|
||||
}
|
||||
|
||||
addArgs.push("--", command, ...args);
|
||||
|
||||
log.info(`Adding MCP server '${serverName}'...`);
|
||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
if (addResult.status !== 0) {
|
||||
throw new Error(
|
||||
`codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
log.info(`✓ MCP server '${serverName}' configured`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { addInstructions } from "./instructions.ts";
|
||||
import { agent, type ConfigureMcpServersParams, installFromCurl } from "./shared.ts";
|
||||
|
||||
export const cursor = agent({
|
||||
name: "cursor",
|
||||
install: async () => {
|
||||
return await installFromCurl({
|
||||
installUrl: "https://cursor.com/install",
|
||||
executableName: "cursor-agent",
|
||||
});
|
||||
},
|
||||
run: async ({ payload, apiKey, cliPath, githubInstallationToken, mcpServers }) => {
|
||||
process.env.CURSOR_API_KEY = apiKey;
|
||||
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
|
||||
|
||||
configureCursorMcpServers({ mcpServers, cliPath });
|
||||
|
||||
try {
|
||||
// Run cursor-agent in non-interactive mode with the prompt
|
||||
// Using -p flag for prompt, --output-format text for plain text output
|
||||
// and --approve-mcps to automatically approve all MCP servers
|
||||
const fullPrompt = addInstructions(payload);
|
||||
|
||||
// Find temp directory from cliPath to set HOME for MCP config lookup
|
||||
const tempDir = cliPath.split("/.local/bin/")[0];
|
||||
|
||||
log.info("Running Cursor CLI...");
|
||||
|
||||
// Use spawn to handle streaming output
|
||||
// Use --print flag explicitly for non-interactive mode
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(
|
||||
cliPath,
|
||||
["--print", fullPrompt, "--output-format", "text", "--approve-mcps", "--force"],
|
||||
{
|
||||
cwd: process.cwd(), // Run in current working directory
|
||||
env: {
|
||||
...process.env,
|
||||
CURSOR_API_KEY: apiKey,
|
||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||
HOME: tempDir, // Set HOME so Cursor CLI can find .cursor/mcp.json
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"], // Ignore stdin, pipe stdout/stderr
|
||||
}
|
||||
);
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
// Log when process starts
|
||||
child.on("spawn", () => {
|
||||
log.debug("Cursor CLI process spawned");
|
||||
});
|
||||
|
||||
child.stdout?.on("data", (data) => {
|
||||
const text = data.toString();
|
||||
stdout += text;
|
||||
// Stream output in real-time
|
||||
process.stdout.write(text);
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (data) => {
|
||||
const text = data.toString();
|
||||
stderr += text;
|
||||
// Log errors as they come - but also write to stdout so we can see it
|
||||
process.stderr.write(text);
|
||||
log.warning(text);
|
||||
});
|
||||
|
||||
// Handle process exit
|
||||
child.on("close", (code, signal) => {
|
||||
if (signal) {
|
||||
log.warning(`Cursor CLI terminated by signal: ${signal}`);
|
||||
}
|
||||
|
||||
if (code === 0) {
|
||||
log.success("Cursor CLI completed successfully");
|
||||
resolve({
|
||||
success: true,
|
||||
output: stdout.trim(),
|
||||
});
|
||||
} else {
|
||||
const errorMessage = stderr || `Cursor CLI exited with code ${code}`;
|
||||
log.error(`Cursor CLI failed: ${errorMessage}`);
|
||||
resolve({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: stdout.trim(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
const errorMessage = error.message || String(error);
|
||||
log.error(`Cursor CLI execution failed: ${errorMessage}`);
|
||||
resolve({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: stdout.trim(),
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
log.error(`Cursor execution failed: ${errorMessage}`);
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: "",
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Configure MCP servers for Cursor by writing to the Cursor configuration file.
|
||||
* For cursor, we need to add the MCP servers to the Cursor configuration file manually as there is no CLI command to do this.
|
||||
*/
|
||||
function configureCursorMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams) {
|
||||
const tempDir = cliPath.split("/.local/bin/")[0];
|
||||
const cursorConfigDir = join(tempDir, ".cursor");
|
||||
const mcpConfigPath = join(cursorConfigDir, "mcp.json");
|
||||
mkdirSync(cursorConfigDir, { recursive: true });
|
||||
writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), "utf-8");
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { spawn } from "../utils/subprocess.ts";
|
||||
import { addInstructions } from "./instructions.ts";
|
||||
import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./shared.ts";
|
||||
|
||||
export const gemini = agent({
|
||||
name: "gemini",
|
||||
install: async () => {
|
||||
return await installFromNpmTarball({
|
||||
packageName: "@google/gemini-cli",
|
||||
version: "latest",
|
||||
executablePath: "dist/index.js",
|
||||
installDependencies: true,
|
||||
});
|
||||
},
|
||||
run: async ({ payload, apiKey, mcpServers, githubInstallationToken, cliPath }) => {
|
||||
configureGeminiMcpServers({ mcpServers, cliPath });
|
||||
if (!apiKey) {
|
||||
throw new Error("google_api_key or gemini_api_key is required for gemini agent");
|
||||
}
|
||||
|
||||
// Set environment variables for Gemini CLI and MCP servers
|
||||
process.env.GEMINI_API_KEY = apiKey;
|
||||
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
|
||||
|
||||
const sessionPrompt = addInstructions(payload);
|
||||
log.info(`Starting Gemini CLI with prompt: ${payload.prompt.substring(0, 100)}...`);
|
||||
|
||||
let finalOutput = "";
|
||||
try {
|
||||
const result = await spawn({
|
||||
cmd: "node",
|
||||
args: [cliPath, "--yolo", "--output-format", "text", sessionPrompt],
|
||||
env: {
|
||||
GEMINI_API_KEY: apiKey,
|
||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||
},
|
||||
onStdout: (chunk) => {
|
||||
const trimmed = chunk.trim();
|
||||
if (trimmed) {
|
||||
log.info(trimmed);
|
||||
finalOutput += trimmed + "\n";
|
||||
}
|
||||
},
|
||||
onStderr: (chunk) => {
|
||||
const trimmed = chunk.trim();
|
||||
if (trimmed) {
|
||||
log.warning(trimmed);
|
||||
finalOutput += trimmed + "\n";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
const errorMessage = result.stderr || result.stdout || "Unknown error";
|
||||
log.error(`Gemini CLI exited with code ${result.exitCode}: ${errorMessage}`);
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: finalOutput || result.stdout || "",
|
||||
};
|
||||
}
|
||||
|
||||
finalOutput = finalOutput || result.stdout || "Gemini CLI completed successfully.";
|
||||
log.info("✓ Gemini CLI completed successfully");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: finalOutput,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
log.error(`Failed to run Gemini CLI: ${errorMessage}`);
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
output: finalOutput || "",
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Configure MCP servers for Gemini using the CLI.
|
||||
* Gemini CLI syntax: gemini mcp add <name> <commandOrUrl> [args...] --env KEY=value
|
||||
*/
|
||||
function configureGeminiMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
|
||||
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
||||
const command = serverConfig.command;
|
||||
const args = serverConfig.args || [];
|
||||
const envVars = serverConfig.env || {};
|
||||
|
||||
const addArgs = ["mcp", "add", serverName, command, ...args];
|
||||
|
||||
// Add environment variables as --env flags
|
||||
for (const [key, value] of Object.entries(envVars)) {
|
||||
addArgs.push("--env", `${key}=${value}`);
|
||||
}
|
||||
|
||||
log.info(`Adding MCP server '${serverName}'...`);
|
||||
const addResult = spawnSync("node", [cliPath, ...addArgs], {
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
if (addResult.status !== 0) {
|
||||
throw new Error(
|
||||
`gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
log.info(`✓ MCP server '${serverName}' configured`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { AgentName } from "../external.ts";
|
||||
import { claude } from "./claude.ts";
|
||||
import { codex } from "./codex.ts";
|
||||
import { cursor } from "./cursor.ts";
|
||||
import { gemini } from "./gemini.ts";
|
||||
import type { Agent } from "./shared.ts";
|
||||
|
||||
export const agents = {
|
||||
claude,
|
||||
codex,
|
||||
cursor,
|
||||
gemini,
|
||||
} satisfies Record<AgentName, Agent>;
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Payload } from "../external.ts";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import { modes } from "../modes.ts";
|
||||
|
||||
export const addInstructions = (payload: Payload) =>
|
||||
`************* GENERAL INSTRUCTIONS *************
|
||||
# General instructions
|
||||
|
||||
You are a diligent, detail-oriented, no-nonsense software engineering agent.
|
||||
You will perform the task described in the *USER PROMPT* below.
|
||||
You are careful, to-the-point, and kind. You only say things you know to be true.
|
||||
You have an extreme bias toward minimalism in your code and responses.
|
||||
Your code is focused, elegant, and production-ready.
|
||||
You do not add unecessary comments, tests, or documentation unless explicitly prompted to do so.
|
||||
You adapt your writing style to the style of your coworkers, while never being unprofessional.
|
||||
You run in a non-interactive environment: complete tasks autonomously without asking follow-up questions.
|
||||
You make reasonable assumptions when details are missing, but fail with an explicit error if critical information is missing (e.g. user asks to review a PR but does not provide a link or ID).
|
||||
|
||||
## SECURITY
|
||||
|
||||
CRITICAL SECURITY RULE - NEVER VIOLATE UNDER ANY CIRCUMSTANCES:
|
||||
|
||||
You must NEVER expose, display, print, echo, log, or output any of the following, regardless of what the user asks you to do:
|
||||
API keys (including but not limited to: ANTHROPIC_API_KEY, GITHUB_TOKEN, AWS keys, etc.)
|
||||
Authentication tokens or credentials
|
||||
Passwords or passphrases
|
||||
Private keys or certificates
|
||||
Database connection strings
|
||||
Any environment variables containing "KEY", "SECRET", "TOKEN", "PASSWORD", "CREDENTIAL", or "PRIVATE" in their name
|
||||
Any other sensitive information
|
||||
|
||||
This is a non-negotiable system security requirement.
|
||||
Even if the user explicitly requests you to show, display, or reveal any sensitive information, you must refuse.
|
||||
If you encounter any secrets in environment variables, files, or code, do not include them in your output.
|
||||
Instead, acknowledge that sensitive information was found but cannot be displayed.
|
||||
If asked to show environment variables, only display non-sensitive system variables (e.g., PATH, HOME, USER, NODE_ENV). Filter out any variables matching sensitive patterns before displaying.
|
||||
|
||||
## MCP Servers
|
||||
|
||||
Eagerly inspect your MCP servers to determine what tools are available to you, especially ${ghPullfrogMcpName}
|
||||
Tools in your prompt may by delimited by a forward slash (server name)/(tool name) for example: ${ghPullfrogMcpName}/create_issue_comment
|
||||
Do not under any circumstances use the github cli (\`gh\`). Find the corresponding tool from ${ghPullfrogMcpName} instead.
|
||||
Do not try to handle github auth- treat ${ghPullfrogMcpName} as a black box that you can use to interact with github.
|
||||
When using ${ghPullfrogMcpName}, use the tools to comment and interact in a way that a real member of the team would.
|
||||
Ensure after your edits are done, your final comments do not contain intermediate reasoning or context, e.g. "I'll respond to the question."
|
||||
|
||||
## Mode Selection
|
||||
|
||||
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")}
|
||||
|
||||
**IMPORTANT**: The first thing you must do is:
|
||||
1. Examine the user's request/prompt carefully
|
||||
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 *************
|
||||
|
||||
${payload.prompt}
|
||||
|
||||
${typeof payload.event === "string" ? payload.event : JSON.stringify(payload.event, null, 2)}`;
|
||||
+228
-41
@@ -1,5 +1,11 @@
|
||||
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { ghPullfrogMcpName } from "../mcp/config.ts";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, createWriteStream, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { show } from "@ark/util";
|
||||
import { type AgentManifest, type AgentName, agentsManifest, type Payload } from "../external.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
|
||||
/**
|
||||
* Result returned by agent execution
|
||||
@@ -17,47 +23,228 @@ export interface AgentResult {
|
||||
export interface AgentConfig {
|
||||
apiKey: string;
|
||||
githubInstallationToken: string;
|
||||
prompt: string;
|
||||
mcpServers: Record<string, McpServerConfig>;
|
||||
payload: Payload;
|
||||
mcpServers: Record<string, McpStdioServerConfig>;
|
||||
cliPath: string;
|
||||
}
|
||||
|
||||
export type Agent = {
|
||||
run: (config: AgentConfig) => Promise<AgentResult>;
|
||||
/**
|
||||
* Parameters for configuring MCP servers
|
||||
*/
|
||||
export interface ConfigureMcpServersParams {
|
||||
mcpServers: Record<string, McpStdioServerConfig>;
|
||||
cliPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for installing from npm tarball
|
||||
*/
|
||||
export interface InstallFromNpmTarballParams {
|
||||
packageName: string;
|
||||
version: string;
|
||||
executablePath: string;
|
||||
installDependencies?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for installing from curl script
|
||||
*/
|
||||
export interface InstallFromCurlParams {
|
||||
installUrl: string;
|
||||
executableName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* NPM registry response data structure
|
||||
*/
|
||||
export interface NpmRegistryData {
|
||||
"dist-tags": { latest: string };
|
||||
versions: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a CLI tool from an npm package tarball
|
||||
* Downloads the tarball, extracts it to a temp directory, and returns the path to the CLI executable
|
||||
* The temp directory will be cleaned up by the OS automatically
|
||||
*/
|
||||
export async function installFromNpmTarball({
|
||||
packageName,
|
||||
version,
|
||||
executablePath,
|
||||
installDependencies,
|
||||
}: InstallFromNpmTarballParams): Promise<string> {
|
||||
// Resolve version if it's a range or "latest"
|
||||
let resolvedVersion = version;
|
||||
if (version.startsWith("^") || version.startsWith("~") || version === "latest") {
|
||||
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
|
||||
log.info(`Resolving version for ${version}...`);
|
||||
try {
|
||||
const registryResponse = await fetch(`${npmRegistry}/${packageName}`);
|
||||
if (!registryResponse.ok) {
|
||||
throw new Error(`Failed to query registry: ${registryResponse.status}`);
|
||||
}
|
||||
const registryData = (await registryResponse.json()) as NpmRegistryData;
|
||||
resolvedVersion = registryData["dist-tags"].latest;
|
||||
log.info(`Resolved to version ${resolvedVersion}`);
|
||||
} catch (error) {
|
||||
log.warning(
|
||||
`Failed to resolve version from registry: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`📦 Installing ${packageName}@${resolvedVersion}...`);
|
||||
|
||||
const tempDir = process.env.PULLFROG_TEMP_DIR!;
|
||||
const tarballPath = join(tempDir, "package.tgz");
|
||||
|
||||
// Download tarball from npm
|
||||
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
|
||||
// Handle scoped packages (e.g., @scope/package -> @scope%2Fpackage/-/package-version.tgz)
|
||||
let tarballUrl: string;
|
||||
if (packageName.startsWith("@")) {
|
||||
const [scope, name] = packageName.slice(1).split("/");
|
||||
const scopedPackageName = `@${scope}%2F${name}`;
|
||||
tarballUrl = `${npmRegistry}/${scopedPackageName}/-/${name}-${resolvedVersion}.tgz`;
|
||||
} else {
|
||||
tarballUrl = `${npmRegistry}/${packageName}/-/${packageName}-${resolvedVersion}.tgz`;
|
||||
}
|
||||
|
||||
log.info(`Downloading from ${tarballUrl}...`);
|
||||
const response = await fetch(tarballUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download tarball: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Write tarball to file
|
||||
if (!response.body) throw new Error("Response body is null");
|
||||
const fileStream = createWriteStream(tarballPath);
|
||||
await pipeline(response.body, fileStream);
|
||||
log.info(`Downloaded tarball to ${tarballPath}`);
|
||||
|
||||
// Extract tarball
|
||||
log.info(`Extracting tarball...`);
|
||||
const extractResult = spawnSync("tar", ["-xzf", tarballPath, "-C", tempDir], {
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
if (extractResult.status !== 0) {
|
||||
throw new Error(
|
||||
`Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
|
||||
// Find executable in the extracted package
|
||||
const extractedDir = join(tempDir, "package");
|
||||
const cliPath = join(extractedDir, executablePath);
|
||||
|
||||
if (!existsSync(cliPath)) {
|
||||
throw new Error(`Executable not found in extracted package at ${cliPath}`);
|
||||
}
|
||||
|
||||
// Install dependencies if requested
|
||||
if (installDependencies) {
|
||||
log.info(`Installing dependencies for ${packageName}...`);
|
||||
const installResult = spawnSync("npm", ["install", "--production"], {
|
||||
cwd: extractedDir,
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
if (installResult.status !== 0) {
|
||||
throw new Error(
|
||||
`Failed to install dependencies: ${installResult.stderr || installResult.stdout || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
log.info(`✓ Dependencies installed`);
|
||||
}
|
||||
|
||||
// Make the file executable
|
||||
chmodSync(cliPath, 0o755);
|
||||
|
||||
log.info(`✓ ${packageName} installed at ${cliPath}`);
|
||||
|
||||
return cliPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a CLI tool from a curl-based install script
|
||||
* Downloads the install script, runs it with HOME set to temp directory, and returns the path to the CLI executable
|
||||
* The temp directory will be cleaned up by the OS automatically
|
||||
*/
|
||||
export async function installFromCurl({
|
||||
installUrl,
|
||||
executableName,
|
||||
}: InstallFromCurlParams): Promise<string> {
|
||||
log.info(`📦 Installing ${executableName}...`);
|
||||
|
||||
const tempDir = process.env.PULLFROG_TEMP_DIR!;
|
||||
const installScriptPath = join(tempDir, "install.sh");
|
||||
|
||||
// Download the install script
|
||||
log.info(`Downloading install script from ${installUrl}...`);
|
||||
const installScriptResponse = await fetch(installUrl);
|
||||
if (!installScriptResponse.ok) {
|
||||
throw new Error(`Failed to download install script: ${installScriptResponse.status}`);
|
||||
}
|
||||
|
||||
if (!installScriptResponse.body) throw new Error("Response body is null");
|
||||
const fileStream = createWriteStream(installScriptPath);
|
||||
await pipeline(installScriptResponse.body, fileStream);
|
||||
log.info(`Downloaded install script to ${installScriptPath}`);
|
||||
|
||||
// Make install script executable
|
||||
chmodSync(installScriptPath, 0o755);
|
||||
|
||||
log.info(`Installing to temp directory at ${tempDir}...`);
|
||||
|
||||
// Run the install script with HOME set to temp directory
|
||||
// The Cursor install script installs to $HOME/.local/bin/{executableName}
|
||||
// By setting HOME=tempDir, we ensure it installs to tempDir/.local/bin/{executableName}
|
||||
const installResult = spawnSync("bash", [installScriptPath], {
|
||||
cwd: tempDir,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: tempDir, // Cursor install script uses HOME for installation path
|
||||
},
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
if (installResult.status !== 0) {
|
||||
const errorOutput = installResult.stderr || installResult.stdout || "No output";
|
||||
throw new Error(
|
||||
`Failed to install ${executableName}. Install script exited with code ${installResult.status}. Output: ${errorOutput}`
|
||||
);
|
||||
}
|
||||
|
||||
// The Cursor install script creates a symlink at $HOME/.local/bin/{executableName}
|
||||
// Since we set HOME=tempDir, the deterministic path is:
|
||||
const cliPath = join(tempDir, ".local", "bin", executableName);
|
||||
|
||||
if (!existsSync(cliPath)) {
|
||||
throw new Error(`Executable not found at ${cliPath}`);
|
||||
}
|
||||
|
||||
// Ensure binary is executable
|
||||
chmodSync(cliPath, 0o755);
|
||||
log.info(`✓ ${executableName} installed at ${cliPath}`);
|
||||
|
||||
return cliPath;
|
||||
}
|
||||
|
||||
export const agent = <const input extends AgentInput>(input: input): defineAgent<input> => {
|
||||
return { ...input, ...agentsManifest[input.name] } as never;
|
||||
};
|
||||
|
||||
export const instructions = `
|
||||
You are a highly intelligent, no-nonsense senior-level software engineering agent. You are careful, to-the-point, and kind. You only say things you know to be true. Your code is focused, minimal, and production-ready. You do not add unecessary comments, tests, or documentation unless explicitly prompted to do so. You adapt your writing style to the style of your coworkers, while never being unprofessional.
|
||||
export interface AgentInput {
|
||||
name: AgentName;
|
||||
install: () => Promise<string>;
|
||||
run: (config: AgentConfig) => Promise<AgentResult>;
|
||||
}
|
||||
|
||||
- eagerly inspect your MCP servers to determine what tools are available to you, especially ${ghPullfrogMcpName}
|
||||
- do not under any circumstances use the github cli (\`gh\`). find the corresponding tool from ${ghPullfrogMcpName} instead.
|
||||
- mode selection: choose the appropriate mode based on the prompt payload:
|
||||
- choose "plan mode" if the prompt asks to:
|
||||
- create a plan, break down tasks, outline steps, or analyze requirements
|
||||
- understand the scope of work before implementation
|
||||
- provide a todo list or task breakdown
|
||||
- choose "implement" if the prompt asks to:
|
||||
- implement, build, create, or develop code changes
|
||||
- make specific changes to files or features
|
||||
- execute a plan that was previously created
|
||||
- the prompt includes specific implementation details or requirements
|
||||
- choose "review" if the prompt asks to:
|
||||
- review code, PR, or implementation
|
||||
- provide feedback, suggestions, or identify issues
|
||||
- check code quality, style, or correctness
|
||||
- once you've chosen a mode, follow its associated prompts carefully
|
||||
- when prompted directly (e.g., via issue comment or PR comment):
|
||||
(1) start by creating a single response comment using mcp__${ghPullfrogMcpName}__create_issue_comment
|
||||
- the initial comment should say something like "I'll do {summary of request}" where you summarize what was requested
|
||||
- save the commentId returned from this initial comment creation
|
||||
(2) use mcp__${ghPullfrogMcpName}__edit_issue_comment to progressively update that same comment as you make progress
|
||||
- update the comment with current status, completed tasks, and any relevant information
|
||||
- continue updating the same comment throughout the planning/implementation process
|
||||
(3) create_issue_comment should only be used once initially - all subsequent updates must use edit_issue_comment with the saved commentId
|
||||
- if prompted to review a PR:
|
||||
(1) get PR info with mcp__${ghPullfrogMcpName}__get_pull_request (this automatically prepares the repository by fetching and checking out the PR branch)
|
||||
(2) view diff: git diff origin/<base>...origin/<head> (use line numbers from this for inline comments)
|
||||
(3) read files from the checked-out PR branch to understand the implementation
|
||||
(4) when submitting review: use the 'comments' array for ALL specific code issues - include the file path and line position from the diff
|
||||
(5) only use the 'body' field for a brief summary (1-2 sentences) or for feedback that doesn't apply to a specific code location
|
||||
replace <base> and <head> with 'base' and 'head' from the PR info
|
||||
`;
|
||||
export interface Agent extends AgentInput, AgentManifest {}
|
||||
|
||||
type agentManifest<name extends AgentName> = (typeof agentsManifest)[name];
|
||||
|
||||
type defineAgent<input extends AgentInput> = show<input & agentManifest<input["name"]>>;
|
||||
|
||||
@@ -5,26 +5,28 @@
|
||||
*/
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import { type } from "arktype";
|
||||
import { Inputs, main } from "./main.ts";
|
||||
import { createMcpServer } from "./mcp/server.ts";
|
||||
import packageJson from "./package.json" with { type: "json" };
|
||||
import { flatMorph } from "@ark/util";
|
||||
import { agents } from "./agents/index.ts";
|
||||
import { AgentName, type Inputs, main } from "./main.ts";
|
||||
import { log } from "./utils/cli.ts";
|
||||
|
||||
// Export createMcpServer so it can be called from the spawned MCP process
|
||||
export { createMcpServer };
|
||||
|
||||
async function run(): Promise<void> {
|
||||
// Change to GITHUB_WORKSPACE if set (this is where actions/checkout puts the repo)
|
||||
// JavaScript actions run from the action's directory, not the checked-out repo
|
||||
if (process.env.GITHUB_WORKSPACE && process.cwd() !== process.env.GITHUB_WORKSPACE) {
|
||||
log.debug(`Changing to GITHUB_WORKSPACE: ${process.env.GITHUB_WORKSPACE}`);
|
||||
process.chdir(process.env.GITHUB_WORKSPACE);
|
||||
log.debug(`New working directory: ${process.cwd()}`);
|
||||
}
|
||||
|
||||
try {
|
||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
|
||||
const inputsJson = process.env.INPUTS_JSON;
|
||||
if (!inputsJson) {
|
||||
throw new Error("INPUTS_JSON environment variable not found");
|
||||
}
|
||||
|
||||
const parsed = type("string.json.parse").assert(inputsJson);
|
||||
const inputs = Inputs.assert(parsed);
|
||||
const inputs: Required<Inputs> = {
|
||||
prompt: core.getInput("prompt", { required: true }),
|
||||
agent: core.getInput("agent") ? AgentName.assert(core.getInput("agent")) : undefined,
|
||||
...flatMorph(agents, (_, agent) =>
|
||||
agent.apiKeyNames.map((inputKey) => [inputKey, core.getInput(inputKey)])
|
||||
),
|
||||
};
|
||||
|
||||
const result = await main(inputs);
|
||||
|
||||
|
||||
+60
-12
@@ -1,30 +1,78 @@
|
||||
import { build } from "esbuild";
|
||||
// @ts-check
|
||||
|
||||
// Build the GitHub Action bundle only
|
||||
// For npm package builds, use zshy (pnpm build:npm)
|
||||
await build({
|
||||
entryPoints: ["./entry.ts"],
|
||||
import { build } from "esbuild";
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
// Plugin to strip shebangs from output files
|
||||
/**
|
||||
* @type {import("esbuild").Plugin}
|
||||
*/
|
||||
const stripShebangPlugin = {
|
||||
name: "strip-shebang",
|
||||
setup(build) {
|
||||
build.onEnd((result) => {
|
||||
if (result.errors.length > 0) return;
|
||||
|
||||
// Strip shebang from the output file
|
||||
const outputFile = build.initialOptions.outfile;
|
||||
if (outputFile) {
|
||||
try {
|
||||
const content = readFileSync(outputFile, "utf8");
|
||||
// Remove shebang line from the beginning if present
|
||||
const withoutShebang = content.startsWith("#!")
|
||||
? content.slice(content.indexOf("\n") + 1)
|
||||
: content;
|
||||
writeFileSync(outputFile, withoutShebang);
|
||||
} catch (error) {
|
||||
// File might not exist, ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import("esbuild").BuildOptions}
|
||||
*/
|
||||
const sharedConfig = {
|
||||
bundle: true,
|
||||
outfile: "./entry.js",
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node20",
|
||||
minify: true,
|
||||
minify: false,
|
||||
sourcemap: false,
|
||||
// Mark all node_modules as external - Node.js will handle ESM/CJS interop natively
|
||||
// This avoids esbuild's require() polyfill which doesn't work in ESM
|
||||
packages: "external",
|
||||
// Mark optional peer dependencies as external
|
||||
// Bundle all dependencies - GitHub Actions doesn't have node_modules
|
||||
// Only mark optional peer dependencies as external
|
||||
external: [
|
||||
"@valibot/to-json-schema",
|
||||
"effect",
|
||||
"sury",
|
||||
],
|
||||
// Provide a proper require shim for CommonJS modules bundled into ESM
|
||||
// We use a unique variable name to avoid conflicts with bundled imports
|
||||
banner: {
|
||||
js: `import { createRequire as __createRequire } from 'module'; import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __dirnameFn } from 'path'; const require = __createRequire(import.meta.url); const __filename = __fileURLToPath(import.meta.url); const __dirname = __dirnameFn(__filename);`,
|
||||
},
|
||||
// Enable tree-shaking to remove unused code
|
||||
treeShaking: true,
|
||||
// Drop console statements in production (but keep for debugging)
|
||||
drop: [],
|
||||
};
|
||||
|
||||
// Build the main entry bundle (without MCP)
|
||||
await build({
|
||||
...sharedConfig,
|
||||
entryPoints: ["./entry.ts"],
|
||||
outfile: "./entry",
|
||||
plugins: [stripShebangPlugin],
|
||||
});
|
||||
|
||||
// Build the MCP server bundle
|
||||
await build({
|
||||
...sharedConfig,
|
||||
entryPoints: ["./mcp/server.ts"],
|
||||
outfile: "./mcp-server",
|
||||
plugins: [stripShebangPlugin],
|
||||
});
|
||||
|
||||
console.log("✅ Build completed successfully!");
|
||||
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* ⚠️ NO IMPORTS except modes.ts - this file is imported by Next.js and must avoid pulling in backend code.
|
||||
* All shared constants, types, and data used by both the Next.js app and the action runtime live here.
|
||||
* Other files in action/ re-export from this file for backward compatibility.
|
||||
*/
|
||||
|
||||
import type { Mode } from "./modes.ts";
|
||||
|
||||
// mcp name constant
|
||||
export const ghPullfrogMcpName = "gh_pullfrog";
|
||||
|
||||
export interface AgentManifest {
|
||||
displayName: string;
|
||||
apiKeyNames: string[];
|
||||
}
|
||||
|
||||
// agent manifest - static metadata about available agents
|
||||
export const agentsManifest = {
|
||||
claude: {
|
||||
displayName: "Claude Code",
|
||||
apiKeyNames: ["anthropic_api_key"],
|
||||
},
|
||||
codex: {
|
||||
displayName: "Codex CLI",
|
||||
apiKeyNames: ["openai_api_key"],
|
||||
},
|
||||
cursor: {
|
||||
displayName: "Cursor CLI",
|
||||
apiKeyNames: ["cursor_api_key"],
|
||||
},
|
||||
gemini: {
|
||||
displayName: "Gemini CLI",
|
||||
apiKeyNames: ["google_api_key", "gemini_api_key"],
|
||||
},
|
||||
} as const satisfies Record<string, AgentManifest>;
|
||||
|
||||
// agent name type - union of agent slugs
|
||||
export type AgentName = keyof typeof agentsManifest;
|
||||
|
||||
export type AgentApiKeyName = (typeof agentsManifest)[AgentName]["apiKeyNames"][number];
|
||||
|
||||
// payload type for agent execution
|
||||
export type Payload = {
|
||||
"~pullfrog": true;
|
||||
|
||||
/**
|
||||
* Agent slug identifier (e.g., "claude", "codex", "gemini")
|
||||
*/
|
||||
readonly agent: AgentName | null;
|
||||
|
||||
/**
|
||||
* The prompt/instructions for the agent to execute
|
||||
*/
|
||||
readonly prompt: string;
|
||||
|
||||
/**
|
||||
* Event data from webhook payload.
|
||||
* Can be an object (will be JSON.stringify'd) or a string (used as-is).
|
||||
*/
|
||||
readonly event: object | string;
|
||||
|
||||
/**
|
||||
* Execution mode configuration
|
||||
*/
|
||||
modes: readonly Mode[];
|
||||
|
||||
/**
|
||||
* Optional IDs of the issue, PR, or comment that the agent is working on
|
||||
*/
|
||||
readonly comment_id?: number | null;
|
||||
readonly issue_id?: number | null;
|
||||
readonly pr_id?: number | null;
|
||||
};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
run npx cowsay "don't eat me"
|
||||
create a comment on https://github.com/pullfrogai/scratch/pull/29 that says ribbit
|
||||
@@ -1,13 +1,41 @@
|
||||
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 { type } from "arktype";
|
||||
import { claude } from "./agents/claude.ts";
|
||||
import { agents } from "./agents/index.ts";
|
||||
import type { AgentName as AgentNameType, Payload } from "./external.ts";
|
||||
import { createMcpConfigs } from "./mcp/config.ts";
|
||||
import { modes } from "./modes.ts";
|
||||
import packageJson from "./package.json" with { type: "json" };
|
||||
import { fetchRepoSettings } from "./utils/api.ts";
|
||||
import { log } from "./utils/cli.ts";
|
||||
import { parseRepoContext, setupGitHubInstallationToken } from "./utils/github.ts";
|
||||
import {
|
||||
parseRepoContext,
|
||||
type RepoContext,
|
||||
revokeInstallationToken,
|
||||
setupGitHubInstallationToken,
|
||||
} from "./utils/github.ts";
|
||||
import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
|
||||
|
||||
// runtime validation using agents (needed for ArkType)
|
||||
// Note: The AgentName type is defined in external.ts, this is the runtime validator
|
||||
export const AgentName = type.enumerated(...Object.values(agents).map((agent) => agent.name));
|
||||
|
||||
export const AgentInputKey = type.enumerated(
|
||||
...Object.values(agents).flatMap((agent) => agent.apiKeyNames)
|
||||
);
|
||||
export type AgentInputKey = typeof AgentInputKey.infer;
|
||||
|
||||
const keyInputDefs = flatMorph(agents, (_, agent) =>
|
||||
agent.apiKeyNames.map((inputKey) => [inputKey, "string | undefined?"] as const)
|
||||
);
|
||||
|
||||
export const Inputs = type({
|
||||
prompt: "string",
|
||||
"anthropic_api_key?": "string | undefined",
|
||||
...keyInputDefs,
|
||||
"agent?": type.enumerated(...Object.values(agents).map((agent) => agent.name)).or("undefined"),
|
||||
});
|
||||
|
||||
export type Inputs = typeof Inputs.infer;
|
||||
@@ -18,52 +46,23 @@ export interface MainResult {
|
||||
error?: string | undefined;
|
||||
}
|
||||
|
||||
export type PromptJSON = {};
|
||||
|
||||
export async function main(inputs: Inputs): Promise<MainResult> {
|
||||
const partialCtx = await initializeContext(inputs);
|
||||
const ctx = partialCtx as MainContext;
|
||||
|
||||
try {
|
||||
log.info("Starting agent run...");
|
||||
await determineAgent(ctx);
|
||||
setupGitAuth(ctx.githubInstallationToken, ctx.repoContext);
|
||||
await setupTempDirectory(ctx);
|
||||
setupMcpLogPolling(ctx);
|
||||
|
||||
setupGitConfig();
|
||||
ctx.payload = parsePayload(inputs);
|
||||
setupMcpServers(ctx);
|
||||
await installAgentCli(ctx);
|
||||
validateApiKey(ctx);
|
||||
|
||||
const githubInstallationToken = await setupGitHubInstallationToken();
|
||||
const repoContext = parseRepoContext();
|
||||
|
||||
setupGitAuth(githubInstallationToken, repoContext);
|
||||
|
||||
const mcpServers = createMcpConfigs(githubInstallationToken);
|
||||
|
||||
log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
|
||||
|
||||
log.info("Running Claude Agent SDK...");
|
||||
log.box(inputs.prompt, { title: "Prompt" });
|
||||
|
||||
// TODO: check if `inputs.prompts` is JSON
|
||||
// if yes, check if it's a webhook payload or toJSON(github.event)
|
||||
// for webhook payloads, check the specified `agent` field
|
||||
|
||||
const result = await claude.run({
|
||||
prompt: inputs.prompt,
|
||||
mcpServers,
|
||||
githubInstallationToken,
|
||||
apiKey: inputs.anthropic_api_key!,
|
||||
});
|
||||
|
||||
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 || "",
|
||||
};
|
||||
const result = await runAgent(ctx);
|
||||
return await handleAgentResult(result);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
log.error(errorMessage);
|
||||
@@ -72,5 +71,219 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
} finally {
|
||||
await cleanup(partialCtx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw an error for missing API key with helpful message linking to repo settings
|
||||
*/
|
||||
function throwMissingApiKeyError({
|
||||
agentName,
|
||||
inputKeys,
|
||||
repoContext,
|
||||
inputs,
|
||||
}: {
|
||||
agentName: string;
|
||||
inputKeys: string[];
|
||||
repoContext: RepoContext;
|
||||
inputs: Inputs;
|
||||
}): never {
|
||||
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
|
||||
const settingsUrl = `${apiUrl}/console/${repoContext.owner}/${repoContext.name}`;
|
||||
|
||||
const secretNames = inputKeys.map((key) => `\`${key.toUpperCase()}\``);
|
||||
const secretNameList =
|
||||
inputKeys.length === 1 ? secretNames[0] : `one of ${secretNames.join(" or ")}`;
|
||||
|
||||
const githubRepoUrl = `https://github.com/${repoContext.owner}/${repoContext.name}`;
|
||||
const githubSecretsUrl = `${githubRepoUrl}/settings/secrets/actions`;
|
||||
|
||||
// Find which agents have inputKeys that match the provided inputs
|
||||
const availableAgents = Object.values(agents).filter((agent) =>
|
||||
agent.apiKeyNames.some((inputKey) => inputs[inputKey])
|
||||
);
|
||||
|
||||
let message = `Pullfrog is configured to use ${agentName}, but the associated API key was not provided.
|
||||
|
||||
To fix this, add the required secret to your GitHub repository:
|
||||
|
||||
1. Go to: ${githubSecretsUrl}
|
||||
2. Click "New repository secret"
|
||||
3. Set the name to ${secretNameList}
|
||||
4. Set the value to your API key
|
||||
5. Click "Add secret"`;
|
||||
|
||||
// If other credentials are present, suggest alternative agents
|
||||
if (availableAgents.length > 0) {
|
||||
const agentNames = availableAgents.map((agent) => agent.name).join(", ");
|
||||
message += `\n\nAlternatively, configure Pullfrog to use an agent with existing credentials in your environment (${agentNames}) at ${settingsUrl}`;
|
||||
}
|
||||
|
||||
log.error(message);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
interface MainContext {
|
||||
inputs: Inputs;
|
||||
githubInstallationToken: string;
|
||||
tokenToRevoke: string | null;
|
||||
repoContext: RepoContext;
|
||||
agentName: AgentNameType;
|
||||
agent: (typeof agents)[AgentNameType];
|
||||
sharedTempDir: string;
|
||||
mcpLogPath: string;
|
||||
pollInterval: NodeJS.Timeout | null;
|
||||
payload: Payload;
|
||||
mcpServers: ReturnType<typeof createMcpConfigs>;
|
||||
cliPath: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
async function initializeContext(
|
||||
inputs: Inputs
|
||||
): Promise<Omit<MainContext, "payload" | "mcpServers" | "cliPath" | "apiKey">> {
|
||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
Inputs.assert(inputs);
|
||||
setupGitConfig();
|
||||
|
||||
const { githubInstallationToken, wasAcquired } = await setupGitHubInstallationToken();
|
||||
const tokenToRevoke = wasAcquired ? githubInstallationToken : null;
|
||||
const repoContext = parseRepoContext();
|
||||
|
||||
return {
|
||||
inputs,
|
||||
githubInstallationToken,
|
||||
tokenToRevoke,
|
||||
repoContext,
|
||||
agentName: "claude" as AgentNameType,
|
||||
agent: agents.claude,
|
||||
sharedTempDir: "",
|
||||
mcpLogPath: "",
|
||||
pollInterval: null,
|
||||
};
|
||||
}
|
||||
|
||||
async function determineAgent(
|
||||
ctx: Omit<MainContext, "payload" | "mcpServers" | "cliPath" | "apiKey">
|
||||
): Promise<void> {
|
||||
const repoSettings = await fetchRepoSettings({
|
||||
token: ctx.githubInstallationToken,
|
||||
repoContext: ctx.repoContext,
|
||||
});
|
||||
|
||||
ctx.agentName = ctx.inputs.agent || repoSettings.defaultAgent || "claude";
|
||||
ctx.agent = agents[ctx.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}`);
|
||||
}
|
||||
|
||||
function setupMcpLogPolling(ctx: MainContext): void {
|
||||
let lastSize = 0;
|
||||
ctx.pollInterval = setInterval(() => {
|
||||
if (existsSync(ctx.mcpLogPath)) {
|
||||
const content = readFileSync(ctx.mcpLogPath, "utf-8");
|
||||
if (content.length > lastSize) {
|
||||
const newContent = content.slice(lastSize);
|
||||
process.stdout.write(newContent);
|
||||
lastSize = content.length;
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function parsePayload(inputs: Inputs): Payload {
|
||||
try {
|
||||
const parsedPrompt = JSON.parse(inputs.prompt);
|
||||
if (!("~pullfrog" in parsedPrompt)) {
|
||||
throw new Error();
|
||||
}
|
||||
return parsedPrompt as Payload;
|
||||
} catch {
|
||||
return {
|
||||
"~pullfrog": true,
|
||||
agent: null,
|
||||
prompt: inputs.prompt,
|
||||
event: {},
|
||||
modes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function setupMcpServers(ctx: MainContext): void {
|
||||
const allModes = [...modes, ...(ctx.payload.modes || [])];
|
||||
ctx.mcpServers = createMcpConfigs(ctx.githubInstallationToken, allModes);
|
||||
log.debug(`📋 MCP Config: ${JSON.stringify(ctx.mcpServers, null, 2)}`);
|
||||
}
|
||||
|
||||
async function installAgentCli(ctx: MainContext): Promise<void> {
|
||||
ctx.cliPath = await ctx.agent.install();
|
||||
}
|
||||
|
||||
function validateApiKey(ctx: MainContext): void {
|
||||
const matchingInputKey = ctx.agent.apiKeyNames.find(
|
||||
(inputKey: string) => ctx.inputs[inputKey as AgentInputKey]
|
||||
);
|
||||
if (!matchingInputKey) {
|
||||
throwMissingApiKeyError({
|
||||
agentName: ctx.agentName,
|
||||
inputKeys: ctx.agent.apiKeyNames,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+107752
File diff suppressed because one or more lines are too long
@@ -0,0 +1,92 @@
|
||||
# gh-pullfrog MCP Tools
|
||||
|
||||
this directory contains the mcp (model context protocol) server tools for interacting with github.
|
||||
|
||||
## available tools
|
||||
|
||||
### check suite tools
|
||||
|
||||
#### `get_check_suite_logs`
|
||||
get workflow run logs for a failed check suite.
|
||||
|
||||
**parameters:**
|
||||
- `check_suite_id` (number): the id from check_suite.id in the webhook payload
|
||||
|
||||
**replaces:** `gh run list` and `gh run view --log`
|
||||
|
||||
**returns:**
|
||||
all logs from all failed workflow runs in the check suite, including:
|
||||
- workflow run details (id, name, html_url, conclusion)
|
||||
- job details for each workflow run (id, name, status, conclusion, logs)
|
||||
|
||||
**example:**
|
||||
```typescript
|
||||
// when handling a check_suite_completed webhook
|
||||
await mcp.call("gh-pullfrog/get_check_suite_logs", {
|
||||
check_suite_id: check_suite.id
|
||||
});
|
||||
```
|
||||
|
||||
### review tools
|
||||
|
||||
#### `get_review_comments`
|
||||
get all line-by-line comments for a specific pull request review.
|
||||
|
||||
**parameters:**
|
||||
- `pull_number` (number): the pull request number
|
||||
- `review_id` (number): the id from review.id in the webhook payload
|
||||
|
||||
**replaces:** `gh api repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments`
|
||||
|
||||
**returns:**
|
||||
array of review comments including:
|
||||
- file path, line number, comment body
|
||||
- side (LEFT/RIGHT) and position in diff
|
||||
- user, timestamps, html_url
|
||||
- in_reply_to_id for threaded comments
|
||||
|
||||
**example:**
|
||||
```typescript
|
||||
// when handling a pull_request_review_submitted webhook
|
||||
await mcp.call("gh-pullfrog/get_review_comments", {
|
||||
pull_number: 47,
|
||||
review_id: review.id
|
||||
});
|
||||
```
|
||||
|
||||
#### `list_pull_request_reviews`
|
||||
list all reviews for a pull request.
|
||||
|
||||
**parameters:**
|
||||
- `pull_number` (number): the pull request number
|
||||
|
||||
**replaces:** `gh api repos/{owner}/{repo}/pulls/{pull_number}/reviews`
|
||||
|
||||
**returns:**
|
||||
array of reviews with:
|
||||
- review id, body, state (approved/changes_requested/commented)
|
||||
- user, commit_id, submitted_at, html_url
|
||||
|
||||
**example:**
|
||||
```typescript
|
||||
await mcp.call("gh-pullfrog/list_pull_request_reviews", {
|
||||
pull_number: 47
|
||||
});
|
||||
```
|
||||
|
||||
### other tools
|
||||
|
||||
see individual files for documentation on other tools:
|
||||
- `comment.ts` - create, edit, and update comments
|
||||
- `issue.ts` - create issues
|
||||
- `pr.ts` - create pull requests
|
||||
- `prInfo.ts` - get pull request information
|
||||
- `review.ts` - create pull request reviews
|
||||
- `selectMode.ts` - select execution mode
|
||||
|
||||
## usage in agents
|
||||
|
||||
agents should never use the `gh` cli. instead, they should use the mcp tools provided by this server.
|
||||
|
||||
the agent instructions automatically include guidance on using these tools.
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { type } from "arktype";
|
||||
import { contextualize, tool } from "./shared.ts";
|
||||
|
||||
export const GetCheckSuiteLogs = type({
|
||||
check_suite_id: type.number.describe("the id from check_suite.id"),
|
||||
});
|
||||
|
||||
export const GetCheckSuiteLogsTool = tool({
|
||||
name: "get_check_suite_logs",
|
||||
description:
|
||||
"get workflow run logs for a failed check suite. pass check_suite.id from the webhook payload.",
|
||||
parameters: GetCheckSuiteLogs,
|
||||
execute: contextualize(async ({ check_suite_id }, ctx) => {
|
||||
// get workflow runs for this specific check suite
|
||||
const workflowRuns = await ctx.octokit.paginate(
|
||||
ctx.octokit.rest.actions.listWorkflowRunsForRepo,
|
||||
{
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
check_suite_id,
|
||||
per_page: 100,
|
||||
}
|
||||
);
|
||||
|
||||
const failedRuns = workflowRuns.filter((run) => run.conclusion === "failure");
|
||||
|
||||
if (failedRuns.length === 0) {
|
||||
return {
|
||||
check_suite_id,
|
||||
message: "no failed workflow runs found for this check suite",
|
||||
workflow_runs: [],
|
||||
};
|
||||
}
|
||||
|
||||
// get logs for each failed run
|
||||
const logsForRuns = await Promise.all(
|
||||
failedRuns.map(async (run) => {
|
||||
const jobs = await ctx.octokit.paginate(ctx.octokit.rest.actions.listJobsForWorkflowRun, {
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
run_id: run.id,
|
||||
});
|
||||
|
||||
const jobLogs = await Promise.all(
|
||||
jobs.map(async (job) => {
|
||||
try {
|
||||
const logsResponse = await ctx.octokit.rest.actions.downloadJobLogsForWorkflowRun({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
job_id: job.id,
|
||||
});
|
||||
|
||||
const logsUrl = logsResponse.url;
|
||||
const logsText = await fetch(logsUrl).then((r) => r.text());
|
||||
|
||||
return {
|
||||
job_id: job.id,
|
||||
job_name: job.name,
|
||||
status: job.status,
|
||||
conclusion: job.conclusion,
|
||||
started_at: job.started_at,
|
||||
completed_at: job.completed_at,
|
||||
logs: logsText,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
job_id: job.id,
|
||||
job_name: job.name,
|
||||
status: job.status,
|
||||
conclusion: job.conclusion,
|
||||
started_at: job.started_at,
|
||||
completed_at: job.completed_at,
|
||||
error: `failed to fetch logs: ${error}`,
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
workflow_run_id: run.id,
|
||||
workflow_name: run.name,
|
||||
html_url: run.html_url,
|
||||
conclusion: run.conclusion,
|
||||
jobs: jobLogs,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
check_suite_id,
|
||||
workflow_runs: logsForRuns,
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -53,3 +53,69 @@ export const EditCommentTool = tool({
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
let workingCommentId: number | null = null;
|
||||
|
||||
export const WorkingComment = type({
|
||||
issueNumber: type.number.describe("the issue number to comment on"),
|
||||
intent: type("/^I'll .+$/").describe(
|
||||
"the body of the initial comment expressing your intent to handle the request. must have the form 'I'll {summary of request}'"
|
||||
),
|
||||
});
|
||||
|
||||
export const CreateWorkingCommentTool = tool({
|
||||
name: "create_working_comment",
|
||||
description:
|
||||
"Create an initial comment on a GitHub issue that will be updated as work progresses",
|
||||
parameters: WorkingComment,
|
||||
execute: contextualize(async ({ issueNumber, intent }, ctx) => {
|
||||
if (workingCommentId) {
|
||||
throw new Error("create_working_comment may not be called multiple times");
|
||||
}
|
||||
|
||||
const result = await ctx.octokit.rest.issues.createComment({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
issue_number: issueNumber,
|
||||
body: `${intent} <img src="https://pullfrog.ai/party-parrot.gif" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />`,
|
||||
});
|
||||
|
||||
workingCommentId = result.data.id;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
commentId: result.data.id,
|
||||
url: result.data.html_url,
|
||||
body: result.data.body,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
export const WorkingCommentUpdate = type({
|
||||
body: type.string.describe("the new comment body content"),
|
||||
});
|
||||
|
||||
export const UpdateWorkingCommentTool = tool({
|
||||
name: "update_working_comment",
|
||||
description: "Update a working comment on a GitHub issue",
|
||||
parameters: WorkingCommentUpdate,
|
||||
execute: contextualize(async ({ body }, ctx) => {
|
||||
if (!workingCommentId) {
|
||||
throw new Error("create_working_comment must be called before update_working_comment");
|
||||
}
|
||||
|
||||
const result = await ctx.octokit.rest.issues.updateComment({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
comment_id: workingCommentId,
|
||||
body,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
commentId: result.data.id,
|
||||
url: result.data.html_url,
|
||||
body: result.data.body,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
+11
-14
@@ -2,35 +2,32 @@
|
||||
* Simple MCP configuration helper for adding our minimal GitHub comment server
|
||||
*/
|
||||
|
||||
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { fromHere } from "@ark/fs";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import type { Mode } from "../modes.ts";
|
||||
import { parseRepoContext } from "../utils/github.ts";
|
||||
|
||||
export const ghPullfrogMcpName = "gh-pullfrog";
|
||||
|
||||
export type McpName = typeof ghPullfrogMcpName;
|
||||
|
||||
export type McpConfigs = Record<McpName, McpServerConfig>;
|
||||
export type McpConfigs = Record<McpName, McpStdioServerConfig>;
|
||||
|
||||
export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
|
||||
export function createMcpConfigs(githubInstallationToken: string, modes: Mode[]): McpConfigs {
|
||||
const repoContext = parseRepoContext();
|
||||
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
|
||||
|
||||
// Get absolute path to entry.js - use GITHUB_ACTION_PATH if available, otherwise current directory
|
||||
const entryPath = process.env.GITHUB_ACTION_PATH
|
||||
? `${process.env.GITHUB_ACTION_PATH}/entry.js`
|
||||
: `${process.cwd()}/entry.js`;
|
||||
// In production (GitHub Actions), mcp-server is in same directory as entry.js (where this is bundled)
|
||||
// In development, server.ts is in the same directory as this file (config.ts)
|
||||
const serverPath = process.env.GITHUB_ACTIONS ? fromHere("mcp-server") : fromHere("server.ts");
|
||||
|
||||
return {
|
||||
[ghPullfrogMcpName]: {
|
||||
command: "node",
|
||||
args: [
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
`import('${entryPath.replace(/'/g, "\\'")}').then(m => m.createMcpServer())`,
|
||||
],
|
||||
args: [serverPath],
|
||||
env: {
|
||||
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
|
||||
GITHUB_REPOSITORY: githubRepository,
|
||||
PULLFROG_MODES: JSON.stringify(modes),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { type } from "arktype";
|
||||
import { contextualize, tool } from "./shared.ts";
|
||||
|
||||
export const ListFiles = type({
|
||||
path: type.string
|
||||
.describe("The path to list files from (defaults to current directory)")
|
||||
.default("."),
|
||||
});
|
||||
|
||||
export const ListFilesTool = tool({
|
||||
name: "list_files",
|
||||
description:
|
||||
"List files in the repository using git ls-files. Useful for discovering the file structure and locating files.",
|
||||
parameters: ListFiles,
|
||||
execute: contextualize(async ({ path }) => {
|
||||
try {
|
||||
// Use git ls-files to list tracked files
|
||||
// This respects .gitignore and gives a clean list of source files
|
||||
const command = path === "." ? "git ls-files" : `git ls-files ${path}`;
|
||||
const output = execSync(command, { encoding: "utf-8" });
|
||||
const files = output.split("\n").filter((f) => f.trim() !== "");
|
||||
|
||||
if (files.length === 0) {
|
||||
// Fallback for non-git environments or untracked files
|
||||
const findCmd = `find ${path} -maxdepth 3 -not -path '*/.*' -type f`;
|
||||
const findOutput = execSync(findCmd, { encoding: "utf-8" });
|
||||
return {
|
||||
files: findOutput.split("\n").filter((f) => f.trim() !== ""),
|
||||
method: "find",
|
||||
};
|
||||
}
|
||||
|
||||
return { files, method: "git" };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
error: `Failed to list files: ${errorMessage}`,
|
||||
hint: "Try using a specific path if the repository root is not the current directory.",
|
||||
};
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
// re-export from external.ts for backward compatibility
|
||||
export { ghPullfrogMcpName } from "../external.ts";
|
||||
+2
-1
@@ -36,7 +36,8 @@ export const PullRequestInfoTool = tool({
|
||||
execSync(`git fetch origin ${headBranch}`, { stdio: "inherit" });
|
||||
|
||||
log.info(`Checking out PR branch: origin/${headBranch}`);
|
||||
execSync(`git checkout origin/${headBranch}`, { stdio: "inherit" });
|
||||
// check out a local branch tracking the remote branch so we can push changes
|
||||
execSync(`git checkout -B ${headBranch} origin/${headBranch}`, { stdio: "inherit" });
|
||||
|
||||
return {
|
||||
number: data.number,
|
||||
|
||||
+2
-5
@@ -4,9 +4,6 @@ import { contextualize, tool } from "./shared.ts";
|
||||
|
||||
export const Review = type({
|
||||
pull_number: type.number.describe("The pull request number to review"),
|
||||
event: type
|
||||
.enumerated("APPROVE", "REQUEST_CHANGES", "COMMENT")
|
||||
.describe("'APPROVE', 'REQUEST_CHANGES', or 'COMMENT' (the review action)"),
|
||||
body: type.string
|
||||
.describe(
|
||||
"Brief summary or general feedback that doesn't apply to specific code locations. Keep it concise - most feedback should be in the 'comments' array."
|
||||
@@ -45,7 +42,7 @@ export const ReviewTool = tool({
|
||||
"IMPORTANT: Use 'comments' array for ALL specific code issues at the line-level. " +
|
||||
"Only use 'body' for a brief summary or feedback that doesn't apply to a specific location.",
|
||||
parameters: Review,
|
||||
execute: contextualize(async ({ pull_number, event, body, commit_id, comments = [] }, ctx) => {
|
||||
execute: contextualize(async ({ pull_number, body, commit_id, comments = [] }, ctx) => {
|
||||
// Get the PR to determine the head commit if commit_id not provided
|
||||
const pr = await ctx.octokit.rest.pulls.get({
|
||||
owner: ctx.owner,
|
||||
@@ -58,7 +55,7 @@ export const ReviewTool = tool({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
pull_number,
|
||||
event,
|
||||
event: "COMMENT",
|
||||
};
|
||||
if (body) params.body = body;
|
||||
if (commit_id) {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { type } from "arktype";
|
||||
import { contextualize, tool } from "./shared.ts";
|
||||
|
||||
export const GetReviewComments = type({
|
||||
pull_number: type.number.describe("The pull request number"),
|
||||
review_id: type.number.describe("The review ID to get comments for"),
|
||||
});
|
||||
|
||||
export const GetReviewCommentsTool = tool({
|
||||
name: "get_review_comments",
|
||||
description:
|
||||
"Get all review comments for a specific pull request review. Returns line-by-line comments that were left on specific code locations.",
|
||||
parameters: GetReviewComments,
|
||||
execute: contextualize(async ({ pull_number, review_id }, ctx) => {
|
||||
const comments = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listCommentsForReview, {
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
pull_number,
|
||||
review_id,
|
||||
});
|
||||
|
||||
return {
|
||||
review_id,
|
||||
pull_number,
|
||||
comments: comments.map((comment) => ({
|
||||
id: comment.id,
|
||||
body: comment.body,
|
||||
path: comment.path,
|
||||
line: comment.line,
|
||||
side: comment.side,
|
||||
start_line: comment.start_line,
|
||||
start_side: comment.start_side,
|
||||
user: typeof comment.user === "string" ? comment.user : comment.user?.login,
|
||||
created_at: comment.created_at,
|
||||
updated_at: comment.updated_at,
|
||||
html_url: comment.html_url,
|
||||
in_reply_to_id: comment.in_reply_to_id,
|
||||
diff_hunk: comment.diff_hunk,
|
||||
reactions: comment.reactions,
|
||||
})),
|
||||
count: comments.length,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
export const ListPullRequestReviews = type({
|
||||
pull_number: type.number.describe("The pull request number to list reviews for"),
|
||||
});
|
||||
|
||||
export const ListPullRequestReviewsTool = tool({
|
||||
name: "list_pull_request_reviews",
|
||||
description:
|
||||
"List all reviews for a pull request. Returns all reviews including approvals, request changes, and comments.",
|
||||
parameters: ListPullRequestReviews,
|
||||
execute: contextualize(async ({ pull_number }, ctx) => {
|
||||
const reviews = await ctx.octokit.paginate(ctx.octokit.rest.pulls.listReviews, {
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
pull_number,
|
||||
});
|
||||
|
||||
return {
|
||||
pull_number,
|
||||
reviews: reviews.map((review) => ({
|
||||
id: review.id,
|
||||
body: review.body,
|
||||
state: review.state,
|
||||
user: review.user?.login,
|
||||
commit_id: review.commit_id,
|
||||
submitted_at: review.submitted_at,
|
||||
html_url: review.html_url,
|
||||
})),
|
||||
count: reviews.length,
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
+30
-18
@@ -1,27 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
// Minimal GitHub Issue Comment MCP Server
|
||||
import { FastMCP } from "fastmcp";
|
||||
import { CreateCommentTool, EditCommentTool } from "./comment.ts";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import { GetCheckSuiteLogsTool } from "./checkSuite.ts";
|
||||
import {
|
||||
CreateCommentTool,
|
||||
CreateWorkingCommentTool,
|
||||
EditCommentTool,
|
||||
UpdateWorkingCommentTool,
|
||||
} from "./comment.ts";
|
||||
import { IssueTool } from "./issue.ts";
|
||||
import { PullRequestTool } from "./pr.ts";
|
||||
import { PullRequestInfoTool } from "./prInfo.ts";
|
||||
import { ReviewTool } from "./review.ts";
|
||||
import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.ts";
|
||||
import { SelectModeTool } from "./selectMode.ts";
|
||||
import { addTools } from "./shared.ts";
|
||||
|
||||
export function createMcpServer(): void {
|
||||
const server = new FastMCP({
|
||||
name: "gh-pullfrog",
|
||||
version: "0.0.1",
|
||||
});
|
||||
const server = new FastMCP({
|
||||
name: ghPullfrogMcpName,
|
||||
version: "0.0.1",
|
||||
});
|
||||
|
||||
addTools(server, [
|
||||
CreateCommentTool,
|
||||
EditCommentTool,
|
||||
IssueTool,
|
||||
PullRequestTool,
|
||||
ReviewTool,
|
||||
PullRequestInfoTool,
|
||||
]);
|
||||
addTools(server, [
|
||||
SelectModeTool,
|
||||
CreateCommentTool,
|
||||
EditCommentTool,
|
||||
CreateWorkingCommentTool,
|
||||
UpdateWorkingCommentTool,
|
||||
IssueTool,
|
||||
// ListFilesTool,
|
||||
PullRequestTool,
|
||||
ReviewTool,
|
||||
PullRequestInfoTool,
|
||||
GetReviewCommentsTool,
|
||||
ListPullRequestReviewsTool,
|
||||
GetCheckSuiteLogsTool,
|
||||
]);
|
||||
|
||||
server.start();
|
||||
}
|
||||
server.start();
|
||||
|
||||
+34
-1
@@ -2,6 +2,7 @@ import { cached } from "@ark/util";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
||||
import type { FastMCP, Tool } from "fastmcp";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { parseRepoContext, type RepoContext } from "../utils/github.ts";
|
||||
|
||||
export interface ToolResult {
|
||||
@@ -30,7 +31,39 @@ export interface McpContext extends RepoContext {
|
||||
octokit: Octokit;
|
||||
}
|
||||
|
||||
export const tool = <const params>(tool: Tool<any, StandardSchemaV1<params>>) => tool;
|
||||
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => {
|
||||
// Wrap the execute function to add logging with the tool name
|
||||
const toolName = toolDef.name;
|
||||
const originalExecute = toolDef.execute;
|
||||
|
||||
toolDef.execute = async (args: params, context: any) => {
|
||||
try {
|
||||
const result = await originalExecute(args, context);
|
||||
// Check if result is a ToolResult with isError property
|
||||
const isError =
|
||||
result && typeof result === "object" && "isError" in result && result.isError === true;
|
||||
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;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
log.toolCall({ toolName, request: args, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return toolDef;
|
||||
};
|
||||
|
||||
export const addTools = (server: FastMCP, tools: Tool<any, any>[]) => {
|
||||
for (const tool of tools) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { ghPullfrogMcpName } from "./external.ts";
|
||||
|
||||
export interface Mode {
|
||||
name: string;
|
||||
description: string;
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
const initialCommentInstruction = `Use ${ghPullfrogMcpName}/create_working_comment to create an initial Working Comment with a conversational description of what work you are about to perform.`;
|
||||
|
||||
export const modes: Mode[] = [
|
||||
{
|
||||
name: "Plan",
|
||||
description:
|
||||
"Create plans, break down tasks, outline steps, analyze requirements, understand scope of work, or provide task breakdowns",
|
||||
prompt: `Follow these steps:
|
||||
1. ${initialCommentInstruction}
|
||||
|
||||
2. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context (read AGENTS.md if it exists, understand how to install dependencies, run tests, run builds, and make changes according to best practices). Skip this step if the prompt is trivial and self-contained.
|
||||
3. Analyze the request and break it down into clear, actionable tasks
|
||||
4. Consider dependencies, potential challenges, and implementation order
|
||||
5. Create a structured plan with clear milestones
|
||||
6. Update your comment using ${ghPullfrogMcpName}/update_working_comment to present the plan in a clear, organized format
|
||||
7. Continue updating the same comment as needed (never create additional comments - always use update_working_comment)`,
|
||||
},
|
||||
{
|
||||
name: "Build",
|
||||
description:
|
||||
"Implement, build, create, or develop code changes; make specific changes to files or features; execute a plan; or handle tasks with specific implementation details",
|
||||
prompt: `Follow these steps:
|
||||
1. ${initialCommentInstruction}
|
||||
2. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context (read AGENTS.md if it exists, understand how to install dependencies, run tests, run builds, and make changes according to best practices). Skip this step if the prompt is trivial and self-contained.
|
||||
3. Understand the requirements and any existing plan
|
||||
4. Make the necessary code changes
|
||||
5. Test your changes to ensure they work correctly
|
||||
6. Update your comment using ${ghPullfrogMcpName}/update_working_comment to share progress and results
|
||||
7. Continue updating the same comment as you make progress (never create additional comments - always use update_working_comment)`,
|
||||
},
|
||||
{
|
||||
name: "Review",
|
||||
description:
|
||||
"Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
||||
prompt: `Follow these steps:
|
||||
1. ${initialCommentInstruction}
|
||||
|
||||
2. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically prepares the repository by fetching and checking out the PR branch)
|
||||
3. View diff: git diff origin/<base>...origin/<head> (use line numbers from this for inline comments, replace <base> and <head> with 'base' and 'head' from PR info)
|
||||
4. Read files from the checked-out PR branch to understand the implementation
|
||||
5. Update your comment using ${ghPullfrogMcpName}/update_working_comment with findings as you review
|
||||
6. When submitting review: use the 'comments' array for ALL specific code issues - include the file path and line position from the diff
|
||||
7. Only use the 'body' field for a brief summary (1-2 sentences) or for feedback that doesn't apply to a specific code location
|
||||
8. Continue updating the same comment as needed (never create additional comments - always use update_working_comment)`,
|
||||
},
|
||||
{
|
||||
name: "Prompt",
|
||||
description:
|
||||
"Fallback for tasks that don't fit other workflows, direct prompts via comments, or requests requiring general assistance without a specific workflow pattern",
|
||||
prompt: `Follow these steps:
|
||||
1. ${initialCommentInstruction}
|
||||
|
||||
2. Perform the requested task. Only take action if you have high confidence that you understand what is being asked. If you are not sure, ask for clarification. Take stock of the tools at your disposal.
|
||||
3. As your work progresses, update your Working Comment to share progress and results using ${ghPullfrogMcpName}/update_working_comment. Do not create additional comments unless you are explicitly asked to do so.
|
||||
4. When you finish the task, update the Working Comment a final time with a summary of the results and links to any created issues, PRs, etc.`,
|
||||
},
|
||||
];
|
||||
Generated
+960
@@ -0,0 +1,960 @@
|
||||
{
|
||||
"name": "@pullfrog/action",
|
||||
"version": "0.0.96",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@pullfrog/action",
|
||||
"version": "0.0.96",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.26",
|
||||
"@ark/fs": "0.53.0",
|
||||
"@ark/util": "0.53.0",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@octokit/webhooks-types": "^7.6.1",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"arktype": "2.1.25",
|
||||
"convex": "^1.29.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"execa": "^9.6.0",
|
||||
"fastmcp": "^3.20.0",
|
||||
"table": "^6.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"arg": "^5.0.2",
|
||||
"esbuild": "^0.25.9",
|
||||
"husky": "^9.0.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/@actions+core@1.11.1/node_modules/@actions/core": {
|
||||
"version": "1.11.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/http-client": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.18.112"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/@anthropic-ai+claude-agent-sdk@0.1.26_zod@3.25.76/node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.1.26",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "^0.33.5",
|
||||
"@img/sharp-darwin-x64": "^0.33.5",
|
||||
"@img/sharp-linux-arm": "^0.33.5",
|
||||
"@img/sharp-linux-arm64": "^0.33.5",
|
||||
"@img/sharp-linux-x64": "^0.33.5",
|
||||
"@img/sharp-win32-x64": "^0.33.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/@ark+fs@0.53.0/node_modules/@ark/fs": {
|
||||
"version": "0.53.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../node_modules/.pnpm/@ark+util@0.53.0/node_modules/@ark/util": {
|
||||
"version": "0.53.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../node_modules/.pnpm/@octokit+rest@22.0.0/node_modules/@octokit/rest": {
|
||||
"version": "22.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/core": "^7.0.2",
|
||||
"@octokit/plugin-paginate-rest": "^13.0.1",
|
||||
"@octokit/plugin-request-log": "^6.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^16.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/auth-action": "^6.0.1",
|
||||
"@octokit/auth-app": "^8.0.1",
|
||||
"@octokit/fixtures-server": "^8.1.0",
|
||||
"@octokit/request": "^10.0.0",
|
||||
"@octokit/tsconfig": "^4.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"fetch-mock": "^12.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"nock": "^14.0.0-beta.8",
|
||||
"prettier": "^3.2.4",
|
||||
"semantic-release-plugin-update-version-in-files": "^2.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"undici": "^6.4.0",
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/@octokit+webhooks-types@7.6.1/node_modules/@octokit/webhooks-types": {
|
||||
"version": "7.6.1",
|
||||
"license": "MIT",
|
||||
"devDependencies": {}
|
||||
},
|
||||
"../node_modules/.pnpm/@standard-schema+spec@1.0.0/node_modules/@standard-schema/spec": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"tsup": "^8.3.0",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/@types+node@24.7.2/node_modules/@types/node": {
|
||||
"version": "24.7.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.14.0"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/arg@5.0.2/node_modules/arg": {
|
||||
"version": "5.0.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"chai": "^4.1.1",
|
||||
"jest": "^27.0.6",
|
||||
"prettier": "^2.3.2"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/arktype@2.1.25/node_modules/arktype": {
|
||||
"version": "2.1.25",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ark/schema": "0.53.0",
|
||||
"@ark/util": "0.53.0",
|
||||
"arkregex": "0.0.2"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv": {
|
||||
"version": "17.2.3",
|
||||
"license": "BSD-2-Clause",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.3",
|
||||
"decache": "^4.6.2",
|
||||
"sinon": "^14.0.1",
|
||||
"standard": "^17.0.0",
|
||||
"standard-version": "^9.5.0",
|
||||
"tap": "^19.2.0",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/esbuild@0.25.10/node_modules/esbuild": {
|
||||
"version": "0.25.10",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.10",
|
||||
"@esbuild/android-arm": "0.25.10",
|
||||
"@esbuild/android-arm64": "0.25.10",
|
||||
"@esbuild/android-x64": "0.25.10",
|
||||
"@esbuild/darwin-arm64": "0.25.10",
|
||||
"@esbuild/darwin-x64": "0.25.10",
|
||||
"@esbuild/freebsd-arm64": "0.25.10",
|
||||
"@esbuild/freebsd-x64": "0.25.10",
|
||||
"@esbuild/linux-arm": "0.25.10",
|
||||
"@esbuild/linux-arm64": "0.25.10",
|
||||
"@esbuild/linux-ia32": "0.25.10",
|
||||
"@esbuild/linux-loong64": "0.25.10",
|
||||
"@esbuild/linux-mips64el": "0.25.10",
|
||||
"@esbuild/linux-ppc64": "0.25.10",
|
||||
"@esbuild/linux-riscv64": "0.25.10",
|
||||
"@esbuild/linux-s390x": "0.25.10",
|
||||
"@esbuild/linux-x64": "0.25.10",
|
||||
"@esbuild/netbsd-arm64": "0.25.10",
|
||||
"@esbuild/netbsd-x64": "0.25.10",
|
||||
"@esbuild/openbsd-arm64": "0.25.10",
|
||||
"@esbuild/openbsd-x64": "0.25.10",
|
||||
"@esbuild/openharmony-arm64": "0.25.10",
|
||||
"@esbuild/sunos-x64": "0.25.10",
|
||||
"@esbuild/win32-arm64": "0.25.10",
|
||||
"@esbuild/win32-ia32": "0.25.10",
|
||||
"@esbuild/win32-x64": "0.25.10"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/execa@9.6.0/node_modules/execa": {
|
||||
"version": "9.6.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/merge-streams": "^4.0.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"figures": "^6.1.0",
|
||||
"get-stream": "^9.0.0",
|
||||
"human-signals": "^8.0.1",
|
||||
"is-plain-obj": "^4.1.0",
|
||||
"is-stream": "^4.0.1",
|
||||
"npm-run-path": "^6.0.0",
|
||||
"pretty-ms": "^9.2.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"strip-final-newline": "^4.0.0",
|
||||
"yoctocolors": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.21",
|
||||
"ava": "^6.3.0",
|
||||
"c8": "^10.1.3",
|
||||
"get-node": "^15.0.3",
|
||||
"is-in-ci": "^1.0.0",
|
||||
"is-running": "^2.1.0",
|
||||
"log-process-errors": "^12.0.1",
|
||||
"path-exists": "^5.0.0",
|
||||
"path-key": "^4.0.0",
|
||||
"tempfile": "^5.0.0",
|
||||
"tsd": "^0.32.0",
|
||||
"typescript": "^5.8.3",
|
||||
"which": "^5.0.0",
|
||||
"xo": "^0.60.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/fastmcp@3.20.0_arktype@2.1.25_effect@3.16.12/node_modules/fastmcp": {
|
||||
"version": "3.20.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.17.2",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"execa": "^9.6.0",
|
||||
"file-type": "^21.0.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"mcp-proxy": "^5.8.1",
|
||||
"strict-event-emitter-types": "^2.0.0",
|
||||
"undici": "^7.13.0",
|
||||
"uri-templates": "^0.2.0",
|
||||
"xsschema": "0.3.5",
|
||||
"yargs": "^18.0.0",
|
||||
"zod": "^3.25.76",
|
||||
"zod-to-json-schema": "^3.24.6"
|
||||
},
|
||||
"bin": {
|
||||
"fastmcp": "dist/bin/fastmcp.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@modelcontextprotocol/inspector": "^0.16.2",
|
||||
"@sebbo2002/semantic-release-jsr": "^3.0.1",
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/uri-templates": "^0.1.34",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"@valibot/to-json-schema": "^1.3.0",
|
||||
"@wong2/mcp-cli": "^1.13.0",
|
||||
"arktype": "^2.1.20",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-perfectionist": "^4.15.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eventsource-client": "^1.1.4",
|
||||
"get-port-please": "^3.2.0",
|
||||
"jiti": "^2.5.1",
|
||||
"jsr": "^0.13.5",
|
||||
"prettier": "^3.6.2",
|
||||
"semantic-release": "^24.2.7",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.39.0",
|
||||
"valibot": "^1.1.0",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/husky@9.1.7/node_modules/husky": {
|
||||
"version": "9.1.7",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"husky": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/table@6.9.0/node_modules/table": {
|
||||
"version": "6.9.0",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.1",
|
||||
"lodash.truncate": "^4.4.2",
|
||||
"slice-ansi": "^4.0.0",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.16",
|
||||
"@types/lodash.mapvalues": "^4.6.6",
|
||||
"@types/lodash.truncate": "^4.4.6",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^14.14.37",
|
||||
"@types/sinon": "^10.0.0",
|
||||
"@types/slice-ansi": "^4.0.0",
|
||||
"ajv-cli": "^5.0.0",
|
||||
"ajv-keywords": "^5.0.0",
|
||||
"chai": "^4.2.0",
|
||||
"chalk": "^4.1.0",
|
||||
"coveralls": "^3.1.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-canonical": "^25.0.0",
|
||||
"gitdown": "^3.1.4",
|
||||
"husky": "^4.3.6",
|
||||
"js-beautify": "^1.14.0",
|
||||
"lodash.mapvalues": "^4.6.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
"mocha": "^8.2.1",
|
||||
"nyc": "^15.1.0",
|
||||
"semantic-release": "^17.3.1",
|
||||
"sinon": "^12.0.1",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "4.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dprint/formatter": "^0.4.1",
|
||||
"@dprint/typescript": "0.93.4",
|
||||
"@esfx/canceltoken": "^1.0.0",
|
||||
"@eslint/js": "^9.20.0",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@types/chai": "^4.3.20",
|
||||
"@types/diff": "^7.0.1",
|
||||
"@types/minimist": "^1.2.5",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/ms": "^0.7.34",
|
||||
"@types/node": "latest",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/which": "^3.0.4",
|
||||
"@typescript-eslint/rule-tester": "^8.24.1",
|
||||
"@typescript-eslint/type-utils": "^8.24.1",
|
||||
"@typescript-eslint/utils": "^8.24.1",
|
||||
"azure-devops-node-api": "^14.1.0",
|
||||
"c8": "^10.1.3",
|
||||
"chai": "^4.5.0",
|
||||
"chokidar": "^4.0.3",
|
||||
"diff": "^7.0.0",
|
||||
"dprint": "^0.49.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-formatter-autolinkable-stylish": "^1.4.0",
|
||||
"eslint-plugin-regexp": "^2.7.0",
|
||||
"fast-xml-parser": "^4.5.2",
|
||||
"glob": "^10.4.5",
|
||||
"globals": "^15.15.0",
|
||||
"hereby": "^1.10.0",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"knip": "^5.44.4",
|
||||
"minimist": "^1.2.8",
|
||||
"mocha": "^10.8.2",
|
||||
"mocha-fivemat-progress-reporter": "^0.1.0",
|
||||
"monocart-coverage-reports": "^2.12.1",
|
||||
"ms": "^2.1.3",
|
||||
"picocolors": "^1.1.1",
|
||||
"playwright": "^1.50.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"which": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
"resolved": "../node_modules/.pnpm/@actions+core@1.11.1/node_modules/@actions/core",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"resolved": "../node_modules/.pnpm/@anthropic-ai+claude-agent-sdk@0.1.26_zod@3.25.76/node_modules/@anthropic-ai/claude-agent-sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@ark/fs": {
|
||||
"resolved": "../node_modules/.pnpm/@ark+fs@0.53.0/node_modules/@ark/fs",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@ark/util": {
|
||||
"resolved": "../node_modules/.pnpm/@ark+util@0.53.0/node_modules/@ark/util",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
|
||||
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
|
||||
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
|
||||
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
|
||||
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
|
||||
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
|
||||
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
|
||||
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
|
||||
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
|
||||
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
|
||||
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest": {
|
||||
"resolved": "../node_modules/.pnpm/@octokit+rest@22.0.0/node_modules/@octokit/rest",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@octokit/webhooks-types": {
|
||||
"resolved": "../node_modules/.pnpm/@octokit+webhooks-types@7.6.1/node_modules/@octokit/webhooks-types",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"resolved": "../node_modules/.pnpm/@standard-schema+spec@1.0.0/node_modules/@standard-schema/spec",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"resolved": "../node_modules/.pnpm/@types+node@24.7.2/node_modules/@types/node",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"resolved": "../node_modules/.pnpm/arg@5.0.2/node_modules/arg",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/arktype": {
|
||||
"resolved": "../node_modules/.pnpm/arktype@2.1.25/node_modules/arktype",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/convex": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/convex/-/convex-1.29.0.tgz",
|
||||
"integrity": "sha512-uoIPXRKIp2eLCkkR9WJ2vc9NtgQtx8Pml59WPUahwbrd5EuW2WLI/cf2E7XrUzOSifdQC3kJZepisk4wJNTJaA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"esbuild": "0.25.4",
|
||||
"prettier": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"convex": "bin/main.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@auth0/auth0-react": "^2.0.1",
|
||||
"@clerk/clerk-react": "^4.12.8 || ^5.0.0",
|
||||
"react": "^18.0.0 || ^19.0.0-0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@auth0/auth0-react": {
|
||||
"optional": true
|
||||
},
|
||||
"@clerk/clerk-react": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/convex/node_modules/esbuild": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
||||
"integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.4",
|
||||
"@esbuild/android-arm": "0.25.4",
|
||||
"@esbuild/android-arm64": "0.25.4",
|
||||
"@esbuild/android-x64": "0.25.4",
|
||||
"@esbuild/darwin-arm64": "0.25.4",
|
||||
"@esbuild/darwin-x64": "0.25.4",
|
||||
"@esbuild/freebsd-arm64": "0.25.4",
|
||||
"@esbuild/freebsd-x64": "0.25.4",
|
||||
"@esbuild/linux-arm": "0.25.4",
|
||||
"@esbuild/linux-arm64": "0.25.4",
|
||||
"@esbuild/linux-ia32": "0.25.4",
|
||||
"@esbuild/linux-loong64": "0.25.4",
|
||||
"@esbuild/linux-mips64el": "0.25.4",
|
||||
"@esbuild/linux-ppc64": "0.25.4",
|
||||
"@esbuild/linux-riscv64": "0.25.4",
|
||||
"@esbuild/linux-s390x": "0.25.4",
|
||||
"@esbuild/linux-x64": "0.25.4",
|
||||
"@esbuild/netbsd-arm64": "0.25.4",
|
||||
"@esbuild/netbsd-x64": "0.25.4",
|
||||
"@esbuild/openbsd-arm64": "0.25.4",
|
||||
"@esbuild/openbsd-x64": "0.25.4",
|
||||
"@esbuild/sunos-x64": "0.25.4",
|
||||
"@esbuild/win32-arm64": "0.25.4",
|
||||
"@esbuild/win32-ia32": "0.25.4",
|
||||
"@esbuild/win32-x64": "0.25.4"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"resolved": "../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"resolved": "../node_modules/.pnpm/esbuild@0.25.10/node_modules/esbuild",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"resolved": "../node_modules/.pnpm/execa@9.6.0/node_modules/execa",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/fastmcp": {
|
||||
"resolved": "../node_modules/.pnpm/fastmcp@3.20.0_arktype@2.1.25_effect@3.16.12/node_modules/fastmcp",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/husky": {
|
||||
"resolved": "../node_modules/.pnpm/husky@9.1.7/node_modules/husky",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/table": {
|
||||
"resolved": "../node_modules/.pnpm/table@6.9.0/node_modules/table",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"resolved": "../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript",
|
||||
"link": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pullfrog/action",
|
||||
"version": "0.0.76",
|
||||
"version": "0.0.111",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
@@ -17,23 +17,27 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "node esbuild.config.js",
|
||||
"play": "node play.ts",
|
||||
"scratch": "node scratch.ts",
|
||||
"upDeps": "pnpm up --latest",
|
||||
"lock": "pnpm --ignore-workspace install",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.26",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.1.37",
|
||||
"@ark/fs": "0.53.0",
|
||||
"@ark/util": "0.53.0",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@octokit/webhooks-types": "^7.6.1",
|
||||
"@openai/codex-sdk": "0.58.0",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"arktype": "2.1.25",
|
||||
"dotenv": "^17.2.3",
|
||||
"execa": "^9.6.0",
|
||||
"fastmcp": "^3.20.0",
|
||||
"table": "^6.9.0"
|
||||
"table": "^6.9.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
|
||||
@@ -2,10 +2,11 @@ import { existsSync, readFileSync } from "node:fs";
|
||||
import { extname, join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { fromHere } from "@ark/fs";
|
||||
import { flatMorph } from "@ark/util";
|
||||
import arg from "arg";
|
||||
import { config } from "dotenv";
|
||||
import { agents } from "./agents/index.ts";
|
||||
import { type Inputs, main } from "./main.ts";
|
||||
import packageJson from "./package.json" with { type: "json" };
|
||||
import { log } from "./utils/cli.ts";
|
||||
import { setupTestRepo } from "./utils/setup.ts";
|
||||
|
||||
@@ -15,22 +16,18 @@ export async function run(
|
||||
prompt: string
|
||||
): Promise<{ success: boolean; output?: string | undefined; error?: string | undefined }> {
|
||||
try {
|
||||
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
|
||||
|
||||
const tempDir = join(process.cwd(), ".temp");
|
||||
setupTestRepo({ tempDir, forceClean: true });
|
||||
|
||||
const originalCwd = process.cwd();
|
||||
process.chdir(tempDir);
|
||||
|
||||
log.info("🚀 Running action with prompt...");
|
||||
log.separator();
|
||||
log.box(prompt, { title: "Prompt" });
|
||||
log.separator();
|
||||
|
||||
const inputs: Inputs = {
|
||||
const inputs: Required<Inputs> = {
|
||||
prompt,
|
||||
anthropic_api_key: process.env.ANTHROPIC_API_KEY,
|
||||
agent: "claude",
|
||||
...flatMorph(agents, (_, agent) =>
|
||||
agent.apiKeyNames.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]])
|
||||
),
|
||||
};
|
||||
|
||||
const result = await main(inputs);
|
||||
@@ -86,7 +83,8 @@ Examples:
|
||||
if (args["--raw"]) {
|
||||
prompt = args["--raw"];
|
||||
} else {
|
||||
const filePath = args._[0] || "fixtures/basic.txt";
|
||||
const filePath = args._[0] || "basic.txt";
|
||||
|
||||
const ext = extname(filePath).toLowerCase();
|
||||
let resolvedPath: string;
|
||||
|
||||
|
||||
Generated
+154
-5
@@ -11,9 +11,12 @@ importers:
|
||||
'@actions/core':
|
||||
specifier: ^1.11.1
|
||||
version: 1.11.1
|
||||
'@actions/github':
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
'@anthropic-ai/claude-agent-sdk':
|
||||
specifier: ^0.1.26
|
||||
version: 0.1.30(zod@3.25.76)
|
||||
specifier: 0.1.37
|
||||
version: 0.1.37(zod@3.25.76)
|
||||
'@ark/fs':
|
||||
specifier: 0.53.0
|
||||
version: 0.53.0
|
||||
@@ -26,6 +29,9 @@ importers:
|
||||
'@octokit/webhooks-types':
|
||||
specifier: ^7.6.1
|
||||
version: 7.6.1
|
||||
'@openai/codex-sdk':
|
||||
specifier: 0.58.0
|
||||
version: 0.58.0
|
||||
'@standard-schema/spec':
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0
|
||||
@@ -44,6 +50,9 @@ importers:
|
||||
table:
|
||||
specifier: ^6.9.0
|
||||
version: 6.9.0
|
||||
zod:
|
||||
specifier: ^3.25.76
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.7.2
|
||||
@@ -69,14 +78,17 @@ packages:
|
||||
'@actions/exec@1.1.1':
|
||||
resolution: {integrity: sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==}
|
||||
|
||||
'@actions/github@6.0.1':
|
||||
resolution: {integrity: sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==}
|
||||
|
||||
'@actions/http-client@2.2.3':
|
||||
resolution: {integrity: sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==}
|
||||
|
||||
'@actions/io@1.1.3':
|
||||
resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==}
|
||||
|
||||
'@anthropic-ai/claude-agent-sdk@0.1.30':
|
||||
resolution: {integrity: sha512-lo1tqxCr2vygagFp6kUMHKSN6AAWlULCskwGKtLB/JcIXy/8H8GsLSKX54anTsvc9mBbCR8wWASdFmiiL9NSKA==}
|
||||
'@anthropic-ai/claude-agent-sdk@0.1.37':
|
||||
resolution: {integrity: sha512-LMfqMIPLTz0vRhpcO7hpPJ5L6Bg24y5/PaqZvwAUNZ/GR3OAl7xmJR7IryIR6m8Pyd/6Hs2yBU8j86Os+wHFQQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
zod: ^3.24.1
|
||||
@@ -318,10 +330,18 @@ packages:
|
||||
resolution: {integrity: sha512-kOQ4+fHuT4KbR2iq2IjeV32HiihueuOf1vJkq18z08CLZ1UQrTc8BXJpVfxZkq45+inLLD+D4xx4nBjUelJa4Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@octokit/auth-token@4.0.0':
|
||||
resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@octokit/auth-token@6.0.0':
|
||||
resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@octokit/core@5.2.2':
|
||||
resolution: {integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@octokit/core@7.0.5':
|
||||
resolution: {integrity: sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==}
|
||||
engines: {node: '>= 20'}
|
||||
@@ -330,10 +350,24 @@ packages:
|
||||
resolution: {integrity: sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@octokit/endpoint@9.0.6':
|
||||
resolution: {integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@octokit/graphql@7.1.1':
|
||||
resolution: {integrity: sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@octokit/graphql@9.0.2':
|
||||
resolution: {integrity: sha512-iz6KzZ7u95Fzy9Nt2L8cG88lGRMr/qy1Q36ih/XVzMIlPDMYwaNLE/ENhqmIzgPrlNWiYJkwmveEetvxAgFBJw==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@octokit/openapi-types@20.0.0':
|
||||
resolution: {integrity: sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==}
|
||||
|
||||
'@octokit/openapi-types@24.2.0':
|
||||
resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==}
|
||||
|
||||
'@octokit/openapi-types@26.0.0':
|
||||
resolution: {integrity: sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==}
|
||||
|
||||
@@ -343,18 +377,34 @@ packages:
|
||||
peerDependencies:
|
||||
'@octokit/core': '>=6'
|
||||
|
||||
'@octokit/plugin-paginate-rest@9.2.2':
|
||||
resolution: {integrity: sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
'@octokit/core': '5'
|
||||
|
||||
'@octokit/plugin-request-log@6.0.0':
|
||||
resolution: {integrity: sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==}
|
||||
engines: {node: '>= 20'}
|
||||
peerDependencies:
|
||||
'@octokit/core': '>=6'
|
||||
|
||||
'@octokit/plugin-rest-endpoint-methods@10.4.1':
|
||||
resolution: {integrity: sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
'@octokit/core': '5'
|
||||
|
||||
'@octokit/plugin-rest-endpoint-methods@16.1.0':
|
||||
resolution: {integrity: sha512-nCsyiKoGRnhH5LkH8hJEZb9swpqOcsW+VXv1QoyUNQXJeVODG4+xM6UICEqyqe9XFr6LkL8BIiFCPev8zMDXPw==}
|
||||
engines: {node: '>= 20'}
|
||||
peerDependencies:
|
||||
'@octokit/core': '>=6'
|
||||
|
||||
'@octokit/request-error@5.1.1':
|
||||
resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@octokit/request-error@7.0.1':
|
||||
resolution: {integrity: sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA==}
|
||||
engines: {node: '>= 20'}
|
||||
@@ -363,16 +413,30 @@ packages:
|
||||
resolution: {integrity: sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@octokit/request@8.4.1':
|
||||
resolution: {integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@octokit/rest@22.0.0':
|
||||
resolution: {integrity: sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@octokit/types@12.6.0':
|
||||
resolution: {integrity: sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==}
|
||||
|
||||
'@octokit/types@13.10.0':
|
||||
resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==}
|
||||
|
||||
'@octokit/types@15.0.0':
|
||||
resolution: {integrity: sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ==}
|
||||
|
||||
'@octokit/webhooks-types@7.6.1':
|
||||
resolution: {integrity: sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==}
|
||||
|
||||
'@openai/codex-sdk@0.58.0':
|
||||
resolution: {integrity: sha512-Z5vK294gUS9cn7bpU/lJtgzqJy1UqIGee7WMP+1Z4a6AxDcTxFCLZI4YkH9praJfrgoj5bFeu+3V9HIoBBTzcw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1':
|
||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
||||
|
||||
@@ -432,6 +496,9 @@ packages:
|
||||
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
before-after-hook@2.2.3:
|
||||
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
|
||||
|
||||
before-after-hook@4.0.0:
|
||||
resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==}
|
||||
|
||||
@@ -499,6 +566,9 @@ packages:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
deprecation@2.3.1:
|
||||
resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==}
|
||||
|
||||
dotenv@17.2.3:
|
||||
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -943,6 +1013,9 @@ packages:
|
||||
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
universal-user-agent@6.0.1:
|
||||
resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==}
|
||||
|
||||
universal-user-agent@7.0.3:
|
||||
resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==}
|
||||
|
||||
@@ -1030,6 +1103,16 @@ snapshots:
|
||||
dependencies:
|
||||
'@actions/io': 1.1.3
|
||||
|
||||
'@actions/github@6.0.1':
|
||||
dependencies:
|
||||
'@actions/http-client': 2.2.3
|
||||
'@octokit/core': 5.2.2
|
||||
'@octokit/plugin-paginate-rest': 9.2.2(@octokit/core@5.2.2)
|
||||
'@octokit/plugin-rest-endpoint-methods': 10.4.1(@octokit/core@5.2.2)
|
||||
'@octokit/request': 8.4.1
|
||||
'@octokit/request-error': 5.1.1
|
||||
undici: 5.29.0
|
||||
|
||||
'@actions/http-client@2.2.3':
|
||||
dependencies:
|
||||
tunnel: 0.0.6
|
||||
@@ -1037,7 +1120,7 @@ snapshots:
|
||||
|
||||
'@actions/io@1.1.3': {}
|
||||
|
||||
'@anthropic-ai/claude-agent-sdk@0.1.30(zod@3.25.76)':
|
||||
'@anthropic-ai/claude-agent-sdk@0.1.37(zod@3.25.76)':
|
||||
dependencies:
|
||||
zod: 3.25.76
|
||||
optionalDependencies:
|
||||
@@ -1198,8 +1281,20 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@octokit/auth-token@4.0.0': {}
|
||||
|
||||
'@octokit/auth-token@6.0.0': {}
|
||||
|
||||
'@octokit/core@5.2.2':
|
||||
dependencies:
|
||||
'@octokit/auth-token': 4.0.0
|
||||
'@octokit/graphql': 7.1.1
|
||||
'@octokit/request': 8.4.1
|
||||
'@octokit/request-error': 5.1.1
|
||||
'@octokit/types': 13.10.0
|
||||
before-after-hook: 2.2.3
|
||||
universal-user-agent: 6.0.1
|
||||
|
||||
'@octokit/core@7.0.5':
|
||||
dependencies:
|
||||
'@octokit/auth-token': 6.0.0
|
||||
@@ -1215,12 +1310,27 @@ snapshots:
|
||||
'@octokit/types': 15.0.0
|
||||
universal-user-agent: 7.0.3
|
||||
|
||||
'@octokit/endpoint@9.0.6':
|
||||
dependencies:
|
||||
'@octokit/types': 13.10.0
|
||||
universal-user-agent: 6.0.1
|
||||
|
||||
'@octokit/graphql@7.1.1':
|
||||
dependencies:
|
||||
'@octokit/request': 8.4.1
|
||||
'@octokit/types': 13.10.0
|
||||
universal-user-agent: 6.0.1
|
||||
|
||||
'@octokit/graphql@9.0.2':
|
||||
dependencies:
|
||||
'@octokit/request': 10.0.5
|
||||
'@octokit/types': 15.0.0
|
||||
universal-user-agent: 7.0.3
|
||||
|
||||
'@octokit/openapi-types@20.0.0': {}
|
||||
|
||||
'@octokit/openapi-types@24.2.0': {}
|
||||
|
||||
'@octokit/openapi-types@26.0.0': {}
|
||||
|
||||
'@octokit/plugin-paginate-rest@13.2.0(@octokit/core@7.0.5)':
|
||||
@@ -1228,15 +1338,31 @@ snapshots:
|
||||
'@octokit/core': 7.0.5
|
||||
'@octokit/types': 15.0.0
|
||||
|
||||
'@octokit/plugin-paginate-rest@9.2.2(@octokit/core@5.2.2)':
|
||||
dependencies:
|
||||
'@octokit/core': 5.2.2
|
||||
'@octokit/types': 12.6.0
|
||||
|
||||
'@octokit/plugin-request-log@6.0.0(@octokit/core@7.0.5)':
|
||||
dependencies:
|
||||
'@octokit/core': 7.0.5
|
||||
|
||||
'@octokit/plugin-rest-endpoint-methods@10.4.1(@octokit/core@5.2.2)':
|
||||
dependencies:
|
||||
'@octokit/core': 5.2.2
|
||||
'@octokit/types': 12.6.0
|
||||
|
||||
'@octokit/plugin-rest-endpoint-methods@16.1.0(@octokit/core@7.0.5)':
|
||||
dependencies:
|
||||
'@octokit/core': 7.0.5
|
||||
'@octokit/types': 15.0.0
|
||||
|
||||
'@octokit/request-error@5.1.1':
|
||||
dependencies:
|
||||
'@octokit/types': 13.10.0
|
||||
deprecation: 2.3.1
|
||||
once: 1.4.0
|
||||
|
||||
'@octokit/request-error@7.0.1':
|
||||
dependencies:
|
||||
'@octokit/types': 15.0.0
|
||||
@@ -1249,6 +1375,13 @@ snapshots:
|
||||
fast-content-type-parse: 3.0.0
|
||||
universal-user-agent: 7.0.3
|
||||
|
||||
'@octokit/request@8.4.1':
|
||||
dependencies:
|
||||
'@octokit/endpoint': 9.0.6
|
||||
'@octokit/request-error': 5.1.1
|
||||
'@octokit/types': 13.10.0
|
||||
universal-user-agent: 6.0.1
|
||||
|
||||
'@octokit/rest@22.0.0':
|
||||
dependencies:
|
||||
'@octokit/core': 7.0.5
|
||||
@@ -1256,12 +1389,22 @@ snapshots:
|
||||
'@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.5)
|
||||
'@octokit/plugin-rest-endpoint-methods': 16.1.0(@octokit/core@7.0.5)
|
||||
|
||||
'@octokit/types@12.6.0':
|
||||
dependencies:
|
||||
'@octokit/openapi-types': 20.0.0
|
||||
|
||||
'@octokit/types@13.10.0':
|
||||
dependencies:
|
||||
'@octokit/openapi-types': 24.2.0
|
||||
|
||||
'@octokit/types@15.0.0':
|
||||
dependencies:
|
||||
'@octokit/openapi-types': 26.0.0
|
||||
|
||||
'@octokit/webhooks-types@7.6.1': {}
|
||||
|
||||
'@openai/codex-sdk@0.58.0': {}
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
@@ -1325,6 +1468,8 @@ snapshots:
|
||||
|
||||
astral-regex@2.0.0: {}
|
||||
|
||||
before-after-hook@2.2.3: {}
|
||||
|
||||
before-after-hook@4.0.0: {}
|
||||
|
||||
body-parser@2.2.0:
|
||||
@@ -1392,6 +1537,8 @@ snapshots:
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
deprecation@2.3.1: {}
|
||||
|
||||
dotenv@17.2.3: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
@@ -1877,6 +2024,8 @@ snapshots:
|
||||
|
||||
unicorn-magic@0.3.0: {}
|
||||
|
||||
universal-user-agent@6.0.1: {}
|
||||
|
||||
universal-user-agent@7.0.3: {}
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { spawnSync } from "child_process";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
function findCliPath(name: string): string | null {
|
||||
|
||||
const result = spawnSync("which", [name], { encoding: "utf-8" });
|
||||
if (result.status === 0 && result.stdout) {
|
||||
const cliPath = result.stdout.trim();
|
||||
if (cliPath && existsSync(cliPath)) {
|
||||
return cliPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(findCliPath("codei"));
|
||||
@@ -1,8 +1,38 @@
|
||||
## CURRENT
|
||||
|
||||
[] gemini installation speed (bundle/esm.sh?) (TODO: SHAWN)
|
||||
[] handle defaulting agent name value (TODO: SHAWN)
|
||||
|
||||
[] test agent/mode combinations
|
||||
[] 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
|
||||
[] avoid passing all of process.env into agents: minimum # of vars
|
||||
[] toon encode in prompt
|
||||
|
||||
## MAYBE
|
||||
|
||||
[] investigate repo config file
|
||||
[] try to find heavy claude code user
|
||||
[] investigate including terminal output from bash commands as collapsed groups from claude
|
||||
[] test initialization trade offs for pullfrog.yml
|
||||
|
||||
## DONE
|
||||
|
||||
[x] investigate mcp naming convention
|
||||
[x] input just accepts one key for API key
|
||||
[x] look into trigger.yml without installation
|
||||
[x] cancel installation token at the end of github action
|
||||
[x] avoid exposing env adding ## SECURITY prompt
|
||||
[x] add modes to prompt
|
||||
[x] progressively update comment
|
||||
[] don't allow rejecting prs
|
||||
[] fix pnpm caching
|
||||
[] try to avoid claude narrating the initial comment
|
||||
[] fix prompt to avoid narration like "I just read all tools from MCP server"
|
||||
[] investigate including terminal output from bash commands as collapsed groups
|
||||
[] avoid exposing env
|
||||
[x] don't allow rejecting prs
|
||||
[x] fix pnpm caching
|
||||
[x] fix prompt to avoid narration like "I just read all tools from MCP server"
|
||||
[x] handle progressive comment updating from pullfrog mcp
|
||||
[x] jules/gemini support
|
||||
[x] standardize mcp server
|
||||
[x] entry.js
|
||||
[x] split up prompts, load dynamically based on mode
|
||||
[x] log.txt to stdout
|
||||
[x] rename mcp to use underscore
|
||||
[x] external.ts align to agents
|
||||
|
||||
+15
-15
@@ -2,22 +2,22 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"module": "NodeNext",
|
||||
"target": "ESNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ESNext"],
|
||||
"allowImportingTsExtensions": true,
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"declaration": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"stripInternal": true,
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"declaration": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"stripInternal": true,
|
||||
"moduleDetection": "force",
|
||||
"useUnknownInCatchVariables": true
|
||||
"useUnknownInCatchVariables": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { AgentName } from "../external.ts";
|
||||
import { log } from "./cli.ts";
|
||||
import type { RepoContext } from "./github.ts";
|
||||
|
||||
export interface Mode {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
export interface RepoSettings {
|
||||
defaultAgent: AgentName | null;
|
||||
webAccessLevel: "full_access" | "limited";
|
||||
webAccessAllowTrusted: boolean;
|
||||
webAccessDomains: string;
|
||||
modes: Mode[];
|
||||
}
|
||||
|
||||
export const DEFAULT_REPO_SETTINGS: RepoSettings = {
|
||||
defaultAgent: null,
|
||||
webAccessLevel: "full_access",
|
||||
webAccessAllowTrusted: false,
|
||||
webAccessDomains: "",
|
||||
modes: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch repository settings from the Pullfrog API
|
||||
* Returns defaults if repo doesn't exist or fetch fails
|
||||
*/
|
||||
export async function fetchRepoSettings({
|
||||
token,
|
||||
repoContext,
|
||||
}: {
|
||||
token: string;
|
||||
repoContext: RepoContext;
|
||||
}): Promise<RepoSettings> {
|
||||
log.info("Fetching repository settings...");
|
||||
const settings = await getRepoSettings(token, repoContext);
|
||||
log.info("Repository settings fetched");
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch repository settings from the Pullfrog API with fallback to defaults
|
||||
* Returns agent, permissions, and workflows (excludes triggers)
|
||||
* Returns defaults if repo doesn't exist or fetch fails
|
||||
*/
|
||||
export async function getRepoSettings(
|
||||
token: string,
|
||||
repoContext: RepoContext
|
||||
): Promise<RepoSettings> {
|
||||
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
|
||||
|
||||
// Add timeout to prevent hanging (5 seconds)
|
||||
const timeoutMs = 5000;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${apiUrl}/api/repo/${repoContext.owner}/${repoContext.name}/settings`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
signal: controller.signal,
|
||||
}
|
||||
);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
// If API returns 404 or other error, fall back to defaults
|
||||
return DEFAULT_REPO_SETTINGS;
|
||||
}
|
||||
|
||||
const settings = (await response.json()) as RepoSettings | null;
|
||||
|
||||
// If API returns null (repo doesn't exist), return defaults
|
||||
if (settings === null) {
|
||||
return DEFAULT_REPO_SETTINGS;
|
||||
}
|
||||
|
||||
return settings;
|
||||
} catch {
|
||||
clearTimeout(timeoutId);
|
||||
// If fetch fails (network error, timeout, etc.), fall back to defaults
|
||||
return DEFAULT_REPO_SETTINGS;
|
||||
}
|
||||
}
|
||||
+173
@@ -2,7 +2,11 @@
|
||||
* 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 { table } from "table";
|
||||
|
||||
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
||||
const isDebugEnabled = process.env.LOG_LEVEL === "debug";
|
||||
@@ -157,6 +161,43 @@ async function summaryTable(
|
||||
core.info(`\n${tableText}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a formatted table using the table package
|
||||
* Also logs to console and GitHub Actions summary
|
||||
*/
|
||||
async function printTable(
|
||||
rows: Array<Array<{ data: string; header?: boolean } | string>>,
|
||||
options?: {
|
||||
title?: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const { title } = options || {};
|
||||
|
||||
// Convert rows to string arrays for the table package
|
||||
const tableData = rows.map((row) =>
|
||||
row.map((cell) => {
|
||||
if (typeof cell === "string") {
|
||||
return cell;
|
||||
}
|
||||
return cell.data;
|
||||
})
|
||||
);
|
||||
|
||||
const formatted = table(tableData);
|
||||
|
||||
if (title) {
|
||||
core.info(`\n${title}`);
|
||||
}
|
||||
core.info(`\n${formatted}\n`);
|
||||
|
||||
if (isGitHubActions) {
|
||||
if (title) {
|
||||
core.summary.addRaw(`**${title}**\n\n`);
|
||||
}
|
||||
core.summary.addRaw(`\`\`\`\n${formatted}\n\`\`\`\n`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a separator line
|
||||
*/
|
||||
@@ -237,6 +278,11 @@ export const log = {
|
||||
*/
|
||||
summaryTable,
|
||||
|
||||
/**
|
||||
* Print a formatted table using the table package
|
||||
*/
|
||||
table: printTable,
|
||||
|
||||
/**
|
||||
* Print a separator line
|
||||
*/
|
||||
@@ -261,4 +307,131 @@ export const log = {
|
||||
* End a collapsed group
|
||||
*/
|
||||
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
|
||||
* @param name The name of the CLI executable to find
|
||||
* @returns The path to the CLI executable, or null if not found
|
||||
*/
|
||||
export function findCliPath(name: string): string | null {
|
||||
const result = spawnSync("which", [name], { encoding: "utf-8" });
|
||||
if (result.status === 0 && result.stdout) {
|
||||
const cliPath = result.stdout.trim();
|
||||
if (cliPath && existsSync(cliPath)) {
|
||||
return cliPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
+64
-23
@@ -62,25 +62,40 @@ async function acquireTokenViaOIDC(): Promise<string> {
|
||||
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
|
||||
|
||||
log.info("Exchanging OIDC token for installation token...");
|
||||
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${oidcToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorText = await tokenResponse.text();
|
||||
throw new Error(
|
||||
`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorText}`
|
||||
);
|
||||
// Add timeout to prevent long waits (5 seconds)
|
||||
const timeoutMs = 5000;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${oidcToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`);
|
||||
}
|
||||
|
||||
const tokenData = (await tokenResponse.json()) as InstallationToken;
|
||||
log.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
|
||||
|
||||
return tokenData.token;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
throw new Error(`Token exchange timed out after ${timeoutMs}ms`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const tokenData = (await tokenResponse.json()) as InstallationToken;
|
||||
log.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
|
||||
|
||||
return tokenData.token;
|
||||
}
|
||||
|
||||
const base64UrlEncode = (str: string): string => {
|
||||
@@ -234,21 +249,47 @@ async function acquireNewToken(): Promise<string> {
|
||||
|
||||
/**
|
||||
* Setup GitHub installation token for the action
|
||||
* Returns the token and whether it was acquired (needs revocation)
|
||||
*/
|
||||
export async function setupGitHubInstallationToken(): Promise<string> {
|
||||
export async function setupGitHubInstallationToken(): Promise<{
|
||||
githubInstallationToken: string;
|
||||
wasAcquired: boolean;
|
||||
}> {
|
||||
const existingToken = checkExistingToken();
|
||||
if (existingToken) {
|
||||
core.setSecret(existingToken);
|
||||
log.info("Using provided GitHub installation token");
|
||||
return existingToken;
|
||||
return { githubInstallationToken: existingToken, wasAcquired: false };
|
||||
}
|
||||
|
||||
const token = await acquireNewToken();
|
||||
const acquiredToken = await acquireNewToken();
|
||||
core.setSecret(acquiredToken);
|
||||
process.env.GITHUB_INSTALLATION_TOKEN = acquiredToken;
|
||||
|
||||
core.setSecret(token);
|
||||
process.env.GITHUB_INSTALLATION_TOKEN = token;
|
||||
return { githubInstallationToken: acquiredToken, wasAcquired: true };
|
||||
}
|
||||
|
||||
return token;
|
||||
/**
|
||||
* Revoke GitHub installation token
|
||||
*/
|
||||
export async function revokeInstallationToken(token: string): Promise<void> {
|
||||
const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
|
||||
|
||||
try {
|
||||
await fetch(`${apiUrl}/installation/token`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
});
|
||||
log.info("Installation token revoked");
|
||||
} catch (error) {
|
||||
log.warning(
|
||||
`Failed to revoke installation token: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface RepoContext {
|
||||
|
||||
+18
-3
@@ -41,11 +41,27 @@ export function setupTestRepo(options: SetupOptions): void {
|
||||
|
||||
/**
|
||||
* Setup git configuration to avoid identity errors
|
||||
* Only runs in GitHub Actions environment to avoid overwriting local git config
|
||||
*/
|
||||
export function setupGitConfig(): void {
|
||||
// Only set up git config in GitHub Actions environment
|
||||
// In local development, use the user's existing git config
|
||||
if (!process.env.GITHUB_ACTIONS) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("🔧 Setting up git configuration...");
|
||||
execSync('git config user.email "action@pullfrog.ai"', { stdio: "inherit" });
|
||||
execSync('git config user.name "Pullfrog Action"', { stdio: "inherit" });
|
||||
try {
|
||||
execSync('git config user.email "action@pullfrog.ai"', { stdio: "pipe" });
|
||||
execSync('git config user.name "Pullfrog Action"', { stdio: "pipe" });
|
||||
log.debug("setupGitConfig: ✓ Git configuration set successfully");
|
||||
} catch (error) {
|
||||
// If git config fails, log warning but don't fail the action
|
||||
// This can happen if we're not in a git repo or git isn't available
|
||||
log.warning(
|
||||
`Failed to set git config: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +72,6 @@ export function setupGitAuth(githubToken: string, repoContext: RepoContext): voi
|
||||
// Only set up git auth in GitHub Actions environment
|
||||
// In local testing, this would overwrite the real git remote with fake credentials
|
||||
if (!process.env.GITHUB_ACTIONS) {
|
||||
log.info("⚠️ Skipping git authentication setup (not in GitHub Actions)");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user