codelantern-agent-cli 0.0.0-stage → 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.
@@ -0,0 +1,4307 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/provider/errors.ts
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
+ };
31
+ var MissingApiKeyError = class extends Error {
32
+ provider;
33
+ constructor(provider = "gemini", envVar = "GEMINI_API_KEY") {
34
+ super(
35
+ `API key for provider '${provider}' is not configured.
36
+
37
+ To resolve this:
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}
42
+ `
43
+ );
44
+ this.name = "MissingApiKeyError";
45
+ this.provider = provider;
46
+ }
47
+ };
48
+ var RateLimitError = class extends Error {
49
+ constructor(message = "API rate limit exceeded.") {
50
+ super(`${message}
51
+ Please wait a moment before trying again or check your account quota.`);
52
+ this.name = "RateLimitError";
53
+ }
54
+ };
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 {
68
+ statusCode;
69
+ provider;
70
+ constructor(message, statusCode, provider) {
71
+ super(message);
72
+ this.name = "LanternProviderError";
73
+ this.statusCode = statusCode;
74
+ this.provider = provider;
75
+ }
76
+ };
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
+ }
92
+ if (err instanceof MissingApiKeyError) {
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
+ }
1005
+ }
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
+ }));
1065
+ }
1066
+ async streamGenerate(params) {
1067
+ const key = this.getApiKey();
1068
+ if (!key) {
1069
+ throw new MissingApiKeyError(this.id, `${this.id.toUpperCase()}_API_KEY`);
1070
+ }
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 });
1238
+ }
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
+ }
1292
+ }
1293
+ return messages;
1294
+ }
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();
1331
+ }
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;
1343
+ isConfigured() {
1344
+ const { key } = getApiKey(this.id);
1345
+ return Boolean(key && key.trim().length > 0);
1346
+ }
1347
+ getApiKey() {
1348
+ return getApiKey(this.id).key;
1349
+ }
1350
+ getMaskedKey() {
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
1364
+ };
1365
+ }
1366
+ async testConnection() {
1367
+ const key = this.getApiKey();
1368
+ if (!key) {
1369
+ return { success: false, latencyMs: 0, error: "Missing ANTHROPIC_API_KEY" };
1370
+ }
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
+ };
1409
+ }
1410
+ }
1411
+ async listModels(refresh = false) {
1412
+ if (!refresh && this.cachedModels && this.cachedModels.length > 0) {
1413
+ return this.cachedModels;
1414
+ }
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;
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;
1491
+ let attempts = 0;
1492
+ const maxAttempts = 3;
1493
+ let lastError = null;
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);
1501
+ try {
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
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);
1533
+ let fullText = "";
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");
1546
+ }
1547
+ }
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
+ };
1563
+ }
1564
+ }
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);
1584
+ }
1585
+ }
1586
+ }
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);
1604
+ }
1605
+ cleanup();
1606
+ return {
1607
+ text: fullText,
1608
+ toolCalls: collectedToolCalls,
1609
+ usage,
1610
+ timing
1611
+ };
1612
+ } catch (err) {
1613
+ cleanup();
1614
+ lastError = err;
1615
+ if (params.signal?.aborted) {
1616
+ throw err;
1617
+ }
1618
+ const msg = String(err?.message || err);
1619
+ const isRateLimit = msg.includes("429") || err instanceof RateLimitError;
1620
+ const isTransient = msg.includes("503") || msg.includes("overloaded") || msg.includes("ECONNRESET") || msg.includes("ETIMEDOUT");
1621
+ if ((isRateLimit || isTransient) && attempts < maxAttempts) {
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));
1627
+ continue;
1628
+ }
1629
+ throw err instanceof Error ? err : new Error(String(err));
1630
+ }
1631
+ }
1632
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
1633
+ }
1634
+ convertHistoryToMessages(systemInstruction, history) {
1635
+ const messages = [];
1636
+ for (const turn of history) {
1637
+ if (turn.role === "user") {
1638
+ const content = [];
1639
+ if (turn.text) {
1640
+ content.push({ type: "text", text: turn.text });
1641
+ }
1642
+ if (turn.attachments) {
1643
+ for (const att of turn.attachments) {
1644
+ if (att.base64Data && att.mimeType.startsWith("image/")) {
1645
+ content.push({
1646
+ type: "image",
1647
+ source: {
1648
+ type: "base64",
1649
+ media_type: att.mimeType,
1650
+ data: att.base64Data
1651
+ }
1652
+ });
1653
+ } else if (att.textContent) {
1654
+ content.push({
1655
+ type: "text",
1656
+ text: `[Attached File: ${att.name}]
1657
+ \`\`\`
1658
+ ${att.textContent}
1659
+ \`\`\``
1660
+ });
1661
+ }
1662
+ }
1663
+ }
1664
+ messages.push({ role: "user", content });
1665
+ } else if (turn.role === "model") {
1666
+ const content = [];
1667
+ if (turn.text) {
1668
+ content.push({ type: "text", text: turn.text });
1669
+ }
1670
+ if (turn.toolCalls) {
1671
+ for (const tc of turn.toolCalls) {
1672
+ content.push({
1673
+ type: "tool_use",
1674
+ id: tc.id,
1675
+ name: tc.name,
1676
+ input: tc.args
1677
+ });
1678
+ }
1679
+ }
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 });
1692
+ }
1693
+ }
1694
+ }
1695
+ return { messages, system: systemInstruction || void 0 };
1696
+ }
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
+ }
1725
+ }
1726
+ }
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;
1791
+ }
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
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);
1912
+ }
1913
+ };
1914
+
1915
+ // src/security/secrets.ts
1916
+ import path3 from "path";
1917
+ var SECRET_FILE_PATTERNS = [
1918
+ /^\.env(\..+)?$/i,
1919
+ /\.pem$/i,
1920
+ /\.key$/i,
1921
+ /\.pfx$/i,
1922
+ /\.p12$/i,
1923
+ /^id_rsa/i,
1924
+ /^id_dsa/i,
1925
+ /^id_ecdsa/i,
1926
+ /^id_ed25519/i,
1927
+ /service[_-]?account.*\.json$/i,
1928
+ /credentials.*\.json$/i,
1929
+ /\.keystore$/i,
1930
+ /\.token$/i,
1931
+ /\.secret/i
1932
+ ];
1933
+ function isSecretFile(filePath) {
1934
+ const base = path3.basename(filePath);
1935
+ return SECRET_FILE_PATTERNS.some((pattern) => pattern.test(base));
1936
+ }
1937
+ function sanitizeSecrets(text) {
1938
+ if (!text) return text;
1939
+ let sanitized = text;
1940
+ sanitized = sanitized.replace(/AIza[0-9A-Za-z\-_]{30,40}/g, "AIza***********************************");
1941
+ sanitized = sanitized.replace(/ghp_[0-9a-zA-Z]{36}/g, "ghp_************************************");
1942
+ sanitized = sanitized.replace(/github_pat_[0-9a-zA-Z_]{82}/g, "github_pat_**********************************************************************************");
1943
+ const currentKey = process.env.GEMINI_API_KEY;
1944
+ if (currentKey && currentKey.length > 5) {
1945
+ sanitized = sanitized.split(currentKey).join("[REDACTED_GEMINI_API_KEY]");
1946
+ }
1947
+ return sanitized;
1948
+ }
1949
+
1950
+ // src/security/sandbox.ts
1951
+ import path4 from "path";
1952
+ import fs3 from "fs";
1953
+ var isWindows = process.platform === "win32";
1954
+ function normalizeForComparison(p) {
1955
+ const resolved = path4.resolve(p);
1956
+ return isWindows ? resolved.toLowerCase() : resolved;
1957
+ }
1958
+ function isContainedWithin(parent, target) {
1959
+ const normParent = normalizeForComparison(parent);
1960
+ const normTarget = normalizeForComparison(target);
1961
+ const rel = path4.relative(normParent, normTarget);
1962
+ return !rel.startsWith("..") && !path4.isAbsolute(rel);
1963
+ }
1964
+ function validateWorkspacePath(targetPath, workspaceDir, allowedExternalPaths = []) {
1965
+ if (!targetPath || typeof targetPath !== "string") {
1966
+ return {
1967
+ allowed: false,
1968
+ resolvedPath: "",
1969
+ relativePath: "",
1970
+ reason: "Empty or invalid file path provided."
1971
+ };
1972
+ }
1973
+ let normalizedWorkspace = path4.resolve(workspaceDir);
1974
+ try {
1975
+ if (fs3.existsSync(normalizedWorkspace)) {
1976
+ normalizedWorkspace = fs3.realpathSync(normalizedWorkspace);
1977
+ }
1978
+ } catch {
1979
+ }
1980
+ const resolvedTarget = path4.isAbsolute(targetPath) ? path4.resolve(targetPath) : path4.resolve(normalizedWorkspace, targetPath);
1981
+ const isExplicitlyAllowed = allowedExternalPaths.some((p) => {
1982
+ const normAllowed = normalizeForComparison(p);
1983
+ const normTarget = normalizeForComparison(resolvedTarget);
1984
+ return normAllowed === normTarget;
1985
+ });
1986
+ if (isExplicitlyAllowed) {
1987
+ return {
1988
+ allowed: true,
1989
+ resolvedPath: resolvedTarget,
1990
+ relativePath: path4.relative(normalizedWorkspace, resolvedTarget) || path4.basename(resolvedTarget)
1991
+ };
1992
+ }
1993
+ if (!isContainedWithin(normalizedWorkspace, resolvedTarget)) {
1994
+ return {
1995
+ allowed: false,
1996
+ resolvedPath: resolvedTarget,
1997
+ relativePath: path4.relative(normalizedWorkspace, resolvedTarget),
1998
+ reason: `Path '${targetPath}' is outside the authorized workspace: ${normalizedWorkspace}`
1999
+ };
2000
+ }
2001
+ try {
2002
+ if (fs3.existsSync(resolvedTarget)) {
2003
+ const realTarget = fs3.realpathSync(resolvedTarget);
2004
+ if (!isContainedWithin(normalizedWorkspace, realTarget)) {
2005
+ return {
2006
+ allowed: false,
2007
+ resolvedPath: realTarget,
2008
+ relativePath: path4.relative(normalizedWorkspace, realTarget),
2009
+ reason: `Symlink target points outside the workspace: ${realTarget}`
2010
+ };
2011
+ }
2012
+ }
2013
+ } catch {
2014
+ }
2015
+ if (isSecretFile(resolvedTarget)) {
2016
+ return {
2017
+ allowed: false,
2018
+ resolvedPath: resolvedTarget,
2019
+ relativePath: path4.relative(normalizedWorkspace, resolvedTarget),
2020
+ reason: `Access to secret or credential file '${path4.basename(resolvedTarget)}' is blocked for security.`
2021
+ };
2022
+ }
2023
+ return {
2024
+ allowed: true,
2025
+ resolvedPath: resolvedTarget,
2026
+ relativePath: path4.relative(normalizedWorkspace, resolvedTarget) || "."
2027
+ };
2028
+ }
2029
+
2030
+ // src/approval/diffViewer.ts
2031
+ import * as Diff from "diff";
2032
+ import pc2 from "picocolors";
2033
+ function formatTerminalDiff(filePath, oldContent, newContent, contextLines = 3) {
2034
+ const normalizedOld = oldContent.replace(/\r\n/g, "\n");
2035
+ const normalizedNew = newContent.replace(/\r\n/g, "\n");
2036
+ const patch = Diff.createPatch(filePath, normalizedOld, normalizedNew, "Original", "Modified", {
2037
+ context: contextLines
2038
+ });
2039
+ const lines = patch.split("\n");
2040
+ const formatted = [];
2041
+ for (let i = 0; i < lines.length; i++) {
2042
+ const line = lines[i];
2043
+ if (line.startsWith("---") || line.startsWith("+++")) {
2044
+ formatted.push(pc2.bold(pc2.cyan(line)));
2045
+ } else if (line.startsWith("@@")) {
2046
+ formatted.push(pc2.magenta(line));
2047
+ } else if (line.startsWith("+")) {
2048
+ formatted.push(pc2.green(line));
2049
+ } else if (line.startsWith("-")) {
2050
+ formatted.push(pc2.red(line));
2051
+ } else {
2052
+ formatted.push(pc2.dim(line));
2053
+ }
2054
+ }
2055
+ return formatted.join("\n");
2056
+ }
2057
+ function formatNewFilePreview(filePath, content, maxPreviewLines = 30) {
2058
+ const normalized = content.replace(/\r\n/g, "\n");
2059
+ const lines = normalized.split("\n");
2060
+ const preview = lines.slice(0, maxPreviewLines).map((line, idx) => {
2061
+ const lineNum = String(idx + 1).padStart(4, " ");
2062
+ return `${pc2.dim(lineNum)} ${pc2.green("+")} ${pc2.green(line)}`;
2063
+ });
2064
+ if (lines.length > maxPreviewLines) {
2065
+ preview.push(pc2.dim(` ... and ${lines.length - maxPreviewLines} more lines`));
2066
+ }
2067
+ return [
2068
+ pc2.bold(pc2.cyan(`+++ Create new file: ${filePath} (${lines.length} lines)`)),
2069
+ ...preview
2070
+ ].join("\n");
2071
+ }
2072
+
2073
+ // src/security/commandGuard.ts
2074
+ var DESTRUCTIVE_PATTERNS = [
2075
+ // Unix root/home deletion
2076
+ { regex: /\brm\s+-[rf]{1,2}\s+([/~]|\$HOME|\.\.\/|\.\.\\)/i, reason: "Attempt to delete root, home, or parent directories." },
2077
+ { regex: /\brm\s+-[rf]{1,2}\s+\*\s*$/i, reason: "Attempt to recursively delete everything in directory." },
2078
+ // Unix filesystem destruction
2079
+ { regex: /\bmkfs(\.\w+)?\b/i, reason: "Filesystem format commands are forbidden." },
2080
+ { regex: /\bdd\s+.*of=\/dev\/(r?[shv]d[a-z]|disk|nvme)/i, reason: "Direct disk write commands are forbidden." },
2081
+ { regex: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, reason: "Fork bombs are strictly blocked." },
2082
+ { regex: /\b(shutdown|reboot|poweroff|init\s+0)\b/i, reason: "System shutdown/reboot commands are forbidden." },
2083
+ { regex: /\bchmod\s+-[rR]\s+777\s+([/~]|\$HOME)/i, reason: "Dangerous permissions modification at root/home." },
2084
+ // Windows destructive commands
2085
+ { regex: /\bformat\s+[a-z]:/i, reason: "Disk format command is forbidden." },
2086
+ { regex: /\b(del|erase)\s+.*\/s.*\/q.*[a-z]:\\/i, reason: "Recursive deletion of root Windows drive is forbidden." },
2087
+ { regex: /\b(rd|rmdir)\s+.*\/s.*\/q.*[a-z]:\\/i, reason: "Recursive removal of root Windows drive is forbidden." },
2088
+ { regex: /\bRemove-Item\s+.*-Recurse\s+.*[a-z]:\\/i, reason: "Recursive PowerShell deletion of drive root is forbidden." }
2089
+ ];
2090
+ function validateCommand(command) {
2091
+ if (!command || command.trim().length === 0) {
2092
+ return { allowed: false, reason: "Empty command string." };
2093
+ }
2094
+ const trimmed = command.trim();
2095
+ for (const check of DESTRUCTIVE_PATTERNS) {
2096
+ if (check.regex.test(trimmed)) {
2097
+ return {
2098
+ allowed: false,
2099
+ reason: `Blocked dangerous command pattern: ${check.reason}`
2100
+ };
2101
+ }
2102
+ }
2103
+ const isPublishOrPush = /\b(npm\s+publish|git\s+push|docker\s+push)\b/i.test(trimmed);
2104
+ return {
2105
+ allowed: true,
2106
+ isHighRisk: isPublishOrPush
2107
+ };
2108
+ }
2109
+
2110
+ // src/tools/fsTools.ts
2111
+ import fs4 from "fs";
2112
+ import path5 from "path";
2113
+ var readFileTool = {
2114
+ definition: {
2115
+ name: "read_file",
2116
+ description: "Read the contents of a text file inside the workspace. Optionally specify a line range.",
2117
+ readOnly: true,
2118
+ parameters: {
2119
+ type: "object",
2120
+ properties: {
2121
+ path: {
2122
+ type: "string",
2123
+ description: "Relative path of the file to read within the workspace."
2124
+ },
2125
+ start_line: {
2126
+ type: "integer",
2127
+ description: "Optional 1-indexed start line number to read from."
2128
+ },
2129
+ end_line: {
2130
+ type: "integer",
2131
+ description: "Optional 1-indexed end line number to read until (inclusive)."
2132
+ }
2133
+ },
2134
+ required: ["path"]
2135
+ }
2136
+ },
2137
+ async execute(args, context) {
2138
+ const rawPath = String(args.path || "").trim();
2139
+ const validation = validateWorkspacePath(rawPath, context.workspace);
2140
+ if (!validation.allowed) {
2141
+ return {
2142
+ success: false,
2143
+ output: `Error: Access denied. ${validation.reason}`,
2144
+ error: validation.reason
2145
+ };
2146
+ }
2147
+ if (!fs4.existsSync(validation.resolvedPath)) {
2148
+ return {
2149
+ success: false,
2150
+ output: `Error: File not found at '${validation.relativePath}'`,
2151
+ error: "File not found"
2152
+ };
2153
+ }
2154
+ const stat = fs4.statSync(validation.resolvedPath);
2155
+ if (!stat.isFile()) {
2156
+ return {
2157
+ success: false,
2158
+ output: `Error: Path is a directory, not a file: '${validation.relativePath}'`,
2159
+ error: "Path is a directory"
2160
+ };
2161
+ }
2162
+ if (stat.size > 1024 * 1024) {
2163
+ return {
2164
+ success: false,
2165
+ output: `Error: File is too large to read entirely (${(stat.size / 1024).toFixed(0)}KB). Please specify start_line and end_line.`,
2166
+ error: "File too large"
2167
+ };
2168
+ }
2169
+ try {
2170
+ const content = fs4.readFileSync(validation.resolvedPath, "utf8");
2171
+ const lines = content.split("\n");
2172
+ const totalLines = lines.length;
2173
+ let start = 1;
2174
+ let end = totalLines;
2175
+ if (typeof args.start_line === "number" && args.start_line > 0) {
2176
+ start = Math.max(1, Math.min(args.start_line, totalLines));
2177
+ }
2178
+ if (typeof args.end_line === "number" && args.end_line >= start) {
2179
+ end = Math.min(args.end_line, totalLines);
2180
+ }
2181
+ const selectedLines = lines.slice(start - 1, end);
2182
+ const numberedLines = selectedLines.map((line, idx) => {
2183
+ const lineNum = String(start + idx).padStart(4, " ");
2184
+ return `${lineNum} | ${line}`;
2185
+ });
2186
+ const output = [
2187
+ `File: ${validation.relativePath} (${start}-${end} of ${totalLines} lines)`,
2188
+ "```",
2189
+ ...numberedLines,
2190
+ "```"
2191
+ ].join("\n");
2192
+ return {
2193
+ success: true,
2194
+ output,
2195
+ metadata: {
2196
+ path: validation.relativePath,
2197
+ startLine: start,
2198
+ endLine: end,
2199
+ totalLines
2200
+ }
2201
+ };
2202
+ } catch (err) {
2203
+ return {
2204
+ success: false,
2205
+ output: `Error reading file: ${err.message}`,
2206
+ error: err.message
2207
+ };
2208
+ }
2209
+ }
2210
+ };
2211
+ var writeFileTool = {
2212
+ definition: {
2213
+ name: "write_file",
2214
+ description: "Create a new file or completely overwrite an existing file with the provided content.",
2215
+ readOnly: false,
2216
+ parameters: {
2217
+ type: "object",
2218
+ properties: {
2219
+ path: {
2220
+ type: "string",
2221
+ description: "Relative path of the file to create or overwrite."
2222
+ },
2223
+ content: {
2224
+ type: "string",
2225
+ description: "The complete text content to write to the file."
2226
+ }
2227
+ },
2228
+ required: ["path", "content"]
2229
+ }
2230
+ },
2231
+ async execute(args, context) {
2232
+ const rawPath = String(args.path || "").trim();
2233
+ const content = String(args.content ?? "");
2234
+ const validation = validateWorkspacePath(rawPath, context.workspace);
2235
+ if (!validation.allowed) {
2236
+ return {
2237
+ success: false,
2238
+ output: `Error: Access denied. ${validation.reason}`,
2239
+ error: validation.reason
2240
+ };
2241
+ }
2242
+ const fileExists = fs4.existsSync(validation.resolvedPath);
2243
+ let oldContent = "";
2244
+ if (fileExists) {
2245
+ try {
2246
+ oldContent = fs4.readFileSync(validation.resolvedPath, "utf8");
2247
+ } catch {
2248
+ oldContent = "";
2249
+ }
2250
+ }
2251
+ const approved = await context.requestApproval({
2252
+ type: "file_write",
2253
+ summary: fileExists ? `Overwrite existing file: ${validation.relativePath}` : `Create new file: ${validation.relativePath}`,
2254
+ details: {
2255
+ path: validation.relativePath,
2256
+ before: oldContent,
2257
+ after: content,
2258
+ diff: fileExists ? formatTerminalDiff(validation.relativePath, oldContent, content) : formatNewFilePreview(validation.relativePath, content)
2259
+ }
2260
+ });
2261
+ if (!approved) {
2262
+ return {
2263
+ success: false,
2264
+ output: `Write rejected: User declined approval to write '${validation.relativePath}'.`,
2265
+ error: "Approval rejected"
2266
+ };
2267
+ }
2268
+ try {
2269
+ const parentDir = path5.dirname(validation.resolvedPath);
2270
+ if (!fs4.existsSync(parentDir)) {
2271
+ fs4.mkdirSync(parentDir, { recursive: true });
2272
+ }
2273
+ fs4.writeFileSync(validation.resolvedPath, content, "utf8");
2274
+ const lines = content.split("\n").length;
2275
+ return {
2276
+ success: true,
2277
+ output: `Successfully wrote ${lines} lines to ${validation.relativePath}`,
2278
+ metadata: { path: validation.relativePath, lines }
2279
+ };
2280
+ } catch (err) {
2281
+ return {
2282
+ success: false,
2283
+ output: `Failed to write file: ${err.message}`,
2284
+ error: err.message
2285
+ };
2286
+ }
2287
+ }
2288
+ };
2289
+ var editFileTool = {
2290
+ definition: {
2291
+ name: "edit_file",
2292
+ description: "Propose and apply a focused edit by replacing exact existing text with new replacement text.",
2293
+ readOnly: false,
2294
+ parameters: {
2295
+ type: "object",
2296
+ properties: {
2297
+ path: {
2298
+ type: "string",
2299
+ description: "Relative path of the file to edit."
2300
+ },
2301
+ target: {
2302
+ type: "string",
2303
+ description: "The exact string in the file to be replaced. Must match character-for-character including indentation."
2304
+ },
2305
+ replacement: {
2306
+ type: "string",
2307
+ description: "The replacement string to insert in place of target."
2308
+ }
2309
+ },
2310
+ required: ["path", "target", "replacement"]
2311
+ }
2312
+ },
2313
+ async execute(args, context) {
2314
+ const rawPath = String(args.path || "").trim();
2315
+ const target = String(args.target ?? "");
2316
+ const replacement = String(args.replacement ?? "");
2317
+ const validation = validateWorkspacePath(rawPath, context.workspace);
2318
+ if (!validation.allowed) {
2319
+ return {
2320
+ success: false,
2321
+ output: `Error: Access denied. ${validation.reason}`,
2322
+ error: validation.reason
2323
+ };
2324
+ }
2325
+ if (!fs4.existsSync(validation.resolvedPath)) {
2326
+ return {
2327
+ success: false,
2328
+ output: `Error: File not found at '${validation.relativePath}'`,
2329
+ error: "File not found"
2330
+ };
2331
+ }
2332
+ try {
2333
+ const original = fs4.readFileSync(validation.resolvedPath, "utf8");
2334
+ const hasCrlf = original.includes("\r\n");
2335
+ let matchedTarget = target;
2336
+ if (!original.includes(target)) {
2337
+ if (hasCrlf && !target.includes("\r\n")) {
2338
+ const crlfTarget = target.replace(/\n/g, "\r\n");
2339
+ if (original.includes(crlfTarget)) {
2340
+ matchedTarget = crlfTarget;
2341
+ }
2342
+ } else if (!hasCrlf && target.includes("\r\n")) {
2343
+ const lfTarget = target.replace(/\r\n/g, "\n");
2344
+ if (original.includes(lfTarget)) {
2345
+ matchedTarget = lfTarget;
2346
+ }
2347
+ }
2348
+ }
2349
+ if (!original.includes(matchedTarget)) {
2350
+ return {
2351
+ success: false,
2352
+ output: `Error: Target string not found in ${validation.relativePath}. Please verify whitespace, indentation, or read the file again to confirm line contents.`,
2353
+ error: "Target string not found"
2354
+ };
2355
+ }
2356
+ const occurrences = original.split(matchedTarget).length - 1;
2357
+ if (occurrences > 1) {
2358
+ return {
2359
+ success: false,
2360
+ output: `Warning: Target string appears ${occurrences} times in ${validation.relativePath}. Please provide more surrounding context in 'target' to make the match unique.`,
2361
+ error: "Target string ambiguous"
2362
+ };
2363
+ }
2364
+ const matchedReplacement = hasCrlf && !replacement.includes("\r\n") ? replacement.replace(/\n/g, "\r\n") : replacement;
2365
+ const updated = original.replace(matchedTarget, matchedReplacement);
2366
+ const approved = await context.requestApproval({
2367
+ type: "file_edit",
2368
+ summary: `Edit file: ${validation.relativePath}`,
2369
+ details: {
2370
+ path: validation.relativePath,
2371
+ before: original,
2372
+ after: updated,
2373
+ diff: formatTerminalDiff(validation.relativePath, original, updated)
2374
+ }
2375
+ });
2376
+ if (!approved) {
2377
+ return {
2378
+ success: false,
2379
+ output: `Edit rejected: User declined approval to edit '${validation.relativePath}'.`,
2380
+ error: "Approval rejected"
2381
+ };
2382
+ }
2383
+ fs4.writeFileSync(validation.resolvedPath, updated, "utf8");
2384
+ return {
2385
+ success: true,
2386
+ output: `Successfully applied edit to ${validation.relativePath}`,
2387
+ metadata: { path: validation.relativePath }
2388
+ };
2389
+ } catch (err) {
2390
+ return {
2391
+ success: false,
2392
+ output: `Failed to edit file: ${err.message}`,
2393
+ error: err.message
2394
+ };
2395
+ }
2396
+ }
2397
+ };
2398
+
2399
+ // src/tools/searchTools.ts
2400
+ import fs5 from "fs";
2401
+ import path6 from "path";
2402
+ import fg from "fast-glob";
2403
+ import ignore from "ignore";
2404
+ var DEFAULT_IGNORED_DIRS = [
2405
+ "node_modules",
2406
+ ".git",
2407
+ ".codelantern",
2408
+ ".lantern",
2409
+ ".orbit",
2410
+ "dist",
2411
+ "build",
2412
+ "coverage",
2413
+ ".turbo",
2414
+ ".next",
2415
+ ".cache",
2416
+ ".gemini",
2417
+ ".vscode",
2418
+ ".idea"
2419
+ ];
2420
+ function getWorkspaceIgnore(workspaceDir) {
2421
+ const ig = ignore();
2422
+ ig.add(DEFAULT_IGNORED_DIRS);
2423
+ const gitignorePath = path6.join(workspaceDir, ".gitignore");
2424
+ if (fs5.existsSync(gitignorePath)) {
2425
+ try {
2426
+ const gitignoreContent = fs5.readFileSync(gitignorePath, "utf8");
2427
+ ig.add(gitignoreContent);
2428
+ } catch {
2429
+ }
2430
+ }
2431
+ return ig;
2432
+ }
2433
+ var listDirectoryTool = {
2434
+ definition: {
2435
+ name: "list_directory",
2436
+ description: "List the contents of a directory in the workspace, respecting ignore rules.",
2437
+ readOnly: true,
2438
+ parameters: {
2439
+ type: "object",
2440
+ properties: {
2441
+ path: {
2442
+ type: "string",
2443
+ description: "Relative path of directory to inspect. Omit or pass empty for workspace root."
2444
+ },
2445
+ depth: {
2446
+ type: "integer",
2447
+ description: "Maximum depth of recursion (default 1, max 4)."
2448
+ }
2449
+ }
2450
+ }
2451
+ },
2452
+ async execute(args, context) {
2453
+ const rawPath = String(args.path || "").trim();
2454
+ let realWorkspace = path6.resolve(context.workspace);
2455
+ try {
2456
+ if (fs5.existsSync(realWorkspace)) realWorkspace = fs5.realpathSync(realWorkspace);
2457
+ } catch {
2458
+ }
2459
+ const validation = validateWorkspacePath(rawPath || ".", realWorkspace);
2460
+ if (!validation.allowed) {
2461
+ return {
2462
+ success: false,
2463
+ output: `Error: Access denied. ${validation.reason}`,
2464
+ error: validation.reason
2465
+ };
2466
+ }
2467
+ if (!fs5.existsSync(validation.resolvedPath)) {
2468
+ return {
2469
+ success: false,
2470
+ output: `Directory not found: ${validation.relativePath || "."}`,
2471
+ error: "Not found"
2472
+ };
2473
+ }
2474
+ const maxDepth = Math.min(Math.max(1, Number(args.depth) || 1), 4);
2475
+ const ig = getWorkspaceIgnore(realWorkspace);
2476
+ try {
2477
+ let walk2 = function(currentDir, currentDepth) {
2478
+ if (currentDepth > maxDepth) return;
2479
+ const entries = fs5.readdirSync(currentDir, { withFileTypes: true });
2480
+ for (const entry of entries) {
2481
+ const fullPath = path6.join(currentDir, entry.name);
2482
+ const relToWorkspace = path6.relative(realWorkspace, fullPath).replace(/\\/g, "/");
2483
+ if (ig.ignores(relToWorkspace) || DEFAULT_IGNORED_DIRS.includes(entry.name)) {
2484
+ continue;
2485
+ }
2486
+ const prefix = " ".repeat(currentDepth - 1);
2487
+ if (entry.isDirectory()) {
2488
+ results.push(`${prefix}\u{1F4C1} ${entry.name}/`);
2489
+ walk2(fullPath, currentDepth + 1);
2490
+ } else {
2491
+ results.push(`${prefix}\u{1F4C4} ${entry.name}`);
2492
+ }
2493
+ }
2494
+ };
2495
+ var walk = walk2;
2496
+ const results = [];
2497
+ walk2(validation.resolvedPath, 1);
2498
+ return {
2499
+ success: true,
2500
+ output: results.length > 0 ? results.join("\n") : "(empty directory)",
2501
+ metadata: { count: results.length }
2502
+ };
2503
+ } catch (err) {
2504
+ return {
2505
+ success: false,
2506
+ output: `Error listing directory: ${err.message}`,
2507
+ error: err.message
2508
+ };
2509
+ }
2510
+ }
2511
+ };
2512
+ var fileSearchTool = {
2513
+ definition: {
2514
+ name: "file_search",
2515
+ description: "Find files matching a glob pattern or partial filename across the project workspace.",
2516
+ readOnly: true,
2517
+ parameters: {
2518
+ type: "object",
2519
+ properties: {
2520
+ pattern: {
2521
+ type: "string",
2522
+ description: 'Glob pattern (e.g. "**/*.ts", "src/**/*.json", "*config*").'
2523
+ },
2524
+ directory: {
2525
+ type: "string",
2526
+ description: "Subdirectory to search inside. Defaults to workspace root."
2527
+ }
2528
+ },
2529
+ required: ["pattern"]
2530
+ }
2531
+ },
2532
+ async execute(args, context) {
2533
+ const rawPattern = String(args.pattern || "").trim();
2534
+ if (!rawPattern) {
2535
+ return { success: false, output: "Pattern parameter is required.", error: "Missing pattern" };
2536
+ }
2537
+ let realWorkspace = path6.resolve(context.workspace);
2538
+ try {
2539
+ if (fs5.existsSync(realWorkspace)) realWorkspace = fs5.realpathSync(realWorkspace);
2540
+ } catch {
2541
+ }
2542
+ const subDir = String(args.directory || "").trim();
2543
+ const searchRoot = subDir ? path6.resolve(realWorkspace, subDir) : realWorkspace;
2544
+ const validation = validateWorkspacePath(path6.relative(realWorkspace, searchRoot) || ".", realWorkspace);
2545
+ if (!validation.allowed) {
2546
+ return { success: false, output: `Access denied: ${validation.reason}`, error: validation.reason };
2547
+ }
2548
+ try {
2549
+ const normalizedSearchRoot = searchRoot.replace(/\\/g, "/");
2550
+ const normalizedPattern = rawPattern.replace(/\\/g, "/");
2551
+ const globPattern = normalizedPattern.includes("/") ? normalizedPattern : `**/*${normalizedPattern}*`;
2552
+ const matches = await fg(globPattern, {
2553
+ cwd: normalizedSearchRoot,
2554
+ dot: false,
2555
+ ignore: DEFAULT_IGNORED_DIRS.map((d) => `**/${d}/**`),
2556
+ onlyFiles: true
2557
+ });
2558
+ const ig = getWorkspaceIgnore(realWorkspace);
2559
+ const filtered = matches.filter((m) => {
2560
+ const full = path6.join(searchRoot, m);
2561
+ const rel = path6.relative(realWorkspace, full).replace(/\\/g, "/");
2562
+ return !ig.ignores(rel);
2563
+ });
2564
+ if (filtered.length === 0) {
2565
+ return {
2566
+ success: true,
2567
+ output: `No files found matching '${rawPattern}' in ${subDir || "workspace"}.`
2568
+ };
2569
+ }
2570
+ return {
2571
+ success: true,
2572
+ output: filtered.slice(0, 100).join("\n") + (filtered.length > 100 ? `
2573
+ ... and ${filtered.length - 100} more matches` : ""),
2574
+ metadata: { total: filtered.length }
2575
+ };
2576
+ } catch (err) {
2577
+ return {
2578
+ success: false,
2579
+ output: `Error during file search: ${err.message}`,
2580
+ error: err.message
2581
+ };
2582
+ }
2583
+ }
2584
+ };
2585
+ var grepSearchTool = {
2586
+ definition: {
2587
+ name: "grep_search",
2588
+ description: "Search for text or regex patterns within project files.",
2589
+ readOnly: true,
2590
+ parameters: {
2591
+ type: "object",
2592
+ properties: {
2593
+ query: {
2594
+ type: "string",
2595
+ description: "Search string or regular expression to search for."
2596
+ },
2597
+ path_pattern: {
2598
+ type: "string",
2599
+ description: 'Optional glob pattern to restrict files searched (e.g. "*.ts", "src/**/*.js").'
2600
+ }
2601
+ },
2602
+ required: ["query"]
2603
+ }
2604
+ },
2605
+ async execute(args, context) {
2606
+ const query = String(args.query || "").trim();
2607
+ if (!query) {
2608
+ return { success: false, output: "Query parameter is required.", error: "Missing query" };
2609
+ }
2610
+ let realWorkspace = path6.resolve(context.workspace);
2611
+ try {
2612
+ if (fs5.existsSync(realWorkspace)) realWorkspace = fs5.realpathSync(realWorkspace);
2613
+ } catch {
2614
+ }
2615
+ const pathPattern = args.path_pattern ? String(args.path_pattern).replace(/\\/g, "/") : "**/*";
2616
+ try {
2617
+ const normalizedCwd = realWorkspace.replace(/\\/g, "/");
2618
+ const files = await fg(pathPattern, {
2619
+ cwd: normalizedCwd,
2620
+ dot: false,
2621
+ ignore: DEFAULT_IGNORED_DIRS.map((d) => `**/${d}/**`),
2622
+ onlyFiles: true
2623
+ });
2624
+ const ig = getWorkspaceIgnore(realWorkspace);
2625
+ const matchingLines = [];
2626
+ const regex = new RegExp(query, "i");
2627
+ for (const relFile of files) {
2628
+ const normalizedRel = relFile.replace(/\\/g, "/");
2629
+ if (ig.ignores(normalizedRel)) continue;
2630
+ if (relFile.endsWith(".map") || relFile.endsWith(".min.js") || relFile.endsWith(".lock")) continue;
2631
+ const fullPath = path6.join(realWorkspace, relFile);
2632
+ try {
2633
+ const stat = fs5.statSync(fullPath);
2634
+ if (stat.size > 500 * 1024) continue;
2635
+ const rawContent = fs5.readFileSync(fullPath, "utf8");
2636
+ const lines = rawContent.replace(/\r\n/g, "\n").split("\n");
2637
+ for (let i = 0; i < lines.length; i++) {
2638
+ if (regex.test(lines[i])) {
2639
+ matchingLines.push(`${normalizedRel}:${i + 1}: ${lines[i].trim()}`);
2640
+ if (matchingLines.length >= 100) break;
2641
+ }
2642
+ }
2643
+ } catch {
2644
+ }
2645
+ if (matchingLines.length >= 100) break;
2646
+ }
2647
+ if (matchingLines.length === 0) {
2648
+ return {
2649
+ success: true,
2650
+ output: `No occurrences found for "${query}".`
2651
+ };
2652
+ }
2653
+ return {
2654
+ success: true,
2655
+ output: matchingLines.join("\n") + (matchingLines.length >= 100 ? "\n(Search truncated to first 100 matches)" : ""),
2656
+ metadata: { matchCount: matchingLines.length }
2657
+ };
2658
+ } catch (err) {
2659
+ return {
2660
+ success: false,
2661
+ output: `Error executing grep search: ${err.message}`,
2662
+ error: err.message
2663
+ };
2664
+ }
2665
+ }
2666
+ };
2667
+ var projectTreeTool = {
2668
+ definition: {
2669
+ name: "project_tree",
2670
+ description: "Display an ASCII tree visualization of the project structure.",
2671
+ readOnly: true,
2672
+ parameters: {
2673
+ type: "object",
2674
+ properties: {
2675
+ directory: {
2676
+ type: "string",
2677
+ description: "Optional subfolder to show tree for. Defaults to workspace root."
2678
+ },
2679
+ max_depth: {
2680
+ type: "integer",
2681
+ description: "Maximum depth (default 3, max 5)."
2682
+ }
2683
+ }
2684
+ }
2685
+ },
2686
+ async execute(args, context) {
2687
+ const rawDir = String(args.directory || "").trim();
2688
+ let realWorkspace = path6.resolve(context.workspace);
2689
+ try {
2690
+ if (fs5.existsSync(realWorkspace)) realWorkspace = fs5.realpathSync(realWorkspace);
2691
+ } catch {
2692
+ }
2693
+ const validation = validateWorkspacePath(rawDir || ".", realWorkspace);
2694
+ if (!validation.allowed) {
2695
+ return { success: false, output: `Access denied: ${validation.reason}`, error: validation.reason };
2696
+ }
2697
+ const maxDepth = Math.min(Math.max(1, Number(args.max_depth) || 3), 5);
2698
+ const ig = getWorkspaceIgnore(realWorkspace);
2699
+ try {
2700
+ let buildTree2 = function(currentDir, prefix, depth) {
2701
+ if (depth > maxDepth) return;
2702
+ let entries = [];
2703
+ try {
2704
+ entries = fs5.readdirSync(currentDir, { withFileTypes: true });
2705
+ } catch {
2706
+ return;
2707
+ }
2708
+ const validEntries = entries.filter((e) => {
2709
+ if (DEFAULT_IGNORED_DIRS.includes(e.name)) return false;
2710
+ const full = path6.join(currentDir, e.name);
2711
+ const rel = path6.relative(realWorkspace, full).replace(/\\/g, "/");
2712
+ return !ig.ignores(rel);
2713
+ });
2714
+ validEntries.sort((a, b) => {
2715
+ if (a.isDirectory() && !b.isDirectory()) return -1;
2716
+ if (!a.isDirectory() && b.isDirectory()) return 1;
2717
+ return a.name.localeCompare(b.name);
2718
+ });
2719
+ validEntries.forEach((entry, idx) => {
2720
+ const isLast = idx === validEntries.length - 1;
2721
+ const pointer = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
2722
+ const fullPath = path6.join(currentDir, entry.name);
2723
+ lines.push(`${prefix}${pointer}${entry.name}${entry.isDirectory() ? "/" : ""}`);
2724
+ if (entry.isDirectory()) {
2725
+ buildTree2(fullPath, prefix + (isLast ? " " : "\u2502 "), depth + 1);
2726
+ }
2727
+ });
2728
+ };
2729
+ var buildTree = buildTree2;
2730
+ const lines = [path6.basename(validation.resolvedPath) || "."];
2731
+ buildTree2(validation.resolvedPath, "", 1);
2732
+ return {
2733
+ success: true,
2734
+ output: lines.join("\n")
2735
+ };
2736
+ } catch (err) {
2737
+ return {
2738
+ success: false,
2739
+ output: `Error building tree: ${err.message}`,
2740
+ error: err.message
2741
+ };
2742
+ }
2743
+ }
2744
+ };
2745
+
2746
+ // src/tools/bashTool.ts
2747
+ import { exec } from "child_process";
2748
+ var runCommandTool = {
2749
+ definition: {
2750
+ name: "run_command",
2751
+ description: "Execute a terminal shell command inside the project workspace directory.",
2752
+ readOnly: false,
2753
+ parameters: {
2754
+ type: "object",
2755
+ properties: {
2756
+ command: {
2757
+ type: "string",
2758
+ description: 'The shell command to execute (e.g. "npm test", "git status", "dir").'
2759
+ },
2760
+ timeout_ms: {
2761
+ type: "integer",
2762
+ description: "Optional execution timeout in milliseconds (default 30000)."
2763
+ }
2764
+ },
2765
+ required: ["command"]
2766
+ }
2767
+ },
2768
+ async execute(args, context) {
2769
+ const command = String(args.command || "").trim();
2770
+ if (!command) {
2771
+ return { success: false, output: "Command argument is required.", error: "Missing command" };
2772
+ }
2773
+ const guard = validateCommand(command);
2774
+ if (!guard.allowed) {
2775
+ return {
2776
+ success: false,
2777
+ output: `Command blocked by security guard: ${guard.reason}`,
2778
+ error: guard.reason
2779
+ };
2780
+ }
2781
+ const approved = await context.requestApproval({
2782
+ type: "command",
2783
+ summary: `Execute command: ${command}`,
2784
+ details: {
2785
+ command
2786
+ }
2787
+ });
2788
+ if (!approved) {
2789
+ return {
2790
+ success: false,
2791
+ output: `Command execution cancelled: User declined approval for '${command}'.`,
2792
+ error: "Approval rejected"
2793
+ };
2794
+ }
2795
+ const timeout = Math.min(Math.max(1e3, Number(args.timeout_ms) || 3e4), 12e4);
2796
+ const startTime = Date.now();
2797
+ return new Promise((resolve) => {
2798
+ exec(
2799
+ command,
2800
+ {
2801
+ cwd: context.workspace,
2802
+ timeout,
2803
+ maxBuffer: 5 * 1024 * 1024,
2804
+ windowsHide: true,
2805
+ env: {
2806
+ ...process.env,
2807
+ CI: "true"
2808
+ }
2809
+ },
2810
+ (error, stdout, stderr) => {
2811
+ const duration = Date.now() - startTime;
2812
+ const exitCode = error && typeof error.code === "number" ? error.code : error ? 1 : 0;
2813
+ let rawOutput = "";
2814
+ if (stdout) rawOutput += stdout;
2815
+ if (stderr) {
2816
+ if (rawOutput) rawOutput += "\n--- stderr ---\n";
2817
+ rawOutput += stderr;
2818
+ }
2819
+ if (!rawOutput) {
2820
+ rawOutput = "(no output)";
2821
+ }
2822
+ const normalized = rawOutput.replace(/\r\n/g, "\n");
2823
+ const cleanOutput = sanitizeSecrets(normalized.trim());
2824
+ const formattedOutput = [
2825
+ `Command: ${command}`,
2826
+ `Exit code: ${exitCode}`,
2827
+ `Duration: ${duration}ms`,
2828
+ "Output:",
2829
+ cleanOutput
2830
+ ].join("\n");
2831
+ resolve({
2832
+ success: exitCode === 0,
2833
+ output: formattedOutput,
2834
+ error: error ? error.message : void 0,
2835
+ metadata: { exitCode, duration }
2836
+ });
2837
+ }
2838
+ );
2839
+ });
2840
+ }
2841
+ };
2842
+
2843
+ // src/tools/gitTools.ts
2844
+ import { exec as exec2 } from "child_process";
2845
+ var gitStatusTool = {
2846
+ definition: {
2847
+ name: "git_status",
2848
+ description: "Get current Git working tree status (branch, modified, untracked files).",
2849
+ readOnly: true,
2850
+ parameters: {
2851
+ type: "object",
2852
+ properties: {}
2853
+ }
2854
+ },
2855
+ async execute(_args, context) {
2856
+ return new Promise((resolve) => {
2857
+ exec2("git status --short --branch", { cwd: context.workspace, timeout: 1e4 }, (error, stdout, stderr) => {
2858
+ if (error) {
2859
+ return resolve({
2860
+ success: false,
2861
+ output: stderr.includes("not a git repository") ? "Workspace is not a Git repository." : `Git status error: ${error.message}`,
2862
+ error: error.message
2863
+ });
2864
+ }
2865
+ const out = stdout.trim() || "Working tree clean. Nothing to commit.";
2866
+ resolve({
2867
+ success: true,
2868
+ output: out
2869
+ });
2870
+ });
2871
+ });
2872
+ }
2873
+ };
2874
+ var gitDiffTool = {
2875
+ definition: {
2876
+ name: "git_diff",
2877
+ description: "Inspect git diff of unstaged or staged changes in the workspace repository.",
2878
+ readOnly: true,
2879
+ parameters: {
2880
+ type: "object",
2881
+ properties: {
2882
+ staged: {
2883
+ type: "boolean",
2884
+ description: "If true, shows diff of staged changes (--staged). Defaults to false."
2885
+ }
2886
+ }
2887
+ }
2888
+ },
2889
+ async execute(args, context) {
2890
+ const cmd = args.staged ? "git diff --staged" : "git diff";
2891
+ return new Promise((resolve) => {
2892
+ exec2(cmd, { cwd: context.workspace, timeout: 15e3, maxBuffer: 5 * 1024 * 1024 }, (error, stdout, stderr) => {
2893
+ if (error) {
2894
+ return resolve({
2895
+ success: false,
2896
+ output: `Git diff error: ${error.message}`,
2897
+ error: error.message
2898
+ });
2899
+ }
2900
+ const out = stdout.trim();
2901
+ if (!out) {
2902
+ return resolve({
2903
+ success: true,
2904
+ output: args.staged ? "No staged changes." : "No unstaged working tree changes."
2905
+ });
2906
+ }
2907
+ const truncated = out.length > 5e4 ? out.slice(0, 5e4) + "\n\n[Diff truncated: exceeded 50KB]" : out;
2908
+ resolve({
2909
+ success: true,
2910
+ output: truncated
2911
+ });
2912
+ });
2913
+ });
2914
+ }
2915
+ };
2916
+
2917
+ // src/tools/webSearchTool.ts
2918
+ var webSearchTool = {
2919
+ definition: {
2920
+ name: "web_search",
2921
+ description: "Search the web for up-to-date documentation, libraries, or error troubleshooting solutions.",
2922
+ readOnly: true,
2923
+ parameters: {
2924
+ type: "object",
2925
+ properties: {
2926
+ query: {
2927
+ type: "string",
2928
+ description: "Search terms or question to look up."
2929
+ }
2930
+ },
2931
+ required: ["query"]
2932
+ }
2933
+ },
2934
+ async execute(args) {
2935
+ const query = String(args.query || "").trim();
2936
+ if (!query) {
2937
+ return { success: false, output: "Missing query argument.", error: "Missing query" };
2938
+ }
2939
+ try {
2940
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
2941
+ const controller = new AbortController();
2942
+ const timer = setTimeout(() => controller.abort(), 8e3);
2943
+ const res = await fetch(url, {
2944
+ headers: {
2945
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko)"
2946
+ },
2947
+ signal: controller.signal
2948
+ });
2949
+ clearTimeout(timer);
2950
+ if (!res.ok) {
2951
+ return {
2952
+ success: true,
2953
+ output: `Search for "${query}" completed with status ${res.status}. No immediate text parsed.`
2954
+ };
2955
+ }
2956
+ const html = await res.text();
2957
+ const snippets = [];
2958
+ const regex = /<a class="result__snippet[^>]*>([\s\S]*?)<\/a>/gi;
2959
+ let match;
2960
+ while ((match = regex.exec(html)) !== null && snippets.length < 5) {
2961
+ const text = match[1].replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
2962
+ if (text) {
2963
+ snippets.push(`- ${text}`);
2964
+ }
2965
+ }
2966
+ if (snippets.length === 0) {
2967
+ return {
2968
+ success: true,
2969
+ output: `Web search executed for "${query}". Found related topics, but snippets were condensed.`
2970
+ };
2971
+ }
2972
+ return {
2973
+ success: true,
2974
+ output: `Search results for "${query}":
2975
+
2976
+ ${snippets.join("\n\n")}`,
2977
+ metadata: { resultCount: snippets.length }
2978
+ };
2979
+ } catch (err) {
2980
+ return {
2981
+ success: false,
2982
+ output: `Web search could not reach external network: ${err.message}`,
2983
+ error: err.message
2984
+ };
2985
+ }
2986
+ }
2987
+ };
2988
+
2989
+ // src/tools/registry.ts
2990
+ var ToolRegistry = class {
2991
+ tools = /* @__PURE__ */ new Map();
2992
+ constructor() {
2993
+ this.registerBuiltins();
2994
+ }
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);
3011
+ }
3012
+ }
3013
+ registerTool(tool) {
3014
+ this.tools.set(tool.definition.name, tool);
3015
+ }
3016
+ registerTools(tools) {
3017
+ for (const tool of tools) {
3018
+ this.tools.set(tool.definition.name, tool);
3019
+ }
3020
+ }
3021
+ getTool(name) {
3022
+ return this.tools.get(name);
3023
+ }
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;
3033
+ }
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
+ };
3061
+
3062
+ // src/mcp/mcpConfig.ts
3063
+ import fs6 from "fs";
3064
+ import path7 from "path";
3065
+ function getMcpConfigPath() {
3066
+ return path7.join(getLanternHomeDir(), "mcp.json");
3067
+ }
3068
+ function loadMcpConfig() {
3069
+ const filePath = getMcpConfigPath();
3070
+ if (!fs6.existsSync(filePath)) {
3071
+ return {};
3072
+ }
3073
+ try {
3074
+ const raw = fs6.readFileSync(filePath, "utf8");
3075
+ const parsed = JSON.parse(raw);
3076
+ if (parsed && typeof parsed === "object") {
3077
+ return parsed.mcpServers || parsed;
3078
+ }
3079
+ return {};
3080
+ } catch {
3081
+ return {};
3082
+ }
3083
+ }
3084
+ function saveMcpConfig(servers) {
3085
+ const filePath = getMcpConfigPath();
3086
+ const data = { mcpServers: servers };
3087
+ try {
3088
+ fs6.writeFileSync(filePath, JSON.stringify(data, null, 2), { encoding: "utf8" });
3089
+ } catch (err) {
3090
+ console.error(`Warning: Failed to save MCP config: ${err.message}`);
3091
+ }
3092
+ }
3093
+ function addMcpServer(name, config) {
3094
+ const current = loadMcpConfig();
3095
+ current[name] = config;
3096
+ saveMcpConfig(current);
3097
+ }
3098
+ function removeMcpServer(name) {
3099
+ const current = loadMcpConfig();
3100
+ if (current[name]) {
3101
+ delete current[name];
3102
+ saveMcpConfig(current);
3103
+ return true;
3104
+ }
3105
+ return false;
3106
+ }
3107
+ function toggleMcpServer(name, disabled) {
3108
+ const current = loadMcpConfig();
3109
+ if (current[name]) {
3110
+ current[name].disabled = disabled;
3111
+ saveMcpConfig(current);
3112
+ return true;
3113
+ }
3114
+ return false;
3115
+ }
3116
+
3117
+ // src/mcp/mcpManager.ts
3118
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3119
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3120
+ var McpManager = class {
3121
+ connections = /* @__PURE__ */ new Map();
3122
+ async initialize() {
3123
+ const servers = loadMcpConfig();
3124
+ const allTools = [];
3125
+ for (const [serverName, config] of Object.entries(servers)) {
3126
+ if (config.disabled) {
3127
+ continue;
3128
+ }
3129
+ try {
3130
+ const tools = await this.connectServer(serverName, config);
3131
+ allTools.push(...tools);
3132
+ } catch (err) {
3133
+ console.error(`Warning: Failed to connect MCP server '${serverName}': ${err.message}`);
3134
+ }
3135
+ }
3136
+ return allTools;
3137
+ }
3138
+ async connectServer(serverName, config) {
3139
+ if (this.connections.has(serverName)) {
3140
+ await this.disconnectServer(serverName);
3141
+ }
3142
+ const safeEnv = {};
3143
+ for (const [k, v] of Object.entries(process.env)) {
3144
+ if (typeof v === "string") safeEnv[k] = v;
3145
+ }
3146
+ if (config.env) {
3147
+ Object.assign(safeEnv, config.env);
3148
+ }
3149
+ const transport = new StdioClientTransport({
3150
+ command: config.command,
3151
+ args: config.args || [],
3152
+ env: safeEnv
3153
+ });
3154
+ const client = new Client(
3155
+ {
3156
+ name: `orbit-client-${serverName}`,
3157
+ version: "1.0.0"
3158
+ },
3159
+ {
3160
+ capabilities: {}
3161
+ }
3162
+ );
3163
+ await client.connect(transport);
3164
+ const toolsResponse = await client.listTools();
3165
+ const serverTools = [];
3166
+ for (const mcpTool of toolsResponse.tools) {
3167
+ const toolName = `mcp__${serverName}__${mcpTool.name}`.replace(/[^a-zA-Z0-9_]/g, "_");
3168
+ const properties = {};
3169
+ const required = [];
3170
+ if (mcpTool.inputSchema && typeof mcpTool.inputSchema === "object") {
3171
+ const schema = mcpTool.inputSchema;
3172
+ if (schema.properties && typeof schema.properties === "object") {
3173
+ for (const [k, v] of Object.entries(schema.properties)) {
3174
+ properties[k] = {
3175
+ type: v.type || "string",
3176
+ description: v.description || ""
3177
+ };
3178
+ }
3179
+ }
3180
+ if (Array.isArray(schema.required)) {
3181
+ required.push(...schema.required);
3182
+ }
3183
+ }
3184
+ const definition = {
3185
+ name: toolName,
3186
+ description: `[MCP Server: ${serverName}] ${mcpTool.description || mcpTool.name}`,
3187
+ readOnly: false,
3188
+ parameters: {
3189
+ type: "object",
3190
+ properties,
3191
+ required
3192
+ }
3193
+ };
3194
+ const orbitTool = {
3195
+ definition,
3196
+ async execute(args, context) {
3197
+ const approved = await context.requestApproval({
3198
+ type: "command",
3199
+ summary: `Run MCP tool ${serverName}:${mcpTool.name}`,
3200
+ details: {
3201
+ command: `mcp ${serverName} ${mcpTool.name} ${JSON.stringify(args)}`
3202
+ }
3203
+ });
3204
+ if (!approved) {
3205
+ return {
3206
+ success: false,
3207
+ output: `Execution denied by user for MCP tool ${serverName}:${mcpTool.name}`,
3208
+ error: "Approval rejected"
3209
+ };
3210
+ }
3211
+ try {
3212
+ const result = await client.callTool({
3213
+ name: mcpTool.name,
3214
+ arguments: args
3215
+ });
3216
+ let textOutput = "";
3217
+ if (Array.isArray(result.content)) {
3218
+ for (const item of result.content) {
3219
+ if (item.type === "text") {
3220
+ textOutput += item.text + "\n";
3221
+ } else {
3222
+ textOutput += JSON.stringify(item) + "\n";
3223
+ }
3224
+ }
3225
+ } else {
3226
+ textOutput = JSON.stringify(result.content);
3227
+ }
3228
+ return {
3229
+ success: !result.isError,
3230
+ output: textOutput.trim(),
3231
+ error: result.isError ? textOutput : void 0
3232
+ };
3233
+ } catch (err) {
3234
+ return {
3235
+ success: false,
3236
+ output: `MCP execution error: ${err.message}`,
3237
+ error: err.message
3238
+ };
3239
+ }
3240
+ }
3241
+ };
3242
+ serverTools.push(orbitTool);
3243
+ }
3244
+ this.connections.set(serverName, {
3245
+ name: serverName,
3246
+ client,
3247
+ transport,
3248
+ tools: serverTools
3249
+ });
3250
+ return serverTools;
3251
+ }
3252
+ async disconnectServer(serverName) {
3253
+ const conn = this.connections.get(serverName);
3254
+ if (conn) {
3255
+ try {
3256
+ await conn.client.close();
3257
+ } catch {
3258
+ }
3259
+ this.connections.delete(serverName);
3260
+ }
3261
+ }
3262
+ async closeAll() {
3263
+ for (const [serverName] of this.connections) {
3264
+ await this.disconnectServer(serverName);
3265
+ }
3266
+ }
3267
+ };
3268
+
3269
+ // src/approval/approver.ts
3270
+ import readline from "readline";
3271
+ import pc3 from "picocolors";
3272
+ var ApprovalEngine = class {
3273
+ yolo;
3274
+ planMode;
3275
+ constructor(options) {
3276
+ this.yolo = Boolean(options.yolo);
3277
+ this.planMode = Boolean(options.planMode);
3278
+ }
3279
+ isYolo() {
3280
+ return this.yolo;
3281
+ }
3282
+ setYolo(val) {
3283
+ this.yolo = val;
3284
+ }
3285
+ isPlanMode() {
3286
+ return this.planMode;
3287
+ }
3288
+ async requestApproval(req) {
3289
+ if (this.planMode) {
3290
+ console.log(pc3.yellow(`
3291
+ [Plan Mode] Tool action skipped (read-only): ${req.summary}`));
3292
+ return false;
3293
+ }
3294
+ if (this.yolo) {
3295
+ console.log(pc3.dim(`\u26A1 [YOLO] Auto-approved: ${req.summary}`));
3296
+ return true;
3297
+ }
3298
+ console.log("\n" + pc3.bold(pc3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Action Approval Required \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501")));
3299
+ console.log(pc3.bold("Action: ") + req.summary);
3300
+ if (req.type === "file_edit" && req.details?.before !== void 0 && req.details?.after !== void 0) {
3301
+ const diffOutput = req.details.diff || formatTerminalDiff(
3302
+ req.details.path || "file",
3303
+ req.details.before,
3304
+ req.details.after
3305
+ );
3306
+ console.log("\n" + diffOutput + "\n");
3307
+ } else if (req.type === "file_write" && req.details?.after !== void 0) {
3308
+ const preview = formatNewFilePreview(req.details.path || "file", req.details.after);
3309
+ console.log("\n" + preview + "\n");
3310
+ } else if (req.type === "command" && req.details?.command) {
3311
+ console.log(pc3.cyan(`$ ${pc3.bold(req.details.command)}
3312
+ `));
3313
+ }
3314
+ const answer = await this.promptUser(
3315
+ pc3.bold(pc3.yellow("Approve this action?")) + " [y/N / (a)ll-yolo]: "
3316
+ );
3317
+ const clean = answer.trim().toLowerCase();
3318
+ if (clean === "a" || clean === "all" || clean === "yolo") {
3319
+ this.yolo = true;
3320
+ console.log(pc3.yellow("\u26A1 YOLO mode enabled for remainder of session.\n"));
3321
+ return true;
3322
+ }
3323
+ if (clean === "y" || clean === "yes") {
3324
+ console.log(pc3.green("\u2713 Approved.\n"));
3325
+ return true;
3326
+ }
3327
+ console.log(pc3.red("\u2717 Denied by user.\n"));
3328
+ return false;
3329
+ }
3330
+ promptUser(question) {
3331
+ if (!process.stdin.isTTY) {
3332
+ return Promise.resolve("no");
3333
+ }
3334
+ const rl = readline.createInterface({
3335
+ input: process.stdin,
3336
+ output: process.stdout
3337
+ });
3338
+ return new Promise((resolve) => {
3339
+ rl.question(question, (answer) => {
3340
+ rl.close();
3341
+ resolve(answer);
3342
+ });
3343
+ });
3344
+ }
3345
+ };
3346
+
3347
+ // src/config/projectInstructions.ts
3348
+ import fs7 from "fs";
3349
+ import path8 from "path";
3350
+ var INSTRUCTION_FILES = [
3351
+ "LANTERN.md",
3352
+ "CODELANTERN.md",
3353
+ ".codelantern/instructions.md",
3354
+ ".lantern.md",
3355
+ "GEMINI.md",
3356
+ "ORBIT.md"
3357
+ ];
3358
+ function loadProjectInstructions(workspaceDir) {
3359
+ for (const filename of INSTRUCTION_FILES) {
3360
+ const filePath = path8.join(workspaceDir, filename);
3361
+ if (fs7.existsSync(filePath)) {
3362
+ try {
3363
+ const stat = fs7.statSync(filePath);
3364
+ if (stat.isFile() && stat.size <= 200 * 1024) {
3365
+ const content = fs7.readFileSync(filePath, "utf8").trim();
3366
+ if (content.length > 0) {
3367
+ return {
3368
+ found: true,
3369
+ filePath,
3370
+ content
3371
+ };
3372
+ }
3373
+ }
3374
+ } catch {
3375
+ }
3376
+ }
3377
+ }
3378
+ return { found: false };
3379
+ }
3380
+
3381
+ // src/session/sessionManager.ts
3382
+ import fs8 from "fs";
3383
+ import path9 from "path";
3384
+ import crypto2 from "crypto";
3385
+ function getSessionsDir() {
3386
+ const dir = path9.join(getLanternHomeDir(), "sessions");
3387
+ if (!fs8.existsSync(dir)) {
3388
+ try {
3389
+ fs8.mkdirSync(dir, { recursive: true });
3390
+ } catch {
3391
+ }
3392
+ }
3393
+ return dir;
3394
+ }
3395
+ function generateSessionId() {
3396
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
3397
+ const rand = crypto2.randomBytes(3).toString("hex");
3398
+ return `session_${timestamp}_${rand}`;
3399
+ }
3400
+ function createNewSession(workspace, model, initialTitle) {
3401
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3402
+ return {
3403
+ id: generateSessionId(),
3404
+ title: initialTitle || `Session in ${path9.basename(workspace) || "workspace"}`,
3405
+ createdAt: now,
3406
+ updatedAt: now,
3407
+ workspace: path9.resolve(workspace),
3408
+ model,
3409
+ turns: [],
3410
+ totalUsage: {
3411
+ promptTokens: 0,
3412
+ candidatesTokens: 0,
3413
+ totalTokens: 0
3414
+ }
3415
+ };
3416
+ }
3417
+ function saveSession(session) {
3418
+ const sessionsDir = getSessionsDir();
3419
+ const filePath = path9.join(sessionsDir, `${session.id}.json`);
3420
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3421
+ const serialized = JSON.stringify(session, (_key, val) => {
3422
+ if (typeof val === "string") {
3423
+ return sanitizeSecrets(val);
3424
+ }
3425
+ return val;
3426
+ }, 2);
3427
+ try {
3428
+ fs8.writeFileSync(filePath, serialized, { encoding: "utf8" });
3429
+ } catch (err) {
3430
+ console.error(`Warning: Failed to save session to ${filePath}: ${err.message}`);
3431
+ }
3432
+ }
3433
+ function loadSession(sessionId) {
3434
+ const sessionsDir = getSessionsDir();
3435
+ const normalizedId = sessionId.endsWith(".json") ? sessionId : `${sessionId}.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"));
3440
+ const match = files.find((f) => f.startsWith(sessionId));
3441
+ if (match) {
3442
+ try {
3443
+ const raw = fs8.readFileSync(path9.join(sessionsDir, match), "utf8");
3444
+ return JSON.parse(raw);
3445
+ } catch {
3446
+ return null;
3447
+ }
3448
+ }
3449
+ return null;
3450
+ }
3451
+ try {
3452
+ const raw = fs8.readFileSync(filePath, "utf8");
3453
+ return JSON.parse(raw);
3454
+ } catch {
3455
+ return null;
3456
+ }
3457
+ }
3458
+ function listSessions() {
3459
+ const sessionsDir = getSessionsDir();
3460
+ if (!fs8.existsSync(sessionsDir)) return [];
3461
+ const files = fs8.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
3462
+ const sessions = [];
3463
+ for (const file of files) {
3464
+ try {
3465
+ const raw = fs8.readFileSync(path9.join(sessionsDir, file), "utf8");
3466
+ const data = JSON.parse(raw);
3467
+ sessions.push({
3468
+ id: data.id,
3469
+ title: data.title || "Untitled Session",
3470
+ updatedAt: data.updatedAt || data.createdAt,
3471
+ turnCount: data.turns?.length || 0,
3472
+ workspace: data.workspace || "",
3473
+ model: data.model || ""
3474
+ });
3475
+ } catch {
3476
+ }
3477
+ }
3478
+ return sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
3479
+ }
3480
+ function deleteSession(sessionId) {
3481
+ const sessionsDir = getSessionsDir();
3482
+ const normalizedId = sessionId.endsWith(".json") ? sessionId : `${sessionId}.json`;
3483
+ const filePath = path9.join(sessionsDir, normalizedId);
3484
+ if (fs8.existsSync(filePath)) {
3485
+ fs8.unlinkSync(filePath);
3486
+ return true;
3487
+ }
3488
+ if (!fs8.existsSync(sessionsDir)) return false;
3489
+ const files = fs8.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
3490
+ const match = files.find((f) => f.startsWith(sessionId));
3491
+ if (match) {
3492
+ fs8.unlinkSync(path9.join(sessionsDir, match));
3493
+ return true;
3494
+ }
3495
+ return false;
3496
+ }
3497
+ function exportSessionMarkdown(session) {
3498
+ const lines = [
3499
+ `# Code Lantern Session: ${session.title}`,
3500
+ `**Session ID:** \`${session.id}\` `,
3501
+ `**Model:** \`${session.model}\` `,
3502
+ `**Workspace:** \`${session.workspace}\` `,
3503
+ `**Created:** ${session.createdAt} `,
3504
+ `**Updated:** ${session.updatedAt} `,
3505
+ `**Total Tokens:** ${session.totalUsage.totalTokens} `,
3506
+ "",
3507
+ "---",
3508
+ ""
3509
+ ];
3510
+ for (const turn of session.turns) {
3511
+ const roleHeading = turn.role === "user" ? "### \u{1F464} User" : turn.role === "model" ? "### \u{1F3EE} Code Lantern" : "### \u{1F6E0}\uFE0F Tool";
3512
+ lines.push(roleHeading);
3513
+ lines.push(`*${turn.timestamp}*`);
3514
+ lines.push("");
3515
+ if (turn.text) {
3516
+ lines.push(turn.text);
3517
+ lines.push("");
3518
+ }
3519
+ if (turn.toolCalls && turn.toolCalls.length > 0) {
3520
+ lines.push("**Tool Calls:**");
3521
+ for (const call of turn.toolCalls) {
3522
+ lines.push(`- \`${call.name}\`: \`${JSON.stringify(call.args)}\``);
3523
+ }
3524
+ lines.push("");
3525
+ }
3526
+ if (turn.toolResults && turn.toolResults.length > 0) {
3527
+ lines.push("**Tool Results:**");
3528
+ for (const res of turn.toolResults) {
3529
+ const preview = typeof res.result === "string" ? res.result.slice(0, 200) : JSON.stringify(res.result).slice(0, 200);
3530
+ lines.push(`- \`${res.name}\`: ${preview}...`);
3531
+ }
3532
+ lines.push("");
3533
+ }
3534
+ lines.push("---");
3535
+ lines.push("");
3536
+ }
3537
+ return lines.join("\n");
3538
+ }
3539
+
3540
+ // src/session/compactor.ts
3541
+ function shouldCompact(turns, threshold = 30) {
3542
+ return turns.length > threshold;
3543
+ }
3544
+ function compactTurns(turns, retainRecentCount = 10) {
3545
+ if (turns.length <= retainRecentCount + 2) {
3546
+ return turns;
3547
+ }
3548
+ const toCompact = turns.slice(0, turns.length - retainRecentCount);
3549
+ const toKeep = turns.slice(turns.length - retainRecentCount);
3550
+ const summaryPoints = [];
3551
+ for (const turn of toCompact) {
3552
+ if (turn.role === "user" && turn.text) {
3553
+ summaryPoints.push(`- User asked: "${turn.text.slice(0, 150).replace(/\n/g, " ")}"`);
3554
+ } else if (turn.role === "model") {
3555
+ if (turn.toolCalls && turn.toolCalls.length > 0) {
3556
+ const toolNames = turn.toolCalls.map((t) => t.name).join(", ");
3557
+ summaryPoints.push(`- Orbit executed tools: ${toolNames}`);
3558
+ }
3559
+ if (turn.text) {
3560
+ summaryPoints.push(`- Orbit responded: "${turn.text.slice(0, 120).replace(/\n/g, " ")}..."`);
3561
+ }
3562
+ }
3563
+ }
3564
+ const compactedTurn = {
3565
+ role: "user",
3566
+ text: `[SYSTEM: Earlier conversation context compacted (${toCompact.length} turns)]:
3567
+ ${summaryPoints.join("\n")}`,
3568
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3569
+ };
3570
+ const ackTurn = {
3571
+ role: "model",
3572
+ text: "Understood. I retain full awareness of these previous tasks and will continue from here.",
3573
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3574
+ };
3575
+ return [compactedTurn, ackTurn, ...toKeep];
3576
+ }
3577
+
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";
3801
+ import pc5 from "picocolors";
3802
+
3803
+ // src/ui/spinner.ts
3804
+ import pc4 from "picocolors";
3805
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3806
+ var TerminalSpinner = class {
3807
+ timer = null;
3808
+ frameIdx = 0;
3809
+ message = "";
3810
+ isTTY = Boolean(process.stdout.isTTY);
3811
+ isRunning = false;
3812
+ start(message) {
3813
+ this.message = message;
3814
+ this.isRunning = true;
3815
+ if (!this.isTTY) {
3816
+ console.log(`[Lantern] ${message}`);
3817
+ return this;
3818
+ }
3819
+ if (this.timer) {
3820
+ clearInterval(this.timer);
3821
+ }
3822
+ process.stdout.write("\x1B[?25l");
3823
+ this.timer = setInterval(() => {
3824
+ this.render();
3825
+ }, 80);
3826
+ this.render();
3827
+ return this;
3828
+ }
3829
+ update(message) {
3830
+ this.message = message;
3831
+ if (!this.isTTY) {
3832
+ console.log(`[Lantern] ${message}`);
3833
+ } else if (this.isRunning) {
3834
+ this.render();
3835
+ }
3836
+ return this;
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
+ }
3855
+ render() {
3856
+ if (!this.isTTY || !this.isRunning) return;
3857
+ const frame = pc4.yellow(SPINNER_FRAMES[this.frameIdx % SPINNER_FRAMES.length]);
3858
+ this.frameIdx++;
3859
+ process.stdout.write(`\r\x1B[2K${frame} ${pc4.dim(this.message)}`);
3860
+ }
3861
+ stop() {
3862
+ if (this.timer) {
3863
+ clearInterval(this.timer);
3864
+ this.timer = null;
3865
+ }
3866
+ this.isRunning = false;
3867
+ if (this.isTTY) {
3868
+ process.stdout.write("\r\x1B[2K");
3869
+ process.stdout.write("\x1B[?25h");
3870
+ }
3871
+ return this;
3872
+ }
3873
+ succeed(message) {
3874
+ this.stop();
3875
+ console.log(`${pc4.green("\u2713")} ${message}`);
3876
+ return this;
3877
+ }
3878
+ fail(message) {
3879
+ this.stop();
3880
+ console.log(`${pc4.red("\u2717")} ${message}`);
3881
+ return this;
3882
+ }
3883
+ info(message) {
3884
+ this.stop();
3885
+ console.log(`${pc4.cyan("\u2139")} ${message}`);
3886
+ return this;
3887
+ }
3888
+ };
3889
+
3890
+ // src/agent/lanternAgent.ts
3891
+ var LanternAgent = class {
3892
+ providerRegistry;
3893
+ tools;
3894
+ mcpManager;
3895
+ skillManager;
3896
+ approvalEngine;
3897
+ spinner;
3898
+ constructor() {
3899
+ this.providerRegistry = new ProviderRegistry();
3900
+ this.tools = new ToolRegistry();
3901
+ this.mcpManager = new McpManager();
3902
+ this.skillManager = new SkillManager();
3903
+ this.approvalEngine = new ApprovalEngine({});
3904
+ this.spinner = new TerminalSpinner();
3905
+ }
3906
+ getProviderRegistry() {
3907
+ return this.providerRegistry;
3908
+ }
3909
+ getSkillManager() {
3910
+ return this.skillManager;
3911
+ }
3912
+ async initMcp() {
3913
+ try {
3914
+ const mcpTools = await this.mcpManager.initialize();
3915
+ this.tools.registerTools(mcpTools);
3916
+ } catch {
3917
+ }
3918
+ }
3919
+ async shutdown() {
3920
+ await this.mcpManager.closeAll();
3921
+ }
3922
+ buildSystemPrompt(workspace, planMode = false, activeSkills = []) {
3923
+ const projectInstructions = loadProjectInstructions(workspace);
3924
+ const basePrompt = [
3925
+ "You are Code Lantern, an elite AI coding assistant operating directly in the terminal.",
3926
+ `Current Workspace: ${path11.resolve(workspace)}`,
3927
+ `Environment OS: ${process.platform} (${process.arch})`,
3928
+ "",
3929
+ "CORE OPERATING PRINCIPLES:",
3930
+ "1. Be direct, pragmatic, and helpful. Prioritize working, robust code over verbosity.",
3931
+ "2. Inspect files before proposing edits to understand real project conventions and existing code.",
3932
+ "3. Always use the provided tools to read, search, edit, create files, or run commands.",
3933
+ "4. When editing files with edit_file, ensure the target string matches exactly, including indentation.",
3934
+ "5. Safety boundaries: Never attempt to read secret files (.env, keys) or escape the workspace.",
3935
+ "6. Never run destructive commands that wipe unrelated data, partitions, or system drives.",
3936
+ "7. Never push code, make commits, or publish artifacts unless explicitly instructed by the user."
3937
+ ];
3938
+ if (planMode) {
3939
+ basePrompt.push(
3940
+ "",
3941
+ "PLAN MODE IS ACTIVE (READ-ONLY):",
3942
+ "- Your objective is to thoroughly inspect the codebase and produce a detailed, actionable implementation plan.",
3943
+ "- You may inspect files and search code using read_file, file_search, grep_search, and project_tree.",
3944
+ "- Do NOT attempt to write files, edit files, or run mutating commands.",
3945
+ "- Structure your plan clearly: Overview, Architecture & Files Impacted, Step-by-Step Implementation, and Verification Strategy."
3946
+ );
3947
+ }
3948
+ if (projectInstructions.found && projectInstructions.content) {
3949
+ basePrompt.push(
3950
+ "",
3951
+ `PROJECT INSTRUCTIONS (${path11.basename(projectInstructions.filePath || "instructions")}):`,
3952
+ "---",
3953
+ projectInstructions.content,
3954
+ "---"
3955
+ );
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
+ }
3969
+ return basePrompt.join("\n");
3970
+ }
3971
+ async runTurn(session, userPrompt, context, attachments = []) {
3972
+ const turnStartTime = Date.now();
3973
+ this.approvalEngine = new ApprovalEngine({
3974
+ yolo: context.yolo,
3975
+ planMode: context.planMode
3976
+ });
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);
3995
+ const activeTools = this.tools.getDefinitions(context.planMode);
3996
+ const userTurn = {
3997
+ role: "user",
3998
+ text: userPrompt,
3999
+ attachments: attachments.length > 0 ? attachments : void 0,
4000
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4001
+ };
4002
+ session.turns.push(userTurn);
4003
+ let step = 0;
4004
+ const maxSteps = context.maxSteps || 25;
4005
+ let totalToolsExecuted = 0;
4006
+ let finalAssistantText = "";
4007
+ const sessionUsage = {
4008
+ promptTokens: 0,
4009
+ candidatesTokens: 0,
4010
+ totalTokens: 0
4011
+ };
4012
+ const aggregatedTiming = {
4013
+ totalMs: 0,
4014
+ toolExecutionMs: {}
4015
+ };
4016
+ while (step < maxSteps) {
4017
+ step++;
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})...`);
4023
+ let currentStepText = "";
4024
+ let currentToolCalls = [];
4025
+ try {
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
+ },
4049
+ onChunk: (chunk) => {
4050
+ if (currentStepText === "") {
4051
+ this.spinner.stop();
4052
+ }
4053
+ currentStepText += chunk;
4054
+ if (!context.jsonOutput) {
4055
+ process.stdout.write(chunk);
4056
+ }
4057
+ },
4058
+ onToolCalls: (calls) => {
4059
+ currentToolCalls = calls;
4060
+ },
4061
+ onUsage: (usage) => {
4062
+ sessionUsage.promptTokens = usage.promptTokens;
4063
+ sessionUsage.candidatesTokens = usage.candidatesTokens;
4064
+ sessionUsage.totalTokens = usage.totalTokens;
4065
+ }
4066
+ }
4067
+ });
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
+ }
4075
+ if (currentStepText) {
4076
+ if (!currentStepText.endsWith("\n")) {
4077
+ console.log("");
4078
+ }
4079
+ finalAssistantText = currentStepText;
4080
+ }
4081
+ if (!streamResult.toolCalls || streamResult.toolCalls.length === 0) {
4082
+ session.turns.push({
4083
+ role: "model",
4084
+ text: currentStepText,
4085
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4086
+ });
4087
+ break;
4088
+ }
4089
+ session.turns.push({
4090
+ role: "model",
4091
+ text: currentStepText || void 0,
4092
+ toolCalls: streamResult.toolCalls,
4093
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4094
+ });
4095
+ const toolResults = [];
4096
+ for (const call of streamResult.toolCalls) {
4097
+ if (context.signal?.aborted) {
4098
+ throw context.signal.reason || new Error("Cancelled by user.");
4099
+ }
4100
+ totalToolsExecuted++;
4101
+ const callSummary = `${call.name}(${Object.keys(call.args).map((k) => `${k}: ${JSON.stringify(call.args[k])}`).join(", ")})`;
4102
+ if (!context.jsonOutput) {
4103
+ console.log(pc5.cyan(`
4104
+ \u{1F6E0}\uFE0F Tool: ${pc5.bold(call.name)}`) + pc5.dim(` ${callSummary.slice(0, 90)}`));
4105
+ }
4106
+ this.spinner.toolExecuting(call.name);
4107
+ const toolContext = {
4108
+ workspace: context.workspace,
4109
+ yolo: this.approvalEngine.isYolo(),
4110
+ planMode: context.planMode,
4111
+ signal: context.signal,
4112
+ requestApproval: (req) => {
4113
+ this.spinner.stop();
4114
+ return this.approvalEngine.requestApproval(req);
4115
+ }
4116
+ };
4117
+ const toolStart = Date.now();
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;
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
+ }
4126
+ if (executionResult.success) {
4127
+ if (!context.jsonOutput) {
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)`));
4130
+ }
4131
+ toolResults.push({
4132
+ callId: call.id,
4133
+ name: call.name,
4134
+ result: sanitizeSecrets(trimmedOutput)
4135
+ });
4136
+ } else {
4137
+ if (!context.jsonOutput) {
4138
+ console.log(pc5.red(`\u2717 Error: ${executionResult.error || executionResult.output}`));
4139
+ }
4140
+ toolResults.push({
4141
+ callId: call.id,
4142
+ name: call.name,
4143
+ result: { error: executionResult.error || executionResult.output },
4144
+ error: executionResult.error
4145
+ });
4146
+ }
4147
+ }
4148
+ session.turns.push({
4149
+ role: "tool",
4150
+ toolResults,
4151
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4152
+ });
4153
+ if (step >= maxSteps) {
4154
+ const limitMsg = `
4155
+ [Reached maximum configured tool steps (${maxSteps}). Code Lantern paused to avoid infinite loops.]`;
4156
+ console.log(pc5.yellow(limitMsg));
4157
+ finalAssistantText += limitMsg;
4158
+ break;
4159
+ }
4160
+ } catch (err) {
4161
+ this.spinner.stop();
4162
+ throw err;
4163
+ }
4164
+ }
4165
+ if (shouldCompact(session.turns)) {
4166
+ session.turns = compactTurns(session.turns);
4167
+ }
4168
+ aggregatedTiming.totalMs = Date.now() - turnStartTime;
4169
+ session.totalUsage.promptTokens += sessionUsage.promptTokens;
4170
+ session.totalUsage.candidatesTokens += sessionUsage.candidatesTokens;
4171
+ session.totalUsage.totalTokens += sessionUsage.totalTokens;
4172
+ saveSession(session);
4173
+ return {
4174
+ reply: finalAssistantText,
4175
+ toolCount: totalToolsExecuted,
4176
+ usage: sessionUsage,
4177
+ timing: aggregatedTiming
4178
+ };
4179
+ }
4180
+ };
4181
+
4182
+ // src/provider/attachment.ts
4183
+ import fs10 from "fs";
4184
+ import path12 from "path";
4185
+ var MIME_MAP = {
4186
+ ".png": "image/png",
4187
+ ".jpg": "image/jpeg",
4188
+ ".jpeg": "image/jpeg",
4189
+ ".webp": "image/webp",
4190
+ ".gif": "image/gif",
4191
+ ".pdf": "application/pdf",
4192
+ ".txt": "text/plain",
4193
+ ".md": "text/markdown",
4194
+ ".json": "application/json",
4195
+ ".ts": "text/plain",
4196
+ ".js": "text/plain",
4197
+ ".py": "text/plain",
4198
+ ".sh": "text/plain",
4199
+ ".html": "text/html",
4200
+ ".css": "text/css"
4201
+ };
4202
+ var BINARY_IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".pdf"]);
4203
+ var MAX_ATTACHMENT_SIZE = 15 * 1024 * 1024;
4204
+ function loadAttachment(filePath) {
4205
+ const resolved = path12.resolve(filePath);
4206
+ if (!fs10.existsSync(resolved)) {
4207
+ throw new Error(`Attached file does not exist: ${filePath}`);
4208
+ }
4209
+ const stat = fs10.statSync(resolved);
4210
+ if (!stat.isFile()) {
4211
+ throw new Error(`Attachment target is not a file: ${filePath}`);
4212
+ }
4213
+ if (stat.size > MAX_ATTACHMENT_SIZE) {
4214
+ throw new Error(`Attachment file too large: ${filePath} (${(stat.size / (1024 * 1024)).toFixed(1)}MB, max 15MB)`);
4215
+ }
4216
+ const ext = path12.extname(resolved).toLowerCase();
4217
+ const mimeType = MIME_MAP[ext] || "application/octet-stream";
4218
+ const name = path12.basename(resolved);
4219
+ if (BINARY_IMAGE_EXTS.has(ext)) {
4220
+ const buffer = fs10.readFileSync(resolved);
4221
+ return {
4222
+ path: resolved,
4223
+ name,
4224
+ mimeType,
4225
+ size: stat.size,
4226
+ base64Data: buffer.toString("base64")
4227
+ };
4228
+ }
4229
+ const textContent = fs10.readFileSync(resolved, "utf8");
4230
+ return {
4231
+ path: resolved,
4232
+ name,
4233
+ mimeType: mimeType.startsWith("text/") ? mimeType : "text/plain",
4234
+ size: stat.size,
4235
+ textContent
4236
+ };
4237
+ }
4238
+ function loadAttachments(filePaths) {
4239
+ if (!filePaths || filePaths.length === 0) return [];
4240
+ return filePaths.map((p) => loadAttachment(p));
4241
+ }
4242
+
4243
+ export {
4244
+ TimeoutError,
4245
+ FirstTokenTimeoutError,
4246
+ CancelledError,
4247
+ MissingApiKeyError,
4248
+ RateLimitError,
4249
+ AuthenticationError,
4250
+ ModelNotFoundError,
4251
+ LanternProviderError,
4252
+ formatErrorMessage,
4253
+ createLinkedAbortController,
4254
+ wrapStreamWithFirstTokenTimeout,
4255
+ DEFAULT_CONFIG,
4256
+ ALLOWED_CONFIG_KEYS,
4257
+ validateConfigValue,
4258
+ getLanternHomeDir,
4259
+ getConfigFilePath,
4260
+ loadConfig,
4261
+ saveConfig,
4262
+ setConfigValue,
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,
4283
+ getMcpConfigPath,
4284
+ loadMcpConfig,
4285
+ saveMcpConfig,
4286
+ addMcpServer,
4287
+ removeMcpServer,
4288
+ toggleMcpServer,
4289
+ McpManager,
4290
+ ApprovalEngine,
4291
+ loadProjectInstructions,
4292
+ getSessionsDir,
4293
+ generateSessionId,
4294
+ createNewSession,
4295
+ saveSession,
4296
+ loadSession,
4297
+ listSessions,
4298
+ deleteSession,
4299
+ exportSessionMarkdown,
4300
+ shouldCompact,
4301
+ compactTurns,
4302
+ SkillManager,
4303
+ LanternAgent,
4304
+ loadAttachment,
4305
+ loadAttachments
4306
+ };
4307
+ //# sourceMappingURL=chunk-PELESOCI.js.map