@coseung2/opencodex 2.8.0-cs.1

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 (451) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -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-notch.mjs +78 -0
  8. package/bin/ocx.mjs +454 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-CAnnes06.js +67 -0
  11. package/gui/dist/assets/index-OY43ubAq.css +1 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/icons.svg +24 -0
  14. package/gui/dist/index.html +25 -0
  15. package/gui/dist/logo.png +0 -0
  16. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  18. package/gui/dist/provider-icons/antigravity.svg +1 -0
  19. package/gui/dist/provider-icons/claude-color.svg +1 -0
  20. package/gui/dist/provider-icons/claude.svg +1 -0
  21. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  23. package/gui/dist/provider-icons/copilot.svg +1 -0
  24. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  25. package/gui/dist/provider-icons/cursor.svg +2 -0
  26. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  27. package/gui/dist/provider-icons/discord.svg +1 -0
  28. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  29. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  31. package/gui/dist/provider-icons/gemini.svg +1 -0
  32. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  33. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok-color.svg +1 -0
  35. package/gui/dist/provider-icons/grok.svg +1 -0
  36. package/gui/dist/provider-icons/groq-color.svg +1 -0
  37. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  38. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  39. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  40. package/gui/dist/provider-icons/kiro.svg +14 -0
  41. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  42. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  43. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  44. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  45. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  46. package/gui/dist/provider-icons/openai.svg +1 -0
  47. package/gui/dist/provider-icons/opencode.svg +1 -0
  48. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  49. package/gui/dist/provider-icons/pi.svg +21 -0
  50. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  51. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  52. package/gui/dist/provider-icons/telegram.svg +1 -0
  53. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  54. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  55. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  56. package/package.json +107 -0
  57. package/packages/ocx-notch/README.md +55 -0
  58. package/packages/ocx-notch/THIRD_PARTY_NOTICES.md +45 -0
  59. package/src/AGENTS.md +28 -0
  60. package/src/adapters/anthropic-image-guard.ts +251 -0
  61. package/src/adapters/anthropic-image-normalize.ts +518 -0
  62. package/src/adapters/anthropic.ts +1003 -0
  63. package/src/adapters/azure.ts +36 -0
  64. package/src/adapters/base.ts +72 -0
  65. package/src/adapters/client-fingerprint.ts +59 -0
  66. package/src/adapters/cursor/arg-codec.ts +38 -0
  67. package/src/adapters/cursor/arg-normalize.ts +104 -0
  68. package/src/adapters/cursor/cursor-errors.ts +165 -0
  69. package/src/adapters/cursor/discovery.ts +276 -0
  70. package/src/adapters/cursor/effort-map.ts +127 -0
  71. package/src/adapters/cursor/exec-policy.ts +88 -0
  72. package/src/adapters/cursor/framing.ts +211 -0
  73. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  74. package/src/adapters/cursor/kv-store.ts +52 -0
  75. package/src/adapters/cursor/live-models.ts +153 -0
  76. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  77. package/src/adapters/cursor/live-transport.ts +1214 -0
  78. package/src/adapters/cursor/mcp-config.ts +42 -0
  79. package/src/adapters/cursor/mcp-manager.ts +333 -0
  80. package/src/adapters/cursor/message-mapper.ts +49 -0
  81. package/src/adapters/cursor/native-exec-common.ts +55 -0
  82. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  83. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  84. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  85. package/src/adapters/cursor/native-exec-network.ts +43 -0
  86. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  87. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  88. package/src/adapters/cursor/native-exec.ts +576 -0
  89. package/src/adapters/cursor/protobuf-events.ts +563 -0
  90. package/src/adapters/cursor/protobuf-request.ts +714 -0
  91. package/src/adapters/cursor/request-builder.ts +255 -0
  92. package/src/adapters/cursor/thread-continuity.ts +67 -0
  93. package/src/adapters/cursor/tool-definitions.ts +505 -0
  94. package/src/adapters/cursor/transport-retry.ts +132 -0
  95. package/src/adapters/cursor/transport.ts +57 -0
  96. package/src/adapters/cursor/types.ts +52 -0
  97. package/src/adapters/cursor.ts +196 -0
  98. package/src/adapters/google-antigravity-replay.ts +303 -0
  99. package/src/adapters/google-antigravity-wire.ts +108 -0
  100. package/src/adapters/google-errors.ts +85 -0
  101. package/src/adapters/google-http.ts +100 -0
  102. package/src/adapters/google-tool-schema.ts +173 -0
  103. package/src/adapters/google-truncation.ts +13 -0
  104. package/src/adapters/google-wire-compiler.ts +232 -0
  105. package/src/adapters/google.ts +758 -0
  106. package/src/adapters/identity.ts +44 -0
  107. package/src/adapters/image.ts +23 -0
  108. package/src/adapters/kiro-constants.ts +16 -0
  109. package/src/adapters/kiro-errors.ts +197 -0
  110. package/src/adapters/kiro-events.ts +179 -0
  111. package/src/adapters/kiro-images.ts +129 -0
  112. package/src/adapters/kiro-retry.ts +312 -0
  113. package/src/adapters/kiro-thinking.ts +96 -0
  114. package/src/adapters/kiro-tool-fallback.ts +36 -0
  115. package/src/adapters/kiro-tools.ts +215 -0
  116. package/src/adapters/kiro-truncation.ts +33 -0
  117. package/src/adapters/kiro-wire.ts +129 -0
  118. package/src/adapters/kiro.ts +1898 -0
  119. package/src/adapters/mimo-free.ts +263 -0
  120. package/src/adapters/openai-chat.ts +1005 -0
  121. package/src/adapters/openai-responses.ts +1137 -0
  122. package/src/adapters/run-turn-queue.ts +114 -0
  123. package/src/adapters/tool-catalog-nudge.ts +71 -0
  124. package/src/adapters/upstream-http-error.ts +48 -0
  125. package/src/bridge.ts +1619 -0
  126. package/src/chat/inbound.ts +295 -0
  127. package/src/chat/outbound.ts +765 -0
  128. package/src/claude/agents-inject.ts +243 -0
  129. package/src/claude/alias.ts +149 -0
  130. package/src/claude/auth-detect.ts +229 -0
  131. package/src/claude/auth-mode-migration.ts +32 -0
  132. package/src/claude/auth-mode.ts +62 -0
  133. package/src/claude/context-windows.ts +189 -0
  134. package/src/claude/desktop-3p-guard.ts +35 -0
  135. package/src/claude/desktop-3p-paths.ts +84 -0
  136. package/src/claude/desktop-3p.ts +381 -0
  137. package/src/claude/desktop-health.ts +26 -0
  138. package/src/claude/desktop-profile.ts +263 -0
  139. package/src/claude/gateway-cache.ts +70 -0
  140. package/src/claude/inbound-debug.ts +163 -0
  141. package/src/claude/inbound.ts +509 -0
  142. package/src/claude/model-info.ts +151 -0
  143. package/src/claude/outbound.ts +872 -0
  144. package/src/cli/access.ts +108 -0
  145. package/src/cli/account-api.ts +268 -0
  146. package/src/cli/account-auth.ts +223 -0
  147. package/src/cli/account-extended.ts +350 -0
  148. package/src/cli/account.ts +275 -0
  149. package/src/cli/agent-driven.ts +70 -0
  150. package/src/cli/agent.ts +184 -0
  151. package/src/cli/catalog-prewarm.ts +27 -0
  152. package/src/cli/claude-desktop.ts +188 -0
  153. package/src/cli/claude.ts +286 -0
  154. package/src/cli/codex-shim-autorestore.ts +45 -0
  155. package/src/cli/combo.ts +119 -0
  156. package/src/cli/config-command.ts +145 -0
  157. package/src/cli/debug.ts +228 -0
  158. package/src/cli/doctor.ts +930 -0
  159. package/src/cli/export-command.ts +187 -0
  160. package/src/cli/help.ts +330 -0
  161. package/src/cli/index.ts +1103 -0
  162. package/src/cli/init.ts +224 -0
  163. package/src/cli/integrations.ts +142 -0
  164. package/src/cli/interactive-confirm.ts +133 -0
  165. package/src/cli/internal-dispatch.ts +20 -0
  166. package/src/cli/models-runtime.ts +212 -0
  167. package/src/cli/models.ts +336 -0
  168. package/src/cli/observe.ts +117 -0
  169. package/src/cli/opencode.ts +586 -0
  170. package/src/cli/provider-runtime.ts +162 -0
  171. package/src/cli/provider.ts +463 -0
  172. package/src/cli/runtime-api.ts +325 -0
  173. package/src/cli/star-prompt.ts +134 -0
  174. package/src/cli/status-oauth.ts +78 -0
  175. package/src/cli/status.ts +321 -0
  176. package/src/cli/system-command.ts +114 -0
  177. package/src/cli/tray-proxy.ts +52 -0
  178. package/src/cli/v2.ts +173 -0
  179. package/src/cli.ts +10 -0
  180. package/src/clients/config-export.ts +377 -0
  181. package/src/codex/account-id.ts +34 -0
  182. package/src/codex/account-label.ts +34 -0
  183. package/src/codex/account-lifecycle.ts +55 -0
  184. package/src/codex/account-namespace-match.ts +63 -0
  185. package/src/codex/account-namespaces.ts +149 -0
  186. package/src/codex/account-pause.ts +20 -0
  187. package/src/codex/account-runtime-state.ts +31 -0
  188. package/src/codex/account-store.ts +517 -0
  189. package/src/codex/account-usability.ts +20 -0
  190. package/src/codex/app-server-processes.ts +756 -0
  191. package/src/codex/auth-api.ts +1540 -0
  192. package/src/codex/auth-collision.ts +107 -0
  193. package/src/codex/auth-context.ts +352 -0
  194. package/src/codex/autostart-health.ts +149 -0
  195. package/src/codex/catalog/aggregation.ts +378 -0
  196. package/src/codex/catalog/bundled.ts +251 -0
  197. package/src/codex/catalog/effort.ts +355 -0
  198. package/src/codex/catalog/metadata.ts +180 -0
  199. package/src/codex/catalog/parsing.ts +456 -0
  200. package/src/codex/catalog/provider-fetch.ts +902 -0
  201. package/src/codex/catalog/sync.ts +620 -0
  202. package/src/codex/catalog.ts +12 -0
  203. package/src/codex/data/upstream-models.json +830 -0
  204. package/src/codex/exec-invocation.ts +22 -0
  205. package/src/codex/features.ts +969 -0
  206. package/src/codex/history-migration-guardian.ts +102 -0
  207. package/src/codex/history-provider.ts +776 -0
  208. package/src/codex/home.ts +206 -0
  209. package/src/codex/inject.ts +799 -0
  210. package/src/codex/injected-marker.ts +72 -0
  211. package/src/codex/journal.ts +163 -0
  212. package/src/codex/main-account-cache.ts +32 -0
  213. package/src/codex/main-account.ts +40 -0
  214. package/src/codex/model-cache.ts +227 -0
  215. package/src/codex/paths.ts +65 -0
  216. package/src/codex/plugins-doctor.ts +242 -0
  217. package/src/codex/pool-rotation.ts +225 -0
  218. package/src/codex/project-config-warnings.ts +411 -0
  219. package/src/codex/quota.ts +411 -0
  220. package/src/codex/refresh.ts +53 -0
  221. package/src/codex/routing.ts +1477 -0
  222. package/src/codex/runtime.ts +538 -0
  223. package/src/codex/shim.ts +1189 -0
  224. package/src/codex/subagent-defaults.ts +550 -0
  225. package/src/codex/subagent-model-fallback.ts +469 -0
  226. package/src/codex/sync.ts +130 -0
  227. package/src/codex/warmup.ts +192 -0
  228. package/src/codex/websocket-registry.ts +100 -0
  229. package/src/combos/failover.ts +140 -0
  230. package/src/combos/index.ts +41 -0
  231. package/src/combos/request.ts +62 -0
  232. package/src/combos/resolve.ts +232 -0
  233. package/src/combos/types.ts +326 -0
  234. package/src/config.ts +2355 -0
  235. package/src/generated/jawcode-model-metadata.ts +104 -0
  236. package/src/github/star-state.ts +203 -0
  237. package/src/grok/inject.ts +519 -0
  238. package/src/grok/status.ts +121 -0
  239. package/src/grok/sync.ts +66 -0
  240. package/src/images/artifacts.ts +516 -0
  241. package/src/images/fulfill-video.ts +163 -0
  242. package/src/images/fulfill.ts +149 -0
  243. package/src/images/index.ts +4 -0
  244. package/src/images/loop.ts +829 -0
  245. package/src/images/plan.ts +133 -0
  246. package/src/images/synthetic-tool.ts +133 -0
  247. package/src/images/types.ts +41 -0
  248. package/src/images/xai-client.ts +141 -0
  249. package/src/images/xai-video-client.ts +163 -0
  250. package/src/index.ts +22 -0
  251. package/src/lib/abort.ts +146 -0
  252. package/src/lib/admin-secrets.ts +25 -0
  253. package/src/lib/admission.ts +83 -0
  254. package/src/lib/app-owned-memory-stores.ts +173 -0
  255. package/src/lib/app-owned-memory.ts +265 -0
  256. package/src/lib/bounded-body.ts +202 -0
  257. package/src/lib/bun-binary-validator.d.mts +3 -0
  258. package/src/lib/bun-binary-validator.mjs +18 -0
  259. package/src/lib/bun-runtime.ts +71 -0
  260. package/src/lib/bun-stream-caps.ts +126 -0
  261. package/src/lib/config-ownership.ts +360 -0
  262. package/src/lib/crash-guard.ts +344 -0
  263. package/src/lib/debug-log-buffer.ts +83 -0
  264. package/src/lib/debug-settings.ts +108 -0
  265. package/src/lib/debug.ts +31 -0
  266. package/src/lib/destination-policy.ts +316 -0
  267. package/src/lib/errors.ts +364 -0
  268. package/src/lib/eventstream-decoder.ts +253 -0
  269. package/src/lib/gcp-adc.ts +341 -0
  270. package/src/lib/injection-debug-log.ts +58 -0
  271. package/src/lib/open-url.ts +25 -0
  272. package/src/lib/pinned-http.ts +151 -0
  273. package/src/lib/privacy.ts +20 -0
  274. package/src/lib/process-control.ts +165 -0
  275. package/src/lib/provider-outbound.ts +170 -0
  276. package/src/lib/provider-url.ts +14 -0
  277. package/src/lib/proxy-env.ts +18 -0
  278. package/src/lib/redact.ts +105 -0
  279. package/src/lib/retry-after.ts +55 -0
  280. package/src/lib/service-secrets.ts +25 -0
  281. package/src/lib/shadow-call.ts +30 -0
  282. package/src/lib/sidecar-tracker.ts +52 -0
  283. package/src/lib/sse-decoder.ts +323 -0
  284. package/src/lib/state-store-registrations.ts +109 -0
  285. package/src/lib/state-store-sweeper.ts +184 -0
  286. package/src/lib/test-home-guard.ts +90 -0
  287. package/src/lib/token-estimate.ts +69 -0
  288. package/src/lib/translator-budget.ts +356 -0
  289. package/src/lib/upstream-retry.ts +239 -0
  290. package/src/lib/win-exec.ts +115 -0
  291. package/src/lib/win-paths.ts +68 -0
  292. package/src/lib/windows-elevation.ts +705 -0
  293. package/src/lib/windows-secret-acl.ts +514 -0
  294. package/src/lib/winsw.ts +375 -0
  295. package/src/oauth/anthropic-routing.ts +594 -0
  296. package/src/oauth/anthropic.ts +177 -0
  297. package/src/oauth/callback-server.ts +294 -0
  298. package/src/oauth/chatgpt.ts +150 -0
  299. package/src/oauth/cursor.ts +211 -0
  300. package/src/oauth/github-copilot.ts +428 -0
  301. package/src/oauth/google-antigravity.ts +230 -0
  302. package/src/oauth/health.ts +399 -0
  303. package/src/oauth/index.ts +1173 -0
  304. package/src/oauth/key-providers.ts +108 -0
  305. package/src/oauth/kimi.ts +213 -0
  306. package/src/oauth/kiro-credentials.ts +726 -0
  307. package/src/oauth/kiro.ts +577 -0
  308. package/src/oauth/local-token-detect.ts +121 -0
  309. package/src/oauth/log.ts +48 -0
  310. package/src/oauth/login-cli.ts +163 -0
  311. package/src/oauth/pkce.ts +15 -0
  312. package/src/oauth/store.ts +630 -0
  313. package/src/oauth/token-guardian.ts +303 -0
  314. package/src/oauth/types.ts +62 -0
  315. package/src/oauth/xai.ts +241 -0
  316. package/src/providers/alibaba-region-backup.ts +75 -0
  317. package/src/providers/alibaba-region-migration.ts +156 -0
  318. package/src/providers/alibaba-region-startup.ts +36 -0
  319. package/src/providers/antigravity-models.ts +205 -0
  320. package/src/providers/api-keys.ts +140 -0
  321. package/src/providers/base-url-choices.ts +64 -0
  322. package/src/providers/context-cap.ts +65 -0
  323. package/src/providers/derive.ts +339 -0
  324. package/src/providers/free-directory.ts +184 -0
  325. package/src/providers/github-copilot-transport.ts +56 -0
  326. package/src/providers/key-failover.ts +203 -0
  327. package/src/providers/kiro-models.ts +67 -0
  328. package/src/providers/label.ts +19 -0
  329. package/src/providers/model-discovery.ts +356 -0
  330. package/src/providers/openai-sidecar.ts +175 -0
  331. package/src/providers/openai-tier-startup.ts +27 -0
  332. package/src/providers/openai-tiers.ts +301 -0
  333. package/src/providers/openai-virtual-models.ts +82 -0
  334. package/src/providers/openrouter-routing.ts +102 -0
  335. package/src/providers/provider-id-rewrite.ts +150 -0
  336. package/src/providers/quota.ts +1045 -0
  337. package/src/providers/registry.ts +1600 -0
  338. package/src/providers/slug-codec.ts +67 -0
  339. package/src/providers/xai-transport.ts +141 -0
  340. package/src/reasoning-effort.ts +135 -0
  341. package/src/responses/compaction.ts +117 -0
  342. package/src/responses/parser.ts +656 -0
  343. package/src/responses/reasoning-envelope.ts +52 -0
  344. package/src/responses/schema.ts +159 -0
  345. package/src/responses/spill-store.ts +394 -0
  346. package/src/responses/state.ts +895 -0
  347. package/src/responses/tool-groups.ts +19 -0
  348. package/src/router.ts +425 -0
  349. package/src/server/adapter-resolve.ts +80 -0
  350. package/src/server/auth-cors.ts +530 -0
  351. package/src/server/chat-completions.ts +368 -0
  352. package/src/server/claude-messages.ts +914 -0
  353. package/src/server/effort-policy.ts +172 -0
  354. package/src/server/gui-static.ts +123 -0
  355. package/src/server/image-retry.ts +42 -0
  356. package/src/server/images.ts +476 -0
  357. package/src/server/index.ts +1126 -0
  358. package/src/server/lifecycle.ts +305 -0
  359. package/src/server/live.ts +598 -0
  360. package/src/server/management/agent-settings-routes.ts +1039 -0
  361. package/src/server/management/api-access.ts +141 -0
  362. package/src/server/management/api-key-usage.ts +167 -0
  363. package/src/server/management/body.ts +35 -0
  364. package/src/server/management/combo-routes.ts +220 -0
  365. package/src/server/management/config-routes.ts +422 -0
  366. package/src/server/management/context.ts +31 -0
  367. package/src/server/management/logs-usage-routes.ts +516 -0
  368. package/src/server/management/model-routes.ts +525 -0
  369. package/src/server/management/oauth-account-routes.ts +561 -0
  370. package/src/server/management/provider-routes.ts +556 -0
  371. package/src/server/management/shared.ts +277 -0
  372. package/src/server/management/sidebar-routes.ts +88 -0
  373. package/src/server/management/system-restart.ts +179 -0
  374. package/src/server/management/system-routes.ts +117 -0
  375. package/src/server/management/usage-summary-cache.ts +86 -0
  376. package/src/server/management-api.ts +211 -0
  377. package/src/server/management-auth.ts +267 -0
  378. package/src/server/memory-watchdog.ts +156 -0
  379. package/src/server/port-reclaim.ts +307 -0
  380. package/src/server/ports.ts +116 -0
  381. package/src/server/proxy-liveness.ts +201 -0
  382. package/src/server/relay-eager.ts +313 -0
  383. package/src/server/relay.ts +1060 -0
  384. package/src/server/request-decompress.ts +132 -0
  385. package/src/server/request-log-conversation.ts +168 -0
  386. package/src/server/request-log.ts +1046 -0
  387. package/src/server/responses/collaboration.ts +354 -0
  388. package/src/server/responses/compact.ts +384 -0
  389. package/src/server/responses/core.ts +2756 -0
  390. package/src/server/responses/encrypted-payload.ts +308 -0
  391. package/src/server/responses/fetch-helpers.ts +157 -0
  392. package/src/server/responses/passthrough-error.ts +78 -0
  393. package/src/server/responses/terminal-guard.ts +230 -0
  394. package/src/server/responses/upstream-error.ts +48 -0
  395. package/src/server/responses-image-gen-repair.ts +132 -0
  396. package/src/server/responses-item-id-repair.ts +224 -0
  397. package/src/server/responses.ts +9 -0
  398. package/src/server/search.ts +136 -0
  399. package/src/server/sse-payload-rewrite.ts +175 -0
  400. package/src/server/startup-action-control.ts +308 -0
  401. package/src/server/startup-health-cache.ts +113 -0
  402. package/src/server/system-env.ts +413 -0
  403. package/src/server/windows-tcp-drop.ts +184 -0
  404. package/src/server/windows-tray-control.ts +41 -0
  405. package/src/server/ws-bridge.ts +471 -0
  406. package/src/service.ts +2554 -0
  407. package/src/stall-timeout.ts +20 -0
  408. package/src/storage/cleanup-job.ts +57 -0
  409. package/src/storage/cleanup.ts +3085 -0
  410. package/src/storage/policy-job.ts +457 -0
  411. package/src/storage/policy-scheduler.ts +40 -0
  412. package/src/storage/policy-worker.ts +59 -0
  413. package/src/storage/policy.ts +527 -0
  414. package/src/storage/restore-job.ts +299 -0
  415. package/src/storage/restore-worker.ts +58 -0
  416. package/src/storage/scanner.ts +238 -0
  417. package/src/storage/storage-mutation-coordinator.ts +139 -0
  418. package/src/storage/worker-lifecycle.ts +215 -0
  419. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  420. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  421. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  422. package/src/tray/assets/opencodex-tray.png +0 -0
  423. package/src/tray/windows-tray.ps1 +290 -0
  424. package/src/tray/windows.ts +730 -0
  425. package/src/types.ts +1237 -0
  426. package/src/update/badge.ts +72 -0
  427. package/src/update/index.ts +392 -0
  428. package/src/update/job.ts +1513 -0
  429. package/src/update/notify.ts +257 -0
  430. package/src/update/npm-invocation.d.mts +23 -0
  431. package/src/update/npm-invocation.mjs +94 -0
  432. package/src/update/tray-update-plan.d.mts +18 -0
  433. package/src/update/tray-update-plan.mjs +38 -0
  434. package/src/usage/cost.ts +0 -0
  435. package/src/usage/debug.ts +97 -0
  436. package/src/usage/expected-prices.ts +164 -0
  437. package/src/usage/log.ts +658 -0
  438. package/src/usage/summary.ts +585 -0
  439. package/src/usage/totals.ts +14 -0
  440. package/src/vision/anthropic-describe.ts +185 -0
  441. package/src/vision/describe.ts +125 -0
  442. package/src/vision/index.ts +467 -0
  443. package/src/web-search/anthropic-executor.ts +189 -0
  444. package/src/web-search/executor.ts +105 -0
  445. package/src/web-search/format-result.ts +89 -0
  446. package/src/web-search/index.ts +196 -0
  447. package/src/web-search/loop.ts +664 -0
  448. package/src/web-search/parse.ts +220 -0
  449. package/src/web-search/progress-stream.ts +342 -0
  450. package/src/web-search/synthetic-tool.ts +47 -0
  451. package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
