@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,139 @@
1
+ /**
2
+ * Single-flight gate for CODEX_HOME storage mutations (cleanup, restore, policy).
3
+ *
4
+ * Manual cleanup, trash restore, and (Phase 3) policy-driven cleanup share one
5
+ * in-flight slot per resolved CODEX_HOME. A second caller receives
6
+ * `storage_mutation_busy` immediately — no per-operation queues.
7
+ */
8
+ import { resolve } from "node:path";
9
+ import { resolveCodexHomeDir } from "../codex/home";
10
+ import { createAdmissionGate, type AdmissionLease, type AdmissionMetrics } from "../lib/admission";
11
+
12
+ export type StorageMutationKind = "cleanup" | "restore" | "policy";
13
+
14
+ export type StorageMutationBusyError = "storage_mutation_busy";
15
+
16
+ export interface StorageMutationCoordinatorTestHooks {
17
+ /** Block after acquiring the slot, before mutation work (race tests). */
18
+ blockMs?: number;
19
+ }
20
+
21
+ interface ActiveSlot {
22
+ kind: StorageMutationKind;
23
+ startedAt: number;
24
+ lease: AdmissionLease;
25
+ }
26
+
27
+ export const MAX_ACTIVE_STORAGE_HOME_SLOTS = 32;
28
+ const slots = new Map<string, ActiveSlot>();
29
+ const slotGate = createAdmissionGate("storage_home_slots", MAX_ACTIVE_STORAGE_HOME_SLOTS);
30
+ let releaseMisses = 0;
31
+ let testHooks: StorageMutationCoordinatorTestHooks | null = null;
32
+
33
+ function slotKey(codexHome?: string): string {
34
+ return resolve(codexHome ?? resolveCodexHomeDir());
35
+ }
36
+
37
+ export function setStorageMutationCoordinatorTestHooks(
38
+ hooks: StorageMutationCoordinatorTestHooks | null,
39
+ ): void {
40
+ testHooks = hooks;
41
+ }
42
+
43
+ export function resetStorageMutationCoordinatorForTests(): void {
44
+ testHooks = null;
45
+ for (const slot of slots.values()) slot.lease.release();
46
+ slots.clear();
47
+ }
48
+
49
+ export function storageMutationAdmissionMetrics(): AdmissionMetrics {
50
+ const metrics = slotGate.metrics();
51
+ return { ...metrics, releaseMisses: metrics.releaseMisses + releaseMisses };
52
+ }
53
+
54
+ export function getActiveStorageMutation(
55
+ codexHome?: string,
56
+ ): { kind: StorageMutationKind; startedAt: number } | null {
57
+ const active = slots.get(slotKey(codexHome));
58
+ return active ? { kind: active.kind, startedAt: active.startedAt } : null;
59
+ }
60
+
61
+ export function tryBeginStorageMutation(
62
+ kind: StorageMutationKind,
63
+ codexHome?: string,
64
+ ): { acquired: true; lease: AdmissionLease } | { acquired: false; error: StorageMutationBusyError } {
65
+ const key = slotKey(codexHome);
66
+ if (slots.has(key)) {
67
+ return { acquired: false, error: "storage_mutation_busy" };
68
+ }
69
+ const lease = slotGate.tryAcquire();
70
+ if (!lease) return { acquired: false, error: "storage_mutation_busy" };
71
+ let active = true;
72
+ const ownerLease: AdmissionLease = {
73
+ release() {
74
+ if (!active) return;
75
+ active = false;
76
+ const owner = slots.get(key);
77
+ if (owner?.lease === ownerLease) slots.delete(key);
78
+ lease.release();
79
+ },
80
+ };
81
+ slots.set(key, { kind, startedAt: Date.now(), lease: ownerLease });
82
+ return { acquired: true, lease: ownerLease };
83
+ }
84
+
85
+ export function endStorageMutation(codexHome?: string): void {
86
+ const key = slotKey(codexHome);
87
+ const slot = slots.get(key);
88
+ if (!slot) {
89
+ releaseMisses += 1;
90
+ return;
91
+ }
92
+ slots.delete(key);
93
+ slot.lease.release();
94
+ }
95
+
96
+ async function applyCoordinatorBlock(): Promise<void> {
97
+ const blockMs = testHooks?.blockMs;
98
+ if (typeof blockMs === "number" && Number.isFinite(blockMs) && blockMs > 0) {
99
+ await Bun.sleep(Math.floor(blockMs));
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Phase 3 policy worker integration — wrap policy-driven cleanup FS/DB work.
105
+ * Returns `{ ok: false, error: 'storage_mutation_busy' }` when another mutation
106
+ * holds the CODEX_HOME slot.
107
+ */
108
+ export async function runPolicyStorageMutation<T>(
109
+ codexHome: string | undefined,
110
+ work: () => T | Promise<T>,
111
+ ): Promise<T | { ok: false; error: StorageMutationBusyError }> {
112
+ const gate = tryBeginStorageMutation("policy", codexHome);
113
+ if (!gate.acquired) {
114
+ return { ok: false, error: "storage_mutation_busy" };
115
+ }
116
+ try {
117
+ await applyCoordinatorBlock();
118
+ return await work();
119
+ } finally {
120
+ gate.lease.release();
121
+ }
122
+ }
123
+
124
+ export async function withStorageMutationSlot<T>(
125
+ kind: StorageMutationKind,
126
+ codexHome: string | undefined,
127
+ work: () => T | Promise<T>,
128
+ ): Promise<T | { ok: false; error: StorageMutationBusyError }> {
129
+ const gate = tryBeginStorageMutation(kind, codexHome);
130
+ if (!gate.acquired) {
131
+ return { ok: false, error: "storage_mutation_busy" };
132
+ }
133
+ try {
134
+ await applyCoordinatorBlock();
135
+ return await work();
136
+ } finally {
137
+ gate.lease.release();
138
+ }
139
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Deterministic teardown for the storage Bun Workers.
3
+ *
4
+ * `Worker.terminate()` returns void and does NOT wait for the thread to be
5
+ * reclaimed. Callers that fire-and-forget leave a window where
6
+ * `bun test --isolate` reclaims the file realm while a Windows worker thread
7
+ * is still exiting — Bun then panics with
8
+ * `workers_spawned(N) workers_terminated(N-1)` / Internal assertion failure.
9
+ *
10
+ * Invariant: every registered worker has a `close` listener attached at spawn
11
+ * time (so we cannot miss `self.close()` / early exit), stays in `liveWorkers`
12
+ * until that close settles, and `drainStorageWorkers()` joins every in-flight
13
+ * terminate. Spawns are serialized through `withStorageWorkerSpawnGate` so the
14
+ * next Worker cannot be created until prior threads have exited. On Windows and
15
+ * macOS, a post-close settle covers the OS join gap Bun does not expose
16
+ * (Windows unbalanced join panic; macOS Silicon balanced-count segfault under
17
+ * `bun test --isolate`).
18
+ */
19
+
20
+ import { createAdmissionGate, type AdmissionMetrics, type AdmissionReservation } from "../lib/admission";
21
+
22
+ type TrackedWorker = {
23
+ worker: Worker;
24
+ closed: Promise<void>;
25
+ resolveClosed: () => void;
26
+ terminatePromise?: Promise<void>;
27
+ reservation?: AdmissionReservation<Worker>;
28
+ };
29
+
30
+ export const MAX_RESERVED_STORAGE_WORKER_SPAWNS = 16;
31
+ export class StorageWorkerAdmissionBusyError extends Error {
32
+ readonly code = "storage_mutation_busy";
33
+
34
+ constructor() {
35
+ super("storage worker spawn queue is busy");
36
+ this.name = "StorageWorkerAdmissionBusyError";
37
+ }
38
+ }
39
+
40
+ const liveWorkers = new Map<Worker, TrackedWorker>();
41
+ const workerGate = createAdmissionGate("storage_worker_reservations", MAX_RESERVED_STORAGE_WORKER_SPAWNS);
42
+
43
+ /** Serialize spawns so a new Worker never overlaps a still-exiting predecessor. */
44
+ let spawnGate: Promise<void> = Promise.resolve();
45
+
46
+ /**
47
+ * Bumped by teardown so a spawn still queued on `spawnGate` (not yet in
48
+ * `liveWorkers`) cannot create a Worker after reset/shutdown reported idle.
49
+ */
50
+ let spawnCancelEpoch = 0;
51
+
52
+ /**
53
+ * OS-join gap after the `close` event on platforms where Bun's Worker reclaim
54
+ * races the isolate/file boundary (not a CI job-timeout bump).
55
+ * Windows GHA at 250ms and 750ms still left `workers_spawned(N)
56
+ * workers_terminated(N-1)` panics under isolate (seen mid
57
+ * `storage-mutation-race` with 11/10). 1500ms covers deferred reclaim under
58
+ * stacked policy/restore workers. Darwin uses 250ms.
59
+ */
60
+ const WORKER_OS_JOIN_MS = process.platform === "win32" ? 1_500 : 250;
61
+
62
+ function needsWorkerOsJoinSettle(): boolean {
63
+ return process.platform === "win32" || process.platform === "darwin";
64
+ }
65
+
66
+ /** Invalidate spawn callbacks still waiting on the gate (reset / server drain). */
67
+ export function cancelQueuedStorageWorkerSpawns(): void {
68
+ spawnCancelEpoch += 1;
69
+ }
70
+
71
+ /** Track a freshly spawned worker so teardown can wait for it later. */
72
+ export function registerStorageWorker(worker: Worker): void {
73
+ if (liveWorkers.has(worker)) return;
74
+
75
+ let resolveClosed!: () => void;
76
+ const closed = new Promise<void>(resolve => {
77
+ resolveClosed = resolve;
78
+ });
79
+
80
+ const tracked: TrackedWorker = { worker, closed, resolveClosed };
81
+ liveWorkers.set(worker, tracked);
82
+
83
+ try {
84
+ worker.addEventListener("close", () => {
85
+ resolveClosed();
86
+ }, { once: true });
87
+ } catch {
88
+ // No close event — terminate path will still resolve via timeout/settle.
89
+ }
90
+ }
91
+
92
+ export function tryReserveStorageWorker(): AdmissionReservation<Worker> | null {
93
+ const gateLease = workerGate.tryAcquire();
94
+ if (!gateLease) return null;
95
+ let active = true;
96
+ let bound: Worker | undefined;
97
+ const reservation: AdmissionReservation<Worker> = {
98
+ bind(worker) {
99
+ if (!active) return;
100
+ bound = worker;
101
+ registerStorageWorker(worker);
102
+ const tracked = liveWorkers.get(worker);
103
+ if (tracked) tracked.reservation = reservation;
104
+ },
105
+ release() {
106
+ if (!active) return;
107
+ active = false;
108
+ if (bound) {
109
+ const tracked = liveWorkers.get(bound);
110
+ if (tracked?.reservation === reservation) tracked.reservation = undefined;
111
+ }
112
+ bound = undefined;
113
+ gateLease.release();
114
+ },
115
+ };
116
+ return reservation;
117
+ }
118
+
119
+ export function storageWorkerAdmissionMetrics(): AdmissionMetrics {
120
+ return workerGate.metrics();
121
+ }
122
+
123
+ /**
124
+ * Run `fn` only after every previously gated spawn has finished tearing down.
125
+ * Used around `new Worker(...)` so tests cannot overlap Windows thread exit.
126
+ */
127
+ export function withStorageWorkerSpawnGate<T>(fn: () => Promise<T>): Promise<T> {
128
+ const epochAtEnqueue = spawnCancelEpoch;
129
+ const run = spawnGate.then(async () => {
130
+ if (epochAtEnqueue !== spawnCancelEpoch) {
131
+ throw new Error("storage_worker_spawn_cancelled");
132
+ }
133
+ await drainStorageWorkers();
134
+ if (epochAtEnqueue !== spawnCancelEpoch) {
135
+ throw new Error("storage_worker_spawn_cancelled");
136
+ }
137
+ return fn();
138
+ });
139
+ spawnGate = run.then(
140
+ () => undefined,
141
+ () => undefined,
142
+ );
143
+ return run;
144
+ }
145
+
146
+ export function queuedStorageWorkerSpawnCount(): number {
147
+ return Math.max(0, workerGate.metrics().active - liveWorkers.size);
148
+ }
149
+
150
+ /**
151
+ * Terminate a worker and resolve once its thread has actually exited.
152
+ *
153
+ * Safe to call twice: the second call joins the in-flight terminate promise.
154
+ */
155
+ export function terminateStorageWorker(worker: Worker, timeoutMs = 5_000): Promise<void> {
156
+ const tracked = liveWorkers.get(worker);
157
+ if (!tracked) {
158
+ try { worker.terminate(); } catch { /* already gone */ }
159
+ return Promise.resolve();
160
+ }
161
+ if (tracked.terminatePromise) return tracked.terminatePromise;
162
+
163
+ tracked.terminatePromise = (async () => {
164
+ let timedOut = false;
165
+ const timer = setTimeout(() => {
166
+ timedOut = true;
167
+ tracked.resolveClosed();
168
+ }, timeoutMs);
169
+
170
+ try {
171
+ try {
172
+ worker.terminate();
173
+ } catch {
174
+ tracked.resolveClosed();
175
+ }
176
+ await tracked.closed;
177
+ // Disarm before the OS-join settle: a late timer firing during the sleep
178
+ // would set timedOut after close already won and throw a false timeout.
179
+ clearTimeout(timer);
180
+ // Always run the OS-join settle before throwing on timeout: the timer
181
+ // only forces `closed`, it does not prove the OS thread has exited.
182
+ // Callers that catch and continue (e.g. drainAndShutdown) still need
183
+ // that gap before the next isolate reclaim or server.stop.
184
+ if (needsWorkerOsJoinSettle()) {
185
+ await Bun.sleep(0);
186
+ await Bun.sleep(WORKER_OS_JOIN_MS);
187
+ }
188
+ if (timedOut) {
189
+ throw new Error(`storage worker did not exit within ${timeoutMs}ms`);
190
+ }
191
+ } finally {
192
+ clearTimeout(timer);
193
+ liveWorkers.delete(worker);
194
+ tracked.reservation?.release();
195
+ tracked.reservation = undefined;
196
+ }
197
+ })();
198
+
199
+ return tracked.terminatePromise;
200
+ }
201
+
202
+ /**
203
+ * Await every worker this module still tracks (including terminations already
204
+ * in flight). Used by test resets so no storage worker outlives the file that
205
+ * spawned it.
206
+ */
207
+ export async function drainStorageWorkers(timeoutMs = 5_000): Promise<void> {
208
+ const pending = [...liveWorkers.keys()].map(worker => terminateStorageWorker(worker, timeoutMs));
209
+ await Promise.all(pending);
210
+ }
211
+
212
+ /** Live worker count — exported so a regression test can assert the invariant. */
213
+ export function liveStorageWorkerCount(): number {
214
+ return liveWorkers.size;
215
+ }
@@ -0,0 +1,290 @@
1
+ param(
2
+ [Parameter(Mandatory = $true)][string]$BunPath,
3
+ [Parameter(Mandatory = $true)][string]$CliPath,
4
+ [Parameter(Mandatory = $true)][string]$CodexHome,
5
+ [Parameter(Mandatory = $true)][string]$OpenCodexHome,
6
+ [ValidateSet("Run", "Stop")][string]$Mode = "Run",
7
+ [int]$HostPid = 0
8
+ )
9
+
10
+ $ErrorActionPreference = "Stop"
11
+ Add-Type -AssemblyName System.Windows.Forms
12
+ Add-Type -AssemblyName System.Drawing
13
+
14
+ # Normalize aliases before deriving singleton/event names. Without this,
15
+ # C:\path and C:\path\. create separate tray instances for the same home.
16
+ function Normalize-HomePath([string]$Value) {
17
+ $full = [System.IO.Path]::GetFullPath($Value)
18
+ $root = [System.IO.Path]::GetPathRoot($full)
19
+ if ($full -eq $root) { return $full }
20
+ return $full.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)
21
+ }
22
+ $OpenCodexHome = Normalize-HomePath $OpenCodexHome
23
+ $CodexHome = Normalize-HomePath $CodexHome
24
+
25
+ $script:ownedIcons = New-Object System.Collections.Generic.List[System.Drawing.Icon]
26
+ function Load-TrayIcon([string]$Name, [System.Drawing.Icon]$Fallback) {
27
+ $path = Join-Path $OpenCodexHome $Name
28
+ if (-not [System.IO.File]::Exists($path)) { return $Fallback }
29
+ try {
30
+ $icon = New-Object System.Drawing.Icon($path)
31
+ [void]$script:ownedIcons.Add($icon)
32
+ return $icon
33
+ } catch {
34
+ return $Fallback
35
+ }
36
+ }
37
+ $onlineIcon = Load-TrayIcon "opencodex-tray-online.ico" ([System.Drawing.SystemIcons]::Information)
38
+ $warningIcon = Load-TrayIcon "opencodex-tray-warning.ico" ([System.Drawing.SystemIcons]::Warning)
39
+ $offlineIcon = Load-TrayIcon "opencodex-tray-offline.ico" ([System.Drawing.SystemIcons]::Error)
40
+
41
+ function Get-StableHash([string]$Value) {
42
+ $sha = [System.Security.Cryptography.SHA256]::Create()
43
+ try {
44
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($Value.ToLowerInvariant())
45
+ $hash = [System.BitConverter]::ToString($sha.ComputeHash($bytes)).Replace("-", "").Substring(0, 20)
46
+ return $hash
47
+ } finally {
48
+ $sha.Dispose()
49
+ }
50
+ }
51
+
52
+ $stableHash = Get-StableHash $OpenCodexHome
53
+ $stopEventCreated = $false
54
+ $stopEvent = New-Object System.Threading.EventWaitHandle($false, [System.Threading.EventResetMode]::AutoReset, "Local\OpenCodexTrayStop-$stableHash", [ref]$stopEventCreated)
55
+ if ($Mode -eq "Stop") {
56
+ [void]$stopEvent.Set()
57
+ $stopEvent.Dispose()
58
+ exit 0
59
+ }
60
+
61
+ $createdNew = $false
62
+ $mutex = New-Object System.Threading.Mutex($true, "Local\OpenCodexTray-$stableHash", [ref]$createdNew)
63
+ if (-not $createdNew) {
64
+ $stopEvent.Dispose()
65
+ $mutex.Dispose()
66
+ exit 0
67
+ }
68
+
69
+ $heartbeatPath = Join-Path $OpenCodexHome "tray-heartbeat.json"
70
+ $actionLogPath = Join-Path $OpenCodexHome "tray-actions.log"
71
+
72
+ function Write-ActionLog([string]$Message) {
73
+ $line = "[$([DateTimeOffset]::Now.ToString('o'))] $Message"
74
+ [System.IO.File]::AppendAllText($actionLogPath, $line + [Environment]::NewLine, (New-Object System.Text.UTF8Encoding($false)))
75
+ }
76
+
77
+ function ConvertTo-NativeArgument([string]$Value) {
78
+ if ($Value.Contains('"') -or $Value.Contains("`r") -or $Value.Contains("`n")) {
79
+ throw "Invalid native command argument"
80
+ }
81
+ return '"' + $Value + '"'
82
+ }
83
+
84
+ function Start-OcxCommand([string[]]$CommandArgs) {
85
+ try {
86
+ $allArgs = @($CliPath) + $CommandArgs
87
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
88
+ $psi.FileName = $BunPath
89
+ $psi.Arguments = (($allArgs | ForEach-Object { ConvertTo-NativeArgument $_ }) -join " ")
90
+ $psi.UseShellExecute = $false
91
+ $psi.CreateNoWindow = $true
92
+ $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
93
+ $psi.EnvironmentVariables["CODEX_HOME"] = $CodexHome
94
+ $psi.EnvironmentVariables["OPENCODEX_HOME"] = $OpenCodexHome
95
+ $process = [System.Diagnostics.Process]::Start($psi)
96
+ if ($null -ne $process) { $process.Dispose() }
97
+ Write-ActionLog "dispatched $($CommandArgs -join ' ')"
98
+ return $true
99
+ } catch {
100
+ Write-ActionLog "launch failed: $($_.Exception.GetType().Name)"
101
+ $notify.ShowBalloonTip(5000, "opencodex action failed", "The action could not start. Open the logs folder or run ocx doctor.", [System.Windows.Forms.ToolTipIcon]::Error)
102
+ return $false
103
+ }
104
+ }
105
+
106
+ function Read-ListenTarget {
107
+ foreach ($path in @((Join-Path $OpenCodexHome "runtime-port.json"), (Join-Path $OpenCodexHome "config.json"))) {
108
+ try {
109
+ $value = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json
110
+ $candidate = [int]$value.port
111
+ if ($candidate -gt 0 -and $candidate -le 65535) {
112
+ $candidateHost = [string]$value.hostname
113
+ $ip = $null
114
+ $hostName = if ([string]::IsNullOrWhiteSpace($candidateHost) -or $candidateHost -in @("localhost", "0.0.0.0", "::", "[::]")) {
115
+ "127.0.0.1"
116
+ } elseif ([System.Net.IPAddress]::TryParse($candidateHost.Trim("[", "]"), [ref]$ip)) {
117
+ if ($ip.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6) { "[$($ip.ToString())]" } else { $ip.ToString() }
118
+ } else {
119
+ "127.0.0.1"
120
+ }
121
+ return @{ port = $candidate; host = $hostName; pid = $value.pid }
122
+ }
123
+ } catch { }
124
+ }
125
+ return @{ port = 10100; host = "127.0.0.1"; pid = $null }
126
+ }
127
+
128
+ function Read-JsonUrl([string]$Url) {
129
+ $request = [System.Net.HttpWebRequest]::Create($Url)
130
+ $request.Method = "GET"
131
+ $request.Timeout = 700
132
+ $request.ReadWriteTimeout = 700
133
+ $response = $request.GetResponse()
134
+ try {
135
+ $reader = New-Object System.IO.StreamReader($response.GetResponseStream())
136
+ try { return ($reader.ReadToEnd() | ConvertFrom-Json) } finally { $reader.Dispose() }
137
+ } finally {
138
+ $response.Dispose()
139
+ }
140
+ }
141
+
142
+ $notify = New-Object System.Windows.Forms.NotifyIcon
143
+ $menu = New-Object System.Windows.Forms.ContextMenuStrip
144
+ $statusItem = New-Object System.Windows.Forms.ToolStripMenuItem
145
+ $statusItem.Enabled = $false
146
+ $safetyItem = New-Object System.Windows.Forms.ToolStripMenuItem
147
+ $safetyItem.Enabled = $false
148
+ $openItem = $menu.Items.Add("Open Dashboard")
149
+ $startItem = $menu.Items.Add("Start Proxy")
150
+ $stopItem = $menu.Items.Add("Stop Proxy and Restore Native Routing")
151
+ $restartItem = $menu.Items.Add("Restart Proxy")
152
+ [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
153
+ [void]$menu.Items.Add($statusItem)
154
+ [void]$menu.Items.Add($safetyItem)
155
+ $logsItem = $menu.Items.Add("Open Logs Folder")
156
+ [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
157
+ $exitItem = $menu.Items.Add("Exit Tray")
158
+
159
+ $script:online = $false
160
+ $script:port = 10100
161
+ $script:proxyPid = $null
162
+ $script:pendingAction = $null
163
+ $script:pendingStarted = 0L
164
+ $script:pendingDeadline = 0L
165
+ $script:pendingOldProxyPid = $null
166
+
167
+ function Set-PendingAction([string]$Action, [int]$TimeoutSeconds) {
168
+ $script:pendingAction = $Action
169
+ $script:pendingStarted = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
170
+ $script:pendingDeadline = $script:pendingStarted + ($TimeoutSeconds * 1000)
171
+ $script:pendingOldProxyPid = $script:proxyPid
172
+ }
173
+
174
+ function Complete-PendingAction([bool]$Success) {
175
+ if ($null -eq $script:pendingAction) { return }
176
+ $action = $script:pendingAction
177
+ $script:pendingAction = $null
178
+ if ($Success) {
179
+ Write-ActionLog "$action completed (port=$($script:port), pid=$($script:proxyPid))"
180
+ $notify.ShowBalloonTip(2500, "opencodex", "$action completed.", [System.Windows.Forms.ToolTipIcon]::Info)
181
+ } else {
182
+ Write-ActionLog "$action failed to reach the expected state"
183
+ $notify.ShowBalloonTip(5000, "opencodex action failed", "$action did not reach the expected state. Open the logs folder or run ocx doctor.", [System.Windows.Forms.ToolTipIcon]::Error)
184
+ }
185
+ }
186
+
187
+ function Update-TrayState {
188
+ $target = Read-ListenTarget
189
+ $script:port = [int]$target.port
190
+ $health = $null
191
+ $origin = "http://$($target.host):$($script:port)"
192
+ try { $health = Read-JsonUrl "$origin/healthz" } catch { }
193
+ $pidMatches = $null -eq $target.pid -or [int]$target.pid -eq [int]$health.pid
194
+ $script:online = $null -ne $health -and $health.status -eq "ok" -and $health.service -eq "opencodex" -and [int]$health.port -eq $script:port -and $pidMatches
195
+ $script:proxyPid = if ($script:online) { [int]$health.pid } else { $null }
196
+ if ($script:online) {
197
+ $statusItem.Text = "Proxy: Online (port $($script:port))"
198
+ $notify.Text = "opencodex: Online"
199
+ $startItem.Enabled = $false
200
+ $stopItem.Enabled = $true
201
+ $restartItem.Enabled = $true
202
+ try {
203
+ $startup = Read-JsonUrl "$origin/api/startup-health"
204
+ $label = if ($startup.status -eq "at-risk") { "At risk" } elseif ($startup.status -eq "protected") { "Protected" } else { "Native routing" }
205
+ $safetyItem.Text = "Restart safety: $label"
206
+ $notify.Icon = if ($startup.status -eq "at-risk") { $warningIcon } else { $onlineIcon }
207
+ } catch {
208
+ $safetyItem.Text = "Restart safety: unavailable"
209
+ $notify.Icon = $warningIcon
210
+ }
211
+ } else {
212
+ $statusItem.Text = "Proxy: Offline"
213
+ $safetyItem.Text = "Restart safety: start the proxy to inspect"
214
+ $notify.Text = "opencodex: Offline"
215
+ $notify.Icon = $offlineIcon
216
+ $startItem.Enabled = $true
217
+ $stopItem.Enabled = $false
218
+ $restartItem.Enabled = $false
219
+ }
220
+ $heartbeat = @{ pid = $PID; timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
221
+ if ($HostPid -gt 0) { $heartbeat.hostPid = $HostPid }
222
+ $heartbeatJson = $heartbeat | ConvertTo-Json -Compress
223
+ [System.IO.File]::WriteAllText($heartbeatPath, $heartbeatJson, (New-Object System.Text.UTF8Encoding($false)))
224
+
225
+ if ($null -ne $script:pendingAction) {
226
+ $now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
227
+ $elapsed = $now - $script:pendingStarted
228
+ $reached = ($script:pendingAction -eq "Start Proxy" -and $script:online) -or
229
+ ($script:pendingAction -eq "Stop Proxy" -and -not $script:online) -or
230
+ ($script:pendingAction -eq "Restart Proxy" -and $elapsed -gt 3000 -and $script:online -and $script:proxyPid -ne $script:pendingOldProxyPid)
231
+ if ($reached) { Complete-PendingAction $true }
232
+ elseif ($now -gt $script:pendingDeadline) { Complete-PendingAction $false }
233
+ }
234
+ }
235
+
236
+ $openItem.add_Click({ Start-OcxCommand @("gui") })
237
+ $startItem.add_Click({
238
+ $statusItem.Text = "Proxy: Starting..."
239
+ Set-PendingAction "Start Proxy" 15
240
+ if (-not (Start-OcxCommand @("__tray-start"))) { $script:pendingAction = $null }
241
+ })
242
+ $stopItem.add_Click({
243
+ $statusItem.Text = "Proxy: Stopping..."
244
+ Set-PendingAction "Stop Proxy" 15
245
+ if (-not (Start-OcxCommand @("stop"))) { $script:pendingAction = $null }
246
+ })
247
+ $restartItem.add_Click({
248
+ $statusItem.Text = "Proxy: Restarting..."
249
+ Set-PendingAction "Restart Proxy" 20
250
+ if (-not (Start-OcxCommand @("__tray-restart"))) { $script:pendingAction = $null }
251
+ })
252
+ $logsItem.add_Click({
253
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
254
+ $psi.FileName = $OpenCodexHome
255
+ $psi.UseShellExecute = $true
256
+ [void][System.Diagnostics.Process]::Start($psi)
257
+ })
258
+ $exitItem.add_Click({ [System.Windows.Forms.Application]::Exit() })
259
+ $notify.add_DoubleClick({ Start-OcxCommand @("gui") })
260
+
261
+ $timer = New-Object System.Windows.Forms.Timer
262
+ $timer.Interval = 3000
263
+ $timer.add_Tick({
264
+ if ($stopEvent.WaitOne(0)) {
265
+ [System.Windows.Forms.Application]::Exit()
266
+ return
267
+ }
268
+ Update-TrayState
269
+ })
270
+ $notify.ContextMenuStrip = $menu
271
+ $notify.Icon = $offlineIcon
272
+ $notify.Visible = $true
273
+ $notify.Text = "opencodex: Checking..."
274
+
275
+ try {
276
+ Update-TrayState
277
+ $timer.Start()
278
+ [System.Windows.Forms.Application]::Run()
279
+ } finally {
280
+ $timer.Stop()
281
+ $timer.Dispose()
282
+ $notify.Visible = $false
283
+ $notify.Dispose()
284
+ foreach ($icon in $script:ownedIcons) { $icon.Dispose() }
285
+ $menu.Dispose()
286
+ try { Remove-Item -LiteralPath $heartbeatPath -Force -ErrorAction SilentlyContinue } catch { }
287
+ try { $mutex.ReleaseMutex() } catch { }
288
+ $mutex.Dispose()
289
+ $stopEvent.Dispose()
290
+ }