@otto-assistant/bridge 0.4.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,390 @@
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 hasPendingQuestionForThread(threadId: string): boolean {
53
+ return [...pendingQuestionContexts.values()].some((ctx) => {
54
+ return ctx.thread.id === threadId
55
+ })
56
+ }
57
+
58
+ /**
59
+ * Show dropdown menus for question tool input.
60
+ * Sends one message per question with the dropdown directly under the question text.
61
+ */
62
+ export async function showAskUserQuestionDropdowns({
63
+ thread,
64
+ sessionId,
65
+ directory,
66
+ requestId,
67
+ input,
68
+ silent,
69
+ }: {
70
+ thread: ThreadChannel
71
+ sessionId: string
72
+ directory: string
73
+ requestId: string // OpenCode question request ID
74
+ input: AskUserQuestionInput
75
+ /** Suppress notification when queue has pending items */
76
+ silent?: boolean
77
+ }): Promise<void> {
78
+ const contextHash = crypto.randomBytes(8).toString('hex')
79
+
80
+ const context: PendingQuestionContext = {
81
+ sessionId,
82
+ directory,
83
+ thread,
84
+ requestId,
85
+ questions: input.questions,
86
+ answers: {},
87
+ totalQuestions: input.questions.length,
88
+ answeredCount: 0,
89
+ contextHash,
90
+
91
+ }
92
+
93
+ pendingQuestionContexts.set(contextHash, context)
94
+ // On TTL expiry: hide the dropdown UI and abort the session so OpenCode
95
+ // unblocks. We intentionally do NOT call question.reply() — sending 'Other'
96
+ // made the model think the user chose an option when they didn't.
97
+ setTimeout(async () => {
98
+ const ctx = pendingQuestionContexts.get(contextHash)
99
+ if (!ctx) {
100
+ return
101
+ }
102
+ // Delete context first so the dropdown becomes inert immediately.
103
+ // Without this, a user clicking during the abort() await would still
104
+ // be accepted by handleAskQuestionSelectMenu, then abort() would
105
+ // kill that valid run.
106
+ pendingQuestionContexts.delete(contextHash)
107
+ // Abort the session so OpenCode isn't stuck waiting for a reply
108
+ const client = getOpencodeClient(ctx.directory)
109
+ if (client) {
110
+ await client.session.abort({
111
+ sessionID: ctx.sessionId,
112
+ }).catch((error) => {
113
+ logger.error('Failed to abort session after question expiry:', error)
114
+ })
115
+ }
116
+ }, QUESTION_CONTEXT_TTL_MS).unref()
117
+
118
+ // Send one message per question with its dropdown directly underneath
119
+ for (let i = 0; i < input.questions.length; i++) {
120
+ const q = input.questions[i]!
121
+
122
+ // Map options to Discord select menu options
123
+ // Discord max: 25 options per select menu
124
+ const options = [
125
+ ...q.options.slice(0, 24).map((opt, optIdx) => ({
126
+ label: opt.label.slice(0, 100),
127
+ value: `${optIdx}`,
128
+ description: opt.description.slice(0, 100),
129
+ })),
130
+ {
131
+ label: 'Other',
132
+ value: 'other',
133
+ description: 'Provide a custom answer in chat',
134
+ },
135
+ ]
136
+
137
+ const placeholder =
138
+ options.find((x) => x.label)?.label || 'Select an option'
139
+ const selectMenu = new StringSelectMenuBuilder()
140
+ .setCustomId(`ask_question:${contextHash}:${i}`)
141
+ .setPlaceholder(placeholder)
142
+ .addOptions(options)
143
+
144
+ // Enable multi-select if the question supports it
145
+ if (q.multiple) {
146
+ selectMenu.setMinValues(1)
147
+ selectMenu.setMaxValues(options.length)
148
+ }
149
+
150
+ const actionRow =
151
+ new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
152
+
153
+ await thread.send({
154
+ content: `**${(q.header || '').slice(0, 200)}**\n${q.question.slice(0, 1700)}`,
155
+ components: [actionRow],
156
+ flags: silent ? SILENT_MESSAGE_FLAGS : NOTIFY_MESSAGE_FLAGS,
157
+ })
158
+ }
159
+
160
+ logger.log(
161
+ `Showed ${input.questions.length} question dropdown(s) for session ${sessionId}`,
162
+ )
163
+ }
164
+
165
+ /**
166
+ * Handle dropdown selection for AskUserQuestion.
167
+ */
168
+ export async function handleAskQuestionSelectMenu(
169
+ interaction: StringSelectMenuInteraction,
170
+ ): Promise<void> {
171
+ const customId = interaction.customId
172
+
173
+ if (!customId.startsWith('ask_question:')) {
174
+ return
175
+ }
176
+
177
+ const parts = customId.split(':')
178
+ const contextHash = parts[1]
179
+ const questionIndex = parseInt(parts[2]!, 10)
180
+
181
+ if (!contextHash) {
182
+ await interaction.reply({
183
+ content: 'Invalid selection.',
184
+ flags: MessageFlags.Ephemeral,
185
+ })
186
+ return
187
+ }
188
+
189
+ const context = pendingQuestionContexts.get(contextHash)
190
+
191
+ if (!context) {
192
+ await interaction.reply({
193
+ content: 'This question has expired. Please ask the AI again.',
194
+ flags: MessageFlags.Ephemeral,
195
+ })
196
+ return
197
+ }
198
+
199
+ await interaction.deferUpdate()
200
+
201
+ const selectedValues = interaction.values
202
+ const question = context.questions[questionIndex]
203
+
204
+ if (!question) {
205
+ logger.error(`Question index ${questionIndex} not found in context`)
206
+ return
207
+ }
208
+
209
+ // Check if "other" was selected
210
+ if (selectedValues.includes('other')) {
211
+ // User wants to provide custom answer
212
+ // For now, mark as "Other" - they can type in chat
213
+ context.answers[questionIndex] = ['Other (please type your answer in chat)']
214
+ } else {
215
+ // Map value indices back to option labels
216
+ context.answers[questionIndex] = selectedValues.map((v) => {
217
+ const optIdx = parseInt(v, 10)
218
+ return question.options[optIdx]?.label || `Option ${optIdx + 1}`
219
+ })
220
+ }
221
+
222
+ context.answeredCount++
223
+
224
+ // Update this question's message: show answer and remove dropdown
225
+ const answeredText = context.answers[questionIndex]!.join(', ')
226
+ await interaction.editReply({
227
+ content: `**${question.header}**\n${question.question}\n✓ _${answeredText}_`,
228
+ components: [], // Remove the dropdown
229
+ })
230
+
231
+ // Check if all questions are answered
232
+ if (context.answeredCount >= context.totalQuestions) {
233
+ // All questions answered - send result back to session
234
+ await submitQuestionAnswers(context)
235
+ pendingQuestionContexts.delete(contextHash)
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Submit all collected answers back to the OpenCode session.
241
+ * Uses the question.reply API to provide answers to the waiting tool.
242
+ */
243
+ async function submitQuestionAnswers(
244
+ context: PendingQuestionContext,
245
+ ): Promise<void> {
246
+ try {
247
+ const client = getOpencodeClient(context.directory)
248
+ if (!client) {
249
+ throw new Error('OpenCode server not found for directory')
250
+ }
251
+
252
+ // Build answers array: each element is an array of selected labels for that question
253
+ const answers = context.questions.map((_, i) => {
254
+ return context.answers[i] || []
255
+ })
256
+
257
+ await client.question.reply({
258
+ requestID: context.requestId,
259
+ directory: context.directory,
260
+ answers,
261
+ })
262
+
263
+ logger.log(
264
+ `Submitted answers for question ${context.requestId} in session ${context.sessionId}`,
265
+ )
266
+ } catch (error) {
267
+ logger.error('Failed to submit answers:', error)
268
+ await sendThreadMessage(
269
+ context.thread,
270
+ `✗ Failed to submit answers: ${error instanceof Error ? error.message : 'Unknown error'}`,
271
+ )
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Check if a tool part is an AskUserQuestion tool.
277
+ * Returns the parsed input if valid, null otherwise.
278
+ */
279
+ export function parseAskUserQuestionTool(part: {
280
+ type: string
281
+ tool?: string
282
+ state?: { input?: unknown }
283
+ }): AskUserQuestionInput | null {
284
+ if (part.type !== 'tool') {
285
+ return null
286
+ }
287
+
288
+ // Check for the tool name (case-insensitive)
289
+ const toolName = part.tool?.toLowerCase()
290
+ if (toolName !== 'question') {
291
+ return null
292
+ }
293
+
294
+ const input = part.state?.input as AskUserQuestionInput | undefined
295
+
296
+ if (
297
+ !input?.questions ||
298
+ !Array.isArray(input.questions) ||
299
+ input.questions.length === 0
300
+ ) {
301
+ return null
302
+ }
303
+
304
+ // Validate structure
305
+ for (const q of input.questions) {
306
+ if (
307
+ typeof q.question !== 'string' ||
308
+ typeof q.header !== 'string' ||
309
+ !Array.isArray(q.options) ||
310
+ q.options.length < 2
311
+ ) {
312
+ return null
313
+ }
314
+ }
315
+
316
+ return input
317
+ }
318
+
319
+ /**
320
+ * Cancel a pending question for a thread.
321
+ *
322
+ * Two modes depending on whether `userMessage` is provided:
323
+ *
324
+ * - `cancelPendingQuestion(threadId)` — cleanup only. Removes the context
325
+ * without replying to OpenCode. Use when aborting the blocked session
326
+ * separately (e.g. voice/attachment messages whose content needs
327
+ * transcription first). Returns 'no-pending' in both "found+cleaned" and
328
+ * "nothing found" cases.
329
+ *
330
+ * - `cancelPendingQuestion(threadId, text)` — reply path. Sends the text as
331
+ * the tool answer so the model sees the user's response. The caller should
332
+ * NOT also enqueue the message as a new prompt.
333
+ * Returns 'replied' on success, 'reply-failed' if the reply call fails
334
+ * (context kept pending so TTL can retry).
335
+ */
336
+ export async function cancelPendingQuestion(
337
+ threadId: string,
338
+ userMessage?: string,
339
+ ): Promise<CancelQuestionResult> {
340
+ // Find pending question for this thread
341
+ let contextHash: string | undefined
342
+ let context: PendingQuestionContext | undefined
343
+ for (const [hash, ctx] of pendingQuestionContexts) {
344
+ if (ctx.thread.id === threadId) {
345
+ contextHash = hash
346
+ context = ctx
347
+ break
348
+ }
349
+ }
350
+
351
+ if (!contextHash || !context) {
352
+ return 'no-pending'
353
+ }
354
+
355
+ // undefined means teardown/cleanup — just remove context, don't reply.
356
+ // The session is already being torn down or the caller wants to dismiss
357
+ // the question without providing an answer (e.g. voice/attachment-only
358
+ // messages where content needs transcription before it can be an answer).
359
+ if (userMessage === undefined) {
360
+ pendingQuestionContexts.delete(contextHash)
361
+ return 'no-pending'
362
+ }
363
+
364
+ try {
365
+ const client = getOpencodeClient(context.directory)
366
+ if (!client) {
367
+ throw new Error('OpenCode server not found for directory')
368
+ }
369
+
370
+ const answers = context.questions.map((_, i) => {
371
+ return context.answers[i] || [userMessage]
372
+ })
373
+
374
+ await client.question.reply({
375
+ requestID: context.requestId,
376
+ directory: context.directory,
377
+ answers,
378
+ })
379
+
380
+ logger.log(`Answered question ${context.requestId} with user message`)
381
+ } catch (error) {
382
+ logger.error('Failed to answer question:', error)
383
+ // Keep context pending so TTL can still fire.
384
+ // Caller should not consume the user message since reply failed.
385
+ return 'reply-failed'
386
+ }
387
+
388
+ pendingQuestionContexts.delete(contextHash)
389
+ return 'replied'
390
+ }
@@ -0,0 +1,164 @@
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 { initializeOpencodeForDirectory } from '../opencode.js'
14
+ import {
15
+ resolveWorkingDirectory,
16
+ resolveTextChannel,
17
+ sendThreadMessage,
18
+ } from '../discord-utils.js'
19
+ import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js'
20
+ import { createLogger, LogPrefix } from '../logger.js'
21
+ import type { CommandContext } from './types.js'
22
+
23
+ const logger = createLogger(LogPrefix.FORK)
24
+
25
+ export async function handleBtwCommand({
26
+ command,
27
+ appId,
28
+ }: CommandContext): Promise<void> {
29
+ const channel = command.channel
30
+
31
+ if (!channel) {
32
+ await command.reply({
33
+ content: 'This command can only be used in a channel',
34
+ flags: MessageFlags.Ephemeral,
35
+ })
36
+ return
37
+ }
38
+
39
+ const isThread = [
40
+ ChannelType.PublicThread,
41
+ ChannelType.PrivateThread,
42
+ ChannelType.AnnouncementThread,
43
+ ].includes(channel.type)
44
+
45
+ if (!isThread) {
46
+ await command.reply({
47
+ content:
48
+ 'This command can only be used in a thread with an active session',
49
+ flags: MessageFlags.Ephemeral,
50
+ })
51
+ return
52
+ }
53
+
54
+ const prompt = command.options.getString('prompt', true)
55
+
56
+ const resolved = await resolveWorkingDirectory({
57
+ channel: channel as ThreadChannel,
58
+ })
59
+
60
+ if (!resolved) {
61
+ await command.reply({
62
+ content: 'Could not determine project directory for this channel',
63
+ flags: MessageFlags.Ephemeral,
64
+ })
65
+ return
66
+ }
67
+
68
+ const { projectDirectory } = resolved
69
+
70
+ const sessionId = await getThreadSession(channel.id)
71
+
72
+ if (!sessionId) {
73
+ await command.reply({
74
+ content: 'No active session in this thread',
75
+ flags: MessageFlags.Ephemeral,
76
+ })
77
+ return
78
+ }
79
+
80
+ await command.deferReply({ flags: MessageFlags.Ephemeral })
81
+
82
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
83
+ if (getClient instanceof Error) {
84
+ await command.editReply({
85
+ content: `Failed to fork session: ${getClient.message}`,
86
+ })
87
+ return
88
+ }
89
+
90
+ try {
91
+ // Fork the entire session (no messageID = fork at the latest point)
92
+ const forkResponse = await getClient().session.fork({
93
+ sessionID: sessionId,
94
+ })
95
+
96
+ if (!forkResponse.data) {
97
+ await command.editReply('Failed to fork session')
98
+ return
99
+ }
100
+
101
+ const forkedSession = forkResponse.data
102
+
103
+ const textChannel = await resolveTextChannel(channel as ThreadChannel)
104
+ if (!textChannel) {
105
+ await command.editReply('Could not resolve parent text channel')
106
+ return
107
+ }
108
+
109
+ const threadName = `btw: ${prompt}`.slice(0, 100)
110
+ const thread = await textChannel.threads.create({
111
+ name: threadName,
112
+ autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
113
+ reason: `btw fork from session ${sessionId}`,
114
+ })
115
+
116
+ // Claim the forked session immediately so external polling does not race
117
+ await setThreadSession(thread.id, forkedSession.id)
118
+
119
+ await thread.members.add(command.user.id)
120
+
121
+ logger.log(
122
+ `Created btw fork session ${forkedSession.id} in thread ${thread.id} from ${sessionId}`,
123
+ )
124
+
125
+ // Short status message with prompt instead of replaying past messages
126
+ const sourceThreadLink = `<#${channel.id}>`
127
+ await sendThreadMessage(
128
+ thread,
129
+ `Reusing context from ${sourceThreadLink} to answer prompt...\n${prompt}`,
130
+ )
131
+
132
+ const wrappedPrompt = [
133
+ `The user asked a side question while you were working on another task.`,
134
+ `This is a forked session whose ONLY goal is to answer this question.`,
135
+ `Do NOT continue, resume, or reference the previous task. Only answer the question below.\n`,
136
+ prompt,
137
+ ].join('\n')
138
+
139
+ const runtime = getOrCreateRuntime({
140
+ threadId: thread.id,
141
+ thread,
142
+ projectDirectory,
143
+ sdkDirectory: projectDirectory,
144
+ channelId: textChannel.id,
145
+ appId,
146
+ })
147
+ await runtime.enqueueIncoming({
148
+ prompt: wrappedPrompt,
149
+ userId: command.user.id,
150
+ username: command.user.displayName,
151
+ appId,
152
+ mode: 'opencode',
153
+ })
154
+
155
+ await command.editReply(
156
+ `Session forked! Continue in ${thread.toString()}`,
157
+ )
158
+ } catch (error) {
159
+ logger.error('Error in /btw:', error)
160
+ await command.editReply(
161
+ `Failed to fork session: ${error instanceof Error ? error.message : 'Unknown error'}`,
162
+ )
163
+ }
164
+ }
@@ -0,0 +1,157 @@
1
+ // /compact command - Trigger context compaction (summarization) for the current session.
2
+
3
+ import {
4
+ ChannelType,
5
+ MessageFlags,
6
+ type TextChannel,
7
+ type ThreadChannel,
8
+ } from 'discord.js'
9
+ import type { CommandContext } from './types.js'
10
+ import { getThreadSession } from '../database.js'
11
+ import {
12
+ initializeOpencodeForDirectory,
13
+ getOpencodeClient,
14
+ } from '../opencode.js'
15
+ import {
16
+ resolveWorkingDirectory,
17
+ SILENT_MESSAGE_FLAGS,
18
+ } from '../discord-utils.js'
19
+ import { createLogger, LogPrefix } from '../logger.js'
20
+
21
+ const logger = createLogger(LogPrefix.COMPACT)
22
+
23
+ export async function handleCompactCommand({
24
+ command,
25
+ }: CommandContext): Promise<void> {
26
+ const channel = command.channel
27
+
28
+ if (!channel) {
29
+ await command.reply({
30
+ content: 'This command can only be used in a channel',
31
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
32
+ })
33
+ return
34
+ }
35
+
36
+ const isThread = [
37
+ ChannelType.PublicThread,
38
+ ChannelType.PrivateThread,
39
+ ChannelType.AnnouncementThread,
40
+ ].includes(channel.type)
41
+
42
+ if (!isThread) {
43
+ await command.reply({
44
+ content:
45
+ 'This command can only be used in a thread with an active session',
46
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
47
+ })
48
+ return
49
+ }
50
+
51
+ const resolved = await resolveWorkingDirectory({
52
+ channel: channel as TextChannel | ThreadChannel,
53
+ })
54
+
55
+ if (!resolved) {
56
+ await command.reply({
57
+ content: 'Could not determine project directory for this channel',
58
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
59
+ })
60
+ return
61
+ }
62
+
63
+ const { projectDirectory, workingDirectory } = resolved
64
+
65
+ const sessionId = await getThreadSession(channel.id)
66
+
67
+ if (!sessionId) {
68
+ await command.reply({
69
+ content: 'No active session in this thread',
70
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
71
+ })
72
+ return
73
+ }
74
+
75
+ // Ensure server is running for the base project directory
76
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
77
+ if (getClient instanceof Error) {
78
+ await command.reply({
79
+ content: `Failed to compact: ${getClient.message}`,
80
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
81
+ })
82
+ return
83
+ }
84
+
85
+ const client = getOpencodeClient(projectDirectory)
86
+ if (!client) {
87
+ await command.reply({
88
+ content: 'Failed to get OpenCode client',
89
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
90
+ })
91
+ return
92
+ }
93
+
94
+ // Defer reply since compaction may take a moment
95
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
96
+
97
+ try {
98
+ // Get session messages to find the model from the last user message
99
+ const messagesResult = await client.session.messages({
100
+ sessionID: sessionId,
101
+ directory: workingDirectory,
102
+ })
103
+
104
+ if (messagesResult.error || !messagesResult.data) {
105
+ logger.error('[COMPACT] Failed to get messages:', messagesResult.error)
106
+ await command.editReply({
107
+ content: 'Failed to compact: Could not retrieve session messages',
108
+ })
109
+ return
110
+ }
111
+
112
+ // Find the last user message to get the model
113
+ const lastUserMessage = [...messagesResult.data]
114
+ .reverse()
115
+ .find((msg) => msg.info.role === 'user')
116
+
117
+ if (!lastUserMessage || lastUserMessage.info.role !== 'user') {
118
+ await command.editReply({
119
+ content: 'Failed to compact: No user message found in session',
120
+ })
121
+ return
122
+ }
123
+
124
+ const { providerID, modelID } = lastUserMessage.info.model
125
+
126
+ const result = await client.session.summarize({
127
+ sessionID: sessionId,
128
+ directory: workingDirectory,
129
+ providerID,
130
+ modelID,
131
+ auto: false,
132
+ })
133
+
134
+ if (result.error) {
135
+ logger.error('[COMPACT] Error:', result.error)
136
+ const errorMessage =
137
+ 'data' in result.error && result.error.data
138
+ ? (result.error.data as { message?: string }).message ||
139
+ 'Unknown error'
140
+ : 'Unknown error'
141
+ await command.editReply({
142
+ content: `Failed to compact: ${errorMessage}`,
143
+ })
144
+ return
145
+ }
146
+
147
+ await command.editReply({
148
+ content: `📦 Session **compacted** successfully`,
149
+ })
150
+ logger.log(`Session ${sessionId} compacted by user`)
151
+ } catch (error) {
152
+ logger.error('[COMPACT] Error:', error)
153
+ await command.editReply({
154
+ content: `Failed to compact: ${error instanceof Error ? error.message : 'Unknown error'}`,
155
+ })
156
+ }
157
+ }