@@ -0,0 +1,516 @@
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 { filterRequestLogs, filteredRequestLogCount, getRequestLogEntries, type RequestLogEntry } from "../request-log";
72
+ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
73
+ import type { PersistedUsageAttempt } from "../../usage/log";
74
+ import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
75
+ import { applySystemEnvToggle } from "../system-env";
76
+
77
+ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
78
+ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
79
+ import type { ManagementContext } from "./context";
80
+ import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
81
+ import {
82
+ discardUsageSummaryCacheEntry,
83
+ getUsageSummaryCacheEntry,
84
+ setUsageSummaryCacheEntry,
85
+ } from "./usage-summary-cache";
86
+
87
+ const USAGE_DAY_MS = 86_400_000;
88
+ function usageEntryMatchesSurface(entry: PersistedUsageEntry, surface: UsageSurface): boolean {
89
+ if (surface === "claude") return entry.surface === "claude" || entry.surface === "claude-desktop";
90
+ if (surface === "grok") return entry.surface === "grok";
91
+ if (surface === "codex") return entry.surface === undefined;
92
+ return true;
93
+ }
94
+
95
+ function nextLocalMidnight(now: number): number {
96
+ const next = new Date(now);
97
+ next.setHours(24, 0, 0, 0);
98
+ return next.getTime();
99
+ }
100
+
101
+ function usageSummaryExpiresAt(
102
+ entries: PersistedUsageEntry[],
103
+ range: UsageRange,
104
+ surface: UsageSurface,
105
+ now: number,
106
+ ): number {
107
+ let expiresAt = nextLocalMidnight(now);
108
+ const windowMs = range === "7d" ? 7 * USAGE_DAY_MS : range === "30d" ? 30 * USAGE_DAY_MS : null;
109
+ if (windowMs === null) return expiresAt;
110
+ for (const entry of entries) {
111
+ if (!usageEntryMatchesSurface(entry, surface)) continue;
112
+ const expiry = entry.timestamp + windowMs;
113
+ if (expiry > now && expiry < expiresAt) expiresAt = expiry;
114
+ }
115
+ return expiresAt;
116
+ }
117
+
118
+ function refreshedUsageSummary<T extends UsageSummary & { historyTruncated: boolean }>(summary: T, range: UsageRange, now: number): T {
119
+ const since = range === "7d" ? now - 7 * USAGE_DAY_MS : range === "30d" ? now - 30 * USAGE_DAY_MS : null;
120
+ return { ...summary, since, generatedAt: now };
121
+ }
122
+
123
+ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Response | null> {
124
+ const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
125
+
126
+ if (url.pathname === "/api/logs" && req.method === "GET") {
127
+ const all = getRequestLogEntries();
128
+ const total = filteredRequestLogCount(all, url.searchParams);
129
+ const logs = filterRequestLogs(all, url.searchParams);
130
+ return jsonResponse({
131
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
132
+ total,
133
+ logs: logs.map(requestLogDto),
134
+ });
135
+ }
136
+
137
+ if (url.pathname === "/api/debug" && req.method === "GET") {
138
+ return jsonResponse(getDebugSettings());
139
+ }
140
+
141
+ if (url.pathname === "/api/debug/logs" && req.method === "GET") {
142
+ const { after, limit } = parseDebugLogQuery(url);
143
+ return jsonResponse(getDebugLogEntries({ after, limit }));
144
+ }
145
+
146
+ if (url.pathname === "/api/debug/usage-logs" && req.method === "GET") {
147
+ const { after, limit } = parseDebugLogQuery(url);
148
+ return jsonResponse(getUsageDebugLogEntries({ after, limit }));
149
+ }
150
+
151
+ if (url.pathname === "/api/claude/inbound-debug" && req.method === "GET") {
152
+ const { getClaudeInboundDebugEntries } = await import("../../claude/inbound-debug");
153
+ const { isClaudeDebugEnabled } = await import("../../lib/debug-settings");
154
+ return jsonResponse({ enabled: isClaudeDebugEnabled(), entries: getClaudeInboundDebugEntries() });
155
+ }
156
+
157
+ if (url.pathname === "/api/debug/injection-logs" && req.method === "GET") {
158
+ const { after, limit } = parseDebugLogQuery(url);
159
+ return jsonResponse(getInjectionDebugLogEntries({ after, limit }));
160
+ }
161
+
162
+ if (url.pathname === "/api/debug" && req.method === "PUT") {
163
+ let body: { debug?: unknown; usage?: unknown; injection?: unknown; claude?: unknown; reset?: unknown };
164
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
165
+ if (body.reset === true) return jsonResponse(clearDebugSettings());
166
+ if (body.reset === "debug" || body.reset === "provider") return jsonResponse(clearDebugSetting("debug"));
167
+ if (body.reset === "usage") return jsonResponse(clearDebugSetting("usage"));
168
+ if (body.reset === "injection") return jsonResponse(clearDebugSetting("injection"));
169
+ if (body.reset === "claude") return jsonResponse(clearDebugSetting("claude"));
170
+ const partial: Partial<Record<DebugFlag, boolean>> = {};
171
+ for (const key of ["debug", "usage", "injection", "claude"] as const) {
172
+ if (body[key] === undefined) continue;
173
+ if (typeof body[key] !== "boolean") return jsonResponse({ error: `${key} must be a boolean` }, 400);
174
+ partial[key] = body[key];
175
+ }
176
+ if (Object.keys(partial).length === 0) {
177
+ return jsonResponse({ error: "provide debug/usage/injection/claude booleans or reset:true" }, 400);
178
+ }
179
+ // Turning capture off should also flush already-captured entries (privacy contract).
180
+ if (partial.claude === false) {
181
+ const { clearClaudeInboundDebug } = await import("../../claude/inbound-debug");
182
+ clearClaudeInboundDebug();
183
+ }
184
+ return jsonResponse(setDebugSettings(partial));
185
+ }
186
+
187
+ if (url.pathname === "/api/usage" && req.method === "GET") {
188
+ const range = parseRange(url.searchParams.get("range"));
189
+ const surface = parseUsageSurface(url.searchParams.get("surface"));
190
+ const now = Date.now();
191
+ try {
192
+ const cacheKey = `${range}:${surface}`;
193
+ const effectiveReadLimit = config.managementUsageMaxReadBytes ?? 64 * 1024 * 1024;
194
+ const observedRevisionKey = `${usageLogRevisionKey(currentUsageLogRevision())}\0${effectiveReadLimit}`;
195
+ const cached = getUsageSummaryCacheEntry(cacheKey);
196
+ if (cached && cached.revisionKey === observedRevisionKey && now < cached.expiresAt) {
197
+ return jsonResponse(refreshedUsageSummary(cached.summary, range, now));
198
+ }
199
+ if (cached) discardUsageSummaryCacheEntry(cacheKey);
200
+ const snapshot = await readUsageSnapshotForManagement(effectiveReadLimit);
201
+ const revisionReadAt = Date.now();
202
+ const summary = {
203
+ ...summarizeUsage(snapshot.entries, range, now, surface),
204
+ historyTruncated: snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated,
205
+ truncatedPrefixBytes: snapshot.truncatedPrefixBytes,
206
+ entriesTruncated: snapshot.entriesTruncated,
207
+ entriesDropped: snapshot.entriesDropped,
208
+ };
209
+ setUsageSummaryCacheEntry(cacheKey, {
210
+ revisionKey: `${usageLogRevisionKey(snapshot.revision)}\0${effectiveReadLimit}`,
211
+ expiresAt: usageSummaryExpiresAt(snapshot.entries, range, surface, now),
212
+ revisionReadAt,
213
+ summary,
214
+ });
215
+ return jsonResponse(summary);
216
+ } catch {
217
+ return jsonResponse({
218
+ range,
219
+ surface,
220
+ since: null,
221
+ generatedAt: now,
222
+ summary: {
223
+ requests: 0,
224
+ attemptCount: 0,
225
+ measuredRequests: 0,
226
+ reportedRequests: 0,
227
+ unreportedRequests: 0,
228
+ unsupportedRequests: 0,
229
+ estimatedRequests: 0,
230
+ inputTokens: 0,
231
+ outputTokens: 0,
232
+ cachedInputTokens: 0,
233
+ cacheReadInputTokens: 0,
234
+ cacheCreationInputTokens: 0,
235
+ reasoningOutputTokens: 0,
236
+ totalTokens: 0,
237
+ coverageRatio: 0,
238
+ estimatedCostUsd: 0,
239
+ pricedRequests: 0,
240
+ unpricedRequests: 0,
241
+ unmeteredRequests: 0,
242
+ },
243
+ days: [],
244
+ models: [],
245
+ providers: [],
246
+ historyTruncated: false,
247
+ truncatedPrefixBytes: 0,
248
+ entriesTruncated: false,
249
+ entriesDropped: 0,
250
+ error: "read_failed",
251
+ });
252
+ }
253
+ }
254
+
255
+ if (url.pathname === "/api/storage" && req.method === "GET") {
256
+ try {
257
+ return jsonResponse(scanStorage());
258
+ } catch {
259
+ return jsonResponse({
260
+ codexHome: resolveCodexHomeDir(),
261
+ generatedAt: Date.now(),
262
+ total: { bytes: 0, fileCount: 0 },
263
+ buckets: [],
264
+ error: "scan_failed",
265
+ });
266
+ }
267
+ }
268
+
269
+ if (url.pathname === "/api/storage/cleanup/preview" && req.method === "POST") {
270
+ let body: { percent?: unknown };
271
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
272
+ const percent = typeof body?.percent === "number" ? body.percent : Number.NaN;
273
+ if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
274
+ return jsonResponse({ error: "invalid_percent" }, 400);
275
+ }
276
+ const preview = previewArchivedCleanup(percent);
277
+ // Omit absolute host paths (codexHome / absPath) from the wire response.
278
+ return jsonResponse({
279
+ percent: preview.percent,
280
+ count: preview.count,
281
+ bytes: preview.bytes,
282
+ digest: preview.digest,
283
+ // Dashboard only lists a handful; count/bytes/digest already bind the full set.
284
+ candidates: preview.candidates.slice(0, 50).map(({ relPath, bytes, mtimeMs, physicalRelPaths }) => ({
285
+ relPath,
286
+ bytes,
287
+ mtimeMs,
288
+ physicalRelPaths,
289
+ })),
290
+ });
291
+ }
292
+
293
+ if (url.pathname === "/api/storage/cleanup" && req.method === "POST") {
294
+ let body: { percent?: unknown; mode?: unknown; digest?: unknown; _test?: unknown };
295
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
296
+ const percent = typeof body?.percent === "number" ? body.percent : Number.NaN;
297
+ if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
298
+ return jsonResponse({ error: "invalid_percent" }, 400);
299
+ }
300
+ const mode = body?.mode;
301
+ if (mode !== "quarantine" && mode !== "permanent") {
302
+ return jsonResponse({ error: "invalid_mode" }, 400);
303
+ }
304
+ const digest = typeof body?.digest === "string" ? body.digest : "";
305
+ const testHooks =
306
+ process.env.OPENCODEX_CLEANUP_TEST_HOOKS === "1" &&
307
+ body &&
308
+ typeof body === "object" &&
309
+ "_test" in body
310
+ ? pickWireCleanupTestHooks(body._test)
311
+ : undefined;
312
+ try {
313
+ const result = await runArchivedCleanupJob({
314
+ percent,
315
+ mode: mode as CleanupMode,
316
+ digest,
317
+ ...(testHooks ? { _test: testHooks } : {}),
318
+ });
319
+ if (!result.ok) {
320
+ const status =
321
+ result.error === "codex_busy"
322
+ || result.error === "stale_preview"
323
+ || result.error === "referenced_history"
324
+ || result.error === "pinned_thread"
325
+ || result.error === "storage_mutation_busy"
326
+ || result.error === "restore_pending_overlap"
327
+ ? 409
328
+ : result.error === "invalid_mode" || result.error === "invalid_digest"
329
+ ? 400
330
+ : 500;
331
+ const messages: Record<string, string> = {
332
+ codex_busy: "Codex is using state.sqlite — try again after quitting Codex.",
333
+ storage_mutation_busy: "Another storage cleanup or restore is in progress — try again shortly.",
334
+ stale_preview: "Archived files changed since preview — run Preview again.",
335
+ restore_pending_overlap: "Selected archives overlap an incomplete trash restore — finish or retry restore first.",
336
+ referenced_history: "Selected archives are still referenced by forked or paginated history.",
337
+ pinned_thread: "Selected archives include a pinned thread — unpin it in Codex before cleanup.",
338
+ invalid_digest: "Preview digest is missing or invalid.",
339
+ invalid_mode: "mode must be quarantine or permanent.",
340
+ fs_failed: "Filesystem cleanup failed. Some changes may already be applied — check CODEX_HOME/.trash and any recovery path in the response.",
341
+ db_reconcile_failed: "Could not update Codex state database.",
342
+ cleanup_failed: "Cleanup failed.",
343
+ };
344
+ return jsonResponse({
345
+ ok: false,
346
+ error: result.error ?? "cleanup_failed",
347
+ message: messages[result.error ?? ""] ?? messages.cleanup_failed,
348
+ ...(result.trashDir ? { trashDir: result.trashDir } : {}),
349
+ }, status);
350
+ }
351
+ return jsonResponse({
352
+ ok: true,
353
+ mode: result.mode,
354
+ percent: result.percent,
355
+ count: result.count,
356
+ bytes: result.bytes,
357
+ ...(result.trashDir ? { trashDir: result.trashDir } : {}),
358
+ removedPaths: result.removedPaths,
359
+ });
360
+ } catch {
361
+ return jsonResponse({
362
+ ok: false,
363
+ error: "cleanup_failed",
364
+ message: "Cleanup failed.",
365
+ }, 500);
366
+ }
367
+ }
368
+
369
+ if (url.pathname === "/api/storage/trash" && req.method === "GET") {
370
+ try {
371
+ const entries = listTrashEntries();
372
+ return jsonResponse({
373
+ entries: entries.map(({ id, epoch, fileCount, bytes, quarantinedAt, mode }) => ({
374
+ id,
375
+ epoch,
376
+ fileCount,
377
+ bytes,
378
+ ...(quarantinedAt !== undefined ? { quarantinedAt } : {}),
379
+ ...(mode ? { mode } : {}),
380
+ })),
381
+ });
382
+ } catch {
383
+ return jsonResponse({ error: "trash_list_failed", entries: [] }, 500);
384
+ }
385
+ }
386
+
387
+ if (url.pathname === "/api/storage/trash/restore/test-stream" && req.method === "GET") {
388
+ if (process.env.OPENCODEX_CLEANUP_TEST_HOOKS === "1") {
389
+ const stream = getRestoreTrashTestStreamResponse();
390
+ if (stream) return stream;
391
+ }
392
+ // Always answer this test-only path — never fall through to the GUI SPA (200 HTML).
393
+ return jsonResponse({ error: "not_available" }, 404);
394
+ }
395
+
396
+ if (url.pathname === "/api/storage/trash/restore" && req.method === "POST") {
397
+ let body: { id?: unknown };
398
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
399
+ const id = typeof body?.id === "string" ? body.id : "";
400
+ if (!id.trim()) {
401
+ return jsonResponse({ error: "invalid_trash", message: "Trash entry id is required." }, 400);
402
+ }
403
+ try {
404
+ const result = await runRestoreTrashEntryJob(id);
405
+ if (!result.ok) {
406
+ const status =
407
+ result.error === "codex_busy"
408
+ || result.error === "dest_exists"
409
+ || result.error === "storage_mutation_busy"
410
+ ? 409
411
+ : result.error === "missing_trash"
412
+ ? 404
413
+ : result.error === "invalid_trash"
414
+ ? 400
415
+ : 500;
416
+ const messages: Record<RestoreErrorCode, string> = {
417
+ invalid_trash: "Trash entry id is missing or invalid.",
418
+ missing_trash: "Trash entry was not found.",
419
+ codex_busy: "Codex is using state.sqlite — try again after quitting Codex.",
420
+ storage_mutation_busy: "Another storage cleanup or restore is in progress — try again shortly.",
421
+ dest_exists: "Restore destination already exists — remove or rename the archived file and retry.",
422
+ fs_failed: "Filesystem restore failed. Some files may already be restored — check archived_sessions and .trash.",
423
+ db_reconcile_failed: "Could not restore Codex state database rows.",
424
+ restore_failed: "Restore failed.",
425
+ restore_worker_timeout: "Restore took too long (over 10 minutes) and was stopped.",
426
+ restore_worker_aborted: "Restore was cancelled during shutdown.",
427
+ restore_worker_failed: "Restore worker crashed or failed unexpectedly.",
428
+ };
429
+ const errorCode = result.error ?? "restore_failed";
430
+ const baseMessage = messages[errorCode] ?? messages.restore_failed;
431
+ const message =
432
+ result.message && errorCode === "restore_worker_failed"
433
+ ? `${baseMessage} (${result.message})`
434
+ : baseMessage;
435
+ return jsonResponse({
436
+ ok: false,
437
+ error: errorCode,
438
+ message,
439
+ count: result.count,
440
+ bytes: result.bytes,
441
+ restoredPaths: result.restoredPaths,
442
+ ...(result.trashDir ? { trashDir: result.trashDir } : {}),
443
+ }, status);
444
+ }
445
+ return jsonResponse({
446
+ ok: true,
447
+ trashDir: result.trashDir,
448
+ count: result.count,
449
+ bytes: result.bytes,
450
+ restoredPaths: result.restoredPaths,
451
+ });
452
+ } catch {
453
+ return jsonResponse({
454
+ ok: false,
455
+ error: "restore_failed",
456
+ message: "Restore failed.",
457
+ }, 500);
458
+ }
459
+ }
460
+
461
+ if (url.pathname === "/api/storage/cleanup-policy/test-stream" && req.method === "GET") {
462
+ const stream = getStorageCleanupPolicyTestStreamResponse();
463
+ if (stream) return stream;
464
+ // Production: hook is off. Return an explicit JSON 404 — do not fall through to the GUI.
465
+ return jsonResponse({ error: "not_found" }, 404);
466
+ }
467
+
468
+ if (url.pathname === "/api/storage/cleanup-policy" && req.method === "GET") {
469
+ const policy = normalizeStorageCleanupPolicy(config.storageCleanupPolicy);
470
+ return jsonResponse({
471
+ ...policy,
472
+ job: getStorageCleanupPolicyJobState(),
473
+ });
474
+ }
475
+
476
+ if (url.pathname === "/api/storage/cleanup-policy" && req.method === "PUT") {
477
+ let raw: unknown;
478
+ try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
479
+ const previous = normalizeStorageCleanupPolicy(config.storageCleanupPolicy);
480
+ const parsed = parseStorageCleanupPolicyInput(raw, previous);
481
+ if (!parsed.ok) return jsonResponse({ error: parsed.error }, 400);
482
+ // Never enable implicitly: if client omitted enabled, keep previous (default false).
483
+ const body = raw as Record<string, unknown>;
484
+ if (body.enabled === undefined) parsed.policy.enabled = previous.enabled;
485
+ const saved = writeStorageCleanupPolicyToConfig(parsed.policy);
486
+ config.storageCleanupPolicy = saved;
487
+ return jsonResponse({ ok: true, policy: saved, job: getStorageCleanupPolicyJobState() });
488
+ }
489
+
490
+ if (url.pathname === "/api/storage/cleanup-policy/run" && req.method === "POST") {
491
+ try {
492
+ const accepted = requestStorageCleanupPolicyRun({ reason: "manual", force: true });
493
+ if (!accepted.accepted) {
494
+ return jsonResponse({
495
+ ok: false,
496
+ started: false,
497
+ error: "already_running",
498
+ message: "A cleanup policy run is already in progress.",
499
+ job: accepted.state,
500
+ policy: normalizeStorageCleanupPolicy(config.storageCleanupPolicy),
501
+ }, 409);
502
+ }
503
+ // Return promptly — clients poll GET for skip/defer/success/error outcomes.
504
+ return jsonResponse({
505
+ ok: true,
506
+ started: true,
507
+ job: accepted.state,
508
+ policy: normalizeStorageCleanupPolicy(config.storageCleanupPolicy),
509
+ });
510
+ } catch {
511
+ return jsonResponse({ ok: false, error: "cleanup_failed" }, 500);
512
+ }
513
+ }
514
+
515
+ return null;
516
+ }