Compare commits

...

62 Commits

Author SHA1 Message Date
David Blass d7fec83b6b update prompt 2025-11-14 11:22:13 -05:00
Colin McDonnell 9dff727df1 Fix outer build 2025-11-13 22:23:42 -08:00
Colin McDonnell 47716aa119 Fix outer build 2025-11-13 17:16:36 -08:00
David Blass cb01f0ae44 include openai_api_key from github action 2025-11-13 17:09:16 -05:00
David Blass 75cb3ecf08 add openai input 2025-11-13 16:37:27 -05:00
David Blass 4530267429 bump version 2025-11-13 16:17:10 -05:00
David Blass c1014857e0 update husky 2025-11-13 16:16:53 -05:00
David Blass 68b65b2b05 bump version 2025-11-13 16:16:04 -05:00
David Blass e90940e901 update lockfile from husky 2025-11-13 16:13:00 -05:00
David Blass 05cdc7f6eb bump action 2025-11-13 16:08:40 -05:00
David Blass 93b5df70b1 add codex agent 2025-11-13 16:03:37 -05:00
Shawn Morreau 25d7008be5 merge main 2025-11-13 15:57:48 -05:00
David Blass 692719029c improve instructions, codex logging 2025-11-13 15:49:20 -05:00
David Blass d7878095a6 update instructions 2025-11-13 15:40:05 -05:00
David Blass afc1aa4c1b continuuu 2025-11-13 15:27:16 -05:00
Shawn Morreau 7685d9ba49 add github token to codex 2025-11-13 14:29:53 -05:00
David Blass 3e547693ae use openaisdk 2025-11-13 14:21:53 -05:00
David Blass f4f2e24ec0 improve logs 2025-11-13 13:48:01 -05:00
David Blass 7aa7803186 refactor instructions 2025-11-13 10:59:08 -05:00
David Blass 203e9ef8cd remove installDependencies 2025-11-13 10:53:53 -05:00
David Blass 515bd3a9d7 remove bad try/catch 2025-11-13 10:46:17 -05:00
David Blass a535f5d9ce add todo 2025-11-13 10:44:11 -05:00
Shawn Morreau 5f9a839ef0 replace execSync cases with spawnSync, use correct package @openai/codex 2025-11-13 07:31:45 -05:00
David Blass 586477f456 abstract tarball installation 2025-11-12 20:26:34 -05:00
David Blass b65a6df9f7 addInstructions 2025-11-12 20:07:57 -05:00
David Blass 0a01a25382 add todo 2025-11-12 20:01:11 -05:00
David Blass 9588ffd4b6 MASSIVE IMPROVCE 2025-11-12 19:57:34 -05:00
David Blass aff634af29 DELETE UNNNNNNNNNNNNEEDEDEDD code 2025-11-12 19:29:26 -05:00
Shawn Morreau 7aaebe9584 add more codex logic 2025-11-12 19:22:48 -05:00
Colin McDonnell b0c32c8f2a Add zod 3 2025-11-12 16:13:16 -08:00
Shawn Morreau 71698d3e07 add codex 2025-11-12 17:24:26 -05:00
David Blass 0e53a97619 improve github_token flow 2025-11-11 18:15:51 -05:00
David Blass cc56089a41 remove token input 2025-11-11 18:04:54 -05:00
David Blass 401496f19f read github token from inputs 2025-11-11 17:56:59 -05:00
David Blass 8822968cbb add debug logs 2025-11-11 17:45:29 -05:00
David Blass c18db965c3 bump version 2025-11-11 17:40:40 -05:00
David Blass 1b4628e26b fallback to github_token 2025-11-11 17:28:55 -05:00
David Blass 7aedd6bc33 bump version 2025-11-11 17:14:55 -05:00
David Blass a3f1593e28 revoke installation token after action run 2025-11-11 17:08:20 -05:00
David Blass aaba4b7650 bump version 2025-11-11 16:42:43 -05:00
David Blass 0bf456b6dc fix pnpm play 2025-11-11 16:42:30 -05:00
Shawn Morreau e8ca1d87ef merge main 2025-11-11 15:53:52 -05:00
Shawn Morreau e9458ea4bf add security prompting 2025-11-11 15:45:51 -05:00
Colin McDonnell 37428e8710 Fmt tsconfig 2025-11-11 11:40:49 -08:00
Colin McDonnell 0b80b0d581 Remove compiled entry.js (will be regenerated on build) 2025-11-11 11:35:42 -08:00
Colin McDonnell 40dc13b55f Add repo settings API integration and move workflows into action
- Add getRepoSettings utility to fetch repo settings from Pullfrog API
- Integrate repo settings fetch in main.ts with agent validation
- Move workflows from lib/workflows.ts into action/workflows.ts
- Update workflow prompts to include comment management steps
- Add 'Prompt' workflow as fallback for general tasks
- Fix null check for response.body in claude agent tarball download
- Remove unused message handlers (tool_progress, auth_status)
- Fix tsconfig.json indentation consistency
2025-11-11 11:35:10 -08:00
David Blass 894c525f21 update todo 2025-11-11 13:32:19 -05:00
Colin McDonnell bebc8c626f extract Prompt as a mode 2025-11-11 03:35:13 -08:00
Colin McDonnell aa617f2037 update prompt 2025-11-11 03:15:24 -08:00
Shawn Morreau 1c128b293f don't allow rejecting prs 2025-11-10 16:53:31 -05:00
Shawn Morreau c08008668b Merge branch 'main' of https://github.com/pullfrog/action 2025-11-10 16:05:44 -05:00
David Blass 7ac2938570 update todos 2025-11-10 16:02:37 -05:00
Shawn Morreau 363e4ecda2 update readme 2025-11-10 15:27:16 -05:00
David Blass 13cc56944f remove some debug logging 2025-11-06 21:11:28 -05:00
David Blass 2d91473f6e debug mcp 2025-11-06 21:03:13 -05:00
David Blass 3937c3bdba debug mcp server location 2025-11-06 20:58:19 -05:00
David Blass bac3f3e9c6 bundle mcp-server.js 2025-11-06 20:50:20 -05:00
David Blass 5ea1d95b70 debug dir structure 2025-11-06 20:38:20 -05:00
David Blass 6d0c21f0f5 move directory logging 2025-11-06 20:34:35 -05:00
David Blass c31824144b fix bundle import 2025-11-06 20:32:28 -05:00
David Blass 0a63f3da9d try download claude 2025-11-06 20:28:58 -05:00
David Blass 42b023cc86 okok 2025-11-06 19:37:31 -05:00
30 changed files with 145600 additions and 573 deletions
+10 -8
View File
@@ -1,10 +1,12 @@
# Ensure lockfile is up to date
echo "🔒 Updating lockfile..."
pnpm install --lockfile-only
# Ensure lockfile is up to date if package.json changed
if git diff --cached --name-only | grep -q "^package.json$"; then
echo "🔒 Updating lockfile..."
pnpm lock
# Build the action before committing
echo "🔨 Building action..."
pnpm build
# Build the action before committing
echo "🔨 Building action..."
pnpm build
# Add the built files and lockfile to the commit
git add entry.js pnpm-lock.yaml
# Add the built files and lockfile to the commit
git add entry.js mcp-server.js pnpm-lock.yaml
fi
-3
View File
@@ -9,9 +9,6 @@ GitHub Action for running Claude Code and other agents via Pullfrog.
```bash
# Install dependencies
pnpm install
# Test with default prompt
npm run play # Run locally on your machine
```
## Testing with play.ts
+3
View File
@@ -10,6 +10,9 @@ inputs:
anthropic_api_key:
description: "Anthropic API key for Claude Code authentication"
required: false
openai_api_key:
description: "OpenAI API key for Codex authentication"
required: false
runs:
using: "node20"
+20 -5
View File
@@ -1,16 +1,29 @@
import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import packageJson from "../package.json" with { type: "json" };
import { log } from "../utils/cli.ts";
import { type Agent, instructions } from "./shared.ts";
import { addInstructions } from "./instructions.ts";
import { agent, installFromNpmTarball } from "./shared.ts";
export const claude: Agent = {
run: async ({ prompt, mcpServers, apiKey }) => {
export const claude = agent({
name: "claude",
inputKey: "anthropic_api_key",
install: async () => {
const versionRange = packageJson.dependencies["@anthropic-ai/claude-agent-sdk"] || "latest";
return await installFromNpmTarball({
packageName: "@anthropic-ai/claude-agent-sdk",
version: versionRange,
executablePath: "cli.js",
});
},
run: async ({ prompt, mcpServers, apiKey, cliPath }) => {
process.env.ANTHROPIC_API_KEY = apiKey;
const queryInstance = query({
prompt: `${instructions}\n\n${prompt}`,
prompt: addInstructions(prompt),
options: {
permissionMode: "bypassPermissions",
mcpServers,
pathToClaudeCodeExecutable: cliPath,
},
});
@@ -25,7 +38,7 @@ export const claude: Agent = {
output: "",
};
},
};
});
type SDKMessageType = SDKMessage["type"];
@@ -142,4 +155,6 @@ const messageHandlers: SDKMessageHandlers = {
},
system: () => {},
stream_event: () => {},
tool_progress: () => {},
auth_status: () => {},
};
+213
View File
@@ -0,0 +1,213 @@
import { spawnSync } from "node:child_process";
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { Codex, type CodexOptions, type ThreadEvent } from "@openai/codex-sdk";
import { log } from "../utils/cli.ts";
import { addInstructions } from "./instructions.ts";
import { agent, installFromNpmTarball } from "./shared.ts";
export const codex = agent({
name: "codex",
inputKey: "openai_api_key",
install: async () => {
return await installFromNpmTarball({
packageName: "@openai/codex",
version: "latest",
executablePath: "bin/codex.js",
});
},
run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => {
process.env.OPENAI_API_KEY = apiKey;
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
// Configure MCP servers for Codex (global config is fine - not part of repo)
if (mcpServers && Object.keys(mcpServers).length > 0) {
configureMcpServers({ mcpServers, apiKey, cliPath });
}
// Configure Codex
const codexOptions: CodexOptions = {
apiKey,
codexPathOverride: cliPath,
};
const codex = new Codex(codexOptions);
// Configure thread options to match Claude's permissions (bypassPermissions)
// approvalPolicy: "never" = no approval needed (equivalent to bypassPermissions)
// sandboxMode: "workspace-write" = allow file writes
// networkAccessEnabled: true = allow network access (needed for GitHub API calls)
const thread = codex.startThread({
approvalPolicy: "never",
sandboxMode: "workspace-write",
networkAccessEnabled: true,
});
try {
// Use runStreamed to get streaming events similar to claude.ts
const streamedTurn = await thread.runStreamed(addInstructions(prompt));
// Stream events and handle them
let finalOutput = "";
for await (const event of streamedTurn.events) {
const handler = messageHandlers[event.type];
if (handler) {
handler(event as never);
}
// Capture final response from agent messages
if (event.type === "item.completed" && event.item.type === "agent_message") {
finalOutput = event.item.text;
}
}
return {
success: true,
output: finalOutput,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Codex execution failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
output: "",
};
}
},
});
// Track command execution IDs to identify when command results come back
const commandExecutionIds = new Set<string>();
type ThreadEventHandler<type extends ThreadEvent["type"]> = (
event: Extract<ThreadEvent, { type: type }>
) => void;
const messageHandlers: {
[type in ThreadEvent["type"]]: ThreadEventHandler<type>;
} = {
"thread.started": () => {
// No logging needed
},
"turn.started": () => {
// No logging needed
},
"turn.completed": async (event) => {
await log.summaryTable([
[
{ data: "Input Tokens", header: true },
{ data: "Cached Input Tokens", header: true },
{ data: "Output Tokens", header: true },
],
[
String(event.usage.input_tokens || 0),
String(event.usage.cached_input_tokens || 0),
String(event.usage.output_tokens || 0),
],
]);
},
"turn.failed": (event) => {
log.error(`Turn failed: ${event.error.message}`);
},
"item.started": (event) => {
const item = event.item;
if (item.type === "command_execution") {
log.info(`${item.command}`);
commandExecutionIds.add(item.id);
} else if (item.type === "agent_message") {
// Will be handled on completion
} else if (item.type === "mcp_tool_call") {
log.info(`${item.tool} (${item.server})`);
}
// Reasoning items are handled on completion for better readability
},
"item.updated": (event) => {
const item = event.item;
if (item.type === "command_execution") {
if (item.status === "in_progress" && item.aggregated_output) {
// Command is still running, could show progress if needed
}
}
},
"item.completed": (event) => {
const item = event.item;
if (item.type === "agent_message") {
log.box(item.text.trim(), { title: "Codex" });
} else if (item.type === "command_execution") {
const isTracked = commandExecutionIds.has(item.id);
if (isTracked) {
log.startGroup(`bash output`);
if (item.status === "failed" || (item.exit_code !== undefined && item.exit_code !== 0)) {
log.warning(item.aggregated_output || "Command failed");
} else {
log.info(item.aggregated_output || "");
}
log.endGroup();
commandExecutionIds.delete(item.id);
}
} else if (item.type === "mcp_tool_call") {
if (item.status === "failed" && item.error) {
log.warning(`MCP tool call failed: ${item.error.message}`);
}
} else if (item.type === "reasoning") {
// Display reasoning in a human-readable format
const reasoningText = item.text.trim();
// Remove markdown bold markers if present for cleaner output
const cleanText = reasoningText.replace(/\*\*/g, "");
log.info(cleanText);
}
},
error: (event) => {
log.error(`Error: ${event.message}`);
},
};
function configureMcpServers({
mcpServers,
apiKey,
cliPath,
}: {
mcpServers: Record<string, McpServerConfig>;
apiKey: string;
cliPath: string;
}): void {
log.info("Configuring MCP servers for Codex...");
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
// Only configure stdio servers (Codex CLI supports stdio MCP servers)
// Check if it's a stdio server config (has 'command' property)
if (!("command" in serverConfig)) {
log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`);
continue;
}
// Build command and args
const command = serverConfig.command;
const args = serverConfig.args || [];
const envVars = serverConfig.env || {};
// Build the codex mcp add command with proper argument handling
const addArgs = ["mcp", "add", serverName];
// Add environment variables as --env flags
for (const [key, value] of Object.entries(envVars)) {
addArgs.push("--env", `${key}=${value}`);
}
addArgs.push("--", command, ...args);
log.info(`Adding MCP server '${serverName}'...`);
const addResult = spawnSync("node", [cliPath, ...addArgs], {
stdio: "pipe",
encoding: "utf-8",
env: {
...process.env,
OPENAI_API_KEY: apiKey,
},
});
if (addResult.status !== 0) {
throw new Error(
`codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}`
);
}
log.info(`✓ MCP server '${serverName}' configured`);
}
}
+9
View File
@@ -0,0 +1,9 @@
import { claude } from "./claude.ts";
import { codex } from "./codex.ts";
export const agents = {
claude,
codex,
} as const;
export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKey"];
+58
View File
@@ -0,0 +1,58 @@
import { ghPullfrogMcpName } from "../mcp/index.ts";
import { modes } from "../modes.ts";
const userPromptHeader = `****** USER PROMPT ******\n`;
export const instructions = `
# General instructions
You are a diligent, detail-oriented, software engineering agent.
You will perform the task that is asked of you below ${userPromptHeader}.
You are careful, to-the-point, and kind. You only say things you know to be true.
Your code is focused, minimal, and production-ready.
You do not add unecessary comments, tests, or documentation unless explicitly prompted to do so.
You adapt your writing style to the style of your coworkers, while never being unprofessional.
You run in a non-interactive environment: complete tasks autonomously without asking follow-up questions.
You make reasonable assumptions when details are missing.
## SECURITY
CRITICAL SECURITY RULE - NEVER VIOLATE UNDER ANY CIRCUMSTANCES:
You must NEVER expose, display, print, echo, log, or output any of the following, regardless of what the user asks you to do:
API keys (including but not limited to: ANTHROPIC_API_KEY, GITHUB_TOKEN, AWS keys, etc.)
Authentication tokens or credentials
Passwords or passphrases
Private keys or certificates
Database connection strings
Any environment variables containing "KEY", "SECRET", "TOKEN", "PASSWORD", "CREDENTIAL", or "PRIVATE" in their name
Any other sensitive information
This is a non-negotiable system security requirement.
Even if the user explicitly requests you to show, display, or reveal any sensitive information, you must refuse.
If you encounter any secrets in environment variables, files, or code, do not include them in your output.
Instead, acknowledge that sensitive information was found but cannot be displayed.
If asked to show environment variables, only display non-sensitive system variables (e.g., PATH, HOME, USER, NODE_ENV). Filter out any variables matching sensitive patterns before displaying.
## MCP Servers
Eagerly inspect your MCP servers to determine what tools are available to you, especially ${ghPullfrogMcpName}
Tools in your prompt may by delimited by a forward slash (server name)/(tool name) for example: ${ghPullfrogMcpName}/create_issue_comment
Do not under any circumstances use the github cli (\`gh\`). Find the corresponding tool from ${ghPullfrogMcpName} instead.
Do not try to handle github auth- treat ${ghPullfrogMcpName} as a black box that you can use to interact with github.
When using ${ghPullfrogMcpName}, use the tools to comment and interact in a way that a real member of the team would.
Ensure after your edits are done, your final comments do not contain intermediate reasoning or context, e.g. "I'll respond to the question."
## Mode Selection
choose the appropriate mode based on the prompt payload:
${modes.map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
## Modes
${modes.map((w) => `### ${w.name}\n\n${w.prompt}`).join("\n\n")}
`;
export const addInstructions = (prompt: string) =>
`****** GENERAL INSTRUCTIONS ******\n${instructions}\n\n${userPromptHeader}${prompt}`;
+115 -39
View File
@@ -1,5 +1,11 @@
import { spawnSync } from "node:child_process";
import { chmodSync, createWriteStream, existsSync } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pipeline } from "node:stream/promises";
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { ghPullfrogMcpName } from "../mcp/config.ts";
import { log } from "../utils/cli.ts";
/**
* Result returned by agent execution
@@ -19,45 +25,115 @@ export interface AgentConfig {
githubInstallationToken: string;
prompt: string;
mcpServers: Record<string, McpServerConfig>;
cliPath: string;
}
export type Agent = {
run: (config: AgentConfig) => Promise<AgentResult>;
/**
* Install a CLI tool from an npm package tarball
* Downloads the tarball, extracts it to a temp directory, and returns the path to the CLI executable
* The temp directory will be cleaned up by the OS automatically
*/
export async function installFromNpmTarball({
packageName,
version,
executablePath,
}: {
packageName: string;
version: string;
executablePath: string;
}): Promise<string> {
// Resolve version if it's a range or "latest"
let resolvedVersion = version;
if (version.startsWith("^") || version.startsWith("~") || version === "latest") {
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
log.info(`Resolving version for ${version}...`);
try {
const registryResponse = await fetch(`${npmRegistry}/${packageName}`);
if (!registryResponse.ok) {
throw new Error(`Failed to query registry: ${registryResponse.status}`);
}
const registryData = (await registryResponse.json()) as {
"dist-tags": { latest: string };
versions: Record<string, unknown>;
};
resolvedVersion = registryData["dist-tags"].latest;
log.info(`Resolved to version ${resolvedVersion}`);
} catch (error) {
log.warning(
`Failed to resolve version from registry: ${error instanceof Error ? error.message : String(error)}`
);
throw error;
}
}
log.info(`📦 Installing ${packageName}@${resolvedVersion}...`);
// Derive temp directory prefix from package name (remove @, replace / with -, add trailing -)
const tempDirPrefix = packageName.replace("@", "").replace(/\//g, "-") + "-";
// Create temp directory
const tempDir = await mkdtemp(join(tmpdir(), tempDirPrefix));
const tarballPath = join(tempDir, "package.tgz");
// Download tarball from npm
const npmRegistry = process.env.NPM_REGISTRY || "https://registry.npmjs.org";
// Handle scoped packages (e.g., @scope/package -> @scope%2Fpackage/-/package-version.tgz)
let tarballUrl: string;
if (packageName.startsWith("@")) {
const [scope, name] = packageName.slice(1).split("/");
const scopedPackageName = `@${scope}%2F${name}`;
tarballUrl = `${npmRegistry}/${scopedPackageName}/-/${name}-${resolvedVersion}.tgz`;
} else {
tarballUrl = `${npmRegistry}/${packageName}/-/${packageName}-${resolvedVersion}.tgz`;
}
log.info(`Downloading from ${tarballUrl}...`);
const response = await fetch(tarballUrl);
if (!response.ok) {
throw new Error(`Failed to download tarball: ${response.status} ${response.statusText}`);
}
// Write tarball to file
if (!response.body) throw new Error("Response body is null");
const fileStream = createWriteStream(tarballPath);
await pipeline(response.body, fileStream);
log.info(`Downloaded tarball to ${tarballPath}`);
// Extract tarball
log.info(`Extracting tarball...`);
const extractResult = spawnSync("tar", ["-xzf", tarballPath, "-C", tempDir], {
stdio: "pipe",
encoding: "utf-8",
});
if (extractResult.status !== 0) {
throw new Error(
`Failed to extract tarball: ${extractResult.stderr || extractResult.stdout || "Unknown error"}`
);
}
// Find executable in the extracted package
const extractedDir = join(tempDir, "package");
const cliPath = join(extractedDir, executablePath);
if (!existsSync(cliPath)) {
throw new Error(`Executable not found in extracted package at ${cliPath}`);
}
// Make the file executable
chmodSync(cliPath, 0o755);
log.info(`${packageName} installed at ${cliPath}`);
return cliPath;
}
export const agent = <const agent extends Agent>(agent: agent): agent => {
return agent;
};
export const instructions = `
You are a highly intelligent, no-nonsense senior-level software engineering agent. You are careful, to-the-point, and kind. You only say things you know to be true. Your code is focused, minimal, and production-ready. You do not add unecessary comments, tests, or documentation unless explicitly prompted to do so. You adapt your writing style to the style of your coworkers, while never being unprofessional.
- eagerly inspect your MCP servers to determine what tools are available to you, especially ${ghPullfrogMcpName}
- do not under any circumstances use the github cli (\`gh\`). find the corresponding tool from ${ghPullfrogMcpName} instead.
- mode selection: choose the appropriate mode based on the prompt payload:
- choose "plan mode" if the prompt asks to:
- create a plan, break down tasks, outline steps, or analyze requirements
- understand the scope of work before implementation
- provide a todo list or task breakdown
- choose "implement" if the prompt asks to:
- implement, build, create, or develop code changes
- make specific changes to files or features
- execute a plan that was previously created
- the prompt includes specific implementation details or requirements
- choose "review" if the prompt asks to:
- review code, PR, or implementation
- provide feedback, suggestions, or identify issues
- check code quality, style, or correctness
- once you've chosen a mode, follow its associated prompts carefully
- when prompted directly (e.g., via issue comment or PR comment):
(1) start by creating a single response comment using mcp__${ghPullfrogMcpName}__create_issue_comment
- the initial comment should say something like "I'll do {summary of request}" where you summarize what was requested
- save the commentId returned from this initial comment creation
(2) use mcp__${ghPullfrogMcpName}__edit_issue_comment to progressively update that same comment as you make progress
- update the comment with current status, completed tasks, and any relevant information
- continue updating the same comment throughout the planning/implementation process
(3) create_issue_comment should only be used once initially - all subsequent updates must use edit_issue_comment with the saved commentId
- if prompted to review a PR:
(1) get PR info with mcp__${ghPullfrogMcpName}__get_pull_request (this automatically prepares the repository by fetching and checking out the PR branch)
(2) view diff: git diff origin/<base>...origin/<head> (use line numbers from this for inline comments)
(3) read files from the checked-out PR branch to understand the implementation
(4) when submitting review: use the 'comments' array for ALL specific code issues - include the file path and line position from the diff
(5) only use the 'body' field for a brief summary (1-2 sentences) or for feedback that doesn't apply to a specific code location
replace <base> and <head> with 'base' and 'head' from the PR info
`;
export type Agent = {
name: string;
inputKey: string;
install: () => Promise<string>;
run: (config: AgentConfig) => Promise<AgentResult>;
};
+42073 -358
View File
File diff suppressed because one or more lines are too long
+13 -10
View File
@@ -5,21 +5,24 @@
*/
import * as core from "@actions/core";
import { type Inputs, main } from "./main.ts";
import { createMcpServer } from "./mcp/server.ts";
import packageJson from "./package.json" with { type: "json" };
import { AgentName, type Inputs, main } from "./main.ts";
import { log } from "./utils/cli.ts";
// Export createMcpServer so it can be called from the spawned MCP process
export { createMcpServer };
async function run(): Promise<void> {
try {
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
// Change to GITHUB_WORKSPACE if set (this is where actions/checkout puts the repo)
// JavaScript actions run from the action's directory, not the checked-out repo
if (process.env.GITHUB_WORKSPACE && process.cwd() !== process.env.GITHUB_WORKSPACE) {
log.debug(`Changing to GITHUB_WORKSPACE: ${process.env.GITHUB_WORKSPACE}`);
process.chdir(process.env.GITHUB_WORKSPACE);
log.debug(`New working directory: ${process.cwd()}`);
}
const inputs: Inputs = {
try {
const inputs: Required<Inputs> = {
prompt: core.getInput("prompt", { required: true }),
anthropic_api_key: core.getInput("anthropic_api_key") || undefined,
anthropic_api_key: core.getInput("anthropic_api_key"),
openai_api_key: core.getInput("openai_api_key"),
agent: core.getInput("agent") ? AgentName.assert(core.getInput("agent")) : undefined,
};
const result = await main(inputs);
+18 -7
View File
@@ -1,15 +1,11 @@
import { build } from "esbuild";
// Build the GitHub Action bundle only
// For npm package builds, use zshy (pnpm build:npm)
await build({
entryPoints: ["./entry.ts"],
const sharedConfig = {
bundle: true,
outfile: "./entry.js",
format: "esm",
platform: "node",
target: "node20",
minify: true,
minify: false,
sourcemap: false,
// Bundle all dependencies - GitHub Actions doesn't have node_modules
// Only mark optional peer dependencies as external
@@ -19,13 +15,28 @@ await build({
"sury",
],
// Provide a proper require shim for CommonJS modules bundled into ESM
// We use a unique variable name to avoid conflicts with bundled imports
banner: {
js: `import { createRequire } from 'module'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const require = createRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);`,
js: `import { createRequire as __createRequire } from 'module'; import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __dirnameFn } from 'path'; const require = __createRequire(import.meta.url); const __filename = __fileURLToPath(import.meta.url); const __dirname = __dirnameFn(__filename);`,
},
// Enable tree-shaking to remove unused code
treeShaking: true,
// Drop console statements in production (but keep for debugging)
drop: [],
};
// Build the main entry bundle (without MCP)
await build({
...sharedConfig,
entryPoints: ["./entry.ts"],
outfile: "./entry.js",
});
// Build the MCP server bundle
await build({
...sharedConfig,
entryPoints: ["./mcp/server.ts"],
outfile: "./mcp-server.js",
});
console.log("✅ Build completed successfully!");
+1 -1
View File
@@ -1 +1 @@
run npx cowsay "don't eat me"
choose a random animal emoji. add a comment to https://github.com/pullfrogai/scratch/issues/21 containing 50 of that emoji.
+58 -25
View File
@@ -1,13 +1,34 @@
import { flatMorph } from "@ark/util";
import { type } from "arktype";
import { claude } from "./agents/claude.ts";
import { agents } from "./agents/index.ts";
import { createMcpConfigs } from "./mcp/config.ts";
import packageJson from "./package.json" with { type: "json" };
import { fetchRepoSettings } from "./utils/api.ts";
import { log } from "./utils/cli.ts";
import { parseRepoContext, setupGitHubInstallationToken } from "./utils/github.ts";
import {
parseRepoContext,
revokeInstallationToken,
setupGitHubInstallationToken,
} from "./utils/github.ts";
import { setupGitAuth, setupGitConfig } from "./utils/setup.ts";
export const AgentName = type.enumerated(...Object.values(agents).map((agent) => agent.name));
export type AgentName = typeof AgentName.infer;
export const AgentInputKey = type.enumerated(
...Object.values(agents).map((agent) => agent.inputKey)
);
export type AgentInputKey = typeof AgentInputKey.infer;
const keyInputDefs = flatMorph(
agents,
(_, agent) => [agent.inputKey, "string | undefined?"] as const
);
export const Inputs = type({
prompt: "string",
"anthropic_api_key?": "string | undefined",
...keyInputDefs,
"agent?": AgentName.or("undefined"),
});
export type Inputs = typeof Inputs.infer;
@@ -21,49 +42,57 @@ export interface MainResult {
export type PromptJSON = {};
export async function main(inputs: Inputs): Promise<MainResult> {
let tokenToRevoke: string | null = null;
try {
log.info("Starting agent run...");
// Debug logging for git repo detection
log.debug(`Current working directory: ${process.cwd()}`);
log.debug(`GITHUB_ACTIONS: ${process.env.GITHUB_ACTIONS}`);
log.debug(`GITHUB_WORKSPACE: ${process.env.GITHUB_WORKSPACE}`);
try {
const { execSync } = await import("node:child_process");
const gitDir = execSync("git rev-parse --git-dir", {
encoding: "utf-8",
stdio: "pipe",
}).trim();
log.debug(`Git directory found: ${gitDir}`);
} catch (error) {
log.debug(
`Git directory check failed: ${error instanceof Error ? error.message : String(error)}`
);
}
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
Inputs.assert(inputs);
setupGitConfig();
const githubInstallationToken = await setupGitHubInstallationToken();
const { githubInstallationToken, wasAcquired } = await setupGitHubInstallationToken();
if (wasAcquired) {
tokenToRevoke = githubInstallationToken;
}
const repoContext = parseRepoContext();
const repoSettings = await fetchRepoSettings({
token: githubInstallationToken,
repoContext,
});
const agentName: AgentName = inputs.agent || repoSettings.defaultAgent || "claude";
const agent = agents[agentName];
setupGitAuth(githubInstallationToken, repoContext);
const mcpServers = createMcpConfigs(githubInstallationToken);
log.debug(`📋 MCP Config: ${JSON.stringify(mcpServers, null, 2)}`);
log.info("Running Claude Agent SDK...");
// Install agent CLI before running
const cliPath = await agent.install();
log.info(`Running ${agentName}...`);
log.box(inputs.prompt, { title: "Prompt" });
// TODO: check if `inputs.prompts` is JSON
// if yes, check if it's a webhook payload or toJSON(github.event)
// for webhook payloads, check the specified `agent` field
const result = await claude.run({
// Get API key based on agent type
const apiKey = inputs[agent.inputKey];
if (!apiKey) throw new Error(`${agent.inputKey} is required for ${agentName} agent`);
const result = await agent.run({
prompt: inputs.prompt,
mcpServers,
githubInstallationToken,
apiKey: inputs.anthropic_api_key!,
apiKey,
cliPath,
});
if (!result.success) {
@@ -89,5 +118,9 @@ export async function main(inputs: Inputs): Promise<MainResult> {
success: false,
error: errorMessage,
};
} finally {
if (tokenToRevoke) {
await revokeInstallationToken(tokenToRevoke);
}
}
}
Executable
+101714
View File
File diff suppressed because one or more lines are too long
+6 -11
View File
@@ -3,9 +3,9 @@
*/
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { fromHere } from "@ark/fs";
import { parseRepoContext } from "../utils/github.ts";
export const ghPullfrogMcpName = "gh-pullfrog";
import { ghPullfrogMcpName } from "./index.ts";
export type McpName = typeof ghPullfrogMcpName;
@@ -15,19 +15,14 @@ export function createMcpConfigs(githubInstallationToken: string): McpConfigs {
const repoContext = parseRepoContext();
const githubRepository = `${repoContext.owner}/${repoContext.name}`;
// Get absolute path to entry.js - use GITHUB_ACTION_PATH if available, otherwise current directory
const entryPath = process.env.GITHUB_ACTION_PATH
? `${process.env.GITHUB_ACTION_PATH}/entry.js`
: `${process.cwd()}/entry.js`;
// In production (GitHub Actions), mcp-server.js is in same directory as entry.js (where this is bundled)
// In development, server.ts is in the same directory as this file (config.ts)
const serverPath = process.env.GITHUB_ACTIONS ? fromHere("mcp-server.js") : fromHere("server.ts");
return {
[ghPullfrogMcpName]: {
command: "node",
args: [
"--input-type=module",
"-e",
`import('${entryPath.replace(/'/g, "\\'")}').then(m => m.createMcpServer())`,
],
args: [serverPath],
env: {
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
GITHUB_REPOSITORY: githubRepository,
+4
View File
@@ -0,0 +1,4 @@
// for reasos this can't live alongide config.ts
// because its imported into the frontend UI
// and we don't want to pull in FastMCP, etc
export const ghPullfrogMcpName = "gh-pullfrog";
+2 -5
View File
@@ -4,9 +4,6 @@ import { contextualize, tool } from "./shared.ts";
export const Review = type({
pull_number: type.number.describe("The pull request number to review"),
event: type
.enumerated("APPROVE", "REQUEST_CHANGES", "COMMENT")
.describe("'APPROVE', 'REQUEST_CHANGES', or 'COMMENT' (the review action)"),
body: type.string
.describe(
"Brief summary or general feedback that doesn't apply to specific code locations. Keep it concise - most feedback should be in the 'comments' array."
@@ -45,7 +42,7 @@ export const ReviewTool = tool({
"IMPORTANT: Use 'comments' array for ALL specific code issues at the line-level. " +
"Only use 'body' for a brief summary or feedback that doesn't apply to a specific location.",
parameters: Review,
execute: contextualize(async ({ pull_number, event, body, commit_id, comments = [] }, ctx) => {
execute: contextualize(async ({ pull_number, body, commit_id, comments = [] }, ctx) => {
// Get the PR to determine the head commit if commit_id not provided
const pr = await ctx.octokit.rest.pulls.get({
owner: ctx.owner,
@@ -58,7 +55,7 @@ export const ReviewTool = tool({
owner: ctx.owner,
repo: ctx.name,
pull_number,
event,
event: "COMMENT",
};
if (body) params.body = body;
if (commit_id) {
+13 -15
View File
@@ -8,20 +8,18 @@ import { PullRequestInfoTool } from "./prInfo.ts";
import { ReviewTool } from "./review.ts";
import { addTools } from "./shared.ts";
export function createMcpServer(): void {
const server = new FastMCP({
name: "gh-pullfrog",
version: "0.0.1",
});
const server = new FastMCP({
name: "gh-pullfrog",
version: "0.0.1",
});
addTools(server, [
CreateCommentTool,
EditCommentTool,
IssueTool,
PullRequestTool,
ReviewTool,
PullRequestInfoTool,
]);
addTools(server, [
CreateCommentTool,
EditCommentTool,
IssueTool,
PullRequestTool,
ReviewTool,
PullRequestInfoTool,
]);
server.start();
}
server.start();
+54
View File
@@ -0,0 +1,54 @@
import { ghPullfrogMcpName } from "./mcp/index.ts";
export const modes = [
{
name: "Plan",
description:
"Create plans, break down tasks, outline steps, analyze requirements, understand scope of work, or provide task breakdowns",
prompt: `Follow these steps:
1. Create initial response comment using ${ghPullfrogMcpName}/create_issue_comment saying "I'll {summary of request}" and save the commentId
2. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context (read AGENTS.md if it exists, understand how to install dependencies, run tests, run builds, and make changes according to best practices). Skip this step if the prompt is trivial and self-contained.
3. Analyze the request and break it down into clear, actionable tasks
4. Consider dependencies, potential challenges, and implementation order
5. Create a structured plan with clear milestones
6. Update your comment using ${ghPullfrogMcpName}/edit_issue_comment with the commentId to present the plan in a clear, organized format
7. Continue updating the same comment as needed (never create additional comments - always use edit_issue_comment)`,
},
{
name: "Build",
description:
"Implement, build, create, or develop code changes; make specific changes to files or features; execute a plan; or handle tasks with specific implementation details",
prompt: `Follow these steps:
1. Create initial response comment using ${ghPullfrogMcpName}/create_issue_comment saying "I'll {summary of request}" and save the commentId
2. If the request requires understanding the codebase structure, dependencies, or conventions, gather relevant context (read AGENTS.md if it exists, understand how to install dependencies, run tests, run builds, and make changes according to best practices). Skip this step if the prompt is trivial and self-contained.
3. Understand the requirements and any existing plan
4. Make the necessary code changes
5. Test your changes to ensure they work correctly
6. Update your comment using ${ghPullfrogMcpName}/edit_issue_comment with the commentId to share progress and results
7. Continue updating the same comment as you make progress (never create additional comments - always use edit_issue_comment)`,
},
{
name: "Review",
description:
"Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
prompt: `Follow these steps:
1. Create initial response comment using ${ghPullfrogMcpName}/create_issue_comment saying "I'll review this" and save the commentId
2. Get PR info with ${ghPullfrogMcpName}/get_pull_request (this automatically prepares the repository by fetching and checking out the PR branch)
3. View diff: git diff origin/<base>...origin/<head> (use line numbers from this for inline comments, replace <base> and <head> with 'base' and 'head' from PR info)
4. Read files from the checked-out PR branch to understand the implementation
5. Update your comment using ${ghPullfrogMcpName}/edit_issue_comment with findings as you review
6. When submitting review: use the 'comments' array for ALL specific code issues - include the file path and line position from the diff
7. Only use the 'body' field for a brief summary (1-2 sentences) or for feedback that doesn't apply to a specific code location
8. Continue updating the same comment as needed (never create additional comments - always use edit_issue_comment)`,
},
{
name: "Prompt",
description:
"Fallback for tasks that don't fit other workflows, direct prompts via comments, or requests requiring general assistance without a specific workflow pattern",
prompt: `Follow these steps:
1. Create an initial "Progress Comment" using ${ghPullfrogMcpName}/create_issue_comment saying "I'll {summary of request}" and save the commentId
2. Perform the requested task. Only take action if you have high confidence that you understand what is being asked. If you are not sure, ask for clarification. Take stock of the tools at your disposal.
3. As your work progresses, update your Progress Comment to share progress and results. Using ${ghPullfrogMcpName}/edit_issue_comment and the commentId you saved earlier. Do not create additional comments unless you are explicitly asked to do so.
4. When you finish the task, update the Progress Comment a final time to confirm completion. If you created any issues, PRs, etc, include appropriate links to it here.`,
},
] as const;
+960
View File
@@ -0,0 +1,960 @@
{
"name": "@pullfrog/action",
"version": "0.0.96",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pullfrog/action",
"version": "0.0.96",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.11.1",
"@anthropic-ai/claude-agent-sdk": "^0.1.26",
"@ark/fs": "0.53.0",
"@ark/util": "0.53.0",
"@octokit/rest": "^22.0.0",
"@octokit/webhooks-types": "^7.6.1",
"@standard-schema/spec": "1.0.0",
"arktype": "2.1.25",
"convex": "^1.29.0",
"dotenv": "^17.2.3",
"execa": "^9.6.0",
"fastmcp": "^3.20.0",
"table": "^6.9.0"
},
"devDependencies": {
"@types/node": "^24.7.2",
"arg": "^5.0.2",
"esbuild": "^0.25.9",
"husky": "^9.0.0",
"typescript": "^5.9.3"
}
},
"../node_modules/.pnpm/@actions+core@1.11.1/node_modules/@actions/core": {
"version": "1.11.1",
"license": "MIT",
"dependencies": {
"@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1"
},
"devDependencies": {
"@types/node": "^16.18.112"
}
},
"../node_modules/.pnpm/@anthropic-ai+claude-agent-sdk@0.1.26_zod@3.25.76/node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.1.26",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "^0.33.5",
"@img/sharp-darwin-x64": "^0.33.5",
"@img/sharp-linux-arm": "^0.33.5",
"@img/sharp-linux-arm64": "^0.33.5",
"@img/sharp-linux-x64": "^0.33.5",
"@img/sharp-win32-x64": "^0.33.5"
},
"peerDependencies": {
"zod": "^3.24.1"
}
},
"../node_modules/.pnpm/@ark+fs@0.53.0/node_modules/@ark/fs": {
"version": "0.53.0",
"license": "MIT"
},
"../node_modules/.pnpm/@ark+util@0.53.0/node_modules/@ark/util": {
"version": "0.53.0",
"license": "MIT"
},
"../node_modules/.pnpm/@octokit+rest@22.0.0/node_modules/@octokit/rest": {
"version": "22.0.0",
"license": "MIT",
"dependencies": {
"@octokit/core": "^7.0.2",
"@octokit/plugin-paginate-rest": "^13.0.1",
"@octokit/plugin-request-log": "^6.0.0",
"@octokit/plugin-rest-endpoint-methods": "^16.0.0"
},
"devDependencies": {
"@octokit/auth-action": "^6.0.1",
"@octokit/auth-app": "^8.0.1",
"@octokit/fixtures-server": "^8.1.0",
"@octokit/request": "^10.0.0",
"@octokit/tsconfig": "^4.0.0",
"@types/node": "^22.0.0",
"@vitest/coverage-v8": "^3.0.0",
"esbuild": "^0.25.0",
"fetch-mock": "^12.0.0",
"glob": "^11.0.0",
"nock": "^14.0.0-beta.8",
"prettier": "^3.2.4",
"semantic-release-plugin-update-version-in-files": "^2.0.0",
"typescript": "^5.3.3",
"undici": "^6.4.0",
"vitest": "^3.0.0"
},
"engines": {
"node": ">= 20"
}
},
"../node_modules/.pnpm/@octokit+webhooks-types@7.6.1/node_modules/@octokit/webhooks-types": {
"version": "7.6.1",
"license": "MIT",
"devDependencies": {}
},
"../node_modules/.pnpm/@standard-schema+spec@1.0.0/node_modules/@standard-schema/spec": {
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"tsup": "^8.3.0",
"typescript": "^5.6.2"
}
},
"../node_modules/.pnpm/@types+node@24.7.2/node_modules/@types/node": {
"version": "24.7.2",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.14.0"
}
},
"../node_modules/.pnpm/arg@5.0.2/node_modules/arg": {
"version": "5.0.2",
"dev": true,
"license": "MIT",
"devDependencies": {
"chai": "^4.1.1",
"jest": "^27.0.6",
"prettier": "^2.3.2"
}
},
"../node_modules/.pnpm/arktype@2.1.25/node_modules/arktype": {
"version": "2.1.25",
"license": "MIT",
"dependencies": {
"@ark/schema": "0.53.0",
"@ark/util": "0.53.0",
"arkregex": "0.0.2"
}
},
"../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv": {
"version": "17.2.3",
"license": "BSD-2-Clause",
"devDependencies": {
"@types/node": "^18.11.3",
"decache": "^4.6.2",
"sinon": "^14.0.1",
"standard": "^17.0.0",
"standard-version": "^9.5.0",
"tap": "^19.2.0",
"typescript": "^4.8.4"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"../node_modules/.pnpm/esbuild@0.25.10/node_modules/esbuild": {
"version": "0.25.10",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.10",
"@esbuild/android-arm": "0.25.10",
"@esbuild/android-arm64": "0.25.10",
"@esbuild/android-x64": "0.25.10",
"@esbuild/darwin-arm64": "0.25.10",
"@esbuild/darwin-x64": "0.25.10",
"@esbuild/freebsd-arm64": "0.25.10",
"@esbuild/freebsd-x64": "0.25.10",
"@esbuild/linux-arm": "0.25.10",
"@esbuild/linux-arm64": "0.25.10",
"@esbuild/linux-ia32": "0.25.10",
"@esbuild/linux-loong64": "0.25.10",
"@esbuild/linux-mips64el": "0.25.10",
"@esbuild/linux-ppc64": "0.25.10",
"@esbuild/linux-riscv64": "0.25.10",
"@esbuild/linux-s390x": "0.25.10",
"@esbuild/linux-x64": "0.25.10",
"@esbuild/netbsd-arm64": "0.25.10",
"@esbuild/netbsd-x64": "0.25.10",
"@esbuild/openbsd-arm64": "0.25.10",
"@esbuild/openbsd-x64": "0.25.10",
"@esbuild/openharmony-arm64": "0.25.10",
"@esbuild/sunos-x64": "0.25.10",
"@esbuild/win32-arm64": "0.25.10",
"@esbuild/win32-ia32": "0.25.10",
"@esbuild/win32-x64": "0.25.10"
}
},
"../node_modules/.pnpm/execa@9.6.0/node_modules/execa": {
"version": "9.6.0",
"license": "MIT",
"dependencies": {
"@sindresorhus/merge-streams": "^4.0.0",
"cross-spawn": "^7.0.6",
"figures": "^6.1.0",
"get-stream": "^9.0.0",
"human-signals": "^8.0.1",
"is-plain-obj": "^4.1.0",
"is-stream": "^4.0.1",
"npm-run-path": "^6.0.0",
"pretty-ms": "^9.2.0",
"signal-exit": "^4.1.0",
"strip-final-newline": "^4.0.0",
"yoctocolors": "^2.1.1"
},
"devDependencies": {
"@types/node": "^22.15.21",
"ava": "^6.3.0",
"c8": "^10.1.3",
"get-node": "^15.0.3",
"is-in-ci": "^1.0.0",
"is-running": "^2.1.0",
"log-process-errors": "^12.0.1",
"path-exists": "^5.0.0",
"path-key": "^4.0.0",
"tempfile": "^5.0.0",
"tsd": "^0.32.0",
"typescript": "^5.8.3",
"which": "^5.0.0",
"xo": "^0.60.0"
},
"engines": {
"node": "^18.19.0 || >=20.5.0"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"../node_modules/.pnpm/fastmcp@3.20.0_arktype@2.1.25_effect@3.16.12/node_modules/fastmcp": {
"version": "3.20.0",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.2",
"@standard-schema/spec": "^1.0.0",
"execa": "^9.6.0",
"file-type": "^21.0.0",
"fuse.js": "^7.1.0",
"mcp-proxy": "^5.8.1",
"strict-event-emitter-types": "^2.0.0",
"undici": "^7.13.0",
"uri-templates": "^0.2.0",
"xsschema": "0.3.5",
"yargs": "^18.0.0",
"zod": "^3.25.76",
"zod-to-json-schema": "^3.24.6"
},
"bin": {
"fastmcp": "dist/bin/fastmcp.js"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@modelcontextprotocol/inspector": "^0.16.2",
"@sebbo2002/semantic-release-jsr": "^3.0.1",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^24.2.1",
"@types/uri-templates": "^0.1.34",
"@types/yargs": "^17.0.33",
"@valibot/to-json-schema": "^1.3.0",
"@wong2/mcp-cli": "^1.13.0",
"arktype": "^2.1.20",
"eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-perfectionist": "^4.15.0",
"eslint-plugin-prettier": "^5.5.4",
"eventsource-client": "^1.1.4",
"get-port-please": "^3.2.0",
"jiti": "^2.5.1",
"jsr": "^0.13.5",
"prettier": "^3.6.2",
"semantic-release": "^24.2.7",
"tsup": "^8.5.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.0",
"valibot": "^1.1.0",
"vitest": "^3.2.4"
}
},
"../node_modules/.pnpm/husky@9.1.7/node_modules/husky": {
"version": "9.1.7",
"dev": true,
"license": "MIT",
"bin": {
"husky": "bin.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"../node_modules/.pnpm/table@6.9.0/node_modules/table": {
"version": "6.9.0",
"license": "BSD-3-Clause",
"dependencies": {
"ajv": "^8.0.1",
"lodash.truncate": "^4.4.2",
"slice-ansi": "^4.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1"
},
"devDependencies": {
"@types/chai": "^4.2.16",
"@types/lodash.mapvalues": "^4.6.6",
"@types/lodash.truncate": "^4.4.6",
"@types/mocha": "^9.0.0",
"@types/node": "^14.14.37",
"@types/sinon": "^10.0.0",
"@types/slice-ansi": "^4.0.0",
"ajv-cli": "^5.0.0",
"ajv-keywords": "^5.0.0",
"chai": "^4.2.0",
"chalk": "^4.1.0",
"coveralls": "^3.1.0",
"eslint": "^7.32.0",
"eslint-config-canonical": "^25.0.0",
"gitdown": "^3.1.4",
"husky": "^4.3.6",
"js-beautify": "^1.14.0",
"lodash.mapvalues": "^4.6.0",
"mkdirp": "^1.0.4",
"mocha": "^8.2.1",
"nyc": "^15.1.0",
"semantic-release": "^17.3.1",
"sinon": "^12.0.1",
"ts-node": "^9.1.1",
"typescript": "4.5.2"
},
"engines": {
"node": ">=10.0.0"
}
},
"../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript": {
"version": "5.9.3",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"devDependencies": {
"@dprint/formatter": "^0.4.1",
"@dprint/typescript": "0.93.4",
"@esfx/canceltoken": "^1.0.0",
"@eslint/js": "^9.20.0",
"@octokit/rest": "^21.1.1",
"@types/chai": "^4.3.20",
"@types/diff": "^7.0.1",
"@types/minimist": "^1.2.5",
"@types/mocha": "^10.0.10",
"@types/ms": "^0.7.34",
"@types/node": "latest",
"@types/source-map-support": "^0.5.10",
"@types/which": "^3.0.4",
"@typescript-eslint/rule-tester": "^8.24.1",
"@typescript-eslint/type-utils": "^8.24.1",
"@typescript-eslint/utils": "^8.24.1",
"azure-devops-node-api": "^14.1.0",
"c8": "^10.1.3",
"chai": "^4.5.0",
"chokidar": "^4.0.3",
"diff": "^7.0.0",
"dprint": "^0.49.0",
"esbuild": "^0.25.0",
"eslint": "^9.20.1",
"eslint-formatter-autolinkable-stylish": "^1.4.0",
"eslint-plugin-regexp": "^2.7.0",
"fast-xml-parser": "^4.5.2",
"glob": "^10.4.5",
"globals": "^15.15.0",
"hereby": "^1.10.0",
"jsonc-parser": "^3.3.1",
"knip": "^5.44.4",
"minimist": "^1.2.8",
"mocha": "^10.8.2",
"mocha-fivemat-progress-reporter": "^0.1.0",
"monocart-coverage-reports": "^2.12.1",
"ms": "^2.1.3",
"picocolors": "^1.1.1",
"playwright": "^1.50.1",
"source-map-support": "^0.5.21",
"tslib": "^2.8.1",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"which": "^3.0.1"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/@actions/core": {
"resolved": "../node_modules/.pnpm/@actions+core@1.11.1/node_modules/@actions/core",
"link": true
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"resolved": "../node_modules/.pnpm/@anthropic-ai+claude-agent-sdk@0.1.26_zod@3.25.76/node_modules/@anthropic-ai/claude-agent-sdk",
"link": true
},
"node_modules/@ark/fs": {
"resolved": "../node_modules/.pnpm/@ark+fs@0.53.0/node_modules/@ark/fs",
"link": true
},
"node_modules/@ark/util": {
"resolved": "../node_modules/.pnpm/@ark+util@0.53.0/node_modules/@ark/util",
"link": true
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
"integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@octokit/rest": {
"resolved": "../node_modules/.pnpm/@octokit+rest@22.0.0/node_modules/@octokit/rest",
"link": true
},
"node_modules/@octokit/webhooks-types": {
"resolved": "../node_modules/.pnpm/@octokit+webhooks-types@7.6.1/node_modules/@octokit/webhooks-types",
"link": true
},
"node_modules/@standard-schema/spec": {
"resolved": "../node_modules/.pnpm/@standard-schema+spec@1.0.0/node_modules/@standard-schema/spec",
"link": true
},
"node_modules/@types/node": {
"resolved": "../node_modules/.pnpm/@types+node@24.7.2/node_modules/@types/node",
"link": true
},
"node_modules/arg": {
"resolved": "../node_modules/.pnpm/arg@5.0.2/node_modules/arg",
"link": true
},
"node_modules/arktype": {
"resolved": "../node_modules/.pnpm/arktype@2.1.25/node_modules/arktype",
"link": true
},
"node_modules/convex": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/convex/-/convex-1.29.0.tgz",
"integrity": "sha512-uoIPXRKIp2eLCkkR9WJ2vc9NtgQtx8Pml59WPUahwbrd5EuW2WLI/cf2E7XrUzOSifdQC3kJZepisk4wJNTJaA==",
"license": "Apache-2.0",
"dependencies": {
"esbuild": "0.25.4",
"prettier": "^3.0.0"
},
"bin": {
"convex": "bin/main.js"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=7.0.0"
},
"peerDependencies": {
"@auth0/auth0-react": "^2.0.1",
"@clerk/clerk-react": "^4.12.8 || ^5.0.0",
"react": "^18.0.0 || ^19.0.0-0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@auth0/auth0-react": {
"optional": true
},
"@clerk/clerk-react": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/convex/node_modules/esbuild": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
"integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.4",
"@esbuild/android-arm": "0.25.4",
"@esbuild/android-arm64": "0.25.4",
"@esbuild/android-x64": "0.25.4",
"@esbuild/darwin-arm64": "0.25.4",
"@esbuild/darwin-x64": "0.25.4",
"@esbuild/freebsd-arm64": "0.25.4",
"@esbuild/freebsd-x64": "0.25.4",
"@esbuild/linux-arm": "0.25.4",
"@esbuild/linux-arm64": "0.25.4",
"@esbuild/linux-ia32": "0.25.4",
"@esbuild/linux-loong64": "0.25.4",
"@esbuild/linux-mips64el": "0.25.4",
"@esbuild/linux-ppc64": "0.25.4",
"@esbuild/linux-riscv64": "0.25.4",
"@esbuild/linux-s390x": "0.25.4",
"@esbuild/linux-x64": "0.25.4",
"@esbuild/netbsd-arm64": "0.25.4",
"@esbuild/netbsd-x64": "0.25.4",
"@esbuild/openbsd-arm64": "0.25.4",
"@esbuild/openbsd-x64": "0.25.4",
"@esbuild/sunos-x64": "0.25.4",
"@esbuild/win32-arm64": "0.25.4",
"@esbuild/win32-ia32": "0.25.4",
"@esbuild/win32-x64": "0.25.4"
}
},
"node_modules/dotenv": {
"resolved": "../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv",
"link": true
},
"node_modules/esbuild": {
"resolved": "../node_modules/.pnpm/esbuild@0.25.10/node_modules/esbuild",
"link": true
},
"node_modules/execa": {
"resolved": "../node_modules/.pnpm/execa@9.6.0/node_modules/execa",
"link": true
},
"node_modules/fastmcp": {
"resolved": "../node_modules/.pnpm/fastmcp@3.20.0_arktype@2.1.25_effect@3.16.12/node_modules/fastmcp",
"link": true
},
"node_modules/husky": {
"resolved": "../node_modules/.pnpm/husky@9.1.7/node_modules/husky",
"link": true
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/table": {
"resolved": "../node_modules/.pnpm/table@6.9.0/node_modules/table",
"link": true
},
"node_modules/typescript": {
"resolved": "../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript",
"link": true
}
}
}
+6 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@pullfrog/action",
"version": "0.0.79",
"version": "0.0.102",
"type": "module",
"files": [
"index.js",
@@ -17,13 +17,15 @@
"typecheck": "tsc --noEmit",
"build": "node esbuild.config.js",
"play": "node play.ts",
"scratch": "node scratch.ts",
"upDeps": "pnpm up --latest",
"lock": "pnpm --ignore-workspace install",
"prepare": "husky"
},
"dependencies": {
"@actions/core": "^1.11.1",
"@anthropic-ai/claude-agent-sdk": "^0.1.26",
"@anthropic-ai/claude-agent-sdk": "0.1.37",
"@openai/codex-sdk": "0.58.0",
"@ark/fs": "0.53.0",
"@ark/util": "0.53.0",
"@octokit/rest": "^22.0.0",
@@ -33,7 +35,8 @@
"dotenv": "^17.2.3",
"execa": "^9.6.0",
"fastmcp": "^3.20.0",
"table": "^6.9.0"
"table": "^6.9.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^24.7.2",
+5 -10
View File
@@ -5,7 +5,6 @@ import { fromHere } from "@ark/fs";
import arg from "arg";
import { config } from "dotenv";
import { type Inputs, main } from "./main.ts";
import packageJson from "./package.json" with { type: "json" };
import { log } from "./utils/cli.ts";
import { setupTestRepo } from "./utils/setup.ts";
@@ -15,22 +14,17 @@ export async function run(
prompt: string
): Promise<{ success: boolean; output?: string | undefined; error?: string | undefined }> {
try {
log.info(`🐸 Running pullfrog/action@${packageJson.version}...`);
const tempDir = join(process.cwd(), ".temp");
setupTestRepo({ tempDir, forceClean: true });
const originalCwd = process.cwd();
process.chdir(tempDir);
log.info("🚀 Running action with prompt...");
log.separator();
log.box(prompt, { title: "Prompt" });
log.separator();
const inputs: Inputs = {
const inputs: Required<Inputs> = {
prompt,
openai_api_key: process.env.OPENAI_API_KEY,
anthropic_api_key: process.env.ANTHROPIC_API_KEY,
agent: "codex",
};
const result = await main(inputs);
@@ -86,7 +80,8 @@ Examples:
if (args["--raw"]) {
prompt = args["--raw"];
} else {
const filePath = args._[0] || "fixtures/basic.txt";
const filePath = args._[0] || "basic.txt";
const ext = extname(filePath).toLowerCase();
let resolvedPath: string;
+17 -5
View File
@@ -12,8 +12,8 @@ importers:
specifier: ^1.11.1
version: 1.11.1
'@anthropic-ai/claude-agent-sdk':
specifier: ^0.1.26
version: 0.1.30(zod@3.25.76)
specifier: 0.1.37
version: 0.1.37(zod@3.25.76)
'@ark/fs':
specifier: 0.53.0
version: 0.53.0
@@ -26,6 +26,9 @@ importers:
'@octokit/webhooks-types':
specifier: ^7.6.1
version: 7.6.1
'@openai/codex-sdk':
specifier: 0.58.0
version: 0.58.0
'@standard-schema/spec':
specifier: 1.0.0
version: 1.0.0
@@ -44,6 +47,9 @@ importers:
table:
specifier: ^6.9.0
version: 6.9.0
zod:
specifier: ^3.25.76
version: 3.25.76
devDependencies:
'@types/node':
specifier: ^24.7.2
@@ -75,8 +81,8 @@ packages:
'@actions/io@1.1.3':
resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==}
'@anthropic-ai/claude-agent-sdk@0.1.30':
resolution: {integrity: sha512-lo1tqxCr2vygagFp6kUMHKSN6AAWlULCskwGKtLB/JcIXy/8H8GsLSKX54anTsvc9mBbCR8wWASdFmiiL9NSKA==}
'@anthropic-ai/claude-agent-sdk@0.1.37':
resolution: {integrity: sha512-LMfqMIPLTz0vRhpcO7hpPJ5L6Bg24y5/PaqZvwAUNZ/GR3OAl7xmJR7IryIR6m8Pyd/6Hs2yBU8j86Os+wHFQQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
zod: ^3.24.1
@@ -373,6 +379,10 @@ packages:
'@octokit/webhooks-types@7.6.1':
resolution: {integrity: sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==}
'@openai/codex-sdk@0.58.0':
resolution: {integrity: sha512-Z5vK294gUS9cn7bpU/lJtgzqJy1UqIGee7WMP+1Z4a6AxDcTxFCLZI4YkH9praJfrgoj5bFeu+3V9HIoBBTzcw==}
engines: {node: '>=18'}
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@@ -1037,7 +1047,7 @@ snapshots:
'@actions/io@1.1.3': {}
'@anthropic-ai/claude-agent-sdk@0.1.30(zod@3.25.76)':
'@anthropic-ai/claude-agent-sdk@0.1.37(zod@3.25.76)':
dependencies:
zod: 3.25.76
optionalDependencies:
@@ -1262,6 +1272,8 @@ snapshots:
'@octokit/webhooks-types@7.6.1': {}
'@openai/codex-sdk@0.58.0': {}
'@sec-ant/readable-stream@0.4.1': {}
'@sindresorhus/merge-streams@4.0.0': {}
+16
View File
@@ -0,0 +1,16 @@
import { spawnSync } from "child_process";
import { existsSync } from "fs";
function findCliPath(name: string): string | null {
const result = spawnSync("which", [name], { encoding: "utf-8" });
if (result.status === 0 && result.stdout) {
const cliPath = result.stdout.trim();
if (cliPath && existsSync(cliPath)) {
return cliPath;
}
}
return null;
}
console.log(findCliPath("codei"));
+21 -6
View File
@@ -1,8 +1,23 @@
## CURRENT
[] handle progressive comment updating from pullfrog mcp
## MAYBE
[] investigate repo config file
[] try to find heavy claude code user
[] investigate including terminal output from bash commands as collapsed groups from claude
[] test initialization trade offs for pullfrog.yml
## DONE
[x] investigate mcp naming convention
[x] input just accepts one key for API key
[x] look into trigger.yml without installation
[x] cancel installation token at the end of github action
[x] avoid exposing env adding ## SECURITY prompt
[x] add modes to prompt
[x] progressively update comment
[] don't allow rejecting prs
[] fix pnpm caching
[] try to avoid claude narrating the initial comment
[] fix prompt to avoid narration like "I just read all tools from MCP server"
[] investigate including terminal output from bash commands as collapsed groups
[] avoid exposing env
[x] don't allow rejecting prs
[x] fix pnpm caching
[x] fix prompt to avoid narration like "I just read all tools from MCP server"
+15 -15
View File
@@ -2,22 +2,22 @@
"compilerOptions": {
"outDir": "./dist",
"module": "NodeNext",
"target": "ESNext",
"moduleResolution": "NodeNext",
"lib": ["ESNext"],
"target": "ESNext",
"moduleResolution": "NodeNext",
"lib": ["ESNext"],
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"skipLibCheck": true,
"strict": true,
"noUncheckedSideEffectImports": true,
"declaration": true,
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"stripInternal": true,
"rewriteRelativeImportExtensions": true,
"skipLibCheck": true,
"strict": true,
"noUncheckedSideEffectImports": true,
"declaration": true,
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"stripInternal": true,
"moduleDetection": "force",
"useUnknownInCatchVariables": true
"useUnknownInCatchVariables": true
}
}
+94
View File
@@ -0,0 +1,94 @@
import type { AgentName } from "../main.ts";
import { log } from "./cli.ts";
import type { RepoContext } from "./github.ts";
export interface Mode {
id: string;
name: string;
description: string;
prompt: string;
}
export interface RepoSettings {
defaultAgent: AgentName | null;
webAccessLevel: "full_access" | "limited";
webAccessAllowTrusted: boolean;
webAccessDomains: string;
modes: Mode[];
}
export const DEFAULT_REPO_SETTINGS: RepoSettings = {
defaultAgent: null,
webAccessLevel: "full_access",
webAccessAllowTrusted: false,
webAccessDomains: "",
modes: [],
};
/**
* Fetch repository settings from the Pullfrog API
* Returns defaults if repo doesn't exist or fetch fails
*/
export async function fetchRepoSettings({
token,
repoContext,
}: {
token: string;
repoContext: RepoContext;
}): Promise<RepoSettings> {
log.info("Fetching repository settings...");
const settings = await getRepoSettings(token, repoContext);
log.info("Repository settings fetched");
return settings;
}
/**
* Fetch repository settings from the Pullfrog API with fallback to defaults
* Returns agent, permissions, and workflows (excludes triggers)
* Returns defaults if repo doesn't exist or fetch fails
*/
export async function getRepoSettings(
token: string,
repoContext: RepoContext
): Promise<RepoSettings> {
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
// Add timeout to prevent hanging (5 seconds)
const timeoutMs = 5000;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(
`${apiUrl}/api/repo/${repoContext.owner}/${repoContext.name}/settings`,
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
signal: controller.signal,
}
);
clearTimeout(timeoutId);
if (!response.ok) {
// If API returns 404 or other error, fall back to defaults
return DEFAULT_REPO_SETTINGS;
}
const settings = (await response.json()) as RepoSettings | null;
// If API returns null (repo doesn't exist), return defaults
if (settings === null) {
return DEFAULT_REPO_SETTINGS;
}
return settings;
} catch {
clearTimeout(timeoutId);
// If fetch fails (network error, timeout, etc.), fall back to defaults
return DEFAULT_REPO_SETTINGS;
}
}
+18
View File
@@ -3,6 +3,8 @@
*/
import * as core from "@actions/core";
import { spawnSync } from "child_process";
import { existsSync } from "fs";
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
const isDebugEnabled = process.env.LOG_LEVEL === "debug";
@@ -262,3 +264,19 @@ export const log = {
*/
endGroup,
};
/**
* Finds a CLI executable path by checking if it's installed globally
* @param name The name of the CLI executable to find
* @returns The path to the CLI executable, or null if not found
*/
export function findCliPath(name: string): string | null {
const result = spawnSync("which", [name], { encoding: "utf-8" });
if (result.status === 0 && result.stdout) {
const cliPath = result.stdout.trim();
if (cliPath && existsSync(cliPath)) {
return cliPath;
}
}
return null;
}
+64 -23
View File
@@ -62,25 +62,40 @@ async function acquireTokenViaOIDC(): Promise<string> {
const apiUrl = process.env.API_URL || "https://pullfrog.ai";
log.info("Exchanging OIDC token for installation token...");
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
method: "POST",
headers: {
Authorization: `Bearer ${oidcToken}`,
"Content-Type": "application/json",
},
});
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
throw new Error(
`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorText}`
);
// Add timeout to prevent long waits (5 seconds)
const timeoutMs = 5000;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const tokenResponse = await fetch(`${apiUrl}/api/github/installation-token`, {
method: "POST",
headers: {
Authorization: `Bearer ${oidcToken}`,
"Content-Type": "application/json",
},
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!tokenResponse.ok) {
throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`);
}
const tokenData = (await tokenResponse.json()) as InstallationToken;
log.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
return tokenData.token;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === "AbortError") {
throw new Error(`Token exchange timed out after ${timeoutMs}ms`);
}
throw error;
}
const tokenData = (await tokenResponse.json()) as InstallationToken;
log.info(`Installation token obtained for ${tokenData.repository || "all repositories"}`);
return tokenData.token;
}
const base64UrlEncode = (str: string): string => {
@@ -234,21 +249,47 @@ async function acquireNewToken(): Promise<string> {
/**
* Setup GitHub installation token for the action
* Returns the token and whether it was acquired (needs revocation)
*/
export async function setupGitHubInstallationToken(): Promise<string> {
export async function setupGitHubInstallationToken(): Promise<{
githubInstallationToken: string;
wasAcquired: boolean;
}> {
const existingToken = checkExistingToken();
if (existingToken) {
core.setSecret(existingToken);
log.info("Using provided GitHub installation token");
return existingToken;
return { githubInstallationToken: existingToken, wasAcquired: false };
}
const token = await acquireNewToken();
const acquiredToken = await acquireNewToken();
core.setSecret(acquiredToken);
process.env.GITHUB_INSTALLATION_TOKEN = acquiredToken;
core.setSecret(token);
process.env.GITHUB_INSTALLATION_TOKEN = token;
return { githubInstallationToken: acquiredToken, wasAcquired: true };
}
return token;
/**
* Revoke GitHub installation token
*/
export async function revokeInstallationToken(token: string): Promise<void> {
const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
try {
await fetch(`${apiUrl}/installation/token`, {
method: "DELETE",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
},
});
log.info("Installation token revoked");
} catch (error) {
log.warning(
`Failed to revoke installation token: ${error instanceof Error ? error.message : String(error)}`
);
}
}
export interface RepoContext {
-24
View File
@@ -47,29 +47,6 @@ export function setupGitConfig(): void {
// Only set up git config in GitHub Actions environment
// In local development, use the user's existing git config
if (!process.env.GITHUB_ACTIONS) {
log.info("⚠️ Skipping git configuration setup (not in GitHub Actions)");
return;
}
// Debug logging for git repo detection
log.debug(`setupGitConfig: Current working directory: ${process.cwd()}`);
log.debug(`setupGitConfig: GITHUB_WORKSPACE: ${process.env.GITHUB_WORKSPACE}`);
// Check if we're in a git repository before trying to set config
log.debug(`setupGitConfig: Checking for git repository...`);
try {
const gitDir = execSync("git rev-parse --git-dir", { encoding: "utf-8", stdio: "pipe" }).trim();
log.debug(`setupGitConfig: Git directory found: ${gitDir}`);
} catch (error) {
log.warning(
`⚠️ Skipping git configuration setup (not in a git repository): ${error instanceof Error ? error.message : String(error)}`
);
try {
const dirContents = execSync("ls -la", { encoding: "utf-8", stdio: "pipe" }).trim();
log.debug(`setupGitConfig: Current directory contents:\n${dirContents}`);
} catch {
// Ignore if ls fails
}
return;
}
@@ -95,7 +72,6 @@ export function setupGitAuth(githubToken: string, repoContext: RepoContext): voi
// Only set up git auth in GitHub Actions environment
// In local testing, this would overwrite the real git remote with fake credentials
if (!process.env.GITHUB_ACTIONS) {
log.info("⚠️ Skipping git authentication setup (not in GitHub Actions)");
return;
}