@otto-assistant/otto 0.1.1 → 0.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (637) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-account-identity.js +62 -0
  7. package/dist/anthropic-account-identity.test.js +38 -0
  8. package/dist/anthropic-auth-plugin.js +917 -0
  9. package/dist/anthropic-auth-state.js +303 -0
  10. package/dist/anthropic-auth-state.test.js +150 -0
  11. package/dist/bin.js +152 -0
  12. package/dist/btw-prefix-detection.js +17 -0
  13. package/dist/btw-prefix-detection.test.js +63 -0
  14. package/dist/channel-management.js +259 -0
  15. package/dist/cli-parsing.test.js +142 -0
  16. package/dist/cli-send-thread.e2e.test.js +353 -0
  17. package/dist/cli-telegram-options.test.js +99 -0
  18. package/dist/cli.js +4210 -568
  19. package/dist/commands/abort.js +65 -0
  20. package/dist/commands/action-buttons.js +245 -0
  21. package/dist/commands/add-dir.js +124 -0
  22. package/dist/commands/add-dir.test.js +126 -0
  23. package/dist/commands/add-project.js +113 -0
  24. package/dist/commands/agent.js +355 -0
  25. package/dist/commands/ask-question.js +320 -0
  26. package/dist/commands/ask-question.test.js +92 -0
  27. package/dist/commands/btw.js +121 -0
  28. package/dist/commands/cli-commands-group-a.test.js +728 -0
  29. package/dist/commands/cli-commands-group-b.test.js +695 -0
  30. package/dist/commands/compact.js +120 -0
  31. package/dist/commands/context-usage.js +140 -0
  32. package/dist/commands/create-new-project.js +130 -0
  33. package/dist/commands/diff.js +63 -0
  34. package/dist/commands/discord-commands-group-a.test.js +621 -0
  35. package/dist/commands/discord-commands-group-b.test.js +595 -0
  36. package/dist/commands/discord-commands-group-c.test.js +739 -0
  37. package/dist/commands/file-upload.js +275 -0
  38. package/dist/commands/fork-subagent.js +177 -0
  39. package/dist/commands/fork.js +262 -0
  40. package/dist/commands/gemini-apikey.js +70 -0
  41. package/dist/commands/login.js +887 -0
  42. package/dist/commands/mcp.js +239 -0
  43. package/dist/commands/memory-snapshot.js +24 -0
  44. package/dist/commands/mention-mode.js +44 -0
  45. package/dist/commands/merge-worktree.js +162 -0
  46. package/dist/commands/model-variant.js +366 -0
  47. package/dist/commands/model.js +794 -0
  48. package/dist/commands/new-worktree.js +465 -0
  49. package/dist/commands/paginated-select.js +57 -0
  50. package/dist/commands/permissions.js +274 -0
  51. package/dist/commands/queue.js +223 -0
  52. package/dist/commands/remove-project.js +115 -0
  53. package/dist/commands/restart-opencode-server.js +127 -0
  54. package/dist/commands/resume.js +149 -0
  55. package/dist/commands/run-command.js +79 -0
  56. package/dist/commands/screenshare.js +303 -0
  57. package/dist/commands/screenshare.test.js +20 -0
  58. package/dist/commands/session-id.js +78 -0
  59. package/dist/commands/session.js +176 -0
  60. package/dist/commands/share.js +80 -0
  61. package/dist/commands/tasks.js +205 -0
  62. package/dist/commands/thread-deletion-sync.js +50 -0
  63. package/dist/commands/types.js +2 -0
  64. package/dist/commands/undo-redo.js +305 -0
  65. package/dist/commands/unset-model.js +139 -0
  66. package/dist/commands/upgrade.js +48 -0
  67. package/dist/commands/user-command.js +155 -0
  68. package/dist/commands/verbosity.js +125 -0
  69. package/dist/commands/vscode.js +269 -0
  70. package/dist/commands/worktree-settings.js +43 -0
  71. package/dist/commands/worktrees.js +468 -0
  72. package/dist/condense-memory.js +33 -0
  73. package/dist/config.js +100 -255
  74. package/dist/context-awareness-plugin.js +340 -0
  75. package/dist/context-awareness-plugin.test.js +126 -0
  76. package/dist/critique-utils.js +95 -0
  77. package/dist/database.js +1355 -0
  78. package/dist/db.js +260 -0
  79. package/dist/db.test.js +138 -0
  80. package/dist/debounce-timeout.js +28 -0
  81. package/dist/debounced-process-flush.js +77 -0
  82. package/dist/discord-bot.js +1124 -0
  83. package/dist/discord-command-registration.js +567 -0
  84. package/dist/discord-urls.js +82 -0
  85. package/dist/discord-utils.js +616 -0
  86. package/dist/discord-utils.test.js +134 -0
  87. package/dist/errors.js +157 -0
  88. package/dist/escape-backticks.test.js +429 -0
  89. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  90. package/dist/eventsource-parser.test.js +327 -0
  91. package/dist/exec-async.js +26 -0
  92. package/dist/external-opencode-sync.js +480 -0
  93. package/dist/format-tables.js +491 -0
  94. package/dist/format-tables.test.js +478 -0
  95. package/dist/forum-sync/config.js +79 -0
  96. package/dist/forum-sync/discord-operations.js +154 -0
  97. package/dist/forum-sync/index.js +5 -0
  98. package/dist/forum-sync/markdown.js +113 -0
  99. package/dist/forum-sync/sync-to-discord.js +417 -0
  100. package/dist/forum-sync/sync-to-files.js +190 -0
  101. package/dist/forum-sync/types.js +53 -0
  102. package/dist/forum-sync/watchers.js +307 -0
  103. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  104. package/dist/gateway-proxy.e2e.test.js +485 -0
  105. package/dist/genai-worker-wrapper.js +111 -0
  106. package/dist/genai-worker.js +311 -0
  107. package/dist/genai.js +232 -0
  108. package/dist/generated/browser.js +17 -0
  109. package/dist/generated/client.js +37 -0
  110. package/dist/generated/commonInputTypes.js +10 -0
  111. package/dist/generated/enums.js +58 -0
  112. package/dist/generated/internal/class.js +49 -0
  113. package/dist/generated/internal/prismaNamespace.js +254 -0
  114. package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
  115. package/dist/generated/models/bot_api_keys.js +1 -0
  116. package/dist/generated/models/bot_tokens.js +1 -0
  117. package/dist/generated/models/channel_agents.js +1 -0
  118. package/dist/generated/models/channel_directories.js +1 -0
  119. package/dist/generated/models/channel_mention_mode.js +1 -0
  120. package/dist/generated/models/channel_models.js +1 -0
  121. package/dist/generated/models/channel_verbosity.js +1 -0
  122. package/dist/generated/models/channel_worktrees.js +1 -0
  123. package/dist/generated/models/forum_sync_configs.js +1 -0
  124. package/dist/generated/models/global_models.js +1 -0
  125. package/dist/generated/models/ipc_requests.js +1 -0
  126. package/dist/generated/models/part_messages.js +1 -0
  127. package/dist/generated/models/scheduled_tasks.js +1 -0
  128. package/dist/generated/models/session_agents.js +1 -0
  129. package/dist/generated/models/session_events.js +1 -0
  130. package/dist/generated/models/session_models.js +1 -0
  131. package/dist/generated/models/session_start_sources.js +1 -0
  132. package/dist/generated/models/thread_sessions.js +1 -0
  133. package/dist/generated/models/thread_worktrees.js +1 -0
  134. package/dist/generated/models.js +1 -0
  135. package/dist/heap-monitor.js +122 -0
  136. package/dist/hrana-server.js +251 -0
  137. package/dist/hrana-server.test.js +370 -0
  138. package/dist/html-actions.js +123 -0
  139. package/dist/html-actions.test.js +70 -0
  140. package/dist/html-components.js +117 -0
  141. package/dist/html-components.test.js +34 -0
  142. package/dist/image-optimizer-plugin.js +153 -0
  143. package/dist/image-utils.js +112 -0
  144. package/dist/interaction-handler.js +420 -0
  145. package/dist/ipc-polling.js +327 -0
  146. package/dist/ipc-tools-plugin.js +193 -0
  147. package/dist/ipc-utils.js +18 -0
  148. package/dist/limit-heading-depth.js +25 -0
  149. package/dist/limit-heading-depth.test.js +105 -0
  150. package/dist/logger.js +171 -0
  151. package/dist/markdown.js +342 -0
  152. package/dist/markdown.test.js +264 -0
  153. package/dist/memory-overview-plugin.js +128 -0
  154. package/dist/message-finish-field.e2e.test.js +168 -0
  155. package/dist/message-formatting.js +415 -0
  156. package/dist/message-formatting.test.js +115 -0
  157. package/dist/message-preprocessing.js +359 -0
  158. package/dist/onboarding-tutorial.js +163 -0
  159. package/dist/onboarding-welcome.js +37 -0
  160. package/dist/openai-realtime.js +224 -0
  161. package/dist/opencode-command-detection.js +65 -0
  162. package/dist/opencode-command-detection.test.js +240 -0
  163. package/dist/opencode-command.js +131 -0
  164. package/dist/opencode-command.test.js +48 -0
  165. package/dist/opencode-interrupt-plugin.js +388 -0
  166. package/dist/opencode-interrupt-plugin.test.js +463 -0
  167. package/dist/opencode.js +1117 -0
  168. package/dist/otto/branding.js +22 -0
  169. package/dist/otto/index.js +21 -0
  170. package/dist/otto-digital-twin.e2e.test.js +161 -0
  171. package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
  172. package/dist/otto-opencode-plugin.js +21 -0
  173. package/dist/otto-opencode-plugin.test.js +98 -0
  174. package/dist/parse-permission-rules.test.js +117 -0
  175. package/dist/patch-text-parser.js +97 -0
  176. package/dist/plugin-logger.js +68 -0
  177. package/dist/privacy-sanitizer.js +105 -0
  178. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  179. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  180. package/dist/queue-advanced-e2e-setup.js +790 -0
  181. package/dist/queue-advanced-footer.e2e.test.js +481 -0
  182. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  183. package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
  184. package/dist/queue-advanced-question.e2e.test.js +261 -0
  185. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  186. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  187. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  188. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  189. package/dist/queue-question-select-drain.e2e.test.js +256 -0
  190. package/dist/runtime-idle-sweeper.js +52 -0
  191. package/dist/runtime-lifecycle.e2e.test.js +514 -0
  192. package/dist/sentry.js +23 -0
  193. package/dist/session-handler/agent-utils.js +67 -0
  194. package/dist/session-handler/event-stream-state.js +475 -0
  195. package/dist/session-handler/event-stream-state.test.js +632 -0
  196. package/dist/session-handler/model-utils.js +147 -0
  197. package/dist/session-handler/opencode-session-event-log.js +94 -0
  198. package/dist/session-handler/thread-runtime-state.js +131 -0
  199. package/dist/session-handler/thread-session-runtime.js +3390 -0
  200. package/dist/session-handler.js +9 -0
  201. package/dist/session-search.js +100 -0
  202. package/dist/session-search.test.js +40 -0
  203. package/dist/session-title-rename.test.js +92 -0
  204. package/dist/skill-filter.js +31 -0
  205. package/dist/skill-filter.test.js +65 -0
  206. package/dist/startup-service.js +153 -0
  207. package/dist/startup-time.e2e.test.js +296 -0
  208. package/dist/store.js +19 -0
  209. package/dist/subagent-rate-limit-plugin.js +175 -0
  210. package/dist/system-message.js +702 -0
  211. package/dist/system-message.test.js +697 -0
  212. package/dist/task-runner.js +530 -0
  213. package/dist/task-schedule.js +213 -0
  214. package/dist/task-schedule.test.js +71 -0
  215. package/dist/test-utils.js +313 -0
  216. package/dist/thinking-utils.js +35 -0
  217. package/dist/thread-message-queue.e2e.test.js +1111 -0
  218. package/dist/tools.js +357 -0
  219. package/dist/undo-redo.e2e.test.js +161 -0
  220. package/dist/unnest-code-blocks.js +146 -0
  221. package/dist/unnest-code-blocks.test.js +673 -0
  222. package/dist/upgrade.js +156 -0
  223. package/dist/utils.js +172 -0
  224. package/dist/utils.test.js +130 -0
  225. package/dist/voice-attachment.js +34 -0
  226. package/dist/voice-handler.js +646 -0
  227. package/dist/voice-message.e2e.test.js +1021 -0
  228. package/dist/voice.js +456 -0
  229. package/dist/voice.test.js +235 -0
  230. package/dist/wait-session.js +171 -0
  231. package/dist/websockify.js +69 -0
  232. package/dist/worker-types.js +4 -0
  233. package/dist/worktree-lifecycle.e2e.test.js +311 -0
  234. package/dist/worktree-utils.js +3 -0
  235. package/dist/worktrees.js +991 -0
  236. package/dist/worktrees.test.js +415 -0
  237. package/dist/xml.js +92 -0
  238. package/dist/xml.test.js +32 -0
  239. package/package.json +90 -38
  240. package/schema.prisma +303 -0
  241. package/skills/batch/SKILL.md +87 -0
  242. package/skills/critique/SKILL.md +112 -0
  243. package/skills/egaki/SKILL.md +100 -0
  244. package/skills/errore/SKILL.md +647 -0
  245. package/skills/event-sourcing-state/SKILL.md +252 -0
  246. package/skills/goke/SKILL.md +38 -0
  247. package/skills/jitter/EDITOR.md +219 -0
  248. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  249. package/skills/jitter/SKILL.md +158 -0
  250. package/skills/jitter/jitter-clipboard.json +1042 -0
  251. package/skills/jitter/package.json +14 -0
  252. package/skills/jitter/tsconfig.json +15 -0
  253. package/skills/jitter/utils/actions.ts +212 -0
  254. package/skills/jitter/utils/export.ts +114 -0
  255. package/skills/jitter/utils/index.ts +141 -0
  256. package/skills/jitter/utils/snapshot.ts +154 -0
  257. package/skills/jitter/utils/traverse.ts +246 -0
  258. package/skills/jitter/utils/types.ts +279 -0
  259. package/skills/jitter/utils/wait.ts +133 -0
  260. package/skills/lintcn/SKILL.md +873 -0
  261. package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
  262. package/skills/new-skill/SKILL.md +237 -0
  263. package/skills/npm-package/SKILL.md +617 -0
  264. package/skills/opensrc/SKILL.md +78 -0
  265. package/skills/otto-publish/SKILL.md +61 -0
  266. package/skills/playwriter/SKILL.md +35 -0
  267. package/skills/profano/SKILL.md +16 -0
  268. package/skills/proxyman/SKILL.md +215 -0
  269. package/skills/security-review/SKILL.md +208 -0
  270. package/skills/sigillo/SKILL.md +101 -0
  271. package/skills/simplify/SKILL.md +58 -0
  272. package/skills/spiceflow/SKILL.md +28 -0
  273. package/skills/termcast/SKILL.md +945 -0
  274. package/skills/tuistory/SKILL.md +98 -0
  275. package/skills/usecomputer/SKILL.md +264 -0
  276. package/skills/x-articles/SKILL.md +554 -0
  277. package/skills/zele/SKILL.md +49 -0
  278. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  279. package/src/agent-model.e2e.test.ts +979 -0
  280. package/src/ai-tool-to-genai.test.ts +296 -0
  281. package/src/ai-tool-to-genai.ts +283 -0
  282. package/src/ai-tool.ts +39 -0
  283. package/src/anthropic-account-identity.test.ts +52 -0
  284. package/src/anthropic-account-identity.ts +77 -0
  285. package/src/anthropic-auth-plugin.ts +1139 -0
  286. package/src/anthropic-auth-state.test.ts +187 -0
  287. package/src/anthropic-auth-state.ts +386 -0
  288. package/src/bin.ts +182 -0
  289. package/src/btw-prefix-detection.test.ts +73 -0
  290. package/src/btw-prefix-detection.ts +23 -0
  291. package/src/channel-management.ts +376 -0
  292. package/src/cli-parsing.test.ts +197 -0
  293. package/src/cli-send-thread.e2e.test.ts +463 -0
  294. package/src/cli-telegram-options.test.ts +114 -0
  295. package/src/cli.ts +5718 -580
  296. package/src/commands/abort.ts +89 -0
  297. package/src/commands/action-buttons.ts +364 -0
  298. package/src/commands/add-dir.test.ts +154 -0
  299. package/src/commands/add-dir.ts +175 -0
  300. package/src/commands/add-project.ts +149 -0
  301. package/src/commands/agent.ts +496 -0
  302. package/src/commands/ask-question.test.ts +111 -0
  303. package/src/commands/ask-question.ts +455 -0
  304. package/src/commands/btw.ts +184 -0
  305. package/src/commands/cli-commands-group-a.test.ts +837 -0
  306. package/src/commands/cli-commands-group-b.test.ts +800 -0
  307. package/src/commands/compact.ts +157 -0
  308. package/src/commands/context-usage.ts +199 -0
  309. package/src/commands/create-new-project.ts +190 -0
  310. package/src/commands/diff.ts +91 -0
  311. package/src/commands/discord-commands-group-a.test.ts +751 -0
  312. package/src/commands/discord-commands-group-b.test.ts +648 -0
  313. package/src/commands/discord-commands-group-c.test.ts +882 -0
  314. package/src/commands/file-upload.ts +389 -0
  315. package/src/commands/fork-subagent.ts +263 -0
  316. package/src/commands/fork.ts +386 -0
  317. package/src/commands/gemini-apikey.ts +104 -0
  318. package/src/commands/login.ts +1175 -0
  319. package/src/commands/mcp.ts +307 -0
  320. package/src/commands/memory-snapshot.ts +30 -0
  321. package/src/commands/mention-mode.ts +68 -0
  322. package/src/commands/merge-worktree.ts +226 -0
  323. package/src/commands/model-variant.ts +485 -0
  324. package/src/commands/model.ts +1078 -0
  325. package/src/commands/new-worktree.ts +645 -0
  326. package/src/commands/paginated-select.ts +81 -0
  327. package/src/commands/permissions.ts +397 -0
  328. package/src/commands/queue.ts +293 -0
  329. package/src/commands/remove-project.ts +155 -0
  330. package/src/commands/restart-opencode-server.ts +162 -0
  331. package/src/commands/resume.ts +230 -0
  332. package/src/commands/run-command.ts +123 -0
  333. package/src/commands/screenshare.test.ts +30 -0
  334. package/src/commands/screenshare.ts +366 -0
  335. package/src/commands/session-id.ts +109 -0
  336. package/src/commands/session.ts +227 -0
  337. package/src/commands/share.ts +106 -0
  338. package/src/commands/tasks.ts +293 -0
  339. package/src/commands/thread-deletion-sync.ts +80 -0
  340. package/src/commands/types.ts +25 -0
  341. package/src/commands/undo-redo.ts +386 -0
  342. package/src/commands/unset-model.ts +174 -0
  343. package/src/commands/upgrade.ts +59 -0
  344. package/src/commands/user-command.ts +198 -0
  345. package/src/commands/verbosity.ts +173 -0
  346. package/src/commands/vscode.ts +342 -0
  347. package/src/commands/worktree-settings.ts +70 -0
  348. package/src/commands/worktrees.ts +645 -0
  349. package/src/condense-memory.ts +36 -0
  350. package/src/config.ts +103 -339
  351. package/src/context-awareness-plugin.test.ts +144 -0
  352. package/src/context-awareness-plugin.ts +469 -0
  353. package/src/critique-utils.ts +139 -0
  354. package/src/database.ts +1949 -0
  355. package/src/db.test.ts +162 -0
  356. package/src/db.ts +295 -0
  357. package/src/debounce-timeout.ts +43 -0
  358. package/src/debounced-process-flush.ts +104 -0
  359. package/src/discord-bot.ts +1505 -0
  360. package/src/discord-command-registration.ts +752 -0
  361. package/src/discord-urls.ts +89 -0
  362. package/src/discord-utils.test.ts +153 -0
  363. package/src/discord-utils.ts +846 -0
  364. package/src/errors.ts +201 -0
  365. package/src/escape-backticks.test.ts +469 -0
  366. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  367. package/src/eventsource-parser.test.ts +351 -0
  368. package/src/exec-async.ts +35 -0
  369. package/src/external-opencode-sync.ts +685 -0
  370. package/src/format-tables.test.ts +515 -0
  371. package/src/format-tables.ts +718 -0
  372. package/src/forum-sync/config.ts +92 -0
  373. package/src/forum-sync/discord-operations.ts +241 -0
  374. package/src/forum-sync/index.ts +9 -0
  375. package/src/forum-sync/markdown.ts +172 -0
  376. package/src/forum-sync/sync-to-discord.ts +595 -0
  377. package/src/forum-sync/sync-to-files.ts +294 -0
  378. package/src/forum-sync/types.ts +175 -0
  379. package/src/forum-sync/watchers.ts +454 -0
  380. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  381. package/src/gateway-proxy.e2e.test.ts +644 -0
  382. package/src/genai-worker-wrapper.ts +164 -0
  383. package/src/genai-worker.ts +386 -0
  384. package/src/genai.ts +321 -0
  385. package/src/generated/browser.ts +114 -0
  386. package/src/generated/client.ts +138 -0
  387. package/src/generated/commonInputTypes.ts +770 -0
  388. package/src/generated/enums.ts +98 -0
  389. package/src/generated/internal/class.ts +384 -0
  390. package/src/generated/internal/prismaNamespace.ts +2394 -0
  391. package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
  392. package/src/generated/models/bot_api_keys.ts +1288 -0
  393. package/src/generated/models/bot_tokens.ts +1700 -0
  394. package/src/generated/models/channel_agents.ts +1256 -0
  395. package/src/generated/models/channel_directories.ts +1859 -0
  396. package/src/generated/models/channel_mention_mode.ts +1300 -0
  397. package/src/generated/models/channel_models.ts +1288 -0
  398. package/src/generated/models/channel_verbosity.ts +1228 -0
  399. package/src/generated/models/channel_worktrees.ts +1300 -0
  400. package/src/generated/models/forum_sync_configs.ts +1452 -0
  401. package/src/generated/models/global_models.ts +1288 -0
  402. package/src/generated/models/ipc_requests.ts +1485 -0
  403. package/src/generated/models/part_messages.ts +1302 -0
  404. package/src/generated/models/scheduled_tasks.ts +2320 -0
  405. package/src/generated/models/session_agents.ts +1086 -0
  406. package/src/generated/models/session_events.ts +1439 -0
  407. package/src/generated/models/session_models.ts +1114 -0
  408. package/src/generated/models/session_start_sources.ts +1408 -0
  409. package/src/generated/models/thread_sessions.ts +1781 -0
  410. package/src/generated/models/thread_worktrees.ts +1356 -0
  411. package/src/generated/models.ts +30 -0
  412. package/src/heap-monitor.ts +152 -0
  413. package/src/hrana-server.test.ts +434 -0
  414. package/src/hrana-server.ts +299 -0
  415. package/src/html-actions.test.ts +87 -0
  416. package/src/html-actions.ts +174 -0
  417. package/src/html-components.test.ts +38 -0
  418. package/src/html-components.ts +181 -0
  419. package/src/image-optimizer-plugin.ts +194 -0
  420. package/src/image-utils.ts +149 -0
  421. package/src/interaction-handler.ts +610 -0
  422. package/src/ipc-polling.ts +427 -0
  423. package/src/ipc-tools-plugin.ts +236 -0
  424. package/src/ipc-utils.ts +29 -0
  425. package/src/limit-heading-depth.test.ts +116 -0
  426. package/src/limit-heading-depth.ts +26 -0
  427. package/src/logger.ts +215 -0
  428. package/src/markdown.test.ts +315 -0
  429. package/src/markdown.ts +410 -0
  430. package/src/memory-overview-plugin.ts +163 -0
  431. package/src/message-finish-field.e2e.test.ts +195 -0
  432. package/src/message-formatting.test.ts +126 -0
  433. package/src/message-formatting.ts +535 -0
  434. package/src/message-preprocessing.ts +488 -0
  435. package/src/onboarding-tutorial.ts +167 -0
  436. package/src/onboarding-welcome.ts +49 -0
  437. package/src/openai-realtime.ts +358 -0
  438. package/src/opencode-command-detection.test.ts +307 -0
  439. package/src/opencode-command-detection.ts +76 -0
  440. package/src/opencode-command.test.ts +70 -0
  441. package/src/opencode-command.ts +191 -0
  442. package/src/opencode-interrupt-plugin.test.ts +682 -0
  443. package/src/opencode-interrupt-plugin.ts +507 -0
  444. package/src/opencode.ts +1453 -0
  445. package/src/otto/branding.ts +23 -0
  446. package/src/otto/index.ts +22 -0
  447. package/src/otto-digital-twin.e2e.test.ts +199 -0
  448. package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
  449. package/src/otto-opencode-plugin.test.ts +108 -0
  450. package/src/otto-opencode-plugin.ts +22 -0
  451. package/src/parse-permission-rules.test.ts +127 -0
  452. package/src/patch-text-parser.ts +107 -0
  453. package/src/plugin-logger.ts +84 -0
  454. package/src/privacy-sanitizer.ts +142 -0
  455. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  456. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  457. package/src/queue-advanced-e2e-setup.ts +877 -0
  458. package/src/queue-advanced-footer.e2e.test.ts +591 -0
  459. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  460. package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
  461. package/src/queue-advanced-question.e2e.test.ts +316 -0
  462. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  463. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  464. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  465. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  466. package/src/queue-question-select-drain.e2e.test.ts +327 -0
  467. package/src/runtime-idle-sweeper.ts +76 -0
  468. package/src/runtime-lifecycle.e2e.test.ts +651 -0
  469. package/src/schema.sql +174 -0
  470. package/src/sentry.ts +26 -0
  471. package/src/session-handler/agent-utils.ts +99 -0
  472. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  473. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  474. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  475. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  476. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  477. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  478. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  479. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  480. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  481. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  482. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  483. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  484. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  485. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  486. package/src/session-handler/event-stream-state.test.ts +717 -0
  487. package/src/session-handler/event-stream-state.ts +706 -0
  488. package/src/session-handler/model-utils.ts +217 -0
  489. package/src/session-handler/opencode-session-event-log.ts +130 -0
  490. package/src/session-handler/thread-runtime-state.ts +247 -0
  491. package/src/session-handler/thread-session-runtime.ts +4440 -0
  492. package/src/session-handler.ts +15 -0
  493. package/src/session-search.test.ts +50 -0
  494. package/src/session-search.ts +148 -0
  495. package/src/session-title-rename.test.ts +130 -0
  496. package/src/skill-filter.test.ts +83 -0
  497. package/src/skill-filter.ts +42 -0
  498. package/src/startup-service.ts +200 -0
  499. package/src/startup-time.e2e.test.ts +373 -0
  500. package/src/store.ts +139 -0
  501. package/src/subagent-rate-limit-plugin.ts +218 -0
  502. package/src/system-message.test.ts +710 -0
  503. package/src/system-message.ts +814 -0
  504. package/src/task-runner.ts +725 -0
  505. package/src/task-schedule.test.ts +84 -0
  506. package/src/task-schedule.ts +317 -0
  507. package/src/test-utils.ts +451 -0
  508. package/src/thinking-utils.ts +61 -0
  509. package/src/thread-message-queue.e2e.test.ts +1350 -0
  510. package/src/tools.ts +430 -0
  511. package/src/undici.d.ts +12 -0
  512. package/src/undo-redo.e2e.test.ts +209 -0
  513. package/src/unnest-code-blocks.test.ts +713 -0
  514. package/src/unnest-code-blocks.ts +185 -0
  515. package/src/upgrade.ts +185 -0
  516. package/src/utils.test.ts +155 -0
  517. package/src/utils.ts +265 -0
  518. package/src/voice-attachment.ts +51 -0
  519. package/src/voice-handler.ts +908 -0
  520. package/src/voice-message.e2e.test.ts +1255 -0
  521. package/src/voice.test.ts +281 -0
  522. package/src/voice.ts +638 -0
  523. package/src/wait-session.ts +273 -0
  524. package/src/websockify.ts +101 -0
  525. package/src/worker-types.ts +64 -0
  526. package/src/worktree-lifecycle.e2e.test.ts +396 -0
  527. package/src/worktree-utils.ts +4 -0
  528. package/src/worktrees.test.ts +489 -0
  529. package/src/worktrees.ts +1370 -0
  530. package/src/xml.test.ts +38 -0
  531. package/src/xml.ts +121 -0
  532. package/dist/cli.d.ts +0 -3
  533. package/dist/cli.d.ts.map +0 -1
  534. package/dist/cli.js.map +0 -1
  535. package/dist/config.d.ts +0 -39
  536. package/dist/config.d.ts.map +0 -1
  537. package/dist/config.js.map +0 -1
  538. package/dist/config.test.d.ts +0 -2
  539. package/dist/config.test.d.ts.map +0 -1
  540. package/dist/config.test.js +0 -202
  541. package/dist/config.test.js.map +0 -1
  542. package/dist/detect.d.ts +0 -9
  543. package/dist/detect.d.ts.map +0 -1
  544. package/dist/detect.js +0 -40
  545. package/dist/detect.js.map +0 -1
  546. package/dist/detect.test.d.ts +0 -2
  547. package/dist/detect.test.d.ts.map +0 -1
  548. package/dist/detect.test.js +0 -26
  549. package/dist/detect.test.js.map +0 -1
  550. package/dist/docker.d.ts +0 -7
  551. package/dist/docker.d.ts.map +0 -1
  552. package/dist/docker.js +0 -17
  553. package/dist/docker.js.map +0 -1
  554. package/dist/docker.test.d.ts +0 -2
  555. package/dist/docker.test.d.ts.map +0 -1
  556. package/dist/docker.test.js +0 -12
  557. package/dist/docker.test.js.map +0 -1
  558. package/dist/health.d.ts +0 -31
  559. package/dist/health.d.ts.map +0 -1
  560. package/dist/health.js +0 -117
  561. package/dist/health.js.map +0 -1
  562. package/dist/health.test.d.ts +0 -2
  563. package/dist/health.test.d.ts.map +0 -1
  564. package/dist/health.test.js +0 -52
  565. package/dist/health.test.js.map +0 -1
  566. package/dist/index.d.ts +0 -20
  567. package/dist/index.d.ts.map +0 -1
  568. package/dist/index.js +0 -15
  569. package/dist/index.js.map +0 -1
  570. package/dist/index.test.d.ts +0 -2
  571. package/dist/index.test.d.ts.map +0 -1
  572. package/dist/index.test.js +0 -8
  573. package/dist/index.test.js.map +0 -1
  574. package/dist/installer.d.ts +0 -10
  575. package/dist/installer.d.ts.map +0 -1
  576. package/dist/installer.js +0 -50
  577. package/dist/installer.js.map +0 -1
  578. package/dist/installer.test.d.ts +0 -2
  579. package/dist/installer.test.d.ts.map +0 -1
  580. package/dist/installer.test.js +0 -43
  581. package/dist/installer.test.js.map +0 -1
  582. package/dist/lifecycle.d.ts +0 -10
  583. package/dist/lifecycle.d.ts.map +0 -1
  584. package/dist/lifecycle.js +0 -45
  585. package/dist/lifecycle.js.map +0 -1
  586. package/dist/lifecycle.test.d.ts +0 -2
  587. package/dist/lifecycle.test.d.ts.map +0 -1
  588. package/dist/lifecycle.test.js +0 -20
  589. package/dist/lifecycle.test.js.map +0 -1
  590. package/dist/manifest.d.ts +0 -18
  591. package/dist/manifest.d.ts.map +0 -1
  592. package/dist/manifest.js +0 -30
  593. package/dist/manifest.js.map +0 -1
  594. package/dist/skills-baseline.d.ts +0 -7
  595. package/dist/skills-baseline.d.ts.map +0 -1
  596. package/dist/skills-baseline.js +0 -9
  597. package/dist/skills-baseline.js.map +0 -1
  598. package/dist/skills.d.ts +0 -110
  599. package/dist/skills.d.ts.map +0 -1
  600. package/dist/skills.js +0 -429
  601. package/dist/skills.js.map +0 -1
  602. package/dist/skills.test.d.ts +0 -2
  603. package/dist/skills.test.d.ts.map +0 -1
  604. package/dist/skills.test.js +0 -416
  605. package/dist/skills.test.js.map +0 -1
  606. package/dist/sync.d.ts +0 -10
  607. package/dist/sync.d.ts.map +0 -1
  608. package/dist/sync.js +0 -39
  609. package/dist/sync.js.map +0 -1
  610. package/dist/tenant.d.ts +0 -13
  611. package/dist/tenant.d.ts.map +0 -1
  612. package/dist/tenant.js +0 -105
  613. package/dist/tenant.js.map +0 -1
  614. package/dist/tenant.test.d.ts +0 -2
  615. package/dist/tenant.test.d.ts.map +0 -1
  616. package/dist/tenant.test.js +0 -37
  617. package/dist/tenant.test.js.map +0 -1
  618. package/src/config.test.ts +0 -237
  619. package/src/detect.test.ts +0 -29
  620. package/src/detect.ts +0 -52
  621. package/src/docker.test.ts +0 -12
  622. package/src/docker.ts +0 -23
  623. package/src/health.test.ts +0 -61
  624. package/src/health.ts +0 -158
  625. package/src/index.test.ts +0 -8
  626. package/src/index.ts +0 -62
  627. package/src/installer.test.ts +0 -52
  628. package/src/installer.ts +0 -62
  629. package/src/lifecycle.test.ts +0 -23
  630. package/src/lifecycle.ts +0 -49
  631. package/src/manifest.ts +0 -42
  632. package/src/skills-baseline.ts +0 -14
  633. package/src/skills.test.ts +0 -503
  634. package/src/skills.ts +0 -512
  635. package/src/sync.ts +0 -53
  636. package/src/tenant.test.ts +0 -49
  637. package/src/tenant.ts +0 -120
