@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,707 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import type { CatalogModel } from "../../codex/catalog";
4
+ import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
5
+ import {
6
+ DEFAULT_SUBAGENT_MODELS,
7
+ codexAutoStartEnabled,
8
+ hasOwnProvider,
9
+ isValidProviderName,
10
+ multiAgentGuidanceEnabled,
11
+ providerBaseUrlConfigError,
12
+ providerHeadersConfigError,
13
+ saveConfigPreservingClaudeCode,
14
+ } from "../../config";
15
+ import {
16
+ clearLoginState,
17
+ getLoginStatus,
18
+ isPublicOAuthProvider,
19
+ listOAuthProviders,
20
+ startLoginFlow,
21
+ submitManualLoginCode,
22
+ upsertOAuthProvider,
23
+ } from "../../oauth";
24
+ import { removeCredential } from "../../oauth/store";
25
+ import { providerDestinationResolvedError } from "../../lib/destination-policy";
26
+ import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
27
+ import { deriveProviderPresets } from "../../providers/derive";
28
+ import { providerCodexAccountMode } from "../../providers/registry";
29
+ import { routedSlug, slugEquals } from "../../providers/slug-codec";
30
+ import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
31
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
32
+ import { clearThreadAccountMap } from "../../codex/routing";
33
+ import { primeCodexPoolQuotas } from "../../codex/auth-api";
34
+ import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
35
+ import { resolveCodexHomeDir } from "../../codex/home";
36
+ import { scanStorage } from "../../storage/scanner";
37
+ import { executeArchivedCleanup, listTrashEntries, pickWireCleanupTestHooks, previewArchivedCleanup, type CleanupMode, type RestoreErrorCode } from "../../storage/cleanup";
38
+ import { runArchivedCleanupJob } from "../../storage/cleanup-job";
39
+ import { getRestoreTrashTestStreamResponse, runRestoreTrashEntryJob } from "../../storage/restore-job";
40
+ import {
41
+ normalizeStorageCleanupPolicy,
42
+ parseStorageCleanupPolicyInput,
43
+ writeStorageCleanupPolicyToConfig,
44
+ } from "../../storage/policy";
45
+ import {
46
+ getStorageCleanupPolicyJobState,
47
+ getStorageCleanupPolicyTestStreamResponse,
48
+ requestStorageCleanupPolicyRun,
49
+ } from "../../storage/policy-job";
50
+ import {
51
+ currentUsageLogRevision,
52
+ readUsageSnapshotForManagement,
53
+ usageLogRevisionKey,
54
+ type PersistedUsageEntry,
55
+ } from "../../usage/log";
56
+ import { getUsageDebugLogEntries } from "../../usage/debug";
57
+ import { parseRange, parseUsageSurface, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
58
+ import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
59
+ import { getProviderRegistryEntry } from "../../providers/registry";
60
+ import { getDebugLogEntries } from "../../lib/debug-log-buffer";
61
+ import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
62
+ import {
63
+ clearDebugSettings,
64
+ clearDebugSetting,
65
+ getDebugSettings,
66
+ setDebugSettings,
67
+ type DebugFlag,
68
+ } from "../../lib/debug-settings";
69
+ import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
70
+ import { drainAndShutdown } from "../lifecycle";
71
+ import { addRequestLog, filterRequestLogs, filteredRequestLogCount, getRequestLogEntries, type RequestLogEntry } from "../request-log";
72
+ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
73
+ import type { PersistedUsageAttempt, UsageStatus } from "../../usage/log";
74
+ import { isKnownUsageSurface } from "../../usage/log";
75
+ import type { OcxUsage } from "../../types";
76
+ import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
77
+ import { applySystemEnvToggle } from "../system-env";
78
+
79
+ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
80
+ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
81
+ import type { ManagementContext } from "./context";
82
+ import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
83
+ import {
84
+ discardUsageSummaryCacheEntry,
85
+ getUsageSummaryCacheEntry,
86
+ resetUsageSummaryCacheForTests,
87
+ setUsageSummaryCacheEntry,
88
+ } from "./usage-summary-cache";
89
+
90
+ const USAGE_DAY_MS = 86_400_000;
91
+ /** Max entries accepted in one POST /api/usage/ingest body. */
92
+ const USAGE_INGEST_MAX_BATCH = 50;
93
+ const USAGE_STATUSES = new Set<UsageStatus>(["reported", "unreported", "unsupported", "estimated"]);
94
+
95
+ function usageEntryMatchesSurface(entry: PersistedUsageEntry, surface: UsageSurface): boolean {
96
+ if (surface === "claude") return entry.surface === "claude" || entry.surface === "claude-desktop";
97
+ if (surface === "grok") return entry.surface === "grok";
98
+ if (surface === "codex") return entry.surface === undefined;
99
+ return true;
100
+ }
101
+
102
+ function isNonNegativeFiniteNumber(value: unknown): value is number {
103
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
104
+ }
105
+
106
+ function extractIngestRawEntries(body: unknown): unknown[] | null {
107
+ if (!isPlainRecord(body)) return null;
108
+ if (Array.isArray(body.entries)) return body.entries;
109
+ // Single entry: must look like a usage row (has requestId or model+provider).
110
+ if (typeof body.requestId === "string" || (typeof body.model === "string" && typeof body.provider === "string")) {
111
+ return [body];
112
+ }
113
+ return null;
114
+ }
115
+
116
+ function parseIngestUsage(raw: unknown): OcxUsage | undefined {
117
+ if (!isPlainRecord(raw)) return undefined;
118
+ if (!isNonNegativeFiniteNumber(raw.inputTokens) || !isNonNegativeFiniteNumber(raw.outputTokens)) {
119
+ return undefined;
120
+ }
121
+ const inputTokens = raw.inputTokens;
122
+ const outputTokens = raw.outputTokens;
123
+ const totalTokens = isNonNegativeFiniteNumber(raw.totalTokens)
124
+ ? raw.totalTokens
125
+ : inputTokens + outputTokens;
126
+ return {
127
+ inputTokens,
128
+ outputTokens,
129
+ totalTokens,
130
+ ...(isNonNegativeFiniteNumber(raw.cachedInputTokens)
131
+ ? { cachedInputTokens: raw.cachedInputTokens }
132
+ : {}),
133
+ ...(isNonNegativeFiniteNumber(raw.cacheReadInputTokens)
134
+ ? { cacheReadInputTokens: raw.cacheReadInputTokens }
135
+ : {}),
136
+ ...(isNonNegativeFiniteNumber(raw.cacheCreationInputTokens)
137
+ ? { cacheCreationInputTokens: raw.cacheCreationInputTokens }
138
+ : {}),
139
+ ...(isNonNegativeFiniteNumber(raw.reasoningOutputTokens)
140
+ ? { reasoningOutputTokens: raw.reasoningOutputTokens }
141
+ : {}),
142
+ ...(raw.estimated === true ? { estimated: true } : {}),
143
+ };
144
+ }
145
+
146
+ function parseIngestEntry(
147
+ raw: unknown,
148
+ index: number,
149
+ ): { ok: true; entry: RequestLogEntry } | { ok: false; error: string } {
150
+ if (!isPlainRecord(raw)) return { ok: false, error: `entries[${index}] must be an object` };
151
+
152
+ const requestId = typeof raw.requestId === "string" ? raw.requestId.trim() : "";
153
+ if (!requestId || requestId.length > 200) {
154
+ return { ok: false, error: `entries[${index}].requestId is required (≤200 chars)` };
155
+ }
156
+ const provider = typeof raw.provider === "string" ? raw.provider.trim() : "";
157
+ const model = typeof raw.model === "string" ? raw.model.trim() : "";
158
+ if (!provider || provider.length > 128) {
159
+ return { ok: false, error: `entries[${index}].provider is required (≤128 chars)` };
160
+ }
161
+ if (!model || model.length > 256) {
162
+ return { ok: false, error: `entries[${index}].model is required (≤256 chars)` };
163
+ }
164
+
165
+ const status = isNonNegativeFiniteNumber(raw.status) ? Math.trunc(raw.status) : 200;
166
+ if (status < 0 || status > 599) {
167
+ return { ok: false, error: `entries[${index}].status must be 0–599` };
168
+ }
169
+ const durationMs = isNonNegativeFiniteNumber(raw.durationMs) ? raw.durationMs : 0;
170
+ const timestamp = isNonNegativeFiniteNumber(raw.timestamp) ? raw.timestamp : Date.now();
171
+
172
+ const usageStatusRaw = typeof raw.usageStatus === "string" ? raw.usageStatus : "reported";
173
+ if (!USAGE_STATUSES.has(usageStatusRaw as UsageStatus)) {
174
+ return { ok: false, error: `entries[${index}].usageStatus is invalid` };
175
+ }
176
+ const usageStatus = usageStatusRaw as UsageStatus;
177
+ const usage = parseIngestUsage(raw.usage);
178
+ if (raw.usage !== undefined && usage === undefined) {
179
+ return { ok: false, error: `entries[${index}].usage is invalid` };
180
+ }
181
+
182
+ const totalTokens = isNonNegativeFiniteNumber(raw.totalTokens)
183
+ ? raw.totalTokens
184
+ : usage?.totalTokens;
185
+
186
+ const surface = isKnownUsageSurface(raw.surface) ? raw.surface : undefined;
187
+ const conversationId = typeof raw.conversationId === "string" && raw.conversationId.length <= 128
188
+ ? raw.conversationId
189
+ : undefined;
190
+ const requestedModel = typeof raw.requestedModel === "string" && raw.requestedModel.length <= 256
191
+ ? raw.requestedModel
192
+ : undefined;
193
+ const resolvedModel = typeof raw.resolvedModel === "string" && raw.resolvedModel.length <= 256
194
+ ? raw.resolvedModel
195
+ : undefined;
196
+
197
+ const entry: RequestLogEntry = {
198
+ requestId,
199
+ timestamp,
200
+ provider,
201
+ model,
202
+ status,
203
+ durationMs,
204
+ usageStatus,
205
+ ...(surface ? { surface } : {}),
206
+ ...(conversationId ? { conversationId } : {}),
207
+ ...(requestedModel ? { requestedModel } : {}),
208
+ ...(resolvedModel ? { resolvedModel } : {}),
209
+ ...(usage ? { usage } : {}),
210
+ ...(totalTokens !== undefined ? { totalTokens } : {}),
211
+ };
212
+ return { ok: true, entry };
213
+ }
214
+
215
+ function nextLocalMidnight(now: number): number {
216
+ const next = new Date(now);
217
+ next.setHours(24, 0, 0, 0);
218
+ return next.getTime();
219
+ }
220
+
221
+ function usageSummaryExpiresAt(
222
+ entries: PersistedUsageEntry[],
223
+ range: UsageRange,
224
+ surface: UsageSurface,
225
+ now: number,
226
+ ): number {
227
+ let expiresAt = nextLocalMidnight(now);
228
+ const windowMs = range === "7d" ? 7 * USAGE_DAY_MS : range === "30d" ? 30 * USAGE_DAY_MS : null;
229
+ if (windowMs === null) return expiresAt;
230
+ for (const entry of entries) {
231
+ if (!usageEntryMatchesSurface(entry, surface)) continue;
232
+ const expiry = entry.timestamp + windowMs;
233
+ if (expiry > now && expiry < expiresAt) expiresAt = expiry;
234
+ }
235
+ return expiresAt;
236
+ }
237
+
238
+ function refreshedUsageSummary<T extends UsageSummary & { historyTruncated: boolean }>(summary: T, range: UsageRange, now: number): T {
239
+ const since = range === "7d" ? now - 7 * USAGE_DAY_MS : range === "30d" ? now - 30 * USAGE_DAY_MS : null;
240
+ return { ...summary, since, generatedAt: now };
241
+ }
242
+
243
+ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Response | null> {
244
+ const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
245
+
246
+ if (url.pathname === "/api/logs" && req.method === "GET") {
247
+ const all = getRequestLogEntries();
248
+ const total = filteredRequestLogCount(all, url.searchParams);
249
+ const logs = filterRequestLogs(all, url.searchParams);
250
+ return jsonResponse({
251
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
252
+ total,
253
+ logs: logs.map(requestLogDto),
254
+ });
255
+ }
256
+
257
+ if (url.pathname === "/api/debug" && req.method === "GET") {
258
+ return jsonResponse(getDebugSettings());
259
+ }
260
+
261
+ if (url.pathname === "/api/debug/logs" && req.method === "GET") {
262
+ const { after, limit } = parseDebugLogQuery(url);
263
+ return jsonResponse(getDebugLogEntries({ after, limit }));
264
+ }
265
+
266
+ if (url.pathname === "/api/debug/usage-logs" && req.method === "GET") {
267
+ const { after, limit } = parseDebugLogQuery(url);
268
+ return jsonResponse(getUsageDebugLogEntries({ after, limit }));
269
+ }
270
+
271
+ if (url.pathname === "/api/claude/inbound-debug" && req.method === "GET") {
272
+ const { getClaudeInboundDebugEntries } = await import("../../claude/inbound-debug");
273
+ const { isClaudeDebugEnabled } = await import("../../lib/debug-settings");
274
+ return jsonResponse({ enabled: isClaudeDebugEnabled(), entries: getClaudeInboundDebugEntries() });
275
+ }
276
+
277
+ if (url.pathname === "/api/debug/injection-logs" && req.method === "GET") {
278
+ const { after, limit } = parseDebugLogQuery(url);
279
+ return jsonResponse(getInjectionDebugLogEntries({ after, limit }));
280
+ }
281
+
282
+ if (url.pathname === "/api/debug" && req.method === "PUT") {
283
+ let body: { debug?: unknown; usage?: unknown; injection?: unknown; claude?: unknown; reset?: unknown };
284
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
285
+ if (body.reset === true) return jsonResponse(clearDebugSettings());
286
+ if (body.reset === "debug" || body.reset === "provider") return jsonResponse(clearDebugSetting("debug"));
287
+ if (body.reset === "usage") return jsonResponse(clearDebugSetting("usage"));
288
+ if (body.reset === "injection") return jsonResponse(clearDebugSetting("injection"));
289
+ if (body.reset === "claude") return jsonResponse(clearDebugSetting("claude"));
290
+ const partial: Partial<Record<DebugFlag, boolean>> = {};
291
+ for (const key of ["debug", "usage", "injection", "claude"] as const) {
292
+ if (body[key] === undefined) continue;
293
+ if (typeof body[key] !== "boolean") return jsonResponse({ error: `${key} must be a boolean` }, 400);
294
+ partial[key] = body[key];
295
+ }
296
+ if (Object.keys(partial).length === 0) {
297
+ return jsonResponse({ error: "provide debug/usage/injection/claude booleans or reset:true" }, 400);
298
+ }
299
+ // Turning capture off should also flush already-captured entries (privacy contract).
300
+ if (partial.claude === false) {
301
+ const { clearClaudeInboundDebug } = await import("../../claude/inbound-debug");
302
+ clearClaudeInboundDebug();
303
+ }
304
+ return jsonResponse(setDebugSettings(partial));
305
+ }
306
+
307
+ if (url.pathname === "/api/usage" && req.method === "GET") {
308
+ const range = parseRange(url.searchParams.get("range"));
309
+ const surface = parseUsageSurface(url.searchParams.get("surface"));
310
+ const now = Date.now();
311
+ try {
312
+ const cacheKey = `${range}:${surface}`;
313
+ const effectiveReadLimit = config.managementUsageMaxReadBytes ?? 64 * 1024 * 1024;
314
+ const observedRevisionKey = `${usageLogRevisionKey(currentUsageLogRevision())}\0${effectiveReadLimit}`;
315
+ const cached = getUsageSummaryCacheEntry(cacheKey);
316
+ if (cached && cached.revisionKey === observedRevisionKey && now < cached.expiresAt) {
317
+ return jsonResponse(refreshedUsageSummary(cached.summary, range, now));
318
+ }
319
+ if (cached) discardUsageSummaryCacheEntry(cacheKey);
320
+ const snapshot = await readUsageSnapshotForManagement(effectiveReadLimit);
321
+ const revisionReadAt = Date.now();
322
+ const summary = {
323
+ ...summarizeUsage(snapshot.entries, range, now, surface),
324
+ historyTruncated: snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated,
325
+ truncatedPrefixBytes: snapshot.truncatedPrefixBytes,
326
+ entriesTruncated: snapshot.entriesTruncated,
327
+ entriesDropped: snapshot.entriesDropped,
328
+ };
329
+ setUsageSummaryCacheEntry(cacheKey, {
330
+ revisionKey: `${usageLogRevisionKey(snapshot.revision)}\0${effectiveReadLimit}`,
331
+ expiresAt: usageSummaryExpiresAt(snapshot.entries, range, surface, now),
332
+ revisionReadAt,
333
+ summary,
334
+ });
335
+ return jsonResponse(summary);
336
+ } catch {
337
+ return jsonResponse({
338
+ range,
339
+ surface,
340
+ since: null,
341
+ generatedAt: now,
342
+ summary: {
343
+ requests: 0,
344
+ attemptCount: 0,
345
+ measuredRequests: 0,
346
+ reportedRequests: 0,
347
+ unreportedRequests: 0,
348
+ unsupportedRequests: 0,
349
+ estimatedRequests: 0,
350
+ inputTokens: 0,
351
+ outputTokens: 0,
352
+ cachedInputTokens: 0,
353
+ cacheReadInputTokens: 0,
354
+ cacheCreationInputTokens: 0,
355
+ reasoningOutputTokens: 0,
356
+ totalTokens: 0,
357
+ coverageRatio: 0,
358
+ estimatedCostUsd: 0,
359
+ pricedRequests: 0,
360
+ unpricedRequests: 0,
361
+ unmeteredRequests: 0,
362
+ },
363
+ days: [],
364
+ models: [],
365
+ providers: [],
366
+ historyTruncated: false,
367
+ truncatedPrefixBytes: 0,
368
+ entriesTruncated: false,
369
+ entriesDropped: 0,
370
+ error: "read_failed",
371
+ });
372
+ }
373
+ }
374
+
375
+ /**
376
+ * External usage ingest (Grok Build hooks, sidecars).
377
+ * Accepts one entry or `{ entries: [...] }`, persists to usage.jsonl, and pushes into the
378
+ * live Logs ring so the dashboard "Logs / calls" view updates without a process restart.
379
+ * Requires management auth (same as other /api/* routes).
380
+ */
381
+ if (url.pathname === "/api/usage/ingest" && req.method === "POST") {
382
+ let body: unknown;
383
+ try {
384
+ body = await req.json();
385
+ } catch {
386
+ return jsonResponse({ error: "invalid JSON body" }, 400);
387
+ }
388
+ const rawEntries = extractIngestRawEntries(body);
389
+ if (!rawEntries) {
390
+ return jsonResponse({
391
+ error: "expected an entry object or { entries: [...] }",
392
+ }, 400);
393
+ }
394
+ if (rawEntries.length === 0) {
395
+ return jsonResponse({ error: "entries must not be empty" }, 400);
396
+ }
397
+ if (rawEntries.length > USAGE_INGEST_MAX_BATCH) {
398
+ return jsonResponse({
399
+ error: `at most ${USAGE_INGEST_MAX_BATCH} entries per request`,
400
+ }, 400);
401
+ }
402
+
403
+ const accepted: string[] = [];
404
+ const skipped: string[] = [];
405
+ const rejected: Array<{ index: number; error: string }> = [];
406
+ const knownIds = new Set(getRequestLogEntries().map(entry => entry.requestId));
407
+
408
+ for (let index = 0; index < rawEntries.length; index += 1) {
409
+ const parsed = parseIngestEntry(rawEntries[index], index);
410
+ if (!parsed.ok) {
411
+ rejected.push({ index, error: parsed.error });
412
+ continue;
413
+ }
414
+ if (knownIds.has(parsed.entry.requestId)) {
415
+ skipped.push(parsed.entry.requestId);
416
+ continue;
417
+ }
418
+ try {
419
+ addRequestLog(parsed.entry);
420
+ knownIds.add(parsed.entry.requestId);
421
+ accepted.push(parsed.entry.requestId);
422
+ } catch (error) {
423
+ rejected.push({
424
+ index,
425
+ error: error instanceof Error ? error.message : String(error),
426
+ });
427
+ }
428
+ }
429
+
430
+ // Invalidate usage summary cache so the next GET /api/usage reflects new rows.
431
+ resetUsageSummaryCacheForTests();
432
+
433
+ const status = accepted.length > 0 || skipped.length > 0
434
+ ? (rejected.length > 0 ? 207 : 200)
435
+ : 400;
436
+ return jsonResponse({
437
+ accepted: accepted.length,
438
+ skipped: skipped.length,
439
+ rejected: rejected.length,
440
+ requestIds: accepted,
441
+ skippedIds: skipped,
442
+ errors: rejected,
443
+ }, status);
444
+ }
445
+
446
+ if (url.pathname === "/api/storage" && req.method === "GET") {
447
+ try {
448
+ return jsonResponse(scanStorage());
449
+ } catch {
450
+ return jsonResponse({
451
+ codexHome: resolveCodexHomeDir(),
452
+ generatedAt: Date.now(),
453
+ total: { bytes: 0, fileCount: 0 },
454
+ buckets: [],
455
+ error: "scan_failed",
456
+ });
457
+ }
458
+ }
459
+
460
+ if (url.pathname === "/api/storage/cleanup/preview" && req.method === "POST") {
461
+ let body: { percent?: unknown };
462
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
463
+ const percent = typeof body?.percent === "number" ? body.percent : Number.NaN;
464
+ if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
465
+ return jsonResponse({ error: "invalid_percent" }, 400);
466
+ }
467
+ const preview = previewArchivedCleanup(percent);
468
+ // Omit absolute host paths (codexHome / absPath) from the wire response.
469
+ return jsonResponse({
470
+ percent: preview.percent,
471
+ count: preview.count,
472
+ bytes: preview.bytes,
473
+ digest: preview.digest,
474
+ // Dashboard only lists a handful; count/bytes/digest already bind the full set.
475
+ candidates: preview.candidates.slice(0, 50).map(({ relPath, bytes, mtimeMs, physicalRelPaths }) => ({
476
+ relPath,
477
+ bytes,
478
+ mtimeMs,
479
+ physicalRelPaths,
480
+ })),
481
+ });
482
+ }
483
+
484
+ if (url.pathname === "/api/storage/cleanup" && req.method === "POST") {
485
+ let body: { percent?: unknown; mode?: unknown; digest?: unknown; _test?: unknown };
486
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
487
+ const percent = typeof body?.percent === "number" ? body.percent : Number.NaN;
488
+ if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
489
+ return jsonResponse({ error: "invalid_percent" }, 400);
490
+ }
491
+ const mode = body?.mode;
492
+ if (mode !== "quarantine" && mode !== "permanent") {
493
+ return jsonResponse({ error: "invalid_mode" }, 400);
494
+ }
495
+ const digest = typeof body?.digest === "string" ? body.digest : "";
496
+ const testHooks =
497
+ process.env.OPENCODEX_CLEANUP_TEST_HOOKS === "1" &&
498
+ body &&
499
+ typeof body === "object" &&
500
+ "_test" in body
501
+ ? pickWireCleanupTestHooks(body._test)
502
+ : undefined;
503
+ try {
504
+ const result = await runArchivedCleanupJob({
505
+ percent,
506
+ mode: mode as CleanupMode,
507
+ digest,
508
+ ...(testHooks ? { _test: testHooks } : {}),
509
+ });
510
+ if (!result.ok) {
511
+ const status =
512
+ result.error === "codex_busy"
513
+ || result.error === "stale_preview"
514
+ || result.error === "referenced_history"
515
+ || result.error === "pinned_thread"
516
+ || result.error === "storage_mutation_busy"
517
+ || result.error === "restore_pending_overlap"
518
+ ? 409
519
+ : result.error === "invalid_mode" || result.error === "invalid_digest"
520
+ ? 400
521
+ : 500;
522
+ const messages: Record<string, string> = {
523
+ codex_busy: "Codex is using state.sqlite — try again after quitting Codex.",
524
+ storage_mutation_busy: "Another storage cleanup or restore is in progress — try again shortly.",
525
+ stale_preview: "Archived files changed since preview — run Preview again.",
526
+ restore_pending_overlap: "Selected archives overlap an incomplete trash restore — finish or retry restore first.",
527
+ referenced_history: "Selected archives are still referenced by forked or paginated history.",
528
+ pinned_thread: "Selected archives include a pinned thread — unpin it in Codex before cleanup.",
529
+ invalid_digest: "Preview digest is missing or invalid.",
530
+ invalid_mode: "mode must be quarantine or permanent.",
531
+ fs_failed: "Filesystem cleanup failed. Some changes may already be applied — check CODEX_HOME/.trash and any recovery path in the response.",
532
+ db_reconcile_failed: "Could not update Codex state database.",
533
+ cleanup_failed: "Cleanup failed.",
534
+ };
535
+ return jsonResponse({
536
+ ok: false,
537
+ error: result.error ?? "cleanup_failed",
538
+ message: messages[result.error ?? ""] ?? messages.cleanup_failed,
539
+ ...(result.trashDir ? { trashDir: result.trashDir } : {}),
540
+ }, status);
541
+ }
542
+ return jsonResponse({
543
+ ok: true,
544
+ mode: result.mode,
545
+ percent: result.percent,
546
+ count: result.count,
547
+ bytes: result.bytes,
548
+ ...(result.trashDir ? { trashDir: result.trashDir } : {}),
549
+ removedPaths: result.removedPaths,
550
+ });
551
+ } catch {
552
+ return jsonResponse({
553
+ ok: false,
554
+ error: "cleanup_failed",
555
+ message: "Cleanup failed.",
556
+ }, 500);
557
+ }
558
+ }
559
+
560
+ if (url.pathname === "/api/storage/trash" && req.method === "GET") {
561
+ try {
562
+ const entries = listTrashEntries();
563
+ return jsonResponse({
564
+ entries: entries.map(({ id, epoch, fileCount, bytes, quarantinedAt, mode }) => ({
565
+ id,
566
+ epoch,
567
+ fileCount,
568
+ bytes,
569
+ ...(quarantinedAt !== undefined ? { quarantinedAt } : {}),
570
+ ...(mode ? { mode } : {}),
571
+ })),
572
+ });
573
+ } catch {
574
+ return jsonResponse({ error: "trash_list_failed", entries: [] }, 500);
575
+ }
576
+ }
577
+
578
+ if (url.pathname === "/api/storage/trash/restore/test-stream" && req.method === "GET") {
579
+ if (process.env.OPENCODEX_CLEANUP_TEST_HOOKS === "1") {
580
+ const stream = getRestoreTrashTestStreamResponse();
581
+ if (stream) return stream;
582
+ }
583
+ // Always answer this test-only path — never fall through to the GUI SPA (200 HTML).
584
+ return jsonResponse({ error: "not_available" }, 404);
585
+ }
586
+
587
+ if (url.pathname === "/api/storage/trash/restore" && req.method === "POST") {
588
+ let body: { id?: unknown };
589
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
590
+ const id = typeof body?.id === "string" ? body.id : "";
591
+ if (!id.trim()) {
592
+ return jsonResponse({ error: "invalid_trash", message: "Trash entry id is required." }, 400);
593
+ }
594
+ try {
595
+ const result = await runRestoreTrashEntryJob(id);
596
+ if (!result.ok) {
597
+ const status =
598
+ result.error === "codex_busy"
599
+ || result.error === "dest_exists"
600
+ || result.error === "storage_mutation_busy"
601
+ ? 409
602
+ : result.error === "missing_trash"
603
+ ? 404
604
+ : result.error === "invalid_trash"
605
+ ? 400
606
+ : 500;
607
+ const messages: Record<RestoreErrorCode, string> = {
608
+ invalid_trash: "Trash entry id is missing or invalid.",
609
+ missing_trash: "Trash entry was not found.",
610
+ codex_busy: "Codex is using state.sqlite — try again after quitting Codex.",
611
+ storage_mutation_busy: "Another storage cleanup or restore is in progress — try again shortly.",
612
+ dest_exists: "Restore destination already exists — remove or rename the archived file and retry.",
613
+ fs_failed: "Filesystem restore failed. Some files may already be restored — check archived_sessions and .trash.",
614
+ db_reconcile_failed: "Could not restore Codex state database rows.",
615
+ restore_failed: "Restore failed.",
616
+ restore_worker_timeout: "Restore took too long (over 10 minutes) and was stopped.",
617
+ restore_worker_aborted: "Restore was cancelled during shutdown.",
618
+ restore_worker_failed: "Restore worker crashed or failed unexpectedly.",
619
+ };
620
+ const errorCode = result.error ?? "restore_failed";
621
+ const baseMessage = messages[errorCode] ?? messages.restore_failed;
622
+ const message =
623
+ result.message && errorCode === "restore_worker_failed"
624
+ ? `${baseMessage} (${result.message})`
625
+ : baseMessage;
626
+ return jsonResponse({
627
+ ok: false,
628
+ error: errorCode,
629
+ message,
630
+ count: result.count,
631
+ bytes: result.bytes,
632
+ restoredPaths: result.restoredPaths,
633
+ ...(result.trashDir ? { trashDir: result.trashDir } : {}),
634
+ }, status);
635
+ }
636
+ return jsonResponse({
637
+ ok: true,
638
+ trashDir: result.trashDir,
639
+ count: result.count,
640
+ bytes: result.bytes,
641
+ restoredPaths: result.restoredPaths,
642
+ });
643
+ } catch {
644
+ return jsonResponse({
645
+ ok: false,
646
+ error: "restore_failed",
647
+ message: "Restore failed.",
648
+ }, 500);
649
+ }
650
+ }
651
+
652
+ if (url.pathname === "/api/storage/cleanup-policy/test-stream" && req.method === "GET") {
653
+ const stream = getStorageCleanupPolicyTestStreamResponse();
654
+ if (stream) return stream;
655
+ // Production: hook is off. Return an explicit JSON 404 — do not fall through to the GUI.
656
+ return jsonResponse({ error: "not_found" }, 404);
657
+ }
658
+
659
+ if (url.pathname === "/api/storage/cleanup-policy" && req.method === "GET") {
660
+ const policy = normalizeStorageCleanupPolicy(config.storageCleanupPolicy);
661
+ return jsonResponse({
662
+ ...policy,
663
+ job: getStorageCleanupPolicyJobState(),
664
+ });
665
+ }
666
+
667
+ if (url.pathname === "/api/storage/cleanup-policy" && req.method === "PUT") {
668
+ let raw: unknown;
669
+ try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
670
+ const previous = normalizeStorageCleanupPolicy(config.storageCleanupPolicy);
671
+ const parsed = parseStorageCleanupPolicyInput(raw, previous);
672
+ if (!parsed.ok) return jsonResponse({ error: parsed.error }, 400);
673
+ // Never enable implicitly: if client omitted enabled, keep previous (default false).
674
+ const body = raw as Record<string, unknown>;
675
+ if (body.enabled === undefined) parsed.policy.enabled = previous.enabled;
676
+ const saved = writeStorageCleanupPolicyToConfig(parsed.policy);
677
+ config.storageCleanupPolicy = saved;
678
+ return jsonResponse({ ok: true, policy: saved, job: getStorageCleanupPolicyJobState() });
679
+ }
680
+
681
+ if (url.pathname === "/api/storage/cleanup-policy/run" && req.method === "POST") {
682
+ try {
683
+ const accepted = requestStorageCleanupPolicyRun({ reason: "manual", force: true });
684
+ if (!accepted.accepted) {
685
+ return jsonResponse({
686
+ ok: false,
687
+ started: false,
688
+ error: "already_running",
689
+ message: "A cleanup policy run is already in progress.",
690
+ job: accepted.state,
691
+ policy: normalizeStorageCleanupPolicy(config.storageCleanupPolicy),
692
+ }, 409);
693
+ }
694
+ // Return promptly — clients poll GET for skip/defer/success/error outcomes.
695
+ return jsonResponse({
696
+ ok: true,
697
+ started: true,
698
+ job: accepted.state,
699
+ policy: normalizeStorageCleanupPolicy(config.storageCleanupPolicy),
700
+ });
701
+ } catch {
702
+ return jsonResponse({ ok: false, error: "cleanup_failed" }, 500);
703
+ }
704
+ }
705
+
706
+ return null;
707
+ }