@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,730 @@
1
+ import { execFile, execFileSync, spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ import { expandUserPath, getConfigDir } from "../config";
7
+ import { durableBunPath } from "../lib/bun-runtime";
8
+ import { forgetHardenedSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
9
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
10
+
11
+ const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
12
+ const RUN_PARENT_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion";
13
+ const TRAY_STATE_VERSION = 1;
14
+ const FOREIGN_RUN_VALUE = "<foreign-or-unreadable-registry-value>";
15
+ const TRAY_ICON_FILES = [
16
+ "opencodex-tray-online.ico",
17
+ "opencodex-tray-warning.ico",
18
+ "opencodex-tray-offline.ico",
19
+ ] as const;
20
+
21
+ export interface WindowsTrayEntry {
22
+ bun: string;
23
+ cli: string;
24
+ script: string;
25
+ codexHome: string;
26
+ opencodexHome: string;
27
+ }
28
+
29
+ interface WindowsTrayState extends WindowsTrayEntry {
30
+ version: 1;
31
+ launcherPath?: string;
32
+ runValue: string;
33
+ runCommand: string;
34
+ }
35
+
36
+ export interface WindowsTrayStatus {
37
+ supported: boolean;
38
+ installed: boolean;
39
+ running: boolean;
40
+ stale: boolean;
41
+ summary: string;
42
+ }
43
+
44
+ function trayStatePath(): string {
45
+ return join(getConfigDir(), "tray-state.json");
46
+ }
47
+
48
+ function trayHeartbeatPath(): string {
49
+ return join(getConfigDir(), "tray-heartbeat.json");
50
+ }
51
+
52
+ function installedTrayScriptPath(): string {
53
+ return join(getConfigDir(), "opencodex-tray.ps1");
54
+ }
55
+
56
+ function installedTrayIconPaths(): string[] {
57
+ return TRAY_ICON_FILES.map(name => join(getConfigDir(), name));
58
+ }
59
+
60
+ export function windowsTrayStatePathsOwned(
61
+ state: Pick<WindowsTrayEntry, "script" | "opencodexHome"> & { launcherPath?: string },
62
+ configDir = getConfigDir(),
63
+ ): boolean {
64
+ if (resolve(state.opencodexHome) !== resolve(configDir)) return false;
65
+ if (resolve(state.script) !== resolve(join(configDir, "opencodex-tray.ps1"))) return false;
66
+ return state.launcherPath === undefined
67
+ || resolve(state.launcherPath) === resolve(join(configDir, "opencodex-tray.vbs"));
68
+ }
69
+
70
+ function sourceTrayScriptPath(): string {
71
+ return join(import.meta.dir, "windows-tray.ps1");
72
+ }
73
+
74
+ function sourceTrayIconPaths(): string[] {
75
+ return TRAY_ICON_FILES.map(name => join(import.meta.dir, "assets", name));
76
+ }
77
+
78
+ function currentCodexHome(): string {
79
+ const raw = process.env.CODEX_HOME?.trim();
80
+ return raw ? resolve(expandUserPath(raw)) : join(homedir(), ".codex");
81
+ }
82
+
83
+ function currentEntry(): WindowsTrayEntry {
84
+ return {
85
+ bun: durableBunPath(),
86
+ cli: join(import.meta.dir, "..", "cli", "index.ts"),
87
+ script: installedTrayScriptPath(),
88
+ codexHome: currentCodexHome(),
89
+ opencodexHome: getConfigDir(),
90
+ };
91
+ }
92
+
93
+ export function windowsTrayRunValue(opencodexHome: string): string {
94
+ const normalized = resolve(opencodexHome).replace(/[\\/](?:\.)?[\\/]*$/, "").toLowerCase();
95
+ return `OpenCodexTray-${createHash("sha256").update(normalized).digest("hex").slice(0, 12)}`;
96
+ }
97
+
98
+ export function windowsPowerShellPath(systemRoot = process.env.SystemRoot): string {
99
+ const candidate = join(
100
+ systemRoot ?? "C:\\Windows",
101
+ "System32",
102
+ "WindowsPowerShell",
103
+ "v1.0",
104
+ "powershell.exe",
105
+ );
106
+ return existsSync(candidate) ? candidate : "powershell.exe";
107
+ }
108
+
109
+ function registryExe(): string {
110
+ const candidate = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "reg.exe");
111
+ return existsSync(candidate) ? candidate : "reg.exe";
112
+ }
113
+
114
+ function runRegistry(args: string[]): string {
115
+ return execFileSync(registryExe(), args, {
116
+ encoding: "utf8",
117
+ stdio: ["ignore", "pipe", "pipe"],
118
+ windowsHide: true,
119
+ }).trim();
120
+ }
121
+
122
+ function safePath(value: string): string {
123
+ if (/[\u0000-\u001f\"]/.test(value)) {
124
+ throw new Error("Windows tray paths cannot contain quotes or control characters.");
125
+ }
126
+ return value;
127
+ }
128
+
129
+ export function windowsTrayProcessArgs(entry: WindowsTrayEntry, mode: "Run" | "Stop" = "Run", hostPid?: number): string[] {
130
+ const args = [
131
+ "-NoLogo",
132
+ "-NoProfile",
133
+ "-NonInteractive",
134
+ "-STA",
135
+ "-ExecutionPolicy", "Bypass",
136
+ "-WindowStyle", "Hidden",
137
+ "-File", safePath(entry.script),
138
+ "-BunPath", safePath(entry.bun),
139
+ "-CliPath", safePath(entry.cli),
140
+ "-CodexHome", safePath(entry.codexHome),
141
+ "-OpenCodexHome", safePath(entry.opencodexHome),
142
+ "-Mode", mode,
143
+ ];
144
+ if (Number.isSafeInteger(hostPid) && (hostPid ?? 0) > 0) args.push("-HostPid", String(hostPid));
145
+ return args;
146
+ }
147
+
148
+ function quoteRunValue(value: string): string {
149
+ safePath(value);
150
+ return `\"${value}\"`;
151
+ }
152
+
153
+ function installedTrayLauncherPath(): string {
154
+ return join(getConfigDir(), "opencodex-tray.vbs");
155
+ }
156
+
157
+ function quoteVbsPath(value: string): string {
158
+ return value.replace(/"/g, '""');
159
+ }
160
+
161
+ /** Full PowerShell invocation used by the owned VBS launcher (not written to HKCU Run). */
162
+ export function buildWindowsTrayPowerShellCommand(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string {
163
+ return [
164
+ quoteRunValue(powershell),
165
+ "-NoLogo",
166
+ "-NoProfile",
167
+ "-NonInteractive",
168
+ "-STA",
169
+ "-ExecutionPolicy", "Bypass",
170
+ "-WindowStyle", "Hidden",
171
+ "-File", quoteRunValue(entry.script),
172
+ "-BunPath", quoteRunValue(entry.bun),
173
+ "-CliPath", quoteRunValue(entry.cli),
174
+ "-CodexHome", quoteRunValue(entry.codexHome),
175
+ "-OpenCodexHome", quoteRunValue(entry.opencodexHome),
176
+ "-Mode", "Run",
177
+ ].join(" ");
178
+ }
179
+
180
+ /** Short HKCU Run command (must stay ≤260 chars under long Windows user/npm paths). */
181
+ export function buildWindowsTrayRunCommand(entry: WindowsTrayEntry & { launcherPath: string }): string {
182
+ const wscript = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "wscript.exe");
183
+ return `${quoteRunValue(wscript)} //B //NoLogo ${quoteRunValue(entry.launcherPath)}`;
184
+ }
185
+
186
+ export function buildWindowsTrayLauncherScript(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string {
187
+ const command = buildWindowsTrayPowerShellCommand(entry, powershell);
188
+ // VBS CreateObject("WScript.Shell").Run command, 0, False — hidden, non-blocking.
189
+ return [
190
+ "' OpenCodex owned tray launcher — do not edit by hand.",
191
+ `CreateObject("WScript.Shell").Run "${quoteVbsPath(command)}", 0, False`,
192
+ "",
193
+ ].join("\r\n");
194
+ }
195
+
196
+ /** @deprecated Prefer buildWindowsTrayPowerShellCommand; kept for callers that still expect the long form. */
197
+ export function buildWindowsTrayLegacyRunCommand(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string {
198
+ return buildWindowsTrayPowerShellCommand(entry, powershell);
199
+ }
200
+
201
+ function readState(): WindowsTrayState | null {
202
+ try {
203
+ const state = JSON.parse(readFileSync(trayStatePath(), "utf8")) as Partial<WindowsTrayState>;
204
+ if (state.version !== TRAY_STATE_VERSION) return null;
205
+ for (const key of ["bun", "cli", "script", "codexHome", "opencodexHome", "runValue", "runCommand"] as const) {
206
+ if (typeof state[key] !== "string" || state[key].length === 0) return null;
207
+ }
208
+ if (state.launcherPath !== undefined && typeof state.launcherPath !== "string") return null;
209
+ const valid = state as WindowsTrayState;
210
+ for (const value of [valid.bun, valid.cli, valid.script, valid.codexHome, valid.opencodexHome]) safePath(value);
211
+ // State is advisory, not an authority for executable or deletion paths. In
212
+ // particular, never let a forged state file redirect PowerShell -File.
213
+ if (!windowsTrayStatePathsOwned(valid)) return null;
214
+ if (valid.runValue !== windowsTrayRunValue(valid.opencodexHome)) return null;
215
+ return valid;
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+
221
+ export interface WindowsTrayOwnedFileIO {
222
+ write(path: string, contents: string | Buffer): void;
223
+ harden(path: string): void;
224
+ rename(source: string, destination: string): void;
225
+ unlink(path: string): void;
226
+ }
227
+
228
+ export function replaceWindowsTrayOwnedFile(
229
+ path: string,
230
+ contents: string | Buffer,
231
+ io: WindowsTrayOwnedFileIO = {
232
+ write: (target, value) => writeFileSync(target, value, { mode: 0o600 }),
233
+ harden: target => {
234
+ try { chmodSync(target, 0o600); } catch { /* best-effort */ }
235
+ if (process.platform !== "win32") return;
236
+ const hardened = hardenSecretPath(target, { required: true });
237
+ if (!hardened.ok) throw new Error("Windows tray ACL hardening did not complete; refusing to persist executable state.");
238
+ },
239
+ rename: renameSync,
240
+ unlink: unlinkSync,
241
+ },
242
+ ): void {
243
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
244
+ io.write(temporary, contents);
245
+ let renamed = false;
246
+ try {
247
+ io.harden(temporary);
248
+ io.rename(temporary, path);
249
+ renamed = true;
250
+ forgetHardenedSecretPath(temporary);
251
+ } finally {
252
+ if (!renamed) {
253
+ try {
254
+ io.unlink(temporary);
255
+ forgetHardenedSecretPath(temporary);
256
+ } catch (error) {
257
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") forgetHardenedSecretPath(temporary);
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ function writeState(
264
+ entry: WindowsTrayEntry & { launcherPath: string },
265
+ runValue: string,
266
+ runCommand: string,
267
+ ): void {
268
+ const path = trayStatePath();
269
+ replaceWindowsTrayOwnedFile(path, JSON.stringify({ version: TRAY_STATE_VERSION, ...entry, runValue, runCommand }, null, 2) + "\n");
270
+ }
271
+
272
+ export function parseWindowsTrayRunValue(output: string, runValue: string): string | null {
273
+ const escaped = runValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
274
+ const match = new RegExp(`^\\s*${escaped}\\s+(\\S+)\\s*(.*)$`, "mi").exec(output);
275
+ if (!match || match[1] !== "REG_SZ" || !match[2]?.trim()) return FOREIGN_RUN_VALUE;
276
+ return match[2].trim();
277
+ }
278
+
279
+ export function windowsRegistryParentShowsRunKey(output: string): boolean {
280
+ const expected = RUN_KEY.toLowerCase();
281
+ return output.split(/\r?\n/).some(line => line.trim().toLowerCase()
282
+ .replace(/^hkey_current_user\\/, "hkcu\\") === expected);
283
+ }
284
+
285
+ export type WindowsRegistryRunner = (args: string[]) => string;
286
+ export type WindowsRegistryAsyncRunner = (args: string[]) => Promise<string>;
287
+
288
+ function registryExitCode(error: unknown): number | null {
289
+ const value = error as { status?: unknown; code?: unknown };
290
+ const code = Number(value.status ?? value.code);
291
+ return Number.isFinite(code) ? code : null;
292
+ }
293
+
294
+ function syncRegistryAbsenceIsProven(run: WindowsRegistryRunner): boolean {
295
+ try {
296
+ run(["query", RUN_KEY, "/reg:64"]);
297
+ return true;
298
+ } catch (runError) {
299
+ if (registryExitCode(runError) !== 1) return false;
300
+ try {
301
+ const parent = run(["query", RUN_PARENT_KEY, "/reg:64"]);
302
+ return !windowsRegistryParentShowsRunKey(parent);
303
+ } catch {
304
+ return false;
305
+ }
306
+ }
307
+ }
308
+
309
+ export function readWindowsTrayRunValueWithRunner(runValue: string, run: WindowsRegistryRunner): string | null {
310
+ try {
311
+ const output = run(["query", RUN_KEY, "/v", runValue, "/reg:64"]);
312
+ return parseWindowsTrayRunValue(output, runValue);
313
+ } catch (error) {
314
+ if (registryExitCode(error) === 1) {
315
+ // reg.exe also uses exit 1 for access/query failures. Only treat the
316
+ // value as absent after proving Run is readable or does not exist under
317
+ // a readable CurrentVersion parent.
318
+ if (syncRegistryAbsenceIsProven(run)) return null;
319
+ }
320
+ throw new Error("Unable to verify the owned Windows tray registry value; refusing to change persistence.");
321
+ }
322
+ }
323
+
324
+ function readOwnedRunValue(runValue = windowsTrayRunValue(getConfigDir())): string | null {
325
+ return readWindowsTrayRunValueWithRunner(runValue, runRegistry);
326
+ }
327
+
328
+ function runRegistryAsync(args: string[]): Promise<string> {
329
+ return new Promise((resolvePromise, rejectPromise) => {
330
+ execFile(registryExe(), args, {
331
+ encoding: "utf8",
332
+ timeout: 2_000,
333
+ windowsHide: true,
334
+ maxBuffer: 64 * 1024,
335
+ }, (error, stdout) => {
336
+ if (error) rejectPromise(error);
337
+ else resolvePromise(stdout.trim());
338
+ });
339
+ });
340
+ }
341
+
342
+ async function asyncRegistryAbsenceIsProven(run: WindowsRegistryAsyncRunner): Promise<boolean> {
343
+ try {
344
+ await run(["query", RUN_KEY, "/reg:64"]);
345
+ return true;
346
+ } catch (runError) {
347
+ if (registryExitCode(runError) !== 1) return false;
348
+ try {
349
+ const parent = await run(["query", RUN_PARENT_KEY, "/reg:64"]);
350
+ return !windowsRegistryParentShowsRunKey(parent);
351
+ } catch {
352
+ return false;
353
+ }
354
+ }
355
+ }
356
+
357
+ export async function readWindowsTrayRunValueWithAsyncRunner(
358
+ runValue: string,
359
+ run: WindowsRegistryAsyncRunner,
360
+ ): Promise<string | null> {
361
+ try {
362
+ const output = await run(["query", RUN_KEY, "/v", runValue, "/reg:64"]);
363
+ return parseWindowsTrayRunValue(output, runValue);
364
+ } catch (error) {
365
+ if (registryExitCode(error) === 1 && await asyncRegistryAbsenceIsProven(run)) {
366
+ return null;
367
+ }
368
+ throw new Error("Unable to verify Windows tray registry status.");
369
+ }
370
+ }
371
+
372
+ async function readOwnedRunValueAsync(runValue = windowsTrayRunValue(getConfigDir())): Promise<string | null> {
373
+ return readWindowsTrayRunValueWithAsyncRunner(runValue, runRegistryAsync);
374
+ }
375
+
376
+ function readHeartbeat(): { pid: number; hostPid?: number; timestamp: number } | null {
377
+ try {
378
+ const heartbeat = JSON.parse(readFileSync(trayHeartbeatPath(), "utf8").replace(/^\uFEFF/, "")) as { pid?: unknown; hostPid?: unknown; timestamp?: unknown };
379
+ if (!Number.isSafeInteger(heartbeat.pid) || (heartbeat.pid as number) <= 0 || typeof heartbeat.timestamp !== "number") return null;
380
+ const hostPid = Number.isSafeInteger(heartbeat.hostPid) && (heartbeat.hostPid as number) > 0 ? heartbeat.hostPid as number : undefined;
381
+ return { pid: heartbeat.pid as number, hostPid, timestamp: heartbeat.timestamp };
382
+ } catch {
383
+ return null;
384
+ }
385
+ }
386
+
387
+ function processAlive(pid: number): boolean {
388
+ try { process.kill(pid, 0); return true; } catch { return false; }
389
+ }
390
+
391
+ function heartbeatProcessAlive(heartbeat = readHeartbeat()): boolean {
392
+ return Boolean(heartbeat && processAlive(heartbeat.pid));
393
+ }
394
+
395
+ function heartbeatRunning(): boolean {
396
+ const heartbeat = readHeartbeat();
397
+ return Boolean(heartbeat && Date.now() - heartbeat.timestamp <= 15_000 && heartbeatProcessAlive(heartbeat));
398
+ }
399
+
400
+ function waitForHeartbeat(expected: boolean, timeoutMs = 8_000): boolean {
401
+ const deadline = Date.now() + timeoutMs;
402
+ while (Date.now() < deadline) {
403
+ if (heartbeatRunning() === expected) return true;
404
+ Bun.sleepSync(100);
405
+ }
406
+ return heartbeatRunning() === expected;
407
+ }
408
+
409
+ function waitForTrayExit(previous: ReturnType<typeof readHeartbeat>, timeoutMs = 15_000): boolean {
410
+ const deadline = Date.now() + timeoutMs;
411
+ while (Date.now() < deadline) {
412
+ const powershellExited = !previous || !processAlive(previous.pid);
413
+ const hostExited = !previous?.hostPid || !processAlive(previous.hostPid);
414
+ if (!heartbeatProcessAlive() && powershellExited && hostExited) return true;
415
+ Bun.sleepSync(100);
416
+ }
417
+ const powershellExited = !previous || !processAlive(previous.pid);
418
+ const hostExited = !previous?.hostPid || !processAlive(previous.hostPid);
419
+ return !heartbeatProcessAlive() && powershellExited && hostExited;
420
+ }
421
+
422
+ export function windowsTrayRegistrationIsStale(inputs: {
423
+ registered: boolean;
424
+ registrationOwned: boolean;
425
+ running: boolean;
426
+ heartbeatFresh: boolean;
427
+ }): boolean {
428
+ if (!inputs.registered && inputs.running) return true;
429
+ if (inputs.registered && !inputs.registrationOwned) return true;
430
+ return inputs.running && !inputs.heartbeatFresh;
431
+ }
432
+
433
+ function trayStatusFrom(registered: string | null): WindowsTrayStatus {
434
+ const state = readState();
435
+ const heartbeat = readHeartbeat();
436
+ const running = heartbeatProcessAlive(heartbeat);
437
+ const registrationOwned = state !== null
438
+ && registered === state.runCommand
439
+ && [state.bun, state.cli, state.script, ...(state.launcherPath ? [state.launcherPath] : []), ...installedTrayIconPaths()]
440
+ .every(path => existsSync(path));
441
+ const stale = windowsTrayRegistrationIsStale({
442
+ registered: registered !== null,
443
+ registrationOwned,
444
+ running,
445
+ heartbeatFresh: Boolean(heartbeat && Date.now() - heartbeat.timestamp <= 15_000),
446
+ });
447
+ const installed = registered !== null && state !== null && registered === state.runCommand && !stale;
448
+ const summary = registered === null
449
+ ? running ? "unregistered tray process is still running" : "not installed"
450
+ : stale
451
+ ? "startup registration is foreign, stale, or points to missing package files"
452
+ : running
453
+ ? "installed and running"
454
+ : "installed, not currently running";
455
+ return { supported: true, installed, running, stale, summary };
456
+ }
457
+
458
+ export function getWindowsTrayStatus(): WindowsTrayStatus {
459
+ if (process.platform !== "win32") {
460
+ return { supported: false, installed: false, running: false, stale: false, summary: `unsupported on ${process.platform}` };
461
+ }
462
+ return trayStatusFrom(readOwnedRunValue());
463
+ }
464
+
465
+ export async function getWindowsTrayStatusAsync(): Promise<WindowsTrayStatus> {
466
+ if (process.platform !== "win32") {
467
+ return { supported: false, installed: false, running: false, stale: false, summary: `unsupported on ${process.platform}` };
468
+ }
469
+ const runValue = windowsTrayRunValue(getConfigDir());
470
+ const registered = await readOwnedRunValueAsync(runValue);
471
+ return trayStatusFrom(registered);
472
+ }
473
+
474
+ function assertWindows(): void {
475
+ if (process.platform !== "win32") throw new Error(`The opencodex tray is Windows-only (current platform: ${process.platform}).`);
476
+ }
477
+
478
+ const DETACHED_TRAY_HOST_LAUNCHER = [
479
+ "$startInfo = New-Object System.Diagnostics.ProcessStartInfo",
480
+ "$startInfo.FileName = $env:OCX_TRAY_HOST_BUN",
481
+ "$startInfo.Arguments = $env:OCX_TRAY_HOST_ARGS",
482
+ "$startInfo.UseShellExecute = $true",
483
+ "$startInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden",
484
+ "$child = [System.Diagnostics.Process]::Start($startInfo)",
485
+ "if ($null -eq $child) { throw 'Windows tray host did not start.' }",
486
+ "$child.Dispose()",
487
+ ].join("; ");
488
+
489
+ const DETACHED_TRAY_HOST_LAUNCHER_B64 = Buffer.from(DETACHED_TRAY_HOST_LAUNCHER, "utf16le").toString("base64");
490
+
491
+ export function launchWindowsTrayHost(state: WindowsTrayEntry): void {
492
+ const bun = safePath(state.bun);
493
+ const cli = safePath(state.cli);
494
+ execFileSync(windowsPowerShellPath(), [
495
+ "-NoLogo",
496
+ "-NoProfile",
497
+ "-NonInteractive",
498
+ "-EncodedCommand",
499
+ DETACHED_TRAY_HOST_LAUNCHER_B64,
500
+ ], {
501
+ stdio: "ignore",
502
+ windowsHide: true,
503
+ timeout: 15_000,
504
+ env: {
505
+ ...process.env,
506
+ OCX_TRAY_HOST_BUN: bun,
507
+ OCX_TRAY_HOST_ARGS: `${quoteRunValue(cli)} __tray-host`,
508
+ OCX_TRAY_ENTRY_B64: Buffer.from(JSON.stringify(state), "utf8").toString("base64"),
509
+ },
510
+ });
511
+ }
512
+
513
+ function spawnTray(state: WindowsTrayEntry): void {
514
+ launchWindowsTrayHost(state);
515
+ }
516
+
517
+ function parseTrayHostEntry(): WindowsTrayEntry {
518
+ const encoded = process.env.OCX_TRAY_ENTRY_B64;
519
+ if (!encoded) throw new Error("Missing tray host entry.");
520
+ const value = JSON.parse(Buffer.from(encoded, "base64").toString("utf8")) as Partial<WindowsTrayEntry>;
521
+ for (const key of ["bun", "cli", "script", "codexHome", "opencodexHome"] as const) {
522
+ if (typeof value[key] !== "string") throw new Error(`Invalid tray host field: ${key}`);
523
+ safePath(value[key]);
524
+ }
525
+ return value as WindowsTrayEntry;
526
+ }
527
+
528
+ /** Detached Bun host keeps the attached WinForms PowerShell process alive. */
529
+ export async function runWindowsTrayHost(): Promise<void> {
530
+ assertWindows();
531
+ const entry = parseTrayHostEntry();
532
+ delete process.env.OCX_TRAY_ENTRY_B64;
533
+ delete process.env.OCX_TRAY_HOST_BUN;
534
+ delete process.env.OCX_TRAY_HOST_ARGS;
535
+ const child = spawn(windowsPowerShellPath(), windowsTrayProcessArgs(entry, "Run", process.pid), {
536
+ stdio: "ignore",
537
+ windowsHide: true,
538
+ env: process.env,
539
+ });
540
+ await new Promise<void>((resolvePromise, rejectPromise) => {
541
+ child.once("error", rejectPromise);
542
+ child.once("exit", code => {
543
+ if (code && code !== 0) rejectPromise(new Error(`Windows tray host exited with code ${code}.`));
544
+ else resolvePromise();
545
+ });
546
+ });
547
+ }
548
+
549
+ function signalTrayStop(): ReturnType<typeof readHeartbeat> {
550
+ const previous = readHeartbeat();
551
+ // The stop event name depends only on the current home. Never execute paths
552
+ // recovered from tray-state.json while attempting cleanup or repair.
553
+ execFileSync(windowsPowerShellPath(), windowsTrayProcessArgs(currentEntry(), "Stop"), {
554
+ stdio: "ignore",
555
+ windowsHide: true,
556
+ timeout: 15_000,
557
+ });
558
+ return previous;
559
+ }
560
+
561
+ export function installWindowsTray(startNow = true): WindowsTrayStatus {
562
+ assertWindows();
563
+ const entry = currentEntry();
564
+ const sourceScript = sourceTrayScriptPath();
565
+ const iconPairs = sourceTrayIconPaths().map((source, index) => ({ source, installed: installedTrayIconPaths()[index] }));
566
+ for (const path of [entry.bun, entry.cli, sourceScript, ...iconPairs.map(pair => pair.source)]) {
567
+ if (!existsSync(path)) throw new Error(`Cannot install the tray because a required file is missing: ${path}`);
568
+ }
569
+ recordOwnedConfigPath(getConfigDir(), trayStatePath());
570
+ if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
571
+ const launcherPath = installedTrayLauncherPath();
572
+ const entryWithLauncher = { ...entry, launcherPath };
573
+ const runCommand = buildWindowsTrayRunCommand(entryWithLauncher);
574
+ if (runCommand.length > 260) {
575
+ throw new Error(`Tray Run command exceeds the Windows 260-character limit (${runCommand.length} chars).`);
576
+ }
577
+ const runValue = windowsTrayRunValue(entry.opencodexHome);
578
+ const existing = readOwnedRunValue(runValue);
579
+ const state = readState();
580
+ if (existing && (!state || existing !== state.runCommand)) {
581
+ throw new Error(`Refusing to replace a foreign or unowned HKCU Run value named ${runValue}.`);
582
+ }
583
+ if (existsSync(entry.script) && (!state || resolve(state.script) !== resolve(entry.script))) {
584
+ throw new Error(`Refusing to overwrite an unowned tray script at ${entry.script}.`);
585
+ }
586
+ if (existsSync(launcherPath) && (!state?.launcherPath || resolve(state.launcherPath) !== resolve(launcherPath))) {
587
+ throw new Error(`Refusing to overwrite an unowned tray launcher at ${launcherPath}.`);
588
+ }
589
+ if (!state && iconPairs.some(pair => existsSync(pair.installed))) {
590
+ throw new Error("Refusing to overwrite unowned Windows tray icon assets.");
591
+ }
592
+ const wasRunning = heartbeatProcessAlive();
593
+ if (wasRunning && !state) {
594
+ throw new Error("Refusing to replace an unowned running tray process. Exit it before installing.");
595
+ }
596
+ if (wasRunning && state) {
597
+ const previous = signalTrayStop();
598
+ if (!waitForTrayExit(previous)) throw new Error("The old tray did not exit; refusing to replace its persistent script.");
599
+ }
600
+
601
+ const previousStateBytes = existsSync(trayStatePath()) ? readFileSync(trayStatePath()) : null;
602
+ const previousScriptBytes = existsSync(entry.script) ? readFileSync(entry.script) : null;
603
+ const previousLauncherBytes = existsSync(launcherPath) ? readFileSync(launcherPath) : null;
604
+ const previousIconBytes = new Map(iconPairs.map(pair => [
605
+ pair.installed,
606
+ existsSync(pair.installed) ? readFileSync(pair.installed) : null,
607
+ ]));
608
+ const restorePreviousInstall = () => {
609
+ try {
610
+ if (previousScriptBytes) replaceWindowsTrayOwnedFile(entry.script, previousScriptBytes);
611
+ else if (existsSync(entry.script)) unlinkSync(entry.script);
612
+ } catch { /* rollback best-effort */ }
613
+ try {
614
+ if (previousLauncherBytes) replaceWindowsTrayOwnedFile(launcherPath, previousLauncherBytes);
615
+ else if (existsSync(launcherPath)) unlinkSync(launcherPath);
616
+ } catch { /* rollback best-effort */ }
617
+ for (const [path, contents] of previousIconBytes) {
618
+ try {
619
+ if (contents) replaceWindowsTrayOwnedFile(path, contents);
620
+ else if (existsSync(path)) unlinkSync(path);
621
+ } catch { /* rollback best-effort */ }
622
+ }
623
+ try {
624
+ if (previousStateBytes) replaceWindowsTrayOwnedFile(trayStatePath(), previousStateBytes);
625
+ else if (existsSync(trayStatePath())) unlinkSync(trayStatePath());
626
+ } catch { /* rollback best-effort */ }
627
+ try {
628
+ if (existing !== null) runRegistry(["add", RUN_KEY, "/v", runValue, "/t", "REG_SZ", "/d", existing, "/f", "/reg:64"]);
629
+ else runRegistry(["delete", RUN_KEY, "/v", runValue, "/f", "/reg:64"]);
630
+ } catch { /* rollback best-effort */ }
631
+ if (wasRunning && state && !heartbeatRunning()) {
632
+ try {
633
+ spawnTray(currentEntry());
634
+ waitForHeartbeat(true);
635
+ } catch { /* retain the primary installation failure */ }
636
+ }
637
+ };
638
+
639
+ try {
640
+ const hardenedDir = hardenSecretDir(getConfigDir(), { required: true });
641
+ if (!hardenedDir.ok) throw new Error("Windows tray directory ACL hardening did not complete; refusing to install persistence.");
642
+ replaceWindowsTrayOwnedFile(entry.script, readFileSync(sourceScript));
643
+ for (const pair of iconPairs) replaceWindowsTrayOwnedFile(pair.installed, readFileSync(pair.source));
644
+ replaceWindowsTrayOwnedFile(launcherPath, Buffer.from("\uFEFF" + buildWindowsTrayLauncherScript(entry), "utf16le"));
645
+ runRegistry(["add", RUN_KEY, "/v", runValue, "/t", "REG_SZ", "/d", runCommand, "/f", "/reg:64"]);
646
+ writeState(entryWithLauncher, runValue, runCommand);
647
+ } catch (error) {
648
+ restorePreviousInstall();
649
+ throw error;
650
+ }
651
+ if (startNow && !heartbeatRunning()) spawnTray(entry);
652
+ if (startNow && !waitForHeartbeat(true)) {
653
+ restorePreviousInstall();
654
+ throw new Error("The tray startup registration was installed, but the tray process did not become healthy.");
655
+ }
656
+ return getWindowsTrayStatus();
657
+ }
658
+
659
+ export function startWindowsTray(): WindowsTrayStatus {
660
+ assertWindows();
661
+ const state = readState();
662
+ if (!state || readOwnedRunValue(state.runValue) !== state.runCommand) throw new Error("The tray is not installed. Install it first.");
663
+ // Persisted state proves registration ownership but never selects an
664
+ // executable. Resolve every launch path from the running installation.
665
+ if (!heartbeatRunning()) spawnTray(currentEntry());
666
+ if (!waitForHeartbeat(true)) throw new Error("The tray process did not become healthy after launch.");
667
+ return getWindowsTrayStatus();
668
+ }
669
+
670
+ export function stopWindowsTray(): WindowsTrayStatus {
671
+ assertWindows();
672
+ let previous = readHeartbeat();
673
+ if (previous) {
674
+ previous = signalTrayStop();
675
+ }
676
+ if (!waitForTrayExit(previous)) throw new Error("The tray did not exit after the stop signal. Its login registration was preserved.");
677
+ return getWindowsTrayStatus();
678
+ }
679
+
680
+ export function uninstallWindowsTray(): WindowsTrayStatus {
681
+ assertWindows();
682
+ const state = readState();
683
+ const existing = state ? readOwnedRunValue(state.runValue) : readOwnedRunValue();
684
+ if (existing && (!state || existing !== state.runCommand)) {
685
+ throw new Error(`Refusing to remove a foreign or unowned HKCU Run value named ${state?.runValue ?? windowsTrayRunValue(getConfigDir())}.`);
686
+ }
687
+ let previous = readHeartbeat();
688
+ if (previous) {
689
+ previous = signalTrayStop();
690
+ }
691
+ if (!waitForTrayExit(previous)) throw new Error("The tray did not exit; refusing to remove its owned registration or state.");
692
+ if (existing) runRegistry(["delete", RUN_KEY, "/v", state?.runValue ?? windowsTrayRunValue(getConfigDir()), "/f", "/reg:64"]);
693
+ const ownedPaths = [trayStatePath(), trayHeartbeatPath(), ...(state?.launcherPath ? [state.launcherPath] : [])];
694
+ if (state?.script && resolve(state.script) === resolve(installedTrayScriptPath())) ownedPaths.push(state.script);
695
+ if (state) ownedPaths.push(...installedTrayIconPaths());
696
+ for (const path of ownedPaths) {
697
+ try { if (existsSync(path)) unlinkSync(path); } catch { /* best-effort */ }
698
+ }
699
+ return getWindowsTrayStatus();
700
+ }
701
+
702
+ /** Update hook: refresh trusted paths and relaunch only when the tray was already installed. */
703
+ export function repairWindowsTrayIfInstalled(startNow = true): WindowsTrayStatus | null {
704
+ if (process.platform !== "win32" || !readState()) return null;
705
+ return installWindowsTray(startNow);
706
+ }
707
+
708
+ export async function windowsTrayCommand(args: string[]): Promise<void> {
709
+ const wantsJson = args.includes("--json");
710
+ const startNow = !args.includes("--no-start");
711
+ const values = args.filter(value => value !== "--json" && value !== "--no-start");
712
+ const sub = values[0] ?? "status";
713
+ if (args.includes("--no-start") && sub !== "install" || values.length > 1 || !["install", "start", "stop", "status", "uninstall", "remove"].includes(sub)) {
714
+ console.error("Usage: ocx tray <install|start|stop|status|uninstall|remove> [--json] [--no-start]");
715
+ process.exitCode = 1;
716
+ return;
717
+ }
718
+ try {
719
+ const status = sub === "install" ? installWindowsTray(startNow)
720
+ : sub === "start" ? startWindowsTray()
721
+ : sub === "stop" ? stopWindowsTray()
722
+ : sub === "uninstall" || sub === "remove" ? uninstallWindowsTray()
723
+ : getWindowsTrayStatus();
724
+ console.log(wantsJson ? JSON.stringify(status) : `Windows tray: ${status.summary}`);
725
+ } catch (error) {
726
+ if (wantsJson) console.log(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }));
727
+ else console.error(`Windows tray error: ${error instanceof Error ? error.message : String(error)}`);
728
+ process.exitCode = 1;
729
+ }
730
+ }