mixdog 0.9.3 → 0.9.4

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 (329) hide show
  1. package/package.json +7 -3
  2. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  3. package/scripts/bench/lead-review-tasks.json +20 -0
  4. package/scripts/bench/r4-mixed-tasks.json +20 -0
  5. package/scripts/bench/review-tasks.json +20 -0
  6. package/scripts/bench/round-codex.json +114 -0
  7. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  8. package/scripts/bench/round-mixdog-lead.json +269 -0
  9. package/scripts/bench/round-mixdog.json +126 -0
  10. package/scripts/bench/round-r10-bigsample.json +679 -0
  11. package/scripts/bench/round-r11-codexalign.json +257 -0
  12. package/scripts/bench/round-r4-codex.json +114 -0
  13. package/scripts/bench/round-r4-mixed.json +225 -0
  14. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  15. package/scripts/bench/round-r6-codex.json +114 -0
  16. package/scripts/bench/round-r6-solo.json +257 -0
  17. package/scripts/bench/round-r7-full.json +254 -0
  18. package/scripts/bench/round-r8-fulldefault.json +255 -0
  19. package/scripts/bench-run.mjs +215 -29
  20. package/scripts/freevar-smoke.mjs +95 -0
  21. package/scripts/internal-comms-bench.mjs +1 -0
  22. package/scripts/internal-comms-smoke.mjs +10 -9
  23. package/scripts/mouse-probe.mjs +45 -0
  24. package/scripts/output-style-bench.mjs +13 -6
  25. package/scripts/output-style-smoke.mjs +4 -4
  26. package/scripts/provider-toolcall-test.mjs +7 -3
  27. package/scripts/recall-usecase-cases.json +18 -0
  28. package/scripts/recall-usecase-probe.json +6 -0
  29. package/scripts/session-bench.mjs +152 -6
  30. package/scripts/tool-smoke.mjs +23 -63
  31. package/scripts/tui-render-smoke.mjs +90 -0
  32. package/scripts/webhook-smoke.mjs +208 -0
  33. package/src/agents/debugger/AGENT.md +4 -1
  34. package/src/agents/heavy-worker/AGENT.md +6 -5
  35. package/src/agents/maintainer/AGENT.md +4 -0
  36. package/src/agents/reviewer/AGENT.md +2 -1
  37. package/src/agents/worker/AGENT.md +8 -4
  38. package/src/lib/rules-builder.cjs +4 -0
  39. package/src/mixdog-session-runtime.mjs +632 -2042
  40. package/src/output-styles/default.md +34 -9
  41. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  42. package/src/output-styles/minimal.md +4 -1
  43. package/src/output-styles/simple.md +22 -7
  44. package/src/rules/agent/00-common.md +2 -0
  45. package/src/rules/lead/lead-brief.md +12 -0
  46. package/src/rules/lead/lead-tool.md +0 -11
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  49. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  50. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  51. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  52. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  53. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  54. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  55. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  56. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  57. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  58. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  59. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  60. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  62. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  63. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  65. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  66. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  67. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  69. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  70. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
  76. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
  77. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  80. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  82. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  83. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  84. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  85. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  87. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  88. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  89. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  90. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  91. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  93. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  94. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  95. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  97. package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
  98. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  99. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  100. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  101. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  102. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  103. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  104. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  105. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  106. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  109. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
  110. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  118. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  119. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  120. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  124. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  125. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  126. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  128. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  130. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  131. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  132. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  133. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  134. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  136. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  137. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  138. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  139. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  140. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  141. package/src/runtime/channels/backends/discord.mjs +12 -292
  142. package/src/runtime/channels/index.mjs +229 -663
  143. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  144. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  145. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  146. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  147. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  148. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  149. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  150. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  151. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  152. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  153. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  155. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  156. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  157. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  158. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  159. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  160. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  161. package/src/runtime/channels/lib/webhook.mjs +36 -570
  162. package/src/runtime/channels/tool-defs.mjs +11 -130
  163. package/src/runtime/memory/index.mjs +201 -1948
  164. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  165. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  166. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  167. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  168. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  169. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  170. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  171. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  172. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  173. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  174. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  175. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  177. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  178. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  179. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  180. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  181. package/src/runtime/memory/tool-defs.mjs +5 -13
  182. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  183. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  184. package/src/runtime/search/lib/web-tools.mjs +24 -602
  185. package/src/runtime/shared/atomic-file.mjs +26 -1
  186. package/src/runtime/shared/launcher-control.mjs +2 -2
  187. package/src/runtime/shared/tool-primitives.mjs +308 -0
  188. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  189. package/src/runtime/shared/tool-surface.mjs +80 -898
  190. package/src/runtime/shared/transcript-writer.mjs +23 -0
  191. package/src/runtime/shared/update-checker.mjs +7 -4
  192. package/src/session-runtime/config-helpers.mjs +84 -2
  193. package/src/session-runtime/config-lifecycle.mjs +232 -0
  194. package/src/session-runtime/cwd-plugins.mjs +226 -0
  195. package/src/session-runtime/mcp-glue.mjs +177 -0
  196. package/src/session-runtime/model-recency.mjs +111 -0
  197. package/src/session-runtime/native-search.mjs +247 -0
  198. package/src/session-runtime/output-styles.mjs +11 -9
  199. package/src/session-runtime/prewarm.mjs +142 -0
  200. package/src/session-runtime/provider-models.mjs +278 -0
  201. package/src/session-runtime/provider-usage.mjs +120 -0
  202. package/src/session-runtime/quick-model-rows.mjs +170 -0
  203. package/src/session-runtime/quick-search-models.mjs +46 -0
  204. package/src/session-runtime/session-hooks.mjs +93 -0
  205. package/src/session-runtime/settings-api.mjs +319 -0
  206. package/src/session-runtime/tool-catalog.mjs +29 -29
  207. package/src/session-runtime/tool-defs.mjs +84 -0
  208. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  209. package/src/standalone/agent-tool/helpers.mjs +237 -0
  210. package/src/standalone/agent-tool/notify.mjs +107 -0
  211. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  212. package/src/standalone/agent-tool/render.mjs +152 -0
  213. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  214. package/src/standalone/agent-tool.mjs +110 -671
  215. package/src/standalone/channel-worker.mjs +4 -7
  216. package/src/standalone/explore-tool.mjs +30 -9
  217. package/src/standalone/hook-bus/config.mjs +207 -0
  218. package/src/standalone/hook-bus/constants.mjs +90 -0
  219. package/src/standalone/hook-bus/handlers.mjs +481 -0
  220. package/src/standalone/hook-bus/payload.mjs +31 -0
  221. package/src/standalone/hook-bus/rules.mjs +77 -0
  222. package/src/standalone/hook-bus.mjs +77 -870
  223. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  224. package/src/standalone/opencode-go-login.mjs +5 -1
  225. package/src/standalone/provider-admin.mjs +1 -16
  226. package/src/standalone/usage-dashboard.mjs +3 -1
  227. package/src/tui/App.jsx +945 -8094
  228. package/src/tui/app/app-format.mjs +206 -0
  229. package/src/tui/app/channel-pickers.mjs +510 -0
  230. package/src/tui/app/clipboard.mjs +67 -0
  231. package/src/tui/app/core-memory-picker.mjs +210 -0
  232. package/src/tui/app/extension-pickers.mjs +506 -0
  233. package/src/tui/app/input-parsers.mjs +193 -0
  234. package/src/tui/app/maintenance-pickers.mjs +324 -0
  235. package/src/tui/app/model-options.mjs +330 -0
  236. package/src/tui/app/model-picker.mjs +365 -0
  237. package/src/tui/app/onboarding-steps.mjs +400 -0
  238. package/src/tui/app/project-picker.mjs +247 -0
  239. package/src/tui/app/provider-setup-picker.mjs +580 -0
  240. package/src/tui/app/resume-picker.mjs +55 -0
  241. package/src/tui/app/route-pickers.mjs +419 -0
  242. package/src/tui/app/settings-picker.mjs +490 -0
  243. package/src/tui/app/slash-commands.mjs +101 -0
  244. package/src/tui/app/slash-dispatch.mjs +427 -0
  245. package/src/tui/app/text-layout.mjs +46 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  247. package/src/tui/app/transcript-window.mjs +671 -0
  248. package/src/tui/app/use-mouse-input.mjs +460 -0
  249. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  250. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  251. package/src/tui/app/use-transcript-window.mjs +589 -0
  252. package/src/tui/components/ConfirmBar.jsx +1 -1
  253. package/src/tui/components/Picker.jsx +32 -4
  254. package/src/tui/components/PromptInput.jsx +23 -101
  255. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  256. package/src/tui/components/StatusLine.jsx +63 -12
  257. package/src/tui/components/TextEntryPanel.jsx +11 -0
  258. package/src/tui/components/ToolExecution.jsx +52 -594
  259. package/src/tui/components/TranscriptItem.jsx +105 -0
  260. package/src/tui/components/UsagePanel.jsx +18 -4
  261. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  262. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  263. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  264. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  265. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  266. package/src/tui/display-width.mjs +20 -3
  267. package/src/tui/dist/index.mjs +19652 -18630
  268. package/src/tui/engine/agent-job-feed.mjs +133 -0
  269. package/src/tui/engine/notification-plan.mjs +76 -0
  270. package/src/tui/engine/render-timing.mjs +17 -0
  271. package/src/tui/engine/tool-approval.mjs +94 -0
  272. package/src/tui/engine/tool-card-results.mjs +234 -0
  273. package/src/tui/engine/tool-result-status.mjs +135 -0
  274. package/src/tui/engine.mjs +122 -562
  275. package/src/tui/figures.mjs +5 -0
  276. package/src/tui/index.jsx +105 -0
  277. package/src/tui/input-editing.mjs +2 -2
  278. package/src/tui/markdown/format-token.mjs +4 -1
  279. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  280. package/src/tui/theme.mjs +6 -0
  281. package/src/ui/statusline-agents.mjs +213 -0
  282. package/src/ui/statusline-format.mjs +146 -0
  283. package/src/ui/statusline-segments.mjs +148 -0
  284. package/src/ui/statusline.mjs +67 -501
  285. package/src/ui/tool-card.mjs +0 -1
  286. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  287. package/src/workflows/default/WORKFLOW.md +1 -1
  288. package/src/workflows/sequential/WORKFLOW.md +1 -1
  289. package/vendor/ink/build/display-width.js +19 -3
  290. package/vendor/ink/build/ink.js +103 -6
  291. package/vendor/ink/build/log-update.js +17 -3
  292. package/vendor/ink/build/wrap-text.js +125 -0
  293. package/scripts/_test-folder-dialog.mjs +0 -30
  294. package/scripts/fix-brief-fn.mjs +0 -35
  295. package/scripts/fix-format-tool-surface.mjs +0 -24
  296. package/scripts/fix-tool-exec-visible.mjs +0 -42
  297. package/scripts/patch-agent-brief.mjs +0 -48
  298. package/scripts/patch-app.mjs +0 -21
  299. package/scripts/patch-app2.mjs +0 -18
  300. package/scripts/patch-dist-brief.mjs +0 -96
  301. package/scripts/patch-tool-exec.mjs +0 -70
  302. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  303. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  304. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  305. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  306. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  307. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  308. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  309. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  310. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  311. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  312. package/src/standalone/agent-task-status.test.mjs +0 -76
  313. package/src/tui/components/tool-output-format.test.mjs +0 -399
  314. package/src/tui/display-width.test.mjs +0 -35
  315. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  316. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  317. package/src/tui/input-editing.selection.test.mjs +0 -75
  318. package/src/tui/markdown/format-token.test.mjs +0 -354
  319. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  320. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  321. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  322. package/src/tui/paste-fix.test.mjs +0 -119
  323. package/src/tui/prompt-history-store.test.mjs +0 -52
  324. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  325. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  326. package/src/ui/markdown.test.mjs +0 -70
  327. package/src/ui/statusline-context-label.test.mjs +0 -15
  328. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  329. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -0,0 +1,489 @@
