@srouter/cli 0.1.4 → 0.1.5-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import "./chunk-BJ7B4R26.js";
2
+
1
3
  // src/index.ts
2
4
  import { Command } from "commander";
3
5
 
@@ -32,7 +34,7 @@ var GOROUTER_BASE_URL = "https://gorouter.app/v1";
32
34
  var GOROUTER_PROVIDER = {
33
35
  id: "gorouter",
34
36
  name: "GoRouter",
35
- category: "api_key",
37
+ category: "free_tier",
36
38
  protocol: "openai",
37
39
  base_url: GOROUTER_BASE_URL,
38
40
  web_url: "https://gorouter.app/sign-up?aff=cJJn",
@@ -46,7 +48,7 @@ var BLUESMINDS_BASE_URL = "https://api.bluesminds.com/v1";
46
48
  var BLUESMINDS_PROVIDER = {
47
49
  id: "bluesminds",
48
50
  name: "BluesMinds",
49
- category: "api_key",
51
+ category: "free_tier",
50
52
  protocol: "openai",
51
53
  base_url: BLUESMINDS_BASE_URL,
52
54
  web_url: "https://api.bluesminds.com/sign-up?aff=nCAw",
@@ -60,7 +62,7 @@ var SEEKAI_BASE_URL = "https://seekai.cc/v1";
60
62
  var SEEKAI_PROVIDER = {
61
63
  id: "seekai",
62
64
  name: "SeekAI",
63
- category: "api_key",
65
+ category: "free_tier",
64
66
  protocol: "openai",
65
67
  base_url: SEEKAI_BASE_URL,
66
68
  web_url: "https://seekai.cc/sign-up?aff=UU0C",
@@ -74,7 +76,7 @@ var TABITOKEN_BASE_URL = "https://tabitoken.com/v1";
74
76
  var TABITOKEN_PROVIDER = {
75
77
  id: "tabitoken",
76
78
  name: "TabiToken",
77
- category: "api_key",
79
+ category: "free_tier",
78
80
  protocol: "openai",
79
81
  base_url: TABITOKEN_BASE_URL,
80
82
  web_url: "https://tabitoken.com/sign-up?aff=h5iN",
@@ -140,6 +142,9 @@ var ANTHROPIC_PROVIDER = {
140
142
  // ../../packages/constants/dist/providers/antigravity.js
141
143
  var ANTIGRAVITY_IDE_BASE_URL = "https://daily-cloudcode-pa.googleapis.com";
142
144
  var ANTIGRAVITY_MODELS = [
145
+ { id: "gemini-3.8-flash-high", name: "Gemini 3.8 Flash (High)" },
146
+ { id: "gemini-3.8-flash-medium", name: "Gemini 3.8 Flash (Medium)" },
147
+ { id: "gemini-3.8-flash-low", name: "Gemini 3.8 Flash (Low)" },
143
148
  { id: "gemini-3.7-flash-high", name: "Gemini 3.7 Flash (High)" },
144
149
  { id: "gemini-3.7-flash-medium", name: "Gemini 3.7 Flash (Medium)" },
145
150
  { id: "gemini-3.7-flash-low", name: "Gemini 3.7 Flash (Low)" },
@@ -290,11 +295,8 @@ var QODER_PROVIDER = {
290
295
  // ../../packages/constants/dist/providers/opencode.js
291
296
  var OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1";
292
297
  var OPENCODE_ZEN_MODELS = [
293
- { id: "x-preview-f-free", name: "Ox Alpha Free (Unlimited)" },
294
298
  { id: "big-pickle", name: "Big Pickle (Free)" },
295
- { id: "laguna-s-2.1-free", name: "Poolside Laguna S 2.1 (Free)" },
296
299
  { id: "nemotron-3.5-lightning-free", name: "Nemotron 3.5 Lightning (Free)" },
297
- { id: "nemotron-3-ultra-free", name: "Nemotron 3 Ultra (Free)" },
298
300
  { id: "mimo-v2.5-free", name: "Xiaomi MiMo V2.5 (Free)" }
299
301
  ];
300
302
  var OPENCODE_ZEN_MODEL_IDS = OPENCODE_ZEN_MODELS.map((m) => m.id);
@@ -312,6 +314,21 @@ var OPENCODE_ZEN_PROVIDER = {
312
314
  status_message: "Free Tier Ready (Unlimited)"
313
315
  };
314
316
 
317
+ // ../../packages/constants/dist/providers/bai.js
318
+ var BAI_BASE_URL = "https://api.b.ai/v1";
319
+ var BAI_PROVIDER = {
320
+ id: "bai",
321
+ name: "B.AI",
322
+ category: "free_tier",
323
+ protocol: "openai",
324
+ alias: "bai",
325
+ base_url: BAI_BASE_URL,
326
+ web_url: "https://b.ai",
327
+ requires_api_key: true,
328
+ supports_custom_url: true,
329
+ status_message: "B.AI API key missing"
330
+ };
331
+
315
332
  // ../../packages/constants/dist/providers/catalog.js
316
333
  var KNOWN_PROVIDERS = [
317
334
  KIRO_PROVIDER,
@@ -328,13 +345,19 @@ var KNOWN_PROVIDERS = [
328
345
  QODER_PROVIDER,
329
346
  CODEBUDDY_PROVIDER,
330
347
  CODEBUDDY_CN_PROVIDER,
331
- OPENCODE_ZEN_PROVIDER
348
+ OPENCODE_ZEN_PROVIDER,
349
+ BAI_PROVIDER
332
350
  ];
333
351
  var KNOWN_PROVIDER_MAP = Object.freeze(Object.fromEntries(KNOWN_PROVIDERS.map((Provider) => [Provider.id, Provider])));
334
352
  var KNOWN_PROVIDER_IDS_DESC = Object.freeze(Object.keys(KNOWN_PROVIDER_MAP).sort((A, B) => B.length - A.length));
335
353
  var LEGACY_ALIAS_MAP = Object.freeze({
336
354
  claude: "claude",
337
- cbai: "codebuddy"
355
+ cbai: "codebuddy",
356
+ // OpenCode Zen's executor registers with base id "opencode_zen"; its
357
+ // listModels splits on "_" and produces "opencode/<model>" ids, while
358
+ // alias-prefixed connections produce "zen/<model>". Map both so log
359
+ // attribution and routing resolve to the registered base id.
360
+ opencode: "opencode_zen"
338
361
  });
339
362
 
340
363
  // ../../packages/constants/dist/seed.js
@@ -342,8 +365,8 @@ var DEFAULT_PROVIDERS = Object.freeze(KNOWN_PROVIDERS.map(({ alias: _alias, ...s
342
365
  var DEFAULT_PROVIDER_MAP = Object.freeze(Object.fromEntries(DEFAULT_PROVIDERS.map((seed) => [seed.id, seed])));
343
366
 
344
367
  // ../../packages/constants/dist/version.js
345
- var GLOBAL_VERSION = "0.1.3";
346
- var CLI_VERSION = GLOBAL_VERSION;
368
+ var GLOBAL_VERSION = "0.1.6";
369
+ var CLI_VERSION = "0.1.5-rc.1";
347
370
  var SROUTER_VERSION_TAG = `v${GLOBAL_VERSION}`;
348
371
 
349
372
  // src/commands/setup.ts
@@ -475,10 +498,10 @@ import { exec } from "node:child_process";
475
498
  import { promisify } from "node:util";
476
499
  var execAsync = promisify(exec);
477
500
  function getPlatform() {
478
- const p2 = process.platform;
479
- if (p2 === "win32") return "windows";
480
- if (p2 === "darwin") return "macos";
481
- if (p2 === "linux") return "linux";
501
+ const p3 = process.platform;
502
+ if (p3 === "win32") return "windows";
503
+ if (p3 === "darwin") return "macos";
504
+ if (p3 === "linux") return "linux";
482
505
  return "unknown";
483
506
  }
484
507
  function isWindows() {
@@ -488,10 +511,10 @@ function isMacOS() {
488
511
  return process.platform === "darwin";
489
512
  }
490
513
  function getOsDisplayName() {
491
- const p2 = getPlatform();
514
+ const p3 = getPlatform();
492
515
  const arch = os2.arch();
493
516
  const rel = os2.release();
494
- switch (p2) {
517
+ switch (p3) {
495
518
  case "macos":
496
519
  return `macOS (${arch === "arm64" ? "Apple Silicon" : arch}) [Darwin ${rel}]`;
497
520
  case "windows":
@@ -550,9 +573,9 @@ function getClaudeConfigPath() {
550
573
  candidatePaths.push(path2.join(process.env.CLAUDE_CONFIG_DIR, "settings.json"));
551
574
  candidatePaths.push(path2.join(process.env.CLAUDE_CONFIG_DIR, "config.json"));
552
575
  }
576
+ candidatePaths.push(path2.join(home, ".claude.json"));
553
577
  candidatePaths.push(path2.join(home, ".claude", "settings.json"));
554
578
  candidatePaths.push(path2.join(home, ".config", "claude", "settings.json"));
555
- candidatePaths.push(path2.join(home, ".claude.json"));
556
579
  if (isWindows()) {
557
580
  const appData = process.env.APPDATA || path2.join(home, "AppData", "Roaming");
558
581
  candidatePaths.push(path2.join(appData, "Claude", "settings.json"));
@@ -561,9 +584,9 @@ function getClaudeConfigPath() {
561
584
  candidatePaths.push(path2.join(home, ".claude", "config.json"));
562
585
  candidatePaths.push(path2.join(home, ".config", "claude", "config.json"));
563
586
  }
564
- for (const p2 of candidatePaths) {
565
- if (fs2.existsSync(p2)) {
566
- return p2;
587
+ for (const p3 of candidatePaths) {
588
+ if (fs2.existsSync(p3)) {
589
+ return p3;
567
590
  }
568
591
  }
569
592
  if (fs2.existsSync(path2.join(home, ".claude"))) {
@@ -605,9 +628,9 @@ function getOpenCodeConfigPath() {
605
628
  }
606
629
  candidatePaths.push(path2.join(home, ".opencode.jsonc"));
607
630
  candidatePaths.push(path2.join(home, ".opencode.json"));
608
- for (const p2 of candidatePaths) {
609
- if (fs2.existsSync(p2)) {
610
- return p2;
631
+ for (const p3 of candidatePaths) {
632
+ if (fs2.existsSync(p3)) {
633
+ return p3;
611
634
  }
612
635
  }
613
636
  if (isWindows()) {
@@ -666,38 +689,55 @@ var ClaudeAdapter = class extends AbstractToolAdapter {
666
689
  async getStatus() {
667
690
  const configPath = this.getConfigPath();
668
691
  const installed = await this.isInstalled();
669
- try {
670
- const raw = await fs3.readFile(configPath, "utf-8");
671
- const parsed = parseJsonSafe(raw);
672
- const baseUrl = parsed.env?.ANTHROPIC_BASE_URL || parsed.ANTHROPIC_BASE_URL || parsed.baseUrl || void 0;
673
- const model = parsed.env?.ANTHROPIC_DEFAULT_MODEL || parsed.env?.ANTHROPIC_MODEL || parsed.model || parsed.ANTHROPIC_MODEL || void 0;
674
- const opusModel = parsed.env?.ANTHROPIC_DEFAULT_OPUS_MODEL || parsed.ANTHROPIC_DEFAULT_OPUS_MODEL || parsed.env?.ANTHROPIC_OPUS_MODEL || parsed.ANTHROPIC_OPUS_MODEL || void 0;
675
- const sonnetModel = parsed.env?.ANTHROPIC_DEFAULT_SONNET_MODEL || parsed.ANTHROPIC_DEFAULT_SONNET_MODEL || parsed.env?.ANTHROPIC_SONNET_MODEL || parsed.ANTHROPIC_SONNET_MODEL || void 0;
676
- const haikuModel = parsed.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL || parsed.ANTHROPIC_DEFAULT_HAIKU_MODEL || parsed.env?.ANTHROPIC_HAIKU_MODEL || parsed.ANTHROPIC_HAIKU_MODEL || void 0;
677
- const linked = Boolean(
678
- baseUrl && (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1") || baseUrl.includes("srouter"))
679
- );
680
- return {
681
- id: this.id,
682
- name: this.name,
683
- installed,
684
- linked,
685
- configPath,
686
- currentBaseUrl: baseUrl,
687
- currentModel: model,
688
- currentOpusModel: opusModel,
689
- currentSonnetModel: sonnetModel,
690
- currentHaikuModel: haikuModel
691
- };
692
- } catch {
693
- return {
694
- id: this.id,
695
- name: this.name,
696
- installed,
697
- linked: false,
698
- configPath
699
- };
692
+ const candidatePaths = [configPath];
693
+ if (!this.customConfigPath) {
694
+ const homeClaudeJson = path3.join(os3.homedir(), ".claude.json");
695
+ const homeClaudeSettings = path3.join(os3.homedir(), ".claude", "settings.json");
696
+ if (!candidatePaths.includes(homeClaudeJson)) candidatePaths.push(homeClaudeJson);
697
+ if (!candidatePaths.includes(homeClaudeSettings)) candidatePaths.push(homeClaudeSettings);
698
+ }
699
+ let unlinkedModel;
700
+ let unlinkedBaseUrl;
701
+ for (const targetPath of candidatePaths) {
702
+ try {
703
+ const raw = await fs3.readFile(targetPath, "utf-8");
704
+ const parsed = parseJsonSafe(raw);
705
+ const baseUrl = parsed.env?.ANTHROPIC_BASE_URL || parsed.ANTHROPIC_BASE_URL || parsed.baseUrl || void 0;
706
+ const model = parsed.env?.ANTHROPIC_DEFAULT_MODEL || parsed.env?.ANTHROPIC_MODEL || parsed.model || parsed.ANTHROPIC_MODEL || void 0;
707
+ const opusModel = parsed.env?.ANTHROPIC_DEFAULT_OPUS_MODEL || parsed.ANTHROPIC_DEFAULT_OPUS_MODEL || parsed.env?.ANTHROPIC_OPUS_MODEL || parsed.ANTHROPIC_OPUS_MODEL || void 0;
708
+ const sonnetModel = parsed.env?.ANTHROPIC_DEFAULT_SONNET_MODEL || parsed.ANTHROPIC_DEFAULT_SONNET_MODEL || parsed.env?.ANTHROPIC_SONNET_MODEL || parsed.ANTHROPIC_SONNET_MODEL || void 0;
709
+ const haikuModel = parsed.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL || parsed.ANTHROPIC_DEFAULT_HAIKU_MODEL || parsed.env?.ANTHROPIC_HAIKU_MODEL || parsed.ANTHROPIC_HAIKU_MODEL || void 0;
710
+ const linked = Boolean(
711
+ baseUrl && (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1") || baseUrl.includes("srouter"))
712
+ );
713
+ if (linked) {
714
+ return {
715
+ id: this.id,
716
+ name: this.name,
717
+ installed,
718
+ linked: true,
719
+ configPath: targetPath,
720
+ currentBaseUrl: baseUrl,
721
+ currentModel: model,
722
+ currentOpusModel: opusModel,
723
+ currentSonnetModel: sonnetModel,
724
+ currentHaikuModel: haikuModel
725
+ };
726
+ }
727
+ if (!unlinkedModel && model) unlinkedModel = model;
728
+ if (!unlinkedBaseUrl && baseUrl) unlinkedBaseUrl = baseUrl;
729
+ } catch {
730
+ }
700
731
  }
732
+ return {
733
+ id: this.id,
734
+ name: this.name,
735
+ installed,
736
+ linked: false,
737
+ configPath,
738
+ currentBaseUrl: unlinkedBaseUrl,
739
+ currentModel: unlinkedModel
740
+ };
701
741
  }
702
742
  async link(context) {
703
743
  const configPath = this.getConfigPath();
@@ -721,6 +761,7 @@ var ClaudeAdapter = class extends AbstractToolAdapter {
721
761
  data.env.ANTHROPIC_DEFAULT_OPUS_MODEL = context.opusModel || defaultModel;
722
762
  data.env.ANTHROPIC_DEFAULT_SONNET_MODEL = context.sonnetModel || defaultModel;
723
763
  data.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = context.haikuModel || defaultModel;
764
+ data.env.CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT = "1";
724
765
  if (data.env.ANTHROPIC_DEFAULT_FABLE_MODEL) {
725
766
  delete data.env.ANTHROPIC_DEFAULT_FABLE_MODEL;
726
767
  }
@@ -783,6 +824,7 @@ var ClaudeAdapter = class extends AbstractToolAdapter {
783
824
  delete data.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
784
825
  delete data.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
785
826
  delete data.env.ANTHROPIC_DEFAULT_FABLE_MODEL;
827
+ delete data.env.CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT;
786
828
  }
787
829
  await fs3.writeFile(configPath, JSON.stringify(data, null, 2), "utf-8");
788
830
  return true;
@@ -802,15 +844,16 @@ var ClaudeAdapter = class extends AbstractToolAdapter {
802
844
  ANTHROPIC_MODEL: defaultModel,
803
845
  ANTHROPIC_DEFAULT_OPUS_MODEL: context.opusModel || defaultModel,
804
846
  ANTHROPIC_DEFAULT_SONNET_MODEL: context.sonnetModel || defaultModel,
805
- ANTHROPIC_DEFAULT_HAIKU_MODEL: context.haikuModel || defaultModel
847
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: context.haikuModel || defaultModel,
848
+ CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT: "1"
806
849
  };
807
850
  return env;
808
851
  }
809
852
  };
810
853
 
811
854
  // src/adapters/opencode.ts
812
- import fs4 from "node:fs/promises";
813
- import path4 from "node:path";
855
+ import fs5 from "node:fs/promises";
856
+ import path5 from "node:path";
814
857
 
815
858
  // src/lib/srouterClient.ts
816
859
  function normalizeBaseUrl(baseUrl) {
@@ -881,7 +924,361 @@ async function fetchAvailableModels(baseUrl, apiKey) {
881
924
  }
882
925
  }
883
926
 
927
+ // ../../packages/pricing/dist/parser.js
928
+ import fs4 from "node:fs";
929
+ import path4 from "node:path";
930
+ import { fileURLToPath } from "node:url";
931
+ function stripJsonComments(jsonc) {
932
+ let insideString = false;
933
+ let isEscaped = false;
934
+ let output = "";
935
+ let i = 0;
936
+ while (i < jsonc.length) {
937
+ const char = jsonc[i];
938
+ const nextChar = jsonc[i + 1];
939
+ if (insideString) {
940
+ output += char;
941
+ if (isEscaped) {
942
+ isEscaped = false;
943
+ } else if (char === "\\") {
944
+ isEscaped = true;
945
+ } else if (char === '"') {
946
+ insideString = false;
947
+ }
948
+ i++;
949
+ continue;
950
+ }
951
+ if (char === '"') {
952
+ insideString = true;
953
+ output += char;
954
+ i++;
955
+ continue;
956
+ }
957
+ if (char === "/" && nextChar === "/") {
958
+ i += 2;
959
+ while (i < jsonc.length && jsonc[i] !== "\n" && jsonc[i] !== "\r") {
960
+ i++;
961
+ }
962
+ continue;
963
+ }
964
+ if (char === "/" && nextChar === "*") {
965
+ i += 2;
966
+ while (i < jsonc.length && !(jsonc[i] === "*" && jsonc[i + 1] === "/")) {
967
+ i++;
968
+ }
969
+ i += 2;
970
+ continue;
971
+ }
972
+ output += char;
973
+ i++;
974
+ }
975
+ return output.replace(/,(\s*[}\]])/g, "$1");
976
+ }
977
+ function parseJsonc(jsonc) {
978
+ const cleanJson = stripJsonComments(jsonc);
979
+ return JSON.parse(cleanJson);
980
+ }
981
+ function flattenModelPrices(models) {
982
+ const flat = {};
983
+ for (const [key, val] of Object.entries(models || {})) {
984
+ if (Array.isArray(val)) {
985
+ for (const item of val) {
986
+ if (item && item.id) {
987
+ flat[item.id] = item;
988
+ }
989
+ }
990
+ } else if (val && typeof val === "object") {
991
+ if ("input" in val && "output" in val) {
992
+ flat[key] = val;
993
+ } else {
994
+ for (const [subKey, subVal] of Object.entries(val)) {
995
+ flat[subKey] = subVal;
996
+ }
997
+ }
998
+ }
999
+ }
1000
+ return flat;
1001
+ }
1002
+ function resolvePricingDataPath(customPath) {
1003
+ if (customPath)
1004
+ return customPath;
1005
+ const currentDir = path4.dirname(fileURLToPath(import.meta.url));
1006
+ const candidates = [
1007
+ path4.resolve(currentDir, "../data/pricing.jsonc"),
1008
+ path4.resolve(currentDir, "../data/pricing.json"),
1009
+ path4.resolve(currentDir, "../../data/pricing.jsonc"),
1010
+ path4.resolve(currentDir, "../../data/pricing.json"),
1011
+ path4.resolve(process.cwd(), "packages/pricing/data/pricing.jsonc"),
1012
+ path4.resolve(process.cwd(), "packages/pricing/data/pricing.json"),
1013
+ path4.resolve(process.cwd(), "data/pricing.jsonc"),
1014
+ path4.resolve(process.cwd(), "data/pricing.json")
1015
+ ];
1016
+ for (const candidate of candidates) {
1017
+ if (fs4.existsSync(candidate)) {
1018
+ return candidate;
1019
+ }
1020
+ }
1021
+ return candidates[0];
1022
+ }
1023
+ function modelsDevToModelPrice(model) {
1024
+ if (!model.cost)
1025
+ return void 0;
1026
+ const input = model.cost.input ?? 0;
1027
+ const output = model.cost.output ?? 0;
1028
+ return {
1029
+ id: model.id,
1030
+ name: model.name,
1031
+ input,
1032
+ output,
1033
+ cached: model.cost.cache_read,
1034
+ reasoning: model.cost.reasoning ?? output,
1035
+ cache_creation: model.cost.cache_write ?? input
1036
+ };
1037
+ }
1038
+ function loadPricingFromModelsDev(customPath) {
1039
+ const modelsData = loadModelsDevData(customPath);
1040
+ const models = {};
1041
+ const providerModels = {};
1042
+ const aliases = {};
1043
+ for (const [key, item] of Object.entries(modelsData)) {
1044
+ const price = modelsDevToModelPrice(item);
1045
+ if (!price)
1046
+ continue;
1047
+ models[key] = price;
1048
+ if (item.id && item.id !== key) {
1049
+ models[item.id] = price;
1050
+ }
1051
+ if (key.includes("/")) {
1052
+ const modelName = key.split("/").slice(1).join("/");
1053
+ if (!aliases[modelName]) {
1054
+ aliases[modelName] = key;
1055
+ }
1056
+ if (!models[modelName]) {
1057
+ models[modelName] = price;
1058
+ }
1059
+ }
1060
+ const provider = key.includes("/") ? key.split("/")[0] : "other";
1061
+ if (!providerModels[provider]) {
1062
+ providerModels[provider] = [];
1063
+ }
1064
+ providerModels[provider].push(price);
1065
+ }
1066
+ return {
1067
+ version: "1.0.0",
1068
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
1069
+ defaults: {
1070
+ input: 2,
1071
+ output: 8,
1072
+ cached: 1,
1073
+ reasoning: 12,
1074
+ cache_creation: 2
1075
+ },
1076
+ models,
1077
+ providerModels,
1078
+ aliases
1079
+ };
1080
+ }
1081
+ function loadPricingData(customPath) {
1082
+ if (customPath && (customPath.endsWith("models.jsonc") || customPath.endsWith("models.json"))) {
1083
+ return loadPricingFromModelsDev(customPath);
1084
+ }
1085
+ const filePath = resolvePricingDataPath(customPath);
1086
+ if (!fs4.existsSync(filePath)) {
1087
+ const modelsDevPath2 = resolveModelsDevDataPath();
1088
+ if (fs4.existsSync(modelsDevPath2)) {
1089
+ return loadPricingFromModelsDev(modelsDevPath2);
1090
+ }
1091
+ throw new Error(`Pricing dataset file not found at: ${filePath}`);
1092
+ }
1093
+ const content = fs4.readFileSync(filePath, "utf-8");
1094
+ const raw = parseJsonc(content);
1095
+ const flatModels = flattenModelPrices(raw.models);
1096
+ const isGroupedArray = raw.models && Object.values(raw.models).length > 0 && Array.isArray(Object.values(raw.models)[0]);
1097
+ const dataset = {
1098
+ version: raw.version,
1099
+ updatedAt: raw.updatedAt,
1100
+ defaults: raw.defaults,
1101
+ models: { ...flatModels },
1102
+ providerModels: isGroupedArray ? { ...raw.models } : void 0,
1103
+ aliases: { ...raw.aliases || {} }
1104
+ };
1105
+ const modelsDevPath = resolveModelsDevDataPath();
1106
+ if (fs4.existsSync(modelsDevPath)) {
1107
+ try {
1108
+ const modelsDevDataset = loadPricingFromModelsDev(modelsDevPath);
1109
+ for (const [k, v] of Object.entries(modelsDevDataset.models)) {
1110
+ if (!dataset.models[k]) {
1111
+ dataset.models[k] = v;
1112
+ }
1113
+ }
1114
+ for (const [k, v] of Object.entries(modelsDevDataset.aliases)) {
1115
+ if (!dataset.aliases[k] && !dataset.models[k]) {
1116
+ dataset.aliases[k] = v;
1117
+ }
1118
+ }
1119
+ } catch {
1120
+ }
1121
+ }
1122
+ return dataset;
1123
+ }
1124
+ function resolveModelsDevDataPath(customPath) {
1125
+ if (customPath) {
1126
+ if (path4.isAbsolute(customPath)) {
1127
+ return customPath;
1128
+ }
1129
+ return path4.resolve(process.cwd(), customPath);
1130
+ }
1131
+ const currentDir = path4.dirname(fileURLToPath(import.meta.url));
1132
+ const candidates = [
1133
+ path4.resolve(currentDir, "../models.jsonc"),
1134
+ path4.resolve(currentDir, "../models.json"),
1135
+ path4.resolve(currentDir, "../../models.jsonc"),
1136
+ path4.resolve(currentDir, "../../models.json"),
1137
+ path4.resolve(process.cwd(), "packages/pricing/models.jsonc"),
1138
+ path4.resolve(process.cwd(), "packages/pricing/models.json"),
1139
+ path4.resolve(process.cwd(), "models.jsonc"),
1140
+ path4.resolve(process.cwd(), "models.json")
1141
+ ];
1142
+ for (const candidate of candidates) {
1143
+ if (fs4.existsSync(candidate)) {
1144
+ return candidate;
1145
+ }
1146
+ }
1147
+ return candidates[0];
1148
+ }
1149
+ function loadModelsDevData(customPath) {
1150
+ const filePath = resolveModelsDevDataPath(customPath);
1151
+ if (!fs4.existsSync(filePath)) {
1152
+ return {};
1153
+ }
1154
+ const content = fs4.readFileSync(filePath, "utf-8");
1155
+ return parseJsonc(content);
1156
+ }
1157
+
1158
+ // ../../packages/pricing/dist/matcher.js
1159
+ function normalizeModelName(rawModel, aliases) {
1160
+ if (!rawModel)
1161
+ return "";
1162
+ let clean = rawModel.trim();
1163
+ if (clean.includes(":") && !clean.includes("://")) {
1164
+ const colonIndex = clean.lastIndexOf(":");
1165
+ clean = clean.substring(0, colonIndex).trim();
1166
+ }
1167
+ if (clean.includes("/")) {
1168
+ clean = clean.split("/").pop()?.trim() ?? clean;
1169
+ }
1170
+ if (aliases) {
1171
+ const lower = clean.toLowerCase();
1172
+ for (const [alias, target] of Object.entries(aliases)) {
1173
+ if (alias.toLowerCase() === lower) {
1174
+ return target;
1175
+ }
1176
+ }
1177
+ }
1178
+ return clean;
1179
+ }
1180
+ function findCanonicalModelKey(rawModel, models, aliases) {
1181
+ if (!rawModel)
1182
+ return void 0;
1183
+ const normalized = normalizeModelName(rawModel, aliases);
1184
+ if (models[normalized]) {
1185
+ return normalized;
1186
+ }
1187
+ if (models[rawModel]) {
1188
+ return rawModel;
1189
+ }
1190
+ const normalizedLower = normalized.toLowerCase();
1191
+ for (const key of Object.keys(models)) {
1192
+ if (key.toLowerCase() === normalizedLower) {
1193
+ return key;
1194
+ }
1195
+ }
1196
+ const baseClean = (rawModel.includes("/") ? rawModel.split("/").pop() ?? rawModel : rawModel).split(":")[0].trim().toLowerCase();
1197
+ for (const key of Object.keys(models)) {
1198
+ if (key.toLowerCase() === baseClean) {
1199
+ return key;
1200
+ }
1201
+ }
1202
+ return void 0;
1203
+ }
1204
+
1205
+ // ../../packages/pricing/dist/pricing.js
1206
+ var EMBEDDED_DEFAULT_PRICING = {
1207
+ input: 2,
1208
+ output: 8,
1209
+ cached: 1,
1210
+ reasoning: 12,
1211
+ cache_creation: 2
1212
+ };
1213
+ var loadedDataset;
1214
+ try {
1215
+ loadedDataset = loadPricingData();
1216
+ } catch {
1217
+ loadedDataset = {
1218
+ defaults: EMBEDDED_DEFAULT_PRICING,
1219
+ models: {},
1220
+ aliases: {}
1221
+ };
1222
+ }
1223
+ var DEFAULT_PRICING = loadedDataset.defaults || EMBEDDED_DEFAULT_PRICING;
1224
+ var MODEL_PRICING = loadedDataset.models;
1225
+ var PROVIDER_MODELS = loadedDataset.providerModels;
1226
+ var MODEL_ALIASES = loadedDataset.aliases;
1227
+ var cachedModelsDevData;
1228
+ function getModelMetadata(modelId) {
1229
+ if (!modelId)
1230
+ return void 0;
1231
+ if (!cachedModelsDevData) {
1232
+ try {
1233
+ cachedModelsDevData = loadModelsDevData();
1234
+ } catch {
1235
+ cachedModelsDevData = {};
1236
+ }
1237
+ }
1238
+ const data = cachedModelsDevData;
1239
+ if (data[modelId]) {
1240
+ return data[modelId];
1241
+ }
1242
+ const canonical = findCanonicalModelKey(modelId, MODEL_PRICING, MODEL_ALIASES) || normalizeModelName(modelId, MODEL_ALIASES);
1243
+ if (canonical && data[canonical]) {
1244
+ return data[canonical];
1245
+ }
1246
+ const targetSuffix = `/${canonical}`;
1247
+ for (const [key, item] of Object.entries(data)) {
1248
+ if (key === canonical || key.endsWith(targetSuffix) || item.id === canonical || item.id.endsWith(targetSuffix)) {
1249
+ return item;
1250
+ }
1251
+ }
1252
+ return void 0;
1253
+ }
1254
+
884
1255
  // src/adapters/opencode.ts
1256
+ function createOpenCodeModelConfig(id, name) {
1257
+ const displayName = name || formatModelDisplayName(id);
1258
+ const config = {
1259
+ id,
1260
+ name: displayName
1261
+ };
1262
+ const meta = getModelMetadata(id);
1263
+ if (meta?.modalities?.input?.includes("image") || meta?.attachment) {
1264
+ config.attachment = true;
1265
+ config.modalities = {
1266
+ input: meta.modalities?.input || ["text", "image"],
1267
+ output: meta.modalities?.output || ["text"]
1268
+ };
1269
+ } else {
1270
+ const lower = id.toLowerCase();
1271
+ const isKnownVision = lower.includes("gemini") || lower.includes("vision") || lower.includes("gpt-4o") || lower.includes("claude-3") || lower.includes("claude-sonnet") || lower.includes("claude-opus") || lower.includes("pixtral");
1272
+ if (isKnownVision) {
1273
+ config.attachment = true;
1274
+ config.modalities = {
1275
+ input: ["text", "image"],
1276
+ output: ["text"]
1277
+ };
1278
+ }
1279
+ }
1280
+ return config;
1281
+ }
885
1282
  var DEFAULT_SROUTER_MODELS = [
886
1283
  { id: "anthropic/claude-3-7-sonnet", name: "Claude 3.7 Sonnet (Anthropic)" },
887
1284
  { id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet" },
@@ -963,7 +1360,7 @@ function formatProviderLabel(provider) {
963
1360
  function humanizeName(str2) {
964
1361
  return str2.replace(/[-_]/g, " ").replace(/\b[a-z]/g, (c) => c.toUpperCase());
965
1362
  }
966
- function parseJsonc(content) {
1363
+ function parseJsonc2(content) {
967
1364
  try {
968
1365
  const clean = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").replace(/,\s*([\]}])/g, "$1");
969
1366
  return JSON.parse(clean);
@@ -999,8 +1396,8 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
999
1396
  const configPath = this.getConfigPath();
1000
1397
  const installed = await this.isInstalled();
1001
1398
  try {
1002
- const raw = await fs4.readFile(configPath, "utf-8");
1003
- const parsed = parseJsonc(raw);
1399
+ const raw = await fs5.readFile(configPath, "utf-8");
1400
+ const parsed = parseJsonc2(raw);
1004
1401
  const baseUrl = parsed.provider?.srouter?.options?.baseURL || parsed.openai_base_url || parsed.api_base || parsed.baseUrl || parsed.providers?.srouter?.baseUrl || void 0;
1005
1402
  const model = parsed.model || parsed.default_model || void 0;
1006
1403
  const linked = Boolean(
@@ -1030,8 +1427,8 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
1030
1427
  const backupPath = context.dryRun ? void 0 : await this.store.createBackup(this.id, configPath);
1031
1428
  let data = {};
1032
1429
  try {
1033
- const raw = await fs4.readFile(configPath, "utf-8");
1034
- data = parseJsonc(raw);
1430
+ const raw = await fs5.readFile(configPath, "utf-8");
1431
+ data = parseJsonc2(raw);
1035
1432
  } catch {
1036
1433
  data = {};
1037
1434
  }
@@ -1046,14 +1443,18 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
1046
1443
  const existingModels = existingSrouter.models || {};
1047
1444
  const modelsMap = {};
1048
1445
  for (const m of DEFAULT_SROUTER_MODELS) {
1049
- modelsMap[m.id] = {
1050
- id: m.id,
1051
- name: m.name
1052
- };
1446
+ modelsMap[m.id] = createOpenCodeModelConfig(m.id, m.name);
1053
1447
  }
1054
1448
  for (const [key, val] of Object.entries(existingModels)) {
1055
1449
  if (val && typeof val === "object") {
1056
- modelsMap[key] = val;
1450
+ const existingVal = val;
1451
+ const fresh = createOpenCodeModelConfig(key, existingVal.name);
1452
+ modelsMap[key] = {
1453
+ ...fresh,
1454
+ ...existingVal,
1455
+ ...fresh.attachment !== void 0 && { attachment: fresh.attachment },
1456
+ ...fresh.modalities !== void 0 && { modalities: fresh.modalities }
1457
+ };
1057
1458
  }
1058
1459
  }
1059
1460
  try {
@@ -1061,19 +1462,16 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
1061
1462
  for (const rawId of liveModels) {
1062
1463
  if (!rawId) continue;
1063
1464
  const cleanId = rawId.startsWith("srouter/") ? rawId.replace(/^srouter\//, "") : rawId;
1064
- modelsMap[cleanId] = {
1065
- id: cleanId,
1066
- name: formatModelDisplayName(cleanId)
1067
- };
1465
+ modelsMap[cleanId] = createOpenCodeModelConfig(cleanId);
1068
1466
  }
1069
1467
  } catch {
1070
1468
  }
1071
1469
  const rawModel = context.model || "claude-3-7-sonnet";
1072
1470
  const cleanModelId = rawModel.startsWith("srouter/") ? rawModel.replace(/^srouter\//, "") : rawModel;
1073
- modelsMap[cleanModelId] = {
1074
- id: cleanModelId,
1075
- name: modelsMap[cleanModelId]?.name || formatModelDisplayName(cleanModelId)
1076
- };
1471
+ modelsMap[cleanModelId] = createOpenCodeModelConfig(
1472
+ cleanModelId,
1473
+ modelsMap[cleanModelId]?.name
1474
+ );
1077
1475
  data.provider.srouter = {
1078
1476
  name: "SRouter",
1079
1477
  npm: "@ai-sdk/openai-compatible",
@@ -1085,8 +1483,8 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
1085
1483
  };
1086
1484
  data.model = `srouter/${cleanModelId}`;
1087
1485
  if (!context.dryRun) {
1088
- await fs4.mkdir(path4.dirname(configPath), { recursive: true });
1089
- await fs4.writeFile(configPath, JSON.stringify(data, null, 4), "utf-8");
1486
+ await fs5.mkdir(path5.dirname(configPath), { recursive: true });
1487
+ await fs5.writeFile(configPath, JSON.stringify(data, null, 4), "utf-8");
1090
1488
  }
1091
1489
  return {
1092
1490
  backupPath,
@@ -1100,8 +1498,8 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
1100
1498
  }
1101
1499
  const configPath = this.getConfigPath();
1102
1500
  try {
1103
- const raw = await fs4.readFile(configPath, "utf-8");
1104
- const data = parseJsonc(raw);
1501
+ const raw = await fs5.readFile(configPath, "utf-8");
1502
+ const data = parseJsonc2(raw);
1105
1503
  if (data.provider?.srouter) {
1106
1504
  delete data.provider.srouter;
1107
1505
  }
@@ -1113,7 +1511,7 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
1113
1511
  delete data.openai_api_key;
1114
1512
  delete data.api_key;
1115
1513
  delete data.providers;
1116
- await fs4.writeFile(configPath, JSON.stringify(data, null, 4), "utf-8");
1514
+ await fs5.writeFile(configPath, JSON.stringify(data, null, 4), "utf-8");
1117
1515
  return true;
1118
1516
  } catch {
1119
1517
  return false;
@@ -1136,9 +1534,183 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
1136
1534
  }
1137
1535
  };
1138
1536
 
1537
+ // src/adapters/hindsight.ts
1538
+ import fs6 from "node:fs/promises";
1539
+ import fsSync from "node:fs";
1540
+ import path6 from "node:path";
1541
+ import os4 from "node:os";
1542
+ function getHindsightConfigPath() {
1543
+ const cwdEnv = path6.join(process.cwd(), ".env");
1544
+ const homeHindsightEnv = path6.join(os4.homedir(), ".hindsight", ".env");
1545
+ if (process.env.HINDSIGHT_CONFIG_PATH) {
1546
+ return process.env.HINDSIGHT_CONFIG_PATH;
1547
+ }
1548
+ try {
1549
+ if (fsSync.existsSync(cwdEnv)) {
1550
+ const content = fsSync.readFileSync(cwdEnv, "utf-8");
1551
+ if (content.includes("HINDSIGHT_")) {
1552
+ return cwdEnv;
1553
+ }
1554
+ }
1555
+ if (fsSync.existsSync(homeHindsightEnv)) {
1556
+ return homeHindsightEnv;
1557
+ }
1558
+ } catch {
1559
+ }
1560
+ return homeHindsightEnv;
1561
+ }
1562
+ function parseEnvLines(content) {
1563
+ const map = /* @__PURE__ */ new Map();
1564
+ const lines = content.split("\n");
1565
+ for (const line of lines) {
1566
+ const trimmed = line.trim();
1567
+ if (!trimmed || trimmed.startsWith("#")) continue;
1568
+ const eqIdx = trimmed.indexOf("=");
1569
+ if (eqIdx > 0) {
1570
+ const key = trimmed.slice(0, eqIdx).trim();
1571
+ let val = trimmed.slice(eqIdx + 1).trim();
1572
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
1573
+ val = val.slice(1, -1);
1574
+ }
1575
+ map.set(key, val);
1576
+ }
1577
+ }
1578
+ return map;
1579
+ }
1580
+ function updateEnvContent(content, updates) {
1581
+ const lines = content.split("\n");
1582
+ const updatedKeys = /* @__PURE__ */ new Set();
1583
+ const newLines = lines.map((line) => {
1584
+ const trimmed = line.trim();
1585
+ if (!trimmed || trimmed.startsWith("#")) return line;
1586
+ const eqIdx = trimmed.indexOf("=");
1587
+ if (eqIdx > 0) {
1588
+ const key = trimmed.slice(0, eqIdx).trim();
1589
+ if (key in updates) {
1590
+ updatedKeys.add(key);
1591
+ return `${key}="${updates[key]}"`;
1592
+ }
1593
+ }
1594
+ return line;
1595
+ });
1596
+ for (const [k, v] of Object.entries(updates)) {
1597
+ if (!updatedKeys.has(k)) {
1598
+ newLines.push(`${k}="${v}"`);
1599
+ }
1600
+ }
1601
+ return newLines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
1602
+ }
1603
+ var HindsightAdapter = class extends AbstractToolAdapter {
1604
+ id = "hindsight";
1605
+ name = "Hindsight";
1606
+ description = "Vectorize agent memory system (retain, recall, reflect)";
1607
+ customConfigPath;
1608
+ constructor(store = defaultStore, customConfigPath) {
1609
+ super(store);
1610
+ this.customConfigPath = customConfigPath;
1611
+ }
1612
+ getConfigPath() {
1613
+ if (this.customConfigPath) {
1614
+ return this.customConfigPath;
1615
+ }
1616
+ return getHindsightConfigPath();
1617
+ }
1618
+ async isInstalled() {
1619
+ return await isExecutableInPath("hindsight") || Boolean(process.env.HINDSIGHT_API_URL) || Boolean(process.env.HINDSIGHT_API_LLM_PROVIDER);
1620
+ }
1621
+ async getStatus() {
1622
+ const configPath = this.getConfigPath();
1623
+ const installed = await this.isInstalled();
1624
+ try {
1625
+ const raw = await fs6.readFile(configPath, "utf-8");
1626
+ const envMap = parseEnvLines(raw);
1627
+ const baseUrl = envMap.get("HINDSIGHT_API_LLM_BASE_URL");
1628
+ const model = envMap.get("HINDSIGHT_API_LLM_MODEL");
1629
+ const linked = Boolean(
1630
+ baseUrl && (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1") || baseUrl.includes("srouter"))
1631
+ );
1632
+ return {
1633
+ id: this.id,
1634
+ name: this.name,
1635
+ installed,
1636
+ linked,
1637
+ configPath,
1638
+ currentBaseUrl: baseUrl,
1639
+ currentModel: model
1640
+ };
1641
+ } catch {
1642
+ return {
1643
+ id: this.id,
1644
+ name: this.name,
1645
+ installed,
1646
+ linked: false,
1647
+ configPath
1648
+ };
1649
+ }
1650
+ }
1651
+ async link(context) {
1652
+ const configPath = this.getConfigPath();
1653
+ const backupPath = context.dryRun ? void 0 : await this.store.createBackup(this.id, configPath);
1654
+ let existingContent = "";
1655
+ try {
1656
+ existingContent = await fs6.readFile(configPath, "utf-8");
1657
+ } catch {
1658
+ existingContent = "# Hindsight Environment Configuration\n";
1659
+ }
1660
+ const model = context.model || "antigravity/claude-sonnet-4-6";
1661
+ const apiKey = context.apiKey || "\xABredacted:sk-\u2026\xBB";
1662
+ const normalizedBaseUrl = context.baseUrl.replace(/\/+$/, "");
1663
+ const updates = {
1664
+ HINDSIGHT_API_LLM_PROVIDER: "openai",
1665
+ HINDSIGHT_API_LLM_BASE_URL: normalizedBaseUrl,
1666
+ HINDSIGHT_API_LLM_API_KEY: apiKey,
1667
+ HINDSIGHT_API_LLM_MODEL: model
1668
+ };
1669
+ const updatedContent = updateEnvContent(existingContent, updates);
1670
+ if (!context.dryRun) {
1671
+ await fs6.mkdir(path6.dirname(configPath), { recursive: true });
1672
+ await fs6.writeFile(configPath, updatedContent, "utf-8");
1673
+ }
1674
+ return {
1675
+ backupPath,
1676
+ modifiedPath: configPath
1677
+ };
1678
+ }
1679
+ async unlink() {
1680
+ const restored = await this.store.restoreLatestBackup(this.id);
1681
+ if (restored) {
1682
+ return true;
1683
+ }
1684
+ const configPath = this.getConfigPath();
1685
+ try {
1686
+ const raw = await fs6.readFile(configPath, "utf-8");
1687
+ const lines = raw.split("\n");
1688
+ const filtered = lines.filter((l) => {
1689
+ const trimmed = l.trim();
1690
+ return !trimmed.startsWith("HINDSIGHT_API_LLM_PROVIDER=") && !trimmed.startsWith("HINDSIGHT_API_LLM_BASE_URL=") && !trimmed.startsWith("HINDSIGHT_API_LLM_API_KEY=") && !trimmed.startsWith("HINDSIGHT_API_LLM_MODEL=");
1691
+ });
1692
+ await fs6.writeFile(configPath, filtered.join("\n"), "utf-8");
1693
+ return true;
1694
+ } catch {
1695
+ return false;
1696
+ }
1697
+ }
1698
+ getEnv(context) {
1699
+ const model = context.model || "antigravity/claude-sonnet-4-6";
1700
+ const apiKey = context.apiKey || "\xABredacted:sk-\u2026\xBB";
1701
+ const normalizedBaseUrl = context.baseUrl.replace(/\/+$/, "");
1702
+ return {
1703
+ HINDSIGHT_API_LLM_PROVIDER: "openai",
1704
+ HINDSIGHT_API_LLM_BASE_URL: normalizedBaseUrl,
1705
+ HINDSIGHT_API_LLM_API_KEY: apiKey,
1706
+ HINDSIGHT_API_LLM_MODEL: model
1707
+ };
1708
+ }
1709
+ };
1710
+
1139
1711
  // src/adapters/index.ts
1140
1712
  function createAdapters(store = defaultStore) {
1141
- return [new ClaudeAdapter(store), new OpenCodeAdapter(store)];
1713
+ return [new ClaudeAdapter(store), new OpenCodeAdapter(store), new HindsightAdapter(store)];
1142
1714
  }
1143
1715
  var cachedAdapters = null;
1144
1716
  function getAllAdapters() {
@@ -1566,17 +2138,903 @@ ${pc.yellow("No tools selected for configuration.")}`,
1566
2138
  );
1567
2139
  }
1568
2140
 
1569
- // src/commands/link.ts
1570
- async function linkCommand(toolId, options) {
1571
- const adapter = getAdapter(toolId);
1572
- if (!adapter) {
1573
- console.error(
1574
- formatError(
1575
- `Tool '${pc.bold(toolId)}' not supported. Available tools: ${getAllAdapters().map((a) => a.id).join(", ")}`
1576
- )
1577
- );
1578
- process.exitCode = 1;
1579
- return;
2141
+ // src/commands/init.ts
2142
+ import fs8 from "node:fs";
2143
+ import os6 from "node:os";
2144
+ import path8 from "node:path";
2145
+ import { spawn } from "node:child_process";
2146
+ import * as p from "@clack/prompts";
2147
+
2148
+ // ../../packages/db/dist/sqlite.js
2149
+ import path7 from "node:path";
2150
+ import fs7 from "node:fs";
2151
+ import os5 from "node:os";
2152
+ import { DatabaseSync } from "node:sqlite";
2153
+ var SROUTER_DIR = path7.join(os5.homedir(), ".srouter");
2154
+ var DEFAULT_DB_PATH = path7.join(SROUTER_DIR, "srouter.db");
2155
+ var LEGACY_DB_LOCATIONS = [
2156
+ path7.resolve(process.cwd(), "apps/api/srouter.db"),
2157
+ path7.resolve(process.cwd(), "srouter.db")
2158
+ ];
2159
+ function getDatabasePath() {
2160
+ if (process.env.DATABASE_PATH)
2161
+ return process.env.DATABASE_PATH;
2162
+ for (const legacyPath of LEGACY_DB_LOCATIONS) {
2163
+ if (fs7.existsSync(legacyPath))
2164
+ return legacyPath;
2165
+ }
2166
+ return DEFAULT_DB_PATH;
2167
+ }
2168
+ function assertNotProductionDatabaseInTests(dbPath) {
2169
+ if (process.env.NODE_ENV !== "test")
2170
+ return;
2171
+ if (path7.resolve(dbPath) === path7.resolve(DEFAULT_DB_PATH)) {
2172
+ throw new Error(`Refusing to open the production database (DATABASE_PATH points at ${DEFAULT_DB_PATH}) while NODE_ENV=test. Run tests through \`pnpm test\` so tests/setup.ts redirects to an isolated temp database.`);
2173
+ }
2174
+ }
2175
+ function openDatabase(dbPath) {
2176
+ assertNotProductionDatabaseInTests(dbPath);
2177
+ const dbDir = path7.dirname(dbPath);
2178
+ if (!fs7.existsSync(dbDir)) {
2179
+ fs7.mkdirSync(dbDir, { recursive: true });
2180
+ }
2181
+ const db2 = new DatabaseSync(dbPath);
2182
+ db2.exec("PRAGMA busy_timeout = 5000;");
2183
+ for (let i = 0; i < 5; i++) {
2184
+ try {
2185
+ db2.exec("PRAGMA journal_mode = WAL;");
2186
+ break;
2187
+ } catch (error) {
2188
+ const code = error.code;
2189
+ if (code !== "SQLITE_BUSY" || i === 4)
2190
+ throw error;
2191
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200 * (i + 1));
2192
+ }
2193
+ }
2194
+ db2.exec("PRAGMA foreign_keys = ON;");
2195
+ db2.exec("PRAGMA synchronous = NORMAL;");
2196
+ db2.exec("PRAGMA temp_store = MEMORY;");
2197
+ db2.exec("PRAGMA cache_size = -20000;");
2198
+ return db2;
2199
+ }
2200
+ var _sqliteDb = null;
2201
+ var _sqliteDbPath = null;
2202
+ function getSqliteDbInstance() {
2203
+ const dbPath = getDatabasePath();
2204
+ if (!_sqliteDb || _sqliteDbPath !== path7.resolve(dbPath)) {
2205
+ try {
2206
+ _sqliteDb?.close();
2207
+ } catch {
2208
+ }
2209
+ _sqliteDb = openDatabase(dbPath);
2210
+ _sqliteDbPath = path7.resolve(dbPath);
2211
+ }
2212
+ return _sqliteDb;
2213
+ }
2214
+ var sqliteDb = new Proxy({}, {
2215
+ get(_target, prop) {
2216
+ const instance = getSqliteDbInstance();
2217
+ const value = instance[prop];
2218
+ return typeof value === "function" ? value.bind(instance) : value;
2219
+ }
2220
+ });
2221
+
2222
+ // ../../packages/db/dist/client.js
2223
+ import {} from "node:sqlite";
2224
+ var SqliteClient = class {
2225
+ db;
2226
+ constructor(db2) {
2227
+ this.db = db2;
2228
+ }
2229
+ /** Normalize undefined → null (node:sqlite previously tolerated undefined; pg needs null). */
2230
+ normalizeParams(params) {
2231
+ return params.map((p3) => p3 === void 0 ? null : p3);
2232
+ }
2233
+ all(sql, ...params) {
2234
+ return Promise.resolve(this.db.prepare(sql).all(...this.normalizeParams(params)));
2235
+ }
2236
+ get(sql, ...params) {
2237
+ return Promise.resolve(this.db.prepare(sql).get(...this.normalizeParams(params)));
2238
+ }
2239
+ run(sql, ...params) {
2240
+ const result = this.db.prepare(sql).run(...this.normalizeParams(params));
2241
+ return Promise.resolve(result);
2242
+ }
2243
+ exec(sql) {
2244
+ this.db.exec(sql);
2245
+ return Promise.resolve();
2246
+ }
2247
+ async tableColumns(table) {
2248
+ const rows = await this.all(`PRAGMA table_info("${table}")`);
2249
+ return rows.map((r) => r.name);
2250
+ }
2251
+ };
2252
+ var pgPool = null;
2253
+ async function getPool() {
2254
+ if (pgPool)
2255
+ return pgPool;
2256
+ const { Pool } = await import("./esm-5KJBUOHT.js");
2257
+ pgPool = new Pool({
2258
+ connectionString: process.env.DATABASE_URL,
2259
+ max: 10,
2260
+ idleTimeoutMillis: 3e4,
2261
+ connectionTimeoutMillis: 1e4,
2262
+ // Heroku Postgres requires TLS; self-signed cert → rejectUnauthorized: false
2263
+ ssl: { rejectUnauthorized: false }
2264
+ });
2265
+ return pgPool;
2266
+ }
2267
+ function toPg(sql) {
2268
+ let i = 0;
2269
+ return sql.replace(/\?/g, () => `$${++i}`);
2270
+ }
2271
+ function splitStatements(sql) {
2272
+ const statements = [];
2273
+ let current = "";
2274
+ let inSingle = false;
2275
+ let inDouble = false;
2276
+ let inDollar = false;
2277
+ for (let i = 0; i < sql.length; i++) {
2278
+ const ch = sql[i];
2279
+ const next = sql[i + 1];
2280
+ if (!inDollar && !inSingle && !inDouble && ch === "'") {
2281
+ inSingle = true;
2282
+ } else if (inSingle && ch === "'" && next !== "'") {
2283
+ inSingle = false;
2284
+ } else if (!inDollar && !inSingle && !inDouble && ch === '"') {
2285
+ inDouble = true;
2286
+ } else if (inDouble && ch === '"' && next !== '"') {
2287
+ inDouble = false;
2288
+ } else if (!inSingle && !inDouble && ch === "$" && /^[a-zA-Z_0-9]*\$/.test(sql.slice(i, i + 40))) {
2289
+ inDollar = true;
2290
+ } else if (inDollar && ch === "$" && next !== "$") {
2291
+ inDollar = false;
2292
+ } else if (!inSingle && !inDouble && !inDollar && ch === ";") {
2293
+ if (current.trim())
2294
+ statements.push(current.trim());
2295
+ current = "";
2296
+ continue;
2297
+ }
2298
+ current += ch;
2299
+ }
2300
+ if (current.trim())
2301
+ statements.push(current.trim());
2302
+ return statements;
2303
+ }
2304
+ var PgClient = class {
2305
+ /** Normalize undefined → null (pg rejects undefined params). */
2306
+ normalizeParams(params) {
2307
+ return params.map((p3) => p3 === void 0 ? null : p3);
2308
+ }
2309
+ async all(sql, ...params) {
2310
+ const pool = await getPool();
2311
+ const result = await pool.query(toPg(sql), this.normalizeParams(params));
2312
+ return result.rows;
2313
+ }
2314
+ async get(sql, ...params) {
2315
+ const pool = await getPool();
2316
+ const result = await pool.query(toPg(sql), this.normalizeParams(params));
2317
+ return result.rows[0] ?? null;
2318
+ }
2319
+ async run(sql, ...params) {
2320
+ const pool = await getPool();
2321
+ const result = await pool.query(toPg(sql), this.normalizeParams(params));
2322
+ return { changes: result.rowCount ?? 0 };
2323
+ }
2324
+ async exec(sql) {
2325
+ const trimmed = sql.trim().toUpperCase();
2326
+ if (trimmed.startsWith("PRAGMA"))
2327
+ return;
2328
+ const pool = await getPool();
2329
+ const statements = splitStatements(sql);
2330
+ for (const stmt of statements) {
2331
+ await pool.query(stmt);
2332
+ }
2333
+ }
2334
+ async tableColumns(table) {
2335
+ const rows = await this.all("SELECT column_name AS name FROM information_schema.columns WHERE table_name = $1", table);
2336
+ return rows.map((r) => r.name);
2337
+ }
2338
+ };
2339
+ var _client = null;
2340
+ function getDbClient() {
2341
+ if (_client)
2342
+ return _client;
2343
+ if (process.env.DATABASE_URL) {
2344
+ _client = new PgClient();
2345
+ } else {
2346
+ _client = new SqliteClient(sqliteDb);
2347
+ }
2348
+ return _client;
2349
+ }
2350
+
2351
+ // ../../packages/db/dist/db.js
2352
+ var CompatStatement = class {
2353
+ client;
2354
+ sql;
2355
+ constructor(client, sql) {
2356
+ this.client = client;
2357
+ this.sql = sql;
2358
+ }
2359
+ all(...params) {
2360
+ return this.client.all(this.sql, ...params);
2361
+ }
2362
+ get(...params) {
2363
+ return this.client.get(this.sql, ...params);
2364
+ }
2365
+ run(...params) {
2366
+ return this.client.run(this.sql, ...params);
2367
+ }
2368
+ };
2369
+ var CompatDb = class {
2370
+ client;
2371
+ constructor(client) {
2372
+ this.client = client;
2373
+ }
2374
+ prepare(sql) {
2375
+ return new CompatStatement(this.client, sql);
2376
+ }
2377
+ exec(sql) {
2378
+ return this.client.exec(sql);
2379
+ }
2380
+ };
2381
+ function makeCompatDb() {
2382
+ return new CompatDb(getDbClient());
2383
+ }
2384
+ var db = makeCompatDb();
2385
+ sqliteDb.exec("PRAGMA busy_timeout = 5000;");
2386
+ function execWithRetry(sql, attempts = 5) {
2387
+ for (let i = 0; i < attempts; i++) {
2388
+ try {
2389
+ sqliteDb.exec(sql);
2390
+ return;
2391
+ } catch (error) {
2392
+ const code = error.code;
2393
+ if (code === "SQLITE_BUSY" && i < attempts - 1) {
2394
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200 * (i + 1));
2395
+ continue;
2396
+ }
2397
+ throw error;
2398
+ }
2399
+ }
2400
+ }
2401
+ execWithRetry("PRAGMA journal_mode = WAL;");
2402
+ execWithRetry("PRAGMA foreign_keys = ON;");
2403
+ execWithRetry("PRAGMA synchronous = NORMAL;");
2404
+ execWithRetry("PRAGMA temp_store = MEMORY;");
2405
+ execWithRetry("PRAGMA cache_size = -20000;");
2406
+ function isPostgres() {
2407
+ return Boolean(process.env.DATABASE_URL);
2408
+ }
2409
+ function tableSql(table, pg) {
2410
+ const cols = table.columns.map((c) => `${c.name} ${pg && c.definition.includes("INTEGER") ? "BIGINT" : c.definition}`).join(",\n ");
2411
+ return `CREATE TABLE IF NOT EXISTS ${table.name} (
2412
+ ${cols}
2413
+ );`;
2414
+ }
2415
+ var TABLES = [
2416
+ {
2417
+ name: "providers",
2418
+ columns: [
2419
+ { name: "id", definition: "TEXT PRIMARY KEY" },
2420
+ { name: "provider_id", definition: "TEXT NOT NULL" },
2421
+ { name: "name", definition: "TEXT NOT NULL" },
2422
+ { name: "alias", definition: "TEXT" },
2423
+ { name: "category", definition: "TEXT NOT NULL" },
2424
+ { name: "protocol", definition: "TEXT NOT NULL" },
2425
+ { name: "base_url", definition: "TEXT" },
2426
+ { name: "api_key", definition: "TEXT" },
2427
+ { name: "access_token", definition: "TEXT" },
2428
+ { name: "refresh_token", definition: "TEXT" },
2429
+ { name: "account_id", definition: "TEXT" },
2430
+ { name: "organization_id", definition: "TEXT" },
2431
+ { name: "provider_specific_data", definition: "TEXT" },
2432
+ { name: "custom_headers", definition: "TEXT" },
2433
+ { name: "token_expires_at", definition: "INTEGER" },
2434
+ { name: "last_refreshed_at", definition: "INTEGER" },
2435
+ { name: "enabled", definition: "INTEGER NOT NULL DEFAULT 1" },
2436
+ { name: "created_at", definition: "INTEGER NOT NULL" }
2437
+ ]
2438
+ },
2439
+ {
2440
+ name: "api_keys",
2441
+ columns: [
2442
+ { name: "id", definition: "TEXT PRIMARY KEY" },
2443
+ { name: "key", definition: "TEXT UNIQUE NOT NULL" },
2444
+ { name: "name", definition: "TEXT NOT NULL" },
2445
+ { name: "enabled", definition: "INTEGER NOT NULL DEFAULT 1" },
2446
+ { name: "rate_limit", definition: "INTEGER DEFAULT 0" },
2447
+ { name: "quota_limit", definition: "INTEGER DEFAULT 0" },
2448
+ { name: "usage_tokens", definition: "INTEGER DEFAULT 0" },
2449
+ { name: "credit_limit", definition: "REAL DEFAULT 0" },
2450
+ { name: "usage_cost", definition: "REAL DEFAULT 0" },
2451
+ { name: "allowed_models", definition: "TEXT" },
2452
+ { name: "created_at", definition: "INTEGER NOT NULL" }
2453
+ ]
2454
+ },
2455
+ {
2456
+ name: "request_logs",
2457
+ columns: [
2458
+ { name: "id", definition: "TEXT PRIMARY KEY" },
2459
+ { name: "api_key_id", definition: "TEXT" },
2460
+ { name: "ip_address", definition: "TEXT" },
2461
+ { name: "user_agent", definition: "TEXT" },
2462
+ { name: "provider_id", definition: "TEXT NOT NULL" },
2463
+ { name: "model", definition: "TEXT NOT NULL" },
2464
+ { name: "prompt_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
2465
+ { name: "completion_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
2466
+ { name: "total_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
2467
+ { name: "status_code", definition: "INTEGER NOT NULL" },
2468
+ { name: "latency_ms", definition: "INTEGER NOT NULL" },
2469
+ { name: "cached_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
2470
+ { name: "cache_creation_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
2471
+ { name: "reasoning_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
2472
+ { name: "estimated_cost", definition: "REAL NOT NULL DEFAULT 0" },
2473
+ { name: "fallback_occurred", definition: "INTEGER NOT NULL DEFAULT 0" },
2474
+ { name: "fallback_path", definition: "TEXT" },
2475
+ { name: "fallback_reason", definition: "TEXT" },
2476
+ { name: "resolved_model", definition: "TEXT" },
2477
+ { name: "created_at", definition: "INTEGER NOT NULL" }
2478
+ ]
2479
+ },
2480
+ {
2481
+ name: "oauth_sessions",
2482
+ columns: [
2483
+ { name: "state", definition: "TEXT PRIMARY KEY" },
2484
+ { name: "code_verifier", definition: "TEXT NOT NULL" },
2485
+ { name: "client_id", definition: "TEXT NOT NULL" },
2486
+ { name: "redirect_uri", definition: "TEXT NOT NULL" },
2487
+ { name: "created_at", definition: "INTEGER NOT NULL" }
2488
+ ]
2489
+ },
2490
+ {
2491
+ name: "fallback_rules",
2492
+ columns: [
2493
+ { name: "id", definition: "TEXT PRIMARY KEY" },
2494
+ { name: "source_model", definition: "TEXT NOT NULL" },
2495
+ { name: "target_model", definition: "TEXT NOT NULL" },
2496
+ { name: "priority", definition: "INTEGER NOT NULL DEFAULT 1" },
2497
+ { name: "enabled", definition: "INTEGER NOT NULL DEFAULT 1" },
2498
+ { name: "trigger_on_status", definition: "TEXT" },
2499
+ { name: "max_retries", definition: "INTEGER DEFAULT 1" },
2500
+ { name: "created_at", definition: "INTEGER NOT NULL" }
2501
+ ]
2502
+ },
2503
+ {
2504
+ name: "system_settings",
2505
+ columns: [
2506
+ { name: "key", definition: "TEXT PRIMARY KEY" },
2507
+ { name: "value", definition: "TEXT NOT NULL" }
2508
+ ]
2509
+ },
2510
+ {
2511
+ name: "custom_models",
2512
+ columns: [
2513
+ { name: "provider_id", definition: "TEXT NOT NULL" },
2514
+ { name: "model_id", definition: "TEXT NOT NULL" },
2515
+ { name: "created_at", definition: "INTEGER NOT NULL" },
2516
+ { name: "PRIMARY KEY (provider_id, model_id)", definition: "" }
2517
+ ]
2518
+ }
2519
+ ];
2520
+ var INDEXES = [
2521
+ { sql: "CREATE INDEX IF NOT EXISTS idx_request_logs_created_at ON request_logs(created_at DESC);" },
2522
+ { sql: "CREATE INDEX IF NOT EXISTS idx_request_logs_provider_created ON request_logs(provider_id, created_at DESC);" },
2523
+ { sql: "CREATE INDEX IF NOT EXISTS idx_request_logs_provider_model ON request_logs(provider_id, model);" },
2524
+ { sql: "CREATE INDEX IF NOT EXISTS idx_request_logs_model ON request_logs(model);" },
2525
+ { sql: "CREATE INDEX IF NOT EXISTS idx_fallback_rules_priority ON fallback_rules(priority ASC, created_at ASC);" },
2526
+ { sql: "CREATE INDEX IF NOT EXISTS idx_providers_provider_id ON providers(provider_id);" },
2527
+ { sql: "CREATE INDEX IF NOT EXISTS idx_custom_models_provider ON custom_models(provider_id, created_at ASC);" }
2528
+ ];
2529
+ var ADMIN_TABLES = (pg) => {
2530
+ const integer = pg ? "BIGINT" : "INTEGER";
2531
+ return `
2532
+ CREATE TABLE IF NOT EXISTS admin_account (
2533
+ id ${integer} PRIMARY KEY CHECK (id = 1),
2534
+ password_hash TEXT NOT NULL,
2535
+ created_at ${integer} NOT NULL,
2536
+ updated_at ${integer} NOT NULL
2537
+ );
2538
+
2539
+ CREATE TABLE IF NOT EXISTS admin_sessions (
2540
+ token_hash TEXT PRIMARY KEY,
2541
+ created_at ${integer} NOT NULL,
2542
+ expires_at ${integer} NOT NULL
2543
+ );
2544
+ `;
2545
+ };
2546
+ async function ensureColumns(table, columns) {
2547
+ const client = getDbClient();
2548
+ const existing = new Set(await client.tableColumns(table));
2549
+ for (const col of columns) {
2550
+ if (existing.has(col.name))
2551
+ continue;
2552
+ try {
2553
+ await client.exec(`ALTER TABLE ${table} ADD COLUMN ${col.definition};`);
2554
+ existing.add(col.name);
2555
+ } catch (error) {
2556
+ const message = error.message || "";
2557
+ if (!message.includes("duplicate column")) {
2558
+ throw error;
2559
+ }
2560
+ }
2561
+ }
2562
+ }
2563
+ function initDatabase() {
2564
+ if (isPostgres()) {
2565
+ return initPostgresSchema();
2566
+ }
2567
+ initSqliteSchemaSync();
2568
+ }
2569
+ function initSqliteSchemaSync() {
2570
+ const raw = sqliteDb;
2571
+ for (const table of TABLES) {
2572
+ raw.exec(tableSql(table, false));
2573
+ }
2574
+ for (const index of INDEXES) {
2575
+ raw.exec(index.sql);
2576
+ }
2577
+ raw.exec(ADMIN_TABLES(false));
2578
+ const ensureSync = (table, columns) => {
2579
+ const existing = new Set(raw.prepare(`PRAGMA table_info("${table}")`).all().map((col) => col.name));
2580
+ for (const col of columns) {
2581
+ if (existing.has(col.name))
2582
+ continue;
2583
+ try {
2584
+ raw.exec(`ALTER TABLE ${table} ADD COLUMN ${col.definition};`);
2585
+ existing.add(col.name);
2586
+ } catch (error) {
2587
+ const message = error.message || "";
2588
+ if (!message.includes("duplicate column")) {
2589
+ throw error;
2590
+ }
2591
+ }
2592
+ }
2593
+ };
2594
+ ensureSync("providers", [
2595
+ { name: "alias", definition: "alias TEXT" },
2596
+ { name: "refresh_token", definition: "refresh_token TEXT" },
2597
+ { name: "account_id", definition: "account_id TEXT" },
2598
+ { name: "provider_specific_data", definition: "provider_specific_data TEXT" },
2599
+ { name: "token_expires_at", definition: "token_expires_at INTEGER" },
2600
+ { name: "last_refreshed_at", definition: "last_refreshed_at INTEGER" },
2601
+ { name: "organization_id", definition: "organization_id TEXT" }
2602
+ ]);
2603
+ ensureSync("api_keys", [
2604
+ { name: "allowed_models", definition: "allowed_models TEXT" },
2605
+ { name: "credit_limit", definition: "credit_limit REAL DEFAULT 0" },
2606
+ { name: "usage_cost", definition: "usage_cost REAL DEFAULT 0" }
2607
+ ]);
2608
+ ensureSync("request_logs", [
2609
+ { name: "ip_address", definition: "ip_address TEXT" },
2610
+ { name: "user_agent", definition: "user_agent TEXT" },
2611
+ { name: "cached_tokens", definition: "cached_tokens INTEGER NOT NULL DEFAULT 0" },
2612
+ { name: "cache_creation_tokens", definition: "cache_creation_tokens INTEGER NOT NULL DEFAULT 0" },
2613
+ { name: "reasoning_tokens", definition: "reasoning_tokens INTEGER NOT NULL DEFAULT 0" },
2614
+ { name: "estimated_cost", definition: "estimated_cost REAL NOT NULL DEFAULT 0" },
2615
+ { name: "fallback_occurred", definition: "fallback_occurred INTEGER NOT NULL DEFAULT 0" },
2616
+ { name: "fallback_path", definition: "fallback_path TEXT" },
2617
+ { name: "fallback_reason", definition: "fallback_reason TEXT" },
2618
+ { name: "resolved_model", definition: "resolved_model TEXT" }
2619
+ ]);
2620
+ }
2621
+ async function initPostgresSchema() {
2622
+ const client = getDbClient();
2623
+ for (const table of TABLES) {
2624
+ await client.exec(tableSql(table, true));
2625
+ }
2626
+ for (const index of INDEXES) {
2627
+ await client.exec(index.sql);
2628
+ }
2629
+ await client.exec(ADMIN_TABLES(true));
2630
+ await ensureColumns("providers", [
2631
+ { name: "alias", definition: "alias TEXT" },
2632
+ { name: "refresh_token", definition: "refresh_token TEXT" },
2633
+ { name: "account_id", definition: "account_id TEXT" },
2634
+ { name: "provider_specific_data", definition: "provider_specific_data TEXT" },
2635
+ { name: "token_expires_at", definition: "token_expires_at INTEGER" },
2636
+ { name: "last_refreshed_at", definition: "last_refreshed_at INTEGER" },
2637
+ { name: "organization_id", definition: "organization_id TEXT" }
2638
+ ]);
2639
+ await ensureColumns("api_keys", [
2640
+ { name: "allowed_models", definition: "allowed_models TEXT" },
2641
+ { name: "credit_limit", definition: "credit_limit REAL DEFAULT 0" },
2642
+ { name: "usage_cost", definition: "usage_cost REAL DEFAULT 0" }
2643
+ ]);
2644
+ await ensureColumns("request_logs", [
2645
+ { name: "ip_address", definition: "ip_address TEXT" },
2646
+ { name: "user_agent", definition: "user_agent TEXT" },
2647
+ { name: "cached_tokens", definition: "cached_tokens INTEGER NOT NULL DEFAULT 0" },
2648
+ { name: "cache_creation_tokens", definition: "cache_creation_tokens INTEGER NOT NULL DEFAULT 0" },
2649
+ { name: "reasoning_tokens", definition: "reasoning_tokens INTEGER NOT NULL DEFAULT 0" },
2650
+ { name: "estimated_cost", definition: "estimated_cost REAL NOT NULL DEFAULT 0" },
2651
+ { name: "fallback_occurred", definition: "fallback_occurred INTEGER NOT NULL DEFAULT 0" },
2652
+ { name: "fallback_path", definition: "fallback_path TEXT" },
2653
+ { name: "fallback_reason", definition: "fallback_reason TEXT" },
2654
+ { name: "resolved_model", definition: "resolved_model TEXT" }
2655
+ ]);
2656
+ }
2657
+ if (!isPostgres()) {
2658
+ initDatabase();
2659
+ }
2660
+
2661
+ // ../../packages/db/dist/row-utils.js
2662
+ function str(value, fallback = "") {
2663
+ return value === null || value === void 0 ? fallback : String(value);
2664
+ }
2665
+ function num(value, fallback = 0) {
2666
+ const n = Number(value);
2667
+ return Number.isFinite(n) ? n : fallback;
2668
+ }
2669
+
2670
+ // ../../packages/db/dist/adminAuth.js
2671
+ var AdminAuthStore = class {
2672
+ initialized = false;
2673
+ client;
2674
+ /** Accept a DbClient for isolation (tests use :memory: SqliteClient); defaults to global. */
2675
+ constructor(client) {
2676
+ this.client = client ?? getDbClient();
2677
+ }
2678
+ async ensureTables() {
2679
+ if (this.initialized)
2680
+ return;
2681
+ const pg = Boolean(process.env.DATABASE_URL);
2682
+ const integer = pg ? "BIGINT" : "INTEGER";
2683
+ await this.client.exec(`
2684
+ CREATE TABLE IF NOT EXISTS admin_account (
2685
+ id ${integer} PRIMARY KEY CHECK (id = 1),
2686
+ password_hash TEXT NOT NULL,
2687
+ created_at ${integer} NOT NULL,
2688
+ updated_at ${integer} NOT NULL
2689
+ );
2690
+
2691
+ CREATE TABLE IF NOT EXISTS admin_sessions (
2692
+ token_hash TEXT PRIMARY KEY,
2693
+ created_at ${integer} NOT NULL,
2694
+ expires_at ${integer} NOT NULL
2695
+ );
2696
+ `);
2697
+ this.initialized = true;
2698
+ }
2699
+ async hasAdminAccount() {
2700
+ await this.ensureTables();
2701
+ const Row = await this.client.get("SELECT 1 AS present FROM admin_account WHERE id = 1");
2702
+ return Boolean(Row);
2703
+ }
2704
+ async createAdminAccount(passwordHash, now = Date.now()) {
2705
+ await this.ensureTables();
2706
+ const Result = await this.client.run(`INSERT INTO admin_account (id, password_hash, created_at, updated_at)
2707
+ VALUES (1, ?, ?, ?)
2708
+ ON CONFLICT(id) DO NOTHING`, passwordHash, now, now);
2709
+ return num(Result.changes) > 0;
2710
+ }
2711
+ async getPasswordHash() {
2712
+ await this.ensureTables();
2713
+ const Row = await this.client.get("SELECT password_hash FROM admin_account WHERE id = 1");
2714
+ return Row?.password_hash ? str(Row.password_hash) : null;
2715
+ }
2716
+ async updatePasswordHash(passwordHash, now = Date.now()) {
2717
+ await this.ensureTables();
2718
+ const Result = await this.client.run(`UPDATE admin_account
2719
+ SET password_hash = ?, updated_at = ?
2720
+ WHERE id = 1`, passwordHash, now);
2721
+ return num(Result.changes) > 0;
2722
+ }
2723
+ async createSession(tokenHash, createdAt, expiresAt) {
2724
+ await this.ensureTables();
2725
+ await this.client.run(`INSERT INTO admin_sessions (token_hash, created_at, expires_at)
2726
+ VALUES (?, ?, ?)`, tokenHash, createdAt, expiresAt);
2727
+ }
2728
+ async getSession(tokenHash, now = Date.now()) {
2729
+ await this.ensureTables();
2730
+ await this.client.run("DELETE FROM admin_sessions WHERE expires_at <= ?", now);
2731
+ const Row = await this.client.get(`SELECT token_hash, created_at, expires_at
2732
+ FROM admin_sessions
2733
+ WHERE token_hash = ? AND expires_at > ?`, tokenHash, now);
2734
+ if (!Row)
2735
+ return null;
2736
+ return {
2737
+ tokenHash: str(Row.token_hash),
2738
+ createdAt: num(Row.created_at),
2739
+ expiresAt: num(Row.expires_at)
2740
+ };
2741
+ }
2742
+ async deleteSession(tokenHash) {
2743
+ await this.ensureTables();
2744
+ const Result = await this.client.run("DELETE FROM admin_sessions WHERE token_hash = ?", tokenHash);
2745
+ return num(Result.changes) > 0;
2746
+ }
2747
+ };
2748
+ var adminAuthStore = new AdminAuthStore();
2749
+
2750
+ // src/commands/init.ts
2751
+ var DEFAULT_PORT = "3000";
2752
+ var REPO_URL = "https://github.com/seaavey/SRouter.git";
2753
+ var DOCKER_IMAGE = "ghcr.io/seaavey/srouter:latest";
2754
+ function runProcess(command, args, options) {
2755
+ return new Promise((resolve) => {
2756
+ const child = spawn(command, args, {
2757
+ cwd: options?.cwd,
2758
+ stdio: options?.stdio ?? "inherit"
2759
+ });
2760
+ let stdout = "";
2761
+ let stderr = "";
2762
+ if (options?.stdio === "pipe") {
2763
+ child.stdout?.on("data", (d) => stdout += d.toString());
2764
+ child.stderr?.on("data", (d) => stderr += d.toString());
2765
+ }
2766
+ child.on("close", (code) => {
2767
+ resolve({ code: code ?? 0, stdout, stderr });
2768
+ });
2769
+ child.on("error", (err) => {
2770
+ resolve({ code: 1, stdout, stderr: err.message });
2771
+ });
2772
+ });
2773
+ }
2774
+ async function initDocker(port, detached) {
2775
+ const s = p.spinner();
2776
+ s.start("Checking Docker environment");
2777
+ const hasDocker = await isExecutableInPath("docker");
2778
+ if (!hasDocker) {
2779
+ s.stop(formatError("Docker is not installed or not available in PATH."));
2780
+ const fallbackChoice = await p.select({
2781
+ message: "Docker was not found. What would you like to do?",
2782
+ options: [
2783
+ {
2784
+ value: "source",
2785
+ label: "Run from Source Code instead (Node.js & pnpm)",
2786
+ hint: "Clones repo and builds locally without Docker"
2787
+ },
2788
+ {
2789
+ value: "guide",
2790
+ label: "View Docker installation instructions",
2791
+ hint: "Get download link for your OS"
2792
+ },
2793
+ {
2794
+ value: "exit",
2795
+ label: "Exit",
2796
+ hint: "Cancel initialization"
2797
+ }
2798
+ ]
2799
+ });
2800
+ if (p.isCancel(fallbackChoice) || fallbackChoice === "exit") {
2801
+ p.outro("Initialization cancelled. Install Docker from https://docs.docker.com/get-docker/ and try again.");
2802
+ return;
2803
+ }
2804
+ if (fallbackChoice === "guide") {
2805
+ p.log.message(
2806
+ [
2807
+ "\u{1F4E6} " + pc.bold("How to install Docker:"),
2808
+ `\u2022 Windows / macOS: ${pc.cyan("https://www.docker.com/products/docker-desktop/")}`,
2809
+ `\u2022 Linux (Ubuntu/Debian): ${pc.yellow("curl -fsSL https://get.docker.com | sh")}`,
2810
+ `\u2022 Linux (Arch): ${pc.yellow("sudo pacman -S docker && sudo systemctl enable --now docker")}`
2811
+ ].join("\n")
2812
+ );
2813
+ p.outro("Run 'srouter init' again after installing Docker.");
2814
+ return;
2815
+ }
2816
+ if (fallbackChoice === "source") {
2817
+ const defaultDir = path8.join(os6.homedir(), "srouter");
2818
+ await initSource(defaultDir, port);
2819
+ return;
2820
+ }
2821
+ }
2822
+ s.message("Verifying Docker daemon status");
2823
+ const checkDaemon = await runProcess("docker", ["info"], { stdio: "pipe" });
2824
+ if (checkDaemon.code !== 0) {
2825
+ s.stop(formatError("Docker daemon is not running."));
2826
+ p.log.info("Please start the Docker service/daemon and try again.");
2827
+ process.exitCode = 1;
2828
+ return;
2829
+ }
2830
+ const dataDir = path8.join(SROUTER_DIR, "data");
2831
+ fs8.mkdirSync(dataDir, { recursive: true, mode: 448 });
2832
+ s.message(`Pulling latest SRouter Docker image (${DOCKER_IMAGE})`);
2833
+ const pullRes = await runProcess("docker", ["pull", DOCKER_IMAGE], { stdio: "inherit" });
2834
+ if (pullRes.code !== 0) {
2835
+ s.stop(formatWarning("Failed to pull image from registry, checking local image cache"));
2836
+ }
2837
+ await runProcess("docker", ["rm", "-f", "srouter"], { stdio: "pipe" });
2838
+ s.message(`Starting SRouter Gateway container on port ${port}`);
2839
+ const dockerArgs = [
2840
+ "run",
2841
+ detached ? "-d" : "-it",
2842
+ "--name",
2843
+ "srouter",
2844
+ "--restart",
2845
+ "unless-stopped",
2846
+ "-p",
2847
+ `${port}:3000`,
2848
+ "-p",
2849
+ "1455:1455",
2850
+ "-v",
2851
+ `${dataDir}:/app/data`,
2852
+ "-e",
2853
+ `PORT=${port}`,
2854
+ "-e",
2855
+ "OAUTH_PORT=1455",
2856
+ "-e",
2857
+ "DATABASE_PATH=/app/data/srouter.db",
2858
+ DOCKER_IMAGE
2859
+ ];
2860
+ if (detached) {
2861
+ const runRes = await runProcess("docker", dockerArgs, { stdio: "pipe" });
2862
+ if (runRes.code !== 0) {
2863
+ s.stop(formatError("Failed to start SRouter Docker container."));
2864
+ p.log.error(runRes.stderr);
2865
+ process.exitCode = 1;
2866
+ return;
2867
+ }
2868
+ s.stop(formatSuccess("SRouter Docker container started successfully!"));
2869
+ p.log.message(
2870
+ [
2871
+ `\u{1F680} Gateway URL: ${pc.bold(pc.cyan(`http://localhost:${port}`))}`,
2872
+ `\u{1F4CA} Web Dashboard: ${pc.bold(pc.cyan(`http://localhost:${port}`))}`,
2873
+ `\u{1F4BE} Persistent Data: ${pc.dim(dataDir)}`,
2874
+ "",
2875
+ `To inspect logs: ${pc.yellow("docker logs -f srouter")}`,
2876
+ `To stop: ${pc.yellow("docker stop srouter")}`
2877
+ ].join("\n")
2878
+ );
2879
+ p.outro("SRouter is ready to use!");
2880
+ } else {
2881
+ s.stop("Launching container in foreground mode...");
2882
+ await runProcess("docker", dockerArgs, { stdio: "inherit" });
2883
+ }
2884
+ }
2885
+ async function initSource(targetDir, port) {
2886
+ const s = p.spinner();
2887
+ s.start("Checking prerequisites (git, node, pnpm)");
2888
+ const hasGit = await isExecutableInPath("git");
2889
+ const hasPnpm = await isExecutableInPath("pnpm");
2890
+ if (!hasGit) {
2891
+ s.stop(formatError("Git is required to clone SRouter repository."));
2892
+ process.exitCode = 1;
2893
+ return;
2894
+ }
2895
+ const resolvedDir = path8.resolve(targetDir);
2896
+ if (!fs8.existsSync(path8.join(resolvedDir, "package.json"))) {
2897
+ s.message(`Cloning SRouter into ${pc.bold(resolvedDir)}`);
2898
+ fs8.mkdirSync(resolvedDir, { recursive: true });
2899
+ const cloneRes = await runProcess("git", ["clone", REPO_URL, resolvedDir], {
2900
+ stdio: "inherit"
2901
+ });
2902
+ if (cloneRes.code !== 0) {
2903
+ s.stop(formatError("Failed to clone SRouter repository."));
2904
+ process.exitCode = 1;
2905
+ return;
2906
+ }
2907
+ } else {
2908
+ p.log.info(`Using existing SRouter repository at ${pc.bold(resolvedDir)}`);
2909
+ }
2910
+ s.message("Installing dependencies with pnpm");
2911
+ const pnpmCmd = hasPnpm ? "pnpm" : "npx";
2912
+ const installArgs = hasPnpm ? ["install"] : ["pnpm", "install"];
2913
+ const installRes = await runProcess(pnpmCmd, installArgs, {
2914
+ cwd: resolvedDir,
2915
+ stdio: "inherit"
2916
+ });
2917
+ if (installRes.code !== 0) {
2918
+ s.stop(formatError("Dependency installation failed."));
2919
+ process.exitCode = 1;
2920
+ return;
2921
+ }
2922
+ s.message("Building SRouter packages, API, and Dashboard");
2923
+ const buildArgs = hasPnpm ? ["run", "build"] : ["pnpm", "run", "build"];
2924
+ const buildRes = await runProcess(pnpmCmd, buildArgs, {
2925
+ cwd: resolvedDir,
2926
+ stdio: "inherit"
2927
+ });
2928
+ if (buildRes.code !== 0) {
2929
+ s.stop(formatError("Build step failed."));
2930
+ process.exitCode = 1;
2931
+ return;
2932
+ }
2933
+ s.stop(formatSuccess("SRouter built successfully!"));
2934
+ p.log.message(
2935
+ [
2936
+ `\u{1F4C1} Project Directory: ${pc.bold(resolvedDir)}`,
2937
+ `\u{1F680} Start Dev Server: ${pc.yellow(`cd ${resolvedDir} && pnpm dev`)}`,
2938
+ `\u26A1 Start Production: ${pc.yellow(`PORT=${port} cd ${resolvedDir}/apps/api && pnpm start`)}`
2939
+ ].join("\n")
2940
+ );
2941
+ const startNow = await p.confirm({
2942
+ message: "Start SRouter API & Web server now?",
2943
+ initialValue: true
2944
+ });
2945
+ if (startNow === true) {
2946
+ p.outro(`Starting SRouter Gateway on port ${port}...`);
2947
+ process.env.PORT = port;
2948
+ const startArgs = hasPnpm ? ["--filter", "api", "start"] : ["pnpm", "--filter", "api", "start"];
2949
+ await runProcess(pnpmCmd, startArgs, {
2950
+ cwd: resolvedDir,
2951
+ stdio: "inherit"
2952
+ });
2953
+ } else {
2954
+ p.outro("Setup complete! You can start SRouter anytime.");
2955
+ }
2956
+ }
2957
+ async function initCommand(options) {
2958
+ p.intro(pc.bold(pc.cyan("\u26A1 SRouter Gateway Initialization")));
2959
+ let mode = options.mode;
2960
+ if (!mode) {
2961
+ const choice = await p.select({
2962
+ message: "How would you like to run SRouter?",
2963
+ options: [
2964
+ {
2965
+ value: "docker",
2966
+ label: "Docker (Recommended)",
2967
+ hint: "Zero-config container with isolated environment & persistent database"
2968
+ },
2969
+ {
2970
+ value: "source",
2971
+ label: "Source Code (Node.js & pnpm)",
2972
+ hint: "Clone repository, build from source, and run natively"
2973
+ }
2974
+ ]
2975
+ });
2976
+ if (p.isCancel(choice)) {
2977
+ p.outro("Initialization cancelled.");
2978
+ return;
2979
+ }
2980
+ mode = choice;
2981
+ }
2982
+ let port = options.port;
2983
+ if (!port && !options.yes) {
2984
+ const inputPort = await p.text({
2985
+ message: "Gateway Port:",
2986
+ defaultValue: DEFAULT_PORT,
2987
+ placeholder: DEFAULT_PORT,
2988
+ validate: (val) => {
2989
+ const n = Number(val);
2990
+ if (Number.isNaN(n) || n <= 0 || n > 65535) {
2991
+ return "Please enter a valid port number (1-65535).";
2992
+ }
2993
+ }
2994
+ });
2995
+ if (p.isCancel(inputPort)) {
2996
+ p.outro("Initialization cancelled.");
2997
+ return;
2998
+ }
2999
+ port = inputPort || DEFAULT_PORT;
3000
+ } else {
3001
+ port = port || DEFAULT_PORT;
3002
+ }
3003
+ if (mode === "docker") {
3004
+ const detached = options.detached ?? true;
3005
+ await initDocker(port, detached);
3006
+ } else {
3007
+ const defaultDir = path8.join(os6.homedir(), "srouter");
3008
+ let targetDir = options.dir;
3009
+ if (!targetDir && !options.yes) {
3010
+ const inputDir = await p.text({
3011
+ message: "Target directory for SRouter source code:",
3012
+ defaultValue: defaultDir,
3013
+ placeholder: defaultDir
3014
+ });
3015
+ if (p.isCancel(inputDir)) {
3016
+ p.outro("Initialization cancelled.");
3017
+ return;
3018
+ }
3019
+ targetDir = inputDir || defaultDir;
3020
+ } else {
3021
+ targetDir = targetDir || defaultDir;
3022
+ }
3023
+ await initSource(targetDir, port);
3024
+ }
3025
+ }
3026
+
3027
+ // src/commands/link.ts
3028
+ async function linkCommand(toolId, options) {
3029
+ const adapter = getAdapter(toolId);
3030
+ if (!adapter) {
3031
+ console.error(
3032
+ formatError(
3033
+ `Tool '${pc.bold(toolId)}' not supported. Available tools: ${getAllAdapters().map((a) => a.id).join(", ")}`
3034
+ )
3035
+ );
3036
+ process.exitCode = 1;
3037
+ return;
1580
3038
  }
1581
3039
  const savedConfig = await defaultStore.loadConfig();
1582
3040
  const baseUrl = options.url || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
@@ -1848,7 +3306,7 @@ async function envCommand(toolId, options = {}) {
1848
3306
  }
1849
3307
 
1850
3308
  // src/commands/run.ts
1851
- import { spawn } from "node:child_process";
3309
+ import { spawn as spawn2 } from "node:child_process";
1852
3310
  async function runCommand(toolId, toolArgs, options = {}) {
1853
3311
  const adapter = getAdapter(toolId);
1854
3312
  if (!adapter) {
@@ -1876,7 +3334,7 @@ async function runCommand(toolId, toolArgs, options = {}) {
1876
3334
  haikuModel
1877
3335
  });
1878
3336
  const binaryName = toolId === "claude" ? "claude" : "opencode";
1879
- const child = spawn(binaryName, toolArgs, {
3337
+ const child = spawn2(binaryName, toolArgs, {
1880
3338
  stdio: "inherit",
1881
3339
  env: {
1882
3340
  ...process.env,
@@ -1903,412 +3361,437 @@ async function runCommand(toolId, toolArgs, options = {}) {
1903
3361
  }
1904
3362
 
1905
3363
  // src/commands/migrate.ts
1906
- import fs6 from "node:fs";
1907
- import path6 from "node:path";
1908
- import * as p from "@clack/prompts";
3364
+ import fs9 from "node:fs";
3365
+ import os7 from "node:os";
3366
+ import path9 from "node:path";
3367
+ import * as p2 from "@clack/prompts";
1909
3368
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
1910
-
1911
- // ../../packages/db/dist/db.js
1912
- import { DatabaseSync } from "node:sqlite";
1913
- import path5 from "node:path";
1914
- import fs5 from "node:fs";
1915
- import os4 from "node:os";
1916
- var SROUTER_DIR = path5.join(os4.homedir(), ".srouter");
1917
- var DEFAULT_DB_PATH = path5.join(SROUTER_DIR, "srouter.db");
1918
- var LEGACY_DB_LOCATIONS = [
1919
- path5.resolve(process.cwd(), "apps/api/srouter.db"),
1920
- path5.resolve(process.cwd(), "srouter.db")
1921
- ];
1922
- function getDatabasePath() {
1923
- if (process.env.DATABASE_PATH)
1924
- return process.env.DATABASE_PATH;
1925
- for (const legacyPath of LEGACY_DB_LOCATIONS) {
1926
- if (fs5.existsSync(legacyPath))
1927
- return legacyPath;
3369
+ function parseDateToTimestamp(val) {
3370
+ if (typeof val === "number" && Number.isFinite(val)) return val;
3371
+ if (typeof val === "string") {
3372
+ const parsed = Date.parse(val);
3373
+ if (!Number.isNaN(parsed)) return parsed;
1928
3374
  }
1929
- return DEFAULT_DB_PATH;
3375
+ return Date.now();
1930
3376
  }
1931
- var dbPath = getDatabasePath();
1932
- var dbDir = path5.dirname(dbPath);
1933
- if (!fs5.existsSync(dbDir)) {
1934
- fs5.mkdirSync(dbDir, { recursive: true });
3377
+ function map9RouterProtocol(apiType, type) {
3378
+ if (apiType === "anthropic" || type === "anthropic" || type?.includes("anthropic")) {
3379
+ return "anthropic";
3380
+ }
3381
+ return "openai";
1935
3382
  }
1936
- var db = new DatabaseSync(dbPath);
1937
- db.exec("PRAGMA busy_timeout = 5000;");
1938
- function execWithRetry(sql, attempts = 5) {
1939
- for (let i = 0; i < attempts; i++) {
1940
- try {
1941
- db.exec(sql);
1942
- return;
1943
- } catch (error) {
1944
- const code = error.code;
1945
- if (code === "SQLITE_BUSY" && i < attempts - 1) {
1946
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200 * (i + 1));
1947
- continue;
1948
- }
1949
- throw error;
3383
+ function importNineRouterJson(data, targetDb, action) {
3384
+ const nodesMap = /* @__PURE__ */ new Map();
3385
+ if (Array.isArray(data.providerNodes)) {
3386
+ for (const node of data.providerNodes) {
3387
+ nodesMap.set(node.id, node);
1950
3388
  }
1951
3389
  }
1952
- }
1953
- execWithRetry("PRAGMA journal_mode = WAL;");
1954
- execWithRetry("PRAGMA foreign_keys = ON;");
1955
- function ensureColumns(table, columns) {
1956
- const existing = new Set(db.prepare(`PRAGMA table_info("${table}")`).all().map((col) => col.name));
1957
- for (const col of columns) {
1958
- if (existing.has(col.name))
1959
- continue;
3390
+ if (action !== "copy") {
1960
3391
  try {
1961
- db.exec(`ALTER TABLE ${table} ADD COLUMN ${col.definition};`);
1962
- existing.add(col.name);
1963
- } catch (error) {
1964
- const message = error.message || "";
1965
- if (!message.includes("duplicate column name")) {
1966
- throw error;
1967
- }
3392
+ targetDb.prepare('DELETE FROM "providers"').run();
3393
+ targetDb.prepare('DELETE FROM "api_keys"').run();
3394
+ targetDb.prepare('DELETE FROM "custom_models"').run();
3395
+ } catch {
1968
3396
  }
1969
3397
  }
1970
- }
1971
- function initDatabase() {
1972
- db.exec(`
1973
- CREATE TABLE IF NOT EXISTS providers (
1974
- id TEXT PRIMARY KEY,
1975
- provider_id TEXT NOT NULL,
1976
- name TEXT NOT NULL,
1977
- category TEXT NOT NULL,
1978
- protocol TEXT NOT NULL,
1979
- base_url TEXT,
1980
- api_key TEXT,
1981
- access_token TEXT,
1982
- refresh_token TEXT,
1983
- custom_headers TEXT,
1984
- enabled INTEGER NOT NULL DEFAULT 1,
1985
- created_at INTEGER NOT NULL
1986
- );
1987
- `);
1988
- ensureColumns("providers", [
1989
- { name: "refresh_token", definition: "refresh_token TEXT" },
1990
- // Multi-account OAuth binding (e.g. Codex ChatGPT-Account-ID)
1991
- { name: "account_id", definition: "account_id TEXT" },
1992
- // Provider-specific metadata (for example Kiro auth method/region/profile ARN)
1993
- { name: "provider_specific_data", definition: "provider_specific_data TEXT" },
1994
- // Token expiry tracking
1995
- { name: "token_expires_at", definition: "token_expires_at INTEGER" },
1996
- { name: "last_refreshed_at", definition: "last_refreshed_at INTEGER" },
1997
- // Claude OAuth organization binding
1998
- { name: "organization_id", definition: "organization_id TEXT" }
1999
- ]);
2000
- db.exec(`
2001
- CREATE TABLE IF NOT EXISTS api_keys (
2002
- id TEXT PRIMARY KEY,
2003
- key TEXT UNIQUE NOT NULL,
2004
- name TEXT NOT NULL,
2005
- enabled INTEGER NOT NULL DEFAULT 1,
2006
- rate_limit INTEGER DEFAULT 0,
2007
- quota_limit INTEGER DEFAULT 0,
2008
- usage_tokens INTEGER DEFAULT 0,
2009
- created_at INTEGER NOT NULL
2010
- );
2011
- `);
2012
- db.exec(`
2013
- CREATE TABLE IF NOT EXISTS request_logs (
2014
- id TEXT PRIMARY KEY,
2015
- api_key_id TEXT,
2016
- provider_id TEXT NOT NULL,
2017
- model TEXT NOT NULL,
2018
- prompt_tokens INTEGER NOT NULL DEFAULT 0,
2019
- completion_tokens INTEGER NOT NULL DEFAULT 0,
2020
- total_tokens INTEGER NOT NULL DEFAULT 0,
2021
- status_code INTEGER NOT NULL,
2022
- latency_ms INTEGER NOT NULL,
2023
- created_at INTEGER NOT NULL
2024
- );
2025
- `);
2026
- db.exec(`
2027
- CREATE TABLE IF NOT EXISTS oauth_sessions (
2028
- state TEXT PRIMARY KEY,
2029
- code_verifier TEXT NOT NULL,
2030
- client_id TEXT NOT NULL,
2031
- redirect_uri TEXT NOT NULL,
2032
- created_at INTEGER NOT NULL
2033
- );
2034
- `);
2035
- ensureColumns("request_logs", [
2036
- { name: "cached_tokens", definition: "cached_tokens INTEGER NOT NULL DEFAULT 0" },
2037
- {
2038
- name: "cache_creation_tokens",
2039
- definition: "cache_creation_tokens INTEGER NOT NULL DEFAULT 0"
2040
- },
2041
- { name: "reasoning_tokens", definition: "reasoning_tokens INTEGER NOT NULL DEFAULT 0" },
2042
- { name: "estimated_cost", definition: "estimated_cost REAL NOT NULL DEFAULT 0" },
2043
- { name: "fallback_occurred", definition: "fallback_occurred INTEGER NOT NULL DEFAULT 0" },
2044
- { name: "fallback_path", definition: "fallback_path TEXT" },
2045
- { name: "fallback_reason", definition: "fallback_reason TEXT" },
2046
- { name: "resolved_model", definition: "resolved_model TEXT" }
2047
- ]);
2048
- db.exec(`
2049
- CREATE TABLE IF NOT EXISTS fallback_rules (
2050
- id TEXT PRIMARY KEY,
2051
- source_model TEXT NOT NULL,
2052
- target_model TEXT NOT NULL,
2053
- priority INTEGER NOT NULL DEFAULT 1,
2054
- enabled INTEGER NOT NULL DEFAULT 1,
2055
- trigger_on_status TEXT,
2056
- max_retries INTEGER DEFAULT 1,
2057
- created_at INTEGER NOT NULL
3398
+ let inserted = 0;
3399
+ let skipped = 0;
3400
+ let tablesCount = 0;
3401
+ if (Array.isArray(data.providerConnections) && data.providerConnections.length > 0) {
3402
+ tablesCount++;
3403
+ const insertProvider = targetDb.prepare(`
3404
+ INSERT OR REPLACE INTO providers (
3405
+ id, provider_id, name, category, protocol, base_url, api_key, access_token,
3406
+ refresh_token, account_id, organization_id, token_expires_at, last_refreshed_at,
3407
+ custom_headers, provider_specific_data, enabled, created_at
3408
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3409
+ `);
3410
+ for (const conn of data.providerConnections) {
3411
+ try {
3412
+ const node = nodesMap.get(conn.provider);
3413
+ const providerId = node ? node.name.toLowerCase() : conn.provider;
3414
+ const protocol = node ? map9RouterProtocol(node.apiType, node.type) : "openai";
3415
+ const baseUrl = node?.baseUrl ?? conn.providerSpecificData?.baseUrl ?? null;
3416
+ const apiKey = conn.apiKey ?? null;
3417
+ const accessToken = conn.accessToken ?? conn.access_token ?? null;
3418
+ const refreshToken = conn.refreshToken ?? conn.refresh_token ?? null;
3419
+ const tokenExpiresAt = conn.tokenExpiresAt ?? (conn.expiresAt ? parseDateToTimestamp(conn.expiresAt) : null);
3420
+ const lastRefreshedAt = conn.lastRefreshedAt ?? (conn.lastRefreshAt ? parseDateToTimestamp(conn.lastRefreshAt) : null);
3421
+ const accountId = conn.providerSpecificData?.chatgptAccountId ?? conn.providerSpecificData?.accountId ?? null;
3422
+ const orgId = conn.providerSpecificData?.organizationId ?? null;
3423
+ const enabled = conn.isActive === false ? 0 : 1;
3424
+ const createdAt = parseDateToTimestamp(conn.createdAt);
3425
+ insertProvider.run(
3426
+ conn.id,
3427
+ providerId,
3428
+ conn.name || providerId,
3429
+ "general",
3430
+ protocol,
3431
+ baseUrl,
3432
+ apiKey,
3433
+ accessToken,
3434
+ refreshToken,
3435
+ accountId,
3436
+ orgId,
3437
+ tokenExpiresAt,
3438
+ lastRefreshedAt,
3439
+ null,
3440
+ conn.providerSpecificData ? JSON.stringify(conn.providerSpecificData) : null,
3441
+ enabled,
3442
+ createdAt
2058
3443
  );
2059
- `);
2060
- db.exec(`
2061
- CREATE TABLE IF NOT EXISTS system_settings (
2062
- key TEXT PRIMARY KEY,
2063
- value TEXT NOT NULL
3444
+ inserted++;
3445
+ } catch {
3446
+ skipped++;
3447
+ }
3448
+ }
3449
+ }
3450
+ if (Array.isArray(data.apiKeys) && data.apiKeys.length > 0) {
3451
+ tablesCount++;
3452
+ const insertKey = targetDb.prepare(`
3453
+ INSERT OR REPLACE INTO api_keys (
3454
+ id, key, name, enabled, rate_limit, quota_limit, usage_tokens, created_at
3455
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3456
+ `);
3457
+ for (const keyItem of data.apiKeys) {
3458
+ try {
3459
+ insertKey.run(
3460
+ keyItem.id || `key_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
3461
+ keyItem.key,
3462
+ keyItem.name || "Default Key",
3463
+ keyItem.isActive === false ? 0 : 1,
3464
+ 0,
3465
+ 0,
3466
+ 0,
3467
+ parseDateToTimestamp(keyItem.createdAt)
2064
3468
  );
2065
- `);
2066
- db.exec(`
2067
- CREATE TABLE IF NOT EXISTS custom_models (
2068
- provider_id TEXT NOT NULL,
2069
- model_id TEXT NOT NULL,
2070
- created_at INTEGER NOT NULL,
2071
- PRIMARY KEY (provider_id, model_id)
3469
+ inserted++;
3470
+ } catch {
3471
+ skipped++;
3472
+ }
3473
+ }
3474
+ }
3475
+ if (Array.isArray(data.customModels) && data.customModels.length > 0) {
3476
+ tablesCount++;
3477
+ const insertCustomModel = targetDb.prepare(`
3478
+ INSERT OR IGNORE INTO custom_models (
3479
+ provider_id, model_id, created_at
3480
+ ) VALUES (?, ?, ?)
3481
+ `);
3482
+ for (const model of data.customModels) {
3483
+ try {
3484
+ insertCustomModel.run(
3485
+ model.providerAlias || "default",
3486
+ model.id || model.name || "unknown",
3487
+ Date.now()
2072
3488
  );
2073
- `);
3489
+ inserted++;
3490
+ } catch {
3491
+ skipped++;
3492
+ }
3493
+ }
3494
+ }
3495
+ return { inserted, skipped, tablesCount };
2074
3496
  }
2075
- initDatabase();
2076
-
2077
- // ../../packages/db/dist/row-utils.js
2078
- function str(value, fallback = "") {
2079
- return value === null || value === void 0 ? fallback : String(value);
3497
+ function getTargetDbPath() {
3498
+ return getDatabasePath();
2080
3499
  }
2081
- function num(value, fallback = 0) {
2082
- const n = Number(value);
2083
- return Number.isFinite(n) ? n : fallback;
3500
+ function getBackupDir(targetDbPath) {
3501
+ return path9.join(path9.dirname(targetDbPath), "backups");
2084
3502
  }
2085
-
2086
- // ../../packages/db/dist/adminAuth.js
2087
- var AdminAuthStore = class {
2088
- database;
2089
- constructor(database = db) {
2090
- this.database = database;
2091
- this.database.exec(`
2092
- CREATE TABLE IF NOT EXISTS admin_account (
2093
- id INTEGER PRIMARY KEY CHECK (id = 1),
2094
- password_hash TEXT NOT NULL,
2095
- created_at INTEGER NOT NULL,
2096
- updated_at INTEGER NOT NULL
2097
- );
2098
-
2099
- CREATE TABLE IF NOT EXISTS admin_sessions (
2100
- token_hash TEXT PRIMARY KEY,
2101
- created_at INTEGER NOT NULL,
2102
- expires_at INTEGER NOT NULL
2103
- );
2104
- `);
2105
- }
2106
- hasAdminAccount() {
2107
- const Row = this.database.prepare("SELECT 1 AS present FROM admin_account WHERE id = 1").get();
2108
- return Boolean(Row);
2109
- }
2110
- createAdminAccount(passwordHash, now = Date.now()) {
2111
- const Result = this.database.prepare(`INSERT OR IGNORE INTO admin_account (id, password_hash, created_at, updated_at)
2112
- VALUES (1, ?, ?, ?)`).run(passwordHash, now, now);
2113
- return num(Result.changes) > 0;
2114
- }
2115
- getPasswordHash() {
2116
- const Row = this.database.prepare("SELECT password_hash FROM admin_account WHERE id = 1").get();
2117
- return Row?.password_hash ? str(Row.password_hash) : null;
2118
- }
2119
- updatePasswordHash(passwordHash, now = Date.now()) {
2120
- const Result = this.database.prepare(`UPDATE admin_account
2121
- SET password_hash = ?, updated_at = ?
2122
- WHERE id = 1`).run(passwordHash, now);
2123
- return num(Result.changes) > 0;
2124
- }
2125
- createSession(tokenHash, createdAt, expiresAt) {
2126
- this.database.prepare(`INSERT INTO admin_sessions (token_hash, created_at, expires_at)
2127
- VALUES (?, ?, ?)`).run(tokenHash, createdAt, expiresAt);
2128
- }
2129
- getSession(tokenHash, now = Date.now()) {
2130
- this.database.prepare("DELETE FROM admin_sessions WHERE expires_at <= ?").run(now);
2131
- const Row = this.database.prepare(`SELECT token_hash, created_at, expires_at
2132
- FROM admin_sessions
2133
- WHERE token_hash = ? AND expires_at > ?`).get(tokenHash, now);
2134
- if (!Row)
2135
- return null;
2136
- return {
2137
- tokenHash: str(Row.token_hash),
2138
- createdAt: num(Row.created_at),
2139
- expiresAt: num(Row.expires_at)
2140
- };
2141
- }
2142
- deleteSession(tokenHash) {
2143
- const Result = this.database.prepare("DELETE FROM admin_sessions WHERE token_hash = ?").run(tokenHash);
2144
- return num(Result.changes) > 0;
2145
- }
2146
- };
2147
- var adminAuthStore = new AdminAuthStore();
2148
-
2149
- // ../../packages/db/dist/quota.js
2150
- var CN_REFILL_GAP_MS = 2 * 24 * 60 * 60 * 1e3;
2151
-
2152
- // src/commands/migrate.ts
2153
- var TargetDbPath = DEFAULT_DB_PATH;
2154
- var BackupDir = path6.join(SROUTER_DIR, "backups");
2155
3503
  var NineRouterDbLocations = [
3504
+ path9.join(os7.homedir(), ".9router", "srouter.db"),
3505
+ path9.join(os7.homedir(), ".9router", "9router.db"),
3506
+ path9.join(os7.homedir(), "9router", "srouter.db"),
3507
+ path9.join(os7.homedir(), "9router", "data.db"),
3508
+ path9.join(os7.homedir(), "9router", "db", "srouter.db"),
3509
+ path9.join(os7.homedir(), ".config", "9router", "srouter.db"),
3510
+ path9.join(os7.homedir(), ".config", "9router", "9router.db"),
2156
3511
  "/root/9router/srouter.db",
2157
3512
  "/root/project/9router/db/srouter.db",
2158
3513
  ...LEGACY_DB_LOCATIONS
2159
3514
  ];
2160
3515
  function fileKb(filePath) {
2161
- return `${(fs6.statSync(filePath).size / 1024).toFixed(2)} KB`;
3516
+ return `${(fs9.statSync(filePath).size / 1024).toFixed(2)} KB`;
2162
3517
  }
2163
- function ensureDirs() {
2164
- fs6.mkdirSync(SROUTER_DIR, { recursive: true, mode: 448 });
2165
- fs6.mkdirSync(BackupDir, { recursive: true, mode: 493 });
3518
+ function ensureDirs(targetDbPath) {
3519
+ fs9.mkdirSync(path9.dirname(targetDbPath), { recursive: true, mode: 448 });
3520
+ fs9.mkdirSync(getBackupDir(targetDbPath), { recursive: true, mode: 493 });
2166
3521
  }
2167
- function backupDb(source, label) {
3522
+ function backupDb(source, label, targetDbPath) {
2168
3523
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2169
- const backupPath = path6.join(BackupDir, `${label}-backup-${timestamp}.db`);
2170
- fs6.copyFileSync(source, backupPath);
2171
- p.log.step(`Backed up to ${pc.dim(backupPath)}`);
3524
+ const backupPath = path9.join(getBackupDir(targetDbPath), `${label}-backup-${timestamp}.db`);
3525
+ fs9.copyFileSync(source, backupPath);
3526
+ p2.log.step(`Backed up to ${pc.dim(backupPath)}`);
2172
3527
  return backupPath;
2173
3528
  }
3529
+ function getDownloadDirs() {
3530
+ const home = os7.homedir();
3531
+ const dirs = [
3532
+ path9.join(home, "Downloads"),
3533
+ path9.join(home, "download"),
3534
+ path9.join(home, "downloads"),
3535
+ path9.join(home, "Desktop")
3536
+ ];
3537
+ if (process.platform === "win32") {
3538
+ if (process.env.USERPROFILE) {
3539
+ dirs.push(path9.join(process.env.USERPROFILE, "Downloads"));
3540
+ dirs.push(path9.join(process.env.USERPROFILE, "Desktop"));
3541
+ }
3542
+ }
3543
+ return Array.from(new Set(dirs.filter((d) => fs9.existsSync(d))));
3544
+ }
3545
+ function scanFor9RouterFiles() {
3546
+ const results = [];
3547
+ const seen = /* @__PURE__ */ new Set();
3548
+ const addFile = (filePath, label) => {
3549
+ const resolved = path9.resolve(filePath);
3550
+ if (!seen.has(resolved) && fs9.existsSync(resolved)) {
3551
+ try {
3552
+ const stat = fs9.statSync(resolved);
3553
+ if (stat.isFile()) {
3554
+ seen.add(resolved);
3555
+ results.push({ path: resolved, label, mtime: stat.mtimeMs });
3556
+ }
3557
+ } catch {
3558
+ }
3559
+ }
3560
+ };
3561
+ for (const loc of NineRouterDbLocations) {
3562
+ addFile(loc, "Installed Database");
3563
+ }
3564
+ for (const downloadDir of getDownloadDirs()) {
3565
+ try {
3566
+ const files = fs9.readdirSync(downloadDir);
3567
+ for (const file of files) {
3568
+ const lower = file.toLowerCase();
3569
+ const fullPath = path9.join(downloadDir, file);
3570
+ if (lower.includes("9router") || lower.includes("srouter-backup") || lower.endsWith(".db") || lower.endsWith(".json") && lower.includes("backup")) {
3571
+ addFile(fullPath, path9.basename(downloadDir));
3572
+ }
3573
+ }
3574
+ } catch {
3575
+ }
3576
+ }
3577
+ return results.sort((a, b) => b.mtime - a.mtime);
3578
+ }
2174
3579
  function findDatabase(candidates, label) {
2175
3580
  for (const candidate of candidates) {
2176
- const fullPath = path6.resolve(candidate);
2177
- if (fs6.existsSync(fullPath)) {
2178
- p.log.info(`Found ${label} database: ${pc.bold(fullPath)} (${fileKb(fullPath)})`);
3581
+ const fullPath = path9.resolve(candidate);
3582
+ if (fs9.existsSync(fullPath)) {
3583
+ p2.log.info(`Found ${label} database: ${pc.bold(fullPath)} (${fileKb(fullPath)})`);
2179
3584
  return fullPath;
2180
3585
  }
2181
3586
  }
2182
3587
  return null;
2183
3588
  }
2184
3589
  async function migrateDb(options) {
2185
- p.intro("SRouter Database Migration");
2186
- if (fs6.existsSync(TargetDbPath)) {
2187
- p.log.warn(`Database already exists at ${TargetDbPath} (${fileKb(TargetDbPath)})`);
2188
- p.outro("No migration needed \u2014 already using the new location.");
3590
+ p2.intro("SRouter Database Migration");
3591
+ const targetDbPath = getTargetDbPath();
3592
+ if (fs9.existsSync(targetDbPath)) {
3593
+ p2.log.warn(`Database already exists at ${targetDbPath} (${fileKb(targetDbPath)})`);
3594
+ p2.outro("No migration needed \u2014 already using the new location.");
2189
3595
  return;
2190
3596
  }
2191
- const source = options.source && fs6.existsSync(options.source) ? path6.resolve(options.source) : findDatabase(LEGACY_DB_LOCATIONS, "legacy");
3597
+ const source = options.source && fs9.existsSync(options.source) ? path9.resolve(options.source) : findDatabase(LEGACY_DB_LOCATIONS, "legacy");
2192
3598
  if (!source) {
2193
- p.log.error("No existing database found.");
2194
- p.outro(
2195
- `Start SRouter and a fresh database will be created at ${TargetDbPath}, or pass --source /path/to/srouter.db.`
3599
+ p2.log.error("No existing database found.");
3600
+ p2.outro(
3601
+ `Start SRouter and a fresh database will be created at ${targetDbPath}, or pass --source /path/to/srouter.db.`
2196
3602
  );
2197
3603
  return;
2198
3604
  }
2199
- ensureDirs();
2200
- const proceed = options.yes || await p.confirm({ message: `Migrate database from ${source}?` }) === true;
3605
+ ensureDirs(targetDbPath);
3606
+ const proceed = options.yes || await p2.confirm({ message: `Migrate database from ${source}?` }) === true;
2201
3607
  if (!proceed) {
2202
- p.outro("Migration cancelled.");
3608
+ p2.outro("Migration cancelled.");
2203
3609
  return;
2204
3610
  }
2205
3611
  try {
2206
- backupDb(source, "srouter");
2207
- fs6.copyFileSync(source, TargetDbPath);
2208
- fs6.chmodSync(TargetDbPath, 384);
2209
- p.log.success(`Database moved to ${TargetDbPath}`);
2210
- p.outro("Restart SRouter to use the migrated database.");
3612
+ backupDb(source, "srouter", targetDbPath);
3613
+ fs9.copyFileSync(source, targetDbPath);
3614
+ fs9.chmodSync(targetDbPath, 384);
3615
+ p2.log.success(`Database moved to ${targetDbPath}`);
3616
+ p2.outro("Restart SRouter to use the migrated database.");
2211
3617
  } catch (error) {
2212
- p.log.error(formatError(`Migration failed: ${error.message}`));
3618
+ p2.log.error(formatError(`Migration failed: ${error.message}`));
2213
3619
  process.exitCode = 1;
2214
3620
  }
2215
3621
  }
2216
3622
  async function migrateNineRouter(options) {
2217
- p.intro("9Router \u2192 SRouter Database Migration");
2218
- const source = options.source && fs6.existsSync(options.source) ? path6.resolve(options.source) : findDatabase(NineRouterDbLocations, "9Router");
3623
+ p2.intro("9Router \u2192 SRouter Database Migration");
3624
+ let source = options.source && fs9.existsSync(options.source) ? path9.resolve(options.source) : null;
2219
3625
  if (!source) {
2220
- p.log.error("No 9Router database found.");
2221
- p.outro(
2222
- "Pass the location explicitly: srouter migrate 9router --source /path/to/9router.db"
3626
+ const foundFiles = scanFor9RouterFiles();
3627
+ if (foundFiles.length === 1) {
3628
+ source = foundFiles[0].path;
3629
+ p2.log.info(`Found 9Router file: ${pc.bold(source)} (${fileKb(source)}) [${foundFiles[0].label}]`);
3630
+ } else if (foundFiles.length > 1) {
3631
+ if (options.yes) {
3632
+ source = foundFiles[0].path;
3633
+ p2.log.info(`Auto-selected latest 9Router file: ${pc.bold(source)} (${fileKb(source)})`);
3634
+ } else {
3635
+ const choice = await p2.select({
3636
+ message: "Multiple 9Router database/backup files found. Select one to migrate:",
3637
+ options: [
3638
+ ...foundFiles.map((f) => ({
3639
+ value: f.path,
3640
+ label: `${path9.basename(f.path)} (${fileKb(f.path)})`,
3641
+ hint: `${f.label} \u2022 ${f.path}`
3642
+ })),
3643
+ { value: "custom", label: "Enter custom path manually..." }
3644
+ ]
3645
+ });
3646
+ if (p2.isCancel(choice)) {
3647
+ p2.outro("Migration cancelled.");
3648
+ return;
3649
+ }
3650
+ if (choice === "custom") {
3651
+ const customPath = await p2.text({
3652
+ message: "Enter path to 9Router .db or .json backup file:",
3653
+ validate: (val) => {
3654
+ if (!val || !fs9.existsSync(path9.resolve(val))) {
3655
+ return "File does not exist. Please check the path.";
3656
+ }
3657
+ }
3658
+ });
3659
+ if (p2.isCancel(customPath) || !customPath) {
3660
+ p2.outro("Migration cancelled.");
3661
+ return;
3662
+ }
3663
+ source = path9.resolve(customPath);
3664
+ } else {
3665
+ source = choice;
3666
+ }
3667
+ }
3668
+ }
3669
+ }
3670
+ if (!source) {
3671
+ p2.log.error("No 9Router database or backup file found.");
3672
+ p2.outro(
3673
+ "Pass the location explicitly: srouter migrate 9router --source /path/to/9router-backup.json"
2223
3674
  );
2224
3675
  process.exitCode = 1;
2225
3676
  return;
2226
3677
  }
2227
- const existingTarget = fs6.existsSync(TargetDbPath);
2228
- let action = "copy";
2229
- if (existingTarget) {
2230
- p.log.warn(`Existing SRouter database found at ${TargetDbPath} (${fileKb(TargetDbPath)})`);
2231
- const choice = await p.select({
2232
- message: "How should the existing SRouter database be handled?",
2233
- options: [
2234
- { value: "backup_and_replace", label: "Backup current, replace with 9Router data" },
2235
- { value: "merge", label: "Overwrite tables with 9Router data" },
2236
- { value: "abort", label: "Cancel migration" }
2237
- ]
2238
- });
2239
- if (choice === "abort" || p.isCancel(choice)) {
2240
- p.outro("Migration cancelled. Your 9Router installation remains intact.");
2241
- return;
2242
- }
2243
- if (choice === "merge" || choice === "backup_and_replace") {
2244
- action = choice;
3678
+ const targetDbPath = getTargetDbPath();
3679
+ const existingTarget = fs9.existsSync(targetDbPath);
3680
+ let action = options.action ?? "copy";
3681
+ if (existingTarget && !options.action) {
3682
+ if (options.yes) {
3683
+ action = "merge";
3684
+ } else {
3685
+ p2.log.warn(`Existing SRouter database found at ${targetDbPath} (${fileKb(targetDbPath)})`);
3686
+ const choice = await p2.select({
3687
+ message: "How should the existing SRouter database be handled?",
3688
+ options: [
3689
+ { value: "backup_and_replace", label: "Backup current, replace with 9Router data" },
3690
+ { value: "merge", label: "Overwrite tables with 9Router data" },
3691
+ { value: "abort", label: "Cancel migration" }
3692
+ ]
3693
+ });
3694
+ if (choice === "abort" || p2.isCancel(choice)) {
3695
+ p2.outro("Migration cancelled. Your 9Router installation remains intact.");
3696
+ return;
3697
+ }
3698
+ if (choice === "merge" || choice === "backup_and_replace") {
3699
+ action = choice;
3700
+ }
2245
3701
  }
2246
3702
  }
2247
- const proceed = options.yes || await p.confirm({ message: "Proceed with migration?" }) === true;
3703
+ const proceed = options.yes || await p2.confirm({ message: "Proceed with migration?" }) === true;
2248
3704
  if (!proceed) {
2249
- p.outro("Migration cancelled. No changes made.");
3705
+ p2.outro("Migration cancelled. No changes made.");
2250
3706
  return;
2251
3707
  }
2252
- ensureDirs();
2253
- const sourceBackup = backupDb(source, "9router");
3708
+ ensureDirs(targetDbPath);
3709
+ const sourceBackup = backupDb(source, "9router", targetDbPath);
2254
3710
  let targetBackup = null;
2255
3711
  if (action === "backup_and_replace") {
2256
- targetBackup = backupDb(TargetDbPath, "srouter");
3712
+ targetBackup = backupDb(targetDbPath, "srouter", targetDbPath);
2257
3713
  }
2258
- const s = p.spinner();
3714
+ const s = p2.spinner();
2259
3715
  try {
2260
- s.start("Opening databases");
2261
- const sourceDb = new DatabaseSync2(source);
2262
- const targetDb = new DatabaseSync2(TargetDbPath);
2263
- s.message("Reading source tables");
2264
- const tables = sourceDb.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all().map((t) => t.name);
3716
+ s.start("Preparing target database");
3717
+ await initDatabase();
3718
+ const targetDb = new DatabaseSync2(targetDbPath);
3719
+ targetDb.exec("PRAGMA foreign_keys = OFF;");
2265
3720
  let inserted = 0;
2266
3721
  let skipped = 0;
2267
- for (const table of tables) {
2268
- const rows = sourceDb.prepare(`SELECT * FROM "${table}"`).all();
2269
- if (rows.length === 0) continue;
2270
- if (action !== "copy") {
2271
- try {
2272
- targetDb.prepare(`DELETE FROM "${table}"`).run();
2273
- } catch {
2274
- p.log.warn(`Could not clear table ${table}, rows will be appended`);
2275
- }
2276
- }
2277
- const columns = sourceDb.prepare(`PRAGMA table_info("${table}")`).all().map((col) => col.name);
2278
- const placeholders = columns.map(() => "?").join(", ");
2279
- const insert = targetDb.prepare(
2280
- `INSERT INTO "${table}" (${columns.join(", ")}) VALUES (${placeholders})`
3722
+ let tablesCount = 0;
3723
+ const isJson = source.endsWith(".json");
3724
+ if (isJson) {
3725
+ s.message("Reading 9Router JSON backup");
3726
+ const raw = fs9.readFileSync(source, "utf-8");
3727
+ const parsed = JSON.parse(raw);
3728
+ const res = importNineRouterJson(parsed, targetDb, action);
3729
+ inserted = res.inserted;
3730
+ skipped = res.skipped;
3731
+ tablesCount = res.tablesCount;
3732
+ } else {
3733
+ const sourceDb = new DatabaseSync2(source);
3734
+ s.message("Reading source SQLite tables");
3735
+ const sourceTables = sourceDb.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all().map((t) => t.name).filter((name) => !name.startsWith("sqlite_"));
3736
+ const targetTables = new Set(
3737
+ targetDb.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((t) => t.name)
2281
3738
  );
2282
- for (const row of rows) {
2283
- try {
2284
- insert.run(...columns.map((col) => row[col]));
2285
- inserted++;
2286
- } catch {
2287
- skipped++;
3739
+ for (const table of sourceTables) {
3740
+ if (!targetTables.has(table)) {
3741
+ p2.log.warn(`Skipping table ${table} (not recognized by SRouter)`);
3742
+ continue;
3743
+ }
3744
+ const rows = sourceDb.prepare(`SELECT * FROM "${table}"`).all();
3745
+ if (rows.length === 0) continue;
3746
+ if (action !== "copy") {
3747
+ try {
3748
+ targetDb.prepare(`DELETE FROM "${table}"`).run();
3749
+ } catch {
3750
+ p2.log.warn(`Could not clear table ${table}, rows will be appended`);
3751
+ }
3752
+ }
3753
+ const sourceColumns = new Set(
3754
+ sourceDb.prepare(`PRAGMA table_info("${table}")`).all().map((col) => col.name)
3755
+ );
3756
+ const targetColumns = targetDb.prepare(`PRAGMA table_info("${table}")`).all().map((col) => col.name).filter((col) => sourceColumns.has(col));
3757
+ if (targetColumns.length === 0) continue;
3758
+ const placeholders = targetColumns.map(() => "?").join(", ");
3759
+ const insert = targetDb.prepare(
3760
+ `INSERT OR REPLACE INTO "${table}" (${targetColumns.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`
3761
+ );
3762
+ for (const row of rows) {
3763
+ try {
3764
+ insert.run(...targetColumns.map((col) => row[col] ?? null));
3765
+ inserted++;
3766
+ } catch {
3767
+ skipped++;
3768
+ }
2288
3769
  }
3770
+ tablesCount++;
2289
3771
  }
3772
+ sourceDb.close();
2290
3773
  }
2291
- sourceDb.close();
3774
+ targetDb.exec("PRAGMA foreign_keys = ON;");
2292
3775
  targetDb.close();
2293
3776
  s.stop("Migration complete");
2294
- p.log.message(
3777
+ p2.log.message(
2295
3778
  [
2296
- `Tables migrated: ${tables.length}`,
2297
- `Rows inserted: ${inserted}`,
2298
- `Rows skipped: ${skipped}`,
3779
+ `Tables/Categories migrated: ${tablesCount}`,
3780
+ `Rows/Items inserted: ${inserted}`,
3781
+ `Rows/Items skipped: ${skipped}`,
2299
3782
  `Source: ${source}`,
2300
- `Target: ${TargetDbPath}`
3783
+ `Target: ${targetDbPath}`
2301
3784
  ].join("\n")
2302
3785
  );
2303
3786
  if (targetBackup) {
2304
- p.log.info(`Old SRouter database backed up to ${targetBackup}`);
3787
+ p2.log.info(`Old SRouter database backed up to ${targetBackup}`);
2305
3788
  }
2306
- p.log.success(formatSuccess(`9Router backup saved to ${sourceBackup}`));
2307
- p.outro("Your 9Router providers are now available in SRouter.");
3789
+ p2.log.success(formatSuccess(`9Router backup saved to ${sourceBackup}`));
3790
+ p2.outro("Your 9Router providers are now available in SRouter.");
2308
3791
  } catch (error) {
2309
3792
  s.stop(formatError("Migration failed"));
2310
- p.log.error(error.message);
2311
- p.log.info(formatInfo(`Restore from backup if needed: cp ${sourceBackup} ${source}`));
3793
+ p2.log.error(error.message);
3794
+ p2.log.info(formatInfo(`Restore from backup if needed: cp ${sourceBackup} ${source}`));
2312
3795
  process.exitCode = 1;
2313
3796
  }
2314
3797
  }
@@ -2319,8 +3802,8 @@ async function migrateCommand(target, options) {
2319
3802
  case "9router":
2320
3803
  return migrateNineRouter(options);
2321
3804
  default:
2322
- p.log.error(formatError(`Unknown migration target: ${target}`));
2323
- p.log.info(
3805
+ p2.log.error(formatError(`Unknown migration target: ${target}`));
3806
+ p2.log.info(
2324
3807
  formatWarning("Available targets: db (legacy location), 9router (9Router import)")
2325
3808
  );
2326
3809
  process.exitCode = 1;
@@ -2347,7 +3830,10 @@ function createCli() {
2347
3830
  ).action(async (opts) => {
2348
3831
  await setupCommand(opts);
2349
3832
  });
2350
- program.command("link <tool>").description("Configure a specific tool to use SRouter proxy (claude, opencode)").option("-u, --url <url>", "SRouter Gateway Base URL (e.g. http://localhost:3000/v1)").option("-k, --key <key>", "SRouter API Key").option("-m, --model <model>", "Model ID").option(
3833
+ program.command("init").description("Initialize and run SRouter Gateway (Docker container or Source Code)").option("-m, --mode <mode>", "Run mode (docker or source)").option("-p, --port <port>", "Gateway port (default: 3000)").option("-d, --dir <dir>", "Source code clone directory (default: ~/srouter)").option("-y, --yes", "Accept default values without interactive prompt").option("--detached", "Run docker container in background (default: true)").action(async (opts) => {
3834
+ await initCommand(opts);
3835
+ });
3836
+ program.command("link <tool>").description("Configure a specific tool to use SRouter proxy (claude, opencode, hindsight)").option("-u, --url <url>", "SRouter Gateway Base URL (e.g. http://localhost:3000/v1)").option("-k, --key <key>", "SRouter API Key").option("-m, --model <model>", "Model ID").option(
2351
3837
  "--opus-model <model>",
2352
3838
  "Opus tier model ID for Claude Code (ANTHROPIC_DEFAULT_OPUS_MODEL)"
2353
3839
  ).option(
@@ -2394,15 +3880,19 @@ function createCli() {
2394
3880
  ).allowUnknownOption().action(async (tool, args, opts) => {
2395
3881
  await runCommand(tool, args, opts);
2396
3882
  });
2397
- program.command("migrate <target>").description("Migrate databases (targets: db = legacy location, 9router = 9Router import)").option("--source <path>", "Explicit path to the source SQLite database").option("-y, --yes", "Skip confirmation prompts").action(async (target, opts) => {
3883
+ program.command("migrate <target>").description("Migrate databases (targets: db = legacy location, 9router = 9Router import)").option("--source <path>", "Explicit path to the source SQLite database").option("-y, --yes", "Skip confirmation prompts").option("-a, --action <action>", "Migration conflict resolution (copy, merge, backup_and_replace)").action(async (target, opts) => {
2398
3884
  await migrateCommand(target, opts);
2399
3885
  });
2400
3886
  return program;
2401
3887
  }
2402
- if (process.argv[1]?.endsWith("srouter.js") || process.argv[1]?.endsWith("index.ts") || process.argv[1]?.endsWith("index.js")) {
2403
- const program = createCli();
2404
- program.parse(process.argv);
3888
+ function runCli() {
3889
+ const isEntrypoint = process.argv[1]?.endsWith("srouter.js") || process.argv[1]?.endsWith("index.js") || process.argv[1]?.endsWith("index.ts") || process.argv[1]?.includes("/.bin/srouter") || process.argv[1]?.includes("/bin/srouter");
3890
+ if (isEntrypoint) {
3891
+ const program = createCli();
3892
+ program.parse(process.argv);
3893
+ }
2405
3894
  }
3895
+ runCli();
2406
3896
  export {
2407
3897
  createCli
2408
3898
  };