@vigent/cli 0.0.0-stage → 0.2.1
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/LICENSE +21 -0
- package/README.md +261 -2
- package/dist/agents.js +65 -0
- package/dist/app.js +2487 -0
- package/dist/builtin-themes.js +352 -0
- package/dist/choices.js +73 -0
- package/dist/cli-engines.js +537 -0
- package/dist/cli.js +440 -0
- package/dist/clipboard.js +56 -0
- package/dist/color.js +126 -0
- package/dist/config-edit.js +61 -0
- package/dist/config.js +406 -0
- package/dist/demo.js +81 -0
- package/dist/diffview.js +94 -0
- package/dist/engines.js +195 -0
- package/dist/export.js +29 -0
- package/dist/float.js +182 -0
- package/dist/import.js +260 -0
- package/dist/keys.js +589 -0
- package/dist/markdown.js +80 -0
- package/dist/palette.js +83 -0
- package/dist/picker.js +301 -0
- package/dist/review.js +158 -0
- package/dist/riftri.js +284 -0
- package/dist/session.js +460 -0
- package/dist/settings.js +174 -0
- package/dist/suggest.js +31 -0
- package/dist/theme-new.js +49 -0
- package/dist/theme.js +487 -0
- package/dist/tree.js +115 -0
- package/dist/version.js +45 -0
- package/dist/vimcolors.js +206 -0
- package/dist/worktree.js +357 -0
- package/package.json +64 -4
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents run by a coding-agent CLI you already have: Claude Code, Codex or
|
|
3
|
+
* OpenCode.
|
|
4
|
+
*
|
|
5
|
+
* vigent starts the CLI once per prompt in the agent's worktree, in its
|
|
6
|
+
* non-interactive JSON mode, and turns the events it prints into the
|
|
7
|
+
* transcript. Each CLI keeps its own conversation; vigent only remembers the
|
|
8
|
+
* id it prints and passes it back to resume. The CLI uses its own login:
|
|
9
|
+
* vigent never reads its credentials.
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import { ENGINE_BINS, ENGINE_NAMES } from "./engines.js";
|
|
14
|
+
import { Session } from "./session.js";
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Claude Code: claude -p --output-format stream-json
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
export const claudeAdapter = {
|
|
19
|
+
id: "claude",
|
|
20
|
+
bin: ENGINE_BINS.claude,
|
|
21
|
+
// -p can't ask before a tool runs; edits are allowed, other tools follow your Claude Code settings
|
|
22
|
+
defaultArgs: ["--permission-mode", "acceptEdits"],
|
|
23
|
+
args(prompt, o) {
|
|
24
|
+
return [
|
|
25
|
+
"-p",
|
|
26
|
+
"--output-format",
|
|
27
|
+
"stream-json",
|
|
28
|
+
"--verbose",
|
|
29
|
+
"--include-partial-messages",
|
|
30
|
+
...(o.extraArgs ?? this.defaultArgs),
|
|
31
|
+
...(o.model ? ["--model", o.model] : []),
|
|
32
|
+
...(o.effort ? ["--effort", o.effort] : []),
|
|
33
|
+
...(o.resumeId ? ["--resume", o.resumeId] : []),
|
|
34
|
+
"--",
|
|
35
|
+
prompt,
|
|
36
|
+
];
|
|
37
|
+
},
|
|
38
|
+
parser() {
|
|
39
|
+
return (e) => {
|
|
40
|
+
if (!e || typeof e !== "object")
|
|
41
|
+
return [];
|
|
42
|
+
// a subagent's own messages: only its result reaches the transcript
|
|
43
|
+
if (e.parent_tool_use_id)
|
|
44
|
+
return [];
|
|
45
|
+
const out = [];
|
|
46
|
+
switch (e.type) {
|
|
47
|
+
case "system":
|
|
48
|
+
if (e.subtype === "init") {
|
|
49
|
+
if (e.session_id)
|
|
50
|
+
out.push({ type: "session", id: e.session_id });
|
|
51
|
+
if (e.model)
|
|
52
|
+
out.push({ type: "model", model: e.model });
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
case "stream_event": {
|
|
56
|
+
const ev = e.event ?? {};
|
|
57
|
+
if (ev.type === "content_block_delta" && ev.delta?.type === "text_delta" && ev.delta.text) {
|
|
58
|
+
out.push({ type: "text_delta", text: ev.delta.text });
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case "assistant":
|
|
63
|
+
for (const b of contentOf(e.message)) {
|
|
64
|
+
if (b?.type === "text" && b.text)
|
|
65
|
+
out.push({ type: "text", text: b.text });
|
|
66
|
+
else if (b?.type === "tool_use")
|
|
67
|
+
out.push({ type: "tool", id: b.id, name: b.name, summary: summarize(b.input) });
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
case "user":
|
|
71
|
+
for (const b of contentOf(e.message)) {
|
|
72
|
+
if (b?.type === "tool_result")
|
|
73
|
+
out.push({ type: "tool_result", id: b.tool_use_id, output: textOf(b.content) });
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case "result": {
|
|
77
|
+
const u = e.usage ?? {};
|
|
78
|
+
const tokens = (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.output_tokens ?? 0);
|
|
79
|
+
if (tokens)
|
|
80
|
+
out.push({ type: "usage", tokens });
|
|
81
|
+
if (e.session_id)
|
|
82
|
+
out.push({ type: "session", id: e.session_id });
|
|
83
|
+
const ok = !e.is_error && e.subtype === "success";
|
|
84
|
+
out.push({ type: "done", ok, ...(ok ? {} : { error: String(e.result ?? e.errors?.join?.("; ") ?? e.subtype ?? "failed") }) });
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Codex: codex exec --json
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
export const codexAdapter = {
|
|
96
|
+
id: "codex",
|
|
97
|
+
bin: ENGINE_BINS.codex,
|
|
98
|
+
// edits and commands inside the worktree, nothing outside it
|
|
99
|
+
defaultArgs: ["-c", 'sandbox_mode="workspace-write"'],
|
|
100
|
+
args(prompt, o) {
|
|
101
|
+
return [
|
|
102
|
+
"exec",
|
|
103
|
+
...(o.resumeId ? ["resume"] : []),
|
|
104
|
+
"--json",
|
|
105
|
+
"--skip-git-repo-check",
|
|
106
|
+
...(o.extraArgs ?? this.defaultArgs),
|
|
107
|
+
...(o.model ? ["-m", o.model] : []),
|
|
108
|
+
...(o.effort ? ["-c", `model_reasoning_effort="${o.effort}"`] : []),
|
|
109
|
+
...(o.resumeId ? [o.resumeId] : []),
|
|
110
|
+
"--",
|
|
111
|
+
prompt,
|
|
112
|
+
];
|
|
113
|
+
},
|
|
114
|
+
parser() {
|
|
115
|
+
let lastError;
|
|
116
|
+
const started = new Set();
|
|
117
|
+
const tool = (item) => {
|
|
118
|
+
switch (item.type) {
|
|
119
|
+
case "command_execution":
|
|
120
|
+
return { type: "tool", id: item.id, name: "bash", summary: shellCommand(item.command) };
|
|
121
|
+
case "file_change":
|
|
122
|
+
return {
|
|
123
|
+
type: "tool",
|
|
124
|
+
id: item.id,
|
|
125
|
+
name: "edit",
|
|
126
|
+
summary: (item.changes ?? []).map((c) => `${c.kind === "add" ? "+" : c.kind === "delete" ? "-" : ""}${c.path}`).join(", "),
|
|
127
|
+
};
|
|
128
|
+
case "mcp_tool_call":
|
|
129
|
+
return { type: "tool", id: item.id, name: [item.server, item.tool].filter(Boolean).join("."), summary: summarize(item.arguments) };
|
|
130
|
+
case "web_search":
|
|
131
|
+
return { type: "tool", id: item.id, name: "web_search", summary: String(item.query ?? "") };
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
};
|
|
135
|
+
return (e) => {
|
|
136
|
+
if (!e || typeof e !== "object")
|
|
137
|
+
return [];
|
|
138
|
+
const out = [];
|
|
139
|
+
switch (e.type) {
|
|
140
|
+
case "thread.started":
|
|
141
|
+
if (e.thread_id)
|
|
142
|
+
out.push({ type: "session", id: e.thread_id });
|
|
143
|
+
break;
|
|
144
|
+
case "item.started": {
|
|
145
|
+
const t = e.item && tool(e.item);
|
|
146
|
+
if (t) {
|
|
147
|
+
started.add(e.item.id);
|
|
148
|
+
out.push(t);
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "item.completed": {
|
|
153
|
+
const item = e.item ?? {};
|
|
154
|
+
if (item.type === "agent_message" && item.text) {
|
|
155
|
+
out.push({ type: "text", text: item.text });
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
const t = tool(item);
|
|
159
|
+
if (!t)
|
|
160
|
+
break;
|
|
161
|
+
if (!started.has(item.id))
|
|
162
|
+
out.push(t);
|
|
163
|
+
const output = item.type === "command_execution"
|
|
164
|
+
? `${String(item.aggregated_output ?? "").trimEnd()}${item.exit_code ? `\n(exit ${item.exit_code})` : ""}`.trim()
|
|
165
|
+
: item.type === "mcp_tool_call"
|
|
166
|
+
? textOf(item.result?.content ?? item.error?.message ?? "")
|
|
167
|
+
: item.status === "failed"
|
|
168
|
+
? "failed"
|
|
169
|
+
: "";
|
|
170
|
+
out.push({ type: "tool_result", id: item.id, output });
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case "turn.completed": {
|
|
174
|
+
const u = e.usage ?? {};
|
|
175
|
+
const tokens = (u.input_tokens ?? 0) + (u.output_tokens ?? 0);
|
|
176
|
+
if (tokens)
|
|
177
|
+
out.push({ type: "usage", tokens });
|
|
178
|
+
out.push({ type: "done", ok: true });
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case "turn.failed":
|
|
182
|
+
out.push({ type: "done", ok: false, error: e.error?.message ?? lastError ?? "turn failed" });
|
|
183
|
+
break;
|
|
184
|
+
case "error":
|
|
185
|
+
// "Reconnecting... 2/5" is codex retrying; only the final error matters
|
|
186
|
+
if (typeof e.message === "string" && !/^Reconnecting\.\.\./.test(e.message))
|
|
187
|
+
lastError = e.message;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// OpenCode: opencode run --format json
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
export const opencodeAdapter = {
|
|
198
|
+
id: "opencode",
|
|
199
|
+
bin: ENGINE_BINS.opencode,
|
|
200
|
+
defaultArgs: [],
|
|
201
|
+
args(prompt, o) {
|
|
202
|
+
return [
|
|
203
|
+
"run",
|
|
204
|
+
"--format",
|
|
205
|
+
"json",
|
|
206
|
+
"--dir",
|
|
207
|
+
o.cwd,
|
|
208
|
+
...(o.extraArgs ?? this.defaultArgs),
|
|
209
|
+
...(o.model ? ["-m", o.model] : []),
|
|
210
|
+
...(o.effort ? ["--variant", o.effort] : []),
|
|
211
|
+
...(o.resumeId ? ["--session", o.resumeId] : []),
|
|
212
|
+
"--",
|
|
213
|
+
prompt,
|
|
214
|
+
];
|
|
215
|
+
},
|
|
216
|
+
parser() {
|
|
217
|
+
let sessionId;
|
|
218
|
+
return (e) => {
|
|
219
|
+
if (!e || typeof e !== "object")
|
|
220
|
+
return [];
|
|
221
|
+
const out = [];
|
|
222
|
+
if (e.sessionID && e.sessionID !== sessionId) {
|
|
223
|
+
sessionId = e.sessionID;
|
|
224
|
+
out.push({ type: "session", id: e.sessionID });
|
|
225
|
+
}
|
|
226
|
+
const part = e.part ?? {};
|
|
227
|
+
switch (e.type) {
|
|
228
|
+
case "text":
|
|
229
|
+
if (part.text?.trim())
|
|
230
|
+
out.push({ type: "text", text: part.text.trim() });
|
|
231
|
+
break;
|
|
232
|
+
case "tool_use": {
|
|
233
|
+
const st = part.state ?? {};
|
|
234
|
+
out.push({ type: "tool", id: part.callID ?? part.id, name: part.tool ?? "tool", summary: st.title || summarize(st.input) });
|
|
235
|
+
out.push({ type: "tool_result", id: part.callID ?? part.id, output: String(st.output ?? st.error ?? "") });
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
case "step_finish": {
|
|
239
|
+
const t = part.tokens ?? {};
|
|
240
|
+
const tokens = (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0);
|
|
241
|
+
if (tokens)
|
|
242
|
+
out.push({ type: "usage", tokens });
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case "error": {
|
|
246
|
+
const err = e.error ?? {};
|
|
247
|
+
out.push({ type: "error", message: String(err.data?.message ?? err.message ?? err.name ?? "error") });
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
export const CLI_ADAPTERS = {
|
|
256
|
+
claude: claudeAdapter,
|
|
257
|
+
codex: codexAdapter,
|
|
258
|
+
opencode: opencodeAdapter,
|
|
259
|
+
};
|
|
260
|
+
export class CliSession extends Session {
|
|
261
|
+
opts;
|
|
262
|
+
cwd;
|
|
263
|
+
resumeId;
|
|
264
|
+
adapter;
|
|
265
|
+
child;
|
|
266
|
+
aborted = false;
|
|
267
|
+
/** the model the user asked for; undefined = the CLI's own default */
|
|
268
|
+
requestedModel;
|
|
269
|
+
constructor(name, engine, model, opts) {
|
|
270
|
+
super(name, model ?? ENGINE_NAMES[engine], opts.worktree);
|
|
271
|
+
this.opts = opts;
|
|
272
|
+
this.engine = engine;
|
|
273
|
+
this.adapter = CLI_ADAPTERS[engine];
|
|
274
|
+
this.cwd = opts.cwd;
|
|
275
|
+
this.requestedModel = model;
|
|
276
|
+
this.effort = opts.effort;
|
|
277
|
+
this.resumeId = opts.resumeId;
|
|
278
|
+
if (opts.resumeId) {
|
|
279
|
+
this.pushInfo(`continuing the ${ENGINE_NAMES[engine]} conversation ${opts.resumeId.slice(0, 8)}… (earlier messages live in ${ENGINE_NAMES[engine]}'s history)`);
|
|
280
|
+
this.refreshStat(0);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
get engineName() {
|
|
284
|
+
return ENGINE_NAMES[this.engine];
|
|
285
|
+
}
|
|
286
|
+
/** The model for the next prompt; undefined (or "default") = the CLI's own. */
|
|
287
|
+
setModel(model) {
|
|
288
|
+
this.requestedModel = model && model !== "default" ? model : undefined;
|
|
289
|
+
this.model = this.requestedModel ?? ENGINE_NAMES[this.engine];
|
|
290
|
+
this.emit();
|
|
291
|
+
}
|
|
292
|
+
/** Reasoning effort for the next prompt; undefined = the CLI's own. */
|
|
293
|
+
setEffort(effort) {
|
|
294
|
+
this.effort = effort && effort !== "default" ? effort : undefined;
|
|
295
|
+
this.emit();
|
|
296
|
+
}
|
|
297
|
+
/** what was asked for (vs. what the CLI reported it used) */
|
|
298
|
+
get chosenModel() {
|
|
299
|
+
return this.requestedModel;
|
|
300
|
+
}
|
|
301
|
+
send(text) {
|
|
302
|
+
this.pushUser(text);
|
|
303
|
+
this.setState("running");
|
|
304
|
+
this.aborted = false;
|
|
305
|
+
const args = this.adapter.args(text, {
|
|
306
|
+
cwd: this.cwd,
|
|
307
|
+
model: this.requestedModel,
|
|
308
|
+
effort: this.effort,
|
|
309
|
+
resumeId: this.resumeId,
|
|
310
|
+
extraArgs: this.opts.extraArgs,
|
|
311
|
+
});
|
|
312
|
+
let child;
|
|
313
|
+
try {
|
|
314
|
+
child = spawn(this.opts.bin ?? this.adapter.bin, args, {
|
|
315
|
+
cwd: this.cwd,
|
|
316
|
+
env: this.opts.env ?? process.env,
|
|
317
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
this.fail(`couldn't start ${this.adapter.bin}: ${err?.message ?? err}`);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.child = child;
|
|
325
|
+
const parse = this.adapter.parser();
|
|
326
|
+
let buf = "";
|
|
327
|
+
let stderr = "";
|
|
328
|
+
let result;
|
|
329
|
+
let lastError;
|
|
330
|
+
const tools = new Map();
|
|
331
|
+
const handle = (ev) => {
|
|
332
|
+
switch (ev.type) {
|
|
333
|
+
case "session":
|
|
334
|
+
if (ev.id !== this.resumeId) {
|
|
335
|
+
this.resumeId = ev.id;
|
|
336
|
+
this.opts.onResumeId?.(ev.id);
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
case "model":
|
|
340
|
+
if (!this.requestedModel)
|
|
341
|
+
this.model = ev.model;
|
|
342
|
+
this.emit();
|
|
343
|
+
break;
|
|
344
|
+
case "text_delta": {
|
|
345
|
+
const last = this.items.at(-1);
|
|
346
|
+
if (last?.kind === "agent" && last.streaming)
|
|
347
|
+
last.text += ev.text;
|
|
348
|
+
else
|
|
349
|
+
this.items.push({ kind: "agent", text: ev.text, streaming: true });
|
|
350
|
+
this.emit();
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case "text": {
|
|
354
|
+
const last = this.items.at(-1);
|
|
355
|
+
if (last?.kind === "agent" && last.streaming) {
|
|
356
|
+
last.text = ev.text;
|
|
357
|
+
last.streaming = false;
|
|
358
|
+
}
|
|
359
|
+
else
|
|
360
|
+
this.items.push({ kind: "agent", text: ev.text });
|
|
361
|
+
this.emit();
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case "tool": {
|
|
365
|
+
this.endStreaming();
|
|
366
|
+
const summary = this.relative(ev.summary).slice(0, 80);
|
|
367
|
+
const item = { kind: "tool", text: "", tool: { name: ev.name, summary, output: "", folded: true } };
|
|
368
|
+
this.items.push(item);
|
|
369
|
+
if (ev.id)
|
|
370
|
+
tools.set(ev.id, item);
|
|
371
|
+
this.emit();
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case "tool_result": {
|
|
375
|
+
const item = (ev.id && tools.get(ev.id)) || [...this.items].reverse().find((i) => i.kind === "tool");
|
|
376
|
+
if (item?.tool)
|
|
377
|
+
item.tool.output = ev.output;
|
|
378
|
+
this.emit();
|
|
379
|
+
this.refreshStat();
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
case "usage":
|
|
383
|
+
this.tokens += ev.tokens;
|
|
384
|
+
this.emit();
|
|
385
|
+
break;
|
|
386
|
+
case "error":
|
|
387
|
+
lastError = ev.message;
|
|
388
|
+
break;
|
|
389
|
+
case "done":
|
|
390
|
+
result = ev;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
child.stdout.setEncoding("utf8");
|
|
395
|
+
child.stdout.on("data", (chunk) => {
|
|
396
|
+
buf += chunk;
|
|
397
|
+
let nl;
|
|
398
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
399
|
+
const line = buf.slice(0, nl).trim();
|
|
400
|
+
buf = buf.slice(nl + 1);
|
|
401
|
+
if (!line.startsWith("{"))
|
|
402
|
+
continue;
|
|
403
|
+
let json;
|
|
404
|
+
try {
|
|
405
|
+
json = JSON.parse(line);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
continue; // not an event (a stray log line)
|
|
409
|
+
}
|
|
410
|
+
for (const ev of parse(json))
|
|
411
|
+
handle(ev);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
child.stderr.setEncoding("utf8");
|
|
415
|
+
child.stderr.on("data", (chunk) => {
|
|
416
|
+
stderr = (stderr + chunk).slice(-4000);
|
|
417
|
+
});
|
|
418
|
+
child.on("error", (err) => {
|
|
419
|
+
this.child = undefined;
|
|
420
|
+
this.fail(err.code === "ENOENT"
|
|
421
|
+
? `${this.adapter.bin} isn't installed (or isn't on PATH) · :checkhealth`
|
|
422
|
+
: `${this.adapter.bin}: ${err.message}`);
|
|
423
|
+
});
|
|
424
|
+
child.on("close", (code, signal) => {
|
|
425
|
+
if (this.child !== child)
|
|
426
|
+
return; // already failed to start
|
|
427
|
+
this.child = undefined;
|
|
428
|
+
this.endStreaming();
|
|
429
|
+
this.refreshStat(0);
|
|
430
|
+
if (this.aborted) {
|
|
431
|
+
this.setState("idle");
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const ok = result ? result.ok : code === 0 && !lastError;
|
|
435
|
+
if (ok) {
|
|
436
|
+
this.setState("idle");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const why = result?.error ??
|
|
440
|
+
lastError ??
|
|
441
|
+
lastLines(stderr) ??
|
|
442
|
+
(signal ? `stopped by ${signal}` : `exited with code ${code}`);
|
|
443
|
+
this.fail(`${this.engineName}: ${why}`);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
/** paths inside the agent's folder, shown relative to it */
|
|
447
|
+
relative(text) {
|
|
448
|
+
let out = text;
|
|
449
|
+
for (const dir of new Set([this.cwd, safeRealpath(this.cwd)])) {
|
|
450
|
+
if (dir)
|
|
451
|
+
out = out.split(`${dir.replace(/\/+$/, "")}/`).join("");
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
endStreaming() {
|
|
456
|
+
const last = this.items.at(-1);
|
|
457
|
+
if (last?.kind === "agent" && last.streaming) {
|
|
458
|
+
last.streaming = false;
|
|
459
|
+
if (!last.text)
|
|
460
|
+
this.items.pop();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
fail(message) {
|
|
464
|
+
this.endStreaming();
|
|
465
|
+
this.pushInfo(`error: ${message}`);
|
|
466
|
+
this.setState("error");
|
|
467
|
+
}
|
|
468
|
+
abort() {
|
|
469
|
+
const child = this.child;
|
|
470
|
+
if (child) {
|
|
471
|
+
this.aborted = true;
|
|
472
|
+
child.kill("SIGINT");
|
|
473
|
+
const t = setTimeout(() => {
|
|
474
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
475
|
+
child.kill("SIGTERM");
|
|
476
|
+
}, 2000);
|
|
477
|
+
t.unref();
|
|
478
|
+
}
|
|
479
|
+
this.setState("idle");
|
|
480
|
+
}
|
|
481
|
+
dispose() {
|
|
482
|
+
super.dispose();
|
|
483
|
+
if (this.child) {
|
|
484
|
+
this.aborted = true;
|
|
485
|
+
this.child.kill("SIGTERM");
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
// helpers
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
function safeRealpath(p) {
|
|
493
|
+
try {
|
|
494
|
+
return fs.realpathSync(p);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function contentOf(message) {
|
|
501
|
+
return Array.isArray(message?.content) ? message.content : [];
|
|
502
|
+
}
|
|
503
|
+
function textOf(content) {
|
|
504
|
+
if (typeof content === "string")
|
|
505
|
+
return content;
|
|
506
|
+
if (!Array.isArray(content))
|
|
507
|
+
return content == null ? "" : JSON.stringify(content);
|
|
508
|
+
return content
|
|
509
|
+
.filter((b) => b?.type === "text" && typeof b.text === "string")
|
|
510
|
+
.map((b) => b.text)
|
|
511
|
+
.join("\n");
|
|
512
|
+
}
|
|
513
|
+
function summarize(args) {
|
|
514
|
+
if (!args)
|
|
515
|
+
return "";
|
|
516
|
+
if (typeof args === "string")
|
|
517
|
+
return args.slice(0, 80);
|
|
518
|
+
const v = args.command ?? args.file_path ?? args.filePath ?? args.path ?? args.pattern ?? args.url ?? args.description ?? JSON.stringify(args);
|
|
519
|
+
return String(v).slice(0, 80);
|
|
520
|
+
}
|
|
521
|
+
/** codex reports commands as `/bin/bash -lc "npm test"` (or an argv array); show the command itself. */
|
|
522
|
+
function shellCommand(cmd) {
|
|
523
|
+
const parts = Array.isArray(cmd) ? cmd.map(String) : undefined;
|
|
524
|
+
if (parts && parts.length >= 3 && /(^|\/)(ba|z)?sh$/.test(parts[0]) && /^-l?c$/.test(parts[1]))
|
|
525
|
+
return parts.slice(2).join(" ");
|
|
526
|
+
const s = parts ? parts.join(" ") : String(cmd ?? "");
|
|
527
|
+
const m = /^\S*\/(?:ba|z)?sh\s+-l?c\s+(['"])([\s\S]*)\1$/.exec(s);
|
|
528
|
+
return m ? m[2] : s;
|
|
529
|
+
}
|
|
530
|
+
function lastLines(stderr) {
|
|
531
|
+
const lines = stderr
|
|
532
|
+
.replace(/\x1b\[[0-9;]*m/g, "")
|
|
533
|
+
.split("\n")
|
|
534
|
+
.map((l) => l.trim())
|
|
535
|
+
.filter((l) => l && !/^Reading additional input from stdin/.test(l) && !/^Warning: no stdin data/.test(l));
|
|
536
|
+
return lines.length ? lines.slice(-2).join(" · ") : undefined;
|
|
537
|
+
}
|