@iislee/opencodex 2.11.0

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 (476) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx.mjs +451 -0
  8. package/bin/package-main.mjs +9 -0
  9. package/gui/dist/assets/index-DTpMHS4F.js +67 -0
  10. package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
  11. package/gui/dist/favicon.png +0 -0
  12. package/gui/dist/icons.svg +24 -0
  13. package/gui/dist/index.html +25 -0
  14. package/gui/dist/logo.png +0 -0
  15. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  16. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/claude.svg +1 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/cursor.svg +2 -0
  25. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  26. package/gui/dist/provider-icons/discord.svg +1 -0
  27. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  28. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  29. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini.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-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok.svg +1 -0
  35. package/gui/dist/provider-icons/groq-color.svg +1 -0
  36. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  37. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  38. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  39. package/gui/dist/provider-icons/kiro.svg +14 -0
  40. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  41. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  42. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  43. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  44. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  45. package/gui/dist/provider-icons/openai.svg +1 -0
  46. package/gui/dist/provider-icons/opencode.svg +1 -0
  47. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  48. package/gui/dist/provider-icons/pi.svg +21 -0
  49. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  50. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  51. package/gui/dist/provider-icons/telegram.svg +1 -0
  52. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  53. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  54. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  55. package/package.json +102 -0
  56. package/src/AGENTS.md +28 -0
  57. package/src/adapters/anthropic-image-guard.ts +251 -0
  58. package/src/adapters/anthropic-image-normalize.ts +518 -0
  59. package/src/adapters/anthropic.ts +1003 -0
  60. package/src/adapters/azure.ts +36 -0
  61. package/src/adapters/base.ts +72 -0
  62. package/src/adapters/client-fingerprint.ts +59 -0
  63. package/src/adapters/cursor/arg-codec.ts +38 -0
  64. package/src/adapters/cursor/arg-normalize.ts +104 -0
  65. package/src/adapters/cursor/cursor-errors.ts +165 -0
  66. package/src/adapters/cursor/discovery.ts +276 -0
  67. package/src/adapters/cursor/effort-map.ts +127 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +211 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/kv-store.ts +52 -0
  72. package/src/adapters/cursor/live-models.ts +153 -0
  73. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  74. package/src/adapters/cursor/live-transport.ts +1214 -0
  75. package/src/adapters/cursor/mcp-config.ts +42 -0
  76. package/src/adapters/cursor/mcp-manager.ts +333 -0
  77. package/src/adapters/cursor/message-mapper.ts +49 -0
  78. package/src/adapters/cursor/native-exec-common.ts +55 -0
  79. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  80. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  81. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  82. package/src/adapters/cursor/native-exec-network.ts +43 -0
  83. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  84. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  85. package/src/adapters/cursor/native-exec.ts +576 -0
  86. package/src/adapters/cursor/protobuf-events.ts +563 -0
  87. package/src/adapters/cursor/protobuf-request.ts +714 -0
  88. package/src/adapters/cursor/request-builder.ts +255 -0
  89. package/src/adapters/cursor/thread-continuity.ts +67 -0
  90. package/src/adapters/cursor/tool-definitions.ts +505 -0
  91. package/src/adapters/cursor/transport-retry.ts +132 -0
  92. package/src/adapters/cursor/transport.ts +57 -0
  93. package/src/adapters/cursor/types.ts +52 -0
  94. package/src/adapters/cursor.ts +196 -0
  95. package/src/adapters/google-antigravity-replay.ts +303 -0
  96. package/src/adapters/google-antigravity-wire.ts +108 -0
  97. package/src/adapters/google-errors.ts +85 -0
  98. package/src/adapters/google-http.ts +100 -0
  99. package/src/adapters/google-tool-schema.ts +173 -0
  100. package/src/adapters/google-truncation.ts +13 -0
  101. package/src/adapters/google-wire-compiler.ts +232 -0
  102. package/src/adapters/google.ts +758 -0
  103. package/src/adapters/identity.ts +44 -0
  104. package/src/adapters/image.ts +23 -0
  105. package/src/adapters/kiro-constants.ts +16 -0
  106. package/src/adapters/kiro-errors.ts +197 -0
  107. package/src/adapters/kiro-events.ts +179 -0
  108. package/src/adapters/kiro-images.ts +129 -0
  109. package/src/adapters/kiro-retry.ts +312 -0
  110. package/src/adapters/kiro-thinking.ts +96 -0
  111. package/src/adapters/kiro-tool-fallback.ts +36 -0
  112. package/src/adapters/kiro-tools.ts +215 -0
  113. package/src/adapters/kiro-truncation.ts +33 -0
  114. package/src/adapters/kiro-wire.ts +129 -0
  115. package/src/adapters/kiro.ts +1898 -0
  116. package/src/adapters/mimo-free.ts +263 -0
  117. package/src/adapters/openai-chat.ts +1005 -0
  118. package/src/adapters/openai-responses.ts +1137 -0
  119. package/src/adapters/run-turn-queue.ts +114 -0
  120. package/src/adapters/tool-catalog-nudge.ts +71 -0
  121. package/src/adapters/upstream-http-error.ts +48 -0
  122. package/src/bridge.ts +1619 -0
  123. package/src/chat/inbound.ts +295 -0
  124. package/src/chat/outbound.ts +765 -0
  125. package/src/claude/agents-inject.ts +243 -0
  126. package/src/claude/alias.ts +149 -0
  127. package/src/claude/auth-detect.ts +229 -0
  128. package/src/claude/auth-mode-migration.ts +32 -0
  129. package/src/claude/auth-mode.ts +62 -0
  130. package/src/claude/context-windows.ts +189 -0
  131. package/src/claude/desktop-3p-guard.ts +35 -0
  132. package/src/claude/desktop-3p-paths.ts +84 -0
  133. package/src/claude/desktop-3p.ts +381 -0
  134. package/src/claude/desktop-health.ts +26 -0
  135. package/src/claude/desktop-profile.ts +263 -0
  136. package/src/claude/gateway-cache.ts +70 -0
  137. package/src/claude/inbound-debug.ts +163 -0
  138. package/src/claude/inbound.ts +509 -0
  139. package/src/claude/model-info.ts +151 -0
  140. package/src/claude/outbound.ts +872 -0
  141. package/src/cli/access.ts +108 -0
  142. package/src/cli/account-api.ts +268 -0
  143. package/src/cli/account-auth.ts +223 -0
  144. package/src/cli/account-extended.ts +350 -0
  145. package/src/cli/account.ts +275 -0
  146. package/src/cli/agent-driven.ts +70 -0
  147. package/src/cli/agent.ts +184 -0
  148. package/src/cli/catalog-prewarm.ts +27 -0
  149. package/src/cli/claude-desktop.ts +188 -0
  150. package/src/cli/claude.ts +286 -0
  151. package/src/cli/codex-shim-autorestore.ts +45 -0
  152. package/src/cli/combo.ts +119 -0
  153. package/src/cli/config-command.ts +145 -0
  154. package/src/cli/debug.ts +228 -0
  155. package/src/cli/doctor.ts +930 -0
  156. package/src/cli/export-command.ts +187 -0
  157. package/src/cli/help.ts +354 -0
  158. package/src/cli/index.ts +1113 -0
  159. package/src/cli/init.ts +224 -0
  160. package/src/cli/integrations.ts +142 -0
  161. package/src/cli/interactive-confirm.ts +133 -0
  162. package/src/cli/internal-dispatch.ts +20 -0
  163. package/src/cli/models-runtime.ts +212 -0
  164. package/src/cli/models.ts +336 -0
  165. package/src/cli/observe.ts +117 -0
  166. package/src/cli/opencode.ts +586 -0
  167. package/src/cli/pi.ts +188 -0
  168. package/src/cli/provider-runtime.ts +162 -0
  169. package/src/cli/provider.ts +463 -0
  170. package/src/cli/runtime-api.ts +325 -0
  171. package/src/cli/star-prompt.ts +155 -0
  172. package/src/cli/status-oauth.ts +78 -0
  173. package/src/cli/status.ts +321 -0
  174. package/src/cli/sync-cloud.ts +283 -0
  175. package/src/cli/system-command.ts +112 -0
  176. package/src/cli/tray-proxy.ts +52 -0
  177. package/src/cli/v2.ts +173 -0
  178. package/src/cli.ts +10 -0
  179. package/src/clients/config-export.ts +377 -0
  180. package/src/clients/effective-status.ts +385 -0
  181. package/src/clients/probes/agy.ts +55 -0
  182. package/src/clients/probes/cc-switch.ts +110 -0
  183. package/src/clients/probes/claude.ts +90 -0
  184. package/src/clients/probes/codex.ts +125 -0
  185. package/src/clients/probes/grok.ts +29 -0
  186. package/src/clients/probes/opencode.ts +109 -0
  187. package/src/clients/probes/paseo.ts +55 -0
  188. package/src/clients/probes/pi.ts +55 -0
  189. package/src/cloud/onedrive-auth.ts +666 -0
  190. package/src/cloud/onedrive-graph.ts +108 -0
  191. package/src/cloud/settings.ts +75 -0
  192. package/src/cloud/sync.ts +212 -0
  193. package/src/cloud/types.ts +56 -0
  194. package/src/cloud/vault.ts +89 -0
  195. package/src/codex/account-id.ts +34 -0
  196. package/src/codex/account-label.ts +34 -0
  197. package/src/codex/account-lifecycle.ts +55 -0
  198. package/src/codex/account-namespace-match.ts +63 -0
  199. package/src/codex/account-namespaces.ts +149 -0
  200. package/src/codex/account-pause.ts +20 -0
  201. package/src/codex/account-runtime-state.ts +31 -0
  202. package/src/codex/account-store.ts +517 -0
  203. package/src/codex/account-usability.ts +20 -0
  204. package/src/codex/app-server-processes.ts +756 -0
  205. package/src/codex/auth-api.ts +1540 -0
  206. package/src/codex/auth-collision.ts +107 -0
  207. package/src/codex/auth-context.ts +352 -0
  208. package/src/codex/autostart-health.ts +149 -0
  209. package/src/codex/catalog/aggregation.ts +378 -0
  210. package/src/codex/catalog/bundled.ts +251 -0
  211. package/src/codex/catalog/effort.ts +355 -0
  212. package/src/codex/catalog/metadata.ts +180 -0
  213. package/src/codex/catalog/parsing.ts +456 -0
  214. package/src/codex/catalog/provider-fetch.ts +902 -0
  215. package/src/codex/catalog/sync.ts +620 -0
  216. package/src/codex/catalog.ts +12 -0
  217. package/src/codex/data/upstream-models.json +830 -0
  218. package/src/codex/exec-invocation.ts +22 -0
  219. package/src/codex/features.ts +969 -0
  220. package/src/codex/history-migration-guardian.ts +102 -0
  221. package/src/codex/history-provider.ts +776 -0
  222. package/src/codex/home.ts +206 -0
  223. package/src/codex/inject.ts +799 -0
  224. package/src/codex/injected-marker.ts +72 -0
  225. package/src/codex/journal.ts +163 -0
  226. package/src/codex/main-account-cache.ts +32 -0
  227. package/src/codex/main-account.ts +40 -0
  228. package/src/codex/model-cache.ts +227 -0
  229. package/src/codex/paths.ts +65 -0
  230. package/src/codex/plugins-doctor.ts +242 -0
  231. package/src/codex/pool-rotation.ts +225 -0
  232. package/src/codex/project-config-warnings.ts +411 -0
  233. package/src/codex/quota.ts +411 -0
  234. package/src/codex/refresh.ts +53 -0
  235. package/src/codex/routing.ts +1477 -0
  236. package/src/codex/runtime.ts +538 -0
  237. package/src/codex/shim.ts +1189 -0
  238. package/src/codex/subagent-defaults.ts +550 -0
  239. package/src/codex/subagent-model-fallback.ts +469 -0
  240. package/src/codex/sync.ts +130 -0
  241. package/src/codex/warmup.ts +192 -0
  242. package/src/codex/websocket-registry.ts +100 -0
  243. package/src/combos/failover.ts +140 -0
  244. package/src/combos/index.ts +41 -0
  245. package/src/combos/request.ts +62 -0
  246. package/src/combos/resolve.ts +232 -0
  247. package/src/combos/types.ts +326 -0
  248. package/src/config.ts +2356 -0
  249. package/src/generated/jawcode-model-metadata.ts +104 -0
  250. package/src/github/star-state.ts +203 -0
  251. package/src/grok/inject.ts +545 -0
  252. package/src/grok/status.ts +121 -0
  253. package/src/grok/sync.ts +103 -0
  254. package/src/grok/usage-hook/report.mjs +348 -0
  255. package/src/grok/usage-hook.ts +278 -0
  256. package/src/images/artifacts.ts +516 -0
  257. package/src/images/fulfill-video.ts +163 -0
  258. package/src/images/fulfill.ts +149 -0
  259. package/src/images/index.ts +4 -0
  260. package/src/images/loop.ts +829 -0
  261. package/src/images/plan.ts +133 -0
  262. package/src/images/synthetic-tool.ts +133 -0
  263. package/src/images/types.ts +41 -0
  264. package/src/images/xai-client.ts +141 -0
  265. package/src/images/xai-video-client.ts +163 -0
  266. package/src/index.ts +22 -0
  267. package/src/lib/abort.ts +146 -0
  268. package/src/lib/admin-secrets.ts +25 -0
  269. package/src/lib/admission.ts +83 -0
  270. package/src/lib/app-owned-memory-stores.ts +173 -0
  271. package/src/lib/app-owned-memory.ts +265 -0
  272. package/src/lib/bounded-body.ts +202 -0
  273. package/src/lib/bun-binary-validator.d.mts +3 -0
  274. package/src/lib/bun-binary-validator.mjs +18 -0
  275. package/src/lib/bun-runtime.ts +71 -0
  276. package/src/lib/bun-stream-caps.ts +126 -0
  277. package/src/lib/config-ownership.ts +360 -0
  278. package/src/lib/crash-guard.ts +344 -0
  279. package/src/lib/debug-log-buffer.ts +83 -0
  280. package/src/lib/debug-settings.ts +108 -0
  281. package/src/lib/debug.ts +31 -0
  282. package/src/lib/destination-policy.ts +316 -0
  283. package/src/lib/errors.ts +364 -0
  284. package/src/lib/eventstream-decoder.ts +253 -0
  285. package/src/lib/gcp-adc.ts +341 -0
  286. package/src/lib/injection-debug-log.ts +58 -0
  287. package/src/lib/open-url.ts +25 -0
  288. package/src/lib/pinned-http.ts +151 -0
  289. package/src/lib/privacy.ts +20 -0
  290. package/src/lib/process-control.ts +165 -0
  291. package/src/lib/provider-outbound.ts +170 -0
  292. package/src/lib/provider-url.ts +14 -0
  293. package/src/lib/proxy-env.ts +18 -0
  294. package/src/lib/redact.ts +105 -0
  295. package/src/lib/retry-after.ts +55 -0
  296. package/src/lib/service-secrets.ts +25 -0
  297. package/src/lib/shadow-call.ts +30 -0
  298. package/src/lib/sidecar-tracker.ts +52 -0
  299. package/src/lib/sse-decoder.ts +323 -0
  300. package/src/lib/state-store-registrations.ts +109 -0
  301. package/src/lib/state-store-sweeper.ts +184 -0
  302. package/src/lib/test-home-guard.ts +90 -0
  303. package/src/lib/token-estimate.ts +69 -0
  304. package/src/lib/translator-budget.ts +356 -0
  305. package/src/lib/upstream-retry.ts +239 -0
  306. package/src/lib/win-exec.ts +115 -0
  307. package/src/lib/win-paths.ts +68 -0
  308. package/src/lib/windows-elevation.ts +705 -0
  309. package/src/lib/windows-secret-acl.ts +514 -0
  310. package/src/lib/winsw.ts +375 -0
  311. package/src/oauth/anthropic-routing.ts +594 -0
  312. package/src/oauth/anthropic.ts +177 -0
  313. package/src/oauth/callback-server.ts +294 -0
  314. package/src/oauth/chatgpt.ts +150 -0
  315. package/src/oauth/cursor.ts +211 -0
  316. package/src/oauth/github-copilot.ts +428 -0
  317. package/src/oauth/google-antigravity.ts +230 -0
  318. package/src/oauth/health.ts +399 -0
  319. package/src/oauth/index.ts +1174 -0
  320. package/src/oauth/key-providers.ts +108 -0
  321. package/src/oauth/kimi.ts +213 -0
  322. package/src/oauth/kiro-credentials.ts +726 -0
  323. package/src/oauth/kiro.ts +577 -0
  324. package/src/oauth/local-token-detect.ts +121 -0
  325. package/src/oauth/log.ts +48 -0
  326. package/src/oauth/login-cli.ts +163 -0
  327. package/src/oauth/pkce.ts +15 -0
  328. package/src/oauth/store.ts +630 -0
  329. package/src/oauth/token-guardian.ts +303 -0
  330. package/src/oauth/types.ts +62 -0
  331. package/src/oauth/xai.ts +241 -0
  332. package/src/pi/extensions.ts +72 -0
  333. package/src/pi/home.ts +42 -0
  334. package/src/pi/index.ts +40 -0
  335. package/src/pi/models.ts +278 -0
  336. package/src/pi/packages.ts +219 -0
  337. package/src/pi/settings.ts +365 -0
  338. package/src/pi/status.ts +68 -0
  339. package/src/pi/sync.ts +75 -0
  340. package/src/providers/alibaba-region-backup.ts +75 -0
  341. package/src/providers/alibaba-region-migration.ts +156 -0
  342. package/src/providers/alibaba-region-startup.ts +36 -0
  343. package/src/providers/antigravity-models.ts +205 -0
  344. package/src/providers/api-keys.ts +140 -0
  345. package/src/providers/base-url-choices.ts +64 -0
  346. package/src/providers/context-cap.ts +65 -0
  347. package/src/providers/derive.ts +339 -0
  348. package/src/providers/free-directory.ts +184 -0
  349. package/src/providers/github-copilot-transport.ts +56 -0
  350. package/src/providers/key-failover.ts +203 -0
  351. package/src/providers/kiro-models.ts +67 -0
  352. package/src/providers/label.ts +19 -0
  353. package/src/providers/model-discovery.ts +356 -0
  354. package/src/providers/openai-sidecar.ts +175 -0
  355. package/src/providers/openai-tier-startup.ts +27 -0
  356. package/src/providers/openai-tiers.ts +301 -0
  357. package/src/providers/openai-virtual-models.ts +82 -0
  358. package/src/providers/openrouter-routing.ts +102 -0
  359. package/src/providers/provider-id-rewrite.ts +150 -0
  360. package/src/providers/quota.ts +1260 -0
  361. package/src/providers/registry.ts +1600 -0
  362. package/src/providers/slug-codec.ts +67 -0
  363. package/src/providers/xai-transport.ts +141 -0
  364. package/src/reasoning-effort.ts +135 -0
  365. package/src/responses/compaction.ts +117 -0
  366. package/src/responses/parser.ts +656 -0
  367. package/src/responses/reasoning-envelope.ts +52 -0
  368. package/src/responses/schema.ts +159 -0
  369. package/src/responses/spill-store.ts +394 -0
  370. package/src/responses/state.ts +895 -0
  371. package/src/responses/tool-groups.ts +19 -0
  372. package/src/router.ts +425 -0
  373. package/src/server/adapter-resolve.ts +80 -0
  374. package/src/server/auth-cors.ts +530 -0
  375. package/src/server/chat-completions.ts +368 -0
  376. package/src/server/claude-messages.ts +914 -0
  377. package/src/server/effort-policy.ts +172 -0
  378. package/src/server/gui-static.ts +123 -0
  379. package/src/server/image-retry.ts +42 -0
  380. package/src/server/images.ts +476 -0
  381. package/src/server/index.ts +1126 -0
  382. package/src/server/lifecycle.ts +227 -0
  383. package/src/server/live.ts +598 -0
  384. package/src/server/management/agent-settings-routes.ts +1169 -0
  385. package/src/server/management/api-access.ts +141 -0
  386. package/src/server/management/api-key-usage.ts +167 -0
  387. package/src/server/management/body.ts +35 -0
  388. package/src/server/management/clients-routes.ts +63 -0
  389. package/src/server/management/cloud-sync-routes.ts +266 -0
  390. package/src/server/management/combo-routes.ts +220 -0
  391. package/src/server/management/config-routes.ts +422 -0
  392. package/src/server/management/context.ts +31 -0
  393. package/src/server/management/logs-usage-routes.ts +707 -0
  394. package/src/server/management/model-routes.ts +525 -0
  395. package/src/server/management/oauth-account-routes.ts +563 -0
  396. package/src/server/management/provider-routes.ts +556 -0
  397. package/src/server/management/shared.ts +277 -0
  398. package/src/server/management/sidebar-routes.ts +90 -0
  399. package/src/server/management/system-restart.ts +179 -0
  400. package/src/server/management/system-routes.ts +117 -0
  401. package/src/server/management/usage-summary-cache.ts +86 -0
  402. package/src/server/management-api.ts +215 -0
  403. package/src/server/management-auth.ts +267 -0
  404. package/src/server/memory-watchdog.ts +156 -0
  405. package/src/server/port-reclaim.ts +307 -0
  406. package/src/server/ports.ts +116 -0
  407. package/src/server/proxy-liveness.ts +201 -0
  408. package/src/server/relay-eager.ts +313 -0
  409. package/src/server/relay.ts +1049 -0
  410. package/src/server/request-decompress.ts +132 -0
  411. package/src/server/request-log-conversation.ts +168 -0
  412. package/src/server/request-log.ts +1046 -0
  413. package/src/server/responses/collaboration.ts +354 -0
  414. package/src/server/responses/compact.ts +384 -0
  415. package/src/server/responses/core.ts +2758 -0
  416. package/src/server/responses/encrypted-payload.ts +308 -0
  417. package/src/server/responses/fetch-helpers.ts +157 -0
  418. package/src/server/responses/passthrough-error.ts +78 -0
  419. package/src/server/responses/terminal-guard.ts +230 -0
  420. package/src/server/responses/upstream-error.ts +48 -0
  421. package/src/server/responses-image-gen-repair.ts +132 -0
  422. package/src/server/responses-item-id-repair.ts +224 -0
  423. package/src/server/responses.ts +9 -0
  424. package/src/server/search.ts +136 -0
  425. package/src/server/sse-payload-rewrite.ts +175 -0
  426. package/src/server/startup-action-control.ts +308 -0
  427. package/src/server/startup-health-cache.ts +113 -0
  428. package/src/server/system-env.ts +413 -0
  429. package/src/server/windows-tcp-drop.ts +184 -0
  430. package/src/server/windows-tray-control.ts +41 -0
  431. package/src/server/ws-bridge.ts +471 -0
  432. package/src/service.ts +2554 -0
  433. package/src/stall-timeout.ts +20 -0
  434. package/src/storage/cleanup-job.ts +57 -0
  435. package/src/storage/cleanup.ts +3085 -0
  436. package/src/storage/policy-job.ts +457 -0
  437. package/src/storage/policy-scheduler.ts +40 -0
  438. package/src/storage/policy-worker.ts +59 -0
  439. package/src/storage/policy.ts +527 -0
  440. package/src/storage/restore-job.ts +299 -0
  441. package/src/storage/restore-worker.ts +58 -0
  442. package/src/storage/scanner.ts +238 -0
  443. package/src/storage/storage-mutation-coordinator.ts +139 -0
  444. package/src/storage/worker-lifecycle.ts +215 -0
  445. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  446. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  447. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  448. package/src/tray/assets/opencodex-tray.png +0 -0
  449. package/src/tray/windows-tray.ps1 +290 -0
  450. package/src/tray/windows.ts +730 -0
  451. package/src/types.ts +1237 -0
  452. package/src/update/badge.ts +72 -0
  453. package/src/update/index.ts +407 -0
  454. package/src/update/job.ts +1520 -0
  455. package/src/update/notify.ts +257 -0
  456. package/src/update/npm-invocation.d.mts +23 -0
  457. package/src/update/npm-invocation.mjs +94 -0
  458. package/src/update/tray-update-plan.d.mts +18 -0
  459. package/src/update/tray-update-plan.mjs +38 -0
  460. package/src/usage/cost.ts +0 -0
  461. package/src/usage/debug.ts +97 -0
  462. package/src/usage/expected-prices.ts +164 -0
  463. package/src/usage/log.ts +658 -0
  464. package/src/usage/summary.ts +585 -0
  465. package/src/usage/totals.ts +14 -0
  466. package/src/vision/anthropic-describe.ts +185 -0
  467. package/src/vision/describe.ts +125 -0
  468. package/src/vision/index.ts +467 -0
  469. package/src/web-search/anthropic-executor.ts +189 -0
  470. package/src/web-search/executor.ts +105 -0
  471. package/src/web-search/format-result.ts +89 -0
  472. package/src/web-search/index.ts +196 -0
  473. package/src/web-search/loop.ts +664 -0
  474. package/src/web-search/parse.ts +220 -0
  475. package/src/web-search/progress-stream.ts +342 -0
  476. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,799 @@
