use openaisdk
This commit is contained in:
+181
-75
@@ -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,93 +16,54 @@ 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) {
|
||||||
log.info("Configuring MCP servers for Codex...");
|
configureMcpServers({ mcpServers, apiKey, cliPath });
|
||||||
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`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
const result = spawnSync("node", [cliPath, "exec", addInstructions(prompt)], {
|
// Use runStreamed to get streaming events similar to claude.ts
|
||||||
encoding: "utf-8",
|
const streamedTurn = await thread.runStreamed(addInstructions(prompt));
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
OPENAI_API_KEY: apiKey,
|
|
||||||
},
|
|
||||||
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.status !== 0) {
|
// Stream events and handle them
|
||||||
const errorMessage = result.stderr || result.stdout || "Codex execution failed";
|
let finalOutput = "";
|
||||||
log.error(`Codex execution failed: ${errorMessage}`);
|
for await (const event of streamedTurn.events) {
|
||||||
return {
|
const handler = messageHandlers[event.type as keyof typeof messageHandlers];
|
||||||
success: false,
|
if (handler) {
|
||||||
error: errorMessage,
|
await handler(event);
|
||||||
output: result.stdout || "",
|
}
|
||||||
};
|
|
||||||
|
// 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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output,
|
output: finalOutput,
|
||||||
};
|
};
|
||||||
} catch (cliError) {
|
} catch (error) {
|
||||||
const errorMessage = cliError instanceof Error ? cliError.message : String(cliError);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
log.error(`Codex execution failed: ${errorMessage}`);
|
log.error(`Codex execution failed: ${errorMessage}`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user