@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,211 @@
1
+ /**
2
+ * Cursor OAuth — PKCE poll flow. Standalone: talks directly to cursor.com / api2.cursor.sh,
3
+ * with no dependency on a local Cursor IDE/CLI install or on jawcode. Ported from jawcode
4
+ * packages/ai/src/utils/oauth/cursor.ts and adapted to opencodex's OAuthController (see kimi.ts).
5
+ *
6
+ * Security: the login URL carries only the PKCE challenge (SHA-256 of the verifier); the verifier
7
+ * is sent only to /auth/poll. Tokens and the verifier are never logged — thrown errors and progress
8
+ * messages are status/string only.
9
+ */
10
+ import { generatePKCE } from "./pkce";
11
+ import type { OAuthController, OAuthCredentials } from "./types";
12
+
13
+ const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
14
+ const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
15
+ const CURSOR_REFRESH_URL = "https://api2.cursor.sh/auth/exchange_user_api_key";
16
+
17
+ const POLL_MAX_ATTEMPTS = 150;
18
+ const POLL_BASE_DELAY_MS = 1000;
19
+ const POLL_MAX_DELAY_MS = 10_000;
20
+ const POLL_BACKOFF = 1.2;
21
+ const EXPIRY_SKEW_MS = 5 * 60 * 1000;
22
+ const FALLBACK_TTL_MS = 60 * 60 * 1000;
23
+
24
+ const REFRESH_TIMEOUT_MS = 15_000;
25
+ const REFRESH_ATTEMPTS = 3;
26
+ const REFRESH_RETRY_BASE_MS = 300;
27
+
28
+ export interface CursorAuthParams {
29
+ verifier: string;
30
+ challenge: string;
31
+ uuid: string;
32
+ loginUrl: string;
33
+ }
34
+
35
+ interface CursorJwtPayload {
36
+ sub?: unknown;
37
+ email?: unknown;
38
+ exp?: unknown;
39
+ }
40
+
41
+ function decodeCursorJwtPayload(token: string): CursorJwtPayload | undefined {
42
+ const parts = token.split(".");
43
+ const payload = parts[1];
44
+ if (parts.length !== 3 || !payload) return undefined;
45
+ try {
46
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf-8")) as CursorJwtPayload;
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ }
51
+
52
+ /** Build OAuthCredentials from Cursor tokens, extracting stable identity from JWT `sub` for multiauth. */
53
+ export function credentialsFromCursorTokens(accessToken: string, refreshToken: string): OAuthCredentials {
54
+ const payload = decodeCursorJwtPayload(accessToken) ?? decodeCursorJwtPayload(refreshToken);
55
+ const accountId = typeof payload?.sub === "string" && payload.sub.length > 0 ? payload.sub : undefined;
56
+ const email = typeof payload?.email === "string" && payload.email.length > 0 ? payload.email.toLowerCase() : undefined;
57
+ return {
58
+ access: accessToken,
59
+ refresh: refreshToken,
60
+ expires: getTokenExpiry(accessToken),
61
+ ...(accountId ? { accountId } : {}),
62
+ ...(email ? { email } : {}),
63
+ };
64
+ }
65
+
66
+ /** Generate PKCE params + the cursor.com deep-link login URL (challenge only — never the verifier). */
67
+ export async function generateCursorAuthParams(): Promise<CursorAuthParams> {
68
+ const { verifier, challenge } = await generatePKCE();
69
+ const uuid = crypto.randomUUID();
70
+ const params = new URLSearchParams({ challenge, uuid, mode: "login", redirectTarget: "cli" });
71
+ return { verifier, challenge, uuid, loginUrl: `${CURSOR_LOGIN_URL}?${params.toString()}` };
72
+ }
73
+
74
+ /** Abort-aware delay (mirrors kimi.ts) — rejects if the controller signal aborts. */
75
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
76
+ return new Promise((resolve, reject) => {
77
+ if (signal?.aborted) return reject(new Error("Cursor login cancelled"));
78
+ const timer = setTimeout(resolve, ms);
79
+ signal?.addEventListener(
80
+ "abort",
81
+ () => {
82
+ clearTimeout(timer);
83
+ reject(new Error("Cursor login cancelled"));
84
+ },
85
+ { once: true },
86
+ );
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Poll cursor.com for login completion. 404 = still pending (back off), 200 = tokens.
92
+ * `baseDelayMs` is injectable so tests can avoid the real 1s cadence; production uses the default.
93
+ */
94
+ export async function pollCursorAuth(
95
+ uuid: string,
96
+ verifier: string,
97
+ signal?: AbortSignal,
98
+ baseDelayMs: number = POLL_BASE_DELAY_MS,
99
+ ): Promise<{ accessToken: string; refreshToken: string }> {
100
+ let delay = baseDelayMs;
101
+ let consecutiveErrors = 0;
102
+
103
+ for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
104
+ await sleep(delay, signal);
105
+
106
+ try {
107
+ const url = `${CURSOR_POLL_URL}?uuid=${encodeURIComponent(uuid)}&verifier=${encodeURIComponent(verifier)}`;
108
+ const response = await fetch(url, { signal });
109
+
110
+ if (response.status === 404) {
111
+ consecutiveErrors = 0;
112
+ delay = Math.min(delay * POLL_BACKOFF, POLL_MAX_DELAY_MS);
113
+ continue;
114
+ }
115
+
116
+ if (response.ok) {
117
+ const data = (await response.json()) as { accessToken?: string; refreshToken?: string };
118
+ if (!data.accessToken || !data.refreshToken) {
119
+ throw new Error("Cursor auth response missing tokens");
120
+ }
121
+ return { accessToken: data.accessToken, refreshToken: data.refreshToken };
122
+ }
123
+
124
+ throw new Error(`Cursor auth poll failed: ${response.status}`);
125
+ } catch (err) {
126
+ if (signal?.aborted) throw err instanceof Error ? err : new Error("Cursor login cancelled");
127
+ consecutiveErrors++;
128
+ if (consecutiveErrors >= 3) {
129
+ throw new Error("Too many consecutive errors during Cursor auth polling");
130
+ }
131
+ delay = Math.min(delay * POLL_BACKOFF, POLL_MAX_DELAY_MS);
132
+ }
133
+ }
134
+
135
+ throw new Error("Cursor authentication polling timeout");
136
+ }
137
+
138
+ /** Run the standalone Cursor login: surface the URL via `onAuth`, then poll until approved. */
139
+ export async function loginCursor(
140
+ ctrl: OAuthController,
141
+ pollBaseDelayMs: number = POLL_BASE_DELAY_MS,
142
+ ): Promise<OAuthCredentials> {
143
+ const { verifier, uuid, loginUrl } = await generateCursorAuthParams();
144
+ ctrl.onAuth?.({ url: loginUrl, instructions: "Approve the Cursor login in your browser, then return here." });
145
+ ctrl.onProgress?.("Waiting for Cursor login approval…");
146
+ const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier, ctrl.signal, pollBaseDelayMs);
147
+ return credentialsFromCursorTokens(accessToken, refreshToken);
148
+ }
149
+
150
+ function isRetryableRefreshStatus(status: number): boolean {
151
+ return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
152
+ }
153
+
154
+ function refreshRetryDelayMs(attempt: number): number {
155
+ const exp = REFRESH_RETRY_BASE_MS * 2 ** attempt;
156
+ return Math.floor(exp * (0.8 + Math.random() * 0.4));
157
+ }
158
+
159
+ function refreshTimeoutSignal(parent: AbortSignal | undefined): AbortSignal {
160
+ const timeout = AbortSignal.timeout(REFRESH_TIMEOUT_MS);
161
+ return parent ? AbortSignal.any([parent, timeout]) : timeout;
162
+ }
163
+
164
+ /**
165
+ * Exchange a refresh token for fresh credentials. Keeps the old refresh if the server omits one.
166
+ *
167
+ * Hardened with a per-attempt timeout and bounded retry on transient failures (network errors and
168
+ * 429/5xx). Non-retryable statuses (e.g. 401/403 from an expired refresh token) fail fast so the
169
+ * caller can surface a re-auth prompt. Errors never include the token value.
170
+ */
171
+ export async function refreshCursorToken(refresh: string, signal?: AbortSignal): Promise<OAuthCredentials> {
172
+ let lastError: unknown;
173
+ for (let attempt = 0; attempt < REFRESH_ATTEMPTS; attempt++) {
174
+ if (signal?.aborted) throw signal.reason ?? new Error("Cursor token refresh aborted");
175
+ let response: Response;
176
+ try {
177
+ response = await fetch(CURSOR_REFRESH_URL, {
178
+ method: "POST",
179
+ headers: { Authorization: `Bearer ${refresh}`, "Content-Type": "application/json" },
180
+ body: "{}",
181
+ signal: refreshTimeoutSignal(signal),
182
+ });
183
+ } catch (err) {
184
+ // Network/timeout error: retry unless the caller aborted or we are out of attempts.
185
+ if (signal?.aborted) throw err;
186
+ lastError = err;
187
+ if (attempt === REFRESH_ATTEMPTS - 1) break;
188
+ await new Promise(resolve => setTimeout(resolve, refreshRetryDelayMs(attempt)));
189
+ continue;
190
+ }
191
+ if (response.ok) {
192
+ const data = (await response.json()) as { accessToken?: string; refreshToken?: string };
193
+ if (!data.accessToken) throw new Error("Cursor refresh response missing access token");
194
+ return credentialsFromCursorTokens(data.accessToken, data.refreshToken || refresh);
195
+ }
196
+ if (!isRetryableRefreshStatus(response.status) || attempt === REFRESH_ATTEMPTS - 1) {
197
+ throw new Error(`Cursor token refresh failed: ${response.status}`);
198
+ }
199
+ lastError = new Error(`Cursor token refresh failed: ${response.status}`);
200
+ await response.body?.cancel().catch(() => {});
201
+ await new Promise(resolve => setTimeout(resolve, refreshRetryDelayMs(attempt)));
202
+ }
203
+ throw lastError instanceof Error ? lastError : new Error("Cursor token refresh failed");
204
+ }
205
+
206
+ /** Resolve a token's expiry (epoch ms) from its JWT `exp`, minus a 5-minute skew; ~1h fallback. */
207
+ export function getTokenExpiry(token: string): number {
208
+ const decoded = decodeCursorJwtPayload(token);
209
+ if (typeof decoded?.exp === "number") return decoded.exp * 1000 - EXPIRY_SKEW_MS;
210
+ return Date.now() + FALLBACK_TTL_MS;
211
+ }
@@ -0,0 +1,428 @@
1
+ /**
2
+ * GitHub Copilot OAuth (device authorization grant + copilot_internal token exchange).
3
+ *
4
+ * Uses the public VS Code GitHub OAuth app client id (community-proven for Copilot Pro).
5
+ * This is an unofficial bridge — GitHub may tighten or revoke access. See registry note.
6
+ */
7
+ import type { OAuthController, OAuthCredentials } from "./types";
8
+
9
+ /** VS Code's public GitHub OAuth app — required for copilot_internal/v2/token to succeed. */
10
+ export const GITHUB_COPILOT_OAUTH_CLIENT_ID = "Iv1.b507a08c87ecfe98";
11
+ export const GITHUB_COPILOT_DEFAULT_API_BASE = "https://api.githubcopilot.com";
12
+ export const GITHUB_DEVICE_VERIFY_ORIGIN = "https://github.com";
13
+ export const GITHUB_DEVICE_VERIFY_PATH = "/login/device";
14
+
15
+ const DEVICE_CODE_URL = "https://github.com/login/device/code";
16
+ const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
17
+ const COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
18
+ const GITHUB_USER_URL = "https://api.github.com/user";
19
+
20
+ const OAUTH_SCOPE = "read:user";
21
+ const DEFAULT_POLL_INTERVAL_MS = 5000;
22
+ const DEFAULT_DEVICE_FLOW_TTL_MS = 15 * 60 * 1000;
23
+ const OAUTH_EXPIRY_SKEW_MS = 2 * 60 * 1000;
24
+ const MIN_POLL_MS = 1000;
25
+ /** OAuth error codes that make a refresh terminally dead (safe to surface verbatim). */
26
+ const TERMINAL_OAUTH_ERROR_CODES = new Set(["invalid_grant", "access_denied", "expired_token"]);
27
+ const IDENTITY_RETRY_DELAY_MS = 500;
28
+
29
+ /** Honest OpenCodex client fingerprint; VS Code-shaped values only if API requires them later. */
30
+ export const GITHUB_COPILOT_EDITOR_HEADERS: Readonly<Record<string, string>> = {
31
+ "Editor-Version": "opencodex/0.1.0",
32
+ "Editor-Plugin-Version": "opencodex/0.1.0",
33
+ "Copilot-Integration-Id": "vscode-chat",
34
+ "User-Agent": "opencodex",
35
+ Accept: "application/json",
36
+ };
37
+
38
+ interface DeviceAuthorizationResponse {
39
+ user_code?: string;
40
+ device_code?: string;
41
+ verification_uri?: string;
42
+ verification_uri_complete?: string;
43
+ expires_in?: number;
44
+ interval?: number;
45
+ error?: string;
46
+ error_description?: string;
47
+ }
48
+
49
+ interface GithubTokenResponse {
50
+ access_token?: string;
51
+ refresh_token?: string;
52
+ expires_in?: number;
53
+ refresh_token_expires_in?: number;
54
+ error?: string;
55
+ error_description?: string;
56
+ interval?: number;
57
+ }
58
+
59
+ interface CopilotTokenResponse {
60
+ token?: string;
61
+ expires_at?: number;
62
+ refresh_in?: number;
63
+ endpoints?: { api?: string };
64
+ }
65
+
66
+ interface GithubUserResponse {
67
+ login?: string;
68
+ id?: number;
69
+ email?: string;
70
+ }
71
+
72
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
73
+ return new Promise((resolve, reject) => {
74
+ if (signal?.aborted) return reject(new Error("Login cancelled"));
75
+ const t = setTimeout(resolve, ms);
76
+ signal?.addEventListener("abort", () => {
77
+ clearTimeout(t);
78
+ reject(new Error("Login cancelled"));
79
+ }, { once: true });
80
+ });
81
+ }
82
+
83
+ /** Status-only errors — never echo response bodies (may contain tokens). */
84
+ export function githubCopilotHttpError(action: string, status: number): Error {
85
+ return new Error(`GitHub Copilot ${action} failed (${status})`);
86
+ }
87
+
88
+ export function buildGithubDeviceVerifyUrl(userCode: string): string {
89
+ const code = userCode.trim();
90
+ if (!code || !/^[A-Z0-9-]+$/i.test(code)) {
91
+ throw new Error("GitHub Copilot device flow returned an invalid user code");
92
+ }
93
+ return `${GITHUB_DEVICE_VERIFY_ORIGIN}${GITHUB_DEVICE_VERIFY_PATH}?user_code=${encodeURIComponent(code)}`;
94
+ }
95
+
96
+ /**
97
+ * Allowlist for browser-open URLs. Prefer {@link buildGithubDeviceVerifyUrl}; this rejects
98
+ * phishing redirects if a caller still passes a server-supplied verification URI.
99
+ */
100
+ export function isAllowedGithubDeviceVerifyUrl(url: string): boolean {
101
+ try {
102
+ const parsed = new URL(url);
103
+ if (parsed.protocol !== "https:") return false;
104
+ if (parsed.username || parsed.password) return false;
105
+ if (parsed.hostname.toLowerCase() !== "github.com") return false;
106
+ if (parsed.port && parsed.port !== "443") return false;
107
+ const path = parsed.pathname.replace(/\/+$/, "") || "/";
108
+ return path === GITHUB_DEVICE_VERIFY_PATH;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Tight allowlist for Copilot API hosts from token `endpoints.api`.
116
+ * Rejects IPs, localhost, non-HTTPS, userinfo, and non-default ports.
117
+ */
118
+ export function validateCopilotApiBaseUrl(raw: string | undefined | null): string | undefined {
119
+ if (raw === undefined || raw === null) return undefined;
120
+ const trimmed = String(raw).trim();
121
+ if (!trimmed) return undefined;
122
+ let parsed: URL;
123
+ try {
124
+ parsed = new URL(trimmed);
125
+ } catch {
126
+ return undefined;
127
+ }
128
+ if (parsed.protocol !== "https:") return undefined;
129
+ if (parsed.username || parsed.password) return undefined;
130
+ if (parsed.port && parsed.port !== "443") return undefined;
131
+ const host = parsed.hostname.toLowerCase();
132
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".localhost")) {
133
+ return undefined;
134
+ }
135
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":")) return undefined;
136
+ if (host !== "api.githubcopilot.com" && !host.endsWith(".githubcopilot.com")) return undefined;
137
+ // Normalize to origin only (no path/query from the network).
138
+ return `https://${host}`;
139
+ }
140
+
141
+ export function resolveCopilotApiBaseUrl(raw: string | undefined | null): string {
142
+ return validateCopilotApiBaseUrl(raw) ?? GITHUB_COPILOT_DEFAULT_API_BASE;
143
+ }
144
+
145
+ async function requestDeviceAuthorization(signal?: AbortSignal): Promise<{
146
+ userCode: string;
147
+ deviceCode: string;
148
+ verifyUrl: string;
149
+ expiresInMs: number;
150
+ intervalMs: number;
151
+ }> {
152
+ const response = await fetch(DEVICE_CODE_URL, {
153
+ method: "POST",
154
+ headers: {
155
+ Accept: "application/json",
156
+ "Content-Type": "application/x-www-form-urlencoded",
157
+ "User-Agent": "opencodex",
158
+ },
159
+ body: new URLSearchParams({
160
+ client_id: GITHUB_COPILOT_OAUTH_CLIENT_ID,
161
+ scope: OAUTH_SCOPE,
162
+ }),
163
+ signal,
164
+ });
165
+ if (!response.ok) throw githubCopilotHttpError("device authorization", response.status);
166
+ const payload = (await response.json()) as DeviceAuthorizationResponse;
167
+ if (!payload.user_code || !payload.device_code) {
168
+ throw new Error("GitHub Copilot device authorization response missing required fields");
169
+ }
170
+ // Construct verify URL ourselves — never trust verification_uri_complete for openUrl.
171
+ const verifyUrl = buildGithubDeviceVerifyUrl(payload.user_code);
172
+ return {
173
+ userCode: payload.user_code,
174
+ deviceCode: payload.device_code,
175
+ verifyUrl,
176
+ expiresInMs: typeof payload.expires_in === "number" ? payload.expires_in * 1000 : DEFAULT_DEVICE_FLOW_TTL_MS,
177
+ intervalMs: typeof payload.interval === "number" && payload.interval > 0
178
+ ? payload.interval * 1000
179
+ : DEFAULT_POLL_INTERVAL_MS,
180
+ };
181
+ }
182
+
183
+ async function pollGithubDeviceToken(
184
+ deviceCode: string,
185
+ intervalMs: number,
186
+ expiresInMs: number,
187
+ signal?: AbortSignal,
188
+ ): Promise<{ access: string; refresh?: string }> {
189
+ const deadline = Date.now() + expiresInMs;
190
+ let waitMs = Math.max(MIN_POLL_MS, intervalMs);
191
+ while (Date.now() < deadline) {
192
+ if (signal?.aborted) throw new Error("Login cancelled");
193
+ // RFC 8628: wait the interval BEFORE every poll — an immediate first request is a
194
+ // cadence violation GitHub may answer with slow_down.
195
+ await sleep(waitMs, signal);
196
+ if (Date.now() >= deadline) break;
197
+ const response = await fetch(ACCESS_TOKEN_URL, {
198
+ method: "POST",
199
+ headers: {
200
+ Accept: "application/json",
201
+ "Content-Type": "application/x-www-form-urlencoded",
202
+ "User-Agent": "opencodex",
203
+ },
204
+ body: new URLSearchParams({
205
+ client_id: GITHUB_COPILOT_OAUTH_CLIENT_ID,
206
+ device_code: deviceCode,
207
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
208
+ }),
209
+ // Bound each poll by the remaining lifetime so a hung fetch cannot outlive the flow.
210
+ signal: signal
211
+ ? AbortSignal.any([signal, AbortSignal.timeout(Math.max(MIN_POLL_MS, deadline - Date.now()))])
212
+ : AbortSignal.timeout(Math.max(MIN_POLL_MS, deadline - Date.now())),
213
+ });
214
+ const payload = (await response.json().catch(() => ({}))) as GithubTokenResponse;
215
+ if (response.ok && payload.access_token) {
216
+ // Classic OAuth apps issue a non-expiring `gho_` access token with NO refresh token;
217
+ // expiring-token apps add `ghr_`. Both are valid device-flow successes.
218
+ return {
219
+ access: payload.access_token,
220
+ ...(payload.refresh_token ? { refresh: payload.refresh_token } : {}),
221
+ };
222
+ }
223
+ const error = payload.error;
224
+ if (error === "authorization_pending") {
225
+ continue;
226
+ }
227
+ if (error === "slow_down") {
228
+ // RFC 8628 §3.5: slow_down means increase the interval by 5 seconds; the
229
+ // server-provided `interval` wins when it demands even more.
230
+ const retryAfter = typeof payload.interval === "number" && payload.interval > 0
231
+ ? payload.interval * 1000
232
+ : 0;
233
+ waitMs = Math.max(waitMs + 5000, retryAfter);
234
+ continue;
235
+ }
236
+ if (error === "expired_token") throw new Error("GitHub Copilot device authorization expired");
237
+ if (error === "access_denied") throw new Error("GitHub Copilot device authorization denied");
238
+ if (error === "unsupported_grant_type" || error === "incorrect_device_code") {
239
+ throw new Error(`GitHub Copilot device flow failed (${error})`);
240
+ }
241
+ if (!response.ok) throw githubCopilotHttpError("device token poll", response.status);
242
+ throw new Error(`GitHub Copilot device flow failed (${error ?? "unknown"})`);
243
+ }
244
+ throw new Error("GitHub Copilot device flow timed out");
245
+ }
246
+
247
+ async function refreshGithubAccessToken(refreshToken: string, signal?: AbortSignal): Promise<{ access: string; refresh: string }> {
248
+ const response = await fetch(ACCESS_TOKEN_URL, {
249
+ method: "POST",
250
+ headers: {
251
+ Accept: "application/json",
252
+ "Content-Type": "application/x-www-form-urlencoded",
253
+ "User-Agent": "opencodex",
254
+ },
255
+ body: new URLSearchParams({
256
+ client_id: GITHUB_COPILOT_OAUTH_CLIENT_ID,
257
+ grant_type: "refresh_token",
258
+ refresh_token: refreshToken,
259
+ }),
260
+ signal,
261
+ });
262
+ const payload = (await response.json().catch(() => ({}))) as GithubTokenResponse;
263
+ if (!response.ok) {
264
+ // Extract ONLY the allowlisted OAuth error code — never the body or error_description,
265
+ // which can echo credential material. The code lets the shared terminal-refresh
266
+ // detector mark the account needsReauth instead of retrying a revoked grant forever.
267
+ const code = payload.error && TERMINAL_OAUTH_ERROR_CODES.has(payload.error) ? payload.error : undefined;
268
+ throw new Error(
269
+ code
270
+ ? `GitHub Copilot token refresh failed: ${code} (HTTP ${response.status})`
271
+ : `GitHub Copilot token refresh failed (${response.status})`,
272
+ );
273
+ }
274
+ if (!payload.access_token) throw new Error("GitHub Copilot token refresh missing access token");
275
+ return {
276
+ access: payload.access_token,
277
+ refresh: payload.refresh_token ?? refreshToken,
278
+ };
279
+ }
280
+
281
+ async function exchangeCopilotToken(githubAccessToken: string, signal?: AbortSignal): Promise<{
282
+ access: string;
283
+ expires: number;
284
+ apiBaseUrl: string;
285
+ }> {
286
+ const response = await fetch(COPILOT_TOKEN_URL, {
287
+ method: "GET",
288
+ headers: {
289
+ ...GITHUB_COPILOT_EDITOR_HEADERS,
290
+ Authorization: `token ${githubAccessToken}`,
291
+ },
292
+ signal,
293
+ });
294
+ if (!response.ok) throw githubCopilotHttpError("token exchange", response.status);
295
+ const payload = (await response.json().catch(() => ({}))) as CopilotTokenResponse;
296
+ if (!payload.token || typeof payload.token !== "string") {
297
+ throw new Error("GitHub Copilot token exchange missing token");
298
+ }
299
+ let expires: number;
300
+ if (typeof payload.expires_at === "number" && Number.isFinite(payload.expires_at)) {
301
+ // expires_at is unix seconds from GitHub.
302
+ expires = payload.expires_at * 1000 - OAUTH_EXPIRY_SKEW_MS;
303
+ } else if (typeof payload.refresh_in === "number" && payload.refresh_in > 0) {
304
+ expires = Date.now() + payload.refresh_in * 1000 - OAUTH_EXPIRY_SKEW_MS;
305
+ } else {
306
+ expires = Date.now() + 25 * 60 * 1000 - OAUTH_EXPIRY_SKEW_MS;
307
+ }
308
+ return {
309
+ access: payload.token,
310
+ expires,
311
+ apiBaseUrl: resolveCopilotApiBaseUrl(payload.endpoints?.api),
312
+ };
313
+ }
314
+
315
+ async function fetchGithubIdentityOnce(githubAccessToken: string, signal?: AbortSignal): Promise<{
316
+ email?: string;
317
+ accountId?: string;
318
+ }> {
319
+ const response = await fetch(GITHUB_USER_URL, {
320
+ headers: {
321
+ Accept: "application/vnd.github+json",
322
+ Authorization: `Bearer ${githubAccessToken}`,
323
+ "User-Agent": "opencodex",
324
+ "X-GitHub-Api-Version": "2022-11-28",
325
+ },
326
+ signal,
327
+ });
328
+ if (!response.ok) throw githubCopilotHttpError("identity lookup", response.status);
329
+ const user = (await response.json()) as GithubUserResponse;
330
+ // Prefer numeric id for multiauth stability. Only persist email when GitHub returns one —
331
+ // do not fabricate noreply addresses (privacy-scan + PII hygiene).
332
+ const accountId = typeof user.id === "number"
333
+ ? String(user.id)
334
+ : (typeof user.login === "string" && user.login ? user.login : undefined);
335
+ const email = typeof user.email === "string" && user.email.includes("@") ? user.email : undefined;
336
+ return {
337
+ ...(email ? { email } : {}),
338
+ ...(accountId ? { accountId } : {}),
339
+ };
340
+ }
341
+
342
+ /**
343
+ * Identity is REQUIRED for multi-account safety: an identity-less credential would
344
+ * replace the active slot in the auth store and clobber another GitHub account. One
345
+ * retry covers transient /user failures; a persistent failure fails the login rather
346
+ * than persisting an anonymous credential.
347
+ */
348
+ async function fetchGithubIdentity(githubAccessToken: string, signal?: AbortSignal): Promise<{
349
+ email?: string;
350
+ accountId?: string;
351
+ }> {
352
+ try {
353
+ const identity = await fetchGithubIdentityOnce(githubAccessToken, signal);
354
+ if (identity.accountId) return identity;
355
+ } catch { /* retry once below */ }
356
+ await sleep(IDENTITY_RETRY_DELAY_MS, signal);
357
+ const identity = await fetchGithubIdentityOnce(githubAccessToken, signal);
358
+ if (!identity.accountId) {
359
+ throw new Error("Could not verify GitHub account identity — retry the login");
360
+ }
361
+ return identity;
362
+ }
363
+
364
+ /**
365
+ * The credential `refresh` field carries the DURABLE GitHub grant:
366
+ * - `ghr_…` refresh token (expiring-token apps) → renewed via the refresh grant;
367
+ * - `gho_…` access token (classic apps, no refresh token) → re-exchanged directly.
368
+ * The `access` field always holds the short-lived Copilot API token.
369
+ */
370
+ async function credentialsFromGithubAccess(
371
+ githubAccess: string,
372
+ durableGrant: string,
373
+ signal?: AbortSignal,
374
+ ): Promise<OAuthCredentials> {
375
+ const [copilot, identity] = await Promise.all([
376
+ exchangeCopilotToken(githubAccess, signal),
377
+ fetchGithubIdentity(githubAccess, signal),
378
+ ]);
379
+ return {
380
+ access: copilot.access,
381
+ refresh: durableGrant,
382
+ expires: copilot.expires,
383
+ apiBaseUrl: copilot.apiBaseUrl,
384
+ source: "oauth",
385
+ ...(identity.email ? { email: identity.email } : {}),
386
+ ...(identity.accountId ? { accountId: identity.accountId } : {}),
387
+ };
388
+ }
389
+
390
+ export async function loginGithubCopilot(ctrl: OAuthController): Promise<OAuthCredentials> {
391
+ const device = await requestDeviceAuthorization(ctrl.signal);
392
+ if (!isAllowedGithubDeviceVerifyUrl(device.verifyUrl)) {
393
+ throw new Error("GitHub Copilot refused to open a non-allowlisted verification URL");
394
+ }
395
+ ctrl.onAuth?.({
396
+ url: device.verifyUrl,
397
+ instructions: `Enter code: ${device.userCode}`,
398
+ deviceCode: device.userCode,
399
+ });
400
+ ctrl.onProgress?.("Waiting for GitHub device authorization…");
401
+ const github = await pollGithubDeviceToken(
402
+ device.deviceCode,
403
+ device.intervalMs,
404
+ device.expiresInMs,
405
+ ctrl.signal,
406
+ );
407
+ ctrl.onProgress?.("Exchanging GitHub token for Copilot access…");
408
+ // Access-only responses (classic gho_ tokens) store the access token itself as the
409
+ // durable grant; expiring-token apps store the ghr_ refresh token.
410
+ return credentialsFromGithubAccess(github.access, github.refresh ?? github.access, ctrl.signal);
411
+ }
412
+
413
+ /**
414
+ * Refresh = renew the durable GitHub grant, then Copilot re-exchange.
415
+ * `refreshToken` is credentials.refresh: a `ghr_` token runs the GitHub refresh grant;
416
+ * anything else (a durable `gho_` access token) re-exchanges directly — both shapes
417
+ * recover expiry AND upstream 401s.
418
+ */
419
+ export async function refreshGithubCopilotToken(
420
+ refreshToken: string,
421
+ signal?: AbortSignal,
422
+ ): Promise<OAuthCredentials> {
423
+ if (refreshToken.startsWith("ghr_")) {
424
+ const github = await refreshGithubAccessToken(refreshToken, signal);
425
+ return credentialsFromGithubAccess(github.access, github.refresh, signal);
426
+ }
427
+ return credentialsFromGithubAccess(refreshToken, refreshToken, signal);
428
+ }