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
+161 -55
View File
@@ -1,5 +1,6 @@
import { spawnSync } from "node:child_process"; 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 { log } from "../utils/cli.ts";
import { addInstructions } from "./instructions.ts"; import { addInstructions } from "./instructions.ts";
import { agent, installFromNpmTarball } from "./shared.ts"; import { agent, installFromNpmTarball } from "./shared.ts";
@@ -15,21 +16,167 @@ export const codex = agent({
}); });
}, },
run: async ({ prompt, mcpServers, apiKey, cliPath }) => { run: async ({ prompt, mcpServers, apiKey, cliPath }) => {
// Equivalent to: printenv OPENAI_API_KEY | codex login --with-api-key process.env.OPENAI_API_KEY = apiKey;
// 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"}`
);
}
// Configure MCP servers for Codex (global config is fine - not part of repo) // Configure MCP servers for Codex (global config is fine - not part of repo)
if (mcpServers && Object.keys(mcpServers).length > 0) { if (mcpServers && Object.keys(mcpServers).length > 0) {
configureMcpServers({ mcpServers, apiKey, cliPath });
}
// Configure Codex
const codexOptions: CodexOptions = {
apiKey,
codexPathOverride: cliPath,
};
const codex = new Codex(codexOptions);
// Configure thread options to match Claude's permissions (bypassPermissions)
// approvalPolicy: "never" = no approval needed (equivalent to bypassPermissions)
// sandboxMode: "workspace-write" = allow file writes
// networkAccessEnabled: true = allow network access (needed for GitHub API calls)
const thread = codex.startThread({
approvalPolicy: "never",
sandboxMode: "workspace-write",
networkAccessEnabled: true,
});
try {
// Use runStreamed to get streaming events similar to claude.ts
const streamedTurn = await thread.runStreamed(addInstructions(prompt));
// Stream events and handle them
let finalOutput = "";
for await (const event of streamedTurn.events) {
const handler = messageHandlers[event.type 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;
}
}
return {
success: true,
output: finalOutput,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Codex execution failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
output: "",
};
}
},
});
// Track command execution IDs to identify when command results come back
const commandExecutionIds = new Set<string>();
type ThreadEventHandler = (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..."); log.info("Configuring MCP servers for Codex...");
for (const [serverName, serverConfig] of Object.entries(mcpServers)) { for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
// Only configure stdio servers (Codex CLI supports stdio MCP servers) // Only configure stdio servers (Codex CLI supports stdio MCP servers)
@@ -69,45 +216,4 @@ export const codex = agent({
} }
log.info(`✓ MCP server '${serverName}' configured`); log.info(`✓ MCP server '${serverName}' configured`);
} }
} }
log.info("Running Codex via CLI...");
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
});
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 || "",
};
}
const output = result.stdout || "";
log.box(output, { title: "Codex" });
return {
success: true,
output,
};
} catch (cliError) {
const errorMessage = cliError instanceof Error ? cliError.message : String(cliError);
log.error(`Codex execution failed: ${errorMessage}`);
return {
success: false,
error: errorMessage,
output: "",
};
}
},
});
+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. Your code is focused, minimal, and production-ready.
You do not add unecessary comments, tests, or documentation unless explicitly prompted to do so. 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 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 ## 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} 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 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 ## Workflow Selection
+4 -1
View File
@@ -1,5 +1,5 @@
import { spawnSync } from "node:child_process"; 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 { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
@@ -119,6 +119,9 @@ export async function installFromNpmTarball({
throw new Error(`Executable not found in extracted package at ${cliPath}`); throw new Error(`Executable not found in extracted package at ${cliPath}`);
} }
// Make the file executable
chmodSync(cliPath, 0o755);
log.info(`${packageName} installed at ${cliPath}`); log.info(`${packageName} installed at ${cliPath}`);
return cliPath; return cliPath;
+612 -197
View File
File diff suppressed because it is too large Load Diff