@srouter/cli 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +203 -0
- package/bin/srouter.js +49 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2409 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2409 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
// ../../packages/constants/dist/providers/kiro.js
|
|
5
|
+
var KIRO_PROVIDER = {
|
|
6
|
+
id: "kiro",
|
|
7
|
+
name: "Kiro",
|
|
8
|
+
category: "api_key",
|
|
9
|
+
protocol: "custom",
|
|
10
|
+
web_url: "https://aws.amazon.com/q/",
|
|
11
|
+
requires_api_key: true,
|
|
12
|
+
supports_custom_url: true,
|
|
13
|
+
status_message: "Kiro credential missing"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// ../../packages/constants/dist/providers/neosantara.js
|
|
17
|
+
var NEOSANTARA_BASE_URL = "https://api.neosantara.xyz/v1";
|
|
18
|
+
var NEOSANTARA_PROVIDER = {
|
|
19
|
+
id: "neosantara",
|
|
20
|
+
name: "Neosantara",
|
|
21
|
+
category: "api_key",
|
|
22
|
+
protocol: "openai",
|
|
23
|
+
base_url: NEOSANTARA_BASE_URL,
|
|
24
|
+
web_url: "https://neosantara.xyz",
|
|
25
|
+
requires_api_key: true,
|
|
26
|
+
supports_custom_url: true,
|
|
27
|
+
status_message: "Neosantara API key missing"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ../../packages/constants/dist/providers/gorouter.js
|
|
31
|
+
var GOROUTER_BASE_URL = "https://gorouter.app/v1";
|
|
32
|
+
var GOROUTER_PROVIDER = {
|
|
33
|
+
id: "gorouter",
|
|
34
|
+
name: "GoRouter",
|
|
35
|
+
category: "api_key",
|
|
36
|
+
protocol: "openai",
|
|
37
|
+
base_url: GOROUTER_BASE_URL,
|
|
38
|
+
web_url: "https://gorouter.app/sign-up?aff=cJJn",
|
|
39
|
+
requires_api_key: true,
|
|
40
|
+
supports_custom_url: true,
|
|
41
|
+
status_message: "GoRouter API key missing"
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ../../packages/constants/dist/providers/bluesminds.js
|
|
45
|
+
var BLUESMINDS_BASE_URL = "https://api.bluesminds.com/v1";
|
|
46
|
+
var BLUESMINDS_PROVIDER = {
|
|
47
|
+
id: "bluesminds",
|
|
48
|
+
name: "BluesMinds",
|
|
49
|
+
category: "api_key",
|
|
50
|
+
protocol: "openai",
|
|
51
|
+
base_url: BLUESMINDS_BASE_URL,
|
|
52
|
+
web_url: "https://api.bluesminds.com/sign-up?aff=nCAw",
|
|
53
|
+
requires_api_key: true,
|
|
54
|
+
supports_custom_url: true,
|
|
55
|
+
status_message: "BluesMinds API key missing"
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ../../packages/constants/dist/providers/seekai.js
|
|
59
|
+
var SEEKAI_BASE_URL = "https://seekai.cc/v1";
|
|
60
|
+
var SEEKAI_PROVIDER = {
|
|
61
|
+
id: "seekai",
|
|
62
|
+
name: "SeekAI",
|
|
63
|
+
category: "api_key",
|
|
64
|
+
protocol: "openai",
|
|
65
|
+
base_url: SEEKAI_BASE_URL,
|
|
66
|
+
web_url: "https://seekai.cc/sign-up?aff=UU0C",
|
|
67
|
+
requires_api_key: true,
|
|
68
|
+
supports_custom_url: true,
|
|
69
|
+
status_message: "SeekAI API key missing"
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ../../packages/constants/dist/providers/tabitoken.js
|
|
73
|
+
var TABITOKEN_BASE_URL = "https://tabitoken.com/v1";
|
|
74
|
+
var TABITOKEN_PROVIDER = {
|
|
75
|
+
id: "tabitoken",
|
|
76
|
+
name: "TabiToken",
|
|
77
|
+
category: "api_key",
|
|
78
|
+
protocol: "openai",
|
|
79
|
+
base_url: TABITOKEN_BASE_URL,
|
|
80
|
+
web_url: "https://tabitoken.com/sign-up?aff=h5iN",
|
|
81
|
+
requires_api_key: true,
|
|
82
|
+
supports_custom_url: true,
|
|
83
|
+
status_message: "TabiToken API key missing"
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ../../packages/constants/dist/providers/tokenrouter.js
|
|
87
|
+
var TOKENROUTER_BASE_URL = "https://api.tokenrouter.com/v1";
|
|
88
|
+
var TOKENROUTER_PROVIDER = {
|
|
89
|
+
id: "tokenrouter",
|
|
90
|
+
name: "TokenRouter",
|
|
91
|
+
category: "api_key",
|
|
92
|
+
protocol: "openai",
|
|
93
|
+
base_url: TOKENROUTER_BASE_URL,
|
|
94
|
+
web_url: "https://tokenrouter.com",
|
|
95
|
+
requires_api_key: true,
|
|
96
|
+
supports_custom_url: true,
|
|
97
|
+
status_message: "TokenRouter API key missing"
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// ../../packages/constants/dist/providers/commandcode.js
|
|
101
|
+
var COMMANDCODE_BASE_URL = "https://api.commandcode.ai/alpha/generate";
|
|
102
|
+
var COMMANDCODE_PROVIDER = {
|
|
103
|
+
id: "commandcode",
|
|
104
|
+
name: "Command Code",
|
|
105
|
+
category: "api_key",
|
|
106
|
+
protocol: "openai",
|
|
107
|
+
base_url: COMMANDCODE_BASE_URL,
|
|
108
|
+
web_url: "https://commandcode.ai",
|
|
109
|
+
requires_api_key: true,
|
|
110
|
+
supports_custom_url: true,
|
|
111
|
+
status_message: "Command Code API key missing"
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// ../../packages/constants/dist/providers/openai.js
|
|
115
|
+
var OPENAI_CODEX_PROVIDER = {
|
|
116
|
+
id: "openai_codex",
|
|
117
|
+
name: "OpenAI Codex / ChatGPT",
|
|
118
|
+
category: "oauth",
|
|
119
|
+
protocol: "openai",
|
|
120
|
+
alias: "openai",
|
|
121
|
+
web_url: "https://chatgpt.com",
|
|
122
|
+
requires_api_key: false,
|
|
123
|
+
requires_oauth: true,
|
|
124
|
+
status_message: "OAuth token missing"
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// ../../packages/constants/dist/providers/anthropic.js
|
|
128
|
+
var ANTHROPIC_PROVIDER = {
|
|
129
|
+
id: "anthropic",
|
|
130
|
+
name: "Anthropic Claude",
|
|
131
|
+
category: "oauth",
|
|
132
|
+
protocol: "anthropic",
|
|
133
|
+
alias: "claude",
|
|
134
|
+
web_url: "https://claude.ai",
|
|
135
|
+
requires_api_key: false,
|
|
136
|
+
requires_oauth: true,
|
|
137
|
+
status_message: "OAuth token missing"
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// ../../packages/constants/dist/providers/antigravity.js
|
|
141
|
+
var ANTIGRAVITY_IDE_BASE_URL = "https://daily-cloudcode-pa.googleapis.com";
|
|
142
|
+
var ANTIGRAVITY_MODELS = [
|
|
143
|
+
{ id: "gemini-3.7-flash-high", name: "Gemini 3.7 Flash (High)" },
|
|
144
|
+
{ id: "gemini-3.7-flash-medium", name: "Gemini 3.7 Flash (Medium)" },
|
|
145
|
+
{ id: "gemini-3.7-flash-low", name: "Gemini 3.7 Flash (Low)" },
|
|
146
|
+
{ id: "gemini-3.6-flash-high", name: "Gemini 3.6 Flash (High)" },
|
|
147
|
+
{ id: "gemini-3.6-flash-medium", name: "Gemini 3.6 Flash (Medium)" },
|
|
148
|
+
{ id: "gemini-3.6-flash-low", name: "Gemini 3.6 Flash (Low)" },
|
|
149
|
+
{ id: "gemini-3.5-flash-high", name: "Gemini 3.5 Flash (High)" },
|
|
150
|
+
{ id: "gemini-3.5-flash-medium", name: "Gemini 3.5 Flash (Medium)" },
|
|
151
|
+
{ id: "gemini-3.5-flash-low", name: "Gemini 3.5 Flash (Low)" },
|
|
152
|
+
{ id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro (High)" },
|
|
153
|
+
{ id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro (Low)" },
|
|
154
|
+
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Thinking)" },
|
|
155
|
+
{ id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 (Thinking)" },
|
|
156
|
+
{ id: "gpt-oss-120b-medium", name: "GPT-OSS 120B (Medium)" }
|
|
157
|
+
];
|
|
158
|
+
var ANTIGRAVITY_MODEL_IDS = ANTIGRAVITY_MODELS.map((m) => m.id);
|
|
159
|
+
var ANTIGRAVITY_PROVIDER = {
|
|
160
|
+
id: "antigravity",
|
|
161
|
+
name: "Google Antigravity",
|
|
162
|
+
category: "oauth",
|
|
163
|
+
protocol: "openai",
|
|
164
|
+
base_url: ANTIGRAVITY_IDE_BASE_URL,
|
|
165
|
+
web_url: "https://ai.google.dev",
|
|
166
|
+
requires_api_key: false,
|
|
167
|
+
requires_oauth: true,
|
|
168
|
+
status_message: "Antigravity OAuth token missing"
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// ../../packages/constants/dist/providers/codebuddy.js
|
|
172
|
+
var CODEBUDDY_BASE_URL = "https://www.codebuddy.ai/v2/chat/completions";
|
|
173
|
+
var CODEBUDDY_CN_BASE_URL = "https://copilot.tencent.com/v2/chat/completions";
|
|
174
|
+
var CODEBUDDY_MODELS = [
|
|
175
|
+
{ id: "default-model", name: "Default (CodeBuddy)" },
|
|
176
|
+
{ id: "default-model-lite", name: "Default-Lite" },
|
|
177
|
+
{ id: "gpt-5.5", name: "GPT-5.5" },
|
|
178
|
+
{ id: "gpt-5.4", name: "GPT-5.4" },
|
|
179
|
+
{ id: "gpt-5.3-codex", name: "GPT-5.3-Codex" },
|
|
180
|
+
{ id: "gpt-5.1-codex", name: "GPT-5.1-Codex" },
|
|
181
|
+
{ id: "gpt-5.1-codex-mini", name: "GPT-5.1-Codex-Mini" },
|
|
182
|
+
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
|
|
183
|
+
{ id: "gemini-3.5-flash", name: "Gemini 3.5 Flash" },
|
|
184
|
+
{ id: "gemini-3.0-flash", name: "Gemini 3.0 Flash" },
|
|
185
|
+
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
|
|
186
|
+
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
|
|
187
|
+
{ id: "gemini-3.1-flash-lite", name: "Gemini 3.1 Flash Lite" },
|
|
188
|
+
{ id: "deepseek-v3", name: "DeepSeek-V3" },
|
|
189
|
+
{ id: "deepseek-v3-2-volc", name: "DeepSeek-V3.2" },
|
|
190
|
+
{ id: "deepseek-v4-pro", name: "DeepSeek-V4-Pro" },
|
|
191
|
+
{ id: "deepseek-v4-flash", name: "DeepSeek-V4-Flash" },
|
|
192
|
+
{ id: "glm-5.3", name: "GLM-5.3" },
|
|
193
|
+
{ id: "glm-5.2", name: "GLM-5.2" },
|
|
194
|
+
{ id: "glm-5.1", name: "GLM-5.1" },
|
|
195
|
+
{ id: "glm-5.0", name: "GLM-5.0" },
|
|
196
|
+
{ id: "glm-5.0-turbo", name: "GLM-5.0-Turbo" },
|
|
197
|
+
{ id: "glm-5v-turbo", name: "GLM-5v-Turbo" },
|
|
198
|
+
{ id: "glm-4.7", name: "GLM-4.7" },
|
|
199
|
+
{ id: "minimax-m3", name: "MiniMax-M3" },
|
|
200
|
+
{ id: "minimax-m2.7", name: "MiniMax-M2.7" },
|
|
201
|
+
{ id: "kimi-k3", name: "Kimi-K3" },
|
|
202
|
+
{ id: "kimi-k2.7", name: "Kimi-K2.7-Code" },
|
|
203
|
+
{ id: "kimi-k2.6", name: "Kimi-K2.6" },
|
|
204
|
+
{ id: "kimi-k2.5", name: "Kimi-K2.5" },
|
|
205
|
+
{ id: "hy3-preview", name: "Hy3 Preview" }
|
|
206
|
+
];
|
|
207
|
+
var CODEBUDDY_MODEL_IDS = CODEBUDDY_MODELS.map((m) => m.id);
|
|
208
|
+
var CODEBUDDY_PROVIDER = {
|
|
209
|
+
id: "codebuddy",
|
|
210
|
+
name: "CodeBuddy",
|
|
211
|
+
category: "oauth",
|
|
212
|
+
protocol: "openai",
|
|
213
|
+
alias: "codebuddy",
|
|
214
|
+
base_url: CODEBUDDY_BASE_URL,
|
|
215
|
+
web_url: "https://www.codebuddy.ai",
|
|
216
|
+
requires_api_key: false,
|
|
217
|
+
requires_oauth: true,
|
|
218
|
+
supports_custom_url: true,
|
|
219
|
+
status_message: "CodeBuddy OAuth token missing"
|
|
220
|
+
};
|
|
221
|
+
var CODEBUDDY_CN_PROVIDER = {
|
|
222
|
+
id: "codebuddy-cn",
|
|
223
|
+
name: "CodeBuddy CN",
|
|
224
|
+
category: "oauth",
|
|
225
|
+
protocol: "openai",
|
|
226
|
+
alias: "codebuddy-cn",
|
|
227
|
+
base_url: CODEBUDDY_CN_BASE_URL,
|
|
228
|
+
web_url: "https://www.codebuddy.cn",
|
|
229
|
+
requires_api_key: false,
|
|
230
|
+
requires_oauth: true,
|
|
231
|
+
supports_custom_url: true,
|
|
232
|
+
status_message: "CodeBuddy CN OAuth token missing"
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// ../../packages/constants/dist/providers/qoder.js
|
|
236
|
+
var QODER_MODELS = [
|
|
237
|
+
{ id: "qwen3.8-max-preview", name: "Qwen 3.8 Max Preview", level: "qmodel_preview" },
|
|
238
|
+
{ id: "qwen3.7-max", name: "Qwen 3.7 Max", level: "qmodel_latest" },
|
|
239
|
+
{ id: "qwen3.7-plus", name: "Qwen 3.7 Plus", level: "qmodel" },
|
|
240
|
+
{ id: "kimi-k3", name: "Kimi K3", level: "kmodel_latest" },
|
|
241
|
+
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code", level: "kmodel" },
|
|
242
|
+
{ id: "glm-5.2", name: "GLM 5.2", level: "gm51model" },
|
|
243
|
+
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", level: "dmodel" },
|
|
244
|
+
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", level: "dfmodel" },
|
|
245
|
+
{ id: "minimax-m3", name: "MiniMax M3", level: "mmodel" },
|
|
246
|
+
{ id: "ultimate", name: "Qoder Ultimate" },
|
|
247
|
+
{ id: "qmodel_38max", name: "Qwen 3.8 Max" },
|
|
248
|
+
{ id: "qmodel_preview", name: "Qwen Preview" },
|
|
249
|
+
{ id: "qmodel_latest", name: "Qwen Latest" },
|
|
250
|
+
{ id: "qmodel", name: "Qwen Standard" },
|
|
251
|
+
{ id: "auto", name: "Qoder Auto" },
|
|
252
|
+
{ id: "performance", name: "Qoder Performance" },
|
|
253
|
+
{ id: "efficient", name: "Qoder Efficient" },
|
|
254
|
+
{ id: "lite", name: "Qoder Lite" },
|
|
255
|
+
{ id: "kmodel_latest", name: "Kimi K3 (Raw)" },
|
|
256
|
+
{ id: "kmodel", name: "Kimi K2.7 (Raw)" },
|
|
257
|
+
{ id: "gmodel", name: "GLM (Raw)" },
|
|
258
|
+
{ id: "gm51model", name: "GLM 5.2 (Raw)" },
|
|
259
|
+
{ id: "dmodel", name: "DeepSeek Pro (Raw)" },
|
|
260
|
+
{ id: "dfmodel", name: "DeepSeek Flash (Raw)" },
|
|
261
|
+
{ id: "mmodel", name: "MiniMax (Raw)" }
|
|
262
|
+
];
|
|
263
|
+
var QODER_MODEL_IDS = QODER_MODELS.map((m) => m.id);
|
|
264
|
+
var QODER_OPENAPI_BASE = "https://openapi.qoder.sh";
|
|
265
|
+
var QODER_CENTER_BASE = "https://center.qoder.sh";
|
|
266
|
+
var QODER_CHAT_BASE = "https://api3.qoder.sh";
|
|
267
|
+
var QODER_DEVICE_TOKEN_URL = `${QODER_OPENAPI_BASE}/api/v1/deviceToken/poll`;
|
|
268
|
+
var QODER_USERINFO_URL = `${QODER_OPENAPI_BASE}/api/v1/userinfo`;
|
|
269
|
+
var QODER_QUOTA_USAGE_URL = `${QODER_OPENAPI_BASE}/api/v2/quota/usage`;
|
|
270
|
+
var QODER_REFRESH_TOKEN_URL = `${QODER_CENTER_BASE}/algo/api/v3/user/refresh_token`;
|
|
271
|
+
var QODER_JOB_TOKEN_EXCHANGE_URL = `${QODER_OPENAPI_BASE}/api/v1/jobToken/exchange`;
|
|
272
|
+
var QODER_CHAT_SIG_PATH = "/api/v2/service/pro/sse/agent_chat_generation";
|
|
273
|
+
var QODER_CHAT_URL = `${QODER_CHAT_BASE}/algo${QODER_CHAT_SIG_PATH}?FetchKeys=llm_model_result&AgentId=agent_common`;
|
|
274
|
+
var QODER_CHAT_URL_ENCODED = `${QODER_CHAT_URL}&Encode=1`;
|
|
275
|
+
var QODER_MODEL_LIST_URL = `${QODER_CHAT_BASE}/algo/api/v2/model/list`;
|
|
276
|
+
var QODER_PROVIDER = {
|
|
277
|
+
id: "qoder",
|
|
278
|
+
name: "Qoder",
|
|
279
|
+
category: "oauth",
|
|
280
|
+
protocol: "openai",
|
|
281
|
+
alias: "qd",
|
|
282
|
+
base_url: QODER_CHAT_URL_ENCODED,
|
|
283
|
+
web_url: "https://qoder.com",
|
|
284
|
+
requires_api_key: false,
|
|
285
|
+
requires_oauth: true,
|
|
286
|
+
supports_custom_url: true,
|
|
287
|
+
status_message: "Qoder token or session missing"
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// ../../packages/constants/dist/providers/opencode.js
|
|
291
|
+
var OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1";
|
|
292
|
+
var OPENCODE_ZEN_MODELS = [
|
|
293
|
+
{ id: "x-preview-f-free", name: "Ox Alpha Free (Unlimited)" },
|
|
294
|
+
{ id: "big-pickle", name: "Big Pickle (Free)" },
|
|
295
|
+
{ id: "laguna-s-2.1-free", name: "Poolside Laguna S 2.1 (Free)" },
|
|
296
|
+
{ id: "nemotron-3.5-lightning-free", name: "Nemotron 3.5 Lightning (Free)" },
|
|
297
|
+
{ id: "nemotron-3-ultra-free", name: "Nemotron 3 Ultra (Free)" },
|
|
298
|
+
{ id: "mimo-v2.5-free", name: "Xiaomi MiMo V2.5 (Free)" }
|
|
299
|
+
];
|
|
300
|
+
var OPENCODE_ZEN_MODEL_IDS = OPENCODE_ZEN_MODELS.map((m) => m.id);
|
|
301
|
+
var OPENCODE_ZEN_PROVIDER = {
|
|
302
|
+
id: "opencode_zen",
|
|
303
|
+
name: "OpenCode Zen",
|
|
304
|
+
category: "free_tier",
|
|
305
|
+
protocol: "openai",
|
|
306
|
+
alias: "zen",
|
|
307
|
+
base_url: OPENCODE_ZEN_BASE_URL,
|
|
308
|
+
web_url: "https://opencode.ai/zen",
|
|
309
|
+
requires_api_key: false,
|
|
310
|
+
requires_oauth: false,
|
|
311
|
+
supports_custom_url: true,
|
|
312
|
+
status_message: "Free Tier Ready (Unlimited)"
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// ../../packages/constants/dist/providers/catalog.js
|
|
316
|
+
var KNOWN_PROVIDERS = [
|
|
317
|
+
KIRO_PROVIDER,
|
|
318
|
+
NEOSANTARA_PROVIDER,
|
|
319
|
+
GOROUTER_PROVIDER,
|
|
320
|
+
BLUESMINDS_PROVIDER,
|
|
321
|
+
SEEKAI_PROVIDER,
|
|
322
|
+
TABITOKEN_PROVIDER,
|
|
323
|
+
TOKENROUTER_PROVIDER,
|
|
324
|
+
OPENAI_CODEX_PROVIDER,
|
|
325
|
+
ANTHROPIC_PROVIDER,
|
|
326
|
+
ANTIGRAVITY_PROVIDER,
|
|
327
|
+
COMMANDCODE_PROVIDER,
|
|
328
|
+
QODER_PROVIDER,
|
|
329
|
+
CODEBUDDY_PROVIDER,
|
|
330
|
+
CODEBUDDY_CN_PROVIDER,
|
|
331
|
+
OPENCODE_ZEN_PROVIDER
|
|
332
|
+
];
|
|
333
|
+
var KNOWN_PROVIDER_MAP = Object.freeze(Object.fromEntries(KNOWN_PROVIDERS.map((Provider) => [Provider.id, Provider])));
|
|
334
|
+
var KNOWN_PROVIDER_IDS_DESC = Object.freeze(Object.keys(KNOWN_PROVIDER_MAP).sort((A, B) => B.length - A.length));
|
|
335
|
+
var LEGACY_ALIAS_MAP = Object.freeze({
|
|
336
|
+
claude: "claude",
|
|
337
|
+
cbai: "codebuddy"
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ../../packages/constants/dist/seed.js
|
|
341
|
+
var DEFAULT_PROVIDERS = Object.freeze(KNOWN_PROVIDERS.map(({ alias: _alias, ...seed }) => seed));
|
|
342
|
+
var DEFAULT_PROVIDER_MAP = Object.freeze(Object.fromEntries(DEFAULT_PROVIDERS.map((seed) => [seed.id, seed])));
|
|
343
|
+
|
|
344
|
+
// ../../packages/constants/dist/version.js
|
|
345
|
+
var GLOBAL_VERSION = "0.1.3";
|
|
346
|
+
var CLI_VERSION = GLOBAL_VERSION;
|
|
347
|
+
var SROUTER_VERSION_TAG = `v${GLOBAL_VERSION}`;
|
|
348
|
+
|
|
349
|
+
// src/commands/setup.ts
|
|
350
|
+
import {
|
|
351
|
+
intro as intro2,
|
|
352
|
+
outro as outro2,
|
|
353
|
+
spinner as spinner2,
|
|
354
|
+
text,
|
|
355
|
+
select,
|
|
356
|
+
multiselect,
|
|
357
|
+
isCancel,
|
|
358
|
+
cancel,
|
|
359
|
+
note as note2
|
|
360
|
+
} from "@clack/prompts";
|
|
361
|
+
|
|
362
|
+
// src/lib/configStore.ts
|
|
363
|
+
import fs from "node:fs/promises";
|
|
364
|
+
import path from "node:path";
|
|
365
|
+
import os from "node:os";
|
|
366
|
+
var DEFAULT_CLI_CONFIG = {
|
|
367
|
+
defaultBaseUrl: "http://localhost:3000/v1",
|
|
368
|
+
backups: []
|
|
369
|
+
};
|
|
370
|
+
var ConfigStore = class {
|
|
371
|
+
baseDir;
|
|
372
|
+
configPath;
|
|
373
|
+
backupsDir;
|
|
374
|
+
constructor(customBaseDir) {
|
|
375
|
+
this.baseDir = customBaseDir || path.join(os.homedir(), ".srouter");
|
|
376
|
+
this.configPath = path.join(this.baseDir, "cli.json");
|
|
377
|
+
this.backupsDir = path.join(this.baseDir, "backups");
|
|
378
|
+
}
|
|
379
|
+
async ensureDirs() {
|
|
380
|
+
await fs.mkdir(this.baseDir, { recursive: true });
|
|
381
|
+
await fs.mkdir(this.backupsDir, { recursive: true });
|
|
382
|
+
}
|
|
383
|
+
async loadConfig() {
|
|
384
|
+
try {
|
|
385
|
+
const raw = await fs.readFile(this.configPath, "utf-8");
|
|
386
|
+
const data = JSON.parse(raw);
|
|
387
|
+
return {
|
|
388
|
+
...DEFAULT_CLI_CONFIG,
|
|
389
|
+
...data,
|
|
390
|
+
backups: Array.isArray(data?.backups) ? data.backups : []
|
|
391
|
+
};
|
|
392
|
+
} catch {
|
|
393
|
+
return { ...DEFAULT_CLI_CONFIG };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async saveConfig(partial) {
|
|
397
|
+
await this.ensureDirs();
|
|
398
|
+
const current = await this.loadConfig();
|
|
399
|
+
const updated = {
|
|
400
|
+
...current,
|
|
401
|
+
...partial,
|
|
402
|
+
backups: partial.backups ?? current.backups
|
|
403
|
+
};
|
|
404
|
+
await fs.writeFile(this.configPath, JSON.stringify(updated, null, 4), "utf-8");
|
|
405
|
+
return updated;
|
|
406
|
+
}
|
|
407
|
+
async createBackup(toolId, originalPath) {
|
|
408
|
+
try {
|
|
409
|
+
await fs.access(originalPath);
|
|
410
|
+
} catch {
|
|
411
|
+
return void 0;
|
|
412
|
+
}
|
|
413
|
+
await this.ensureDirs();
|
|
414
|
+
const timestamp = Date.now();
|
|
415
|
+
const ext = path.extname(originalPath) || ".json";
|
|
416
|
+
const backupFileName = `${toolId}-${timestamp}${ext}`;
|
|
417
|
+
const backupPath = path.join(this.backupsDir, backupFileName);
|
|
418
|
+
await fs.copyFile(originalPath, backupPath);
|
|
419
|
+
const config = await this.loadConfig();
|
|
420
|
+
const entry = {
|
|
421
|
+
toolId,
|
|
422
|
+
originalPath,
|
|
423
|
+
backupPath,
|
|
424
|
+
timestamp
|
|
425
|
+
};
|
|
426
|
+
await this.saveConfig({
|
|
427
|
+
backups: [...config.backups, entry]
|
|
428
|
+
});
|
|
429
|
+
return backupPath;
|
|
430
|
+
}
|
|
431
|
+
async getLatestBackup(toolId) {
|
|
432
|
+
const config = await this.loadConfig();
|
|
433
|
+
const entries = config.backups.filter((b) => b.toolId === toolId);
|
|
434
|
+
if (entries.length === 0) return void 0;
|
|
435
|
+
return entries.sort((a, b) => b.timestamp - a.timestamp)[0];
|
|
436
|
+
}
|
|
437
|
+
async restoreLatestBackup(toolId) {
|
|
438
|
+
const latest = await this.getLatestBackup(toolId);
|
|
439
|
+
if (!latest) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
await fs.mkdir(path.dirname(latest.originalPath), { recursive: true });
|
|
444
|
+
await fs.copyFile(latest.backupPath, latest.originalPath);
|
|
445
|
+
const config = await this.loadConfig();
|
|
446
|
+
const updatedBackups = config.backups.filter((b) => b.backupPath !== latest.backupPath);
|
|
447
|
+
await this.saveConfig({ backups: updatedBackups });
|
|
448
|
+
await fs.rm(latest.backupPath, { force: true });
|
|
449
|
+
return true;
|
|
450
|
+
} catch {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
var defaultStore = new ConfigStore();
|
|
456
|
+
|
|
457
|
+
// src/adapters/claude.ts
|
|
458
|
+
import fs3 from "node:fs/promises";
|
|
459
|
+
import path3 from "node:path";
|
|
460
|
+
import os3 from "node:os";
|
|
461
|
+
|
|
462
|
+
// src/adapters/base.ts
|
|
463
|
+
var AbstractToolAdapter = class {
|
|
464
|
+
store;
|
|
465
|
+
constructor(store = defaultStore) {
|
|
466
|
+
this.store = store;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// src/lib/platform.ts
|
|
471
|
+
import os2 from "node:os";
|
|
472
|
+
import path2 from "node:path";
|
|
473
|
+
import fs2 from "node:fs";
|
|
474
|
+
import { exec } from "node:child_process";
|
|
475
|
+
import { promisify } from "node:util";
|
|
476
|
+
var execAsync = promisify(exec);
|
|
477
|
+
function getPlatform() {
|
|
478
|
+
const p2 = process.platform;
|
|
479
|
+
if (p2 === "win32") return "windows";
|
|
480
|
+
if (p2 === "darwin") return "macos";
|
|
481
|
+
if (p2 === "linux") return "linux";
|
|
482
|
+
return "unknown";
|
|
483
|
+
}
|
|
484
|
+
function isWindows() {
|
|
485
|
+
return process.platform === "win32";
|
|
486
|
+
}
|
|
487
|
+
function isMacOS() {
|
|
488
|
+
return process.platform === "darwin";
|
|
489
|
+
}
|
|
490
|
+
function getOsDisplayName() {
|
|
491
|
+
const p2 = getPlatform();
|
|
492
|
+
const arch = os2.arch();
|
|
493
|
+
const rel = os2.release();
|
|
494
|
+
switch (p2) {
|
|
495
|
+
case "macos":
|
|
496
|
+
return `macOS (${arch === "arm64" ? "Apple Silicon" : arch}) [Darwin ${rel}]`;
|
|
497
|
+
case "windows":
|
|
498
|
+
return `Windows (${arch}) [NT ${rel}]`;
|
|
499
|
+
case "linux":
|
|
500
|
+
return `Linux (${arch}) [${rel}]`;
|
|
501
|
+
default:
|
|
502
|
+
return `${process.platform} (${arch})`;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function detectShell() {
|
|
506
|
+
const shellEnv = process.env.SHELL?.toLowerCase() || "";
|
|
507
|
+
if (shellEnv.includes("fish")) return "fish";
|
|
508
|
+
if (shellEnv.includes("zsh")) return "zsh";
|
|
509
|
+
if (shellEnv.includes("bash")) return "bash";
|
|
510
|
+
if (isWindows()) {
|
|
511
|
+
if (process.env.PSModulePath || process.env.POWERSHELL_DISTRIBUTION_CHANNEL) {
|
|
512
|
+
return "powershell";
|
|
513
|
+
}
|
|
514
|
+
return "cmd";
|
|
515
|
+
}
|
|
516
|
+
return "bash";
|
|
517
|
+
}
|
|
518
|
+
function getSystemInfo() {
|
|
519
|
+
return {
|
|
520
|
+
platform: getPlatform(),
|
|
521
|
+
osType: os2.type(),
|
|
522
|
+
osRelease: os2.release(),
|
|
523
|
+
arch: os2.arch(),
|
|
524
|
+
displayName: getOsDisplayName(),
|
|
525
|
+
homeDir: os2.homedir(),
|
|
526
|
+
detectedShell: detectShell()
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
async function isExecutableInPath(command) {
|
|
530
|
+
const lookupCmd = isWindows() ? `where.exe ${command}` : `which ${command}`;
|
|
531
|
+
try {
|
|
532
|
+
await execAsync(lookupCmd);
|
|
533
|
+
return true;
|
|
534
|
+
} catch {
|
|
535
|
+
if (isWindows()) {
|
|
536
|
+
try {
|
|
537
|
+
await execAsync(`where ${command}`);
|
|
538
|
+
return true;
|
|
539
|
+
} catch {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function getClaudeConfigPath() {
|
|
547
|
+
const home = os2.homedir();
|
|
548
|
+
const candidatePaths = [];
|
|
549
|
+
if (process.env.CLAUDE_CONFIG_DIR) {
|
|
550
|
+
candidatePaths.push(path2.join(process.env.CLAUDE_CONFIG_DIR, "settings.json"));
|
|
551
|
+
candidatePaths.push(path2.join(process.env.CLAUDE_CONFIG_DIR, "config.json"));
|
|
552
|
+
}
|
|
553
|
+
candidatePaths.push(path2.join(home, ".claude", "settings.json"));
|
|
554
|
+
candidatePaths.push(path2.join(home, ".config", "claude", "settings.json"));
|
|
555
|
+
candidatePaths.push(path2.join(home, ".claude.json"));
|
|
556
|
+
if (isWindows()) {
|
|
557
|
+
const appData = process.env.APPDATA || path2.join(home, "AppData", "Roaming");
|
|
558
|
+
candidatePaths.push(path2.join(appData, "Claude", "settings.json"));
|
|
559
|
+
candidatePaths.push(path2.join(appData, "Claude", "config.json"));
|
|
560
|
+
} else {
|
|
561
|
+
candidatePaths.push(path2.join(home, ".claude", "config.json"));
|
|
562
|
+
candidatePaths.push(path2.join(home, ".config", "claude", "config.json"));
|
|
563
|
+
}
|
|
564
|
+
for (const p2 of candidatePaths) {
|
|
565
|
+
if (fs2.existsSync(p2)) {
|
|
566
|
+
return p2;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (fs2.existsSync(path2.join(home, ".claude"))) {
|
|
570
|
+
return path2.join(home, ".claude", "settings.json");
|
|
571
|
+
}
|
|
572
|
+
return path2.join(home, ".claude", "settings.json");
|
|
573
|
+
}
|
|
574
|
+
function getOpenCodeConfigPath() {
|
|
575
|
+
const home = os2.homedir();
|
|
576
|
+
const candidatePaths = [];
|
|
577
|
+
candidatePaths.push(path2.join(process.cwd(), "opencode.jsonc"));
|
|
578
|
+
candidatePaths.push(path2.join(process.cwd(), "opencode.json"));
|
|
579
|
+
if (isWindows()) {
|
|
580
|
+
const appData = process.env.APPDATA || path2.join(home, "AppData", "Roaming");
|
|
581
|
+
candidatePaths.push(path2.join(appData, "opencode", "opencode.jsonc"));
|
|
582
|
+
candidatePaths.push(path2.join(appData, "opencode", "opencode.json"));
|
|
583
|
+
candidatePaths.push(path2.join(appData, "opencode", "config.json"));
|
|
584
|
+
candidatePaths.push(path2.join(home, ".config", "opencode", "opencode.jsonc"));
|
|
585
|
+
candidatePaths.push(path2.join(home, ".config", "opencode", "opencode.json"));
|
|
586
|
+
candidatePaths.push(path2.join(home, ".config", "opencode", "config.json"));
|
|
587
|
+
} else if (isMacOS()) {
|
|
588
|
+
candidatePaths.push(
|
|
589
|
+
path2.join(home, "Library", "Application Support", "opencode", "opencode.jsonc")
|
|
590
|
+
);
|
|
591
|
+
candidatePaths.push(
|
|
592
|
+
path2.join(home, "Library", "Application Support", "opencode", "opencode.json")
|
|
593
|
+
);
|
|
594
|
+
candidatePaths.push(
|
|
595
|
+
path2.join(home, "Library", "Application Support", "opencode", "config.json")
|
|
596
|
+
);
|
|
597
|
+
candidatePaths.push(path2.join(home, ".config", "opencode", "opencode.jsonc"));
|
|
598
|
+
candidatePaths.push(path2.join(home, ".config", "opencode", "opencode.json"));
|
|
599
|
+
candidatePaths.push(path2.join(home, ".config", "opencode", "config.json"));
|
|
600
|
+
} else {
|
|
601
|
+
const xdgConfig2 = process.env.XDG_CONFIG_HOME || path2.join(home, ".config");
|
|
602
|
+
candidatePaths.push(path2.join(xdgConfig2, "opencode", "opencode.jsonc"));
|
|
603
|
+
candidatePaths.push(path2.join(xdgConfig2, "opencode", "opencode.json"));
|
|
604
|
+
candidatePaths.push(path2.join(xdgConfig2, "opencode", "config.json"));
|
|
605
|
+
}
|
|
606
|
+
candidatePaths.push(path2.join(home, ".opencode.jsonc"));
|
|
607
|
+
candidatePaths.push(path2.join(home, ".opencode.json"));
|
|
608
|
+
for (const p2 of candidatePaths) {
|
|
609
|
+
if (fs2.existsSync(p2)) {
|
|
610
|
+
return p2;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (isWindows()) {
|
|
614
|
+
const appData = process.env.APPDATA || path2.join(home, "AppData", "Roaming");
|
|
615
|
+
return path2.join(appData, "opencode", "opencode.jsonc");
|
|
616
|
+
}
|
|
617
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || path2.join(home, ".config");
|
|
618
|
+
return path2.join(xdgConfig, "opencode", "opencode.jsonc");
|
|
619
|
+
}
|
|
620
|
+
function formatShellExport(key, value, shell) {
|
|
621
|
+
switch (shell) {
|
|
622
|
+
case "powershell":
|
|
623
|
+
return `$env:${key} = "${value}"`;
|
|
624
|
+
case "cmd":
|
|
625
|
+
return `set ${key}=${value}`;
|
|
626
|
+
case "fish":
|
|
627
|
+
return `set -gx ${key} "${value}";`;
|
|
628
|
+
case "bash":
|
|
629
|
+
case "zsh":
|
|
630
|
+
default:
|
|
631
|
+
return `export ${key}="${value}"`;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/adapters/claude.ts
|
|
636
|
+
function parseJsonSafe(content) {
|
|
637
|
+
try {
|
|
638
|
+
const clean = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").replace(/,\s*([\]}])/g, "$1");
|
|
639
|
+
return JSON.parse(clean);
|
|
640
|
+
} catch {
|
|
641
|
+
try {
|
|
642
|
+
return JSON.parse(content);
|
|
643
|
+
} catch {
|
|
644
|
+
return {};
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
var ClaudeAdapter = class extends AbstractToolAdapter {
|
|
649
|
+
id = "claude";
|
|
650
|
+
name = "Claude Code";
|
|
651
|
+
description = "Anthropic's official CLI tool for agentic coding";
|
|
652
|
+
customConfigPath;
|
|
653
|
+
constructor(store = defaultStore, customConfigPath) {
|
|
654
|
+
super(store);
|
|
655
|
+
this.customConfigPath = customConfigPath;
|
|
656
|
+
}
|
|
657
|
+
getConfigPath() {
|
|
658
|
+
if (this.customConfigPath) {
|
|
659
|
+
return this.customConfigPath;
|
|
660
|
+
}
|
|
661
|
+
return getClaudeConfigPath();
|
|
662
|
+
}
|
|
663
|
+
async isInstalled() {
|
|
664
|
+
return isExecutableInPath("claude");
|
|
665
|
+
}
|
|
666
|
+
async getStatus() {
|
|
667
|
+
const configPath = this.getConfigPath();
|
|
668
|
+
const installed = await this.isInstalled();
|
|
669
|
+
try {
|
|
670
|
+
const raw = await fs3.readFile(configPath, "utf-8");
|
|
671
|
+
const parsed = parseJsonSafe(raw);
|
|
672
|
+
const baseUrl = parsed.env?.ANTHROPIC_BASE_URL || parsed.ANTHROPIC_BASE_URL || parsed.baseUrl || void 0;
|
|
673
|
+
const model = parsed.env?.ANTHROPIC_DEFAULT_MODEL || parsed.env?.ANTHROPIC_MODEL || parsed.model || parsed.ANTHROPIC_MODEL || void 0;
|
|
674
|
+
const opusModel = parsed.env?.ANTHROPIC_DEFAULT_OPUS_MODEL || parsed.ANTHROPIC_DEFAULT_OPUS_MODEL || parsed.env?.ANTHROPIC_OPUS_MODEL || parsed.ANTHROPIC_OPUS_MODEL || void 0;
|
|
675
|
+
const sonnetModel = parsed.env?.ANTHROPIC_DEFAULT_SONNET_MODEL || parsed.ANTHROPIC_DEFAULT_SONNET_MODEL || parsed.env?.ANTHROPIC_SONNET_MODEL || parsed.ANTHROPIC_SONNET_MODEL || void 0;
|
|
676
|
+
const haikuModel = parsed.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL || parsed.ANTHROPIC_DEFAULT_HAIKU_MODEL || parsed.env?.ANTHROPIC_HAIKU_MODEL || parsed.ANTHROPIC_HAIKU_MODEL || void 0;
|
|
677
|
+
const linked = Boolean(
|
|
678
|
+
baseUrl && (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1") || baseUrl.includes("srouter"))
|
|
679
|
+
);
|
|
680
|
+
return {
|
|
681
|
+
id: this.id,
|
|
682
|
+
name: this.name,
|
|
683
|
+
installed,
|
|
684
|
+
linked,
|
|
685
|
+
configPath,
|
|
686
|
+
currentBaseUrl: baseUrl,
|
|
687
|
+
currentModel: model,
|
|
688
|
+
currentOpusModel: opusModel,
|
|
689
|
+
currentSonnetModel: sonnetModel,
|
|
690
|
+
currentHaikuModel: haikuModel
|
|
691
|
+
};
|
|
692
|
+
} catch {
|
|
693
|
+
return {
|
|
694
|
+
id: this.id,
|
|
695
|
+
name: this.name,
|
|
696
|
+
installed,
|
|
697
|
+
linked: false,
|
|
698
|
+
configPath
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async link(context) {
|
|
703
|
+
const configPath = this.getConfigPath();
|
|
704
|
+
const backupPath = context.dryRun ? void 0 : await this.store.createBackup(this.id, configPath);
|
|
705
|
+
let data = {};
|
|
706
|
+
try {
|
|
707
|
+
const raw = await fs3.readFile(configPath, "utf-8");
|
|
708
|
+
data = parseJsonSafe(raw);
|
|
709
|
+
} catch {
|
|
710
|
+
data = {};
|
|
711
|
+
}
|
|
712
|
+
const defaultModel = context.model || "claude-3-7-sonnet";
|
|
713
|
+
const apiKey = context.apiKey || "sk-local-srouter";
|
|
714
|
+
const anthropicBaseUrl = context.baseUrl.replace(/\/v1\/?$/, "");
|
|
715
|
+
data.env = data.env || {};
|
|
716
|
+
data.env.ANTHROPIC_BASE_URL = anthropicBaseUrl;
|
|
717
|
+
data.env.ANTHROPIC_API_KEY = apiKey;
|
|
718
|
+
data.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
719
|
+
data.env.ANTHROPIC_DEFAULT_MODEL = defaultModel;
|
|
720
|
+
data.env.ANTHROPIC_MODEL = defaultModel;
|
|
721
|
+
data.env.ANTHROPIC_DEFAULT_OPUS_MODEL = context.opusModel || defaultModel;
|
|
722
|
+
data.env.ANTHROPIC_DEFAULT_SONNET_MODEL = context.sonnetModel || defaultModel;
|
|
723
|
+
data.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = context.haikuModel || defaultModel;
|
|
724
|
+
if (data.env.ANTHROPIC_DEFAULT_FABLE_MODEL) {
|
|
725
|
+
delete data.env.ANTHROPIC_DEFAULT_FABLE_MODEL;
|
|
726
|
+
}
|
|
727
|
+
data.ANTHROPIC_BASE_URL = anthropicBaseUrl;
|
|
728
|
+
data.ANTHROPIC_API_KEY = apiKey;
|
|
729
|
+
data.model = defaultModel;
|
|
730
|
+
data.ANTHROPIC_DEFAULT_OPUS_MODEL = context.opusModel || defaultModel;
|
|
731
|
+
data.ANTHROPIC_DEFAULT_SONNET_MODEL = context.sonnetModel || defaultModel;
|
|
732
|
+
data.ANTHROPIC_DEFAULT_HAIKU_MODEL = context.haikuModel || defaultModel;
|
|
733
|
+
if (!context.dryRun) {
|
|
734
|
+
await fs3.mkdir(path3.dirname(configPath), { recursive: true });
|
|
735
|
+
await fs3.writeFile(configPath, JSON.stringify(data, null, 2), "utf-8");
|
|
736
|
+
const homeClaudeJson = path3.join(os3.homedir(), ".claude.json");
|
|
737
|
+
if (configPath !== homeClaudeJson) {
|
|
738
|
+
try {
|
|
739
|
+
let rootData = {};
|
|
740
|
+
try {
|
|
741
|
+
rootData = parseJsonSafe(await fs3.readFile(homeClaudeJson, "utf-8"));
|
|
742
|
+
} catch {
|
|
743
|
+
rootData = {};
|
|
744
|
+
}
|
|
745
|
+
rootData.ANTHROPIC_BASE_URL = context.baseUrl;
|
|
746
|
+
rootData.ANTHROPIC_API_KEY = apiKey;
|
|
747
|
+
rootData.model = defaultModel;
|
|
748
|
+
rootData.ANTHROPIC_DEFAULT_OPUS_MODEL = context.opusModel || defaultModel;
|
|
749
|
+
rootData.ANTHROPIC_DEFAULT_SONNET_MODEL = context.sonnetModel || defaultModel;
|
|
750
|
+
rootData.ANTHROPIC_DEFAULT_HAIKU_MODEL = context.haikuModel || defaultModel;
|
|
751
|
+
await fs3.writeFile(homeClaudeJson, JSON.stringify(rootData, null, 2), "utf-8");
|
|
752
|
+
} catch {
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
backupPath,
|
|
758
|
+
modifiedPath: configPath
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
async unlink() {
|
|
762
|
+
const restored = await this.store.restoreLatestBackup(this.id);
|
|
763
|
+
if (restored) {
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
const configPath = this.getConfigPath();
|
|
767
|
+
try {
|
|
768
|
+
const raw = await fs3.readFile(configPath, "utf-8");
|
|
769
|
+
const data = parseJsonSafe(raw);
|
|
770
|
+
delete data.ANTHROPIC_BASE_URL;
|
|
771
|
+
delete data.ANTHROPIC_API_KEY;
|
|
772
|
+
delete data.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
773
|
+
delete data.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
774
|
+
delete data.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
775
|
+
delete data.model;
|
|
776
|
+
if (data.env && typeof data.env === "object") {
|
|
777
|
+
delete data.env.ANTHROPIC_BASE_URL;
|
|
778
|
+
delete data.env.ANTHROPIC_API_KEY;
|
|
779
|
+
delete data.env.ANTHROPIC_AUTH_TOKEN;
|
|
780
|
+
delete data.env.ANTHROPIC_DEFAULT_MODEL;
|
|
781
|
+
delete data.env.ANTHROPIC_MODEL;
|
|
782
|
+
delete data.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
783
|
+
delete data.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
784
|
+
delete data.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
785
|
+
delete data.env.ANTHROPIC_DEFAULT_FABLE_MODEL;
|
|
786
|
+
}
|
|
787
|
+
await fs3.writeFile(configPath, JSON.stringify(data, null, 2), "utf-8");
|
|
788
|
+
return true;
|
|
789
|
+
} catch {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
getEnv(context) {
|
|
794
|
+
const defaultModel = context.model || "claude-3-7-sonnet";
|
|
795
|
+
const apiKey = context.apiKey || "sk-local-srouter";
|
|
796
|
+
const anthropicBaseUrl = context.baseUrl.replace(/\/v1\/?$/, "");
|
|
797
|
+
const env = {
|
|
798
|
+
ANTHROPIC_BASE_URL: anthropicBaseUrl,
|
|
799
|
+
ANTHROPIC_API_KEY: apiKey,
|
|
800
|
+
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
801
|
+
ANTHROPIC_DEFAULT_MODEL: defaultModel,
|
|
802
|
+
ANTHROPIC_MODEL: defaultModel,
|
|
803
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: context.opusModel || defaultModel,
|
|
804
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: context.sonnetModel || defaultModel,
|
|
805
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: context.haikuModel || defaultModel
|
|
806
|
+
};
|
|
807
|
+
return env;
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
// src/adapters/opencode.ts
|
|
812
|
+
import fs4 from "node:fs/promises";
|
|
813
|
+
import path4 from "node:path";
|
|
814
|
+
|
|
815
|
+
// src/lib/srouterClient.ts
|
|
816
|
+
function normalizeBaseUrl(baseUrl) {
|
|
817
|
+
return baseUrl.trim().replace(/\/+$/, "");
|
|
818
|
+
}
|
|
819
|
+
function getModelsEndpoint(baseUrl) {
|
|
820
|
+
const cleanUrl = normalizeBaseUrl(baseUrl);
|
|
821
|
+
return cleanUrl.endsWith("/v1") ? `${cleanUrl}/models` : `${cleanUrl}/v1/models`;
|
|
822
|
+
}
|
|
823
|
+
async function checkServerHealth(baseUrl, apiKey, timeoutMs = 3e3) {
|
|
824
|
+
const endpoint = getModelsEndpoint(baseUrl);
|
|
825
|
+
const start = Date.now();
|
|
826
|
+
try {
|
|
827
|
+
const headers = {
|
|
828
|
+
Accept: "application/json"
|
|
829
|
+
};
|
|
830
|
+
if (apiKey) {
|
|
831
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
832
|
+
}
|
|
833
|
+
const controller = new AbortController();
|
|
834
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
835
|
+
const res = await fetch(endpoint, {
|
|
836
|
+
method: "GET",
|
|
837
|
+
headers,
|
|
838
|
+
signal: controller.signal
|
|
839
|
+
});
|
|
840
|
+
clearTimeout(timer);
|
|
841
|
+
if (!res.ok) {
|
|
842
|
+
return {
|
|
843
|
+
healthy: false,
|
|
844
|
+
modelsCount: 0,
|
|
845
|
+
error: `HTTP ${res.status}: ${res.statusText}`,
|
|
846
|
+
latencyMs: Date.now() - start
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
const data = await res.json();
|
|
850
|
+
const models = Array.isArray(data?.data) ? data.data : [];
|
|
851
|
+
return {
|
|
852
|
+
healthy: true,
|
|
853
|
+
modelsCount: models.length,
|
|
854
|
+
latencyMs: Date.now() - start
|
|
855
|
+
};
|
|
856
|
+
} catch (err) {
|
|
857
|
+
const errorName = err instanceof Error ? err.name : "";
|
|
858
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
859
|
+
return {
|
|
860
|
+
healthy: false,
|
|
861
|
+
modelsCount: 0,
|
|
862
|
+
error: errorName === "AbortError" ? "Connection timeout" : errorMessage,
|
|
863
|
+
latencyMs: Date.now() - start
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
async function fetchAvailableModels(baseUrl, apiKey) {
|
|
868
|
+
const endpoint = getModelsEndpoint(baseUrl);
|
|
869
|
+
try {
|
|
870
|
+
const headers = { Accept: "application/json" };
|
|
871
|
+
if (apiKey) {
|
|
872
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
873
|
+
}
|
|
874
|
+
const res = await fetch(endpoint, { headers });
|
|
875
|
+
if (!res.ok) return [];
|
|
876
|
+
const data = await res.json();
|
|
877
|
+
if (!Array.isArray(data?.data)) return [];
|
|
878
|
+
return data.data.map((m) => typeof m === "string" ? m : m?.id).filter((id) => typeof id === "string" && id.length > 0);
|
|
879
|
+
} catch {
|
|
880
|
+
return [];
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/adapters/opencode.ts
|
|
885
|
+
var DEFAULT_SROUTER_MODELS = [
|
|
886
|
+
{ id: "anthropic/claude-3-7-sonnet", name: "Claude 3.7 Sonnet (Anthropic)" },
|
|
887
|
+
{ id: "claude-3-7-sonnet", name: "Claude 3.7 Sonnet" },
|
|
888
|
+
{ id: "anthropic/claude-3-5-sonnet", name: "Claude 3.5 Sonnet (Anthropic)" },
|
|
889
|
+
{ id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet" },
|
|
890
|
+
{ id: "anthropic/claude-3-5-haiku", name: "Claude 3.5 Haiku (Anthropic)" },
|
|
891
|
+
{ id: "openai_codex/gpt-4o", name: "GPT-4o (OpenAI Codex)" },
|
|
892
|
+
{ id: "gpt-4o", name: "GPT-4o" },
|
|
893
|
+
{ id: "openai_codex/gpt-4.5-preview", name: "GPT-4.5 Preview (OpenAI Codex)" },
|
|
894
|
+
{ id: "openai_codex/o3-mini", name: "o3-mini (OpenAI Codex)" },
|
|
895
|
+
{ id: "openai_codex/o1", name: "o1 (OpenAI Codex)" },
|
|
896
|
+
{ id: "antigravity/gemini-2.5-pro", name: "Gemini 2.5 Pro (Antigravity)" },
|
|
897
|
+
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
|
|
898
|
+
{ id: "antigravity/gemini-2.5-flash", name: "Gemini 2.5 Flash (Antigravity)" },
|
|
899
|
+
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
|
|
900
|
+
{ id: "kiro/claude-3-7-sonnet", name: "Claude 3.7 Sonnet (Kiro)" },
|
|
901
|
+
{ id: "qoder/qwen-2.5-coder-32b", name: "Qwen 2.5 Coder 32B (Qoder)" },
|
|
902
|
+
{ id: "deepseek/deepseek-r1", name: "DeepSeek R1" },
|
|
903
|
+
{ id: "deepseek/deepseek-v3", name: "DeepSeek V3" },
|
|
904
|
+
{ id: "combo/flagship", name: "Combo: Flagship Cascade" },
|
|
905
|
+
{ id: "combo/fast", name: "Combo: Fast Fallback" }
|
|
906
|
+
];
|
|
907
|
+
function formatModelDisplayName(modelId) {
|
|
908
|
+
const knownNames = {
|
|
909
|
+
"claude-3-7-sonnet": "Claude 3.7 Sonnet",
|
|
910
|
+
"claude-3-5-sonnet": "Claude 3.5 Sonnet",
|
|
911
|
+
"claude-3-5-haiku": "Claude 3.5 Haiku",
|
|
912
|
+
"claude-3-opus": "Claude 3 Opus",
|
|
913
|
+
"gpt-4o": "GPT-4o",
|
|
914
|
+
"gpt-4o-mini": "GPT-4o Mini",
|
|
915
|
+
"gpt-4.5-preview": "GPT-4.5 Preview",
|
|
916
|
+
"o3-mini": "o3-mini",
|
|
917
|
+
o1: "o1",
|
|
918
|
+
"o1-preview": "o1 Preview",
|
|
919
|
+
"gemini-2.5-pro": "Gemini 2.5 Pro",
|
|
920
|
+
"gemini-2.5-flash": "Gemini 2.5 Flash",
|
|
921
|
+
"gemini-2.0-flash": "Gemini 2.0 Flash",
|
|
922
|
+
"deepseek-r1": "DeepSeek R1",
|
|
923
|
+
"deepseek-v3": "DeepSeek V3",
|
|
924
|
+
"deepseek-coder": "DeepSeek Coder",
|
|
925
|
+
"qwen-2.5-coder-32b": "Qwen 2.5 Coder 32B",
|
|
926
|
+
"qwen-2.5-72b-instruct": "Qwen 2.5 72B",
|
|
927
|
+
"qwen3-max": "Qwen 3 Max",
|
|
928
|
+
flagship: "Flagship Cascade",
|
|
929
|
+
fast: "Fast Fallback"
|
|
930
|
+
};
|
|
931
|
+
if (knownNames[modelId]) {
|
|
932
|
+
return knownNames[modelId];
|
|
933
|
+
}
|
|
934
|
+
if (modelId.includes("/")) {
|
|
935
|
+
const [provider, ...rest] = modelId.split("/");
|
|
936
|
+
const baseModel = rest.join("/");
|
|
937
|
+
const baseName = knownNames[baseModel] || humanizeName(baseModel);
|
|
938
|
+
const providerName = formatProviderLabel(provider);
|
|
939
|
+
return `${baseName} (${providerName})`;
|
|
940
|
+
}
|
|
941
|
+
return humanizeName(modelId);
|
|
942
|
+
}
|
|
943
|
+
function formatProviderLabel(provider) {
|
|
944
|
+
const providerMap = {
|
|
945
|
+
anthropic: "Anthropic",
|
|
946
|
+
openai_codex: "OpenAI Codex",
|
|
947
|
+
openai: "OpenAI",
|
|
948
|
+
antigravity: "Antigravity",
|
|
949
|
+
kiro: "Kiro",
|
|
950
|
+
qoder: "Qoder",
|
|
951
|
+
codebuddy: "CodeBuddy",
|
|
952
|
+
deepseek: "DeepSeek",
|
|
953
|
+
gorouter: "GoRouter",
|
|
954
|
+
bluesminds: "BluesMinds",
|
|
955
|
+
seekai: "SeekAI",
|
|
956
|
+
tabitoken: "TabiToken",
|
|
957
|
+
tokenrouter: "TokenRouter",
|
|
958
|
+
commandcode: "CommandCode",
|
|
959
|
+
combo: "Combo"
|
|
960
|
+
};
|
|
961
|
+
return providerMap[provider] || provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
962
|
+
}
|
|
963
|
+
function humanizeName(str2) {
|
|
964
|
+
return str2.replace(/[-_]/g, " ").replace(/\b[a-z]/g, (c) => c.toUpperCase());
|
|
965
|
+
}
|
|
966
|
+
function parseJsonc(content) {
|
|
967
|
+
try {
|
|
968
|
+
const clean = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").replace(/,\s*([\]}])/g, "$1");
|
|
969
|
+
return JSON.parse(clean);
|
|
970
|
+
} catch {
|
|
971
|
+
try {
|
|
972
|
+
return JSON.parse(content);
|
|
973
|
+
} catch {
|
|
974
|
+
return {};
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
var OpenCodeAdapter = class extends AbstractToolAdapter {
|
|
979
|
+
id = "opencode";
|
|
980
|
+
name = "OpenCode";
|
|
981
|
+
description = "Open-source AI coding assistant and agent";
|
|
982
|
+
customConfigPath;
|
|
983
|
+
constructor(store = defaultStore, customConfigPath) {
|
|
984
|
+
super(store);
|
|
985
|
+
this.customConfigPath = customConfigPath;
|
|
986
|
+
}
|
|
987
|
+
getConfigPath() {
|
|
988
|
+
if (this.customConfigPath) {
|
|
989
|
+
return this.customConfigPath;
|
|
990
|
+
}
|
|
991
|
+
return getOpenCodeConfigPath();
|
|
992
|
+
}
|
|
993
|
+
async isInstalled() {
|
|
994
|
+
const opencode = await isExecutableInPath("opencode");
|
|
995
|
+
if (opencode) return true;
|
|
996
|
+
return isExecutableInPath("interpreter");
|
|
997
|
+
}
|
|
998
|
+
async getStatus() {
|
|
999
|
+
const configPath = this.getConfigPath();
|
|
1000
|
+
const installed = await this.isInstalled();
|
|
1001
|
+
try {
|
|
1002
|
+
const raw = await fs4.readFile(configPath, "utf-8");
|
|
1003
|
+
const parsed = parseJsonc(raw);
|
|
1004
|
+
const baseUrl = parsed.provider?.srouter?.options?.baseURL || parsed.openai_base_url || parsed.api_base || parsed.baseUrl || parsed.providers?.srouter?.baseUrl || void 0;
|
|
1005
|
+
const model = parsed.model || parsed.default_model || void 0;
|
|
1006
|
+
const linked = Boolean(
|
|
1007
|
+
baseUrl && (baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1") || baseUrl.includes("srouter"))
|
|
1008
|
+
);
|
|
1009
|
+
return {
|
|
1010
|
+
id: this.id,
|
|
1011
|
+
name: this.name,
|
|
1012
|
+
installed,
|
|
1013
|
+
linked,
|
|
1014
|
+
configPath,
|
|
1015
|
+
currentBaseUrl: baseUrl,
|
|
1016
|
+
currentModel: model
|
|
1017
|
+
};
|
|
1018
|
+
} catch {
|
|
1019
|
+
return {
|
|
1020
|
+
id: this.id,
|
|
1021
|
+
name: this.name,
|
|
1022
|
+
installed,
|
|
1023
|
+
linked: false,
|
|
1024
|
+
configPath
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
async link(context) {
|
|
1029
|
+
const configPath = this.getConfigPath();
|
|
1030
|
+
const backupPath = context.dryRun ? void 0 : await this.store.createBackup(this.id, configPath);
|
|
1031
|
+
let data = {};
|
|
1032
|
+
try {
|
|
1033
|
+
const raw = await fs4.readFile(configPath, "utf-8");
|
|
1034
|
+
data = parseJsonc(raw);
|
|
1035
|
+
} catch {
|
|
1036
|
+
data = {};
|
|
1037
|
+
}
|
|
1038
|
+
data["$schema"] = "https://opencode.ai/config.json";
|
|
1039
|
+
delete data.providers;
|
|
1040
|
+
delete data.openai_base_url;
|
|
1041
|
+
delete data.api_base;
|
|
1042
|
+
delete data.api_key;
|
|
1043
|
+
delete data.openai_api_key;
|
|
1044
|
+
data.provider = data.provider || {};
|
|
1045
|
+
const existingSrouter = data.provider.srouter || {};
|
|
1046
|
+
const existingModels = existingSrouter.models || {};
|
|
1047
|
+
const modelsMap = {};
|
|
1048
|
+
for (const m of DEFAULT_SROUTER_MODELS) {
|
|
1049
|
+
modelsMap[m.id] = {
|
|
1050
|
+
id: m.id,
|
|
1051
|
+
name: m.name
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
for (const [key, val] of Object.entries(existingModels)) {
|
|
1055
|
+
if (val && typeof val === "object") {
|
|
1056
|
+
modelsMap[key] = val;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
try {
|
|
1060
|
+
const liveModels = context.availableModels && context.availableModels.length > 0 ? context.availableModels : await fetchAvailableModels(context.baseUrl, context.apiKey);
|
|
1061
|
+
for (const rawId of liveModels) {
|
|
1062
|
+
if (!rawId) continue;
|
|
1063
|
+
const cleanId = rawId.startsWith("srouter/") ? rawId.replace(/^srouter\//, "") : rawId;
|
|
1064
|
+
modelsMap[cleanId] = {
|
|
1065
|
+
id: cleanId,
|
|
1066
|
+
name: formatModelDisplayName(cleanId)
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
} catch {
|
|
1070
|
+
}
|
|
1071
|
+
const rawModel = context.model || "claude-3-7-sonnet";
|
|
1072
|
+
const cleanModelId = rawModel.startsWith("srouter/") ? rawModel.replace(/^srouter\//, "") : rawModel;
|
|
1073
|
+
modelsMap[cleanModelId] = {
|
|
1074
|
+
id: cleanModelId,
|
|
1075
|
+
name: modelsMap[cleanModelId]?.name || formatModelDisplayName(cleanModelId)
|
|
1076
|
+
};
|
|
1077
|
+
data.provider.srouter = {
|
|
1078
|
+
name: "SRouter",
|
|
1079
|
+
npm: "@ai-sdk/openai-compatible",
|
|
1080
|
+
options: {
|
|
1081
|
+
baseURL: context.baseUrl,
|
|
1082
|
+
apiKey: context.apiKey || "sk-local-srouter"
|
|
1083
|
+
},
|
|
1084
|
+
models: modelsMap
|
|
1085
|
+
};
|
|
1086
|
+
data.model = `srouter/${cleanModelId}`;
|
|
1087
|
+
if (!context.dryRun) {
|
|
1088
|
+
await fs4.mkdir(path4.dirname(configPath), { recursive: true });
|
|
1089
|
+
await fs4.writeFile(configPath, JSON.stringify(data, null, 4), "utf-8");
|
|
1090
|
+
}
|
|
1091
|
+
return {
|
|
1092
|
+
backupPath,
|
|
1093
|
+
modifiedPath: configPath
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
async unlink() {
|
|
1097
|
+
const restored = await this.store.restoreLatestBackup(this.id);
|
|
1098
|
+
if (restored) {
|
|
1099
|
+
return true;
|
|
1100
|
+
}
|
|
1101
|
+
const configPath = this.getConfigPath();
|
|
1102
|
+
try {
|
|
1103
|
+
const raw = await fs4.readFile(configPath, "utf-8");
|
|
1104
|
+
const data = parseJsonc(raw);
|
|
1105
|
+
if (data.provider?.srouter) {
|
|
1106
|
+
delete data.provider.srouter;
|
|
1107
|
+
}
|
|
1108
|
+
if (data.model?.startsWith("srouter/")) {
|
|
1109
|
+
delete data.model;
|
|
1110
|
+
}
|
|
1111
|
+
delete data.openai_base_url;
|
|
1112
|
+
delete data.api_base;
|
|
1113
|
+
delete data.openai_api_key;
|
|
1114
|
+
delete data.api_key;
|
|
1115
|
+
delete data.providers;
|
|
1116
|
+
await fs4.writeFile(configPath, JSON.stringify(data, null, 4), "utf-8");
|
|
1117
|
+
return true;
|
|
1118
|
+
} catch {
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
getEnv(context) {
|
|
1123
|
+
const env = {
|
|
1124
|
+
OPENAI_BASE_URL: context.baseUrl,
|
|
1125
|
+
ANTHROPIC_BASE_URL: context.baseUrl
|
|
1126
|
+
};
|
|
1127
|
+
if (context.apiKey) {
|
|
1128
|
+
env.OPENAI_API_KEY = context.apiKey;
|
|
1129
|
+
env.ANTHROPIC_API_KEY = context.apiKey;
|
|
1130
|
+
}
|
|
1131
|
+
if (context.model) {
|
|
1132
|
+
const cleanModelId = context.model.startsWith("srouter/") ? context.model.replace(/^srouter\//, "") : context.model;
|
|
1133
|
+
env.OPENCODE_MODEL = `srouter/${cleanModelId}`;
|
|
1134
|
+
}
|
|
1135
|
+
return env;
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// src/adapters/index.ts
|
|
1140
|
+
function createAdapters(store = defaultStore) {
|
|
1141
|
+
return [new ClaudeAdapter(store), new OpenCodeAdapter(store)];
|
|
1142
|
+
}
|
|
1143
|
+
var cachedAdapters = null;
|
|
1144
|
+
function getAllAdapters() {
|
|
1145
|
+
if (!cachedAdapters) {
|
|
1146
|
+
cachedAdapters = createAdapters();
|
|
1147
|
+
}
|
|
1148
|
+
return cachedAdapters;
|
|
1149
|
+
}
|
|
1150
|
+
function getAdapter(id) {
|
|
1151
|
+
const normalized = id.toLowerCase().trim();
|
|
1152
|
+
return getAllAdapters().find(
|
|
1153
|
+
(a) => a.id.toLowerCase() === normalized || a.name.toLowerCase().includes(normalized)
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/lib/ui.ts
|
|
1158
|
+
import pc from "picocolors";
|
|
1159
|
+
import { intro, outro, spinner, note } from "@clack/prompts";
|
|
1160
|
+
var banner = `
|
|
1161
|
+
____ ____ __
|
|
1162
|
+
/ __/ / __ \\___ __ __ / /____ ____
|
|
1163
|
+
_\\ \\ / /_/ / _ \\/ // / / __/ -_)/ __/
|
|
1164
|
+
/___/ \\____/\\___/\\_,_/ \\__/\\__//_/
|
|
1165
|
+
${pc.bold(pc.cyan("AI Gateway & LLM Proxy Router CLI"))}
|
|
1166
|
+
`;
|
|
1167
|
+
function showHeader() {
|
|
1168
|
+
console.log(pc.magenta(banner));
|
|
1169
|
+
}
|
|
1170
|
+
function formatSuccess(msg) {
|
|
1171
|
+
return `${pc.green("\u2714")} ${msg}`;
|
|
1172
|
+
}
|
|
1173
|
+
function formatError(msg) {
|
|
1174
|
+
return `${pc.red("\u2716")} ${msg}`;
|
|
1175
|
+
}
|
|
1176
|
+
function formatWarning(msg) {
|
|
1177
|
+
return `${pc.yellow("\u26A0")} ${msg}`;
|
|
1178
|
+
}
|
|
1179
|
+
function formatInfo(msg) {
|
|
1180
|
+
return `${pc.cyan("\u2139")} ${msg}`;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/commands/setup.ts
|
|
1184
|
+
async function setupCommand(options = {}) {
|
|
1185
|
+
showHeader();
|
|
1186
|
+
const sysInfo = getSystemInfo();
|
|
1187
|
+
intro2(
|
|
1188
|
+
`${pc.bold(pc.magenta("SRouter AI Coding Setup Wizard"))} ${pc.gray(`(${sysInfo.displayName})`)}`
|
|
1189
|
+
);
|
|
1190
|
+
const savedConfig = await defaultStore.loadConfig();
|
|
1191
|
+
let baseUrl = options.url || process.env.SROUTER_BASE_URL || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
|
|
1192
|
+
let apiKey = options.key || process.env.SROUTER_API_KEY || savedConfig.defaultApiKey;
|
|
1193
|
+
let selectedModel = options.model;
|
|
1194
|
+
let opusModel = options.opusModel;
|
|
1195
|
+
let sonnetModel = options.sonnetModel;
|
|
1196
|
+
let haikuModel = options.haikuModel;
|
|
1197
|
+
const s = spinner2();
|
|
1198
|
+
s.start(`Checking SRouter gateway connectivity at ${pc.cyan(baseUrl)}...`);
|
|
1199
|
+
let health = await checkServerHealth(baseUrl, apiKey);
|
|
1200
|
+
let availableModels = [];
|
|
1201
|
+
if (health.healthy) {
|
|
1202
|
+
s.stop(
|
|
1203
|
+
pc.green(
|
|
1204
|
+
`SRouter is ONLINE (${health.latencyMs}ms, ${health.modelsCount} models found)`
|
|
1205
|
+
)
|
|
1206
|
+
);
|
|
1207
|
+
availableModels = await fetchAvailableModels(baseUrl, apiKey);
|
|
1208
|
+
} else {
|
|
1209
|
+
s.stop(pc.yellow(`Could not reach SRouter at ${baseUrl} (${health.error || "offline"})`));
|
|
1210
|
+
const urlInput = await text({
|
|
1211
|
+
message: "Enter SRouter Gateway Base URL:",
|
|
1212
|
+
initialValue: baseUrl,
|
|
1213
|
+
validate(value) {
|
|
1214
|
+
const val = typeof value === "string" ? value.trim() : "";
|
|
1215
|
+
if (!val) return "Base URL cannot be empty";
|
|
1216
|
+
if (!val.startsWith("http://") && !val.startsWith("https://")) {
|
|
1217
|
+
return "URL must start with http:// or https://";
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
if (isCancel(urlInput)) {
|
|
1222
|
+
cancel("Setup cancelled.");
|
|
1223
|
+
process.exitCode = 0;
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const urlStr = typeof urlInput === "string" ? urlInput.trim() : baseUrl;
|
|
1227
|
+
if (urlStr) {
|
|
1228
|
+
baseUrl = urlStr;
|
|
1229
|
+
}
|
|
1230
|
+
s.start(`Connecting to ${pc.cyan(baseUrl)}...`);
|
|
1231
|
+
health = await checkServerHealth(baseUrl, apiKey);
|
|
1232
|
+
if (health.healthy) {
|
|
1233
|
+
s.stop(pc.green(`Connected to SRouter (${health.modelsCount} models found)`));
|
|
1234
|
+
availableModels = await fetchAvailableModels(baseUrl, apiKey);
|
|
1235
|
+
} else {
|
|
1236
|
+
s.stop(pc.yellow(`Proceeding with offline / unverified endpoint: ${baseUrl}`));
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (!apiKey) {
|
|
1240
|
+
const keyInput = await text({
|
|
1241
|
+
message: "Enter SRouter API Key (press Enter to skip if running in local auth-free mode):",
|
|
1242
|
+
placeholder: "sk-..."
|
|
1243
|
+
});
|
|
1244
|
+
if (isCancel(keyInput)) {
|
|
1245
|
+
cancel("Setup cancelled.");
|
|
1246
|
+
process.exitCode = 0;
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const keyStr = typeof keyInput === "string" ? keyInput.trim() : "";
|
|
1250
|
+
if (keyStr) {
|
|
1251
|
+
apiKey = keyStr;
|
|
1252
|
+
if (availableModels.length === 0) {
|
|
1253
|
+
availableModels = await fetchAvailableModels(baseUrl, apiKey);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const adapters = getAllAdapters();
|
|
1258
|
+
const adapterStatuses = await Promise.all(adapters.map((a) => a.getStatus()));
|
|
1259
|
+
const toolOptions = adapters.map((adapter, idx) => {
|
|
1260
|
+
const st = adapterStatuses[idx];
|
|
1261
|
+
let label = adapter.name;
|
|
1262
|
+
let hint = "";
|
|
1263
|
+
if (st.linked) {
|
|
1264
|
+
label = `${adapter.name} [\u2714 CONFIGURED]`;
|
|
1265
|
+
const modelPart = st.currentModel ? `, Model: ${st.currentModel}` : "";
|
|
1266
|
+
hint = `Active on ${st.currentBaseUrl}${modelPart} (Select to update settings)`;
|
|
1267
|
+
} else if (st.installed) {
|
|
1268
|
+
label = `${adapter.name} [\u25CB NOT CONFIGURED]`;
|
|
1269
|
+
hint = `${adapter.description} (Installed on system)`;
|
|
1270
|
+
} else {
|
|
1271
|
+
label = `${adapter.name} [\u2716 NOT INSTALLED]`;
|
|
1272
|
+
hint = `${adapter.description} (Executable not found in PATH)`;
|
|
1273
|
+
}
|
|
1274
|
+
return {
|
|
1275
|
+
value: adapter.id,
|
|
1276
|
+
label,
|
|
1277
|
+
hint
|
|
1278
|
+
};
|
|
1279
|
+
});
|
|
1280
|
+
const selectedTools = await multiselect({
|
|
1281
|
+
message: "Select AI coding tools to connect to SRouter (Use Space to select, Enter to confirm):",
|
|
1282
|
+
options: toolOptions,
|
|
1283
|
+
required: false
|
|
1284
|
+
});
|
|
1285
|
+
if (isCancel(selectedTools)) {
|
|
1286
|
+
cancel("Setup cancelled.");
|
|
1287
|
+
process.exitCode = 0;
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const toolsToConfigure = Array.isArray(selectedTools) ? selectedTools : [];
|
|
1291
|
+
if (toolsToConfigure.length === 0) {
|
|
1292
|
+
await defaultStore.saveConfig({
|
|
1293
|
+
defaultBaseUrl: baseUrl,
|
|
1294
|
+
defaultApiKey: apiKey,
|
|
1295
|
+
lastSetupAt: Date.now()
|
|
1296
|
+
});
|
|
1297
|
+
note2(
|
|
1298
|
+
`Base URL: ${pc.cyan(baseUrl)}
|
|
1299
|
+
${apiKey ? `API Key: ${pc.gray("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" + apiKey.slice(-4))}
|
|
1300
|
+
` : ""}
|
|
1301
|
+
${pc.yellow("No tools selected for configuration.")}`,
|
|
1302
|
+
"Configuration Summary"
|
|
1303
|
+
);
|
|
1304
|
+
outro2(
|
|
1305
|
+
pc.bold(
|
|
1306
|
+
pc.green(
|
|
1307
|
+
"\u2714 Gateway settings saved. You can run 'srouter setup' or 'srouter link <tool>' at any time."
|
|
1308
|
+
)
|
|
1309
|
+
)
|
|
1310
|
+
);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (!selectedModel) {
|
|
1314
|
+
const currentDefault = savedConfig.defaultModel || "claude-3-7-sonnet";
|
|
1315
|
+
if (availableModels.length > 0) {
|
|
1316
|
+
const defaultHint = `(${currentDefault}) - all ${availableModels.length} models are automatically registered in OpenCode`;
|
|
1317
|
+
const modelChoice = await select({
|
|
1318
|
+
message: "Select default model for tools:",
|
|
1319
|
+
options: [
|
|
1320
|
+
{
|
|
1321
|
+
value: "__skip__",
|
|
1322
|
+
label: `Use default / Skip ${pc.gray(defaultHint)}`
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
value: "__popular__",
|
|
1326
|
+
label: "Choose from popular models (Claude 3.7, GPT-4o, Gemini 2.5, DeepSeek R1)..."
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
value: "__custom__",
|
|
1330
|
+
label: "Enter model ID manually..."
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
value: "__browse__",
|
|
1334
|
+
label: `Browse full model list (${availableModels.length} models)...`
|
|
1335
|
+
}
|
|
1336
|
+
]
|
|
1337
|
+
});
|
|
1338
|
+
if (isCancel(modelChoice)) {
|
|
1339
|
+
cancel("Setup cancelled.");
|
|
1340
|
+
process.exitCode = 0;
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
if (modelChoice === "__skip__") {
|
|
1344
|
+
selectedModel = currentDefault;
|
|
1345
|
+
} else if (modelChoice === "__popular__") {
|
|
1346
|
+
const popularChoice = await select({
|
|
1347
|
+
message: "Choose a popular model:",
|
|
1348
|
+
options: [
|
|
1349
|
+
{ value: "claude-3-7-sonnet", label: "Claude 3.7 Sonnet (Recommended)" },
|
|
1350
|
+
{ value: "claude-3-5-sonnet", label: "Claude 3.5 Sonnet" },
|
|
1351
|
+
{ value: "gpt-4o", label: "GPT-4o (OpenAI)" },
|
|
1352
|
+
{ value: "gemini-2.5-pro", label: "Gemini 2.5 Pro (Google)" },
|
|
1353
|
+
{ value: "gemini-2.5-flash", label: "Gemini 2.5 Flash (Fast)" },
|
|
1354
|
+
{ value: "deepseek-r1", label: "DeepSeek R1 (Reasoning)" },
|
|
1355
|
+
{ value: "combo/flagship", label: "Combo: Flagship Cascade" },
|
|
1356
|
+
{ value: "__custom__", label: "Other / Custom ID..." }
|
|
1357
|
+
]
|
|
1358
|
+
});
|
|
1359
|
+
if (isCancel(popularChoice)) {
|
|
1360
|
+
cancel("Setup cancelled.");
|
|
1361
|
+
process.exitCode = 0;
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (popularChoice === "__custom__") {
|
|
1365
|
+
const customModelInput = await text({
|
|
1366
|
+
message: "Enter custom model ID:",
|
|
1367
|
+
placeholder: currentDefault
|
|
1368
|
+
});
|
|
1369
|
+
if (isCancel(customModelInput)) {
|
|
1370
|
+
cancel("Setup cancelled.");
|
|
1371
|
+
process.exitCode = 0;
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
const customModelStr = typeof customModelInput === "string" ? customModelInput.trim() : "";
|
|
1375
|
+
selectedModel = customModelStr || currentDefault;
|
|
1376
|
+
} else {
|
|
1377
|
+
selectedModel = popularChoice;
|
|
1378
|
+
}
|
|
1379
|
+
} else if (modelChoice === "__custom__") {
|
|
1380
|
+
const customModelInput = await text({
|
|
1381
|
+
message: "Enter custom model ID:",
|
|
1382
|
+
placeholder: currentDefault
|
|
1383
|
+
});
|
|
1384
|
+
if (isCancel(customModelInput)) {
|
|
1385
|
+
cancel("Setup cancelled.");
|
|
1386
|
+
process.exitCode = 0;
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
const customModelStr = typeof customModelInput === "string" ? customModelInput.trim() : "";
|
|
1390
|
+
selectedModel = customModelStr || currentDefault;
|
|
1391
|
+
} else if (modelChoice === "__browse__") {
|
|
1392
|
+
const modelOptions = availableModels.map((m) => ({
|
|
1393
|
+
value: m,
|
|
1394
|
+
label: m
|
|
1395
|
+
}));
|
|
1396
|
+
const browseChoice = await select({
|
|
1397
|
+
message: `Select from ${availableModels.length} available models:`,
|
|
1398
|
+
options: [
|
|
1399
|
+
...modelOptions,
|
|
1400
|
+
{ value: "__custom__", label: "Custom model name..." }
|
|
1401
|
+
]
|
|
1402
|
+
});
|
|
1403
|
+
if (isCancel(browseChoice)) {
|
|
1404
|
+
cancel("Setup cancelled.");
|
|
1405
|
+
process.exitCode = 0;
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (browseChoice === "__custom__") {
|
|
1409
|
+
const customModelInput = await text({
|
|
1410
|
+
message: "Enter custom model ID:",
|
|
1411
|
+
placeholder: currentDefault
|
|
1412
|
+
});
|
|
1413
|
+
if (isCancel(customModelInput)) {
|
|
1414
|
+
cancel("Setup cancelled.");
|
|
1415
|
+
process.exitCode = 0;
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const customModelStr = typeof customModelInput === "string" ? customModelInput.trim() : "";
|
|
1419
|
+
selectedModel = customModelStr || currentDefault;
|
|
1420
|
+
} else {
|
|
1421
|
+
selectedModel = browseChoice;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
} else {
|
|
1425
|
+
const modelInput = await text({
|
|
1426
|
+
message: "Enter default model ID for tools (optional, press Enter for default):",
|
|
1427
|
+
placeholder: "claude-3-7-sonnet"
|
|
1428
|
+
});
|
|
1429
|
+
if (isCancel(modelInput)) {
|
|
1430
|
+
cancel("Setup cancelled.");
|
|
1431
|
+
process.exitCode = 0;
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
const modelStr = typeof modelInput === "string" ? modelInput.trim() : "";
|
|
1435
|
+
selectedModel = modelStr || void 0;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (toolsToConfigure.includes("claude") && !options.opusModel && !options.sonnetModel && !options.haikuModel) {
|
|
1439
|
+
const configTiersChoice = await select({
|
|
1440
|
+
message: "Configure Claude Code specific model tiers (Opus, Sonnet, Haiku)?",
|
|
1441
|
+
options: [
|
|
1442
|
+
{
|
|
1443
|
+
value: "auto",
|
|
1444
|
+
label: "Use general default model / skip",
|
|
1445
|
+
hint: selectedModel || "Default Claude models"
|
|
1446
|
+
},
|
|
1447
|
+
{
|
|
1448
|
+
value: "custom",
|
|
1449
|
+
label: "Customize per tier (Opus, Sonnet, Haiku)"
|
|
1450
|
+
}
|
|
1451
|
+
]
|
|
1452
|
+
});
|
|
1453
|
+
if (isCancel(configTiersChoice)) {
|
|
1454
|
+
cancel("Setup cancelled.");
|
|
1455
|
+
process.exitCode = 0;
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (configTiersChoice === "custom") {
|
|
1459
|
+
const pickTierModel = async (tierName, envVar, defaultVal) => {
|
|
1460
|
+
if (availableModels.length > 0) {
|
|
1461
|
+
const choice = await select({
|
|
1462
|
+
message: `Select model for ${tierName} (${envVar}):`,
|
|
1463
|
+
options: [
|
|
1464
|
+
{ value: "__skip__", label: "Skip (Use default)" },
|
|
1465
|
+
...availableModels.map((m) => ({ value: m, label: m })),
|
|
1466
|
+
{ value: "__custom__", label: "Custom model name..." }
|
|
1467
|
+
]
|
|
1468
|
+
});
|
|
1469
|
+
if (isCancel(choice)) return void 0;
|
|
1470
|
+
if (choice === "__skip__") return void 0;
|
|
1471
|
+
if (choice === "__custom__") {
|
|
1472
|
+
const customInput2 = await text({
|
|
1473
|
+
message: `Enter custom model ID for ${tierName}:`,
|
|
1474
|
+
placeholder: defaultVal || "claude-3-7-sonnet"
|
|
1475
|
+
});
|
|
1476
|
+
if (isCancel(customInput2)) return void 0;
|
|
1477
|
+
const val2 = typeof customInput2 === "string" ? customInput2.trim() : "";
|
|
1478
|
+
return val2 || void 0;
|
|
1479
|
+
}
|
|
1480
|
+
return choice;
|
|
1481
|
+
}
|
|
1482
|
+
const customInput = await text({
|
|
1483
|
+
message: `Enter model ID for ${tierName} (${envVar}, optional):`,
|
|
1484
|
+
placeholder: defaultVal || "claude-3-7-sonnet"
|
|
1485
|
+
});
|
|
1486
|
+
if (isCancel(customInput)) return void 0;
|
|
1487
|
+
const val = typeof customInput === "string" ? customInput.trim() : "";
|
|
1488
|
+
return val || void 0;
|
|
1489
|
+
};
|
|
1490
|
+
const pickedSonnet = await pickTierModel(
|
|
1491
|
+
"Sonnet",
|
|
1492
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
1493
|
+
selectedModel || "claude-3-7-sonnet"
|
|
1494
|
+
);
|
|
1495
|
+
if (pickedSonnet !== void 0) {
|
|
1496
|
+
sonnetModel = pickedSonnet;
|
|
1497
|
+
}
|
|
1498
|
+
const pickedOpus = await pickTierModel(
|
|
1499
|
+
"Opus",
|
|
1500
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
1501
|
+
"claude-3-opus-20240229"
|
|
1502
|
+
);
|
|
1503
|
+
if (pickedOpus !== void 0) {
|
|
1504
|
+
opusModel = pickedOpus;
|
|
1505
|
+
}
|
|
1506
|
+
const pickedHaiku = await pickTierModel(
|
|
1507
|
+
"Haiku",
|
|
1508
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
1509
|
+
"claude-3-5-haiku-20241022"
|
|
1510
|
+
);
|
|
1511
|
+
if (pickedHaiku !== void 0) {
|
|
1512
|
+
haikuModel = pickedHaiku;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
s.start("Applying SRouter configurations...");
|
|
1517
|
+
const linkResults = [];
|
|
1518
|
+
for (const toolId of toolsToConfigure) {
|
|
1519
|
+
const adapter = adapters.find((a) => a.id === toolId);
|
|
1520
|
+
if (!adapter) continue;
|
|
1521
|
+
const result = await adapter.link({
|
|
1522
|
+
baseUrl,
|
|
1523
|
+
apiKey,
|
|
1524
|
+
model: selectedModel,
|
|
1525
|
+
opusModel: toolId === "claude" ? opusModel : void 0,
|
|
1526
|
+
sonnetModel: toolId === "claude" ? sonnetModel : void 0,
|
|
1527
|
+
haikuModel: toolId === "claude" ? haikuModel : void 0,
|
|
1528
|
+
availableModels
|
|
1529
|
+
});
|
|
1530
|
+
linkResults.push({
|
|
1531
|
+
name: adapter.name,
|
|
1532
|
+
path: result.modifiedPath,
|
|
1533
|
+
backup: result.backupPath
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
await defaultStore.saveConfig({
|
|
1537
|
+
defaultBaseUrl: baseUrl,
|
|
1538
|
+
defaultApiKey: apiKey,
|
|
1539
|
+
defaultModel: selectedModel,
|
|
1540
|
+
defaultOpusModel: opusModel,
|
|
1541
|
+
defaultSonnetModel: sonnetModel,
|
|
1542
|
+
defaultHaikuModel: haikuModel,
|
|
1543
|
+
lastSetupAt: Date.now()
|
|
1544
|
+
});
|
|
1545
|
+
s.stop(pc.green("All configurations applied successfully!"));
|
|
1546
|
+
const summaryLines = [
|
|
1547
|
+
`OS / System: ${pc.cyan(sysInfo.displayName)}`,
|
|
1548
|
+
`Base URL: ${pc.cyan(baseUrl)}`,
|
|
1549
|
+
...apiKey ? [`API Key: ${pc.gray("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" + apiKey.slice(-4))}`] : [],
|
|
1550
|
+
...selectedModel ? [`Model: ${pc.cyan(selectedModel)}`] : [],
|
|
1551
|
+
...opusModel ? [`Opus: ${pc.cyan(opusModel)}`] : [],
|
|
1552
|
+
...sonnetModel ? [`Sonnet: ${pc.cyan(sonnetModel)}`] : [],
|
|
1553
|
+
...haikuModel ? [`Haiku: ${pc.cyan(haikuModel)}`] : [],
|
|
1554
|
+
"",
|
|
1555
|
+
pc.bold("Configured Tools:")
|
|
1556
|
+
];
|
|
1557
|
+
for (const r of linkResults) {
|
|
1558
|
+
summaryLines.push(` \u2714 ${pc.bold(r.name)} -> ${pc.gray(r.path)}`);
|
|
1559
|
+
if (r.backup) {
|
|
1560
|
+
summaryLines.push(` ${pc.gray("Backup:")} ${pc.gray(r.backup)}`);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
note2(summaryLines.join("\n"), "Configuration Summary");
|
|
1564
|
+
outro2(
|
|
1565
|
+
pc.bold(pc.green("\u2714 SRouter is connected! You can now use your AI coding tools directly."))
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/commands/link.ts
|
|
1570
|
+
async function linkCommand(toolId, options) {
|
|
1571
|
+
const adapter = getAdapter(toolId);
|
|
1572
|
+
if (!adapter) {
|
|
1573
|
+
console.error(
|
|
1574
|
+
formatError(
|
|
1575
|
+
`Tool '${pc.bold(toolId)}' not supported. Available tools: ${getAllAdapters().map((a) => a.id).join(", ")}`
|
|
1576
|
+
)
|
|
1577
|
+
);
|
|
1578
|
+
process.exitCode = 1;
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
const savedConfig = await defaultStore.loadConfig();
|
|
1582
|
+
const baseUrl = options.url || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
|
|
1583
|
+
const apiKey = options.key || savedConfig.defaultApiKey;
|
|
1584
|
+
const model = options.model || savedConfig.defaultModel;
|
|
1585
|
+
const opusModel = options.opusModel || savedConfig.defaultOpusModel;
|
|
1586
|
+
const sonnetModel = options.sonnetModel || savedConfig.defaultSonnetModel;
|
|
1587
|
+
const haikuModel = options.haikuModel || savedConfig.defaultHaikuModel;
|
|
1588
|
+
try {
|
|
1589
|
+
const result = await adapter.link({
|
|
1590
|
+
baseUrl,
|
|
1591
|
+
apiKey,
|
|
1592
|
+
model,
|
|
1593
|
+
opusModel,
|
|
1594
|
+
sonnetModel,
|
|
1595
|
+
haikuModel,
|
|
1596
|
+
dryRun: options.dryRun
|
|
1597
|
+
});
|
|
1598
|
+
console.log(
|
|
1599
|
+
formatSuccess(
|
|
1600
|
+
`Successfully configured ${pc.bold(pc.cyan(adapter.name))} with SRouter proxy!`
|
|
1601
|
+
)
|
|
1602
|
+
);
|
|
1603
|
+
console.log(` ${pc.gray("Target Config:")} ${pc.white(result.modifiedPath)}`);
|
|
1604
|
+
console.log(` ${pc.gray("Proxy URL:")} ${pc.white(baseUrl)}`);
|
|
1605
|
+
if (model) {
|
|
1606
|
+
console.log(` ${pc.gray("Model:")} ${pc.white(model)}`);
|
|
1607
|
+
}
|
|
1608
|
+
if (opusModel) {
|
|
1609
|
+
console.log(` ${pc.gray("Opus Model:")} ${pc.white(opusModel)}`);
|
|
1610
|
+
}
|
|
1611
|
+
if (sonnetModel) {
|
|
1612
|
+
console.log(` ${pc.gray("Sonnet Model:")} ${pc.white(sonnetModel)}`);
|
|
1613
|
+
}
|
|
1614
|
+
if (haikuModel) {
|
|
1615
|
+
console.log(` ${pc.gray("Haiku Model:")} ${pc.white(haikuModel)}`);
|
|
1616
|
+
}
|
|
1617
|
+
if (result.backupPath) {
|
|
1618
|
+
console.log(` ${pc.gray("Backup Saved:")} ${pc.white(result.backupPath)}`);
|
|
1619
|
+
}
|
|
1620
|
+
} catch (err) {
|
|
1621
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1622
|
+
console.error(formatError(`Failed to link ${adapter.name}: ${msg}`));
|
|
1623
|
+
process.exitCode = 1;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// src/commands/unlink.ts
|
|
1628
|
+
async function unlinkCommand(toolId) {
|
|
1629
|
+
const adapter = getAdapter(toolId);
|
|
1630
|
+
if (!adapter) {
|
|
1631
|
+
console.error(
|
|
1632
|
+
formatError(
|
|
1633
|
+
`Tool '${pc.bold(toolId)}' not supported. Available tools: ${getAllAdapters().map((a) => a.id).join(", ")}`
|
|
1634
|
+
)
|
|
1635
|
+
);
|
|
1636
|
+
process.exitCode = 1;
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
try {
|
|
1640
|
+
const restored = await adapter.unlink();
|
|
1641
|
+
if (restored) {
|
|
1642
|
+
console.log(
|
|
1643
|
+
formatSuccess(
|
|
1644
|
+
`Restored original configuration for ${pc.bold(pc.cyan(adapter.name))}!`
|
|
1645
|
+
)
|
|
1646
|
+
);
|
|
1647
|
+
} else {
|
|
1648
|
+
console.log(
|
|
1649
|
+
formatInfo(
|
|
1650
|
+
`No active SRouter configuration or backup found for ${pc.bold(adapter.name)}.`
|
|
1651
|
+
)
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
} catch (err) {
|
|
1655
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1656
|
+
console.error(formatError(`Failed to unlink ${adapter.name}: ${msg}`));
|
|
1657
|
+
process.exitCode = 1;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/commands/sync.ts
|
|
1662
|
+
async function syncCommand(toolId, options = {}) {
|
|
1663
|
+
const savedConfig = await defaultStore.loadConfig();
|
|
1664
|
+
const baseUrl = options.url || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
|
|
1665
|
+
const apiKey = options.key || savedConfig.defaultApiKey;
|
|
1666
|
+
const health = await checkServerHealth(baseUrl, apiKey);
|
|
1667
|
+
if (!health.healthy) {
|
|
1668
|
+
console.error(
|
|
1669
|
+
formatError(
|
|
1670
|
+
`Cannot sync: SRouter Gateway is unreachable at ${pc.bold(baseUrl)} (${health.error || "offline"}).`
|
|
1671
|
+
)
|
|
1672
|
+
);
|
|
1673
|
+
process.exitCode = 1;
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
const availableModels = await fetchAvailableModels(baseUrl, apiKey);
|
|
1677
|
+
if (availableModels.length === 0) {
|
|
1678
|
+
console.warn(
|
|
1679
|
+
formatWarning(`SRouter Gateway responded at ${baseUrl}, but returned 0 models.`)
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
const adaptersToSync = toolId ? [getAdapter(toolId)].filter((a) => Boolean(a)) : getAllAdapters();
|
|
1683
|
+
if (toolId && adaptersToSync.length === 0) {
|
|
1684
|
+
console.error(formatError(`Tool '${pc.bold(toolId)}' not supported.`));
|
|
1685
|
+
process.exitCode = 1;
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
for (const adapter of adaptersToSync) {
|
|
1689
|
+
if (!adapter) continue;
|
|
1690
|
+
const status = await adapter.getStatus();
|
|
1691
|
+
if (!status.linked && !toolId) {
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
try {
|
|
1695
|
+
const result = await adapter.link({
|
|
1696
|
+
baseUrl,
|
|
1697
|
+
apiKey,
|
|
1698
|
+
model: savedConfig.defaultModel || status.currentModel,
|
|
1699
|
+
opusModel: savedConfig.defaultOpusModel,
|
|
1700
|
+
sonnetModel: savedConfig.defaultSonnetModel,
|
|
1701
|
+
haikuModel: savedConfig.defaultHaikuModel,
|
|
1702
|
+
availableModels
|
|
1703
|
+
});
|
|
1704
|
+
console.log(
|
|
1705
|
+
formatSuccess(
|
|
1706
|
+
`Synced ${pc.bold(pc.cyan(availableModels.length.toString()))} models to ${pc.bold(adapter.name)} (${pc.gray(result.modifiedPath)})`
|
|
1707
|
+
)
|
|
1708
|
+
);
|
|
1709
|
+
} catch (err) {
|
|
1710
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1711
|
+
console.error(formatError(`Failed to sync ${adapter.name}: ${msg}`));
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/commands/status.ts
|
|
1717
|
+
async function statusCommand(options) {
|
|
1718
|
+
showHeader();
|
|
1719
|
+
const sysInfo = getSystemInfo();
|
|
1720
|
+
console.log(pc.bold(pc.underline("System Environment:")));
|
|
1721
|
+
console.log(` ${pc.gray("OS:")} ${pc.white(sysInfo.displayName)}`);
|
|
1722
|
+
console.log(` ${pc.gray("Platform:")} ${pc.white(sysInfo.platform)} (${sysInfo.arch})`);
|
|
1723
|
+
console.log(` ${pc.gray("Shell:")} ${pc.white(sysInfo.detectedShell)}`);
|
|
1724
|
+
console.log(` ${pc.gray("Home Dir:")} ${pc.white(sysInfo.homeDir)}`);
|
|
1725
|
+
console.log("");
|
|
1726
|
+
const savedConfig = await defaultStore.loadConfig();
|
|
1727
|
+
const baseUrl = options.url || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
|
|
1728
|
+
const apiKey = options.key || savedConfig.defaultApiKey;
|
|
1729
|
+
console.log(pc.bold(pc.underline("Gateway Status:")));
|
|
1730
|
+
console.log(` ${pc.gray("Target URL:")} ${pc.white(baseUrl)}`);
|
|
1731
|
+
const health = await checkServerHealth(baseUrl, apiKey);
|
|
1732
|
+
if (health.healthy) {
|
|
1733
|
+
console.log(
|
|
1734
|
+
` ${pc.gray("Health:")} ${pc.green("ONLINE")} ${pc.gray(`(${health.latencyMs}ms)`)}`
|
|
1735
|
+
);
|
|
1736
|
+
const models = await fetchAvailableModels(baseUrl, apiKey);
|
|
1737
|
+
console.log(` ${pc.gray("Models:")} ${pc.cyan(`${models.length} available`)}`);
|
|
1738
|
+
if (models.length > 0) {
|
|
1739
|
+
const preview = models.slice(0, 5).join(", ");
|
|
1740
|
+
const extra = models.length > 5 ? ` +${models.length - 5} more` : "";
|
|
1741
|
+
console.log(` ${pc.gray(`[${preview}${extra}]`)}`);
|
|
1742
|
+
}
|
|
1743
|
+
} else {
|
|
1744
|
+
console.log(` ${pc.gray("Health:")} ${pc.red("OFFLINE / UNREACHABLE")}`);
|
|
1745
|
+
if (health.error) {
|
|
1746
|
+
console.log(` ${pc.gray("Reason:")} ${pc.yellow(health.error)}`);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
console.log("");
|
|
1750
|
+
console.log(pc.bold(pc.underline("Supported AI Coding Tools:")));
|
|
1751
|
+
const adapters = getAllAdapters();
|
|
1752
|
+
for (const adapter of adapters) {
|
|
1753
|
+
const status = await adapter.getStatus();
|
|
1754
|
+
const icon = status.linked ? pc.green("\u25CF CONFIGURED (LINKED)") : pc.gray("\u25CB NOT CONFIGURED (UNLINKED)");
|
|
1755
|
+
const installBadge = status.installed ? pc.green("[Installed]") : pc.yellow("[Not in PATH]");
|
|
1756
|
+
console.log(`
|
|
1757
|
+
${pc.bold(pc.cyan(adapter.name))} ${icon} ${installBadge}`);
|
|
1758
|
+
console.log(` ${pc.gray("ID:")} ${adapter.id}`);
|
|
1759
|
+
console.log(` ${pc.gray("Config Path:")} ${status.configPath || "N/A"}`);
|
|
1760
|
+
if (status.linked) {
|
|
1761
|
+
console.log(` ${pc.gray("Active URL:")} ${pc.green(status.currentBaseUrl || "N/A")}`);
|
|
1762
|
+
if (status.currentModel) {
|
|
1763
|
+
console.log(` ${pc.gray("Model:")} ${pc.green(status.currentModel)}`);
|
|
1764
|
+
}
|
|
1765
|
+
if (status.currentOpusModel) {
|
|
1766
|
+
console.log(` ${pc.gray("Opus Model:")} ${pc.green(status.currentOpusModel)}`);
|
|
1767
|
+
}
|
|
1768
|
+
if (status.currentSonnetModel) {
|
|
1769
|
+
console.log(` ${pc.gray("Sonnet Model:")}${pc.green(status.currentSonnetModel)}`);
|
|
1770
|
+
}
|
|
1771
|
+
if (status.currentHaikuModel) {
|
|
1772
|
+
console.log(` ${pc.gray("Haiku Model:")} ${pc.green(status.currentHaikuModel)}`);
|
|
1773
|
+
}
|
|
1774
|
+
} else {
|
|
1775
|
+
console.log(
|
|
1776
|
+
` ${pc.gray("Status:")} ${pc.yellow("Not connected to SRouter proxy")}`
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
console.log("");
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// src/commands/env.ts
|
|
1784
|
+
async function envCommand(toolId, options = {}) {
|
|
1785
|
+
const savedConfig = await defaultStore.loadConfig();
|
|
1786
|
+
const baseUrl = options.url || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
|
|
1787
|
+
const apiKey = options.key || savedConfig.defaultApiKey;
|
|
1788
|
+
const model = options.model || savedConfig.defaultModel;
|
|
1789
|
+
const opusModel = options.opusModel || savedConfig.defaultOpusModel;
|
|
1790
|
+
const sonnetModel = options.sonnetModel || savedConfig.defaultSonnetModel;
|
|
1791
|
+
const haikuModel = options.haikuModel || savedConfig.defaultHaikuModel;
|
|
1792
|
+
const context = {
|
|
1793
|
+
baseUrl,
|
|
1794
|
+
apiKey,
|
|
1795
|
+
model,
|
|
1796
|
+
opusModel,
|
|
1797
|
+
sonnetModel,
|
|
1798
|
+
haikuModel
|
|
1799
|
+
};
|
|
1800
|
+
let envVars = {};
|
|
1801
|
+
if (toolId) {
|
|
1802
|
+
const adapter = getAdapter(toolId);
|
|
1803
|
+
if (!adapter) {
|
|
1804
|
+
console.error(
|
|
1805
|
+
formatError(
|
|
1806
|
+
`Tool '${pc.bold(toolId)}' not supported. Available: ${getAllAdapters().map((a) => a.id).join(", ")}`
|
|
1807
|
+
)
|
|
1808
|
+
);
|
|
1809
|
+
process.exitCode = 1;
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
envVars = adapter.getEnv(context);
|
|
1813
|
+
} else {
|
|
1814
|
+
envVars = {
|
|
1815
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
1816
|
+
OPENAI_BASE_URL: baseUrl
|
|
1817
|
+
};
|
|
1818
|
+
if (apiKey) {
|
|
1819
|
+
envVars.ANTHROPIC_API_KEY = apiKey;
|
|
1820
|
+
envVars.OPENAI_API_KEY = apiKey;
|
|
1821
|
+
}
|
|
1822
|
+
if (model) {
|
|
1823
|
+
envVars.ANTHROPIC_MODEL = model;
|
|
1824
|
+
envVars.OPENCODE_MODEL = model;
|
|
1825
|
+
}
|
|
1826
|
+
if (opusModel) {
|
|
1827
|
+
envVars.ANTHROPIC_DEFAULT_OPUS_MODEL = opusModel;
|
|
1828
|
+
}
|
|
1829
|
+
if (sonnetModel) {
|
|
1830
|
+
envVars.ANTHROPIC_DEFAULT_SONNET_MODEL = sonnetModel;
|
|
1831
|
+
}
|
|
1832
|
+
if (haikuModel) {
|
|
1833
|
+
envVars.ANTHROPIC_DEFAULT_HAIKU_MODEL = haikuModel;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
let targetShell = detectShell();
|
|
1837
|
+
if (options.fish) {
|
|
1838
|
+
targetShell = "fish";
|
|
1839
|
+
} else if (options.shell) {
|
|
1840
|
+
const s = options.shell.toLowerCase();
|
|
1841
|
+
if (s === "fish" || s === "powershell" || s === "pwsh" || s === "cmd" || s === "zsh" || s === "bash") {
|
|
1842
|
+
targetShell = s === "pwsh" ? "powershell" : s;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
1846
|
+
console.log(formatShellExport(key, value, targetShell));
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// src/commands/run.ts
|
|
1851
|
+
import { spawn } from "node:child_process";
|
|
1852
|
+
async function runCommand(toolId, toolArgs, options = {}) {
|
|
1853
|
+
const adapter = getAdapter(toolId);
|
|
1854
|
+
if (!adapter) {
|
|
1855
|
+
console.error(
|
|
1856
|
+
formatError(
|
|
1857
|
+
`Tool '${pc.bold(toolId)}' not supported. Available: ${getAllAdapters().map((a) => a.id).join(", ")}`
|
|
1858
|
+
)
|
|
1859
|
+
);
|
|
1860
|
+
process.exitCode = 1;
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
const savedConfig = await defaultStore.loadConfig();
|
|
1864
|
+
const baseUrl = options.url || savedConfig.defaultBaseUrl || "http://localhost:3000/v1";
|
|
1865
|
+
const apiKey = options.key || savedConfig.defaultApiKey;
|
|
1866
|
+
const model = options.model || savedConfig.defaultModel;
|
|
1867
|
+
const opusModel = options.opusModel || savedConfig.defaultOpusModel;
|
|
1868
|
+
const sonnetModel = options.sonnetModel || savedConfig.defaultSonnetModel;
|
|
1869
|
+
const haikuModel = options.haikuModel || savedConfig.defaultHaikuModel;
|
|
1870
|
+
const envToInject = adapter.getEnv({
|
|
1871
|
+
baseUrl,
|
|
1872
|
+
apiKey,
|
|
1873
|
+
model,
|
|
1874
|
+
opusModel,
|
|
1875
|
+
sonnetModel,
|
|
1876
|
+
haikuModel
|
|
1877
|
+
});
|
|
1878
|
+
const binaryName = toolId === "claude" ? "claude" : "opencode";
|
|
1879
|
+
const child = spawn(binaryName, toolArgs, {
|
|
1880
|
+
stdio: "inherit",
|
|
1881
|
+
env: {
|
|
1882
|
+
...process.env,
|
|
1883
|
+
...envToInject
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
child.on("error", (err) => {
|
|
1887
|
+
if (err.code === "ENOENT") {
|
|
1888
|
+
console.error(
|
|
1889
|
+
formatError(
|
|
1890
|
+
`Executable '${pc.bold(binaryName)}' not found in your PATH. Please install ${adapter.name} first.`
|
|
1891
|
+
)
|
|
1892
|
+
);
|
|
1893
|
+
} else {
|
|
1894
|
+
console.error(formatError(`Failed to run ${binaryName}: ${err.message}`));
|
|
1895
|
+
}
|
|
1896
|
+
process.exitCode = 1;
|
|
1897
|
+
});
|
|
1898
|
+
child.on("exit", (code) => {
|
|
1899
|
+
if (code !== null && code !== 0) {
|
|
1900
|
+
process.exitCode = code;
|
|
1901
|
+
}
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// src/commands/migrate.ts
|
|
1906
|
+
import fs6 from "node:fs";
|
|
1907
|
+
import path6 from "node:path";
|
|
1908
|
+
import * as p from "@clack/prompts";
|
|
1909
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
1910
|
+
|
|
1911
|
+
// ../../packages/db/dist/db.js
|
|
1912
|
+
import { DatabaseSync } from "node:sqlite";
|
|
1913
|
+
import path5 from "node:path";
|
|
1914
|
+
import fs5 from "node:fs";
|
|
1915
|
+
import os4 from "node:os";
|
|
1916
|
+
var SROUTER_DIR = path5.join(os4.homedir(), ".srouter");
|
|
1917
|
+
var DEFAULT_DB_PATH = path5.join(SROUTER_DIR, "srouter.db");
|
|
1918
|
+
var LEGACY_DB_LOCATIONS = [
|
|
1919
|
+
path5.resolve(process.cwd(), "apps/api/srouter.db"),
|
|
1920
|
+
path5.resolve(process.cwd(), "srouter.db")
|
|
1921
|
+
];
|
|
1922
|
+
function getDatabasePath() {
|
|
1923
|
+
if (process.env.DATABASE_PATH)
|
|
1924
|
+
return process.env.DATABASE_PATH;
|
|
1925
|
+
for (const legacyPath of LEGACY_DB_LOCATIONS) {
|
|
1926
|
+
if (fs5.existsSync(legacyPath))
|
|
1927
|
+
return legacyPath;
|
|
1928
|
+
}
|
|
1929
|
+
return DEFAULT_DB_PATH;
|
|
1930
|
+
}
|
|
1931
|
+
var dbPath = getDatabasePath();
|
|
1932
|
+
var dbDir = path5.dirname(dbPath);
|
|
1933
|
+
if (!fs5.existsSync(dbDir)) {
|
|
1934
|
+
fs5.mkdirSync(dbDir, { recursive: true });
|
|
1935
|
+
}
|
|
1936
|
+
var db = new DatabaseSync(dbPath);
|
|
1937
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
1938
|
+
function execWithRetry(sql, attempts = 5) {
|
|
1939
|
+
for (let i = 0; i < attempts; i++) {
|
|
1940
|
+
try {
|
|
1941
|
+
db.exec(sql);
|
|
1942
|
+
return;
|
|
1943
|
+
} catch (error) {
|
|
1944
|
+
const code = error.code;
|
|
1945
|
+
if (code === "SQLITE_BUSY" && i < attempts - 1) {
|
|
1946
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200 * (i + 1));
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
throw error;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
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;
|
|
1960
|
+
try {
|
|
1961
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${col.definition};`);
|
|
1962
|
+
existing.add(col.name);
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
const message = error.message || "";
|
|
1965
|
+
if (!message.includes("duplicate column name")) {
|
|
1966
|
+
throw error;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
function initDatabase() {
|
|
1972
|
+
db.exec(`
|
|
1973
|
+
CREATE TABLE IF NOT EXISTS providers (
|
|
1974
|
+
id TEXT PRIMARY KEY,
|
|
1975
|
+
provider_id TEXT NOT NULL,
|
|
1976
|
+
name TEXT NOT NULL,
|
|
1977
|
+
category TEXT NOT NULL,
|
|
1978
|
+
protocol TEXT NOT NULL,
|
|
1979
|
+
base_url TEXT,
|
|
1980
|
+
api_key TEXT,
|
|
1981
|
+
access_token TEXT,
|
|
1982
|
+
refresh_token TEXT,
|
|
1983
|
+
custom_headers TEXT,
|
|
1984
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1985
|
+
created_at INTEGER NOT NULL
|
|
1986
|
+
);
|
|
1987
|
+
`);
|
|
1988
|
+
ensureColumns("providers", [
|
|
1989
|
+
{ name: "refresh_token", definition: "refresh_token TEXT" },
|
|
1990
|
+
// Multi-account OAuth binding (e.g. Codex ChatGPT-Account-ID)
|
|
1991
|
+
{ name: "account_id", definition: "account_id TEXT" },
|
|
1992
|
+
// Provider-specific metadata (for example Kiro auth method/region/profile ARN)
|
|
1993
|
+
{ name: "provider_specific_data", definition: "provider_specific_data TEXT" },
|
|
1994
|
+
// Token expiry tracking
|
|
1995
|
+
{ name: "token_expires_at", definition: "token_expires_at INTEGER" },
|
|
1996
|
+
{ name: "last_refreshed_at", definition: "last_refreshed_at INTEGER" },
|
|
1997
|
+
// Claude OAuth organization binding
|
|
1998
|
+
{ name: "organization_id", definition: "organization_id TEXT" }
|
|
1999
|
+
]);
|
|
2000
|
+
db.exec(`
|
|
2001
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
2002
|
+
id TEXT PRIMARY KEY,
|
|
2003
|
+
key TEXT UNIQUE NOT NULL,
|
|
2004
|
+
name TEXT NOT NULL,
|
|
2005
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
2006
|
+
rate_limit INTEGER DEFAULT 0,
|
|
2007
|
+
quota_limit INTEGER DEFAULT 0,
|
|
2008
|
+
usage_tokens INTEGER DEFAULT 0,
|
|
2009
|
+
created_at INTEGER NOT NULL
|
|
2010
|
+
);
|
|
2011
|
+
`);
|
|
2012
|
+
db.exec(`
|
|
2013
|
+
CREATE TABLE IF NOT EXISTS request_logs (
|
|
2014
|
+
id TEXT PRIMARY KEY,
|
|
2015
|
+
api_key_id TEXT,
|
|
2016
|
+
provider_id TEXT NOT NULL,
|
|
2017
|
+
model TEXT NOT NULL,
|
|
2018
|
+
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2019
|
+
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2020
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
2021
|
+
status_code INTEGER NOT NULL,
|
|
2022
|
+
latency_ms INTEGER NOT NULL,
|
|
2023
|
+
created_at INTEGER NOT NULL
|
|
2024
|
+
);
|
|
2025
|
+
`);
|
|
2026
|
+
db.exec(`
|
|
2027
|
+
CREATE TABLE IF NOT EXISTS oauth_sessions (
|
|
2028
|
+
state TEXT PRIMARY KEY,
|
|
2029
|
+
code_verifier TEXT NOT NULL,
|
|
2030
|
+
client_id TEXT NOT NULL,
|
|
2031
|
+
redirect_uri TEXT NOT NULL,
|
|
2032
|
+
created_at INTEGER NOT NULL
|
|
2033
|
+
);
|
|
2034
|
+
`);
|
|
2035
|
+
ensureColumns("request_logs", [
|
|
2036
|
+
{ name: "cached_tokens", definition: "cached_tokens INTEGER NOT NULL DEFAULT 0" },
|
|
2037
|
+
{
|
|
2038
|
+
name: "cache_creation_tokens",
|
|
2039
|
+
definition: "cache_creation_tokens INTEGER NOT NULL DEFAULT 0"
|
|
2040
|
+
},
|
|
2041
|
+
{ name: "reasoning_tokens", definition: "reasoning_tokens INTEGER NOT NULL DEFAULT 0" },
|
|
2042
|
+
{ name: "estimated_cost", definition: "estimated_cost REAL NOT NULL DEFAULT 0" },
|
|
2043
|
+
{ name: "fallback_occurred", definition: "fallback_occurred INTEGER NOT NULL DEFAULT 0" },
|
|
2044
|
+
{ name: "fallback_path", definition: "fallback_path TEXT" },
|
|
2045
|
+
{ name: "fallback_reason", definition: "fallback_reason TEXT" },
|
|
2046
|
+
{ name: "resolved_model", definition: "resolved_model TEXT" }
|
|
2047
|
+
]);
|
|
2048
|
+
db.exec(`
|
|
2049
|
+
CREATE TABLE IF NOT EXISTS fallback_rules (
|
|
2050
|
+
id TEXT PRIMARY KEY,
|
|
2051
|
+
source_model TEXT NOT NULL,
|
|
2052
|
+
target_model TEXT NOT NULL,
|
|
2053
|
+
priority INTEGER NOT NULL DEFAULT 1,
|
|
2054
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
2055
|
+
trigger_on_status TEXT,
|
|
2056
|
+
max_retries INTEGER DEFAULT 1,
|
|
2057
|
+
created_at INTEGER NOT NULL
|
|
2058
|
+
);
|
|
2059
|
+
`);
|
|
2060
|
+
db.exec(`
|
|
2061
|
+
CREATE TABLE IF NOT EXISTS system_settings (
|
|
2062
|
+
key TEXT PRIMARY KEY,
|
|
2063
|
+
value TEXT NOT NULL
|
|
2064
|
+
);
|
|
2065
|
+
`);
|
|
2066
|
+
db.exec(`
|
|
2067
|
+
CREATE TABLE IF NOT EXISTS custom_models (
|
|
2068
|
+
provider_id TEXT NOT NULL,
|
|
2069
|
+
model_id TEXT NOT NULL,
|
|
2070
|
+
created_at INTEGER NOT NULL,
|
|
2071
|
+
PRIMARY KEY (provider_id, model_id)
|
|
2072
|
+
);
|
|
2073
|
+
`);
|
|
2074
|
+
}
|
|
2075
|
+
initDatabase();
|
|
2076
|
+
|
|
2077
|
+
// ../../packages/db/dist/row-utils.js
|
|
2078
|
+
function str(value, fallback = "") {
|
|
2079
|
+
return value === null || value === void 0 ? fallback : String(value);
|
|
2080
|
+
}
|
|
2081
|
+
function num(value, fallback = 0) {
|
|
2082
|
+
const n = Number(value);
|
|
2083
|
+
return Number.isFinite(n) ? n : fallback;
|
|
2084
|
+
}
|
|
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
|
+
var NineRouterDbLocations = [
|
|
2156
|
+
"/root/9router/srouter.db",
|
|
2157
|
+
"/root/project/9router/db/srouter.db",
|
|
2158
|
+
...LEGACY_DB_LOCATIONS
|
|
2159
|
+
];
|
|
2160
|
+
function fileKb(filePath) {
|
|
2161
|
+
return `${(fs6.statSync(filePath).size / 1024).toFixed(2)} KB`;
|
|
2162
|
+
}
|
|
2163
|
+
function ensureDirs() {
|
|
2164
|
+
fs6.mkdirSync(SROUTER_DIR, { recursive: true, mode: 448 });
|
|
2165
|
+
fs6.mkdirSync(BackupDir, { recursive: true, mode: 493 });
|
|
2166
|
+
}
|
|
2167
|
+
function backupDb(source, label) {
|
|
2168
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2169
|
+
const backupPath = path6.join(BackupDir, `${label}-backup-${timestamp}.db`);
|
|
2170
|
+
fs6.copyFileSync(source, backupPath);
|
|
2171
|
+
p.log.step(`Backed up to ${pc.dim(backupPath)}`);
|
|
2172
|
+
return backupPath;
|
|
2173
|
+
}
|
|
2174
|
+
function findDatabase(candidates, label) {
|
|
2175
|
+
for (const candidate of candidates) {
|
|
2176
|
+
const fullPath = path6.resolve(candidate);
|
|
2177
|
+
if (fs6.existsSync(fullPath)) {
|
|
2178
|
+
p.log.info(`Found ${label} database: ${pc.bold(fullPath)} (${fileKb(fullPath)})`);
|
|
2179
|
+
return fullPath;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
return null;
|
|
2183
|
+
}
|
|
2184
|
+
async function migrateDb(options) {
|
|
2185
|
+
p.intro("SRouter Database Migration");
|
|
2186
|
+
if (fs6.existsSync(TargetDbPath)) {
|
|
2187
|
+
p.log.warn(`Database already exists at ${TargetDbPath} (${fileKb(TargetDbPath)})`);
|
|
2188
|
+
p.outro("No migration needed \u2014 already using the new location.");
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
const source = options.source && fs6.existsSync(options.source) ? path6.resolve(options.source) : findDatabase(LEGACY_DB_LOCATIONS, "legacy");
|
|
2192
|
+
if (!source) {
|
|
2193
|
+
p.log.error("No existing database found.");
|
|
2194
|
+
p.outro(
|
|
2195
|
+
`Start SRouter and a fresh database will be created at ${TargetDbPath}, or pass --source /path/to/srouter.db.`
|
|
2196
|
+
);
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
ensureDirs();
|
|
2200
|
+
const proceed = options.yes || await p.confirm({ message: `Migrate database from ${source}?` }) === true;
|
|
2201
|
+
if (!proceed) {
|
|
2202
|
+
p.outro("Migration cancelled.");
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
try {
|
|
2206
|
+
backupDb(source, "srouter");
|
|
2207
|
+
fs6.copyFileSync(source, TargetDbPath);
|
|
2208
|
+
fs6.chmodSync(TargetDbPath, 384);
|
|
2209
|
+
p.log.success(`Database moved to ${TargetDbPath}`);
|
|
2210
|
+
p.outro("Restart SRouter to use the migrated database.");
|
|
2211
|
+
} catch (error) {
|
|
2212
|
+
p.log.error(formatError(`Migration failed: ${error.message}`));
|
|
2213
|
+
process.exitCode = 1;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
async function migrateNineRouter(options) {
|
|
2217
|
+
p.intro("9Router \u2192 SRouter Database Migration");
|
|
2218
|
+
const source = options.source && fs6.existsSync(options.source) ? path6.resolve(options.source) : findDatabase(NineRouterDbLocations, "9Router");
|
|
2219
|
+
if (!source) {
|
|
2220
|
+
p.log.error("No 9Router database found.");
|
|
2221
|
+
p.outro(
|
|
2222
|
+
"Pass the location explicitly: srouter migrate 9router --source /path/to/9router.db"
|
|
2223
|
+
);
|
|
2224
|
+
process.exitCode = 1;
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
const existingTarget = fs6.existsSync(TargetDbPath);
|
|
2228
|
+
let action = "copy";
|
|
2229
|
+
if (existingTarget) {
|
|
2230
|
+
p.log.warn(`Existing SRouter database found at ${TargetDbPath} (${fileKb(TargetDbPath)})`);
|
|
2231
|
+
const choice = await p.select({
|
|
2232
|
+
message: "How should the existing SRouter database be handled?",
|
|
2233
|
+
options: [
|
|
2234
|
+
{ value: "backup_and_replace", label: "Backup current, replace with 9Router data" },
|
|
2235
|
+
{ value: "merge", label: "Overwrite tables with 9Router data" },
|
|
2236
|
+
{ value: "abort", label: "Cancel migration" }
|
|
2237
|
+
]
|
|
2238
|
+
});
|
|
2239
|
+
if (choice === "abort" || p.isCancel(choice)) {
|
|
2240
|
+
p.outro("Migration cancelled. Your 9Router installation remains intact.");
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
if (choice === "merge" || choice === "backup_and_replace") {
|
|
2244
|
+
action = choice;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
const proceed = options.yes || await p.confirm({ message: "Proceed with migration?" }) === true;
|
|
2248
|
+
if (!proceed) {
|
|
2249
|
+
p.outro("Migration cancelled. No changes made.");
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
ensureDirs();
|
|
2253
|
+
const sourceBackup = backupDb(source, "9router");
|
|
2254
|
+
let targetBackup = null;
|
|
2255
|
+
if (action === "backup_and_replace") {
|
|
2256
|
+
targetBackup = backupDb(TargetDbPath, "srouter");
|
|
2257
|
+
}
|
|
2258
|
+
const s = p.spinner();
|
|
2259
|
+
try {
|
|
2260
|
+
s.start("Opening databases");
|
|
2261
|
+
const sourceDb = new DatabaseSync2(source);
|
|
2262
|
+
const targetDb = new DatabaseSync2(TargetDbPath);
|
|
2263
|
+
s.message("Reading source tables");
|
|
2264
|
+
const tables = sourceDb.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all().map((t) => t.name);
|
|
2265
|
+
let inserted = 0;
|
|
2266
|
+
let skipped = 0;
|
|
2267
|
+
for (const table of tables) {
|
|
2268
|
+
const rows = sourceDb.prepare(`SELECT * FROM "${table}"`).all();
|
|
2269
|
+
if (rows.length === 0) continue;
|
|
2270
|
+
if (action !== "copy") {
|
|
2271
|
+
try {
|
|
2272
|
+
targetDb.prepare(`DELETE FROM "${table}"`).run();
|
|
2273
|
+
} catch {
|
|
2274
|
+
p.log.warn(`Could not clear table ${table}, rows will be appended`);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
const columns = sourceDb.prepare(`PRAGMA table_info("${table}")`).all().map((col) => col.name);
|
|
2278
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
2279
|
+
const insert = targetDb.prepare(
|
|
2280
|
+
`INSERT INTO "${table}" (${columns.join(", ")}) VALUES (${placeholders})`
|
|
2281
|
+
);
|
|
2282
|
+
for (const row of rows) {
|
|
2283
|
+
try {
|
|
2284
|
+
insert.run(...columns.map((col) => row[col]));
|
|
2285
|
+
inserted++;
|
|
2286
|
+
} catch {
|
|
2287
|
+
skipped++;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
sourceDb.close();
|
|
2292
|
+
targetDb.close();
|
|
2293
|
+
s.stop("Migration complete");
|
|
2294
|
+
p.log.message(
|
|
2295
|
+
[
|
|
2296
|
+
`Tables migrated: ${tables.length}`,
|
|
2297
|
+
`Rows inserted: ${inserted}`,
|
|
2298
|
+
`Rows skipped: ${skipped}`,
|
|
2299
|
+
`Source: ${source}`,
|
|
2300
|
+
`Target: ${TargetDbPath}`
|
|
2301
|
+
].join("\n")
|
|
2302
|
+
);
|
|
2303
|
+
if (targetBackup) {
|
|
2304
|
+
p.log.info(`Old SRouter database backed up to ${targetBackup}`);
|
|
2305
|
+
}
|
|
2306
|
+
p.log.success(formatSuccess(`9Router backup saved to ${sourceBackup}`));
|
|
2307
|
+
p.outro("Your 9Router providers are now available in SRouter.");
|
|
2308
|
+
} catch (error) {
|
|
2309
|
+
s.stop(formatError("Migration failed"));
|
|
2310
|
+
p.log.error(error.message);
|
|
2311
|
+
p.log.info(formatInfo(`Restore from backup if needed: cp ${sourceBackup} ${source}`));
|
|
2312
|
+
process.exitCode = 1;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
async function migrateCommand(target, options) {
|
|
2316
|
+
switch (target) {
|
|
2317
|
+
case "db":
|
|
2318
|
+
return migrateDb(options);
|
|
2319
|
+
case "9router":
|
|
2320
|
+
return migrateNineRouter(options);
|
|
2321
|
+
default:
|
|
2322
|
+
p.log.error(formatError(`Unknown migration target: ${target}`));
|
|
2323
|
+
p.log.info(
|
|
2324
|
+
formatWarning("Available targets: db (legacy location), 9router (9Router import)")
|
|
2325
|
+
);
|
|
2326
|
+
process.exitCode = 1;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// src/index.ts
|
|
2331
|
+
function createCli() {
|
|
2332
|
+
const program = new Command();
|
|
2333
|
+
program.name("srouter").description(
|
|
2334
|
+
"CLI tool to connect, configure, and proxy AI coding tools with SRouter Gateway"
|
|
2335
|
+
).version(CLI_VERSION);
|
|
2336
|
+
program.command("setup").alias("config").description(
|
|
2337
|
+
"Interactive wizard to connect AI coding tools (Claude Code, OpenCode) to SRouter"
|
|
2338
|
+
).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>", "Default model ID to configure").option(
|
|
2339
|
+
"--opus-model <model>",
|
|
2340
|
+
"Default Opus tier model for Claude Code (ANTHROPIC_DEFAULT_OPUS_MODEL)"
|
|
2341
|
+
).option(
|
|
2342
|
+
"--sonnet-model <model>",
|
|
2343
|
+
"Default Sonnet tier model for Claude Code (ANTHROPIC_DEFAULT_SONNET_MODEL)"
|
|
2344
|
+
).option(
|
|
2345
|
+
"--haiku-model <model>",
|
|
2346
|
+
"Default Haiku tier model for Claude Code (ANTHROPIC_DEFAULT_HAIKU_MODEL)"
|
|
2347
|
+
).action(async (opts) => {
|
|
2348
|
+
await setupCommand(opts);
|
|
2349
|
+
});
|
|
2350
|
+
program.command("link <tool>").description("Configure a specific tool to use SRouter proxy (claude, opencode)").option("-u, --url <url>", "SRouter Gateway Base URL (e.g. http://localhost:3000/v1)").option("-k, --key <key>", "SRouter API Key").option("-m, --model <model>", "Model ID").option(
|
|
2351
|
+
"--opus-model <model>",
|
|
2352
|
+
"Opus tier model ID for Claude Code (ANTHROPIC_DEFAULT_OPUS_MODEL)"
|
|
2353
|
+
).option(
|
|
2354
|
+
"--sonnet-model <model>",
|
|
2355
|
+
"Sonnet tier model ID for Claude Code (ANTHROPIC_DEFAULT_SONNET_MODEL)"
|
|
2356
|
+
).option(
|
|
2357
|
+
"--haiku-model <model>",
|
|
2358
|
+
"Haiku tier model ID for Claude Code (ANTHROPIC_DEFAULT_HAIKU_MODEL)"
|
|
2359
|
+
).option("--dry-run", "Preview configuration changes without writing files").action(async (tool, opts) => {
|
|
2360
|
+
await linkCommand(tool, opts);
|
|
2361
|
+
});
|
|
2362
|
+
program.command("unlink <tool>").description("Restore original configuration for a tool from backup").action(async (tool) => {
|
|
2363
|
+
await unlinkCommand(tool);
|
|
2364
|
+
});
|
|
2365
|
+
program.command("sync [tool]").description(
|
|
2366
|
+
"Fetch latest models from SRouter Gateway and refresh tool configs (OpenCode, etc.)"
|
|
2367
|
+
).option("-u, --url <url>", "SRouter Gateway Base URL (e.g. http://localhost:3000/v1)").option("-k, --key <key>", "SRouter API Key").action(async (tool, opts) => {
|
|
2368
|
+
await syncCommand(tool, opts);
|
|
2369
|
+
});
|
|
2370
|
+
program.command("status").alias("doctor").description("Inspect SRouter gateway connectivity, active models, and tool link status").option("-u, --url <url>", "SRouter Gateway Base URL (e.g. http://localhost:3000/v1)").option("-k, --key <key>", "SRouter API Key").action(async (opts) => {
|
|
2371
|
+
await statusCommand(opts);
|
|
2372
|
+
});
|
|
2373
|
+
program.command("env [tool]").description("Print shell export commands for SRouter proxy environment variables").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(
|
|
2374
|
+
"--opus-model <model>",
|
|
2375
|
+
"Opus tier model ID for Claude Code (ANTHROPIC_DEFAULT_OPUS_MODEL)"
|
|
2376
|
+
).option(
|
|
2377
|
+
"--sonnet-model <model>",
|
|
2378
|
+
"Sonnet tier model ID for Claude Code (ANTHROPIC_DEFAULT_SONNET_MODEL)"
|
|
2379
|
+
).option(
|
|
2380
|
+
"--haiku-model <model>",
|
|
2381
|
+
"Haiku tier model ID for Claude Code (ANTHROPIC_DEFAULT_HAIKU_MODEL)"
|
|
2382
|
+
).option("--fish", "Generate fish shell export syntax (shorthand for --shell fish)").option("--shell <shell>", "Target shell syntax (bash, zsh, fish, powershell, cmd)").action(async (tool, opts) => {
|
|
2383
|
+
await envCommand(tool, opts);
|
|
2384
|
+
});
|
|
2385
|
+
program.command("run <tool> [args...]").description("Run a tool CLI directly with SRouter proxy environment variables injected").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(
|
|
2386
|
+
"--opus-model <model>",
|
|
2387
|
+
"Opus tier model ID for Claude Code (ANTHROPIC_DEFAULT_OPUS_MODEL)"
|
|
2388
|
+
).option(
|
|
2389
|
+
"--sonnet-model <model>",
|
|
2390
|
+
"Sonnet tier model ID for Claude Code (ANTHROPIC_DEFAULT_SONNET_MODEL)"
|
|
2391
|
+
).option(
|
|
2392
|
+
"--haiku-model <model>",
|
|
2393
|
+
"Haiku tier model ID for Claude Code (ANTHROPIC_DEFAULT_HAIKU_MODEL)"
|
|
2394
|
+
).allowUnknownOption().action(async (tool, args, opts) => {
|
|
2395
|
+
await runCommand(tool, args, opts);
|
|
2396
|
+
});
|
|
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) => {
|
|
2398
|
+
await migrateCommand(target, opts);
|
|
2399
|
+
});
|
|
2400
|
+
return program;
|
|
2401
|
+
}
|
|
2402
|
+
if (process.argv[1]?.endsWith("srouter.js") || process.argv[1]?.endsWith("index.ts") || process.argv[1]?.endsWith("index.js")) {
|
|
2403
|
+
const program = createCli();
|
|
2404
|
+
program.parse(process.argv);
|
|
2405
|
+
}
|
|
2406
|
+
export {
|
|
2407
|
+
createCli
|
|
2408
|
+
};
|
|
2409
|
+
//# sourceMappingURL=index.js.map
|