codelantern-agent-cli 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +283 -163
- package/dist/{chunk-PELESOCI.js → chunk-45FEFNMM.js} +877 -154
- package/dist/chunk-45FEFNMM.js.map +1 -0
- package/dist/cli.js +351 -62
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +91 -5
- package/dist/index.js +19 -1
- package/package.json +2 -1
- package/dist/chunk-PELESOCI.js.map +0 -1
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/security/secrets.ts
|
|
4
|
+
import path from "path";
|
|
5
|
+
var SECRET_FILE_PATTERNS = [
|
|
6
|
+
/^\.env(\..+)?$/i,
|
|
7
|
+
/\.pem$/i,
|
|
8
|
+
/\.key$/i,
|
|
9
|
+
/\.pfx$/i,
|
|
10
|
+
/\.p12$/i,
|
|
11
|
+
/^id_rsa/i,
|
|
12
|
+
/^id_dsa/i,
|
|
13
|
+
/^id_ecdsa/i,
|
|
14
|
+
/^id_ed25519/i,
|
|
15
|
+
/service[_-]?account.*\.json$/i,
|
|
16
|
+
/credentials.*\.json$/i,
|
|
17
|
+
/\.keystore$/i,
|
|
18
|
+
/\.token$/i,
|
|
19
|
+
/\.secret/i
|
|
20
|
+
];
|
|
21
|
+
function isSecretFile(filePath) {
|
|
22
|
+
const base = path.basename(filePath);
|
|
23
|
+
return SECRET_FILE_PATTERNS.some((pattern) => pattern.test(base));
|
|
24
|
+
}
|
|
25
|
+
function sanitizeSecrets(text) {
|
|
26
|
+
if (!text) return text;
|
|
27
|
+
let sanitized = text;
|
|
28
|
+
sanitized = sanitized.replace(/AIza[0-9A-Za-z\-_]{30,40}/g, "AIza***********************************");
|
|
29
|
+
sanitized = sanitized.replace(/sk-(?:proj-)?[A-Za-z0-9_\-]{32,}/g, "sk-************************************");
|
|
30
|
+
sanitized = sanitized.replace(/sk-ant-[A-Za-z0-9_\-]{32,}/g, "sk-ant-********************************");
|
|
31
|
+
sanitized = sanitized.replace(/sk-or-v1-[A-Za-z0-9_\-]{32,}/g, "sk-or-v1-******************************");
|
|
32
|
+
sanitized = sanitized.replace(/ghp_[0-9a-zA-Z]{36}/g, "ghp_************************************");
|
|
33
|
+
sanitized = sanitized.replace(/github_pat_[0-9a-zA-Z_]{82}/g, "github_pat_**********************************************************************************");
|
|
34
|
+
const envKeys = [
|
|
35
|
+
process.env.GEMINI_API_KEY,
|
|
36
|
+
process.env.GOOGLE_API_KEY,
|
|
37
|
+
process.env.OPENAI_API_KEY,
|
|
38
|
+
process.env.ANTHROPIC_API_KEY,
|
|
39
|
+
process.env.OPENROUTER_API_KEY,
|
|
40
|
+
process.env.CUSTOM_API_KEY
|
|
41
|
+
];
|
|
42
|
+
for (const envKey of envKeys) {
|
|
43
|
+
if (envKey && envKey.trim().length > 6) {
|
|
44
|
+
sanitized = sanitized.split(envKey.trim()).join("[REDACTED_API_KEY]");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return sanitized;
|
|
48
|
+
}
|
|
49
|
+
|
|
3
50
|
// src/provider/errors.ts
|
|
4
51
|
import pc from "picocolors";
|
|
5
52
|
var TimeoutError = class extends Error {
|
|
@@ -77,38 +124,38 @@ var LanternProviderError = class extends Error {
|
|
|
77
124
|
function formatErrorMessage(err) {
|
|
78
125
|
if (err instanceof CancelledError) {
|
|
79
126
|
return pc.yellow(`
|
|
80
|
-
[Cancelled] ${err.message}`);
|
|
127
|
+
[Cancelled] ${sanitizeSecrets(err.message)}`);
|
|
81
128
|
}
|
|
82
129
|
if (err instanceof FirstTokenTimeoutError) {
|
|
83
130
|
return pc.red(`
|
|
84
|
-
\u23F1\uFE0F ${err.message}
|
|
85
|
-
` + pc.yellow(` Tip: Check your network connection or try
|
|
131
|
+
\u23F1\uFE0F ${sanitizeSecrets(err.message)}
|
|
132
|
+
`) + pc.yellow(` Tip: Check your network connection or try switching to a faster model with '/model' or 'lantern model set <id>'.`);
|
|
86
133
|
}
|
|
87
134
|
if (err instanceof TimeoutError) {
|
|
88
135
|
return pc.red(`
|
|
89
|
-
\u23F1\uFE0F ${err.message}
|
|
90
|
-
` + pc.yellow(` Tip: You can increase the timeout limit using 'lantern config set timeoutMs 120000'.`)
|
|
136
|
+
\u23F1\uFE0F ${sanitizeSecrets(err.message)}
|
|
137
|
+
`) + pc.yellow(` Tip: Check your network connection. You can increase the timeout limit using 'lantern config set timeoutMs 120000' or test provider connectivity with 'lantern providers test'.`);
|
|
91
138
|
}
|
|
92
139
|
if (err instanceof MissingApiKeyError) {
|
|
93
140
|
return pc.red(`
|
|
94
|
-
${err.message}`);
|
|
141
|
+
${sanitizeSecrets(err.message)}`);
|
|
95
142
|
}
|
|
96
143
|
if (err instanceof RateLimitError) {
|
|
97
144
|
return pc.yellow(`
|
|
98
|
-
\u26A0\uFE0F ${err.message}`);
|
|
145
|
+
\u26A0\uFE0F ${sanitizeSecrets(err.message)}`);
|
|
99
146
|
}
|
|
100
147
|
if (err instanceof AuthenticationError) {
|
|
101
148
|
return pc.red(`
|
|
102
|
-
\u{1F512} ${err.message}`);
|
|
149
|
+
\u{1F512} ${sanitizeSecrets(err.message)}`);
|
|
103
150
|
}
|
|
104
151
|
if (err instanceof ModelNotFoundError) {
|
|
105
152
|
return pc.red(`
|
|
106
|
-
\u{1F50D} ${err.message}
|
|
107
|
-
` + pc.
|
|
153
|
+
\u{1F50D} ${sanitizeSecrets(err.message)}
|
|
154
|
+
`) + pc.yellow(` Tip: Run '/model' inside Lantern or 'lantern models list' to view and select available models for your account.`);
|
|
108
155
|
}
|
|
109
156
|
if (err instanceof Error) {
|
|
110
|
-
const msg = err.message || "";
|
|
111
|
-
if (msg.includes("API_KEY_INVALID") || msg.includes("401") || msg.includes("Incorrect API key")) {
|
|
157
|
+
const msg = sanitizeSecrets(err.message || "");
|
|
158
|
+
if (msg.includes("API_KEY_INVALID") || msg.includes("401") || msg.includes("Incorrect API key") || msg.includes("API key not valid")) {
|
|
112
159
|
return pc.red(`Authentication failed: Invalid API key. Update it with 'lantern keys set <provider>'.`);
|
|
113
160
|
}
|
|
114
161
|
if (msg.includes("RESOURCE_EXHAUSTED") || msg.includes("429")) {
|
|
@@ -119,7 +166,7 @@ ${err.message}`);
|
|
|
119
166
|
}
|
|
120
167
|
return pc.red(`Error: ${msg}`);
|
|
121
168
|
}
|
|
122
|
-
return pc.red(`An unexpected error occurred: ${String(err)}`);
|
|
169
|
+
return pc.red(`An unexpected error occurred: ${sanitizeSecrets(String(err))}`);
|
|
123
170
|
}
|
|
124
171
|
|
|
125
172
|
// src/provider/timeout.ts
|
|
@@ -152,15 +199,49 @@ function createLinkedAbortController(parentSignal, timeoutMs) {
|
|
|
152
199
|
};
|
|
153
200
|
return { controller, cleanup };
|
|
154
201
|
}
|
|
202
|
+
async function withTimeoutRace(promise, timeoutMs, operationName, signal, onTimeout) {
|
|
203
|
+
if (signal?.aborted) {
|
|
204
|
+
throw signal.reason || new CancelledError();
|
|
205
|
+
}
|
|
206
|
+
let timer = null;
|
|
207
|
+
let abortListener = null;
|
|
208
|
+
try {
|
|
209
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
210
|
+
timer = setTimeout(() => {
|
|
211
|
+
if (onTimeout) {
|
|
212
|
+
try {
|
|
213
|
+
onTimeout();
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
reject(new TimeoutError(timeoutMs, operationName));
|
|
218
|
+
}, timeoutMs);
|
|
219
|
+
if (signal) {
|
|
220
|
+
abortListener = () => {
|
|
221
|
+
if (timer) clearTimeout(timer);
|
|
222
|
+
reject(signal.reason || new CancelledError());
|
|
223
|
+
};
|
|
224
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
228
|
+
} finally {
|
|
229
|
+
if (timer) {
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
timer = null;
|
|
232
|
+
}
|
|
233
|
+
if (signal && abortListener) {
|
|
234
|
+
signal.removeEventListener("abort", abortListener);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
155
238
|
async function* wrapStreamWithFirstTokenTimeout(stream, firstTokenTimeoutMs, provider, model, signal, onFirstToken) {
|
|
156
239
|
let firstTokenReceived = false;
|
|
157
240
|
const startTime = Date.now();
|
|
158
241
|
let timer = null;
|
|
159
|
-
let timedOut = false;
|
|
160
242
|
const timerPromise = new Promise((_, reject) => {
|
|
161
243
|
timer = setTimeout(() => {
|
|
162
244
|
if (!firstTokenReceived) {
|
|
163
|
-
timedOut = true;
|
|
164
245
|
reject(new FirstTokenTimeoutError(firstTokenTimeoutMs, provider, model));
|
|
165
246
|
}
|
|
166
247
|
}, firstTokenTimeoutMs);
|
|
@@ -188,6 +269,9 @@ async function* wrapStreamWithFirstTokenTimeout(stream, firstTokenTimeoutMs, pro
|
|
|
188
269
|
timer = null;
|
|
189
270
|
}
|
|
190
271
|
if (result.done) {
|
|
272
|
+
if (onFirstToken) {
|
|
273
|
+
onFirstToken(Date.now() - startTime);
|
|
274
|
+
}
|
|
191
275
|
break;
|
|
192
276
|
}
|
|
193
277
|
firstTokenReceived = true;
|
|
@@ -348,7 +432,7 @@ function validateConfigValue(key, value) {
|
|
|
348
432
|
|
|
349
433
|
// src/config/config.ts
|
|
350
434
|
import fs from "fs";
|
|
351
|
-
import
|
|
435
|
+
import path2 from "path";
|
|
352
436
|
import os from "os";
|
|
353
437
|
function getLanternHomeDir() {
|
|
354
438
|
const customHome = process.env.CODELANTERN_HOME || process.env.LANTERN_HOME;
|
|
@@ -361,7 +445,7 @@ function getLanternHomeDir() {
|
|
|
361
445
|
}
|
|
362
446
|
return customHome;
|
|
363
447
|
}
|
|
364
|
-
const defaultDir =
|
|
448
|
+
const defaultDir = path2.join(os.homedir(), ".codelantern");
|
|
365
449
|
if (!fs.existsSync(defaultDir)) {
|
|
366
450
|
try {
|
|
367
451
|
fs.mkdirSync(defaultDir, { recursive: true });
|
|
@@ -371,7 +455,7 @@ function getLanternHomeDir() {
|
|
|
371
455
|
return defaultDir;
|
|
372
456
|
}
|
|
373
457
|
function getConfigFilePath() {
|
|
374
|
-
return
|
|
458
|
+
return path2.join(getLanternHomeDir(), "config.json");
|
|
375
459
|
}
|
|
376
460
|
function loadConfig() {
|
|
377
461
|
const configPath = getConfigFilePath();
|
|
@@ -435,11 +519,304 @@ function getConfigValue(key) {
|
|
|
435
519
|
return current[key];
|
|
436
520
|
}
|
|
437
521
|
|
|
522
|
+
// src/security/nativeStore.ts
|
|
523
|
+
import { execFileSync } from "child_process";
|
|
524
|
+
import { Entry } from "@napi-rs/keyring";
|
|
525
|
+
var NapiRsKeyringStore = class {
|
|
526
|
+
name;
|
|
527
|
+
constructor() {
|
|
528
|
+
this.name = this.resolveStoreName();
|
|
529
|
+
}
|
|
530
|
+
resolveStoreName() {
|
|
531
|
+
if (process.platform === "darwin") return "macOS Keychain";
|
|
532
|
+
if (process.platform === "win32") return "Windows Credential Manager";
|
|
533
|
+
if (process.platform === "linux") return "Linux Secret Service / Keyring";
|
|
534
|
+
return "OS Credential Store";
|
|
535
|
+
}
|
|
536
|
+
isAvailable() {
|
|
537
|
+
return typeof Entry === "function";
|
|
538
|
+
}
|
|
539
|
+
get(service, account) {
|
|
540
|
+
try {
|
|
541
|
+
const entry = new Entry(service, account);
|
|
542
|
+
const res = entry.getPassword();
|
|
543
|
+
return res ?? null;
|
|
544
|
+
} catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
set(service, account, secret) {
|
|
549
|
+
try {
|
|
550
|
+
const entry = new Entry(service, account);
|
|
551
|
+
entry.setPassword(secret);
|
|
552
|
+
return true;
|
|
553
|
+
} catch {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
delete(service, account) {
|
|
558
|
+
try {
|
|
559
|
+
const entry = new Entry(service, account);
|
|
560
|
+
return entry.deletePassword();
|
|
561
|
+
} catch {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
var MacosKeychainCliStore = class {
|
|
567
|
+
name = "macOS Keychain (CLI)";
|
|
568
|
+
isAvailable() {
|
|
569
|
+
return process.platform === "darwin";
|
|
570
|
+
}
|
|
571
|
+
get(service, account) {
|
|
572
|
+
if (process.platform !== "darwin") return null;
|
|
573
|
+
try {
|
|
574
|
+
const output = execFileSync(
|
|
575
|
+
"/usr/bin/security",
|
|
576
|
+
["find-generic-password", "-s", service, "-a", account, "-w"],
|
|
577
|
+
{ stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }
|
|
578
|
+
);
|
|
579
|
+
return output.trim();
|
|
580
|
+
} catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
set(service, account, secret) {
|
|
585
|
+
if (process.platform !== "darwin") return false;
|
|
586
|
+
try {
|
|
587
|
+
execFileSync(
|
|
588
|
+
"/usr/bin/security",
|
|
589
|
+
["add-generic-password", "-U", "-s", service, "-a", account, "-w", secret],
|
|
590
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
591
|
+
);
|
|
592
|
+
return true;
|
|
593
|
+
} catch {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
delete(service, account) {
|
|
598
|
+
if (process.platform !== "darwin") return false;
|
|
599
|
+
try {
|
|
600
|
+
execFileSync(
|
|
601
|
+
"/usr/bin/security",
|
|
602
|
+
["delete-generic-password", "-s", service, "-a", account],
|
|
603
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
604
|
+
);
|
|
605
|
+
return true;
|
|
606
|
+
} catch {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
var WindowsCredentialManagerCliStore = class {
|
|
612
|
+
name = "Windows Credential Manager (PowerShell)";
|
|
613
|
+
isAvailable() {
|
|
614
|
+
return process.platform === "win32";
|
|
615
|
+
}
|
|
616
|
+
get(service, account) {
|
|
617
|
+
if (process.platform !== "win32") return null;
|
|
618
|
+
try {
|
|
619
|
+
const target = `${service}:${account}`;
|
|
620
|
+
const psScript = `
|
|
621
|
+
Add-Type -TypeDefinition @"
|
|
622
|
+
using System;
|
|
623
|
+
using System.Runtime.InteropServices;
|
|
624
|
+
public class WinCred {
|
|
625
|
+
[DllImport("Advapi32.dll", SetLastError = true, EntryPoint = "CredReadW", CharSet = CharSet.Unicode)]
|
|
626
|
+
public static extern bool CredRead(string target, int type, int reservedFlag, out IntPtr credentialPtr);
|
|
627
|
+
[DllImport("Advapi32.dll", SetLastError = true)]
|
|
628
|
+
public static extern void CredFree([In] IntPtr cred);
|
|
629
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
630
|
+
public struct CREDENTIAL {
|
|
631
|
+
public uint Flags; public uint Type; public string TargetName; public string Comment;
|
|
632
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
633
|
+
public uint CredentialBlobSize; public IntPtr CredentialBlob;
|
|
634
|
+
public uint Persist; public uint AttributeCount; public IntPtr Attributes;
|
|
635
|
+
public string TargetAlias; public string UserName;
|
|
636
|
+
}
|
|
637
|
+
public static string Read(string target) {
|
|
638
|
+
IntPtr ptr;
|
|
639
|
+
if (!CredRead(target, 1, 0, out ptr)) return null;
|
|
640
|
+
var cred = (CREDENTIAL)Marshal.PtrToStructure(ptr, typeof(CREDENTIAL));
|
|
641
|
+
byte[] bytes = new byte[cred.CredentialBlobSize];
|
|
642
|
+
Marshal.Copy(cred.CredentialBlob, bytes, 0, (int)cred.CredentialBlobSize);
|
|
643
|
+
CredFree(ptr);
|
|
644
|
+
return System.Text.Encoding.Unicode.GetString(bytes);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
"@
|
|
648
|
+
[WinCred]::Read("${target.replace(/"/g, '`"')}")
|
|
649
|
+
`;
|
|
650
|
+
const output = execFileSync(
|
|
651
|
+
"powershell.exe",
|
|
652
|
+
["-NoProfile", "-NonInteractive", "-Command", psScript],
|
|
653
|
+
{ stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }
|
|
654
|
+
);
|
|
655
|
+
const res = output.trim();
|
|
656
|
+
return res.length > 0 ? res : null;
|
|
657
|
+
} catch {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
set(service, account, secret) {
|
|
662
|
+
if (process.platform !== "win32") return false;
|
|
663
|
+
try {
|
|
664
|
+
const target = `${service}:${account}`;
|
|
665
|
+
const psScript = `
|
|
666
|
+
Add-Type -TypeDefinition @"
|
|
667
|
+
using System;
|
|
668
|
+
using System.Runtime.InteropServices;
|
|
669
|
+
public class WinCredWrite {
|
|
670
|
+
[DllImport("Advapi32.dll", SetLastError = true, EntryPoint = "CredWriteW", CharSet = CharSet.Unicode)]
|
|
671
|
+
public static extern bool CredWrite([In] ref CREDENTIAL userCredential, [In] uint flags);
|
|
672
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
673
|
+
public struct CREDENTIAL {
|
|
674
|
+
public uint Flags; public uint Type; public string TargetName; public string Comment;
|
|
675
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
676
|
+
public uint CredentialBlobSize; public IntPtr CredentialBlob;
|
|
677
|
+
public uint Persist; public uint AttributeCount; public IntPtr Attributes;
|
|
678
|
+
public string TargetAlias; public string UserName;
|
|
679
|
+
}
|
|
680
|
+
public static bool Write(string target, string user, string secret) {
|
|
681
|
+
byte[] bytes = System.Text.Encoding.Unicode.GetBytes(secret);
|
|
682
|
+
IntPtr bPtr = Marshal.AllocHGlobal(bytes.Length);
|
|
683
|
+
Marshal.Copy(bytes, 0, bPtr, bytes.Length);
|
|
684
|
+
var c = new CREDENTIAL {
|
|
685
|
+
Flags = 0, Type = 1, TargetName = target, UserName = user,
|
|
686
|
+
CredentialBlobSize = (uint)bytes.Length, CredentialBlob = bPtr, Persist = 2
|
|
687
|
+
};
|
|
688
|
+
bool ok = CredWrite(ref c, 0);
|
|
689
|
+
Marshal.FreeHGlobal(bPtr);
|
|
690
|
+
return ok;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
"@
|
|
694
|
+
[WinCredWrite]::Write("${target.replace(/"/g, '`"')}", "${account.replace(/"/g, '`"')}", "${secret.replace(/"/g, '`"')}")
|
|
695
|
+
`;
|
|
696
|
+
execFileSync(
|
|
697
|
+
"powershell.exe",
|
|
698
|
+
["-NoProfile", "-NonInteractive", "-Command", psScript],
|
|
699
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
700
|
+
);
|
|
701
|
+
return true;
|
|
702
|
+
} catch {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
delete(service, account) {
|
|
707
|
+
if (process.platform !== "win32") return false;
|
|
708
|
+
try {
|
|
709
|
+
const target = `${service}:${account}`;
|
|
710
|
+
const psScript = `
|
|
711
|
+
Add-Type -TypeDefinition @"
|
|
712
|
+
using System;
|
|
713
|
+
using System.Runtime.InteropServices;
|
|
714
|
+
public class WinCredDel {
|
|
715
|
+
[DllImport("Advapi32.dll", SetLastError = true, EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode)]
|
|
716
|
+
public static extern bool CredDelete(string target, int type, int reservedFlag);
|
|
717
|
+
public static bool Del(string target) { return CredDelete(target, 1, 0); }
|
|
718
|
+
}
|
|
719
|
+
"@
|
|
720
|
+
[WinCredDel]::Del("${target.replace(/"/g, '`"')}")
|
|
721
|
+
`;
|
|
722
|
+
execFileSync(
|
|
723
|
+
"powershell.exe",
|
|
724
|
+
["-NoProfile", "-NonInteractive", "-Command", psScript],
|
|
725
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
726
|
+
);
|
|
727
|
+
return true;
|
|
728
|
+
} catch {
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
var NativeCredentialStore = class _NativeCredentialStore {
|
|
734
|
+
static instance;
|
|
735
|
+
primaryStore;
|
|
736
|
+
fallbackStore = null;
|
|
737
|
+
constructor() {
|
|
738
|
+
this.primaryStore = new NapiRsKeyringStore();
|
|
739
|
+
if (process.platform === "darwin") {
|
|
740
|
+
this.fallbackStore = new MacosKeychainCliStore();
|
|
741
|
+
} else if (process.platform === "win32") {
|
|
742
|
+
this.fallbackStore = new WindowsCredentialManagerCliStore();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
static getInstance() {
|
|
746
|
+
if (!_NativeCredentialStore.instance) {
|
|
747
|
+
_NativeCredentialStore.instance = new _NativeCredentialStore();
|
|
748
|
+
}
|
|
749
|
+
return _NativeCredentialStore.instance;
|
|
750
|
+
}
|
|
751
|
+
getStoreName() {
|
|
752
|
+
if (process.platform === "darwin") return "macOS Keychain";
|
|
753
|
+
if (process.platform === "win32") return "Windows Credential Manager";
|
|
754
|
+
if (process.platform === "linux") return "Linux Secret Service / Keyring";
|
|
755
|
+
return "OS Credential Store";
|
|
756
|
+
}
|
|
757
|
+
isAvailable() {
|
|
758
|
+
return this.primaryStore.isAvailable() || (this.fallbackStore?.isAvailable() ?? false);
|
|
759
|
+
}
|
|
760
|
+
get(service, account) {
|
|
761
|
+
if (this.primaryStore.isAvailable()) {
|
|
762
|
+
try {
|
|
763
|
+
const val = this.primaryStore.get(service, account);
|
|
764
|
+
if (val) return val;
|
|
765
|
+
} catch {
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (this.fallbackStore && this.fallbackStore.isAvailable()) {
|
|
769
|
+
try {
|
|
770
|
+
return this.fallbackStore.get(service, account);
|
|
771
|
+
} catch {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
set(service, account, secret) {
|
|
778
|
+
let succeeded = false;
|
|
779
|
+
if (this.primaryStore.isAvailable()) {
|
|
780
|
+
try {
|
|
781
|
+
succeeded = this.primaryStore.set(service, account, secret);
|
|
782
|
+
} catch {
|
|
783
|
+
succeeded = false;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (!succeeded && this.fallbackStore && this.fallbackStore.isAvailable()) {
|
|
787
|
+
try {
|
|
788
|
+
succeeded = this.fallbackStore.set(service, account, secret);
|
|
789
|
+
} catch {
|
|
790
|
+
succeeded = false;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return succeeded;
|
|
794
|
+
}
|
|
795
|
+
delete(service, account) {
|
|
796
|
+
let deleted = false;
|
|
797
|
+
if (this.primaryStore.isAvailable()) {
|
|
798
|
+
try {
|
|
799
|
+
deleted = this.primaryStore.delete(service, account) || deleted;
|
|
800
|
+
} catch {
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (this.fallbackStore && this.fallbackStore.isAvailable()) {
|
|
804
|
+
try {
|
|
805
|
+
deleted = this.fallbackStore.delete(service, account) || deleted;
|
|
806
|
+
} catch {
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return deleted;
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
|
|
438
813
|
// src/security/credentials.ts
|
|
439
814
|
import fs2 from "fs";
|
|
440
|
-
import
|
|
815
|
+
import path3 from "path";
|
|
441
816
|
import os2 from "os";
|
|
442
817
|
import crypto from "crypto";
|
|
818
|
+
import readline from "readline";
|
|
819
|
+
var SERVICE_NAME = "codelantern";
|
|
443
820
|
var PROVIDER_ENV_VARS = {
|
|
444
821
|
gemini: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
|
445
822
|
openai: ["OPENAI_API_KEY"],
|
|
@@ -447,17 +824,20 @@ var PROVIDER_ENV_VARS = {
|
|
|
447
824
|
openrouter: ["OPENROUTER_API_KEY"],
|
|
448
825
|
custom: ["CUSTOM_API_KEY", "OPENAI_API_KEY"]
|
|
449
826
|
};
|
|
450
|
-
function
|
|
451
|
-
return
|
|
827
|
+
function getLegacyCredentialsFilePath() {
|
|
828
|
+
return path3.join(getLanternHomeDir(), "credentials.json");
|
|
829
|
+
}
|
|
830
|
+
function getStoreName() {
|
|
831
|
+
return NativeCredentialStore.getInstance().getStoreName();
|
|
452
832
|
}
|
|
453
|
-
function
|
|
833
|
+
function getLegacyMachineKey() {
|
|
454
834
|
const salt = `codelantern_${os2.hostname()}_${os2.userInfo().username}`;
|
|
455
835
|
const secret = `${os2.homedir()}_${process.platform}_${os2.arch()}`;
|
|
456
836
|
return crypto.scryptSync(secret, salt, 32);
|
|
457
837
|
}
|
|
458
838
|
function encryptSecret(plainText) {
|
|
459
839
|
const iv = crypto.randomBytes(12);
|
|
460
|
-
const key =
|
|
840
|
+
const key = getLegacyMachineKey();
|
|
461
841
|
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
462
842
|
let encrypted = cipher.update(plainText, "utf8", "hex");
|
|
463
843
|
encrypted += cipher.final("hex");
|
|
@@ -471,7 +851,7 @@ function decryptSecret(encryptedPayload) {
|
|
|
471
851
|
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
472
852
|
const iv = Buffer.from(ivHex, "hex");
|
|
473
853
|
const authTag = Buffer.from(authTagHex, "hex");
|
|
474
|
-
const key =
|
|
854
|
+
const key = getLegacyMachineKey();
|
|
475
855
|
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
476
856
|
decipher.setAuthTag(authTag);
|
|
477
857
|
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
@@ -487,85 +867,206 @@ function maskApiKey(key) {
|
|
|
487
867
|
if (clean.length <= 8) return "********";
|
|
488
868
|
return `${clean.slice(0, 4)}...${clean.slice(-4)}`;
|
|
489
869
|
}
|
|
870
|
+
var migrationPerformed = false;
|
|
871
|
+
function migrateLegacyCredentials(force = false) {
|
|
872
|
+
if (migrationPerformed && !force) {
|
|
873
|
+
return { attempted: false, migratedProviders: [], failedProviders: [] };
|
|
874
|
+
}
|
|
875
|
+
migrationPerformed = true;
|
|
876
|
+
const legacyPath = getLegacyCredentialsFilePath();
|
|
877
|
+
if (!fs2.existsSync(legacyPath)) {
|
|
878
|
+
return { attempted: false, migratedProviders: [], failedProviders: [] };
|
|
879
|
+
}
|
|
880
|
+
const store = NativeCredentialStore.getInstance();
|
|
881
|
+
if (!store.isAvailable()) {
|
|
882
|
+
return { attempted: false, migratedProviders: [], failedProviders: [] };
|
|
883
|
+
}
|
|
884
|
+
let legacyData = {};
|
|
885
|
+
try {
|
|
886
|
+
const raw = fs2.readFileSync(legacyPath, "utf8");
|
|
887
|
+
legacyData = JSON.parse(raw);
|
|
888
|
+
} catch {
|
|
889
|
+
return { attempted: true, migratedProviders: [], failedProviders: [] };
|
|
890
|
+
}
|
|
891
|
+
const providers = Object.keys(legacyData);
|
|
892
|
+
if (providers.length === 0) {
|
|
893
|
+
try {
|
|
894
|
+
fs2.rmSync(legacyPath, { force: true });
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
return { attempted: true, migratedProviders: [], failedProviders: [] };
|
|
898
|
+
}
|
|
899
|
+
const migratedProviders = [];
|
|
900
|
+
const failedProviders = [];
|
|
901
|
+
for (const provider of providers) {
|
|
902
|
+
const encrypted = legacyData[provider];
|
|
903
|
+
if (!encrypted) continue;
|
|
904
|
+
const decryptedKey = decryptSecret(encrypted);
|
|
905
|
+
if (!decryptedKey || decryptedKey.trim().length === 0) {
|
|
906
|
+
failedProviders.push(provider);
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
const saved = store.set(SERVICE_NAME, provider.toLowerCase(), decryptedKey.trim());
|
|
910
|
+
if (!saved) {
|
|
911
|
+
failedProviders.push(provider);
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const verified = store.get(SERVICE_NAME, provider.toLowerCase());
|
|
915
|
+
if (verified !== decryptedKey.trim()) {
|
|
916
|
+
failedProviders.push(provider);
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
migratedProviders.push(provider);
|
|
920
|
+
delete legacyData[provider];
|
|
921
|
+
try {
|
|
922
|
+
fs2.writeFileSync(legacyPath, JSON.stringify(legacyData, null, 2), {
|
|
923
|
+
encoding: "utf8",
|
|
924
|
+
mode: 384
|
|
925
|
+
});
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (Object.keys(legacyData).length === 0) {
|
|
930
|
+
try {
|
|
931
|
+
fs2.rmSync(legacyPath, { force: true });
|
|
932
|
+
} catch {
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
attempted: true,
|
|
937
|
+
migratedProviders,
|
|
938
|
+
failedProviders
|
|
939
|
+
};
|
|
940
|
+
}
|
|
490
941
|
function getApiKey(provider) {
|
|
491
942
|
const norm = provider.toLowerCase();
|
|
492
943
|
const envVarNames = PROVIDER_ENV_VARS[norm] || [`${norm.toUpperCase()}_API_KEY`];
|
|
493
944
|
for (const envVar of envVarNames) {
|
|
494
945
|
const val = process.env[envVar];
|
|
495
946
|
if (val && val.trim().length > 0) {
|
|
496
|
-
return {
|
|
947
|
+
return {
|
|
948
|
+
key: val.trim(),
|
|
949
|
+
source: "env",
|
|
950
|
+
sourceDescription: `env: ${envVar}`
|
|
951
|
+
};
|
|
497
952
|
}
|
|
498
953
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
} catch {
|
|
511
|
-
}
|
|
954
|
+
migrateLegacyCredentials();
|
|
955
|
+
const store = NativeCredentialStore.getInstance();
|
|
956
|
+
const storedKey = store.get(SERVICE_NAME, norm);
|
|
957
|
+
if (storedKey && storedKey.trim().length > 0) {
|
|
958
|
+
return {
|
|
959
|
+
key: storedKey.trim(),
|
|
960
|
+
source: "store",
|
|
961
|
+
sourceDescription: store.getStoreName()
|
|
962
|
+
};
|
|
512
963
|
}
|
|
513
964
|
return { key: null };
|
|
514
965
|
}
|
|
515
966
|
function setApiKey(provider, apiKey) {
|
|
516
967
|
const norm = provider.toLowerCase();
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
968
|
+
const cleanKey = apiKey.trim();
|
|
969
|
+
if (!cleanKey) {
|
|
970
|
+
throw new Error("API key cannot be empty.");
|
|
971
|
+
}
|
|
972
|
+
const store = NativeCredentialStore.getInstance();
|
|
973
|
+
if (!store.isAvailable()) {
|
|
974
|
+
throw new Error(
|
|
975
|
+
`Secure OS credential store (${store.getStoreName()}) is unavailable on this system.
|
|
976
|
+
Please set your key using an environment variable:
|
|
977
|
+
export ${norm.toUpperCase()}_API_KEY="your-api-key"`
|
|
978
|
+
);
|
|
526
979
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
console.error(`Warning: Failed to save credentials: ${err.message}`);
|
|
980
|
+
const ok = store.set(SERVICE_NAME, norm, cleanKey);
|
|
981
|
+
if (!ok) {
|
|
982
|
+
throw new Error(`Failed to write API key for '${provider}' to ${store.getStoreName()}.`);
|
|
983
|
+
}
|
|
984
|
+
const check = store.get(SERVICE_NAME, norm);
|
|
985
|
+
if (check !== cleanKey) {
|
|
986
|
+
throw new Error(`Verification error: Stored key for '${provider}' could not be read back from ${store.getStoreName()}.`);
|
|
535
987
|
}
|
|
536
988
|
}
|
|
537
989
|
function removeApiKey(provider) {
|
|
538
990
|
const norm = provider.toLowerCase();
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
991
|
+
const store = NativeCredentialStore.getInstance();
|
|
992
|
+
const deletedFromStore = store.delete(SERVICE_NAME, norm);
|
|
993
|
+
const legacyPath = getLegacyCredentialsFilePath();
|
|
994
|
+
if (fs2.existsSync(legacyPath)) {
|
|
995
|
+
try {
|
|
996
|
+
const raw = fs2.readFileSync(legacyPath, "utf8");
|
|
997
|
+
const data = JSON.parse(raw);
|
|
998
|
+
if (data[norm]) {
|
|
999
|
+
delete data[norm];
|
|
1000
|
+
fs2.writeFileSync(legacyPath, JSON.stringify(data, null, 2), {
|
|
1001
|
+
encoding: "utf8",
|
|
1002
|
+
mode: 384
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
} catch {
|
|
551
1006
|
}
|
|
552
|
-
} catch {
|
|
553
1007
|
}
|
|
554
|
-
return
|
|
1008
|
+
return deletedFromStore;
|
|
555
1009
|
}
|
|
556
1010
|
function listConfiguredKeys() {
|
|
1011
|
+
migrateLegacyCredentials();
|
|
557
1012
|
const providers = ["gemini", "openai", "anthropic", "openrouter", "custom"];
|
|
558
1013
|
const result = {};
|
|
559
1014
|
for (const p of providers) {
|
|
560
|
-
const { key, source } = getApiKey(p);
|
|
1015
|
+
const { key, source, sourceDescription } = getApiKey(p);
|
|
561
1016
|
result[p] = {
|
|
562
1017
|
configured: Boolean(key),
|
|
563
1018
|
source,
|
|
1019
|
+
sourceDescription: sourceDescription ? `[${sourceDescription}]` : "none",
|
|
564
1020
|
maskedKey: key ? maskApiKey(key) : "(not configured)"
|
|
565
1021
|
};
|
|
566
1022
|
}
|
|
567
1023
|
return result;
|
|
568
1024
|
}
|
|
1025
|
+
async function promptMaskedApiKey(query) {
|
|
1026
|
+
const promptText = query.endsWith(": ") ? query : `${query}: `;
|
|
1027
|
+
if (!process.stdin.isTTY || !process.stdin.setRawMode) {
|
|
1028
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1029
|
+
return new Promise((resolve) => {
|
|
1030
|
+
rl.question(promptText, (ans) => {
|
|
1031
|
+
rl.close();
|
|
1032
|
+
resolve(ans.trim());
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
return new Promise((resolve) => {
|
|
1037
|
+
process.stdout.write(promptText);
|
|
1038
|
+
const wasRaw = process.stdin.isRaw;
|
|
1039
|
+
process.stdin.setRawMode(true);
|
|
1040
|
+
process.stdin.resume();
|
|
1041
|
+
process.stdin.setEncoding("utf8");
|
|
1042
|
+
let key = "";
|
|
1043
|
+
const onData = (chunk) => {
|
|
1044
|
+
for (const char of chunk) {
|
|
1045
|
+
if (char === "") {
|
|
1046
|
+
process.stdin.setRawMode(wasRaw ?? false);
|
|
1047
|
+
process.stdin.removeListener("data", onData);
|
|
1048
|
+
process.stdout.write("\n");
|
|
1049
|
+
process.exit(130);
|
|
1050
|
+
} else if (char === "\r" || char === "\n") {
|
|
1051
|
+
process.stdin.setRawMode(wasRaw ?? false);
|
|
1052
|
+
process.stdin.removeListener("data", onData);
|
|
1053
|
+
process.stdout.write("\n");
|
|
1054
|
+
resolve(key.trim());
|
|
1055
|
+
return;
|
|
1056
|
+
} else if (char === "\b" || char === "\x7F") {
|
|
1057
|
+
if (key.length > 0) {
|
|
1058
|
+
key = key.slice(0, -1);
|
|
1059
|
+
process.stdout.write("\b \b");
|
|
1060
|
+
}
|
|
1061
|
+
} else if (char >= " ") {
|
|
1062
|
+
key += char;
|
|
1063
|
+
process.stdout.write("*");
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
process.stdin.on("data", onData);
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
569
1070
|
|
|
570
1071
|
// src/provider/gemini.ts
|
|
571
1072
|
import { GoogleGenAI, Type } from "@google/genai";
|
|
@@ -586,12 +1087,15 @@ var GeminiProvider = class {
|
|
|
586
1087
|
const key = this.getApiKey();
|
|
587
1088
|
return key ? maskApiKey(key) : "(not configured)";
|
|
588
1089
|
}
|
|
589
|
-
getClient() {
|
|
1090
|
+
getClient(httpOptions) {
|
|
590
1091
|
const key = this.getApiKey();
|
|
591
1092
|
if (!key) {
|
|
592
1093
|
throw new MissingApiKeyError("gemini", "GEMINI_API_KEY");
|
|
593
1094
|
}
|
|
594
|
-
return new GoogleGenAI({
|
|
1095
|
+
return new GoogleGenAI({
|
|
1096
|
+
apiKey: key,
|
|
1097
|
+
httpOptions: httpOptions || { timeout: 3e4 }
|
|
1098
|
+
});
|
|
595
1099
|
}
|
|
596
1100
|
getCapabilities(modelId) {
|
|
597
1101
|
const isPro = modelId.includes("pro");
|
|
@@ -605,14 +1109,29 @@ var GeminiProvider = class {
|
|
|
605
1109
|
};
|
|
606
1110
|
}
|
|
607
1111
|
async testConnection() {
|
|
1112
|
+
if (!this.isConfigured()) {
|
|
1113
|
+
return { success: false, latencyMs: 0, error: `Missing API key for ${this.displayName}` };
|
|
1114
|
+
}
|
|
608
1115
|
const startTime = Date.now();
|
|
609
1116
|
try {
|
|
610
|
-
const client = this.getClient();
|
|
1117
|
+
const client = this.getClient({ timeout: 1e4 });
|
|
611
1118
|
const testModel = this.defaultModel;
|
|
612
|
-
|
|
1119
|
+
const abortController = new AbortController();
|
|
1120
|
+
const countPromise = client.models.countTokens({
|
|
613
1121
|
model: testModel,
|
|
614
|
-
contents: "test"
|
|
1122
|
+
contents: "test",
|
|
1123
|
+
config: {
|
|
1124
|
+
abortSignal: abortController.signal,
|
|
1125
|
+
httpOptions: { timeout: 1e4 }
|
|
1126
|
+
}
|
|
615
1127
|
});
|
|
1128
|
+
await withTimeoutRace(
|
|
1129
|
+
countPromise,
|
|
1130
|
+
1e4,
|
|
1131
|
+
`Connection check for ${this.displayName}`,
|
|
1132
|
+
abortController.signal,
|
|
1133
|
+
() => abortController.abort()
|
|
1134
|
+
);
|
|
616
1135
|
return {
|
|
617
1136
|
success: true,
|
|
618
1137
|
latencyMs: Date.now() - startTime,
|
|
@@ -622,7 +1141,7 @@ var GeminiProvider = class {
|
|
|
622
1141
|
return {
|
|
623
1142
|
success: false,
|
|
624
1143
|
latencyMs: Date.now() - startTime,
|
|
625
|
-
error: err.message || String(err)
|
|
1144
|
+
error: sanitizeSecrets(err.message || String(err))
|
|
626
1145
|
};
|
|
627
1146
|
}
|
|
628
1147
|
}
|
|
@@ -631,8 +1150,21 @@ var GeminiProvider = class {
|
|
|
631
1150
|
return this.cachedModels;
|
|
632
1151
|
}
|
|
633
1152
|
try {
|
|
634
|
-
const client = this.getClient();
|
|
635
|
-
const
|
|
1153
|
+
const client = this.getClient({ timeout: 1e4 });
|
|
1154
|
+
const abortController = new AbortController();
|
|
1155
|
+
const listPromise = client.models.list({
|
|
1156
|
+
config: {
|
|
1157
|
+
abortSignal: abortController.signal,
|
|
1158
|
+
httpOptions: { timeout: 1e4 }
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
const pagedList = await withTimeoutRace(
|
|
1162
|
+
listPromise,
|
|
1163
|
+
1e4,
|
|
1164
|
+
`Model listing for ${this.displayName}`,
|
|
1165
|
+
abortController.signal,
|
|
1166
|
+
() => abortController.abort()
|
|
1167
|
+
);
|
|
636
1168
|
const models = [];
|
|
637
1169
|
for await (const m of pagedList) {
|
|
638
1170
|
const rawName = m.name || "";
|
|
@@ -679,12 +1211,26 @@ var GeminiProvider = class {
|
|
|
679
1211
|
totalMs: 0,
|
|
680
1212
|
toolExecutionMs: {}
|
|
681
1213
|
};
|
|
682
|
-
|
|
1214
|
+
if (params.callbacks.onStateChange) {
|
|
1215
|
+
params.callbacks.onStateChange("resolving_credentials");
|
|
1216
|
+
}
|
|
1217
|
+
if (params.signal?.aborted) {
|
|
1218
|
+
throw params.signal.reason || new CancelledError();
|
|
1219
|
+
}
|
|
1220
|
+
const key = this.getApiKey();
|
|
1221
|
+
if (!key) {
|
|
1222
|
+
throw new MissingApiKeyError(this.id, "GEMINI_API_KEY");
|
|
1223
|
+
}
|
|
683
1224
|
const contents = this.convertHistoryToContents(params.history);
|
|
684
1225
|
const functionDeclarations = this.convertToolsToDeclarations(params.tools);
|
|
1226
|
+
const timeoutMs = params.timeoutMs || 6e4;
|
|
1227
|
+
const firstTokenTimeoutMs = params.firstTokenTimeoutMs || 3e4;
|
|
1228
|
+
const connectTimeoutMs = Math.min(timeoutMs, 15e3);
|
|
1229
|
+
const client = this.getClient({ timeout: connectTimeoutMs });
|
|
685
1230
|
const config = {
|
|
686
1231
|
systemInstruction: params.systemInstruction,
|
|
687
|
-
temperature: params.temperature ?? (params.lowLatency ? 0.1 : 0.2)
|
|
1232
|
+
temperature: params.temperature ?? (params.lowLatency ? 0.1 : 0.2),
|
|
1233
|
+
httpOptions: { timeout: connectTimeoutMs }
|
|
688
1234
|
};
|
|
689
1235
|
if (params.maxOutputTokens) {
|
|
690
1236
|
config.maxOutputTokens = params.maxOutputTokens;
|
|
@@ -699,24 +1245,33 @@ var GeminiProvider = class {
|
|
|
699
1245
|
if (toolList.length > 0) {
|
|
700
1246
|
config.tools = toolList;
|
|
701
1247
|
}
|
|
702
|
-
const timeoutMs = params.timeoutMs || 6e4;
|
|
703
|
-
const firstTokenTimeoutMs = params.firstTokenTimeoutMs || 3e4;
|
|
704
1248
|
let attempts = 0;
|
|
705
1249
|
const maxAttempts = 3;
|
|
706
1250
|
let lastError = null;
|
|
707
1251
|
while (attempts < maxAttempts) {
|
|
708
1252
|
attempts++;
|
|
709
1253
|
const connectStart = Date.now();
|
|
1254
|
+
if (params.signal?.aborted) {
|
|
1255
|
+
throw params.signal.reason || new CancelledError();
|
|
1256
|
+
}
|
|
710
1257
|
if (params.callbacks.onStateChange) {
|
|
711
1258
|
params.callbacks.onStateChange(attempts > 1 ? `retrying:${attempts}` : "connecting");
|
|
712
1259
|
}
|
|
713
1260
|
const { controller, cleanup } = createLinkedAbortController(params.signal, timeoutMs);
|
|
1261
|
+
config.abortSignal = controller.signal;
|
|
714
1262
|
try {
|
|
715
|
-
const
|
|
1263
|
+
const connectionPromise = client.models.generateContentStream({
|
|
716
1264
|
model: params.model,
|
|
717
1265
|
contents,
|
|
718
1266
|
config
|
|
719
1267
|
});
|
|
1268
|
+
const responseStream = await withTimeoutRace(
|
|
1269
|
+
connectionPromise,
|
|
1270
|
+
connectTimeoutMs,
|
|
1271
|
+
`Connection to ${this.displayName} (${params.model})`,
|
|
1272
|
+
controller.signal,
|
|
1273
|
+
() => controller.abort(new TimeoutError(connectTimeoutMs, `Connection to ${this.displayName} (${params.model})`))
|
|
1274
|
+
);
|
|
720
1275
|
timing.connectMs = Date.now() - connectStart;
|
|
721
1276
|
if (params.callbacks.onStateChange) {
|
|
722
1277
|
params.callbacks.onStateChange("waiting_first_token");
|
|
@@ -743,6 +1298,9 @@ var GeminiProvider = class {
|
|
|
743
1298
|
);
|
|
744
1299
|
const streamStart = Date.now();
|
|
745
1300
|
for await (const chunk of wrappedStream) {
|
|
1301
|
+
if (controller.signal.aborted) {
|
|
1302
|
+
throw controller.signal.reason || new CancelledError();
|
|
1303
|
+
}
|
|
746
1304
|
if (chunk.text) {
|
|
747
1305
|
fullText += chunk.text;
|
|
748
1306
|
if (params.callbacks.onChunk) {
|
|
@@ -757,6 +1315,9 @@ var GeminiProvider = class {
|
|
|
757
1315
|
args: fc.args || {}
|
|
758
1316
|
});
|
|
759
1317
|
}
|
|
1318
|
+
if (params.callbacks.onStateChange) {
|
|
1319
|
+
params.callbacks.onStateChange("streaming");
|
|
1320
|
+
}
|
|
760
1321
|
}
|
|
761
1322
|
if (chunk.usageMetadata) {
|
|
762
1323
|
usage.promptTokens = chunk.usageMetadata.promptTokenCount ?? usage.promptTokens;
|
|
@@ -773,6 +1334,9 @@ var GeminiProvider = class {
|
|
|
773
1334
|
params.callbacks.onToolCalls(collectedToolCalls);
|
|
774
1335
|
}
|
|
775
1336
|
cleanup();
|
|
1337
|
+
if (!fullText && collectedToolCalls.length === 0) {
|
|
1338
|
+
fullText = `[Received empty response from ${this.displayName} (${params.model})]`;
|
|
1339
|
+
}
|
|
776
1340
|
return {
|
|
777
1341
|
text: fullText,
|
|
778
1342
|
toolCalls: collectedToolCalls,
|
|
@@ -782,19 +1346,22 @@ var GeminiProvider = class {
|
|
|
782
1346
|
} catch (err) {
|
|
783
1347
|
cleanup();
|
|
784
1348
|
lastError = err;
|
|
785
|
-
if (params.signal?.aborted) {
|
|
1349
|
+
if (params.signal?.aborted || err instanceof CancelledError) {
|
|
1350
|
+
throw err instanceof CancelledError ? err : params.signal?.reason || new CancelledError();
|
|
1351
|
+
}
|
|
1352
|
+
if (err instanceof TimeoutError || err instanceof FirstTokenTimeoutError) {
|
|
786
1353
|
throw err;
|
|
787
1354
|
}
|
|
788
1355
|
const msg = String(err?.message || err);
|
|
789
|
-
if (msg.includes("API_KEY_INVALID") || msg.includes("401") || msg.includes("403")) {
|
|
790
|
-
throw new
|
|
1356
|
+
if (msg.includes("API_KEY_INVALID") || msg.includes("API key not valid") || msg.includes("401") || msg.includes("403") || msg.includes("PERMISSION_DENIED")) {
|
|
1357
|
+
throw new AuthenticationError(`Authentication failed for ${this.displayName}: Invalid or restricted API key. Please run 'lantern keys set ${this.id}' or verify GEMINI_API_KEY.`);
|
|
791
1358
|
}
|
|
792
|
-
if (msg.includes("NOT_FOUND") || msg.includes("models/")) {
|
|
1359
|
+
if (msg.includes("NOT_FOUND") || msg.includes("not found") || msg.includes("is not supported") || msg.includes("models/")) {
|
|
793
1360
|
throw new ModelNotFoundError(params.model, this.displayName);
|
|
794
1361
|
}
|
|
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) {
|
|
1362
|
+
const isRateLimit = msg.includes("429") || msg.includes("RESOURCE_EXHAUSTED") || msg.includes("quota");
|
|
1363
|
+
const isTransient = msg.includes("503") || msg.includes("500") || msg.includes("overloaded") || msg.includes("ECONNRESET") || msg.includes("ETIMEDOUT") || msg.includes("socket hang up") || msg.includes("fetch failed");
|
|
1364
|
+
if ((isRateLimit || isTransient) && attempts < maxAttempts && !params.signal?.aborted) {
|
|
798
1365
|
const backoff = Math.min(1e3 * Math.pow(1.5, attempts), 3e3);
|
|
799
1366
|
if (params.callbacks.onStateChange) {
|
|
800
1367
|
params.callbacks.onStateChange(`retrying:${attempts}`);
|
|
@@ -803,12 +1370,12 @@ var GeminiProvider = class {
|
|
|
803
1370
|
continue;
|
|
804
1371
|
}
|
|
805
1372
|
if (isRateLimit) {
|
|
806
|
-
throw new RateLimitError(
|
|
1373
|
+
throw new RateLimitError(`Rate limit or quota reached for ${this.displayName}. Please wait a few seconds before retrying.`);
|
|
807
1374
|
}
|
|
808
|
-
throw new LanternProviderError(`Gemini request failed: ${msg}`, void 0, this.id);
|
|
1375
|
+
throw new LanternProviderError(`Gemini request failed: ${sanitizeSecrets(msg)}`, void 0, this.id);
|
|
809
1376
|
}
|
|
810
1377
|
}
|
|
811
|
-
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
1378
|
+
throw lastError instanceof Error ? lastError : new Error(sanitizeSecrets(String(lastError)));
|
|
812
1379
|
}
|
|
813
1380
|
convertHistoryToContents(turns) {
|
|
814
1381
|
const contents = [];
|
|
@@ -1092,18 +1659,25 @@ var OpenAIProvider = class {
|
|
|
1092
1659
|
}
|
|
1093
1660
|
const timeoutMs = params.timeoutMs || 6e4;
|
|
1094
1661
|
const firstTokenTimeoutMs = params.firstTokenTimeoutMs || 3e4;
|
|
1662
|
+
const connectTimeoutMs = Math.min(timeoutMs, 15e3);
|
|
1663
|
+
if (params.callbacks.onStateChange) {
|
|
1664
|
+
params.callbacks.onStateChange("resolving_credentials");
|
|
1665
|
+
}
|
|
1095
1666
|
let attempts = 0;
|
|
1096
1667
|
const maxAttempts = 3;
|
|
1097
1668
|
let lastError = null;
|
|
1098
1669
|
while (attempts < maxAttempts) {
|
|
1099
1670
|
attempts++;
|
|
1100
1671
|
const connectStart = Date.now();
|
|
1672
|
+
if (params.signal?.aborted) {
|
|
1673
|
+
throw params.signal.reason || new CancelledError();
|
|
1674
|
+
}
|
|
1101
1675
|
if (params.callbacks.onStateChange) {
|
|
1102
1676
|
params.callbacks.onStateChange(attempts > 1 ? `retrying:${attempts}` : "connecting");
|
|
1103
1677
|
}
|
|
1104
1678
|
const { controller, cleanup } = createLinkedAbortController(params.signal, timeoutMs);
|
|
1105
1679
|
try {
|
|
1106
|
-
const
|
|
1680
|
+
const fetchPromise = fetch(`${this.baseUrl}/chat/completions`, {
|
|
1107
1681
|
method: "POST",
|
|
1108
1682
|
headers: {
|
|
1109
1683
|
"Content-Type": "application/json",
|
|
@@ -1113,6 +1687,13 @@ var OpenAIProvider = class {
|
|
|
1113
1687
|
body: JSON.stringify(payload),
|
|
1114
1688
|
signal: controller.signal
|
|
1115
1689
|
});
|
|
1690
|
+
const response = await withTimeoutRace(
|
|
1691
|
+
fetchPromise,
|
|
1692
|
+
connectTimeoutMs,
|
|
1693
|
+
`Connection to ${this.displayName} (${params.model})`,
|
|
1694
|
+
controller.signal,
|
|
1695
|
+
() => controller.abort(new TimeoutError(connectTimeoutMs, `Connection to ${this.displayName} (${params.model})`))
|
|
1696
|
+
);
|
|
1116
1697
|
timing.connectMs = Date.now() - connectStart;
|
|
1117
1698
|
if (!response.ok) {
|
|
1118
1699
|
const errorBody = await response.text();
|
|
@@ -1488,18 +2069,25 @@ var AnthropicProvider = class {
|
|
|
1488
2069
|
}
|
|
1489
2070
|
const timeoutMs = params.timeoutMs || 6e4;
|
|
1490
2071
|
const firstTokenTimeoutMs = params.firstTokenTimeoutMs || 3e4;
|
|
2072
|
+
const connectTimeoutMs = Math.min(timeoutMs, 15e3);
|
|
2073
|
+
if (params.callbacks.onStateChange) {
|
|
2074
|
+
params.callbacks.onStateChange("resolving_credentials");
|
|
2075
|
+
}
|
|
1491
2076
|
let attempts = 0;
|
|
1492
2077
|
const maxAttempts = 3;
|
|
1493
2078
|
let lastError = null;
|
|
1494
2079
|
while (attempts < maxAttempts) {
|
|
1495
2080
|
attempts++;
|
|
1496
2081
|
const connectStart = Date.now();
|
|
2082
|
+
if (params.signal?.aborted) {
|
|
2083
|
+
throw params.signal.reason || new CancelledError();
|
|
2084
|
+
}
|
|
1497
2085
|
if (params.callbacks.onStateChange) {
|
|
1498
2086
|
params.callbacks.onStateChange(attempts > 1 ? `retrying:${attempts}` : "connecting");
|
|
1499
2087
|
}
|
|
1500
2088
|
const { controller, cleanup } = createLinkedAbortController(params.signal, timeoutMs);
|
|
1501
2089
|
try {
|
|
1502
|
-
const
|
|
2090
|
+
const fetchPromise = fetch(`${this.baseUrl}/messages`, {
|
|
1503
2091
|
method: "POST",
|
|
1504
2092
|
headers: {
|
|
1505
2093
|
"Content-Type": "application/json",
|
|
@@ -1509,6 +2097,13 @@ var AnthropicProvider = class {
|
|
|
1509
2097
|
body: JSON.stringify(payload),
|
|
1510
2098
|
signal: controller.signal
|
|
1511
2099
|
});
|
|
2100
|
+
const response = await withTimeoutRace(
|
|
2101
|
+
fetchPromise,
|
|
2102
|
+
connectTimeoutMs,
|
|
2103
|
+
`Connection to ${this.displayName} (${params.model})`,
|
|
2104
|
+
controller.signal,
|
|
2105
|
+
() => controller.abort(new TimeoutError(connectTimeoutMs, `Connection to ${this.displayName} (${params.model})`))
|
|
2106
|
+
);
|
|
1512
2107
|
timing.connectMs = Date.now() - connectStart;
|
|
1513
2108
|
if (!response.ok) {
|
|
1514
2109
|
const errorBody = await response.text();
|
|
@@ -1775,18 +2370,29 @@ var OpenRouterProvider = class extends OpenAIProvider {
|
|
|
1775
2370
|
if (res.ok) {
|
|
1776
2371
|
const json = await res.json();
|
|
1777
2372
|
if (Array.isArray(json?.data)) {
|
|
1778
|
-
const models = json.data.slice(0, 100).map((m) =>
|
|
1779
|
-
id: m.id
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
2373
|
+
const models = json.data.slice(0, 100).map((m) => {
|
|
2374
|
+
const hasTools = Array.isArray(m.supported_parameters) ? m.supported_parameters.includes("tools") : !m.id.includes(":free") && !m.id.includes("deepseek-r1");
|
|
2375
|
+
const hasVision = typeof m.architecture?.modality === "string" ? m.architecture.modality.includes("image") : m.id.includes("vision") || m.id.includes("gpt-4o") || m.id.includes("claude-3");
|
|
2376
|
+
const isReasoning = m.id.includes("r1") || m.id.includes("o1") || m.id.includes("o3") || m.id.includes("reason");
|
|
2377
|
+
return {
|
|
2378
|
+
id: m.id,
|
|
2379
|
+
name: m.name || m.id,
|
|
2380
|
+
provider: this.id,
|
|
2381
|
+
description: m.description,
|
|
2382
|
+
contextWindow: m.context_length || 128e3,
|
|
2383
|
+
capabilities: {
|
|
2384
|
+
streaming: true,
|
|
2385
|
+
tools: hasTools,
|
|
2386
|
+
images: hasVision,
|
|
2387
|
+
reasoning: isReasoning,
|
|
2388
|
+
contextWindow: m.context_length || 128e3
|
|
2389
|
+
},
|
|
2390
|
+
pricing: m.pricing ? {
|
|
2391
|
+
promptPer1k: parseFloat(m.pricing.prompt) * 1e3,
|
|
2392
|
+
completionPer1k: parseFloat(m.pricing.completion) * 1e3
|
|
2393
|
+
} : void 0
|
|
2394
|
+
};
|
|
2395
|
+
});
|
|
1790
2396
|
return models;
|
|
1791
2397
|
}
|
|
1792
2398
|
}
|
|
@@ -1912,41 +2518,6 @@ var ProviderRegistry = class {
|
|
|
1912
2518
|
}
|
|
1913
2519
|
};
|
|
1914
2520
|
|
|
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
2521
|
// src/security/sandbox.ts
|
|
1951
2522
|
import path4 from "path";
|
|
1952
2523
|
import fs3 from "fs";
|
|
@@ -3267,7 +3838,7 @@ var McpManager = class {
|
|
|
3267
3838
|
};
|
|
3268
3839
|
|
|
3269
3840
|
// src/approval/approver.ts
|
|
3270
|
-
import
|
|
3841
|
+
import readline2 from "readline";
|
|
3271
3842
|
import pc3 from "picocolors";
|
|
3272
3843
|
var ApprovalEngine = class {
|
|
3273
3844
|
yolo;
|
|
@@ -3331,7 +3902,7 @@ var ApprovalEngine = class {
|
|
|
3331
3902
|
if (!process.stdin.isTTY) {
|
|
3332
3903
|
return Promise.resolve("no");
|
|
3333
3904
|
}
|
|
3334
|
-
const rl =
|
|
3905
|
+
const rl = readline2.createInterface({
|
|
3335
3906
|
input: process.stdin,
|
|
3336
3907
|
output: process.stdout
|
|
3337
3908
|
});
|
|
@@ -3835,11 +4406,14 @@ var TerminalSpinner = class {
|
|
|
3835
4406
|
}
|
|
3836
4407
|
return this;
|
|
3837
4408
|
}
|
|
4409
|
+
resolvingCredentials(provider) {
|
|
4410
|
+
return this.update(`Resolving credentials for ${pc4.cyan(provider)}...`);
|
|
4411
|
+
}
|
|
3838
4412
|
connecting(provider, model) {
|
|
3839
4413
|
return this.update(`Connecting to ${pc4.cyan(provider)} (${pc4.bold(model)})...`);
|
|
3840
4414
|
}
|
|
3841
4415
|
waitingFirstToken(provider, model) {
|
|
3842
|
-
return this.update(`Waiting for first token from ${pc4.cyan(provider)}...`);
|
|
4416
|
+
return this.update(`Waiting for first token from ${pc4.cyan(provider)} (${pc4.bold(model)})...`);
|
|
3843
4417
|
}
|
|
3844
4418
|
receiving(tokensCount) {
|
|
3845
4419
|
const suffix = tokensCount ? ` (${tokensCount} tokens)` : "...";
|
|
@@ -4035,27 +4609,28 @@ var LanternAgent = class {
|
|
|
4035
4609
|
lowLatency: context.lowLatency,
|
|
4036
4610
|
callbacks: {
|
|
4037
4611
|
onStateChange: (state) => {
|
|
4038
|
-
if (state === "
|
|
4612
|
+
if (state === "resolving_credentials") {
|
|
4613
|
+
this.spinner.resolvingCredentials(provider.displayName);
|
|
4614
|
+
} else if (state === "connecting") {
|
|
4039
4615
|
this.spinner.connecting(provider.displayName, context.model);
|
|
4040
4616
|
} else if (state === "waiting_first_token") {
|
|
4041
4617
|
this.spinner.waitingFirstToken(provider.displayName, context.model);
|
|
4042
4618
|
} else if (state.startsWith("retrying:")) {
|
|
4043
4619
|
const attempt = state.split(":")[1];
|
|
4044
4620
|
this.spinner.retrying(parseInt(attempt, 10), 3, 1500);
|
|
4045
|
-
} else if (state === "streaming") {
|
|
4621
|
+
} else if (state === "streaming" || state === "receiving") {
|
|
4046
4622
|
this.spinner.stop();
|
|
4047
4623
|
}
|
|
4048
4624
|
},
|
|
4049
4625
|
onChunk: (chunk) => {
|
|
4050
|
-
|
|
4051
|
-
this.spinner.stop();
|
|
4052
|
-
}
|
|
4626
|
+
this.spinner.stop();
|
|
4053
4627
|
currentStepText += chunk;
|
|
4054
4628
|
if (!context.jsonOutput) {
|
|
4055
4629
|
process.stdout.write(chunk);
|
|
4056
4630
|
}
|
|
4057
4631
|
},
|
|
4058
4632
|
onToolCalls: (calls) => {
|
|
4633
|
+
this.spinner.stop();
|
|
4059
4634
|
currentToolCalls = calls;
|
|
4060
4635
|
},
|
|
4061
4636
|
onUsage: (usage) => {
|
|
@@ -4240,7 +4815,148 @@ function loadAttachments(filePaths) {
|
|
|
4240
4815
|
return filePaths.map((p) => loadAttachment(p));
|
|
4241
4816
|
}
|
|
4242
4817
|
|
|
4818
|
+
// src/provider/modelClassifier.ts
|
|
4819
|
+
var DEFAULT_RECOMMENDED_CODING_MODELS = {
|
|
4820
|
+
gemini: ["gemini-3.8-flash", "gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash"],
|
|
4821
|
+
openai: ["gpt-4o", "gpt-4o-mini", "o3-mini", "gpt-4-turbo"],
|
|
4822
|
+
anthropic: ["claude-3-7-sonnet-20250219", "claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"],
|
|
4823
|
+
openrouter: [
|
|
4824
|
+
"anthropic/claude-3.7-sonnet",
|
|
4825
|
+
"anthropic/claude-3.5-sonnet",
|
|
4826
|
+
"openai/gpt-4o",
|
|
4827
|
+
"google/gemini-2.5-flash",
|
|
4828
|
+
"meta-llama/llama-3.3-70b-instruct",
|
|
4829
|
+
"deepseek/deepseek-chat"
|
|
4830
|
+
]
|
|
4831
|
+
};
|
|
4832
|
+
function classifyModel(modelId, capabilities, description) {
|
|
4833
|
+
const lower = modelId.toLowerCase();
|
|
4834
|
+
const desc = (description || "").toLowerCase();
|
|
4835
|
+
if (lower.includes("embedding") || lower.includes("embed") || desc.includes("embedding")) {
|
|
4836
|
+
return {
|
|
4837
|
+
id: modelId,
|
|
4838
|
+
category: "specialized",
|
|
4839
|
+
specializedType: "Embedding",
|
|
4840
|
+
isRecommendedForCoding: false,
|
|
4841
|
+
capabilities: {
|
|
4842
|
+
streaming: false,
|
|
4843
|
+
tools: false,
|
|
4844
|
+
images: false,
|
|
4845
|
+
reasoning: false,
|
|
4846
|
+
...capabilities
|
|
4847
|
+
},
|
|
4848
|
+
warning: "Embeddings model: generates vector representations, not conversational text or tool calls."
|
|
4849
|
+
};
|
|
4850
|
+
}
|
|
4851
|
+
if (lower.includes("dall-e") || lower.includes("imagen") || lower.includes("stable-diffusion") || desc.includes("image generation")) {
|
|
4852
|
+
return {
|
|
4853
|
+
id: modelId,
|
|
4854
|
+
category: "specialized",
|
|
4855
|
+
specializedType: "Image Generation",
|
|
4856
|
+
isRecommendedForCoding: false,
|
|
4857
|
+
capabilities: {
|
|
4858
|
+
streaming: false,
|
|
4859
|
+
tools: false,
|
|
4860
|
+
images: false,
|
|
4861
|
+
reasoning: false,
|
|
4862
|
+
...capabilities
|
|
4863
|
+
},
|
|
4864
|
+
warning: "Image generation model: generates graphics/images, cannot execute coding tools."
|
|
4865
|
+
};
|
|
4866
|
+
}
|
|
4867
|
+
if (lower.includes("whisper") || lower.includes("tts") || lower.includes("speech") || lower.includes("audio") || lower.includes("realtime") || desc.includes("audio") || desc.includes("transcription")) {
|
|
4868
|
+
return {
|
|
4869
|
+
id: modelId,
|
|
4870
|
+
category: "specialized",
|
|
4871
|
+
specializedType: "Audio/Speech",
|
|
4872
|
+
isRecommendedForCoding: false,
|
|
4873
|
+
capabilities: {
|
|
4874
|
+
streaming: Boolean(capabilities?.streaming),
|
|
4875
|
+
tools: false,
|
|
4876
|
+
images: false,
|
|
4877
|
+
reasoning: false,
|
|
4878
|
+
...capabilities
|
|
4879
|
+
},
|
|
4880
|
+
warning: "Audio/Speech model: designed for audio processing, not interactive text coding."
|
|
4881
|
+
};
|
|
4882
|
+
}
|
|
4883
|
+
if (lower.includes(":batch") || lower.includes("-batch") || desc.includes("batch only")) {
|
|
4884
|
+
return {
|
|
4885
|
+
id: modelId,
|
|
4886
|
+
category: "specialized",
|
|
4887
|
+
specializedType: "Batch Processing",
|
|
4888
|
+
isRecommendedForCoding: false,
|
|
4889
|
+
capabilities: {
|
|
4890
|
+
streaming: false,
|
|
4891
|
+
tools: Boolean(capabilities?.tools),
|
|
4892
|
+
images: Boolean(capabilities?.images),
|
|
4893
|
+
reasoning: Boolean(capabilities?.reasoning),
|
|
4894
|
+
...capabilities
|
|
4895
|
+
},
|
|
4896
|
+
warning: "Batch model: processed asynchronously in queues with high latency, not interactive."
|
|
4897
|
+
};
|
|
4898
|
+
}
|
|
4899
|
+
if (lower.includes("moderation") || lower.includes("rerank")) {
|
|
4900
|
+
return {
|
|
4901
|
+
id: modelId,
|
|
4902
|
+
category: "specialized",
|
|
4903
|
+
specializedType: lower.includes("rerank") ? "Reranker" : "Moderation",
|
|
4904
|
+
isRecommendedForCoding: false,
|
|
4905
|
+
capabilities: {
|
|
4906
|
+
streaming: false,
|
|
4907
|
+
tools: false,
|
|
4908
|
+
images: false,
|
|
4909
|
+
reasoning: false,
|
|
4910
|
+
...capabilities
|
|
4911
|
+
},
|
|
4912
|
+
warning: "Specialized classification or reranking filter, not an interactive assistant."
|
|
4913
|
+
};
|
|
4914
|
+
}
|
|
4915
|
+
const isKnownNonTool = lower.includes("o1-preview") || lower.includes(":free") && !lower.includes("gemini") || lower.includes("deepseek-r1") && !lower.includes("distill");
|
|
4916
|
+
const toolsSupported = capabilities?.tools !== void 0 ? capabilities.tools : !isKnownNonTool;
|
|
4917
|
+
const streamingSupported = capabilities?.streaming ?? true;
|
|
4918
|
+
const imagesSupported = capabilities?.images ?? (lower.includes("vision") || lower.includes("gpt-4o") || lower.includes("claude-3") || lower.includes("gemini"));
|
|
4919
|
+
const reasoningSupported = capabilities?.reasoning ?? (lower.includes("o1") || lower.includes("o3") || lower.includes("r1") || lower.includes("thinking"));
|
|
4920
|
+
const resolvedCapabilities = {
|
|
4921
|
+
streaming: streamingSupported,
|
|
4922
|
+
tools: toolsSupported,
|
|
4923
|
+
images: imagesSupported,
|
|
4924
|
+
reasoning: reasoningSupported,
|
|
4925
|
+
contextWindow: capabilities?.contextWindow || 128e3
|
|
4926
|
+
};
|
|
4927
|
+
if (!toolsSupported) {
|
|
4928
|
+
return {
|
|
4929
|
+
id: modelId,
|
|
4930
|
+
category: "chat",
|
|
4931
|
+
isRecommendedForCoding: false,
|
|
4932
|
+
capabilities: resolvedCapabilities,
|
|
4933
|
+
warning: "Chat-only model: does not support tool/function calling (cannot inspect files, apply edits, or execute commands)."
|
|
4934
|
+
};
|
|
4935
|
+
}
|
|
4936
|
+
return {
|
|
4937
|
+
id: modelId,
|
|
4938
|
+
category: "coding",
|
|
4939
|
+
isRecommendedForCoding: true,
|
|
4940
|
+
capabilities: resolvedCapabilities
|
|
4941
|
+
};
|
|
4942
|
+
}
|
|
4943
|
+
function getRecommendedCodingModels(providerId, availableModels) {
|
|
4944
|
+
const norm = providerId.toLowerCase();
|
|
4945
|
+
if (availableModels && availableModels.length > 0) {
|
|
4946
|
+
const fromList = availableModels.filter((m) => {
|
|
4947
|
+
const c = classifyModel(m.id, m.capabilities, m.description);
|
|
4948
|
+
return c.category === "coding";
|
|
4949
|
+
}).map((m) => m.id);
|
|
4950
|
+
if (fromList.length > 0) {
|
|
4951
|
+
return fromList.slice(0, 5);
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
return DEFAULT_RECOMMENDED_CODING_MODELS[norm] || ["gemini-3.8-flash", "gpt-4o", "claude-3-7-sonnet"];
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4243
4957
|
export {
|
|
4958
|
+
isSecretFile,
|
|
4959
|
+
sanitizeSecrets,
|
|
4244
4960
|
TimeoutError,
|
|
4245
4961
|
FirstTokenTimeoutError,
|
|
4246
4962
|
CancelledError,
|
|
@@ -4251,6 +4967,7 @@ export {
|
|
|
4251
4967
|
LanternProviderError,
|
|
4252
4968
|
formatErrorMessage,
|
|
4253
4969
|
createLinkedAbortController,
|
|
4970
|
+
withTimeoutRace,
|
|
4254
4971
|
wrapStreamWithFirstTokenTimeout,
|
|
4255
4972
|
DEFAULT_CONFIG,
|
|
4256
4973
|
ALLOWED_CONFIG_KEYS,
|
|
@@ -4261,20 +4978,24 @@ export {
|
|
|
4261
4978
|
saveConfig,
|
|
4262
4979
|
setConfigValue,
|
|
4263
4980
|
getConfigValue,
|
|
4981
|
+
NativeCredentialStore,
|
|
4982
|
+
SERVICE_NAME,
|
|
4983
|
+
getLegacyCredentialsFilePath,
|
|
4984
|
+
getStoreName,
|
|
4264
4985
|
encryptSecret,
|
|
4265
4986
|
decryptSecret,
|
|
4266
4987
|
maskApiKey,
|
|
4988
|
+
migrateLegacyCredentials,
|
|
4267
4989
|
getApiKey,
|
|
4268
4990
|
setApiKey,
|
|
4269
4991
|
removeApiKey,
|
|
4270
4992
|
listConfiguredKeys,
|
|
4993
|
+
promptMaskedApiKey,
|
|
4271
4994
|
GeminiProvider,
|
|
4272
4995
|
OpenAIProvider,
|
|
4273
4996
|
AnthropicProvider,
|
|
4274
4997
|
OpenRouterProvider,
|
|
4275
4998
|
ProviderRegistry,
|
|
4276
|
-
isSecretFile,
|
|
4277
|
-
sanitizeSecrets,
|
|
4278
4999
|
validateWorkspacePath,
|
|
4279
5000
|
formatTerminalDiff,
|
|
4280
5001
|
formatNewFilePreview,
|
|
@@ -4302,6 +5023,8 @@ export {
|
|
|
4302
5023
|
SkillManager,
|
|
4303
5024
|
LanternAgent,
|
|
4304
5025
|
loadAttachment,
|
|
4305
|
-
loadAttachments
|
|
5026
|
+
loadAttachments,
|
|
5027
|
+
classifyModel,
|
|
5028
|
+
getRecommendedCodingModels
|
|
4306
5029
|
};
|
|
4307
|
-
//# sourceMappingURL=chunk-
|
|
5030
|
+
//# sourceMappingURL=chunk-45FEFNMM.js.map
|