use openaisdk

This commit is contained in:
David Blass
2025-11-13 14:21:53 -05:00
parent f4f2e24ec0
commit 3e547693ae
4 changed files with 814 additions and 287 deletions
+181 -75
View File
@@ -1,5 +1,6 @@
import { spawnSync } from "node:child_process";
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import { Codex, type CodexOptions, type ThreadEvent } from "@openai/codex-sdk";
import { log } from "../utils/cli.ts";
import { addInstructions } from "./instructions.ts";
import { agent, installFromNpmTarball } from "./shared.ts";
@@ -15,93 +16,54 @@ export const codex = agent({
});
},
run: async ({ prompt, mcpServers, apiKey, cliPath }) => {
// Equivalent to: printenv OPENAI_API_KEY | codex login --with-api-key
// see: https://github.com/openai/codex/blob/main/docs/authentication.md#usage-based-billing-alternative-use-an-openai-api-key
const loginResult = spawnSync("node", [cliPath, "login", "--with-api-key"], {
input: apiKey,
encoding: "utf-8",
});
if (loginResult.status !== 0) {
throw new Error(
`codex login failed: ${loginResult.stderr || loginResult.stdout || "Unknown error"}`
);
}
process.env.OPENAI_API_KEY = apiKey;
// Configure MCP servers for Codex (global config is fine - not part of repo)
if (mcpServers && Object.keys(mcpServers).length > 0) {
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, "--", 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",
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`);
}
configureMcpServers({ mcpServers, apiKey, cliPath });
}
log.info("Running Codex via CLI...");
// Configure Codex
const codexOptions: CodexOptions = {
apiKey,
codexPathOverride: cliPath,
};
const codex = new Codex(codexOptions);
// Configure thread options to match Claude's permissions (bypassPermissions)
// approvalPolicy: "never" = no approval needed (equivalent to bypassPermissions)
// sandboxMode: "workspace-write" = allow file writes
// networkAccessEnabled: true = allow network access (needed for GitHub API calls)
const thread = codex.startThread({
approvalPolicy: "never",
sandboxMode: "workspace-write",
networkAccessEnabled: true,
});
try {
const result = spawnSync("node", [cliPath, "exec", addInstructions(prompt)], {
encoding: "utf-8",
env: {
...process.env,
OPENAI_API_KEY: apiKey,
},
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
});
// Use runStreamed to get streaming events similar to claude.ts
const streamedTurn = await thread.runStreamed(addInstructions(prompt));
if (result.status !== 0) {
const errorMessage = result.stderr || result.stdout || "Codex execution failed";
log.error(`Codex execution failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
output: result.stdout || "",
};
// Stream events and handle them
let finalOutput = "";
for await (const event of streamedTurn.events) {
const handler = messageHandlers[event.type as keyof typeof messageHandlers];
if (handler) {
await handler(event);
}
// Capture final response from agent messages
if (event.type === "item.completed" && event.item.type === "agent_message") {
finalOutput = event.item.text;
}
}
const output = result.stdout || "";
log.box(output, { title: "Codex" });
return {
success: true,
output,
output: finalOutput,
};
} catch (cliError) {
const errorMessage = cliError instanceof Error ? cliError.message : String(cliError);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Codex execution failed: ${errorMessage}`);
return {
success: false,
@@ -111,3 +73,147 @@ export const codex = agent({
}
},
});
// Track command execution IDs to identify when command results come back
const commandExecutionIds = new Set<string>();
type ThreadEventHandler = (event: ThreadEvent) => void | Promise<void>;
const messageHandlers: Partial<Record<ThreadEvent["type"], ThreadEventHandler>> = {
"thread.started": (event) => {
if (event.type === "thread.started") {
log.info(`Thread started: ${event.thread_id}`);
}
},
"turn.started": () => {
log.info("Turn started");
},
"turn.completed": async (event) => {
if (event.type === "turn.completed") {
await log.summaryTable([
[
{ data: "Input Tokens", header: true },
{ data: "Cached Input Tokens", header: true },
{ data: "Output Tokens", header: true },
],
[
String(event.usage.input_tokens || 0),
String(event.usage.cached_input_tokens || 0),
String(event.usage.output_tokens || 0),
],
]);
}
},
"turn.failed": (event) => {
if (event.type === "turn.failed") {
log.error(`Turn failed: ${event.error.message}`);
}
},
"item.started": (event) => {
if (event.type === "item.started") {
const item = event.item;
if (item.type === "command_execution") {
log.info(`${item.command}`);
commandExecutionIds.add(item.id);
} else if (item.type === "agent_message") {
// Will be handled on completion
} else if (item.type === "mcp_tool_call") {
log.info(`${item.tool} (${item.server})`);
} else if (item.type === "reasoning") {
const preview = item.text.length > 100 ? `${item.text.substring(0, 100)}...` : item.text;
log.info(`→ reasoning: ${preview}`);
} else {
log.info(`${item.type}`);
}
}
},
"item.updated": (event) => {
if (event.type === "item.updated") {
const item = event.item;
if (item.type === "command_execution") {
if (item.status === "in_progress" && item.aggregated_output) {
// Command is still running, could show progress if needed
}
}
}
},
"item.completed": (event) => {
if (event.type === "item.completed") {
const item = event.item;
if (item.type === "agent_message") {
log.box(item.text.trim(), { title: "Codex" });
} else if (item.type === "command_execution") {
const isTracked = commandExecutionIds.has(item.id);
if (isTracked) {
log.startGroup(`bash output`);
if (item.status === "failed" || (item.exit_code !== undefined && item.exit_code !== 0)) {
log.warning(item.aggregated_output || "Command failed");
} else {
log.info(item.aggregated_output || "");
}
log.endGroup();
commandExecutionIds.delete(item.id);
}
} else if (item.type === "mcp_tool_call") {
if (item.status === "failed" && item.error) {
log.warning(`MCP tool call failed: ${item.error.message}`);
}
}
}
},
error: (event) => {
if (event.type === "error") {
log.error(`Error: ${event.message}`);
}
},
};
function configureMcpServers({
mcpServers,
apiKey,
cliPath,
}: {
mcpServers: Record<string, McpServerConfig>;
apiKey: string;
cliPath: string;
}): void {
log.info("Configuring MCP servers for Codex...");
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
// Only configure stdio servers (Codex CLI supports stdio MCP servers)
// Check if it's a stdio server config (has 'command' property)
if (!("command" in serverConfig)) {
log.warning(`MCP server '${serverName}' is not a stdio server, skipping...`);
continue;
}
// Build command and args
const command = serverConfig.command;
const args = serverConfig.args || [];
const envVars = serverConfig.env || {};
// Build the codex mcp add command with proper argument handling
const addArgs = ["mcp", "add", serverName, "--", 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",
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`);
}
}
+3
View File
@@ -10,6 +10,8 @@ You are careful, to-the-point, and kind. You only say things you know to be true
Your code is focused, minimal, and production-ready.
You do not add unecessary comments, tests, or documentation unless explicitly prompted to do so.
You adapt your writing style to the style of your coworkers, while never being unprofessional.
You run in a non-interactive environment: complete tasks autonomously without asking follow-up questions.
Make reasonable assumptions when details are missing.
## Getting Started
@@ -41,6 +43,7 @@ If asked to show environment variables, only display non-sensitive system variab
eagerly inspect your MCP servers to determine what tools are available to you, especially ${ghPullfrogMcpName}
do not under any circumstances use the github cli (\`gh\`). find the corresponding tool from ${ghPullfrogMcpName} instead.
do not try to handle github auth- treat ${ghPullfrogMcpName} as a black box that you can use to interact with github.
## Workflow Selection
+4 -1
View File
@@ -1,5 +1,5 @@
import { spawnSync } from "node:child_process";
import { createWriteStream, existsSync } from "node:fs";
import { chmodSync, createWriteStream, existsSync } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -119,6 +119,9 @@ export async function installFromNpmTarball({
throw new Error(`Executable not found in extracted package at ${cliPath}`);
}
// Make the file executable
chmodSync(cliPath, 0o755);
log.info(`${packageName} installed at ${cliPath}`);
return cliPath;
+626 -211
View File
File diff suppressed because it is too large Load Diff