@mcoda/agents 0.1.9 → 0.1.11

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.
@@ -2,6 +2,478 @@ import { spawn, spawnSync } from "node:child_process";
2
2
  const CODEX_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
3
3
  const CODEX_REASONING_ENV = "MCODA_CODEX_REASONING_EFFORT";
4
4
  const CODEX_REASONING_ENV_FALLBACK = "CODEX_REASONING_EFFORT";
5
+ const CODEX_NO_SANDBOX_ENV = "MCODA_CODEX_NO_SANDBOX";
6
+ const CODEX_STREAM_IO_ENV = "MCODA_STREAM_IO";
7
+ const CODEX_STREAM_IO_FORMAT_ENV = "MCODA_STREAM_IO_FORMAT";
8
+ const CODEX_STREAM_IO_COLOR_ENV = "MCODA_STREAM_IO_COLOR";
9
+ const CODEX_STREAM_IO_PREFIX = "codex-cli";
10
+ const ANSI = {
11
+ reset: "\u001b[0m",
12
+ bold: "\u001b[1m",
13
+ red: "\u001b[31m",
14
+ green: "\u001b[32m",
15
+ yellow: "\u001b[33m",
16
+ blue: "\u001b[34m",
17
+ magenta: "\u001b[35m",
18
+ cyan: "\u001b[36m",
19
+ gray: "\u001b[90m",
20
+ };
21
+ const isStreamIoEnabled = () => {
22
+ const raw = process.env[CODEX_STREAM_IO_ENV];
23
+ if (!raw)
24
+ return false;
25
+ const normalized = raw.trim().toLowerCase();
26
+ return !["0", "false", "off", "no"].includes(normalized);
27
+ };
28
+ const isStreamIoRaw = () => {
29
+ const raw = process.env[CODEX_STREAM_IO_FORMAT_ENV];
30
+ if (!raw)
31
+ return false;
32
+ const normalized = raw.trim().toLowerCase();
33
+ return ["raw", "json", "jsonl"].includes(normalized);
34
+ };
35
+ const isStreamIoColorEnabled = () => {
36
+ const raw = process.env[CODEX_STREAM_IO_COLOR_ENV];
37
+ if (!raw)
38
+ return true;
39
+ const normalized = raw.trim().toLowerCase();
40
+ return !["0", "false", "off", "no"].includes(normalized);
41
+ };
42
+ const resolveSandboxArgs = () => {
43
+ const raw = process.env[CODEX_NO_SANDBOX_ENV];
44
+ if (raw === undefined || raw.trim() === "") {
45
+ return { args: ["--dangerously-bypass-approvals-and-sandbox"], bypass: true };
46
+ }
47
+ const normalized = raw.trim().toLowerCase();
48
+ if (normalized === "0") {
49
+ return { args: [], bypass: false };
50
+ }
51
+ return { args: ["--dangerously-bypass-approvals-and-sandbox"], bypass: true };
52
+ };
53
+ let streamIoQueue = Promise.resolve();
54
+ const emitStreamIoLine = (line) => {
55
+ if (!isStreamIoEnabled())
56
+ return;
57
+ const normalized = line.endsWith("\n") ? line : `${line}\n`;
58
+ streamIoQueue = streamIoQueue
59
+ .then(() => new Promise((resolve) => {
60
+ try {
61
+ process.stderr.write(normalized, () => resolve());
62
+ }
63
+ catch {
64
+ resolve();
65
+ }
66
+ }))
67
+ .catch(() => { });
68
+ };
69
+ const safeJsonParse = (line) => {
70
+ try {
71
+ return JSON.parse(line);
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ };
77
+ const colorize = (text, color, bold = false) => {
78
+ if (!isStreamIoColorEnabled())
79
+ return text;
80
+ const colorCode = color ? ANSI[color] : "";
81
+ const boldCode = bold ? ANSI.bold : "";
82
+ if (!colorCode && !boldCode)
83
+ return text;
84
+ return `${boldCode}${colorCode}${text}${ANSI.reset}`;
85
+ };
86
+ const formatTextLines = (prefix, text, color) => {
87
+ if (!text)
88
+ return [];
89
+ const lines = text.split(/\r?\n/).map((line) => line.replace(/\s+$/, ""));
90
+ const output = [];
91
+ for (const line of lines) {
92
+ if (!line.trim()) {
93
+ output.push({ text: "", color });
94
+ continue;
95
+ }
96
+ output.push({ text: `${prefix}${line}`, color });
97
+ }
98
+ while (output.length && !output[0].text.trim())
99
+ output.shift();
100
+ while (output.length && !output[output.length - 1].text.trim())
101
+ output.pop();
102
+ return output;
103
+ };
104
+ const extractItemText = (item) => {
105
+ if (!item)
106
+ return "";
107
+ if (typeof item.text === "string")
108
+ return item.text;
109
+ if (Array.isArray(item.content)) {
110
+ return item.content
111
+ .map((entry) => (typeof entry?.text === "string" ? entry.text : ""))
112
+ .filter(Boolean)
113
+ .join("");
114
+ }
115
+ return "";
116
+ };
117
+ const normalizeValue = (value) => {
118
+ if (typeof value !== "string")
119
+ return value;
120
+ const trimmed = value.trim();
121
+ if (!trimmed)
122
+ return value;
123
+ if (!(trimmed.startsWith("{") || trimmed.startsWith("[")))
124
+ return value;
125
+ if (trimmed.length > 200000)
126
+ return value;
127
+ try {
128
+ return JSON.parse(trimmed);
129
+ }
130
+ catch {
131
+ return value;
132
+ }
133
+ };
134
+ const formatValueLines = (value, indent, depth = 0, maxDepth = Number.POSITIVE_INFINITY, maxLines = Number.POSITIVE_INFINITY) => {
135
+ const lines = [];
136
+ if (lines.length >= maxLines)
137
+ return lines;
138
+ const normalized = normalizeValue(value);
139
+ if (depth >= maxDepth) {
140
+ lines.push({ text: "…", indent, color: "gray" });
141
+ return lines;
142
+ }
143
+ if (normalized === null || normalized === undefined) {
144
+ lines.push({ text: String(normalized), indent, color: "gray" });
145
+ return lines;
146
+ }
147
+ if (typeof normalized === "string") {
148
+ const entries = normalized.split(/\r?\n/);
149
+ for (const entry of entries) {
150
+ if (!entry.trim()) {
151
+ lines.push({ text: "", indent });
152
+ continue;
153
+ }
154
+ lines.push({ text: entry, indent });
155
+ if (lines.length >= maxLines)
156
+ break;
157
+ }
158
+ return lines;
159
+ }
160
+ if (typeof normalized !== "object") {
161
+ lines.push({ text: String(normalized), indent });
162
+ return lines;
163
+ }
164
+ if (Array.isArray(normalized)) {
165
+ if (normalized.length === 0) {
166
+ lines.push({ text: "[]", indent, color: "gray" });
167
+ return lines;
168
+ }
169
+ for (const entry of normalized) {
170
+ if (lines.length >= maxLines)
171
+ break;
172
+ const normalizedEntry = normalizeValue(entry);
173
+ if (normalizedEntry !== null && typeof normalizedEntry === "object") {
174
+ lines.push({ text: "-", indent });
175
+ lines.push(...formatValueLines(normalizedEntry, indent + 1, depth + 1, maxDepth, maxLines));
176
+ continue;
177
+ }
178
+ if (typeof normalizedEntry === "string" && normalizedEntry.includes("\n")) {
179
+ lines.push({ text: "-", indent });
180
+ lines.push(...formatValueLines(normalizedEntry, indent + 1, depth + 1, maxDepth, maxLines));
181
+ continue;
182
+ }
183
+ lines.push({ text: `- ${String(normalizedEntry)}`, indent });
184
+ }
185
+ if (normalized.length + 1 > maxLines) {
186
+ lines.push({ text: "…", indent, color: "gray" });
187
+ }
188
+ return lines;
189
+ }
190
+ const keys = Object.keys(normalized);
191
+ if (!keys.length) {
192
+ lines.push({ text: "{}", indent, color: "gray" });
193
+ return lines;
194
+ }
195
+ for (const key of keys) {
196
+ if (lines.length >= maxLines)
197
+ break;
198
+ const entry = normalized[key];
199
+ const normalizedEntry = normalizeValue(entry);
200
+ if (normalizedEntry !== null && typeof normalizedEntry === "object") {
201
+ lines.push({ text: `${key}:`, indent });
202
+ lines.push(...formatValueLines(normalizedEntry, indent + 1, depth + 1, maxDepth, maxLines));
203
+ continue;
204
+ }
205
+ if (typeof normalizedEntry === "string" && normalizedEntry.includes("\n")) {
206
+ lines.push({ text: `${key}:`, indent });
207
+ lines.push(...formatValueLines(normalizedEntry, indent + 1, depth + 1, maxDepth, maxLines));
208
+ continue;
209
+ }
210
+ const valueText = normalizedEntry === null || normalizedEntry === undefined ? String(normalizedEntry) : String(normalizedEntry);
211
+ lines.push({ text: `${key}: ${valueText}`, indent });
212
+ }
213
+ if (keys.length + 1 > maxLines) {
214
+ lines.push({ text: "…", indent, color: "gray" });
215
+ }
216
+ return lines;
217
+ };
218
+ const formatValueBlock = (title, value, indent, color) => {
219
+ const lines = [{ text: title, indent, color, bold: true }];
220
+ lines.push(...formatValueLines(value, indent + 1));
221
+ return lines;
222
+ };
223
+ const statusColor = (status) => {
224
+ if (!status)
225
+ return undefined;
226
+ const normalized = status.toLowerCase();
227
+ if (["failed", "error", "cancelled"].includes(normalized))
228
+ return "red";
229
+ if (["completed", "succeeded", "success"].includes(normalized))
230
+ return "green";
231
+ if (["in_progress", "started", "running"].includes(normalized))
232
+ return "yellow";
233
+ return undefined;
234
+ };
235
+ const formatItemEvent = (eventType, item) => {
236
+ const verb = eventType.replace("item.", "");
237
+ const itemTypeRaw = item?.item_type ?? item?.itemType ?? item?.type ?? "unknown";
238
+ const itemType = String(itemTypeRaw);
239
+ const id = item?.id ? ` id=${item.id}` : "";
240
+ let status = item?.status ? String(item.status) : verb;
241
+ let headerColor = statusColor(status);
242
+ if (item?.error || itemType.toLowerCase().includes("error")) {
243
+ headerColor = "red";
244
+ }
245
+ const lines = [
246
+ { text: `${itemType} (${status})${id}`, color: headerColor, bold: true },
247
+ ];
248
+ switch (itemType.toLowerCase()) {
249
+ case "reasoning": {
250
+ lines.push(...formatTextLines("reasoning: ", item?.text, "magenta").map((line) => ({ ...line, indent: 1 })));
251
+ break;
252
+ }
253
+ case "assistant_message":
254
+ case "agent_message": {
255
+ const text = extractItemText(item);
256
+ lines.push(...formatTextLines("assistant: ", text, "green").map((line) => ({ ...line, indent: 1 })));
257
+ break;
258
+ }
259
+ case "command_execution": {
260
+ const command = item?.command ? ` command="${item.command}"` : "";
261
+ const exitCode = item?.exit_code ?? item?.exitCode;
262
+ const rawCommand = typeof item?.command === "string" ? item.command : "";
263
+ const rgNoMatch = exitCode === 1 &&
264
+ rawCommand.includes("rg ") &&
265
+ (!item?.aggregated_output || String(item.aggregated_output).trim().length === 0);
266
+ if (rgNoMatch) {
267
+ status = "no_matches";
268
+ headerColor = "yellow";
269
+ }
270
+ else if (exitCode !== undefined && exitCode !== 0) {
271
+ headerColor = "red";
272
+ }
273
+ const exitText = exitCode !== undefined ? ` exit=${exitCode}` : "";
274
+ const statusLine = `${itemType} (${status})${id}${exitText}${command}`;
275
+ lines[0] = { text: statusLine, color: headerColor, bold: true };
276
+ if (item?.aggregated_output) {
277
+ lines.push({ text: "output:", indent: 1, color: "gray", bold: true });
278
+ const outputColor = exitCode !== undefined && exitCode !== 0 && !rgNoMatch ? "red" : undefined;
279
+ lines.push(...formatTextLines("", item.aggregated_output, outputColor).map((line) => ({ ...line, indent: 2 })));
280
+ }
281
+ break;
282
+ }
283
+ case "file_change": {
284
+ const changes = Array.isArray(item?.changes) ? item.changes : [];
285
+ for (const change of changes) {
286
+ const kind = change?.kind ? String(change.kind) : "update";
287
+ const path = change?.path ? String(change.path) : "unknown";
288
+ const changeColor = kind === "add" ? "green" : kind === "delete" ? "red" : "yellow";
289
+ lines.push({ text: `file_change: ${kind} ${path}`, indent: 1, color: changeColor });
290
+ }
291
+ break;
292
+ }
293
+ case "mcp_tool_call": {
294
+ const server = item?.server ? String(item.server) : "mcp";
295
+ const tool = item?.tool ? String(item.tool) : "tool";
296
+ if (item?.error) {
297
+ headerColor = "red";
298
+ }
299
+ lines[0] = { text: `tool: ${server}.${tool} (${status})${id}`, color: headerColor, bold: true };
300
+ if (item?.arguments !== undefined) {
301
+ lines.push(...formatValueBlock("args:", item.arguments, 1, "blue"));
302
+ }
303
+ if (item?.error) {
304
+ lines.push(...formatValueBlock("error:", item.error, 1, "red"));
305
+ }
306
+ if (item?.result) {
307
+ lines.push(...formatValueBlock("result:", item.result, 1, "green"));
308
+ }
309
+ break;
310
+ }
311
+ case "web_search": {
312
+ if (item?.query) {
313
+ lines.push({ text: `web_search: ${String(item.query)}`, indent: 1, color: "blue" });
314
+ }
315
+ break;
316
+ }
317
+ case "error": {
318
+ if (item?.message) {
319
+ lines.push({ text: `error: ${String(item.message)}`, indent: 1, color: "red" });
320
+ }
321
+ break;
322
+ }
323
+ default: {
324
+ const text = extractItemText(item);
325
+ if (text) {
326
+ lines.push(...formatTextLines("text: ", text).map((line) => ({ ...line, indent: 1 })));
327
+ }
328
+ break;
329
+ }
330
+ }
331
+ return lines;
332
+ };
333
+ const formatCodexEvent = (parsed) => {
334
+ const type = typeof parsed?.type === "string" ? parsed.type : "unknown";
335
+ if (type === "thread.started") {
336
+ const id = parsed.thread_id ?? parsed.threadId ?? "";
337
+ return [{ text: `Thread started${id ? ` (id=${id})` : ""}`, color: "cyan", bold: true }];
338
+ }
339
+ if (type === "turn.started")
340
+ return [{ text: "Turn started", color: "cyan", bold: true }];
341
+ if (type === "turn.completed") {
342
+ const usage = parsed?.usage ?? {};
343
+ const parts = [];
344
+ if (typeof usage.input_tokens === "number")
345
+ parts.push(`input=${usage.input_tokens}`);
346
+ if (typeof usage.cached_input_tokens === "number")
347
+ parts.push(`cached=${usage.cached_input_tokens}`);
348
+ if (typeof usage.output_tokens === "number")
349
+ parts.push(`output=${usage.output_tokens}`);
350
+ const suffix = parts.length ? ` usage(${parts.join(",")})` : "";
351
+ return [{ text: `Turn completed${suffix}`, color: "green", bold: true }];
352
+ }
353
+ if (type === "turn.failed") {
354
+ const message = parsed?.error?.message ?? parsed?.error ?? "";
355
+ return [{ text: `Turn failed${message ? `: ${String(message)}` : ""}`, color: "red", bold: true }];
356
+ }
357
+ if (type.startsWith("output_text.")) {
358
+ const event = extractAssistantText(parsed);
359
+ if (event)
360
+ return formatTextLines("assistant: ", event.text, "green");
361
+ return [{ text: type, color: "gray" }];
362
+ }
363
+ if (type.startsWith("item.")) {
364
+ return formatItemEvent(type, parsed?.item ?? {});
365
+ }
366
+ if (type === "error" && parsed?.message) {
367
+ return [{ text: `error: ${String(parsed.message)}`, color: "red", bold: true }];
368
+ }
369
+ return [{ text: type, color: "gray" }];
370
+ };
371
+ const createStreamFormatter = (model) => {
372
+ let started = false;
373
+ let lastWasBlank = true;
374
+ let assistantBuffer = "";
375
+ let assistantActive = false;
376
+ const baseIndent = 1;
377
+ const emitLine = (line) => {
378
+ if (!line.text) {
379
+ emitBlank();
380
+ return;
381
+ }
382
+ const indent = " ".repeat(baseIndent + (line.indent ?? 0));
383
+ const text = colorize(line.text, line.color, line.bold);
384
+ emitStreamIoLine(`${indent}${text}`);
385
+ lastWasBlank = false;
386
+ };
387
+ const emitBlank = () => {
388
+ emitStreamIoLine("");
389
+ lastWasBlank = true;
390
+ };
391
+ const start = () => {
392
+ if (started)
393
+ return;
394
+ started = true;
395
+ const headerDetails = model ? ` (model=${model})` : "";
396
+ emitStreamIoLine(colorize(`${CODEX_STREAM_IO_PREFIX} ------- output start --------${headerDetails}`, "cyan", true));
397
+ emitBlank();
398
+ };
399
+ const end = () => {
400
+ if (!started)
401
+ return;
402
+ flushAssistant(true);
403
+ if (!lastWasBlank)
404
+ emitBlank();
405
+ emitStreamIoLine(colorize(`${CODEX_STREAM_IO_PREFIX} ------- output end --------`, "cyan", true));
406
+ emitBlank();
407
+ };
408
+ const emitLines = (lines, blankBefore = true) => {
409
+ if (!lines.length)
410
+ return;
411
+ start();
412
+ if (blankBefore && !lastWasBlank)
413
+ emitBlank();
414
+ for (const line of lines)
415
+ emitLine(line);
416
+ };
417
+ const flushAssistant = (force = false) => {
418
+ if (!assistantBuffer)
419
+ return;
420
+ if (!force && !assistantBuffer.includes("\n"))
421
+ return;
422
+ const chunks = assistantBuffer.split(/\r?\n/);
423
+ const trailing = assistantBuffer.endsWith("\n") ? "" : chunks.pop() ?? "";
424
+ const lines = [];
425
+ for (const rawLine of chunks) {
426
+ const line = rawLine.trimEnd();
427
+ if (!line.trim()) {
428
+ lines.push({ text: "" });
429
+ continue;
430
+ }
431
+ lines.push({ text: `assistant: ${line}`, color: "green" });
432
+ }
433
+ if (lines.length) {
434
+ emitLines(lines, !assistantActive);
435
+ assistantActive = true;
436
+ }
437
+ assistantBuffer = trailing ?? "";
438
+ if (force && assistantBuffer.trim()) {
439
+ emitLines([{ text: `assistant: ${assistantBuffer.trimEnd()}`, color: "green" }], !assistantActive);
440
+ assistantBuffer = "";
441
+ assistantActive = false;
442
+ }
443
+ if (force && !assistantBuffer) {
444
+ assistantActive = false;
445
+ }
446
+ };
447
+ const handleLine = (line) => {
448
+ if (!isStreamIoEnabled())
449
+ return;
450
+ start();
451
+ if (isStreamIoRaw()) {
452
+ emitLine({ text: line });
453
+ return;
454
+ }
455
+ const parsed = safeJsonParse(line);
456
+ if (!parsed) {
457
+ emitLine({ text: line });
458
+ return;
459
+ }
460
+ const type = typeof parsed?.type === "string" ? parsed.type : "";
461
+ if (type.startsWith("output_text.")) {
462
+ const event = extractAssistantText(parsed);
463
+ if (event?.text) {
464
+ assistantBuffer += event.text;
465
+ flushAssistant(event.kind === "final");
466
+ return;
467
+ }
468
+ }
469
+ if (assistantActive) {
470
+ flushAssistant(true);
471
+ }
472
+ const formatted = formatCodexEvent(parsed);
473
+ emitLines(formatted, true);
474
+ };
475
+ return { handleLine, end };
476
+ };
5
477
  const normalizeReasoningEffort = (raw) => {
6
478
  if (!raw)
7
479
  return undefined;
@@ -25,6 +497,50 @@ const resolveReasoningEffort = (model) => {
25
497
  return "high";
26
498
  return undefined;
27
499
  };
500
+ const extractAssistantText = (parsed) => {
501
+ if (!parsed || typeof parsed !== "object")
502
+ return null;
503
+ const type = typeof parsed.type === "string" ? parsed.type : "";
504
+ if (type.includes("output_text.delta") && typeof parsed.delta === "string") {
505
+ return { text: parsed.delta, kind: "delta" };
506
+ }
507
+ if (type.includes("output_text.done") && typeof parsed.text === "string") {
508
+ return { text: parsed.text, kind: "final" };
509
+ }
510
+ const item = parsed.item;
511
+ const itemType = item?.item_type ?? item?.itemType ?? item?.type;
512
+ if (!itemType)
513
+ return null;
514
+ const normalizedType = String(itemType).toLowerCase();
515
+ if (normalizedType !== "assistant_message" && normalizedType !== "agent_message")
516
+ return null;
517
+ if (typeof item.delta === "string") {
518
+ return { text: item.delta, kind: "delta" };
519
+ }
520
+ if (Array.isArray(item.delta)) {
521
+ const parts = item.delta
522
+ .map((entry) => (typeof entry?.text === "string" ? entry.text : ""))
523
+ .filter(Boolean)
524
+ .join("");
525
+ if (parts) {
526
+ return { text: parts, kind: "delta" };
527
+ }
528
+ }
529
+ const isFinal = type.includes("completed") || type.includes("done");
530
+ if (typeof item.text === "string" && isFinal) {
531
+ return { text: item.text, kind: "final" };
532
+ }
533
+ if (Array.isArray(item.content) && isFinal) {
534
+ const parts = item.content
535
+ .map((entry) => (typeof entry?.text === "string" ? entry.text : ""))
536
+ .filter(Boolean)
537
+ .join("");
538
+ if (parts) {
539
+ return { text: parts, kind: "final" };
540
+ }
541
+ }
542
+ return null;
543
+ };
28
544
  export const cliHealthy = (throwOnError = false) => {
29
545
  if (process.env.MCODA_CLI_STUB === "1") {
30
546
  return { ok: true, details: { stub: true } };
@@ -61,11 +577,16 @@ export const runCodexExec = (prompt, model) => {
61
577
  }
62
578
  const health = cliHealthy(true);
63
579
  const resolvedModel = model ?? "gpt-5.1-codex-max";
64
- const args = ["exec", "--model", resolvedModel, "--full-auto", "--json"];
580
+ const sandboxArgs = resolveSandboxArgs();
581
+ const args = [...sandboxArgs.args, "exec", "--model", resolvedModel, "--json"];
582
+ if (!sandboxArgs.bypass) {
583
+ args.push("--full-auto");
584
+ }
65
585
  const reasoningEffort = resolveReasoningEffort(resolvedModel);
66
586
  if (reasoningEffort) {
67
587
  args.push("-c", `model_reasoning_effort=${reasoningEffort}`);
68
588
  }
589
+ args.push("-");
69
590
  const result = spawnSync("codex", args, {
70
591
  input: prompt,
71
592
  encoding: "utf8",
@@ -83,13 +604,18 @@ export const runCodexExec = (prompt, model) => {
83
604
  }
84
605
  const raw = result.stdout?.toString() ?? "";
85
606
  const lines = raw.split(/\r?\n/).filter((l) => l.trim().length > 0);
86
- let message;
607
+ let message = "";
87
608
  for (const line of lines) {
88
609
  try {
89
610
  const parsed = JSON.parse(line);
90
- if (parsed?.type === "item.completed" && parsed?.item?.type === "agent_message" && typeof parsed?.item?.text === "string") {
91
- message = parsed.item.text;
611
+ const event = extractAssistantText(parsed);
612
+ if (!event)
613
+ continue;
614
+ if (event.kind === "delta") {
615
+ message += event.text;
616
+ continue;
92
617
  }
618
+ message = event.text;
93
619
  }
94
620
  catch {
95
621
  /* ignore parse errors */
@@ -109,11 +635,16 @@ export async function* runCodexExecStream(prompt, model) {
109
635
  }
110
636
  cliHealthy(true);
111
637
  const resolvedModel = model ?? "gpt-5.1-codex-max";
112
- const args = ["exec", "--model", resolvedModel, "--full-auto", "--json"];
638
+ const sandboxArgs = resolveSandboxArgs();
639
+ const args = [...sandboxArgs.args, "exec", "--model", resolvedModel, "--json"];
640
+ if (!sandboxArgs.bypass) {
641
+ args.push("--full-auto");
642
+ }
113
643
  const reasoningEffort = resolveReasoningEffort(resolvedModel);
114
644
  if (reasoningEffort) {
115
645
  args.push("-c", `model_reasoning_effort=${reasoningEffort}`);
116
646
  }
647
+ args.push("-");
117
648
  const child = spawn("codex", args, { stdio: ["pipe", "pipe", "pipe"] });
118
649
  child.stdin.write(prompt);
119
650
  child.stdin.end();
@@ -129,50 +660,78 @@ export async function* runCodexExecStream(prompt, model) {
129
660
  const parseLine = (line) => {
130
661
  try {
131
662
  const parsed = JSON.parse(line);
132
- const item = parsed?.item;
133
- if (item?.type === "agent_message" && typeof item.text === "string") {
134
- return item.text;
135
- }
136
- // The codex CLI emits many JSONL event types (thread/turn/task/tool events).
137
- // We only want the agent's textual output here.
138
- return null;
663
+ return extractAssistantText(parsed);
139
664
  }
140
665
  catch {
141
- // `codex exec --json` is expected to emit JSONL, but it can still print non-JSON
142
- // preamble lines (e.g., "Reading prompt from stdin..."). Treat those as noise.
143
666
  return null;
144
667
  }
145
668
  };
146
- const normalizeOutput = (value) => (value.endsWith("\n") ? value : `${value}\n`);
147
669
  const stream = child.stdout;
148
670
  stream?.setEncoding("utf8");
671
+ const formatter = createStreamFormatter(resolvedModel);
149
672
  let buffer = "";
150
- for await (const chunk of stream ?? []) {
151
- buffer += chunk;
152
- let idx;
153
- while ((idx = buffer.indexOf("\n")) !== -1) {
154
- const line = buffer.slice(0, idx);
155
- buffer = buffer.slice(idx + 1);
156
- const normalized = line.replace(/\r$/, "");
157
- const parsed = parseLine(normalized);
158
- if (!parsed)
159
- continue;
160
- const output = normalizeOutput(parsed);
161
- yield { output, raw: normalized };
673
+ let sawDelta = false;
674
+ let streamError = null;
675
+ try {
676
+ for await (const chunk of stream ?? []) {
677
+ buffer += chunk;
678
+ let idx;
679
+ while ((idx = buffer.indexOf("\n")) !== -1) {
680
+ const line = buffer.slice(0, idx);
681
+ buffer = buffer.slice(idx + 1);
682
+ const normalized = line.replace(/\r$/, "");
683
+ formatter.handleLine(normalized);
684
+ const parsed = parseLine(normalized);
685
+ if (!parsed)
686
+ continue;
687
+ if (parsed.kind === "delta") {
688
+ sawDelta = true;
689
+ yield { output: parsed.text, raw: normalized };
690
+ continue;
691
+ }
692
+ if (!sawDelta) {
693
+ const output = parsed.text.endsWith("\n") ? parsed.text : `${parsed.text}\n`;
694
+ yield { output, raw: normalized };
695
+ }
696
+ sawDelta = false;
697
+ }
162
698
  }
163
- }
164
- const trailing = buffer.replace(/\r$/, "");
165
- if (trailing) {
166
- const parsed = parseLine(trailing);
167
- if (parsed) {
168
- const output = normalizeOutput(parsed);
169
- yield { output, raw: trailing };
699
+ const trailing = buffer.replace(/\r$/, "");
700
+ if (trailing) {
701
+ formatter.handleLine(trailing);
702
+ const parsed = parseLine(trailing);
703
+ if (parsed) {
704
+ if (parsed.kind === "delta") {
705
+ sawDelta = true;
706
+ yield { output: parsed.text, raw: trailing };
707
+ }
708
+ else if (!sawDelta) {
709
+ const output = parsed.text.endsWith("\n") ? parsed.text : `${parsed.text}\n`;
710
+ yield { output, raw: trailing };
711
+ sawDelta = false;
712
+ }
713
+ }
170
714
  }
171
715
  }
716
+ catch (error) {
717
+ streamError = error;
718
+ }
172
719
  const exitCode = await closePromise;
173
720
  if (exitCode !== 0) {
721
+ formatter.handleLine(JSON.stringify({
722
+ type: "error",
723
+ message: `codex exec failed with exit ${exitCode}: ${stderr || "no output"}`,
724
+ }));
174
725
  const error = new Error(`AUTH_ERROR: codex CLI failed (exit ${exitCode}): ${stderr || "no output"}`);
175
726
  error.details = { reason: "cli_error", exitCode, stderr };
727
+ formatter.end();
728
+ if (streamError) {
729
+ throw streamError;
730
+ }
176
731
  throw error;
177
732
  }
733
+ formatter.end();
734
+ if (streamError) {
735
+ throw streamError;
736
+ }
178
737
  }