codelantern-agent-cli 1.0.0 → 1.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/dist/cli.js CHANGED
@@ -1,27 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ALLOWED_CONFIG_KEYS,
4
+ CancelledError,
5
+ LanternAgent,
4
6
  McpManager,
5
- OrbitAgent,
7
+ ProviderRegistry,
8
+ SkillManager,
6
9
  addMcpServer,
7
10
  createNewSession,
8
11
  deleteSession,
9
12
  exportSessionMarkdown,
10
13
  formatErrorMessage,
14
+ getApiKey,
11
15
  getConfigFilePath,
12
16
  getLanternHomeDir,
13
17
  getSessionsDir,
18
+ listConfiguredKeys,
14
19
  listSessions,
15
20
  loadAttachments,
16
21
  loadConfig,
17
22
  loadMcpConfig,
18
23
  loadProjectInstructions,
19
24
  loadSession,
25
+ maskApiKey,
26
+ removeApiKey,
20
27
  removeMcpServer,
21
28
  saveSession,
29
+ setApiKey,
22
30
  setConfigValue,
23
31
  toggleMcpServer
24
- } from "./chunk-Y3PEOLTZ.js";
32
+ } from "./chunk-PELESOCI.js";
25
33
 
26
34
  // src/cli.ts
27
35
  import { Command } from "commander";
