@remodex/rmx 1.0.2

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 (588) hide show
  1. package/AGENTS_INSTALL.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +584 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-CZqebSPQ.css +1 -0
  11. package/gui/dist/assets/index-CkETtt7P.js +71 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
  14. package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
  15. package/gui/dist/icons.svg +24 -0
  16. package/gui/dist/index.html +25 -0
  17. package/gui/dist/logo.png +0 -0
  18. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  19. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  20. package/gui/dist/provider-icons/claude-color.svg +1 -0
  21. package/gui/dist/provider-icons/cline-color.svg +16 -0
  22. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  23. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  24. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  25. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  26. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  27. package/gui/dist/provider-icons/discord.svg +1 -0
  28. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  29. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok.svg +1 -0
  34. package/gui/dist/provider-icons/groq-color.svg +1 -0
  35. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  36. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  37. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  38. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  39. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  40. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  41. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  42. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  43. package/gui/dist/provider-icons/openai.svg +1 -0
  44. package/gui/dist/provider-icons/opencode.svg +2 -0
  45. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  46. package/gui/dist/provider-icons/pi.svg +21 -0
  47. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  48. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  49. package/gui/dist/provider-icons/telegram.svg +1 -0
  50. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  51. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  52. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  53. package/package.json +118 -0
  54. package/src/AGENTS.md +28 -0
  55. package/src/adapters/anthropic-image-guard.ts +251 -0
  56. package/src/adapters/anthropic-image-normalize.ts +518 -0
  57. package/src/adapters/anthropic.ts +1205 -0
  58. package/src/adapters/azure.ts +36 -0
  59. package/src/adapters/base.ts +83 -0
  60. package/src/adapters/client-fingerprint.ts +59 -0
  61. package/src/adapters/command-code.ts +453 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/cursor-errors.ts +165 -0
  65. package/src/adapters/cursor/discovery.ts +276 -0
  66. package/src/adapters/cursor/effort-map.ts +139 -0
  67. package/src/adapters/cursor/exec-policy.ts +88 -0
  68. package/src/adapters/cursor/framing.ts +250 -0
  69. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  70. package/src/adapters/cursor/kv-store.ts +52 -0
  71. package/src/adapters/cursor/live-models.ts +153 -0
  72. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  73. package/src/adapters/cursor/live-transport.ts +1235 -0
  74. package/src/adapters/cursor/mcp-config.ts +42 -0
  75. package/src/adapters/cursor/mcp-manager.ts +333 -0
  76. package/src/adapters/cursor/message-mapper.ts +49 -0
  77. package/src/adapters/cursor/native-exec-common.ts +59 -0
  78. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  79. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  80. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  81. package/src/adapters/cursor/native-exec-network.ts +43 -0
  82. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  83. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  84. package/src/adapters/cursor/native-exec.ts +604 -0
  85. package/src/adapters/cursor/protobuf-events.ts +735 -0
  86. package/src/adapters/cursor/protobuf-request.ts +719 -0
  87. package/src/adapters/cursor/request-builder.ts +280 -0
  88. package/src/adapters/cursor/thread-continuity.ts +67 -0
  89. package/src/adapters/cursor/tool-definitions.ts +621 -0
  90. package/src/adapters/cursor/transport-retry.ts +132 -0
  91. package/src/adapters/cursor/transport.ts +57 -0
  92. package/src/adapters/cursor/types.ts +59 -0
  93. package/src/adapters/cursor.ts +196 -0
  94. package/src/adapters/google-antigravity-replay.ts +520 -0
  95. package/src/adapters/google-antigravity-wire.ts +140 -0
  96. package/src/adapters/google-errors.ts +85 -0
  97. package/src/adapters/google-http.ts +100 -0
  98. package/src/adapters/google-tool-schema.ts +173 -0
  99. package/src/adapters/google-truncation.ts +24 -0
  100. package/src/adapters/google-wire-compiler.ts +232 -0
  101. package/src/adapters/google.ts +859 -0
  102. package/src/adapters/identity.ts +77 -0
  103. package/src/adapters/image.ts +23 -0
  104. package/src/adapters/kiro-constants.ts +16 -0
  105. package/src/adapters/kiro-errors.ts +208 -0
  106. package/src/adapters/kiro-events.ts +197 -0
  107. package/src/adapters/kiro-images.ts +129 -0
  108. package/src/adapters/kiro-retry.ts +312 -0
  109. package/src/adapters/kiro-thinking.ts +104 -0
  110. package/src/adapters/kiro-tool-fallback.ts +36 -0
  111. package/src/adapters/kiro-tools.ts +224 -0
  112. package/src/adapters/kiro-truncation.ts +33 -0
  113. package/src/adapters/kiro-wire.ts +129 -0
  114. package/src/adapters/kiro.ts +1924 -0
  115. package/src/adapters/mimo-free.ts +263 -0
  116. package/src/adapters/openai-chat.ts +1265 -0
  117. package/src/adapters/openai-responses.ts +1309 -0
  118. package/src/adapters/run-turn-queue.ts +114 -0
  119. package/src/adapters/tool-catalog-nudge.ts +71 -0
  120. package/src/adapters/upstream-http-error.ts +48 -0
  121. package/src/android-remote/assets.ts +218 -0
  122. package/src/android-remote/attachments.ts +168 -0
  123. package/src/android-remote/auth.ts +237 -0
  124. package/src/android-remote/cloudflare-provisioning.ts +409 -0
  125. package/src/android-remote/cloudflare-secret.ts +106 -0
  126. package/src/android-remote/cloudflare-tunnel.ts +488 -0
  127. package/src/android-remote/cloudflared.ts +286 -0
  128. package/src/android-remote/codex-app-server.ts +565 -0
  129. package/src/android-remote/desktop-history-page.ts +936 -0
  130. package/src/android-remote/desktop-ipc.ts +3643 -0
  131. package/src/android-remote/desktop-ownership-store.ts +98 -0
  132. package/src/android-remote/desktop-project-registration.ts +129 -0
  133. package/src/android-remote/desktop-session-stream.ts +1110 -0
  134. package/src/android-remote/desktop-workspace-state.ts +443 -0
  135. package/src/android-remote/file-change-parser.ts +112 -0
  136. package/src/android-remote/gateway.ts +9108 -0
  137. package/src/android-remote/mutation-store.ts +299 -0
  138. package/src/android-remote/projection.ts +1780 -0
  139. package/src/android-remote/queued-turn-store.ts +248 -0
  140. package/src/android-remote/session-command-recovery.ts +1384 -0
  141. package/src/android-remote/store.ts +466 -0
  142. package/src/android-remote/thread-reconciliation.ts +133 -0
  143. package/src/android-remote/thread-source-paths.ts +307 -0
  144. package/src/android-remote/thread-stream.ts +546 -0
  145. package/src/android-remote/turn-activity.ts +235 -0
  146. package/src/android-remote/user-message-identity.ts +94 -0
  147. package/src/bridge.ts +1793 -0
  148. package/src/chat/inbound.ts +295 -0
  149. package/src/chat/outbound.ts +821 -0
  150. package/src/claude/agents-inject.ts +267 -0
  151. package/src/claude/alias.ts +149 -0
  152. package/src/claude/auth-detect.ts +229 -0
  153. package/src/claude/auth-mode-migration.ts +32 -0
  154. package/src/claude/auth-mode.ts +62 -0
  155. package/src/claude/context-windows.ts +189 -0
  156. package/src/claude/desktop-3p-guard.ts +35 -0
  157. package/src/claude/desktop-3p-paths.ts +84 -0
  158. package/src/claude/desktop-3p.ts +601 -0
  159. package/src/claude/desktop-health.ts +26 -0
  160. package/src/claude/desktop-profile.ts +263 -0
  161. package/src/claude/gateway-cache.ts +70 -0
  162. package/src/claude/inbound-debug.ts +163 -0
  163. package/src/claude/inbound.ts +519 -0
  164. package/src/claude/model-info.ts +154 -0
  165. package/src/claude/outbound.ts +898 -0
  166. package/src/cli/access.ts +108 -0
  167. package/src/cli/account-api.ts +296 -0
  168. package/src/cli/account-auth.ts +250 -0
  169. package/src/cli/account-catalog-refresh.ts +14 -0
  170. package/src/cli/account-extended.ts +476 -0
  171. package/src/cli/account-main.ts +317 -0
  172. package/src/cli/account.ts +297 -0
  173. package/src/cli/agent-driven.ts +70 -0
  174. package/src/cli/agent.ts +184 -0
  175. package/src/cli/catalog-prewarm.ts +27 -0
  176. package/src/cli/claude-desktop.ts +211 -0
  177. package/src/cli/claude.ts +302 -0
  178. package/src/cli/codex-shim-autorestore.ts +45 -0
  179. package/src/cli/codex-shim-readiness.ts +69 -0
  180. package/src/cli/combo.ts +124 -0
  181. package/src/cli/config-command.ts +183 -0
  182. package/src/cli/debug.ts +228 -0
  183. package/src/cli/desktop-first-run.ts +25 -0
  184. package/src/cli/doctor.ts +1022 -0
  185. package/src/cli/export-command.ts +201 -0
  186. package/src/cli/help.ts +370 -0
  187. package/src/cli/index.ts +1565 -0
  188. package/src/cli/init.ts +224 -0
  189. package/src/cli/integrations.ts +225 -0
  190. package/src/cli/interactive-confirm.ts +133 -0
  191. package/src/cli/internal-dispatch.ts +35 -0
  192. package/src/cli/launcher-context.ts +77 -0
  193. package/src/cli/models-runtime.ts +224 -0
  194. package/src/cli/models.ts +340 -0
  195. package/src/cli/observe.ts +170 -0
  196. package/src/cli/opencode.ts +587 -0
  197. package/src/cli/provider-runtime.ts +179 -0
  198. package/src/cli/provider.ts +476 -0
  199. package/src/cli/ready.ts +301 -0
  200. package/src/cli/route-policy.ts +92 -0
  201. package/src/cli/runtime-api.ts +328 -0
  202. package/src/cli/star-prompt.ts +211 -0
  203. package/src/cli/status-oauth.ts +78 -0
  204. package/src/cli/status.ts +321 -0
  205. package/src/cli/system-command.ts +196 -0
  206. package/src/cli/system-restart-client.ts +146 -0
  207. package/src/cli/tray-proxy.ts +205 -0
  208. package/src/cli/v2.ts +200 -0
  209. package/src/cli.ts +10 -0
  210. package/src/clients/config-export.ts +1109 -0
  211. package/src/codex/account-id.ts +34 -0
  212. package/src/codex/account-label.ts +34 -0
  213. package/src/codex/account-lifecycle.ts +172 -0
  214. package/src/codex/account-namespace-match.ts +63 -0
  215. package/src/codex/account-namespaces.ts +195 -0
  216. package/src/codex/account-pause.ts +20 -0
  217. package/src/codex/account-priority.ts +83 -0
  218. package/src/codex/account-runtime-state.ts +31 -0
  219. package/src/codex/account-store.ts +517 -0
  220. package/src/codex/account-usability.ts +40 -0
  221. package/src/codex/admission.ts +263 -0
  222. package/src/codex/app-server-processes.ts +799 -0
  223. package/src/codex/auth-api.ts +2098 -0
  224. package/src/codex/auth-collision.ts +107 -0
  225. package/src/codex/auth-context.ts +480 -0
  226. package/src/codex/autostart-health.ts +156 -0
  227. package/src/codex/catalog/account-models.ts +67 -0
  228. package/src/codex/catalog/aggregation.ts +471 -0
  229. package/src/codex/catalog/bundled.ts +533 -0
  230. package/src/codex/catalog/effort.ts +432 -0
  231. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  232. package/src/codex/catalog/kinds.ts +2 -0
  233. package/src/codex/catalog/metadata.ts +287 -0
  234. package/src/codex/catalog/native-models.ts +7 -0
  235. package/src/codex/catalog/parsing.ts +503 -0
  236. package/src/codex/catalog/provider-fetch.ts +2267 -0
  237. package/src/codex/catalog/sync.ts +1606 -0
  238. package/src/codex/catalog-admission.ts +197 -0
  239. package/src/codex/catalog-refresh-status.ts +87 -0
  240. package/src/codex/catalog-write-serialization.ts +242 -0
  241. package/src/codex/catalog.ts +15 -0
  242. package/src/codex/codex-write-lock.ts +384 -0
  243. package/src/codex/convergence-types.ts +593 -0
  244. package/src/codex/convergence.ts +580 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/desktop-client-processes.ts +521 -0
  249. package/src/codex/exec-invocation.ts +22 -0
  250. package/src/codex/features.ts +1091 -0
  251. package/src/codex/generation.ts +202 -0
  252. package/src/codex/history-job.ts +347 -0
  253. package/src/codex/history-lock.ts +242 -0
  254. package/src/codex/history-migration-guardian.ts +115 -0
  255. package/src/codex/history-provider.ts +1075 -0
  256. package/src/codex/history-transition.ts +105 -0
  257. package/src/codex/history-worker.ts +204 -0
  258. package/src/codex/home.ts +206 -0
  259. package/src/codex/inject-coordination.ts +257 -0
  260. package/src/codex/inject.ts +1857 -0
  261. package/src/codex/injected-marker.ts +79 -0
  262. package/src/codex/integration-record.ts +266 -0
  263. package/src/codex/internal/catalog-writer.ts +203 -0
  264. package/src/codex/internal/history-writer.ts +105 -0
  265. package/src/codex/journal.ts +172 -0
  266. package/src/codex/main-account-cache.ts +56 -0
  267. package/src/codex/main-account.ts +40 -0
  268. package/src/codex/management-convergence.ts +114 -0
  269. package/src/codex/model-cache.ts +267 -0
  270. package/src/codex/native-main-admission.ts +47 -0
  271. package/src/codex/native-main-auth-temp.ts +187 -0
  272. package/src/codex/native-main-claim.ts +167 -0
  273. package/src/codex/native-main-lock-file.ts +162 -0
  274. package/src/codex/native-main-owner.ts +329 -0
  275. package/src/codex/native-profile-api.ts +247 -0
  276. package/src/codex/native-profile-manager.ts +1531 -0
  277. package/src/codex/native-profile-processes.ts +121 -0
  278. package/src/codex/native-profile-recovery.ts +99 -0
  279. package/src/codex/native-profile-stage-store.ts +387 -0
  280. package/src/codex/native-profile-startup.ts +348 -0
  281. package/src/codex/native-profile-store.ts +855 -0
  282. package/src/codex/native-profile-types.ts +120 -0
  283. package/src/codex/native-residue.ts +691 -0
  284. package/src/codex/paths.ts +78 -0
  285. package/src/codex/plugins-doctor.ts +242 -0
  286. package/src/codex/pool-rotation.ts +295 -0
  287. package/src/codex/project-config-warnings.ts +426 -0
  288. package/src/codex/prompt-journal.ts +311 -0
  289. package/src/codex/prompt-layers.ts +967 -0
  290. package/src/codex/prompt-lock.ts +143 -0
  291. package/src/codex/provider-adoption.ts +242 -0
  292. package/src/codex/quota-rejection.ts +224 -0
  293. package/src/codex/quota.ts +494 -0
  294. package/src/codex/refresh.ts +60 -0
  295. package/src/codex/routing.ts +1855 -0
  296. package/src/codex/runtime.ts +659 -0
  297. package/src/codex/shim.ts +1215 -0
  298. package/src/codex/subagent-defaults.ts +557 -0
  299. package/src/codex/subagent-model-fallback.ts +560 -0
  300. package/src/codex/sync.ts +238 -0
  301. package/src/codex/transition-state.ts +612 -0
  302. package/src/codex/upstream-host-health.ts +368 -0
  303. package/src/codex/user-identity.ts +374 -0
  304. package/src/codex/warmup.ts +192 -0
  305. package/src/codex/websocket-registry.ts +100 -0
  306. package/src/codex/write-coordination.ts +114 -0
  307. package/src/combos/failover.ts +140 -0
  308. package/src/combos/index.ts +44 -0
  309. package/src/combos/request.ts +64 -0
  310. package/src/combos/resolve.ts +232 -0
  311. package/src/combos/types.ts +392 -0
  312. package/src/config.ts +3270 -0
  313. package/src/generated/model-metadata.ts +144 -0
  314. package/src/github/star-state.ts +203 -0
  315. package/src/grok/inject.ts +540 -0
  316. package/src/grok/inspect.ts +45 -0
  317. package/src/grok/status.ts +127 -0
  318. package/src/grok/sync.ts +66 -0
  319. package/src/images/artifacts.ts +516 -0
  320. package/src/images/fulfill-video.ts +163 -0
  321. package/src/images/fulfill.ts +149 -0
  322. package/src/images/index.ts +4 -0
  323. package/src/images/loop.ts +922 -0
  324. package/src/images/plan.ts +133 -0
  325. package/src/images/synthetic-tool.ts +133 -0
  326. package/src/images/types.ts +41 -0
  327. package/src/images/xai-client.ts +141 -0
  328. package/src/images/xai-video-client.ts +163 -0
  329. package/src/index.ts +22 -0
  330. package/src/integrations/config-io.ts +151 -0
  331. package/src/integrations/journal.ts +315 -0
  332. package/src/integrations/merge.ts +135 -0
  333. package/src/integrations/native/ownership-preflight.ts +202 -0
  334. package/src/integrations/ownership.ts +111 -0
  335. package/src/integrations/registry.ts +108 -0
  336. package/src/integrations/serialize.ts +235 -0
  337. package/src/integrations/state.ts +290 -0
  338. package/src/integrations/store.ts +103 -0
  339. package/src/integrations/writer.ts +492 -0
  340. package/src/lib/abort.ts +146 -0
  341. package/src/lib/admin-secrets.ts +25 -0
  342. package/src/lib/admission.ts +83 -0
  343. package/src/lib/app-owned-memory-stores.ts +173 -0
  344. package/src/lib/app-owned-memory.ts +265 -0
  345. package/src/lib/bounded-body.ts +242 -0
  346. package/src/lib/bun-binary-validator.d.mts +3 -0
  347. package/src/lib/bun-binary-validator.mjs +18 -0
  348. package/src/lib/bun-runtime.ts +184 -0
  349. package/src/lib/bun-stream-caps.ts +127 -0
  350. package/src/lib/config-ownership.ts +438 -0
  351. package/src/lib/crash-guard.ts +344 -0
  352. package/src/lib/debug-log-buffer.ts +83 -0
  353. package/src/lib/debug-settings.ts +108 -0
  354. package/src/lib/debug.ts +31 -0
  355. package/src/lib/destination-policy.ts +316 -0
  356. package/src/lib/errors.ts +364 -0
  357. package/src/lib/eventstream-decoder.ts +253 -0
  358. package/src/lib/gcp-adc.ts +341 -0
  359. package/src/lib/injection-debug-log.ts +58 -0
  360. package/src/lib/local-management-attestation.ts +51 -0
  361. package/src/lib/open-url.ts +25 -0
  362. package/src/lib/pinned-http.ts +182 -0
  363. package/src/lib/privacy.ts +20 -0
  364. package/src/lib/process-control.ts +168 -0
  365. package/src/lib/provider-environment.ts +470 -0
  366. package/src/lib/provider-outbound.ts +203 -0
  367. package/src/lib/provider-url.ts +14 -0
  368. package/src/lib/proxy-env.ts +18 -0
  369. package/src/lib/redact.ts +510 -0
  370. package/src/lib/remodex-home.ts +616 -0
  371. package/src/lib/retry-after.ts +55 -0
  372. package/src/lib/service-secrets.ts +178 -0
  373. package/src/lib/shadow-call.ts +54 -0
  374. package/src/lib/sidecar-tracker.ts +52 -0
  375. package/src/lib/sse-decoder.ts +364 -0
  376. package/src/lib/state-store-registrations.ts +109 -0
  377. package/src/lib/state-store-sweeper.ts +184 -0
  378. package/src/lib/system-restart-contract.ts +73 -0
  379. package/src/lib/test-home-guard.ts +98 -0
  380. package/src/lib/token-estimate.ts +69 -0
  381. package/src/lib/translator-budget.ts +366 -0
  382. package/src/lib/upstream-reachability.ts +91 -0
  383. package/src/lib/upstream-retry.ts +508 -0
  384. package/src/lib/win-exec.ts +115 -0
  385. package/src/lib/win-paths.ts +68 -0
  386. package/src/lib/windows-elevation.ts +705 -0
  387. package/src/lib/windows-secret-acl.ts +817 -0
  388. package/src/lib/windows-user-principal.ts +283 -0
  389. package/src/lib/winsw.ts +402 -0
  390. package/src/model-sources.ts +73 -0
  391. package/src/oauth/anthropic-routing.ts +594 -0
  392. package/src/oauth/anthropic.ts +177 -0
  393. package/src/oauth/callback-server.ts +294 -0
  394. package/src/oauth/chatgpt.ts +150 -0
  395. package/src/oauth/command-code.ts +239 -0
  396. package/src/oauth/cursor.ts +231 -0
  397. package/src/oauth/github-copilot.ts +428 -0
  398. package/src/oauth/google-antigravity.ts +230 -0
  399. package/src/oauth/health.ts +443 -0
  400. package/src/oauth/index.ts +1280 -0
  401. package/src/oauth/key-providers.ts +128 -0
  402. package/src/oauth/kimi.ts +213 -0
  403. package/src/oauth/kiro-credentials.ts +726 -0
  404. package/src/oauth/kiro.ts +621 -0
  405. package/src/oauth/local-token-detect.ts +121 -0
  406. package/src/oauth/log.ts +48 -0
  407. package/src/oauth/login-cli.ts +163 -0
  408. package/src/oauth/pkce.ts +15 -0
  409. package/src/oauth/store.ts +655 -0
  410. package/src/oauth/token-guardian.ts +309 -0
  411. package/src/oauth/types.ts +62 -0
  412. package/src/oauth/xai.ts +241 -0
  413. package/src/providers/alibaba-region-backup.ts +75 -0
  414. package/src/providers/alibaba-region-migration.ts +156 -0
  415. package/src/providers/alibaba-region-startup.ts +36 -0
  416. package/src/providers/antigravity-models.ts +317 -0
  417. package/src/providers/api-keys.ts +140 -0
  418. package/src/providers/base-url-choices.ts +64 -0
  419. package/src/providers/codex-capacity.ts +288 -0
  420. package/src/providers/command-code-efforts.ts +85 -0
  421. package/src/providers/context-cap.ts +73 -0
  422. package/src/providers/derive.ts +451 -0
  423. package/src/providers/free-directory.ts +187 -0
  424. package/src/providers/github-copilot-transport.ts +56 -0
  425. package/src/providers/google-vertex-location.ts +14 -0
  426. package/src/providers/key-failover.ts +271 -0
  427. package/src/providers/kiro-models.ts +67 -0
  428. package/src/providers/label.ts +19 -0
  429. package/src/providers/model-discovery-limits.ts +16 -0
  430. package/src/providers/model-discovery.ts +361 -0
  431. package/src/providers/openai-sidecar.ts +235 -0
  432. package/src/providers/openai-tier-startup.ts +27 -0
  433. package/src/providers/openai-tiers.ts +301 -0
  434. package/src/providers/openai-virtual-models.ts +83 -0
  435. package/src/providers/openrouter-routing.ts +102 -0
  436. package/src/providers/provider-id-rewrite.ts +179 -0
  437. package/src/providers/quota.ts +1942 -0
  438. package/src/providers/reasoning-capabilities.ts +336 -0
  439. package/src/providers/registry.ts +2375 -0
  440. package/src/providers/slug-codec.ts +74 -0
  441. package/src/providers/xai-transport.ts +149 -0
  442. package/src/reasoning-effort.ts +243 -0
  443. package/src/responses/compaction.ts +124 -0
  444. package/src/responses/hosted-tool-policy.ts +9 -0
  445. package/src/responses/parser.ts +714 -0
  446. package/src/responses/reasoning-envelope.ts +60 -0
  447. package/src/responses/reasoning-replay-cache.ts +106 -0
  448. package/src/responses/schema.ts +159 -0
  449. package/src/responses/spill-store.ts +431 -0
  450. package/src/responses/state.ts +1039 -0
  451. package/src/responses/tool-groups.ts +19 -0
  452. package/src/router.ts +802 -0
  453. package/src/routing/analytics.ts +377 -0
  454. package/src/routing/capability.ts +205 -0
  455. package/src/routing/cost.ts +77 -0
  456. package/src/routing/evaluator.ts +444 -0
  457. package/src/routing/health.ts +401 -0
  458. package/src/routing/history/cursor.ts +43 -0
  459. package/src/routing/history/indexer.ts +605 -0
  460. package/src/routing/history/schema.ts +72 -0
  461. package/src/routing/profile-namespace.ts +15 -0
  462. package/src/routing/profile.ts +424 -0
  463. package/src/routing/quota.ts +145 -0
  464. package/src/routing/request-evidence.ts +45 -0
  465. package/src/routing/trace.ts +686 -0
  466. package/src/server/adapter-resolve.ts +83 -0
  467. package/src/server/auth-cors.ts +606 -0
  468. package/src/server/chat-completions.ts +379 -0
  469. package/src/server/claude-messages.ts +980 -0
  470. package/src/server/effort-policy.ts +251 -0
  471. package/src/server/github-copilot-responses-repair.ts +338 -0
  472. package/src/server/gui-static.ts +152 -0
  473. package/src/server/image-retry.ts +42 -0
  474. package/src/server/images.ts +485 -0
  475. package/src/server/index.ts +1633 -0
  476. package/src/server/lifecycle.ts +482 -0
  477. package/src/server/live.ts +609 -0
  478. package/src/server/management/agent-settings-routes.ts +1180 -0
  479. package/src/server/management/android-remote-routes.ts +390 -0
  480. package/src/server/management/api-access.ts +141 -0
  481. package/src/server/management/api-key-usage.ts +167 -0
  482. package/src/server/management/body.ts +35 -0
  483. package/src/server/management/combo-routes.ts +244 -0
  484. package/src/server/management/config-routes.ts +602 -0
  485. package/src/server/management/context.ts +88 -0
  486. package/src/server/management/integration-routes.ts +538 -0
  487. package/src/server/management/logs-usage-routes.ts +516 -0
  488. package/src/server/management/model-routes.ts +519 -0
  489. package/src/server/management/model-rows.ts +143 -0
  490. package/src/server/management/native-integration-routes.ts +781 -0
  491. package/src/server/management/oauth-account-routes.ts +573 -0
  492. package/src/server/management/provider-routes.ts +781 -0
  493. package/src/server/management/request-history-routes.ts +191 -0
  494. package/src/server/management/routing-analytics-routes.ts +74 -0
  495. package/src/server/management/routing-profile-routes.ts +384 -0
  496. package/src/server/management/shared.ts +277 -0
  497. package/src/server/management/sidebar-routes.ts +106 -0
  498. package/src/server/management/sync-response.ts +69 -0
  499. package/src/server/management/system-restart.ts +433 -0
  500. package/src/server/management/system-routes.ts +141 -0
  501. package/src/server/management/usage-summary-cache.ts +86 -0
  502. package/src/server/management-api.ts +269 -0
  503. package/src/server/management-auth.ts +353 -0
  504. package/src/server/memory-watchdog.ts +156 -0
  505. package/src/server/port-reclaim.ts +307 -0
  506. package/src/server/ports.ts +156 -0
  507. package/src/server/proxy-liveness.ts +326 -0
  508. package/src/server/proxy-stop.ts +92 -0
  509. package/src/server/readiness.ts +99 -0
  510. package/src/server/relay-eager.ts +353 -0
  511. package/src/server/relay.ts +1179 -0
  512. package/src/server/request-decompress.ts +132 -0
  513. package/src/server/request-log-conversation.ts +168 -0
  514. package/src/server/request-log.ts +1072 -0
  515. package/src/server/responses/collaboration.ts +409 -0
  516. package/src/server/responses/compact.ts +710 -0
  517. package/src/server/responses/core.ts +3561 -0
  518. package/src/server/responses/encrypted-payload.ts +308 -0
  519. package/src/server/responses/fetch-helpers.ts +171 -0
  520. package/src/server/responses/passthrough-error.ts +78 -0
  521. package/src/server/responses/policy-fallback.ts +152 -0
  522. package/src/server/responses/terminal-guard.ts +230 -0
  523. package/src/server/responses/upstream-error.ts +48 -0
  524. package/src/server/responses-image-gen-repair.ts +132 -0
  525. package/src/server/responses-item-id-repair.ts +272 -0
  526. package/src/server/responses-json-events.ts +52 -0
  527. package/src/server/responses-model-rewrite.ts +29 -0
  528. package/src/server/responses-snapshot-repair.ts +621 -0
  529. package/src/server/responses.ts +10 -0
  530. package/src/server/search.ts +181 -0
  531. package/src/server/sse-frame-buffer.ts +292 -0
  532. package/src/server/sse-payload-rewrite.ts +263 -0
  533. package/src/server/startup-action-control.ts +308 -0
  534. package/src/server/startup-health-cache.ts +119 -0
  535. package/src/server/system-env.ts +418 -0
  536. package/src/server/windows-tcp-drop.ts +184 -0
  537. package/src/server/windows-tray-control.ts +41 -0
  538. package/src/server/ws-bridge.ts +470 -0
  539. package/src/service-manager-probe.ts +824 -0
  540. package/src/service.ts +3011 -0
  541. package/src/stall-timeout.ts +20 -0
  542. package/src/storage/cleanup-job.ts +57 -0
  543. package/src/storage/cleanup.ts +3085 -0
  544. package/src/storage/policy-job.ts +457 -0
  545. package/src/storage/policy-scheduler.ts +40 -0
  546. package/src/storage/policy-worker.ts +59 -0
  547. package/src/storage/policy.ts +527 -0
  548. package/src/storage/restore-job.ts +299 -0
  549. package/src/storage/restore-worker.ts +58 -0
  550. package/src/storage/scanner.ts +238 -0
  551. package/src/storage/storage-mutation-coordinator.ts +139 -0
  552. package/src/storage/worker-lifecycle.ts +215 -0
  553. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  554. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  555. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  556. package/src/tray/assets/opencodex-tray.png +0 -0
  557. package/src/tray/windows-tray.ps1 +364 -0
  558. package/src/tray/windows.ts +738 -0
  559. package/src/types.ts +1531 -0
  560. package/src/update/badge.ts +72 -0
  561. package/src/update/desktop-release.ts +1620 -0
  562. package/src/update/index.ts +402 -0
  563. package/src/update/job.ts +1906 -0
  564. package/src/update/notify.ts +261 -0
  565. package/src/update/npm-cache-preflight.d.mts +47 -0
  566. package/src/update/npm-cache-preflight.mjs +201 -0
  567. package/src/update/npm-invocation.d.mts +23 -0
  568. package/src/update/npm-invocation.mjs +94 -0
  569. package/src/update/tray-update-plan.d.mts +18 -0
  570. package/src/update/tray-update-plan.mjs +38 -0
  571. package/src/usage/cost.ts +0 -0
  572. package/src/usage/debug.ts +97 -0
  573. package/src/usage/expected-prices.ts +283 -0
  574. package/src/usage/log.ts +695 -0
  575. package/src/usage/summary.ts +585 -0
  576. package/src/usage/totals.ts +14 -0
  577. package/src/vision/anthropic-describe.ts +185 -0
  578. package/src/vision/describe.ts +127 -0
  579. package/src/vision/index.ts +558 -0
  580. package/src/vision/reasoning.ts +55 -0
  581. package/src/web-search/anthropic-executor.ts +189 -0
  582. package/src/web-search/executor.ts +105 -0
  583. package/src/web-search/format-result.ts +89 -0
  584. package/src/web-search/index.ts +196 -0
  585. package/src/web-search/loop.ts +791 -0
  586. package/src/web-search/parse.ts +235 -0
  587. package/src/web-search/progress-stream.ts +342 -0
  588. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,1565 @@
