@jsonstudio/rcc 0.89.942 → 0.89.1086

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.
Files changed (97) hide show
  1. package/README.md +1 -42
  2. package/dist/build-info.js +2 -2
  3. package/dist/build-info.js.map +1 -1
  4. package/dist/cli.js +181 -55
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/quota-daemon.d.ts +2 -0
  7. package/dist/commands/quota-daemon.js +89 -0
  8. package/dist/commands/quota-daemon.js.map +1 -0
  9. package/dist/docs/daemon-admin-ui.html +958 -0
  10. package/dist/index.js +46 -10
  11. package/dist/index.js.map +1 -1
  12. package/dist/manager/modules/quota/index.d.ts +34 -0
  13. package/dist/manager/modules/quota/index.js +291 -0
  14. package/dist/manager/modules/quota/index.js.map +1 -1
  15. package/dist/manager/modules/token/index.js +13 -2
  16. package/dist/manager/modules/token/index.js.map +1 -1
  17. package/dist/manager/quota/provider-quota-center.d.ts +48 -0
  18. package/dist/manager/quota/provider-quota-center.js +239 -0
  19. package/dist/manager/quota/provider-quota-center.js.map +1 -0
  20. package/dist/manager/quota/provider-quota-store.d.ts +17 -0
  21. package/dist/manager/quota/provider-quota-store.js +88 -0
  22. package/dist/manager/quota/provider-quota-store.js.map +1 -0
  23. package/dist/providers/auth/token-scanner/index.js +11 -3
  24. package/dist/providers/auth/token-scanner/index.js.map +1 -1
  25. package/dist/providers/core/runtime/http-request-executor.js +24 -7
  26. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  27. package/dist/providers/core/runtime/http-transport-provider.js +11 -3
  28. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  29. package/dist/providers/core/runtime/responses-provider.js +9 -3
  30. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  31. package/dist/providers/core/utils/http-client.d.ts +1 -0
  32. package/dist/providers/core/utils/http-client.js +139 -4
  33. package/dist/providers/core/utils/http-client.js.map +1 -1
  34. package/dist/providers/core/utils/snapshot-writer.d.ts +12 -0
  35. package/dist/providers/core/utils/snapshot-writer.js +99 -18
  36. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  37. package/dist/providers/mock/mock-provider-runtime.d.ts +3 -0
  38. package/dist/providers/mock/mock-provider-runtime.js +176 -4
  39. package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
  40. package/dist/server/handlers/chat-handler.js +13 -1
  41. package/dist/server/handlers/chat-handler.js.map +1 -1
  42. package/dist/server/handlers/handler-utils.js +5 -0
  43. package/dist/server/handlers/handler-utils.js.map +1 -1
  44. package/dist/server/handlers/messages-handler.js +13 -1
  45. package/dist/server/handlers/messages-handler.js.map +1 -1
  46. package/dist/server/handlers/responses-handler.js +73 -1
  47. package/dist/server/handlers/responses-handler.js.map +1 -1
  48. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +174 -2
  49. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  50. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +519 -0
  51. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  52. package/dist/server/runtime/http-server/executor-response.js +6 -0
  53. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  54. package/dist/server/runtime/http-server/index.d.ts +5 -0
  55. package/dist/server/runtime/http-server/index.js +205 -4
  56. package/dist/server/runtime/http-server/index.js.map +1 -1
  57. package/dist/server/runtime/http-server/middleware.d.ts +2 -0
  58. package/dist/server/runtime/http-server/middleware.js +63 -0
  59. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  60. package/dist/server/runtime/http-server/request-executor.d.ts +2 -0
  61. package/dist/server/runtime/http-server/request-executor.js +57 -10
  62. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  63. package/dist/server/runtime/http-server/routes.js +38 -1
  64. package/dist/server/runtime/http-server/routes.js.map +1 -1
  65. package/dist/server/runtime/http-server/stats-manager.d.ts +55 -0
  66. package/dist/server/runtime/http-server/stats-manager.js +462 -4
  67. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  68. package/dist/server/runtime/http-server/types.d.ts +1 -0
  69. package/dist/token-daemon/token-daemon.js +70 -25
  70. package/dist/token-daemon/token-daemon.js.map +1 -1
  71. package/dist/token-daemon/token-utils.d.ts +1 -0
  72. package/dist/token-daemon/token-utils.js +9 -1
  73. package/dist/token-daemon/token-utils.js.map +1 -1
  74. package/dist/tools/semantic-replay.js +29 -0
  75. package/dist/tools/semantic-replay.js.map +1 -1
  76. package/dist/utils/is-direct-execution.d.ts +1 -0
  77. package/dist/utils/is-direct-execution.js +15 -0
  78. package/dist/utils/is-direct-execution.js.map +1 -0
  79. package/dist/utils/snapshot-writer.d.ts +2 -0
  80. package/dist/utils/snapshot-writer.js +47 -4
  81. package/dist/utils/snapshot-writer.js.map +1 -1
  82. package/dist/utils/windows-netstat.d.ts +1 -0
  83. package/dist/utils/windows-netstat.js +34 -0
  84. package/dist/utils/windows-netstat.js.map +1 -0
  85. package/package.json +3 -4
  86. package/scripts/analyze-codex-error-failures.mjs +24 -14
  87. package/scripts/classify-codex-samples.mjs +0 -35
  88. package/scripts/copy-modules-config.mjs +17 -1
  89. package/scripts/generate-snapshot-data.mjs +41 -11
  90. package/scripts/mock-provider/extract.mjs +239 -21
  91. package/scripts/mock-provider/run-regressions.mjs +79 -16
  92. package/scripts/quota-dryrun.mjs +124 -0
  93. package/scripts/tests/apply-patch-loop.mjs +5 -1
  94. package/scripts/tests/exec-command-loop.mjs +16 -19
  95. package/scripts/verify-apply-patch.mjs +335 -5
  96. package/scripts/verify-e2e-toolcall.mjs +49 -10
  97. package/scripts/toon-suite.mjs +0 -141
