codelantern-agent-cli 1.0.0 → 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 +294 -223
- package/dist/chunk-45FEFNMM.js +5030 -0
- package/dist/chunk-45FEFNMM.js.map +1 -0
- package/dist/cli.js +974 -89
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +371 -23
- package/dist/index.js +64 -7
- package/package.json +3 -2
- package/dist/chunk-Y3PEOLTZ.js +0 -2513
- package/dist/chunk-Y3PEOLTZ.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ALLOWED_CONFIG_KEYS,
|
|
4
|
+
CancelledError,
|
|
5
|
+
LanternAgent,
|
|
4
6
|
McpManager,
|
|
5
|
-
|
|
7
|
+
ProviderRegistry,
|
|
8
|
+
SkillManager,
|
|
6
9
|
addMcpServer,
|
|
10
|
+
classifyModel,
|
|
7
11
|
createNewSession,
|
|
8
12
|
deleteSession,
|
|
9
13
|
exportSessionMarkdown,
|
|
10
14
|
formatErrorMessage,
|
|
15
|
+
getApiKey,
|
|
11
16
|
getConfigFilePath,
|
|
12
17
|
getLanternHomeDir,
|
|
18
|
+
getRecommendedCodingModels,
|
|
13
19
|
getSessionsDir,
|
|
20
|
+
getStoreName,
|
|
21
|
+
listConfiguredKeys,
|
|
14
22
|
listSessions,
|
|
15
23
|
loadAttachments,
|
|
16
24
|
loadConfig,
|
|
17
25
|
loadMcpConfig,
|
|
18
26
|
loadProjectInstructions,
|
|
19
27
|
loadSession,
|
|
28
|
+
maskApiKey,
|
|
29
|
+
promptMaskedApiKey,
|
|
30
|
+
removeApiKey,
|
|
20
31
|
removeMcpServer,
|
|
21
32
|
saveSession,
|
|
33
|
+
setApiKey,
|
|
22
34
|
setConfigValue,
|
|
23
35
|
toggleMcpServer
|
|
24
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-45FEFNMM.js";
|
|
25
37
|
|
|
26
38
|
// src/cli.ts
|
|
27
39
|
import { Command } from "commander";
|
|
@@ -61,6 +73,18 @@ var TerminalPrompt = class {
|
|
|
61
73
|
constructor() {
|
|
62
74
|
this.history = loadHistory();
|
|
63
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
|
+
}
|
|
64
88
|
async askPrompt(promptPrefix = pc.yellow("lantern> ")) {
|
|
65
89
|
const rl = readline.createInterface({
|
|
66
90
|
input: process.stdin,
|
|
@@ -144,12 +168,13 @@ function printBanner(options) {
|
|
|
144
168
|
| | / _ \\ / _\` |/ _ \\ | | / _\` | '_ \\ | __/ _ \\ '__| '_ \\
|
|
145
169
|
| |__| (_) | (_| | __/ | |__| (_| | | | || || __/ | | | | |
|
|
146
170
|
\\____\\___/ \\__,_|\\___| |_____\\__,_|_| |_(_)__\\___|_| |_| |_|
|
|
147
|
-
\u{1F3EE} Personal AI Coding Assistant for Terminal (
|
|
171
|
+
\u{1F3EE} Personal AI Coding Assistant for Terminal (Multi-Provider)
|
|
148
172
|
`;
|
|
149
173
|
console.log(pc2.yellow(logo));
|
|
150
174
|
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"));
|
|
175
|
+
const providerDisplay = options.provider ? `${pc2.bold("Provider:")} ${pc2.magenta(options.provider)} ` : "";
|
|
151
176
|
console.log(`${pc2.bold("Product:")} Code Lantern CLI (${pc2.cyan("lantern")}) ${pc2.bold("Version:")} ${options.version}`);
|
|
152
|
-
console.log(`${pc2.bold("
|
|
177
|
+
console.log(`${providerDisplay}${pc2.bold("Model:")} ${pc2.green(options.model)}`);
|
|
153
178
|
console.log(`${pc2.bold("Workspace:")} ${pc2.blue(options.workspace)}`);
|
|
154
179
|
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
180
|
console.log(`${pc2.bold("Status:")} ${modeBadge}`);
|
|
@@ -169,11 +194,18 @@ async function handleSlashCommand(input, context) {
|
|
|
169
194
|
case "/help":
|
|
170
195
|
console.log("\n" + pc3.bold(pc3.yellow("\u{1F3EE} Code Lantern CLI \u2014 Help & Commands")));
|
|
171
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"));
|
|
172
|
-
console.log(pc3.bold("Slash Commands:"));
|
|
197
|
+
console.log(pc3.bold("Slash Commands (Entered inside Lantern session):"));
|
|
173
198
|
console.log(` ${pc3.cyan("/help")} Show this help overview`);
|
|
174
199
|
console.log(` ${pc3.cyan("/clear")} Clear the terminal screen`);
|
|
175
|
-
console.log(` ${pc3.cyan("/status")} Display
|
|
176
|
-
console.log(` ${pc3.cyan("/
|
|
200
|
+
console.log(` ${pc3.cyan("/status")} Display session, provider, model, and skill status`);
|
|
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`);
|
|
207
|
+
console.log(` ${pc3.cyan("/skills [name]")} List, enable, or disable active skills`);
|
|
208
|
+
console.log(` ${pc3.cyan("/timing [on|off]")} Toggle request timing diagnostics display`);
|
|
177
209
|
console.log(` ${pc3.cyan("/plan [on|off]")} Toggle read-only planning mode`);
|
|
178
210
|
console.log(` ${pc3.cyan("/yolo [on|off]")} Toggle YOLO auto-approval mode`);
|
|
179
211
|
console.log(` ${pc3.cyan("/sessions")} List saved local sessions`);
|
|
@@ -181,25 +213,12 @@ async function handleSlashCommand(input, context) {
|
|
|
181
213
|
console.log(` ${pc3.cyan("/export [md|json]")} Export current conversation transcript`);
|
|
182
214
|
console.log(` ${pc3.cyan("/exit")} or ${pc3.cyan("/quit")} Exit Code Lantern CLI`);
|
|
183
215
|
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
216
|
console.log(pc3.bold("Keyboard Shortcuts:"));
|
|
198
217
|
console.log(` ${pc3.dim("Enter")} Submit prompt`);
|
|
199
218
|
console.log(` ${pc3.dim("\\ + Enter")} Continue prompt on next line (multiline)`);
|
|
200
219
|
console.log(` ${pc3.dim('""" ... """')} Multiline prompt block`);
|
|
201
220
|
console.log(` ${pc3.dim("Up/Down Arrows")} Navigate prompt history`);
|
|
202
|
-
console.log(` ${pc3.dim("Ctrl+C")} Cancel
|
|
221
|
+
console.log(` ${pc3.dim("Ctrl+C")} Cancel active request / double-tap to exit`);
|
|
203
222
|
console.log(` ${pc3.dim("Ctrl+D")} Exit session`);
|
|
204
223
|
console.log("");
|
|
205
224
|
return true;
|
|
@@ -209,22 +228,318 @@ async function handleSlashCommand(input, context) {
|
|
|
209
228
|
case "/status":
|
|
210
229
|
console.log("\n" + pc3.bold(pc3.yellow("\u{1F3EE} Code Lantern Status")));
|
|
211
230
|
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:")}
|
|
213
|
-
console.log(`${pc3.bold("Workspace:")}
|
|
214
|
-
console.log(`${pc3.bold("Active
|
|
215
|
-
console.log(`${pc3.bold("
|
|
216
|
-
console.log(`${pc3.bold("
|
|
217
|
-
console.log(`${pc3.bold("
|
|
218
|
-
console.log(`${pc3.bold("
|
|
231
|
+
console.log(`${pc3.bold("Session ID:")} ${pc3.cyan(context.session.id)}`);
|
|
232
|
+
console.log(`${pc3.bold("Workspace:")} ${pc3.blue(context.workspace)}`);
|
|
233
|
+
console.log(`${pc3.bold("Active Provider:")} ${pc3.magenta(context.provider)}`);
|
|
234
|
+
console.log(`${pc3.bold("Active Model:")} ${pc3.green(context.model)}`);
|
|
235
|
+
console.log(`${pc3.bold("Active Skills:")} ${context.activeSkills.length > 0 ? pc3.yellow(context.activeSkills.join(", ")) : pc3.dim("none")}`);
|
|
236
|
+
console.log(`${pc3.bold("Turns:")} ${context.session.turns.length}`);
|
|
237
|
+
console.log(`${pc3.bold("Total Tokens:")} ${context.session.totalUsage.totalTokens.toLocaleString()}`);
|
|
238
|
+
console.log(`${pc3.bold("Timing Logs:")} ${context.showTiming ? pc3.green("ON") : pc3.dim("OFF")}`);
|
|
239
|
+
console.log(`${pc3.bold("YOLO Mode:")} ${context.yolo ? pc3.red("ON (Auto-approve)") : pc3.green("OFF (Prompt approval)")}`);
|
|
240
|
+
console.log(`${pc3.bold("Plan Mode:")} ${context.planMode ? pc3.blue("ON (Read-only)") : pc3.dim("OFF (Full execution)")}`);
|
|
219
241
|
console.log("");
|
|
220
242
|
return true;
|
|
221
|
-
case "/
|
|
243
|
+
case "/provider": {
|
|
244
|
+
const registry = new ProviderRegistry();
|
|
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}`));
|
|
270
|
+
} else {
|
|
271
|
+
console.log(pc3.dim(`\u2713 Verified ${p.displayName} connection (${testRes.latencyMs}ms)`));
|
|
272
|
+
}
|
|
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
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
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
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
case "/skills": {
|
|
498
|
+
const manager = new SkillManager();
|
|
499
|
+
const skills = manager.listSkills(context.workspace);
|
|
222
500
|
if (arg) {
|
|
223
|
-
|
|
224
|
-
|
|
501
|
+
const target = arg.trim();
|
|
502
|
+
const skill = manager.getSkill(target, context.workspace);
|
|
503
|
+
if (skill) {
|
|
504
|
+
const isCurrentlyActive = context.activeSkills.includes(skill.name);
|
|
505
|
+
if (isCurrentlyActive) {
|
|
506
|
+
context.setActiveSkills(context.activeSkills.filter((s) => s !== skill.name));
|
|
507
|
+
console.log(pc3.yellow(`\u2713 Deactivated skill '${skill.name}' for this session.`));
|
|
508
|
+
} else {
|
|
509
|
+
context.setActiveSkills([...context.activeSkills, skill.name]);
|
|
510
|
+
console.log(pc3.green(`\u2713 Activated skill '${skill.name}' for this session.`));
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
console.log(pc3.red(`Skill '${target}' not found.`));
|
|
514
|
+
}
|
|
225
515
|
} else {
|
|
226
|
-
console.log(
|
|
227
|
-
|
|
516
|
+
console.log("\n" + pc3.bold(pc3.yellow("\u{1F3EE} Code Lantern Skills:")));
|
|
517
|
+
if (skills.length === 0) {
|
|
518
|
+
console.log(pc3.dim(" (No skills currently installed. Add one with: lantern skills add <path>)"));
|
|
519
|
+
} else {
|
|
520
|
+
for (const s of skills) {
|
|
521
|
+
const isSessionActive = context.activeSkills.includes(s.name);
|
|
522
|
+
const status = isSessionActive ? pc3.green("\u25CF ACTIVE") : s.enabled ? pc3.cyan("\u25CB default") : pc3.dim("\u25CB inactive");
|
|
523
|
+
console.log(` ${status} ${pc3.bold(s.name)} - ${pc3.dim(s.description.slice(0, 50))}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
console.log(pc3.dim(`
|
|
527
|
+
Toggle skill for session: /skills <name>
|
|
528
|
+
`));
|
|
529
|
+
}
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
case "/timing":
|
|
533
|
+
if (arg === "off") {
|
|
534
|
+
context.setShowTiming(false);
|
|
535
|
+
console.log(pc3.yellow("\u2713 Timing diagnostics disabled."));
|
|
536
|
+
} else if (arg === "on") {
|
|
537
|
+
context.setShowTiming(true);
|
|
538
|
+
console.log(pc3.green("\u2713 Timing diagnostics enabled."));
|
|
539
|
+
} else {
|
|
540
|
+
const next = !context.showTiming;
|
|
541
|
+
context.setShowTiming(next);
|
|
542
|
+
console.log(next ? pc3.green("\u2713 Timing diagnostics enabled.") : pc3.yellow("\u2713 Timing diagnostics disabled."));
|
|
228
543
|
}
|
|
229
544
|
return true;
|
|
230
545
|
case "/plan":
|
|
@@ -302,9 +617,13 @@ async function handleSlashCommand(input, context) {
|
|
|
302
617
|
async function interactiveCommand(options) {
|
|
303
618
|
const config2 = loadConfig();
|
|
304
619
|
const workspace = options.cwd || process.cwd();
|
|
620
|
+
let provider = options.provider || config2.activeProvider || "gemini";
|
|
305
621
|
let model = options.model || config2.defaultModel || "gemini-3.8-flash";
|
|
306
622
|
let yolo = Boolean(options.yolo ?? config2.yolo);
|
|
307
623
|
let planMode = Boolean(options.plan);
|
|
624
|
+
let showTiming = Boolean(options.timing ?? config2.showTiming);
|
|
625
|
+
let lowLatency = Boolean(options.fast ?? config2.lowLatency);
|
|
626
|
+
let activeSkills = options.skill || config2.activeSkills || [];
|
|
308
627
|
let session;
|
|
309
628
|
if (options.session) {
|
|
310
629
|
const loaded = loadSession(options.session);
|
|
@@ -313,46 +632,87 @@ async function interactiveCommand(options) {
|
|
|
313
632
|
process.exit(1);
|
|
314
633
|
}
|
|
315
634
|
session = loaded;
|
|
635
|
+
if (session.provider) provider = session.provider;
|
|
636
|
+
if (session.model) model = session.model;
|
|
637
|
+
if (session.activeSkills) activeSkills = session.activeSkills;
|
|
316
638
|
console.log(pc4.green(`\u2713 Resumed session: ${session.id} (${session.turns.length} turns)`));
|
|
317
639
|
} else {
|
|
318
640
|
session = createNewSession(workspace, model);
|
|
641
|
+
session.provider = provider;
|
|
319
642
|
}
|
|
320
643
|
const attachments = loadAttachments(options.attach);
|
|
321
644
|
printBanner({
|
|
322
|
-
version: "1.0.
|
|
645
|
+
version: "1.0.2",
|
|
646
|
+
provider,
|
|
323
647
|
model,
|
|
324
648
|
workspace,
|
|
325
649
|
yolo,
|
|
326
650
|
planMode
|
|
327
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
|
+
}
|
|
328
666
|
if (attachments.length > 0) {
|
|
329
667
|
console.log(pc4.cyan(`\u{1F4CE} Attached ${attachments.length} file(s): ${attachments.map((a) => a.name).join(", ")}
|
|
330
668
|
`));
|
|
331
669
|
}
|
|
332
|
-
const agent = new
|
|
670
|
+
const agent = new LanternAgent();
|
|
333
671
|
await agent.initMcp();
|
|
334
672
|
const promptUI = new TerminalPrompt();
|
|
335
673
|
const slashContext = {
|
|
336
674
|
session,
|
|
675
|
+
provider,
|
|
337
676
|
model,
|
|
338
677
|
workspace,
|
|
339
678
|
yolo,
|
|
340
679
|
planMode,
|
|
680
|
+
showTiming,
|
|
681
|
+
activeSkills,
|
|
682
|
+
setProvider: (p) => {
|
|
683
|
+
provider = p;
|
|
684
|
+
session.provider = p;
|
|
685
|
+
slashContext.provider = p;
|
|
686
|
+
},
|
|
341
687
|
setModel: (m) => {
|
|
342
688
|
model = m;
|
|
343
689
|
session.model = m;
|
|
690
|
+
slashContext.model = m;
|
|
344
691
|
},
|
|
345
692
|
setYolo: (y) => {
|
|
346
693
|
yolo = y;
|
|
694
|
+
slashContext.yolo = y;
|
|
347
695
|
},
|
|
348
696
|
setPlanMode: (p) => {
|
|
349
697
|
planMode = p;
|
|
350
|
-
|
|
698
|
+
slashContext.planMode = p;
|
|
699
|
+
},
|
|
700
|
+
setShowTiming: (t) => {
|
|
701
|
+
showTiming = t;
|
|
702
|
+
slashContext.showTiming = t;
|
|
703
|
+
},
|
|
704
|
+
setActiveSkills: (skills) => {
|
|
705
|
+
activeSkills = skills;
|
|
706
|
+
session.activeSkills = skills;
|
|
707
|
+
slashContext.activeSkills = skills;
|
|
708
|
+
},
|
|
709
|
+
askQuestion: (q) => promptUI.askQuestion(q)
|
|
351
710
|
};
|
|
352
711
|
try {
|
|
353
712
|
while (true) {
|
|
354
|
-
const
|
|
355
|
-
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")} `;
|
|
356
716
|
const input = await promptUI.askPrompt(promptStr);
|
|
357
717
|
if (input === null || input === "/exit" || input === "/quit") {
|
|
358
718
|
console.log(pc4.dim("\nGoodbye! Session saved: " + session.id));
|
|
@@ -370,25 +730,52 @@ async function interactiveCommand(options) {
|
|
|
370
730
|
}
|
|
371
731
|
continue;
|
|
372
732
|
}
|
|
733
|
+
const abortController = new AbortController();
|
|
734
|
+
const onSigInt = () => {
|
|
735
|
+
abortController.abort(new CancelledError("Request cancelled by user (Ctrl+C)."));
|
|
736
|
+
};
|
|
737
|
+
process.on("SIGINT", onSigInt);
|
|
373
738
|
const context = {
|
|
374
739
|
workspace,
|
|
740
|
+
provider,
|
|
375
741
|
model,
|
|
376
742
|
yolo,
|
|
377
743
|
planMode,
|
|
378
744
|
jsonOutput: false,
|
|
379
745
|
maxSteps: config2.maxSteps || 25,
|
|
380
|
-
|
|
746
|
+
timeoutMs: options.timeout || config2.timeoutMs || 6e4,
|
|
747
|
+
firstTokenTimeoutMs: options.firstTokenTimeout || config2.firstTokenTimeoutMs || 3e4,
|
|
748
|
+
showTiming,
|
|
749
|
+
lowLatency,
|
|
750
|
+
activeSkills,
|
|
751
|
+
attachments,
|
|
752
|
+
signal: abortController.signal
|
|
381
753
|
};
|
|
382
754
|
try {
|
|
383
755
|
console.log("");
|
|
384
756
|
const result = await agent.runTurn(session, trimmed, context, attachments);
|
|
757
|
+
const parts = [];
|
|
385
758
|
if (result.usage.totalTokens > 0) {
|
|
759
|
+
parts.push(`Tokens: ${result.usage.totalTokens.toLocaleString()}`);
|
|
760
|
+
}
|
|
761
|
+
if (showTiming && result.timing) {
|
|
762
|
+
const ttft = result.timing.ttftMs ? `TTFT: ${result.timing.ttftMs}ms` : "";
|
|
763
|
+
const total = `Total: ${(result.timing.totalMs / 1e3).toFixed(2)}s`;
|
|
764
|
+
parts.push(`[${[provider, model, ttft, total].filter(Boolean).join(" | ")}]`);
|
|
765
|
+
}
|
|
766
|
+
if (parts.length > 0) {
|
|
386
767
|
console.log(pc4.dim(`
|
|
387
|
-
[
|
|
768
|
+
[${parts.join(" | ")}]`));
|
|
388
769
|
}
|
|
389
770
|
console.log("");
|
|
390
771
|
} catch (err) {
|
|
391
|
-
|
|
772
|
+
if (err instanceof CancelledError || abortController.signal.aborted) {
|
|
773
|
+
console.log(pc4.yellow("\n[Cancelled by user]"));
|
|
774
|
+
} else {
|
|
775
|
+
console.error("\n" + formatErrorMessage(err) + "\n");
|
|
776
|
+
}
|
|
777
|
+
} finally {
|
|
778
|
+
process.removeListener("SIGINT", onSigInt);
|
|
392
779
|
}
|
|
393
780
|
}
|
|
394
781
|
} finally {
|
|
@@ -400,48 +787,93 @@ async function interactiveCommand(options) {
|
|
|
400
787
|
// src/commands/ask.ts
|
|
401
788
|
import pc5 from "picocolors";
|
|
402
789
|
async function askCommand(question, options) {
|
|
790
|
+
const config2 = loadConfig();
|
|
403
791
|
const workspace = options.cwd || process.cwd();
|
|
404
|
-
const
|
|
405
|
-
const
|
|
792
|
+
const provider = options.provider || config2.activeProvider || "gemini";
|
|
793
|
+
const model = options.model || config2.defaultModel || "gemini-3.8-flash";
|
|
794
|
+
const yolo = Boolean(options.yolo ?? config2.yolo);
|
|
406
795
|
const jsonOutput = Boolean(options.json);
|
|
796
|
+
const showTiming = Boolean(options.timing ?? config2.showTiming);
|
|
797
|
+
const lowLatency = Boolean(options.fast ?? config2.lowLatency);
|
|
407
798
|
const attachments = loadAttachments(options.attach);
|
|
408
799
|
const session = createNewSession(workspace, model, `Ask: ${question.slice(0, 40)}`);
|
|
409
|
-
|
|
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
|
+
}
|
|
815
|
+
const agent = new LanternAgent();
|
|
410
816
|
await agent.initMcp();
|
|
817
|
+
const abortController = new AbortController();
|
|
818
|
+
const onSigInt = () => {
|
|
819
|
+
abortController.abort(new CancelledError("Request cancelled by user (Ctrl+C)."));
|
|
820
|
+
};
|
|
821
|
+
process.on("SIGINT", onSigInt);
|
|
411
822
|
const context = {
|
|
412
823
|
workspace,
|
|
824
|
+
provider,
|
|
413
825
|
model,
|
|
414
826
|
yolo,
|
|
415
827
|
planMode: false,
|
|
416
828
|
jsonOutput,
|
|
417
|
-
maxSteps: options.maxSteps || 25,
|
|
418
|
-
|
|
829
|
+
maxSteps: options.maxSteps || config2.maxSteps || 25,
|
|
830
|
+
timeoutMs: options.timeout || config2.timeoutMs || 6e4,
|
|
831
|
+
firstTokenTimeoutMs: options.firstTokenTimeout || config2.firstTokenTimeoutMs || 3e4,
|
|
832
|
+
showTiming,
|
|
833
|
+
lowLatency,
|
|
834
|
+
activeSkills: options.skill || config2.activeSkills || [],
|
|
835
|
+
attachments,
|
|
836
|
+
signal: abortController.signal
|
|
419
837
|
};
|
|
420
838
|
try {
|
|
421
839
|
const result = await agent.runTurn(session, question, context, attachments);
|
|
422
840
|
if (jsonOutput) {
|
|
423
841
|
console.log(JSON.stringify({
|
|
424
842
|
sessionId: session.id,
|
|
843
|
+
provider,
|
|
425
844
|
model,
|
|
426
845
|
question,
|
|
427
846
|
reply: result.reply,
|
|
428
847
|
toolCount: result.toolCount,
|
|
429
|
-
usage: result.usage
|
|
848
|
+
usage: result.usage,
|
|
849
|
+
timing: result.timing
|
|
430
850
|
}, null, 2));
|
|
431
851
|
} else {
|
|
852
|
+
const parts = [];
|
|
432
853
|
if (result.usage.totalTokens > 0) {
|
|
854
|
+
parts.push(`Tokens: ${result.usage.totalTokens.toLocaleString()}`);
|
|
855
|
+
}
|
|
856
|
+
if (showTiming && result.timing) {
|
|
857
|
+
const ttft = result.timing.ttftMs ? `TTFT: ${result.timing.ttftMs}ms` : "";
|
|
858
|
+
const total = `Total: ${(result.timing.totalMs / 1e3).toFixed(2)}s`;
|
|
859
|
+
parts.push(`[${[provider, model, ttft, total].filter(Boolean).join(" | ")}]`);
|
|
860
|
+
}
|
|
861
|
+
if (parts.length > 0) {
|
|
433
862
|
console.log(pc5.dim(`
|
|
434
|
-
[
|
|
863
|
+
[${parts.join(" | ")}]`));
|
|
435
864
|
}
|
|
436
865
|
}
|
|
437
866
|
} catch (err) {
|
|
438
867
|
if (jsonOutput) {
|
|
439
868
|
console.error(JSON.stringify({ error: err.message }));
|
|
869
|
+
} else if (err instanceof CancelledError || abortController.signal.aborted) {
|
|
870
|
+
console.log(pc5.yellow("\n[Cancelled by user]"));
|
|
440
871
|
} else {
|
|
441
872
|
console.error(formatErrorMessage(err));
|
|
442
873
|
}
|
|
443
874
|
process.exit(1);
|
|
444
875
|
} finally {
|
|
876
|
+
process.removeListener("SIGINT", onSigInt);
|
|
445
877
|
await agent.shutdown();
|
|
446
878
|
}
|
|
447
879
|
}
|
|
@@ -449,25 +881,56 @@ async function askCommand(question, options) {
|
|
|
449
881
|
// src/commands/plan.ts
|
|
450
882
|
import pc6 from "picocolors";
|
|
451
883
|
async function planCommand(task, options) {
|
|
884
|
+
const config2 = loadConfig();
|
|
452
885
|
const workspace = options.cwd || process.cwd();
|
|
453
|
-
const
|
|
886
|
+
const provider = options.provider || config2.activeProvider || "gemini";
|
|
887
|
+
const model = options.model || config2.defaultModel || "gemini-3.8-flash";
|
|
454
888
|
const jsonOutput = Boolean(options.json);
|
|
889
|
+
const showTiming = Boolean(options.timing ?? config2.showTiming);
|
|
455
890
|
const attachments = loadAttachments(options.attach);
|
|
456
891
|
const session = createNewSession(workspace, model, `Plan: ${task.slice(0, 40)}`);
|
|
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
|
+
}
|
|
457
907
|
if (!jsonOutput) {
|
|
458
908
|
console.log(pc6.bold(pc6.yellow("\n\u{1F3EE} Code Lantern Plan Mode (Read-Only)")));
|
|
459
|
-
console.log(pc6.dim(
|
|
909
|
+
console.log(pc6.dim(`Inspecting workspace with ${provider} (${model})...
|
|
910
|
+
`));
|
|
460
911
|
}
|
|
461
|
-
const agent = new
|
|
912
|
+
const agent = new LanternAgent();
|
|
462
913
|
await agent.initMcp();
|
|
914
|
+
const abortController = new AbortController();
|
|
915
|
+
const onSigInt = () => {
|
|
916
|
+
abortController.abort(new CancelledError("Plan cancelled by user (Ctrl+C)."));
|
|
917
|
+
};
|
|
918
|
+
process.on("SIGINT", onSigInt);
|
|
463
919
|
const context = {
|
|
464
920
|
workspace,
|
|
921
|
+
provider,
|
|
465
922
|
model,
|
|
466
923
|
yolo: false,
|
|
467
924
|
planMode: true,
|
|
468
925
|
jsonOutput,
|
|
469
|
-
maxSteps: options.maxSteps || 25,
|
|
470
|
-
|
|
926
|
+
maxSteps: options.maxSteps || config2.maxSteps || 25,
|
|
927
|
+
timeoutMs: options.timeout || config2.timeoutMs || 6e4,
|
|
928
|
+
firstTokenTimeoutMs: options.firstTokenTimeout || config2.firstTokenTimeoutMs || 3e4,
|
|
929
|
+
showTiming,
|
|
930
|
+
lowLatency: false,
|
|
931
|
+
activeSkills: options.skill || config2.activeSkills || [],
|
|
932
|
+
attachments,
|
|
933
|
+
signal: abortController.signal
|
|
471
934
|
};
|
|
472
935
|
const planPrompt = `Please inspect the codebase as needed and generate a detailed, structured, read-only implementation plan for the following task:
|
|
473
936
|
|
|
@@ -477,23 +940,31 @@ ${task}`;
|
|
|
477
940
|
if (jsonOutput) {
|
|
478
941
|
console.log(JSON.stringify({
|
|
479
942
|
sessionId: session.id,
|
|
943
|
+
provider,
|
|
480
944
|
model,
|
|
481
945
|
task,
|
|
482
946
|
plan: result.reply,
|
|
483
|
-
usage: result.usage
|
|
947
|
+
usage: result.usage,
|
|
948
|
+
timing: result.timing
|
|
484
949
|
}, null, 2));
|
|
485
950
|
} else {
|
|
486
951
|
console.log(pc6.dim(`
|
|
487
952
|
[Plan complete. Session ID: ${session.id}]`));
|
|
953
|
+
if (showTiming && result.timing) {
|
|
954
|
+
console.log(pc6.dim(`[Total Plan Time: ${(result.timing.totalMs / 1e3).toFixed(2)}s]`));
|
|
955
|
+
}
|
|
488
956
|
}
|
|
489
957
|
} catch (err) {
|
|
490
958
|
if (jsonOutput) {
|
|
491
959
|
console.error(JSON.stringify({ error: err.message }));
|
|
960
|
+
} else if (err instanceof CancelledError || abortController.signal.aborted) {
|
|
961
|
+
console.log(pc6.yellow("\n[Plan cancelled by user]"));
|
|
492
962
|
} else {
|
|
493
963
|
console.error(formatErrorMessage(err));
|
|
494
964
|
}
|
|
495
965
|
process.exit(1);
|
|
496
966
|
} finally {
|
|
967
|
+
process.removeListener("SIGINT", onSigInt);
|
|
497
968
|
await agent.shutdown();
|
|
498
969
|
}
|
|
499
970
|
}
|
|
@@ -641,11 +1112,13 @@ import fs2 from "fs";
|
|
|
641
1112
|
import path2 from "path";
|
|
642
1113
|
import { execSync } from "child_process";
|
|
643
1114
|
import pc10 from "picocolors";
|
|
644
|
-
import { GoogleGenAI } from "@google/genai";
|
|
645
1115
|
async function doctorCommand() {
|
|
646
1116
|
console.log("\n" + pc10.bold(pc10.yellow("\u{1F3EE} Code Lantern Doctor \u2014 Diagnostics")));
|
|
647
1117
|
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
1118
|
let issuesFound = 0;
|
|
1119
|
+
const config2 = loadConfig();
|
|
1120
|
+
const registry = new ProviderRegistry();
|
|
1121
|
+
const skillManager = new SkillManager();
|
|
649
1122
|
const platformName = process.platform === "darwin" ? `macOS (${process.arch === "arm64" ? "Apple Silicon" : "Intel"})` : process.platform === "win32" ? `Windows (${process.arch})` : `${process.platform} (${process.arch})`;
|
|
650
1123
|
console.log(`${pc10.green("\u2713")} Platform / OS: ${platformName}`);
|
|
651
1124
|
const nodeVer = process.version;
|
|
@@ -656,28 +1129,38 @@ async function doctorCommand() {
|
|
|
656
1129
|
issuesFound++;
|
|
657
1130
|
console.log(`${pc10.red("\u2717")} Node.js Runtime: ${nodeVer} (Node 18+ required)`);
|
|
658
1131
|
}
|
|
659
|
-
const
|
|
660
|
-
|
|
1132
|
+
const activeProviderId = config2.activeProvider || "gemini";
|
|
1133
|
+
const activeProvider = registry.getProvider(activeProviderId);
|
|
1134
|
+
const keys = listConfiguredKeys();
|
|
1135
|
+
console.log(`${pc10.green("\u2713")} Active Provider: ${pc10.bold(activeProvider.displayName)} (${activeProviderId})`);
|
|
1136
|
+
console.log(`${pc10.green("\u2713")} Active Model: ${config2.defaultModel}`);
|
|
1137
|
+
console.log(`${pc10.green("\u2713")} Credential Storage: ${getStoreName()}`);
|
|
1138
|
+
const activeKeyInfo = keys[activeProviderId];
|
|
1139
|
+
if (!activeKeyInfo || !activeKeyInfo.configured) {
|
|
661
1140
|
issuesFound++;
|
|
662
|
-
console.log(`${pc10.red("\u2717")}
|
|
663
|
-
console.log(pc10.yellow(
|
|
1141
|
+
console.log(`${pc10.red("\u2717")} API Key (${activeProviderId}): Missing!`);
|
|
1142
|
+
console.log(pc10.yellow(` \u2192 To fix: set environment variable or run 'lantern keys set ${activeProviderId}'`));
|
|
664
1143
|
} else {
|
|
665
|
-
const
|
|
666
|
-
console.log(`${pc10.green("\u2713")}
|
|
1144
|
+
const src = activeKeyInfo.sourceDescription ? `[${activeKeyInfo.sourceDescription}]` : "";
|
|
1145
|
+
console.log(`${pc10.green("\u2713")} API Key (${activeProviderId}): Found (${activeKeyInfo.maskedKey}) ${src}`);
|
|
667
1146
|
try {
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1147
|
+
const pingRes = await activeProvider.testConnection();
|
|
1148
|
+
if (pingRes.success) {
|
|
1149
|
+
console.log(`${pc10.green("\u2713")} Provider Auth & Ping: Verified (${pingRes.latencyMs}ms latency)`);
|
|
1150
|
+
} else {
|
|
1151
|
+
issuesFound++;
|
|
1152
|
+
console.log(`${pc10.red("\u2717")} Provider Auth & Ping: Failed: ${pingRes.error}`);
|
|
1153
|
+
}
|
|
675
1154
|
} catch (err) {
|
|
676
1155
|
issuesFound++;
|
|
677
|
-
console.log(`${pc10.red("\u2717")}
|
|
678
|
-
console.log(pc10.yellow(" \u2192 Verify your API key at https://aistudio.google.com/app/apikey"));
|
|
1156
|
+
console.log(`${pc10.red("\u2717")} Provider Auth & Ping: Error: ${err.message}`);
|
|
679
1157
|
}
|
|
680
1158
|
}
|
|
1159
|
+
const otherConfigured = Object.entries(keys).filter(([p, k]) => p !== activeProviderId && k.configured);
|
|
1160
|
+
if (otherConfigured.length > 0) {
|
|
1161
|
+
const others = otherConfigured.map(([p]) => p).join(", ");
|
|
1162
|
+
console.log(`${pc10.cyan("\u2139")} Additional Providers: Configured (${others})`);
|
|
1163
|
+
}
|
|
681
1164
|
try {
|
|
682
1165
|
const gitVer = execSync("git --version", { stdio: "pipe" }).toString().trim();
|
|
683
1166
|
console.log(`${pc10.green("\u2713")} Git CLI: Available (${gitVer})`);
|
|
@@ -686,30 +1169,32 @@ async function doctorCommand() {
|
|
|
686
1169
|
}
|
|
687
1170
|
try {
|
|
688
1171
|
const configFile = getConfigFilePath();
|
|
689
|
-
|
|
690
|
-
console.log(`${pc10.green("\u2713")} Config File: ${configFile} (Model: ${config2.defaultModel})`);
|
|
1172
|
+
console.log(`${pc10.green("\u2713")} Config File: ${configFile} (Timeout: ${config2.timeoutMs}ms, TTFT: ${config2.firstTokenTimeoutMs}ms)`);
|
|
691
1173
|
} catch (err) {
|
|
692
1174
|
issuesFound++;
|
|
693
|
-
console.log(`${pc10.red("\u2717")} Config File: Invalid
|
|
1175
|
+
console.log(`${pc10.red("\u2717")} Config File: Invalid: ${err.message}`);
|
|
694
1176
|
}
|
|
695
1177
|
try {
|
|
696
1178
|
const sessionsDir = getSessionsDir();
|
|
697
|
-
const count = fs2.readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).length;
|
|
1179
|
+
const count = fs2.existsSync(sessionsDir) ? fs2.readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).length : 0;
|
|
698
1180
|
console.log(`${pc10.green("\u2713")} Session Storage: ${sessionsDir} (${count} stored session(s))`);
|
|
699
1181
|
} catch (err) {
|
|
700
1182
|
issuesFound++;
|
|
701
|
-
console.log(`${pc10.red("\u2717")} Session Storage: Failed
|
|
1183
|
+
console.log(`${pc10.red("\u2717")} Session Storage: Failed: ${err.message}`);
|
|
1184
|
+
}
|
|
1185
|
+
const installedSkills = skillManager.listSkills();
|
|
1186
|
+
if (installedSkills.length > 0) {
|
|
1187
|
+
const activeCount = installedSkills.filter((s) => s.enabled).length;
|
|
1188
|
+
console.log(`${pc10.green("\u2713")} Skills: ${installedSkills.length} installed (${activeCount} active default)`);
|
|
1189
|
+
} else {
|
|
1190
|
+
console.log(`${pc10.cyan("\u2139")} Skills: None installed (add with: lantern skills add <path>)`);
|
|
702
1191
|
}
|
|
703
1192
|
const mcpServers = loadMcpConfig();
|
|
704
1193
|
const serverCount = Object.keys(mcpServers).length;
|
|
705
1194
|
if (serverCount === 0) {
|
|
706
|
-
console.log(`${pc10.cyan("\u2139")} MCP
|
|
1195
|
+
console.log(`${pc10.cyan("\u2139")} MCP Servers: No external MCP servers configured (optional)`);
|
|
707
1196
|
} else {
|
|
708
|
-
console.log(`${pc10.green("\u2713")} MCP
|
|
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
|
-
}
|
|
1197
|
+
console.log(`${pc10.green("\u2713")} MCP Servers: ${serverCount} server(s) configured`);
|
|
713
1198
|
}
|
|
714
1199
|
const cwd = process.cwd();
|
|
715
1200
|
const instructions = loadProjectInstructions(cwd);
|
|
@@ -826,39 +1311,411 @@ async function mcpCommand(action, serverName, command, args = []) {
|
|
|
826
1311
|
process.exit(1);
|
|
827
1312
|
}
|
|
828
1313
|
|
|
1314
|
+
// src/commands/providersCmd.ts
|
|
1315
|
+
import pc12 from "picocolors";
|
|
1316
|
+
import readline2 from "readline";
|
|
1317
|
+
function promptLine(query, hide = false) {
|
|
1318
|
+
const rl = readline2.createInterface({
|
|
1319
|
+
input: process.stdin,
|
|
1320
|
+
output: process.stdout
|
|
1321
|
+
});
|
|
1322
|
+
return new Promise((resolve) => {
|
|
1323
|
+
rl.question(query, (answer) => {
|
|
1324
|
+
rl.close();
|
|
1325
|
+
resolve(answer.trim());
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
async function providersCommand(action, target, options) {
|
|
1330
|
+
const registry = new ProviderRegistry();
|
|
1331
|
+
const subAction = (action || "list").toLowerCase();
|
|
1332
|
+
if (subAction === "list") {
|
|
1333
|
+
const providers = registry.listProviders();
|
|
1334
|
+
console.log("\n" + pc12.bold(pc12.yellow("\u{1F3EE} Configured AI Providers")));
|
|
1335
|
+
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"));
|
|
1336
|
+
console.log(
|
|
1337
|
+
pc12.bold("Provider".padEnd(16)) + pc12.bold("Status".padEnd(16)) + pc12.bold("Default Model".padEnd(30)) + pc12.bold("Key Source")
|
|
1338
|
+
);
|
|
1339
|
+
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"));
|
|
1340
|
+
for (const p of providers) {
|
|
1341
|
+
const activeMarker = p.isActive ? pc12.green(" (active)") : "";
|
|
1342
|
+
const nameCol = pc12.bold(p.name) + activeMarker;
|
|
1343
|
+
const statusCol = p.isConfigured ? pc12.green("\u2713 Ready") : pc12.yellow("! Needs Key");
|
|
1344
|
+
const keyInfo = getApiKey(p.id);
|
|
1345
|
+
const sourceCol = keyInfo.sourceDescription ? pc12.cyan(`[${keyInfo.sourceDescription}]`) : pc12.dim("(missing)");
|
|
1346
|
+
console.log(
|
|
1347
|
+
nameCol.padEnd(p.isActive ? 25 : 16) + statusCol.padEnd(25) + p.defaultModel.padEnd(30) + sourceCol
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
console.log(pc12.dim("\nTo set default: lantern providers set-default <name>"));
|
|
1351
|
+
console.log(pc12.dim("To test connection: lantern providers test [name]"));
|
|
1352
|
+
console.log(pc12.dim("To add custom provider: lantern providers add <name>\n"));
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
if (subAction === "set-default" || subAction === "use") {
|
|
1356
|
+
if (!target) {
|
|
1357
|
+
console.error(pc12.red("Error: Missing provider name. Usage: lantern providers set-default <name>"));
|
|
1358
|
+
process.exit(1);
|
|
1359
|
+
}
|
|
1360
|
+
const norm = target.toLowerCase();
|
|
1361
|
+
if (!registry.hasProvider(norm)) {
|
|
1362
|
+
console.error(pc12.red(`Error: Provider '${target}' is not registered.`));
|
|
1363
|
+
process.exit(1);
|
|
1364
|
+
}
|
|
1365
|
+
setConfigValue("activeProvider", norm);
|
|
1366
|
+
const p = registry.getProvider(norm);
|
|
1367
|
+
setConfigValue("defaultModel", p.defaultModel);
|
|
1368
|
+
console.log(pc12.green(`\u2713 Set active provider to '${p.displayName}' (default model: ${p.defaultModel})`));
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
if (subAction === "test") {
|
|
1372
|
+
const targetProvider = target || loadConfig().activeProvider || "gemini";
|
|
1373
|
+
const p = registry.getProvider(targetProvider);
|
|
1374
|
+
console.log(`Testing connection to ${pc12.cyan(p.displayName)}...`);
|
|
1375
|
+
const result = await p.testConnection();
|
|
1376
|
+
if (result.success) {
|
|
1377
|
+
console.log(pc12.green(`\u2713 Connection verified! Latency: ${result.latencyMs}ms`));
|
|
1378
|
+
if (result.modelsCount !== void 0) {
|
|
1379
|
+
console.log(pc12.dim(` Available models discovered: ${result.modelsCount}`));
|
|
1380
|
+
}
|
|
1381
|
+
} else {
|
|
1382
|
+
console.log(pc12.red(`\u2717 Connection failed: ${result.error}`));
|
|
1383
|
+
console.log(pc12.yellow(` Check your credentials with 'lantern keys list'`));
|
|
1384
|
+
process.exit(1);
|
|
1385
|
+
}
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (subAction === "add") {
|
|
1389
|
+
const name = target || await promptLine("Provider ID / Name: ");
|
|
1390
|
+
if (!name) {
|
|
1391
|
+
console.error(pc12.red("Error: Provider name is required."));
|
|
1392
|
+
process.exit(1);
|
|
1393
|
+
}
|
|
1394
|
+
const baseUrl = options?.baseUrl || await promptLine("Base URL (e.g. http://localhost:11434/v1): ");
|
|
1395
|
+
const defaultModel = options?.model || await promptLine("Default Model ID (e.g. llama3, qwen2.5): ");
|
|
1396
|
+
const apiKey = options?.apiKey || await promptMaskedApiKey("API Key (optional, press Enter to skip)");
|
|
1397
|
+
registry.registerCustomProvider({
|
|
1398
|
+
id: name.toLowerCase(),
|
|
1399
|
+
name,
|
|
1400
|
+
type: "custom",
|
|
1401
|
+
baseUrl,
|
|
1402
|
+
defaultModel: defaultModel || "default"
|
|
1403
|
+
});
|
|
1404
|
+
if (apiKey) {
|
|
1405
|
+
setApiKey(name.toLowerCase(), apiKey);
|
|
1406
|
+
}
|
|
1407
|
+
console.log(pc12.green(`\u2713 Registered custom OpenAI-compatible provider '${name}'`));
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (subAction === "remove" || subAction === "rm") {
|
|
1411
|
+
if (!target) {
|
|
1412
|
+
console.error(pc12.red("Error: Missing provider name. Usage: lantern providers remove <name>"));
|
|
1413
|
+
process.exit(1);
|
|
1414
|
+
}
|
|
1415
|
+
const removed = registry.removeCustomProvider(target);
|
|
1416
|
+
if (removed) {
|
|
1417
|
+
console.log(pc12.green(`\u2713 Removed provider '${target}'`));
|
|
1418
|
+
} else {
|
|
1419
|
+
console.log(pc12.red(`Cannot remove built-in provider '${target}'. Only custom providers can be removed.`));
|
|
1420
|
+
process.exit(1);
|
|
1421
|
+
}
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
console.error(pc12.red(`Unknown action '${action}'. Available: list, set-default, test, add, remove`));
|
|
1425
|
+
process.exit(1);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/commands/modelsCmd.ts
|
|
1429
|
+
import pc13 from "picocolors";
|
|
1430
|
+
async function modelsCommand(action, modelId, options) {
|
|
1431
|
+
const registry = new ProviderRegistry();
|
|
1432
|
+
const config2 = loadConfig();
|
|
1433
|
+
const subAction = (action || "list").toLowerCase();
|
|
1434
|
+
const providerId = options?.provider || config2.activeProvider || "gemini";
|
|
1435
|
+
const provider = registry.getProvider(providerId);
|
|
1436
|
+
if (subAction === "list") {
|
|
1437
|
+
if (!options?.json) {
|
|
1438
|
+
console.log(`
|
|
1439
|
+
Fetching models for ${pc13.cyan(provider.displayName)}...`);
|
|
1440
|
+
}
|
|
1441
|
+
const models = await provider.listModels(Boolean(options?.refresh));
|
|
1442
|
+
if (options?.json) {
|
|
1443
|
+
console.log(JSON.stringify(models, null, 2));
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
console.log("\n" + pc13.bold(pc13.yellow(`\u{1F3EE} Available Models for ${provider.displayName}`)));
|
|
1447
|
+
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"));
|
|
1448
|
+
console.log(
|
|
1449
|
+
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")
|
|
1450
|
+
);
|
|
1451
|
+
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"));
|
|
1452
|
+
const currentModel = config2.defaultModel;
|
|
1453
|
+
for (const m of models) {
|
|
1454
|
+
const isCurrent = m.id === currentModel;
|
|
1455
|
+
const marker = isCurrent ? pc13.green(" (active)") : "";
|
|
1456
|
+
const idCol = (m.id + marker).slice(0, 34);
|
|
1457
|
+
const toolsCol = m.capabilities.tools ? pc13.green("\u2713") : pc13.dim("\u2717");
|
|
1458
|
+
const visionCol = m.capabilities.images ? pc13.green("\u2713") : pc13.dim("\u2717");
|
|
1459
|
+
const reasoningCol = m.capabilities.reasoning ? pc13.magenta("\u2713") : pc13.dim("\u2717");
|
|
1460
|
+
const ctxCol = m.contextWindow ? `${(m.contextWindow / 1e3).toFixed(0)}k` : pc13.dim("standard");
|
|
1461
|
+
console.log(
|
|
1462
|
+
idCol.padEnd(isCurrent ? 45 : 36) + toolsCol.padEnd(16) + visionCol.padEnd(16) + reasoningCol.padEnd(20) + ctxCol
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
console.log(pc13.dim("\nTo set active model: lantern model set <model-id>"));
|
|
1466
|
+
console.log(pc13.dim(`Example: lantern model set ${models[0]?.id || "gemini-3.8-flash"}
|
|
1467
|
+
`));
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (subAction === "set") {
|
|
1471
|
+
if (!modelId) {
|
|
1472
|
+
console.error(pc13.red("Error: Missing model ID. Usage: lantern model set <model-id> [--provider <provider>]"));
|
|
1473
|
+
process.exit(1);
|
|
1474
|
+
}
|
|
1475
|
+
if (options?.provider) {
|
|
1476
|
+
setConfigValue("activeProvider", options.provider.toLowerCase());
|
|
1477
|
+
}
|
|
1478
|
+
setConfigValue("defaultModel", modelId.trim());
|
|
1479
|
+
console.log(pc13.green(`\u2713 Set active model to '${modelId.trim()}' (provider: ${provider.displayName})`));
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
console.error(pc13.red(`Unknown action '${action}'. Available: list, set`));
|
|
1483
|
+
process.exit(1);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// src/commands/keysCmd.ts
|
|
1487
|
+
import pc14 from "picocolors";
|
|
1488
|
+
async function keysCommand(action, provider, key) {
|
|
1489
|
+
const subAction = (action || "list").toLowerCase();
|
|
1490
|
+
if (subAction === "list") {
|
|
1491
|
+
const keys = listConfiguredKeys();
|
|
1492
|
+
console.log("\n" + pc14.bold(pc14.yellow("\u{1F3EE} Configured Provider API Keys")));
|
|
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"));
|
|
1495
|
+
console.log(
|
|
1496
|
+
pc14.bold("Provider".padEnd(16)) + pc14.bold("Source".padEnd(28)) + pc14.bold("Masked Key")
|
|
1497
|
+
);
|
|
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"));
|
|
1499
|
+
for (const [p, info] of Object.entries(keys)) {
|
|
1500
|
+
const srcText = info.configured ? info.source === "env" ? pc14.cyan(info.sourceDescription) : pc14.blue(info.sourceDescription) : pc14.dim("none");
|
|
1501
|
+
console.log(
|
|
1502
|
+
pc14.bold(p.padEnd(16)) + srcText.padEnd(36) + (info.configured ? pc14.green(info.maskedKey) : pc14.dim("(not configured)"))
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
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"));
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
if (subAction === "set") {
|
|
1511
|
+
if (!provider) {
|
|
1512
|
+
console.error(pc14.red("Error: Missing provider name. Usage: lantern keys set <provider> [key]"));
|
|
1513
|
+
process.exit(1);
|
|
1514
|
+
}
|
|
1515
|
+
const norm = provider.toLowerCase().trim();
|
|
1516
|
+
let apiKey = key;
|
|
1517
|
+
if (!apiKey) {
|
|
1518
|
+
apiKey = await promptMaskedApiKey(`Enter API key for '${norm}'`);
|
|
1519
|
+
}
|
|
1520
|
+
if (!apiKey) {
|
|
1521
|
+
console.error(pc14.red("Error: API key cannot be empty."));
|
|
1522
|
+
process.exit(1);
|
|
1523
|
+
}
|
|
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
|
+
}
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
if (subAction === "remove" || subAction === "rm") {
|
|
1535
|
+
if (!provider) {
|
|
1536
|
+
console.error(pc14.red("Error: Missing provider name. Usage: lantern keys remove <provider>"));
|
|
1537
|
+
process.exit(1);
|
|
1538
|
+
}
|
|
1539
|
+
const norm = provider.toLowerCase().trim();
|
|
1540
|
+
const removed = removeApiKey(norm);
|
|
1541
|
+
if (removed) {
|
|
1542
|
+
console.log(pc14.green(`\u2713 Removed stored key for '${norm}' from ${getStoreName()}`));
|
|
1543
|
+
} else {
|
|
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).`));
|
|
1546
|
+
}
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
console.error(pc14.red(`Unknown action '${action}'. Available: list, set, remove`));
|
|
1550
|
+
process.exit(1);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// src/commands/skillsCmd.ts
|
|
1554
|
+
import pc15 from "picocolors";
|
|
1555
|
+
async function skillsCommand(action, target) {
|
|
1556
|
+
const manager = new SkillManager();
|
|
1557
|
+
const subAction = (action || "list").toLowerCase();
|
|
1558
|
+
if (subAction === "list") {
|
|
1559
|
+
const skills = manager.listSkills();
|
|
1560
|
+
console.log("\n" + pc15.bold(pc15.yellow("\u{1F3EE} Installed Code Lantern Skills")));
|
|
1561
|
+
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"));
|
|
1562
|
+
console.log(
|
|
1563
|
+
pc15.bold("Skill Name".padEnd(24)) + pc15.bold("Source".padEnd(12)) + pc15.bold("Status".padEnd(14)) + pc15.bold("Description")
|
|
1564
|
+
);
|
|
1565
|
+
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"));
|
|
1566
|
+
if (skills.length === 0) {
|
|
1567
|
+
console.log(pc15.dim("No skills currently installed."));
|
|
1568
|
+
console.log(pc15.dim(`
|
|
1569
|
+
Add a skill using: lantern skills add <local-folder-or-git-url>`));
|
|
1570
|
+
console.log(pc15.dim(`User skills folder: ${manager.getUserSkillsDir()}
|
|
1571
|
+
`));
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
for (const s of skills) {
|
|
1575
|
+
const statusText = s.enabled ? pc15.green("\u2713 Active") : pc15.dim("Disabled");
|
|
1576
|
+
const sourceText = s.source === "project" ? pc15.cyan("[project]") : pc15.blue("[user]");
|
|
1577
|
+
console.log(
|
|
1578
|
+
pc15.bold(s.name.padEnd(24)) + sourceText.padEnd(20) + statusText.padEnd(22) + pc15.dim(s.description.slice(0, 40))
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
console.log(pc15.dim("\nTo inspect: lantern skills show <name>"));
|
|
1582
|
+
console.log(pc15.dim("To enable: lantern skills enable <name>"));
|
|
1583
|
+
console.log(pc15.dim("To disable: lantern skills disable <name>"));
|
|
1584
|
+
console.log(pc15.dim("To add: lantern skills add <folder-path-or-git-url>\n"));
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (subAction === "show") {
|
|
1588
|
+
if (!target) {
|
|
1589
|
+
console.error(pc15.red("Error: Missing skill name. Usage: lantern skills show <name>"));
|
|
1590
|
+
process.exit(1);
|
|
1591
|
+
}
|
|
1592
|
+
const skill = manager.getSkill(target);
|
|
1593
|
+
if (!skill) {
|
|
1594
|
+
console.error(pc15.red(`Error: Skill '${target}' not found.`));
|
|
1595
|
+
process.exit(1);
|
|
1596
|
+
}
|
|
1597
|
+
console.log("\n" + pc15.bold(pc15.yellow(`\u{1F3EE} Skill: ${skill.name}`)));
|
|
1598
|
+
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"));
|
|
1599
|
+
console.log(`${pc15.bold("Source:")} ${skill.source}`);
|
|
1600
|
+
console.log(`${pc15.bold("Path:")} ${skill.path}`);
|
|
1601
|
+
console.log(`${pc15.bold("Status:")} ${skill.enabled ? pc15.green("Enabled") : pc15.dim("Disabled")}`);
|
|
1602
|
+
console.log(`${pc15.bold("Description:")} ${skill.description}`);
|
|
1603
|
+
if (skill.references && skill.references.length > 0) {
|
|
1604
|
+
console.log(`${pc15.bold("References:")} ${skill.references.length} reference file(s) available`);
|
|
1605
|
+
}
|
|
1606
|
+
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"));
|
|
1607
|
+
console.log("\n" + pc15.cyan("Instructions Content:"));
|
|
1608
|
+
console.log(skill.content.slice(0, 1e3) + (skill.content.length > 1e3 ? "\n... (truncated)" : ""));
|
|
1609
|
+
console.log("");
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
if (subAction === "enable") {
|
|
1613
|
+
if (!target) {
|
|
1614
|
+
console.error(pc15.red("Error: Missing skill name. Usage: lantern skills enable <name>"));
|
|
1615
|
+
process.exit(1);
|
|
1616
|
+
}
|
|
1617
|
+
const skill = manager.getSkill(target);
|
|
1618
|
+
if (!skill) {
|
|
1619
|
+
console.error(pc15.red(`Error: Skill '${target}' not found.`));
|
|
1620
|
+
process.exit(1);
|
|
1621
|
+
}
|
|
1622
|
+
manager.enableSkill(skill.name);
|
|
1623
|
+
console.log(pc15.green(`\u2713 Enabled skill '${skill.name}'`));
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
if (subAction === "disable") {
|
|
1627
|
+
if (!target) {
|
|
1628
|
+
console.error(pc15.red("Error: Missing skill name. Usage: lantern skills disable <name>"));
|
|
1629
|
+
process.exit(1);
|
|
1630
|
+
}
|
|
1631
|
+
manager.disableSkill(target);
|
|
1632
|
+
console.log(pc15.yellow(`\u2713 Disabled skill '${target}'`));
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
if (subAction === "add") {
|
|
1636
|
+
if (!target) {
|
|
1637
|
+
console.error(pc15.red("Error: Missing path or repository URL. Usage: lantern skills add <path-or-git-url>"));
|
|
1638
|
+
process.exit(1);
|
|
1639
|
+
}
|
|
1640
|
+
console.log(`Adding skill from '${target}'...`);
|
|
1641
|
+
const isGit = target.startsWith("http://") || target.startsWith("https://") || target.startsWith("git@");
|
|
1642
|
+
const result = isGit ? manager.addSkillFromGit(target) : manager.addSkillFromPath(target);
|
|
1643
|
+
if (result.success) {
|
|
1644
|
+
console.log(pc15.green(`\u2713 Successfully added and enabled skill '${result.skillName}'`));
|
|
1645
|
+
} else {
|
|
1646
|
+
console.error(pc15.red(`\u2717 Failed to add skill: ${result.error}`));
|
|
1647
|
+
process.exit(1);
|
|
1648
|
+
}
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
if (subAction === "remove" || subAction === "rm") {
|
|
1652
|
+
if (!target) {
|
|
1653
|
+
console.error(pc15.red("Error: Missing skill name. Usage: lantern skills remove <name>"));
|
|
1654
|
+
process.exit(1);
|
|
1655
|
+
}
|
|
1656
|
+
const removed = manager.removeSkill(target);
|
|
1657
|
+
if (removed) {
|
|
1658
|
+
console.log(pc15.green(`\u2713 Removed skill '${target}'`));
|
|
1659
|
+
} else {
|
|
1660
|
+
console.error(pc15.red(`\u2717 Skill '${target}' not found or could not be removed.`));
|
|
1661
|
+
process.exit(1);
|
|
1662
|
+
}
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
console.error(pc15.red(`Unknown action '${action}'. Available: list, show, add, enable, disable, remove`));
|
|
1666
|
+
process.exit(1);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
829
1669
|
// src/cli.ts
|
|
830
1670
|
var program = new Command();
|
|
831
1671
|
var config = loadConfig();
|
|
832
|
-
program.name("lantern").description("Code Lantern CLI \u2014 Personal AI coding assistant for the terminal
|
|
833
|
-
program.option("-m, --model <model>", "
|
|
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");
|
|
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");
|
|
834
1674
|
program.command("ask <question...>").description("Ask a single question or task and stream the answer").action(async (questionParts) => {
|
|
835
1675
|
const opts = program.opts();
|
|
836
1676
|
const question = questionParts.join(" ");
|
|
837
1677
|
await askCommand(question, {
|
|
1678
|
+
provider: opts.provider,
|
|
838
1679
|
model: opts.model,
|
|
839
1680
|
cwd: opts.cwd,
|
|
840
1681
|
attach: opts.attach,
|
|
841
1682
|
yolo: opts.yolo,
|
|
842
|
-
json: opts.json
|
|
1683
|
+
json: opts.json,
|
|
1684
|
+
timeout: opts.timeout,
|
|
1685
|
+
firstTokenTimeout: opts.firstTokenTimeout,
|
|
1686
|
+
timing: opts.timing,
|
|
1687
|
+
fast: opts.fast,
|
|
1688
|
+
skill: opts.skill
|
|
843
1689
|
});
|
|
844
1690
|
});
|
|
845
1691
|
program.command("plan <task...>").description("Inspect the codebase and produce a read-only implementation plan").action(async (taskParts) => {
|
|
846
1692
|
const opts = program.opts();
|
|
847
1693
|
const task = taskParts.join(" ");
|
|
848
1694
|
await planCommand(task, {
|
|
1695
|
+
provider: opts.provider,
|
|
849
1696
|
model: opts.model,
|
|
850
1697
|
cwd: opts.cwd,
|
|
851
1698
|
attach: opts.attach,
|
|
852
|
-
json: opts.json
|
|
1699
|
+
json: opts.json,
|
|
1700
|
+
timeout: opts.timeout,
|
|
1701
|
+
firstTokenTimeout: opts.firstTokenTimeout,
|
|
1702
|
+
timing: opts.timing,
|
|
1703
|
+
skill: opts.skill
|
|
853
1704
|
});
|
|
854
1705
|
});
|
|
855
1706
|
program.command("resume [sessionId]").description("Continue a saved conversation session").action(async (sessionId) => {
|
|
856
1707
|
const opts = program.opts();
|
|
857
1708
|
await resumeCommand(sessionId, {
|
|
1709
|
+
provider: opts.provider,
|
|
858
1710
|
model: opts.model,
|
|
859
1711
|
cwd: opts.cwd,
|
|
860
1712
|
attach: opts.attach,
|
|
861
|
-
yolo: opts.yolo
|
|
1713
|
+
yolo: opts.yolo,
|
|
1714
|
+
timeout: opts.timeout,
|
|
1715
|
+
firstTokenTimeout: opts.firstTokenTimeout,
|
|
1716
|
+
timing: opts.timing,
|
|
1717
|
+
fast: opts.fast,
|
|
1718
|
+
skill: opts.skill
|
|
862
1719
|
});
|
|
863
1720
|
});
|
|
864
1721
|
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 +1729,29 @@ program.command("sessions").description("List, delete, or export saved local ses
|
|
|
872
1729
|
program.command("config [action] [key] [value]").description("View or update configuration (list, get, set, path)").action(async (action, key, value) => {
|
|
873
1730
|
await configCommand(action, key, value);
|
|
874
1731
|
});
|
|
875
|
-
program.command("doctor").description("Diagnose API
|
|
1732
|
+
program.command("doctor").description("Diagnose API keys, configuration, environment, and MCP server health").action(async () => {
|
|
876
1733
|
await doctorCommand();
|
|
877
1734
|
});
|
|
878
1735
|
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
1736
|
const rawArgs = rest.flat().filter((a) => typeof a === "string");
|
|
880
1737
|
await mcpCommand(action, serverName, cmd, rawArgs);
|
|
881
1738
|
});
|
|
1739
|
+
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) => {
|
|
1740
|
+
await providersCommand(action, target, cmdOpts);
|
|
1741
|
+
});
|
|
1742
|
+
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) => {
|
|
1743
|
+
const globalOpts = program.opts();
|
|
1744
|
+
const provider = cmdOpts?.provider || globalOpts.provider;
|
|
1745
|
+
const json = Boolean(cmdOpts?.json || globalOpts.json);
|
|
1746
|
+
const refresh = Boolean(cmdOpts?.refresh);
|
|
1747
|
+
await modelsCommand(action, modelId, { provider, refresh, json });
|
|
1748
|
+
});
|
|
1749
|
+
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) => {
|
|
1750
|
+
await keysCommand(action, provider, key);
|
|
1751
|
+
});
|
|
1752
|
+
program.command("skills [action] [target]").description("Manage portable skills in SKILL.md format (list, show, add, enable, disable, remove)").action(async (action, target) => {
|
|
1753
|
+
await skillsCommand(action, target);
|
|
1754
|
+
});
|
|
882
1755
|
program.action(async () => {
|
|
883
1756
|
const opts = program.opts();
|
|
884
1757
|
if (!process.stdin.isTTY) {
|
|
@@ -890,21 +1763,33 @@ program.action(async () => {
|
|
|
890
1763
|
const trimmedPiped = pipedData.trim();
|
|
891
1764
|
if (trimmedPiped) {
|
|
892
1765
|
await askCommand(trimmedPiped, {
|
|
1766
|
+
provider: opts.provider,
|
|
893
1767
|
model: opts.model,
|
|
894
1768
|
cwd: opts.cwd,
|
|
895
1769
|
attach: opts.attach,
|
|
896
1770
|
yolo: opts.yolo,
|
|
897
|
-
json: opts.json
|
|
1771
|
+
json: opts.json,
|
|
1772
|
+
timeout: opts.timeout,
|
|
1773
|
+
firstTokenTimeout: opts.firstTokenTimeout,
|
|
1774
|
+
timing: opts.timing,
|
|
1775
|
+
fast: opts.fast,
|
|
1776
|
+
skill: opts.skill
|
|
898
1777
|
});
|
|
899
1778
|
return;
|
|
900
1779
|
}
|
|
901
1780
|
}
|
|
902
1781
|
await interactiveCommand({
|
|
1782
|
+
provider: opts.provider,
|
|
903
1783
|
model: opts.model,
|
|
904
1784
|
cwd: opts.cwd,
|
|
905
1785
|
session: opts.session,
|
|
906
1786
|
attach: opts.attach,
|
|
907
|
-
yolo: opts.yolo
|
|
1787
|
+
yolo: opts.yolo,
|
|
1788
|
+
timeout: opts.timeout,
|
|
1789
|
+
firstTokenTimeout: opts.firstTokenTimeout,
|
|
1790
|
+
timing: opts.timing,
|
|
1791
|
+
fast: opts.fast,
|
|
1792
|
+
skill: opts.skill
|
|
908
1793
|
});
|
|
909
1794
|
});
|
|
910
1795
|
program.parseAsync(process.argv).catch((err) => {
|