@otto-assistant/bridge 0.4.92

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 (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,861 @@
1
+ /**
2
+ * Anthropic OAuth authentication plugin for OpenCode.
3
+ *
4
+ * If you're copy-pasting this plugin into your OpenCode config folder,
5
+ * you need to install the runtime dependencies first:
6
+ *
7
+ * cd ~/.config/opencode
8
+ * bun init -y
9
+ * bun add proper-lockfile
10
+ *
11
+ * Handles three concerns:
12
+ * 1. OAuth login + token refresh (PKCE flow against claude.ai)
13
+ * 2. Request/response rewriting (tool names, system prompt, beta headers)
14
+ * so the Anthropic API treats requests as Claude Code CLI requests.
15
+ * 3. Multi-account OAuth rotation after Anthropic rate-limit/auth failures.
16
+ *
17
+ * Login mode is chosen from environment:
18
+ * - `KIMAKI` set: remote-first pasted callback URL/raw code flow
19
+ * - otherwise: standard localhost auto-complete flow
20
+ *
21
+ * Source references:
22
+ * - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/anthropic.ts
23
+ * - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts
24
+ */
25
+
26
+ import type { Plugin } from '@opencode-ai/plugin'
27
+ import {
28
+ loadAccountStore,
29
+ rememberAnthropicOAuth,
30
+ rotateAnthropicAccount,
31
+ saveAccountStore,
32
+ setAnthropicAuth,
33
+ shouldRotateAuth,
34
+ type OAuthStored,
35
+ upsertAccount,
36
+ withAuthStateLock,
37
+ } from './anthropic-auth-state.js'
38
+ // PKCE (Proof Key for Code Exchange) using Web Crypto API.
39
+ // Reference: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/pkce.ts
40
+ function base64urlEncode(bytes: Uint8Array): string {
41
+ let binary = ''
42
+ for (const byte of bytes) {
43
+ binary += String.fromCharCode(byte)
44
+ }
45
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
46
+ }
47
+
48
+ async function generatePKCE(): Promise<{ verifier: string; challenge: string }> {
49
+ const verifierBytes = new Uint8Array(32)
50
+ crypto.getRandomValues(verifierBytes)
51
+ const verifier = base64urlEncode(verifierBytes)
52
+ const data = new TextEncoder().encode(verifier)
53
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data)
54
+ const challenge = base64urlEncode(new Uint8Array(hashBuffer))
55
+ return { verifier, challenge }
56
+ }
57
+ import { spawn } from 'node:child_process'
58
+ import { createServer, type Server } from 'node:http'
59
+
60
+ // --- Constants ---
61
+
62
+ const CLIENT_ID = (() => {
63
+ const encoded = 'OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl'
64
+ return typeof atob === 'function'
65
+ ? atob(encoded)
66
+ : Buffer.from(encoded, 'base64').toString('utf8')
67
+ })()
68
+
69
+ const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token'
70
+ const CREATE_API_KEY_URL = 'https://api.anthropic.com/api/oauth/claude_cli/create_api_key'
71
+ const CALLBACK_PORT = 53692
72
+ const CALLBACK_PATH = '/callback'
73
+ const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`
74
+ const SCOPES =
75
+ 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload'
76
+ const OAUTH_TIMEOUT_MS = 5 * 60 * 1000
77
+ const CLAUDE_CODE_VERSION = '2.1.75'
78
+ const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude."
79
+ const OPENCODE_IDENTITY = 'You are OpenCode, the best coding agent on the planet.'
80
+ const CLAUDE_CODE_BETA = 'claude-code-20250219'
81
+ const OAUTH_BETA = 'oauth-2025-04-20'
82
+ const FINE_GRAINED_TOOL_STREAMING_BETA = 'fine-grained-tool-streaming-2025-05-14'
83
+ const INTERLEAVED_THINKING_BETA = 'interleaved-thinking-2025-05-14'
84
+
85
+ const ANTHROPIC_HOSTS = new Set([
86
+ 'api.anthropic.com',
87
+ 'claude.ai',
88
+ 'console.anthropic.com',
89
+ 'platform.claude.com',
90
+ ])
91
+
92
+ const OPENCODE_TO_CLAUDE_CODE_TOOL_NAME: Record<string, string> = {
93
+ bash: 'Bash',
94
+ edit: 'Edit',
95
+ glob: 'Glob',
96
+ grep: 'Grep',
97
+ question: 'AskUserQuestion',
98
+ read: 'Read',
99
+ skill: 'Skill',
100
+ task: 'Task',
101
+ todowrite: 'TodoWrite',
102
+ webfetch: 'WebFetch',
103
+ websearch: 'WebSearch',
104
+ write: 'Write',
105
+ }
106
+
107
+ // --- Types ---
108
+
109
+ type OAuthSuccess = {
110
+ type: 'success'
111
+ provider?: string
112
+ refresh: string
113
+ access: string
114
+ expires: number
115
+ }
116
+
117
+ type ApiKeySuccess = {
118
+ type: 'success'
119
+ provider?: string
120
+ key: string
121
+ }
122
+
123
+ type AuthResult = OAuthSuccess | ApiKeySuccess | { type: 'failed' }
124
+
125
+ // --- HTTP helpers ---
126
+
127
+ // Claude OAuth token exchange can 429 when this runs inside the opencode auth
128
+ // process, even with the same payload that succeeds in a plain Node process.
129
+ // Run these OAuth-only HTTP calls in an isolated Node child to avoid whatever
130
+ // parent-process runtime state is affecting the in-process requests.
131
+ async function requestText(
132
+ urlString: string,
133
+ options: {
134
+ method: string
135
+ headers?: Record<string, string>
136
+ body?: string
137
+ },
138
+ ): Promise<string> {
139
+ return new Promise((resolve, reject) => {
140
+ const payload = JSON.stringify({
141
+ body: options.body,
142
+ headers: options.headers,
143
+ method: options.method,
144
+ url: urlString,
145
+ })
146
+ const child = spawn(
147
+ 'node',
148
+ [
149
+ '-e',
150
+ `
151
+ const input = JSON.parse(process.argv[1]);
152
+ (async () => {
153
+ const response = await fetch(input.url, {
154
+ method: input.method,
155
+ headers: input.headers,
156
+ body: input.body,
157
+ });
158
+ const text = await response.text();
159
+ if (!response.ok) {
160
+ console.error(JSON.stringify({ status: response.status, body: text }));
161
+ process.exit(1);
162
+ }
163
+ process.stdout.write(text);
164
+ })().catch((error) => {
165
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error));
166
+ process.exit(1);
167
+ });
168
+ `.trim(),
169
+ payload,
170
+ ],
171
+ {
172
+ stdio: ['ignore', 'pipe', 'pipe'],
173
+ },
174
+ )
175
+
176
+ let stdout = ''
177
+ let stderr = ''
178
+ const timeout = setTimeout(() => {
179
+ child.kill()
180
+ reject(new Error(`Request timed out. url=${urlString}`))
181
+ }, 30_000)
182
+
183
+ child.stdout.on('data', (chunk) => {
184
+ stdout += String(chunk)
185
+ })
186
+ child.stderr.on('data', (chunk) => {
187
+ stderr += String(chunk)
188
+ })
189
+
190
+ child.on('error', (error) => {
191
+ clearTimeout(timeout)
192
+ reject(error)
193
+ })
194
+
195
+ child.on('close', (code) => {
196
+ clearTimeout(timeout)
197
+ if (code !== 0) {
198
+ let details = stderr.trim()
199
+ try {
200
+ const parsed = JSON.parse(details) as { status?: number; body?: string }
201
+ if (typeof parsed.status === 'number') {
202
+ reject(new Error(`HTTP ${parsed.status} from ${urlString}: ${parsed.body ?? ''}`))
203
+ return
204
+ }
205
+ } catch {
206
+ // fall back to raw stderr
207
+ }
208
+ reject(new Error(details || `Node helper exited with code ${code}`))
209
+ return
210
+ }
211
+ resolve(stdout)
212
+ })
213
+ })
214
+ }
215
+
216
+ async function postJson(url: string, body: Record<string, string | number>): Promise<unknown> {
217
+ const requestBody = JSON.stringify(body)
218
+ const responseText = await requestText(url, {
219
+ method: 'POST',
220
+ headers: {
221
+ Accept: 'application/json',
222
+ 'Content-Length': String(Buffer.byteLength(requestBody)),
223
+ 'Content-Type': 'application/json',
224
+ },
225
+ body: requestBody,
226
+ })
227
+ return JSON.parse(responseText) as unknown
228
+ }
229
+
230
+ const pendingRefresh = new Map<string, Promise<OAuthStored>>()
231
+
232
+ // --- OAuth token exchange & refresh ---
233
+
234
+ function parseTokenResponse(json: unknown): {
235
+ access_token: string
236
+ refresh_token: string
237
+ expires_in: number
238
+ } {
239
+ const data = json as { access_token: string; refresh_token: string; expires_in: number }
240
+ if (!data.access_token || !data.refresh_token) {
241
+ throw new Error(`Invalid token response: ${JSON.stringify(json)}`)
242
+ }
243
+ return data
244
+ }
245
+
246
+ function tokenExpiry(expiresIn: number) {
247
+ return Date.now() + expiresIn * 1000 - 5 * 60 * 1000
248
+ }
249
+
250
+ async function exchangeAuthorizationCode(
251
+ code: string,
252
+ state: string,
253
+ verifier: string,
254
+ redirectUri: string,
255
+ ): Promise<OAuthSuccess> {
256
+ const json = await postJson(TOKEN_URL, {
257
+ grant_type: 'authorization_code',
258
+ client_id: CLIENT_ID,
259
+ code,
260
+ state,
261
+ redirect_uri: redirectUri,
262
+ code_verifier: verifier,
263
+ })
264
+ const data = parseTokenResponse(json)
265
+ return {
266
+ type: 'success',
267
+ refresh: data.refresh_token,
268
+ access: data.access_token,
269
+ expires: tokenExpiry(data.expires_in),
270
+ }
271
+ }
272
+
273
+ async function refreshAnthropicToken(refreshToken: string): Promise<OAuthStored> {
274
+ const json = await postJson(TOKEN_URL, {
275
+ grant_type: 'refresh_token',
276
+ client_id: CLIENT_ID,
277
+ refresh_token: refreshToken,
278
+ })
279
+ const data = parseTokenResponse(json)
280
+ return {
281
+ type: 'oauth',
282
+ refresh: data.refresh_token,
283
+ access: data.access_token,
284
+ expires: tokenExpiry(data.expires_in),
285
+ }
286
+ }
287
+
288
+ async function createApiKey(accessToken: string): Promise<ApiKeySuccess> {
289
+ const responseText = await requestText(CREATE_API_KEY_URL, {
290
+ method: 'POST',
291
+ headers: {
292
+ Accept: 'application/json',
293
+ authorization: `Bearer ${accessToken}`,
294
+ 'Content-Type': 'application/json',
295
+ },
296
+ })
297
+ const json = JSON.parse(responseText) as { raw_key: string }
298
+ return { type: 'success', key: json.raw_key }
299
+ }
300
+
301
+ // --- Localhost callback server ---
302
+
303
+ type CallbackResult = { code: string; state: string }
304
+
305
+ async function startCallbackServer(expectedState: string) {
306
+ return new Promise<{
307
+ server: Server
308
+ cancelWait: () => void
309
+ waitForCode: () => Promise<CallbackResult | null>
310
+ }>((resolve, reject) => {
311
+ let settle: ((value: CallbackResult | null) => void) | undefined
312
+ let settled = false
313
+ const waitPromise = new Promise<CallbackResult | null>((res) => {
314
+ settle = (v) => {
315
+ if (settled) return
316
+ settled = true
317
+ res(v)
318
+ }
319
+ })
320
+
321
+ const server = createServer((req, res) => {
322
+ try {
323
+ const url = new URL(req.url || '', 'http://localhost')
324
+ if (url.pathname !== CALLBACK_PATH) {
325
+ res.writeHead(404).end('Not found')
326
+ return
327
+ }
328
+ const code = url.searchParams.get('code')
329
+ const state = url.searchParams.get('state')
330
+ const error = url.searchParams.get('error')
331
+ if (error || !code || !state || state !== expectedState) {
332
+ res.writeHead(400).end('Authentication failed: ' + (error || 'missing code/state'))
333
+ return
334
+ }
335
+ res
336
+ .writeHead(200, { 'Content-Type': 'text/plain' })
337
+ .end('Authentication successful. You can close this window.')
338
+ settle?.({ code, state })
339
+ } catch {
340
+ res.writeHead(500).end('Internal error')
341
+ }
342
+ })
343
+
344
+ server.once('error', reject)
345
+ server.listen(CALLBACK_PORT, '127.0.0.1', () => {
346
+ resolve({
347
+ server,
348
+ cancelWait: () => {
349
+ settle?.(null)
350
+ },
351
+ waitForCode: () => waitPromise,
352
+ })
353
+ })
354
+ })
355
+ }
356
+
357
+ function closeServer(server: Server) {
358
+ return new Promise<void>((resolve) => {
359
+ server.close(() => {
360
+ resolve()
361
+ })
362
+ })
363
+ }
364
+
365
+ // --- Authorization flow ---
366
+ // Unified flow: beginAuthorizationFlow starts PKCE + callback server,
367
+ // then waitForCallback handles both auto (localhost) and manual (pasted code) paths.
368
+
369
+ async function beginAuthorizationFlow() {
370
+ const pkce = await generatePKCE()
371
+ const callbackServer = await startCallbackServer(pkce.verifier)
372
+
373
+ const authParams = new URLSearchParams({
374
+ code: 'true',
375
+ client_id: CLIENT_ID,
376
+ response_type: 'code',
377
+ redirect_uri: REDIRECT_URI,
378
+ scope: SCOPES,
379
+ code_challenge: pkce.challenge,
380
+ code_challenge_method: 'S256',
381
+ state: pkce.verifier,
382
+ })
383
+
384
+ return {
385
+ url: `https://claude.ai/oauth/authorize?${authParams.toString()}`,
386
+ verifier: pkce.verifier,
387
+ callbackServer,
388
+ }
389
+ }
390
+
391
+ async function waitForCallback(
392
+ callbackServer: Awaited<ReturnType<typeof startCallbackServer>>,
393
+ manualInput?: string,
394
+ ): Promise<CallbackResult> {
395
+ try {
396
+ // Try localhost callback first (instant check)
397
+ const quick = await Promise.race([
398
+ callbackServer.waitForCode(),
399
+ new Promise<null>((r) => {
400
+ setTimeout(() => {
401
+ r(null)
402
+ }, 50)
403
+ }),
404
+ ])
405
+ if (quick?.code) return quick
406
+
407
+ // If manual input was provided, parse it
408
+ const trimmed = manualInput?.trim()
409
+ if (trimmed) {
410
+ return parseManualInput(trimmed)
411
+ }
412
+
413
+ // Wait for localhost callback with timeout
414
+ const result = await Promise.race([
415
+ callbackServer.waitForCode(),
416
+ new Promise<null>((r) => {
417
+ setTimeout(() => {
418
+ r(null)
419
+ }, OAUTH_TIMEOUT_MS)
420
+ }),
421
+ ])
422
+ if (!result?.code) {
423
+ throw new Error('Timed out waiting for OAuth callback')
424
+ }
425
+ return result
426
+ } finally {
427
+ callbackServer.cancelWait()
428
+ await closeServer(callbackServer.server)
429
+ }
430
+ }
431
+
432
+ function parseManualInput(input: string): CallbackResult {
433
+ try {
434
+ const url = new URL(input)
435
+ const code = url.searchParams.get('code')
436
+ const state = url.searchParams.get('state')
437
+ if (code) return { code, state: state || '' }
438
+ } catch {
439
+ // not a URL
440
+ }
441
+ if (input.includes('#')) {
442
+ const [code = '', state = ''] = input.split('#', 2)
443
+ return { code, state }
444
+ }
445
+ if (input.includes('code=')) {
446
+ const params = new URLSearchParams(input)
447
+ const code = params.get('code')
448
+ if (code) return { code, state: params.get('state') || '' }
449
+ }
450
+ return { code: input, state: '' }
451
+ }
452
+
453
+ // Unified authorize handler: returns either OAuth tokens or an API key,
454
+ // for both auto and remote-first modes.
455
+ function buildAuthorizeHandler(mode: 'oauth' | 'apikey') {
456
+ return async () => {
457
+ const auth = await beginAuthorizationFlow()
458
+ const isRemote = Boolean(process.env.KIMAKI)
459
+ let pendingAuthResult: Promise<AuthResult> | undefined
460
+
461
+ const finalize = async (result: CallbackResult): Promise<AuthResult> => {
462
+ const verifier = auth.verifier
463
+ const creds = await exchangeAuthorizationCode(
464
+ result.code,
465
+ result.state || verifier,
466
+ verifier,
467
+ REDIRECT_URI,
468
+ )
469
+ if (mode === 'apikey') {
470
+ return createApiKey(creds.access)
471
+ }
472
+ await rememberAnthropicOAuth({
473
+ type: 'oauth',
474
+ refresh: creds.refresh,
475
+ access: creds.access,
476
+ expires: creds.expires,
477
+ })
478
+ return creds
479
+ }
480
+
481
+ if (!isRemote) {
482
+ return {
483
+ url: auth.url,
484
+ instructions:
485
+ 'Complete login in your browser on this machine. OpenCode will catch the localhost callback automatically.',
486
+ method: 'auto' as const,
487
+ callback: async (): Promise<AuthResult> => {
488
+ pendingAuthResult ??= (async () => {
489
+ try {
490
+ const result = await waitForCallback(auth.callbackServer)
491
+ return await finalize(result)
492
+ } catch (error) {
493
+ console.error(`[anthropic-auth] ${error}`)
494
+ return { type: 'failed' }
495
+ }
496
+ })()
497
+ return pendingAuthResult
498
+ },
499
+ }
500
+ }
501
+
502
+ return {
503
+ url: auth.url,
504
+ instructions:
505
+ 'Complete login in your browser, then paste the final redirect URL from the address bar here. Pasting just the authorization code also works.',
506
+ method: 'code' as const,
507
+ callback: async (input: string): Promise<AuthResult> => {
508
+ pendingAuthResult ??= (async () => {
509
+ try {
510
+ const result = await waitForCallback(auth.callbackServer, input)
511
+ return await finalize(result)
512
+ } catch (error) {
513
+ console.error(`[anthropic-auth] ${error}`)
514
+ return { type: 'failed' }
515
+ }
516
+ })()
517
+ return pendingAuthResult
518
+ },
519
+ }
520
+ }
521
+ }
522
+
523
+ // --- Request/response rewriting ---
524
+ // Renames opencode tool names to Claude Code tool names in requests,
525
+ // and reverses the mapping in streamed responses.
526
+
527
+ function toClaudeCodeToolName(name: string) {
528
+ return OPENCODE_TO_CLAUDE_CODE_TOOL_NAME[name.toLowerCase()] ?? name
529
+ }
530
+
531
+ function sanitizeSystemText(text: string) {
532
+ return text.replaceAll(OPENCODE_IDENTITY, CLAUDE_CODE_IDENTITY)
533
+ }
534
+
535
+ function prependClaudeCodeIdentity(system: unknown) {
536
+ const identityBlock = { type: 'text', text: CLAUDE_CODE_IDENTITY }
537
+
538
+ if (typeof system === 'undefined') return [identityBlock]
539
+
540
+ if (typeof system === 'string') {
541
+ const sanitized = sanitizeSystemText(system)
542
+ if (sanitized === CLAUDE_CODE_IDENTITY) return [identityBlock]
543
+ return [identityBlock, { type: 'text', text: sanitized }]
544
+ }
545
+
546
+ if (!Array.isArray(system)) return [identityBlock, system]
547
+
548
+ const sanitized = system.map((item) => {
549
+ if (typeof item === 'string') return { type: 'text', text: sanitizeSystemText(item) }
550
+ if (item && typeof item === 'object' && (item as { type?: unknown }).type === 'text') {
551
+ const text = (item as { text?: unknown }).text
552
+ if (typeof text === 'string') {
553
+ return { ...(item as Record<string, unknown>), text: sanitizeSystemText(text) }
554
+ }
555
+ }
556
+ return item
557
+ })
558
+
559
+ const first = sanitized[0]
560
+ if (
561
+ first &&
562
+ typeof first === 'object' &&
563
+ (first as { type?: unknown }).type === 'text' &&
564
+ (first as { text?: unknown }).text === CLAUDE_CODE_IDENTITY
565
+ ) {
566
+ return sanitized
567
+ }
568
+ return [identityBlock, ...sanitized]
569
+ }
570
+
571
+ function rewriteRequestPayload(body: string | undefined) {
572
+ if (!body) return { body, modelId: undefined, reverseToolNameMap: new Map<string, string>() }
573
+
574
+ try {
575
+ const payload = JSON.parse(body) as Record<string, unknown>
576
+ const reverseToolNameMap = new Map<string, string>()
577
+ const modelId = typeof payload.model === 'string' ? payload.model : undefined
578
+
579
+ // Build reverse map and rename tools
580
+ if (Array.isArray(payload.tools)) {
581
+ payload.tools = payload.tools.map((tool) => {
582
+ if (!tool || typeof tool !== 'object') return tool
583
+ const name = (tool as { name?: unknown }).name
584
+ if (typeof name !== 'string') return tool
585
+ const mapped = toClaudeCodeToolName(name)
586
+ reverseToolNameMap.set(mapped, name)
587
+ return { ...(tool as Record<string, unknown>), name: mapped }
588
+ })
589
+ }
590
+
591
+ // Rename system prompt
592
+ payload.system = prependClaudeCodeIdentity(payload.system)
593
+
594
+ // Rename tool_choice
595
+ if (
596
+ payload.tool_choice &&
597
+ typeof payload.tool_choice === 'object' &&
598
+ (payload.tool_choice as { type?: unknown }).type === 'tool'
599
+ ) {
600
+ const name = (payload.tool_choice as { name?: unknown }).name
601
+ if (typeof name === 'string') {
602
+ payload.tool_choice = {
603
+ ...(payload.tool_choice as Record<string, unknown>),
604
+ name: toClaudeCodeToolName(name),
605
+ }
606
+ }
607
+ }
608
+
609
+ // Rename tool_use blocks in messages
610
+ if (Array.isArray(payload.messages)) {
611
+ payload.messages = payload.messages.map((message) => {
612
+ if (!message || typeof message !== 'object') return message
613
+ const content = (message as { content?: unknown }).content
614
+ if (!Array.isArray(content)) return message
615
+ return {
616
+ ...(message as Record<string, unknown>),
617
+ content: content.map((block) => {
618
+ if (!block || typeof block !== 'object') return block
619
+ const b = block as { type?: unknown; name?: unknown }
620
+ if (b.type !== 'tool_use' || typeof b.name !== 'string') return block
621
+ return { ...(block as Record<string, unknown>), name: toClaudeCodeToolName(b.name) }
622
+ }),
623
+ }
624
+ })
625
+ }
626
+
627
+ return { body: JSON.stringify(payload), modelId, reverseToolNameMap }
628
+ } catch {
629
+ return { body, modelId: undefined, reverseToolNameMap: new Map<string, string>() }
630
+ }
631
+ }
632
+
633
+ function wrapResponseStream(response: Response, reverseToolNameMap: Map<string, string>) {
634
+ if (!response.body || reverseToolNameMap.size === 0) return response
635
+
636
+ const reader = response.body.getReader()
637
+ const decoder = new TextDecoder()
638
+ const encoder = new TextEncoder()
639
+ let carry = ''
640
+
641
+ const transform = (text: string) => {
642
+ return text.replace(/"name"\s*:\s*"([^"]+)"/g, (full, name: string) => {
643
+ const original = reverseToolNameMap.get(name)
644
+ return original ? full.replace(`"${name}"`, `"${original}"`) : full
645
+ })
646
+ }
647
+
648
+ const stream = new ReadableStream<Uint8Array>({
649
+ async pull(controller) {
650
+ const { done, value } = await reader.read()
651
+ if (done) {
652
+ const finalText = carry + decoder.decode()
653
+ if (finalText) controller.enqueue(encoder.encode(transform(finalText)))
654
+ controller.close()
655
+ return
656
+ }
657
+ carry += decoder.decode(value, { stream: true })
658
+ // Buffer 256 chars to avoid splitting JSON keys across chunks
659
+ if (carry.length <= 256) return
660
+ const output = carry.slice(0, -256)
661
+ carry = carry.slice(-256)
662
+ controller.enqueue(encoder.encode(transform(output)))
663
+ },
664
+ async cancel(reason) {
665
+ await reader.cancel(reason)
666
+ },
667
+ })
668
+
669
+ return new Response(stream, {
670
+ status: response.status,
671
+ statusText: response.statusText,
672
+ headers: response.headers,
673
+ })
674
+ }
675
+
676
+ // --- Beta headers ---
677
+
678
+ function getRequiredBetas(modelId: string | undefined) {
679
+ const betas = [CLAUDE_CODE_BETA, OAUTH_BETA, FINE_GRAINED_TOOL_STREAMING_BETA]
680
+ const isAdaptive =
681
+ modelId?.includes('opus-4-6') ||
682
+ modelId?.includes('opus-4.6') ||
683
+ modelId?.includes('sonnet-4-6') ||
684
+ modelId?.includes('sonnet-4.6')
685
+ if (!isAdaptive) betas.push(INTERLEAVED_THINKING_BETA)
686
+ return betas
687
+ }
688
+
689
+ function mergeBetas(existing: string | null, required: string[]) {
690
+ return [
691
+ ...new Set([
692
+ ...required,
693
+ ...(existing || '')
694
+ .split(',')
695
+ .map((s) => s.trim())
696
+ .filter(Boolean),
697
+ ]),
698
+ ].join(',')
699
+ }
700
+
701
+ // --- Token refresh with dedup ---
702
+
703
+ function isOAuthStored(auth: { type: string }): auth is OAuthStored {
704
+ return auth.type === 'oauth'
705
+ }
706
+
707
+ async function getFreshOAuth(
708
+ getAuth: () => Promise<OAuthStored | { type: string }>,
709
+ client: Parameters<Plugin>[0]['client'],
710
+ ) {
711
+ const auth = await getAuth()
712
+ if (!isOAuthStored(auth)) return undefined
713
+ if (auth.access && auth.expires > Date.now()) return auth
714
+
715
+ const pending = pendingRefresh.get(auth.refresh)
716
+ if (pending) {
717
+ return pending
718
+ }
719
+
720
+ const refreshPromise = withAuthStateLock(async () => {
721
+ const latest = await getAuth()
722
+ if (!isOAuthStored(latest)) {
723
+ throw new Error('Anthropic OAuth credentials disappeared during refresh')
724
+ }
725
+ if (latest.access && latest.expires > Date.now()) return latest
726
+
727
+ const refreshed = await refreshAnthropicToken(latest.refresh)
728
+ await setAnthropicAuth(refreshed, client)
729
+ const store = await loadAccountStore()
730
+ if (store.accounts.length > 0) {
731
+ upsertAccount(store, refreshed)
732
+ await saveAccountStore(store)
733
+ }
734
+ return refreshed
735
+ })
736
+ pendingRefresh.set(auth.refresh, refreshPromise)
737
+ return refreshPromise.finally(() => {
738
+ pendingRefresh.delete(auth.refresh)
739
+ })
740
+ }
741
+
742
+ // --- Plugin export ---
743
+
744
+ const AnthropicAuthPlugin: Plugin = async ({ client }) => {
745
+ return {
746
+ auth: {
747
+ provider: 'anthropic',
748
+ async loader(
749
+ getAuth: () => Promise<OAuthStored | { type: string }>,
750
+ provider: { models: Record<string, { cost?: unknown }> },
751
+ ) {
752
+ const auth = await getAuth()
753
+ if (auth.type !== 'oauth') return {}
754
+
755
+ // Zero out costs for OAuth users (Claude Pro/Max subscription)
756
+ for (const model of Object.values(provider.models)) {
757
+ model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } }
758
+ }
759
+
760
+ return {
761
+ apiKey: '',
762
+ async fetch(input: Request | string | URL, init?: RequestInit) {
763
+ const url = (() => {
764
+ try {
765
+ return new URL(input instanceof Request ? input.url : input.toString())
766
+ } catch {
767
+ return null
768
+ }
769
+ })()
770
+ if (!url || !ANTHROPIC_HOSTS.has(url.hostname)) return fetch(input, init)
771
+
772
+ const originalBody =
773
+ typeof init?.body === 'string'
774
+ ? init.body
775
+ : input instanceof Request
776
+ ? await input
777
+ .clone()
778
+ .text()
779
+ .catch(() => undefined)
780
+ : undefined
781
+
782
+ const rewritten = rewriteRequestPayload(originalBody)
783
+ const headers = new Headers(init?.headers)
784
+ if (input instanceof Request) {
785
+ input.headers.forEach((v, k) => {
786
+ if (!headers.has(k)) headers.set(k, v)
787
+ })
788
+ }
789
+ const betas = getRequiredBetas(rewritten.modelId)
790
+
791
+ const runRequest = async (auth: OAuthStored) => {
792
+ const requestHeaders = new Headers(headers)
793
+ requestHeaders.set('accept', 'application/json')
794
+ requestHeaders.set(
795
+ 'anthropic-beta',
796
+ mergeBetas(requestHeaders.get('anthropic-beta'), betas),
797
+ )
798
+ requestHeaders.set('anthropic-dangerous-direct-browser-access', 'true')
799
+ requestHeaders.set('authorization', `Bearer ${auth.access}`)
800
+ requestHeaders.set(
801
+ 'user-agent',
802
+ process.env.OPENCODE_ANTHROPIC_USER_AGENT || `claude-cli/${CLAUDE_CODE_VERSION}`,
803
+ )
804
+ requestHeaders.set('x-app', 'cli')
805
+ requestHeaders.delete('x-api-key')
806
+
807
+ return fetch(input, {
808
+ ...(init ?? {}),
809
+ body: rewritten.body,
810
+ headers: requestHeaders,
811
+ })
812
+ }
813
+
814
+ const freshAuth = await getFreshOAuth(getAuth, client)
815
+ if (!freshAuth) return fetch(input, init)
816
+
817
+ let response = await runRequest(freshAuth)
818
+ if (!response.ok) {
819
+ const bodyText = await response
820
+ .clone()
821
+ .text()
822
+ .catch(() => '')
823
+ if (shouldRotateAuth(response.status, bodyText)) {
824
+ const rotated = await rotateAnthropicAccount(freshAuth, client)
825
+ if (rotated) {
826
+ const retryAuth = await getFreshOAuth(getAuth, client)
827
+ if (retryAuth) {
828
+ response = await runRequest(retryAuth)
829
+ }
830
+ }
831
+ }
832
+ }
833
+
834
+ return wrapResponseStream(response, rewritten.reverseToolNameMap)
835
+ },
836
+ }
837
+ },
838
+ methods: [
839
+ {
840
+ label: 'Claude Pro/Max',
841
+ type: 'oauth',
842
+ authorize: buildAuthorizeHandler('oauth'),
843
+ },
844
+ {
845
+ label: 'Create an API Key',
846
+ type: 'oauth',
847
+ authorize: buildAuthorizeHandler('apikey'),
848
+ },
849
+ {
850
+ provider: 'anthropic',
851
+ label: 'Manually enter API Key',
852
+ type: 'api',
853
+ },
854
+ ],
855
+ },
856
+ }
857
+ }
858
+
859
+ export {
860
+ AnthropicAuthPlugin as anthropicAuthPlugin,
861
+ }