@otto-assistant/otto 0.1.2 → 0.7.16

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 (638) 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-account-identity.js +62 -0
  7. package/dist/anthropic-account-identity.test.js +38 -0
  8. package/dist/anthropic-auth-plugin.js +917 -0
  9. package/dist/anthropic-auth-state.js +303 -0
  10. package/dist/anthropic-auth-state.test.js +150 -0
  11. package/dist/bin.js +152 -0
  12. package/dist/btw-prefix-detection.js +17 -0
  13. package/dist/btw-prefix-detection.test.js +63 -0
  14. package/dist/channel-management.js +259 -0
  15. package/dist/cli-parsing.test.js +142 -0
  16. package/dist/cli-send-thread.e2e.test.js +353 -0
  17. package/dist/cli-telegram-options.test.js +99 -0
  18. package/dist/cli.js +4210 -568
  19. package/dist/commands/abort.js +65 -0
  20. package/dist/commands/action-buttons.js +245 -0
  21. package/dist/commands/add-dir.js +124 -0
  22. package/dist/commands/add-dir.test.js +126 -0
  23. package/dist/commands/add-project.js +113 -0
  24. package/dist/commands/agent.js +355 -0
  25. package/dist/commands/ask-question.js +320 -0
  26. package/dist/commands/ask-question.test.js +92 -0
  27. package/dist/commands/btw.js +121 -0
  28. package/dist/commands/cli-commands-group-a.test.js +728 -0
  29. package/dist/commands/cli-commands-group-b.test.js +695 -0
  30. package/dist/commands/compact.js +120 -0
  31. package/dist/commands/context-usage.js +140 -0
  32. package/dist/commands/create-new-project.js +130 -0
  33. package/dist/commands/diff.js +63 -0
  34. package/dist/commands/discord-commands-group-a.test.js +655 -0
  35. package/dist/commands/discord-commands-group-b.test.js +595 -0
  36. package/dist/commands/discord-commands-group-c.test.js +739 -0
  37. package/dist/commands/file-upload.js +275 -0
  38. package/dist/commands/fork-subagent.js +177 -0
  39. package/dist/commands/fork.js +262 -0
  40. package/dist/commands/gemini-apikey.js +70 -0
  41. package/dist/commands/login.js +893 -0
  42. package/dist/commands/mcp.js +239 -0
  43. package/dist/commands/memory-snapshot.js +24 -0
  44. package/dist/commands/mention-mode.js +44 -0
  45. package/dist/commands/merge-worktree.js +162 -0
  46. package/dist/commands/model-variant.js +369 -0
  47. package/dist/commands/model.js +798 -0
  48. package/dist/commands/new-worktree.js +465 -0
  49. package/dist/commands/paginated-select.js +57 -0
  50. package/dist/commands/permissions.js +274 -0
  51. package/dist/commands/queue.js +223 -0
  52. package/dist/commands/remove-project.js +115 -0
  53. package/dist/commands/restart-opencode-server.js +127 -0
  54. package/dist/commands/resume.js +149 -0
  55. package/dist/commands/run-command.js +79 -0
  56. package/dist/commands/screenshare.js +303 -0
  57. package/dist/commands/screenshare.test.js +20 -0
  58. package/dist/commands/session-id.js +78 -0
  59. package/dist/commands/session.js +176 -0
  60. package/dist/commands/share.js +80 -0
  61. package/dist/commands/tasks.js +205 -0
  62. package/dist/commands/thread-deletion-sync.js +50 -0
  63. package/dist/commands/types.js +2 -0
  64. package/dist/commands/undo-redo.js +305 -0
  65. package/dist/commands/unset-model.js +139 -0
  66. package/dist/commands/upgrade.js +48 -0
  67. package/dist/commands/user-command.js +155 -0
  68. package/dist/commands/verbosity.js +125 -0
  69. package/dist/commands/vscode.js +269 -0
  70. package/dist/commands/worktree-settings.js +43 -0
  71. package/dist/commands/worktrees.js +468 -0
  72. package/dist/condense-memory.js +33 -0
  73. package/dist/config.js +100 -255
  74. package/dist/context-awareness-plugin.js +340 -0
  75. package/dist/context-awareness-plugin.test.js +126 -0
  76. package/dist/critique-utils.js +95 -0
  77. package/dist/database.js +1355 -0
  78. package/dist/db.js +260 -0
  79. package/dist/db.test.js +138 -0
  80. package/dist/debounce-timeout.js +28 -0
  81. package/dist/debounced-process-flush.js +77 -0
  82. package/dist/discord-bot.js +1124 -0
  83. package/dist/discord-command-registration.js +567 -0
  84. package/dist/discord-urls.js +82 -0
  85. package/dist/discord-utils.js +616 -0
  86. package/dist/discord-utils.test.js +134 -0
  87. package/dist/errors.js +179 -0
  88. package/dist/escape-backticks.test.js +429 -0
  89. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  90. package/dist/eventsource-parser.test.js +327 -0
  91. package/dist/exec-async.js +26 -0
  92. package/dist/external-opencode-sync.js +480 -0
  93. package/dist/format-tables.js +491 -0
  94. package/dist/format-tables.test.js +478 -0
  95. package/dist/forum-sync/config.js +79 -0
  96. package/dist/forum-sync/discord-operations.js +154 -0
  97. package/dist/forum-sync/index.js +5 -0
  98. package/dist/forum-sync/markdown.js +113 -0
  99. package/dist/forum-sync/sync-to-discord.js +417 -0
  100. package/dist/forum-sync/sync-to-files.js +190 -0
  101. package/dist/forum-sync/types.js +53 -0
  102. package/dist/forum-sync/watchers.js +307 -0
  103. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  104. package/dist/gateway-proxy.e2e.test.js +485 -0
  105. package/dist/genai-worker-wrapper.js +111 -0
  106. package/dist/genai-worker.js +311 -0
  107. package/dist/genai.js +232 -0
  108. package/dist/generated/browser.js +17 -0
  109. package/dist/generated/client.js +37 -0
  110. package/dist/generated/commonInputTypes.js +10 -0
  111. package/dist/generated/enums.js +58 -0
  112. package/dist/generated/internal/class.js +49 -0
  113. package/dist/generated/internal/prismaNamespace.js +254 -0
  114. package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
  115. package/dist/generated/models/bot_api_keys.js +1 -0
  116. package/dist/generated/models/bot_tokens.js +1 -0
  117. package/dist/generated/models/channel_agents.js +1 -0
  118. package/dist/generated/models/channel_directories.js +1 -0
  119. package/dist/generated/models/channel_mention_mode.js +1 -0
  120. package/dist/generated/models/channel_models.js +1 -0
  121. package/dist/generated/models/channel_verbosity.js +1 -0
  122. package/dist/generated/models/channel_worktrees.js +1 -0
  123. package/dist/generated/models/forum_sync_configs.js +1 -0
  124. package/dist/generated/models/global_models.js +1 -0
  125. package/dist/generated/models/ipc_requests.js +1 -0
  126. package/dist/generated/models/part_messages.js +1 -0
  127. package/dist/generated/models/scheduled_tasks.js +1 -0
  128. package/dist/generated/models/session_agents.js +1 -0
  129. package/dist/generated/models/session_events.js +1 -0
  130. package/dist/generated/models/session_models.js +1 -0
  131. package/dist/generated/models/session_start_sources.js +1 -0
  132. package/dist/generated/models/thread_sessions.js +1 -0
  133. package/dist/generated/models/thread_worktrees.js +1 -0
  134. package/dist/generated/models.js +1 -0
  135. package/dist/heap-monitor.js +122 -0
  136. package/dist/hrana-server.js +251 -0
  137. package/dist/hrana-server.test.js +370 -0
  138. package/dist/html-actions.js +123 -0
  139. package/dist/html-actions.test.js +70 -0
  140. package/dist/html-components.js +117 -0
  141. package/dist/html-components.test.js +34 -0
  142. package/dist/image-optimizer-plugin.js +153 -0
  143. package/dist/image-utils.js +112 -0
  144. package/dist/interaction-handler.js +420 -0
  145. package/dist/ipc-polling.js +327 -0
  146. package/dist/ipc-tools-plugin.js +193 -0
  147. package/dist/ipc-utils.js +18 -0
  148. package/dist/limit-heading-depth.js +25 -0
  149. package/dist/limit-heading-depth.test.js +105 -0
  150. package/dist/logger.js +171 -0
  151. package/dist/markdown.js +342 -0
  152. package/dist/markdown.test.js +264 -0
  153. package/dist/memory-overview-plugin.js +128 -0
  154. package/dist/message-finish-field.e2e.test.js +168 -0
  155. package/dist/message-formatting.js +415 -0
  156. package/dist/message-formatting.test.js +115 -0
  157. package/dist/message-preprocessing.js +359 -0
  158. package/dist/onboarding-tutorial.js +163 -0
  159. package/dist/onboarding-welcome.js +37 -0
  160. package/dist/openai-realtime.js +224 -0
  161. package/dist/opencode-command-detection.js +65 -0
  162. package/dist/opencode-command-detection.test.js +240 -0
  163. package/dist/opencode-command.js +131 -0
  164. package/dist/opencode-command.test.js +48 -0
  165. package/dist/opencode-interrupt-plugin.js +388 -0
  166. package/dist/opencode-interrupt-plugin.test.js +463 -0
  167. package/dist/opencode.js +1124 -0
  168. package/dist/otto/branding.js +22 -0
  169. package/dist/otto/index.js +21 -0
  170. package/dist/otto-digital-twin.e2e.test.js +161 -0
  171. package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
  172. package/dist/otto-opencode-plugin.js +21 -0
  173. package/dist/otto-opencode-plugin.test.js +98 -0
  174. package/dist/parse-permission-rules.test.js +117 -0
  175. package/dist/patch-text-parser.js +97 -0
  176. package/dist/plugin-logger.js +68 -0
  177. package/dist/privacy-sanitizer.js +105 -0
  178. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  179. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  180. package/dist/queue-advanced-e2e-setup.js +790 -0
  181. package/dist/queue-advanced-footer.e2e.test.js +481 -0
  182. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  183. package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
  184. package/dist/queue-advanced-question.e2e.test.js +261 -0
  185. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  186. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  187. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  188. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  189. package/dist/queue-question-select-drain.e2e.test.js +256 -0
  190. package/dist/runtime-idle-sweeper.js +52 -0
  191. package/dist/runtime-lifecycle.e2e.test.js +514 -0
  192. package/dist/sentry.js +23 -0
  193. package/dist/session-handler/agent-utils.js +67 -0
  194. package/dist/session-handler/event-stream-state.js +475 -0
  195. package/dist/session-handler/event-stream-state.test.js +632 -0
  196. package/dist/session-handler/model-utils.js +147 -0
  197. package/dist/session-handler/opencode-session-event-log.js +94 -0
  198. package/dist/session-handler/thread-runtime-state.js +131 -0
  199. package/dist/session-handler/thread-session-runtime.js +3390 -0
  200. package/dist/session-handler.js +9 -0
  201. package/dist/session-search.js +100 -0
  202. package/dist/session-search.test.js +40 -0
  203. package/dist/session-title-rename.test.js +92 -0
  204. package/dist/skill-filter.js +31 -0
  205. package/dist/skill-filter.test.js +65 -0
  206. package/dist/startup-service.js +153 -0
  207. package/dist/startup-time.e2e.test.js +296 -0
  208. package/dist/store.js +19 -0
  209. package/dist/subagent-rate-limit-plugin.js +175 -0
  210. package/dist/system-message.js +702 -0
  211. package/dist/system-message.test.js +697 -0
  212. package/dist/task-runner.js +530 -0
  213. package/dist/task-schedule.js +213 -0
  214. package/dist/task-schedule.test.js +71 -0
  215. package/dist/test-utils.js +313 -0
  216. package/dist/thinking-utils.js +35 -0
  217. package/dist/thread-message-queue.e2e.test.js +1111 -0
  218. package/dist/tools.js +357 -0
  219. package/dist/undo-redo.e2e.test.js +161 -0
  220. package/dist/unnest-code-blocks.js +146 -0
  221. package/dist/unnest-code-blocks.test.js +673 -0
  222. package/dist/upgrade.js +156 -0
  223. package/dist/utils.js +172 -0
  224. package/dist/utils.test.js +130 -0
  225. package/dist/voice-attachment.js +34 -0
  226. package/dist/voice-handler.js +646 -0
  227. package/dist/voice-message.e2e.test.js +1021 -0
  228. package/dist/voice.js +456 -0
  229. package/dist/voice.test.js +235 -0
  230. package/dist/wait-session.js +171 -0
  231. package/dist/websockify.js +69 -0
  232. package/dist/worker-types.js +4 -0
  233. package/dist/worktree-lifecycle.e2e.test.js +311 -0
  234. package/dist/worktree-utils.js +3 -0
  235. package/dist/worktrees.js +991 -0
  236. package/dist/worktrees.test.js +415 -0
  237. package/dist/xml.js +92 -0
  238. package/dist/xml.test.js +32 -0
  239. package/package.json +90 -38
  240. package/schema.prisma +303 -0
  241. package/skills/batch/SKILL.md +87 -0
  242. package/skills/critique/SKILL.md +112 -0
  243. package/skills/egaki/SKILL.md +100 -0
  244. package/skills/errore/SKILL.md +647 -0
  245. package/skills/event-sourcing-state/SKILL.md +252 -0
  246. package/skills/goke/SKILL.md +38 -0
  247. package/skills/jitter/EDITOR.md +219 -0
  248. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  249. package/skills/jitter/SKILL.md +158 -0
  250. package/skills/jitter/jitter-clipboard.json +1042 -0
  251. package/skills/jitter/package.json +14 -0
  252. package/skills/jitter/tsconfig.json +15 -0
  253. package/skills/jitter/utils/actions.ts +212 -0
  254. package/skills/jitter/utils/export.ts +114 -0
  255. package/skills/jitter/utils/index.ts +141 -0
  256. package/skills/jitter/utils/snapshot.ts +154 -0
  257. package/skills/jitter/utils/traverse.ts +246 -0
  258. package/skills/jitter/utils/types.ts +279 -0
  259. package/skills/jitter/utils/wait.ts +133 -0
  260. package/skills/lintcn/SKILL.md +873 -0
  261. package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
  262. package/skills/new-skill/SKILL.md +237 -0
  263. package/skills/npm-package/SKILL.md +617 -0
  264. package/skills/opensrc/SKILL.md +78 -0
  265. package/skills/otto-publish/SKILL.md +61 -0
  266. package/skills/playwriter/SKILL.md +35 -0
  267. package/skills/profano/SKILL.md +16 -0
  268. package/skills/proxyman/SKILL.md +215 -0
  269. package/skills/security-review/SKILL.md +208 -0
  270. package/skills/sigillo/SKILL.md +101 -0
  271. package/skills/simplify/SKILL.md +58 -0
  272. package/skills/spiceflow/SKILL.md +28 -0
  273. package/skills/termcast/SKILL.md +945 -0
  274. package/skills/tuistory/SKILL.md +98 -0
  275. package/skills/usecomputer/SKILL.md +264 -0
  276. package/skills/x-articles/SKILL.md +554 -0
  277. package/skills/zele/SKILL.md +49 -0
  278. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  279. package/src/agent-model.e2e.test.ts +979 -0
  280. package/src/ai-tool-to-genai.test.ts +296 -0
  281. package/src/ai-tool-to-genai.ts +283 -0
  282. package/src/ai-tool.ts +39 -0
  283. package/src/anthropic-account-identity.test.ts +52 -0
  284. package/src/anthropic-account-identity.ts +77 -0
  285. package/src/anthropic-auth-plugin.ts +1139 -0
  286. package/src/anthropic-auth-state.test.ts +187 -0
  287. package/src/anthropic-auth-state.ts +386 -0
  288. package/src/bin.ts +182 -0
  289. package/src/btw-prefix-detection.test.ts +73 -0
  290. package/src/btw-prefix-detection.ts +23 -0
  291. package/src/channel-management.ts +376 -0
  292. package/src/cli-parsing.test.ts +197 -0
  293. package/src/cli-send-thread.e2e.test.ts +463 -0
  294. package/src/cli-telegram-options.test.ts +114 -0
  295. package/src/cli.ts +5718 -580
  296. package/src/commands/abort.ts +89 -0
  297. package/src/commands/action-buttons.ts +364 -0
  298. package/src/commands/add-dir.test.ts +154 -0
  299. package/src/commands/add-dir.ts +175 -0
  300. package/src/commands/add-project.ts +149 -0
  301. package/src/commands/agent.ts +496 -0
  302. package/src/commands/ask-question.test.ts +111 -0
  303. package/src/commands/ask-question.ts +455 -0
  304. package/src/commands/btw.ts +184 -0
  305. package/src/commands/cli-commands-group-a.test.ts +837 -0
  306. package/src/commands/cli-commands-group-b.test.ts +800 -0
  307. package/src/commands/compact.ts +157 -0
  308. package/src/commands/context-usage.ts +199 -0
  309. package/src/commands/create-new-project.ts +190 -0
  310. package/src/commands/diff.ts +91 -0
  311. package/src/commands/discord-commands-group-a.test.ts +789 -0
  312. package/src/commands/discord-commands-group-b.test.ts +648 -0
  313. package/src/commands/discord-commands-group-c.test.ts +882 -0
  314. package/src/commands/file-upload.ts +389 -0
  315. package/src/commands/fork-subagent.ts +263 -0
  316. package/src/commands/fork.ts +386 -0
  317. package/src/commands/gemini-apikey.ts +104 -0
  318. package/src/commands/login.ts +1181 -0
  319. package/src/commands/mcp.ts +307 -0
  320. package/src/commands/memory-snapshot.ts +30 -0
  321. package/src/commands/mention-mode.ts +68 -0
  322. package/src/commands/merge-worktree.ts +226 -0
  323. package/src/commands/model-variant.ts +488 -0
  324. package/src/commands/model.ts +1082 -0
  325. package/src/commands/new-worktree.ts +645 -0
  326. package/src/commands/paginated-select.ts +81 -0
  327. package/src/commands/permissions.ts +397 -0
  328. package/src/commands/queue.ts +293 -0
  329. package/src/commands/remove-project.ts +155 -0
  330. package/src/commands/restart-opencode-server.ts +162 -0
  331. package/src/commands/resume.ts +230 -0
  332. package/src/commands/run-command.ts +123 -0
  333. package/src/commands/screenshare.test.ts +30 -0
  334. package/src/commands/screenshare.ts +366 -0
  335. package/src/commands/session-id.ts +109 -0
  336. package/src/commands/session.ts +227 -0
  337. package/src/commands/share.ts +106 -0
  338. package/src/commands/tasks.ts +293 -0
  339. package/src/commands/thread-deletion-sync.ts +80 -0
  340. package/src/commands/types.ts +25 -0
  341. package/src/commands/undo-redo.ts +386 -0
  342. package/src/commands/unset-model.ts +174 -0
  343. package/src/commands/upgrade.ts +59 -0
  344. package/src/commands/user-command.ts +198 -0
  345. package/src/commands/verbosity.ts +173 -0
  346. package/src/commands/vscode.ts +342 -0
  347. package/src/commands/worktree-settings.ts +70 -0
  348. package/src/commands/worktrees.ts +645 -0
  349. package/src/condense-memory.ts +36 -0
  350. package/src/config.ts +103 -339
  351. package/src/context-awareness-plugin.test.ts +144 -0
  352. package/src/context-awareness-plugin.ts +469 -0
  353. package/src/critique-utils.ts +139 -0
  354. package/src/database.ts +1949 -0
  355. package/src/db.test.ts +162 -0
  356. package/src/db.ts +295 -0
  357. package/src/debounce-timeout.ts +43 -0
  358. package/src/debounced-process-flush.ts +104 -0
  359. package/src/discord-bot.ts +1507 -0
  360. package/src/discord-command-registration.ts +752 -0
  361. package/src/discord-urls.ts +89 -0
  362. package/src/discord-utils.test.ts +153 -0
  363. package/src/discord-utils.ts +846 -0
  364. package/src/errors.ts +232 -0
  365. package/src/escape-backticks.test.ts +469 -0
  366. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  367. package/src/eventsource-parser.test.ts +351 -0
  368. package/src/exec-async.ts +35 -0
  369. package/src/external-opencode-sync.ts +685 -0
  370. package/src/format-tables.test.ts +515 -0
  371. package/src/format-tables.ts +718 -0
  372. package/src/forum-sync/config.ts +92 -0
  373. package/src/forum-sync/discord-operations.ts +241 -0
  374. package/src/forum-sync/index.ts +9 -0
  375. package/src/forum-sync/markdown.ts +172 -0
  376. package/src/forum-sync/sync-to-discord.ts +595 -0
  377. package/src/forum-sync/sync-to-files.ts +294 -0
  378. package/src/forum-sync/types.ts +175 -0
  379. package/src/forum-sync/watchers.ts +454 -0
  380. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  381. package/src/gateway-proxy.e2e.test.ts +644 -0
  382. package/src/genai-worker-wrapper.ts +164 -0
  383. package/src/genai-worker.ts +386 -0
  384. package/src/genai.ts +321 -0
  385. package/src/generated/browser.ts +114 -0
  386. package/src/generated/client.ts +138 -0
  387. package/src/generated/commonInputTypes.ts +770 -0
  388. package/src/generated/enums.ts +98 -0
  389. package/src/generated/internal/class.ts +384 -0
  390. package/src/generated/internal/prismaNamespace.ts +2394 -0
  391. package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
  392. package/src/generated/models/bot_api_keys.ts +1288 -0
  393. package/src/generated/models/bot_tokens.ts +1700 -0
  394. package/src/generated/models/channel_agents.ts +1256 -0
  395. package/src/generated/models/channel_directories.ts +1859 -0
  396. package/src/generated/models/channel_mention_mode.ts +1300 -0
  397. package/src/generated/models/channel_models.ts +1288 -0
  398. package/src/generated/models/channel_verbosity.ts +1228 -0
  399. package/src/generated/models/channel_worktrees.ts +1300 -0
  400. package/src/generated/models/forum_sync_configs.ts +1452 -0
  401. package/src/generated/models/global_models.ts +1288 -0
  402. package/src/generated/models/ipc_requests.ts +1485 -0
  403. package/src/generated/models/part_messages.ts +1302 -0
  404. package/src/generated/models/scheduled_tasks.ts +2320 -0
  405. package/src/generated/models/session_agents.ts +1086 -0
  406. package/src/generated/models/session_events.ts +1439 -0
  407. package/src/generated/models/session_models.ts +1114 -0
  408. package/src/generated/models/session_start_sources.ts +1408 -0
  409. package/src/generated/models/thread_sessions.ts +1781 -0
  410. package/src/generated/models/thread_worktrees.ts +1356 -0
  411. package/src/generated/models.ts +30 -0
  412. package/src/heap-monitor.ts +152 -0
  413. package/src/hrana-server.test.ts +434 -0
  414. package/src/hrana-server.ts +299 -0
  415. package/src/html-actions.test.ts +87 -0
  416. package/src/html-actions.ts +174 -0
  417. package/src/html-components.test.ts +38 -0
  418. package/src/html-components.ts +181 -0
  419. package/src/image-optimizer-plugin.ts +194 -0
  420. package/src/image-utils.ts +149 -0
  421. package/src/interaction-handler.ts +610 -0
  422. package/src/ipc-polling.ts +427 -0
  423. package/src/ipc-tools-plugin.ts +236 -0
  424. package/src/ipc-utils.ts +29 -0
  425. package/src/limit-heading-depth.test.ts +116 -0
  426. package/src/limit-heading-depth.ts +26 -0
  427. package/src/logger.ts +215 -0
  428. package/src/markdown.test.ts +315 -0
  429. package/src/markdown.ts +410 -0
  430. package/src/memory-overview-plugin.ts +163 -0
  431. package/src/message-finish-field.e2e.test.ts +195 -0
  432. package/src/message-formatting.test.ts +126 -0
  433. package/src/message-formatting.ts +535 -0
  434. package/src/message-preprocessing.ts +488 -0
  435. package/src/onboarding-tutorial.ts +167 -0
  436. package/src/onboarding-welcome.ts +49 -0
  437. package/src/openai-realtime.ts +358 -0
  438. package/src/opencode-command-detection.test.ts +307 -0
  439. package/src/opencode-command-detection.ts +76 -0
  440. package/src/opencode-command.test.ts +70 -0
  441. package/src/opencode-command.ts +191 -0
  442. package/src/opencode-interrupt-plugin.test.ts +682 -0
  443. package/src/opencode-interrupt-plugin.ts +507 -0
  444. package/src/opencode.ts +1462 -0
  445. package/src/otto/branding.ts +23 -0
  446. package/src/otto/index.ts +22 -0
  447. package/src/otto-digital-twin.e2e.test.ts +199 -0
  448. package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
  449. package/src/otto-opencode-plugin.test.ts +108 -0
  450. package/src/otto-opencode-plugin.ts +22 -0
  451. package/src/parse-permission-rules.test.ts +127 -0
  452. package/src/patch-text-parser.ts +107 -0
  453. package/src/plugin-logger.ts +84 -0
  454. package/src/privacy-sanitizer.ts +142 -0
  455. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  456. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  457. package/src/queue-advanced-e2e-setup.ts +877 -0
  458. package/src/queue-advanced-footer.e2e.test.ts +591 -0
  459. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  460. package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
  461. package/src/queue-advanced-question.e2e.test.ts +316 -0
  462. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  463. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  464. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  465. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  466. package/src/queue-question-select-drain.e2e.test.ts +327 -0
  467. package/src/runtime-idle-sweeper.ts +76 -0
  468. package/src/runtime-lifecycle.e2e.test.ts +651 -0
  469. package/src/schema.sql +174 -0
  470. package/src/sentry.ts +26 -0
  471. package/src/session-handler/agent-utils.ts +99 -0
  472. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  473. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  474. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  475. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  476. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  477. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  478. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  479. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  480. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  481. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  482. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  483. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  484. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  485. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  486. package/src/session-handler/event-stream-state.test.ts +717 -0
  487. package/src/session-handler/event-stream-state.ts +706 -0
  488. package/src/session-handler/model-utils.ts +217 -0
  489. package/src/session-handler/opencode-session-event-log.ts +130 -0
  490. package/src/session-handler/thread-runtime-state.ts +247 -0
  491. package/src/session-handler/thread-session-runtime.ts +4440 -0
  492. package/src/session-handler.ts +15 -0
  493. package/src/session-search.test.ts +50 -0
  494. package/src/session-search.ts +148 -0
  495. package/src/session-title-rename.test.ts +130 -0
  496. package/src/skill-filter.test.ts +83 -0
  497. package/src/skill-filter.ts +42 -0
  498. package/src/startup-service.ts +200 -0
  499. package/src/startup-time.e2e.test.ts +373 -0
  500. package/src/store.ts +139 -0
  501. package/src/subagent-rate-limit-plugin.ts +218 -0
  502. package/src/system-message.test.ts +710 -0
  503. package/src/system-message.ts +814 -0
  504. package/src/task-runner.ts +725 -0
  505. package/src/task-schedule.test.ts +84 -0
  506. package/src/task-schedule.ts +317 -0
  507. package/src/test-utils.ts +451 -0
  508. package/src/thinking-utils.ts +61 -0
  509. package/src/thread-message-queue.e2e.test.ts +1350 -0
  510. package/src/tools.ts +430 -0
  511. package/src/undici.d.ts +12 -0
  512. package/src/undo-redo.e2e.test.ts +209 -0
  513. package/src/unnest-code-blocks.test.ts +713 -0
  514. package/src/unnest-code-blocks.ts +185 -0
  515. package/src/upgrade.ts +185 -0
  516. package/src/utils.test.ts +155 -0
  517. package/src/utils.ts +265 -0
  518. package/src/voice-attachment.ts +51 -0
  519. package/src/voice-handler.ts +908 -0
  520. package/src/voice-message.e2e.test.ts +1255 -0
  521. package/src/voice.test.ts +281 -0
  522. package/src/voice.ts +638 -0
  523. package/src/wait-session.ts +273 -0
  524. package/src/websockify.ts +101 -0
  525. package/src/worker-types.ts +64 -0
  526. package/src/worktree-lifecycle.e2e.test.ts +396 -0
  527. package/src/worktree-utils.ts +4 -0
  528. package/src/worktrees.test.ts +489 -0
  529. package/src/worktrees.ts +1370 -0
  530. package/src/xml.test.ts +38 -0
  531. package/src/xml.ts +121 -0
  532. package/README.md +0 -142
  533. package/dist/cli.d.ts +0 -3
  534. package/dist/cli.d.ts.map +0 -1
  535. package/dist/cli.js.map +0 -1
  536. package/dist/config.d.ts +0 -39
  537. package/dist/config.d.ts.map +0 -1
  538. package/dist/config.js.map +0 -1
  539. package/dist/config.test.d.ts +0 -2
  540. package/dist/config.test.d.ts.map +0 -1
  541. package/dist/config.test.js +0 -202
  542. package/dist/config.test.js.map +0 -1
  543. package/dist/detect.d.ts +0 -9
  544. package/dist/detect.d.ts.map +0 -1
  545. package/dist/detect.js +0 -40
  546. package/dist/detect.js.map +0 -1
  547. package/dist/detect.test.d.ts +0 -2
  548. package/dist/detect.test.d.ts.map +0 -1
  549. package/dist/detect.test.js +0 -26
  550. package/dist/detect.test.js.map +0 -1
  551. package/dist/docker.d.ts +0 -7
  552. package/dist/docker.d.ts.map +0 -1
  553. package/dist/docker.js +0 -17
  554. package/dist/docker.js.map +0 -1
  555. package/dist/docker.test.d.ts +0 -2
  556. package/dist/docker.test.d.ts.map +0 -1
  557. package/dist/docker.test.js +0 -12
  558. package/dist/docker.test.js.map +0 -1
  559. package/dist/health.d.ts +0 -31
  560. package/dist/health.d.ts.map +0 -1
  561. package/dist/health.js +0 -117
  562. package/dist/health.js.map +0 -1
  563. package/dist/health.test.d.ts +0 -2
  564. package/dist/health.test.d.ts.map +0 -1
  565. package/dist/health.test.js +0 -52
  566. package/dist/health.test.js.map +0 -1
  567. package/dist/index.d.ts +0 -20
  568. package/dist/index.d.ts.map +0 -1
  569. package/dist/index.js +0 -15
  570. package/dist/index.js.map +0 -1
  571. package/dist/index.test.d.ts +0 -2
  572. package/dist/index.test.d.ts.map +0 -1
  573. package/dist/index.test.js +0 -8
  574. package/dist/index.test.js.map +0 -1
  575. package/dist/installer.d.ts +0 -10
  576. package/dist/installer.d.ts.map +0 -1
  577. package/dist/installer.js +0 -50
  578. package/dist/installer.js.map +0 -1
  579. package/dist/installer.test.d.ts +0 -2
  580. package/dist/installer.test.d.ts.map +0 -1
  581. package/dist/installer.test.js +0 -43
  582. package/dist/installer.test.js.map +0 -1
  583. package/dist/lifecycle.d.ts +0 -10
  584. package/dist/lifecycle.d.ts.map +0 -1
  585. package/dist/lifecycle.js +0 -45
  586. package/dist/lifecycle.js.map +0 -1
  587. package/dist/lifecycle.test.d.ts +0 -2
  588. package/dist/lifecycle.test.d.ts.map +0 -1
  589. package/dist/lifecycle.test.js +0 -20
  590. package/dist/lifecycle.test.js.map +0 -1
  591. package/dist/manifest.d.ts +0 -18
  592. package/dist/manifest.d.ts.map +0 -1
  593. package/dist/manifest.js +0 -30
  594. package/dist/manifest.js.map +0 -1
  595. package/dist/skills-baseline.d.ts +0 -7
  596. package/dist/skills-baseline.d.ts.map +0 -1
  597. package/dist/skills-baseline.js +0 -9
  598. package/dist/skills-baseline.js.map +0 -1
  599. package/dist/skills.d.ts +0 -110
  600. package/dist/skills.d.ts.map +0 -1
  601. package/dist/skills.js +0 -429
  602. package/dist/skills.js.map +0 -1
  603. package/dist/skills.test.d.ts +0 -2
  604. package/dist/skills.test.d.ts.map +0 -1
  605. package/dist/skills.test.js +0 -416
  606. package/dist/skills.test.js.map +0 -1
  607. package/dist/sync.d.ts +0 -10
  608. package/dist/sync.d.ts.map +0 -1
  609. package/dist/sync.js +0 -39
  610. package/dist/sync.js.map +0 -1
  611. package/dist/tenant.d.ts +0 -13
  612. package/dist/tenant.d.ts.map +0 -1
  613. package/dist/tenant.js +0 -105
  614. package/dist/tenant.js.map +0 -1
  615. package/dist/tenant.test.d.ts +0 -2
  616. package/dist/tenant.test.d.ts.map +0 -1
  617. package/dist/tenant.test.js +0 -37
  618. package/dist/tenant.test.js.map +0 -1
  619. package/src/config.test.ts +0 -237
  620. package/src/detect.test.ts +0 -29
  621. package/src/detect.ts +0 -52
  622. package/src/docker.test.ts +0 -12
  623. package/src/docker.ts +0 -23
  624. package/src/health.test.ts +0 -61
  625. package/src/health.ts +0 -158
  626. package/src/index.test.ts +0 -8
  627. package/src/index.ts +0 -62
  628. package/src/installer.test.ts +0 -52
  629. package/src/installer.ts +0 -62
  630. package/src/lifecycle.test.ts +0 -23
  631. package/src/lifecycle.ts +0 -49
  632. package/src/manifest.ts +0 -42
  633. package/src/skills-baseline.ts +0 -14
  634. package/src/skills.test.ts +0 -503
  635. package/src/skills.ts +0 -512
  636. package/src/sync.ts +0 -53
  637. package/src/tenant.test.ts +0 -49
  638. package/src/tenant.ts +0 -120
