Compare commits

...

40 Commits

Author SHA1 Message Date
David Blass c647c923f3 fix instructions 2025-11-19 15:34:14 -05:00
David Blass c5700b195d todos 2025-11-19 15:01:52 -05:00
David Blass 849d133f20 payload.ts to external.ts 2025-11-19 14:08:59 -05:00
David Blass e477ad81b2 update todos, cleanup 2025-11-19 12:25:49 -05:00
Colin McDonnell 06a19567c0 Switch to payload 2025-11-18 23:15:44 -08:00
David Blass 3ef1635bb6 update todos 2025-11-18 20:24:12 -05:00
Shawn Morreau e455ec0682 add Cursor, fix Gemini 2025-11-18 20:23:50 -05:00
Shawn Morreau 7bbca2fdeb remove slop 2025-11-18 20:18:00 -05:00
Shawn Morreau 0ac4975b50 fix agents 2025-11-18 20:10:39 -05:00
Shawn Morreau bf6212cae3 undo david 2025-11-18 20:02:25 -05:00
Shawn Morreau 3982b147f9 log 2025-11-18 19:20:26 -05:00
Shawn Morreau c72d44382f logging 2025-11-18 19:02:47 -05:00
Shawn Morreau fc1b035f5d fix pnpm play for cursor with MCP access 2025-11-18 18:41:45 -05:00
Shawn Morreau 7ec4fd52b1 merge 2025-11-18 14:45:18 -05:00
ssalbdivad dbf906a7f0 use gemini cli instead of jules, iterate on mcp config 2025-11-18 14:42:07 -05:00
Shawn Morreau 68c38ed042 merge main 2025-11-18 11:28:35 -05:00
Colin McDonnell c63581a90c tweak 2025-11-18 08:27:01 -08:00
Shawn Morreau e218afc35c continue 2025-11-18 11:26:41 -05:00
Colin McDonnell ccf740bfdf Tweak 2025-11-14 17:25:10 -08:00
Colin McDonnell f45b6dca62 gitattr 2025-11-14 16:53:49 -08:00
David Blass c766daefa4 broken jules 2025-11-14 17:00:58 -05:00
Shawn Morreau 50c0095e87 merge main 2025-11-14 16:14:36 -05:00
Shawn Morreau 49cb159124 continue 2025-11-14 16:13:50 -05:00
David Blass ddb481f14e bump version 2025-11-14 16:13:31 -05:00
David Blass 1b55da51a1 inputKeys array, missing key error message 2025-11-14 16:12:32 -05:00
Shawn Morreau b2a9b60271 first iteration of pnpm play working 2025-11-14 16:03:19 -05:00
David Blass 7c724d931b gemini_api_key 2025-11-14 15:41:55 -05:00
David Blass 57e72ddf2b iterate on jules 2025-11-14 15:40:15 -05:00
Shawn Morreau 41a4f44e2d merge main 2025-11-14 14:28:36 -05:00
Shawn Morreau d1f16e9dd2 begin cursor 2025-11-14 14:27:40 -05:00
David Blass 6f2ccedbf8 begin jules support, derive inputs 2025-11-14 14:27:00 -05:00
David Blass d4a4dd59bb use working comment 2025-11-14 14:01:44 -05:00
David Blass 1044806f8e tweak prompt 2025-11-14 11:27:11 -05:00
David Blass d7fec83b6b update prompt 2025-11-14 11:22:13 -05:00
Colin McDonnell 9dff727df1 Fix outer build 2025-11-13 22:23:42 -08:00
Colin McDonnell 47716aa119 Fix outer build 2025-11-13 17:16:36 -08:00
David Blass cb01f0ae44 include openai_api_key from github action 2025-11-13 17:09:16 -05:00
David Blass 75cb3ecf08 add openai input 2025-11-13 16:37:27 -05:00
David Blass 4530267429 bump version 2025-11-13 16:17:10 -05:00
David Blass c1014857e0 update husky 2025-11-13 16:16:53 -05:00
27 changed files with 9547 additions and 8245 deletions
+2
View File
@@ -0,0 +1,2 @@
# Mark generated files as linguist-generated to exclude from language statistics
entry.js linguist-detectable=false
+9 -9
View File
@@ -1,12 +1,12 @@
# 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
echo "🔒 Updating lockfile..."
pnpm lock
# Build the action before committing
echo "🔨 Building action..."
pnpm build
# Add the built files and lockfile to the commit
git add entry.js mcp-server.js pnpm-lock.yaml
fi
# Build the action before committing
echo "🔨 Building action..."
pnpm build
# Add the built files and lockfile to the commit
git add entry.js mcp-server.js pnpm-lock.yaml
+1 -1
View File
@@ -11,7 +11,7 @@ GitHub Action for running Claude Code and other agents via Pullfrog.
pnpm install
```
## Testing with play.ts
## Testing with `play.ts`
```bash
pnpm play # Uses fixtures/play.txt
+12
View File
@@ -10,6 +10,18 @@ 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"
+5 -3
View File
@@ -6,7 +6,7 @@ import { agent, installFromNpmTarball } from "./shared.ts";
export const claude = agent({
name: "claude",
inputKey: "anthropic_api_key",
inputKeys: ["anthropic_api_key"],
install: async () => {
const versionRange = packageJson.dependencies["@anthropic-ai/claude-agent-sdk"] || "latest";
return await installFromNpmTarball({
@@ -15,11 +15,13 @@ export const claude = agent({
executablePath: "cli.js",
});
},
run: async ({ prompt, mcpServers, apiKey, cliPath }) => {
run: async ({ payload, mcpServers, apiKey, cliPath }) => {
process.env.ANTHROPIC_API_KEY = apiKey;
const prompt = addInstructions(payload);
const queryInstance = query({
prompt: addInstructions(prompt),
prompt,
options: {
permissionMode: "bypassPermissions",
mcpServers,
+12 -33
View File
@@ -1,13 +1,12 @@
import { spawnSync } from "node:child_process";
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { Codex, type CodexOptions, type ThreadEvent } from "@openai/codex-sdk";
import { log } from "../utils/cli.ts";
import { addInstructions } from "./instructions.ts";
import { agent, installFromNpmTarball } from "./shared.ts";
import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./shared.ts";
export const codex = agent({
name: "codex",
inputKey: "openai_api_key",
inputKeys: ["openai_api_key"],
install: async () => {
return await installFromNpmTarball({
packageName: "@openai/codex",
@@ -15,13 +14,11 @@ export const codex = agent({
executablePath: "bin/codex.js",
});
},
run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => {
run: async ({ payload, mcpServers, apiKey, cliPath, githubInstallationToken }) => {
process.env.OPENAI_API_KEY = apiKey;
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
// Configure MCP servers for Codex (global config is fine - not part of repo)
if (mcpServers && Object.keys(mcpServers).length > 0) {
configureMcpServers({ mcpServers, apiKey, cliPath });
}
configureCodexMcpServers({ mcpServers, cliPath });
// Configure Codex
const codexOptions: CodexOptions = {
@@ -42,7 +39,7 @@ export const codex = agent({
try {
// Use runStreamed to get streaming events similar to claude.ts
const streamedTurn = await thread.runStreamed(addInstructions(prompt));
const streamedTurn = await thread.runStreamed(addInstructions(payload));
// Stream events and handle them
let finalOutput = "";
@@ -160,33 +157,19 @@ const messageHandlers: {
},
};
function configureMcpServers({
mcpServers,
apiKey,
cliPath,
}: {
mcpServers: Record<string, McpServerConfig>;
apiKey: string;
cliPath: string;
}): void {
log.info("Configuring MCP servers for Codex...");
/**
* 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)) {
// Only configure stdio servers (Codex CLI supports stdio MCP servers)
// Check if it's a stdio server config (has 'command' property)
if (!("command" in serverConfig)) {
log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`);
continue;
}
// Build command and args
const command = serverConfig.command;
const args = serverConfig.args || [];
const envVars = serverConfig.env || {};
// Build the codex mcp add command with proper argument handling
const addArgs = ["mcp", "add", serverName];
// Add environment variables as --env flags
// Add environment variables as --env flags first
for (const [key, value] of Object.entries(envVars)) {
addArgs.push("--env", `${key}=${value}`);
}
@@ -197,10 +180,6 @@ function configureMcpServers({
const addResult = spawnSync("node", [cliPath, ...addArgs], {
stdio: "pipe",
encoding: "utf-8",
env: {
...process.env,
OPENAI_API_KEY: apiKey,
},
});
if (addResult.status !== 0) {
+130
View File
@@ -0,0 +1,130 @@
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",
inputKeys: ["cursor_api_key"],
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");
}
+115
View File
@@ -0,0 +1,115 @@
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",
inputKeys: ["google_api_key", "gemini_api_key"],
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`);
}
}
+5 -1
View File
@@ -1,9 +1,13 @@
import { claude } from "./claude.ts";
import { codex } from "./codex.ts";
import { cursor } from "./cursor.ts";
import { gemini } from "./gemini.ts";
export const agents = {
claude,
codex,
cursor,
gemini,
} as const;
export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKey"];
export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKeys"][number];
+23 -16
View File
@@ -1,17 +1,20 @@
import { ghPullfrogMcpName } from "../mcp/config.ts";
import type { Payload } from "../external.ts";
import { ghPullfrogMcpName } from "../mcp/index.ts";
import { modes } from "../modes.ts";
export const instructions = `
export const addInstructions = (payload: Payload) =>
`************* GENERAL INSTRUCTIONS *************
# General instructions
You are a highly intelligent, no-nonsense senior-level software engineering agent.
You will perform the task that is asked of you in the prompt below.
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 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.
Make reasonable assumptions when details are missing.
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
@@ -34,21 +37,25 @@ If asked to show environment variables, only display non-sensitive system variab
## 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.
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
choose the appropriate mode based on the prompt payload:
${modes.map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
${[...modes, ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
## Modes
${modes.map((w) => `### ${w.name}\n\n${w.prompt}`).join("\n\n")}
`;
${[...modes, ...payload.modes].map((w) => `### ${w.name}\n\n${w.prompt}`).join("\n\n")}
export const addInstructions = (prompt: string) =>
`****** GENERAL INSTRUCTIONS ******\n${instructions}\n\n****** USER PROMPT ******\n${prompt}`;
************* USER PROMPT *************
${payload.prompt}
${JSON.stringify(payload.event, null, 2)}`;
+129 -13
View File
@@ -4,7 +4,8 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pipeline } from "node:stream/promises";
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import type { McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
import type { Payload } from "../external.ts";
import { log } from "../utils/cli.ts";
/**
@@ -23,11 +24,45 @@ export interface AgentResult {
export interface AgentConfig {
apiKey: string;
githubInstallationToken: string;
prompt: string;
mcpServers: Record<string, McpServerConfig>;
payload: Payload;
mcpServers: Record<string, McpStdioServerConfig>;
cliPath: string;
}
/**
* 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
@@ -37,11 +72,8 @@ export async function installFromNpmTarball({
packageName,
version,
executablePath,
}: {
packageName: string;
version: string;
executablePath: string;
}): Promise<string> {
installDependencies,
}: InstallFromNpmTarballParams): Promise<string> {
// Resolve version if it's a range or "latest"
let resolvedVersion = version;
if (version.startsWith("^") || version.startsWith("~") || version === "latest") {
@@ -52,10 +84,7 @@ export async function installFromNpmTarball({
if (!registryResponse.ok) {
throw new Error(`Failed to query registry: ${registryResponse.status}`);
}
const registryData = (await registryResponse.json()) as {
"dist-tags": { latest: string };
versions: Record<string, unknown>;
};
const registryData = (await registryResponse.json()) as NpmRegistryData;
resolvedVersion = registryData["dist-tags"].latest;
log.info(`Resolved to version ${resolvedVersion}`);
} catch (error) {
@@ -119,6 +148,22 @@ export async function installFromNpmTarball({
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);
@@ -127,13 +172,84 @@ export async function installFromNpmTarball({
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}...`);
// Derive temp directory prefix from executable name (sanitize similar to package name)
// Replace any special characters with - and ensure trailing -
const tempDirPrefix = executableName.replace(/[^a-zA-Z0-9]/g, "-") + "-";
// Create temp directory
const tempDir = await mkdtemp(join(tmpdir(), tempDirPrefix));
const installScriptPath = join(tempDir, "install.sh");
// 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...");
// 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 agent extends Agent>(agent: agent): agent => {
return agent;
};
export type Agent = {
name: string;
inputKey: string;
inputKeys: string[];
install: () => Promise<string>;
run: (config: AgentConfig) => Promise<AgentResult>;
};
+8511 -8122
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -5,7 +5,9 @@
*/
import * as core from "@actions/core";
import { type Inputs, main } from "./main.ts";
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";
async function run(): Promise<void> {
@@ -18,9 +20,12 @@ async function run(): Promise<void> {
}
try {
const inputs: Inputs = {
const inputs: Required<Inputs> = {
prompt: core.getInput("prompt", { required: true }),
anthropic_api_key: core.getInput("anthropic_api_key") || undefined,
agent: core.getInput("agent") ? AgentName.assert(core.getInput("agent")) : undefined,
...flatMorph(agents, (_, agent) =>
agent.inputKeys.map((inputKey) => [inputKey, core.getInput(inputKey)])
),
};
const result = await main(inputs);
+38
View File
@@ -0,0 +1,38 @@
/** May be a `github.event` payload that has been stringified. This case needs to be detected and handled appropriately. */
import type { AgentName } from "./main.ts";
import type { Mode } from "./modes.ts";
// type Payload = GithubEventPayload | WorkflowDispatchPayload;
// type GithubEventPayload = Record<string, any>;
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.
*/
readonly event: object;
/**
* 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
View File
@@ -1 +1 @@
choose a random animal emoji. add a comment to https://github.com/pullfrogai/scratch/issues/21 containing 50 of that emoji.
create a comment on https://github.com/pullfrogai/scratch/pull/29 that says ribbit
+87 -14
View File
@@ -1,12 +1,15 @@
import { flatMorph } from "@ark/util";
import { type } from "arktype";
import { agents } from "./agents/index.ts";
import type { 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,
type RepoContext,
revokeInstallationToken,
setupGitHubInstallationToken,
} from "./utils/github.ts";
@@ -16,19 +19,18 @@ export const AgentName = type.enumerated(...Object.values(agents).map((agent) =>
export type AgentName = typeof AgentName.infer;
export const AgentInputKey = type.enumerated(
...Object.values(agents).map((agent) => agent.inputKey)
...Object.values(agents).flatMap((agent) => agent.inputKeys)
);
export type AgentInputKey = typeof AgentInputKey.infer;
const keyInputDefs = flatMorph(
agents,
(_, agent) => [agent.inputKey, "string | undefined?"] as const
const keyInputDefs = flatMorph(agents, (_, agent) =>
agent.inputKeys.map((inputKey) => [inputKey, "string | undefined?"] as const)
);
export const Inputs = type({
prompt: "string",
...keyInputDefs,
"agent?": AgentName,
"agent?": AgentName.or("undefined"),
});
export type Inputs = typeof Inputs.infer;
@@ -39,7 +41,54 @@ export interface MainResult {
error?: string | undefined;
}
export type PromptJSON = {};
/**
* 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.inputKeys.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);
}
export async function main(inputs: Inputs): Promise<MainResult> {
let tokenToRevoke: string | null = null;
@@ -75,20 +124,44 @@ export async function main(inputs: Inputs): Promise<MainResult> {
const cliPath = await agent.install();
log.info(`Running ${agentName}...`);
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
let payload: Payload;
// Get API key based on agent type
try {
// attempt JSON parsing
const parsedPrompt = JSON.parse(inputs.prompt);
if (!("~pullfrog" in parsedPrompt)) {
// is non-pullfrog JSON (probably from a GitHub event), treat it as a plain text prompt
throw new Error();
}
payload = parsedPrompt as Payload;
} catch {
payload = {
"~pullfrog": true,
agent: null,
prompt: inputs.prompt,
event: {},
modes,
};
}
const apiKey = inputs[agent.inputKey];
log.box(payload.prompt, { title: "Prompt" });
if (!apiKey) throw new Error(`${agent.inputKey} is required for ${agentName} agent`);
const matchingInputKey = agent.inputKeys.find((inputKey) => inputs[inputKey]);
if (!matchingInputKey) {
throwMissingApiKeyError({
agentName,
inputKeys: agent.inputKeys,
repoContext,
inputs,
});
}
const apiKey = inputs[matchingInputKey]!;
const result = await agent.run({
prompt: inputs.prompt,
payload,
mcpServers,
githubInstallationToken,
apiKey,
+113 -1
View File
@@ -97760,6 +97760,10 @@ var schema = ark.schema;
var define2 = ark.define;
var declare = ark.declare;
// mcp/shared.ts
import { appendFileSync } from "node:fs";
import { join } from "node:path";
// node_modules/.pnpm/universal-user-agent@7.0.3/node_modules/universal-user-agent/index.js
function getUserAgent() {
if (typeof navigator === "object" && "userAgent" in navigator) {
@@ -101412,7 +101416,58 @@ var getMcpContext = cached2(() => {
})
};
});
var tool = (tool2) => tool2;
function getLogPath() {
return join(process.cwd(), "log.txt");
}
function logToolCall({
toolName,
request: request2,
error: error41,
success
}) {
const logPath = getLogPath();
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
const requestStr = JSON.stringify(request2, null, 2);
let logEntry = `[${timestamp}] Tool: ${toolName}
Request: ${requestStr}
`;
if (error41) {
const errorMessage = error41 instanceof Error ? error41.message : String(error41);
const errorStack = error41 instanceof Error ? error41.stack : void 0;
logEntry += `Error: ${errorMessage}
`;
if (errorStack) {
logEntry += `Stack: ${errorStack}
`;
}
logEntry += `Status: FAILED
`;
} else if (success !== void 0) {
logEntry += `Status: ${success ? "SUCCESS" : "FAILED"}
`;
}
logEntry += `${"=".repeat(80)}
`;
appendFileSync(logPath, logEntry, "utf-8");
}
var tool = (toolDef) => {
const toolName = toolDef.name;
const originalExecute = toolDef.execute;
toolDef.execute = async (args2, context) => {
try {
logToolCall({ toolName, request: args2 });
const result = await originalExecute(args2, context);
const isError = result && typeof result === "object" && "isError" in result && result.isError === true;
logToolCall({ toolName, request: args2, success: !isError });
return result;
} catch (error41) {
logToolCall({ toolName, request: args2, error: error41 });
throw error41;
}
};
return toolDef;
};
var addTools = (server2, tools) => {
for (const tool2 of tools) {
server2.addTool(tool2);
@@ -101499,6 +101554,61 @@ var EditCommentTool = tool({
};
})
});
var workingCommentId = null;
var 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}'"
)
});
var 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
});
workingCommentId = result.data.id;
return {
success: true,
commentId: result.data.id,
url: result.data.html_url,
body: result.data.body
};
})
});
var WorkingCommentUpdate = type({
body: type.string.describe("the new comment body content")
});
var 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
};
})
});
// mcp/issue.ts
var Issue = type({
@@ -101685,6 +101795,8 @@ var server = new FastMCP({
addTools(server, [
CreateCommentTool,
EditCommentTool,
CreateWorkingCommentTool,
UpdateWorkingCommentTool,
IssueTool,
PullRequestTool,
ReviewTool,
+66
View File
@@ -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,
});
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,
};
}),
});
+22 -4
View File
@@ -2,15 +2,15 @@
* Simple MCP configuration helper for adding our minimal GitHub comment server
*/
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import type { McpServerConfig, McpStdioServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { fromHere } from "@ark/fs";
import { log } from "../utils/cli.ts";
import { parseRepoContext } from "../utils/github.ts";
export const ghPullfrogMcpName = "gh-pullfrog";
import { ghPullfrogMcpName } from "./index.ts";
export type McpName = typeof ghPullfrogMcpName;
export type McpConfigs = Record<McpName, McpServerConfig>;
export type McpConfigs = Record<McpName, McpStdioServerConfig>;
export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
const repoContext = parseRepoContext();
@@ -31,3 +31,21 @@ export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
},
};
}
/**
* Iterate through MCP servers and call the provided handler for each stdio server
* Shared logic to avoid duplication across agents
*/
export function forEachStdioMcpServer(
mcpServers: Record<string, McpServerConfig>,
handler: (serverName: string, serverConfig: McpStdioServerConfig) => void
): void {
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
// Only configure stdio servers (CLIs support stdio MCP servers)
if (!("command" in serverConfig)) {
log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`);
continue;
}
handler(serverName, serverConfig);
}
}
+4
View File
@@ -0,0 +1,4 @@
// for reasos this can't live alongide config.ts
// because its imported into the frontend UI
// and we don't want to pull in FastMCP, etc
export const ghPullfrogMcpName = "gh-pullfrog";
+8 -1
View File
@@ -1,7 +1,12 @@
#!/usr/bin/env node
// Minimal GitHub Issue Comment MCP Server
import { FastMCP } from "fastmcp";
import { CreateCommentTool, EditCommentTool } from "./comment.ts";
import {
CreateCommentTool,
CreateWorkingCommentTool,
EditCommentTool,
UpdateWorkingCommentTool,
} from "./comment.ts";
import { IssueTool } from "./issue.ts";
import { PullRequestTool } from "./pr.ts";
import { PullRequestInfoTool } from "./prInfo.ts";
@@ -16,6 +21,8 @@ const server = new FastMCP({
addTools(server, [
CreateCommentTool,
EditCommentTool,
CreateWorkingCommentTool,
UpdateWorkingCommentTool,
IssueTool,
PullRequestTool,
ReviewTool,
+67 -1
View File
@@ -1,3 +1,5 @@
import { appendFileSync } from "node:fs";
import { join } from "node:path";
import { cached } from "@ark/util";
import { Octokit } from "@octokit/rest";
import type { StandardSchemaV1 } from "@standard-schema/spec";
@@ -30,7 +32,71 @@ export interface McpContext extends RepoContext {
octokit: Octokit;
}
export const tool = <const params>(tool: Tool<any, StandardSchemaV1<params>>) => tool;
/**
* Get the log file path
*/
function getLogPath(): string {
return join(process.cwd(), "log.txt");
}
/**
* Log MCP tool call information to log.txt
*/
function logToolCall({
toolName,
request,
error,
success,
}: {
toolName: string;
request: unknown;
error?: unknown;
success?: boolean;
}) {
const logPath = getLogPath();
const timestamp = new Date().toISOString();
const requestStr = JSON.stringify(request, null, 2);
let logEntry = `[${timestamp}] Tool: ${toolName}\nRequest: ${requestStr}\n`;
if (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
logEntry += `Error: ${errorMessage}\n`;
if (errorStack) {
logEntry += `Stack: ${errorStack}\n`;
}
logEntry += `Status: FAILED\n`;
} else if (success !== undefined) {
logEntry += `Status: ${success ? "SUCCESS" : "FAILED"}\n`;
}
logEntry += `${"=".repeat(80)}\n\n`;
appendFileSync(logPath, logEntry, "utf-8");
}
export const tool = <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => {
// 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 {
logToolCall({ toolName, request: args });
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;
logToolCall({ toolName, request: args, success: !isError });
return result;
} catch (error) {
logToolCall({ toolName, request: args, error });
throw error;
}
};
return toolDef;
};
export const addTools = (server: FastMCP, tools: Tool<any, any>[]) => {
for (const tool of tools) {
+21 -15
View File
@@ -1,54 +1,60 @@
import { ghPullfrogMcpName } from "./mcp/config.ts";
import { ghPullfrogMcpName } from "./mcp/index.ts";
export const modes = [
export interface Mode {
name: string;
description: string;
prompt: string;
}
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. Create initial response comment using ${ghPullfrogMcpName}/create_issue_comment saying "I'll {summary of request}" and save the commentId
1. Create initial response comment using ${ghPullfrogMcpName}/create_working_comment saying "I'll {summary of request}"
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}/edit_issue_comment with the commentId to present the plan in a clear, organized format
7. Continue updating the same comment as needed (never create additional comments - always use edit_issue_comment)`,
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. Create initial response comment using ${ghPullfrogMcpName}/create_issue_comment saying "I'll {summary of request}" and save the commentId
1. Create initial response comment using ${ghPullfrogMcpName}/create_working_comment saying "I'll {summary of request}"
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}/edit_issue_comment with the commentId to share progress and results
7. Continue updating the same comment as you make progress (never create additional comments - always use edit_issue_comment)`,
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. Create initial response comment using ${ghPullfrogMcpName}/create_issue_comment saying "I'll review this" and save the commentId
1. Create initial response comment using ${ghPullfrogMcpName}/create_working_comment saying "I'll review this"
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}/edit_issue_comment with findings as you review
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 edit_issue_comment)`,
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. Create an initial "Progress Comment" using ${ghPullfrogMcpName}/create_issue_comment saying "I'll {summary of request}" and save the commentId
1. Create an initial "Working Comment" using ${ghPullfrogMcpName}/create_working_comment saying "I'll {summary of request}"
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 Progress Comment to share progress and results. Using ${ghPullfrogMcpName}/edit_issue_comment and the commentId you saved earlier. Do not create additional comments unless you are explicitly asked to do so.
4. When you finish the task, update the Progress Comment a final time to confirm completion. If you created any issues, PRs, etc, include appropriate links to it here.`,
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.`,
},
] as const;
];
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@pullfrog/action",
"version": "0.0.98",
"version": "0.0.106",
"type": "module",
"files": [
"index.js",
@@ -24,12 +24,13 @@
},
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.1",
"@anthropic-ai/claude-agent-sdk": "0.1.37",
"@openai/codex-sdk": "0.58.0",
"@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",
+7 -4
View File
@@ -2,8 +2,10 @@ 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 { log } from "./utils/cli.ts";
import { setupTestRepo } from "./utils/setup.ts";
@@ -20,11 +22,12 @@ export async function run(
const originalCwd = process.cwd();
process.chdir(tempDir);
const inputs: Inputs = {
const inputs: Required<Inputs> = {
prompt,
openai_api_key: process.env.OPENAI_API_KEY,
anthropic_api_key: process.env.ANTHROPIC_API_KEY,
agent: "codex",
agent: "claude",
...flatMorph(agents, (_, agent) =>
agent.inputKeys.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]])
),
};
const result = await main(inputs);
+137
View File
@@ -11,6 +11,9 @@ 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.37
version: 0.1.37(zod@3.25.76)
@@ -75,6 +78,9 @@ 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==}
@@ -324,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'}
@@ -336,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==}
@@ -349,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'}
@@ -369,10 +413,20 @@ 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==}
@@ -442,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==}
@@ -509,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'}
@@ -953,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==}
@@ -1040,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
@@ -1208,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
@@ -1225,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)':
@@ -1238,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
@@ -1259,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
@@ -1266,6 +1389,14 @@ 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
@@ -1337,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:
@@ -1404,6 +1537,8 @@ snapshots:
depd@2.0.0: {}
deprecation@2.3.1: {}
dotenv@17.2.3: {}
dunder-proto@1.0.1:
@@ -1889,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: {}
+11 -1
View File
@@ -1,6 +1,12 @@
## CURRENT
[] handle progressive comment updating from pullfrog mcp
[] gemini installation speed (bundle/esm.sh?)
[] entry.js
[] handle defaulting agent name value
[] 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
[] log.txt to stdout
## MAYBE
@@ -8,6 +14,7 @@
[] 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
[] split up prompts, load dynamically based on mode
## DONE
@@ -21,3 +28,6 @@
[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