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