1
+ #!/usr/bin/env bun
2
+ import { spawn } from "node:child_process";
3
+ import { currentExternalCodexModelProvider, restoreNativeCodex, restoreNativeCodexAsync, shouldInjectApiAuthHeader } from "../codex/inject";
4
+ import { stripGrokConfig } from "../grok/inject";
5
+ import {
6
+ describeHistoryJobFailure,
7
+ resolveCodexHistoryJobTarget,
8
+ runCodexHistoryJob,
9
+ } from "../codex/history-job";
10
+ import { reconcileJournal } from "../codex/journal";
11
+ import {
12
+ codexAutoStartEnabled,
13
+ getConfigDir,
14
+ loadConfig,
15
+ readPid,
16
+ readPidFileValue,
17
+ readRuntimePort,
18
+ readConfigDiagnostics,
19
+ removePid,
20
+ removePidIfValueIs,
21
+ removeRuntimePort,
22
+ removeRuntimePortIfPidIs,
23
+ saveConfig,
24
+ writePid,
25
+ writeRuntimePort,
26
+ } from "../config";
27
+ import { collectStatus } from "./status";
28
+ import { dispatchInternalCliCommand, type InternalCliCommand } from "./internal-dispatch";
29
+ import {
30
+ discoverStableProxyForRestart,
31
+ isProxyReplacement,
32
+ runProxyRestart,
33
+ runTrayProxyStart,
34
+ type ProxyRestartLive,
35
+ type ProxyRestartResult,
36
+ } from "./tray-proxy";
37
+ import { requestBoundSystemRestart } from "./system-restart-client";
38
+ import { installCrashGuards } from "../lib/crash-guard";
39
+ import { hasHelpFlag, printSubcommandUsage, printUsage, printVersion } from "./help";
40
+ import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
41
+ import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
42
+ import { createReadinessGate } from "../server/readiness";
43
+ import { parseReadyArgs, runReady, type ReadyArgs } from "./ready";
44
+ import { ProxyOwnershipRefusedError, stopProxy } from "../lib/process-control";
45
+ import { loadServiceProviderEnvironmentFromFile, loadServiceTokenFromFile, shouldLoadServiceProviderEnvironment } from "../lib/service-secrets";
46
+ import { hydrateProviderEnvironment } from "../lib/provider-environment";
47
+ import { assertServiceEnvironmentMatchesInstall, diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
48
+ import { startupHealthSummary } from "../codex/autostart-health";
49
+ import { drainAndShutdown, isRecyclingForExit, startServer } from "../server";
50
+ import { injectSystemEnv, revertSystemEnv } from "../server/system-env";
51
+ import { buildDesktop3pRegistry } from "../claude/desktop-3p";
52
+ import { installShellHook, uninstallShellHook } from "../server/system-env";
53
+ import { startTokenGuardian } from "../oauth/token-guardian";
54
+ import { startHistoryMigrationGuardian } from "../codex/history-migration-guardian";
55
+ import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
56
+ import { maybeShowStarPrompt } from "./star-prompt";
57
+ import { scheduleCatalogPrewarm } from "./catalog-prewarm";
58
+ import { maybeShowUpdatePrompt } from "../update/notify";
59
+ import { syncModelsToCodex } from "../codex/sync";
60
+ import { setIntegrationEnabled, shouldSyncCodexOnStart, shouldSyncGrokOnStart, syncCodexOnStartIfEnabled } from "../codex/desired-state";
61
+ import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
62
+ import { currentVersion } from "../update/index";
63
+ import {
64
+ desktopUpdateInstallAction,
65
+ readDesktopUpdateState,
66
+ runAutomaticDesktopUpdateCheck,
67
+ runDesktopUpdateWorker,
68
+ startDesktopUpdateJob,
69
+ type DesktopReleaseChannel,
70
+ } from "../update/desktop-release";
71
+ import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
72
+ import { removeOwnedConfigState } from "../lib/config-ownership";
73
+ import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
74
+ import { initializeNodeLauncherContext } from "./launcher-context";
75
+ import { createLocalAttestationSecret } from "../lib/local-management-attestation";
76
+ import { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../lib/system-restart-contract";
77
+ import { bootstrapDesktopConfigIfNeeded } from "./desktop-first-run";
78
+
79
+ initializeNodeLauncherContext();
80
+ if (shouldLoadServiceProviderEnvironment(process.env)) loadServiceProviderEnvironmentFromFile(process.env);
81
+ const args = process.argv.slice(2);
82
+ const command = args[0];
83
+
84
+ if (command === "--version" || command === "-v" || command === "version") {
85
+ printVersion();
86
+ process.exit(0);
87
+ }
88
+
89
+ if (command === undefined || command === "help" || command === "--help" || command === "-h") {
90
+ if (command === "help" && args[1]) printSubcommandUsage(args[1]);
91
+ else printUsage();
92
+ process.exit(0);
93
+ }
94
+
95
+ if (command !== undefined && command !== "help" && hasHelpFlag(args.slice(1))) {
96
+ printSubcommandUsage(command);
97
+ process.exit(0);
98
+ }
99
+
100
+ // P1: pre-parse `rmx ready` and reject invalid arguments with exit 64 BEFORE
101
+ // maybeAutoRestoreCodexShim (or any discovery/probe/filesystem-capable global
102
+ // preflight) runs. `ready --help` / `help ready` already exited above, so this
103
+ // only sees ready args without a help flag. Valid args are stashed so the
104
+ // switch dispatch can call runReady without a second parse.
105
+ let readyArgs: ReadyArgs | undefined;
106
+ if (command === "ready") {
107
+ const parsed = parseReadyArgs(args.slice(1));
108
+ if (!parsed.ok) {
109
+ console.error("Usage: rmx ready [--json] [--wait [--timeout <seconds>]]");
110
+ console.error(" --timeout requires --wait; <seconds> must be a positive integer (1..300).");
111
+ console.error(" Default wait timeout is 45 seconds.");
112
+ process.exit(parsed.code);
113
+ }
114
+ readyArgs = parsed.args;
115
+ }
116
+
117
+ // A terminal-launched process normally inherits these values, but the desktop
118
+ // shell, login services, and IDE launchers often do not. Do this after all help
119
+ // and argument validation so read-only help/invalid-ready paths stay side-effect free.
120
+ try {
121
+ // The preflight only needs the parsed provider/proxy references. Use the
122
+ // diagnostics reader so read-only commands such as `rmx status --json` do
123
+ // not trigger loadConfig's malformed-config backup/warning path before their
124
+ // own read-only reporting runs.
125
+ hydrateProviderEnvironment(readConfigDiagnostics().config);
126
+ } catch {
127
+ // Environment discovery is optional; normal config/provider validation reports
128
+ // any genuinely required missing credential at the point it is used.
129
+ }
130
+
131
+ maybeAutoRestoreCodexShim(command, args);
132
+
133
+ function parsePortOption(): number | undefined {
134
+ if (args.length === 1) return undefined;
135
+ if (args.length !== 3 || args[1] !== "--port") {
136
+ console.error("Usage: rmx start [--port <port>]");
137
+ process.exit(1);
138
+ }
139
+ const portIdx = args.indexOf("--port");
140
+ if (portIdx === -1) return undefined;
141
+ const value = args[portIdx + 1];
142
+ const port = value && /^\d+$/.test(value) ? Number(value) : NaN;
143
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
144
+ console.error("Invalid port number");
145
+ process.exit(1);
146
+ }
147
+ return port;
148
+ }
149
+
150
+ async function waitForProxy(timeoutMs = 8_000): Promise<LiveProxy | null> {
151
+ const deadline = Date.now() + timeoutMs;
152
+ while (Date.now() < deadline) {
153
+ // Runtime-state-first with identity: finds the proxy even when it started on a
154
+ // fallback port, and never mistakes a foreign 200 for our proxy.
155
+ const live = await findLiveProxy();
156
+ if (live) return live;
157
+ await new Promise(resolve => setTimeout(resolve, 150));
158
+ }
159
+ return null;
160
+ }
161
+
162
+ /**
163
+ * A Grok fence sync that throws is best-effort by design — it must never block startup.
164
+ * Reporting nothing, however, is what lets a STALE fence survive: `~/.grok/config.toml`
165
+ * keeps naming whatever port the last successful sync wrote, and once that listener is
166
+ * gone every grok turn retries against a refused connection while our own log stays
167
+ * silent (2026-07-27 field report: 8 entries pinned to a dead 127.0.0.1:4179).
168
+ * So say what failed and name the single command that repairs it.
169
+ */
170
+ function grokSyncFailureMessage(err: unknown): string {
171
+ const detail = err instanceof Error ? err.message : String(err);
172
+ return `Grok Build config sync failed: ${detail}. `
173
+ + "~/.grok/config.toml may still point at a previous proxy port — "
174
+ + "run 'rmx ensure' (or apply from the dashboard's Grok page) to repoint it.";
175
+ }
176
+
177
+ /** Argv for detached `start`, optionally hard-pinning the listen port. */
178
+ function startArgv(port?: number): string[] {
179
+ const args = [process.argv[1], "start"];
180
+ if (typeof port === "number" && Number.isFinite(port) && port > 0 && port <= 65535) {
181
+ args.push("--port", String(Math.trunc(port)));
182
+ }
183
+ return args;
184
+ }
185
+
186
+ async function chooseListenPort(requestedPort?: number): Promise<number> {
187
+ const config = loadConfig();
188
+ const preferred = requestedPort ?? config.port ?? 10100;
189
+ const hardPin = requestedPort !== undefined && requestedPort > 0;
190
+ const reservedLoopbackPort = config.unauthenticatedLoopbackListener?.enabled
191
+ ? config.unauthenticatedLoopbackListener.port
192
+ : undefined;
193
+ // Before the reclaim path, not after (#1102). Asking for the port the loopback listener is
194
+ // configured to bind is a configuration mistake, and reclaim would spend up to 60 seconds
195
+ // waiting for a socket to free before reporting "port is busy" — the wrong diagnosis for a
196
+ // collision the config can state outright.
197
+ if (reservedLoopbackPort !== undefined && preferred === reservedLoopbackPort) {
198
+ throw new Error(
199
+ `Port ${preferred} is reserved for unauthenticatedLoopbackListener; choose a different proxy port.`,
200
+ );
201
+ }
202
+ // Soft start: brief prefer-retry then ephemeral hop.
203
+ // Explicit `--port` (service wrappers / update restart): wait for the pinned port
204
+ // to free without killing any listener (healthy rmx / foreign). Never hop.
205
+ if (hardPin && preferred > 0) {
206
+ const { reclaimListenPort } = await import("../server/port-reclaim");
207
+ await reclaimListenPort(preferred, config.hostname ?? "127.0.0.1", {
208
+ // Ghost LISTEN rows with a dead PID can outlive the process for a while.
209
+ // SetTcpEntry(DELETE_TCB) needs elevation (often returns 317), so the only
210
+ // reliable non-admin recovery is to wait for the OS to release the TCB.
211
+ timeoutMs: 60_000,
212
+ intervalMs: 100,
213
+ scanIntervalMs: 500,
214
+ killOcxHolders: false,
215
+ dropTcpRows: true,
216
+ });
217
+ }
218
+ try {
219
+ const selected = await findAvailablePort(preferred, config.hostname ?? "127.0.0.1", {
220
+ // After reclaim, keep probing briefly — ghost rows sometimes clear between
221
+ // the reclaim deadline and the final listen. Still never hop off `--port`.
222
+ preferRetryMs: hardPin ? 5_000 : 750,
223
+ preferRetryIntervalMs: 50,
224
+ allowEphemeralFallback: !hardPin,
225
+ // Never hand the public listener the port the loopback listener is configured to
226
+ // bind (#1102). Without this, `--port <loopback port>` binds the public listener
227
+ // first and the loopback bind then fails, rolling back a startup that was only
228
+ // ever a config collision.
229
+ ...(reservedLoopbackPort !== undefined ? { reservedPort: reservedLoopbackPort } : {}),
230
+ });
231
+ if (preferred > 0 && selected !== preferred) {
232
+ console.log(`⚠️ Port ${preferred} is busy; starting Remodex on ${selected}.`);
233
+ }
234
+ if (shouldPersistSelectedPort(config.port, selected, preferred)) {
235
+ config.port = selected;
236
+ saveConfig(config);
237
+ }
238
+ return selected;
239
+ } catch (err) {
240
+ if (err instanceof PortUnavailableError) {
241
+ console.error(`❌ ${err.message}`);
242
+ console.error(" Stop whatever holds that port, or change config.port, then retry.");
243
+ process.exit(1);
244
+ }
245
+ throw err;
246
+ }
247
+ }
248
+
249
+ async function handleStart(options: { block?: boolean } = {}) {
250
+ // Native (WinSW) service mode has no batch wrapper to read the service token file
251
+ // into the environment, so the app loads it here before the server binds. The server
252
+ // auth path reads OPENCODEX_API_AUTH_TOKEN from the environment.
253
+ const serviceToken = loadServiceTokenFromFile(process.env);
254
+ if (serviceToken) process.env.OPENCODEX_API_AUTH_TOKEN = serviceToken;
255
+ const desktopBootstrap = bootstrapDesktopConfigIfNeeded();
256
+ if (desktopBootstrap.ran && desktopBootstrap.result.status === "invalid") {
257
+ console.error(
258
+ "⚠️ Remodex config.json is malformed; desktop first-launch convergence will preserve it and refuse Codex writes until it is repaired.",
259
+ );
260
+ }
261
+ const requestedPort = parsePortOption();
262
+ if (!currentExternalCodexModelProvider()) reconcileJournal();
263
+ const existingPid = readPid();
264
+ if (existingPid) {
265
+ const live = await findLiveProxy();
266
+ if (live) {
267
+ console.error(`⚠️ Proxy already running (PID ${live.pid ?? existingPid}, port ${live.port}). Use 'rmx stop' first.`);
268
+ process.exit(1);
269
+ }
270
+ removePid(existingPid);
271
+ }
272
+
273
+ // Interactive-only update prompt. Must run BEFORE we bind a port / write a
274
+ // PID: choosing "Update now" installs globally and exits, so we never want a
275
+ // live daemon holding resources while it overwrites its own binary.
276
+ await maybeShowUpdatePrompt();
277
+
278
+ // Port selection is check-then-bind: a concurrent `rmx start`/`ensure` can win the port
279
+ // between the probe and Bun.serve. Soft starts may re-pick; hard-pinned `--port` retries
280
+ // the same port only (never hop — that was the remaining PR #152 gap).
281
+ let port = await chooseListenPort(requestedPort);
282
+ // One private readiness gate for this startServer invocation, captured by the
283
+ // listener's closure. handleStart owns it and transitions it after the
284
+ // post-startup sync settles. A second startServer in the same process would
285
+ // get its own gate and could never reset/mutate this one.
286
+ const readinessGate = createReadinessGate();
287
+ let server: ReturnType<typeof startServer>;
288
+ const localAttestationSecret = createLocalAttestationSecret();
289
+ for (let attempt = 0; ; attempt++) {
290
+ try {
291
+ server = startServer(port, { localAttestationSecret, readinessGate });
292
+ // Prewarm the live provider model cache as soon as the port is bound so the
293
+ // first GUI /v1/models (and syncModelsToCodex below) share one discovery flight
294
+ // instead of racing duplicate upstream /models fetches.
295
+ scheduleCatalogPrewarm();
296
+ break;
297
+ } catch (err) {
298
+ if (!isAddrInUse(err) || attempt >= 2) throw err;
299
+ if (requestedPort !== undefined) {
300
+ console.log(`⚠️ Port ${port} was taken while starting; waiting to retry the same port...`);
301
+ const hostname = loadConfig().hostname ?? "127.0.0.1";
302
+ const freed = await waitForPortAvailable(port, hostname, { timeoutMs: 3_000, intervalMs: 50 });
303
+ if (!freed) {
304
+ console.error(`❌ Port ${port} stayed busy; refusing to hop to an ephemeral port.`);
305
+ process.exit(1);
306
+ }
307
+ continue;
308
+ }
309
+ console.log(`⚠️ Port ${port} was taken while starting; picking another...`);
310
+ port = await chooseListenPort(requestedPort);
311
+ }
312
+ }
313
+ // A single request's streaming error must never crash the daemon serving every
314
+ // other Codex session — capture the full stack to crash.log and stay up.
315
+ installCrashGuards();
316
+ writePid(process.pid);
317
+
318
+ const config = loadConfig();
319
+ writeRuntimePort({ pid: process.pid, port, hostname: config.hostname, attestationSecret: localAttestationSecret });
320
+ // No pre-emptive snapshot here. `injectCodexConfig` journals the exact bytes it
321
+ // is about to transform; snapshotting earlier only captured a baseline that could
322
+ // already be stale by the time injection ran (#477).
323
+
324
+ // Background proactive token refresh. No-op unless config.tokenGuardian.enabled; timer is unref'd
325
+ // so it never keeps the process alive on its own. Stopped in syncCleanup so no refresh fires mid-drain.
326
+ const guardian = startTokenGuardian();
327
+ // Design B upgrade path: keep retrying the one-time opencodex→openai history migration in the
328
+ // background — the first `rmx start` after an update usually races the Codex app's DB lock.
329
+ // Loopback-only (legacy mode still forward-tags) and respects syncResumeHistory opt-out.
330
+ let historyGuardian: ReturnType<typeof startHistoryMigrationGuardian> | undefined;
331
+
332
+ let cleaned = false;
333
+ let cleanupSucceeded = true;
334
+ const syncCleanup = () => {
335
+ if (cleaned) return cleanupSucceeded;
336
+ cleaned = true;
337
+ try { guardian.stop(); } catch { /* best-effort */ }
338
+ try { historyGuardian?.stop(); } catch { /* best-effort */ }
339
+ // Dashboard drain-and-restart (#563) must not tear down injection: the replacement
340
+ // process expects Codex/Grok/env fences to still be in place.
341
+ const recycling = isRecyclingForExit();
342
+ if (!recycling) {
343
+ try { revertSystemEnv(); } catch { /* best-effort */ }
344
+ }
345
+ removePid(process.pid);
346
+ removeRuntimePort(process.pid);
347
+ const preserveRouting = process.env.OCX_SERVICE === "1";
348
+ if (!recycling && !preserveRouting && !currentExternalCodexModelProvider()) {
349
+ try {
350
+ const restored = restoreNativeCodex();
351
+ if (!restored.success) {
352
+ cleanupSucceeded = false;
353
+ console.error(`⚠️ Native Codex restore failed during shutdown: ${restored.message}`);
354
+ }
355
+ } catch (error) {
356
+ cleanupSucceeded = false;
357
+ console.error(`⚠️ Native Codex restore failed during shutdown: ${error instanceof Error ? error.message : String(error)}`);
358
+ }
359
+ }
360
+ // Same ownership rule as `rmx stop`: if the installed service belongs to another home, the
361
+ // Grok fence is shared state we must not remove — that service keeps running and would be
362
+ // left pointing nowhere. This guard also covers signal-driven exits, which is the path that
363
+ // would otherwise bypass handleStop's gate entirely.
364
+ if (!recycling && !preserveRouting && serviceEnvironmentOwnedHere()) {
365
+ try { stripGrokConfig(); } catch { /* best-effort restore */ }
366
+ }
367
+ return cleanupSucceeded;
368
+ };
369
+
370
+ let shuttingDown = false;
371
+ let shutdownStartedAt = 0;
372
+ // Terminal Ctrl-C delivers SIGINT to the whole foreground group AND the launcher
373
+ // forwards its own — two signals land within milliseconds. Treat a duplicate inside
374
+ // this window as the same Ctrl-C (one graceful drain); a deliberate later press
375
+ // escalates to an immediate force-exit ("gradual kill").
376
+ const FORCE_AFTER_MS = 500;
377
+ const shutdown = () => {
378
+ const now = Date.now();
379
+ if (shuttingDown) {
380
+ if (now - shutdownStartedAt < FORCE_AFTER_MS) return; // near-simultaneous duplicate — ignore
381
+ console.log("\n⏹ Force shutdown (second signal).");
382
+ try { syncCleanup(); } catch { /* best-effort */ }
383
+ process.exit(130);
384
+ }
385
+ shuttingDown = true;
386
+ shutdownStartedAt = now;
387
+ console.log("\n🛑 Shutting down Remodex proxy...");
388
+ void (async () => {
389
+ try {
390
+ await drainAndShutdown(server, config.shutdownTimeoutMs ?? 5000);
391
+ } finally {
392
+ const restored = syncCleanup(); // idempotent (cleaned-guard); also re-run by process.on("exit")
393
+ process.exit(restored ? 0 : 1);
394
+ }
395
+ })();
396
+ };
397
+
398
+ process.on("SIGINT", shutdown);
399
+ process.on("SIGTERM", shutdown);
400
+ // The launcher (bin/ocx.mjs) forwards SIGHUP too (e.g. terminal close); handle it
401
+ // gracefully here so it drains + cleans up instead of a default immediate kill.
402
+ process.on("SIGHUP", shutdown);
403
+ process.on("exit", syncCleanup);
404
+
405
+ // System-wide env injection AFTER signal handlers are registered (crash safety:
406
+ // syncCleanup reverts even if injection itself or subsequent startup steps fail).
407
+ await injectSystemEnv(port, config).catch(() => {});
408
+ // Auto-install .zshrc hook (idempotent — skips if already present).
409
+ installShellHook();
410
+
411
+ await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
412
+ // Post-startup sync drives the readiness gate AND the #1046 stale app-server
413
+ // warning. `syncCodexOnStartIfEnabled` respects the Codex integration toggle
414
+ // (OFF → no sync) and reports whether anything was written; the readiness gate
415
+ // observes the real sync outcome (ok/warning) so /readyz never advertises a
416
+ // half-synced proxy as ready while /healthz stays live.
417
+ const startupSync = await syncCodexOnStartIfEnabled(port, config, undefined, readinessGate);
418
+ if (!startupSync.ran) console.log(" Codex integration OFF; startup left Codex native.");
419
+ // #1046: one warning per startup, after BOTH writes. The server's cache
420
+ // invalidation happens first and the catalog sync second, so the mtime is only
421
+ // final here — and neither write site warns on its own, or a boot that hits
422
+ // both would warn twice.
423
+ const { consumeStartupCacheInvalidationWrite } = await import("../server");
424
+ if (consumeStartupCacheInvalidationWrite() || startupSync.catalogWritten || startupSync.cacheSynced) {
425
+ const { warnIfStaleCodexAppServersAfterStartupWrite } = await import("../codex/app-server-processes");
426
+ warnIfStaleCodexAppServersAfterStartupWrite({ log: console });
427
+ }
428
+ if (!currentExternalCodexModelProvider() && !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false) {
429
+ historyGuardian = startHistoryMigrationGuardian();
430
+ }
431
+ // Build Desktop 3P alias registry so inbound claude-opus-4-8-{code} aliases (and legacy claude-opus-4-{code}) decode correctly.
432
+ try {
433
+ const { fetchAllModels } = await import("../server/management-api");
434
+ const { visibleNativeSlugs, filterCatalogVisibleModels } = await import("../codex/catalog");
435
+ const models = filterCatalogVisibleModels(await fetchAllModels(config), config);
436
+ buildDesktop3pRegistry(
437
+ [...visibleNativeSlugs(config)],
438
+ models.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
439
+ config.claudeCode?.desktopProfile,
440
+ );
441
+ } catch { /* best-effort — registry rebuilds on first /v1/models call */ }
442
+ // Grok Build auto-registration: additive fenced block in ~/.grok/config.toml so an installed
443
+ // grok CLI can pick opencodex-routed models without manual config. No-op when ~/.grok is
444
+ // absent or the bind is non-loopback; removed again by stop/eject/uninstall/shutdown.
445
+ // Deliberately a SIBLING of the Desktop-3P block above: nesting it there meant a catalog
446
+ // failure skipped the fence entirely, even though syncGrokConfig handles that case itself.
447
+ //
448
+ // Gated on the persisted switch: without this, turning Grok off lasted exactly
449
+ // one restart, because the toggle removed the fence and start wrote it back.
450
+ if (shouldSyncGrokOnStart(config)) try {
451
+ const { syncGrokConfig } = await import("../grok/sync");
452
+ const r = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
453
+ if (r.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
454
+ else if (!r.ok) console.error(`⚠️ ${r.message}`);
455
+ } catch (err) {
456
+ // Best-effort: grok integration must never block startup. But swallowing the error
457
+ // silently is how a stale fence survives unnoticed — ~/.grok/config.toml keeps
458
+ // pointing at whatever port the LAST successful sync wrote, and if that listener is
459
+ // gone every grok turn retries against a refused connection with nothing in our log
460
+ // to explain it. Name the failure and the one command that repairs it.
461
+ console.error(`⚠️ ${grokSyncFailureMessage(err)}`);
462
+ }
463
+ if (options.block ?? true) {
464
+ setInterval(() => {}, 60_000);
465
+ await new Promise<void>(() => {});
466
+ }
467
+ }
468
+
469
+ function detachedStartEnvironment(): NodeJS.ProcessEnv {
470
+ const env: NodeJS.ProcessEnv = { ...process.env };
471
+ // Only a real service wrapper may claim supervision. A detached ensure/tray child
472
+ // is an ordinary owner: while live it maintains routing, and on exit it restores it.
473
+ delete env.OCX_SERVICE;
474
+ return withProcessRuntimeProvenance(env);
475
+ }
476
+
477
+ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Promise<boolean> {
478
+ if (!currentExternalCodexModelProvider()) reconcileJournal();
479
+ const config = loadConfig();
480
+ if (!codexAutoStartEnabled(config)) {
481
+ console.log("Codex autostart is disabled.");
482
+ return false;
483
+ }
484
+ const live = await findLiveProxy();
485
+ if (live) {
486
+ if (options.existingIsSuccess === false) {
487
+ console.error("Proxy appeared while restart was confirming absence; no start was attempted.");
488
+ return false;
489
+ }
490
+ const synced = await syncModelsToCodex(live.port).catch(e => {
491
+ console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
492
+ return null;
493
+ });
494
+ if (synced?.status === "skipped") console.log(" Codex integration OFF; startup left Codex native.");
495
+ // Ensure env file exists for already-running proxy (may have been deleted or pre-dates this feature).
496
+ await injectSystemEnv(live.port, config).catch(() => {});
497
+ // Refresh the Grok Build fence too (same contract as start). live.hostname is the
498
+ // hostname the running proxy actually bound — config.hostname may have drifted.
499
+ try {
500
+ const { syncGrokConfig } = await import("../grok/sync");
501
+ const g = await syncGrokConfig(live.port, config, live.hostname ? { hostname: live.hostname } : {});
502
+ if (g.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
503
+ else if (!g.ok) console.error(`⚠️ ${g.message}`);
504
+ } catch (err) { console.error(`⚠️ ${grokSyncFailureMessage(err)}`); }
505
+ console.log(`✅ Proxy running on port ${live.port}`);
506
+ return true;
507
+ }
508
+
509
+ const pinPort = config.port ?? 10100;
510
+ const child = spawn(process.execPath, startArgv(pinPort > 0 ? pinPort : undefined), {
511
+ detached: true,
512
+ stdio: "ignore",
513
+ windowsHide: true,
514
+ env: detachedStartEnvironment(),
515
+ });
516
+ child.unref();
517
+
518
+ const port = (await waitForProxy())?.port;
519
+ if (!port) {
520
+ console.error("❌ Proxy did not become healthy after starting.");
521
+ process.exitCode = 1;
522
+ return false;
523
+ }
524
+ // Deterministic fence guarantee: the spawned child injects late in its own startup, but
525
+ // this parent returns as soon as /healthz responds — inject here too (idempotent block
526
+ // replace) so `rmx ensure` never returns without the Grok fence in place.
527
+ try {
528
+ const { syncGrokConfig } = await import("../grok/sync");
529
+ const g = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
530
+ if (g.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
531
+ else if (!g.ok) console.error(`⚠️ ${g.message}`);
532
+ } catch (err) { console.error(`⚠️ ${grokSyncFailureMessage(err)}`); }
533
+ // Always sync the LIVE port: after a fallback-port start, config.port still names the
534
+ // busy preferred port — syncing that would point Codex at a dead listener.
535
+ const synced = await syncModelsToCodex(port).catch(e => {
536
+ console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`);
537
+ return null;
538
+ });
539
+ if (synced?.status === "skipped") console.log(" Codex integration OFF; startup left Codex native.");
540
+ console.log(`✅ Proxy running on port ${port}`);
541
+ return true;
542
+ }
543
+
544
+ /** Fixed tray action: start the proxy without depending on codexAutoStart. */
545
+ async function handleTrayProxyStart(existingIsSuccess = true): Promise<boolean> {
546
+ const ok = await runTrayProxyStart({
547
+ findLive: findLiveProxy,
548
+ existingIsSuccess,
549
+ diagnoseService: () => {
550
+ const service = diagnoseService();
551
+ return { installed: service.installed, startable: serviceStartableFromTray(service), summary: service.summary };
552
+ },
553
+ startService: () => serviceCommand("start"),
554
+ startDirect: () => {
555
+ const config = loadConfig();
556
+ const port = (config.port ?? 10100) > 0 ? (config.port ?? 10100) : 10100;
557
+ const child = spawn(process.execPath, startArgv(port), {
558
+ detached: true,
559
+ stdio: "ignore",
560
+ windowsHide: true,
561
+ env: detachedStartEnvironment(),
562
+ });
563
+ child.unref();
564
+ },
565
+ // serviceCommand("start") already spends up to 20s confirming the supervised
566
+ // child. Slow Windows hosts can still be publishing native-main state after that
567
+ // first window, so keep one shared follow-up budget instead of returning a false
568
+ // failure while Task Scheduler is still starting the approved child.
569
+ waitForProxy: () => waitForProxy(40_000),
570
+ // The service child performs its own startup sync before /readyz settles. A
571
+ // tray start can observe health slightly earlier, so wait for that private
572
+ // readiness state and run one serialized convergence pass here as well. This
573
+ // makes Stop → Start deterministic and prevents a stale native-only
574
+ // models.json from winning a startup race.
575
+ converge: async live => {
576
+ await runReady(
577
+ { json: true, wait: true, timeoutSeconds: 45 },
578
+ { stdout: { log: () => {} } },
579
+ );
580
+ const synced = await syncModelsToCodex(live.port).catch(error => {
581
+ console.error(`⚠️ Model sync skipped: ${error instanceof Error ? error.message : String(error)}`);
582
+ return null;
583
+ });
584
+ return synced !== null && synced.ok && synced.status !== "refused";
585
+ },
586
+ info: message => console.log(message),
587
+ error: message => console.error(message),
588
+ });
589
+ // serviceCommand("start") can set exitCode=1 after its own 20s probe, while
590
+ // the coordinator's bounded follow-up observes the same service become live.
591
+ // The final observed state, not the earlier probe, owns this command result.
592
+ process.exitCode = ok ? 0 : 1;
593
+ return ok;
594
+ }
595
+
596
+ const PROXY_RESTART_OBSERVE_MS = MEMORY_DRAIN_RESTART_MS + REPLACEMENT_READY_TIMEOUT_MS + 15_000;
597
+
598
+ async function waitForProxyReplacement(
599
+ previous: ProxyRestartLive,
600
+ deadlineAt: number,
601
+ ): Promise<ProxyRestartLive | null> {
602
+ while (Date.now() < deadlineAt) {
603
+ const live = await findLiveProxy({ deadlineAt });
604
+ if (Date.now() >= deadlineAt) return null;
605
+ // Modern /healthz publishes a PID. Require a different, identity-verified process;
606
+ // merely seeing the old port online again is not proof that restart completed.
607
+ if (isProxyReplacement(previous, live)) {
608
+ return live;
609
+ }
610
+ const remainingMs = deadlineAt - Date.now();
611
+ if (remainingMs > 0) await Bun.sleep(Math.min(250, remainingMs));
612
+ }
613
+ return null;
614
+ }
615
+
616
+ function reportRestartFailure(result: Extract<ProxyRestartResult, { ok: false }>): void {
617
+ if (result.phase === "identity") {
618
+ console.error("❌ Refusing to restart because the running proxy identity could not be attested.");
619
+ } else if (result.phase === "request") {
620
+ const code = result.error instanceof Error ? result.error.message : "";
621
+ if (code === "restart_capability_unsupported") {
622
+ console.error("❌ The running proxy predates process-bound restart support; no unsafe fallback was attempted.");
623
+ console.error(" After confirming this home owns the proxy, run `rmx stop` and then `rmx start` once.");
624
+ } else {
625
+ console.error("❌ Proxy restart request could not be confirmed; no fallback stop/start was attempted.");
626
+ }
627
+ } else if (result.phase === "replacement") {
628
+ console.error("❌ Proxy restart was accepted, but no identity-verified replacement became healthy in time.");
629
+ } else {
630
+ console.error("❌ Proxy was not running and the fallback start did not become healthy.");
631
+ }
632
+ }
633
+
634
+ async function handleProxyRestart(
635
+ startWhenStopped: () => Promise<boolean | "skipped">,
636
+ ): Promise<boolean> {
637
+ const deadlineAt = Date.now() + PROXY_RESTART_OBSERVE_MS;
638
+ const result = await runProxyRestart({
639
+ findLive: () => discoverStableProxyForRestart({
640
+ findLive: () => findLiveProxy({ deadlineAt, attempts: 2 }),
641
+ expired: () => Date.now() >= deadlineAt,
642
+ }),
643
+ startWhenStopped,
644
+ requestInPlaceRestart: previous => requestBoundSystemRestart(previous, deadlineAt),
645
+ waitForReplacement: previous => waitForProxyReplacement(previous, deadlineAt),
646
+ });
647
+ if (!result.ok) reportRestartFailure(result);
648
+ process.exitCode = result.ok ? 0 : 1;
649
+ return result.ok;
650
+ }
651
+
652
+ async function handleTrayProxyRestart(): Promise<void> {
653
+ await handleProxyRestart(() => handleTrayProxyStart(false));
654
+ }
655
+
656
+ /**
657
+ * Fixed desktop-shell action used by the tray. A first click checks and
658
+ * downloads a verified artifact; once the tray shows "ready", the next click
659
+ * hands it to the platform installer. No caller-selected command or path is
660
+ * accepted here.
661
+ */
662
+ async function handleDesktopUpdateRequest(requestArgs: string[] = []): Promise<void> {
663
+ let expectedReady: { id: string; channel: DesktopReleaseChannel } | undefined;
664
+ if (requestArgs.length > 0) {
665
+ const [mode, id, channel] = requestArgs;
666
+ if (
667
+ requestArgs.length !== 3
668
+ || mode !== "--install-ready"
669
+ || !id
670
+ || !/^desktop-\d+-[a-z0-9]{1,32}$/.test(id)
671
+ || (channel !== "latest" && channel !== "preview")
672
+ ) {
673
+ console.error("Invalid desktop update request.");
674
+ process.exitCode = 64;
675
+ return;
676
+ }
677
+ expectedReady = { id, channel };
678
+ }
679
+ const previous = readDesktopUpdateState();
680
+ const install = expectedReady !== undefined || desktopUpdateInstallAction(previous);
681
+ const channel: DesktopReleaseChannel = expectedReady?.channel
682
+ ?? previous?.channel
683
+ ?? (currentVersion().includes("-") ? "preview" : "latest");
684
+ try {
685
+ const job = startDesktopUpdateJob(channel, {
686
+ install,
687
+ ...(expectedReady ? { expectedReady } : {}),
688
+ });
689
+ console.log(
690
+ install
691
+ ? `Desktop update installation started (${job.latestVersion ?? "latest"}).`
692
+ : "Desktop update check started.",
693
+ );
694
+ } catch (error) {
695
+ console.error(error instanceof Error ? error.message : "Desktop update could not start.");
696
+ process.exitCode = 1;
697
+ }
698
+ }
699
+
700
+ async function handleAutomaticDesktopUpdateCheck(): Promise<void> {
701
+ const channel: DesktopReleaseChannel = currentVersion().includes("-") ? "preview" : "latest";
702
+ const result = await runAutomaticDesktopUpdateCheck(channel);
703
+ console.log(JSON.stringify(result));
704
+ }
705
+
706
+ async function handleRestartStartWhenStopped(): Promise<boolean | "skipped"> {
707
+ if (!codexAutoStartEnabled(loadConfig())) {
708
+ console.log("Codex autostart is disabled; no proxy was started.");
709
+ return "skipped";
710
+ }
711
+ return handleEnsure({ existingIsSuccess: false });
712
+ }
713
+
714
+ async function restoreSharedClientStateAfterStop(): Promise<boolean> {
715
+ let restored = true;
716
+ try {
717
+ const result = await restoreNativeCodexAsync();
718
+ if (result.success) console.log(`↩️ ${result.message}`);
719
+ else {
720
+ restored = false;
721
+ console.error(`⚠️ ${result.message}`);
722
+ }
723
+ } catch (error) {
724
+ restored = false;
725
+ console.error(`⚠️ Native Codex restore failed: ${error instanceof Error ? error.message : String(error)}`);
726
+ }
727
+
728
+ // A refused or thrown Grok strip is actionable because it would point Grok at a dead proxy.
729
+ try {
730
+ const grok = stripGrokConfig();
731
+ if (grok.changed) console.log(`↩️ ${grok.message}`);
732
+ else if (!grok.ok) { restored = false; console.error(`⚠️ ${grok.message}`); }
733
+ } catch (error) {
734
+ restored = false;
735
+ console.error(`⚠️ Grok config restore failed: ${error instanceof Error ? error.message : String(error)}`);
736
+ }
737
+ return restored;
738
+ }
739
+
740
+ async function handleStop() {
741
+ let stopFailed = false;
742
+ let stoppedService = false;
743
+ // An ownership mismatch means the service manager was never even contacted: the installed
744
+ // service is still live and will respawn the proxy. Tearing down SHARED state in that
745
+ // situation (native Codex config, the Grok fence) removes config out from under a running
746
+ // service — the exact failure this flag prevents.
747
+ let ownershipBlocked = false;
748
+ try {
749
+ assertServiceEnvironmentMatchesInstall();
750
+ } catch (err) {
751
+ ownershipBlocked = true;
752
+ stopFailed = true;
753
+ console.error(`❌ ${err instanceof Error ? err.message : String(err)}`);
754
+ console.error(" Proxy left running; no Remodex-owned client configuration was changed.");
755
+ }
756
+
757
+ // Restore routing before stopping either the supervisor or the live process. If
758
+ // restoration is refused, Codex must keep pointing at a live local endpoint.
759
+ if (!ownershipBlocked && !await restoreSharedClientStateAfterStop()) {
760
+ stopFailed = true;
761
+ console.error("❌ Proxy left running because Remodex-owned client configuration could not be restored safely.");
762
+ }
763
+ if (stopFailed) {
764
+ process.exitCode = 1;
765
+ return false;
766
+ }
767
+
768
+ const serviceWasInstalled = diagnoseService().installed;
769
+ try {
770
+ stoppedService = stopServiceIfInstalled();
771
+ if (stoppedService) console.log("🛑 Service manager stopped (won't respawn).");
772
+ if (serviceWasInstalled && !stoppedService) {
773
+ stopFailed = true;
774
+ console.error("❌ Background service could not be stopped; proxy left running.");
775
+ }
776
+ } catch (err) {
777
+ if (isServiceOwnershipError(err)) {
778
+ ownershipBlocked = true;
779
+ stopFailed = true;
780
+ console.error(`❌ ${err.message}`);
781
+ console.error(" Skipping shared teardown (native Codex restore, Grok config): the installed service is still running.");
782
+ } else {
783
+ stopFailed = true;
784
+ console.error(`⚠️ Service manager stop failed: ${err instanceof Error ? err.message : String(err)}`);
785
+ }
786
+ }
787
+
788
+ if (stopFailed) {
789
+ process.exitCode = 1;
790
+ return false;
791
+ }
792
+
793
+ const pid = readPid();
794
+ if (pid) {
795
+ try {
796
+ // Graceful-first (management-API drain) — on Windows this is the only path where
797
+ // the proxy's shutdown handlers actually run; taskkill /F is the fallback inside.
798
+ await stopProxy(pid);
799
+ console.log(`✅ Proxy (PID ${pid}) stopped.`);
800
+ removePid(pid);
801
+ removeRuntimePort(pid);
802
+ } catch (err) {
803
+ stopFailed = true;
804
+ console.error(`❌ Failed to stop proxy (PID ${pid}).`);
805
+ // stopProxy throws with the reason — an ownership refusal (409) carries the
806
+ // remediation ("run the stop from that home"). Swallowing it leaves the operator
807
+ // with a bare failure and a manual `kill` as the obvious next move, which is the
808
+ // exact teardown the refusal exists to prevent.
809
+ const detail = err instanceof Error ? err.message : String(err);
810
+ if (detail) console.error(` ${detail}`);
811
+ if (err instanceof ProxyOwnershipRefusedError) {
812
+ ownershipBlocked = true;
813
+ console.error(" Skipping shared teardown (native Codex restore, Grok config): the foreign proxy is still running.");
814
+ }
815
+ }
816
+ } else {
817
+ // Snapshot the stale on-disk state BEFORE the async probe: a concurrent `rmx start`
818
+ // can write fresh records mid-probe, and the purge below must never delete those.
819
+ const stalePidValue = readPidFileValue();
820
+ const staleRuntimePid = readRuntimePort()?.pid ?? null;
821
+ // Orphan recovery: a live proxy can outlive its pid file (crash, manual delete,
822
+ // corrupt file). Identity-checked liveness still finds it via the runtime record.
823
+ const live = await findLiveProxy();
824
+ if (live?.pid) {
825
+ try {
826
+ await stopProxy(live.pid);
827
+ console.log(`✅ Proxy (PID ${live.pid}) stopped.`);
828
+ } catch (err) {
829
+ stopFailed = true;
830
+ console.error(`❌ Failed to stop proxy (PID ${live.pid}).`);
831
+ const detail = err instanceof Error ? err.message : String(err);
832
+ if (detail) console.error(` ${detail}`);
833
+ if (err instanceof ProxyOwnershipRefusedError) {
834
+ ownershipBlocked = true;
835
+ console.error(" Skipping shared teardown (native Codex restore, Grok config): the foreign proxy is still running.");
836
+ }
837
+ }
838
+ } else if (!stoppedService) {
839
+ console.log("No running proxy found.");
840
+ }
841
+ if (!stopFailed) {
842
+ // `readPid() === null` means the snapshotted pid file was absent, invalid, dead, or
843
+ // not ours — stale by definition. Purge (guarded by the snapshot) so `rmx update`'s
844
+ // stop gate can't wedge on it.
845
+ removePidIfValueIs(stalePidValue);
846
+ removeRuntimePortIfPidIs(staleRuntimePid);
847
+ }
848
+ }
849
+ // Environment ownership is independent from service ownership. Only roll it
850
+ // back after a clean stop; a refused stop intentionally leaves the live proxy's
851
+ // client environment intact.
852
+ if (!stopFailed) {
853
+ try { revertSystemEnv(); } catch { /* best-effort */ }
854
+ }
855
+ // Set the code rather than exiting inline: `restart` and the tray coordinator call this
856
+ // function and need it to RETURN so they can decide what to do next.
857
+ if (stopFailed) process.exitCode = 1;
858
+ return !stopFailed;
859
+ }
860
+
861
+ async function handleUninstall() {
862
+ const failures: string[] = [];
863
+
864
+ const runStep = async (label: string, step: () => void | boolean | Promise<void | boolean>) => {
865
+ try {
866
+ const changed = await step();
867
+ if (changed === false) console.log(`- ${label}: not installed`);
868
+ else console.log(`✅ ${label}`);
869
+ } catch (err) {
870
+ failures.push(label);
871
+ console.error(`⚠️ ${label} failed: ${err instanceof Error ? err.message : String(err)}`);
872
+ }
873
+ };
874
+
875
+ await runStep("service stopped", () => stopServiceIfInstalled());
876
+
877
+ await runStep("proxy stopped", async () => {
878
+ const pid = readPid();
879
+ if (!pid) return false;
880
+ await stopProxy(pid);
881
+ removePid(pid);
882
+ removeRuntimePort(pid);
883
+ return true;
884
+ });
885
+
886
+ await runStep("service removed", () => uninstallServiceIfInstalled());
887
+
888
+ if (process.platform === "win32") {
889
+ await runStep("Windows tray removed", async () => {
890
+ const { getWindowsTrayStatus, uninstallWindowsTray } = await import("../tray/windows");
891
+ const tray = getWindowsTrayStatus();
892
+ if (!tray.installed && !tray.stale && !tray.running) return false;
893
+ uninstallWindowsTray();
894
+ });
895
+ }
896
+
897
+ await runStep("native Codex restored", async () => {
898
+ const r = await restoreNativeCodexAsync();
899
+ if (!r.success) throw new Error(r.message);
900
+ });
901
+
902
+ await runStep("Grok Build config restored", () => {
903
+ const r = stripGrokConfig();
904
+ if (!r.ok) throw new Error(r.message);
905
+ return r.changed;
906
+ });
907
+
908
+ await runStep("system env vars reverted", () => {
909
+ const r = revertSystemEnv();
910
+ if (!r.reverted && r.reason !== "no tracking file" && r.reason !== "not macOS") throw new Error(r.reason ?? "revert failed");
911
+ });
912
+
913
+ await runStep("shell hook removed", () => {
914
+ const r = uninstallShellHook();
915
+ if (!r.removed && r.reason !== "not installed" && r.reason !== "not macOS") throw new Error(r.reason ?? "remove failed");
916
+ });
917
+
918
+ try {
919
+ const { uninstallCodexShim } = await import("../codex/shim");
920
+ const r = uninstallCodexShim();
921
+ console.log(r.removed ? "✅ Codex autostart shim removed" : "- Codex autostart shim removed: not installed");
922
+ } catch (err) {
923
+ failures.push("Codex autostart shim removed");
924
+ console.error(`⚠️ Codex autostart shim removed failed: ${err instanceof Error ? err.message : String(err)}`);
925
+ }
926
+
927
+ if (failures.length === 0) {
928
+ await runStep("Remodex config removed", () => {
929
+ const result = removeOwnedConfigState(getConfigDir());
930
+ if (result.status === "absent") return false;
931
+ if (result.status === "removed") return true;
932
+ const residual = result.residualPaths.length > 0
933
+ ? ` Residual path(s): ${result.residualPaths.join(", ")}`
934
+ : "";
935
+ throw new Error(`${result.status} uninstall: ${result.reason ?? "config state was not removed"}.${residual}`);
936
+ });
937
+ } else {
938
+ console.error("Leaving Remodex config/backups in place so the failed restore step can be retried.");
939
+ }
940
+
941
+ if (failures.length > 0) {
942
+ console.error(`\nUninstall finished with ${failures.length} failed step(s): ${failures.join(", ")}`);
943
+ process.exit(1);
944
+ }
945
+ console.log("\n✅ Remodex local state removed. Remove the package with: npm uninstall -g @remodex/rmx");
946
+ }
947
+
948
+ async function handleStatus() {
949
+ const statusArgs = args.slice(1);
950
+ const wantsJson = statusArgs.length === 1 && statusArgs[0] === "--json";
951
+ if (statusArgs.length > 1 || (statusArgs.length === 1 && !wantsJson)) {
952
+ console.error("Usage: rmx status [--json]");
953
+ process.exit(1);
954
+ }
955
+
956
+ const status = await collectStatus();
957
+ if (wantsJson) {
958
+ console.log(JSON.stringify(status.json, null, 2));
959
+ return;
960
+ }
961
+
962
+ if (status.json.proxy.pid || status.json.proxy.health.ok) {
963
+ console.log(`✅ Proxy: ${status.proxyLabel}`);
964
+ } else {
965
+ console.log(`❌ Proxy: ${status.proxyLabel}`);
966
+ }
967
+ console.log(` Health: ${status.healthLabel}`);
968
+ if (!(status.json.proxy.pid || status.json.proxy.health.ok)) {
969
+ console.log(" ↳ Not running — Codex/Claude requests will fail with connection errors.");
970
+ // The service summary a few lines below already tells a registered-but-not-serving
971
+ // user to repair. Printing "install the persistent service" unconditionally
972
+ // contradicted it in the same report, and install re-registers: UAC on Windows and a
973
+ // possible WinSW-to-scheduler switch for someone who already has a service.
974
+ const installed = status.json.startup.serviceInstalled && !status.json.startup.serviceConflict;
975
+ console.log(installed
976
+ ? " Restart with 'rmx start', or refresh the installed service: 'rmx service repair'."
977
+ : " Restart with 'rmx start', or install the persistent service: 'rmx service install'.");
978
+ }
979
+ console.log(` Dashboard: ${status.json.dashboard.url}`);
980
+ console.log(` Config: ${status.json.paths.config}`);
981
+ console.log(` PID file: ${status.json.paths.pid}`);
982
+ console.log(` Runtime: ${status.json.paths.runtime}`);
983
+ console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}`);
984
+ console.log(` Default provider: ${status.json.defaultProvider}`);
985
+ console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`);
986
+ console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}`);
987
+ console.log(` Service: ${status.json.service.summary}`);
988
+ console.log(` ${status.json.codexShim.summary}`);
989
+ console.log(` Codex runtime: ${status.json.codexRuntime.path}`);
990
+ console.log(` Codex version: ${status.json.codexRuntime.version ?? "unknown"}`);
991
+ console.log(` Codex source: ${status.json.codexRuntime.source}`);
992
+ console.log(` Codex home: ${status.json.codexHome.effectiveCodexHome}`);
993
+ if (status.json.codexHome.warning) {
994
+ console.log(` ⚠️ ${status.json.codexHome.warning}`);
995
+ console.log(` Action: ${status.json.codexHome.action}`);
996
+ }
997
+ console.log(` Catalog clamp: ${status.json.codexRuntime.catalogClamp.active ? "active" : "inactive"}`);
998
+ if (status.json.codexRuntime.catalogClamp.removedEfforts.length > 0) {
999
+ console.log(` Removed efforts: ${status.json.codexRuntime.catalogClamp.removedEfforts.join(", ")}`);
1000
+ }
1001
+ if (status.json.codexRuntime.warning) {
1002
+ console.log(` ⚠️ ${status.json.codexRuntime.warning}`);
1003
+ }
1004
+ if (status.json.codexPlugins.applicable) {
1005
+ const icon = status.json.codexPlugins.stale ? "⚠️ " : "✅";
1006
+ console.log(` ${icon} Codex bundled plugins: ${status.json.codexPlugins.summary}`);
1007
+ if (status.json.codexPlugins.suggestedRepair) {
1008
+ console.log(` Suggested: ${status.json.codexPlugins.suggestedRepair}`);
1009
+ }
1010
+ }
1011
+ const { collectOAuthHealthEntriesForCli, oauthLoginSummary } = await import("../oauth");
1012
+ const { formatOAuthHealthForStatus } = await import("./status-oauth");
1013
+ console.log(` OAuth logins:`);
1014
+ for (const e of oauthLoginSummary()) {
1015
+ console.log(` ${e.provider.padEnd(10)} ${e.loggedIn ? `✓ logged in${e.email ? ` (${e.email})` : ""}` : "✗ not logged in"}`);
1016
+ }
1017
+ const oauthHealthBlock = formatOAuthHealthForStatus(await collectOAuthHealthEntriesForCli());
1018
+ if (oauthHealthBlock) {
1019
+ for (const line of oauthHealthBlock.split("\n")) {
1020
+ console.log(` ${line}`);
1021
+ }
1022
+ }
1023
+ }
1024
+
1025
+ async function handleRecoverHistory() {
1026
+ if (args[1] !== "--legacy-openai") {
1027
+ console.error("Usage: rmx recover-history --legacy-openai");
1028
+ console.error("Only use this if an older Remodex build already remapped OpenAI Codex App history to the legacy provider id before backup support existed.");
1029
+ process.exit(1);
1030
+ }
1031
+ // Manifest-independent legacy ejection, serialized like every other history
1032
+ // mutation. It is a separate operation from generic restore precisely because
1033
+ // it must not read, consume or replace the backup manifest.
1034
+ const outcome = await runCodexHistoryJob({
1035
+ ...resolveCodexHistoryJobTarget(),
1036
+ operation: "recover-legacy-openai",
1037
+ });
1038
+ const r = outcome.kind === "converged"
1039
+ ? { rows: outcome.rows, files: outcome.files, failed: undefined }
1040
+ : { rows: 0, files: 0, failed: true as const };
1041
+ if (r.failed) {
1042
+ console.error(
1043
+ `⚠️ Recovery SKIPPED: ${describeHistoryJobFailure(outcome, "recover-legacy")}`,
1044
+ );
1045
+ process.exit(1);
1046
+ }
1047
+ console.log(`Recovered ${r.rows} legacy thread(s) to openai (${r.files} rollout file(s) updated).`);
1048
+ }
1049
+
1050
+ /**
1051
+ * `rmx ready` — arguments are pre-parsed above (before
1052
+ * maybeAutoRestoreCodexShim) so invalid usage exits 64 before any global
1053
+ * preflight. This handler only runs the dependency-injected runner in ./ready
1054
+ * and exits with the returned code; it performs no parsing and no I/O of its
1055
+ * own. The full behavior is unit-testable without spawning a subprocess.
1056
+ */
1057
+ async function handleReady(args: ReadyArgs): Promise<never> {
1058
+ process.exit(await runReady(args));
1059
+ }
1060
+
1061
+ switch (command) {
1062
+ case "init":
1063
+ case "setup": {
1064
+ const { runInit } = await import("./init");
1065
+ await runInit();
1066
+ break;
1067
+ }
1068
+ case "start":
1069
+ await handleStart();
1070
+ break;
1071
+ case "stop": {
1072
+ // Downtime warning lives HERE, not in handleStop: `restart`/tray-restart callers
1073
+ // re-start the proxy immediately, so warning there would contradict the next line.
1074
+ if (await handleStop()) {
1075
+ console.log("⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('rmx start' or 'rmx service start').");
1076
+ }
1077
+ break;
1078
+ }
1079
+ case "restore":
1080
+ case "eject": {
1081
+ const restoreJson = args[1] === "--json";
1082
+ if (args[1] === "back") {
1083
+ // Reverse switch: re-point plain `codex` at the RUNNING proxy without touching its
1084
+ // lifecycle — the counterpart of `rmx restore`. Start/stop triggers are unchanged;
1085
+ // this only re-runs the same inject (config + catalog + history) `rmx start` does.
1086
+ const live = await findLiveProxy();
1087
+ if (!live) {
1088
+ console.error("No running proxy found. Run 'rmx start' — it injects Remodex automatically.");
1089
+ process.exit(1);
1090
+ }
1091
+ const desired = setIntegrationEnabled("codex", true);
1092
+ if (!desired.ok) {
1093
+ process.exitCode = desired.reason === "conflict" ? 2 : 1;
1094
+ console.error(`Codex desired state was not saved (${desired.reason}).`);
1095
+ break;
1096
+ }
1097
+ const synced = await syncModelsToCodex(live.port);
1098
+ if (synced.status === "skipped") {
1099
+ process.exitCode = 2;
1100
+ console.error("Codex integration is OFF; restore back did not change Codex. Retry after the competing integration change finishes.");
1101
+ break;
1102
+ }
1103
+ if (!synced.ok) {
1104
+ process.exitCode = 1;
1105
+ console.error("Plain `codex` was not switched back to Remodex. Fix the reported Codex config issue and retry.");
1106
+ break;
1107
+ }
1108
+ const target = collectOrcaCodexHomeDiagnostic();
1109
+ console.log(`Plain \`codex\` now routes through Remodex in ${target.effectiveCodexHome} (undo with: rmx restore).`);
1110
+ break;
1111
+ }
1112
+ const desired = setIntegrationEnabled("codex", false);
1113
+ if (!desired.ok) {
1114
+ process.exitCode = desired.reason === "conflict" ? 2 : 1;
1115
+ if (restoreJson) {
1116
+ // Machine-readable contract: every restore --json outcome emits one
1117
+ // schema-complete envelope on stdout, including pre-machinery failures.
1118
+ const { skippedRestoreEnvelope } = await import("../codex/inject");
1119
+ console.log(JSON.stringify(skippedRestoreEnvelope(false, `Codex desired state was not saved (${desired.reason}).`)));
1120
+ } else {
1121
+ console.error(`Codex desired state was not saved (${desired.reason}).`);
1122
+ }
1123
+ break;
1124
+ }
1125
+ // A repeated OFF on an already-clean home is a policy no-op. Do not enter
1126
+ // restore's native-profile machinery merely to prove there is nothing to
1127
+ // restore: those locks live in CODEX_HOME and a skip must create nothing.
1128
+ if (desired.status === "unchanged") {
1129
+ const { classifyNativeRoutedResidue } = await import("../codex/native-residue");
1130
+ if (classifyNativeRoutedResidue().kind === "clean") {
1131
+ const alreadyOff = "Codex integration is already OFF and native; no Codex files changed.";
1132
+ if (restoreJson) {
1133
+ const { skippedRestoreEnvelope } = await import("../codex/inject");
1134
+ console.log(JSON.stringify(skippedRestoreEnvelope(true, alreadyOff)));
1135
+ } else {
1136
+ console.log(alreadyOff);
1137
+ }
1138
+ break;
1139
+ }
1140
+ }
1141
+ let r: { success: boolean; message: string };
1142
+ try {
1143
+ r = await restoreNativeCodexAsync({ revalidateDesiredState: true });
1144
+ } catch (err) {
1145
+ r = { success: false, message: err instanceof Error ? err.message : String(err) };
1146
+ }
1147
+ if (restoreJson) {
1148
+ // Spawned callers need the artifact-level result to distinguish a busy
1149
+ // history worker from a successful native restore. Keep stdout machine
1150
+ // readable; human framing remains the default command contract.
1151
+ console.log(JSON.stringify(r));
1152
+ if (!r.success) process.exitCode = 1;
1153
+ break;
1154
+ }
1155
+ if (r.success) console.log(`✅ ${r.message}`);
1156
+ else {
1157
+ console.error(`⚠️ ${r.message}`);
1158
+ process.exitCode = 1;
1159
+ }
1160
+ try {
1161
+ const g = stripGrokConfig();
1162
+ if (g.changed) console.log(`✅ ${g.message}`);
1163
+ else if (!g.ok) {
1164
+ console.error(`⚠️ ${g.message}`);
1165
+ process.exitCode = 1;
1166
+ }
1167
+ } catch { /* best-effort */ }
1168
+ if (r.success) {
1169
+ console.log("Codex integration is OFF and plain `codex` now runs natively. Switch back with: rmx restore back");
1170
+ } else {
1171
+ console.error("Plain `codex` was not fully restored. Inspect $CODEX_HOME/config.toml before using native Codex.");
1172
+ }
1173
+ break;
1174
+ }
1175
+ case "recover-history":
1176
+ await handleRecoverHistory();
1177
+ break;
1178
+ case "uninstall":
1179
+ case "remove":
1180
+ await handleUninstall();
1181
+ break;
1182
+ case "status":
1183
+ await handleStatus();
1184
+ break;
1185
+ case "doctor": {
1186
+ const { runDoctor } = await import("./doctor");
1187
+ await runDoctor(args.slice(1));
1188
+ break;
1189
+ }
1190
+ case "debug": {
1191
+ const { handleDebugCommand } = await import("./debug");
1192
+ await handleDebugCommand(args.slice(1));
1193
+ break;
1194
+ }
1195
+ case "ensure":
1196
+ await handleEnsure();
1197
+ break;
1198
+ case "login": {
1199
+ const { handleLogin } = await import("../oauth/login-cli");
1200
+ await handleLogin(args[1]);
1201
+ break;
1202
+ }
1203
+ case "logout": {
1204
+ const { removeCredential } = await import("../oauth/store");
1205
+ const name = (args[1] ?? "").trim().toLowerCase();
1206
+ await removeCredential(name);
1207
+ console.log(`Logged out of ${name || "(none)"}.`);
1208
+ break;
1209
+ }
1210
+ case "sync": {
1211
+ const restartCodex = args.slice(1).includes("--restart-codex");
1212
+ const synced = await syncModelsToCodex((await findLiveProxy())?.port);
1213
+ if (synced.status === "skipped") {
1214
+ console.log("Codex integration is OFF; sync skipped and no Codex files changed.");
1215
+ } else if (!synced.ok) {
1216
+ process.exitCode = 1;
1217
+ console.error("Codex sync did not complete. Fix the reported Codex config issue and retry.");
1218
+ }
1219
+ // Only warn/restart when a catalog or models_cache write actually happened. This is
1220
+ // deliberately not an `else`: refreshCodexModelCatalog runs before injectCodexConfig,
1221
+ // so a sync can fail (`ok: false`) after the catalog was already rewritten — which is
1222
+ // exactly when a long-lived app-server is holding the stale list.
1223
+ if (synced.catalogWritten || synced.cacheSynced) {
1224
+ const { afterCatalogWriteHandleAppServers } = await import("../codex/app-server-processes");
1225
+ afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
1226
+ }
1227
+ break;
1228
+ }
1229
+ case "v2": {
1230
+ const { cmdV2 } = await import("./v2");
1231
+ process.exitCode = await cmdV2(args.slice(1), {}, async () => (await findLiveProxy())?.port);
1232
+ break;
1233
+ }
1234
+ case "sync-cache": {
1235
+ const restartCodex = args.slice(1).includes("--restart-codex");
1236
+ if (!shouldSyncCodexOnStart(loadConfig())) {
1237
+ console.log("Codex integration is OFF; cache sync skipped and no Codex files changed.");
1238
+ break;
1239
+ }
1240
+ const { withCatalogWriteSerialization } = await import("../codex/catalog-write-serialization");
1241
+ const { invalidateCodexModelsCacheWithPermit } = await import("../codex/catalog/sync");
1242
+ const { getCodexHome } = await import("../codex/paths");
1243
+ const owningCodexHome = getCodexHome();
1244
+ const invalidated = withCatalogWriteSerialization(owningCodexHome, permit =>
1245
+ invalidateCodexModelsCacheWithPermit(permit, owningCodexHome));
1246
+ // Only warn/restart when models_cache was actually rewritten from a readable catalog.
1247
+ if (invalidated.kind === "completed" && invalidated.value) {
1248
+ const { afterCatalogWriteHandleAppServers } = await import("../codex/app-server-processes");
1249
+ afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
1250
+ }
1251
+ break;
1252
+ }
1253
+ case "gui": {
1254
+ const cfg = await import("../config");
1255
+ const config = cfg.loadConfig();
1256
+ // Identity-checked liveness (not the pid file + a fixed sleep): finds a fallback-port
1257
+ // proxy and waits until the spawned one actually answers before opening the browser.
1258
+ let live = await findLiveProxy();
1259
+ if (!live) {
1260
+ console.log("Proxy not running. Starting...");
1261
+ const child = spawn(process.execPath, startArgv((config.port ?? 10100) > 0 ? (config.port ?? 10100) : undefined), {
1262
+ detached: true,
1263
+ stdio: "ignore",
1264
+ windowsHide: true,
1265
+ env: withProcessRuntimeProvenance(process.env),
1266
+ });
1267
+ child.unref();
1268
+ live = await waitForProxy();
1269
+ if (!live) {
1270
+ console.error("❌ Proxy did not become healthy after starting. Not opening the GUI.");
1271
+ process.exit(1);
1272
+ }
1273
+ }
1274
+ // Open the host the proxy actually binds — `localhost` only answers for
1275
+ // loopback/wildcard binds, not a concrete LAN/IPv6 hostname.
1276
+ const guiHost = probeHostname(live?.hostname ?? config.hostname);
1277
+ const guiUrl = `http://${guiHost === "127.0.0.1" ? "localhost" : guiHost}:${live?.port ?? config.port}`;
1278
+ console.log(`Opening ${guiUrl}`);
1279
+ const { openUrl } = await import("../lib/open-url");
1280
+ openUrl(guiUrl);
1281
+ break;
1282
+ }
1283
+ case "service":
1284
+ await serviceCommand(...args.slice(1));
1285
+ break;
1286
+ case "tray": {
1287
+ const { windowsTrayCommand } = await import("../tray/windows");
1288
+ await windowsTrayCommand(args.slice(1));
1289
+ break;
1290
+ }
1291
+ case "codex-shim": {
1292
+ const { codexShimStatus, diagnoseCodexShim, installCodexShim, uninstallCodexShim } = await import("../codex/shim");
1293
+ switch (args[1]) {
1294
+ case "install": {
1295
+ const r = installCodexShim();
1296
+ const { collectCodexShimReadinessWarnings } = await import("./codex-shim-readiness");
1297
+ const warnings = diagnoseCodexShim().healthy
1298
+ ? collectCodexShimReadinessWarnings()
1299
+ : [];
1300
+ console.log(`${r.installed && warnings.length === 0 ? "✅ " : "⚠️ "}${r.message}`);
1301
+ for (const warning of warnings) console.warn(` ${warning}`);
1302
+ break;
1303
+ }
1304
+ case "status":
1305
+ console.log(codexShimStatus());
1306
+ break;
1307
+ case "uninstall":
1308
+ case "remove": {
1309
+ const r = uninstallCodexShim();
1310
+ console.log(r.removed ? `✅ ${r.message}` : `⚠️ ${r.message}`);
1311
+ break;
1312
+ }
1313
+ default:
1314
+ console.error("Usage: rmx codex-shim <install|status|uninstall|remove>");
1315
+ process.exit(1);
1316
+ }
1317
+ break;
1318
+ }
1319
+ case "update": {
1320
+ // `rmx update --help` must print usage and exit WITHOUT side effects — running the
1321
+ // real self-update stops the proxy and drops in-flight routed streams (issue #168).
1322
+ if (hasHelpFlag(args.slice(1))) {
1323
+ printSubcommandUsage("update");
1324
+ break;
1325
+ }
1326
+ const { runUpdate } = await import("../update");
1327
+ await runUpdate();
1328
+ break;
1329
+ }
1330
+ case "__refresh-version": {
1331
+ // Hidden, detached helper spawned by the update prompt to refresh the
1332
+ // cached latest version without blocking the foreground start. Not in help.
1333
+ const { refreshVersionCache } = await import("../update/notify");
1334
+ const channel = args[1] === "preview" ? "preview" : "latest";
1335
+ await refreshVersionCache(channel);
1336
+ break;
1337
+ }
1338
+ case "__desktop-update-worker": {
1339
+ const id = args[1];
1340
+ const mode = args[2];
1341
+ if (!id || !/^desktop-\d+-[a-z0-9]+$/.test(id) || (mode !== "download" && mode !== "install")) {
1342
+ console.error("Invalid desktop update worker arguments.");
1343
+ process.exitCode = 64;
1344
+ break;
1345
+ }
1346
+ await runDesktopUpdateWorker(id, mode === "install");
1347
+ break;
1348
+ }
1349
+ case "__desktop-update":
1350
+ case "__desktop-auto-update-check":
1351
+ case "__tray-start":
1352
+ case "__tray-restart":
1353
+ case "__startup-health":
1354
+ case "__desktop-restart-codex":
1355
+ case "__desktop-restart-client":
1356
+ await dispatchInternalCliCommand(command as InternalCliCommand, {
1357
+ trayStart: async () => { await handleTrayProxyStart(); },
1358
+ trayRestart: handleTrayProxyRestart,
1359
+ desktopUpdate: () => handleDesktopUpdateRequest(args.slice(1)),
1360
+ desktopAutoUpdateCheck: handleAutomaticDesktopUpdateCheck,
1361
+ startupHealth: async () => {
1362
+ const { collectStartupHealth } = await import("../codex/autostart-health");
1363
+ console.log(JSON.stringify(collectStartupHealth(loadConfig())));
1364
+ },
1365
+ desktopRestartCodex: async () => {
1366
+ const {
1367
+ listCodexAppServerProcesses,
1368
+ restartCodexAppServers,
1369
+ } = await import("../codex/app-server-processes");
1370
+ const processes = listCodexAppServerProcesses();
1371
+ if (processes.length === 0) {
1372
+ console.log("No Codex app-server process is currently running.");
1373
+ return;
1374
+ }
1375
+ const result = restartCodexAppServers(processes);
1376
+ if (result.failed.length > 0 || result.surviving.length > 0) {
1377
+ console.error(
1378
+ `Codex restart was incomplete; surviving PID(s): ${result.surviving.join(", ") || "none"}.`,
1379
+ );
1380
+ process.exitCode = 1;
1381
+ return;
1382
+ }
1383
+ console.log(`Codex app-server restart requested for PID(s): ${result.requested.join(", ")}.`);
1384
+ },
1385
+ desktopRestartClient: async () => {
1386
+ const { restartDesktopClient } = await import("../codex/desktop-client-processes");
1387
+ const result = await restartDesktopClient();
1388
+ if (!result.ok) {
1389
+ console.error(result.message);
1390
+ process.exitCode = 1;
1391
+ return;
1392
+ }
1393
+ console.log(`Restarted the ${result.target.product} desktop application.`);
1394
+ },
1395
+ });
1396
+ break;
1397
+ case "__tray-host": {
1398
+ const { runWindowsTrayHost } = await import("../tray/windows");
1399
+ await runWindowsTrayHost();
1400
+ break;
1401
+ }
1402
+ case "__gui-update-worker": {
1403
+ const jobId = args[1];
1404
+ if (!jobId) process.exit(1);
1405
+ const channel = normalizeUpdateChannel(args[2]);
1406
+ await runGuiUpdateWorker(jobId, channel, args[3] === "restart");
1407
+ break;
1408
+ }
1409
+ case "restart": {
1410
+ // The running proxy owns its drain and replacement through /api/system/restart.
1411
+ // If nothing is live, restart degrades to the documented `ensure` start behavior.
1412
+ await handleProxyRestart(handleRestartStartWhenStopped);
1413
+ break;
1414
+ }
1415
+ case "health": {
1416
+ const healthArgs = args.slice(1);
1417
+ const wantsHealthJson = healthArgs.includes("--json");
1418
+ const live = await findLiveProxy();
1419
+ if (wantsHealthJson) {
1420
+ console.log(JSON.stringify({ ok: !!live, pid: live?.pid ?? null, port: live?.port ?? null }));
1421
+ } else {
1422
+ console.log(live ? `Proxy healthy (PID ${live.pid}, port ${live.port})` : "Proxy not healthy");
1423
+ }
1424
+ process.exit(live ? 0 : 1);
1425
+ }
1426
+ case "ready":
1427
+ // Fail-closed impossible-state guard: readyArgs is populated by the
1428
+ // preparse block before maybeAutoRestoreCodexShim, so reaching here
1429
+ // without it means dispatch diverged. Refuse with code 64 and perform
1430
+ // NO I/O (no discovery/probe). process.exit is `never`, narrowing below.
1431
+ if (!readyArgs) process.exit(64);
1432
+ await handleReady(readyArgs);
1433
+ break;
1434
+ case "provider": {
1435
+ const { handleProviderCommand } = await import("./provider");
1436
+ await handleProviderCommand(args.slice(1));
1437
+ break;
1438
+ }
1439
+ case "account": {
1440
+ const { cmdAccount } = await import("./account");
1441
+ process.exitCode = await cmdAccount(args.slice(1));
1442
+ break;
1443
+ }
1444
+ case "models":
1445
+ case "model": {
1446
+ const { handleModels } = await import("./models");
1447
+ await handleModels(args.slice(1));
1448
+ break;
1449
+ }
1450
+ case "combo": {
1451
+ const { handleComboCommand } = await import("./combo");
1452
+ process.exitCode = await handleComboCommand(args.slice(1));
1453
+ break;
1454
+ }
1455
+ case "route": {
1456
+ if (args[1] !== "combo" && args[1] !== "policy") {
1457
+ console.error("Usage: rmx route <combo|policy> <subcommand>");
1458
+ process.exitCode = 2;
1459
+ break;
1460
+ }
1461
+ if (args[1] === "combo") {
1462
+ const { handleComboCommand } = await import("./combo");
1463
+ process.exitCode = await handleComboCommand(args.slice(2));
1464
+ } else {
1465
+ const { handleRoutePolicyCommand } = await import("./route-policy");
1466
+ process.exitCode = await handleRoutePolicyCommand(args.slice(2));
1467
+ }
1468
+ break;
1469
+ }
1470
+ case "agent": {
1471
+ const { handleAgentCommand } = await import("./agent");
1472
+ process.exitCode = await handleAgentCommand(args.slice(1));
1473
+ break;
1474
+ }
1475
+ case "observe": {
1476
+ const { handleObserveCommand } = await import("./observe");
1477
+ process.exitCode = await handleObserveCommand(args.slice(1));
1478
+ break;
1479
+ }
1480
+ case "logs":
1481
+ case "usage":
1482
+ case "storage":
1483
+ case "memory": {
1484
+ const { handleObserveCommand } = await import("./observe");
1485
+ process.exitCode = await handleObserveCommand([command, ...args.slice(1)]);
1486
+ break;
1487
+ }
1488
+ case "access": {
1489
+ const { handleAccessCommand } = await import("./access");
1490
+ process.exitCode = await handleAccessCommand(args.slice(1));
1491
+ break;
1492
+ }
1493
+ case "api-key": {
1494
+ const { handleAccessCommand } = await import("./access");
1495
+ process.exitCode = await handleAccessCommand(["key", ...args.slice(1)]);
1496
+ break;
1497
+ }
1498
+ case "export": {
1499
+ const { handleExportCommand } = await import("./export-command");
1500
+ process.exitCode = await handleExportCommand(args.slice(1));
1501
+ break;
1502
+ }
1503
+ case "grok": {
1504
+ const { handleGrokCommand } = await import("./integrations");
1505
+ process.exitCode = await handleGrokCommand(args.slice(1));
1506
+ break;
1507
+ }
1508
+ case "integration": {
1509
+ const integration = args[1];
1510
+ if (integration === "grok") {
1511
+ const { handleGrokCommand } = await import("./integrations");
1512
+ process.exitCode = await handleGrokCommand(args.slice(2));
1513
+ } else if (integration === "claude") {
1514
+ const { handleClaudeConfigCommand } = await import("./integrations");
1515
+ process.exitCode = await handleClaudeConfigCommand(args.slice(2));
1516
+ } else if (integration === "client") {
1517
+ const { handleClientIntegrationCommand } = await import("./integrations");
1518
+ process.exitCode = await handleClientIntegrationCommand(args.slice(2));
1519
+ } else {
1520
+ console.error("Usage: rmx integration <claude|grok|client> <subcommand>");
1521
+ process.exitCode = 2;
1522
+ }
1523
+ break;
1524
+ }
1525
+ case "system": {
1526
+ const { handleSystemCommand } = await import("./system-command");
1527
+ process.exitCode = await handleSystemCommand(args.slice(1));
1528
+ break;
1529
+ }
1530
+ case "config": {
1531
+ const { handleConfigCommand } = await import("./config-command");
1532
+ process.exitCode = await handleConfigCommand(args.slice(1));
1533
+ break;
1534
+ }
1535
+ case "claude": {
1536
+ const { cmdClaude } = await import("./claude");
1537
+ // "rmx claude desktop" → write Desktop 3P config
1538
+ if (args[1] === "desktop") {
1539
+ const { handleClaudeDesktopCommand } = await import("./claude-desktop");
1540
+ const exitCode = await handleClaudeDesktopCommand(args.slice(2));
1541
+ if (exitCode !== 0) process.exit(exitCode);
1542
+ break;
1543
+ }
1544
+ if (args[1] === "config") {
1545
+ const { handleClaudeConfigCommand } = await import("./integrations");
1546
+ process.exitCode = await handleClaudeConfigCommand(args.slice(2));
1547
+ break;
1548
+ }
1549
+ process.exit(await cmdClaude(args.slice(1)));
1550
+ }
1551
+ case "opencode": {
1552
+ const { cmdOpencode } = await import("./opencode");
1553
+ process.exit(await cmdOpencode(args.slice(1)));
1554
+ }
1555
+ case "help":
1556
+ case "--help":
1557
+ case "-h":
1558
+ case undefined:
1559
+ printUsage();
1560
+ break;
1561
+ default:
1562
+ console.error(`Unknown command: ${command}`);
1563
+ printUsage();
1564
+ process.exit(1);
1565
+ }