@jsonstudio/rcc 0.89.683 → 0.89.912

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 (215) hide show
  1. package/README.md +44 -0
  2. package/dist/build-info.js +2 -2
  3. package/dist/cli.js +164 -116
  4. package/dist/cli.js.map +1 -1
  5. package/dist/client/anthropic/anthropic-protocol-client.js +42 -1
  6. package/dist/client/anthropic/anthropic-protocol-client.js.map +1 -1
  7. package/dist/client/gemini-cli/gemini-cli-protocol-client.js +4 -1
  8. package/dist/client/gemini-cli/gemini-cli-protocol-client.js.map +1 -1
  9. package/dist/commands/camoufox-backfill.d.ts +2 -0
  10. package/dist/commands/camoufox-backfill.js +33 -0
  11. package/dist/commands/camoufox-backfill.js.map +1 -0
  12. package/dist/commands/camoufox-fp.d.ts +2 -0
  13. package/dist/commands/camoufox-fp.js +86 -0
  14. package/dist/commands/camoufox-fp.js.map +1 -0
  15. package/dist/commands/oauth.d.ts +2 -0
  16. package/dist/commands/oauth.js +170 -0
  17. package/dist/commands/oauth.js.map +1 -0
  18. package/dist/commands/provider-update.js +439 -2
  19. package/dist/commands/provider-update.js.map +1 -1
  20. package/dist/commands/quota-status.d.ts +2 -0
  21. package/dist/commands/quota-status.js +80 -0
  22. package/dist/commands/quota-status.js.map +1 -0
  23. package/dist/commands/token-daemon.js +12 -1
  24. package/dist/commands/token-daemon.js.map +1 -1
  25. package/dist/config/provider-v2-loader.d.ts +16 -0
  26. package/dist/config/provider-v2-loader.js +84 -0
  27. package/dist/config/provider-v2-loader.js.map +1 -0
  28. package/dist/config/routecodex-config-loader.js +27 -4
  29. package/dist/config/routecodex-config-loader.js.map +1 -1
  30. package/dist/config/system-prompts/codex-cli.txt +1 -0
  31. package/dist/config/virtual-router-builder.d.ts +9 -0
  32. package/dist/config/virtual-router-builder.js +34 -0
  33. package/dist/config/virtual-router-builder.js.map +1 -0
  34. package/dist/config/virtual-router-types.d.ts +25 -0
  35. package/dist/config/virtual-router-types.js +30 -0
  36. package/dist/config/virtual-router-types.js.map +1 -0
  37. package/dist/manager/index.d.ts +10 -0
  38. package/dist/manager/index.js +27 -0
  39. package/dist/manager/index.js.map +1 -0
  40. package/dist/manager/modules/health/index.d.ts +22 -0
  41. package/dist/manager/modules/health/index.js +82 -0
  42. package/dist/manager/modules/health/index.js.map +1 -0
  43. package/dist/manager/modules/quota/index.d.ts +57 -0
  44. package/dist/manager/modules/quota/index.js +426 -0
  45. package/dist/manager/modules/quota/index.js.map +1 -0
  46. package/dist/manager/modules/routing/index.d.ts +17 -0
  47. package/dist/manager/modules/routing/index.js +61 -0
  48. package/dist/manager/modules/routing/index.js.map +1 -0
  49. package/dist/manager/modules/token/index.d.ts +10 -0
  50. package/dist/manager/modules/token/index.js +58 -0
  51. package/dist/manager/modules/token/index.js.map +1 -0
  52. package/dist/manager/storage/base-store.d.ts +6 -0
  53. package/dist/manager/storage/base-store.js +2 -0
  54. package/dist/manager/storage/base-store.js.map +1 -0
  55. package/dist/manager/storage/file-store.d.ts +25 -0
  56. package/dist/manager/storage/file-store.js +117 -0
  57. package/dist/manager/storage/file-store.js.map +1 -0
  58. package/dist/manager/types.d.ts +9 -0
  59. package/dist/manager/types.js +2 -0
  60. package/dist/manager/types.js.map +1 -0
  61. package/dist/message-center/index.d.ts +5 -0
  62. package/dist/message-center/index.js +6 -0
  63. package/dist/message-center/index.js.map +1 -0
  64. package/dist/message-center/message-center.d.ts +93 -0
  65. package/dist/message-center/message-center.js +189 -0
  66. package/dist/message-center/message-center.js.map +1 -0
  67. package/dist/providers/auth/antigravity-userinfo-helper.d.ts +2 -0
  68. package/dist/providers/auth/antigravity-userinfo-helper.js +102 -0
  69. package/dist/providers/auth/antigravity-userinfo-helper.js.map +1 -1
  70. package/dist/providers/auth/iflow-cookie-auth.d.ts +27 -0
  71. package/dist/providers/auth/iflow-cookie-auth.js +209 -0
  72. package/dist/providers/auth/iflow-cookie-auth.js.map +1 -0
  73. package/dist/providers/auth/oauth-lifecycle.js +29 -22
  74. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  75. package/dist/providers/auth/token-scanner/index.js +16 -1
  76. package/dist/providers/auth/token-scanner/index.js.map +1 -1
  77. package/dist/providers/core/config/camoufox-launcher.d.ts +16 -0
  78. package/dist/providers/core/config/camoufox-launcher.js +314 -0
  79. package/dist/providers/core/config/camoufox-launcher.js.map +1 -0
  80. package/dist/providers/core/config/oauth-flows.d.ts +9 -0
  81. package/dist/providers/core/config/oauth-flows.js +50 -19
  82. package/dist/providers/core/config/oauth-flows.js.map +1 -1
  83. package/dist/providers/core/config/provider-oauth-configs.d.ts +6 -0
  84. package/dist/providers/core/config/provider-oauth-configs.js +12 -0
  85. package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
  86. package/dist/providers/core/config/service-profiles.js +26 -3
  87. package/dist/providers/core/config/service-profiles.js.map +1 -1
  88. package/dist/providers/core/runtime/antigravity-quota-client.d.ts +10 -0
  89. package/dist/providers/core/runtime/antigravity-quota-client.js +88 -0
  90. package/dist/providers/core/runtime/antigravity-quota-client.js.map +1 -0
  91. package/dist/providers/core/runtime/base-provider.d.ts +2 -1
  92. package/dist/providers/core/runtime/base-provider.js +93 -34
  93. package/dist/providers/core/runtime/base-provider.js.map +1 -1
  94. package/dist/providers/core/runtime/gemini-cli-http-provider.js +42 -10
  95. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  96. package/dist/providers/core/runtime/http-request-executor.js +24 -0
  97. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  98. package/dist/providers/core/runtime/http-transport-provider.d.ts +0 -3
  99. package/dist/providers/core/runtime/http-transport-provider.js +32 -136
  100. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  101. package/dist/providers/core/runtime/provider-error-classifier.js +18 -10
  102. package/dist/providers/core/runtime/provider-error-classifier.js.map +1 -1
  103. package/dist/providers/core/runtime/rate-limit-manager.d.ts +6 -0
  104. package/dist/providers/core/runtime/rate-limit-manager.js +23 -0
  105. package/dist/providers/core/runtime/rate-limit-manager.js.map +1 -1
  106. package/dist/providers/core/runtime/responses-provider.js +17 -19
  107. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  108. package/dist/providers/core/strategies/oauth-auth-code-flow.d.ts +1 -0
  109. package/dist/providers/core/strategies/oauth-auth-code-flow.js +3 -2
  110. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  111. package/dist/providers/core/strategies/oauth-device-flow.d.ts +1 -0
  112. package/dist/providers/core/strategies/oauth-device-flow.js +3 -2
  113. package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
  114. package/dist/providers/core/strategies/oauth-hybrid-flow.d.ts +1 -0
  115. package/dist/providers/core/strategies/oauth-hybrid-flow.js +3 -2
  116. package/dist/providers/core/strategies/oauth-hybrid-flow.js.map +1 -1
  117. package/dist/providers/core/utils/http-client.js +43 -1
  118. package/dist/providers/core/utils/http-client.js.map +1 -1
  119. package/dist/providers/mock/mock-provider-runtime.js +4 -4
  120. package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
  121. package/dist/providers/profile/provider-profile-loader.js +13 -1
  122. package/dist/providers/profile/provider-profile-loader.js.map +1 -1
  123. package/dist/providers/profile/provider-profile.d.ts +5 -0
  124. package/dist/scripts/camoufox/gen-fingerprint-env.py +171 -0
  125. package/dist/scripts/camoufox/launch-auth.mjs +617 -0
  126. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.d.ts +3 -0
  127. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +138 -0
  128. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -0
  129. package/dist/server/runtime/http-server/daemon-admin/providers-handler.d.ts +3 -0
  130. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +166 -0
  131. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -0
  132. package/dist/server/runtime/http-server/daemon-admin/quota-handler.d.ts +3 -0
  133. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +109 -0
  134. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -0
  135. package/dist/server/runtime/http-server/daemon-admin/status-handler.d.ts +3 -0
  136. package/dist/server/runtime/http-server/daemon-admin/status-handler.js +43 -0
  137. package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -0
  138. package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +19 -0
  139. package/dist/server/runtime/http-server/daemon-admin-routes.js +27 -0
  140. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -0
  141. package/dist/server/runtime/http-server/executor-provider.d.ts +1 -0
  142. package/dist/server/runtime/http-server/executor-provider.js +26 -0
  143. package/dist/server/runtime/http-server/executor-provider.js.map +1 -1
  144. package/dist/server/runtime/http-server/executor-response.d.ts +16 -0
  145. package/dist/server/runtime/http-server/executor-response.js +164 -0
  146. package/dist/server/runtime/http-server/executor-response.js.map +1 -0
  147. package/dist/server/runtime/http-server/index.d.ts +6 -0
  148. package/dist/server/runtime/http-server/index.js +121 -53
  149. package/dist/server/runtime/http-server/index.js.map +1 -1
  150. package/dist/server/runtime/http-server/request-executor.d.ts +3 -0
  151. package/dist/server/runtime/http-server/request-executor.js +73 -21
  152. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  153. package/dist/server/runtime/http-server/routes.d.ts +5 -0
  154. package/dist/server/runtime/http-server/routes.js +45 -1
  155. package/dist/server/runtime/http-server/routes.js.map +1 -1
  156. package/dist/server/runtime/http-server/types.d.ts +1 -0
  157. package/dist/server/utils/client-connection-state.d.ts +8 -0
  158. package/dist/server/utils/client-connection-state.js +52 -0
  159. package/dist/server/utils/client-connection-state.js.map +1 -0
  160. package/dist/server/utils/request-id-manager.js +21 -3
  161. package/dist/server/utils/request-id-manager.js.map +1 -1
  162. package/dist/token-daemon/history-store.d.ts +2 -0
  163. package/dist/token-daemon/history-store.js +6 -2
  164. package/dist/token-daemon/history-store.js.map +1 -1
  165. package/dist/token-daemon/index.js +36 -5
  166. package/dist/token-daemon/index.js.map +1 -1
  167. package/dist/token-daemon/leader-lock.d.ts +11 -0
  168. package/dist/token-daemon/leader-lock.js +79 -0
  169. package/dist/token-daemon/leader-lock.js.map +1 -0
  170. package/dist/token-daemon/message-bus-integrator.d.ts +98 -0
  171. package/dist/token-daemon/message-bus-integrator.js +144 -0
  172. package/dist/token-daemon/message-bus-integrator.js.map +1 -0
  173. package/dist/token-daemon/provider-registry.d.ts +22 -0
  174. package/dist/token-daemon/provider-registry.js +201 -0
  175. package/dist/token-daemon/provider-registry.js.map +1 -0
  176. package/dist/token-daemon/token-daemon.d.ts +8 -0
  177. package/dist/token-daemon/token-daemon.js +196 -11
  178. package/dist/token-daemon/token-daemon.js.map +1 -1
  179. package/dist/token-portal/local-token-portal.d.ts +1 -0
  180. package/dist/token-portal/local-token-portal.js +18 -0
  181. package/dist/token-portal/local-token-portal.js.map +1 -1
  182. package/dist/token-portal/render.js +1 -0
  183. package/dist/token-portal/render.js.map +1 -1
  184. package/dist/tools/error-log.d.ts +31 -0
  185. package/dist/tools/error-log.js +117 -0
  186. package/dist/tools/error-log.js.map +1 -0
  187. package/dist/tools/stats-request-events.d.ts +2 -0
  188. package/dist/tools/stats-request-events.js +16 -0
  189. package/dist/tools/stats-request-events.js.map +1 -0
  190. package/dist/tools/stats-usage.d.ts +31 -0
  191. package/dist/tools/stats-usage.js +206 -0
  192. package/dist/tools/stats-usage.js.map +1 -0
  193. package/package.json +9 -4
  194. package/scripts/analyze-codex-error-failures.mjs +111 -0
  195. package/scripts/analyze-usage-estimate.mjs +240 -0
  196. package/scripts/camoufox/gen-fingerprint-env.py +171 -0
  197. package/scripts/camoufox/launch-auth.mjs +617 -0
  198. package/scripts/classify-codex-samples.mjs +251 -0
  199. package/scripts/cleanup-codex-error-samples.mjs +88 -0
  200. package/scripts/compare-codex-rccx.mjs +268 -0
  201. package/scripts/copy-compat-assets.mjs +18 -0
  202. package/scripts/install-release.sh +1 -1
  203. package/scripts/local-replay-openai-response.mjs +1 -2
  204. package/scripts/pack-mode.mjs +16 -6
  205. package/scripts/replay-codex-sample.mjs +24 -2
  206. package/scripts/responses-compare-server.mjs +119 -0
  207. package/scripts/tests/apply-patch-loop.mjs +266 -7
  208. package/scripts/tests/exec-command-loop.mjs +165 -0
  209. package/scripts/tool-classification-report.ts +281 -0
  210. package/scripts/verification/samples/openai-chat-list-local-files.json +1 -1
  211. package/scripts/verify-apply-patch.mjs +28 -17
  212. package/scripts/verify-codex-error-samples.mjs +102 -0
  213. package/scripts/verify-e2e-toolcall.mjs +71 -4
  214. package/scripts/virtual-router-shadow-v2-real.mjs +143 -0
  215. package/scripts/virtual-router-shadow-v2.mjs +122 -0
