codelantern-agent-cli 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  ProviderRegistry,
8
8
  SkillManager,
9
9
  addMcpServer,
10
+ classifyModel,
10
11
  createNewSession,
11
12
  deleteSession,
12
13
  exportSessionMarkdown,
@@ -14,7 +15,9 @@ import {
14
15
  getApiKey,
15
16
  getConfigFilePath,
16
17
  getLanternHomeDir,
18
+ getRecommendedCodingModels,
17
19
  getSessionsDir,
20
+ getStoreName,
18
21
  listConfiguredKeys,
19
22
  listSessions,
20
23
  loadAttachments,
@@ -23,13 +26,14 @@ import {
23
26
  loadProjectInstructions,
24
27
  loadSession,
25
28
  maskApiKey,
29
+ promptMaskedApiKey,
26
30
  removeApiKey,
27
31
  removeMcpServer,
28
32
  saveSession,
29
33
  setApiKey,
30
34
  setConfigValue,
31
35
  toggleMcpServer
32
- } from "./chunk-PELESOCI.js";
36
+ } from "./chunk-45FEFNMM.js";
33
37
 
34
38
  // src/cli.ts
35
39
  import { Command } from "commander";
@@ -69,6 +73,18 @@ var TerminalPrompt = class {
69
73
  constructor() {
70
74
  this.history = loadHistory();
71
75
  }
76
+ async askQuestion(query) {
77
+ const rl = readline.createInterface({
78
+ input: process.stdin,
79
+ output: process.stdout
80
+ });
81
+ return new Promise((resolve) => {
82
+ rl.question(query, (answer) => {
83
+ rl.close();
84
+ resolve(answer.trim());
85
+ });
86
+ });
87
+ }
72
88
  async askPrompt(promptPrefix = pc.yellow("lantern> ")) {
73
89
  const rl = readline.createInterface({
74
90
  input: process.stdin,
@@ -178,12 +194,16 @@ async function handleSlashCommand(input, context) {
178
194
  case "/help":
179
195
  console.log("\n" + pc3.bold(pc3.yellow("\u{1F3EE} Code Lantern CLI \u2014 Help & Commands")));
180
196
  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"));
181
- console.log(pc3.bold("Slash Commands:"));
197
+ console.log(pc3.bold("Slash Commands (Entered inside Lantern session):"));
182
198
  console.log(` ${pc3.cyan("/help")} Show this help overview`);
183
199
  console.log(` ${pc3.cyan("/clear")} Clear the terminal screen`);
184
200
  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`);
201
+ console.log(` ${pc3.cyan("/provider")} List configured providers and select one interactively`);
202
+ console.log(` ${pc3.cyan("/provider <name>")} Switch current session to provider (gemini, openrouter, openai, anthropic)`);
203
+ console.log(` ${pc3.cyan("/provider default <name>")} Set provider as default and switch session`);
204
+ console.log(` ${pc3.cyan("/model")} Discover & browse models for active provider with interactive selection`);
205
+ console.log(` ${pc3.cyan("/model <model-id>")} Switch current session to specified model`);
206
+ console.log(` ${pc3.cyan("/model default <id>")} Set model as default and switch session`);
187
207
  console.log(` ${pc3.cyan("/skills [name]")} List, enable, or disable active skills`);
188
208
  console.log(` ${pc3.cyan("/timing [on|off]")} Toggle request timing diagnostics display`);
189
209
  console.log(` ${pc3.cyan("/plan [on|off]")} Toggle read-only planning mode`);
@@ -222,38 +242,258 @@ async function handleSlashCommand(input, context) {
222
242
  return true;
223
243
  case "/provider": {
224
244
  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})`));
245
+ const trimmedArg = arg.trim();
246
+ const switchProvider = async (target, isDefault = false) => {
247
+ const norm = target.toLowerCase().trim();
248
+ if (!registry.hasProvider(norm)) {
249
+ console.log(pc3.red(`Provider '${target}' not found. Available: ${registry.listProviders().map((p2) => p2.id).join(", ")}`));
250
+ return;
251
+ }
252
+ const p = registry.getProvider(norm);
253
+ if (!p.isConfigured()) {
254
+ console.log("\n" + pc3.yellow(`\u{1F3EE} No API key configured for ${p.displayName} (${norm}).`));
255
+ console.log(pc3.dim(`Lantern will securely save this key to your ${getStoreName()} so you never have to re-enter it.`));
256
+ const key = await promptMaskedApiKey(`Enter API key for '${norm}' (or press Enter to cancel)`);
257
+ if (!key) {
258
+ console.log(pc3.dim("Provider switch cancelled.\n"));
259
+ return;
260
+ }
261
+ setApiKey(norm, key);
262
+ console.log(pc3.green(`\u2713 Saved key for '${p.displayName}' to ${getStoreName()} (${maskApiKey(key)})
263
+ `));
264
+ }
265
+ console.log(pc3.dim(`Verifying connection to ${p.displayName}...`));
266
+ const testRes = await p.testConnection();
267
+ if (!testRes.success) {
268
+ console.log(pc3.yellow(`\u26A0\uFE0F Connection warning for ${p.displayName}: ${testRes.error || "Network error"}`));
269
+ console.log(pc3.dim(` You can test provider connectivity from the shell: lantern providers test ${norm}`));
232
270
  } else {
233
- console.log(pc3.red(`Provider '${arg}' not found. Available: ${registry.listProviders().map((p) => p.id).join(", ")}`));
271
+ console.log(pc3.dim(`\u2713 Verified ${p.displayName} connection (${testRes.latencyMs}ms)`));
234
272
  }
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}`);
273
+ context.setProvider(norm);
274
+ context.setModel(p.defaultModel);
275
+ if (isDefault) {
276
+ setConfigValue("activeProvider", norm);
277
+ console.log(pc3.green(`\u2713 Saved '${p.displayName}' as default provider and switched active session.`));
278
+ } else {
279
+ console.log(pc3.green(`\u2713 Switched provider to '${p.displayName}' (model: ${p.defaultModel}).`));
280
+ }
281
+ if (context.session.turns.length > 0) {
282
+ console.log(pc3.dim(` Preserved ${context.session.turns.length} conversation turn(s) for this session.`));
283
+ }
284
+ };
285
+ if (trimmedArg.toLowerCase().startsWith("default ")) {
286
+ const target = trimmedArg.slice(8).trim();
287
+ if (!target) {
288
+ console.log(pc3.yellow("Usage: /provider default <name> (e.g. /provider default openrouter)"));
289
+ return true;
290
+ }
291
+ await switchProvider(target, true);
292
+ return true;
293
+ }
294
+ if (trimmedArg) {
295
+ await switchProvider(trimmedArg, false);
296
+ return true;
297
+ }
298
+ const providers = registry.listProviders();
299
+ console.log("\n" + pc3.bold(pc3.yellow("\u{1F3EE} Configured AI Providers")));
300
+ 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"));
301
+ console.log(
302
+ pc3.bold("#".padEnd(4)) + pc3.bold("Provider".padEnd(16)) + pc3.bold("Status".padEnd(16)) + pc3.bold("Default Model".padEnd(28)) + pc3.bold("Key Source")
303
+ );
304
+ 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"));
305
+ for (let i = 0; i < providers.length; i++) {
306
+ const p = providers[i];
307
+ const num = pc3.dim(`${i + 1}.`.padEnd(4));
308
+ const activeMarker = p.id === context.provider ? pc3.green(" (active)") : "";
309
+ const nameCol = (p.id + activeMarker).padEnd(20);
310
+ const statusCol = p.isConfigured ? pc3.green("\u2713 Ready").padEnd(18) : pc3.yellow("! Needs Key").padEnd(18);
311
+ const modelCol = pc3.cyan(p.defaultModel).padEnd(30);
312
+ const keyInfo = getApiKey(p.id);
313
+ const srcCol = keyInfo.sourceDescription ? pc3.cyan(`[${keyInfo.sourceDescription}]`) : pc3.dim("(missing)");
314
+ console.log(`${num}${nameCol}${statusCol}${modelCol}${srcCol}`);
315
+ }
316
+ console.log(pc3.dim("\nUsage:"));
317
+ console.log(pc3.dim(" /provider <name> Switch current session to provider"));
318
+ console.log(pc3.dim(" /provider default <name> Set as persistent default and switch session"));
319
+ if (context.askQuestion) {
320
+ const answer = await context.askQuestion(
321
+ pc3.cyan(`
322
+ Select provider [1-${providers.length} or enter name] (Enter to cancel): `)
323
+ );
324
+ if (answer) {
325
+ const num = parseInt(answer, 10);
326
+ if (!isNaN(num) && num >= 1 && num <= providers.length) {
327
+ await switchProvider(providers[num - 1].id, false);
328
+ } else {
329
+ await switchProvider(answer, false);
330
+ }
243
331
  }
244
- console.log(pc3.dim(`Usage: /provider <name> (e.g. /provider openai)`));
245
332
  }
246
333
  return true;
247
334
  }
248
- case "/model":
249
- if (arg) {
250
- context.setModel(arg.trim());
251
- console.log(pc3.green(`\u2713 Model changed to: ${arg.trim()}`));
252
- } else {
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)`));
335
+ case "/model": {
336
+ const registry = new ProviderRegistry();
337
+ const p = registry.getProvider(context.provider);
338
+ const trimmedArg = arg.trim();
339
+ const switchModel = (targetModelId, isDefault = false) => {
340
+ const classification = classifyModel(targetModelId);
341
+ if (classification.category === "specialized") {
342
+ console.log("\n" + pc3.red(`\u274C Cannot use specialized ${classification.specializedType} model '${targetModelId}' for interactive coding.`));
343
+ if (classification.warning) {
344
+ console.log(pc3.dim(` ${classification.warning}`));
345
+ }
346
+ const recs = getRecommendedCodingModels(context.provider);
347
+ console.log(pc3.yellow(`
348
+ Compatible coding models for ${p.displayName}:`));
349
+ for (const rec of recs) {
350
+ console.log(` - ${pc3.bold(pc3.cyan(rec))}`);
351
+ }
352
+ console.log("");
353
+ return;
354
+ }
355
+ if (classification.category === "chat") {
356
+ console.log("\n" + pc3.yellow(`\u26A0\uFE0F Warning: Model '${targetModelId}' does not support tool use / function calling.`));
357
+ console.log(pc3.dim(" Lantern will answer chat questions, but cannot inspect files, edit code, or run shell commands."));
358
+ const recs = getRecommendedCodingModels(context.provider);
359
+ console.log(pc3.dim(` Recommended coding models with tool support: ${recs.join(", ")}
360
+ `));
361
+ }
362
+ context.setModel(targetModelId);
363
+ if (isDefault) {
364
+ setConfigValue("defaultModel", targetModelId);
365
+ console.log(pc3.green(`\u2713 Saved model '${targetModelId}' as default and switched active session.`));
366
+ } else {
367
+ console.log(pc3.green(`\u2713 Switched model to: ${targetModelId}`));
368
+ }
369
+ if (context.session.turns.length > 0) {
370
+ console.log(pc3.dim(` Preserved ${context.session.turns.length} conversation turn(s) for this session.`));
371
+ }
372
+ };
373
+ if (trimmedArg.toLowerCase().startsWith("default ")) {
374
+ const target = trimmedArg.slice(8).trim();
375
+ if (!target) {
376
+ console.log(pc3.yellow("Usage: /model default <model-id> (e.g. /model default gemini-3.8-flash)"));
377
+ return true;
378
+ }
379
+ switchModel(target, true);
380
+ return true;
381
+ }
382
+ if (trimmedArg) {
383
+ switchModel(trimmedArg, false);
384
+ return true;
385
+ }
386
+ if (!p.isConfigured()) {
387
+ console.log("\n" + pc3.yellow(`\u{1F3EE} No API key configured for ${p.displayName} (${p.id}).`));
388
+ console.log(pc3.dim(`Lantern will securely save this key to your ${getStoreName()} so you won't need to enter it again.`));
389
+ const key = await promptMaskedApiKey(`Enter API key for '${p.id}' (or press Enter to cancel)`);
390
+ if (key) {
391
+ setApiKey(p.id, key);
392
+ console.log(pc3.green(`\u2713 Saved key for '${p.displayName}' to ${getStoreName()} (${maskApiKey(key)})
393
+ `));
394
+ }
395
+ }
396
+ console.log(pc3.dim(`
397
+ Discovering available models for ${p.displayName}...`));
398
+ let models = [];
399
+ let discoveryError = null;
400
+ try {
401
+ models = await p.listModels();
402
+ } catch (err) {
403
+ discoveryError = err.message || String(err);
404
+ }
405
+ if (models.length === 0) {
406
+ if (discoveryError) {
407
+ console.log(pc3.yellow(`\u26A0\uFE0F Model discovery unavailable for ${p.displayName}: ${discoveryError}`));
408
+ } else {
409
+ console.log(pc3.yellow(`No models returned by discovery for ${p.displayName}.`));
410
+ }
411
+ console.log(pc3.dim(`You can manually enter any model ID supported by ${p.displayName}.`));
412
+ console.log(`Current model: ${pc3.cyan(context.model)}`);
413
+ if (context.askQuestion) {
414
+ const manual = await context.askQuestion(
415
+ pc3.cyan(`Enter model ID to switch (or press Enter to cancel): `)
416
+ );
417
+ if (manual) {
418
+ switchModel(manual, false);
419
+ }
420
+ }
421
+ return true;
422
+ }
423
+ const codingModels = [];
424
+ const chatOnlyModels = [];
425
+ const specializedModels = [];
426
+ for (const m of models) {
427
+ const c = classifyModel(m.id, m.capabilities, m.description);
428
+ if (c.category === "coding") {
429
+ codingModels.push({ info: m, classification: c });
430
+ } else if (c.category === "chat") {
431
+ chatOnlyModels.push({ info: m, classification: c });
432
+ } else {
433
+ specializedModels.push({ info: m, classification: c });
434
+ }
435
+ }
436
+ console.log("\n" + pc3.bold(pc3.yellow(`\u{1F3EE} Available Models for ${p.displayName}`)));
437
+ 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"));
438
+ console.log(
439
+ pc3.bold("#".padEnd(4)) + pc3.bold("Model ID".padEnd(36)) + pc3.bold("Tools".padEnd(8)) + pc3.bold("Vision".padEnd(8)) + pc3.bold("Reasoning".padEnd(12)) + pc3.bold("Context Window")
440
+ );
441
+ 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"));
442
+ const selectableList = [];
443
+ let index = 1;
444
+ for (const item of codingModels) {
445
+ const m = item.info;
446
+ const isCurrent = m.id === context.model;
447
+ const marker = isCurrent ? pc3.green(" (active)") : "";
448
+ const idCol = (m.id + marker).slice(0, 34).padEnd(36);
449
+ const toolsCol = pc3.green("\u2713").padEnd(8);
450
+ const visionCol = m.capabilities.images ? pc3.green("\u2713").padEnd(8) : pc3.dim("\u2717").padEnd(8);
451
+ const reasoningCol = m.capabilities.reasoning ? pc3.magenta("\u2713").padEnd(12) : pc3.dim("\u2717").padEnd(12);
452
+ const ctxCol = m.contextWindow ? `${(m.contextWindow / 1e3).toFixed(0)}k` : pc3.dim("standard");
453
+ console.log(`${pc3.dim(`${index}.`.padEnd(4))}${idCol}${toolsCol}${visionCol}${reasoningCol}${ctxCol}`);
454
+ selectableList.push({ id: m.id, category: "coding" });
455
+ index++;
456
+ }
457
+ if (chatOnlyModels.length > 0) {
458
+ console.log(pc3.dim("\nChat-Only Models (No tool execution / file editing):"));
459
+ for (const item of chatOnlyModels.slice(0, 5)) {
460
+ const m = item.info;
461
+ const isCurrent = m.id === context.model;
462
+ const marker = isCurrent ? pc3.green(" (active)") : "";
463
+ const idCol = (m.id + marker).slice(0, 34).padEnd(36);
464
+ const toolsCol = pc3.dim("\u2717").padEnd(8);
465
+ const visionCol = m.capabilities.images ? pc3.green("\u2713").padEnd(8) : pc3.dim("\u2717").padEnd(8);
466
+ const reasoningCol = m.capabilities.reasoning ? pc3.magenta("\u2713").padEnd(12) : pc3.dim("\u2717").padEnd(12);
467
+ const ctxCol = m.contextWindow ? `${(m.contextWindow / 1e3).toFixed(0)}k` : pc3.dim("standard");
468
+ console.log(`${pc3.dim(`${index}.`.padEnd(4))}${idCol}${toolsCol}${visionCol}${reasoningCol}${ctxCol}`);
469
+ selectableList.push({ id: m.id, category: "chat" });
470
+ index++;
471
+ }
472
+ }
473
+ if (specializedModels.length > 0) {
474
+ console.log(pc3.dim("\nSpecialized Models (Embeddings, Batch, Audio, Images - not suitable for coding):"));
475
+ const samples = specializedModels.slice(0, 6).map((s) => `${s.info.id} [${s.classification.specializedType || "Specialized"}]`).join(", ");
476
+ console.log(pc3.dim(` ${samples}${specializedModels.length > 6 ? ` (+${specializedModels.length - 6} more)` : ""}`));
477
+ }
478
+ console.log(pc3.dim("\nUsage:"));
479
+ console.log(pc3.dim(" /model <model-id> Switch current session to model"));
480
+ console.log(pc3.dim(" /model default <model-id> Set as persistent default and switch session"));
481
+ if (context.askQuestion) {
482
+ const answer = await context.askQuestion(
483
+ pc3.cyan(`
484
+ Select model [1-${selectableList.length} or enter model ID] (Enter to keep current): `)
485
+ );
486
+ if (answer) {
487
+ const num = parseInt(answer, 10);
488
+ if (!isNaN(num) && num >= 1 && num <= selectableList.length) {
489
+ switchModel(selectableList[num - 1].id, false);
490
+ } else {
491
+ switchModel(answer, false);
492
+ }
493
+ }
255
494
  }
256
495
  return true;
496
+ }
257
497
  case "/skills": {
258
498
  const manager = new SkillManager();
259
499
  const skills = manager.listSkills(context.workspace);
@@ -402,13 +642,27 @@ async function interactiveCommand(options) {
402
642
  }
403
643
  const attachments = loadAttachments(options.attach);
404
644
  printBanner({
405
- version: "1.0.1",
645
+ version: "1.0.2",
406
646
  provider,
407
647
  model,
408
648
  workspace,
409
649
  yolo,
410
650
  planMode
411
651
  });
652
+ const initialKeyInfo = getApiKey(provider);
653
+ if (!initialKeyInfo.key && process.stdin.isTTY) {
654
+ const p = new ProviderRegistry().getProvider(provider);
655
+ console.log(pc4.yellow(`
656
+ \u{1F3EE} No API key configured for ${p.displayName} (${provider}).`));
657
+ console.log(pc4.dim(`Lantern will securely store this key in your ${getStoreName()} so you only enter it once.
658
+ `));
659
+ const entered = await promptMaskedApiKey(`Enter API key for '${provider}'`);
660
+ if (entered) {
661
+ setApiKey(provider, entered);
662
+ console.log(pc4.green(`\u2713 Saved ${p.displayName} API key to ${getStoreName()} (${maskApiKey(entered)})
663
+ `));
664
+ }
665
+ }
412
666
  if (attachments.length > 0) {
413
667
  console.log(pc4.cyan(`\u{1F4CE} Attached ${attachments.length} file(s): ${attachments.map((a) => a.name).join(", ")}
414
668
  `));
@@ -428,29 +682,37 @@ async function interactiveCommand(options) {
428
682
  setProvider: (p) => {
429
683
  provider = p;
430
684
  session.provider = p;
685
+ slashContext.provider = p;
431
686
  },
432
687
  setModel: (m) => {
433
688
  model = m;
434
689
  session.model = m;
690
+ slashContext.model = m;
435
691
  },
436
692
  setYolo: (y) => {
437
693
  yolo = y;
694
+ slashContext.yolo = y;
438
695
  },
439
696
  setPlanMode: (p) => {
440
697
  planMode = p;
698
+ slashContext.planMode = p;
441
699
  },
442
700
  setShowTiming: (t) => {
443
701
  showTiming = t;
702
+ slashContext.showTiming = t;
444
703
  },
445
704
  setActiveSkills: (skills) => {
446
705
  activeSkills = skills;
447
706
  session.activeSkills = skills;
448
- }
707
+ slashContext.activeSkills = skills;
708
+ },
709
+ askQuestion: (q) => promptUI.askQuestion(q)
449
710
  };
450
711
  try {
451
712
  while (true) {
452
- const modeIndicator = planMode ? pc4.blue("[plan] ") : yolo ? pc4.red("[yolo] ") : "";
453
- const promptStr = `${pc4.yellow("lantern")} ${modeIndicator}${pc4.dim("\u276F")} `;
713
+ const provIndicator = pc4.cyan(`(${provider})`);
714
+ const modeIndicator = planMode ? pc4.blue(" [plan]") : yolo ? pc4.red(" [yolo]") : "";
715
+ const promptStr = `${pc4.yellow("lantern")} ${provIndicator}${modeIndicator} ${pc4.dim("\u276F")} `;
454
716
  const input = await promptUI.askPrompt(promptStr);
455
717
  if (input === null || input === "/exit" || input === "/quit") {
456
718
  console.log(pc4.dim("\nGoodbye! Session saved: " + session.id));
@@ -536,6 +798,20 @@ async function askCommand(question, options) {
536
798
  const attachments = loadAttachments(options.attach);
537
799
  const session = createNewSession(workspace, model, `Ask: ${question.slice(0, 40)}`);
538
800
  session.provider = provider;
801
+ const initialKeyInfo = getApiKey(provider);
802
+ if (!initialKeyInfo.key && process.stdin.isTTY && !jsonOutput) {
803
+ const p = new ProviderRegistry().getProvider(provider);
804
+ console.log(pc5.yellow(`
805
+ \u{1F3EE} No API key configured for ${p.displayName} (${provider}).`));
806
+ console.log(pc5.dim(`Lantern will securely store this key in your ${getStoreName()} so you only enter it once.
807
+ `));
808
+ const entered = await promptMaskedApiKey(`Enter API key for '${provider}'`);
809
+ if (entered) {
810
+ setApiKey(provider, entered);
811
+ console.log(pc5.green(`\u2713 Saved ${p.displayName} API key to ${getStoreName()} (${maskApiKey(entered)})
812
+ `));
813
+ }
814
+ }
539
815
  const agent = new LanternAgent();
540
816
  await agent.initMcp();
541
817
  const abortController = new AbortController();
@@ -614,6 +890,20 @@ async function planCommand(task, options) {
614
890
  const attachments = loadAttachments(options.attach);
615
891
  const session = createNewSession(workspace, model, `Plan: ${task.slice(0, 40)}`);
616
892
  session.provider = provider;
893
+ const initialKeyInfo = getApiKey(provider);
894
+ if (!initialKeyInfo.key && process.stdin.isTTY && !jsonOutput) {
895
+ const p = new ProviderRegistry().getProvider(provider);
896
+ console.log(pc6.yellow(`
897
+ \u{1F3EE} No API key configured for ${p.displayName} (${provider}).`));
898
+ console.log(pc6.dim(`Lantern will securely store this key in your ${getStoreName()} so you only enter it once.
899
+ `));
900
+ const entered = await promptMaskedApiKey(`Enter API key for '${provider}'`);
901
+ if (entered) {
902
+ setApiKey(provider, entered);
903
+ console.log(pc6.green(`\u2713 Saved ${p.displayName} API key to ${getStoreName()} (${maskApiKey(entered)})
904
+ `));
905
+ }
906
+ }
617
907
  if (!jsonOutput) {
618
908
  console.log(pc6.bold(pc6.yellow("\n\u{1F3EE} Code Lantern Plan Mode (Read-Only)")));
619
909
  console.log(pc6.dim(`Inspecting workspace with ${provider} (${model})...
@@ -844,13 +1134,14 @@ async function doctorCommand() {
844
1134
  const keys = listConfiguredKeys();
845
1135
  console.log(`${pc10.green("\u2713")} Active Provider: ${pc10.bold(activeProvider.displayName)} (${activeProviderId})`);
846
1136
  console.log(`${pc10.green("\u2713")} Active Model: ${config2.defaultModel}`);
1137
+ console.log(`${pc10.green("\u2713")} Credential Storage: ${getStoreName()}`);
847
1138
  const activeKeyInfo = keys[activeProviderId];
848
1139
  if (!activeKeyInfo || !activeKeyInfo.configured) {
849
1140
  issuesFound++;
850
1141
  console.log(`${pc10.red("\u2717")} API Key (${activeProviderId}): Missing!`);
851
1142
  console.log(pc10.yellow(` \u2192 To fix: set environment variable or run 'lantern keys set ${activeProviderId}'`));
852
1143
  } else {
853
- const src = activeKeyInfo.source ? `[${activeKeyInfo.source}]` : "";
1144
+ const src = activeKeyInfo.sourceDescription ? `[${activeKeyInfo.sourceDescription}]` : "";
854
1145
  console.log(`${pc10.green("\u2713")} API Key (${activeProviderId}): Found (${activeKeyInfo.maskedKey}) ${src}`);
855
1146
  try {
856
1147
  const pingRes = await activeProvider.testConnection();
@@ -1051,7 +1342,7 @@ async function providersCommand(action, target, options) {
1051
1342
  const nameCol = pc12.bold(p.name) + activeMarker;
1052
1343
  const statusCol = p.isConfigured ? pc12.green("\u2713 Ready") : pc12.yellow("! Needs Key");
1053
1344
  const keyInfo = getApiKey(p.id);
1054
- const sourceCol = keyInfo.source ? pc12.cyan(`[${keyInfo.source}]`) : pc12.dim("(missing)");
1345
+ const sourceCol = keyInfo.sourceDescription ? pc12.cyan(`[${keyInfo.sourceDescription}]`) : pc12.dim("(missing)");
1055
1346
  console.log(
1056
1347
  nameCol.padEnd(p.isActive ? 25 : 16) + statusCol.padEnd(25) + p.defaultModel.padEnd(30) + sourceCol
1057
1348
  );
@@ -1102,7 +1393,7 @@ async function providersCommand(action, target, options) {
1102
1393
  }
1103
1394
  const baseUrl = options?.baseUrl || await promptLine("Base URL (e.g. http://localhost:11434/v1): ");
1104
1395
  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): ");
1396
+ const apiKey = options?.apiKey || await promptMaskedApiKey("API Key (optional, press Enter to skip)");
1106
1397
  registry.registerCustomProvider({
1107
1398
  id: name.toLowerCase(),
1108
1399
  name,
@@ -1193,38 +1484,27 @@ Fetching models for ${pc13.cyan(provider.displayName)}...`);
1193
1484
  }
1194
1485
 
1195
1486
  // src/commands/keysCmd.ts
1196
- import readline3 from "readline";
1197
1487
  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
1488
  async function keysCommand(action, provider, key) {
1211
1489
  const subAction = (action || "list").toLowerCase();
1212
1490
  if (subAction === "list") {
1213
1491
  const keys = listConfiguredKeys();
1214
1492
  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"));
1493
+ console.log(pc14.dim(`Storage: ${getStoreName()}`));
1494
+ 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1216
1495
  console.log(
1217
- pc14.bold("Provider".padEnd(16)) + pc14.bold("Source".padEnd(14)) + pc14.bold("Masked Key")
1496
+ pc14.bold("Provider".padEnd(16)) + pc14.bold("Source".padEnd(28)) + pc14.bold("Masked Key")
1218
1497
  );
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"));
1498
+ 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1220
1499
  for (const [p, info] of Object.entries(keys)) {
1221
- const srcText = info.source ? pc14.cyan(`[${info.source}]`) : pc14.dim("none");
1500
+ const srcText = info.configured ? info.source === "env" ? pc14.cyan(info.sourceDescription) : pc14.blue(info.sourceDescription) : pc14.dim("none");
1222
1501
  console.log(
1223
- pc14.bold(p.padEnd(16)) + srcText.padEnd(22) + (info.configured ? pc14.green(info.maskedKey) : pc14.dim("(not configured)"))
1502
+ pc14.bold(p.padEnd(16)) + srcText.padEnd(36) + (info.configured ? pc14.green(info.maskedKey) : pc14.dim("(not configured)"))
1224
1503
  );
1225
1504
  }
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"));
1505
+ console.log(pc14.dim("\nTo set key: lantern keys set <provider>"));
1506
+ console.log(pc14.dim("To remove key: lantern keys remove <provider>"));
1507
+ console.log(pc14.dim("Note: Keys are securely stored in your native OS credential store and reused automatically.\n"));
1228
1508
  return;
1229
1509
  }
1230
1510
  if (subAction === "set") {
@@ -1232,16 +1512,23 @@ async function keysCommand(action, provider, key) {
1232
1512
  console.error(pc14.red("Error: Missing provider name. Usage: lantern keys set <provider> [key]"));
1233
1513
  process.exit(1);
1234
1514
  }
1515
+ const norm = provider.toLowerCase().trim();
1235
1516
  let apiKey = key;
1236
1517
  if (!apiKey) {
1237
- apiKey = await promptPassword(`Enter API key for '${provider}': `);
1518
+ apiKey = await promptMaskedApiKey(`Enter API key for '${norm}'`);
1238
1519
  }
1239
1520
  if (!apiKey) {
1240
1521
  console.error(pc14.red("Error: API key cannot be empty."));
1241
1522
  process.exit(1);
1242
1523
  }
1243
- setApiKey(provider, apiKey);
1244
- console.log(pc14.green(`\u2713 Successfully saved encrypted API key for '${provider}' (${maskApiKey(apiKey)})`));
1524
+ try {
1525
+ setApiKey(norm, apiKey);
1526
+ console.log(pc14.green(`\u2713 Successfully saved API key for '${norm}' to ${getStoreName()} (${maskApiKey(apiKey)})`));
1527
+ console.log(pc14.dim(" Lantern will automatically reuse this key in future sessions and commands."));
1528
+ } catch (err) {
1529
+ console.error(pc14.red(`Error saving key: ${err.message}`));
1530
+ process.exit(1);
1531
+ }
1245
1532
  return;
1246
1533
  }
1247
1534
  if (subAction === "remove" || subAction === "rm") {
@@ -1249,11 +1536,13 @@ async function keysCommand(action, provider, key) {
1249
1536
  console.error(pc14.red("Error: Missing provider name. Usage: lantern keys remove <provider>"));
1250
1537
  process.exit(1);
1251
1538
  }
1252
- const removed = removeApiKey(provider);
1539
+ const norm = provider.toLowerCase().trim();
1540
+ const removed = removeApiKey(norm);
1253
1541
  if (removed) {
1254
- console.log(pc14.green(`\u2713 Removed stored key for '${provider}'`));
1542
+ console.log(pc14.green(`\u2713 Removed stored key for '${norm}' from ${getStoreName()}`));
1255
1543
  } else {
1256
- console.log(pc14.yellow(`No stored key found for '${provider}'. (If set in environment variables, unset the env var).`));
1544
+ console.log(pc14.yellow(`No stored key found for '${norm}' in ${getStoreName()}.`));
1545
+ console.log(pc14.dim(`(If set via environment variable, unset ${norm.toUpperCase()}_API_KEY).`));
1257
1546
  }
1258
1547
  return;
1259
1548
  }
@@ -1380,7 +1669,7 @@ Add a skill using: lantern skills add <local-folder-or-git-url>`));
1380
1669
  // src/cli.ts
1381
1670
  var program = new Command();
1382
1671
  var config = loadConfig();
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");
1672
+ program.name("lantern").description("Code Lantern CLI \u2014 Personal AI coding assistant for the terminal with multi-provider and skills support").version("1.0.2");
1384
1673
  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");
1385
1674
  program.command("ask <question...>").description("Ask a single question or task and stream the answer").action(async (questionParts) => {
1386
1675
  const opts = program.opts();