@otto-assistant/otto 0.1.1 → 0.7.15

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 (637) 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 +621 -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 +887 -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 +366 -0
  47. package/dist/commands/model.js +794 -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 +157 -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 +1117 -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 +751 -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 +1175 -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 +485 -0
  324. package/src/commands/model.ts +1078 -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 +1505 -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 +201 -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 +1453 -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/dist/cli.d.ts +0 -3
  533. package/dist/cli.d.ts.map +0 -1
  534. package/dist/cli.js.map +0 -1
  535. package/dist/config.d.ts +0 -39
  536. package/dist/config.d.ts.map +0 -1
  537. package/dist/config.js.map +0 -1
  538. package/dist/config.test.d.ts +0 -2
  539. package/dist/config.test.d.ts.map +0 -1
  540. package/dist/config.test.js +0 -202
  541. package/dist/config.test.js.map +0 -1
  542. package/dist/detect.d.ts +0 -9
  543. package/dist/detect.d.ts.map +0 -1
  544. package/dist/detect.js +0 -40
  545. package/dist/detect.js.map +0 -1
  546. package/dist/detect.test.d.ts +0 -2
  547. package/dist/detect.test.d.ts.map +0 -1
  548. package/dist/detect.test.js +0 -26
  549. package/dist/detect.test.js.map +0 -1
  550. package/dist/docker.d.ts +0 -7
  551. package/dist/docker.d.ts.map +0 -1
  552. package/dist/docker.js +0 -17
  553. package/dist/docker.js.map +0 -1
  554. package/dist/docker.test.d.ts +0 -2
  555. package/dist/docker.test.d.ts.map +0 -1
  556. package/dist/docker.test.js +0 -12
  557. package/dist/docker.test.js.map +0 -1
  558. package/dist/health.d.ts +0 -31
  559. package/dist/health.d.ts.map +0 -1
  560. package/dist/health.js +0 -117
  561. package/dist/health.js.map +0 -1
  562. package/dist/health.test.d.ts +0 -2
  563. package/dist/health.test.d.ts.map +0 -1
  564. package/dist/health.test.js +0 -52
  565. package/dist/health.test.js.map +0 -1
  566. package/dist/index.d.ts +0 -20
  567. package/dist/index.d.ts.map +0 -1
  568. package/dist/index.js +0 -15
  569. package/dist/index.js.map +0 -1
  570. package/dist/index.test.d.ts +0 -2
  571. package/dist/index.test.d.ts.map +0 -1
  572. package/dist/index.test.js +0 -8
  573. package/dist/index.test.js.map +0 -1
  574. package/dist/installer.d.ts +0 -10
  575. package/dist/installer.d.ts.map +0 -1
  576. package/dist/installer.js +0 -50
  577. package/dist/installer.js.map +0 -1
  578. package/dist/installer.test.d.ts +0 -2
  579. package/dist/installer.test.d.ts.map +0 -1
  580. package/dist/installer.test.js +0 -43
  581. package/dist/installer.test.js.map +0 -1
  582. package/dist/lifecycle.d.ts +0 -10
  583. package/dist/lifecycle.d.ts.map +0 -1
  584. package/dist/lifecycle.js +0 -45
  585. package/dist/lifecycle.js.map +0 -1
  586. package/dist/lifecycle.test.d.ts +0 -2
  587. package/dist/lifecycle.test.d.ts.map +0 -1
  588. package/dist/lifecycle.test.js +0 -20
  589. package/dist/lifecycle.test.js.map +0 -1
  590. package/dist/manifest.d.ts +0 -18
  591. package/dist/manifest.d.ts.map +0 -1
  592. package/dist/manifest.js +0 -30
  593. package/dist/manifest.js.map +0 -1
  594. package/dist/skills-baseline.d.ts +0 -7
  595. package/dist/skills-baseline.d.ts.map +0 -1
  596. package/dist/skills-baseline.js +0 -9
  597. package/dist/skills-baseline.js.map +0 -1
  598. package/dist/skills.d.ts +0 -110
  599. package/dist/skills.d.ts.map +0 -1
  600. package/dist/skills.js +0 -429
  601. package/dist/skills.js.map +0 -1
  602. package/dist/skills.test.d.ts +0 -2
  603. package/dist/skills.test.d.ts.map +0 -1
  604. package/dist/skills.test.js +0 -416
  605. package/dist/skills.test.js.map +0 -1
  606. package/dist/sync.d.ts +0 -10
  607. package/dist/sync.d.ts.map +0 -1
  608. package/dist/sync.js +0 -39
  609. package/dist/sync.js.map +0 -1
  610. package/dist/tenant.d.ts +0 -13
  611. package/dist/tenant.d.ts.map +0 -1
  612. package/dist/tenant.js +0 -105
  613. package/dist/tenant.js.map +0 -1
  614. package/dist/tenant.test.d.ts +0 -2
  615. package/dist/tenant.test.d.ts.map +0 -1
  616. package/dist/tenant.test.js +0 -37
  617. package/dist/tenant.test.js.map +0 -1
  618. package/src/config.test.ts +0 -237
  619. package/src/detect.test.ts +0 -29
  620. package/src/detect.ts +0 -52
  621. package/src/docker.test.ts +0 -12
  622. package/src/docker.ts +0 -23
  623. package/src/health.test.ts +0 -61
  624. package/src/health.ts +0 -158
  625. package/src/index.test.ts +0 -8
  626. package/src/index.ts +0 -62
  627. package/src/installer.test.ts +0 -52
  628. package/src/installer.ts +0 -62
  629. package/src/lifecycle.test.ts +0 -23
  630. package/src/lifecycle.ts +0 -49
  631. package/src/manifest.ts +0 -42
  632. package/src/skills-baseline.ts +0 -14
  633. package/src/skills.test.ts +0 -503
  634. package/src/skills.ts +0 -512
  635. package/src/sync.ts +0 -53
  636. package/src/tenant.test.ts +0 -49
  637. package/src/tenant.ts +0 -120
