agent-dealer 0.1.13 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/bundle/server/dist/adapters/agent-health.js +28 -1
  2. package/bundle/server/dist/cli-env.js +16 -0
  3. package/bundle/server/dist/cli-env.test.js +14 -1
  4. package/bundle/server/dist/db/index.js +2 -1
  5. package/bundle/server/dist/queue/result-qa.js +1 -1
  6. package/bundle/server/dist/runners/claude.js +6 -87
  7. package/bundle/server/dist/runners/codex-args.js +59 -0
  8. package/bundle/server/dist/runners/codex-args.test.js +72 -0
  9. package/bundle/server/dist/runners/codex-jsonl.js +150 -0
  10. package/bundle/server/dist/runners/codex-jsonl.test.js +36 -0
  11. package/bundle/server/dist/runners/codex-resume.test.js +19 -0
  12. package/bundle/server/dist/runners/codex.js +46 -0
  13. package/bundle/server/dist/runners/models-codex.test.js +14 -0
  14. package/bundle/server/dist/runners/models.js +53 -1
  15. package/bundle/server/dist/runners/persist.js +4 -1
  16. package/bundle/server/dist/runners/qa.js +17 -1
  17. package/bundle/server/dist/runners/spawn-cli.js +90 -0
  18. package/bundle/server/package.json +2 -2
  19. package/bundle/server/static-ui/assets/index-DTcianAH.js +79 -0
  20. package/bundle/server/static-ui/index.html +1 -1
  21. package/bundle/shared/dist/agents.d.ts +18 -17
  22. package/bundle/shared/dist/agents.js +1 -0
  23. package/bundle/shared/dist/index.d.ts +83 -83
  24. package/bundle/shared/dist/runtime.d.ts +1 -1
  25. package/bundle/shared/dist/runtime.js +1 -1
  26. package/bundle/shared/package.json +1 -1
  27. package/dist/doctor.js +18 -0
  28. package/dist/index.js +14 -2
  29. package/dist/install.d.ts +8 -0
  30. package/dist/install.js +72 -0
  31. package/dist/managed/activate.d.ts +2 -0
  32. package/dist/managed/activate.js +49 -0
  33. package/dist/managed/activate.test.d.ts +1 -0
  34. package/dist/managed/activate.test.js +34 -0
  35. package/dist/managed/index.d.ts +8 -0
  36. package/dist/managed/index.js +8 -0
  37. package/dist/managed/install-kind.d.ts +5 -0
  38. package/dist/managed/install-kind.js +16 -0
  39. package/dist/managed/launcher.d.ts +1 -0
  40. package/dist/managed/launcher.js +19 -0
  41. package/dist/managed/npm-prefix-install.d.ts +16 -0
  42. package/dist/managed/npm-prefix-install.js +38 -0
  43. package/dist/managed/paths.d.ts +11 -0
  44. package/dist/managed/paths.js +45 -0
  45. package/dist/managed/semver.d.ts +2 -0
  46. package/dist/managed/semver.js +12 -0
  47. package/dist/managed/update-state.d.ts +7 -0
  48. package/dist/managed/update-state.js +22 -0
  49. package/dist/managed/updater.d.ts +21 -0
  50. package/dist/managed/updater.js +107 -0
  51. package/dist/update-check.d.ts +1 -0
  52. package/dist/update-check.js +25 -37
  53. package/dist/upgrade.d.ts +1 -0
  54. package/dist/upgrade.js +55 -6
  55. package/package.json +1 -1
  56. package/bundle/server/static-ui/assets/index-B_7S7xJb.js +0 -79
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
- import { claudeBinExists, cursorBinExists, resolveClaudeBin, cursorInvokeArgs, resolveCursorBin, } from "../cli-env.js";
3
+ import { claudeBinExists, cursorBinExists, resolveClaudeBin, cursorInvokeArgs, resolveCursorBin, resolveCodexBin, codexBinExists, } from "../cli-env.js";
4
4
  import { checkAgentDeckHealth, isAgentDeckMcpRegistered } from "./agent-deck.js";
5
5
  const RUNTIME_CACHE_MS = 60_000;
6
6
  const runtimeIssueCache = new Map();
@@ -41,6 +41,32 @@ async function runtimeIssuesUncached(runtime) {
41
41
  }
42
42
  return [];
43
43
  }
