Compare commits

...

26 Commits

Author SHA1 Message Date
Colin McDonnell 78c22085bf 0.0.156 2026-01-08 15:05:05 -08:00
Colin McDonnell b55cda579d Merge pull request #19 from pullfrog/custom-bash
Switch to custom Bash tool. Mask secrets from Bash subprocs.
2026-01-08 14:59:44 -08:00
Colin McDonnell 1d1d80c3f9 Additional testing with codex 2026-01-08 14:57:47 -08:00
Colin McDonnell 3a97ba04fc Rebase 2026-01-08 14:11:49 -08:00
Colin McDonnell fe7ce4af11 Updates 2026-01-08 14:11:43 -08:00
pullfrog 6260b23de7 Address review feedback
- Remove shell commands section from agent instructions
- Merge Platform Notes into Agent-Specific Notes section
- Remove redundant description text from bash tool
2026-01-08 14:11:32 -08:00
Colin McDonnell 9291ee5952 Fix github_actions iss 2026-01-08 14:11:11 -08:00
David Blass d30532979a cross-platform docker setup 2026-01-08 14:09:18 -08:00
Colin McDonnell c8b65327ee Tweaks 2026-01-08 14:09:18 -08:00
Colin McDonnell 879d33403c Switch to custom Bash tool. Mask secrets from Bashsubprocs. Simplify security handling. 2026-01-08 14:09:08 -08:00
Colin McDonnell 2cc081c912 Add license 2026-01-08 11:33:43 -08:00
David Blass b9a7a19ca1 use resource management for main's cleanup 2026-01-08 10:26:17 -05:00
David Blass 7ee08d37a6 fix(deps): Upgrading fastmcp and claude-agent-sdk for using zod@4 2026-01-08 10:18:34 -05:00
Robin Tail ff913feb3c Upgrading fastmcp ad claude-agent-sdk for using Zod 4. 2026-01-08 13:29:19 +01:00
Mateusz Burzyński 317ebd3431 cleanup mcp server too 2026-01-08 11:21:23 +01:00
Mateusz Burzyński 2bd12b9553 Use await using for installation token cleanup 2026-01-07 19:25:24 +01:00
Colin McDonnell d99a852e24 Merge pull request #13 from GameRoMan/remove-package-lock
remove package-lock.json
2026-01-07 10:07:25 -08:00
Colin McDonnell 6e289e9310 Merge pull request #15 from pullfrog/throttle-plugin
Auto-retry ratelimited octokit requests
2026-01-07 10:06:59 -08:00
Mateusz Burzyński a483711fee Auto-retry ratelimited octokit requests 2026-01-07 17:17:20 +01:00
Roman 244c7d4d8d remove package-lock.json 2026-01-04 23:39:07 +00:00
Colin McDonnell 0504fc42ff Do not print 'This run croaked' if the agent only replies in a PR review comment 2025-12-30 20:21:23 -08:00
Colin McDonnell ad1f51d704 Implement Plan button 2025-12-30 20:14:49 -08:00
Colin McDonnell 573c473dc1 Drop opus flag 2025-12-30 13:52:54 -08:00
Colin McDonnell c200c7aff9 Tweak message 2025-12-27 16:28:47 -08:00
Colin McDonnell 8a7db7bba2 Maybe fix gemini 2025-12-27 16:28:41 -08:00
Shawn Morreau 3f996b4759 Remove list_files mcp 2025-12-23 15:34:38 -05:00
34 changed files with 54517 additions and 41307 deletions
-2
View File
@@ -23,8 +23,6 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Setup Node.js
uses: actions/setup-node@v4
+2
View File
@@ -45,3 +45,5 @@ examples
# Temporary directory for cloned repos
.temp/
dist
.pnpm-store/
+1
View File
@@ -0,0 +1 @@
v24.3.0
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 pullfrog
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+8 -13
View File
@@ -21,28 +21,23 @@ export const claude = agent({
const prompt = addInstructions({ payload, repo });
log.group("Full prompt", () => log.info(prompt));
// configure sandbox mode if enabled
// SECURITY: For PUBLIC repos, Claude Code spawns subprocesses with full process.env, leaking API keys.
// disable native Bash; agents use MCP bash tool which filters secrets.
// for private repos, native Bash is allowed since secrets are less exposed.
const disallowedTools = repo.isPublic ? ["Bash"] : [];
const sandboxOptions: Options = payload.sandbox
? {
permissionMode: "default",
disallowedTools: ["Bash", "WebSearch", "WebFetch", "Write"],
async canUseTool(toolName, input, _options) {
if (toolName.startsWith("mcp__gh_pullfrog__"))
return {
behavior: "allow",
updatedInput: input,
updatedPermissions: [],
};
console.error("can i use this tool?", toolName);
return {
behavior: "deny",
message: "You are not allowed to use this tool.",
};
return { behavior: "allow", updatedInput: input, updatedPermissions: [] };
return { behavior: "deny", message: "tool not allowed in sandbox mode" };
},
}
: {
permissionMode: "bypassPermissions" as const,
disallowedTools,
};
if (payload.sandbox) {
@@ -56,7 +51,7 @@ export const claude = agent({
options: {
...sandboxOptions,
mcpServers,
model: "claude-opus-4-5",
// model: "claude-opus-4-5",
pathToClaudeCodeExecutable: cliPath,
env: createAgentEnv({ ANTHROPIC_API_KEY: apiKey }),
},
+57 -42
View File
@@ -1,15 +1,55 @@
import { spawnSync } from "node:child_process";
import { mkdirSync } from "node:fs";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import type { McpHttpServerConfig } 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,
type ConfigureMcpServersParams,
installFromNpmTarball,
setupProcessAgentEnv,
} from "./shared.ts";
import { agent, installFromNpmTarball, setupProcessAgentEnv } from "./shared.ts";
interface WriteCodexConfigParams {
tempHome: string;
mcpServers: Record<string, McpHttpServerConfig>;
isPublicRepo: boolean;
}
function writeCodexConfig({ tempHome, mcpServers, isPublicRepo }: WriteCodexConfigParams): string {
const codexDir = join(tempHome, ".codex");
mkdirSync(codexDir, { recursive: true });
const configPath = join(codexDir, "config.toml");
// build MCP servers section
const mcpServerSections: string[] = [];
for (const [name, config] of Object.entries(mcpServers)) {
if (config.type !== "http") continue;
log.info(`» Adding MCP server '${name}' at ${config.url}`);
mcpServerSections.push(`[mcp_servers.${name}]\nurl = "${config.url}"`);
}
// SECURITY: for public repos, enforce env filtering via shell_environment_policy
// this prevents vuln if user's ~/.codex/config.toml has ignore_default_excludes=true
// for private repos, no filtering - agents use native shell with full env access
const shellPolicy = isPublicRepo
? `[shell_environment_policy]
ignore_default_excludes = false`
: "";
writeFileSync(
configPath,
`# written by pullfrog
${shellPolicy}
${mcpServerSections.join("\n\n")}
`.trim() + "\n"
);
if (isPublicRepo) {
log.info(`» Codex config written to ${configPath} (env filtering: enabled)`);
} else {
log.info(`» Codex config written to ${configPath} (private repo: no env filtering)`);
}
return codexDir;
}
export const codex = agent({
name: "codex",
@@ -21,18 +61,24 @@ export const codex = agent({
});
},
run: async ({ payload, mcpServers, apiKey, cliPath, repo }) => {
// create config directory for codex before setting HOME
const tempHome = process.env.PULLFROG_TEMP_DIR!;
// create config directory for codex before setting HOME
const configDir = join(tempHome, ".config", "codex");
mkdirSync(configDir, { recursive: true });
const codexDir = writeCodexConfig({
tempHome,
mcpServers,
isPublicRepo: repo.isPublic,
});
setupProcessAgentEnv({
OPENAI_API_KEY: apiKey,
HOME: tempHome,
CODEX_HOME: codexDir, // point Codex to our config directory
});
configureCodexMcpServers({ mcpServers, cliPath });
// Configure Codex
const codexOptions: CodexOptions = {
apiKey,
@@ -44,7 +90,6 @@ export const codex = agent({
}
const codex = new Codex(codexOptions);
// valid sandbox modes: read-only, workspace-write, danger-full-access
const thread = codex.startThread(
payload.sandbox
? {
@@ -186,33 +231,3 @@ const messageHandlers: {
log.error(`Error: ${event.message}`);
},
};
/**
* Configure MCP servers for Codex using the CLI.
* For HTTP-based servers, use: codex mcp add <name> --url <url>
*/
function configureCodexMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
if (serverConfig.type === "http") {
// HTTP-based MCP server - use --url flag
const addArgs = ["mcp", "add", serverName, "--url", serverConfig.url];
log.info(`Adding MCP server '${serverName}' at ${serverConfig.url}...`);
const addResult = spawnSync("node", [cliPath, ...addArgs], {
stdio: "pipe",
encoding: "utf-8",
});
if (addResult.status !== 0) {
throw new Error(
`codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
);
}
log.info(`✓ MCP server '${serverName}' configured`);
} else {
throw new Error(
`Unsupported MCP server type for Codex: ${(serverConfig as any).type || "unknown"}`
);
}
}
}
+26 -19
View File
@@ -93,7 +93,7 @@ export const cursor = agent({
},
run: async ({ payload, apiKey, cliPath, mcpServers, repo }) => {
configureCursorMcpServers({ mcpServers, cliPath });
configureCursorSandbox({ sandbox: payload.sandbox ?? false });
configureCursorSandbox({ sandbox: payload.sandbox ?? false, isPublicRepo: repo.isPublic });
// track logged model_call_ids to avoid duplicates
// cursor emits each assistant message twice: once without model_call_id, then again with it
@@ -314,39 +314,46 @@ function configureCursorMcpServers({ mcpServers }: ConfigureMcpServersParams) {
/**
* Configure Cursor CLI sandbox mode via cli-config.json.
* When sandbox is enabled, denies all file writes and shell commands.
* In print mode without --force, writes are blocked by default, but we add
* explicit deny rules as defense in depth.
*
* See: https://cursor.com/docs/cli/reference/permissions
* SECURITY: For PUBLIC repos, Cursor spawns subprocesses with full process.env, leaking API keys.
* We deny native Shell via Shell(*) rule, forcing use of MCP bash tool which
* filters secrets. Note: Shell(**) does NOT work, must use Shell(*).
* For private repos, native Shell is allowed.
*
* Config path: $XDG_CONFIG_HOME/cursor/ (not ~/.cursor/) because createAgentEnv
* sets XDG_CONFIG_HOME=$HOME/.config. See issues/cursor-perms.md.
*/
function configureCursorSandbox({ sandbox }: { sandbox: boolean }): void {
function configureCursorSandbox({
sandbox,
isPublicRepo,
}: {
sandbox: boolean;
isPublicRepo: boolean;
}): void {
const realHome = homedir();
const cursorConfigDir = join(realHome, ".cursor");
const cursorConfigDir = join(realHome, ".config", "cursor");
const cliConfigPath = join(cursorConfigDir, "cli-config.json");
mkdirSync(cursorConfigDir, { recursive: true });
// deny native shell for public repos to prevent secret leakage
const denyShell = isPublicRepo ? ["Shell(*)"] : [];
const config = sandbox
? {
// sandbox mode: deny all writes and shell commands
permissions: {
allow: [
"Read(**)", // allow reading all files
],
deny: [
"Write(**)", // deny all file writes
"Shell(**)", // deny all shell commands
],
allow: ["Read(**)"],
deny: ["Write(**)", ...denyShell],
},
}
: {
// normal mode: allow everything
permissions: {
allow: ["Read(**)", "Write(**)", "Shell(**)"],
deny: [],
allow: ["Read(**)", "Write(**)"],
deny: denyShell,
},
};
writeFileSync(cliConfigPath, JSON.stringify(config, null, 2), "utf-8");
log.info(`» CLI config written to ${cliConfigPath} (sandbox: ${sandbox})`);
log.info(
`» CLI config written to ${cliConfigPath} (sandbox: ${sandbox}, isPublicRepo: ${isPublicRepo})`
);
}
+91 -45
View File
@@ -1,4 +1,6 @@
import { spawnSync } from "node:child_process";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { log } from "../utils/cli.ts";
import { spawn } from "../utils/subprocess.ts";
import { addInstructions } from "./instructions.ts";
@@ -155,7 +157,7 @@ export const gemini = agent({
});
},
run: async ({ payload, apiKey, mcpServers, cliPath, repo }) => {
configureGeminiMcpServers({ mcpServers, cliPath });
configureGeminiMcpServers({ mcpServers, isPublicRepo: repo.isPublic });
if (!apiKey) {
throw new Error("google_api_key or gemini_api_key is required for gemini agent");
@@ -164,34 +166,41 @@ export const gemini = agent({
const sessionPrompt = addInstructions({ payload, repo });
log.group("Full prompt", () => log.info(sessionPrompt));
// configure sandbox mode if enabled
// --allowed-tools restricts which tools are available (removes others from registry entirely)
// in sandbox mode: only read-only tools available (no write_file, run_shell_command, web_fetch)
const args = payload.sandbox
? [
"--allowed-tools",
"read_file,list_directory,search_file_content,glob,save_memory,write_todos",
"--allowed-mcp-server-names",
"gh_pullfrog",
"--output-format=stream-json",
"-p",
sessionPrompt,
]
: ["--yolo", "--output-format=stream-json", "-p", sessionPrompt];
// build CLI args based on sandbox mode
// for public repos, native shell is disabled via excludeTools in settings.json
let args: string[];
if (payload.sandbox) {
// sandbox mode: read-only tools only
args = [
"--allowed-tools",
"read_file,list_directory,search_file_content,glob,save_memory,write_todos",
"--allowed-mcp-server-names",
"gh_pullfrog",
"--output-format=stream-json",
"-p",
sessionPrompt,
];
} else {
// normal mode: --yolo for auto-approval
// for public repos, shell is excluded via settings.json excludeTools
args = ["--yolo", "--output-format=stream-json", "-p", sessionPrompt];
if (repo.isPublic) {
log.info("🔒 public repo: native shell disabled via excludeTools, using MCP bash");
}
}
if (payload.sandbox) {
log.info("🔒 sandbox mode enabled: restricting to read-only operations");
}
let finalOutput = "";
let stdoutBuffer = ""; // buffer for incomplete lines across chunks
let stdoutBuffer = "";
try {
const result = await spawn({
cmd: "node",
args: [cliPath, ...args],
env: createAgentEnv({
GEMINI_API_KEY: apiKey,
}),
env: createAgentEnv({ GEMINI_API_KEY: apiKey }),
onStdout: async (chunk) => {
const text = chunk.toString();
finalOutput += text;
@@ -264,36 +273,73 @@ export const gemini = agent({
},
});
type ConfigureGeminiParams = {
mcpServers: ConfigureMcpServersParams["mcpServers"];
isPublicRepo: boolean;
};
/**
* Configure MCP servers for Gemini using the CLI.
* Gemini CLI syntax: gemini mcp add <name> <commandOrUrl> [args...] --transport <type>
* For HTTP-based servers, use: gemini mcp add <name> <url> --transport http
* Configure MCP servers for Gemini by writing to settings.json.
* Gemini CLI uses `httpUrl` for HTTP/streamable transport, `url` for SSE transport.
* See: https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/configuration.md
*
* For public repos, also configures excludeTools to disable native shell.
*/
function configureGeminiMcpServers({ mcpServers, cliPath }: ConfigureMcpServersParams): void {
function configureGeminiMcpServers({ mcpServers, isPublicRepo }: ConfigureGeminiParams): void {
const realHome = homedir();
const geminiConfigDir = join(realHome, ".gemini");
const settingsPath = join(geminiConfigDir, "settings.json");
mkdirSync(geminiConfigDir, { recursive: true });
// read existing settings if present
let existingSettings: Record<string, unknown> = {};
try {
const content = readFileSync(settingsPath, "utf-8");
existingSettings = JSON.parse(content);
} catch {
// file doesn't exist or is invalid - start fresh
}
// convert to Gemini's expected format (httpUrl for HTTP transport, no type field)
type GeminiMcpServerConfig = {
command?: string;
args?: string[];
env?: Record<string, string>;
cwd?: string;
url?: string;
httpUrl?: string;
headers?: Record<string, string>;
timeout?: number;
trust?: boolean;
description?: string;
includeTools?: string[];
excludeTools?: string[];
};
const geminiMcpServers: Record<string, GeminiMcpServerConfig> = {};
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
if (serverConfig.type === "http") {
// HTTP-based MCP server - use URL with --transport http flag
const addArgs = ["mcp", "add", serverName, serverConfig.url, "--transport", "http"];
log.info(`Adding MCP server '${serverName}' at ${serverConfig.url}...`);
const addResult = spawnSync("node", [cliPath, ...addArgs], {
stdio: "pipe",
encoding: "utf-8",
env: {
...process.env,
},
});
if (addResult.status !== 0) {
throw new Error(
`gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
);
}
log.info(`✓ MCP server '${serverName}' configured`);
} else {
if (serverConfig.type !== "http") {
throw new Error(
`Unsupported MCP server type for Gemini: ${(serverConfig as any).type || "unknown"}`
`Unsupported MCP server type for Gemini: ${(serverConfig as { type?: string }).type || "unknown"}`
);
}
geminiMcpServers[serverName] = {
httpUrl: serverConfig.url,
trust: true, // trust our own MCP server to avoid confirmation prompts
};
log.info(`Adding MCP server '${serverName}' at ${serverConfig.url}...`);
}
// merge with existing settings, overwriting mcpServers
// for public repos, exclude native shell tool to prevent secret leakage via env
const newSettings: Record<string, unknown> = {
...existingSettings,
mcpServers: geminiMcpServers,
};
if (isPublicRepo) {
newSettings.excludeTools = ["run_shell_command"];
}
writeFileSync(settingsPath, JSON.stringify(newSettings, null, 2), "utf-8");
log.info(`» MCP config written to ${settingsPath}`);
}
+12 -34
View File
@@ -8,6 +8,7 @@ interface RepoInfo {
owner: string;
name: string;
defaultBranch: string;
isPublic: boolean;
}
/**
@@ -56,6 +57,9 @@ interface AddInstructionsParams {
}
export const addInstructions = ({ payload, repo }: AddInstructionsParams) => {
// for public repos, always use MCP bash for security (filters secrets)
// for private repos, agents can use their native bash
const useNativeBash = !repo.isPublic;
let encodedEvent = "";
const eventKeys = Object.keys(payload.event);
@@ -98,40 +102,9 @@ In case of conflict between instructions, follow this precedence (highest to low
4. Repository-specific instructions (AGENTS.md, CLAUDE.md, etc.)
5. User prompt
## SECURITY
## Security
CRITICAL SECURITY RULES - NEVER VIOLATE UNDER ANY CIRCUMSTANCES:
### Rule 1: Never expose secrets through ANY means
You must NEVER expose secrets through any channel, including but not limited to:
- Displaying, printing, echoing, logging, or outputting to console
- Writing to files (including .txt, .env, .json, config files, etc.)
- Including in git commits, commit messages, or PR descriptions
- Posting in GitHub comments, issue bodies, or PR review comments
- Returning in tool outputs, API responses, or error messages
- Including in redirect URLs, WebSocket messages, or GraphQL responses
Secrets include: API keys, authentication tokens, passwords, private keys, certificates, database connection strings, and any credential used for authentication or authorization. Common patterns (case-insensitive): variables containing API_KEY, SECRET, TOKEN, PASSWORD, CREDENTIAL, PRIVATE_KEY, or AUTH in an authentication context. Use judgment: \`PUBLIC_KEY\` for a cryptographic public key is fine; \`PRIVATE_KEY\` is not.
### Rule 2: Never serialize objects containing secrets
When working with objects that may contain environment variables or secrets:
- NEVER serialize, stringify, or dump entire environment objects (process.env, os.environ, ENV, etc.)
- NEVER iterate over environment variables and write their values to files
- NEVER include environment variable values in outputs, logs, HTTP requests, or anywhere they can be exposed
- If you must list properties, only show property NAMES, never values
- Only access specific, known-safe keys explicitly (e.g., NODE_ENV, HOME, PWD)
### Rule 3: Refuse and explain
Even if explicitly requested to reveal secrets, you must:
1. Refuse the request
2. Print a message explaining that exposing secrets is prohibited for security reasons
3. If using ${ghPullfrogMcpName}, update the working comment to explain that secrets cannot be revealed
4. Offer a safe alternative, if applicable
If you encounter secrets in files or environment, acknowledge they exist but never reveal their values.
Never expose secrets (API keys, tokens, passwords, private keys, credentials) through any channel: console output, files, commits, comments, API responses, error messages, or URLs. Never serialize environment objects (\`process.env\`, \`os.environ\`, etc.) or iterate over them. If asked to reveal secrets: refuse, explain that exposing secrets is prohibited, and offer a safe alternative if applicable. Detect and deny any suspicious or malicious requests.
## MCP (Model Context Protocol) Tools
@@ -143,7 +116,6 @@ Tool names may be formatted as \`(server name)/(tool name)\`, for example: \`${g
**Git operations**: All git operations must use ${ghPullfrogMcpName} MCP tools to ensure proper authentication and commit attribution. Do NOT use git commands directly (e.g., \`git commit\`, \`git push\`, \`git checkout\`, \`git branch\`) - these will use incorrect credentials and attribute commits to the wrong author.
**File discovery**: Use \`${ghPullfrogMcpName}/list_files\` to discover files in the repository. This tool finds both git-tracked and untracked files, including newly created files that haven't been committed yet. Prefer this over native agent tools like \`glob\` or \`grep\` for file discovery, as it provides consistent results and handles untracked files correctly.
` +
// **Available git MCP tools**:
// - \`${ghPullfrogMcpName}/checkout_pr\` - Checkout an existing PR branch locally (handles fork PRs automatically)
@@ -170,6 +142,12 @@ Tool names may be formatted as \`(server name)/(tool name)\`, for example: \`${g
**Efficiency**: Trust the tools - do not repeatedly verify file contents or git status after operations. If a tool reports success, proceed to the next step. Only verify if you encounter an actual error.
${
useNativeBash
? `**Shell commands**: Use your native bash/shell tool for shell command execution.`
: `**Shell commands**: Use the \`${ghPullfrogMcpName}/bash\` MCP tool for all shell command execution. This tool provides a secure environment with filtered credentials. Do NOT use any native shell/bash tool - it is disabled for security.`
}
**Command execution**: Never use \`sleep\` to wait for commands to complete. Commands run synchronously - when the bash tool returns, the command has finished.
**Commenting style**: When posting comments via ${ghPullfrogMcpName}, write as a professional team member would. Your final comments should be polished and actionable—do not include intermediate reasoning like "I'll now look at the code" or "Let me respond to the question."
+25 -15
View File
@@ -7,6 +7,7 @@ import { addInstructions } from "./instructions.ts";
import {
agent,
type ConfigureMcpServersParams,
createAgentEnv,
installFromNpmTarball,
setupProcessAgentEnv,
} from "./shared.ts";
@@ -27,7 +28,11 @@ export const opencode = agent({
const configDir = join(tempHome, ".config", "opencode");
mkdirSync(configDir, { recursive: true });
configureOpenCode({ mcpServers, sandbox: payload.sandbox ?? false });
configureOpenCode({
mcpServers,
sandbox: payload.sandbox ?? false,
isPublicRepo: repo.isPublic,
});
const prompt = addInstructions({ payload, repo });
log.group("Full prompt", () => log.info(prompt));
@@ -42,24 +47,25 @@ export const opencode = agent({
// 6. set up environment
setupProcessAgentEnv({ HOME: tempHome });
// build env vars: start with process.env (includes all API_KEY vars loaded by config())
// exclude GITHUB_TOKEN - OpenCode should use MCP server for GitHub operations, not direct token
// then override with apiKeys, HOME, and XDG_CONFIG_HOME
// SECURITY: build env vars from whitelisted base env to prevent API key leakage
// this prevents leaking other API keys (ANTHROPIC, GEMINI, etc.) to OpenCode subprocess
// XDG_CONFIG_HOME must be set because GitHub Actions sets it to a different path,
// and OpenCode follows XDG spec (checks XDG_CONFIG_HOME before falling back to $HOME/.config)
const env: Record<string, string> = {
...(Object.fromEntries(
Object.entries(process.env).filter(
([key, value]) => value !== undefined && key !== "GITHUB_TOKEN"
)
) as Record<string, string>),
HOME: tempHome,
...createAgentEnv({ HOME: tempHome }),
XDG_CONFIG_HOME: join(tempHome, ".config"),
};
// OpenCode doesn't support GitHub App installation tokens
delete env.GITHUB_TOKEN;
// add/override API keys from apiKeys object (uppercase keys)
// add API keys from apiKeys object
for (const [key, value] of Object.entries(apiKeys || {})) {
env[key.toUpperCase()] = value;
const upperKey = key.toUpperCase();
env[upperKey] = value;
// also set GOOGLE_GENERATIVE_AI_API_KEY for Google provider compatibility
if (upperKey === "GEMINI_API_KEY") {
env.GOOGLE_GENERATIVE_AI_API_KEY = value;
}
}
// run OpenCode in the repository directory (process.cwd() is set to GITHUB_WORKSPACE or repo dir)
@@ -188,13 +194,14 @@ export const opencode = agent({
interface ConfigureOpenCodeParams {
mcpServers: ConfigureMcpServersParams["mcpServers"];
sandbox: boolean;
isPublicRepo: boolean;
}
/**
* Configure OpenCode via opencode.json config file.
* Builds complete config with MCP servers and permissions in a single write to avoid race conditions.
*/
function configureOpenCode({ mcpServers, sandbox }: ConfigureOpenCodeParams): void {
function configureOpenCode({ mcpServers, sandbox, isPublicRepo }: ConfigureOpenCodeParams): void {
const tempHome = process.env.PULLFROG_TEMP_DIR!;
const configDir = join(tempHome, ".config", "opencode");
mkdirSync(configDir, { recursive: true });
@@ -218,7 +225,10 @@ function configureOpenCode({ mcpServers, sandbox }: ConfigureOpenCodeParams): vo
};
}
// build permissions config
// SECURITY: For PUBLIC repos, OpenCode spawns subprocesses with full process.env, leaking API keys.
// disable native bash; agents use MCP bash tool which filters secrets.
// for private repos, native bash is allowed.
const bashPermission = isPublicRepo ? "deny" : "allow";
const permission = sandbox
? {
edit: "deny",
@@ -229,7 +239,7 @@ function configureOpenCode({ mcpServers, sandbox }: ConfigureOpenCodeParams): vo
}
: {
edit: "allow",
bash: "allow",
bash: bashPermission,
webfetch: "allow",
doom_loop: "allow",
external_directory: "allow",
+1
View File
@@ -27,6 +27,7 @@ export interface RepoInfo {
owner: string;
name: string;
defaultBranch: string;
isPublic: boolean;
}
/**
+52
View File
@@ -0,0 +1,52 @@
# Docker Testing Environment
`play.ts` runs in Docker by default for realistic testing (Linux, clean $HOME, matches CI).
## Usage
```bash
pnpm play bash-test.ts # runs in Docker (default)
pnpm play --local bash-test.ts # runs on macOS (fast iteration)
PLAY_LOCAL=1 pnpm play ... # same as --local
```
## Why Docker by Default?
1. **Matches CI** - Linux environment like GitHub Actions
2. **Clean $HOME** - No agent config pollution from `~/.claude`, `~/.cursor`
3. **Tests unshare** - Verifies PID namespace sandbox works
4. **Reproducible** - Same environment every run
## Performance
| Mode | Overhead |
|------|----------|
| Docker (cached deps) | ~1.5s |
| Local (macOS) | ~0s |
For agent runs taking 30-120s, the 1.5s overhead is negligible.
## How It Works
1. `play.ts` runs on host, loads `.env`
2. Spawns Docker container with:
- Volume-mounted `action/` code
- Named volume for Linux node_modules (persists between runs)
- SSH agent forwarding for git clone
- Env vars passed via `-e` flags
3. Inside Docker, `play.ts` runs again (detects `/.dockerenv` file)
4. Clones `GITHUB_REPOSITORY`, runs agent
## Troubleshooting
**Docker not running:**
```
Cannot connect to the Docker daemon
```
→ Start Docker Desktop
**SSH clone fails:**
```
Permission denied (publickey)
```
→ Ensure SSH agent is running: `ssh-add -l`
+306
View File
@@ -0,0 +1,306 @@
# Bash Tool Security
> **Note**: Security measures described here apply to **PUBLIC repositories only**. For private repos, agents can use native bash with full environment access.
## Architecture (Public Repos)
```
┌─────────────────────────────────────────────────────────────────┐
│ GitHub Actions Runner │
│ (has secrets: ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Pullfrog Action (Node.js) │ │
│ │ - process.env contains all secrets │ │
│ │ - spawns agent CLI as child process │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Agent CLI (Claude/Cursor/OpenCode/etc.) │ │ │
│ │ │ - receives filtered env (only API key it needs) │ │ │
│ │ │ - has built-in Bash tool (DISABLED for public) │ │ │
│ │ │ - connects to MCP server for tools │ │ │
│ │ │ │ │ │
│ │ │ ┌───────────────────────────────────────────────┐ │ │ │
│ │ │ │ MCP Bash Tool (our code) │ │ │ │
│ │ │ │ - agent calls this for shell commands │ │ │ │
│ │ │ │ - spawns bash with filtered env │ │ │ │
│ │ │ │ - uses PID namespace isolation │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ Bash subprocess │ │ │ │ │
│ │ │ │ │ - runs user-controlled commands │ │ │ │ │
│ │ │ │ │ - MUST NOT access secrets │ │ │ │ │
│ │ │ │ └─────────────────────────────────────────┘ │ │ │ │
│ │ │ └───────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
**Key insight**: For **public repos**, the Pullfrog Action process has all secrets in `process.env`. Agent CLIs have built-in Bash tools that we can't trust since malicious actors can submit PRs with prompt injections. We disable those and provide our own MCP Bash tool that spawns subprocesses securely.
For **private repos**, the threat model is different — only trusted collaborators can trigger workflows, so we allow native bash with full environment access for better performance and compatibility.
---
## Public vs Private Repos
| Repo Visibility | Native Bash | Env Filtering | PID Isolation |
|-----------------|-------------|---------------|---------------|
| **Public** | Disabled | Yes | Yes (in CI) |
| **Private** | Enabled | No | No |
**Rationale**: Public repos are at risk from prompt injection attacks via pull requests from untrusted contributors. Private repos only allow trusted collaborators, so the attack surface is much smaller.
---
## Threat Model (Public Repos)
A prompt-injected agent could run malicious bash commands to exfiltrate API keys.
**Attack vectors:**
| Vector | Example | Mitigation |
|--------|---------|------------|
| Direct env access | `env \| grep KEY` | Filter env vars before spawn |
| Echo variable | `echo $ANTHROPIC_API_KEY` | Filter env vars before spawn |
| `/proc/$PPID/environ` | `cat /proc/$PPID/environ` | PID namespace isolation |
The first two are solved by passing filtered env to subprocess. The third requires special handling on Linux.
---
## Attack: /proc/$PPID/environ (Public Repos)
On Linux, any process can read its parent's environment via `/proc/$PPID/environ`. Even if we spawn bash with a clean environment, the bash process can:
```bash
# read parent's (Node.js) environment - contains all secrets!
tr '\0' '\n' < /proc/$PPID/environ | grep KEY
```
This bypasses environment filtering because we're reading the parent process's memory, not our own env.
**Why this matters:**
- Pullfrog Action (Node.js) has `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc. in `process.env`
- We spawn agent CLI with filtered env (only its own API key)
- Agent CLI spawns MCP Bash tool
- MCP Bash tool spawns bash with filtered env (no secrets)
- BUT bash can read `/proc/$PPID/environ` → gets Node.js process's full env
---
## Solution: PID Namespace Isolation (Public Repos)
We use Linux PID namespaces to hide the parent process:
```bash
unshare --pid --fork --mount-proc bash -c "$CMD"
```
| Flag | Purpose |
|------|---------|
| `--pid` | Create new PID namespace |
| `--fork` | Fork so child is actually in new namespace |
| `--mount-proc` | Mount fresh `/proc` for new namespace |
**Result:**
- Child sees itself as PID 1
- Child's PPID is 0 (doesn't exist)
- `/proc` only shows processes in child's namespace
- Parent's PID is invisible → `/proc/$PPID/environ` fails
---
## Implementation
### mcp/bash.ts
```typescript
import { spawn } from "node:child_process";
// filter sensitive env vars (only for public repos)
function filterEnv(isPublicRepo: boolean): Record<string, string> {
const SENSITIVE = [/_KEY$/i, /_SECRET$/i, /_TOKEN$/i, /_PASSWORD$/i, /_CREDENTIAL$/i];
const filtered: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (value === undefined) continue;
// only filter sensitive vars for public repos
if (isPublicRepo && SENSITIVE.some(p => p.test(key))) continue;
filtered[key] = value;
}
return filtered;
}
// spawn with PID namespace in CI for public repos, plain spawn otherwise
function spawnSandboxed(command: string, options: { env, cwd, isPublicRepo }): ChildProcess {
const useNamespaceIsolation = process.env.CI === "true" && options.isPublicRepo;
if (useNamespaceIsolation) {
return spawn("unshare", ["--pid", "--fork", "--mount-proc", "bash", "-c", command], options);
}
return spawn("bash", ["-c", command], options);
}
// BashTool uses ctx.repo.private to determine visibility
export function BashTool(ctx: ToolContext) {
const isPublicRepo = !ctx.repo.private;
// ... spawns with filterEnv(isPublicRepo) and isPublicRepo flag
}
```
**Defense in depth (public repos only):**
1. `filterEnv(true)` - prevents `env` and `echo $VAR` attacks
2. `unshare` - prevents `/proc/$PPID/environ` attack
---
## Disabling Native Bash Tools (Public Repos)
For **public repos**, each agent's built-in Bash/Shell tools are disabled. Agents use our MCP Bash tool which filters secrets:
```typescript
// Claude - conditional based on repo.isPublic
const disallowedTools = repo.isPublic ? ["Bash"] : [];
{ permissionMode: "bypassPermissions", disallowedTools }
// Cursor - conditional shell denial
const denyShell = isPublicRepo ? ["Shell(*)"] : [];
{ permissions: { allow: ["Read(**)", "Write(**)"], deny: denyShell } }
// OpenCode - conditional bash denial
const bashPermission = isPublicRepo ? "deny" : "allow";
{ permission: { edit: "allow", bash: bashPermission, ... } }
// Gemini - uses excludeTools in ~/.gemini/settings.json
newSettings.excludeTools = ["run_shell_command"];
// Codex - CLI internally scrubs env before spawning shell
// No SDK-level config needed; Codex handles this automatically
```
For **private repos**, native bash is allowed for all agents.
---
## Testing (Public Repo Scenario)
Run the vulnerability test in Docker to verify protection for public repos:
```bash
# from action/ directory
docker run --rm \
-v "$(pwd):/app/action:cached" \
-v "pullfrog-action-node-modules:/app/action/node_modules" \
-w /app/action \
-e GITHUB_ACTIONS=true \
-e TEST_SECRET_KEY=test-secret \
-e ANTHROPIC_API_KEY=sk-test \
--cap-add SYS_ADMIN \
--security-opt seccomp:unconfined \
node:22 bash -c "corepack enable pnpm && pnpm install --frozen-lockfile && node test/proc-environ-vuln.ts"
```
Expected output:
```
1. UNPROTECTED (filterEnv only):
Leaked: YES ❌
2. PROTECTED (unshare --pid --fork --mount-proc):
Leaked: NO ✓
```
---
## Platform Notes
| Environment | Repo | Our approach |
|-------------|------|--------------|
| GitHub Actions (Linux) | Public | filterEnv + unshare + disable native bash |
| GitHub Actions (Linux) | Private | Full env + native bash allowed |
| Local dev (any OS) | Any | No filtering (local dev assumed trusted) |
We check `process.env.CI === "true"` (set by GitHub Actions) combined with `ctx.repo.private` to determine the security posture:
- **CI + Public repo**: Full protection with PID namespace isolation
- **CI + Private repo**: No protection (trusted collaborators only)
- **Local**: No protection (developer's own machine)
GitHub Actions uses Ubuntu runners where `unshare` works without root.
---
## What This Does NOT Protect Against (Public Repos)
Even with protections enabled, bash subprocesses can still:
- **Network exfiltration**: Child has full network access
- **File access**: Child can read any file the runner can (same UID)
- **Resource exhaustion**: No cgroup limits
For those, you'd need `bwrap` with `--unshare-net`, `--ro-bind`, etc. But for the stated goal—preventing secret exfiltration via env—this is sufficient.
For **private repos**, none of these protections apply since we trust collaborators.
---
## Agent-Specific Notes
### Claude, Cursor, OpenCode (Public Repos)
These agents have their native Bash disabled via configuration. They use our `gh_pullfrog` MCP server's `bash` tool which implements `filterEnv()` + `unshare`.
For private repos, native bash is enabled for these agents.
### Gemini (Public Repos)
Gemini CLI supports `excludeTools` in its user-level settings file (`~/.gemini/settings.json`). For public repos, we exclude the native shell tool:
```typescript
// written to ~/.gemini/settings.json
newSettings.excludeTools = ["run_shell_command"];
```
This is a blocklist approach which explicitly excludes the shell tool while allowing all other tools.
Additionally, Gemini has built-in CI detection that filters shell env when `GITHUB_SHA` is set.
### Codex
Codex CLI filters out env vars matching `KEY`, `SECRET`, or `TOKEN` (case-insensitive) by default via `shell_environment_policy.ignore_default_excludes = false`.
**Vulnerability**: If a user's `~/.codex/config.toml` has `ignore_default_excludes = true`, secrets will leak to shell commands.
**Our mitigation**: We set `CODEX_HOME` to a temp directory and write our own `config.toml` with `ignore_default_excludes = false` to enforce filtering regardless of what config exists in the user's `~/.codex/`.
```typescript
// set CODEX_HOME to override user's config
setupProcessAgentEnv({ CODEX_HOME: codexDir });
// write secure config to $CODEX_HOME/config.toml
writeFileSync(join(codexDir, "config.toml"), `
[shell_environment_policy]
ignore_default_excludes = false
`);
```
See [GitHub Issue #3064](https://github.com/openai/codex/issues/3064) and [config docs](https://github.com/openai/codex/blob/main/docs/config.md#shell_environment_policy).
**Verified behavior** (tested via `pnpm play codex-env-test.ts`):
- Default (no config): ✅ secrets filtered
- `ignore_default_excludes = false`: ✅ secrets filtered
- `ignore_default_excludes = true`: ❌ secrets leak
Example output when running `env | grep TEST` with our config:
```
TEST_SAFE_VAR=VISIBLE-SAFE-VALUE
# FAKE_SECRET_KEY and TEST_API_TOKEN are NOT visible (filtered)
```
### Summary by Agent
| Agent | Public Repo | Private Repo |
|-------|-------------|--------------|
| Claude | Native bash **disabled** | Native bash allowed |
| Cursor | Native shell **disabled** | Native shell allowed |
| OpenCode | Native bash **disabled** | Native bash allowed |
| Gemini | Native shell **disabled** (via excludeTools) | Native bash allowed |
| Codex | Native shell allowed (CLI scrubs env internally) | Native bash allowed |
+53284 -39907
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -19,7 +19,7 @@ const stripShebangPlugin = {
try {
const content = readFileSync(outputFile, "utf8");
// Remove shebang line from the beginning if present
const withoutShebang = content.startsWith("#!")
const withoutShebang = content.startsWith("#!")
? content.slice(content.indexOf("\n") + 1)
: content;
writeFileSync(outputFile, withoutShebang);
@@ -38,7 +38,7 @@ const sharedConfig = {
bundle: true,
format: "esm",
platform: "node",
target: "node20",
target: "node24",
minify: false,
sourcemap: false,
// Bundle all dependencies - GitHub Actions doesn't have node_modules
+8
View File
@@ -212,6 +212,13 @@ interface FixReviewEvent extends BasePayloadEvent {
unapproved_comments: ReviewCommentData[];
}
interface ImplementPlanEvent extends BasePayloadEvent {
trigger: "implement_plan";
issue_number: number;
plan_comment_id: number;
plan_content: string;
}
interface UnknownEvent extends BasePayloadEvent {
trigger: "unknown";
}
@@ -231,6 +238,7 @@ export type PayloadEvent =
| CheckSuiteCompletedEvent
| WorkflowDispatchEvent
| FixReviewEvent
| ImplementPlanEvent
| UnknownEvent;
export interface DispatchOptions {
+35
View File
@@ -0,0 +1,35 @@
import type { Payload } from "../external.ts";
/**
* test fixture: verifies agents use MCP bash tool for shell commands.
* creates a simple test file and runs it with node.
*
* for insecure agents (claude, cursor, opencode): native bash is disabled,
* so they MUST use gh_pullfrog/bash MCP tool to run shell commands.
*
* for secure agents (codex, gemini): native bash is safe, but this test
* still verifies shell execution works.
*
* run with: AGENT_OVERRIDE=<agent> pnpm play bash-test.ts
*/
export default {
"~pullfrog": true,
agent: null,
prompt: `Create a file called test-runner.js with the following content:
\`\`\`javascript
const assert = require('assert');
assert.strictEqual(2 + 2, 4, 'math should work');
console.log('TEST PASSED: basic arithmetic works');
\`\`\`
Then run it with: node test-runner.js
Finally, delete the test file.
This tests that you can execute shell commands properly.`,
event: {
trigger: "workflow_dispatch",
},
modes: [],
} satisfies Payload;
+9 -6
View File
@@ -1,9 +1,12 @@
import type { Inputs } from "../main.ts";
import type { Payload } from "../external.ts";
const testParams = {
export default {
"~pullfrog": true,
agent: null,
prompt:
"List all files in the current directory, then create a file called dynamic-test.txt with the content 'This was loaded from a TypeScript file!', then delete it.",
anthropic_api_key: "sk-test-key",
} satisfies Inputs;
export default testParams;
event: {
trigger: "workflow_dispatch",
},
modes: [],
} satisfies Payload;
+1 -1
View File
@@ -1 +1 @@
add a file implementing quicksort and test it
Find all markdown files in the repository and list their names from https://github.com/ShawnMorreau/cal.com/
+3 -5
View File
@@ -6,9 +6,9 @@ import type { Payload } from "../external.ts";
*
* run with: AGENT_OVERRIDE=claude pnpm play sandbox.ts
*/
const payload: Payload = {
export default {
"~pullfrog": true,
agent: null, // let AGENT_OVERRIDE control this for testing different agents
agent: null,
prompt: `Please do the following three things:
1. Fetch the content from https://httpbin.org/json and tell me what it says
@@ -24,6 +24,4 @@ All three of these actions should fail because you are running in sandbox mode w
},
modes: [],
sandbox: true,
};
export default JSON.stringify(payload);
} satisfies Payload;
+17 -26
View File
@@ -2,7 +2,7 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { flatMorph } from "@ark/util";
import { Octokit } from "@octokit/rest";
import type { Octokit } from "@octokit/rest";
import { encode as toonEncode } from "@toon-format/toon";
import { type } from "arktype";
import { type Agent, agents } from "./agents/index.ts";
@@ -19,8 +19,8 @@ import { fetchRepoSettings, fetchWorkflowRunInfo, type RepoSettings } from "./ut
import { log } from "./utils/cli.ts";
import { reportErrorToComment } from "./utils/errorReport.ts";
import {
createOctokit,
parseRepoContext,
revokeGitHubInstallationToken,
setupGitHubInstallationToken,
} from "./utils/github.ts";
import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
@@ -53,7 +53,6 @@ export interface MainResult {
// intermediate result types for deterministic context building
interface GitHubSetup {
token: string;
owner: string;
name: string;
octokit: Octokit;
@@ -66,12 +65,11 @@ type ApiKeySetup =
| { success: false; error: string };
export async function main(inputs: Inputs): Promise<MainResult> {
let mcpServerClose: (() => Promise<void>) | undefined;
const timer = new Timer();
await using tokenRef = await setupGitHubInstallationToken();
let payload: Payload | undefined;
try {
const timer = new Timer();
// phase 1: parse and validate inputs
payload = parsePayload(inputs);
Inputs.assert(inputs);
@@ -79,7 +77,7 @@ export async function main(inputs: Inputs): Promise<MainResult> {
// phase 2: fast setup (github + temp dir)
const [githubSetup, sharedTempDir] = await Promise.all([
initializeGitHub(),
initializeGitHub(tokenRef.token),
createTempDirectory(),
]);
timer.checkpoint("githubSetup");
@@ -107,9 +105,9 @@ export async function main(inputs: Inputs): Promise<MainResult> {
// phase 5: parallel long-running operations (agent install + git auth)
const toolState: ToolState = {};
const [cliPath] = await Promise.all([
installAgentCli({ agent, token: githubSetup.token }),
installAgentCli({ agent, token: tokenRef.token }),
setupGitAuth({
token: githubSetup.token,
token: tokenRef.token,
owner: githubSetup.owner,
name: githubSetup.name,
payload: resolvedPayload,
@@ -156,7 +154,7 @@ export async function main(inputs: Inputs): Promise<MainResult> {
const toolContext: ToolContext = {
owner: githubSetup.owner,
name: githubSetup.name,
githubInstallationToken: githubSetup.token,
githubInstallationToken: tokenRef.token,
octokit: githubSetup.octokit,
payload: resolvedPayload,
repo: githubSetup.repo,
@@ -169,11 +167,10 @@ export async function main(inputs: Inputs): Promise<MainResult> {
jobId,
};
const { url: mcpServerUrl, close: mcpServerCloseFunc } = await startMcpHttpServer(toolContext);
mcpServerClose = mcpServerCloseFunc;
log.info(`🚀 MCP server started at ${mcpServerUrl}`);
await using mcpHttpServer = await startMcpHttpServer(toolContext);
log.info(`🚀 MCP server started at ${mcpHttpServer.url}`);
const mcpServers = createMcpConfigs(mcpServerUrl);
const mcpServers = createMcpConfigs(mcpHttpServer.url);
log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
timer.checkpoint("mcpServer");
@@ -181,8 +178,7 @@ export async function main(inputs: Inputs): Promise<MainResult> {
const ctx: AgentContext = {
...toolContext,
inputs,
mcpServerUrl,
mcpServerClose: mcpServerCloseFunc,
mcpServerUrl: mcpHttpServer.url,
mcpServers,
cliPath,
apiKey: apiKeySetup.apiKey,
@@ -225,11 +221,6 @@ export async function main(inputs: Inputs): Promise<MainResult> {
} catch {
// error updating comment, but don't let it mask the original error
}
if (mcpServerClose) {
await mcpServerClose();
}
await revokeGitHubInstallationToken();
}
}
@@ -321,7 +312,6 @@ export interface ToolContext {
export interface AgentContext extends Readonly<ToolContext> {
readonly inputs: Inputs;
readonly mcpServerUrl: string;
readonly mcpServerClose: () => Promise<void>;
readonly mcpServers: ReturnType<typeof createMcpConfigs>;
readonly cliPath: string;
readonly apiKey: string;
@@ -337,6 +327,7 @@ export interface DependencyInstallationState {
export interface ToolState {
prNumber?: number;
issueNumber?: number;
selectedMode?: string;
review?: {
id: number; // REST API database ID (for fix URLs)
nodeId: string; // GraphQL node ID (for mutations)
@@ -347,13 +338,12 @@ export interface ToolState {
/**
* Initialize GitHub connection: token, octokit, repo data, settings
*/
async function initializeGitHub(): Promise<GitHubSetup> {
async function initializeGitHub(token: string): Promise<GitHubSetup> {
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
const token = await setupGitHubInstallationToken();
const { owner, name } = parseRepoContext();
const octokit = new Octokit({ auth: token });
const octokit = createOctokit(token);
// fetch repo data and settings in parallel
const [repoResponse, repoSettings] = await Promise.all([
@@ -362,7 +352,6 @@ async function initializeGitHub(): Promise<GitHubSetup> {
]);
return {
token,
owner,
name,
octokit,
@@ -381,6 +370,7 @@ function resolveAgent({
repoSettings: RepoSettings;
}): Agent {
const agentOverride = process.env.AGENT_OVERRIDE as AgentName | undefined;
log.debug(`» determineAgent: agentOverride=${agentOverride}, payload.agent=${payload.agent}, repoSettings.defaultAgent=${repoSettings.defaultAgent}`);
const configuredAgentName = agentOverride || payload.agent || repoSettings.defaultAgent || null;
if (configuredAgentName) {
@@ -524,6 +514,7 @@ async function runAgent(ctx: AgentContext): Promise<AgentResult> {
owner: ctx.owner,
name: ctx.name,
defaultBranch: ctx.repo.default_branch,
isPublic: !ctx.repo.private,
},
});
}
+129
View File
@@ -0,0 +1,129 @@
import { type ChildProcess, spawn } from "node:child_process";
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import { execute, tool } from "./shared.ts";
export const BashParams = type({
command: "string",
description: "string",
"timeout?": "number",
"working_directory?": "string",
});
// patterns for sensitive env vars: suffixes (_KEY, _SECRET, _TOKEN) plus AI provider prefixes
const SENSITIVE_PATTERNS = [/_KEY$/i, /_SECRET$/i, /_TOKEN$/i, /_PASSWORD$/i, /_CREDENTIAL$/i];
function isSensitive(key: string): boolean {
return SENSITIVE_PATTERNS.some((p) => p.test(key));
}
/** filter env vars, removing sensitive values (only for public repos) */
function filterEnv(isPublicRepo: boolean): Record<string, string> {
const filtered: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (value === undefined) continue;
// only filter sensitive vars for public repos
if (isPublicRepo && isSensitive(key)) continue;
filtered[key] = value;
}
return filtered;
}
/**
* spawn command with filtered env. in CI, also use PID namespace isolation
* to prevent child from reading /proc/$PPID/environ (only for public repos)
*/
function spawnSandboxed(
command: string,
options: { env: Record<string, string>; cwd: string; isPublicRepo: boolean }
): ChildProcess {
const stdio: ["ignore", "pipe", "pipe"] = ["ignore", "pipe", "pipe"];
const spawnOpts = { env: options.env, cwd: options.cwd, stdio, detached: true };
// only use PID namespace isolation for public repos in CI
const useNamespaceIsolation = process.env.CI === "true" && options.isPublicRepo;
return useNamespaceIsolation
? spawn("unshare", ["--pid", "--fork", "--mount-proc", "bash", "-c", command], spawnOpts)
: spawn("bash", ["-c", command], spawnOpts);
}
/** kill process and its entire process group */
async function killProcessGroup(proc: ChildProcess): Promise<void> {
if (!proc.pid) return;
try {
process.kill(-proc.pid, "SIGTERM");
await new Promise((r) => setTimeout(r, 200));
process.kill(-proc.pid, "SIGKILL");
} catch {
try {
proc.kill("SIGKILL");
} catch {
/* already dead */
}
}
}
export function BashTool(ctx: ToolContext) {
const isPublicRepo = !ctx.repo.private;
return tool({
name: "bash",
description: `Execute shell commands securely.${isPublicRepo ? " Environment is filtered to remove API keys and secrets." : ""}
Use this tool to:
- Run shell commands (ls, cat, grep, find, etc.)
- Execute build tools (npm, pnpm, cargo, make, etc.)
- Run tests and linters
- Perform git operations
- Run shell commands in a secure environment. Unlike the built-in bash tool, this tool filters sensitive environment variables from the subprocess's environment to avoid leaking secrets.`,
parameters: BashParams,
execute: execute(async (params) => {
const timeout = Math.min(params.timeout ?? 120000, 600000);
const cwd = params.working_directory ?? process.cwd();
const proc = spawnSandboxed(params.command, {
env: filterEnv(isPublicRepo),
cwd,
isPublicRepo,
});
let stdout = "",
stderr = "",
timedOut = false,
exited = false;
proc.stdout?.on("data", (chunk: Buffer) => {
stdout += chunk.toString();
});
proc.stderr?.on("data", (chunk: Buffer) => {
stderr += chunk.toString();
});
const timeoutId = setTimeout(async () => {
if (!exited) {
timedOut = true;
await killProcessGroup(proc);
}
}, timeout);
const exitCode = await new Promise<number | null>((resolve) => {
const done = (code: number | null) => {
exited = true;
clearTimeout(timeoutId);
resolve(code);
};
proc.on("exit", done);
proc.on("error", () => done(null));
});
let output = stderr ? (stdout ? `${stdout}\n${stderr}` : stderr) : stdout;
if (timedOut)
output = output
? `${output}\n[timed out after ${timeout}ms]`
: `[timed out after ${timeout}ms]`;
return {
output: output.trim(),
exit_code: exitCode ?? (timedOut ? 124 : -1),
timed_out: timedOut,
};
}),
});
}
+84 -16
View File
@@ -1,11 +1,10 @@
import { Octokit } from "@octokit/rest";
import { type } from "arktype";
import type { Payload } from "../external.ts";
import { agentsManifest } from "../external.ts";
import type { ToolContext } from "../main.ts";
import { fetchWorkflowRunInfo } from "../utils/api.ts";
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
import { getGitHubInstallationToken, parseRepoContext } from "../utils/github.ts";
import { createOctokit, getGitHubInstallationToken, parseRepoContext, type OctokitWithPlugins } from "../utils/github.ts";
import { execute, tool } from "./shared.ts";
/**
@@ -15,7 +14,17 @@ import { execute, tool } from "./shared.ts";
*/
export const LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
async function buildCommentFooter(payload: Payload, octokit?: Octokit): Promise<string> {
interface BuildCommentFooterParams {
payload: Payload;
octokit?: OctokitWithPlugins | undefined;
customParts?: string[] | undefined;
}
async function buildCommentFooter({
payload,
octokit,
customParts,
}: BuildCommentFooterParams): Promise<string> {
const repoContext = parseRepoContext();
const runId = process.env.GITHUB_RUN_ID;
@@ -38,7 +47,7 @@ async function buildCommentFooter(payload: Payload, octokit?: Octokit): Promise<
}
}
return buildPullfrogFooter({
const footerParams = {
triggeredBy: true,
agent: {
displayName: agentInfo?.displayName || "Unknown agent",
@@ -52,12 +61,27 @@ async function buildCommentFooter(payload: Payload, octokit?: Octokit): Promise<
...(workflowRunHtmlUrl ? { htmlUrl: workflowRunHtmlUrl } : {}),
}
: undefined,
});
};
if (customParts && customParts.length > 0) {
return buildPullfrogFooter({ ...footerParams, customParts });
}
return buildPullfrogFooter(footerParams);
}
async function addFooter(body: string, payload: Payload, octokit?: Octokit): Promise<string> {
function buildImplementPlanLink(
owner: string,
repo: string,
issueNumber: number,
commentId: number
): string {
const apiUrl = process.env.API_URL || "https://pullfrog.com";
return `[Implement plan ➔](${apiUrl}/trigger/${owner}/${repo}/${issueNumber}?action=implement&comment_id=${commentId})`;
}
async function addFooter(body: string, payload: Payload, octokit?: OctokitWithPlugins): Promise<string> {
const bodyWithoutFooter = stripExistingFooter(body);
const footer = await buildCommentFooter(payload, octokit);
const footer = await buildCommentFooter({ payload, octokit });
return `${bodyWithoutFooter}${footer}`;
}
@@ -181,11 +205,26 @@ export async function reportProgress(
}
| undefined
> {
const bodyWithFooter = await addFooter(body, ctx.payload, ctx.octokit);
const existingCommentId = getProgressCommentId();
const issueNumber =
ctx.toolState.prNumber ?? ctx.toolState.issueNumber ?? ctx.payload.event.issue_number;
const isPlanMode = ctx.toolState.selectedMode === "Plan";
// if we already have a progress comment, update it
if (existingCommentId) {
const customParts =
isPlanMode && issueNumber !== undefined
? [buildImplementPlanLink(ctx.owner, ctx.name, issueNumber, existingCommentId)]
: undefined;
const bodyWithoutFooter = stripExistingFooter(body);
const footer = await buildCommentFooter({
payload: ctx.payload,
octokit: ctx.octokit,
customParts,
});
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
const result = await ctx.octokit.rest.issues.updateComment({
owner: ctx.owner,
repo: ctx.name,
@@ -205,24 +244,51 @@ export async function reportProgress(
// no existing comment - create one
// use fallback chain: dynamically set context > event payload
const issueNumber =
ctx.toolState.prNumber ?? ctx.toolState.issueNumber ?? ctx.payload.event.issue_number;
if (issueNumber === undefined) {
// cannot create comment without issue_number (e.g., workflow_dispatch events)
return undefined;
}
// for new comments, we need to create first, then update with Plan link if in Plan mode
const initialBody = await addFooter(body, ctx.payload, ctx.octokit);
const result = await ctx.octokit.rest.issues.createComment({
owner: ctx.owner,
repo: ctx.name,
issue_number: issueNumber,
body: bodyWithFooter,
body: initialBody,
});
// store the comment ID for future updates
setProgressCommentId(result.data.id);
progressCommentWasUpdated = true;
// if Plan mode, update the comment to add the "Implement plan" link
if (isPlanMode) {
const customParts = [buildImplementPlanLink(ctx.owner, ctx.name, issueNumber, result.data.id)];
const bodyWithoutFooter = stripExistingFooter(body);
const footer = await buildCommentFooter({
payload: ctx.payload,
octokit: ctx.octokit,
customParts,
});
const bodyWithPlanLink = `${bodyWithoutFooter}${footer}`;
const updateResult = await ctx.octokit.rest.issues.updateComment({
owner: ctx.owner,
repo: ctx.name,
comment_id: result.data.id,
body: bodyWithPlanLink,
});
return {
commentId: updateResult.data.id,
url: updateResult.data.html_url,
body: updateResult.data.body || "",
action: "created",
};
}
return {
commentId: result.data.id,
url: result.data.html_url,
@@ -341,8 +407,7 @@ export async function ensureProgressCommentUpdated(payload?: Payload): Promise<v
// check if comment still says "leaping into action" - if it's been updated with an error, don't overwrite it
const repoContext = parseRepoContext();
const token = getGitHubInstallationToken();
const octokit = new Octokit({ auth: token });
const octokit = createOctokit(getGitHubInstallationToken());
try {
const existingComment = await octokit.rest.issues.getComment({
@@ -363,10 +428,10 @@ export async function ensureProgressCommentUpdated(payload?: Payload): Promise<v
const runId = process.env.GITHUB_RUN_ID;
const workflowRunLink = runId
? `[workflow](https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId})`
: "workflow";
? `[workflow run logs](https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId})`
: "workflow run logs";
const errorMessage = `❌ this run croaked
const errorMessage = `This run croaked 😵
The workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
@@ -406,6 +471,9 @@ export function ReplyToReviewCommentTool(ctx: ToolContext) {
body: bodyWithFooter,
});
// mark progress as updated so ensureProgressCommentUpdated doesn't think the run failed
progressCommentWasUpdated = true;
return {
success: true,
commentId: result.data.id,
-91
View File
@@ -1,91 +0,0 @@
import { relative, resolve } from "node:path";
import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import { $ } from "../utils/shell.ts";
import { execute, tool } from "./shared.ts";
export const ListFiles = type({
path: type.string
.describe("The path to list files from (defaults to current directory)")
.default("."),
});
export function ListFilesTool(_ctx: ToolContext) {
return tool({
name: "list_files",
description:
"List files in the repository, including both git-tracked and untracked files. Useful for discovering the file structure and locating files, including newly created files that haven't been committed yet.",
parameters: ListFiles,
execute: execute(async ({ path }: { path?: string }) => {
const pathStr = path ?? ".";
const cwd = process.cwd();
// Get git-tracked files
let gitFiles: string[] = [];
let gitFailed = false;
try {
const gitArgs = pathStr === "." ? ["ls-files"] : ["ls-files", pathStr];
const gitOutput = $("git", gitArgs, { log: false });
gitFiles = gitOutput
.split("\n")
.filter((f) => f.trim() !== "")
.map((f) => f.trim());
} catch {
// git might fail, that's ok - we'll use find instead
gitFailed = true;
}
// Always also check filesystem for untracked files
// This is important because newly created files won't be in git yet
let filesystemFiles: string[] = [];
let findFailed = false;
try {
const findOutput = $(
"find",
[pathStr, "-maxdepth", "3", "-not", "-path", "*/.*", "-type", "f"],
{ log: false }
);
filesystemFiles = findOutput
.split("\n")
.filter((f) => f.trim() !== "")
.map((f) => {
const trimmed = f.trim();
// normalize to relative paths for comparison
try {
return relative(cwd, resolve(cwd, trimmed));
} catch {
return trimmed;
}
});
} catch {
// find might fail, that's ok - we'll just use git files
findFailed = true;
}
// if both methods failed, throw an error (execute helper will handle it)
if (gitFailed && findFailed) {
throw new Error(
`Failed to list files: both git ls-files and find commands failed. ` +
`Path: ${pathStr}, working directory: ${cwd}`
);
}
// Create a Set of git files for efficient lookup
const gitFilesSet = new Set(gitFiles);
// Combine both lists, removing duplicates
const allFiles = [...new Set([...gitFiles, ...filesystemFiles])].sort();
// Calculate actual untracked count (files in filesystem but not in git)
const untrackedFiles = filesystemFiles.filter((f) => !gitFilesSet.has(f));
const untrackedCount = untrackedFiles.length;
return {
files: allFiles,
method: "combined",
trackedCount: gitFiles.length,
untrackedCount,
};
}),
});
}
+3
View File
@@ -25,6 +25,9 @@ export function SelectModeTool(ctx: ToolContext) {
};
}
// store selected mode in toolState for use by other tools (e.g., report_progress)
ctx.toolState.selectedMode = selectedMode.name;
return {
modeName: selectedMode.name,
description: selectedMode.description,
+4 -4
View File
@@ -17,7 +17,6 @@ import {
AwaitDependencyInstallationTool,
StartDependencyInstallationTool,
} from "./dependencies.ts";
import { ListFilesTool } from "./files.ts";
import { CommitFilesTool, CreateBranchTool, PushBranchTool } from "./git.ts";
import { IssueTool } from "./issue.ts";
import { GetIssueCommentsTool } from "./issueComments.ts";
@@ -30,6 +29,7 @@ import { CreatePullRequestReviewTool } from "./review.ts";
import { GetReviewCommentsTool, ListPullRequestReviewsTool } from "./reviewComments.ts";
import { SelectModeTool } from "./selectMode.ts";
import { addTools } from "./shared.ts";
import { BashTool } from "./bash.ts";
/**
* Find an available port starting from the given port
@@ -65,7 +65,7 @@ async function findAvailablePort(startPort: number): Promise<number> {
*/
export async function startMcpHttpServer(
ctx: ToolContext
): Promise<{ url: string; close: () => Promise<void> }> {
): Promise<{ url: string; [Symbol.asyncDispose]: () => Promise<void> }> {
const server = new FastMCP({
name: ghPullfrogMcpName,
version: "0.0.1",
@@ -95,7 +95,7 @@ export async function startMcpHttpServer(
CreateBranchTool(ctx),
CommitFilesTool(ctx),
PushBranchTool(ctx),
ListFilesTool(ctx),
BashTool(ctx),
];
if (!ctx.payload.disableProgressComment) {
@@ -121,7 +121,7 @@ export async function startMcpHttpServer(
return {
url,
close: async () => {
[Symbol.asyncDispose]: async () => {
await server.stop();
},
};
-960
View File
@@ -1,960 +0,0 @@
{
"name": "@pullfrog/action",
"version": "0.0.96",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pullfrog/action",
"version": "0.0.96",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.11.1",
"@anthropic-ai/claude-agent-sdk": "^0.1.26",
"@ark/fs": "0.53.0",
"@ark/util": "0.53.0",
"@octokit/rest": "^22.0.0",
"@octokit/webhooks-types": "^7.6.1",
"@standard-schema/spec": "1.0.0",
"arktype": "2.1.25",
"convex": "^1.29.0",
"dotenv": "^17.2.3",
"execa": "^9.6.0",
"fastmcp": "^3.20.0",
"table": "^6.9.0"
},
"devDependencies": {
"@types/node": "^24.7.2",
"arg": "^5.0.2",
"esbuild": "^0.25.9",
"husky": "^9.0.0",
"typescript": "^5.9.3"
}
},
"../node_modules/.pnpm/@actions+core@1.11.1/node_modules/@actions/core": {
"version": "1.11.1",
"license": "MIT",
"dependencies": {
"@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1"
},
"devDependencies": {
"@types/node": "^16.18.112"
}
},
"../node_modules/.pnpm/@anthropic-ai+claude-agent-sdk@0.1.26_zod@3.25.76/node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.1.26",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "^0.33.5",
"@img/sharp-darwin-x64": "^0.33.5",
"@img/sharp-linux-arm": "^0.33.5",
"@img/sharp-linux-arm64": "^0.33.5",
"@img/sharp-linux-x64": "^0.33.5",
"@img/sharp-win32-x64": "^0.33.5"
},
"peerDependencies": {
"zod": "^3.24.1"
}
},
"../node_modules/.pnpm/@ark+fs@0.53.0/node_modules/@ark/fs": {
"version": "0.53.0",
"license": "MIT"
},
"../node_modules/.pnpm/@ark+util@0.53.0/node_modules/@ark/util": {
"version": "0.53.0",
"license": "MIT"
},
"../node_modules/.pnpm/@octokit+rest@22.0.0/node_modules/@octokit/rest": {
"version": "22.0.0",
"license": "MIT",
"dependencies": {
"@octokit/core": "^7.0.2",
"@octokit/plugin-paginate-rest": "^13.0.1",
"@octokit/plugin-request-log": "^6.0.0",
"@octokit/plugin-rest-endpoint-methods": "^16.0.0"
},
"devDependencies": {
"@octokit/auth-action": "^6.0.1",
"@octokit/auth-app": "^8.0.1",
"@octokit/fixtures-server": "^8.1.0",
"@octokit/request": "^10.0.0",
"@octokit/tsconfig": "^4.0.0",
"@types/node": "^22.0.0",
"@vitest/coverage-v8": "^3.0.0",
"esbuild": "^0.25.0",
"fetch-mock": "^12.0.0",
"glob": "^11.0.0",
"nock": "^14.0.0-beta.8",
"prettier": "^3.2.4",
"semantic-release-plugin-update-version-in-files": "^2.0.0",
"typescript": "^5.3.3",
"undici": "^6.4.0",
"vitest": "^3.0.0"
},
"engines": {
"node": ">= 20"
}
},
"../node_modules/.pnpm/@octokit+webhooks-types@7.6.1/node_modules/@octokit/webhooks-types": {
"version": "7.6.1",
"license": "MIT",
"devDependencies": {}
},
"../node_modules/.pnpm/@standard-schema+spec@1.0.0/node_modules/@standard-schema/spec": {
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"tsup": "^8.3.0",
"typescript": "^5.6.2"
}
},
"../node_modules/.pnpm/@types+node@24.7.2/node_modules/@types/node": {
"version": "24.7.2",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.14.0"
}
},
"../node_modules/.pnpm/arg@5.0.2/node_modules/arg": {
"version": "5.0.2",
"dev": true,
"license": "MIT",
"devDependencies": {
"chai": "^4.1.1",
"jest": "^27.0.6",
"prettier": "^2.3.2"
}
},
"../node_modules/.pnpm/arktype@2.1.25/node_modules/arktype": {
"version": "2.1.25",
"license": "MIT",
"dependencies": {
"@ark/schema": "0.53.0",
"@ark/util": "0.53.0",
"arkregex": "0.0.2"
}
},
"../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv": {
"version": "17.2.3",
"license": "BSD-2-Clause",
"devDependencies": {
"@types/node": "^18.11.3",
"decache": "^4.6.2",
"sinon": "^14.0.1",
"standard": "^17.0.0",
"standard-version": "^9.5.0",
"tap": "^19.2.0",
"typescript": "^4.8.4"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"../node_modules/.pnpm/esbuild@0.25.10/node_modules/esbuild": {
"version": "0.25.10",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.10",
"@esbuild/android-arm": "0.25.10",
"@esbuild/android-arm64": "0.25.10",
"@esbuild/android-x64": "0.25.10",
"@esbuild/darwin-arm64": "0.25.10",
"@esbuild/darwin-x64": "0.25.10",
"@esbuild/freebsd-arm64": "0.25.10",
"@esbuild/freebsd-x64": "0.25.10",
"@esbuild/linux-arm": "0.25.10",
"@esbuild/linux-arm64": "0.25.10",
"@esbuild/linux-ia32": "0.25.10",
"@esbuild/linux-loong64": "0.25.10",
"@esbuild/linux-mips64el": "0.25.10",
"@esbuild/linux-ppc64": "0.25.10",
"@esbuild/linux-riscv64": "0.25.10",
"@esbuild/linux-s390x": "0.25.10",
"@esbuild/linux-x64": "0.25.10",
"@esbuild/netbsd-arm64": "0.25.10",
"@esbuild/netbsd-x64": "0.25.10",
"@esbuild/openbsd-arm64": "0.25.10",
"@esbuild/openbsd-x64": "0.25.10",
"@esbuild/openharmony-arm64": "0.25.10",
"@esbuild/sunos-x64": "0.25.10",
"@esbuild/win32-arm64": "0.25.10",
"@esbuild/win32-ia32": "0.25.10",
"@esbuild/win32-x64": "0.25.10"
}
},
"../node_modules/.pnpm/execa@9.6.0/node_modules/execa": {
"version": "9.6.0",
"license": "MIT",
"dependencies": {
"@sindresorhus/merge-streams": "^4.0.0",
"cross-spawn": "^7.0.6",
"figures": "^6.1.0",
"get-stream": "^9.0.0",
"human-signals": "^8.0.1",
"is-plain-obj": "^4.1.0",
"is-stream": "^4.0.1",
"npm-run-path": "^6.0.0",
"pretty-ms": "^9.2.0",
"signal-exit": "^4.1.0",
"strip-final-newline": "^4.0.0",
"yoctocolors": "^2.1.1"
},
"devDependencies": {
"@types/node": "^22.15.21",
"ava": "^6.3.0",
"c8": "^10.1.3",
"get-node": "^15.0.3",
"is-in-ci": "^1.0.0",
"is-running": "^2.1.0",
"log-process-errors": "^12.0.1",
"path-exists": "^5.0.0",
"path-key": "^4.0.0",
"tempfile": "^5.0.0",
"tsd": "^0.32.0",
"typescript": "^5.8.3",
"which": "^5.0.0",
"xo": "^0.60.0"
},
"engines": {
"node": "^18.19.0 || >=20.5.0"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"../node_modules/.pnpm/fastmcp@3.20.0_arktype@2.1.25_effect@3.16.12/node_modules/fastmcp": {
"version": "3.20.0",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.2",
"@standard-schema/spec": "^1.0.0",
"execa": "^9.6.0",
"file-type": "^21.0.0",
"fuse.js": "^7.1.0",
"mcp-proxy": "^5.8.1",
"strict-event-emitter-types": "^2.0.0",
"undici": "^7.13.0",
"uri-templates": "^0.2.0",
"xsschema": "0.3.5",
"yargs": "^18.0.0",
"zod": "^3.25.76",
"zod-to-json-schema": "^3.24.6"
},
"bin": {
"fastmcp": "dist/bin/fastmcp.js"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@modelcontextprotocol/inspector": "^0.16.2",
"@sebbo2002/semantic-release-jsr": "^3.0.1",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^24.2.1",
"@types/uri-templates": "^0.1.34",
"@types/yargs": "^17.0.33",
"@valibot/to-json-schema": "^1.3.0",
"@wong2/mcp-cli": "^1.13.0",
"arktype": "^2.1.20",
"eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-perfectionist": "^4.15.0",
"eslint-plugin-prettier": "^5.5.4",
"eventsource-client": "^1.1.4",
"get-port-please": "^3.2.0",
"jiti": "^2.5.1",
"jsr": "^0.13.5",
"prettier": "^3.6.2",
"semantic-release": "^24.2.7",
"tsup": "^8.5.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.0",
"valibot": "^1.1.0",
"vitest": "^3.2.4"
}
},
"../node_modules/.pnpm/husky@9.1.7/node_modules/husky": {
"version": "9.1.7",
"dev": true,
"license": "MIT",
"bin": {
"husky": "bin.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"../node_modules/.pnpm/table@6.9.0/node_modules/table": {
"version": "6.9.0",
"license": "BSD-3-Clause",
"dependencies": {
"ajv": "^8.0.1",
"lodash.truncate": "^4.4.2",
"slice-ansi": "^4.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1"
},
"devDependencies": {
"@types/chai": "^4.2.16",
"@types/lodash.mapvalues": "^4.6.6",
"@types/lodash.truncate": "^4.4.6",
"@types/mocha": "^9.0.0",
"@types/node": "^14.14.37",
"@types/sinon": "^10.0.0",
"@types/slice-ansi": "^4.0.0",
"ajv-cli": "^5.0.0",
"ajv-keywords": "^5.0.0",
"chai": "^4.2.0",
"chalk": "^4.1.0",
"coveralls": "^3.1.0",
"eslint": "^7.32.0",
"eslint-config-canonical": "^25.0.0",
"gitdown": "^3.1.4",
"husky": "^4.3.6",
"js-beautify": "^1.14.0",
"lodash.mapvalues": "^4.6.0",
"mkdirp": "^1.0.4",
"mocha": "^8.2.1",
"nyc": "^15.1.0",
"semantic-release": "^17.3.1",
"sinon": "^12.0.1",
"ts-node": "^9.1.1",
"typescript": "4.5.2"
},
"engines": {
"node": ">=10.0.0"
}
},
"../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript": {
"version": "5.9.3",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"devDependencies": {
"@dprint/formatter": "^0.4.1",
"@dprint/typescript": "0.93.4",
"@esfx/canceltoken": "^1.0.0",
"@eslint/js": "^9.20.0",
"@octokit/rest": "^21.1.1",
"@types/chai": "^4.3.20",
"@types/diff": "^7.0.1",
"@types/minimist": "^1.2.5",
"@types/mocha": "^10.0.10",
"@types/ms": "^0.7.34",
"@types/node": "latest",
"@types/source-map-support": "^0.5.10",
"@types/which": "^3.0.4",
"@typescript-eslint/rule-tester": "^8.24.1",
"@typescript-eslint/type-utils": "^8.24.1",
"@typescript-eslint/utils": "^8.24.1",
"azure-devops-node-api": "^14.1.0",
"c8": "^10.1.3",
"chai": "^4.5.0",
"chokidar": "^4.0.3",
"diff": "^7.0.0",
"dprint": "^0.49.0",
"esbuild": "^0.25.0",
"eslint": "^9.20.1",
"eslint-formatter-autolinkable-stylish": "^1.4.0",
"eslint-plugin-regexp": "^2.7.0",
"fast-xml-parser": "^4.5.2",
"glob": "^10.4.5",
"globals": "^15.15.0",
"hereby": "^1.10.0",
"jsonc-parser": "^3.3.1",
"knip": "^5.44.4",
"minimist": "^1.2.8",
"mocha": "^10.8.2",
"mocha-fivemat-progress-reporter": "^0.1.0",
"monocart-coverage-reports": "^2.12.1",
"ms": "^2.1.3",
"picocolors": "^1.1.1",
"playwright": "^1.50.1",
"source-map-support": "^0.5.21",
"tslib": "^2.8.1",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"which": "^3.0.1"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/@actions/core": {
"resolved": "../node_modules/.pnpm/@actions+core@1.11.1/node_modules/@actions/core",
"link": true
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"resolved": "../node_modules/.pnpm/@anthropic-ai+claude-agent-sdk@0.1.26_zod@3.25.76/node_modules/@anthropic-ai/claude-agent-sdk",
"link": true
},
"node_modules/@ark/fs": {
"resolved": "../node_modules/.pnpm/@ark+fs@0.53.0/node_modules/@ark/fs",
"link": true
},
"node_modules/@ark/util": {
"resolved": "../node_modules/.pnpm/@ark+util@0.53.0/node_modules/@ark/util",
"link": true
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
"integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@octokit/rest": {
"resolved": "../node_modules/.pnpm/@octokit+rest@22.0.0/node_modules/@octokit/rest",
"link": true
},
"node_modules/@octokit/webhooks-types": {
"resolved": "../node_modules/.pnpm/@octokit+webhooks-types@7.6.1/node_modules/@octokit/webhooks-types",
"link": true
},
"node_modules/@standard-schema/spec": {
"resolved": "../node_modules/.pnpm/@standard-schema+spec@1.0.0/node_modules/@standard-schema/spec",
"link": true
},
"node_modules/@types/node": {
"resolved": "../node_modules/.pnpm/@types+node@24.7.2/node_modules/@types/node",
"link": true
},
"node_modules/arg": {
"resolved": "../node_modules/.pnpm/arg@5.0.2/node_modules/arg",
"link": true
},
"node_modules/arktype": {
"resolved": "../node_modules/.pnpm/arktype@2.1.25/node_modules/arktype",
"link": true
},
"node_modules/convex": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/convex/-/convex-1.29.0.tgz",
"integrity": "sha512-uoIPXRKIp2eLCkkR9WJ2vc9NtgQtx8Pml59WPUahwbrd5EuW2WLI/cf2E7XrUzOSifdQC3kJZepisk4wJNTJaA==",
"license": "Apache-2.0",
"dependencies": {
"esbuild": "0.25.4",
"prettier": "^3.0.0"
},
"bin": {
"convex": "bin/main.js"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=7.0.0"
},
"peerDependencies": {
"@auth0/auth0-react": "^2.0.1",
"@clerk/clerk-react": "^4.12.8 || ^5.0.0",
"react": "^18.0.0 || ^19.0.0-0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@auth0/auth0-react": {
"optional": true
},
"@clerk/clerk-react": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/convex/node_modules/esbuild": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
"integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.4",
"@esbuild/android-arm": "0.25.4",
"@esbuild/android-arm64": "0.25.4",
"@esbuild/android-x64": "0.25.4",
"@esbuild/darwin-arm64": "0.25.4",
"@esbuild/darwin-x64": "0.25.4",
"@esbuild/freebsd-arm64": "0.25.4",
"@esbuild/freebsd-x64": "0.25.4",
"@esbuild/linux-arm": "0.25.4",
"@esbuild/linux-arm64": "0.25.4",
"@esbuild/linux-ia32": "0.25.4",
"@esbuild/linux-loong64": "0.25.4",
"@esbuild/linux-mips64el": "0.25.4",
"@esbuild/linux-ppc64": "0.25.4",
"@esbuild/linux-riscv64": "0.25.4",
"@esbuild/linux-s390x": "0.25.4",
"@esbuild/linux-x64": "0.25.4",
"@esbuild/netbsd-arm64": "0.25.4",
"@esbuild/netbsd-x64": "0.25.4",
"@esbuild/openbsd-arm64": "0.25.4",
"@esbuild/openbsd-x64": "0.25.4",
"@esbuild/sunos-x64": "0.25.4",
"@esbuild/win32-arm64": "0.25.4",
"@esbuild/win32-ia32": "0.25.4",
"@esbuild/win32-x64": "0.25.4"
}
},
"node_modules/dotenv": {
"resolved": "../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv",
"link": true
},
"node_modules/esbuild": {
"resolved": "../node_modules/.pnpm/esbuild@0.25.10/node_modules/esbuild",
"link": true
},
"node_modules/execa": {
"resolved": "../node_modules/.pnpm/execa@9.6.0/node_modules/execa",
"link": true
},
"node_modules/fastmcp": {
"resolved": "../node_modules/.pnpm/fastmcp@3.20.0_arktype@2.1.25_effect@3.16.12/node_modules/fastmcp",
"link": true
},
"node_modules/husky": {
"resolved": "../node_modules/.pnpm/husky@9.1.7/node_modules/husky",
"link": true
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/table": {
"resolved": "../node_modules/.pnpm/table@6.9.0/node_modules/table",
"link": true
},
"node_modules/typescript": {
"resolved": "../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript",
"link": true
}
}
}
+8 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@pullfrog/action",
"version": "0.0.155",
"version": "0.0.156",
"type": "module",
"files": [
"index.js",
@@ -25,19 +25,21 @@
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.1",
"@anthropic-ai/claude-agent-sdk": "0.1.37",
"@anthropic-ai/claude-agent-sdk": "0.1.77",
"@ark/fs": "0.53.0",
"@ark/util": "0.53.0",
"@octokit/plugin-throttling": "^11.0.3",
"@octokit/rest": "^22.0.0",
"@octokit/webhooks-types": "^7.6.1",
"@openai/codex-sdk": "0.58.0",
"@opencode-ai/sdk": "^1.0.143",
"@standard-schema/spec": "1.0.0",
"@toon-format/toon": "^1.0.0",
"arktype": "2.1.28",
"package-manager-detector": "^1.6.0",
"dotenv": "^17.2.3",
"execa": "^9.6.0",
"fastmcp": "^3.20.0",
"fastmcp": "^3.26.8",
"package-manager-detector": "^1.6.0",
"table": "^6.9.0"
},
"devDependencies": {
@@ -70,5 +72,6 @@
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
},
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
}
+78 -7
View File
@@ -1,3 +1,4 @@
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { extname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
@@ -67,7 +68,9 @@ if (import.meta.url === `file://${process.argv[1]}`) {
const args = arg({
"--help": Boolean,
"--raw": String,
"--local": Boolean,
"-h": "--help",
"-l": "--local",
});
if (args["--help"]) {
@@ -81,18 +84,85 @@ Arguments:
Options:
--raw [prompt] Use raw string as prompt instead of loading from file
--local, -l Run locally on macOS (default: runs in Docker)
-h, --help Show this help message
Environment:
PLAY_LOCAL=1 Same as --local
Examples:
tsx play.ts # Use default fixture
tsx play.ts fixtures/basic.txt # Use specific text file
tsx play.ts custom.json # Use JSON file
tsx play.ts fixtures/test.ts # Use TypeScript file
tsx play.ts bash-test.ts # Run in Docker (default)
tsx play.ts --local bash-test.ts # Run locally on macOS
tsx play.ts --raw "Hello world" # Use raw string as prompt
`);
process.exit(0);
}
// default: run in Docker (unless --local or PLAY_LOCAL=1 or already inside Docker)
const isInsideDocker = existsSync("/.dockerenv");
const useLocal = args["--local"] || process.env.PLAY_LOCAL === "1" || isInsideDocker;
if (!useLocal) {
log.info("» running in Docker container...");
const passArgs = process.argv.slice(2);
const nodeCmd = `node play.ts ${passArgs.join(" ")}`;
// pass all env vars to docker
const envFlags = Object.entries(process.env).flatMap(([key, value]) =>
value !== undefined ? ["-e", `${key}=${value}`] : []
);
// SSH for git - mount individual SSH files to avoid permission issues
const sshFlags: string[] = [];
const home = process.env.HOME;
if (home) {
const sshDir = join(home, ".ssh");
// mount SSH keys (try common key names)
for (const keyName of ["id_rsa", "id_ed25519", "id_ecdsa"]) {
const keyPath = join(sshDir, keyName);
if (existsSync(keyPath)) {
sshFlags.push("-v", `${keyPath}:/root/.ssh/${keyName}:ro`);
}
}
// mount known_hosts
const knownHostsPath = join(sshDir, "known_hosts");
if (existsSync(knownHostsPath)) {
sshFlags.push("-v", `${knownHostsPath}:/root/.ssh/known_hosts:ro`);
}
}
const ttyFlags = process.stdin.isTTY ? ["-it"] : [];
const result = spawnSync(
"docker",
[
"run",
"--rm",
...ttyFlags,
"-v",
`${process.cwd()}:/app/action:cached`,
"-v",
"pullfrog-action-node-modules:/app/action/node_modules",
"-w",
"/app/action",
...envFlags,
...sshFlags,
"--cap-add",
"SYS_ADMIN",
"--security-opt",
"seccomp:unconfined",
"node:24",
"bash",
"-c",
`corepack enable pnpm >/dev/null 2>&1 && pnpm install --frozen-lockfile && ${nodeCmd}`,
],
{ stdio: "inherit" }
);
process.exit(result.status ?? 1);
}
let prompt: string;
if (args["--raw"]) {
@@ -135,10 +205,11 @@ Examples:
if (typeof module.default === "string") {
prompt = module.default;
} else if (typeof module.default === "object" && module.default.prompt) {
prompt = module.default.prompt;
} else {
} else if (typeof module.default === "object") {
// Payload objects (with ~pullfrog) should be stringified
prompt = JSON.stringify(module.default, null, 2);
} else {
throw new Error(`Unsupported default export type: ${typeof module.default}`);
}
break;
}
+199 -83
View File
@@ -15,14 +15,17 @@ importers:
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)
specifier: 0.1.77
version: 0.1.77(zod@4.3.5)
'@ark/fs':
specifier: 0.53.0
version: 0.53.0
'@ark/util':
specifier: 0.53.0
version: 0.53.0
'@octokit/plugin-throttling':
specifier: ^11.0.3
version: 11.0.3(@octokit/core@7.0.5)
'@octokit/rest':
specifier: ^22.0.0
version: 22.0.0
@@ -38,6 +41,9 @@ importers:
'@standard-schema/spec':
specifier: 1.0.0
version: 1.0.0
'@toon-format/toon':
specifier: ^1.0.0
version: 1.4.0
arktype:
specifier: 2.1.28
version: 2.1.28
@@ -48,8 +54,8 @@ importers:
specifier: ^9.6.0
version: 9.6.0
fastmcp:
specifier: ^3.20.0
version: 3.20.0(arktype@2.1.28)
specifier: ^3.26.8
version: 3.26.8(arktype@2.1.28)(hono@4.11.3)
package-manager-detector:
specifier: ^1.6.0
version: 1.6.0
@@ -90,11 +96,11 @@ packages:
'@actions/io@1.1.3':
resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==}
'@anthropic-ai/claude-agent-sdk@0.1.37':
resolution: {integrity: sha512-LMfqMIPLTz0vRhpcO7hpPJ5L6Bg24y5/PaqZvwAUNZ/GR3OAl7xmJR7IryIR6m8Pyd/6Hs2yBU8j86Os+wHFQQ==}
'@anthropic-ai/claude-agent-sdk@0.1.77':
resolution: {integrity: sha512-ZEjWQtkoB2MEY6K16DWMmF+8OhywAynH0m08V265cerbZ8xPD/2Ng2jPzbbO40mPeFSsMDJboShL+a3aObP0Jg==}
engines: {node: '>=18.0.0'}
peerDependencies:
zod: ^3.24.1
zod: ^3.25.0 || ^4.0.0
'@ark/fs@0.53.0':
resolution: {integrity: sha512-XL0EbBAZgyy+j9aPhftYaBsbKAW5PTNSKCN6oLRRdrHuHPSAZgR6765/z0YZGhPxHEUNmq0vBoSk8yOLk91dNQ==}
@@ -271,6 +277,12 @@ packages:
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
engines: {node: '>=14'}
'@hono/node-server@1.19.7':
resolution: {integrity: sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==}
engines: {node: '>=18.14.1'}
peerDependencies:
hono: ^4
'@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -308,6 +320,16 @@ packages:
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -326,15 +348,33 @@ packages:
cpu: [x64]
os: [linux]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-win32-x64@0.33.5':
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@modelcontextprotocol/sdk@1.20.0':
resolution: {integrity: sha512-kOQ4+fHuT4KbR2iq2IjeV32HiihueuOf1vJkq18z08CLZ1UQrTc8BXJpVfxZkq45+inLLD+D4xx4nBjUelJa4Q==}
'@modelcontextprotocol/sdk@1.25.2':
resolution: {integrity: sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==}
engines: {node: '>=18'}
peerDependencies:
'@cfworker/json-schema': ^4.1.1
zod: ^3.25 || ^4.0
peerDependenciesMeta:
'@cfworker/json-schema':
optional: true
'@octokit/auth-token@4.0.0':
resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==}
@@ -377,6 +417,9 @@ packages:
'@octokit/openapi-types@26.0.0':
resolution: {integrity: sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==}
'@octokit/openapi-types@27.0.0':
resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==}
'@octokit/plugin-paginate-rest@13.2.0':
resolution: {integrity: sha512-YuAlyjR8o5QoRSOvMHxSJzPtogkNMgeMv2mpccrvdUGeC3MKyfi/hS+KiFwyH/iRKIKyx+eIMsDjbt3p9r2GYA==}
engines: {node: '>= 20'}
@@ -407,6 +450,12 @@ packages:
peerDependencies:
'@octokit/core': '>=6'
'@octokit/plugin-throttling@11.0.3':
resolution: {integrity: sha512-34eE0RkFCKycLl2D2kq7W+LovheM/ex3AwZCYN8udpi6bxsyjZidb2McXs69hZhLmJlDqTSP8cH+jSRpiaijBg==}
engines: {node: '>= 20'}
peerDependencies:
'@octokit/core': ^7.0.0
'@octokit/request-error@5.1.1':
resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==}
engines: {node: '>= 18'}
@@ -436,6 +485,9 @@ packages:
'@octokit/types@15.0.0':
resolution: {integrity: sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ==}
'@octokit/types@16.0.0':
resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==}
'@octokit/webhooks-types@7.6.1':
resolution: {integrity: sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==}
@@ -456,13 +508,16 @@ packages:
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
'@tokenizer/inflate@0.2.7':
resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==}
'@tokenizer/inflate@0.4.1':
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
engines: {node: '>=18'}
'@tokenizer/token@0.3.0':
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
'@toon-format/toon@1.4.0':
resolution: {integrity: sha512-bjdhhIPjnX2oVk+pKy/nD3bwuESDLX/5fwW0TxwpV7Q4PVNkiRSv1S0sPeuy9TI4PfAlulow1HShdmMTnYvoLg==}
'@types/node@24.7.2':
resolution: {integrity: sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==}
@@ -470,8 +525,13 @@ packages:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
@@ -515,6 +575,9 @@ packages:
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
engines: {node: '>=18'}
bottleneck@2.19.5:
resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==}
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
@@ -639,6 +702,10 @@ packages:
resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==}
engines: {node: ^18.19.0 || >=20.5.0}
execa@9.6.1:
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
engines: {node: ^18.19.0 || >=20.5.0}
express-rate-limit@7.5.1:
resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==}
engines: {node: '>= 16'}
@@ -655,25 +722,24 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
fastmcp@3.20.0:
resolution: {integrity: sha512-W4Mx0Ft0mqb+BvZ2rHijQQSIKCX8FlRhLOCpK22rMJMxFR33rRyoDRa/WZ4+sDTQc099G7JvkrMBpkTJv1O12g==}
fastmcp@3.26.8:
resolution: {integrity: sha512-DQgvEdSoQpISqPDgLZe5K2RU8ICCz3/5QcEhHmz4KiNcNSdFOo7o817mGwWxPffOLe36vypCulq22cvEwqBi/A==}
hasBin: true
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
peerDependencies:
jose: ^5.0.0
peerDependenciesMeta:
jose:
optional: true
figures@6.1.0:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'}
file-type@21.0.0:
resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==}
file-type@21.3.0:
resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==}
engines: {node: '>=20'}
finalhandler@2.1.0:
@@ -727,6 +793,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hono@4.11.3:
resolution: {integrity: sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==}
engines: {node: '>=16.9.0'}
http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
@@ -780,12 +850,15 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
jose@6.1.3:
resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-schema-typed@8.0.2:
resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
@@ -793,8 +866,8 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mcp-proxy@5.9.0:
resolution: {integrity: sha512-xonJSkuy4wmwXeykBFl0mjRMt4D0pAGCtFIfBFUz4O80VrO92ruJseIdASJO3wN6MdYHi3vbZLOQol3NsUpg4g==}
mcp-proxy@5.12.5:
resolution: {integrity: sha512-Vawdc8vi36fXxKCaDpluRvbGcmrUXJdvXcDhkh30HYsws8XqX2rWPBflZpavzeS+6SwijRFV7g+9ypQRJZlrEQ==}
hasBin: true
media-typer@1.1.0:
@@ -873,10 +946,6 @@ packages:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
qs@6.14.0:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
@@ -1035,9 +1104,6 @@ packages:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
uri-templates@0.2.0:
resolution: {integrity: sha512-EWkjYEN0L6KOfEoOH6Wj4ghQqU7eBZMJqRHQnxQAq+dSEzRPClkWjf8557HkWQXF6BrAUoLSAyy9i3RVTliaNg==}
@@ -1057,8 +1123,8 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
xsschema@0.3.5:
resolution: {integrity: sha512-f+dcy11dTv7aBEEfTbXWnrm/1b/Ds2k4taN0TpN6KCtPAD58kyE/R1znUdrHdBnhIFexj9k+pS1fm6FgtSISrQ==}
xsschema@0.4.0-beta.5:
resolution: {integrity: sha512-73pYwf1hMy++7SnOkghJdgdPaGi+Y5I0SaO6rIlxb1ouV6tEyDbEcXP82kyr32KQVTlUbFj6qewi9eUVEiXm+g==}
peerDependencies:
'@valibot/to-json-schema': ^1.0.0
arktype: ^2.1.20
@@ -1096,13 +1162,13 @@ packages:
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
engines: {node: '>=18'}
zod-to-json-schema@3.24.6:
resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==}
zod-to-json-schema@3.25.1:
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
peerDependencies:
zod: ^3.24.1
zod: ^3.25 || ^4
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zod@4.3.5:
resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==}
snapshots:
@@ -1132,15 +1198,17 @@ snapshots:
'@actions/io@1.1.3': {}
'@anthropic-ai/claude-agent-sdk@0.1.37(zod@3.25.76)':
'@anthropic-ai/claude-agent-sdk@0.1.77(zod@4.3.5)':
dependencies:
zod: 3.25.76
zod: 4.3.5
optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.5
'@img/sharp-darwin-x64': 0.33.5
'@img/sharp-linux-arm': 0.33.5
'@img/sharp-linux-arm64': 0.33.5
'@img/sharp-linux-x64': 0.33.5
'@img/sharp-linuxmusl-arm64': 0.33.5
'@img/sharp-linuxmusl-x64': 0.33.5
'@img/sharp-win32-x64': 0.33.5
'@ark/fs@0.53.0': {}
@@ -1235,6 +1303,10 @@ snapshots:
'@fastify/busboy@2.1.1': {}
'@hono/node-server@1.19.7(hono@4.11.3)':
dependencies:
hono: 4.11.3
'@img/sharp-darwin-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.0.4
@@ -1260,6 +1332,12 @@ snapshots:
'@img/sharp-libvips-linux-x64@1.0.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
optional: true
'@img/sharp-linux-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.0.4
@@ -1275,12 +1353,24 @@ snapshots:
'@img/sharp-libvips-linux-x64': 1.0.4
optional: true
'@img/sharp-linuxmusl-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
optional: true
'@img/sharp-linuxmusl-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
optional: true
'@img/sharp-win32-x64@0.33.5':
optional: true
'@modelcontextprotocol/sdk@1.20.0':
'@modelcontextprotocol/sdk@1.25.2(hono@4.11.3)(zod@4.3.5)':
dependencies:
ajv: 6.12.6
'@hono/node-server': 1.19.7(hono@4.11.3)
ajv: 8.17.1
ajv-formats: 3.0.1(ajv@8.17.1)
content-type: 1.0.5
cors: 2.8.5
cross-spawn: 7.0.6
@@ -1288,11 +1378,14 @@ snapshots:
eventsource-parser: 3.0.6
express: 5.1.0
express-rate-limit: 7.5.1(express@5.1.0)
jose: 6.1.3
json-schema-typed: 8.0.2
pkce-challenge: 5.0.0
raw-body: 3.0.1
zod: 3.25.76
zod-to-json-schema: 3.24.6(zod@3.25.76)
zod: 4.3.5
zod-to-json-schema: 3.25.1(zod@4.3.5)
transitivePeerDependencies:
- hono
- supports-color
'@octokit/auth-token@4.0.0': {}
@@ -1347,6 +1440,8 @@ snapshots:
'@octokit/openapi-types@26.0.0': {}
'@octokit/openapi-types@27.0.0': {}
'@octokit/plugin-paginate-rest@13.2.0(@octokit/core@7.0.5)':
dependencies:
'@octokit/core': 7.0.5
@@ -1371,6 +1466,12 @@ snapshots:
'@octokit/core': 7.0.5
'@octokit/types': 15.0.0
'@octokit/plugin-throttling@11.0.3(@octokit/core@7.0.5)':
dependencies:
'@octokit/core': 7.0.5
'@octokit/types': 16.0.0
bottleneck: 2.19.5
'@octokit/request-error@5.1.1':
dependencies:
'@octokit/types': 13.10.0
@@ -1415,6 +1516,10 @@ snapshots:
dependencies:
'@octokit/openapi-types': 26.0.0
'@octokit/types@16.0.0':
dependencies:
'@octokit/openapi-types': 27.0.0
'@octokit/webhooks-types@7.6.1': {}
'@openai/codex-sdk@0.58.0': {}
@@ -1427,16 +1532,17 @@ snapshots:
'@standard-schema/spec@1.0.0': {}
'@tokenizer/inflate@0.2.7':
'@tokenizer/inflate@0.4.1':
dependencies:
debug: 4.4.3
fflate: 0.8.2
token-types: 6.1.1
transitivePeerDependencies:
- supports-color
'@tokenizer/token@0.3.0': {}
'@toon-format/toon@1.4.0': {}
'@types/node@24.7.2':
dependencies:
undici-types: 7.14.0
@@ -1446,12 +1552,9 @@ snapshots:
mime-types: 3.0.1
negotiator: 1.0.0
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
fast-json-stable-stringify: 2.1.0
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ajv-formats@3.0.1(ajv@8.17.1):
optionalDependencies:
ajv: 8.17.1
ajv@8.17.1:
dependencies:
@@ -1502,6 +1605,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
bottleneck@2.19.5: {}
bytes@3.1.2: {}
call-bind-apply-helpers@1.0.2:
@@ -1635,6 +1740,21 @@ snapshots:
strip-final-newline: 4.0.0
yoctocolors: 2.1.2
execa@9.6.1:
dependencies:
'@sindresorhus/merge-streams': 4.0.0
cross-spawn: 7.0.6
figures: 6.1.0
get-stream: 9.0.1
human-signals: 8.0.1
is-plain-obj: 4.1.0
is-stream: 4.0.1
npm-run-path: 6.0.0
pretty-ms: 9.3.0
signal-exit: 4.1.0
strip-final-newline: 4.0.0
yoctocolors: 2.1.2
express-rate-limit@7.5.1(express@5.1.0):
dependencies:
express: 5.1.0
@@ -1675,41 +1795,39 @@ snapshots:
fast-deep-equal@3.1.3: {}
fast-json-stable-stringify@2.1.0: {}
fast-uri@3.1.0: {}
fastmcp@3.20.0(arktype@2.1.28):
fastmcp@3.26.8(arktype@2.1.28)(hono@4.11.3):
dependencies:
'@modelcontextprotocol/sdk': 1.20.0
'@modelcontextprotocol/sdk': 1.25.2(hono@4.11.3)(zod@4.3.5)
'@standard-schema/spec': 1.0.0
execa: 9.6.0
file-type: 21.0.0
execa: 9.6.1
file-type: 21.3.0
fuse.js: 7.1.0
mcp-proxy: 5.9.0
mcp-proxy: 5.12.5
strict-event-emitter-types: 2.0.0
undici: 7.16.0
uri-templates: 0.2.0
xsschema: 0.3.5(arktype@2.1.28)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76)
xsschema: 0.4.0-beta.5(arktype@2.1.28)(zod-to-json-schema@3.25.1(zod@4.3.5))(zod@4.3.5)
yargs: 18.0.0
zod: 3.25.76
zod-to-json-schema: 3.24.6(zod@3.25.76)
zod: 4.3.5
zod-to-json-schema: 3.25.1(zod@4.3.5)
transitivePeerDependencies:
- '@cfworker/json-schema'
- '@valibot/to-json-schema'
- arktype
- effect
- hono
- supports-color
- sury
fflate@0.8.2: {}
figures@6.1.0:
dependencies:
is-unicode-supported: 2.1.0
file-type@21.0.0:
file-type@21.3.0:
dependencies:
'@tokenizer/inflate': 0.2.7
'@tokenizer/inflate': 0.4.1
strtok3: 10.3.4
token-types: 6.1.1
uint8array-extras: 1.5.0
@@ -1770,6 +1888,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
hono@4.11.3: {}
http-errors@2.0.0:
dependencies:
depd: 2.0.0
@@ -1808,15 +1928,17 @@ snapshots:
isexe@2.0.0: {}
json-schema-traverse@0.4.1: {}
jose@6.1.3: {}
json-schema-traverse@1.0.0: {}
json-schema-typed@8.0.2: {}
lodash.truncate@4.4.2: {}
math-intrinsics@1.1.0: {}
mcp-proxy@5.9.0: {}
mcp-proxy@5.12.5: {}
media-typer@1.1.0: {}
@@ -1872,8 +1994,6 @@ snapshots:
forwarded: 0.2.0
ipaddr.js: 1.9.1
punycode@2.3.1: {}
qs@6.14.0:
dependencies:
side-channel: 1.1.0
@@ -2048,10 +2168,6 @@ snapshots:
unpipe@1.0.0: {}
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
uri-templates@0.2.0: {}
vary@1.1.2: {}
@@ -2068,11 +2184,11 @@ snapshots:
wrappy@1.0.2: {}
xsschema@0.3.5(arktype@2.1.28)(zod-to-json-schema@3.24.6(zod@3.25.76))(zod@3.25.76):
xsschema@0.4.0-beta.5(arktype@2.1.28)(zod-to-json-schema@3.25.1(zod@4.3.5))(zod@4.3.5):
optionalDependencies:
arktype: 2.1.28
zod: 3.25.76
zod-to-json-schema: 3.24.6(zod@3.25.76)
zod: 4.3.5
zod-to-json-schema: 3.25.1(zod@4.3.5)
y18n@5.0.8: {}
@@ -2089,8 +2205,8 @@ snapshots:
yoctocolors@2.1.2: {}
zod-to-json-schema@3.24.6(zod@3.25.76):
zod-to-json-schema@3.25.1(zod@4.3.5):
dependencies:
zod: 3.25.76
zod: 4.3.5
zod@3.25.76: {}
zod@4.3.5: {}
+2 -4
View File
@@ -1,6 +1,5 @@
import { Octokit } from "@octokit/rest";
import { fetchWorkflowRunInfo } from "./api.ts";
import { getGitHubInstallationToken, parseRepoContext } from "./github.ts";
import { createOctokit, getGitHubInstallationToken, parseRepoContext } from "./github.ts";
/**
* Get progress comment ID from environment variable or database.
@@ -55,8 +54,7 @@ export async function reportErrorToComment({
// update comment directly using GitHub API
const repoContext = parseRepoContext();
const token = getGitHubInstallationToken();
const octokit = new Octokit({ auth: token });
const octokit = createOctokit(getGitHubInstallationToken());
await octokit.rest.issues.updateComment({
owner: repoContext.owner,
+44 -16
View File
@@ -1,5 +1,8 @@
import assert from "node:assert/strict";
import { createSign } from "node:crypto";
import * as core from "@actions/core";
import { throttling } from "@octokit/plugin-throttling";
import { Octokit } from "@octokit/rest";
import { log } from "./cli.ts";
import { retry } from "./retry.ts";
@@ -44,8 +47,11 @@ interface RepositoriesResponse {
repositories: Repository[];
}
function isGitHubActionsEnvironment(): boolean {
return Boolean(process.env.GITHUB_ACTIONS);
function isOIDCAvailable(): boolean {
// OIDC requires both env vars to be set (only in real GitHub Actions with id-token permission)
return Boolean(
process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
);
}
async function acquireTokenViaOIDC(): Promise<string> {
@@ -233,7 +239,7 @@ async function acquireTokenViaGitHubApp(): Promise<string> {
}
async function acquireNewToken(): Promise<string> {
if (isGitHubActionsEnvironment()) {
if (isOIDCAvailable()) {
return await retry(() => acquireTokenViaOIDC(), { label: "token exchange" });
} else {
return await acquireTokenViaGitHubApp();
@@ -246,31 +252,32 @@ let githubInstallationToken: string | undefined;
/**
* Setup GitHub installation token for the action
*/
export async function setupGitHubInstallationToken(): Promise<string> {
export async function setupGitHubInstallationToken() {
assert(!githubInstallationToken, "GitHub installation token is already set.");
const acquiredToken = await acquireNewToken();
core.setSecret(acquiredToken);
githubInstallationToken = acquiredToken;
return acquiredToken;
return {
token: acquiredToken,
[Symbol.asyncDispose]() {
githubInstallationToken = undefined;
return revokeGitHubInstallationToken(acquiredToken);
},
};
}
/**
* Get the GitHub installation token from memory
*/
export function getGitHubInstallationToken(): string {
if (!githubInstallationToken) {
throw new Error("GitHub installation token not set. Call setupGitHubInstallationToken first.");
}
assert(
githubInstallationToken,
"GitHub installation token not set. Call setupGitHubInstallationToken first."
);
return githubInstallationToken;
}
export async function revokeGitHubInstallationToken(): Promise<void> {
if (!githubInstallationToken) {
return;
}
const token = githubInstallationToken;
githubInstallationToken = undefined;
async function revokeGitHubInstallationToken(token: string): Promise<void> {
const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
try {
@@ -311,3 +318,24 @@ export function parseRepoContext(): RepoContext {
return { owner, name };
}
export type OctokitWithPlugins = InstanceType<
ReturnType<typeof Octokit.plugin<typeof Octokit, [typeof throttling]>>
>;
export function createOctokit(token: string): OctokitWithPlugins {
// `OctokitWithPlugins` initialization based on https://github.com/actions/toolkit/blob/2506e78e82fbd2f9e94d63e75f5309118c8de1b1/packages/github/src/github.ts#L15-L22
// we can't use it directly because it's stuck on `@octokit/core@v5` and we use the hottest `@octokit/core@v7`
const OctokitWithPlugins = Octokit.plugin(throttling);
return new OctokitWithPlugins({
auth: token,
throttle: {
onRateLimit: (retryAfter, options, octokit, retryCount) => {
return retryCount <= 2;
},
onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => {
return retryCount <= 2;
},
},
});
}
+3 -2
View File
@@ -37,11 +37,12 @@ export async function retry<T>(fn: () => Promise<T>, options: RetryOptions = {})
}
const delay = delayMs * attempt;
log.warning(`» ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`);
log.warning(
`» ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`
);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}
+2 -2
View File
@@ -1,10 +1,10 @@
import { execSync } from "node:child_process";
import { existsSync, rmSync } from "node:fs";
import type { Octokit } from "@octokit/rest";
import type { Payload } from "../external.ts";
import type { ToolState } from "../main.ts";
import { checkoutPrBranch } from "../mcp/checkout.ts";
import { log } from "./cli.ts";
import type { OctokitWithPlugins } from "./github.ts";
import { $ } from "./shell.ts";
export interface SetupOptions {
@@ -66,7 +66,7 @@ interface SetupGitAuthParams {
owner: string;
name: string;
payload: Payload;
octokit: Octokit;
octokit: OctokitWithPlugins;
toolState: ToolState;
}