@jsonstudio/rcc 0.89.1803 → 0.89.1959

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 (283) hide show
  1. package/configsamples/config.json +19 -0
  2. package/configsamples/provider/deepseek/config.v1.json +59 -0
  3. package/dist/build-info.js +2 -2
  4. package/dist/cli/commands/claude.d.ts +4 -0
  5. package/dist/cli/commands/claude.js +56 -0
  6. package/dist/cli/commands/claude.js.map +1 -0
  7. package/dist/cli/commands/clock-admin.d.ts +20 -0
  8. package/dist/cli/commands/clock-admin.js +234 -0
  9. package/dist/cli/commands/clock-admin.js.map +1 -0
  10. package/dist/cli/commands/code.d.ts +0 -42
  11. package/dist/cli/commands/code.js +4 -414
  12. package/dist/cli/commands/code.js.map +1 -1
  13. package/dist/cli/commands/codex.d.ts +4 -0
  14. package/dist/cli/commands/codex.js +43 -0
  15. package/dist/cli/commands/codex.js.map +1 -0
  16. package/dist/cli/commands/examples.js +13 -16
  17. package/dist/cli/commands/examples.js.map +1 -1
  18. package/dist/cli/commands/init/basic.d.ts +40 -0
  19. package/dist/cli/commands/init/basic.js +482 -0
  20. package/dist/cli/commands/init/basic.js.map +1 -0
  21. package/dist/cli/commands/init/camoufox.d.ts +7 -0
  22. package/dist/cli/commands/init/camoufox.js +59 -0
  23. package/dist/cli/commands/init/camoufox.js.map +1 -0
  24. package/dist/cli/commands/init/interactive.d.ts +18 -0
  25. package/dist/cli/commands/init/interactive.js +223 -0
  26. package/dist/cli/commands/init/interactive.js.map +1 -0
  27. package/dist/cli/commands/init/shared.d.ts +66 -0
  28. package/dist/cli/commands/init/shared.js +9 -0
  29. package/dist/cli/commands/init/shared.js.map +1 -0
  30. package/dist/cli/commands/init/workflows.d.ts +29 -0
  31. package/dist/cli/commands/init/workflows.js +341 -0
  32. package/dist/cli/commands/init/workflows.js.map +1 -0
  33. package/dist/cli/commands/init.d.ts +2 -26
  34. package/dist/cli/commands/init.js +220 -53
  35. package/dist/cli/commands/init.js.map +1 -1
  36. package/dist/cli/commands/launcher-kernel.d.ts +78 -0
  37. package/dist/cli/commands/launcher-kernel.js +1194 -0
  38. package/dist/cli/commands/launcher-kernel.js.map +1 -0
  39. package/dist/cli/commands/start.js +27 -1
  40. package/dist/cli/commands/start.js.map +1 -1
  41. package/dist/cli/commands/status.d.ts +2 -0
  42. package/dist/cli/commands/status.js +24 -1
  43. package/dist/cli/commands/status.js.map +1 -1
  44. package/dist/cli/commands/stop.d.ts +1 -0
  45. package/dist/cli/commands/stop.js +201 -4
  46. package/dist/cli/commands/stop.js.map +1 -1
  47. package/dist/cli/commands/tmux-inject.d.ts +20 -0
  48. package/dist/cli/commands/tmux-inject.js +212 -0
  49. package/dist/cli/commands/tmux-inject.js.map +1 -0
  50. package/dist/cli/config/init-provider-catalog.js +34 -0
  51. package/dist/cli/config/init-provider-catalog.js.map +1 -1
  52. package/dist/cli/register/claude-command.d.ts +3 -0
  53. package/dist/cli/register/claude-command.js +5 -0
  54. package/dist/cli/register/claude-command.js.map +1 -0
  55. package/dist/cli/register/clock-admin-command.d.ts +3 -0
  56. package/dist/cli/register/clock-admin-command.js +5 -0
  57. package/dist/cli/register/clock-admin-command.js.map +1 -0
  58. package/dist/cli/register/codex-command.d.ts +3 -0
  59. package/dist/cli/register/codex-command.js +5 -0
  60. package/dist/cli/register/codex-command.js.map +1 -0
  61. package/dist/cli/register/status-config-commands.d.ts +2 -0
  62. package/dist/cli/register/status-config-commands.js.map +1 -1
  63. package/dist/cli/register/tmux-inject-command.d.ts +3 -0
  64. package/dist/cli/register/tmux-inject-command.js +5 -0
  65. package/dist/cli/register/tmux-inject-command.js.map +1 -0
  66. package/dist/cli/server/port-utils.d.ts +3 -2
  67. package/dist/cli/server/port-utils.js +171 -32
  68. package/dist/cli/server/port-utils.js.map +1 -1
  69. package/dist/cli.js +45 -6
  70. package/dist/cli.js.map +1 -1
  71. package/dist/client/gemini/gemini-protocol-client.js +56 -5
  72. package/dist/client/gemini/gemini-protocol-client.js.map +1 -1
  73. package/dist/commands/token-daemon.js +59 -7
  74. package/dist/commands/token-daemon.js.map +1 -1
  75. package/dist/commands/validate.js +87 -15
  76. package/dist/commands/validate.js.map +1 -1
  77. package/dist/config/routecodex-config-loader.js +31 -2
  78. package/dist/config/routecodex-config-loader.js.map +1 -1
  79. package/dist/docs/daemon-admin-ui.html +948 -74
  80. package/dist/index.d.ts +1 -0
  81. package/dist/index.js +325 -37
  82. package/dist/index.js.map +1 -1
  83. package/dist/manager/quota/provider-quota-center.js +8 -14
  84. package/dist/manager/quota/provider-quota-center.js.map +1 -1
  85. package/dist/modules/llmswitch/bridge.d.ts +39 -0
  86. package/dist/modules/llmswitch/bridge.js +169 -0
  87. package/dist/modules/llmswitch/bridge.js.map +1 -1
  88. package/dist/modules/pipeline/utils/colored-logger.js +1 -1
  89. package/dist/modules/pipeline/utils/colored-logger.js.map +1 -1
  90. package/dist/providers/auth/deepseek-account-auth.d.ts +39 -0
  91. package/dist/providers/auth/deepseek-account-auth.js +329 -0
  92. package/dist/providers/auth/deepseek-account-auth.js.map +1 -0
  93. package/dist/providers/auth/deepseek-account-token-acquirer.d.ts +15 -0
  94. package/dist/providers/auth/deepseek-account-token-acquirer.js +644 -0
  95. package/dist/providers/auth/deepseek-account-token-acquirer.js.map +1 -0
  96. package/dist/providers/auth/oauth-lifecycle.js +26 -4
  97. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  98. package/dist/providers/auth/oauth-repair-cooldown.d.ts +5 -0
  99. package/dist/providers/auth/oauth-repair-cooldown.js +39 -0
  100. package/dist/providers/auth/oauth-repair-cooldown.js.map +1 -1
  101. package/dist/providers/auth/token-scanner/index.d.ts +6 -0
  102. package/dist/providers/auth/token-scanner/index.js +53 -0
  103. package/dist/providers/auth/token-scanner/index.js.map +1 -1
  104. package/dist/providers/core/api/provider-config.d.ts +17 -2
  105. package/dist/providers/core/api/provider-types.d.ts +6 -0
  106. package/dist/providers/core/api/provider-types.js.map +1 -1
  107. package/dist/providers/core/config/camoufox-launcher.d.ts +7 -0
  108. package/dist/providers/core/config/camoufox-launcher.js +68 -21
  109. package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
  110. package/dist/providers/core/config/service-profiles.js +19 -0
  111. package/dist/providers/core/config/service-profiles.js.map +1 -1
  112. package/dist/providers/core/contracts/deepseek-provider-contract.d.ts +34 -0
  113. package/dist/providers/core/contracts/deepseek-provider-contract.js +100 -0
  114. package/dist/providers/core/contracts/deepseek-provider-contract.js.map +1 -0
  115. package/dist/providers/core/runtime/anthropic-http-provider.d.ts +0 -5
  116. package/dist/providers/core/runtime/anthropic-http-provider.js +0 -26
  117. package/dist/providers/core/runtime/anthropic-http-provider.js.map +1 -1
  118. package/dist/providers/core/runtime/deepseek-http-provider.d.ts +35 -0
  119. package/dist/providers/core/runtime/deepseek-http-provider.js +373 -0
  120. package/dist/providers/core/runtime/deepseek-http-provider.js.map +1 -0
  121. package/dist/providers/core/runtime/deepseek-session-pow.d.ts +55 -0
  122. package/dist/providers/core/runtime/deepseek-session-pow.js +422 -0
  123. package/dist/providers/core/runtime/deepseek-session-pow.js.map +1 -0
  124. package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +0 -3
  125. package/dist/providers/core/runtime/gemini-cli-http-provider.js +0 -72
  126. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  127. package/dist/providers/core/runtime/gemini-http-provider.d.ts +1 -7
  128. package/dist/providers/core/runtime/gemini-http-provider.js +3 -110
  129. package/dist/providers/core/runtime/gemini-http-provider.js.map +1 -1
  130. package/dist/providers/core/runtime/http-request-executor.d.ts +1 -0
  131. package/dist/providers/core/runtime/http-request-executor.js +4 -0
  132. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  133. package/dist/providers/core/runtime/http-transport-provider.d.ts +10 -4
  134. package/dist/providers/core/runtime/http-transport-provider.js +308 -82
  135. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  136. package/dist/providers/core/runtime/iflow-http-provider.d.ts +0 -4
  137. package/dist/providers/core/runtime/iflow-http-provider.js +0 -28
  138. package/dist/providers/core/runtime/iflow-http-provider.js.map +1 -1
  139. package/dist/providers/core/runtime/provider-factory.d.ts +5 -0
  140. package/dist/providers/core/runtime/provider-factory.js +59 -6
  141. package/dist/providers/core/runtime/provider-factory.js.map +1 -1
  142. package/dist/providers/core/runtime/responses-provider.d.ts +0 -2
  143. package/dist/providers/core/runtime/responses-provider.js +0 -11
  144. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  145. package/dist/providers/core/strategies/oauth-device-flow.js +16 -1
  146. package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
  147. package/dist/providers/core/utils/provider-type-utils.js +2 -1
  148. package/dist/providers/core/utils/provider-type-utils.js.map +1 -1
  149. package/dist/providers/profile/families/anthropic-profile.d.ts +2 -0
  150. package/dist/providers/profile/families/anthropic-profile.js +32 -0
  151. package/dist/providers/profile/families/anthropic-profile.js.map +1 -0
  152. package/dist/providers/profile/families/antigravity-profile.d.ts +2 -0
  153. package/dist/providers/profile/families/antigravity-profile.js +109 -0
  154. package/dist/providers/profile/families/antigravity-profile.js.map +1 -0
  155. package/dist/providers/profile/families/glm-profile.d.ts +2 -0
  156. package/dist/providers/profile/families/glm-profile.js +48 -0
  157. package/dist/providers/profile/families/glm-profile.js.map +1 -0
  158. package/dist/providers/profile/families/iflow-profile.d.ts +2 -0
  159. package/dist/providers/profile/families/iflow-profile.js +232 -0
  160. package/dist/providers/profile/families/iflow-profile.js.map +1 -0
  161. package/dist/providers/profile/families/qwen-profile.d.ts +2 -0
  162. package/dist/providers/profile/families/qwen-profile.js +14 -0
  163. package/dist/providers/profile/families/qwen-profile.js.map +1 -0
  164. package/dist/providers/profile/families/responses-profile.d.ts +2 -0
  165. package/dist/providers/profile/families/responses-profile.js +28 -0
  166. package/dist/providers/profile/families/responses-profile.js.map +1 -0
  167. package/dist/providers/profile/profile-contracts.d.ts +74 -0
  168. package/dist/providers/profile/profile-contracts.js +2 -0
  169. package/dist/providers/profile/profile-contracts.js.map +1 -0
  170. package/dist/providers/profile/profile-registry.d.ts +3 -0
  171. package/dist/providers/profile/profile-registry.js +40 -0
  172. package/dist/providers/profile/profile-registry.js.map +1 -0
  173. package/dist/providers/profile/provider-directory.d.ts +2 -0
  174. package/dist/providers/profile/provider-directory.js +55 -0
  175. package/dist/providers/profile/provider-directory.js.map +1 -0
  176. package/dist/providers/profile/provider-profile-loader.js +43 -3
  177. package/dist/providers/profile/provider-profile-loader.js.map +1 -1
  178. package/dist/providers/profile/provider-profile.d.ts +8 -0
  179. package/dist/scripts/deepseek/pow-solver.mjs +146 -0
  180. package/dist/scripts/deepseek/sha3_wasm_bg.7b9ca65ddd.wasm +0 -0
  181. package/dist/server/handlers/config-admin-handler.js +27 -0
  182. package/dist/server/handlers/config-admin-handler.js.map +1 -1
  183. package/dist/server/runtime/http-server/clock-client-registry.d.ts +113 -0
  184. package/dist/server/runtime/http-server/clock-client-registry.js +592 -0
  185. package/dist/server/runtime/http-server/clock-client-registry.js.map +1 -0
  186. package/dist/server/runtime/http-server/clock-client-routes.d.ts +2 -0
  187. package/dist/server/runtime/http-server/clock-client-routes.js +481 -0
  188. package/dist/server/runtime/http-server/clock-client-routes.js.map +1 -0
  189. package/dist/server/runtime/http-server/clock-daemon-inject-config.d.ts +1 -0
  190. package/dist/server/runtime/http-server/clock-daemon-inject-config.js +11 -0
  191. package/dist/server/runtime/http-server/clock-daemon-inject-config.js.map +1 -0
  192. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js +3 -3
  193. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js.map +1 -1
  194. package/dist/server/runtime/http-server/daemon-admin/auth-session.d.ts +1 -0
  195. package/dist/server/runtime/http-server/daemon-admin/auth-session.js +18 -2
  196. package/dist/server/runtime/http-server/daemon-admin/auth-session.js.map +1 -1
  197. package/dist/server/runtime/http-server/daemon-admin/control-handler.js +2 -15
  198. package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -1
  199. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +65 -7
  200. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  201. package/dist/server/runtime/http-server/executor-metadata.js +37 -1
  202. package/dist/server/runtime/http-server/executor-metadata.js.map +1 -1
  203. package/dist/server/runtime/http-server/executor-provider.js +55 -0
  204. package/dist/server/runtime/http-server/executor-provider.js.map +1 -1
  205. package/dist/server/runtime/http-server/executor-response.js +49 -1
  206. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  207. package/dist/server/runtime/http-server/index.d.ts +10 -0
  208. package/dist/server/runtime/http-server/index.js +534 -9
  209. package/dist/server/runtime/http-server/index.js.map +1 -1
  210. package/dist/server/runtime/http-server/managed-process-probe.d.ts +6 -0
  211. package/dist/server/runtime/http-server/managed-process-probe.js +294 -0
  212. package/dist/server/runtime/http-server/managed-process-probe.js.map +1 -0
  213. package/dist/server/runtime/http-server/middleware.js +16 -1
  214. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  215. package/dist/server/runtime/http-server/provider-utils.js +6 -2
  216. package/dist/server/runtime/http-server/provider-utils.js.map +1 -1
  217. package/dist/server/runtime/http-server/request-executor.d.ts +1 -0
  218. package/dist/server/runtime/http-server/request-executor.js +360 -35
  219. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  220. package/dist/server/runtime/http-server/routes.js +95 -3
  221. package/dist/server/runtime/http-server/routes.js.map +1 -1
  222. package/dist/server/runtime/http-server/stats-manager.d.ts +10 -0
  223. package/dist/server/runtime/http-server/stats-manager.js +119 -16
  224. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  225. package/dist/server/runtime/http-server/tmux-session-probe.d.ts +3 -0
  226. package/dist/server/runtime/http-server/tmux-session-probe.js +101 -0
  227. package/dist/server/runtime/http-server/tmux-session-probe.js.map +1 -0
  228. package/dist/server/utils/stage-logger.js +21 -5
  229. package/dist/server/utils/stage-logger.js.map +1 -1
  230. package/dist/token-daemon/index.js +59 -10
  231. package/dist/token-daemon/index.js.map +1 -1
  232. package/dist/token-daemon/server-utils.d.ts +1 -0
  233. package/dist/token-daemon/server-utils.js +4 -1
  234. package/dist/token-daemon/server-utils.js.map +1 -1
  235. package/dist/token-daemon/token-daemon.js +38 -4
  236. package/dist/token-daemon/token-daemon.js.map +1 -1
  237. package/dist/token-daemon/token-types.d.ts +1 -1
  238. package/dist/token-daemon/token-types.js +2 -1
  239. package/dist/token-daemon/token-types.js.map +1 -1
  240. package/dist/token-daemon/token-utils.js +5 -2
  241. package/dist/token-daemon/token-utils.js.map +1 -1
  242. package/dist/utils/clock-client-token.d.ts +3 -0
  243. package/dist/utils/clock-client-token.js +54 -0
  244. package/dist/utils/clock-client-token.js.map +1 -0
  245. package/dist/utils/managed-server-pids.d.ts +25 -0
  246. package/dist/utils/managed-server-pids.js +176 -0
  247. package/dist/utils/managed-server-pids.js.map +1 -0
  248. package/dist/utils/process-lifecycle-logger.d.ts +8 -0
  249. package/dist/utils/process-lifecycle-logger.js +151 -0
  250. package/dist/utils/process-lifecycle-logger.js.map +1 -0
  251. package/dist/utils/runtime-exit-forensics.d.ts +30 -0
  252. package/dist/utils/runtime-exit-forensics.js +101 -0
  253. package/dist/utils/runtime-exit-forensics.js.map +1 -0
  254. package/dist/utils/shutdown-caller-context.d.ts +22 -0
  255. package/dist/utils/shutdown-caller-context.js +25 -0
  256. package/dist/utils/shutdown-caller-context.js.map +1 -0
  257. package/docs/PROVIDERS_BUILTIN.md +8 -0
  258. package/docs/PROVIDER_TYPES.md +3 -1
  259. package/docs/SERVERTOOL_PRE_COMMAND_HOOKS.md +85 -0
  260. package/docs/clock-client-daemon-design.md +343 -0
  261. package/docs/daemon-admin-ui.html +948 -74
  262. package/docs/providers/deepseek-web-provider-design.md +192 -0
  263. package/docs/routing-instructions.md +4 -1
  264. package/docs/stop-message-auto.md +4 -3
  265. package/docs/v2-architecture/PROVIDER-V2-CHANGESET-RELEASE-CHECKLIST.md +80 -0
  266. package/docs/v2-architecture/PROVIDER-V2-LAYERING-ADR-DRAFT.md +225 -0
  267. package/docs/v2-architecture/PROVIDER-V2-MIGRATION-MATRIX-DRAFT.md +88 -0
  268. package/docs/v2-architecture/PROVIDER-V2-PHASED-MIGRATION-ROLLBACK-DRAFT.md +164 -0
  269. package/docs/v2-architecture/PROVIDER-V2-PROFILE-API-REGISTRY-DRAFT.md +201 -0
  270. package/docs/v2-architecture/PROVIDER-V2-PROFILE-GEMINI-DRAFT.md +56 -0
  271. package/docs/v2-architecture/PROVIDER-V2-REFACTOR-OVERVIEW-DRAFT.md +102 -0
  272. package/docs/v2-architecture/PROVIDER-V2-VERIFICATION-MATRIX-DRAFT.md +163 -0
  273. package/package.json +10 -9
  274. package/scripts/copy-compat-assets.mjs +18 -0
  275. package/scripts/copy-modules-config.mjs +1 -0
  276. package/scripts/deepseek/pow-solver.mjs +146 -0
  277. package/scripts/deepseek/sha3_wasm_bg.7b9ca65ddd.wasm +0 -0
  278. package/scripts/ensure-cli-executable.mjs +64 -0
  279. package/scripts/install-global.sh +5 -2
  280. package/scripts/install.sh +1 -1
  281. package/scripts/monitor/daemon-kill-watch.mjs +184 -0
  282. package/scripts/monitor/port-kill-watch.sh +74 -0
  283. package/scripts/quick-install.sh +1 -1