@@ -0,0 +1,274 @@
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
+ import { ButtonBuilder, ButtonStyle, ActionRowBuilder, MessageFlags, } from 'discord.js';
5
+ import crypto from 'node:crypto';
6
+ import { getOpencodeClient } from '../opencode.js';
7
+ import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
8
+ import { createLogger, LogPrefix } from '../logger.js';
9
+ const logger = createLogger(LogPrefix.PERMISSIONS);
10
+ function wildcardMatch({ value, pattern, }) {
11
+ let escapedPattern = pattern
12
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
13
+ .replace(/\*/g, '.*')
14
+ .replace(/\?/g, '.');
15
+ if (escapedPattern.endsWith(' .*')) {
16
+ escapedPattern = escapedPattern.slice(0, -3) + '( .*)?';
17
+ }
18
+ return new RegExp(`^${escapedPattern}$`, 's').test(value);
19
+ }
20
+ export function arePatternsCoveredBy({ patterns, coveringPatterns, }) {
21
+ return patterns.every((pattern) => {
22
+ return coveringPatterns.some((coveringPattern) => {
23
+ return wildcardMatch({ value: pattern, pattern: coveringPattern });
24
+ });
25
+ });
26
+ }
27
+ export function compactPermissionPatterns(patterns) {
28
+ const uniquePatterns = Array.from(new Set(patterns));
29
+ return uniquePatterns.filter((pattern, index) => {
30
+ return !uniquePatterns.some((candidate, candidateIndex) => {
31
+ if (candidateIndex === index) {
32
+ return false;
33
+ }
34
+ return wildcardMatch({ value: pattern, pattern: candidate });
35
+ });
36
+ });
37
+ }
38
+ // Store pending permission contexts by hash.
39
+ // TTL prevents unbounded growth if user never clicks a permission button.
40
+ const PERMISSION_CONTEXT_TTL_MS = 10 * 60 * 1000;
41
+ export const pendingPermissionContexts = new Map();
42
+ // Atomic take: removes context from Map and returns it. Only the first caller
43
+ // (TTL expiry or button click) wins, preventing duplicate permission replies.
44
+ function takePendingPermissionContext(contextHash) {
45
+ const ctx = pendingPermissionContexts.get(contextHash);
46
+ if (!ctx) {
47
+ return undefined;
48
+ }
49
+ pendingPermissionContexts.delete(contextHash);
50
+ return ctx;
51
+ }
52
+ /**
53
+ * Show permission buttons for a permission request.
54
+ * Displays 3 buttons in a row: Accept, Accept Always, Deny.
55
+ * Returns the message ID and context hash for tracking.
56
+ */
57
+ export async function showPermissionButtons({ thread, permission, directory, permissionDirectory, subtaskLabel, }) {
58
+ const contextHash = crypto.randomBytes(8).toString('hex');
59
+ const context = {
60
+ permission,
61
+ requestIds: [permission.id],
62
+ directory,
63
+ permissionDirectory,
64
+ thread,
65
+ contextHash,
66
+ };
67
+ pendingPermissionContexts.set(contextHash, context);
68
+ // Auto-reject on TTL expiry so the OpenCode session doesn't hang forever
69
+ // waiting for a permission reply that will never come. Uses atomic take
70
+ // so only one of TTL-expiry or button-click can win.
71
+ setTimeout(async () => {
72
+ const ctx = takePendingPermissionContext(contextHash);
73
+ if (!ctx) {
74
+ return;
75
+ }
76
+ const client = getOpencodeClient(ctx.directory);
77
+ if (client) {
78
+ const requestIds = ctx.requestIds.length > 0
79
+ ? ctx.requestIds
80
+ : [ctx.permission.id];
81
+ await Promise.all(requestIds.map((requestId) => {
82
+ return client.permission.reply({
83
+ requestID: requestId,
84
+ directory: ctx.permissionDirectory,
85
+ reply: 'reject',
86
+ });
87
+ })).catch((error) => {
88
+ logger.error('Failed to auto-reject expired permission:', error);
89
+ });
90
+ }
91
+ }, PERMISSION_CONTEXT_TTL_MS).unref();
92
+ const patternStr = compactPermissionPatterns(permission.patterns).join(', ');
93
+ // Build 3 buttons for permission actions
94
+ const acceptButton = new ButtonBuilder()
95
+ .setCustomId(`permission_once:${contextHash}`)
96
+ .setLabel('Accept')
97
+ .setStyle(ButtonStyle.Success);
98
+ const acceptAlwaysButton = new ButtonBuilder()
99
+ .setCustomId(`permission_always:${contextHash}`)
100
+ .setLabel('Accept Always')
101
+ .setStyle(ButtonStyle.Success);
102
+ const denyButton = new ButtonBuilder()
103
+ .setCustomId(`permission_reject:${contextHash}`)
104
+ .setLabel('Deny')
105
+ .setStyle(ButtonStyle.Secondary);
106
+ const actionRow = new ActionRowBuilder().addComponents(acceptButton, acceptAlwaysButton, denyButton);
107
+ const subtaskLine = subtaskLabel ? `**From:** \`${subtaskLabel}\`\n` : '';
108
+ const externalDirLine = permission.permission === 'external_directory'
109
+ ? `Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n`
110
+ : '';
111
+ const fullContent = `⚠️ **Permission Required**\n` +
112
+ subtaskLine +
113
+ `**Type:** \`${permission.permission}\`\n` +
114
+ externalDirLine +
115
+ (patternStr ? `**Pattern:** \`${patternStr}\`` : '');
116
+ const permissionMessage = await thread.send({
117
+ content: fullContent.slice(0, 1900),
118
+ components: [actionRow],
119
+ flags: NOTIFY_MESSAGE_FLAGS | MessageFlags.SuppressEmbeds,
120
+ });
121
+ context.messageId = permissionMessage.id;
122
+ logger.log(`Showed permission buttons for ${permission.id}`);
123
+ return { messageId: permissionMessage.id, contextHash };
124
+ }
125
+ function updatePermissionMessage({ context, status, }) {
126
+ if (!context.messageId) {
127
+ return;
128
+ }
129
+ context.thread.messages
130
+ .fetch(context.messageId)
131
+ .then((message) => {
132
+ const patternStr = compactPermissionPatterns(context.permission.patterns).join(', ');
133
+ const externalDirLine = context.permission.permission === 'external_directory'
134
+ ? 'Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n'
135
+ : '';
136
+ return message.edit({
137
+ content: `⚠️ **Permission Required**\n` +
138
+ `**Type:** \`${context.permission.permission}\`\n` +
139
+ externalDirLine +
140
+ (patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
141
+ status,
142
+ components: [],
143
+ });
144
+ })
145
+ .catch((error) => {
146
+ logger.error('Failed to update permission message:', error);
147
+ });
148
+ }
149
+ export async function cancelPendingPermission(threadId) {
150
+ const contexts = Array.from(pendingPermissionContexts.values()).filter((context) => {
151
+ return context.thread.id === threadId;
152
+ });
153
+ if (contexts.length === 0) {
154
+ return false;
155
+ }
156
+ let cancelledCount = 0;
157
+ for (const context of contexts) {
158
+ const pendingContext = takePendingPermissionContext(context.contextHash);
159
+ if (!pendingContext) {
160
+ continue;
161
+ }
162
+ const client = getOpencodeClient(pendingContext.directory);
163
+ if (!client) {
164
+ pendingPermissionContexts.set(pendingContext.contextHash, pendingContext);
165
+ logger.error('Failed to dismiss pending permission: OpenCode server not found');
166
+ continue;
167
+ }
168
+ const requestIds = pendingContext.requestIds.length > 0
169
+ ? pendingContext.requestIds
170
+ : [pendingContext.permission.id];
171
+ const result = await Promise.all(requestIds.map((requestId) => {
172
+ return client.permission.reply({
173
+ requestID: requestId,
174
+ directory: pendingContext.permissionDirectory,
175
+ reply: 'reject',
176
+ });
177
+ })).then(() => {
178
+ return 'ok';
179
+ }).catch((error) => {
180
+ pendingPermissionContexts.set(pendingContext.contextHash, pendingContext);
181
+ logger.error('Failed to dismiss pending permission:', error);
182
+ return 'error';
183
+ });
184
+ if (result === 'error') {
185
+ continue;
186
+ }
187
+ updatePermissionMessage({
188
+ context: pendingContext,
189
+ status: '_Permission dismissed - user sent a new message._',
190
+ });
191
+ cancelledCount++;
192
+ }
193
+ if (cancelledCount > 0) {
194
+ logger.log(`Dismissed ${cancelledCount} pending permission request(s) for thread ${threadId}`);
195
+ }
196
+ return cancelledCount > 0;
197
+ }
198
+ /**
199
+ * Handle button click for permission.
200
+ */
201
+ export async function handlePermissionButton(interaction) {
202
+ const customId = interaction.customId;
203
+ // Extract action and hash from customId (e.g., "permission_once:abc123")
204
+ const [actionPart, contextHash] = customId.split(':');
205
+ if (!actionPart || !contextHash) {
206
+ return;
207
+ }
208
+ const response = actionPart.replace('permission_', '');
209
+ // Atomic take: if TTL already expired and auto-rejected, context is gone.
210
+ const context = takePendingPermissionContext(contextHash);
211
+ if (!context) {
212
+ await interaction.update({ components: [] });
213
+ return;
214
+ }
215
+ await interaction.deferUpdate();
216
+ try {
217
+ const permClient = getOpencodeClient(context.directory);
218
+ if (!permClient) {
219
+ throw new Error('OpenCode server not found for directory');
220
+ }
221
+ const requestIds = context.requestIds.length > 0
222
+ ? context.requestIds
223
+ : [context.permission.id];
224
+ await Promise.all(requestIds.map((requestId) => {
225
+ return permClient.permission.reply({
226
+ requestID: requestId,
227
+ directory: context.permissionDirectory,
228
+ reply: response,
229
+ });
230
+ }));
231
+ // Context already removed by takePendingPermissionContext above.
232
+ // Update message: show result and remove dropdown
233
+ const resultText = (() => {
234
+ switch (response) {
235
+ case 'once':
236
+ return '✅ Permission **accepted**';
237
+ case 'always':
238
+ return '✅ Permission **accepted** (auto-approve similar requests)';
239
+ case 'reject':
240
+ return '❌ Permission **rejected**';
241
+ }
242
+ })();
243
+ updatePermissionMessage({
244
+ context,
245
+ status: resultText,
246
+ });
247
+ logger.log(`Permission ${context.permission.id} ${response} (${requestIds.length} request(s))`);
248
+ }
249
+ catch (error) {
250
+ logger.error('Error handling permission:', error);
251
+ await interaction.editReply({
252
+ content: `Failed to process permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
253
+ components: [],
254
+ });
255
+ }
256
+ }
257
+ export function addPermissionRequestToContext({ contextHash, requestId, }) {
258
+ const context = pendingPermissionContexts.get(contextHash);
259
+ if (!context) {
260
+ return false;
261
+ }
262
+ if (context.requestIds.includes(requestId)) {
263
+ return false;
264
+ }
265
+ context.requestIds = [...context.requestIds, requestId];
266
+ pendingPermissionContexts.set(contextHash, context);
267
+ return true;
268
+ }
269
+ /**
270
+ * Clean up a pending permission context (e.g., on auto-reject).
271
+ */
272
+ export function cleanupPermissionContext(contextHash) {
273
+ pendingPermissionContexts.delete(contextHash);
274
+ }
@@ -0,0 +1,223 @@
1
+ // Queue commands - /queue, /queue-command, /clear-queue
2
+ import { ChannelType, MessageFlags } from 'discord.js';
3
+ import { getThreadSession } from '../database.js';
4
+ import { resolveWorkingDirectory, sendThreadMessage, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
5
+ import { getRuntime, getOrCreateRuntime, } from '../session-handler/thread-session-runtime.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ import { notifyError } from '../sentry.js';
8
+ import { store } from '../store.js';
9
+ const logger = createLogger(LogPrefix.QUEUE);
10
+ export async function handleQueueCommand({ command, appId, }) {
11
+ const message = command.options.getString('message', true);
12
+ const channel = command.channel;
13
+ if (!channel) {
14
+ await command.reply({
15
+ content: 'This command can only be used in a channel',
16
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
17
+ });
18
+ return;
19
+ }
20
+ const isThread = [
21
+ ChannelType.PublicThread,
22
+ ChannelType.PrivateThread,
23
+ ChannelType.AnnouncementThread,
24
+ ].includes(channel.type);
25
+ if (!isThread) {
26
+ await command.reply({
27
+ content: 'This command can only be used in a thread with an active session',
28
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
29
+ });
30
+ return;
31
+ }
32
+ const thread = channel;
33
+ const sessionId = await getThreadSession(thread.id);
34
+ if (!sessionId) {
35
+ await command.reply({
36
+ content: 'No active session in this thread. Send a message directly instead.',
37
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
38
+ });
39
+ return;
40
+ }
41
+ const resolved = await resolveWorkingDirectory({ channel: thread });
42
+ if (!resolved) {
43
+ await command.reply({
44
+ content: 'Could not determine project directory',
45
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
46
+ });
47
+ return;
48
+ }
49
+ const runtime = getOrCreateRuntime({
50
+ threadId: thread.id,
51
+ thread,
52
+ projectDirectory: resolved.projectDirectory,
53
+ sdkDirectory: resolved.workingDirectory,
54
+ channelId: thread.parentId || thread.id,
55
+ appId,
56
+ });
57
+ // /queue explicitly uses otto local queue mode.
58
+ const enqueueResult = await runtime.enqueueIncoming({
59
+ prompt: message,
60
+ userId: command.user.id,
61
+ username: command.user.displayName,
62
+ appId,
63
+ mode: 'local-queue',
64
+ });
65
+ const responseText = enqueueResult.queued
66
+ ? `Queued message${enqueueResult.position ? ` (position ${enqueueResult.position})` : ''}`
67
+ : `» **${command.user.displayName}:** ${message.slice(0, 1000)}${message.length > 1000 ? '...' : ''}`;
68
+ await command.reply({
69
+ content: responseText,
70
+ flags: SILENT_MESSAGE_FLAGS,
71
+ });
72
+ }
73
+ export async function handleClearQueueCommand({ command, }) {
74
+ const channel = command.channel;
75
+ const position = command.options.getInteger('position') ?? undefined;
76
+ if (!channel) {
77
+ await command.reply({
78
+ content: 'This command can only be used in a channel',
79
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
80
+ });
81
+ return;
82
+ }
83
+ const isThread = [
84
+ ChannelType.PublicThread,
85
+ ChannelType.PrivateThread,
86
+ ChannelType.AnnouncementThread,
87
+ ].includes(channel.type);
88
+ if (!isThread) {
89
+ await command.reply({
90
+ content: 'This command can only be used in a thread',
91
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
92
+ });
93
+ return;
94
+ }
95
+ const runtime = getRuntime(channel.id);
96
+ const queueLength = runtime?.getQueueLength() ?? 0;
97
+ if (queueLength === 0) {
98
+ await command.reply({
99
+ content: 'No messages in queue',
100
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
101
+ });
102
+ return;
103
+ }
104
+ if (position !== undefined) {
105
+ const removed = runtime?.removeQueuePosition(position);
106
+ if (!removed) {
107
+ await command.reply({
108
+ content: `No queued message at position ${position}`,
109
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
110
+ });
111
+ return;
112
+ }
113
+ await command.reply({
114
+ content: `Cleared queued message at position ${position}`,
115
+ flags: SILENT_MESSAGE_FLAGS,
116
+ });
117
+ logger.log(`[QUEUE] User ${command.user.displayName} cleared queued position ${position} in thread ${channel.id}`);
118
+ return;
119
+ }
120
+ runtime?.clearQueue();
121
+ await command.reply({
122
+ content: `Cleared ${queueLength} queued message${queueLength > 1 ? 's' : ''}`,
123
+ flags: SILENT_MESSAGE_FLAGS,
124
+ });
125
+ logger.log(`[QUEUE] User ${command.user.displayName} cleared queue in thread ${channel.id}`);
126
+ }
127
+ export async function handleQueueCommandCommand({ command, appId, }) {
128
+ const commandName = command.options.getString('command', true);
129
+ const args = command.options.getString('arguments') || '';
130
+ const channel = command.channel;
131
+ if (!channel) {
132
+ await command.reply({
133
+ content: 'This command can only be used in a channel',
134
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
135
+ });
136
+ return;
137
+ }
138
+ const isThread = [
139
+ ChannelType.PublicThread,
140
+ ChannelType.PrivateThread,
141
+ ChannelType.AnnouncementThread,
142
+ ].includes(channel.type);
143
+ if (!isThread) {
144
+ await command.reply({
145
+ content: 'This command can only be used in a thread with an active session',
146
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
147
+ });
148
+ return;
149
+ }
150
+ const sessionId = await getThreadSession(channel.id);
151
+ if (!sessionId) {
152
+ await command.reply({
153
+ content: 'No active session in this thread. Send a message directly instead.',
154
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
155
+ });
156
+ return;
157
+ }
158
+ // Validate command exists in registered user commands
159
+ const isKnownCommand = store.getState().registeredUserCommands.some((cmd) => {
160
+ return cmd.name === commandName;
161
+ });
162
+ if (!isKnownCommand) {
163
+ await command.reply({
164
+ content: `Unknown command: /${commandName}. Use autocomplete to pick from available commands.`,
165
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
166
+ });
167
+ return;
168
+ }
169
+ const commandPayload = { name: commandName, arguments: args };
170
+ const displayText = `/${commandName}`;
171
+ const thread = channel;
172
+ const resolved = await resolveWorkingDirectory({ channel: thread });
173
+ if (!resolved) {
174
+ await command.reply({
175
+ content: 'Could not determine project directory',
176
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
177
+ });
178
+ return;
179
+ }
180
+ const runtime = getOrCreateRuntime({
181
+ threadId: thread.id,
182
+ thread,
183
+ projectDirectory: resolved.projectDirectory,
184
+ sdkDirectory: resolved.workingDirectory,
185
+ channelId: thread.parentId || thread.id,
186
+ appId,
187
+ });
188
+ // /queue-command explicitly uses otto local queue mode.
189
+ const enqueueResult = await runtime.enqueueIncoming({
190
+ prompt: '',
191
+ userId: command.user.id,
192
+ username: command.user.displayName,
193
+ appId,
194
+ command: commandPayload,
195
+ mode: 'local-queue',
196
+ });
197
+ const responseText = enqueueResult.queued
198
+ ? `Queued message${enqueueResult.position ? ` (position ${enqueueResult.position})` : ''}`
199
+ : `» **${command.user.displayName}:** ${displayText}`;
200
+ await command.reply({
201
+ content: responseText,
202
+ flags: SILENT_MESSAGE_FLAGS,
203
+ });
204
+ logger.log(`[QUEUE] User ${command.user.displayName} queued command /${commandName} in thread ${channel.id}`);
205
+ }
206
+ export async function handleQueueCommandAutocomplete({ interaction, }) {
207
+ const focused = interaction.options.getFocused(true);
208
+ if (focused.name !== 'command') {
209
+ await interaction.respond([]);
210
+ return;
211
+ }
212
+ const query = focused.value.toLowerCase();
213
+ const choices = store.getState().registeredUserCommands
214
+ .filter((cmd) => {
215
+ return cmd.name.toLowerCase().includes(query);
216
+ })
217
+ .slice(0, 25)
218
+ .map((cmd) => ({
219
+ name: `/${cmd.name} [${cmd.source === 'skill' ? 'skill' : cmd.source === 'mcp' ? 'mcp' : 'cmd'}] - ${cmd.description}`.slice(0, 100),
220
+ value: cmd.name.slice(0, 100),
221
+ }));
222
+ await interaction.respond(choices);
223
+ }
@@ -0,0 +1,115 @@
1
+ // /remove-project command - Remove Discord channels for a project.
2
+ import path from 'node:path';
3
+ import * as errore from 'errore';
4
+ import { findChannelsByDirectory, deleteChannelDirectoriesByDirectory, getAllTextChannelDirectories, } from '../database.js';
5
+ import { createLogger, LogPrefix } from '../logger.js';
6
+ import { abbreviatePath } from '../utils.js';
7
+ const logger = createLogger(LogPrefix.REMOVE_PROJECT);
8
+ export async function handleRemoveProjectCommand({ command, appId, }) {
9
+ await command.deferReply();
10
+ const directory = command.options.getString('project', true);
11
+ const guild = command.guild;
12
+ if (!guild) {
13
+ await command.editReply('This command can only be used in a guild');
14
+ return;
15
+ }
16
+ try {
17
+ // Get channel IDs for this directory
18
+ const channels = await findChannelsByDirectory({ directory });
19
+ if (channels.length === 0) {
20
+ await command.editReply(`No channels found for directory: \`${directory}\``);
21
+ return;
22
+ }
23
+ const deletedChannels = [];
24
+ const failedChannels = [];
25
+ for (const { channel_id, channel_type } of channels) {
26
+ const channel = await errore.tryAsync({
27
+ try: () => guild.channels.fetch(channel_id),
28
+ catch: (e) => e,
29
+ });
30
+ if (channel instanceof Error) {
31
+ logger.error(`Failed to fetch channel ${channel_id}:`, channel);
32
+ failedChannels.push(`${channel_type}: ${channel_id}`);
33
+ continue;
34
+ }
35
+ if (channel) {
36
+ try {
37
+ await channel.delete(`Removed by /remove-project command`);
38
+ deletedChannels.push(`${channel_type}: ${channel_id}`);
39
+ }
40
+ catch (error) {
41
+ logger.error(`Failed to delete channel ${channel_id}:`, error);
42
+ failedChannels.push(`${channel_type}: ${channel_id}`);
43
+ }
44
+ }
45
+ else {
46
+ deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`);
47
+ }
48
+ }
49
+ // Remove from database
50
+ await deleteChannelDirectoriesByDirectory(directory);
51
+ const projectName = path.basename(directory);
52
+ let message = `Removed project **${projectName}**\n`;
53
+ message += `Directory: \`${directory}\`\n\n`;
54
+ if (deletedChannels.length > 0) {
55
+ message += `Deleted channels:\n${deletedChannels.map((c) => `- ${c}`).join('\n')}`;
56
+ }
57
+ if (failedChannels.length > 0) {
58
+ message += `\n\nFailed to delete (may be in another server):\n${failedChannels.map((c) => `- ${c}`).join('\n')}`;
59
+ }
60
+ await command.editReply(message);
61
+ logger.log(`Removed project ${projectName} at ${directory}`);
62
+ }
63
+ catch (error) {
64
+ logger.error('[REMOVE-PROJECT] Error:', error);
65
+ await command.editReply(`Failed to remove project: ${error instanceof Error ? error.message : 'Unknown error'}`);
66
+ }
67
+ }
68
+ export async function handleRemoveProjectAutocomplete({ interaction, appId, }) {
69
+ const focusedValue = interaction.options.getFocused();
70
+ const guild = interaction.guild;
71
+ if (!guild) {
72
+ await interaction.respond([]);
73
+ return;
74
+ }
75
+ try {
76
+ // Get all directories with channels
77
+ const allChannels = (await findChannelsByDirectory({
78
+ channelType: 'text',
79
+ }));
80
+ // Filter to only channels that exist in this guild
81
+ const projectsInGuild = [];
82
+ for (const { directory, channel_id } of allChannels) {
83
+ const channel = await errore.tryAsync({
84
+ try: () => guild.channels.fetch(channel_id),
85
+ catch: (e) => e,
86
+ });
87
+ if (channel instanceof Error) {
88
+ // Channel not in this guild, skip
89
+ continue;
90
+ }
91
+ if (channel) {
92
+ projectsInGuild.push({ directory, channelId: channel_id });
93
+ }
94
+ }
95
+ const projects = projectsInGuild
96
+ .filter(({ directory }) => {
97
+ const baseName = path.basename(directory);
98
+ const searchText = `${baseName} ${directory}`.toLowerCase();
99
+ return searchText.includes(focusedValue.toLowerCase());
100
+ })
101
+ .slice(0, 25)
102
+ .map(({ directory }) => {
103
+ const name = `${path.basename(directory)} (${abbreviatePath(directory)})`;
104
+ return {
105
+ name: name.length > 100 ? name.slice(0, 99) + '...' : name,
106
+ value: directory,
107
+ };
108
+ });
109
+ await interaction.respond(projects);
110
+ }
111
+ catch (error) {
112
+ logger.error('[AUTOCOMPLETE] Error fetching projects:', error);
113
+ await interaction.respond([]);
114
+ }
115
+ }