@srouter/cli 0.1.3-rc.2 → 0.1.3-rc.4

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,7 +295,6 @@ 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
299
  { id: "laguna-s-2.1-free", name: "Poolside Laguna S 2.1 (Free)" },
296
300
  { id: "nemotron-3.5-lightning-free", name: "Nemotron 3.5 Lightning (Free)" },
@@ -312,6 +316,21 @@ var OPENCODE_ZEN_PROVIDER = {
312
316
  status_message: "Free Tier Ready (Unlimited)"
313
317
  };
314
318
 
319
+ // ../../packages/constants/dist/providers/bai.js
320
+ var BAI_BASE_URL = "https://api.b.ai/v1";
321
+ var BAI_PROVIDER = {
322
+ id: "bai",
323
+ name: "B.AI",
324
+ category: "free_tier",
325
+ protocol: "openai",
326
+ alias: "bai",
327
+ base_url: BAI_BASE_URL,
328
+ web_url: "https://b.ai",
329
+ requires_api_key: true,
330
+ supports_custom_url: true,
331
+ status_message: "B.AI API key missing"
332
+ };
333
+
315
334
  // ../../packages/constants/dist/providers/catalog.js
316
335
  var KNOWN_PROVIDERS = [
317
336
  KIRO_PROVIDER,
@@ -328,13 +347,19 @@ var KNOWN_PROVIDERS = [
328
347
  QODER_PROVIDER,
329
348
  CODEBUDDY_PROVIDER,
330
349
  CODEBUDDY_CN_PROVIDER,
331
- OPENCODE_ZEN_PROVIDER
350
+ OPENCODE_ZEN_PROVIDER,
351
+ BAI_PROVIDER
332
352
  ];
333
353
  var KNOWN_PROVIDER_MAP = Object.freeze(Object.fromEntries(KNOWN_PROVIDERS.map((Provider) => [Provider.id, Provider])));
334
354
  var KNOWN_PROVIDER_IDS_DESC = Object.freeze(Object.keys(KNOWN_PROVIDER_MAP).sort((A, B) => B.length - A.length));
335
355
  var LEGACY_ALIAS_MAP = Object.freeze({
336
356
  claude: "claude",
337
- cbai: "codebuddy"
357
+ cbai: "codebuddy",
358
+ // OpenCode Zen's executor registers with base id "opencode_zen"; its
359
+ // listModels splits on "_" and produces "opencode/<model>" ids, while
360
+ // alias-prefixed connections produce "zen/<model>". Map both so log
361
+ // attribution and routing resolve to the registered base id.
362
+ opencode: "opencode_zen"
338
363
  });
339
364
 
340
365
  // ../../packages/constants/dist/seed.js
@@ -342,8 +367,8 @@ var DEFAULT_PROVIDERS = Object.freeze(KNOWN_PROVIDERS.map(({ alias: _alias, ...s
342
367
  var DEFAULT_PROVIDER_MAP = Object.freeze(Object.fromEntries(DEFAULT_PROVIDERS.map((seed) => [seed.id, seed])));
343
368
 
344
369
  // ../../packages/constants/dist/version.js
345
- var GLOBAL_VERSION = "0.1.3";
346
- var CLI_VERSION = GLOBAL_VERSION;
370
+ var GLOBAL_VERSION = "0.1.4";
371
+ var CLI_VERSION = "0.1.3-rc.4";
347
372
  var SROUTER_VERSION_TAG = `v${GLOBAL_VERSION}`;
348
373
 
349
374
  // src/commands/setup.ts
@@ -475,10 +500,10 @@ import { exec } from "node:child_process";
475
500
  import { promisify } from "node:util";
476
501
  var execAsync = promisify(exec);
477
502
  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";
503
+ const p3 = process.platform;
504
+ if (p3 === "win32") return "windows";
505
+ if (p3 === "darwin") return "macos";
506
+ if (p3 === "linux") return "linux";
482
507
  return "unknown";
483
508
  }
484
509
  function isWindows() {
@@ -488,10 +513,10 @@ function isMacOS() {
488
513
  return process.platform === "darwin";
489
514
  }
490
515
  function getOsDisplayName() {
491
- const p2 = getPlatform();
516
+ const p3 = getPlatform();
492
517
  const arch = os2.arch();
493
518
  const rel = os2.release();
494
- switch (p2) {
519
+ switch (p3) {
495
520
  case "macos":
496
521
  return `macOS (${arch === "arm64" ? "Apple Silicon" : arch}) [Darwin ${rel}]`;
497
522
  case "windows":
@@ -561,9 +586,9 @@ function getClaudeConfigPath() {
561
586
  candidatePaths.push(path2.join(home, ".claude", "config.json"));
562
587
  candidatePaths.push(path2.join(home, ".config", "claude", "config.json"));
563
588
  }
564
- for (const p2 of candidatePaths) {
565
- if (fs2.existsSync(p2)) {
566
- return p2;
589
+ for (const p3 of candidatePaths) {
590
+ if (fs2.existsSync(p3)) {
591
+ return p3;
567
592
  }
568
593
  }
569
594
  if (fs2.existsSync(path2.join(home, ".claude"))) {
@@ -605,9 +630,9 @@ function getOpenCodeConfigPath() {
605
630
  }
606
631
  candidatePaths.push(path2.join(home, ".opencode.jsonc"));
607
632
  candidatePaths.push(path2.join(home, ".opencode.json"));
608
- for (const p2 of candidatePaths) {
609
- if (fs2.existsSync(p2)) {
610
- return p2;
633
+ for (const p3 of candidatePaths) {
634
+ if (fs2.existsSync(p3)) {
635
+ return p3;
611
636
  }
612
637
  }
613
638
  if (isWindows()) {
@@ -721,6 +746,7 @@ var ClaudeAdapter = class extends AbstractToolAdapter {
721
746
  data.env.ANTHROPIC_DEFAULT_OPUS_MODEL = context.opusModel || defaultModel;
722
747
  data.env.ANTHROPIC_DEFAULT_SONNET_MODEL = context.sonnetModel || defaultModel;
723
748
  data.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = context.haikuModel || defaultModel;
749
+ data.env.CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT = "1";
724
750
  if (data.env.ANTHROPIC_DEFAULT_FABLE_MODEL) {
725
751
  delete data.env.ANTHROPIC_DEFAULT_FABLE_MODEL;
726
752
  }
@@ -783,6 +809,7 @@ var ClaudeAdapter = class extends AbstractToolAdapter {
783
809
  delete data.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
784
810
  delete data.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
785
811
  delete data.env.ANTHROPIC_DEFAULT_FABLE_MODEL;
812
+ delete data.env.CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT;
786
813
  }
787
814
  await fs3.writeFile(configPath, JSON.stringify(data, null, 2), "utf-8");
788
815
  return true;
@@ -802,15 +829,16 @@ var ClaudeAdapter = class extends AbstractToolAdapter {
802
829
  ANTHROPIC_MODEL: defaultModel,
803
830
  ANTHROPIC_DEFAULT_OPUS_MODEL: context.opusModel || defaultModel,
804
831
  ANTHROPIC_DEFAULT_SONNET_MODEL: context.sonnetModel || defaultModel,
805
- ANTHROPIC_DEFAULT_HAIKU_MODEL: context.haikuModel || defaultModel
832
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: context.haikuModel || defaultModel,
833
+ CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT: "1"
806
834
  };
807
835
  return env;
808
836
  }
809
837
  };
810
838
 
811
839
  // src/adapters/opencode.ts
812
- import fs4 from "node:fs/promises";
813
- import path4 from "node:path";
840
+ import fs5 from "node:fs/promises";
841
+ import path5 from "node:path";
814
842
 
815
843
  // src/lib/srouterClient.ts
816
844
  function normalizeBaseUrl(baseUrl) {
@@ -881,7 +909,361 @@ async function fetchAvailableModels(baseUrl, apiKey) {
881
909
  }
882
910
  }
883
911
 
912
+ // ../../packages/pricing/dist/parser.js
913
+ import fs4 from "node:fs";
914
+ import path4 from "node:path";
915
+ import { fileURLToPath } from "node:url";
916
+ function stripJsonComments(jsonc) {
917
+ let insideString = false;
918
+ let isEscaped = false;
919
+ let output = "";
920
+ let i = 0;
921
+ while (i < jsonc.length) {
922
+ const char = jsonc[i];
923
+ const nextChar = jsonc[i + 1];
924
+ if (insideString) {
925
+ output += char;
926
+ if (isEscaped) {
927
+ isEscaped = false;
928
+ } else if (char === "\\") {
929
+ isEscaped = true;
930
+ } else if (char === '"') {
931
+ insideString = false;
932
+ }
933
+ i++;
934
+ continue;
935
+ }
936
+ if (char === '"') {
937
+ insideString = true;
938
+ output += char;
939
+ i++;
940
+ continue;
941
+ }
942
+ if (char === "/" && nextChar === "/") {
943
+ i += 2;
944
+ while (i < jsonc.length && jsonc[i] !== "\n" && jsonc[i] !== "\r") {
945
+ i++;
946
+ }
947
+ continue;
948
+ }
949
+ if (char === "/" && nextChar === "*") {
950
+ i += 2;
951
+ while (i < jsonc.length && !(jsonc[i] === "*" && jsonc[i + 1] === "/")) {
952
+ i++;
953
+ }
954
+ i += 2;
955
+ continue;
956
+ }
957
+ output += char;
958
+ i++;
959
+ }
960
+ return output.replace(/,(\s*[}\]])/g, "$1");
961
+ }
962
+ function parseJsonc(jsonc) {
963
+ const cleanJson = stripJsonComments(jsonc);
964
+ return JSON.parse(cleanJson);
965
+ }
966
+ function flattenModelPrices(models) {
967
+ const flat = {};
968
+ for (const [key, val] of Object.entries(models || {})) {
969
+ if (Array.isArray(val)) {
970
+ for (const item of val) {
971
+ if (item && item.id) {
972
+ flat[item.id] = item;
973
+ }
974
+ }
975
+ } else if (val && typeof val === "object") {
976
+ if ("input" in val && "output" in val) {
977
+ flat[key] = val;
978
+ } else {
979
+ for (const [subKey, subVal] of Object.entries(val)) {
980
+ flat[subKey] = subVal;
981
+ }
982
+ }
983
+ }
984
+ }
985
+ return flat;
986
+ }
987
+ function resolvePricingDataPath(customPath) {
988
+ if (customPath)
989
+ return customPath;
990
+ const currentDir = path4.dirname(fileURLToPath(import.meta.url));
991
+ const candidates = [
992
+ path4.resolve(currentDir, "../data/pricing.jsonc"),
993
+ path4.resolve(currentDir, "../data/pricing.json"),
994
+ path4.resolve(currentDir, "../../data/pricing.jsonc"),
995
+ path4.resolve(currentDir, "../../data/pricing.json"),
996
+ path4.resolve(process.cwd(), "packages/pricing/data/pricing.jsonc"),
997
+ path4.resolve(process.cwd(), "packages/pricing/data/pricing.json"),
998
+ path4.resolve(process.cwd(), "data/pricing.jsonc"),
999
+ path4.resolve(process.cwd(), "data/pricing.json")
1000
+ ];
1001
+ for (const candidate of candidates) {
1002
+ if (fs4.existsSync(candidate)) {
1003
+ return candidate;
1004
+ }
1005
+ }
1006
+ return candidates[0];
1007
+ }
1008
+ function modelsDevToModelPrice(model) {
1009
+ if (!model.cost)
1010
+ return void 0;
1011
+ const input = model.cost.input ?? 0;
1012
+ const output = model.cost.output ?? 0;
1013
+ return {
1014
+ id: model.id,
1015
+ name: model.name,
1016
+ input,
1017
+ output,
1018
+ cached: model.cost.cache_read,
1019
+ reasoning: model.cost.reasoning ?? output,
1020
+ cache_creation: model.cost.cache_write ?? input
1021
+ };
1022
+ }
1023
+ function loadPricingFromModelsDev(customPath) {
1024
+ const modelsData = loadModelsDevData(customPath);
1025
+ const models = {};
1026
+ const providerModels = {};
1027
+ const aliases = {};
1028
+ for (const [key, item] of Object.entries(modelsData)) {
1029
+ const price = modelsDevToModelPrice(item);
1030
+ if (!price)
1031
+ continue;
1032
+ models[key] = price;
1033
+ if (item.id && item.id !== key) {
1034
+ models[item.id] = price;
1035
+ }
1036
+ if (key.includes("/")) {
1037
+ const modelName = key.split("/").slice(1).join("/");
1038
+ if (!aliases[modelName]) {
1039
+ aliases[modelName] = key;
1040
+ }
1041
+ if (!models[modelName]) {
1042
+ models[modelName] = price;
1043
+ }
1044
+ }
1045
+ const provider = key.includes("/") ? key.split("/")[0] : "other";
1046
+ if (!providerModels[provider]) {
1047
+ providerModels[provider] = [];
1048
+ }
1049
+ providerModels[provider].push(price);
1050
+ }
1051
+ return {
1052
+ version: "1.0.0",
1053
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
1054
+ defaults: {
1055
+ input: 2,
1056
+ output: 8,
1057
+ cached: 1,
1058
+ reasoning: 12,
1059
+ cache_creation: 2
1060
+ },
1061
+ models,
1062
+ providerModels,
1063
+ aliases
1064
+ };
1065
+ }
1066
+ function loadPricingData(customPath) {
1067
+ if (customPath && (customPath.endsWith("models.jsonc") || customPath.endsWith("models.json"))) {
1068
+ return loadPricingFromModelsDev(customPath);
1069
+ }
1070
+ const filePath = resolvePricingDataPath(customPath);
1071
+ if (!fs4.existsSync(filePath)) {
1072
+ const modelsDevPath2 = resolveModelsDevDataPath();
1073
+ if (fs4.existsSync(modelsDevPath2)) {
1074
+ return loadPricingFromModelsDev(modelsDevPath2);
1075
+ }
1076
+ throw new Error(`Pricing dataset file not found at: ${filePath}`);
1077
+ }
1078
+ const content = fs4.readFileSync(filePath, "utf-8");
1079
+ const raw = parseJsonc(content);
1080
+ const flatModels = flattenModelPrices(raw.models);
1081
+ const isGroupedArray = raw.models && Object.values(raw.models).length > 0 && Array.isArray(Object.values(raw.models)[0]);
1082
+ const dataset = {
1083
+ version: raw.version,
1084
+ updatedAt: raw.updatedAt,
1085
+ defaults: raw.defaults,
1086
+ models: { ...flatModels },
1087
+ providerModels: isGroupedArray ? { ...raw.models } : void 0,
1088
+ aliases: { ...raw.aliases || {} }
1089
+ };
1090
+ const modelsDevPath = resolveModelsDevDataPath();
1091
+ if (fs4.existsSync(modelsDevPath)) {
1092
+ try {
1093
+ const modelsDevDataset = loadPricingFromModelsDev(modelsDevPath);
1094
+ for (const [k, v] of Object.entries(modelsDevDataset.models)) {
1095
+ if (!dataset.models[k]) {
1096
+ dataset.models[k] = v;
1097
+ }
1098
+ }
1099
+ for (const [k, v] of Object.entries(modelsDevDataset.aliases)) {
1100
+ if (!dataset.aliases[k] && !dataset.models[k]) {
1101
+ dataset.aliases[k] = v;
1102
+ }
1103
+ }
1104
+ } catch {
1105
+ }
1106
+ }
1107
+ return dataset;
1108
+ }
1109
+ function resolveModelsDevDataPath(customPath) {
1110
+ if (customPath) {
1111
+ if (path4.isAbsolute(customPath)) {
1112
+ return customPath;
1113
+ }
1114
+ return path4.resolve(process.cwd(), customPath);
1115
+ }
1116
+ const currentDir = path4.dirname(fileURLToPath(import.meta.url));
1117
+ const candidates = [
1118
+ path4.resolve(currentDir, "../models.jsonc"),
1119
+ path4.resolve(currentDir, "../models.json"),
1120
+ path4.resolve(currentDir, "../../models.jsonc"),
1121
+ path4.resolve(currentDir, "../../models.json"),
1122
+ path4.resolve(process.cwd(), "packages/pricing/models.jsonc"),
1123
+ path4.resolve(process.cwd(), "packages/pricing/models.json"),
1124
+ path4.resolve(process.cwd(), "models.jsonc"),
1125
+ path4.resolve(process.cwd(), "models.json")
1126
+ ];
1127
+ for (const candidate of candidates) {
1128
+ if (fs4.existsSync(candidate)) {
1129
+ return candidate;
1130
+ }
1131
+ }
1132
+ return candidates[0];
1133
+ }
1134
+ function loadModelsDevData(customPath) {
1135
+ const filePath = resolveModelsDevDataPath(customPath);
1136
+ if (!fs4.existsSync(filePath)) {
1137
+ return {};
1138
+ }
1139
+ const content = fs4.readFileSync(filePath, "utf-8");
1140
+ return parseJsonc(content);
1141
+ }
1142
+
1143
+ // ../../packages/pricing/dist/matcher.js
1144
+ function normalizeModelName(rawModel, aliases) {
1145
+ if (!rawModel)
1146
+ return "";
1147
+ let clean = rawModel.trim();
1148
+ if (clean.includes(":") && !clean.includes("://")) {
1149
+ const colonIndex = clean.lastIndexOf(":");
1150
+ clean = clean.substring(0, colonIndex).trim();
1151
+ }
1152
+ if (clean.includes("/")) {
1153
+ clean = clean.split("/").pop()?.trim() ?? clean;
1154
+ }
1155
+ if (aliases) {
1156
+ const lower = clean.toLowerCase();
1157
+ for (const [alias, target] of Object.entries(aliases)) {
1158
+ if (alias.toLowerCase() === lower) {
1159
+ return target;
1160
+ }
1161
+ }
1162
+ }
1163
+ return clean;
1164
+ }
1165
+ function findCanonicalModelKey(rawModel, models, aliases) {
1166
+ if (!rawModel)
1167
+ return void 0;
1168
+ const normalized = normalizeModelName(rawModel, aliases);
1169
+ if (models[normalized]) {
1170
+ return normalized;
1171
+ }
1172
+ if (models[rawModel]) {
1173
+ return rawModel;
1174
+ }
1175
+ const normalizedLower = normalized.toLowerCase();
1176
+ for (const key of Object.keys(models)) {
1177
+ if (key.toLowerCase() === normalizedLower) {
1178
+ return key;
1179
+ }
1180
+ }
1181
+ const baseClean = (rawModel.includes("/") ? rawModel.split("/").pop() ?? rawModel : rawModel).split(":")[0].trim().toLowerCase();
1182
+ for (const key of Object.keys(models)) {
1183
+ if (key.toLowerCase() === baseClean) {
1184
+ return key;
1185
+ }
1186
+ }
1187
+ return void 0;
1188
+ }
1189
+
1190
+ // ../../packages/pricing/dist/pricing.js
1191
+ var EMBEDDED_DEFAULT_PRICING = {
1192
+ input: 2,
1193
+ output: 8,
1194
+ cached: 1,
1195
+ reasoning: 12,
1196
+ cache_creation: 2
1197
+ };
1198
+ var loadedDataset;
1199
+ try {
1200
+ loadedDataset = loadPricingData();
1201
+ } catch {
1202
+ loadedDataset = {
1203
+ defaults: EMBEDDED_DEFAULT_PRICING,
1204
+ models: {},
1205
+ aliases: {}
1206
+ };
1207
+ }
1208
+ var DEFAULT_PRICING = loadedDataset.defaults || EMBEDDED_DEFAULT_PRICING;
1209
+ var MODEL_PRICING = loadedDataset.models;
1210
+ var PROVIDER_MODELS = loadedDataset.providerModels;
1211
+ var MODEL_ALIASES = loadedDataset.aliases;
1212
+ var cachedModelsDevData;
1213
+ function getModelMetadata(modelId) {
1214
+ if (!modelId)
1215
+ return void 0;
1216
+ if (!cachedModelsDevData) {
1217
+ try {
1218
+ cachedModelsDevData = loadModelsDevData();
1219
+ } catch {
1220
+ cachedModelsDevData = {};
1221
+ }
1222
+ }
1223
+ const data = cachedModelsDevData;
1224
+ if (data[modelId]) {
1225
+ return data[modelId];
1226
+ }
1227
+ const canonical = findCanonicalModelKey(modelId, MODEL_PRICING, MODEL_ALIASES) || normalizeModelName(modelId, MODEL_ALIASES);
1228
+ if (canonical && data[canonical]) {
1229
+ return data[canonical];
1230
+ }
1231
+ const targetSuffix = `/${canonical}`;
1232
+ for (const [key, item] of Object.entries(data)) {
1233
+ if (key === canonical || key.endsWith(targetSuffix) || item.id === canonical || item.id.endsWith(targetSuffix)) {
1234
+ return item;
1235
+ }
1236
+ }
1237
+ return void 0;
1238
+ }
1239
+
884
1240
  // src/adapters/opencode.ts
1241
+ function createOpenCodeModelConfig(id, name) {
1242
+ const displayName = name || formatModelDisplayName(id);
1243
+ const config = {
1244
+ id,
1245
+ name: displayName
1246
+ };
1247
+ const meta = getModelMetadata(id);
1248
+ if (meta?.modalities?.input?.includes("image") || meta?.attachment) {
1249
+ config.attachment = true;
1250
+ config.modalities = {
1251
+ input: meta.modalities?.input || ["text", "image"],
1252
+ output: meta.modalities?.output || ["text"]
1253
+ };
1254
+ } else {
1255
+ const lower = id.toLowerCase();
1256
+ 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");
1257
+ if (isKnownVision) {
1258
+ config.attachment = true;
1259
+ config.modalities = {
1260
+ input: ["text", "image"],
1261
+ output: ["text"]
1262
+ };
1263
+ }
1264
+ }
1265
+ return config;
1266
+ }
885
1267
  var DEFAULT_SROUTER_MODELS = [
886
1268
  { id: "anthropic/claude-3-7-sonnet", name: "Claude 3.7 Sonnet (Anthropic)" },
887
1269
  { id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet" },
@@ -963,7 +1345,7 @@ function formatProviderLabel(provider) {
963
1345
  function humanizeName(str2) {
964
1346
  return str2.replace(/[-_]/g, " ").replace(/\b[a-z]/g, (c) => c.toUpperCase());
965
1347
  }
966
- function parseJsonc(content) {
1348
+ function parseJsonc2(content) {
967
1349
  try {
968
1350
  const clean = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").replace(/,\s*([\]}])/g, "$1");
969
1351
  return JSON.parse(clean);
@@ -999,8 +1381,8 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
999
1381
  const configPath = this.getConfigPath();
1000
1382
  const installed = await this.isInstalled();
1001
1383
  try {
1002
- const raw = await fs4.readFile(configPath, "utf-8");
1003
- const parsed = parseJsonc(raw);
1384
+ const raw = await fs5.readFile(configPath, "utf-8");
1385
+ const parsed = parseJsonc2(raw);
1004
1386
  const baseUrl = parsed.provider?.srouter?.options?.baseURL || parsed.openai_base_url || parsed.api_base || parsed.baseUrl || parsed.providers?.srouter?.baseUrl || void 0;
1005
1387
  const model = parsed.model || parsed.default_model || void 0;
1006
1388
  const linked = Boolean(
@@ -1030,8 +1412,8 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
1030
1412
  const backupPath = context.dryRun ? void 0 : await this.store.createBackup(this.id, configPath);
1031
1413
  let data = {};
1032
1414
  try {
1033
- const raw = await fs4.readFile(configPath, "utf-8");
1034
- data = parseJsonc(raw);
1415
+ const raw = await fs5.readFile(configPath, "utf-8");
1416
+ data = parseJsonc2(raw);
1035
1417
  } catch {
1036
1418
  data = {};
1037
1419
  }
@@ -1046,14 +1428,18 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
1046
1428
  const existingModels = existingSrouter.models || {};
1047
1429
  const modelsMap = {};
1048
1430
  for (const m of DEFAULT_SROUTER_MODELS) {
1049
- modelsMap[m.id] = {
1050
- id: m.id,
1051
- name: m.name
1052
- };
1431
+ modelsMap[m.id] = createOpenCodeModelConfig(m.id, m.name);
1053
1432
  }
1054
1433
  for (const [key, val] of Object.entries(existingModels)) {
1055
1434
  if (val && typeof val === "object") {
1056
- modelsMap[key] = val;
1435
+ const existingVal = val;
1436
+ const fresh = createOpenCodeModelConfig(key, existingVal.name);
1437
+ modelsMap[key] = {
1438
+ ...fresh,
1439
+ ...existingVal,
1440
+ ...fresh.attachment !== void 0 && { attachment: fresh.attachment },
1441
+ ...fresh.modalities !== void 0 && { modalities: fresh.modalities }
1442
+ };
1057
1443
  }
1058
1444
  }
1059
1445
  try {
@@ -1061,19 +1447,16 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
1061
1447
  for (const rawId of liveModels) {
1062
1448
  if (!rawId) continue;
1063
1449
  const cleanId = rawId.startsWith("srouter/") ? rawId.replace(/^srouter\//, "") : rawId;
1064
- modelsMap[cleanId] = {
1065
- id: cleanId,
1066
- name: formatModelDisplayName(cleanId)
1067
- };
1450
+ modelsMap[cleanId] = createOpenCodeModelConfig(cleanId);
1068
1451
  }
1069
1452
  } catch {
1070
1453
  }
1071
1454
  const rawModel = context.model || "claude-3-7-sonnet";
1072
1455
  const cleanModelId = rawModel.startsWith("srouter/") ? rawModel.replace(/^srouter\//, "") : rawModel;
1073
- modelsMap[cleanModelId] = {
1074
- id: cleanModelId,
1075
- name: modelsMap[cleanModelId]?.name || formatModelDisplayName(cleanModelId)
1076
- };
1456
+ modelsMap[cleanModelId] = createOpenCodeModelConfig(
1457
+ cleanModelId,
1458
+ modelsMap[cleanModelId]?.name
1459
+ );
1077
1460
  data.provider.srouter = {
1078
1461
  name: "SRouter",
1079
1462
  npm: "@ai-sdk/openai-compatible",
@@ -1085,8 +1468,8 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
1085
1468
  };
1086
1469
  data.model = `srouter/${cleanModelId}`;
1087
1470
  if (!context.dryRun) {
1088
- await fs4.mkdir(path4.dirname(configPath), { recursive: true });
1089
- await fs4.writeFile(configPath, JSON.stringify(data, null, 4), "utf-8");
1471
+ await fs5.mkdir(path5.dirname(configPath), { recursive: true });
1472
+ await fs5.writeFile(configPath, JSON.stringify(data, null, 4), "utf-8");
1090
1473
  }
1091
1474
  return {
1092
1475
  backupPath,
@@ -1100,8 +1483,8 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
1100
1483
  }
1101
1484
  const configPath = this.getConfigPath();
1102
1485
  try {
1103
- const raw = await fs4.readFile(configPath, "utf-8");
1104
- const data = parseJsonc(raw);
1486
+ const raw = await fs5.readFile(configPath, "utf-8");
1487
+ const data = parseJsonc2(raw);
1105
1488
  if (data.provider?.srouter) {
1106
1489
  delete data.provider.srouter;
1107
1490
  }
@@ -1113,7 +1496,7 @@ var OpenCodeAdapter = class extends AbstractToolAdapter {
1113
1496
  delete data.openai_api_key;
1114
1497
  delete data.api_key;
1115
1498
  delete data.providers;
1116
- await fs4.writeFile(configPath, JSON.stringify(data, null, 4), "utf-8");
1499
+ await fs5.writeFile(configPath, JSON.stringify(data, null, 4), "utf-8");
1117
1500
  return true;
1118
1501
  } catch {
1119
1502
  return false;
@@ -1566,124 +1949,988 @@ ${pc.yellow("No tools selected for configuration.")}`,
1566
1949
  );
1567
1950
  }
1568
1951
 
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;
1952
+ // src/commands/init.ts
1953
+ import fs7 from "node:fs";
1954
+ import os6 from "node:os";
1955
+ import path8 from "node:path";
1956
+ import { spawn } from "node:child_process";
1957
+ import * as p from "@clack/prompts";
1958
+
1959
+ // ../../packages/db/dist/db.js
1960
+ import path7 from "node:path";
1961
+ import os5 from "node:os";
1962
+
1963
+ // ../../packages/db/dist/sqlite.js
1964
+ import path6 from "node:path";
1965
+ import fs6 from "node:fs";
1966
+ import os4 from "node:os";
1967
+ import { DatabaseSync } from "node:sqlite";
1968
+ var SROUTER_DIR = path6.join(os4.homedir(), ".srouter");
1969
+ var DEFAULT_DB_PATH = path6.join(SROUTER_DIR, "srouter.db");
1970
+ var LEGACY_DB_LOCATIONS = [
1971
+ path6.resolve(process.cwd(), "apps/api/srouter.db"),
1972
+ path6.resolve(process.cwd(), "srouter.db")
1973
+ ];
1974
+ function getDatabasePath() {
1975
+ if (process.env.DATABASE_PATH)
1976
+ return process.env.DATABASE_PATH;
1977
+ for (const legacyPath of LEGACY_DB_LOCATIONS) {
1978
+ if (fs6.existsSync(legacyPath))
1979
+ return legacyPath;
1580
1980
  }
1581
- const savedConfig = await defaultStore.loadConfig();
1582
- const baseUrl = options.url || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
1583
- const apiKey = options.key || savedConfig.defaultApiKey;
1584
- const model = options.model || savedConfig.defaultModel;
1585
- const opusModel = options.opusModel || savedConfig.defaultOpusModel;
1586
- const sonnetModel = options.sonnetModel || savedConfig.defaultSonnetModel;
1587
- const haikuModel = options.haikuModel || savedConfig.defaultHaikuModel;
1588
- try {
1589
- const result = await adapter.link({
1590
- baseUrl,
1591
- apiKey,
1592
- model,
1593
- opusModel,
1594
- sonnetModel,
1595
- haikuModel,
1596
- dryRun: options.dryRun
1597
- });
1598
- console.log(
1599
- formatSuccess(
1600
- `Successfully configured ${pc.bold(pc.cyan(adapter.name))} with SRouter proxy!`
1601
- )
1602
- );
1603
- console.log(` ${pc.gray("Target Config:")} ${pc.white(result.modifiedPath)}`);
1604
- console.log(` ${pc.gray("Proxy URL:")} ${pc.white(baseUrl)}`);
1605
- if (model) {
1606
- console.log(` ${pc.gray("Model:")} ${pc.white(model)}`);
1607
- }
1608
- if (opusModel) {
1609
- console.log(` ${pc.gray("Opus Model:")} ${pc.white(opusModel)}`);
1610
- }
1611
- if (sonnetModel) {
1612
- console.log(` ${pc.gray("Sonnet Model:")} ${pc.white(sonnetModel)}`);
1613
- }
1614
- if (haikuModel) {
1615
- console.log(` ${pc.gray("Haiku Model:")} ${pc.white(haikuModel)}`);
1616
- }
1617
- if (result.backupPath) {
1618
- console.log(` ${pc.gray("Backup Saved:")} ${pc.white(result.backupPath)}`);
1981
+ return DEFAULT_DB_PATH;
1982
+ }
1983
+ var dbPath = getDatabasePath();
1984
+ var dbDir = path6.dirname(dbPath);
1985
+ if (!fs6.existsSync(dbDir)) {
1986
+ fs6.mkdirSync(dbDir, { recursive: true });
1987
+ }
1988
+ var sqliteDb = new DatabaseSync(dbPath);
1989
+ sqliteDb.exec("PRAGMA busy_timeout = 5000;");
1990
+ function execWithRetry(sql, attempts = 5) {
1991
+ for (let i = 0; i < attempts; i++) {
1992
+ try {
1993
+ sqliteDb.exec(sql);
1994
+ return;
1995
+ } catch (error) {
1996
+ const code = error.code;
1997
+ if (code === "SQLITE_BUSY" && i < attempts - 1) {
1998
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200 * (i + 1));
1999
+ continue;
2000
+ }
2001
+ throw error;
1619
2002
  }
1620
- } catch (err) {
1621
- const msg = err instanceof Error ? err.message : String(err);
1622
- console.error(formatError(`Failed to link ${adapter.name}: ${msg}`));
1623
- process.exitCode = 1;
1624
2003
  }
1625
2004
  }
2005
+ execWithRetry("PRAGMA journal_mode = WAL;");
2006
+ execWithRetry("PRAGMA foreign_keys = ON;");
2007
+ execWithRetry("PRAGMA synchronous = NORMAL;");
2008
+ execWithRetry("PRAGMA temp_store = MEMORY;");
2009
+ execWithRetry("PRAGMA cache_size = -20000;");
1626
2010
 
1627
- // src/commands/unlink.ts
1628
- async function unlinkCommand(toolId) {
1629
- const adapter = getAdapter(toolId);
1630
- if (!adapter) {
1631
- console.error(
1632
- formatError(
1633
- `Tool '${pc.bold(toolId)}' not supported. Available tools: ${getAllAdapters().map((a) => a.id).join(", ")}`
1634
- )
1635
- );
1636
- process.exitCode = 1;
1637
- return;
2011
+ // ../../packages/db/dist/client.js
2012
+ import {} from "node:sqlite";
2013
+ var SqliteClient = class {
2014
+ db;
2015
+ constructor(db2) {
2016
+ this.db = db2;
1638
2017
  }
1639
- try {
1640
- const restored = await adapter.unlink();
1641
- if (restored) {
1642
- console.log(
1643
- formatSuccess(
1644
- `Restored original configuration for ${pc.bold(pc.cyan(adapter.name))}!`
1645
- )
1646
- );
1647
- } else {
1648
- console.log(
1649
- formatInfo(
1650
- `No active SRouter configuration or backup found for ${pc.bold(adapter.name)}.`
1651
- )
1652
- );
1653
- }
1654
- } catch (err) {
1655
- const msg = err instanceof Error ? err.message : String(err);
1656
- console.error(formatError(`Failed to unlink ${adapter.name}: ${msg}`));
1657
- process.exitCode = 1;
2018
+ /** Normalize undefined → null (node:sqlite previously tolerated undefined; pg needs null). */
2019
+ normalizeParams(params) {
2020
+ return params.map((p3) => p3 === void 0 ? null : p3);
1658
2021
  }
1659
- }
1660
-
1661
- // src/commands/sync.ts
1662
- async function syncCommand(toolId, options = {}) {
1663
- const savedConfig = await defaultStore.loadConfig();
1664
- const baseUrl = options.url || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
1665
- const apiKey = options.key || savedConfig.defaultApiKey;
1666
- const health = await checkServerHealth(baseUrl, apiKey);
1667
- if (!health.healthy) {
1668
- console.error(
1669
- formatError(
1670
- `Cannot sync: SRouter Gateway is unreachable at ${pc.bold(baseUrl)} (${health.error || "offline"}).`
1671
- )
1672
- );
1673
- process.exitCode = 1;
1674
- return;
2022
+ all(sql, ...params) {
2023
+ return Promise.resolve(this.db.prepare(sql).all(...this.normalizeParams(params)));
1675
2024
  }
1676
- const availableModels = await fetchAvailableModels(baseUrl, apiKey);
1677
- if (availableModels.length === 0) {
1678
- console.warn(
1679
- formatWarning(`SRouter Gateway responded at ${baseUrl}, but returned 0 models.`)
1680
- );
2025
+ get(sql, ...params) {
2026
+ return Promise.resolve(this.db.prepare(sql).get(...this.normalizeParams(params)));
1681
2027
  }
1682
- const adaptersToSync = toolId ? [getAdapter(toolId)].filter((a) => Boolean(a)) : getAllAdapters();
1683
- if (toolId && adaptersToSync.length === 0) {
1684
- console.error(formatError(`Tool '${pc.bold(toolId)}' not supported.`));
1685
- process.exitCode = 1;
1686
- return;
2028
+ run(sql, ...params) {
2029
+ const result = this.db.prepare(sql).run(...this.normalizeParams(params));
2030
+ return Promise.resolve(result);
2031
+ }
2032
+ exec(sql) {
2033
+ this.db.exec(sql);
2034
+ return Promise.resolve();
2035
+ }
2036
+ async tableColumns(table) {
2037
+ const rows = await this.all(`PRAGMA table_info("${table}")`);
2038
+ return rows.map((r) => r.name);
2039
+ }
2040
+ };
2041
+ var pgPool = null;
2042
+ async function getPool() {
2043
+ if (pgPool)
2044
+ return pgPool;
2045
+ const { Pool } = await import("./esm-5KJBUOHT.js");
2046
+ pgPool = new Pool({
2047
+ connectionString: process.env.DATABASE_URL,
2048
+ max: 10,
2049
+ idleTimeoutMillis: 3e4,
2050
+ connectionTimeoutMillis: 1e4,
2051
+ // Heroku Postgres requires TLS; self-signed cert → rejectUnauthorized: false
2052
+ ssl: { rejectUnauthorized: false }
2053
+ });
2054
+ return pgPool;
2055
+ }
2056
+ function toPg(sql) {
2057
+ let i = 0;
2058
+ return sql.replace(/\?/g, () => `$${++i}`);
2059
+ }
2060
+ function splitStatements(sql) {
2061
+ const statements = [];
2062
+ let current = "";
2063
+ let inSingle = false;
2064
+ let inDouble = false;
2065
+ let inDollar = false;
2066
+ for (let i = 0; i < sql.length; i++) {
2067
+ const ch = sql[i];
2068
+ const next = sql[i + 1];
2069
+ if (!inDollar && !inSingle && !inDouble && ch === "'") {
2070
+ inSingle = true;
2071
+ } else if (inSingle && ch === "'" && next !== "'") {
2072
+ inSingle = false;
2073
+ } else if (!inDollar && !inSingle && !inDouble && ch === '"') {
2074
+ inDouble = true;
2075
+ } else if (inDouble && ch === '"' && next !== '"') {
2076
+ inDouble = false;
2077
+ } else if (!inSingle && !inDouble && ch === "$" && /^[a-zA-Z_0-9]*\$/.test(sql.slice(i, i + 40))) {
2078
+ inDollar = true;
2079
+ } else if (inDollar && ch === "$" && next !== "$") {
2080
+ inDollar = false;
2081
+ } else if (!inSingle && !inDouble && !inDollar && ch === ";") {
2082
+ if (current.trim())
2083
+ statements.push(current.trim());
2084
+ current = "";
2085
+ continue;
2086
+ }
2087
+ current += ch;
2088
+ }
2089
+ if (current.trim())
2090
+ statements.push(current.trim());
2091
+ return statements;
2092
+ }
2093
+ var PgClient = class {
2094
+ /** Normalize undefined → null (pg rejects undefined params). */
2095
+ normalizeParams(params) {
2096
+ return params.map((p3) => p3 === void 0 ? null : p3);
2097
+ }
2098
+ async all(sql, ...params) {
2099
+ const pool = await getPool();
2100
+ const result = await pool.query(toPg(sql), this.normalizeParams(params));
2101
+ return result.rows;
2102
+ }
2103
+ async get(sql, ...params) {
2104
+ const pool = await getPool();
2105
+ const result = await pool.query(toPg(sql), this.normalizeParams(params));
2106
+ return result.rows[0] ?? null;
2107
+ }
2108
+ async run(sql, ...params) {
2109
+ const pool = await getPool();
2110
+ const result = await pool.query(toPg(sql), this.normalizeParams(params));
2111
+ return { changes: result.rowCount ?? 0 };
2112
+ }
2113
+ async exec(sql) {
2114
+ const trimmed = sql.trim().toUpperCase();
2115
+ if (trimmed.startsWith("PRAGMA"))
2116
+ return;
2117
+ const pool = await getPool();
2118
+ const statements = splitStatements(sql);
2119
+ for (const stmt of statements) {
2120
+ await pool.query(stmt);
2121
+ }
2122
+ }
2123
+ async tableColumns(table) {
2124
+ const rows = await this.all("SELECT column_name AS name FROM information_schema.columns WHERE table_name = $1", table);
2125
+ return rows.map((r) => r.name);
2126
+ }
2127
+ };
2128
+ var _client = null;
2129
+ function getDbClient() {
2130
+ if (_client)
2131
+ return _client;
2132
+ if (process.env.DATABASE_URL) {
2133
+ _client = new PgClient();
2134
+ } else {
2135
+ _client = new SqliteClient(sqliteDb);
2136
+ }
2137
+ return _client;
2138
+ }
2139
+
2140
+ // ../../packages/db/dist/db.js
2141
+ var SROUTER_DIR2 = path7.join(os5.homedir(), ".srouter");
2142
+ var DEFAULT_DB_PATH2 = path7.join(SROUTER_DIR2, "srouter.db");
2143
+ var LEGACY_DB_LOCATIONS2 = [
2144
+ path7.resolve(process.cwd(), "apps/api/srouter.db"),
2145
+ path7.resolve(process.cwd(), "srouter.db")
2146
+ ];
2147
+ var CompatStatement = class {
2148
+ client;
2149
+ sql;
2150
+ constructor(client, sql) {
2151
+ this.client = client;
2152
+ this.sql = sql;
2153
+ }
2154
+ all(...params) {
2155
+ return this.client.all(this.sql, ...params);
2156
+ }
2157
+ get(...params) {
2158
+ return this.client.get(this.sql, ...params);
2159
+ }
2160
+ run(...params) {
2161
+ return this.client.run(this.sql, ...params);
2162
+ }
2163
+ };
2164
+ var CompatDb = class {
2165
+ client;
2166
+ constructor(client) {
2167
+ this.client = client;
2168
+ }
2169
+ prepare(sql) {
2170
+ return new CompatStatement(this.client, sql);
2171
+ }
2172
+ exec(sql) {
2173
+ return this.client.exec(sql);
2174
+ }
2175
+ };
2176
+ function makeCompatDb() {
2177
+ return new CompatDb(getDbClient());
2178
+ }
2179
+ var db = makeCompatDb();
2180
+ sqliteDb.exec("PRAGMA busy_timeout = 5000;");
2181
+ function execWithRetry2(sql, attempts = 5) {
2182
+ for (let i = 0; i < attempts; i++) {
2183
+ try {
2184
+ sqliteDb.exec(sql);
2185
+ return;
2186
+ } catch (error) {
2187
+ const code = error.code;
2188
+ if (code === "SQLITE_BUSY" && i < attempts - 1) {
2189
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200 * (i + 1));
2190
+ continue;
2191
+ }
2192
+ throw error;
2193
+ }
2194
+ }
2195
+ }
2196
+ execWithRetry2("PRAGMA journal_mode = WAL;");
2197
+ execWithRetry2("PRAGMA foreign_keys = ON;");
2198
+ execWithRetry2("PRAGMA synchronous = NORMAL;");
2199
+ execWithRetry2("PRAGMA temp_store = MEMORY;");
2200
+ execWithRetry2("PRAGMA cache_size = -20000;");
2201
+ function isPostgres() {
2202
+ return Boolean(process.env.DATABASE_URL);
2203
+ }
2204
+ function tableSql(table, pg) {
2205
+ const cols = table.columns.map((c) => `${c.name} ${pg && c.definition.includes("INTEGER") ? "BIGINT" : c.definition}`).join(",\n ");
2206
+ return `CREATE TABLE IF NOT EXISTS ${table.name} (
2207
+ ${cols}
2208
+ );`;
2209
+ }
2210
+ var TABLES = [
2211
+ {
2212
+ name: "providers",
2213
+ columns: [
2214
+ { name: "id", definition: "TEXT PRIMARY KEY" },
2215
+ { name: "provider_id", definition: "TEXT NOT NULL" },
2216
+ { name: "name", definition: "TEXT NOT NULL" },
2217
+ { name: "alias", definition: "TEXT" },
2218
+ { name: "category", definition: "TEXT NOT NULL" },
2219
+ { name: "protocol", definition: "TEXT NOT NULL" },
2220
+ { name: "base_url", definition: "TEXT" },
2221
+ { name: "api_key", definition: "TEXT" },
2222
+ { name: "access_token", definition: "TEXT" },
2223
+ { name: "refresh_token", definition: "TEXT" },
2224
+ { name: "account_id", definition: "TEXT" },
2225
+ { name: "organization_id", definition: "TEXT" },
2226
+ { name: "provider_specific_data", definition: "TEXT" },
2227
+ { name: "custom_headers", definition: "TEXT" },
2228
+ { name: "token_expires_at", definition: "INTEGER" },
2229
+ { name: "last_refreshed_at", definition: "INTEGER" },
2230
+ { name: "enabled", definition: "INTEGER NOT NULL DEFAULT 1" },
2231
+ { name: "created_at", definition: "INTEGER NOT NULL" }
2232
+ ]
2233
+ },
2234
+ {
2235
+ name: "api_keys",
2236
+ columns: [
2237
+ { name: "id", definition: "TEXT PRIMARY KEY" },
2238
+ { name: "key", definition: "TEXT UNIQUE NOT NULL" },
2239
+ { name: "name", definition: "TEXT NOT NULL" },
2240
+ { name: "enabled", definition: "INTEGER NOT NULL DEFAULT 1" },
2241
+ { name: "rate_limit", definition: "INTEGER DEFAULT 0" },
2242
+ { name: "quota_limit", definition: "INTEGER DEFAULT 0" },
2243
+ { name: "usage_tokens", definition: "INTEGER DEFAULT 0" },
2244
+ { name: "credit_limit", definition: "REAL DEFAULT 0" },
2245
+ { name: "usage_cost", definition: "REAL DEFAULT 0" },
2246
+ { name: "allowed_models", definition: "TEXT" },
2247
+ { name: "created_at", definition: "INTEGER NOT NULL" }
2248
+ ]
2249
+ },
2250
+ {
2251
+ name: "request_logs",
2252
+ columns: [
2253
+ { name: "id", definition: "TEXT PRIMARY KEY" },
2254
+ { name: "api_key_id", definition: "TEXT" },
2255
+ { name: "provider_id", definition: "TEXT NOT NULL" },
2256
+ { name: "model", definition: "TEXT NOT NULL" },
2257
+ { name: "prompt_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
2258
+ { name: "completion_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
2259
+ { name: "total_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
2260
+ { name: "status_code", definition: "INTEGER NOT NULL" },
2261
+ { name: "latency_ms", definition: "INTEGER NOT NULL" },
2262
+ { name: "cached_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
2263
+ { name: "cache_creation_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
2264
+ { name: "reasoning_tokens", definition: "INTEGER NOT NULL DEFAULT 0" },
2265
+ { name: "estimated_cost", definition: "REAL NOT NULL DEFAULT 0" },
2266
+ { name: "fallback_occurred", definition: "INTEGER NOT NULL DEFAULT 0" },
2267
+ { name: "fallback_path", definition: "TEXT" },
2268
+ { name: "fallback_reason", definition: "TEXT" },
2269
+ { name: "resolved_model", definition: "TEXT" },
2270
+ { name: "created_at", definition: "INTEGER NOT NULL" }
2271
+ ]
2272
+ },
2273
+ {
2274
+ name: "oauth_sessions",
2275
+ columns: [
2276
+ { name: "state", definition: "TEXT PRIMARY KEY" },
2277
+ { name: "code_verifier", definition: "TEXT NOT NULL" },
2278
+ { name: "client_id", definition: "TEXT NOT NULL" },
2279
+ { name: "redirect_uri", definition: "TEXT NOT NULL" },
2280
+ { name: "created_at", definition: "INTEGER NOT NULL" }
2281
+ ]
2282
+ },
2283
+ {
2284
+ name: "fallback_rules",
2285
+ columns: [
2286
+ { name: "id", definition: "TEXT PRIMARY KEY" },
2287
+ { name: "source_model", definition: "TEXT NOT NULL" },
2288
+ { name: "target_model", definition: "TEXT NOT NULL" },
2289
+ { name: "priority", definition: "INTEGER NOT NULL DEFAULT 1" },
2290
+ { name: "enabled", definition: "INTEGER NOT NULL DEFAULT 1" },
2291
+ { name: "trigger_on_status", definition: "TEXT" },
2292
+ { name: "max_retries", definition: "INTEGER DEFAULT 1" },
2293
+ { name: "created_at", definition: "INTEGER NOT NULL" }
2294
+ ]
2295
+ },
2296
+ {
2297
+ name: "system_settings",
2298
+ columns: [
2299
+ { name: "key", definition: "TEXT PRIMARY KEY" },
2300
+ { name: "value", definition: "TEXT NOT NULL" }
2301
+ ]
2302
+ },
2303
+ {
2304
+ name: "custom_models",
2305
+ columns: [
2306
+ { name: "provider_id", definition: "TEXT NOT NULL" },
2307
+ { name: "model_id", definition: "TEXT NOT NULL" },
2308
+ { name: "created_at", definition: "INTEGER NOT NULL" },
2309
+ { name: "PRIMARY KEY (provider_id, model_id)", definition: "" }
2310
+ ]
2311
+ }
2312
+ ];
2313
+ var INDEXES = [
2314
+ { sql: "CREATE INDEX IF NOT EXISTS idx_request_logs_created_at ON request_logs(created_at DESC);" },
2315
+ { sql: "CREATE INDEX IF NOT EXISTS idx_request_logs_provider_created ON request_logs(provider_id, created_at DESC);" },
2316
+ { sql: "CREATE INDEX IF NOT EXISTS idx_request_logs_provider_model ON request_logs(provider_id, model);" },
2317
+ { sql: "CREATE INDEX IF NOT EXISTS idx_request_logs_model ON request_logs(model);" },
2318
+ { sql: "CREATE INDEX IF NOT EXISTS idx_fallback_rules_priority ON fallback_rules(priority ASC, created_at ASC);" },
2319
+ { sql: "CREATE INDEX IF NOT EXISTS idx_providers_provider_id ON providers(provider_id);" },
2320
+ { sql: "CREATE INDEX IF NOT EXISTS idx_custom_models_provider ON custom_models(provider_id, created_at ASC);" }
2321
+ ];
2322
+ var ADMIN_TABLES = (pg) => {
2323
+ const integer = pg ? "BIGINT" : "INTEGER";
2324
+ return `
2325
+ CREATE TABLE IF NOT EXISTS admin_account (
2326
+ id ${integer} PRIMARY KEY CHECK (id = 1),
2327
+ password_hash TEXT NOT NULL,
2328
+ created_at ${integer} NOT NULL,
2329
+ updated_at ${integer} NOT NULL
2330
+ );
2331
+
2332
+ CREATE TABLE IF NOT EXISTS admin_sessions (
2333
+ token_hash TEXT PRIMARY KEY,
2334
+ created_at ${integer} NOT NULL,
2335
+ expires_at ${integer} NOT NULL
2336
+ );
2337
+ `;
2338
+ };
2339
+ async function ensureColumns(table, columns) {
2340
+ const client = getDbClient();
2341
+ const existing = new Set(await client.tableColumns(table));
2342
+ for (const col of columns) {
2343
+ if (existing.has(col.name))
2344
+ continue;
2345
+ try {
2346
+ await client.exec(`ALTER TABLE ${table} ADD COLUMN ${col.definition};`);
2347
+ existing.add(col.name);
2348
+ } catch (error) {
2349
+ const message = error.message || "";
2350
+ if (!message.includes("duplicate column")) {
2351
+ throw error;
2352
+ }
2353
+ }
2354
+ }
2355
+ }
2356
+ function initDatabase() {
2357
+ if (isPostgres()) {
2358
+ return initPostgresSchema();
2359
+ }
2360
+ initSqliteSchemaSync();
2361
+ }
2362
+ function initSqliteSchemaSync() {
2363
+ const raw = sqliteDb;
2364
+ for (const table of TABLES) {
2365
+ raw.exec(tableSql(table, false));
2366
+ }
2367
+ for (const index of INDEXES) {
2368
+ raw.exec(index.sql);
2369
+ }
2370
+ raw.exec(ADMIN_TABLES(false));
2371
+ const ensureSync = (table, columns) => {
2372
+ const existing = new Set(raw.prepare(`PRAGMA table_info("${table}")`).all().map((col) => col.name));
2373
+ for (const col of columns) {
2374
+ if (existing.has(col.name))
2375
+ continue;
2376
+ try {
2377
+ raw.exec(`ALTER TABLE ${table} ADD COLUMN ${col.definition};`);
2378
+ existing.add(col.name);
2379
+ } catch (error) {
2380
+ const message = error.message || "";
2381
+ if (!message.includes("duplicate column")) {
2382
+ throw error;
2383
+ }
2384
+ }
2385
+ }
2386
+ };
2387
+ ensureSync("providers", [
2388
+ { name: "alias", definition: "alias TEXT" },
2389
+ { name: "refresh_token", definition: "refresh_token TEXT" },
2390
+ { name: "account_id", definition: "account_id TEXT" },
2391
+ { name: "provider_specific_data", definition: "provider_specific_data TEXT" },
2392
+ { name: "token_expires_at", definition: "token_expires_at INTEGER" },
2393
+ { name: "last_refreshed_at", definition: "last_refreshed_at INTEGER" },
2394
+ { name: "organization_id", definition: "organization_id TEXT" }
2395
+ ]);
2396
+ ensureSync("api_keys", [
2397
+ { name: "allowed_models", definition: "allowed_models TEXT" },
2398
+ { name: "credit_limit", definition: "credit_limit REAL DEFAULT 0" },
2399
+ { name: "usage_cost", definition: "usage_cost REAL DEFAULT 0" }
2400
+ ]);
2401
+ ensureSync("request_logs", [
2402
+ { name: "cached_tokens", definition: "cached_tokens INTEGER NOT NULL DEFAULT 0" },
2403
+ { name: "cache_creation_tokens", definition: "cache_creation_tokens INTEGER NOT NULL DEFAULT 0" },
2404
+ { name: "reasoning_tokens", definition: "reasoning_tokens INTEGER NOT NULL DEFAULT 0" },
2405
+ { name: "estimated_cost", definition: "estimated_cost REAL NOT NULL DEFAULT 0" },
2406
+ { name: "fallback_occurred", definition: "fallback_occurred INTEGER NOT NULL DEFAULT 0" },
2407
+ { name: "fallback_path", definition: "fallback_path TEXT" },
2408
+ { name: "fallback_reason", definition: "fallback_reason TEXT" },
2409
+ { name: "resolved_model", definition: "resolved_model TEXT" }
2410
+ ]);
2411
+ }
2412
+ async function initPostgresSchema() {
2413
+ const client = getDbClient();
2414
+ for (const table of TABLES) {
2415
+ await client.exec(tableSql(table, true));
2416
+ }
2417
+ for (const index of INDEXES) {
2418
+ await client.exec(index.sql);
2419
+ }
2420
+ await client.exec(ADMIN_TABLES(true));
2421
+ await ensureColumns("providers", [
2422
+ { name: "alias", definition: "alias TEXT" },
2423
+ { name: "refresh_token", definition: "refresh_token TEXT" },
2424
+ { name: "account_id", definition: "account_id TEXT" },
2425
+ { name: "provider_specific_data", definition: "provider_specific_data TEXT" },
2426
+ { name: "token_expires_at", definition: "token_expires_at INTEGER" },
2427
+ { name: "last_refreshed_at", definition: "last_refreshed_at INTEGER" },
2428
+ { name: "organization_id", definition: "organization_id TEXT" }
2429
+ ]);
2430
+ await ensureColumns("api_keys", [
2431
+ { name: "allowed_models", definition: "allowed_models TEXT" },
2432
+ { name: "credit_limit", definition: "credit_limit REAL DEFAULT 0" },
2433
+ { name: "usage_cost", definition: "usage_cost REAL DEFAULT 0" }
2434
+ ]);
2435
+ await ensureColumns("request_logs", [
2436
+ { name: "cached_tokens", definition: "cached_tokens INTEGER NOT NULL DEFAULT 0" },
2437
+ { name: "cache_creation_tokens", definition: "cache_creation_tokens INTEGER NOT NULL DEFAULT 0" },
2438
+ { name: "reasoning_tokens", definition: "reasoning_tokens INTEGER NOT NULL DEFAULT 0" },
2439
+ { name: "estimated_cost", definition: "estimated_cost REAL NOT NULL DEFAULT 0" },
2440
+ { name: "fallback_occurred", definition: "fallback_occurred INTEGER NOT NULL DEFAULT 0" },
2441
+ { name: "fallback_path", definition: "fallback_path TEXT" },
2442
+ { name: "fallback_reason", definition: "fallback_reason TEXT" },
2443
+ { name: "resolved_model", definition: "resolved_model TEXT" }
2444
+ ]);
2445
+ }
2446
+ if (!isPostgres()) {
2447
+ initDatabase();
2448
+ }
2449
+
2450
+ // ../../packages/db/dist/row-utils.js
2451
+ function str(value, fallback = "") {
2452
+ return value === null || value === void 0 ? fallback : String(value);
2453
+ }
2454
+ function num(value, fallback = 0) {
2455
+ const n = Number(value);
2456
+ return Number.isFinite(n) ? n : fallback;
2457
+ }
2458
+
2459
+ // ../../packages/db/dist/adminAuth.js
2460
+ var AdminAuthStore = class {
2461
+ initialized = false;
2462
+ client;
2463
+ /** Accept a DbClient for isolation (tests use :memory: SqliteClient); defaults to global. */
2464
+ constructor(client) {
2465
+ this.client = client ?? getDbClient();
2466
+ }
2467
+ async ensureTables() {
2468
+ if (this.initialized)
2469
+ return;
2470
+ const pg = Boolean(process.env.DATABASE_URL);
2471
+ const integer = pg ? "BIGINT" : "INTEGER";
2472
+ await this.client.exec(`
2473
+ CREATE TABLE IF NOT EXISTS admin_account (
2474
+ id ${integer} PRIMARY KEY CHECK (id = 1),
2475
+ password_hash TEXT NOT NULL,
2476
+ created_at ${integer} NOT NULL,
2477
+ updated_at ${integer} NOT NULL
2478
+ );
2479
+
2480
+ CREATE TABLE IF NOT EXISTS admin_sessions (
2481
+ token_hash TEXT PRIMARY KEY,
2482
+ created_at ${integer} NOT NULL,
2483
+ expires_at ${integer} NOT NULL
2484
+ );
2485
+ `);
2486
+ this.initialized = true;
2487
+ }
2488
+ async hasAdminAccount() {
2489
+ await this.ensureTables();
2490
+ const Row = await this.client.get("SELECT 1 AS present FROM admin_account WHERE id = 1");
2491
+ return Boolean(Row);
2492
+ }
2493
+ async createAdminAccount(passwordHash, now = Date.now()) {
2494
+ await this.ensureTables();
2495
+ const Result = await this.client.run(`INSERT INTO admin_account (id, password_hash, created_at, updated_at)
2496
+ VALUES (1, ?, ?, ?)
2497
+ ON CONFLICT(id) DO NOTHING`, passwordHash, now, now);
2498
+ return num(Result.changes) > 0;
2499
+ }
2500
+ async getPasswordHash() {
2501
+ await this.ensureTables();
2502
+ const Row = await this.client.get("SELECT password_hash FROM admin_account WHERE id = 1");
2503
+ return Row?.password_hash ? str(Row.password_hash) : null;
2504
+ }
2505
+ async updatePasswordHash(passwordHash, now = Date.now()) {
2506
+ await this.ensureTables();
2507
+ const Result = await this.client.run(`UPDATE admin_account
2508
+ SET password_hash = ?, updated_at = ?
2509
+ WHERE id = 1`, passwordHash, now);
2510
+ return num(Result.changes) > 0;
2511
+ }
2512
+ async createSession(tokenHash, createdAt, expiresAt) {
2513
+ await this.ensureTables();
2514
+ await this.client.run(`INSERT INTO admin_sessions (token_hash, created_at, expires_at)
2515
+ VALUES (?, ?, ?)`, tokenHash, createdAt, expiresAt);
2516
+ }
2517
+ async getSession(tokenHash, now = Date.now()) {
2518
+ await this.ensureTables();
2519
+ await this.client.run("DELETE FROM admin_sessions WHERE expires_at <= ?", now);
2520
+ const Row = await this.client.get(`SELECT token_hash, created_at, expires_at
2521
+ FROM admin_sessions
2522
+ WHERE token_hash = ? AND expires_at > ?`, tokenHash, now);
2523
+ if (!Row)
2524
+ return null;
2525
+ return {
2526
+ tokenHash: str(Row.token_hash),
2527
+ createdAt: num(Row.created_at),
2528
+ expiresAt: num(Row.expires_at)
2529
+ };
2530
+ }
2531
+ async deleteSession(tokenHash) {
2532
+ await this.ensureTables();
2533
+ const Result = await this.client.run("DELETE FROM admin_sessions WHERE token_hash = ?", tokenHash);
2534
+ return num(Result.changes) > 0;
2535
+ }
2536
+ };
2537
+ var adminAuthStore = new AdminAuthStore();
2538
+
2539
+ // src/commands/init.ts
2540
+ var DEFAULT_PORT = "3000";
2541
+ var REPO_URL = "https://github.com/seaavey/SRouter.git";
2542
+ var DOCKER_IMAGE = "ghcr.io/seaavey/srouter:latest";
2543
+ function runProcess(command, args, options) {
2544
+ return new Promise((resolve) => {
2545
+ const child = spawn(command, args, {
2546
+ cwd: options?.cwd,
2547
+ stdio: options?.stdio ?? "inherit"
2548
+ });
2549
+ let stdout = "";
2550
+ let stderr = "";
2551
+ if (options?.stdio === "pipe") {
2552
+ child.stdout?.on("data", (d) => stdout += d.toString());
2553
+ child.stderr?.on("data", (d) => stderr += d.toString());
2554
+ }
2555
+ child.on("close", (code) => {
2556
+ resolve({ code: code ?? 0, stdout, stderr });
2557
+ });
2558
+ child.on("error", (err) => {
2559
+ resolve({ code: 1, stdout, stderr: err.message });
2560
+ });
2561
+ });
2562
+ }
2563
+ async function initDocker(port, detached) {
2564
+ const s = p.spinner();
2565
+ s.start("Checking Docker environment");
2566
+ const hasDocker = await isExecutableInPath("docker");
2567
+ if (!hasDocker) {
2568
+ s.stop(formatError("Docker is not installed or not available in PATH."));
2569
+ const fallbackChoice = await p.select({
2570
+ message: "Docker was not found. What would you like to do?",
2571
+ options: [
2572
+ {
2573
+ value: "source",
2574
+ label: "Run from Source Code instead (Node.js & pnpm)",
2575
+ hint: "Clones repo and builds locally without Docker"
2576
+ },
2577
+ {
2578
+ value: "guide",
2579
+ label: "View Docker installation instructions",
2580
+ hint: "Get download link for your OS"
2581
+ },
2582
+ {
2583
+ value: "exit",
2584
+ label: "Exit",
2585
+ hint: "Cancel initialization"
2586
+ }
2587
+ ]
2588
+ });
2589
+ if (p.isCancel(fallbackChoice) || fallbackChoice === "exit") {
2590
+ p.outro("Initialization cancelled. Install Docker from https://docs.docker.com/get-docker/ and try again.");
2591
+ return;
2592
+ }
2593
+ if (fallbackChoice === "guide") {
2594
+ p.log.message(
2595
+ [
2596
+ "\u{1F4E6} " + pc.bold("How to install Docker:"),
2597
+ `\u2022 Windows / macOS: ${pc.cyan("https://www.docker.com/products/docker-desktop/")}`,
2598
+ `\u2022 Linux (Ubuntu/Debian): ${pc.yellow("curl -fsSL https://get.docker.com | sh")}`,
2599
+ `\u2022 Linux (Arch): ${pc.yellow("sudo pacman -S docker && sudo systemctl enable --now docker")}`
2600
+ ].join("\n")
2601
+ );
2602
+ p.outro("Run 'srouter init' again after installing Docker.");
2603
+ return;
2604
+ }
2605
+ if (fallbackChoice === "source") {
2606
+ const defaultDir = path8.join(os6.homedir(), "srouter");
2607
+ await initSource(defaultDir, port);
2608
+ return;
2609
+ }
2610
+ }
2611
+ s.message("Verifying Docker daemon status");
2612
+ const checkDaemon = await runProcess("docker", ["info"], { stdio: "pipe" });
2613
+ if (checkDaemon.code !== 0) {
2614
+ s.stop(formatError("Docker daemon is not running."));
2615
+ p.log.info("Please start the Docker service/daemon and try again.");
2616
+ process.exitCode = 1;
2617
+ return;
2618
+ }
2619
+ const dataDir = path8.join(SROUTER_DIR2, "data");
2620
+ fs7.mkdirSync(dataDir, { recursive: true, mode: 448 });
2621
+ s.message(`Pulling latest SRouter Docker image (${DOCKER_IMAGE})`);
2622
+ const pullRes = await runProcess("docker", ["pull", DOCKER_IMAGE], { stdio: "inherit" });
2623
+ if (pullRes.code !== 0) {
2624
+ s.stop(formatWarning("Failed to pull image from registry, checking local image cache"));
2625
+ }
2626
+ await runProcess("docker", ["rm", "-f", "srouter"], { stdio: "pipe" });
2627
+ s.message(`Starting SRouter Gateway container on port ${port}`);
2628
+ const dockerArgs = [
2629
+ "run",
2630
+ detached ? "-d" : "-it",
2631
+ "--name",
2632
+ "srouter",
2633
+ "--restart",
2634
+ "unless-stopped",
2635
+ "-p",
2636
+ `${port}:3000`,
2637
+ "-p",
2638
+ "1455:1455",
2639
+ "-v",
2640
+ `${dataDir}:/app/data`,
2641
+ "-e",
2642
+ `PORT=${port}`,
2643
+ "-e",
2644
+ "OAUTH_PORT=1455",
2645
+ "-e",
2646
+ "DATABASE_PATH=/app/data/srouter.db",
2647
+ DOCKER_IMAGE
2648
+ ];
2649
+ if (detached) {
2650
+ const runRes = await runProcess("docker", dockerArgs, { stdio: "pipe" });
2651
+ if (runRes.code !== 0) {
2652
+ s.stop(formatError("Failed to start SRouter Docker container."));
2653
+ p.log.error(runRes.stderr);
2654
+ process.exitCode = 1;
2655
+ return;
2656
+ }
2657
+ s.stop(formatSuccess("SRouter Docker container started successfully!"));
2658
+ p.log.message(
2659
+ [
2660
+ `\u{1F680} Gateway URL: ${pc.bold(pc.cyan(`http://localhost:${port}`))}`,
2661
+ `\u{1F4CA} Web Dashboard: ${pc.bold(pc.cyan(`http://localhost:${port}`))}`,
2662
+ `\u{1F4BE} Persistent Data: ${pc.dim(dataDir)}`,
2663
+ "",
2664
+ `To inspect logs: ${pc.yellow("docker logs -f srouter")}`,
2665
+ `To stop: ${pc.yellow("docker stop srouter")}`
2666
+ ].join("\n")
2667
+ );
2668
+ p.outro("SRouter is ready to use!");
2669
+ } else {
2670
+ s.stop("Launching container in foreground mode...");
2671
+ await runProcess("docker", dockerArgs, { stdio: "inherit" });
2672
+ }
2673
+ }
2674
+ async function initSource(targetDir, port) {
2675
+ const s = p.spinner();
2676
+ s.start("Checking prerequisites (git, node, pnpm)");
2677
+ const hasGit = await isExecutableInPath("git");
2678
+ const hasPnpm = await isExecutableInPath("pnpm");
2679
+ if (!hasGit) {
2680
+ s.stop(formatError("Git is required to clone SRouter repository."));
2681
+ process.exitCode = 1;
2682
+ return;
2683
+ }
2684
+ const resolvedDir = path8.resolve(targetDir);
2685
+ if (!fs7.existsSync(path8.join(resolvedDir, "package.json"))) {
2686
+ s.message(`Cloning SRouter into ${pc.bold(resolvedDir)}`);
2687
+ fs7.mkdirSync(resolvedDir, { recursive: true });
2688
+ const cloneRes = await runProcess("git", ["clone", REPO_URL, resolvedDir], {
2689
+ stdio: "inherit"
2690
+ });
2691
+ if (cloneRes.code !== 0) {
2692
+ s.stop(formatError("Failed to clone SRouter repository."));
2693
+ process.exitCode = 1;
2694
+ return;
2695
+ }
2696
+ } else {
2697
+ p.log.info(`Using existing SRouter repository at ${pc.bold(resolvedDir)}`);
2698
+ }
2699
+ s.message("Installing dependencies with pnpm");
2700
+ const pnpmCmd = hasPnpm ? "pnpm" : "npx";
2701
+ const installArgs = hasPnpm ? ["install"] : ["pnpm", "install"];
2702
+ const installRes = await runProcess(pnpmCmd, installArgs, {
2703
+ cwd: resolvedDir,
2704
+ stdio: "inherit"
2705
+ });
2706
+ if (installRes.code !== 0) {
2707
+ s.stop(formatError("Dependency installation failed."));
2708
+ process.exitCode = 1;
2709
+ return;
2710
+ }
2711
+ s.message("Building SRouter packages, API, and Dashboard");
2712
+ const buildArgs = hasPnpm ? ["run", "build"] : ["pnpm", "run", "build"];
2713
+ const buildRes = await runProcess(pnpmCmd, buildArgs, {
2714
+ cwd: resolvedDir,
2715
+ stdio: "inherit"
2716
+ });
2717
+ if (buildRes.code !== 0) {
2718
+ s.stop(formatError("Build step failed."));
2719
+ process.exitCode = 1;
2720
+ return;
2721
+ }
2722
+ s.stop(formatSuccess("SRouter built successfully!"));
2723
+ p.log.message(
2724
+ [
2725
+ `\u{1F4C1} Project Directory: ${pc.bold(resolvedDir)}`,
2726
+ `\u{1F680} Start Dev Server: ${pc.yellow(`cd ${resolvedDir} && pnpm dev`)}`,
2727
+ `\u26A1 Start Production: ${pc.yellow(`PORT=${port} cd ${resolvedDir}/apps/api && pnpm start`)}`
2728
+ ].join("\n")
2729
+ );
2730
+ const startNow = await p.confirm({
2731
+ message: "Start SRouter API & Web server now?",
2732
+ initialValue: true
2733
+ });
2734
+ if (startNow === true) {
2735
+ p.outro(`Starting SRouter Gateway on port ${port}...`);
2736
+ process.env.PORT = port;
2737
+ const startArgs = hasPnpm ? ["--filter", "api", "start"] : ["pnpm", "--filter", "api", "start"];
2738
+ await runProcess(pnpmCmd, startArgs, {
2739
+ cwd: resolvedDir,
2740
+ stdio: "inherit"
2741
+ });
2742
+ } else {
2743
+ p.outro("Setup complete! You can start SRouter anytime.");
2744
+ }
2745
+ }
2746
+ async function initCommand(options) {
2747
+ p.intro(pc.bold(pc.cyan("\u26A1 SRouter Gateway Initialization")));
2748
+ let mode = options.mode;
2749
+ if (!mode) {
2750
+ const choice = await p.select({
2751
+ message: "How would you like to run SRouter?",
2752
+ options: [
2753
+ {
2754
+ value: "docker",
2755
+ label: "Docker (Recommended)",
2756
+ hint: "Zero-config container with isolated environment & persistent database"
2757
+ },
2758
+ {
2759
+ value: "source",
2760
+ label: "Source Code (Node.js & pnpm)",
2761
+ hint: "Clone repository, build from source, and run natively"
2762
+ }
2763
+ ]
2764
+ });
2765
+ if (p.isCancel(choice)) {
2766
+ p.outro("Initialization cancelled.");
2767
+ return;
2768
+ }
2769
+ mode = choice;
2770
+ }
2771
+ let port = options.port;
2772
+ if (!port && !options.yes) {
2773
+ const inputPort = await p.text({
2774
+ message: "Gateway Port:",
2775
+ defaultValue: DEFAULT_PORT,
2776
+ placeholder: DEFAULT_PORT,
2777
+ validate: (val) => {
2778
+ const n = Number(val);
2779
+ if (Number.isNaN(n) || n <= 0 || n > 65535) {
2780
+ return "Please enter a valid port number (1-65535).";
2781
+ }
2782
+ }
2783
+ });
2784
+ if (p.isCancel(inputPort)) {
2785
+ p.outro("Initialization cancelled.");
2786
+ return;
2787
+ }
2788
+ port = inputPort || DEFAULT_PORT;
2789
+ } else {
2790
+ port = port || DEFAULT_PORT;
2791
+ }
2792
+ if (mode === "docker") {
2793
+ const detached = options.detached ?? true;
2794
+ await initDocker(port, detached);
2795
+ } else {
2796
+ const defaultDir = path8.join(os6.homedir(), "srouter");
2797
+ let targetDir = options.dir;
2798
+ if (!targetDir && !options.yes) {
2799
+ const inputDir = await p.text({
2800
+ message: "Target directory for SRouter source code:",
2801
+ defaultValue: defaultDir,
2802
+ placeholder: defaultDir
2803
+ });
2804
+ if (p.isCancel(inputDir)) {
2805
+ p.outro("Initialization cancelled.");
2806
+ return;
2807
+ }
2808
+ targetDir = inputDir || defaultDir;
2809
+ } else {
2810
+ targetDir = targetDir || defaultDir;
2811
+ }
2812
+ await initSource(targetDir, port);
2813
+ }
2814
+ }
2815
+
2816
+ // src/commands/link.ts
2817
+ async function linkCommand(toolId, options) {
2818
+ const adapter = getAdapter(toolId);
2819
+ if (!adapter) {
2820
+ console.error(
2821
+ formatError(
2822
+ `Tool '${pc.bold(toolId)}' not supported. Available tools: ${getAllAdapters().map((a) => a.id).join(", ")}`
2823
+ )
2824
+ );
2825
+ process.exitCode = 1;
2826
+ return;
2827
+ }
2828
+ const savedConfig = await defaultStore.loadConfig();
2829
+ const baseUrl = options.url || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
2830
+ const apiKey = options.key || savedConfig.defaultApiKey;
2831
+ const model = options.model || savedConfig.defaultModel;
2832
+ const opusModel = options.opusModel || savedConfig.defaultOpusModel;
2833
+ const sonnetModel = options.sonnetModel || savedConfig.defaultSonnetModel;
2834
+ const haikuModel = options.haikuModel || savedConfig.defaultHaikuModel;
2835
+ try {
2836
+ const result = await adapter.link({
2837
+ baseUrl,
2838
+ apiKey,
2839
+ model,
2840
+ opusModel,
2841
+ sonnetModel,
2842
+ haikuModel,
2843
+ dryRun: options.dryRun
2844
+ });
2845
+ console.log(
2846
+ formatSuccess(
2847
+ `Successfully configured ${pc.bold(pc.cyan(adapter.name))} with SRouter proxy!`
2848
+ )
2849
+ );
2850
+ console.log(` ${pc.gray("Target Config:")} ${pc.white(result.modifiedPath)}`);
2851
+ console.log(` ${pc.gray("Proxy URL:")} ${pc.white(baseUrl)}`);
2852
+ if (model) {
2853
+ console.log(` ${pc.gray("Model:")} ${pc.white(model)}`);
2854
+ }
2855
+ if (opusModel) {
2856
+ console.log(` ${pc.gray("Opus Model:")} ${pc.white(opusModel)}`);
2857
+ }
2858
+ if (sonnetModel) {
2859
+ console.log(` ${pc.gray("Sonnet Model:")} ${pc.white(sonnetModel)}`);
2860
+ }
2861
+ if (haikuModel) {
2862
+ console.log(` ${pc.gray("Haiku Model:")} ${pc.white(haikuModel)}`);
2863
+ }
2864
+ if (result.backupPath) {
2865
+ console.log(` ${pc.gray("Backup Saved:")} ${pc.white(result.backupPath)}`);
2866
+ }
2867
+ } catch (err) {
2868
+ const msg = err instanceof Error ? err.message : String(err);
2869
+ console.error(formatError(`Failed to link ${adapter.name}: ${msg}`));
2870
+ process.exitCode = 1;
2871
+ }
2872
+ }
2873
+
2874
+ // src/commands/unlink.ts
2875
+ async function unlinkCommand(toolId) {
2876
+ const adapter = getAdapter(toolId);
2877
+ if (!adapter) {
2878
+ console.error(
2879
+ formatError(
2880
+ `Tool '${pc.bold(toolId)}' not supported. Available tools: ${getAllAdapters().map((a) => a.id).join(", ")}`
2881
+ )
2882
+ );
2883
+ process.exitCode = 1;
2884
+ return;
2885
+ }
2886
+ try {
2887
+ const restored = await adapter.unlink();
2888
+ if (restored) {
2889
+ console.log(
2890
+ formatSuccess(
2891
+ `Restored original configuration for ${pc.bold(pc.cyan(adapter.name))}!`
2892
+ )
2893
+ );
2894
+ } else {
2895
+ console.log(
2896
+ formatInfo(
2897
+ `No active SRouter configuration or backup found for ${pc.bold(adapter.name)}.`
2898
+ )
2899
+ );
2900
+ }
2901
+ } catch (err) {
2902
+ const msg = err instanceof Error ? err.message : String(err);
2903
+ console.error(formatError(`Failed to unlink ${adapter.name}: ${msg}`));
2904
+ process.exitCode = 1;
2905
+ }
2906
+ }
2907
+
2908
+ // src/commands/sync.ts
2909
+ async function syncCommand(toolId, options = {}) {
2910
+ const savedConfig = await defaultStore.loadConfig();
2911
+ const baseUrl = options.url || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
2912
+ const apiKey = options.key || savedConfig.defaultApiKey;
2913
+ const health = await checkServerHealth(baseUrl, apiKey);
2914
+ if (!health.healthy) {
2915
+ console.error(
2916
+ formatError(
2917
+ `Cannot sync: SRouter Gateway is unreachable at ${pc.bold(baseUrl)} (${health.error || "offline"}).`
2918
+ )
2919
+ );
2920
+ process.exitCode = 1;
2921
+ return;
2922
+ }
2923
+ const availableModels = await fetchAvailableModels(baseUrl, apiKey);
2924
+ if (availableModels.length === 0) {
2925
+ console.warn(
2926
+ formatWarning(`SRouter Gateway responded at ${baseUrl}, but returned 0 models.`)
2927
+ );
2928
+ }
2929
+ const adaptersToSync = toolId ? [getAdapter(toolId)].filter((a) => Boolean(a)) : getAllAdapters();
2930
+ if (toolId && adaptersToSync.length === 0) {
2931
+ console.error(formatError(`Tool '${pc.bold(toolId)}' not supported.`));
2932
+ process.exitCode = 1;
2933
+ return;
1687
2934
  }
1688
2935
  for (const adapter of adaptersToSync) {
1689
2936
  if (!adapter) continue;
@@ -1848,7 +3095,7 @@ async function envCommand(toolId, options = {}) {
1848
3095
  }
1849
3096
 
1850
3097
  // src/commands/run.ts
1851
- import { spawn } from "node:child_process";
3098
+ import { spawn as spawn2 } from "node:child_process";
1852
3099
  async function runCommand(toolId, toolArgs, options = {}) {
1853
3100
  const adapter = getAdapter(toolId);
1854
3101
  if (!adapter) {
@@ -1876,7 +3123,7 @@ async function runCommand(toolId, toolArgs, options = {}) {
1876
3123
  haikuModel
1877
3124
  });
1878
3125
  const binaryName = toolId === "claude" ? "claude" : "opencode";
1879
- const child = spawn(binaryName, toolArgs, {
3126
+ const child = spawn2(binaryName, toolArgs, {
1880
3127
  stdio: "inherit",
1881
3128
  env: {
1882
3129
  ...process.env,
@@ -1903,254 +3150,11 @@ async function runCommand(toolId, toolArgs, options = {}) {
1903
3150
  }
1904
3151
 
1905
3152
  // src/commands/migrate.ts
1906
- import fs6 from "node:fs";
1907
- import os5 from "node:os";
1908
- import path6 from "node:path";
1909
- import * as p from "@clack/prompts";
3153
+ import fs8 from "node:fs";
3154
+ import os7 from "node:os";
3155
+ import path9 from "node:path";
3156
+ import * as p2 from "@clack/prompts";
1910
3157
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
1911
-
1912
- // ../../packages/db/dist/db.js
1913
- import { DatabaseSync } from "node:sqlite";
1914
- import path5 from "node:path";
1915
- import fs5 from "node:fs";
1916
- import os4 from "node:os";
1917
- var SROUTER_DIR = path5.join(os4.homedir(), ".srouter");
1918
- var DEFAULT_DB_PATH = path5.join(SROUTER_DIR, "srouter.db");
1919
- var LEGACY_DB_LOCATIONS = [
1920
- path5.resolve(process.cwd(), "apps/api/srouter.db"),
1921
- path5.resolve(process.cwd(), "srouter.db")
1922
- ];
1923
- function getDatabasePath() {
1924
- if (process.env.DATABASE_PATH)
1925
- return process.env.DATABASE_PATH;
1926
- for (const legacyPath of LEGACY_DB_LOCATIONS) {
1927
- if (fs5.existsSync(legacyPath))
1928
- return legacyPath;
1929
- }
1930
- return DEFAULT_DB_PATH;
1931
- }
1932
- var dbPath = getDatabasePath();
1933
- var dbDir = path5.dirname(dbPath);
1934
- if (!fs5.existsSync(dbDir)) {
1935
- fs5.mkdirSync(dbDir, { recursive: true });
1936
- }
1937
- var db = new DatabaseSync(dbPath);
1938
- db.exec("PRAGMA busy_timeout = 5000;");
1939
- function execWithRetry(sql, attempts = 5) {
1940
- for (let i = 0; i < attempts; i++) {
1941
- try {
1942
- db.exec(sql);
1943
- return;
1944
- } catch (error) {
1945
- const code = error.code;
1946
- if (code === "SQLITE_BUSY" && i < attempts - 1) {
1947
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200 * (i + 1));
1948
- continue;
1949
- }
1950
- throw error;
1951
- }
1952
- }
1953
- }
1954
- execWithRetry("PRAGMA journal_mode = WAL;");
1955
- execWithRetry("PRAGMA foreign_keys = ON;");
1956
- function ensureColumns(table, columns) {
1957
- const existing = new Set(db.prepare(`PRAGMA table_info("${table}")`).all().map((col) => col.name));
1958
- for (const col of columns) {
1959
- if (existing.has(col.name))
1960
- continue;
1961
- try {
1962
- db.exec(`ALTER TABLE ${table} ADD COLUMN ${col.definition};`);
1963
- existing.add(col.name);
1964
- } catch (error) {
1965
- const message = error.message || "";
1966
- if (!message.includes("duplicate column name")) {
1967
- throw error;
1968
- }
1969
- }
1970
- }
1971
- }
1972
- function initDatabase() {
1973
- db.exec(`
1974
- CREATE TABLE IF NOT EXISTS providers (
1975
- id TEXT PRIMARY KEY,
1976
- provider_id TEXT NOT NULL,
1977
- name TEXT NOT NULL,
1978
- category TEXT NOT NULL,
1979
- protocol TEXT NOT NULL,
1980
- base_url TEXT,
1981
- api_key TEXT,
1982
- access_token TEXT,
1983
- refresh_token TEXT,
1984
- custom_headers TEXT,
1985
- enabled INTEGER NOT NULL DEFAULT 1,
1986
- created_at INTEGER NOT NULL
1987
- );
1988
- `);
1989
- ensureColumns("providers", [
1990
- { name: "refresh_token", definition: "refresh_token TEXT" },
1991
- // Multi-account OAuth binding (e.g. Codex ChatGPT-Account-ID)
1992
- { name: "account_id", definition: "account_id TEXT" },
1993
- // Provider-specific metadata (for example Kiro auth method/region/profile ARN)
1994
- { name: "provider_specific_data", definition: "provider_specific_data TEXT" },
1995
- // Token expiry tracking
1996
- { name: "token_expires_at", definition: "token_expires_at INTEGER" },
1997
- { name: "last_refreshed_at", definition: "last_refreshed_at INTEGER" },
1998
- // Claude OAuth organization binding
1999
- { name: "organization_id", definition: "organization_id TEXT" }
2000
- ]);
2001
- db.exec(`
2002
- CREATE TABLE IF NOT EXISTS api_keys (
2003
- id TEXT PRIMARY KEY,
2004
- key TEXT UNIQUE NOT NULL,
2005
- name TEXT NOT NULL,
2006
- enabled INTEGER NOT NULL DEFAULT 1,
2007
- rate_limit INTEGER DEFAULT 0,
2008
- quota_limit INTEGER DEFAULT 0,
2009
- usage_tokens INTEGER DEFAULT 0,
2010
- created_at INTEGER NOT NULL
2011
- );
2012
- `);
2013
- db.exec(`
2014
- CREATE TABLE IF NOT EXISTS request_logs (
2015
- id TEXT PRIMARY KEY,
2016
- api_key_id TEXT,
2017
- provider_id TEXT NOT NULL,
2018
- model TEXT NOT NULL,
2019
- prompt_tokens INTEGER NOT NULL DEFAULT 0,
2020
- completion_tokens INTEGER NOT NULL DEFAULT 0,
2021
- total_tokens INTEGER NOT NULL DEFAULT 0,
2022
- status_code INTEGER NOT NULL,
2023
- latency_ms INTEGER NOT NULL,
2024
- created_at INTEGER NOT NULL
2025
- );
2026
- `);
2027
- db.exec(`
2028
- CREATE TABLE IF NOT EXISTS oauth_sessions (
2029
- state TEXT PRIMARY KEY,
2030
- code_verifier TEXT NOT NULL,
2031
- client_id TEXT NOT NULL,
2032
- redirect_uri TEXT NOT NULL,
2033
- created_at INTEGER NOT NULL
2034
- );
2035
- `);
2036
- ensureColumns("request_logs", [
2037
- { name: "cached_tokens", definition: "cached_tokens INTEGER NOT NULL DEFAULT 0" },
2038
- {
2039
- name: "cache_creation_tokens",
2040
- definition: "cache_creation_tokens INTEGER NOT NULL DEFAULT 0"
2041
- },
2042
- { name: "reasoning_tokens", definition: "reasoning_tokens INTEGER NOT NULL DEFAULT 0" },
2043
- { name: "estimated_cost", definition: "estimated_cost REAL NOT NULL DEFAULT 0" },
2044
- { name: "fallback_occurred", definition: "fallback_occurred INTEGER NOT NULL DEFAULT 0" },
2045
- { name: "fallback_path", definition: "fallback_path TEXT" },
2046
- { name: "fallback_reason", definition: "fallback_reason TEXT" },
2047
- { name: "resolved_model", definition: "resolved_model TEXT" }
2048
- ]);
2049
- db.exec(`
2050
- CREATE TABLE IF NOT EXISTS fallback_rules (
2051
- id TEXT PRIMARY KEY,
2052
- source_model TEXT NOT NULL,
2053
- target_model TEXT NOT NULL,
2054
- priority INTEGER NOT NULL DEFAULT 1,
2055
- enabled INTEGER NOT NULL DEFAULT 1,
2056
- trigger_on_status TEXT,
2057
- max_retries INTEGER DEFAULT 1,
2058
- created_at INTEGER NOT NULL
2059
- );
2060
- `);
2061
- db.exec(`
2062
- CREATE TABLE IF NOT EXISTS system_settings (
2063
- key TEXT PRIMARY KEY,
2064
- value TEXT NOT NULL
2065
- );
2066
- `);
2067
- db.exec(`
2068
- CREATE TABLE IF NOT EXISTS custom_models (
2069
- provider_id TEXT NOT NULL,
2070
- model_id TEXT NOT NULL,
2071
- created_at INTEGER NOT NULL,
2072
- PRIMARY KEY (provider_id, model_id)
2073
- );
2074
- `);
2075
- }
2076
- initDatabase();
2077
-
2078
- // ../../packages/db/dist/row-utils.js
2079
- function str(value, fallback = "") {
2080
- return value === null || value === void 0 ? fallback : String(value);
2081
- }
2082
- function num(value, fallback = 0) {
2083
- const n = Number(value);
2084
- return Number.isFinite(n) ? n : fallback;
2085
- }
2086
-
2087
- // ../../packages/db/dist/adminAuth.js
2088
- var AdminAuthStore = class {
2089
- database;
2090
- constructor(database = db) {
2091
- this.database = database;
2092
- this.database.exec(`
2093
- CREATE TABLE IF NOT EXISTS admin_account (
2094
- id INTEGER PRIMARY KEY CHECK (id = 1),
2095
- password_hash TEXT NOT NULL,
2096
- created_at INTEGER NOT NULL,
2097
- updated_at INTEGER NOT NULL
2098
- );
2099
-
2100
- CREATE TABLE IF NOT EXISTS admin_sessions (
2101
- token_hash TEXT PRIMARY KEY,
2102
- created_at INTEGER NOT NULL,
2103
- expires_at INTEGER NOT NULL
2104
- );
2105
- `);
2106
- }
2107
- hasAdminAccount() {
2108
- const Row = this.database.prepare("SELECT 1 AS present FROM admin_account WHERE id = 1").get();
2109
- return Boolean(Row);
2110
- }
2111
- createAdminAccount(passwordHash, now = Date.now()) {
2112
- const Result = this.database.prepare(`INSERT OR IGNORE INTO admin_account (id, password_hash, created_at, updated_at)
2113
- VALUES (1, ?, ?, ?)`).run(passwordHash, now, now);
2114
- return num(Result.changes) > 0;
2115
- }
2116
- getPasswordHash() {
2117
- const Row = this.database.prepare("SELECT password_hash FROM admin_account WHERE id = 1").get();
2118
- return Row?.password_hash ? str(Row.password_hash) : null;
2119
- }
2120
- updatePasswordHash(passwordHash, now = Date.now()) {
2121
- const Result = this.database.prepare(`UPDATE admin_account
2122
- SET password_hash = ?, updated_at = ?
2123
- WHERE id = 1`).run(passwordHash, now);
2124
- return num(Result.changes) > 0;
2125
- }
2126
- createSession(tokenHash, createdAt, expiresAt) {
2127
- this.database.prepare(`INSERT INTO admin_sessions (token_hash, created_at, expires_at)
2128
- VALUES (?, ?, ?)`).run(tokenHash, createdAt, expiresAt);
2129
- }
2130
- getSession(tokenHash, now = Date.now()) {
2131
- this.database.prepare("DELETE FROM admin_sessions WHERE expires_at <= ?").run(now);
2132
- const Row = this.database.prepare(`SELECT token_hash, created_at, expires_at
2133
- FROM admin_sessions
2134
- WHERE token_hash = ? AND expires_at > ?`).get(tokenHash, now);
2135
- if (!Row)
2136
- return null;
2137
- return {
2138
- tokenHash: str(Row.token_hash),
2139
- createdAt: num(Row.created_at),
2140
- expiresAt: num(Row.expires_at)
2141
- };
2142
- }
2143
- deleteSession(tokenHash) {
2144
- const Result = this.database.prepare("DELETE FROM admin_sessions WHERE token_hash = ?").run(tokenHash);
2145
- return num(Result.changes) > 0;
2146
- }
2147
- };
2148
- var adminAuthStore = new AdminAuthStore();
2149
-
2150
- // ../../packages/db/dist/quota.js
2151
- var CN_REFILL_GAP_MS = 2 * 24 * 60 * 60 * 1e3;
2152
-
2153
- // src/commands/migrate.ts
2154
3158
  function parseDateToTimestamp(val) {
2155
3159
  if (typeof val === "number" && Number.isFinite(val)) return val;
2156
3160
  if (typeof val === "string") {
@@ -2279,58 +3283,58 @@ function importNineRouterJson(data, targetDb, action) {
2279
3283
  }
2280
3284
  return { inserted, skipped, tablesCount };
2281
3285
  }
2282
- var TargetDbPath = DEFAULT_DB_PATH;
2283
- var BackupDir = path6.join(SROUTER_DIR, "backups");
3286
+ var TargetDbPath = DEFAULT_DB_PATH2;
3287
+ var BackupDir = path9.join(SROUTER_DIR2, "backups");
2284
3288
  var NineRouterDbLocations = [
2285
- path6.join(os5.homedir(), ".9router", "srouter.db"),
2286
- path6.join(os5.homedir(), ".9router", "9router.db"),
2287
- path6.join(os5.homedir(), "9router", "srouter.db"),
2288
- path6.join(os5.homedir(), "9router", "data.db"),
2289
- path6.join(os5.homedir(), "9router", "db", "srouter.db"),
2290
- path6.join(os5.homedir(), ".config", "9router", "srouter.db"),
2291
- path6.join(os5.homedir(), ".config", "9router", "9router.db"),
3289
+ path9.join(os7.homedir(), ".9router", "srouter.db"),
3290
+ path9.join(os7.homedir(), ".9router", "9router.db"),
3291
+ path9.join(os7.homedir(), "9router", "srouter.db"),
3292
+ path9.join(os7.homedir(), "9router", "data.db"),
3293
+ path9.join(os7.homedir(), "9router", "db", "srouter.db"),
3294
+ path9.join(os7.homedir(), ".config", "9router", "srouter.db"),
3295
+ path9.join(os7.homedir(), ".config", "9router", "9router.db"),
2292
3296
  "/root/9router/srouter.db",
2293
3297
  "/root/project/9router/db/srouter.db",
2294
- ...LEGACY_DB_LOCATIONS
3298
+ ...LEGACY_DB_LOCATIONS2
2295
3299
  ];
2296
3300
  function fileKb(filePath) {
2297
- return `${(fs6.statSync(filePath).size / 1024).toFixed(2)} KB`;
3301
+ return `${(fs8.statSync(filePath).size / 1024).toFixed(2)} KB`;
2298
3302
  }
2299
3303
  function ensureDirs() {
2300
- fs6.mkdirSync(SROUTER_DIR, { recursive: true, mode: 448 });
2301
- fs6.mkdirSync(BackupDir, { recursive: true, mode: 493 });
3304
+ fs8.mkdirSync(SROUTER_DIR2, { recursive: true, mode: 448 });
3305
+ fs8.mkdirSync(BackupDir, { recursive: true, mode: 493 });
2302
3306
  }
2303
3307
  function backupDb(source, label) {
2304
3308
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2305
- const backupPath = path6.join(BackupDir, `${label}-backup-${timestamp}.db`);
2306
- fs6.copyFileSync(source, backupPath);
2307
- p.log.step(`Backed up to ${pc.dim(backupPath)}`);
3309
+ const backupPath = path9.join(BackupDir, `${label}-backup-${timestamp}.db`);
3310
+ fs8.copyFileSync(source, backupPath);
3311
+ p2.log.step(`Backed up to ${pc.dim(backupPath)}`);
2308
3312
  return backupPath;
2309
3313
  }
2310
3314
  function getDownloadDirs() {
2311
- const home = os5.homedir();
3315
+ const home = os7.homedir();
2312
3316
  const dirs = [
2313
- path6.join(home, "Downloads"),
2314
- path6.join(home, "download"),
2315
- path6.join(home, "downloads"),
2316
- path6.join(home, "Desktop")
3317
+ path9.join(home, "Downloads"),
3318
+ path9.join(home, "download"),
3319
+ path9.join(home, "downloads"),
3320
+ path9.join(home, "Desktop")
2317
3321
  ];
2318
3322
  if (process.platform === "win32") {
2319
3323
  if (process.env.USERPROFILE) {
2320
- dirs.push(path6.join(process.env.USERPROFILE, "Downloads"));
2321
- dirs.push(path6.join(process.env.USERPROFILE, "Desktop"));
3324
+ dirs.push(path9.join(process.env.USERPROFILE, "Downloads"));
3325
+ dirs.push(path9.join(process.env.USERPROFILE, "Desktop"));
2322
3326
  }
2323
3327
  }
2324
- return Array.from(new Set(dirs.filter((d) => fs6.existsSync(d))));
3328
+ return Array.from(new Set(dirs.filter((d) => fs8.existsSync(d))));
2325
3329
  }
2326
3330
  function scanFor9RouterFiles() {
2327
3331
  const results = [];
2328
3332
  const seen = /* @__PURE__ */ new Set();
2329
3333
  const addFile = (filePath, label) => {
2330
- const resolved = path6.resolve(filePath);
2331
- if (!seen.has(resolved) && fs6.existsSync(resolved)) {
3334
+ const resolved = path9.resolve(filePath);
3335
+ if (!seen.has(resolved) && fs8.existsSync(resolved)) {
2332
3336
  try {
2333
- const stat = fs6.statSync(resolved);
3337
+ const stat = fs8.statSync(resolved);
2334
3338
  if (stat.isFile()) {
2335
3339
  seen.add(resolved);
2336
3340
  results.push({ path: resolved, label, mtime: stat.mtimeMs });
@@ -2344,12 +3348,12 @@ function scanFor9RouterFiles() {
2344
3348
  }
2345
3349
  for (const downloadDir of getDownloadDirs()) {
2346
3350
  try {
2347
- const files = fs6.readdirSync(downloadDir);
3351
+ const files = fs8.readdirSync(downloadDir);
2348
3352
  for (const file of files) {
2349
3353
  const lower = file.toLowerCase();
2350
- const fullPath = path6.join(downloadDir, file);
3354
+ const fullPath = path9.join(downloadDir, file);
2351
3355
  if (lower.includes("9router") || lower.includes("srouter-backup") || lower.endsWith(".db") || lower.endsWith(".json") && lower.includes("backup")) {
2352
- addFile(fullPath, path6.basename(downloadDir));
3356
+ addFile(fullPath, path9.basename(downloadDir));
2353
3357
  }
2354
3358
  }
2355
3359
  } catch {
@@ -2359,88 +3363,88 @@ function scanFor9RouterFiles() {
2359
3363
  }
2360
3364
  function findDatabase(candidates, label) {
2361
3365
  for (const candidate of candidates) {
2362
- const fullPath = path6.resolve(candidate);
2363
- if (fs6.existsSync(fullPath)) {
2364
- p.log.info(`Found ${label} database: ${pc.bold(fullPath)} (${fileKb(fullPath)})`);
3366
+ const fullPath = path9.resolve(candidate);
3367
+ if (fs8.existsSync(fullPath)) {
3368
+ p2.log.info(`Found ${label} database: ${pc.bold(fullPath)} (${fileKb(fullPath)})`);
2365
3369
  return fullPath;
2366
3370
  }
2367
3371
  }
2368
3372
  return null;
2369
3373
  }
2370
3374
  async function migrateDb(options) {
2371
- p.intro("SRouter Database Migration");
2372
- if (fs6.existsSync(TargetDbPath)) {
2373
- p.log.warn(`Database already exists at ${TargetDbPath} (${fileKb(TargetDbPath)})`);
2374
- p.outro("No migration needed \u2014 already using the new location.");
3375
+ p2.intro("SRouter Database Migration");
3376
+ if (fs8.existsSync(TargetDbPath)) {
3377
+ p2.log.warn(`Database already exists at ${TargetDbPath} (${fileKb(TargetDbPath)})`);
3378
+ p2.outro("No migration needed \u2014 already using the new location.");
2375
3379
  return;
2376
3380
  }
2377
- const source = options.source && fs6.existsSync(options.source) ? path6.resolve(options.source) : findDatabase(LEGACY_DB_LOCATIONS, "legacy");
3381
+ const source = options.source && fs8.existsSync(options.source) ? path9.resolve(options.source) : findDatabase(LEGACY_DB_LOCATIONS2, "legacy");
2378
3382
  if (!source) {
2379
- p.log.error("No existing database found.");
2380
- p.outro(
3383
+ p2.log.error("No existing database found.");
3384
+ p2.outro(
2381
3385
  `Start SRouter and a fresh database will be created at ${TargetDbPath}, or pass --source /path/to/srouter.db.`
2382
3386
  );
2383
3387
  return;
2384
3388
  }
2385
3389
  ensureDirs();
2386
- const proceed = options.yes || await p.confirm({ message: `Migrate database from ${source}?` }) === true;
3390
+ const proceed = options.yes || await p2.confirm({ message: `Migrate database from ${source}?` }) === true;
2387
3391
  if (!proceed) {
2388
- p.outro("Migration cancelled.");
3392
+ p2.outro("Migration cancelled.");
2389
3393
  return;
2390
3394
  }
2391
3395
  try {
2392
3396
  backupDb(source, "srouter");
2393
- fs6.copyFileSync(source, TargetDbPath);
2394
- fs6.chmodSync(TargetDbPath, 384);
2395
- p.log.success(`Database moved to ${TargetDbPath}`);
2396
- p.outro("Restart SRouter to use the migrated database.");
3397
+ fs8.copyFileSync(source, TargetDbPath);
3398
+ fs8.chmodSync(TargetDbPath, 384);
3399
+ p2.log.success(`Database moved to ${TargetDbPath}`);
3400
+ p2.outro("Restart SRouter to use the migrated database.");
2397
3401
  } catch (error) {
2398
- p.log.error(formatError(`Migration failed: ${error.message}`));
3402
+ p2.log.error(formatError(`Migration failed: ${error.message}`));
2399
3403
  process.exitCode = 1;
2400
3404
  }
2401
3405
  }
2402
3406
  async function migrateNineRouter(options) {
2403
- p.intro("9Router \u2192 SRouter Database Migration");
2404
- let source = options.source && fs6.existsSync(options.source) ? path6.resolve(options.source) : null;
3407
+ p2.intro("9Router \u2192 SRouter Database Migration");
3408
+ let source = options.source && fs8.existsSync(options.source) ? path9.resolve(options.source) : null;
2405
3409
  if (!source) {
2406
3410
  const foundFiles = scanFor9RouterFiles();
2407
3411
  if (foundFiles.length === 1) {
2408
3412
  source = foundFiles[0].path;
2409
- p.log.info(`Found 9Router file: ${pc.bold(source)} (${fileKb(source)}) [${foundFiles[0].label}]`);
3413
+ p2.log.info(`Found 9Router file: ${pc.bold(source)} (${fileKb(source)}) [${foundFiles[0].label}]`);
2410
3414
  } else if (foundFiles.length > 1) {
2411
3415
  if (options.yes) {
2412
3416
  source = foundFiles[0].path;
2413
- p.log.info(`Auto-selected latest 9Router file: ${pc.bold(source)} (${fileKb(source)})`);
3417
+ p2.log.info(`Auto-selected latest 9Router file: ${pc.bold(source)} (${fileKb(source)})`);
2414
3418
  } else {
2415
- const choice = await p.select({
3419
+ const choice = await p2.select({
2416
3420
  message: "Multiple 9Router database/backup files found. Select one to migrate:",
2417
3421
  options: [
2418
3422
  ...foundFiles.map((f) => ({
2419
3423
  value: f.path,
2420
- label: `${path6.basename(f.path)} (${fileKb(f.path)})`,
3424
+ label: `${path9.basename(f.path)} (${fileKb(f.path)})`,
2421
3425
  hint: `${f.label} \u2022 ${f.path}`
2422
3426
  })),
2423
3427
  { value: "custom", label: "Enter custom path manually..." }
2424
3428
  ]
2425
3429
  });
2426
- if (p.isCancel(choice)) {
2427
- p.outro("Migration cancelled.");
3430
+ if (p2.isCancel(choice)) {
3431
+ p2.outro("Migration cancelled.");
2428
3432
  return;
2429
3433
  }
2430
3434
  if (choice === "custom") {
2431
- const customPath = await p.text({
3435
+ const customPath = await p2.text({
2432
3436
  message: "Enter path to 9Router .db or .json backup file:",
2433
3437
  validate: (val) => {
2434
- if (!val || !fs6.existsSync(path6.resolve(val))) {
3438
+ if (!val || !fs8.existsSync(path9.resolve(val))) {
2435
3439
  return "File does not exist. Please check the path.";
2436
3440
  }
2437
3441
  }
2438
3442
  });
2439
- if (p.isCancel(customPath) || !customPath) {
2440
- p.outro("Migration cancelled.");
3443
+ if (p2.isCancel(customPath) || !customPath) {
3444
+ p2.outro("Migration cancelled.");
2441
3445
  return;
2442
3446
  }
2443
- source = path6.resolve(customPath);
3447
+ source = path9.resolve(customPath);
2444
3448
  } else {
2445
3449
  source = choice;
2446
3450
  }
@@ -2448,21 +3452,21 @@ async function migrateNineRouter(options) {
2448
3452
  }
2449
3453
  }
2450
3454
  if (!source) {
2451
- p.log.error("No 9Router database or backup file found.");
2452
- p.outro(
3455
+ p2.log.error("No 9Router database or backup file found.");
3456
+ p2.outro(
2453
3457
  "Pass the location explicitly: srouter migrate 9router --source /path/to/9router-backup.json"
2454
3458
  );
2455
3459
  process.exitCode = 1;
2456
3460
  return;
2457
3461
  }
2458
- const existingTarget = fs6.existsSync(TargetDbPath);
3462
+ const existingTarget = fs8.existsSync(TargetDbPath);
2459
3463
  let action = options.action ?? "copy";
2460
3464
  if (existingTarget && !options.action) {
2461
3465
  if (options.yes) {
2462
3466
  action = "merge";
2463
3467
  } else {
2464
- p.log.warn(`Existing SRouter database found at ${TargetDbPath} (${fileKb(TargetDbPath)})`);
2465
- const choice = await p.select({
3468
+ p2.log.warn(`Existing SRouter database found at ${TargetDbPath} (${fileKb(TargetDbPath)})`);
3469
+ const choice = await p2.select({
2466
3470
  message: "How should the existing SRouter database be handled?",
2467
3471
  options: [
2468
3472
  { value: "backup_and_replace", label: "Backup current, replace with 9Router data" },
@@ -2470,8 +3474,8 @@ async function migrateNineRouter(options) {
2470
3474
  { value: "abort", label: "Cancel migration" }
2471
3475
  ]
2472
3476
  });
2473
- if (choice === "abort" || p.isCancel(choice)) {
2474
- p.outro("Migration cancelled. Your 9Router installation remains intact.");
3477
+ if (choice === "abort" || p2.isCancel(choice)) {
3478
+ p2.outro("Migration cancelled. Your 9Router installation remains intact.");
2475
3479
  return;
2476
3480
  }
2477
3481
  if (choice === "merge" || choice === "backup_and_replace") {
@@ -2479,9 +3483,9 @@ async function migrateNineRouter(options) {
2479
3483
  }
2480
3484
  }
2481
3485
  }
2482
- const proceed = options.yes || await p.confirm({ message: "Proceed with migration?" }) === true;
3486
+ const proceed = options.yes || await p2.confirm({ message: "Proceed with migration?" }) === true;
2483
3487
  if (!proceed) {
2484
- p.outro("Migration cancelled. No changes made.");
3488
+ p2.outro("Migration cancelled. No changes made.");
2485
3489
  return;
2486
3490
  }
2487
3491
  ensureDirs();
@@ -2490,10 +3494,10 @@ async function migrateNineRouter(options) {
2490
3494
  if (action === "backup_and_replace") {
2491
3495
  targetBackup = backupDb(TargetDbPath, "srouter");
2492
3496
  }
2493
- const s = p.spinner();
3497
+ const s = p2.spinner();
2494
3498
  try {
2495
3499
  s.start("Preparing target database");
2496
- initDatabase();
3500
+ await initDatabase();
2497
3501
  const targetDb = new DatabaseSync2(TargetDbPath);
2498
3502
  targetDb.exec("PRAGMA foreign_keys = OFF;");
2499
3503
  let inserted = 0;
@@ -2502,7 +3506,7 @@ async function migrateNineRouter(options) {
2502
3506
  const isJson = source.endsWith(".json");
2503
3507
  if (isJson) {
2504
3508
  s.message("Reading 9Router JSON backup");
2505
- const raw = fs6.readFileSync(source, "utf-8");
3509
+ const raw = fs8.readFileSync(source, "utf-8");
2506
3510
  const parsed = JSON.parse(raw);
2507
3511
  const res = importNineRouterJson(parsed, targetDb, action);
2508
3512
  inserted = res.inserted;
@@ -2517,7 +3521,7 @@ async function migrateNineRouter(options) {
2517
3521
  );
2518
3522
  for (const table of sourceTables) {
2519
3523
  if (!targetTables.has(table)) {
2520
- p.log.warn(`Skipping table ${table} (not recognized by SRouter)`);
3524
+ p2.log.warn(`Skipping table ${table} (not recognized by SRouter)`);
2521
3525
  continue;
2522
3526
  }
2523
3527
  const rows = sourceDb.prepare(`SELECT * FROM "${table}"`).all();
@@ -2526,7 +3530,7 @@ async function migrateNineRouter(options) {
2526
3530
  try {
2527
3531
  targetDb.prepare(`DELETE FROM "${table}"`).run();
2528
3532
  } catch {
2529
- p.log.warn(`Could not clear table ${table}, rows will be appended`);
3533
+ p2.log.warn(`Could not clear table ${table}, rows will be appended`);
2530
3534
  }
2531
3535
  }
2532
3536
  const sourceColumns = new Set(
@@ -2553,7 +3557,7 @@ async function migrateNineRouter(options) {
2553
3557
  targetDb.exec("PRAGMA foreign_keys = ON;");
2554
3558
  targetDb.close();
2555
3559
  s.stop("Migration complete");
2556
- p.log.message(
3560
+ p2.log.message(
2557
3561
  [
2558
3562
  `Tables/Categories migrated: ${tablesCount}`,
2559
3563
  `Rows/Items inserted: ${inserted}`,
@@ -2563,14 +3567,14 @@ async function migrateNineRouter(options) {
2563
3567
  ].join("\n")
2564
3568
  );
2565
3569
  if (targetBackup) {
2566
- p.log.info(`Old SRouter database backed up to ${targetBackup}`);
3570
+ p2.log.info(`Old SRouter database backed up to ${targetBackup}`);
2567
3571
  }
2568
- p.log.success(formatSuccess(`9Router backup saved to ${sourceBackup}`));
2569
- p.outro("Your 9Router providers are now available in SRouter.");
3572
+ p2.log.success(formatSuccess(`9Router backup saved to ${sourceBackup}`));
3573
+ p2.outro("Your 9Router providers are now available in SRouter.");
2570
3574
  } catch (error) {
2571
3575
  s.stop(formatError("Migration failed"));
2572
- p.log.error(error.message);
2573
- p.log.info(formatInfo(`Restore from backup if needed: cp ${sourceBackup} ${source}`));
3576
+ p2.log.error(error.message);
3577
+ p2.log.info(formatInfo(`Restore from backup if needed: cp ${sourceBackup} ${source}`));
2574
3578
  process.exitCode = 1;
2575
3579
  }
2576
3580
  }
@@ -2581,8 +3585,8 @@ async function migrateCommand(target, options) {
2581
3585
  case "9router":
2582
3586
  return migrateNineRouter(options);
2583
3587
  default:
2584
- p.log.error(formatError(`Unknown migration target: ${target}`));
2585
- p.log.info(
3588
+ p2.log.error(formatError(`Unknown migration target: ${target}`));
3589
+ p2.log.info(
2586
3590
  formatWarning("Available targets: db (legacy location), 9router (9Router import)")
2587
3591
  );
2588
3592
  process.exitCode = 1;
@@ -2609,6 +3613,9 @@ function createCli() {
2609
3613
  ).action(async (opts) => {
2610
3614
  await setupCommand(opts);
2611
3615
  });
3616
+ 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) => {
3617
+ await initCommand(opts);
3618
+ });
2612
3619
  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(
2613
3620
  "--opus-model <model>",
2614
3621
  "Opus tier model ID for Claude Code (ANTHROPIC_DEFAULT_OPUS_MODEL)"
@@ -2661,10 +3668,14 @@ function createCli() {
2661
3668
  });
2662
3669
  return program;
2663
3670
  }
2664
- if (process.argv[1]?.endsWith("srouter.js") || process.argv[1]?.endsWith("index.ts") || process.argv[1]?.endsWith("index.js")) {
2665
- const program = createCli();
2666
- program.parse(process.argv);
3671
+ function runCli() {
3672
+ 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");
3673
+ if (isEntrypoint) {
3674
+ const program = createCli();
3675
+ program.parse(process.argv);
3676
+ }
2667
3677
  }
3678
+ runCli();
2668
3679
  export {
2669
3680
  createCli
2670
3681
  };