@otto-assistant/otto 0.1.2 → 0.7.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (638) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-account-identity.js +62 -0
  7. package/dist/anthropic-account-identity.test.js +38 -0
  8. package/dist/anthropic-auth-plugin.js +917 -0
  9. package/dist/anthropic-auth-state.js +303 -0
  10. package/dist/anthropic-auth-state.test.js +150 -0
  11. package/dist/bin.js +152 -0
  12. package/dist/btw-prefix-detection.js +17 -0
  13. package/dist/btw-prefix-detection.test.js +63 -0
  14. package/dist/channel-management.js +259 -0
  15. package/dist/cli-parsing.test.js +142 -0
  16. package/dist/cli-send-thread.e2e.test.js +353 -0
  17. package/dist/cli-telegram-options.test.js +99 -0
  18. package/dist/cli.js +4210 -568
  19. package/dist/commands/abort.js +65 -0
  20. package/dist/commands/action-buttons.js +245 -0
  21. package/dist/commands/add-dir.js +124 -0
  22. package/dist/commands/add-dir.test.js +126 -0
  23. package/dist/commands/add-project.js +113 -0
  24. package/dist/commands/agent.js +355 -0
  25. package/dist/commands/ask-question.js +320 -0
  26. package/dist/commands/ask-question.test.js +92 -0
  27. package/dist/commands/btw.js +121 -0
  28. package/dist/commands/cli-commands-group-a.test.js +728 -0
  29. package/dist/commands/cli-commands-group-b.test.js +695 -0
  30. package/dist/commands/compact.js +120 -0
  31. package/dist/commands/context-usage.js +140 -0
  32. package/dist/commands/create-new-project.js +130 -0
  33. package/dist/commands/diff.js +63 -0
  34. package/dist/commands/discord-commands-group-a.test.js +655 -0
  35. package/dist/commands/discord-commands-group-b.test.js +595 -0
  36. package/dist/commands/discord-commands-group-c.test.js +739 -0
  37. package/dist/commands/file-upload.js +275 -0
  38. package/dist/commands/fork-subagent.js +177 -0
  39. package/dist/commands/fork.js +262 -0
  40. package/dist/commands/gemini-apikey.js +70 -0
  41. package/dist/commands/login.js +893 -0
  42. package/dist/commands/mcp.js +239 -0
  43. package/dist/commands/memory-snapshot.js +24 -0
  44. package/dist/commands/mention-mode.js +44 -0
  45. package/dist/commands/merge-worktree.js +162 -0
  46. package/dist/commands/model-variant.js +369 -0
  47. package/dist/commands/model.js +798 -0
  48. package/dist/commands/new-worktree.js +465 -0
  49. package/dist/commands/paginated-select.js +57 -0
  50. package/dist/commands/permissions.js +274 -0
  51. package/dist/commands/queue.js +223 -0
  52. package/dist/commands/remove-project.js +115 -0
  53. package/dist/commands/restart-opencode-server.js +127 -0
  54. package/dist/commands/resume.js +149 -0
  55. package/dist/commands/run-command.js +79 -0
  56. package/dist/commands/screenshare.js +303 -0
  57. package/dist/commands/screenshare.test.js +20 -0
  58. package/dist/commands/session-id.js +78 -0
  59. package/dist/commands/session.js +176 -0
  60. package/dist/commands/share.js +80 -0
  61. package/dist/commands/tasks.js +205 -0
  62. package/dist/commands/thread-deletion-sync.js +50 -0
  63. package/dist/commands/types.js +2 -0
  64. package/dist/commands/undo-redo.js +305 -0
  65. package/dist/commands/unset-model.js +139 -0
  66. package/dist/commands/upgrade.js +48 -0
  67. package/dist/commands/user-command.js +155 -0
  68. package/dist/commands/verbosity.js +125 -0
  69. package/dist/commands/vscode.js +269 -0
  70. package/dist/commands/worktree-settings.js +43 -0
  71. package/dist/commands/worktrees.js +468 -0
  72. package/dist/condense-memory.js +33 -0
  73. package/dist/config.js +100 -255
  74. package/dist/context-awareness-plugin.js +340 -0
  75. package/dist/context-awareness-plugin.test.js +126 -0
  76. package/dist/critique-utils.js +95 -0
  77. package/dist/database.js +1355 -0
  78. package/dist/db.js +260 -0
  79. package/dist/db.test.js +138 -0
  80. package/dist/debounce-timeout.js +28 -0
  81. package/dist/debounced-process-flush.js +77 -0
  82. package/dist/discord-bot.js +1124 -0
  83. package/dist/discord-command-registration.js +567 -0
  84. package/dist/discord-urls.js +82 -0
  85. package/dist/discord-utils.js +616 -0
  86. package/dist/discord-utils.test.js +134 -0
  87. package/dist/errors.js +179 -0
  88. package/dist/escape-backticks.test.js +429 -0
  89. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  90. package/dist/eventsource-parser.test.js +327 -0
  91. package/dist/exec-async.js +26 -0
  92. package/dist/external-opencode-sync.js +480 -0
  93. package/dist/format-tables.js +491 -0
  94. package/dist/format-tables.test.js +478 -0
  95. package/dist/forum-sync/config.js +79 -0
  96. package/dist/forum-sync/discord-operations.js +154 -0
  97. package/dist/forum-sync/index.js +5 -0
  98. package/dist/forum-sync/markdown.js +113 -0
  99. package/dist/forum-sync/sync-to-discord.js +417 -0
  100. package/dist/forum-sync/sync-to-files.js +190 -0
  101. package/dist/forum-sync/types.js +53 -0
  102. package/dist/forum-sync/watchers.js +307 -0
  103. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  104. package/dist/gateway-proxy.e2e.test.js +485 -0
  105. package/dist/genai-worker-wrapper.js +111 -0
  106. package/dist/genai-worker.js +311 -0
  107. package/dist/genai.js +232 -0
  108. package/dist/generated/browser.js +17 -0
  109. package/dist/generated/client.js +37 -0
  110. package/dist/generated/commonInputTypes.js +10 -0
  111. package/dist/generated/enums.js +58 -0
  112. package/dist/generated/internal/class.js +49 -0
  113. package/dist/generated/internal/prismaNamespace.js +254 -0
  114. package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
  115. package/dist/generated/models/bot_api_keys.js +1 -0
  116. package/dist/generated/models/bot_tokens.js +1 -0
  117. package/dist/generated/models/channel_agents.js +1 -0
  118. package/dist/generated/models/channel_directories.js +1 -0
  119. package/dist/generated/models/channel_mention_mode.js +1 -0
  120. package/dist/generated/models/channel_models.js +1 -0
  121. package/dist/generated/models/channel_verbosity.js +1 -0
  122. package/dist/generated/models/channel_worktrees.js +1 -0
  123. package/dist/generated/models/forum_sync_configs.js +1 -0
  124. package/dist/generated/models/global_models.js +1 -0
  125. package/dist/generated/models/ipc_requests.js +1 -0
  126. package/dist/generated/models/part_messages.js +1 -0
  127. package/dist/generated/models/scheduled_tasks.js +1 -0
  128. package/dist/generated/models/session_agents.js +1 -0
  129. package/dist/generated/models/session_events.js +1 -0
  130. package/dist/generated/models/session_models.js +1 -0
  131. package/dist/generated/models/session_start_sources.js +1 -0
  132. package/dist/generated/models/thread_sessions.js +1 -0
  133. package/dist/generated/models/thread_worktrees.js +1 -0
  134. package/dist/generated/models.js +1 -0
  135. package/dist/heap-monitor.js +122 -0
  136. package/dist/hrana-server.js +251 -0
  137. package/dist/hrana-server.test.js +370 -0
  138. package/dist/html-actions.js +123 -0
  139. package/dist/html-actions.test.js +70 -0
  140. package/dist/html-components.js +117 -0
  141. package/dist/html-components.test.js +34 -0
  142. package/dist/image-optimizer-plugin.js +153 -0
  143. package/dist/image-utils.js +112 -0
  144. package/dist/interaction-handler.js +420 -0
  145. package/dist/ipc-polling.js +327 -0
  146. package/dist/ipc-tools-plugin.js +193 -0
  147. package/dist/ipc-utils.js +18 -0
  148. package/dist/limit-heading-depth.js +25 -0
  149. package/dist/limit-heading-depth.test.js +105 -0
  150. package/dist/logger.js +171 -0
  151. package/dist/markdown.js +342 -0
  152. package/dist/markdown.test.js +264 -0
  153. package/dist/memory-overview-plugin.js +128 -0
  154. package/dist/message-finish-field.e2e.test.js +168 -0
  155. package/dist/message-formatting.js +415 -0
  156. package/dist/message-formatting.test.js +115 -0
  157. package/dist/message-preprocessing.js +359 -0
  158. package/dist/onboarding-tutorial.js +163 -0
  159. package/dist/onboarding-welcome.js +37 -0
  160. package/dist/openai-realtime.js +224 -0
  161. package/dist/opencode-command-detection.js +65 -0
  162. package/dist/opencode-command-detection.test.js +240 -0
  163. package/dist/opencode-command.js +131 -0
  164. package/dist/opencode-command.test.js +48 -0
  165. package/dist/opencode-interrupt-plugin.js +388 -0
  166. package/dist/opencode-interrupt-plugin.test.js +463 -0
  167. package/dist/opencode.js +1124 -0
  168. package/dist/otto/branding.js +22 -0
  169. package/dist/otto/index.js +21 -0
  170. package/dist/otto-digital-twin.e2e.test.js +161 -0
  171. package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
  172. package/dist/otto-opencode-plugin.js +21 -0
  173. package/dist/otto-opencode-plugin.test.js +98 -0
  174. package/dist/parse-permission-rules.test.js +117 -0
  175. package/dist/patch-text-parser.js +97 -0
  176. package/dist/plugin-logger.js +68 -0
  177. package/dist/privacy-sanitizer.js +105 -0
  178. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  179. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  180. package/dist/queue-advanced-e2e-setup.js +790 -0
  181. package/dist/queue-advanced-footer.e2e.test.js +481 -0
  182. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  183. package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
  184. package/dist/queue-advanced-question.e2e.test.js +261 -0
  185. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  186. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  187. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  188. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  189. package/dist/queue-question-select-drain.e2e.test.js +256 -0
  190. package/dist/runtime-idle-sweeper.js +52 -0
  191. package/dist/runtime-lifecycle.e2e.test.js +514 -0
  192. package/dist/sentry.js +23 -0
  193. package/dist/session-handler/agent-utils.js +67 -0
  194. package/dist/session-handler/event-stream-state.js +475 -0
  195. package/dist/session-handler/event-stream-state.test.js +632 -0
  196. package/dist/session-handler/model-utils.js +147 -0
  197. package/dist/session-handler/opencode-session-event-log.js +94 -0
  198. package/dist/session-handler/thread-runtime-state.js +131 -0
  199. package/dist/session-handler/thread-session-runtime.js +3390 -0
  200. package/dist/session-handler.js +9 -0
  201. package/dist/session-search.js +100 -0
  202. package/dist/session-search.test.js +40 -0
  203. package/dist/session-title-rename.test.js +92 -0
  204. package/dist/skill-filter.js +31 -0
  205. package/dist/skill-filter.test.js +65 -0
  206. package/dist/startup-service.js +153 -0
  207. package/dist/startup-time.e2e.test.js +296 -0
  208. package/dist/store.js +19 -0
  209. package/dist/subagent-rate-limit-plugin.js +175 -0
  210. package/dist/system-message.js +702 -0
  211. package/dist/system-message.test.js +697 -0
  212. package/dist/task-runner.js +530 -0
  213. package/dist/task-schedule.js +213 -0
  214. package/dist/task-schedule.test.js +71 -0
  215. package/dist/test-utils.js +313 -0
  216. package/dist/thinking-utils.js +35 -0
  217. package/dist/thread-message-queue.e2e.test.js +1111 -0
  218. package/dist/tools.js +357 -0
  219. package/dist/undo-redo.e2e.test.js +161 -0
  220. package/dist/unnest-code-blocks.js +146 -0
  221. package/dist/unnest-code-blocks.test.js +673 -0
  222. package/dist/upgrade.js +156 -0
  223. package/dist/utils.js +172 -0
  224. package/dist/utils.test.js +130 -0
  225. package/dist/voice-attachment.js +34 -0
  226. package/dist/voice-handler.js +646 -0
  227. package/dist/voice-message.e2e.test.js +1021 -0
  228. package/dist/voice.js +456 -0
  229. package/dist/voice.test.js +235 -0
  230. package/dist/wait-session.js +171 -0
  231. package/dist/websockify.js +69 -0
  232. package/dist/worker-types.js +4 -0
  233. package/dist/worktree-lifecycle.e2e.test.js +311 -0
  234. package/dist/worktree-utils.js +3 -0
  235. package/dist/worktrees.js +991 -0
  236. package/dist/worktrees.test.js +415 -0
  237. package/dist/xml.js +92 -0
  238. package/dist/xml.test.js +32 -0
  239. package/package.json +90 -38
  240. package/schema.prisma +303 -0
  241. package/skills/batch/SKILL.md +87 -0
  242. package/skills/critique/SKILL.md +112 -0
  243. package/skills/egaki/SKILL.md +100 -0
  244. package/skills/errore/SKILL.md +647 -0
  245. package/skills/event-sourcing-state/SKILL.md +252 -0
  246. package/skills/goke/SKILL.md +38 -0
  247. package/skills/jitter/EDITOR.md +219 -0
  248. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  249. package/skills/jitter/SKILL.md +158 -0
  250. package/skills/jitter/jitter-clipboard.json +1042 -0
  251. package/skills/jitter/package.json +14 -0
  252. package/skills/jitter/tsconfig.json +15 -0
  253. package/skills/jitter/utils/actions.ts +212 -0
  254. package/skills/jitter/utils/export.ts +114 -0
  255. package/skills/jitter/utils/index.ts +141 -0
  256. package/skills/jitter/utils/snapshot.ts +154 -0
  257. package/skills/jitter/utils/traverse.ts +246 -0
  258. package/skills/jitter/utils/types.ts +279 -0
  259. package/skills/jitter/utils/wait.ts +133 -0
  260. package/skills/lintcn/SKILL.md +873 -0
  261. package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
  262. package/skills/new-skill/SKILL.md +237 -0
  263. package/skills/npm-package/SKILL.md +617 -0
  264. package/skills/opensrc/SKILL.md +78 -0
  265. package/skills/otto-publish/SKILL.md +61 -0
  266. package/skills/playwriter/SKILL.md +35 -0
  267. package/skills/profano/SKILL.md +16 -0
  268. package/skills/proxyman/SKILL.md +215 -0
  269. package/skills/security-review/SKILL.md +208 -0
  270. package/skills/sigillo/SKILL.md +101 -0
  271. package/skills/simplify/SKILL.md +58 -0
  272. package/skills/spiceflow/SKILL.md +28 -0
  273. package/skills/termcast/SKILL.md +945 -0
  274. package/skills/tuistory/SKILL.md +98 -0
  275. package/skills/usecomputer/SKILL.md +264 -0
  276. package/skills/x-articles/SKILL.md +554 -0
  277. package/skills/zele/SKILL.md +49 -0
  278. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  279. package/src/agent-model.e2e.test.ts +979 -0
  280. package/src/ai-tool-to-genai.test.ts +296 -0
  281. package/src/ai-tool-to-genai.ts +283 -0
  282. package/src/ai-tool.ts +39 -0
  283. package/src/anthropic-account-identity.test.ts +52 -0
  284. package/src/anthropic-account-identity.ts +77 -0
  285. package/src/anthropic-auth-plugin.ts +1139 -0
  286. package/src/anthropic-auth-state.test.ts +187 -0
  287. package/src/anthropic-auth-state.ts +386 -0
  288. package/src/bin.ts +182 -0
  289. package/src/btw-prefix-detection.test.ts +73 -0
  290. package/src/btw-prefix-detection.ts +23 -0
  291. package/src/channel-management.ts +376 -0
  292. package/src/cli-parsing.test.ts +197 -0
  293. package/src/cli-send-thread.e2e.test.ts +463 -0
  294. package/src/cli-telegram-options.test.ts +114 -0
  295. package/src/cli.ts +5718 -580
  296. package/src/commands/abort.ts +89 -0
  297. package/src/commands/action-buttons.ts +364 -0
  298. package/src/commands/add-dir.test.ts +154 -0
  299. package/src/commands/add-dir.ts +175 -0
  300. package/src/commands/add-project.ts +149 -0
  301. package/src/commands/agent.ts +496 -0
  302. package/src/commands/ask-question.test.ts +111 -0
  303. package/src/commands/ask-question.ts +455 -0
  304. package/src/commands/btw.ts +184 -0
  305. package/src/commands/cli-commands-group-a.test.ts +837 -0
  306. package/src/commands/cli-commands-group-b.test.ts +800 -0
  307. package/src/commands/compact.ts +157 -0
  308. package/src/commands/context-usage.ts +199 -0
  309. package/src/commands/create-new-project.ts +190 -0
  310. package/src/commands/diff.ts +91 -0
  311. package/src/commands/discord-commands-group-a.test.ts +789 -0
  312. package/src/commands/discord-commands-group-b.test.ts +648 -0
  313. package/src/commands/discord-commands-group-c.test.ts +882 -0
  314. package/src/commands/file-upload.ts +389 -0
  315. package/src/commands/fork-subagent.ts +263 -0
  316. package/src/commands/fork.ts +386 -0
  317. package/src/commands/gemini-apikey.ts +104 -0
  318. package/src/commands/login.ts +1181 -0
  319. package/src/commands/mcp.ts +307 -0
  320. package/src/commands/memory-snapshot.ts +30 -0
  321. package/src/commands/mention-mode.ts +68 -0
  322. package/src/commands/merge-worktree.ts +226 -0
  323. package/src/commands/model-variant.ts +488 -0
  324. package/src/commands/model.ts +1082 -0
  325. package/src/commands/new-worktree.ts +645 -0
  326. package/src/commands/paginated-select.ts +81 -0
  327. package/src/commands/permissions.ts +397 -0
  328. package/src/commands/queue.ts +293 -0
  329. package/src/commands/remove-project.ts +155 -0
  330. package/src/commands/restart-opencode-server.ts +162 -0
  331. package/src/commands/resume.ts +230 -0
  332. package/src/commands/run-command.ts +123 -0
  333. package/src/commands/screenshare.test.ts +30 -0
  334. package/src/commands/screenshare.ts +366 -0
  335. package/src/commands/session-id.ts +109 -0
  336. package/src/commands/session.ts +227 -0
  337. package/src/commands/share.ts +106 -0
  338. package/src/commands/tasks.ts +293 -0
  339. package/src/commands/thread-deletion-sync.ts +80 -0
  340. package/src/commands/types.ts +25 -0
  341. package/src/commands/undo-redo.ts +386 -0
  342. package/src/commands/unset-model.ts +174 -0
  343. package/src/commands/upgrade.ts +59 -0
  344. package/src/commands/user-command.ts +198 -0
  345. package/src/commands/verbosity.ts +173 -0
  346. package/src/commands/vscode.ts +342 -0
  347. package/src/commands/worktree-settings.ts +70 -0
  348. package/src/commands/worktrees.ts +645 -0
  349. package/src/condense-memory.ts +36 -0
  350. package/src/config.ts +103 -339
  351. package/src/context-awareness-plugin.test.ts +144 -0
  352. package/src/context-awareness-plugin.ts +469 -0
  353. package/src/critique-utils.ts +139 -0
  354. package/src/database.ts +1949 -0
  355. package/src/db.test.ts +162 -0
  356. package/src/db.ts +295 -0
  357. package/src/debounce-timeout.ts +43 -0
  358. package/src/debounced-process-flush.ts +104 -0
  359. package/src/discord-bot.ts +1507 -0
  360. package/src/discord-command-registration.ts +752 -0
  361. package/src/discord-urls.ts +89 -0
  362. package/src/discord-utils.test.ts +153 -0
  363. package/src/discord-utils.ts +846 -0
  364. package/src/errors.ts +232 -0
  365. package/src/escape-backticks.test.ts +469 -0
  366. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  367. package/src/eventsource-parser.test.ts +351 -0
  368. package/src/exec-async.ts +35 -0
  369. package/src/external-opencode-sync.ts +685 -0
  370. package/src/format-tables.test.ts +515 -0
  371. package/src/format-tables.ts +718 -0
  372. package/src/forum-sync/config.ts +92 -0
  373. package/src/forum-sync/discord-operations.ts +241 -0
  374. package/src/forum-sync/index.ts +9 -0
  375. package/src/forum-sync/markdown.ts +172 -0
  376. package/src/forum-sync/sync-to-discord.ts +595 -0
  377. package/src/forum-sync/sync-to-files.ts +294 -0
  378. package/src/forum-sync/types.ts +175 -0
  379. package/src/forum-sync/watchers.ts +454 -0
  380. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  381. package/src/gateway-proxy.e2e.test.ts +644 -0
  382. package/src/genai-worker-wrapper.ts +164 -0
  383. package/src/genai-worker.ts +386 -0
  384. package/src/genai.ts +321 -0
  385. package/src/generated/browser.ts +114 -0
  386. package/src/generated/client.ts +138 -0
  387. package/src/generated/commonInputTypes.ts +770 -0
  388. package/src/generated/enums.ts +98 -0
  389. package/src/generated/internal/class.ts +384 -0
  390. package/src/generated/internal/prismaNamespace.ts +2394 -0
  391. package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
  392. package/src/generated/models/bot_api_keys.ts +1288 -0
  393. package/src/generated/models/bot_tokens.ts +1700 -0
  394. package/src/generated/models/channel_agents.ts +1256 -0
  395. package/src/generated/models/channel_directories.ts +1859 -0
  396. package/src/generated/models/channel_mention_mode.ts +1300 -0
  397. package/src/generated/models/channel_models.ts +1288 -0
  398. package/src/generated/models/channel_verbosity.ts +1228 -0
  399. package/src/generated/models/channel_worktrees.ts +1300 -0
  400. package/src/generated/models/forum_sync_configs.ts +1452 -0
  401. package/src/generated/models/global_models.ts +1288 -0
  402. package/src/generated/models/ipc_requests.ts +1485 -0
  403. package/src/generated/models/part_messages.ts +1302 -0
  404. package/src/generated/models/scheduled_tasks.ts +2320 -0
  405. package/src/generated/models/session_agents.ts +1086 -0
  406. package/src/generated/models/session_events.ts +1439 -0
  407. package/src/generated/models/session_models.ts +1114 -0
  408. package/src/generated/models/session_start_sources.ts +1408 -0
  409. package/src/generated/models/thread_sessions.ts +1781 -0
  410. package/src/generated/models/thread_worktrees.ts +1356 -0
  411. package/src/generated/models.ts +30 -0
  412. package/src/heap-monitor.ts +152 -0
  413. package/src/hrana-server.test.ts +434 -0
  414. package/src/hrana-server.ts +299 -0
  415. package/src/html-actions.test.ts +87 -0
  416. package/src/html-actions.ts +174 -0
  417. package/src/html-components.test.ts +38 -0
  418. package/src/html-components.ts +181 -0
  419. package/src/image-optimizer-plugin.ts +194 -0
  420. package/src/image-utils.ts +149 -0
  421. package/src/interaction-handler.ts +610 -0
  422. package/src/ipc-polling.ts +427 -0
  423. package/src/ipc-tools-plugin.ts +236 -0
  424. package/src/ipc-utils.ts +29 -0
  425. package/src/limit-heading-depth.test.ts +116 -0
  426. package/src/limit-heading-depth.ts +26 -0
  427. package/src/logger.ts +215 -0
  428. package/src/markdown.test.ts +315 -0
  429. package/src/markdown.ts +410 -0
  430. package/src/memory-overview-plugin.ts +163 -0
  431. package/src/message-finish-field.e2e.test.ts +195 -0
  432. package/src/message-formatting.test.ts +126 -0
  433. package/src/message-formatting.ts +535 -0
  434. package/src/message-preprocessing.ts +488 -0
  435. package/src/onboarding-tutorial.ts +167 -0
  436. package/src/onboarding-welcome.ts +49 -0
  437. package/src/openai-realtime.ts +358 -0
  438. package/src/opencode-command-detection.test.ts +307 -0
  439. package/src/opencode-command-detection.ts +76 -0
  440. package/src/opencode-command.test.ts +70 -0
  441. package/src/opencode-command.ts +191 -0
  442. package/src/opencode-interrupt-plugin.test.ts +682 -0
  443. package/src/opencode-interrupt-plugin.ts +507 -0
  444. package/src/opencode.ts +1462 -0
  445. package/src/otto/branding.ts +23 -0
  446. package/src/otto/index.ts +22 -0
  447. package/src/otto-digital-twin.e2e.test.ts +199 -0
  448. package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
  449. package/src/otto-opencode-plugin.test.ts +108 -0
  450. package/src/otto-opencode-plugin.ts +22 -0
  451. package/src/parse-permission-rules.test.ts +127 -0
  452. package/src/patch-text-parser.ts +107 -0
  453. package/src/plugin-logger.ts +84 -0
  454. package/src/privacy-sanitizer.ts +142 -0
  455. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  456. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  457. package/src/queue-advanced-e2e-setup.ts +877 -0
  458. package/src/queue-advanced-footer.e2e.test.ts +591 -0
  459. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  460. package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
  461. package/src/queue-advanced-question.e2e.test.ts +316 -0
  462. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  463. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  464. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  465. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  466. package/src/queue-question-select-drain.e2e.test.ts +327 -0
  467. package/src/runtime-idle-sweeper.ts +76 -0
  468. package/src/runtime-lifecycle.e2e.test.ts +651 -0
  469. package/src/schema.sql +174 -0
  470. package/src/sentry.ts +26 -0
  471. package/src/session-handler/agent-utils.ts +99 -0
  472. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  473. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  474. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  475. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  476. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  477. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  478. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  479. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  480. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  481. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  482. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  483. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  484. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  485. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  486. package/src/session-handler/event-stream-state.test.ts +717 -0
  487. package/src/session-handler/event-stream-state.ts +706 -0
  488. package/src/session-handler/model-utils.ts +217 -0
  489. package/src/session-handler/opencode-session-event-log.ts +130 -0
  490. package/src/session-handler/thread-runtime-state.ts +247 -0
  491. package/src/session-handler/thread-session-runtime.ts +4440 -0
  492. package/src/session-handler.ts +15 -0
  493. package/src/session-search.test.ts +50 -0
  494. package/src/session-search.ts +148 -0
  495. package/src/session-title-rename.test.ts +130 -0
  496. package/src/skill-filter.test.ts +83 -0
  497. package/src/skill-filter.ts +42 -0
  498. package/src/startup-service.ts +200 -0
  499. package/src/startup-time.e2e.test.ts +373 -0
  500. package/src/store.ts +139 -0
  501. package/src/subagent-rate-limit-plugin.ts +218 -0
  502. package/src/system-message.test.ts +710 -0
  503. package/src/system-message.ts +814 -0
  504. package/src/task-runner.ts +725 -0
  505. package/src/task-schedule.test.ts +84 -0
  506. package/src/task-schedule.ts +317 -0
  507. package/src/test-utils.ts +451 -0
  508. package/src/thinking-utils.ts +61 -0
  509. package/src/thread-message-queue.e2e.test.ts +1350 -0
  510. package/src/tools.ts +430 -0
  511. package/src/undici.d.ts +12 -0
  512. package/src/undo-redo.e2e.test.ts +209 -0
  513. package/src/unnest-code-blocks.test.ts +713 -0
  514. package/src/unnest-code-blocks.ts +185 -0
  515. package/src/upgrade.ts +185 -0
  516. package/src/utils.test.ts +155 -0
  517. package/src/utils.ts +265 -0
  518. package/src/voice-attachment.ts +51 -0
  519. package/src/voice-handler.ts +908 -0
  520. package/src/voice-message.e2e.test.ts +1255 -0
  521. package/src/voice.test.ts +281 -0
  522. package/src/voice.ts +638 -0
  523. package/src/wait-session.ts +273 -0
  524. package/src/websockify.ts +101 -0
  525. package/src/worker-types.ts +64 -0
  526. package/src/worktree-lifecycle.e2e.test.ts +396 -0
  527. package/src/worktree-utils.ts +4 -0
  528. package/src/worktrees.test.ts +489 -0
  529. package/src/worktrees.ts +1370 -0
  530. package/src/xml.test.ts +38 -0
  531. package/src/xml.ts +121 -0
  532. package/README.md +0 -142
  533. package/dist/cli.d.ts +0 -3
  534. package/dist/cli.d.ts.map +0 -1
  535. package/dist/cli.js.map +0 -1
  536. package/dist/config.d.ts +0 -39
  537. package/dist/config.d.ts.map +0 -1
  538. package/dist/config.js.map +0 -1
  539. package/dist/config.test.d.ts +0 -2
  540. package/dist/config.test.d.ts.map +0 -1
  541. package/dist/config.test.js +0 -202
  542. package/dist/config.test.js.map +0 -1
  543. package/dist/detect.d.ts +0 -9
  544. package/dist/detect.d.ts.map +0 -1
  545. package/dist/detect.js +0 -40
  546. package/dist/detect.js.map +0 -1
  547. package/dist/detect.test.d.ts +0 -2
  548. package/dist/detect.test.d.ts.map +0 -1
  549. package/dist/detect.test.js +0 -26
  550. package/dist/detect.test.js.map +0 -1
  551. package/dist/docker.d.ts +0 -7
  552. package/dist/docker.d.ts.map +0 -1
  553. package/dist/docker.js +0 -17
  554. package/dist/docker.js.map +0 -1
  555. package/dist/docker.test.d.ts +0 -2
  556. package/dist/docker.test.d.ts.map +0 -1
  557. package/dist/docker.test.js +0 -12
  558. package/dist/docker.test.js.map +0 -1
  559. package/dist/health.d.ts +0 -31
  560. package/dist/health.d.ts.map +0 -1
  561. package/dist/health.js +0 -117
  562. package/dist/health.js.map +0 -1
  563. package/dist/health.test.d.ts +0 -2
  564. package/dist/health.test.d.ts.map +0 -1
  565. package/dist/health.test.js +0 -52
  566. package/dist/health.test.js.map +0 -1
  567. package/dist/index.d.ts +0 -20
  568. package/dist/index.d.ts.map +0 -1
  569. package/dist/index.js +0 -15
  570. package/dist/index.js.map +0 -1
  571. package/dist/index.test.d.ts +0 -2
  572. package/dist/index.test.d.ts.map +0 -1
  573. package/dist/index.test.js +0 -8
  574. package/dist/index.test.js.map +0 -1
  575. package/dist/installer.d.ts +0 -10
  576. package/dist/installer.d.ts.map +0 -1
  577. package/dist/installer.js +0 -50
  578. package/dist/installer.js.map +0 -1
  579. package/dist/installer.test.d.ts +0 -2
  580. package/dist/installer.test.d.ts.map +0 -1
  581. package/dist/installer.test.js +0 -43
  582. package/dist/installer.test.js.map +0 -1
  583. package/dist/lifecycle.d.ts +0 -10
  584. package/dist/lifecycle.d.ts.map +0 -1
  585. package/dist/lifecycle.js +0 -45
  586. package/dist/lifecycle.js.map +0 -1
  587. package/dist/lifecycle.test.d.ts +0 -2
  588. package/dist/lifecycle.test.d.ts.map +0 -1
  589. package/dist/lifecycle.test.js +0 -20
  590. package/dist/lifecycle.test.js.map +0 -1
  591. package/dist/manifest.d.ts +0 -18
  592. package/dist/manifest.d.ts.map +0 -1
  593. package/dist/manifest.js +0 -30
  594. package/dist/manifest.js.map +0 -1
  595. package/dist/skills-baseline.d.ts +0 -7
  596. package/dist/skills-baseline.d.ts.map +0 -1
  597. package/dist/skills-baseline.js +0 -9
  598. package/dist/skills-baseline.js.map +0 -1
  599. package/dist/skills.d.ts +0 -110
  600. package/dist/skills.d.ts.map +0 -1
  601. package/dist/skills.js +0 -429
  602. package/dist/skills.js.map +0 -1
  603. package/dist/skills.test.d.ts +0 -2
  604. package/dist/skills.test.d.ts.map +0 -1
  605. package/dist/skills.test.js +0 -416
  606. package/dist/skills.test.js.map +0 -1
  607. package/dist/sync.d.ts +0 -10
  608. package/dist/sync.d.ts.map +0 -1
  609. package/dist/sync.js +0 -39
  610. package/dist/sync.js.map +0 -1
  611. package/dist/tenant.d.ts +0 -13
  612. package/dist/tenant.d.ts.map +0 -1
  613. package/dist/tenant.js +0 -105
  614. package/dist/tenant.js.map +0 -1
  615. package/dist/tenant.test.d.ts +0 -2
  616. package/dist/tenant.test.d.ts.map +0 -1
  617. package/dist/tenant.test.js +0 -37
  618. package/dist/tenant.test.js.map +0 -1
  619. package/src/config.test.ts +0 -237
  620. package/src/detect.test.ts +0 -29
  621. package/src/detect.ts +0 -52
  622. package/src/docker.test.ts +0 -12
  623. package/src/docker.ts +0 -23
  624. package/src/health.test.ts +0 -61
  625. package/src/health.ts +0 -158
  626. package/src/index.test.ts +0 -8
  627. package/src/index.ts +0 -62
  628. package/src/installer.test.ts +0 -52
  629. package/src/installer.ts +0 -62
  630. package/src/lifecycle.test.ts +0 -23
  631. package/src/lifecycle.ts +0 -49
  632. package/src/manifest.ts +0 -42
  633. package/src/skills-baseline.ts +0 -14
  634. package/src/skills.test.ts +0 -503
  635. package/src/skills.ts +0 -512
  636. package/src/sync.ts +0 -53
  637. package/src/tenant.test.ts +0 -49
  638. package/src/tenant.ts +0 -120
