Compare commits

..

33 Commits

Author SHA1 Message Date
David Blass 4b43b617f0 rename bundle without .js, bump version 2025-11-19 17:05:04 -05:00
David Blass 1e8abe442b remove .js 2025-11-19 16:55:39 -05:00
David Blass fed62adb69 try removing 2025-11-19 16:50:06 -05:00
David Blass 5889d20930 switch back to js 2025-11-19 16:31:13 -05:00
David Blass dcc257ff7a remove js suffix 2025-11-19 16:22:01 -05:00
David Blass 2ba6cf7c0b rename entry.js to entry 2025-11-19 16:18:59 -05:00
David Blass aa5eb4c43c update todos and cleanup 2025-11-19 15:47:42 -05:00
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
Shawn Morreau b2a9b60271 first iteration of pnpm play working 2025-11-14 16:03:19 -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
23 changed files with 1268 additions and 592 deletions
+1 -1
View File
@@ -8,5 +8,5 @@ if git diff --cached --name-only | grep -q "^package.json$"; then
pnpm build
# Add the built files and lockfile to the commit
git add entry.js mcp-server.js pnpm-lock.yaml
git add entry mcp-server pnpm-lock.yaml
fi
+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
+1 -1
View File
@@ -25,7 +25,7 @@ inputs:
runs:
using: "node20"
main: "entry.js"
main: "entry"
branding:
icon: "code"
+4 -2
View File
@@ -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,
+11 -32
View File
@@ -1,9 +1,8 @@
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",
@@ -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`);
}
}
+4 -2
View File
@@ -1,11 +1,13 @@
import { claude } from "./claude.ts";
import { codex } from "./codex.ts";
import { jules } from "./jules.ts";
import { cursor } from "./cursor.ts";
import { gemini } from "./gemini.ts";
export const agents = {
claude,
codex,
jules,
cursor,
gemini,
} as const;
export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKeys"][number];
+12 -10
View File
@@ -1,20 +1,20 @@
import type { Payload } from "../external.ts";
import { ghPullfrogMcpName } from "../mcp/index.ts";
import { modes } from "../modes.ts";
const userPromptHeader = `****** USER PROMPT ******\n`;
export const instructions = `
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 that is asked of you below ${userPromptHeader}.
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.
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
@@ -48,12 +48,14 @@ Ensure after your edits are done, your final comments do not contain intermediat
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${userPromptHeader}${prompt}`;
************* USER PROMPT *************
${payload.prompt}
${JSON.stringify(payload.event, null, 2)}`;
-174
View File
@@ -1,174 +0,0 @@
import { log } from "../utils/cli.ts";
import { parseRepoContext } from "../utils/github.ts";
import { spawn } from "../utils/subprocess.ts";
import { addInstructions } from "./instructions.ts";
import { agent, installFromNpmTarball } from "./shared.ts";
export const jules = agent({
name: "jules",
inputKeys: ["google_api_key", "gemini_api_key"],
install: async () => {
return await installFromNpmTarball({
packageName: "@google/jules",
version: "latest",
executablePath: "run.cjs",
});
},
run: async ({
prompt,
apiKey,
mcpServers: _mcpServers,
githubInstallationToken: _githubInstallationToken,
cliPath,
}) => {
if (!apiKey) {
throw new Error("google_api_key is required for jules agent");
}
process.env.GOOGLE_API_KEY = apiKey;
const repoContext = parseRepoContext();
const repoName = `${repoContext.owner}/${repoContext.name}`;
log.info(`Creating Jules session for ${repoName}...`);
// Create a new remote session
const sessionPrompt = addInstructions(prompt);
log.info(`Starting session with prompt: ${prompt.substring(0, 100)}...`);
let sessionId: string | undefined;
try {
const createResult = await spawn({
cmd: "node",
args: [cliPath, "remote", "new", "--repo", repoName, "--session", sessionPrompt],
onStdout: (chunk) => {
log.info(chunk.trim());
// Try to extract session ID from output
const match = chunk.match(/session[:\s]+(\d+)/i) || chunk.match(/id[:\s]+(\d+)/i);
if (match && !sessionId) {
sessionId = match[1];
log.info(`✓ Session ID: ${sessionId}`);
}
},
onStderr: (chunk) => {
log.warning(chunk.trim());
},
});
if (createResult.exitCode !== 0) {
throw new Error(
`Failed to create Jules session: ${createResult.stderr || createResult.stdout || "Unknown error"}`
);
}
// If session ID wasn't extracted from stdout, try to parse it
if (!sessionId) {
const output = createResult.stdout + createResult.stderr;
const match = output.match(/session[:\s]+(\d+)/i) || output.match(/id[:\s]+(\d+)/i);
if (match) {
sessionId = match[1];
}
}
if (!sessionId) {
log.warning("Could not extract session ID from output. Session may have been created.");
log.info(`Output: ${createResult.stdout}`);
} else {
log.info(`✓ Session created: ${sessionId}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Failed to create Jules session: ${errorMessage}`);
return {
success: false,
error: errorMessage,
output: "",
};
}
// Monitor session progress by polling session list
log.info("Monitoring session progress...");
let finalOutput = "";
const maxPollAttempts = 300; // ~50 minutes max (10 second intervals)
let pollAttempts = 0;
while (pollAttempts < maxPollAttempts) {
await new Promise((resolve) => setTimeout(resolve, 10000)); // Wait 10 seconds between polls
pollAttempts++;
try {
// List sessions to check status
const listResult = await spawn({
cmd: "node",
args: [cliPath, "remote", "list", "--session"],
onStdout: (chunk) => {
// Log session updates
const trimmed = chunk.trim();
if (trimmed) {
log.info(trimmed);
}
},
});
if (listResult.exitCode === 0) {
const output = listResult.stdout;
// Check if our session is complete
// The CLI output format may vary, so we look for completion indicators
if (sessionId && output.includes(sessionId)) {
// Try to determine if session is complete
// This is a heuristic - the actual output format may differ
if (
output.includes("completed") ||
output.includes("done") ||
output.includes("finished")
) {
log.info("Session appears to be completed");
finalOutput = "Session completed. Pulling results...";
break;
}
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.warning(`Error checking session status: ${errorMessage}`);
}
}
// Pull results if session ID is available
if (sessionId) {
try {
log.info(`Pulling results for session ${sessionId}...`);
const pullResult = await spawn({
cmd: "node",
args: [cliPath, "remote", "pull", "--session", sessionId],
onStdout: (chunk) => {
log.info(chunk.trim());
},
onStderr: (chunk) => {
log.warning(chunk.trim());
},
});
if (pullResult.exitCode === 0) {
finalOutput = pullResult.stdout || "Results pulled successfully.";
} else {
log.warning(`Failed to pull results: ${pullResult.stderr || pullResult.stdout}`);
finalOutput = finalOutput || "Session completed. Check Jules dashboard for results.";
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.warning(`Error pulling results: ${errorMessage}`);
}
}
if (pollAttempts >= maxPollAttempts) {
log.warning("Session monitoring timeout reached. Session may still be in progress.");
finalOutput =
finalOutput || "Session monitoring timeout. Check Jules dashboard for session status.";
}
return {
success: true,
output: finalOutput || "Jules session completed. Check the Jules dashboard for results.",
};
},
});
+128 -12
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,6 +172,77 @@ 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;
};
+459 -337
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env node
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);
var __create = Object.create;
var __defProp = Object.defineProperty;
@@ -32889,7 +32888,7 @@ function query({
// package.json
var package_default = {
name: "@pullfrog/action",
version: "0.0.105",
version: "0.0.107",
type: "module",
files: [
"index.js",
@@ -32913,12 +32912,13 @@ var package_default = {
},
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",
@@ -33214,20 +33214,18 @@ var modes = [
];
// agents/instructions.ts
var userPromptHeader = `****** USER PROMPT ******
`;
var instructions = `
var addInstructions = (payload) => `************* GENERAL INSTRUCTIONS *************
# General instructions
You are a diligent, detail-oriented, no-nonsense software engineering agent.
You will perform the task that is asked of you below ${userPromptHeader}.
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.
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
@@ -33261,18 +33259,19 @@ Ensure after your edits are done, your final comments do not contain intermediat
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}
${[...modes, ...payload.modes].map((w) => `### ${w.name}
${w.prompt}`).join("\n\n")}
`;
var addInstructions = (prompt) => `****** GENERAL INSTRUCTIONS ******
${instructions}
${userPromptHeader}${prompt}`;
************* USER PROMPT *************
${payload.prompt}
${JSON.stringify(payload.event, null, 2)}`;
// agents/shared.ts
import { spawnSync } from "node:child_process";
@@ -33284,7 +33283,8 @@ import { pipeline } from "node:stream/promises";
async function installFromNpmTarball({
packageName,
version,
executablePath
executablePath,
installDependencies
}) {
let resolvedVersion = version;
if (version.startsWith("^") || version.startsWith("~") || version === "latest") {
@@ -33342,10 +33342,67 @@ async function installFromNpmTarball({
if (!existsSync2(cliPath)) {
throw new Error(`Executable not found in extracted package at ${cliPath}`);
}
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(`\u2713 Dependencies installed`);
}
chmodSync(cliPath, 493);
log.info(`\u2713 ${packageName} installed at ${cliPath}`);
return cliPath;
}
async function installFromCurl({
installUrl,
executableName
}) {
log.info(`\u{1F4E6} Installing ${executableName}...`);
const tempDirPrefix = executableName.replace(/[^a-zA-Z0-9]/g, "-") + "-";
const tempDir = await mkdtemp(join4(tmpdir(), tempDirPrefix));
const installScriptPath = join4(tempDir, "install.sh");
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 = createWriteStream2(installScriptPath);
await pipeline(installScriptResponse.body, fileStream);
log.info(`Downloaded install script to ${installScriptPath}`);
chmodSync(installScriptPath, 493);
log.info("Installing to temp directory...");
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}`
);
}
const cliPath = join4(tempDir, ".local", "bin", executableName);
if (!existsSync2(cliPath)) {
throw new Error(`Executable not found at ${cliPath}`);
}
chmodSync(cliPath, 493);
log.info(`\u2713 ${executableName} installed at ${cliPath}`);
return cliPath;
}
var agent = (agent2) => {
return agent2;
};
@@ -33362,10 +33419,11 @@ var 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,
@@ -33812,12 +33870,10 @@ var 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;
if (mcpServers && Object.keys(mcpServers).length > 0) {
configureMcpServers({ mcpServers, apiKey, cliPath });
}
configureCodexMcpServers({ mcpServers, cliPath });
const codexOptions = {
apiKey,
codexPathOverride: cliPath
@@ -33829,7 +33885,7 @@ var codex = agent({
networkAccessEnabled: true
});
try {
const streamedTurn = await thread.runStreamed(addInstructions(prompt));
const streamedTurn = await thread.runStreamed(addInstructions(payload));
let finalOutput = "";
for await (const event of streamedTurn.events) {
const handler = messageHandlers2[event.type];
@@ -33925,17 +33981,8 @@ var messageHandlers2 = {
log.error(`Error: ${event.message}`);
}
};
function configureMcpServers({
mcpServers,
apiKey,
cliPath
}) {
log.info("Configuring MCP servers for Codex...");
function configureCodexMcpServers({ mcpServers, cliPath }) {
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
if (!("command" in serverConfig)) {
log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`);
continue;
}
const command = serverConfig.command;
const args2 = serverConfig.args || [];
const envVars = serverConfig.env || {};
@@ -33947,11 +33994,7 @@ function configureMcpServers({
log.info(`Adding MCP server '${serverName}'...`);
const addResult = spawnSync2("node", [cliPath, ...addArgs], {
stdio: "pipe",
encoding: "utf-8",
env: {
...process.env,
OPENAI_API_KEY: apiKey
}
encoding: "utf-8"
});
if (addResult.status !== 0) {
throw new Error(
@@ -33962,197 +34005,116 @@ function configureMcpServers({
}
}
// utils/github.ts
var core2 = __toESM(require_core(), 1);
import { createSign } from "node:crypto";
function checkExistingToken() {
const inputToken = core2.getInput("github_installation_token");
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
return inputToken || envToken || null;
}
function isGitHubActionsEnvironment() {
return Boolean(process.env.GITHUB_ACTIONS);
}
async function acquireTokenViaOIDC() {
log.info("Generating OIDC token...");
const oidcToken = await core2.getIDToken("pullfrog-api");
log.info("OIDC token generated successfully");
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
log.info("Exchanging OIDC token for installation token...");
const timeoutMs = 5e3;
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
// agents/cursor.ts
import { spawn as spawn3 } from "node:child_process";
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
import { join as join5 } from "node:path";
var cursor = agent({
name: "cursor",
inputKeys: ["cursor_api_key"],
install: async () => {
return await installFromCurl({
installUrl: "https://cursor.com/install",
executableName: "cursor-agent"
});
clearTimeout(timeoutId);
if (!tokenResponse.ok) {
throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`);
}
const tokenData = await tokenResponse.json();
log.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
return tokenData.token;
} catch (error2) {
clearTimeout(timeoutId);
if (error2 instanceof Error && error2.name === "AbortError") {
throw new Error(`Token exchange timed out after ${timeoutMs}ms`);
}
throw error2;
}
}
var base64UrlEncode = (str) => {
return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
};
var generateJWT = (appId, privateKey) => {
const now = Math.floor(Date.now() / 1e3);
const payload = {
iat: now - 60,
exp: now + 5 * 60,
iss: appId
};
const header = {
alg: "RS256",
typ: "JWT"
};
const encodedHeader = base64UrlEncode(JSON.stringify(header));
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
const signaturePart = `${encodedHeader}.${encodedPayload}`;
const signature = createSign("RSA-SHA256").update(signaturePart).sign(privateKey, "base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
return `${signaturePart}.${signature}`;
};
var githubRequest = async (path4, options = {}) => {
const { method = "GET", headers = {}, body } = options;
const url2 = `https://api.github.com${path4}`;
const requestHeaders = {
Accept: "application/vnd.github.v3+json",
"User-Agent": "Pullfrog-Installation-Token-Generator/1.0",
...headers
};
const response = await fetch(url2, {
method,
headers: requestHeaders,
...body && { body }
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`GitHub API request failed: ${response.status} ${response.statusText}
${errorText}`
);
}
return response.json();
};
var checkRepositoryAccess = async (token, repoOwner, repoName) => {
try {
const response = await githubRequest("/installation/repositories", {
headers: { Authorization: `token ${token}` }
});
return response.repositories.some(
(repo) => repo.owner.login === repoOwner && repo.name === repoName
);
} catch {
return false;
}
};
var createInstallationToken = async (jwt, installationId) => {
const response = await githubRequest(
`/app/installations/${installationId}/access_tokens`,
{
method: "POST",
headers: { Authorization: `Bearer ${jwt}` }
}
);
return response.token;
};
var findInstallationId = async (jwt, repoOwner, repoName) => {
const installations = await githubRequest("/app/installations", {
headers: { Authorization: `Bearer ${jwt}` }
});
for (const installation of installations) {
},
run: async ({ payload, apiKey, cliPath, githubInstallationToken, mcpServers }) => {
process.env.CURSOR_API_KEY = apiKey;
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
configureCursorMcpServers({ mcpServers, cliPath });
try {
const tempToken = await createInstallationToken(jwt, installation.id);
const hasAccess = await checkRepositoryAccess(tempToken, repoOwner, repoName);
if (hasAccess) {
return installation.id;
}
} catch {
const fullPrompt = addInstructions(payload);
const tempDir = cliPath.split("/.local/bin/")[0];
log.info("Running Cursor CLI...");
return new Promise((resolve) => {
const child = spawn3(
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 = "";
child.on("spawn", () => {
log.debug("Cursor CLI process spawned");
});
child.stdout?.on("data", (data) => {
const text = data.toString();
stdout += text;
process.stdout.write(text);
});
child.stderr?.on("data", (data) => {
const text = data.toString();
stderr += text;
process.stderr.write(text);
log.warning(text);
});
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", (error2) => {
const errorMessage = error2.message || String(error2);
log.error(`Cursor CLI execution failed: ${errorMessage}`);
resolve({
success: false,
error: errorMessage,
output: stdout.trim()
});
});
});
} catch (error2) {
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
log.error(`Cursor execution failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
output: ""
};
}
}
throw new Error(
`No installation found with access to ${repoOwner}/${repoName}. Ensure the GitHub App is installed on the target repository.`
);
};
async function acquireTokenViaGitHubApp() {
const repoContext = parseRepoContext();
const config = {
appId: process.env.GITHUB_APP_ID,
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n"),
repoOwner: repoContext.owner,
repoName: repoContext.name
};
const jwt = generateJWT(config.appId, config.privateKey);
const installationId = await findInstallationId(jwt, config.repoOwner, config.repoName);
const token = await createInstallationToken(jwt, installationId);
return token;
}
async function acquireNewToken() {
if (isGitHubActionsEnvironment()) {
return await acquireTokenViaOIDC();
} else {
return await acquireTokenViaGitHubApp();
}
}
async function setupGitHubInstallationToken() {
const existingToken = checkExistingToken();
if (existingToken) {
core2.setSecret(existingToken);
log.info("Using provided GitHub installation token");
return { githubInstallationToken: existingToken, wasAcquired: false };
}
const acquiredToken = await acquireNewToken();
core2.setSecret(acquiredToken);
process.env.GITHUB_INSTALLATION_TOKEN = acquiredToken;
return { githubInstallationToken: acquiredToken, wasAcquired: true };
}
async function revokeInstallationToken(token) {
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 (error2) {
log.warning(
`Failed to revoke installation token: ${error2 instanceof Error ? error2.message : String(error2)}`
);
}
}
function parseRepoContext() {
const githubRepo = process.env.GITHUB_REPOSITORY;
if (!githubRepo) {
throw new Error("GITHUB_REPOSITORY environment variable is required");
}
const [owner, name] = githubRepo.split("/");
if (!owner || !name) {
throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`);
}
return { owner, name };
});
function configureCursorMcpServers({ mcpServers, cliPath }) {
const tempDir = cliPath.split("/.local/bin/")[0];
const cursorConfigDir = join5(tempDir, ".cursor");
const mcpConfigPath = join5(cursorConfigDir, "mcp.json");
mkdirSync2(cursorConfigDir, { recursive: true });
writeFileSync2(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), "utf-8");
}
// agents/gemini.ts
import { spawnSync as spawnSync3 } from "node:child_process";
// utils/subprocess.ts
import { spawn as nodeSpawn } from "node:child_process";
async function spawn3(options) {
async function spawn4(options) {
const { cmd, args: args2, env: env2, input, timeout, cwd: cwd4, onStdout, onStderr } = options;
const startTime = Date.now();
let stdoutBuffer = "";
@@ -34225,150 +34187,106 @@ async function spawn3(options) {
});
}
// agents/jules.ts
var jules = agent({
name: "jules",
// agents/gemini.ts
var gemini = agent({
name: "gemini",
inputKeys: ["google_api_key", "gemini_api_key"],
install: async () => {
return await installFromNpmTarball({
packageName: "@google/jules",
packageName: "@google/gemini-cli",
version: "latest",
executablePath: "run.cjs"
executablePath: "dist/index.js",
installDependencies: true
});
},
run: async ({
prompt,
apiKey,
mcpServers: _mcpServers,
githubInstallationToken: _githubInstallationToken,
cliPath
}) => {
run: async ({ payload, apiKey, mcpServers, githubInstallationToken, cliPath }) => {
configureGeminiMcpServers({ mcpServers, cliPath });
if (!apiKey) {
throw new Error("google_api_key is required for jules agent");
throw new Error("google_api_key or gemini_api_key is required for gemini agent");
}
process.env.GOOGLE_API_KEY = apiKey;
const repoContext = parseRepoContext();
const repoName = `${repoContext.owner}/${repoContext.name}`;
log.info(`Creating Jules session for ${repoName}...`);
const sessionPrompt = addInstructions(prompt);
log.info(`Starting session with prompt: ${prompt.substring(0, 100)}...`);
let sessionId;
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 createResult = await spawn3({
const result = await spawn4({
cmd: "node",
args: [cliPath, "remote", "new", "--repo", repoName, "--session", sessionPrompt],
args: [cliPath, "--yolo", "--output-format", "text", sessionPrompt],
env: {
GEMINI_API_KEY: apiKey,
GITHUB_INSTALLATION_TOKEN: githubInstallationToken
},
onStdout: (chunk) => {
log.info(chunk.trim());
const match2 = chunk.match(/session[:\s]+(\d+)/i) || chunk.match(/id[:\s]+(\d+)/i);
if (match2 && !sessionId) {
sessionId = match2[1];
log.info(`\u2713 Session ID: ${sessionId}`);
const trimmed = chunk.trim();
if (trimmed) {
log.info(trimmed);
finalOutput += trimmed + "\n";
}
},
onStderr: (chunk) => {
log.warning(chunk.trim());
const trimmed = chunk.trim();
if (trimmed) {
log.warning(trimmed);
finalOutput += trimmed + "\n";
}
}
});
if (createResult.exitCode !== 0) {
throw new Error(
`Failed to create Jules session: ${createResult.stderr || createResult.stdout || "Unknown error"}`
);
}
if (!sessionId) {
const output = createResult.stdout + createResult.stderr;
const match2 = output.match(/session[:\s]+(\d+)/i) || output.match(/id[:\s]+(\d+)/i);
if (match2) {
sessionId = match2[1];
}
}
if (!sessionId) {
log.warning("Could not extract session ID from output. Session may have been created.");
log.info(`Output: ${createResult.stdout}`);
} else {
log.info(`\u2713 Session created: ${sessionId}`);
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("\u2713 Gemini CLI completed successfully");
return {
success: true,
output: finalOutput
};
} catch (error2) {
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
log.error(`Failed to create Jules session: ${errorMessage}`);
log.error(`Failed to run Gemini CLI: ${errorMessage}`);
return {
success: false,
error: errorMessage,
output: ""
output: finalOutput || ""
};
}
log.info("Monitoring session progress...");
let finalOutput = "";
const maxPollAttempts = 300;
let pollAttempts = 0;
while (pollAttempts < maxPollAttempts) {
await new Promise((resolve) => setTimeout(resolve, 1e4));
pollAttempts++;
try {
const listResult = await spawn3({
cmd: "node",
args: [cliPath, "remote", "list", "--session"],
onStdout: (chunk) => {
const trimmed = chunk.trim();
if (trimmed) {
log.info(trimmed);
}
}
});
if (listResult.exitCode === 0) {
const output = listResult.stdout;
if (sessionId && output.includes(sessionId)) {
if (output.includes("completed") || output.includes("done") || output.includes("finished")) {
log.info("Session appears to be completed");
finalOutput = "Session completed. Pulling results...";
break;
}
}
}
} catch (error2) {
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
log.warning(`Error checking session status: ${errorMessage}`);
}
}
if (sessionId) {
try {
log.info(`Pulling results for session ${sessionId}...`);
const pullResult = await spawn3({
cmd: "node",
args: [cliPath, "remote", "pull", "--session", sessionId],
onStdout: (chunk) => {
log.info(chunk.trim());
},
onStderr: (chunk) => {
log.warning(chunk.trim());
}
});
if (pullResult.exitCode === 0) {
finalOutput = pullResult.stdout || "Results pulled successfully.";
} else {
log.warning(`Failed to pull results: ${pullResult.stderr || pullResult.stdout}`);
finalOutput = finalOutput || "Session completed. Check Jules dashboard for results.";
}
} catch (error2) {
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
log.warning(`Error pulling results: ${errorMessage}`);
}
}
if (pollAttempts >= maxPollAttempts) {
log.warning("Session monitoring timeout reached. Session may still be in progress.");
finalOutput = finalOutput || "Session monitoring timeout. Check Jules dashboard for session status.";
}
return {
success: true,
output: finalOutput || "Jules session completed. Check the Jules dashboard for results."
};
}
});
function configureGeminiMcpServers({ mcpServers, cliPath }) {
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
const command = serverConfig.command;
const args2 = serverConfig.args || [];
const envVars = serverConfig.env || {};
const addArgs = ["mcp", "add", serverName, command, ...args2];
for (const [key, value2] of Object.entries(envVars)) {
addArgs.push("--env", `${key}=${value2}`);
}
log.info(`Adding MCP server '${serverName}'...`);
const addResult = spawnSync3("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(`\u2713 MCP server '${serverName}' configured`);
}
}
// agents/index.ts
var agents = {
claude,
codex,
jules
cursor,
gemini
};
// node_modules/.pnpm/@ark+schema@0.53.0/node_modules/@ark/schema/out/shared/registry.js
@@ -42098,7 +42016,7 @@ var caller = (options = {}) => {
};
// node_modules/.pnpm/@ark+fs@0.53.0/node_modules/@ark/fs/out/fs.js
import { dirname as dirname2, join as join5, parse } from "node:path";
import { dirname as dirname2, join as join6, parse } from "node:path";
import * as process3 from "node:process";
import { URL as URL2, fileURLToPath as fileURLToPath4 } from "node:url";
var filePath = (path4) => {
@@ -42112,9 +42030,197 @@ var filePath = (path4) => {
return file;
};
var dirOfCaller = () => dirname2(filePath(caller({ methodName: "dirOfCaller", upStackBy: 1 }).file));
var fromHere = (...joinWith) => join5(dirOfCaller(), ...joinWith);
var fromHere = (...joinWith) => join6(dirOfCaller(), ...joinWith);
var fsRoot = parse(process3.cwd()).root;
// utils/github.ts
var core2 = __toESM(require_core(), 1);
import { createSign } from "node:crypto";
function checkExistingToken() {
const inputToken = core2.getInput("github_installation_token");
const envToken = process.env.GITHUB_INSTALLATION_TOKEN;
return inputToken || envToken || null;
}
function isGitHubActionsEnvironment() {
return Boolean(process.env.GITHUB_ACTIONS);
}
async function acquireTokenViaOIDC() {
log.info("Generating OIDC token...");
const oidcToken = await core2.getIDToken("pullfrog-api");
log.info("OIDC token generated successfully");
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
log.info("Exchanging OIDC token for installation token...");
const timeoutMs = 5e3;
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();
log.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
return tokenData.token;
} catch (error2) {
clearTimeout(timeoutId);
if (error2 instanceof Error && error2.name === "AbortError") {
throw new Error(`Token exchange timed out after ${timeoutMs}ms`);
}
throw error2;
}
}
var base64UrlEncode = (str) => {
return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
};
var generateJWT = (appId, privateKey) => {
const now = Math.floor(Date.now() / 1e3);
const payload = {
iat: now - 60,
exp: now + 5 * 60,
iss: appId
};
const header = {
alg: "RS256",
typ: "JWT"
};
const encodedHeader = base64UrlEncode(JSON.stringify(header));
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
const signaturePart = `${encodedHeader}.${encodedPayload}`;
const signature = createSign("RSA-SHA256").update(signaturePart).sign(privateKey, "base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
return `${signaturePart}.${signature}`;
};
var githubRequest = async (path4, options = {}) => {
const { method = "GET", headers = {}, body } = options;
const url2 = `https://api.github.com${path4}`;
const requestHeaders = {
Accept: "application/vnd.github.v3+json",
"User-Agent": "Pullfrog-Installation-Token-Generator/1.0",
...headers
};
const response = await fetch(url2, {
method,
headers: requestHeaders,
...body && { body }
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`GitHub API request failed: ${response.status} ${response.statusText}
${errorText}`
);
}
return response.json();
};
var checkRepositoryAccess = async (token, repoOwner, repoName) => {
try {
const response = await githubRequest("/installation/repositories", {
headers: { Authorization: `token ${token}` }
});
return response.repositories.some(
(repo) => repo.owner.login === repoOwner && repo.name === repoName
);
} catch {
return false;
}
};
var createInstallationToken = async (jwt, installationId) => {
const response = await githubRequest(
`/app/installations/${installationId}/access_tokens`,
{
method: "POST",
headers: { Authorization: `Bearer ${jwt}` }
}
);
return response.token;
};
var findInstallationId = async (jwt, repoOwner, repoName) => {
const installations = await githubRequest("/app/installations", {
headers: { Authorization: `Bearer ${jwt}` }
});
for (const installation of installations) {
try {
const tempToken = await createInstallationToken(jwt, installation.id);
const hasAccess = await checkRepositoryAccess(tempToken, repoOwner, repoName);
if (hasAccess) {
return installation.id;
}
} catch {
}
}
throw new Error(
`No installation found with access to ${repoOwner}/${repoName}. Ensure the GitHub App is installed on the target repository.`
);
};
async function acquireTokenViaGitHubApp() {
const repoContext = parseRepoContext();
const config = {
appId: process.env.GITHUB_APP_ID,
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n"),
repoOwner: repoContext.owner,
repoName: repoContext.name
};
const jwt = generateJWT(config.appId, config.privateKey);
const installationId = await findInstallationId(jwt, config.repoOwner, config.repoName);
const token = await createInstallationToken(jwt, installationId);
return token;
}
async function acquireNewToken() {
if (isGitHubActionsEnvironment()) {
return await acquireTokenViaOIDC();
} else {
return await acquireTokenViaGitHubApp();
}
}
async function setupGitHubInstallationToken() {
const existingToken = checkExistingToken();
if (existingToken) {
core2.setSecret(existingToken);
log.info("Using provided GitHub installation token");
return { githubInstallationToken: existingToken, wasAcquired: false };
}
const acquiredToken = await acquireNewToken();
core2.setSecret(acquiredToken);
process.env.GITHUB_INSTALLATION_TOKEN = acquiredToken;
return { githubInstallationToken: acquiredToken, wasAcquired: true };
}
async function revokeInstallationToken(token) {
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 (error2) {
log.warning(
`Failed to revoke installation token: ${error2 instanceof Error ? error2.message : String(error2)}`
);
}
}
function parseRepoContext() {
const githubRepo = process.env.GITHUB_REPOSITORY;
if (!githubRepo) {
throw new Error("GITHUB_REPOSITORY environment variable is required");
}
const [owner, name] = githubRepo.split("/");
if (!owner || !name) {
throw new Error(`Invalid GITHUB_REPOSITORY format: ${githubRepo}. Expected 'owner/repo'`);
}
return { owner, name };
}
// mcp/config.ts
function createMcpConfigs(githubInstallationToken) {
const repoContext = parseRepoContext();
@@ -42283,7 +42389,23 @@ async function main(inputs) {
log.debug(`\u{1F4CB} MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
const cliPath = await agent2.install();
log.info(`Running ${agentName}...`);
log.box(inputs.prompt, { title: "Prompt" });
let payload;
try {
const parsedPrompt = JSON.parse(inputs.prompt);
if (!("~pullfrog" in parsedPrompt)) {
throw new Error();
}
payload = parsedPrompt;
} catch {
payload = {
"~pullfrog": true,
agent: null,
prompt: inputs.prompt,
event: {},
modes
};
}
log.box(payload.prompt, { title: "Prompt" });
const matchingInputKey = agent2.inputKeys.find((inputKey) => inputs[inputKey]);
if (!matchingInputKey) {
throwMissingApiKeyError({
@@ -42295,7 +42417,7 @@ async function main(inputs) {
}
const apiKey = inputs[matchingInputKey];
const result = await agent2.run({
prompt: inputs.prompt,
payload,
mcpServers,
githubInstallationToken,
apiKey,
+39 -4
View File
@@ -1,5 +1,39 @@
import { build } from "esbuild";
// @ts-check
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,
format: "esm",
@@ -29,15 +63,16 @@ const sharedConfig = {
await build({
...sharedConfig,
entryPoints: ["./entry.ts"],
outfile: "./entry.js",
outfile: "./entry",
plugins: [stripShebangPlugin],
});
// Build the MCP server bundle
await build({
...sharedConfig,
entryPoints: ["./mcp/server.ts"],
outfile: "./mcp-server.js",
outfile: "./mcp-server",
plugins: [stripShebangPlugin],
});
console.log("✅ Build completed successfully!");
+33
View File
@@ -0,0 +1,33 @@
import type { AgentName } from "./main.ts";
import type { Mode } from "./modes.ts";
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 @@
create a comment on https://github.com/pullfrogai/scratch/issues/21 with an implementation of an mcp tool for fetching issue comments from github
create a comment on https://github.com/pullfrogai/scratch/pull/29 that says ribbit
+24 -5
View File
@@ -1,7 +1,9 @@
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";
@@ -122,11 +124,28 @@ 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;
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,
};
}
log.box(payload.prompt, { title: "Prompt" });
const matchingInputKey = agent.inputKeys.find((inputKey) => inputs[inputKey]);
@@ -142,7 +161,7 @@ export async function main(inputs: Inputs): Promise<MainResult> {
const apiKey = inputs[matchingInputKey]!;
const result = await agent.run({
prompt: inputs.prompt,
payload,
mcpServers,
githubInstallationToken,
apiKey,
+56 -2
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env node
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);
var __create = Object.create;
var __defProp = Object.defineProperty;
@@ -97760,6 +97759,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 +101415,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);
+21 -2
View File
@@ -2,14 +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";
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();
@@ -30,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);
}
}
+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) {
+8 -2
View File
@@ -1,6 +1,12 @@
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:
@@ -51,4 +57,4 @@ export const modes = [
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.105",
"version": "0.0.107",
"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",
+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: {}
+13 -1
View File
@@ -1,6 +1,15 @@
## CURRENT
[] handle progressive comment updating from pullfrog mcp
[] gemini installation speed (bundle/esm.sh?) (TODO: SHAWN)
[] handle defaulting agent name value (TODO: SHAWN)
[] split up prompts, load dynamically based on mode
[] log.txt to stdout
[] entry.js
[] test agent/mode combinations
[] test 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
## MAYBE
@@ -21,3 +30,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