codelantern-agent-cli 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,204 +2,1657 @@
2
2
 
3
3
  // src/provider/errors.ts
4
4
  import pc from "picocolors";
5
+ var TimeoutError = class extends Error {
6
+ timeoutMs;
7
+ constructor(timeoutMs, operation = "Request") {
8
+ super(`${operation} timed out after ${(timeoutMs / 1e3).toFixed(1)}s.`);
9
+ this.name = "TimeoutError";
10
+ this.timeoutMs = timeoutMs;
11
+ }
12
+ };
13
+ var FirstTokenTimeoutError = class extends Error {
14
+ timeoutMs;
15
+ provider;
16
+ model;
17
+ constructor(timeoutMs, provider, model) {
18
+ super(`No response tokens received from ${provider} (${model}) after ${(timeoutMs / 1e3).toFixed(1)}s.`);
19
+ this.name = "FirstTokenTimeoutError";
20
+ this.timeoutMs = timeoutMs;
21
+ this.provider = provider;
22
+ this.model = model;
23
+ }
24
+ };
25
+ var CancelledError = class extends Error {
26
+ constructor(message = "Operation cancelled by user (Ctrl+C).") {
27
+ super(message);
28
+ this.name = "CancelledError";
29
+ }
30
+ };
5
31
  var MissingApiKeyError = class extends Error {
6
- constructor() {
32
+ provider;
33
+ constructor(provider = "gemini", envVar = "GEMINI_API_KEY") {
7
34
  super(
8
- `GEMINI_API_KEY environment variable is not configured.
35
+ `API key for provider '${provider}' is not configured.
9
36
 
10
37
  To resolve this:
11
- 1. Get a Gemini API key from https://aistudio.google.com/app/apikey
12
- 2. Set the environment variable in your terminal or shell profile:
13
- export GEMINI_API_KEY="your-api-key-here"
14
- 3. Or run Code Lantern with the key prefixed:
15
- GEMINI_API_KEY="your-key" lantern
38
+ 1. Set the environment variable:
39
+ export ${envVar}="your-api-key-here"
40
+ 2. Or store it securely using Lantern:
41
+ lantern keys set ${provider}
16
42
  `
17
43
  );
18
44
  this.name = "MissingApiKeyError";
45
+ this.provider = provider;
19
46
  }
20
47
  };
21
48
  var RateLimitError = class extends Error {
22
- constructor(message = "Gemini API rate limit exceeded.") {
49
+ constructor(message = "API rate limit exceeded.") {
23
50
  super(`${message}
24
- Please wait a moment before trying again or check your API quota at https://aistudio.google.com/`);
51
+ Please wait a moment before trying again or check your account quota.`);
25
52
  this.name = "RateLimitError";
26
53
  }
27
54
  };
28
- var GeminiProviderError = class extends Error {
55
+ var AuthenticationError = class extends Error {
56
+ constructor(message = "Authentication failed.") {
57
+ super(message);
58
+ this.name = "AuthenticationError";
59
+ }
60
+ };
61
+ var ModelNotFoundError = class extends Error {
62
+ constructor(model, provider) {
63
+ super(`Model '${model}' not found or not available for provider '${provider}'.`);
64
+ this.name = "ModelNotFoundError";
65
+ }
66
+ };
67
+ var LanternProviderError = class extends Error {
29
68
  statusCode;
30
- constructor(message, statusCode) {
69
+ provider;
70
+ constructor(message, statusCode, provider) {
31
71
  super(message);
32
- this.name = "GeminiProviderError";
72
+ this.name = "LanternProviderError";
33
73
  this.statusCode = statusCode;
74
+ this.provider = provider;
34
75
  }
35
76
  };
36
77
  function formatErrorMessage(err) {
78
+ if (err instanceof CancelledError) {
79
+ return pc.yellow(`
80
+ [Cancelled] ${err.message}`);
81
+ }
82
+ if (err instanceof FirstTokenTimeoutError) {
83
+ return pc.red(`
84
+ \u23F1\uFE0F ${err.message}
85
+ ` + pc.yellow(` Tip: Check your network connection or try lowering temperature or setting a faster model with 'lantern model set <id>'.`));
86
+ }
87
+ if (err instanceof TimeoutError) {
88
+ return pc.red(`
89
+ \u23F1\uFE0F ${err.message}
90
+ ` + pc.yellow(` Tip: You can increase the timeout limit using 'lantern config set timeoutMs 120000'.`));
91
+ }
37
92
  if (err instanceof MissingApiKeyError) {
38
- return pc.red(err.message);
93
+ return pc.red(`
94
+ ${err.message}`);
95
+ }
96
+ if (err instanceof RateLimitError) {
97
+ return pc.yellow(`
98
+ \u26A0\uFE0F ${err.message}`);
99
+ }
100
+ if (err instanceof AuthenticationError) {
101
+ return pc.red(`
102
+ \u{1F512} ${err.message}`);
103
+ }
104
+ if (err instanceof ModelNotFoundError) {
105
+ return pc.red(`
106
+ \u{1F50D} ${err.message}
107
+ ` + pc.dim(`Run 'lantern models list' to view available models for your account.`));
108
+ }
109
+ if (err instanceof Error) {
110
+ const msg = err.message || "";
111
+ if (msg.includes("API_KEY_INVALID") || msg.includes("401") || msg.includes("Incorrect API key")) {
112
+ return pc.red(`Authentication failed: Invalid API key. Update it with 'lantern keys set <provider>'.`);
113
+ }
114
+ if (msg.includes("RESOURCE_EXHAUSTED") || msg.includes("429")) {
115
+ return pc.yellow("Rate limit or quota reached. Please wait a few seconds before retrying.");
116
+ }
117
+ if (msg.includes("AbortError") || msg.includes("aborted") || msg.includes("canceled")) {
118
+ return pc.yellow("[Request aborted]");
119
+ }
120
+ return pc.red(`Error: ${msg}`);
121
+ }
122
+ return pc.red(`An unexpected error occurred: ${String(err)}`);
123
+ }
124
+
125
+ // src/provider/timeout.ts
126
+ function createLinkedAbortController(parentSignal, timeoutMs) {
127
+ const controller = new AbortController();
128
+ let timer = null;
129
+ if (timeoutMs && timeoutMs > 0) {
130
+ timer = setTimeout(() => {
131
+ controller.abort(new TimeoutError(timeoutMs));
132
+ }, timeoutMs);
133
+ }
134
+ const onParentAbort = () => {
135
+ controller.abort(parentSignal?.reason || new CancelledError());
136
+ };
137
+ if (parentSignal) {
138
+ if (parentSignal.aborted) {
139
+ onParentAbort();
140
+ } else {
141
+ parentSignal.addEventListener("abort", onParentAbort, { once: true });
142
+ }
143
+ }
144
+ const cleanup = () => {
145
+ if (timer) {
146
+ clearTimeout(timer);
147
+ timer = null;
148
+ }
149
+ if (parentSignal) {
150
+ parentSignal.removeEventListener("abort", onParentAbort);
151
+ }
152
+ };
153
+ return { controller, cleanup };
154
+ }
155
+ async function* wrapStreamWithFirstTokenTimeout(stream, firstTokenTimeoutMs, provider, model, signal, onFirstToken) {
156
+ let firstTokenReceived = false;
157
+ const startTime = Date.now();
158
+ let timer = null;
159
+ let timedOut = false;
160
+ const timerPromise = new Promise((_, reject) => {
161
+ timer = setTimeout(() => {
162
+ if (!firstTokenReceived) {
163
+ timedOut = true;
164
+ reject(new FirstTokenTimeoutError(firstTokenTimeoutMs, provider, model));
165
+ }
166
+ }, firstTokenTimeoutMs);
167
+ });
168
+ const abortPromise = new Promise((_, reject) => {
169
+ if (signal) {
170
+ if (signal.aborted) {
171
+ reject(signal.reason || new CancelledError());
172
+ } else {
173
+ signal.addEventListener("abort", () => reject(signal.reason || new CancelledError()), { once: true });
174
+ }
175
+ }
176
+ });
177
+ const iterator = stream[Symbol.asyncIterator]();
178
+ try {
179
+ while (true) {
180
+ if (signal?.aborted) {
181
+ throw signal.reason || new CancelledError();
182
+ }
183
+ if (!firstTokenReceived) {
184
+ const nextPromise = iterator.next();
185
+ const result = await Promise.race([nextPromise, timerPromise, abortPromise]);
186
+ if (timer) {
187
+ clearTimeout(timer);
188
+ timer = null;
189
+ }
190
+ if (result.done) {
191
+ break;
192
+ }
193
+ firstTokenReceived = true;
194
+ if (onFirstToken) {
195
+ onFirstToken(Date.now() - startTime);
196
+ }
197
+ yield result.value;
198
+ } else {
199
+ if (signal?.aborted) {
200
+ throw signal.reason || new CancelledError();
201
+ }
202
+ const nextPromise = iterator.next();
203
+ const result = signal ? await Promise.race([nextPromise, abortPromise]) : await nextPromise;
204
+ if (result.done) {
205
+ break;
206
+ }
207
+ yield result.value;
208
+ }
209
+ }
210
+ } finally {
211
+ if (timer) {
212
+ clearTimeout(timer);
213
+ }
214
+ if (typeof iterator.return === "function") {
215
+ try {
216
+ await iterator.return();
217
+ } catch {
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ // src/config/schema.ts
224
+ var DEFAULT_CONFIG = {
225
+ activeProvider: "gemini",
226
+ defaultModel: "gemini-3.8-flash",
227
+ maxSteps: 25,
228
+ temperature: 0.2,
229
+ maxOutputTokens: 8192,
230
+ yolo: false,
231
+ theme: "default",
232
+ autoCompactThreshold: 30,
233
+ // turns before auto-compacting
234
+ timeoutMs: 6e4,
235
+ firstTokenTimeoutMs: 3e4,
236
+ showTiming: false,
237
+ lowLatency: false,
238
+ activeSkills: [],
239
+ mcpServers: {},
240
+ providers: {}
241
+ };
242
+ var ALLOWED_CONFIG_KEYS = [
243
+ "activeProvider",
244
+ "defaultModel",
245
+ "maxSteps",
246
+ "temperature",
247
+ "maxOutputTokens",
248
+ "yolo",
249
+ "theme",
250
+ "autoCompactThreshold",
251
+ "timeoutMs",
252
+ "firstTokenTimeoutMs",
253
+ "showTiming",
254
+ "lowLatency",
255
+ "activeSkills"
256
+ ];
257
+ function validateConfigValue(key, value) {
258
+ switch (key) {
259
+ case "activeProvider":
260
+ if (typeof value !== "string" || value.trim().length === 0) {
261
+ return { valid: false, error: "activeProvider must be a non-empty string" };
262
+ }
263
+ return { valid: true, parsedValue: value.trim().toLowerCase() };
264
+ case "defaultModel":
265
+ if (typeof value !== "string" || value.trim().length === 0) {
266
+ return { valid: false, error: "defaultModel must be a non-empty string" };
267
+ }
268
+ return { valid: true, parsedValue: value.trim() };
269
+ case "maxSteps": {
270
+ const num = Number(value);
271
+ if (isNaN(num) || num < 1 || num > 100) {
272
+ return { valid: false, error: "maxSteps must be a number between 1 and 100" };
273
+ }
274
+ return { valid: true, parsedValue: Math.round(num) };
275
+ }
276
+ case "temperature": {
277
+ const num = Number(value);
278
+ if (isNaN(num) || num < 0 || num > 2) {
279
+ return { valid: false, error: "temperature must be a number between 0.0 and 2.0" };
280
+ }
281
+ return { valid: true, parsedValue: num };
282
+ }
283
+ case "maxOutputTokens": {
284
+ const num = Number(value);
285
+ if (isNaN(num) || num < 256 || num > 65536) {
286
+ return { valid: false, error: "maxOutputTokens must be between 256 and 65536" };
287
+ }
288
+ return { valid: true, parsedValue: Math.round(num) };
289
+ }
290
+ case "yolo": {
291
+ if (typeof value === "boolean") return { valid: true, parsedValue: value };
292
+ if (value === "true" || value === "1") return { valid: true, parsedValue: true };
293
+ if (value === "false" || value === "0") return { valid: true, parsedValue: false };
294
+ return { valid: false, error: "yolo must be a boolean (true/false)" };
295
+ }
296
+ case "theme": {
297
+ if (value === "default" || value === "ocean" || value === "minimal") {
298
+ return { valid: true, parsedValue: value };
299
+ }
300
+ return { valid: false, error: "theme must be 'default', 'ocean', or 'minimal'" };
301
+ }
302
+ case "autoCompactThreshold": {
303
+ const num = Number(value);
304
+ if (isNaN(num) || num < 5 || num > 200) {
305
+ return { valid: false, error: "autoCompactThreshold must be a number between 5 and 200" };
306
+ }
307
+ return { valid: true, parsedValue: Math.round(num) };
308
+ }
309
+ case "timeoutMs": {
310
+ const num = Number(value);
311
+ if (isNaN(num) || num < 5e3) {
312
+ return { valid: false, error: "timeoutMs must be at least 5000 (ms)" };
313
+ }
314
+ return { valid: true, parsedValue: Math.round(num) };
315
+ }
316
+ case "firstTokenTimeoutMs": {
317
+ const num = Number(value);
318
+ if (isNaN(num) || num < 3e3) {
319
+ return { valid: false, error: "firstTokenTimeoutMs must be at least 3000 (ms)" };
320
+ }
321
+ return { valid: true, parsedValue: Math.round(num) };
322
+ }
323
+ case "showTiming": {
324
+ if (typeof value === "boolean") return { valid: true, parsedValue: value };
325
+ if (value === "true" || value === "1") return { valid: true, parsedValue: true };
326
+ if (value === "false" || value === "0") return { valid: true, parsedValue: false };
327
+ return { valid: false, error: "showTiming must be a boolean (true/false)" };
328
+ }
329
+ case "lowLatency": {
330
+ if (typeof value === "boolean") return { valid: true, parsedValue: value };
331
+ if (value === "true" || value === "1") return { valid: true, parsedValue: true };
332
+ if (value === "false" || value === "0") return { valid: true, parsedValue: false };
333
+ return { valid: false, error: "lowLatency must be a boolean (true/false)" };
334
+ }
335
+ case "activeSkills": {
336
+ if (Array.isArray(value)) {
337
+ return { valid: true, parsedValue: value.map(String) };
338
+ }
339
+ if (typeof value === "string") {
340
+ return { valid: true, parsedValue: value.split(",").map((s) => s.trim()).filter(Boolean) };
341
+ }
342
+ return { valid: false, error: "activeSkills must be an array of strings or comma-separated string" };
343
+ }
344
+ default:
345
+ return { valid: false, error: `Unknown configuration key: ${key}` };
346
+ }
347
+ }
348
+
349
+ // src/config/config.ts
350
+ import fs from "fs";
351
+ import path from "path";
352
+ import os from "os";
353
+ function getLanternHomeDir() {
354
+ const customHome = process.env.CODELANTERN_HOME || process.env.LANTERN_HOME;
355
+ if (customHome) {
356
+ if (!fs.existsSync(customHome)) {
357
+ try {
358
+ fs.mkdirSync(customHome, { recursive: true });
359
+ } catch {
360
+ }
361
+ }
362
+ return customHome;
363
+ }
364
+ const defaultDir = path.join(os.homedir(), ".codelantern");
365
+ if (!fs.existsSync(defaultDir)) {
366
+ try {
367
+ fs.mkdirSync(defaultDir, { recursive: true });
368
+ } catch {
369
+ }
370
+ }
371
+ return defaultDir;
372
+ }
373
+ function getConfigFilePath() {
374
+ return path.join(getLanternHomeDir(), "config.json");
375
+ }
376
+ function loadConfig() {
377
+ const configPath = getConfigFilePath();
378
+ if (!fs.existsSync(configPath)) {
379
+ return { ...DEFAULT_CONFIG };
380
+ }
381
+ try {
382
+ const raw = fs.readFileSync(configPath, "utf8");
383
+ const parsed = JSON.parse(raw);
384
+ const merged = {
385
+ ...DEFAULT_CONFIG,
386
+ ...parsed
387
+ };
388
+ if (!merged.mcpServers || typeof merged.mcpServers !== "object") {
389
+ merged.mcpServers = {};
390
+ }
391
+ return merged;
392
+ } catch {
393
+ return { ...DEFAULT_CONFIG };
394
+ }
395
+ }
396
+ function saveConfig(config) {
397
+ const configPath = getConfigFilePath();
398
+ const safeConfig = {
399
+ activeProvider: config.activeProvider || DEFAULT_CONFIG.activeProvider,
400
+ defaultModel: config.defaultModel || DEFAULT_CONFIG.defaultModel,
401
+ maxSteps: config.maxSteps || DEFAULT_CONFIG.maxSteps,
402
+ temperature: config.temperature ?? DEFAULT_CONFIG.temperature,
403
+ maxOutputTokens: config.maxOutputTokens || DEFAULT_CONFIG.maxOutputTokens,
404
+ yolo: Boolean(config.yolo),
405
+ theme: config.theme || DEFAULT_CONFIG.theme,
406
+ autoCompactThreshold: config.autoCompactThreshold || DEFAULT_CONFIG.autoCompactThreshold,
407
+ timeoutMs: config.timeoutMs || DEFAULT_CONFIG.timeoutMs,
408
+ firstTokenTimeoutMs: config.firstTokenTimeoutMs || DEFAULT_CONFIG.firstTokenTimeoutMs,
409
+ showTiming: Boolean(config.showTiming),
410
+ lowLatency: Boolean(config.lowLatency),
411
+ activeSkills: Array.isArray(config.activeSkills) ? config.activeSkills : [],
412
+ mcpServers: config.mcpServers || {},
413
+ providers: config.providers || {}
414
+ };
415
+ try {
416
+ fs.writeFileSync(configPath, JSON.stringify(safeConfig, null, 2), {
417
+ encoding: "utf8"
418
+ });
419
+ } catch (err) {
420
+ console.error(`Warning: Failed to save config to ${configPath}: ${err.message}`);
421
+ }
422
+ }
423
+ function setConfigValue(key, value) {
424
+ const validation = validateConfigValue(key, value);
425
+ if (!validation.valid) {
426
+ return { success: false, error: validation.error };
427
+ }
428
+ const current = loadConfig();
429
+ current[key] = validation.parsedValue;
430
+ saveConfig(current);
431
+ return { success: true, updatedConfig: current };
432
+ }
433
+ function getConfigValue(key) {
434
+ const current = loadConfig();
435
+ return current[key];
436
+ }
437
+
438
+ // src/security/credentials.ts
439
+ import fs2 from "fs";
440
+ import path2 from "path";
441
+ import os2 from "os";
442
+ import crypto from "crypto";
443
+ var PROVIDER_ENV_VARS = {
444
+ gemini: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
445
+ openai: ["OPENAI_API_KEY"],
446
+ anthropic: ["ANTHROPIC_API_KEY"],
447
+ openrouter: ["OPENROUTER_API_KEY"],
448
+ custom: ["CUSTOM_API_KEY", "OPENAI_API_KEY"]
449
+ };
450
+ function getCredentialsFilePath() {
451
+ return path2.join(getLanternHomeDir(), "credentials.json");
452
+ }
453
+ function getMachineDerivedKey() {
454
+ const salt = `codelantern_${os2.hostname()}_${os2.userInfo().username}`;
455
+ const secret = `${os2.homedir()}_${process.platform}_${os2.arch()}`;
456
+ return crypto.scryptSync(secret, salt, 32);
457
+ }
458
+ function encryptSecret(plainText) {
459
+ const iv = crypto.randomBytes(12);
460
+ const key = getMachineDerivedKey();
461
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
462
+ let encrypted = cipher.update(plainText, "utf8", "hex");
463
+ encrypted += cipher.final("hex");
464
+ const authTag = cipher.getAuthTag().toString("hex");
465
+ return `${iv.toString("hex")}:${authTag}:${encrypted}`;
466
+ }
467
+ function decryptSecret(encryptedPayload) {
468
+ try {
469
+ const parts = encryptedPayload.split(":");
470
+ if (parts.length !== 3) return null;
471
+ const [ivHex, authTagHex, encryptedHex] = parts;
472
+ const iv = Buffer.from(ivHex, "hex");
473
+ const authTag = Buffer.from(authTagHex, "hex");
474
+ const key = getMachineDerivedKey();
475
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
476
+ decipher.setAuthTag(authTag);
477
+ let decrypted = decipher.update(encryptedHex, "hex", "utf8");
478
+ decrypted += decipher.final("utf8");
479
+ return decrypted;
480
+ } catch {
481
+ return null;
482
+ }
483
+ }
484
+ function maskApiKey(key) {
485
+ if (!key) return "(not configured)";
486
+ const clean = key.trim();
487
+ if (clean.length <= 8) return "********";
488
+ return `${clean.slice(0, 4)}...${clean.slice(-4)}`;
489
+ }
490
+ function getApiKey(provider) {
491
+ const norm = provider.toLowerCase();
492
+ const envVarNames = PROVIDER_ENV_VARS[norm] || [`${norm.toUpperCase()}_API_KEY`];
493
+ for (const envVar of envVarNames) {
494
+ const val = process.env[envVar];
495
+ if (val && val.trim().length > 0) {
496
+ return { key: val.trim(), source: "env" };
497
+ }
498
+ }
499
+ const filePath = getCredentialsFilePath();
500
+ if (fs2.existsSync(filePath)) {
501
+ try {
502
+ const raw = fs2.readFileSync(filePath, "utf8");
503
+ const data = JSON.parse(raw);
504
+ if (data[norm]) {
505
+ const decrypted = decryptSecret(data[norm]);
506
+ if (decrypted) {
507
+ return { key: decrypted, source: "store" };
508
+ }
509
+ }
510
+ } catch {
511
+ }
512
+ }
513
+ return { key: null };
514
+ }
515
+ function setApiKey(provider, apiKey) {
516
+ const norm = provider.toLowerCase();
517
+ const filePath = getCredentialsFilePath();
518
+ let data = {};
519
+ if (fs2.existsSync(filePath)) {
520
+ try {
521
+ const raw = fs2.readFileSync(filePath, "utf8");
522
+ data = JSON.parse(raw);
523
+ } catch {
524
+ data = {};
525
+ }
526
+ }
527
+ data[norm] = encryptSecret(apiKey.trim());
528
+ try {
529
+ fs2.writeFileSync(filePath, JSON.stringify(data, null, 2), {
530
+ encoding: "utf8",
531
+ mode: 384
532
+ });
533
+ } catch (err) {
534
+ console.error(`Warning: Failed to save credentials: ${err.message}`);
535
+ }
536
+ }
537
+ function removeApiKey(provider) {
538
+ const norm = provider.toLowerCase();
539
+ const filePath = getCredentialsFilePath();
540
+ if (!fs2.existsSync(filePath)) return false;
541
+ try {
542
+ const raw = fs2.readFileSync(filePath, "utf8");
543
+ const data = JSON.parse(raw);
544
+ if (data[norm]) {
545
+ delete data[norm];
546
+ fs2.writeFileSync(filePath, JSON.stringify(data, null, 2), {
547
+ encoding: "utf8",
548
+ mode: 384
549
+ });
550
+ return true;
551
+ }
552
+ } catch {
553
+ }
554
+ return false;
555
+ }
556
+ function listConfiguredKeys() {
557
+ const providers = ["gemini", "openai", "anthropic", "openrouter", "custom"];
558
+ const result = {};
559
+ for (const p of providers) {
560
+ const { key, source } = getApiKey(p);
561
+ result[p] = {
562
+ configured: Boolean(key),
563
+ source,
564
+ maskedKey: key ? maskApiKey(key) : "(not configured)"
565
+ };
566
+ }
567
+ return result;
568
+ }
569
+
570
+ // src/provider/gemini.ts
571
+ import { GoogleGenAI, Type } from "@google/genai";
572
+ var GeminiProvider = class {
573
+ id = "gemini";
574
+ name = "gemini";
575
+ displayName = "Google Gemini";
576
+ defaultModel = "gemini-3.8-flash";
577
+ cachedModels = null;
578
+ isConfigured() {
579
+ const { key } = getApiKey(this.id);
580
+ return Boolean(key && key.trim().length > 0);
581
+ }
582
+ getApiKey() {
583
+ return getApiKey(this.id).key;
584
+ }
585
+ getMaskedKey() {
586
+ const key = this.getApiKey();
587
+ return key ? maskApiKey(key) : "(not configured)";
588
+ }
589
+ getClient() {
590
+ const key = this.getApiKey();
591
+ if (!key) {
592
+ throw new MissingApiKeyError("gemini", "GEMINI_API_KEY");
593
+ }
594
+ return new GoogleGenAI({ apiKey: key });
595
+ }
596
+ getCapabilities(modelId) {
597
+ const isPro = modelId.includes("pro");
598
+ const isThinking = modelId.includes("thinking") || modelId.includes("2.0-flash-thinking");
599
+ return {
600
+ streaming: true,
601
+ tools: true,
602
+ images: true,
603
+ reasoning: isThinking || isPro,
604
+ contextWindow: isPro ? 2e6 : 1e6
605
+ };
606
+ }
607
+ async testConnection() {
608
+ const startTime = Date.now();
609
+ try {
610
+ const client = this.getClient();
611
+ const testModel = this.defaultModel;
612
+ await client.models.countTokens({
613
+ model: testModel,
614
+ contents: "test"
615
+ });
616
+ return {
617
+ success: true,
618
+ latencyMs: Date.now() - startTime,
619
+ resolvedModel: testModel
620
+ };
621
+ } catch (err) {
622
+ return {
623
+ success: false,
624
+ latencyMs: Date.now() - startTime,
625
+ error: err.message || String(err)
626
+ };
627
+ }
628
+ }
629
+ async listModels(refresh = false) {
630
+ if (!refresh && this.cachedModels && this.cachedModels.length > 0) {
631
+ return this.cachedModels;
632
+ }
633
+ try {
634
+ const client = this.getClient();
635
+ const pagedList = await client.models.list();
636
+ const models = [];
637
+ for await (const m of pagedList) {
638
+ const rawName = m.name || "";
639
+ const modelId = rawName.startsWith("models/") ? rawName.slice(7) : rawName;
640
+ if (!modelId.includes("gemini")) {
641
+ continue;
642
+ }
643
+ const capabilities = this.getCapabilities(modelId);
644
+ models.push({
645
+ id: modelId,
646
+ name: m.displayName || modelId,
647
+ provider: this.id,
648
+ description: m.description,
649
+ contextWindow: capabilities.contextWindow,
650
+ capabilities
651
+ });
652
+ }
653
+ if (models.length > 0) {
654
+ this.cachedModels = models;
655
+ return models;
656
+ }
657
+ } catch {
658
+ }
659
+ const verifiedModels = [
660
+ "gemini-3.8-flash",
661
+ "gemini-2.5-flash",
662
+ "gemini-2.5-pro",
663
+ "gemini-2.0-flash",
664
+ "gemini-2.0-flash-lite",
665
+ "gemini-1.5-flash",
666
+ "gemini-1.5-pro"
667
+ ];
668
+ return verifiedModels.map((id) => ({
669
+ id,
670
+ name: id,
671
+ provider: this.id,
672
+ capabilities: this.getCapabilities(id),
673
+ contextWindow: id.includes("pro") ? 2e6 : 1e6
674
+ }));
675
+ }
676
+ async streamGenerate(params) {
677
+ const overallStartTime = Date.now();
678
+ const timing = {
679
+ totalMs: 0,
680
+ toolExecutionMs: {}
681
+ };
682
+ const client = this.getClient();
683
+ const contents = this.convertHistoryToContents(params.history);
684
+ const functionDeclarations = this.convertToolsToDeclarations(params.tools);
685
+ const config = {
686
+ systemInstruction: params.systemInstruction,
687
+ temperature: params.temperature ?? (params.lowLatency ? 0.1 : 0.2)
688
+ };
689
+ if (params.maxOutputTokens) {
690
+ config.maxOutputTokens = params.maxOutputTokens;
691
+ }
692
+ const toolList = [];
693
+ if (functionDeclarations.length > 0) {
694
+ toolList.push({ functionDeclarations });
695
+ }
696
+ if (params.enableSearchGrounding) {
697
+ toolList.push({ googleSearch: {} });
698
+ }
699
+ if (toolList.length > 0) {
700
+ config.tools = toolList;
701
+ }
702
+ const timeoutMs = params.timeoutMs || 6e4;
703
+ const firstTokenTimeoutMs = params.firstTokenTimeoutMs || 3e4;
704
+ let attempts = 0;
705
+ const maxAttempts = 3;
706
+ let lastError = null;
707
+ while (attempts < maxAttempts) {
708
+ attempts++;
709
+ const connectStart = Date.now();
710
+ if (params.callbacks.onStateChange) {
711
+ params.callbacks.onStateChange(attempts > 1 ? `retrying:${attempts}` : "connecting");
712
+ }
713
+ const { controller, cleanup } = createLinkedAbortController(params.signal, timeoutMs);
714
+ try {
715
+ const responseStream = await client.models.generateContentStream({
716
+ model: params.model,
717
+ contents,
718
+ config
719
+ });
720
+ timing.connectMs = Date.now() - connectStart;
721
+ if (params.callbacks.onStateChange) {
722
+ params.callbacks.onStateChange("waiting_first_token");
723
+ }
724
+ let fullText = "";
725
+ const collectedToolCalls = [];
726
+ const usage = {
727
+ promptTokens: 0,
728
+ candidatesTokens: 0,
729
+ totalTokens: 0
730
+ };
731
+ const wrappedStream = wrapStreamWithFirstTokenTimeout(
732
+ responseStream,
733
+ firstTokenTimeoutMs,
734
+ this.displayName,
735
+ params.model,
736
+ controller.signal,
737
+ (latencyMs) => {
738
+ timing.ttftMs = latencyMs;
739
+ if (params.callbacks.onStateChange) {
740
+ params.callbacks.onStateChange("streaming");
741
+ }
742
+ }
743
+ );
744
+ const streamStart = Date.now();
745
+ for await (const chunk of wrappedStream) {
746
+ if (chunk.text) {
747
+ fullText += chunk.text;
748
+ if (params.callbacks.onChunk) {
749
+ params.callbacks.onChunk(chunk.text);
750
+ }
751
+ }
752
+ if (chunk.functionCalls && chunk.functionCalls.length > 0) {
753
+ for (const fc of chunk.functionCalls) {
754
+ collectedToolCalls.push({
755
+ id: fc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
756
+ name: fc.name || "",
757
+ args: fc.args || {}
758
+ });
759
+ }
760
+ }
761
+ if (chunk.usageMetadata) {
762
+ usage.promptTokens = chunk.usageMetadata.promptTokenCount ?? usage.promptTokens;
763
+ usage.candidatesTokens = chunk.usageMetadata.candidatesTokenCount ?? usage.candidatesTokens;
764
+ usage.totalTokens = chunk.usageMetadata.totalTokenCount ?? usage.totalTokens;
765
+ if (params.callbacks.onUsage) {
766
+ params.callbacks.onUsage(usage);
767
+ }
768
+ }
769
+ }
770
+ timing.streamMs = Date.now() - streamStart;
771
+ timing.totalMs = Date.now() - overallStartTime;
772
+ if (params.callbacks.onToolCalls && collectedToolCalls.length > 0) {
773
+ params.callbacks.onToolCalls(collectedToolCalls);
774
+ }
775
+ cleanup();
776
+ return {
777
+ text: fullText,
778
+ toolCalls: collectedToolCalls,
779
+ usage,
780
+ timing
781
+ };
782
+ } catch (err) {
783
+ cleanup();
784
+ lastError = err;
785
+ if (params.signal?.aborted) {
786
+ throw err;
787
+ }
788
+ const msg = String(err?.message || err);
789
+ if (msg.includes("API_KEY_INVALID") || msg.includes("401") || msg.includes("403")) {
790
+ throw new LanternProviderError(`Authentication failed: ${msg}`, 401, this.id);
791
+ }
792
+ if (msg.includes("NOT_FOUND") || msg.includes("models/")) {
793
+ throw new ModelNotFoundError(params.model, this.displayName);
794
+ }
795
+ const isRateLimit = msg.includes("429") || msg.includes("RESOURCE_EXHAUSTED");
796
+ const isTransient = msg.includes("503") || msg.includes("overloaded") || msg.includes("ECONNRESET") || msg.includes("ETIMEDOUT") || msg.includes("socket hang up");
797
+ if ((isRateLimit || isTransient) && attempts < maxAttempts) {
798
+ const backoff = Math.min(1e3 * Math.pow(1.5, attempts), 3e3);
799
+ if (params.callbacks.onStateChange) {
800
+ params.callbacks.onStateChange(`retrying:${attempts}`);
801
+ }
802
+ await new Promise((r) => setTimeout(r, backoff));
803
+ continue;
804
+ }
805
+ if (isRateLimit) {
806
+ throw new RateLimitError(msg);
807
+ }
808
+ throw new LanternProviderError(`Gemini request failed: ${msg}`, void 0, this.id);
809
+ }
810
+ }
811
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
812
+ }
813
+ convertHistoryToContents(turns) {
814
+ const contents = [];
815
+ for (const turn of turns) {
816
+ const parts = [];
817
+ if (turn.role === "user") {
818
+ if (turn.text) {
819
+ parts.push({ text: turn.text });
820
+ }
821
+ if (turn.attachments && turn.attachments.length > 0) {
822
+ for (const att of turn.attachments) {
823
+ if (att.base64Data && att.mimeType) {
824
+ parts.push({
825
+ inlineData: {
826
+ mimeType: att.mimeType,
827
+ data: att.base64Data
828
+ }
829
+ });
830
+ } else if (att.textContent) {
831
+ parts.push({
832
+ text: `[Attached File: ${att.name}]
833
+ \`\`\`
834
+ ${att.textContent}
835
+ \`\`\``
836
+ });
837
+ }
838
+ }
839
+ }
840
+ if (parts.length > 0) {
841
+ contents.push({ role: "user", parts });
842
+ }
843
+ } else if (turn.role === "model") {
844
+ if (turn.text) {
845
+ parts.push({ text: turn.text });
846
+ }
847
+ if (turn.toolCalls && turn.toolCalls.length > 0) {
848
+ for (const tc of turn.toolCalls) {
849
+ parts.push({
850
+ functionCall: {
851
+ id: tc.id,
852
+ name: tc.name,
853
+ args: tc.args
854
+ }
855
+ });
856
+ }
857
+ }
858
+ if (parts.length > 0) {
859
+ contents.push({ role: "model", parts });
860
+ }
861
+ } else if (turn.role === "tool") {
862
+ if (turn.toolResults && turn.toolResults.length > 0) {
863
+ for (const res of turn.toolResults) {
864
+ parts.push({
865
+ functionResponse: {
866
+ id: res.callId,
867
+ name: res.name,
868
+ response: {
869
+ result: res.error ? { error: res.error } : res.result
870
+ }
871
+ }
872
+ });
873
+ }
874
+ }
875
+ if (parts.length > 0) {
876
+ contents.push({ role: "tool", parts });
877
+ }
878
+ }
879
+ }
880
+ return contents;
881
+ }
882
+ convertToolsToDeclarations(tools) {
883
+ return tools.map((tool) => {
884
+ const properties = {};
885
+ for (const [propName, propDef] of Object.entries(tool.parameters.properties)) {
886
+ let typeVal = Type.STRING;
887
+ switch (propDef.type) {
888
+ case "number":
889
+ case "integer":
890
+ typeVal = Type.NUMBER;
891
+ break;
892
+ case "boolean":
893
+ typeVal = Type.BOOLEAN;
894
+ break;
895
+ case "array":
896
+ typeVal = Type.ARRAY;
897
+ break;
898
+ case "object":
899
+ typeVal = Type.OBJECT;
900
+ break;
901
+ default:
902
+ typeVal = Type.STRING;
903
+ }
904
+ properties[propName] = {
905
+ type: typeVal,
906
+ description: propDef.description,
907
+ ...propDef.enum ? { enum: propDef.enum } : {},
908
+ ...propDef.items ? { items: { type: Type.STRING } } : {}
909
+ };
910
+ }
911
+ return {
912
+ name: tool.name,
913
+ description: tool.description,
914
+ parameters: {
915
+ type: Type.OBJECT,
916
+ properties,
917
+ required: tool.parameters.required || []
918
+ }
919
+ };
920
+ });
921
+ }
922
+ };
923
+
924
+ // src/provider/openaiProvider.ts
925
+ var OpenAIProvider = class {
926
+ id;
927
+ name;
928
+ displayName;
929
+ defaultModel;
930
+ baseUrl;
931
+ customHeaders;
932
+ cachedModels = null;
933
+ constructor(options) {
934
+ this.id = options?.id || "openai";
935
+ this.name = options?.name || "openai";
936
+ this.displayName = options?.displayName || "OpenAI";
937
+ this.baseUrl = (options?.baseUrl || "https://api.openai.com/v1").replace(/\/+$/, "");
938
+ this.defaultModel = options?.defaultModel || "gpt-4o";
939
+ this.customHeaders = options?.headers || {};
940
+ }
941
+ isConfigured() {
942
+ const { key } = getApiKey(this.id);
943
+ return Boolean(key && key.trim().length > 0);
944
+ }
945
+ getApiKey() {
946
+ return getApiKey(this.id).key;
947
+ }
948
+ getMaskedKey() {
949
+ const key = this.getApiKey();
950
+ return key ? maskApiKey(key) : "(not configured)";
951
+ }
952
+ getCapabilities(modelId) {
953
+ const lower = modelId.toLowerCase();
954
+ const isReasoning = lower.includes("o1") || lower.includes("o3") || lower.includes("r1");
955
+ const isMini = lower.includes("mini") || lower.includes("nano");
956
+ return {
957
+ streaming: true,
958
+ tools: !isReasoning || lower.includes("o3-mini") || lower.includes("o1-mini"),
959
+ images: !isMini && !lower.includes("embedding"),
960
+ reasoning: isReasoning,
961
+ contextWindow: lower.includes("gpt-4o") ? 128e3 : 64e3
962
+ };
963
+ }
964
+ async testConnection() {
965
+ const key = this.getApiKey();
966
+ if (!key) {
967
+ return { success: false, latencyMs: 0, error: `Missing API key for ${this.displayName}` };
968
+ }
969
+ const startTime = Date.now();
970
+ try {
971
+ const controller = new AbortController();
972
+ const timer = setTimeout(() => controller.abort(), 1e4);
973
+ const res = await fetch(`${this.baseUrl}/models`, {
974
+ method: "GET",
975
+ headers: {
976
+ "Authorization": `Bearer ${key}`,
977
+ ...this.customHeaders
978
+ },
979
+ signal: controller.signal
980
+ });
981
+ clearTimeout(timer);
982
+ if (!res.ok) {
983
+ const text = await res.text();
984
+ return {
985
+ success: false,
986
+ latencyMs: Date.now() - startTime,
987
+ error: `HTTP ${res.status}: ${text.slice(0, 200)}`
988
+ };
989
+ }
990
+ const data = await res.json();
991
+ const count = Array.isArray(data?.data) ? data.data.length : 0;
992
+ return {
993
+ success: true,
994
+ latencyMs: Date.now() - startTime,
995
+ modelsCount: count,
996
+ resolvedModel: this.defaultModel
997
+ };
998
+ } catch (err) {
999
+ return {
1000
+ success: false,
1001
+ latencyMs: Date.now() - startTime,
1002
+ error: err.message || String(err)
1003
+ };
1004
+ }
39
1005
  }
40
- if (err instanceof RateLimitError) {
41
- return pc.yellow(`\u26A0\uFE0F ${err.message}`);
1006
+ async listModels(refresh = false) {
1007
+ if (!refresh && this.cachedModels && this.cachedModels.length > 0) {
1008
+ return this.cachedModels;
1009
+ }
1010
+ const key = this.getApiKey();
1011
+ if (key) {
1012
+ try {
1013
+ const controller = new AbortController();
1014
+ const timer = setTimeout(() => controller.abort(), 1e4);
1015
+ const res = await fetch(`${this.baseUrl}/models`, {
1016
+ method: "GET",
1017
+ headers: {
1018
+ "Authorization": `Bearer ${key}`,
1019
+ ...this.customHeaders
1020
+ },
1021
+ signal: controller.signal
1022
+ });
1023
+ clearTimeout(timer);
1024
+ if (res.ok) {
1025
+ const json = await res.json();
1026
+ if (Array.isArray(json?.data)) {
1027
+ const models = [];
1028
+ for (const item of json.data) {
1029
+ const id = item.id;
1030
+ if (typeof id !== "string") continue;
1031
+ const lower = id.toLowerCase();
1032
+ if (lower.includes("whisper") || lower.includes("tts") || lower.includes("dall-e") || lower.includes("text-embedding") || lower.includes("moderation")) {
1033
+ continue;
1034
+ }
1035
+ models.push({
1036
+ id,
1037
+ name: id,
1038
+ provider: this.id,
1039
+ capabilities: this.getCapabilities(id)
1040
+ });
1041
+ }
1042
+ if (models.length > 0) {
1043
+ this.cachedModels = models;
1044
+ return models;
1045
+ }
1046
+ }
1047
+ }
1048
+ } catch {
1049
+ }
1050
+ }
1051
+ const verified = [
1052
+ "gpt-4o",
1053
+ "gpt-4o-mini",
1054
+ "o3-mini",
1055
+ "o1",
1056
+ "o1-mini",
1057
+ "gpt-4-turbo"
1058
+ ];
1059
+ return verified.map((id) => ({
1060
+ id,
1061
+ name: id,
1062
+ provider: this.id,
1063
+ capabilities: this.getCapabilities(id)
1064
+ }));
42
1065
  }
43
- if (err instanceof Error) {
44
- const msg = err.message || "";
45
- if (msg.includes("API_KEY_INVALID") || msg.includes("API key not valid")) {
46
- return pc.red("Invalid Gemini API Key. Please verify your GEMINI_API_KEY at https://aistudio.google.com/app/apikey");
1066
+ async streamGenerate(params) {
1067
+ const key = this.getApiKey();
1068
+ if (!key) {
1069
+ throw new MissingApiKeyError(this.id, `${this.id.toUpperCase()}_API_KEY`);
47
1070
  }
48
- if (msg.includes("RESOURCE_EXHAUSTED") || msg.includes("429")) {
49
- return pc.yellow("Rate limit / quota exceeded. Please wait a few seconds or check your quota limits.");
1071
+ const overallStartTime = Date.now();
1072
+ const timing = {
1073
+ totalMs: 0,
1074
+ toolExecutionMs: {}
1075
+ };
1076
+ const messages = this.convertHistoryToMessages(params.systemInstruction, params.history);
1077
+ const tools = this.convertTools(params.tools);
1078
+ const payload = {
1079
+ model: params.model,
1080
+ messages,
1081
+ stream: true,
1082
+ stream_options: { include_usage: true }
1083
+ };
1084
+ if (tools.length > 0) {
1085
+ payload.tools = tools;
1086
+ }
1087
+ if (typeof params.temperature === "number") {
1088
+ payload.temperature = params.temperature;
1089
+ }
1090
+ if (params.maxOutputTokens) {
1091
+ payload.max_tokens = params.maxOutputTokens;
1092
+ }
1093
+ const timeoutMs = params.timeoutMs || 6e4;
1094
+ const firstTokenTimeoutMs = params.firstTokenTimeoutMs || 3e4;
1095
+ let attempts = 0;
1096
+ const maxAttempts = 3;
1097
+ let lastError = null;
1098
+ while (attempts < maxAttempts) {
1099
+ attempts++;
1100
+ const connectStart = Date.now();
1101
+ if (params.callbacks.onStateChange) {
1102
+ params.callbacks.onStateChange(attempts > 1 ? `retrying:${attempts}` : "connecting");
1103
+ }
1104
+ const { controller, cleanup } = createLinkedAbortController(params.signal, timeoutMs);
1105
+ try {
1106
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
1107
+ method: "POST",
1108
+ headers: {
1109
+ "Content-Type": "application/json",
1110
+ "Authorization": `Bearer ${key}`,
1111
+ ...this.customHeaders
1112
+ },
1113
+ body: JSON.stringify(payload),
1114
+ signal: controller.signal
1115
+ });
1116
+ timing.connectMs = Date.now() - connectStart;
1117
+ if (!response.ok) {
1118
+ const errorBody = await response.text();
1119
+ if (response.status === 401 || response.status === 403) {
1120
+ throw new AuthenticationError(`OpenAI Auth Error (${response.status}): ${errorBody}`);
1121
+ }
1122
+ if (response.status === 429) {
1123
+ throw new RateLimitError(`OpenAI Rate Limit: ${errorBody}`);
1124
+ }
1125
+ if (response.status === 404) {
1126
+ throw new ModelNotFoundError(params.model, this.displayName);
1127
+ }
1128
+ throw new LanternProviderError(`OpenAI API Error (${response.status}): ${errorBody}`, response.status, this.id);
1129
+ }
1130
+ if (!response.body) {
1131
+ throw new LanternProviderError("OpenAI response body is null", void 0, this.id);
1132
+ }
1133
+ if (params.callbacks.onStateChange) {
1134
+ params.callbacks.onStateChange("waiting_first_token");
1135
+ }
1136
+ const rawStream = this.readSSEStream(response.body);
1137
+ let fullText = "";
1138
+ const toolCallsMap = {};
1139
+ const usage = { promptTokens: 0, candidatesTokens: 0, totalTokens: 0 };
1140
+ const wrappedStream = wrapStreamWithFirstTokenTimeout(
1141
+ rawStream,
1142
+ firstTokenTimeoutMs,
1143
+ this.displayName,
1144
+ params.model,
1145
+ controller.signal,
1146
+ (latencyMs) => {
1147
+ timing.ttftMs = latencyMs;
1148
+ if (params.callbacks.onStateChange) {
1149
+ params.callbacks.onStateChange("streaming");
1150
+ }
1151
+ }
1152
+ );
1153
+ const streamStart = Date.now();
1154
+ for await (const chunk of wrappedStream) {
1155
+ const delta = chunk.choices?.[0]?.delta;
1156
+ if (delta?.content) {
1157
+ fullText += delta.content;
1158
+ if (params.callbacks.onChunk) {
1159
+ params.callbacks.onChunk(delta.content);
1160
+ }
1161
+ }
1162
+ if (Array.isArray(delta?.tool_calls)) {
1163
+ for (const tc of delta.tool_calls) {
1164
+ const index = tc.index ?? 0;
1165
+ if (!toolCallsMap[index]) {
1166
+ toolCallsMap[index] = {
1167
+ id: tc.id || `call_${Date.now()}_${index}`,
1168
+ name: tc.function?.name || "",
1169
+ argsText: tc.function?.arguments || ""
1170
+ };
1171
+ } else {
1172
+ if (tc.id) toolCallsMap[index].id = tc.id;
1173
+ if (tc.function?.name) toolCallsMap[index].name = tc.function.name;
1174
+ if (tc.function?.arguments) toolCallsMap[index].argsText += tc.function.arguments;
1175
+ }
1176
+ }
1177
+ }
1178
+ if (chunk.usage) {
1179
+ usage.promptTokens = chunk.usage.prompt_tokens ?? usage.promptTokens;
1180
+ usage.candidatesTokens = chunk.usage.completion_tokens ?? usage.candidatesTokens;
1181
+ usage.totalTokens = chunk.usage.total_tokens ?? usage.totalTokens;
1182
+ if (params.callbacks.onUsage) {
1183
+ params.callbacks.onUsage(usage);
1184
+ }
1185
+ }
1186
+ }
1187
+ timing.streamMs = Date.now() - streamStart;
1188
+ timing.totalMs = Date.now() - overallStartTime;
1189
+ const collectedToolCalls = Object.values(toolCallsMap).map((tc) => {
1190
+ let parsedArgs = {};
1191
+ try {
1192
+ parsedArgs = JSON.parse(tc.argsText || "{}");
1193
+ } catch {
1194
+ parsedArgs = { raw: tc.argsText };
1195
+ }
1196
+ return {
1197
+ id: tc.id,
1198
+ name: tc.name,
1199
+ args: parsedArgs
1200
+ };
1201
+ });
1202
+ if (params.callbacks.onToolCalls && collectedToolCalls.length > 0) {
1203
+ params.callbacks.onToolCalls(collectedToolCalls);
1204
+ }
1205
+ cleanup();
1206
+ return {
1207
+ text: fullText,
1208
+ toolCalls: collectedToolCalls,
1209
+ usage,
1210
+ timing
1211
+ };
1212
+ } catch (err) {
1213
+ cleanup();
1214
+ lastError = err;
1215
+ if (params.signal?.aborted) {
1216
+ throw err;
1217
+ }
1218
+ const msg = String(err?.message || err);
1219
+ const isRateLimit = msg.includes("429") || err instanceof RateLimitError;
1220
+ const isTransient = msg.includes("503") || msg.includes("ECONNRESET") || msg.includes("ETIMEDOUT") || msg.includes("socket hang up");
1221
+ if ((isRateLimit || isTransient) && attempts < maxAttempts) {
1222
+ const backoff = Math.min(1e3 * Math.pow(1.5, attempts), 3e3);
1223
+ if (params.callbacks.onStateChange) {
1224
+ params.callbacks.onStateChange(`retrying:${attempts}`);
1225
+ }
1226
+ await new Promise((r) => setTimeout(r, backoff));
1227
+ continue;
1228
+ }
1229
+ throw err instanceof Error ? err : new Error(String(err));
1230
+ }
1231
+ }
1232
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
1233
+ }
1234
+ convertHistoryToMessages(systemInstruction, history) {
1235
+ const messages = [];
1236
+ if (systemInstruction) {
1237
+ messages.push({ role: "system", content: systemInstruction });
50
1238
  }
51
- if (msg.includes("NOT_FOUND") || msg.includes("models/")) {
52
- return pc.red(`Model not found or unavailable. Please check the requested model ID: ${msg}`);
1239
+ for (const turn of history) {
1240
+ if (turn.role === "user") {
1241
+ const contentParts = [];
1242
+ if (turn.text) {
1243
+ contentParts.push({ type: "text", text: turn.text });
1244
+ }
1245
+ if (turn.attachments) {
1246
+ for (const att of turn.attachments) {
1247
+ if (att.base64Data && att.mimeType.startsWith("image/")) {
1248
+ contentParts.push({
1249
+ type: "image_url",
1250
+ image_url: { url: `data:${att.mimeType};base64,${att.base64Data}` }
1251
+ });
1252
+ } else if (att.textContent) {
1253
+ contentParts.push({
1254
+ type: "text",
1255
+ text: `[Attached File: ${att.name}]
1256
+ \`\`\`
1257
+ ${att.textContent}
1258
+ \`\`\``
1259
+ });
1260
+ }
1261
+ }
1262
+ }
1263
+ messages.push({
1264
+ role: "user",
1265
+ content: contentParts.length === 1 && contentParts[0].type === "text" ? contentParts[0].text : contentParts
1266
+ });
1267
+ } else if (turn.role === "model") {
1268
+ const msg = { role: "assistant" };
1269
+ if (turn.text) msg.content = turn.text;
1270
+ if (turn.toolCalls && turn.toolCalls.length > 0) {
1271
+ msg.tool_calls = turn.toolCalls.map((tc) => ({
1272
+ id: tc.id,
1273
+ type: "function",
1274
+ function: {
1275
+ name: tc.name,
1276
+ arguments: JSON.stringify(tc.args)
1277
+ }
1278
+ }));
1279
+ }
1280
+ messages.push(msg);
1281
+ } else if (turn.role === "tool") {
1282
+ if (turn.toolResults) {
1283
+ for (const tr of turn.toolResults) {
1284
+ messages.push({
1285
+ role: "tool",
1286
+ tool_call_id: tr.callId,
1287
+ content: typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result)
1288
+ });
1289
+ }
1290
+ }
1291
+ }
53
1292
  }
54
- return pc.red(`Gemini Error: ${msg}`);
1293
+ return messages;
55
1294
  }
56
- return pc.red(`An unexpected error occurred: ${String(err)}`);
57
- }
58
-
59
- // src/provider/gemini.ts
60
- import { GoogleGenAI, Type } from "@google/genai";
61
- var GeminiProvider = class {
62
- client = null;
63
- apiKey = null;
64
- constructor(apiKey) {
65
- const key = apiKey || process.env.GEMINI_API_KEY;
66
- if (key && key.trim().length > 0) {
67
- this.apiKey = key.trim();
68
- this.client = new GoogleGenAI({ apiKey: this.apiKey });
1295
+ convertTools(tools) {
1296
+ return tools.map((t) => ({
1297
+ type: "function",
1298
+ function: {
1299
+ name: t.name,
1300
+ description: t.description,
1301
+ parameters: t.parameters
1302
+ }
1303
+ }));
1304
+ }
1305
+ async *readSSEStream(stream) {
1306
+ const reader = stream.getReader();
1307
+ const decoder = new TextDecoder();
1308
+ let buffer = "";
1309
+ try {
1310
+ while (true) {
1311
+ const { done, value } = await reader.read();
1312
+ if (done) break;
1313
+ buffer += decoder.decode(value, { stream: true });
1314
+ const lines = buffer.split("\n");
1315
+ buffer = lines.pop() || "";
1316
+ for (const line of lines) {
1317
+ const trimmed = line.trim();
1318
+ if (!trimmed || trimmed.startsWith(":")) continue;
1319
+ if (trimmed.startsWith("data: ")) {
1320
+ const dataStr = trimmed.slice(6).trim();
1321
+ if (dataStr === "[DONE]") return;
1322
+ try {
1323
+ yield JSON.parse(dataStr);
1324
+ } catch {
1325
+ }
1326
+ }
1327
+ }
1328
+ }
1329
+ } finally {
1330
+ reader.releaseLock();
69
1331
  }
70
1332
  }
1333
+ };
1334
+
1335
+ // src/provider/anthropicProvider.ts
1336
+ var AnthropicProvider = class {
1337
+ id = "anthropic";
1338
+ name = "anthropic";
1339
+ displayName = "Anthropic Claude";
1340
+ defaultModel = "claude-3-7-sonnet-20250219";
1341
+ baseUrl = "https://api.anthropic.com/v1";
1342
+ cachedModels = null;
71
1343
  isConfigured() {
72
- return Boolean(this.apiKey && this.client);
1344
+ const { key } = getApiKey(this.id);
1345
+ return Boolean(key && key.trim().length > 0);
1346
+ }
1347
+ getApiKey() {
1348
+ return getApiKey(this.id).key;
73
1349
  }
74
1350
  getMaskedKey() {
75
- if (!this.apiKey) return "Not configured";
76
- if (this.apiKey.length <= 8) return "********";
77
- return `${this.apiKey.slice(0, 4)}...${this.apiKey.slice(-4)}`;
78
- }
79
- ensureClient() {
80
- if (!this.client || !this.apiKey) {
81
- const envKey = process.env.GEMINI_API_KEY;
82
- if (envKey && envKey.trim().length > 0) {
83
- this.apiKey = envKey.trim();
84
- this.client = new GoogleGenAI({ apiKey: this.apiKey });
85
- return this.client;
86
- }
87
- throw new MissingApiKeyError();
88
- }
89
- return this.client;
90
- }
91
- async streamGenerate(modelId, systemInstruction, history, tools, callbacks, options) {
92
- const client = this.ensureClient();
93
- const contents = this.convertHistoryToContents(history);
94
- const functionDeclarations = this.convertToolsToDeclarations(tools);
95
- const config = {
96
- systemInstruction,
97
- temperature: options?.temperature ?? 0.2
1351
+ const key = this.getApiKey();
1352
+ return key ? maskApiKey(key) : "(not configured)";
1353
+ }
1354
+ getCapabilities(modelId) {
1355
+ const lower = modelId.toLowerCase();
1356
+ const isSonnet = lower.includes("sonnet");
1357
+ const isOpus = lower.includes("opus");
1358
+ return {
1359
+ streaming: true,
1360
+ tools: true,
1361
+ images: true,
1362
+ reasoning: isSonnet || isOpus,
1363
+ contextWindow: 2e5
98
1364
  };
99
- if (options?.maxOutputTokens) {
100
- config.maxOutputTokens = options.maxOutputTokens;
1365
+ }
1366
+ async testConnection() {
1367
+ const key = this.getApiKey();
1368
+ if (!key) {
1369
+ return { success: false, latencyMs: 0, error: "Missing ANTHROPIC_API_KEY" };
101
1370
  }
102
- const toolList = [];
103
- if (functionDeclarations.length > 0) {
104
- toolList.push({ functionDeclarations });
1371
+ const startTime = Date.now();
1372
+ try {
1373
+ const controller = new AbortController();
1374
+ const timer = setTimeout(() => controller.abort(), 1e4);
1375
+ const res = await fetch(`${this.baseUrl}/messages`, {
1376
+ method: "POST",
1377
+ headers: {
1378
+ "Content-Type": "application/json",
1379
+ "x-api-key": key,
1380
+ "anthropic-version": "2023-06-01"
1381
+ },
1382
+ body: JSON.stringify({
1383
+ model: "claude-3-5-haiku-20241022",
1384
+ max_tokens: 1,
1385
+ messages: [{ role: "user", content: "ping" }]
1386
+ }),
1387
+ signal: controller.signal
1388
+ });
1389
+ clearTimeout(timer);
1390
+ if (!res.ok) {
1391
+ const text = await res.text();
1392
+ return {
1393
+ success: false,
1394
+ latencyMs: Date.now() - startTime,
1395
+ error: `HTTP ${res.status}: ${text.slice(0, 200)}`
1396
+ };
1397
+ }
1398
+ return {
1399
+ success: true,
1400
+ latencyMs: Date.now() - startTime,
1401
+ resolvedModel: this.defaultModel
1402
+ };
1403
+ } catch (err) {
1404
+ return {
1405
+ success: false,
1406
+ latencyMs: Date.now() - startTime,
1407
+ error: err.message || String(err)
1408
+ };
105
1409
  }
106
- if (options?.enableSearchGrounding) {
107
- toolList.push({ googleSearch: {} });
1410
+ }
1411
+ async listModels(refresh = false) {
1412
+ if (!refresh && this.cachedModels && this.cachedModels.length > 0) {
1413
+ return this.cachedModels;
108
1414
  }
109
- if (toolList.length > 0) {
110
- config.tools = toolList;
1415
+ const key = this.getApiKey();
1416
+ if (key) {
1417
+ try {
1418
+ const controller = new AbortController();
1419
+ const timer = setTimeout(() => controller.abort(), 8e3);
1420
+ const res = await fetch(`${this.baseUrl}/models`, {
1421
+ method: "GET",
1422
+ headers: {
1423
+ "x-api-key": key,
1424
+ "anthropic-version": "2023-06-01"
1425
+ },
1426
+ signal: controller.signal
1427
+ });
1428
+ clearTimeout(timer);
1429
+ if (res.ok) {
1430
+ const json = await res.json();
1431
+ if (Array.isArray(json?.data)) {
1432
+ const models = json.data.map((m) => ({
1433
+ id: m.id,
1434
+ name: m.display_name || m.id,
1435
+ provider: this.id,
1436
+ capabilities: this.getCapabilities(m.id),
1437
+ contextWindow: 2e5
1438
+ }));
1439
+ if (models.length > 0) {
1440
+ this.cachedModels = models;
1441
+ return models;
1442
+ }
1443
+ }
1444
+ }
1445
+ } catch {
1446
+ }
1447
+ }
1448
+ const verified = [
1449
+ "claude-3-7-sonnet-20250219",
1450
+ "claude-3-5-sonnet-20241022",
1451
+ "claude-3-5-haiku-20241022",
1452
+ "claude-3-opus-20240229"
1453
+ ];
1454
+ return verified.map((id) => ({
1455
+ id,
1456
+ name: id,
1457
+ provider: this.id,
1458
+ capabilities: this.getCapabilities(id),
1459
+ contextWindow: 2e5
1460
+ }));
1461
+ }
1462
+ async streamGenerate(params) {
1463
+ const key = this.getApiKey();
1464
+ if (!key) {
1465
+ throw new MissingApiKeyError(this.id, "ANTHROPIC_API_KEY");
1466
+ }
1467
+ const overallStartTime = Date.now();
1468
+ const timing = {
1469
+ totalMs: 0,
1470
+ toolExecutionMs: {}
1471
+ };
1472
+ const { messages, system } = this.convertHistoryToMessages(params.systemInstruction, params.history);
1473
+ const tools = this.convertTools(params.tools);
1474
+ const payload = {
1475
+ model: params.model,
1476
+ messages,
1477
+ max_tokens: params.maxOutputTokens || 4096,
1478
+ stream: true
1479
+ };
1480
+ if (system) {
1481
+ payload.system = system;
111
1482
  }
1483
+ if (tools.length > 0) {
1484
+ payload.tools = tools;
1485
+ }
1486
+ if (typeof params.temperature === "number") {
1487
+ payload.temperature = params.temperature;
1488
+ }
1489
+ const timeoutMs = params.timeoutMs || 6e4;
1490
+ const firstTokenTimeoutMs = params.firstTokenTimeoutMs || 3e4;
112
1491
  let attempts = 0;
113
1492
  const maxAttempts = 3;
114
1493
  let lastError = null;
115
1494
  while (attempts < maxAttempts) {
1495
+ attempts++;
1496
+ const connectStart = Date.now();
1497
+ if (params.callbacks.onStateChange) {
1498
+ params.callbacks.onStateChange(attempts > 1 ? `retrying:${attempts}` : "connecting");
1499
+ }
1500
+ const { controller, cleanup } = createLinkedAbortController(params.signal, timeoutMs);
116
1501
  try {
117
- attempts++;
118
- const responseStream = await client.models.generateContentStream({
119
- model: modelId,
120
- contents,
121
- config
1502
+ const response = await fetch(`${this.baseUrl}/messages`, {
1503
+ method: "POST",
1504
+ headers: {
1505
+ "Content-Type": "application/json",
1506
+ "x-api-key": key,
1507
+ "anthropic-version": "2023-06-01"
1508
+ },
1509
+ body: JSON.stringify(payload),
1510
+ signal: controller.signal
122
1511
  });
1512
+ timing.connectMs = Date.now() - connectStart;
1513
+ if (!response.ok) {
1514
+ const errorBody = await response.text();
1515
+ if (response.status === 401 || response.status === 403) {
1516
+ throw new AuthenticationError(`Anthropic Auth Error (${response.status}): ${errorBody}`);
1517
+ }
1518
+ if (response.status === 429) {
1519
+ throw new RateLimitError(`Anthropic Rate Limit: ${errorBody}`);
1520
+ }
1521
+ if (response.status === 404) {
1522
+ throw new ModelNotFoundError(params.model, this.displayName);
1523
+ }
1524
+ throw new LanternProviderError(`Anthropic API Error (${response.status}): ${errorBody}`, response.status, this.id);
1525
+ }
1526
+ if (!response.body) {
1527
+ throw new LanternProviderError("Anthropic response body is null", void 0, this.id);
1528
+ }
1529
+ if (params.callbacks.onStateChange) {
1530
+ params.callbacks.onStateChange("waiting_first_token");
1531
+ }
1532
+ const rawStream = this.readSSEStream(response.body);
123
1533
  let fullText = "";
124
- const collectedToolCalls = [];
125
- const usage = {
126
- promptTokens: 0,
127
- candidatesTokens: 0,
128
- totalTokens: 0
129
- };
130
- for await (const chunk of responseStream) {
131
- if (chunk.text) {
132
- fullText += chunk.text;
133
- if (callbacks.onChunk) {
134
- callbacks.onChunk(chunk.text);
1534
+ const toolCallsMap = {};
1535
+ const usage = { promptTokens: 0, candidatesTokens: 0, totalTokens: 0 };
1536
+ const wrappedStream = wrapStreamWithFirstTokenTimeout(
1537
+ rawStream,
1538
+ firstTokenTimeoutMs,
1539
+ this.displayName,
1540
+ params.model,
1541
+ controller.signal,
1542
+ (latencyMs) => {
1543
+ timing.ttftMs = latencyMs;
1544
+ if (params.callbacks.onStateChange) {
1545
+ params.callbacks.onStateChange("streaming");
135
1546
  }
136
1547
  }
137
- if (chunk.functionCalls && chunk.functionCalls.length > 0) {
138
- for (const fc of chunk.functionCalls) {
139
- collectedToolCalls.push({
140
- id: fc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
141
- name: fc.name || "",
142
- args: fc.args || {}
143
- });
1548
+ );
1549
+ const streamStart = Date.now();
1550
+ for await (const event of wrappedStream) {
1551
+ const type = event.type;
1552
+ if (type === "message_start" && event.message?.usage) {
1553
+ usage.promptTokens = event.message.usage.input_tokens || 0;
1554
+ }
1555
+ if (type === "content_block_start") {
1556
+ const index = event.index ?? 0;
1557
+ if (event.content_block?.type === "tool_use") {
1558
+ toolCallsMap[index] = {
1559
+ id: event.content_block.id || `call_${Date.now()}_${index}`,
1560
+ name: event.content_block.name || "",
1561
+ argsText: ""
1562
+ };
144
1563
  }
145
1564
  }
146
- if (chunk.usageMetadata) {
147
- usage.promptTokens = chunk.usageMetadata.promptTokenCount ?? usage.promptTokens;
148
- usage.candidatesTokens = chunk.usageMetadata.candidatesTokenCount ?? usage.candidatesTokens;
149
- usage.totalTokens = chunk.usageMetadata.totalTokenCount ?? usage.totalTokens;
150
- if (callbacks.onUsage) {
151
- callbacks.onUsage(usage);
1565
+ if (type === "content_block_delta") {
1566
+ const index = event.index ?? 0;
1567
+ const delta = event.delta;
1568
+ if (delta?.type === "text_delta" && delta.text) {
1569
+ fullText += delta.text;
1570
+ if (params.callbacks.onChunk) {
1571
+ params.callbacks.onChunk(delta.text);
1572
+ }
1573
+ } else if (delta?.type === "input_json_delta" && delta.partial_json) {
1574
+ if (toolCallsMap[index]) {
1575
+ toolCallsMap[index].argsText += delta.partial_json;
1576
+ }
1577
+ }
1578
+ }
1579
+ if (type === "message_delta" && event.usage) {
1580
+ usage.candidatesTokens = event.usage.output_tokens || 0;
1581
+ usage.totalTokens = usage.promptTokens + usage.candidatesTokens;
1582
+ if (params.callbacks.onUsage) {
1583
+ params.callbacks.onUsage(usage);
152
1584
  }
153
1585
  }
154
1586
  }
155
- if (callbacks.onToolCalls && collectedToolCalls.length > 0) {
156
- callbacks.onToolCalls(collectedToolCalls);
1587
+ timing.streamMs = Date.now() - streamStart;
1588
+ timing.totalMs = Date.now() - overallStartTime;
1589
+ const collectedToolCalls = Object.values(toolCallsMap).map((tc) => {
1590
+ let parsedArgs = {};
1591
+ try {
1592
+ parsedArgs = JSON.parse(tc.argsText || "{}");
1593
+ } catch {
1594
+ parsedArgs = { raw: tc.argsText };
1595
+ }
1596
+ return {
1597
+ id: tc.id,
1598
+ name: tc.name,
1599
+ args: parsedArgs
1600
+ };
1601
+ });
1602
+ if (params.callbacks.onToolCalls && collectedToolCalls.length > 0) {
1603
+ params.callbacks.onToolCalls(collectedToolCalls);
157
1604
  }
1605
+ cleanup();
158
1606
  return {
159
1607
  text: fullText,
160
1608
  toolCalls: collectedToolCalls,
161
- usage
1609
+ usage,
1610
+ timing
162
1611
  };
163
1612
  } catch (err) {
1613
+ cleanup();
164
1614
  lastError = err;
165
- const msg = String(err?.message || err);
166
- if (msg.includes("API_KEY_INVALID") || msg.includes("401") || msg.includes("403")) {
167
- throw new GeminiProviderError(`Authentication failed: ${msg}`, 401);
1615
+ if (params.signal?.aborted) {
1616
+ throw err;
168
1617
  }
169
- const isRateLimit = msg.includes("429") || msg.includes("RESOURCE_EXHAUSTED");
1618
+ const msg = String(err?.message || err);
1619
+ const isRateLimit = msg.includes("429") || err instanceof RateLimitError;
170
1620
  const isTransient = msg.includes("503") || msg.includes("overloaded") || msg.includes("ECONNRESET") || msg.includes("ETIMEDOUT");
171
1621
  if ((isRateLimit || isTransient) && attempts < maxAttempts) {
172
- const delay = Math.pow(2, attempts) * 1e3 + Math.random() * 500;
173
- await new Promise((r) => setTimeout(r, delay));
1622
+ const backoff = Math.min(1e3 * Math.pow(1.5, attempts), 3e3);
1623
+ if (params.callbacks.onStateChange) {
1624
+ params.callbacks.onStateChange(`retrying:${attempts}`);
1625
+ }
1626
+ await new Promise((r) => setTimeout(r, backoff));
174
1627
  continue;
175
1628
  }
176
- if (isRateLimit) {
177
- throw new RateLimitError(msg);
178
- }
179
- throw new GeminiProviderError(`Gemini request failed: ${msg}`);
1629
+ throw err instanceof Error ? err : new Error(String(err));
180
1630
  }
181
1631
  }
182
1632
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
183
1633
  }
184
- convertHistoryToContents(turns) {
185
- const contents = [];
186
- for (const turn of turns) {
187
- const parts = [];
1634
+ convertHistoryToMessages(systemInstruction, history) {
1635
+ const messages = [];
1636
+ for (const turn of history) {
188
1637
  if (turn.role === "user") {
1638
+ const content = [];
189
1639
  if (turn.text) {
190
- parts.push({ text: turn.text });
1640
+ content.push({ type: "text", text: turn.text });
191
1641
  }
192
- if (turn.attachments && turn.attachments.length > 0) {
1642
+ if (turn.attachments) {
193
1643
  for (const att of turn.attachments) {
194
- if (att.base64Data && att.mimeType) {
195
- parts.push({
196
- inlineData: {
197
- mimeType: att.mimeType,
1644
+ if (att.base64Data && att.mimeType.startsWith("image/")) {
1645
+ content.push({
1646
+ type: "image",
1647
+ source: {
1648
+ type: "base64",
1649
+ media_type: att.mimeType,
198
1650
  data: att.base64Data
199
1651
  }
200
1652
  });
201
1653
  } else if (att.textContent) {
202
- parts.push({
1654
+ content.push({
1655
+ type: "text",
203
1656
  text: `[Attached File: ${att.name}]
204
1657
  \`\`\`
205
1658
  ${att.textContent}
@@ -208,92 +1661,259 @@ ${att.textContent}
208
1661
  }
209
1662
  }
210
1663
  }
211
- if (parts.length > 0) {
212
- contents.push({ role: "user", parts });
213
- }
1664
+ messages.push({ role: "user", content });
214
1665
  } else if (turn.role === "model") {
1666
+ const content = [];
215
1667
  if (turn.text) {
216
- parts.push({ text: turn.text });
1668
+ content.push({ type: "text", text: turn.text });
217
1669
  }
218
- if (turn.toolCalls && turn.toolCalls.length > 0) {
1670
+ if (turn.toolCalls) {
219
1671
  for (const tc of turn.toolCalls) {
220
- parts.push({
221
- functionCall: {
222
- id: tc.id,
223
- name: tc.name,
224
- args: tc.args
225
- }
226
- });
227
- }
228
- }
229
- if (parts.length > 0) {
230
- contents.push({ role: "model", parts });
231
- }
232
- } else if (turn.role === "tool") {
233
- if (turn.toolResults && turn.toolResults.length > 0) {
234
- for (const res of turn.toolResults) {
235
- parts.push({
236
- functionResponse: {
237
- id: res.callId,
238
- name: res.name,
239
- response: {
240
- result: res.error ? { error: res.error } : res.result
241
- }
242
- }
1672
+ content.push({
1673
+ type: "tool_use",
1674
+ id: tc.id,
1675
+ name: tc.name,
1676
+ input: tc.args
243
1677
  });
244
1678
  }
245
1679
  }
246
- if (parts.length > 0) {
247
- contents.push({ role: "tool", parts });
1680
+ messages.push({ role: "assistant", content });
1681
+ } else if (turn.role === "tool") {
1682
+ if (turn.toolResults) {
1683
+ const content = [];
1684
+ for (const tr of turn.toolResults) {
1685
+ content.push({
1686
+ type: "tool_result",
1687
+ tool_use_id: tr.callId,
1688
+ content: typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result)
1689
+ });
1690
+ }
1691
+ messages.push({ role: "user", content });
248
1692
  }
249
1693
  }
250
1694
  }
251
- return contents;
1695
+ return { messages, system: systemInstruction || void 0 };
252
1696
  }
253
- convertToolsToDeclarations(tools) {
254
- return tools.map((tool) => {
255
- const properties = {};
256
- for (const [propName, propDef] of Object.entries(tool.parameters.properties)) {
257
- let typeVal = Type.STRING;
258
- switch (propDef.type) {
259
- case "number":
260
- case "integer":
261
- typeVal = Type.NUMBER;
262
- break;
263
- case "boolean":
264
- typeVal = Type.BOOLEAN;
265
- break;
266
- case "array":
267
- typeVal = Type.ARRAY;
268
- break;
269
- case "object":
270
- typeVal = Type.OBJECT;
271
- break;
272
- default:
273
- typeVal = Type.STRING;
1697
+ convertTools(tools) {
1698
+ return tools.map((t) => ({
1699
+ name: t.name,
1700
+ description: t.description,
1701
+ input_schema: t.parameters
1702
+ }));
1703
+ }
1704
+ async *readSSEStream(stream) {
1705
+ const reader = stream.getReader();
1706
+ const decoder = new TextDecoder();
1707
+ let buffer = "";
1708
+ try {
1709
+ while (true) {
1710
+ const { done, value } = await reader.read();
1711
+ if (done) break;
1712
+ buffer += decoder.decode(value, { stream: true });
1713
+ const lines = buffer.split("\n");
1714
+ buffer = lines.pop() || "";
1715
+ for (const line of lines) {
1716
+ const trimmed = line.trim();
1717
+ if (!trimmed || trimmed.startsWith(":")) continue;
1718
+ if (trimmed.startsWith("data: ")) {
1719
+ const dataStr = trimmed.slice(6).trim();
1720
+ try {
1721
+ yield JSON.parse(dataStr);
1722
+ } catch {
1723
+ }
1724
+ }
274
1725
  }
275
- properties[propName] = {
276
- type: typeVal,
277
- description: propDef.description,
278
- ...propDef.enum ? { enum: propDef.enum } : {},
279
- ...propDef.items ? { items: { type: Type.STRING } } : {}
280
- };
281
1726
  }
282
- return {
283
- name: tool.name,
284
- description: tool.description,
285
- parameters: {
286
- type: Type.OBJECT,
287
- properties,
288
- required: tool.parameters.required || []
1727
+ } finally {
1728
+ reader.releaseLock();
1729
+ }
1730
+ }
1731
+ };
1732
+
1733
+ // src/provider/openrouterProvider.ts
1734
+ var OpenRouterProvider = class extends OpenAIProvider {
1735
+ constructor() {
1736
+ super({
1737
+ id: "openrouter",
1738
+ name: "openrouter",
1739
+ displayName: "OpenRouter",
1740
+ baseUrl: "https://openrouter.ai/api/v1",
1741
+ defaultModel: "anthropic/claude-3.7-sonnet",
1742
+ headers: {
1743
+ "HTTP-Referer": "https://github.com/codelantern/codelantern-agent-cli",
1744
+ "X-Title": "Code Lantern CLI"
1745
+ }
1746
+ });
1747
+ }
1748
+ getCapabilities(modelId) {
1749
+ const lower = modelId.toLowerCase();
1750
+ const isReasoning = lower.includes("r1") || lower.includes("o1") || lower.includes("o3") || lower.includes("reason");
1751
+ return {
1752
+ streaming: true,
1753
+ tools: !lower.includes(":free") && !lower.includes("deepseek-r1"),
1754
+ // Most OpenRouter models support tool calling
1755
+ images: lower.includes("vision") || lower.includes("gpt-4o") || lower.includes("claude-3") || lower.includes("gemini"),
1756
+ reasoning: isReasoning,
1757
+ contextWindow: 128e3
1758
+ };
1759
+ }
1760
+ async listModels(refresh = false) {
1761
+ const key = this.getApiKey();
1762
+ try {
1763
+ const controller = new AbortController();
1764
+ const timer = setTimeout(() => controller.abort(), 1e4);
1765
+ const headers = { ...this.customHeaders };
1766
+ if (key) {
1767
+ headers["Authorization"] = `Bearer ${key}`;
1768
+ }
1769
+ const res = await fetch(`${this.baseUrl}/models`, {
1770
+ method: "GET",
1771
+ headers,
1772
+ signal: controller.signal
1773
+ });
1774
+ clearTimeout(timer);
1775
+ if (res.ok) {
1776
+ const json = await res.json();
1777
+ if (Array.isArray(json?.data)) {
1778
+ const models = json.data.slice(0, 100).map((m) => ({
1779
+ id: m.id,
1780
+ name: m.name || m.id,
1781
+ provider: this.id,
1782
+ description: m.description,
1783
+ contextWindow: m.context_length || 128e3,
1784
+ capabilities: this.getCapabilities(m.id),
1785
+ pricing: m.pricing ? {
1786
+ promptPer1k: parseFloat(m.pricing.prompt) * 1e3,
1787
+ completionPer1k: parseFloat(m.pricing.completion) * 1e3
1788
+ } : void 0
1789
+ }));
1790
+ return models;
289
1791
  }
290
- };
1792
+ }
1793
+ } catch {
1794
+ }
1795
+ const verified = [
1796
+ "anthropic/claude-3.7-sonnet",
1797
+ "anthropic/claude-3.5-sonnet",
1798
+ "openai/gpt-4o",
1799
+ "openai/o3-mini",
1800
+ "google/gemini-2.5-flash",
1801
+ "deepseek/deepseek-chat",
1802
+ "meta-llama/llama-3.3-70b-instruct"
1803
+ ];
1804
+ return verified.map((id) => ({
1805
+ id,
1806
+ name: id,
1807
+ provider: this.id,
1808
+ capabilities: this.getCapabilities(id)
1809
+ }));
1810
+ }
1811
+ };
1812
+
1813
+ // src/provider/registry.ts
1814
+ var ProviderRegistry = class {
1815
+ providers = /* @__PURE__ */ new Map();
1816
+ constructor() {
1817
+ this.registerBuiltins();
1818
+ this.loadCustomProviders();
1819
+ }
1820
+ registerBuiltins() {
1821
+ const gemini = new GeminiProvider();
1822
+ const openai = new OpenAIProvider();
1823
+ const anthropic = new AnthropicProvider();
1824
+ const openrouter = new OpenRouterProvider();
1825
+ this.providers.set(gemini.id, gemini);
1826
+ this.providers.set(openai.id, openai);
1827
+ this.providers.set(anthropic.id, anthropic);
1828
+ this.providers.set(openrouter.id, openrouter);
1829
+ }
1830
+ loadCustomProviders() {
1831
+ const config = loadConfig();
1832
+ if (config.providers) {
1833
+ for (const [id, provConfig] of Object.entries(config.providers)) {
1834
+ if (!provConfig.disabled) {
1835
+ const adapter = new OpenAIProvider({
1836
+ id,
1837
+ name: provConfig.name || id,
1838
+ displayName: provConfig.name || id,
1839
+ baseUrl: provConfig.baseUrl,
1840
+ defaultModel: provConfig.defaultModel || "default",
1841
+ headers: provConfig.headers
1842
+ });
1843
+ this.providers.set(id, adapter);
1844
+ }
1845
+ }
1846
+ }
1847
+ }
1848
+ getProvider(id) {
1849
+ const targetId = (id || loadConfig().activeProvider || "gemini").toLowerCase();
1850
+ const provider = this.providers.get(targetId);
1851
+ if (provider) {
1852
+ return provider;
1853
+ }
1854
+ const gemini = this.providers.get("gemini");
1855
+ if (gemini) return gemini;
1856
+ const first = Array.from(this.providers.values())[0];
1857
+ if (first) return first;
1858
+ throw new Error(`No provider adapter registered for '${targetId}'.`);
1859
+ }
1860
+ hasProvider(id) {
1861
+ return this.providers.has(id.toLowerCase());
1862
+ }
1863
+ listProviders() {
1864
+ const activeId = (loadConfig().activeProvider || "gemini").toLowerCase();
1865
+ return Array.from(this.providers.values()).map((p) => ({
1866
+ id: p.id,
1867
+ name: p.name,
1868
+ displayName: p.displayName,
1869
+ isConfigured: p.isConfigured(),
1870
+ maskedKey: p.getMaskedKey(),
1871
+ defaultModel: p.defaultModel,
1872
+ isActive: p.id === activeId
1873
+ }));
1874
+ }
1875
+ registerCustomProvider(cfg) {
1876
+ const id = cfg.id.toLowerCase();
1877
+ const adapter = new OpenAIProvider({
1878
+ id,
1879
+ name: cfg.name || id,
1880
+ displayName: cfg.name || id,
1881
+ baseUrl: cfg.baseUrl,
1882
+ defaultModel: cfg.defaultModel || "default",
1883
+ headers: cfg.headers
291
1884
  });
1885
+ this.providers.set(id, adapter);
1886
+ const currentConfig = loadConfig();
1887
+ if (!currentConfig.providers) currentConfig.providers = {};
1888
+ currentConfig.providers[id] = cfg;
1889
+ saveConfig(currentConfig);
1890
+ }
1891
+ removeCustomProvider(id) {
1892
+ const norm = id.toLowerCase();
1893
+ const builtins = ["gemini", "openai", "anthropic", "openrouter"];
1894
+ if (builtins.includes(norm)) {
1895
+ return false;
1896
+ }
1897
+ const deleted = this.providers.delete(norm);
1898
+ const currentConfig = loadConfig();
1899
+ if (currentConfig.providers && currentConfig.providers[norm]) {
1900
+ delete currentConfig.providers[norm];
1901
+ saveConfig(currentConfig);
1902
+ }
1903
+ return deleted;
1904
+ }
1905
+ async testProvider(id) {
1906
+ const p = this.getProvider(id);
1907
+ return await p.testConnection();
1908
+ }
1909
+ async listModels(providerId, refresh = false) {
1910
+ const p = this.getProvider(providerId);
1911
+ return await p.listModels(refresh);
292
1912
  }
293
1913
  };
294
1914
 
295
1915
  // src/security/secrets.ts
296
- import path from "path";
1916
+ import path3 from "path";
297
1917
  var SECRET_FILE_PATTERNS = [
298
1918
  /^\.env(\..+)?$/i,
299
1919
  /\.pem$/i,
@@ -311,7 +1931,7 @@ var SECRET_FILE_PATTERNS = [
311
1931
  /\.secret/i
312
1932
  ];
313
1933
  function isSecretFile(filePath) {
314
- const base = path.basename(filePath);
1934
+ const base = path3.basename(filePath);
315
1935
  return SECRET_FILE_PATTERNS.some((pattern) => pattern.test(base));
316
1936
  }
317
1937
  function sanitizeSecrets(text) {
@@ -328,18 +1948,18 @@ function sanitizeSecrets(text) {
328
1948
  }
329
1949
 
330
1950
  // src/security/sandbox.ts
331
- import path2 from "path";
332
- import fs from "fs";
1951
+ import path4 from "path";
1952
+ import fs3 from "fs";
333
1953
  var isWindows = process.platform === "win32";
334
1954
  function normalizeForComparison(p) {
335
- const resolved = path2.resolve(p);
1955
+ const resolved = path4.resolve(p);
336
1956
  return isWindows ? resolved.toLowerCase() : resolved;
337
1957
  }
338
1958
  function isContainedWithin(parent, target) {
339
1959
  const normParent = normalizeForComparison(parent);
340
1960
  const normTarget = normalizeForComparison(target);
341
- const rel = path2.relative(normParent, normTarget);
342
- return !rel.startsWith("..") && !path2.isAbsolute(rel);
1961
+ const rel = path4.relative(normParent, normTarget);
1962
+ return !rel.startsWith("..") && !path4.isAbsolute(rel);
343
1963
  }
344
1964
  function validateWorkspacePath(targetPath, workspaceDir, allowedExternalPaths = []) {
345
1965
  if (!targetPath || typeof targetPath !== "string") {
@@ -350,14 +1970,14 @@ function validateWorkspacePath(targetPath, workspaceDir, allowedExternalPaths =
350
1970
  reason: "Empty or invalid file path provided."
351
1971
  };
352
1972
  }
353
- let normalizedWorkspace = path2.resolve(workspaceDir);
1973
+ let normalizedWorkspace = path4.resolve(workspaceDir);
354
1974
  try {
355
- if (fs.existsSync(normalizedWorkspace)) {
356
- normalizedWorkspace = fs.realpathSync(normalizedWorkspace);
1975
+ if (fs3.existsSync(normalizedWorkspace)) {
1976
+ normalizedWorkspace = fs3.realpathSync(normalizedWorkspace);
357
1977
  }
358
1978
  } catch {
359
1979
  }
360
- const resolvedTarget = path2.isAbsolute(targetPath) ? path2.resolve(targetPath) : path2.resolve(normalizedWorkspace, targetPath);
1980
+ const resolvedTarget = path4.isAbsolute(targetPath) ? path4.resolve(targetPath) : path4.resolve(normalizedWorkspace, targetPath);
361
1981
  const isExplicitlyAllowed = allowedExternalPaths.some((p) => {
362
1982
  const normAllowed = normalizeForComparison(p);
363
1983
  const normTarget = normalizeForComparison(resolvedTarget);
@@ -367,25 +1987,25 @@ function validateWorkspacePath(targetPath, workspaceDir, allowedExternalPaths =
367
1987
  return {
368
1988
  allowed: true,
369
1989
  resolvedPath: resolvedTarget,
370
- relativePath: path2.relative(normalizedWorkspace, resolvedTarget) || path2.basename(resolvedTarget)
1990
+ relativePath: path4.relative(normalizedWorkspace, resolvedTarget) || path4.basename(resolvedTarget)
371
1991
  };
372
1992
  }
373
1993
  if (!isContainedWithin(normalizedWorkspace, resolvedTarget)) {
374
1994
  return {
375
1995
  allowed: false,
376
1996
  resolvedPath: resolvedTarget,
377
- relativePath: path2.relative(normalizedWorkspace, resolvedTarget),
1997
+ relativePath: path4.relative(normalizedWorkspace, resolvedTarget),
378
1998
  reason: `Path '${targetPath}' is outside the authorized workspace: ${normalizedWorkspace}`
379
1999
  };
380
2000
  }
381
2001
  try {
382
- if (fs.existsSync(resolvedTarget)) {
383
- const realTarget = fs.realpathSync(resolvedTarget);
2002
+ if (fs3.existsSync(resolvedTarget)) {
2003
+ const realTarget = fs3.realpathSync(resolvedTarget);
384
2004
  if (!isContainedWithin(normalizedWorkspace, realTarget)) {
385
2005
  return {
386
2006
  allowed: false,
387
2007
  resolvedPath: realTarget,
388
- relativePath: path2.relative(normalizedWorkspace, realTarget),
2008
+ relativePath: path4.relative(normalizedWorkspace, realTarget),
389
2009
  reason: `Symlink target points outside the workspace: ${realTarget}`
390
2010
  };
391
2011
  }
@@ -396,14 +2016,14 @@ function validateWorkspacePath(targetPath, workspaceDir, allowedExternalPaths =
396
2016
  return {
397
2017
  allowed: false,
398
2018
  resolvedPath: resolvedTarget,
399
- relativePath: path2.relative(normalizedWorkspace, resolvedTarget),
400
- reason: `Access to secret or credential file '${path2.basename(resolvedTarget)}' is blocked for security.`
2019
+ relativePath: path4.relative(normalizedWorkspace, resolvedTarget),
2020
+ reason: `Access to secret or credential file '${path4.basename(resolvedTarget)}' is blocked for security.`
401
2021
  };
402
2022
  }
403
2023
  return {
404
2024
  allowed: true,
405
2025
  resolvedPath: resolvedTarget,
406
- relativePath: path2.relative(normalizedWorkspace, resolvedTarget) || "."
2026
+ relativePath: path4.relative(normalizedWorkspace, resolvedTarget) || "."
407
2027
  };
408
2028
  }
409
2029
 
@@ -488,8 +2108,8 @@ function validateCommand(command) {
488
2108
  }
489
2109
 
490
2110
  // src/tools/fsTools.ts
491
- import fs2 from "fs";
492
- import path3 from "path";
2111
+ import fs4 from "fs";
2112
+ import path5 from "path";
493
2113
  var readFileTool = {
494
2114
  definition: {
495
2115
  name: "read_file",
@@ -524,14 +2144,14 @@ var readFileTool = {
524
2144
  error: validation.reason
525
2145
  };
526
2146
  }
527
- if (!fs2.existsSync(validation.resolvedPath)) {
2147
+ if (!fs4.existsSync(validation.resolvedPath)) {
528
2148
  return {
529
2149
  success: false,
530
2150
  output: `Error: File not found at '${validation.relativePath}'`,
531
2151
  error: "File not found"
532
2152
  };
533
2153
  }
534
- const stat = fs2.statSync(validation.resolvedPath);
2154
+ const stat = fs4.statSync(validation.resolvedPath);
535
2155
  if (!stat.isFile()) {
536
2156
  return {
537
2157
  success: false,
@@ -547,7 +2167,7 @@ var readFileTool = {
547
2167
  };
548
2168
  }
549
2169
  try {
550
- const content = fs2.readFileSync(validation.resolvedPath, "utf8");
2170
+ const content = fs4.readFileSync(validation.resolvedPath, "utf8");
551
2171
  const lines = content.split("\n");
552
2172
  const totalLines = lines.length;
553
2173
  let start = 1;
@@ -619,11 +2239,11 @@ var writeFileTool = {
619
2239
  error: validation.reason
620
2240
  };
621
2241
  }
622
- const fileExists = fs2.existsSync(validation.resolvedPath);
2242
+ const fileExists = fs4.existsSync(validation.resolvedPath);
623
2243
  let oldContent = "";
624
2244
  if (fileExists) {
625
2245
  try {
626
- oldContent = fs2.readFileSync(validation.resolvedPath, "utf8");
2246
+ oldContent = fs4.readFileSync(validation.resolvedPath, "utf8");
627
2247
  } catch {
628
2248
  oldContent = "";
629
2249
  }
@@ -646,11 +2266,11 @@ var writeFileTool = {
646
2266
  };
647
2267
  }
648
2268
  try {
649
- const parentDir = path3.dirname(validation.resolvedPath);
650
- if (!fs2.existsSync(parentDir)) {
651
- fs2.mkdirSync(parentDir, { recursive: true });
2269
+ const parentDir = path5.dirname(validation.resolvedPath);
2270
+ if (!fs4.existsSync(parentDir)) {
2271
+ fs4.mkdirSync(parentDir, { recursive: true });
652
2272
  }
653
- fs2.writeFileSync(validation.resolvedPath, content, "utf8");
2273
+ fs4.writeFileSync(validation.resolvedPath, content, "utf8");
654
2274
  const lines = content.split("\n").length;
655
2275
  return {
656
2276
  success: true,
@@ -702,7 +2322,7 @@ var editFileTool = {
702
2322
  error: validation.reason
703
2323
  };
704
2324
  }
705
- if (!fs2.existsSync(validation.resolvedPath)) {
2325
+ if (!fs4.existsSync(validation.resolvedPath)) {
706
2326
  return {
707
2327
  success: false,
708
2328
  output: `Error: File not found at '${validation.relativePath}'`,
@@ -710,7 +2330,7 @@ var editFileTool = {
710
2330
  };
711
2331
  }
712
2332
  try {
713
- const original = fs2.readFileSync(validation.resolvedPath, "utf8");
2333
+ const original = fs4.readFileSync(validation.resolvedPath, "utf8");
714
2334
  const hasCrlf = original.includes("\r\n");
715
2335
  let matchedTarget = target;
716
2336
  if (!original.includes(target)) {
@@ -760,7 +2380,7 @@ var editFileTool = {
760
2380
  error: "Approval rejected"
761
2381
  };
762
2382
  }
763
- fs2.writeFileSync(validation.resolvedPath, updated, "utf8");
2383
+ fs4.writeFileSync(validation.resolvedPath, updated, "utf8");
764
2384
  return {
765
2385
  success: true,
766
2386
  output: `Successfully applied edit to ${validation.relativePath}`,
@@ -777,8 +2397,8 @@ var editFileTool = {
777
2397
  };
778
2398
 
779
2399
  // src/tools/searchTools.ts
780
- import fs3 from "fs";
781
- import path4 from "path";
2400
+ import fs5 from "fs";
2401
+ import path6 from "path";
782
2402
  import fg from "fast-glob";
783
2403
  import ignore from "ignore";
784
2404
  var DEFAULT_IGNORED_DIRS = [
@@ -800,10 +2420,10 @@ var DEFAULT_IGNORED_DIRS = [
800
2420
  function getWorkspaceIgnore(workspaceDir) {
801
2421
  const ig = ignore();
802
2422
  ig.add(DEFAULT_IGNORED_DIRS);
803
- const gitignorePath = path4.join(workspaceDir, ".gitignore");
804
- if (fs3.existsSync(gitignorePath)) {
2423
+ const gitignorePath = path6.join(workspaceDir, ".gitignore");
2424
+ if (fs5.existsSync(gitignorePath)) {
805
2425
  try {
806
- const gitignoreContent = fs3.readFileSync(gitignorePath, "utf8");
2426
+ const gitignoreContent = fs5.readFileSync(gitignorePath, "utf8");
807
2427
  ig.add(gitignoreContent);
808
2428
  } catch {
809
2429
  }
@@ -831,9 +2451,9 @@ var listDirectoryTool = {
831
2451
  },
832
2452
  async execute(args, context) {
833
2453
  const rawPath = String(args.path || "").trim();
834
- let realWorkspace = path4.resolve(context.workspace);
2454
+ let realWorkspace = path6.resolve(context.workspace);
835
2455
  try {
836
- if (fs3.existsSync(realWorkspace)) realWorkspace = fs3.realpathSync(realWorkspace);
2456
+ if (fs5.existsSync(realWorkspace)) realWorkspace = fs5.realpathSync(realWorkspace);
837
2457
  } catch {
838
2458
  }
839
2459
  const validation = validateWorkspacePath(rawPath || ".", realWorkspace);
@@ -844,7 +2464,7 @@ var listDirectoryTool = {
844
2464
  error: validation.reason
845
2465
  };
846
2466
  }
847
- if (!fs3.existsSync(validation.resolvedPath)) {
2467
+ if (!fs5.existsSync(validation.resolvedPath)) {
848
2468
  return {
849
2469
  success: false,
850
2470
  output: `Directory not found: ${validation.relativePath || "."}`,
@@ -856,10 +2476,10 @@ var listDirectoryTool = {
856
2476
  try {
857
2477
  let walk2 = function(currentDir, currentDepth) {
858
2478
  if (currentDepth > maxDepth) return;
859
- const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
2479
+ const entries = fs5.readdirSync(currentDir, { withFileTypes: true });
860
2480
  for (const entry of entries) {
861
- const fullPath = path4.join(currentDir, entry.name);
862
- const relToWorkspace = path4.relative(realWorkspace, fullPath).replace(/\\/g, "/");
2481
+ const fullPath = path6.join(currentDir, entry.name);
2482
+ const relToWorkspace = path6.relative(realWorkspace, fullPath).replace(/\\/g, "/");
863
2483
  if (ig.ignores(relToWorkspace) || DEFAULT_IGNORED_DIRS.includes(entry.name)) {
864
2484
  continue;
865
2485
  }
@@ -914,14 +2534,14 @@ var fileSearchTool = {
914
2534
  if (!rawPattern) {
915
2535
  return { success: false, output: "Pattern parameter is required.", error: "Missing pattern" };
916
2536
  }
917
- let realWorkspace = path4.resolve(context.workspace);
2537
+ let realWorkspace = path6.resolve(context.workspace);
918
2538
  try {
919
- if (fs3.existsSync(realWorkspace)) realWorkspace = fs3.realpathSync(realWorkspace);
2539
+ if (fs5.existsSync(realWorkspace)) realWorkspace = fs5.realpathSync(realWorkspace);
920
2540
  } catch {
921
2541
  }
922
2542
  const subDir = String(args.directory || "").trim();
923
- const searchRoot = subDir ? path4.resolve(realWorkspace, subDir) : realWorkspace;
924
- const validation = validateWorkspacePath(path4.relative(realWorkspace, searchRoot) || ".", realWorkspace);
2543
+ const searchRoot = subDir ? path6.resolve(realWorkspace, subDir) : realWorkspace;
2544
+ const validation = validateWorkspacePath(path6.relative(realWorkspace, searchRoot) || ".", realWorkspace);
925
2545
  if (!validation.allowed) {
926
2546
  return { success: false, output: `Access denied: ${validation.reason}`, error: validation.reason };
927
2547
  }
@@ -937,8 +2557,8 @@ var fileSearchTool = {
937
2557
  });
938
2558
  const ig = getWorkspaceIgnore(realWorkspace);
939
2559
  const filtered = matches.filter((m) => {
940
- const full = path4.join(searchRoot, m);
941
- const rel = path4.relative(realWorkspace, full).replace(/\\/g, "/");
2560
+ const full = path6.join(searchRoot, m);
2561
+ const rel = path6.relative(realWorkspace, full).replace(/\\/g, "/");
942
2562
  return !ig.ignores(rel);
943
2563
  });
944
2564
  if (filtered.length === 0) {
@@ -987,9 +2607,9 @@ var grepSearchTool = {
987
2607
  if (!query) {
988
2608
  return { success: false, output: "Query parameter is required.", error: "Missing query" };
989
2609
  }
990
- let realWorkspace = path4.resolve(context.workspace);
2610
+ let realWorkspace = path6.resolve(context.workspace);
991
2611
  try {
992
- if (fs3.existsSync(realWorkspace)) realWorkspace = fs3.realpathSync(realWorkspace);
2612
+ if (fs5.existsSync(realWorkspace)) realWorkspace = fs5.realpathSync(realWorkspace);
993
2613
  } catch {
994
2614
  }
995
2615
  const pathPattern = args.path_pattern ? String(args.path_pattern).replace(/\\/g, "/") : "**/*";
@@ -1008,11 +2628,11 @@ var grepSearchTool = {
1008
2628
  const normalizedRel = relFile.replace(/\\/g, "/");
1009
2629
  if (ig.ignores(normalizedRel)) continue;
1010
2630
  if (relFile.endsWith(".map") || relFile.endsWith(".min.js") || relFile.endsWith(".lock")) continue;
1011
- const fullPath = path4.join(realWorkspace, relFile);
2631
+ const fullPath = path6.join(realWorkspace, relFile);
1012
2632
  try {
1013
- const stat = fs3.statSync(fullPath);
2633
+ const stat = fs5.statSync(fullPath);
1014
2634
  if (stat.size > 500 * 1024) continue;
1015
- const rawContent = fs3.readFileSync(fullPath, "utf8");
2635
+ const rawContent = fs5.readFileSync(fullPath, "utf8");
1016
2636
  const lines = rawContent.replace(/\r\n/g, "\n").split("\n");
1017
2637
  for (let i = 0; i < lines.length; i++) {
1018
2638
  if (regex.test(lines[i])) {
@@ -1065,9 +2685,9 @@ var projectTreeTool = {
1065
2685
  },
1066
2686
  async execute(args, context) {
1067
2687
  const rawDir = String(args.directory || "").trim();
1068
- let realWorkspace = path4.resolve(context.workspace);
2688
+ let realWorkspace = path6.resolve(context.workspace);
1069
2689
  try {
1070
- if (fs3.existsSync(realWorkspace)) realWorkspace = fs3.realpathSync(realWorkspace);
2690
+ if (fs5.existsSync(realWorkspace)) realWorkspace = fs5.realpathSync(realWorkspace);
1071
2691
  } catch {
1072
2692
  }
1073
2693
  const validation = validateWorkspacePath(rawDir || ".", realWorkspace);
@@ -1081,14 +2701,14 @@ var projectTreeTool = {
1081
2701
  if (depth > maxDepth) return;
1082
2702
  let entries = [];
1083
2703
  try {
1084
- entries = fs3.readdirSync(currentDir, { withFileTypes: true });
2704
+ entries = fs5.readdirSync(currentDir, { withFileTypes: true });
1085
2705
  } catch {
1086
2706
  return;
1087
2707
  }
1088
2708
  const validEntries = entries.filter((e) => {
1089
2709
  if (DEFAULT_IGNORED_DIRS.includes(e.name)) return false;
1090
- const full = path4.join(currentDir, e.name);
1091
- const rel = path4.relative(realWorkspace, full).replace(/\\/g, "/");
2710
+ const full = path6.join(currentDir, e.name);
2711
+ const rel = path6.relative(realWorkspace, full).replace(/\\/g, "/");
1092
2712
  return !ig.ignores(rel);
1093
2713
  });
1094
2714
  validEntries.sort((a, b) => {
@@ -1099,7 +2719,7 @@ var projectTreeTool = {
1099
2719
  validEntries.forEach((entry, idx) => {
1100
2720
  const isLast = idx === validEntries.length - 1;
1101
2721
  const pointer = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
1102
- const fullPath = path4.join(currentDir, entry.name);
2722
+ const fullPath = path6.join(currentDir, entry.name);
1103
2723
  lines.push(`${prefix}${pointer}${entry.name}${entry.isDirectory() ? "/" : ""}`);
1104
2724
  if (entry.isDirectory()) {
1105
2725
  buildTree2(fullPath, prefix + (isLast ? " " : "\u2502 "), depth + 1);
@@ -1107,7 +2727,7 @@ var projectTreeTool = {
1107
2727
  });
1108
2728
  };
1109
2729
  var buildTree = buildTree2;
1110
- const lines = [path4.basename(validation.resolvedPath) || "."];
2730
+ const lines = [path6.basename(validation.resolvedPath) || "."];
1111
2731
  buildTree2(validation.resolvedPath, "", 1);
1112
2732
  return {
1113
2733
  success: true,
@@ -1360,263 +2980,98 @@ ${snippets.join("\n\n")}`,
1360
2980
  return {
1361
2981
  success: false,
1362
2982
  output: `Web search could not reach external network: ${err.message}`,
1363
- error: err.message
1364
- };
1365
- }
1366
- }
1367
- };
1368
-
1369
- // src/tools/registry.ts
1370
- var ToolRegistry = class {
1371
- tools = /* @__PURE__ */ new Map();
1372
- constructor() {
1373
- this.registerBuiltins();
1374
- }
1375
- registerBuiltins() {
1376
- const builtins = [
1377
- readFileTool,
1378
- writeFileTool,
1379
- editFileTool,
1380
- listDirectoryTool,
1381
- fileSearchTool,
1382
- grepSearchTool,
1383
- projectTreeTool,
1384
- runCommandTool,
1385
- gitStatusTool,
1386
- gitDiffTool,
1387
- webSearchTool
1388
- ];
1389
- for (const tool of builtins) {
1390
- this.tools.set(tool.definition.name, tool);
1391
- }
1392
- }
1393
- registerTool(tool) {
1394
- this.tools.set(tool.definition.name, tool);
1395
- }
1396
- registerTools(tools) {
1397
- for (const tool of tools) {
1398
- this.tools.set(tool.definition.name, tool);
1399
- }
1400
- }
1401
- getTool(name) {
1402
- return this.tools.get(name);
1403
- }
1404
- getDefinitions(planMode = false) {
1405
- const defs = [];
1406
- for (const tool of this.tools.values()) {
1407
- if (planMode && !tool.definition.readOnly) {
1408
- continue;
1409
- }
1410
- defs.push(tool.definition);
1411
- }
1412
- return defs;
1413
- }
1414
- async executeTool(name, args, context) {
1415
- const tool = this.tools.get(name);
1416
- if (!tool) {
1417
- return {
1418
- success: false,
1419
- output: `Error: Unknown tool "${name}". Available tools: ${Array.from(this.tools.keys()).join(", ")}`,
1420
- error: "Tool not found"
1421
- };
1422
- }
1423
- if (context.planMode && !tool.definition.readOnly) {
1424
- return {
1425
- success: false,
1426
- output: `Plan mode is active (read-only). Action "${name}" was blocked.`,
1427
- error: "Plan mode read-only restriction"
1428
- };
1429
- }
1430
- try {
1431
- return await tool.execute(args, context);
1432
- } catch (err) {
1433
- return {
1434
- success: false,
1435
- output: `Error executing tool "${name}": ${err.message}`,
1436
- error: err.message
1437
- };
1438
- }
1439
- }
1440
- };
1441
-
1442
- // src/config/schema.ts
1443
- var DEFAULT_CONFIG = {
1444
- defaultModel: "gemini-3.8-flash",
1445
- maxSteps: 25,
1446
- temperature: 0.2,
1447
- maxOutputTokens: 8192,
1448
- yolo: false,
1449
- theme: "default",
1450
- autoCompactThreshold: 30,
1451
- // turns before auto-compacting
1452
- timeoutMs: 6e4,
1453
- mcpServers: {}
1454
- };
1455
- var ALLOWED_CONFIG_KEYS = [
1456
- "defaultModel",
1457
- "maxSteps",
1458
- "temperature",
1459
- "maxOutputTokens",
1460
- "yolo",
1461
- "theme",
1462
- "autoCompactThreshold",
1463
- "timeoutMs"
1464
- ];
1465
- function validateConfigValue(key, value) {
1466
- switch (key) {
1467
- case "defaultModel":
1468
- if (typeof value !== "string" || value.trim().length === 0) {
1469
- return { valid: false, error: "defaultModel must be a non-empty string" };
1470
- }
1471
- return { valid: true, parsedValue: value.trim() };
1472
- case "maxSteps": {
1473
- const num = Number(value);
1474
- if (isNaN(num) || num < 1 || num > 100) {
1475
- return { valid: false, error: "maxSteps must be a number between 1 and 100" };
1476
- }
1477
- return { valid: true, parsedValue: Math.round(num) };
1478
- }
1479
- case "temperature": {
1480
- const num = Number(value);
1481
- if (isNaN(num) || num < 0 || num > 2) {
1482
- return { valid: false, error: "temperature must be a number between 0.0 and 2.0" };
1483
- }
1484
- return { valid: true, parsedValue: num };
1485
- }
1486
- case "maxOutputTokens": {
1487
- const num = Number(value);
1488
- if (isNaN(num) || num < 256 || num > 65536) {
1489
- return { valid: false, error: "maxOutputTokens must be between 256 and 65536" };
1490
- }
1491
- return { valid: true, parsedValue: Math.round(num) };
1492
- }
1493
- case "yolo": {
1494
- if (typeof value === "boolean") return { valid: true, parsedValue: value };
1495
- if (value === "true" || value === "1") return { valid: true, parsedValue: true };
1496
- if (value === "false" || value === "0") return { valid: true, parsedValue: false };
1497
- return { valid: false, error: "yolo must be a boolean (true/false)" };
1498
- }
1499
- case "theme": {
1500
- if (value === "default" || value === "ocean" || value === "minimal") {
1501
- return { valid: true, parsedValue: value };
1502
- }
1503
- return { valid: false, error: "theme must be 'default', 'ocean', or 'minimal'" };
1504
- }
1505
- case "autoCompactThreshold": {
1506
- const num = Number(value);
1507
- if (isNaN(num) || num < 5 || num > 200) {
1508
- return { valid: false, error: "autoCompactThreshold must be a number between 5 and 200" };
1509
- }
1510
- return { valid: true, parsedValue: Math.round(num) };
1511
- }
1512
- case "timeoutMs": {
1513
- const num = Number(value);
1514
- if (isNaN(num) || num < 5e3) {
1515
- return { valid: false, error: "timeoutMs must be at least 5000 (ms)" };
1516
- }
1517
- return { valid: true, parsedValue: Math.round(num) };
2983
+ error: err.message
2984
+ };
1518
2985
  }
1519
- default:
1520
- return { valid: false, error: `Unknown configuration key: ${key}` };
1521
2986
  }
1522
- }
2987
+ };
1523
2988
 
1524
- // src/config/config.ts
1525
- import fs4 from "fs";
1526
- import path5 from "path";
1527
- import os from "os";
1528
- function getLanternHomeDir() {
1529
- const customHome = process.env.CODELANTERN_HOME || process.env.LANTERN_HOME;
1530
- if (customHome) {
1531
- if (!fs4.existsSync(customHome)) {
1532
- try {
1533
- fs4.mkdirSync(customHome, { recursive: true });
1534
- } catch {
1535
- }
1536
- }
1537
- return customHome;
2989
+ // src/tools/registry.ts
2990
+ var ToolRegistry = class {
2991
+ tools = /* @__PURE__ */ new Map();
2992
+ constructor() {
2993
+ this.registerBuiltins();
1538
2994
  }
1539
- const defaultDir = path5.join(os.homedir(), ".codelantern");
1540
- if (!fs4.existsSync(defaultDir)) {
1541
- try {
1542
- fs4.mkdirSync(defaultDir, { recursive: true });
1543
- } catch {
2995
+ registerBuiltins() {
2996
+ const builtins = [
2997
+ readFileTool,
2998
+ writeFileTool,
2999
+ editFileTool,
3000
+ listDirectoryTool,
3001
+ fileSearchTool,
3002
+ grepSearchTool,
3003
+ projectTreeTool,
3004
+ runCommandTool,
3005
+ gitStatusTool,
3006
+ gitDiffTool,
3007
+ webSearchTool
3008
+ ];
3009
+ for (const tool of builtins) {
3010
+ this.tools.set(tool.definition.name, tool);
1544
3011
  }
1545
3012
  }
1546
- return defaultDir;
1547
- }
1548
- function getConfigFilePath() {
1549
- return path5.join(getLanternHomeDir(), "config.json");
1550
- }
1551
- function loadConfig() {
1552
- const configPath = getConfigFilePath();
1553
- if (!fs4.existsSync(configPath)) {
1554
- return { ...DEFAULT_CONFIG };
3013
+ registerTool(tool) {
3014
+ this.tools.set(tool.definition.name, tool);
1555
3015
  }
1556
- try {
1557
- const raw = fs4.readFileSync(configPath, "utf8");
1558
- const parsed = JSON.parse(raw);
1559
- const merged = {
1560
- ...DEFAULT_CONFIG,
1561
- ...parsed
1562
- };
1563
- if (!merged.mcpServers || typeof merged.mcpServers !== "object") {
1564
- merged.mcpServers = {};
3016
+ registerTools(tools) {
3017
+ for (const tool of tools) {
3018
+ this.tools.set(tool.definition.name, tool);
1565
3019
  }
1566
- return merged;
1567
- } catch {
1568
- return { ...DEFAULT_CONFIG };
1569
3020
  }
1570
- }
1571
- function saveConfig(config) {
1572
- const configPath = getConfigFilePath();
1573
- const safeConfig = {
1574
- defaultModel: config.defaultModel || DEFAULT_CONFIG.defaultModel,
1575
- maxSteps: config.maxSteps || DEFAULT_CONFIG.maxSteps,
1576
- temperature: config.temperature ?? DEFAULT_CONFIG.temperature,
1577
- maxOutputTokens: config.maxOutputTokens || DEFAULT_CONFIG.maxOutputTokens,
1578
- yolo: Boolean(config.yolo),
1579
- theme: config.theme || DEFAULT_CONFIG.theme,
1580
- autoCompactThreshold: config.autoCompactThreshold || DEFAULT_CONFIG.autoCompactThreshold,
1581
- timeoutMs: config.timeoutMs || DEFAULT_CONFIG.timeoutMs,
1582
- mcpServers: config.mcpServers || {}
1583
- };
1584
- try {
1585
- fs4.writeFileSync(configPath, JSON.stringify(safeConfig, null, 2), {
1586
- encoding: "utf8"
1587
- });
1588
- } catch (err) {
1589
- console.error(`Warning: Failed to save config to ${configPath}: ${err.message}`);
3021
+ getTool(name) {
3022
+ return this.tools.get(name);
1590
3023
  }
1591
- }
1592
- function setConfigValue(key, value) {
1593
- const validation = validateConfigValue(key, value);
1594
- if (!validation.valid) {
1595
- return { success: false, error: validation.error };
3024
+ getDefinitions(planMode = false) {
3025
+ const defs = [];
3026
+ for (const tool of this.tools.values()) {
3027
+ if (planMode && !tool.definition.readOnly) {
3028
+ continue;
3029
+ }
3030
+ defs.push(tool.definition);
3031
+ }
3032
+ return defs;
1596
3033
  }
1597
- const current = loadConfig();
1598
- current[key] = validation.parsedValue;
1599
- saveConfig(current);
1600
- return { success: true, updatedConfig: current };
1601
- }
1602
- function getConfigValue(key) {
1603
- const current = loadConfig();
1604
- return current[key];
1605
- }
3034
+ async executeTool(name, args, context) {
3035
+ const tool = this.tools.get(name);
3036
+ if (!tool) {
3037
+ return {
3038
+ success: false,
3039
+ output: `Error: Unknown tool "${name}". Available tools: ${Array.from(this.tools.keys()).join(", ")}`,
3040
+ error: "Tool not found"
3041
+ };
3042
+ }
3043
+ if (context.planMode && !tool.definition.readOnly) {
3044
+ return {
3045
+ success: false,
3046
+ output: `Plan mode is active (read-only). Action "${name}" was blocked.`,
3047
+ error: "Plan mode read-only restriction"
3048
+ };
3049
+ }
3050
+ try {
3051
+ return await tool.execute(args, context);
3052
+ } catch (err) {
3053
+ return {
3054
+ success: false,
3055
+ output: `Error executing tool "${name}": ${err.message}`,
3056
+ error: err.message
3057
+ };
3058
+ }
3059
+ }
3060
+ };
1606
3061
 
1607
3062
  // src/mcp/mcpConfig.ts
1608
- import fs5 from "fs";
1609
- import path6 from "path";
3063
+ import fs6 from "fs";
3064
+ import path7 from "path";
1610
3065
  function getMcpConfigPath() {
1611
- return path6.join(getLanternHomeDir(), "mcp.json");
3066
+ return path7.join(getLanternHomeDir(), "mcp.json");
1612
3067
  }
1613
3068
  function loadMcpConfig() {
1614
3069
  const filePath = getMcpConfigPath();
1615
- if (!fs5.existsSync(filePath)) {
3070
+ if (!fs6.existsSync(filePath)) {
1616
3071
  return {};
1617
3072
  }
1618
3073
  try {
1619
- const raw = fs5.readFileSync(filePath, "utf8");
3074
+ const raw = fs6.readFileSync(filePath, "utf8");
1620
3075
  const parsed = JSON.parse(raw);
1621
3076
  if (parsed && typeof parsed === "object") {
1622
3077
  return parsed.mcpServers || parsed;
@@ -1630,7 +3085,7 @@ function saveMcpConfig(servers) {
1630
3085
  const filePath = getMcpConfigPath();
1631
3086
  const data = { mcpServers: servers };
1632
3087
  try {
1633
- fs5.writeFileSync(filePath, JSON.stringify(data, null, 2), { encoding: "utf8" });
3088
+ fs6.writeFileSync(filePath, JSON.stringify(data, null, 2), { encoding: "utf8" });
1634
3089
  } catch (err) {
1635
3090
  console.error(`Warning: Failed to save MCP config: ${err.message}`);
1636
3091
  }
@@ -1890,8 +3345,8 @@ var ApprovalEngine = class {
1890
3345
  };
1891
3346
 
1892
3347
  // src/config/projectInstructions.ts
1893
- import fs6 from "fs";
1894
- import path7 from "path";
3348
+ import fs7 from "fs";
3349
+ import path8 from "path";
1895
3350
  var INSTRUCTION_FILES = [
1896
3351
  "LANTERN.md",
1897
3352
  "CODELANTERN.md",
@@ -1902,12 +3357,12 @@ var INSTRUCTION_FILES = [
1902
3357
  ];
1903
3358
  function loadProjectInstructions(workspaceDir) {
1904
3359
  for (const filename of INSTRUCTION_FILES) {
1905
- const filePath = path7.join(workspaceDir, filename);
1906
- if (fs6.existsSync(filePath)) {
3360
+ const filePath = path8.join(workspaceDir, filename);
3361
+ if (fs7.existsSync(filePath)) {
1907
3362
  try {
1908
- const stat = fs6.statSync(filePath);
3363
+ const stat = fs7.statSync(filePath);
1909
3364
  if (stat.isFile() && stat.size <= 200 * 1024) {
1910
- const content = fs6.readFileSync(filePath, "utf8").trim();
3365
+ const content = fs7.readFileSync(filePath, "utf8").trim();
1911
3366
  if (content.length > 0) {
1912
3367
  return {
1913
3368
  found: true,
@@ -1924,14 +3379,14 @@ function loadProjectInstructions(workspaceDir) {
1924
3379
  }
1925
3380
 
1926
3381
  // src/session/sessionManager.ts
1927
- import fs7 from "fs";
1928
- import path8 from "path";
1929
- import crypto from "crypto";
3382
+ import fs8 from "fs";
3383
+ import path9 from "path";
3384
+ import crypto2 from "crypto";
1930
3385
  function getSessionsDir() {
1931
- const dir = path8.join(getLanternHomeDir(), "sessions");
1932
- if (!fs7.existsSync(dir)) {
3386
+ const dir = path9.join(getLanternHomeDir(), "sessions");
3387
+ if (!fs8.existsSync(dir)) {
1933
3388
  try {
1934
- fs7.mkdirSync(dir, { recursive: true });
3389
+ fs8.mkdirSync(dir, { recursive: true });
1935
3390
  } catch {
1936
3391
  }
1937
3392
  }
@@ -1939,17 +3394,17 @@ function getSessionsDir() {
1939
3394
  }
1940
3395
  function generateSessionId() {
1941
3396
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
1942
- const rand = crypto.randomBytes(3).toString("hex");
3397
+ const rand = crypto2.randomBytes(3).toString("hex");
1943
3398
  return `session_${timestamp}_${rand}`;
1944
3399
  }
1945
3400
  function createNewSession(workspace, model, initialTitle) {
1946
3401
  const now = (/* @__PURE__ */ new Date()).toISOString();
1947
3402
  return {
1948
3403
  id: generateSessionId(),
1949
- title: initialTitle || `Session in ${path8.basename(workspace) || "workspace"}`,
3404
+ title: initialTitle || `Session in ${path9.basename(workspace) || "workspace"}`,
1950
3405
  createdAt: now,
1951
3406
  updatedAt: now,
1952
- workspace: path8.resolve(workspace),
3407
+ workspace: path9.resolve(workspace),
1953
3408
  model,
1954
3409
  turns: [],
1955
3410
  totalUsage: {
@@ -1961,7 +3416,7 @@ function createNewSession(workspace, model, initialTitle) {
1961
3416
  }
1962
3417
  function saveSession(session) {
1963
3418
  const sessionsDir = getSessionsDir();
1964
- const filePath = path8.join(sessionsDir, `${session.id}.json`);
3419
+ const filePath = path9.join(sessionsDir, `${session.id}.json`);
1965
3420
  session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1966
3421
  const serialized = JSON.stringify(session, (_key, val) => {
1967
3422
  if (typeof val === "string") {
@@ -1970,7 +3425,7 @@ function saveSession(session) {
1970
3425
  return val;
1971
3426
  }, 2);
1972
3427
  try {
1973
- fs7.writeFileSync(filePath, serialized, { encoding: "utf8" });
3428
+ fs8.writeFileSync(filePath, serialized, { encoding: "utf8" });
1974
3429
  } catch (err) {
1975
3430
  console.error(`Warning: Failed to save session to ${filePath}: ${err.message}`);
1976
3431
  }
@@ -1978,14 +3433,14 @@ function saveSession(session) {
1978
3433
  function loadSession(sessionId) {
1979
3434
  const sessionsDir = getSessionsDir();
1980
3435
  const normalizedId = sessionId.endsWith(".json") ? sessionId : `${sessionId}.json`;
1981
- const filePath = path8.join(sessionsDir, normalizedId);
1982
- if (!fs7.existsSync(filePath)) {
1983
- if (!fs7.existsSync(sessionsDir)) return null;
1984
- const files = fs7.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
3436
+ const filePath = path9.join(sessionsDir, normalizedId);
3437
+ if (!fs8.existsSync(filePath)) {
3438
+ if (!fs8.existsSync(sessionsDir)) return null;
3439
+ const files = fs8.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
1985
3440
  const match = files.find((f) => f.startsWith(sessionId));
1986
3441
  if (match) {
1987
3442
  try {
1988
- const raw = fs7.readFileSync(path8.join(sessionsDir, match), "utf8");
3443
+ const raw = fs8.readFileSync(path9.join(sessionsDir, match), "utf8");
1989
3444
  return JSON.parse(raw);
1990
3445
  } catch {
1991
3446
  return null;
@@ -1994,7 +3449,7 @@ function loadSession(sessionId) {
1994
3449
  return null;
1995
3450
  }
1996
3451
  try {
1997
- const raw = fs7.readFileSync(filePath, "utf8");
3452
+ const raw = fs8.readFileSync(filePath, "utf8");
1998
3453
  return JSON.parse(raw);
1999
3454
  } catch {
2000
3455
  return null;
@@ -2002,12 +3457,12 @@ function loadSession(sessionId) {
2002
3457
  }
2003
3458
  function listSessions() {
2004
3459
  const sessionsDir = getSessionsDir();
2005
- if (!fs7.existsSync(sessionsDir)) return [];
2006
- const files = fs7.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
3460
+ if (!fs8.existsSync(sessionsDir)) return [];
3461
+ const files = fs8.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
2007
3462
  const sessions = [];
2008
3463
  for (const file of files) {
2009
3464
  try {
2010
- const raw = fs7.readFileSync(path8.join(sessionsDir, file), "utf8");
3465
+ const raw = fs8.readFileSync(path9.join(sessionsDir, file), "utf8");
2011
3466
  const data = JSON.parse(raw);
2012
3467
  sessions.push({
2013
3468
  id: data.id,
@@ -2025,16 +3480,16 @@ function listSessions() {
2025
3480
  function deleteSession(sessionId) {
2026
3481
  const sessionsDir = getSessionsDir();
2027
3482
  const normalizedId = sessionId.endsWith(".json") ? sessionId : `${sessionId}.json`;
2028
- const filePath = path8.join(sessionsDir, normalizedId);
2029
- if (fs7.existsSync(filePath)) {
2030
- fs7.unlinkSync(filePath);
3483
+ const filePath = path9.join(sessionsDir, normalizedId);
3484
+ if (fs8.existsSync(filePath)) {
3485
+ fs8.unlinkSync(filePath);
2031
3486
  return true;
2032
3487
  }
2033
- if (!fs7.existsSync(sessionsDir)) return false;
2034
- const files = fs7.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
3488
+ if (!fs8.existsSync(sessionsDir)) return false;
3489
+ const files = fs8.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
2035
3490
  const match = files.find((f) => f.startsWith(sessionId));
2036
3491
  if (match) {
2037
- fs7.unlinkSync(path8.join(sessionsDir, match));
3492
+ fs8.unlinkSync(path9.join(sessionsDir, match));
2038
3493
  return true;
2039
3494
  }
2040
3495
  return false;
@@ -2120,8 +3575,229 @@ ${summaryPoints.join("\n")}`,
2120
3575
  return [compactedTurn, ackTurn, ...toKeep];
2121
3576
  }
2122
3577
 
2123
- // src/agent/orbitAgent.ts
2124
- import path9 from "path";
3578
+ // src/skills/skillManager.ts
3579
+ import fs9 from "fs";
3580
+ import path10 from "path";
3581
+ import { execSync } from "child_process";
3582
+ var SkillManager = class {
3583
+ userSkillsDir;
3584
+ constructor() {
3585
+ this.userSkillsDir = path10.join(getLanternHomeDir(), "skills");
3586
+ if (!fs9.existsSync(this.userSkillsDir)) {
3587
+ try {
3588
+ fs9.mkdirSync(this.userSkillsDir, { recursive: true });
3589
+ } catch {
3590
+ }
3591
+ }
3592
+ }
3593
+ getUserSkillsDir() {
3594
+ return this.userSkillsDir;
3595
+ }
3596
+ getProjectSkillsDirs(workspace) {
3597
+ return [
3598
+ path10.join(workspace, ".codelantern", "skills"),
3599
+ path10.join(workspace, ".lantern", "skills")
3600
+ ];
3601
+ }
3602
+ /**
3603
+ * Discovers all available skills from user-level and project-level directories
3604
+ */
3605
+ listSkills(workspace = process.cwd()) {
3606
+ const skillsMap = /* @__PURE__ */ new Map();
3607
+ const config = loadConfig();
3608
+ const activeSkillsList = new Set(config.activeSkills || []);
3609
+ this.scanDirectoryForSkills(this.userSkillsDir, "user", skillsMap, activeSkillsList);
3610
+ const projectDirs = this.getProjectSkillsDirs(workspace);
3611
+ for (const pDir of projectDirs) {
3612
+ if (fs9.existsSync(pDir)) {
3613
+ this.scanDirectoryForSkills(pDir, "project", skillsMap, activeSkillsList);
3614
+ }
3615
+ }
3616
+ return Array.from(skillsMap.values()).sort((a, b) => a.name.localeCompare(b.name));
3617
+ }
3618
+ scanDirectoryForSkills(rootDir, source, map, activeSkillsList) {
3619
+ if (!fs9.existsSync(rootDir)) return;
3620
+ try {
3621
+ const entries = fs9.readdirSync(rootDir, { withFileTypes: true });
3622
+ for (const entry of entries) {
3623
+ if (!entry.isDirectory()) continue;
3624
+ const skillFolder = path10.join(rootDir, entry.name);
3625
+ const skillMdPath = path10.join(skillFolder, "SKILL.md");
3626
+ if (fs9.existsSync(skillMdPath)) {
3627
+ try {
3628
+ const raw = fs9.readFileSync(skillMdPath, "utf8");
3629
+ const { name, description, content } = this.parseSkillMd(raw, entry.name);
3630
+ const references = [];
3631
+ const refsDir = path10.join(skillFolder, "references");
3632
+ if (fs9.existsSync(refsDir)) {
3633
+ try {
3634
+ const refFiles = fs9.readdirSync(refsDir).filter((f) => f.endsWith(".md") || f.endsWith(".txt"));
3635
+ for (const rf of refFiles) {
3636
+ references.push(path10.join(refsDir, rf));
3637
+ }
3638
+ } catch {
3639
+ }
3640
+ }
3641
+ const isExplicitlyActive = activeSkillsList.has(name);
3642
+ map.set(name, {
3643
+ name,
3644
+ description,
3645
+ path: skillFolder,
3646
+ source,
3647
+ enabled: isExplicitlyActive,
3648
+ content,
3649
+ references
3650
+ });
3651
+ } catch {
3652
+ }
3653
+ }
3654
+ }
3655
+ } catch {
3656
+ }
3657
+ }
3658
+ parseSkillMd(rawContent, defaultName) {
3659
+ let name = defaultName;
3660
+ let description = "";
3661
+ let content = rawContent.trim();
3662
+ if (rawContent.startsWith("---")) {
3663
+ const endIdx = rawContent.indexOf("---", 3);
3664
+ if (endIdx !== -1) {
3665
+ const frontmatter = rawContent.slice(3, endIdx).trim();
3666
+ content = rawContent.slice(endIdx + 3).trim();
3667
+ const lines = frontmatter.split("\n");
3668
+ for (const line of lines) {
3669
+ const colonIdx = line.indexOf(":");
3670
+ if (colonIdx !== -1) {
3671
+ const key = line.slice(0, colonIdx).trim().toLowerCase();
3672
+ const val = line.slice(colonIdx + 1).trim().replace(/^['"]|['"]$/g, "");
3673
+ if (key === "name" && val) name = val;
3674
+ if (key === "description" && val) description = val;
3675
+ }
3676
+ }
3677
+ }
3678
+ }
3679
+ if (!description) {
3680
+ const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#"));
3681
+ description = firstLine ? firstLine.slice(0, 100) : "Custom skill instructions";
3682
+ }
3683
+ return { name, description, content };
3684
+ }
3685
+ getSkill(name, workspace = process.cwd()) {
3686
+ const all = this.listSkills(workspace);
3687
+ return all.find((s) => s.name.toLowerCase() === name.toLowerCase()) || null;
3688
+ }
3689
+ enableSkill(name) {
3690
+ const config = loadConfig();
3691
+ const skills = new Set(config.activeSkills || []);
3692
+ skills.add(name);
3693
+ config.activeSkills = Array.from(skills);
3694
+ saveConfig(config);
3695
+ return true;
3696
+ }
3697
+ disableSkill(name) {
3698
+ const config = loadConfig();
3699
+ const skills = new Set(config.activeSkills || []);
3700
+ const removed = skills.delete(name);
3701
+ config.activeSkills = Array.from(skills);
3702
+ saveConfig(config);
3703
+ return removed;
3704
+ }
3705
+ addSkillFromPath(sourcePath) {
3706
+ const resolved = path10.resolve(sourcePath);
3707
+ if (!fs9.existsSync(resolved)) {
3708
+ return { success: false, error: `Path does not exist: ${sourcePath}` };
3709
+ }
3710
+ const stat = fs9.statSync(resolved);
3711
+ let targetSkillDir = "";
3712
+ let skillName = "";
3713
+ if (stat.isDirectory()) {
3714
+ const skillMd = path10.join(resolved, "SKILL.md");
3715
+ if (!fs9.existsSync(skillMd)) {
3716
+ return { success: false, error: `No SKILL.md found in directory: ${sourcePath}` };
3717
+ }
3718
+ const raw = fs9.readFileSync(skillMd, "utf8");
3719
+ const parsed = this.parseSkillMd(raw, path10.basename(resolved));
3720
+ skillName = parsed.name;
3721
+ targetSkillDir = path10.join(this.userSkillsDir, skillName);
3722
+ fs9.cpSync(resolved, targetSkillDir, { recursive: true });
3723
+ } else if (stat.isFile() && path10.basename(resolved).toLowerCase().endsWith(".md")) {
3724
+ const raw = fs9.readFileSync(resolved, "utf8");
3725
+ skillName = path10.basename(resolved, path10.extname(resolved));
3726
+ const parsed = this.parseSkillMd(raw, skillName);
3727
+ skillName = parsed.name;
3728
+ targetSkillDir = path10.join(this.userSkillsDir, skillName);
3729
+ if (!fs9.existsSync(targetSkillDir)) {
3730
+ fs9.mkdirSync(targetSkillDir, { recursive: true });
3731
+ }
3732
+ fs9.writeFileSync(path10.join(targetSkillDir, "SKILL.md"), raw, "utf8");
3733
+ } else {
3734
+ return { success: false, error: "Source must be a skill folder with SKILL.md or a markdown file." };
3735
+ }
3736
+ this.enableSkill(skillName);
3737
+ return { success: true, skillName };
3738
+ }
3739
+ addSkillFromGit(repoUrl) {
3740
+ try {
3741
+ const tempCloneDir = path10.join(this.userSkillsDir, `_temp_${Date.now()}`);
3742
+ execSync(`git clone --depth 1 "${repoUrl}" "${tempCloneDir}"`, { stdio: "pipe" });
3743
+ const res = this.addSkillFromPath(tempCloneDir);
3744
+ try {
3745
+ fs9.rmSync(tempCloneDir, { recursive: true, force: true });
3746
+ } catch {
3747
+ }
3748
+ return res;
3749
+ } catch (err) {
3750
+ return { success: false, error: `Git clone failed: ${err.message}` };
3751
+ }
3752
+ }
3753
+ removeSkill(name) {
3754
+ this.disableSkill(name);
3755
+ const targetDir = path10.join(this.userSkillsDir, name);
3756
+ if (fs9.existsSync(targetDir)) {
3757
+ try {
3758
+ fs9.rmSync(targetDir, { recursive: true, force: true });
3759
+ return true;
3760
+ } catch {
3761
+ return false;
3762
+ }
3763
+ }
3764
+ return false;
3765
+ }
3766
+ /**
3767
+ * Matches prompt against available skills by keyword and intent.
3768
+ * Transparent and non-intrusive.
3769
+ */
3770
+ matchSkillsForPrompt(userPrompt, workspace = process.cwd()) {
3771
+ const allSkills = this.listSkills(workspace);
3772
+ const matched = [];
3773
+ const promptLower = userPrompt.toLowerCase();
3774
+ for (const skill of allSkills) {
3775
+ if (skill.enabled) {
3776
+ matched.push(skill);
3777
+ continue;
3778
+ }
3779
+ const skillName = skill.name.toLowerCase();
3780
+ const descWords = skill.description.toLowerCase().split(/\W+/).filter((w) => w.length > 3);
3781
+ if (promptLower.includes(skillName)) {
3782
+ matched.push(skill);
3783
+ continue;
3784
+ }
3785
+ let matchCount = 0;
3786
+ for (const word of descWords) {
3787
+ if (promptLower.includes(word)) {
3788
+ matchCount++;
3789
+ }
3790
+ }
3791
+ if (matchCount >= 2) {
3792
+ matched.push(skill);
3793
+ }
3794
+ }
3795
+ return matched;
3796
+ }
3797
+ };
3798
+
3799
+ // src/agent/lanternAgent.ts
3800
+ import path11 from "path";
2125
3801
  import pc5 from "picocolors";
2126
3802
 
2127
3803
  // src/ui/spinner.ts
@@ -2159,9 +3835,26 @@ var TerminalSpinner = class {
2159
3835
  }
2160
3836
  return this;
2161
3837
  }
3838
+ connecting(provider, model) {
3839
+ return this.update(`Connecting to ${pc4.cyan(provider)} (${pc4.bold(model)})...`);
3840
+ }
3841
+ waitingFirstToken(provider, model) {
3842
+ return this.update(`Waiting for first token from ${pc4.cyan(provider)}...`);
3843
+ }
3844
+ receiving(tokensCount) {
3845
+ const suffix = tokensCount ? ` (${tokensCount} tokens)` : "...";
3846
+ return this.update(`Receiving streaming response${suffix}`);
3847
+ }
3848
+ retrying(attempt, maxAttempts, waitMs) {
3849
+ return this.update(`Transient error encountered. Retrying (${attempt}/${maxAttempts}) in ${(waitMs / 1e3).toFixed(1)}s...`);
3850
+ }
3851
+ toolExecuting(toolName, preview) {
3852
+ const detail = preview ? `: ${preview}` : "";
3853
+ return this.update(`Executing tool ${pc4.bold(toolName)}${detail}...`);
3854
+ }
2162
3855
  render() {
2163
3856
  if (!this.isTTY || !this.isRunning) return;
2164
- const frame = pc4.cyan(SPINNER_FRAMES[this.frameIdx % SPINNER_FRAMES.length]);
3857
+ const frame = pc4.yellow(SPINNER_FRAMES[this.frameIdx % SPINNER_FRAMES.length]);
2165
3858
  this.frameIdx++;
2166
3859
  process.stdout.write(`\r\x1B[2K${frame} ${pc4.dim(this.message)}`);
2167
3860
  }
@@ -2194,20 +3887,28 @@ var TerminalSpinner = class {
2194
3887
  }
2195
3888
  };
2196
3889
 
2197
- // src/agent/orbitAgent.ts
2198
- var OrbitAgent = class {
2199
- provider;
3890
+ // src/agent/lanternAgent.ts
3891
+ var LanternAgent = class {
3892
+ providerRegistry;
2200
3893
  tools;
2201
3894
  mcpManager;
3895
+ skillManager;
2202
3896
  approvalEngine;
2203
3897
  spinner;
2204
- constructor(apiKey) {
2205
- this.provider = new GeminiProvider(apiKey);
3898
+ constructor() {
3899
+ this.providerRegistry = new ProviderRegistry();
2206
3900
  this.tools = new ToolRegistry();
2207
3901
  this.mcpManager = new McpManager();
3902
+ this.skillManager = new SkillManager();
2208
3903
  this.approvalEngine = new ApprovalEngine({});
2209
3904
  this.spinner = new TerminalSpinner();
2210
3905
  }
3906
+ getProviderRegistry() {
3907
+ return this.providerRegistry;
3908
+ }
3909
+ getSkillManager() {
3910
+ return this.skillManager;
3911
+ }
2211
3912
  async initMcp() {
2212
3913
  try {
2213
3914
  const mcpTools = await this.mcpManager.initialize();
@@ -2218,12 +3919,12 @@ var OrbitAgent = class {
2218
3919
  async shutdown() {
2219
3920
  await this.mcpManager.closeAll();
2220
3921
  }
2221
- buildSystemPrompt(workspace, planMode = false) {
3922
+ buildSystemPrompt(workspace, planMode = false, activeSkills = []) {
2222
3923
  const projectInstructions = loadProjectInstructions(workspace);
2223
3924
  const basePrompt = [
2224
3925
  "You are Code Lantern, an elite AI coding assistant operating directly in the terminal.",
2225
- `Current Workspace: ${path9.resolve(workspace)}`,
2226
- `Environment OS: ${process.platform}`,
3926
+ `Current Workspace: ${path11.resolve(workspace)}`,
3927
+ `Environment OS: ${process.platform} (${process.arch})`,
2227
3928
  "",
2228
3929
  "CORE OPERATING PRINCIPLES:",
2229
3930
  "1. Be direct, pragmatic, and helpful. Prioritize working, robust code over verbosity.",
@@ -2247,20 +3948,50 @@ var OrbitAgent = class {
2247
3948
  if (projectInstructions.found && projectInstructions.content) {
2248
3949
  basePrompt.push(
2249
3950
  "",
2250
- `PROJECT INSTRUCTIONS (${path9.basename(projectInstructions.filePath || "instructions")}):`,
3951
+ `PROJECT INSTRUCTIONS (${path11.basename(projectInstructions.filePath || "instructions")}):`,
2251
3952
  "---",
2252
3953
  projectInstructions.content,
2253
3954
  "---"
2254
3955
  );
2255
3956
  }
3957
+ if (activeSkills.length > 0) {
3958
+ basePrompt.push("", "ACTIVE SKILLS & GUIDELINES:");
3959
+ for (const skill of activeSkills) {
3960
+ basePrompt.push(
3961
+ `### Skill: ${skill.name}`,
3962
+ `Description: ${skill.description}`,
3963
+ "Instructions:",
3964
+ skill.content,
3965
+ "---"
3966
+ );
3967
+ }
3968
+ }
2256
3969
  return basePrompt.join("\n");
2257
3970
  }
2258
3971
  async runTurn(session, userPrompt, context, attachments = []) {
3972
+ const turnStartTime = Date.now();
2259
3973
  this.approvalEngine = new ApprovalEngine({
2260
3974
  yolo: context.yolo,
2261
3975
  planMode: context.planMode
2262
3976
  });
2263
- const systemPrompt = this.buildSystemPrompt(context.workspace, context.planMode);
3977
+ const candidateSkills = this.skillManager.matchSkillsForPrompt(userPrompt, context.workspace);
3978
+ const explicitlyRequested = (context.activeSkills || []).map(
3979
+ (name) => this.skillManager.getSkill(name, context.workspace)
3980
+ ).filter((s) => Boolean(s));
3981
+ const combinedSkillsMap = /* @__PURE__ */ new Map();
3982
+ for (const s of [...explicitlyRequested, ...candidateSkills]) {
3983
+ combinedSkillsMap.set(s.name, s);
3984
+ }
3985
+ const activeSkills = Array.from(combinedSkillsMap.values());
3986
+ if (activeSkills.length > 0 && !context.jsonOutput) {
3987
+ const names = activeSkills.map((s) => pc5.yellow(s.name)).join(", ");
3988
+ console.log(pc5.dim(`\u{1F4A1} Active Skills: ${names}`));
3989
+ }
3990
+ session.activeSkills = activeSkills.map((s) => s.name);
3991
+ const provider = this.providerRegistry.getProvider(context.provider);
3992
+ session.provider = provider.id;
3993
+ session.model = context.model;
3994
+ const systemPrompt = this.buildSystemPrompt(context.workspace, context.planMode, activeSkills);
2264
3995
  const activeTools = this.tools.getDefinitions(context.planMode);
2265
3996
  const userTurn = {
2266
3997
  role: "user",
@@ -2278,18 +4009,43 @@ var OrbitAgent = class {
2278
4009
  candidatesTokens: 0,
2279
4010
  totalTokens: 0
2280
4011
  };
4012
+ const aggregatedTiming = {
4013
+ totalMs: 0,
4014
+ toolExecutionMs: {}
4015
+ };
2281
4016
  while (step < maxSteps) {
2282
4017
  step++;
2283
- this.spinner.start(`Thinking (step ${step}/${maxSteps})...`);
4018
+ if (context.signal?.aborted) {
4019
+ this.spinner.stop();
4020
+ throw context.signal.reason || new Error("Operation cancelled by user.");
4021
+ }
4022
+ this.spinner.start(`Connecting to ${provider.displayName} (${context.model})...`);
2284
4023
  let currentStepText = "";
2285
4024
  let currentToolCalls = [];
2286
4025
  try {
2287
- const result = await this.provider.streamGenerate(
2288
- context.model,
2289
- systemPrompt,
2290
- session.turns,
2291
- activeTools,
2292
- {
4026
+ const streamResult = await provider.streamGenerate({
4027
+ model: context.model,
4028
+ systemInstruction: systemPrompt,
4029
+ history: session.turns,
4030
+ tools: activeTools,
4031
+ signal: context.signal,
4032
+ temperature: 0.2,
4033
+ timeoutMs: context.timeoutMs,
4034
+ firstTokenTimeoutMs: context.firstTokenTimeoutMs,
4035
+ lowLatency: context.lowLatency,
4036
+ callbacks: {
4037
+ onStateChange: (state) => {
4038
+ if (state === "connecting") {
4039
+ this.spinner.connecting(provider.displayName, context.model);
4040
+ } else if (state === "waiting_first_token") {
4041
+ this.spinner.waitingFirstToken(provider.displayName, context.model);
4042
+ } else if (state.startsWith("retrying:")) {
4043
+ const attempt = state.split(":")[1];
4044
+ this.spinner.retrying(parseInt(attempt, 10), 3, 1500);
4045
+ } else if (state === "streaming") {
4046
+ this.spinner.stop();
4047
+ }
4048
+ },
2293
4049
  onChunk: (chunk) => {
2294
4050
  if (currentStepText === "") {
2295
4051
  this.spinner.stop();
@@ -2308,15 +4064,21 @@ var OrbitAgent = class {
2308
4064
  sessionUsage.totalTokens = usage.totalTokens;
2309
4065
  }
2310
4066
  }
2311
- );
4067
+ });
2312
4068
  this.spinner.stop();
4069
+ if (streamResult.timing) {
4070
+ aggregatedTiming.providerInitMs = streamResult.timing.providerInitMs;
4071
+ aggregatedTiming.connectMs = streamResult.timing.connectMs;
4072
+ aggregatedTiming.ttftMs = streamResult.timing.ttftMs;
4073
+ aggregatedTiming.streamMs = streamResult.timing.streamMs;
4074
+ }
2313
4075
  if (currentStepText) {
2314
4076
  if (!currentStepText.endsWith("\n")) {
2315
4077
  console.log("");
2316
4078
  }
2317
4079
  finalAssistantText = currentStepText;
2318
4080
  }
2319
- if (!result.toolCalls || result.toolCalls.length === 0) {
4081
+ if (!streamResult.toolCalls || streamResult.toolCalls.length === 0) {
2320
4082
  session.turns.push({
2321
4083
  role: "model",
2322
4084
  text: currentStepText,
@@ -2327,38 +4089,49 @@ var OrbitAgent = class {
2327
4089
  session.turns.push({
2328
4090
  role: "model",
2329
4091
  text: currentStepText || void 0,
2330
- toolCalls: result.toolCalls,
4092
+ toolCalls: streamResult.toolCalls,
2331
4093
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2332
4094
  });
2333
4095
  const toolResults = [];
2334
- for (const call of result.toolCalls) {
4096
+ for (const call of streamResult.toolCalls) {
4097
+ if (context.signal?.aborted) {
4098
+ throw context.signal.reason || new Error("Cancelled by user.");
4099
+ }
2335
4100
  totalToolsExecuted++;
2336
4101
  const callSummary = `${call.name}(${Object.keys(call.args).map((k) => `${k}: ${JSON.stringify(call.args[k])}`).join(", ")})`;
2337
4102
  if (!context.jsonOutput) {
2338
4103
  console.log(pc5.cyan(`
2339
- \u{1F6E0}\uFE0F Tool: ${pc5.bold(call.name)}`) + pc5.dim(` ${callSummary.slice(0, 100)}`));
4104
+ \u{1F6E0}\uFE0F Tool: ${pc5.bold(call.name)}`) + pc5.dim(` ${callSummary.slice(0, 90)}`));
2340
4105
  }
2341
- this.spinner.start(`Executing ${call.name}...`);
4106
+ this.spinner.toolExecuting(call.name);
2342
4107
  const toolContext = {
2343
4108
  workspace: context.workspace,
2344
4109
  yolo: this.approvalEngine.isYolo(),
2345
4110
  planMode: context.planMode,
4111
+ signal: context.signal,
2346
4112
  requestApproval: (req) => {
2347
4113
  this.spinner.stop();
2348
4114
  return this.approvalEngine.requestApproval(req);
2349
4115
  }
2350
4116
  };
4117
+ const toolStart = Date.now();
2351
4118
  const executionResult = await this.tools.executeTool(call.name, call.args, toolContext);
4119
+ const toolDuration = Date.now() - toolStart;
4120
+ aggregatedTiming.toolExecutionMs[call.name] = (aggregatedTiming.toolExecutionMs[call.name] || 0) + toolDuration;
2352
4121
  this.spinner.stop();
4122
+ let trimmedOutput = executionResult.output;
4123
+ if (trimmedOutput.length > 15e3) {
4124
+ trimmedOutput = trimmedOutput.slice(0, 15e3) + "\n\n... [Output truncated to 15KB for context efficiency]";
4125
+ }
2353
4126
  if (executionResult.success) {
2354
4127
  if (!context.jsonOutput) {
2355
- const preview = executionResult.output.length > 200 ? executionResult.output.slice(0, 200) + "..." : executionResult.output;
2356
- console.log(pc5.green(`\u2713 Result:`) + pc5.dim(` ${preview.replace(/\n/g, " ")}`));
4128
+ const preview = executionResult.output.length > 180 ? executionResult.output.slice(0, 180) + "..." : executionResult.output;
4129
+ console.log(pc5.green(`\u2713 Result:`) + pc5.dim(` ${preview.replace(/\n/g, " ")} (${toolDuration}ms)`));
2357
4130
  }
2358
4131
  toolResults.push({
2359
4132
  callId: call.id,
2360
4133
  name: call.name,
2361
- result: executionResult.output
4134
+ result: sanitizeSecrets(trimmedOutput)
2362
4135
  });
2363
4136
  } else {
2364
4137
  if (!context.jsonOutput) {
@@ -2392,6 +4165,7 @@ var OrbitAgent = class {
2392
4165
  if (shouldCompact(session.turns)) {
2393
4166
  session.turns = compactTurns(session.turns);
2394
4167
  }
4168
+ aggregatedTiming.totalMs = Date.now() - turnStartTime;
2395
4169
  session.totalUsage.promptTokens += sessionUsage.promptTokens;
2396
4170
  session.totalUsage.candidatesTokens += sessionUsage.candidatesTokens;
2397
4171
  session.totalUsage.totalTokens += sessionUsage.totalTokens;
@@ -2399,14 +4173,15 @@ var OrbitAgent = class {
2399
4173
  return {
2400
4174
  reply: finalAssistantText,
2401
4175
  toolCount: totalToolsExecuted,
2402
- usage: sessionUsage
4176
+ usage: sessionUsage,
4177
+ timing: aggregatedTiming
2403
4178
  };
2404
4179
  }
2405
4180
  };
2406
4181
 
2407
4182
  // src/provider/attachment.ts
2408
- import fs8 from "fs";
2409
- import path10 from "path";
4183
+ import fs10 from "fs";
4184
+ import path12 from "path";
2410
4185
  var MIME_MAP = {
2411
4186
  ".png": "image/png",
2412
4187
  ".jpg": "image/jpeg",
@@ -2427,22 +4202,22 @@ var MIME_MAP = {
2427
4202
  var BINARY_IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".pdf"]);
2428
4203
  var MAX_ATTACHMENT_SIZE = 15 * 1024 * 1024;
2429
4204
  function loadAttachment(filePath) {
2430
- const resolved = path10.resolve(filePath);
2431
- if (!fs8.existsSync(resolved)) {
4205
+ const resolved = path12.resolve(filePath);
4206
+ if (!fs10.existsSync(resolved)) {
2432
4207
  throw new Error(`Attached file does not exist: ${filePath}`);
2433
4208
  }
2434
- const stat = fs8.statSync(resolved);
4209
+ const stat = fs10.statSync(resolved);
2435
4210
  if (!stat.isFile()) {
2436
4211
  throw new Error(`Attachment target is not a file: ${filePath}`);
2437
4212
  }
2438
4213
  if (stat.size > MAX_ATTACHMENT_SIZE) {
2439
4214
  throw new Error(`Attachment file too large: ${filePath} (${(stat.size / (1024 * 1024)).toFixed(1)}MB, max 15MB)`);
2440
4215
  }
2441
- const ext = path10.extname(resolved).toLowerCase();
4216
+ const ext = path12.extname(resolved).toLowerCase();
2442
4217
  const mimeType = MIME_MAP[ext] || "application/octet-stream";
2443
- const name = path10.basename(resolved);
4218
+ const name = path12.basename(resolved);
2444
4219
  if (BINARY_IMAGE_EXTS.has(ext)) {
2445
- const buffer = fs8.readFileSync(resolved);
4220
+ const buffer = fs10.readFileSync(resolved);
2446
4221
  return {
2447
4222
  path: resolved,
2448
4223
  name,
@@ -2451,7 +4226,7 @@ function loadAttachment(filePath) {
2451
4226
  base64Data: buffer.toString("base64")
2452
4227
  };
2453
4228
  }
2454
- const textContent = fs8.readFileSync(resolved, "utf8");
4229
+ const textContent = fs10.readFileSync(resolved, "utf8");
2455
4230
  return {
2456
4231
  path: resolved,
2457
4232
  name,
@@ -2466,18 +4241,17 @@ function loadAttachments(filePaths) {
2466
4241
  }
2467
4242
 
2468
4243
  export {
4244
+ TimeoutError,
4245
+ FirstTokenTimeoutError,
4246
+ CancelledError,
2469
4247
  MissingApiKeyError,
2470
4248
  RateLimitError,
2471
- GeminiProviderError,
4249
+ AuthenticationError,
4250
+ ModelNotFoundError,
4251
+ LanternProviderError,
2472
4252
  formatErrorMessage,
2473
- GeminiProvider,
2474
- isSecretFile,
2475
- sanitizeSecrets,
2476
- validateWorkspacePath,
2477
- formatTerminalDiff,
2478
- formatNewFilePreview,
2479
- validateCommand,
2480
- ToolRegistry,
4253
+ createLinkedAbortController,
4254
+ wrapStreamWithFirstTokenTimeout,
2481
4255
  DEFAULT_CONFIG,
2482
4256
  ALLOWED_CONFIG_KEYS,
2483
4257
  validateConfigValue,
@@ -2487,6 +4261,25 @@ export {
2487
4261
  saveConfig,
2488
4262
  setConfigValue,
2489
4263
  getConfigValue,
4264
+ encryptSecret,
4265
+ decryptSecret,
4266
+ maskApiKey,
4267
+ getApiKey,
4268
+ setApiKey,
4269
+ removeApiKey,
4270
+ listConfiguredKeys,
4271
+ GeminiProvider,
4272
+ OpenAIProvider,
4273
+ AnthropicProvider,
4274
+ OpenRouterProvider,
4275
+ ProviderRegistry,
4276
+ isSecretFile,
4277
+ sanitizeSecrets,
4278
+ validateWorkspacePath,
4279
+ formatTerminalDiff,
4280
+ formatNewFilePreview,
4281
+ validateCommand,
4282
+ ToolRegistry,
2490
4283
  getMcpConfigPath,
2491
4284
  loadMcpConfig,
2492
4285
  saveMcpConfig,
@@ -2506,8 +4299,9 @@ export {
2506
4299
  exportSessionMarkdown,
2507
4300
  shouldCompact,
2508
4301
  compactTurns,
2509
- OrbitAgent,
4302
+ SkillManager,
4303
+ LanternAgent,
2510
4304
  loadAttachment,
2511
4305
  loadAttachments
2512
4306
  };
2513
- //# sourceMappingURL=chunk-Y3PEOLTZ.js.map
4307
+ //# sourceMappingURL=chunk-PELESOCI.js.map