mixdog 0.7.18 → 0.8.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 (844) hide show
  1. package/README.md +37 -331
  2. package/package.json +67 -99
  3. package/scripts/boot-smoke.mjs +94 -0
  4. package/scripts/build-tui.mjs +52 -0
  5. package/scripts/compact-smoke.mjs +199 -0
  6. package/scripts/lead-workflow-smoke.mjs +598 -0
  7. package/scripts/live-worker-smoke.mjs +239 -0
  8. package/scripts/output-style-smoke.mjs +101 -0
  9. package/scripts/smoke-loop-report.mjs +221 -0
  10. package/scripts/smoke-loop.mjs +201 -0
  11. package/scripts/smoke.mjs +113 -0
  12. package/scripts/tool-failures.mjs +143 -0
  13. package/scripts/tool-smoke.mjs +456 -0
  14. package/src/agents/debugger/AGENT.md +3 -0
  15. package/src/agents/debugger/agent.json +6 -0
  16. package/src/agents/explore/AGENT.md +4 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/agents/heavy-worker/AGENT.md +3 -0
  19. package/src/agents/heavy-worker/agent.json +6 -0
  20. package/src/agents/maintainer/AGENT.md +3 -0
  21. package/src/agents/maintainer/agent.json +6 -0
  22. package/src/agents/reviewer/AGENT.md +3 -0
  23. package/src/agents/reviewer/agent.json +6 -0
  24. package/src/agents/scheduler-task.md +3 -0
  25. package/src/agents/web-researcher/AGENT.md +3 -0
  26. package/src/agents/web-researcher/agent.json +6 -0
  27. package/src/agents/webhook-handler.md +3 -0
  28. package/src/agents/worker/AGENT.md +3 -0
  29. package/src/agents/worker/agent.json +6 -0
  30. package/src/app.mjs +90 -0
  31. package/src/cli.mjs +11 -0
  32. package/src/defaults/hidden-roles.json +72 -0
  33. package/src/defaults/mixdog-config.template.json +15 -0
  34. package/src/hooks/lib/permission-evaluator.cjs +488 -0
  35. package/src/hooks/lib/settings-loader.cjs +112 -0
  36. package/src/lib/keychain-cjs.cjs +332 -0
  37. package/src/lib/plugin-paths.cjs +28 -0
  38. package/src/lib/rules-builder.cjs +315 -0
  39. package/src/mixdog-session-runtime.mjs +3704 -0
  40. package/src/output-styles/default.md +38 -0
  41. package/src/output-styles/extreme-simple.md +17 -0
  42. package/src/output-styles/simple.md +17 -0
  43. package/src/repl.mjs +322 -0
  44. package/src/rules/bridge/00-common.md +5 -0
  45. package/src/rules/bridge/20-skip-protocol.md +11 -0
  46. package/src/rules/bridge/30-explorer.md +4 -0
  47. package/src/rules/bridge/40-cycle1-agent.md +28 -0
  48. package/src/rules/bridge/41-cycle2-agent.md +59 -0
  49. package/src/rules/lead/00-tool-lead.md +5 -0
  50. package/src/rules/lead/01-general.md +5 -0
  51. package/src/rules/lead/02-channels.md +3 -0
  52. package/src/rules/lead/04-workflow.md +12 -0
  53. package/src/rules/shared/00-language.md +3 -0
  54. package/src/rules/shared/01-tool.md +3 -0
  55. package/src/runtime/agent/orchestrator/bridge-trace.mjs +814 -0
  56. package/src/runtime/agent/orchestrator/cache-mtime.mjs +60 -0
  57. package/src/runtime/agent/orchestrator/config.mjs +446 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +796 -0
  59. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +417 -0
  60. package/src/runtime/agent/orchestrator/internal-roles.mjs +188 -0
  61. package/src/runtime/agent/orchestrator/internal-tools.mjs +88 -0
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +345 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2104 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +784 -0
  65. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +341 -0
  66. package/src/runtime/agent/orchestrator/providers/gemini.mjs +1679 -0
  67. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +959 -0
  68. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +213 -0
  69. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +38 -0
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +471 -0
  71. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +615 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +808 -0
  73. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +1719 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2587 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1953 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +136 -0
  77. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +317 -0
  78. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +109 -0
  79. package/src/runtime/agent/orchestrator/providers/registry.mjs +247 -0
  80. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +332 -0
  81. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/providers/trace-utils.mjs +50 -0
  83. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  84. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +318 -0
  85. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +367 -0
  86. package/src/runtime/agent/orchestrator/session/compact.mjs +882 -0
  87. package/src/runtime/agent/orchestrator/session/context-utils.mjs +233 -0
  88. package/src/runtime/agent/orchestrator/session/loop.mjs +2320 -0
  89. package/src/runtime/agent/orchestrator/session/manager.mjs +2960 -0
  90. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  91. package/src/runtime/agent/orchestrator/session/store.mjs +663 -0
  92. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  93. package/src/runtime/agent/orchestrator/smart-bridge/bridge-llm.mjs +339 -0
  94. package/src/runtime/agent/orchestrator/smart-bridge/cache-strategy.mjs +419 -0
  95. package/src/runtime/agent/orchestrator/stall-policy.mjs +227 -0
  96. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +235 -0
  97. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +723 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +389 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +637 -0
  100. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +165 -0
  101. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +104 -0
  102. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +194 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +596 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +110 -0
  105. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +153 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +189 -0
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +731 -0
  109. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +168 -0
  110. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +465 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +160 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +982 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +1087 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +231 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +223 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin.mjs +478 -0
  118. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  119. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4102 -0
  120. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  121. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +154 -0
  122. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +26 -0
  123. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +143 -0
  124. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +18 -0
  125. package/src/runtime/agent/orchestrator/tools/patch.mjs +2772 -0
  126. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +114 -0
  127. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +880 -0
  128. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +312 -0
  129. package/src/runtime/channels/backends/discord.mjs +781 -0
  130. package/src/runtime/channels/data/voice-runtime-manifest.json +138 -0
  131. package/src/runtime/channels/index.mjs +3309 -0
  132. package/src/runtime/channels/lib/config.mjs +285 -0
  133. package/src/runtime/channels/lib/drop-trace.mjs +71 -0
  134. package/src/runtime/channels/lib/event-pipeline.mjs +81 -0
  135. package/src/runtime/channels/lib/holidays.mjs +138 -0
  136. package/src/runtime/channels/lib/hook-pipe-server.mjs +671 -0
  137. package/src/runtime/channels/lib/output-forwarder.mjs +765 -0
  138. package/src/runtime/channels/lib/runtime-paths.mjs +497 -0
  139. package/src/runtime/channels/lib/scheduler.mjs +710 -0
  140. package/src/runtime/channels/lib/session-discovery.mjs +102 -0
  141. package/src/runtime/channels/lib/state-file.mjs +68 -0
  142. package/src/runtime/channels/lib/status-snapshot.mjs +224 -0
  143. package/src/runtime/channels/lib/tool-format.mjs +124 -0
  144. package/src/runtime/channels/lib/transcript-discovery.mjs +195 -0
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +734 -0
  146. package/src/runtime/channels/lib/webhook.mjs +1288 -0
  147. package/src/runtime/channels/tool-defs.mjs +177 -0
  148. package/src/runtime/lib/keychain-cjs.cjs +289 -0
  149. package/src/runtime/memory/data/runtime-manifest.json +40 -0
  150. package/src/runtime/memory/index.mjs +3600 -0
  151. package/src/runtime/memory/lib/core-memory-store.mjs +336 -0
  152. package/src/runtime/memory/lib/embedding-provider.mjs +275 -0
  153. package/src/runtime/memory/lib/embedding-worker.mjs +331 -0
  154. package/src/runtime/memory/lib/memory-cycle-requests.mjs +276 -0
  155. package/src/runtime/memory/lib/memory-cycle1.mjs +783 -0
  156. package/src/runtime/memory/lib/memory-cycle2.mjs +1389 -0
  157. package/src/runtime/memory/lib/memory-cycle3.mjs +646 -0
  158. package/src/runtime/memory/lib/memory-embed.mjs +300 -0
  159. package/src/runtime/memory/lib/memory-ops-policy.mjs +149 -0
  160. package/src/runtime/memory/lib/memory-recall-store.mjs +644 -0
  161. package/src/runtime/memory/lib/memory.mjs +418 -0
  162. package/src/runtime/memory/lib/pg/adapter.mjs +314 -0
  163. package/src/runtime/memory/lib/pg/process.mjs +366 -0
  164. package/src/runtime/memory/lib/pg/supervisor.mjs +495 -0
  165. package/src/runtime/memory/lib/runtime-fetcher.mjs +464 -0
  166. package/src/runtime/memory/lib/trace-store.mjs +734 -0
  167. package/src/runtime/memory/tool-defs.mjs +79 -0
  168. package/src/runtime/search/index.mjs +925 -0
  169. package/src/runtime/search/lib/config.mjs +61 -0
  170. package/src/runtime/search/lib/web-tools.mjs +1278 -0
  171. package/src/runtime/search/tool-defs.mjs +64 -0
  172. package/src/runtime/shared/atomic-file.mjs +435 -0
  173. package/src/runtime/shared/background-tasks.mjs +376 -0
  174. package/src/runtime/shared/child-guardian.mjs +98 -0
  175. package/src/runtime/shared/config.mjs +393 -0
  176. package/src/runtime/shared/err-text.mjs +121 -0
  177. package/src/runtime/shared/launcher-control.mjs +259 -0
  178. package/src/runtime/shared/llm/http-agent.mjs +129 -0
  179. package/src/runtime/shared/open-url.mjs +37 -0
  180. package/src/runtime/shared/plugin-paths.mjs +25 -0
  181. package/src/runtime/shared/process-shutdown.mjs +147 -0
  182. package/src/runtime/shared/schedules-store.mjs +70 -0
  183. package/src/runtime/shared/tool-execution-contract.mjs +104 -0
  184. package/src/runtime/shared/tool-surface.mjs +950 -0
  185. package/src/runtime/shared/user-cwd.mjs +221 -0
  186. package/src/runtime/shared/user-data-guard.mjs +232 -0
  187. package/src/runtime/shared/workspace-router.mjs +259 -0
  188. package/src/standalone/bridge-tool.mjs +1414 -0
  189. package/src/standalone/channel-admin.mjs +366 -0
  190. package/src/standalone/channel-worker-preload.cjs +3 -0
  191. package/src/standalone/channel-worker.mjs +353 -0
  192. package/src/standalone/explore-tool.mjs +233 -0
  193. package/src/standalone/hook-bus.mjs +246 -0
  194. package/src/standalone/plugin-admin.mjs +247 -0
  195. package/src/standalone/provider-admin.mjs +338 -0
  196. package/src/standalone/seeds.mjs +94 -0
  197. package/src/standalone/usage-dashboard.mjs +510 -0
  198. package/src/tui/App.jsx +5438 -0
  199. package/src/tui/components/AnsiText.jsx +199 -0
  200. package/src/tui/components/ContextPanel.jsx +217 -0
  201. package/src/tui/components/Markdown.jsx +205 -0
  202. package/src/tui/components/MarkdownTable.jsx +204 -0
  203. package/src/tui/components/Message.jsx +103 -0
  204. package/src/tui/components/Picker.jsx +317 -0
  205. package/src/tui/components/PromptInput.jsx +584 -0
  206. package/src/tui/components/QueuedCommands.jsx +47 -0
  207. package/src/tui/components/SlashCommandPalette.jsx +114 -0
  208. package/src/tui/components/Spinner.jsx +317 -0
  209. package/src/tui/components/StatusLine.jsx +87 -0
  210. package/src/tui/components/TextEntryPanel.jsx +323 -0
  211. package/src/tui/components/ToolExecution.jsx +772 -0
  212. package/src/tui/components/TurnDone.jsx +78 -0
  213. package/src/tui/components/UsagePanel.jsx +331 -0
  214. package/src/tui/dist/index.mjs +12359 -0
  215. package/src/tui/engine.mjs +2410 -0
  216. package/src/tui/figures.mjs +50 -0
  217. package/src/tui/hooks/useEngine.mjs +16 -0
  218. package/src/tui/index.jsx +254 -0
  219. package/src/tui/input-editing.mjs +242 -0
  220. package/src/tui/markdown/format-token.mjs +194 -0
  221. package/src/tui/paste-attachments.mjs +198 -0
  222. package/src/tui/runtime/shared/process-shutdown.mjs +1 -0
  223. package/src/tui/spinner-verbs.mjs +45 -0
  224. package/src/tui/theme.mjs +67 -0
  225. package/src/tui/time-format.mjs +53 -0
  226. package/src/ui/ansi.mjs +115 -0
  227. package/src/ui/markdown.mjs +195 -0
  228. package/src/ui/statusline.mjs +730 -0
  229. package/src/ui/tool-card.mjs +101 -0
  230. package/src/vendor/statusline/bin/statusline-lib.mjs +805 -0
  231. package/src/vendor/statusline/bin/statusline-route.mjs +596 -0
  232. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +285 -0
  233. package/src/vendor/statusline/src/gateway/claude-current.mjs +320 -0
  234. package/src/vendor/statusline/src/gateway/route-meta.mjs +753 -0
  235. package/src/vendor/statusline/src/gateway/session-routes.mjs +244 -0
  236. package/src/workflows/default/WORKFLOW.md +7 -0
  237. package/src/workflows/default/workflow.json +14 -0
  238. package/vendor/ink/build/ansi-tokenizer.d.ts +38 -0
  239. package/vendor/ink/build/ansi-tokenizer.js +316 -0
  240. package/vendor/ink/build/ansi-tokenizer.js.map +1 -0
  241. package/vendor/ink/build/colorize.d.ts +3 -0
  242. package/vendor/ink/build/colorize.js +48 -0
  243. package/vendor/ink/build/colorize.js.map +1 -0
  244. package/vendor/ink/build/components/AccessibilityContext.d.ts +3 -0
  245. package/vendor/ink/build/components/AccessibilityContext.js +5 -0
  246. package/vendor/ink/build/components/AccessibilityContext.js.map +1 -0
  247. package/vendor/ink/build/components/AnimationContext.d.ts +9 -0
  248. package/vendor/ink/build/components/AnimationContext.js +13 -0
  249. package/vendor/ink/build/components/AnimationContext.js.map +1 -0
  250. package/vendor/ink/build/components/App.d.ts +24 -0
  251. package/vendor/ink/build/components/App.js +554 -0
  252. package/vendor/ink/build/components/App.js.map +1 -0
  253. package/vendor/ink/build/components/AppContext.d.ts +80 -0
  254. package/vendor/ink/build/components/AppContext.js +25 -0
  255. package/vendor/ink/build/components/AppContext.js.map +1 -0
  256. package/vendor/ink/build/components/BackgroundContext.d.ts +4 -0
  257. package/vendor/ink/build/components/BackgroundContext.js +3 -0
  258. package/vendor/ink/build/components/BackgroundContext.js.map +1 -0
  259. package/vendor/ink/build/components/Box.d.ts +130 -0
  260. package/vendor/ink/build/components/Box.js +34 -0
  261. package/vendor/ink/build/components/Box.js.map +1 -0
  262. package/vendor/ink/build/components/CursorContext.d.ts +11 -0
  263. package/vendor/ink/build/components/CursorContext.js +8 -0
  264. package/vendor/ink/build/components/CursorContext.js.map +1 -0
  265. package/vendor/ink/build/components/ErrorBoundary.d.ts +18 -0
  266. package/vendor/ink/build/components/ErrorBoundary.js +23 -0
  267. package/vendor/ink/build/components/ErrorBoundary.js.map +1 -0
  268. package/vendor/ink/build/components/ErrorOverview.d.ts +6 -0
  269. package/vendor/ink/build/components/ErrorOverview.js +90 -0
  270. package/vendor/ink/build/components/ErrorOverview.js.map +1 -0
  271. package/vendor/ink/build/components/FocusContext.d.ts +16 -0
  272. package/vendor/ink/build/components/FocusContext.js +17 -0
  273. package/vendor/ink/build/components/FocusContext.js.map +1 -0
  274. package/vendor/ink/build/components/Newline.d.ts +13 -0
  275. package/vendor/ink/build/components/Newline.js +8 -0
  276. package/vendor/ink/build/components/Newline.js.map +1 -0
  277. package/vendor/ink/build/components/Spacer.d.ts +7 -0
  278. package/vendor/ink/build/components/Spacer.js +11 -0
  279. package/vendor/ink/build/components/Spacer.js.map +1 -0
  280. package/vendor/ink/build/components/Static.d.ts +24 -0
  281. package/vendor/ink/build/components/Static.js +28 -0
  282. package/vendor/ink/build/components/Static.js.map +1 -0
  283. package/vendor/ink/build/components/StderrContext.d.ts +15 -0
  284. package/vendor/ink/build/components/StderrContext.js +13 -0
  285. package/vendor/ink/build/components/StderrContext.js.map +1 -0
  286. package/vendor/ink/build/components/StdinContext.d.ts +28 -0
  287. package/vendor/ink/build/components/StdinContext.js +20 -0
  288. package/vendor/ink/build/components/StdinContext.js.map +1 -0
  289. package/vendor/ink/build/components/StdoutContext.d.ts +15 -0
  290. package/vendor/ink/build/components/StdoutContext.js +13 -0
  291. package/vendor/ink/build/components/StdoutContext.js.map +1 -0
  292. package/vendor/ink/build/components/Text.d.ts +55 -0
  293. package/vendor/ink/build/components/Text.js +50 -0
  294. package/vendor/ink/build/components/Text.js.map +1 -0
  295. package/vendor/ink/build/components/Transform.d.ts +16 -0
  296. package/vendor/ink/build/components/Transform.js +15 -0
  297. package/vendor/ink/build/components/Transform.js.map +1 -0
  298. package/vendor/ink/build/cursor-helpers.d.ts +39 -0
  299. package/vendor/ink/build/cursor-helpers.js +62 -0
  300. package/vendor/ink/build/cursor-helpers.js.map +1 -0
  301. package/vendor/ink/build/devtools-window-polyfill.d.ts +1 -0
  302. package/vendor/ink/build/devtools-window-polyfill.js +68 -0
  303. package/vendor/ink/build/devtools-window-polyfill.js.map +1 -0
  304. package/vendor/ink/build/devtools.d.ts +1 -0
  305. package/vendor/ink/build/devtools.js +36 -0
  306. package/vendor/ink/build/devtools.js.map +1 -0
  307. package/vendor/ink/build/dom.d.ts +62 -0
  308. package/vendor/ink/build/dom.js +143 -0
  309. package/vendor/ink/build/dom.js.map +1 -0
  310. package/vendor/ink/build/get-max-width.d.ts +3 -0
  311. package/vendor/ink/build/get-max-width.js +10 -0
  312. package/vendor/ink/build/get-max-width.js.map +1 -0
  313. package/vendor/ink/build/hooks/use-animation.d.ts +49 -0
  314. package/vendor/ink/build/hooks/use-animation.js +87 -0
  315. package/vendor/ink/build/hooks/use-animation.js.map +1 -0
  316. package/vendor/ink/build/hooks/use-app.d.ts +5 -0
  317. package/vendor/ink/build/hooks/use-app.js +8 -0
  318. package/vendor/ink/build/hooks/use-app.js.map +1 -0
  319. package/vendor/ink/build/hooks/use-box-metrics.d.ts +59 -0
  320. package/vendor/ink/build/hooks/use-box-metrics.js +81 -0
  321. package/vendor/ink/build/hooks/use-box-metrics.js.map +1 -0
  322. package/vendor/ink/build/hooks/use-cursor.d.ts +12 -0
  323. package/vendor/ink/build/hooks/use-cursor.js +29 -0
  324. package/vendor/ink/build/hooks/use-cursor.js.map +1 -0
  325. package/vendor/ink/build/hooks/use-focus-manager.d.ts +43 -0
  326. package/vendor/ink/build/hooks/use-focus-manager.js +18 -0
  327. package/vendor/ink/build/hooks/use-focus-manager.js.map +1 -0
  328. package/vendor/ink/build/hooks/use-focus.d.ts +30 -0
  329. package/vendor/ink/build/hooks/use-focus.js +43 -0
  330. package/vendor/ink/build/hooks/use-focus.js.map +1 -0
  331. package/vendor/ink/build/hooks/use-input.d.ts +132 -0
  332. package/vendor/ink/build/hooks/use-input.js +126 -0
  333. package/vendor/ink/build/hooks/use-input.js.map +1 -0
  334. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.d.ts +6 -0
  335. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js +12 -0
  336. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js.map +1 -0
  337. package/vendor/ink/build/hooks/use-paste.d.ts +35 -0
  338. package/vendor/ink/build/hooks/use-paste.js +62 -0
  339. package/vendor/ink/build/hooks/use-paste.js.map +1 -0
  340. package/vendor/ink/build/hooks/use-stderr.d.ts +5 -0
  341. package/vendor/ink/build/hooks/use-stderr.js +8 -0
  342. package/vendor/ink/build/hooks/use-stderr.js.map +1 -0
  343. package/vendor/ink/build/hooks/use-stdin.d.ts +7 -0
  344. package/vendor/ink/build/hooks/use-stdin.js +9 -0
  345. package/vendor/ink/build/hooks/use-stdin.js.map +1 -0
  346. package/vendor/ink/build/hooks/use-stdout.d.ts +5 -0
  347. package/vendor/ink/build/hooks/use-stdout.js +8 -0
  348. package/vendor/ink/build/hooks/use-stdout.js.map +1 -0
  349. package/vendor/ink/build/hooks/use-window-size.d.ts +18 -0
  350. package/vendor/ink/build/hooks/use-window-size.js +22 -0
  351. package/vendor/ink/build/hooks/use-window-size.js.map +1 -0
  352. package/vendor/ink/build/index.d.ts +42 -0
  353. package/vendor/ink/build/index.js +24 -0
  354. package/vendor/ink/build/index.js.map +1 -0
  355. package/vendor/ink/build/ink.d.ts +146 -0
  356. package/vendor/ink/build/ink.js +1022 -0
  357. package/vendor/ink/build/ink.js.map +1 -0
  358. package/vendor/ink/build/input-parser.d.ts +10 -0
  359. package/vendor/ink/build/input-parser.js +194 -0
  360. package/vendor/ink/build/input-parser.js.map +1 -0
  361. package/vendor/ink/build/instances.d.ts +3 -0
  362. package/vendor/ink/build/instances.js +8 -0
  363. package/vendor/ink/build/instances.js.map +1 -0
  364. package/vendor/ink/build/kitty-keyboard.d.ts +23 -0
  365. package/vendor/ink/build/kitty-keyboard.js +32 -0
  366. package/vendor/ink/build/kitty-keyboard.js.map +1 -0
  367. package/vendor/ink/build/log-update.d.ts +20 -0
  368. package/vendor/ink/build/log-update.js +261 -0
  369. package/vendor/ink/build/log-update.js.map +1 -0
  370. package/vendor/ink/build/measure-element.d.ts +20 -0
  371. package/vendor/ink/build/measure-element.js +13 -0
  372. package/vendor/ink/build/measure-element.js.map +1 -0
  373. package/vendor/ink/build/measure-text.d.ts +6 -0
  374. package/vendor/ink/build/measure-text.js +21 -0
  375. package/vendor/ink/build/measure-text.js.map +1 -0
  376. package/vendor/ink/build/output.d.ts +35 -0
  377. package/vendor/ink/build/output.js +328 -0
  378. package/vendor/ink/build/output.js.map +1 -0
  379. package/vendor/ink/build/parse-keypress.d.ts +20 -0
  380. package/vendor/ink/build/parse-keypress.js +495 -0
  381. package/vendor/ink/build/parse-keypress.js.map +1 -0
  382. package/vendor/ink/build/reconciler.d.ts +4 -0
  383. package/vendor/ink/build/reconciler.js +306 -0
  384. package/vendor/ink/build/reconciler.js.map +1 -0
  385. package/vendor/ink/build/render-background.d.ts +4 -0
  386. package/vendor/ink/build/render-background.js +25 -0
  387. package/vendor/ink/build/render-background.js.map +1 -0
  388. package/vendor/ink/build/render-border.d.ts +4 -0
  389. package/vendor/ink/build/render-border.js +84 -0
  390. package/vendor/ink/build/render-border.js.map +1 -0
  391. package/vendor/ink/build/render-node-to-output.d.ts +14 -0
  392. package/vendor/ink/build/render-node-to-output.js +162 -0
  393. package/vendor/ink/build/render-node-to-output.js.map +1 -0
  394. package/vendor/ink/build/render-to-string.d.ts +38 -0
  395. package/vendor/ink/build/render-to-string.js +116 -0
  396. package/vendor/ink/build/render-to-string.js.map +1 -0
  397. package/vendor/ink/build/render.d.ts +176 -0
  398. package/vendor/ink/build/render.js +71 -0
  399. package/vendor/ink/build/render.js.map +1 -0
  400. package/vendor/ink/build/renderer.d.ts +8 -0
  401. package/vendor/ink/build/renderer.js +64 -0
  402. package/vendor/ink/build/renderer.js.map +1 -0
  403. package/vendor/ink/build/sanitize-ansi.d.ts +2 -0
  404. package/vendor/ink/build/sanitize-ansi.js +27 -0
  405. package/vendor/ink/build/sanitize-ansi.js.map +1 -0
  406. package/vendor/ink/build/squash-text-nodes.d.ts +3 -0
  407. package/vendor/ink/build/squash-text-nodes.js +36 -0
  408. package/vendor/ink/build/squash-text-nodes.js.map +1 -0
  409. package/vendor/ink/build/styles.d.ts +302 -0
  410. package/vendor/ink/build/styles.js +303 -0
  411. package/vendor/ink/build/styles.js.map +1 -0
  412. package/vendor/ink/build/utils.d.ts +9 -0
  413. package/vendor/ink/build/utils.js +19 -0
  414. package/vendor/ink/build/utils.js.map +1 -0
  415. package/vendor/ink/build/wrap-text.d.ts +3 -0
  416. package/vendor/ink/build/wrap-text.js +38 -0
  417. package/vendor/ink/build/wrap-text.js.map +1 -0
  418. package/vendor/ink/build/write-synchronized.d.ts +4 -0
  419. package/vendor/ink/build/write-synchronized.js +9 -0
  420. package/vendor/ink/build/write-synchronized.js.map +1 -0
  421. package/vendor/ink/license +10 -0
  422. package/vendor/ink/package.json +137 -0
  423. package/.claude-plugin/marketplace.json +0 -34
  424. package/.claude-plugin/plugin.json +0 -20
  425. package/.gitattributes +0 -34
  426. package/.mcp.json +0 -14
  427. package/ARCHITECTURE.md +0 -77
  428. package/CHANGELOG.md +0 -30
  429. package/CONTRIBUTING.md +0 -45
  430. package/DATA-FLOW.md +0 -79
  431. package/LICENSE +0 -21
  432. package/SECURITY.md +0 -138
  433. package/UNINSTALL.md +0 -115
  434. package/agents/maintenance.md +0 -5
  435. package/agents/memory-classification.md +0 -30
  436. package/agents/scheduler-task.md +0 -18
  437. package/agents/webhook-handler.md +0 -27
  438. package/agents/worker.md +0 -24
  439. package/bin/bridge +0 -133
  440. package/bin/statusline-launcher.mjs +0 -82
  441. package/bin/statusline-lib.mjs +0 -581
  442. package/bin/statusline-route.mjs +0 -273
  443. package/bin/statusline.mjs +0 -638
  444. package/bun.lock +0 -927
  445. package/commands/config.md +0 -16
  446. package/commands/doctor.md +0 -13
  447. package/commands/model.md +0 -61
  448. package/commands/setup.md +0 -17
  449. package/defaults/hidden-roles.json +0 -68
  450. package/defaults/memory-chunk-prompt.md +0 -63
  451. package/defaults/mixdog-config.template.json +0 -27
  452. package/defaults/user-workflow.json +0 -8
  453. package/defaults/user-workflow.md +0 -17
  454. package/hooks/hooks.json +0 -73
  455. package/hooks/lib/active-instance.cjs +0 -77
  456. package/hooks/lib/permission-evaluator.cjs +0 -411
  457. package/hooks/lib/permission-route.cjs +0 -63
  458. package/hooks/lib/settings-loader.cjs +0 -117
  459. package/hooks/post-tool-use.cjs +0 -84
  460. package/hooks/pre-mcp-sandbox.cjs +0 -158
  461. package/hooks/pre-tool-subagent.cjs +0 -258
  462. package/hooks/session-start.cjs +0 -1493
  463. package/hooks/shim-launcher.cjs +0 -65
  464. package/hooks/turn-timer.cjs +0 -82
  465. package/lib/claude-md-writer.cjs +0 -386
  466. package/lib/keychain-cjs.cjs +0 -290
  467. package/lib/plugin-paths.cjs +0 -69
  468. package/lib/rules-builder.cjs +0 -241
  469. package/native/README.md +0 -117
  470. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  471. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  472. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  473. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  474. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  475. package/prompts/code-review.txt +0 -16
  476. package/prompts/security-audit.txt +0 -17
  477. package/rules/bridge/00-common.md +0 -39
  478. package/rules/bridge/20-skip-protocol.md +0 -18
  479. package/rules/bridge/30-explorer.md +0 -33
  480. package/rules/bridge/40-cycle1-agent.md +0 -52
  481. package/rules/bridge/41-cycle2-agent.md +0 -62
  482. package/rules/lead/00-tool-lead.md +0 -61
  483. package/rules/lead/01-general.md +0 -26
  484. package/rules/lead/02-channels.md +0 -49
  485. package/rules/lead/03-team.md +0 -27
  486. package/rules/lead/04-workflow.md +0 -20
  487. package/rules/shared/00-language.md +0 -14
  488. package/rules/shared/01-tool.md +0 -138
  489. package/scripts/bootstrap.mjs +0 -130
  490. package/scripts/bridge-unify-smoke.mjs +0 -308
  491. package/scripts/build-runtime-linux.sh +0 -348
  492. package/scripts/build-runtime-macos.sh +0 -217
  493. package/scripts/build-runtime-windows.ps1 +0 -242
  494. package/scripts/builtin-utils-smoke.mjs +0 -398
  495. package/scripts/bump.mjs +0 -80
  496. package/scripts/check-json.mjs +0 -45
  497. package/scripts/check-syntax-changed.mjs +0 -102
  498. package/scripts/check-syntax.mjs +0 -58
  499. package/scripts/code-graph-batch.test.mjs +0 -33
  500. package/scripts/config-preserve-smoke.mjs +0 -180
  501. package/scripts/doctor.mjs +0 -489
  502. package/scripts/edit-normalize-fuzz.mjs +0 -130
  503. package/scripts/edit-normalize-smoke.mjs +0 -401
  504. package/scripts/edit-operation-smoke.mjs +0 -369
  505. package/scripts/edit2-smoke.mjs +0 -63
  506. package/scripts/ensure-deps.mjs +0 -259
  507. package/scripts/fuzzy-e2e.mjs +0 -28
  508. package/scripts/fuzzy-smoke.mjs +0 -26
  509. package/scripts/gateway-model.mjs +0 -596
  510. package/scripts/generate-runtime-manifest.mjs +0 -166
  511. package/scripts/guard-smoke.mjs +0 -66
  512. package/scripts/hidden-role-schema-smoke.mjs +0 -162
  513. package/scripts/hook-routing-smoke.mjs +0 -29
  514. package/scripts/inject-input.ps1 +0 -204
  515. package/scripts/io-complex-smoke.mjs +0 -667
  516. package/scripts/io-explore-bench.mjs +0 -424
  517. package/scripts/io-guardrails-smoke.mjs +0 -205
  518. package/scripts/io-mini-bench-baseline.json +0 -11
  519. package/scripts/io-mini-bench.mjs +0 -216
  520. package/scripts/io-route-harness.mjs +0 -933
  521. package/scripts/io-telemetry-report.mjs +0 -691
  522. package/scripts/lib/gateway-inventory.mjs +0 -178
  523. package/scripts/lib/gateway-settings.mjs +0 -78
  524. package/scripts/mutation-bench.mjs +0 -564
  525. package/scripts/mutation-io-smoke.mjs +0 -1097
  526. package/scripts/native-patch-bridge-smoke.mjs +0 -288
  527. package/scripts/native-patch-smoke.mjs +0 -304
  528. package/scripts/openai-oauth-catalog-smoke.mjs +0 -53
  529. package/scripts/patch-interior-context-smoke.mjs +0 -49
  530. package/scripts/patch-newline-utf8-smoke.mjs +0 -157
  531. package/scripts/perf-hook-smoke.mjs +0 -71
  532. package/scripts/permission-eval-smoke.mjs +0 -443
  533. package/scripts/prep-patch.mjs +0 -53
  534. package/scripts/prep-shim.mjs +0 -96
  535. package/scripts/provider-cache-smoke.mjs +0 -687
  536. package/scripts/report-runtime-health.mjs +0 -132
  537. package/scripts/resolve-bun.mjs +0 -60
  538. package/scripts/run-mcp.mjs +0 -1473
  539. package/scripts/salvage-v4a-shatter.test.mjs +0 -58
  540. package/scripts/scoped-cache-io-smoke.mjs +0 -103
  541. package/scripts/shell-policy-round3-smoke.mjs +0 -46
  542. package/scripts/smoke-runtime-negative.ps1 +0 -100
  543. package/scripts/smoke-runtime-negative.sh +0 -95
  544. package/scripts/stall-policy-smoke.mjs +0 -50
  545. package/scripts/start-memory-worker.mjs +0 -23
  546. package/scripts/statusline-launcher-smoke.mjs +0 -235
  547. package/scripts/stress-atomic-write.mjs +0 -1028
  548. package/scripts/test-fault-inject.mjs +0 -164
  549. package/scripts/test-large-file.mjs +0 -174
  550. package/scripts/tool-edge-smoke.mjs +0 -209
  551. package/scripts/uninstall.mjs +0 -238
  552. package/scripts/webhook-selfheal-smoke.mjs +0 -27
  553. package/scripts/write-overwrite-guard-smoke.mjs +0 -56
  554. package/server-main.mjs +0 -3350
  555. package/server.mjs +0 -468
  556. package/setup/config-merge.mjs +0 -246
  557. package/setup/install.mjs +0 -574
  558. package/setup/launch-core.mjs +0 -617
  559. package/setup/launch.mjs +0 -101
  560. package/setup/locate-claude.mjs +0 -56
  561. package/setup/mixdog-cli.mjs +0 -122
  562. package/setup/setup-server.mjs +0 -3305
  563. package/setup/setup.html +0 -3740
  564. package/setup/tui.mjs +0 -325
  565. package/skills/retro-skill-proposer/SKILL.md +0 -92
  566. package/skills/schedule-add/SKILL.md +0 -77
  567. package/skills/setup/SKILL.md +0 -356
  568. package/skills/webhook-add/SKILL.md +0 -81
  569. package/src/agent/bridge-stall-watchdog.mjs +0 -337
  570. package/src/agent/index.mjs +0 -2229
  571. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +0 -1010
  572. package/src/agent/orchestrator/bridge-retry.mjs +0 -220
  573. package/src/agent/orchestrator/bridge-trace.mjs +0 -601
  574. package/src/agent/orchestrator/cache-mtime.mjs +0 -58
  575. package/src/agent/orchestrator/config.mjs +0 -405
  576. package/src/agent/orchestrator/context/collect.mjs +0 -651
  577. package/src/agent/orchestrator/dispatch-persist.mjs +0 -549
  578. package/src/agent/orchestrator/drain-registry.mjs +0 -50
  579. package/src/agent/orchestrator/explore-validator.mjs +0 -8
  580. package/src/agent/orchestrator/internal-roles.mjs +0 -118
  581. package/src/agent/orchestrator/internal-tools.mjs +0 -88
  582. package/src/agent/orchestrator/jobs.mjs +0 -116
  583. package/src/agent/orchestrator/mcp/client.mjs +0 -364
  584. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +0 -1884
  585. package/src/agent/orchestrator/providers/anthropic.mjs +0 -598
  586. package/src/agent/orchestrator/providers/gemini.mjs +0 -1530
  587. package/src/agent/orchestrator/providers/grok-oauth.mjs +0 -779
  588. package/src/agent/orchestrator/providers/model-catalog.mjs +0 -374
  589. package/src/agent/orchestrator/providers/openai-compat-stream.mjs +0 -366
  590. package/src/agent/orchestrator/providers/openai-compat.mjs +0 -1511
  591. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -1891
  592. package/src/agent/orchestrator/providers/openai-oauth.mjs +0 -1456
  593. package/src/agent/orchestrator/providers/openai-ws.mjs +0 -127
  594. package/src/agent/orchestrator/providers/registry.mjs +0 -192
  595. package/src/agent/orchestrator/providers/retry-classifier.mjs +0 -325
  596. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +0 -142
  597. package/src/agent/orchestrator/session/cache/read-cache.mjs +0 -319
  598. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +0 -361
  599. package/src/agent/orchestrator/session/loop.mjs +0 -1619
  600. package/src/agent/orchestrator/session/manager.mjs +0 -1991
  601. package/src/agent/orchestrator/session/result-classification.mjs +0 -65
  602. package/src/agent/orchestrator/session/store.mjs +0 -632
  603. package/src/agent/orchestrator/session/stream-watchdog.mjs +0 -130
  604. package/src/agent/orchestrator/session/tool-result-offload.mjs +0 -166
  605. package/src/agent/orchestrator/session/trim.mjs +0 -491
  606. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +0 -115
  607. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +0 -331
  608. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +0 -150
  609. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +0 -228
  610. package/src/agent/orchestrator/smart-bridge/index.mjs +0 -215
  611. package/src/agent/orchestrator/smart-bridge/profiles.mjs +0 -37
  612. package/src/agent/orchestrator/smart-bridge/registry.mjs +0 -348
  613. package/src/agent/orchestrator/stall-policy.mjs +0 -201
  614. package/src/agent/orchestrator/tool-loop-guard.mjs +0 -75
  615. package/src/agent/orchestrator/tools/bash-session.mjs +0 -722
  616. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +0 -511
  617. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +0 -480
  618. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +0 -256
  619. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +0 -58
  620. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +0 -240
  621. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +0 -113
  622. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +0 -74
  623. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +0 -242
  624. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +0 -211
  625. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +0 -1364
  626. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +0 -126
  627. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +0 -141
  628. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +0 -194
  629. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +0 -60
  630. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +0 -168
  631. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +0 -173
  632. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +0 -48
  633. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -99
  634. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +0 -193
  635. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +0 -597
  636. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  637. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +0 -300
  638. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +0 -152
  639. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +0 -118
  640. package/src/agent/orchestrator/tools/builtin/read-open.mjs +0 -190
  641. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +0 -728
  642. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +0 -173
  643. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +0 -602
  644. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +0 -196
  645. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +0 -422
  646. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +0 -158
  647. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +0 -869
  648. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +0 -962
  649. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +0 -223
  650. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +0 -206
  651. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +0 -401
  652. package/src/agent/orchestrator/tools/builtin.mjs +0 -503
  653. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +0 -24
  654. package/src/agent/orchestrator/tools/code-graph.mjs +0 -4095
  655. package/src/agent/orchestrator/tools/cwd-tool.mjs +0 -298
  656. package/src/agent/orchestrator/tools/destructive-warning.mjs +0 -323
  657. package/src/agent/orchestrator/tools/edit-normalize.mjs +0 -603
  658. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +0 -154
  659. package/src/agent/orchestrator/tools/graph-manifest.json +0 -26
  660. package/src/agent/orchestrator/tools/host-input.mjs +0 -204
  661. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +0 -143
  662. package/src/agent/orchestrator/tools/patch-manifest.json +0 -26
  663. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +0 -20
  664. package/src/agent/orchestrator/tools/patch.mjs +0 -2754
  665. package/src/agent/orchestrator/tools/progress-message.mjs +0 -118
  666. package/src/agent/orchestrator/tools/shell-command.mjs +0 -865
  667. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +0 -7
  668. package/src/agent/orchestrator/tools/shell-snapshot.mjs +0 -313
  669. package/src/agent/orchestrator/workflow-store.mjs +0 -93
  670. package/src/agent/tool-defs.mjs +0 -110
  671. package/src/channels/backends/discord.mjs +0 -784
  672. package/src/channels/data/voice-runtime-manifest.json +0 -138
  673. package/src/channels/index.mjs +0 -3268
  674. package/src/channels/lib/config.mjs +0 -292
  675. package/src/channels/lib/drop-trace.mjs +0 -71
  676. package/src/channels/lib/event-pipeline.mjs +0 -81
  677. package/src/channels/lib/holidays.mjs +0 -138
  678. package/src/channels/lib/hook-pipe-server.mjs +0 -822
  679. package/src/channels/lib/output-forwarder.mjs +0 -765
  680. package/src/channels/lib/runtime-paths.mjs +0 -552
  681. package/src/channels/lib/scheduler.mjs +0 -723
  682. package/src/channels/lib/session-discovery.mjs +0 -103
  683. package/src/channels/lib/state-file.mjs +0 -68
  684. package/src/channels/lib/status-snapshot.mjs +0 -219
  685. package/src/channels/lib/tool-format.mjs +0 -140
  686. package/src/channels/lib/transcript-discovery.mjs +0 -195
  687. package/src/channels/lib/voice-runtime-fetcher.mjs +0 -734
  688. package/src/channels/lib/webhook.mjs +0 -1318
  689. package/src/channels/tool-defs.mjs +0 -170
  690. package/src/daemon/host.mjs +0 -118
  691. package/src/daemon/mcp-transport.mjs +0 -47
  692. package/src/daemon/session.mjs +0 -100
  693. package/src/daemon/thin-client.mjs +0 -71
  694. package/src/daemon/transport.mjs +0 -163
  695. package/src/gateway/claude-current.mjs +0 -255
  696. package/src/gateway/oauth-usage.mjs +0 -598
  697. package/src/gateway/route-meta.mjs +0 -629
  698. package/src/gateway/server.mjs +0 -713
  699. package/src/memory/data/runtime-manifest.json +0 -40
  700. package/src/memory/index.mjs +0 -3332
  701. package/src/memory/lib/core-memory-store.mjs +0 -330
  702. package/src/memory/lib/embedding-provider.mjs +0 -269
  703. package/src/memory/lib/embedding-worker.mjs +0 -323
  704. package/src/memory/lib/memory-cycle1.mjs +0 -645
  705. package/src/memory/lib/memory-cycle2.mjs +0 -1284
  706. package/src/memory/lib/memory-cycle3.mjs +0 -540
  707. package/src/memory/lib/memory-embed.mjs +0 -299
  708. package/src/memory/lib/memory-ops-policy.mjs +0 -190
  709. package/src/memory/lib/memory-recall-store.mjs +0 -638
  710. package/src/memory/lib/memory.mjs +0 -412
  711. package/src/memory/lib/pg/adapter.mjs +0 -308
  712. package/src/memory/lib/pg/process.mjs +0 -360
  713. package/src/memory/lib/pg/supervisor.mjs +0 -396
  714. package/src/memory/lib/runtime-fetcher.mjs +0 -458
  715. package/src/memory/lib/trace-store.mjs +0 -728
  716. package/src/memory/tool-defs.mjs +0 -79
  717. package/src/search/index.mjs +0 -1173
  718. package/src/search/lib/backends/anthropic-oauth.mjs +0 -98
  719. package/src/search/lib/backends/exa.mjs +0 -50
  720. package/src/search/lib/backends/firecrawl.mjs +0 -61
  721. package/src/search/lib/backends/gemini-api.mjs +0 -83
  722. package/src/search/lib/backends/grok-oauth.mjs +0 -86
  723. package/src/search/lib/backends/index.mjs +0 -150
  724. package/src/search/lib/backends/openai-api.mjs +0 -144
  725. package/src/search/lib/backends/openai-oauth.mjs +0 -102
  726. package/src/search/lib/backends/openai-web-search.mjs +0 -76
  727. package/src/search/lib/backends/tavily.mjs +0 -55
  728. package/src/search/lib/backends/xai-api.mjs +0 -113
  729. package/src/search/lib/config.mjs +0 -192
  730. package/src/search/lib/provider-usage.mjs +0 -67
  731. package/src/search/lib/providers.mjs +0 -47
  732. package/src/search/lib/search-intent.mjs +0 -109
  733. package/src/search/lib/setup-handler.mjs +0 -261
  734. package/src/search/lib/web-tools.mjs +0 -1219
  735. package/src/search/tool-defs.mjs +0 -83
  736. package/src/setup/defender-exclusion.mjs +0 -183
  737. package/src/shared/atomic-file.mjs +0 -436
  738. package/src/shared/config.mjs +0 -372
  739. package/src/shared/daemon-recycle.mjs +0 -108
  740. package/src/shared/disable-claude-builtins.mjs +0 -91
  741. package/src/shared/err-text.mjs +0 -12
  742. package/src/shared/llm/http-agent.mjs +0 -123
  743. package/src/shared/open-url.mjs +0 -62
  744. package/src/shared/plugin-paths.mjs +0 -58
  745. package/src/shared/schedules-store.mjs +0 -70
  746. package/src/shared/seed.mjs +0 -161
  747. package/src/shared/user-cwd.mjs +0 -225
  748. package/src/shared/user-data-guard.mjs +0 -244
  749. package/src/status/aggregator.mjs +0 -584
  750. package/src/status/server.mjs +0 -413
  751. package/tools.json +0 -1671
  752. /package/{defaults → src/defaults}/cycle3-review-prompt.md +0 -0
  753. /package/{defaults → src/defaults}/memory-promote-prompt.md +0 -0
  754. /package/{hooks → src/hooks}/lib/permission-rules.cjs +0 -0
  755. /package/{lib → src/lib}/config-cjs.cjs +0 -0
  756. /package/{lib → src/lib}/hook-pipe-path.cjs +0 -0
  757. /package/{lib → src/lib}/mixdog-debug.cjs +0 -0
  758. /package/{lib → src/lib}/text-utils.cjs +0 -0
  759. /package/{rules → src/rules}/bridge/42-cycle3-agent.md +0 -0
  760. /package/src/{agent → runtime/agent}/orchestrator/activity-bus.mjs +0 -0
  761. /package/src/{agent → runtime/agent}/orchestrator/providers/anthropic-betas.mjs +0 -0
  762. /package/src/{agent → runtime/agent}/orchestrator/session/abort-lookup.mjs +0 -0
  763. /package/src/{agent → runtime/agent}/orchestrator/session/cache/post-edit-marks.mjs +0 -0
  764. /package/src/{agent → runtime/agent}/orchestrator/session/cache/scoped-cache-outcome.mjs +0 -0
  765. /package/src/{agent → runtime/agent}/orchestrator/session/cache/util.mjs +0 -0
  766. /package/src/{agent → runtime/agent}/orchestrator/session/read-dedup.mjs +0 -0
  767. /package/src/{agent → runtime/agent}/orchestrator/session/save-session-worker.mjs +0 -0
  768. /package/src/{agent → runtime/agent}/orchestrator/smart-bridge/session-builder.mjs +0 -0
  769. /package/src/{agent → runtime/agent}/orchestrator/tools/bash-policy-scan.mjs +0 -0
  770. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/advisory-lock.mjs +0 -0
  771. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/atomic-write.mjs +0 -0
  772. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/binary-file.mjs +0 -0
  773. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cache-layers.mjs +0 -0
  774. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cwd-utils.mjs +0 -0
  775. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/device-paths.mjs +0 -0
  776. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -0
  777. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diff-utils.mjs +0 -0
  778. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/fs-reachability.mjs +0 -0
  779. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/grep-formatting.mjs +0 -0
  780. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/hash-utils.mjs +0 -0
  781. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/list-formatting.mjs +0 -0
  782. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/open-config-tool.mjs +0 -0
  783. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-locks.mjs +0 -0
  784. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-utils.mjs +0 -0
  785. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-args.mjs +0 -0
  786. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-batch.mjs +0 -0
  787. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-constants.mjs +0 -0
  788. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image-resize.mjs +0 -0
  789. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image.mjs +0 -0
  790. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-lines.mjs +0 -0
  791. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-mode-tool.mjs +0 -0
  792. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-range-index.mjs +0 -0
  793. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-ranges.mjs +0 -0
  794. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-special-files.mjs +0 -0
  795. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-tool.mjs +0 -0
  796. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-windows.mjs +0 -0
  797. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-analysis.mjs +0 -0
  798. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-output.mjs +0 -0
  799. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-helpers.mjs +0 -0
  800. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-validation.mjs +0 -0
  801. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/text-stats.mjs +0 -0
  802. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/windows-roots.mjs +0 -0
  803. /package/src/{agent → runtime/agent}/orchestrator/tools/code-graph-prewarm-worker.mjs +0 -0
  804. /package/src/{agent → runtime/agent}/orchestrator/tools/env-scrub.mjs +0 -0
  805. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-content-cache.mjs +0 -0
  806. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-planner.mjs +0 -0
  807. /package/src/{agent → runtime/agent}/orchestrator/tools/next-call-utils.mjs +0 -0
  808. /package/src/{agent → runtime/agent}/orchestrator/tools/result-compression.mjs +0 -0
  809. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-exec-policy.mjs +0 -0
  810. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy-danger-target.mjs +0 -0
  811. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy.mjs +0 -0
  812. /package/src/{channels → runtime/channels}/lib/cli-worker-host.mjs +0 -0
  813. /package/src/{channels → runtime/channels}/lib/config-lock.mjs +0 -0
  814. /package/src/{channels → runtime/channels}/lib/event-queue.mjs +0 -0
  815. /package/src/{channels → runtime/channels}/lib/executor.mjs +0 -0
  816. /package/src/{channels → runtime/channels}/lib/format.mjs +0 -0
  817. /package/src/{channels → runtime/channels}/lib/interaction-workflows.mjs +0 -0
  818. /package/src/{channels → runtime/channels}/lib/memory-client.mjs +0 -0
  819. /package/src/{channels → runtime/channels}/lib/session-control.mjs +0 -0
  820. /package/src/{channels → runtime/channels}/lib/settings.mjs +0 -0
  821. /package/src/{channels → runtime/channels}/lib/whisper-server.mjs +0 -0
  822. /package/src/{memory → runtime/memory}/lib/agent-ipc.mjs +0 -0
  823. /package/src/{memory → runtime/memory}/lib/bridge-trace-queries.mjs +0 -0
  824. /package/src/{memory → runtime/memory}/lib/llm-worker-host.mjs +0 -0
  825. /package/src/{memory → runtime/memory}/lib/memory-cycle.mjs +0 -0
  826. /package/src/{memory → runtime/memory}/lib/memory-extraction.mjs +0 -0
  827. /package/src/{memory → runtime/memory}/lib/memory-maintenance-store.mjs +0 -0
  828. /package/src/{memory → runtime/memory}/lib/memory-recall-id-patch.mjs +0 -0
  829. /package/src/{memory → runtime/memory}/lib/memory-recall-read-query.mjs +0 -0
  830. /package/src/{memory → runtime/memory}/lib/memory-recall-scope-filter.mjs +0 -0
  831. /package/src/{memory → runtime/memory}/lib/memory-retrievers.mjs +0 -0
  832. /package/src/{memory → runtime/memory}/lib/memory-score.mjs +0 -0
  833. /package/src/{memory → runtime/memory}/lib/memory-text-utils.mjs +0 -0
  834. /package/src/{memory → runtime/memory}/lib/model-profile.mjs +0 -0
  835. /package/src/{memory → runtime/memory}/lib/project-id-resolver.mjs +0 -0
  836. /package/src/{search → runtime/search}/lib/cache.mjs +0 -0
  837. /package/src/{search → runtime/search}/lib/formatter.mjs +0 -0
  838. /package/src/{search → runtime/search}/lib/state.mjs +0 -0
  839. /package/src/{shared → runtime/shared}/abort-controller.mjs +0 -0
  840. /package/src/{shared → runtime/shared}/llm/cost.mjs +0 -0
  841. /package/src/{shared → runtime/shared}/llm/index.mjs +0 -0
  842. /package/src/{shared → runtime/shared}/llm/pid-cleanup.mjs +0 -0
  843. /package/src/{shared → runtime/shared}/llm/usage-log.mjs +0 -0
  844. /package/src/{shared → runtime/shared}/wsl.mjs +0 -0