package/README.md CHANGED
@@ -228,48 +228,6 @@ RouteCodex 会按以下优先级查找配置:
228
228
 
229
229
  ---
230
230
 
231
- ## TOON 工具协议与 CLI 解码说明
232
-
233
- RouteCodex / llmswitch-core 对「模型看到的工具参数」与「CLI/执行器真正消费的参数」做了明确分层:
234
-
235
- - **模型视角(统一协议)**
236
- - 所有支持 TOON 的工具(例如 `exec_command`、`apply_patch`)都可以通过 `arguments.toon` 传参:
237
- - 形如 `command: ...\nworkdir: ...\n` 的多行 `key: value`。
238
- - 模型无需关心 CLI 的内部 JSON 结构(`cmd` / `input` 等字段名)。
239
- - **执行视角(CLI 黑盒)**
240
- - Codex CLI 的工具实现不会理解 TOON,只接受传统 JSON 形态:
241
- - `exec_command` 只认 `{ cmd: string, workdir?, command? ... }`。
242
- - `apply_patch` 只认 `{ input: string, patch?: string }`,且 `input` 必须是标准统一 diff(`*** Begin Patch` 开头)。
243
- - CLI 被视为黑盒:不能指望它去解析 TOON 或结构化 `changes`。
244
-
245
- 为此,llmswitch-core 在 **响应侧** 增加了成对的解码过滤器,用于在把响应发回 CLI 之前“翻译”工具参数:
246
-
247
- - `ResponseToolArgumentsToonDecodeFilter`
248
- - 作用于所有协议(包括 `/v1/responses`),在响应处理阶段对 `choices[].message.tool_calls[*].function.arguments` 解码:
249
- - 对 shell/exec 类工具(`shell` / `shell_command` / `exec_command`):
250
- - 从 `toon` 中解析 `command`/`cmd`、`workdir`/`cwd`、`timeout_ms`、`with_escalated_permissions`、`justification` 等字段。
251
- - 统一输出为 JSON 字符串:`{"cmd":"...","command":"...","workdir":"...","timeout_ms":...,"with_escalated_permissions":...,"justification":"..."}`。
252
- - 对其它工具(如 `view_image`、MCP 工具等):
253
- - 将所有 TOON `key: value` 对映射为普通 JSON 字段,并做轻量类型推断(`true/false`→布尔,数字→number,可解析的 `{}`/`[]`→JSON 对象/数组)。
254
- - 对 `apply_patch`:
255
- - 交由专门的 `ResponseApplyPatchToonDecodeFilter` 处理,当前过滤器只负责 shell/exec 与通用工具,避免相互覆盖。
256
- - `ResponseApplyPatchToonDecodeFilter`
257
- - 专门负责 `apply_patch` 的响应参数规范化:
258
- - 支持两类输入:
259
- - `{"toon":"*** Begin Patch ... *** End Patch"}`;
260
- - 结构化 `changes` payload(多种 `kind`:insert_after / insert_before / replace / delete / create_file / delete_file)。
261
- - 将其统一转换为 `{ input: "<统一 diff>", patch: "<统一 diff>" }` 的 JSON 字符串挂回 `function.arguments`,以兼容 CLI 旧语义。
262
-
263
- 整体约束可以概括为:
264
-
265
- - **对模型**:可以使用 TOON 或结构化 JSON(例如 `changes`);RouteCodex 会在 Hub Pipeline 内对齐为统一 JSON 结构。
266
- - **对 CLI / 客户端**:始终看到历史兼容形态:
267
- - `exec_command`:具备 `cmd` 字段的 JSON;
268
- - `apply_patch`:具备 `input`(统一 diff)的 JSON。
269
- - **对维护者**:
270
- - 所有 TOON → JSON 的解码逻辑集中在 `sharedmodule/llmswitch-core/src/filters/special/` 及响应工具治理路径中;
271
- - CLI 侧不需要理解 TOON,也无需修改其内部工具实现;一切转换在 Hub 层完成。
272
-
273
231
  ---
