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/README.md +283 -163
- package/dist/{chunk-PELESOCI.js → chunk-45FEFNMM.js} +877 -154
- package/dist/chunk-45FEFNMM.js.map +1 -0
- package/dist/cli.js +351 -62
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +91 -5
- package/dist/index.js +19 -1
- package/package.json +2 -1
- package/dist/chunk-PELESOCI.js.map +0 -1
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-
|
|
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
|
|
186
|
-
console.log(` ${pc3.cyan("/
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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.
|
|
271
|
+
console.log(pc3.dim(`\u2713 Verified ${p.displayName} connection (${testRes.latencyMs}ms)`));
|
|
234
272
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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.
|
|
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
|
|
453
|
-
const
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
1227
|
-
console.log(pc14.dim("To remove key: lantern keys remove <provider
|
|
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
|
|
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
|
-
|
|
1244
|
-
|
|
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
|
|
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 '${
|
|
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 '${
|
|
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.
|
|
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();
|