@dadado/agent-kit-cli 5.17.0 → 6.0.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 (62) hide show
  1. package/LICENSE +201 -74
  2. package/NOTICE +2 -0
  3. package/README.md +3 -3
  4. package/dashboard/README.md +10 -1
  5. package/dashboard/dashboard-data.mjs +23 -13
  6. package/dashboard/dashboard.html +58 -0
  7. package/dashboard/lib/processes.mjs +1 -1
  8. package/dashboard/lib/run-logs.d.mts +34 -0
  9. package/dashboard/lib/run-logs.mjs +218 -0
  10. package/dashboard/lib/semantic-model.mjs +4 -3
  11. package/dashboard/serve.mjs +4 -4
  12. package/dashboard/start-broadcast.mjs +1 -1
  13. package/dashboard/start.mjs +1 -1
  14. package/dist/{add-HTYIFKMN.js → add-YTCDLPUS.js} +4 -4
  15. package/dist/{chunk-SMGFN3Z4.js → chunk-2FQX2O4I.js} +4 -4
  16. package/dist/{chunk-PNG7LO2F.js → chunk-3DGTPONS.js} +1 -1
  17. package/dist/{chunk-VY7MTPJ2.js → chunk-426GITN7.js} +1 -1
  18. package/dist/{chunk-2IPXQFJE.js → chunk-4CRBUEJ6.js} +2 -2
  19. package/dist/{chunk-STGFBAYT.js → chunk-4U7DLAQH.js} +11 -19
  20. package/dist/{chunk-77V75A7O.js → chunk-5B4DVO6C.js} +1 -1
  21. package/dist/{chunk-4IWG5CVE.js → chunk-B3VR5YBQ.js} +10 -5
  22. package/dist/{chunk-OUQZC744.js → chunk-B5CH47OS.js} +852 -74
  23. package/dist/{chunk-2RDRMGD3.js → chunk-BBVBYQJO.js} +2 -2
  24. package/dist/chunk-C26I6HTO.js +39 -0
  25. package/dist/{chunk-AW74INUW.js → chunk-DBPNGZNH.js} +6 -5
  26. package/dist/{chunk-DQA434Y4.js → chunk-DBXDV5O7.js} +3 -3
  27. package/dist/{chunk-S6GMFDKV.js → chunk-DRND2KYV.js} +2 -2
  28. package/dist/{chunk-F4VMYR7U.js → chunk-GN4SAZF2.js} +4 -4
  29. package/dist/{chunk-ZXKZBTXE.js → chunk-HAFRAXGV.js} +1 -1
  30. package/dist/{chunk-CBCW5R5J.js → chunk-MCH3E46V.js} +2 -2
  31. package/dist/{chunk-WW3YZKHW.js → chunk-NDIGZXFF.js} +26 -5
  32. package/dist/{chunk-TFZ2JUXH.js → chunk-PAB35H27.js} +523 -73
  33. package/dist/{chunk-NQ7IQ5XT.js → chunk-PFEUZXVT.js} +1 -1
  34. package/dist/{chunk-W24CMQ32.js → chunk-PPKX55GI.js} +3 -3
  35. package/dist/{chunk-ZCTSUQRR.js → chunk-T6P5ZMG2.js} +3 -3
  36. package/dist/{chunk-N3HTJVBR.js → chunk-VTOUQAVR.js} +1 -1
  37. package/dist/{chunk-B73KQ5KX.js → chunk-WFXHMP47.js} +11 -11
  38. package/dist/chunk-WGLPZXNU.js +45 -0
  39. package/dist/{chunk-KD2MYBB6.js → chunk-WUWRGOUV.js} +2 -2
  40. package/dist/{contribute-C6AW2P6J.js → contribute-A7NCWTUM.js} +5 -5
  41. package/dist/{cursor-awareness-KTL2RGKD.js → cursor-awareness-TN5JXD2C.js} +3 -3
  42. package/dist/{diff-VHNR62SI.js → diff-4MDU5SLE.js} +6 -6
  43. package/dist/{doctor-PET2HRRJ.js → doctor-MQAXOFKM.js} +16 -14
  44. package/dist/{dogfood-KKFVRWT6.js → dogfood-2JAHSYQA.js} +4 -4
  45. package/dist/{handoff-3UI6VKVG.js → handoff-MK5JG3J5.js} +4 -4
  46. package/dist/{hook-DYIMKJYL.js → hook-D2WHBKRN.js} +12 -12
  47. package/dist/index.js +26 -24
  48. package/dist/{init-Q4LBM6KT.js → init-7T6KNDFE.js} +17 -16
  49. package/dist/install-6G3D7J4U.js +35 -0
  50. package/dist/{monitors-QV2IS3MP.js → monitors-PDXBLB3U.js} +1 -1
  51. package/dist/{plan-index-JMC7Z4DW.js → plan-index-V7T5MKL6.js} +3 -3
  52. package/dist/{public-inbound-radar-WP3XF7GS.js → public-inbound-radar-5U4JMBRU.js} +3 -3
  53. package/dist/{remote-issues-SZ4HDQL5.js → remote-issues-APYOMIJC.js} +4 -4
  54. package/dist/{run-I3BFGULT.js → run-MWOI7UW5.js} +7 -6
  55. package/dist/{run-plan-FO7CL4M3.js → run-plan-UWQD3BIJ.js} +37 -6
  56. package/dist/{scan-NI2SWJ2E.js → scan-3MIUE7OR.js} +2 -2
  57. package/dist/{setup-global-M42GEODI.js → setup-global-AT4DD3MG.js} +9 -9
  58. package/dist/{status-XPVQVQMX.js → status-MXNTBKPL.js} +11 -11
  59. package/dist/{update-FJ2EBZVO.js → update-P62637ZI.js} +12 -12
  60. package/package.json +10 -6
  61. package/dist/chunk-5VCOWTTP.js +0 -6
  62. package/dist/install-RHOS3OYZ.js +0 -34
@@ -1,3 +1,6 @@
1
+ import {
2
+ findOnPath
3
+ } from "./chunk-WGLPZXNU.js";
1
4
  import {
2
5
  HEADLESS_DENY_RULES
3
6
  } from "./chunk-MAPOQEA2.js";
@@ -38,10 +41,10 @@ var LineSplitter = class {
38
41
  onLine(segment);
39
42
  } else {
40
43
  this.parts.push(segment);
41
- const line = this.parts.join("");
44
+ const line3 = this.parts.join("");
42
45
  this.parts = [];
43
46
  this.length = 0;
44
- onLine(line);
47
+ onLine(line3);
45
48
  }
46
49
  start = nl + 1;
47
50
  nl = text.indexOf("\n", start);
@@ -80,17 +83,20 @@ var FALLBACK_WORDING_RES = [
80
83
  /skip or cancel means stop/i
81
84
  ];
82
85
  var TICK_SENTINEL_RE = /LOOP_TICK_RESULT:/;
