@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,355 @@
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
+ import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, MessageFlags, } from 'discord.js';
4
+ import crypto from 'node:crypto';
5
+ import { setChannelAgent, setSessionAgent, clearSessionModel, getThreadSession, getSessionAgent, getChannelAgent, } from '../database.js';
6
+ import { initializeOpencodeForDirectory } from '../opencode.js';
7
+ import { resolveTextChannel, getOttoMetadata } from '../discord-utils.js';
8
+ import { createLogger, LogPrefix } from '../logger.js';
9
+ import { getCurrentModelInfo } from './model.js';
10
+ const agentLogger = createLogger(LogPrefix.AGENT);
11
+ const AGENT_CONTEXT_TTL_MS = 10 * 60 * 1000;
12
+ const pendingAgentContexts = new Map();
13
+ /**
14
+ * Get the current agent info for a channel/session, including where it comes from.
15
+ * Priority: session > channel > none
16
+ */
17
+ export async function getCurrentAgentInfo({ sessionId, channelId, }) {
18
+ if (sessionId) {
19
+ const sessionAgent = await getSessionAgent(sessionId);
20
+ if (sessionAgent) {
21
+ return { type: 'session', agent: sessionAgent };
22
+ }
23
+ }
24
+ if (channelId) {
25
+ const channelAgent = await getChannelAgent(channelId);
26
+ if (channelAgent) {
27
+ return { type: 'channel', agent: channelAgent };
28
+ }
29
+ }
30
+ return { type: 'none' };
31
+ }
32
+ /**
33
+ * Sanitize an agent name to be a valid Discord command name component.
34
+ * Lowercase, alphanumeric and hyphens only.
35
+ */
36
+ export function sanitizeAgentName(name) {
37
+ return name
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9-]/g, '-')
40
+ .replace(/-+/g, '-')
41
+ .replace(/^-|-$/g, '');
42
+ }
43
+ const QUICK_AGENT_DESCRIPTION_PATTERN = /^\[agent:([^\]]+)\]/;
44
+ /**
45
+ * Build quick-agent command description with an embedded original agent name.
46
+ * Metadata format: [agent:<original-name>] <visible description>
47
+ */
48
+ export function buildQuickAgentCommandDescription({ agentName, description, }) {
49
+ const metadataPrefix = `[agent:${agentName}]`;
50
+ if (metadataPrefix.length > 100) {
51
+ return metadataPrefix.slice(0, 100);
52
+ }
53
+ const visibleDescription = description || `Switch to ${agentName} agent`;
54
+ const maxVisibleLength = 100 - metadataPrefix.length - 1;
55
+ if (maxVisibleLength <= 0) {
56
+ return metadataPrefix;
57
+ }
58
+ const trimmedVisible = visibleDescription.slice(0, maxVisibleLength).trim();
59
+ if (!trimmedVisible) {
60
+ return metadataPrefix;
61
+ }
62
+ return `${metadataPrefix} ${trimmedVisible}`;
63
+ }
64
+ function parseQuickAgentNameFromDescription(description) {
65
+ if (!description) {
66
+ return undefined;
67
+ }
68
+ const match = QUICK_AGENT_DESCRIPTION_PATTERN.exec(description);
69
+ if (!match) {
70
+ return undefined;
71
+ }
72
+ const agentName = match[1]?.trim();
73
+ if (!agentName) {
74
+ return undefined;
75
+ }
76
+ return agentName;
77
+ }
78
+ async function resolveQuickAgentNameFromInteraction({ command, }) {
79
+ const fromCommandObject = parseQuickAgentNameFromDescription(command.command?.description);
80
+ if (fromCommandObject) {
81
+ return fromCommandObject;
82
+ }
83
+ if (!command.guild) {
84
+ return undefined;
85
+ }
86
+ const fetchedCommand = await command.guild.commands.fetch(command.commandId);
87
+ if (!fetchedCommand) {
88
+ return undefined;
89
+ }
90
+ return parseQuickAgentNameFromDescription(fetchedCommand.description);
91
+ }
92
+ /**
93
+ * Resolve the context for an agent command (directory, channel, session).
94
+ * Returns null if the command cannot be executed in this context.
95
+ */
96
+ export async function resolveAgentCommandContext({ interaction, }) {
97
+ const channel = interaction.channel;
98
+ if (!channel) {
99
+ await interaction.editReply({
100
+ content: 'This command can only be used in a channel',
101
+ });
102
+ return null;
103
+ }
104
+ const isThread = [
105
+ ChannelType.PublicThread,
106
+ ChannelType.PrivateThread,
107
+ ChannelType.AnnouncementThread,
108
+ ].includes(channel.type);
109
+ let projectDirectory;
110
+ let targetChannelId;
111
+ let sessionId;
112
+ if (isThread) {
113
+ const thread = channel;
114
+ const textChannel = await resolveTextChannel(thread);
115
+ const metadata = await getOttoMetadata(textChannel);
116
+ projectDirectory = metadata.projectDirectory;
117
+ targetChannelId = textChannel?.id || channel.id;
118
+ sessionId = await getThreadSession(thread.id);
119
+ }
120
+ else if (channel.type === ChannelType.GuildText) {
121
+ const textChannel = channel;
122
+ const metadata = await getOttoMetadata(textChannel);
123
+ projectDirectory = metadata.projectDirectory;
124
+ targetChannelId = channel.id;
125
+ }
126
+ else {
127
+ await interaction.editReply({
128
+ content: 'This command can only be used in text channels or threads',
129
+ });
130
+ return null;
131
+ }
132
+ if (!projectDirectory) {
133
+ await interaction.editReply({
134
+ content: 'This channel is not configured with a project directory',
135
+ });
136
+ return null;
137
+ }
138
+ return {
139
+ dir: projectDirectory,
140
+ channelId: targetChannelId,
141
+ sessionId,
142
+ isThread,
143
+ };
144
+ }
145
+ /**
146
+ * Set the agent preference for a context (session or channel).
147
+ * When switching agents for a session, clears session model preference
148
+ * so the new agent's model takes effect (agent model > channel model).
149
+ */
150
+ export async function setAgentForContext({ context, agentName, }) {
151
+ if (context.isThread && context.sessionId) {
152
+ await setSessionAgent(context.sessionId, agentName);
153
+ // Clear session model so the new agent's model takes effect
154
+ await clearSessionModel(context.sessionId);
155
+ agentLogger.log(`Set agent ${agentName} for session ${context.sessionId} (cleared session model)`);
156
+ }
157
+ else {
158
+ await setChannelAgent(context.channelId, agentName);
159
+ agentLogger.log(`Set agent ${agentName} for channel ${context.channelId}`);
160
+ }
161
+ }
162
+ export async function handleAgentCommand({ interaction, appId, }) {
163
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
164
+ const context = await resolveAgentCommandContext({ interaction, appId });
165
+ if (!context) {
166
+ return;
167
+ }
168
+ try {
169
+ const getClient = await initializeOpencodeForDirectory(context.dir);
170
+ if (getClient instanceof Error) {
171
+ await interaction.editReply({ content: getClient.message });
172
+ return;
173
+ }
174
+ const agentsResponse = await getClient().app.agents({
175
+ directory: context.dir,
176
+ });
177
+ if (!agentsResponse.data || agentsResponse.data.length === 0) {
178
+ await interaction.editReply({ content: 'No agents available' });
179
+ return;
180
+ }
181
+ const agents = agentsResponse.data
182
+ .filter((agent) => {
183
+ const hidden = agent.hidden;
184
+ return (agent.mode === 'primary' || agent.mode === 'all') && !hidden;
185
+ })
186
+ .slice(0, 25);
187
+ if (agents.length === 0) {
188
+ await interaction.editReply({ content: 'No primary agents available' });
189
+ return;
190
+ }
191
+ const currentAgentInfo = await getCurrentAgentInfo({
192
+ sessionId: context.sessionId,
193
+ channelId: context.channelId,
194
+ });
195
+ const currentAgentText = (() => {
196
+ switch (currentAgentInfo.type) {
197
+ case 'session':
198
+ return `**Current (session override):** \`${currentAgentInfo.agent}\``;
199
+ case 'channel':
200
+ return `**Current (channel override):** \`${currentAgentInfo.agent}\``;
201
+ case 'none':
202
+ return '**Current:** none';
203
+ }
204
+ })();
205
+ const contextHash = crypto.randomBytes(8).toString('hex');
206
+ pendingAgentContexts.set(contextHash, context);
207
+ setTimeout(() => {
208
+ pendingAgentContexts.delete(contextHash);
209
+ }, AGENT_CONTEXT_TTL_MS).unref();
210
+ const options = agents.map((agent) => ({
211
+ label: agent.name.slice(0, 100),
212
+ value: agent.name,
213
+ description: (agent.description || `${agent.mode} agent`).slice(0, 100),
214
+ }));
215
+ const selectMenu = new StringSelectMenuBuilder()
216
+ .setCustomId(`agent_select:${contextHash}`)
217
+ .setPlaceholder('Select an agent')
218
+ .addOptions(options);
219
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
220
+ await interaction.editReply({
221
+ content: `**Set Agent Preference**\n${currentAgentText}\nSelect an agent:`,
222
+ components: [actionRow],
223
+ });
224
+ }
225
+ catch (error) {
226
+ agentLogger.error('Error loading agents:', error);
227
+ await interaction.editReply({
228
+ content: `Failed to load agents: ${error instanceof Error ? error.message : 'Unknown error'}`,
229
+ });
230
+ }
231
+ }
232
+ export async function handleAgentSelectMenu(interaction) {
233
+ const customId = interaction.customId;
234
+ if (!customId.startsWith('agent_select:')) {
235
+ return;
236
+ }
237
+ await interaction.deferUpdate();
238
+ const contextHash = customId.replace('agent_select:', '');
239
+ const context = pendingAgentContexts.get(contextHash);
240
+ if (!context) {
241
+ await interaction.editReply({
242
+ content: 'Selection expired. Please run /agent again.',
243
+ components: [],
244
+ });
245
+ return;
246
+ }
247
+ const selectedAgent = interaction.values[0];
248
+ if (!selectedAgent) {
249
+ await interaction.editReply({
250
+ content: 'No agent selected',
251
+ components: [],
252
+ });
253
+ return;
254
+ }
255
+ try {
256
+ await setAgentForContext({ context, agentName: selectedAgent });
257
+ if (context.isThread && context.sessionId) {
258
+ await interaction.editReply({
259
+ content: `Agent preference set for this session: **${selectedAgent}**\nThe agent will change on the next message.`,
260
+ components: [],
261
+ });
262
+ }
263
+ else {
264
+ await interaction.editReply({
265
+ content: `Agent preference set for this channel: **${selectedAgent}**\nAll new sessions in this channel will use this agent.`,
266
+ components: [],
267
+ });
268
+ }
269
+ pendingAgentContexts.delete(contextHash);
270
+ }
271
+ catch (error) {
272
+ agentLogger.error('Error saving agent preference:', error);
273
+ await interaction.editReply({
274
+ content: `Failed to save agent preference: ${error instanceof Error ? error.message : 'Unknown error'}`,
275
+ components: [],
276
+ });
277
+ }
278
+ }
279
+ /**
280
+ * Handle quick agent commands like /plan-agent, /build-agent.
281
+ * These instantly switch to the specified agent without showing a dropdown.
282
+ *
283
+ * The slash command name is sanitized for Discord and can be lossy
284
+ * (for example gpt5.4 -> gpt5-4-agent). To keep the original agent name,
285
+ * registration stores [agent:<name>] metadata in the description and this
286
+ * handler resolves from that metadata first.
287
+ */
288
+ export async function handleQuickAgentCommand({ command, appId, }) {
289
+ const fallbackAgentName = command.commandName.replace(/-agent$/, '');
290
+ await command.deferReply({ flags: MessageFlags.Ephemeral });
291
+ const context = await resolveAgentCommandContext({
292
+ interaction: command,
293
+ appId,
294
+ });
295
+ if (!context) {
296
+ return;
297
+ }
298
+ try {
299
+ const resolvedAgentName = (await resolveQuickAgentNameFromInteraction({ command })) ||
300
+ fallbackAgentName;
301
+ // Check current agent and set new one.
302
+ // getCurrentAgentInfo is fast (DB only), use it for the "was X" text.
303
+ const previousAgent = await getCurrentAgentInfo({
304
+ sessionId: context.sessionId,
305
+ channelId: context.channelId,
306
+ });
307
+ const previousAgentName = previousAgent.type !== 'none' ? previousAgent.agent : undefined;
308
+ if (previousAgentName === resolvedAgentName) {
309
+ await command.editReply({
310
+ content: `Already using **${resolvedAgentName}** agent`,
311
+ });
312
+ return;
313
+ }
314
+ // Set the agent preference in DB for this context.
315
+ await setAgentForContext({ context, agentName: resolvedAgentName });
316
+ const previousText = previousAgentName
317
+ ? ` (was **${previousAgentName}**)`
318
+ : '';
319
+ // Resolve the model that will now be used for the new agent so we can
320
+ // show it in the reply. setAgentForContext already cleared any session
321
+ // model preference, so getCurrentModelInfo falls through to the agent's
322
+ // configured model (or channel/global/default).
323
+ const modelInfo = await (async () => {
324
+ const getClient = await initializeOpencodeForDirectory(context.dir);
325
+ if (getClient instanceof Error) {
326
+ return { type: 'none' };
327
+ }
328
+ return getCurrentModelInfo({
329
+ sessionId: context.sessionId,
330
+ channelId: context.channelId,
331
+ appId,
332
+ agentPreference: resolvedAgentName,
333
+ getClient,
334
+ directory: context.dir,
335
+ });
336
+ })();
337
+ const modelText = modelInfo.type === 'none' ? '' : `\nModel: *${modelInfo.model}*`;
338
+ if (context.isThread && context.sessionId) {
339
+ await command.editReply({
340
+ content: `Switched to **${resolvedAgentName}** agent for this session${previousText}${modelText}\nThe agent will change on the next message.`,
341
+ });
342
+ }
343
+ else {
344
+ await command.editReply({
345
+ content: `Switched to **${resolvedAgentName}** agent for this channel${previousText}${modelText}\nAll new sessions will use this agent.`,
346
+ });
347
+ }
348
+ }
349
+ catch (error) {
350
+ agentLogger.error('Error in quick agent command:', error);
351
+ await command.editReply({
352
+ content: `Failed to switch agent: ${error instanceof Error ? error.message : 'Unknown error'}`,
353
+ });
354
+ }
355
+ }
@@ -0,0 +1,320 @@
1
+ // AskUserQuestion tool handler - Shows Discord dropdowns for AI questions.
2
+ // When the AI uses the AskUserQuestion tool, this module renders dropdowns
3
+ // for each question and collects user responses.
4
+ import { StringSelectMenuBuilder, StringSelectMenuInteraction, ActionRowBuilder, MessageFlags, } from 'discord.js';
5
+ import crypto from 'node:crypto';
6
+ import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
7
+ import { getOpencodeClient } from '../opencode.js';
8
+ import { createLogger, LogPrefix } from '../logger.js';
9
+ const logger = createLogger(LogPrefix.ASK_QUESTION);
10
+ // Store pending question contexts by hash.
11
+ // TTL prevents unbounded growth if user never answers a question.
12
+ const QUESTION_CONTEXT_TTL_MS = 10 * 60 * 1000;
13
+ export const pendingQuestionContexts = new Map();
14
+ export function findPendingQuestionContextForRequest({ threadId, requestId, }) {
15
+ for (const [contextHash, context] of pendingQuestionContexts) {
16
+ if (context.thread.id !== threadId) {
17
+ continue;
18
+ }
19
+ if (context.requestId !== requestId) {
20
+ continue;
21
+ }
22
+ return { contextHash, context };
23
+ }
24
+ return null;
25
+ }
26
+ export function deletePendingQuestionContextsForRequest({ threadId, requestId, }) {
27
+ const matchingContextHashes = [...pendingQuestionContexts.entries()]
28
+ .filter(([, context]) => {
29
+ return context.thread.id === threadId && context.requestId === requestId;
30
+ })
31
+ .map(([contextHash]) => {
32
+ return contextHash;
33
+ });
34
+ matchingContextHashes.map((contextHash) => {
35
+ pendingQuestionContexts.delete(contextHash);
36
+ return contextHash;
37
+ });
38
+ return matchingContextHashes.length;
39
+ }
40
+ export function hasPendingQuestionForThread(threadId) {
41
+ return [...pendingQuestionContexts.values()].some((ctx) => {
42
+ return ctx.thread.id === threadId;
43
+ });
44
+ }
45
+ /**
46
+ * Show dropdown menus for question tool input.
47
+ * Sends one message per question with the dropdown directly under the question text.
48
+ */
49
+ export async function showAskUserQuestionDropdowns({ thread, sessionId, directory, requestId, input, silent, }) {
50
+ const existingPending = findPendingQuestionContextForRequest({
51
+ threadId: thread.id,
52
+ requestId,
53
+ });
54
+ if (existingPending) {
55
+ logger.log(`Deduped question ${requestId} for thread ${thread.id} (existing context ${existingPending.contextHash})`);
56
+ return;
57
+ }
58
+ const contextHash = crypto.randomBytes(8).toString('hex');
59
+ const context = {
60
+ sessionId,
61
+ directory,
62
+ thread,
63
+ requestId,
64
+ questions: input.questions,
65
+ answers: {},
66
+ totalQuestions: input.questions.length,
67
+ answeredCount: 0,
68
+ contextHash,
69
+ };
70
+ pendingQuestionContexts.set(contextHash, context);
71
+ // On TTL expiry: hide the dropdown UI and abort the session so OpenCode
72
+ // unblocks. We intentionally do NOT call question.reply() — sending 'Other'
73
+ // made the model think the user chose an option when they didn't.
74
+ setTimeout(async () => {
75
+ const ctx = pendingQuestionContexts.get(contextHash);
76
+ if (!ctx) {
77
+ return;
78
+ }
79
+ // Delete context first so the dropdown becomes inert immediately.
80
+ // Without this, a user clicking during the abort() await would still
81
+ // be accepted by handleAskQuestionSelectMenu, then abort() would
82
+ // kill that valid run.
83
+ deletePendingQuestionContextsForRequest({
84
+ threadId: ctx.thread.id,
85
+ requestId: ctx.requestId,
86
+ });
87
+ // Abort the session so OpenCode isn't stuck waiting for a reply
88
+ const client = getOpencodeClient(ctx.directory);
89
+ if (client) {
90
+ await client.session.abort({
91
+ sessionID: ctx.sessionId,
92
+ }).catch((error) => {
93
+ logger.error('Failed to abort session after question expiry:', error);
94
+ });
95
+ }
96
+ }, QUESTION_CONTEXT_TTL_MS).unref();
97
+ // Send one message per question with its dropdown directly underneath
98
+ for (let i = 0; i < input.questions.length; i++) {
99
+ const q = input.questions[i];
100
+ // Map options to Discord select menu options
101
+ // Discord max: 25 options per select menu
102
+ const options = [
103
+ ...q.options.slice(0, 24).map((opt, optIdx) => ({
104
+ label: opt.label.slice(0, 100),
105
+ value: `${optIdx}`,
106
+ description: opt.description.slice(0, 100),
107
+ })),
108
+ {
109
+ label: 'Other',
110
+ value: 'other',
111
+ description: 'Provide a custom answer in chat',
112
+ },
113
+ ];
114
+ const placeholder = options.find((x) => x.label)?.label || 'Select an option';
115
+ const selectMenu = new StringSelectMenuBuilder()
116
+ .setCustomId(`ask_question:${contextHash}:${i}`)
117
+ .setPlaceholder(placeholder)
118
+ .addOptions(options);
119
+ // Enable multi-select if the question supports it
120
+ if (q.multiple) {
121
+ selectMenu.setMinValues(1);
122
+ selectMenu.setMaxValues(options.length);
123
+ }
124
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
125
+ await thread.send({
126
+ content: `**${(q.header || '').slice(0, 200)}**\n${q.question.slice(0, 1700)}`,
127
+ components: [actionRow],
128
+ flags: silent ? SILENT_MESSAGE_FLAGS : NOTIFY_MESSAGE_FLAGS,
129
+ });
130
+ }
131
+ logger.log(`Showed ${input.questions.length} question dropdown(s) for session ${sessionId}`);
132
+ }
133
+ /**
134
+ * Handle dropdown selection for AskUserQuestion.
135
+ */
136
+ export async function handleAskQuestionSelectMenu(interaction) {
137
+ const customId = interaction.customId;
138
+ if (!customId.startsWith('ask_question:')) {
139
+ return;
140
+ }
141
+ const parts = customId.split(':');
142
+ const contextHash = parts[1];
143
+ const questionIndex = parseInt(parts[2], 10);
144
+ if (!contextHash) {
145
+ await interaction.reply({
146
+ content: 'Invalid selection.',
147
+ flags: MessageFlags.Ephemeral,
148
+ });
149
+ return;
150
+ }
151
+ const context = pendingQuestionContexts.get(contextHash);
152
+ if (!context) {
153
+ await interaction.reply({
154
+ content: 'This question has expired. Please ask the AI again.',
155
+ flags: MessageFlags.Ephemeral,
156
+ });
157
+ return;
158
+ }
159
+ await interaction.deferUpdate();
160
+ const selectedValues = interaction.values;
161
+ const question = context.questions[questionIndex];
162
+ if (!question) {
163
+ logger.error(`Question index ${questionIndex} not found in context`);
164
+ return;
165
+ }
166
+ // Check if "other" was selected
167
+ if (selectedValues.includes('other')) {
168
+ // User wants to provide custom answer
169
+ // For now, mark as "Other" - they can type in chat
170
+ context.answers[questionIndex] = ['Other (please type your answer in chat)'];
171
+ }
172
+ else {
173
+ // Map value indices back to option labels
174
+ context.answers[questionIndex] = selectedValues.map((v) => {
175
+ const optIdx = parseInt(v, 10);
176
+ return question.options[optIdx]?.label || `Option ${optIdx + 1}`;
177
+ });
178
+ }
179
+ context.answeredCount++;
180
+ // Update this question's message: show answer and remove dropdown
181
+ const answeredText = context.answers[questionIndex].join(', ');
182
+ await interaction.editReply({
183
+ content: `**${question.header}**\n${question.question}\n✓ _${answeredText}_`,
184
+ components: [], // Remove the dropdown
185
+ });
186
+ // Check if all questions are answered
187
+ if (context.answeredCount >= context.totalQuestions) {
188
+ // All questions answered - send result back to session
189
+ await submitQuestionAnswers(context);
190
+ deletePendingQuestionContextsForRequest({
191
+ threadId: context.thread.id,
192
+ requestId: context.requestId,
193
+ });
194
+ }
195
+ }
196
+ /**
197
+ * Submit all collected answers back to the OpenCode session.
198
+ * Uses the question.reply API to provide answers to the waiting tool.
199
+ */
200
+ async function submitQuestionAnswers(context) {
201
+ try {
202
+ const client = getOpencodeClient(context.directory);
203
+ if (!client) {
204
+ throw new Error('OpenCode server not found for directory');
205
+ }
206
+ // Build answers array: each element is an array of selected labels for that question
207
+ const answers = context.questions.map((_, i) => {
208
+ return context.answers[i] || [];
209
+ });
210
+ await client.question.reply({
211
+ requestID: context.requestId,
212
+ directory: context.directory,
213
+ answers,
214
+ });
215
+ logger.log(`Submitted answers for question ${context.requestId} in session ${context.sessionId}`);
216
+ }
217
+ catch (error) {
218
+ logger.error('Failed to submit answers:', error);
219
+ await sendThreadMessage(context.thread, `✗ Failed to submit answers: ${error instanceof Error ? error.message : 'Unknown error'}`);
220
+ }
221
+ }
222
+ /**
223
+ * Check if a tool part is an AskUserQuestion tool.
224
+ * Returns the parsed input if valid, null otherwise.
225
+ */
226
+ export function parseAskUserQuestionTool(part) {
227
+ if (part.type !== 'tool') {
228
+ return null;
229
+ }
230
+ // Check for the tool name (case-insensitive)
231
+ const toolName = part.tool?.toLowerCase();
232
+ if (toolName !== 'question') {
233
+ return null;
234
+ }
235
+ const input = part.state?.input;
236
+ if (!input?.questions ||
237
+ !Array.isArray(input.questions) ||
238
+ input.questions.length === 0) {
239
+ return null;
240
+ }
241
+ // Validate structure
242
+ for (const q of input.questions) {
243
+ if (typeof q.question !== 'string' ||
244
+ typeof q.header !== 'string' ||
245
+ !Array.isArray(q.options) ||
246
+ q.options.length < 2) {
247
+ return null;
248
+ }
249
+ }
250
+ return input;
251
+ }
252
+ /**
253
+ * Cancel a pending question for a thread.
254
+ *
255
+ * Two modes depending on whether `userMessage` is provided:
256
+ *
257
+ * - `cancelPendingQuestion(threadId)` — cleanup only. Removes the context
258
+ * without replying to OpenCode. Use when aborting the blocked session
259
+ * separately (e.g. voice/attachment messages whose content needs
260
+ * transcription first). Returns 'no-pending' in both "found+cleaned" and
261
+ * "nothing found" cases.
262
+ *
263
+ * - `cancelPendingQuestion(threadId, text)` — reply path. Sends the text as
264
+ * the tool answer so the model sees the user's response. The caller should
265
+ * NOT also enqueue the message as a new prompt.
266
+ * Returns 'replied' on success, 'reply-failed' if the reply call fails
267
+ * (context kept pending so TTL can retry).
268
+ */
269
+ export async function cancelPendingQuestion(threadId, userMessage) {
270
+ // Find pending question for this thread
271
+ let contextHash;
272
+ let context;
273
+ for (const [hash, ctx] of pendingQuestionContexts) {
274
+ if (ctx.thread.id === threadId) {
275
+ contextHash = hash;
276
+ context = ctx;
277
+ break;
278
+ }
279
+ }
280
+ if (!contextHash || !context) {
281
+ return 'no-pending';
282
+ }
283
+ // undefined means teardown/cleanup — just remove context, don't reply.
284
+ // The session is already being torn down or the caller wants to dismiss
285
+ // the question without providing an answer (e.g. voice/attachment-only
286
+ // messages where content needs transcription before it can be an answer).
287
+ if (userMessage === undefined) {
288
+ deletePendingQuestionContextsForRequest({
289
+ threadId: context.thread.id,
290
+ requestId: context.requestId,
291
+ });
292
+ return 'no-pending';
293
+ }
294
+ try {
295
+ const client = getOpencodeClient(context.directory);
296
+ if (!client) {
297
+ throw new Error('OpenCode server not found for directory');
298
+ }
299
+ const answers = context.questions.map((_, i) => {
300
+ return context.answers[i] || [userMessage];
301
+ });
302
+ await client.question.reply({
303
+ requestID: context.requestId,
304
+ directory: context.directory,
305
+ answers,
306
+ });
307
+ logger.log(`Answered question ${context.requestId} with user message`);
308
+ }
309
+ catch (error) {
310
+ logger.error('Failed to answer question:', error);
311
+ // Keep context pending so TTL can still fire.
312
+ // Caller should not consume the user message since reply failed.
313
+ return 'reply-failed';
314
+ }
315
+ deletePendingQuestionContextsForRequest({
316
+ threadId: context.thread.id,
317
+ requestId: context.requestId,
318
+ });
319
+ return 'replied';
320
+ }