@@ -0,0 +1,455 @@
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
+
5
+ import {
6
+ StringSelectMenuBuilder,
7
+ StringSelectMenuInteraction,
8
+ ActionRowBuilder,
9
+ type ThreadChannel,
10
+ MessageFlags,
11
+ } from 'discord.js'
12
+ import crypto from 'node:crypto'
13
+ import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
14
+ import { getOpencodeClient } from '../opencode.js'
15
+ import { createLogger, LogPrefix } from '../logger.js'
16
+
17
+ const logger = createLogger(LogPrefix.ASK_QUESTION)
18
+
19
+ // Schema matching the question tool input
20
+ export type AskUserQuestionInput = {
21
+ questions: Array<{
22
+ question: string
23
+ header: string // max 12 chars
24
+ options: Array<{
25
+ label: string
26
+ description: string
27
+ }>
28
+ multiple?: boolean // optional, defaults to false
29
+ }>
30
+ }
31
+
32
+ export type CancelQuestionResult = 'no-pending' | 'replied' | 'reply-failed'
33
+
34
+ type PendingQuestionContext = {
35
+ sessionId: string
36
+ directory: string
37
+ thread: ThreadChannel
38
+ requestId: string // OpenCode question request ID for replying
39
+ questions: AskUserQuestionInput['questions']
40
+ answers: Record<number, string[]> // questionIndex -> selected labels
41
+ totalQuestions: number
42
+ answeredCount: number
43
+ contextHash: string
44
+
45
+ }
46
+
47
+ // Store pending question contexts by hash.
48
+ // TTL prevents unbounded growth if user never answers a question.
49
+ const QUESTION_CONTEXT_TTL_MS = 10 * 60 * 1000
50
+ export const pendingQuestionContexts = new Map<string, PendingQuestionContext>()
51
+
52
+ export function findPendingQuestionContextForRequest({
53
+ threadId,
54
+ requestId,
55
+ }: {
56
+ threadId: string
57
+ requestId: string
58
+ }): { contextHash: string; context: PendingQuestionContext } | null {
59
+ for (const [contextHash, context] of pendingQuestionContexts) {
60
+ if (context.thread.id !== threadId) {
61
+ continue
62
+ }
63
+ if (context.requestId !== requestId) {
64
+ continue
65
+ }
66
+ return { contextHash, context }
67
+ }
68
+ return null
69
+ }
70
+
71
+ export function deletePendingQuestionContextsForRequest({
72
+ threadId,
73
+ requestId,
74
+ }: {
75
+ threadId: string
76
+ requestId: string
77
+ }): number {
78
+ const matchingContextHashes = [...pendingQuestionContexts.entries()]
79
+ .filter(([, context]) => {
80
+ return context.thread.id === threadId && context.requestId === requestId
81
+ })
82
+ .map(([contextHash]) => {
83
+ return contextHash
84
+ })
85
+
86
+ matchingContextHashes.map((contextHash) => {
87
+ pendingQuestionContexts.delete(contextHash)
88
+ return contextHash
89
+ })
90
+
91
+ return matchingContextHashes.length
92
+ }
93
+
94
+ export function hasPendingQuestionForThread(threadId: string): boolean {
95
+ return [...pendingQuestionContexts.values()].some((ctx) => {
96
+ return ctx.thread.id === threadId
97
+ })
98
+ }
99
+
100
+ /**
101
+ * Show dropdown menus for question tool input.
102
+ * Sends one message per question with the dropdown directly under the question text.
103
+ */
104
+ export async function showAskUserQuestionDropdowns({
105
+ thread,
106
+ sessionId,
107
+ directory,
108
+ requestId,
109
+ input,
110
+ silent,
111
+ }: {
112
+ thread: ThreadChannel
113
+ sessionId: string
114
+ directory: string
115
+ requestId: string // OpenCode question request ID
116
+ input: AskUserQuestionInput
117
+ /** Suppress notification when queue has pending items */
118
+ silent?: boolean
119
+ }): Promise<void> {
120
+ const existingPending = findPendingQuestionContextForRequest({
121
+ threadId: thread.id,
122
+ requestId,
123
+ })
124
+ if (existingPending) {
125
+ logger.log(
126
+ `Deduped question ${requestId} for thread ${thread.id} (existing context ${existingPending.contextHash})`,
127
+ )
128
+ return
129
+ }
130
+
131
+ const contextHash = crypto.randomBytes(8).toString('hex')
132
+
133
+ const context: PendingQuestionContext = {
134
+ sessionId,
135
+ directory,
136
+ thread,
137
+ requestId,
138
+ questions: input.questions,
139
+ answers: {},
140
+ totalQuestions: input.questions.length,
141
+ answeredCount: 0,
142
+ contextHash,
143
+
144
+ }
145
+
146
+ pendingQuestionContexts.set(contextHash, context)
147
+ // On TTL expiry: hide the dropdown UI and abort the session so OpenCode
148
+ // unblocks. We intentionally do NOT call question.reply() — sending 'Other'
149
+ // made the model think the user chose an option when they didn't.
150
+ setTimeout(async () => {
151
+ const ctx = pendingQuestionContexts.get(contextHash)
152
+ if (!ctx) {
153
+ return
154
+ }
155
+ // Delete context first so the dropdown becomes inert immediately.
156
+ // Without this, a user clicking during the abort() await would still
157
+ // be accepted by handleAskQuestionSelectMenu, then abort() would
158
+ // kill that valid run.
159
+ deletePendingQuestionContextsForRequest({
160
+ threadId: ctx.thread.id,
161
+ requestId: ctx.requestId,
162
+ })
163
+ // Abort the session so OpenCode isn't stuck waiting for a reply
164
+ const client = getOpencodeClient(ctx.directory)
165
+ if (client) {
166
+ await client.session.abort({
167
+ sessionID: ctx.sessionId,
168
+ }).catch((error) => {
169
+ logger.error('Failed to abort session after question expiry:', error)
170
+ })
171
+ }
172
+ }, QUESTION_CONTEXT_TTL_MS).unref()
173
+
174
+ // Send one message per question with its dropdown directly underneath
175
+ for (let i = 0; i < input.questions.length; i++) {
176
+ const q = input.questions[i]!
177
+
178
+ // Map options to Discord select menu options
179
+ // Discord max: 25 options per select menu
180
+ const options = [
181
+ ...q.options.slice(0, 24).map((opt, optIdx) => ({
182
+ label: opt.label.slice(0, 100),
183
+ value: `${optIdx}`,
184
+ description: opt.description.slice(0, 100),
185
+ })),
186
+ {
187
+ label: 'Other',
188
+ value: 'other',
189
+ description: 'Provide a custom answer in chat',
190
+ },
191
+ ]
192
+
193
+ const placeholder =
194
+ options.find((x) => x.label)?.label || 'Select an option'
195
+ const selectMenu = new StringSelectMenuBuilder()
196
+ .setCustomId(`ask_question:${contextHash}:${i}`)
197
+ .setPlaceholder(placeholder)
198
+ .addOptions(options)
199
+
200
+ // Enable multi-select if the question supports it
201
+ if (q.multiple) {
202
+ selectMenu.setMinValues(1)
203
+ selectMenu.setMaxValues(options.length)
204
+ }
205
+
206
+ const actionRow =
207
+ new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
208
+
209
+ await thread.send({
210
+ content: `**${(q.header || '').slice(0, 200)}**\n${q.question.slice(0, 1700)}`,
211
+ components: [actionRow],
212
+ flags: silent ? SILENT_MESSAGE_FLAGS : NOTIFY_MESSAGE_FLAGS,
213
+ })
214
+ }
215
+
216
+ logger.log(
217
+ `Showed ${input.questions.length} question dropdown(s) for session ${sessionId}`,
218
+ )
219
+ }
220
+
221
+ /**
222
+ * Handle dropdown selection for AskUserQuestion.
223
+ */
224
+ export async function handleAskQuestionSelectMenu(
225
+ interaction: StringSelectMenuInteraction,
226
+ ): Promise<void> {
227
+ const customId = interaction.customId
228
+
229
+ if (!customId.startsWith('ask_question:')) {
230
+ return
231
+ }
232
+
233
+ const parts = customId.split(':')
234
+ const contextHash = parts[1]
235
+ const questionIndex = parseInt(parts[2]!, 10)
236
+
237
+ if (!contextHash) {
238
+ await interaction.reply({
239
+ content: 'Invalid selection.',
240
+ flags: MessageFlags.Ephemeral,
241
+ })
242
+ return
243
+ }
244
+
245
+ const context = pendingQuestionContexts.get(contextHash)
246
+
247
+ if (!context) {
248
+ await interaction.reply({
249
+ content: 'This question has expired. Please ask the AI again.',
250
+ flags: MessageFlags.Ephemeral,
251
+ })
252
+ return
253
+ }
254
+
255
+ await interaction.deferUpdate()
256
+
257
+ const selectedValues = interaction.values
258
+ const question = context.questions[questionIndex]
259
+
260
+ if (!question) {
261
+ logger.error(`Question index ${questionIndex} not found in context`)
262
+ return
263
+ }
264
+
265
+ // Check if "other" was selected
266
+ if (selectedValues.includes('other')) {
267
+ // User wants to provide custom answer
268
+ // For now, mark as "Other" - they can type in chat
269
+ context.answers[questionIndex] = ['Other (please type your answer in chat)']
270
+ } else {
271
+ // Map value indices back to option labels
272
+ context.answers[questionIndex] = selectedValues.map((v) => {
273
+ const optIdx = parseInt(v, 10)
274
+ return question.options[optIdx]?.label || `Option ${optIdx + 1}`
275
+ })
276
+ }
277
+
278
+ context.answeredCount++
279
+
280
+ // Update this question's message: show answer and remove dropdown
281
+ const answeredText = context.answers[questionIndex]!.join(', ')
282
+ await interaction.editReply({
283
+ content: `**${question.header}**\n${question.question}\n✓ _${answeredText}_`,
284
+ components: [], // Remove the dropdown
285
+ })
286
+
287
+ // Check if all questions are answered
288
+ if (context.answeredCount >= context.totalQuestions) {
289
+ // All questions answered - send result back to session
290
+ await submitQuestionAnswers(context)
291
+ deletePendingQuestionContextsForRequest({
292
+ threadId: context.thread.id,
293
+ requestId: context.requestId,
294
+ })
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Submit all collected answers back to the OpenCode session.
300
+ * Uses the question.reply API to provide answers to the waiting tool.
301
+ */
302
+ async function submitQuestionAnswers(
303
+ context: PendingQuestionContext,
304
+ ): Promise<void> {
305
+ try {
306
+ const client = getOpencodeClient(context.directory)
307
+ if (!client) {
308
+ throw new Error('OpenCode server not found for directory')
309
+ }
310
+
311
+ // Build answers array: each element is an array of selected labels for that question
312
+ const answers = context.questions.map((_, i) => {
313
+ return context.answers[i] || []
314
+ })
315
+
316
+ await client.question.reply({
317
+ requestID: context.requestId,
318
+ directory: context.directory,
319
+ answers,
320
+ })
321
+
322
+ logger.log(
323
+ `Submitted answers for question ${context.requestId} in session ${context.sessionId}`,
324
+ )
325
+ } catch (error) {
326
+ logger.error('Failed to submit answers:', error)
327
+ await sendThreadMessage(
328
+ context.thread,
329
+ `✗ Failed to submit answers: ${error instanceof Error ? error.message : 'Unknown error'}`,
330
+ )
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Check if a tool part is an AskUserQuestion tool.
336
+ * Returns the parsed input if valid, null otherwise.
337
+ */
338
+ export function parseAskUserQuestionTool(part: {
339
+ type: string
340
+ tool?: string
341
+ state?: { input?: unknown }
342
+ }): AskUserQuestionInput | null {
343
+ if (part.type !== 'tool') {
344
+ return null
345
+ }
346
+
347
+ // Check for the tool name (case-insensitive)
348
+ const toolName = part.tool?.toLowerCase()
349
+ if (toolName !== 'question') {
350
+ return null
351
+ }
352
+
353
+ const input = part.state?.input as AskUserQuestionInput | undefined
354
+
355
+ if (
356
+ !input?.questions ||
357
+ !Array.isArray(input.questions) ||
358
+ input.questions.length === 0
359
+ ) {
360
+ return null
361
+ }
362
+
363
+ // Validate structure
364
+ for (const q of input.questions) {
365
+ if (
366
+ typeof q.question !== 'string' ||
367
+ typeof q.header !== 'string' ||
368
+ !Array.isArray(q.options) ||
369
+ q.options.length < 2
370
+ ) {
371
+ return null
372
+ }
373
+ }
374
+
375
+ return input
376
+ }
377
+
378
+ /**
379
+ * Cancel a pending question for a thread.
380
+ *
381
+ * Two modes depending on whether `userMessage` is provided:
382
+ *
383
+ * - `cancelPendingQuestion(threadId)` — cleanup only. Removes the context
384
+ * without replying to OpenCode. Use when aborting the blocked session
385
+ * separately (e.g. voice/attachment messages whose content needs
386
+ * transcription first). Returns 'no-pending' in both "found+cleaned" and
387
+ * "nothing found" cases.
388
+ *
389
+ * - `cancelPendingQuestion(threadId, text)` — reply path. Sends the text as
390
+ * the tool answer so the model sees the user's response. The caller should
391
+ * NOT also enqueue the message as a new prompt.
392
+ * Returns 'replied' on success, 'reply-failed' if the reply call fails
393
+ * (context kept pending so TTL can retry).
394
+ */
395
+ export async function cancelPendingQuestion(
396
+ threadId: string,
397
+ userMessage?: string,
398
+ ): Promise<CancelQuestionResult> {
399
+ // Find pending question for this thread
400
+ let contextHash: string | undefined
401
+ let context: PendingQuestionContext | undefined
402
+ for (const [hash, ctx] of pendingQuestionContexts) {
403
+ if (ctx.thread.id === threadId) {
404
+ contextHash = hash
405
+ context = ctx
406
+ break
407
+ }
408
+ }
409
+
410
+ if (!contextHash || !context) {
411
+ return 'no-pending'
412
+ }
413
+
414
+ // undefined means teardown/cleanup — just remove context, don't reply.
415
+ // The session is already being torn down or the caller wants to dismiss
416
+ // the question without providing an answer (e.g. voice/attachment-only
417
+ // messages where content needs transcription before it can be an answer).
418
+ if (userMessage === undefined) {
419
+ deletePendingQuestionContextsForRequest({
420
+ threadId: context.thread.id,
421
+ requestId: context.requestId,
422
+ })
423
+ return 'no-pending'
424
+ }
425
+
426
+ try {
427
+ const client = getOpencodeClient(context.directory)
428
+ if (!client) {
429
+ throw new Error('OpenCode server not found for directory')
430
+ }
431
+
432
+ const answers = context.questions.map((_, i) => {
433
+ return context.answers[i] || [userMessage]
434
+ })
435
+
436
+ await client.question.reply({
437
+ requestID: context.requestId,
438
+ directory: context.directory,
439
+ answers,
440
+ })
441
+
442
+ logger.log(`Answered question ${context.requestId} with user message`)
443
+ } catch (error) {
444
+ logger.error('Failed to answer question:', error)
445
+ // Keep context pending so TTL can still fire.
446
+ // Caller should not consume the user message since reply failed.
447
+ return 'reply-failed'
448
+ }
449
+
450
+ deletePendingQuestionContextsForRequest({
451
+ threadId: context.thread.id,
452
+ requestId: context.requestId,
453
+ })
454
+ return 'replied'
455
+ }
@@ -0,0 +1,184 @@
1
+ // /btw command - Fork the current session with full context and send a new prompt.
2
+ // Unlike /fork, this does not replay past messages in Discord. It just creates
3
+ // a new thread, forks the entire session (no messageID), and immediately
4
+ // dispatches the user's prompt so the forked session starts working right away.
5
+
6
+ import {
7
+ ChannelType,
8
+ ThreadAutoArchiveDuration,
9
+ type ThreadChannel,
10
+ MessageFlags,
11
+ } from 'discord.js'
12
+ import { getThreadSession, setThreadSession } from '../database.js'
13
+ import {
14
+ resolveWorkingDirectory,
15
+ resolveTextChannel,
16
+ sendThreadMessage,
17
+ } from '../discord-utils.js'
18
+ import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js'
19
+ import { createLogger, LogPrefix } from '../logger.js'
20
+ import type { CommandContext } from './types.js'
21
+ import { initializeOpencodeForDirectory } from '../opencode.js'
22
+
23
+ const logger = createLogger(LogPrefix.FORK)
24
+
25
+ export async function forkSessionToBtwThread({
26
+ sourceThread,
27
+ projectDirectory,
28
+ prompt,
29
+ userId,
30
+ username,
31
+ appId,
32
+ }: {
33
+ sourceThread: ThreadChannel
34
+ projectDirectory: string
35
+ prompt: string
36
+ userId: string
37
+ username: string
38
+ appId: string | undefined
39
+ }): Promise<{ thread: ThreadChannel; forkedSessionId: string } | Error> {
40
+ const sessionId = await getThreadSession(sourceThread.id)
41
+ if (!sessionId) {
42
+ return new Error('No active session in this thread')
43
+ }
44
+
45
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
46
+ if (getClient instanceof Error) {
47
+ return new Error(`Failed to fork session: ${getClient.message}`, {
48
+ cause: getClient,
49
+ })
50
+ }
51
+
52
+ const forkResponse = await getClient().session.fork({
53
+ sessionID: sessionId,
54
+ })
55
+ if (!forkResponse.data) {
56
+ return new Error('Failed to fork session')
57
+ }
58
+
59
+ const textChannel = await resolveTextChannel(sourceThread)
60
+ if (!textChannel) {
61
+ return new Error('Could not resolve parent text channel')
62
+ }
63
+
64
+ const forkedSession = forkResponse.data
65
+ const thread = await textChannel.threads.create({
66
+ name: `btw: ${prompt}`.slice(0, 100),
67
+ autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
68
+ reason: `btw fork from session ${sessionId}`,
69
+ })
70
+
71
+ await setThreadSession(thread.id, forkedSession.id)
72
+ await thread.members.add(userId)
73
+
74
+ logger.log(
75
+ `Created btw fork session ${forkedSession.id} in thread ${thread.id} from ${sessionId}`,
76
+ )
77
+
78
+ const sourceThreadLink = `<#${sourceThread.id}>`
79
+ await sendThreadMessage(
80
+ thread,
81
+ `Reusing context from ${sourceThreadLink} to answer prompt...\n${prompt}`,
82
+ )
83
+
84
+ const wrappedPrompt = [
85
+ `The user asked a side question while you were working on another task.`,
86
+ `This is a forked session whose ONLY goal is to answer this question.`,
87
+ `Do NOT continue, resume, or reference the previous task. Only answer the question below.\n`,
88
+ prompt,
89
+ ].join('\n')
90
+
91
+ const runtime = getOrCreateRuntime({
92
+ threadId: thread.id,
93
+ thread,
94
+ projectDirectory,
95
+ sdkDirectory: projectDirectory,
96
+ channelId: sourceThread.parentId || sourceThread.id,
97
+ appId,
98
+ })
99
+ await runtime.enqueueIncoming({
100
+ prompt: wrappedPrompt,
101
+ userId,
102
+ username,
103
+ appId,
104
+ mode: 'opencode',
105
+ })
106
+
107
+ return {
108
+ thread,
109
+ forkedSessionId: forkedSession.id,
110
+ }
111
+ }
112
+
113
+ export async function handleBtwCommand({
114
+ command,
115
+ appId,
116
+ }: CommandContext): Promise<void> {
117
+ const channel = command.channel
118
+
119
+ if (!channel) {
120
+ await command.reply({
121
+ content: 'This command can only be used in a channel',
122
+ flags: MessageFlags.Ephemeral,
123
+ })
124
+ return
125
+ }
126
+
127
+ if (
128
+ channel.type !== ChannelType.PublicThread
129
+ && channel.type !== ChannelType.PrivateThread
130
+ && channel.type !== ChannelType.AnnouncementThread
131
+ ) {
132
+ await command.reply({
133
+ content:
134
+ 'This command can only be used in a thread with an active session',
135
+ flags: MessageFlags.Ephemeral,
136
+ })
137
+ return
138
+ }
139
+
140
+ const threadChannel = channel
141
+
142
+ const prompt = command.options.getString('prompt', true)
143
+
144
+ const resolved = await resolveWorkingDirectory({
145
+ channel: threadChannel,
146
+ })
147
+
148
+ if (!resolved) {
149
+ await command.reply({
150
+ content: 'Could not determine project directory for this channel',
151
+ flags: MessageFlags.Ephemeral,
152
+ })
153
+ return
154
+ }
155
+
156
+ const { projectDirectory } = resolved
157
+
158
+ await command.deferReply({ flags: MessageFlags.Ephemeral })
159
+
160
+ try {
161
+ const result = await forkSessionToBtwThread({
162
+ sourceThread: threadChannel,
163
+ projectDirectory,
164
+ prompt,
165
+ userId: command.user.id,
166
+ username: command.user.displayName,
167
+ appId,
168
+ })
169
+
170
+ if (result instanceof Error) {
171
+ await command.editReply(result.message)
172
+ return
173
+ }
174
+
175
+ await command.editReply(
176
+ `Session forked! Continue in ${result.thread.toString()}`,
177
+ )
178
+ } catch (error) {
179
+ logger.error('Error in /btw:', error)
180
+ await command.editReply(
181
+ `Failed to fork session: ${error instanceof Error ? error.message : 'Unknown error'}`,
182
+ )
183
+ }
184
+ }