@otto-assistant/otto 0.1.2 → 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 (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 +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/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,752 @@
1
+ // Discord slash command registration logic, extracted from cli.ts to avoid
2
+ // circular dependencies (cli → discord-bot → interaction-handler → command → cli).
3
+ // Imported by both cli.ts (startup registration) and restart-opencode-server.ts
4
+ // (post-restart re-registration).
5
+
6
+ import {
7
+ type REST,
8
+ Routes,
9
+ SlashCommandBuilder,
10
+ } from 'discord.js'
11
+ import type { Command as OpencodeCommand } from '@opencode-ai/sdk/v2'
12
+ import { createDiscordRest } from './discord-urls.js'
13
+ import { createLogger, LogPrefix } from './logger.js'
14
+ import { store, type RegisteredUserCommand } from './store.js'
15
+ import {
16
+ sanitizeAgentName,
17
+ buildQuickAgentCommandDescription,
18
+ } from './commands/agent.js'
19
+ import { THREAD_DELETION_SYNC_CHOICES } from './commands/thread-deletion-sync.js'
20
+
21
+ const cliLogger = createLogger(LogPrefix.CLI)
22
+
23
+ // Commands to skip when registering user commands (reserved names)
24
+ export const SKIP_USER_COMMANDS = ['init']
25
+
26
+ export type AgentInfo = {
27
+ name: string
28
+ description?: string
29
+ mode: string
30
+ hidden?: boolean
31
+ }
32
+
33
+ function getDiscordCommandSuffix(
34
+ command: OpencodeCommand,
35
+ ): '-cmd' | '-skill' | '-mcp-prompt' {
36
+ if (command.source === 'skill') {
37
+ return '-skill'
38
+ }
39
+ if (command.source === 'mcp') {
40
+ return '-mcp-prompt'
41
+ }
42
+ return '-cmd'
43
+ }
44
+
45
+ type DiscordCommandSummary = {
46
+ id: string
47
+ name: string
48
+ }
49
+
50
+ function isDiscordCommandSummary(value: unknown): value is DiscordCommandSummary {
51
+ if (typeof value !== 'object' || value === null) {
52
+ return false
53
+ }
54
+
55
+ const id = Reflect.get(value, 'id')
56
+ const name = Reflect.get(value, 'name')
57
+ return typeof id === 'string' && typeof name === 'string'
58
+ }
59
+
60
+ async function deleteLegacyGlobalCommands({
61
+ rest,
62
+ appId,
63
+ commandNames,
64
+ }: {
65
+ rest: REST
66
+ appId: string
67
+ commandNames: Set<string>
68
+ }) {
69
+ try {
70
+ const response = await rest.get(Routes.applicationCommands(appId))
71
+ if (!Array.isArray(response)) {
72
+ cliLogger.warn(
73
+ 'COMMANDS: Unexpected global command payload while cleaning legacy global commands',
74
+ )
75
+ return
76
+ }
77
+
78
+ const legacyGlobalCommands = response
79
+ .filter(isDiscordCommandSummary)
80
+ .filter((command) => {
81
+ return commandNames.has(command.name)
82
+ })
83
+
84
+ if (legacyGlobalCommands.length === 0) {
85
+ return
86
+ }
87
+
88
+ const deletionResults = await Promise.allSettled(
89
+ legacyGlobalCommands.map(async (command) => {
90
+ await rest.delete(Routes.applicationCommand(appId, command.id))
91
+ return command
92
+ }),
93
+ )
94
+
95
+ const failedDeletions = deletionResults.filter((result) => {
96
+ return result.status === 'rejected'
97
+ })
98
+ if (failedDeletions.length > 0) {
99
+ cliLogger.warn(
100
+ `COMMANDS: Failed to delete ${failedDeletions.length} legacy global command(s)`,
101
+ )
102
+ }
103
+
104
+ const deletedCount = deletionResults.length - failedDeletions.length
105
+ if (deletedCount > 0) {
106
+ cliLogger.info(
107
+ `COMMANDS: Deleted ${deletedCount} legacy global command(s) to avoid guild/global duplicates`,
108
+ )
109
+ }
110
+ } catch (error) {
111
+ cliLogger.warn(
112
+ `COMMANDS: Could not clean legacy global commands: ${error instanceof Error ? error.stack : String(error)}`,
113
+ )
114
+ }
115
+ }
116
+
117
+ // Discord slash command descriptions must be 1-100 chars.
118
+ // Truncate to 100 so @sapphire/shapeshift validation never throws.
119
+ function truncateCommandDescription(description: string): string {
120
+ return description.slice(0, 100)
121
+ }
122
+
123
+ export async function registerCommands({
124
+ token,
125
+ appId,
126
+ guildIds,
127
+ userCommands = [],
128
+ agents = [],
129
+ }: {
130
+ token: string
131
+ appId: string
132
+ guildIds: string[]
133
+ userCommands?: OpencodeCommand[]
134
+ agents?: AgentInfo[]
135
+ }) {
136
+ const commands = [
137
+ new SlashCommandBuilder()
138
+ .setName('resume')
139
+ .setDescription(truncateCommandDescription('Resume an existing OpenCode session'))
140
+ .addStringOption((option) => {
141
+ option
142
+ .setName('session')
143
+ .setDescription(truncateCommandDescription('The session to resume'))
144
+ .setRequired(true)
145
+ .setAutocomplete(true)
146
+
147
+ return option
148
+ })
149
+ .setDMPermission(false)
150
+ .toJSON(),
151
+ new SlashCommandBuilder()
152
+ .setName('new-session')
153
+ .setDescription(truncateCommandDescription('Start a new OpenCode session'))
154
+ .addStringOption((option) => {
155
+ option
156
+ .setName('prompt')
157
+ .setDescription(truncateCommandDescription('Prompt content for the session'))
158
+ .setRequired(true)
159
+
160
+ return option
161
+ })
162
+ .addStringOption((option) => {
163
+ option
164
+ .setName('files')
165
+ .setDescription(
166
+ truncateCommandDescription('Files to mention (comma or space separated; autocomplete)'),
167
+ )
168
+ .setAutocomplete(true)
169
+ .setMaxLength(6000)
170
+
171
+ return option
172
+ })
173
+ .addStringOption((option) => {
174
+ option
175
+ .setName('agent')
176
+ .setDescription(truncateCommandDescription('Agent to use for this session'))
177
+ .setAutocomplete(true)
178
+
179
+ return option
180
+ })
181
+ .setDMPermission(false)
182
+ .toJSON(),
183
+ new SlashCommandBuilder()
184
+ .setName('new-worktree')
185
+ .setDescription(
186
+ truncateCommandDescription('Create a git worktree from the current HEAD by default. Optionally pick a base branch.'),
187
+ )
188
+ .addStringOption((option) => {
189
+ option
190
+ .setName('name')
191
+ .setDescription(
192
+ truncateCommandDescription('Name for worktree (optional in threads - uses thread name)'),
193
+ )
194
+ .setRequired(false)
195
+
196
+ return option
197
+ })
198
+ .addStringOption((option) => {
199
+ option
200
+ .setName('base-branch')
201
+ .setDescription(
202
+ truncateCommandDescription('Branch to create the worktree from (default: current HEAD)'),
203
+ )
204
+ .setRequired(false)
205
+ .setAutocomplete(true)
206
+
207
+ return option
208
+ })
209
+ .setDMPermission(false)
210
+ .toJSON(),
211
+ new SlashCommandBuilder()
212
+ .setName('merge-worktree')
213
+ .setDescription(
214
+ truncateCommandDescription('Squash-merge worktree into default branch. Aborts if main has uncommitted changes.'),
215
+ )
216
+ .addStringOption((option) => {
217
+ option
218
+ .setName('target-branch')
219
+ .setDescription(
220
+ truncateCommandDescription('Branch to merge into (default: origin/HEAD or main)'),
221
+ )
222
+ .setRequired(false)
223
+ .setAutocomplete(true)
224
+
225
+ return option
226
+ })
227
+ .setDMPermission(false)
228
+ .toJSON(),
229
+ new SlashCommandBuilder()
230
+ .setName('toggle-worktrees')
231
+ .setDescription(
232
+ truncateCommandDescription('Toggle automatic git worktree creation for new sessions in this channel'),
233
+ )
234
+ .setDMPermission(false)
235
+ .toJSON(),
236
+ new SlashCommandBuilder()
237
+ .setName('worktrees')
238
+ .setDescription(truncateCommandDescription('List all active worktree sessions'))
239
+ .setDMPermission(false)
240
+ .toJSON(),
241
+ new SlashCommandBuilder()
242
+ .setName('tasks')
243
+ .setDescription(truncateCommandDescription('List scheduled tasks created via send --send-at'))
244
+ .addBooleanOption((option) => {
245
+ return option
246
+ .setName('all')
247
+ .setDescription(
248
+ truncateCommandDescription('Include completed, cancelled, and failed tasks'),
249
+ )
250
+ })
251
+ .setDMPermission(false)
252
+ .toJSON(),
253
+
254
+ new SlashCommandBuilder()
255
+ .setName('add-project')
256
+ .setDescription(
257
+ truncateCommandDescription('Create Discord channels for a project. Use `npx otto project add` for unlisted projects'),
258
+ )
259
+ .addStringOption((option) => {
260
+ option
261
+ .setName('project')
262
+ .setDescription(
263
+ truncateCommandDescription('Recent OpenCode projects. Use `npx otto project add` if not listed'),
264
+ )
265
+ .setRequired(true)
266
+ .setAutocomplete(true)
267
+
268
+ return option
269
+ })
270
+ .setDMPermission(false)
271
+ .toJSON(),
272
+ new SlashCommandBuilder()
273
+ .setName('remove-project')
274
+ .setDescription(truncateCommandDescription('Remove Discord channels for a project'))
275
+ .addStringOption((option) => {
276
+ option
277
+ .setName('project')
278
+ .setDescription(truncateCommandDescription('Select a project to remove'))
279
+ .setRequired(true)
280
+ .setAutocomplete(true)
281
+
282
+ return option
283
+ })
284
+ .setDMPermission(false)
285
+ .toJSON(),
286
+ new SlashCommandBuilder()
287
+ .setName('create-new-project')
288
+ .setDescription(
289
+ truncateCommandDescription('Create a new project folder, initialize git, and start a session'),
290
+ )
291
+ .addStringOption((option) => {
292
+ option
293
+ .setName('name')
294
+ .setDescription(truncateCommandDescription('Name for the new project folder'))
295
+ .setRequired(true)
296
+
297
+ return option
298
+ })
299
+ .setDMPermission(false)
300
+ .toJSON(),
301
+ new SlashCommandBuilder()
302
+ .setName('add-dir')
303
+ .setDescription(
304
+ truncateCommandDescription('Allow the current session to access an extra directory or * for all folders'),
305
+ )
306
+ .addStringOption((option) => {
307
+ option
308
+ .setName('directory')
309
+ .setDescription(truncateCommandDescription('Directory to allow, resolved from the current worktree. Use * for all folders'))
310
+ .setRequired(false)
311
+
312
+ return option
313
+ })
314
+ .setDMPermission(false)
315
+ .toJSON(),
316
+ new SlashCommandBuilder()
317
+ .setName('abort')
318
+ .setDescription(truncateCommandDescription('Abort the current OpenCode request in this thread'))
319
+ .setDMPermission(false)
320
+ .toJSON(),
321
+ new SlashCommandBuilder()
322
+ .setName('compact')
323
+ .setDescription(
324
+ truncateCommandDescription('Compact the session context by summarizing conversation history'),
325
+ )
326
+ .setDMPermission(false)
327
+ .toJSON(),
328
+
329
+ new SlashCommandBuilder()
330
+ .setName('share')
331
+ .setDescription(truncateCommandDescription('Share the current session as a public URL'))
332
+ .setDMPermission(false)
333
+ .toJSON(),
334
+ new SlashCommandBuilder()
335
+ .setName('diff')
336
+ .setDescription(truncateCommandDescription('Show git diff as a shareable URL'))
337
+ .setDMPermission(false)
338
+ .toJSON(),
339
+ new SlashCommandBuilder()
340
+ .setName('fork')
341
+ .setDescription(truncateCommandDescription('Fork the session from a past user message'))
342
+ .setDMPermission(false)
343
+ .toJSON(),
344
+ new SlashCommandBuilder()
345
+ .setName('fork-subagent')
346
+ .setDescription(truncateCommandDescription('Fork a subagent task session into a new thread'))
347
+ .setDMPermission(false)
348
+ .toJSON(),
349
+ new SlashCommandBuilder()
350
+ .setName('btw')
351
+ .setDescription(truncateCommandDescription('Ask something without polluting or blocking the current session'))
352
+ .addStringOption((option) => {
353
+ option
354
+ .setName('prompt')
355
+ .setDescription(truncateCommandDescription('The message to send in the forked session'))
356
+ .setRequired(true)
357
+ return option
358
+ })
359
+ .setDMPermission(false)
360
+ .toJSON(),
361
+ new SlashCommandBuilder()
362
+ .setName('model')
363
+ .setDescription(truncateCommandDescription('Set the preferred model for this channel or session'))
364
+ .setDMPermission(false)
365
+ .toJSON(),
366
+ new SlashCommandBuilder()
367
+ .setName('model-variant')
368
+ .setDescription(
369
+ truncateCommandDescription('Change thinking level for current model. Tied to the model; lost when you switch models'),
370
+ )
371
+ .setDMPermission(false)
372
+ .toJSON(),
373
+ new SlashCommandBuilder()
374
+ .setName('unset-model-override')
375
+ .setDescription(truncateCommandDescription('Remove model override and use default instead'))
376
+ .setDMPermission(false)
377
+ .toJSON(),
378
+ new SlashCommandBuilder()
379
+ .setName('login')
380
+ .setDescription(
381
+ truncateCommandDescription('Authenticate with an AI provider (OAuth or API key). Use this instead of /connect'),
382
+ )
383
+ .setDMPermission(false)
384
+ .toJSON(),
385
+ new SlashCommandBuilder()
386
+ .setName('agent')
387
+ .setDescription(truncateCommandDescription('Set the preferred agent for this channel or session'))
388
+ .setDMPermission(false)
389
+ .toJSON(),
390
+ new SlashCommandBuilder()
391
+ .setName('queue')
392
+ .setDescription(
393
+ truncateCommandDescription('Queue a message to be sent after the current response finishes'),
394
+ )
395
+ .addStringOption((option) => {
396
+ option
397
+ .setName('message')
398
+ .setDescription(truncateCommandDescription('The message to queue'))
399
+ .setRequired(true)
400
+
401
+ return option
402
+ })
403
+ .setDMPermission(false)
404
+ .toJSON(),
405
+ new SlashCommandBuilder()
406
+ .setName('clear-queue')
407
+ .setDescription(truncateCommandDescription('Clear all queued messages in this thread'))
408
+ .addIntegerOption((option) => {
409
+ option
410
+ .setName('position')
411
+ .setDescription(
412
+ truncateCommandDescription('1-based queued message position to clear (default: all)'),
413
+ )
414
+ .setMinValue(1)
415
+
416
+ return option
417
+ })
418
+ .setDMPermission(false)
419
+ .toJSON(),
420
+ new SlashCommandBuilder()
421
+ .setName('queue-command')
422
+ .setDescription(
423
+ truncateCommandDescription('Queue a user command to run after the current response finishes'),
424
+ )
425
+ .addStringOption((option) => {
426
+ option
427
+ .setName('command')
428
+ .setDescription(truncateCommandDescription('The command to run'))
429
+ .setRequired(true)
430
+ .setAutocomplete(true)
431
+ return option
432
+ })
433
+ .addStringOption((option) => {
434
+ option
435
+ .setName('arguments')
436
+ .setDescription(truncateCommandDescription('Arguments to pass to the command'))
437
+ .setRequired(false)
438
+ return option
439
+ })
440
+ .setDMPermission(false)
441
+ .toJSON(),
442
+ new SlashCommandBuilder()
443
+ .setName('undo')
444
+ .setDescription(truncateCommandDescription('Undo the last assistant message (revert file changes)'))
445
+ .setDMPermission(false)
446
+ .toJSON(),
447
+ new SlashCommandBuilder()
448
+ .setName('redo')
449
+ .setDescription(truncateCommandDescription('Redo previously undone changes'))
450
+ .setDMPermission(false)
451
+ .toJSON(),
452
+ new SlashCommandBuilder()
453
+ .setName('verbosity')
454
+ .setDescription(truncateCommandDescription('Set output verbosity for this channel'))
455
+ .setDMPermission(false)
456
+ .toJSON(),
457
+ new SlashCommandBuilder()
458
+ .setName('thread-deletion-sync')
459
+ .setDescription(
460
+ truncateCommandDescription(
461
+ 'Set how Discord thread deletion syncs to OpenCode sessions',
462
+ ),
463
+ )
464
+ .addStringOption((option) => {
465
+ option
466
+ .setName('mode')
467
+ .setDescription(
468
+ truncateCommandDescription(
469
+ 'soft, hard, or reset to default soft mode',
470
+ ),
471
+ )
472
+ .addChoices(...THREAD_DELETION_SYNC_CHOICES)
473
+ .setRequired(false)
474
+ return option
475
+ })
476
+ .setDMPermission(false)
477
+ .toJSON(),
478
+ new SlashCommandBuilder()
479
+ .setName('restart-opencode-server')
480
+ .setDescription(
481
+ truncateCommandDescription('Restart opencode server and re-register slash commands'),
482
+ )
483
+ .setDMPermission(false)
484
+ .toJSON(),
485
+ new SlashCommandBuilder()
486
+ .setName('run-shell-command')
487
+ .setDescription(
488
+ truncateCommandDescription('Run a shell command in the project directory. Tip: prefix messages with ! as shortcut'),
489
+ )
490
+ .addStringOption((option) => {
491
+ option
492
+ .setName('command')
493
+ .setDescription(truncateCommandDescription('Command to run'))
494
+ .setRequired(true)
495
+ return option
496
+ })
497
+ .setDMPermission(false)
498
+ .toJSON(),
499
+ new SlashCommandBuilder()
500
+ .setName('context-usage')
501
+ .setDescription(
502
+ truncateCommandDescription('Show token usage and context window percentage for this session'),
503
+ )
504
+ .setDMPermission(false)
505
+ .toJSON(),
506
+ new SlashCommandBuilder()
507
+ .setName('session-id')
508
+ .setDescription(
509
+ truncateCommandDescription('Show current session ID and opencode attach command for this thread'),
510
+ )
511
+ .setDMPermission(false)
512
+ .toJSON(),
513
+
514
+ new SlashCommandBuilder()
515
+ .setName('upgrade-and-restart')
516
+ .setDescription(
517
+ truncateCommandDescription('Upgrade otto to the latest version and restart the bot'),
518
+ )
519
+ .setDMPermission(false)
520
+ .toJSON(),
521
+ new SlashCommandBuilder()
522
+ .setName('transcription-key')
523
+ .setDescription(
524
+ truncateCommandDescription('Set API key for voice message transcription (OpenAI or Gemini)'),
525
+ )
526
+ .setDMPermission(false)
527
+ .toJSON(),
528
+ new SlashCommandBuilder()
529
+ .setName('mcp')
530
+ .setDescription(truncateCommandDescription('List and manage MCP servers for this project'))
531
+ .setDMPermission(false)
532
+ .toJSON(),
533
+ new SlashCommandBuilder()
534
+ .setName('screenshare')
535
+ .setDescription(truncateCommandDescription('Start screen sharing via VNC tunnel (auto-stops after 30 minutes)'))
536
+ .setDMPermission(false)
537
+ .toJSON(),
538
+ new SlashCommandBuilder()
539
+ .setName('screenshare-stop')
540
+ .setDescription(truncateCommandDescription('Stop screen sharing'))
541
+ .setDMPermission(false)
542
+ .toJSON(),
543
+ new SlashCommandBuilder()
544
+ .setName('vscode')
545
+ .setDescription(
546
+ truncateCommandDescription('Open VS Code in the browser for this project or worktree (auto-stops after 30 minutes)'),
547
+ )
548
+ .setDMPermission(false)
549
+ .toJSON(),
550
+ ]
551
+
552
+ // Dynamic commands are registered in priority order: agents → user commands → skills → MCP prompts.
553
+ // This ordering matters because we slice to MAX_DISCORD_COMMANDS (100) at the end,
554
+ // so lower-priority dynamic commands get trimmed first if the total exceeds the limit.
555
+
556
+ // 1. Agent-specific quick commands like /plan-agent, /build-agent
557
+ // Filter to primary/all mode agents (same as /agent command shows), excluding hidden agents
558
+ const primaryAgents = agents.filter(
559
+ (a) => (a.mode === 'primary' || a.mode === 'all') && !a.hidden,
560
+ )
561
+ for (const agent of primaryAgents) {
562
+ const sanitizedName = sanitizeAgentName(agent.name)
563
+ // Skip if sanitized name is empty or would create invalid command name
564
+ // Discord command names must start with a lowercase letter or number
565
+ if (!sanitizedName || !/^[a-z0-9]/.test(sanitizedName)) {
566
+ continue
567
+ }
568
+ // Truncate base name before appending suffix so the -agent suffix is never
569
+ // lost to Discord's 32-char command name limit.
570
+ const agentSuffix = '-agent'
571
+ const agentBaseName = sanitizedName.slice(0, 32 - agentSuffix.length)
572
+ const commandName = `${agentBaseName}${agentSuffix}`
573
+ const description = buildQuickAgentCommandDescription({
574
+ agentName: agent.name,
575
+ description: agent.description,
576
+ })
577
+
578
+ commands.push(
579
+ new SlashCommandBuilder()
580
+ .setName(commandName)
581
+ .setDescription(truncateCommandDescription(description))
582
+ .setDMPermission(false)
583
+ .toJSON(),
584
+ )
585
+ }
586
+
587
+ // 2. User-defined commands, skills, and MCP prompts (ordered by priority)
588
+ // Also populate registeredUserCommands in the store for /queue-command autocomplete
589
+ const newRegisteredCommands: RegisteredUserCommand[] = []
590
+ // Sort: regular commands first, then skills, then MCP prompts
591
+ const sourceOrder: Record<string, number> = { config: 0, skill: 1, mcp: 2 }
592
+ const sortedUserCommands = [...userCommands].sort((a, b) => {
593
+ return (sourceOrder[a.source || ''] ?? 0) - (sourceOrder[b.source || ''] ?? 0)
594
+ })
595
+ for (const cmd of sortedUserCommands) {
596
+ if (SKIP_USER_COMMANDS.includes(cmd.name)) {
597
+ continue
598
+ }
599
+
600
+ // Sanitize command name: oh-my-opencode uses MCP commands with colons and slashes,
601
+ // which Discord doesn't allow in command names.
602
+ // Discord command names: lowercase, alphanumeric and hyphens only, must start with letter/number.
603
+ const sanitizedName = cmd.name
604
+ .toLowerCase()
605
+ .replace(/[:/]/g, '-') // Replace : and / with hyphens first
606
+ .replace(/[^a-z0-9-]/g, '-') // Replace any other non-alphanumeric chars
607
+ .replace(/-+/g, '-') // Collapse multiple hyphens
608
+ .replace(/^-|-$/g, '') // Remove leading/trailing hyphens
609
+
610
+ // Skip if sanitized name is empty - would create invalid command name like "-cmd"
611
+ if (!sanitizedName) {
612
+ continue
613
+ }
614
+
615
+ const commandSuffix = getDiscordCommandSuffix(cmd)
616
+
617
+ // Truncate base name before appending suffix so the suffix is never
618
+ // lost to Discord's 32-char command name limit.
619
+ const baseName = sanitizedName.slice(0, 32 - commandSuffix.length)
620
+ const commandName = `${baseName}${commandSuffix}`
621
+ const description = cmd.description || `Run /${cmd.name} command`
622
+
623
+ newRegisteredCommands.push({
624
+ name: cmd.name,
625
+ discordCommandName: commandName,
626
+ description,
627
+ source: cmd.source,
628
+ })
629
+
630
+ commands.push(
631
+ new SlashCommandBuilder()
632
+ .setName(commandName)
633
+ .setDescription(truncateCommandDescription(description))
634
+ .addStringOption((option) => {
635
+ option
636
+ .setName('arguments')
637
+ .setDescription(truncateCommandDescription('Arguments to pass to the command'))
638
+ .setRequired(false)
639
+ return option
640
+ })
641
+ .setDMPermission(false)
642
+ .toJSON(),
643
+ )
644
+ }
645
+ store.setState({ registeredUserCommands: newRegisteredCommands })
646
+
647
+ // Discord allows max 100 guild commands. Slice to stay within the limit,
648
+ // trimming lowest-priority dynamic commands (MCP prompts, then skills) first.
649
+ const MAX_DISCORD_COMMANDS = 100
650
+ if (commands.length > MAX_DISCORD_COMMANDS) {
651
+ cliLogger.warn(
652
+ `COMMANDS: ${commands.length} commands exceed Discord limit of ${MAX_DISCORD_COMMANDS}, truncating to ${MAX_DISCORD_COMMANDS}`,
653
+ )
654
+ commands.length = MAX_DISCORD_COMMANDS
655
+ }
656
+
657
+ const rest = createDiscordRest(token)
658
+ const uniqueGuildIds = Array.from(new Set(guildIds.filter((guildId) => guildId)))
659
+ const guildCommandNames = new Set(
660
+ commands
661
+ .map((command) => {
662
+ return command.name
663
+ })
664
+ .filter((name): name is string => {
665
+ return typeof name === 'string'
666
+ }),
667
+ )
668
+
669
+ if (uniqueGuildIds.length === 0) {
670
+ cliLogger.warn('COMMANDS: No guilds available, skipping slash command registration')
671
+ return
672
+ }
673
+
674
+ try {
675
+ // PUT is a bulk overwrite: Discord matches by name, updates changed fields
676
+ // (description, options, etc.) in place, creates new commands, and deletes
677
+ // any not present in the body. No local diffing needed.
678
+ const results = await Promise.allSettled(
679
+ uniqueGuildIds.map(async (guildId) => {
680
+ const response = await rest.put(
681
+ Routes.applicationGuildCommands(appId, guildId),
682
+ {
683
+ body: commands,
684
+ },
685
+ )
686
+
687
+ const registeredCount = Array.isArray(response)
688
+ ? response.length
689
+ : commands.length
690
+
691
+ return { guildId, registeredCount }
692
+ }),
693
+ )
694
+
695
+ const failedGuilds = results
696
+ .map((result, index) => {
697
+ if (result.status === 'fulfilled') {
698
+ return null
699
+ }
700
+
701
+ return {
702
+ guildId: uniqueGuildIds[index],
703
+ error:
704
+ result.reason instanceof Error
705
+ ? result.reason.message
706
+ : String(result.reason),
707
+ }
708
+ })
709
+ .filter((value): value is { guildId: string; error: string } => {
710
+ return value !== null
711
+ })
712
+
713
+ if (failedGuilds.length > 0) {
714
+ failedGuilds.forEach((failure) => {
715
+ cliLogger.warn(
716
+ `COMMANDS: Failed to register slash commands for guild ${failure.guildId}: ${failure.error}`,
717
+ )
718
+ })
719
+ throw new Error(
720
+ `Failed to register slash commands for ${failedGuilds.length} guild(s)`,
721
+ )
722
+ }
723
+
724
+ const successfulGuilds = results.length
725
+ const firstRegisteredCount = results[0]
726
+ const registeredCommandCount =
727
+ firstRegisteredCount && firstRegisteredCount.status === 'fulfilled'
728
+ ? firstRegisteredCount.value.registeredCount
729
+ : commands.length
730
+
731
+ // In gateway mode, global application routes (/applications/{app_id}/commands)
732
+ // are denied by the proxy (DeniedWithoutGuild). Legacy global commands only
733
+ // exist for self-hosted bots that previously registered commands globally.
734
+ const isGateway = store.getState().discordBaseUrl !== 'https://discord.com'
735
+ if (!isGateway) {
736
+ await deleteLegacyGlobalCommands({
737
+ rest,
738
+ appId,
739
+ commandNames: guildCommandNames,
740
+ })
741
+ }
742
+
743
+ cliLogger.info(
744
+ `COMMANDS: Successfully registered ${registeredCommandCount} slash commands for ${successfulGuilds} guild(s)`,
745
+ )
746
+ } catch (error) {
747
+ cliLogger.error(
748
+ 'COMMANDS: Failed to register slash commands: ' + String(error),
749
+ )
750
+ throw error
751
+ }
752
+ }