274
232
 
275
233
  ## 参考文档
@@ -444,6 +402,7 @@ RouteCodex 支持在用户消息中通过 `<**...**>` 标签设置**当前会话
444
402
  - 当前响应 `finish_reason = "stop"`;
445
403
  - 当前轮没有工具调用(`tool_calls` 为空);
446
404
  - `stopMessageUsed < stopMessageMaxRepeats` 且客户端仍连接。
405
+ - 会话要求:sticky 状态依赖 `sessionId` / `conversationId`。`/v1/messages` 请求请确保 `metadata.user_id` 内包含 `session_<uuid>` 字样,系统会在缺少 header/metadata.sessionId 时从 `metadata.__raw_request_body.metadata.user_id` 自动提取用作会话键。
447
406
  - 行为:在保存的原始请求消息末尾追加一条 `{ role: "user", content: "<配置的文本>" }`,通过内部 `reenterPipeline` 自动发下一轮对话,对客户端透明。
448
407
 
449
408
  > 完整说明、状态持久化规则及 daemon 管理示例,参见 `docs/routing-instructions.md`。
@@ -1,6 +1,6 @@
1
1
  export const buildInfo = {
2
2
  mode: 'release',
3
- version: '0.89.942',
4
- buildTime: '2026-01-12T04:43:01.796Z'
3
+ version: '0.89.1086',
4
+ buildTime: '2026-01-15T13:27:28.446Z'
5
5
  };
6
6
  //# sourceMappingURL=build-info.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-info.js","sourceRoot":"","sources":["../src/build-info.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,SAAS,GAAc;IAClC,IAAI,EAAE,SAAS;IACf,OAAO,EAAE,UAAU;IACnB,SAAS,EAAE,0BAA0B;CACtC,CAAC"}
