@peterwangze/claude-trigger-router 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -224,6 +224,40 @@ function inferTransformer(protocol) {
224
224
  }
225
225
  return void 0;
226
226
  }
227
+ function inferCompatibilityProfile(item, modelInterface) {
228
+ if (modelInterface === "anthropic") {
229
+ return "anthropic-native";
230
+ }
231
+ const api = getModelApi(item);
232
+ const vendorHint = item.metadata?.vendor_hint?.trim().toLowerCase();
233
+ if (vendorHint === "openrouter" || api.includes("openrouter.ai")) {
234
+ return "openrouter-like";
235
+ }
236
+ if (vendorHint === "qianfan" || vendorHint === "qianfan-coding" || api.includes("qianfan.baidubce.com/v2/coding")) {
237
+ return "qianfan-coding";
238
+ }
239
+ if (vendorHint === "minimax" || vendorHint === "minimax-chatcompletion-v2" || api.includes("/v1/text/chatcompletion_v2")) {
240
+ return "minimax-chatcompletion-v2";
241
+ }
242
+ return "generic-openai-compatible";
243
+ }
244
+ function getDispatchFormatForProfile(modelInterface, compatibilityProfile) {
245
+ if (modelInterface === "anthropic") {
246
+ return "anthropic_messages";
247
+ }
248
+ switch (compatibilityProfile) {
249
+ case "openrouter-like":
250
+ case "qianfan-coding":
251
+ case "minimax-chatcompletion-v2":
252
+ return "anthropic_messages";
253
+ case "anthropic-native":
254
+ return "anthropic_messages";
255
+ case "generic-openai-compatible":
256
+ return "anthropic_messages";
257
+ default:
258
+ return "anthropic_messages";
259
+ }
260
+ }
227
261
  function buildCompiledCapabilities(item, modelInterface) {
228
262
  const reasoningSupported = item.metadata?.supports_reasoning !== false;
229
263
  return {
@@ -255,12 +289,15 @@ function buildModelRegistry(config) {
255
289
  const modelMap2 = config.Models.reduce((result, rawItem) => {
256
290
  const item = normalizeModelEndpointConfig(rawItem);
257
291
  const modelInterface = getModelInterface(item) || "openai";
292
+ const compatibilityProfile = inferCompatibilityProfile(item, modelInterface);
258
293
  result[item.id] = {
259
294
  id: item.id,
260
295
  providerName: `model__${item.id}`,
261
296
  modelName: item.model,
262
297
  interface: modelInterface,
263
298
  protocol: modelInterface,
299
+ compatibilityProfile,
300
+ dispatchFormat: getDispatchFormatForProfile(modelInterface, compatibilityProfile),
264
301
  thinking: item.thinking,
265
302
  capabilities: buildCompiledCapabilities(item, modelInterface),
266
303
  source: "models"
@@ -275,12 +312,23 @@ function buildModelRegistry(config) {
275
312
  const providers = config.Providers ?? [];
276
313
  const modelMap = providers.reduce((result, provider) => {
277
314
  for (const model of provider.models ?? []) {
315
+ const compatibilityProfile = inferCompatibilityProfile(
316
+ {
317
+ api_base_url: provider.api_base_url,
318
+ metadata: {
319
+ vendor_hint: provider.transformer?.use?.[0]
320
+ }
321
+ },
322
+ "openai"
323
+ );
278
324
  result[`${provider.name},${model}`] = {
279
325
  id: `${provider.name},${model}`,
280
326
  providerName: provider.name,
281
327
  modelName: model,
282
328
  interface: "openai",
283
329
  protocol: "openai",
330
+ compatibilityProfile,
331
+ dispatchFormat: getDispatchFormatForProfile("openai", compatibilityProfile),
284
332
  capabilities: {
285
333
  thinking: {
286
334
  supported: true
@@ -452,6 +500,9 @@ async function initDir() {
452
500
  if (!(0, import_fs.existsSync)(CONFIG_DIR)) {
453
501
  (0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
454
502
  }
503
+ if (!(0, import_fs.existsSync)(HOME_DIR)) {
504
+ (0, import_fs.mkdirSync)(HOME_DIR, { recursive: true });
505
+ }
455
506
  }
456
507
  async function loadYamlConfig(path) {
457
508
  if (!(0, import_fs.existsSync)(path)) {
@@ -930,6 +981,31 @@ async function probeServiceHealth(port, timeoutMs = 500) {
930
981
  return false;
931
982
  }
932
983
  }
984
+ async function isTcpPortOccupied(port, timeoutMs = 500) {
985
+ return new Promise((resolve) => {
986
+ const socket = new import_net.Socket();
987
+ let settled = false;
988
+ const finish = (value) => {
989
+ if (settled) {
990
+ return;
991
+ }
992
+ settled = true;
993
+ socket.destroy();
994
+ resolve(value);
995
+ };
996
+ socket.setTimeout(timeoutMs);
997
+ socket.once("connect", () => finish(true));
998
+ socket.once("timeout", () => finish(false));
999
+ socket.once("error", (error) => {
1000
+ if (error.code === "ECONNREFUSED") {
1001
+ finish(false);
1002
+ return;
1003
+ }
1004
+ finish(false);
1005
+ });
1006
+ socket.connect(port, "127.0.0.1");
1007
+ });
1008
+ }
933
1009
  async function waitForService(port, timeoutMs = 5e3) {
934
1010
  const start = Date.now();
935
1011
  while (Date.now() - start < timeoutMs) {
@@ -940,10 +1016,11 @@ async function waitForService(port, timeoutMs = 5e3) {
940
1016
  }
941
1017
  return false;
942
1018
  }
943
- var SERVICE_NAME, SERVICE_HEALTH_PATH;
1019
+ var import_net, SERVICE_NAME, SERVICE_HEALTH_PATH;
944
1020
  var init_service_health = __esm({
945
1021
  "src/service-health.ts"() {
946
1022
  "use strict";
1023
+ import_net = require("net");
947
1024
  SERVICE_NAME = "claude-trigger-router";
948
1025
  SERVICE_HEALTH_PATH = "/api/health";
949
1026
  }
@@ -957,25 +1034,25 @@ var init_types = __esm({
957
1034
  });
958
1035
 
959
1036
  // src/governance/trace.ts
960
- function createGovernanceTrace(input2 = {}) {
1037
+ function createGovernanceTrace(input3 = {}) {
961
1038
  return {
962
- requestId: input2.requestId ?? (0, import_crypto.randomUUID)(),
963
- sessionKey: input2.sessionKey,
964
- initialModel: input2.initialModel,
965
- finalModel: input2.finalModel,
966
- routeReason: input2.routeReason ? [...input2.routeReason] : [],
967
- stickyHit: input2.stickyHit ?? false,
968
- alignmentUsed: input2.alignmentUsed ?? false,
969
- semanticIntent: input2.semanticIntent,
970
- cascadeTriggered: input2.cascadeTriggered ?? false,
971
- cascadeEvidence: input2.cascadeEvidence ? [...input2.cascadeEvidence] : [],
972
- cascadeNextModel: input2.cascadeNextModel,
973
- shadowChecked: input2.shadowChecked ?? false,
974
- verificationResult: input2.verificationResult,
975
- latencyMs: input2.latencyMs,
976
- estimatedCost: input2.estimatedCost,
977
- startedAt: input2.startedAt ?? Date.now(),
978
- completedAt: input2.completedAt
1039
+ requestId: input3.requestId ?? (0, import_crypto.randomUUID)(),
1040
+ sessionKey: input3.sessionKey,
1041
+ initialModel: input3.initialModel,
1042
+ finalModel: input3.finalModel,
1043
+ routeReason: input3.routeReason ? [...input3.routeReason] : [],
1044
+ stickyHit: input3.stickyHit ?? false,
1045
+ alignmentUsed: input3.alignmentUsed ?? false,
1046
+ semanticIntent: input3.semanticIntent,
1047
+ cascadeTriggered: input3.cascadeTriggered ?? false,
1048
+ cascadeEvidence: input3.cascadeEvidence ? [...input3.cascadeEvidence] : [],
1049
+ cascadeNextModel: input3.cascadeNextModel,
1050
+ shadowChecked: input3.shadowChecked ?? false,
1051
+ verificationResult: input3.verificationResult,
1052
+ latencyMs: input3.latencyMs,
1053
+ estimatedCost: input3.estimatedCost,
1054
+ startedAt: input3.startedAt ?? Date.now(),
1055
+ completedAt: input3.completedAt
979
1056
  };
980
1057
  }
981
1058
  function appendTraceReason(trace, reason) {
@@ -1300,16 +1377,16 @@ function normalizeContentParts(content) {
1300
1377
  }
1301
1378
  });
1302
1379
  }
1303
- function createMessageIR(input2) {
1304
- const system = typeof input2.system === "string" ? [input2.system] : Array.isArray(input2.system) ? input2.system.flatMap((item) => item?.type === "text" && typeof item.text === "string" ? [item.text] : []) : [];
1305
- const messages = Array.isArray(input2.messages) ? input2.messages.filter((item) => item?.role).map((item) => ({
1380
+ function createMessageIR(input3) {
1381
+ const system = typeof input3.system === "string" ? [input3.system] : Array.isArray(input3.system) ? input3.system.flatMap((item) => item?.type === "text" && typeof item.text === "string" ? [item.text] : []) : [];
1382
+ const messages = Array.isArray(input3.messages) ? input3.messages.filter((item) => item?.role).map((item) => ({
1306
1383
  role: item.role,
1307
1384
  parts: normalizeContentParts(item.content)
1308
1385
  })) : [];
1309
- const thinking = input2.thinking ? {
1310
- enabled: input2.thinking?.type === "enabled" || input2.thinking?.enabled === true,
1311
- effort: input2.thinking?.effort,
1312
- budget_tokens: input2.thinking?.budget_tokens
1386
+ const thinking = input3.thinking ? {
1387
+ enabled: input3.thinking?.type === "enabled" || input3.thinking?.enabled === true,
1388
+ effort: input3.thinking?.effort,
1389
+ budget_tokens: input3.thinking?.budget_tokens
1313
1390
  } : void 0;
1314
1391
  return {
1315
1392
  system,
@@ -1361,34 +1438,38 @@ function toAnthropicContent(parts) {
1361
1438
  }
1362
1439
  });
1363
1440
  }
1364
- function toAnthropicMessagesRequest(input2) {
1441
+ function toAnthropicMessagesRequest(input3) {
1365
1442
  const body = {
1366
- model: input2.model,
1367
- messages: input2.ir.messages.map((message) => ({
1443
+ model: input3.model,
1444
+ messages: input3.ir.messages.map((message) => ({
1368
1445
  role: message.role,
1369
1446
  content: toAnthropicContent(message.parts)
1370
1447
  }))
1371
1448
  };
1372
- if (input2.max_tokens !== void 0) {
1373
- body.max_tokens = input2.max_tokens;
1449
+ if (input3.max_tokens !== void 0) {
1450
+ body.max_tokens = input3.max_tokens;
1374
1451
  }
1375
- if (input2.stream !== void 0) {
1376
- body.stream = input2.stream;
1452
+ if (input3.stream !== void 0) {
1453
+ body.stream = input3.stream;
1377
1454
  }
1378
- if (input2.metadata) {
1379
- body.metadata = input2.metadata;
1455
+ if (input3.metadata) {
1456
+ body.metadata = input3.metadata;
1380
1457
  }
1381
- if (input2.tools) {
1382
- body.tools = input2.tools;
1458
+ if (input3.tools) {
1459
+ body.tools = input3.tools.map((tool) => ({
1460
+ name: tool?.name ?? tool?.function?.name,
1461
+ description: tool?.description ?? tool?.function?.description,
1462
+ input_schema: tool?.input_schema ?? tool?.function?.parameters
1463
+ }));
1383
1464
  }
1384
- if (input2.ir.system.length) {
1385
- body.system = input2.ir.system.map((text) => ({ type: "text", text }));
1465
+ if (input3.ir.system.length) {
1466
+ body.system = input3.ir.system.map((text) => ({ type: "text", text }));
1386
1467
  }
1387
- if (input2.ir.options?.thinking?.enabled) {
1468
+ if (input3.ir.options?.thinking?.enabled) {
1388
1469
  body.thinking = {
1389
1470
  type: "enabled",
1390
- ...input2.ir.options.thinking.effort ? { effort: input2.ir.options.thinking.effort } : {},
1391
- ...input2.ir.options.thinking.budget_tokens ? { budget_tokens: input2.ir.options.thinking.budget_tokens } : {}
1471
+ ...input3.ir.options.thinking.effort ? { effort: input3.ir.options.thinking.effort } : {},
1472
+ ...input3.ir.options.thinking.budget_tokens ? { budget_tokens: input3.ir.options.thinking.budget_tokens } : {}
1392
1473
  };
1393
1474
  }
1394
1475
  return body;
@@ -2121,17 +2202,17 @@ var init_SSEParser_transform = __esm({
2121
2202
 
2122
2203
  // src/governance/stream-response-governance.ts
2123
2204
  function serializeEvent(event2) {
2124
- let output2 = "";
2205
+ let output3 = "";
2125
2206
  if (event2.event) {
2126
- output2 += `event: ${event2.event}
2207
+ output3 += `event: ${event2.event}
2127
2208
  `;
2128
2209
  }
2129
2210
  if (event2.data !== void 0) {
2130
- output2 += `data: ${typeof event2.data === "string" ? event2.data : JSON.stringify(event2.data)}
2211
+ output3 += `data: ${typeof event2.data === "string" ? event2.data : JSON.stringify(event2.data)}
2131
2212
  `;
2132
2213
  }
2133
- output2 += "\n";
2134
- return new TextEncoder().encode(output2);
2214
+ output3 += "\n";
2215
+ return new TextEncoder().encode(output3);
2135
2216
  }
2136
2217
  async function collectSSE(stream) {
2137
2218
  const parser = new SSEParserTransform();
@@ -2241,17 +2322,17 @@ var init_stream_response_governance = __esm({
2241
2322
  });
2242
2323
 
2243
2324
  // src/governance/metrics.ts
2244
- function normalizeAnomalyThresholds(input2) {
2325
+ function normalizeAnomalyThresholds(input3) {
2245
2326
  return {
2246
- minSampleSize: input2?.minSampleSize ?? DEFAULT_ANOMALY_THRESHOLDS.minSampleSize,
2247
- cascadeWarnRate: input2?.cascadeWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.cascadeWarnRate,
2248
- cascadeCriticalRate: input2?.cascadeCriticalRate ?? DEFAULT_ANOMALY_THRESHOLDS.cascadeCriticalRate,
2249
- shadowWarnRate: input2?.shadowWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.shadowWarnRate,
2250
- shadowCriticalRate: input2?.shadowCriticalRate ?? DEFAULT_ANOMALY_THRESHOLDS.shadowCriticalRate,
2251
- latencyWarnMs: input2?.latencyWarnMs ?? DEFAULT_ANOMALY_THRESHOLDS.latencyWarnMs,
2252
- latencyCriticalMs: input2?.latencyCriticalMs ?? DEFAULT_ANOMALY_THRESHOLDS.latencyCriticalMs,
2253
- spikeWarnRate: input2?.spikeWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.spikeWarnRate,
2254
- spikeDeltaRate: input2?.spikeDeltaRate ?? DEFAULT_ANOMALY_THRESHOLDS.spikeDeltaRate
2327
+ minSampleSize: input3?.minSampleSize ?? DEFAULT_ANOMALY_THRESHOLDS.minSampleSize,
2328
+ cascadeWarnRate: input3?.cascadeWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.cascadeWarnRate,
2329
+ cascadeCriticalRate: input3?.cascadeCriticalRate ?? DEFAULT_ANOMALY_THRESHOLDS.cascadeCriticalRate,
2330
+ shadowWarnRate: input3?.shadowWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.shadowWarnRate,
2331
+ shadowCriticalRate: input3?.shadowCriticalRate ?? DEFAULT_ANOMALY_THRESHOLDS.shadowCriticalRate,
2332
+ latencyWarnMs: input3?.latencyWarnMs ?? DEFAULT_ANOMALY_THRESHOLDS.latencyWarnMs,
2333
+ latencyCriticalMs: input3?.latencyCriticalMs ?? DEFAULT_ANOMALY_THRESHOLDS.latencyCriticalMs,
2334
+ spikeWarnRate: input3?.spikeWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.spikeWarnRate,
2335
+ spikeDeltaRate: input3?.spikeDeltaRate ?? DEFAULT_ANOMALY_THRESHOLDS.spikeDeltaRate
2255
2336
  };
2256
2337
  }
2257
2338
  function rate(count, total) {
@@ -3262,7 +3343,7 @@ var init_server = __esm({
3262
3343
  server.app.post("/api/restart", async (req, reply) => {
3263
3344
  reply.send({ success: true, message: "Service restart initiated" });
3264
3345
  setTimeout(() => {
3265
- const { spawn: spawn2 } = require("child_process");
3346
+ const { spawn: spawn3 } = require("child_process");
3266
3347
  const { join: join8 } = require("path");
3267
3348
  const cliPath = join8(__dirname, "cli.js");
3268
3349
  const currentPort = config.initialConfig?.PORT;
@@ -3270,7 +3351,7 @@ var init_server = __esm({
3270
3351
  if (currentPort) {
3271
3352
  restartArgs.push("--port", String(currentPort));
3272
3353
  }
3273
- spawn2(process.execPath, restartArgs, {
3354
+ spawn3(process.execPath, restartArgs, {
3274
3355
  detached: true,
3275
3356
  stdio: "ignore"
3276
3357
  }).unref();
@@ -3576,18 +3657,18 @@ var init_SSESerializer_transform = __esm({
3576
3657
  constructor() {
3577
3658
  const transformStream = new TransformStream({
3578
3659
  transform: (event2, controller) => {
3579
- let output2 = "";
3660
+ let output3 = "";
3580
3661
  if (event2.event) {
3581
- output2 += `event: ${event2.event}
3662
+ output3 += `event: ${event2.event}
3582
3663
  `;
3583
3664
  }
3584
3665
  if (event2.data) {
3585
3666
  const dataStr = typeof event2.data === "string" ? event2.data : JSON.stringify(event2.data);
3586
- output2 += `data: ${dataStr}
3667
+ output3 += `data: ${dataStr}
3587
3668
  `;
3588
3669
  }
3589
- output2 += "\n";
3590
- controller.enqueue(new TextEncoder().encode(output2));
3670
+ output3 += "\n";
3671
+ controller.enqueue(new TextEncoder().encode(output3));
3591
3672
  }
3592
3673
  });
3593
3674
  this.readable = transformStream.readable;
@@ -4377,6 +4458,7 @@ Important:
4377
4458
  method: "POST",
4378
4459
  headers: {
4379
4460
  "Content-Type": "application/json",
4461
+ "x-ctr-smart-router": "1",
4380
4462
  ...apiKey ? { "x-api-key": apiKey } : {}
4381
4463
  },
4382
4464
  body: JSON.stringify(
@@ -4889,6 +4971,15 @@ function toOpenAIToolResultMessages(parts) {
4889
4971
  content: typeof part.content === "string" ? part.content : JSON.stringify(part.content)
4890
4972
  }));
4891
4973
  }
4974
+ function getToolName(tool) {
4975
+ return tool?.name ?? tool?.function?.name;
4976
+ }
4977
+ function getToolDescription(tool) {
4978
+ return tool?.description ?? tool?.function?.description;
4979
+ }
4980
+ function getToolInputSchema(tool) {
4981
+ return tool?.input_schema ?? tool?.function?.parameters;
4982
+ }
4892
4983
  function toOpenAITools(tools) {
4893
4984
  if (!Array.isArray(tools) || !tools.length) {
4894
4985
  return void 0;
@@ -4896,9 +4987,9 @@ function toOpenAITools(tools) {
4896
4987
  return tools.map((tool) => ({
4897
4988
  type: "function",
4898
4989
  function: {
4899
- name: tool.name,
4900
- description: tool.description,
4901
- parameters: tool.input_schema
4990
+ name: getToolName(tool),
4991
+ description: getToolDescription(tool),
4992
+ parameters: getToolInputSchema(tool)
4902
4993
  }
4903
4994
  }));
4904
4995
  }
@@ -4925,10 +5016,10 @@ function toOpenAIToolChoice(toolChoice) {
4925
5016
  }
4926
5017
  return toolChoice;
4927
5018
  }
4928
- function toOpenAIChatRequest(input2) {
5019
+ function toOpenAIChatRequest(input3) {
4929
5020
  const messages = [
4930
- ...input2.ir.system.map((text) => ({ role: "system", content: text })),
4931
- ...input2.ir.messages.flatMap((message) => {
5021
+ ...input3.ir.system.map((text) => ({ role: "system", content: text })),
5022
+ ...input3.ir.messages.flatMap((message) => {
4932
5023
  const content = toOpenAIContent(message.parts);
4933
5024
  const toolCalls = message.role === "assistant" ? toOpenAIToolCalls(message.parts) : [];
4934
5025
  const toolResults = toOpenAIToolResultMessages(message.parts);
@@ -4947,26 +5038,26 @@ function toOpenAIChatRequest(input2) {
4947
5038
  })
4948
5039
  ];
4949
5040
  const body = {
4950
- model: input2.model,
5041
+ model: input3.model,
4951
5042
  messages
4952
5043
  };
4953
- if (input2.max_completion_tokens !== void 0) {
4954
- body.max_completion_tokens = input2.max_completion_tokens;
5044
+ if (input3.max_completion_tokens !== void 0) {
5045
+ body.max_completion_tokens = input3.max_completion_tokens;
4955
5046
  }
4956
- if (input2.stream !== void 0) {
4957
- body.stream = input2.stream;
5047
+ if (input3.stream !== void 0) {
5048
+ body.stream = input3.stream;
4958
5049
  }
4959
- const tools = toOpenAITools(input2.tools);
5050
+ const tools = toOpenAITools(input3.tools);
4960
5051
  if (tools) {
4961
5052
  body.tools = tools;
4962
5053
  }
4963
- const toolChoice = toOpenAIToolChoice(input2.tool_choice);
5054
+ const toolChoice = toOpenAIToolChoice(input3.tool_choice);
4964
5055
  if (toolChoice !== void 0) {
4965
5056
  body.tool_choice = toolChoice;
4966
5057
  }
4967
- if (input2.ir.options?.thinking?.enabled) {
5058
+ if (input3.ir.options?.thinking?.enabled) {
4968
5059
  body.reasoning = {
4969
- ...input2.ir.options.thinking.effort ? { effort: input2.ir.options.thinking.effort } : {}
5060
+ ...input3.ir.options.thinking.effort ? { effort: input3.ir.options.thinking.effort } : {}
4970
5061
  };
4971
5062
  }
4972
5063
  return body;
@@ -4991,19 +5082,19 @@ function stringifyFallbackContent(value) {
4991
5082
  return String(value);
4992
5083
  }
4993
5084
  }
4994
- function applyCapabilityFallbacks(input2) {
5085
+ function applyCapabilityFallbacks(input3) {
4995
5086
  const diagnostics = [];
4996
- const nextRequest = { ...input2.request };
5087
+ const nextRequest = { ...input3.request };
4997
5088
  const nextIR = {
4998
- ...input2.ir,
4999
- system: [...input2.ir.system],
5000
- messages: input2.ir.messages.map((message) => ({
5089
+ ...input3.ir,
5090
+ system: [...input3.ir.system],
5091
+ messages: input3.ir.messages.map((message) => ({
5001
5092
  ...message,
5002
5093
  parts: message.parts.map((part) => ({ ...part }))
5003
5094
  })),
5004
- options: input2.ir.options ? { ...input2.ir.options } : void 0
5095
+ options: input3.ir.options ? { ...input3.ir.options } : void 0
5005
5096
  };
5006
- if (input2.capabilities?.thinking.supported === false && nextIR.options?.thinking) {
5097
+ if (input3.capabilities?.thinking.supported === false && nextIR.options?.thinking) {
5007
5098
  diagnostics.push("thinking_ignored");
5008
5099
  delete nextIR.options.thinking;
5009
5100
  delete nextRequest.thinking;
@@ -5014,7 +5105,7 @@ function applyCapabilityFallbacks(input2) {
5014
5105
  const hasImageParts = nextIR.messages.some(
5015
5106
  (message) => message.parts.some((part) => part.type === "image")
5016
5107
  );
5017
- if (input2.capabilities?.images === false && hasImageParts) {
5108
+ if (input3.capabilities?.images === false && hasImageParts) {
5018
5109
  diagnostics.push("images_text_fallback");
5019
5110
  nextIR.messages = nextIR.messages.map((message) => ({
5020
5111
  ...message,
@@ -5034,7 +5125,7 @@ function applyCapabilityFallbacks(input2) {
5034
5125
  const hasToolParts = nextIR.messages.some(
5035
5126
  (message) => message.parts.some((part) => part.type === "tool_call" || part.type === "tool_result")
5036
5127
  );
5037
- if (input2.capabilities?.tools === false && (Array.isArray(nextRequest.tools) && nextRequest.tools.length || hasToolParts)) {
5128
+ if (input3.capabilities?.tools === false && (Array.isArray(nextRequest.tools) && nextRequest.tools.length || hasToolParts)) {
5038
5129
  diagnostics.push("tools_text_fallback");
5039
5130
  delete nextRequest.tools;
5040
5131
  delete nextRequest.tool_choice;
@@ -5080,53 +5171,58 @@ function omitRequestFields(body) {
5080
5171
  } = body;
5081
5172
  return rest;
5082
5173
  }
5083
- function buildUpstreamRequestFromIR(input2) {
5174
+ function buildProviderDispatchRequestFromIR(input3) {
5084
5175
  const fallback = applyCapabilityFallbacks({
5085
- ir: input2.ir,
5086
- request: input2.request,
5087
- capabilities: input2.capabilities
5176
+ ir: input3.ir,
5177
+ request: input3.request,
5178
+ capabilities: input3.capabilities
5088
5179
  });
5089
5180
  const passthrough = omitRequestFields(fallback.request);
5090
- if (input2.interface === "anthropic") {
5181
+ const dispatchFormat = getDispatchFormatForProfile(input3.interface, input3.compatibilityProfile);
5182
+ if (dispatchFormat === "openai_chat") {
5091
5183
  return {
5184
+ dispatchFormat,
5092
5185
  diagnostics: fallback.diagnostics,
5093
5186
  ...passthrough,
5094
- ...toAnthropicMessagesRequest({
5095
- model: input2.model,
5096
- max_tokens: fallback.request.max_tokens,
5187
+ ...toOpenAIChatRequest({
5188
+ model: input3.model,
5189
+ max_completion_tokens: fallback.request.max_tokens ?? fallback.request.max_completion_tokens,
5097
5190
  stream: fallback.request.stream,
5098
- metadata: fallback.request.metadata,
5099
5191
  tools: fallback.request.tools,
5192
+ tool_choice: fallback.request.tool_choice,
5100
5193
  ir: fallback.ir
5101
5194
  })
5102
5195
  };
5103
5196
  }
5104
5197
  return {
5198
+ dispatchFormat,
5105
5199
  diagnostics: fallback.diagnostics,
5106
5200
  ...passthrough,
5107
- ...toOpenAIChatRequest({
5108
- model: input2.model,
5109
- max_completion_tokens: fallback.request.max_tokens ?? fallback.request.max_completion_tokens,
5201
+ ...toAnthropicMessagesRequest({
5202
+ model: input3.model,
5203
+ max_tokens: fallback.request.max_tokens,
5110
5204
  stream: fallback.request.stream,
5205
+ metadata: fallback.request.metadata,
5111
5206
  tools: fallback.request.tools,
5112
- tool_choice: fallback.request.tool_choice,
5113
5207
  ir: fallback.ir
5114
5208
  })
5115
5209
  };
5116
5210
  }
5117
- function buildUpstreamRequest(input2) {
5118
- const ir = createMessageIR(input2.request);
5119
- const { diagnostics, ...body } = buildUpstreamRequestFromIR({
5120
- model: input2.model,
5121
- interface: input2.interface,
5122
- request: input2.request,
5211
+ function buildProviderDispatchRequest(input3) {
5212
+ const ir = createMessageIR(input3.request);
5213
+ const { diagnostics, dispatchFormat, ...body } = buildProviderDispatchRequestFromIR({
5214
+ model: input3.model,
5215
+ interface: input3.interface,
5216
+ compatibilityProfile: input3.compatibilityProfile,
5217
+ request: input3.request,
5123
5218
  ir,
5124
- capabilities: input2.capabilities
5219
+ capabilities: input3.capabilities
5125
5220
  });
5126
5221
  return {
5127
5222
  ir,
5128
5223
  body,
5129
- diagnostics
5224
+ diagnostics,
5225
+ dispatchFormat
5130
5226
  };
5131
5227
  }
5132
5228
  var init_protocols = __esm({
@@ -5135,6 +5231,7 @@ var init_protocols = __esm({
5135
5231
  init_message_ir();
5136
5232
  init_anthropic();
5137
5233
  init_openai();
5234
+ init_compile();
5138
5235
  }
5139
5236
  });
5140
5237
 
@@ -5202,7 +5299,7 @@ async function run(options = {}) {
5202
5299
  const hour = pad(date.getHours());
5203
5300
  const minute = pad(date.getMinutes());
5204
5301
  const seconds = pad(date.getSeconds());
5205
- return `./logs/ctr-${month}${day}${hour}${minute}${seconds}${index ? `_${index}` : ""}.log`;
5302
+ return `ctr-${month}${day}${hour}${minute}${seconds}${index ? `_${index}` : ""}.log`;
5206
5303
  };
5207
5304
  const loggerConfig = config.LOG !== false ? {
5208
5305
  level: config.LOG_LEVEL || "debug",
@@ -5213,10 +5310,11 @@ async function run(options = {}) {
5213
5310
  compress: "gzip"
5214
5311
  })
5215
5312
  } : false;
5313
+ const registry = buildModelRegistry(config);
5216
5314
  const server = createServer({
5217
- jsonPath: CONFIG_FILE,
5315
+ useJsonFile: false,
5218
5316
  initialConfig: {
5219
- providers: config.Providers,
5317
+ providers: registry.providers,
5220
5318
  HOST,
5221
5319
  PORT: servicePort,
5222
5320
  LOG_FILE: (0, import_path5.join)(
@@ -5252,9 +5350,10 @@ async function run(options = {}) {
5252
5350
  initialModel: req.body?.model
5253
5351
  });
5254
5352
  appendTraceReason(req.governanceTrace, "request_received");
5255
- const triggerResult = await triggerRouter.route(req);
5353
+ const bypassTriggerRouter = req.headers["x-ctr-smart-router"] === "1";
5354
+ const triggerResult = bypassTriggerRouter ? { matched: false, confidence: 0, analysisTime: 0 } : await triggerRouter.route(req);
5256
5355
  req.triggerResult = triggerResult;
5257
- if (triggerResult.matched && triggerResult.model) {
5356
+ if (!bypassTriggerRouter && triggerResult.matched && triggerResult.model) {
5258
5357
  const previousSessionState = req.sessionId ? sessionStateStore.get(req.sessionId) : void 0;
5259
5358
  const previousModel = previousSessionState?.lastSuccessfulModel;
5260
5359
  const alignmentConfig = config.Governance?.sticky?.alignment;
@@ -5319,9 +5418,10 @@ async function run(options = {}) {
5319
5418
  const compiledModel = getCompiledModelRef(config, req.body?.model);
5320
5419
  if (compiledModel?.interface && req.body?.messages) {
5321
5420
  const originalBody = cloneRequestBody(req.body);
5322
- const upstream = buildUpstreamRequest({
5421
+ const upstream = buildProviderDispatchRequest({
5323
5422
  model: compiledModel.modelName,
5324
5423
  interface: compiledModel.interface,
5424
+ compatibilityProfile: compiledModel.compatibilityProfile,
5325
5425
  request: originalBody,
5326
5426
  capabilities: compiledModel.capabilities
5327
5427
  });
@@ -5508,7 +5608,7 @@ async function run(options = {}) {
5508
5608
  event.emit("onSend", req, reply, payload);
5509
5609
  return payload;
5510
5610
  });
5511
- server.start();
5611
+ await server.start();
5512
5612
  }
5513
5613
  var import_fs5, import_promises2, import_os2, import_path5, import_json5, import_node_events, import_rotating_file_stream, event;
5514
5614
  var init_index = __esm({
@@ -5542,32 +5642,32 @@ var init_index = __esm({
5542
5642
  });
5543
5643
 
5544
5644
  // src/setup/service.ts
5545
- function decideServiceAction(input2) {
5546
- if (input2.detectedService.kind === "non_self_occupied") {
5645
+ function decideServiceAction(input3) {
5646
+ if (input3.detectedService.kind === "non_self_occupied") {
5547
5647
  throw new Error("target port is occupied by another service");
5548
5648
  }
5549
- if (input2.detectedService.kind === "none") {
5649
+ if (input3.detectedService.kind === "none") {
5550
5650
  return { kind: "start" };
5551
5651
  }
5552
- if (input2.detectedService.kind === "self_unhealthy") {
5652
+ if (input3.detectedService.kind === "self_unhealthy") {
5553
5653
  return { kind: "restart" };
5554
5654
  }
5555
- if (input2.configChanged && input2.detectedService.kind === "self_healthy") {
5556
- return input2.reloadSupported ? { kind: "reload" } : { kind: "restart" };
5655
+ if (input3.configChanged && input3.detectedService.kind === "self_healthy") {
5656
+ return input3.reloadSupported ? { kind: "reload" } : { kind: "restart" };
5557
5657
  }
5558
5658
  return { kind: "reuse" };
5559
5659
  }
5560
- async function applyServiceAction(input2) {
5561
- if (input2.action.kind === "start") {
5562
- await input2.executeStart();
5660
+ async function applyServiceAction(input3) {
5661
+ if (input3.action.kind === "start") {
5662
+ await input3.executeStart();
5563
5663
  }
5564
- if (input2.action.kind === "reload") {
5565
- await input2.executeReload();
5664
+ if (input3.action.kind === "reload") {
5665
+ await input3.executeReload();
5566
5666
  }
5567
- if (input2.action.kind === "restart") {
5568
- await input2.executeRestart();
5667
+ if (input3.action.kind === "restart") {
5668
+ await input3.executeRestart();
5569
5669
  }
5570
- const healthy = await input2.verifyHealth();
5670
+ const healthy = await input3.verifyHealth();
5571
5671
  if (!healthy) {
5572
5672
  throw new Error("service health check failed");
5573
5673
  }
@@ -5617,9 +5717,12 @@ function inferProtocolFromApiBaseUrl(apiBaseUrl) {
5617
5717
  }
5618
5718
  return "openai";
5619
5719
  }
5720
+ function normalizeSegment(value) {
5721
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
5722
+ }
5620
5723
  function toModelId(name, model, index) {
5621
- const normalizedName = name.trim() || `provider_${index + 1}`;
5622
- const normalizedModel = model.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
5724
+ const normalizedName = normalizeSegment(name) || `provider_${index + 1}`;
5725
+ const normalizedModel = normalizeSegment(model);
5623
5726
  return normalizedModel ? `${normalizedName}_${normalizedModel}` : normalizedName;
5624
5727
  }
5625
5728
  function createEmptyDraft() {
@@ -5634,66 +5737,176 @@ function createNonMigratableResult() {
5634
5737
  draft: createEmptyDraft(),
5635
5738
  skippedFields: [],
5636
5739
  needsCompletion: true,
5637
- missingFields: ["defaultModel", "apiKey"]
5740
+ missingFields: ["defaultModel", "apiKey", "apiBaseUrl"]
5638
5741
  };
5639
5742
  }
5743
+ function inferVendorHintFromLegacyProvider(provider) {
5744
+ const transformerUse = Array.isArray(provider.transformer?.use) ? provider.transformer.use.map((item) => String(item).trim().toLowerCase()) : [];
5745
+ const normalizedName = (provider.name ?? "").trim().toLowerCase();
5746
+ if (transformerUse.includes("openrouter")) {
5747
+ return "openrouter";
5748
+ }
5749
+ if (normalizedName.includes("qianfan")) {
5750
+ return "qianfan-coding";
5751
+ }
5752
+ if (normalizedName.includes("minimax")) {
5753
+ return "minimax-chatcompletion-v2";
5754
+ }
5755
+ return void 0;
5756
+ }
5640
5757
  function isLegacyProviderInput(value) {
5641
5758
  return typeof value === "object" && value !== null;
5642
5759
  }
5643
- function migrateLegacyConfig(input2) {
5644
- if (!Array.isArray(input2.providers)) {
5645
- return createNonMigratableResult();
5760
+ function isLegacyRouterInput(value) {
5761
+ return typeof value === "object" && value !== null;
5762
+ }
5763
+ function pushUnique(target, value) {
5764
+ if (!target.includes(value)) {
5765
+ target.push(value);
5646
5766
  }
5647
- if (!input2.providers.every(isLegacyProviderInput)) {
5648
- return createNonMigratableResult();
5767
+ }
5768
+ function normalizeLegacyConfig(input3) {
5769
+ const lowerProviders = Array.isArray(input3.providers) && input3.providers.every(isLegacyProviderInput) ? input3.providers : null;
5770
+ const upperProviders = Array.isArray(input3.Providers) && input3.Providers.every(isLegacyProviderInput) ? input3.Providers : null;
5771
+ const providerKey = lowerProviders && lowerProviders.length > 0 ? "providers" : upperProviders && upperProviders.length > 0 ? "Providers" : lowerProviders ? "providers" : upperProviders ? "Providers" : null;
5772
+ if (!providerKey) {
5773
+ return null;
5774
+ }
5775
+ const rawProviders = providerKey === "providers" ? lowerProviders : upperProviders;
5776
+ if (!rawProviders) {
5777
+ return null;
5649
5778
  }
5650
5779
  const skippedFields = [];
5651
- const providers = input2.providers.map((provider, index) => {
5780
+ const alternateProviderKey = providerKey === "providers" ? "Providers" : "providers";
5781
+ const alternateDefaultKey = providerKey === "providers" ? "Router" : "default";
5782
+ const alternateDefaultValue = providerKey === "providers" ? input3.Router : input3.default;
5783
+ const alternateProviders = providerKey === "providers" ? upperProviders : lowerProviders;
5784
+ if (alternateProviders !== null) {
5785
+ pushUnique(skippedFields, alternateProviderKey);
5786
+ }
5787
+ if (alternateDefaultValue !== void 0) {
5788
+ pushUnique(skippedFields, alternateDefaultKey);
5789
+ }
5790
+ const consumedTopLevelFields = /* @__PURE__ */ new Set([providerKey]);
5791
+ const providers = rawProviders.map((provider, index) => {
5652
5792
  if (provider.transformer !== void 0) {
5653
- skippedFields.push(`providers[${index}].transformer`);
5793
+ pushUnique(skippedFields, `${providerKey}[${index}].transformer`);
5794
+ }
5795
+ if (provider.headers !== void 0) {
5796
+ pushUnique(skippedFields, `${providerKey}[${index}].headers`);
5654
5797
  }
5655
5798
  return {
5656
5799
  name: provider.name ?? "",
5657
5800
  api_base_url: provider.api_base_url,
5658
5801
  api_key: provider.api_key ?? "",
5659
- models: Array.isArray(provider.models) ? provider.models : []
5802
+ models: Array.isArray(provider.models) ? provider.models : [],
5803
+ vendor_hint: inferVendorHintFromLegacyProvider(provider)
5660
5804
  };
5661
5805
  });
5662
- const models = providers.flatMap(
5806
+ let defaultRoute;
5807
+ if (providerKey === "providers") {
5808
+ consumedTopLevelFields.add("default");
5809
+ defaultRoute = typeof input3.default === "string" ? input3.default : void 0;
5810
+ } else {
5811
+ consumedTopLevelFields.add("Router");
5812
+ if (isLegacyRouterInput(input3.Router)) {
5813
+ defaultRoute = typeof input3.Router.default === "string" ? input3.Router.default : void 0;
5814
+ if (input3.Router.background !== void 0) {
5815
+ pushUnique(skippedFields, "Router.background");
5816
+ }
5817
+ if (input3.Router.think !== void 0) {
5818
+ pushUnique(skippedFields, "Router.think");
5819
+ }
5820
+ if (input3.Router.longContext !== void 0) {
5821
+ pushUnique(skippedFields, "Router.longContext");
5822
+ }
5823
+ if (input3.Router.longContextThreshold !== void 0) {
5824
+ pushUnique(skippedFields, "Router.longContextThreshold");
5825
+ }
5826
+ }
5827
+ }
5828
+ for (const key of Object.keys(input3)) {
5829
+ if (consumedTopLevelFields.has(key)) {
5830
+ continue;
5831
+ }
5832
+ pushUnique(skippedFields, key);
5833
+ }
5834
+ return {
5835
+ providers,
5836
+ defaultRoute,
5837
+ skippedFields
5838
+ };
5839
+ }
5840
+ function migrateLegacyConfig(input3) {
5841
+ const normalized = normalizeLegacyConfig(input3);
5842
+ if (!normalized) {
5843
+ return createNonMigratableResult();
5844
+ }
5845
+ const rawEntries = normalized.providers.flatMap(
5663
5846
  (provider, providerIndex) => (provider.models.length ? provider.models : [""]).map((model) => ({
5664
- id: toModelId(provider.name, model, providerIndex),
5847
+ candidateId: toModelId(provider.name, model, providerIndex),
5665
5848
  api: provider.api_base_url,
5666
5849
  api_base_url: provider.api_base_url,
5667
5850
  key: provider.api_key,
5668
5851
  api_key: provider.api_key,
5669
5852
  interface: inferProtocolFromApiBaseUrl(provider.api_base_url),
5670
5853
  protocol: inferProtocolFromApiBaseUrl(provider.api_base_url),
5671
- model
5672
- }))
5673
- ).filter((item) => item.model);
5674
- if (input2.trigger_router !== void 0) {
5675
- skippedFields.push("trigger_router");
5676
- }
5677
- const hasDefaultModel = typeof input2.default === "string" && input2.default.length > 0;
5678
- const defaultModelId = hasDefaultModel ? (() => {
5679
- const [providerName, modelName] = String(input2.default).split(",");
5680
- return models.find((item) => item.id === toModelId(providerName, modelName, 0) || item.id.startsWith(`${providerName}_`) && item.model === modelName)?.id;
5854
+ model,
5855
+ providerName: provider.name,
5856
+ vendorHint: provider.vendor_hint
5857
+ })).filter((item) => item.model)
5858
+ );
5859
+ const seenIds = /* @__PURE__ */ new Map();
5860
+ const routeLookup = /* @__PURE__ */ new Map();
5861
+ const models = rawEntries.map((entry) => {
5862
+ const count = seenIds.get(entry.candidateId) ?? 0;
5863
+ seenIds.set(entry.candidateId, count + 1);
5864
+ const finalId = count === 0 ? entry.candidateId : `${entry.candidateId}_${count + 1}`;
5865
+ routeLookup.set(`${entry.providerName.trim()},${entry.model}`, finalId);
5866
+ return {
5867
+ id: finalId,
5868
+ api: entry.api,
5869
+ api_base_url: entry.api_base_url,
5870
+ key: entry.key,
5871
+ api_key: entry.api_key,
5872
+ interface: entry.interface,
5873
+ protocol: entry.protocol,
5874
+ model: entry.model,
5875
+ metadata: entry.vendorHint ? {
5876
+ vendor_hint: entry.vendorHint
5877
+ } : void 0
5878
+ };
5879
+ });
5880
+ const hasLegacyDefaultRoute = typeof normalized.defaultRoute === "string" && normalized.defaultRoute.length > 0;
5881
+ const defaultModelId = hasLegacyDefaultRoute ? (() => {
5882
+ const [rawProviderName, rawModelName] = String(normalized.defaultRoute).split(",");
5883
+ const providerName = (rawProviderName ?? "").trim();
5884
+ const modelName = (rawModelName ?? "").trim();
5885
+ const fromLookup = routeLookup.get(`${providerName},${modelName}`);
5886
+ if (fromLookup) return fromLookup;
5887
+ return models.find(
5888
+ (item) => item.id === toModelId(providerName, modelName, 0) || item.id.startsWith(`${normalizeSegment(providerName)}_`) && item.model === modelName
5889
+ )?.id;
5681
5890
  })() : void 0;
5682
- const hasMissingApiKey = providers.some((provider) => provider.api_key.length === 0);
5891
+ const hasMissingApiKey = normalized.providers.some((provider) => provider.api_key.length === 0);
5892
+ const hasMissingApiBaseUrl = normalized.providers.some((provider) => (provider.api_base_url?.trim() ?? "").length === 0);
5683
5893
  const missingFields = [];
5684
- if (!hasDefaultModel) {
5894
+ if (!defaultModelId) {
5685
5895
  missingFields.push("defaultModel");
5686
5896
  }
5687
5897
  if (hasMissingApiKey) {
5688
5898
  missingFields.push("apiKey");
5689
5899
  }
5900
+ if (hasMissingApiBaseUrl) {
5901
+ missingFields.push("apiBaseUrl");
5902
+ }
5690
5903
  return {
5691
5904
  draft: {
5692
5905
  Providers: [],
5693
5906
  Models: models,
5694
- Router: hasDefaultModel && defaultModelId ? { default: defaultModelId } : {}
5907
+ Router: defaultModelId ? { default: defaultModelId } : {}
5695
5908
  },
5696
- skippedFields,
5909
+ skippedFields: normalized.skippedFields,
5697
5910
  needsCompletion: missingFields.length > 0,
5698
5911
  missingFields
5699
5912
  };
@@ -5758,11 +5971,12 @@ function getProviderPreset2(key) {
5758
5971
  api: preset.api,
5759
5972
  api_base_url: preset.api_base_url,
5760
5973
  interface: preset.interface,
5761
- protocol: preset.protocol
5974
+ protocol: preset.protocol,
5975
+ default_thinking: preset.default_thinking
5762
5976
  };
5763
5977
  }
5764
- function buildMinimalConfig(input2) {
5765
- const providers = input2.providers;
5978
+ function buildMinimalConfig(input3) {
5979
+ const providers = input3.providers;
5766
5980
  const models = providers.map((p) => {
5767
5981
  const preset = p.preset ? getProviderPreset2(p.preset) : void 0;
5768
5982
  const modelDraft = {
@@ -5782,15 +5996,15 @@ function buildMinimalConfig(input2) {
5782
5996
  }
5783
5997
  return modelDraft;
5784
5998
  });
5785
- let defaultModel = input2.defaultModel?.trim();
5786
- if (input2.defaultModel && input2.defaultModel.includes(",")) {
5787
- const [providerName, modelName] = input2.defaultModel.split(",");
5999
+ let defaultModel = input3.defaultModel?.trim();
6000
+ if (input3.defaultModel && input3.defaultModel.includes(",")) {
6001
+ const [providerName, modelName] = input3.defaultModel.split(",");
5788
6002
  const matched = models.find((item) => item.id === providerName && item.model === modelName);
5789
6003
  if (matched) {
5790
6004
  defaultModel = matched.id;
5791
6005
  }
5792
6006
  }
5793
- if (input2.defaultModel === void 0 && models.length > 0) {
6007
+ if (input3.defaultModel === void 0 && models.length > 0) {
5794
6008
  const firstModelId = models[0].id;
5795
6009
  if (firstModelId && models[0].model) {
5796
6010
  defaultModel = firstModelId;
@@ -5804,31 +6018,68 @@ function buildMinimalConfig(input2) {
5804
6018
  Router: defaultModel ? { default: defaultModel } : {}
5805
6019
  };
5806
6020
  }
6021
+ function buildUsableMinimalTemplateConfig() {
6022
+ const openRouterPreset = getProviderPreset("openrouter");
6023
+ const modelId = openRouterPreset?.suggested_id ?? "sonnet";
6024
+ const modelName = openRouterPreset?.default_model ?? "anthropic/claude-sonnet-4";
6025
+ const thinking = openRouterPreset?.default_thinking ?? "auto";
6026
+ const draft = buildMinimalConfig({
6027
+ providers: [
6028
+ {
6029
+ name: "openrouter",
6030
+ model_id: modelId,
6031
+ preset: "openrouter",
6032
+ api_key: "sk-xxx",
6033
+ models: [modelName]
6034
+ }
6035
+ ],
6036
+ defaultModel: modelId
6037
+ });
6038
+ const primaryModel = draft.Models?.[0];
6039
+ return {
6040
+ HOST: DEFAULT_CONFIG2.HOST,
6041
+ PORT: DEFAULT_CONFIG2.PORT,
6042
+ LOG: DEFAULT_CONFIG2.LOG,
6043
+ LOG_LEVEL: DEFAULT_CONFIG2.LOG_LEVEL,
6044
+ Models: primaryModel ? [
6045
+ {
6046
+ id: primaryModel.id,
6047
+ api: primaryModel.api,
6048
+ key: primaryModel.key,
6049
+ interface: primaryModel.interface,
6050
+ model: primaryModel.model,
6051
+ thinking
6052
+ }
6053
+ ] : [],
6054
+ Router: draft.Router
6055
+ };
6056
+ }
5807
6057
  var init_templates = __esm({
5808
6058
  "src/setup/templates.ts"() {
5809
6059
  "use strict";
6060
+ init_constants();
5810
6061
  init_provider_presets();
5811
6062
  }
5812
6063
  });
5813
6064
 
5814
6065
  // src/setup/persist.ts
5815
- async function persistSetupConfig(input2) {
5816
- const errors = input2.validateConfig(input2.config);
6066
+ async function persistSetupConfig(input3) {
6067
+ const errors = input3.validateConfig(input3.config);
5817
6068
  if (errors.length > 0) {
5818
6069
  throw new Error("config validation failed");
5819
6070
  }
5820
6071
  let backupPath;
5821
- if (input2.hasExistingConfig) {
5822
- const createdBackupPath = await input2.backupCurrentConfig();
6072
+ if (input3.hasExistingConfig) {
6073
+ const createdBackupPath = await input3.backupCurrentConfig();
5823
6074
  if (!createdBackupPath) {
5824
6075
  throw new Error("failed to back up existing config");
5825
6076
  }
5826
6077
  backupPath = createdBackupPath;
5827
6078
  }
5828
- await input2.writeConfig(input2.config);
6079
+ await input3.writeConfig(input3.config);
5829
6080
  return {
5830
6081
  configChanged: true,
5831
- configPath: input2.currentConfigPath,
6082
+ configPath: input3.currentConfigPath,
5832
6083
  backupPath
5833
6084
  };
5834
6085
  }
@@ -5877,8 +6128,8 @@ function ensureLegacyFlow(detection, legacyConfigAction) {
5877
6128
  function invalidAction() {
5878
6129
  return invalidCurrentAction();
5879
6130
  }
5880
- function decideSetupBranch(input2) {
5881
- const { detection, currentConfigAction, legacyConfigAction } = input2;
6131
+ function decideSetupBranch(input3) {
6132
+ const { detection, currentConfigAction, legacyConfigAction } = input3;
5882
6133
  if (currentConfigAction === "cancel") {
5883
6134
  ensureNoLegacyAction(legacyConfigAction);
5884
6135
  return { kind: "cancelled" };
@@ -5931,6 +6182,28 @@ function getTargetConfigPath(detection) {
5931
6182
  }
5932
6183
  return CONFIG_FILE;
5933
6184
  }
6185
+ function getLegacyProviderCount(input3) {
6186
+ if (typeof input3 !== "object" || input3 === null) {
6187
+ return 0;
6188
+ }
6189
+ const legacyConfig = input3;
6190
+ if (Array.isArray(legacyConfig.providers)) {
6191
+ return legacyConfig.providers.length;
6192
+ }
6193
+ if (Array.isArray(legacyConfig.Providers)) {
6194
+ return legacyConfig.Providers.length;
6195
+ }
6196
+ return 0;
6197
+ }
6198
+ function getMigratedModelCount(draft) {
6199
+ if (Array.isArray(draft.Models)) {
6200
+ return draft.Models.length;
6201
+ }
6202
+ if (Array.isArray(draft.Providers)) {
6203
+ return draft.Providers.reduce((total, provider) => total + (provider.models?.length ?? 0), 0);
6204
+ }
6205
+ return 0;
6206
+ }
5934
6207
  async function runSetup(deps) {
5935
6208
  const detection = await deps.detectSetupEnvironment();
5936
6209
  const currentConfigAction = await deps.chooseCurrentConfigAction({
@@ -5938,7 +6211,7 @@ async function runSetup(deps) {
5938
6211
  legacyConfig: detection.legacyConfig
5939
6212
  });
5940
6213
  let legacyConfigAction;
5941
- if (currentConfigAction === "create" || currentConfigAction === "overwrite") {
6214
+ if (currentConfigAction === "create" || currentConfigAction === "overwrite" || currentConfigAction === "fresh") {
5942
6215
  if (detection.legacyConfig.kind === "found" || detection.legacyConfig.kind === "read_error") {
5943
6216
  legacyConfigAction = await deps.chooseLegacyConfigAction({
5944
6217
  legacyConfig: detection.legacyConfig
@@ -6012,6 +6285,16 @@ async function runSetup(deps) {
6012
6285
  throw new Error("migrate_legacy requires legacy config");
6013
6286
  }
6014
6287
  const migrated = deps.migrateLegacyConfig(detection.legacyConfig.config);
6288
+ deps.io.info(`\u5DF2\u8BC6\u522B\u65E7\u914D\u7F6E\u4E2D\u7684 ${getLegacyProviderCount(detection.legacyConfig.config)} \u4E2A provider\u3002`);
6289
+ deps.io.info(`\u5DF2\u4ECE\u65E7\u914D\u7F6E\u8FC1\u79FB ${getMigratedModelCount(migrated.draft)} \u4E2A\u6A21\u578B\u3002`);
6290
+ if (migrated.draft.Router.default) {
6291
+ deps.io.info(`\u8FC1\u79FB\u540E\u7684\u9ED8\u8BA4\u6A21\u578B\uFF1A${migrated.draft.Router.default}`);
6292
+ } else {
6293
+ deps.io.info("\u8FC1\u79FB\u540E\u7684\u9ED8\u8BA4\u6A21\u578B\u4ECD\u9700\u8865\u5168\u3002");
6294
+ }
6295
+ if (migrated.skippedFields.length > 0) {
6296
+ deps.io.info(`\u4EE5\u4E0B\u65E7\u5B57\u6BB5\u672A\u81EA\u52A8\u8FC1\u79FB\uFF1A${migrated.skippedFields.join(", ")}`);
6297
+ }
6015
6298
  let finalDraft = migrated.draft;
6016
6299
  if (migrated.needsCompletion) {
6017
6300
  finalDraft = await deps.completeDraft({
@@ -6046,6 +6329,96 @@ var init_setup = __esm({
6046
6329
 
6047
6330
  // src/setup/index.ts
6048
6331
  function createConsoleIO() {
6332
+ if (process.env.CTR_SETUP_FORCE_SCRIPTED_INPUT === "1") {
6333
+ const scriptedInput = (0, import_fs6.readFileSync)(0, "utf-8");
6334
+ const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter((item) => item.length > 0);
6335
+ let cursor = 0;
6336
+ const nextAnswer = async () => answers[cursor++] ?? "";
6337
+ return {
6338
+ async choose(message, options) {
6339
+ import_process.stdout.write(`${message}
6340
+ `);
6341
+ options.forEach((option, index) => {
6342
+ import_process.stdout.write(` ${index + 1}. ${option}
6343
+ `);
6344
+ });
6345
+ const answer = await nextAnswer();
6346
+ const pickedIndex = Number(answer);
6347
+ if (Number.isInteger(pickedIndex) && pickedIndex >= 1 && pickedIndex <= options.length) {
6348
+ return options[pickedIndex - 1];
6349
+ }
6350
+ const matched = options.find((option) => option === answer);
6351
+ if (matched) {
6352
+ return matched;
6353
+ }
6354
+ throw new Error(`invalid scripted answer for "${message}": ${answer || "<empty>"}`);
6355
+ },
6356
+ async input(message, defaultValue) {
6357
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
6358
+ import_process.stdout.write(`${message}${suffix}: `);
6359
+ const answer = await nextAnswer();
6360
+ return answer || defaultValue || "";
6361
+ },
6362
+ info(message) {
6363
+ import_process.stdout.write(`${message}
6364
+ `);
6365
+ },
6366
+ close() {
6367
+ }
6368
+ };
6369
+ }
6370
+ if (!import_process.stdin.isTTY) {
6371
+ let loaded = false;
6372
+ let answers = [];
6373
+ let cursor = 0;
6374
+ const loadAnswers = async () => {
6375
+ if (loaded) {
6376
+ return;
6377
+ }
6378
+ loaded = true;
6379
+ const chunks = [];
6380
+ for await (const chunk of import_process.stdin) {
6381
+ chunks.push(String(chunk));
6382
+ }
6383
+ answers = chunks.join("").split(/\r?\n/).map((item) => item.trim()).filter((item) => item.length > 0);
6384
+ };
6385
+ const nextAnswer = async () => {
6386
+ await loadAnswers();
6387
+ return answers[cursor++] ?? "";
6388
+ };
6389
+ return {
6390
+ async choose(message, options) {
6391
+ import_process.stdout.write(`${message}
6392
+ `);
6393
+ options.forEach((option, index) => {
6394
+ import_process.stdout.write(` ${index + 1}. ${option}
6395
+ `);
6396
+ });
6397
+ const answer = await nextAnswer();
6398
+ const pickedIndex = Number(answer);
6399
+ if (Number.isInteger(pickedIndex) && pickedIndex >= 1 && pickedIndex <= options.length) {
6400
+ return options[pickedIndex - 1];
6401
+ }
6402
+ const matched = options.find((option) => option === answer);
6403
+ if (matched) {
6404
+ return matched;
6405
+ }
6406
+ throw new Error(`invalid scripted answer for "${message}": ${answer || "<empty>"}`);
6407
+ },
6408
+ async input(message, defaultValue) {
6409
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
6410
+ import_process.stdout.write(`${message}${suffix}: `);
6411
+ const answer = await nextAnswer();
6412
+ return answer || defaultValue || "";
6413
+ },
6414
+ info(message) {
6415
+ import_process.stdout.write(`${message}
6416
+ `);
6417
+ },
6418
+ close() {
6419
+ }
6420
+ };
6421
+ }
6049
6422
  const rl = (0, import_promises3.createInterface)({ input: import_process.stdin, output: import_process.stdout });
6050
6423
  const ask = async (message) => {
6051
6424
  const answer = await rl.question(message);
@@ -6080,6 +6453,9 @@ function createConsoleIO() {
6080
6453
  info(message) {
6081
6454
  import_process.stdout.write(`${message}
6082
6455
  `);
6456
+ },
6457
+ close() {
6458
+ rl.close();
6083
6459
  }
6084
6460
  };
6085
6461
  }
@@ -6090,14 +6466,79 @@ function readStructuredConfigFile(filePath) {
6090
6466
  }
6091
6467
  return import_js_yaml.default.load(content);
6092
6468
  }
6469
+ function getCurrentRuntimeFields() {
6470
+ const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
6471
+ const currentPath = candidates.find((filePath) => (0, import_fs6.existsSync)(filePath));
6472
+ if (!currentPath) {
6473
+ return {};
6474
+ }
6475
+ try {
6476
+ const config = readStructuredConfigFile(currentPath);
6477
+ if (!config || typeof config !== "object") {
6478
+ return {};
6479
+ }
6480
+ const fields = {};
6481
+ for (const key of ["HOST", "PORT", "LOG", "LOG_LEVEL", "API_TIMEOUT_MS"]) {
6482
+ if (config[key] !== void 0) {
6483
+ fields[key] = config[key];
6484
+ }
6485
+ }
6486
+ return fields;
6487
+ } catch {
6488
+ return {};
6489
+ }
6490
+ }
6491
+ function getConfiguredPortFromCurrentFiles() {
6492
+ const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
6493
+ const currentPath = candidates.find((filePath) => (0, import_fs6.existsSync)(filePath));
6494
+ if (!currentPath) {
6495
+ return DEFAULT_CONFIG2.PORT;
6496
+ }
6497
+ try {
6498
+ const config = readStructuredConfigFile(currentPath);
6499
+ if (config && typeof config.PORT === "number" && Number.isFinite(config.PORT) && config.PORT > 0) {
6500
+ return config.PORT;
6501
+ }
6502
+ } catch {
6503
+ }
6504
+ return DEFAULT_CONFIG2.PORT;
6505
+ }
6506
+ async function getAvailablePort() {
6507
+ const server = (0, import_net2.createServer)();
6508
+ try {
6509
+ return await new Promise((resolve, reject) => {
6510
+ server.once("error", reject);
6511
+ server.listen(0, "127.0.0.1", () => {
6512
+ const address = server.address();
6513
+ if (!address || typeof address === "string") {
6514
+ reject(new Error("failed to resolve available port"));
6515
+ return;
6516
+ }
6517
+ resolve(address.port);
6518
+ });
6519
+ });
6520
+ } finally {
6521
+ if (server.listening) {
6522
+ await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
6523
+ }
6524
+ }
6525
+ }
6526
+ function readLegacyConfigFile(filePath) {
6527
+ const content = (0, import_fs6.readFileSync)(filePath, "utf-8");
6528
+ if (filePath.endsWith(".json")) {
6529
+ return import_json52.default.parse(content);
6530
+ }
6531
+ return import_js_yaml.default.load(content);
6532
+ }
6093
6533
  async function readLegacyConfig(deps = {}) {
6094
6534
  const baseHomeDir = deps.homeDir || (0, import_os3.homedir)();
6095
6535
  const exists = deps.exists || import_fs6.existsSync;
6096
- const readConfig = deps.readConfig || readStructuredConfigFile;
6536
+ const readConfig = deps.readConfig || readLegacyConfigFile;
6097
6537
  const overridePath = process.env.CTR_SETUP_LEGACY_CONFIG_PATH;
6098
6538
  const candidatePaths = overridePath ? [overridePath] : [
6099
6539
  (0, import_path6.join)(baseHomeDir, ".ccr", "config.yaml"),
6100
- (0, import_path6.join)(baseHomeDir, ".claude-code-router", "config.yaml")
6540
+ (0, import_path6.join)(baseHomeDir, ".claude-code-router", "config.yaml"),
6541
+ (0, import_path6.join)(baseHomeDir, ".claude-code-router", "config.json")
6101
6542
  ];
6102
6543
  const legacyPath = candidatePaths.find((filePath) => exists(filePath));
6103
6544
  if (!legacyPath) {
@@ -6140,16 +6581,30 @@ async function readCurrentConfig() {
6140
6581
  }
6141
6582
  }
6142
6583
  async function probeService() {
6143
- const healthy = await waitForService(DEFAULT_CONFIG2.PORT, 500);
6144
- return healthy ? { kind: "self_healthy", port: DEFAULT_CONFIG2.PORT } : { kind: "none" };
6584
+ const port = getConfiguredPortFromCurrentFiles();
6585
+ const healthy = await waitForService(port, 500);
6586
+ if (healthy) {
6587
+ return isServiceRunning() ? { kind: "self_healthy", port } : { kind: "non_self_occupied", port };
6588
+ }
6589
+ const occupied = await isTcpPortOccupied(port, 500);
6590
+ if (!occupied) {
6591
+ return { kind: "none" };
6592
+ }
6593
+ return isServiceRunning() ? { kind: "self_unhealthy", port } : { kind: "non_self_occupied", port };
6145
6594
  }
6146
6595
  async function enterClaudeCode() {
6596
+ if (process.env.CTR_SETUP_SKIP_ENTER_CODE === "1") {
6597
+ return;
6598
+ }
6147
6599
  const cliModule = await Promise.resolve().then(() => (init_cli(), cli_exports));
6148
6600
  await cliModule.runClaudeCode();
6149
6601
  }
6602
+ function shouldAutoEnterClaudeCodeAfterSetup() {
6603
+ return process.env.CTR_SETUP_AUTO_ENTER_CODE === "1";
6604
+ }
6150
6605
  async function executeStart() {
6151
6606
  const childProcess = await import("child_process");
6152
- childProcess.spawn(process.execPath, [process.argv[1], "start", "--daemon"], {
6607
+ childProcess.spawn(process.execPath, [process.argv[1], "start"], {
6153
6608
  detached: true,
6154
6609
  stdio: "ignore",
6155
6610
  env: { ...process.env, CTR_DAEMON: "1" }
@@ -6339,12 +6794,12 @@ async function buildFreshConfig(io) {
6339
6794
  }
6340
6795
  return draft;
6341
6796
  }
6342
- async function completeDraft(input2) {
6343
- const draft = toDraftFromConfig(input2.draft);
6344
- if (input2.fields.includes("defaultModel")) {
6797
+ async function completeDraft(input3) {
6798
+ const draft = toDraftFromConfig(input3.draft);
6799
+ if (input3.fields.includes("defaultModel")) {
6345
6800
  const defaultProvider = draft.Models?.[0]?.id ?? draft.Providers?.[0]?.name ?? "provider";
6346
6801
  const defaultModel = draft.Models?.[0]?.model ?? draft.Providers?.[0]?.models?.[0] ?? "";
6347
- const model = await input2.io.input("\u9ED8\u8BA4\u6A21\u578B", defaultModel);
6802
+ const model = await input3.io.input("\u9ED8\u8BA4\u6A21\u578B", defaultModel);
6348
6803
  if (draft.Models?.[0]) {
6349
6804
  draft.Models[0].model = model;
6350
6805
  draft.Router.default = defaultProvider;
@@ -6353,16 +6808,16 @@ async function completeDraft(input2) {
6353
6808
  draft.Router.default = `${defaultProvider},${model}`;
6354
6809
  }
6355
6810
  }
6356
- if (input2.fields.includes("apiKey")) {
6357
- const apiKey = await input2.io.input("API Key");
6811
+ if (input3.fields.includes("apiKey")) {
6812
+ const apiKey = await input3.io.input("API Key");
6358
6813
  if (draft.Models?.length) {
6359
6814
  draft.Models = draft.Models.map((model) => ({ ...model, key: model.key || apiKey, api_key: model.api_key || apiKey }));
6360
6815
  } else {
6361
6816
  draft.Providers = draft.Providers?.map((provider) => ({ ...provider, api_key: provider.api_key || apiKey }));
6362
6817
  }
6363
6818
  }
6364
- if (input2.fields.includes("apiBaseUrl")) {
6365
- const apiBaseUrl = await input2.io.input("API Base URL");
6819
+ if (input3.fields.includes("apiBaseUrl")) {
6820
+ const apiBaseUrl = await input3.io.input("API Base URL");
6366
6821
  if (draft.Models?.length) {
6367
6822
  draft.Models = draft.Models.map((model) => ({
6368
6823
  ...model,
@@ -6376,8 +6831,8 @@ async function completeDraft(input2) {
6376
6831
  }));
6377
6832
  }
6378
6833
  }
6379
- if (input2.fields.includes("capabilityHints") && draft.Models?.[0]) {
6380
- await promptCapabilityMetadataForDraft(draft, input2.io);
6834
+ if (input3.fields.includes("capabilityHints") && draft.Models?.[0]) {
6835
+ await promptCapabilityMetadataForDraft(draft, input3.io);
6381
6836
  }
6382
6837
  return draft;
6383
6838
  }
@@ -6391,112 +6846,150 @@ function createDefaultDeps(io = createConsoleIO()) {
6391
6846
  executeStart,
6392
6847
  executeReload: executeRestart,
6393
6848
  executeRestart,
6394
- verifyHealth: () => waitForService(DEFAULT_CONFIG2.PORT, 5e3),
6849
+ verifyHealth: () => waitForService(getConfiguredPortFromCurrentFiles(), 5e3),
6395
6850
  enterClaudeCode,
6396
6851
  io
6397
6852
  };
6398
6853
  }
6854
+ function printRoutingNextSteps(io) {
6855
+ io.info("\u4F60\u53EF\u4EE5\u6309\u9700\u7EE7\u7EED\u914D\u7F6E\u8DEF\u7531\u80FD\u529B\uFF1A");
6856
+ io.info(" - TriggerRouter\uFF1A\u9002\u5408\u9AD8\u786E\u5B9A\u6027\u4EFB\u52A1\uFF0C\u628A\u67B6\u6784\u8BBE\u8BA1\u3001\u4EE3\u7801\u5BA1\u67E5\u7B49\u8BF7\u6C42\u56FA\u5B9A\u5207\u5230\u6307\u5B9A\u6A21\u578B");
6857
+ io.info(" - SmartRouter\uFF1A\u9002\u5408\u6A21\u7CCA\u4EFB\u52A1\uFF0C\u5728\u5019\u9009\u6A21\u578B\u4E4B\u95F4\u81EA\u52A8\u9009\u62E9\u66F4\u5408\u9002\u7684\u6A21\u578B");
6858
+ io.info(" - \u914D\u7F6E\u6A21\u677F\u53C2\u8003\uFF1Aconfig/trigger.advanced.yaml");
6859
+ }
6399
6860
  async function runSetupCli(customDeps) {
6400
6861
  const defaults = createDefaultDeps(customDeps?.io);
6401
6862
  const deps = { ...defaults, ...customDeps };
6402
- await runSetup({
6403
- detectSetupEnvironment: () => detectSetupEnvironment({
6404
- readCurrentConfig: deps.readCurrentConfig,
6405
- readLegacyConfig: deps.readLegacyConfig,
6406
- probeService: deps.probeService
6407
- }),
6408
- chooseCurrentConfigAction: async ({ currentConfig }) => {
6409
- if (currentConfig.kind === "missing") {
6410
- return "create";
6411
- }
6412
- if (currentConfig.kind === "valid") {
6413
- deps.io.info("\u68C0\u6D4B\u5230\u5F53\u524D claude-trigger-router \u914D\u7F6E\u5DF2\u53EF\u7528\u3002");
6414
- if (currentConfig.warnings.length > 0) {
6415
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
6416
- }
6417
- return mapValidCurrentConfigChoice(
6418
- await deps.io.choose("\u4F60\u60F3\u76F4\u63A5\u4F7F\u7528\u5B83\uFF0C\u8FD8\u662F\u91CD\u65B0\u8C03\u6574\uFF1F", [
6419
- "\u76F4\u63A5\u4F7F\u7528\u5F53\u524D\u914D\u7F6E\uFF08\u63A8\u8350\uFF09",
6420
- "\u68C0\u67E5\u5E76\u8C03\u6574\u5F53\u524D\u914D\u7F6E",
6421
- "\u653E\u5F03\u5F53\u524D\u914D\u7F6E\uFF0C\u91CD\u65B0\u5F00\u59CB"
6422
- ])
6423
- );
6424
- }
6425
- if (currentConfig.kind === "invalid") {
6426
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A${currentConfig.errors.join("; ")}`);
6427
- if (currentConfig.warnings.length > 0) {
6428
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
6863
+ try {
6864
+ await runSetup({
6865
+ detectSetupEnvironment: () => detectSetupEnvironment({
6866
+ readCurrentConfig: deps.readCurrentConfig,
6867
+ readLegacyConfig: deps.readLegacyConfig,
6868
+ probeService: deps.probeService
6869
+ }),
6870
+ chooseCurrentConfigAction: async ({ currentConfig }) => {
6871
+ if (currentConfig.kind === "missing") {
6872
+ return "create";
6873
+ }
6874
+ if (currentConfig.kind === "valid") {
6875
+ deps.io.info("\u68C0\u6D4B\u5230\u5F53\u524D claude-trigger-router \u914D\u7F6E\u5DF2\u53EF\u7528\u3002");
6876
+ if (currentConfig.warnings.length > 0) {
6877
+ deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
6878
+ }
6879
+ return mapValidCurrentConfigChoice(
6880
+ await deps.io.choose("\u4F60\u60F3\u76F4\u63A5\u4F7F\u7528\u5B83\uFF0C\u8FD8\u662F\u91CD\u65B0\u8C03\u6574\uFF1F", [
6881
+ "\u76F4\u63A5\u4F7F\u7528\u5F53\u524D\u914D\u7F6E\uFF08\u63A8\u8350\uFF09",
6882
+ "\u68C0\u67E5\u5E76\u8C03\u6574\u5F53\u524D\u914D\u7F6E",
6883
+ "\u653E\u5F03\u5F53\u524D\u914D\u7F6E\uFF0C\u91CD\u65B0\u5F00\u59CB"
6884
+ ])
6885
+ );
6429
6886
  }
6430
- return await deps.io.choose("\u9009\u62E9\u4E0B\u4E00\u6B65", ["repair", "overwrite", "cancel"]);
6431
- }
6432
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u65E0\u6CD5\u89E3\u6790\uFF1A${currentConfig.error}`);
6433
- return await deps.io.choose("\u9009\u62E9\u4E0B\u4E00\u6B65", ["rebuild", "cancel"]);
6434
- },
6435
- chooseLegacyConfigAction: async ({ legacyConfig }) => {
6436
- if (legacyConfig.kind === "found") {
6437
- return mapLegacyConfigChoice(
6438
- await deps.io.choose("\u68C0\u6D4B\u5230\u65E7 claude-code-router \u914D\u7F6E\u3002\u662F\u5426\u8FC1\u79FB\u4E3A\u5F53\u524D\u63A8\u8350\u914D\u7F6E\uFF1F", [
6439
- "\u8FC1\u79FB\u65E7\u914D\u7F6E\uFF08\u63A8\u8350\uFF09",
6440
- "\u8DF3\u8FC7\u8FC1\u79FB\uFF0C\u624B\u52A8\u65B0\u5EFA"
6441
- ])
6442
- );
6443
- }
6444
- if (legacyConfig.kind === "read_error") {
6445
- deps.io.info(`\u65E7 ccr \u914D\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF1A${legacyConfig.error}`);
6446
- }
6447
- return "skip";
6448
- },
6449
- buildFreshConfig: () => buildFreshConfig(deps.io),
6450
- buildRepairConfig: async ({ currentConfig }) => toDraftFromConfig(currentConfig),
6451
- completeDraft: ({ draft, fields }) => completeDraft({ draft, fields, io: deps.io }),
6452
- migrateLegacyConfig,
6453
- mapConfigErrorsToRepairFields,
6454
- persistConfig: async ({ config, currentConfigPath, hasExistingConfig }) => {
6455
- const normalized = normalizeAndValidateConfig(config);
6456
- const persisted = await persistSetupConfig({
6457
- config: normalized.config,
6458
- currentConfigPath,
6459
- hasExistingConfig,
6460
- validateConfig: (inputConfig) => normalizeAndValidateConfig(inputConfig).errors,
6461
- backupCurrentConfig: deps.backupCurrentConfig,
6462
- writeConfig: deps.writeConfig
6463
- });
6464
- if (normalized.warnings.length > 0) {
6465
- deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${normalized.warnings.join("; ")}`);
6466
- }
6467
- return persisted;
6468
- },
6469
- ensureServiceReady: async ({ configChanged, detectedService, reloadSupported }) => {
6470
- const action = decideServiceAction({
6471
- configChanged,
6472
- detectedService,
6473
- reloadSupported
6474
- });
6475
- await applyServiceAction({
6476
- action,
6477
- executeStart: deps.executeStart,
6478
- executeReload: deps.executeReload,
6479
- executeRestart: deps.executeRestart,
6480
- verifyHealth: deps.verifyHealth
6481
- });
6482
- return {
6483
- action: action.kind,
6484
- healthChecked: true
6485
- };
6486
- },
6487
- enterClaudeCode: deps.enterClaudeCode,
6488
- reloadSupported: false
6489
- });
6887
+ if (currentConfig.kind === "invalid") {
6888
+ deps.io.info(`\u5F53\u524D\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A${currentConfig.errors.join("; ")}`);
6889
+ if (currentConfig.warnings.length > 0) {
6890
+ deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
6891
+ }
6892
+ return await deps.io.choose("\u9009\u62E9\u4E0B\u4E00\u6B65", ["repair", "overwrite", "cancel"]);
6893
+ }
6894
+ deps.io.info(`\u5F53\u524D\u914D\u7F6E\u65E0\u6CD5\u89E3\u6790\uFF1A${currentConfig.error}`);
6895
+ return await deps.io.choose("\u9009\u62E9\u4E0B\u4E00\u6B65", ["rebuild", "cancel"]);
6896
+ },
6897
+ chooseLegacyConfigAction: async ({ legacyConfig }) => {
6898
+ if (legacyConfig.kind === "found") {
6899
+ return mapLegacyConfigChoice(
6900
+ await deps.io.choose("\u68C0\u6D4B\u5230\u65E7 claude-code-router \u914D\u7F6E\u3002\u662F\u5426\u8FC1\u79FB\u4E3A\u5F53\u524D\u63A8\u8350\u914D\u7F6E\uFF1F", [
6901
+ "\u8FC1\u79FB\u65E7\u914D\u7F6E\uFF08\u63A8\u8350\uFF09",
6902
+ "\u8DF3\u8FC7\u8FC1\u79FB\uFF0C\u624B\u52A8\u65B0\u5EFA"
6903
+ ])
6904
+ );
6905
+ }
6906
+ if (legacyConfig.kind === "read_error") {
6907
+ deps.io.info(`\u65E7 ccr \u914D\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF1A${legacyConfig.error}`);
6908
+ }
6909
+ return "skip";
6910
+ },
6911
+ buildFreshConfig: () => buildFreshConfig(deps.io),
6912
+ buildRepairConfig: async ({ currentConfig }) => toDraftFromConfig(currentConfig),
6913
+ completeDraft: ({ draft, fields }) => completeDraft({ draft, fields, io: deps.io }),
6914
+ migrateLegacyConfig,
6915
+ mapConfigErrorsToRepairFields,
6916
+ io: deps.io,
6917
+ persistConfig: async ({ config, currentConfigPath, hasExistingConfig }) => {
6918
+ let normalized = normalizeAndValidateConfig({
6919
+ ...hasExistingConfig ? getCurrentRuntimeFields() : {},
6920
+ ...config
6921
+ });
6922
+ {
6923
+ const targetPort = normalized.config.PORT ?? DEFAULT_CONFIG2.PORT;
6924
+ const occupied = await isTcpPortOccupied(targetPort, 500);
6925
+ if (occupied && !isServiceRunning()) {
6926
+ const fallbackPort = await getAvailablePort();
6927
+ deps.io.info(`\u68C0\u6D4B\u5230\u9ED8\u8BA4\u7AEF\u53E3 ${targetPort} \u5DF2\u88AB\u5360\u7528\uFF0Csetup \u5DF2\u81EA\u52A8\u6539\u7528\u53EF\u7528\u7AEF\u53E3 ${fallbackPort}\u3002`);
6928
+ normalized = normalizeAndValidateConfig({
6929
+ ...normalized.config,
6930
+ PORT: fallbackPort
6931
+ });
6932
+ }
6933
+ }
6934
+ const persisted = await persistSetupConfig({
6935
+ config: normalized.config,
6936
+ currentConfigPath,
6937
+ hasExistingConfig,
6938
+ validateConfig: (inputConfig) => normalizeAndValidateConfig(inputConfig).errors,
6939
+ backupCurrentConfig: deps.backupCurrentConfig,
6940
+ writeConfig: deps.writeConfig
6941
+ });
6942
+ if (normalized.warnings.length > 0) {
6943
+ deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${normalized.warnings.join("; ")}`);
6944
+ }
6945
+ return persisted;
6946
+ },
6947
+ ensureServiceReady: async ({ configChanged, detectedService, reloadSupported }) => {
6948
+ const effectiveDetectedService = configChanged ? await deps.probeService() : detectedService;
6949
+ const action = decideServiceAction({
6950
+ configChanged,
6951
+ detectedService: effectiveDetectedService,
6952
+ reloadSupported
6953
+ });
6954
+ await applyServiceAction({
6955
+ action,
6956
+ executeStart: deps.executeStart,
6957
+ executeReload: deps.executeReload,
6958
+ executeRestart: deps.executeRestart,
6959
+ verifyHealth: deps.verifyHealth
6960
+ });
6961
+ return {
6962
+ action: action.kind,
6963
+ healthChecked: true
6964
+ };
6965
+ },
6966
+ enterClaudeCode: async () => {
6967
+ printRoutingNextSteps(deps.io);
6968
+ if (!shouldAutoEnterClaudeCodeAfterSetup()) {
6969
+ deps.io.info("\u4E3A\u907F\u514D setup \u7ED3\u675F\u540E\u63A5\u7BA1\u5F53\u524D\u7EC8\u7AEF\uFF0C\u8BF7\u624B\u52A8\u8FD0\u884C\uFF1Actr code");
6970
+ deps.io.info("\u5982\u679C\u4F60\u660E\u786E\u9700\u8981 setup \u7ED3\u675F\u540E\u81EA\u52A8\u8FDB\u5165 Claude Code\uFF0C\u53EF\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF CTR_SETUP_AUTO_ENTER_CODE=1");
6971
+ return;
6972
+ }
6973
+ deps.io.close?.();
6974
+ await deps.enterClaudeCode();
6975
+ },
6976
+ reloadSupported: false
6977
+ });
6978
+ } finally {
6979
+ deps.io.close?.();
6980
+ }
6490
6981
  }
6491
- var import_fs6, import_os3, import_path6, import_promises3, import_process, import_js_yaml;
6982
+ var import_fs6, import_net2, import_os3, import_path6, import_promises3, import_process, import_json52, import_js_yaml;
6492
6983
  var init_setup2 = __esm({
6493
6984
  "src/setup/index.ts"() {
6494
6985
  "use strict";
6495
6986
  import_fs6 = require("fs");
6987
+ import_net2 = require("net");
6496
6988
  import_os3 = require("os");
6497
6989
  import_path6 = require("path");
6498
6990
  import_promises3 = require("readline/promises");
6499
6991
  import_process = require("process");
6992
+ import_json52 = __toESM(require("json5"));
6500
6993
  import_js_yaml = __toESM(require("js-yaml"));
6501
6994
  init_constants();
6502
6995
  init_provider_presets();
@@ -6513,6 +7006,525 @@ var init_setup2 = __esm({
6513
7006
  }
6514
7007
  });
6515
7008
 
7009
+ // src/doctor/index.ts
7010
+ function hasArg(flag) {
7011
+ return process.argv.slice(2).includes(flag);
7012
+ }
7013
+ function createConsoleIO2() {
7014
+ if (process.env.CTR_DOCTOR_FORCE_SCRIPTED_INPUT === "1") {
7015
+ const scriptedInput = (0, import_fs7.readFileSync)(0, "utf-8");
7016
+ const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
7017
+ let cursor = 0;
7018
+ const nextAnswer = async () => answers[cursor++] ?? "";
7019
+ return {
7020
+ info(message) {
7021
+ import_process2.stdout.write(`${message}
7022
+ `);
7023
+ },
7024
+ error(message) {
7025
+ import_process2.stdout.write(`${message}
7026
+ `);
7027
+ },
7028
+ async choose(message, options) {
7029
+ import_process2.stdout.write(`${message}
7030
+ `);
7031
+ options.forEach((option, index2) => import_process2.stdout.write(` ${index2 + 1}. ${option}
7032
+ `));
7033
+ const answer = await nextAnswer();
7034
+ const index = Number(answer);
7035
+ if (Number.isInteger(index) && index >= 1 && index <= options.length) {
7036
+ return options[index - 1];
7037
+ }
7038
+ return options.find((option) => option === answer) ?? options[0];
7039
+ },
7040
+ async input(message, defaultValue) {
7041
+ import_process2.stdout.write(`${message}${defaultValue ? ` (${defaultValue})` : ""}: `);
7042
+ const answer = await nextAnswer();
7043
+ return answer || defaultValue || "";
7044
+ },
7045
+ async confirm(message, defaultValue = true) {
7046
+ import_process2.stdout.write(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"}
7047
+ `);
7048
+ const answer = (await nextAnswer()).toLowerCase();
7049
+ if (!answer) {
7050
+ return defaultValue;
7051
+ }
7052
+ return ["y", "yes", "1", "true"].includes(answer);
7053
+ },
7054
+ close() {
7055
+ }
7056
+ };
7057
+ }
7058
+ const rl = (0, import_promises4.createInterface)({ input: import_process2.stdin, output: import_process2.stdout });
7059
+ const ask = async (message) => (await rl.question(message)).trim();
7060
+ return {
7061
+ info(message) {
7062
+ import_process2.stdout.write(`${message}
7063
+ `);
7064
+ },
7065
+ error(message) {
7066
+ import_process2.stdout.write(`${message}
7067
+ `);
7068
+ },
7069
+ async choose(message, options) {
7070
+ import_process2.stdout.write(`${message}
7071
+ `);
7072
+ options.forEach((option, index) => import_process2.stdout.write(` ${index + 1}. ${option}
7073
+ `));
7074
+ while (true) {
7075
+ const answer = await ask("> ");
7076
+ const index = Number(answer);
7077
+ if (Number.isInteger(index) && index >= 1 && index <= options.length) {
7078
+ return options[index - 1];
7079
+ }
7080
+ const matched = options.find((option) => option === answer);
7081
+ if (matched) {
7082
+ return matched;
7083
+ }
7084
+ import_process2.stdout.write("\u8BF7\u8F93\u5165\u9009\u9879\u7F16\u53F7\u3002\n");
7085
+ }
7086
+ },
7087
+ async input(message, defaultValue) {
7088
+ const answer = await ask(`${message}${defaultValue ? ` (${defaultValue})` : ""}: `);
7089
+ return answer || defaultValue || "";
7090
+ },
7091
+ async confirm(message, defaultValue = true) {
7092
+ const answer = (await ask(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"}: `)).toLowerCase();
7093
+ if (!answer) {
7094
+ return defaultValue;
7095
+ }
7096
+ return ["y", "yes"].includes(answer);
7097
+ },
7098
+ close() {
7099
+ rl.close();
7100
+ }
7101
+ };
7102
+ }
7103
+ function getConfigCandidates() {
7104
+ return [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
7105
+ }
7106
+ function inferInterfaceFromApi(api) {
7107
+ const trimmed = api?.trim();
7108
+ if (!trimmed) {
7109
+ return void 0;
7110
+ }
7111
+ return trimmed.includes("/v1/messages") ? "anthropic" : "openai";
7112
+ }
7113
+ function sanitizeModelId(value) {
7114
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "model";
7115
+ }
7116
+ function tryLoadStructuredConfig(filePath, content) {
7117
+ if (filePath.endsWith(".json")) {
7118
+ try {
7119
+ return { config: JSON.parse(content), repairedParse: false, messages: [] };
7120
+ } catch {
7121
+ return {
7122
+ config: import_json53.default.parse(content),
7123
+ repairedParse: true,
7124
+ messages: ["\u68C0\u6D4B\u5230 JSON \u914D\u7F6E\u5305\u542B\u5BBD\u677E\u8BED\u6CD5\uFF0Cdoctor \u5DF2\u6309\u6807\u51C6 JSON \u7ED3\u6784\u91CD\u65B0\u5F52\u4E00\u5316\u3002"]
7125
+ };
7126
+ }
7127
+ }
7128
+ try {
7129
+ return { config: import_js_yaml2.default.load(content), repairedParse: false, messages: [] };
7130
+ } catch (error) {
7131
+ const sanitized = content.replace(/\t/g, " ");
7132
+ if (sanitized !== content) {
7133
+ return {
7134
+ config: import_js_yaml2.default.load(sanitized),
7135
+ repairedParse: true,
7136
+ messages: ["\u68C0\u6D4B\u5230 YAML \u4F7F\u7528\u4E86 Tab \u7F29\u8FDB\uFF0Cdoctor \u5DF2\u81EA\u52A8\u4FEE\u590D\u4E3A\u7A7A\u683C\u7F29\u8FDB\u3002"]
7137
+ };
7138
+ }
7139
+ throw error;
7140
+ }
7141
+ }
7142
+ function loadCurrentConfig() {
7143
+ const existingPath = getConfigCandidates().find((filePath) => (0, import_fs7.existsSync)(filePath));
7144
+ const path = existingPath ?? CONFIG_FILE;
7145
+ if (!existingPath) {
7146
+ return {
7147
+ path,
7148
+ existed: false,
7149
+ repairedParse: false,
7150
+ messages: ["\u672A\u68C0\u6D4B\u5230\u5F53\u524D Claude Trigger Router \u914D\u7F6E\u3002"]
7151
+ };
7152
+ }
7153
+ const content = (0, import_fs7.readFileSync)(existingPath, "utf-8");
7154
+ const loaded = tryLoadStructuredConfig(existingPath, content);
7155
+ return {
7156
+ path,
7157
+ existed: true,
7158
+ repairedParse: loaded.repairedParse,
7159
+ messages: loaded.messages,
7160
+ config: loaded.config
7161
+ };
7162
+ }
7163
+ function getModelLookupId(model) {
7164
+ return model.id?.trim() || sanitizeModelId(model.model ?? "");
7165
+ }
7166
+ function repairDeterministicConfig(config) {
7167
+ const nextConfig = {
7168
+ ...config,
7169
+ HOST: config.HOST ?? DEFAULT_CONFIG2.HOST,
7170
+ PORT: config.PORT ?? DEFAULT_CONFIG2.PORT,
7171
+ LOG: config.LOG ?? DEFAULT_CONFIG2.LOG,
7172
+ LOG_LEVEL: config.LOG_LEVEL ?? DEFAULT_CONFIG2.LOG_LEVEL
7173
+ };
7174
+ const changes = [];
7175
+ if (Array.isArray(config.Models) && config.Models.length > 0) {
7176
+ nextConfig.Models = config.Models.map((item, index) => {
7177
+ const api = getModelApi(item);
7178
+ const key = getModelKey(item);
7179
+ const inferredInterface = getModelInterface(item) ?? inferInterfaceFromApi(api);
7180
+ const id = item.id?.trim() || (item.model ? sanitizeModelId(item.model) : `model_${index + 1}`);
7181
+ if (!item.id?.trim()) {
7182
+ changes.push(`\u5DF2\u8865\u5168 Models[${index}].id -> ${id}`);
7183
+ }
7184
+ if (inferredInterface && !getModelInterface(item)) {
7185
+ changes.push(`\u5DF2\u8865\u5168 Models[${index}].interface -> ${inferredInterface}`);
7186
+ }
7187
+ if (api && item.api !== api) {
7188
+ changes.push(`\u5DF2\u5F52\u4E00 Models[${index}].api`);
7189
+ }
7190
+ if (key && item.key !== key) {
7191
+ changes.push(`\u5DF2\u5F52\u4E00 Models[${index}].key`);
7192
+ }
7193
+ return {
7194
+ ...item,
7195
+ id,
7196
+ api: api || void 0,
7197
+ api_base_url: api || void 0,
7198
+ key: key || void 0,
7199
+ api_key: key || void 0,
7200
+ interface: inferredInterface,
7201
+ protocol: inferredInterface
7202
+ };
7203
+ });
7204
+ if (!nextConfig.Router?.default) {
7205
+ if (nextConfig.Models.length === 1) {
7206
+ nextConfig.Router = {
7207
+ ...nextConfig.Router ?? {},
7208
+ default: getModelLookupId(nextConfig.Models[0])
7209
+ };
7210
+ changes.push(`\u5DF2\u8865\u5168 Router.default -> ${nextConfig.Router.default}`);
7211
+ } else if (typeof config.Router?.default === "string" && config.Router.default.includes(",")) {
7212
+ const [providerName, modelName] = config.Router.default.split(",").map((item) => item.trim());
7213
+ const matched = nextConfig.Models.find(
7214
+ (item) => item.model === modelName && (item.id === providerName || item.id.startsWith(`${sanitizeModelId(providerName)}_`))
7215
+ );
7216
+ if (matched) {
7217
+ nextConfig.Router = {
7218
+ ...nextConfig.Router ?? {},
7219
+ default: matched.id
7220
+ };
7221
+ changes.push(`\u5DF2\u5F52\u4E00 Router.default -> ${matched.id}`);
7222
+ }
7223
+ }
7224
+ }
7225
+ } else if (Array.isArray(config.Providers) && config.Providers.length > 0) {
7226
+ const migrated = migrateLegacyConfig(config);
7227
+ nextConfig.Models = migrated.draft.Models;
7228
+ nextConfig.Router = {
7229
+ ...nextConfig.Router ?? {},
7230
+ ...migrated.draft.Router
7231
+ };
7232
+ nextConfig.Providers = [];
7233
+ changes.push("\u5DF2\u5C06 legacy Providers \u7ED3\u6784\u5F52\u4E00\u4E3A Models \u7ED3\u6784\u3002");
7234
+ }
7235
+ return { config: nextConfig, changes };
7236
+ }
7237
+ async function completeMissingModelFields(config, io) {
7238
+ const changes = [];
7239
+ const nextConfig = {
7240
+ ...config,
7241
+ Models: Array.isArray(config.Models) ? config.Models.map((item) => ({ ...item })) : [],
7242
+ Router: { ...config.Router ?? {} }
7243
+ };
7244
+ for (let index = 0; index < (nextConfig.Models?.length ?? 0); index += 1) {
7245
+ const model = nextConfig.Models[index];
7246
+ const label = model.id || model.model || `Models[${index}]`;
7247
+ if (!model.id?.trim()) {
7248
+ model.id = sanitizeModelId(await io.input(`\u8865\u5168 ${label} \u7684\u6A21\u578B ID`, sanitizeModelId(model.model || `model_${index + 1}`)));
7249
+ changes.push(`\u5DF2\u8865\u5168 ${label} \u7684\u6A21\u578B ID -> ${model.id}`);
7250
+ }
7251
+ if (!getModelApi(model)) {
7252
+ const api = await io.input(`\u8865\u5168 ${label} \u7684 API Base URL`);
7253
+ model.api = api;
7254
+ model.api_base_url = api;
7255
+ changes.push(`\u5DF2\u8865\u5168 ${label} \u7684 API Base URL`);
7256
+ }
7257
+ if (!getModelKey(model)) {
7258
+ const key = await io.input(`\u8865\u5168 ${label} \u7684 API Key`);
7259
+ model.key = key;
7260
+ model.api_key = key;
7261
+ changes.push(`\u5DF2\u8865\u5168 ${label} \u7684 API Key`);
7262
+ }
7263
+ if (!getModelInterface(model)) {
7264
+ const interfaceChoice = await io.choose(`\u8865\u5168 ${label} \u7684\u63A5\u53E3\u7C7B\u578B`, ["openai", "anthropic"]);
7265
+ model.interface = interfaceChoice;
7266
+ model.protocol = model.interface;
7267
+ changes.push(`\u5DF2\u8865\u5168 ${label} \u7684\u63A5\u53E3\u7C7B\u578B -> ${model.interface}`);
7268
+ }
7269
+ if (!model.model?.trim()) {
7270
+ model.model = await io.input(`\u8865\u5168 ${label} \u7684\u4E0A\u6E38\u6A21\u578B\u540D`);
7271
+ changes.push(`\u5DF2\u8865\u5168 ${label} \u7684\u4E0A\u6E38\u6A21\u578B\u540D`);
7272
+ }
7273
+ }
7274
+ if (!nextConfig.Router?.default) {
7275
+ if ((nextConfig.Models?.length ?? 0) === 1) {
7276
+ nextConfig.Router.default = nextConfig.Models[0].id;
7277
+ changes.push(`\u5DF2\u8865\u5168 Router.default -> ${nextConfig.Router.default}`);
7278
+ } else if ((nextConfig.Models?.length ?? 0) > 1) {
7279
+ const choice = await io.choose("\u8865\u5168\u9ED8\u8BA4\u6A21\u578B", nextConfig.Models.map((item) => item.id));
7280
+ nextConfig.Router.default = choice;
7281
+ changes.push(`\u5DF2\u8865\u5168 Router.default -> ${choice}`);
7282
+ }
7283
+ }
7284
+ return { config: nextConfig, changes };
7285
+ }
7286
+ async function probeModelAvailability(model) {
7287
+ const api = getModelApi(model);
7288
+ const key = getModelKey(model);
7289
+ const modelInterface = getModelInterface(model);
7290
+ if (!api || !key || !modelInterface || !model.model) {
7291
+ return {
7292
+ kind: "failure",
7293
+ category: "protocol_mismatch",
7294
+ message: "\u6A21\u578B\u914D\u7F6E\u7F3A\u5C11 api/key/interface/model\uFF0C\u65E0\u6CD5\u53D1\u8D77\u63A2\u6D4B\u3002"
7295
+ };
7296
+ }
7297
+ try {
7298
+ const registry = buildModelRegistry({
7299
+ Providers: [],
7300
+ Models: [model],
7301
+ Router: {
7302
+ default: model.id
7303
+ }
7304
+ });
7305
+ const compiledModel = registry.modelMap[model.id];
7306
+ const dispatchRequest = compiledModel ? buildProviderDispatchRequest({
7307
+ model: compiledModel.modelName,
7308
+ interface: compiledModel.interface ?? modelInterface,
7309
+ compatibilityProfile: compiledModel.compatibilityProfile,
7310
+ capabilities: compiledModel.capabilities,
7311
+ request: {
7312
+ model: compiledModel.id,
7313
+ max_tokens: 1,
7314
+ stream: true,
7315
+ messages: [
7316
+ {
7317
+ role: "user",
7318
+ content: [
7319
+ {
7320
+ type: "text",
7321
+ text: "ok"
7322
+ }
7323
+ ]
7324
+ }
7325
+ ]
7326
+ }
7327
+ }) : null;
7328
+ const response = await fetch(api, {
7329
+ method: "POST",
7330
+ signal: AbortSignal.timeout(1e4),
7331
+ headers: modelInterface === "anthropic" ? {
7332
+ "content-type": "application/json",
7333
+ "x-api-key": key,
7334
+ "anthropic-version": "2023-06-01"
7335
+ } : {
7336
+ "content-type": "application/json",
7337
+ authorization: `Bearer ${key}`
7338
+ },
7339
+ body: JSON.stringify(dispatchRequest?.body ?? (modelInterface === "anthropic" ? {
7340
+ model: model.model,
7341
+ max_tokens: 1,
7342
+ stream: true,
7343
+ messages: [
7344
+ {
7345
+ role: "user",
7346
+ content: [
7347
+ {
7348
+ type: "text",
7349
+ text: "ok"
7350
+ }
7351
+ ]
7352
+ }
7353
+ ]
7354
+ } : {
7355
+ model: model.model,
7356
+ max_tokens: 1,
7357
+ stream: true,
7358
+ messages: [
7359
+ {
7360
+ role: "user",
7361
+ content: "ok"
7362
+ }
7363
+ ]
7364
+ }))
7365
+ });
7366
+ if (response.ok) {
7367
+ return { kind: "success" };
7368
+ }
7369
+ const body = await response.text();
7370
+ if (response.status === 401 || response.status === 403) {
7371
+ return { kind: "failure", category: "auth_error", message: `${response.status} ${body}` };
7372
+ }
7373
+ if (response.status === 404) {
7374
+ return { kind: "failure", category: "model_not_found", message: `${response.status} ${body}` };
7375
+ }
7376
+ if (response.status === 400) {
7377
+ return { kind: "failure", category: "protocol_mismatch", message: `${response.status} ${body}` };
7378
+ }
7379
+ return { kind: "failure", category: "remote_error", message: `${response.status} ${body}` };
7380
+ } catch (error) {
7381
+ return {
7382
+ kind: "failure",
7383
+ category: "endpoint_unreachable",
7384
+ message: error?.message || String(error)
7385
+ };
7386
+ }
7387
+ }
7388
+ async function ensureServiceUsable(config, deps, configChanged) {
7389
+ const port = config.PORT ?? DEFAULT_CONFIG2.PORT;
7390
+ const healthy = await deps.probeServiceHealth(port, 500);
7391
+ const occupied = await deps.isTcpPortOccupied(port, 500);
7392
+ const running = deps.isServiceRunning();
7393
+ if (healthy && !configChanged) {
7394
+ deps.io.info(`\u670D\u52A1\u5065\u5EB7\u68C0\u67E5\u901A\u8FC7\uFF1Ahttp://127.0.0.1:${port}`);
7395
+ return;
7396
+ }
7397
+ if (occupied && !healthy && !running) {
7398
+ throw new Error(`\u7AEF\u53E3 ${port} \u5DF2\u88AB\u5176\u4ED6\u670D\u52A1\u5360\u7528\uFF0Cdoctor \u65E0\u6CD5\u81EA\u52A8\u542F\u52A8\u5F53\u524D\u670D\u52A1\u3002`);
7399
+ }
7400
+ if (running) {
7401
+ const info = deps.readServiceInfo();
7402
+ if (info) {
7403
+ try {
7404
+ deps.killProcess(info.pid);
7405
+ } catch {
7406
+ }
7407
+ }
7408
+ }
7409
+ await deps.startDaemon();
7410
+ const verified = await deps.waitForService(port, 5e3);
7411
+ if (!verified) {
7412
+ throw new Error(`doctor \u81EA\u52A8\u542F\u52A8\u540E\u5065\u5EB7\u68C0\u67E5\u4ECD\u672A\u901A\u8FC7\uFF08\u7AEF\u53E3 ${port}\uFF09\u3002`);
7413
+ }
7414
+ deps.io.info(`\u670D\u52A1\u5DF2\u5C31\u7EEA\uFF1Ahttp://127.0.0.1:${port}`);
7415
+ }
7416
+ function createDefaultDeps2(io = createConsoleIO2()) {
7417
+ return {
7418
+ readLegacyConfig,
7419
+ backupCurrentConfig: backupConfigFile,
7420
+ writeConfig: writeConfigFile,
7421
+ isServiceRunning,
7422
+ readServiceInfo,
7423
+ killProcess,
7424
+ probeServiceHealth,
7425
+ isTcpPortOccupied,
7426
+ waitForService,
7427
+ io,
7428
+ startDaemon: async () => {
7429
+ (0, import_child_process2.spawn)(process.execPath, [process.argv[1], "start", "--daemon"], {
7430
+ detached: true,
7431
+ stdio: "ignore",
7432
+ env: { ...process.env, CTR_DAEMON: "1" }
7433
+ }).unref();
7434
+ }
7435
+ };
7436
+ }
7437
+ async function runDoctorCli(customDeps) {
7438
+ const defaults = createDefaultDeps2(customDeps?.io);
7439
+ const deps = { ...defaults, ...customDeps };
7440
+ let configChanged = false;
7441
+ try {
7442
+ deps.io.info("\u5F00\u59CB\u8BCA\u65AD\u5F53\u524D Claude Trigger Router \u914D\u7F6E...");
7443
+ const current = loadCurrentConfig();
7444
+ current.messages.forEach((message) => deps.io.info(message));
7445
+ let workingConfig = current.config;
7446
+ if (!workingConfig) {
7447
+ const legacy = await deps.readLegacyConfig();
7448
+ if (legacy.kind === "found") {
7449
+ deps.io.info("\u672A\u68C0\u6D4B\u5230\u5F53\u524D\u914D\u7F6E\uFF0C\u4F46\u53D1\u73B0\u65E7 claude-code-router \u914D\u7F6E\uFF0Cdoctor \u5C06\u5148\u5C1D\u8BD5\u8FC1\u79FB\u3002");
7450
+ const migrated = migrateLegacyConfig(legacy.config);
7451
+ workingConfig = {
7452
+ ...buildUsableMinimalTemplateConfig(),
7453
+ ...migrated.draft
7454
+ };
7455
+ } else {
7456
+ throw new Error("\u672A\u68C0\u6D4B\u5230\u53EF\u8BCA\u65AD\u7684\u5F53\u524D\u914D\u7F6E\uFF1B\u8BF7\u5148\u8FD0\u884C ctr setup \u6216 ctr init --force\u3002");
7457
+ }
7458
+ }
7459
+ const deterministic = repairDeterministicConfig(workingConfig);
7460
+ workingConfig = deterministic.config;
7461
+ deterministic.changes.forEach((message) => deps.io.info(message));
7462
+ const completed = await completeMissingModelFields(workingConfig, deps.io);
7463
+ workingConfig = completed.config;
7464
+ completed.changes.forEach((message) => deps.io.info(message));
7465
+ const normalized = normalizeAndValidateConfig(workingConfig);
7466
+ if (normalized.errors.length > 0) {
7467
+ deps.io.error(`doctor \u4ECD\u53D1\u73B0\u65E0\u6CD5\u81EA\u52A8\u4FEE\u590D\u7684\u914D\u7F6E\u9519\u8BEF\uFF1A${normalized.errors.join("; ")}`);
7468
+ throw new Error("doctor could not fully repair config");
7469
+ }
7470
+ if (normalized.warnings.length > 0) {
7471
+ deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${normalized.warnings.join("; ")}`);
7472
+ }
7473
+ const needWrite = current.repairedParse || deterministic.changes.length > 0 || completed.changes.length > 0 || !current.existed;
7474
+ if (needWrite) {
7475
+ if (current.existed) {
7476
+ const backupPath = await deps.backupCurrentConfig();
7477
+ if (backupPath) {
7478
+ deps.io.info(`\u5DF2\u5907\u4EFD\u5F53\u524D\u914D\u7F6E\uFF1A${backupPath}`);
7479
+ }
7480
+ }
7481
+ await deps.writeConfig(normalized.config);
7482
+ deps.io.info(`\u5DF2\u5199\u56DE\u4FEE\u590D\u540E\u7684\u914D\u7F6E\uFF1A${current.path}`);
7483
+ configChanged = true;
7484
+ }
7485
+ await ensureServiceUsable(normalized.config, deps, configChanged);
7486
+ const shouldProbeModels = hasArg("--check-models") ? await deps.io.confirm(`\u5373\u5C06\u5411 ${normalized.config.Models?.length ?? 0} \u4E2A\u6A21\u578B\u53D1\u9001\u6700\u5C0F\u63A2\u6D4B\u8BF7\u6C42\uFF0C\u53EF\u80FD\u6D88\u8017\u5C11\u91CF\u989D\u5EA6\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F`, true) : await deps.io.confirm(`\u662F\u5426\u7EE7\u7EED\u63A2\u6D4B ${normalized.config.Models?.length ?? 0} \u4E2A\u6A21\u578B\u7684\u53EF\u7528\u6027\uFF1F\u8FD9\u4F1A\u6D88\u8017\u5C11\u91CF\u989D\u5EA6\u3002`, false);
7487
+ if (!shouldProbeModels) {
7488
+ deps.io.info("\u5DF2\u8DF3\u8FC7\u6A21\u578B\u63A2\u6D4B\u3002\u914D\u7F6E\u548C\u670D\u52A1\u8BCA\u65AD\u5DF2\u5B8C\u6210\u3002");
7489
+ return;
7490
+ }
7491
+ for (const model of normalized.config.Models ?? []) {
7492
+ const result = await probeModelAvailability(model);
7493
+ if (result.kind === "success") {
7494
+ deps.io.info(`\u6A21\u578B\u63A2\u6D4B\u6210\u529F\uFF1A${model.id}`);
7495
+ continue;
7496
+ }
7497
+ deps.io.error(`\u6A21\u578B\u63A2\u6D4B\u5931\u8D25\uFF1A${model.id} -> ${result.category} -> ${result.message}`);
7498
+ deps.io.info("\u8FD9\u7C7B\u8FDC\u7AEF\u5931\u8D25\u9700\u8981\u4F60\u786E\u8BA4\u5E76\u624B\u52A8\u5904\u7406\uFF1Bdoctor \u4E0D\u4F1A\u81EA\u52A8\u4FEE\u6539\u6A21\u578B\u8BED\u4E49\u6216\u8FDC\u7AEF\u8D26\u53F7\u914D\u7F6E\u3002");
7499
+ }
7500
+ deps.io.info("doctor \u8BCA\u65AD\u5B8C\u6210\u3002");
7501
+ } finally {
7502
+ deps.io.close?.();
7503
+ }
7504
+ }
7505
+ var import_fs7, import_promises4, import_process2, import_child_process2, import_json53, import_js_yaml2;
7506
+ var init_doctor = __esm({
7507
+ "src/doctor/index.ts"() {
7508
+ "use strict";
7509
+ import_fs7 = require("fs");
7510
+ import_promises4 = require("readline/promises");
7511
+ import_process2 = require("process");
7512
+ import_child_process2 = require("child_process");
7513
+ import_json53 = __toESM(require("json5"));
7514
+ import_js_yaml2 = __toESM(require("js-yaml"));
7515
+ init_constants();
7516
+ init_utils();
7517
+ init_migrate();
7518
+ init_setup2();
7519
+ init_schema();
7520
+ init_compile();
7521
+ init_protocols();
7522
+ init_processCheck();
7523
+ init_service_health();
7524
+ init_templates();
7525
+ }
7526
+ });
7527
+
6516
7528
  // src/cli.ts
6517
7529
  var cli_exports = {};
6518
7530
  __export(cli_exports, {
@@ -6522,7 +7534,7 @@ __export(cli_exports, {
6522
7534
  });
6523
7535
  module.exports = __toCommonJS(cli_exports);
6524
7536
  function getPackageInfo() {
6525
- const content = (0, import_fs7.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
7537
+ const content = (0, import_fs8.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
6526
7538
  const pkg = JSON.parse(content);
6527
7539
  return {
6528
7540
  name: pkg.name ?? "@peterwangze/claude-trigger-router",
@@ -6535,7 +7547,7 @@ function getArgs() {
6535
7547
  function getCommand() {
6536
7548
  return getArgs()[0];
6537
7549
  }
6538
- function hasArg(flag, shortFlag) {
7550
+ function hasArg2(flag, shortFlag) {
6539
7551
  const args = getArgs();
6540
7552
  return args.includes(flag) || (shortFlag ? args.includes(shortFlag) : false);
6541
7553
  }
@@ -6544,23 +7556,34 @@ function getArgValue(flag, shortFlag) {
6544
7556
  const index = args.indexOf(flag) !== -1 ? args.indexOf(flag) : shortFlag ? args.indexOf(shortFlag) : -1;
6545
7557
  return index !== -1 ? args[index + 1] : void 0;
6546
7558
  }
7559
+ function parsePortValue(portValue, sourceLabel) {
7560
+ const trimmed = portValue.trim();
7561
+ if (!/^\d+$/.test(trimmed)) {
7562
+ throw new Error(`${sourceLabel} \u4E0D\u662F\u5408\u6CD5\u7AEF\u53E3\uFF1A${portValue}`);
7563
+ }
7564
+ const port = Number.parseInt(trimmed, 10);
7565
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
7566
+ throw new Error(`${sourceLabel} \u8D85\u51FA\u5408\u6CD5\u8303\u56F4\uFF081-65535\uFF09\uFF1A${portValue}`);
7567
+ }
7568
+ return port;
7569
+ }
6547
7570
  function getPort() {
6548
7571
  const portValue = getArgValue("--port", "-p");
6549
7572
  if (portValue) {
6550
- return parseInt(portValue, 10);
7573
+ return parsePortValue(portValue, "\u547D\u4EE4\u884C\u7AEF\u53E3\u53C2\u6570");
6551
7574
  }
6552
7575
  try {
6553
- const yaml3 = require("js-yaml");
6554
- if ((0, import_fs7.existsSync)(CONFIG_FILE)) {
6555
- const content = (0, import_fs7.readFileSync)(CONFIG_FILE, "utf-8");
6556
- const config = yaml3.load(content);
7576
+ const yaml4 = require("js-yaml");
7577
+ if ((0, import_fs8.existsSync)(CONFIG_FILE)) {
7578
+ const content = (0, import_fs8.readFileSync)(CONFIG_FILE, "utf-8");
7579
+ const config = yaml4.load(content);
6557
7580
  if (config?.PORT) return config.PORT;
6558
- } else if ((0, import_fs7.existsSync)(CONFIG_FILE_YML)) {
6559
- const content = (0, import_fs7.readFileSync)(CONFIG_FILE_YML, "utf-8");
6560
- const config = yaml3.load(content);
7581
+ } else if ((0, import_fs8.existsSync)(CONFIG_FILE_YML)) {
7582
+ const content = (0, import_fs8.readFileSync)(CONFIG_FILE_YML, "utf-8");
7583
+ const config = yaml4.load(content);
6561
7584
  if (config?.PORT) return config.PORT;
6562
- } else if ((0, import_fs7.existsSync)(CONFIG_FILE_JSON)) {
6563
- const content = (0, import_fs7.readFileSync)(CONFIG_FILE_JSON, "utf-8");
7585
+ } else if ((0, import_fs8.existsSync)(CONFIG_FILE_JSON)) {
7586
+ const content = (0, import_fs8.readFileSync)(CONFIG_FILE_JSON, "utf-8");
6564
7587
  const config = JSON.parse(content);
6565
7588
  if (config?.PORT) return config.PORT;
6566
7589
  }
@@ -6569,7 +7592,7 @@ function getPort() {
6569
7592
  return DEFAULT_CONFIG2.PORT;
6570
7593
  }
6571
7594
  function isDaemonMode() {
6572
- return hasArg("--daemon", "-d");
7595
+ return hasArg2("--daemon", "-d");
6573
7596
  }
6574
7597
  function printHelp() {
6575
7598
  console.log(`
@@ -6579,6 +7602,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6579
7602
 
6580
7603
  \u547D\u4EE4\uFF1A
6581
7604
  setup \u68C0\u6D4B\u5E76\u590D\u7528\u5DF2\u6709\u914D\u7F6E\uFF0C\u5FC5\u8981\u65F6\u8FC1\u79FB\u65E7\u914D\u7F6E\u6216\u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
7605
+ doctor \u8BCA\u65AD\u5E76\u4FEE\u590D\u5F53\u524D\u914D\u7F6E\uFF0C\u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
6582
7606
  init \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
6583
7607
  start \u542F\u52A8\u8DEF\u7531\u670D\u52A1\uFF08\u9ED8\u8BA4\u524D\u53F0\u8FD0\u884C\uFF09
6584
7608
  stop \u505C\u6B62\u540E\u53F0\u670D\u52A1
@@ -6587,7 +7611,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6587
7611
  version \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C\u4E0E\u5305\u4FE1\u606F
6588
7612
  upgrade \u67E5\u770B\u5347\u7EA7\u5230\u6700\u65B0 npm \u7248\u672C\u7684\u6307\u5F15
6589
7613
  code \u901A\u8FC7\u8DEF\u7531\u5668\u8FD0\u884C Claude Code\uFF08\u9700\u5148\u542F\u52A8\u670D\u52A1\uFF09
6590
- ui \u6253\u5F00\u7BA1\u7406 API \u8BF4\u660E\u9875\uFF08Web UI \u5F00\u53D1\u4E2D\uFF09
7614
+ ui \u6253\u5F00\u672C\u5730\u7BA1\u7406\u9875\uFF08\u914D\u7F6E\u9884\u89C8\u4E0E\u8C03\u8BD5\uFF09
6591
7615
  help \u663E\u793A\u6B64\u5E2E\u52A9\u4FE1\u606F
6592
7616
 
6593
7617
  \u9009\u9879\uFF1A
@@ -6597,6 +7621,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6597
7621
 
6598
7622
  \u4F7F\u7528\u793A\u4F8B\uFF1A
6599
7623
  ctr setup # \u590D\u7528\u5F53\u524D\u914D\u7F6E / \u8FC1\u79FB\u65E7\u914D\u7F6E / \u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
7624
+ ctr doctor # \u8BCA\u65AD\u914D\u7F6E / \u4FEE\u590D\u683C\u5F0F\u95EE\u9898 / \u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
6600
7625
  ctr init # \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
6601
7626
  ctr version # \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C
6602
7627
  ctr upgrade # \u67E5\u770B\u5347\u7EA7\u5230\u6700\u65B0\u7248\u672C\u7684\u547D\u4EE4
@@ -6604,6 +7629,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6604
7629
  ctr start --daemon # \u540E\u53F0\u542F\u52A8
6605
7630
  ctr status # \u67E5\u770B\u670D\u52A1\u72B6\u6001
6606
7631
  ctr code # \u542F\u52A8 Claude Code\uFF08\u9700\u5148\u8FD0\u884C ctr start\uFF09
7632
+ ctr ui # \u6253\u5F00\u672C\u5730\u7BA1\u7406\u9875\uFF08\u53EF\u9009\uFF09
6607
7633
  ctr stop # \u505C\u6B62\u540E\u53F0\u670D\u52A1
6608
7634
  ctr restart --daemon # \u91CD\u542F\u540E\u53F0\u670D\u52A1
6609
7635
 
@@ -6613,10 +7639,29 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6613
7639
 
6614
7640
  \u914D\u7F6E\u76EE\u5F55\uFF1A${CONFIG_DIR}
6615
7641
 
7642
+ \u8865\u5145\u8BF4\u660E\uFF1A
7643
+ ctr restart \u5F53\u524D\u9ED8\u8BA4\u6309\u540E\u53F0\u6A21\u5F0F\u91CD\u542F\uFF1B\u53EF\u5199 ctr restart \u6216 ctr restart --daemon
7644
+
6616
7645
  \u66F4\u591A\u4FE1\u606F\uFF1Ahttps://github.com/peterwangze/claude-trigger-router
6617
7646
  `);
6618
7647
  }
6619
- async function getLatestPackageVersion(timeoutMs = 1500) {
7648
+ function getLatestPackageVersionViaNpm(packageName, timeoutMs = 5e3) {
7649
+ try {
7650
+ const result = (0, import_child_process3.spawnSync)("npm", ["view", packageName, "version", "--registry", PACKAGE_REGISTRY_URL], {
7651
+ encoding: "utf-8",
7652
+ timeout: timeoutMs,
7653
+ shell: process.platform === "win32"
7654
+ });
7655
+ if (result.status !== 0) {
7656
+ return null;
7657
+ }
7658
+ const value = result.stdout?.trim();
7659
+ return value ? value : null;
7660
+ } catch {
7661
+ return null;
7662
+ }
7663
+ }
7664
+ async function getLatestPackageVersion(packageName, timeoutMs = 4e3) {
6620
7665
  try {
6621
7666
  const response = await fetch(PACKAGE_REGISTRY_LATEST_URL, {
6622
7667
  signal: AbortSignal.timeout(timeoutMs)
@@ -6625,10 +7670,12 @@ async function getLatestPackageVersion(timeoutMs = 1500) {
6625
7670
  return null;
6626
7671
  }
6627
7672
  const payload = await response.json();
6628
- return typeof payload.version === "string" ? payload.version : null;
7673
+ if (typeof payload.version === "string") {
7674
+ return payload.version;
7675
+ }
6629
7676
  } catch {
6630
- return null;
6631
7677
  }
7678
+ return getLatestPackageVersionViaNpm(packageName);
6632
7679
  }
6633
7680
  function isNewerVersion(current, latest) {
6634
7681
  const currentParts = current.split(".").map((part) => Number.parseInt(part, 10));
@@ -6648,7 +7695,7 @@ function isNewerVersion(current, latest) {
6648
7695
  }
6649
7696
  async function printVersion() {
6650
7697
  const pkg = getPackageInfo();
6651
- const latestVersion = await getLatestPackageVersion();
7698
+ const latestVersion = await getLatestPackageVersion(pkg.name);
6652
7699
  console.log(`Package: ${pkg.name}`);
6653
7700
  console.log(`Version: ${pkg.version}`);
6654
7701
  console.log(`Latest: ${latestVersion ?? "unavailable"}`);
@@ -6668,30 +7715,42 @@ function printUpgradeGuidance() {
6668
7715
  console.log("\u5168\u5C40\u5B89\u88C5\u5728\u67D0\u4E9B\u73AF\u5883\u4E0B\u53EF\u80FD\u9700\u8981\u7BA1\u7406\u5458/root \u6743\u9650\u3002");
6669
7716
  console.log(`NPM: ${PACKAGE_PAGE_URL}`);
6670
7717
  }
7718
+ function printRestartGuidanceHint() {
7719
+ console.log("\u8BF4\u660E\uFF1A`ctr restart` \u5F53\u524D\u9ED8\u8BA4\u6309\u540E\u53F0\u6A21\u5F0F\u91CD\u542F\u670D\u52A1\uFF0C`--daemon` \u53EA\u662F\u663E\u5F0F\u5199\u6CD5\u3002");
7720
+ }
7721
+ function isClaudeCommandAvailable(timeoutMs = 3e3) {
7722
+ try {
7723
+ const result = (0, import_child_process3.spawnSync)("claude", ["--version"], {
7724
+ encoding: "utf-8",
7725
+ timeout: timeoutMs,
7726
+ stdio: "ignore",
7727
+ shell: process.platform === "win32"
7728
+ });
7729
+ return result.status === 0;
7730
+ } catch {
7731
+ return false;
7732
+ }
7733
+ }
6671
7734
  function initConfig2() {
6672
- const force = hasArg("--force");
6673
- const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs7.existsSync);
7735
+ const force = hasArg2("--force");
7736
+ const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs8.existsSync);
6674
7737
  if (existingConfig && !force) {
6675
7738
  console.log(`\u26A0\uFE0F \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${existingConfig}`);
6676
7739
  console.log(" \u5982\u9700\u8986\u76D6\uFF0C\u8BF7\u4F7F\u7528 --force \u53C2\u6570\u3002");
6677
7740
  return;
6678
7741
  }
6679
- if (!(0, import_fs7.existsSync)(CONFIG_DIR)) {
6680
- (0, import_fs7.mkdirSync)(CONFIG_DIR, { recursive: true });
6681
- }
6682
- const examplePaths = [
6683
- (0, import_path7.join)(__dirname, "..", "config", "trigger.example.yaml"),
6684
- (0, import_path7.join)((0, import_path7.dirname)(process.argv[1]), "..", "config", "trigger.example.yaml")
6685
- ];
6686
- const exampleFile = examplePaths.find((p) => (0, import_fs7.existsSync)(p));
6687
- if (!exampleFile) {
6688
- console.error("\u274C \u627E\u4E0D\u5230\u793A\u4F8B\u914D\u7F6E\u6587\u4EF6\u3002");
6689
- console.log(` \u8BF7\u624B\u52A8\u521B\u5EFA ${CONFIG_FILE}`);
6690
- console.log(" \u53C2\u8003\u6587\u6863\uFF1Ahttps://github.com/peterwangze/claude-trigger-router#configuration");
6691
- process.exit(1);
7742
+ if (!(0, import_fs8.existsSync)(CONFIG_DIR)) {
7743
+ (0, import_fs8.mkdirSync)(CONFIG_DIR, { recursive: true });
6692
7744
  }
6693
7745
  try {
6694
- (0, import_fs7.copyFileSync)(exampleFile, CONFIG_FILE);
7746
+ const yaml4 = require("js-yaml");
7747
+ const templateConfig = buildUsableMinimalTemplateConfig();
7748
+ const content = yaml4.dump(templateConfig, {
7749
+ indent: 2,
7750
+ lineWidth: -1,
7751
+ noRefs: true
7752
+ });
7753
+ (0, import_fs8.writeFileSync)(CONFIG_FILE, content, "utf-8");
6695
7754
  const action = force ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA";
6696
7755
  console.log(`\u2705 \u914D\u7F6E\u6587\u4EF6${action}\uFF1A${CONFIG_FILE}`);
6697
7756
  console.log("");
@@ -6707,10 +7766,22 @@ function initConfig2() {
6707
7766
  }
6708
7767
  }
6709
7768
  async function startForeground(port) {
7769
+ const targetPort = port ?? getPort();
7770
+ const healthy = await waitForService(targetPort, 500);
7771
+ const occupied = await isTcpPortOccupied(targetPort, 500);
7772
+ if (healthy && occupied && isServiceRunning()) {
7773
+ console.log(`\u2705 Service is already running on port ${targetPort}.`);
7774
+ console.log(" Use 'ctr status' to inspect it or 'ctr stop' before starting again.");
7775
+ return;
7776
+ }
7777
+ if (!healthy && occupied && !isServiceRunning()) {
7778
+ console.error(`\u274C Port ${targetPort} is already occupied by another service.`);
7779
+ process.exit(1);
7780
+ }
6710
7781
  console.log("\u{1F680} Starting Claude Trigger Router (foreground)...");
6711
7782
  console.log(" Press Ctrl+C to stop");
6712
7783
  try {
6713
- await run({ port });
7784
+ await run({ port: targetPort });
6714
7785
  } catch (error) {
6715
7786
  if (error.message?.includes("Invalid configuration")) {
6716
7787
  console.error("\n\u274C Configuration error. Run 'ctr init' to create a config file.");
@@ -6720,7 +7791,14 @@ async function startForeground(port) {
6720
7791
  process.exit(1);
6721
7792
  }
6722
7793
  }
6723
- function startDaemon(port) {
7794
+ async function startDaemon(port) {
7795
+ const targetPort = port ?? getPort();
7796
+ const healthy = await waitForService(targetPort, 500);
7797
+ const occupied = await isTcpPortOccupied(targetPort, 500);
7798
+ if (!healthy && occupied && !isServiceRunning()) {
7799
+ console.log(`\u274C Port ${targetPort} is already occupied by another service.`);
7800
+ return;
7801
+ }
6724
7802
  if (isServiceRunning()) {
6725
7803
  console.log("\u2705 Service is already running in the background.");
6726
7804
  return;
@@ -6731,30 +7809,58 @@ function startDaemon(port) {
6731
7809
  if (port) {
6732
7810
  childArgs.push("--port", String(port));
6733
7811
  }
6734
- const child = (0, import_child_process2.spawn)(nodeExec, childArgs, {
7812
+ const child = (0, import_child_process3.spawn)(nodeExec, childArgs, {
6735
7813
  detached: true,
6736
7814
  stdio: "ignore",
6737
7815
  env: { ...process.env, CTR_DAEMON: "1" }
6738
7816
  });
6739
7817
  child.unref();
6740
- const targetPort = port ?? getPort();
6741
- let waited = 0;
6742
- const interval = setInterval(() => {
6743
- waited += 500;
6744
- if (isServiceRunning()) {
6745
- clearInterval(interval);
6746
- console.log(`\u2705 Service started in background (port: ${targetPort})`);
6747
- console.log(` Run 'ctr stop' to stop it.`);
6748
- } else if (waited >= 5e3) {
6749
- clearInterval(interval);
6750
- console.log(`\u2705 Service launched in background (port: ${targetPort})`);
6751
- console.log(` If it fails to start, run 'ctr start' (without --daemon) to see errors.`);
6752
- }
6753
- }, 500);
6754
- }
6755
- function showStatus() {
7818
+ const startConfirmed = await new Promise((resolve, reject) => {
7819
+ let settled = false;
7820
+ const deadline = Date.now() + 5e3;
7821
+ const finish = (value) => {
7822
+ if (settled) {
7823
+ return;
7824
+ }
7825
+ settled = true;
7826
+ resolve(value);
7827
+ };
7828
+ child.once("error", reject);
7829
+ child.once("exit", () => finish(false));
7830
+ const poll = () => {
7831
+ if (settled) {
7832
+ return;
7833
+ }
7834
+ if (isServiceRunning()) {
7835
+ finish(true);
7836
+ return;
7837
+ }
7838
+ if (Date.now() >= deadline) {
7839
+ finish(false);
7840
+ return;
7841
+ }
7842
+ setTimeout(poll, 250);
7843
+ };
7844
+ poll();
7845
+ });
7846
+ if (!startConfirmed) {
7847
+ console.error(`\u274C Service failed to start in background (port: ${targetPort}).`);
7848
+ console.error(" Run 'ctr start' (without --daemon) to inspect the startup error.");
7849
+ process.exit(1);
7850
+ }
7851
+ console.log(`\u2705 Service started in background (port: ${targetPort})`);
7852
+ console.log(` Run 'ctr stop' to stop it.`);
7853
+ }
7854
+ async function showStatus() {
6756
7855
  const info = readServiceInfo();
6757
7856
  if (!info || !isServiceRunning()) {
7857
+ const targetPort = getPort();
7858
+ const healthy = await waitForService(targetPort, 500);
7859
+ const occupied = await isTcpPortOccupied(targetPort, 500);
7860
+ if (!healthy && occupied) {
7861
+ console.log(`\u26A0\uFE0F \u7AEF\u53E3 ${targetPort} \u5DF2\u88AB\u5176\u4ED6\u670D\u52A1\u5360\u7528\uFF0C\u5F53\u524D\u4E0D\u662F claude-trigger-router\u3002`);
7862
+ return;
7863
+ }
6758
7864
  console.log("\u23F9 \u670D\u52A1\u672A\u8FD0\u884C");
6759
7865
  return;
6760
7866
  }
@@ -6779,9 +7885,10 @@ function stopService() {
6779
7885
  console.error("\u274C \u505C\u6B62\u670D\u52A1\u5931\u8D25:", error.message);
6780
7886
  }
6781
7887
  }
6782
- function restartService() {
7888
+ async function restartService() {
6783
7889
  stopService();
6784
- setTimeout(() => startDaemon(getPort()), 1500);
7890
+ await new Promise((resolve) => setTimeout(resolve, 1500));
7891
+ await startDaemon(getPort());
6785
7892
  }
6786
7893
  async function runClaudeCode() {
6787
7894
  const port = getPort();
@@ -6796,14 +7903,16 @@ async function runClaudeCode() {
6796
7903
  console.log(" 1. Start service first: ctr start --daemon");
6797
7904
  console.log(" 2. Or start interactively in another terminal: ctr start");
6798
7905
  console.log("");
6799
- const proceed = process.env.CTR_AUTO_START === "1";
6800
- if (!proceed) {
6801
- process.exit(1);
6802
- }
7906
+ process.exit(1);
6803
7907
  }
6804
7908
  console.log(`\u{1F680} Starting Claude Code with Trigger Router (port: ${port})...`);
7909
+ if (!isClaudeCommandAvailable()) {
7910
+ console.error("\u274C \u672A\u68C0\u6D4B\u5230 Claude Code CLI\u3002");
7911
+ console.log(" \u8BF7\u5148\u5B89\u88C5\uFF1Anpm install -g @anthropic-ai/claude-code");
7912
+ process.exit(1);
7913
+ }
6805
7914
  const isWindows = process.platform === "win32";
6806
- const claude = (0, import_child_process2.spawn)("claude", [], {
7915
+ const claude = (0, import_child_process3.spawn)("claude", [], {
6807
7916
  stdio: "inherit",
6808
7917
  shell: isWindows,
6809
7918
  env: {
@@ -6822,10 +7931,18 @@ async function runClaudeCode() {
6822
7931
  process.exit(code || 0);
6823
7932
  });
6824
7933
  }
6825
- function openUI() {
7934
+ async function openUI() {
6826
7935
  const port = getPort();
6827
7936
  const url = `http://127.0.0.1:${port}/ui`;
7937
+ const healthy = await waitForService(port, 800);
6828
7938
  console.log(`\u{1F310} Opening UI at ${url}`);
7939
+ if (!healthy) {
7940
+ console.log("\u26A0\uFE0F \u5F53\u524D UI \u670D\u52A1\u672A\u5C31\u7EEA\uFF1B\u5982\u679C\u9875\u9762\u65E0\u6CD5\u6253\u5F00\uFF0C\u8BF7\u5148\u8FD0\u884C ctr start \u6216 ctr start --daemon\u3002");
7941
+ }
7942
+ if (process.env.CTR_UI_SKIP_OPEN === "1") {
7943
+ console.log(" Browser launch skipped by CTR_UI_SKIP_OPEN=1");
7944
+ return;
7945
+ }
6829
7946
  try {
6830
7947
  (0, import_openurl.default)(url);
6831
7948
  } catch (error) {
@@ -6838,12 +7955,15 @@ async function main() {
6838
7955
  case "setup":
6839
7956
  await runSetupCli();
6840
7957
  break;
7958
+ case "doctor":
7959
+ await runDoctorCli();
7960
+ break;
6841
7961
  case "init":
6842
7962
  initConfig2();
6843
7963
  break;
6844
7964
  case "start":
6845
7965
  if (isDaemonMode()) {
6846
- startDaemon(getPort());
7966
+ await startDaemon(getPort());
6847
7967
  } else {
6848
7968
  await startForeground(getPort());
6849
7969
  }
@@ -6852,7 +7972,7 @@ async function main() {
6852
7972
  stopService();
6853
7973
  break;
6854
7974
  case "status":
6855
- showStatus();
7975
+ await showStatus();
6856
7976
  break;
6857
7977
  case "version":
6858
7978
  await printVersion();
@@ -6861,13 +7981,14 @@ async function main() {
6861
7981
  printUpgradeGuidance();
6862
7982
  break;
6863
7983
  case "restart":
6864
- restartService();
7984
+ printRestartGuidanceHint();
7985
+ await restartService();
6865
7986
  break;
6866
7987
  case "code":
6867
7988
  await runClaudeCode();
6868
7989
  break;
6869
7990
  case "ui":
6870
- openUI();
7991
+ await openUI();
6871
7992
  break;
6872
7993
  case "help":
6873
7994
  case "--help":
@@ -6882,21 +8003,24 @@ async function main() {
6882
8003
  process.exit(command ? 1 : 0);
6883
8004
  }
6884
8005
  }
6885
- var import_child_process2, import_path7, import_openurl, import_fs7, PACKAGE_JSON_PATH, PACKAGE_PAGE_URL, PACKAGE_REGISTRY_LATEST_URL;
8006
+ var import_child_process3, import_path7, import_openurl, import_fs8, PACKAGE_JSON_PATH, PACKAGE_PAGE_URL, PACKAGE_REGISTRY_LATEST_URL, PACKAGE_REGISTRY_URL;
6886
8007
  var init_cli = __esm({
6887
8008
  "src/cli.ts"() {
6888
- import_child_process2 = require("child_process");
8009
+ import_child_process3 = require("child_process");
6889
8010
  import_path7 = require("path");
6890
8011
  import_openurl = __toESM(require("openurl"));
6891
- import_fs7 = require("fs");
8012
+ import_fs8 = require("fs");
6892
8013
  init_index();
6893
8014
  init_processCheck();
6894
8015
  init_constants();
6895
8016
  init_service_health();
6896
8017
  init_setup2();
8018
+ init_templates();
8019
+ init_doctor();
6897
8020
  PACKAGE_JSON_PATH = (0, import_path7.join)(__dirname, "..", "package.json");
6898
8021
  PACKAGE_PAGE_URL = "https://www.npmjs.com/package/@peterwangze/claude-trigger-router";
6899
8022
  PACKAGE_REGISTRY_LATEST_URL = "https://registry.npmjs.org/@peterwangze%2Fclaude-trigger-router/latest";
8023
+ PACKAGE_REGISTRY_URL = "https://registry.npmjs.org/";
6900
8024
  if (process.env.CTR_SKIP_MAIN !== "1") {
6901
8025
  main().catch((error) => {
6902
8026
  console.error("Error:", error);