44
+ if (runtime === "codex_local") {
45
+ if (!codexBinExists()) {
46
+ const ver = await runCommand(resolveCodexBin(), ["--version"]);
47
+ if (!ver.ok) {
48
+ return [{ code: "cli_missing", message: "Codex CLI not found — install Codex (`codex`)" }];
49
+ }
50
+ }
51
+ const ver = await runCommand(resolveCodexBin(), ["--version"]);
52
+ if (!ver.ok) {
53
+ return [{ code: "cli_missing", message: "Codex CLI not found — install Codex (`codex`)" }];
54
+ }
55
+ // `codex --version` succeeds without auth — use login status for auth health.
56
+ const login = await runCommand(resolveCodexBin(), ["login", "status"]);
57
+ const out = login.output.toLowerCase();
58
+ const loggedIn = login.ok &&
59
+ (out.includes("logged in") || out.includes("authenticated") || out.includes("api key"));
60
+ if (!loggedIn) {
61
+ return [
62
+ {
63
+ code: "runtime_auth",
64
+ message: "Run `codex login` (or set OPENAI_API_KEY for automation)",
65
+ },
66
+ ];
67
+ }
68
+ return [];
69
+ }
44
70
  const status = await runCommand(resolveCursorBin(), cursorInvokeArgs(["status"]));
45
71
  if (!status.ok && !status.output.trim() && !cursorBinExists()) {
46
72
  return [{ code: "cli_missing", message: "cursor-agent not found — run: curl https://cursor.com/install -fsS | bash" }];
@@ -80,6 +106,7 @@ function agentSpecificIssues(agent, agentDeckOnline, mcpRegistered) {
80
106
  message: "Run agent-deck setup --client claude --start (Claude MCP not registered)",
81
107
  });
82
108
  }
109
+ // Codex deck binding uses the Agent Deck marketplace plugin (not `agent-deck use --client codex`).
83
110
  return issues;
84
111
  }
