@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,243 @@
1
+ /**
2
+ * Claude Code custom-agent definition injection (devlog 260712 070).
3
+ *
4
+ * The Agent tool's `model` argument is a hard 4-alias enum (2.1.207 binary), but an
5
+ * agent DEFINITION's frontmatter `model:` is a free string ("Model alias this agent
6
+ * uses. If omitted, inherits the parent's model"). So we sync the featured
7
+ * subagent roster (config.subagentModels, <=5) plus the main model (when not
8
+ * already covered) into ~/.claude/agents/ocx-*.md — one dispatchable
9
+ * `subagent_type` per routed model, loaded at the next session start.
10
+ *
11
+ * Ownership contract: this module only creates/overwrites/deletes files matching
12
+ * `ocx-*.md` inside the agents dir. User-authored agents are never touched.
13
+ */
14
+ import { lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import type { OcxConfig } from "../types";
17
+ import { claudeCodeAlias, claudeCodeNativeAlias } from "./alias";
18
+ import { resolveAutoContext, stripOneMillionMarker, withOneMillionMarker } from "./context-windows";
19
+ import { claudeConfigDir } from "./gateway-cache";
20
+ import { DEFAULT_SUBAGENT_MODELS, hasOwnProvider } from "../config";
21
+ import { effectiveBlockedSkillNames, resolveInboundModel } from "./inbound";
22
+ import { knownModelIdsForProvider } from "../router";
23
+ import { decodeRoutedModelId } from "../providers/slug-codec";
24
+
25
+ export interface ClaudeAgentDef {
26
+ file: string;
27
+ name: string;
28
+ model: string;
29
+ description: string;
30
+ effort?: NonNullable<OcxConfig["claudeCode"]>["subagentEffort"];
31
+ blockedSkills: readonly string[];
32
+ }
33
+
34
+ const OWNED_PREFIX = "ocx-";
35
+ /** Ownership proof (audit 071 #2): a file without this marker is NEVER touched. */
36
+ const GENERATED_MARKER = "generated-by: opencodex";
37
+
38
+ function sanitizeName(value: string): string {
39
+ const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
40
+ return cleaned.length > 0 ? cleaned : "model";
41
+ }
42
+
43
+ /**
44
+ * The user's default model as saved by the /model picker (settings.json `model`).
45
+ * `model: "inherit"` in agent frontmatter is DISPROVEN on 2.1.207 (live: a
46
+ * no-model ocx-self dispatch fell back to claude-fable-5 — devlog 072), so the
47
+ * self-clone pins this value instead, refreshed at every launch-time sync.
48
+ */
49
+ function pickerDefaultModel(configDir: string): string | null {
50
+ try {
51
+ const parsed = JSON.parse(readFileSync(join(configDir, "settings.json"), "utf8")) as Record<string, unknown>;
52
+ return typeof parsed.model === "string" && parsed.model.trim() !== "" ? parsed.model.trim() : null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /** Roster entry -> alias + display parts. Entries are bare native slugs or "provider/id".
59
+ * Codex-facing encoded ids (`provider/vendor-model`) decode to the native slash id first
60
+ * so the alias joins the raw-native context-window map (context-windows.ts). */
61
+ function entryParts(entry: string, config: OcxConfig): { alias: string; id: string; provider: string } {
62
+ const slash = entry.indexOf("/");
63
+ if (slash > 0) {
64
+ const provider = entry.slice(0, slash);
65
+ const prov = hasOwnProvider(config.providers, provider) ? config.providers[provider] : undefined;
66
+ const id = prov
67
+ ? decodeRoutedModelId(entry.slice(slash + 1), knownModelIdsForProvider(provider, prov))
68
+ : entry.slice(slash + 1);
69
+ return { alias: claudeCodeAlias(provider, id), id, provider };
70
+ }
71
+ return { alias: claudeCodeNativeAlias(entry), id: entry, provider: "native" };
72
+ }
73
+
74
+ export function buildClaudeAgentDefs(config: OcxConfig, windows: Record<string, number>, configDir = claudeConfigDir()): ClaudeAgentDef[] {
75
+ const auto = resolveAutoContext(config.claudeCode);
76
+ const blockedSkills = effectiveBlockedSkillNames(config.claudeCode);
77
+ const blockedSkillsFor = (model: string): readonly string[] => {
78
+ const unmarked = stripOneMillionMarker(model);
79
+ const nativePassthrough = config.claudeCode?.nativePassthrough !== false
80
+ && !unmarked.includes("/")
81
+ && /^(claude|anthropic)(?:-|$)/i.test(unmarked)
82
+ && resolveInboundModel(unmarked, config.claudeCode) === unmarked;
83
+ return nativePassthrough ? [] : blockedSkills;
84
+ };
85
+ const defs: ClaudeAgentDef[] = [];
86
+ const usedNames = new Set<string>();
87
+ const coveredModels = new Set<string>();
88
+
89
+ const push = (name: string, alias: string, description: string) => {
90
+ // Effective model value: [1m] marking follows the same predicate as env slots.
91
+ const model = withOneMillionMarker(alias, windows, auto) ?? alias;
92
+ const bare = alias.toLowerCase();
93
+ if (coveredModels.has(bare)) return;
94
+ coveredModels.add(bare);
95
+ let unique = name;
96
+ for (let i = 2; usedNames.has(unique); i++) unique = `${name}-${i}`;
97
+ usedNames.add(unique);
98
+ defs.push({
99
+ file: `${OWNED_PREFIX}${unique}.md`,
100
+ name: `${OWNED_PREFIX}${unique}`,
101
+ model,
102
+ description,
103
+ effort: config.claudeCode?.subagentEffort,
104
+ blockedSkills: blockedSkillsFor(model),
105
+ });
106
+ };
107
+
108
+ // Default roster applies only when the field is UNSET — an explicit [] is
109
+ // respected (audit 071 #6: an upgraded config must not lose the default five).
110
+ const roster = config.subagentModels === undefined ? DEFAULT_SUBAGENT_MODELS : config.subagentModels;
111
+ for (const entry of roster.slice(0, 5)) {
112
+ if (typeof entry !== "string" || entry.trim() === "") continue;
113
+ const { alias, id, provider } = entryParts(entry.trim(), config);
114
+ push(sanitizeName(id), alias, `Delegate work to ${id} (${provider}) via opencodex routing. General-purpose worker/explorer on that model. ${NO_MODEL_ARG}`);
115
+ }
116
+
117
+ // Self-clone slot: pin the picker-saved default (settings.json), falling back to
118
+ // config.claudeCode.model. `inherit` is NOT honored by 2.1.207 (live-disproven,
119
+ // devlog 072); a session started with a divergent --model stays divergent until
120
+ // the next launch sync — documented limit. No resolvable default -> no self def.
121
+ const selfModel = pickerDefaultModel(configDir) ?? (config.claudeCode?.model?.trim() || null);
122
+ if (selfModel) {
123
+ const marked = withOneMillionMarker(selfModel, windows, auto) ?? selfModel;
124
+ defs.push({
125
+ file: `${OWNED_PREFIX}self.md`,
126
+ name: `${OWNED_PREFIX}self`,
127
+ model: marked,
128
+ description: `Self-clone: delegate to your default main model (${marked}), synced from the /model picker at launch. ${NO_MODEL_ARG}`,
129
+ effort: config.claudeCode?.subagentEffort,
130
+ blockedSkills: blockedSkillsFor(marked),
131
+ });
132
+ }
133
+ return defs;
134
+ }
135
+
136
+ function skillNameLiteral(name: string): string {
137
+ return JSON.stringify(name)
138
+ .replaceAll("`", "\\u0060")
139
+ .replaceAll("<", "\\u003c")
140
+ .replaceAll(">", "\\u003e");
141
+ }
142
+
143
+ function renderAgentDef(def: ClaudeAgentDef): string {
144
+ const blockedSkillGuard = def.blockedSkills.length === 0 ? [] : [
145
+ "",
146
+ `Do not invoke blocked Claude Code skills: ${def.blockedSkills.map(skillNameLiteral).join(", ")}.`,
147
+ "Their document bundles are intentionally omitted for routed models; continue without loading them.",
148
+ ];
149
+ // YAML frontmatter: model ids carry dots/brackets — always double-quote scalars.
150
+ return [
151
+ "---",
152
+ `name: ${JSON.stringify(def.name)}`,
153
+ `description: ${JSON.stringify(def.description)}`,
154
+ `model: ${JSON.stringify(def.model)}`,
155
+ ...(def.effort ? [`effort: ${JSON.stringify(def.effort)}`] : []),
156
+ "---",
157
+ "",
158
+ `<!-- ${GENERATED_MARKER} -->`,
159
+ // Proxy routing directive (devlog 072): 2.1.207 does not honor custom gateway
160
+ // ids in agent frontmatter (falls back to sonnet — live-proven), but the agent
161
+ // BODY rides the subagent's system prompt verbatim. The proxy detects this
162
+ // directive and overrides the request model before routing/passthrough.
163
+ `<!-- ocx-route: ${def.model} -->`,
164
+ ...(def.effort ? [`<!-- ocx-effort: ${def.effort} -->`] : []),
165
+ "",
166
+ `You are a delegated worker running on \`${def.model}\` through the local opencodex proxy.`,
167
+ `IDENTITY: your ACTUAL underlying model is \`${def.model}\` — the opencodex proxy routes this`,
168
+ "session there regardless of what model name the Claude Code harness displays or claims.",
169
+ "If asked which model you are, answer with the id above; do not guess a Claude model name.",
170
+ ...blockedSkillGuard,
171
+ "",
172
+ "Complete the dispatched task directly and report results concisely. This file is",
173
+ "auto-generated by opencodex (`ocx claude`) from the featured subagent roster —",
174
+ "manual edits will be overwritten; remove the model from the roster to drop it.",
175
+ "",
176
+ ].join("\n");
177
+ }
178
+
179
+ /** True only for a REGULAR file we generated (marker present; symlinks never owned). */
180
+ function isOwnedFile(path: string): boolean {
181
+ try {
182
+ const st = lstatSync(path);
183
+ if (!st.isFile()) return false; // symlink or dir: never touch (audit 071 #2)
184
+ return readFileSync(path, "utf8").includes(GENERATED_MARKER);
185
+ } catch {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Sync owned agent files: write/overwrite current defs, prune stale ocx-*.md,
192
+ * never touch anything else. Ownership requires the generated marker; writes are
193
+ * atomic (tmp + rename). Best-effort — returns null on any failure.
194
+ */
195
+ export function syncClaudeAgentDefs(defs: readonly ClaudeAgentDef[], configDir = claudeConfigDir()): string[] | null {
196
+ try {
197
+ const dir = join(configDir, "agents");
198
+ mkdirSync(dir, { recursive: true });
199
+ const keep = new Set(defs.map(d => d.file));
200
+ for (const existing of readdirSync(dir)) {
201
+ if (!existing.startsWith(OWNED_PREFIX) || !existing.endsWith(".md")) continue;
202
+ if (!keep.has(existing) && isOwnedFile(join(dir, existing))) {
203
+ try { unlinkSync(join(dir, existing)); } catch { /* best-effort prune */ }
204
+ }
205
+ }
206
+ const written: string[] = [];
207
+ for (const def of defs) {
208
+ const target = join(dir, def.file);
209
+ // A pre-existing ocx-* file WITHOUT our marker is user property: skip the def.
210
+ try {
211
+ lstatSync(target);
212
+ if (!isOwnedFile(target)) continue;
213
+ } catch { /* does not exist: ours to create */ }
214
+ const tmp = `${target}.tmp-${process.pid}`;
215
+ writeFileSync(tmp, renderAgentDef(def), { encoding: "utf8", mode: 0o644 });
216
+ renameSync(tmp, target);
217
+ written.push(def.file);
218
+ }
219
+ return written;
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
224
+
225
+ /** Launch-time hook: gate + build + sync in one call (used by ocx claude and systemEnv). */
226
+ export function injectClaudeAgentDefs(config: OcxConfig, windows: Record<string, number>, configDir?: string): string[] | null {
227
+ if (config.claudeCode?.enabled === false || config.claudeCode?.injectAgents === false) {
228
+ // Disabled: prune verified-owned files so stale definitions stop loading
229
+ // in future sessions (audit 071 #3).
230
+ return syncClaudeAgentDefs([], configDir);
231
+ }
232
+ return syncClaudeAgentDefs(buildClaudeAgentDefs(config, windows, configDir), configDir);
233
+ }
234
+ /**
235
+ * Dispatcher directive appended to every ocx-* description. The ocx-route body
236
+ * directive makes the Agent tool's `model` argument INERT (the proxy overrides
237
+ * the request model before routing — live-proven), so instead of asking the
238
+ * dispatcher to omit it (which caused schema-anxiety loops), we hand it a fixed
239
+ * placeholder: any value works; "haiku" is canonical because a haiku-labeled call
240
+ * is visibly a placeholder in the Claude Code UI, while "sonnet" was
241
+ * indistinguishable from a genuine Sonnet call (issue #252).
242
+ */
243
+ const NO_MODEL_ARG = "NOTE: this agent's real model is pinned by the opencodex proxy — the `model` argument is ignored. Pass model: \"haiku\" as a placeholder (or omit it); routing is unaffected either way.";
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Gateway model-discovery aliases (devlog/260711_claude_inbound/020, 003 G1-G6).
3
+ *
4
+ * Claude Code's /model picker only lists discovery entries whose id literally
5
+ * begins with `claude` or `anthropic`, so routed models are exposed as
6
+ * `claude-ocx-<provider>--<model>` with an honest display_name. Aliases must be
7
+ * deterministic, reversible, and STABLE across releases (picker selections
8
+ * persist to Claude Code's settings.json `model` field).
9
+ *
10
+ * Versioned prefixes:
11
+ * - `claude-ocx-` (v1) — legacy / plain model ids with no `/` or `~`. Decode
12
+ * is literal (no escape expansion), so a persisted model id that literally
13
+ * contained the two-char sequences `~s` / `~t` keeps resolving.
14
+ * - `claude-ocx2-` (v2) — used whenever the model id needs escape encoding
15
+ * (`/` → `~s`, `~` → `~t`). Decode expands those escapes. New slash/tilde
16
+ * models always mint v2 so they cannot collide with v1 literals.
17
+ *
18
+ * Reversibility rules:
19
+ * - providers containing `--` or `/` are not aliased (split boundary safety);
20
+ * - model ids MAY contain `/` or `~` — minted under the v2 prefix with escapes
21
+ * (e.g. openrouter `anthropic/claude-opus-4-8` →
22
+ * `claude-ocx2-openrouter--anthropic~sclaude-opus-4-8`);
23
+ * - model ids MAY contain `--` (resolve splits on the FIRST `--` only);
24
+ * - native OpenAI slugs use the pseudo-provider `native` and resolve back to
25
+ * the bare slug; a real provider named "native" is therefore never aliased.
26
+ */
27
+
28
+ import { desktop3pAlias } from "./desktop-3p";
29
+
30
+ /** Legacy / plain readable prefix (literal model portion on decode). */
31
+ export const CLAUDE_ALIAS_PREFIX_V1 = "claude-ocx-";
32
+ /** Escape-encoded readable prefix (`~s`/`~t` expanded on decode). */
33
+ export const CLAUDE_ALIAS_PREFIX_V2 = "claude-ocx2-";
34
+ /**
35
+ * Current write prefix for plain (unescaped) model ids.
36
+ * Escape-needing models mint {@link CLAUDE_ALIAS_PREFIX_V2} instead.
37
+ */
38
+ export const CLAUDE_ALIAS_PREFIX = CLAUDE_ALIAS_PREFIX_V1;
39
+
40
+ /** Encoded `/` inside the model portion of a v2 Claude Code alias. */
41
+ const CLAUDE_ALIAS_SLASH_ENC = "~s";
42
+ /** Encoded literal `~` inside the model portion of a v2 Claude Code alias. */
43
+ const CLAUDE_ALIAS_TILDE_ENC = "~t";
44
+ const NATIVE_PSEUDO_PROVIDER = "native";
45
+
46
+ function modelNeedsEscapeEncoding(modelId: string): boolean {
47
+ return modelId.includes("/") || modelId.includes("~");
48
+ }
49
+
50
+ function encodeModelId(modelId: string): string {
51
+ // Escape literal tildes first so slash encoding cannot create ambiguity.
52
+ return modelId
53
+ .replaceAll("~", CLAUDE_ALIAS_TILDE_ENC)
54
+ .replaceAll("/", CLAUDE_ALIAS_SLASH_ENC);
55
+ }
56
+
57
+ function decodeEscapedModelId(encoded: string): string {
58
+ let out = "";
59
+ for (let i = 0; i < encoded.length; i++) {
60
+ if (encoded[i] === "~" && i + 1 < encoded.length) {
61
+ const next = encoded[i + 1];
62
+ if (next === "s") {
63
+ out += "/";
64
+ i += 1;
65
+ continue;
66
+ }
67
+ if (next === "t") {
68
+ out += "~";
69
+ i += 1;
70
+ continue;
71
+ }
72
+ }
73
+ out += encoded[i];
74
+ }
75
+ return out;
76
+ }
77
+
78
+ function splitAlias(id: string, prefix: string): { provider: string; model: string } | null {
79
+ const rest = id.slice(prefix.length);
80
+ const sep = rest.indexOf("--");
81
+ if (sep <= 0) return null;
82
+ const provider = rest.slice(0, sep);
83
+ const model = rest.slice(sep + 2);
84
+ if (!provider || !model) return null;
85
+ return { provider, model };
86
+ }
87
+
88
+ /** Alias for a routed "<provider>/<model>" pair; null when not representable. */
89
+ export function aliasForRoute(provider: string, modelId: string): string | null {
90
+ if (!provider || provider.includes("--") || provider.includes("/") || provider === NATIVE_PSEUDO_PROVIDER) return null;
91
+ if (!modelId) return null;
92
+ if (modelNeedsEscapeEncoding(modelId)) {
93
+ return `${CLAUDE_ALIAS_PREFIX_V2}${provider}--${encodeModelId(modelId)}`;
94
+ }
95
+ return `${CLAUDE_ALIAS_PREFIX_V1}${provider}--${modelId}`;
96
+ }
97
+
98
+ /** Alias for a native OpenAI slug (bare model id, no provider namespace). */
99
+ export function aliasForNative(slug: string): string | null {
100
+ // Reject "/" — native ids are bare slugs. Literal `~` is fine via v2 + ~t.
101
+ if (!slug || slug.includes("/") || slug.includes("--")) return null;
102
+ if (modelNeedsEscapeEncoding(slug)) {
103
+ return `${CLAUDE_ALIAS_PREFIX_V2}${NATIVE_PSEUDO_PROVIDER}--${encodeModelId(slug)}`;
104
+ }
105
+ return `${CLAUDE_ALIAS_PREFIX_V1}${NATIVE_PSEUDO_PROVIDER}--${slug}`;
106
+ }
107
+
108
+ /**
109
+ * Reverse an alias to the inbound model string routeModel understands:
110
+ * routed -> "<provider>/<model>", native -> bare slug. Null when not an alias.
111
+ */
112
+ export function resolveAlias(id: string): string | null {
113
+ // Check v2 before v1 for clarity (prefixes are disjoint: ocx2 vs ocx-).
114
+ if (id.startsWith(CLAUDE_ALIAS_PREFIX_V2)) {
115
+ const parts = splitAlias(id, CLAUDE_ALIAS_PREFIX_V2);
116
+ if (!parts) return null;
117
+ const model = decodeEscapedModelId(parts.model);
118
+ if (!model) return null;
119
+ return parts.provider === NATIVE_PSEUDO_PROVIDER ? model : `${parts.provider}/${model}`;
120
+ }
121
+ if (id.startsWith(CLAUDE_ALIAS_PREFIX_V1)) {
122
+ const parts = splitAlias(id, CLAUDE_ALIAS_PREFIX_V1);
123
+ if (!parts) return null;
124
+ // Literal decode — preserves pre-escape aliases whose model id contained
125
+ // the two-char sequences ~s / ~t.
126
+ return parts.provider === NATIVE_PSEUDO_PROVIDER ? parts.model : `${parts.provider}/${parts.model}`;
127
+ }
128
+ return null;
129
+ }
130
+
131
+ /**
132
+ * Claude Code (CLI) surface alias — devlog 050 + audit 051 #2.
133
+ *
134
+ * The readable `claude-ocx*` form when representable; otherwise the desktop-3p
135
+ * hash so the model still appears in discovery (collisions follow the same
136
+ * first-wins policy as the desktop registry — audit 051 #1). Real Anthropic
137
+ * models pass through unchanged (they must keep hitting the sk-ant passthrough).
138
+ * Both families keep decoding forever in resolveInboundModel, so ids persisted
139
+ * in Claude Code's settings.json never break when the surface style changes.
140
+ */
141
+ export function claudeCodeAlias(provider: string, modelId: string): string {
142
+ if (provider === "anthropic" && modelId.startsWith("claude-")) return modelId;
143
+ return aliasForRoute(provider, modelId) ?? desktop3pAlias(provider, modelId);
144
+ }
145
+
146
+ /** Claude Code (CLI) surface alias for a native OpenAI slug. */
147
+ export function claudeCodeNativeAlias(slug: string): string {
148
+ return aliasForNative(slug) ?? desktop3pAlias("native", slug);
149
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Claude auth presence detection.
3
+ *
4
+ * THREE values, not two. `unknown` means a source could not be READ (denied keychain,
5
+ * permission error, corrupt JSON) — treating that as `absent` would silently flip a
6
+ * subscriber into proxy mode, which is the failure this whole unit exists to prevent
7
+ * (devlog/_plan/260726_claude_auth_auto/001 F1). The resolver maps unknown to the
8
+ * historical default (subscription), never to proxy.
9
+ *
10
+ * IO is injectable so every source is testable without touching the real home
11
+ * directory, keychain, or environment.
12
+ */
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { spawnSync } from "node:child_process";
17
+
18
+ export type AuthPresence = "present" | "absent" | "unknown";
19
+
20
+ export type AuthSourceId =
21
+ | "claude-json-oauth" // S1: <config-dir>/../.claude.json oauthAccount
22
+ | "claude-credentials-file" // S2: <config-dir>/.credentials.json
23
+ | "macos-keychain" // S3: security find-generic-password (metadata only)
24
+ | "exported-env"; // S5: ANTHROPIC_API_KEY / a USER's ANTHROPIC_AUTH_TOKEN
25
+
26
+ // NOTE: there is deliberately no "opencodex anthropic OAuth" source. That is a
27
+ // PROVIDER credential in opencodex's own store which the Claude CLI never consumes,
28
+ // so it is not evidence the client can authenticate natively (audit 002 §5).
29
+
30
+ /**
31
+ * The one opencodex-owned dummy token. Exported so the CLI, the system-env writer and
32
+ * the tests all compare against the same literal — a second copy is how the marker
33
+ * feedback loop (002 §1) sneaks back in.
34
+ */
35
+ export const PROXY_MARKER = "opencodex-proxy";
36
+
37
+ const KEYCHAIN_SERVICE = "Claude Code-credentials";
38
+ /** `security` exit code for "the item does not exist" — a real absent, not a failure. */
39
+ const KEYCHAIN_ITEM_NOT_FOUND = 44;
40
+ const KEYCHAIN_TIMEOUT_MS = 1_500;
41
+
42
+ export interface AuthSourceResult {
43
+ source: AuthSourceId;
44
+ presence: AuthPresence;
45
+ /** One short, credential-free line. Never contains token material. */
46
+ detail?: string;
47
+ }
48
+
49
+ export interface AuthDetectDeps {
50
+ /** Parsed ~/.claude.json; undefined when missing; throws on corrupt/unreadable. */
51
+ readClaudeJson(): Record<string, unknown> | undefined;
52
+ /** Whether <config-dir>/.credentials.json exists; throws on read error. */
53
+ credentialsFileExists(): boolean;
54
+ /** present/absent/unknown for the macOS keychain probe; absent on non-Darwin. */
55
+ keychainProbe(): AuthPresence;
56
+ /**
57
+ * The environment to inspect for S5. Callers pass the SAME base env the launch will
58
+ * use, so detection and the spawned process can never disagree. Injection is typed
59
+ * to exclude this key (see `detectClaudeAuth` callers) so a test fake cannot
60
+ * silently replace the binding.
61
+ */
62
+ env(): NodeJS.ProcessEnv;
63
+ /**
64
+ * Token values opencodex itself put into the environment. `system-env.ts` exports the
65
+ * configured admission key as `ANTHROPIC_AUTH_TOKEN`, so without this the detector
66
+ * reads OUR OWN output back as proof the user can authenticate natively — the same
67
+ * feedback loop the `PROXY_MARKER` guard closes, one variable over.
68
+ */
69
+ ownTokens?: readonly string[];
70
+ }
71
+
72
+ export interface AuthDetectResult {
73
+ /** present if ANY source is present; unknown if none present but ANY unknown. */
74
+ presence: AuthPresence;
75
+ /** The source that proved presence, when any — feeds the GUI reason badge. */
76
+ foundBy?: AuthSourceId;
77
+ sources: AuthSourceResult[];
78
+ /**
79
+ * True when the inspected env carries OUR OWN dummy marker. It must never count as
80
+ * user auth, and a subscription resolution strips it before launch (002 §1).
81
+ */
82
+ staleProxyMarker: boolean;
83
+ }
84
+
85
+ /** Claude Code config dir: `CLAUDE_CONFIG_DIR` override, else `~/.claude`. */
86
+ export function claudeConfigDir(env: NodeJS.ProcessEnv = process.env): string {
87
+ const explicit = env.CLAUDE_CONFIG_DIR?.trim();
88
+ return explicit ? explicit : join(homeDir(env), ".claude");
89
+ }
90
+
91
+ /**
92
+ * `os.homedir()` reads the OS user database and ignores a reassigned `HOME`, which
93
+ * makes an isolated-profile probe silently read the real user's files. Prefer the
94
+ * environment the caller handed us; fall back to the OS value.
95
+ */
96
+ function homeDir(env: NodeJS.ProcessEnv): string {
97
+ const fromEnv = env.HOME?.trim() || env.USERPROFILE?.trim();
98
+ return fromEnv ? fromEnv : homedir();
99
+ }
100
+
101
+ function detectClaudeJson(deps: AuthDetectDeps): AuthSourceResult {
102
+ try {
103
+ const parsed = deps.readClaudeJson();
104
+ if (parsed === undefined) return { source: "claude-json-oauth", presence: "absent" };
105
+ const account = parsed.oauthAccount;
106
+ if (account && typeof account === "object" && !Array.isArray(account)) {
107
+ const email = (account as Record<string, unknown>).emailAddress;
108
+ if (typeof email === "string" && email.trim().length > 0) {
109
+ // Never echo the address itself — the detail line is UI copy, not a credential.
110
+ return { source: "claude-json-oauth", presence: "present", detail: "oauthAccount" };
111
+ }
112
+ }
113
+ return { source: "claude-json-oauth", presence: "absent" };
114
+ } catch {
115
+ // Corrupt or unreadable is NOT absent (F1).
116
+ return { source: "claude-json-oauth", presence: "unknown", detail: "unreadable" };
117
+ }
118
+ }
119
+
120
+ function detectCredentialsFile(deps: AuthDetectDeps): AuthSourceResult {
121
+ try {
122
+ return {
123
+ source: "claude-credentials-file",
124
+ presence: deps.credentialsFileExists() ? "present" : "absent",
125
+ };
126
+ } catch {
127
+ return { source: "claude-credentials-file", presence: "unknown", detail: "unreadable" };
128
+ }
129
+ }
130
+
131
+ function detectKeychain(deps: AuthDetectDeps): AuthSourceResult {
132
+ try {
133
+ return { source: "macos-keychain", presence: deps.keychainProbe() };
134
+ } catch {
135
+ return { source: "macos-keychain", presence: "unknown", detail: "probe failed" };
136
+ }
137
+ }
138
+
139
+ function detectExportedEnv(deps: AuthDetectDeps): AuthSourceResult {
140
+ try {
141
+ const env = deps.env();
142
+ const isOwn = (value: string): boolean =>
143
+ value === PROXY_MARKER || (deps.ownTokens ?? []).includes(value);
144
+ const apiKey = env.ANTHROPIC_API_KEY?.trim();
145
+ if (apiKey && !isOwn(apiKey)) {
146
+ return { source: "exported-env", presence: "present", detail: "ANTHROPIC_API_KEY" };
147
+ }
148
+ const token = env.ANTHROPIC_AUTH_TOKEN?.trim();
149
+ // Our own dummy is opencodex state, never user auth: counting it would make a
150
+ // proxy-mode launch look authenticated on the NEXT launch (002 §1). The configured
151
+ // admission key is opencodex state for exactly the same reason — the system-env
152
+ // writer exports it into this very variable.
153
+ if (token && !isOwn(token)) {
154
+ return { source: "exported-env", presence: "present", detail: "ANTHROPIC_AUTH_TOKEN" };
155
+ }
156
+ return { source: "exported-env", presence: "absent" };
157
+ } catch {
158
+ return { source: "exported-env", presence: "unknown" };
159
+ }
160
+ }
161
+
162
+ export function detectClaudeAuth(deps: AuthDetectDeps): AuthDetectResult {
163
+ const sources = [
164
+ detectClaudeJson(deps),
165
+ detectCredentialsFile(deps),
166
+ detectKeychain(deps),
167
+ detectExportedEnv(deps),
168
+ ];
169
+ let staleProxyMarker = false;
170
+ try {
171
+ staleProxyMarker = deps.env().ANTHROPIC_AUTH_TOKEN?.trim() === PROXY_MARKER;
172
+ } catch {
173
+ staleProxyMarker = false;
174
+ }
175
+
176
+ const present = sources.find(source => source.presence === "present");
177
+ if (present) return { presence: "present", foundBy: present.source, sources, staleProxyMarker };
178
+ if (sources.some(source => source.presence === "unknown")) {
179
+ return { presence: "unknown", sources, staleProxyMarker };
180
+ }
181
+ return { presence: "absent", sources, staleProxyMarker };
182
+ }
183
+
184
+ /**
185
+ * Real IO. The keychain probe is METADATA ONLY: no `-g` and no `-w`, because those
186
+ * flags print the password itself. We only need the exit code.
187
+ */
188
+ export function defaultAuthDetectDeps(
189
+ env: NodeJS.ProcessEnv = process.env,
190
+ ownTokens: readonly string[] = [],
191
+ ): AuthDetectDeps {
192
+ const configDir = claudeConfigDir(env);
193
+ return {
194
+ readClaudeJson() {
195
+ // Sibling of the config dir when CLAUDE_CONFIG_DIR is set, else ~/.claude.json.
196
+ const path = env.CLAUDE_CONFIG_DIR?.trim()
197
+ ? join(configDir, "..", ".claude.json")
198
+ : join(homeDir(env), ".claude.json");
199
+ if (!existsSync(path)) return undefined;
200
+ // A parse failure propagates so the caller records `unknown`, not `absent`.
201
+ return JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
202
+ },
203
+ credentialsFileExists() {
204
+ return existsSync(join(configDir, ".credentials.json"));
205
+ },
206
+ keychainProbe() {
207
+ if (process.platform !== "darwin") return "absent";
208
+ const result = spawnSync("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE], {
209
+ timeout: KEYCHAIN_TIMEOUT_MS,
210
+ stdio: ["ignore", "ignore", "ignore"],
211
+ });
212
+ if (result.error || result.signal !== null) return "unknown";
213
+ if (result.status === 0) return "present";
214
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND) return "absent";
215
+ return "unknown";
216
+ },
217
+ env: () => env,
218
+ ownTokens,
219
+ };
220
+ }
221
+
222
+ /**
223
+ * The token values opencodex itself exports. Configured admission keys land in
224
+ * `ANTHROPIC_AUTH_TOKEN` (see `system-env.ts`), so detection must not read them back
225
+ * as user auth.
226
+ */
227
+ export function ownAdmissionTokens(config: { apiKeys?: Array<{ key: string }> }): string[] {
228
+ return (config.apiKeys ?? []).map(entry => entry.key).filter(key => key.length > 0);
229
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * One-time authMode migration (devlog/_plan/260726_claude_auth_auto/015).
3
+ *
4
+ * Before auto existed, choosing "Subscription" in the GUI DELETED the key. So on disk
5
+ * an explicit subscription choice and "never chose anything" are byte-identical, and
6
+ * reading both as auto would flip a deliberate subscriber into proxy mode the moment
7
+ * their credentials are undetectable. This migration preserves the old EFFECTIVE
8
+ * behaviour for configs that already had a claudeCode block, and leaves genuinely
9
+ * untouched configs on auto.
10
+ */
11
+ import type { OcxConfig } from "../types";
12
+
13
+ /**
14
+ * @returns true when the config changed and the caller should persist it.
15
+ */
16
+ export function runClaudeAuthModeMigration(config: OcxConfig, now = new Date()): boolean {
17
+ const claudeCode = config.claudeCode;
18
+ // No block at all = a fresh or Claude-untouched install: auto is the right default,
19
+ // and writing a sentinel here would create the block for no reason.
20
+ if (!claudeCode) return false;
21
+ // Idempotent: the sentinel's presence is the "already migrated" fact. A user who
22
+ // later picks Auto deletes authMode but KEEPS the sentinel, so the next start must
23
+ // not resurrect subscription.
24
+ if (typeof claudeCode.authModeMigratedAt === "string" && claudeCode.authModeMigratedAt.length > 0) {
25
+ return false;
26
+ }
27
+ if (claudeCode.authMode === undefined) {
28
+ claudeCode.authMode = "subscription";
29
+ }
30
+ claudeCode.authModeMigratedAt = now.toISOString();
31
+ return true;
32
+ }