1
+ {"version":3,"file":"build-info.js","sourceRoot":"","sources":["../src/build-info.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,SAAS,GAAc;IAClC,IAAI,EAAE,SAAS;IACf,OAAO,EAAE,WAAW;IACpB,SAAS,EAAE,0BAA0B;CACtC,CAAC"}
package/dist/cli.js CHANGED
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'url';
13
13
  import { LOCAL_HOSTS, HTTP_PROTOCOLS, API_PATHS, DEFAULT_CONFIG, API_ENDPOINTS } from './constants/index.js';
14
14
  import { buildInfo } from './build-info.js';
15
15
  import { ensureLocalTokenPortalEnv } from './token-portal/local-token-portal.js';
16
+ import { parseNetstatListeningPids } from './utils/windows-netstat.js';
16
17
  async function createSpinner(text) {
17
18
  const mod = await dynamicImport('ora');
18
19
  const oraFactory = typeof mod?.default === 'function' ? mod.default : undefined;
@@ -111,6 +112,7 @@ const pkgName = (() => {
111
112
  // - routecodex(dev 包):默认端口 5555,用于本地开发调试,不读取配置端口,除非显式设置 ROUTECODEX_PORT/RCC_PORT
112
113
  // - rcc(release 包):严格按配置文件端口启动(httpserver.port/server.port/port)
113
114
  const IS_DEV_PACKAGE = pkgName === 'routecodex';
115
+ const IS_WINDOWS = process.platform === 'win32';
114
116
  const DEFAULT_DEV_PORT = 5555;
115
117
  const TOKEN_DAEMON_PID_FILE = path.join(homedir(), '.routecodex', 'token-daemon.pid');
116
118
  program
@@ -127,6 +129,30 @@ async function ensureTokenDaemonAutoStart() {
127
129
  logger.info('Token manager is now integrated into the server process; automatic external token-daemon auto-start is disabled.');
128
130
  }
129
131
  }
132
+ function killPidBestEffort(pid, opts) {
133
+ if (!Number.isFinite(pid) || pid <= 0) {
134
+ return;
135
+ }
136
+ if (IS_WINDOWS) {
137
+ const args = ['/PID', String(pid), '/T'];
138
+ if (opts.force) {
139
+ args.push('/F');
140
+ }
141
+ try {
142
+ spawnSync('taskkill', args, { stdio: 'ignore', encoding: 'utf8' });
143
+ }
144
+ catch {
145
+ // best-effort
146
+ }
147
+ return;
148
+ }
149
+ try {
150
+ process.kill(pid, opts.force ? 'SIGKILL' : 'SIGTERM');
151
+ }
152
+ catch {
153
+ // best-effort
154
+ }
155
+ }
130
156
  async function stopTokenDaemonIfRunning() {
131
157
  try {
132
158
  if (!fs.existsSync(TOKEN_DAEMON_PID_FILE)) {
@@ -154,7 +180,7 @@ async function stopTokenDaemonIfRunning() {
154
180
  return;
155
181
  }
156
182
  try {
157
- process.kill(pid, 'SIGTERM');
183
+ killPidBestEffort(pid, { force: false });
158
184
  }
159
185
  catch {
160
186
  // ignore
@@ -208,6 +234,12 @@ try {
208
234
  program.addCommand(createQuotaStatusCommand());
209
235
  }
210
236
  catch { /* optional */ }
237
+ // Quota daemon command - offline replay/once maintenance for provider-quota snapshot
238
+ try {
239
+ const { createQuotaDaemonCommand } = await import('./commands/quota-daemon.js');
240
+ program.addCommand(createQuotaDaemonCommand());
241
+ }
242
+ catch { /* optional */ }
211
243
  // OAuth command - force re-auth for a specific token (Camoufox-aware when enabled)
212
244
  try {
213
245
  const { createOauthCommand } = await import('./commands/oauth.js');
@@ -227,7 +259,9 @@ program
227
259
  .option('-p, --port <port>', 'RouteCodex server port (overrides config file)')
228
260
  // Default to IPv4 localhost to avoid environments where localhost resolves to ::1
229
261
  .option('-h, --host <host>', 'RouteCodex server host', LOCAL_HOSTS.IPV4)
262
+ .option('--url <url>', 'RouteCodex base URL (overrides host/port), e.g. https://code.codewhisper.cc')
230
263
  .option('-c, --config <config>', 'RouteCodex configuration file path')
264
+ .option('--apikey <apikey>', 'RouteCodex server apikey (defaults to httpserver.apikey in config when present)')
231
265
  .option('--claude-path <path>', 'Path to Claude Code executable', 'claude')
232
266
  .option('--cwd <dir>', 'Working directory for Claude Code (defaults to current shell cwd)')
233
267
  .option('--model <model>', 'Model to use with Claude Code')
@@ -240,13 +274,57 @@ program
240
274
  const extraArgsFromCommander = Array.isArray(extraArgs) ? extraArgs : [];
241
275
  const spinner = await createSpinner('Preparing Claude Code with RouteCodex...');
242
276
  try {
277
+ const parseServerUrl = (raw) => {
278
+ const trimmed = String(raw || '').trim();
279
+ if (!trimmed) {
280
+ throw new Error('--url is empty');
281
+ }
282
+ let parsed;
283
+ try {
284
+ parsed = new URL(trimmed);
285
+ }
286
+ catch {
287
+ parsed = new URL(`http://${trimmed}`);
288
+ }
289
+ const protocol = parsed.protocol === 'https:' ? 'https' : 'http';
290
+ const host = parsed.hostname;
291
+ const hasExplicitPort = Boolean(parsed.port && parsed.port.trim());
292
+ const port = hasExplicitPort ? Number(parsed.port) : null;
293
+ const rawPath = typeof parsed.pathname === 'string' ? parsed.pathname : '';
294
+ const basePath = rawPath && rawPath !== '/' ? rawPath.replace(/\/+$/, '') : '';
295
+ return { protocol, host, port: Number.isFinite(port) ? port : null, basePath };
296
+ };
297
+ const readConfigApiKey = (configPath) => {
298
+ try {
299
+ if (!configPath || !fs.existsSync(configPath)) {
300
+ return null;
301
+ }
302
+ const txt = fs.readFileSync(configPath, 'utf8');
303
+ const cfg = JSON.parse(txt);
304
+ const direct = cfg?.httpserver?.apikey ?? cfg?.modules?.httpserver?.config?.apikey ?? cfg?.server?.apikey;
305
+ const value = typeof direct === 'string' ? direct.trim() : '';
306
+ return value ? value : null;
307
+ }
308
+ catch {
309
+ return null;
310
+ }
311
+ };
243
312
  // Resolve configuration and determine port
244
313
  let configPath = options.config;
245
314
  if (!configPath) {
246
315
  configPath = path.join(homedir(), '.routecodex', 'config.json');
247
316
  }
317
+ let actualProtocol = 'http';
248
318
  let actualPort = options.port ? parseInt(options.port, 10) : null;
249
319
  let actualHost = options.host;
320
+ let actualBasePath = '';
321
+ if (options.url && String(options.url).trim()) {
322
+ const parsed = parseServerUrl(options.url);
323
+ actualProtocol = parsed.protocol;
324
+ actualHost = parsed.host || actualHost;
325
+ actualPort = parsed.port ?? actualPort;
326
+ actualBasePath = parsed.basePath;
327
+ }
250
328
  // Determine effective port for code command:
251
329
  // - dev package (routecodex): env override, otherwise固定 5555,不读取配置端口
252
330
  // - release package (rcc): 按配置/参数解析端口
@@ -259,7 +337,7 @@ program
259
337
  }
260
338
  else {
261
339
  // 非 dev 包:若未显式指定端口,则从配置文件解析
262
- if (!actualPort && fs.existsSync(configPath)) {
340
+ if (!actualPort && fs.existsSync(configPath) && !(options.url && String(options.url).trim())) {
263
341
  try {
264
342
  const configContent = fs.readFileSync(configPath, 'utf8');
265
343
  const config = JSON.parse(configContent);
@@ -271,12 +349,22 @@ program
271
349
  }
272
350
  }
273
351
  }
274
- // Require explicit port if not resolved
275
- if (!actualPort) {
352
+ // Require explicit port if not resolved (except when --url is used; default ports are implicit).
353
+ if (!(options.url && String(options.url).trim()) && !actualPort) {
276
354
  spinner.fail('Invalid or missing port configuration for RouteCodex server');
277
355
  logger.error('Please set httpserver.port in your configuration (e.g., ~/.routecodex/config.json) or use --port');
278
356
  process.exit(1);
279
357
  }
358
+ const configuredApiKey = (typeof options.apikey === 'string' && options.apikey.trim()
359
+ ? options.apikey.trim()
360
+ : null)
361
+ ?? (typeof process.env.ROUTECODEX_APIKEY === 'string' && process.env.ROUTECODEX_APIKEY.trim()
362
+ ? process.env.ROUTECODEX_APIKEY.trim()
363
+ : null)
364
+ ?? (typeof process.env.RCC_APIKEY === 'string' && process.env.RCC_APIKEY.trim()
365
+ ? process.env.RCC_APIKEY.trim()
366
+ : null)
367
+ ?? readConfigApiKey(configPath);
280
368
  // Check if RouteCodex server needs to be started
281
369
  if (options.ensureServer) {
282
370
  spinner.text = 'Checking RouteCodex server status...';
@@ -291,11 +379,13 @@ program
291
379
  return h || LOCAL_HOSTS.IPV4;
292
380
  };
293
381
  const connectHost = normalizeConnectHost(actualHost);
294
- const serverUrl = `http://${connectHost}:${actualPort}`;
382
+ const portPart = actualPort ? `:${actualPort}` : '';
383
+ const serverUrl = `${actualProtocol}://${connectHost}${portPart}${actualBasePath}`;
295
384
  try {
296
385
  const controller = new AbortController();
297
386
  const timeoutId = setTimeout(() => controller.abort(), 3000);
298
- const response = await fetch(`${serverUrl}/ready`, { signal: controller.signal, method: 'GET' });
387
+ const headers = configuredApiKey ? { 'x-api-key': configuredApiKey } : undefined;
388
+ const response = await fetch(`${serverUrl}/ready`, { signal: controller.signal, method: 'GET', headers });
299
389
  clearTimeout(timeoutId);
300
390
  if (!response.ok) {
301
391
  throw new Error('Server not ready');
@@ -307,6 +397,11 @@ program
307
397
  spinner.succeed('RouteCodex server is ready');
308
398
  }
309
399
  catch (error) {
400
+ if (options.url && String(options.url).trim()) {
401
+ spinner.fail('RouteCodex server is not reachable (ensure-server with --url cannot auto-start)');
402
+ logger.error(error instanceof Error ? error.message : String(error));
403
+ process.exit(1);
404
+ }
310
405
  spinner.info('RouteCodex server is not running, starting it...');
311
406
  // Start RouteCodex server in background
312
407
  const { spawn } = await import('child_process');
@@ -324,7 +419,8 @@ program
324
419
  for (let i = 0; i < 30; i++) {
325
420
  await sleep(1000);
326
421
  try {
327
- const res = await fetch(`${serverUrl}/ready`, { method: 'GET' });
422
+ const headers = configuredApiKey ? { 'x-api-key': configuredApiKey } : undefined;
423
+ const res = await fetch(`${serverUrl}/ready`, { method: 'GET', headers });
328
424
  if (res.ok) {
329
425
  const jr = await res.json().catch(() => ({}));
330
426
  if (jr?.status === 'ready') {
@@ -355,7 +451,8 @@ program
355
451
  }
356
452
  return actualHost || LOCAL_HOSTS.IPV4;
357
453
  })());
358
- const anthropicBase = `http://${resolvedBaseHost}:${actualPort}`;
454
+ const portPart = actualPort ? `:${actualPort}` : '';
455
+ const anthropicBase = `${actualProtocol}://${resolvedBaseHost}${portPart}${actualBasePath}`;
359
456
  const currentCwd = (() => {
360
457
  try {
361
458
  const d = options.cwd ? String(options.cwd) : process.cwd();
@@ -379,7 +476,7 @@ program
379
476
  // Cover both common env var names used by Anthropic SDK / tools
380
477
  ANTHROPIC_BASE_URL: anthropicBase,
381
478
  ANTHROPIC_API_URL: anthropicBase,
382
- ANTHROPIC_API_KEY: 'rcc-proxy-key'
479
+ ANTHROPIC_API_KEY: configuredApiKey || 'rcc-proxy-key'
383
480
  };
384
481
  // Avoid auth conflict: prefer API key routed via RouteCodex; remove shell tokens
385
482
  try {
@@ -409,8 +506,33 @@ program
409
506
  const sepIndex = afterCode.indexOf('--');
410
507
  const tail = sepIndex >= 0 ? afterCode.slice(sepIndex + 1) : afterCode;
411
508
  // 过滤本命令自身已识别的选项,剩余的作为透传参数
412
- const knownOpts = new Set(['-p', '--port', '-h', '--host', '-c', '--config', '--claude-path', '--model', '--profile', '--ensure-server']);
413
- const requireValue = new Set(['-p', '--port', '-h', '--host', '-c', '--config', '--claude-path', '--model', '--profile']);
509
+ const knownOpts = new Set([
510
+ '-p',
511
+ '--port',
512
+ '-h',
513
+ '--host',
514
+ '--url',
515
+ '-c',
516
+ '--config',
517
+ '--apikey',
518
+ '--claude-path',
519
+ '--model',
520
+ '--profile',
521
+ '--ensure-server'
522
+ ]);
523
+ const requireValue = new Set([
524
+ '-p',
525
+ '--port',
526
+ '-h',
527
+ '--host',
528
+ '--url',
529
+ '-c',
530
+ '--config',
531
+ '--apikey',
532
+ '--claude-path',
533
+ '--model',
534
+ '--profile'
535
+ ]);
414
536
  const passThrough = [];
415
537
  for (let i = 0; i < tail.length; i++) {
416
538
  const tok = tail[i];
@@ -466,10 +588,16 @@ program
466
588
  const envPath = String(process.env.CLAUDE_PATH || '').trim();
467
589
  return envPath || 'claude';
468
590
  })();
591
+ // Windows: Node spawn does not resolve .cmd shims unless using a shell. Prefer shell for bare commands.
592
+ const shouldUseShell = IS_WINDOWS &&
593
+ !path.extname(claudeBin) &&
594
+ !claudeBin.includes('/') &&
595
+ !claudeBin.includes('\\');
469
596
  const claudeProcess = spawn(claudeBin, claudeArgs, {
470
597
  stdio: 'inherit',
471
598
  env: claudeEnv,
472
- cwd: currentCwd
599
+ cwd: currentCwd,
600
+ shell: shouldUseShell
473
601
  });
474
602
  spinner.succeed('Claude Code launched with RouteCodex proxy');
475
603
  // Log normalized IPv4 host to avoid confusion (do not print ::/localhost)
@@ -490,6 +618,16 @@ program
490
618
  };
491
619
  process.on('SIGINT', () => { void shutdown('SIGINT'); });
492
620
  process.on('SIGTERM', () => { void shutdown('SIGTERM'); });
621
+ claudeProcess.on('error', (err) => {
622
+ try {
623
+ logger.error(`Failed to launch Claude Code (${claudeBin}): ${err instanceof Error ? err.message : String(err)}`);
624
+ if (IS_WINDOWS && shouldUseShell) {
625
+ logger.error('Tip: If Claude is installed via npm, ensure the shim is in PATH (e.g. claude.cmd).');
626
+ }
627
+ }
628
+ catch { /* ignore */ }
629
+ process.exit(1);
630
+ });
493
631
  claudeProcess.on('exit', (code, signal) => {
494
632
  if (signal) {
495
633
  process.exit(0);
@@ -769,12 +907,14 @@ program
769
907
  childProc.kill(sig);
770
908
  }
771
909
  catch (error) { /* ignore */ }
772
- try {
773
- if (childProc.pid) {
774
- process.kill(-childProc.pid, sig);
910
+ if (!IS_WINDOWS) {
911
+ try {
912
+ if (childProc.pid) {
913
+ process.kill(-childProc.pid, sig);
914
+ }
775
915
  }
916
+ catch (error) { /* ignore */ }
776
917
  }
777
- catch (error) { /* ignore */ }
778
918
  // 3) Wait briefly; if still listening, try SIGTERM/SIGKILL by port
779
919
  const deadline = Date.now() + 3500;
780
920
  while (Date.now() < deadline) {
@@ -786,10 +926,7 @@ program
786
926
  const remain = findListeningPids(resolvedPort);
787
927
  if (remain.length) {
788
928
  for (const pid of remain) {
789
- try {
790
- process.kill(pid, 'SIGTERM');
791
- }
792
- catch { /* ignore */ }
929
+ killPidBestEffort(pid, { force: false });
793
930
  }
794
931
  const killDeadline = Date.now() + 1500;
795
932
  while (Date.now() < killDeadline) {
@@ -802,10 +939,7 @@ program
802
939
  const still = findListeningPids(resolvedPort);
803
940
  if (still.length) {
804
941
  for (const pid of still) {
805
- try {
806
- process.kill(pid, 'SIGKILL');
807
- }
808
- catch { /* ignore */ }
942
+ killPidBestEffort(pid, { force: true });
809
943
  }
810
944
  }
811
945
  if (IS_DEV_PACKAGE) {
@@ -1138,10 +1272,7 @@ program
1138
1272
  return;
1139
1273
  }
1140
1274
  for (const pid of pids) {
1141
- try {
1142
- process.kill(pid, 'SIGTERM');
1143
- }
1144
- catch { /* ignore */ }
1275
+ killPidBestEffort(pid, { force: false });
1145
1276
  }
1146
1277
  const deadline = Date.now() + 3000;
1147
1278
  while (Date.now() < deadline) {
@@ -1157,10 +1288,7 @@ program
1157
1288
  const remain = findListeningPids(resolvedPort);
1158
1289
  if (remain.length) {
1159
1290
  for (const pid of remain) {
1160
- try {
1161
- process.kill(pid, 'SIGKILL');
1162
- }
1163
- catch { /* ignore */ }
1291
+ killPidBestEffort(pid, { force: true });
1164
1292
  }
1165
1293
  }
1166
1294
  spinner.succeed(`Force stopped server on ${resolvedPort}.`);
@@ -1233,10 +1361,7 @@ program
1233
1361
  const pids = findListeningPids(resolvedPort);
1234
1362
  if (pids.length) {
1235
1363
  for (const pid of pids) {
1236
- try {
1237
- process.kill(pid, 'SIGTERM');
1238
- }
1239
- catch { /* ignore */ }
1364
+ killPidBestEffort(pid, { force: false });
1240
1365
  }
1241
1366
  const deadline = Date.now() + 3500;
1242
1367
  while (Date.now() < deadline) {
@@ -1247,10 +1372,7 @@ program
1247
1372
  }
1248
1373
  const remain = findListeningPids(resolvedPort);
1249
1374
  for (const pid of remain) {
1250
- try {
1251
- process.kill(pid, 'SIGKILL');
1252
- }
1253
- catch { /* ignore */ }
1375
+ killPidBestEffort(pid, { force: true });
1254
1376
  }
1255
1377
  }
1256
1378
  spinner.text = 'Starting RouteCodex server...';
@@ -1288,12 +1410,14 @@ program
1288
1410
  child.kill(sig);
1289
1411
  }
1290
1412
  catch (error) { /* ignore */ }
1291
- try {
1292
- if (child.pid) {
1293
- process.kill(-child.pid, sig);
1413
+ if (!IS_WINDOWS) {
1414
+ try {
1415
+ if (child.pid) {
1416
+ process.kill(-child.pid, sig);
1417
+ }
1294
1418
  }
1419
+ catch (error) { /* ignore */ }
1295
1420
  }
1296
- catch (error) { /* ignore */ }
1297
1421
  const deadline = Date.now() + 3500;
1298
1422
  while (Date.now() < deadline) {
1299
1423
  if (findListeningPids(resolvedPort).length === 0) {
@@ -1303,10 +1427,7 @@ program
1303
1427
  }
1304
1428
  const remain = findListeningPids(resolvedPort);
1305
1429
  for (const pid of remain) {
1306
- try {
1307
- process.kill(pid, 'SIGTERM');
1308
- }
1309
- catch (error) { /* ignore */ }
1430
+ killPidBestEffort(pid, { force: false });
1310
1431
  }
1311
1432
  const killDeadline = Date.now() + 1500;
1312
1433
  while (Date.now() < killDeadline) {
@@ -1317,10 +1438,7 @@ program
1317
1438
  }
1318
1439
  const still = findListeningPids(resolvedPort);
1319
1440
  for (const pid of still) {
1320
- try {
1321
- process.kill(pid, 'SIGKILL');
1322
- }
1323
- catch (error) { /* ignore */ }
1441
+ killPidBestEffort(pid, { force: true });
1324
1442
  }
1325
1443
  // Ensure parent exits in any case
1326
1444
  try {
@@ -1623,7 +1741,7 @@ async function ensurePortAvailable(port, parentSpinner, opts = {}) {
1623
1741
  const pollInterval = 150;
1624
1742
  for (const pid of initialPids) {
1625
1743
  try {
1626
- process.kill(pid, 'SIGTERM');
1744
+ killPidBestEffort(pid, { force: false });
1627
1745
  }
1628
1746
  catch (error) {
1629
1747
  stopSpinner.warn(`Failed to send SIGTERM to PID ${pid}: ${error.message}`);
@@ -1645,7 +1763,7 @@ async function ensurePortAvailable(port, parentSpinner, opts = {}) {
1645
1763
  logger.warning(`Graceful stop timed out. Forcing SIGKILL to PID(s): ${remaining.join(', ')}`);
1646
1764
  for (const pid of remaining) {
1647
1765
  try {
1648
- process.kill(pid, 'SIGKILL');
1766
+ killPidBestEffort(pid, { force: true });
1649
1767
  }
1650
1768
  catch (error) {
1651
1769
  const message = error.message;
@@ -1676,6 +1794,14 @@ async function ensurePortAvailable(port, parentSpinner, opts = {}) {
1676
1794
  }
1677
1795
  function findListeningPids(port) {
1678
1796
  try {
1797
+ if (IS_WINDOWS) {
1798
+ const result = spawnSync('netstat', ['-ano', '-p', 'tcp'], { encoding: 'utf8' });
1799
+ if (result.error) {
1800
+ logger.warning(`netstat not available to inspect port usage: ${result.error.message}`);
1801
+ return [];
1802
+ }
1803
+ return parseNetstatListeningPids(result.stdout || '', port);
1804
+ }
1679
1805
  // macOS/BSD lsof expects either "-i TCP:port" or "-tiTCP:port" as a single argument.
1680
1806
  // Use the compact form to avoid treating ":port" as a filename.
1681
1807
  const result = spawnSync('lsof', [`-tiTCP:${port}`, '-sTCP:LISTEN'], { encoding: 'utf8' });
@@ -1846,7 +1972,7 @@ program
1846
1972
  const ksp = await createSpinner(`Killing ${pids.length} listener(s) on ${port}...`);
1847
1973
  for (const pid of pids) {
1848
1974
  try {
1849
- process.kill(pid, 'SIGKILL');
1975
+ killPidBestEffort(pid, { force: true });
1850
1976
  }
1851
1977
  catch (e) {
1852
1978
  ksp.warn(`Failed to kill ${pid}: ${e.message}`);