83
- function parseHitlGateLine(line) {
84
- const m = HITL_GATE_LINE_RE.exec(line.trim());
86
+ function parseHitlGateLine(line3) {
87
+ const m = HITL_GATE_LINE_RE.exec(line3.trim());
85
88
  if (!m?.[1] || !m[2]) return null;
86
89
  const labels = m[2].split("|").map((s) => s.trim()).filter((s) => s.length > 0);
87
90
  if (labels.length === 0) return null;
88
91
  return { askId: m[1], labels, detection: "sentinel" };
89
92
  }
93
+ function formatHitlGateLine(gate) {
94
+ return `HITL_GATE: ${gate.askId} | ${gate.labels.join(" | ")}`;
95
+ }
90
96
  function findHitlGateInText(text) {
91
97
  let last = null;
92
- for (const line of text.split(/\r?\n/)) {
93
- const gate = parseHitlGateLine(line);
98
+ for (const line3 of text.split(/\r?\n/)) {
99
+ const gate = parseHitlGateLine(line3);
94
100
  if (gate) last = gate;
95
101
  }
96
102
  return last;
@@ -107,8 +113,8 @@ function detectFallbackGate(text) {
107
113
  if (!FALLBACK_WORDING_RES.some((re) => re.test(text))) return null;
108
114
  const labels = [];
109
115
  let expected = 1;
110
- for (const line of text.split(/\r?\n/)) {
111
- const m = NUMBERED_LINE_RE.exec(line);
116
+ for (const line3 of text.split(/\r?\n/)) {
117
+ const m = NUMBERED_LINE_RE.exec(line3);
112
118
  if (!m?.[1] || !m[2]) continue;
113
119
  const n = Number(m[1]);
114
120
  if (n === 1) {
@@ -192,7 +198,7 @@ function createTerminalAnswerReader(opts = {}) {
192
198
  readOpts.signal?.addEventListener("abort", onAbort, { once: true });
193
199
  rl.on("SIGINT", () => finish({ kind: "sigint" }));
194
200
  rl.on("close", () => finish({ kind: "eof" }));
195
- rl.question("> ", (line) => finish({ kind: "line", text: line }));
201
+ rl.question("> ", (line3) => finish({ kind: "line", text: line3 }));
196
202
  });
197
203
  }
198
204
  function emptyHitlRecord() {
@@ -222,6 +228,15 @@ function unansweredStop(askId, cause) {
222
228
  exitCode: HITL_UNANSWERED_EXIT_CODE
223
229
  };
224
230
  }
231
+ function driverStop(askId, raw) {
232
+ const detail = raw.detail ? `: ${raw.detail}` : "";
233
+ return {
234
+ askId,
235
+ cause: raw.cause,
236
+ message: raw.cause === "driver stop" ? `stopped-by-operator: gate ${askId}, no reply (driver stop)` : `stopped: unanswered gate (driver protocol error${detail})`,
237
+ exitCode: HITL_UNANSWERED_EXIT_CODE
238
+ };
239
+ }
225
240
  function noRelayStop(askId, backend) {
226
241
  return {
227
242
  askId,
@@ -251,7 +266,9 @@ function formatGatePrompt(gate) {
251
266
  async function askOperator(gate, opts) {
252
267
  if (isReservedGateId(gate.askId)) return { kind: "stop", stop: reservedStop(gate.askId) };
253
268
  if (opts.policy === "off") return { kind: "stop", stop: unansweredStop(gate.askId, "--no-hitl") };
254
- if (!opts.isTTY) return { kind: "stop", stop: unansweredStop(gate.askId, "no TTY") };
269
+ if (!opts.isTTY && opts.policy !== "driver") {
270
+ return { kind: "stop", stop: unansweredStop(gate.askId, "no TTY") };
271
+ }
255
272
  opts.onPromptStart?.();
256
273
  let raw;
257
274
  try {
@@ -265,14 +282,15 @@ async function askOperator(gate, opts) {
265
282
  }
266
283
  if (raw.kind === "eof") return { kind: "stop", stop: operatorStop(gate.askId, "EOF") };
267
284
  if (raw.kind === "sigint") return { kind: "stop", stop: operatorStop(gate.askId, "SIGINT") };
285
+ if (raw.kind === "stop") return { kind: "stop", stop: driverStop(gate.askId, raw) };
268
286
  const answer = resolveOperatorAnswer(raw.text, gate.labels);
269
287
  if (answer.kind === "stop") return { kind: "stop", stop: operatorStop(gate.askId, answer.cause) };
270
- const line = formatHitlReply(gate.askId, answer);
288
+ const line3 = formatHitlReply(gate.askId, answer);
271
289
  const stamp = {
272
290
  askId: gate.askId,
273
291
  reply: answer.kind === "reply" ? `operator reply ${answer.n}` : "operator reply other",
274
292
  label: answer.kind === "reply" ? answer.label : answer.text,
275
- line,
293
+ line: line3,
276
294
  at: (/* @__PURE__ */ new Date()).toISOString(),
277
295
  detection: gate.detection
278
296
  };
@@ -308,15 +326,15 @@ var TurnWatcher = class {
308
326
  feed(chunk) {
309
327
  const text = typeof chunk === "string" ? chunk : this.decoder.write(chunk);
310
328
  if (!text) return;
311
- this.lines.feed(text, (line) => this.take(line));
329
+ this.lines.feed(text, (line3) => this.take(line3));
312
330
  }
313
331
  end() {
314
- this.lines.feed(this.decoder.end(), (line) => this.take(line));
332
+ this.lines.feed(this.decoder.end(), (line3) => this.take(line3));
315
333
  const rest = this.lines.end();
316
334
  if (rest.trim()) this.take(rest);
317
335
  }
318
- take(line) {
319
- const trimmed = line.trim();
336
+ take(line3) {
337
+ const trimmed = line3.trim();
320
338
  if (!trimmed.startsWith("{")) return;
321
339
  let event;
322
340
  try {
@@ -354,10 +372,335 @@ var TurnWatcher = class {
354
372
  };
355
373
 
356
374
  // src/plan-loop/backends.ts
357
- import { execFileSync, spawn } from "child_process";
358
- import { createWriteStream } from "fs";
375
+ import { execFileSync as execFileSync2, spawn } from "child_process";
376
+ import { createWriteStream as createWriteStream2 } from "fs";
377
+ import { access as access2 } from "fs/promises";
378
+ import path2 from "path";
379
+ import { StringDecoder as StringDecoder4 } from "string_decoder";
380
+
381
+ // src/plan-loop/codex.ts
382
+ import { execFileSync } from "child_process";
359
383
  import { access } from "fs/promises";
360
384
  import path from "path";
385
+
386
+ // src/plan-loop/vendor-cli-pins.ts
387
+ var VENDOR_CLI_PINS = {
388
+ claude: {
389
+ bin: "claude",
390
+ min: "2.1.259",
391
+ tested: "2.1.292",
392
+ testedOn: "2026-10-08 `claude --version` (Linux)",
393
+ versionRe: /(\d+\.\d+\.\d+)/
394
+ },
395
+ "cursor-agent": {
396
+ bin: "cursor-agent",
397
+ min: null,
398
+ tested: "2026.10.01",
399
+ testedOn: "2026-10-08 `cursor-agent --version` = 2026.10.01-e373342 (Linux)",
400
+ versionRe: /(\d{4}\.\d{2}\.\d{2})(?:-[0-9a-f]+)?/
401
+ },
402
+ codex: {
403
+ bin: "codex",
404
+ min: null,
405
+ tested: "0.160.1",
406
+ testedOn: "2026-10-08 `codex --version` = codex-cli 0.160.1 (Linux)",
407
+ versionRe: /(\d+\.\d+\.\d+)/
408
+ }
409
+ };
410
+ function parseVendorVersion(id, raw) {
411
+ if (!raw) return null;
412
+ return VENDOR_CLI_PINS[id].versionRe.exec(raw)?.[1] ?? null;
413
+ }
414
+ function compareVendorVersions(a, b) {
415
+ const pa = a.split(/\D+/).filter(Boolean).map(Number);
416
+ const pb = b.split(/\D+/).filter(Boolean).map(Number);
417
+ for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
418
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
419
+ if (d !== 0) return d;
420
+ }
421
+ return 0;
422
+ }
423
+ function vendorVersionStatus(id, raw) {
424
+ const pin = VENDOR_CLI_PINS[id];
425
+ const version = parseVendorVersion(id, raw);
426
+ if (!version) return { status: "unknown", version: null };
427
+ if (pin.min && compareVendorVersions(version, pin.min) < 0) {
428
+ return {
429
+ status: "too-old",
430
+ version,
431
+ message: `${pin.bin} ${version} is older than ${pin.min}, the oldest version the headless runner supports.`
432
+ };
433
+ }
434
+ if (compareVendorVersions(version, pin.tested) > 0) {
435
+ return {
436
+ status: "newer-than-tested",
437
+ version,
438
+ message: `${pin.bin} ${version} is newer than ${pin.tested}, the last version the kit's headless contract was checked against; continuing (a changed output format is named here first).`
439
+ };
440
+ }
441
+ return { status: "supported", version };
442
+ }
443
+
444
+ // src/plan-loop/codex.ts
445
+ var CODEX_CHATGPT_LOGIN_ENV = "AGENT_KIT_CODEX_CHATGPT_LOGIN";
446
+ var CODEX_MAX_RESUMES = 50;
447
+ function codexExecArgs(opts) {
448
+ const args = [
449
+ "exec",
450
+ "--json",
451
+ "--color",
452
+ "never",
453
+ "--cd",
454
+ opts.workspace,
455
+ "--dangerously-bypass-approvals-and-sandbox"
456
+ ];
457
+ if (opts.model) args.push("--model", opts.model);
458
+ args.push("-");
459
+ return args;
460
+ }
461
+ function codexResumeArgs(opts) {
462
+ const args = ["exec", "resume", "--json", "--dangerously-bypass-approvals-and-sandbox"];
463
+ if (opts.model) args.push("--model", opts.model);
464
+ args.push(opts.threadId, "-");
465
+ return args;
466
+ }
467
+ function resolveCodexAuth(env) {
468
+ if (env[CODEX_CHATGPT_LOGIN_ENV] === "1") return { mode: "chatgpt-login", env };
469
+ const key = env.CODEX_API_KEY || env.OPENAI_API_KEY;
470
+ if (!key) {
471
+ return {
472
+ mode: "refused",
473
+ message: `codex backend: API-key mode needs OPENAI_API_KEY (or CODEX_API_KEY) in the environment. Using a ChatGPT sign-in is off by default until OpenAI clears it for third-party apps; set ${CODEX_CHATGPT_LOGIN_ENV}=1 to run on your own codex login.`
474
+ };
475
+ }
476
+ return { mode: "api-key", env: { ...env, CODEX_API_KEY: key } };
477
+ }
478
+ function codexSlashPointer(name) {
479
+ return [
480
+ `Read \`.cursor/commands/${name}.md\` now and follow that contract exactly \u2014 it is the source of truth for /${name}; this paragraph is only a thin adapter for the Codex CLI.`,
481
+ "",
482
+ "Adapter rules (Codex CLI, headless):",
483
+ "- Ask questions is unavailable here: print one line `HITL_GATE: <ask-id> | <label 1> | <label 2> | ...` immediately followed by the same labels as one numbered list, one list per message, then end your turn and WAIT for the answer (ask-id and labels: `.cursor/skills/core/hitl-gates/SKILL.md`). The answer arrives as your next prompt, `HITL_REPLY: <ask-id> | operator reply <n> | <label>`; cite that line as the Ask provenance.",
484
+ "- Skip or cancel means stop.",
485
+ "- Never `/git-prod` without an explicit operator yes.",
486
+ "- Do not clone Cursor hooks or invent behavior beyond the SoT file."
487
+ ].join("\n");
488
+ }
489
+ async function codexPrompt(workspace, prompt) {
490
+ const m = /^\/([A-Za-z0-9][A-Za-z0-9_-]*)/.exec(prompt.trimStart());
491
+ if (!m?.[1]) return { ok: true, prompt };
492
+ const rel = path.join(".cursor", "commands", `${m[1]}.md`);
493
+ try {
494
+ await access(path.join(workspace, rel));
495
+ } catch {
496
+ return { ok: false, missing: rel };
497
+ }
498
+ return { ok: true, prompt: `${codexSlashPointer(m[1])}
499
+
500
+ ${prompt}` };
501
+ }
502
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
503
+ var line = (event) => JSON.stringify(event);
504
+ function createCodexTranslator(onThread) {
505
+ let lastMessage = "";
506
+ let threadId = "";
507
+ return (raw) => {
508
+ const text = raw.trim();
509
+ if (!text.startsWith("{")) return [];
510
+ let event;
511
+ try {
512
+ event = JSON.parse(text);
513
+ } catch {
514
+ return [];
515
+ }
516
+ if (!isRecord(event)) return [];
517
+ const item = isRecord(event.item) ? event.item : null;
518
+ const itemType = item ? item.type ?? item.item_type : void 0;
519
+ const isMessage = itemType === "agent_message" || itemType === "assistant_message";
520
+ switch (event.type) {
521
+ case "thread.started":
522
+ if (typeof event.thread_id === "string") {
523
+ threadId = event.thread_id;
524
+ onThread(threadId);
525
+ }
526
+ return [line({ type: "system", subtype: "init", session_id: threadId, model: "codex" })];
527
+ case "item.started":
528
+ if (itemType === "command_execution" && typeof item?.command === "string") {
529
+ return [
530
+ line({
531
+ type: "assistant",
532
+ message: {
533
+ content: [{ type: "tool_use", name: "shell", input: { command: item.command } }]
534
+ }
535
+ })
536
+ ];
537
+ }
538
+ return [];
539
+ case "item.completed":
540
+ if (isMessage && typeof item?.text === "string") {
541
+ lastMessage = item.text;
542
+ return [
543
+ line({ type: "assistant", message: { content: [{ type: "text", text: item.text }] } })
544
+ ];
545
+ }
546
+ if (item && itemType === "command_execution") {
547
+ const exit = typeof item.exit_code === "number" ? item.exit_code : null;
548
+ return [
549
+ line({
550
+ type: "user",
551
+ message: {
552
+ content: [
553
+ {
554
+ type: "tool_result",
555
+ content: typeof item.aggregated_output === "string" ? item.aggregated_output : "",
556
+ is_error: exit !== null && exit !== 0
557
+ }
558
+ ]
559
+ }
560
+ })
561
+ ];
562
+ }
563
+ return [];
564
+ case "turn.completed": {
565
+ const result = lastMessage;
566
+ lastMessage = "";
567
+ return [
568
+ line({
569
+ type: "result",
570
+ subtype: "success",
571
+ is_error: false,
572
+ result,
573
+ session_id: threadId,
574
+ ...isRecord(event.usage) ? { usage: event.usage } : {}
575
+ })
576
+ ];
577
+ }
578
+ case "turn.failed": {
579
+ const message = isRecord(event.error) && typeof event.error.message === "string" ? event.error.message : "codex turn failed";
580
+ lastMessage = "";
581
+ return [
582
+ line({
583
+ type: "result",
584
+ subtype: "error_during_execution",
585
+ is_error: true,
586
+ result: message,
587
+ errors: [message],
588
+ session_id: threadId
589
+ })
590
+ ];
591
+ }
592
+ default:
593
+ return [];
594
+ }
595
+ };
596
+ }
597
+ var codexVersionCache = /* @__PURE__ */ new Map();
598
+ function readCodexVersion(bin) {
599
+ if (!codexVersionCache.has(bin)) {
600
+ let out = null;
601
+ try {
602
+ out = execFileSync(bin, ["--version"], {
603
+ encoding: "utf8",
604
+ stdio: ["ignore", "pipe", "ignore"],
605
+ timeout: 1e4
606
+ });
607
+ } catch {
608
+ out = null;
609
+ }
610
+ codexVersionCache.set(bin, out);
611
+ }
612
+ return codexVersionCache.get(bin) ?? null;
613
+ }
614
+ function mergeRecords(records) {
615
+ const merged = emptyHitlRecord();
616
+ for (const r of records) {
617
+ merged.replies.push(...r.replies);
618
+ merged.fallbackDetections += r.fallbackDetections;
619
+ if (r.stop) merged.stop = r.stop;
620
+ }
621
+ return merged;
622
+ }
623
+ var codexBackend = {
624
+ id: "codex",
625
+ async resolve() {
626
+ return whichBinary("codex");
627
+ },
628
+ async run(opts) {
629
+ const log = opts.log ?? ((text) => console.log(text));
630
+ const auth = resolveCodexAuth(mergeChildEnv(opts.env));
631
+ if (auth.mode === "refused") throw new Error(auth.message);
632
+ const env = auth.env;
633
+ const redact = claudeRedactions(env);
634
+ const pin = vendorVersionStatus("codex", (opts.versionFn ?? readCodexVersion)("codex"));
635
+ if (pin.status === "newer-than-tested" || pin.status === "too-old") log(pin.message);
636
+ const prepared = await codexPrompt(opts.workspace, opts.prompt);
637
+ if (!prepared.ok) {
638
+ throw new Error(
639
+ `codex backend: ${prepared.missing} is missing in the workspace, so the tick's leading slash has no contract to point codex at. Install the kit (\`agent-kit install\`) and retry.`
640
+ );
641
+ }
642
+ log(
643
+ `codex tick: codex exec --json (no approvals, resume relay, ${auth.mode === "api-key" ? "API key" : "ChatGPT sign-in, opt-in"}).`
644
+ );
645
+ let threadId = null;
646
+ const translate = createCodexTranslator((id) => {
647
+ threadId = id;
648
+ });
649
+ const hitl = resolveHitlRunOptions(opts.hitl, { kind: "resume", backend: "codex" });
650
+ const common = {
651
+ cwd: opts.workspace,
652
+ env,
653
+ redact,
654
+ spawnFn: opts.spawnFn,
655
+ hitl,
656
+ render: opts.render,
657
+ onSpawn: opts.onSpawn,
658
+ backendId: "codex",
659
+ translate
660
+ };
661
+ const records = [];
662
+ let result;
663
+ try {
664
+ result = await spawnLogged(
665
+ "codex",
666
+ codexExecArgs({ workspace: opts.workspace, model: opts.model }),
667
+ opts.logPath,
668
+ { ...common, stdinText: prepared.prompt }
669
+ );
670
+ if (result.hitl) records.push(result.hitl);
671
+ for (let i = 0; result.resumeWith && i < CODEX_MAX_RESUMES; i += 1) {
672
+ const reply = result.resumeWith;
673
+ const thread = threadId;
674
+ if (!thread) {
675
+ const record = emptyHitlRecord();
676
+ record.stop = {
677
+ ...unansweredStop("none", "child exited"),
678
+ message: "stopped: codex reported no thread id, so the reply could not be delivered"
679
+ };
680
+ records.push(record);
681
+ break;
682
+ }
683
+ result = await spawnLogged(
684
+ "codex",
685
+ codexResumeArgs({ threadId: thread, model: opts.model }),
686
+ opts.logPath,
687
+ { ...common, stdinText: reply, appendLog: true }
688
+ );
689
+ if (result.hitl) records.push(result.hitl);
690
+ }
691
+ } catch (err) {
692
+ throw new Error(`codex backend failed to start: ${redactSecrets(String(err), redact)}`);
693
+ }
694
+ return {
695
+ exitCode: result.exitCode,
696
+ hitl: mergeRecords(records),
697
+ ...result.lastResult ? { lastResult: result.lastResult } : {}
698
+ };
699
+ }
700
+ };
701
+
702
+ // src/plan-loop/cursor-acp.ts
703
+ import { createWriteStream } from "fs";
361
704
  import { StringDecoder as StringDecoder3 } from "string_decoder";
362
705
 
363
706
  // src/plan-loop/stream-render.ts
@@ -365,7 +708,7 @@ import { StringDecoder as StringDecoder2 } from "string_decoder";
365
708
  import { trueColor } from "kolorist";
366
709
 
367
710
  // src/plan-loop/stream-events.ts
368
- function isRecord(value) {
711
+ function isRecord2(value) {
369
712
  return typeof value === "object" && value !== null && !Array.isArray(value);
370
713
  }
371
714
  function asString(value) {
@@ -390,7 +733,7 @@ var SUMMARY_KEYS = [
390
733
  ];
391
734
  function summarizeToolInput(input) {
392
735
  if (typeof input === "string") return oneLine(input);
393
- if (!isRecord(input)) return input === void 0 ? "" : oneLine(JSON.stringify(input));
736
+ if (!isRecord2(input)) return input === void 0 ? "" : oneLine(JSON.stringify(input));
394
737
  for (const key of SUMMARY_KEYS) {
395
738
  const value = input[key];
396
739
  if (typeof value === "string" && value.trim()) return oneLine(value);
@@ -403,31 +746,31 @@ function firstLineOfContent(content) {
403
746
  if (typeof content === "string") return firstNonEmptyLine(content);
404
747
  if (Array.isArray(content)) {
405
748
  for (const part of content) {
406
- if (isRecord(part) && typeof part.text === "string") {
407
- const line = firstNonEmptyLine(part.text);
408
- if (line) return line;
749
+ if (isRecord2(part) && typeof part.text === "string") {
750
+ const line3 = firstNonEmptyLine(part.text);
751
+ if (line3) return line3;
409
752
  }
410
753
  }
411
754
  return "";
412
755
  }
413
- if (isRecord(content)) return oneLine(JSON.stringify(content));
756
+ if (isRecord2(content)) return oneLine(JSON.stringify(content));
414
757
  return "";
415
758
  }
416
759
  function firstNonEmptyLine(text) {
417
- for (const line of text.split(/\r?\n/)) {
418
- const trimmed = line.trim();
760
+ for (const line3 of text.split(/\r?\n/)) {
761
+ const trimmed = line3.trim();
419
762
  if (trimmed) return trimmed;
420
763
  }
421
764
  return "";
422
765
  }
423
766
  function parseAssistant(event) {
424
767
  const message = event.message;
425
- const content = isRecord(message) ? message.content : void 0;
768
+ const content = isRecord2(message) ? message.content : void 0;
426
769
  if (typeof content === "string") return content ? [{ kind: "text", text: content }] : [];
427
770
  if (!Array.isArray(content)) return [{ kind: "silent" }];
428
771
  const out = [];
429
772
  for (const part of content) {
430
- if (!isRecord(part)) continue;
773
+ if (!isRecord2(part)) continue;
431
774
  const type = asString(part.type);
432
775
  if (type === "text" && typeof part.text === "string") {
433
776
  if (part.text) out.push({ kind: "text", text: part.text });
@@ -444,7 +787,7 @@ function parseAssistant(event) {
444
787
  var HITL_REPLY_PREFIX2 = "HITL_REPLY:";
445
788
  function parseUser(event) {
446
789
  const message = event.message;
447
- const content = isRecord(message) ? message.content : void 0;
790
+ const content = isRecord2(message) ? message.content : void 0;
448
791
  if (typeof content === "string") {
449
792
  if (event.isReplay === true && content.startsWith(HITL_REPLY_PREFIX2)) {
450
793
  return [{ kind: "hitl_reply", line: oneLine(content) }];
@@ -455,7 +798,7 @@ function parseUser(event) {
455
798
  const out = [];
456
799
  let promptChars = 0;
457
800
  for (const part of content) {
458
- if (!isRecord(part)) continue;
801
+ if (!isRecord2(part)) continue;
459
802
  const type = asString(part.type);
460
803
  if (type === "tool_result") {
461
804
  out.push({
@@ -504,16 +847,16 @@ function percent(value) {
504
847
  return n === void 0 ? null : `${Math.round(n * 100)}%`;
505
848
  }
506
849
  function parseRateLimit(event) {
507
- const info = isRecord(event.rate_limit_info) ? event.rate_limit_info : event;
850
+ const info = isRecord2(event.rate_limit_info) ? event.rate_limit_info : event;
508
851
  const status = asString(info.status) ?? "unknown";
509
852
  const type = asString(info.rateLimitType);
510
853
  const parts = [];
511
854
  if (type) parts.push(type);
512
855
  parts.push(status);
513
- const windows = isRecord(info.unifiedWindows) ? info.unifiedWindows : null;
856
+ const windows = isRecord2(info.unifiedWindows) ? info.unifiedWindows : null;
514
857
  if (windows) {
515
858
  for (const [name, window] of Object.entries(windows)) {
516
- if (!isRecord(window)) continue;
859
+ if (!isRecord2(window)) continue;
517
860
  const used = percent(window.utilization);
518
861
  if (used) parts.push(`${name} ${used}`);
519
862
  }
@@ -521,30 +864,30 @@ function parseRateLimit(event) {
521
864
  return { kind: "rate_limit", line: parts.join(" \xB7 "), severe: status !== "allowed" };
522
865
  }
523
866
  function parseCursorToolCall(event) {
524
- const call = isRecord(event.tool_call) ? event.tool_call : null;
525
- const entry = call ? Object.entries(call).find(([, v]) => isRecord(v)) : void 0;
867
+ const call = isRecord2(event.tool_call) ? event.tool_call : null;
868
+ const entry = call ? Object.entries(call).find(([, v]) => isRecord2(v)) : void 0;
526
869
  const name = entry ? entry[0].replace(/ToolCall$/, "") : "tool";
527
- const body = entry && isRecord(entry[1]) ? entry[1] : {};
870
+ const body = entry && isRecord2(entry[1]) ? entry[1] : {};
528
871
  const subtype = asString(event.subtype) ?? "";
529
872
  if (subtype === "started") {
530
873
  return { kind: "tool_use", name, summary: summarizeToolInput(body.args) };
531
874
  }
532
875
  if (subtype === "completed") {
533
- const result = isRecord(body.result) ? body.result : null;
876
+ const result = isRecord2(body.result) ? body.result : null;
534
877
  const failed = result !== null && ("error" in result || "failure" in result || "rejected" in result);
535
- let line = "";
878
+ let line3 = "";
536
879
  if (result) {
537
880
  const payload = failed ? result.error ?? result.failure ?? result.rejected : result.success;
538
- line = firstLineOfContent(
881
+ line3 = firstLineOfContent(
539
882
  typeof payload === "string" ? payload : payload === void 0 ? result : payload
540
883
  );
541
884
  }
542
- return { kind: "tool_result", ok: !failed, line };
885
+ return { kind: "tool_result", ok: !failed, line: line3 };
543
886
  }
544
887
  return { kind: "silent" };
545
888
  }
546
- function parseStreamLine(line) {
547
- const trimmed = line.trim();
889
+ function parseStreamLine(line3) {
890
+ const trimmed = line3.trim();
548
891
  if (!trimmed || !trimmed.startsWith("{")) return [{ kind: "unparsed" }];
549
892
  let event;
550
893
  try {
@@ -552,7 +895,7 @@ function parseStreamLine(line) {
552
895
  } catch {
553
896
  return [{ kind: "unparsed" }];
554
897
  }
555
- if (!isRecord(event)) return [{ kind: "unparsed" }];
898
+ if (!isRecord2(event)) return [{ kind: "unparsed" }];
556
899
  switch (asString(event.type)) {
557
900
  case "assistant":
558
901
  return parseAssistant(event);
@@ -613,8 +956,8 @@ function renderStreamEvent(event, opts) {
613
956
  case "tool_result": {
614
957
  const label = event.ok ? "ok" : "error";
615
958
  const head = ` ${SPACE_MARKS.tick} ${label}`;
616
- const line = truncate(oneLine(event.line), width - head.length - 3);
617
- const text = line ? `${head} ${SPACE_MARKS.tick} ${line}` : head;
959
+ const line3 = truncate(oneLine(event.line), width - head.length - 3);
960
+ const text = line3 ? `${head} ${SPACE_MARKS.tick} ${line3}` : head;
618
961
  return paint(event.ok ? LABEL_MUTED : STATUS_ERR, text, color);
619
962
  }
620
963
  case "result": {
@@ -676,12 +1019,12 @@ var StreamRenderer = class {
676
1019
  const combined = this.pending + text;
677
1020
  const lines = combined.split("\n");
678
1021
  this.pending = lines.pop() ?? "";
679
- for (const line of lines) {
1022
+ for (const line3 of lines) {
680
1023
  if (this.dropped) {
681
1024
  this.dropped = false;
682
1025
  continue;
683
1026
  }
684
- this.renderLine(line);
1027
+ this.renderLine(line3);
685
1028
  }
686
1029
  if (this.pending.length > PENDING_MAX_CHARS2) {
687
1030
  this.pending = "";
@@ -700,8 +1043,8 @@ var StreamRenderer = class {
700
1043
  } catch {
701
1044
  }
702
1045
  }
703
- renderLine(line) {
704
- for (const event of parseStreamLine(line)) {
1046
+ renderLine(line3) {
1047
+ for (const event of parseStreamLine(line3)) {
705
1048
  const rendered = renderStreamEvent(event, { color: this.color, columns: this.columns });
706
1049
  if (rendered === null) continue;
707
1050
  if (event.kind === "text") {
@@ -724,7 +1067,402 @@ function createStreamRenderer(opts = {}) {
724
1067
  return new StreamRenderer(opts);
725
1068
  }
726
1069
 
1070
+ // src/plan-loop/cursor-acp.ts
1071
+ var ACP_PROTOCOL_VERSION = 1;
1072
+ var CURSOR_ASK_QUESTION_ID = "cursor-ask-question";
1073
+ var ACP_EXIT_GRACE_MS = 1e3;
1074
+ var ACP_KILL_GRACE_MS = 5e3;
1075
+ var ACP_MAX_TURNS = 50;
1076
+ function cursorAcpArgs() {
1077
+ return ["acp"];
1078
+ }
1079
+ var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1080
+ function acpInitializeParams() {
1081
+ return {
1082
+ protocolVersion: ACP_PROTOCOL_VERSION,
1083
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
1084
+ clientInfo: { name: "agent-kit", version: "1" }
1085
+ };
1086
+ }
1087
+ function acpPermissionOutcome(params) {
1088
+ const options = isRecord3(params) && Array.isArray(params.options) ? params.options : [];
1089
+ const pick = options.find((o) => isRecord3(o) && (o.optionId === "allow-once" || o.kind === "allow_once")) ?? options.find(
1090
+ (o) => isRecord3(o) && (typeof o.optionId === "string" && o.optionId.startsWith("allow") || typeof o.kind === "string" && o.kind.startsWith("allow"))
1091
+ );
1092
+ if (isRecord3(pick) && typeof pick.optionId === "string") {
1093
+ return { outcome: { outcome: "selected", optionId: pick.optionId } };
1094
+ }
1095
+ return { outcome: { outcome: "cancelled" } };
1096
+ }
1097
+ function acpQuestions(params) {
1098
+ const raw = isRecord3(params) && Array.isArray(params.questions) ? params.questions : [];
1099
+ const out = [];
1100
+ for (const q of raw) {
1101
+ if (!isRecord3(q) || typeof q.id !== "string" || !Array.isArray(q.options)) continue;
1102
+ const options = q.options.filter((o) => isRecord3(o) && typeof o.id === "string").map((o) => ({
1103
+ id: o.id,
1104
+ label: String(typeof o.label === "string" ? o.label : o.id).replace(/\|/g, "/").trim()
1105
+ }));
1106
+ if (options.length === 0) continue;
1107
+ out.push({ id: q.id, prompt: typeof q.prompt === "string" ? q.prompt : "", options });
1108
+ }
1109
+ return out;
1110
+ }
1111
+ var line2 = (event) => `${JSON.stringify(event)}
1112
+ `;
1113
+ async function runCursorAcp(opts) {
1114
+ const say = opts.log ?? ((text) => console.log(text));
1115
+ const env = mergeChildEnv(opts.env);
1116
+ const redact = claudeRedactions(env);
1117
+ const hitl = resolveHitlRunOptions(opts.hitl, { kind: "none", backend: "cursor-acp" });
1118
+ const render = opts.render ?? createStreamRenderer();
1119
+ const spawnFn = opts.spawnFn ?? (await import("child_process")).spawn;
1120
+ const record = emptyHitlRecord();
1121
+ const abort = new AbortController();
1122
+ if (opts.model) say("cursor-acp: --model is not sent over ACP; the agent's default model runs.");
1123
+ say("cursor tick: cursor-agent acp (JSON-RPC stdio, HITL relay, tool permissions allow-once).");
1124
+ const out = createWriteStream(opts.logPath, { flags: "w" });
1125
+ let logError = null;
1126
+ out.on("error", (err) => {
1127
+ logError = err;
1128
+ });
1129
+ const logLine = (text) => out.write(`${redactSecrets(text, redact)}
1130
+ `);
1131
+ const child = spawnFn("cursor-agent", cursorAcpArgs(), {
1132
+ cwd: opts.workspace,
1133
+ env,
1134
+ stdio: ["pipe", "pipe", "pipe"]
1135
+ });
1136
+ const pending = /* @__PURE__ */ new Map();
1137
+ let closed = false;
1138
+ let exitCode = null;
1139
+ let spawnError = null;
1140
+ let resolveClosed = () => {
1141
+ };
1142
+ const closedPromise = new Promise((resolve) => {
1143
+ resolveClosed = resolve;
1144
+ });
1145
+ const gone = (code2) => {
1146
+ if (closed) return;
1147
+ closed = true;
1148
+ exitCode = code2;
1149
+ abort.abort();
1150
+ for (const p of pending.values()) {
1151
+ p.reject({ message: `cursor-agent acp exited (code ${code2 ?? "signal"})` });
1152
+ }
1153
+ pending.clear();
1154
+ resolveClosed();
1155
+ };
1156
+ child.on("close", (code2) => gone(code2));
1157
+ child.on("error", (err) => {
1158
+ spawnError = new Error(redactSecrets(String(err), redact));
1159
+ gone(null);
1160
+ });
1161
+ let nextId = 1;
1162
+ let turnText = "";
1163
+ let sessionId = null;
1164
+ const send = (msg) => {
1165
+ const text = JSON.stringify(msg);
1166
+ logLine(text);
1167
+ try {
1168
+ child.stdin?.write(`${text}
1169
+ `);
1170
+ } catch {
1171
+ }
1172
+ };
1173
+ const request = (method, params) => new Promise((resolve, reject) => {
1174
+ if (closed) {
1175
+ reject({ message: "cursor-agent acp is not running" });
1176
+ return;
1177
+ }
1178
+ const id = nextId++;
1179
+ pending.set(id, { resolve, reject });
1180
+ send({ jsonrpc: "2.0", id, method, params });
1181
+ });
1182
+ const respond = (id, result) => send({ jsonrpc: "2.0", id, result });
1183
+ const respondError = (id, code2, message) => send({ jsonrpc: "2.0", id, error: { code: code2, message } });
1184
+ const ask = (gate) => askOperator(gate, {
1185
+ isTTY: hitl.isTTY,
1186
+ policy: hitl.policy,
1187
+ readAnswer: hitl.readAnswer,
1188
+ write: hitl.write,
1189
+ signal: abort.signal,
1190
+ onPromptStart: hitl.onPromptStart,
1191
+ onPromptEnd: hitl.onPromptEnd
1192
+ });
1193
+ const cancelSession = () => {
1194
+ if (sessionId && !closed)
1195
+ send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
1196
+ };
1197
+ const handleAskQuestion = async (id, params) => {
1198
+ const answers = [];
1199
+ for (const q of acpQuestions(params)) {
1200
+ if (record.stop) break;
1201
+ const gate = {
1202
+ askId: CURSOR_ASK_QUESTION_ID,
1203
+ labels: q.options.map((o) => o.label),
1204
+ detection: "sentinel"
1205
+ };
1206
+ const outcome = await ask(gate);
1207
+ if (outcome.kind === "stop") {
1208
+ record.stop = outcome.stop;
1209
+ break;
1210
+ }
1211
+ record.replies.push(outcome.stamp);
1212
+ hitl.write(`\u2192 ${outcome.stamp.line}
1213
+ `);
1214
+ const n = /operator reply (\d+)/.exec(outcome.stamp.reply)?.[1];
1215
+ const option = n ? q.options[Number(n) - 1] : void 0;
1216
+ if (!option) {
1217
+ hitl.write(
1218
+ "cursor-acp: a free-text answer has no cursor/ask_question option; the question is answered as cancelled.\n"
1219
+ );
1220
+ respond(id, { outcome: { outcome: "cancelled" } });
1221
+ return;
1222
+ }
1223
+ answers.push({ questionId: q.id, selectedOptionIds: [option.id] });
1224
+ }
1225
+ if (record.stop) {
1226
+ respond(id, { outcome: { outcome: "cancelled" } });
1227
+ cancelSession();
1228
+ return;
1229
+ }
1230
+ respond(id, { outcome: { outcome: "answered", answers } });
1231
+ };
1232
+ const handleRequest = (msg) => {
1233
+ const { id, method, params } = msg;
1234
+ if (method === "session/request_permission") {
1235
+ respond(id, acpPermissionOutcome(params));
1236
+ return;
1237
+ }
1238
+ if (method === "cursor/ask_question") {
1239
+ void handleAskQuestion(id, params);
1240
+ return;
1241
+ }
1242
+ respondError(id, -32601, `agent-kit does not implement ${String(method)}`);
1243
+ };
1244
+ const handleNotification = (msg) => {
1245
+ if (msg.method !== "session/update" || !isRecord3(msg.params)) return;
1246
+ const update = isRecord3(msg.params.update) ? msg.params.update : null;
1247
+ if (!update) return;
1248
+ if (update.sessionUpdate === "agent_message_chunk") {
1249
+ const content = isRecord3(update.content) ? update.content : null;
1250
+ if (content?.type === "text" && typeof content.text === "string") turnText += content.text;
1251
+ return;
1252
+ }
1253
+ if (update.sessionUpdate === "tool_call") {
1254
+ const title = typeof update.title === "string" ? update.title : "tool";
1255
+ render.feed(
1256
+ line2({
1257
+ type: "assistant",
1258
+ message: {
1259
+ content: [
1260
+ {
1261
+ type: "tool_use",
1262
+ name: typeof update.kind === "string" ? update.kind : "tool",
1263
+ input: { title }
1264
+ }
1265
+ ]
1266
+ }
1267
+ })
1268
+ );
1269
+ }
1270
+ };
1271
+ const take = (raw) => {
1272
+ const text = redactSecrets(raw.replace(/\r$/, ""), redact);
1273
+ if (!text.trim()) return;
1274
+ out.write(`${text}
1275
+ `);
1276
+ let msg;
1277
+ try {
1278
+ msg = JSON.parse(text);
1279
+ } catch {
1280
+ return;
1281
+ }
1282
+ if (!isRecord3(msg)) return;
1283
+ if (typeof msg.method === "string") {
1284
+ if (msg.id !== void 0 && msg.id !== null) handleRequest(msg);
1285
+ else handleNotification(msg);
1286
+ return;
1287
+ }
1288
+ if (typeof msg.id === "number") {
1289
+ const p = pending.get(msg.id);
1290
+ if (!p) return;
1291
+ pending.delete(msg.id);
1292
+ if (isRecord3(msg.error)) {
1293
+ p.reject({
1294
+ message: typeof msg.error.message === "string" ? msg.error.message : "JSON-RPC error",
1295
+ ...typeof msg.error.code === "number" ? { code: msg.error.code } : {}
1296
+ });
1297
+ } else p.resolve(msg.result);
1298
+ }
1299
+ };
1300
+ const decoder = new StringDecoder3("utf8");
1301
+ const splitter = new LineSplitter();
1302
+ child.stdout?.on("data", (chunk) => {
1303
+ splitter.feed(typeof chunk === "string" ? chunk : decoder.write(chunk), take);
1304
+ });
1305
+ const errBuffer = new RedactingStreamBuffer(redact);
1306
+ child.stderr?.on("data", (chunk) => {
1307
+ const text = redact.length ? errBuffer.push(chunk) : String(chunk);
1308
+ if (text) out.write(text);
1309
+ });
1310
+ child.stdin?.on("error", () => {
1311
+ });
1312
+ let stopped = false;
1313
+ const endChild = () => {
1314
+ if (stopped) return;
1315
+ stopped = true;
1316
+ try {
1317
+ child.stdin?.end();
1318
+ } catch {
1319
+ }
1320
+ const term = setTimeout(() => {
1321
+ if (closed) return;
1322
+ child.kill("SIGTERM");
1323
+ const kill = setTimeout(() => {
1324
+ if (!closed) child.kill("SIGKILL");
1325
+ }, ACP_KILL_GRACE_MS);
1326
+ kill.unref?.();
1327
+ }, ACP_EXIT_GRACE_MS);
1328
+ term.unref?.();
1329
+ };
1330
+ opts.onSpawn?.({
1331
+ backend: "cursor-acp",
1332
+ pid: child.pid,
1333
+ stop: (stop) => {
1334
+ if (closed || record.stop) return;
1335
+ record.stop = stop;
1336
+ cancelSession();
1337
+ abort.abort();
1338
+ endChild();
1339
+ }
1340
+ });
1341
+ let lastResult;
1342
+ const finishTurn = (text, isError, subtype, errors) => {
1343
+ render.feed(line2({ type: "assistant", message: { content: [{ type: "text", text }] } }));
1344
+ render.feed(
1345
+ line2({
1346
+ type: "result",
1347
+ subtype,
1348
+ is_error: isError,
1349
+ result: text,
1350
+ session_id: sessionId ?? "",
1351
+ ...errors.length ? { errors } : {}
1352
+ })
1353
+ );
1354
+ lastResult = { resultText: text, assistantTexts: text ? [text] : [], isError, subtype, errors };
1355
+ };
1356
+ const failed = (message, subtype = "error") => finishTurn(message, true, subtype, [message]);
1357
+ const session = async () => {
1358
+ const init = await request("initialize", acpInitializeParams());
1359
+ const methods = isRecord3(init) && Array.isArray(init.authMethods) ? init.authMethods : [];
1360
+ if (methods.some((m) => isRecord3(m) && m.id === "cursor_login")) {
1361
+ try {
1362
+ await request("authenticate", { methodId: "cursor_login" });
1363
+ } catch (err) {
1364
+ const message = err.message ?? String(err);
1365
+ throw {
1366
+ message: `Cursor sign-in needed: run \`cursor-agent login\` in a terminal (the kit never handles Cursor credentials). ${message}`
1367
+ };
1368
+ }
1369
+ }
1370
+ const created = await request("session/new", { cwd: opts.workspace, mcpServers: [] });
1371
+ if (!isRecord3(created) || typeof created.sessionId !== "string") {
1372
+ throw { message: "cursor-agent acp returned no sessionId" };
1373
+ }
1374
+ sessionId = created.sessionId;
1375
+ render.feed(
1376
+ line2({ type: "system", subtype: "init", session_id: sessionId, model: "cursor-acp" })
1377
+ );
1378
+ let prompt = opts.prompt;
1379
+ for (let turn = 0; turn < ACP_MAX_TURNS && !record.stop; turn += 1) {
1380
+ turnText = "";
1381
+ let stopReason = "end_turn";
1382
+ try {
1383
+ const res = await request("session/prompt", {
1384
+ sessionId,
1385
+ prompt: [{ type: "text", text: prompt }]
1386
+ });
1387
+ if (isRecord3(res) && typeof res.stopReason === "string") stopReason = res.stopReason;
1388
+ } catch (err) {
1389
+ failed(err.message ?? String(err));
1390
+ return;
1391
+ }
1392
+ const text = turnText;
1393
+ finishTurn(text, stopReason === "refusal", stopReason, []);
1394
+ if (record.stop || stopReason === "cancelled") return;
1395
+ const gate = detectHitlGate(text, []);
1396
+ if (!gate) return;
1397
+ if (gate.detection === "fallback") record.fallbackDetections += 1;
1398
+ const outcome = await ask(gate);
1399
+ if (outcome.kind === "stop") {
1400
+ record.stop = outcome.stop;
1401
+ return;
1402
+ }
1403
+ record.replies.push(outcome.stamp);
1404
+ hitl.write(`\u2192 ${outcome.stamp.line}
1405
+ `);
1406
+ prompt = outcome.stamp.line;
1407
+ }
1408
+ };
1409
+ await session().catch((err) => {
1410
+ failed(err.message ?? String(err));
1411
+ });
1412
+ const failure = spawnError;
1413
+ if (failure) {
1414
+ out.end();
1415
+ throw new Error(`cursor-acp backend failed to start: ${failure.message}`);
1416
+ }
1417
+ const endedByClient = !closed;
1418
+ endChild();
1419
+ await closedPromise;
1420
+ const rest = splitter.end();
1421
+ if (rest.trim()) take(rest);
1422
+ const errRest = errBuffer.flush();
1423
+ if (errRest) out.write(errRest);
1424
+ render.end();
1425
+ await new Promise((resolve) => out.end(() => resolve()));
1426
+ if (logError) throw new Error(`cursor-acp backend: ${redactSecrets(String(logError), redact)}`);
1427
+ const code = lastResult?.isError ? 1 : endedByClient ? 0 : exitCode ?? 1;
1428
+ return { exitCode: code, hitl: record, ...lastResult ? { lastResult } : {} };
1429
+ }
1430
+ var cursorAcpBackend = {
1431
+ id: "cursor-acp",
1432
+ async resolve() {
1433
+ return whichBinary("cursor-agent");
1434
+ },
1435
+ run: runCursorAcp
1436
+ };
1437
+
727
1438
  // src/plan-loop/backends.ts
1439
+ function createLineTranslator(translate) {
1440
+ const decoder = new StringDecoder4("utf8");
1441
+ const lines = new LineSplitter();
1442
+ const run = (line3, out) => {
1443
+ try {
1444
+ out.push(...translate(line3));
1445
+ } catch {
1446
+ }
1447
+ };
1448
+ return {
1449
+ push(chunk) {
1450
+ const text = typeof chunk === "string" ? chunk : decoder.write(chunk);
1451
+ const out = [];
1452
+ lines.feed(text, (line3) => run(line3, out));
1453
+ return out.length ? `${out.join("\n")}
1454
+ ` : "";
1455
+ },
1456
+ end() {
1457
+ const out = [];
1458
+ lines.feed(decoder.end(), (line3) => run(line3, out));
1459
+ const rest = lines.end();
1460
+ if (rest.trim()) run(rest, out);
1461
+ return out.length ? `${out.join("\n")}
1462
+ ` : "";
1463
+ }
1464
+ };
1465
+ }
728
1466
  var HITL_KILL_GRACE_MS = 5e3;
729
1467
  function resolveHitlRunOptions(hitl, transport) {
730
1468
  return {
@@ -754,7 +1492,7 @@ function redactSecrets(text, secrets) {
754
1492
  var REDACT_HOLD_MAX_CHARS = 64 * 1024;
755
1493
  var RedactingStreamBuffer = class {
756
1494
  pending = "";
757
- decoder = new StringDecoder3("utf8");
1495
+ decoder = new StringDecoder4("utf8");
758
1496
  secrets;
759
1497
  keep;
760
1498
  constructor(secrets) {
@@ -839,6 +1577,14 @@ function claudeRunCaps(env) {
839
1577
  }
840
1578
  return { caps, invalid };
841
1579
  }
1580
+ var VENDOR_SECRET_ENV = [
1581
+ "CLAUDE_CODE_OAUTH_TOKEN",
1582
+ "CURSOR_API_KEY",
1583
+ "CURSOR_AUTH_TOKEN",
1584
+ "OPENAI_API_KEY",
1585
+ "CODEX_API_KEY",
1586
+ "CODEX_ACCESS_TOKEN"
1587
+ ];
842
1588
  var REDACT_DERIVED_MIN_CHARS = 4;
843
1589
  var REDACT_DERIVED_SKIP = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"]);
844
1590
  function derivedSecretForms(value) {
@@ -874,7 +1620,7 @@ function claudeRedactions(env) {
874
1620
  seen.add(value);
875
1621
  entries.push({ label, value });
876
1622
  };
877
- for (const key of CLAUDE_ENV_PASSTHROUGH) {
1623
+ for (const key of [...CLAUDE_ENV_PASSTHROUGH, ...VENDOR_SECRET_ENV]) {
878
1624
  const value = env[key];
879
1625
  if (!value) continue;
880
1626
  add(key, value, false);
@@ -884,12 +1630,7 @@ function claudeRedactions(env) {
884
1630
  return entries;
885
1631
  }
886
1632
  async function whichBinary(bin) {
887
- try {
888
- const out = execFileSync("which", [bin], { encoding: "utf8" }).trim();
889
- return out || null;
890
- } catch {
891
- return null;
892
- }
1633
+ return findOnPath(bin);
893
1634
  }
894
1635
  function spawnLogged(command, args, logPath, options = {}) {
895
1636
  const spawnFn = options.spawnFn ?? spawn;
@@ -897,18 +1638,19 @@ function spawnLogged(command, args, logPath, options = {}) {
897
1638
  const redactedError = (err) => new Error(redactSecrets(String(err), redact));
898
1639
  const hitl = options.hitl;
899
1640
  const stdinTransport = hitl?.transport.kind === "stdin-stream-json";
1641
+ const stdinPiped = stdinTransport || options.stdinText !== void 0;
900
1642
  return new Promise((resolve, reject) => {
901
1643
  let out;
902
1644
  let child;
903
1645
  let logError = null;
904
1646
  try {
905
- out = createWriteStream(logPath, { flags: "w" });
1647
+ out = createWriteStream2(logPath, { flags: options.appendLog ? "a" : "w" });
906
1648
  out.on("error", (err) => {
907
1649
  logError = err;
908
1650
  reject(redactedError(err));
909
1651
  });
910
1652
  child = spawnFn(command, args, {
911
- stdio: [stdinTransport ? "pipe" : "ignore", "pipe", "pipe"],
1653
+ stdio: [stdinPiped ? "pipe" : "ignore", "pipe", "pipe"],
912
1654
  ...options.cwd ? { cwd: options.cwd } : {},
913
1655
  ...options.env ? { env: options.env } : {}
914
1656
  });
@@ -918,12 +1660,25 @@ function spawnLogged(command, args, logPath, options = {}) {
918
1660
  }
919
1661
  const render = options.render ?? createStreamRenderer();
920
1662
  const relay = hitl ? createRelay(hitl, child, redactedError) : null;
921
- const emit = (text) => {
1663
+ const translator = options.translate ? createLineTranslator(options.translate) : null;
1664
+ const sinks = (text) => {
922
1665
  if (text.length === 0) return;
923
- out.write(text);
924
1666
  render.feed(text);
925
1667
  relay?.feed(text);
926
1668
  };
1669
+ const emit = (text) => {
1670
+ if (text.length === 0) return;
1671
+ out.write(text);
1672
+ sinks(translator ? translator.push(text) : text);
1673
+ };
1674
+ if (options.stdinText !== void 0 && !stdinTransport) {
1675
+ const stdin = child.stdin;
1676
+ if (stdin) {
1677
+ stdin.on("error", () => {
1678
+ });
1679
+ stdin.end(options.stdinText);
1680
+ }
1681
+ }
927
1682
  if (relay?.startError) {
928
1683
  out.end();
929
1684
  reject(relay.startError);
@@ -962,6 +1717,7 @@ function spawnLogged(command, args, logPath, options = {}) {
962
1717
  });
963
1718
  child.on("close", (code) => {
964
1719
  for (const buffer of buffers) emit(buffer.flush());
1720
+ if (translator) sinks(translator.end());
965
1721
  render.end();
966
1722
  relay?.finish();
967
1723
  const finish = () => {
@@ -970,10 +1726,12 @@ function spawnLogged(command, args, logPath, options = {}) {
970
1726
  if (failure) reject(redactedError(failure));
971
1727
  else if (relay) {
972
1728
  const lastResult = relay.lastResult();
1729
+ const resumeWith = relay.resumeWith();
973
1730
  resolve({
974
1731
  exitCode: code ?? 1,
975
1732
  hitl: relay.record,
976
- ...lastResult ? { lastResult } : {}
1733
+ ...lastResult ? { lastResult } : {},
1734
+ ...resumeWith ? { resumeWith } : {}
977
1735
  });
978
1736
  } else resolve({ exitCode: code ?? 1 });
979
1737
  });
@@ -986,6 +1744,8 @@ function spawnLogged(command, args, logPath, options = {}) {
986
1744
  function createRelay(hitl, child, redactedError) {
987
1745
  const record = emptyHitlRecord();
988
1746
  const stdinTransport = hitl.transport.kind === "stdin-stream-json";
1747
+ const resumeTransport = hitl.transport.kind === "resume";
1748
+ let resumeWith;
989
1749
  const abort = new AbortController();
990
1750
  const grace = hitl.killAfterMs ?? HITL_KILL_GRACE_MS;
991
1751
  let startError = null;
@@ -1065,6 +1825,13 @@ function createRelay(hitl, child, redactedError) {
1065
1825
  stopChild();
1066
1826
  return;
1067
1827
  }
1828
+ if (resumeTransport) {
1829
+ record.replies.push(outcome.stamp);
1830
+ hitl.write(`\u2192 ${outcome.stamp.line}
1831
+ `);
1832
+ resumeWith = outcome.stamp.line;
1833
+ return;
1834
+ }
1068
1835
  if (closed || !stdin) {
1069
1836
  record.stop = unansweredStop(gate.askId, "child exited");
1070
1837
  return;
@@ -1086,7 +1853,7 @@ function createRelay(hitl, child, redactedError) {
1086
1853
  child.on("close", () => {
1087
1854
  closed = true;
1088
1855
  for (const t of timers) clearTimeout(t);
1089
- abort.abort();
1856
+ if (!resumeTransport) abort.abort();
1090
1857
  const settle = () => resolveClosed();
1091
1858
  if (pendingAsk) pendingAsk.then(settle, settle);
1092
1859
  else settle();
@@ -1095,6 +1862,7 @@ function createRelay(hitl, child, redactedError) {
1095
1862
  startError,
1096
1863
  record,
1097
1864
  lastResult: () => lastResult,
1865
+ resumeWith: () => resumeWith,
1098
1866
  feed: (text) => watcher.feed(text),
1099
1867
  finish: () => watcher.end(),
1100
1868
  closed: () => closedPromise,
@@ -1143,7 +1911,7 @@ var cursorAgentBackend = {
1143
1911
  });
1144
1912
  }
1145
1913
  };
1146
- var CLAUDE_MIN_VERSION = "2.1.259";
1914
+ var CLAUDE_MIN_VERSION = VENDOR_CLI_PINS.claude.min ?? "2.1.259";
1147
1915
  function parseClaudeVersion(raw) {
1148
1916
  if (!raw) return null;
1149
1917
  const m = /(\d+)\.(\d+)\.(\d+)/.exec(raw);
@@ -1161,7 +1929,7 @@ function compareVersions(a, b) {
1161
1929
  var claudeVersionCache = /* @__PURE__ */ new Map();
1162
1930
  function defaultVersionFn(bin) {
1163
1931
  try {
1164
- return execFileSync(bin, ["--version"], {
1932
+ return execFileSync2(bin, ["--version"], {
1165
1933
  encoding: "utf8",
1166
1934
  stdio: ["ignore", "pipe", "ignore"]
1167
1935
  });
@@ -1188,6 +1956,8 @@ function checkClaudeVersion(bin, versionFn = defaultVersionFn) {
1188
1956
  message: `claude ${version} is older than ${CLAUDE_MIN_VERSION}, the first release that accepts \`--permission-prompts none\`. Run \`claude update\` and retry.`
1189
1957
  };
1190
1958
  }
1959
+ const pin = vendorVersionStatus("claude", version);
1960
+ if (pin.status === "newer-than-tested") return { ok: true, version, warning: pin.message };
1191
1961
  return { ok: true, version };
1192
1962
  }
1193
1963
  function claudeHeadlessArgs(opts = {}) {
@@ -1218,9 +1988,9 @@ function claudeHeadlessArgs(opts = {}) {
1218
1988
  async function missingClaudeAdapter(workspace, prompt) {
1219
1989
  const m = /^\/([A-Za-z0-9][A-Za-z0-9_-]*)/.exec(prompt.trimStart());
1220
1990
  if (!m?.[1]) return null;
1221
- const rel = path.join(".claude", "commands", `${m[1]}.md`);
1991
+ const rel = path2.join(".claude", "commands", `${m[1]}.md`);
1222
1992
  try {
1223
- await access(path.join(workspace, rel));
1993
+ await access2(path2.join(workspace, rel));
1224
1994
  return null;
1225
1995
  } catch {
1226
1996
  return rel;
@@ -1232,7 +2002,7 @@ var claudeBackend = {
1232
2002
  return whichBinary("claude");
1233
2003
  },
1234
2004
  async run(opts) {
1235
- const log = opts.log ?? ((line) => console.log(line));
2005
+ const log = opts.log ?? ((line3) => console.log(line3));
1236
2006
  const env = claudeChildEnv(opts.env);
1237
2007
  const redact = claudeRedactions(env);
1238
2008
  const version = checkClaudeVersion("claude", opts.versionFn);
@@ -1279,14 +2049,19 @@ var claudeBackend = {
1279
2049
  }
1280
2050
  }
1281
2051
  };
1282
- var BACKENDS = {
1283
- "cursor-agent": cursorAgentBackend,
1284
- claude: claudeBackend
1285
- };
2052
+ function backends() {
2053
+ return {
2054
+ "cursor-agent": cursorAgentBackend,
2055
+ claude: claudeBackend,
2056
+ codex: codexBackend,
2057
+ "cursor-acp": cursorAcpBackend
2058
+ };
2059
+ }
1286
2060
  function getBackend(id) {
1287
- const backend = BACKENDS[id];
2061
+ const all = backends();
2062
+ const backend = all[id];
1288
2063
  if (!backend) {
1289
- throw new Error(`Unknown backend '${id}'. Supported: ${Object.keys(BACKENDS).join(", ")}`);
2064
+ throw new Error(`Unknown backend '${id}'. Supported: ${Object.keys(all).join(", ")}`);
1290
2065
  }
1291
2066
  return backend;
1292
2067
  }
@@ -1295,11 +2070,14 @@ export {
1295
2070
  LineSplitter,
1296
2071
  RESERVED_GATE_IDS,
1297
2072
  HITL_UNANSWERED_EXIT_CODE,
2073
+ formatHitlGateLine,
1298
2074
  operatorQuitStop,
1299
2075
  formatHitlSummary,
1300
2076
  hitlExitCode,
2077
+ codexBackend,
1301
2078
  parseStreamLine,
1302
2079
  renderStreamEvent,
2080
+ cursorAcpBackend,
1303
2081
  resolveHitlRunOptions,
1304
2082
  redactSecrets,
1305
2083
  RedactingStreamBuffer,