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.
@@ -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 lowering temperature or setting a faster model with 'lantern model set <id>'.`));
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.dim(`Run 'lantern models list' to view available models for your account.`));
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 path from "path";
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 = path.join(os.homedir(), ".codelantern");
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 path.join(getLanternHomeDir(), "config.json");
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 path2 from "path";
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 getCredentialsFilePath() {
451
- return path2.join(getLanternHomeDir(), "credentials.json");
827
+ function getLegacyCredentialsFilePath() {
828
+ return path3.join(getLanternHomeDir(), "credentials.json");
829
+ }
830
+ function getStoreName() {
831
+ return NativeCredentialStore.getInstance().getStoreName();
452
832
  }
453
- function getMachineDerivedKey() {
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 = getMachineDerivedKey();
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 = getMachineDerivedKey();
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 { key: val.trim(), source: "env" };
947
+ return {
948
+ key: val.trim(),
949
+ source: "env",
950
+ sourceDescription: `env: ${envVar}`
951
+ };
497
952
  }
498
953
  }
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
- }
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 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
- }
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
- 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}`);
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 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;
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 false;
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({ apiKey: key });
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
- await client.models.countTokens({
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 pagedList = await client.models.list();
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
- const client = this.getClient();
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 responseStream = await client.models.generateContentStream({
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 LanternProviderError(`Authentication failed: ${msg}`, 401, this.id);
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(msg);
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 response = await fetch(`${this.baseUrl}/chat/completions`, {
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 response = await fetch(`${this.baseUrl}/messages`, {
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
- 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
- }));
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 readline from "readline";
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 = readline.createInterface({
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 === "connecting") {
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
- if (currentStepText === "") {
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-PELESOCI.js.map
5030
+ //# sourceMappingURL=chunk-45FEFNMM.js.map