llm-agent-cli 2.0.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.
Files changed (89) hide show
  1. package/dist/agent.d.ts +112 -0
  2. package/dist/agent.d.ts.map +1 -0
  3. package/dist/agent.js +730 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/audit.d.ts +24 -0
  6. package/dist/audit.d.ts.map +1 -0
  7. package/dist/audit.js +94 -0
  8. package/dist/audit.js.map +1 -0
  9. package/dist/auth.d.ts +36 -0
  10. package/dist/auth.d.ts.map +1 -0
  11. package/dist/auth.js +236 -0
  12. package/dist/auth.js.map +1 -0
  13. package/dist/config.d.ts +35 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +92 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/context.d.ts +48 -0
  18. package/dist/context.d.ts.map +1 -0
  19. package/dist/context.js +94 -0
  20. package/dist/context.js.map +1 -0
  21. package/dist/diff.d.ts +27 -0
  22. package/dist/diff.d.ts.map +1 -0
  23. package/dist/diff.js +174 -0
  24. package/dist/diff.js.map +1 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +905 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/input.d.ts +13 -0
  30. package/dist/input.d.ts.map +1 -0
  31. package/dist/input.js +88 -0
  32. package/dist/input.js.map +1 -0
  33. package/dist/memory.d.ts +32 -0
  34. package/dist/memory.d.ts.map +1 -0
  35. package/dist/memory.js +103 -0
  36. package/dist/memory.js.map +1 -0
  37. package/dist/preprocessor.d.ts +12 -0
  38. package/dist/preprocessor.d.ts.map +1 -0
  39. package/dist/preprocessor.js +138 -0
  40. package/dist/preprocessor.js.map +1 -0
  41. package/dist/project.d.ts +10 -0
  42. package/dist/project.d.ts.map +1 -0
  43. package/dist/project.js +145 -0
  44. package/dist/project.js.map +1 -0
  45. package/dist/renderer.d.ts +2 -0
  46. package/dist/renderer.d.ts.map +1 -0
  47. package/dist/renderer.js +31 -0
  48. package/dist/renderer.js.map +1 -0
  49. package/dist/session.d.ts +36 -0
  50. package/dist/session.d.ts.map +1 -0
  51. package/dist/session.js +78 -0
  52. package/dist/session.js.map +1 -0
  53. package/dist/tools/filesystem.d.ts +138 -0
  54. package/dist/tools/filesystem.d.ts.map +1 -0
  55. package/dist/tools/filesystem.js +539 -0
  56. package/dist/tools/filesystem.js.map +1 -0
  57. package/dist/tools/git.d.ts +60 -0
  58. package/dist/tools/git.d.ts.map +1 -0
  59. package/dist/tools/git.js +188 -0
  60. package/dist/tools/git.js.map +1 -0
  61. package/dist/tools/index.d.ts +386 -0
  62. package/dist/tools/index.d.ts.map +1 -0
  63. package/dist/tools/index.js +142 -0
  64. package/dist/tools/index.js.map +1 -0
  65. package/dist/tools/linter.d.ts +44 -0
  66. package/dist/tools/linter.d.ts.map +1 -0
  67. package/dist/tools/linter.js +426 -0
  68. package/dist/tools/linter.js.map +1 -0
  69. package/dist/tools/network.d.ts +17 -0
  70. package/dist/tools/network.d.ts.map +1 -0
  71. package/dist/tools/network.js +121 -0
  72. package/dist/tools/network.js.map +1 -0
  73. package/dist/tools/search.d.ts +33 -0
  74. package/dist/tools/search.d.ts.map +1 -0
  75. package/dist/tools/search.js +263 -0
  76. package/dist/tools/search.js.map +1 -0
  77. package/dist/tools/security.d.ts +18 -0
  78. package/dist/tools/security.d.ts.map +1 -0
  79. package/dist/tools/security.js +242 -0
  80. package/dist/tools/security.js.map +1 -0
  81. package/dist/tools/shell.d.ts +14 -0
  82. package/dist/tools/shell.d.ts.map +1 -0
  83. package/dist/tools/shell.js +68 -0
  84. package/dist/tools/shell.js.map +1 -0
  85. package/dist/tools/types.d.ts +5 -0
  86. package/dist/tools/types.d.ts.map +1 -0
  87. package/dist/tools/types.js +2 -0
  88. package/dist/tools/types.js.map +1 -0
  89. package/package.json +59 -0
