From dbf906a7f097da6c82f0b0d859d158da40328397 Mon Sep 17 00:00:00 2001 From: ssalbdivad Date: Tue, 18 Nov 2025 14:42:07 -0500 Subject: [PATCH] use gemini cli instead of jules, iterate on mcp config --- agents/claude.ts | 3 + agents/codex.ts | 84 ++++++++--------------- agents/gemini.ts | 110 +++++++++++++++++++++++++++++ agents/index.ts | 4 +- agents/jules.ts | 175 ----------------------------------------------- agents/shared.ts | 10 +++ main.ts | 14 +++- mcp/config.ts | 20 ++++++ play.ts | 2 +- todo.md | 5 +- 10 files changed, 191 insertions(+), 236 deletions(-) create mode 100644 agents/gemini.ts delete mode 100644 agents/jules.ts diff --git a/agents/claude.ts b/agents/claude.ts index a00a036..91274ef 100644 --- a/agents/claude.ts +++ b/agents/claude.ts @@ -15,6 +15,9 @@ export const claude = agent({ executablePath: "cli.js", }); }, + addMcpServer: () => { + // Claude accepts mcpServers directly in the query options, no configuration needed + }, run: async ({ prompt, mcpServers, apiKey, cliPath }) => { process.env.ANTHROPIC_API_KEY = apiKey; diff --git a/agents/codex.ts b/agents/codex.ts index 3506eec..ce8d5f3 100644 --- a/agents/codex.ts +++ b/agents/codex.ts @@ -15,13 +15,37 @@ export const codex = agent({ executablePath: "bin/codex.js", }); }, + addMcpServer: ({ serverName, serverConfig, cliPath }) => { + // Codex CLI syntax: codex mcp add --env KEY=value -- [args...] + const command = serverConfig.command; + const args = serverConfig.args || []; + const envVars = serverConfig.env || {}; + + const addArgs = ["mcp", "add", serverName]; + + // Add environment variables as --env flags first + for (const [key, value] of Object.entries(envVars)) { + addArgs.push("--env", `${key}=${value}`); + } + + addArgs.push("--", command, ...args); + + log.info(`Adding MCP server '${serverName}'...`); + const addResult = spawnSync("node", [cliPath, ...addArgs], { + stdio: "pipe", + encoding: "utf-8", + }); + + if (addResult.status !== 0) { + throw new Error( + `codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}` + ); + } + log.info(`✓ MCP server '${serverName}' configured`); + }, run: async ({ prompt, mcpServers, apiKey, cliPath, githubInstallationToken }) => { process.env.OPENAI_API_KEY = apiKey; process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; - // Configure MCP servers for Codex (global config is fine - not part of repo) - if (mcpServers && Object.keys(mcpServers).length > 0) { - configureMcpServers({ mcpServers, apiKey, cliPath }); - } // Configure Codex const codexOptions: CodexOptions = { @@ -159,55 +183,3 @@ const messageHandlers: { log.error(`Error: ${event.message}`); }, }; - -function configureMcpServers({ - mcpServers, - apiKey, - cliPath, -}: { - mcpServers: Record; - apiKey: string; - cliPath: string; -}): void { - log.info("Configuring MCP servers for Codex..."); - for (const [serverName, serverConfig] of Object.entries(mcpServers)) { - // Only configure stdio servers (Codex CLI supports stdio MCP servers) - // Check if it's a stdio server config (has 'command' property) - if (!("command" in serverConfig)) { - log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`); - continue; - } - - // Build command and args - const command = serverConfig.command; - const args = serverConfig.args || []; - const envVars = serverConfig.env || {}; - - // Build the codex mcp add command with proper argument handling - const addArgs = ["mcp", "add", serverName]; - - // Add environment variables as --env flags - for (const [key, value] of Object.entries(envVars)) { - addArgs.push("--env", `${key}=${value}`); - } - - addArgs.push("--", command, ...args); - - log.info(`Adding MCP server '${serverName}'...`); - const addResult = spawnSync("node", [cliPath, ...addArgs], { - stdio: "pipe", - encoding: "utf-8", - env: { - ...process.env, - OPENAI_API_KEY: apiKey, - }, - }); - - if (addResult.status !== 0) { - throw new Error( - `codex mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}` - ); - } - log.info(`✓ MCP server '${serverName}' configured`); - } -} diff --git a/agents/gemini.ts b/agents/gemini.ts new file mode 100644 index 0000000..621ac2c --- /dev/null +++ b/agents/gemini.ts @@ -0,0 +1,110 @@ +import { spawnSync } from "node:child_process"; +import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk"; +import { log } from "../utils/cli.ts"; +import { spawn } from "../utils/subprocess.ts"; +import { addInstructions } from "./instructions.ts"; +import { agent, installFromNpmTarball } from "./shared.ts"; + +export const gemini = agent({ + name: "gemini", + inputKeys: ["google_api_key", "gemini_api_key"], + install: async () => { + return await installFromNpmTarball({ + packageName: "@google/gemini-cli", + version: "latest", + executablePath: "dist/index.js", + installDependencies: true, + }); + }, + addMcpServer: ({ serverName, serverConfig, cliPath }) => { + // Gemini CLI syntax: gemini mcp add [args...] --env KEY=value + const command = serverConfig.command; + const args = serverConfig.args || []; + const envVars = serverConfig.env || {}; + + const addArgs = ["mcp", "add", serverName, command, ...args]; + + // Add environment variables as --env flags + for (const [key, value] of Object.entries(envVars)) { + addArgs.push("--env", `${key}=${value}`); + } + + log.info(`Adding MCP server '${serverName}'...`); + const addResult = spawnSync("node", [cliPath, ...addArgs], { + stdio: "pipe", + encoding: "utf-8", + }); + + if (addResult.status !== 0) { + throw new Error( + `gemini mcp add failed: ${addResult.stderr || addResult.stdout || "Unknown error"}` + ); + } + log.info(`✓ MCP server '${serverName}' configured`); + }, + run: async ({ prompt, apiKey, mcpServers, githubInstallationToken, cliPath }) => { + if (!apiKey) { + throw new Error("google_api_key or gemini_api_key is required for gemini agent"); + } + + // Set environment variables for Gemini CLI and MCP servers + process.env.GEMINI_API_KEY = apiKey; + process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken; + + const sessionPrompt = addInstructions(prompt); + log.info(`Starting Gemini CLI with prompt: ${prompt.substring(0, 100)}...`); + + let finalOutput = ""; + try { + const result = await spawn({ + cmd: "node", + args: [cliPath, "--yolo", "--output-format", "text", sessionPrompt], + env: { + GEMINI_API_KEY: apiKey, + GITHUB_INSTALLATION_TOKEN: githubInstallationToken, + }, + onStdout: (chunk) => { + const trimmed = chunk.trim(); + if (trimmed) { + log.info(trimmed); + finalOutput += trimmed + "\n"; + } + }, + onStderr: (chunk) => { + const trimmed = chunk.trim(); + if (trimmed) { + log.warning(trimmed); + finalOutput += trimmed + "\n"; + } + }, + }); + + if (result.exitCode !== 0) { + const errorMessage = result.stderr || result.stdout || "Unknown error"; + log.error(`Gemini CLI exited with code ${result.exitCode}: ${errorMessage}`); + return { + success: false, + error: errorMessage, + output: finalOutput || result.stdout || "", + }; + } + + finalOutput = finalOutput || result.stdout || "Gemini CLI completed successfully."; + log.info("✓ Gemini CLI completed successfully"); + + return { + success: true, + output: finalOutput, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + log.error(`Failed to run Gemini CLI: ${errorMessage}`); + return { + success: false, + error: errorMessage, + output: finalOutput || "", + }; + } + }, +}); + diff --git a/agents/index.ts b/agents/index.ts index b2f2a75..aa52a10 100644 --- a/agents/index.ts +++ b/agents/index.ts @@ -1,11 +1,11 @@ import { claude } from "./claude.ts"; import { codex } from "./codex.ts"; -import { jules } from "./jules.ts"; +import { gemini } from "./gemini.ts"; export const agents = { claude, codex, - jules, + gemini, } as const; export type AgentInputKey = (typeof agents)[keyof typeof agents]["inputKeys"][number]; diff --git a/agents/jules.ts b/agents/jules.ts deleted file mode 100644 index a61379f..0000000 --- a/agents/jules.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { log } from "../utils/cli.ts"; -import { parseRepoContext } from "../utils/github.ts"; -import { spawn } from "../utils/subprocess.ts"; -import { addInstructions } from "./instructions.ts"; -import { agent, installFromNpmTarball } from "./shared.ts"; - -export const jules = agent({ - name: "jules", - inputKeys: ["google_api_key", "gemini_api_key"], - install: async () => { - return await installFromNpmTarball({ - packageName: "@google/jules", - version: "latest", - executablePath: "run.cjs", - installDependencies: true, - }); - }, - run: async ({ - prompt, - apiKey, - mcpServers: _mcpServers, - githubInstallationToken: _githubInstallationToken, - cliPath, - }) => { - if (!apiKey) { - throw new Error("google_api_key is required for jules agent"); - } - process.env.GOOGLE_API_KEY = apiKey; - - const repoContext = parseRepoContext(); - const repoName = `${repoContext.owner}/${repoContext.name}`; - - log.info(`Creating Jules session for ${repoName}...`); - - // Create a new remote session - const sessionPrompt = addInstructions(prompt); - log.info(`Starting session with prompt: ${prompt.substring(0, 100)}...`); - - let sessionId: string | undefined; - try { - const createResult = await spawn({ - cmd: "node", - args: [cliPath, "remote", "new", "--repo", repoName, "--session", sessionPrompt], - onStdout: (chunk) => { - log.info(chunk.trim()); - // Try to extract session ID from output - const match = chunk.match(/session[:\s]+(\d+)/i) || chunk.match(/id[:\s]+(\d+)/i); - if (match && !sessionId) { - sessionId = match[1]; - log.info(`✓ Session ID: ${sessionId}`); - } - }, - onStderr: (chunk) => { - log.warning(chunk.trim()); - }, - }); - - if (createResult.exitCode !== 0) { - throw new Error( - `Failed to create Jules session: ${createResult.stderr || createResult.stdout || "Unknown error"}` - ); - } - - // If session ID wasn't extracted from stdout, try to parse it - if (!sessionId) { - const output = createResult.stdout + createResult.stderr; - const match = output.match(/session[:\s]+(\d+)/i) || output.match(/id[:\s]+(\d+)/i); - if (match) { - sessionId = match[1]; - } - } - - if (!sessionId) { - log.warning("Could not extract session ID from output. Session may have been created."); - log.info(`Output: ${createResult.stdout}`); - } else { - log.info(`✓ Session created: ${sessionId}`); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - log.error(`Failed to create Jules session: ${errorMessage}`); - return { - success: false, - error: errorMessage, - output: "", - }; - } - - // Monitor session progress by polling session list - log.info("Monitoring session progress..."); - let finalOutput = ""; - const maxPollAttempts = 300; // ~50 minutes max (10 second intervals) - let pollAttempts = 0; - - while (pollAttempts < maxPollAttempts) { - await new Promise((resolve) => setTimeout(resolve, 10000)); // Wait 10 seconds between polls - pollAttempts++; - - try { - // List sessions to check status - const listResult = await spawn({ - cmd: "node", - args: [cliPath, "remote", "list", "--session"], - onStdout: (chunk) => { - // Log session updates - const trimmed = chunk.trim(); - if (trimmed) { - log.info(trimmed); - } - }, - }); - - if (listResult.exitCode === 0) { - const output = listResult.stdout; - // Check if our session is complete - // The CLI output format may vary, so we look for completion indicators - if (sessionId && output.includes(sessionId)) { - // Try to determine if session is complete - // This is a heuristic - the actual output format may differ - if ( - output.includes("completed") || - output.includes("done") || - output.includes("finished") - ) { - log.info("Session appears to be completed"); - finalOutput = "Session completed. Pulling results..."; - break; - } - } - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - log.warning(`Error checking session status: ${errorMessage}`); - } - } - - // Pull results if session ID is available - if (sessionId) { - try { - log.info(`Pulling results for session ${sessionId}...`); - const pullResult = await spawn({ - cmd: "node", - args: [cliPath, "remote", "pull", "--session", sessionId], - onStdout: (chunk) => { - log.info(chunk.trim()); - }, - onStderr: (chunk) => { - log.warning(chunk.trim()); - }, - }); - - if (pullResult.exitCode === 0) { - finalOutput = pullResult.stdout || "Results pulled successfully."; - } else { - log.warning(`Failed to pull results: ${pullResult.stderr || pullResult.stdout}`); - finalOutput = finalOutput || "Session completed. Check Jules dashboard for results."; - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - log.warning(`Error pulling results: ${errorMessage}`); - } - } - - if (pollAttempts >= maxPollAttempts) { - log.warning("Session monitoring timeout reached. Session may still be in progress."); - finalOutput = - finalOutput || "Session monitoring timeout. Check Jules dashboard for session status."; - } - - return { - success: true, - output: finalOutput || "Jules session completed. Check the Jules dashboard for results.", - }; - }, -}); diff --git a/agents/shared.ts b/agents/shared.ts index c474abc..63a641e 100644 --- a/agents/shared.ts +++ b/agents/shared.ts @@ -149,9 +149,19 @@ export const agent = (agent: agent): agent => { return agent; }; +/** + * Parameters for adding an MCP server to an agent + */ +export interface AddMcpServerParams { + serverName: string; + serverConfig: Extract; + cliPath: string; +} + export type Agent = { name: string; inputKeys: string[]; install: () => Promise; + addMcpServer: (params: AddMcpServerParams) => void; run: (config: AgentConfig) => Promise; }; diff --git a/main.ts b/main.ts index dd09ff4..ba761a6 100644 --- a/main.ts +++ b/main.ts @@ -1,7 +1,7 @@ import { flatMorph } from "@ark/util"; import { type } from "arktype"; import { agents } from "./agents/index.ts"; -import { createMcpConfigs } from "./mcp/config.ts"; +import { createMcpConfigs, forEachStdioMcpServer } from "./mcp/config.ts"; import packageJson from "./package.json" with { type: "json" }; import { fetchRepoSettings } from "./utils/api.ts"; import { log } from "./utils/cli.ts"; @@ -141,6 +141,18 @@ export async function main(inputs: Inputs): Promise { const apiKey = inputs[matchingInputKey]!; + // Configure MCP servers if provided (global config is fine - not part of repo) + if (mcpServers && Object.keys(mcpServers).length > 0) { + log.info(`Configuring MCP servers for ${agentName}...`); + forEachStdioMcpServer(mcpServers, (serverName, serverConfig) => { + agent.addMcpServer({ + serverName, + serverConfig, + cliPath, + }); + }); + } + const result = await agent.run({ prompt: inputs.prompt, mcpServers, diff --git a/mcp/config.ts b/mcp/config.ts index 4f25d39..fde1572 100644 --- a/mcp/config.ts +++ b/mcp/config.ts @@ -4,6 +4,7 @@ import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk"; import { fromHere } from "@ark/fs"; +import { log } from "../utils/cli.ts"; import { parseRepoContext } from "../utils/github.ts"; import { ghPullfrogMcpName } from "./index.ts"; @@ -30,3 +31,22 @@ export function createMcpConfigs(githubInstallationToken: string): McpConfigs { }, }; } + +/** + * Iterate through MCP servers and call the provided handler for each stdio server + * Shared logic to avoid duplication across agents + */ +export function forEachStdioMcpServer( + mcpServers: Record, + handler: (serverName: string, serverConfig: Extract) => void +): void { + for (const [serverName, serverConfig] of Object.entries(mcpServers)) { + // Only configure stdio servers (CLIs support stdio MCP servers) + if (!("command" in serverConfig)) { + log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`); + continue; + } + handler(serverName, serverConfig as Extract); + } +} + diff --git a/play.ts b/play.ts index 5db6aea..5e972a6 100644 --- a/play.ts +++ b/play.ts @@ -24,7 +24,7 @@ export async function run( const inputs: Required = { prompt, - agent: "jules", + agent: "gemini", ...flatMorph(agents, (_, agent) => agent.inputKeys.map((inputKey) => [inputKey, process.env[inputKey.toUpperCase()]]) ), diff --git a/todo.md b/todo.md index cd6f11c..bc42a17 100644 --- a/todo.md +++ b/todo.md @@ -1,6 +1,8 @@ ## CURRENT -[] handle progressive comment updating from pullfrog mcp +[] jules/gemini support +[] gemini installation speed +[] standardize mcp server ## MAYBE @@ -21,3 +23,4 @@ [x] don't allow rejecting prs [x] fix pnpm caching [x] fix prompt to avoid narration like "I just read all tools from MCP server" +[x] handle progressive comment updating from pullfrog mcp