llm-agent-cli 2.0.0 → 2.0.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/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 +451 -108
- 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.map +1 -1
- package/dist/renderer.js +1 -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
|
@@ -21,6 +21,30 @@ 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
|
+
const UI = {
|
|
25
|
+
accent: chalk.hex("#D97757"),
|
|
26
|
+
info: chalk.cyanBright,
|
|
27
|
+
success: chalk.greenBright,
|
|
28
|
+
warning: chalk.yellowBright,
|
|
29
|
+
danger: chalk.redBright,
|
|
30
|
+
muted: chalk.gray,
|
|
31
|
+
title: chalk.bold.white,
|
|
32
|
+
};
|
|
33
|
+
const ICON = {
|
|
34
|
+
spark: "✦",
|
|
35
|
+
gear: "⚙",
|
|
36
|
+
shield: "🛡",
|
|
37
|
+
folder: "📁",
|
|
38
|
+
network: "🌐",
|
|
39
|
+
model: "🧠",
|
|
40
|
+
stack: "🧩",
|
|
41
|
+
session: "🗂",
|
|
42
|
+
tip: "💡",
|
|
43
|
+
status: "●",
|
|
44
|
+
command: "⌘",
|
|
45
|
+
token: "◉",
|
|
46
|
+
retry: "↻",
|
|
47
|
+
};
|
|
24
48
|
function isPermissionMode(value) {
|
|
25
49
|
return PERMISSION_MODES.includes(value);
|
|
26
50
|
}
|
|
@@ -30,56 +54,161 @@ function isOutputFormat(value) {
|
|
|
30
54
|
function formatUsd(value) {
|
|
31
55
|
return `$${value.toFixed(4)}`;
|
|
32
56
|
}
|
|
33
|
-
function
|
|
34
|
-
return
|
|
57
|
+
function stripAnsi(value) {
|
|
58
|
+
return value.replace(/\x1B\[[0-9;]*m/g, "");
|
|
59
|
+
}
|
|
60
|
+
function badge(label, tone = "info") {
|
|
61
|
+
const color = tone === "success"
|
|
62
|
+
? UI.success
|
|
63
|
+
: tone === "warning"
|
|
64
|
+
? UI.warning
|
|
65
|
+
: tone === "danger"
|
|
66
|
+
? UI.danger
|
|
67
|
+
: tone === "muted"
|
|
68
|
+
? UI.muted
|
|
69
|
+
: UI.info;
|
|
70
|
+
return color(`◈ ${label}`);
|
|
71
|
+
}
|
|
72
|
+
function fitCell(value, width) {
|
|
73
|
+
const plain = stripAnsi(value);
|
|
74
|
+
if (plain.length <= width) {
|
|
75
|
+
return `${value}${" ".repeat(width - plain.length)}`;
|
|
76
|
+
}
|
|
77
|
+
return `${plain.slice(0, Math.max(0, width - 1))}…`;
|
|
78
|
+
}
|
|
79
|
+
function fitInline(value, width) {
|
|
80
|
+
const plain = stripAnsi(value);
|
|
81
|
+
if (plain.length <= width) {
|
|
82
|
+
return `${value}${" ".repeat(width - plain.length)}`;
|
|
83
|
+
}
|
|
84
|
+
return `${plain.slice(0, Math.max(0, width - 1))}…`;
|
|
85
|
+
}
|
|
86
|
+
function renderPanel(lines) {
|
|
87
|
+
const minWidth = 72;
|
|
88
|
+
const maxWidth = 112;
|
|
89
|
+
const dynamicWidth = Math.max(minWidth, Math.min(maxWidth, (processStdout.columns ?? 100) - 6));
|
|
90
|
+
const innerWidth = dynamicWidth;
|
|
91
|
+
const top = UI.accent(`╭${"─".repeat(innerWidth)}╮`);
|
|
92
|
+
const body = lines.map((line) => `${UI.accent("│")}${fitInline(line, innerWidth)}${UI.accent("│")}`);
|
|
93
|
+
const bottom = UI.accent(`╰${"─".repeat(innerWidth)}╯`);
|
|
94
|
+
return [top, ...body, bottom].join("\n");
|
|
95
|
+
}
|
|
96
|
+
function printTable(headers, rows, widths) {
|
|
97
|
+
const divider = widths.map((w) => "─".repeat(w)).join(" ");
|
|
98
|
+
const headerLine = headers
|
|
99
|
+
.map((header, idx) => fitCell(UI.title(header), widths[idx] ?? 20))
|
|
100
|
+
.join(" ");
|
|
101
|
+
const bodyLines = rows.map((row) => row
|
|
102
|
+
.map((cell, idx) => fitCell(cell, widths[idx] ?? 20))
|
|
103
|
+
.join(" "));
|
|
104
|
+
return [headerLine, UI.muted(divider), ...bodyLines].join("\n");
|
|
105
|
+
}
|
|
106
|
+
function contextBar(ratio, width = 18) {
|
|
107
|
+
const clamped = Math.max(0, Math.min(1, ratio));
|
|
108
|
+
const filled = Math.round(clamped * width);
|
|
109
|
+
const bar = `${"█".repeat(filled)}${"░".repeat(Math.max(0, width - filled))}`;
|
|
110
|
+
if (clamped >= 0.9)
|
|
111
|
+
return UI.danger(bar);
|
|
112
|
+
if (clamped >= 0.7)
|
|
113
|
+
return UI.warning(bar);
|
|
114
|
+
return UI.success(bar);
|
|
35
115
|
}
|
|
36
116
|
function printBanner(runtime, permissionMode) {
|
|
37
117
|
const model = process.env.LLM_ROUTER_MODEL ?? "auto";
|
|
38
118
|
const baseUrl = process.env.LLM_ROUTER_BASE_URL ?? "(not set)";
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
119
|
+
const permissionTone = permissionMode === "safe"
|
|
120
|
+
? "success"
|
|
121
|
+
: permissionMode === "yolo"
|
|
122
|
+
? "danger"
|
|
123
|
+
: "warning";
|
|
124
|
+
const connectionTone = baseUrl.includes("localhost") ? "info" : "success";
|
|
125
|
+
const headerLine = `${UI.title(`${ICON.spark} LLM-Router CLI Agent`)} ${badge("interactive", "muted")} ${badge(permissionMode, permissionTone)} ${badge("v2", "muted")}`;
|
|
126
|
+
const subtitleLine = UI.muted("Claude-style terminal workflow with streaming tool execution");
|
|
127
|
+
const summaryLeft = `${ICON.network} ${UI.muted("Router")} ${UI.info(baseUrl)}`;
|
|
128
|
+
const summaryRight = `${ICON.model} ${UI.muted("Model")} ${UI.success(model)}`;
|
|
129
|
+
const workspaceLine = `${ICON.folder} ${UI.muted("Workspace")} ${runtime.project.workspaceRoot}`;
|
|
130
|
+
const stackLine = `${ICON.stack} ${UI.muted("Stack")} ${runtime.project.language} • ${runtime.project.framework} • ${runtime.project.packageManager}`;
|
|
131
|
+
const sessionLine = `${ICON.session} ${UI.muted("Session")} ${runtime.sessionId}`;
|
|
132
|
+
const statusLine = `${UI.muted(ICON.status)} ${badge("ready", connectionTone)} ${badge("drop files to include", "muted")}`;
|
|
133
|
+
const panel = renderPanel([
|
|
134
|
+
` ${headerLine}`,
|
|
135
|
+
` ${subtitleLine}`,
|
|
136
|
+
` ${summaryLeft}`,
|
|
137
|
+
` ${summaryRight}`,
|
|
138
|
+
` ${workspaceLine}`,
|
|
139
|
+
` ${stackLine}`,
|
|
140
|
+
` ${sessionLine}`,
|
|
141
|
+
` ${statusLine}`,
|
|
142
|
+
]);
|
|
143
|
+
console.log("");
|
|
144
|
+
console.log(panel);
|
|
145
|
+
console.log("");
|
|
146
|
+
console.log(UI.muted(`${ICON.tip} Quick start`));
|
|
147
|
+
console.log(UI.muted(` ${ICON.command} /help ${ICON.command} /tools ${ICON.command} /models ${ICON.command} /cost ${ICON.command} /compact`));
|
|
148
|
+
console.log("");
|
|
149
|
+
}
|
|
150
|
+
function printCommandGroup(title, commands) {
|
|
151
|
+
console.log(UI.accent.bold(` ${title}`));
|
|
152
|
+
console.log(printTable(["Command", "Description"], commands.map(([cmd, desc]) => [UI.info(cmd), desc]), [28, 70]));
|
|
153
|
+
console.log("");
|
|
46
154
|
}
|
|
47
155
|
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
|
-
|
|
156
|
+
console.log("");
|
|
157
|
+
console.log(renderPanel([` ${UI.title(`${ICON.command} Command Palette`)}`]));
|
|
158
|
+
console.log("");
|
|
159
|
+
printCommandGroup("🧭 Core", [
|
|
160
|
+
["/help", "Show this help"],
|
|
161
|
+
["/exit", "Quit the CLI"],
|
|
162
|
+
["/clear", "Clear terminal output only"],
|
|
163
|
+
["/retry", "Resend last prompt"],
|
|
164
|
+
["/undo", "Remove last user+assistant turn"],
|
|
165
|
+
]);
|
|
166
|
+
printCommandGroup("🗂 Session", [
|
|
167
|
+
["/compact", "Summarize and compress conversation"],
|
|
168
|
+
["/save [name]", "Save named session snapshot"],
|
|
169
|
+
["/load [name]", "Load named session or list saved names"],
|
|
170
|
+
["/models", "List available router models"],
|
|
171
|
+
["/cost", "Show token and cost totals"],
|
|
172
|
+
]);
|
|
173
|
+
printCommandGroup("📁 Workspace", [
|
|
174
|
+
["/cd <path>", "Change working directory (within workspace root)"],
|
|
175
|
+
["/tools", "List tools and descriptions"],
|
|
176
|
+
["/permissions", "Show current permission mode"],
|
|
177
|
+
["/verbose", "Toggle verbose tool result output"],
|
|
178
|
+
]);
|
|
179
|
+
printCommandGroup("🧪 Quality", [
|
|
180
|
+
["/review [path]", "Run a structured code review prompt"],
|
|
181
|
+
["/lint [path]", "Run linter (auto detects toolchain)"],
|
|
182
|
+
["/test [path]", "Run tests (auto detects runner)"],
|
|
183
|
+
["/scan [path]", "Run security scan"],
|
|
184
|
+
]);
|
|
185
|
+
printCommandGroup("🔑 Auth", [
|
|
186
|
+
["/login", "Log in (opens browser for Google/GitHub auth)"],
|
|
187
|
+
["/logout", "Log out and clear credentials"],
|
|
188
|
+
["/whoami", "Show current account and router URL"],
|
|
189
|
+
]);
|
|
190
|
+
printCommandGroup("🧠 Memory", [
|
|
191
|
+
["/memory", "Show global + project memory"],
|
|
192
|
+
["/memory clear", "Clear project memory"],
|
|
193
|
+
["/memory clear --global", "Clear global memory"],
|
|
194
|
+
["/remember <text>", "Remember fact in project memory"],
|
|
195
|
+
["/remember --global <text>", "Remember fact in global memory"],
|
|
196
|
+
]);
|
|
72
197
|
console.log();
|
|
73
|
-
console.log(
|
|
74
|
-
console.log(
|
|
75
|
-
console.log(
|
|
198
|
+
console.log(UI.accent.bold("⌨ Input Modes"));
|
|
199
|
+
console.log(UI.muted(" • End a line with \\ for multiline continuation"));
|
|
200
|
+
console.log(UI.muted(" • Type <<< then paste blocks; finish with a single . line"));
|
|
76
201
|
console.log();
|
|
77
202
|
}
|
|
78
203
|
function printTools() {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
204
|
+
const tools = listAvailableTools();
|
|
205
|
+
const rows = tools.map((tool) => [
|
|
206
|
+
`${ICON.gear} ${UI.info(tool.name)}`,
|
|
207
|
+
tool.description,
|
|
208
|
+
]);
|
|
209
|
+
console.log("");
|
|
210
|
+
console.log(UI.accent.bold(`${ICON.gear} Tools (${tools.length})`));
|
|
211
|
+
console.log(printTable(["Name", "Description"], rows, [30, 68]));
|
|
83
212
|
console.log();
|
|
84
213
|
}
|
|
85
214
|
function modelFlag(model, key) {
|
|
@@ -120,22 +249,26 @@ function formatModels(models) {
|
|
|
120
249
|
const rows = models.map((model) => ({
|
|
121
250
|
id: String(model.id ?? ""),
|
|
122
251
|
tier: String(model.tier ?? ""),
|
|
123
|
-
|
|
252
|
+
provider: String(model.provider ?? model.owned_by ?? ""),
|
|
253
|
+
context: Number(model.contextWindow ?? model.context_window ?? 0),
|
|
124
254
|
caps: extractModelCapabilities(model),
|
|
255
|
+
isBlocked: Boolean(model.isBlocked ?? model.is_blocked ?? false),
|
|
125
256
|
}));
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
257
|
+
const tierText = (tier) => {
|
|
258
|
+
if (tier === "strong" || tier === "reasoning")
|
|
259
|
+
return UI.success(tier);
|
|
260
|
+
if (tier === "cheap")
|
|
261
|
+
return UI.warning(tier);
|
|
262
|
+
return UI.info(tier || "unknown");
|
|
263
|
+
};
|
|
264
|
+
return printTable(["Model", "Tier", "Provider", "Context", "Capabilities", "Status"], rows.map((row) => [
|
|
265
|
+
`${ICON.model} ${row.id}`,
|
|
266
|
+
tierText(row.tier),
|
|
267
|
+
row.provider || "-",
|
|
268
|
+
row.context > 0 ? `${Math.round(row.context / 1000)}k` : "-",
|
|
269
|
+
row.caps || "-",
|
|
270
|
+
row.isBlocked ? UI.warning("blocked") : UI.success("active"),
|
|
271
|
+
]), [35, 10, 14, 9, 32, 10]);
|
|
139
272
|
}
|
|
140
273
|
async function readAllStdin() {
|
|
141
274
|
const chunks = [];
|
|
@@ -210,6 +343,56 @@ async function bootstrapRuntime(options) {
|
|
|
210
343
|
async function persistSnapshot(runtime) {
|
|
211
344
|
await runtime.sessions.appendSnapshot(runtime.sessionId, runtime.agent.exportState(runtime.context.serializeState()));
|
|
212
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Turn a raw tool name + args JSON into a concise, human-readable action label.
|
|
348
|
+
* e.g. "read_file" + '{"path":"src/index.ts"}' → "Reading src/index.ts"
|
|
349
|
+
*/
|
|
350
|
+
function humanizeToolAction(toolName, argsPreview) {
|
|
351
|
+
// Try to extract a path or key arg from the preview
|
|
352
|
+
let target = "";
|
|
353
|
+
try {
|
|
354
|
+
const parsed = JSON.parse(argsPreview.replace(/\.\.\.$/, ""));
|
|
355
|
+
target = parsed.path || parsed.file_path || parsed.pattern || parsed.command || parsed.url || parsed.directory || "";
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// argsPreview may be truncated / not valid JSON — try to extract a path-like string
|
|
359
|
+
const pathMatch = argsPreview.match(/["']?([^\s"',{}]+\.[a-z]{1,6})["']?/i)
|
|
360
|
+
|| argsPreview.match(/["']?([^\s"',{}]{3,60})["']?/);
|
|
361
|
+
target = pathMatch?.[1] ?? "";
|
|
362
|
+
}
|
|
363
|
+
// Shorten long paths to last 2 segments
|
|
364
|
+
if (target.length > 50) {
|
|
365
|
+
const parts = target.split("/");
|
|
366
|
+
target = parts.length > 2 ? `…/${parts.slice(-2).join("/")}` : target.slice(-47) + "…";
|
|
367
|
+
}
|
|
368
|
+
const suffix = target ? ` ${UI.info(target)}` : "";
|
|
369
|
+
const verbs = {
|
|
370
|
+
read_file: "Reading",
|
|
371
|
+
write_to_file: "Writing",
|
|
372
|
+
apply_diff: "Editing",
|
|
373
|
+
list_directory: "Listing",
|
|
374
|
+
create_directory: "Creating directory",
|
|
375
|
+
delete_file: "Deleting",
|
|
376
|
+
move_file: "Moving",
|
|
377
|
+
get_file_info: "Checking",
|
|
378
|
+
search_files: "Searching",
|
|
379
|
+
find_files: "Finding files",
|
|
380
|
+
show_diff: "Diffing",
|
|
381
|
+
search_and_replace_all: "Replacing in",
|
|
382
|
+
fetch_url: "Fetching",
|
|
383
|
+
run_bash_command: "Running",
|
|
384
|
+
git_status: "Git status",
|
|
385
|
+
git_diff: "Git diff",
|
|
386
|
+
git_log: "Git log",
|
|
387
|
+
git_blame: "Git blame",
|
|
388
|
+
git_stash: "Git stash",
|
|
389
|
+
run_linter: "Linting",
|
|
390
|
+
run_tests: "Testing",
|
|
391
|
+
security_scan: "Scanning",
|
|
392
|
+
};
|
|
393
|
+
const verb = verbs[toolName] ?? toolName;
|
|
394
|
+
return `${verb}${suffix}`;
|
|
395
|
+
}
|
|
213
396
|
async function executeTurn(runtime, rawPrompt, signal, outputFormat, streamOutput) {
|
|
214
397
|
const preprocessed = await preprocessPrompt(rawPrompt, Math.min(runtime.config.maxReadBytes, 16_000));
|
|
215
398
|
if (preprocessed.includedFiles.length > 0) {
|
|
@@ -231,7 +414,12 @@ async function executeTurn(runtime, rawPrompt, signal, outputFormat, streamOutpu
|
|
|
231
414
|
process.stderr.write(`warning: ${warning}\n`);
|
|
232
415
|
}
|
|
233
416
|
}
|
|
234
|
-
const
|
|
417
|
+
const turnStartedAt = Date.now();
|
|
418
|
+
const spinner = ora({
|
|
419
|
+
text: `${ICON.spark} thinking...`,
|
|
420
|
+
color: "cyan",
|
|
421
|
+
spinner: "dots",
|
|
422
|
+
}).start();
|
|
235
423
|
let printedToken = false;
|
|
236
424
|
let printedAgentPrefix = false;
|
|
237
425
|
let toolTimer = null;
|
|
@@ -251,37 +439,51 @@ async function executeTurn(runtime, rawPrompt, signal, outputFormat, streamOutpu
|
|
|
251
439
|
spinner.stop();
|
|
252
440
|
clearToolTimer();
|
|
253
441
|
if (!printedAgentPrefix) {
|
|
254
|
-
processStdout.write(
|
|
442
|
+
processStdout.write(UI.accent.bold(`${ICON.spark} Assistant `));
|
|
255
443
|
printedAgentPrefix = true;
|
|
256
444
|
}
|
|
257
445
|
printedToken = true;
|
|
258
446
|
processStdout.write(token);
|
|
259
447
|
},
|
|
260
|
-
onRetry: async (attempt, delayMs) => {
|
|
261
|
-
|
|
448
|
+
onRetry: async (attempt, delayMs, reason) => {
|
|
449
|
+
const normalizedReason = reason.replace(/\s+/g, " ").trim();
|
|
450
|
+
const shortReason = normalizedReason.length > 72
|
|
451
|
+
? `${normalizedReason.slice(0, 69)}...`
|
|
452
|
+
: normalizedReason;
|
|
453
|
+
spinner.text = `${ICON.retry} retry ${attempt}/3 in ${Math.floor(delayMs / 1000)}s (${shortReason})`;
|
|
262
454
|
},
|
|
263
455
|
onToolStart: async (event) => {
|
|
264
456
|
if (!streamOutput)
|
|
265
457
|
return;
|
|
266
458
|
clearToolTimer();
|
|
267
459
|
toolStartAt = Date.now();
|
|
268
|
-
currentToolLabel =
|
|
269
|
-
spinner.start(
|
|
460
|
+
currentToolLabel = humanizeToolAction(event.toolName, event.argsPreview);
|
|
461
|
+
spinner.start(`${chalk.dim(currentToolLabel)} (0.0s)`);
|
|
270
462
|
toolTimer = setInterval(() => {
|
|
271
|
-
const elapsed =
|
|
272
|
-
spinner.text =
|
|
273
|
-
},
|
|
463
|
+
const elapsed = ((Date.now() - toolStartAt) / 1000).toFixed(1);
|
|
464
|
+
spinner.text = `${chalk.dim(currentToolLabel)} (${elapsed}s)`;
|
|
465
|
+
}, 200);
|
|
274
466
|
},
|
|
275
467
|
onToolEnd: async (event) => {
|
|
276
468
|
if (!streamOutput)
|
|
277
469
|
return;
|
|
278
470
|
clearToolTimer();
|
|
471
|
+
const elapsed = ((Date.now() - toolStartAt) / 1000).toFixed(1);
|
|
279
472
|
spinner.stop();
|
|
473
|
+
const label = humanizeToolAction(event.toolName, "");
|
|
474
|
+
console.log(UI.muted(` ${UI.success("✓")} ${label}${elapsed !== "0.0" ? ` (${elapsed}s)` : ""}`));
|
|
280
475
|
if (runtime.agent.isVerboseTools()) {
|
|
281
|
-
console.log(
|
|
476
|
+
console.log(UI.muted(` ${event.resultPreview.split("\n").join("\n ")}`));
|
|
282
477
|
}
|
|
283
478
|
else if (event.truncated) {
|
|
284
|
-
console.log(
|
|
479
|
+
console.log(UI.warning(` Output truncated before returning to model.`));
|
|
480
|
+
}
|
|
481
|
+
// Restart spinner so the user sees progress while the agent processes
|
|
482
|
+
// tool results and calls the router again.
|
|
483
|
+
if (event.index === event.total) {
|
|
484
|
+
printedToken = false;
|
|
485
|
+
printedAgentPrefix = false;
|
|
486
|
+
spinner.start(`${ICON.spark} thinking...`);
|
|
285
487
|
}
|
|
286
488
|
},
|
|
287
489
|
};
|
|
@@ -295,15 +497,11 @@ async function executeTurn(runtime, rawPrompt, signal, outputFormat, streamOutpu
|
|
|
295
497
|
const responseText = result.response;
|
|
296
498
|
if (streamOutput) {
|
|
297
499
|
if (!printedToken) {
|
|
298
|
-
console.log(
|
|
500
|
+
console.log(UI.accent.bold(`${ICON.spark} Assistant`));
|
|
299
501
|
console.log(renderMarkdown(responseText).trimEnd());
|
|
300
502
|
}
|
|
301
503
|
else {
|
|
302
504
|
processStdout.write("\n");
|
|
303
|
-
if (looksLikeMarkdown(responseText)) {
|
|
304
|
-
console.log(chalk.dim("[Rendered Markdown]"));
|
|
305
|
-
console.log(renderMarkdown(responseText).trimEnd());
|
|
306
|
-
}
|
|
307
505
|
}
|
|
308
506
|
}
|
|
309
507
|
if (!streamOutput && outputFormat === "text") {
|
|
@@ -311,16 +509,24 @@ async function executeTurn(runtime, rawPrompt, signal, outputFormat, streamOutpu
|
|
|
311
509
|
}
|
|
312
510
|
if (result.usage) {
|
|
313
511
|
const metrics = runtime.context.recordTurn(result.model, result.usage);
|
|
512
|
+
const turnMs = Date.now() - turnStartedAt;
|
|
314
513
|
if (streamOutput) {
|
|
315
|
-
console.log(
|
|
514
|
+
console.log(UI.muted([
|
|
515
|
+
`ctx ${contextBar(metrics.contextUtilization)} ${(metrics.contextUtilization * 100).toFixed(1)}%`,
|
|
516
|
+
`${ICON.token} in ${metrics.promptTokens.toLocaleString()}`,
|
|
517
|
+
`${ICON.token} out ${metrics.completionTokens.toLocaleString()}`,
|
|
518
|
+
`turn ${formatUsd(metrics.turnCostUsd)}`,
|
|
519
|
+
`total ${formatUsd(metrics.cumulativeCostUsd)}`,
|
|
520
|
+
`${(turnMs / 1000).toFixed(1)}s`,
|
|
521
|
+
].join(" | ")));
|
|
316
522
|
}
|
|
317
523
|
if (metrics.warnThresholdCrossed && streamOutput) {
|
|
318
|
-
console.log(
|
|
524
|
+
console.log(UI.warning(`Context usage warning: ${(metrics.contextUtilization * 100).toFixed(1)}% of configured window (${runtime.config.contextWindowTokens.toLocaleString()} tokens).`));
|
|
319
525
|
}
|
|
320
526
|
if (metrics.autoCompactRecommended) {
|
|
321
527
|
const summary = await runtime.agent.compactHistory(signal);
|
|
322
528
|
if (streamOutput) {
|
|
323
|
-
console.log(
|
|
529
|
+
console.log(UI.warning(`Auto-compact triggered at ${(metrics.contextUtilization * 100).toFixed(1)}% context usage.`));
|
|
324
530
|
console.log(renderMarkdown(summary).trimEnd());
|
|
325
531
|
}
|
|
326
532
|
}
|
|
@@ -357,23 +563,58 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
357
563
|
return { handled: true, shouldExit: true };
|
|
358
564
|
case "/clear":
|
|
359
565
|
console.clear();
|
|
566
|
+
printBanner(runtime, runtime.agent.getPermissionMode());
|
|
360
567
|
return { handled: true, shouldExit: false };
|
|
361
568
|
case "/tools":
|
|
362
569
|
printTools();
|
|
363
570
|
return { handled: true, shouldExit: false };
|
|
364
|
-
case "/permissions":
|
|
365
|
-
|
|
571
|
+
case "/permissions": {
|
|
572
|
+
const mode = runtime.agent.getPermissionMode();
|
|
573
|
+
const policyRows = mode === "safe"
|
|
574
|
+
? [
|
|
575
|
+
["Writes", UI.danger("disabled")],
|
|
576
|
+
["Shell", UI.danger("disabled")],
|
|
577
|
+
["Approvals", UI.success("n/a")],
|
|
578
|
+
]
|
|
579
|
+
: mode === "standard"
|
|
580
|
+
? [
|
|
581
|
+
["Writes", UI.warning("requires confirmation")],
|
|
582
|
+
["Shell", UI.warning("requires confirmation")],
|
|
583
|
+
["Approvals", UI.info("interactive")],
|
|
584
|
+
]
|
|
585
|
+
: [
|
|
586
|
+
["Writes", UI.success("enabled")],
|
|
587
|
+
["Shell", UI.success("enabled")],
|
|
588
|
+
["Approvals", UI.warning("auto-approved")],
|
|
589
|
+
];
|
|
590
|
+
console.log("");
|
|
591
|
+
console.log(UI.accent.bold(`${ICON.shield} Permission Mode`));
|
|
592
|
+
console.log(printTable(["Setting", "Value"], policyRows, [18, 28]));
|
|
593
|
+
console.log(UI.muted(`Current mode: ${mode}`));
|
|
594
|
+
console.log("");
|
|
366
595
|
return { handled: true, shouldExit: false };
|
|
596
|
+
}
|
|
367
597
|
case "/verbose": {
|
|
368
598
|
const next = !runtime.agent.isVerboseTools();
|
|
369
599
|
runtime.agent.setVerboseTools(next);
|
|
370
|
-
console.log(
|
|
600
|
+
console.log(next
|
|
601
|
+
? UI.success("Verbose tool output: ON")
|
|
602
|
+
: UI.muted("Verbose tool output: OFF"));
|
|
371
603
|
await persistSnapshot(runtime);
|
|
372
604
|
return { handled: true, shouldExit: false };
|
|
373
605
|
}
|
|
374
606
|
case "/cost": {
|
|
375
607
|
const summary = runtime.context.getCostSummary();
|
|
376
|
-
|
|
608
|
+
const rows = [
|
|
609
|
+
["Input tokens", summary.inputTokens.toLocaleString()],
|
|
610
|
+
["Output tokens", summary.outputTokens.toLocaleString()],
|
|
611
|
+
["Total tokens", summary.totalTokens.toLocaleString()],
|
|
612
|
+
["Estimated cost", formatUsd(summary.totalCostUsd)],
|
|
613
|
+
];
|
|
614
|
+
console.log("");
|
|
615
|
+
console.log(UI.accent.bold(`${ICON.token} Session Cost`));
|
|
616
|
+
console.log(printTable(["Metric", "Value"], rows, [20, 22]));
|
|
617
|
+
console.log("");
|
|
377
618
|
return { handled: true, shouldExit: false };
|
|
378
619
|
}
|
|
379
620
|
case "/compact": {
|
|
@@ -384,18 +625,21 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
384
625
|
}
|
|
385
626
|
case "/models": {
|
|
386
627
|
const models = await runtime.agent.listModels();
|
|
628
|
+
console.log("");
|
|
629
|
+
console.log(UI.accent.bold(`${ICON.model} Models (${models.length})`));
|
|
387
630
|
console.log(formatModels(models));
|
|
631
|
+
console.log("");
|
|
388
632
|
return { handled: true, shouldExit: false };
|
|
389
633
|
}
|
|
390
634
|
case "/undo": {
|
|
391
635
|
const undone = runtime.agent.undoLastTurn();
|
|
392
636
|
if (undone) {
|
|
393
637
|
setLastRawPrompt(undone);
|
|
394
|
-
console.log("Removed last user+assistant turn.");
|
|
638
|
+
console.log(UI.success("Removed last user+assistant turn."));
|
|
395
639
|
await persistSnapshot(runtime);
|
|
396
640
|
}
|
|
397
641
|
else {
|
|
398
|
-
console.log("Nothing to undo.");
|
|
642
|
+
console.log(UI.muted("Nothing to undo."));
|
|
399
643
|
}
|
|
400
644
|
return { handled: true, shouldExit: false };
|
|
401
645
|
}
|
|
@@ -404,17 +648,21 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
404
648
|
case "/save": {
|
|
405
649
|
const name = argText || `session-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
406
650
|
const location = await runtime.sessions.saveNamed(name, runtime.agent.exportState(runtime.context.serializeState()));
|
|
407
|
-
console.log(`Saved session
|
|
651
|
+
console.log(UI.success(`Saved session "${name}"`));
|
|
652
|
+
console.log(UI.muted(`Path: ${location}`));
|
|
408
653
|
return { handled: true, shouldExit: false };
|
|
409
654
|
}
|
|
410
655
|
case "/load": {
|
|
411
656
|
if (!argText) {
|
|
412
657
|
const names = await runtime.sessions.listNamed();
|
|
413
658
|
if (names.length === 0) {
|
|
414
|
-
console.log("No named sessions found.");
|
|
659
|
+
console.log(UI.muted("No named sessions found."));
|
|
415
660
|
}
|
|
416
661
|
else {
|
|
417
|
-
console.log(
|
|
662
|
+
console.log("");
|
|
663
|
+
console.log(UI.accent.bold(`Named Sessions (${names.length})`));
|
|
664
|
+
console.log(names.map((name) => ` ${UI.info(name)}`).join("\n"));
|
|
665
|
+
console.log("");
|
|
418
666
|
}
|
|
419
667
|
return { handled: true, shouldExit: false };
|
|
420
668
|
}
|
|
@@ -425,7 +673,7 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
425
673
|
}
|
|
426
674
|
await restoreSnapshot(runtime, loaded);
|
|
427
675
|
setLastRawPrompt(loaded.lastUserInput);
|
|
428
|
-
console.log(`Loaded session
|
|
676
|
+
console.log(UI.success(`Loaded session "${argText}".`));
|
|
429
677
|
await persistSnapshot(runtime);
|
|
430
678
|
return { handled: true, shouldExit: false };
|
|
431
679
|
}
|
|
@@ -441,7 +689,7 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
441
689
|
return { handled: true, shouldExit: false };
|
|
442
690
|
}
|
|
443
691
|
process.chdir(path.resolve(resolved));
|
|
444
|
-
console.log(`Working directory: ${process.cwd()}`);
|
|
692
|
+
console.log(UI.success(`Working directory: ${process.cwd()}`));
|
|
445
693
|
await persistSnapshot(runtime);
|
|
446
694
|
return { handled: true, shouldExit: false };
|
|
447
695
|
}
|
|
@@ -451,11 +699,14 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
451
699
|
try {
|
|
452
700
|
const result = await run_linter({ path: targetPath, linter: "auto", fix: false });
|
|
453
701
|
spinner.stop();
|
|
702
|
+
console.log("");
|
|
703
|
+
console.log(UI.accent.bold(`Lint Results — ${targetPath}`));
|
|
454
704
|
console.log(result);
|
|
705
|
+
console.log("");
|
|
455
706
|
}
|
|
456
707
|
catch (err) {
|
|
457
708
|
spinner.stop();
|
|
458
|
-
console.log(
|
|
709
|
+
console.log(UI.danger(`Linter error: ${err instanceof Error ? err.message : String(err)}`));
|
|
459
710
|
}
|
|
460
711
|
return { handled: true, shouldExit: false };
|
|
461
712
|
}
|
|
@@ -465,11 +716,14 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
465
716
|
try {
|
|
466
717
|
const result = await run_tests({ path: targetPath, runner: "auto", bail: false });
|
|
467
718
|
spinner.stop();
|
|
719
|
+
console.log("");
|
|
720
|
+
console.log(UI.accent.bold(`Test Results${targetPath ? ` — ${targetPath}` : ""}`));
|
|
468
721
|
console.log(result);
|
|
722
|
+
console.log("");
|
|
469
723
|
}
|
|
470
724
|
catch (err) {
|
|
471
725
|
spinner.stop();
|
|
472
|
-
console.log(
|
|
726
|
+
console.log(UI.danger(`Test runner error: ${err instanceof Error ? err.message : String(err)}`));
|
|
473
727
|
}
|
|
474
728
|
return { handled: true, shouldExit: false };
|
|
475
729
|
}
|
|
@@ -479,11 +733,14 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
479
733
|
try {
|
|
480
734
|
const result = await security_scan({ path: targetPath, include_low_severity: false });
|
|
481
735
|
spinner.stop();
|
|
736
|
+
console.log("");
|
|
737
|
+
console.log(UI.accent.bold(`Security Scan — ${targetPath}`));
|
|
482
738
|
console.log(result);
|
|
739
|
+
console.log("");
|
|
483
740
|
}
|
|
484
741
|
catch (err) {
|
|
485
742
|
spinner.stop();
|
|
486
|
-
console.log(
|
|
743
|
+
console.log(UI.danger(`Scan error: ${err instanceof Error ? err.message : String(err)}`));
|
|
487
744
|
}
|
|
488
745
|
return { handled: true, shouldExit: false };
|
|
489
746
|
}
|
|
@@ -493,7 +750,10 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
493
750
|
// /memory clear --global — clear global memory
|
|
494
751
|
if (!argText) {
|
|
495
752
|
const output = await showMemory(runtime.workspaceRoot);
|
|
753
|
+
console.log("");
|
|
754
|
+
console.log(UI.accent.bold("Memory"));
|
|
496
755
|
console.log(output);
|
|
756
|
+
console.log("");
|
|
497
757
|
return { handled: true, shouldExit: false };
|
|
498
758
|
}
|
|
499
759
|
const memArgs = argText.split(/\s+/);
|
|
@@ -502,17 +762,17 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
502
762
|
await clearMemory(scope, runtime.workspaceRoot);
|
|
503
763
|
const fresh = await loadMemory(runtime.workspaceRoot);
|
|
504
764
|
runtime.agent.updateMemory(fresh);
|
|
505
|
-
console.log(`${scope === "global" ? "Global" : "Project"} memory cleared.`);
|
|
765
|
+
console.log(UI.success(`${scope === "global" ? "Global" : "Project"} memory cleared.`));
|
|
506
766
|
return { handled: true, shouldExit: false };
|
|
507
767
|
}
|
|
508
|
-
console.log(`Unknown /memory subcommand: ${argText}`);
|
|
768
|
+
console.log(UI.warning(`Unknown /memory subcommand: ${argText}`));
|
|
509
769
|
return { handled: true, shouldExit: false };
|
|
510
770
|
}
|
|
511
771
|
case "/remember": {
|
|
512
772
|
// /remember <text> — append to project memory
|
|
513
773
|
// /remember --global <text> — append to global memory
|
|
514
774
|
if (!argText) {
|
|
515
|
-
console.log("Usage: /remember <text> or /remember --global <text>");
|
|
775
|
+
console.log(UI.warning("Usage: /remember <text> or /remember --global <text>"));
|
|
516
776
|
return { handled: true, shouldExit: false };
|
|
517
777
|
}
|
|
518
778
|
let scope = "project";
|
|
@@ -522,14 +782,43 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
522
782
|
fact = fact.slice("--global".length).trim();
|
|
523
783
|
}
|
|
524
784
|
if (!fact) {
|
|
525
|
-
console.log("Nothing to remember — provide text after the flag.");
|
|
785
|
+
console.log(UI.warning("Nothing to remember — provide text after the flag."));
|
|
526
786
|
return { handled: true, shouldExit: false };
|
|
527
787
|
}
|
|
528
788
|
const savedPath = await appendMemory(fact, scope, runtime.workspaceRoot);
|
|
529
789
|
const fresh = await loadMemory(runtime.workspaceRoot);
|
|
530
790
|
runtime.agent.updateMemory(fresh);
|
|
531
|
-
console.log(
|
|
532
|
-
console.log(
|
|
791
|
+
console.log(UI.success(`Remembered (${scope}): ${fact}`));
|
|
792
|
+
console.log(UI.muted(`Path: ${savedPath}`));
|
|
793
|
+
return { handled: true, shouldExit: false };
|
|
794
|
+
}
|
|
795
|
+
case "/logout": {
|
|
796
|
+
try {
|
|
797
|
+
await runLogout();
|
|
798
|
+
console.log(UI.success("Logged out. Run /login or restart to switch accounts."));
|
|
799
|
+
}
|
|
800
|
+
catch (err) {
|
|
801
|
+
console.log(UI.danger(`Logout error: ${err instanceof Error ? err.message : String(err)}`));
|
|
802
|
+
}
|
|
803
|
+
return { handled: true, shouldExit: false };
|
|
804
|
+
}
|
|
805
|
+
case "/login": {
|
|
806
|
+
const routerUrl = process.env.LLM_ROUTER_BASE_URL || "";
|
|
807
|
+
try {
|
|
808
|
+
await runLogin({ routerUrl });
|
|
809
|
+
// Reload credentials into env so the agent uses the new account immediately
|
|
810
|
+
const conn = await resolveConnectionFromEnvOrCreds();
|
|
811
|
+
if (conn) {
|
|
812
|
+
console.log(UI.success("Logged in. New credentials active for this session."));
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
catch (err) {
|
|
816
|
+
console.log(UI.danger(`Login error: ${err instanceof Error ? err.message : String(err)}`));
|
|
817
|
+
}
|
|
818
|
+
return { handled: true, shouldExit: false };
|
|
819
|
+
}
|
|
820
|
+
case "/whoami": {
|
|
821
|
+
await runWhoami();
|
|
533
822
|
return { handled: true, shouldExit: false };
|
|
534
823
|
}
|
|
535
824
|
case "/review": {
|
|
@@ -554,7 +843,7 @@ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
|
|
|
554
843
|
}
|
|
555
844
|
catch (err) {
|
|
556
845
|
if (!(err instanceof Error && err.name === "TurnCancelledError")) {
|
|
557
|
-
console.log(
|
|
846
|
+
console.log(UI.danger(`Review error: ${err instanceof Error ? err.message : String(err)}`));
|
|
558
847
|
}
|
|
559
848
|
}
|
|
560
849
|
return { handled: true, shouldExit: false };
|
|
@@ -575,10 +864,16 @@ async function runNonInteractive(runtime, options) {
|
|
|
575
864
|
await executeTurn(runtime, promptText, controller.signal, options.outputFormat, false);
|
|
576
865
|
}
|
|
577
866
|
async function runInteractive(runtime, options) {
|
|
867
|
+
_isInteractiveSession = true;
|
|
578
868
|
printBanner(runtime, options.permissions);
|
|
579
869
|
const health = await checkRouterHealth();
|
|
580
870
|
if (!health.ok) {
|
|
581
|
-
console.log(
|
|
871
|
+
console.log(UI.warning(`${ICON.status} connection warning`));
|
|
872
|
+
console.log(UI.warning(` ${health.message} — check LLM_ROUTER_BASE_URL.`));
|
|
873
|
+
console.log();
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
console.log(UI.success(`${ICON.status} router connection healthy.`));
|
|
582
877
|
console.log();
|
|
583
878
|
}
|
|
584
879
|
const replInput = new ReplInput(runtime.paths.historyFile, runtime.config.historyLimit);
|
|
@@ -602,7 +897,25 @@ async function runInteractive(runtime, options) {
|
|
|
602
897
|
});
|
|
603
898
|
});
|
|
604
899
|
while (true) {
|
|
605
|
-
|
|
900
|
+
// ora spinners write ANSI codes to stdout while readline manages the same
|
|
901
|
+
// stream, which can leave the readline interface in a closed/corrupt state
|
|
902
|
+
// (especially on Node.js 22+). Always reset after a turn so the next
|
|
903
|
+
// prompt() call starts with a fresh, healthy interface.
|
|
904
|
+
let userInput;
|
|
905
|
+
try {
|
|
906
|
+
userInput = await replInput.prompt();
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
try {
|
|
910
|
+
await replInput.resetInterface();
|
|
911
|
+
userInput = await replInput.prompt();
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
// Truly unrecoverable (e.g. stdin closed / Ctrl-D); exit gracefully.
|
|
915
|
+
console.log(chalk.yellow("\nSession ended."));
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
606
919
|
const trimmed = userInput.trim();
|
|
607
920
|
if (!trimmed)
|
|
608
921
|
continue;
|
|
@@ -617,18 +930,19 @@ async function runInteractive(runtime, options) {
|
|
|
617
930
|
}
|
|
618
931
|
if (trimmed === "/retry") {
|
|
619
932
|
if (!lastRawPrompt) {
|
|
620
|
-
console.log("No previous prompt to retry.");
|
|
933
|
+
console.log(UI.warning("No previous prompt to retry."));
|
|
621
934
|
continue;
|
|
622
935
|
}
|
|
623
936
|
}
|
|
624
937
|
else {
|
|
625
|
-
console.log(`Unknown command: ${trimmed}`);
|
|
938
|
+
console.log(UI.warning(`Unknown command: ${trimmed}`));
|
|
939
|
+
console.log(UI.muted(`${ICON.tip} Run /help to view available commands.`));
|
|
626
940
|
continue;
|
|
627
941
|
}
|
|
628
942
|
}
|
|
629
943
|
const rawPrompt = trimmed === "/retry" ? lastRawPrompt ?? "" : userInput;
|
|
630
944
|
if (!rawPrompt.trim()) {
|
|
631
|
-
console.log("No previous prompt to retry.");
|
|
945
|
+
console.log(UI.warning("No previous prompt to retry."));
|
|
632
946
|
continue;
|
|
633
947
|
}
|
|
634
948
|
lastRawPrompt = rawPrompt;
|
|
@@ -642,31 +956,38 @@ async function runInteractive(runtime, options) {
|
|
|
642
956
|
}
|
|
643
957
|
else {
|
|
644
958
|
const message = err instanceof Error ? err.message : String(err);
|
|
645
|
-
console.log(
|
|
959
|
+
console.log(UI.danger(`Error: ${message}`));
|
|
646
960
|
}
|
|
647
961
|
}
|
|
648
962
|
finally {
|
|
649
963
|
activeTurnController = null;
|
|
650
964
|
}
|
|
651
965
|
console.log();
|
|
966
|
+
// Reset readline interface after every turn so ora spinner side-effects
|
|
967
|
+
// don't accumulate and corrupt the next prompt() call.
|
|
968
|
+
await replInput.resetInterface();
|
|
652
969
|
}
|
|
653
970
|
await replInput.close();
|
|
654
971
|
}
|
|
655
972
|
// ── First-run setup wizard ────────────────────────────────────────────────────
|
|
656
973
|
async function runFirstTimeSetup() {
|
|
657
|
-
const {
|
|
658
|
-
console.log(
|
|
659
|
-
console.log(
|
|
974
|
+
const { input, select } = await import("@inquirer/prompts");
|
|
975
|
+
console.log("");
|
|
976
|
+
console.log(renderPanel([
|
|
977
|
+
` ${UI.title(`${ICON.spark} Welcome to LLM-Router CLI Agent`)}`,
|
|
978
|
+
` ${UI.muted("No connection found. Let's configure authentication.")}`,
|
|
979
|
+
]));
|
|
980
|
+
console.log("");
|
|
660
981
|
const routerUrl = await input({
|
|
661
982
|
message: "Router URL:",
|
|
662
|
-
default: "https://
|
|
983
|
+
default: "https://llm-router-6o4u74bhpq-uc.a.run.app",
|
|
663
984
|
validate: (v) => {
|
|
664
985
|
try {
|
|
665
986
|
new URL(v);
|
|
666
987
|
return true;
|
|
667
988
|
}
|
|
668
989
|
catch {
|
|
669
|
-
return "Enter a valid URL (e.g. https://
|
|
990
|
+
return "Enter a valid URL (e.g. https://llm-router-6o4u74bhpq-uc.a.run.app)";
|
|
670
991
|
}
|
|
671
992
|
},
|
|
672
993
|
});
|
|
@@ -695,7 +1016,7 @@ async function runFirstTimeSetup() {
|
|
|
695
1016
|
});
|
|
696
1017
|
process.env.LLM_ROUTER_BASE_URL = routerUrl;
|
|
697
1018
|
process.env.LLM_ROUTER_API_KEY = apiKey;
|
|
698
|
-
console.log(
|
|
1019
|
+
console.log(UI.success(`\n${ICON.status} API key saved.\n`));
|
|
699
1020
|
}
|
|
700
1021
|
return true;
|
|
701
1022
|
}
|
|
@@ -761,7 +1082,14 @@ program
|
|
|
761
1082
|
await persistSnapshot(runtime);
|
|
762
1083
|
const nonInteractive = Boolean(options.prompt) || !processStdin.isTTY;
|
|
763
1084
|
if (nonInteractive) {
|
|
764
|
-
|
|
1085
|
+
try {
|
|
1086
|
+
await runNonInteractive(runtime, options);
|
|
1087
|
+
}
|
|
1088
|
+
catch (err) {
|
|
1089
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1090
|
+
console.error(chalk.red(`\nError: ${msg}`));
|
|
1091
|
+
process.exit(1);
|
|
1092
|
+
}
|
|
765
1093
|
return;
|
|
766
1094
|
}
|
|
767
1095
|
await runInteractive(runtime, options);
|
|
@@ -781,7 +1109,7 @@ program
|
|
|
781
1109
|
const { input } = await import("@inquirer/prompts");
|
|
782
1110
|
routerUrl = await input({
|
|
783
1111
|
message: "Router URL:",
|
|
784
|
-
default: "https://
|
|
1112
|
+
default: "https://llm-router-6o4u74bhpq-uc.a.run.app",
|
|
785
1113
|
validate: (v) => {
|
|
786
1114
|
try {
|
|
787
1115
|
new URL(v);
|
|
@@ -897,6 +1225,21 @@ program
|
|
|
897
1225
|
console.log();
|
|
898
1226
|
}
|
|
899
1227
|
});
|
|
1228
|
+
// Safety net: catch any unhandled promise rejections so they display a useful
|
|
1229
|
+
// message instead of crashing the process silently (Node 22 terminates on them).
|
|
1230
|
+
// In interactive mode we log but do NOT exit — the REPL should survive stray
|
|
1231
|
+
// async errors (e.g. failed snapshot writes, readline hiccups).
|
|
1232
|
+
let _isInteractiveSession = false;
|
|
1233
|
+
process.on("unhandledRejection", (reason) => {
|
|
1234
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
1235
|
+
if (_isInteractiveSession) {
|
|
1236
|
+
console.error(chalk.red(`\nAsync error (non-fatal): ${msg}`));
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
console.error(chalk.red(`\nUnhandled error: ${msg}`));
|
|
1240
|
+
process.exit(1);
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
900
1243
|
program.parseAsync().catch((err) => {
|
|
901
1244
|
const message = err instanceof Error ? err.message : String(err);
|
|
902
1245
|
console.error(chalk.red(message));
|