@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,397 @@
1
+ // Permission button handler - Shows buttons for permission requests.
2
+ // When OpenCode asks for permission, this module renders 3 buttons:
3
+ // Accept, Accept Always, and Deny.
4
+
5
+ import {
6
+ ButtonBuilder,
7
+ ButtonStyle,
8
+ type ButtonInteraction,
9
+ ActionRowBuilder,
10
+ type ThreadChannel,
11
+ MessageFlags,
12
+ } from 'discord.js'
13
+ import crypto from 'node:crypto'
14
+ import type { PermissionRequest } from '@opencode-ai/sdk/v2'
15
+ import { getOpencodeClient } from '../opencode.js'
16
+ import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js'
17
+ import { createLogger, LogPrefix } from '../logger.js'
18
+
19
+ const logger = createLogger(LogPrefix.PERMISSIONS)
20
+
21
+ function wildcardMatch({
22
+ value,
23
+ pattern,
24
+ }: {
25
+ value: string
26
+ pattern: string
27
+ }): boolean {
28
+ let escapedPattern = pattern
29
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
30
+ .replace(/\*/g, '.*')
31
+ .replace(/\?/g, '.')
32
+
33
+ if (escapedPattern.endsWith(' .*')) {
34
+ escapedPattern = escapedPattern.slice(0, -3) + '( .*)?'
35
+ }
36
+
37
+ return new RegExp(`^${escapedPattern}$`, 's').test(value)
38
+ }
39
+
40
+ export function arePatternsCoveredBy({
41
+ patterns,
42
+ coveringPatterns,
43
+ }: {
44
+ patterns: string[]
45
+ coveringPatterns: string[]
46
+ }): boolean {
47
+ return patterns.every((pattern) => {
48
+ return coveringPatterns.some((coveringPattern) => {
49
+ return wildcardMatch({ value: pattern, pattern: coveringPattern })
50
+ })
51
+ })
52
+ }
53
+
54
+ export function compactPermissionPatterns(patterns: string[]): string[] {
55
+ const uniquePatterns = Array.from(new Set(patterns))
56
+ return uniquePatterns.filter((pattern, index) => {
57
+ return !uniquePatterns.some((candidate, candidateIndex) => {
58
+ if (candidateIndex === index) {
59
+ return false
60
+ }
61
+ return wildcardMatch({ value: pattern, pattern: candidate })
62
+ })
63
+ })
64
+ }
65
+
66
+ type PendingPermissionContext = {
67
+ permission: PermissionRequest
68
+ requestIds: string[]
69
+ directory: string
70
+ permissionDirectory: string
71
+ thread: ThreadChannel
72
+ contextHash: string
73
+ messageId?: string
74
+ }
75
+
76
+ // Store pending permission contexts by hash.
77
+ // TTL prevents unbounded growth if user never clicks a permission button.
78
+ const PERMISSION_CONTEXT_TTL_MS = 10 * 60 * 1000
79
+ export const pendingPermissionContexts = new Map<
80
+ string,
81
+ PendingPermissionContext
82
+ >()
83
+
84
+ // Atomic take: removes context from Map and returns it. Only the first caller
85
+ // (TTL expiry or button click) wins, preventing duplicate permission replies.
86
+ function takePendingPermissionContext(contextHash: string): PendingPermissionContext | undefined {
87
+ const ctx = pendingPermissionContexts.get(contextHash)
88
+ if (!ctx) {
89
+ return undefined
90
+ }
91
+ pendingPermissionContexts.delete(contextHash)
92
+ return ctx
93
+ }
94
+
95
+ /**
96
+ * Show permission buttons for a permission request.
97
+ * Displays 3 buttons in a row: Accept, Accept Always, Deny.
98
+ * Returns the message ID and context hash for tracking.
99
+ */
100
+ export async function showPermissionButtons({
101
+ thread,
102
+ permission,
103
+ directory,
104
+ permissionDirectory,
105
+ subtaskLabel,
106
+ }: {
107
+ thread: ThreadChannel
108
+ permission: PermissionRequest
109
+ directory: string
110
+ permissionDirectory: string
111
+ subtaskLabel?: string
112
+ }): Promise<{ messageId: string; contextHash: string }> {
113
+ const contextHash = crypto.randomBytes(8).toString('hex')
114
+
115
+ const context: PendingPermissionContext = {
116
+ permission,
117
+ requestIds: [permission.id],
118
+ directory,
119
+ permissionDirectory,
120
+ thread,
121
+ contextHash,
122
+ }
123
+
124
+ pendingPermissionContexts.set(contextHash, context)
125
+ // Auto-reject on TTL expiry so the OpenCode session doesn't hang forever
126
+ // waiting for a permission reply that will never come. Uses atomic take
127
+ // so only one of TTL-expiry or button-click can win.
128
+ setTimeout(async () => {
129
+ const ctx = takePendingPermissionContext(contextHash)
130
+ if (!ctx) {
131
+ return
132
+ }
133
+ const client = getOpencodeClient(ctx.directory)
134
+ if (client) {
135
+ const requestIds = ctx.requestIds.length > 0
136
+ ? ctx.requestIds
137
+ : [ctx.permission.id]
138
+ await Promise.all(
139
+ requestIds.map((requestId) => {
140
+ return client.permission.reply({
141
+ requestID: requestId,
142
+ directory: ctx.permissionDirectory,
143
+ reply: 'reject',
144
+ })
145
+ }),
146
+ ).catch((error) => {
147
+ logger.error('Failed to auto-reject expired permission:', error)
148
+ })
149
+ }
150
+ }, PERMISSION_CONTEXT_TTL_MS).unref()
151
+
152
+ const patternStr = compactPermissionPatterns(permission.patterns).join(', ')
153
+
154
+ // Build 3 buttons for permission actions
155
+ const acceptButton = new ButtonBuilder()
156
+ .setCustomId(`permission_once:${contextHash}`)
157
+ .setLabel('Accept')
158
+ .setStyle(ButtonStyle.Success)
159
+
160
+ const acceptAlwaysButton = new ButtonBuilder()
161
+ .setCustomId(`permission_always:${contextHash}`)
162
+ .setLabel('Accept Always')
163
+ .setStyle(ButtonStyle.Success)
164
+
165
+ const denyButton = new ButtonBuilder()
166
+ .setCustomId(`permission_reject:${contextHash}`)
167
+ .setLabel('Deny')
168
+ .setStyle(ButtonStyle.Secondary)
169
+
170
+ const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
171
+ acceptButton,
172
+ acceptAlwaysButton,
173
+ denyButton,
174
+ )
175
+
176
+ const subtaskLine = subtaskLabel ? `**From:** \`${subtaskLabel}\`\n` : ''
177
+ const externalDirLine =
178
+ permission.permission === 'external_directory'
179
+ ? `Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n`
180
+ : ''
181
+ const fullContent =
182
+ `⚠️ **Permission Required**\n` +
183
+ subtaskLine +
184
+ `**Type:** \`${permission.permission}\`\n` +
185
+ externalDirLine +
186
+ (patternStr ? `**Pattern:** \`${patternStr}\`` : '')
187
+ const permissionMessage = await thread.send({
188
+ content: fullContent.slice(0, 1900),
189
+ components: [actionRow],
190
+ flags: NOTIFY_MESSAGE_FLAGS | MessageFlags.SuppressEmbeds,
191
+ })
192
+
193
+ context.messageId = permissionMessage.id
194
+
195
+ logger.log(`Showed permission buttons for ${permission.id}`)
196
+
197
+ return { messageId: permissionMessage.id, contextHash }
198
+ }
199
+
200
+ function updatePermissionMessage({
201
+ context,
202
+ status,
203
+ }: {
204
+ context: PendingPermissionContext
205
+ status: string
206
+ }): void {
207
+ if (!context.messageId) {
208
+ return
209
+ }
210
+ context.thread.messages
211
+ .fetch(context.messageId)
212
+ .then((message) => {
213
+ const patternStr = compactPermissionPatterns(context.permission.patterns).join(', ')
214
+ const externalDirLine =
215
+ context.permission.permission === 'external_directory'
216
+ ? 'Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n'
217
+ : ''
218
+ return message.edit({
219
+ content:
220
+ `⚠️ **Permission Required**\n` +
221
+ `**Type:** \`${context.permission.permission}\`\n` +
222
+ externalDirLine +
223
+ (patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
224
+ status,
225
+ components: [],
226
+ })
227
+ })
228
+ .catch((error) => {
229
+ logger.error('Failed to update permission message:', error)
230
+ })
231
+ }
232
+
233
+ export async function cancelPendingPermission(threadId: string): Promise<boolean> {
234
+ const contexts = Array.from(pendingPermissionContexts.values()).filter((context) => {
235
+ return context.thread.id === threadId
236
+ })
237
+
238
+ if (contexts.length === 0) {
239
+ return false
240
+ }
241
+
242
+ let cancelledCount = 0
243
+ for (const context of contexts) {
244
+ const pendingContext = takePendingPermissionContext(context.contextHash)
245
+ if (!pendingContext) {
246
+ continue
247
+ }
248
+
249
+ const client = getOpencodeClient(pendingContext.directory)
250
+ if (!client) {
251
+ pendingPermissionContexts.set(pendingContext.contextHash, pendingContext)
252
+ logger.error('Failed to dismiss pending permission: OpenCode server not found')
253
+ continue
254
+ }
255
+
256
+ const requestIds = pendingContext.requestIds.length > 0
257
+ ? pendingContext.requestIds
258
+ : [pendingContext.permission.id]
259
+
260
+ const result = await Promise.all(
261
+ requestIds.map((requestId) => {
262
+ return client.permission.reply({
263
+ requestID: requestId,
264
+ directory: pendingContext.permissionDirectory,
265
+ reply: 'reject',
266
+ })
267
+ }),
268
+ ).then(() => {
269
+ return 'ok' as const
270
+ }).catch((error) => {
271
+ pendingPermissionContexts.set(pendingContext.contextHash, pendingContext)
272
+ logger.error('Failed to dismiss pending permission:', error)
273
+ return 'error' as const
274
+ })
275
+
276
+ if (result === 'error') {
277
+ continue
278
+ }
279
+
280
+ updatePermissionMessage({
281
+ context: pendingContext,
282
+ status: '_Permission dismissed - user sent a new message._',
283
+ })
284
+ cancelledCount++
285
+ }
286
+
287
+ if (cancelledCount > 0) {
288
+ logger.log(`Dismissed ${cancelledCount} pending permission request(s) for thread ${threadId}`)
289
+ }
290
+
291
+ return cancelledCount > 0
292
+ }
293
+
294
+ /**
295
+ * Handle button click for permission.
296
+ */
297
+ export async function handlePermissionButton(
298
+ interaction: ButtonInteraction,
299
+ ): Promise<void> {
300
+ const customId = interaction.customId
301
+
302
+ // Extract action and hash from customId (e.g., "permission_once:abc123")
303
+ const [actionPart, contextHash] = customId.split(':')
304
+ if (!actionPart || !contextHash) {
305
+ return
306
+ }
307
+
308
+ const response = actionPart.replace('permission_', '') as
309
+ | 'once'
310
+ | 'always'
311
+ | 'reject'
312
+
313
+ // Atomic take: if TTL already expired and auto-rejected, context is gone.
314
+ const context = takePendingPermissionContext(contextHash)
315
+
316
+ if (!context) {
317
+ await interaction.update({ components: [] })
318
+ return
319
+ }
320
+
321
+ await interaction.deferUpdate()
322
+
323
+ try {
324
+ const permClient = getOpencodeClient(context.directory)
325
+ if (!permClient) {
326
+ throw new Error('OpenCode server not found for directory')
327
+ }
328
+ const requestIds =
329
+ context.requestIds.length > 0
330
+ ? context.requestIds
331
+ : [context.permission.id]
332
+ await Promise.all(
333
+ requestIds.map((requestId) => {
334
+ return permClient.permission.reply({
335
+ requestID: requestId,
336
+ directory: context.permissionDirectory,
337
+ reply: response,
338
+ })
339
+ }),
340
+ )
341
+
342
+ // Context already removed by takePendingPermissionContext above.
343
+
344
+ // Update message: show result and remove dropdown
345
+ const resultText = (() => {
346
+ switch (response) {
347
+ case 'once':
348
+ return '✅ Permission **accepted**'
349
+ case 'always':
350
+ return '✅ Permission **accepted** (auto-approve similar requests)'
351
+ case 'reject':
352
+ return '❌ Permission **rejected**'
353
+ }
354
+ })()
355
+
356
+ updatePermissionMessage({
357
+ context,
358
+ status: resultText,
359
+ })
360
+
361
+ logger.log(
362
+ `Permission ${context.permission.id} ${response} (${requestIds.length} request(s))`,
363
+ )
364
+ } catch (error) {
365
+ logger.error('Error handling permission:', error)
366
+ await interaction.editReply({
367
+ content: `Failed to process permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
368
+ components: [],
369
+ })
370
+ }
371
+ }
372
+
373
+ export function addPermissionRequestToContext({
374
+ contextHash,
375
+ requestId,
376
+ }: {
377
+ contextHash: string
378
+ requestId: string
379
+ }): boolean {
380
+ const context = pendingPermissionContexts.get(contextHash)
381
+ if (!context) {
382
+ return false
383
+ }
384
+ if (context.requestIds.includes(requestId)) {
385
+ return false
386
+ }
387
+ context.requestIds = [...context.requestIds, requestId]
388
+ pendingPermissionContexts.set(contextHash, context)
389
+ return true
390
+ }
391
+
392
+ /**
393
+ * Clean up a pending permission context (e.g., on auto-reject).
394
+ */
395
+ export function cleanupPermissionContext(contextHash: string): void {
396
+ pendingPermissionContexts.delete(contextHash)
397
+ }
@@ -0,0 +1,271 @@
1
+ // Queue commands - /queue, /queue-command, /clear-queue
2
+
3
+ import { ChannelType, MessageFlags, type ThreadChannel } from 'discord.js'
4
+ import type { AutocompleteContext, CommandContext } from './types.js'
5
+ import { getThreadSession } from '../database.js'
6
+ import {
7
+ resolveWorkingDirectory,
8
+ sendThreadMessage,
9
+ SILENT_MESSAGE_FLAGS,
10
+ } from '../discord-utils.js'
11
+ import {
12
+ getRuntime,
13
+ getOrCreateRuntime,
14
+ } from '../session-handler/thread-session-runtime.js'
15
+ import { createLogger, LogPrefix } from '../logger.js'
16
+ import { notifyError } from '../sentry.js'
17
+ import { store } from '../store.js'
18
+
19
+ const logger = createLogger(LogPrefix.QUEUE)
20
+
21
+ export async function handleQueueCommand({
22
+ command,
23
+ appId,
24
+ }: CommandContext): Promise<void> {
25
+ const message = command.options.getString('message', true)
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 thread = channel as ThreadChannel
52
+ const sessionId = await getThreadSession(thread.id)
53
+ if (!sessionId) {
54
+ await command.reply({
55
+ content:
56
+ 'No active session in this thread. Send a message directly instead.',
57
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
58
+ })
59
+ return
60
+ }
61
+
62
+ const resolved = await resolveWorkingDirectory({ channel: thread })
63
+ if (!resolved) {
64
+ await command.reply({
65
+ content: 'Could not determine project directory',
66
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
67
+ })
68
+ return
69
+ }
70
+
71
+ const runtime = getOrCreateRuntime({
72
+ threadId: thread.id,
73
+ thread,
74
+ projectDirectory: resolved.projectDirectory,
75
+ sdkDirectory: resolved.workingDirectory,
76
+ channelId: thread.parentId || thread.id,
77
+ appId,
78
+ })
79
+
80
+ // /queue explicitly uses kimaki local queue mode.
81
+ const enqueueResult = await runtime.enqueueIncoming({
82
+ prompt: message,
83
+ userId: command.user.id,
84
+ username: command.user.displayName,
85
+ appId,
86
+ mode: 'local-queue',
87
+ })
88
+
89
+ const responseText = enqueueResult.queued
90
+ ? `Queued message${enqueueResult.position ? ` (position ${enqueueResult.position})` : ''}`
91
+ : `» **${command.user.displayName}:** ${message.slice(0, 1000)}${message.length > 1000 ? '...' : ''}`
92
+
93
+ await command.reply({
94
+ content: responseText,
95
+ flags: SILENT_MESSAGE_FLAGS,
96
+ })
97
+ }
98
+
99
+ export async function handleClearQueueCommand({
100
+ command,
101
+ }: CommandContext): Promise<void> {
102
+ const channel = command.channel
103
+
104
+ if (!channel) {
105
+ await command.reply({
106
+ content: 'This command can only be used in a channel',
107
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
108
+ })
109
+ return
110
+ }
111
+
112
+ const isThread = [
113
+ ChannelType.PublicThread,
114
+ ChannelType.PrivateThread,
115
+ ChannelType.AnnouncementThread,
116
+ ].includes(channel.type)
117
+
118
+ if (!isThread) {
119
+ await command.reply({
120
+ content: 'This command can only be used in a thread',
121
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
122
+ })
123
+ return
124
+ }
125
+
126
+ const runtime = getRuntime(channel.id)
127
+ const queueLength = runtime?.getQueueLength() ?? 0
128
+
129
+ if (queueLength === 0) {
130
+ await command.reply({
131
+ content: 'No messages in queue',
132
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
133
+ })
134
+ return
135
+ }
136
+
137
+ runtime?.clearQueue()
138
+
139
+ await command.reply({
140
+ content: `Cleared ${queueLength} queued message${queueLength > 1 ? 's' : ''}`,
141
+ flags: SILENT_MESSAGE_FLAGS,
142
+ })
143
+
144
+ logger.log(
145
+ `[QUEUE] User ${command.user.displayName} cleared queue in thread ${channel.id}`,
146
+ )
147
+ }
148
+
149
+ export async function handleQueueCommandCommand({
150
+ command,
151
+ appId,
152
+ }: CommandContext): Promise<void> {
153
+ const commandName = command.options.getString('command', true)
154
+ const args = command.options.getString('arguments') || ''
155
+ const channel = command.channel
156
+
157
+ if (!channel) {
158
+ await command.reply({
159
+ content: 'This command can only be used in a channel',
160
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
161
+ })
162
+ return
163
+ }
164
+
165
+ const isThread = [
166
+ ChannelType.PublicThread,
167
+ ChannelType.PrivateThread,
168
+ ChannelType.AnnouncementThread,
169
+ ].includes(channel.type)
170
+
171
+ if (!isThread) {
172
+ await command.reply({
173
+ content:
174
+ 'This command can only be used in a thread with an active session',
175
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
176
+ })
177
+ return
178
+ }
179
+
180
+ const sessionId = await getThreadSession(channel.id)
181
+
182
+ if (!sessionId) {
183
+ await command.reply({
184
+ content:
185
+ 'No active session in this thread. Send a message directly instead.',
186
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
187
+ })
188
+ return
189
+ }
190
+
191
+ // Validate command exists in registered user commands
192
+ const isKnownCommand = store.getState().registeredUserCommands.some((cmd) => {
193
+ return cmd.name === commandName
194
+ })
195
+ if (!isKnownCommand) {
196
+ await command.reply({
197
+ content: `Unknown command: /${commandName}. Use autocomplete to pick from available commands.`,
198
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
199
+ })
200
+ return
201
+ }
202
+
203
+ const commandPayload = { name: commandName, arguments: args }
204
+ const displayText = `/${commandName}`
205
+ const thread = channel as ThreadChannel
206
+
207
+ const resolved = await resolveWorkingDirectory({ channel: thread })
208
+ if (!resolved) {
209
+ await command.reply({
210
+ content: 'Could not determine project directory',
211
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
212
+ })
213
+ return
214
+ }
215
+
216
+ const runtime = getOrCreateRuntime({
217
+ threadId: thread.id,
218
+ thread,
219
+ projectDirectory: resolved.projectDirectory,
220
+ sdkDirectory: resolved.workingDirectory,
221
+ channelId: thread.parentId || thread.id,
222
+ appId,
223
+ })
224
+
225
+ // /queue-command explicitly uses kimaki local queue mode.
226
+ const enqueueResult = await runtime.enqueueIncoming({
227
+ prompt: '',
228
+ userId: command.user.id,
229
+ username: command.user.displayName,
230
+ appId,
231
+ command: commandPayload,
232
+ mode: 'local-queue',
233
+ })
234
+
235
+ const responseText = enqueueResult.queued
236
+ ? `Queued message${enqueueResult.position ? ` (position ${enqueueResult.position})` : ''}`
237
+ : `» **${command.user.displayName}:** ${displayText}`
238
+
239
+ await command.reply({
240
+ content: responseText,
241
+ flags: SILENT_MESSAGE_FLAGS,
242
+ })
243
+
244
+ logger.log(
245
+ `[QUEUE] User ${command.user.displayName} queued command /${commandName} in thread ${channel.id}`,
246
+ )
247
+ }
248
+
249
+ export async function handleQueueCommandAutocomplete({
250
+ interaction,
251
+ }: AutocompleteContext): Promise<void> {
252
+ const focused = interaction.options.getFocused(true)
253
+
254
+ if (focused.name !== 'command') {
255
+ await interaction.respond([])
256
+ return
257
+ }
258
+
259
+ const query = focused.value.toLowerCase()
260
+ const choices = store.getState().registeredUserCommands
261
+ .filter((cmd) => {
262
+ return cmd.name.toLowerCase().includes(query)
263
+ })
264
+ .slice(0, 25)
265
+ .map((cmd) => ({
266
+ name: `/${cmd.name} [${cmd.source === 'skill' ? 'skill' : cmd.source === 'mcp' ? 'mcp' : 'cmd'}] - ${cmd.description}`.slice(0, 100),
267
+ value: cmd.name.slice(0, 100),
268
+ }))
269
+
270
+ await interaction.respond(choices)
271
+ }