@@ -0,0 +1,1194 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { createServer } from 'node:http';
5
+ import crypto from 'node:crypto';
6
+ import { LOCAL_HOSTS } from '../../constants/index.js';
7
+ import { encodeClockClientApiKey } from '../../utils/clock-client-token.js';
8
+ import { logProcessLifecycle } from '../../utils/process-lifecycle-logger.js';
9
+ function resolveBinary(options) {
10
+ const raw = String(options.command || '').trim();
11
+ if (!raw) {
12
+ return '';
13
+ }
14
+ if (raw.includes('/') || raw.includes('\\')) {
15
+ return raw;
16
+ }
17
+ const candidates = [];
18
+ try {
19
+ candidates.push(options.pathImpl.join('/opt/homebrew/bin', raw));
20
+ }
21
+ catch {
22
+ // ignore
23
+ }
24
+ try {
25
+ candidates.push(options.pathImpl.join('/usr/local/bin', raw));
26
+ }
27
+ catch {
28
+ // ignore
29
+ }
30
+ try {
31
+ candidates.push(options.pathImpl.join(options.homedir(), '.local', 'bin', raw));
32
+ }
33
+ catch {
34
+ // ignore
35
+ }
36
+ for (const candidate of candidates) {
37
+ try {
38
+ if (candidate && options.fsImpl.existsSync(candidate)) {
39
+ return candidate;
40
+ }
41
+ }
42
+ catch {
43
+ // ignore
44
+ }
45
+ }
46
+ return raw;
47
+ }
48
+ function parseServerUrl(raw) {
49
+ const trimmed = String(raw || '').trim();
50
+ if (!trimmed) {
51
+ throw new Error('--url is empty');
52
+ }
53
+ let parsed;
54
+ try {
55
+ parsed = new URL(trimmed);
56
+ }
57
+ catch {
58
+ parsed = new URL(`http://${trimmed}`);
59
+ }
60
+ const protocol = parsed.protocol === 'https:' ? 'https' : 'http';
61
+ const host = parsed.hostname;
62
+ const hasExplicitPort = Boolean(parsed.port && parsed.port.trim());
63
+ const port = hasExplicitPort ? Number(parsed.port) : null;
64
+ const rawPath = typeof parsed.pathname === 'string' ? parsed.pathname : '';
65
+ const basePath = rawPath && rawPath !== '/' ? rawPath.replace(/\/+$/, '') : '';
66
+ return { protocol, host, port: Number.isFinite(port) ? port : null, basePath };
67
+ }
68
+ function readConfigApiKey(fsImpl, configPath) {
69
+ try {
70
+ if (!configPath || !fsImpl.existsSync(configPath)) {
71
+ return null;
72
+ }
73
+ const txt = fsImpl.readFileSync(configPath, 'utf8');
74
+ const cfg = JSON.parse(txt);
75
+ const direct = cfg?.httpserver?.apikey ?? cfg?.modules?.httpserver?.config?.apikey ?? cfg?.server?.apikey;
76
+ const value = typeof direct === 'string' ? direct.trim() : '';
77
+ return value ? value : null;
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ function normalizeConnectHost(host) {
84
+ const value = String(host || '').toLowerCase();
85
+ if (value === '0.0.0.0') {
86
+ return '0.0.0.0';
87
+ }
88
+ if (value === '::' || value === '::1' || value === 'localhost') {
89
+ return '0.0.0.0';
90
+ }
91
+ return host || '0.0.0.0';
92
+ }
93
+ function toIntegerPort(value) {
94
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
95
+ return Math.floor(value);
96
+ }
97
+ if (typeof value === 'string' && value.trim()) {
98
+ const parsed = Number(value.trim());
99
+ if (Number.isFinite(parsed) && parsed > 0) {
100
+ return Math.floor(parsed);
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ function tryReadConfigHostPort(fsImpl, configPath) {
106
+ if (!configPath || !fsImpl.existsSync(configPath)) {
107
+ return { host: null, port: null };
108
+ }
109
+ try {
110
+ const configContent = fsImpl.readFileSync(configPath, 'utf8');
111
+ const config = JSON.parse(configContent);
112
+ const port = toIntegerPort(config?.httpserver?.port ?? config?.server?.port ?? config?.port);
113
+ const hostRaw = config?.httpserver?.host ?? config?.server?.host ?? config?.host;
114
+ const host = typeof hostRaw === 'string' && hostRaw.trim() ? hostRaw.trim() : null;
115
+ return { host, port };
116
+ }
117
+ catch {
118
+ return { host: null, port: null };
119
+ }
120
+ }
121
+ function resolveServerConnection(ctx, fsImpl, pathImpl, options) {
122
+ let configPath = typeof options.config === 'string' && options.config.trim() ? options.config.trim() : '';
123
+ if (!configPath) {
124
+ configPath = pathImpl.join(ctx.homedir(), '.routecodex', 'config.json');
125
+ }
126
+ let actualProtocol = 'http';
127
+ let actualPort = toIntegerPort(options.port);
128
+ let actualHost = typeof options.host === 'string' && options.host.trim() ? options.host.trim() : LOCAL_HOSTS.ANY;
129
+ let actualBasePath = '';
130
+ if (typeof options.url === 'string' && options.url.trim()) {
131
+ const parsed = parseServerUrl(options.url);
132
+ actualProtocol = parsed.protocol;
133
+ actualHost = parsed.host || actualHost;
134
+ actualPort = parsed.port ?? actualPort;
135
+ actualBasePath = parsed.basePath;
136
+ }
137
+ if (!(typeof options.url === 'string' && options.url.trim())) {
138
+ if (!actualPort) {
139
+ const configMaybe = tryReadConfigHostPort(fsImpl, configPath);
140
+ if (configMaybe.port) {
141
+ actualPort = configMaybe.port;
142
+ }
143
+ if (configMaybe.host) {
144
+ actualHost = configMaybe.host;
145
+ }
146
+ }
147
+ if (!actualPort) {
148
+ const envPort = toIntegerPort(ctx.env.ROUTECODEX_PORT || ctx.env.RCC_PORT);
149
+ if (envPort) {
150
+ actualPort = envPort;
151
+ }
152
+ }
153
+ if (!actualPort && ctx.isDevPackage) {
154
+ actualPort = ctx.defaultDevPort;
155
+ ctx.logger.info(`Using dev default port ${actualPort} for routecodex launcher mode`);
156
+ }
157
+ }
158
+ if (!(typeof options.url === 'string' && options.url.trim()) && !actualPort) {
159
+ throw new Error('Invalid or missing port configuration for RouteCodex server');
160
+ }
161
+ const configuredApiKey = (typeof options.apikey === 'string' && options.apikey.trim() ? options.apikey.trim() : null) ??
162
+ (typeof ctx.env.ROUTECODEX_APIKEY === 'string' && ctx.env.ROUTECODEX_APIKEY.trim()
163
+ ? ctx.env.ROUTECODEX_APIKEY.trim()
164
+ : null) ??
165
+ (typeof ctx.env.RCC_APIKEY === 'string' && ctx.env.RCC_APIKEY.trim() ? ctx.env.RCC_APIKEY.trim() : null) ??
166
+ readConfigApiKey(fsImpl, configPath);
167
+ const connectHost = normalizeConnectHost(actualHost);
168
+ const portPart = actualPort ? `:${actualPort}` : '';
169
+ const serverUrl = `${actualProtocol}://${connectHost}${portPart}${actualBasePath}`;
170
+ return {
171
+ configPath,
172
+ protocol: actualProtocol,
173
+ host: actualHost,
174
+ connectHost,
175
+ port: actualPort,
176
+ basePath: actualBasePath,
177
+ portPart,
178
+ serverUrl,
179
+ configuredApiKey
180
+ };
181
+ }
182
+ async function checkServerReady(ctx, serverUrl, apiKey, timeoutMs = 2500) {
183
+ try {
184
+ const headers = apiKey ? { 'x-api-key': apiKey } : undefined;
185
+ const probe = async (pathSuffix) => {
186
+ const controller = new AbortController();
187
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
188
+ try {
189
+ const response = await ctx.fetch(`${serverUrl}${pathSuffix}`, {
190
+ signal: controller.signal,
191
+ method: 'GET',
192
+ headers
193
+ }).catch(() => null);
194
+ if (!response || !response.ok) {
195
+ return { ok: false, body: null };
196
+ }
197
+ const body = await response.json().catch(() => null);
198
+ return { ok: true, body };
199
+ }
200
+ finally {
201
+ clearTimeout(timeoutId);
202
+ }
203
+ };
204
+ const readyProbe = await probe('/ready');
205
+ if (readyProbe.ok) {
206
+ const status = typeof readyProbe.body?.status === 'string' ? readyProbe.body.status : '';
207
+ if (status.toLowerCase() === 'ready' || readyProbe.body?.ready === true) {
208
+ return true;
209
+ }
210
+ }
211
+ const healthProbe = await probe('/health');
212
+ if (!healthProbe.ok) {
213
+ return false;
214
+ }
215
+ const status = typeof healthProbe.body?.status === 'string' ? healthProbe.body.status.toLowerCase() : '';
216
+ return status === 'ok' || status === 'ready' || healthProbe.body?.ready === true || healthProbe.body?.pipelineReady === true;
217
+ }
218
+ catch {
219
+ return false;
220
+ }
221
+ }
222
+ function rotateLogFile(fsImpl, filePath, maxBytes = 8 * 1024 * 1024, maxBackups = 3) {
223
+ try {
224
+ if (!fsImpl.existsSync(filePath)) {
225
+ return;
226
+ }
227
+ const stat = fsImpl.statSync(filePath);
228
+ if (!stat.isFile() || stat.size < maxBytes) {
229
+ return;
230
+ }
231
+ for (let index = maxBackups - 1; index >= 1; index--) {
232
+ const from = `${filePath}.${index}`;
233
+ const to = `${filePath}.${index + 1}`;
234
+ try {
235
+ if (fsImpl.existsSync(from)) {
236
+ if (fsImpl.existsSync(to)) {
237
+ fsImpl.unlinkSync(to);
238
+ }
239
+ fsImpl.renameSync(from, to);
240
+ }
241
+ }
242
+ catch {
243
+ // ignore
244
+ }
245
+ }
246
+ const firstBackup = `${filePath}.1`;
247
+ if (fsImpl.existsSync(firstBackup)) {
248
+ try {
249
+ fsImpl.unlinkSync(firstBackup);
250
+ }
251
+ catch {
252
+ // ignore
253
+ }
254
+ }
255
+ fsImpl.renameSync(filePath, firstBackup);
256
+ }
257
+ catch {
258
+ // ignore rotation failures
259
+ }
260
+ }
261
+ function ensureServerLogPath(ctx, fsImpl, pathImpl, port) {
262
+ const logsDir = pathImpl.join(ctx.homedir(), '.routecodex', 'logs');
263
+ fsImpl.mkdirSync(logsDir, { recursive: true });
264
+ const logPath = pathImpl.join(logsDir, `server-${port}.log`);
265
+ rotateLogFile(fsImpl, logPath);
266
+ return logPath;
267
+ }
268
+ async function ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, resolved) {
269
+ const alreadyReady = await checkServerReady(ctx, resolved.serverUrl, resolved.configuredApiKey);
270
+ if (alreadyReady) {
271
+ return { started: false };
272
+ }
273
+ const hasExplicitUrl = typeof options.url === 'string' && options.url.trim().length > 0;
274
+ if (hasExplicitUrl) {
275
+ throw new Error('RouteCodex server is not reachable with --url; auto-start is disabled for explicit URLs');
276
+ }
277
+ spinner.info('RouteCodex server is not running, starting it in background...');
278
+ const logPath = ensureServerLogPath(ctx, fsImpl, pathImpl, resolved.port);
279
+ const logFd = fsImpl.openSync(logPath, 'a');
280
+ const env = {
281
+ ...ctx.env,
282
+ ROUTECODEX_CONFIG: resolved.configPath,
283
+ ROUTECODEX_CONFIG_PATH: resolved.configPath,
284
+ ROUTECODEX_PORT: String(resolved.port),
285
+ RCC_PORT: String(resolved.port)
286
+ };
287
+ logProcessLifecycle({
288
+ event: 'detached_spawn',
289
+ source: 'cli.launcher.ensureServerReady',
290
+ details: {
291
+ role: 'routecodex-server',
292
+ result: 'attempt',
293
+ port: resolved.port,
294
+ command: ctx.nodeBin,
295
+ args: [ctx.resolveServerEntryPath(), ctx.getModulesConfigPath()],
296
+ logPath
297
+ }
298
+ });
299
+ try {
300
+ try {
301
+ const serverProcess = ctx.spawn(ctx.nodeBin, [ctx.resolveServerEntryPath(), ctx.getModulesConfigPath()], {
302
+ stdio: ['ignore', logFd, logFd],
303
+ env,
304
+ detached: true
305
+ });
306
+ logProcessLifecycle({
307
+ event: 'detached_spawn',
308
+ source: 'cli.launcher.ensureServerReady',
309
+ details: {
310
+ role: 'routecodex-server',
311
+ result: 'success',
312
+ port: resolved.port,
313
+ command: ctx.nodeBin,
314
+ args: [ctx.resolveServerEntryPath(), ctx.getModulesConfigPath()],
315
+ childPid: serverProcess.pid ?? null,
316
+ logPath
317
+ }
318
+ });
319
+ const onChildError = (error) => {
320
+ logProcessLifecycle({
321
+ event: 'detached_spawn',
322
+ source: 'cli.launcher.ensureServerReady',
323
+ details: {
324
+ role: 'routecodex-server',
325
+ result: 'failed',
326
+ port: resolved.port,
327
+ command: ctx.nodeBin,
328
+ args: [ctx.resolveServerEntryPath(), ctx.getModulesConfigPath()],
329
+ childPid: serverProcess.pid ?? null,
330
+ logPath,
331
+ error
332
+ }
333
+ });
334
+ };
335
+ if (typeof serverProcess.once === 'function') {
336
+ serverProcess.once('error', onChildError);
337
+ }
338
+ else if (typeof serverProcess.on === 'function') {
339
+ serverProcess.on('error', onChildError);
340
+ }
341
+ try {
342
+ serverProcess.unref?.();
343
+ }
344
+ catch {
345
+ // ignore
346
+ }
347
+ }
348
+ catch (error) {
349
+ logProcessLifecycle({
350
+ event: 'detached_spawn',
351
+ source: 'cli.launcher.ensureServerReady',
352
+ details: {
353
+ role: 'routecodex-server',
354
+ result: 'failed',
355
+ port: resolved.port,
356
+ command: ctx.nodeBin,
357
+ args: [ctx.resolveServerEntryPath(), ctx.getModulesConfigPath()],
358
+ logPath,
359
+ error
360
+ }
361
+ });
362
+ throw error;
363
+ }
364
+ }
365
+ finally {
366
+ try {
367
+ fsImpl.closeSync(logFd);
368
+ }
369
+ catch {
370
+ // ignore
371
+ }
372
+ }
373
+ spinner.text = 'Waiting for RouteCodex server to become ready...';
374
+ for (let attempt = 0; attempt < 45; attempt++) {
375
+ await ctx.sleep(1000);
376
+ const ready = await checkServerReady(ctx, resolved.serverUrl, resolved.configuredApiKey, 1500);
377
+ if (ready) {
378
+ return { started: true, logPath };
379
+ }
380
+ }
381
+ logProcessLifecycle({
382
+ event: 'detached_spawn',
383
+ source: 'cli.launcher.ensureServerReady',
384
+ details: {
385
+ role: 'routecodex-server',
386
+ result: 'not_ready_timeout',
387
+ port: resolved.port,
388
+ logPath
389
+ }
390
+ });
391
+ throw new Error(`RouteCodex server did not become ready in time. Check logs: ${logPath}`);
392
+ }
393
+ function resolveWorkingDirectory(ctx, fsImpl, pathImpl, requested) {
394
+ const getCwd = ctx.cwd ?? (() => process.cwd());
395
+ try {
396
+ const candidate = requested ? String(requested) : getCwd();
397
+ const resolved = pathImpl.resolve(candidate);
398
+ if (fsImpl.existsSync(resolved)) {
399
+ return resolved;
400
+ }
401
+ }
402
+ catch {
403
+ return getCwd();
404
+ }
405
+ return getCwd();
406
+ }
407
+ function isTmuxAvailable(spawnSyncImpl = spawnSync) {
408
+ try {
409
+ const result = spawnSyncImpl('tmux', ['-V'], { encoding: 'utf8' });
410
+ return result.status === 0;
411
+ }
412
+ catch {
413
+ return false;
414
+ }
415
+ }
416
+ function resolveCurrentTmuxTarget(env, spawnSyncImpl = spawnSync) {
417
+ const tmuxEnv = typeof env.TMUX === 'string' ? env.TMUX.trim() : '';
418
+ if (!tmuxEnv) {
419
+ return null;
420
+ }
421
+ try {
422
+ const result = spawnSyncImpl('tmux', ['display-message', '-p', '#S:#I.#P'], { encoding: 'utf8' });
423
+ if (result.status !== 0) {
424
+ return null;
425
+ }
426
+ const target = String(result.stdout || '').trim();
427
+ return target || null;
428
+ }
429
+ catch {
430
+ return null;
431
+ }
432
+ }
433
+ function isReusableTmuxPaneTarget(spawnSyncImpl, tmuxTarget, cwd) {
434
+ const normalizedTarget = String(tmuxTarget || '').trim();
435
+ if (!normalizedTarget) {
436
+ return false;
437
+ }
438
+ const expectedCwd = normalizePathForComparison(cwd);
439
+ if (!expectedCwd) {
440
+ return false;
441
+ }
442
+ try {
443
+ const paneResult = spawnSyncImpl('tmux', ['list-panes', '-t', normalizedTarget, '-F', '#{pane_current_command}\t#{pane_current_path}'], { encoding: 'utf8' });
444
+ if (paneResult.status !== 0) {
445
+ return false;
446
+ }
447
+ const firstLine = String(paneResult.stdout || '')
448
+ .split(/\r?\n/)
449
+ .map((line) => line.trim())
450
+ .find(Boolean);
451
+ if (!firstLine) {
452
+ return false;
453
+ }
454
+ const [command, panePath] = firstLine.split('\t');
455
+ const normalizedPanePath = normalizePathForComparison(String(panePath || '').trim());
456
+ return isReusableIdlePaneCommand(String(command || '').trim()) && normalizedPanePath === expectedCwd;
457
+ }
458
+ catch {
459
+ return false;
460
+ }
461
+ }
462
+ function shellQuote(value) {
463
+ return `'${String(value ?? '').replace(/'/g, `'"'"'`)}'`;
464
+ }
465
+ function buildShellCommand(tokens) {
466
+ return tokens.map((token) => shellQuote(token)).join(' ');
467
+ }
468
+ function collectChangedEnv(baseEnv, nextEnv) {
469
+ const out = [];
470
+ for (const [key, value] of Object.entries(nextEnv)) {
471
+ if (typeof value !== 'string') {
472
+ continue;
473
+ }
474
+ if (baseEnv[key] !== value) {
475
+ out.push([key, value]);
476
+ }
477
+ }
478
+ return out;
479
+ }
480
+ function sendTmuxSubmitKey(spawnSyncImpl, tmuxTarget, clientType) {
481
+ const type = String(clientType || '').trim().toLowerCase();
482
+ const submitKeys = type === 'codex' || type === 'claude'
483
+ ? ['Enter', 'C-m', 'KPEnter']
484
+ : ['Enter', 'C-m'];
485
+ let lastError = '';
486
+ for (const submitKey of submitKeys) {
487
+ try {
488
+ const result = spawnSyncImpl('tmux', ['send-keys', '-t', tmuxTarget, submitKey], { encoding: 'utf8' });
489
+ if (result.status === 0) {
490
+ return { ok: true };
491
+ }
492
+ lastError =
493
+ String(result.stderr || result.stdout || `tmux send-keys ${submitKey} failed`).trim()
494
+ || `tmux send-keys ${submitKey} failed`;
495
+ }
496
+ catch (error) {
497
+ lastError = error instanceof Error ? error.message : String(error ?? `tmux send-keys ${submitKey} failed`);
498
+ }
499
+ }
500
+ return { ok: false, error: lastError || 'tmux send-keys submit failed' };
501
+ }
502
+ function isReusableIdlePaneCommand(command) {
503
+ const normalized = String(command || '').trim().toLowerCase();
504
+ if (!normalized) {
505
+ return true;
506
+ }
507
+ return normalized === 'zsh'
508
+ || normalized === 'bash'
509
+ || normalized === 'sh'
510
+ || normalized === 'fish'
511
+ || normalized === 'nu';
512
+ }
513
+ function normalizeSessionToken(value) {
514
+ return String(value || '').replace(/[^a-zA-Z0-9_-]+/g, '_') || 'launcher';
515
+ }
516
+ function normalizePathForComparison(candidate) {
517
+ const raw = String(candidate || '').trim();
518
+ if (!raw) {
519
+ return '';
520
+ }
521
+ try {
522
+ const resolved = path.resolve(raw).replace(/[\\/]+$/, '');
523
+ if (process.platform === 'win32') {
524
+ return resolved.toLowerCase();
525
+ }
526
+ return resolved;
527
+ }
528
+ catch {
529
+ return raw;
530
+ }
531
+ }
532
+ function findReusableManagedTmuxSession(spawnSyncImpl, cwd, commandName) {
533
+ const expectedCwd = normalizePathForComparison(cwd);
534
+ const expectedSessionPrefix = `rcc_${normalizeSessionToken(commandName)}_`;
535
+ try {
536
+ const listResult = spawnSyncImpl('tmux', ['list-sessions', '-F', '#S #{session_attached}'], { encoding: 'utf8' });
537
+ if (listResult.status !== 0) {
538
+ return null;
539
+ }
540
+ const lines = String(listResult.stdout || '')
541
+ .split(/\r?\n/)
542
+ .map((line) => line.trim())
543
+ .filter(Boolean);
544
+ for (const line of lines) {
545
+ const [sessionName, attachedFlag] = line.split(' ');
546
+ const normalizedName = String(sessionName || '').trim();
547
+ if (!normalizedName.startsWith(expectedSessionPrefix)) {
548
+ continue;
549
+ }
550
+ if (String(attachedFlag || '').trim() === '1') {
551
+ continue;
552
+ }
553
+ const panesResult = spawnSyncImpl('tmux', [
554
+ 'list-panes',
555
+ '-t',
556
+ normalizedName,
557
+ '-F',
558
+ '#{session_name}:#{window_index}.#{pane_index} #{pane_current_command} #{pane_current_path}'
559
+ ], { encoding: 'utf8' });
560
+ if (panesResult.status !== 0) {
561
+ continue;
562
+ }
563
+ const panes = String(panesResult.stdout || '')
564
+ .split(/\r?\n/)
565
+ .map((paneLine) => paneLine.trim())
566
+ .filter(Boolean);
567
+ for (const pane of panes) {
568
+ const [target, command, panePath] = pane.split(' ');
569
+ const tmuxTarget = String(target || '').trim();
570
+ const currentCommand = String(command || '').trim().toLowerCase();
571
+ const normalizedPanePath = normalizePathForComparison(String(panePath || '').trim());
572
+ if (!tmuxTarget) {
573
+ continue;
574
+ }
575
+ if (!isReusableIdlePaneCommand(currentCommand)) {
576
+ continue;
577
+ }
578
+ if (!normalizedPanePath || normalizedPanePath !== expectedCwd) {
579
+ continue;
580
+ }
581
+ return { sessionName: normalizedName, tmuxTarget };
582
+ }
583
+ }
584
+ return null;
585
+ }
586
+ catch {
587
+ return null;
588
+ }
589
+ }
590
+ function createManagedTmuxSession(args) {
591
+ const { spawnSyncImpl, cwd, commandName } = args;
592
+ const reusable = findReusableManagedTmuxSession(spawnSyncImpl, cwd, commandName);
593
+ if (reusable) {
594
+ return {
595
+ sessionName: reusable.sessionName,
596
+ tmuxTarget: reusable.tmuxTarget,
597
+ reused: true,
598
+ stop: () => {
599
+ try {
600
+ spawnSyncImpl('tmux', ['kill-session', '-t', reusable.sessionName], { encoding: 'utf8' });
601
+ }
602
+ catch {
603
+ // ignore
604
+ }
605
+ }
606
+ };
607
+ }
608
+ const sessionName = (() => {
609
+ const token = normalizeSessionToken(commandName);
610
+ return `rcc_${token}_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
611
+ })();
612
+ try {
613
+ const result = spawnSyncImpl('tmux', ['new-session', '-d', '-s', sessionName, '-c', cwd], { encoding: 'utf8' });
614
+ if (result.status !== 0) {
615
+ return null;
616
+ }
617
+ }
618
+ catch {
619
+ return null;
620
+ }
621
+ const tmuxTarget = `${sessionName}:0.0`;
622
+ return {
623
+ sessionName,
624
+ tmuxTarget,
625
+ reused: false,
626
+ stop: () => {
627
+ try {
628
+ spawnSyncImpl('tmux', ['kill-session', '-t', sessionName], { encoding: 'utf8' });
629
+ }
630
+ catch {
631
+ // ignore
632
+ }
633
+ }
634
+ };
635
+ }
636
+ function launchCommandInTmuxPane(args) {
637
+ const { spawnSyncImpl, tmuxTarget, cwd, command, commandArgs, envOverrides, managedSessionName } = args;
638
+ const envTokens = envOverrides.map(([key, value]) => `${key}=${value}`);
639
+ const baseCommand = `cd -- ${shellQuote(cwd)} && ${buildShellCommand(['env', ...envTokens, command, ...commandArgs])}`;
640
+ const shellCommand = managedSessionName
641
+ ? `${baseCommand}; __rcc_exit=$?; tmux kill-session -t ${shellQuote(managedSessionName)} >/dev/null 2>&1; exit $__rcc_exit`
642
+ : baseCommand;
643
+ try {
644
+ const literal = spawnSyncImpl('tmux', ['send-keys', '-t', tmuxTarget, '-l', '--', shellCommand], { encoding: 'utf8' });
645
+ if (literal.status !== 0) {
646
+ return false;
647
+ }
648
+ const submit = sendTmuxSubmitKey(spawnSyncImpl, tmuxTarget);
649
+ return submit.ok;
650
+ }
651
+ catch {
652
+ return false;
653
+ }
654
+ }
655
+ function readJsonBody(req) {
656
+ return new Promise((resolve) => {
657
+ const chunks = [];
658
+ req.on('data', (chunk) => {
659
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
660
+ });
661
+ req.on('end', () => {
662
+ if (!chunks.length) {
663
+ resolve({});
664
+ return;
665
+ }
666
+ try {
667
+ const payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
668
+ resolve(payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {});
669
+ }
670
+ catch {
671
+ resolve({});
672
+ }
673
+ });
674
+ req.on('error', () => resolve({}));
675
+ });
676
+ }
677
+ function sendJson(res, status, payload) {
678
+ const body = JSON.stringify(payload);
679
+ res.statusCode = status;
680
+ res.setHeader('content-type', 'application/json');
681
+ res.end(body);
682
+ }
683
+ async function startClockClientService(args) {
684
+ const { ctx, resolved, tmuxTarget, spawnSyncImpl, clientType, managedTmuxSession, getManagedProcessState } = args;
685
+ const daemonId = (() => {
686
+ try {
687
+ return `clockd_${crypto.randomUUID()}`;
688
+ }
689
+ catch {
690
+ return `clockd_${Date.now()}_${Math.random().toString(16).slice(2)}`;
691
+ }
692
+ })();
693
+ const normalizedTmuxTarget = String(tmuxTarget || '').trim();
694
+ const tmuxSessionId = (() => {
695
+ if (!normalizedTmuxTarget) {
696
+ return daemonId;
697
+ }
698
+ const idx = normalizedTmuxTarget.indexOf(':');
699
+ const candidate = (idx >= 0 ? normalizedTmuxTarget.slice(0, idx) : normalizedTmuxTarget).trim();
700
+ return candidate || daemonId;
701
+ })();
702
+ let server = null;
703
+ let callbackUrl = 'http://127.0.0.1:0/inject';
704
+ if (normalizedTmuxTarget) {
705
+ server = createServer(async (req, res) => {
706
+ if (req.method !== 'POST' || req.url !== '/inject') {
707
+ sendJson(res, 404, { ok: false, message: 'not_found' });
708
+ return;
709
+ }
710
+ const body = await readJsonBody(req);
711
+ const text = typeof body.text === 'string' ? body.text.trim() : '';
712
+ if (!text) {
713
+ sendJson(res, 400, { ok: false, message: 'text is required' });
714
+ return;
715
+ }
716
+ try {
717
+ const literal = spawnSyncImpl('tmux', ['send-keys', '-t', normalizedTmuxTarget, '-l', '--', text], { encoding: 'utf8' });
718
+ if (literal.status !== 0) {
719
+ sendJson(res, 500, {
720
+ ok: false,
721
+ message: String(literal.stderr || literal.stdout || 'tmux send-keys failed').trim() || 'tmux send-keys failed'
722
+ });
723
+ return;
724
+ }
725
+ await ctx.sleep(80);
726
+ const submit = sendTmuxSubmitKey(spawnSyncImpl, normalizedTmuxTarget, clientType);
727
+ if (!submit.ok) {
728
+ sendJson(res, 500, {
729
+ ok: false,
730
+ message: submit.error
731
+ });
732
+ return;
733
+ }
734
+ sendJson(res, 200, { ok: true, tmuxTarget: normalizedTmuxTarget });
735
+ }
736
+ catch (error) {
737
+ sendJson(res, 500, { ok: false, message: error instanceof Error ? error.message : String(error ?? 'unknown') });
738
+ }
739
+ });
740
+ const port = await new Promise((resolve, reject) => {
741
+ server?.once('error', reject);
742
+ server?.listen(0, '127.0.0.1', () => {
743
+ const address = server?.address();
744
+ if (!address || typeof address === 'string') {
745
+ reject(new Error('failed to resolve clock daemon callback address'));
746
+ return;
747
+ }
748
+ resolve(address.port);
749
+ });
750
+ }).catch(() => 0);
751
+ if (!port) {
752
+ try {
753
+ server.close();
754
+ }
755
+ catch {
756
+ // ignore
757
+ }
758
+ return null;
759
+ }
760
+ callbackUrl = `http://127.0.0.1:${port}/inject`;
761
+ }
762
+ const controlUrl = `${resolved.protocol}://127.0.0.1:${resolved.port}${resolved.basePath}`;
763
+ const normalizeManagedProcessPayload = () => {
764
+ const state = typeof getManagedProcessState === 'function' ? getManagedProcessState() : undefined;
765
+ const managedClientProcess = state?.managedClientProcess === true;
766
+ const managedClientPid = typeof state?.managedClientPid === 'number' && Number.isFinite(state.managedClientPid) && state.managedClientPid > 0
767
+ ? Math.floor(state.managedClientPid)
768
+ : undefined;
769
+ const managedClientCommandHint = typeof state?.managedClientCommandHint === 'string' && state.managedClientCommandHint.trim()
770
+ ? state.managedClientCommandHint.trim()
771
+ : undefined;
772
+ return {
773
+ ...(managedClientProcess ? { managedClientProcess: true } : {}),
774
+ ...(managedClientPid ? { managedClientPid } : {}),
775
+ ...(managedClientCommandHint ? { managedClientCommandHint } : {})
776
+ };
777
+ };
778
+ const post = async (pathSuffix, payload) => {
779
+ try {
780
+ const response = await ctx.fetch(`${controlUrl}${pathSuffix}`, {
781
+ method: 'POST',
782
+ headers: { 'content-type': 'application/json' },
783
+ body: JSON.stringify(payload)
784
+ });
785
+ return response.ok;
786
+ }
787
+ catch {
788
+ return false;
789
+ }
790
+ };
791
+ const syncHeartbeat = async () => {
792
+ return await post('/daemon/clock-client/heartbeat', {
793
+ daemonId,
794
+ tmuxSessionId,
795
+ sessionId: tmuxSessionId,
796
+ managedTmuxSession,
797
+ ...normalizeManagedProcessPayload()
798
+ });
799
+ };
800
+ const registered = await post('/daemon/clock-client/register', {
801
+ daemonId,
802
+ tmuxSessionId,
803
+ sessionId: tmuxSessionId,
804
+ clientType,
805
+ ...(normalizedTmuxTarget ? { tmuxTarget: normalizedTmuxTarget } : {}),
806
+ managedTmuxSession,
807
+ callbackUrl,
808
+ ...normalizeManagedProcessPayload()
809
+ });
810
+ if (!registered) {
811
+ if (server) {
812
+ try {
813
+ server.close();
814
+ }
815
+ catch {
816
+ // ignore
817
+ }
818
+ }
819
+ return null;
820
+ }
821
+ const heartbeat = setInterval(() => {
822
+ void syncHeartbeat();
823
+ }, 10_000);
824
+ heartbeat.unref?.();
825
+ return {
826
+ daemonId,
827
+ tmuxSessionId,
828
+ ...(normalizedTmuxTarget ? { tmuxTarget: normalizedTmuxTarget } : {}),
829
+ syncHeartbeat,
830
+ stop: async () => {
831
+ clearInterval(heartbeat);
832
+ await post('/daemon/clock-client/unregister', { daemonId });
833
+ if (!server) {
834
+ return;
835
+ }
836
+ await new Promise((resolve) => {
837
+ try {
838
+ server?.close(() => resolve());
839
+ }
840
+ catch {
841
+ resolve();
842
+ }
843
+ });
844
+ }
845
+ };
846
+ }
847
+ function collectPassThroughArgs(args) {
848
+ const { rawArgv, commandName, knownOptions, requiredValueOptions, extraArgsFromCommander } = args;
849
+ const indexCommand = rawArgv.findIndex((token) => token === commandName);
850
+ const afterCommand = indexCommand >= 0 ? rawArgv.slice(indexCommand + 1) : [];
851
+ const separatorIndex = afterCommand.indexOf('--');
852
+ const tail = separatorIndex >= 0 ? afterCommand.slice(separatorIndex + 1) : afterCommand;
853
+ const passThrough = [];
854
+ for (let index = 0; index < tail.length; index++) {
855
+ const token = tail[index];
856
+ if (knownOptions.has(token)) {
857
+ if (requiredValueOptions.has(token)) {
858
+ index += 1;
859
+ }
860
+ continue;
861
+ }
862
+ if (token.startsWith('--')) {
863
+ const equalIndex = token.indexOf('=');
864
+ if (equalIndex > 2) {
865
+ const optionName = token.slice(0, equalIndex);
866
+ if (knownOptions.has(optionName)) {
867
+ continue;
868
+ }
869
+ }
870
+ }
871
+ passThrough.push(token);
872
+ }
873
+ const merged = [];
874
+ const seen = new Set();
875
+ const appendUnique = (values) => {
876
+ for (const value of values) {
877
+ if (!seen.has(value)) {
878
+ seen.add(value);
879
+ merged.push(value);
880
+ }
881
+ }
882
+ };
883
+ appendUnique(extraArgsFromCommander);
884
+ appendUnique(passThrough);
885
+ return merged;
886
+ }
887
+ function normalizeOpenAiBaseUrl(baseUrl) {
888
+ const trimmed = baseUrl.replace(/\/+$/, '');
889
+ if (trimmed.endsWith('/v1')) {
890
+ return trimmed;
891
+ }
892
+ return `${trimmed}/v1`;
893
+ }
894
+ export function createLauncherCommand(program, ctx, spec) {
895
+ const fsImpl = ctx.fsImpl ?? fs;
896
+ const pathImpl = ctx.pathImpl ?? path;
897
+ const command = program
898
+ .command(spec.commandName)
899
+ .description(spec.description)
900
+ .option('--port <port>', 'RouteCodex server port (overrides config file)')
901
+ .option('-h, --host <host>', 'RouteCodex server host', LOCAL_HOSTS.ANY)
902
+ .option('--url <url>', 'RouteCodex base URL (overrides host/port), e.g. https://proxy.example.com')
903
+ .option('-c, --config <config>', 'RouteCodex configuration file path')
904
+ .option('--apikey <apikey>', 'RouteCodex server apikey (defaults to httpserver.apikey in config when present)')
905
+ .option('--cwd <dir>', `Working directory for ${spec.displayName} (defaults to current shell cwd)`)
906
+ .option('--ensure-server', 'Ensure RouteCodex server is running before launching')
907
+ .option(spec.binaryOptionFlags, spec.binaryOptionDescription, spec.binaryDefault)
908
+ .argument('[extraArgs...]', `Additional args to pass through to ${spec.displayName}`)
909
+ .allowUnknownOption(true)
910
+ .allowExcessArguments(true);
911
+ if (spec.withModelOption) {
912
+ command.option('--model <model>', `Model to use with ${spec.displayName}`);
913
+ }
914
+ if (spec.withProfileOption) {
915
+ command.option('--profile <profile>', `${spec.displayName} profile to use`);
916
+ }
917
+ command.action(async (extraArgs = [], options) => {
918
+ const spinner = await ctx.createSpinner(`Preparing ${spec.displayName} with RouteCodex...`);
919
+ try {
920
+ const resolved = resolveServerConnection(ctx, fsImpl, pathImpl, options);
921
+ const ensureResult = await ensureServerReady(ctx, fsImpl, pathImpl, spinner, options, resolved);
922
+ spinner.text = `Launching ${spec.displayName}...`;
923
+ const baseUrl = `${resolved.protocol}://${resolved.connectHost}${resolved.portPart}${resolved.basePath}`;
924
+ const currentCwd = resolveWorkingDirectory(ctx, fsImpl, pathImpl, options.cwd);
925
+ const spawnSyncImpl = ctx.spawnSyncImpl ?? spawnSync;
926
+ const toolArgs = spec.buildArgs(options);
927
+ const knownOptions = new Set([
928
+ '--port',
929
+ '-h',
930
+ '--host',
931
+ '--url',
932
+ '-c',
933
+ '--config',
934
+ '--apikey',
935
+ '--cwd',
936
+ '--ensure-server',
937
+ ...spec.extraKnownOptions
938
+ ]);
939
+ const requiredValueOptions = new Set([
940
+ '--port',
941
+ '-h',
942
+ '--host',
943
+ '--url',
944
+ '-c',
945
+ '--config',
946
+ '--apikey',
947
+ '--cwd',
948
+ ...spec.extraKnownOptions
949
+ ]);
950
+ const passThroughArgs = collectPassThroughArgs({
951
+ rawArgv: Array.isArray(ctx.rawArgv) ? ctx.rawArgv : [],
952
+ commandName: spec.commandName,
953
+ knownOptions,
954
+ requiredValueOptions,
955
+ extraArgsFromCommander: Array.isArray(extraArgs) ? extraArgs : []
956
+ });
957
+ if (passThroughArgs.length) {
958
+ toolArgs.push(...passThroughArgs);
959
+ }
960
+ const binaryCandidate = (() => {
961
+ const fromOption = String(options[spec.binaryOptionName] ?? '').trim();
962
+ if (fromOption) {
963
+ return fromOption;
964
+ }
965
+ if (spec.binaryEnvKey) {
966
+ const fromEnv = String(ctx.env[spec.binaryEnvKey] || '').trim();
967
+ if (fromEnv) {
968
+ return fromEnv;
969
+ }
970
+ }
971
+ return spec.binaryDefault;
972
+ })();
973
+ const resolvedBinary = resolveBinary({
974
+ fsImpl,
975
+ pathImpl,
976
+ homedir: ctx.homedir,
977
+ command: binaryCandidate
978
+ });
979
+ let managedTmuxSession = null;
980
+ const tmuxEnabled = isTmuxAvailable(spawnSyncImpl);
981
+ if (!tmuxEnabled) {
982
+ ctx.logger.warning('[clock-advanced] tmux not found; advanced clock client service disabled (launcher will continue).');
983
+ }
984
+ let tmuxTarget = tmuxEnabled ? resolveCurrentTmuxTarget(ctx.env, spawnSyncImpl) : null;
985
+ if (tmuxTarget && !isReusableTmuxPaneTarget(spawnSyncImpl, tmuxTarget, currentCwd)) {
986
+ tmuxTarget = null;
987
+ }
988
+ if (tmuxEnabled && !tmuxTarget) {
989
+ managedTmuxSession = createManagedTmuxSession({
990
+ spawnSyncImpl,
991
+ cwd: currentCwd,
992
+ commandName: spec.commandName
993
+ });
994
+ if (managedTmuxSession) {
995
+ tmuxTarget = managedTmuxSession.tmuxTarget;
996
+ if (managedTmuxSession.reused) {
997
+ ctx.logger.info('[clock-advanced] reused existing managed tmux session and rebound launcher automatically.');
998
+ }
999
+ else {
1000
+ ctx.logger.info('[clock-advanced] started managed tmux session automatically; no manual tmux setup needed.');
1001
+ }
1002
+ }
1003
+ else {
1004
+ ctx.logger.warning('[clock-advanced] failed to start managed tmux session; launcher continues without advanced mode.');
1005
+ }
1006
+ }
1007
+ const managedClientProcessEnabled = !managedTmuxSession;
1008
+ let managedClientPid = null;
1009
+ const managedClientCommandHint = managedClientProcessEnabled ? resolvedBinary : undefined;
1010
+ const reclaimRequiredRaw = String(ctx.env.ROUTECODEX_CLOCK_RECLAIM_REQUIRED
1011
+ ?? ctx.env.RCC_CLOCK_RECLAIM_REQUIRED
1012
+ ?? '1')
1013
+ .trim()
1014
+ .toLowerCase();
1015
+ const reclaimRequired = reclaimRequiredRaw !== '0' && reclaimRequiredRaw !== 'false' && reclaimRequiredRaw !== 'no';
1016
+ const clockClientService = await startClockClientService({
1017
+ ctx,
1018
+ resolved,
1019
+ tmuxTarget,
1020
+ spawnSyncImpl,
1021
+ clientType: spec.commandName,
1022
+ managedTmuxSession: Boolean(managedTmuxSession),
1023
+ getManagedProcessState: () => ({
1024
+ managedClientProcess: managedClientProcessEnabled,
1025
+ managedClientPid,
1026
+ managedClientCommandHint
1027
+ })
1028
+ });
1029
+ if (managedClientProcessEnabled && reclaimRequired && !clockClientService) {
1030
+ throw new Error('clock client registration failed for managed child process; aborting launch to avoid orphan process');
1031
+ }
1032
+ if (tmuxTarget && !clockClientService) {
1033
+ ctx.logger.warning('[clock-advanced] failed to start clock client daemon service; launcher continues without advanced mode.');
1034
+ }
1035
+ const clockAdvancedEnabled = Boolean(clockClientService && tmuxTarget);
1036
+ const clockClientApiKey = clockAdvancedEnabled && clockClientService
1037
+ ? encodeClockClientApiKey(resolved.configuredApiKey || 'rcc-proxy-key', clockClientService.daemonId)
1038
+ : (resolved.configuredApiKey || 'rcc-proxy-key');
1039
+ const toolEnv = spec.buildEnv({
1040
+ env: {
1041
+ ...ctx.env,
1042
+ PWD: currentCwd,
1043
+ RCC_WORKDIR: currentCwd,
1044
+ ROUTECODEX_WORKDIR: currentCwd,
1045
+ OPENAI_BASE_URL: normalizeOpenAiBaseUrl(baseUrl),
1046
+ OPENAI_API_BASE: normalizeOpenAiBaseUrl(baseUrl),
1047
+ OPENAI_API_BASE_URL: normalizeOpenAiBaseUrl(baseUrl),
1048
+ OPENAI_API_KEY: clockClientApiKey,
1049
+ RCC_CLOCK_ADVANCED_ENABLED: clockAdvancedEnabled ? '1' : '0',
1050
+ ...(clockAdvancedEnabled && clockClientService
1051
+ ? {
1052
+ RCC_CLOCK_CLIENT_SESSION_ID: clockClientService.tmuxSessionId,
1053
+ RCC_CLOCK_CLIENT_TMUX_SESSION_ID: clockClientService.tmuxSessionId,
1054
+ RCC_CLOCK_CLIENT_DAEMON_ID: clockClientService.daemonId
1055
+ }
1056
+ : {})
1057
+ },
1058
+ baseUrl,
1059
+ configuredApiKey: resolved.configuredApiKey,
1060
+ cwd: currentCwd
1061
+ });
1062
+ const shouldUseShell = ctx.isWindows &&
1063
+ !pathImpl.extname(resolvedBinary) &&
1064
+ !resolvedBinary.includes('/') &&
1065
+ !resolvedBinary.includes('\\');
1066
+ const toolProcess = (() => {
1067
+ if (managedTmuxSession) {
1068
+ const envOverrides = collectChangedEnv(ctx.env, toolEnv);
1069
+ const launched = launchCommandInTmuxPane({
1070
+ spawnSyncImpl,
1071
+ tmuxTarget: managedTmuxSession.tmuxTarget,
1072
+ cwd: currentCwd,
1073
+ command: resolvedBinary,
1074
+ commandArgs: toolArgs,
1075
+ envOverrides,
1076
+ managedSessionName: managedTmuxSession.sessionName
1077
+ });
1078
+ if (!launched) {
1079
+ managedTmuxSession.stop();
1080
+ managedTmuxSession = null;
1081
+ throw new Error(`Failed to send ${spec.displayName} command to managed tmux session`);
1082
+ }
1083
+ return ctx.spawn('tmux', ['attach-session', '-t', managedTmuxSession.sessionName], {
1084
+ stdio: 'inherit',
1085
+ env: ctx.env,
1086
+ cwd: currentCwd
1087
+ });
1088
+ }
1089
+ return ctx.spawn(resolvedBinary, toolArgs, {
1090
+ stdio: 'inherit',
1091
+ env: toolEnv,
1092
+ cwd: currentCwd,
1093
+ shell: shouldUseShell
1094
+ });
1095
+ })();
1096
+ managedClientPid = typeof toolProcess.pid === 'number' && Number.isFinite(toolProcess.pid)
1097
+ ? Math.floor(toolProcess.pid)
1098
+ : null;
1099
+ if (clockClientService && managedClientProcessEnabled && managedClientPid) {
1100
+ void clockClientService.syncHeartbeat();
1101
+ }
1102
+ spinner.succeed(`${spec.displayName} launched with RouteCodex proxy`);
1103
+ if (!managedTmuxSession) {
1104
+ ctx.logger.info(`Using RouteCodex server at: ${baseUrl}`);
1105
+ ctx.logger.info(`${spec.displayName} binary: ${resolvedBinary}`);
1106
+ if (ensureResult.started && ensureResult.logPath) {
1107
+ ctx.logger.info(`RouteCodex auto-start logs: ${ensureResult.logPath}`);
1108
+ }
1109
+ ctx.logger.info(`Working directory for ${spec.displayName}: ${currentCwd}`);
1110
+ ctx.logger.info(`Press Ctrl+C to exit ${spec.displayName}`);
1111
+ }
1112
+ const shutdown = async (signal) => {
1113
+ try {
1114
+ toolProcess.kill(signal);
1115
+ }
1116
+ catch {
1117
+ // ignore
1118
+ }
1119
+ try {
1120
+ await clockClientService?.stop();
1121
+ }
1122
+ catch {
1123
+ // ignore
1124
+ }
1125
+ try {
1126
+ managedTmuxSession?.stop();
1127
+ }
1128
+ catch {
1129
+ // ignore
1130
+ }
1131
+ ctx.exit(0);
1132
+ };
1133
+ const onSignal = ctx.onSignal ?? ((signal, cb) => process.on(signal, cb));
1134
+ onSignal('SIGINT', () => {
1135
+ void shutdown('SIGINT');
1136
+ });
1137
+ onSignal('SIGTERM', () => {
1138
+ void shutdown('SIGTERM');
1139
+ });
1140
+ toolProcess.on('error', (error) => {
1141
+ void (async () => {
1142
+ try {
1143
+ ctx.logger.error(`Failed to launch ${spec.displayName} (${resolvedBinary}): ${error instanceof Error ? error.message : String(error)}`);
1144
+ }
1145
+ catch {
1146
+ // ignore
1147
+ }
1148
+ try {
1149
+ await clockClientService?.stop();
1150
+ }
1151
+ catch {
1152
+ // ignore
1153
+ }
1154
+ try {
1155
+ managedTmuxSession?.stop();
1156
+ }
1157
+ catch {
1158
+ // ignore
1159
+ }
1160
+ ctx.exit(1);
1161
+ })();
1162
+ });
1163
+ toolProcess.on('exit', (code, signal) => {
1164
+ void (async () => {
1165
+ try {
1166
+ await clockClientService?.stop();
1167
+ }
1168
+ catch {
1169
+ // ignore
1170
+ }
1171
+ try {
1172
+ managedTmuxSession?.stop();
1173
+ }
1174
+ catch {
1175
+ // ignore
1176
+ }
1177
+ if (signal) {
1178
+ ctx.exit(0);
1179
+ return;
1180
+ }
1181
+ ctx.exit(code ?? 0);
1182
+ })();
1183
+ });
1184
+ await ctx.waitForever();
1185
+ }
1186
+ catch (error) {
1187
+ spinner.fail(`Failed to launch ${spec.displayName}`);
1188
+ ctx.logger.error(error instanceof Error ? error.message : String(error));
1189
+ ctx.exit(1);
1190
+ }
1191
+ });
1192
+ }
1193
+ export { normalizeOpenAiBaseUrl };
1194
+ //# sourceMappingURL=launcher-kernel.js.map