From 2d2d31adfad2a60c8d7877e783a3efe02e7d453c Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Thu, 15 Jan 2026 22:06:53 +0000 Subject: [PATCH] Code style (#97) * Cleanup * fix: populate deny array before assigning to config, add CursorCliConfig type * Fix deny array ordering and add CursorCliConfig type Move deny array population before config declaration to avoid relying on reference semantics. Add proper type interface for the CLI config object. --------- Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com> --- agents/claude.ts | 28 ++-- agents/codex.ts | 44 +++--- agents/cursor.ts | 70 ++++----- agents/gemini.ts | 55 +++---- agents/instructions.ts | 27 ++-- agents/opencode.ts | 39 ++--- agents/shared.ts | 8 - dispatch/entry | 213 +++++++++++++------------- entry | 213 +++++++++++++------------- get-installation-token/entry | 30 ++-- run/entry | 213 +++++++++++++------------- utils/cli.ts | 269 +-------------------------------- utils/log.ts | 283 +++++++++++++++++++++++++++++++++++ 13 files changed, 713 insertions(+), 779 deletions(-) create mode 100644 utils/log.ts diff --git a/agents/claude.ts b/agents/claude.ts index 8a17840..0d89403 100644 --- a/agents/claude.ts +++ b/agents/claude.ts @@ -3,7 +3,7 @@ import type { Effort } from "../external.ts"; import packageJson from "../package.json" with { type: "json" }; import { log } from "../utils/cli.ts"; import { addInstructions } from "./instructions.ts"; -import { agent, createAgentEnv, installFromNpmTarball, type ToolPermissions } from "./shared.ts"; +import { type AgentConfig, agent, createAgentEnv, installFromNpmTarball } from "./shared.ts"; // Model selection based on effort level // Note: mini uses Haiku for speed, auto uses opusplan for balance, max uses Opus for capability @@ -22,14 +22,14 @@ const claudeEffortModels: Record = { /** * Build disallowedTools list from ToolPermissions. */ -function buildDisallowedTools(tools: ToolPermissions): string[] { +function buildDisallowedTools(ctx: AgentConfig): string[] { const disallowed: string[] = []; - if (tools.web === "disabled") disallowed.push("WebFetch"); - if (tools.search === "disabled") disallowed.push("WebSearch"); - if (tools.write === "disabled") disallowed.push("Write"); + if (ctx.tools.web === "disabled") disallowed.push("WebFetch"); + if (ctx.tools.search === "disabled") disallowed.push("WebSearch"); + if (ctx.tools.write === "disabled") disallowed.push("Write"); // both "disabled" and "restricted" block native bash // "restricted" means use MCP bash tool instead - if (tools.bash !== "enabled") disallowed.push("Bash"); + if (ctx.tools.bash !== "enabled") disallowed.push("Bash"); return disallowed; } @@ -43,19 +43,19 @@ export const claude = agent({ executablePath: "cli.js", }); }, - run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort, tools }) => { + run: async (ctx) => { // Ensure API key is NOT in process.env - only pass via SDK's env option delete process.env.ANTHROPIC_API_KEY; - const prompt = addInstructions({ payload, repo, tools }); + const prompt = addInstructions(ctx); log.group("Full prompt", () => log.info(prompt)); // select model based on effort level - const model = claudeEffortModels[effort]; - log.info(`Using model: ${model} (effort: ${effort})`); + const model = claudeEffortModels[ctx.effort]; + log.info(`Using model: ${model} (effort: ${ctx.effort})`); // build disallowedTools based on tool permissions - const disallowedTools = buildDisallowedTools(tools); + const disallowedTools = buildDisallowedTools(ctx); if (disallowedTools.length > 0) { log.info(`๐Ÿ”’ disallowed tools: ${disallowedTools.join(", ")}`); } @@ -65,10 +65,10 @@ export const claude = agent({ const queryOptions: Options = { permissionMode: "bypassPermissions" as const, disallowedTools, - mcpServers, + mcpServers: ctx.mcpServers, model, - pathToClaudeCodeExecutable: cliPath, - env: createAgentEnv({ ANTHROPIC_API_KEY: apiKey }), + pathToClaudeCodeExecutable: ctx.cliPath, + env: createAgentEnv({ ANTHROPIC_API_KEY: ctx.apiKey }), }; const queryInstance = query({ diff --git a/agents/codex.ts b/agents/codex.ts index 20c1367..1c817ad 100644 --- a/agents/codex.ts +++ b/agents/codex.ts @@ -1,6 +1,5 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk"; import { Codex, type CodexOptions, @@ -13,9 +12,9 @@ import { log } from "../utils/cli.ts"; import { addInstructions } from "./instructions.ts"; import { agent, + type AgentConfig, installFromNpmTarball, setupProcessAgentEnv, - type ToolPermissions, } from "./shared.ts"; // model configuration based on effort level @@ -35,20 +34,15 @@ const codexReasoningEffort: Record = { max: "high", }; -interface WriteCodexConfigParams { - tempHome: string; - mcpServers: Record; - tools: ToolPermissions; -} - -function writeCodexConfig({ tempHome, mcpServers, tools }: WriteCodexConfigParams): string { +function writeCodexConfig(ctx: AgentConfig): string { + const tempHome = process.env.PULLFROG_TEMP_DIR!; 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)) { + for (const [name, config] of Object.entries(ctx.mcpServers)) { if (config.type !== "http") continue; log.info(`ยป adding MCP server '${name}' at ${config.url}`); mcpServerSections.push(`[mcp_servers.${name}]\nurl = "${config.url}"`); @@ -58,7 +52,7 @@ function writeCodexConfig({ tempHome, mcpServers, tools }: WriteCodexConfigParam // disable native shell if bash is "disabled" or "restricted" // when "restricted", agent uses MCP bash tool which filters secrets const features: string[] = []; - if (tools.bash !== "enabled") { + if (ctx.tools.bash !== "enabled") { features.push("shell_command_tool = false"); features.push("unified_exec = false"); } @@ -74,7 +68,7 @@ ${mcpServerSections.join("\n\n")} ); log.info( - `ยป Codex config written to ${configPath} (shell: ${tools.bash === "enabled" ? "enabled" : "disabled"})` + `ยป Codex config written to ${configPath} (shell: ${ctx.tools.bash === "enabled" ? "enabled" : "disabled"})` ); return codexDir; @@ -89,28 +83,24 @@ export const codex = agent({ executablePath: "bin/codex.js", }); }, - run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort, tools }) => { + run: async (ctx) => { 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, - tools, - }); + const codexDir = writeCodexConfig(ctx); setupProcessAgentEnv({ - OPENAI_API_KEY: apiKey, + OPENAI_API_KEY: ctx.apiKey, HOME: tempHome, CODEX_HOME: codexDir, // point Codex to our config directory }); // get model and reasoning effort based on effort level - const model = codexModel[effort]; - const modelReasoningEffort = codexReasoningEffort[effort]; + const model = codexModel[ctx.effort]; + const modelReasoningEffort = codexReasoningEffort[ctx.effort]; log.info(`Using model: ${model}`); if (modelReasoningEffort) { log.info(`Using modelReasoningEffort: ${modelReasoningEffort}`); @@ -118,8 +108,8 @@ export const codex = agent({ // Configure Codex const codexOptions: CodexOptions = { - apiKey, - codexPathOverride: cliPath, + apiKey: ctx.apiKey, + codexPathOverride: ctx.cliPath, }; const codex = new Codex(codexOptions); @@ -129,11 +119,11 @@ export const codex = agent({ model, approvalPolicy: "never" as const, // write: "disabled" โ†’ read-only sandbox, otherwise full access for git ops - sandboxMode: tools.write === "disabled" ? "read-only" : "danger-full-access", + sandboxMode: ctx.tools.write === "disabled" ? "read-only" : "danger-full-access", // web: controls network access - networkAccessEnabled: tools.web !== "disabled", + networkAccessEnabled: ctx.tools.web !== "disabled", // search: controls web search - webSearchEnabled: tools.search !== "disabled", + webSearchEnabled: ctx.tools.search !== "disabled", ...(modelReasoningEffort && { modelReasoningEffort }), }; @@ -144,7 +134,7 @@ export const codex = agent({ const thread = codex.startThread(threadOptions); try { - const streamedTurn = await thread.runStreamed(addInstructions({ payload, repo, tools })); + const streamedTurn = await thread.runStreamed(addInstructions(ctx)); let finalOutput = ""; for await (const event of streamedTurn.events) { diff --git a/agents/cursor.ts b/agents/cursor.ts index d415c14..7de5b75 100644 --- a/agents/cursor.ts +++ b/agents/cursor.ts @@ -5,13 +5,7 @@ import { join } from "node:path"; import type { Effort } from "../external.ts"; import { log } from "../utils/cli.ts"; import { addInstructions } from "./instructions.ts"; -import { - agent, - type ConfigureMcpServersParams, - createAgentEnv, - installFromCurl, - type ToolPermissions, -} from "./shared.ts"; +import { type AgentConfig, agent, createAgentEnv, installFromCurl } from "./shared.ts"; // effort configuration for Cursor // only "max" overrides the model; mini/auto use default ("auto") @@ -101,9 +95,9 @@ export const cursor = agent({ executableName: "cursor-agent", }); }, - run: async ({ payload, apiKey, cliPath, mcpServers, repo, effort, tools }) => { - configureCursorMcpServers({ mcpServers, cliPath }); - configureCursorTools({ tools }); + run: async (ctx) => { + configureCursorMcpServers(ctx); + configureCursorTools(ctx); // determine model based on effort level // respect project's .cursor/cli.json if it specifies a model @@ -116,19 +110,19 @@ export const cursor = agent({ if (projectConfig.model) { log.info(`Using model from project .cursor/cli.json: ${projectConfig.model}`); } else { - modelOverride = cursorEffortModels[effort]; + modelOverride = cursorEffortModels[ctx.effort]; } } catch { - modelOverride = cursorEffortModels[effort]; + modelOverride = cursorEffortModels[ctx.effort]; } } else { - modelOverride = cursorEffortModels[effort]; + modelOverride = cursorEffortModels[ctx.effort]; } if (modelOverride) { - log.info(`Using model: ${modelOverride} (effort: ${effort})`); + log.info(`Using model: ${modelOverride} (effort: ${ctx.effort})`); } else if (!existsSync(projectCliConfigPath)) { - log.info(`Using default model (effort: ${effort})`); + log.info(`Using default model (effort: ${ctx.effort})`); } // track logged model_call_ids to avoid duplicates @@ -202,7 +196,7 @@ export const cursor = agent({ }; try { - const fullPrompt = addInstructions({ payload, repo, tools }); + const fullPrompt = addInstructions(ctx); log.group("Full prompt", () => log.info(fullPrompt)); // build CLI args @@ -221,10 +215,10 @@ export const cursor = agent({ const startTime = Date.now(); return new Promise((resolve) => { - const child = spawn(cliPath, cursorArgs, { + const child = spawn(ctx.cliPath, cursorArgs, { cwd: process.cwd(), env: createAgentEnv({ - CURSOR_API_KEY: apiKey, + CURSOR_API_KEY: ctx.apiKey, }), stdio: ["ignore", "pipe", "pipe"], // Ignore stdin, pipe stdout/stderr }); @@ -317,7 +311,7 @@ export const cursor = agent({ // There was an issue on macOS when you set HOME to a temp directory // it was unable to find the macOS keychain and would fail // temp solution is to stick with the actual $HOME -function configureCursorMcpServers({ mcpServers }: ConfigureMcpServersParams) { +function configureCursorMcpServers(ctx: AgentConfig): void { const realHome = homedir(); const cursorConfigDir = join(realHome, ".cursor"); const mcpConfigPath = join(cursorConfigDir, "mcp.json"); @@ -325,7 +319,7 @@ function configureCursorMcpServers({ mcpServers }: ConfigureMcpServersParams) { // Convert to Cursor's expected format (HTTP config) const cursorMcpServers: Record = {}; - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) { if (serverConfig.type !== "http") { throw new Error( `Unsupported MCP server type for Cursor: ${(serverConfig as any).type || "unknown"}` @@ -342,8 +336,15 @@ function configureCursorMcpServers({ mcpServers }: ConfigureMcpServersParams) { log.info(`ยป MCP config written to ${mcpConfigPath}`); } -interface ConfigureCursorToolsParams { - tools: ToolPermissions; +interface CursorCliConfig { + permissions: { + allow: string[]; + deny: string[]; + }; + sandbox?: { + mode: "enabled" | "disabled"; + networkAccess?: "allowlist" | "full"; + }; } /** @@ -352,7 +353,7 @@ interface ConfigureCursorToolsParams { * Config path: $XDG_CONFIG_HOME/cursor/ (not ~/.cursor/) because createAgentEnv * sets XDG_CONFIG_HOME=$HOME/.config. */ -function configureCursorTools({ tools }: ConfigureCursorToolsParams): void { +function configureCursorTools(ctx: AgentConfig): void { const realHome = homedir(); const cursorConfigDir = join(realHome, ".config", "cursor"); const cliConfigPath = join(cursorConfigDir, "cli-config.json"); @@ -360,23 +361,21 @@ function configureCursorTools({ tools }: ConfigureCursorToolsParams): void { // build deny list based on tool permissions const deny: string[] = []; - if (tools.search === "disabled") deny.push("WebSearch"); - if (tools.write === "disabled") deny.push("Write(**)"); + if (ctx.tools.search === "disabled") deny.push("WebSearch"); + if (ctx.tools.write === "disabled") deny.push("Write(**)"); // both "disabled" and "restricted" block native shell - if (tools.bash !== "enabled") deny.push("Shell(*)"); + if (ctx.tools.bash !== "enabled") deny.push("Shell(*)"); - // web: "disabled" requires sandbox with network blocking - // sandbox.networkAccess: "allowlist" blocks network in shell subprocesses via seatbelt - const needsSandbox = tools.web === "disabled"; - - const config: Record = { + const config: CursorCliConfig = { permissions: { - allow: tools.write === "disabled" ? ["Read(**)"] : ["Read(**)", "Write(**)"], + allow: ctx.tools.write === "disabled" ? ["Read(**)"] : ["Read(**)", "Write(**)"], deny, }, }; - if (needsSandbox) { + // web: "disabled" requires sandbox with network blocking + // sandbox.networkAccess: "allowlist" blocks network in shell subprocesses via seatbelt + if (ctx.tools.web === "disabled") { config.sandbox = { mode: "enabled", networkAccess: "allowlist", @@ -384,8 +383,5 @@ function configureCursorTools({ tools }: ConfigureCursorToolsParams): void { } writeFileSync(cliConfigPath, JSON.stringify(config, null, 2), "utf-8"); - log.info(`ยป CLI config written to ${cliConfigPath}`); - log.info( - `๐Ÿ”ง Cursor permissions: allow=${(config.permissions as { allow: string[] }).allow.join(",")}, deny=${deny.join(",") || "(none)"}, sandbox=${needsSandbox ? "enabled" : "disabled"}` - ); + log.info(`ยป CLI config written to ${cliConfigPath}`, JSON.stringify(config, null, 2)); } diff --git a/agents/gemini.ts b/agents/gemini.ts index 07f2ef9..c90455c 100644 --- a/agents/gemini.ts +++ b/agents/gemini.ts @@ -5,13 +5,7 @@ import type { Effort } from "../external.ts"; import { log } from "../utils/cli.ts"; import { spawn } from "../utils/subprocess.ts"; import { addInstructions } from "./instructions.ts"; -import { - agent, - type ConfigureMcpServersParams, - createAgentEnv, - installFromGithub, - type ToolPermissions, -} from "./shared.ts"; +import { type AgentConfig, agent, createAgentEnv, installFromGithub } from "./shared.ts"; // effort configuration: model + thinking level // thinkingLevel is set via settings.json modelConfig.generateContentConfig.thinkingConfig @@ -172,18 +166,14 @@ export const gemini = agent({ ...(githubInstallationToken && { githubInstallationToken }), }); }, - run: async ({ payload, apiKey, mcpServers, cliPath, repo, effort, tools }) => { - // get model and thinking level based on effort - const { model, thinkingLevel } = geminiEffortConfig[effort]; - log.info(`Using model: ${model}, thinkingLevel: ${thinkingLevel}`); + run: async (ctx) => { + const model = configureGeminiSettings(ctx); - configureGeminiSettings({ mcpServers, tools, thinkingLevel }); - - if (!apiKey) { + if (!ctx.apiKey) { throw new Error("google_api_key or gemini_api_key is required for gemini agent"); } - const sessionPrompt = addInstructions({ payload, repo, tools }); + const sessionPrompt = addInstructions(ctx); log.group("Full prompt", () => log.info(sessionPrompt)); // build CLI args - --yolo for auto-approval @@ -196,8 +186,8 @@ export const gemini = agent({ try { const result = await spawn({ cmd: "node", - args: [cliPath, ...args], - env: createAgentEnv({ GEMINI_API_KEY: apiKey }), + args: [ctx.cliPath, ...args], + env: createAgentEnv({ GEMINI_API_KEY: ctx.apiKey }), onStdout: async (chunk) => { const text = chunk.toString(); finalOutput += text; @@ -270,25 +260,16 @@ export const gemini = agent({ }, }); -interface ConfigureGeminiParams { - mcpServers: ConfigureMcpServersParams["mcpServers"]; - tools: ToolPermissions; - thinkingLevel: string; -} - /** * Configure Gemini CLI settings by writing to settings.json. - * - MCP servers: uses `httpUrl` for HTTP/streamable transport - * - thinkingLevel: configured via modelConfig.generateContentConfig.thinkingConfig - * - tools.exclude: disables native tools based on ToolPermissions (v0.3.0+ format) + * Returns the model to use for CLI args. * * See: https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/configuration.md */ -function configureGeminiSettings({ - mcpServers, - tools, - thinkingLevel, -}: ConfigureGeminiParams): void { +function configureGeminiSettings(ctx: AgentConfig): string { + const { model, thinkingLevel } = geminiEffortConfig[ctx.effort]; + log.info(`Using model: ${model}, thinkingLevel: ${thinkingLevel}`); + const realHome = homedir(); const geminiConfigDir = join(realHome, ".gemini"); const settingsPath = join(geminiConfigDir, "settings.json"); @@ -319,7 +300,7 @@ function configureGeminiSettings({ excludeTools?: string[]; } const geminiMcpServers: Record = {}; - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) { if (serverConfig.type !== "http") { throw new Error( `Unsupported MCP server type for Gemini: ${(serverConfig as { type?: string }).type || "unknown"}` @@ -334,10 +315,10 @@ function configureGeminiSettings({ // build tools.exclude based on permissions (v0.3.0+ nested format) const exclude: string[] = []; - if (tools.bash !== "enabled") exclude.push("run_shell_command"); - if (tools.write === "disabled") exclude.push("write_file"); - if (tools.web === "disabled") exclude.push("web_fetch"); - if (tools.search === "disabled") exclude.push("google_web_search"); + if (ctx.tools.bash !== "enabled") exclude.push("run_shell_command"); + if (ctx.tools.write === "disabled") exclude.push("write_file"); + if (ctx.tools.web === "disabled") exclude.push("web_fetch"); + if (ctx.tools.search === "disabled") exclude.push("google_web_search"); // merge with existing settings, overwriting mcpServers and modelConfig const newSettings: Record = { @@ -361,4 +342,6 @@ function configureGeminiSettings({ if (exclude.length > 0) { log.info(`๐Ÿ”’ excluded tools: ${exclude.join(", ")}`); } + + return model; } diff --git a/agents/instructions.ts b/agents/instructions.ts index 4e19c8e..92aad38 100644 --- a/agents/instructions.ts +++ b/agents/instructions.ts @@ -3,14 +3,7 @@ import { encode as toonEncode } from "@toon-format/toon"; import type { Payload } from "../external.ts"; import { ghPullfrogMcpName } from "../external.ts"; import { getModes } from "../modes.ts"; -import type { ToolPermissions } from "./shared.ts"; - -interface RepoInfo { - owner: string; - name: string; - defaultBranch: string; - isPublic: boolean; -} +import type { RepoInfo, ToolPermissions } from "./shared.ts"; /** * Build runtime context string with git status, repo data, and GitHub Actions variables @@ -52,7 +45,7 @@ function buildRuntimeContext(repo: RepoInfo): string { return lines.join("\n"); } -interface AddInstructionsParams { +interface AddInstructionsCtx { payload: Payload; repo: RepoInfo; tools: ToolPermissions; @@ -76,19 +69,19 @@ function getShellInstructions(bash: ToolPermissions["bash"]): string { } } -export const addInstructions = ({ payload, repo, tools }: AddInstructionsParams) => { +export const addInstructions = (ctx: AddInstructionsCtx) => { let encodedEvent = ""; - const eventKeys = Object.keys(payload.event); + const eventKeys = Object.keys(ctx.payload.event); if (eventKeys.length === 1 && eventKeys[0] === "trigger") { // no meaningful event data to encode } else { // extract only essential fields to reduce token usage - // const essentialEvent = payload.event; - encodedEvent = toonEncode(payload.event); + // const essentialEvent = ctx.payload.event; + encodedEvent = toonEncode(ctx.payload.event); } - const runtimeContext = buildRuntimeContext(repo); + const runtimeContext = buildRuntimeContext(ctx.repo); return ( ` @@ -159,7 +152,7 @@ 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. -${getShellInstructions(tools.bash)} +${getShellInstructions(ctx.tools.bash)} **Command execution**: Never use \`sleep\` to wait for commands to complete. Commands run synchronously - when the bash tool returns, the command has finished. @@ -180,7 +173,7 @@ ${getShellInstructions(tools.bash)} ### Available modes -${[...getModes({ disableProgressComment: payload.disableProgressComment }), ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")} +${[...getModes({ disableProgressComment: ctx.payload.disableProgressComment }), ...ctx.payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")} ### Following the mode instructions @@ -190,7 +183,7 @@ Eagerly inspect the MCP tools available to you via the \`${ghPullfrogMcpName}\` ************* USER PROMPT ************* -${payload.prompt +${ctx.payload.prompt .split("\n") .map((line) => `> ${line}`) .join("\n")} diff --git a/agents/opencode.ts b/agents/opencode.ts index c5a2439..030e32b 100644 --- a/agents/opencode.ts +++ b/agents/opencode.ts @@ -5,11 +5,10 @@ import { spawn } from "../utils/subprocess.ts"; import { addInstructions } from "./instructions.ts"; import { agent, - type ConfigureMcpServersParams, + type AgentConfig, createAgentEnv, installFromNpmTarball, setupProcessAgentEnv, - type ToolPermissions, } from "./shared.ts"; export const opencode = agent({ @@ -22,24 +21,15 @@ export const opencode = agent({ installDependencies: true, }); }, - run: async ({ - payload, - apiKey: _apiKey, - apiKeys, - mcpServers, - cliPath, - repo, - effort: _effort, - tools, - }) => { + run: async (ctx) => { // 1. configure home/config directory const tempHome = process.env.PULLFROG_TEMP_DIR!; const configDir = join(tempHome, ".config", "opencode"); mkdirSync(configDir, { recursive: true }); - configureOpenCode({ mcpServers, tools }); + configureOpenCode(ctx); - const prompt = addInstructions({ payload, repo, tools }); + const prompt = addInstructions(ctx); log.group("Full prompt", () => log.info(prompt)); // message positional must come right after "run", before flags @@ -60,7 +50,7 @@ export const opencode = agent({ delete env.GITHUB_TOKEN; // add API keys from apiKeys object - for (const [key, value] of Object.entries(apiKeys || {})) { + for (const [key, value] of Object.entries(ctx.apiKeys || {})) { env[key.toUpperCase()] = value; // also set GOOGLE_GENERATIVE_AI_API_KEY for Google provider compatibility if (key === "GEMINI_API_KEY") { @@ -71,7 +61,7 @@ export const opencode = agent({ // run OpenCode in the repository directory (process.cwd() is set to GITHUB_WORKSPACE or repo dir) const repoDir = process.cwd(); - log.info(`๐Ÿš€ Starting OpenCode CLI: ${cliPath} ${args.join(" ")}`); + log.info(`๐Ÿš€ Starting OpenCode CLI: ${ctx.cliPath} ${args.join(" ")}`); log.info(`๐Ÿ“ Working directory: ${repoDir}`); log.debug(`๐Ÿ  HOME: ${env.HOME}`); log.debug(`๐Ÿ“‹ XDG_CONFIG_HOME: ${env.XDG_CONFIG_HOME}`); @@ -83,7 +73,7 @@ export const opencode = agent({ let output = ""; let stdoutBuffer = ""; // buffer for incomplete lines across chunks const result = await spawn({ - cmd: cliPath, + cmd: ctx.cliPath, args, cwd: repoDir, env, @@ -191,16 +181,11 @@ export const opencode = agent({ }, }); -interface ConfigureOpenCodeParams { - mcpServers: ConfigureMcpServersParams["mcpServers"]; - tools: ToolPermissions; -} - /** * 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, tools }: ConfigureOpenCodeParams): void { +function configureOpenCode(ctx: AgentConfig): void { const tempHome = process.env.PULLFROG_TEMP_DIR!; const configDir = join(tempHome, ".config", "opencode"); mkdirSync(configDir, { recursive: true }); @@ -208,7 +193,7 @@ function configureOpenCode({ mcpServers, tools }: ConfigureOpenCodeParams): void // build MCP servers config const opencodeMcpServers: Record = {}; - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) { if (serverConfig.type !== "http") { log.error( `unsupported MCP server type for OpenCode: ${(serverConfig as never as { type: string }).type || "unknown"}` @@ -227,9 +212,9 @@ function configureOpenCode({ mcpServers, tools }: ConfigureOpenCodeParams): void // build permission object based on tool permissions // note: OpenCode has no built-in web search tool const permission = { - edit: tools.write === "disabled" ? "deny" : "allow", - bash: tools.bash !== "enabled" ? "deny" : "allow", - webfetch: tools.web === "disabled" ? "deny" : "allow", + edit: ctx.tools.write === "disabled" ? "deny" : "allow", + bash: ctx.tools.bash !== "enabled" ? "deny" : "allow", + webfetch: ctx.tools.web === "disabled" ? "deny" : "allow", doom_loop: "allow", external_directory: "allow", }; diff --git a/agents/shared.ts b/agents/shared.ts index fe2c873..78a486a 100644 --- a/agents/shared.ts +++ b/agents/shared.ts @@ -66,14 +66,6 @@ export interface AgentConfig { tools: ToolPermissions; } -/** - * Parameters for configuring MCP servers - */ -export interface ConfigureMcpServersParams { - mcpServers: Record; - cliPath: string; -} - /** * Add agent-specific vars to a whitelisted environment object for agent subprocesses. * diff --git a/dispatch/entry b/dispatch/entry index 26a4065..f0e3767 100755 --- a/dispatch/entry +++ b/dispatch/entry @@ -100032,11 +100032,19 @@ var package_default = { packageManager: "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a" }; -// utils/cli.ts +// utils/log.ts var core = __toESM(require_core(), 1); var import_table = __toESM(require_src(), 1); var isGitHubActions = !!process.env.GITHUB_ACTIONS; var isDebugEnabled = () => process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true" || process.env.RUNNER_DEBUG === "1" || core.isDebug(); +function formatArgs(args3) { + return args3.map((arg) => { + if (typeof arg === "string") return arg; + if (arg instanceof Error) return `${arg.message} +${arg.stack}`; + return JSON.stringify(arg); + }).join(" "); +} function startGroup2(name) { if (isGitHubActions) { core.startGroup(name); @@ -100145,25 +100153,25 @@ function separator(length = 50) { } var log = { /** Print info message */ - info: (message) => { - core.info(message); + info: (...args3) => { + core.info(formatArgs(args3)); }, /** Print warning message */ - warning: (message) => { - core.warning(message); + warning: (...args3) => { + core.warning(formatArgs(args3)); }, /** Print error message */ - error: (message) => { - core.error(message); + error: (...args3) => { + core.error(formatArgs(args3)); }, /** Print success message */ - success: (message) => { - core.info(`\u2705 ${message}`); + success: (...args3) => { + core.info(`\u2705 ${formatArgs(args3)}`); }, /** Print debug message (only if LOG_LEVEL=debug) */ - debug: (message) => { + debug: (...args3) => { if (isDebugEnabled()) { - core.info(`[DEBUG] ${message}`); + core.info(`[DEBUG] ${formatArgs(args3)}`); } }, /** Print a formatted box with text */ @@ -100420,14 +100428,14 @@ function getShellInstructions(bash) { } } } -var addInstructions = ({ payload, repo, tools }) => { +var addInstructions = (ctx) => { let encodedEvent = ""; - const eventKeys = Object.keys(payload.event); + const eventKeys = Object.keys(ctx.payload.event); if (eventKeys.length === 1 && eventKeys[0] === "trigger") { } else { - encodedEvent = encode(payload.event); + encodedEvent = encode(ctx.payload.event); } - const runtimeContext = buildRuntimeContext(repo); + const runtimeContext = buildRuntimeContext(ctx.repo); return ` *********************************************** ************* SYSTEM INSTRUCTIONS ************* @@ -100475,7 +100483,7 @@ 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. -${getShellInstructions(tools.bash)} +${getShellInstructions(ctx.tools.bash)} **Command execution**: Never use \`sleep\` to wait for commands to complete. Commands run synchronously - when the bash tool returns, the command has finished. @@ -100496,7 +100504,7 @@ ${getShellInstructions(tools.bash)} ### Available modes -${[...getModes({ disableProgressComment: payload.disableProgressComment }), ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")} +${[...getModes({ disableProgressComment: ctx.payload.disableProgressComment }), ...ctx.payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")} ### Following the mode instructions @@ -100506,7 +100514,7 @@ Eagerly inspect the MCP tools available to you via the \`${ghPullfrogMcpName}\` ************* USER PROMPT ************* -${payload.prompt.split("\n").map((line) => `> ${line}`).join("\n")} +${ctx.payload.prompt.split("\n").map((line) => `> ${line}`).join("\n")} ${encodedEvent ? `************* EVENT DATA ************* @@ -104635,12 +104643,12 @@ var claudeEffortModels = { auto: "opusplan", max: "opus" }; -function buildDisallowedTools(tools) { +function buildDisallowedTools(ctx) { const disallowed = []; - if (tools.web === "disabled") disallowed.push("WebFetch"); - if (tools.search === "disabled") disallowed.push("WebSearch"); - if (tools.write === "disabled") disallowed.push("Write"); - if (tools.bash !== "enabled") disallowed.push("Bash"); + if (ctx.tools.web === "disabled") disallowed.push("WebFetch"); + if (ctx.tools.search === "disabled") disallowed.push("WebSearch"); + if (ctx.tools.write === "disabled") disallowed.push("Write"); + if (ctx.tools.bash !== "enabled") disallowed.push("Bash"); return disallowed; } var claude = agent({ @@ -104653,23 +104661,23 @@ var claude = agent({ executablePath: "cli.js" }); }, - run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort, tools }) => { + run: async (ctx) => { delete process.env.ANTHROPIC_API_KEY; - const prompt = addInstructions({ payload, repo, tools }); + const prompt = addInstructions(ctx); log.group("Full prompt", () => log.info(prompt)); - const model = claudeEffortModels[effort]; - log.info(`Using model: ${model} (effort: ${effort})`); - const disallowedTools = buildDisallowedTools(tools); + const model = claudeEffortModels[ctx.effort]; + log.info(`Using model: ${model} (effort: ${ctx.effort})`); + const disallowedTools = buildDisallowedTools(ctx); if (disallowedTools.length > 0) { log.info(`\u{1F512} disallowed tools: ${disallowedTools.join(", ")}`); } const queryOptions = { permissionMode: "bypassPermissions", disallowedTools, - mcpServers, + mcpServers: ctx.mcpServers, model, - pathToClaudeCodeExecutable: cliPath, - env: createAgentEnv({ ANTHROPIC_API_KEY: apiKey }) + pathToClaudeCodeExecutable: ctx.cliPath, + env: createAgentEnv({ ANTHROPIC_API_KEY: ctx.apiKey }) }; const queryInstance = query({ prompt, @@ -105132,19 +105140,20 @@ var codexReasoningEffort = { // use default max: "high" }; -function writeCodexConfig({ tempHome, mcpServers, tools }) { +function writeCodexConfig(ctx) { + const tempHome = process.env.PULLFROG_TEMP_DIR; const codexDir = join6(tempHome, ".codex"); mkdirSync3(codexDir, { recursive: true }); const configPath = join6(codexDir, "config.toml"); const mcpServerSections = []; - for (const [name, config4] of Object.entries(mcpServers)) { + for (const [name, config4] of Object.entries(ctx.mcpServers)) { if (config4.type !== "http") continue; log.info(`\xBB adding MCP server '${name}' at ${config4.url}`); mcpServerSections.push(`[mcp_servers.${name}] url = "${config4.url}"`); } const features = []; - if (tools.bash !== "enabled") { + if (ctx.tools.bash !== "enabled") { features.push("shell_command_tool = false"); features.push("unified_exec = false"); } @@ -105159,7 +105168,7 @@ ${mcpServerSections.join("\n\n")} `.trim() + "\n" ); log.info( - `\xBB Codex config written to ${configPath} (shell: ${tools.bash === "enabled" ? "enabled" : "disabled"})` + `\xBB Codex config written to ${configPath} (shell: ${ctx.tools.bash === "enabled" ? "enabled" : "disabled"})` ); return codexDir; } @@ -105172,41 +105181,37 @@ var codex = agent({ executablePath: "bin/codex.js" }); }, - run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort, tools }) => { + run: async (ctx) => { const tempHome = process.env.PULLFROG_TEMP_DIR; const configDir = join6(tempHome, ".config", "codex"); mkdirSync3(configDir, { recursive: true }); - const codexDir = writeCodexConfig({ - tempHome, - mcpServers, - tools - }); + const codexDir = writeCodexConfig(ctx); setupProcessAgentEnv({ - OPENAI_API_KEY: apiKey, + OPENAI_API_KEY: ctx.apiKey, HOME: tempHome, CODEX_HOME: codexDir // point Codex to our config directory }); - const model = codexModel[effort]; - const modelReasoningEffort = codexReasoningEffort[effort]; + const model = codexModel[ctx.effort]; + const modelReasoningEffort = codexReasoningEffort[ctx.effort]; log.info(`Using model: ${model}`); if (modelReasoningEffort) { log.info(`Using modelReasoningEffort: ${modelReasoningEffort}`); } const codexOptions = { - apiKey, - codexPathOverride: cliPath + apiKey: ctx.apiKey, + codexPathOverride: ctx.cliPath }; const codex2 = new Codex(codexOptions); const threadOptions = { model, approvalPolicy: "never", // write: "disabled" โ†’ read-only sandbox, otherwise full access for git ops - sandboxMode: tools.write === "disabled" ? "read-only" : "danger-full-access", + sandboxMode: ctx.tools.write === "disabled" ? "read-only" : "danger-full-access", // web: controls network access - networkAccessEnabled: tools.web !== "disabled", + networkAccessEnabled: ctx.tools.web !== "disabled", // search: controls web search - webSearchEnabled: tools.search !== "disabled", + webSearchEnabled: ctx.tools.search !== "disabled", ...modelReasoningEffort && { modelReasoningEffort } }; log.info( @@ -105214,7 +105219,7 @@ var codex = agent({ ); const thread = codex2.startThread(threadOptions); try { - const streamedTurn = await thread.runStreamed(addInstructions({ payload, repo, tools })); + const streamedTurn = await thread.runStreamed(addInstructions(ctx)); let finalOutput2 = ""; for await (const event of streamedTurn.events) { const handler2 = messageHandlers2[event.type]; @@ -105341,9 +105346,9 @@ var cursor = agent({ executableName: "cursor-agent" }); }, - run: async ({ payload, apiKey, cliPath, mcpServers, repo, effort, tools }) => { - configureCursorMcpServers({ mcpServers, cliPath }); - configureCursorTools({ tools }); + run: async (ctx) => { + configureCursorMcpServers(ctx); + configureCursorTools(ctx); const projectCliConfigPath = join7(process.cwd(), ".cursor", "cli.json"); let modelOverride = null; if (existsSync4(projectCliConfigPath)) { @@ -105352,18 +105357,18 @@ var cursor = agent({ if (projectConfig.model) { log.info(`Using model from project .cursor/cli.json: ${projectConfig.model}`); } else { - modelOverride = cursorEffortModels[effort]; + modelOverride = cursorEffortModels[ctx.effort]; } } catch { - modelOverride = cursorEffortModels[effort]; + modelOverride = cursorEffortModels[ctx.effort]; } } else { - modelOverride = cursorEffortModels[effort]; + modelOverride = cursorEffortModels[ctx.effort]; } if (modelOverride) { - log.info(`Using model: ${modelOverride} (effort: ${effort})`); + log.info(`Using model: ${modelOverride} (effort: ${ctx.effort})`); } else if (!existsSync4(projectCliConfigPath)) { - log.info(`Using default model (effort: ${effort})`); + log.info(`Using default model (effort: ${ctx.effort})`); } const loggedModelCallIds = /* @__PURE__ */ new Set(); const messageHandlers5 = { @@ -105415,7 +105420,7 @@ var cursor = agent({ } }; try { - const fullPrompt = addInstructions({ payload, repo, tools }); + const fullPrompt = addInstructions(ctx); log.group("Full prompt", () => log.info(fullPrompt)); const baseArgs = ["--print", fullPrompt, "--output-format", "stream-json", "--approve-mcps"]; if (modelOverride) { @@ -105425,10 +105430,10 @@ var cursor = agent({ log.info("Running Cursor CLI..."); const startTime = Date.now(); return new Promise((resolve2) => { - const child = spawn3(cliPath, cursorArgs, { + const child = spawn3(ctx.cliPath, cursorArgs, { cwd: process.cwd(), env: createAgentEnv({ - CURSOR_API_KEY: apiKey + CURSOR_API_KEY: ctx.apiKey }), stdio: ["ignore", "pipe", "pipe"] // Ignore stdin, pipe stdout/stderr @@ -105503,13 +105508,13 @@ var cursor = agent({ } } }); -function configureCursorMcpServers({ mcpServers }) { +function configureCursorMcpServers(ctx) { const realHome = homedir2(); const cursorConfigDir = join7(realHome, ".cursor"); const mcpConfigPath = join7(cursorConfigDir, "mcp.json"); mkdirSync4(cursorConfigDir, { recursive: true }); const cursorMcpServers = {}; - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) { if (serverConfig.type !== "http") { throw new Error( `Unsupported MCP server type for Cursor: ${serverConfig.type || "unknown"}` @@ -105523,33 +105528,29 @@ function configureCursorMcpServers({ mcpServers }) { writeFileSync2(mcpConfigPath, JSON.stringify({ mcpServers: cursorMcpServers }, null, 2), "utf-8"); log.info(`\xBB MCP config written to ${mcpConfigPath}`); } -function configureCursorTools({ tools }) { +function configureCursorTools(ctx) { const realHome = homedir2(); const cursorConfigDir = join7(realHome, ".config", "cursor"); const cliConfigPath = join7(cursorConfigDir, "cli-config.json"); mkdirSync4(cursorConfigDir, { recursive: true }); const deny = []; - if (tools.search === "disabled") deny.push("WebSearch"); - if (tools.write === "disabled") deny.push("Write(**)"); - if (tools.bash !== "enabled") deny.push("Shell(*)"); - const needsSandbox = tools.web === "disabled"; + if (ctx.tools.search === "disabled") deny.push("WebSearch"); + if (ctx.tools.write === "disabled") deny.push("Write(**)"); + if (ctx.tools.bash !== "enabled") deny.push("Shell(*)"); const config4 = { permissions: { - allow: tools.write === "disabled" ? ["Read(**)"] : ["Read(**)", "Write(**)"], + allow: ctx.tools.write === "disabled" ? ["Read(**)"] : ["Read(**)", "Write(**)"], deny } }; - if (needsSandbox) { + if (ctx.tools.web === "disabled") { config4.sandbox = { mode: "enabled", networkAccess: "allowlist" }; } writeFileSync2(cliConfigPath, JSON.stringify(config4, null, 2), "utf-8"); - log.info(`\xBB CLI config written to ${cliConfigPath}`); - log.info( - `\u{1F527} Cursor permissions: allow=${config4.permissions.allow.join(",")}, deny=${deny.join(",") || "(none)"}, sandbox=${needsSandbox ? "enabled" : "disabled"}` - ); + log.info(`\xBB CLI config written to ${cliConfigPath}`, JSON.stringify(config4, null, 2)); } // agents/gemini.ts @@ -105725,14 +105726,12 @@ var gemini = agent({ ...githubInstallationToken2 && { githubInstallationToken: githubInstallationToken2 } }); }, - run: async ({ payload, apiKey, mcpServers, cliPath, repo, effort, tools }) => { - const { model, thinkingLevel } = geminiEffortConfig[effort]; - log.info(`Using model: ${model}, thinkingLevel: ${thinkingLevel}`); - configureGeminiSettings({ mcpServers, tools, thinkingLevel }); - if (!apiKey) { + run: async (ctx) => { + const model = configureGeminiSettings(ctx); + if (!ctx.apiKey) { throw new Error("google_api_key or gemini_api_key is required for gemini agent"); } - const sessionPrompt = addInstructions({ payload, repo, tools }); + const sessionPrompt = addInstructions(ctx); log.group("Full prompt", () => log.info(sessionPrompt)); const args3 = ["--model", model, "--yolo", "--output-format=stream-json", "-p", sessionPrompt]; let finalOutput2 = ""; @@ -105740,8 +105739,8 @@ var gemini = agent({ try { const result = await spawn4({ cmd: "node", - args: [cliPath, ...args3], - env: createAgentEnv({ GEMINI_API_KEY: apiKey }), + args: [ctx.cliPath, ...args3], + env: createAgentEnv({ GEMINI_API_KEY: ctx.apiKey }), onStdout: async (chunk) => { const text = chunk.toString(); finalOutput2 += text; @@ -105798,11 +105797,9 @@ var gemini = agent({ } } }); -function configureGeminiSettings({ - mcpServers, - tools, - thinkingLevel -}) { +function configureGeminiSettings(ctx) { + const { model, thinkingLevel } = geminiEffortConfig[ctx.effort]; + log.info(`Using model: ${model}, thinkingLevel: ${thinkingLevel}`); const realHome = homedir3(); const geminiConfigDir = join8(realHome, ".gemini"); const settingsPath = join8(geminiConfigDir, "settings.json"); @@ -105814,7 +105811,7 @@ function configureGeminiSettings({ } catch { } const geminiMcpServers = {}; - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) { if (serverConfig.type !== "http") { throw new Error( `Unsupported MCP server type for Gemini: ${serverConfig.type || "unknown"}` @@ -105828,10 +105825,10 @@ function configureGeminiSettings({ log.info(`adding MCP server '${serverName}' at ${serverConfig.url}...`); } const exclude = []; - if (tools.bash !== "enabled") exclude.push("run_shell_command"); - if (tools.write === "disabled") exclude.push("write_file"); - if (tools.web === "disabled") exclude.push("web_fetch"); - if (tools.search === "disabled") exclude.push("google_web_search"); + if (ctx.tools.bash !== "enabled") exclude.push("run_shell_command"); + if (ctx.tools.write === "disabled") exclude.push("write_file"); + if (ctx.tools.web === "disabled") exclude.push("web_fetch"); + if (ctx.tools.search === "disabled") exclude.push("google_web_search"); const newSettings = { ...existingSettings, mcpServers: geminiMcpServers, @@ -105852,6 +105849,7 @@ function configureGeminiSettings({ if (exclude.length > 0) { log.info(`\u{1F512} excluded tools: ${exclude.join(", ")}`); } + return model; } // agents/opencode.ts @@ -105867,21 +105865,12 @@ var opencode = agent({ installDependencies: true }); }, - run: async ({ - payload, - apiKey: _apiKey, - apiKeys, - mcpServers, - cliPath, - repo, - effort: _effort, - tools - }) => { + run: async (ctx) => { const tempHome = process.env.PULLFROG_TEMP_DIR; const configDir = join9(tempHome, ".config", "opencode"); mkdirSync6(configDir, { recursive: true }); - configureOpenCode({ mcpServers, tools }); - const prompt = addInstructions({ payload, repo, tools }); + configureOpenCode(ctx); + const prompt = addInstructions(ctx); log.group("Full prompt", () => log.info(prompt)); const args3 = ["run", prompt, "--format", "json"]; setupProcessAgentEnv({ HOME: tempHome }); @@ -105890,14 +105879,14 @@ var opencode = agent({ XDG_CONFIG_HOME: join9(tempHome, ".config") }; delete env3.GITHUB_TOKEN; - for (const [key, value2] of Object.entries(apiKeys || {})) { + for (const [key, value2] of Object.entries(ctx.apiKeys || {})) { env3[key.toUpperCase()] = value2; if (key === "GEMINI_API_KEY") { env3.GOOGLE_GENERATIVE_AI_API_KEY = value2; } } const repoDir = process.cwd(); - log.info(`\u{1F680} Starting OpenCode CLI: ${cliPath} ${args3.join(" ")}`); + log.info(`\u{1F680} Starting OpenCode CLI: ${ctx.cliPath} ${args3.join(" ")}`); log.info(`\u{1F4C1} Working directory: ${repoDir}`); log.debug(`\u{1F3E0} HOME: ${env3.HOME}`); log.debug(`\u{1F4CB} XDG_CONFIG_HOME: ${env3.XDG_CONFIG_HOME}`); @@ -105907,7 +105896,7 @@ var opencode = agent({ let output = ""; let stdoutBuffer = ""; const result = await spawn4({ - cmd: cliPath, + cmd: ctx.cliPath, args: args3, cwd: repoDir, env: env3, @@ -105993,13 +105982,13 @@ var opencode = agent({ }; } }); -function configureOpenCode({ mcpServers, tools }) { +function configureOpenCode(ctx) { const tempHome = process.env.PULLFROG_TEMP_DIR; const configDir = join9(tempHome, ".config", "opencode"); mkdirSync6(configDir, { recursive: true }); const configPath = join9(configDir, "opencode.json"); const opencodeMcpServers = {}; - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) { if (serverConfig.type !== "http") { log.error( `unsupported MCP server type for OpenCode: ${serverConfig.type || "unknown"}` @@ -106014,9 +106003,9 @@ function configureOpenCode({ mcpServers, tools }) { }; } const permission = { - edit: tools.write === "disabled" ? "deny" : "allow", - bash: tools.bash !== "enabled" ? "deny" : "allow", - webfetch: tools.web === "disabled" ? "deny" : "allow", + edit: ctx.tools.write === "disabled" ? "deny" : "allow", + bash: ctx.tools.bash !== "enabled" ? "deny" : "allow", + webfetch: ctx.tools.web === "disabled" ? "deny" : "allow", doom_loop: "allow", external_directory: "allow" }; diff --git a/entry b/entry index 7f32aae..7652443 100755 --- a/entry +++ b/entry @@ -100032,11 +100032,19 @@ var package_default = { packageManager: "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a" }; -// utils/cli.ts +// utils/log.ts var core = __toESM(require_core(), 1); var import_table = __toESM(require_src(), 1); var isGitHubActions = !!process.env.GITHUB_ACTIONS; var isDebugEnabled = () => process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true" || process.env.RUNNER_DEBUG === "1" || core.isDebug(); +function formatArgs(args3) { + return args3.map((arg) => { + if (typeof arg === "string") return arg; + if (arg instanceof Error) return `${arg.message} +${arg.stack}`; + return JSON.stringify(arg); + }).join(" "); +} function startGroup2(name) { if (isGitHubActions) { core.startGroup(name); @@ -100145,25 +100153,25 @@ function separator(length = 50) { } var log = { /** Print info message */ - info: (message) => { - core.info(message); + info: (...args3) => { + core.info(formatArgs(args3)); }, /** Print warning message */ - warning: (message) => { - core.warning(message); + warning: (...args3) => { + core.warning(formatArgs(args3)); }, /** Print error message */ - error: (message) => { - core.error(message); + error: (...args3) => { + core.error(formatArgs(args3)); }, /** Print success message */ - success: (message) => { - core.info(`\u2705 ${message}`); + success: (...args3) => { + core.info(`\u2705 ${formatArgs(args3)}`); }, /** Print debug message (only if LOG_LEVEL=debug) */ - debug: (message) => { + debug: (...args3) => { if (isDebugEnabled()) { - core.info(`[DEBUG] ${message}`); + core.info(`[DEBUG] ${formatArgs(args3)}`); } }, /** Print a formatted box with text */ @@ -100420,14 +100428,14 @@ function getShellInstructions(bash) { } } } -var addInstructions = ({ payload, repo, tools }) => { +var addInstructions = (ctx) => { let encodedEvent = ""; - const eventKeys = Object.keys(payload.event); + const eventKeys = Object.keys(ctx.payload.event); if (eventKeys.length === 1 && eventKeys[0] === "trigger") { } else { - encodedEvent = encode(payload.event); + encodedEvent = encode(ctx.payload.event); } - const runtimeContext = buildRuntimeContext(repo); + const runtimeContext = buildRuntimeContext(ctx.repo); return ` *********************************************** ************* SYSTEM INSTRUCTIONS ************* @@ -100475,7 +100483,7 @@ 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. -${getShellInstructions(tools.bash)} +${getShellInstructions(ctx.tools.bash)} **Command execution**: Never use \`sleep\` to wait for commands to complete. Commands run synchronously - when the bash tool returns, the command has finished. @@ -100496,7 +100504,7 @@ ${getShellInstructions(tools.bash)} ### Available modes -${[...getModes({ disableProgressComment: payload.disableProgressComment }), ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")} +${[...getModes({ disableProgressComment: ctx.payload.disableProgressComment }), ...ctx.payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")} ### Following the mode instructions @@ -100506,7 +100514,7 @@ Eagerly inspect the MCP tools available to you via the \`${ghPullfrogMcpName}\` ************* USER PROMPT ************* -${payload.prompt.split("\n").map((line) => `> ${line}`).join("\n")} +${ctx.payload.prompt.split("\n").map((line) => `> ${line}`).join("\n")} ${encodedEvent ? `************* EVENT DATA ************* @@ -104635,12 +104643,12 @@ var claudeEffortModels = { auto: "opusplan", max: "opus" }; -function buildDisallowedTools(tools) { +function buildDisallowedTools(ctx) { const disallowed = []; - if (tools.web === "disabled") disallowed.push("WebFetch"); - if (tools.search === "disabled") disallowed.push("WebSearch"); - if (tools.write === "disabled") disallowed.push("Write"); - if (tools.bash !== "enabled") disallowed.push("Bash"); + if (ctx.tools.web === "disabled") disallowed.push("WebFetch"); + if (ctx.tools.search === "disabled") disallowed.push("WebSearch"); + if (ctx.tools.write === "disabled") disallowed.push("Write"); + if (ctx.tools.bash !== "enabled") disallowed.push("Bash"); return disallowed; } var claude = agent({ @@ -104653,23 +104661,23 @@ var claude = agent({ executablePath: "cli.js" }); }, - run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort, tools }) => { + run: async (ctx) => { delete process.env.ANTHROPIC_API_KEY; - const prompt = addInstructions({ payload, repo, tools }); + const prompt = addInstructions(ctx); log.group("Full prompt", () => log.info(prompt)); - const model = claudeEffortModels[effort]; - log.info(`Using model: ${model} (effort: ${effort})`); - const disallowedTools = buildDisallowedTools(tools); + const model = claudeEffortModels[ctx.effort]; + log.info(`Using model: ${model} (effort: ${ctx.effort})`); + const disallowedTools = buildDisallowedTools(ctx); if (disallowedTools.length > 0) { log.info(`\u{1F512} disallowed tools: ${disallowedTools.join(", ")}`); } const queryOptions = { permissionMode: "bypassPermissions", disallowedTools, - mcpServers, + mcpServers: ctx.mcpServers, model, - pathToClaudeCodeExecutable: cliPath, - env: createAgentEnv({ ANTHROPIC_API_KEY: apiKey }) + pathToClaudeCodeExecutable: ctx.cliPath, + env: createAgentEnv({ ANTHROPIC_API_KEY: ctx.apiKey }) }; const queryInstance = query({ prompt, @@ -105132,19 +105140,20 @@ var codexReasoningEffort = { // use default max: "high" }; -function writeCodexConfig({ tempHome, mcpServers, tools }) { +function writeCodexConfig(ctx) { + const tempHome = process.env.PULLFROG_TEMP_DIR; const codexDir = join6(tempHome, ".codex"); mkdirSync3(codexDir, { recursive: true }); const configPath = join6(codexDir, "config.toml"); const mcpServerSections = []; - for (const [name, config4] of Object.entries(mcpServers)) { + for (const [name, config4] of Object.entries(ctx.mcpServers)) { if (config4.type !== "http") continue; log.info(`\xBB adding MCP server '${name}' at ${config4.url}`); mcpServerSections.push(`[mcp_servers.${name}] url = "${config4.url}"`); } const features = []; - if (tools.bash !== "enabled") { + if (ctx.tools.bash !== "enabled") { features.push("shell_command_tool = false"); features.push("unified_exec = false"); } @@ -105159,7 +105168,7 @@ ${mcpServerSections.join("\n\n")} `.trim() + "\n" ); log.info( - `\xBB Codex config written to ${configPath} (shell: ${tools.bash === "enabled" ? "enabled" : "disabled"})` + `\xBB Codex config written to ${configPath} (shell: ${ctx.tools.bash === "enabled" ? "enabled" : "disabled"})` ); return codexDir; } @@ -105172,41 +105181,37 @@ var codex = agent({ executablePath: "bin/codex.js" }); }, - run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort, tools }) => { + run: async (ctx) => { const tempHome = process.env.PULLFROG_TEMP_DIR; const configDir = join6(tempHome, ".config", "codex"); mkdirSync3(configDir, { recursive: true }); - const codexDir = writeCodexConfig({ - tempHome, - mcpServers, - tools - }); + const codexDir = writeCodexConfig(ctx); setupProcessAgentEnv({ - OPENAI_API_KEY: apiKey, + OPENAI_API_KEY: ctx.apiKey, HOME: tempHome, CODEX_HOME: codexDir // point Codex to our config directory }); - const model = codexModel[effort]; - const modelReasoningEffort = codexReasoningEffort[effort]; + const model = codexModel[ctx.effort]; + const modelReasoningEffort = codexReasoningEffort[ctx.effort]; log.info(`Using model: ${model}`); if (modelReasoningEffort) { log.info(`Using modelReasoningEffort: ${modelReasoningEffort}`); } const codexOptions = { - apiKey, - codexPathOverride: cliPath + apiKey: ctx.apiKey, + codexPathOverride: ctx.cliPath }; const codex2 = new Codex(codexOptions); const threadOptions = { model, approvalPolicy: "never", // write: "disabled" โ†’ read-only sandbox, otherwise full access for git ops - sandboxMode: tools.write === "disabled" ? "read-only" : "danger-full-access", + sandboxMode: ctx.tools.write === "disabled" ? "read-only" : "danger-full-access", // web: controls network access - networkAccessEnabled: tools.web !== "disabled", + networkAccessEnabled: ctx.tools.web !== "disabled", // search: controls web search - webSearchEnabled: tools.search !== "disabled", + webSearchEnabled: ctx.tools.search !== "disabled", ...modelReasoningEffort && { modelReasoningEffort } }; log.info( @@ -105214,7 +105219,7 @@ var codex = agent({ ); const thread = codex2.startThread(threadOptions); try { - const streamedTurn = await thread.runStreamed(addInstructions({ payload, repo, tools })); + const streamedTurn = await thread.runStreamed(addInstructions(ctx)); let finalOutput2 = ""; for await (const event of streamedTurn.events) { const handler2 = messageHandlers2[event.type]; @@ -105341,9 +105346,9 @@ var cursor = agent({ executableName: "cursor-agent" }); }, - run: async ({ payload, apiKey, cliPath, mcpServers, repo, effort, tools }) => { - configureCursorMcpServers({ mcpServers, cliPath }); - configureCursorTools({ tools }); + run: async (ctx) => { + configureCursorMcpServers(ctx); + configureCursorTools(ctx); const projectCliConfigPath = join7(process.cwd(), ".cursor", "cli.json"); let modelOverride = null; if (existsSync4(projectCliConfigPath)) { @@ -105352,18 +105357,18 @@ var cursor = agent({ if (projectConfig.model) { log.info(`Using model from project .cursor/cli.json: ${projectConfig.model}`); } else { - modelOverride = cursorEffortModels[effort]; + modelOverride = cursorEffortModels[ctx.effort]; } } catch { - modelOverride = cursorEffortModels[effort]; + modelOverride = cursorEffortModels[ctx.effort]; } } else { - modelOverride = cursorEffortModels[effort]; + modelOverride = cursorEffortModels[ctx.effort]; } if (modelOverride) { - log.info(`Using model: ${modelOverride} (effort: ${effort})`); + log.info(`Using model: ${modelOverride} (effort: ${ctx.effort})`); } else if (!existsSync4(projectCliConfigPath)) { - log.info(`Using default model (effort: ${effort})`); + log.info(`Using default model (effort: ${ctx.effort})`); } const loggedModelCallIds = /* @__PURE__ */ new Set(); const messageHandlers5 = { @@ -105415,7 +105420,7 @@ var cursor = agent({ } }; try { - const fullPrompt = addInstructions({ payload, repo, tools }); + const fullPrompt = addInstructions(ctx); log.group("Full prompt", () => log.info(fullPrompt)); const baseArgs = ["--print", fullPrompt, "--output-format", "stream-json", "--approve-mcps"]; if (modelOverride) { @@ -105425,10 +105430,10 @@ var cursor = agent({ log.info("Running Cursor CLI..."); const startTime = Date.now(); return new Promise((resolve2) => { - const child = spawn3(cliPath, cursorArgs, { + const child = spawn3(ctx.cliPath, cursorArgs, { cwd: process.cwd(), env: createAgentEnv({ - CURSOR_API_KEY: apiKey + CURSOR_API_KEY: ctx.apiKey }), stdio: ["ignore", "pipe", "pipe"] // Ignore stdin, pipe stdout/stderr @@ -105503,13 +105508,13 @@ var cursor = agent({ } } }); -function configureCursorMcpServers({ mcpServers }) { +function configureCursorMcpServers(ctx) { const realHome = homedir2(); const cursorConfigDir = join7(realHome, ".cursor"); const mcpConfigPath = join7(cursorConfigDir, "mcp.json"); mkdirSync4(cursorConfigDir, { recursive: true }); const cursorMcpServers = {}; - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) { if (serverConfig.type !== "http") { throw new Error( `Unsupported MCP server type for Cursor: ${serverConfig.type || "unknown"}` @@ -105523,33 +105528,29 @@ function configureCursorMcpServers({ mcpServers }) { writeFileSync2(mcpConfigPath, JSON.stringify({ mcpServers: cursorMcpServers }, null, 2), "utf-8"); log.info(`\xBB MCP config written to ${mcpConfigPath}`); } -function configureCursorTools({ tools }) { +function configureCursorTools(ctx) { const realHome = homedir2(); const cursorConfigDir = join7(realHome, ".config", "cursor"); const cliConfigPath = join7(cursorConfigDir, "cli-config.json"); mkdirSync4(cursorConfigDir, { recursive: true }); const deny = []; - if (tools.search === "disabled") deny.push("WebSearch"); - if (tools.write === "disabled") deny.push("Write(**)"); - if (tools.bash !== "enabled") deny.push("Shell(*)"); - const needsSandbox = tools.web === "disabled"; + if (ctx.tools.search === "disabled") deny.push("WebSearch"); + if (ctx.tools.write === "disabled") deny.push("Write(**)"); + if (ctx.tools.bash !== "enabled") deny.push("Shell(*)"); const config4 = { permissions: { - allow: tools.write === "disabled" ? ["Read(**)"] : ["Read(**)", "Write(**)"], + allow: ctx.tools.write === "disabled" ? ["Read(**)"] : ["Read(**)", "Write(**)"], deny } }; - if (needsSandbox) { + if (ctx.tools.web === "disabled") { config4.sandbox = { mode: "enabled", networkAccess: "allowlist" }; } writeFileSync2(cliConfigPath, JSON.stringify(config4, null, 2), "utf-8"); - log.info(`\xBB CLI config written to ${cliConfigPath}`); - log.info( - `\u{1F527} Cursor permissions: allow=${config4.permissions.allow.join(",")}, deny=${deny.join(",") || "(none)"}, sandbox=${needsSandbox ? "enabled" : "disabled"}` - ); + log.info(`\xBB CLI config written to ${cliConfigPath}`, JSON.stringify(config4, null, 2)); } // agents/gemini.ts @@ -105725,14 +105726,12 @@ var gemini = agent({ ...githubInstallationToken2 && { githubInstallationToken: githubInstallationToken2 } }); }, - run: async ({ payload, apiKey, mcpServers, cliPath, repo, effort, tools }) => { - const { model, thinkingLevel } = geminiEffortConfig[effort]; - log.info(`Using model: ${model}, thinkingLevel: ${thinkingLevel}`); - configureGeminiSettings({ mcpServers, tools, thinkingLevel }); - if (!apiKey) { + run: async (ctx) => { + const model = configureGeminiSettings(ctx); + if (!ctx.apiKey) { throw new Error("google_api_key or gemini_api_key is required for gemini agent"); } - const sessionPrompt = addInstructions({ payload, repo, tools }); + const sessionPrompt = addInstructions(ctx); log.group("Full prompt", () => log.info(sessionPrompt)); const args3 = ["--model", model, "--yolo", "--output-format=stream-json", "-p", sessionPrompt]; let finalOutput2 = ""; @@ -105740,8 +105739,8 @@ var gemini = agent({ try { const result = await spawn4({ cmd: "node", - args: [cliPath, ...args3], - env: createAgentEnv({ GEMINI_API_KEY: apiKey }), + args: [ctx.cliPath, ...args3], + env: createAgentEnv({ GEMINI_API_KEY: ctx.apiKey }), onStdout: async (chunk) => { const text = chunk.toString(); finalOutput2 += text; @@ -105798,11 +105797,9 @@ var gemini = agent({ } } }); -function configureGeminiSettings({ - mcpServers, - tools, - thinkingLevel -}) { +function configureGeminiSettings(ctx) { + const { model, thinkingLevel } = geminiEffortConfig[ctx.effort]; + log.info(`Using model: ${model}, thinkingLevel: ${thinkingLevel}`); const realHome = homedir3(); const geminiConfigDir = join8(realHome, ".gemini"); const settingsPath = join8(geminiConfigDir, "settings.json"); @@ -105814,7 +105811,7 @@ function configureGeminiSettings({ } catch { } const geminiMcpServers = {}; - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) { if (serverConfig.type !== "http") { throw new Error( `Unsupported MCP server type for Gemini: ${serverConfig.type || "unknown"}` @@ -105828,10 +105825,10 @@ function configureGeminiSettings({ log.info(`adding MCP server '${serverName}' at ${serverConfig.url}...`); } const exclude = []; - if (tools.bash !== "enabled") exclude.push("run_shell_command"); - if (tools.write === "disabled") exclude.push("write_file"); - if (tools.web === "disabled") exclude.push("web_fetch"); - if (tools.search === "disabled") exclude.push("google_web_search"); + if (ctx.tools.bash !== "enabled") exclude.push("run_shell_command"); + if (ctx.tools.write === "disabled") exclude.push("write_file"); + if (ctx.tools.web === "disabled") exclude.push("web_fetch"); + if (ctx.tools.search === "disabled") exclude.push("google_web_search"); const newSettings = { ...existingSettings, mcpServers: geminiMcpServers, @@ -105852,6 +105849,7 @@ function configureGeminiSettings({ if (exclude.length > 0) { log.info(`\u{1F512} excluded tools: ${exclude.join(", ")}`); } + return model; } // agents/opencode.ts @@ -105867,21 +105865,12 @@ var opencode = agent({ installDependencies: true }); }, - run: async ({ - payload, - apiKey: _apiKey, - apiKeys, - mcpServers, - cliPath, - repo, - effort: _effort, - tools - }) => { + run: async (ctx) => { const tempHome = process.env.PULLFROG_TEMP_DIR; const configDir = join9(tempHome, ".config", "opencode"); mkdirSync6(configDir, { recursive: true }); - configureOpenCode({ mcpServers, tools }); - const prompt = addInstructions({ payload, repo, tools }); + configureOpenCode(ctx); + const prompt = addInstructions(ctx); log.group("Full prompt", () => log.info(prompt)); const args3 = ["run", prompt, "--format", "json"]; setupProcessAgentEnv({ HOME: tempHome }); @@ -105890,14 +105879,14 @@ var opencode = agent({ XDG_CONFIG_HOME: join9(tempHome, ".config") }; delete env3.GITHUB_TOKEN; - for (const [key, value2] of Object.entries(apiKeys || {})) { + for (const [key, value2] of Object.entries(ctx.apiKeys || {})) { env3[key.toUpperCase()] = value2; if (key === "GEMINI_API_KEY") { env3.GOOGLE_GENERATIVE_AI_API_KEY = value2; } } const repoDir = process.cwd(); - log.info(`\u{1F680} Starting OpenCode CLI: ${cliPath} ${args3.join(" ")}`); + log.info(`\u{1F680} Starting OpenCode CLI: ${ctx.cliPath} ${args3.join(" ")}`); log.info(`\u{1F4C1} Working directory: ${repoDir}`); log.debug(`\u{1F3E0} HOME: ${env3.HOME}`); log.debug(`\u{1F4CB} XDG_CONFIG_HOME: ${env3.XDG_CONFIG_HOME}`); @@ -105907,7 +105896,7 @@ var opencode = agent({ let output = ""; let stdoutBuffer = ""; const result = await spawn4({ - cmd: cliPath, + cmd: ctx.cliPath, args: args3, cwd: repoDir, env: env3, @@ -105993,13 +105982,13 @@ var opencode = agent({ }; } }); -function configureOpenCode({ mcpServers, tools }) { +function configureOpenCode(ctx) { const tempHome = process.env.PULLFROG_TEMP_DIR; const configDir = join9(tempHome, ".config", "opencode"); mkdirSync6(configDir, { recursive: true }); const configPath = join9(configDir, "opencode.json"); const opencodeMcpServers = {}; - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) { if (serverConfig.type !== "http") { log.error( `unsupported MCP server type for OpenCode: ${serverConfig.type || "unknown"}` @@ -106014,9 +106003,9 @@ function configureOpenCode({ mcpServers, tools }) { }; } const permission = { - edit: tools.write === "disabled" ? "deny" : "allow", - bash: tools.bash !== "enabled" ? "deny" : "allow", - webfetch: tools.web === "disabled" ? "deny" : "allow", + edit: ctx.tools.write === "disabled" ? "deny" : "allow", + bash: ctx.tools.bash !== "enabled" ? "deny" : "allow", + webfetch: ctx.tools.web === "disabled" ? "deny" : "allow", doom_loop: "allow", external_directory: "allow" }; diff --git a/get-installation-token/entry b/get-installation-token/entry index eedf7cf..8ea265e 100755 --- a/get-installation-token/entry +++ b/get-installation-token/entry @@ -25505,11 +25505,19 @@ var core3 = __toESM(require_core(), 1); var core2 = __toESM(require_core(), 1); import { createSign } from "node:crypto"; -// utils/cli.ts +// utils/log.ts var core = __toESM(require_core(), 1); var import_table = __toESM(require_src(), 1); var isGitHubActions = !!process.env.GITHUB_ACTIONS; var isDebugEnabled = () => process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true" || process.env.RUNNER_DEBUG === "1" || core.isDebug(); +function formatArgs(args) { + return args.map((arg) => { + if (typeof arg === "string") return arg; + if (arg instanceof Error) return `${arg.message} +${arg.stack}`; + return JSON.stringify(arg); + }).join(" "); +} function startGroup2(name) { if (isGitHubActions) { core.startGroup(name); @@ -25614,25 +25622,25 @@ function separator(length = 50) { } var log = { /** Print info message */ - info: (message) => { - core.info(message); + info: (...args) => { + core.info(formatArgs(args)); }, /** Print warning message */ - warning: (message) => { - core.warning(message); + warning: (...args) => { + core.warning(formatArgs(args)); }, /** Print error message */ - error: (message) => { - core.error(message); + error: (...args) => { + core.error(formatArgs(args)); }, /** Print success message */ - success: (message) => { - core.info(`\u2705 ${message}`); + success: (...args) => { + core.info(`\u2705 ${formatArgs(args)}`); }, /** Print debug message (only if LOG_LEVEL=debug) */ - debug: (message) => { + debug: (...args) => { if (isDebugEnabled()) { - core.info(`[DEBUG] ${message}`); + core.info(`[DEBUG] ${formatArgs(args)}`); } }, /** Print a formatted box with text */ diff --git a/run/entry b/run/entry index 2ba2370..04722d2 100755 --- a/run/entry +++ b/run/entry @@ -100032,11 +100032,19 @@ var package_default = { packageManager: "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a" }; -// utils/cli.ts +// utils/log.ts var core = __toESM(require_core(), 1); var import_table = __toESM(require_src(), 1); var isGitHubActions = !!process.env.GITHUB_ACTIONS; var isDebugEnabled = () => process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true" || process.env.RUNNER_DEBUG === "1" || core.isDebug(); +function formatArgs(args3) { + return args3.map((arg) => { + if (typeof arg === "string") return arg; + if (arg instanceof Error) return `${arg.message} +${arg.stack}`; + return JSON.stringify(arg); + }).join(" "); +} function startGroup2(name) { if (isGitHubActions) { core.startGroup(name); @@ -100145,25 +100153,25 @@ function separator(length = 50) { } var log = { /** Print info message */ - info: (message) => { - core.info(message); + info: (...args3) => { + core.info(formatArgs(args3)); }, /** Print warning message */ - warning: (message) => { - core.warning(message); + warning: (...args3) => { + core.warning(formatArgs(args3)); }, /** Print error message */ - error: (message) => { - core.error(message); + error: (...args3) => { + core.error(formatArgs(args3)); }, /** Print success message */ - success: (message) => { - core.info(`\u2705 ${message}`); + success: (...args3) => { + core.info(`\u2705 ${formatArgs(args3)}`); }, /** Print debug message (only if LOG_LEVEL=debug) */ - debug: (message) => { + debug: (...args3) => { if (isDebugEnabled()) { - core.info(`[DEBUG] ${message}`); + core.info(`[DEBUG] ${formatArgs(args3)}`); } }, /** Print a formatted box with text */ @@ -100420,14 +100428,14 @@ function getShellInstructions(bash) { } } } -var addInstructions = ({ payload, repo, tools }) => { +var addInstructions = (ctx) => { let encodedEvent = ""; - const eventKeys = Object.keys(payload.event); + const eventKeys = Object.keys(ctx.payload.event); if (eventKeys.length === 1 && eventKeys[0] === "trigger") { } else { - encodedEvent = encode(payload.event); + encodedEvent = encode(ctx.payload.event); } - const runtimeContext = buildRuntimeContext(repo); + const runtimeContext = buildRuntimeContext(ctx.repo); return ` *********************************************** ************* SYSTEM INSTRUCTIONS ************* @@ -100475,7 +100483,7 @@ 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. -${getShellInstructions(tools.bash)} +${getShellInstructions(ctx.tools.bash)} **Command execution**: Never use \`sleep\` to wait for commands to complete. Commands run synchronously - when the bash tool returns, the command has finished. @@ -100496,7 +100504,7 @@ ${getShellInstructions(tools.bash)} ### Available modes -${[...getModes({ disableProgressComment: payload.disableProgressComment }), ...payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")} +${[...getModes({ disableProgressComment: ctx.payload.disableProgressComment }), ...ctx.payload.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")} ### Following the mode instructions @@ -100506,7 +100514,7 @@ Eagerly inspect the MCP tools available to you via the \`${ghPullfrogMcpName}\` ************* USER PROMPT ************* -${payload.prompt.split("\n").map((line) => `> ${line}`).join("\n")} +${ctx.payload.prompt.split("\n").map((line) => `> ${line}`).join("\n")} ${encodedEvent ? `************* EVENT DATA ************* @@ -104635,12 +104643,12 @@ var claudeEffortModels = { auto: "opusplan", max: "opus" }; -function buildDisallowedTools(tools) { +function buildDisallowedTools(ctx) { const disallowed = []; - if (tools.web === "disabled") disallowed.push("WebFetch"); - if (tools.search === "disabled") disallowed.push("WebSearch"); - if (tools.write === "disabled") disallowed.push("Write"); - if (tools.bash !== "enabled") disallowed.push("Bash"); + if (ctx.tools.web === "disabled") disallowed.push("WebFetch"); + if (ctx.tools.search === "disabled") disallowed.push("WebSearch"); + if (ctx.tools.write === "disabled") disallowed.push("Write"); + if (ctx.tools.bash !== "enabled") disallowed.push("Bash"); return disallowed; } var claude = agent({ @@ -104653,23 +104661,23 @@ var claude = agent({ executablePath: "cli.js" }); }, - run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort, tools }) => { + run: async (ctx) => { delete process.env.ANTHROPIC_API_KEY; - const prompt = addInstructions({ payload, repo, tools }); + const prompt = addInstructions(ctx); log.group("Full prompt", () => log.info(prompt)); - const model = claudeEffortModels[effort]; - log.info(`Using model: ${model} (effort: ${effort})`); - const disallowedTools = buildDisallowedTools(tools); + const model = claudeEffortModels[ctx.effort]; + log.info(`Using model: ${model} (effort: ${ctx.effort})`); + const disallowedTools = buildDisallowedTools(ctx); if (disallowedTools.length > 0) { log.info(`\u{1F512} disallowed tools: ${disallowedTools.join(", ")}`); } const queryOptions = { permissionMode: "bypassPermissions", disallowedTools, - mcpServers, + mcpServers: ctx.mcpServers, model, - pathToClaudeCodeExecutable: cliPath, - env: createAgentEnv({ ANTHROPIC_API_KEY: apiKey }) + pathToClaudeCodeExecutable: ctx.cliPath, + env: createAgentEnv({ ANTHROPIC_API_KEY: ctx.apiKey }) }; const queryInstance = query({ prompt, @@ -105132,19 +105140,20 @@ var codexReasoningEffort = { // use default max: "high" }; -function writeCodexConfig({ tempHome, mcpServers, tools }) { +function writeCodexConfig(ctx) { + const tempHome = process.env.PULLFROG_TEMP_DIR; const codexDir = join6(tempHome, ".codex"); mkdirSync3(codexDir, { recursive: true }); const configPath = join6(codexDir, "config.toml"); const mcpServerSections = []; - for (const [name, config4] of Object.entries(mcpServers)) { + for (const [name, config4] of Object.entries(ctx.mcpServers)) { if (config4.type !== "http") continue; log.info(`\xBB adding MCP server '${name}' at ${config4.url}`); mcpServerSections.push(`[mcp_servers.${name}] url = "${config4.url}"`); } const features = []; - if (tools.bash !== "enabled") { + if (ctx.tools.bash !== "enabled") { features.push("shell_command_tool = false"); features.push("unified_exec = false"); } @@ -105159,7 +105168,7 @@ ${mcpServerSections.join("\n\n")} `.trim() + "\n" ); log.info( - `\xBB Codex config written to ${configPath} (shell: ${tools.bash === "enabled" ? "enabled" : "disabled"})` + `\xBB Codex config written to ${configPath} (shell: ${ctx.tools.bash === "enabled" ? "enabled" : "disabled"})` ); return codexDir; } @@ -105172,41 +105181,37 @@ var codex = agent({ executablePath: "bin/codex.js" }); }, - run: async ({ payload, mcpServers, apiKey, cliPath, repo, effort, tools }) => { + run: async (ctx) => { const tempHome = process.env.PULLFROG_TEMP_DIR; const configDir = join6(tempHome, ".config", "codex"); mkdirSync3(configDir, { recursive: true }); - const codexDir = writeCodexConfig({ - tempHome, - mcpServers, - tools - }); + const codexDir = writeCodexConfig(ctx); setupProcessAgentEnv({ - OPENAI_API_KEY: apiKey, + OPENAI_API_KEY: ctx.apiKey, HOME: tempHome, CODEX_HOME: codexDir // point Codex to our config directory }); - const model = codexModel[effort]; - const modelReasoningEffort = codexReasoningEffort[effort]; + const model = codexModel[ctx.effort]; + const modelReasoningEffort = codexReasoningEffort[ctx.effort]; log.info(`Using model: ${model}`); if (modelReasoningEffort) { log.info(`Using modelReasoningEffort: ${modelReasoningEffort}`); } const codexOptions = { - apiKey, - codexPathOverride: cliPath + apiKey: ctx.apiKey, + codexPathOverride: ctx.cliPath }; const codex2 = new Codex(codexOptions); const threadOptions = { model, approvalPolicy: "never", // write: "disabled" โ†’ read-only sandbox, otherwise full access for git ops - sandboxMode: tools.write === "disabled" ? "read-only" : "danger-full-access", + sandboxMode: ctx.tools.write === "disabled" ? "read-only" : "danger-full-access", // web: controls network access - networkAccessEnabled: tools.web !== "disabled", + networkAccessEnabled: ctx.tools.web !== "disabled", // search: controls web search - webSearchEnabled: tools.search !== "disabled", + webSearchEnabled: ctx.tools.search !== "disabled", ...modelReasoningEffort && { modelReasoningEffort } }; log.info( @@ -105214,7 +105219,7 @@ var codex = agent({ ); const thread = codex2.startThread(threadOptions); try { - const streamedTurn = await thread.runStreamed(addInstructions({ payload, repo, tools })); + const streamedTurn = await thread.runStreamed(addInstructions(ctx)); let finalOutput2 = ""; for await (const event of streamedTurn.events) { const handler2 = messageHandlers2[event.type]; @@ -105341,9 +105346,9 @@ var cursor = agent({ executableName: "cursor-agent" }); }, - run: async ({ payload, apiKey, cliPath, mcpServers, repo, effort, tools }) => { - configureCursorMcpServers({ mcpServers, cliPath }); - configureCursorTools({ tools }); + run: async (ctx) => { + configureCursorMcpServers(ctx); + configureCursorTools(ctx); const projectCliConfigPath = join7(process.cwd(), ".cursor", "cli.json"); let modelOverride = null; if (existsSync4(projectCliConfigPath)) { @@ -105352,18 +105357,18 @@ var cursor = agent({ if (projectConfig.model) { log.info(`Using model from project .cursor/cli.json: ${projectConfig.model}`); } else { - modelOverride = cursorEffortModels[effort]; + modelOverride = cursorEffortModels[ctx.effort]; } } catch { - modelOverride = cursorEffortModels[effort]; + modelOverride = cursorEffortModels[ctx.effort]; } } else { - modelOverride = cursorEffortModels[effort]; + modelOverride = cursorEffortModels[ctx.effort]; } if (modelOverride) { - log.info(`Using model: ${modelOverride} (effort: ${effort})`); + log.info(`Using model: ${modelOverride} (effort: ${ctx.effort})`); } else if (!existsSync4(projectCliConfigPath)) { - log.info(`Using default model (effort: ${effort})`); + log.info(`Using default model (effort: ${ctx.effort})`); } const loggedModelCallIds = /* @__PURE__ */ new Set(); const messageHandlers5 = { @@ -105415,7 +105420,7 @@ var cursor = agent({ } }; try { - const fullPrompt = addInstructions({ payload, repo, tools }); + const fullPrompt = addInstructions(ctx); log.group("Full prompt", () => log.info(fullPrompt)); const baseArgs = ["--print", fullPrompt, "--output-format", "stream-json", "--approve-mcps"]; if (modelOverride) { @@ -105425,10 +105430,10 @@ var cursor = agent({ log.info("Running Cursor CLI..."); const startTime = Date.now(); return new Promise((resolve2) => { - const child = spawn3(cliPath, cursorArgs, { + const child = spawn3(ctx.cliPath, cursorArgs, { cwd: process.cwd(), env: createAgentEnv({ - CURSOR_API_KEY: apiKey + CURSOR_API_KEY: ctx.apiKey }), stdio: ["ignore", "pipe", "pipe"] // Ignore stdin, pipe stdout/stderr @@ -105503,13 +105508,13 @@ var cursor = agent({ } } }); -function configureCursorMcpServers({ mcpServers }) { +function configureCursorMcpServers(ctx) { const realHome = homedir2(); const cursorConfigDir = join7(realHome, ".cursor"); const mcpConfigPath = join7(cursorConfigDir, "mcp.json"); mkdirSync4(cursorConfigDir, { recursive: true }); const cursorMcpServers = {}; - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) { if (serverConfig.type !== "http") { throw new Error( `Unsupported MCP server type for Cursor: ${serverConfig.type || "unknown"}` @@ -105523,33 +105528,29 @@ function configureCursorMcpServers({ mcpServers }) { writeFileSync2(mcpConfigPath, JSON.stringify({ mcpServers: cursorMcpServers }, null, 2), "utf-8"); log.info(`\xBB MCP config written to ${mcpConfigPath}`); } -function configureCursorTools({ tools }) { +function configureCursorTools(ctx) { const realHome = homedir2(); const cursorConfigDir = join7(realHome, ".config", "cursor"); const cliConfigPath = join7(cursorConfigDir, "cli-config.json"); mkdirSync4(cursorConfigDir, { recursive: true }); const deny = []; - if (tools.search === "disabled") deny.push("WebSearch"); - if (tools.write === "disabled") deny.push("Write(**)"); - if (tools.bash !== "enabled") deny.push("Shell(*)"); - const needsSandbox = tools.web === "disabled"; + if (ctx.tools.search === "disabled") deny.push("WebSearch"); + if (ctx.tools.write === "disabled") deny.push("Write(**)"); + if (ctx.tools.bash !== "enabled") deny.push("Shell(*)"); const config4 = { permissions: { - allow: tools.write === "disabled" ? ["Read(**)"] : ["Read(**)", "Write(**)"], + allow: ctx.tools.write === "disabled" ? ["Read(**)"] : ["Read(**)", "Write(**)"], deny } }; - if (needsSandbox) { + if (ctx.tools.web === "disabled") { config4.sandbox = { mode: "enabled", networkAccess: "allowlist" }; } writeFileSync2(cliConfigPath, JSON.stringify(config4, null, 2), "utf-8"); - log.info(`\xBB CLI config written to ${cliConfigPath}`); - log.info( - `\u{1F527} Cursor permissions: allow=${config4.permissions.allow.join(",")}, deny=${deny.join(",") || "(none)"}, sandbox=${needsSandbox ? "enabled" : "disabled"}` - ); + log.info(`\xBB CLI config written to ${cliConfigPath}`, JSON.stringify(config4, null, 2)); } // agents/gemini.ts @@ -105725,14 +105726,12 @@ var gemini = agent({ ...githubInstallationToken2 && { githubInstallationToken: githubInstallationToken2 } }); }, - run: async ({ payload, apiKey, mcpServers, cliPath, repo, effort, tools }) => { - const { model, thinkingLevel } = geminiEffortConfig[effort]; - log.info(`Using model: ${model}, thinkingLevel: ${thinkingLevel}`); - configureGeminiSettings({ mcpServers, tools, thinkingLevel }); - if (!apiKey) { + run: async (ctx) => { + const model = configureGeminiSettings(ctx); + if (!ctx.apiKey) { throw new Error("google_api_key or gemini_api_key is required for gemini agent"); } - const sessionPrompt = addInstructions({ payload, repo, tools }); + const sessionPrompt = addInstructions(ctx); log.group("Full prompt", () => log.info(sessionPrompt)); const args3 = ["--model", model, "--yolo", "--output-format=stream-json", "-p", sessionPrompt]; let finalOutput2 = ""; @@ -105740,8 +105739,8 @@ var gemini = agent({ try { const result = await spawn4({ cmd: "node", - args: [cliPath, ...args3], - env: createAgentEnv({ GEMINI_API_KEY: apiKey }), + args: [ctx.cliPath, ...args3], + env: createAgentEnv({ GEMINI_API_KEY: ctx.apiKey }), onStdout: async (chunk) => { const text = chunk.toString(); finalOutput2 += text; @@ -105798,11 +105797,9 @@ var gemini = agent({ } } }); -function configureGeminiSettings({ - mcpServers, - tools, - thinkingLevel -}) { +function configureGeminiSettings(ctx) { + const { model, thinkingLevel } = geminiEffortConfig[ctx.effort]; + log.info(`Using model: ${model}, thinkingLevel: ${thinkingLevel}`); const realHome = homedir3(); const geminiConfigDir = join8(realHome, ".gemini"); const settingsPath = join8(geminiConfigDir, "settings.json"); @@ -105814,7 +105811,7 @@ function configureGeminiSettings({ } catch { } const geminiMcpServers = {}; - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) { if (serverConfig.type !== "http") { throw new Error( `Unsupported MCP server type for Gemini: ${serverConfig.type || "unknown"}` @@ -105828,10 +105825,10 @@ function configureGeminiSettings({ log.info(`adding MCP server '${serverName}' at ${serverConfig.url}...`); } const exclude = []; - if (tools.bash !== "enabled") exclude.push("run_shell_command"); - if (tools.write === "disabled") exclude.push("write_file"); - if (tools.web === "disabled") exclude.push("web_fetch"); - if (tools.search === "disabled") exclude.push("google_web_search"); + if (ctx.tools.bash !== "enabled") exclude.push("run_shell_command"); + if (ctx.tools.write === "disabled") exclude.push("write_file"); + if (ctx.tools.web === "disabled") exclude.push("web_fetch"); + if (ctx.tools.search === "disabled") exclude.push("google_web_search"); const newSettings = { ...existingSettings, mcpServers: geminiMcpServers, @@ -105852,6 +105849,7 @@ function configureGeminiSettings({ if (exclude.length > 0) { log.info(`\u{1F512} excluded tools: ${exclude.join(", ")}`); } + return model; } // agents/opencode.ts @@ -105867,21 +105865,12 @@ var opencode = agent({ installDependencies: true }); }, - run: async ({ - payload, - apiKey: _apiKey, - apiKeys, - mcpServers, - cliPath, - repo, - effort: _effort, - tools - }) => { + run: async (ctx) => { const tempHome = process.env.PULLFROG_TEMP_DIR; const configDir = join9(tempHome, ".config", "opencode"); mkdirSync6(configDir, { recursive: true }); - configureOpenCode({ mcpServers, tools }); - const prompt = addInstructions({ payload, repo, tools }); + configureOpenCode(ctx); + const prompt = addInstructions(ctx); log.group("Full prompt", () => log.info(prompt)); const args3 = ["run", prompt, "--format", "json"]; setupProcessAgentEnv({ HOME: tempHome }); @@ -105890,14 +105879,14 @@ var opencode = agent({ XDG_CONFIG_HOME: join9(tempHome, ".config") }; delete env3.GITHUB_TOKEN; - for (const [key, value2] of Object.entries(apiKeys || {})) { + for (const [key, value2] of Object.entries(ctx.apiKeys || {})) { env3[key.toUpperCase()] = value2; if (key === "GEMINI_API_KEY") { env3.GOOGLE_GENERATIVE_AI_API_KEY = value2; } } const repoDir = process.cwd(); - log.info(`\u{1F680} Starting OpenCode CLI: ${cliPath} ${args3.join(" ")}`); + log.info(`\u{1F680} Starting OpenCode CLI: ${ctx.cliPath} ${args3.join(" ")}`); log.info(`\u{1F4C1} Working directory: ${repoDir}`); log.debug(`\u{1F3E0} HOME: ${env3.HOME}`); log.debug(`\u{1F4CB} XDG_CONFIG_HOME: ${env3.XDG_CONFIG_HOME}`); @@ -105907,7 +105896,7 @@ var opencode = agent({ let output = ""; let stdoutBuffer = ""; const result = await spawn4({ - cmd: cliPath, + cmd: ctx.cliPath, args: args3, cwd: repoDir, env: env3, @@ -105993,13 +105982,13 @@ var opencode = agent({ }; } }); -function configureOpenCode({ mcpServers, tools }) { +function configureOpenCode(ctx) { const tempHome = process.env.PULLFROG_TEMP_DIR; const configDir = join9(tempHome, ".config", "opencode"); mkdirSync6(configDir, { recursive: true }); const configPath = join9(configDir, "opencode.json"); const opencodeMcpServers = {}; - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + for (const [serverName, serverConfig] of Object.entries(ctx.mcpServers)) { if (serverConfig.type !== "http") { log.error( `unsupported MCP server type for OpenCode: ${serverConfig.type || "unknown"}` @@ -106014,9 +106003,9 @@ function configureOpenCode({ mcpServers, tools }) { }; } const permission = { - edit: tools.write === "disabled" ? "deny" : "allow", - bash: tools.bash !== "enabled" ? "deny" : "allow", - webfetch: tools.web === "disabled" ? "deny" : "allow", + edit: ctx.tools.write === "disabled" ? "deny" : "allow", + bash: ctx.tools.bash !== "enabled" ? "deny" : "allow", + webfetch: ctx.tools.web === "disabled" ? "deny" : "allow", doom_loop: "allow", external_directory: "allow" }; diff --git a/utils/cli.ts b/utils/cli.ts index e60e710..1b78de1 100644 --- a/utils/cli.ts +++ b/utils/cli.ts @@ -1,275 +1,12 @@ /** - * CLI output utilities that work well in both local and GitHub Actions environments + * CLI utilities */ import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; -import * as core from "@actions/core"; -import { table } from "table"; -const isGitHubActions = !!process.env.GITHUB_ACTIONS; - -const isDebugEnabled = () => - process.env.LOG_LEVEL === "debug" || - process.env.ACTIONS_STEP_DEBUG === "true" || - process.env.RUNNER_DEBUG === "1" || - core.isDebug(); - -/** - * Start a collapsed group (GitHub Actions) or regular group (local) - */ -function startGroup(name: string): void { - if (isGitHubActions) { - core.startGroup(name); - } else { - console.group(name); - } -} - -/** - * End a collapsed group - */ -function endGroup(): void { - if (isGitHubActions) { - core.endGroup(); - } else { - console.groupEnd(); - } -} - -/** - * Run a callback within a collapsed group - */ -function group(name: string, fn: () => void): void { - startGroup(name); - fn(); - endGroup(); -} - -/** - * Print a formatted box with text (for console output) - */ -function boxString( - text: string, - options?: { - title?: string; - maxWidth?: number; - indent?: string; - padding?: number; - } -): string { - const { title, maxWidth = 80, indent = "", padding = 1 } = options || {}; - - const lines = text.trim().split("\n"); - const wrappedLines: string[] = []; - - for (const line of lines) { - if (line.length <= maxWidth - padding * 2) { - wrappedLines.push(line); - } else { - const words = line.split(" "); - let currentLine = ""; - - for (const word of words) { - const testLine = currentLine ? `${currentLine} ${word}` : word; - if (testLine.length <= maxWidth - padding * 2) { - currentLine = testLine; - } else { - if (currentLine) { - wrappedLines.push(currentLine); - currentLine = ""; - } - // wrap long words by breaking them into chunks - const maxLineLength = maxWidth - padding * 2; - let remainingWord = word; - while (remainingWord.length > maxLineLength) { - wrappedLines.push(remainingWord.substring(0, maxLineLength)); - remainingWord = remainingWord.substring(maxLineLength); - } - currentLine = remainingWord; - } - } - - if (currentLine) { - wrappedLines.push(currentLine); - } - } - } - - const maxLineLength = Math.max(...wrappedLines.map((line) => line.length)); - const contentBoxWidth = maxLineLength + padding * 2; - - // ensure box width is at least as wide as the title line when title exists - const titleLineLength = title ? ` ${title} `.length : 0; - const boxWidth = Math.max(contentBoxWidth, titleLineLength); - - let result = ""; - - if (title) { - const titleLine = ` ${title} `; - const titlePadding = Math.max(0, boxWidth - titleLine.length); - result += `${indent}โ”Œ${titleLine}${"โ”€".repeat(titlePadding)}โ”\n`; - } - - if (!title) { - result += `${indent}โ”Œ${"โ”€".repeat(boxWidth)}โ”\n`; - } - - for (const line of wrappedLines) { - const paddedLine = line.padEnd(maxLineLength); - result += `${indent}โ”‚${" ".repeat(padding)}${paddedLine}${" ".repeat(padding)}โ”‚\n`; - } - - result += `${indent}โ””${"โ”€".repeat(boxWidth)}โ”˜`; - - return result; -} - -/** - * Print a formatted box with text - */ -function box( - text: string, - options?: { - title?: string; - maxWidth?: number; - } -): void { - const boxContent = boxString(text, options); - core.info(boxContent); -} - -/** - * Overwrite the job summary with the given text. - */ -export function writeSummary(text: string): void { - if (!isGitHubActions) return; - core.summary.addRaw(text).write({ overwrite: true }); -} - -/** - * Print a formatted table using the table package - */ -function printTable( - rows: Array>, - options?: { - title?: string; - } -): void { - const { title } = options || {}; - - // Convert rows to string arrays for the table package - const tableData = rows.map((row) => - row.map((cell) => { - if (typeof cell === "string") { - return cell; - } - return cell.data; - }) - ); - - const formatted = table(tableData); - - if (title) { - core.info(`\n${title}`); - } - core.info(`\n${formatted}\n`); -} - -/** - * Print a separator line - */ -function separator(length: number = 50): void { - const separatorText = "โ”€".repeat(length); - core.info(separatorText); -} - -/** - * Main logging utility object - import this once and access all utilities - */ -export const log = { - /** Print info message */ - info: (message: string): void => { - core.info(message); - }, - - /** Print warning message */ - warning: (message: string): void => { - core.warning(message); - }, - - /** Print error message */ - error: (message: string): void => { - core.error(message); - }, - - /** Print success message */ - success: (message: string): void => { - core.info(`โœ… ${message}`); - }, - - /** Print debug message (only if LOG_LEVEL=debug) */ - debug: (message: string | unknown): void => { - if (isDebugEnabled()) { - core.info(`[DEBUG] ${message}`); - } - }, - - /** Print a formatted box with text */ - box, - - /** Print a formatted table using the table package */ - table: printTable, - - /** Print a separator line */ - separator, - - /** Start a collapsed group (GitHub Actions) or regular group (local) */ - startGroup, - - /** End a collapsed group */ - endGroup, - - /** Run a callback within a collapsed group */ - group, - - /** Log tool call information to console with formatted output */ - toolCall: ({ toolName, input }: { toolName: string; input: unknown }): void => { - const inputFormatted = formatJsonValue(input); - const timestamp = isDebugEnabled() ? ` [${new Date().toISOString()}]` : ""; - const output = - inputFormatted !== "{}" - ? `โ†’ ${toolName}(${inputFormatted})${timestamp}` - : `โ†’ ${toolName}()${timestamp}`; - - log.info(output.trimEnd()); - }, -}; - -/** - * Format a value as JSON, using compact format for simple values and pretty-printed for complex ones - */ -export function formatJsonValue(value: unknown): string { - const compact = JSON.stringify(value); - return compact.length > 80 || compact.includes("\n") ? JSON.stringify(value, null, 2) : compact; -} - -/** - * Format a multi-line string with proper indentation for tool call output - * First line has the label, subsequent lines are indented 4 spaces - */ -export function formatIndentedField(label: string, content: string): string { - if (!content.includes("\n")) { - return ` ${label}: ${content}\n`; - } - - const lines = content.split("\n"); - let formatted = ` ${label}: ${lines[0]}\n`; - for (let i = 1; i < lines.length; i++) { - formatted += ` ${lines[i]}\n`; - } - return formatted; -} +// re-export logging utilities for backward compatibility +export { formatIndentedField, formatJsonValue, log, writeSummary } from "./log.ts"; /** * Finds a CLI executable path by checking if it's installed globally diff --git a/utils/log.ts b/utils/log.ts new file mode 100644 index 0000000..c394b69 --- /dev/null +++ b/utils/log.ts @@ -0,0 +1,283 @@ +/** + * Logging utilities that work well in both local and GitHub Actions environments + */ + +import * as core from "@actions/core"; +import { table } from "table"; + +const isGitHubActions = !!process.env.GITHUB_ACTIONS; + +const isDebugEnabled = () => + process.env.LOG_LEVEL === "debug" || + process.env.ACTIONS_STEP_DEBUG === "true" || + process.env.RUNNER_DEBUG === "1" || + core.isDebug(); + +/** + * Format arguments into a single string for logging + */ +function formatArgs(args: unknown[]): string { + return args + .map((arg) => { + if (typeof arg === "string") return arg; + if (arg instanceof Error) return `${arg.message}\n${arg.stack}`; + return JSON.stringify(arg); + }) + .join(" "); +} + +/** + * Start a collapsed group (GitHub Actions) or regular group (local) + */ +function startGroup(name: string): void { + if (isGitHubActions) { + core.startGroup(name); + } else { + console.group(name); + } +} + +/** + * End a collapsed group + */ +function endGroup(): void { + if (isGitHubActions) { + core.endGroup(); + } else { + console.groupEnd(); + } +} + +/** + * Run a callback within a collapsed group + */ +function group(name: string, fn: () => void): void { + startGroup(name); + fn(); + endGroup(); +} + +/** + * Print a formatted box with text (for console output) + */ +function boxString( + text: string, + options?: { + title?: string; + maxWidth?: number; + indent?: string; + padding?: number; + } +): string { + const { title, maxWidth = 80, indent = "", padding = 1 } = options || {}; + + const lines = text.trim().split("\n"); + const wrappedLines: string[] = []; + + for (const line of lines) { + if (line.length <= maxWidth - padding * 2) { + wrappedLines.push(line); + } else { + const words = line.split(" "); + let currentLine = ""; + + for (const word of words) { + const testLine = currentLine ? `${currentLine} ${word}` : word; + if (testLine.length <= maxWidth - padding * 2) { + currentLine = testLine; + } else { + if (currentLine) { + wrappedLines.push(currentLine); + currentLine = ""; + } + // wrap long words by breaking them into chunks + const maxLineLength = maxWidth - padding * 2; + let remainingWord = word; + while (remainingWord.length > maxLineLength) { + wrappedLines.push(remainingWord.substring(0, maxLineLength)); + remainingWord = remainingWord.substring(maxLineLength); + } + currentLine = remainingWord; + } + } + + if (currentLine) { + wrappedLines.push(currentLine); + } + } + } + + const maxLineLength = Math.max(...wrappedLines.map((line) => line.length)); + const contentBoxWidth = maxLineLength + padding * 2; + + // ensure box width is at least as wide as the title line when title exists + const titleLineLength = title ? ` ${title} `.length : 0; + const boxWidth = Math.max(contentBoxWidth, titleLineLength); + + let result = ""; + + if (title) { + const titleLine = ` ${title} `; + const titlePadding = Math.max(0, boxWidth - titleLine.length); + result += `${indent}โ”Œ${titleLine}${"โ”€".repeat(titlePadding)}โ”\n`; + } + + if (!title) { + result += `${indent}โ”Œ${"โ”€".repeat(boxWidth)}โ”\n`; + } + + for (const line of wrappedLines) { + const paddedLine = line.padEnd(maxLineLength); + result += `${indent}โ”‚${" ".repeat(padding)}${paddedLine}${" ".repeat(padding)}โ”‚\n`; + } + + result += `${indent}โ””${"โ”€".repeat(boxWidth)}โ”˜`; + + return result; +} + +/** + * Print a formatted box with text + */ +function box( + text: string, + options?: { + title?: string; + maxWidth?: number; + } +): void { + const boxContent = boxString(text, options); + core.info(boxContent); +} + +/** + * Overwrite the job summary with the given text. + */ +export function writeSummary(text: string): void { + if (!isGitHubActions) return; + core.summary.addRaw(text).write({ overwrite: true }); +} + +/** + * Print a formatted table using the table package + */ +function printTable( + rows: Array>, + options?: { + title?: string; + } +): void { + const { title } = options || {}; + + // Convert rows to string arrays for the table package + const tableData = rows.map((row) => + row.map((cell) => { + if (typeof cell === "string") { + return cell; + } + return cell.data; + }) + ); + + const formatted = table(tableData); + + if (title) { + core.info(`\n${title}`); + } + core.info(`\n${formatted}\n`); +} + +/** + * Print a separator line + */ +function separator(length: number = 50): void { + const separatorText = "โ”€".repeat(length); + core.info(separatorText); +} + +/** + * Main logging utility object - import this once and access all utilities + */ +export const log = { + /** Print info message */ + info: (...args: unknown[]): void => { + core.info(formatArgs(args)); + }, + + /** Print warning message */ + warning: (...args: unknown[]): void => { + core.warning(formatArgs(args)); + }, + + /** Print error message */ + error: (...args: unknown[]): void => { + core.error(formatArgs(args)); + }, + + /** Print success message */ + success: (...args: unknown[]): void => { + core.info(`โœ… ${formatArgs(args)}`); + }, + + /** Print debug message (only if LOG_LEVEL=debug) */ + debug: (...args: unknown[]): void => { + if (isDebugEnabled()) { + core.info(`[DEBUG] ${formatArgs(args)}`); + } + }, + + /** Print a formatted box with text */ + box, + + /** Print a formatted table using the table package */ + table: printTable, + + /** Print a separator line */ + separator, + + /** Start a collapsed group (GitHub Actions) or regular group (local) */ + startGroup, + + /** End a collapsed group */ + endGroup, + + /** Run a callback within a collapsed group */ + group, + + /** Log tool call information to console with formatted output */ + toolCall: ({ toolName, input }: { toolName: string; input: unknown }): void => { + const inputFormatted = formatJsonValue(input); + const timestamp = isDebugEnabled() ? ` [${new Date().toISOString()}]` : ""; + const output = + inputFormatted !== "{}" + ? `โ†’ ${toolName}(${inputFormatted})${timestamp}` + : `โ†’ ${toolName}()${timestamp}`; + + log.info(output.trimEnd()); + }, +}; + +/** + * Format a value as JSON, using compact format for simple values and pretty-printed for complex ones + */ +export function formatJsonValue(value: unknown): string { + const compact = JSON.stringify(value); + return compact.length > 80 || compact.includes("\n") ? JSON.stringify(value, null, 2) : compact; +} + +/** + * Format a multi-line string with proper indentation for tool call output + * First line has the label, subsequent lines are indented 4 spaces + */ +export function formatIndentedField(label: string, content: string): string { + if (!content.includes("\n")) { + return ` ${label}: ${content}\n`; + } + + const lines = content.split("\n"); + let formatted = ` ${label}: ${lines[0]}\n`; + for (let i = 1; i < lines.length; i++) { + formatted += ` ${lines[i]}\n`; + } + return formatted; +}