package/dist/index.js ADDED
@@ -0,0 +1,905 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import fs from "fs/promises";
4
+ import path from "path";
5
+ import { stdin as processStdin, stdout as processStdout } from "process";
6
+ import { Command } from "commander";
7
+ import chalk from "chalk";
8
+ import ora from "ora";
9
+ import { Agent, TurnCancelledError, checkRouterHealth, } from "./agent.js";
10
+ import { AuditLogger } from "./audit.js";
11
+ import { getAgentPaths, loadConfig, ensureAgentDirectories, } from "./config.js";
12
+ import { ContextManager } from "./context.js";
13
+ import { ReplInput } from "./input.js";
14
+ import { preprocessPrompt } from "./preprocessor.js";
15
+ import { detectProjectContext } from "./project.js";
16
+ import { renderMarkdown } from "./renderer.js";
17
+ import { SessionStore } from "./session.js";
18
+ import { listAvailableTools } from "./tools/index.js";
19
+ import { resolveProtectedPath, getRuntimeConfig } from "./tools/security.js";
20
+ import { run_linter, run_tests, security_scan } from "./tools/linter.js";
21
+ import { loadCredentials, resolveConnectionFromEnvOrCreds, runLogin, runLogout, runWhoami, } from "./auth.js";
22
+ import { loadMemory, appendMemory, clearMemory, showMemory, } from "./memory.js";
23
+ const PERMISSION_MODES = ["safe", "standard", "yolo"];
24
+ function isPermissionMode(value) {
25
+ return PERMISSION_MODES.includes(value);
26
+ }
27
+ function isOutputFormat(value) {
28
+ return value === "text" || value === "json";
29
+ }
30
+ function formatUsd(value) {
31
+ return `$${value.toFixed(4)}`;
32
+ }
33
+ function looksLikeMarkdown(text) {
34
+ return /(^\s{0,3}#{1,6}\s|\*\*|`[^`]+`|^\s*[-*+]\s|\|.+\|)/m.test(text);
35
+ }
36
+ function printBanner(runtime, permissionMode) {
37
+ const model = process.env.LLM_ROUTER_MODEL ?? "auto";
38
+ const baseUrl = process.env.LLM_ROUTER_BASE_URL ?? "(not set)";
39
+ console.log(chalk.bold.cyan(`\n ╔══════════════════════════════════════╗\n ║ LLM-Router CLI Agent ║\n ║ Powered by your local LLM-Router ║\n ╚══════════════════════════════════════╝`));
40
+ console.log(chalk.dim(` Router: ${baseUrl} | Model: ${model}`));
41
+ console.log(chalk.dim(` Session: ${runtime.sessionId}`));
42
+ console.log(chalk.dim(` Workspace: ${runtime.project.workspaceRoot}`));
43
+ console.log(chalk.dim(` Permissions: ${permissionMode}`));
44
+ console.log(chalk.dim(` Type /help for commands`));
45
+ console.log();
46
+ }
47
+ function printHelp() {
48
+ console.log(chalk.bold("\nSlash commands:"));
49
+ console.log(chalk.cyan(" /help") + " — show commands");
50
+ console.log(chalk.cyan(" /exit") + " — quit");
51
+ console.log(chalk.cyan(" /clear") + " — clear terminal");
52
+ console.log(chalk.cyan(" /compact") + " — summarize and compact conversation");
53
+ console.log(chalk.cyan(" /models") + " — list router models");
54
+ console.log(chalk.cyan(" /cost") + " — show token/cost totals");
55
+ console.log(chalk.cyan(" /save [name]") + " — save named session snapshot");
56
+ console.log(chalk.cyan(" /load [name]") + " — load named session or list names");
57
+ console.log(chalk.cyan(" /retry") + " — resend last user prompt");
58
+ console.log(chalk.cyan(" /undo") + " — remove last user+assistant turn");
59
+ console.log(chalk.cyan(" /cd <path>") + " — change working directory (within workspace)");
60
+ console.log(chalk.cyan(" /tools") + " — list tools");
61
+ console.log(chalk.cyan(" /permissions") + " — show permission mode");
62
+ console.log(chalk.cyan(" /verbose") + " — toggle verbose tool output");
63
+ console.log(chalk.cyan(" /review [path]") + " — code review a file or directory");
64
+ console.log(chalk.cyan(" /scan [path]") + " — security scan (secrets, XSS, SQLi, etc.)");
65
+ console.log(chalk.cyan(" /lint [path]") + " — run project linter");
66
+ console.log(chalk.cyan(" /test [path]") + " — run test suite");
67
+ console.log(chalk.cyan(" /memory") + " — show persistent memory (global + project)");
68
+ console.log(chalk.cyan(" /remember <text>") + " — append a fact to project memory");
69
+ console.log(chalk.cyan(" /remember --global <text>") + " — append to global memory");
70
+ console.log(chalk.cyan(" /memory clear") + " — clear project memory");
71
+ console.log(chalk.cyan(" /memory clear --global") + " — clear global memory");
72
+ console.log();
73
+ console.log(chalk.bold("Multiline input:"));
74
+ console.log(chalk.dim(" End a line with \\ to continue onto next line."));
75
+ console.log(chalk.dim(" Type <<< then paste lines; end with a single . line."));
76
+ console.log();
77
+ }
78
+ function printTools() {
79
+ console.log(chalk.bold("\nAvailable tools:"));
80
+ for (const tool of listAvailableTools()) {
81
+ console.log(` ${chalk.yellow(tool.name)} — ${tool.description}`);
82
+ }
83
+ console.log();
84
+ }
85
+ function modelFlag(model, key) {
86
+ const direct = model[key];
87
+ if (typeof direct === "boolean")
88
+ return direct;
89
+ const snake = key
90
+ .replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
91
+ .toLowerCase();
92
+ const snakeValue = model[snake];
93
+ return typeof snakeValue === "boolean" ? snakeValue : false;
94
+ }
95
+ function extractModelCapabilities(model) {
96
+ const explicit = model.capabilities;
97
+ if (Array.isArray(explicit)) {
98
+ return explicit.map(String).slice(0, 6).join(",");
99
+ }
100
+ const derived = [];
101
+ const mapping = [
102
+ ["supportsFunctionCalling", "function_calling"],
103
+ ["supportsStreaming", "streaming"],
104
+ ["supportsJsonMode", "json_mode"],
105
+ ["supportsVision", "vision"],
106
+ ["supportsThinking", "thinking"],
107
+ ["supportsImageGen", "image_gen"],
108
+ ["supportsAudio", "audio"],
109
+ ["supportsVideo", "video"],
110
+ ["supportsEmbeddings", "embeddings"],
111
+ ];
112
+ for (const [field, label] of mapping) {
113
+ if (modelFlag(model, field)) {
114
+ derived.push(label);
115
+ }
116
+ }
117
+ return derived.slice(0, 6).join(",");
118
+ }
119
+ function formatModels(models) {
120
+ const rows = models.map((model) => ({
121
+ id: String(model.id ?? ""),
122
+ tier: String(model.tier ?? ""),
123
+ owner: String(model.owned_by ?? ""),
124
+ caps: extractModelCapabilities(model),
125
+ }));
126
+ const headers = ["ID", "TIER", "OWNER", "CAPABILITIES"];
127
+ const widths = [40, 10, 12, 45];
128
+ const line = (values) => values
129
+ .map((value, idx) => {
130
+ const width = widths[idx] ?? 20;
131
+ if (value.length <= width)
132
+ return value.padEnd(width, " ");
133
+ return `${value.slice(0, Math.max(0, width - 1))}…`;
134
+ })
135
+ .join(" ");
136
+ const divider = widths.map((w) => "-".repeat(w)).join(" ");
137
+ const body = rows.map((row) => line([row.id, row.tier, row.owner, row.caps]));
138
+ return [line(headers), divider, ...body].join("\n");
139
+ }
140
+ async function readAllStdin() {
141
+ const chunks = [];
142
+ for await (const chunk of processStdin) {
143
+ chunks.push(Buffer.from(chunk));
144
+ }
145
+ return Buffer.concat(chunks).toString("utf-8");
146
+ }
147
+ async function restoreSnapshot(runtime, snapshot) {
148
+ runtime.agent.importState(snapshot);
149
+ runtime.context.loadState(snapshot.contextState);
150
+ if (snapshot.cwd) {
151
+ try {
152
+ const resolved = await resolveProtectedPath(snapshot.cwd);
153
+ process.chdir(resolved);
154
+ }
155
+ catch {
156
+ // keep current cwd if saved path is invalid now
157
+ }
158
+ }
159
+ }
160
+ async function bootstrapRuntime(options) {
161
+ const project = await detectProjectContext(process.cwd());
162
+ const paths = getAgentPaths();
163
+ await ensureAgentDirectories(paths);
164
+ const config = await loadConfig(paths);
165
+ const audit = new AuditLogger(paths);
166
+ const context = new ContextManager(config);
167
+ const sessions = new SessionStore(paths);
168
+ const memory = await loadMemory(project.workspaceRoot);
169
+ const agent = new Agent({
170
+ workspaceRoot: project.workspaceRoot,
171
+ permissionMode: options.permissions,
172
+ maxReadBytes: config.maxReadBytes,
173
+ maxToolResultChars: config.maxToolResultChars,
174
+ maxCommandOutputChars: config.maxCommandOutputChars,
175
+ shellTimeoutMs: config.shellTimeoutMs,
176
+ systemContext: project.summary,
177
+ auditLogger: audit,
178
+ contextWindowTokens: config.contextWindowTokens,
179
+ memory,
180
+ });
181
+ let sessionId = options.resume ?? sessions.createSessionId();
182
+ if (options.resume) {
183
+ const resumed = await sessions.loadLatestSnapshotById(options.resume);
184
+ if (resumed) {
185
+ await restoreSnapshot({
186
+ agent,
187
+ config,
188
+ paths,
189
+ project,
190
+ audit,
191
+ context,
192
+ sessions,
193
+ sessionId,
194
+ workspaceRoot: project.workspaceRoot,
195
+ }, resumed);
196
+ }
197
+ }
198
+ return {
199
+ agent,
200
+ config,
201
+ paths,
202
+ project,
203
+ audit,
204
+ context,
205
+ sessions,
206
+ sessionId,
207
+ workspaceRoot: project.workspaceRoot,
208
+ };
209
+ }
210
+ async function persistSnapshot(runtime) {
211
+ await runtime.sessions.appendSnapshot(runtime.sessionId, runtime.agent.exportState(runtime.context.serializeState()));
212
+ }
213
+ async function executeTurn(runtime, rawPrompt, signal, outputFormat, streamOutput) {
214
+ const preprocessed = await preprocessPrompt(rawPrompt, Math.min(runtime.config.maxReadBytes, 16_000));
215
+ if (preprocessed.includedFiles.length > 0) {
216
+ const files = preprocessed.includedFiles.map((file) => file.path).join(", ");
217
+ if (streamOutput) {
218
+ console.log(chalk.dim(`Including files in prompt: ${files}`));
219
+ }
220
+ else {
221
+ process.stderr.write(`info: including files in prompt: ${files}\n`);
222
+ }
223
+ }
224
+ for (const warning of preprocessed.warnings) {
225
+ if (streamOutput) {
226
+ console.log(chalk.yellow(`Warning: ${warning}`));
227
+ }
228
+ else {
229
+ // In non-interactive / JSON mode route to stderr so stdout stays clean
230
+ // for piping: llm-agent --prompt "..." --output-format json | jq '.'
231
+ process.stderr.write(`warning: ${warning}\n`);
232
+ }
233
+ }
234
+ const spinner = ora({ text: "Thinking...", color: "cyan" }).start();
235
+ let printedToken = false;
236
+ let printedAgentPrefix = false;
237
+ let toolTimer = null;
238
+ let toolStartAt = 0;
239
+ let currentToolLabel = "";
240
+ const clearToolTimer = () => {
241
+ if (toolTimer) {
242
+ clearInterval(toolTimer);
243
+ toolTimer = null;
244
+ }
245
+ };
246
+ const callbacks = {
247
+ onToken: async (token) => {
248
+ if (!streamOutput)
249
+ return;
250
+ if (spinner.isSpinning)
251
+ spinner.stop();
252
+ clearToolTimer();
253
+ if (!printedAgentPrefix) {
254
+ processStdout.write(chalk.bold.white("Agent: "));
255
+ printedAgentPrefix = true;
256
+ }
257
+ printedToken = true;
258
+ processStdout.write(token);
259
+ },
260
+ onRetry: async (attempt, delayMs) => {
261
+ spinner.text = `Retry ${attempt}/3 in ${Math.floor(delayMs / 1000)}s...`;
262
+ },
263
+ onToolStart: async (event) => {
264
+ if (!streamOutput)
265
+ return;
266
+ clearToolTimer();
267
+ toolStartAt = Date.now();
268
+ currentToolLabel = `${event.toolName} ${event.argsPreview}`;
269
+ spinner.start(`> ${currentToolLabel} (0s)`);
270
+ toolTimer = setInterval(() => {
271
+ const elapsed = Math.floor((Date.now() - toolStartAt) / 1000);
272
+ spinner.text = `> ${currentToolLabel} (${elapsed}s)`;
273
+ }, 1000);
274
+ },
275
+ onToolEnd: async (event) => {
276
+ if (!streamOutput)
277
+ return;
278
+ clearToolTimer();
279
+ spinner.stop();
280
+ if (runtime.agent.isVerboseTools()) {
281
+ console.log(chalk.dim(`\n[${event.toolName}]\n${event.resultPreview}`));
282
+ }
283
+ else if (event.truncated) {
284
+ console.log(chalk.yellow(`\nTool ${event.toolName} output was truncated before returning to model.`));
285
+ }
286
+ },
287
+ };
288
+ try {
289
+ const result = await runtime.agent.run(preprocessed.processedPrompt, {
290
+ signal,
291
+ callbacks,
292
+ });
293
+ clearToolTimer();
294
+ spinner.stop();
295
+ const responseText = result.response;
296
+ if (streamOutput) {
297
+ if (!printedToken) {
298
+ console.log(chalk.bold.white("Agent:"));
299
+ console.log(renderMarkdown(responseText).trimEnd());
300
+ }
301
+ else {
302
+ processStdout.write("\n");
303
+ if (looksLikeMarkdown(responseText)) {
304
+ console.log(chalk.dim("[Rendered Markdown]"));
305
+ console.log(renderMarkdown(responseText).trimEnd());
306
+ }
307
+ }
308
+ }
309
+ if (!streamOutput && outputFormat === "text") {
310
+ console.log(responseText);
311
+ }
312
+ if (result.usage) {
313
+ const metrics = runtime.context.recordTurn(result.model, result.usage);
314
+ if (streamOutput) {
315
+ console.log(chalk.dim(`[↑ ${metrics.promptTokens.toLocaleString()} | ↓ ${metrics.completionTokens.toLocaleString()} | ~${formatUsd(metrics.turnCostUsd)} | total ${formatUsd(metrics.cumulativeCostUsd)}]`));
316
+ }
317
+ if (metrics.warnThresholdCrossed && streamOutput) {
318
+ console.log(chalk.yellow(`Context usage warning: ${(metrics.contextUtilization * 100).toFixed(1)}% of configured window (${runtime.config.contextWindowTokens.toLocaleString()} tokens).`));
319
+ }
320
+ if (metrics.autoCompactRecommended) {
321
+ const summary = await runtime.agent.compactHistory(signal);
322
+ if (streamOutput) {
323
+ console.log(chalk.yellow(`Auto-compact triggered at ${(metrics.contextUtilization * 100).toFixed(1)}% context usage.`));
324
+ console.log(renderMarkdown(summary).trimEnd());
325
+ }
326
+ }
327
+ }
328
+ await persistSnapshot(runtime);
329
+ if (outputFormat === "json") {
330
+ const cost = runtime.context.getCostSummary();
331
+ const payload = {
332
+ response: responseText,
333
+ usage: result.usage,
334
+ model: result.model,
335
+ finish_reason: result.finishReason,
336
+ tool_calls: result.toolCalls,
337
+ cost,
338
+ };
339
+ console.log(JSON.stringify(payload, null, 2));
340
+ }
341
+ return { response: responseText };
342
+ }
343
+ catch (err) {
344
+ clearToolTimer();
345
+ spinner.stop();
346
+ throw err;
347
+ }
348
+ }
349
+ async function handleSlashCommand(runtime, commandLine, setLastRawPrompt) {
350
+ const [command, ...rest] = commandLine.trim().split(/\s+/);
351
+ const argText = rest.join(" ").trim();
352
+ switch (command) {
353
+ case "/help":
354
+ printHelp();
355
+ return { handled: true, shouldExit: false };
356
+ case "/exit":
357
+ return { handled: true, shouldExit: true };
358
+ case "/clear":
359
+ console.clear();
360
+ return { handled: true, shouldExit: false };
361
+ case "/tools":
362
+ printTools();
363
+ return { handled: true, shouldExit: false };
364
+ case "/permissions":
365
+ console.log(`Current permission mode: ${runtime.agent.getPermissionMode()}`);
366
+ return { handled: true, shouldExit: false };
367
+ case "/verbose": {
368
+ const next = !runtime.agent.isVerboseTools();
369
+ runtime.agent.setVerboseTools(next);
370
+ console.log(`Verbose tool output: ${next ? "ON" : "OFF"}`);
371
+ await persistSnapshot(runtime);
372
+ return { handled: true, shouldExit: false };
373
+ }
374
+ case "/cost": {
375
+ const summary = runtime.context.getCostSummary();
376
+ console.log(`${chalk.dim("input")}: ${summary.inputTokens.toLocaleString()} ${chalk.dim("output")}: ${summary.outputTokens.toLocaleString()} ${chalk.dim("total")}: ${summary.totalTokens.toLocaleString()} ${chalk.dim("cost")}: ${formatUsd(summary.totalCostUsd)}`);
377
+ return { handled: true, shouldExit: false };
378
+ }
379
+ case "/compact": {
380
+ const summary = await runtime.agent.compactHistory();
381
+ console.log(renderMarkdown(summary).trimEnd());
382
+ await persistSnapshot(runtime);
383
+ return { handled: true, shouldExit: false };
384
+ }
385
+ case "/models": {
386
+ const models = await runtime.agent.listModels();
387
+ console.log(formatModels(models));
388
+ return { handled: true, shouldExit: false };
389
+ }
390
+ case "/undo": {
391
+ const undone = runtime.agent.undoLastTurn();
392
+ if (undone) {
393
+ setLastRawPrompt(undone);
394
+ console.log("Removed last user+assistant turn.");
395
+ await persistSnapshot(runtime);
396
+ }
397
+ else {
398
+ console.log("Nothing to undo.");
399
+ }
400
+ return { handled: true, shouldExit: false };
401
+ }
402
+ case "/retry":
403
+ return { handled: false, shouldExit: false };
404
+ case "/save": {
405
+ const name = argText || `session-${new Date().toISOString().replace(/[:.]/g, "-")}`;
406
+ const location = await runtime.sessions.saveNamed(name, runtime.agent.exportState(runtime.context.serializeState()));
407
+ console.log(`Saved session as \"${name}\" -> ${location}`);
408
+ return { handled: true, shouldExit: false };
409
+ }
410
+ case "/load": {
411
+ if (!argText) {
412
+ const names = await runtime.sessions.listNamed();
413
+ if (names.length === 0) {
414
+ console.log("No named sessions found.");
415
+ }
416
+ else {
417
+ console.log(`Named sessions: ${names.join(", ")}`);
418
+ }
419
+ return { handled: true, shouldExit: false };
420
+ }
421
+ const loaded = await runtime.sessions.loadNamed(argText);
422
+ if (!loaded) {
423
+ console.log(`No named session found: ${argText}`);
424
+ return { handled: true, shouldExit: false };
425
+ }
426
+ await restoreSnapshot(runtime, loaded);
427
+ setLastRawPrompt(loaded.lastUserInput);
428
+ console.log(`Loaded session \"${argText}\".`);
429
+ await persistSnapshot(runtime);
430
+ return { handled: true, shouldExit: false };
431
+ }
432
+ case "/cd": {
433
+ if (!argText) {
434
+ console.log(process.cwd());
435
+ return { handled: true, shouldExit: false };
436
+ }
437
+ const resolved = await resolveProtectedPath(argText);
438
+ const stat = await fs.stat(resolved);
439
+ if (!stat.isDirectory()) {
440
+ console.log(`Not a directory: ${resolved}`);
441
+ return { handled: true, shouldExit: false };
442
+ }
443
+ process.chdir(path.resolve(resolved));
444
+ console.log(`Working directory: ${process.cwd()}`);
445
+ await persistSnapshot(runtime);
446
+ return { handled: true, shouldExit: false };
447
+ }
448
+ case "/lint": {
449
+ const targetPath = argText || getRuntimeConfig().workspaceRoot;
450
+ const spinner = ora({ text: "Running linter...", color: "cyan" }).start();
451
+ try {
452
+ const result = await run_linter({ path: targetPath, linter: "auto", fix: false });
453
+ spinner.stop();
454
+ console.log(result);
455
+ }
456
+ catch (err) {
457
+ spinner.stop();
458
+ console.log(chalk.red(`Linter error: ${err instanceof Error ? err.message : String(err)}`));
459
+ }
460
+ return { handled: true, shouldExit: false };
461
+ }
462
+ case "/test": {
463
+ const targetPath = argText || undefined;
464
+ const spinner = ora({ text: "Running tests...", color: "cyan" }).start();
465
+ try {
466
+ const result = await run_tests({ path: targetPath, runner: "auto", bail: false });
467
+ spinner.stop();
468
+ console.log(result);
469
+ }
470
+ catch (err) {
471
+ spinner.stop();
472
+ console.log(chalk.red(`Test runner error: ${err instanceof Error ? err.message : String(err)}`));
473
+ }
474
+ return { handled: true, shouldExit: false };
475
+ }
476
+ case "/scan": {
477
+ const targetPath = argText || getRuntimeConfig().workspaceRoot;
478
+ const spinner = ora({ text: "Scanning for security issues...", color: "cyan" }).start();
479
+ try {
480
+ const result = await security_scan({ path: targetPath, include_low_severity: false });
481
+ spinner.stop();
482
+ console.log(result);
483
+ }
484
+ catch (err) {
485
+ spinner.stop();
486
+ console.log(chalk.red(`Scan error: ${err instanceof Error ? err.message : String(err)}`));
487
+ }
488
+ return { handled: true, shouldExit: false };
489
+ }
490
+ case "/memory": {
491
+ // /memory — show both scopes
492
+ // /memory clear — clear project memory
493
+ // /memory clear --global — clear global memory
494
+ if (!argText) {
495
+ const output = await showMemory(runtime.workspaceRoot);
496
+ console.log(output);
497
+ return { handled: true, shouldExit: false };
498
+ }
499
+ const memArgs = argText.split(/\s+/);
500
+ if (memArgs[0] === "clear") {
501
+ const scope = memArgs.includes("--global") ? "global" : "project";
502
+ await clearMemory(scope, runtime.workspaceRoot);
503
+ const fresh = await loadMemory(runtime.workspaceRoot);
504
+ runtime.agent.updateMemory(fresh);
505
+ console.log(`${scope === "global" ? "Global" : "Project"} memory cleared.`);
506
+ return { handled: true, shouldExit: false };
507
+ }
508
+ console.log(`Unknown /memory subcommand: ${argText}`);
509
+ return { handled: true, shouldExit: false };
510
+ }
511
+ case "/remember": {
512
+ // /remember <text> — append to project memory
513
+ // /remember --global <text> — append to global memory
514
+ if (!argText) {
515
+ console.log("Usage: /remember <text> or /remember --global <text>");
516
+ return { handled: true, shouldExit: false };
517
+ }
518
+ let scope = "project";
519
+ let fact = argText;
520
+ if (fact.startsWith("--global")) {
521
+ scope = "global";
522
+ fact = fact.slice("--global".length).trim();
523
+ }
524
+ if (!fact) {
525
+ console.log("Nothing to remember — provide text after the flag.");
526
+ return { handled: true, shouldExit: false };
527
+ }
528
+ const savedPath = await appendMemory(fact, scope, runtime.workspaceRoot);
529
+ const fresh = await loadMemory(runtime.workspaceRoot);
530
+ runtime.agent.updateMemory(fresh);
531
+ console.log(chalk.green(`Remembered (${scope}): ${fact}`));
532
+ console.log(chalk.dim(` → ${savedPath}`));
533
+ return { handled: true, shouldExit: false };
534
+ }
535
+ case "/review": {
536
+ const targetPath = argText || process.cwd();
537
+ const reviewPrompt = [
538
+ `Please perform a thorough code review of: ${targetPath}`,
539
+ "",
540
+ "Structure your review with these sections:",
541
+ "1. **Summary** — what the code does",
542
+ "2. **Critical Issues** — bugs, security flaws, data loss risks (severity: critical)",
543
+ "3. **High Issues** — performance, correctness, API misuse (severity: high)",
544
+ "4. **Medium Issues** — code quality, maintainability, missing error handling (severity: medium)",
545
+ "5. **Suggestions** — style, naming, refactoring opportunities",
546
+ "6. **Verdict** — overall assessment (Approve / Request Changes / Needs Major Rework)",
547
+ "",
548
+ "For each issue provide: file:line location, description, and suggested fix.",
549
+ "Use read_file and search_files tools to explore the code before reviewing.",
550
+ ].join("\n");
551
+ const reviewController = new AbortController();
552
+ try {
553
+ await executeTurn(runtime, reviewPrompt, reviewController.signal, "text", true);
554
+ }
555
+ catch (err) {
556
+ if (!(err instanceof Error && err.name === "TurnCancelledError")) {
557
+ console.log(chalk.red(`Review error: ${err instanceof Error ? err.message : String(err)}`));
558
+ }
559
+ }
560
+ return { handled: true, shouldExit: false };
561
+ }
562
+ default:
563
+ return { handled: false, shouldExit: false };
564
+ }
565
+ }
566
+ async function runNonInteractive(runtime, options) {
567
+ let promptText = options.prompt ?? "";
568
+ if (!promptText) {
569
+ promptText = (await readAllStdin()).trim();
570
+ }
571
+ if (!promptText) {
572
+ throw new Error("No prompt provided. Use --prompt or pipe input.");
573
+ }
574
+ const controller = new AbortController();
575
+ await executeTurn(runtime, promptText, controller.signal, options.outputFormat, false);
576
+ }
577
+ async function runInteractive(runtime, options) {
578
+ printBanner(runtime, options.permissions);
579
+ const health = await checkRouterHealth();
580
+ if (!health.ok) {
581
+ console.log(chalk.yellow(`Warning: ${health.message} — check LLM_ROUTER_BASE_URL before running requests.`));
582
+ console.log();
583
+ }
584
+ const replInput = new ReplInput(runtime.paths.historyFile, runtime.config.historyLimit);
585
+ await replInput.init();
586
+ let activeTurnController = null;
587
+ let lastRawPrompt = runtime.agent.getLastUserInput();
588
+ process.on("SIGINT", () => {
589
+ if (activeTurnController && !activeTurnController.signal.aborted) {
590
+ activeTurnController.abort();
591
+ console.log(chalk.yellow("\nTurn cancelled. Type a new message."));
592
+ return;
593
+ }
594
+ replInput
595
+ .close()
596
+ .catch(() => {
597
+ // ignore
598
+ })
599
+ .finally(() => {
600
+ console.log(chalk.yellow("\nGoodbye!"));
601
+ process.exit(0);
602
+ });
603
+ });
604
+ while (true) {
605
+ const userInput = await replInput.prompt();
606
+ const trimmed = userInput.trim();
607
+ if (!trimmed)
608
+ continue;
609
+ if (trimmed.startsWith("/")) {
610
+ const commandResult = await handleSlashCommand(runtime, trimmed, (value) => {
611
+ lastRawPrompt = value;
612
+ });
613
+ if (commandResult.handled) {
614
+ if (commandResult.shouldExit)
615
+ break;
616
+ continue;
617
+ }
618
+ if (trimmed === "/retry") {
619
+ if (!lastRawPrompt) {
620
+ console.log("No previous prompt to retry.");
621
+ continue;
622
+ }
623
+ }
624
+ else {
625
+ console.log(`Unknown command: ${trimmed}`);
626
+ continue;
627
+ }
628
+ }
629
+ const rawPrompt = trimmed === "/retry" ? lastRawPrompt ?? "" : userInput;
630
+ if (!rawPrompt.trim()) {
631
+ console.log("No previous prompt to retry.");
632
+ continue;
633
+ }
634
+ lastRawPrompt = rawPrompt;
635
+ activeTurnController = new AbortController();
636
+ try {
637
+ await executeTurn(runtime, rawPrompt, activeTurnController.signal, "text", true);
638
+ }
639
+ catch (err) {
640
+ if (err instanceof TurnCancelledError) {
641
+ // already messaged by SIGINT handler
642
+ }
643
+ else {
644
+ const message = err instanceof Error ? err.message : String(err);
645
+ console.log(chalk.red(`Error: ${message}`));
646
+ }
647
+ }
648
+ finally {
649
+ activeTurnController = null;
650
+ }
651
+ console.log();
652
+ }
653
+ await replInput.close();
654
+ }
655
+ // ── First-run setup wizard ────────────────────────────────────────────────────
656
+ async function runFirstTimeSetup() {
657
+ const { confirm, input, select } = await import("@inquirer/prompts");
658
+ console.log(chalk.bold.cyan("\n Welcome to LLM-Router CLI Agent!\n"));
659
+ console.log(chalk.dim(" No connection configured. Let's get you set up.\n"));
660
+ const routerUrl = await input({
661
+ message: "Router URL:",
662
+ default: "https://api.llm-router.dev",
663
+ validate: (v) => {
664
+ try {
665
+ new URL(v);
666
+ return true;
667
+ }
668
+ catch {
669
+ return "Enter a valid URL (e.g. https://api.llm-router.dev)";
670
+ }
671
+ },
672
+ });
673
+ const method = await select({
674
+ message: "How would you like to authenticate?",
675
+ choices: [
676
+ { name: "Log in with browser (Google / GitHub)", value: "browser" },
677
+ { name: "Paste an API key manually", value: "key" },
678
+ ],
679
+ });
680
+ if (method === "browser") {
681
+ await runLogin({ routerUrl });
682
+ }
683
+ else {
684
+ const apiKey = await input({
685
+ message: "API key:",
686
+ validate: (v) => v.startsWith("llr_prod_sk_") ? true : "Key must start with llr_prod_sk_",
687
+ });
688
+ const { saveCredentials } = await import("./auth.js");
689
+ await saveCredentials({
690
+ apiKey,
691
+ email: "",
692
+ tenantId: "",
693
+ routerUrl,
694
+ authenticatedAt: new Date().toISOString(),
695
+ });
696
+ process.env.LLM_ROUTER_BASE_URL = routerUrl;
697
+ process.env.LLM_ROUTER_API_KEY = apiKey;
698
+ console.log(chalk.green("\n✓ API key saved.\n"));
699
+ }
700
+ return true;
701
+ }
702
+ // ── Commander program ─────────────────────────────────────────────────────────
703
+ const program = new Command();
704
+ program
705
+ .name("llm-agent")
706
+ .description("Terminal-based AI agent powered by LLM-Router")
707
+ .version("2.0.0")
708
+ .option("-m, --model <model>", "Override the model to use")
709
+ .option("-u, --url <url>", "Override the LLM Router base URL")
710
+ .option("-p, --permissions <mode>", "Permission mode: safe|standard|yolo", "standard")
711
+ .option("--prompt <text>", "Run one non-interactive prompt and exit")
712
+ .option("--output-format <format>", "Output format for non-interactive mode: text|json", "text")
713
+ .option("--resume <sessionId>", "Resume a prior session by session ID")
714
+ .option("--local", "Bypass router — connect directly using OPENAI_API_KEY / ANTHROPIC_API_KEY")
715
+ .action(async (options) => {
716
+ if (!isPermissionMode(options.permissions)) {
717
+ console.error(chalk.red(`Invalid permission mode \"${options.permissions}\". Use one of: ${PERMISSION_MODES.join(", ")}`));
718
+ process.exit(1);
719
+ }
720
+ if (!isOutputFormat(options.outputFormat)) {
721
+ console.error(chalk.red(`Invalid output format \"${options.outputFormat}\". Use text or json.`));
722
+ process.exit(1);
723
+ }
724
+ // --local mode: bypass router, use provider key directly
725
+ if (options.local) {
726
+ const directKey = process.env.OPENAI_API_KEY ||
727
+ process.env.ANTHROPIC_API_KEY ||
728
+ process.env.GEMINI_API_KEY;
729
+ const directUrl = process.env.OPENAI_API_KEY
730
+ ? "https://api.openai.com/v1"
731
+ : process.env.ANTHROPIC_API_KEY
732
+ ? "https://api.anthropic.com/v1"
733
+ : "https://generativelanguage.googleapis.com/v1beta/openai";
734
+ if (!directKey) {
735
+ console.error(chalk.red("--local requires OPENAI_API_KEY, ANTHROPIC_API_KEY, or GEMINI_API_KEY to be set."));
736
+ process.exit(1);
737
+ }
738
+ process.env.LLM_ROUTER_BASE_URL = directUrl;
739
+ process.env.LLM_ROUTER_API_KEY = directKey;
740
+ if (!options.model)
741
+ process.env.LLM_ROUTER_MODEL = process.env.OPENAI_API_KEY ? "gpt-4o" : "claude-opus-4-6";
742
+ }
743
+ else {
744
+ // Apply explicit overrides first, then fall back to credentials file
745
+ if (options.url)
746
+ process.env.LLM_ROUTER_BASE_URL = options.url;
747
+ const conn = await resolveConnectionFromEnvOrCreds();
748
+ if (!conn) {
749
+ // No connection config at all — run first-time setup
750
+ if (!processStdin.isTTY) {
751
+ console.error(chalk.red("No credentials found. Run `llm-agent login` to authenticate first."));
752
+ process.exit(1);
753
+ }
754
+ await runFirstTimeSetup();
755
+ }
756
+ }
757
+ if (options.model)
758
+ process.env.LLM_ROUTER_MODEL = options.model;
759
+ process.env.LLM_AGENT_PERMISSION_MODE = options.permissions;
760
+ const runtime = await bootstrapRuntime(options);
761
+ await persistSnapshot(runtime);
762
+ const nonInteractive = Boolean(options.prompt) || !processStdin.isTTY;
763
+ if (nonInteractive) {
764
+ await runNonInteractive(runtime, options);
765
+ return;
766
+ }
767
+ await runInteractive(runtime, options);
768
+ });
769
+ // ── llm-agent login ───────────────────────────────────────────────────────────
770
+ program
771
+ .command("login")
772
+ .description("Authenticate with your LLM-Router account")
773
+ .option("-u, --url <url>", "Router base URL", "")
774
+ .action(async (opts) => {
775
+ let routerUrl = opts.url || process.env.LLM_ROUTER_BASE_URL || "";
776
+ if (!routerUrl) {
777
+ const creds = await loadCredentials();
778
+ routerUrl = creds?.routerUrl ?? "";
779
+ }
780
+ if (!routerUrl) {
781
+ const { input } = await import("@inquirer/prompts");
782
+ routerUrl = await input({
783
+ message: "Router URL:",
784
+ default: "https://api.llm-router.dev",
785
+ validate: (v) => {
786
+ try {
787
+ new URL(v);
788
+ return true;
789
+ }
790
+ catch {
791
+ return "Enter a valid URL";
792
+ }
793
+ },
794
+ });
795
+ }
796
+ try {
797
+ await runLogin({ routerUrl });
798
+ }
799
+ catch (err) {
800
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
801
+ process.exit(1);
802
+ }
803
+ });
804
+ // ── llm-agent logout ──────────────────────────────────────────────────────────
805
+ program
806
+ .command("logout")
807
+ .description("Revoke CLI credentials and sign out")
808
+ .action(async () => {
809
+ try {
810
+ await runLogout();
811
+ }
812
+ catch (err) {
813
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
814
+ process.exit(1);
815
+ }
816
+ });
817
+ // ── llm-agent whoami ──────────────────────────────────────────────────────────
818
+ program
819
+ .command("whoami")
820
+ .description("Show the currently authenticated account and router")
821
+ .action(async () => {
822
+ await runWhoami();
823
+ });
824
+ // ── llm-agent update ──────────────────────────────────────────────────────────
825
+ program
826
+ .command("update")
827
+ .description("Update llm-agent to the latest version")
828
+ .action(async () => {
829
+ const { exec: execCb } = await import("child_process");
830
+ const { promisify: prom } = await import("util");
831
+ const execP = prom(execCb);
832
+ const spinner = ora("Checking for updates…").start();
833
+ // Detect which package manager installed us
834
+ let installCmd = "npm install -g llm-agent-cli@latest";
835
+ try {
836
+ await execP("which pnpm");
837
+ installCmd = "pnpm add -g llm-agent-cli@latest";
838
+ }
839
+ catch {
840
+ try {
841
+ await execP("which yarn");
842
+ installCmd = "yarn global add llm-agent-cli@latest";
843
+ }
844
+ catch {
845
+ // default npm
846
+ }
847
+ }
848
+ try {
849
+ const { stdout } = await execP(`npm show llm-agent-cli version`);
850
+ const latest = stdout.trim();
851
+ spinner.succeed(`Latest version: ${latest}`);
852
+ console.log(chalk.dim(`Running: ${installCmd}`));
853
+ const updateSpinner = ora("Installing update…").start();
854
+ await execP(installCmd);
855
+ updateSpinner.succeed("Updated successfully. Restart your terminal to use the new version.");
856
+ }
857
+ catch (err) {
858
+ spinner.fail("Update failed");
859
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
860
+ process.exit(1);
861
+ }
862
+ });
863
+ // ── llm-agent config ──────────────────────────────────────────────────────────
864
+ program
865
+ .command("config")
866
+ .description("Show or edit CLI configuration")
867
+ .option("--show", "Print current config and credentials (hides key)")
868
+ .action(async (opts) => {
869
+ const creds = await loadCredentials();
870
+ const paths = getAgentPaths();
871
+ const config = await loadConfig(paths);
872
+ if (opts.show || true) {
873
+ console.log(chalk.bold("\nConnection"));
874
+ if (creds) {
875
+ console.log(` Router: ${creds.routerUrl}`);
876
+ console.log(` Email: ${creds.email || "(not set)"}`);
877
+ console.log(` API Key: ${creds.apiKey.slice(0, 16)}…`);
878
+ console.log(` Auth date: ${new Date(creds.authenticatedAt).toLocaleString()}`);
879
+ }
880
+ else if (process.env.LLM_ROUTER_BASE_URL) {
881
+ console.log(` Router: ${process.env.LLM_ROUTER_BASE_URL} (from env)`);
882
+ console.log(` API Key: (from env)`);
883
+ }
884
+ else {
885
+ console.log(chalk.yellow(" Not logged in. Run `llm-agent login`."));
886
+ }
887
+ console.log(chalk.bold("\nAgent config"));
888
+ console.log(` Context window: ${config.contextWindowTokens.toLocaleString()} tokens`);
889
+ console.log(` Max read bytes: ${(config.maxReadBytes / 1024).toFixed(0)} KB`);
890
+ console.log(` Shell timeout: ${config.shellTimeoutMs / 1000}s`);
891
+ console.log(` History limit: ${config.historyLimit} entries`);
892
+ console.log(chalk.bold("\nPaths"));
893
+ console.log(` Home: ${paths.homeDir}`);
894
+ console.log(` Config: ${paths.configFile}`);
895
+ console.log(` Sessions: ${paths.sessionsDir}`);
896
+ console.log(` Audit: ${paths.auditFile}`);
897
+ console.log();
898
+ }
899
+ });
900
+ program.parseAsync().catch((err) => {
901
+ const message = err instanceof Error ? err.message : String(err);
902
+ console.error(chalk.red(message));
903
+ process.exit(1);
904
+ });
905
+ //# sourceMappingURL=index.js.map