@@ -0,0 +1,496 @@
1
+ // /agent command - Set the preferred agent for this channel or session.
2
+ // Also provides quick agent commands like /plan-agent, /build-agent that switch instantly.
3
+
4
+ import {
5
+ ChatInputCommandInteraction,
6
+ StringSelectMenuInteraction,
7
+ StringSelectMenuBuilder,
8
+ ActionRowBuilder,
9
+ ChannelType,
10
+ type ThreadChannel,
11
+ type TextChannel,
12
+ MessageFlags,
13
+ } from 'discord.js'
14
+ import crypto from 'node:crypto'
15
+ import {
16
+ setChannelAgent,
17
+ setSessionAgent,
18
+ clearSessionModel,
19
+ getThreadSession,
20
+ getSessionAgent,
21
+ getChannelAgent,
22
+ } from '../database.js'
23
+ import { initializeOpencodeForDirectory } from '../opencode.js'
24
+ import { resolveTextChannel, getOttoMetadata } from '../discord-utils.js'
25
+ import { createLogger, LogPrefix } from '../logger.js'
26
+ import { getCurrentModelInfo } from './model.js'
27
+
28
+ const agentLogger = createLogger(LogPrefix.AGENT)
29
+
30
+ const AGENT_CONTEXT_TTL_MS = 10 * 60 * 1000
31
+ const pendingAgentContexts = new Map<
32
+ string,
33
+ {
34
+ dir: string
35
+ channelId: string
36
+ sessionId?: string
37
+ isThread: boolean
38
+ }
39
+ >()
40
+
41
+ /**
42
+ * Context for agent commands, containing channel/session info.
43
+ */
44
+ export type AgentCommandContext = {
45
+ dir: string
46
+ channelId: string
47
+ sessionId?: string
48
+ isThread: boolean
49
+ }
50
+
51
+ export type CurrentAgentInfo =
52
+ | { type: 'session'; agent: string }
53
+ | { type: 'channel'; agent: string }
54
+ | { type: 'none' }
55
+
56
+ /**
57
+ * Get the current agent info for a channel/session, including where it comes from.
58
+ * Priority: session > channel > none
59
+ */
60
+ export async function getCurrentAgentInfo({
61
+ sessionId,
62
+ channelId,
63
+ }: {
64
+ sessionId?: string
65
+ channelId?: string
66
+ }): Promise<CurrentAgentInfo> {
67
+ if (sessionId) {
68
+ const sessionAgent = await getSessionAgent(sessionId)
69
+ if (sessionAgent) {
70
+ return { type: 'session', agent: sessionAgent }
71
+ }
72
+ }
73
+ if (channelId) {
74
+ const channelAgent = await getChannelAgent(channelId)
75
+ if (channelAgent) {
76
+ return { type: 'channel', agent: channelAgent }
77
+ }
78
+ }
79
+ return { type: 'none' }
80
+ }
81
+
82
+ /**
83
+ * Sanitize an agent name to be a valid Discord command name component.
84
+ * Lowercase, alphanumeric and hyphens only.
85
+ */
86
+ export function sanitizeAgentName(name: string): string {
87
+ return name
88
+ .toLowerCase()
89
+ .replace(/[^a-z0-9-]/g, '-')
90
+ .replace(/-+/g, '-')
91
+ .replace(/^-|-$/g, '')
92
+ }
93
+
94
+ const QUICK_AGENT_DESCRIPTION_PATTERN = /^\[agent:([^\]]+)\]/
95
+
96
+ /**
97
+ * Build quick-agent command description with an embedded original agent name.
98
+ * Metadata format: [agent:<original-name>] <visible description>
99
+ */
100
+ export function buildQuickAgentCommandDescription({
101
+ agentName,
102
+ description,
103
+ }: {
104
+ agentName: string
105
+ description?: string
106
+ }): string {
107
+ const metadataPrefix = `[agent:${agentName}]`
108
+ if (metadataPrefix.length > 100) {
109
+ return metadataPrefix.slice(0, 100)
110
+ }
111
+
112
+ const visibleDescription = description || `Switch to ${agentName} agent`
113
+ const maxVisibleLength = 100 - metadataPrefix.length - 1
114
+
115
+ if (maxVisibleLength <= 0) {
116
+ return metadataPrefix
117
+ }
118
+
119
+ const trimmedVisible = visibleDescription.slice(0, maxVisibleLength).trim()
120
+ if (!trimmedVisible) {
121
+ return metadataPrefix
122
+ }
123
+
124
+ return `${metadataPrefix} ${trimmedVisible}`
125
+ }
126
+
127
+ function parseQuickAgentNameFromDescription(
128
+ description: string | undefined,
129
+ ): string | undefined {
130
+ if (!description) {
131
+ return undefined
132
+ }
133
+ const match = QUICK_AGENT_DESCRIPTION_PATTERN.exec(description)
134
+ if (!match) {
135
+ return undefined
136
+ }
137
+ const agentName = match[1]?.trim()
138
+ if (!agentName) {
139
+ return undefined
140
+ }
141
+ return agentName
142
+ }
143
+
144
+ async function resolveQuickAgentNameFromInteraction({
145
+ command,
146
+ }: {
147
+ command: ChatInputCommandInteraction
148
+ }): Promise<string | undefined> {
149
+ const fromCommandObject = parseQuickAgentNameFromDescription(
150
+ command.command?.description,
151
+ )
152
+ if (fromCommandObject) {
153
+ return fromCommandObject
154
+ }
155
+
156
+ if (!command.guild) {
157
+ return undefined
158
+ }
159
+
160
+ const fetchedCommand = await command.guild.commands.fetch(command.commandId)
161
+ if (!fetchedCommand) {
162
+ return undefined
163
+ }
164
+
165
+ return parseQuickAgentNameFromDescription(fetchedCommand.description)
166
+ }
167
+
168
+ /**
169
+ * Resolve the context for an agent command (directory, channel, session).
170
+ * Returns null if the command cannot be executed in this context.
171
+ */
172
+ export async function resolveAgentCommandContext({
173
+ interaction,
174
+ }: {
175
+ interaction: ChatInputCommandInteraction
176
+ appId: string
177
+ }): Promise<AgentCommandContext | null> {
178
+ const channel = interaction.channel
179
+
180
+ if (!channel) {
181
+ await interaction.editReply({
182
+ content: 'This command can only be used in a channel',
183
+ })
184
+ return null
185
+ }
186
+
187
+ const isThread = [
188
+ ChannelType.PublicThread,
189
+ ChannelType.PrivateThread,
190
+ ChannelType.AnnouncementThread,
191
+ ].includes(channel.type)
192
+
193
+ let projectDirectory: string | undefined
194
+ let targetChannelId: string
195
+ let sessionId: string | undefined
196
+
197
+ if (isThread) {
198
+ const thread = channel as ThreadChannel
199
+ const textChannel = await resolveTextChannel(thread)
200
+ const metadata = await getOttoMetadata(textChannel)
201
+ projectDirectory = metadata.projectDirectory
202
+ targetChannelId = textChannel?.id || channel.id
203
+
204
+ sessionId = await getThreadSession(thread.id)
205
+ } else if (channel.type === ChannelType.GuildText) {
206
+ const textChannel = channel as TextChannel
207
+ const metadata = await getOttoMetadata(textChannel)
208
+ projectDirectory = metadata.projectDirectory
209
+ targetChannelId = channel.id
210
+ } else {
211
+ await interaction.editReply({
212
+ content: 'This command can only be used in text channels or threads',
213
+ })
214
+ return null
215
+ }
216
+
217
+ if (!projectDirectory) {
218
+ await interaction.editReply({
219
+ content: 'This channel is not configured with a project directory',
220
+ })
221
+ return null
222
+ }
223
+
224
+ return {
225
+ dir: projectDirectory,
226
+ channelId: targetChannelId,
227
+ sessionId,
228
+ isThread,
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Set the agent preference for a context (session or channel).
234
+ * When switching agents for a session, clears session model preference
235
+ * so the new agent's model takes effect (agent model > channel model).
236
+ */
237
+ export async function setAgentForContext({
238
+ context,
239
+ agentName,
240
+ }: {
241
+ context: AgentCommandContext
242
+ agentName: string
243
+ }): Promise<void> {
244
+ if (context.isThread && context.sessionId) {
245
+ await setSessionAgent(context.sessionId, agentName)
246
+ // Clear session model so the new agent's model takes effect
247
+ await clearSessionModel(context.sessionId)
248
+ agentLogger.log(
249
+ `Set agent ${agentName} for session ${context.sessionId} (cleared session model)`,
250
+ )
251
+ } else {
252
+ await setChannelAgent(context.channelId, agentName)
253
+ agentLogger.log(`Set agent ${agentName} for channel ${context.channelId}`)
254
+ }
255
+ }
256
+
257
+ export async function handleAgentCommand({
258
+ interaction,
259
+ appId,
260
+ }: {
261
+ interaction: ChatInputCommandInteraction
262
+ appId: string
263
+ }): Promise<void> {
264
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral })
265
+
266
+ const context = await resolveAgentCommandContext({ interaction, appId })
267
+ if (!context) {
268
+ return
269
+ }
270
+
271
+ try {
272
+ const getClient = await initializeOpencodeForDirectory(context.dir)
273
+ if (getClient instanceof Error) {
274
+ await interaction.editReply({ content: getClient.message })
275
+ return
276
+ }
277
+
278
+ const agentsResponse = await getClient().app.agents({
279
+ directory: context.dir,
280
+ })
281
+
282
+ if (!agentsResponse.data || agentsResponse.data.length === 0) {
283
+ await interaction.editReply({ content: 'No agents available' })
284
+ return
285
+ }
286
+
287
+ const agents = agentsResponse.data
288
+ .filter((agent) => {
289
+ const hidden = (agent as { hidden?: boolean }).hidden
290
+ return (agent.mode === 'primary' || agent.mode === 'all') && !hidden
291
+ })
292
+ .slice(0, 25)
293
+
294
+ if (agents.length === 0) {
295
+ await interaction.editReply({ content: 'No primary agents available' })
296
+ return
297
+ }
298
+
299
+ const currentAgentInfo = await getCurrentAgentInfo({
300
+ sessionId: context.sessionId,
301
+ channelId: context.channelId,
302
+ })
303
+
304
+ const currentAgentText = (() => {
305
+ switch (currentAgentInfo.type) {
306
+ case 'session':
307
+ return `**Current (session override):** \`${currentAgentInfo.agent}\``
308
+ case 'channel':
309
+ return `**Current (channel override):** \`${currentAgentInfo.agent}\``
310
+ case 'none':
311
+ return '**Current:** none'
312
+ }
313
+ })()
314
+
315
+ const contextHash = crypto.randomBytes(8).toString('hex')
316
+ pendingAgentContexts.set(contextHash, context)
317
+ setTimeout(() => {
318
+ pendingAgentContexts.delete(contextHash)
319
+ }, AGENT_CONTEXT_TTL_MS).unref()
320
+
321
+ const options = agents.map((agent) => ({
322
+ label: agent.name.slice(0, 100),
323
+ value: agent.name,
324
+ description: (agent.description || `${agent.mode} agent`).slice(0, 100),
325
+ }))
326
+
327
+ const selectMenu = new StringSelectMenuBuilder()
328
+ .setCustomId(`agent_select:${contextHash}`)
329
+ .setPlaceholder('Select an agent')
330
+ .addOptions(options)
331
+
332
+ const actionRow =
333
+ new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
334
+
335
+ await interaction.editReply({
336
+ content: `**Set Agent Preference**\n${currentAgentText}\nSelect an agent:`,
337
+ components: [actionRow],
338
+ })
339
+ } catch (error) {
340
+ agentLogger.error('Error loading agents:', error)
341
+ await interaction.editReply({
342
+ content: `Failed to load agents: ${error instanceof Error ? error.message : 'Unknown error'}`,
343
+ })
344
+ }
345
+ }
346
+
347
+ export async function handleAgentSelectMenu(
348
+ interaction: StringSelectMenuInteraction,
349
+ ): Promise<void> {
350
+ const customId = interaction.customId
351
+
352
+ if (!customId.startsWith('agent_select:')) {
353
+ return
354
+ }
355
+
356
+ await interaction.deferUpdate()
357
+
358
+ const contextHash = customId.replace('agent_select:', '')
359
+ const context = pendingAgentContexts.get(contextHash)
360
+
361
+ if (!context) {
362
+ await interaction.editReply({
363
+ content: 'Selection expired. Please run /agent again.',
364
+ components: [],
365
+ })
366
+ return
367
+ }
368
+
369
+ const selectedAgent = interaction.values[0]
370
+ if (!selectedAgent) {
371
+ await interaction.editReply({
372
+ content: 'No agent selected',
373
+ components: [],
374
+ })
375
+ return
376
+ }
377
+
378
+ try {
379
+ await setAgentForContext({ context, agentName: selectedAgent })
380
+
381
+ if (context.isThread && context.sessionId) {
382
+ await interaction.editReply({
383
+ content: `Agent preference set for this session: **${selectedAgent}**\nThe agent will change on the next message.`,
384
+ components: [],
385
+ })
386
+ } else {
387
+ await interaction.editReply({
388
+ content: `Agent preference set for this channel: **${selectedAgent}**\nAll new sessions in this channel will use this agent.`,
389
+ components: [],
390
+ })
391
+ }
392
+
393
+ pendingAgentContexts.delete(contextHash)
394
+ } catch (error) {
395
+ agentLogger.error('Error saving agent preference:', error)
396
+ await interaction.editReply({
397
+ content: `Failed to save agent preference: ${error instanceof Error ? error.message : 'Unknown error'}`,
398
+ components: [],
399
+ })
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Handle quick agent commands like /plan-agent, /build-agent.
405
+ * These instantly switch to the specified agent without showing a dropdown.
406
+ *
407
+ * The slash command name is sanitized for Discord and can be lossy
408
+ * (for example gpt5.4 -> gpt5-4-agent). To keep the original agent name,
409
+ * registration stores [agent:<name>] metadata in the description and this
410
+ * handler resolves from that metadata first.
411
+ */
412
+ export async function handleQuickAgentCommand({
413
+ command,
414
+ appId,
415
+ }: {
416
+ command: ChatInputCommandInteraction
417
+ appId: string
418
+ }): Promise<void> {
419
+ const fallbackAgentName = command.commandName.replace(/-agent$/, '')
420
+
421
+ await command.deferReply({ flags: MessageFlags.Ephemeral })
422
+
423
+ const context = await resolveAgentCommandContext({
424
+ interaction: command,
425
+ appId,
426
+ })
427
+ if (!context) {
428
+ return
429
+ }
430
+
431
+ try {
432
+ const resolvedAgentName =
433
+ (await resolveQuickAgentNameFromInteraction({ command })) ||
434
+ fallbackAgentName
435
+
436
+ // Check current agent and set new one.
437
+ // getCurrentAgentInfo is fast (DB only), use it for the "was X" text.
438
+ const previousAgent = await getCurrentAgentInfo({
439
+ sessionId: context.sessionId,
440
+ channelId: context.channelId,
441
+ })
442
+ const previousAgentName =
443
+ previousAgent.type !== 'none' ? previousAgent.agent : undefined
444
+
445
+ if (previousAgentName === resolvedAgentName) {
446
+ await command.editReply({
447
+ content: `Already using **${resolvedAgentName}** agent`,
448
+ })
449
+ return
450
+ }
451
+
452
+ // Set the agent preference in DB for this context.
453
+ await setAgentForContext({ context, agentName: resolvedAgentName })
454
+
455
+ const previousText = previousAgentName
456
+ ? ` (was **${previousAgentName}**)`
457
+ : ''
458
+
459
+ // Resolve the model that will now be used for the new agent so we can
460
+ // show it in the reply. setAgentForContext already cleared any session
461
+ // model preference, so getCurrentModelInfo falls through to the agent's
462
+ // configured model (or channel/global/default).
463
+ const modelInfo = await (async () => {
464
+ const getClient = await initializeOpencodeForDirectory(context.dir)
465
+ if (getClient instanceof Error) {
466
+ return { type: 'none' as const }
467
+ }
468
+ return getCurrentModelInfo({
469
+ sessionId: context.sessionId,
470
+ channelId: context.channelId,
471
+ appId,
472
+ agentPreference: resolvedAgentName,
473
+ getClient,
474
+ directory: context.dir,
475
+ })
476
+ })()
477
+
478
+ const modelText =
479
+ modelInfo.type === 'none' ? '' : `\nModel: *${modelInfo.model}*`
480
+
481
+ if (context.isThread && context.sessionId) {
482
+ await command.editReply({
483
+ content: `Switched to **${resolvedAgentName}** agent for this session${previousText}${modelText}\nThe agent will change on the next message.`,
484
+ })
485
+ } else {
486
+ await command.editReply({
487
+ content: `Switched to **${resolvedAgentName}** agent for this channel${previousText}${modelText}\nAll new sessions will use this agent.`,
488
+ })
489
+ }
490
+ } catch (error) {
491
+ agentLogger.error('Error in quick agent command:', error)
492
+ await command.editReply({
493
+ content: `Failed to switch agent: ${error instanceof Error ? error.message : 'Unknown error'}`,
494
+ })
495
+ }
496
+ }
@@ -0,0 +1,111 @@
1
+ // Tests AskUserQuestion request deduplication and cleanup helpers.
2
+
3
+ import { afterEach, describe, expect, test, vi } from 'vitest'
4
+ import type { ThreadChannel } from 'discord.js'
5
+ import {
6
+ deletePendingQuestionContextsForRequest,
7
+ pendingQuestionContexts,
8
+ showAskUserQuestionDropdowns,
9
+ } from './ask-question.js'
10
+
11
+ function createFakeThread(): ThreadChannel {
12
+ const send = vi.fn(async () => {
13
+ return { id: 'msg-1' }
14
+ })
15
+
16
+ return {
17
+ id: 'thread-1',
18
+ send,
19
+ } as unknown as ThreadChannel
20
+ }
21
+
22
+ afterEach(() => {
23
+ pendingQuestionContexts.clear()
24
+ vi.restoreAllMocks()
25
+ })
26
+
27
+ describe('ask-question', () => {
28
+ test('dedupes duplicate question requests for the same thread', async () => {
29
+ const thread = createFakeThread()
30
+
31
+ await showAskUserQuestionDropdowns({
32
+ thread,
33
+ sessionId: 'ses-1',
34
+ directory: '/project',
35
+ requestId: 'req-1',
36
+ input: {
37
+ questions: [{
38
+ question: 'Choose one',
39
+ header: 'Pick',
40
+ options: [
41
+ { label: 'Alpha', description: 'A' },
42
+ { label: 'Beta', description: 'B' },
43
+ ],
44
+ }],
45
+ },
46
+ })
47
+
48
+ await showAskUserQuestionDropdowns({
49
+ thread,
50
+ sessionId: 'ses-1',
51
+ directory: '/project',
52
+ requestId: 'req-1',
53
+ input: {
54
+ questions: [{
55
+ question: 'Choose one',
56
+ header: 'Pick',
57
+ options: [
58
+ { label: 'Alpha', description: 'A' },
59
+ { label: 'Beta', description: 'B' },
60
+ ],
61
+ }],
62
+ },
63
+ })
64
+
65
+ expect(thread.send).toHaveBeenCalledTimes(1)
66
+ expect(pendingQuestionContexts.size).toBe(1)
67
+ })
68
+
69
+ test('removes all duplicate contexts for one request', () => {
70
+ const thread = createFakeThread()
71
+ const baseContext: typeof pendingQuestionContexts extends Map<string, infer T>
72
+ ? T
73
+ : never = {
74
+ sessionId: 'ses-1',
75
+ directory: '/project',
76
+ thread,
77
+ requestId: 'req-1',
78
+ questions: [{
79
+ question: 'Choose one',
80
+ header: 'Pick',
81
+ options: [
82
+ { label: 'Alpha', description: 'A' },
83
+ { label: 'Beta', description: 'B' },
84
+ ],
85
+ }],
86
+ answers: {},
87
+ totalQuestions: 1,
88
+ answeredCount: 0,
89
+ contextHash: 'ctx-1',
90
+ }
91
+
92
+ pendingQuestionContexts.set('ctx-1', baseContext)
93
+ pendingQuestionContexts.set('ctx-2', {
94
+ ...baseContext,
95
+ contextHash: 'ctx-2',
96
+ })
97
+ pendingQuestionContexts.set('ctx-3', {
98
+ ...baseContext,
99
+ requestId: 'req-2',
100
+ contextHash: 'ctx-3',
101
+ })
102
+
103
+ const removed = deletePendingQuestionContextsForRequest({
104
+ threadId: thread.id,
105
+ requestId: 'req-1',
106
+ })
107
+
108
+ expect(removed).toBe(2)
109
+ expect([...pendingQuestionContexts.keys()]).toEqual(['ctx-3'])
110
+ })
111
+ })