@otto-assistant/otto 0.1.2 → 0.7.16

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