@apolloyh/apollo-agent 0.1.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.
- package/.apollo/config.example.json +37 -0
- package/.env.example +17 -0
- package/README.md +62 -0
- package/dist/brand.d.ts +17 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +23 -0
- package/dist/cli-args.d.ts +13 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +50 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +190 -0
- package/dist/constants/spinner-verbs.d.ts +7 -0
- package/dist/constants/spinner-verbs.d.ts.map +1 -0
- package/dist/constants/spinner-verbs.js +21 -0
- package/dist/coordinator/workflow.d.ts +20 -0
- package/dist/coordinator/workflow.d.ts.map +1 -0
- package/dist/coordinator/workflow.js +120 -0
- package/dist/goal/goal-mode.d.ts +26 -0
- package/dist/goal/goal-mode.d.ts.map +1 -0
- package/dist/goal/goal-mode.js +141 -0
- package/dist/hooks/runner.d.ts +43 -0
- package/dist/hooks/runner.d.ts.map +1 -0
- package/dist/hooks/runner.js +94 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1716 -0
- package/dist/llm/anthropic.d.ts +46 -0
- package/dist/llm/anthropic.d.ts.map +1 -0
- package/dist/llm/anthropic.js +278 -0
- package/dist/mcp/manager.d.ts +14 -0
- package/dist/mcp/manager.d.ts.map +1 -0
- package/dist/mcp/manager.js +93 -0
- package/dist/memory/memdir.d.ts +19 -0
- package/dist/memory/memdir.d.ts.map +1 -0
- package/dist/memory/memdir.js +96 -0
- package/dist/plan/plan-mode.d.ts +14 -0
- package/dist/plan/plan-mode.d.ts.map +1 -0
- package/dist/plan/plan-mode.js +90 -0
- package/dist/project-instructions.d.ts +8 -0
- package/dist/project-instructions.d.ts.map +1 -0
- package/dist/project-instructions.js +33 -0
- package/dist/runtime/compaction.d.ts +5 -0
- package/dist/runtime/compaction.d.ts.map +1 -0
- package/dist/runtime/compaction.js +37 -0
- package/dist/runtime/context.d.ts +25 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/context.js +75 -0
- package/dist/runtime/permissions.d.ts +11 -0
- package/dist/runtime/permissions.d.ts.map +1 -0
- package/dist/runtime/permissions.js +119 -0
- package/dist/runtime/prompt-builder.d.ts +27 -0
- package/dist/runtime/prompt-builder.d.ts.map +1 -0
- package/dist/runtime/prompt-builder.js +112 -0
- package/dist/runtime/query-engine.d.ts +136 -0
- package/dist/runtime/query-engine.d.ts.map +1 -0
- package/dist/runtime/query-engine.js +1182 -0
- package/dist/runtime/tool-policy.d.ts +13 -0
- package/dist/runtime/tool-policy.d.ts.map +1 -0
- package/dist/runtime/tool-policy.js +106 -0
- package/dist/sdk.d.ts +33 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +41 -0
- package/dist/session/resume-ui.d.ts +17 -0
- package/dist/session/resume-ui.d.ts.map +1 -0
- package/dist/session/resume-ui.js +64 -0
- package/dist/session/store.d.ts +66 -0
- package/dist/session/store.d.ts.map +1 -0
- package/dist/session/store.js +120 -0
- package/dist/skillify/skillify.d.ts +12 -0
- package/dist/skillify/skillify.d.ts.map +1 -0
- package/dist/skillify/skillify.js +52 -0
- package/dist/skills/skills.d.ts +26 -0
- package/dist/skills/skills.d.ts.map +1 -0
- package/dist/skills/skills.js +195 -0
- package/dist/status-ui.d.ts +37 -0
- package/dist/status-ui.d.ts.map +1 -0
- package/dist/status-ui.js +130 -0
- package/dist/thought-fold.d.ts +48 -0
- package/dist/thought-fold.d.ts.map +1 -0
- package/dist/thought-fold.js +156 -0
- package/dist/tools/builtin.d.ts +27 -0
- package/dist/tools/builtin.d.ts.map +1 -0
- package/dist/tools/builtin.js +898 -0
- package/dist/tools/registry.d.ts +17 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +86 -0
- package/dist/trace.d.ts +68 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +600 -0
- package/dist/types.d.ts +202 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +78 -0
- package/docs/sdk.md +259 -0
- package/package.json +53 -0
- package/src/skills/tech-research-skill/SKILL.md +200 -0
- package/src/skills/tech-research-skill/agents/openai.yaml +4 -0
- package/src/skills/tech-research-skill/repo-analyze +435 -0
- package/src/skills/tech-research-skill/repo-fetch +310 -0
- package/src/skills/tech-research-skill/report-generate +254 -0
package/dist/trace.js
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { stderr } from "node:process";
|
|
2
|
+
import { formatWaitLine } from "./status-ui.js";
|
|
3
|
+
const color = {
|
|
4
|
+
reset: "\x1b[0m",
|
|
5
|
+
normal: "",
|
|
6
|
+
dim: "\x1b[2m",
|
|
7
|
+
bold: "\x1b[1m",
|
|
8
|
+
cyan: "\x1b[36m",
|
|
9
|
+
blue: "\x1b[38;2;96;165;250m",
|
|
10
|
+
green: "\x1b[32m",
|
|
11
|
+
yellow: "\x1b[33m",
|
|
12
|
+
red: "\x1b[31m",
|
|
13
|
+
magenta: "\x1b[35m",
|
|
14
|
+
gray: "\x1b[90m",
|
|
15
|
+
};
|
|
16
|
+
export class CliTraceRenderer {
|
|
17
|
+
options;
|
|
18
|
+
spinner;
|
|
19
|
+
spinnerIndex = 0;
|
|
20
|
+
statusText = "";
|
|
21
|
+
statusDetail = "";
|
|
22
|
+
statusLines = 0;
|
|
23
|
+
statusStartedAt = 0;
|
|
24
|
+
wroteAssistantText = false;
|
|
25
|
+
thoughtStartedAt = 0;
|
|
26
|
+
turnStartedAt = 0;
|
|
27
|
+
hasPrintedTurn = false;
|
|
28
|
+
turnSpacingPrinted = false;
|
|
29
|
+
staticStatusActive = false;
|
|
30
|
+
assistantLineBuffer = "";
|
|
31
|
+
insideCodeFence = false;
|
|
32
|
+
activeTasks = new Map();
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.options = options;
|
|
35
|
+
}
|
|
36
|
+
sink() {
|
|
37
|
+
return (event) => this.render(event);
|
|
38
|
+
}
|
|
39
|
+
resetTurn() {
|
|
40
|
+
this.stopStatus();
|
|
41
|
+
this.wroteAssistantText = false;
|
|
42
|
+
this.turnSpacingPrinted = false;
|
|
43
|
+
this.statusDetail = "";
|
|
44
|
+
this.staticStatusActive = false;
|
|
45
|
+
this.assistantLineBuffer = "";
|
|
46
|
+
this.insideCodeFence = false;
|
|
47
|
+
this.thoughtStartedAt = 0;
|
|
48
|
+
this.turnStartedAt = Date.now();
|
|
49
|
+
this.activeTasks.clear();
|
|
50
|
+
}
|
|
51
|
+
finishTurn() {
|
|
52
|
+
this.stopStatus();
|
|
53
|
+
this.flushAssistantLine(false);
|
|
54
|
+
if (this.wroteAssistantText) {
|
|
55
|
+
this.out("\n");
|
|
56
|
+
this.wroteAssistantText = false;
|
|
57
|
+
}
|
|
58
|
+
if (this.options.verbose && this.turnStartedAt) {
|
|
59
|
+
this.bullet(`Done · ${this.elapsedSince(this.turnStartedAt)}`, "green");
|
|
60
|
+
this.out("\n");
|
|
61
|
+
}
|
|
62
|
+
this.hasPrintedTurn = true;
|
|
63
|
+
this.activeTasks.clear();
|
|
64
|
+
}
|
|
65
|
+
render(event) {
|
|
66
|
+
// Quiet mode: stream answers, flush before tools, and show concise results.
|
|
67
|
+
if (!this.options.verbose &&
|
|
68
|
+
event.type !== "assistant_delta" &&
|
|
69
|
+
event.type !== "llm_retry" &&
|
|
70
|
+
event.type !== "llm_tool_delta" &&
|
|
71
|
+
event.type !== "tool_call" &&
|
|
72
|
+
event.type !== "tool_result" &&
|
|
73
|
+
event.type !== "context_compacted" &&
|
|
74
|
+
event.type !== "context_compaction_failed")
|
|
75
|
+
return;
|
|
76
|
+
switch (event.type) {
|
|
77
|
+
case "llm_retry":
|
|
78
|
+
this.completeStatus(undefined, "yellow", Boolean(this.options.staticStatus));
|
|
79
|
+
this.bullet(`${retryLabel(event.mode, event.delayMs)} · ${compactValue(event.reason, 160)}`, "yellow");
|
|
80
|
+
break;
|
|
81
|
+
case "assistant_delta":
|
|
82
|
+
if (this.statusText === "thinking" && !this.statusDetail) {
|
|
83
|
+
this.statusDetail = "receiving direct answer";
|
|
84
|
+
}
|
|
85
|
+
// Interactive: index.ts already printed ● Thought — clear silently
|
|
86
|
+
this.completeStatus(undefined, "green", Boolean(this.options.staticStatus));
|
|
87
|
+
if (this.options.stream) {
|
|
88
|
+
if (this.options.verbose)
|
|
89
|
+
this.ensureTurnSpacing();
|
|
90
|
+
this.writeAssistantDelta(event.text);
|
|
91
|
+
this.wroteAssistantText = true;
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
case "llm_request":
|
|
95
|
+
if (this.options.verbose)
|
|
96
|
+
this.ensureTurnSpacing();
|
|
97
|
+
this.thoughtStartedAt = Date.now();
|
|
98
|
+
this.startStatus("thinking");
|
|
99
|
+
break;
|
|
100
|
+
case "llm_tool_delta":
|
|
101
|
+
if (this.options.staticStatus) {
|
|
102
|
+
this.flushAssistantLine(true);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
if (this.statusText !== "thinking")
|
|
106
|
+
this.startStatus("thinking");
|
|
107
|
+
this.statusDetail = `preparing ${event.tool ?? "tool"} input (${formatBytes(event.bytes)})`;
|
|
108
|
+
break;
|
|
109
|
+
case "llm_response":
|
|
110
|
+
if (this.statusText === "thinking") {
|
|
111
|
+
this.statusDetail = thoughtDetail(event.stopReason);
|
|
112
|
+
}
|
|
113
|
+
this.completeStatus(undefined, "green", Boolean(this.options.staticStatus));
|
|
114
|
+
if (!this.options.stream && event.text) {
|
|
115
|
+
if (this.options.verbose)
|
|
116
|
+
this.ensureTurnSpacing();
|
|
117
|
+
this.writeAssistantDelta(event.text);
|
|
118
|
+
this.flushAssistantLine(true);
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
case "tool_call":
|
|
122
|
+
this.flushAssistantLine(true);
|
|
123
|
+
if (event.tool === "Task") {
|
|
124
|
+
this.completeStatus(undefined, "green", Boolean(this.options.staticStatus));
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
this.completeStatus(undefined, "green", Boolean(this.options.staticStatus));
|
|
128
|
+
// Tool wait line is owned by index live status in interactive mode
|
|
129
|
+
if (this.options.staticStatus)
|
|
130
|
+
break;
|
|
131
|
+
this.ensureTurnSpacing();
|
|
132
|
+
this.startStatus(`using ${event.tool}`, `${riskLabel(event.risk)} ${compactToolInput(event.tool, event.input, 160)}`);
|
|
133
|
+
break;
|
|
134
|
+
case "tool_result":
|
|
135
|
+
if (event.tool === "Task")
|
|
136
|
+
break;
|
|
137
|
+
if (event.fileChange && !event.isError) {
|
|
138
|
+
this.completeStatus(undefined, "green", true);
|
|
139
|
+
this.renderFileChange(event.fileChange);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
if (this.options.staticStatus) {
|
|
143
|
+
const denied = isPermissionDenied(event.content);
|
|
144
|
+
this.renderInteractiveToolResult(event, denied);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
this.completeStatus(event.isError ? `${event.tool} failed ${compactValue(event.content, 240)}` : `${event.tool} done`, event.isError ? "normal" : "green");
|
|
148
|
+
break;
|
|
149
|
+
case "approval_required":
|
|
150
|
+
this.completeStatus(undefined, "green", Boolean(this.options.staticStatus));
|
|
151
|
+
break;
|
|
152
|
+
case "approval_result":
|
|
153
|
+
this.bullet(`${event.tool}: ${event.approved ? "approved" : "denied"}`, event.approved ? "green" : "normal");
|
|
154
|
+
break;
|
|
155
|
+
case "context_compacted":
|
|
156
|
+
this.bullet(`context compacted ${event.beforeChars} → ${event.afterChars} chars`, "green");
|
|
157
|
+
break;
|
|
158
|
+
case "context_compaction_failed":
|
|
159
|
+
this.bullet(`context compaction failed · ${compactValue(event.reason, 200)}`, "red");
|
|
160
|
+
break;
|
|
161
|
+
case "context_compaction_start":
|
|
162
|
+
break;
|
|
163
|
+
case "task_start": {
|
|
164
|
+
this.completeStatus(undefined, "green", Boolean(this.options.staticStatus));
|
|
165
|
+
this.ensureTurnSpacing();
|
|
166
|
+
this.activeTasks.set(event.id, {
|
|
167
|
+
subagentType: event.subagentType,
|
|
168
|
+
description: event.description,
|
|
169
|
+
});
|
|
170
|
+
this.bullet(`Task(${event.subagentType}): ${event.description}`, "blue");
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case "task_progress":
|
|
174
|
+
break;
|
|
175
|
+
case "task_finish": {
|
|
176
|
+
this.completeStatus(undefined, "green", Boolean(this.options.staticStatus));
|
|
177
|
+
this.activeTasks.delete(event.id);
|
|
178
|
+
const secs = (event.durationMs / 1000).toFixed(1);
|
|
179
|
+
this.subBullet(`finished · ${secs}s`, "gray");
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
startStatus(text, detail = "") {
|
|
185
|
+
this.statusText = text;
|
|
186
|
+
this.statusDetail = detail;
|
|
187
|
+
this.statusStartedAt = Date.now();
|
|
188
|
+
if (this.options.staticStatus) {
|
|
189
|
+
this.staticStatusActive = true;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!this.options.status || !this.options.stderr?.isTTY || this.options.suspendLiveStatus?.())
|
|
193
|
+
return;
|
|
194
|
+
this.spinnerIndex = 0;
|
|
195
|
+
this.spinner ??= setInterval(() => this.renderStatus(), 160);
|
|
196
|
+
this.renderStatus();
|
|
197
|
+
}
|
|
198
|
+
stopStatus() {
|
|
199
|
+
if (!this.spinner)
|
|
200
|
+
return;
|
|
201
|
+
clearInterval(this.spinner);
|
|
202
|
+
this.spinner = undefined;
|
|
203
|
+
if (this.options.stderr?.isTTY && this.statusLines > 0) {
|
|
204
|
+
this.clearStatusLines();
|
|
205
|
+
}
|
|
206
|
+
this.statusLines = 0;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* @param silent When true, clear internal status without printing a bullet
|
|
210
|
+
* (used in interactive mode where index.ts owns the outcome line).
|
|
211
|
+
*/
|
|
212
|
+
completeStatus(text = undefined, colorName = "green", silent = false) {
|
|
213
|
+
const label = text ?? this.statusText;
|
|
214
|
+
const hadStatus = this.spinner !== undefined || this.statusLines > 0 || Boolean(this.statusText);
|
|
215
|
+
const detail = this.statusDetail;
|
|
216
|
+
const startedAt = this.statusStartedAt;
|
|
217
|
+
this.stopStatus();
|
|
218
|
+
this.statusText = "";
|
|
219
|
+
this.statusDetail = "";
|
|
220
|
+
this.staticStatusActive = false;
|
|
221
|
+
this.statusStartedAt = 0;
|
|
222
|
+
if (silent || !hadStatus || !label)
|
|
223
|
+
return;
|
|
224
|
+
const elapsed = startedAt ? ` ${this.elapsedSince(startedAt)}` : "";
|
|
225
|
+
this.bullet(`${formatCompletedStatusText(label)}${elapsed}`, colorName);
|
|
226
|
+
if (detail)
|
|
227
|
+
this.subBullet(detail, "gray");
|
|
228
|
+
}
|
|
229
|
+
/** Non-interactive: Claude Code–style morphing glyph + shimmer label. */
|
|
230
|
+
renderStatus() {
|
|
231
|
+
const frame = this.spinnerIndex;
|
|
232
|
+
this.spinnerIndex += 1;
|
|
233
|
+
const label = formatStatusText(this.statusText);
|
|
234
|
+
const elapsed = this.statusStartedAt ? this.elapsedSince(this.statusStartedAt) : this.elapsedThought();
|
|
235
|
+
const line = formatWaitLine({
|
|
236
|
+
frame,
|
|
237
|
+
label,
|
|
238
|
+
elapsed,
|
|
239
|
+
useColor: this.options.color,
|
|
240
|
+
});
|
|
241
|
+
this.out(`\r\x1b[2K${line}`);
|
|
242
|
+
this.statusLines = 1;
|
|
243
|
+
}
|
|
244
|
+
bullet(text, colorName) {
|
|
245
|
+
this.stopStatus();
|
|
246
|
+
this.flushAssistantLine(false);
|
|
247
|
+
if (this.wroteAssistantText) {
|
|
248
|
+
this.out("\n");
|
|
249
|
+
this.wroteAssistantText = false;
|
|
250
|
+
}
|
|
251
|
+
this.out(`${this.paint("•", colorName)} ${this.paint(text, colorName)}\n`);
|
|
252
|
+
}
|
|
253
|
+
subBullet(text, colorName) {
|
|
254
|
+
this.stopStatus();
|
|
255
|
+
this.flushAssistantLine(false);
|
|
256
|
+
if (this.wroteAssistantText) {
|
|
257
|
+
this.out("\n");
|
|
258
|
+
this.wroteAssistantText = false;
|
|
259
|
+
}
|
|
260
|
+
this.out(` ${this.paint("└", colorName)} ${this.paint(text, colorName)}\n`);
|
|
261
|
+
}
|
|
262
|
+
renderFileChange(change) {
|
|
263
|
+
this.stopStatus();
|
|
264
|
+
this.flushAssistantLine(false);
|
|
265
|
+
if (this.wroteAssistantText) {
|
|
266
|
+
this.out("\n");
|
|
267
|
+
this.wroteAssistantText = false;
|
|
268
|
+
}
|
|
269
|
+
const path = stripTerminalEscapes(change.path);
|
|
270
|
+
const action = change.kind === "create" ? "Write" : "Update";
|
|
271
|
+
const added = `${change.added} ${change.added === 1 ? "line" : "lines"}`;
|
|
272
|
+
const removed = `${change.removed} ${change.removed === 1 ? "line" : "lines"}`;
|
|
273
|
+
const summary = change.kind === "create" ? `Added ${added}` : `Added ${added}, removed ${removed}`;
|
|
274
|
+
this.out(`${this.paint("●", "green")} ${this.paint(`${action}(${path})`, "bold")}\n`);
|
|
275
|
+
this.out(` ${this.paint("└", "gray")} ${this.paint(summary, "gray")}\n`);
|
|
276
|
+
this.renderCodeChange(change);
|
|
277
|
+
if (change.omitted > 0) {
|
|
278
|
+
this.out(` ${this.paint("└", "gray")} ${this.paint(`… +${change.omitted} lines`, "gray")}\n`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
renderInteractiveToolResult(event, denied) {
|
|
282
|
+
this.bullet(formatInteractiveToolResult(event.tool, event.content, event.isError, event.input), denied ? "yellow" : event.isError ? "red" : "green");
|
|
283
|
+
if (event.tool !== "shell_exec" || event.isError || event.content === "(no output)")
|
|
284
|
+
return;
|
|
285
|
+
const lines = stripTerminalEscapes(event.content).split(/\r?\n/);
|
|
286
|
+
for (const [index, line] of lines.slice(0, 6).entries()) {
|
|
287
|
+
this.out(` ${this.paint(index === 0 ? "└" : " ", "gray")} ${this.paint(truncateDisplayLine(line, 140), "gray")}\n`);
|
|
288
|
+
}
|
|
289
|
+
if (lines.length > 6)
|
|
290
|
+
this.out(` ${this.paint(`… +${lines.length - 6} lines`, "gray")}\n`);
|
|
291
|
+
}
|
|
292
|
+
renderCodeChange(change) {
|
|
293
|
+
const width = Math.max(40, Math.min(this.options.stdout?.columns ?? 120, 160));
|
|
294
|
+
let oldLine = 1;
|
|
295
|
+
let newLine = 1;
|
|
296
|
+
for (const rawLine of change.lines) {
|
|
297
|
+
const line = stripTerminalEscapes(rawLine);
|
|
298
|
+
const hunk = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
299
|
+
if (hunk) {
|
|
300
|
+
oldLine = Number(hunk[1]);
|
|
301
|
+
newLine = Number(hunk[2]);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (line.startsWith("\\ No newline"))
|
|
305
|
+
continue;
|
|
306
|
+
let number = newLine;
|
|
307
|
+
let marker = " ";
|
|
308
|
+
let style = "context";
|
|
309
|
+
let content = line;
|
|
310
|
+
if (change.kind === "create") {
|
|
311
|
+
newLine += 1;
|
|
312
|
+
}
|
|
313
|
+
else if (line.startsWith("+")) {
|
|
314
|
+
marker = "+";
|
|
315
|
+
style = "add";
|
|
316
|
+
content = line.slice(1);
|
|
317
|
+
newLine += 1;
|
|
318
|
+
}
|
|
319
|
+
else if (line.startsWith("-")) {
|
|
320
|
+
number = oldLine;
|
|
321
|
+
marker = "-";
|
|
322
|
+
style = "remove";
|
|
323
|
+
content = line.slice(1);
|
|
324
|
+
oldLine += 1;
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
content = line.startsWith(" ") ? line.slice(1) : line;
|
|
328
|
+
oldLine += 1;
|
|
329
|
+
newLine += 1;
|
|
330
|
+
}
|
|
331
|
+
this.out(`${renderCodeLine(number, marker, content, style, width, this.options.color)}\n`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
paint(text, colorName) {
|
|
335
|
+
if (!this.options.color)
|
|
336
|
+
return text;
|
|
337
|
+
if (colorName === "normal")
|
|
338
|
+
return text;
|
|
339
|
+
return `${color[colorName]}${text}${color.reset}`;
|
|
340
|
+
}
|
|
341
|
+
elapsedThought() {
|
|
342
|
+
if (!this.thoughtStartedAt)
|
|
343
|
+
return "0.0s";
|
|
344
|
+
return `${((Date.now() - this.thoughtStartedAt) / 1000).toFixed(1)}s`;
|
|
345
|
+
}
|
|
346
|
+
elapsedSince(startedAt) {
|
|
347
|
+
return `${((Date.now() - startedAt) / 1000).toFixed(1)}s`;
|
|
348
|
+
}
|
|
349
|
+
writeAssistantDelta(text) {
|
|
350
|
+
// Buffer only the current line, then render it once. This preserves
|
|
351
|
+
// streaming without rewriting terminal content or exposing raw Markdown.
|
|
352
|
+
this.assistantLineBuffer += text;
|
|
353
|
+
let newlineIndex = this.assistantLineBuffer.indexOf("\n");
|
|
354
|
+
while (newlineIndex >= 0) {
|
|
355
|
+
const line = this.assistantLineBuffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
356
|
+
this.out(`${this.renderMarkdownLine(line)}\n`);
|
|
357
|
+
this.assistantLineBuffer = this.assistantLineBuffer.slice(newlineIndex + 1);
|
|
358
|
+
newlineIndex = this.assistantLineBuffer.indexOf("\n");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
flushAssistantLine(terminate) {
|
|
362
|
+
if (!this.assistantLineBuffer)
|
|
363
|
+
return;
|
|
364
|
+
this.out(this.renderMarkdownLine(this.assistantLineBuffer));
|
|
365
|
+
this.assistantLineBuffer = "";
|
|
366
|
+
if (terminate)
|
|
367
|
+
this.out("\n");
|
|
368
|
+
}
|
|
369
|
+
renderMarkdownLine(line) {
|
|
370
|
+
// Strip any terminal escapes from model/tool-relayed content before we add our own.
|
|
371
|
+
// This is a web agent: untrusted page text can carry OSC/CSI sequences (title/clipboard/
|
|
372
|
+
// hyperlink/screen-clear injection). Every branch below starts from sanitized input.
|
|
373
|
+
line = stripTerminalEscapes(line);
|
|
374
|
+
const fence = line.match(/^\s*```\s*([^\s`]*)/);
|
|
375
|
+
if (fence) {
|
|
376
|
+
const opening = !this.insideCodeFence;
|
|
377
|
+
this.insideCodeFence = !this.insideCodeFence;
|
|
378
|
+
const label = opening ? ` ${fence[1] || "code"} ` : "";
|
|
379
|
+
return this.paint(label ? `─────${label}─────` : "───────────────", "gray");
|
|
380
|
+
}
|
|
381
|
+
if (this.insideCodeFence)
|
|
382
|
+
return ` ${this.paint(line, "cyan")}`;
|
|
383
|
+
const heading = line.match(/^\s{0,3}(#{1,6})\s+(.+)$/);
|
|
384
|
+
if (heading)
|
|
385
|
+
return this.paint(renderInlineMarkdown(heading[2] ?? "", this.options.color), "blue");
|
|
386
|
+
const bullet = line.match(/^\s{0,3}[-*+]\s+(.+)$/);
|
|
387
|
+
if (bullet) {
|
|
388
|
+
return ` ${this.paint("•", "blue")} ${renderInlineMarkdown(bullet[1] ?? "", this.options.color)}`;
|
|
389
|
+
}
|
|
390
|
+
const numbered = line.match(/^\s{0,3}(\d+)[.)]\s+(.+)$/);
|
|
391
|
+
if (numbered) {
|
|
392
|
+
return ` ${this.paint(`${numbered[1]}.`, "blue")} ${renderInlineMarkdown(numbered[2] ?? "", this.options.color)}`;
|
|
393
|
+
}
|
|
394
|
+
const quote = line.match(/^\s{0,3}>\s?(.*)$/);
|
|
395
|
+
if (quote) {
|
|
396
|
+
return ` ${this.paint("│", "gray")} ${this.paint(renderInlineMarkdown(quote[1] ?? "", this.options.color), "gray")}`;
|
|
397
|
+
}
|
|
398
|
+
if (/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(line))
|
|
399
|
+
return this.paint("─".repeat(48), "gray");
|
|
400
|
+
return renderInlineMarkdown(line, this.options.color);
|
|
401
|
+
}
|
|
402
|
+
ensureTurnSpacing() {
|
|
403
|
+
if (this.turnSpacingPrinted)
|
|
404
|
+
return;
|
|
405
|
+
this.out("\n");
|
|
406
|
+
this.turnSpacingPrinted = true;
|
|
407
|
+
}
|
|
408
|
+
clearStatusLines() {
|
|
409
|
+
if (this.statusLines === 2) {
|
|
410
|
+
this.out("\r\x1b[2K\x1b[1F\r\x1b[2K");
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
this.out("\r\x1b[2K");
|
|
414
|
+
}
|
|
415
|
+
out(text) {
|
|
416
|
+
this.options.beforeOutput?.();
|
|
417
|
+
(this.options.stdout ?? process.stdout).write(text);
|
|
418
|
+
this.options.afterOutput?.(text);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
export function renderTraceEvent(event) {
|
|
422
|
+
new CliTraceRenderer({
|
|
423
|
+
verbose: true,
|
|
424
|
+
stream: false,
|
|
425
|
+
color: stderr.isTTY,
|
|
426
|
+
status: false,
|
|
427
|
+
}).render(event);
|
|
428
|
+
}
|
|
429
|
+
export function createCliTraceSink(verbose) {
|
|
430
|
+
return new CliTraceRenderer({
|
|
431
|
+
verbose,
|
|
432
|
+
stream: false,
|
|
433
|
+
color: stderr.isTTY,
|
|
434
|
+
status: false,
|
|
435
|
+
}).sink();
|
|
436
|
+
}
|
|
437
|
+
function riskLabel(risk) {
|
|
438
|
+
if (risk === "high")
|
|
439
|
+
return "high risk";
|
|
440
|
+
if (risk === "medium")
|
|
441
|
+
return "medium risk";
|
|
442
|
+
return "low risk";
|
|
443
|
+
}
|
|
444
|
+
function compactValue(value, max) {
|
|
445
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
446
|
+
if (!text)
|
|
447
|
+
return "";
|
|
448
|
+
const normalized = text.replace(/\s+/g, " ");
|
|
449
|
+
return normalized.length > max ? `${normalized.slice(0, max)}...` : normalized;
|
|
450
|
+
}
|
|
451
|
+
function truncateDisplayLine(value, max) {
|
|
452
|
+
const characters = Array.from(value.replace(/\t/g, " "));
|
|
453
|
+
return characters.length <= max ? characters.join("") : `${characters.slice(0, max - 1).join("")}…`;
|
|
454
|
+
}
|
|
455
|
+
function renderCodeLine(lineNumber, marker, value, style, width, useColor) {
|
|
456
|
+
const prefix = ` ${String(lineNumber).padStart(5)} ${marker} `;
|
|
457
|
+
const content = truncateDisplayLine(value, Math.max(10, width - displayCells(prefix)));
|
|
458
|
+
const padding = " ".repeat(Math.max(0, width - displayCells(prefix) - displayCells(content)));
|
|
459
|
+
if (!useColor)
|
|
460
|
+
return `${prefix}${content}${padding}`;
|
|
461
|
+
const background = style === "add"
|
|
462
|
+
? "\x1b[48;2;0;55;18m"
|
|
463
|
+
: style === "remove"
|
|
464
|
+
? "\x1b[48;2;75;8;14m"
|
|
465
|
+
: "";
|
|
466
|
+
const gutter = style === "add"
|
|
467
|
+
? "\x1b[38;2;74;222;128m"
|
|
468
|
+
: style === "remove"
|
|
469
|
+
? "\x1b[38;2;248;113;113m"
|
|
470
|
+
: "\x1b[38;2;115;115;125m";
|
|
471
|
+
return `${background}${gutter}${prefix}${highlightCode(content)}${padding}\x1b[0m`;
|
|
472
|
+
}
|
|
473
|
+
function highlightCode(value) {
|
|
474
|
+
const normal = "\x1b[38;2;235;235;240m";
|
|
475
|
+
return `${normal}${value.replace(/(\/\/.*$|\/\*.*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b(?:const|let|var|new|if|else|for|while|return|async|await|function|class|interface|type|import|export|from|extends|throw|try|catch|finally)\b|\b(?:true|false|null|undefined)\b|\b\d+(?:\.\d+)?\b)/g, (token) => {
|
|
476
|
+
if (token.startsWith("//") || token.startsWith("/*"))
|
|
477
|
+
return `\x1b[38;2;130;130;120m${token}${normal}`;
|
|
478
|
+
if (/^["'`]/.test(token))
|
|
479
|
+
return `\x1b[38;2;250;204;92m${token}${normal}`;
|
|
480
|
+
if (/^(?:true|false|null|undefined|\d)/.test(token))
|
|
481
|
+
return `\x1b[38;2;189;147;249m${token}${normal}`;
|
|
482
|
+
return `\x1b[38;2;255;80;155m${token}${normal}`;
|
|
483
|
+
})}`;
|
|
484
|
+
}
|
|
485
|
+
function displayCells(value) {
|
|
486
|
+
let width = 0;
|
|
487
|
+
for (const char of value) {
|
|
488
|
+
width += /[\u1100-\u115F\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6]/u.test(char) ? 2 : 1;
|
|
489
|
+
}
|
|
490
|
+
return width;
|
|
491
|
+
}
|
|
492
|
+
function retryLabel(mode, delayMs) {
|
|
493
|
+
if (mode === "after-compaction")
|
|
494
|
+
return "Context compacted; retrying model response";
|
|
495
|
+
if (mode === "continue")
|
|
496
|
+
return "Output limit reached; continuing response";
|
|
497
|
+
const delay = delayMs && delayMs > 0 ? ` in ${(delayMs / 1000).toFixed(1)}s` : "";
|
|
498
|
+
return mode === "non-stream"
|
|
499
|
+
? `Retrying model response without streaming${delay}`
|
|
500
|
+
: `Retrying model response${delay}`;
|
|
501
|
+
}
|
|
502
|
+
export function stripTerminalEscapes(value) {
|
|
503
|
+
return value
|
|
504
|
+
// CSI: ESC [ ... final-byte
|
|
505
|
+
.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "")
|
|
506
|
+
// OSC: ESC ] ... (BEL or ST terminator)
|
|
507
|
+
.replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)/g, "")
|
|
508
|
+
// DCS/SOS/PM/APC: ESC (P|X|^|_) ... ST
|
|
509
|
+
.replace(/\x1B[PX^_][^\x1B]*(?:\x1B\\)?/g, "")
|
|
510
|
+
// Any remaining bare ESC (incl. single-char sequences)
|
|
511
|
+
.replace(/\x1B[@-Z\\-~]?/g, "")
|
|
512
|
+
// Other C0 controls except tab/newline/carriage-return
|
|
513
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
514
|
+
}
|
|
515
|
+
function renderInlineMarkdown(value, useColor) {
|
|
516
|
+
let rendered = stripTerminalEscapes(value);
|
|
517
|
+
rendered = rendered.replace(/\[([^\]]+)]\((https?:\/\/[^)]+)\)/g, (_match, label, url) => useColor ? `\x1b[4;38;2;96;165;250m${label}\x1b[0m \x1b[90m(${url})\x1b[0m` : `${label} (${url})`);
|
|
518
|
+
rendered = rendered.replace(/`([^`]+)`/g, (_match, codeText) => useColor ? `\x1b[48;5;236m\x1b[38;2;125;211;252m ${codeText} \x1b[0m` : codeText);
|
|
519
|
+
rendered = rendered.replace(/\*\*([^*]+)\*\*/g, (_match, boldText) => useColor ? `\x1b[1m${boldText}\x1b[0m` : boldText);
|
|
520
|
+
rendered = rendered.replace(/__([^_]+)__/g, (_match, boldText) => useColor ? `\x1b[1m${boldText}\x1b[0m` : boldText);
|
|
521
|
+
rendered = rendered.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, (_match, prefix, italicText) => useColor ? `${prefix}\x1b[3m${italicText}\x1b[0m` : `${prefix}${italicText}`);
|
|
522
|
+
return rendered;
|
|
523
|
+
}
|
|
524
|
+
function compactToolInput(tool, value, max) {
|
|
525
|
+
if (tool === "Task" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
526
|
+
const input = value;
|
|
527
|
+
const subagentType = (typeof input.subagent_type === "string" && input.subagent_type) ||
|
|
528
|
+
(typeof input.worker === "string" && input.worker) ||
|
|
529
|
+
"agent";
|
|
530
|
+
const description = typeof input.description === "string" ? input.description : "subtask";
|
|
531
|
+
return compactValue({ subagent_type: subagentType, description }, max);
|
|
532
|
+
}
|
|
533
|
+
return compactValue(value, max);
|
|
534
|
+
}
|
|
535
|
+
function formatBytes(bytes) {
|
|
536
|
+
if (bytes < 1024)
|
|
537
|
+
return `${bytes} B`;
|
|
538
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
539
|
+
}
|
|
540
|
+
function formatStatusText(text) {
|
|
541
|
+
if (text === "thinking")
|
|
542
|
+
return "Thinking...";
|
|
543
|
+
if (text.startsWith("running "))
|
|
544
|
+
return `Running ${text.slice("running ".length)}...`;
|
|
545
|
+
if (text.startsWith("using "))
|
|
546
|
+
return `Using ${text.slice("using ".length)}...`;
|
|
547
|
+
return `${text.slice(0, 1).toUpperCase()}${text.slice(1)}...`;
|
|
548
|
+
}
|
|
549
|
+
function formatCompletedStatusText(text) {
|
|
550
|
+
if (text === "thinking")
|
|
551
|
+
return "Thought";
|
|
552
|
+
if (text.startsWith("using "))
|
|
553
|
+
return `Used ${text.slice("using ".length)}`;
|
|
554
|
+
if (text.startsWith("running "))
|
|
555
|
+
return `Ran ${text.slice("running ".length)}`;
|
|
556
|
+
return text;
|
|
557
|
+
}
|
|
558
|
+
function thoughtDetail(stopReason) {
|
|
559
|
+
if (stopReason === "tool_use")
|
|
560
|
+
return "model requested tool use";
|
|
561
|
+
if (stopReason === "max_tokens")
|
|
562
|
+
return "model reached max token limit";
|
|
563
|
+
if (stopReason === "end_turn")
|
|
564
|
+
return "answered directly, no tools used";
|
|
565
|
+
return stopReason ? `stopped by ${stopReason}` : "model response received";
|
|
566
|
+
}
|
|
567
|
+
function formatInteractiveToolResult(tool, content, isError, input) {
|
|
568
|
+
const name = tool === "write_file" ? "Write" : tool === "shell_exec" ? "Bash" : toolLabel(tool);
|
|
569
|
+
const key = tool === "list_files" ? "directory" : tool === "web_search" ? "query" : tool === "web_fetch" ? "url" : tool === "shell_exec" ? "command" : "path";
|
|
570
|
+
const rawDetail = input?.[key];
|
|
571
|
+
const detail = typeof rawDetail === "string" && rawDetail.trim()
|
|
572
|
+
? `(${compactValue(stripTerminalEscapes(rawDetail), 100)})`
|
|
573
|
+
: "";
|
|
574
|
+
const label = `${name}${detail}`;
|
|
575
|
+
if (isPermissionDenied(content))
|
|
576
|
+
return `${label} skipped · approval not granted`;
|
|
577
|
+
if (isError)
|
|
578
|
+
return `${label} failed · ${compactValue(content, 160)}`;
|
|
579
|
+
if (tool === "write_file" && content.startsWith("Wrote "))
|
|
580
|
+
return content;
|
|
581
|
+
if (tool === "list_files")
|
|
582
|
+
return `${label} · ${content ? content.split(/\r?\n/).length : 0} entries`;
|
|
583
|
+
if (tool === "web_search") {
|
|
584
|
+
const count = content.match(/"count"\s*:\s*(\d+)/)?.[1];
|
|
585
|
+
return `${label}${count ? ` · ${count} results` : ""}`;
|
|
586
|
+
}
|
|
587
|
+
if (tool === "shell_exec")
|
|
588
|
+
return label;
|
|
589
|
+
return `${label} completed`;
|
|
590
|
+
}
|
|
591
|
+
function toolLabel(tool) {
|
|
592
|
+
return tool
|
|
593
|
+
.split("_")
|
|
594
|
+
.filter(Boolean)
|
|
595
|
+
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
596
|
+
.join(" ");
|
|
597
|
+
}
|
|
598
|
+
function isPermissionDenied(content) {
|
|
599
|
+
return content.startsWith("Permission denied for tool ");
|
|
600
|
+
}
|