1
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
2
+ import { atomicWriteFile, loadConfig, subagentDefaultSyncEffective, websocketsEnabled } from "../config";
3
+ import { markJournalInjectedState, removeJournal, restoreJournalState, writeJournal } from "./journal";
4
+ import { restoreCodexCatalog } from "./catalog";
5
+ import { migrateHistoryToOpenai, syncCodexHistoryProvider } from "./history-provider";
6
+ import {
7
+ OCX_SECTION_MARKER,
8
+ hasInjectedCodexRouting,
9
+ hasInjectedOpenaiBaseUrl,
10
+ isRootOpenaiBaseUrlLine,
11
+ providerTableStart,
12
+ providerTableString,
13
+ rootTomlString,
14
+ tomlStringPattern,
15
+ } from "./injected-marker";
16
+ import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, DEFAULT_CATALOG_PATH, parseTomlString, readRootTomlString, resolveCodexConfigPath, tomlString } from "./paths";
17
+ import { resolveEffectiveProjectModelProvider } from "./project-config-warnings";
18
+ import {
19
+ transformManagedSubagentDefaults,
20
+ type ManagedSubagentDefaults,
21
+ } from "./subagent-defaults";
22
+ import type { OcxConfig } from "../types";
23
+
24
+ // Ownership predicates live in `./injected-marker` so `journal.ts` can reach them
25
+ // without importing this module back. Re-exported for existing external callers.
26
+ export { hasInjectedCodexRouting, hasInjectedOpenaiBaseUrl };
27
+
28
+ export function externalCodexModelProvider(content: string): string | null {
29
+ const provider = resolveEffectiveProjectModelProvider(content).provider;
30
+ return provider && provider !== "openai" && provider !== "opencodex" ? provider : null;
31
+ }
32
+
33
+ export function currentExternalCodexModelProvider(): string | null {
34
+ if (!existsSync(CODEX_CONFIG_PATH)) return null;
35
+ return externalCodexModelProvider(readFileSync(CODEX_CONFIG_PATH, "utf8"));
36
+ }
37
+
38
+ /**
39
+ * Detect the file's dominant line ending. Every transform in this module is LF-pure
40
+ * (split("\n") + hard "\n" joins), so CRLF configs (Windows-edited config.toml) are
41
+ * normalized to LF at the pipeline boundary and converted back on write — otherwise a
42
+ * single inject would leave a mixed-EOL file.
43
+ */
44
+ export function dominantEol(content: string): "\r\n" | "\n" {
45
+ const crlf = (content.match(/\r\n/g) ?? []).length;
46
+ if (crlf === 0) return "\n";
47
+ const bareLf = (content.match(/\n/g) ?? []).length - crlf;
48
+ return crlf >= bareLf ? "\r\n" : "\n";
49
+ }
50
+
51
+ /** Normalize all line endings to `eol` (CRLF first collapsed to LF, then expanded). */
52
+ export function applyEol(content: string, eol: "\r\n" | "\n"): string {
53
+ const lf = content.replace(/\r\n/g, "\n");
54
+ return eol === "\n" ? lf : lf.replace(/\n/g, "\r\n");
55
+ }
56
+
57
+ /**
58
+ * Design B (2026-07-06): loopback installs no longer re-tag the provider. Instead of
59
+ * `model_provider = "opencodex"` + a `[model_providers.opencodex]` table, we set the official
60
+ * built-in override `openai_base_url` (codex-rs config_toml.rs) so codex's own `openai`
61
+ * provider points at the proxy. Threads keep `model_provider = "openai"`, so history never
62
+ * needs remapping or restore. Non-loopback binds keep the legacy table injection because the
63
+ * built-in provider cannot carry the `x-opencodex-api-key` env header.
64
+ */
65
+
66
+ export interface InjectCodexOptions {
67
+ /**
68
+ * Absolute or CODEX_HOME-relative catalog path to advertise to Codex. Pass `null` only when the
69
+ * opencodex catalog could not be materialized; Codex will then keep its native catalog instead of
70
+ * failing on a missing model_catalog_json file.
71
+ */
72
+ catalogPath?: string | null;
73
+ }
74
+
75
+ function configuredManagedSubagentDefaults(
76
+ config: Pick<OcxConfig, "injectionModel" | "injectionEffort" | "syncCodexSubagentDefaults"> | undefined,
77
+ ): ManagedSubagentDefaults | null {
78
+ if (!subagentDefaultSyncEffective(config ?? {})) return null;
79
+ return {
80
+ model: config!.injectionModel!.trim(),
81
+ ...(config!.injectionEffort?.trim() ? { reasoningEffort: config!.injectionEffort.trim() } : {}),
82
+ };
83
+ }
84
+
85
+ /**
86
+ * The `[model_providers.opencodex]` TABLE only. A table is position-independent in TOML, so it is
87
+ * safe to append at EOF. The bare root key `model_provider = "opencodex"` is NOT included here —
88
+ * it must live at the document root (before any table header) and is set separately by
89
+ * setRootModelProvider(). Appending the bare key at EOF was the original bug: it nested under
90
+ * whatever `[table]` happened to be open last (e.g. `[plugins."chrome@openai-bundled"]`), so Codex
91
+ * never saw a global model_provider and silently fell back to the `openai` (ChatGPT) provider.
92
+ */
93
+ /**
94
+ * True only for hostnames that bind loopback ONLY. Wildcard binds ("0.0.0.0", "::") are NOT
95
+ * loopback: they expose the proxy on every interface and therefore require the admission token.
96
+ * Do not use `providerBaseHost` for this decision — it folds wildcards to 127.0.0.1 because it
97
+ * answers "what address do I dial", which is a different question from "is this exposed".
98
+ */
99
+ export function isLoopbackHostname(hostname: string | undefined): boolean {
100
+ const normalized = (hostname ?? "127.0.0.1").trim().toLowerCase();
101
+ return normalized === "" || normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
102
+ }
103
+
104
+ export function providerBaseHost(hostname: string | undefined): string {
105
+ const trimmed = (hostname ?? "127.0.0.1").trim();
106
+ const lower = trimmed.toLowerCase();
107
+ // Match what the server actually binds. Writing "localhost" while binding IPv4-only
108
+ // 127.0.0.1 breaks on Windows, where localhost commonly resolves to ::1 first.
109
+ if (lower === "::1" || lower === "[::1]") return "[::1]";
110
+ if (isLoopbackHostname(trimmed) || trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]") return "127.0.0.1";
111
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed;
112
+ return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
113
+ }
114
+
115
+ export function shouldInjectApiAuthHeader(config: Pick<OcxConfig, "hostname"> | undefined): boolean {
116
+ return !isLoopbackHostname(config?.hostname);
117
+ }
118
+
119
+ export function buildProviderTableBlock(port: number, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string): string {
120
+ const host = providerBaseHost(hostname);
121
+ const lines = [
122
+ "",
123
+ OCX_SECTION_MARKER,
124
+ "[model_providers.opencodex]",
125
+ 'name = "OpenCodex Proxy"',
126
+ `base_url = "http://${host}:${port}/v1"`,
127
+ 'wire_api = "responses"',
128
+ "requires_openai_auth = true",
129
+ ];
130
+ if (includeApiAuthHeader) {
131
+ lines.push('env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" }');
132
+ }
133
+ if (supportsWebsockets) lines.push("supports_websockets = true");
134
+ return lines.join("\n") + "\n";
135
+ }
136
+
137
+ export function buildOpenaiBaseUrlLine(port: number, hostname?: string): string {
138
+ return `openai_base_url = "http://${providerBaseHost(hostname)}:${port}/v1"`;
139
+ }
140
+
141
+ /**
142
+ * Design B root-key injection: place `OCX_SECTION_MARKER` + `openai_base_url` at the document
143
+ * ROOT (before the first table header). Idempotent: an existing marker-owned line is rewritten
144
+ * in place. A user's OWN root `openai_base_url` (no marker above it) is respected — we keep it
145
+ * and inject nothing, reporting `keptUserBaseUrl` so the caller can surface it.
146
+ */
147
+ export function setRootOpenaiBaseUrl(content: string, port: number, hostname?: string): { content: string; keptUserBaseUrl: boolean } {
148
+ const lines = content.split("\n");
149
+ const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
150
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
151
+ const key = buildOpenaiBaseUrlLine(port, hostname);
152
+
153
+ for (let i = 0; i < rootEnd; i++) {
154
+ if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
155
+ const markerOwned = i > 0 && lines[i - 1].includes(OCX_SECTION_MARKER);
156
+ if (!markerOwned) return { content, keptUserBaseUrl: true };
157
+ lines[i] = key;
158
+ return { content: lines.join("\n"), keptUserBaseUrl: false };
159
+ }
160
+
161
+ if (firstTable === -1) {
162
+ return { content: content.replace(/\n+$/, "") + "\n" + OCX_SECTION_MARKER + "\n" + key + "\n", keptUserBaseUrl: false };
163
+ }
164
+ let insertAt = firstTable;
165
+ while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
166
+ lines.splice(insertAt, 0, OCX_SECTION_MARKER, key);
167
+ return { content: lines.join("\n"), keptUserBaseUrl: false };
168
+ }
169
+
170
+ /**
171
+ * Remove the marker-owned root `openai_base_url` (marker line + the key line right after it).
172
+ * A user's own root override (no marker) survives; an orphaned marker with no key line after
173
+ * it is dropped too so repeated strip/inject cycles cannot accumulate marker comments.
174
+ */
175
+ export function stripInjectedOpenaiBaseUrl(content: string): string {
176
+ const lines = content.split("\n");
177
+ const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
178
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
179
+ const drop = new Set<number>();
180
+ for (let i = 0; i < rootEnd; i++) {
181
+ if (!lines[i].includes(OCX_SECTION_MARKER)) continue;
182
+ if (i + 1 < rootEnd && isRootOpenaiBaseUrlLine(lines[i + 1])) {
183
+ drop.add(i);
184
+ drop.add(i + 1);
185
+ } else if (i + 1 >= rootEnd || lines[i + 1].trim() === "") {
186
+ drop.add(i); // orphaned marker at root
187
+ }
188
+ }
189
+ if (drop.size === 0) return content;
190
+ return lines.filter((_, i) => !drop.has(i)).join("\n");
191
+ }
192
+
193
+ export type CodexRoutingKind = "native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown";
194
+
195
+ type RoutingEndpointKind = "local" | "remote" | "unknown";
196
+
197
+ function ipv4Octets(hostname: string): number[] | null {
198
+ const dotted = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
199
+ if (dotted) {
200
+ const octets = dotted.slice(1).map(Number);
201
+ return octets.some(octet => octet > 255) ? null : octets;
202
+ }
203
+ const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(hostname);
204
+ if (!mapped) return null;
205
+ const high = Number.parseInt(mapped[1], 16);
206
+ const low = Number.parseInt(mapped[2], 16);
207
+ return [high >>> 8, high & 0xff, low >>> 8, low & 0xff];
208
+ }
209
+
210
+ function classifyRoutingEndpoint(value: string): RoutingEndpointKind {
211
+ try {
212
+ const url = new URL(value);
213
+ if (url.protocol !== "http:" && url.protocol !== "https:") return "unknown";
214
+ const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
215
+ if (!hostname) return "unknown";
216
+ if (hostname === "localhost" || hostname.endsWith(".localhost")) return "local";
217
+ if (hostname === "::" || hostname === "::1" || hostname === "0.0.0.0") return "local";
218
+ const octets = ipv4Octets(hostname);
219
+ if (octets) {
220
+ if (octets.every(octet => octet === 0)) return "local";
221
+ if (octets[0] === 127) return "local";
222
+ return "remote";
223
+ }
224
+ if (/^::ffff:/i.test(hostname)) return "unknown";
225
+ return "remote";
226
+ } catch {
227
+ return "unknown";
228
+ }
229
+ }
230
+
231
+ /** Classify actual routing dependency separately from opencodex ownership. */
232
+ export function classifyCodexRouting(content: string): CodexRoutingKind {
233
+ const rootBaseUrl = rootTomlString(content, "openai_base_url");
234
+ if (rootBaseUrl) {
235
+ const endpoint = classifyRoutingEndpoint(rootBaseUrl);
236
+ if (endpoint === "unknown") return "unknown";
237
+ if (hasInjectedOpenaiBaseUrl(content)) return "opencodex-local";
238
+ return endpoint === "local" ? "custom-local" : "custom-remote";
239
+ }
240
+ const rootProvider = rootTomlString(content, "model_provider");
241
+ if (rootProvider) {
242
+ const providerTableExists = providerTableStart(content.split("\n"), rootProvider) !== -1;
243
+ const providerBaseUrl = providerTableString(content, rootProvider, "base_url");
244
+ if (providerBaseUrl) {
245
+ const endpoint = classifyRoutingEndpoint(providerBaseUrl);
246
+ if (endpoint === "unknown") return "unknown";
247
+ if (rootProvider === "opencodex") return "opencodex-local";
248
+ return endpoint === "local" ? "custom-local" : "custom-remote";
249
+ }
250
+ if (rootProvider === "opencodex" || providerTableExists || rootProvider !== "openai") return "unknown";
251
+ }
252
+ return "native";
253
+ }
254
+
255
+ /** Read-only probe used by status, doctor, and the dashboard. */
256
+ export function isCodexRoutingInjected(): boolean {
257
+ const path = CODEX_CONFIG_PATH;
258
+ if (!existsSync(path)) return false;
259
+ try {
260
+ return hasInjectedCodexRouting(readFileSync(path, "utf8"));
261
+ } catch {
262
+ return false;
263
+ }
264
+ }
265
+
266
+ export function getCodexRoutingKind(): CodexRoutingKind {
267
+ const path = CODEX_CONFIG_PATH;
268
+ if (!existsSync(path)) return "native";
269
+ try {
270
+ return classifyCodexRouting(readFileSync(path, "utf8"));
271
+ } catch {
272
+ return "unknown";
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Strip every existing `model_provider` line that we must not duplicate: any line set to
278
+ * "opencodex" (wherever it sits — including a previously mis-nested one under a table), plus any
279
+ * ROOT-level model_provider (before the first table) of any value, since we override the global.
280
+ * A `model_provider` legitimately inside a user table/profile with a non-opencodex value is left
281
+ * untouched.
282
+ */
283
+ function stripExistingModelProvider(content: string): string {
284
+ const lines = content.split("\n");
285
+ const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
286
+ const out: string[] = [];
287
+ lines.forEach((line, i) => {
288
+ if (/^\s*model_provider\s*=/.test(line)) {
289
+ const isOurs = /^\s*model_provider\s*=\s*"opencodex"\s*$/.test(line);
290
+ const isRoot = firstTable === -1 || i < firstTable;
291
+ if (isOurs || isRoot) return; // drop it
292
+ }
293
+ out.push(line);
294
+ });
295
+ return out.join("\n");
296
+ }
297
+
298
+ /**
299
+ * Drop ROOT-level `model_context_window` overrides (keys before the first table header). Codex
300
+ * treats this root key as a global override that wins over the per-model catalog values, so a stale
301
+ * `model_context_window = 1000000` makes every model (e.g. gpt-5.5) report a 1M window. User-owned
302
+ * compaction limits do not alter the advertised context window and must survive reinjection.
303
+ */
304
+ export function stripRootContextWindowOverrides(content: string): string {
305
+ const lines = content.split("\n");
306
+ const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
307
+ return lines
308
+ .filter((line, i) => {
309
+ const isRoot = firstTable === -1 || i < firstTable;
310
+ return !isRoot || !/^\s*model_context_window\s*=/.test(line);
311
+ })
312
+ .join("\n");
313
+ }
314
+
315
+ function stripRootRoutedModel(content: string): string {
316
+ const lines = content.split("\n");
317
+ const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
318
+ return lines
319
+ .filter((line, i) => {
320
+ const isRoot = firstTable === -1 || i < firstTable;
321
+ if (!isRoot) return true;
322
+ const m = line.match(/^\s*model\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/);
323
+ if (!m) return true;
324
+ const model = parseTomlString(m[1]);
325
+ return !model?.includes("/");
326
+ })
327
+ .join("\n");
328
+ }
329
+
330
+ /**
331
+ * Insert `model_provider = "opencodex"` at the document ROOT — immediately before the first table
332
+ * header (TOML root keys must precede all tables). If there are no tables, append it to the root body.
333
+ */
334
+ function setRootModelProvider(content: string): string {
335
+ const lines = content.split("\n");
336
+ const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
337
+ const key = 'model_provider = "opencodex"';
338
+ if (firstTable === -1) {
339
+ return content.replace(/\n+$/, "") + "\n" + key + "\n";
340
+ }
341
+ let insertAt = firstTable;
342
+ while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
343
+ lines.splice(insertAt, 0, key);
344
+ return lines.join("\n");
345
+ }
346
+
347
+ function readRootModelCatalogPath(content: string): string | null {
348
+ return readRootTomlString(content, "model_catalog_json");
349
+ }
350
+
351
+ function setRootModelCatalogPath(content: string, catalogPath: string): string {
352
+ const lines = content.split("\n");
353
+ const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
354
+ const key = `model_catalog_json = ${tomlString(catalogPath)}`;
355
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
356
+ for (let i = 0; i < rootEnd; i++) {
357
+ const m = lines[i].match(/^\s*model_catalog_json\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/);
358
+ if (!m) continue;
359
+ const existing = parseTomlString(m[1]);
360
+ if (isOpencodexCatalogPath(existing)) {
361
+ lines[i] = key;
362
+ return lines.join("\n");
363
+ }
364
+ return content;
365
+ }
366
+ if (firstTable === -1) {
367
+ return content.replace(/\n+$/, "") + "\n" + key + "\n";
368
+ }
369
+ let insertAt = firstTable;
370
+ while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
371
+ lines.splice(insertAt, 0, key);
372
+ return lines.join("\n");
373
+ }
374
+
375
+ function removeProfileSection(content: string): string {
376
+ const lines = content.split("\n");
377
+ const filtered: string[] = [];
378
+ let inProfile = false;
379
+ for (const line of lines) {
380
+ if (line.trim() === "[profiles.opencodex]") {
381
+ inProfile = true;
382
+ continue;
383
+ }
384
+ if (inProfile) {
385
+ if (/^\s*\[/.test(line) && line.trim() !== "[profiles.opencodex]") {
386
+ inProfile = false;
387
+ filtered.push(line);
388
+ }
389
+ continue;
390
+ }
391
+ filtered.push(line);
392
+ }
393
+ return filtered.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
394
+ }
395
+
396
+ function normalizeServiceTier(content: string): string {
397
+ return content.replace(/^(\s*service_tier\s*=\s*)["']priority["']\s*$/gm, '$1"fast"');
398
+ }
399
+
400
+ function ensureFastModeFeature(content: string): string {
401
+ const lines = content.split("\n");
402
+ const featuresStart = lines.findIndex(line => line.trim() === "[features]");
403
+ if (featuresStart === -1) {
404
+ return content.trimEnd() + "\n\n[features]\nfast_mode = true\n";
405
+ }
406
+
407
+ const nextTable = lines.findIndex((line, index) => index > featuresStart && /^\s*\[/.test(line));
408
+ const featuresEnd = nextTable === -1 ? lines.length : nextTable;
409
+ for (let i = featuresStart + 1; i < featuresEnd; i++) {
410
+ if (/^\s*fast_mode\s*=/.test(lines[i])) {
411
+ lines[i] = lines[i].replace(/^(\s*)fast_mode\s*=.*$/, "$1fast_mode = true");
412
+ return lines.join("\n");
413
+ }
414
+ }
415
+
416
+ let insertAt = featuresEnd;
417
+ while (insertAt > featuresStart + 1 && lines[insertAt - 1].trim() === "") insertAt--;
418
+ lines.splice(insertAt, 0, "fast_mode = true");
419
+ return lines.join("\n");
420
+ }
421
+
422
+ function isOpencodexCatalogPath(path: string): boolean {
423
+ return path.replace(/\\/g, "/").split("/").pop() === "opencodex-catalog.json";
424
+ }
425
+
426
+ function stripOpencodexCatalogPath(content: string): string {
427
+ return content
428
+ .split("\n")
429
+ .filter(line => {
430
+ const m = line.match(/^\s*model_catalog_json\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/);
431
+ return !m || !isOpencodexCatalogPath(parseTomlString(m[1]));
432
+ })
433
+ .join("\n");
434
+ }
435
+
436
+ export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string): string {
437
+ const host = providerBaseHost(hostname);
438
+ // Design B (loopback): the reference/fallback file documents the root override form.
439
+ // Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
440
+ // the x-opencodex-api-key env header).
441
+ if (!includeApiAuthHeader) {
442
+ const lines = [
443
+ "# OpenCodex proxy fallback config (Design B)",
444
+ `# Root override that points Codex's built-in openai provider at the proxy on ${host}:${port}.`,
445
+ "# Merge these root keys into ~/.codex/config.toml manually if auto-injection was removed.",
446
+ buildOpenaiBaseUrlLine(port, hostname),
447
+ ];
448
+ if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
449
+ lines.push("", "[features]", "fast_mode = true", "");
450
+ return lines.join("\n");
451
+ }
452
+ const lines = [
453
+ "# OpenCodex proxy profile — use with: codex --profile opencodex",
454
+ `# Routes all model requests through the opencodex proxy at ${host}:${port}`,
455
+ 'model_provider = "opencodex"',
456
+ ];
457
+ if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
458
+ lines.push("", "[features]", "fast_mode = true");
459
+ lines.push(buildProviderTableBlock(port, supportsWebsockets, includeApiAuthHeader, hostname).trimEnd(), "");
460
+ return lines.join("\n");
461
+ }
462
+
463
+ export function chooseCatalogPathForInjection(content: string, requested?: string | null): string | null {
464
+ if (requested !== undefined) return requested;
465
+
466
+ const existing = readRootModelCatalogPath(content);
467
+ if (existing) {
468
+ const resolved = resolveCodexConfigPath(existing);
469
+ if (!isOpencodexCatalogPath(resolved) || existsSync(resolved)) return existing;
470
+ }
471
+
472
+ return existsSync(DEFAULT_CATALOG_PATH) ? DEFAULT_CATALOG_PATH : null;
473
+ }
474
+
475
+ export interface CodexInjectResult {
476
+ success: boolean;
477
+ message: string;
478
+ nativeSubagentDefaultsWarning?: string;
479
+ }
480
+
481
+ export async function injectCodexConfig(port: number, config?: OcxConfig, options: InjectCodexOptions = {}): Promise<CodexInjectResult> {
482
+ if (!existsSync(CODEX_CONFIG_PATH)) {
483
+ return { success: false, message: `Codex config not found at ${CODEX_CONFIG_PATH}. Is Codex installed?` };
484
+ }
485
+
486
+ const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
487
+ const activeProvider = externalCodexModelProvider(rawContent);
488
+ if (activeProvider) {
489
+ // A launcher may have journaled before the provider manager took ownership. Never let shutdown
490
+ // replay that stale snapshot over externally managed config.
491
+ removeJournal();
492
+ const nativeSubagentDefaultsWarning = configuredManagedSubagentDefaults(config)
493
+ ? `Native Codex sub-agent defaults were not injected: external model_provider ${tomlString(activeProvider)} owns config.toml.`
494
+ : undefined;
495
+ return {
496
+ success: true,
497
+ ...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
498
+ message: `⚠️ Codex routing NOT injected: config.toml selects the external model_provider ${tomlString(activeProvider)}.\n` +
499
+ ` OpenCodex preserves external provider configuration so existing ${tomlString(activeProvider)} session history stays visible.\n` +
500
+ ` Configure that provider for Responses passthrough at http://${providerBaseHost(config?.hostname)}:${port}/v1` +
501
+ `${shouldInjectApiAuthHeader(config) ? ` with x-opencodex-api-key from OPENCODEX_API_AUTH_TOKEN` : ""}.\n` +
502
+ ` For direct injection, switch to the built-in openai provider, remove any user-owned root openai_base_url, and rerun 'ocx start'.`,
503
+ };
504
+ }
505
+
506
+ // Marker-owned native defaults are OpenCodex residue, never part of the
507
+ // user's journal baseline. Clean them before either snapshotting or adding a
508
+ // root routing key: inserting that key ahead of a marker-owned first table
509
+ // would otherwise separate the table marker from its header. Ambiguous
510
+ // markers fail closed without writing config, profile, or journal state.
511
+ const nativeDefaultsBaseline = transformManagedSubagentDefaults(rawContent, null);
512
+ if (!nativeDefaultsBaseline.ok) {
513
+ return {
514
+ success: false,
515
+ message: `Codex config injection refused: existing OpenCodex-managed native sub-agent defaults are ambiguous: ${nativeDefaultsBaseline.error}. `
516
+ + `No files were changed; inspect ${CODEX_CONFIG_PATH}.`,
517
+ };
518
+ }
519
+ const baselineContent = nativeDefaultsBaseline.content;
520
+
521
+ // Classify and journal the same bytes: a native config is a valid original and
522
+ // supersedes a stale snapshot (#477), while an injected one must never become
523
+ // one — that is how opencodex routing would survive `ocx stop`.
524
+ writeJournal({
525
+ currentStateIsNative: !hasInjectedCodexRouting(rawContent),
526
+ configContent: baselineContent,
527
+ });
528
+ // EOL boundary: transforms below are LF-pure; preserve the file's dominant ending on write.
529
+ const eol = dominantEol(rawContent);
530
+ let content = applyEol(baselineContent, "\n");
531
+
532
+ // Idempotent clean-up of any prior injection: drop the provider table (marker-based) and every
533
+ // stray/mis-nested model_provider line, so re-injecting can't duplicate keys or leave the buggy
534
+ // table-nested key behind.
535
+ // Design B form FIRST: removeOcxSection also keys on the marker line, so a root-level
536
+ // marker + openai_base_url pair must be gone before it scans or it would swallow root keys.
537
+ content = stripInjectedOpenaiBaseUrl(content);
538
+ if (content.includes("[model_providers.opencodex]")) {
539
+ content = removeOcxSection(content);
540
+ }
541
+ content = removeProfileSection(content);
542
+ content = stripExistingModelProvider(content);
543
+ content = stripRootContextWindowOverrides(content);
544
+ content = normalizeServiceTier(content);
545
+ content = ensureFastModeFeature(content);
546
+
547
+ const catalogPath = chooseCatalogPathForInjection(content, options.catalogPath);
548
+ content = catalogPath ? setRootModelCatalogPath(content, catalogPath) : stripOpencodexCatalogPath(content);
549
+
550
+ const legacyMode = shouldInjectApiAuthHeader(config);
551
+ let keptUserBaseUrl = false;
552
+ if (legacyMode) {
553
+ // Legacy (non-loopback) injection: the built-in openai provider cannot carry the
554
+ // x-opencodex-api-key env header, so keep the opencodex provider table + root re-tag.
555
+ // 1) Root key BEFORE the first table header (must be a global, not nested under a table).
556
+ content = setRootModelProvider(content);
557
+ // 2) Provider table appended at EOF (position-independent).
558
+ content = content.trimEnd() + "\n" + buildProviderTableBlock(port, websocketsEnabled(config ?? {}), true, config?.hostname);
559
+ } else {
560
+ // Design B (loopback): a single root override; codex keeps its native `openai` provider id
561
+ // so thread history is never remapped. Any legacy form was already stripped above.
562
+ content = stripInjectedOpenaiBaseUrl(content); // normalize before idempotent re-insert
563
+ const result = setRootOpenaiBaseUrl(content, port, config?.hostname);
564
+ content = result.content;
565
+ keptUserBaseUrl = result.keptUserBaseUrl;
566
+ }
567
+
568
+ const desiredSubagentDefaults = configuredManagedSubagentDefaults(config);
569
+ const routingOwnershipWarning = keptUserBaseUrl && desiredSubagentDefaults
570
+ ? "Native Codex sub-agent defaults were not injected: a user-owned root openai_base_url prevents OpenCodex from managing active Codex routing."
571
+ : undefined;
572
+ const managedDefaults = transformManagedSubagentDefaults(
573
+ content,
574
+ keptUserBaseUrl ? null : desiredSubagentDefaults,
575
+ );
576
+ let nativeSubagentDefaultsWarning = routingOwnershipWarning;
577
+ let managedDefaultsMessage = routingOwnershipWarning ? ` ⚠️ ${routingOwnershipWarning}\n` : "";
578
+ if (managedDefaults.ok) {
579
+ content = managedDefaults.content;
580
+ if (desiredSubagentDefaults && managedDefaults.conflicts.length > 0) {
581
+ const keys = managedDefaults.conflicts.map(conflict => `agents.${conflict.key}`).join(", ");
582
+ nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults were not injected: user-owned ${keys} preserved.`;
583
+ managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
584
+ }
585
+ } else {
586
+ const action = desiredSubagentDefaults && !keptUserBaseUrl
587
+ ? "were not injected"
588
+ : "could not be safely removed";
589
+ nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults ${action}: ${managedDefaults.error}.`;
590
+ managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
591
+ }
592
+
593
+ const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname);
594
+ content = applyEol(content, eol);
595
+ atomicWriteFile(CODEX_CONFIG_PATH, content);
596
+ atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
597
+ markJournalInjectedState(content, profileContent);
598
+ // Legacy mode still forward-tags history so re-tagged threads stay listable. Design B needs
599
+ // the opposite: a one-time migration of previously re-tagged threads BACK to openai (restore
600
+ // machinery; cheap no-op when there is nothing to migrate).
601
+ const history = config?.syncResumeHistory !== false
602
+ ? (legacyMode ? syncCodexHistoryProvider("opencodex") : migrateHistoryToOpenai())
603
+ : { rows: 0, files: 0 };
604
+
605
+ const catalogMessage = catalogPath
606
+ ? ` Codex model catalog: ${catalogPath}\n`
607
+ : ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`;
608
+ const migratedRows = (history.rows ?? 0) + ("ejectedRows" in history ? history.ejectedRows ?? 0 : 0);
609
+ const historyMessage = config?.syncResumeHistory === false
610
+ ? ` Codex resume history: left unchanged (syncResumeHistory=false).\n`
611
+ : history.failed
612
+ ? (legacyMode
613
+ ? ` ⚠️ Codex resume history sync SKIPPED: the history DB is locked (Codex app/IDE open?). Close it and rerun 'ocx start'.\n`
614
+ // Honest in every caller context: the daemon retries in the background while it runs,
615
+ // and this inject path re-runs the migration on every future start/sync anyway.
616
+ : ` ⚠️ Codex resume history migration deferred: the history DB is locked (Codex app/IDE open?). It is retried automatically (while the proxy runs and on every 'ocx start'); to force it now, close the Codex app and run 'ocx sync'.\n`)
617
+ : legacyMode
618
+ ? ` Codex resume history: ${history.rows} thread(s) made visible for opencodex; originals backed up for restore.\n`
619
+ : migratedRows > 0
620
+ ? ` Codex resume history: ${migratedRows} legacy opencodex-tagged thread(s) migrated back to openai (one-time).\n`
621
+ : ` Codex resume history: untouched (threads keep their native openai tag).\n`;
622
+ // A user-owned root openai_base_url means we did NOT install routing — say so honestly
623
+ // instead of claiming the proxy route is active (catalog/fast_mode were still written).
624
+ if (keptUserBaseUrl) {
625
+ return {
626
+ success: true,
627
+ ...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
628
+ message: `⚠️ Codex routing NOT injected: your config already sets a root openai_base_url, and opencodex never overwrites a user-owned override.\n` +
629
+ catalogMessage +
630
+ historyMessage +
631
+ managedDefaultsMessage +
632
+ ` To route plain codex through the proxy, remove your openai_base_url line from ~/.codex/config.toml and rerun 'ocx start'.\n` +
633
+ ` Reference config: ${CODEX_PROFILE_PATH}`,
634
+ };
635
+ }
636
+ const headline = legacyMode
637
+ ? `Injected opencodex as default provider into Codex config.\n`
638
+ : `Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url).\n`;
639
+ return {
640
+ success: true,
641
+ ...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
642
+ message: headline +
643
+ catalogMessage +
644
+ historyMessage +
645
+ managedDefaultsMessage +
646
+ ` All models now route through opencodex proxy (like OpenRouter).\n` +
647
+ ` OpenAI models (gpt-5.5, etc.) are passed through to OpenAI.\n` +
648
+ ` Custom models route to their configured providers.\n` +
649
+ (legacyMode
650
+ ? ` Fallback: codex --profile opencodex (same behavior)`
651
+ : ` Fallback reference: ${CODEX_PROFILE_PATH}`),
652
+ };
653
+ }
654
+
655
+ function removeOcxSection(content: string): string {
656
+ const lines = content.split("\n");
657
+ const filtered: string[] = [];
658
+ let inOcxSection = false;
659
+ for (const line of lines) {
660
+ if (line.includes(OCX_SECTION_MARKER) || line.trim() === "[model_providers.opencodex]") {
661
+ inOcxSection = true;
662
+ continue;
663
+ }
664
+ if (inOcxSection) {
665
+ // End the injected section at the next table header that ISN'T our own — exact match so a
666
+ // user's "[model_providers.opencodex_backup]" (or similar) is preserved, not swallowed.
667
+ if (/^\s*\[/.test(line) && line.trim() !== "[model_providers.opencodex]") {
668
+ inOcxSection = false;
669
+ filtered.push(line);
670
+ }
671
+ continue;
672
+ }
673
+ filtered.push(line);
674
+ }
675
+ return filtered.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
676
+ }
677
+
678
+ interface StripOpencodexConfigResult {
679
+ content: string;
680
+ managedDefaultsError: string | null;
681
+ }
682
+
683
+ /**
684
+ * Detailed form used by the on-disk restore path. A damaged ownership marker is
685
+ * ambiguous: keep the associated value, but return the transform error so the
686
+ * caller cannot report a complete restore.
687
+ */
688
+ function stripOpencodexConfigResult(content: string): StripOpencodexConfigResult {
689
+ let out = content;
690
+ const hadRootOcxProvider = readRootTomlString(out, "model_provider") === "opencodex";
691
+ const hadInjectedBaseUrl = hasInjectedOpenaiBaseUrl(out);
692
+ out = stripInjectedOpenaiBaseUrl(out); // before removeOcxSection — it keys on the marker line too
693
+ if (out.includes("[model_providers.opencodex]")) {
694
+ out = removeOcxSection(out);
695
+ }
696
+ out = removeProfileSection(out);
697
+ // Regex (not exact-string) removal so compact `model_provider="opencodex"` is stripped too —
698
+ // must match the detection regex above, or a detected line could survive un-removed.
699
+ out = out.split("\n").filter(l => !/^\s*model_provider\s*=\s*"opencodex"\s*$/.test(l)).join("\n");
700
+ // Routed root model ids (`model = "provider/slug"`) only make sense while the proxy serves
701
+ // them — strip on both the legacy re-tag form and the Design B injected-base-url form.
702
+ if (hadRootOcxProvider || hadInjectedBaseUrl) out = stripRootRoutedModel(out);
703
+ const managedDefaults = transformManagedSubagentDefaults(out, null);
704
+ if (managedDefaults.ok) out = managedDefaults.content;
705
+ out = stripOpencodexCatalogPath(out);
706
+ return {
707
+ content: out.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n",
708
+ managedDefaultsError: !managedDefaults.ok ? managedDefaults.error : null,
709
+ };
710
+ }
711
+
712
+ /** Pure transform: strip the opencodex provider block + `model_provider = "opencodex"` lines. */
713
+ export function stripOpencodexConfig(content: string): string {
714
+ return stripOpencodexConfigResult(content).content;
715
+ }
716
+
717
+ function hasOpencodexRouting(content: string): boolean {
718
+ return content.includes("[model_providers.opencodex]")
719
+ || /^\s*model_provider\s*=\s*"opencodex"/m.test(content)
720
+ || hasInjectedOpenaiBaseUrl(content);
721
+ }
722
+
723
+ export function removeCodexConfig(options: { preserveProfile?: boolean } = {}): { success: boolean; message: string } {
724
+ if (!existsSync(CODEX_CONFIG_PATH)) {
725
+ if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH)) unlinkSync(CODEX_PROFILE_PATH);
726
+ return {
727
+ success: true,
728
+ message: `Codex config not found; no native restore was needed${options.preserveProfile ? "." : ", and the opencodex profile was removed if present."}`,
729
+ };
730
+ }
731
+ const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
732
+ // Same EOL boundary as inject: strip in LF space, write back in the file's own ending.
733
+ // The unchanged fast path compares in LF space so an untouched file is never rewritten.
734
+ const eol = dominantEol(rawContent);
735
+ const content = applyEol(rawContent, "\n");
736
+ const had = hasOpencodexRouting(content);
737
+ const stripped = stripOpencodexConfigResult(content);
738
+ if (had || stripped.content !== content) {
739
+ atomicWriteFile(CODEX_CONFIG_PATH, applyEol(stripped.content, eol));
740
+ }
741
+ if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH)) unlinkSync(CODEX_PROFILE_PATH);
742
+ const removedMessage = had
743
+ ? `Removed opencodex routing from Codex config${options.preserveProfile ? "." : " + profile."}`
744
+ : "opencodex not present in Codex config.";
745
+ if (stripped.managedDefaultsError) {
746
+ const routingMessage = had ? removedMessage : "No opencodex routing was present in Codex config.";
747
+ return {
748
+ success: false,
749
+ message: `${routingMessage} Native Codex sub-agent defaults could not be safely removed: ${stripped.managedDefaultsError}. ` +
750
+ "The ambiguous marker and adjacent value were preserved; inspect $CODEX_HOME/config.toml before using native Codex.",
751
+ };
752
+ }
753
+ return {
754
+ success: true,
755
+ message: removedMessage,
756
+ };
757
+ }
758
+
759
+ /**
760
+ * Recover native Codex: strip opencodex from config.toml AND drop proxy-routed catalog entries,
761
+ * so plain `codex` works when the proxy is stopped. Called by `ocx stop`, the proxy shutdown
762
+ * handler, and `ocx restore`. Idempotent + atomic.
763
+ */
764
+ export function restoreNativeCodex(): { success: boolean; message: string } {
765
+ const activeProvider = currentExternalCodexModelProvider();
766
+ if (activeProvider) {
767
+ removeJournal();
768
+ return { success: true, message: `External Codex provider ${tomlString(activeProvider)} preserved; no native restore was needed.` };
769
+ }
770
+ const journal = restoreJournalState();
771
+ const cfg = journal.configRestored
772
+ ? { success: true, message: "Codex config restored from opencodex journal." }
773
+ : removeCodexConfig({ preserveProfile: journal.profileRestored || journal.profileChanged });
774
+ const cat = restoreCodexCatalog();
775
+ // Design B (loopback) steady state: threads are already tagged openai, so prove the
776
+ // no-op with a readonly probe instead of write-opening a DB the Codex app may hold
777
+ // (Windows: WAL writer lock -> seconds of stalling + a false warning on every stop).
778
+ // Legacy (non-loopback) installs keep the unconditional write-open restore.
779
+ let skipWhenProvablyNoop = false;
780
+ try {
781
+ skipWhenProvablyNoop = !shouldInjectApiAuthHeader(loadConfig());
782
+ } catch { /* unreadable config: keep the conservative write-open restore */ }
783
+ const history = syncCodexHistoryProvider("openai", undefined, undefined, { skipWhenProvablyNoop });
784
+ const msg = cat.removed > 0
785
+ ? `${cfg.message} Catalog restored to ${cat.kept} native model(s) (dropped ${cat.removed} proxy-routed).`
786
+ : cfg.message;
787
+ const historyMsg = history.failed
788
+ ? ` ⚠️ Codex resume history could NOT be restored — the Codex app appears to be holding the history DB. Close the Codex app/IDE and run 'ocx stop' again; until then routed threads stay hidden in the native app.`
789
+ : history.rows > 0
790
+ ? ` Resume history restored from opencodex backup (${history.rows} thread(s)).`
791
+ : history.ejectedRows
792
+ ? ` ${history.ejectedRows} opencodex history thread(s) were ejected to openai so native Codex can resume them.`
793
+ : "";
794
+ return { success: cfg.success, message: `${msg}${historyMsg}` };
795
+ }
796
+
797
+ export function getCodexConfigPath(): string {
798
+ return CODEX_CONFIG_PATH;
799
+ }