@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,141 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import { probeHostname } from "../proxy-liveness";
3
+
4
+ export interface ApiAccessEndpoints {
5
+ baseUrl: string;
6
+ responsesEndpoint: string;
7
+ chatCompletionsEndpoint: string;
8
+ messagesEndpoint: string;
9
+ modelsEndpoint: string;
10
+ claudeCodeEnabled: boolean;
11
+ /** Back-compat alias for older GUI clients. */
12
+ endpoint: string;
13
+ }
14
+
15
+ export type BuildApiAccessEndpointsOptions = {
16
+ /** Full request URL (preferred when the bind host is a wildcard). */
17
+ requestUrl?: string | URL | null;
18
+ /** Raw `Host` header from the inbound request. */
19
+ requestHost?: string | null;
20
+ /** Raw `Origin` header from the inbound request. */
21
+ requestOrigin?: string | null;
22
+ };
23
+
24
+ function isWildcardBindHost(hostname: string | undefined): boolean {
25
+ const trimmed = (hostname ?? "").trim();
26
+ return !trimmed || trimmed === "0.0.0.0" || trimmed === "::" || trimmed === "[::]";
27
+ }
28
+
29
+ /** Bracket bare IPv6 literals for URL authority composition. */
30
+ export function formatAuthorityHost(hostname: string): string {
31
+ const trimmed = hostname.trim();
32
+ if (!trimmed) return "127.0.0.1";
33
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed;
34
+ return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
35
+ }
36
+
37
+ function hostnameFromAuthority(authority: string): string | null {
38
+ const raw = authority.trim();
39
+ if (!raw) return null;
40
+ if (raw.startsWith("[")) {
41
+ const end = raw.indexOf("]");
42
+ if (end > 0) return raw.slice(1, end) || null;
43
+ return null;
44
+ }
45
+ // Host headers for IPv6 are bracketed; treat multi-colon values as unusable.
46
+ if (raw.includes(":") && raw.split(":").length > 2) return null;
47
+ const colon = raw.lastIndexOf(":");
48
+ if (colon > 0 && /^\d+$/.test(raw.slice(colon + 1))) {
49
+ return raw.slice(0, colon) || null;
50
+ }
51
+ return raw;
52
+ }
53
+
54
+ function originBaseUrl(raw: string): string | null {
55
+ try {
56
+ const url = new URL(raw);
57
+ if (!url.hostname || isWildcardBindHost(url.hostname)) return null;
58
+ return `${url.protocol}//${url.host}/v1`;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Prefer the client's request host/origin when the proxy is bound to a wildcard.
66
+ * Falls back to loopback only when no usable request context is available.
67
+ */
68
+ export function resolveApiAccessBaseUrl(
69
+ config: Pick<OcxConfig, "hostname" | "port">,
70
+ opts: BuildApiAccessEndpointsOptions = {},
71
+ ): string {
72
+ const port = config.port ?? 10100;
73
+
74
+ if (!isWildcardBindHost(config.hostname)) {
75
+ return `http://${probeHostname(config.hostname)}:${port}/v1`;
76
+ }
77
+
78
+ const fromOrigin = opts.requestOrigin ? originBaseUrl(opts.requestOrigin) : null;
79
+ if (fromOrigin) return fromOrigin;
80
+
81
+ if (opts.requestUrl) {
82
+ try {
83
+ const url = typeof opts.requestUrl === "string" ? new URL(opts.requestUrl) : opts.requestUrl;
84
+ if (url.hostname && !isWildcardBindHost(url.hostname)) {
85
+ return `${url.protocol}//${url.host}/v1`;
86
+ }
87
+ } catch {
88
+ /* ignore malformed request URL */
89
+ }
90
+ }
91
+
92
+ const hostHeader = opts.requestHost?.trim();
93
+ if (hostHeader) {
94
+ const hostname = hostnameFromAuthority(hostHeader);
95
+ if (hostname && !isWildcardBindHost(hostname)) {
96
+ // Preserve an explicit port from the Host header; otherwise use the bind port.
97
+ const hasPort = hostHeader.startsWith("[")
98
+ ? /\]:\d+$/.test(hostHeader)
99
+ : /:\d+$/.test(hostHeader) && hostHeader.split(":").length === 2;
100
+ const authority = hasPort
101
+ ? (hostHeader.startsWith("[") ? hostHeader : hostHeader)
102
+ : `${formatAuthorityHost(hostname)}:${port}`;
103
+ return `http://${authority}/v1`;
104
+ }
105
+ }
106
+
107
+ return `http://127.0.0.1:${port}/v1`;
108
+ }
109
+
110
+ /** @deprecated Prefer resolveApiAccessBaseUrl; retained for focused host-format tests. */
111
+ export function resolveApiAccessDisplayHost(
112
+ configHostname: string | undefined,
113
+ opts: BuildApiAccessEndpointsOptions = {},
114
+ ): string {
115
+ if (!isWildcardBindHost(configHostname)) {
116
+ return probeHostname(configHostname);
117
+ }
118
+ try {
119
+ return new URL(resolveApiAccessBaseUrl({ hostname: configHostname, port: 10100 }, opts)).hostname
120
+ || "127.0.0.1";
121
+ } catch {
122
+ return "127.0.0.1";
123
+ }
124
+ }
125
+
126
+ export function buildApiAccessEndpoints(
127
+ config: OcxConfig,
128
+ opts: BuildApiAccessEndpointsOptions = {},
129
+ ): ApiAccessEndpoints {
130
+ const baseUrl = resolveApiAccessBaseUrl(config, opts);
131
+ const responsesEndpoint = `${baseUrl}/responses`;
132
+ return {
133
+ baseUrl,
134
+ responsesEndpoint,
135
+ chatCompletionsEndpoint: `${baseUrl}/chat/completions`,
136
+ messagesEndpoint: `${baseUrl}/messages`,
137
+ modelsEndpoint: `${baseUrl}/models`,
138
+ claudeCodeEnabled: config.claudeCode?.enabled !== false,
139
+ endpoint: responsesEndpoint,
140
+ };
141
+ }
@@ -0,0 +1,167 @@
1
+ import {
2
+ currentUsageLogRevision,
3
+ readUsageSnapshotForManagement,
4
+ usageLogRevisionKey,
5
+ type PersistedUsageEntry,
6
+ } from "../../usage/log";
7
+
8
+ /**
9
+ * Per-key usage as the API tab renders it.
10
+ *
11
+ * A discriminated union rather than numbers with a flag beside them: when two
12
+ * config entries share an id there IS no per-key total, and an optional marker
13
+ * next to `requests7d: 7` invites a consumer to render the 7 anyway.
14
+ */
15
+ export type ApiKeyUsage =
16
+ | { ambiguous: true }
17
+ | { ambiguous?: false; requests7d: number; totalRequests: number; lastUsedAt?: string };
18
+
19
+ export interface ApiKeyUsageSnapshot {
20
+ rollup: Map<string, ApiKeyUsage>;
21
+ historyTruncated?: true;
22
+ /**
23
+ * Earliest row carrying a recognized `admissionKind`. A property of the DATA
24
+ * SET, not of a key, so it is singular and lives beside the map: it is what
25
+ * lets the GUI tell "this key was used zero times" from "nothing is
26
+ * attributable yet". Keyed on the kind rather than on `apiKeyId`, because an
27
+ * environment or loopback row is attributed traffic with no configured key.
28
+ */
29
+ attributionSince?: string;
30
+ }
31
+
32
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
33
+
34
+ /**
35
+ * A timestamp we can actually do date arithmetic with.
36
+ *
37
+ * `usage.jsonl` is hand-editable and JSON permits numbers outside the Date
38
+ * range: `1e309` survives normalization and then throws `RangeError` from
39
+ * `toISOString()`. Since the caller catches to protect key management, one bad
40
+ * row would have zeroed the rollup for EVERY key — active keys reported as
41
+ * unused is exactly the wrong answer to hand someone deciding what to delete.
42
+ */
43
+ function usableTimestamp(value: unknown): number | null {
44
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
45
+ return Number.isNaN(new Date(value).getTime()) ? null : value;
46
+ }
47
+
48
+ /**
49
+ * Pure: one pass over an already-read snapshot, so it is unit-testable without
50
+ * touching the filesystem.
51
+ *
52
+ * Rows are bucketed only when `admissionKind === "configured"`. Keying on
53
+ * `apiKeyId` alone would let a hand-edited entry whose id is `loopback` absorb
54
+ * traffic it never admitted.
55
+ */
56
+ export function rollupApiKeyUsage(
57
+ entries: PersistedUsageEntry[],
58
+ configuredIds: string[],
59
+ now: number = Date.now(),
60
+ ): ApiKeyUsageSnapshot {
61
+ const duplicated = new Set<string>();
62
+ const seen = new Set<string>();
63
+ for (const id of configuredIds) {
64
+ if (seen.has(id)) duplicated.add(id);
65
+ seen.add(id);
66
+ }
67
+
68
+ const totals = new Map<string, { requests7d: number; totalRequests: number; lastUsedAt?: string }>();
69
+ let attributionSince: number | undefined;
70
+ const cutoff = now - SEVEN_DAYS_MS;
71
+
72
+ for (const entry of entries) {
73
+ if (!entry.admissionKind) continue;
74
+ const timestamp = usableTimestamp(entry.timestamp);
75
+ if (timestamp !== null && (attributionSince === undefined || timestamp < attributionSince)) {
76
+ attributionSince = timestamp;
77
+ }
78
+ if (entry.admissionKind !== "configured" || !entry.apiKeyId) continue;
79
+
80
+ const bucket = totals.get(entry.apiKeyId) ?? { requests7d: 0, totalRequests: 0 };
81
+ // The request happened even if its clock reading is unusable, so it still
82
+ // counts toward the total; only the time-based fields are skipped.
83
+ bucket.totalRequests += 1;
84
+ if (timestamp !== null) {
85
+ if (timestamp >= cutoff) bucket.requests7d += 1;
86
+ const iso = new Date(timestamp).toISOString();
87
+ if (!bucket.lastUsedAt || iso > bucket.lastUsedAt) bucket.lastUsedAt = iso;
88
+ }
89
+ totals.set(entry.apiKeyId, bucket);
90
+ }
91
+
92
+ const rollup = new Map<string, ApiKeyUsage>();
93
+ for (const id of configuredIds) {
94
+ if (duplicated.has(id)) {
95
+ rollup.set(id, { ambiguous: true });
96
+ continue;
97
+ }
98
+ rollup.set(id, totals.get(id) ?? { requests7d: 0, totalRequests: 0 });
99
+ }
100
+ return {
101
+ rollup,
102
+ ...(attributionSince !== undefined ? { attributionSince: new Date(attributionSince).toISOString() } : {}),
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Rollup cache keyed by the exact usage-log revision, mirroring the /api/usage
108
+ * summary cache. Without it, every key-list read reparses an append-only log
109
+ * that only ever grows — and the GUI fetches this route on mount and after every
110
+ * create/rename/delete. The compact rollup is a handful of counters per key, so
111
+ * caching it costs nothing; a new row changes the revision and invalidates it.
112
+ */
113
+ let rollupCache: { revisionKey: string; expiresAt: number; snapshot: ApiKeyUsageSnapshot } | null = null;
114
+
115
+ /**
116
+ * The rollup is a function of the log AND of the clock: a request ages out of
117
+ * the seven-day window with no write to bump the file revision, so a purely
118
+ * revision-keyed entry would report a stale `requests7d` indefinitely.
119
+ *
120
+ * A minute is the whole rule. Deriving the exact next-transition instant would
121
+ * mean tracking the OLDEST counted request per key, which the compact rollup
122
+ * deliberately does not keep — and a count that can be at most 60s stale is
123
+ * already far tighter than the window it describes.
124
+ */
125
+ const ROLLUP_CACHE_TTL_MS = 60_000;
126
+
127
+ /** Test seam: the cache is module state and would otherwise leak between cases. */
128
+ export function clearApiKeyUsageCacheForTests(): void {
129
+ rollupCache = null;
130
+ }
131
+
132
+ /**
133
+ * Reads the durable usage snapshot the way /api/usage does, then rolls it up.
134
+ *
135
+ * Never throws: an unreadable snapshot yields empty rollups and no
136
+ * `attributionSince`. Key management working matters more than usage numbers
137
+ * being present, and the GUI already treats an absent field as "no data".
138
+ */
139
+ export async function readApiKeyUsageRollup(configuredIds: string[], maxReadBytes?: number): Promise<ApiKeyUsageSnapshot> {
140
+ // JSON rather than a joined string: ids are only validated as non-empty
141
+ // strings, so `["a\0b","c"]` and `["a","b\0c"]` join to the same value and one
142
+ // config could be served the other's cached rollup.
143
+ const idsKey = JSON.stringify([configuredIds, maxReadBytes]);
144
+ const now = Date.now();
145
+ try {
146
+ const observedKey = `${usageLogRevisionKey(currentUsageLogRevision())}|${idsKey}`;
147
+ if (rollupCache?.revisionKey === observedKey && now < rollupCache.expiresAt) {
148
+ return rollupCache.snapshot;
149
+ }
150
+
151
+ const snapshot = await readUsageSnapshotForManagement(maxReadBytes);
152
+ const rolled = {
153
+ ...rollupApiKeyUsage(snapshot.entries, configuredIds, now),
154
+ ...(snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated ? { historyTruncated: true as const } : {}),
155
+ };
156
+ rollupCache = {
157
+ revisionKey: `${usageLogRevisionKey(snapshot.revision)}|${idsKey}`,
158
+ expiresAt: now + ROLLUP_CACHE_TTL_MS,
159
+ snapshot: rolled,
160
+ };
161
+ return rolled;
162
+ } catch {
163
+ const rollup = new Map<string, ApiKeyUsage>();
164
+ for (const id of configuredIds) rollup.set(id, { requests7d: 0, totalRequests: 0 });
165
+ return { rollup };
166
+ }
167
+ }
@@ -0,0 +1,35 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import { jsonResponse } from "../auth-cors";
3
+ import {
4
+ DecompressedBodyTooLargeError,
5
+ readBoundedJsonRequestBody,
6
+ } from "../request-decompress";
7
+
8
+ export const MANAGEMENT_JSON_BODY_MAX_BYTES = 4 * 1024 * 1024;
9
+
10
+ export function readManagementJsonBody<T = unknown>(req: Request): Promise<T> {
11
+ return readBoundedJsonRequestBody(req, MANAGEMENT_JSON_BODY_MAX_BYTES) as Promise<T>;
12
+ }
13
+
14
+ export function managementBodyTooLargeResponse(
15
+ error: unknown,
16
+ req: Request,
17
+ config: OcxConfig,
18
+ ): Response | null {
19
+ return error instanceof DecompressedBodyTooLargeError
20
+ ? jsonResponse({ error: "request body too large" }, 413, req, config)
21
+ : null;
22
+ }
23
+
24
+ export function rethrowManagementBodyTooLarge(error: unknown): void {
25
+ if (error instanceof DecompressedBodyTooLargeError) throw error;
26
+ }
27
+
28
+ export async function readManagementJsonBodyOr<T>(req: Request, fallback: T): Promise<unknown | T> {
29
+ try {
30
+ return await readManagementJsonBody(req);
31
+ } catch (error) {
32
+ rethrowManagementBodyTooLarge(error);
33
+ return fallback;
34
+ }
35
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Clients effective-status management route.
3
+ *
4
+ * GET /api/clients/status — read-only snapshot of each coding agent's on-disk
5
+ * routing (base URL / model / verdict). Never serializes secrets.
6
+ */
7
+ import { readRuntimePort } from "../../config";
8
+ import { jsonResponse } from "../auth-cors";
9
+ import type { ManagementContext } from "./context";
10
+
11
+ export async function handleClientsRoutes(ctx: ManagementContext): Promise<Response | null> {
12
+ const { req, url, config } = ctx;
13
+
14
+ if (url.pathname === "/api/clients/status" && req.method === "GET") {
15
+ try {
16
+ const { readClientsEffectiveStatus } = await import("../../clients/effective-status");
17
+ const { findLiveProxy } = await import("../proxy-liveness");
18
+
19
+ // Prefer live bind metadata so the page compares client base URLs against the
20
+ // port the proxy actually answered on, not just config.port.
21
+ let livePort: number | undefined;
22
+ let liveHostname: string | undefined;
23
+ let running = false;
24
+ try {
25
+ const live = await findLiveProxy();
26
+ if (live) {
27
+ running = true;
28
+ livePort = live.port;
29
+ liveHostname = live.hostname;
30
+ }
31
+ } catch {
32
+ // Fall back to runtime-port file / config.
33
+ }
34
+ if (!running) {
35
+ try {
36
+ const runtime = readRuntimePort(process.pid);
37
+ if (runtime?.port) {
38
+ // Runtime file exists for this process → we are the proxy answering this request.
39
+ running = true;
40
+ livePort = runtime.port;
41
+ liveHostname = runtime.hostname;
42
+ }
43
+ } catch { /* ignore */ }
44
+ }
45
+ // Management API is served by the proxy itself, so if we got here the process is up.
46
+ // Still report running=true even when findLiveProxy races during startup.
47
+ if (!running) running = true;
48
+ if (livePort === undefined) livePort = config.port;
49
+ if (liveHostname === undefined) liveHostname = config.hostname;
50
+
51
+ const snapshot = await readClientsEffectiveStatus(config, {
52
+ running,
53
+ livePort,
54
+ liveHostname,
55
+ });
56
+ return jsonResponse(snapshot);
57
+ } catch (error) {
58
+ return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
59
+ }
60
+ }
61
+
62
+ return null;
63
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * /api/cloud-sync/* — OneDrive backup/restore for ~/.opencodex (management auth).
3
+ */
4
+ import {
5
+ azureRedirectUri,
6
+ cancelBrowserLogin,
7
+ clearOneDriveToken,
8
+ fetchOneDriveAccountEmail,
9
+ getBrowserLoginStatus,
10
+ pollDeviceCodeTokenOnce,
11
+ readOneDriveToken,
12
+ resolveLoopbackPort,
13
+ resolveMsAuthority,
14
+ startBrowserLoopbackLogin,
15
+ startDeviceCodeLogin,
16
+ writeOneDriveToken,
17
+ } from "../../cloud/onedrive-auth";
18
+ import type { MsAuthority } from "../../cloud/types";
19
+ import { remoteSyncRootDescription } from "../../cloud/onedrive-graph";
20
+ import { pullFromOneDrive, pushToOneDrive, readRemoteManifest } from "../../cloud/sync";
21
+ import {
22
+ getDeviceName,
23
+ getOrCreateDeviceId,
24
+ readCloudSyncSettings,
25
+ resolveClientId,
26
+ writeCloudSyncSettings,
27
+ } from "../../cloud/settings";
28
+ import { jsonResponse } from "../auth-cors";
29
+ import type { ManagementContext } from "./context";
30
+
31
+ interface PendingDeviceLogin {
32
+ clientId: string;
33
+ deviceCode: string;
34
+ userCode: string;
35
+ verificationUri: string;
36
+ verificationUriComplete?: string;
37
+ interval: number;
38
+ expiresAt: number;
39
+ authority: MsAuthority;
40
+ }
41
+
42
+ let pendingLogin: PendingDeviceLogin | null = null;
43
+
44
+ function statusPayload() {
45
+ const settings = readCloudSyncSettings();
46
+ const token = readOneDriveToken();
47
+ return {
48
+ clientId: settings.clientId ?? process.env.OPENCODEX_MS_CLIENT_ID ?? process.env.OPENCODEX_ONEDRIVE_CLIENT_ID ?? null,
49
+ hasClientSecret: Boolean(settings.clientSecret?.trim() || process.env.OPENCODEX_MS_CLIENT_SECRET?.trim()),
50
+ loggedIn: Boolean(token),
51
+ account: token?.account ?? null,
52
+ msAuthority: resolveMsAuthority(),
53
+ deviceId: getOrCreateDeviceId(),
54
+ deviceName: getDeviceName(),
55
+ lastSyncAt: settings.lastSyncAt ?? null,
56
+ lastSyncDirection: settings.lastSyncDirection ?? null,
57
+ remoteRoot: remoteSyncRootDescription(),
58
+ includeUsage: settings.includeUsage === true,
59
+ includeVault: settings.includeVault !== false,
60
+ loopbackPort: resolveLoopbackPort(),
61
+ /** Exact value to paste into Azure → Mobile and desktop → Redirect URIs */
62
+ azureRedirectUri: azureRedirectUri(),
63
+ };
64
+ }
65
+
66
+ export async function handleCloudSyncRoutes(ctx: ManagementContext): Promise<Response | null> {
67
+ const { req, url, config } = ctx;
68
+
69
+ if (url.pathname === "/api/cloud-sync/status" && req.method === "GET") {
70
+ const base = statusPayload();
71
+ let remoteManifest: unknown = null;
72
+ let remoteError: string | null = null;
73
+ if (base.loggedIn) {
74
+ try {
75
+ remoteManifest = await readRemoteManifest();
76
+ } catch (e) {
77
+ remoteError = e instanceof Error ? e.message : String(e);
78
+ }
79
+ }
80
+ return jsonResponse({ ...base, remoteManifest, remoteError });
81
+ }
82
+
83
+ if (url.pathname === "/api/cloud-sync/client-id" && req.method === "POST") {
84
+ let body: { clientId?: unknown; clientSecret?: unknown };
85
+ try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON" }, 400); }
86
+ const clientId = typeof body.clientId === "string" ? body.clientId.trim() : "";
87
+ if (!clientId || clientId.length < 8) return jsonResponse({ error: "clientId is required" }, 400);
88
+ const settings = readCloudSyncSettings();
89
+ const next = { ...settings, clientId };
90
+ // Only update secret when the field is present (empty string clears it).
91
+ if ("clientSecret" in body) {
92
+ const secret = typeof body.clientSecret === "string" ? body.clientSecret.trim() : "";
93
+ if (secret) next.clientSecret = secret;
94
+ else delete next.clientSecret;
95
+ }
96
+ writeCloudSyncSettings(next);
97
+ return jsonResponse({
98
+ ok: true,
99
+ clientId,
100
+ hasClientSecret: Boolean(next.clientSecret),
101
+ });
102
+ }
103
+
104
+ if (url.pathname === "/api/cloud-sync/login/start" && req.method === "POST") {
105
+ let body: { clientId?: unknown; msAuthority?: unknown; mode?: unknown } = {};
106
+ try { body = await req.json(); } catch { /* empty ok */ }
107
+ let clientId: string;
108
+ try {
109
+ clientId = resolveClientId(typeof body.clientId === "string" ? body.clientId : undefined);
110
+ } catch (e) {
111
+ return jsonResponse({ error: e instanceof Error ? e.message : String(e) }, 400);
112
+ }
113
+ const authorityHint = typeof body.msAuthority === "string" ? body.msAuthority : undefined;
114
+ const authority =
115
+ authorityHint === "common" || authorityHint === "consumers" || authorityHint === "organizations"
116
+ ? authorityHint
117
+ : undefined;
118
+ const settings = readCloudSyncSettings();
119
+ writeCloudSyncSettings({
120
+ ...settings,
121
+ clientId,
122
+ ...(authority ? { msAuthority: authority } : {}),
123
+ });
124
+ // Default: browser loopback PKCE (works with Mobile+desktop + http://localhost).
125
+ // mode=device forces classic device-code (needs Allow public client flows / "mobile").
126
+ const mode = body.mode === "device" ? "device" : "browser";
127
+ try {
128
+ if (mode === "browser") {
129
+ cancelBrowserLogin();
130
+ pendingLogin = null;
131
+ const start = await startBrowserLoopbackLogin(clientId, authority);
132
+ return jsonResponse({
133
+ ok: true,
134
+ mode: "browser",
135
+ authUrl: start.authUrl,
136
+ redirectUri: start.redirectUri,
137
+ azureRedirectUri: start.azureRedirectUri,
138
+ verificationUri: start.authUrl,
139
+ verificationUriComplete: start.authUrl,
140
+ message:
141
+ `Complete sign-in in the browser. Azure must list redirect URI exactly: ${start.azureRedirectUri}`,
142
+ expiresIn: start.expiresIn,
143
+ interval: 2,
144
+ msAuthority: start.authority,
145
+ });
146
+ }
147
+ cancelBrowserLogin();
148
+ const start = await startDeviceCodeLogin(clientId, authority);
149
+ pendingLogin = {
150
+ clientId,
151
+ deviceCode: start.deviceCode,
152
+ userCode: start.userCode,
153
+ verificationUri: start.verificationUri,
154
+ verificationUriComplete: start.verificationUriComplete,
155
+ interval: start.interval,
156
+ expiresAt: Date.now() + start.expiresIn * 1000,
157
+ authority: start.authority,
158
+ };
159
+ return jsonResponse({
160
+ ok: true,
161
+ mode: "device",
162
+ userCode: start.userCode,
163
+ verificationUri: start.verificationUri,
164
+ verificationUriComplete: start.verificationUriComplete ?? null,
165
+ message: start.message,
166
+ expiresIn: start.expiresIn,
167
+ interval: start.interval,
168
+ msAuthority: start.authority,
169
+ });
170
+ } catch (e) {
171
+ return jsonResponse({ error: e instanceof Error ? e.message : String(e) }, 500);
172
+ }
173
+ }
174
+
175
+ if (url.pathname === "/api/cloud-sync/login/poll" && req.method === "POST") {
176
+ // Browser loopback session takes precedence when active.
177
+ const browser = getBrowserLoginStatus();
178
+ if (browser.status === "pending") {
179
+ return jsonResponse({ ok: true, pending: true, mode: "browser" });
180
+ }
181
+ if (browser.status === "ok") {
182
+ const account = browser.account ?? browser.store.account ?? null;
183
+ cancelBrowserLogin();
184
+ return jsonResponse({ ok: true, pending: false, mode: "browser", account, msAuthority: browser.store.authority });
185
+ }
186
+ if (browser.status === "error") {
187
+ const err = browser.error;
188
+ cancelBrowserLogin();
189
+ return jsonResponse({ pending: false, mode: "browser", error: err }, 400);
190
+ }
191
+
192
+ if (!pendingLogin) return jsonResponse({ pending: false, error: "no login in progress" }, 400);
193
+ if (Date.now() > pendingLogin.expiresAt) {
194
+ pendingLogin = null;
195
+ return jsonResponse({ pending: false, error: "device code expired" }, 400);
196
+ }
197
+ try {
198
+ const result = await pollDeviceCodeTokenOnce(
199
+ pendingLogin.clientId,
200
+ pendingLogin.deviceCode,
201
+ pendingLogin.authority,
202
+ );
203
+ if (result.status === "pending") {
204
+ return jsonResponse({ ok: true, pending: true, mode: "device", error: result.error });
205
+ }
206
+ if (result.status === "error") {
207
+ pendingLogin = null;
208
+ return jsonResponse({ pending: false, mode: "device", error: result.error }, 400);
209
+ }
210
+ const store = result.store;
211
+ const email = await fetchOneDriveAccountEmail(store.accessToken);
212
+ if (email) {
213
+ store.account = email;
214
+ writeOneDriveToken(store);
215
+ }
216
+ pendingLogin = null;
217
+ return jsonResponse({ ok: true, pending: false, mode: "device", account: email ?? null, msAuthority: store.authority });
218
+ } catch (e) {
219
+ return jsonResponse({ pending: false, error: e instanceof Error ? e.message : String(e) }, 500);
220
+ }
221
+ }
222
+
223
+ if (url.pathname === "/api/cloud-sync/logout" && req.method === "POST") {
224
+ pendingLogin = null;
225
+ cancelBrowserLogin();
226
+ clearOneDriveToken();
227
+ return jsonResponse({ ok: true });
228
+ }
229
+
230
+ if (url.pathname === "/api/cloud-sync/push" && req.method === "POST") {
231
+ let body: { passphrase?: unknown; includeUsage?: unknown; noVault?: unknown } = {};
232
+ try { body = await req.json(); } catch { /* empty ok */ }
233
+ const passphrase = typeof body.passphrase === "string" ? body.passphrase : undefined;
234
+ const includeUsage = body.includeUsage === true;
235
+ const noVault = body.noVault === true;
236
+ try {
237
+ resolveClientId();
238
+ const result = await pushToOneDrive({
239
+ passphrase: noVault ? undefined : passphrase,
240
+ includeUsage,
241
+ includeVault: !noVault && Boolean(passphrase),
242
+ });
243
+ return jsonResponse({ ok: true, ...result });
244
+ } catch (e) {
245
+ return jsonResponse({ error: e instanceof Error ? e.message : String(e) }, 400);
246
+ }
247
+ }
248
+
249
+ if (url.pathname === "/api/cloud-sync/pull" && req.method === "POST") {
250
+ let body: { passphrase?: unknown; includeUsage?: unknown; yes?: unknown } = {};
251
+ try { body = await req.json(); } catch { /* empty ok */ }
252
+ if (body.yes !== true) return jsonResponse({ error: "pull requires yes: true (overwrites local state)" }, 400);
253
+ const passphrase = typeof body.passphrase === "string" ? body.passphrase : undefined;
254
+ const includeUsage = body.includeUsage === true;
255
+ try {
256
+ resolveClientId();
257
+ const result = await pullFromOneDrive({ yes: true, passphrase, includeUsage });
258
+ return jsonResponse({ ok: true, ...result });
259
+ } catch (e) {
260
+ return jsonResponse({ error: e instanceof Error ? e.message : String(e) }, 400);
261
+ }
262
+ }
263
+
264
+ void config;
265
+ return null;
266
+ }