@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.
- package/README.md +1 -42
- package/dist/build-info.js +2 -2
- package/dist/build-info.js.map +1 -1
- package/dist/cli.js +181 -55
- package/dist/cli.js.map +1 -1
- package/dist/commands/quota-daemon.d.ts +2 -0
- package/dist/commands/quota-daemon.js +89 -0
- package/dist/commands/quota-daemon.js.map +1 -0
- package/dist/docs/daemon-admin-ui.html +958 -0
- package/dist/index.js +46 -10
- package/dist/index.js.map +1 -1
- package/dist/manager/modules/quota/index.d.ts +34 -0
- package/dist/manager/modules/quota/index.js +291 -0
- package/dist/manager/modules/quota/index.js.map +1 -1
- package/dist/manager/modules/token/index.js +13 -2
- package/dist/manager/modules/token/index.js.map +1 -1
- package/dist/manager/quota/provider-quota-center.d.ts +48 -0
- package/dist/manager/quota/provider-quota-center.js +239 -0
- package/dist/manager/quota/provider-quota-center.js.map +1 -0
- package/dist/manager/quota/provider-quota-store.d.ts +17 -0
- package/dist/manager/quota/provider-quota-store.js +88 -0
- package/dist/manager/quota/provider-quota-store.js.map +1 -0
- package/dist/providers/auth/token-scanner/index.js +11 -3
- package/dist/providers/auth/token-scanner/index.js.map +1 -1
- package/dist/providers/core/runtime/http-request-executor.js +24 -7
- package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.js +11 -3
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/runtime/responses-provider.js +9 -3
- package/dist/providers/core/runtime/responses-provider.js.map +1 -1
- package/dist/providers/core/utils/http-client.d.ts +1 -0
- package/dist/providers/core/utils/http-client.js +139 -4
- package/dist/providers/core/utils/http-client.js.map +1 -1
- package/dist/providers/core/utils/snapshot-writer.d.ts +12 -0
- package/dist/providers/core/utils/snapshot-writer.js +99 -18
- package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
- package/dist/providers/mock/mock-provider-runtime.d.ts +3 -0
- package/dist/providers/mock/mock-provider-runtime.js +176 -4
- package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
- package/dist/server/handlers/chat-handler.js +13 -1
- package/dist/server/handlers/chat-handler.js.map +1 -1
- package/dist/server/handlers/handler-utils.js +5 -0
- package/dist/server/handlers/handler-utils.js.map +1 -1
- package/dist/server/handlers/messages-handler.js +13 -1
- package/dist/server/handlers/messages-handler.js.map +1 -1
- package/dist/server/handlers/responses-handler.js +73 -1
- package/dist/server/handlers/responses-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +174 -2
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +519 -0
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
- package/dist/server/runtime/http-server/executor-response.js +6 -0
- package/dist/server/runtime/http-server/executor-response.js.map +1 -1
- package/dist/server/runtime/http-server/index.d.ts +5 -0
- package/dist/server/runtime/http-server/index.js +205 -4
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/middleware.d.ts +2 -0
- package/dist/server/runtime/http-server/middleware.js +63 -0
- package/dist/server/runtime/http-server/middleware.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.d.ts +2 -0
- package/dist/server/runtime/http-server/request-executor.js +57 -10
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/dist/server/runtime/http-server/routes.js +38 -1
- package/dist/server/runtime/http-server/routes.js.map +1 -1
- package/dist/server/runtime/http-server/stats-manager.d.ts +55 -0
- package/dist/server/runtime/http-server/stats-manager.js +462 -4
- package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
- package/dist/server/runtime/http-server/types.d.ts +1 -0
- package/dist/token-daemon/token-daemon.js +70 -25
- package/dist/token-daemon/token-daemon.js.map +1 -1
- package/dist/token-daemon/token-utils.d.ts +1 -0
- package/dist/token-daemon/token-utils.js +9 -1
- package/dist/token-daemon/token-utils.js.map +1 -1
- package/dist/tools/semantic-replay.js +29 -0
- package/dist/tools/semantic-replay.js.map +1 -1
- package/dist/utils/is-direct-execution.d.ts +1 -0
- package/dist/utils/is-direct-execution.js +15 -0
- package/dist/utils/is-direct-execution.js.map +1 -0
- package/dist/utils/snapshot-writer.d.ts +2 -0
- package/dist/utils/snapshot-writer.js +47 -4
- package/dist/utils/snapshot-writer.js.map +1 -1
- package/dist/utils/windows-netstat.d.ts +1 -0
- package/dist/utils/windows-netstat.js +34 -0
- package/dist/utils/windows-netstat.js.map +1 -0
- package/package.json +3 -4
- package/scripts/analyze-codex-error-failures.mjs +24 -14
- package/scripts/classify-codex-samples.mjs +0 -35
- package/scripts/copy-modules-config.mjs +17 -1
- package/scripts/generate-snapshot-data.mjs +41 -11
- package/scripts/mock-provider/extract.mjs +239 -21
- package/scripts/mock-provider/run-regressions.mjs +79 -16
- package/scripts/quota-dryrun.mjs +124 -0
- package/scripts/tests/apply-patch-loop.mjs +5 -1
- package/scripts/tests/exec-command-loop.mjs +16 -19
- package/scripts/verify-apply-patch.mjs +335 -5
- package/scripts/verify-e2e-toolcall.mjs +49 -10
- 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`。
|
package/dist/build-info.js
CHANGED
package/dist/build-info.js.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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([
|
|
413
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1975
|
+
killPidBestEffort(pid, { force: true });
|
|
1850
1976
|
}
|
|
1851
1977
|
catch (e) {
|
|
1852
1978
|
ksp.warn(`Failed to kill ${pid}: ${e.message}`);
|