@@ -144,12 +152,13 @@ function printBanner(options) {
144
152
  | | / _ \\ / _\` |/ _ \\ | | / _\` | '_ \\ | __/ _ \\ '__| '_ \\
145
153
  | |__| (_) | (_| | __/ | |__| (_| | | | || || __/ | | | | |
146
154
  \\____\\___/ \\__,_|\\___| |_____\\__,_|_| |_(_)__\\___|_| |_| |_|
147
- \u{1F3EE} Personal AI Coding Assistant for Terminal (Gemini)
155
+ \u{1F3EE} Personal AI Coding Assistant for Terminal (Multi-Provider)
148
156
  `;
149
157
  console.log(pc2.yellow(logo));
150
158
  console.log(pc2.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
159
+ const providerDisplay = options.provider ? `${pc2.bold("Provider:")} ${pc2.magenta(options.provider)} ` : "";
151
160
  console.log(`${pc2.bold("Product:")} Code Lantern CLI (${pc2.cyan("lantern")}) ${pc2.bold("Version:")} ${options.version}`);
152
- console.log(`${pc2.bold("Default Model:")} ${pc2.green(options.model)}`);
161
+ console.log(`${providerDisplay}${pc2.bold("Model:")} ${pc2.green(options.model)}`);
153
162
  console.log(`${pc2.bold("Workspace:")} ${pc2.blue(options.workspace)}`);
154
163
  const modeBadge = options.planMode ? pc2.bgBlue(pc2.white(" PLAN MODE (Read-only) ")) : options.yolo ? pc2.bgRed(pc2.white(" YOLO MODE (Auto-approve) ")) : pc2.bgGreen(pc2.black(" SAFE MODE (Approval Prompts) "));
155
164
  console.log(`${pc2.bold("Status:")} ${modeBadge}`);
@@ -172,8 +181,11 @@ async function handleSlashCommand(input, context) {
172
181
  console.log(pc3.bold("Slash Commands:"));
173
182
  console.log(` ${pc3.cyan("/help")} Show this help overview`);
174
183
  console.log(` ${pc3.cyan("/clear")} Clear the terminal screen`);
175
- console.log(` ${pc3.cyan("/status")} Display current session, workspace, and model status`);
176
- console.log(` ${pc3.cyan("/model [name]")} View or change current Gemini model`);
184
+ console.log(` ${pc3.cyan("/status")} Display session, provider, model, and skill status`);
185
+ console.log(` ${pc3.cyan("/provider [name]")} View or switch active AI provider (gemini, openai, anthropic, openrouter)`);
186
+ console.log(` ${pc3.cyan("/model [id]")} View or switch active model`);
187
+ console.log(` ${pc3.cyan("/skills [name]")} List, enable, or disable active skills`);
188
+ console.log(` ${pc3.cyan("/timing [on|off]")} Toggle request timing diagnostics display`);
177
189
  console.log(` ${pc3.cyan("/plan [on|off]")} Toggle read-only planning mode`);
178
190
  console.log(` ${pc3.cyan("/yolo [on|off]")} Toggle YOLO auto-approval mode`);
179
191
  console.log(` ${pc3.cyan("/sessions")} List saved local sessions`);
@@ -181,25 +193,12 @@ async function handleSlashCommand(input, context) {
181
193
  console.log(` ${pc3.cyan("/export [md|json]")} Export current conversation transcript`);
182
194
  console.log(` ${pc3.cyan("/exit")} or ${pc3.cyan("/quit")} Exit Code Lantern CLI`);
183
195
  console.log("");
184
- console.log(pc3.bold("Available Agent Tools:"));
185
- console.log(` ${pc3.yellow("read_file")} Read file contents with line ranges`);
186
- console.log(` ${pc3.yellow("write_file")} Create new files (requires approval in safe mode)`);
187
- console.log(` ${pc3.yellow("edit_file")} Apply targeted patch with diff preview`);
188
- console.log(` ${pc3.yellow("list_directory")} Inspect directories honoring .gitignore`);
189
- console.log(` ${pc3.yellow("file_search")} Find files matching glob/pattern`);
190
- console.log(` ${pc3.yellow("grep_search")} Search code patterns across files`);
191
- console.log(` ${pc3.yellow("project_tree")} Generate ASCII project tree`);
192
- console.log(` ${pc3.yellow("run_command")} Run shell commands (requires approval)`);
193
- console.log(` ${pc3.yellow("git_status")} Inspect Git status`);
194
- console.log(` ${pc3.yellow("git_diff")} Inspect Git working tree / staged diffs`);
195
- console.log(` ${pc3.yellow("web_search")} Search documentation / error references`);
196
- console.log("");
197
196
  console.log(pc3.bold("Keyboard Shortcuts:"));
198
197
  console.log(` ${pc3.dim("Enter")} Submit prompt`);
199
198
  console.log(` ${pc3.dim("\\ + Enter")} Continue prompt on next line (multiline)`);
200
199
  console.log(` ${pc3.dim('""" ... """')} Multiline prompt block`);
201
200
  console.log(` ${pc3.dim("Up/Down Arrows")} Navigate prompt history`);
202
- console.log(` ${pc3.dim("Ctrl+C")} Cancel current line / double-tap to exit`);
201
+ console.log(` ${pc3.dim("Ctrl+C")} Cancel active request / double-tap to exit`);
203
202
  console.log(` ${pc3.dim("Ctrl+D")} Exit session`);
204
203
  console.log("");
205
204
  return true;
@@ -209,22 +208,98 @@ async function handleSlashCommand(input, context) {
209
208
  case "/status":
210
209
  console.log("\n" + pc3.bold(pc3.yellow("\u{1F3EE} Code Lantern Status")));
211
210
  console.log(pc3.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
212
- console.log(`${pc3.bold("Session ID:")} ${pc3.cyan(context.session.id)}`);
213
- console.log(`${pc3.bold("Workspace:")} ${pc3.blue(context.workspace)}`);
214
- console.log(`${pc3.bold("Active Model:")} ${pc3.green(context.model)}`);
215
- console.log(`${pc3.bold("Turns:")} ${context.session.turns.length}`);
216
- console.log(`${pc3.bold("Total Tokens:")} ${context.session.totalUsage.totalTokens.toLocaleString()}`);
217
- console.log(`${pc3.bold("YOLO Mode:")} ${context.yolo ? pc3.red("ON (Auto-approve)") : pc3.green("OFF (Prompt approval)")}`);
218
- console.log(`${pc3.bold("Plan Mode:")} ${context.planMode ? pc3.blue("ON (Read-only)") : pc3.dim("OFF (Full execution)")}`);
211
+ console.log(`${pc3.bold("Session ID:")} ${pc3.cyan(context.session.id)}`);
212
+ console.log(`${pc3.bold("Workspace:")} ${pc3.blue(context.workspace)}`);
213
+ console.log(`${pc3.bold("Active Provider:")} ${pc3.magenta(context.provider)}`);
214
+ console.log(`${pc3.bold("Active Model:")} ${pc3.green(context.model)}`);
215
+ console.log(`${pc3.bold("Active Skills:")} ${context.activeSkills.length > 0 ? pc3.yellow(context.activeSkills.join(", ")) : pc3.dim("none")}`);
216
+ console.log(`${pc3.bold("Turns:")} ${context.session.turns.length}`);
217
+ console.log(`${pc3.bold("Total Tokens:")} ${context.session.totalUsage.totalTokens.toLocaleString()}`);
218
+ console.log(`${pc3.bold("Timing Logs:")} ${context.showTiming ? pc3.green("ON") : pc3.dim("OFF")}`);
219
+ console.log(`${pc3.bold("YOLO Mode:")} ${context.yolo ? pc3.red("ON (Auto-approve)") : pc3.green("OFF (Prompt approval)")}`);
220
+ console.log(`${pc3.bold("Plan Mode:")} ${context.planMode ? pc3.blue("ON (Read-only)") : pc3.dim("OFF (Full execution)")}`);
219
221
  console.log("");
220
222
  return true;
223
+ case "/provider": {
224
+ const registry = new ProviderRegistry();
225
+ if (arg) {
226
+ const norm = arg.toLowerCase().trim();
227
+ if (registry.hasProvider(norm)) {
228
+ context.setProvider(norm);
229
+ const p = registry.getProvider(norm);
230
+ context.setModel(p.defaultModel);
231
+ console.log(pc3.green(`\u2713 Switched provider to '${p.displayName}' (default model: ${p.defaultModel})`));
232
+ } else {
233
+ console.log(pc3.red(`Provider '${arg}' not found. Available: ${registry.listProviders().map((p) => p.id).join(", ")}`));
234
+ }
235
+ } else {
236
+ const providers = registry.listProviders();
237
+ console.log(`
238
+ Current provider: ${pc3.magenta(context.provider)}`);
239
+ console.log("Available providers:");
240
+ for (const p of providers) {
241
+ const active = p.id === context.provider ? pc3.green(" (active)") : "";
242
+ console.log(` - ${pc3.bold(p.id)}: ${p.displayName} [${p.isConfigured ? pc3.green("Ready") : pc3.yellow("Needs key")}]${active}`);
243
+ }
244
+ console.log(pc3.dim(`Usage: /provider <name> (e.g. /provider openai)`));
245
+ }
246
+ return true;
247
+ }
221
248
  case "/model":
222
249
  if (arg) {
223
250
  context.setModel(arg.trim());
224
251
  console.log(pc3.green(`\u2713 Model changed to: ${arg.trim()}`));
225
252
  } else {
226
- console.log(`Current model: ${pc3.cyan(context.model)}`);
227
- console.log(pc3.dim(`Usage: /model <model-id> (e.g. /model gemini-3.8-flash)`));
253
+ console.log(`Current model: ${pc3.cyan(context.model)} (Provider: ${context.provider})`);
254
+ console.log(pc3.dim(`Usage: /model <model-id> (or run 'lantern models list' to view available models)`));
255
+ }
256
+ return true;
257
+ case "/skills": {
258
+ const manager = new SkillManager();
259
+ const skills = manager.listSkills(context.workspace);
260
+ if (arg) {
261
+ const target = arg.trim();
262
+ const skill = manager.getSkill(target, context.workspace);
263
+ if (skill) {
264
+ const isCurrentlyActive = context.activeSkills.includes(skill.name);
265
+ if (isCurrentlyActive) {
266
+ context.setActiveSkills(context.activeSkills.filter((s) => s !== skill.name));
267
+ console.log(pc3.yellow(`\u2713 Deactivated skill '${skill.name}' for this session.`));
268
+ } else {
269
+ context.setActiveSkills([...context.activeSkills, skill.name]);
270
+ console.log(pc3.green(`\u2713 Activated skill '${skill.name}' for this session.`));
271
+ }
272
+ } else {
273
+ console.log(pc3.red(`Skill '${target}' not found.`));
274
+ }
275
+ } else {
276
+ console.log("\n" + pc3.bold(pc3.yellow("\u{1F3EE} Code Lantern Skills:")));
277
+ if (skills.length === 0) {
278
+ console.log(pc3.dim(" (No skills currently installed. Add one with: lantern skills add <path>)"));
279
+ } else {
280
+ for (const s of skills) {
281
+ const isSessionActive = context.activeSkills.includes(s.name);
282
+ const status = isSessionActive ? pc3.green("\u25CF ACTIVE") : s.enabled ? pc3.cyan("\u25CB default") : pc3.dim("\u25CB inactive");
283
+ console.log(` ${status} ${pc3.bold(s.name)} - ${pc3.dim(s.description.slice(0, 50))}`);
284
+ }
285
+ }
286
+ console.log(pc3.dim(`
287
+ Toggle skill for session: /skills <name>
288
+ `));
289
+ }
290
+ return true;
291
+ }
292
+ case "/timing":
293
+ if (arg === "off") {
294
+ context.setShowTiming(false);
295
+ console.log(pc3.yellow("\u2713 Timing diagnostics disabled."));
296
+ } else if (arg === "on") {
297
+ context.setShowTiming(true);
298
+ console.log(pc3.green("\u2713 Timing diagnostics enabled."));
299
+ } else {
300
+ const next = !context.showTiming;
301
+ context.setShowTiming(next);
302
+ console.log(next ? pc3.green("\u2713 Timing diagnostics enabled.") : pc3.yellow("\u2713 Timing diagnostics disabled."));
228
303
  }
229
304
  return true;
230
305
  case "/plan":
@@ -302,9 +377,13 @@ async function handleSlashCommand(input, context) {
302
377
  async function interactiveCommand(options) {
303
378
  const config2 = loadConfig();
304
379
  const workspace = options.cwd || process.cwd();
380
+ let provider = options.provider || config2.activeProvider || "gemini";
305
381
  let model = options.model || config2.defaultModel || "gemini-3.8-flash";
306
382
  let yolo = Boolean(options.yolo ?? config2.yolo);
307
383
  let planMode = Boolean(options.plan);
384
+ let showTiming = Boolean(options.timing ?? config2.showTiming);
385
+ let lowLatency = Boolean(options.fast ?? config2.lowLatency);
386
+ let activeSkills = options.skill || config2.activeSkills || [];
308
387
  let session;
309
388
  if (options.session) {
310
389
  const loaded = loadSession(options.session);
@@ -313,13 +392,18 @@ async function interactiveCommand(options) {
313
392
  process.exit(1);
314
393
  }
315
394
  session = loaded;
395
+ if (session.provider) provider = session.provider;
396
+ if (session.model) model = session.model;
397
+ if (session.activeSkills) activeSkills = session.activeSkills;
316
398
  console.log(pc4.green(`\u2713 Resumed session: ${session.id} (${session.turns.length} turns)`));
317
399
  } else {
318
400
  session = createNewSession(workspace, model);
401
+ session.provider = provider;
319
402
  }
320
403
  const attachments = loadAttachments(options.attach);
321
404
  printBanner({
322
- version: "1.0.0",
405
+ version: "1.0.1",
406
+ provider,
323
407
  model,
324
408
  workspace,
325
409
  yolo,
@@ -329,15 +413,22 @@ async function interactiveCommand(options) {
329
413
  console.log(pc4.cyan(`\u{1F4CE} Attached ${attachments.length} file(s): ${attachments.map((a) => a.name).join(", ")}
330
414
  `));
331
415
  }
332
- const agent = new OrbitAgent();
416
+ const agent = new LanternAgent();
333
417
  await agent.initMcp();
334
418
  const promptUI = new TerminalPrompt();
335
419
  const slashContext = {
336
420
  session,
421
+ provider,
337
422
  model,
338
423
  workspace,
339
424
  yolo,
340
425
  planMode,
426
+ showTiming,
427
+ activeSkills,
428
+ setProvider: (p) => {
429
+ provider = p;
430
+ session.provider = p;
431
+ },
341
432
  setModel: (m) => {
342
433
  model = m;
343
434
  session.model = m;
@@ -347,6 +438,13 @@ async function interactiveCommand(options) {
347
438
  },
348
439
  setPlanMode: (p) => {
349
440
  planMode = p;
441
+ },
442
+ setShowTiming: (t) => {
443
+ showTiming = t;
444
+ },
445
+ setActiveSkills: (skills) => {
446
+ activeSkills = skills;
447
+ session.activeSkills = skills;
350
448
  }
351
449
  };
352
450
  try {
@@ -370,25 +468,52 @@ async function interactiveCommand(options) {
370
468
  }
371
469
  continue;
372
470
  }
471
+ const abortController = new AbortController();
472
+ const onSigInt = () => {
473
+ abortController.abort(new CancelledError("Request cancelled by user (Ctrl+C)."));
474
+ };
475
+ process.on("SIGINT", onSigInt);
373
476
  const context = {
374
477
  workspace,
478
+ provider,
375
479
  model,
376
480
  yolo,
377
481
  planMode,
378
482
  jsonOutput: false,
379
483
  maxSteps: config2.maxSteps || 25,
380
- attachments
484
+ timeoutMs: options.timeout || config2.timeoutMs || 6e4,
485
+ firstTokenTimeoutMs: options.firstTokenTimeout || config2.firstTokenTimeoutMs || 3e4,
486
+ showTiming,
487
+ lowLatency,
488
+ activeSkills,
489
+ attachments,
490
+ signal: abortController.signal
381
491
  };
382
492
  try {
383
493
  console.log("");
384
494
  const result = await agent.runTurn(session, trimmed, context, attachments);
495
+ const parts = [];
385
496
  if (result.usage.totalTokens > 0) {
497
+ parts.push(`Tokens: ${result.usage.totalTokens.toLocaleString()}`);
498
+ }
499
+ if (showTiming && result.timing) {
500
+ const ttft = result.timing.ttftMs ? `TTFT: ${result.timing.ttftMs}ms` : "";
501
+ const total = `Total: ${(result.timing.totalMs / 1e3).toFixed(2)}s`;
502
+ parts.push(`[${[provider, model, ttft, total].filter(Boolean).join(" | ")}]`);
503
+ }
504
+ if (parts.length > 0) {
386
505
  console.log(pc4.dim(`
387
- [Tokens: ${result.usage.totalTokens.toLocaleString()} | Total turns: ${session.turns.length}]`));
506
+ [${parts.join(" | ")}]`));
388
507
  }
389
508
  console.log("");
390
509
  } catch (err) {
391
- console.error("\n" + formatErrorMessage(err) + "\n");
510
+ if (err instanceof CancelledError || abortController.signal.aborted) {
511
+ console.log(pc4.yellow("\n[Cancelled by user]"));
512
+ } else {
513
+ console.error("\n" + formatErrorMessage(err) + "\n");
514
+ }
515
+ } finally {
516
+ process.removeListener("SIGINT", onSigInt);
392
517
  }
393
518
  }
394
519
  } finally {
@@ -400,48 +525,79 @@ async function interactiveCommand(options) {
400
525
  // src/commands/ask.ts
401
526
  import pc5 from "picocolors";
402
527
  async function askCommand(question, options) {
528
+ const config2 = loadConfig();
403
529
  const workspace = options.cwd || process.cwd();
404
- const model = options.model || "gemini-3.8-flash";
405
- const yolo = Boolean(options.yolo);
530
+ const provider = options.provider || config2.activeProvider || "gemini";
531
+ const model = options.model || config2.defaultModel || "gemini-3.8-flash";
532
+ const yolo = Boolean(options.yolo ?? config2.yolo);
406
533
  const jsonOutput = Boolean(options.json);
534
+ const showTiming = Boolean(options.timing ?? config2.showTiming);
535
+ const lowLatency = Boolean(options.fast ?? config2.lowLatency);
407
536
  const attachments = loadAttachments(options.attach);
408
537
  const session = createNewSession(workspace, model, `Ask: ${question.slice(0, 40)}`);
409
- const agent = new OrbitAgent();
538
+ session.provider = provider;
539
+ const agent = new LanternAgent();
410
540
  await agent.initMcp();
541
+ const abortController = new AbortController();
542
+ const onSigInt = () => {
543
+ abortController.abort(new CancelledError("Request cancelled by user (Ctrl+C)."));
544
+ };
545
+ process.on("SIGINT", onSigInt);
411
546
  const context = {
412
547
  workspace,
548
+ provider,
413
549
  model,
414
550
  yolo,
415
551
  planMode: false,
416
552
  jsonOutput,
417
- maxSteps: options.maxSteps || 25,
418
- attachments
553
+ maxSteps: options.maxSteps || config2.maxSteps || 25,
554
+ timeoutMs: options.timeout || config2.timeoutMs || 6e4,
555
+ firstTokenTimeoutMs: options.firstTokenTimeout || config2.firstTokenTimeoutMs || 3e4,
556
+ showTiming,
557
+ lowLatency,
558
+ activeSkills: options.skill || config2.activeSkills || [],
559
+ attachments,
560
+ signal: abortController.signal
419
561
  };
420
562
  try {
421
563
  const result = await agent.runTurn(session, question, context, attachments);
422
564
  if (jsonOutput) {
423
565
  console.log(JSON.stringify({
424
566
  sessionId: session.id,
567
+ provider,
425
568
  model,
426
569
  question,
427
570
  reply: result.reply,
428
571
  toolCount: result.toolCount,
429
- usage: result.usage
572
+ usage: result.usage,
573
+ timing: result.timing
430
574
  }, null, 2));
431
575
  } else {
576
+ const parts = [];
432
577
  if (result.usage.totalTokens > 0) {
578
+ parts.push(`Tokens: ${result.usage.totalTokens.toLocaleString()}`);
579
+ }
580
+ if (showTiming && result.timing) {
581
+ const ttft = result.timing.ttftMs ? `TTFT: ${result.timing.ttftMs}ms` : "";
582
+ const total = `Total: ${(result.timing.totalMs / 1e3).toFixed(2)}s`;
583
+ parts.push(`[${[provider, model, ttft, total].filter(Boolean).join(" | ")}]`);
584
+ }
585
+ if (parts.length > 0) {
433
586
  console.log(pc5.dim(`
434
- [Tokens: ${result.usage.totalTokens} | Tools executed: ${result.toolCount}]`));
587
+ [${parts.join(" | ")}]`));
435
588
  }
436
589
  }
437
590
  } catch (err) {
438
591
  if (jsonOutput) {
439
592
  console.error(JSON.stringify({ error: err.message }));
593
+ } else if (err instanceof CancelledError || abortController.signal.aborted) {
594
+ console.log(pc5.yellow("\n[Cancelled by user]"));
440
595
  } else {
441
596
  console.error(formatErrorMessage(err));
442
597
  }
443
598
  process.exit(1);
444
599
  } finally {
600
+ process.removeListener("SIGINT", onSigInt);
445
601
  await agent.shutdown();
446
602
  }
447
603
  }
@@ -449,25 +605,42 @@ async function askCommand(question, options) {
449
605
  // src/commands/plan.ts
450
606
  import pc6 from "picocolors";
451
607
  async function planCommand(task, options) {
608
+ const config2 = loadConfig();
452
609
  const workspace = options.cwd || process.cwd();
453
- const model = options.model || "gemini-3.8-flash";
610
+ const provider = options.provider || config2.activeProvider || "gemini";
611
+ const model = options.model || config2.defaultModel || "gemini-3.8-flash";
454
612
  const jsonOutput = Boolean(options.json);
613
+ const showTiming = Boolean(options.timing ?? config2.showTiming);
455
614
  const attachments = loadAttachments(options.attach);
456
615
  const session = createNewSession(workspace, model, `Plan: ${task.slice(0, 40)}`);
616
+ session.provider = provider;
457
617
  if (!jsonOutput) {
458
618
  console.log(pc6.bold(pc6.yellow("\n\u{1F3EE} Code Lantern Plan Mode (Read-Only)")));
459
- console.log(pc6.dim("Inspecting workspace and formulating step-by-step implementation plan...\n"));
619
+ console.log(pc6.dim(`Inspecting workspace with ${provider} (${model})...
620
+ `));
460
621
  }
461
- const agent = new OrbitAgent();
622
+ const agent = new LanternAgent();
462
623
  await agent.initMcp();
624
+ const abortController = new AbortController();
625
+ const onSigInt = () => {
626
+ abortController.abort(new CancelledError("Plan cancelled by user (Ctrl+C)."));
627
+ };
628
+ process.on("SIGINT", onSigInt);
463
629
  const context = {
464
630
  workspace,
631
+ provider,
465
632
  model,
466
633
  yolo: false,
467
634
  planMode: true,
468
635
  jsonOutput,
469
- maxSteps: options.maxSteps || 25,
470
- attachments
636
+ maxSteps: options.maxSteps || config2.maxSteps || 25,
637
+ timeoutMs: options.timeout || config2.timeoutMs || 6e4,
638
+ firstTokenTimeoutMs: options.firstTokenTimeout || config2.firstTokenTimeoutMs || 3e4,
639
+ showTiming,
640
+ lowLatency: false,
641
+ activeSkills: options.skill || config2.activeSkills || [],
642
+ attachments,
643
+ signal: abortController.signal
471
644
  };
472
645
  const planPrompt = `Please inspect the codebase as needed and generate a detailed, structured, read-only implementation plan for the following task:
473
646
 
@@ -477,23 +650,31 @@ ${task}`;
477
650
  if (jsonOutput) {
478
651
  console.log(JSON.stringify({
479
652
  sessionId: session.id,
653
+ provider,
480
654
  model,
481
655
  task,
482
656
  plan: result.reply,
483
- usage: result.usage
657
+ usage: result.usage,
658
+ timing: result.timing
484
659
  }, null, 2));
485
660
  } else {
486
661
  console.log(pc6.dim(`
487
662
  [Plan complete. Session ID: ${session.id}]`));
663
+ if (showTiming && result.timing) {
664
+ console.log(pc6.dim(`[Total Plan Time: ${(result.timing.totalMs / 1e3).toFixed(2)}s]`));
665
+ }
488
666
  }
489
667
  } catch (err) {
490
668
  if (jsonOutput) {
491
669
  console.error(JSON.stringify({ error: err.message }));
670
+ } else if (err instanceof CancelledError || abortController.signal.aborted) {
671
+ console.log(pc6.yellow("\n[Plan cancelled by user]"));
492
672
  } else {
493
673
  console.error(formatErrorMessage(err));
494
674
  }
495
675
  process.exit(1);
496
676
  } finally {
677
+ process.removeListener("SIGINT", onSigInt);
497
678
  await agent.shutdown();
498
679
  }
499
680
  }
@@ -641,11 +822,13 @@ import fs2 from "fs";
641
822
  import path2 from "path";
642
823
  import { execSync } from "child_process";
643
824
  import pc10 from "picocolors";
644
- import { GoogleGenAI } from "@google/genai";
645
825
  async function doctorCommand() {
646
826
  console.log("\n" + pc10.bold(pc10.yellow("\u{1F3EE} Code Lantern Doctor \u2014 Diagnostics")));
647
827
  console.log(pc10.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
648
828
  let issuesFound = 0;
829
+ const config2 = loadConfig();
830
+ const registry = new ProviderRegistry();
831
+ const skillManager = new SkillManager();
649
832
  const platformName = process.platform === "darwin" ? `macOS (${process.arch === "arm64" ? "Apple Silicon" : "Intel"})` : process.platform === "win32" ? `Windows (${process.arch})` : `${process.platform} (${process.arch})`;
650
833
  console.log(`${pc10.green("\u2713")} Platform / OS: ${platformName}`);
651
834
  const nodeVer = process.version;
@@ -656,28 +839,37 @@ async function doctorCommand() {
656
839
  issuesFound++;
657
840
  console.log(`${pc10.red("\u2717")} Node.js Runtime: ${nodeVer} (Node 18+ required)`);
658
841
  }
659
- const apiKey = process.env.GEMINI_API_KEY;
660
- if (!apiKey || apiKey.trim().length === 0) {
842
+ const activeProviderId = config2.activeProvider || "gemini";
843
+ const activeProvider = registry.getProvider(activeProviderId);
844
+ const keys = listConfiguredKeys();
845
+ console.log(`${pc10.green("\u2713")} Active Provider: ${pc10.bold(activeProvider.displayName)} (${activeProviderId})`);
846
+ console.log(`${pc10.green("\u2713")} Active Model: ${config2.defaultModel}`);
847
+ const activeKeyInfo = keys[activeProviderId];
848
+ if (!activeKeyInfo || !activeKeyInfo.configured) {
661
849
  issuesFound++;
662
- console.log(`${pc10.red("\u2717")} GEMINI_API_KEY: Missing!`);
663
- console.log(pc10.yellow(' \u2192 To fix: export GEMINI_API_KEY="your-api-key" from https://aistudio.google.com/app/apikey'));
850
+ console.log(`${pc10.red("\u2717")} API Key (${activeProviderId}): Missing!`);
851
+ console.log(pc10.yellow(` \u2192 To fix: set environment variable or run 'lantern keys set ${activeProviderId}'`));
664
852
  } else {
665
- const masked = apiKey.length > 8 ? `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}` : "configured";
666
- console.log(`${pc10.green("\u2713")} GEMINI_API_KEY: Found (${masked})`);
853
+ const src = activeKeyInfo.source ? `[${activeKeyInfo.source}]` : "";
854
+ console.log(`${pc10.green("\u2713")} API Key (${activeProviderId}): Found (${activeKeyInfo.maskedKey}) ${src}`);
667
855
  try {
668
- const ai = new GoogleGenAI({ apiKey: apiKey.trim() });
669
- const testModel = "gemini-3.8-flash";
670
- await ai.models.countTokens({
671
- model: testModel,
672
- contents: "test"
673
- });
674
- console.log(`${pc10.green("\u2713")} Gemini API Auth: Verified (contacted '${testModel}')`);
856
+ const pingRes = await activeProvider.testConnection();
857
+ if (pingRes.success) {
858
+ console.log(`${pc10.green("\u2713")} Provider Auth & Ping: Verified (${pingRes.latencyMs}ms latency)`);
859
+ } else {
860
+ issuesFound++;
861
+ console.log(`${pc10.red("\u2717")} Provider Auth & Ping: Failed: ${pingRes.error}`);
862
+ }
675
863
  } catch (err) {
676
864
  issuesFound++;
677
- console.log(`${pc10.red("\u2717")} Gemini API Auth: Failed to contact API: ${err.message}`);
678
- console.log(pc10.yellow(" \u2192 Verify your API key at https://aistudio.google.com/app/apikey"));
865
+ console.log(`${pc10.red("\u2717")} Provider Auth & Ping: Error: ${err.message}`);
679
866
  }
680
867
  }
868
+ const otherConfigured = Object.entries(keys).filter(([p, k]) => p !== activeProviderId && k.configured);
869
+ if (otherConfigured.length > 0) {
870
+ const others = otherConfigured.map(([p]) => p).join(", ");
871
+ console.log(`${pc10.cyan("\u2139")} Additional Providers: Configured (${others})`);
872
+ }
681
873
  try {
682
874
  const gitVer = execSync("git --version", { stdio: "pipe" }).toString().trim();
683
875
  console.log(`${pc10.green("\u2713")} Git CLI: Available (${gitVer})`);
@@ -686,30 +878,32 @@ async function doctorCommand() {
686
878
  }
687
879
  try {
688
880
  const configFile = getConfigFilePath();
689
- const config2 = loadConfig();
690
- console.log(`${pc10.green("\u2713")} Config File: ${configFile} (Model: ${config2.defaultModel})`);
881
+ console.log(`${pc10.green("\u2713")} Config File: ${configFile} (Timeout: ${config2.timeoutMs}ms, TTFT: ${config2.firstTokenTimeoutMs}ms)`);
691
882
  } catch (err) {
692
883
  issuesFound++;
693
- console.log(`${pc10.red("\u2717")} Config File: Invalid config: ${err.message}`);
884
+ console.log(`${pc10.red("\u2717")} Config File: Invalid: ${err.message}`);
694
885
  }
695
886
  try {
696
887
  const sessionsDir = getSessionsDir();
697
- const count = fs2.readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).length;
888
+ const count = fs2.existsSync(sessionsDir) ? fs2.readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).length : 0;
698
889
  console.log(`${pc10.green("\u2713")} Session Storage: ${sessionsDir} (${count} stored session(s))`);
699
890
  } catch (err) {
700
891
  issuesFound++;
701
- console.log(`${pc10.red("\u2717")} Session Storage: Failed to access: ${err.message}`);
892
+ console.log(`${pc10.red("\u2717")} Session Storage: Failed: ${err.message}`);
893
+ }
894
+ const installedSkills = skillManager.listSkills();
895
+ if (installedSkills.length > 0) {
896
+ const activeCount = installedSkills.filter((s) => s.enabled).length;
897
+ console.log(`${pc10.green("\u2713")} Skills: ${installedSkills.length} installed (${activeCount} active default)`);
898
+ } else {
899
+ console.log(`${pc10.cyan("\u2139")} Skills: None installed (add with: lantern skills add <path>)`);
702
900
  }
703
901
  const mcpServers = loadMcpConfig();
704
902
  const serverCount = Object.keys(mcpServers).length;
705
903
  if (serverCount === 0) {
706
- console.log(`${pc10.cyan("\u2139")} MCP Configuration: No external MCP servers configured (optional)`);
904
+ console.log(`${pc10.cyan("\u2139")} MCP Servers: No external MCP servers configured (optional)`);
707
905
  } else {
708
- console.log(`${pc10.green("\u2713")} MCP Configuration: ${serverCount} server(s) configured`);
709
- for (const [name, s] of Object.entries(mcpServers)) {
710
- const status = s.disabled ? pc10.dim("(disabled)") : pc10.green("(enabled)");
711
- console.log(` \u2022 ${name}: ${s.command} ${(s.args || []).join(" ")} ${status}`);
712
- }
906
+ console.log(`${pc10.green("\u2713")} MCP Servers: ${serverCount} server(s) configured`);
713
907
  }
714
908
  const cwd = process.cwd();
715
909
  const instructions = loadProjectInstructions(cwd);
@@ -826,39 +1020,413 @@ async function mcpCommand(action, serverName, command, args = []) {
826
1020
  process.exit(1);
827
1021
  }
828
1022
 
1023
+ // src/commands/providersCmd.ts
1024
+ import pc12 from "picocolors";
1025
+ import readline2 from "readline";
1026
+ function promptLine(query, hide = false) {
1027
+ const rl = readline2.createInterface({
1028
+ input: process.stdin,
1029
+ output: process.stdout
1030
+ });
1031
+ return new Promise((resolve) => {
1032
+ rl.question(query, (answer) => {
1033
+ rl.close();
1034
+ resolve(answer.trim());
1035
+ });
1036
+ });
1037
+ }
1038
+ async function providersCommand(action, target, options) {
1039
+ const registry = new ProviderRegistry();
1040
+ const subAction = (action || "list").toLowerCase();
1041
+ if (subAction === "list") {
1042
+ const providers = registry.listProviders();
1043
+ console.log("\n" + pc12.bold(pc12.yellow("\u{1F3EE} Configured AI Providers")));
1044
+ console.log(pc12.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1045
+ console.log(
1046
+ pc12.bold("Provider".padEnd(16)) + pc12.bold("Status".padEnd(16)) + pc12.bold("Default Model".padEnd(30)) + pc12.bold("Key Source")
1047
+ );
1048
+ console.log(pc12.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1049
+ for (const p of providers) {
1050
+ const activeMarker = p.isActive ? pc12.green(" (active)") : "";
1051
+ const nameCol = pc12.bold(p.name) + activeMarker;
1052
+ const statusCol = p.isConfigured ? pc12.green("\u2713 Ready") : pc12.yellow("! Needs Key");
1053
+ const keyInfo = getApiKey(p.id);
1054
+ const sourceCol = keyInfo.source ? pc12.cyan(`[${keyInfo.source}]`) : pc12.dim("(missing)");
1055
+ console.log(
1056
+ nameCol.padEnd(p.isActive ? 25 : 16) + statusCol.padEnd(25) + p.defaultModel.padEnd(30) + sourceCol
1057
+ );
1058
+ }
1059
+ console.log(pc12.dim("\nTo set default: lantern providers set-default <name>"));
1060
+ console.log(pc12.dim("To test connection: lantern providers test [name]"));
1061
+ console.log(pc12.dim("To add custom provider: lantern providers add <name>\n"));
1062
+ return;
1063
+ }
1064
+ if (subAction === "set-default" || subAction === "use") {
1065
+ if (!target) {
1066
+ console.error(pc12.red("Error: Missing provider name. Usage: lantern providers set-default <name>"));
1067
+ process.exit(1);
1068
+ }
1069
+ const norm = target.toLowerCase();
1070
+ if (!registry.hasProvider(norm)) {
1071
+ console.error(pc12.red(`Error: Provider '${target}' is not registered.`));
1072
+ process.exit(1);
1073
+ }
1074
+ setConfigValue("activeProvider", norm);
1075
+ const p = registry.getProvider(norm);
1076
+ setConfigValue("defaultModel", p.defaultModel);
1077
+ console.log(pc12.green(`\u2713 Set active provider to '${p.displayName}' (default model: ${p.defaultModel})`));
1078
+ return;
1079
+ }
1080
+ if (subAction === "test") {
1081
+ const targetProvider = target || loadConfig().activeProvider || "gemini";
1082
+ const p = registry.getProvider(targetProvider);
1083
+ console.log(`Testing connection to ${pc12.cyan(p.displayName)}...`);
1084
+ const result = await p.testConnection();
1085
+ if (result.success) {
1086
+ console.log(pc12.green(`\u2713 Connection verified! Latency: ${result.latencyMs}ms`));
1087
+ if (result.modelsCount !== void 0) {
1088
+ console.log(pc12.dim(` Available models discovered: ${result.modelsCount}`));
1089
+ }
1090
+ } else {
1091
+ console.log(pc12.red(`\u2717 Connection failed: ${result.error}`));
1092
+ console.log(pc12.yellow(` Check your credentials with 'lantern keys list'`));
1093
+ process.exit(1);
1094
+ }
1095
+ return;
1096
+ }
1097
+ if (subAction === "add") {
1098
+ const name = target || await promptLine("Provider ID / Name: ");
1099
+ if (!name) {
1100
+ console.error(pc12.red("Error: Provider name is required."));
1101
+ process.exit(1);
1102
+ }
1103
+ const baseUrl = options?.baseUrl || await promptLine("Base URL (e.g. http://localhost:11434/v1): ");
1104
+ const defaultModel = options?.model || await promptLine("Default Model ID (e.g. llama3, qwen2.5): ");
1105
+ const apiKey = options?.apiKey || await promptLine("API Key (optional, press Enter to skip): ");
1106
+ registry.registerCustomProvider({
1107
+ id: name.toLowerCase(),
1108
+ name,
1109
+ type: "custom",
1110
+ baseUrl,
1111
+ defaultModel: defaultModel || "default"
1112
+ });
1113
+ if (apiKey) {
1114
+ setApiKey(name.toLowerCase(), apiKey);
1115
+ }
1116
+ console.log(pc12.green(`\u2713 Registered custom OpenAI-compatible provider '${name}'`));
1117
+ return;
1118
+ }
1119
+ if (subAction === "remove" || subAction === "rm") {
1120
+ if (!target) {
1121
+ console.error(pc12.red("Error: Missing provider name. Usage: lantern providers remove <name>"));
1122
+ process.exit(1);
1123
+ }
1124
+ const removed = registry.removeCustomProvider(target);
1125
+ if (removed) {
1126
+ console.log(pc12.green(`\u2713 Removed provider '${target}'`));
1127
+ } else {
1128
+ console.log(pc12.red(`Cannot remove built-in provider '${target}'. Only custom providers can be removed.`));
1129
+ process.exit(1);
1130
+ }
1131
+ return;
1132
+ }
1133
+ console.error(pc12.red(`Unknown action '${action}'. Available: list, set-default, test, add, remove`));
1134
+ process.exit(1);
1135
+ }
1136
+
1137
+ // src/commands/modelsCmd.ts
1138
+ import pc13 from "picocolors";
1139
+ async function modelsCommand(action, modelId, options) {
1140
+ const registry = new ProviderRegistry();
1141
+ const config2 = loadConfig();
1142
+ const subAction = (action || "list").toLowerCase();
1143
+ const providerId = options?.provider || config2.activeProvider || "gemini";
1144
+ const provider = registry.getProvider(providerId);
1145
+ if (subAction === "list") {
1146
+ if (!options?.json) {
1147
+ console.log(`
1148
+ Fetching models for ${pc13.cyan(provider.displayName)}...`);
1149
+ }
1150
+ const models = await provider.listModels(Boolean(options?.refresh));
1151
+ if (options?.json) {
1152
+ console.log(JSON.stringify(models, null, 2));
1153
+ return;
1154
+ }
1155
+ console.log("\n" + pc13.bold(pc13.yellow(`\u{1F3EE} Available Models for ${provider.displayName}`)));
1156
+ console.log(pc13.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1157
+ console.log(
1158
+ pc13.bold("Model ID".padEnd(36)) + pc13.bold("Tools".padEnd(8)) + pc13.bold("Vision".padEnd(8)) + pc13.bold("Reasoning".padEnd(12)) + pc13.bold("Context Window")
1159
+ );
1160
+ console.log(pc13.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1161
+ const currentModel = config2.defaultModel;
1162
+ for (const m of models) {
1163
+ const isCurrent = m.id === currentModel;
1164
+ const marker = isCurrent ? pc13.green(" (active)") : "";
1165
+ const idCol = (m.id + marker).slice(0, 34);
1166
+ const toolsCol = m.capabilities.tools ? pc13.green("\u2713") : pc13.dim("\u2717");
1167
+ const visionCol = m.capabilities.images ? pc13.green("\u2713") : pc13.dim("\u2717");
1168
+ const reasoningCol = m.capabilities.reasoning ? pc13.magenta("\u2713") : pc13.dim("\u2717");
1169
+ const ctxCol = m.contextWindow ? `${(m.contextWindow / 1e3).toFixed(0)}k` : pc13.dim("standard");
1170
+ console.log(
1171
+ idCol.padEnd(isCurrent ? 45 : 36) + toolsCol.padEnd(16) + visionCol.padEnd(16) + reasoningCol.padEnd(20) + ctxCol
1172
+ );
1173
+ }
1174
+ console.log(pc13.dim("\nTo set active model: lantern model set <model-id>"));
1175
+ console.log(pc13.dim(`Example: lantern model set ${models[0]?.id || "gemini-3.8-flash"}
1176
+ `));
1177
+ return;
1178
+ }
1179
+ if (subAction === "set") {
1180
+ if (!modelId) {
1181
+ console.error(pc13.red("Error: Missing model ID. Usage: lantern model set <model-id> [--provider <provider>]"));
1182
+ process.exit(1);
1183
+ }
1184
+ if (options?.provider) {
1185
+ setConfigValue("activeProvider", options.provider.toLowerCase());
1186
+ }
1187
+ setConfigValue("defaultModel", modelId.trim());
1188
+ console.log(pc13.green(`\u2713 Set active model to '${modelId.trim()}' (provider: ${provider.displayName})`));
1189
+ return;
1190
+ }
1191
+ console.error(pc13.red(`Unknown action '${action}'. Available: list, set`));
1192
+ process.exit(1);
1193
+ }
1194
+
1195
+ // src/commands/keysCmd.ts
1196
+ import readline3 from "readline";
1197
+ import pc14 from "picocolors";
1198
+ function promptPassword(prompt) {
1199
+ const rl = readline3.createInterface({
1200
+ input: process.stdin,
1201
+ output: process.stdout
1202
+ });
1203
+ return new Promise((resolve) => {
1204
+ rl.question(prompt, (answer) => {
1205
+ rl.close();
1206
+ resolve(answer.trim());
1207
+ });
1208
+ });
1209
+ }
1210
+ async function keysCommand(action, provider, key) {
1211
+ const subAction = (action || "list").toLowerCase();
1212
+ if (subAction === "list") {
1213
+ const keys = listConfiguredKeys();
1214
+ console.log("\n" + pc14.bold(pc14.yellow("\u{1F3EE} Configured Provider API Keys")));
1215
+ console.log(pc14.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1216
+ console.log(
1217
+ pc14.bold("Provider".padEnd(16)) + pc14.bold("Source".padEnd(14)) + pc14.bold("Masked Key")
1218
+ );
1219
+ console.log(pc14.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1220
+ for (const [p, info] of Object.entries(keys)) {
1221
+ const srcText = info.source ? pc14.cyan(`[${info.source}]`) : pc14.dim("none");
1222
+ console.log(
1223
+ pc14.bold(p.padEnd(16)) + srcText.padEnd(22) + (info.configured ? pc14.green(info.maskedKey) : pc14.dim("(not configured)"))
1224
+ );
1225
+ }
1226
+ console.log(pc14.dim("\nTo set key: lantern keys set <provider> [key]"));
1227
+ console.log(pc14.dim("To remove key: lantern keys remove <provider>\n"));
1228
+ return;
1229
+ }
1230
+ if (subAction === "set") {
1231
+ if (!provider) {
1232
+ console.error(pc14.red("Error: Missing provider name. Usage: lantern keys set <provider> [key]"));
1233
+ process.exit(1);
1234
+ }
1235
+ let apiKey = key;
1236
+ if (!apiKey) {
1237
+ apiKey = await promptPassword(`Enter API key for '${provider}': `);
1238
+ }
1239
+ if (!apiKey) {
1240
+ console.error(pc14.red("Error: API key cannot be empty."));
1241
+ process.exit(1);
1242
+ }
1243
+ setApiKey(provider, apiKey);
1244
+ console.log(pc14.green(`\u2713 Successfully saved encrypted API key for '${provider}' (${maskApiKey(apiKey)})`));
1245
+ return;
1246
+ }
1247
+ if (subAction === "remove" || subAction === "rm") {
1248
+ if (!provider) {
1249
+ console.error(pc14.red("Error: Missing provider name. Usage: lantern keys remove <provider>"));
1250
+ process.exit(1);
1251
+ }
1252
+ const removed = removeApiKey(provider);
1253
+ if (removed) {
1254
+ console.log(pc14.green(`\u2713 Removed stored key for '${provider}'`));
1255
+ } else {
1256
+ console.log(pc14.yellow(`No stored key found for '${provider}'. (If set in environment variables, unset the env var).`));
1257
+ }
1258
+ return;
1259
+ }
1260
+ console.error(pc14.red(`Unknown action '${action}'. Available: list, set, remove`));
1261
+ process.exit(1);
1262
+ }
1263
+
1264
+ // src/commands/skillsCmd.ts
1265
+ import pc15 from "picocolors";
1266
+ async function skillsCommand(action, target) {
1267
+ const manager = new SkillManager();
1268
+ const subAction = (action || "list").toLowerCase();
1269
+ if (subAction === "list") {
1270
+ const skills = manager.listSkills();
1271
+ console.log("\n" + pc15.bold(pc15.yellow("\u{1F3EE} Installed Code Lantern Skills")));
1272
+ console.log(pc15.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1273
+ console.log(
1274
+ pc15.bold("Skill Name".padEnd(24)) + pc15.bold("Source".padEnd(12)) + pc15.bold("Status".padEnd(14)) + pc15.bold("Description")
1275
+ );
1276
+ console.log(pc15.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1277
+ if (skills.length === 0) {
1278
+ console.log(pc15.dim("No skills currently installed."));
1279
+ console.log(pc15.dim(`
1280
+ Add a skill using: lantern skills add <local-folder-or-git-url>`));
1281
+ console.log(pc15.dim(`User skills folder: ${manager.getUserSkillsDir()}
1282
+ `));
1283
+ return;
1284
+ }
1285
+ for (const s of skills) {
1286
+ const statusText = s.enabled ? pc15.green("\u2713 Active") : pc15.dim("Disabled");
1287
+ const sourceText = s.source === "project" ? pc15.cyan("[project]") : pc15.blue("[user]");
1288
+ console.log(
1289
+ pc15.bold(s.name.padEnd(24)) + sourceText.padEnd(20) + statusText.padEnd(22) + pc15.dim(s.description.slice(0, 40))
1290
+ );
1291
+ }
1292
+ console.log(pc15.dim("\nTo inspect: lantern skills show <name>"));
1293
+ console.log(pc15.dim("To enable: lantern skills enable <name>"));
1294
+ console.log(pc15.dim("To disable: lantern skills disable <name>"));
1295
+ console.log(pc15.dim("To add: lantern skills add <folder-path-or-git-url>\n"));
1296
+ return;
1297
+ }
1298
+ if (subAction === "show") {
1299
+ if (!target) {
1300
+ console.error(pc15.red("Error: Missing skill name. Usage: lantern skills show <name>"));
1301
+ process.exit(1);
1302
+ }
1303
+ const skill = manager.getSkill(target);
1304
+ if (!skill) {
1305
+ console.error(pc15.red(`Error: Skill '${target}' not found.`));
1306
+ process.exit(1);
1307
+ }
1308
+ console.log("\n" + pc15.bold(pc15.yellow(`\u{1F3EE} Skill: ${skill.name}`)));
1309
+ console.log(pc15.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1310
+ console.log(`${pc15.bold("Source:")} ${skill.source}`);
1311
+ console.log(`${pc15.bold("Path:")} ${skill.path}`);
1312
+ console.log(`${pc15.bold("Status:")} ${skill.enabled ? pc15.green("Enabled") : pc15.dim("Disabled")}`);
1313
+ console.log(`${pc15.bold("Description:")} ${skill.description}`);
1314
+ if (skill.references && skill.references.length > 0) {
1315
+ console.log(`${pc15.bold("References:")} ${skill.references.length} reference file(s) available`);
1316
+ }
1317
+ console.log(pc15.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1318
+ console.log("\n" + pc15.cyan("Instructions Content:"));
1319
+ console.log(skill.content.slice(0, 1e3) + (skill.content.length > 1e3 ? "\n... (truncated)" : ""));
1320
+ console.log("");
1321
+ return;
1322
+ }
1323
+ if (subAction === "enable") {
1324
+ if (!target) {
1325
+ console.error(pc15.red("Error: Missing skill name. Usage: lantern skills enable <name>"));
1326
+ process.exit(1);
1327
+ }
1328
+ const skill = manager.getSkill(target);
1329
+ if (!skill) {
1330
+ console.error(pc15.red(`Error: Skill '${target}' not found.`));
1331
+ process.exit(1);
1332
+ }
1333
+ manager.enableSkill(skill.name);
1334
+ console.log(pc15.green(`\u2713 Enabled skill '${skill.name}'`));
1335
+ return;
1336
+ }
1337
+ if (subAction === "disable") {
1338
+ if (!target) {
1339
+ console.error(pc15.red("Error: Missing skill name. Usage: lantern skills disable <name>"));
1340
+ process.exit(1);
1341
+ }
1342
+ manager.disableSkill(target);
1343
+ console.log(pc15.yellow(`\u2713 Disabled skill '${target}'`));
1344
+ return;
1345
+ }
1346
+ if (subAction === "add") {
1347
+ if (!target) {
1348
+ console.error(pc15.red("Error: Missing path or repository URL. Usage: lantern skills add <path-or-git-url>"));
1349
+ process.exit(1);
1350
+ }
1351
+ console.log(`Adding skill from '${target}'...`);
1352
+ const isGit = target.startsWith("http://") || target.startsWith("https://") || target.startsWith("git@");
1353
+ const result = isGit ? manager.addSkillFromGit(target) : manager.addSkillFromPath(target);
1354
+ if (result.success) {
1355
+ console.log(pc15.green(`\u2713 Successfully added and enabled skill '${result.skillName}'`));
1356
+ } else {
1357
+ console.error(pc15.red(`\u2717 Failed to add skill: ${result.error}`));
1358
+ process.exit(1);
1359
+ }
1360
+ return;
1361
+ }
1362
+ if (subAction === "remove" || subAction === "rm") {
1363
+ if (!target) {
1364
+ console.error(pc15.red("Error: Missing skill name. Usage: lantern skills remove <name>"));
1365
+ process.exit(1);
1366
+ }
1367
+ const removed = manager.removeSkill(target);
1368
+ if (removed) {
1369
+ console.log(pc15.green(`\u2713 Removed skill '${target}'`));
1370
+ } else {
1371
+ console.error(pc15.red(`\u2717 Skill '${target}' not found or could not be removed.`));
1372
+ process.exit(1);
1373
+ }
1374
+ return;
1375
+ }
1376
+ console.error(pc15.red(`Unknown action '${action}'. Available: list, show, add, enable, disable, remove`));
1377
+ process.exit(1);
1378
+ }
1379
+
829
1380
  // src/cli.ts
830
1381
  var program = new Command();
831
1382
  var config = loadConfig();
832
- program.name("lantern").description("Code Lantern CLI \u2014 Personal AI coding assistant for the terminal powered by Google Gemini").version("1.0.0");
833
- program.option("-m, --model <model>", "Gemini model ID to use", config.defaultModel || "gemini-3.8-flash").option("-C, --cwd <path>", "Working directory for Code Lantern agent", process.cwd()).option("-s, --session <id>", "Resume or use a specific session ID").option("-a, --attach <files...>", "Attach one or more files/images/documents to the request").option("--yolo", "Enable YOLO mode (auto-approve file edits and shell commands)", false).option("--json", "Output results formatted as JSON", false);
1383
+ program.name("lantern").description("Code Lantern CLI \u2014 Personal AI coding assistant for the terminal with multi-provider and skills support").version("1.0.1");
1384
+ program.option("-p, --provider <provider>", "AI provider to use (gemini, openai, anthropic, openrouter, or custom)", config.activeProvider || "gemini").option("-m, --model <model>", "Model ID to use", config.defaultModel || "gemini-3.8-flash").option("-C, --cwd <path>", "Working directory for Code Lantern agent", process.cwd()).option("-s, --session <id>", "Resume or use a specific session ID").option("-a, --attach <files...>", "Attach one or more files/images/documents to the request").option("--yolo", "Enable YOLO mode (auto-approve file edits and shell commands)", false).option("--json", "Output results formatted as JSON", false).option("--timeout <ms>", "Maximum total request duration in milliseconds (default: 60000)", (val) => parseInt(val, 10)).option("--first-token-timeout <ms>", "Maximum wait time for initial token in milliseconds (default: 30000)", (val) => parseInt(val, 10)).option("--timing", "Display detailed execution and latency timing diagnostics", false).option("--fast", "Enable low-latency streaming mode where supported", false).option("--skill <skills...>", "Activate specific skills by name for this run");
834
1385
  program.command("ask <question...>").description("Ask a single question or task and stream the answer").action(async (questionParts) => {
835
1386
  const opts = program.opts();
836
1387
  const question = questionParts.join(" ");
837
1388
  await askCommand(question, {
1389
+ provider: opts.provider,
838
1390
  model: opts.model,
839
1391
  cwd: opts.cwd,
840
1392
  attach: opts.attach,
841
1393
  yolo: opts.yolo,
842
- json: opts.json
1394
+ json: opts.json,
1395
+ timeout: opts.timeout,
1396
+ firstTokenTimeout: opts.firstTokenTimeout,
1397
+ timing: opts.timing,
1398
+ fast: opts.fast,
1399
+ skill: opts.skill
843
1400
  });
844
1401
  });
845
1402
  program.command("plan <task...>").description("Inspect the codebase and produce a read-only implementation plan").action(async (taskParts) => {
846
1403
  const opts = program.opts();
847
1404
  const task = taskParts.join(" ");
848
1405
  await planCommand(task, {
1406
+ provider: opts.provider,
849
1407
  model: opts.model,
850
1408
  cwd: opts.cwd,
851
1409
  attach: opts.attach,
852
- json: opts.json
1410
+ json: opts.json,
1411
+ timeout: opts.timeout,
1412
+ firstTokenTimeout: opts.firstTokenTimeout,
1413
+ timing: opts.timing,
1414
+ skill: opts.skill
853
1415
  });
854
1416
  });
855
1417
  program.command("resume [sessionId]").description("Continue a saved conversation session").action(async (sessionId) => {
856
1418
  const opts = program.opts();
857
1419
  await resumeCommand(sessionId, {
1420
+ provider: opts.provider,
858
1421
  model: opts.model,
859
1422
  cwd: opts.cwd,
860
1423
  attach: opts.attach,
861
- yolo: opts.yolo
1424
+ yolo: opts.yolo,
1425
+ timeout: opts.timeout,
1426
+ firstTokenTimeout: opts.firstTokenTimeout,
1427
+ timing: opts.timing,
1428
+ fast: opts.fast,
1429
+ skill: opts.skill
862
1430
  });
863
1431
  });
864
1432
  program.command("sessions").description("List, delete, or export saved local sessions").option("-d, --delete <id>", "Delete session by ID").option("-e, --export <id>", "Export session transcript by ID").action(async (cmdOpts) => {
@@ -872,13 +1440,29 @@ program.command("sessions").description("List, delete, or export saved local ses
872
1440
  program.command("config [action] [key] [value]").description("View or update configuration (list, get, set, path)").action(async (action, key, value) => {
873
1441
  await configCommand(action, key, value);
874
1442
  });
875
- program.command("doctor").description("Diagnose API key, configuration, environment, and MCP server health").action(async () => {
1443
+ program.command("doctor").description("Diagnose API keys, configuration, environment, and MCP server health").action(async () => {
876
1444
  await doctorCommand();
877
1445
  });
878
1446
  program.command("mcp [action] [serverName] [command] [args...]").description("Manage configured MCP servers (list, add, remove, enable, disable, test)").allowUnknownOption().action(async (action, serverName, cmd, ...rest) => {
879
1447
  const rawArgs = rest.flat().filter((a) => typeof a === "string");
880
1448
  await mcpCommand(action, serverName, cmd, rawArgs);
881
1449
  });
1450
+ program.command("providers [action] [target]").description("Manage AI providers (list, set-default, test, add, remove)").option("--base-url <url>", "Base URL for custom OpenAI-compatible provider").option("--api-key <key>", "API key for provider").option("--model <model>", "Default model ID for provider").action(async (action, target, cmdOpts) => {
1451
+ await providersCommand(action, target, cmdOpts);
1452
+ });
1453
+ program.command("models [action] [modelId]").alias("model").description("List available models or switch the active model").option("-p, --provider <provider>", "Provider to inspect models for").option("-r, --refresh", "Bypass cache and query live model list", false).option("--json", "Output models list as JSON", false).action(async (action, modelId, cmdOpts) => {
1454
+ const globalOpts = program.opts();
1455
+ const provider = cmdOpts?.provider || globalOpts.provider;
1456
+ const json = Boolean(cmdOpts?.json || globalOpts.json);
1457
+ const refresh = Boolean(cmdOpts?.refresh);
1458
+ await modelsCommand(action, modelId, { provider, refresh, json });
1459
+ });
1460
+ program.command("keys [action] [provider] [key]").description("Securely manage API keys with local AES-256-GCM encryption (list, set, remove)").action(async (action, provider, key) => {
1461
+ await keysCommand(action, provider, key);
1462
+ });
1463
+ program.command("skills [action] [target]").description("Manage portable skills in SKILL.md format (list, show, add, enable, disable, remove)").action(async (action, target) => {
1464
+ await skillsCommand(action, target);
1465
+ });
882
1466
  program.action(async () => {
883
1467
  const opts = program.opts();
884
1468
  if (!process.stdin.isTTY) {
@@ -890,21 +1474,33 @@ program.action(async () => {
890
1474
  const trimmedPiped = pipedData.trim();
891
1475
  if (trimmedPiped) {
892
1476
  await askCommand(trimmedPiped, {
1477
+ provider: opts.provider,
893
1478
  model: opts.model,
894
1479
  cwd: opts.cwd,
895
1480
  attach: opts.attach,
896
1481
  yolo: opts.yolo,
897
- json: opts.json
1482
+ json: opts.json,
1483
+ timeout: opts.timeout,
1484
+ firstTokenTimeout: opts.firstTokenTimeout,
1485
+ timing: opts.timing,
1486
+ fast: opts.fast,
1487
+ skill: opts.skill
898
1488
  });
899
1489
  return;
900
1490
  }
901
1491
  }
902
1492
  await interactiveCommand({
1493
+ provider: opts.provider,
903
1494
  model: opts.model,
904
1495
  cwd: opts.cwd,
905
1496
  session: opts.session,
906
1497
  attach: opts.attach,
907
- yolo: opts.yolo
1498
+ yolo: opts.yolo,
1499
+ timeout: opts.timeout,
1500
+ firstTokenTimeout: opts.firstTokenTimeout,
1501
+ timing: opts.timing,
1502
+ fast: opts.fast,
1503
+ skill: opts.skill
908
1504
  });
909
1505
  });
910
1506
  program.parseAsync(process.argv).catch((err) => {