This commit is contained in:
Shawn Morreau
2025-12-01 10:45:14 -05:00
20 changed files with 851 additions and 546 deletions
+2 -2
View File
@@ -2,7 +2,7 @@ import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import packageJson from "../package.json" with { type: "json" };
import { log } from "../utils/cli.ts";
import { addInstructions } from "./instructions.ts";
import { agent, installFromNpmTarball } from "./shared.ts";
import { agent, installFromNpmTarball, setupProcessAgentEnv } from "./shared.ts";
export const claude = agent({
name: "claude",
@@ -15,7 +15,7 @@ export const claude = agent({
});
},
run: async ({ payload, mcpServers, apiKey, cliPath }) => {
process.env.ANTHROPIC_API_KEY = apiKey;
setupProcessAgentEnv({ ANTHROPIC_API_KEY: apiKey });
const prompt = addInstructions(payload);
console.log(prompt);
+13 -14
View File
@@ -4,7 +4,12 @@ import { join } from "node:path";
import { Codex, type CodexOptions, type ThreadEvent } from "@openai/codex-sdk";
import { log } from "../utils/cli.ts";
import { addInstructions } from "./instructions.ts";
import { agent, type ConfigureMcpServersParams, installFromNpmTarball } from "./shared.ts";
import {
agent,
type ConfigureMcpServersParams,
installFromNpmTarball,
setupProcessAgentEnv,
} from "./shared.ts";
export const codex = agent({
name: "codex",
@@ -15,16 +20,16 @@ export const codex = agent({
executablePath: "bin/codex.js",
});
},
run: async ({ payload, mcpServers, apiKey, cliPath, githubInstallationToken }) => {
process.env.OPENAI_API_KEY = apiKey;
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
run: async ({ payload, mcpServers, apiKey, cliPath }) => {
// create config directory for codex before setting HOME
const tempHome = process.env.PULLFROG_TEMP_DIR!;
const configDir = join(tempHome, ".config", "codex");
mkdirSync(configDir, { recursive: true });
process.env.HOME = tempHome;
setupProcessAgentEnv({
OPENAI_API_KEY: apiKey,
HOME: tempHome,
});
configureCodexMcpServers({ mcpServers, cliPath });
@@ -35,21 +40,16 @@ export const codex = agent({
};
const codex = new Codex(codexOptions);
// Configure thread options to match Claude's permissions (bypassPermissions)
// approvalPolicy: "never" = no approval needed (equivalent to bypassPermissions)
// sandboxMode: "workspace-write" = allow file writes
// networkAccessEnabled: true = allow network access (needed for GitHub API calls)
const thread = codex.startThread({
approvalPolicy: "never",
sandboxMode: "workspace-write",
// use danger-full-access to allow git operations (workspace-write blocks .git directory writes)
sandboxMode: "danger-full-access",
networkAccessEnabled: true,
});
try {
// Use runStreamed to get streaming events similar to claude.ts
const streamedTurn = await thread.runStreamed(addInstructions(payload));
// Stream events and handle them
let finalOutput = "";
for await (const event of streamedTurn.events) {
const handler = messageHandlers[event.type];
@@ -58,7 +58,6 @@ export const codex = agent({
handler(event as never);
}
// Capture final response from agent messages
if (event.type === "item.completed" && event.item.type === "agent_message") {
finalOutput = event.item.text;
}
+9 -14
View File
@@ -4,7 +4,12 @@ import { homedir } from "node:os";
import { join } from "node:path";
import { log } from "../utils/cli.ts";
import { addInstructions } from "./instructions.ts";
import { agent, type ConfigureMcpServersParams, installFromCurl } from "./shared.ts";
import {
agent,
type ConfigureMcpServersParams,
createAgentEnv,
installFromCurl,
} from "./shared.ts";
// cursor cli event types inferred from stream-json output
interface CursorSystemEvent {
@@ -138,10 +143,7 @@ export const cursor = agent({
executableName: "cursor-agent",
});
},
run: async ({ payload, apiKey, cliPath, githubInstallationToken, mcpServers }) => {
process.env.CURSOR_API_KEY = apiKey;
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
run: async ({ payload, apiKey, cliPath, mcpServers }) => {
configureCursorMcpServers({ mcpServers, cliPath });
try {
@@ -165,16 +167,9 @@ export const cursor = agent({
],
{
cwd: process.cwd(),
env: {
env: createAgentEnv({
CURSOR_API_KEY: apiKey,
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
LOG_LEVEL: process.env.LOG_LEVEL,
NODE_ENV: process.env.NODE_ENV,
HOME: process.env.HOME,
PATH: process.env.PATH,
// Don't override HOME - Cursor CLI needs access to macOS keychain
// MCP config is written to tempDir/.cursor/mcp.json which Cursor will find
},
}),
stdio: ["ignore", "pipe", "pipe"], // Ignore stdin, pipe stdout/stderr
}
);
+9 -14
View File
@@ -2,7 +2,12 @@ import { spawnSync } from "node:child_process";
import { log } from "../utils/cli.ts";
import { spawn } from "../utils/subprocess.ts";
import { addInstructions } from "./instructions.ts";
import { agent, type ConfigureMcpServersParams, installFromGithub } from "./shared.ts";
import {
agent,
type ConfigureMcpServersParams,
createAgentEnv,
installFromGithub,
} from "./shared.ts";
// gemini cli event types inferred from stream-json output (NDJSON format)
interface GeminiInitEvent {
@@ -152,16 +157,12 @@ export const gemini = agent({
...(githubInstallationToken && { githubInstallationToken }),
});
},
run: async ({ payload, apiKey, mcpServers, githubInstallationToken, cliPath }) => {
run: async ({ payload, apiKey, mcpServers, cliPath }) => {
configureGeminiMcpServers({ mcpServers, cliPath });
if (!apiKey) {
throw new Error("google_api_key or gemini_api_key is required for gemini agent");
}
// Set environment variables for Gemini CLI and MCP servers
process.env.GEMINI_API_KEY = apiKey;
process.env.GITHUB_INSTALLATION_TOKEN = githubInstallationToken;
const sessionPrompt = addInstructions(payload);
log.info(`Starting Gemini CLI with prompt: ${payload.prompt.substring(0, 100)}...`);
@@ -170,15 +171,9 @@ export const gemini = agent({
const result = await spawn({
cmd: "node",
args: [cliPath, "--yolo", "--output-format=stream-json", "-p", sessionPrompt],
env: {
PATH: process.env.PATH || "",
HOME: process.env.HOME || "",
TMPDIR: process.env.TMPDIR || "/tmp",
env: createAgentEnv({
GEMINI_API_KEY: apiKey,
GITHUB_INSTALLATION_TOKEN: githubInstallationToken,
LOG_LEVEL: process.env.LOG_LEVEL!,
NODE_ENV: process.env.NODE_ENV!,
},
}),
timeout: 600000, // 10 minutes
onStdout: async (chunk) => {
const text = chunk.toString();
+35 -9
View File
@@ -8,6 +8,7 @@ import type { McpHttpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import type { show } from "@ark/util";
import { type AgentManifest, type AgentName, agentsManifest, type Payload } from "../external.ts";
import { log } from "../utils/cli.ts";
import { getGitHubInstallationToken } from "../utils/github.ts";
/**
* Result returned by agent execution
@@ -24,7 +25,6 @@ export interface AgentResult {
*/
export interface AgentConfig {
apiKey: string;
githubInstallationToken: string;
payload: Payload;
mcpServers: Record<string, McpHttpServerConfig>;
cliPath: string;
@@ -38,6 +38,35 @@ export interface ConfigureMcpServersParams {
cliPath: string;
}
/**
* Add agent-specific vars to a whitelisted environment object for agent subprocesses.
*
* @param agentSpecificVars - Object containing agent-specific environment variables to include
* @returns Whitelisted environment object safe for subprocess spawning
*/
export function createAgentEnv(agentSpecificVars: Record<string, string>): Record<string, string> {
return {
PATH: process.env.PATH,
HOME: process.env.HOME,
LOG_LEVEL: process.env.LOG_LEVEL,
NODE_ENV: process.env.NODE_ENV,
GITHUB_TOKEN: getGitHubInstallationToken(),
...agentSpecificVars,
// values could be undefined but will be ignored
} as never;
}
/**
* Set up whitelisted environment variables in the current process.
* Used for SDKs that run in the same process (e.g., Claude SDK, Codex SDK).
* Includes agent-agnostic vars (PATH, HOME, LOG_LEVEL, NODE_ENV) plus agent-specific vars.
*
* @param agentSpecificVars - Object containing agent-specific environment variables to include
*/
export function setupProcessAgentEnv(agentSpecificVars: Record<string, string>): void {
Object.assign(process.env, createAgentEnv(agentSpecificVars));
}
/**
* Parameters for installing from npm tarball
*/
@@ -320,17 +349,14 @@ export async function installFromCurl({
log.info(`Installing to temp directory at ${tempDir}...`);
// Run the install script with HOME set to temp directory
// The Cursor install script installs to $HOME/.local/bin/{executableName}
// By setting HOME=tempDir, we ensure it installs to tempDir/.local/bin/{executableName}
const installResult = spawnSync("bash", [installScriptPath], {
cwd: tempDir,
env: {
HOME: tempDir, // Cursor install script uses HOME for installation path
PATH: process.env.PATH || "",
SHELL: process.env.SHELL || "/bin/bash",
USER: process.env.USER || "",
TMPDIR: process.env.TMPDIR || "/tmp",
// Run the install script with HOME set to temp directory
// ensuring a fresh install for each run
HOME: tempDir,
SHELL: process.env.SHELL,
USER: process.env.USER,
},
stdio: "pipe",
encoding: "utf-8",