@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.
- package/dist/AgentService/AgentService.d.ts +2 -1
- package/dist/AgentService/AgentService.d.ts.map +1 -1
- package/dist/AgentService/AgentService.js +161 -28
- package/dist/adapters/AdapterTypes.d.ts +10 -0
- package/dist/adapters/AdapterTypes.d.ts.map +1 -1
- package/dist/adapters/codali/CodaliAdapter.d.ts +19 -0
- package/dist/adapters/codali/CodaliAdapter.d.ts.map +1 -0
- package/dist/adapters/codali/CodaliAdapter.js +290 -0
- package/dist/adapters/codali/CodaliCliRunner.d.ts +36 -0
- package/dist/adapters/codali/CodaliCliRunner.d.ts.map +1 -0
- package/dist/adapters/codali/CodaliCliRunner.js +230 -0
- package/dist/adapters/codex/CodexCliRunner.d.ts.map +1 -1
- package/dist/adapters/codex/CodexCliRunner.js +593 -34
- package/dist/adapters/ollama/OllamaRemoteAdapter.d.ts.map +1 -1
- package/dist/adapters/ollama/OllamaRemoteAdapter.js +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/package.json +3 -3
|
@@ -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
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
}
|