1
+ /**
2
+ * Anthropic OAuth credential store + PKCE login flow.
3
+ *
4
+ * Split out of anthropic-oauth.mjs (section-scoped extraction). Owns the
5
+ * on-disk credentials file (load/save/refresh/forget) and the loopback
6
+ * PKCE login flow. anthropic-oauth.mjs imports these back and re-exports
7
+ * the public functions so external callers keep their existing import path.
8
+ */
9
+ import { readFileSync, existsSync, statSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { createServer } from 'http';
12
+ import { randomBytes, createHash } from 'crypto';
13
+ import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
14
+ import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
15
+ import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
16
+
17
+ // SSRF guard for the OAuth token endpoint override. Env-supplied URLs must be
18
+ // https with a valid http(s) URL shape; reject file:/data:/ftp:/etc. and any
19
+ // http override so a hostile env cannot redirect refresh-token requests.
20
+ function assertSafeTokenURL(rawURL) {
21
+ let parsed;
22
+ try {
23
+ parsed = new URL(String(rawURL));
24
+ } catch {
25
+ throw new Error(`[anthropic-oauth] invalid ANTHROPIC_OAUTH_TOKEN_URL: ${rawURL}`);
26
+ }
27
+ if (parsed.protocol.toLowerCase() !== 'https:') {
28
+ throw new Error(`[anthropic-oauth] ANTHROPIC_OAUTH_TOKEN_URL must use https (got ${parsed.protocol})`);
29
+ }
30
+ return rawURL;
31
+ }
32
+ export const TOKEN_URL = assertSafeTokenURL(process.env.ANTHROPIC_OAUTH_TOKEN_URL || 'https://platform.claude.com/v1/oauth/token');
33
+ export const DEFAULT_CREDENTIALS_PATH = join(resolvePluginData(), 'anthropic-oauth-credentials.json');
34
+ export const CLAUDE_CODE_CLIENT_ID = process.env.ANTHROPIC_OAUTH_CLIENT_ID || '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
35
+ export const TOKEN_REFRESH_SKEW_MS = 5 * 60_000;
36
+ const CLAUDE_AI_AUTHORIZE_URL = 'https://claude.com/cai/oauth/authorize';
37
+ const ALL_OAUTH_SCOPES = [
38
+ 'org:create_api_key',
39
+ 'user:profile',
40
+ 'user:inference',
41
+ 'user:sessions:claude_code',
42
+ 'user:mcp_servers',
43
+ 'user:file_upload',
44
+ ];
45
+ const OAUTH_LOGIN_SCOPE = ALL_OAUTH_SCOPES.join(' ');
46
+ const OAUTH_CALLBACK_HOST = 'localhost';
47
+ const OAUTH_CALLBACK_PORT = 54545;
48
+ const OAUTH_CALLBACK_PATH = '/callback';
49
+ const OAUTH_REDIRECT_URI = `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`;
50
+ const OAUTH_MANUAL_REDIRECT_URI = process.env.ANTHROPIC_OAUTH_MANUAL_REDIRECT_URI || 'https://platform.claude.com/oauth/code/callback';
51
+ const OAUTH_SUCCESS_REDIRECT_URL = process.env.ANTHROPIC_OAUTH_SUCCESS_REDIRECT_URL || 'https://platform.claude.com/oauth/code/success?app=claude-code';
52
+ const OAUTH_LOGIN_TIMEOUT_MS = 5 * 60_000;
53
+ const OAUTH_TOKEN_TIMEOUT_MS = 30_000;
54
+
55
+ export const DEFAULT_CLI_VERSION = '2.1.77';
56
+
57
+ export function resolveCliVersion() {
58
+ return process.env.MIXDOG_CLI_VERSION
59
+ || DEFAULT_CLI_VERSION;
60
+ }
61
+
62
+ // --- Credential helpers ---
63
+
64
+ function _pushUnique(list, value) {
65
+ if (!value || typeof value !== 'string') return;
66
+ if (!list.includes(value)) list.push(value);
67
+ }
68
+
69
+ export function credentialCandidates() {
70
+ const paths = [];
71
+ _pushUnique(paths, process.env.ANTHROPIC_OAUTH_CREDENTIALS_PATH);
72
+ _pushUnique(paths, DEFAULT_CREDENTIALS_PATH);
73
+ return paths;
74
+ }
75
+
76
+ // Fallback expiry from the access_token's JWT `exp` claim (epoch ms) when the
77
+ // credentials file carries no explicit expiresAt — without it expiresAt stays 0,
78
+ // which ensureAuth reads as "never expires", disabling proactive refresh. Claude
79
+ // OAuth tokens are opaque so this returns 0 and the file's expiresAt governs.
80
+ // JWT `exp` is epoch SECONDS (RFC 7519).
81
+ function _expiryFromAccessToken(token) {
82
+ try {
83
+ const parts = String(token || '').split('.');
84
+ if (parts.length !== 3) return 0;
85
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));
86
+ const exp = Number(payload?.exp);
87
+ return Number.isFinite(exp) && exp > 0 ? exp * 1000 : 0;
88
+ } catch { return 0; }
89
+ }
90
+
91
+ function _loadCredentialsFile(path) {
92
+ if (!existsSync(path)) return null;
93
+ try {
94
+ const stat = statSync(path);
95
+ const raw = JSON.parse(readFileSync(path, 'utf-8'));
96
+ const oauth = raw?.claudeAiOauth;
97
+ if (!oauth?.accessToken) return null;
98
+ return {
99
+ path,
100
+ mtimeMs: stat.mtimeMs,
101
+ accessToken: oauth.accessToken,
102
+ refreshToken: oauth.refreshToken || null,
103
+ expiresAt: _normalizeExpiresAt(oauth.expiresAt ?? oauth.expires_at) || _expiryFromAccessToken(oauth.accessToken),
104
+ scopes: Array.isArray(oauth.scopes) ? oauth.scopes : [],
105
+ subscriptionType: oauth.subscriptionType || null,
106
+ };
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ // Cross-process safe credential save. Lockfile (O_EXCL) prevents two Mixdog
113
+ // refreshers from clobbering each other; atomic rename guarantees readers see
114
+ // either the old or new file, never a half-written one. Used so refresh_token
115
+ // rotation propagates to other Mixdog readers of the same credentials file
116
+ // instead of leaving them stuck on the previous refresh_token.
117
+ export function _saveCredentialsFile(path, raw) {
118
+ // Secret file, not parent-dir ACL mutation. `secret: true` clamps the file
119
+ // itself on Windows; it deliberately leaves the data dir inheritance alone.
120
+ writeJsonAtomicSync(path, raw, { lock: true, fsyncDir: true, mode: 0o600, secret: true });
121
+ }
122
+
123
+ // Cheap stat-only probe so ensureAuth can detect Mixdog-updated credentials
124
+ // without paying a full JSON read every call.
125
+ export function _credentialsMaxMtime() {
126
+ let max = 0;
127
+ for (const p of credentialCandidates()) {
128
+ try {
129
+ const s = statSync(p);
130
+ if (s.mtimeMs > max) max = s.mtimeMs;
131
+ } catch { /* not present — skip */ }
132
+ }
133
+ return max;
134
+ }
135
+
136
+ export function loadCredentials() {
137
+ const loaded = credentialCandidates()
138
+ .map(_loadCredentialsFile)
139
+ .filter(Boolean);
140
+ if (!loaded.length) return null;
141
+ loaded.sort((a, b) => (Number(b.expiresAt) || 0) - (Number(a.expiresAt) || 0));
142
+ return loaded[0];
143
+ }
144
+
145
+ // Public predicate used by config.buildDefaultConfig — provider is enabled
146
+ // when on-disk credentials exist AND carry the inference scope. Single
147
+ // truth: same loader the runtime uses, no parallel hard-coded path probe.
148
+ export function hasAnthropicOAuthCredentials() {
149
+ const creds = loadCredentials();
150
+ if (!creds?.accessToken) return false;
151
+ return Array.isArray(creds.scopes) && creds.scopes.includes('user:inference');
152
+ }
153
+
154
+ export function describeAnthropicOAuthCredentials() {
155
+ try {
156
+ const creds = loadCredentials();
157
+ if (!creds?.accessToken) {
158
+ return { authenticated: false, status: 'Not Set', detail: 'Mixdog OAuth credentials' };
159
+ }
160
+ const hasInferenceScope = Array.isArray(creds.scopes) && creds.scopes.includes('user:inference');
161
+ const hasRefresh = Boolean(creds.refreshToken);
162
+ const expiresAt = _normalizeExpiresAt(creds.expiresAt);
163
+ const expiring = expiresAt > 0 && expiresAt < Date.now() + TOKEN_REFRESH_SKEW_MS;
164
+ const expired = expiresAt > 0 && expiresAt <= Date.now();
165
+ const detail = creds.path || DEFAULT_CREDENTIALS_PATH;
166
+ if (!hasInferenceScope) {
167
+ return { authenticated: false, status: 'Missing Scope', detail, expiresAt };
168
+ }
169
+ if (!hasRefresh) {
170
+ return {
171
+ authenticated: expiresAt === 0 || !expired,
172
+ status: expired ? 'Reauth Required' : 'Access Only',
173
+ detail: `${detail}; no refresh token`,
174
+ expiresAt,
175
+ };
176
+ }
177
+ if (expired) return { authenticated: true, status: 'Refresh Required', detail, expiresAt };
178
+ if (expiring) return { authenticated: true, status: 'Refresh Soon', detail, expiresAt };
179
+ return { authenticated: true, status: 'Valid', detail, expiresAt };
180
+ } catch (err) {
181
+ return { authenticated: false, status: 'Error', detail: String(err?.message || err).slice(0, 200) };
182
+ }
183
+ }
184
+
185
+ export function forgetAnthropicOAuthCredentials() {
186
+ let removed = false;
187
+ for (const path of credentialCandidates()) {
188
+ if (!existsSync(path)) continue;
189
+ try {
190
+ const raw = JSON.parse(readFileSync(path, 'utf-8'));
191
+ if (raw?.claudeAiOauth) {
192
+ delete raw.claudeAiOauth;
193
+ _saveCredentialsFile(path, raw);
194
+ removed = true;
195
+ }
196
+ } catch (err) {
197
+ throw new Error(`Anthropic OAuth reset failed for ${path}: ${err?.message || err}`);
198
+ }
199
+ }
200
+ return { removed };
201
+ }
202
+
203
+ export function _normalizeExpiresAt(value) {
204
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return 0;
205
+ return value < 1e12 ? value * 1000 : value;
206
+ }
207
+
208
+ export function _scrubTokens(text) {
209
+ return String(text || '')
210
+ .replace(/Bearer [A-Za-z0-9._\-]+/g, 'Bearer [REDACTED]')
211
+ .replace(/sk-ant-[A-Za-z0-9._\-]+/g, '[REDACTED]')
212
+ .replace(/"access[Tt]oken"\s*:\s*"[^"]+"/g, '"accessToken":"[REDACTED]"')
213
+ .replace(/"refresh[Tt]oken"\s*:\s*"[^"]+"/g, '"refreshToken":"[REDACTED]"')
214
+ .replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token":"[REDACTED]"')
215
+ .replace(/"refresh_token"\s*:\s*"[^"]+"/g, '"refresh_token":"[REDACTED]"');
216
+ }
217
+
218
+ export async function refreshOAuthCredentials(creds) {
219
+ if (!creds?.refreshToken) {
220
+ throw new Error('Anthropic OAuth refresh token not available. Open /providers in mixdog to sign in again.');
221
+ }
222
+
223
+ const controller = new AbortController();
224
+ const timeout = setTimeout(() => controller.abort(), 30_000);
225
+ try {
226
+ const res = await fetch(TOKEN_URL, {
227
+ method: 'POST',
228
+ headers: {
229
+ 'Content-Type': 'application/json',
230
+ 'anthropic-dangerous-direct-browser-access': 'true',
231
+ 'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
232
+ },
233
+ body: JSON.stringify({
234
+ grant_type: 'refresh_token',
235
+ refresh_token: creds.refreshToken,
236
+ client_id: CLAUDE_CODE_CLIENT_ID,
237
+ }),
238
+ // Never follow a redirect on a secret-bearing request: a token
239
+ // endpoint that 307/308-redirects would replay the refresh_token to
240
+ // the redirect target. Fail loud instead.
241
+ redirect: 'error',
242
+ signal: controller.signal,
243
+ dispatcher: getLlmDispatcher(),
244
+ });
245
+
246
+ const text = await res.text();
247
+ let json = null;
248
+ try { json = text ? JSON.parse(text) : null; } catch { /* handled below */ }
249
+ if (!res.ok) {
250
+ const isInvalidGrant = text.includes('invalid_grant') || json?.error === 'invalid_grant';
251
+ throw Object.assign(new Error(`token refresh ${res.status}: ${_scrubTokens(text).slice(0, 200)}`), { isInvalidGrant });
252
+ }
253
+
254
+ const accessToken = json?.access_token || json?.accessToken;
255
+ if (!accessToken) throw new Error('token refresh returned no access token');
256
+ const expiresAt = _normalizeExpiresAt(json?.expires_at ?? json?.expiresAt)
257
+ || (typeof json?.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0);
258
+ const refreshed = {
259
+ path: creds.path,
260
+ accessToken,
261
+ refreshToken: json?.refresh_token || json?.refreshToken || creds.refreshToken,
262
+ expiresAt,
263
+ scopes: Array.isArray(json?.scope) ? json.scope : creds.scopes,
264
+ subscriptionType: creds.subscriptionType,
265
+ };
266
+ // Persist rotated tokens back so any other Mixdog reader of the same
267
+ // credentials file picks up the new refresh_token. Without this, a
268
+ // later process can replay an old single-use refresh token and loop on
269
+ // invalid_grant.
270
+ if (creds.path && existsSync(creds.path)) {
271
+ try {
272
+ const raw = JSON.parse(readFileSync(creds.path, 'utf-8'));
273
+ raw.claudeAiOauth = {
274
+ ...(raw.claudeAiOauth || {}),
275
+ accessToken: refreshed.accessToken,
276
+ refreshToken: refreshed.refreshToken,
277
+ expiresAt: refreshed.expiresAt,
278
+ scopes: refreshed.scopes,
279
+ };
280
+ _saveCredentialsFile(creds.path, raw);
281
+ } catch (err) {
282
+ process.stderr.write(`[anthropic-oauth] credential save failed: ${_scrubTokens(err?.message || String(err)).slice(0, 200)}\n`);
283
+ throw new Error(`[oauth] credentials save failed: ${err?.message ?? String(err)}`);
284
+ }
285
+ }
286
+ return refreshed;
287
+ } catch (err) {
288
+ if (err?.name === 'AbortError') {
289
+ throw new Error('Anthropic OAuth token refresh timed out after 30000ms');
290
+ }
291
+ throw err;
292
+ } finally {
293
+ clearTimeout(timeout);
294
+ }
295
+ }
296
+
297
+ // --- Login flow (PKCE loopback, export for setup UI / CLI) ---
298
+
299
+ function _oauthGeneratePKCE() {
300
+ const verifier = randomBytes(32).toString('base64url');
301
+ const challenge = createHash('sha256').update(verifier).digest('base64url');
302
+ return { verifier, challenge };
303
+ }
304
+
305
+ function _oauthCredentialsWritePath() {
306
+ for (const p of credentialCandidates()) {
307
+ if (existsSync(p)) return p;
308
+ }
309
+ return DEFAULT_CREDENTIALS_PATH;
310
+ }
311
+
312
+ function _oauthParseScopeField(scope) {
313
+ if (Array.isArray(scope)) return scope;
314
+ return String(scope || '').split(' ').filter(Boolean);
315
+ }
316
+
317
+ function _parseOAuthCodeInput(input) {
318
+ const value = String(input || '').trim();
319
+ if (!value) return { code: '', state: '' };
320
+ try {
321
+ const url = new URL(value);
322
+ const code = url.searchParams.get('code') || '';
323
+ const state = url.searchParams.get('state') || '';
324
+ if (code || state) return { code, state, redirectUri: `${url.origin}${url.pathname}` };
325
+ } catch { /* not a URL */ }
326
+ if (value.includes('#')) {
327
+ const [code, state] = value.split('#', 2);
328
+ return { code: String(code || '').trim(), state: String(state || '').trim() };
329
+ }
330
+ if (value.includes('code=')) {
331
+ const params = new URLSearchParams(value.startsWith('?') ? value.slice(1) : value);
332
+ return { code: params.get('code') || '', state: params.get('state') || '' };
333
+ }
334
+ return { code: value, state: '' };
335
+ }
336
+
337
+ async function exchangeAuthorizationCode({ pkce, code, state, redirectUri }) {
338
+ const cleanCode = String(code || '').trim();
339
+ if (!cleanCode) throw new Error('[anthropic-oauth] authorization code is required');
340
+ const tokenRes = await fetch(TOKEN_URL, {
341
+ method: 'POST',
342
+ headers: {
343
+ 'Content-Type': 'application/json',
344
+ 'anthropic-dangerous-direct-browser-access': 'true',
345
+ 'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
346
+ },
347
+ body: JSON.stringify({
348
+ grant_type: 'authorization_code',
349
+ code: cleanCode,
350
+ redirect_uri: redirectUri,
351
+ client_id: CLAUDE_CODE_CLIENT_ID,
352
+ code_verifier: pkce.verifier,
353
+ state,
354
+ }),
355
+ redirect: 'error',
356
+ signal: AbortSignal.timeout(OAUTH_TOKEN_TIMEOUT_MS),
357
+ dispatcher: getLlmDispatcher(),
358
+ });
359
+ if (!tokenRes.ok) {
360
+ const text = await tokenRes.text().catch(() => '');
361
+ throw new Error(`[anthropic-oauth] token exchange ${tokenRes.status}: ${_scrubTokens(text).slice(0, 500)}`);
362
+ }
363
+ const json = await tokenRes.json();
364
+ const accessToken = json?.access_token || json?.accessToken;
365
+ const refreshToken = json?.refresh_token || json?.refreshToken;
366
+ if (!accessToken || !refreshToken) {
367
+ throw new Error('[anthropic-oauth] token exchange response missing access_token or refresh_token');
368
+ }
369
+ const expiresAt = _normalizeExpiresAt(json?.expires_at ?? json?.expiresAt)
370
+ || (typeof json?.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0);
371
+ const scopes = _oauthParseScopeField(json?.scope);
372
+ const credPath = _oauthCredentialsWritePath();
373
+ let raw = {};
374
+ if (existsSync(credPath)) {
375
+ raw = JSON.parse(readFileSync(credPath, 'utf-8'));
376
+ }
377
+ const existingOauth = raw.claudeAiOauth || {};
378
+ raw.claudeAiOauth = {
379
+ ...existingOauth,
380
+ accessToken,
381
+ refreshToken,
382
+ expiresAt,
383
+ scopes,
384
+ subscriptionType: existingOauth.subscriptionType ?? null,
385
+ };
386
+ _saveCredentialsFile(credPath, raw);
387
+ return {
388
+ path: credPath,
389
+ accessToken,
390
+ refreshToken,
391
+ expiresAt,
392
+ scopes,
393
+ subscriptionType: raw.claudeAiOauth.subscriptionType,
394
+ };
395
+ }
396
+
397
+ export async function beginOAuthLogin() {
398
+ const pkce = _oauthGeneratePKCE();
399
+ const state = randomBytes(32).toString('base64url');
400
+ const buildUrl = (redirectUri) => {
401
+ const url = new URL(CLAUDE_AI_AUTHORIZE_URL);
402
+ url.searchParams.set('code', 'true');
403
+ url.searchParams.set('client_id', CLAUDE_CODE_CLIENT_ID);
404
+ url.searchParams.set('response_type', 'code');
405
+ url.searchParams.set('redirect_uri', redirectUri);
406
+ url.searchParams.set('scope', OAUTH_LOGIN_SCOPE);
407
+ url.searchParams.set('code_challenge', pkce.challenge);
408
+ url.searchParams.set('code_challenge_method', 'S256');
409
+ url.searchParams.set('state', state);
410
+ return url;
411
+ };
412
+ const url = buildUrl(OAUTH_REDIRECT_URI);
413
+ const manualUrl = buildUrl(OAUTH_MANUAL_REDIRECT_URI);
414
+
415
+ let server = null;
416
+ let timeout = null;
417
+ let finish = null;
418
+ const waitForCallback = new Promise((resolve, reject) => {
419
+ let settled = false;
420
+ finish = (value, error = null) => {
421
+ if (settled) return;
422
+ settled = true;
423
+ if (timeout) clearTimeout(timeout);
424
+ try { server?.close(); } catch { /* already closed */ }
425
+ if (error) reject(error);
426
+ else resolve(value);
427
+ };
428
+ server = createServer(async (req, res) => {
429
+ const u = new URL(req.url || '/', `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}`);
430
+ if (u.pathname !== OAUTH_CALLBACK_PATH) {
431
+ res.writeHead(404);
432
+ res.end();
433
+ return;
434
+ }
435
+ const code = u.searchParams.get('code');
436
+ if (!code || u.searchParams.get('state') !== state) {
437
+ res.writeHead(400);
438
+ res.end('Invalid');
439
+ finish(null);
440
+ return;
441
+ }
442
+ try {
443
+ const tokens = await exchangeAuthorizationCode({ pkce, code, state, redirectUri: OAUTH_REDIRECT_URI });
444
+ res.writeHead(302, { Location: OAUTH_SUCCESS_REDIRECT_URL });
445
+ res.end();
446
+ finish(tokens);
447
+ } catch (err) {
448
+ const error = err instanceof Error ? err : new Error(String(err));
449
+ res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
450
+ res.end(`Claude login failed: ${error.message}`);
451
+ finish(null, error);
452
+ }
453
+ });
454
+ timeout = setTimeout(() => finish(null), OAUTH_LOGIN_TIMEOUT_MS);
455
+ server.listen(OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_HOST, async () => {
456
+ process.stderr.write(`\n[anthropic-oauth] Open this URL to log in with Claude:\n${url.toString()}\n\nIf the localhost callback cannot complete, open this manual URL and paste the shown code#state:\n${manualUrl.toString()}\n\n`);
457
+ try {
458
+ const { openInBrowser } = await import('../../../shared/open-url.mjs');
459
+ openInBrowser(url.toString());
460
+ } catch (err) {
461
+ process.stderr.write(`[anthropic-oauth] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
462
+ }
463
+ });
464
+ server.on('error', (err) => finish(null, new Error(`[anthropic-oauth] callback server failed on ${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}: ${err?.message || err}`)));
465
+ });
466
+
467
+ return {
468
+ provider: 'anthropic-oauth',
469
+ url: url.toString(),
470
+ manualUrl: manualUrl.toString(),
471
+ waitForCallback,
472
+ completeCode: async (input) => {
473
+ const parsed = _parseOAuthCodeInput(input);
474
+ if (parsed.state && parsed.state !== state) throw new Error('[anthropic-oauth] OAuth state mismatch');
475
+ const redirectUri = parsed.redirectUri || (parsed.state ? OAUTH_MANUAL_REDIRECT_URI : OAUTH_REDIRECT_URI);
476
+ const tokens = await exchangeAuthorizationCode({ pkce, code: parsed.code, state, redirectUri });
477
+ finish?.(tokens);
478
+ return tokens;
479
+ },
480
+ cancel: () => {
481
+ finish?.(null);
482
+ },
483
+ };
484
+ }
485
+
486
+ export async function loginOAuth() {
487
+ const login = await beginOAuthLogin();
488
+ return await login.waitForCallback;
489
+ }