@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,72 @@
1
+ /**
2
+ * Ownership predicates for `~/.codex/config.toml`: does opencodex own the routing
3
+ * currently written there?
4
+ *
5
+ * These live in their own leaf module rather than in `inject.ts` because
6
+ * `journal.ts` needs them and `inject.ts` already imports `journal.ts`. Keeping
7
+ * them here breaks that cycle. `inject.ts` imports them back and re-exports the
8
+ * two public predicates, so external callers see no change.
9
+ */
10
+ export const OCX_SECTION_MARKER = "# Auto-injected by opencodex";
11
+
12
+ export function isRootOpenaiBaseUrlLine(line: string): boolean {
13
+ return /^\s*openai_base_url\s*=/.test(line);
14
+ }
15
+
16
+ export function tomlStringPattern(key: string): RegExp {
17
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18
+ const keyToken = `(?:${escaped}|"${escaped}"|'${escaped}')`;
19
+ return new RegExp(`^\\s*${keyToken}\\s*=\\s*["']([^"']+)["']\\s*(?:#.*)?$`);
20
+ }
21
+
22
+ export function rootTomlString(content: string, key: string): string | null {
23
+ const lines = content.split("\n");
24
+ const firstTable = lines.findIndex(line => /^\s*\[/.test(line));
25
+ const rootLines = lines.slice(0, firstTable === -1 ? lines.length : firstTable);
26
+ const pattern = tomlStringPattern(key);
27
+ for (const line of rootLines) {
28
+ const match = pattern.exec(line);
29
+ if (match?.[1]) return match[1].trim();
30
+ }
31
+ return null;
32
+ }
33
+
34
+ export function providerTableStart(lines: string[], provider: string): number {
35
+ const escapedProvider = provider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
36
+ const providerToken = `(?:${escapedProvider}|"${escapedProvider}"|'${escapedProvider}')`;
37
+ const header = new RegExp(`^\\s*\\[\\s*(?:model_providers|"model_providers"|'model_providers')\\s*\\.\\s*${providerToken}\\s*\\]\\s*(?:#.*)?$`);
38
+ return lines.findIndex(line => header.test(line));
39
+ }
40
+
41
+ export function providerTableString(content: string, provider: string, key: string): string | null {
42
+ const lines = content.split("\n");
43
+ const start = providerTableStart(lines, provider);
44
+ if (start === -1) return null;
45
+ const pattern = tomlStringPattern(key);
46
+ for (let index = start + 1; index < lines.length && !/^\s*\[/.test(lines[index]); index += 1) {
47
+ const match = pattern.exec(lines[index]);
48
+ if (match?.[1]) return match[1].trim();
49
+ }
50
+ return null;
51
+ }
52
+
53
+ export function hasInjectedOpenaiBaseUrl(content: string): boolean {
54
+ const lines = content.split("\n");
55
+ const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
56
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
57
+ for (let i = 1; i < rootEnd; i++) {
58
+ if (isRootOpenaiBaseUrlLine(lines[i]!) && lines[i - 1]!.includes(OCX_SECTION_MARKER)) return true;
59
+ }
60
+ return false;
61
+ }
62
+
63
+ /**
64
+ * True when the active Codex config is owned by opencodex routing. Covers the
65
+ * loopback Design B root override and the legacy/non-loopback provider table.
66
+ * A user-owned `openai_base_url` is intentionally not classified as injected.
67
+ */
68
+ export function hasInjectedCodexRouting(content: string): boolean {
69
+ if (hasInjectedOpenaiBaseUrl(content)) return true;
70
+ return rootTomlString(content, "model_provider") === "opencodex"
71
+ && providerTableString(content, "opencodex", "base_url") !== null;
72
+ }
@@ -0,0 +1,163 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { atomicWriteFile } from "../config";
5
+ import { hasInjectedCodexRouting } from "./injected-marker";
6
+ import { CODEX_HOME, CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "./paths";
7
+
8
+ const JOURNAL_PATH = join(CODEX_HOME, "opencodex-journal.json");
9
+
10
+ interface Journal {
11
+ version: 1;
12
+ originalConfig: string;
13
+ originalProfile: string | null;
14
+ injectedConfigHash?: string;
15
+ injectedProfileHash?: string | null;
16
+ pid: number;
17
+ timestamp: string;
18
+ }
19
+
20
+ interface RestoreJournalResult {
21
+ configRestored: boolean;
22
+ profileRestored: boolean;
23
+ configChanged: boolean;
24
+ profileChanged: boolean;
25
+ complete: boolean;
26
+ }
27
+
28
+ function sha256(content: string | null): string | null {
29
+ return content === null ? null : createHash("sha256").update(content).digest("hex");
30
+ }
31
+
32
+ export interface WriteJournalOptions {
33
+ /**
34
+ * The caller's verdict on the config it is about to transform: false when
35
+ * `hasInjectedCodexRouting` matched. This does NOT decide whether the content
36
+ * may be journaled — that is checked below, from the bytes themselves. It only
37
+ * authorizes REPLACING an existing snapshot, which is why omitting it still
38
+ * allows a first snapshot but never an overwrite.
39
+ */
40
+ currentStateIsNative?: boolean;
41
+ /**
42
+ * The exact bytes the caller classified. Journaling these rather than re-reading
43
+ * the file keeps the snapshot and the verdict describing the same content when
44
+ * another process rewrites config.toml mid-flight.
45
+ */
46
+ configContent?: string;
47
+ }
48
+
49
+ /**
50
+ * Snapshot the pre-injection Codex state.
51
+ *
52
+ * Only native (non-opencodex-owned) config may be journaled, and native config
53
+ * always supersedes an older snapshot. The first half stops a re-inject from
54
+ * recording opencodex's own routing as the user's original — which would survive
55
+ * `ocx stop` and make the injection unremovable. The second half is the #477 fix:
56
+ * without it the first snapshot a machine ever takes is the only one it ever has,
57
+ * so an unclean shutdown days later replays a day-one config over the user's
58
+ * plugins, model choice, and trusted projects.
59
+ */
60
+ export function writeJournal(options: WriteJournalOptions = {}): void {
61
+ if (!existsSync(CODEX_CONFIG_PATH)) return;
62
+ const config = options.configContent ?? readFileSync(CODEX_CONFIG_PATH, "utf-8");
63
+ // Ownership is decided HERE, from the bytes about to be journaled — never taken
64
+ // on the caller's word. A caller that says "native" about injected content would
65
+ // otherwise make opencodex's own routing the user's permanent "original".
66
+ if (hasInjectedCodexRouting(config)) return;
67
+ // The caller's verdict only authorizes REPLACEMENT. It is weaker evidence than
68
+ // the check above (it may describe bytes read a moment earlier), so an
69
+ // unclassified call creates a first snapshot but never overwrites one.
70
+ if (existsSync(JOURNAL_PATH) && readJournal() && options.currentStateIsNative !== true) return;
71
+ const profile = existsSync(CODEX_PROFILE_PATH)
72
+ ? readFileSync(CODEX_PROFILE_PATH, "utf-8")
73
+ : null;
74
+ const journal: Journal = {
75
+ version: 1,
76
+ originalConfig: Buffer.from(config).toString("base64"),
77
+ originalProfile: profile ? Buffer.from(profile).toString("base64") : null,
78
+ pid: process.pid,
79
+ timestamp: new Date().toISOString(),
80
+ };
81
+ atomicWriteFile(JOURNAL_PATH, JSON.stringify(journal));
82
+ }
83
+
84
+ export function markJournalInjectedState(config: string, profile: string | null): void {
85
+ const journal = readJournal();
86
+ if (!journal) return;
87
+ if (journal.injectedConfigHash) return;
88
+ journal.injectedConfigHash = sha256(config) ?? undefined;
89
+ journal.injectedProfileHash = sha256(profile);
90
+ atomicWriteFile(JOURNAL_PATH, JSON.stringify(journal));
91
+ }
92
+
93
+ export function removeJournal(): void {
94
+ try { unlinkSync(JOURNAL_PATH); } catch { /* ignore */ }
95
+ }
96
+
97
+ function readJournal(): Journal | null {
98
+ if (!existsSync(JOURNAL_PATH)) return null;
99
+ try {
100
+ const journal = JSON.parse(readFileSync(JOURNAL_PATH, "utf-8")) as Journal;
101
+ if (journal.version !== 1) throw new Error("unknown version");
102
+ return journal;
103
+ } catch {
104
+ removeJournal();
105
+ return null;
106
+ }
107
+ }
108
+
109
+ export function restoreJournalState(): RestoreJournalResult {
110
+ const journal = readJournal();
111
+ if (!journal) {
112
+ return { configRestored: false, profileRestored: false, configChanged: false, profileChanged: false, complete: false };
113
+ }
114
+ const currentConfig = existsSync(CODEX_CONFIG_PATH) ? readFileSync(CODEX_CONFIG_PATH, "utf-8") : "";
115
+ const currentProfile = existsSync(CODEX_PROFILE_PATH) ? readFileSync(CODEX_PROFILE_PATH, "utf-8") : null;
116
+ const configUnchanged = !journal.injectedConfigHash || sha256(currentConfig) === journal.injectedConfigHash;
117
+ const profileUnchanged = journal.injectedProfileHash === undefined || sha256(currentProfile) === (journal.injectedProfileHash ?? null);
118
+
119
+ let configRestored = false;
120
+ let profileRestored = false;
121
+ if (configUnchanged) {
122
+ atomicWriteFile(CODEX_CONFIG_PATH, Buffer.from(journal.originalConfig, "base64").toString("utf-8"));
123
+ configRestored = true;
124
+ }
125
+ if (profileUnchanged) {
126
+ if (journal.originalProfile !== null) {
127
+ atomicWriteFile(CODEX_PROFILE_PATH, Buffer.from(journal.originalProfile, "base64").toString("utf-8"));
128
+ } else if (existsSync(CODEX_PROFILE_PATH)) {
129
+ try { unlinkSync(CODEX_PROFILE_PATH); } catch { /* ignore */ }
130
+ }
131
+ profileRestored = true;
132
+ }
133
+ const complete = configRestored && profileRestored;
134
+ if (complete) removeJournal();
135
+ return {
136
+ configRestored,
137
+ profileRestored,
138
+ configChanged: !configUnchanged,
139
+ profileChanged: !profileUnchanged,
140
+ complete,
141
+ };
142
+ }
143
+
144
+ export function restoreJournal(): boolean {
145
+ return restoreJournalState().complete;
146
+ }
147
+
148
+ export function reconcileJournal(): boolean {
149
+ const journal = readJournal();
150
+ if (!journal) return false;
151
+ try {
152
+ process.kill(journal.pid, 0);
153
+ return false;
154
+ } catch (e: unknown) {
155
+ if ((e as NodeJS.ErrnoException).code === "EPERM") {
156
+ return false;
157
+ }
158
+ }
159
+ const restored = restoreJournalState();
160
+ if (!restored.configRestored && !restored.profileRestored) return false;
161
+ console.error(`⚠️ Previous session (PID ${journal.pid}) did not shut down cleanly. Codex state restored from journal.`);
162
+ return true;
163
+ }
@@ -0,0 +1,32 @@
1
+ import type { StoredAccountQuota } from "./quota";
2
+ import { truncateRetainedUtf8 } from "../lib/admission";
3
+
4
+ const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
5
+
6
+ export interface MainAccountInfo {
7
+ email: string | null;
8
+ plan: string | null;
9
+ quota: Omit<StoredAccountQuota, "updatedAt"> | null;
10
+ }
11
+
12
+ export interface CachedMainAccountInfo extends MainAccountInfo {
13
+ ts: number;
14
+ }
15
+
16
+ let cachedMainAccountInfo: CachedMainAccountInfo | null = null;
17
+
18
+ export function getMainAccountInfoCache(): CachedMainAccountInfo | null {
19
+ return cachedMainAccountInfo;
20
+ }
21
+
22
+ export function setMainAccountInfoCache(value: CachedMainAccountInfo): void {
23
+ cachedMainAccountInfo = {
24
+ ...value,
25
+ email: value.email === null ? null : truncateRetainedUtf8(value.email, MAX_DIAGNOSTIC_VALUE_BYTES),
26
+ plan: value.plan === null ? null : truncateRetainedUtf8(value.plan, MAX_DIAGNOSTIC_VALUE_BYTES),
27
+ };
28
+ }
29
+
30
+ export function clearMainAccountInfoCache(): void {
31
+ cachedMainAccountInfo = null;
32
+ }
@@ -0,0 +1,40 @@
1
+ import { readCodexTokens } from "./auth-collision";
2
+ import { decodeJwtPayload } from "../oauth/chatgpt";
3
+ import { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
4
+
5
+ export { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
6
+
7
+ /**
8
+ * Main account plan (e.g. "plus", "go", "free", "team"), populated from the WHAM usage
9
+ * fetch. Used by the rotation usage-score so go/free main accounts score on monthly
10
+ * percent, matching pool-account behavior.
11
+ */
12
+ let mainAccountPlan: string | null = null;
13
+
14
+ export function setMainAccountPlan(plan: string | null): void {
15
+ mainAccountPlan = plan;
16
+ }
17
+
18
+ export function getMainAccountPlan(): string | undefined {
19
+ return mainAccountPlan ?? undefined;
20
+ }
21
+
22
+ /** Read-only main account token from ~/.codex/auth.json, or null when not logged in. */
23
+ export function getMainAccountToken(): { accessToken: string; chatgptAccountId: string } | null {
24
+ const tokens = readCodexTokens();
25
+ if (!tokens?.access_token) return null;
26
+ return { accessToken: tokens.access_token, chatgptAccountId: tokens.account_id };
27
+ }
28
+
29
+ /**
30
+ * The main token is usable when it exists and — if its JWT carries a decodable `exp` — is
31
+ * not expired. When `exp` cannot be decoded we treat the token as live (best-effort); an
32
+ * actually-invalid token then surfaces via the upstream 401 → cooldown path.
33
+ */
34
+ export function isMainAccountTokenLive(now = Date.now()): boolean {
35
+ const tokens = readCodexTokens();
36
+ if (!tokens?.access_token) return false;
37
+ const payload = decodeJwtPayload(tokens.access_token);
38
+ const exp = typeof payload?.exp === "number" ? payload.exp * 1000 : undefined;
39
+ return exp === undefined || exp > now;
40
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * In-memory, per-provider TTL cache for live `/models` results.
3
+ *
4
+ * Ported in spirit from jawcode's packages/ai/src/model-manager.ts (the "always load the latest
5
+ * model list" resolver): live fetch when the cache is stale, serve the cache while it is fresh,
6
+ * and fall back to the last-known-good list when a live fetch fails. opencodex's proxy is a single
7
+ * long-running process and the on-disk Codex catalog already persists the last sync across
8
+ * restarts, so an in-memory cache is sufficient here (no SQLite layer needed).
9
+ */
10
+ import type { CatalogModel } from "./catalog";
11
+ import type { GenerationContext } from "../lib/state-store-sweeper";
12
+ import { enforceAppOwnedMemoryBudget, type RetainedStoreSnapshot } from "../lib/app-owned-memory";
13
+
14
+ /** Default freshness window. Matches Codex's own 5-min models cache so the two stay in step. */
15
+ export const DEFAULT_MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
16
+
17
+ interface CacheEntry {
18
+ models: CatalogModel[];
19
+ fetchedAt: number;
20
+ sizeBytes: number;
21
+ }
22
+
23
+ export type ProviderModelDiscoveryFailureReason =
24
+ | "http"
25
+ | "blocked"
26
+ | "invalid_response"
27
+ | "network"
28
+ | "provider";
29
+
30
+ export type ProviderModelDiscoveryStatus =
31
+ | { status: "ok" }
32
+ | { status: "failed"; reason: "http"; httpStatus: number }
33
+ | {
34
+ status: "failed";
35
+ reason: Exclude<ProviderModelDiscoveryFailureReason, "http">;
36
+ httpStatus?: never;
37
+ };
38
+
39
+ export type ProviderModelDiscoveryFailure = ProviderModelDiscoveryStatus extends infer Status
40
+ ? Status extends { status: "failed" }
41
+ ? Omit<Status, "status">
42
+ : never
43
+ : never;
44
+
45
+ const cache = new Map<string, CacheEntry>();
46
+ let cacheBytes = 0;
47
+ let oldestCachedProvider: string | undefined;
48
+ let oldestCachedAt: number | null = null;
49
+ const modelCacheEncoder = new TextEncoder();
50
+
51
+ function recomputeOldestCachedProvider(): void {
52
+ oldestCachedProvider = undefined;
53
+ oldestCachedAt = null;
54
+ for (const [provider, entry] of cache) {
55
+ if (oldestCachedAt !== null && entry.fetchedAt >= oldestCachedAt) continue;
56
+ oldestCachedProvider = provider;
57
+ oldestCachedAt = entry.fetchedAt;
58
+ }
59
+ }
60
+
61
+ function deleteCachedProvider(provider: string): number {
62
+ const entry = cache.get(provider);
63
+ if (!entry) return 0;
64
+ cache.delete(provider);
65
+ cacheBytes = Math.max(0, cacheBytes - entry.sizeBytes);
66
+ if (oldestCachedProvider === provider) recomputeOldestCachedProvider();
67
+ return entry.sizeBytes;
68
+ }
69
+
70
+ /** Cooldown after a failed live `/models` fetch, so a dead/unreachable provider doesn't re-pay
71
+ * the full fetch timeout on every catalog poll (issue #54: UI stalls behind corporate proxies). */
72
+ export const MODELS_FETCH_FAILURE_COOLDOWN_MS = 30_000;
73
+
74
+ const failureAt = new Map<string, number>();
75
+ const discoveryStatus = new Map<string, ProviderModelDiscoveryStatus>();
76
+ /**
77
+ * How many models the last successful discovery actually returned, before any configured-alias
78
+ * augmentation or custom-model merge. Consumers cannot recover this by subtracting known custom
79
+ * ids: a custom id that later also appears upstream would make a real live catalog look
80
+ * custom-only, and the provider's configured fallback would wrongly stay authoritative.
81
+ */
82
+ const liveModelCounts = new Map<string, number>();
83
+ let lastReconciledGeneration = 0;
84
+
85
+ export function markModelsFetchFailure(provider: string, now = Date.now()): void {
86
+ failureAt.set(provider, now);
87
+ }
88
+
89
+ /** `liveModelCount` is required so a caller that forgets to pass it fails typecheck instead of
90
+ * silently recording zero and misclassifying the provider as having no live catalog. */
91
+ export function markProviderDiscoveryOk(provider: string, liveModelCount: number): void {
92
+ discoveryStatus.set(provider, { status: "ok" });
93
+ liveModelCounts.set(provider, Math.max(0, Math.floor(liveModelCount)));
94
+ }
95
+
96
+ export function markProviderDiscoveryFailed(
97
+ provider: string,
98
+ failure: ProviderModelDiscoveryFailure,
99
+ ): void {
100
+ discoveryStatus.set(provider, { status: "failed", ...failure });
101
+ }
102
+
103
+ /**
104
+ * Decide whether a discovery FAILURE should be logged, to avoid flooding the log with an identical
105
+ * warning on every poll (#395: an anthropic-adapter baseUrl without `/v1/models`, e.g. Azure AI
106
+ * Foundry, returns HTTP 404 forever; the 30s cooldown re-probes and previously re-logged each time).
107
+ *
108
+ * Returns true only when the failure SIGNATURE changed since the last observed status — i.e. the
109
+ * previous state was ok/undefined, or a different reason/httpStatus. Repeated identical failures
110
+ * stay observable through `getProviderDiscoveryStatus()` / the providers API without log spam.
111
+ * Call this BEFORE `markProviderDiscoveryFailed` so it can see the prior state.
112
+ */
113
+ export function shouldLogDiscoveryFailure(
114
+ provider: string,
115
+ failure: ProviderModelDiscoveryFailure,
116
+ ): boolean {
117
+ const prev = discoveryStatus.get(provider);
118
+ if (!prev || prev.status !== "failed") return true;
119
+ if (prev.reason !== failure.reason) return true;
120
+ if (prev.reason === "http" && failure.reason === "http") {
121
+ return prev.httpStatus !== failure.httpStatus;
122
+ }
123
+ return false;
124
+ }
125
+
126
+ export function clearProviderDiscoveryStatus(provider: string): void {
127
+ discoveryStatus.delete(provider);
128
+ liveModelCounts.delete(provider);
129
+ }
130
+
131
+ export function getProviderDiscoveryStatus(provider: string): ProviderModelDiscoveryStatus | undefined {
132
+ return discoveryStatus.get(provider);
133
+ }
134
+
135
+ /** Undefined when discovery has never succeeded for this provider. A failed refresh keeps the
136
+ * last successful count so a stale catalog is still recognised as live-origin. */
137
+ export function getProviderLiveModelCount(provider: string): number | undefined {
138
+ return liveModelCounts.get(provider);
139
+ }
140
+
141
+ export function isModelsFetchCoolingDown(provider: string, cooldownMs = MODELS_FETCH_FAILURE_COOLDOWN_MS, now = Date.now()): boolean {
142
+ const at = failureAt.get(provider);
143
+ return at !== undefined && now - at < cooldownMs;
144
+ }
145
+
146
+ /** Fresh cached models for a provider, or null when absent/stale (caller should re-fetch). */
147
+ export function getFreshCached(provider: string, ttlMs: number, now = Date.now()): CatalogModel[] | null {
148
+ const entry = cache.get(provider);
149
+ if (!entry) return null;
150
+ return now - entry.fetchedAt < ttlMs ? entry.models : null;
151
+ }
152
+
153
+ /** Last-known-good models regardless of age — the fallback when a live fetch fails. */
154
+ export function getStaleCached(provider: string): CatalogModel[] | null {
155
+ return cache.get(provider)?.models ?? null;
156
+ }
157
+
158
+ export function setCached(provider: string, models: CatalogModel[], now = Date.now()): void {
159
+ deleteCachedProvider(provider);
160
+ const sizeBytes = modelCacheEncoder.encode(provider).byteLength
161
+ + modelCacheEncoder.encode(JSON.stringify(models)).byteLength;
162
+ cache.set(provider, { models, fetchedAt: now, sizeBytes });
163
+ cacheBytes += sizeBytes;
164
+ if (oldestCachedAt === null || now < oldestCachedAt) {
165
+ oldestCachedProvider = provider;
166
+ oldestCachedAt = now;
167
+ }
168
+ enforceAppOwnedMemoryBudget();
169
+ }
170
+
171
+ /** Drop one provider's cache (or all) so the next resolve forces a live re-fetch. */
172
+ export function clearModelCache(provider?: string): void {
173
+ if (provider) {
174
+ deleteCachedProvider(provider);
175
+ failureAt.delete(provider);
176
+ discoveryStatus.delete(provider);
177
+ liveModelCounts.delete(provider);
178
+ } else {
179
+ cache.clear();
180
+ cacheBytes = 0;
181
+ oldestCachedProvider = undefined;
182
+ oldestCachedAt = null;
183
+ failureAt.clear();
184
+ discoveryStatus.clear();
185
+ liveModelCounts.clear();
186
+ }
187
+ }
188
+
189
+ export function reconcileModelCacheProviders(
190
+ validProviders: ReadonlySet<string>,
191
+ generation = lastReconciledGeneration + 1,
192
+ ): number {
193
+ if (generation <= lastReconciledGeneration) return 0;
194
+ const removedProviders = new Set<string>();
195
+ for (const store of [failureAt, discoveryStatus, liveModelCounts]) {
196
+ for (const provider of store.keys()) {
197
+ if (validProviders.has(provider)) continue;
198
+ store.delete(provider);
199
+ removedProviders.add(provider);
200
+ }
201
+ }
202
+ for (const provider of cache.keys()) {
203
+ if (validProviders.has(provider)) continue;
204
+ deleteCachedProvider(provider);
205
+ removedProviders.add(provider);
206
+ }
207
+ lastReconciledGeneration = generation;
208
+ return removedProviders.size;
209
+ }
210
+
211
+ export function reconcileModelCacheGeneration(context: GenerationContext): number {
212
+ return reconcileModelCacheProviders(context.providerNames, context.generation);
213
+ }
214
+
215
+ export function modelCacheRetainedStoreSnapshot(): RetainedStoreSnapshot {
216
+ return {
217
+ count: cache.size,
218
+ bytes: cacheBytes,
219
+ evictableBytes: cacheBytes,
220
+ pinnedBytes: 0,
221
+ oldestAt: oldestCachedAt,
222
+ };
223
+ }
224
+
225
+ export function evictOldestModelCacheForBudget(): number {
226
+ return oldestCachedProvider === undefined ? 0 : deleteCachedProvider(oldestCachedProvider);
227
+ }
@@ -0,0 +1,65 @@
1
+ import { realpathSync, statSync } from "node:fs";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ import { expandUserPath } from "../config";
4
+ import { defaultCodexHome } from "./home";
5
+
6
+ function resolveCodexHome(): string {
7
+ const raw = process.env.CODEX_HOME?.trim();
8
+ if (raw) {
9
+ const path = resolve(expandUserPath(raw));
10
+ let stat;
11
+ try {
12
+ stat = statSync(path);
13
+ } catch (err) {
14
+ const message = err instanceof Error ? err.message : String(err);
15
+ throw new Error(`CODEX_HOME points to ${raw}, but that path could not be read: ${message}`);
16
+ }
17
+ if (!stat.isDirectory()) {
18
+ throw new Error(`CODEX_HOME points to ${raw}, but that path is not a directory`);
19
+ }
20
+ return realpathSync.native(path);
21
+ }
22
+
23
+ return defaultCodexHome();
24
+ }
25
+
26
+ export const CODEX_HOME = resolveCodexHome();
27
+ export const CODEX_CONFIG_PATH = join(CODEX_HOME, "config.toml");
28
+ export const CODEX_PROFILE_PATH = join(CODEX_HOME, "opencodex.config.toml");
29
+ export const DEFAULT_CATALOG_PATH = join(CODEX_HOME, "opencodex-catalog.json");
30
+ export const CODEX_MODELS_CACHE_PATH = join(CODEX_HOME, "models_cache.json");
31
+
32
+ /** Runtime CODEX_HOME lookup (honors CODEX_HOME env changes after import). */
33
+ export function getCodexHome(): string {
34
+ return resolveCodexHome();
35
+ }
36
+
37
+ export function tomlString(value: string): string {
38
+ return JSON.stringify(value);
39
+ }
40
+
41
+ export function parseTomlString(raw: string): string {
42
+ if (raw.startsWith("\"")) {
43
+ try {
44
+ return JSON.parse(raw) as string;
45
+ } catch {
46
+ return raw.slice(1, -1);
47
+ }
48
+ }
49
+ return raw.slice(1, -1);
50
+ }
51
+
52
+ export function readRootTomlString(content: string, key: string): string | null {
53
+ const lines = content.split("\n");
54
+ const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
55
+ const rootLines = firstTable === -1 ? lines : lines.slice(0, firstTable);
56
+ for (const line of rootLines) {
57
+ const m = line.match(new RegExp(`^\\s*${key}\\s*=\\s*(\"(?:\\\\.|[^\"])*\"|'[^']*')`));
58
+ if (m) return parseTomlString(m[1]);
59
+ }
60
+ return null;
61
+ }
62
+
63
+ export function resolveCodexConfigPath(path: string): string {
64
+ return isAbsolute(path) ? path : join(CODEX_HOME, path);
65
+ }