package/README.md CHANGED
@@ -228,6 +228,50 @@ 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
+ ---
274
+
231
275
  ## 参考文档
232
276
 
233
277
  - `docs/ARCHITECTURE.md` – 全量架构细节与数据流
@@ -1,6 +1,6 @@
1
1
  export const buildInfo = {
2
2
  mode: 'release',
3
- version: '0.89.683',
4
- buildTime: '2026-01-04T14:19:20.587Z'
3
+ version: '0.89.912',
4
+ buildTime: '2026-01-11T04:53:36.258Z'
5
5
  };
6
6
  //# sourceMappingURL=build-info.js.map
package/dist/cli.js CHANGED
@@ -118,84 +118,64 @@ program
118
118
  .description('RouteCodex CLI - Multi-provider OpenAI proxy server and Claude Code interface')
119
119
  .version(cliVersion);
120
120
  async function ensureTokenDaemonAutoStart() {
121
+ // Token 刷新逻辑已经在服务器进程内通过 ManagerDaemon/TokenManagerModule 执行。
122
+ // 为避免重复启动独立的 token-daemon 进程,这里不再自动拉起后台守护,仅保留显式 CLI 命令。
123
+ const disabledEnv = String(process.env.ROUTECODEX_TOKEN_DAEMON_DISABLED || process.env.RCC_TOKEN_DAEMON_DISABLED || '')
124
+ .trim()
125
+ .toLowerCase();
126
+ if (disabledEnv !== '1' && disabledEnv !== 'true' && disabledEnv !== 'yes') {
127
+ logger.info('Token manager is now integrated into the server process; automatic external token-daemon auto-start is disabled.');
128
+ }
129
+ }
130
+ async function stopTokenDaemonIfRunning() {
121
131
  try {
122
- const disabledEnv = String(process.env.ROUTECODEX_TOKEN_DAEMON_DISABLED || process.env.RCC_TOKEN_DAEMON_DISABLED || '')
123
- .trim()
124
- .toLowerCase();
125
- if (disabledEnv === '1' || disabledEnv === 'true' || disabledEnv === 'yes') {
126
- logger.info('Token daemon auto-start disabled via env (ROUTECODEX_TOKEN_DAEMON_DISABLED/RCC_TOKEN_DAEMON_DISABLED)');
132
+ if (!fs.existsSync(TOKEN_DAEMON_PID_FILE)) {
127
133
  return;
128
134
  }
129
- let existingPid = null;
135
+ const txt = fs.readFileSync(TOKEN_DAEMON_PID_FILE, 'utf8');
136
+ const parsed = Number(String(txt || '').trim());
137
+ if (!Number.isFinite(parsed) || parsed <= 0) {
138
+ return;
139
+ }
140
+ const pid = parsed;
141
+ let running = false;
130
142
  try {
131
- if (fs.existsSync(TOKEN_DAEMON_PID_FILE)) {
132
- const txt = fs.readFileSync(TOKEN_DAEMON_PID_FILE, 'utf8');
133
- const parsed = Number(String(txt || '').trim());
134
- if (Number.isFinite(parsed) && parsed > 0) {
135
- existingPid = parsed;
136
- }
137
- }
143
+ process.kill(pid, 0);
144
+ running = true;
138
145
  }
139
146
  catch {
140
- existingPid = null;
147
+ running = false;
141
148
  }
142
- const waitForProcessExit = async (pid, timeoutMs) => {
143
- const deadline = Date.now() + timeoutMs;
144
- while (Date.now() < deadline) {
145
- try {
146
- process.kill(pid, 0);
147
- }
148
- catch {
149
- return;
150
- }
151
- await new Promise((resolve) => setTimeout(resolve, 100));
152
- }
153
- };
154
- if (existingPid) {
155
- let running = false;
156
- try {
157
- process.kill(existingPid, 0);
158
- running = true;
159
- }
160
- catch {
161
- running = false;
162
- }
163
- if (running) {
164
- logger.info(`Restarting token daemon to refresh environment (pid=${existingPid})`);
165
- try {
166
- process.kill(existingPid, 'SIGTERM');
167
- }
168
- catch {
169
- // ignore
170
- }
171
- await waitForProcessExit(existingPid, 2000);
172
- }
149
+ if (!running) {
173
150
  try {
174
151
  fs.unlinkSync(TOKEN_DAEMON_PID_FILE);
175
152
  }
176
- catch {
177
- // ignore
178
- }
153
+ catch { /* ignore */ }
154
+ return;
179
155
  }
180
- const nodeBin = process.execPath;
181
- const cliEntry = path.resolve(__dirname, 'cli.js');
182
- const args = [cliEntry, 'token-daemon', 'start'];
183
- const { spawn } = await import('child_process');
184
- const child = spawn(nodeBin, args, {
185
- stdio: 'ignore',
186
- detached: true,
187
- env: { ...process.env }
188
- });
189
156
  try {
190
- child.unref();
157
+ process.kill(pid, 'SIGTERM');
191
158
  }
192
159
  catch {
193
160
  // ignore
194
161
  }
195
- logger.info('Token daemon auto-started in background');
162
+ const deadline = Date.now() + 2000;
163
+ while (Date.now() < deadline) {
164
+ try {
165
+ process.kill(pid, 0);
166
+ }
167
+ catch {
168
+ break;
169
+ }
170
+ await new Promise(resolve => setTimeout(resolve, 100));
171
+ }
172
+ try {
173
+ fs.unlinkSync(TOKEN_DAEMON_PID_FILE);
174
+ }
175
+ catch { /* ignore */ }
196
176
  }
197
- catch (error) {
198
- logger.debug(`Failed to auto-start token daemon: ${error instanceof Error ? error.message : String(error)}`);
177
+ catch {
178
+ // best-effort: failures here must not break CLI shutdown
199
179
  }
200
180
  }
201
181
  // Provider command group - update models and generate minimal provider config
@@ -204,12 +184,36 @@ try {
204
184
  program.addCommand(createProviderUpdateCommand());
205
185
  }
206
186
  catch { /* optional: command not available in some builds */ }
187
+ // Camoufox fingerprint debug command (optional)
188
+ try {
189
+ const { createCamoufoxFpCommand } = await import('./commands/camoufox-fp.js');
190
+ program.addCommand(createCamoufoxFpCommand());
191
+ }
192
+ catch { /* optional */ }
193
+ // Camoufox fingerprint backfill command (optional)
194
+ try {
195
+ const { createCamoufoxBackfillCommand } = await import('./commands/camoufox-backfill.js');
196
+ program.addCommand(createCamoufoxBackfillCommand());
197
+ }
198
+ catch { /* optional */ }
207
199
  // Token daemon command group - manage OAuth tokens
208
200
  try {
209
201
  const { createTokenDaemonCommand } = await import('./commands/token-daemon.js');
210
202
  program.addCommand(createTokenDaemonCommand());
211
203
  }
212
204
  catch { /* optional: command not available in some builds */ }
205
+ // Quota status command - inspect daemon-managed quota snapshot
206
+ try {
207
+ const { createQuotaStatusCommand } = await import('./commands/quota-status.js');
208
+ program.addCommand(createQuotaStatusCommand());
209
+ }
210
+ catch { /* optional */ }
211
+ // OAuth command - force re-auth for a specific token (Camoufox-aware when enabled)
212
+ try {
213
+ const { createOauthCommand } = await import('./commands/oauth.js');
214
+ program.addCommand(createOauthCommand());
215
+ }
216
+ catch { /* optional: command not available in some builds */ }
213
217
  // Validate command - auto start server then run E2E checks
214
218
  try {
215
219
  const { createValidateCommand } = await import('./commands/validate.js');
@@ -796,6 +800,9 @@ program
796
800
  catch { /* ignore */ }
797
801
  }
798
802
  }
803
+ if (IS_DEV_PACKAGE) {
804
+ await stopTokenDaemonIfRunning();
805
+ }
799
806
  // Ensure parent exits even if child fails to exit
800
807
  try {
801
808
  process.exit(0);
@@ -1072,37 +1079,54 @@ program
1072
1079
  .action(async () => {
1073
1080
  const spinner = await createSpinner('Stopping RouteCodex server...');
1074
1081
  try {
1075
- // Resolve config path and port
1076
- const configPath = path.join(homedir(), '.routecodex', 'config.json');
1077
- // Check if config exists
1078
- if (!fs.existsSync(configPath)) {
1079
- spinner.fail(`Configuration file not found: ${configPath}`);
1080
- logger.error('Cannot determine server port without configuration file');
1081
- logger.info('Please create a configuration file first:');
1082
- logger.info(' rcc config init');
1083
- process.exit(1);
1084
- }
1085
- // Load configuration to get port
1086
- let config;
1087
- try {
1088
- const configContent = fs.readFileSync(configPath, 'utf8');
1089
- config = JSON.parse(configContent);
1090
- }
1091
- catch (error) {
1092
- spinner.fail('Failed to parse configuration file');
1093
- logger.error(`Invalid JSON in configuration file: ${configPath}`);
1094
- process.exit(1);
1082
+ let resolvedPort;
1083
+ if (IS_DEV_PACKAGE) {
1084
+ const envPort = Number(process.env.ROUTECODEX_PORT || process.env.RCC_PORT || NaN);
1085
+ if (!Number.isNaN(envPort) && envPort > 0) {
1086
+ logger.info(`Using port ${envPort} from environment (ROUTECODEX_PORT/RCC_PORT) [dev package: routecodex]`);
1087
+ resolvedPort = envPort;
1088
+ }
1089
+ else {
1090
+ resolvedPort = DEFAULT_DEV_PORT;
1091
+ logger.info(`Using dev default port ${resolvedPort} (routecodex dev package)`);
1092
+ }
1095
1093
  }
1096
- const port = (config?.httpserver?.port ?? config?.server?.port ?? config?.port);
1097
- if (!port || typeof port !== 'number' || port <= 0) {
1098
- spinner.fail('Invalid or missing port configuration');
1099
- logger.error('Configuration file must specify a valid port number');
1100
- process.exit(1);
1094
+ else {
1095
+ // Resolve config path and port
1096
+ const configPath = path.join(homedir(), '.routecodex', 'config.json');
1097
+ // Check if config exists
1098
+ if (!fs.existsSync(configPath)) {
1099
+ spinner.fail(`Configuration file not found: ${configPath}`);
1100
+ logger.error('Cannot determine server port without configuration file');
1101
+ logger.info('Please create a configuration file first:');
1102
+ logger.info(' rcc config init');
1103
+ process.exit(1);
1104
+ }
1105
+ // Load configuration to get port
1106
+ let config;
1107
+ try {
1108
+ const configContent = fs.readFileSync(configPath, 'utf8');
1109
+ config = JSON.parse(configContent);
1110
+ }
1111
+ catch (error) {
1112
+ spinner.fail('Failed to parse configuration file');
1113
+ logger.error(`Invalid JSON in configuration file: ${configPath}`);
1114
+ process.exit(1);
1115
+ }
1116
+ const port = (config?.httpserver?.port ?? config?.server?.port ?? config?.port);
1117
+ if (!port || typeof port !== 'number' || port <= 0) {
1118
+ spinner.fail('Invalid or missing port configuration');
1119
+ logger.error('Configuration file must specify a valid port number');
1120
+ process.exit(1);
1121
+ }
1122
+ resolvedPort = port;
1101
1123
  }
1102
- const resolvedPort = port;
1103
1124
  const pids = findListeningPids(resolvedPort);
1104
1125
  if (!pids.length) {
1105
1126
  spinner.succeed(`No server listening on ${resolvedPort}.`);
1127
+ if (IS_DEV_PACKAGE) {
1128
+ await stopTokenDaemonIfRunning();
1129
+ }
1106
1130
  return;
1107
1131
  }
1108
1132
  for (const pid of pids) {
@@ -1115,6 +1139,9 @@ program
1115
1139
  while (Date.now() < deadline) {
1116
1140
  if (findListeningPids(resolvedPort).length === 0) {
1117
1141
  spinner.succeed(`Stopped server on ${resolvedPort}.`);
1142
+ if (IS_DEV_PACKAGE) {
1143
+ await stopTokenDaemonIfRunning();
1144
+ }
1118
1145
  return;
1119
1146
  }
1120
1147
  await sleep(100);
@@ -1129,6 +1156,9 @@ program
1129
1156
  }
1130
1157
  }
1131
1158
  spinner.succeed(`Force stopped server on ${resolvedPort}.`);
1159
+ if (IS_DEV_PACKAGE) {
1160
+ await stopTokenDaemonIfRunning();
1161
+ }
1132
1162
  }
1133
1163
  catch (e) {
1134
1164
  spinner.fail(`Failed to stop: ${e.message}`);
@@ -1146,34 +1176,51 @@ program
1146
1176
  .action(async (options) => {
1147
1177
  const spinner = await createSpinner('Restarting RouteCodex server...');
1148
1178
  try {
1149
- // Resolve config path
1150
- const configPath = options.config || path.join(homedir(), '.routecodex', 'config.json');
1151
- // Check if config exists
1152
- if (!fs.existsSync(configPath)) {
1153
- spinner.fail(`Configuration file not found: ${configPath}`);
1154
- logger.error('Cannot determine server port without configuration file');
1155
- logger.info('Please create a configuration file first:');
1156
- logger.info(' rcc config init');
1157
- process.exit(1);
1158
- }
1159
- // Load configuration to get port
1160
- let config;
1161
- try {
1162
- const configContent = fs.readFileSync(configPath, 'utf8');
1163
- config = JSON.parse(configContent);
1164
- }
1165
- catch (error) {
1166
- spinner.fail('Failed to parse configuration file');
1167
- logger.error(`Invalid JSON in configuration file: ${configPath}`);
1168
- process.exit(1);
1179
+ let resolvedPort;
1180
+ let resolvedHost = LOCAL_HOSTS.LOCALHOST;
1181
+ if (IS_DEV_PACKAGE) {
1182
+ const envPort = Number(process.env.ROUTECODEX_PORT || process.env.RCC_PORT || NaN);
1183
+ if (!Number.isNaN(envPort) && envPort > 0) {
1184
+ logger.info(`Using port ${envPort} from environment (ROUTECODEX_PORT/RCC_PORT) [dev package: routecodex]`);
1185
+ resolvedPort = envPort;
1186
+ }
1187
+ else {
1188
+ resolvedPort = DEFAULT_DEV_PORT;
1189
+ logger.info(`Using dev default port ${resolvedPort} (routecodex dev package)`);
1190
+ }
1169
1191
  }
1170
- const port = (config?.httpserver?.port ?? config?.server?.port ?? config?.port);
1171
- if (!port || typeof port !== 'number' || port <= 0) {
1172
- spinner.fail('Invalid or missing port configuration');
1173
- logger.error('Configuration file must specify a valid port number');
1174
- process.exit(1);
1192
+ else {
1193
+ // Resolve config path
1194
+ const configPath = options.config || path.join(homedir(), '.routecodex', 'config.json');
1195
+ // Check if config exists
1196
+ if (!fs.existsSync(configPath)) {
1197
+ spinner.fail(`Configuration file not found: ${configPath}`);
1198
+ logger.error('Cannot determine server port without configuration file');
1199
+ logger.info('Please create a configuration file first:');
1200
+ logger.info(' rcc config init');
1201
+ process.exit(1);
1202
+ }
1203
+ // Load configuration to get port
1204
+ let config;
1205
+ try {
1206
+ const configContent = fs.readFileSync(configPath, 'utf8');
1207
+ config = JSON.parse(configContent);
1208
+ }
1209
+ catch (error) {
1210
+ spinner.fail('Failed to parse configuration file');
1211
+ logger.error(`Invalid JSON in configuration file: ${configPath}`);
1212
+ process.exit(1);
1213
+ }
1214
+ const port = (config?.httpserver?.port ?? config?.server?.port ?? config?.port);
1215
+ if (!port || typeof port !== 'number' || port <= 0) {
1216
+ spinner.fail('Invalid or missing port configuration');
1217
+ logger.error('Configuration file must specify a valid port number');
1218
+ process.exit(1);
1219
+ }
1220
+ resolvedPort = port;
1221
+ resolvedHost =
1222
+ (config?.httpserver?.host || config?.server?.host || config?.host || LOCAL_HOSTS.LOCALHOST);
1175
1223
  }
1176
- const resolvedPort = port;
1177
1224
  // Stop current instance (if any)
1178
1225
  const pids = findListeningPids(resolvedPort);
1179
1226
  if (pids.length) {
@@ -1221,8 +1268,7 @@ program
1221
1268
  fs.writeFileSync(path.join(homedir(), '.routecodex', 'server.cli.pid'), String(child.pid ?? ''), 'utf8');
1222
1269
  }
1223
1270
  catch (error) { /* ignore */ }
1224
- const host = (config?.httpserver?.host || config?.server?.host || config?.host || LOCAL_HOSTS.LOCALHOST);
1225
- spinner.succeed(`RouteCodex server restarting on ${host}:${resolvedPort}`);
1271
+ spinner.succeed(`RouteCodex server restarting on ${resolvedHost}:${resolvedPort}`);
1226
1272
  logger.info(`Server will run on port: ${resolvedPort}`);
1227
1273
  logger.info('Press Ctrl+C to stop the server');
1228
1274
  const shutdown = async (sig) => {
@@ -1622,7 +1668,9 @@ async function ensurePortAvailable(port, parentSpinner, opts = {}) {
1622
1668
  }
1623
1669
  function findListeningPids(port) {
1624
1670
  try {
1625
- const result = spawnSync('lsof', ['-tiTCP', `:${port}`, '-sTCP:LISTEN'], { encoding: 'utf8' });
1671
+ // macOS/BSD lsof expects either "-i TCP:port" or "-tiTCP:port" as a single argument.
1672
+ // Use the compact form to avoid treating ":port" as a filename.
1673
+ const result = spawnSync('lsof', [`-tiTCP:${port}`, '-sTCP:LISTEN'], { encoding: 'utf8' });
1626
1674
  if (result.error) {
1627
1675
  logger.warning(`lsof not available to inspect port usage: ${result.error.message}`);
1628
1676
  return [];