@@ -0,0 +1,187 @@
1
+ // Tests Anthropic OAuth account persistence, deduplication, and rotation.
2
+
3
+ import { mkdtemp, readFile, rm, mkdir, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import path from 'node:path'
6
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest'
7
+ import {
8
+ accountLabel,
9
+ authFilePath,
10
+ loadAccountStore,
11
+ rememberAnthropicOAuth,
12
+ removeAccount,
13
+ rotateAnthropicAccount,
14
+ saveAccountStore,
15
+ shouldRotateAuth,
16
+ } from './anthropic-auth-state.js'
17
+
18
+ const firstAccount = {
19
+ type: 'oauth' as const,
20
+ refresh: 'refresh-first',
21
+ access: 'access-first',
22
+ expires: 1,
23
+ }
24
+
25
+ const secondAccount = {
26
+ type: 'oauth' as const,
27
+ refresh: 'refresh-second',
28
+ access: 'access-second',
29
+ expires: 2,
30
+ }
31
+
32
+ let originalXdgDataHome: string | undefined
33
+ let tempDir = ''
34
+
35
+ beforeEach(async () => {
36
+ originalXdgDataHome = process.env.XDG_DATA_HOME
37
+ tempDir = await mkdtemp(path.join(tmpdir(), 'anthropic-auth-plugin-'))
38
+ process.env.XDG_DATA_HOME = tempDir
39
+ })
40
+
41
+ afterEach(async () => {
42
+ if (originalXdgDataHome === undefined) {
43
+ delete process.env.XDG_DATA_HOME
44
+ } else {
45
+ process.env.XDG_DATA_HOME = originalXdgDataHome
46
+ }
47
+ await rm(tempDir, { force: true, recursive: true })
48
+ })
49
+
50
+ describe('rememberAnthropicOAuth', () => {
51
+ test('stores accounts and updates existing entries by refresh token', async () => {
52
+ await rememberAnthropicOAuth(firstAccount)
53
+ await rememberAnthropicOAuth({ ...firstAccount, access: 'access-first-new', expires: 3 })
54
+
55
+ const store = await loadAccountStore()
56
+ expect(store.activeIndex).toBe(0)
57
+ expect(store.accounts).toHaveLength(1)
58
+ expect(store.accounts[0]).toMatchObject({
59
+ refresh: 'refresh-first',
60
+ access: 'access-first-new',
61
+ expires: 3,
62
+ })
63
+ })
64
+
65
+ test('deduplicates new tokens by email or account ID', async () => {
66
+ await rememberAnthropicOAuth(firstAccount, {
67
+ email: 'user@example.com',
68
+ accountId: 'usr_123',
69
+ })
70
+ await rememberAnthropicOAuth(secondAccount, {
71
+ email: 'User@example.com',
72
+ accountId: 'usr_123',
73
+ })
74
+
75
+ const store = await loadAccountStore()
76
+ expect(store.accounts).toHaveLength(1)
77
+ expect(store.accounts[0]).toMatchObject({
78
+ refresh: 'refresh-second',
79
+ access: 'access-second',
80
+ email: 'user@example.com',
81
+ accountId: 'usr_123',
82
+ })
83
+ expect(accountLabel(store.accounts[0]!)).toBe('user@example.com')
84
+ })
85
+ })
86
+
87
+ describe('rotateAnthropicAccount', () => {
88
+ test('rotates to the next stored account and syncs auth state', async () => {
89
+ await saveAccountStore({
90
+ version: 1,
91
+ activeIndex: 0,
92
+ accounts: [
93
+ { ...firstAccount, addedAt: 1, lastUsed: 1 },
94
+ { ...secondAccount, addedAt: 2, lastUsed: 2 },
95
+ ],
96
+ })
97
+
98
+ const authSetCalls: unknown[] = []
99
+ const client = {
100
+ auth: {
101
+ set: async (input: unknown) => {
102
+ authSetCalls.push(input)
103
+ },
104
+ },
105
+ }
106
+
107
+ const rotated = await rotateAnthropicAccount(firstAccount, client as never)
108
+ const store = await loadAccountStore()
109
+ const authJson = JSON.parse(await readFile(authFilePath(), 'utf8')) as {
110
+ anthropic?: { refresh?: string }
111
+ }
112
+
113
+ expect(rotated).toMatchObject({
114
+ auth: { refresh: 'refresh-second' },
115
+ fromLabel: '#1 (refresh-...irst)',
116
+ toLabel: '#2 (refresh-...cond)',
117
+ fromIndex: 0,
118
+ toIndex: 1,
119
+ })
120
+ expect(store.activeIndex).toBe(1)
121
+ expect(authJson.anthropic?.refresh).toBe('refresh-second')
122
+ expect(authSetCalls).toEqual([
123
+ {
124
+ path: { id: 'anthropic' },
125
+ body: {
126
+ type: 'oauth',
127
+ refresh: 'refresh-second',
128
+ access: 'access-second',
129
+ expires: 2,
130
+ },
131
+ },
132
+ ])
133
+ })
134
+ })
135
+
136
+ describe('removeAccount', () => {
137
+ test('removing the active account promotes the next stored account', async () => {
138
+ await saveAccountStore({
139
+ version: 1,
140
+ activeIndex: 1,
141
+ accounts: [
142
+ { ...firstAccount, addedAt: 1, lastUsed: 1 },
143
+ { ...secondAccount, addedAt: 2, lastUsed: 2 },
144
+ ],
145
+ })
146
+
147
+ await removeAccount(1)
148
+
149
+ const store = await loadAccountStore()
150
+ const authJson = JSON.parse(await readFile(authFilePath(), 'utf8')) as {
151
+ anthropic?: { refresh?: string }
152
+ }
153
+
154
+ expect(store.activeIndex).toBe(0)
155
+ expect(store.accounts).toHaveLength(1)
156
+ expect(store.accounts[0]?.refresh).toBe('refresh-first')
157
+ expect(authJson.anthropic?.refresh).toBe('refresh-first')
158
+ })
159
+
160
+ test('removing the last account clears active Anthropic auth', async () => {
161
+ await saveAccountStore({
162
+ version: 1,
163
+ activeIndex: 0,
164
+ accounts: [{ ...firstAccount, addedAt: 1, lastUsed: 1 }],
165
+ })
166
+ await mkdir(path.dirname(authFilePath()), { recursive: true })
167
+ await writeFile(authFilePath(), JSON.stringify({ anthropic: firstAccount }, null, 2))
168
+
169
+ await removeAccount(0)
170
+
171
+ const store = await loadAccountStore()
172
+ const authJson = JSON.parse(await readFile(authFilePath(), 'utf8')) as {
173
+ anthropic?: unknown
174
+ }
175
+
176
+ expect(store.accounts).toHaveLength(0)
177
+ expect(authJson.anthropic).toBeUndefined()
178
+ })
179
+ })
180
+
181
+ describe('shouldRotateAuth', () => {
182
+ test('only rotates on rate limit or auth failures', () => {
183
+ expect(shouldRotateAuth(429, '')).toBe(true)
184
+ expect(shouldRotateAuth(401, 'permission_error')).toBe(true)
185
+ expect(shouldRotateAuth(400, 'bad request')).toBe(false)
186
+ })
187
+ })
@@ -0,0 +1,386 @@
1
+ import type { Plugin } from '@opencode-ai/plugin'
2
+ import * as fs from 'node:fs/promises'
3
+ import { homedir } from 'node:os'
4
+ import path from 'node:path'
5
+ import {
6
+ normalizeAnthropicAccountIdentity,
7
+ type AnthropicAccountIdentity,
8
+ } from './anthropic-account-identity.js'
9
+
10
+ const AUTH_LOCK_STALE_MS = 30_000
11
+ const AUTH_LOCK_RETRY_MS = 100
12
+
13
+ export type OAuthStored = {
14
+ type: 'oauth'
15
+ refresh: string
16
+ access: string
17
+ expires: number
18
+ }
19
+
20
+ export type CurrentAnthropicAccount = {
21
+ auth: OAuthStored
22
+ account?: OAuthStored & AnthropicAccountIdentity
23
+ index?: number
24
+ }
25
+
26
+ type AccountRecord = OAuthStored & {
27
+ email?: string
28
+ accountId?: string
29
+ addedAt: number
30
+ lastUsed: number
31
+ }
32
+
33
+ type AccountStore = {
34
+ version: number
35
+ activeIndex: number
36
+ accounts: AccountRecord[]
37
+ }
38
+
39
+ async function readJson<T>(filePath: string, fallback: T): Promise<T> {
40
+ try {
41
+ return JSON.parse(await fs.readFile(filePath, 'utf8')) as T
42
+ } catch {
43
+ return fallback
44
+ }
45
+ }
46
+
47
+ async function writeJson(filePath: string, value: unknown) {
48
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
49
+ await fs.writeFile(filePath, JSON.stringify(value, null, 2), 'utf8')
50
+ await fs.chmod(filePath, 0o600)
51
+ }
52
+
53
+ function getErrorCode(error: unknown) {
54
+ if (!(error instanceof Error)) return undefined
55
+ return (error as NodeJS.ErrnoException).code
56
+ }
57
+
58
+ async function sleep(ms: number) {
59
+ await new Promise<void>((resolve) => {
60
+ setTimeout(resolve, ms)
61
+ })
62
+ }
63
+
64
+ export function authFilePath() {
65
+ if (process.env.XDG_DATA_HOME) {
66
+ return path.join(process.env.XDG_DATA_HOME, 'opencode', 'auth.json')
67
+ }
68
+ return path.join(homedir(), '.local', 'share', 'opencode', 'auth.json')
69
+ }
70
+
71
+ export function accountsFilePath() {
72
+ if (process.env.XDG_DATA_HOME) {
73
+ return path.join(process.env.XDG_DATA_HOME, 'opencode', 'anthropic-oauth-accounts.json')
74
+ }
75
+ return path.join(homedir(), '.local', 'share', 'opencode', 'anthropic-oauth-accounts.json')
76
+ }
77
+
78
+ export async function withAuthStateLock<T>(fn: () => Promise<T>) {
79
+ const file = authFilePath()
80
+ const lockDir = `${file}.lock`
81
+ const deadline = Date.now() + AUTH_LOCK_STALE_MS
82
+
83
+ await fs.mkdir(path.dirname(file), { recursive: true })
84
+
85
+ while (true) {
86
+ try {
87
+ await fs.mkdir(lockDir)
88
+ break
89
+ } catch (error) {
90
+ const code = getErrorCode(error)
91
+ if (code !== 'EEXIST') {
92
+ throw error
93
+ }
94
+
95
+ const stats = await fs.stat(lockDir).catch(() => {
96
+ return null
97
+ })
98
+ if (stats && Date.now() - stats.mtimeMs > AUTH_LOCK_STALE_MS) {
99
+ await fs.rm(lockDir, { force: true, recursive: true }).catch(() => {})
100
+ continue
101
+ }
102
+
103
+ if (Date.now() >= deadline) {
104
+ throw new Error(`Timed out waiting for auth lock: ${lockDir}`)
105
+ }
106
+
107
+ await sleep(AUTH_LOCK_RETRY_MS)
108
+ }
109
+ }
110
+
111
+ try {
112
+ return await fn()
113
+ } finally {
114
+ await fs.rm(lockDir, { force: true, recursive: true }).catch(() => {})
115
+ }
116
+ }
117
+
118
+ export function normalizeAccountStore(
119
+ input: Partial<AccountStore> | null | undefined,
120
+ ): AccountStore {
121
+ const accounts = Array.isArray(input?.accounts)
122
+ ? input.accounts.filter(
123
+ (account): account is AccountRecord =>
124
+ !!account &&
125
+ account.type === 'oauth' &&
126
+ typeof account.refresh === 'string' &&
127
+ typeof account.access === 'string' &&
128
+ typeof account.expires === 'number' &&
129
+ (typeof account.email === 'undefined' || typeof account.email === 'string') &&
130
+ (typeof account.accountId === 'undefined' || typeof account.accountId === 'string') &&
131
+ typeof account.addedAt === 'number' &&
132
+ typeof account.lastUsed === 'number',
133
+ )
134
+ : []
135
+ const rawIndex = typeof input?.activeIndex === 'number' ? Math.floor(input.activeIndex) : 0
136
+ const activeIndex =
137
+ accounts.length === 0 ? 0 : ((rawIndex % accounts.length) + accounts.length) % accounts.length
138
+ return { version: 1, activeIndex, accounts }
139
+ }
140
+
141
+ export async function loadAccountStore() {
142
+ const raw = await readJson<Partial<AccountStore> | null>(accountsFilePath(), null)
143
+ return normalizeAccountStore(raw)
144
+ }
145
+
146
+ export async function saveAccountStore(store: AccountStore) {
147
+ await writeJson(accountsFilePath(), normalizeAccountStore(store))
148
+ }
149
+
150
+ /** Short label for an account: first 8 + last 4 chars of refresh token. */
151
+ export function accountLabel(account: OAuthStored, index?: number): string {
152
+ const accountWithIdentity = account as OAuthStored & AnthropicAccountIdentity
153
+ const identity = accountWithIdentity.email || accountWithIdentity.accountId
154
+ const r = account.refresh
155
+ const short = r.length > 12 ? `${r.slice(0, 8)}...${r.slice(-4)}` : r
156
+ if (identity) {
157
+ return index !== undefined ? `#${index + 1} (${identity})` : identity
158
+ }
159
+ return index !== undefined ? `#${index + 1} (${short})` : short
160
+ }
161
+
162
+ export type RotationResult = {
163
+ auth: OAuthStored
164
+ fromLabel: string
165
+ toLabel: string
166
+ fromIndex: number
167
+ toIndex: number
168
+ }
169
+
170
+ function findCurrentAccountIndex(store: AccountStore, auth: OAuthStored) {
171
+ if (!store.accounts.length) return 0
172
+ const byRefresh = store.accounts.findIndex((account) => {
173
+ return account.refresh === auth.refresh
174
+ })
175
+ if (byRefresh >= 0) return byRefresh
176
+ const byAccess = store.accounts.findIndex((account) => {
177
+ return account.access === auth.access
178
+ })
179
+ if (byAccess >= 0) return byAccess
180
+ return store.activeIndex
181
+ }
182
+
183
+ export function upsertAccount(store: AccountStore, auth: OAuthStored, now = Date.now()) {
184
+ const authWithIdentity = auth as OAuthStored & AnthropicAccountIdentity
185
+ const identity = normalizeAnthropicAccountIdentity({
186
+ email: authWithIdentity.email,
187
+ accountId: authWithIdentity.accountId,
188
+ })
189
+ const index = store.accounts.findIndex((account) => {
190
+ if (account.refresh === auth.refresh || account.access === auth.access) {
191
+ return true
192
+ }
193
+ if (identity?.accountId && account.accountId === identity.accountId) {
194
+ return true
195
+ }
196
+ if (identity?.email && account.email === identity.email) {
197
+ return true
198
+ }
199
+ return false
200
+ })
201
+ const nextAccount: AccountRecord = {
202
+ type: 'oauth',
203
+ refresh: auth.refresh,
204
+ access: auth.access,
205
+ expires: auth.expires,
206
+ ...identity,
207
+ addedAt: now,
208
+ lastUsed: now,
209
+ }
210
+
211
+ if (index < 0) {
212
+ store.accounts.push(nextAccount)
213
+ store.activeIndex = store.accounts.length - 1
214
+ return store.activeIndex
215
+ }
216
+
217
+ const existing = store.accounts[index]
218
+ if (!existing) return index
219
+ store.accounts[index] = {
220
+ ...existing,
221
+ ...nextAccount,
222
+ addedAt: existing.addedAt,
223
+ email: nextAccount.email || existing.email,
224
+ accountId: nextAccount.accountId || existing.accountId,
225
+ }
226
+ store.activeIndex = index
227
+ return index
228
+ }
229
+
230
+ export async function rememberAnthropicOAuth(
231
+ auth: OAuthStored,
232
+ identity?: AnthropicAccountIdentity,
233
+ ) {
234
+ await withAuthStateLock(async () => {
235
+ const store = await loadAccountStore()
236
+ upsertAccount(store, { ...auth, ...normalizeAnthropicAccountIdentity(identity) })
237
+ await saveAccountStore(store)
238
+ })
239
+ }
240
+
241
+ async function writeAnthropicAuthFile(auth: OAuthStored | undefined) {
242
+ const file = authFilePath()
243
+ const data = await readJson<Record<string, unknown>>(file, {})
244
+ if (auth) {
245
+ data.anthropic = auth
246
+ } else {
247
+ delete data.anthropic
248
+ }
249
+ await writeJson(file, data)
250
+ }
251
+
252
+ function isOAuthStored(value: unknown): value is OAuthStored {
253
+ if (!value || typeof value !== 'object') {
254
+ return false
255
+ }
256
+
257
+ const record = value as Record<string, unknown>
258
+ return (
259
+ record.type === 'oauth' &&
260
+ typeof record.refresh === 'string' &&
261
+ typeof record.access === 'string' &&
262
+ typeof record.expires === 'number'
263
+ )
264
+ }
265
+
266
+ export async function getCurrentAnthropicAccount() {
267
+ const authJson = await readJson<Record<string, unknown>>(authFilePath(), {})
268
+ const auth = authJson.anthropic
269
+ if (!isOAuthStored(auth)) {
270
+ return null
271
+ }
272
+
273
+ const store = await loadAccountStore()
274
+ const index = findCurrentAccountIndex(store, auth)
275
+ const account = store.accounts[index]
276
+ if (!account) {
277
+ return { auth } satisfies CurrentAnthropicAccount
278
+ }
279
+
280
+ if (account.refresh !== auth.refresh && account.access !== auth.access) {
281
+ return { auth } satisfies CurrentAnthropicAccount
282
+ }
283
+
284
+ return {
285
+ auth,
286
+ account,
287
+ index,
288
+ } satisfies CurrentAnthropicAccount
289
+ }
290
+
291
+ export async function setAnthropicAuth(
292
+ auth: OAuthStored,
293
+ client: Parameters<Plugin>[0]['client'],
294
+ ) {
295
+ await writeAnthropicAuthFile(auth)
296
+ await client.auth.set({ path: { id: 'anthropic' }, body: auth })
297
+ }
298
+
299
+ export async function rotateAnthropicAccount(
300
+ auth: OAuthStored,
301
+ client: Parameters<Plugin>[0]['client'],
302
+ ): Promise<RotationResult | undefined> {
303
+ return withAuthStateLock(async () => {
304
+ const store = await loadAccountStore()
305
+ if (store.accounts.length < 2) return undefined
306
+
307
+ const currentIndex = findCurrentAccountIndex(store, auth)
308
+ const currentAccount = store.accounts[currentIndex]
309
+ const nextIndex = (currentIndex + 1) % store.accounts.length
310
+ const nextAccount = store.accounts[nextIndex]
311
+ if (!nextAccount) return undefined
312
+
313
+ const fromLabel = currentAccount
314
+ ? accountLabel(currentAccount, currentIndex)
315
+ : accountLabel(auth, currentIndex)
316
+
317
+ nextAccount.lastUsed = Date.now()
318
+ store.activeIndex = nextIndex
319
+ await saveAccountStore(store)
320
+
321
+ const nextAuth: OAuthStored = {
322
+ type: 'oauth',
323
+ refresh: nextAccount.refresh,
324
+ access: nextAccount.access,
325
+ expires: nextAccount.expires,
326
+ }
327
+ await setAnthropicAuth(nextAuth, client)
328
+ return {
329
+ auth: nextAuth,
330
+ fromLabel,
331
+ toLabel: accountLabel(nextAccount, nextIndex),
332
+ fromIndex: currentIndex,
333
+ toIndex: nextIndex,
334
+ }
335
+ })
336
+ }
337
+
338
+ export async function removeAccount(index: number) {
339
+ return withAuthStateLock(async () => {
340
+ const store = await loadAccountStore()
341
+ if (!Number.isInteger(index) || index < 0 || index >= store.accounts.length) {
342
+ throw new Error(`Account ${index + 1} does not exist`)
343
+ }
344
+
345
+ store.accounts.splice(index, 1)
346
+ if (store.accounts.length === 0) {
347
+ store.activeIndex = 0
348
+ await saveAccountStore(store)
349
+ await writeAnthropicAuthFile(undefined)
350
+ return { store, active: undefined }
351
+ }
352
+
353
+ if (store.activeIndex > index) {
354
+ store.activeIndex -= 1
355
+ } else if (store.activeIndex >= store.accounts.length) {
356
+ store.activeIndex = 0
357
+ }
358
+
359
+ const active = store.accounts[store.activeIndex]
360
+ if (!active) throw new Error('Active Anthropic account disappeared during removal')
361
+ active.lastUsed = Date.now()
362
+ await saveAccountStore(store)
363
+ const nextAuth: OAuthStored = {
364
+ type: 'oauth',
365
+ refresh: active.refresh,
366
+ access: active.access,
367
+ expires: active.expires,
368
+ }
369
+ await writeAnthropicAuthFile(nextAuth)
370
+ return { store, active: nextAuth }
371
+ })
372
+ }
373
+
374
+ export function shouldRotateAuth(status: number, bodyText: string) {
375
+ const haystack = bodyText.toLowerCase()
376
+ if (status === 429) return true
377
+ if (status === 401 || status === 403) return true
378
+ return (
379
+ haystack.includes('rate_limit') ||
380
+ haystack.includes('rate limit') ||
381
+ haystack.includes('invalid api key') ||
382
+ haystack.includes('authentication_error') ||
383
+ haystack.includes('permission_error') ||
384
+ haystack.includes('oauth')
385
+ )
386
+ }