@@ -0,0 +1,2104 @@
1
+ /**
2
+ * Anthropic OAuth provider — uses Mixdog-owned local OAuth credentials
3
+ * for Claude Max subscription access.
4
+ *
5
+ * Raw HTTP + SSE streaming, reuses message/tool conversion patterns
6
+ * from anthropic.mjs. Bridge-trace instrumented.
7
+ */
8
+ import { readFileSync, existsSync, statSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { homedir } from 'os';
11
+ import { createServer } from 'http';
12
+ import { randomBytes, createHash } from 'crypto';
13
+ import {
14
+ traceBridgeFetch,
15
+ traceBridgeSse,
16
+ traceBridgeUsage,
17
+ } from '../bridge-trace.mjs';
18
+ import { createAbortController } from '../../../shared/abort-controller.mjs';
19
+ import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
20
+ import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
21
+ import { enrichModels } from './model-catalog.mjs';
22
+ import { makeModelCache } from './model-cache.mjs';
23
+ import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
24
+ import {
25
+ PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
26
+ PROVIDER_RETRY_BACKOFF_MS,
27
+ PROVIDER_RETRY_MAX_ATTEMPTS,
28
+ PROVIDER_SSE_IDLE_TIMEOUT_MS,
29
+ PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
30
+ createPassthroughSignal,
31
+ } from '../stall-policy.mjs';
32
+ import {
33
+ classifyError,
34
+ retryAfterMsFromError,
35
+ withRetry,
36
+ } from './retry-classifier.mjs';
37
+ import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropic-betas.mjs';
38
+ import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
39
+ import { normalizeContentForAnthropic } from './media-normalization.mjs';
40
+
41
+ // --- Model catalog cache helpers ---
42
+ // Disk-backed cache so repeated process starts (cron, tool calls) don't
43
+ // hammer /v1/models. 24h TTL matches the upstream client cadence.
44
+ const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
45
+ // SSE progress emits (per-request "Response …" and "Done:" lines). Off by default.
46
+ const SSE_VERBOSE = process.env.MIXDOG_SSE_VERBOSE === '1';
47
+
48
+ const _modelCache = makeModelCache({
49
+ fileName: 'anthropic-oauth-models.json',
50
+ ttlMs: MODEL_CACHE_TTL_MS,
51
+ onSave: (m) => { _inMemoryCatalog = Array.isArray(m) ? m.slice() : null; },
52
+ });
53
+
54
+ // Async wrappers so callers can keep awaiting; the shared cache CRUD is sync.
55
+ async function _loadModelCache() {
56
+ return _modelCache.loadSync();
57
+ }
58
+
59
+ async function _saveModelCache(models) {
60
+ _modelCache.save(models);
61
+ }
62
+
63
+ // In-memory mirror of the disk catalog — populated on first listModels() and
64
+ // refreshed after every _saveModelCache. Used by _catalogHas and _displayModel
65
+ // so hot paths don't hit disk on every response.
66
+ let _inMemoryCatalog = null;
67
+ let _modelRefreshInFlight = null;
68
+ let _oauthRefreshInFlight = null;
69
+ // No in-memory credential cache: the canonical credentials file is the
70
+ // single source of truth. Cross-process refresh_token rotation by another
71
+ // concurrent reader would invalidate any cached copy here and produce
72
+ // invalid_grant on the next refresh. Reading from
73
+ // disk on demand is cheap (one stat + one small JSON parse) and removes
74
+ // the cache-vs-disk skew entirely.
75
+
76
+
77
+ function _catalogHas(id) {
78
+ if (!id || !Array.isArray(_inMemoryCatalog)) return false;
79
+ return _inMemoryCatalog.some(m => m.id === id);
80
+ }
81
+
82
+ // Display-name normalization for trace / usage. Turns dated or version-alias
83
+ // ids into the version alias form: claude-opus-4-7 → claude-opus-4.7,
84
+ // claude-haiku-4-5-20251001 → claude-haiku-4.5. Falls back to the raw id.
85
+ function _displayModel(id) {
86
+ if (!id || typeof id !== 'string') return id;
87
+ const m = id.match(/^claude-([a-z]+)-(\d+)(?:-(\d+))?(?:-\d{8})?$/i);
88
+ if (!m) return id;
89
+ return `claude-${m[1].toLowerCase()}-${m[2]}${m[3] ? `.${m[3]}` : ''}`;
90
+ }
91
+
92
+ // Classify a model id into our common tier/family shape. Anthropic's catalog
93
+ // mixes dated ids (claude-opus-4-5-20251101), versioned aliases
94
+ // (claude-opus-4-6), and the raw family tokens resolved via env vars.
95
+ function _normalizeAnthropicModel(raw) {
96
+ const id = raw?.id || raw?.name;
97
+ if (!id) return null;
98
+ const familyMatch = id.match(/^claude-([a-z]+)/i);
99
+ const family = familyMatch ? familyMatch[1].toLowerCase() : 'other';
100
+ // Dated: trailing -YYYYMMDD (8 digits).
101
+ const dated = /-\d{8}$/.test(id);
102
+ // Versioned alias: claude-<family>-<major>-<minor>[-...] with no dated suffix.
103
+ const versioned = !dated && /^claude-[a-z]+-\d+(?:-\d+)?$/i.test(id);
104
+ const tier = dated ? 'dated' : versioned ? 'version' : 'family';
105
+ const releaseDate = dated
106
+ ? id.match(/-(\d{4})(\d{2})(\d{2})$/)
107
+ : null;
108
+ return {
109
+ id,
110
+ display: raw?.display_name || _prettyName(id, family),
111
+ family,
112
+ provider: 'anthropic-oauth',
113
+ contextWindow: raw?.context_window || raw?.max_context_window || _defaultContextForModel(id, family),
114
+ tier,
115
+ latest: false, // assigned in a second pass once full list is known
116
+ releaseDate: releaseDate ? `${releaseDate[1]}-${releaseDate[2]}-${releaseDate[3]}` : null,
117
+ };
118
+ }
119
+
120
+ function _prettyName(id, family) {
121
+ const v = id.match(/^claude-[a-z]+-(\d+)(?:-(\d+))?/i);
122
+ const base = family[0].toUpperCase() + family.slice(1);
123
+ return v ? `${base} ${v[1]}${v[2] ? `.${v[2]}` : ''}` : base;
124
+ }
125
+
126
+ function _defaultContextForModel(id, family) {
127
+ const text = String(id || '');
128
+ const version = text.match(/^claude-[a-z]+-(\d+)(?:-(\d+))?/i);
129
+ if (Number(version?.[1] || 0) >= 5) return 1000000;
130
+ if (/^claude-(opus|sonnet)-4-(6|7|8)(?:$|-)/i.test(text)) return 1000000;
131
+ if (family && family !== 'other') return 200000;
132
+ return 200000;
133
+ }
134
+
135
+ // Mark the highest-numbered version per family as `latest: true`. Uses a simple
136
+ // lexicographic comparison on the numeric parts embedded in the id.
137
+ function _markLatestByFamily(models) {
138
+ const byFamily = new Map();
139
+ for (const m of models) {
140
+ if (m.tier !== 'version') continue;
141
+ const cur = byFamily.get(m.family);
142
+ if (!cur || _compareVersion(m.id, cur.id) > 0) {
143
+ byFamily.set(m.family, m);
144
+ }
145
+ }
146
+ for (const m of byFamily.values()) m.latest = true;
147
+ }
148
+
149
+ function _compareVersion(a, b) {
150
+ const na = (a.match(/^claude-[a-z]+-(\d+)(?:-(\d+))?/i) || []).slice(1).map(Number);
151
+ const nb = (b.match(/^claude-[a-z]+-(\d+)(?:-(\d+))?/i) || []).slice(1).map(Number);
152
+ for (let i = 0; i < Math.max(na.length, nb.length); i++) {
153
+ if ((na[i] || 0) !== (nb[i] || 0)) return (na[i] || 0) - (nb[i] || 0);
154
+ }
155
+ return a.localeCompare(b);
156
+ }
157
+
158
+ // Newest HIGH-TIER chat model by version, read from the SYNC in-memory catalog
159
+ // mirror. Symmetric with resolveLatestGrokModel / resolveLatestCodexModel.
160
+ // Anthropic ships three families: opus / sonnet / haiku. "Latest" is the
161
+ // highest version across opus + sonnet only — haiku is the cheap tier and is
162
+ // never the flagship default. Returns null until listModels() populates the
163
+ // mirror; callers must warm the catalog (ensureLatestAnthropicModel) when null.
164
+ export function resolveLatestAnthropicModel() {
165
+ if (!Array.isArray(_inMemoryCatalog)) return null;
166
+ let best = null;
167
+ for (const m of _inMemoryCatalog) {
168
+ if (!m?.id || (m.family !== 'opus' && m.family !== 'sonnet')) continue;
169
+ if (!best || _compareVersion(m.id, best.id) > 0) best = m;
170
+ }
171
+ return best?.id || null;
172
+ }
173
+
174
+ function resolveAnthropicModelAfter404(requested) {
175
+ if (!Array.isArray(_inMemoryCatalog)) return null;
176
+ const wanted = String(requested || '');
177
+ const family = (wanted.match(/^claude-([a-z]+)/i) || [])[1]?.toLowerCase() || null;
178
+ let best = null;
179
+ for (const m of _inMemoryCatalog) {
180
+ if (!m?.id) continue;
181
+ if (family && m.family !== family) continue;
182
+ if (!family && m.family !== 'opus' && m.family !== 'sonnet') continue;
183
+ if (!best || _compareVersion(m.id, best.id) > 0) best = m;
184
+ }
185
+ if (best?.id && best.id !== wanted) return best.id;
186
+ if (family === 'opus') {
187
+ const flagship = resolveLatestAnthropicModel();
188
+ if (flagship && flagship !== wanted) return flagship;
189
+ }
190
+ return null;
191
+ }
192
+
193
+ export async function ensureLatestAnthropicModel(provider) {
194
+ let m = resolveLatestAnthropicModel();
195
+ if (m) return m;
196
+ await provider._refreshModelCache();
197
+ m = resolveLatestAnthropicModel();
198
+ if (m) return m;
199
+ throw new Error('[anthropic-oauth] model catalog unavailable after warmup — cannot resolve default model');
200
+ }
201
+
202
+ const API_URL = 'https://api.anthropic.com/v1/messages';
203
+ // SSRF guard for the OAuth token endpoint override. Env-supplied URLs must be
204
+ // https with a valid http(s) URL shape; reject file:/data:/ftp:/etc. and any
205
+ // http override so a hostile env cannot redirect refresh-token requests.
206
+ function assertSafeTokenURL(rawURL) {
207
+ let parsed;
208
+ try {
209
+ parsed = new URL(String(rawURL));
210
+ } catch {
211
+ throw new Error(`[anthropic-oauth] invalid ANTHROPIC_OAUTH_TOKEN_URL: ${rawURL}`);
212
+ }
213
+ if (parsed.protocol.toLowerCase() !== 'https:') {
214
+ throw new Error(`[anthropic-oauth] ANTHROPIC_OAUTH_TOKEN_URL must use https (got ${parsed.protocol})`);
215
+ }
216
+ return rawURL;
217
+ }
218
+ const TOKEN_URL = assertSafeTokenURL(process.env.ANTHROPIC_OAUTH_TOKEN_URL || 'https://console.anthropic.com/v1/oauth/token');
219
+ const ANTHROPIC_VERSION = '2023-06-01';
220
+ const DEFAULT_CREDENTIALS_PATH = join(resolvePluginData(), 'anthropic-oauth-credentials.json');
221
+ const CLAUDE_CODE_CREDENTIALS_PATH = join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'), '.credentials.json');
222
+ const CLAUDE_CODE_CLIENT_ID = process.env.ANTHROPIC_OAUTH_CLIENT_ID || '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
223
+ const TOKEN_REFRESH_SKEW_MS = 5 * 60_000;
224
+ const CLAUDE_AI_AUTHORIZE_URL = 'https://claude.com/cai/oauth/authorize';
225
+ const ALL_OAUTH_SCOPES = [
226
+ 'org:create_api_key',
227
+ 'user:profile',
228
+ 'user:inference',
229
+ 'user:sessions:claude_code',
230
+ 'user:mcp_servers',
231
+ 'user:file_upload',
232
+ ];
233
+ const OAUTH_LOGIN_SCOPE = ALL_OAUTH_SCOPES.join(' ');
234
+ const OAUTH_CALLBACK_HOST = 'localhost';
235
+ const OAUTH_CALLBACK_PORT = 54545;
236
+ const OAUTH_CALLBACK_PATH = '/callback';
237
+ const OAUTH_REDIRECT_URI = `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`;
238
+ const OAUTH_LOGIN_TIMEOUT_MS = 5 * 60_000;
239
+ const OAUTH_TOKEN_TIMEOUT_MS = 30_000;
240
+
241
+ // Anthropic OAuth contract for first-party Claude Code clients.
242
+ // Opus/Sonnet requests are gated on a specific system-prompt prefix.
243
+ // Mixdog keeps that upstream client contract for OAuth routing. Haiku is not
244
+ // gated and ignores this prefix.
245
+ const CLAUDE_CODE_SYSTEM_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude.";
246
+ const OAUTH_BETA_HEADERS = 'oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,extended-cache-ttl-2025-04-11';
247
+ const DEFAULT_CLI_VERSION = '2.1.77';
248
+
249
+ function resolveCliVersion() {
250
+ return process.env.MIXDOG_CLI_VERSION
251
+ || DEFAULT_CLI_VERSION;
252
+ }
253
+
254
+ function requiresSystemPrefix(model) {
255
+ // Opus / Sonnet require the Claude Code system prefix when authenticated
256
+ // via OAuth. Haiku does not.
257
+ return /^claude-(opus|sonnet)/i.test(String(model || ''));
258
+ }
259
+
260
+ // OAuth rate-limit pool routing is gated by the server inspecting the first
261
+ // system block. When it reads exactly "You are Claude Code, Anthropic's
262
+ // official CLI for Claude." it routes into the Claude Code pool; any other
263
+ // content (even the prefix concatenated with extra text in the same block)
264
+ // falls into the standard pool and Opus/Sonnet return 429. Splitting into
265
+ // two blocks — [prefix, rest] — keeps both routing and user instructions.
266
+ function buildSystemBlocks(systemText, model, cacheControl, maxCacheBlocks = 2) {
267
+ // systemText is an array of strings — each element becomes its own Anthropic
268
+ // content block with its own cache_control breakpoint (BP1 + BP2).
269
+ // Invariant: callers must pass an array; scalar strings are not accepted.
270
+ const texts = Array.isArray(systemText)
271
+ ? systemText.map(s => typeof s === 'string' ? s.trim() : '').filter(Boolean)
272
+ : [];
273
+ const gated = requiresSystemPrefix(model);
274
+
275
+ const blocks = [];
276
+ if (gated) {
277
+ blocks.push({ type: 'text', text: CLAUDE_CODE_SYSTEM_PREFIX });
278
+ }
279
+ for (let i = 0; i < texts.length; i++) {
280
+ let body = texts[i];
281
+ // Strip a duplicated Claude Code prefix from the first block if present.
282
+ if (gated && i === 0 && body.startsWith(CLAUDE_CODE_SYSTEM_PREFIX)) {
283
+ body = body.slice(CLAUDE_CODE_SYSTEM_PREFIX.length).trim();
284
+ if (!body) continue;
285
+ }
286
+ blocks.push({ type: 'text', text: body });
287
+ }
288
+ if (cacheControl) {
289
+ let remaining = Math.max(0, Math.min(2, Number(maxCacheBlocks) || 0));
290
+ for (let i = blocks.length - 1; i >= 0 && remaining > 0; i--) {
291
+ if (blocks[i]?.text === CLAUDE_CODE_SYSTEM_PREFIX) continue;
292
+ blocks[i] = { ...blocks[i], cache_control: cacheControl };
293
+ remaining -= 1;
294
+ }
295
+ }
296
+ return blocks;
297
+ }
298
+
299
+ // Per-model max_tokens when the model id is explicitly listed. New models
300
+ // (e.g., Sonnet 4.7) won't match a specific entry and fall through to the
301
+ // family-based heuristic below. Conservative defaults — model may support
302
+ // more but we'd rather stay within safe bounds.
303
+ const MAX_TOKENS = {
304
+ 'claude-opus-4-8': 65536,
305
+ 'claude-opus-4-7': 65536,
306
+ 'claude-opus-4-6': 65536,
307
+ 'claude-sonnet-4-6': 16384,
308
+ 'claude-haiku-4-5-20251001': 8192,
309
+ };
310
+
311
+ function resolveMaxTokens(model) {
312
+ if (MAX_TOKENS[model]) return MAX_TOKENS[model];
313
+ const id = String(model || '').toLowerCase();
314
+ if (id.includes('opus')) return 65536;
315
+ if (id.includes('sonnet')) return 16384;
316
+ if (id.includes('haiku')) return 8192;
317
+ return 8192;
318
+ }
319
+
320
+ const EFFORT_BUDGET = {
321
+ low: 1024,
322
+ medium: 4096,
323
+ high: 16384,
324
+ xhigh: 32768,
325
+ max: 32768,
326
+ };
327
+
328
+ // Tracks which unknown effort labels we've already logged so a repeated
329
+ // session-level misconfig doesn't flood stderr with the same warning.
330
+ const _LOGGED_UNKNOWN_EFFORT = new Set();
331
+
332
+ // Layered cache TTLs — stable layers get 1h, volatile layers get 5m.
333
+ // Anthropic requires 1h entries to appear before 5m entries in the request.
334
+ const CACHE_TTL_STABLE = { type: 'ephemeral', ttl: '1h' }; // tools, system, tier3, messages
335
+ const CACHE_TTL_VOLATILE = { type: 'ephemeral' }; // explicit 5m override
336
+
337
+ // --- Credential helpers ---
338
+
339
+ function _pushUnique(list, value) {
340
+ if (!value || typeof value !== 'string') return;
341
+ if (!list.includes(value)) list.push(value);
342
+ }
343
+
344
+ function credentialCandidates() {
345
+ const paths = [];
346
+ _pushUnique(paths, process.env.ANTHROPIC_OAUTH_CREDENTIALS_PATH);
347
+ _pushUnique(paths, DEFAULT_CREDENTIALS_PATH);
348
+ _pushUnique(paths, CLAUDE_CODE_CREDENTIALS_PATH);
349
+ return paths;
350
+ }
351
+
352
+ // Fallback expiry from the access_token's JWT `exp` claim (epoch ms) when the
353
+ // credentials file carries no explicit expiresAt — without it expiresAt stays 0,
354
+ // which ensureAuth reads as "never expires", disabling proactive refresh. Claude
355
+ // OAuth tokens are opaque so this returns 0 and the file's expiresAt governs; kept
356
+ // for parity with the other OAuth providers. JWT `exp` is epoch SECONDS (RFC 7519).
357
+ function _expiryFromAccessToken(token) {
358
+ try {
359
+ const parts = String(token || '').split('.');
360
+ if (parts.length !== 3) return 0;
361
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));
362
+ const exp = Number(payload?.exp);
363
+ return Number.isFinite(exp) && exp > 0 ? exp * 1000 : 0;
364
+ } catch { return 0; }
365
+ }
366
+
367
+ function _loadCredentialsFile(path) {
368
+ if (!existsSync(path)) return null;
369
+ try {
370
+ const stat = statSync(path);
371
+ const raw = JSON.parse(readFileSync(path, 'utf-8'));
372
+ const oauth = raw?.claudeAiOauth;
373
+ if (!oauth?.accessToken) return null;
374
+ return {
375
+ path,
376
+ mtimeMs: stat.mtimeMs,
377
+ accessToken: oauth.accessToken,
378
+ refreshToken: oauth.refreshToken || null,
379
+ expiresAt: _normalizeExpiresAt(oauth.expiresAt ?? oauth.expires_at) || _expiryFromAccessToken(oauth.accessToken),
380
+ scopes: Array.isArray(oauth.scopes) ? oauth.scopes : [],
381
+ subscriptionType: oauth.subscriptionType || null,
382
+ };
383
+ } catch {
384
+ return null;
385
+ }
386
+ }
387
+
388
+ // Cross-process safe write-back. Lockfile (O_EXCL) prevents two refreshers
389
+ // from clobbering each other; atomic rename guarantees readers see either
390
+ // the old or new file, never a half-written one. Used so refresh_token
391
+ // rotation propagates to other readers of the same credentials file instead of
392
+ // leaving them stuck on the previous
393
+ // refresh_token. Mirrors openai-oauth.mjs:saveTokens.
394
+ function _saveCredentialsFile(path, raw) {
395
+ // No `secret: true`: this may be an externally-owned credentials file.
396
+ // Mixdog only writes back the rotated refresh_token and must not rewrite
397
+ // parent directory permissions.
398
+ writeJsonAtomicSync(path, raw, { lock: true, fsyncDir: true, mode: 0o600 });
399
+ }
400
+
401
+ // Cheap stat-only probe so ensureAuth can detect host-rotated credentials
402
+ // (claude login, logout/relogin) without paying a full JSON read every call.
403
+ function _credentialsMaxMtime() {
404
+ let max = 0;
405
+ for (const p of credentialCandidates()) {
406
+ try {
407
+ const s = statSync(p);
408
+ if (s.mtimeMs > max) max = s.mtimeMs;
409
+ } catch { /* not present — skip */ }
410
+ }
411
+ return max;
412
+ }
413
+
414
+ function loadCredentials() {
415
+ const loaded = credentialCandidates()
416
+ .map(_loadCredentialsFile)
417
+ .filter(Boolean);
418
+ if (!loaded.length) return null;
419
+ loaded.sort((a, b) => (Number(b.expiresAt) || 0) - (Number(a.expiresAt) || 0));
420
+ return loaded[0];
421
+ }
422
+
423
+ // Public predicate used by config.buildDefaultConfig — provider is enabled
424
+ // when on-disk credentials exist AND carry the inference scope. Single
425
+ // truth: same loader the runtime uses, no parallel hard-coded path probe.
426
+ export function hasAnthropicOAuthCredentials() {
427
+ const creds = loadCredentials();
428
+ if (!creds?.accessToken) return false;
429
+ return Array.isArray(creds.scopes) && creds.scopes.includes('user:inference');
430
+ }
431
+
432
+ export function describeAnthropicOAuthCredentials() {
433
+ try {
434
+ const creds = loadCredentials();
435
+ if (!creds?.accessToken) {
436
+ return { authenticated: false, status: 'Not Set', detail: DEFAULT_CREDENTIALS_PATH };
437
+ }
438
+ const hasInferenceScope = Array.isArray(creds.scopes) && creds.scopes.includes('user:inference');
439
+ const hasRefresh = Boolean(creds.refreshToken);
440
+ const expiresAt = _normalizeExpiresAt(creds.expiresAt);
441
+ const expiring = expiresAt > 0 && expiresAt < Date.now() + TOKEN_REFRESH_SKEW_MS;
442
+ const expired = expiresAt > 0 && expiresAt <= Date.now();
443
+ const detail = creds.path || DEFAULT_CREDENTIALS_PATH;
444
+ if (!hasInferenceScope) {
445
+ return { authenticated: false, status: 'Missing Scope', detail, expiresAt };
446
+ }
447
+ if (!hasRefresh) {
448
+ return {
449
+ authenticated: expiresAt === 0 || !expired,
450
+ status: expired ? 'Reauth Required' : 'Access Only',
451
+ detail: `${detail}; no refresh token`,
452
+ expiresAt,
453
+ };
454
+ }
455
+ if (expired) return { authenticated: true, status: 'Refresh Required', detail, expiresAt };
456
+ if (expiring) return { authenticated: true, status: 'Refresh Soon', detail, expiresAt };
457
+ return { authenticated: true, status: 'Valid', detail, expiresAt };
458
+ } catch (err) {
459
+ return { authenticated: false, status: 'Error', detail: String(err?.message || err).slice(0, 200) };
460
+ }
461
+ }
462
+
463
+ export function forgetAnthropicOAuthCredentials() {
464
+ let removed = false;
465
+ for (const path of credentialCandidates()) {
466
+ if (!existsSync(path)) continue;
467
+ try {
468
+ const raw = JSON.parse(readFileSync(path, 'utf-8'));
469
+ if (raw?.claudeAiOauth) {
470
+ delete raw.claudeAiOauth;
471
+ _saveCredentialsFile(path, raw);
472
+ removed = true;
473
+ }
474
+ } catch (err) {
475
+ throw new Error(`Anthropic OAuth reset failed for ${path}: ${err?.message || err}`);
476
+ }
477
+ }
478
+ return { removed };
479
+ }
480
+
481
+ function _normalizeExpiresAt(value) {
482
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return 0;
483
+ return value < 1e12 ? value * 1000 : value;
484
+ }
485
+
486
+ function _scrubTokens(text) {
487
+ return String(text || '')
488
+ .replace(/Bearer [A-Za-z0-9._\-]+/g, 'Bearer [REDACTED]')
489
+ .replace(/sk-ant-[A-Za-z0-9._\-]+/g, '[REDACTED]')
490
+ .replace(/"access[Tt]oken"\s*:\s*"[^"]+"/g, '"accessToken":"[REDACTED]"')
491
+ .replace(/"refresh[Tt]oken"\s*:\s*"[^"]+"/g, '"refreshToken":"[REDACTED]"')
492
+ .replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token":"[REDACTED]"')
493
+ .replace(/"refresh_token"\s*:\s*"[^"]+"/g, '"refresh_token":"[REDACTED]"');
494
+ }
495
+
496
+ async function refreshOAuthCredentials(creds) {
497
+ if (!creds?.refreshToken) {
498
+ throw new Error('Anthropic OAuth refresh token not available. Run /auth anthropic-oauth or /providers in mixdog to re-authenticate.');
499
+ }
500
+
501
+ const controller = new AbortController();
502
+ const timeout = setTimeout(() => controller.abort(), 30_000);
503
+ try {
504
+ const res = await fetch(TOKEN_URL, {
505
+ method: 'POST',
506
+ headers: {
507
+ 'Content-Type': 'application/json',
508
+ 'anthropic-dangerous-direct-browser-access': 'true',
509
+ 'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
510
+ },
511
+ body: JSON.stringify({
512
+ grant_type: 'refresh_token',
513
+ refresh_token: creds.refreshToken,
514
+ client_id: CLAUDE_CODE_CLIENT_ID,
515
+ }),
516
+ // Never follow a redirect on a secret-bearing request: a token
517
+ // endpoint that 307/308-redirects would replay the refresh_token to
518
+ // the redirect target. Fail loud instead.
519
+ redirect: 'error',
520
+ signal: controller.signal,
521
+ dispatcher: getLlmDispatcher(),
522
+ });
523
+
524
+ const text = await res.text();
525
+ let json = null;
526
+ try { json = text ? JSON.parse(text) : null; } catch { /* handled below */ }
527
+ if (!res.ok) {
528
+ const isInvalidGrant = text.includes('invalid_grant') || json?.error === 'invalid_grant';
529
+ throw Object.assign(new Error(`token refresh ${res.status}: ${_scrubTokens(text).slice(0, 200)}`), { isInvalidGrant });
530
+ }
531
+
532
+ const accessToken = json?.access_token || json?.accessToken;
533
+ if (!accessToken) throw new Error('token refresh returned no access token');
534
+ const expiresAt = _normalizeExpiresAt(json?.expires_at ?? json?.expiresAt)
535
+ || (typeof json?.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0);
536
+ const refreshed = {
537
+ path: creds.path,
538
+ accessToken,
539
+ refreshToken: json?.refresh_token || json?.refreshToken || creds.refreshToken,
540
+ expiresAt,
541
+ scopes: Array.isArray(json?.scope) ? json.scope : creds.scopes,
542
+ subscriptionType: creds.subscriptionType,
543
+ };
544
+ // Persist rotated tokens back so any other reader of the same
545
+ // credentials file picks up the new refresh_token.
546
+ // Without this, host's next refresh invalidates our copy and we
547
+ // loop on invalid_grant.
548
+ if (creds.path && existsSync(creds.path)) {
549
+ try {
550
+ const raw = JSON.parse(readFileSync(creds.path, 'utf-8'));
551
+ raw.claudeAiOauth = {
552
+ ...(raw.claudeAiOauth || {}),
553
+ accessToken: refreshed.accessToken,
554
+ refreshToken: refreshed.refreshToken,
555
+ expiresAt: refreshed.expiresAt,
556
+ scopes: refreshed.scopes,
557
+ };
558
+ _saveCredentialsFile(creds.path, raw);
559
+ } catch (err) {
560
+ process.stderr.write(`[anthropic-oauth] credential write-back failed: ${_scrubTokens(err?.message || String(err)).slice(0, 200)}\n`);
561
+ throw new Error(`[oauth] credentials write-back failed: ${err?.message ?? String(err)}`);
562
+ }
563
+ }
564
+ return refreshed;
565
+ } catch (err) {
566
+ if (err?.name === 'AbortError') {
567
+ throw new Error('Anthropic OAuth token refresh timed out after 30000ms');
568
+ }
569
+ throw err;
570
+ } finally {
571
+ clearTimeout(timeout);
572
+ }
573
+ }
574
+
575
+ // Exported so callers can detect re-auth-required scenarios and prompt the user.
576
+ export class ReauthRequired extends Error {
577
+ constructor(message) {
578
+ super(message);
579
+ this.name = 'ReauthRequired';
580
+ }
581
+ }
582
+
583
+ // --- Message conversion (mirrors anthropic.mjs) ---
584
+
585
+ function withCacheControl(block, ttl = CACHE_TTL_VOLATILE) {
586
+ if (!block || typeof block !== 'object' || block.cache_control) return block;
587
+ return { ...block, cache_control: ttl };
588
+ }
589
+
590
+ function appendCacheControl(content, ttl = CACHE_TTL_VOLATILE) {
591
+ if (Array.isArray(content)) {
592
+ if (content.length === 0) return content;
593
+ const next = [...content];
594
+ next[next.length - 1] = withCacheControl(next[next.length - 1], ttl);
595
+ return next;
596
+ }
597
+ if (typeof content === 'string') {
598
+ return [withCacheControl({ type: 'text', text: content }, ttl)];
599
+ }
600
+ return content;
601
+ }
602
+
603
+ // Anthropic's tool spec forbids oneOf / allOf / anyOf at the TOP level of
604
+ // input_schema (nested usage inside properties is allowed). External MCP
605
+ // servers sometimes emit such schemas.
606
+ // Convert them to a flat object schema so the API never sees a 400.
607
+ function _sanitizeInputSchema(schema, toolName) {
608
+ if (!schema || typeof schema !== 'object') {
609
+ return { type: 'object', properties: {} };
610
+ }
611
+ const compound = schema.oneOf || schema.anyOf || schema.allOf;
612
+ if (!compound) return structuredClone(schema);
613
+ // Merge all branch properties into one permissive object schema.
614
+ // None of the branches' required lists are hoisted — callers that relied
615
+ // on discriminated-union semantics will still function; the model simply
616
+ // receives a union of the property surface with no hard-required constraint.
617
+ const mergedProps = {};
618
+ const branchDescs = [];
619
+ for (const branch of Array.isArray(compound) ? compound : []) {
620
+ if (branch && typeof branch === 'object' && branch.properties) {
621
+ Object.assign(mergedProps, branch.properties);
622
+ }
623
+ if (branch && typeof branch === 'object') {
624
+ const parts = [];
625
+ if (branch.description) parts.push(branch.description);
626
+ else if (branch.type) parts.push(`type:${branch.type}`);
627
+ if (parts.length) branchDescs.push(parts.join(' '));
628
+ }
629
+ }
630
+ const compoundKey = schema.oneOf ? 'oneOf' : schema.anyOf ? 'anyOf' : 'allOf';
631
+ let description = schema.description || '';
632
+ if (branchDescs.length) {
633
+ const parts = [];
634
+ let used = 0;
635
+ for (let i = 0; i < branchDescs.length; i++) {
636
+ const v = `(variant ${i + 1}: ${branchDescs[i]})`;
637
+ if (used + v.length + (parts.length ? 1 : 0) > 500) break;
638
+ parts.push(v);
639
+ used += v.length + (parts.length > 1 ? 1 : 0);
640
+ }
641
+ const addition = parts.join(' ');
642
+ if (addition) description = description ? `${description} ${addition}` : addition;
643
+ }
644
+ const mergedPropsCount = Object.keys(mergedProps).length;
645
+ process.stderr.write(
646
+ `[anthropic-oauth-sanitizer] tool="${toolName ?? ''}" compound="${compoundKey}" branches=${Array.isArray(compound) ? compound.length : 0} mergedProps=${mergedPropsCount}\n`
647
+ );
648
+ return {
649
+ type: 'object',
650
+ ...(description ? { description } : {}),
651
+ properties: mergedProps,
652
+ };
653
+ }
654
+
655
+ function toAnthropicTools(tools) {
656
+ return tools.map(t => ({
657
+ name: t.name,
658
+ description: t.description,
659
+ input_schema: _sanitizeInputSchema(t.inputSchema, t.name),
660
+ }));
661
+ }
662
+
663
+ function toAnthropicMessages(messages) {
664
+ // Marker-free lowering. cache_control is applied AFTER sanitization by
665
+ // applyAnthropicCacheMarkers() so that block drops/inserts/reorders
666
+ // performed by sanitizeAnthropicContentPairs cannot move or delete a
667
+ // marked block (the root cause of the sporadic COLD-turn cache miss:
668
+ // pre-sanitize markers landed on blocks the sanitizer then rewrote, so
669
+ // the provider-visible breakpoint diverged from the cached one).
670
+ const result = [];
671
+ for (let idx = 0; idx < messages.length; idx++) {
672
+ const m = messages[idx];
673
+ if (m.role === 'system') continue;
674
+
675
+ if (m.role === 'assistant' && (m.toolCalls?.length || m.assistantBlocks?.length)) {
676
+ let content;
677
+ if (m.assistantBlocks?.length) {
678
+ content = m.assistantBlocks.slice();
679
+ } else {
680
+ content = [];
681
+ if (m.content) content.push({ type: 'text', text: m.content });
682
+ for (const tc of m.toolCalls) {
683
+ content.push({
684
+ type: 'tool_use',
685
+ id: tc.id,
686
+ name: tc.name,
687
+ input: tc.arguments,
688
+ });
689
+ }
690
+ }
691
+ result.push({ role: 'assistant', content });
692
+ continue;
693
+ }
694
+
695
+ if (m.role === 'tool') {
696
+ const last = result[result.length - 1];
697
+ const block = {
698
+ type: 'tool_result',
699
+ tool_use_id: m.toolCallId || '',
700
+ content: normalizeContentForAnthropic(m.content),
701
+ };
702
+ if (last?.role === 'user' && Array.isArray(last.content)) {
703
+ last.content.push(block);
704
+ } else {
705
+ result.push({ role: 'user', content: [block] });
706
+ }
707
+ continue;
708
+ }
709
+
710
+ result.push({ role: m.role, content: normalizeContentForAnthropic(m.content) });
711
+ }
712
+ return sanitizeAnthropicContentPairs(result);
713
+ }
714
+
715
+ // Applies cache_control markers to the FINAL, already-sanitized Anthropic
716
+ // message array — by INVARIANT, never by pre-sanitize index. Because
717
+ // sanitizeAnthropicContentPairs has already run (and must NOT run again
718
+ // after this), the blocks we mark here are exactly the blocks the provider
719
+ // sees, so the cache breakpoint is stable across turns.
720
+ // tier3: the user message whose first text block / string content
721
+ // startsWith '<system-reminder>' AND includes BP3_SENTINEL —
722
+ // mark its last content block with tier3Ttl.
723
+ // message-anchor: prefer a safe tool_result tail, then a previous real user
724
+ // text turn if another slot remains. Synthetic
725
+ // <system-reminder> messages and current pure-text prompts
726
+ // are excluded so first-turn prompts do not create a fresh
727
+ // BP4 write on every new session.
728
+ // messageTtl === null disables the tail; tier3Ttl === null disables tier3.
729
+ // ANTHROPIC_MSG_SLOTS=0 is honoured upstream by passing messageTtl = null.
730
+ function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_VOLATILE, messageSlots = 1, tier3Ttl = null } = {}) {
731
+ if (!Array.isArray(sanitizedMessages) || sanitizedMessages.length === 0) {
732
+ return sanitizedMessages;
733
+ }
734
+
735
+ const firstText = (content) => {
736
+ if (typeof content === 'string') return content;
737
+ if (Array.isArray(content)) {
738
+ const first = content.find((b) => b?.type === 'text');
739
+ return first && typeof first.text === 'string' ? first.text : '';
740
+ }
741
+ return '';
742
+ };
743
+ const isSystemReminder = (content) => firstText(content).startsWith('<system-reminder>');
744
+ const isTier3SystemReminder = (content) => {
745
+ const text = firstText(content);
746
+ return text.startsWith('<system-reminder>') && text.includes(BP3_SENTINEL);
747
+ };
748
+
749
+ const markLast = (msg, ttl) => {
750
+ if (!msg) return;
751
+ msg.content = appendCacheControl(msg.content, ttl);
752
+ };
753
+ const ttlRank = (ttl) => ttl?.ttl === '1h' ? 2 : 1;
754
+ const canMarkMessageIdx = (idx, tier3Idx) => {
755
+ // Anthropic requires 1h cache_control blocks to appear before 5m
756
+ // blocks. If tier3 is a later 1h marker, do not put a 5m marker before
757
+ // it. Gateway Anthropic routes set messages:'1h', so this guard mostly
758
+ // protects raw/default callers.
759
+ if (idx < 0) return false;
760
+ const msg = sanitizedMessages[idx];
761
+ if (ttlRank(messageTtl) > ttlRank(CACHE_TTL_VOLATILE)
762
+ && isSystemReminder(msg?.content)
763
+ && !isTier3SystemReminder(msg?.content)) {
764
+ return false;
765
+ }
766
+ if (tier3Idx >= 0 && idx < tier3Idx && ttlRank(messageTtl) < ttlRank(tier3Ttl)) {
767
+ return false;
768
+ }
769
+ return true;
770
+ };
771
+ const hasUserText = (msg) => {
772
+ if (msg?.role !== 'user') return false;
773
+ if (isSystemReminder(msg.content)) return false;
774
+ if (typeof msg.content === 'string') return msg.content.trim().length > 0;
775
+ if (!Array.isArray(msg.content)) return false;
776
+ return msg.content.some(b => b?.type === 'text' && typeof b.text === 'string' && b.text.trim().length > 0);
777
+ };
778
+ const previousUserTextAnchorIdx = () => {
779
+ // Prefer the user text turn before the current tail. In a normal
780
+ // user->assistant->tool loop this is the last prompt that was already
781
+ // present in the previous API request, so its prefix can overlap.
782
+ const tailIdx = sanitizedMessages.length - 1;
783
+ for (let i = tailIdx - 1; i >= 0; i--) {
784
+ if (hasUserText(sanitizedMessages[i])) return i;
785
+ }
786
+ return -1;
787
+ };
788
+ const latestToolResultTailIdx = () => {
789
+ // Claude/pi refs allow cache_control on tool_result blocks. Keep this
790
+ // narrower than "last message" so a fresh user prompt or steering text
791
+ // never becomes a 1h breakpoint.
792
+ for (let i = sanitizedMessages.length - 1; i >= 0; i--) {
793
+ const msg = sanitizedMessages[i];
794
+ if (msg?.role !== 'user' || !Array.isArray(msg.content) || msg.content.length === 0) continue;
795
+ const lastBlock = msg.content[msg.content.length - 1];
796
+ if (lastBlock?.type === 'tool_result') return i;
797
+ }
798
+ return -1;
799
+ };
800
+
801
+ // tier3 — locate the sentinel-tagged system-reminder user message.
802
+ let tier3MsgIdx = -1;
803
+ if (tier3Ttl !== null) {
804
+ for (let i = 0; i < sanitizedMessages.length; i++) {
805
+ const m = sanitizedMessages[i];
806
+ if (m?.role === 'user' && isTier3SystemReminder(m.content)) {
807
+ tier3MsgIdx = i;
808
+ break;
809
+ }
810
+ }
811
+ if (tier3MsgIdx >= 0) markLast(sanitizedMessages[tier3MsgIdx], tier3Ttl);
812
+ }
813
+
814
+ if (messageTtl !== null) {
815
+ const slots = Math.max(0, Math.min(4, Number(messageSlots) || 0));
816
+ const marked = new Set(tier3MsgIdx >= 0 ? [tier3MsgIdx] : []);
817
+ const candidates = [latestToolResultTailIdx(), previousUserTextAnchorIdx()];
818
+ for (const idx of candidates) {
819
+ if (slots <= 0) break;
820
+ if (idx < 0 || marked.has(idx) || !canMarkMessageIdx(idx, tier3MsgIdx)) continue;
821
+ markLast(sanitizedMessages[idx], messageTtl);
822
+ marked.add(idx);
823
+ if (marked.size - (tier3MsgIdx >= 0 ? 1 : 0) >= slots) break;
824
+ }
825
+ }
826
+
827
+ return sanitizedMessages;
828
+ }
829
+
830
+ // --- SSE parser ---
831
+
832
+ function _captureMidstreamAbort(state, reason) {
833
+ if (!state) return;
834
+ const reasonName = reason?.name || '';
835
+ if (reasonName === 'BridgeStallAbortError' || reasonName === 'StreamStalledAbortError') {
836
+ state.watchdogAbort = reasonName;
837
+ } else {
838
+ state.userAbort = true;
839
+ }
840
+ }
841
+
842
+ function _statusForAnthropicSseError(type, message) {
843
+ const kind = String(type || '').toLowerCase();
844
+ const text = String(message || '').toLowerCase();
845
+ if (kind.includes('overload') || text.includes('overload')) return 503;
846
+ if (kind.includes('rate_limit') || text.includes('rate limit') || text.includes('quota')) return 429;
847
+ if (kind.includes('authentication') || text.includes('authentication') || text.includes('unauthorized')) return 401;
848
+ if (kind.includes('permission') || text.includes('forbidden')) return 403;
849
+ if (kind.includes('not_found') || text.includes('not found')) return 404;
850
+ if (kind.includes('invalid_request')) return 400;
851
+ return 0;
852
+ }
853
+
854
+ function _anthropicSseError(event) {
855
+ const payload = event?.error && typeof event.error === 'object' ? event.error : event;
856
+ const type = payload?.type || event?.type || 'error';
857
+ const message = payload?.message || 'Anthropic SSE error';
858
+ const err = new Error(`Anthropic OAuth SSE error ${type}: ${message}`);
859
+ err.name = 'AnthropicSseError';
860
+ err.code = 'EANTHROPIC_SSE_ERROR';
861
+ err.providerErrorType = type;
862
+ err.requestId = event?.request_id || event?.requestId || null;
863
+ const status = _statusForAnthropicSseError(type, message);
864
+ if (status) {
865
+ err.httpStatus = status;
866
+ err.status = status;
867
+ }
868
+ return err;
869
+ }
870
+
871
+ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onToolCall, state, onTextDelta) {
872
+ const reader = response.body.getReader();
873
+ const decoder = new TextDecoder();
874
+ const SSE_IDLE_TIMEOUT_MS = PROVIDER_SSE_IDLE_TIMEOUT_MS;
875
+ let content = '';
876
+ let hasThinkingContent = false;
877
+ const contentBlockTypes = new Set();
878
+ let model = '';
879
+ let toolCalls = [];
880
+ let usage = { inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, raw: null };
881
+ let stopReason = null;
882
+ let buffer = '';
883
+ let idleTimedOut = false;
884
+ let idleTimer = null;
885
+ let currentEvent = '';
886
+
887
+ const pendingToolInputs = new Map();
888
+
889
+ // Holds the in-flight reader.read() race rejector so the idle timer can
890
+ // force-unblock the loop even when reader.cancel() fails to settle the
891
+ // pending read (undici half-open socket). See resetIdleTimer below.
892
+ let idleReject = null;
893
+
894
+ const resetIdleTimer = () => {
895
+ // OFF by default. When disabled the
896
+ // idle timer never arms, so the stream is never killed on inactivity;
897
+ // the bridge stall watchdog (600s) remains the dead-stream backstop.
898
+ if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED) return;
899
+ if (idleTimer) clearTimeout(idleTimer);
900
+ idleTimer = setTimeout(() => {
901
+ idleTimedOut = true;
902
+ try { abortStream?.(); } catch (err) {
903
+ try { process.stderr.write(`[anthropic-oauth] sse idle abortStream failed: ${err?.message ?? String(err)}\n`); } catch {}
904
+ }
905
+ try {
906
+ const _c = reader.cancel('SSE idle timeout');
907
+ if (_c && typeof _c.catch === 'function') _c.catch(() => {});
908
+ } catch (err) {
909
+ try { process.stderr.write(`[anthropic-oauth] sse idle cancel failed: ${err?.message ?? String(err)}\n`); } catch {}
910
+ }
911
+ // Force-reject the in-flight reader.read() race even when reader.cancel()
912
+ // fails to settle the pending read: without this the await below stays
913
+ // pending forever and the SSE idle timeout never unblocks the loop —
914
+ // the 391s-hang root cause.
915
+ if (idleReject) {
916
+ const e = new Error(`Anthropic OAuth SSE stream timed out after ${SSE_IDLE_TIMEOUT_MS}ms of inactivity`);
917
+ e.code = 'ETIMEDOUT';
918
+ const r = idleReject; idleReject = null; r(e);
919
+ }
920
+ // Shared provider policy: short inter-chunk inactivity catches the
921
+ // sess_9cfd11-class stuck pattern where SSE starts but then goes silent.
922
+ }, SSE_IDLE_TIMEOUT_MS);
923
+ };
924
+
925
+ const onAbort = () => {
926
+ try {
927
+ const _c = reader.cancel('SSE aborted');
928
+ if (_c && typeof _c.catch === 'function') _c.catch(() => {});
929
+ } catch {}
930
+ };
931
+ if (signal) {
932
+ if (signal.aborted) {
933
+ _captureMidstreamAbort(state, signal.reason);
934
+ throw signal.reason instanceof Error
935
+ ? signal.reason
936
+ : new Error('Anthropic OAuth SSE stream aborted');
937
+ }
938
+ signal.addEventListener('abort', onAbort, { once: true });
939
+ }
940
+
941
+ try {
942
+ resetIdleTimer();
943
+ streamLoop: while (true) {
944
+ let chunk;
945
+ try {
946
+ // Race the read against the idle timer's rejector so a stuck
947
+ // reader.read() (cancel did not settle it) still unblocks here.
948
+ chunk = await new Promise((resolve, reject) => {
949
+ idleReject = reject;
950
+ reader.read().then(resolve, reject);
951
+ });
952
+ } catch (err) {
953
+ if (idleTimedOut) {
954
+ const idleErr = new Error(`Anthropic OAuth SSE stream timed out after ${SSE_IDLE_TIMEOUT_MS}ms of inactivity`);
955
+ idleErr.code = 'ETIMEDOUT';
956
+ throw idleErr;
957
+ }
958
+ if (signal?.aborted) {
959
+ _captureMidstreamAbort(state, signal.reason);
960
+ throw signal.reason instanceof Error
961
+ ? signal.reason
962
+ : new Error('Anthropic OAuth SSE stream aborted');
963
+ }
964
+ throw err;
965
+ }
966
+ const { done, value } = chunk;
967
+ if (done) break;
968
+
969
+ resetIdleTimer();
970
+ buffer += decoder.decode(value, { stream: true });
971
+ const lines = buffer.split('\n');
972
+ buffer = lines.pop() || '';
973
+
974
+ for (const line of lines) {
975
+ if (line.startsWith(':')) {
976
+ // SSE comment frame (Anthropic `:ping` keepalive). The HTML Standard SSE
977
+ // spec says comments are silently ignored, but we surface them here so
978
+ // the bridge-stall-watchdog sees the stream is still alive during Opus
979
+ // extended-thinking pauses. No content is emitted — this only refreshes
980
+ // the runtime's lastStreamDeltaAt timestamp.
981
+ try { onStreamDelta?.(); } catch {}
982
+ continue;
983
+ }
984
+ if (line.startsWith('event: ')) {
985
+ currentEvent = line.slice(7).trim();
986
+ continue;
987
+ }
988
+ if (!line.startsWith('data: ')) continue;
989
+ const data = line.slice(6).trim();
990
+ if (!data) continue;
991
+
992
+ try {
993
+ const event = JSON.parse(data);
994
+
995
+ if (currentEvent === 'error' || event?.type === 'error' || event?.error) {
996
+ throw _anthropicSseError(event);
997
+ }
998
+
999
+ if (event.type === 'message_start' && event.message) {
1000
+ if (state) state.sawMessageStart = true;
1001
+ if (event.message.model) model = event.message.model;
1002
+ if (event.message.usage) {
1003
+ usage.inputTokens = event.message.usage.input_tokens || 0;
1004
+ usage.cachedTokens = event.message.usage.cache_read_input_tokens || 0;
1005
+ usage.cacheWriteTokens = event.message.usage.cache_creation_input_tokens || 0;
1006
+ usage.raw = { ...event.message.usage };
1007
+ }
1008
+ }
1009
+
1010
+ if (event.type === 'content_block_start') {
1011
+ const block = event.content_block;
1012
+ if (block?.type === 'tool_use') {
1013
+ pendingToolInputs.set(event.index, {
1014
+ id: block.id || '',
1015
+ name: block.name || '',
1016
+ inputJson: '',
1017
+ });
1018
+ }
1019
+ }
1020
+
1021
+ if (event.type === 'content_block_delta') {
1022
+ const delta = event.delta;
1023
+ if (delta?.type) contentBlockTypes.add(delta.type);
1024
+ if (delta?.type === 'text_delta') {
1025
+ content += delta.text || '';
1026
+ try { onStreamDelta?.(); } catch {}
1027
+ // Live text relay (gateway): forward the explicit
1028
+ // text chunk. thinking/signature/input_json deltas
1029
+ // intentionally stay off this path.
1030
+ // Invariant: once a non-empty chunk has been relayed
1031
+ // live it cannot be withdrawn, so flag the attempt so
1032
+ // the mid-stream retry loop treats any later failure
1033
+ // as final (a retry would concatenate attempts).
1034
+ if (delta.text && onTextDelta) {
1035
+ if (state) state.emittedText = true;
1036
+ try { onTextDelta(delta.text); } catch {}
1037
+ }
1038
+ }
1039
+ if (delta?.type === 'thinking_delta' || delta?.type === 'signature_delta') {
1040
+ // Extended-thinking block: provider reasoning without
1041
+ // user-visible text. Track presence so a final turn
1042
+ // that emitted ONLY thinking (no text_delta, no
1043
+ // tool_use) can be classified by the loop as
1044
+ // synthesis-stalled rather than silent empty.
1045
+ hasThinkingContent = true;
1046
+ try { onStreamDelta?.(); } catch {}
1047
+ }
1048
+ if (delta?.type === 'input_json_delta') {
1049
+ const pending = pendingToolInputs.get(event.index);
1050
+ if (pending) {
1051
+ pending.inputJson += delta.partial_json || '';
1052
+ }
1053
+ try { onStreamDelta?.(); } catch {}
1054
+ }
1055
+ }
1056
+
1057
+ if (event.type === 'content_block_stop') {
1058
+ const pending = pendingToolInputs.get(event.index);
1059
+ if (pending) {
1060
+ // Bare JSON.parse threw straight up into the
1061
+ // surrounding broad catch, which swallowed the
1062
+ // whole tool_call — the loop never saw it and
1063
+ // the assistant turn ended with an unmatched
1064
+ // tool_use id. Wrap the parse so a malformed
1065
+ // input still produces a tool_call (with empty
1066
+ // arguments and a logged error) instead of a
1067
+ // silent drop.
1068
+ let parsedArgs = {};
1069
+ if (pending.inputJson) {
1070
+ try { parsedArgs = JSON.parse(pending.inputJson); }
1071
+ catch (parseErr) {
1072
+ process.stderr.write(`[anthropic-oauth] tool args JSON.parse failed (id=${pending.id}, name=${pending.name}): ${parseErr?.message || parseErr}\n`);
1073
+ parsedArgs = {};
1074
+ }
1075
+ }
1076
+ // Tool arguments must be a plain object. Anthropic's
1077
+ // tool_use input is always a JSON object, but a
1078
+ // malformed stream could parse to an array/string/
1079
+ // number — wrap those as {} to keep the contract
1080
+ // (invariant-based, no heuristic coercion).
1081
+ if (parsedArgs === null
1082
+ || typeof parsedArgs !== 'object'
1083
+ || Array.isArray(parsedArgs)) {
1084
+ process.stderr.write(`[anthropic-oauth] tool args not a plain object (id=${pending.id}, name=${pending.name}, type=${Array.isArray(parsedArgs) ? 'array' : typeof parsedArgs}); using {}\n`);
1085
+ parsedArgs = {};
1086
+ }
1087
+ const call = {
1088
+ id: pending.id,
1089
+ name: pending.name,
1090
+ arguments: parsedArgs,
1091
+ };
1092
+ toolCalls.push(call);
1093
+ pendingToolInputs.delete(event.index);
1094
+ if (state) state.emittedToolCall = true;
1095
+ // Eager dispatch: let the loop start this tool
1096
+ // before message_stop arrives. The loop keys
1097
+ // pending promises by call.id so order is safe.
1098
+ try { onToolCall?.(call); } catch {}
1099
+ try { onStreamDelta?.(); } catch {}
1100
+ }
1101
+ }
1102
+
1103
+ if (event.type === 'message_delta') {
1104
+ if (event.delta?.stop_reason) {
1105
+ stopReason = event.delta.stop_reason;
1106
+ }
1107
+ if (event.usage) {
1108
+ usage.outputTokens = event.usage.output_tokens || 0;
1109
+ usage.raw = { ...(usage.raw || {}), ...event.usage };
1110
+ }
1111
+ if (stopReason === 'tool_use' && toolCalls.length > 0 && pendingToolInputs.size === 0) {
1112
+ if (state) state.sawCompleted = true;
1113
+ break streamLoop;
1114
+ }
1115
+ }
1116
+ if (event.type === 'message_stop') {
1117
+ if (state) state.sawCompleted = true;
1118
+ // Anthropic streams can keep emitting `:ping` keepalive
1119
+ // frames after `message_stop`; if we wait for EOF the
1120
+ // outer reader.read() loop hangs indefinitely. Break
1121
+ // out of streamLoop the moment the message ends.
1122
+ break streamLoop;
1123
+ }
1124
+ // Unified prompt volume — what the model actually ingested.
1125
+ // Anthropic splits input into three billable slots (uncached
1126
+ // input + cache_read + cache_create); keep them separate for
1127
+ // cost math but also expose the sum so cross-provider logs
1128
+ // have a consistent `promptTokens` meaning.
1129
+ usage.promptTokens = (usage.inputTokens || 0)
1130
+ + (usage.cachedTokens || 0)
1131
+ + (usage.cacheWriteTokens || 0);
1132
+ } catch (err) {
1133
+ if (err?.code === 'EANTHROPIC_SSE_ERROR') throw err;
1134
+ /* skip malformed events */
1135
+ }
1136
+ }
1137
+ }
1138
+
1139
+ // Truncated-stream guard: if the reader loop exited (EOF or break)
1140
+ // after message_start but without seeing message_stop / a tool_use
1141
+ // stop_reason, the assistant turn was cut off mid-flight. Returning
1142
+ // success here would silently surface partial content (or a partially
1143
+ // streamed tool_use whose input_json never completed) as final.
1144
+ // Throw a typed truncated-stream error so the loop can decide whether
1145
+ // to retry, surface, or escalate instead of accepting the partial.
1146
+ if (state?.sawMessageStart && !state?.sawCompleted) {
1147
+ const pendingToolUse = pendingToolInputs.size > 0;
1148
+ const err = Object.assign(
1149
+ new Error(
1150
+ `Anthropic OAuth SSE stream truncated: message_start without message_stop`
1151
+ + (pendingToolUse ? ` (pending tool_use input)` : ''),
1152
+ ),
1153
+ {
1154
+ name: 'TruncatedStreamError',
1155
+ code: 'TRUNCATED_STREAM',
1156
+ truncatedStream: true,
1157
+ pendingToolUse,
1158
+ stopReason,
1159
+ },
1160
+ );
1161
+ throw err;
1162
+ }
1163
+
1164
+ return {
1165
+ content,
1166
+ model,
1167
+ toolCalls: toolCalls.length ? toolCalls : undefined,
1168
+ usage,
1169
+ stopReason,
1170
+ hasThinkingContent,
1171
+ contentBlockTypes: Array.from(contentBlockTypes),
1172
+ };
1173
+ } finally {
1174
+ if (idleTimer) clearTimeout(idleTimer);
1175
+ if (signal) signal.removeEventListener('abort', onAbort);
1176
+ try { reader.releaseLock(); } catch (err) {
1177
+ try { process.stderr.write(`[anthropic-oauth] reader releaseLock failed: ${err?.message ?? String(err)}\n`); } catch {}
1178
+ }
1179
+ }
1180
+ }
1181
+
1182
+ /**
1183
+ * Classify an Anthropic SSE failure for single-shot mid-stream retry.
1184
+ *
1185
+ * Retry is allowed only after `message_start` and before `message_stop`,
1186
+ * and only when no tool call has already been surfaced to the loop.
1187
+ * That keeps recovery limited to transport/stream stalls without risking
1188
+ * duplicate eager tool execution.
1189
+ */
1190
+ export function _classifyMidstreamError(err, state) {
1191
+ if (!state) return null;
1192
+ if ((state.attemptIndex | 0) >= 1) return null;
1193
+ if (state.sawCompleted) return null;
1194
+ if (!state.sawMessageStart) return null;
1195
+ if (state.userAbort) return null;
1196
+ if (state.emittedToolCall) return null;
1197
+
1198
+ if (!err) return null;
1199
+ const status = Number(err?.httpStatus || 0);
1200
+ if (status === 401 || status === 403 || status === 429) return null;
1201
+
1202
+ const name = err?.name || '';
1203
+ if (name === 'BridgeStallAbortError') return 'bridge_stall';
1204
+ if (name === 'StreamStalledAbortError') return 'stream_stalled';
1205
+ if (state.watchdogAbort === 'BridgeStallAbortError') return 'bridge_stall';
1206
+ if (state.watchdogAbort === 'StreamStalledAbortError') return 'stream_stalled';
1207
+
1208
+ const code = err?.code || err?.cause?.code || '';
1209
+ if (code === 'ECONNRESET') return 'reset';
1210
+ if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout';
1211
+ if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'EAI_NODATA') return 'dns';
1212
+
1213
+ const msg = String(err?.message || '').toLowerCase();
1214
+ if (msg.includes('stream timed out after') && msg.includes('of inactivity')) return 'sse_idle_timeout';
1215
+ if (msg.includes('body stream') && msg.includes('terminated')) return 'stream_terminated';
1216
+ if (msg.includes('fetch failed')) return 'fetch_failed';
1217
+
1218
+ return null;
1219
+ }
1220
+
1221
+ // --- Build request body ---
1222
+
1223
+ function resolveCacheTtls(opts) {
1224
+ // Layered cache strategy — caller may override per-layer via opts.cacheStrategy.
1225
+ // Anthropic enforces: 1h entries must appear before 5m entries in the request.
1226
+ const strategy = opts.cacheStrategy || {};
1227
+ const pick = (layer, fallback) => {
1228
+ const v = strategy[layer];
1229
+ if (v === '1h') return CACHE_TTL_STABLE;
1230
+ if (v === '5m') return CACHE_TTL_VOLATILE;
1231
+ if (v === 'none') return null;
1232
+ return fallback;
1233
+ };
1234
+ // BP budget (4 total):
1235
+ // BP1 baseRules — 1h (shared across ALL roles)
1236
+ // BP2 roleCatalog — 1h (shared across ALL roles)
1237
+ // BP3 tier3 — 1h (sessionMarker: mixdog.md + stable role body)
1238
+ // BP4 messages — 1h sliding tail (tool_result cache across iter)
1239
+ // tools BP is dropped — system BP covers the tools prefix via
1240
+ // Anthropic's prompt cache prefix semantics (order: tools → system
1241
+ // → messages).
1242
+ // tier3 defaults to 1h (stable) — sessionMarker content is stable per
1243
+ // (role, permission, project) tuple and Anthropic only spends the BP
1244
+ // slot when findTier3Index() actually finds a <system-reminder> block,
1245
+ // so this default is free for sessions that don't carry one. Previously
1246
+ // null here meant any caller that skipped smart bridge resolve (CLI,
1247
+ // raw bridge spawn) silently lost the tier3 cache layer even
1248
+ // though their message layout supported it.
1249
+ return {
1250
+ tools: pick('tools', CACHE_TTL_STABLE),
1251
+ system: pick('system', CACHE_TTL_STABLE),
1252
+ tier3: pick('tier3', CACHE_TTL_STABLE),
1253
+ messages: pick('messages', CACHE_TTL_STABLE),
1254
+ };
1255
+ }
1256
+
1257
+ // Tier 3 is injected by session/manager as a user message wrapped in
1258
+ // `<system-reminder>` whose body starts with the explicit sentinel
1259
+ // `<!-- bp3-sentinel -->` (emitted by collect.mjs:composeSystemPrompt only
1260
+ // when stable mixdog.md/role-specific context is present). The sentinel is mandatory:
1261
+ // volatileTail (role/permission/taskBrief/memoryRecap) is also wrapped in
1262
+ // `<system-reminder>` but varies per-call, so a plain prefix match would
1263
+ // pin per-call data to the 1h BP3 slot and explode the cache.
1264
+ const BP3_SENTINEL = '<!-- bp3-sentinel -->';
1265
+ function findTier3Index(chatMsgs) {
1266
+ for (let i = 0; i < chatMsgs.length; i++) {
1267
+ const m = chatMsgs[i];
1268
+ if (m?.role === 'user' && typeof m.content === 'string'
1269
+ && m.content.startsWith('<system-reminder>')
1270
+ && m.content.includes(BP3_SENTINEL)) {
1271
+ return i;
1272
+ }
1273
+ }
1274
+ return -1;
1275
+ }
1276
+
1277
+ function buildRequestBody(messages, model, tools, sendOpts) {
1278
+ const systemMsgs = messages.filter(m => m.role === 'system');
1279
+ const chatMsgs = messages.filter(m => m.role !== 'system');
1280
+ // Pass each system message text as its own entry so the Anthropic body
1281
+ // gets N separate content blocks — each can have its own BP
1282
+ // independent of the others.
1283
+ const systemTexts = systemMsgs.map(m => m.content);
1284
+ const maxTokens = resolveMaxTokens(model);
1285
+ const opts = sendOpts || {};
1286
+ const ttls = resolveCacheTtls(opts);
1287
+ const systemBlocks = buildSystemBlocks(systemTexts, model, ttls?.system, 2);
1288
+
1289
+ // 4-BP budget layout. tools BP is dropped — system BP covers the
1290
+ // tools prefix via Anthropic's prompt cache prefix semantics
1291
+ // (order: tools → system → messages). That frees slots for
1292
+ // tier3 + messages-tail.
1293
+ const systemBpUsed = ttls.system ? systemBlocks.filter(b => b.cache_control).length : 0;
1294
+ const toolsBpUsed = 0;
1295
+ const tier3Idx = ttls.tier3 ? findTier3Index(chatMsgs) : -1;
1296
+ const tier3BpUsed = tier3Idx >= 0 ? 1 : 0;
1297
+ const usedSlots = toolsBpUsed + systemBpUsed + tier3BpUsed;
1298
+ // Env override for BP strategy. ANTHROPIC_MSG_SLOTS=0 disables message
1299
+ // caching entirely. Any value >=1 first marks the previous user text turn
1300
+ // so consecutive requests share a breakpoint; a second free slot marks the
1301
+ // tail for the newest delta.
1302
+ const msgSlotsCap = Number.parseInt(process.env.ANTHROPIC_MSG_SLOTS, 10);
1303
+ const defaultMsgSlots = Math.max(0, 4 - usedSlots);
1304
+ const msgSlots = ttls.messages
1305
+ ? (Number.isFinite(msgSlotsCap) && msgSlotsCap >= 0 ? Math.min(msgSlotsCap, defaultMsgSlots) : defaultMsgSlots)
1306
+ : 0;
1307
+ // Build → sanitize (once, inside toAnthropicMessages) → mark. Markers are
1308
+ // applied to the FINAL sanitized array by invariant, so block drops /
1309
+ // inserts / reorders performed by the sanitizer can never move or delete a
1310
+ // marked block. NEVER sanitize again after this (see send path).
1311
+ // msgSlots === 0 (ANTHROPIC_MSG_SLOTS=0, or no free slot) → tail disabled.
1312
+ const tailTtl = msgSlots > 0 ? ttls.messages : null;
1313
+ const tier3Ttl = tier3Idx >= 0 ? ttls.tier3 : null;
1314
+ const anthropicMessages = applyAnthropicCacheMarkers(
1315
+ toAnthropicMessages(chatMsgs),
1316
+ { messageTtl: tailTtl, messageSlots: msgSlots, tier3Ttl },
1317
+ );
1318
+
1319
+ const body = {
1320
+ model,
1321
+ max_tokens: maxTokens,
1322
+ messages: anthropicMessages,
1323
+ stream: true,
1324
+ };
1325
+
1326
+ if (systemBlocks.length) body.system = systemBlocks;
1327
+
1328
+ if (tools?.length) {
1329
+ // No cache_control on tools — the systemBase BP already covers the
1330
+ // tools prefix via Anthropic's prompt cache prefix semantics (order:
1331
+ // tools → system → messages). Placing a separate BP here would waste
1332
+ // a slot that's better spent on messages tail.
1333
+ body.tools = toAnthropicTools(tools);
1334
+ }
1335
+
1336
+ const thinkingBudgetTokens = Number(opts.thinkingBudgetTokens);
1337
+ if (Number.isFinite(thinkingBudgetTokens) && thinkingBudgetTokens > 0) {
1338
+ body.thinking = { type: 'enabled', budget_tokens: Math.floor(thinkingBudgetTokens) };
1339
+ } else if (opts.effort) {
1340
+ if (EFFORT_BUDGET[opts.effort]) {
1341
+ body.thinking = { type: 'enabled', budget_tokens: EFFORT_BUDGET[opts.effort] };
1342
+ } else if (!_LOGGED_UNKNOWN_EFFORT.has(opts.effort)) {
1343
+ _LOGGED_UNKNOWN_EFFORT.add(opts.effort);
1344
+ try {
1345
+ process.stderr.write(`[anthropic-oauth] unknown effort=${opts.effort} ignored (known: ${Object.keys(EFFORT_BUDGET).join(',')})\n`);
1346
+ } catch {}
1347
+ }
1348
+ }
1349
+
1350
+ if (opts.fast === true && supportsAnthropicFastMode(model)) {
1351
+ body.speed = 'fast';
1352
+ }
1353
+
1354
+ return body;
1355
+ }
1356
+
1357
+ export function _buildRequestBodyForCacheSmoke(messages, model, tools = [], sendOpts = {}) {
1358
+ return buildRequestBody(messages, model, tools, sendOpts);
1359
+ }
1360
+
1361
+ // --- Provider ---
1362
+
1363
+ export class AnthropicOAuthProvider {
1364
+ // input_tokens EXCLUDES cache_read_input_tokens (separate field) — add the
1365
+ // cache back for the real context footprint. See registry.mjs.
1366
+ static inputExcludesCache = true;
1367
+ name = 'anthropic-oauth';
1368
+ credentials = null;
1369
+ config;
1370
+ fastModeBetaHeaderLatched = false;
1371
+
1372
+ constructor(config) {
1373
+ this.config = config || {};
1374
+ this.credentials = loadCredentials();
1375
+ // Warm a kept-alive socket to the messages API so the first request
1376
+ // skips the cold TLS handshake. Best-effort; never throws.
1377
+ preconnect('https://api.anthropic.com');
1378
+ }
1379
+
1380
+ async ensureAuth({ forceRefresh = false, reason = 'preemptive' } = {}) {
1381
+ if (!this.credentials) {
1382
+ this.credentials = loadCredentials();
1383
+ }
1384
+ if (!this.credentials) {
1385
+ throw new Error('Anthropic OAuth credentials not found. Run /auth anthropic-oauth or /providers in mixdog to authenticate.');
1386
+ }
1387
+
1388
+ // Pick up host-rotated tokens the moment the credentials file is
1389
+ // rewritten — without this, a fresh `claude login` is ignored until
1390
+ // the in-memory token's expiry skew triggers a refresh.
1391
+ const diskMtime = _credentialsMaxMtime();
1392
+ if (diskMtime > 0 && diskMtime > (this.credentials.mtimeMs || 0)) {
1393
+ const fresh = loadCredentials();
1394
+ if (fresh?.accessToken) {
1395
+ this.credentials = fresh;
1396
+ process.stderr.write(`[anthropic-oauth] Credentials reloaded from disk (mtime change)\n`);
1397
+ }
1398
+ }
1399
+
1400
+ const expiring = this.credentials.expiresAt
1401
+ && this.credentials.expiresAt < Date.now() + TOKEN_REFRESH_SKEW_MS;
1402
+ if (forceRefresh || expiring) {
1403
+ this.credentials = await this._refreshCredentials({ force: forceRefresh, reason });
1404
+ }
1405
+
1406
+ return this.credentials;
1407
+ }
1408
+
1409
+ async _refreshCredentials({ force = false, reason = 'preemptive' } = {}) {
1410
+ const currentToken = this.credentials?.accessToken || null;
1411
+ const disk = loadCredentials();
1412
+ const validAfter = Date.now() + (force ? 0 : TOKEN_REFRESH_SKEW_MS);
1413
+ if (disk?.accessToken && disk.accessToken !== currentToken
1414
+ && (!disk.expiresAt || disk.expiresAt >= validAfter)) {
1415
+ this.credentials = disk;
1416
+ process.stderr.write(`[anthropic-oauth] Credentials reloaded from disk\n`);
1417
+ return disk;
1418
+ }
1419
+ if (!this.credentials && disk) this.credentials = disk;
1420
+
1421
+ if (_oauthRefreshInFlight) {
1422
+ const shared = await _oauthRefreshInFlight;
1423
+ this.credentials = shared;
1424
+ if (!force || shared?.accessToken !== currentToken) return this.credentials;
1425
+ }
1426
+
1427
+ const startingCreds = this.credentials || disk;
1428
+ _oauthRefreshInFlight = (async () => {
1429
+ const latest = loadCredentials() || startingCreds;
1430
+ const latestValidAfter = Date.now() + (force ? 0 : TOKEN_REFRESH_SKEW_MS);
1431
+ if (latest?.accessToken && latest.accessToken !== currentToken
1432
+ && (!latest.expiresAt || latest.expiresAt >= latestValidAfter)) {
1433
+ process.stderr.write(`[anthropic-oauth] Credentials reloaded from disk\n`);
1434
+ return latest;
1435
+ }
1436
+
1437
+ if (!latest?.refreshToken) {
1438
+ if (!force && latest?.accessToken && (!latest.expiresAt || latest.expiresAt > Date.now())) {
1439
+ process.stderr.write(`[anthropic-oauth] WARNING: token expiring but no refresh token; using current token until expiry\n`);
1440
+ return latest;
1441
+ }
1442
+ throw new Error('Anthropic OAuth refresh token not available. Run /auth anthropic-oauth or /providers in mixdog to re-authenticate.');
1443
+ }
1444
+
1445
+ try {
1446
+ if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[anthropic-oauth] Token ${reason}, refreshing...\n`);
1447
+ const refreshed = await refreshOAuthCredentials(latest);
1448
+ process.stderr.write(`[anthropic-oauth] Token refreshed, expires in ${Math.round(((refreshed.expiresAt || Date.now()) - Date.now()) / 1000)}s\n`);
1449
+ return refreshed;
1450
+ } catch (err) {
1451
+ if (!force && latest?.accessToken && (!latest.expiresAt || latest.expiresAt > Date.now())) {
1452
+ const msg = err instanceof Error ? err.message : String(err);
1453
+ process.stderr.write(`[anthropic-oauth] Refresh failed (${msg}); using still-valid current token\n`);
1454
+ return latest;
1455
+ }
1456
+ throw err;
1457
+ }
1458
+ })().finally(() => { _oauthRefreshInFlight = null; });
1459
+
1460
+ this.credentials = await _oauthRefreshInFlight;
1461
+ return this.credentials;
1462
+ }
1463
+
1464
+ scrubTokens(text) {
1465
+ return _scrubTokens(text);
1466
+ }
1467
+
1468
+ async send(messages, model, tools, sendOpts) {
1469
+ // Defense-in-depth: enforce tool_use / tool_result pairing before
1470
+ // the Anthropic API call. The trim.mjs sanitize pass is normally
1471
+ // invoked by the budget trimmer in loop.mjs, but dispatches under
1472
+ // budget skip it — a tool that aborted mid-flight then leaves an
1473
+ // unmatched tool_use in messages, which the provider rejects with
1474
+ // a hard 400. Pairing here closes the gap regardless of caller.
1475
+ messages = sanitizeToolPairs(messages);
1476
+ const opts = sendOpts || {};
1477
+ const onStageChange = typeof opts.onStageChange === 'function' ? opts.onStageChange : null;
1478
+ const onStreamDelta = typeof opts.onStreamDelta === 'function' ? opts.onStreamDelta : null;
1479
+ const onToolCall = typeof opts.onToolCall === 'function' ? opts.onToolCall : null;
1480
+ const onTextDelta = typeof opts.onTextDelta === 'function' ? opts.onTextDelta : null;
1481
+ const externalSignal = opts.signal || null;
1482
+ // Test seam: lets the retry harness drive stream outcomes without a
1483
+ // live OAuth session.
1484
+ const parseSSEFn = typeof opts._parseSSEFn === 'function' ? opts._parseSSEFn : parseSSEStream;
1485
+
1486
+ let creds = await this.ensureAuth();
1487
+ // Default when the caller doesn't pin a model: newest high-tier chat
1488
+ // model from the live catalog (one warmup round-trip if cache is cold).
1489
+ const useModel = model || await ensureLatestAnthropicModel(this);
1490
+ const body = buildRequestBody(messages, useModel, tools, sendOpts);
1491
+ if (body.speed === 'fast') {
1492
+ this.fastModeBetaHeaderLatched = true;
1493
+ }
1494
+ const sessionId = opts.sessionId || null;
1495
+ const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
1496
+ // Option A: no absolute wall-clock cap on streaming generation. A stream
1497
+ // that keeps emitting SSE deltas must NOT be killed by a fixed total-lifetime
1498
+ // timer — the old PROVIDER_GENERATE_TOTAL_TIMEOUT_MS (~285s, derived from the
1499
+ // stall WARN threshold) false-aborted live high-reasoning turns that were
1500
+ // still alive and producing tokens. The streaming phase is bounded instead by:
1501
+ // (a) the per-attempt initial-response timeout in requestWithRetry
1502
+ // (PROVIDER_HTTP_RESPONSE_TIMEOUT_MS) for a socket that never sends a
1503
+ // first byte (truly wedged),
1504
+ // (b) externalSignal (client disconnect / replaced-by-newer-request), and
1505
+ // (c) the bridge stall watchdog (STALL_ABORT_S, 600s, progress-based) plus
1506
+ // the optional SSE idle watchdog for a stream that goes dead mid-flight.
1507
+ // totalSignal is therefore a pure pass-through of externalSignal with no timer.
1508
+ const totalTimeout = createPassthroughSignal(externalSignal);
1509
+ const totalSignal = totalTimeout.signal;
1510
+
1511
+ const cleanupCancelHandler = (handler) => {
1512
+ if (!handler) return;
1513
+ try { totalSignal.removeEventListener('abort', handler); } catch {}
1514
+ };
1515
+
1516
+ const doRequest = async (accessToken, requestSignal = null) => {
1517
+ const controller = createAbortController();
1518
+ const fetchStartedAt = Date.now();
1519
+
1520
+ let cancelHandler = null;
1521
+ let attemptCancelHandler = null;
1522
+ if (totalSignal) {
1523
+ if (totalSignal.aborted) {
1524
+ controller.abort(totalSignal.reason);
1525
+ throw totalSignal.reason instanceof Error
1526
+ ? totalSignal.reason
1527
+ : new Error('Anthropic OAuth request aborted by session close');
1528
+ }
1529
+ cancelHandler = () => { try { controller.abort(totalSignal.reason); } catch {} };
1530
+ totalSignal.addEventListener('abort', cancelHandler, { once: true });
1531
+ }
1532
+ if (requestSignal && requestSignal !== totalSignal) {
1533
+ if (requestSignal.aborted) {
1534
+ cleanupCancelHandler(cancelHandler);
1535
+ controller.abort(requestSignal.reason);
1536
+ throw requestSignal.reason instanceof Error
1537
+ ? requestSignal.reason
1538
+ : new Error('Anthropic OAuth request attempt aborted');
1539
+ }
1540
+ attemptCancelHandler = () => { try { controller.abort(requestSignal.reason); } catch {} };
1541
+ requestSignal.addEventListener('abort', attemptCancelHandler, { once: true });
1542
+ }
1543
+
1544
+ try {
1545
+ try { onStageChange?.('requesting'); } catch {}
1546
+ // NOTE: do NOT sanitize here. body.messages was already
1547
+ // sanitized once inside toAnthropicMessages and then had cache
1548
+ // markers applied by applyAnthropicCacheMarkers. Re-sanitizing
1549
+ // after marking could drop/reorder a marked block and move the
1550
+ // provider-visible cache breakpoint off the cached one — the
1551
+ // exact COLD-turn bug this change fixes. Order is fixed:
1552
+ // build → sanitize (once) → mark → JSON.stringify.
1553
+ const response = await fetch(API_URL, {
1554
+ method: 'POST',
1555
+ headers: {
1556
+ 'Authorization': `Bearer ${accessToken}`,
1557
+ 'anthropic-version': ANTHROPIC_VERSION,
1558
+ 'anthropic-beta': buildAnthropicBetaHeaders({
1559
+ base: OAUTH_BETA_HEADERS,
1560
+ fastMode: this.fastModeBetaHeaderLatched,
1561
+ }),
1562
+ 'anthropic-dangerous-direct-browser-access': 'true',
1563
+ 'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
1564
+ 'x-app': 'cli',
1565
+ 'Content-Type': 'application/json',
1566
+ },
1567
+ body: JSON.stringify(body),
1568
+ signal: controller.signal,
1569
+ dispatcher: getLlmDispatcher(),
1570
+ });
1571
+
1572
+ traceBridgeFetch({
1573
+ sessionId,
1574
+ headersMs: Date.now() - fetchStartedAt,
1575
+ httpStatus: response.status,
1576
+ provider: 'anthropic-oauth',
1577
+ model: useModel,
1578
+ transport: 'sse',
1579
+ });
1580
+
1581
+ if (attemptCancelHandler) {
1582
+ try { requestSignal.removeEventListener('abort', attemptCancelHandler); } catch {}
1583
+ }
1584
+ return { response, controller, cancelHandler };
1585
+ } catch (err) {
1586
+ if (attemptCancelHandler) {
1587
+ try { requestSignal.removeEventListener('abort', attemptCancelHandler); } catch {}
1588
+ }
1589
+ cleanupCancelHandler(cancelHandler);
1590
+ if (requestSignal?.aborted) {
1591
+ const reason = requestSignal.reason;
1592
+ throw reason instanceof Error ? reason : new Error('Anthropic OAuth request attempt aborted');
1593
+ }
1594
+ if (totalSignal?.aborted) {
1595
+ const reason = totalSignal.reason;
1596
+ throw reason instanceof Error ? reason : new Error('Anthropic OAuth request aborted by session close');
1597
+ }
1598
+ if (err?.name === 'AbortError') {
1599
+ const timeoutErr = new Error(`Anthropic OAuth API initial response timed out after ${PROVIDER_HTTP_RESPONSE_TIMEOUT_MS}ms`);
1600
+ timeoutErr.code = 'EPROVIDERTIMEOUT';
1601
+ throw timeoutErr;
1602
+ }
1603
+ throw err;
1604
+ }
1605
+ };
1606
+ // Test seam: injectable request factory for retry-path tests.
1607
+ const doRequestImpl = typeof opts._doRequestFn === 'function' ? opts._doRequestFn : doRequest;
1608
+
1609
+ const requestWithRetry = async (accessToken) => withRetry(async ({ signal: attemptSignal }) => {
1610
+ const result = await doRequestImpl(accessToken, attemptSignal);
1611
+ const status = Number(result?.response?.status || 0);
1612
+ const transientStatus = classifyError({ httpStatus: status }) === 'transient';
1613
+ if (transientStatus || status === 429) {
1614
+ const err = new Error(`Anthropic OAuth API ${status}`);
1615
+ err.httpStatus = status;
1616
+ err.status = status;
1617
+ err.headers = result?.response?.headers;
1618
+ err.response = { status, headers: result?.response?.headers };
1619
+ const retryAfterMs = retryAfterMsFromError(err);
1620
+ if (transientStatus || retryAfterMs != null) {
1621
+ try { await result.response.text(); } catch {}
1622
+ cleanupCancelHandler(result.cancelHandler);
1623
+ try { result.controller?.abort?.(); } catch {}
1624
+ throw err;
1625
+ }
1626
+ }
1627
+ return result;
1628
+ }, {
1629
+ signal: totalSignal,
1630
+ maxAttempts: PROVIDER_RETRY_MAX_ATTEMPTS,
1631
+ backoffMs: PROVIDER_RETRY_BACKOFF_MS,
1632
+ perAttemptTimeoutMs: PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
1633
+ perAttemptLabel: 'Anthropic OAuth initial response',
1634
+ onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
1635
+ const status = Number(lastErr?.httpStatus || lastErr?.status || lastErr?.response?.status || 0) || null;
1636
+ const reason = status || lastErr?.code || lastErr?.message || 'network error';
1637
+ const suffix = delayReason ? ` (${delayReason})` : '';
1638
+ try {
1639
+ process.stderr.write(
1640
+ `[anthropic-oauth] retry attempt ${attempt + 1}/${PROVIDER_RETRY_MAX_ATTEMPTS} after ${reason}, backoff ${delayMs}ms${suffix}\n`,
1641
+ );
1642
+ } catch {}
1643
+ },
1644
+ });
1645
+ // One retry only: enough to recover transient stream loss without
1646
+ // quietly replaying long-running work multiple times.
1647
+ const MAX_MIDSTREAM_RETRIES = 1;
1648
+ let firstAttemptError = null;
1649
+ let firstAttemptClassifier = null;
1650
+
1651
+ try {
1652
+ for (let attemptIndex = 0; attemptIndex <= MAX_MIDSTREAM_RETRIES; attemptIndex++) {
1653
+ let response, controller, cancelHandler;
1654
+ ({ response, controller, cancelHandler } = await requestWithRetry(creds.accessToken));
1655
+
1656
+ // 401: token expired/revoked. 403: organization permission flipped
1657
+ // (e.g. relogin into a different org). Both: force a shared refresh
1658
+ // and retry once with the new token.
1659
+ if (response.status === 401 || response.status === 403) {
1660
+ process.stderr.write(`[anthropic-oauth] ${response.status} — forcing refresh and retrying once\n`);
1661
+ cleanupCancelHandler(cancelHandler);
1662
+ creds = await this.ensureAuth({ forceRefresh: true, reason: String(response.status) });
1663
+ ({ response, controller, cancelHandler } = await requestWithRetry(creds.accessToken));
1664
+ }
1665
+
1666
+ if (!response.ok) {
1667
+ cleanupCancelHandler(cancelHandler);
1668
+ const text = await response.text().catch(() => '');
1669
+ const safeText = this.scrubTokens(text).slice(0, 200);
1670
+ process.stderr.write(`[anthropic-oauth] API error ${response.status}: ${safeText}\n`);
1671
+
1672
+ // Phase I: on unknown/404 model errors, force a catalog refresh and
1673
+ // retry once. Protects against a silently-rotated model id.
1674
+ const isUnknownModel = response.status === 404
1675
+ || /unknown[_\s-]?model|model[_\s-]?not[_\s-]?found/i.test(safeText);
1676
+ if (isUnknownModel && !opts._modelRetry) {
1677
+ process.stderr.write(`[anthropic-oauth] unknown model — refreshing catalog + 1 retry\n`);
1678
+ await this._refreshModelCache();
1679
+ const fallbackModel = resolveAnthropicModelAfter404(useModel);
1680
+ if (fallbackModel) {
1681
+ process.stderr.write(`[anthropic-oauth] model fallback ${useModel} -> ${fallbackModel}\n`);
1682
+ }
1683
+ return this.send(messages, fallbackModel || model, tools, { ...opts, _modelRetry: true });
1684
+ }
1685
+ throw new Error(`Anthropic OAuth API ${response.status}: ${safeText}`);
1686
+ }
1687
+
1688
+ if (SSE_VERBOSE) process.stderr.write(`[anthropic-oauth] Response ${response.status}, parsing SSE...\n`);
1689
+ try { onStageChange?.('streaming'); } catch {}
1690
+
1691
+ const midState = {
1692
+ attemptIndex,
1693
+ sawMessageStart: false,
1694
+ sawCompleted: false,
1695
+ emittedToolCall: false,
1696
+ // Gateway live-text relay invariant: set by parseSSEStream once
1697
+ // a non-empty text chunk has been forwarded to the client. A
1698
+ // later failure is non-retryable (rendered text cannot be
1699
+ // withdrawn; a retry would concatenate attempts).
1700
+ emittedText: false,
1701
+ userAbort: false,
1702
+ watchdogAbort: null,
1703
+ ttftAt: null,
1704
+ };
1705
+
1706
+ try {
1707
+ const sseStartedAt = Date.now();
1708
+ const result = await parseSSEFn(
1709
+ response,
1710
+ controller.signal,
1711
+ () => controller.abort(),
1712
+ onStreamDelta,
1713
+ onToolCall,
1714
+ midState,
1715
+ onTextDelta,
1716
+ );
1717
+
1718
+ const ttftMs = midState.ttftAt ? midState.ttftAt - sseStartedAt : null;
1719
+ const liveModel = result.model || useModel;
1720
+ traceBridgeSse({
1721
+ sessionId,
1722
+ sseParseMs: Date.now() - sseStartedAt,
1723
+ ttftMs,
1724
+ provider: 'anthropic-oauth',
1725
+ model: liveModel,
1726
+ transport: 'sse',
1727
+ });
1728
+
1729
+ traceBridgeUsage({
1730
+ sessionId,
1731
+ iteration,
1732
+ inputTokens: result.usage?.inputTokens || 0,
1733
+ outputTokens: result.usage?.outputTokens || 0,
1734
+ cachedTokens: result.usage?.cachedTokens || 0,
1735
+ cacheWriteTokens: result.usage?.cacheWriteTokens || 0,
1736
+ promptTokens: result.usage?.promptTokens || 0,
1737
+ model: liveModel,
1738
+ modelDisplay: _displayModel(liveModel),
1739
+ rawUsage: result.usage?.raw || null,
1740
+ provider: 'anthropic-oauth',
1741
+ requestKind: opts.requestKind || null,
1742
+ });
1743
+
1744
+ // Phase I: if the live response surfaced a model id we don't know
1745
+ // about yet, kick off a background catalog refresh. Fire-and-forget
1746
+ // — do not await, do not surface errors.
1747
+ if (result.model && !_catalogHas(result.model)) {
1748
+ void this._refreshModelCache();
1749
+ }
1750
+
1751
+ if (SSE_VERBOSE) process.stderr.write(`[anthropic-oauth] Done: ${result.content.length} chars, ${result.toolCalls?.length || 0} tool calls\n`);
1752
+ // Empty-stream guard. Invariant: a valid Anthropic SSE response
1753
+ // ALWAYS opens with message_start (which carries usage.input_tokens).
1754
+ // A 200 whose body produced no message_start delivered nothing —
1755
+ // no usage, no content, no tool calls — i.e. a dropped/empty stream
1756
+ // (transient, often rate-limit-adjacent under concurrent load), NOT
1757
+ // a valid terminal turn. Returning it surfaces upstream as a silent
1758
+ // empty turn (0 tokens, no content) that masks the cause. Throw a
1759
+ // marked error: retry is provably safe here (no message_start ⇒
1760
+ // nothing was emitted ⇒ no duplicate-tool risk), and once retries
1761
+ // are exhausted the error is surfaced instead of swallowed.
1762
+ if (!midState.sawMessageStart
1763
+ && !midState.userAbort
1764
+ && !midState.watchdogAbort
1765
+ && !result.content
1766
+ && !(result.toolCalls && result.toolCalls.length)
1767
+ && !(result.usage && result.usage.inputTokens > 0)) {
1768
+ const emptyErr = new Error('Anthropic OAuth SSE stream produced no message_start (empty/dropped stream — likely transient or rate-limited)');
1769
+ emptyErr.code = 'EEMPTYSTREAM';
1770
+ emptyErr.isEmptyStream = true;
1771
+ throw emptyErr;
1772
+ }
1773
+ try {
1774
+ Object.defineProperty(result, '__midstreamRetries', { value: attemptIndex, enumerable: false });
1775
+ } catch { /* ignore non-extensible result */ }
1776
+ return result;
1777
+ } catch (err) {
1778
+ // Live-text invariant: once a non-empty text chunk has been
1779
+ // relayed to the client (gateway live mode), the rendered output
1780
+ // cannot be withdrawn and re-issuing would concatenate a second
1781
+ // attempt. Surface the failure immediately — never retry — and
1782
+ // tag the error so upstream layers refuse to retry as well.
1783
+ if (midState.emittedText) {
1784
+ try { err.liveTextEmitted = true; err.unsafeToRetry = true; } catch {}
1785
+ try { controller?.abort?.(err); } catch { /* best-effort teardown */ }
1786
+ if (attemptIndex > 0 && firstAttemptError) {
1787
+ try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
1788
+ try { firstAttemptError.midstreamClassifier = firstAttemptClassifier; } catch {}
1789
+ throw firstAttemptError;
1790
+ }
1791
+ throw err;
1792
+ }
1793
+ // Empty/dropped stream (no message_start): safe to retry once —
1794
+ // nothing was emitted, so there is no duplicate-tool risk. This
1795
+ // is intentionally NOT routed through _classifyMidstreamError,
1796
+ // which requires sawMessageStart and would reject it.
1797
+ if (err?.isEmptyStream && attemptIndex < MAX_MIDSTREAM_RETRIES) {
1798
+ firstAttemptError = err;
1799
+ firstAttemptClassifier = 'empty_stream';
1800
+ try { controller?.abort?.(err); } catch { /* best-effort teardown */ }
1801
+ try { process.stderr.write(`[anthropic-oauth] empty stream (no message_start) — retry ${attemptIndex + 1}/${MAX_MIDSTREAM_RETRIES}\n`); } catch {}
1802
+ continue;
1803
+ }
1804
+ if (classifyError(err) === 'transient'
1805
+ && !midState.sawMessageStart
1806
+ && !midState.emittedToolCall
1807
+ && attemptIndex < MAX_MIDSTREAM_RETRIES) {
1808
+ firstAttemptError = err;
1809
+ firstAttemptClassifier = err?.providerErrorType || 'sse_transient';
1810
+ try { controller?.abort?.(err); } catch { /* best-effort teardown */ }
1811
+ try {
1812
+ process.stderr.write(`[anthropic-oauth] transient SSE error — retry ${attemptIndex + 1}/${MAX_MIDSTREAM_RETRIES} (${err?.providerErrorType || err?.message || 'unknown'})\n`);
1813
+ } catch {}
1814
+ continue;
1815
+ }
1816
+ // Truncated stream (message_start without message_stop): the
1817
+ // partial result is discarded and re-requesting is safe (a
1818
+ // pendingToolUse means the tool_use input JSON never completed).
1819
+ // _classifyMidstreamError does not cover this; route it through
1820
+ // the shared classifier so it inherits the cross-provider
1821
+ // transient policy instead of escaping and killing the worker.
1822
+ // Guard: parseSSEStream eagerly fires onToolCall and sets
1823
+ // emittedToolCall=true at content_block_stop, BEFORE message_stop.
1824
+ // If the stream truncates after that, retrying would
1825
+ // double-execute the tool. Only retry when nothing was emitted
1826
+ // yet; otherwise let the error surface.
1827
+ if ((err?.truncatedStream === true || err?.code === 'TRUNCATED_STREAM')
1828
+ && classifyError(err) === 'transient'
1829
+ && !midState.emittedToolCall
1830
+ && attemptIndex < MAX_MIDSTREAM_RETRIES) {
1831
+ firstAttemptError = err;
1832
+ firstAttemptClassifier = 'truncated_stream';
1833
+ try { controller?.abort?.(err); } catch { /* best-effort teardown */ }
1834
+ try { process.stderr.write(`[anthropic-oauth] truncated stream — retry ${attemptIndex + 1}/${MAX_MIDSTREAM_RETRIES}\n`); } catch {}
1835
+ continue;
1836
+ }
1837
+ const classifier = _classifyMidstreamError(err, midState);
1838
+ if (classifier && attemptIndex < MAX_MIDSTREAM_RETRIES) {
1839
+ firstAttemptError = err;
1840
+ firstAttemptClassifier = classifier;
1841
+ try { controller?.abort?.(err); } catch (abortErr) {
1842
+ /* best-effort stream teardown */
1843
+ try { process.stderr.write(`[anthropic-oauth] abort on stream error failed: ${abortErr?.message ?? String(abortErr)}\n`); } catch {}
1844
+ }
1845
+ try {
1846
+ process.stderr.write(`[anthropic-oauth] mid-stream recovered: retry ${attemptIndex + 1}/${MAX_MIDSTREAM_RETRIES} (cause: ${classifier})\n`);
1847
+ } catch {}
1848
+ continue;
1849
+ }
1850
+ if (attemptIndex > 0 && firstAttemptError) {
1851
+ try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
1852
+ try { firstAttemptError.midstreamClassifier = firstAttemptClassifier; } catch {}
1853
+ throw firstAttemptError;
1854
+ }
1855
+ throw err;
1856
+ } finally {
1857
+ cleanupCancelHandler(cancelHandler);
1858
+ }
1859
+ }
1860
+ throw firstAttemptError || new Error('Anthropic OAuth mid-stream retry: unreachable');
1861
+ } finally {
1862
+ totalTimeout.cleanup();
1863
+ }
1864
+ }
1865
+
1866
+ async listModels() {
1867
+ // Dynamic lookup via /v1/models — returns whatever Anthropic currently
1868
+ // exposes for this OAuth account. Cached on disk with 24h TTL; falls
1869
+ // back to the static MODELS list on any failure so the plugin still
1870
+ // works offline or when Anthropic's /v1/models is momentarily down.
1871
+ const cached = await _loadModelCache();
1872
+ if (cached) {
1873
+ _inMemoryCatalog = cached.slice();
1874
+ return cached;
1875
+ }
1876
+ try {
1877
+ const creds = await this.ensureAuth();
1878
+ const res = await fetch('https://api.anthropic.com/v1/models', {
1879
+ signal: AbortSignal.timeout(10_000),
1880
+ method: 'GET',
1881
+ headers: {
1882
+ 'Authorization': `Bearer ${creds.accessToken}`,
1883
+ 'anthropic-version': ANTHROPIC_VERSION,
1884
+ 'anthropic-beta': OAUTH_BETA_HEADERS,
1885
+ 'anthropic-dangerous-direct-browser-access': 'true',
1886
+ 'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
1887
+ 'x-app': 'cli',
1888
+ },
1889
+ dispatcher: getLlmDispatcher(),
1890
+ });
1891
+ if (!res.ok) throw new Error(`list_models ${res.status}`);
1892
+ const data = await res.json();
1893
+ const items = Array.isArray(data?.data) ? data.data : [];
1894
+ const normalized = items
1895
+ .map(m => _normalizeAnthropicModel(m))
1896
+ .filter(Boolean);
1897
+ _markLatestByFamily(normalized);
1898
+ // Enrich with LiteLLM catalog metadata (context, pricing, capabilities)
1899
+ const enriched = await enrichModels(normalized);
1900
+ await _saveModelCache(enriched);
1901
+ return enriched;
1902
+ } catch (err) {
1903
+ if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[anthropic-oauth] listModels fetch failed (${err.message})\n`);
1904
+ // Fallback with full API model IDs. Short family tokens leaked
1905
+ // through here would be accepted by setup and reintroduce the
1906
+ // legacy shape. Env var override keeps this tracking defaults.
1907
+ const opusId = process.env.ANTHROPIC_DEFAULT_OPUS_MODEL || 'claude-opus-4-8';
1908
+ const sonnetId = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL || 'claude-sonnet-4-6';
1909
+ const haikuId = process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL || 'claude-haiku-4-5-20251001';
1910
+ return [
1911
+ { id: opusId, display: 'Opus (auto)', family: 'opus', provider: 'anthropic-oauth', tier: 'family', latest: true, contextWindow: 1000000 },
1912
+ { id: sonnetId, display: 'Sonnet (auto)', family: 'sonnet', provider: 'anthropic-oauth', tier: 'family', latest: true, contextWindow: 1000000 },
1913
+ { id: haikuId, display: 'Haiku (auto)', family: 'haiku', provider: 'anthropic-oauth', tier: 'family', latest: true, contextWindow: 200000 },
1914
+ ];
1915
+ }
1916
+ }
1917
+
1918
+ // Force a catalog refresh (ignores the 24h TTL). De-duped via
1919
+ // _modelRefreshInFlight so concurrent callers share one HTTP round-trip.
1920
+ // Returns the new catalog on success, null on failure.
1921
+ async _refreshModelCache() {
1922
+ if (_modelRefreshInFlight) return _modelRefreshInFlight;
1923
+ _modelRefreshInFlight = (async () => {
1924
+ try {
1925
+ const creds = await this.ensureAuth();
1926
+ const res = await fetch('https://api.anthropic.com/v1/models', {
1927
+ signal: AbortSignal.timeout(10_000),
1928
+ method: 'GET',
1929
+ headers: {
1930
+ 'Authorization': `Bearer ${creds.accessToken}`,
1931
+ 'anthropic-version': ANTHROPIC_VERSION,
1932
+ 'anthropic-beta': OAUTH_BETA_HEADERS,
1933
+ 'anthropic-dangerous-direct-browser-access': 'true',
1934
+ 'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
1935
+ 'x-app': 'cli',
1936
+ },
1937
+ dispatcher: getLlmDispatcher(),
1938
+ });
1939
+ if (!res.ok) throw new Error(`list_models ${res.status}`);
1940
+ const data = await res.json();
1941
+ const items = Array.isArray(data?.data) ? data.data : [];
1942
+ const normalized = items
1943
+ .map(m => _normalizeAnthropicModel(m))
1944
+ .filter(Boolean);
1945
+ _markLatestByFamily(normalized);
1946
+ const enriched = await enrichModels(normalized);
1947
+ await _saveModelCache(enriched);
1948
+ if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[anthropic-oauth] catalog refreshed (${enriched.length} models)\n`);
1949
+ return enriched;
1950
+ } catch (err) {
1951
+ if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[anthropic-oauth] catalog refresh failed (${err.message})\n`);
1952
+ return null;
1953
+ } finally {
1954
+ _modelRefreshInFlight = null;
1955
+ }
1956
+ })();
1957
+ return _modelRefreshInFlight;
1958
+ }
1959
+
1960
+ async isAvailable() {
1961
+ return this.credentials !== null || loadCredentials() !== null;
1962
+ }
1963
+ }
1964
+
1965
+ // --- Login flow (PKCE loopback, export for setup UI / CLI) ---
1966
+
1967
+ function _oauthGeneratePKCE() {
1968
+ const verifier = randomBytes(32).toString('base64url');
1969
+ const challenge = createHash('sha256').update(verifier).digest('base64url');
1970
+ return { verifier, challenge };
1971
+ }
1972
+
1973
+ function _oauthCredentialsWritePath() {
1974
+ for (const p of credentialCandidates()) {
1975
+ if (existsSync(p)) return p;
1976
+ }
1977
+ return DEFAULT_CREDENTIALS_PATH;
1978
+ }
1979
+
1980
+ function _oauthParseScopeField(scope) {
1981
+ if (Array.isArray(scope)) return scope;
1982
+ return String(scope || '').split(' ').filter(Boolean);
1983
+ }
1984
+
1985
+ export async function loginOAuth() {
1986
+ const pkce = _oauthGeneratePKCE();
1987
+ const state = randomBytes(32).toString('base64url');
1988
+ const url = new URL(CLAUDE_AI_AUTHORIZE_URL);
1989
+ url.searchParams.set('code', 'true');
1990
+ url.searchParams.set('client_id', CLAUDE_CODE_CLIENT_ID);
1991
+ url.searchParams.set('response_type', 'code');
1992
+ url.searchParams.set('redirect_uri', OAUTH_REDIRECT_URI);
1993
+ url.searchParams.set('scope', OAUTH_LOGIN_SCOPE);
1994
+ url.searchParams.set('code_challenge', pkce.challenge);
1995
+ url.searchParams.set('code_challenge_method', 'S256');
1996
+ url.searchParams.set('state', state);
1997
+
1998
+ return new Promise((resolve, reject) => {
1999
+ let settled = false;
2000
+ let timeout = null;
2001
+ const settle = (value, error = null) => {
2002
+ if (settled) return;
2003
+ settled = true;
2004
+ if (timeout) clearTimeout(timeout);
2005
+ try { server.close(); } catch { /* already closed */ }
2006
+ if (error) reject(error);
2007
+ else resolve(value);
2008
+ };
2009
+ const server = createServer(async (req, res) => {
2010
+ const u = new URL(req.url || '/', `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}`);
2011
+ if (u.pathname !== OAUTH_CALLBACK_PATH) {
2012
+ res.writeHead(404);
2013
+ res.end();
2014
+ return;
2015
+ }
2016
+ const code = u.searchParams.get('code');
2017
+ if (!code || u.searchParams.get('state') !== state) {
2018
+ res.writeHead(400);
2019
+ res.end('Invalid');
2020
+ settle(null);
2021
+ return;
2022
+ }
2023
+ res.writeHead(200, { 'Content-Type': 'text/html' });
2024
+ res.end('<html><body><h2>Claude login successful! You can close this tab.</h2></body></html>');
2025
+ try {
2026
+ const tokenRes = await fetch(TOKEN_URL, {
2027
+ method: 'POST',
2028
+ headers: {
2029
+ 'Content-Type': 'application/json',
2030
+ 'anthropic-dangerous-direct-browser-access': 'true',
2031
+ 'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
2032
+ },
2033
+ body: JSON.stringify({
2034
+ grant_type: 'authorization_code',
2035
+ code,
2036
+ redirect_uri: OAUTH_REDIRECT_URI,
2037
+ client_id: CLAUDE_CODE_CLIENT_ID,
2038
+ code_verifier: pkce.verifier,
2039
+ state,
2040
+ }),
2041
+ redirect: 'error',
2042
+ signal: AbortSignal.timeout(OAUTH_TOKEN_TIMEOUT_MS),
2043
+ dispatcher: getLlmDispatcher(),
2044
+ });
2045
+ if (!tokenRes.ok) {
2046
+ const text = await tokenRes.text().catch(() => '');
2047
+ settle(null, new Error(`[anthropic-oauth] token exchange ${tokenRes.status}: ${_scrubTokens(text).slice(0, 500)}`));
2048
+ return;
2049
+ }
2050
+ const json = await tokenRes.json();
2051
+ const accessToken = json?.access_token || json?.accessToken;
2052
+ const refreshToken = json?.refresh_token || json?.refreshToken;
2053
+ if (!accessToken || !refreshToken) {
2054
+ settle(null, new Error('[anthropic-oauth] token exchange response missing access_token or refresh_token'));
2055
+ return;
2056
+ }
2057
+ const expiresAt = _normalizeExpiresAt(json?.expires_at ?? json?.expiresAt)
2058
+ || (typeof json?.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0);
2059
+ const scopes = _oauthParseScopeField(json?.scope);
2060
+ const credPath = _oauthCredentialsWritePath();
2061
+ let raw = {};
2062
+ if (existsSync(credPath)) {
2063
+ raw = JSON.parse(readFileSync(credPath, 'utf-8'));
2064
+ }
2065
+ const existingOauth = raw.claudeAiOauth || {};
2066
+ raw.claudeAiOauth = {
2067
+ ...existingOauth,
2068
+ accessToken,
2069
+ refreshToken,
2070
+ expiresAt,
2071
+ scopes,
2072
+ subscriptionType: existingOauth.subscriptionType ?? null,
2073
+ };
2074
+ _saveCredentialsFile(credPath, raw);
2075
+ resolve({
2076
+ path: credPath,
2077
+ accessToken,
2078
+ refreshToken,
2079
+ expiresAt,
2080
+ scopes,
2081
+ subscriptionType: raw.claudeAiOauth.subscriptionType,
2082
+ });
2083
+ } catch (err) {
2084
+ settle(null, err instanceof Error ? err : new Error(String(err)));
2085
+ }
2086
+ });
2087
+ timeout = setTimeout(() => settle(null), OAUTH_LOGIN_TIMEOUT_MS);
2088
+ server.listen(OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_HOST, async () => {
2089
+ process.stderr.write(`\n[anthropic-oauth] Open this URL to log in with Claude:\n${url.toString()}\n\n`);
2090
+ try {
2091
+ const { openInBrowser } = await import('../../../shared/open-url.mjs');
2092
+ openInBrowser(url.toString());
2093
+ } catch (err) {
2094
+ process.stderr.write(`[anthropic-oauth] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
2095
+ }
2096
+ });
2097
+ server.on('error', (err) => settle(null, new Error(`[anthropic-oauth] callback server failed on ${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}: ${err?.message || err}`)));
2098
+ });
2099
+ }
2100
+
2101
+ // Additive exports for test harnesses.
2102
+ // Lets the SSE parser be exercised in isolation against a synthetic
2103
+ // ReadableStream without needing a live OAuth session.
2104
+ export { parseSSEStream };