llm-agent-cli 2.0.0 → 2.0.2
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/README.md +91 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +109 -40
- package/dist/agent.js.map +1 -1
- package/dist/index.js +466 -110
- package/dist/index.js.map +1 -1
- package/dist/input.d.ts +5 -0
- package/dist/input.d.ts.map +1 -1
- package/dist/input.js +20 -1
- package/dist/input.js.map +1 -1
- package/dist/project.js +1 -1
- package/dist/renderer.d.ts +46 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +145 -0
- package/dist/renderer.js.map +1 -1
- package/dist/tools/index.d.ts +1 -6
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +9 -1
- package/dist/tools/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { ContextManager } from "./context.js";
|
|
|
13
13
|
import { ReplInput } from "./input.js";
|
|
14
14
|
import { preprocessPrompt } from "./preprocessor.js";
|
|
15
15
|
import { detectProjectContext } from "./project.js";
|
|
16
|
-
import { renderMarkdown } from "./renderer.js";
|
|
16
|
+
import { renderMarkdown, renderPanel as renderPanelComponent, renderTable as renderTableComponent, contextBar as contextBarComponent, renderErrorCard, renderModelBadge, renderToolProgress, formatCost, getTheme, setTheme, getThemeName, stripAnsi as stripAnsiComponent, fitCell as fitCellComponent, } from "./renderer.js";
|
|
17
17
|
import { SessionStore } from "./session.js";
|
|
18
18
|
import { listAvailableTools } from "./tools/index.js";
|
|
19
19
|
import { resolveProtectedPath, getRuntimeConfig } from "./tools/security.js";
|
|
@@ -21,6 +21,26 @@ import { run_linter, run_tests, security_scan } from "./tools/linter.js";
|
|
|
21
21
|
import { loadCredentials, resolveConnectionFromEnvOrCreds, runLogin, runLogout, runWhoami, } from "./auth.js";
|
|
22
22
|
import { loadMemory, appendMemory, clearMemory, showMemory, } from "./memory.js";
|
|
23
23
|
const PERMISSION_MODES = ["safe", "standard", "yolo"];
|
|
24
|
+
/** UI accessor — always reads from the active theme so theme switches propagate. */
|
|
25
|
+
function UI() {
|
|
26
|
+
return getTheme();
|
|
27
|
+
}
|
|
28
|
+
const ICON = {
|
|
29
|
+
spark: "✦",
|
|
30
|
+
gear: "⚙",
|
|
31
|
+
shield: "🛡",
|
|
32
|
+
folder: "📁",
|
|
33
|
+
network: "🌐",
|
|
34
|
+
model: "🧠",
|
|
35
|
+
stack: "🧩",
|
|
36
|
+
session: "🗂",
|
|
37
|
+
tip: "💡",
|
|
38
|
+
status: "●",
|
|
39
|
+
command: "⌘",
|
|
40
|
+
token: "◉",
|
|
41
|
+
retry: "↻",
|
|
42
|
+
ttft: "⚡",
|
|
43
|
+
};
|
|
24
44
|
function isPermissionMode(value) {
|
|
25
45
|
return PERMISSION_MODES.includes(value);
|
|
26
46
|
}
|
|
@@ -28,58 +48,131 @@ function isOutputFormat(value) {
|
|
|
28
48
|
return value === "text" || value === "json";
|
|
29
49
|
}
|
|
30
50
|
function formatUsd(value) {
|
|
31
|
-
return
|
|
51
|
+
return formatCost(value);
|
|
52
|
+
}
|
|
53
|
+
function stripAnsi(value) {
|
|
54
|
+
return stripAnsiComponent(value);
|
|
55
|
+
}
|
|
56
|
+
function badge(label, tone = "info") {
|
|
57
|
+
const ui = UI();
|
|
58
|
+
const color = tone === "success"
|
|
59
|
+
? ui.success
|
|
60
|
+
: tone === "warning"
|
|
61
|
+
? ui.warning
|
|
62
|
+
: tone === "danger"
|
|
63
|
+
? ui.danger
|
|
64
|
+
: tone === "muted"
|
|
65
|
+
? ui.muted
|
|
66
|
+
: ui.info;
|
|
67
|
+
return color(`◈ ${label}`);
|
|
68
|
+
}
|
|
69
|
+
function fitCell(value, width) {
|
|
70
|
+
return fitCellComponent(value, width);
|
|
71
|
+
}
|
|
72
|
+
function renderPanel(lines) {
|
|
73
|
+
return renderPanelComponent(lines);
|
|
32
74
|
}
|
|
33
|
-
function
|
|
34
|
-
return
|
|
75
|
+
function printTable(headers, rows, widths) {
|
|
76
|
+
return renderTableComponent(headers, rows, widths);
|
|
77
|
+
}
|
|
78
|
+
function contextBar(ratio, width = 18) {
|
|
79
|
+
return contextBarComponent(ratio, width);
|
|
35
80
|
}
|
|
36
81
|
function printBanner(runtime, permissionMode) {
|
|
82
|
+
const ui = UI();
|
|
37
83
|
const model = process.env.LLM_ROUTER_MODEL ?? "auto";
|
|
38
84
|
const baseUrl = process.env.LLM_ROUTER_BASE_URL ?? "(not set)";
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
85
|
+
const permissionTone = permissionMode === "safe"
|
|
86
|
+
? "success"
|
|
87
|
+
: permissionMode === "yolo"
|
|
88
|
+
? "danger"
|
|
89
|
+
: "warning";
|
|
90
|
+
const headerLine = `${ui.title(`${ICON.spark} LLM-Router Agent`)} ${badge(permissionMode, permissionTone)} ${ui.muted("v2.0")}`;
|
|
91
|
+
const routerLine = ` ${ui.muted("router")} ${ui.info(baseUrl)}`;
|
|
92
|
+
const modelLine = ` ${ui.muted("model")} ${ui.success(model)}`;
|
|
93
|
+
const wsLine = ` ${ui.muted("workspace")} ${runtime.project.workspaceRoot}`;
|
|
94
|
+
const stackLine = ` ${ui.muted("stack")} ${runtime.project.language} ${ui.muted("·")} ${runtime.project.framework} ${ui.muted("·")} ${runtime.project.packageManager}`;
|
|
95
|
+
const sessionLine = ` ${ui.muted("session")} ${ui.muted(runtime.sessionId)}`;
|
|
96
|
+
const themeLine = ` ${ui.muted("theme")} ${ui.muted(getThemeName())}`;
|
|
97
|
+
const panel = renderPanel([
|
|
98
|
+
` ${headerLine}`,
|
|
99
|
+
``,
|
|
100
|
+
` ${routerLine}`,
|
|
101
|
+
` ${modelLine}`,
|
|
102
|
+
` ${wsLine}`,
|
|
103
|
+
` ${stackLine}`,
|
|
104
|
+
` ${sessionLine}`,
|
|
105
|
+
` ${themeLine}`,
|
|
106
|
+
]);
|
|
107
|
+
console.log("");
|
|
108
|
+
console.log(panel);
|
|
109
|
+
console.log("");
|
|
110
|
+
console.log(ui.muted(` /help /tools /models /cost /compact /theme`));
|
|
111
|
+
console.log("");
|
|
112
|
+
}
|
|
113
|
+
function printCommandGroup(title, commands) {
|
|
114
|
+
console.log(UI().accent.bold(` ${title}`));
|
|
115
|
+
console.log(printTable(["Command", "Description"], commands.map(([cmd, desc]) => [UI().info(cmd), desc]), [28, 70]));
|
|
116
|
+
console.log("");
|
|
46
117
|
}
|
|
47
118
|
function printHelp() {
|
|
48
|
-
console.log(
|
|
49
|
-
console.log(
|
|
50
|
-
console.log(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
119
|
+
console.log("");
|
|
120
|
+
console.log(renderPanel([` ${UI().title(`${ICON.command} Command Palette`)}`]));
|
|
121
|
+
console.log("");
|
|
122
|
+
printCommandGroup("🧭 Core", [
|
|
123
|
+
["/help", "Show this help"],
|
|
124
|
+
["/exit", "Quit the CLI"],
|
|
125
|
+
["/clear", "Clear terminal output only"],
|
|
126
|
+
["/retry", "Resend last prompt"],
|
|
127
|
+
["/undo", "Remove last user+assistant turn"],
|
|
128
|
+
]);
|
|
129
|
+
printCommandGroup("🗂 Session", [
|
|
130
|
+
["/compact", "Summarize and compress conversation"],
|
|
131
|
+
["/save [name]", "Save named session snapshot"],
|
|
132
|
+
["/load [name]", "Load named session or list saved names"],
|
|
133
|
+
["/models", "List available router models"],
|
|
134
|
+
["/cost", "Show token and cost totals"],
|
|
135
|
+
]);
|
|
136
|
+
printCommandGroup("📁 Workspace", [
|
|
137
|
+
["/cd <path>", "Change working directory (within workspace root)"],
|
|
138
|
+
["/tools", "List tools and descriptions"],
|
|
139
|
+
["/permissions", "Show current permission mode"],
|
|
140
|
+
["/verbose", "Toggle verbose tool result output"],
|
|
141
|
+
["/theme <name>", "Switch UI theme: dark, light, minimal"],
|
|
142
|
+
]);
|
|
143
|
+
printCommandGroup("🧪 Quality", [
|
|
144
|
+
["/review [path]", "Run a structured code review prompt"],
|
|
145
|
+
["/lint [path]", "Run linter (auto detects toolchain)"],
|
|
146
|
+
["/test [path]", "Run tests (auto detects runner)"],
|
|
147
|
+
["/scan [path]", "Run security scan"],
|
|
148
|
+
]);
|
|
149
|
+
printCommandGroup("🔑 Auth", [
|
|
150
|
+
["/login", "Log in (opens browser for Google/GitHub auth)"],
|
|
151
|
+
["/logout", "Log out and clear credentials"],
|
|
152
|
+
["/whoami", "Show current account and router URL"],
|
|
153
|
+
]);
|
|
154
|
+
printCommandGroup("🧠 Memory", [
|
|
155
|
+
["/memory", "Show global + project memory"],
|
|
156
|
+
["/memory clear", "Clear project memory"],
|
|
157
|
+
["/memory clear --global", "Clear global memory"],
|
|
158
|
+
["/remember <text>", "Remember fact in project memory"],
|
|
159
|
+
["/remember --global <text>", "Remember fact in global memory"],
|
|
160
|
+
]);
|
|
72
161
|
console.log();
|
|
73
|
-
console.log(
|
|
74
|
-
console.log(
|
|
75
|
-
console.log(
|
|
162
|
+
console.log(UI().accent.bold("⌨ Input Modes"));
|
|
163
|
+
console.log(UI().muted(" • End a line with \\ for multiline continuation"));
|
|
164
|
+
console.log(UI().muted(" • Type <<< then paste blocks; finish with a single . line"));
|
|
76
165
|
console.log();
|
|
77
166
|
}
|
|
78
167
|
function printTools() {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
168
|
+
const tools = listAvailableTools();
|
|
169
|
+
const rows = tools.map((tool) => [
|
|
170
|
+
`${ICON.gear} ${UI().info(tool.name)}`,
|
|
171
|
+
tool.description,
|
|
172
|
+
]);
|
|
173
|
+
console.log("");
|
|
174
|
+
console.log(UI().accent.bold(`${ICON.gear} Tools (${tools.length})`));
|
|
175
|
+
console.log(printTable(["Name", "Description"], rows, [30, 68]));
|
|
83
176
|
console.log();
|
|
84
177
|
}
|
|
85
178
|
function modelFlag(model, key) {
|
|
@@ -120,22 +213,26 @@ function formatModels(models) {
|
|
|
120
213
|
const rows = models.map((model) => ({
|
|
121
214
|
id: String(model.id ?? ""),
|
|
122
215
|
tier: String(model.tier ?? ""),
|
|
123
|
-
|
|
216
|
+
provider: String(model.provider ?? model.owned_by ?? ""),
|
|
217
|
+
context: Number(model.contextWindow ?? model.context_window ?? 0),
|
|
124
218
|
caps: extractModelCapabilities(model),
|
|
219
|
+
isBlocked: Boolean(model.isBlocked ?? model.is_blocked ?? false),
|
|
125
220
|
}));
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
221
|
+
const tierText = (tier) => {
|
|
222
|
+
if (tier === "strong" || tier === "reasoning")
|
|
223
|
+
return UI().success(tier);
|
|
224
|
+
if (tier === "cheap")
|
|
225
|
+
return UI().warning(tier);
|
|
226
|
+
return UI().info(tier || "unknown");
|
|
227
|
+
};
|
|
228
|
+
return printTable(["Model", "Tier", "Provider", "Context", "Capabilities", "Status"], rows.map((row) => [
|
|
229
|
+
`${ICON.model} ${row.id}`,
|
|
230
|
+
tierText(row.tier),
|
|
231
|
+
row.provider || "-",
|
|
232
|
+
row.context > 0 ? `${Math.round(row.context / 1000)}k` : "-",
|
|
233
|
+
row.caps || "-",
|
|
234
|
+
row.isBlocked ? UI().warning("blocked") : UI().success("active"),
|
|
235
|
+
]), [35, 10, 14, 9, 32, 10]);
|
|
139
236
|
}
|
|
140
237
|
async function readAllStdin() {
|
|
141
238
|
const chunks = [];
|
|
@@ -193,6 +290,10 @@ async function bootstrapRuntime(options) {
|
|
|
193
290
|
sessionId,
|
|
194
291
|
workspaceRoot: project.workspaceRoot,
|
|
195
292
|
}, resumed);
|
|
293
|
+
const ui = UI();
|
|
294
|
+
const historyLen = resumed.history?.length ?? 0;
|
|
295
|
+
console.log(ui.success(`${ICON.session} Session restored: ${options.resume}`));
|
|
296
|
+
console.log(ui.muted(` ${historyLen} messages recovered, cwd: ${resumed.cwd || process.cwd()}`));
|
|
196
297
|
}
|
|
197
298
|
}
|
|
198
299
|
return {
|
|
@@ -210,6 +311,56 @@ async function bootstrapRuntime(options) {
|
|
|
210
311
|
async function persistSnapshot(runtime) {
|
|
211
312
|
await runtime.sessions.appendSnapshot(runtime.sessionId, runtime.agent.exportState(runtime.context.serializeState()));
|
|
212
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Turn a raw tool name + args JSON into a concise, human-readable action label.
|
|
316
|
+
* e.g. "read_file" + '{"path":"src/index.ts"}' → "Reading src/index.ts"
|
|
317
|
+
*/
|
|
318
|
+
function humanizeToolAction(toolName, argsPreview) {
|
|
319
|
+
// Try to extract a path or key arg from the preview
|
|
320
|
+
let target = "";
|
|
321
|
+
try {
|
|
322
|
+
const parsed = JSON.parse(argsPreview.replace(/\.\.\.$/, ""));
|
|
323
|
+
target = parsed.path || parsed.file_path || parsed.pattern || parsed.command || parsed.url || parsed.directory || "";
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// argsPreview may be truncated / not valid JSON — try to extract a path-like string
|
|
327
|
+
const pathMatch = argsPreview.match(/["']?([^\s"',{}]+\.[a-z]{1,6})["']?/i)
|
|
328
|
+
|| argsPreview.match(/["']?([^\s"',{}]{3,60})["']?/);
|
|
329
|
+
target = pathMatch?.[1] ?? "";
|
|
330
|
+
}
|
|
331
|
+
// Shorten long paths to last 2 segments
|
|
332
|
+
if (target.length > 50) {
|
|
333
|
+
const parts = target.split("/");
|
|
334
|
+
target = parts.length > 2 ? `…/${parts.slice(-2).join("/")}` : target.slice(-47) + "…";
|
|
335
|
+
}
|
|
336
|
+
const suffix = target ? ` ${UI().info(target)}` : "";
|
|
337
|
+
const verbs = {
|
|
338
|
+
read_file: "Reading",
|
|
339
|
+
write_to_file: "Writing",
|
|
340
|
+
apply_diff: "Editing",
|
|
341
|
+
list_directory: "Listing",
|
|
342
|
+
create_directory: "Creating directory",
|
|
343
|
+
delete_file: "Deleting",
|
|
344
|
+
move_file: "Moving",
|
|
345
|
+
get_file_info: "Checking",
|
|
346
|
+
search_files: "Searching",
|
|
347
|
+
find_files: "Finding files",
|
|
348
|
+
show_diff: "Diffing",
|
|
349
|
+
search_and_replace_all: "Replacing in",
|
|
350
|
+
fetch_url: "Fetching",
|
|
351
|
+
run_bash_command: "Running",
|
|
352
|
+
git_status: "Git status",
|
|
353
|
+
git_diff: "Git diff",
|
|
354
|
+
git_log: "Git log",
|
|
355
|
+
git_blame: "Git blame",
|
|
356
|
+
git_stash: "Git stash",
|
|
357
|
+
run_linter: "Linting",
|
|
358
|
+
run_tests: "Testing",
|
|
359
|
+
security_scan: "Scanning",
|
|
360
|
+
};
|
|
361
|
+
const verb = verbs[toolName] ?? toolName;
|
|
362
|
+
return `${verb}${suffix}`;
|
|
363
|
+
}
|
|
213
364
|
async function executeTurn(runtime, rawPrompt, signal, outputFormat, streamOutput) {
|
|
214
365
|
const preprocessed = await preprocessPrompt(rawPrompt, Math.min(runtime.config.maxReadBytes, 16_000));
|
|
215
366
|
if (preprocessed.includedFiles.length > 0) {
|
|
@@ -231,18 +382,26 @@ async function executeTurn(runtime, rawPrompt, signal, outputFormat, streamOutpu
|
|
|
231
382
|
process.stderr.write(`warning: ${warning}\n`);
|
|
232
383
|
}
|
|
233
384
|
}
|
|
234
|
-
const
|
|
385
|
+
const turnStartedAt = Date.now();
|
|
386
|
+
let firstTokenAt = 0;
|
|
387
|
+
const spinner = ora({
|
|
388
|
+
text: `${ICON.spark} thinking...`,
|
|
389
|
+
color: "cyan",
|
|
390
|
+
spinner: "dots",
|
|
391
|
+
}).start();
|
|
235
392
|
let printedToken = false;
|
|
236
393
|
let printedAgentPrefix = false;
|
|
237
394
|
let toolTimer = null;
|
|
238
395
|
let toolStartAt = 0;
|
|
239
396
|
let currentToolLabel = "";
|
|
397
|
+
let toolGroupTotal = 0;
|
|
240
398
|
const clearToolTimer = () => {
|
|
241
399
|
if (toolTimer) {
|
|
242
400
|
clearInterval(toolTimer);
|
|
243
401
|
toolTimer = null;
|
|
244
402
|
}
|
|
245
403
|
};
|
|
404
|
+
const ui = UI();
|
|
246
405
|
const callbacks = {
|
|
247
406
|
onToken: async (token) => {
|
|
248
407
|
if (!streamOutput)
|
|
@@ -251,37 +410,55 @@ async function executeTurn(runtime, rawPrompt, signal, outputFormat, streamOutpu
|
|
|
251
410
|
spinner.stop();
|
|
252
411
|
clearToolTimer();
|
|
253
412
|
if (!printedAgentPrefix) {
|
|
254
|
-
|
|
413
|
+
if (!firstTokenAt)
|
|
414
|
+
firstTokenAt = Date.now();
|
|
415
|
+
processStdout.write(ui.accent.bold(`${ICON.spark} Assistant `));
|
|
255
416
|
printedAgentPrefix = true;
|
|
256
417
|
}
|
|
257
418
|
printedToken = true;
|
|
258
419
|
processStdout.write(token);
|
|
259
420
|
},
|
|
260
|
-
onRetry: async (attempt, delayMs) => {
|
|
261
|
-
|
|
421
|
+
onRetry: async (attempt, delayMs, reason) => {
|
|
422
|
+
const normalizedReason = reason.replace(/\s+/g, " ").trim();
|
|
423
|
+
const shortReason = normalizedReason.length > 72
|
|
424
|
+
? `${normalizedReason.slice(0, 69)}...`
|
|
425
|
+
: normalizedReason;
|
|
426
|
+
spinner.text = `${ICON.retry} retry ${attempt}/3 in ${Math.floor(delayMs / 1000)}s (${shortReason})`;
|
|
262
427
|
},
|
|
263
428
|
onToolStart: async (event) => {
|
|
264
429
|
if (!streamOutput)
|
|
265
430
|
return;
|
|
266
431
|
clearToolTimer();
|
|
267
432
|
toolStartAt = Date.now();
|
|
268
|
-
|
|
269
|
-
|
|
433
|
+
toolGroupTotal = event.total;
|
|
434
|
+
currentToolLabel = humanizeToolAction(event.toolName, event.argsPreview);
|
|
435
|
+
const progressPrefix = event.total > 1 ? ui.muted(`[${event.index}/${event.total}] `) : "";
|
|
436
|
+
spinner.start(`${progressPrefix}${chalk.dim(currentToolLabel)} (0.0s)`);
|
|
270
437
|
toolTimer = setInterval(() => {
|
|
271
|
-
const elapsed =
|
|
272
|
-
spinner.text =
|
|
273
|
-
},
|
|
438
|
+
const elapsed = ((Date.now() - toolStartAt) / 1000).toFixed(1);
|
|
439
|
+
spinner.text = `${progressPrefix}${chalk.dim(currentToolLabel)} (${elapsed}s)`;
|
|
440
|
+
}, 200);
|
|
274
441
|
},
|
|
275
442
|
onToolEnd: async (event) => {
|
|
276
443
|
if (!streamOutput)
|
|
277
444
|
return;
|
|
278
445
|
clearToolTimer();
|
|
446
|
+
const elapsed = ((Date.now() - toolStartAt) / 1000).toFixed(1);
|
|
279
447
|
spinner.stop();
|
|
448
|
+
const label = humanizeToolAction(event.toolName, "");
|
|
449
|
+
console.log(renderToolProgress(event.index, event.total, label, elapsed !== "0.0" ? `${elapsed}s` : undefined));
|
|
280
450
|
if (runtime.agent.isVerboseTools()) {
|
|
281
|
-
console.log(
|
|
451
|
+
console.log(ui.muted(` ${event.resultPreview.split("\n").join("\n ")}`));
|
|
282
452
|
}
|
|
283
453
|
else if (event.truncated) {
|
|
284
|
-
console.log(
|
|
454
|
+
console.log(ui.warning(` Output truncated before returning to model.`));
|
|
455
|
+
}
|
|
456
|
+
// Restart spinner so the user sees progress while the agent processes
|
|
457
|
+
// tool results and calls the router again.
|
|
458
|
+
if (event.index === event.total) {
|
|
459
|
+
printedToken = false;
|
|
460
|
+
printedAgentPrefix = false;
|
|
461
|
+
spinner.start(`${ICON.spark} thinking...`);
|
|
285
462
|
}
|
|
286
463
|
},
|
|
287
464
|
};
|
|
@@ -295,15 +472,11 @@ async function executeTurn(runtime, rawPrompt, signal, outputFormat, streamOutpu
|
|
|
295
472
|
const responseText = result.response;
|
|
296
473
|
if (streamOutput) {
|
|
297
474
|
if (!printedToken) {
|
|
298
|
-
console.log(
|
|
475
|
+
console.log(UI().accent.bold(`${ICON.spark} Assistant`));
|
|
299
476
|
console.log(renderMarkdown(responseText).trimEnd());
|
|
300
477
|
}
|
|
301
478
|
else {
|
|
302
479
|
processStdout.write("\n");
|
|
303
|
-
if (looksLikeMarkdown(responseText)) {
|
|
304
|
-
console.log(chalk.dim("[Rendered Markdown]"));
|
|
305
|
-
console.log(renderMarkdown(responseText).trimEnd());
|
|
306
|
-
}
|
|
307
480
|
}
|
|
308
481
|
}
|
|
309
482
|
if (!streamOutput && outputFormat === "text") {
|
|
@@ -311,16 +484,30 @@ async function executeTurn(runtime, rawPrompt, signal, outputFormat, streamOutpu
|
|
|
311
484
|
}
|
|
312
485
|
if (result.usage) {
|
|
313
486
|
const metrics = runtime.context.recordTurn(result.model, result.usage);
|
|
487
|
+
const turnMs = Date.now() - turnStartedAt;
|
|
488
|
+
const ttftMs = firstTokenAt ? firstTokenAt - turnStartedAt : 0;
|
|
314
489
|
if (streamOutput) {
|
|
315
|
-
|
|
490
|
+
const parts = [
|
|
491
|
+
renderModelBadge(result.model),
|
|
492
|
+
`ctx ${contextBar(metrics.contextUtilization)} ${(metrics.contextUtilization * 100).toFixed(1)}%`,
|
|
493
|
+
`${ICON.token} in ${metrics.promptTokens.toLocaleString()}`,
|
|
494
|
+
`${ICON.token} out ${metrics.completionTokens.toLocaleString()}`,
|
|
495
|
+
`turn ${formatUsd(metrics.turnCostUsd)}`,
|
|
496
|
+
`total ${formatUsd(metrics.cumulativeCostUsd)}`,
|
|
497
|
+
];
|
|
498
|
+
if (ttftMs > 0) {
|
|
499
|
+
parts.push(`${ICON.ttft} ${(ttftMs / 1000).toFixed(2)}s TTFT`);
|
|
500
|
+
}
|
|
501
|
+
parts.push(`${(turnMs / 1000).toFixed(1)}s`);
|
|
502
|
+
console.log(UI().muted(parts.join(" | ")));
|
|
316
503
|
}
|
|
317
504
|
if (metrics.warnThresholdCrossed && streamOutput) {
|
|
318
|
-
console.log(
|
|
505
|
+
console.log(UI().warning(`Context usage warning: ${(metrics.contextUtilization * 100).toFixed(1)}% of configured window (${runtime.config.contextWindowTokens.toLocaleString()} tokens).`));
|
|
319
506
|
}
|
|
320
507
|
if (metrics.autoCompactRecommended) {
|
|
321
508
|
const summary = await runtime.agent.compactHistory(signal);
|
|
322
509
|
if (streamOutput) {
|
|
323
|
-
console.log(
|
|
510
|
+
console.log(UI().warning(`Auto-compact triggered at ${(metrics.contextUtilization * 100).toFixed(1)}% context usage.`));
|
|
324
511
|
console.log(renderMarkdown(summary).trimEnd());
|
|
325
512
|
}
|
|
326
513
|
}
|
|
@@ -357,23 +544,58 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
357
544
|
return { handled: true, shouldExit: true };
|
|
358
545
|
case "/clear":
|
|
359
546
|
console.clear();
|
|
547
|
+
printBanner(runtime, runtime.agent.getPermissionMode());
|
|
360
548
|
return { handled: true, shouldExit: false };
|
|
361
549
|
case "/tools":
|
|
362
550
|
printTools();
|
|
363
551
|
return { handled: true, shouldExit: false };
|
|
364
|
-
case "/permissions":
|
|
365
|
-
|
|
552
|
+
case "/permissions": {
|
|
553
|
+
const mode = runtime.agent.getPermissionMode();
|
|
554
|
+
const policyRows = mode === "safe"
|
|
555
|
+
? [
|
|
556
|
+
["Writes", UI().danger("disabled")],
|
|
557
|
+
["Shell", UI().danger("disabled")],
|
|
558
|
+
["Approvals", UI().success("n/a")],
|
|
559
|
+
]
|
|
560
|
+
: mode === "standard"
|
|
561
|
+
? [
|
|
562
|
+
["Writes", UI().warning("requires confirmation")],
|
|
563
|
+
["Shell", UI().warning("requires confirmation")],
|
|
564
|
+
["Approvals", UI().info("interactive")],
|
|
565
|
+
]
|
|
566
|
+
: [
|
|
567
|
+
["Writes", UI().success("enabled")],
|
|
568
|
+
["Shell", UI().success("enabled")],
|
|
569
|
+
["Approvals", UI().warning("auto-approved")],
|
|
570
|
+
];
|
|
571
|
+
console.log("");
|
|
572
|
+
console.log(UI().accent.bold(`${ICON.shield} Permission Mode`));
|
|
573
|
+
console.log(printTable(["Setting", "Value"], policyRows, [18, 28]));
|
|
574
|
+
console.log(UI().muted(`Current mode: ${mode}`));
|
|
575
|
+
console.log("");
|
|
366
576
|
return { handled: true, shouldExit: false };
|
|
577
|
+
}
|
|
367
578
|
case "/verbose": {
|
|
368
579
|
const next = !runtime.agent.isVerboseTools();
|
|
369
580
|
runtime.agent.setVerboseTools(next);
|
|
370
|
-
console.log(
|
|
581
|
+
console.log(next
|
|
582
|
+
? UI().success("Verbose tool output: ON")
|
|
583
|
+
: UI().muted("Verbose tool output: OFF"));
|
|
371
584
|
await persistSnapshot(runtime);
|
|
372
585
|
return { handled: true, shouldExit: false };
|
|
373
586
|
}
|
|
374
587
|
case "/cost": {
|
|
375
588
|
const summary = runtime.context.getCostSummary();
|
|
376
|
-
|
|
589
|
+
const rows = [
|
|
590
|
+
["Input tokens", summary.inputTokens.toLocaleString()],
|
|
591
|
+
["Output tokens", summary.outputTokens.toLocaleString()],
|
|
592
|
+
["Total tokens", summary.totalTokens.toLocaleString()],
|
|
593
|
+
["Estimated cost", formatUsd(summary.totalCostUsd)],
|
|
594
|
+
];
|
|
595
|
+
console.log("");
|
|
596
|
+
console.log(UI().accent.bold(`${ICON.token} Session Cost`));
|
|
597
|
+
console.log(printTable(["Metric", "Value"], rows, [20, 22]));
|
|
598
|
+
console.log("");
|
|
377
599
|
return { handled: true, shouldExit: false };
|
|
378
600
|
}
|
|
379
601
|
case "/compact": {
|
|
@@ -384,18 +606,21 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
384
606
|
}
|
|
385
607
|
case "/models": {
|
|
386
608
|
const models = await runtime.agent.listModels();
|
|
609
|
+
console.log("");
|
|
610
|
+
console.log(UI().accent.bold(`${ICON.model} Models (${models.length})`));
|
|
387
611
|
console.log(formatModels(models));
|
|
612
|
+
console.log("");
|
|
388
613
|
return { handled: true, shouldExit: false };
|
|
389
614
|
}
|
|
390
615
|
case "/undo": {
|
|
391
616
|
const undone = runtime.agent.undoLastTurn();
|
|
392
617
|
if (undone) {
|
|
393
618
|
setLastRawPrompt(undone);
|
|
394
|
-
console.log("Removed last user+assistant turn.");
|
|
619
|
+
console.log(UI().success("Removed last user+assistant turn."));
|
|
395
620
|
await persistSnapshot(runtime);
|
|
396
621
|
}
|
|
397
622
|
else {
|
|
398
|
-
console.log("Nothing to undo.");
|
|
623
|
+
console.log(UI().muted("Nothing to undo."));
|
|
399
624
|
}
|
|
400
625
|
return { handled: true, shouldExit: false };
|
|
401
626
|
}
|
|
@@ -404,17 +629,21 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
404
629
|
case "/save": {
|
|
405
630
|
const name = argText || `session-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
406
631
|
const location = await runtime.sessions.saveNamed(name, runtime.agent.exportState(runtime.context.serializeState()));
|
|
407
|
-
console.log(`Saved session
|
|
632
|
+
console.log(UI().success(`Saved session "${name}"`));
|
|
633
|
+
console.log(UI().muted(`Path: ${location}`));
|
|
408
634
|
return { handled: true, shouldExit: false };
|
|
409
635
|
}
|
|
410
636
|
case "/load": {
|
|
411
637
|
if (!argText) {
|
|
412
638
|
const names = await runtime.sessions.listNamed();
|
|
413
639
|
if (names.length === 0) {
|
|
414
|
-
console.log("No named sessions found.");
|
|
640
|
+
console.log(UI().muted("No named sessions found."));
|
|
415
641
|
}
|
|
416
642
|
else {
|
|
417
|
-
console.log(
|
|
643
|
+
console.log("");
|
|
644
|
+
console.log(UI().accent.bold(`Named Sessions (${names.length})`));
|
|
645
|
+
console.log(names.map((name) => ` ${UI().info(name)}`).join("\n"));
|
|
646
|
+
console.log("");
|
|
418
647
|
}
|
|
419
648
|
return { handled: true, shouldExit: false };
|
|
420
649
|
}
|
|
@@ -425,7 +654,7 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
425
654
|
}
|
|
426
655
|
await restoreSnapshot(runtime, loaded);
|
|
427
656
|
setLastRawPrompt(loaded.lastUserInput);
|
|
428
|
-
console.log(`Loaded session
|
|
657
|
+
console.log(UI().success(`Loaded session "${argText}".`));
|
|
429
658
|
await persistSnapshot(runtime);
|
|
430
659
|
return { handled: true, shouldExit: false };
|
|
431
660
|
}
|
|
@@ -441,7 +670,7 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
441
670
|
return { handled: true, shouldExit: false };
|
|
442
671
|
}
|
|
443
672
|
process.chdir(path.resolve(resolved));
|
|
444
|
-
console.log(`Working directory: ${process.cwd()}`);
|
|
673
|
+
console.log(UI().success(`Working directory: ${process.cwd()}`));
|
|
445
674
|
await persistSnapshot(runtime);
|
|
446
675
|
return { handled: true, shouldExit: false };
|
|
447
676
|
}
|
|
@@ -451,11 +680,14 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
451
680
|
try {
|
|
452
681
|
const result = await run_linter({ path: targetPath, linter: "auto", fix: false });
|
|
453
682
|
spinner.stop();
|
|
683
|
+
console.log("");
|
|
684
|
+
console.log(UI().accent.bold(`Lint Results — ${targetPath}`));
|
|
454
685
|
console.log(result);
|
|
686
|
+
console.log("");
|
|
455
687
|
}
|
|
456
688
|
catch (err) {
|
|
457
689
|
spinner.stop();
|
|
458
|
-
console.log(
|
|
690
|
+
console.log(UI().danger(`Linter error: ${err instanceof Error ? err.message : String(err)}`));
|
|
459
691
|
}
|
|
460
692
|
return { handled: true, shouldExit: false };
|
|
461
693
|
}
|
|
@@ -465,11 +697,14 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
465
697
|
try {
|
|
466
698
|
const result = await run_tests({ path: targetPath, runner: "auto", bail: false });
|
|
467
699
|
spinner.stop();
|
|
700
|
+
console.log("");
|
|
701
|
+
console.log(UI().accent.bold(`Test Results${targetPath ? ` — ${targetPath}` : ""}`));
|
|
468
702
|
console.log(result);
|
|
703
|
+
console.log("");
|
|
469
704
|
}
|
|
470
705
|
catch (err) {
|
|
471
706
|
spinner.stop();
|
|
472
|
-
console.log(
|
|
707
|
+
console.log(UI().danger(`Test runner error: ${err instanceof Error ? err.message : String(err)}`));
|
|
473
708
|
}
|
|
474
709
|
return { handled: true, shouldExit: false };
|
|
475
710
|
}
|
|
@@ -479,11 +714,14 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
479
714
|
try {
|
|
480
715
|
const result = await security_scan({ path: targetPath, include_low_severity: false });
|
|
481
716
|
spinner.stop();
|
|
717
|
+
console.log("");
|
|
718
|
+
console.log(UI().accent.bold(`Security Scan — ${targetPath}`));
|
|
482
719
|
console.log(result);
|
|
720
|
+
console.log("");
|
|
483
721
|
}
|
|
484
722
|
catch (err) {
|
|
485
723
|
spinner.stop();
|
|
486
|
-
console.log(
|
|
724
|
+
console.log(UI().danger(`Scan error: ${err instanceof Error ? err.message : String(err)}`));
|
|
487
725
|
}
|
|
488
726
|
return { handled: true, shouldExit: false };
|
|
489
727
|
}
|
|
@@ -493,7 +731,10 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
493
731
|
// /memory clear --global — clear global memory
|
|
494
732
|
if (!argText) {
|
|
495
733
|
const output = await showMemory(runtime.workspaceRoot);
|
|
734
|
+
console.log("");
|
|
735
|
+
console.log(UI().accent.bold("Memory"));
|
|
496
736
|
console.log(output);
|
|
737
|
+
console.log("");
|
|
497
738
|
return { handled: true, shouldExit: false };
|
|
498
739
|
}
|
|
499
740
|
const memArgs = argText.split(/\s+/);
|
|
@@ -502,17 +743,17 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
502
743
|
await clearMemory(scope, runtime.workspaceRoot);
|
|
503
744
|
const fresh = await loadMemory(runtime.workspaceRoot);
|
|
504
745
|
runtime.agent.updateMemory(fresh);
|
|
505
|
-
console.log(`${scope === "global" ? "Global" : "Project"} memory cleared.`);
|
|
746
|
+
console.log(UI().success(`${scope === "global" ? "Global" : "Project"} memory cleared.`));
|
|
506
747
|
return { handled: true, shouldExit: false };
|
|
507
748
|
}
|
|
508
|
-
console.log(`Unknown /memory subcommand: ${argText}`);
|
|
749
|
+
console.log(UI().warning(`Unknown /memory subcommand: ${argText}`));
|
|
509
750
|
return { handled: true, shouldExit: false };
|
|
510
751
|
}
|
|
511
752
|
case "/remember": {
|
|
512
753
|
// /remember <text> — append to project memory
|
|
513
754
|
// /remember --global <text> — append to global memory
|
|
514
755
|
if (!argText) {
|
|
515
|
-
console.log("Usage: /remember <text> or /remember --global <text>");
|
|
756
|
+
console.log(UI().warning("Usage: /remember <text> or /remember --global <text>"));
|
|
516
757
|
return { handled: true, shouldExit: false };
|
|
517
758
|
}
|
|
518
759
|
let scope = "project";
|
|
@@ -522,14 +763,61 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
522
763
|
fact = fact.slice("--global".length).trim();
|
|
523
764
|
}
|
|
524
765
|
if (!fact) {
|
|
525
|
-
console.log("Nothing to remember — provide text after the flag.");
|
|
766
|
+
console.log(UI().warning("Nothing to remember — provide text after the flag."));
|
|
526
767
|
return { handled: true, shouldExit: false };
|
|
527
768
|
}
|
|
528
769
|
const savedPath = await appendMemory(fact, scope, runtime.workspaceRoot);
|
|
529
770
|
const fresh = await loadMemory(runtime.workspaceRoot);
|
|
530
771
|
runtime.agent.updateMemory(fresh);
|
|
531
|
-
console.log(
|
|
532
|
-
console.log(
|
|
772
|
+
console.log(UI().success(`Remembered (${scope}): ${fact}`));
|
|
773
|
+
console.log(UI().muted(`Path: ${savedPath}`));
|
|
774
|
+
return { handled: true, shouldExit: false };
|
|
775
|
+
}
|
|
776
|
+
case "/logout": {
|
|
777
|
+
try {
|
|
778
|
+
await runLogout();
|
|
779
|
+
console.log(UI().success("Logged out. Run /login or restart to switch accounts."));
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
console.log(UI().danger(`Logout error: ${err instanceof Error ? err.message : String(err)}`));
|
|
783
|
+
}
|
|
784
|
+
return { handled: true, shouldExit: false };
|
|
785
|
+
}
|
|
786
|
+
case "/login": {
|
|
787
|
+
const routerUrl = process.env.LLM_ROUTER_BASE_URL || "";
|
|
788
|
+
try {
|
|
789
|
+
await runLogin({ routerUrl });
|
|
790
|
+
// Reload credentials into env so the agent uses the new account immediately
|
|
791
|
+
const conn = await resolveConnectionFromEnvOrCreds();
|
|
792
|
+
if (conn) {
|
|
793
|
+
console.log(UI().success("Logged in. New credentials active for this session."));
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
catch (err) {
|
|
797
|
+
console.log(UI().danger(`Login error: ${err instanceof Error ? err.message : String(err)}`));
|
|
798
|
+
}
|
|
799
|
+
return { handled: true, shouldExit: false };
|
|
800
|
+
}
|
|
801
|
+
case "/whoami": {
|
|
802
|
+
await runWhoami();
|
|
803
|
+
return { handled: true, shouldExit: false };
|
|
804
|
+
}
|
|
805
|
+
case "/theme": {
|
|
806
|
+
const validThemes = ["dark", "light", "minimal"];
|
|
807
|
+
if (!argText || !validThemes.includes(argText)) {
|
|
808
|
+
console.log("");
|
|
809
|
+
console.log(UI().accent.bold(`Themes`));
|
|
810
|
+
console.log(printTable(["Theme", "Description"], [
|
|
811
|
+
[getThemeName() === "dark" ? UI().success("dark (active)") : "dark", "Full color with warm accent tones"],
|
|
812
|
+
[getThemeName() === "light" ? UI().success("light (active)") : "light", "Muted palette for light terminals"],
|
|
813
|
+
[getThemeName() === "minimal" ? UI().success("minimal (active)") : "minimal", "No color, bold/dim only"],
|
|
814
|
+
], [22, 45]));
|
|
815
|
+
console.log(UI().muted(`\nUsage: /theme <dark|light|minimal>`));
|
|
816
|
+
console.log("");
|
|
817
|
+
return { handled: true, shouldExit: false };
|
|
818
|
+
}
|
|
819
|
+
setTheme(argText);
|
|
820
|
+
console.log(UI().success(`Theme set to: ${argText}`));
|
|
533
821
|
return { handled: true, shouldExit: false };
|
|
534
822
|
}
|
|
535
823
|
case "/review": {
|
|
@@ -554,7 +842,7 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
554
842
|
}
|
|
555
843
|
catch (err) {
|
|
556
844
|
if (!(err instanceof Error && err.name === "TurnCancelledError")) {
|
|
557
|
-
console.log(
|
|
845
|
+
console.log(UI().danger(`Review error: ${err instanceof Error ? err.message : String(err)}`));
|
|
558
846
|
}
|
|
559
847
|
}
|
|
560
848
|
return { handled: true, shouldExit: false };
|
|
@@ -575,10 +863,16 @@ async function runNonInteractive(runtime, options) {
|
|
|
575
863
|
await executeTurn(runtime, promptText, controller.signal, options.outputFormat, false);
|
|
576
864
|
}
|
|
577
865
|
async function runInteractive(runtime, options) {
|
|
866
|
+
_isInteractiveSession = true;
|
|
578
867
|
printBanner(runtime, options.permissions);
|
|
579
868
|
const health = await checkRouterHealth();
|
|
580
869
|
if (!health.ok) {
|
|
581
|
-
console.log(
|
|
870
|
+
console.log(UI().warning(`${ICON.status} connection warning`));
|
|
871
|
+
console.log(UI().warning(` ${health.message} — check LLM_ROUTER_BASE_URL.`));
|
|
872
|
+
console.log();
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
console.log(UI().success(`${ICON.status} router connection healthy.`));
|
|
582
876
|
console.log();
|
|
583
877
|
}
|
|
584
878
|
const replInput = new ReplInput(runtime.paths.historyFile, runtime.config.historyLimit);
|
|
@@ -602,7 +896,25 @@ async function runInteractive(runtime, options) {
|
|
|
602
896
|
});
|
|
603
897
|
});
|
|
604
898
|
while (true) {
|
|
605
|
-
|
|
899
|
+
// ora spinners write ANSI codes to stdout while readline manages the same
|
|
900
|
+
// stream, which can leave the readline interface in a closed/corrupt state
|
|
901
|
+
// (especially on Node.js 22+). Always reset after a turn so the next
|
|
902
|
+
// prompt() call starts with a fresh, healthy interface.
|
|
903
|
+
let userInput;
|
|
904
|
+
try {
|
|
905
|
+
userInput = await replInput.prompt();
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
try {
|
|
909
|
+
await replInput.resetInterface();
|
|
910
|
+
userInput = await replInput.prompt();
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
// Truly unrecoverable (e.g. stdin closed / Ctrl-D); exit gracefully.
|
|
914
|
+
console.log(chalk.yellow("\nSession ended."));
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
606
918
|
const trimmed = userInput.trim();
|
|
607
919
|
if (!trimmed)
|
|
608
920
|
continue;
|
|
@@ -617,18 +929,19 @@ async function runInteractive(runtime, options) {
|
|
|
617
929
|
}
|
|
618
930
|
if (trimmed === "/retry") {
|
|
619
931
|
if (!lastRawPrompt) {
|
|
620
|
-
console.log("No previous prompt to retry.");
|
|
932
|
+
console.log(UI().warning("No previous prompt to retry."));
|
|
621
933
|
continue;
|
|
622
934
|
}
|
|
623
935
|
}
|
|
624
936
|
else {
|
|
625
|
-
console.log(`Unknown command: ${trimmed}`);
|
|
937
|
+
console.log(UI().warning(`Unknown command: ${trimmed}`));
|
|
938
|
+
console.log(UI().muted(`${ICON.tip} Run /help to view available commands.`));
|
|
626
939
|
continue;
|
|
627
940
|
}
|
|
628
941
|
}
|
|
629
942
|
const rawPrompt = trimmed === "/retry" ? lastRawPrompt ?? "" : userInput;
|
|
630
943
|
if (!rawPrompt.trim()) {
|
|
631
|
-
console.log("No previous prompt to retry.");
|
|
944
|
+
console.log(UI().warning("No previous prompt to retry."));
|
|
632
945
|
continue;
|
|
633
946
|
}
|
|
634
947
|
lastRawPrompt = rawPrompt;
|
|
@@ -642,31 +955,52 @@ async function runInteractive(runtime, options) {
|
|
|
642
955
|
}
|
|
643
956
|
else {
|
|
644
957
|
const message = err instanceof Error ? err.message : String(err);
|
|
645
|
-
|
|
958
|
+
// Contextual error suggestions
|
|
959
|
+
let suggestion;
|
|
960
|
+
if (message.includes("504") || message.includes("timeout")) {
|
|
961
|
+
suggestion = "The upstream model timed out. Try a faster model with --model or check /models.";
|
|
962
|
+
}
|
|
963
|
+
else if (message.includes("context") || message.includes("token")) {
|
|
964
|
+
suggestion = "Try /compact to reduce context usage, or start a new session.";
|
|
965
|
+
}
|
|
966
|
+
else if (message.includes("429") || message.includes("rate")) {
|
|
967
|
+
suggestion = "Rate limited. Wait a moment and try again, or switch models.";
|
|
968
|
+
}
|
|
969
|
+
else if (message.includes("401") || message.includes("auth")) {
|
|
970
|
+
suggestion = "Authentication failed. Try /login or check your API key.";
|
|
971
|
+
}
|
|
972
|
+
console.log(renderErrorCard(message, suggestion));
|
|
646
973
|
}
|
|
647
974
|
}
|
|
648
975
|
finally {
|
|
649
976
|
activeTurnController = null;
|
|
650
977
|
}
|
|
651
978
|
console.log();
|
|
979
|
+
// Reset readline interface after every turn so ora spinner side-effects
|
|
980
|
+
// don't accumulate and corrupt the next prompt() call.
|
|
981
|
+
await replInput.resetInterface();
|
|
652
982
|
}
|
|
653
983
|
await replInput.close();
|
|
654
984
|
}
|
|
655
985
|
// ── First-run setup wizard ────────────────────────────────────────────────────
|
|
656
986
|
async function runFirstTimeSetup() {
|
|
657
|
-
const {
|
|
658
|
-
console.log(
|
|
659
|
-
console.log(
|
|
987
|
+
const { input, select } = await import("@inquirer/prompts");
|
|
988
|
+
console.log("");
|
|
989
|
+
console.log(renderPanel([
|
|
990
|
+
` ${UI().title(`${ICON.spark} Welcome to LLM-Router CLI Agent`)}`,
|
|
991
|
+
` ${UI().muted("No connection found. Let's configure authentication.")}`,
|
|
992
|
+
]));
|
|
993
|
+
console.log("");
|
|
660
994
|
const routerUrl = await input({
|
|
661
995
|
message: "Router URL:",
|
|
662
|
-
default: "https://
|
|
996
|
+
default: "https://llm-router-6o4u74bhpq-uc.a.run.app",
|
|
663
997
|
validate: (v) => {
|
|
664
998
|
try {
|
|
665
999
|
new URL(v);
|
|
666
1000
|
return true;
|
|
667
1001
|
}
|
|
668
1002
|
catch {
|
|
669
|
-
return "Enter a valid URL (e.g. https://
|
|
1003
|
+
return "Enter a valid URL (e.g. https://llm-router-6o4u74bhpq-uc.a.run.app)";
|
|
670
1004
|
}
|
|
671
1005
|
},
|
|
672
1006
|
});
|
|
@@ -695,7 +1029,7 @@ async function runFirstTimeSetup() {
|
|
|
695
1029
|
});
|
|
696
1030
|
process.env.LLM_ROUTER_BASE_URL = routerUrl;
|
|
697
1031
|
process.env.LLM_ROUTER_API_KEY = apiKey;
|
|
698
|
-
console.log(
|
|
1032
|
+
console.log(UI().success(`\n${ICON.status} API key saved.\n`));
|
|
699
1033
|
}
|
|
700
1034
|
return true;
|
|
701
1035
|
}
|
|
@@ -761,7 +1095,14 @@ program
|
|
|
761
1095
|
await persistSnapshot(runtime);
|
|
762
1096
|
const nonInteractive = Boolean(options.prompt) || !processStdin.isTTY;
|
|
763
1097
|
if (nonInteractive) {
|
|
764
|
-
|
|
1098
|
+
try {
|
|
1099
|
+
await runNonInteractive(runtime, options);
|
|
1100
|
+
}
|
|
1101
|
+
catch (err) {
|
|
1102
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1103
|
+
console.error(chalk.red(`\nError: ${msg}`));
|
|
1104
|
+
process.exit(1);
|
|
1105
|
+
}
|
|
765
1106
|
return;
|
|
766
1107
|
}
|
|
767
1108
|
await runInteractive(runtime, options);
|
|
@@ -781,7 +1122,7 @@ program
|
|
|
781
1122
|
const { input } = await import("@inquirer/prompts");
|
|
782
1123
|
routerUrl = await input({
|
|
783
1124
|
message: "Router URL:",
|
|
784
|
-
default: "https://
|
|
1125
|
+
default: "https://llm-router-6o4u74bhpq-uc.a.run.app",
|
|
785
1126
|
validate: (v) => {
|
|
786
1127
|
try {
|
|
787
1128
|
new URL(v);
|
|
@@ -897,6 +1238,21 @@ program
|
|
|
897
1238
|
console.log();
|
|
898
1239
|
}
|
|
899
1240
|
});
|
|
1241
|
+
// Safety net: catch any unhandled promise rejections so they display a useful
|
|
1242
|
+
// message instead of crashing the process silently (Node 22 terminates on them).
|
|
1243
|
+
// In interactive mode we log but do NOT exit — the REPL should survive stray
|
|
1244
|
+
// async errors (e.g. failed snapshot writes, readline hiccups).
|
|
1245
|
+
let _isInteractiveSession = false;
|
|
1246
|
+
process.on("unhandledRejection", (reason) => {
|
|
1247
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
1248
|
+
if (_isInteractiveSession) {
|
|
1249
|
+
console.error(chalk.red(`\nAsync error (non-fatal): ${msg}`));
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
console.error(chalk.red(`\nUnhandled error: ${msg}`));
|
|
1253
|
+
process.exit(1);
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
900
1256
|
program.parseAsync().catch((err) => {
|
|
901
1257
|
const message = err instanceof Error ? err.message : String(err);
|
|
902
1258
|
console.error(chalk.red(message));
|