85
112
  export async function healthForAgent(agent, agentDeckOnline, runtimeIssuesByRuntime, mcpRegistered) {
@@ -75,3 +75,19 @@ export function cursorBinExists() {
75
75
  const bin = resolveCursorBin();
76
76
  return bin !== "cursor-agent" && bin !== "cursor" ? fs.existsSync(bin) : false;
77
77
  }
78
+ /** Resolve Codex CLI binary (`codex` from OpenAI Codex install). */
79
+ export function resolveCodexBin() {
80
+ const home = process.env.HOME ?? os.homedir();
81
+ if (process.env.CODEX_CLI)
82
+ return process.env.CODEX_CLI;
83
+ return (firstExisting([
84
+ path.join(home, ".local/bin/codex"),
85
+ path.join(home, ".codex/bin/codex"),
86
+ "/opt/homebrew/bin/codex",
87
+ "/usr/local/bin/codex",
88
+ ]) ?? "codex");
89
+ }
90
+ export function codexBinExists() {
91
+ const bin = resolveCodexBin();
92
+ return bin !== "codex" ? fs.existsSync(bin) : false;
93
+ }
@@ -1,6 +1,6 @@
1
1
  import { test } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { cursorInvokeArgs } from "./cli-env.js";
3
+ import { cursorInvokeArgs, resolveCodexBin } from "./cli-env.js";
4
4
  test("cursorInvokeArgs passes through for cursor-agent", () => {
5
5
  const prev = process.env.CURSOR_CLI;
6
6
  process.env.CURSOR_CLI = "/home/user/.local/bin/cursor-agent";
@@ -28,3 +28,16 @@ test("cursorInvokeArgs prepends agent subcommand for legacy cursor shim", () =>
28
28
  process.env.CURSOR_CLI = prev;
29
29
  }
30
30
  });
31
+ test("resolveCodexBin respects CODEX_CLI override", () => {
32
+ const prev = process.env.CODEX_CLI;
33
+ process.env.CODEX_CLI = "/tmp/custom-codex";
34
+ try {
35
+ assert.equal(resolveCodexBin(), "/tmp/custom-codex");
36
+ }
37
+ finally {
38
+ if (prev === undefined)
39
+ delete process.env.CODEX_CLI;
40
+ else
41
+ process.env.CODEX_CLI = prev;
42
+ }
43
+ });
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import Database from "better-sqlite3";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
- import { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID, CURSOR_DEFAULT_MODEL, } from "@agent-dealer/shared";
7
+ import { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID, BUILTIN_AGENT_CODEX_ID, CURSOR_DEFAULT_MODEL, } from "@agent-dealer/shared";
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
  export function getDataDir() {
10
10
  const home = process.env.AGENT_DEALER_HOME ?? path.join(os.homedir(), ".agent-dealer");
@@ -117,6 +117,7 @@ function seedBuiltinAgents(db) {
117
117
  `);
118
118
  insert.run(BUILTIN_AGENT_CLAUDE_ID, "Claude", "claude_code", now, now);
119
119
  insert.run(BUILTIN_AGENT_CURSOR_ID, "Cursor", "cursor_local", now, now);
120
+ insert.run(BUILTIN_AGENT_CODEX_ID, "Codex", "codex_local", now, now);
120
121
  db.prepare(`UPDATE agents SET
121
122
  default_plan_model = COALESCE(default_plan_model, ?),
122
123
  default_execute_model = COALESCE(default_execute_model, ?),
@@ -3,7 +3,7 @@ import { addArtifact, getRun } from "../repository/runs.js";
3
3
  import { hasPendingQaExchange, latestExecuteSessionId } from "../repository/result-qa.js";
4
4
  import { runQa } from "../runners/qa.js";
5
5
  const ASKABLE_STATUSES = new Set(["review", "done"]);
6
- const QA_RUNTIMES = new Set(["claude_code", "cursor_local"]);
6
+ const QA_RUNTIMES = new Set(["claude_code", "cursor_local", "codex_local"]);
7
7
  /** Ask the run's agent about its finished result. Read-only; never changes run status. */
8
8
  export function askResultQuestion(runId, question, opts) {
9
9
  // An empty question would persist a result_qa artifact that ResultQaContent then
@@ -1,99 +1,15 @@
1
- import { spawn } from "node:child_process";
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import { buildExecutionPrompt, buildExecutionContinuationPrompt, buildPlanPrompt, buildReflectPrompt, workspaceForRun } from "./prompts.js";
5
4
  import { humanFeedbackText, lineageParentExecuteSessionId } from "./run-context.js";
6
- import { getTemporalLogsDir } from "../paths.js";
7
5
  import { cursorInvokeArgs, resolveClaudeBin, resolveCursorBin } from "../cli-env.js";
8
6
  import { resolveBudgetForPhase, resolveModelForPhase, getRun } from "../repository/runs.js";
9
7
  import { budgetCliArgs } from "@agent-dealer/shared";
10
8
  import { buildClaudePhaseArgs } from "./claude-args.js";
11
- import { acquireSpawnSlot, killRunProcess, registerChild, releaseSpawnSlot, unregisterChild, } from "./process-registry.js";
9
+ import { runCodex } from "./codex.js";
10
+ import { logPathFor, spawnCli, timeoutMsForMode, } from "./spawn-cli.js";
12
11
  export { getActiveLogPath, killRunProcess } from "./process-registry.js";
13
- function timeoutMsForMode(mode) {
14
- const envKey = mode === "plan"
15
- ? "PLAN_TIMEOUT_MS"
16
- : mode === "execute"
17
- ? "EXECUTE_TIMEOUT_MS"
18
- : mode === "reflect"
19
- ? "REFLECT_TIMEOUT_MS"
20
- : "QA_TIMEOUT_MS";
21
- const defaults = {
22
- plan: 15 * 60_000,
23
- execute: 60 * 60_000,
24
- reflect: 10 * 60_000,
25
- qa: 5 * 60_000,
26
- };
27
- const raw = process.env[envKey];
28
- if (raw !== undefined && raw !== "")
29
- return Number(raw);
30
- return defaults[mode];
31
- }
32
- async function spawnCli(runId, cmd, args, cwd, opts) {
33
- await acquireSpawnSlot();
34
- try {
35
- return await new Promise((resolve, reject) => {
36
- const stdoutChunks = [];
37
- const stderrChunks = [];
38
- let timedOut = false;
39
- let settled = false;
40
- const logStream = fs.createWriteStream(opts.logPath, { flags: "w" });
41
- const child = spawn(cmd, args, {
42
- cwd,
43
- env: { ...process.env },
44
- stdio: ["ignore", "pipe", "pipe"],
45
- });
46
- registerChild(runId, child, opts.logPath);
47
- const finish = (exitCode) => {
48
- if (settled)
49
- return;
50
- settled = true;
51
- clearTimeout(timer);
52
- unregisterChild(runId);
53
- logStream.end();
54
- const stderr = stderrChunks.join("");
55
- if (stderr.trim()) {
56
- logStream.write(`\n--- stderr ---\n${stderr}`);
57
- }
58
- resolve({
59
- exitCode,
60
- transcript: stdoutChunks.join(""),
61
- timedOut,
62
- });
63
- };
64
- const timer = setTimeout(() => {
65
- timedOut = true;
66
- killRunProcess(runId);
67
- setTimeout(() => finish(124), 500);
68
- }, opts.timeoutMs);
69
- child.stdout?.on("data", (buf) => {
70
- const chunk = buf.toString();
71
- stdoutChunks.push(chunk);
72
- logStream.write(buf);
73
- });
74
- child.stderr?.on("data", (buf) => {
75
- stderrChunks.push(buf.toString());
76
- });
77
- child.on("error", (err) => {
78
- if (settled)
79
- return;
80
- settled = true;
81
- clearTimeout(timer);
82
- unregisterChild(runId);
83
- logStream.end();
84
- reject(err);
85
- });
86
- child.on("close", (code) => finish(code ?? 1));
87
- });
88
- }
89
- finally {
90
- releaseSpawnSlot();
91
- }
92
- }
93
- function logPathFor(run, mode) {
94
- const logDir = getTemporalLogsDir();
95
- return path.join(logDir, `${run.id}-${mode}-${Date.now()}.ndjson`);
96
- }
12
+ export { logPathFor, spawnCli, timeoutMsForMode } from "./spawn-cli.js";
97
13
  export async function runClaude(run, mode = "execute", model, opts) {
98
14
  const mcpConfig = process.env.CLAUDE_MCP_CONFIG ?? path.join(process.env.HOME ?? "", ".claude.json");
99
15
  if (!fs.existsSync(mcpConfig)) {
@@ -152,6 +68,9 @@ export async function runAgent(run, mode = "execute", revise) {
152
68
  const model = resolveModelForPhase(fresh, mode);
153
69
  if (fresh.runtime === "cursor_local")
154
70
  return runCursor(fresh, mode, model);
71
+ if (fresh.runtime === "codex_local") {
72
+ return runCodex(fresh, mode, model ?? undefined, revise ? { promptOverride: revise.prompt, resumeSessionId: revise.resumeSessionId } : undefined);
73
+ }
155
74
  return runClaude(fresh, mode, model, revise ? { promptOverride: revise.prompt, resumeSessionId: revise.resumeSessionId } : undefined);
156
75
  }
157
76
  /** @deprecated use stream-json extractPlanMarkdown via persistRunOutput */
@@ -0,0 +1,59 @@
1
+ const FORBIDDEN_SANDBOX = "danger-full-access";
2
+ const FORBIDDEN_FLAGS = [
3
+ "--dangerously-bypass-approvals-and-sandbox",
4
+ "--dangerously-bypass-hook-trust",
5
+ ];
6
+ /** Phase → sandbox. Never returns danger-full-access. */
7
+ export function codexSandboxForMode(mode) {
8
+ return mode === "execute" ? "workspace-write" : "read-only";
9
+ }
10
+ /**
11
+ * Build `codex` argv for non-interactive exec.
12
+ * Caller prepends nothing — pass these to spawn(resolveCodexBin(), args).
13
+ */
14
+ export function buildCodexExecArgs(opts) {
15
+ const sandbox = codexSandboxForMode(opts.mode);
16
+ assertSafeSandbox(sandbox);
17
+ const args = [
18
+ "exec",
19
+ "--json",
20
+ "-C",
21
+ opts.workspaceRoot,
22
+ "-s",
23
+ sandbox,
24
+ ];
25
+ if (opts.model) {
26
+ args.push("-m", opts.model);
27
+ }
28
+ if (opts.outputSchemaPath) {
29
+ args.push("--output-schema", opts.outputSchemaPath);
30
+ }
31
+ if (opts.outputLastMessagePath) {
32
+ args.push("-o", opts.outputLastMessagePath);
33
+ }
34
+ for (const dir of opts.addDirs ?? []) {
35
+ args.push("--add-dir", dir);
36
+ }
37
+ // Prompt is user-controlled — do not include it in flag safety checks.
38
+ if (opts.resumeSessionId) {
39
+ args.push("resume", opts.resumeSessionId, opts.prompt);
40
+ }
41
+ else {
42
+ args.push(opts.prompt);
43
+ }
44
+ assertNoDangerFlags(args.slice(0, -1)); // exclude trailing prompt
45
+ return args;
46
+ }
47
+ function assertSafeSandbox(sandbox) {
48
+ if (sandbox === FORBIDDEN_SANDBOX) {
49
+ throw new Error("codex managed runner refused dangerous sandbox/bypass flags");
50
+ }
51
+ }
52
+ /** Validate generated option tokens only — never the prompt string. */
53
+ function assertNoDangerFlags(generatedArgs) {
54
+ for (const token of generatedArgs) {
55
+ if (token === FORBIDDEN_SANDBOX || FORBIDDEN_FLAGS.includes(token)) {
56
+ throw new Error("codex managed runner refused dangerous sandbox/bypass flags");
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,72 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { buildCodexExecArgs, codexSandboxForMode } from "./codex-args.js";
4
+ test("codexSandboxForMode maps plan/qa to read-only and execute to workspace-write", () => {
5
+ assert.equal(codexSandboxForMode("plan"), "read-only");
6
+ assert.equal(codexSandboxForMode("qa"), "read-only");
7
+ assert.equal(codexSandboxForMode("execute"), "workspace-write");
8
+ });
9
+ test("buildCodexExecArgs includes json, cd, sandbox, and prompt", () => {
10
+ const args = buildCodexExecArgs({
11
+ mode: "plan",
12
+ workspaceRoot: "/tmp/ws",
13
+ prompt: "draft a plan",
14
+ });
15
+ assert.deepEqual(args, [
16
+ "exec",
17
+ "--json",
18
+ "-C",
19
+ "/tmp/ws",
20
+ "-s",
21
+ "read-only",
22
+ "draft a plan",
23
+ ]);
24
+ });
25
+ test("buildCodexExecArgs resume places subcommand before prompt", () => {
26
+ const args = buildCodexExecArgs({
27
+ mode: "execute",
28
+ workspaceRoot: "/tmp/ws",
29
+ prompt: "fix the findings",
30
+ resumeSessionId: "thread-abc",
31
+ model: "gpt-5.6-sol",
32
+ });
33
+ assert.ok(args.includes("-s") && args[args.indexOf("-s") + 1] === "workspace-write");
34
+ assert.deepEqual(args.slice(-3), ["resume", "thread-abc", "fix the findings"]);
35
+ assert.ok(args.includes("-m") && args[args.indexOf("-m") + 1] === "gpt-5.6-sol");
36
+ });
37
+ test("buildCodexExecArgs supports output-schema and -o", () => {
38
+ const args = buildCodexExecArgs({
39
+ mode: "qa",
40
+ workspaceRoot: "/tmp/ws",
41
+ prompt: "answer",
42
+ outputSchemaPath: "/tmp/schema.json",
43
+ outputLastMessagePath: "/tmp/last.json",
44
+ addDirs: ["/tmp/extra"],
45
+ });
46
+ assert.ok(args.includes("--output-schema"));
47
+ assert.equal(args[args.indexOf("--output-schema") + 1], "/tmp/schema.json");
48
+ assert.ok(args.includes("-o"));
49
+ assert.equal(args[args.indexOf("-o") + 1], "/tmp/last.json");
50
+ assert.ok(args.includes("--add-dir"));
51
+ assert.equal(args[args.indexOf("--add-dir") + 1], "/tmp/extra");
52
+ assert.equal(args[args.indexOf("-s") + 1], "read-only");
53
+ });
54
+ test("buildCodexExecArgs never emits danger flags", () => {
55
+ const args = buildCodexExecArgs({
56
+ mode: "execute",
57
+ workspaceRoot: "/tmp/ws",
58
+ prompt: "go",
59
+ });
60
+ const joined = args.join(" ");
61
+ assert.equal(joined.includes("danger-full-access"), false);
62
+ assert.equal(joined.includes("dangerously-bypass"), false);
63
+ });
64
+ test("buildCodexExecArgs allows danger-full-access in the prompt text", () => {
65
+ const args = buildCodexExecArgs({
66
+ mode: "plan",
67
+ workspaceRoot: "/tmp/ws",
68
+ prompt: "verify that danger-full-access is never used",
69
+ });
70
+ assert.equal(args.at(-1), "verify that danger-full-access is never used");
71
+ assert.equal(args[args.indexOf("-s") + 1], "read-only");
72
+ });
@@ -0,0 +1,150 @@
1
+ export function parseCodexJsonl(raw) {
2
+ const stdoutOnly = stripStderrTrailer(raw);
3
+ const events = [];
4
+ for (const line of stdoutOnly.split("\n")) {
5
+ const t = line.trim();
6
+ if (!t)
7
+ continue;
8
+ try {
9
+ events.push(JSON.parse(t));
10
+ }
11
+ catch {
12
+ // skip non-json (e.g. trailing stderr markers if present)
13
+ }
14
+ }
15
+ return events;
16
+ }
17
+ /** spawnCli appends stderr after a marker — ignore that for JSONL parse. */
18
+ export function stripStderrTrailer(raw) {
19
+ const idx = raw.indexOf("\n--- stderr ---\n");
20
+ return idx >= 0 ? raw.slice(0, idx) : raw;
21
+ }
22
+ export function extractCodexThreadId(events) {
23
+ for (const e of events) {
24
+ if (e.type === "thread.started" && typeof e.thread_id === "string") {
25
+ return e.thread_id;
26
+ }
27
+ }
28
+ return undefined;
29
+ }
30
+ function itemOf(e) {
31
+ const item = e.item;
32
+ if (item && typeof item === "object")
33
+ return item;
34
+ return undefined;
35
+ }
36
+ export function extractCodexResultText(events) {
37
+ for (let i = events.length - 1; i >= 0; i--) {
38
+ const e = events[i];
39
+ if (e.type !== "item.completed")
40
+ continue;
41
+ const item = itemOf(e);
42
+ if (item?.type === "agent_message" && typeof item.text === "string" && item.text.length > 0) {
43
+ return item.text;
44
+ }
45
+ }
46
+ return undefined;
47
+ }
48
+ function usageFromTurn(events) {
49
+ for (let i = events.length - 1; i >= 0; i--) {
50
+ const e = events[i];
51
+ if (e.type !== "turn.completed")
52
+ continue;
53
+ const usage = e.usage;
54
+ if (!usage || typeof usage !== "object")
55
+ return undefined;
56
+ const u = usage;
57
+ const out = {};
58
+ if (typeof u.input_tokens === "number")
59
+ out.input_tokens = u.input_tokens;
60
+ if (typeof u.output_tokens === "number")
61
+ out.output_tokens = u.output_tokens;
62
+ if (typeof u.cached_input_tokens === "number")
63
+ out.cache_read_input_tokens = u.cached_input_tokens;
64
+ if (typeof u.reasoning_output_tokens === "number") {
65
+ out.output_tokens = (out.output_tokens ?? 0) + u.reasoning_output_tokens;
66
+ }
67
+ return Object.keys(out).length > 0 ? out : undefined;
68
+ }
69
+ return undefined;
70
+ }
71
+ function errorMessage(events) {
72
+ for (let i = events.length - 1; i >= 0; i--) {
73
+ const e = events[i];
74
+ if (e.type === "turn.failed") {
75
+ const err = e.error;
76
+ if (typeof err === "string")
77
+ return err;
78
+ if (err && typeof err === "object" && typeof err.message === "string") {
79
+ return err.message;
80
+ }
81
+ return "turn failed";
82
+ }
83
+ if (e.type === "error") {
84
+ if (typeof e.message === "string")
85
+ return e.message;
86
+ return "codex error";
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+ /**
92
+ * Map Codex JSONL events into Claude/Cursor-shaped NDJSON so persist extractors work unchanged.
93
+ */
94
+ export function normalizeCodexEvents(events) {
95
+ const out = [];
96
+ const threadId = extractCodexThreadId(events);
97
+ if (threadId) {
98
+ out.push({ type: "system", subtype: "init", session_id: threadId });
99
+ }
100
+ for (const e of events) {
101
+ if (e.type === "item.completed" || e.type === "item.started" || e.type === "item.updated") {
102
+ const item = itemOf(e);
103
+ if (!item)
104
+ continue;
105
+ const itemType = String(item.type ?? "");
106
+ if (itemType === "agent_message" && typeof item.text === "string" && item.text) {
107
+ out.push({
108
+ type: "assistant",
109
+ message: { content: [{ type: "text", text: item.text }] },
110
+ });
111
+ }
112
+ else if (itemType === "reasoning" && typeof item.text === "string" && item.text) {
113
+ out.push({ type: "thinking", text: item.text });
114
+ }
115
+ else if (itemType === "command_execution") {
116
+ const cmd = typeof item.command === "string" ? item.command : "command";
117
+ out.push({ type: "tool_call", name: "Bash", text: cmd });
118
+ }
119
+ else if (itemType === "mcp_tool_call") {
120
+ const name = typeof item.name === "string" ? item.name : "mcp";
121
+ out.push({ type: "tool_call", name });
122
+ }
123
+ else if (itemType === "file_change") {
124
+ out.push({ type: "tool_call", name: "Edit" });
125
+ }
126
+ }
127
+ }
128
+ const err = errorMessage(events);
129
+ const resultText = extractCodexResultText(events);
130
+ const usage = usageFromTurn(events);
131
+ if (err) {
132
+ out.push({
133
+ type: "result",
134
+ is_error: true,
135
+ result: err,
136
+ ...(usage ? { usage } : {}),
137
+ });
138
+ }
139
+ else if (resultText) {
140
+ out.push({
141
+ type: "result",
142
+ result: resultText,
143
+ ...(usage ? { usage } : {}),
144
+ });
145
+ }
146
+ else if (usage) {
147
+ out.push({ type: "result", result: "", usage });
148
+ }
149
+ return out;
150
+ }
@@ -0,0 +1,36 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { extractCodexResultText, extractCodexThreadId, normalizeCodexEvents, parseCodexJsonl, } from "./codex-jsonl.js";
4
+ import { extractSessionId, extractResultText, extractUsage } from "./stream-json.js";
5
+ const SAMPLE = `
6
+ {"type":"thread.started","thread_id":"0199a213-81c0-7800-8aa1-bbab2a035a53"}
7
+ {"type":"turn.started"}
8
+ {"type":"item.started","item":{"id":"item_1","type":"command_execution","command":"bash -lc ls","status":"in_progress"}}
9
+ {"type":"item.completed","item":{"id":"item_3","type":"agent_message","text":"Repo contains docs, sdk, and examples directories."}}
10
+ {"type":"turn.completed","usage":{"input_tokens":24763,"cached_input_tokens":24448,"output_tokens":122,"reasoning_output_tokens":0}}
11
+ `.trim();
12
+ test("parseCodexJsonl reads JSONL events", () => {
13
+ const events = parseCodexJsonl(SAMPLE);
14
+ assert.equal(events.length, 5);
15
+ assert.equal(extractCodexThreadId(events), "0199a213-81c0-7800-8aa1-bbab2a035a53");
16
+ assert.equal(extractCodexResultText(events), "Repo contains docs, sdk, and examples directories.");
17
+ });
18
+ test("normalizeCodexEvents feeds existing extractors", () => {
19
+ const normalized = normalizeCodexEvents(parseCodexJsonl(SAMPLE));
20
+ assert.equal(extractSessionId(normalized), "0199a213-81c0-7800-8aa1-bbab2a035a53");
21
+ assert.equal(extractResultText(normalized), "Repo contains docs, sdk, and examples directories.");
22
+ const usage = extractUsage(normalized, "plan", "codex_local");
23
+ assert.equal(usage.inputTokens, 24763);
24
+ assert.equal(usage.outputTokens, 122);
25
+ assert.equal(usage.cacheReadTokens, 24448);
26
+ });
27
+ test("normalizeCodexEvents marks turn.failed as error result", () => {
28
+ const raw = `
29
+ {"type":"thread.started","thread_id":"t1"}
30
+ {"type":"turn.failed","error":{"message":"sandbox denied"}}
31
+ `.trim();
32
+ const normalized = normalizeCodexEvents(parseCodexJsonl(raw));
33
+ const result = normalized.find((e) => e.type === "result");
34
+ assert.equal(result?.is_error, true);
35
+ assert.equal(result?.result, "sandbox denied");
36
+ });
@@ -0,0 +1,19 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { buildCodexExecArgs } from "./codex-args.js";
4
+ test("execute resume argv matches Claude-style fix-round handoff", () => {
5
+ const args = buildCodexExecArgs({
6
+ mode: "execute",
7
+ workspaceRoot: "/tmp/ws",
8
+ prompt: "Address the human feedback and continue.",
9
+ resumeSessionId: "0199a213-81c0-7800-8aa1-bbab2a035a53",
10
+ });
11
+ assert.equal(args[0], "exec");
12
+ assert.ok(args.includes("--json"));
13
+ assert.equal(args[args.indexOf("-s") + 1], "workspace-write");
14
+ assert.deepEqual(args.slice(-3), [
15
+ "resume",
16
+ "0199a213-81c0-7800-8aa1-bbab2a035a53",
17
+ "Address the human feedback and continue.",
18
+ ]);
19
+ });
@@ -0,0 +1,46 @@
1
+ import { resolveCodexBin } from "../cli-env.js";
2
+ import { getTemporalOutputDir } from "../paths.js";
3
+ import { buildCodexExecArgs } from "./codex-args.js";
4
+ import { logPathFor, spawnCli, timeoutMsForMode } from "./spawn-cli.js";
5
+ import { buildExecutionContinuationPrompt, buildExecutionPrompt, buildPlanPrompt, workspaceForRun, } from "./prompts.js";
6
+ import { humanFeedbackText, lineageParentExecuteSessionId } from "./run-context.js";
7
+ /**
8
+ * Resume session for Codex — mirrors Claude:
9
+ * explicit opts win; else execute + human feedback → lineage parent execute session.
10
+ */
11
+ export function resolveCodexResumeSessionId(run, mode, optsResumeSessionId) {
12
+ if (optsResumeSessionId)
13
+ return optsResumeSessionId;
14
+ if (mode === "execute" && humanFeedbackText(run)) {
15
+ return lineageParentExecuteSessionId(run) ?? undefined;
16
+ }
17
+ return undefined;
18
+ }
19
+ export async function runCodex(run, mode = "execute", model, opts) {
20
+ if (mode === "qa" && !opts?.promptOverride) {
21
+ throw new Error("qa mode requires a promptOverride");
22
+ }
23
+ const resumeSessionId = resolveCodexResumeSessionId(run, mode, opts?.resumeSessionId);
24
+ const prompt = opts?.promptOverride ??
25
+ (mode === "plan"
26
+ ? buildPlanPrompt(run)
27
+ : resumeSessionId
28
+ ? buildExecutionContinuationPrompt(run)
29
+ : buildExecutionPrompt(run));
30
+ const logPath = logPathFor(run, mode);
31
+ const workspaceRoot = workspaceForRun(run);
32
+ // Match Claude execute: temporal output dir must be writable for document artifacts.
33
+ const addDirs = opts?.addDirs ?? (mode === "execute" ? [getTemporalOutputDir()] : undefined);
34
+ const args = buildCodexExecArgs({
35
+ mode,
36
+ workspaceRoot,
37
+ prompt,
38
+ model,
39
+ resumeSessionId,
40
+ outputSchemaPath: opts?.outputSchemaPath,
41
+ outputLastMessagePath: opts?.outputLastMessagePath,
42
+ addDirs,
43
+ });
44
+ const { exitCode, transcript, timedOut } = await spawnCli(run.id, resolveCodexBin(), args, workspaceRoot, { logPath, timeoutMs: timeoutMsForMode(mode) });
45
+ return { exitCode, transcript, logPath, timedOut };
46
+ }
@@ -0,0 +1,14 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { parseCodexModelsCache } from "./models.js";
4
+ test("parseCodexModelsCache keeps visibility=list and drops hide", () => {
5
+ const models = parseCodexModelsCache({
6
+ models: [
7
+ { slug: "gpt-reserve", display_name: "GPT-Reserve", visibility: "hide" },
8
+ { slug: "gpt-5.6-sol", display_name: "GPT-5.6-Sol", visibility: "list" },
9
+ { slug: "codex-auto-review", display_name: "Codex Auto Review", visibility: "hide" },
10
+ { slug: "gpt-5.6-terra", display_name: "GPT-5.6-Terra", visibility: "list" },
11
+ ],
12
+ });
13
+ assert.deepEqual(models.map((m) => m.id), ["gpt-5.6-sol", "gpt-5.6-terra"]);
14
+ });