@peterwangze/claude-trigger-router 1.0.3 → 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
@@ -32,7 +32,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
32
32
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
33
 
34
34
  // src/constants.ts
35
- var import_os, import_path, CONFIG_DIR, CONFIG_FILE, CONFIG_FILE_JSON, CONFIG_FILE_YML, HOME_DIR, PID_FILE, GOVERNANCE_TRACE_FILE, GOVERNANCE_TRACE_ARCHIVE_DIR, GOVERNANCE_EXPORT_HISTORY_FILE, GOVERNANCE_SNAPSHOT_DIR, GOVERNANCE_SCHEDULE_FILE, DEFAULT_CONFIG, DEFAULT_TRIGGER_CONFIG, DEFAULT_SMART_ROUTER_CONFIG, DEFAULT_GOVERNANCE_CONFIG;
35
+ var import_os, import_path, CONFIG_DIR, CONFIG_FILE, CONFIG_FILE_JSON, CONFIG_FILE_YML, HOME_DIR, PID_FILE, GOVERNANCE_TRACE_FILE, GOVERNANCE_TRACE_ARCHIVE_DIR, GOVERNANCE_EXPORT_HISTORY_FILE, GOVERNANCE_SNAPSHOT_DIR, GOVERNANCE_SCHEDULE_FILE, DEFAULT_CONFIG2, DEFAULT_TRIGGER_CONFIG, DEFAULT_SMART_ROUTER_CONFIG, DEFAULT_GOVERNANCE_CONFIG;
36
36
  var init_constants = __esm({
37
37
  "src/constants.ts"() {
38
38
  "use strict";
@@ -49,9 +49,9 @@ var init_constants = __esm({
49
49
  GOVERNANCE_EXPORT_HISTORY_FILE = (0, import_path.join)(CONFIG_DIR, "governance-metrics-export-history.json");
50
50
  GOVERNANCE_SNAPSHOT_DIR = (0, import_path.join)(CONFIG_DIR, "governance-metric-snapshots");
51
51
  GOVERNANCE_SCHEDULE_FILE = (0, import_path.join)(CONFIG_DIR, "governance-metric-schedules.json");
52
- DEFAULT_CONFIG = {
52
+ DEFAULT_CONFIG2 = {
53
53
  HOST: "127.0.0.1",
54
- PORT: 3456,
54
+ PORT: 5678,
55
55
  LOG: true,
56
56
  LOG_LEVEL: "debug",
57
57
  API_TIMEOUT_MS: 6e5,
@@ -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)) {
@@ -775,7 +826,7 @@ function validateConfig(config) {
775
826
  function normalizeAndValidateConfig(config = {}) {
776
827
  const normalizedConfig = deepMerge(
777
828
  {
778
- ...DEFAULT_CONFIG,
829
+ ...DEFAULT_CONFIG2,
779
830
  Router: {
780
831
  default: ""
781
832
  },
@@ -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;
@@ -1404,6 +1485,7 @@ var CONTEXT_ALIGNMENT_PROMPT, ContextAlignmentService, contextAlignmentService;
1404
1485
  var init_context_alignment = __esm({
1405
1486
  "src/governance/context-alignment.ts"() {
1406
1487
  "use strict";
1488
+ init_constants();
1407
1489
  init_log();
1408
1490
  init_message_ir();
1409
1491
  init_anthropic();
@@ -1432,7 +1514,7 @@ Do not include markdown fences.`;
1432
1514
  buildPrompt(text, previousModel, nextModel) {
1433
1515
  return CONTEXT_ALIGNMENT_PROMPT.replace("{previousModel}", previousModel).replace("{nextModel}", nextModel).replace("{request}", text);
1434
1516
  }
1435
- async summarizeTransition(text, previousModel, nextModel, config, port = 3456, fetchFn, apiKey, timeoutMs) {
1517
+ async summarizeTransition(text, previousModel, nextModel, config, port = DEFAULT_CONFIG2.PORT, fetchFn, apiKey, timeoutMs) {
1436
1518
  if (!config.enabled || !config.summarizer_model || !text.trim()) {
1437
1519
  return null;
1438
1520
  }
@@ -1656,6 +1738,7 @@ var SemanticRouter, semanticRouter;
1656
1738
  var init_semantic_router = __esm({
1657
1739
  "src/governance/semantic-router.ts"() {
1658
1740
  "use strict";
1741
+ init_constants();
1659
1742
  init_log();
1660
1743
  init_message_ir();
1661
1744
  init_anthropic();
@@ -1714,7 +1797,7 @@ Return JSON only:
1714
1797
  analyze(text, config) {
1715
1798
  return this.analyzeEmbedding(text, config);
1716
1799
  }
1717
- async analyzeWithClassifier(text, config, port = 3456, fetchFn, apiKey, timeoutMs) {
1800
+ async analyzeWithClassifier(text, config, port = DEFAULT_CONFIG2.PORT, fetchFn, apiKey, timeoutMs) {
1718
1801
  if (!config?.enabled || config.mode !== "classifier" || !config.prototypes || Object.keys(config.prototypes).length === 0) {
1719
1802
  return this.analyzeEmbedding(text, config);
1720
1803
  }
@@ -1778,6 +1861,7 @@ var SHADOW_VERIFIER_PROMPT, ShadowSupervisor, shadowSupervisor;
1778
1861
  var init_shadow_supervisor = __esm({
1779
1862
  "src/governance/shadow-supervisor.ts"() {
1780
1863
  "use strict";
1864
+ init_constants();
1781
1865
  init_log();
1782
1866
  init_message_ir();
1783
1867
  init_anthropic();
@@ -1852,7 +1936,7 @@ If no issue is found, return:
1852
1936
  }
1853
1937
  });
1854
1938
  }
1855
- async inspectWithVerifier(payload, config, port = 3456, fetchFn, apiKey, timeoutMs) {
1939
+ async inspectWithVerifier(payload, config, port = DEFAULT_CONFIG2.PORT, fetchFn, apiKey, timeoutMs) {
1856
1940
  const text = extractText(payload).trim();
1857
1941
  if (!config.enabled || !config.verifier_model || !text) {
1858
1942
  return this.inspect(payload, config);
@@ -2118,17 +2202,17 @@ var init_SSEParser_transform = __esm({
2118
2202
 
2119
2203
  // src/governance/stream-response-governance.ts
2120
2204
  function serializeEvent(event2) {
2121
- let output2 = "";
2205
+ let output3 = "";
2122
2206
  if (event2.event) {
2123
- output2 += `event: ${event2.event}
2207
+ output3 += `event: ${event2.event}
2124
2208
  `;
2125
2209
  }
2126
2210
  if (event2.data !== void 0) {
2127
- 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)}
2128
2212
  `;
2129
2213
  }
2130
- output2 += "\n";
2131
- return new TextEncoder().encode(output2);
2214
+ output3 += "\n";
2215
+ return new TextEncoder().encode(output3);
2132
2216
  }
2133
2217
  async function collectSSE(stream) {
2134
2218
  const parser = new SSEParserTransform();
@@ -2238,17 +2322,17 @@ var init_stream_response_governance = __esm({
2238
2322
  });
2239
2323
 
2240
2324
  // src/governance/metrics.ts
2241
- function normalizeAnomalyThresholds(input2) {
2325
+ function normalizeAnomalyThresholds(input3) {
2242
2326
  return {
2243
- minSampleSize: input2?.minSampleSize ?? DEFAULT_ANOMALY_THRESHOLDS.minSampleSize,
2244
- cascadeWarnRate: input2?.cascadeWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.cascadeWarnRate,
2245
- cascadeCriticalRate: input2?.cascadeCriticalRate ?? DEFAULT_ANOMALY_THRESHOLDS.cascadeCriticalRate,
2246
- shadowWarnRate: input2?.shadowWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.shadowWarnRate,
2247
- shadowCriticalRate: input2?.shadowCriticalRate ?? DEFAULT_ANOMALY_THRESHOLDS.shadowCriticalRate,
2248
- latencyWarnMs: input2?.latencyWarnMs ?? DEFAULT_ANOMALY_THRESHOLDS.latencyWarnMs,
2249
- latencyCriticalMs: input2?.latencyCriticalMs ?? DEFAULT_ANOMALY_THRESHOLDS.latencyCriticalMs,
2250
- spikeWarnRate: input2?.spikeWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.spikeWarnRate,
2251
- 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
2252
2336
  };
2253
2337
  }
2254
2338
  function rate(count, total) {
@@ -3259,7 +3343,7 @@ var init_server = __esm({
3259
3343
  server.app.post("/api/restart", async (req, reply) => {
3260
3344
  reply.send({ success: true, message: "Service restart initiated" });
3261
3345
  setTimeout(() => {
3262
- const { spawn: spawn2 } = require("child_process");
3346
+ const { spawn: spawn3 } = require("child_process");
3263
3347
  const { join: join8 } = require("path");
3264
3348
  const cliPath = join8(__dirname, "cli.js");
3265
3349
  const currentPort = config.initialConfig?.PORT;
@@ -3267,7 +3351,7 @@ var init_server = __esm({
3267
3351
  if (currentPort) {
3268
3352
  restartArgs.push("--port", String(currentPort));
3269
3353
  }
3270
- spawn2(process.execPath, restartArgs, {
3354
+ spawn3(process.execPath, restartArgs, {
3271
3355
  detached: true,
3272
3356
  stdio: "ignore"
3273
3357
  }).unref();
@@ -3527,7 +3611,7 @@ function isServiceRunning() {
3527
3611
  function savePid(pid, port) {
3528
3612
  const info = {
3529
3613
  pid,
3530
- port: port ?? 3456,
3614
+ port: port ?? DEFAULT_CONFIG2.PORT,
3531
3615
  startTime: (/* @__PURE__ */ new Date()).toISOString()
3532
3616
  };
3533
3617
  (0, import_fs4.writeFileSync)(PID_FILE, JSON.stringify(info, null, 2), "utf-8");
@@ -3537,7 +3621,7 @@ function readServiceInfo() {
3537
3621
  try {
3538
3622
  const content = (0, import_fs4.readFileSync)(PID_FILE, "utf-8").trim();
3539
3623
  if (/^\d+$/.test(content)) {
3540
- return { pid: parseInt(content, 10), port: 3456, startTime: "" };
3624
+ return { pid: parseInt(content, 10), port: DEFAULT_CONFIG2.PORT, startTime: "" };
3541
3625
  }
3542
3626
  return JSON.parse(content);
3543
3627
  } catch {
@@ -3573,18 +3657,18 @@ var init_SSESerializer_transform = __esm({
3573
3657
  constructor() {
3574
3658
  const transformStream = new TransformStream({
3575
3659
  transform: (event2, controller) => {
3576
- let output2 = "";
3660
+ let output3 = "";
3577
3661
  if (event2.event) {
3578
- output2 += `event: ${event2.event}
3662
+ output3 += `event: ${event2.event}
3579
3663
  `;
3580
3664
  }
3581
3665
  if (event2.data) {
3582
3666
  const dataStr = typeof event2.data === "string" ? event2.data : JSON.stringify(event2.data);
3583
- output2 += `data: ${dataStr}
3667
+ output3 += `data: ${dataStr}
3584
3668
  `;
3585
3669
  }
3586
- output2 += "\n";
3587
- controller.enqueue(new TextEncoder().encode(output2));
3670
+ output3 += "\n";
3671
+ controller.enqueue(new TextEncoder().encode(output3));
3588
3672
  }
3589
3673
  });
3590
3674
  this.readable = transformStream.readable;
@@ -4130,6 +4214,7 @@ var init_intent = __esm({
4130
4214
  "src/trigger/intent.ts"() {
4131
4215
  "use strict";
4132
4216
  import_lru_cache6 = require("lru-cache");
4217
+ init_constants();
4133
4218
  init_log();
4134
4219
  intentCache = new import_lru_cache6.LRUCache({
4135
4220
  max: 500,
@@ -4198,7 +4283,7 @@ Important: Respond ONLY with the JSON, no additional text.`;
4198
4283
  * @param fetchFn fetch 函数(用于发起 API 请求)
4199
4284
  * @returns 意图识别结果
4200
4285
  */
4201
- async detectIntent(text, config, port = 3456, fetchFn, apiKey, timeoutMs) {
4286
+ async detectIntent(text, config, port = DEFAULT_CONFIG2.PORT, fetchFn, apiKey, timeoutMs) {
4202
4287
  if (!config.llm_intent_recognition) {
4203
4288
  return {
4204
4289
  intent: "general",
@@ -4286,6 +4371,7 @@ var init_smart_router = __esm({
4286
4371
  "src/trigger/smart-router.ts"() {
4287
4372
  "use strict";
4288
4373
  import_lru_cache7 = require("lru-cache");
4374
+ init_constants();
4289
4375
  init_log();
4290
4376
  init_message_ir();
4291
4377
  init_anthropic();
@@ -4349,11 +4435,11 @@ Important:
4349
4435
  *
4350
4436
  * @param text 请求文本
4351
4437
  * @param config SmartRouter 配置
4352
- * @param port 本地服务端口(默认 3456
4438
+ * @param port 本地服务端口(默认 5678
4353
4439
  * @param fetchFn 可注入的 fetch 函数(用于测试)
4354
4440
  * @returns 选择结果,失败时返回 null
4355
4441
  */
4356
- async selectModel(text, config, port = 3456, fetchFn, apiKey, timeoutMs) {
4442
+ async selectModel(text, config, port = DEFAULT_CONFIG2.PORT, fetchFn, apiKey, timeoutMs) {
4357
4443
  if (!config.enabled) {
4358
4444
  return null;
4359
4445
  }
@@ -4372,6 +4458,7 @@ Important:
4372
4458
  method: "POST",
4373
4459
  headers: {
4374
4460
  "Content-Type": "application/json",
4461
+ "x-ctr-smart-router": "1",
4375
4462
  ...apiKey ? { "x-api-key": apiKey } : {}
4376
4463
  },
4377
4464
  body: JSON.stringify(
@@ -4428,6 +4515,7 @@ var init_selector = __esm({
4428
4515
  init_intent();
4429
4516
  init_smart_router();
4430
4517
  init_log();
4518
+ init_constants();
4431
4519
  init_session_store();
4432
4520
  init_semantic_router();
4433
4521
  init_compile();
@@ -4484,7 +4572,7 @@ var init_selector = __esm({
4484
4572
  * @param config 触发配置
4485
4573
  * @returns 分析结果
4486
4574
  */
4487
- async selectModel(req, config, port = 3456, smartRouterConfig, governanceConfig, apiKey, timeoutMs) {
4575
+ async selectModel(req, config, port = DEFAULT_CONFIG2.PORT, smartRouterConfig, governanceConfig, apiKey, timeoutMs) {
4488
4576
  const startTime = Date.now();
4489
4577
  const appConfig = req.appConfig;
4490
4578
  if (!config.enabled) {
@@ -4683,10 +4771,11 @@ var init_trigger = __esm({
4683
4771
  init_selector();
4684
4772
  init_analyzer();
4685
4773
  init_log();
4774
+ init_constants();
4686
4775
  TriggerRouter = class {
4687
4776
  config = null;
4688
4777
  appConfig = null;
4689
- port = 3456;
4778
+ port = DEFAULT_CONFIG2.PORT;
4690
4779
  smartRouterConfig = void 0;
4691
4780
  governanceConfig = void 0;
4692
4781
  apiKey;
@@ -4699,7 +4788,7 @@ var init_trigger = __esm({
4699
4788
  init(appConfig) {
4700
4789
  this.appConfig = appConfig;
4701
4790
  this.config = appConfig.TriggerRouter || this.getDefaultConfig();
4702
- this.port = appConfig.PORT || 3456;
4791
+ this.port = appConfig.PORT || DEFAULT_CONFIG2.PORT;
4703
4792
  this.smartRouterConfig = appConfig.SmartRouter;
4704
4793
  this.governanceConfig = appConfig.Governance;
4705
4794
  this.apiKey = appConfig.APIKEY;
@@ -4882,6 +4971,15 @@ function toOpenAIToolResultMessages(parts) {
4882
4971
  content: typeof part.content === "string" ? part.content : JSON.stringify(part.content)
4883
4972
  }));
4884
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
+ }
4885
4983
  function toOpenAITools(tools) {
4886
4984
  if (!Array.isArray(tools) || !tools.length) {
4887
4985
  return void 0;
@@ -4889,9 +4987,9 @@ function toOpenAITools(tools) {
4889
4987
  return tools.map((tool) => ({
4890
4988
  type: "function",
4891
4989
  function: {
4892
- name: tool.name,
4893
- description: tool.description,
4894
- parameters: tool.input_schema
4990
+ name: getToolName(tool),
4991
+ description: getToolDescription(tool),
4992
+ parameters: getToolInputSchema(tool)
4895
4993
  }
4896
4994
  }));
4897
4995
  }
@@ -4918,10 +5016,10 @@ function toOpenAIToolChoice(toolChoice) {
4918
5016
  }
4919
5017
  return toolChoice;
4920
5018
  }
4921
- function toOpenAIChatRequest(input2) {
5019
+ function toOpenAIChatRequest(input3) {
4922
5020
  const messages = [
4923
- ...input2.ir.system.map((text) => ({ role: "system", content: text })),
4924
- ...input2.ir.messages.flatMap((message) => {
5021
+ ...input3.ir.system.map((text) => ({ role: "system", content: text })),
5022
+ ...input3.ir.messages.flatMap((message) => {
4925
5023
  const content = toOpenAIContent(message.parts);
4926
5024
  const toolCalls = message.role === "assistant" ? toOpenAIToolCalls(message.parts) : [];
4927
5025
  const toolResults = toOpenAIToolResultMessages(message.parts);
@@ -4940,26 +5038,26 @@ function toOpenAIChatRequest(input2) {
4940
5038
  })
4941
5039
  ];
4942
5040
  const body = {
4943
- model: input2.model,
5041
+ model: input3.model,
4944
5042
  messages
4945
5043
  };
4946
- if (input2.max_completion_tokens !== void 0) {
4947
- 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;
4948
5046
  }
4949
- if (input2.stream !== void 0) {
4950
- body.stream = input2.stream;
5047
+ if (input3.stream !== void 0) {
5048
+ body.stream = input3.stream;
4951
5049
  }
4952
- const tools = toOpenAITools(input2.tools);
5050
+ const tools = toOpenAITools(input3.tools);
4953
5051
  if (tools) {
4954
5052
  body.tools = tools;
4955
5053
  }
4956
- const toolChoice = toOpenAIToolChoice(input2.tool_choice);
5054
+ const toolChoice = toOpenAIToolChoice(input3.tool_choice);
4957
5055
  if (toolChoice !== void 0) {
4958
5056
  body.tool_choice = toolChoice;
4959
5057
  }
4960
- if (input2.ir.options?.thinking?.enabled) {
5058
+ if (input3.ir.options?.thinking?.enabled) {
4961
5059
  body.reasoning = {
4962
- ...input2.ir.options.thinking.effort ? { effort: input2.ir.options.thinking.effort } : {}
5060
+ ...input3.ir.options.thinking.effort ? { effort: input3.ir.options.thinking.effort } : {}
4963
5061
  };
4964
5062
  }
4965
5063
  return body;
@@ -4984,19 +5082,19 @@ function stringifyFallbackContent(value) {
4984
5082
  return String(value);
4985
5083
  }
4986
5084
  }
4987
- function applyCapabilityFallbacks(input2) {
5085
+ function applyCapabilityFallbacks(input3) {
4988
5086
  const diagnostics = [];
4989
- const nextRequest = { ...input2.request };
5087
+ const nextRequest = { ...input3.request };
4990
5088
  const nextIR = {
4991
- ...input2.ir,
4992
- system: [...input2.ir.system],
4993
- messages: input2.ir.messages.map((message) => ({
5089
+ ...input3.ir,
5090
+ system: [...input3.ir.system],
5091
+ messages: input3.ir.messages.map((message) => ({
4994
5092
  ...message,
4995
5093
  parts: message.parts.map((part) => ({ ...part }))
4996
5094
  })),
4997
- options: input2.ir.options ? { ...input2.ir.options } : void 0
5095
+ options: input3.ir.options ? { ...input3.ir.options } : void 0
4998
5096
  };
4999
- if (input2.capabilities?.thinking.supported === false && nextIR.options?.thinking) {
5097
+ if (input3.capabilities?.thinking.supported === false && nextIR.options?.thinking) {
5000
5098
  diagnostics.push("thinking_ignored");
5001
5099
  delete nextIR.options.thinking;
5002
5100
  delete nextRequest.thinking;
@@ -5007,7 +5105,7 @@ function applyCapabilityFallbacks(input2) {
5007
5105
  const hasImageParts = nextIR.messages.some(
5008
5106
  (message) => message.parts.some((part) => part.type === "image")
5009
5107
  );
5010
- if (input2.capabilities?.images === false && hasImageParts) {
5108
+ if (input3.capabilities?.images === false && hasImageParts) {
5011
5109
  diagnostics.push("images_text_fallback");
5012
5110
  nextIR.messages = nextIR.messages.map((message) => ({
5013
5111
  ...message,
@@ -5027,7 +5125,7 @@ function applyCapabilityFallbacks(input2) {
5027
5125
  const hasToolParts = nextIR.messages.some(
5028
5126
  (message) => message.parts.some((part) => part.type === "tool_call" || part.type === "tool_result")
5029
5127
  );
5030
- 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)) {
5031
5129
  diagnostics.push("tools_text_fallback");
5032
5130
  delete nextRequest.tools;
5033
5131
  delete nextRequest.tool_choice;
@@ -5073,53 +5171,58 @@ function omitRequestFields(body) {
5073
5171
  } = body;
5074
5172
  return rest;
5075
5173
  }
5076
- function buildUpstreamRequestFromIR(input2) {
5174
+ function buildProviderDispatchRequestFromIR(input3) {
5077
5175
  const fallback = applyCapabilityFallbacks({
5078
- ir: input2.ir,
5079
- request: input2.request,
5080
- capabilities: input2.capabilities
5176
+ ir: input3.ir,
5177
+ request: input3.request,
5178
+ capabilities: input3.capabilities
5081
5179
  });
5082
5180
  const passthrough = omitRequestFields(fallback.request);
5083
- if (input2.interface === "anthropic") {
5181
+ const dispatchFormat = getDispatchFormatForProfile(input3.interface, input3.compatibilityProfile);
5182
+ if (dispatchFormat === "openai_chat") {
5084
5183
  return {
5184
+ dispatchFormat,
5085
5185
  diagnostics: fallback.diagnostics,
5086
5186
  ...passthrough,
5087
- ...toAnthropicMessagesRequest({
5088
- model: input2.model,
5089
- 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,
5090
5190
  stream: fallback.request.stream,
5091
- metadata: fallback.request.metadata,
5092
5191
  tools: fallback.request.tools,
5192
+ tool_choice: fallback.request.tool_choice,
5093
5193
  ir: fallback.ir
5094
5194
  })
5095
5195
  };
5096
5196
  }
5097
5197
  return {
5198
+ dispatchFormat,
5098
5199
  diagnostics: fallback.diagnostics,
5099
5200
  ...passthrough,
5100
- ...toOpenAIChatRequest({
5101
- model: input2.model,
5102
- 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,
5103
5204
  stream: fallback.request.stream,
5205
+ metadata: fallback.request.metadata,
5104
5206
  tools: fallback.request.tools,
5105
- tool_choice: fallback.request.tool_choice,
5106
5207
  ir: fallback.ir
5107
5208
  })
5108
5209
  };
5109
5210
  }
5110
- function buildUpstreamRequest(input2) {
5111
- const ir = createMessageIR(input2.request);
5112
- const { diagnostics, ...body } = buildUpstreamRequestFromIR({
5113
- model: input2.model,
5114
- interface: input2.interface,
5115
- 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,
5116
5218
  ir,
5117
- capabilities: input2.capabilities
5219
+ capabilities: input3.capabilities
5118
5220
  });
5119
5221
  return {
5120
5222
  ir,
5121
5223
  body,
5122
- diagnostics
5224
+ diagnostics,
5225
+ dispatchFormat
5123
5226
  };
5124
5227
  }
5125
5228
  var init_protocols = __esm({
@@ -5128,6 +5231,7 @@ var init_protocols = __esm({
5128
5231
  init_message_ir();
5129
5232
  init_anthropic();
5130
5233
  init_openai();
5234
+ init_compile();
5131
5235
  }
5132
5236
  });
5133
5237
 
@@ -5171,7 +5275,7 @@ async function run(options = {}) {
5171
5275
  HOST = "127.0.0.1";
5172
5276
  logWarn("\u26A0\uFE0F API key is not set. HOST is forced to 127.0.0.1.");
5173
5277
  }
5174
- const port = options.port ?? config.PORT ?? 3456;
5278
+ const port = options.port ?? config.PORT ?? DEFAULT_CONFIG.PORT;
5175
5279
  savePid(process.pid, port);
5176
5280
  process.on("SIGINT", () => {
5177
5281
  log("Received SIGINT, cleaning up...");
@@ -5195,7 +5299,7 @@ async function run(options = {}) {
5195
5299
  const hour = pad(date.getHours());
5196
5300
  const minute = pad(date.getMinutes());
5197
5301
  const seconds = pad(date.getSeconds());
5198
- return `./logs/ctr-${month}${day}${hour}${minute}${seconds}${index ? `_${index}` : ""}.log`;
5302
+ return `ctr-${month}${day}${hour}${minute}${seconds}${index ? `_${index}` : ""}.log`;
5199
5303
  };
5200
5304
  const loggerConfig = config.LOG !== false ? {
5201
5305
  level: config.LOG_LEVEL || "debug",
@@ -5206,10 +5310,11 @@ async function run(options = {}) {
5206
5310
  compress: "gzip"
5207
5311
  })
5208
5312
  } : false;
5313
+ const registry = buildModelRegistry(config);
5209
5314
  const server = createServer({
5210
- jsonPath: CONFIG_FILE,
5315
+ useJsonFile: false,
5211
5316
  initialConfig: {
5212
- providers: config.Providers,
5317
+ providers: registry.providers,
5213
5318
  HOST,
5214
5319
  PORT: servicePort,
5215
5320
  LOG_FILE: (0, import_path5.join)(
@@ -5245,9 +5350,10 @@ async function run(options = {}) {
5245
5350
  initialModel: req.body?.model
5246
5351
  });
5247
5352
  appendTraceReason(req.governanceTrace, "request_received");
5248
- 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);
5249
5355
  req.triggerResult = triggerResult;
5250
- if (triggerResult.matched && triggerResult.model) {
5356
+ if (!bypassTriggerRouter && triggerResult.matched && triggerResult.model) {
5251
5357
  const previousSessionState = req.sessionId ? sessionStateStore.get(req.sessionId) : void 0;
5252
5358
  const previousModel = previousSessionState?.lastSuccessfulModel;
5253
5359
  const alignmentConfig = config.Governance?.sticky?.alignment;
@@ -5312,9 +5418,10 @@ async function run(options = {}) {
5312
5418
  const compiledModel = getCompiledModelRef(config, req.body?.model);
5313
5419
  if (compiledModel?.interface && req.body?.messages) {
5314
5420
  const originalBody = cloneRequestBody(req.body);
5315
- const upstream = buildUpstreamRequest({
5421
+ const upstream = buildProviderDispatchRequest({
5316
5422
  model: compiledModel.modelName,
5317
5423
  interface: compiledModel.interface,
5424
+ compatibilityProfile: compiledModel.compatibilityProfile,
5318
5425
  request: originalBody,
5319
5426
  capabilities: compiledModel.capabilities
5320
5427
  });
@@ -5501,7 +5608,7 @@ async function run(options = {}) {
5501
5608
  event.emit("onSend", req, reply, payload);
5502
5609
  return payload;
5503
5610
  });
5504
- server.start();
5611
+ await server.start();
5505
5612
  }
5506
5613
  var import_fs5, import_promises2, import_os2, import_path5, import_json5, import_node_events, import_rotating_file_stream, event;
5507
5614
  var init_index = __esm({
@@ -5535,32 +5642,32 @@ var init_index = __esm({
5535
5642
  });
5536
5643
 
5537
5644
  // src/setup/service.ts
5538
- function decideServiceAction(input2) {
5539
- if (input2.detectedService.kind === "non_self_occupied") {
5645
+ function decideServiceAction(input3) {
5646
+ if (input3.detectedService.kind === "non_self_occupied") {
5540
5647
  throw new Error("target port is occupied by another service");
5541
5648
  }
5542
- if (input2.detectedService.kind === "none") {
5649
+ if (input3.detectedService.kind === "none") {
5543
5650
  return { kind: "start" };
5544
5651
  }
5545
- if (input2.detectedService.kind === "self_unhealthy") {
5652
+ if (input3.detectedService.kind === "self_unhealthy") {
5546
5653
  return { kind: "restart" };
5547
5654
  }
5548
- if (input2.configChanged && input2.detectedService.kind === "self_healthy") {
5549
- return input2.reloadSupported ? { kind: "reload" } : { kind: "restart" };
5655
+ if (input3.configChanged && input3.detectedService.kind === "self_healthy") {
5656
+ return input3.reloadSupported ? { kind: "reload" } : { kind: "restart" };
5550
5657
  }
5551
5658
  return { kind: "reuse" };
5552
5659
  }
5553
- async function applyServiceAction(input2) {
5554
- if (input2.action.kind === "start") {
5555
- await input2.executeStart();
5660
+ async function applyServiceAction(input3) {
5661
+ if (input3.action.kind === "start") {
5662
+ await input3.executeStart();
5556
5663
  }
5557
- if (input2.action.kind === "reload") {
5558
- await input2.executeReload();
5664
+ if (input3.action.kind === "reload") {
5665
+ await input3.executeReload();
5559
5666
  }
5560
- if (input2.action.kind === "restart") {
5561
- await input2.executeRestart();
5667
+ if (input3.action.kind === "restart") {
5668
+ await input3.executeRestart();
5562
5669
  }
5563
- const healthy = await input2.verifyHealth();
5670
+ const healthy = await input3.verifyHealth();
5564
5671
  if (!healthy) {
5565
5672
  throw new Error("service health check failed");
5566
5673
  }
@@ -5610,9 +5717,12 @@ function inferProtocolFromApiBaseUrl(apiBaseUrl) {
5610
5717
  }
5611
5718
  return "openai";
5612
5719
  }
5720
+ function normalizeSegment(value) {
5721
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
5722
+ }
5613
5723
  function toModelId(name, model, index) {
5614
- const normalizedName = name.trim() || `provider_${index + 1}`;
5615
- 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);
5616
5726
  return normalizedModel ? `${normalizedName}_${normalizedModel}` : normalizedName;
5617
5727
  }
5618
5728
  function createEmptyDraft() {
@@ -5627,66 +5737,176 @@ function createNonMigratableResult() {
5627
5737
  draft: createEmptyDraft(),
5628
5738
  skippedFields: [],
5629
5739
  needsCompletion: true,
5630
- missingFields: ["defaultModel", "apiKey"]
5740
+ missingFields: ["defaultModel", "apiKey", "apiBaseUrl"]
5631
5741
  };
5632
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
+ }
5633
5757
  function isLegacyProviderInput(value) {
5634
5758
  return typeof value === "object" && value !== null;
5635
5759
  }
5636
- function migrateLegacyConfig(input2) {
5637
- if (!Array.isArray(input2.providers)) {
5638
- 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);
5639
5766
  }
5640
- if (!input2.providers.every(isLegacyProviderInput)) {
5641
- 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;
5642
5778
  }
5643
5779
  const skippedFields = [];
5644
- 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) => {
5645
5792
  if (provider.transformer !== void 0) {
5646
- skippedFields.push(`providers[${index}].transformer`);
5793
+ pushUnique(skippedFields, `${providerKey}[${index}].transformer`);
5794
+ }
5795
+ if (provider.headers !== void 0) {
5796
+ pushUnique(skippedFields, `${providerKey}[${index}].headers`);
5647
5797
  }
5648
5798
  return {
5649
5799
  name: provider.name ?? "",
5650
5800
  api_base_url: provider.api_base_url,
5651
5801
  api_key: provider.api_key ?? "",
5652
- models: Array.isArray(provider.models) ? provider.models : []
5802
+ models: Array.isArray(provider.models) ? provider.models : [],
5803
+ vendor_hint: inferVendorHintFromLegacyProvider(provider)
5653
5804
  };
5654
5805
  });
5655
- 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(
5656
5846
  (provider, providerIndex) => (provider.models.length ? provider.models : [""]).map((model) => ({
5657
- id: toModelId(provider.name, model, providerIndex),
5847
+ candidateId: toModelId(provider.name, model, providerIndex),
5658
5848
  api: provider.api_base_url,
5659
5849
  api_base_url: provider.api_base_url,
5660
5850
  key: provider.api_key,
5661
5851
  api_key: provider.api_key,
5662
5852
  interface: inferProtocolFromApiBaseUrl(provider.api_base_url),
5663
5853
  protocol: inferProtocolFromApiBaseUrl(provider.api_base_url),
5664
- model
5665
- }))
5666
- ).filter((item) => item.model);
5667
- if (input2.trigger_router !== void 0) {
5668
- skippedFields.push("trigger_router");
5669
- }
5670
- const hasDefaultModel = typeof input2.default === "string" && input2.default.length > 0;
5671
- const defaultModelId = hasDefaultModel ? (() => {
5672
- const [providerName, modelName] = String(input2.default).split(",");
5673
- 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;
5674
5890
  })() : void 0;
5675
- 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);
5676
5893
  const missingFields = [];
5677
- if (!hasDefaultModel) {
5894
+ if (!defaultModelId) {
5678
5895
  missingFields.push("defaultModel");
5679
5896
  }
5680
5897
  if (hasMissingApiKey) {
5681
5898
  missingFields.push("apiKey");
5682
5899
  }
5900
+ if (hasMissingApiBaseUrl) {
5901
+ missingFields.push("apiBaseUrl");
5902
+ }
5683
5903
  return {
5684
5904
  draft: {
5685
5905
  Providers: [],
5686
5906
  Models: models,
5687
- Router: hasDefaultModel && defaultModelId ? { default: defaultModelId } : {}
5907
+ Router: defaultModelId ? { default: defaultModelId } : {}
5688
5908
  },
5689
- skippedFields,
5909
+ skippedFields: normalized.skippedFields,
5690
5910
  needsCompletion: missingFields.length > 0,
5691
5911
  missingFields
5692
5912
  };
@@ -5751,11 +5971,12 @@ function getProviderPreset2(key) {
5751
5971
  api: preset.api,
5752
5972
  api_base_url: preset.api_base_url,
5753
5973
  interface: preset.interface,
5754
- protocol: preset.protocol
5974
+ protocol: preset.protocol,
5975
+ default_thinking: preset.default_thinking
5755
5976
  };
5756
5977
  }
5757
- function buildMinimalConfig(input2) {
5758
- const providers = input2.providers;
5978
+ function buildMinimalConfig(input3) {
5979
+ const providers = input3.providers;
5759
5980
  const models = providers.map((p) => {
5760
5981
  const preset = p.preset ? getProviderPreset2(p.preset) : void 0;
5761
5982
  const modelDraft = {
@@ -5775,15 +5996,15 @@ function buildMinimalConfig(input2) {
5775
5996
  }
5776
5997
  return modelDraft;
5777
5998
  });
5778
- let defaultModel = input2.defaultModel?.trim();
5779
- if (input2.defaultModel && input2.defaultModel.includes(",")) {
5780
- 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(",");
5781
6002
  const matched = models.find((item) => item.id === providerName && item.model === modelName);
5782
6003
  if (matched) {
5783
6004
  defaultModel = matched.id;
5784
6005
  }
5785
6006
  }
5786
- if (input2.defaultModel === void 0 && models.length > 0) {
6007
+ if (input3.defaultModel === void 0 && models.length > 0) {
5787
6008
  const firstModelId = models[0].id;
5788
6009
  if (firstModelId && models[0].model) {
5789
6010
  defaultModel = firstModelId;
@@ -5797,31 +6018,68 @@ function buildMinimalConfig(input2) {
5797
6018
  Router: defaultModel ? { default: defaultModel } : {}
5798
6019
  };
5799
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
+ }
5800
6057
  var init_templates = __esm({
5801
6058
  "src/setup/templates.ts"() {
5802
6059
  "use strict";
6060
+ init_constants();
5803
6061
  init_provider_presets();
5804
6062
  }
5805
6063
  });
5806
6064
 
5807
6065
  // src/setup/persist.ts
5808
- async function persistSetupConfig(input2) {
5809
- const errors = input2.validateConfig(input2.config);
6066
+ async function persistSetupConfig(input3) {
6067
+ const errors = input3.validateConfig(input3.config);
5810
6068
  if (errors.length > 0) {
5811
6069
  throw new Error("config validation failed");
5812
6070
  }
5813
6071
  let backupPath;
5814
- if (input2.hasExistingConfig) {
5815
- const createdBackupPath = await input2.backupCurrentConfig();
6072
+ if (input3.hasExistingConfig) {
6073
+ const createdBackupPath = await input3.backupCurrentConfig();
5816
6074
  if (!createdBackupPath) {
5817
6075
  throw new Error("failed to back up existing config");
5818
6076
  }
5819
6077
  backupPath = createdBackupPath;
5820
6078
  }
5821
- await input2.writeConfig(input2.config);
6079
+ await input3.writeConfig(input3.config);
5822
6080
  return {
5823
6081
  configChanged: true,
5824
- configPath: input2.currentConfigPath,
6082
+ configPath: input3.currentConfigPath,
5825
6083
  backupPath
5826
6084
  };
5827
6085
  }
@@ -5870,8 +6128,8 @@ function ensureLegacyFlow(detection, legacyConfigAction) {
5870
6128
  function invalidAction() {
5871
6129
  return invalidCurrentAction();
5872
6130
  }
5873
- function decideSetupBranch(input2) {
5874
- const { detection, currentConfigAction, legacyConfigAction } = input2;
6131
+ function decideSetupBranch(input3) {
6132
+ const { detection, currentConfigAction, legacyConfigAction } = input3;
5875
6133
  if (currentConfigAction === "cancel") {
5876
6134
  ensureNoLegacyAction(legacyConfigAction);
5877
6135
  return { kind: "cancelled" };
@@ -5924,6 +6182,28 @@ function getTargetConfigPath(detection) {
5924
6182
  }
5925
6183
  return CONFIG_FILE;
5926
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
+ }
5927
6207
  async function runSetup(deps) {
5928
6208
  const detection = await deps.detectSetupEnvironment();
5929
6209
  const currentConfigAction = await deps.chooseCurrentConfigAction({
@@ -5931,7 +6211,7 @@ async function runSetup(deps) {
5931
6211
  legacyConfig: detection.legacyConfig
5932
6212
  });
5933
6213
  let legacyConfigAction;
5934
- if (currentConfigAction === "create" || currentConfigAction === "overwrite") {
6214
+ if (currentConfigAction === "create" || currentConfigAction === "overwrite" || currentConfigAction === "fresh") {
5935
6215
  if (detection.legacyConfig.kind === "found" || detection.legacyConfig.kind === "read_error") {
5936
6216
  legacyConfigAction = await deps.chooseLegacyConfigAction({
5937
6217
  legacyConfig: detection.legacyConfig
@@ -6005,6 +6285,16 @@ async function runSetup(deps) {
6005
6285
  throw new Error("migrate_legacy requires legacy config");
6006
6286
  }
6007
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
+ }
6008
6298
  let finalDraft = migrated.draft;
6009
6299
  if (migrated.needsCompletion) {
6010
6300
  finalDraft = await deps.completeDraft({
@@ -6039,6 +6329,96 @@ var init_setup = __esm({
6039
6329
 
6040
6330
  // src/setup/index.ts
6041
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
+ }
6042
6422
  const rl = (0, import_promises3.createInterface)({ input: import_process.stdin, output: import_process.stdout });
6043
6423
  const ask = async (message) => {
6044
6424
  const answer = await rl.question(message);
@@ -6073,6 +6453,9 @@ function createConsoleIO() {
6073
6453
  info(message) {
6074
6454
  import_process.stdout.write(`${message}
6075
6455
  `);
6456
+ },
6457
+ close() {
6458
+ rl.close();
6076
6459
  }
6077
6460
  };
6078
6461
  }
@@ -6083,14 +6466,79 @@ function readStructuredConfigFile(filePath) {
6083
6466
  }
6084
6467
  return import_js_yaml.default.load(content);
6085
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
+ }
6086
6533
  async function readLegacyConfig(deps = {}) {
6087
6534
  const baseHomeDir = deps.homeDir || (0, import_os3.homedir)();
6088
6535
  const exists = deps.exists || import_fs6.existsSync;
6089
- const readConfig = deps.readConfig || readStructuredConfigFile;
6536
+ const readConfig = deps.readConfig || readLegacyConfigFile;
6090
6537
  const overridePath = process.env.CTR_SETUP_LEGACY_CONFIG_PATH;
6091
6538
  const candidatePaths = overridePath ? [overridePath] : [
6092
6539
  (0, import_path6.join)(baseHomeDir, ".ccr", "config.yaml"),
6093
- (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")
6094
6542
  ];
6095
6543
  const legacyPath = candidatePaths.find((filePath) => exists(filePath));
6096
6544
  if (!legacyPath) {
@@ -6133,16 +6581,30 @@ async function readCurrentConfig() {
6133
6581
  }
6134
6582
  }
6135
6583
  async function probeService() {
6136
- const healthy = await waitForService(DEFAULT_CONFIG.PORT, 500);
6137
- return healthy ? { kind: "self_healthy", port: DEFAULT_CONFIG.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 };
6138
6594
  }
6139
6595
  async function enterClaudeCode() {
6596
+ if (process.env.CTR_SETUP_SKIP_ENTER_CODE === "1") {
6597
+ return;
6598
+ }
6140
6599
  const cliModule = await Promise.resolve().then(() => (init_cli(), cli_exports));
6141
6600
  await cliModule.runClaudeCode();
6142
6601
  }
6602
+ function shouldAutoEnterClaudeCodeAfterSetup() {
6603
+ return process.env.CTR_SETUP_AUTO_ENTER_CODE === "1";
6604
+ }
6143
6605
  async function executeStart() {
6144
6606
  const childProcess = await import("child_process");
6145
- childProcess.spawn(process.execPath, [process.argv[1], "start", "--daemon"], {
6607
+ childProcess.spawn(process.execPath, [process.argv[1], "start"], {
6146
6608
  detached: true,
6147
6609
  stdio: "ignore",
6148
6610
  env: { ...process.env, CTR_DAEMON: "1" }
@@ -6332,12 +6794,12 @@ async function buildFreshConfig(io) {
6332
6794
  }
6333
6795
  return draft;
6334
6796
  }
6335
- async function completeDraft(input2) {
6336
- const draft = toDraftFromConfig(input2.draft);
6337
- if (input2.fields.includes("defaultModel")) {
6797
+ async function completeDraft(input3) {
6798
+ const draft = toDraftFromConfig(input3.draft);
6799
+ if (input3.fields.includes("defaultModel")) {
6338
6800
  const defaultProvider = draft.Models?.[0]?.id ?? draft.Providers?.[0]?.name ?? "provider";
6339
6801
  const defaultModel = draft.Models?.[0]?.model ?? draft.Providers?.[0]?.models?.[0] ?? "";
6340
- const model = await input2.io.input("\u9ED8\u8BA4\u6A21\u578B", defaultModel);
6802
+ const model = await input3.io.input("\u9ED8\u8BA4\u6A21\u578B", defaultModel);
6341
6803
  if (draft.Models?.[0]) {
6342
6804
  draft.Models[0].model = model;
6343
6805
  draft.Router.default = defaultProvider;
@@ -6346,16 +6808,16 @@ async function completeDraft(input2) {
6346
6808
  draft.Router.default = `${defaultProvider},${model}`;
6347
6809
  }
6348
6810
  }
6349
- if (input2.fields.includes("apiKey")) {
6350
- const apiKey = await input2.io.input("API Key");
6811
+ if (input3.fields.includes("apiKey")) {
6812
+ const apiKey = await input3.io.input("API Key");
6351
6813
  if (draft.Models?.length) {
6352
6814
  draft.Models = draft.Models.map((model) => ({ ...model, key: model.key || apiKey, api_key: model.api_key || apiKey }));
6353
6815
  } else {
6354
6816
  draft.Providers = draft.Providers?.map((provider) => ({ ...provider, api_key: provider.api_key || apiKey }));
6355
6817
  }
6356
6818
  }
6357
- if (input2.fields.includes("apiBaseUrl")) {
6358
- 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");
6359
6821
  if (draft.Models?.length) {
6360
6822
  draft.Models = draft.Models.map((model) => ({
6361
6823
  ...model,
@@ -6369,8 +6831,8 @@ async function completeDraft(input2) {
6369
6831
  }));
6370
6832
  }
6371
6833
  }
6372
- if (input2.fields.includes("capabilityHints") && draft.Models?.[0]) {
6373
- await promptCapabilityMetadataForDraft(draft, input2.io);
6834
+ if (input3.fields.includes("capabilityHints") && draft.Models?.[0]) {
6835
+ await promptCapabilityMetadataForDraft(draft, input3.io);
6374
6836
  }
6375
6837
  return draft;
6376
6838
  }
@@ -6384,112 +6846,150 @@ function createDefaultDeps(io = createConsoleIO()) {
6384
6846
  executeStart,
6385
6847
  executeReload: executeRestart,
6386
6848
  executeRestart,
6387
- verifyHealth: () => waitForService(DEFAULT_CONFIG.PORT, 5e3),
6849
+ verifyHealth: () => waitForService(getConfiguredPortFromCurrentFiles(), 5e3),
6388
6850
  enterClaudeCode,
6389
6851
  io
6390
6852
  };
6391
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
+ }
6392
6860
  async function runSetupCli(customDeps) {
6393
6861
  const defaults = createDefaultDeps(customDeps?.io);
6394
6862
  const deps = { ...defaults, ...customDeps };
6395
- await runSetup({
6396
- detectSetupEnvironment: () => detectSetupEnvironment({
6397
- readCurrentConfig: deps.readCurrentConfig,
6398
- readLegacyConfig: deps.readLegacyConfig,
6399
- probeService: deps.probeService
6400
- }),
6401
- chooseCurrentConfigAction: async ({ currentConfig }) => {
6402
- if (currentConfig.kind === "missing") {
6403
- return "create";
6404
- }
6405
- if (currentConfig.kind === "valid") {
6406
- deps.io.info("\u68C0\u6D4B\u5230\u5F53\u524D claude-trigger-router \u914D\u7F6E\u5DF2\u53EF\u7528\u3002");
6407
- if (currentConfig.warnings.length > 0) {
6408
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
6409
- }
6410
- return mapValidCurrentConfigChoice(
6411
- await deps.io.choose("\u4F60\u60F3\u76F4\u63A5\u4F7F\u7528\u5B83\uFF0C\u8FD8\u662F\u91CD\u65B0\u8C03\u6574\uFF1F", [
6412
- "\u76F4\u63A5\u4F7F\u7528\u5F53\u524D\u914D\u7F6E\uFF08\u63A8\u8350\uFF09",
6413
- "\u68C0\u67E5\u5E76\u8C03\u6574\u5F53\u524D\u914D\u7F6E",
6414
- "\u653E\u5F03\u5F53\u524D\u914D\u7F6E\uFF0C\u91CD\u65B0\u5F00\u59CB"
6415
- ])
6416
- );
6417
- }
6418
- if (currentConfig.kind === "invalid") {
6419
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A${currentConfig.errors.join("; ")}`);
6420
- if (currentConfig.warnings.length > 0) {
6421
- 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
+ );
6422
6886
  }
6423
- return await deps.io.choose("\u9009\u62E9\u4E0B\u4E00\u6B65", ["repair", "overwrite", "cancel"]);
6424
- }
6425
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u65E0\u6CD5\u89E3\u6790\uFF1A${currentConfig.error}`);
6426
- return await deps.io.choose("\u9009\u62E9\u4E0B\u4E00\u6B65", ["rebuild", "cancel"]);
6427
- },
6428
- chooseLegacyConfigAction: async ({ legacyConfig }) => {
6429
- if (legacyConfig.kind === "found") {
6430
- return mapLegacyConfigChoice(
6431
- 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", [
6432
- "\u8FC1\u79FB\u65E7\u914D\u7F6E\uFF08\u63A8\u8350\uFF09",
6433
- "\u8DF3\u8FC7\u8FC1\u79FB\uFF0C\u624B\u52A8\u65B0\u5EFA"
6434
- ])
6435
- );
6436
- }
6437
- if (legacyConfig.kind === "read_error") {
6438
- deps.io.info(`\u65E7 ccr \u914D\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF1A${legacyConfig.error}`);
6439
- }
6440
- return "skip";
6441
- },
6442
- buildFreshConfig: () => buildFreshConfig(deps.io),
6443
- buildRepairConfig: async ({ currentConfig }) => toDraftFromConfig(currentConfig),
6444
- completeDraft: ({ draft, fields }) => completeDraft({ draft, fields, io: deps.io }),
6445
- migrateLegacyConfig,
6446
- mapConfigErrorsToRepairFields,
6447
- persistConfig: async ({ config, currentConfigPath, hasExistingConfig }) => {
6448
- const normalized = normalizeAndValidateConfig(config);
6449
- const persisted = await persistSetupConfig({
6450
- config: normalized.config,
6451
- currentConfigPath,
6452
- hasExistingConfig,
6453
- validateConfig: (inputConfig) => normalizeAndValidateConfig(inputConfig).errors,
6454
- backupCurrentConfig: deps.backupCurrentConfig,
6455
- writeConfig: deps.writeConfig
6456
- });
6457
- if (normalized.warnings.length > 0) {
6458
- deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${normalized.warnings.join("; ")}`);
6459
- }
6460
- return persisted;
6461
- },
6462
- ensureServiceReady: async ({ configChanged, detectedService, reloadSupported }) => {
6463
- const action = decideServiceAction({
6464
- configChanged,
6465
- detectedService,
6466
- reloadSupported
6467
- });
6468
- await applyServiceAction({
6469
- action,
6470
- executeStart: deps.executeStart,
6471
- executeReload: deps.executeReload,
6472
- executeRestart: deps.executeRestart,
6473
- verifyHealth: deps.verifyHealth
6474
- });
6475
- return {
6476
- action: action.kind,
6477
- healthChecked: true
6478
- };
6479
- },
6480
- enterClaudeCode: deps.enterClaudeCode,
6481
- reloadSupported: false
6482
- });
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
+ }
6483
6981
  }
6484
- 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;
6485
6983
  var init_setup2 = __esm({
6486
6984
  "src/setup/index.ts"() {
6487
6985
  "use strict";
6488
6986
  import_fs6 = require("fs");
6987
+ import_net2 = require("net");
6489
6988
  import_os3 = require("os");
6490
6989
  import_path6 = require("path");
6491
6990
  import_promises3 = require("readline/promises");
6492
6991
  import_process = require("process");
6992
+ import_json52 = __toESM(require("json5"));
6493
6993
  import_js_yaml = __toESM(require("js-yaml"));
6494
6994
  init_constants();
6495
6995
  init_provider_presets();
@@ -6506,6 +7006,525 @@ var init_setup2 = __esm({
6506
7006
  }
6507
7007
  });
6508
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
+
6509
7528
  // src/cli.ts
6510
7529
  var cli_exports = {};
6511
7530
  __export(cli_exports, {
@@ -6515,7 +7534,7 @@ __export(cli_exports, {
6515
7534
  });
6516
7535
  module.exports = __toCommonJS(cli_exports);
6517
7536
  function getPackageInfo() {
6518
- const content = (0, import_fs7.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
7537
+ const content = (0, import_fs8.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
6519
7538
  const pkg = JSON.parse(content);
6520
7539
  return {
6521
7540
  name: pkg.name ?? "@peterwangze/claude-trigger-router",
@@ -6528,7 +7547,7 @@ function getArgs() {
6528
7547
  function getCommand() {
6529
7548
  return getArgs()[0];
6530
7549
  }
6531
- function hasArg(flag, shortFlag) {
7550
+ function hasArg2(flag, shortFlag) {
6532
7551
  const args = getArgs();
6533
7552
  return args.includes(flag) || (shortFlag ? args.includes(shortFlag) : false);
6534
7553
  }
@@ -6537,32 +7556,43 @@ function getArgValue(flag, shortFlag) {
6537
7556
  const index = args.indexOf(flag) !== -1 ? args.indexOf(flag) : shortFlag ? args.indexOf(shortFlag) : -1;
6538
7557
  return index !== -1 ? args[index + 1] : void 0;
6539
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
+ }
6540
7570
  function getPort() {
6541
7571
  const portValue = getArgValue("--port", "-p");
6542
7572
  if (portValue) {
6543
- return parseInt(portValue, 10);
7573
+ return parsePortValue(portValue, "\u547D\u4EE4\u884C\u7AEF\u53E3\u53C2\u6570");
6544
7574
  }
6545
7575
  try {
6546
- const yaml3 = require("js-yaml");
6547
- if ((0, import_fs7.existsSync)(CONFIG_FILE)) {
6548
- const content = (0, import_fs7.readFileSync)(CONFIG_FILE, "utf-8");
6549
- 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);
6550
7580
  if (config?.PORT) return config.PORT;
6551
- } else if ((0, import_fs7.existsSync)(CONFIG_FILE_YML)) {
6552
- const content = (0, import_fs7.readFileSync)(CONFIG_FILE_YML, "utf-8");
6553
- 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);
6554
7584
  if (config?.PORT) return config.PORT;
6555
- } else if ((0, import_fs7.existsSync)(CONFIG_FILE_JSON)) {
6556
- 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");
6557
7587
  const config = JSON.parse(content);
6558
7588
  if (config?.PORT) return config.PORT;
6559
7589
  }
6560
7590
  } catch {
6561
7591
  }
6562
- return DEFAULT_CONFIG.PORT;
7592
+ return DEFAULT_CONFIG2.PORT;
6563
7593
  }
6564
7594
  function isDaemonMode() {
6565
- return hasArg("--daemon", "-d");
7595
+ return hasArg2("--daemon", "-d");
6566
7596
  }
6567
7597
  function printHelp() {
6568
7598
  console.log(`
@@ -6572,6 +7602,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6572
7602
 
6573
7603
  \u547D\u4EE4\uFF1A
6574
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
6575
7606
  init \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
6576
7607
  start \u542F\u52A8\u8DEF\u7531\u670D\u52A1\uFF08\u9ED8\u8BA4\u524D\u53F0\u8FD0\u884C\uFF09
6577
7608
  stop \u505C\u6B62\u540E\u53F0\u670D\u52A1
@@ -6580,16 +7611,17 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6580
7611
  version \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C\u4E0E\u5305\u4FE1\u606F
6581
7612
  upgrade \u67E5\u770B\u5347\u7EA7\u5230\u6700\u65B0 npm \u7248\u672C\u7684\u6307\u5F15
6582
7613
  code \u901A\u8FC7\u8DEF\u7531\u5668\u8FD0\u884C Claude Code\uFF08\u9700\u5148\u542F\u52A8\u670D\u52A1\uFF09
6583
- 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
6584
7615
  help \u663E\u793A\u6B64\u5E2E\u52A9\u4FE1\u606F
6585
7616
 
6586
7617
  \u9009\u9879\uFF1A
6587
- --port, -p \u6307\u5B9A\u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4\uFF1A3456\uFF09
7618
+ --port, -p \u6307\u5B9A\u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4\uFF1A5678\uFF09
6588
7619
  --daemon, -d \u4EE5\u540E\u53F0\u65B9\u5F0F\u8FD0\u884C\uFF08\u914D\u5408 start/restart \u4F7F\u7528\uFF09
6589
7620
  --force \u5F3A\u5236\u8986\u76D6\u5DF2\u6709\u914D\u7F6E\uFF08\u914D\u5408 init \u4F7F\u7528\uFF09
6590
7621
 
6591
7622
  \u4F7F\u7528\u793A\u4F8B\uFF1A
6592
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
6593
7625
  ctr init # \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
6594
7626
  ctr version # \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C
6595
7627
  ctr upgrade # \u67E5\u770B\u5347\u7EA7\u5230\u6700\u65B0\u7248\u672C\u7684\u547D\u4EE4
@@ -6597,6 +7629,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6597
7629
  ctr start --daemon # \u540E\u53F0\u542F\u52A8
6598
7630
  ctr status # \u67E5\u770B\u670D\u52A1\u72B6\u6001
6599
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
6600
7633
  ctr stop # \u505C\u6B62\u540E\u53F0\u670D\u52A1
6601
7634
  ctr restart --daemon # \u91CD\u542F\u540E\u53F0\u670D\u52A1
6602
7635
 
@@ -6606,10 +7639,29 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6606
7639
 
6607
7640
  \u914D\u7F6E\u76EE\u5F55\uFF1A${CONFIG_DIR}
6608
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
+
6609
7645
  \u66F4\u591A\u4FE1\u606F\uFF1Ahttps://github.com/peterwangze/claude-trigger-router
6610
7646
  `);
6611
7647
  }
6612
- 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) {
6613
7665
  try {
6614
7666
  const response = await fetch(PACKAGE_REGISTRY_LATEST_URL, {
6615
7667
  signal: AbortSignal.timeout(timeoutMs)
@@ -6618,10 +7670,12 @@ async function getLatestPackageVersion(timeoutMs = 1500) {
6618
7670
  return null;
6619
7671
  }
6620
7672
  const payload = await response.json();
6621
- return typeof payload.version === "string" ? payload.version : null;
7673
+ if (typeof payload.version === "string") {
7674
+ return payload.version;
7675
+ }
6622
7676
  } catch {
6623
- return null;
6624
7677
  }
7678
+ return getLatestPackageVersionViaNpm(packageName);
6625
7679
  }
6626
7680
  function isNewerVersion(current, latest) {
6627
7681
  const currentParts = current.split(".").map((part) => Number.parseInt(part, 10));
@@ -6641,7 +7695,7 @@ function isNewerVersion(current, latest) {
6641
7695
  }
6642
7696
  async function printVersion() {
6643
7697
  const pkg = getPackageInfo();
6644
- const latestVersion = await getLatestPackageVersion();
7698
+ const latestVersion = await getLatestPackageVersion(pkg.name);
6645
7699
  console.log(`Package: ${pkg.name}`);
6646
7700
  console.log(`Version: ${pkg.version}`);
6647
7701
  console.log(`Latest: ${latestVersion ?? "unavailable"}`);
@@ -6661,30 +7715,42 @@ function printUpgradeGuidance() {
6661
7715
  console.log("\u5168\u5C40\u5B89\u88C5\u5728\u67D0\u4E9B\u73AF\u5883\u4E0B\u53EF\u80FD\u9700\u8981\u7BA1\u7406\u5458/root \u6743\u9650\u3002");
6662
7716
  console.log(`NPM: ${PACKAGE_PAGE_URL}`);
6663
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
+ }
6664
7734
  function initConfig2() {
6665
- const force = hasArg("--force");
6666
- 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);
6667
7737
  if (existingConfig && !force) {
6668
7738
  console.log(`\u26A0\uFE0F \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${existingConfig}`);
6669
7739
  console.log(" \u5982\u9700\u8986\u76D6\uFF0C\u8BF7\u4F7F\u7528 --force \u53C2\u6570\u3002");
6670
7740
  return;
6671
7741
  }
6672
- if (!(0, import_fs7.existsSync)(CONFIG_DIR)) {
6673
- (0, import_fs7.mkdirSync)(CONFIG_DIR, { recursive: true });
6674
- }
6675
- const examplePaths = [
6676
- (0, import_path7.join)(__dirname, "..", "config", "trigger.example.yaml"),
6677
- (0, import_path7.join)((0, import_path7.dirname)(process.argv[1]), "..", "config", "trigger.example.yaml")
6678
- ];
6679
- const exampleFile = examplePaths.find((p) => (0, import_fs7.existsSync)(p));
6680
- if (!exampleFile) {
6681
- console.error("\u274C \u627E\u4E0D\u5230\u793A\u4F8B\u914D\u7F6E\u6587\u4EF6\u3002");
6682
- console.log(` \u8BF7\u624B\u52A8\u521B\u5EFA ${CONFIG_FILE}`);
6683
- console.log(" \u53C2\u8003\u6587\u6863\uFF1Ahttps://github.com/peterwangze/claude-trigger-router#configuration");
6684
- process.exit(1);
7742
+ if (!(0, import_fs8.existsSync)(CONFIG_DIR)) {
7743
+ (0, import_fs8.mkdirSync)(CONFIG_DIR, { recursive: true });
6685
7744
  }
6686
7745
  try {
6687
- (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");
6688
7754
  const action = force ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA";
6689
7755
  console.log(`\u2705 \u914D\u7F6E\u6587\u4EF6${action}\uFF1A${CONFIG_FILE}`);
6690
7756
  console.log("");
@@ -6700,10 +7766,22 @@ function initConfig2() {
6700
7766
  }
6701
7767
  }
6702
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
+ }
6703
7781
  console.log("\u{1F680} Starting Claude Trigger Router (foreground)...");
6704
7782
  console.log(" Press Ctrl+C to stop");
6705
7783
  try {
6706
- await run({ port });
7784
+ await run({ port: targetPort });
6707
7785
  } catch (error) {
6708
7786
  if (error.message?.includes("Invalid configuration")) {
6709
7787
  console.error("\n\u274C Configuration error. Run 'ctr init' to create a config file.");
@@ -6713,7 +7791,14 @@ async function startForeground(port) {
6713
7791
  process.exit(1);
6714
7792
  }
6715
7793
  }
6716
- 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
+ }
6717
7802
  if (isServiceRunning()) {
6718
7803
  console.log("\u2705 Service is already running in the background.");
6719
7804
  return;
@@ -6724,30 +7809,58 @@ function startDaemon(port) {
6724
7809
  if (port) {
6725
7810
  childArgs.push("--port", String(port));
6726
7811
  }
6727
- const child = (0, import_child_process2.spawn)(nodeExec, childArgs, {
7812
+ const child = (0, import_child_process3.spawn)(nodeExec, childArgs, {
6728
7813
  detached: true,
6729
7814
  stdio: "ignore",
6730
7815
  env: { ...process.env, CTR_DAEMON: "1" }
6731
7816
  });
6732
7817
  child.unref();
6733
- const targetPort = port ?? getPort();
6734
- let waited = 0;
6735
- const interval = setInterval(() => {
6736
- waited += 500;
6737
- if (isServiceRunning()) {
6738
- clearInterval(interval);
6739
- console.log(`\u2705 Service started in background (port: ${targetPort})`);
6740
- console.log(` Run 'ctr stop' to stop it.`);
6741
- } else if (waited >= 5e3) {
6742
- clearInterval(interval);
6743
- console.log(`\u2705 Service launched in background (port: ${targetPort})`);
6744
- console.log(` If it fails to start, run 'ctr start' (without --daemon) to see errors.`);
6745
- }
6746
- }, 500);
6747
- }
6748
- 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() {
6749
7855
  const info = readServiceInfo();
6750
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
+ }
6751
7864
  console.log("\u23F9 \u670D\u52A1\u672A\u8FD0\u884C");
6752
7865
  return;
6753
7866
  }
@@ -6772,9 +7885,10 @@ function stopService() {
6772
7885
  console.error("\u274C \u505C\u6B62\u670D\u52A1\u5931\u8D25:", error.message);
6773
7886
  }
6774
7887
  }
6775
- function restartService() {
7888
+ async function restartService() {
6776
7889
  stopService();
6777
- setTimeout(() => startDaemon(getPort()), 1500);
7890
+ await new Promise((resolve) => setTimeout(resolve, 1500));
7891
+ await startDaemon(getPort());
6778
7892
  }
6779
7893
  async function runClaudeCode() {
6780
7894
  const port = getPort();
@@ -6789,14 +7903,16 @@ async function runClaudeCode() {
6789
7903
  console.log(" 1. Start service first: ctr start --daemon");
6790
7904
  console.log(" 2. Or start interactively in another terminal: ctr start");
6791
7905
  console.log("");
6792
- const proceed = process.env.CTR_AUTO_START === "1";
6793
- if (!proceed) {
6794
- process.exit(1);
6795
- }
7906
+ process.exit(1);
6796
7907
  }
6797
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
+ }
6798
7914
  const isWindows = process.platform === "win32";
6799
- const claude = (0, import_child_process2.spawn)("claude", [], {
7915
+ const claude = (0, import_child_process3.spawn)("claude", [], {
6800
7916
  stdio: "inherit",
6801
7917
  shell: isWindows,
6802
7918
  env: {
@@ -6815,10 +7931,18 @@ async function runClaudeCode() {
6815
7931
  process.exit(code || 0);
6816
7932
  });
6817
7933
  }
6818
- function openUI() {
7934
+ async function openUI() {
6819
7935
  const port = getPort();
6820
7936
  const url = `http://127.0.0.1:${port}/ui`;
7937
+ const healthy = await waitForService(port, 800);
6821
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
+ }
6822
7946
  try {
6823
7947
  (0, import_openurl.default)(url);
6824
7948
  } catch (error) {
@@ -6831,12 +7955,15 @@ async function main() {
6831
7955
  case "setup":
6832
7956
  await runSetupCli();
6833
7957
  break;
7958
+ case "doctor":
7959
+ await runDoctorCli();
7960
+ break;
6834
7961
  case "init":
6835
7962
  initConfig2();
6836
7963
  break;
6837
7964
  case "start":
6838
7965
  if (isDaemonMode()) {
6839
- startDaemon(getPort());
7966
+ await startDaemon(getPort());
6840
7967
  } else {
6841
7968
  await startForeground(getPort());
6842
7969
  }
@@ -6845,7 +7972,7 @@ async function main() {
6845
7972
  stopService();
6846
7973
  break;
6847
7974
  case "status":
6848
- showStatus();
7975
+ await showStatus();
6849
7976
  break;
6850
7977
  case "version":
6851
7978
  await printVersion();
@@ -6854,13 +7981,14 @@ async function main() {
6854
7981
  printUpgradeGuidance();
6855
7982
  break;
6856
7983
  case "restart":
6857
- restartService();
7984
+ printRestartGuidanceHint();
7985
+ await restartService();
6858
7986
  break;
6859
7987
  case "code":
6860
7988
  await runClaudeCode();
6861
7989
  break;
6862
7990
  case "ui":
6863
- openUI();
7991
+ await openUI();
6864
7992
  break;
6865
7993
  case "help":
6866
7994
  case "--help":
@@ -6875,21 +8003,24 @@ async function main() {
6875
8003
  process.exit(command ? 1 : 0);
6876
8004
  }
6877
8005
  }
6878
- 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;
6879
8007
  var init_cli = __esm({
6880
8008
  "src/cli.ts"() {
6881
- import_child_process2 = require("child_process");
8009
+ import_child_process3 = require("child_process");
6882
8010
  import_path7 = require("path");
6883
8011
  import_openurl = __toESM(require("openurl"));
6884
- import_fs7 = require("fs");
8012
+ import_fs8 = require("fs");
6885
8013
  init_index();
6886
8014
  init_processCheck();
6887
8015
  init_constants();
6888
8016
  init_service_health();
6889
8017
  init_setup2();
8018
+ init_templates();
8019
+ init_doctor();
6890
8020
  PACKAGE_JSON_PATH = (0, import_path7.join)(__dirname, "..", "package.json");
6891
8021
  PACKAGE_PAGE_URL = "https://www.npmjs.com/package/@peterwangze/claude-trigger-router";
6892
8022
  PACKAGE_REGISTRY_LATEST_URL = "https://registry.npmjs.org/@peterwangze%2Fclaude-trigger-router/latest";
8023
+ PACKAGE_REGISTRY_URL = "https://registry.npmjs.org/";
6893
8024
  if (process.env.CTR_SKIP_MAIN !== "1") {
6894
8025
  main().catch((error) => {
6895
8026
  console.error("Error:", error);