@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,303 @@
1
+ /**
2
+ * Token Guardian — background proactive OAuth refresh.
3
+ *
4
+ * Keeps idle tokens from aging out server-side (the reported multi-account Codex-pool bug) by
5
+ * refreshing them BEFORE a request needs them. It is a CALLER of the existing refresh machinery —
6
+ * it adds no new refresh/locking logic:
7
+ * - single-account providers → getValidAccessToken() (in-memory dedup + persist)
8
+ * - multi-account Codex pool → getValidCodexToken() (file lock + generation CAS + grant fingerprint)
9
+ *
10
+ * Safety by construction: the guardian only touches a provider whose EFFECTIVE refreshPolicy is
11
+ * "proactive", and the global switch (config.tokenGuardian.enabled) defaults OFF, so a default
12
+ * install adds zero ToS-detection surface. See devlog 260703_oauth-multi-account-refresh-and-tos.
13
+ */
14
+ import { loadConfig } from "../config";
15
+ import type { OcxConfig, OcxTokenGuardianConfig } from "../types";
16
+ import { listAccounts } from "./store";
17
+ import { getValidAccessTokenForAccount, listOAuthProviders, OAuthLoginRequiredError, resolveRefreshPolicy } from "./index";
18
+ import {
19
+ getValidCodexToken,
20
+ listCodexAccountIds,
21
+ markCodexAccountValidated,
22
+ markCodexAccountValidationFailed,
23
+ readCodexAccountRecord,
24
+ TokenRefreshError,
25
+ CodexCredentialRefreshBusyError,
26
+ CodexCredentialRefreshStaleError,
27
+ } from "../codex/account-store";
28
+ import { codexWarmupFailureReason, warmCodexAccount } from "../codex/warmup";
29
+ import { getMainAccountToken, MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
30
+ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
31
+ import { providerCodexAccountMode } from "../providers/registry";
32
+ import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
33
+
34
+ export interface TokenGuardianHandle {
35
+ stop(): void;
36
+ }
37
+
38
+ export interface GuardianSweepResult {
39
+ enabled: boolean;
40
+ refreshed: string[];
41
+ warmed: string[];
42
+ failed: string[];
43
+ skippedBackoff: string[];
44
+ }
45
+
46
+ const DEFAULTS = {
47
+ tickSeconds: 21600, // 6h — matches codex-lb's guardian cadence
48
+ jitterSeconds: 300,
49
+ concurrency: 3,
50
+ leadSeconds: 900,
51
+ failureBackoffBaseSeconds: 300,
52
+ failureBackoffMaxSeconds: 3600,
53
+ codexWarmupMaxAgeSeconds: 691_200, // 8d — matches Codex managed-auth last_refresh cadence.
54
+ codexWarmupModel: "gpt-5.4-mini",
55
+ };
56
+
57
+ interface BackoffEntry {
58
+ attempts: number;
59
+ retryAfterMs: number;
60
+ }
61
+
62
+ // Module-scoped so backoff survives across sweeps within one process (keyed "oauth:<p>" / "codex:<id>").
63
+ const backoff = new Map<string, BackoffEntry>();
64
+ let lastReconciledGeneration = 0;
65
+ let liveBackoffKeys = new Set<string>();
66
+
67
+ /** Test hook: clear backoff state between cases. */
68
+ export function __resetGuardianState(): void {
69
+ backoff.clear();
70
+ }
71
+
72
+ function num(value: number | undefined, fallback: number, min: number): number {
73
+ return typeof value === "number" && Number.isFinite(value) && value >= min ? value : fallback;
74
+ }
75
+
76
+ function resolved(g: OcxTokenGuardianConfig | undefined) {
77
+ return {
78
+ tickSeconds: num(g?.tickSeconds, DEFAULTS.tickSeconds, 60),
79
+ jitterSeconds: num(g?.jitterSeconds, DEFAULTS.jitterSeconds, 0),
80
+ concurrency: Math.max(1, Math.floor(num(g?.concurrency, DEFAULTS.concurrency, 1))),
81
+ leadSeconds: num(g?.leadSeconds, DEFAULTS.leadSeconds, 0),
82
+ backoffBaseSeconds: num(g?.failureBackoffBaseSeconds, DEFAULTS.failureBackoffBaseSeconds, 0),
83
+ backoffMaxSeconds: num(g?.failureBackoffMaxSeconds, DEFAULTS.failureBackoffMaxSeconds, 0),
84
+ codexWarmupEnabled: g?.codexWarmupEnabled === true,
85
+ codexWarmupMaxAgeSeconds: num(g?.codexWarmupMaxAgeSeconds, DEFAULTS.codexWarmupMaxAgeSeconds, 60),
86
+ codexWarmupModel: g?.codexWarmupModel?.trim() || DEFAULTS.codexWarmupModel,
87
+ };
88
+ }
89
+
90
+ function inBackoff(key: string, nowMs: number): boolean {
91
+ const entry = backoff.get(key);
92
+ return entry !== undefined && entry.retryAfterMs > nowMs;
93
+ }
94
+
95
+ function recordFailure(
96
+ key: string,
97
+ nowMs: number,
98
+ baseSeconds: number,
99
+ maxSeconds: number,
100
+ permanent: boolean,
101
+ writerGeneration = captureConfigGeneration(),
102
+ ): void {
103
+ if (writerGeneration < lastReconciledGeneration && !liveBackoffKeys.has(key)) return;
104
+ const prev = backoff.get(key);
105
+ const attempts = (prev?.attempts ?? 0) + 1;
106
+ // Permanent failures (revoked/expired refresh token) wait the full ceiling — nothing but a
107
+ // re-login fixes them, so there is no point retrying sooner.
108
+ const delaySeconds = permanent
109
+ ? maxSeconds
110
+ : Math.min(maxSeconds, baseSeconds * 2 ** (attempts - 1));
111
+ backoff.set(key, { attempts, retryAfterMs: nowMs + delaySeconds * 1000 });
112
+ }
113
+
114
+ async function runWithConcurrency(tasks: Array<() => Promise<void>>, limit: number): Promise<void> {
115
+ let cursor = 0;
116
+ const workers = Array.from({ length: Math.min(limit, tasks.length) }, async () => {
117
+ while (cursor < tasks.length) {
118
+ const task = tasks[cursor++];
119
+ if (task) await task();
120
+ }
121
+ });
122
+ await Promise.all(workers);
123
+ }
124
+
125
+ /**
126
+ * One refresh sweep. Reads live config + stores; refreshes every proactive-policy credential that
127
+ * will expire before the next sweep (tick + lead horizon). Never throws — per-credential failures
128
+ * are captured into backoff and the result. Returns a redacted summary (provider names / account
129
+ * ids only, never tokens).
130
+ */
131
+ export async function guardianSweep(nowMs: number = Date.now()): Promise<GuardianSweepResult> {
132
+ const writerGeneration = captureConfigGeneration();
133
+ const config: OcxConfig = loadConfig();
134
+ const g = config.tokenGuardian;
135
+ const result: GuardianSweepResult = { enabled: !!g?.enabled, refreshed: [], warmed: [], failed: [], skippedBackoff: [] };
136
+ if (!g?.enabled) return result;
137
+
138
+ const opts = resolved(g);
139
+ const horizonMs = (opts.tickSeconds + opts.leadSeconds) * 1000;
140
+ const tasks: Array<() => Promise<void>> = [];
141
+
142
+ // A) OAuth providers — every account in each provider's set (multiauth keep-alive),
143
+ // skipping accounts already marked needsReauth (terminal; only a re-login fixes them).
144
+ for (const provider of listOAuthProviders()) {
145
+ if (resolveRefreshPolicy(provider, config) !== "proactive") continue;
146
+ for (const account of listAccounts(provider)) {
147
+ if (account.needsReauth) continue;
148
+ if (account.credential.expires > nowMs + horizonMs) continue;
149
+ const key = `oauth:${provider}:${account.id}`;
150
+ if (inBackoff(key, nowMs)) { result.skippedBackoff.push(key); continue; }
151
+ tasks.push(async () => {
152
+ try {
153
+ await getValidAccessTokenForAccount(provider, account.id);
154
+ backoff.delete(key);
155
+ result.refreshed.push(key);
156
+ } catch (err) {
157
+ // Terminal grant failures surface as OAuthLoginRequiredError (account marked
158
+ // needsReauth by the resolver) — back off at the ceiling; transient errors backoff exponentially.
159
+ const permanent = err instanceof OAuthLoginRequiredError;
160
+ recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, permanent, writerGeneration);
161
+ result.failed.push(key);
162
+ }
163
+ });
164
+ }
165
+ }
166
+
167
+ // B) canonical OpenAI Codex login. Direct warms main only; pool additionally maintains every
168
+ // added account. The direct branch returns before the managed account store is enumerated.
169
+ const openai = config.providers[OPENAI_CODEX_PROVIDER_ID];
170
+ if (
171
+ openai
172
+ && openai.disabled !== true
173
+ && isCanonicalOpenAiForwardProvider(openai)
174
+ && resolveRefreshPolicy(OPENAI_CODEX_PROVIDER_ID, config) === "proactive"
175
+ ) {
176
+ const mode = providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) ?? "pool";
177
+ if (opts.codexWarmupEnabled) {
178
+ const mainToken = getMainAccountToken();
179
+ const key = `codex:${MAIN_CODEX_ACCOUNT_ID}`;
180
+ if (mainToken && !inBackoff(key, nowMs)) {
181
+ tasks.push(async () => {
182
+ try {
183
+ await warmCodexAccount({
184
+ accessToken: mainToken.accessToken,
185
+ chatgptAccountId: mainToken.chatgptAccountId,
186
+ model: opts.codexWarmupModel,
187
+ });
188
+ backoff.delete(key);
189
+ result.warmed.push(key);
190
+ } catch (err) {
191
+ recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, false, writerGeneration);
192
+ result.failed.push(key);
193
+ }
194
+ });
195
+ } else if (mainToken) {
196
+ result.skippedBackoff.push(key);
197
+ }
198
+ }
199
+
200
+ const addedAccountIds = mode === "pool" ? listCodexAccountIds() : [];
201
+ for (const id of addedAccountIds) {
202
+ const record = readCodexAccountRecord(id);
203
+ if (!record || record.deletedAt != null) continue;
204
+ const cred = record.credential;
205
+ if (!cred) continue;
206
+ const needsRefresh = cred.expiresAt <= nowMs + horizonMs;
207
+ const needsWarmup = opts.codexWarmupEnabled
208
+ && (record.lastCodexValidatedAt === undefined || nowMs - record.lastCodexValidatedAt > opts.codexWarmupMaxAgeSeconds * 1000);
209
+ if (!needsRefresh && !needsWarmup) continue;
210
+ const key = `codex:${id}`;
211
+ if (inBackoff(key, nowMs)) { result.skippedBackoff.push(key); continue; }
212
+ tasks.push(async () => {
213
+ try {
214
+ const token = await getValidCodexToken(id);
215
+ if (needsRefresh) result.refreshed.push(key);
216
+ if (needsWarmup) {
217
+ await warmCodexAccount({
218
+ accessToken: token.accessToken,
219
+ chatgptAccountId: token.chatgptAccountId,
220
+ model: opts.codexWarmupModel,
221
+ });
222
+ markCodexAccountValidated(id, Date.now());
223
+ result.warmed.push(key);
224
+ }
225
+ backoff.delete(key);
226
+ } catch (err) {
227
+ if (err instanceof CodexCredentialRefreshBusyError || err instanceof CodexCredentialRefreshStaleError) {
228
+ recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, false, writerGeneration);
229
+ result.skippedBackoff.push(key);
230
+ return;
231
+ }
232
+ const permanent = err instanceof TokenRefreshError && (err.reason === "revoked" || err.reason === "expired");
233
+ if (needsWarmup && !(err instanceof TokenRefreshError)) {
234
+ markCodexAccountValidationFailed(id, codexWarmupFailureReason(err));
235
+ }
236
+ recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, permanent, writerGeneration);
237
+ result.failed.push(key);
238
+ }
239
+ });
240
+ }
241
+ }
242
+
243
+ await runWithConcurrency(tasks, opts.concurrency);
244
+ return result;
245
+ }
246
+
247
+ export function reconcileGuardianBackoff(context: GenerationContext): number {
248
+ if (context.generation <= lastReconciledGeneration) return 0;
249
+ const valid = new Set<string>();
250
+ for (const id of context.codexAccountIds) valid.add(`codex:${id}`);
251
+ for (const key of context.oauthAccountKeys) {
252
+ const separator = key.indexOf("\0");
253
+ if (separator <= 0) continue;
254
+ valid.add(`oauth:${key.slice(0, separator)}:${key.slice(separator + 1)}`);
255
+ }
256
+ let removed = 0;
257
+ for (const key of backoff.keys()) {
258
+ if (valid.has(key)) continue;
259
+ backoff.delete(key);
260
+ removed += 1;
261
+ }
262
+ liveBackoffKeys = valid;
263
+ lastReconciledGeneration = context.generation;
264
+ return removed;
265
+ }
266
+
267
+ /**
268
+ * Start the background sweep loop. Returns a handle whose stop() clears the pending timer (in-flight
269
+ * refreshes settle on their own). Schedules recursively so each interval gets fresh jitter. The loop
270
+ * runs even when the guardian is disabled (each sweep is a cheap no-op) so toggling `enabled` in
271
+ * config takes effect on the next tick without a restart.
272
+ */
273
+ export function startTokenGuardian(): TokenGuardianHandle {
274
+ let stopped = false;
275
+ let timer: ReturnType<typeof setTimeout> | undefined;
276
+
277
+ const scheduleNext = () => {
278
+ if (stopped) return;
279
+ const opts = resolved(loadConfig().tokenGuardian);
280
+ const delayMs = (opts.tickSeconds + Math.random() * opts.jitterSeconds) * 1000;
281
+ timer = setTimeout(runSweep, delayMs);
282
+ if (typeof timer.unref === "function") timer.unref(); // never keep the process alive for a sweep
283
+ };
284
+
285
+ const runSweep = () => {
286
+ void guardianSweep()
287
+ .then(r => {
288
+ if (r.enabled && (r.refreshed.length || r.warmed.length || r.failed.length)) {
289
+ console.log(`🛡️ token-guardian: refreshed ${r.refreshed.length}, warmed ${r.warmed.length}, failed ${r.failed.length}`);
290
+ }
291
+ })
292
+ .catch(err => console.log(`token-guardian sweep error: ${err instanceof Error ? err.message : String(err)}`))
293
+ .finally(scheduleNext);
294
+ };
295
+
296
+ scheduleNext();
297
+ return {
298
+ stop() {
299
+ stopped = true;
300
+ if (timer) clearTimeout(timer);
301
+ },
302
+ };
303
+ }
@@ -0,0 +1,62 @@
1
+ /** Minimal OAuth types, ported from jawcode packages/ai/src/utils/oauth/types.ts. */
2
+ export type OAuthCredentialSource = "oauth" | "local-cli" | "credential-file" | "environment" | "manual";
3
+
4
+ /** Account-scoped Kiro data required for refresh and request routing. */
5
+ export interface KiroOAuthMetadata {
6
+ profileArn?: string;
7
+ ssoRegion?: string;
8
+ apiRegion?: string;
9
+ clientId?: string;
10
+ clientSecret?: string;
11
+ }
12
+
13
+ export type OAuthCredentials = {
14
+ refresh: string;
15
+ access: string;
16
+ /** Epoch ms after any small provider-specific early-refresh margin; the shared gate adds 1 minute. */
17
+ expires: number;
18
+ email?: string;
19
+ accountId?: string;
20
+ source?: OAuthCredentialSource;
21
+ /** Google Antigravity (Cloud Code Assist) discovered project id; injected into the CCA envelope. */
22
+ projectId?: string;
23
+ /**
24
+ * GitHub Copilot allowlisted API origin from token `endpoints.api` (HTTPS `*.githubcopilot.com` only).
25
+ * Never reuse for Antigravity projectId; validated on write and again at request time.
26
+ */
27
+ apiBaseUrl?: string;
28
+ /** Never returned by management APIs; persisted only inside the protected auth-store boundary. */
29
+ kiro?: KiroOAuthMetadata;
30
+ };
31
+
32
+ /** One logged-in account inside a provider's account set (multiauth). */
33
+ export interface ProviderAccount {
34
+ /** Stable short id, generated once at append time; never re-derived after rotation. */
35
+ id: string;
36
+ /** User-owned display label; never participates in auth identity or routing. */
37
+ alias?: string;
38
+ credential: OAuthCredentials;
39
+ /** Terminal refresh failure (invalid_grant / reused / revoked) — re-login required. */
40
+ needsReauth?: boolean;
41
+ addedAt?: number;
42
+ }
43
+
44
+ /** auth.json value per provider: N accounts + which one requests use. */
45
+ export interface ProviderAccountSet {
46
+ activeAccountId: string;
47
+ accounts: ProviderAccount[];
48
+ }
49
+
50
+ export interface OAuthController {
51
+ onAuth?(info: { url: string; instructions?: string; deviceCode?: string }): void;
52
+ onProgress?(message: string): void;
53
+ onManualCodeInput?(expectedState?: string): Promise<string>;
54
+ signal?: AbortSignal;
55
+ }
56
+
57
+ /**
58
+ * How a login flow may use a locally detected CLI token.
59
+ * "off" goes straight to the real OAuth flow, "fallback" imports a local token when present
60
+ * and falls back to OAuth otherwise, "only" imports without any OAuth fallback.
61
+ */
62
+ export type LocalTokenImportMode = "off" | "fallback" | "only";
@@ -0,0 +1,241 @@
1
+ /** xAI OAuth flow (Grok account login). Ported from jawcode oauth/xai.ts. */
2
+ import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
3
+ import { generatePKCE } from "./pkce";
4
+ import type { LocalTokenImportMode, OAuthController, OAuthCredentials } from "./types";
5
+
6
+ const XAI_OAUTH_ISSUER = "https://auth.x.ai";
7
+ export const XAI_OAUTH_DISCOVERY_URL = `${XAI_OAUTH_ISSUER}/.well-known/openid-configuration`;
8
+ export const XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
9
+ export const XAI_OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
10
+ const XAI_OAUTH_CALLBACK_PORT = 56121;
11
+ const XAI_OAUTH_CALLBACK_PATH = "/callback";
12
+ const XAI_OAUTH_REFRESH_SKEW_MS = 2 * 60 * 1000;
13
+ const TOKEN_REQUEST_TIMEOUT_MS = 30_000;
14
+
15
+ export const XAI_LOCAL_CLI_DETACH_WARNING =
16
+ "[oauth:xai] Grok CLI credential was stale; refreshed into OpenCodex ownership. Grok CLI may require login again.";
17
+
18
+ interface XaiDiscovery {
19
+ authorizationEndpoint: string;
20
+ tokenEndpoint: string;
21
+ }
22
+
23
+ interface XaiDiscoveryPayload {
24
+ authorization_endpoint?: unknown;
25
+ token_endpoint?: unknown;
26
+ }
27
+
28
+ export interface XaiTokenPayload {
29
+ access_token?: unknown;
30
+ refresh_token?: unknown;
31
+ expires_in?: unknown;
32
+ id_token?: unknown;
33
+ token_type?: unknown;
34
+ }
35
+
36
+ interface XaiJwtPayload {
37
+ sub?: unknown;
38
+ email?: unknown;
39
+ [key: string]: unknown;
40
+ }
41
+
42
+ function requestSignal(signal: AbortSignal | undefined): AbortSignal {
43
+ const timeoutSignal = AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS);
44
+ return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
45
+ }
46
+
47
+ function validateXaiEndpoint(rawUrl: string): string {
48
+ const parsed = new URL(rawUrl);
49
+ const host = parsed.hostname.toLowerCase();
50
+ if (parsed.protocol !== "https:" || (host !== "x.ai" && !host.endsWith(".x.ai"))) {
51
+ throw new Error(`xAI OAuth discovery returned an unexpected endpoint: ${rawUrl}`);
52
+ }
53
+ return parsed.toString();
54
+ }
55
+
56
+ export async function discoverXaiOAuthEndpoints(signal?: AbortSignal): Promise<XaiDiscovery> {
57
+ const response = await fetch(XAI_OAUTH_DISCOVERY_URL, {
58
+ headers: { Accept: "application/json" },
59
+ signal: requestSignal(signal),
60
+ });
61
+ if (!response.ok) {
62
+ throw new Error(`xAI OAuth discovery failed: ${response.status} ${await response.text()}`);
63
+ }
64
+
65
+ const payload = (await response.json()) as XaiDiscoveryPayload;
66
+ if (typeof payload.authorization_endpoint !== "string" || typeof payload.token_endpoint !== "string") {
67
+ throw new Error("xAI OAuth discovery response missing authorization/token endpoints");
68
+ }
69
+
70
+ return {
71
+ authorizationEndpoint: validateXaiEndpoint(payload.authorization_endpoint),
72
+ tokenEndpoint: validateXaiEndpoint(payload.token_endpoint),
73
+ };
74
+ }
75
+
76
+ function decodeJwtPayload(token: string): XaiJwtPayload | undefined {
77
+ const parts = token.split(".");
78
+ const payload = parts[1];
79
+ if (parts.length !== 3 || !payload) return undefined;
80
+ try {
81
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as XaiJwtPayload;
82
+ } catch {
83
+ return undefined;
84
+ }
85
+ }
86
+
87
+ function getTokenIdentity(accessToken: string, idToken: string | undefined): { accountId?: string; email?: string } {
88
+ const payload = (idToken ? decodeJwtPayload(idToken) : undefined) ?? decodeJwtPayload(accessToken);
89
+ const accountId = typeof payload?.sub === "string" && payload.sub.length > 0 ? payload.sub : undefined;
90
+ const email =
91
+ typeof payload?.email === "string" && payload.email.length > 0 ? payload.email.toLowerCase() : undefined;
92
+ return { accountId, email };
93
+ }
94
+
95
+ export class XaiTokenRequestError extends Error { constructor(public readonly status?:number,public readonly oauthError?:string,message="xAI token request failed",options?:{cause?:unknown}){super(message,options);this.name="XaiTokenRequestError";} }
96
+ export interface XaiTokenRetryDeps { sleep?:(ms:number)=>Promise<void>; random?:()=>number }
97
+ function isAbortError(error:unknown):boolean{return error instanceof DOMException&&error.name==="AbortError";}
98
+ function retryDelay(attempt:number,retryAfter:string|null,random:()=>number):number{const base=attempt===1?100:250,j=Math.round(base*(.75+random()*.5)),seconds=retryAfter!==null&&/^\d+$/.test(retryAfter)?Number(retryAfter):0;return Math.min(2000,Math.max(j,seconds*1000));}
99
+ async function readTokenError(response:Response):Promise<XaiTokenRequestError>{let oauthError:string|undefined,detail="";try{const body=await response.json() as {error?:unknown;error_description?:unknown};if(typeof body.error==="string")oauthError=body.error;if(typeof body.error_description==="string")detail=body.error_description;}catch{}const suffix=detail?`: ${detail}`:oauthError?`: ${oauthError}`:"";return new XaiTokenRequestError(response.status,oauthError,`xAI token request failed: ${response.status}${suffix}`);}
100
+ export async function postXaiToken(
101
+ tokenEndpoint: string,
102
+ body: Record<string, string>,
103
+ signal?: AbortSignal, deps:XaiTokenRetryDeps={},
104
+ ): Promise<XaiTokenPayload> {
105
+ const sleep=deps.sleep??(ms=>Bun.sleep(ms)),random=deps.random??Math.random;let last:unknown;
106
+ for(let attempt=1;attempt<=3;attempt++){let response:Response;try{response=await fetch(tokenEndpoint, {
107
+ method: "POST",
108
+ headers: {
109
+ Accept: "application/json",
110
+ "Content-Type": "application/x-www-form-urlencoded",
111
+ },
112
+ body: new URLSearchParams(body).toString(),
113
+ signal: requestSignal(signal),
114
+ });}catch(error){if(isAbortError(error)&&signal?.aborted)throw error;last=error;if(attempt===3)throw new XaiTokenRequestError(undefined,undefined,"xAI token request failed: network error",{cause:error});await sleep(retryDelay(attempt,null,random));continue;}if(response.ok)return await response.json() as XaiTokenPayload;const error=await readTokenError(response);last=error;if(!(response.status===429||response.status>=500)||attempt===3)throw error;await sleep(retryDelay(attempt,response.headers.get("retry-after"),random));}throw last;
115
+ }
116
+
117
+ function credentialsFromTokenPayload(payload: XaiTokenPayload, refreshFallback = ""): OAuthCredentials {
118
+ if (typeof payload.access_token !== "string" || payload.access_token.length === 0) {
119
+ throw new Error("xAI token response did not include an access token");
120
+ }
121
+ const refresh =
122
+ typeof payload.refresh_token === "string" && payload.refresh_token.length > 0
123
+ ? payload.refresh_token
124
+ : refreshFallback;
125
+ if (!refresh) {
126
+ throw new Error("xAI token response did not include a refresh token");
127
+ }
128
+ const expiresIn =
129
+ typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in) ? payload.expires_in : 3600;
130
+ const idToken = typeof payload.id_token === "string" ? payload.id_token : undefined;
131
+ const { accountId, email } = getTokenIdentity(payload.access_token, idToken);
132
+ return {
133
+ refresh,
134
+ access: payload.access_token,
135
+ expires: Date.now() + expiresIn * 1000 - XAI_OAUTH_REFRESH_SKEW_MS,
136
+ accountId,
137
+ email,
138
+ };
139
+ }
140
+
141
+ export class XaiOAuthFlow extends OAuthCallbackFlow {
142
+ #verifier = "";
143
+ #discovery: XaiDiscovery | undefined;
144
+
145
+ constructor(ctrl: OAuthController) {
146
+ super(ctrl, {
147
+ preferredPort: XAI_OAUTH_CALLBACK_PORT,
148
+ callbackPath: XAI_OAUTH_CALLBACK_PATH,
149
+ callbackHostname: "127.0.0.1",
150
+ callbackBindHostname: "127.0.0.1",
151
+ redirectUri: `http://127.0.0.1:${XAI_OAUTH_CALLBACK_PORT}${XAI_OAUTH_CALLBACK_PATH}`,
152
+ } satisfies OAuthCallbackFlowOptions);
153
+ }
154
+
155
+ async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
156
+ const pkce = await generatePKCE();
157
+ this.#verifier = pkce.verifier;
158
+ this.#discovery = await discoverXaiOAuthEndpoints(this.ctrl.signal);
159
+ const params = new URLSearchParams({
160
+ response_type: "code",
161
+ client_id: XAI_OAUTH_CLIENT_ID,
162
+ redirect_uri: redirectUri,
163
+ scope: XAI_OAUTH_SCOPE,
164
+ code_challenge: pkce.challenge,
165
+ code_challenge_method: "S256",
166
+ state,
167
+ nonce: crypto.randomUUID(),
168
+ });
169
+ return {
170
+ url: `${this.#discovery.authorizationEndpoint}?${params.toString()}`,
171
+ instructions:
172
+ "Complete xAI/Grok login in your browser. If the browser cannot reach this machine, paste the final redirect URL or authorization code when prompted.",
173
+ };
174
+ }
175
+
176
+ async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
177
+ if (!this.#verifier) {
178
+ throw new Error("xAI OAuth PKCE verifier was not initialized");
179
+ }
180
+ const discovery = this.#discovery ?? (await discoverXaiOAuthEndpoints(this.ctrl.signal));
181
+ const tokenPayload = await postXaiToken(
182
+ discovery.tokenEndpoint,
183
+ {
184
+ grant_type: "authorization_code",
185
+ client_id: XAI_OAUTH_CLIENT_ID,
186
+ code,
187
+ redirect_uri: redirectUri,
188
+ code_verifier: this.#verifier,
189
+ },
190
+ this.ctrl.signal,
191
+ );
192
+ return credentialsFromTokenPayload(tokenPayload);
193
+ }
194
+ }
195
+
196
+ export async function loginXai(
197
+ ctrl: OAuthController,
198
+ opts?: { importLocal?: LocalTokenImportMode },
199
+ ): Promise<OAuthCredentials> {
200
+ const importLocal = opts?.importLocal ?? "off";
201
+ if (importLocal !== "off") {
202
+ const { detectGrokCliToken } = await import("./local-token-detect");
203
+ const local = detectGrokCliToken();
204
+ if (local) {
205
+ ctrl.onProgress?.("Found Grok CLI token, importing automatically");
206
+ if (local.expires >= Date.now() + 60_000) return local;
207
+ try {
208
+ const fresh = await refreshXaiToken(local.refresh, ctrl.signal);
209
+ ctrl.onProgress?.(XAI_LOCAL_CLI_DETACH_WARNING);
210
+ return { ...fresh, source: "oauth" };
211
+ } catch (error) {
212
+ if (importLocal === "only") {
213
+ throw new Error(
214
+ `Grok CLI token is expired and could not be refreshed: ${error instanceof Error ? error.message : String(error)}`,
215
+ );
216
+ }
217
+ }
218
+ } else if (importLocal === "only") {
219
+ throw new Error("No Grok CLI token found at ~/.grok/auth.json. Run 'ocx login xai' for browser OAuth.");
220
+ }
221
+ }
222
+
223
+ return new XaiOAuthFlow(ctrl).login();
224
+ }
225
+
226
+ export async function refreshXaiToken(refreshToken: string, signal?: AbortSignal): Promise<OAuthCredentials> {
227
+ if (!refreshToken) {
228
+ throw new Error("xAI credentials are expired and do not include a refresh token");
229
+ }
230
+ const discovery = await discoverXaiOAuthEndpoints(signal);
231
+ const tokenPayload = await postXaiToken(
232
+ discovery.tokenEndpoint,
233
+ {
234
+ grant_type: "refresh_token",
235
+ client_id: XAI_OAUTH_CLIENT_ID,
236
+ refresh_token: refreshToken,
237
+ },
238
+ signal,
239
+ );
240
+ return credentialsFromTokenPayload(tokenPayload, refreshToken);
241
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Inventory of Pi local extensions (auto-discovered directory + settings paths).
3
+ *
4
+ * We list only; enabling/disabling package resources stays with `pi config` /
5
+ * package filters. Writing arbitrary extension source from the dashboard is out
6
+ * of scope (extensions are executable TypeScript).
7
+ */
8
+ import { existsSync, readdirSync, statSync } from "node:fs";
9
+ import { basename, join } from "node:path";
10
+ import { piExtensionsDir } from "./home";
11
+ import { readPiSettings } from "./settings";
12
+
13
+ export interface PiExtensionEntry {
14
+ /** Display name (filename or configured path basenamed). */
15
+ name: string;
16
+ path: string;
17
+ /** auto = ~/.pi/agent/extensions; settings = listed in settings.extensions */
18
+ origin: "auto" | "settings";
19
+ kind: "file" | "directory" | "missing";
20
+ }
21
+
22
+ export interface PiExtensionsStatus {
23
+ autoDir: string;
24
+ entries: PiExtensionEntry[];
25
+ }
26
+
27
+ function classify(path: string): "file" | "directory" | "missing" {
28
+ try {
29
+ if (!existsSync(path)) return "missing";
30
+ return statSync(path).isDirectory() ? "directory" : "file";
31
+ } catch {
32
+ return "missing";
33
+ }
34
+ }
35
+
36
+ export function readPiExtensions(opts: { piHome?: string } = {}): PiExtensionsStatus {
37
+ const autoDir = piExtensionsDir(opts.piHome);
38
+ const entries: PiExtensionEntry[] = [];
39
+ const seen = new Set<string>();
40
+
41
+ if (existsSync(autoDir)) {
42
+ try {
43
+ for (const name of readdirSync(autoDir).sort()) {
44
+ if (name.startsWith(".")) continue;
45
+ const path = join(autoDir, name);
46
+ const kind = classify(path);
47
+ if (kind === "missing") continue;
48
+ // Auto-load is for .ts/.js files and package directories.
49
+ if (kind === "file" && !/\.(ts|js|mjs|cjs)$/i.test(name)) continue;
50
+ seen.add(path);
51
+ entries.push({ name, path, origin: "auto", kind });
52
+ }
53
+ } catch {
54
+ // Unreadable dir → empty auto list.
55
+ }
56
+ }
57
+
58
+ const settings = readPiSettings(opts);
59
+ for (const configured of settings.settings.extensions ?? []) {
60
+ const path = configured;
61
+ if (seen.has(path)) continue;
62
+ seen.add(path);
63
+ entries.push({
64
+ name: basename(path),
65
+ path,
66
+ origin: "settings",
67
+ kind: classify(path),
68
+ });
69
+ }
70
+
71
+ return { autoDir, entries };
72
+ }