@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,567 @@
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
+ import { Routes, SlashCommandBuilder, } from 'discord.js';
6
+ import { createDiscordRest } from './discord-urls.js';
7
+ import { createLogger, LogPrefix } from './logger.js';
8
+ import { store } from './store.js';
9
+ import { sanitizeAgentName, buildQuickAgentCommandDescription, } from './commands/agent.js';
10
+ import { THREAD_DELETION_SYNC_CHOICES } from './commands/thread-deletion-sync.js';
11
+ const cliLogger = createLogger(LogPrefix.CLI);
12
+ // Commands to skip when registering user commands (reserved names)
13
+ export const SKIP_USER_COMMANDS = ['init'];
14
+ function getDiscordCommandSuffix(command) {
15
+ if (command.source === 'skill') {
16
+ return '-skill';
17
+ }
18
+ if (command.source === 'mcp') {
19
+ return '-mcp-prompt';
20
+ }
21
+ return '-cmd';
22
+ }
23
+ function isDiscordCommandSummary(value) {
24
+ if (typeof value !== 'object' || value === null) {
25
+ return false;
26
+ }
27
+ const id = Reflect.get(value, 'id');
28
+ const name = Reflect.get(value, 'name');
29
+ return typeof id === 'string' && typeof name === 'string';
30
+ }
31
+ async function deleteLegacyGlobalCommands({ rest, appId, commandNames, }) {
32
+ try {
33
+ const response = await rest.get(Routes.applicationCommands(appId));
34
+ if (!Array.isArray(response)) {
35
+ cliLogger.warn('COMMANDS: Unexpected global command payload while cleaning legacy global commands');
36
+ return;
37
+ }
38
+ const legacyGlobalCommands = response
39
+ .filter(isDiscordCommandSummary)
40
+ .filter((command) => {
41
+ return commandNames.has(command.name);
42
+ });
43
+ if (legacyGlobalCommands.length === 0) {
44
+ return;
45
+ }
46
+ const deletionResults = await Promise.allSettled(legacyGlobalCommands.map(async (command) => {
47
+ await rest.delete(Routes.applicationCommand(appId, command.id));
48
+ return command;
49
+ }));
50
+ const failedDeletions = deletionResults.filter((result) => {
51
+ return result.status === 'rejected';
52
+ });
53
+ if (failedDeletions.length > 0) {
54
+ cliLogger.warn(`COMMANDS: Failed to delete ${failedDeletions.length} legacy global command(s)`);
55
+ }
56
+ const deletedCount = deletionResults.length - failedDeletions.length;
57
+ if (deletedCount > 0) {
58
+ cliLogger.info(`COMMANDS: Deleted ${deletedCount} legacy global command(s) to avoid guild/global duplicates`);
59
+ }
60
+ }
61
+ catch (error) {
62
+ cliLogger.warn(`COMMANDS: Could not clean legacy global commands: ${error instanceof Error ? error.stack : String(error)}`);
63
+ }
64
+ }
65
+ // Discord slash command descriptions must be 1-100 chars.
66
+ // Truncate to 100 so @sapphire/shapeshift validation never throws.
67
+ function truncateCommandDescription(description) {
68
+ return description.slice(0, 100);
69
+ }
70
+ export async function registerCommands({ token, appId, guildIds, userCommands = [], agents = [], }) {
71
+ const commands = [
72
+ new SlashCommandBuilder()
73
+ .setName('resume')
74
+ .setDescription(truncateCommandDescription('Resume an existing OpenCode session'))
75
+ .addStringOption((option) => {
76
+ option
77
+ .setName('session')
78
+ .setDescription(truncateCommandDescription('The session to resume'))
79
+ .setRequired(true)
80
+ .setAutocomplete(true);
81
+ return option;
82
+ })
83
+ .setDMPermission(false)
84
+ .toJSON(),
85
+ new SlashCommandBuilder()
86
+ .setName('new-session')
87
+ .setDescription(truncateCommandDescription('Start a new OpenCode session'))
88
+ .addStringOption((option) => {
89
+ option
90
+ .setName('prompt')
91
+ .setDescription(truncateCommandDescription('Prompt content for the session'))
92
+ .setRequired(true);
93
+ return option;
94
+ })
95
+ .addStringOption((option) => {
96
+ option
97
+ .setName('files')
98
+ .setDescription(truncateCommandDescription('Files to mention (comma or space separated; autocomplete)'))
99
+ .setAutocomplete(true)
100
+ .setMaxLength(6000);
101
+ return option;
102
+ })
103
+ .addStringOption((option) => {
104
+ option
105
+ .setName('agent')
106
+ .setDescription(truncateCommandDescription('Agent to use for this session'))
107
+ .setAutocomplete(true);
108
+ return option;
109
+ })
110
+ .setDMPermission(false)
111
+ .toJSON(),
112
+ new SlashCommandBuilder()
113
+ .setName('new-worktree')
114
+ .setDescription(truncateCommandDescription('Create a git worktree from the current HEAD by default. Optionally pick a base branch.'))
115
+ .addStringOption((option) => {
116
+ option
117
+ .setName('name')
118
+ .setDescription(truncateCommandDescription('Name for worktree (optional in threads - uses thread name)'))
119
+ .setRequired(false);
120
+ return option;
121
+ })
122
+ .addStringOption((option) => {
123
+ option
124
+ .setName('base-branch')
125
+ .setDescription(truncateCommandDescription('Branch to create the worktree from (default: current HEAD)'))
126
+ .setRequired(false)
127
+ .setAutocomplete(true);
128
+ return option;
129
+ })
130
+ .setDMPermission(false)
131
+ .toJSON(),
132
+ new SlashCommandBuilder()
133
+ .setName('merge-worktree')
134
+ .setDescription(truncateCommandDescription('Squash-merge worktree into default branch. Aborts if main has uncommitted changes.'))
135
+ .addStringOption((option) => {
136
+ option
137
+ .setName('target-branch')
138
+ .setDescription(truncateCommandDescription('Branch to merge into (default: origin/HEAD or main)'))
139
+ .setRequired(false)
140
+ .setAutocomplete(true);
141
+ return option;
142
+ })
143
+ .setDMPermission(false)
144
+ .toJSON(),
145
+ new SlashCommandBuilder()
146
+ .setName('toggle-worktrees')
147
+ .setDescription(truncateCommandDescription('Toggle automatic git worktree creation for new sessions in this channel'))
148
+ .setDMPermission(false)
149
+ .toJSON(),
150
+ new SlashCommandBuilder()
151
+ .setName('worktrees')
152
+ .setDescription(truncateCommandDescription('List all active worktree sessions'))
153
+ .setDMPermission(false)
154
+ .toJSON(),
155
+ new SlashCommandBuilder()
156
+ .setName('tasks')
157
+ .setDescription(truncateCommandDescription('List scheduled tasks created via send --send-at'))
158
+ .addBooleanOption((option) => {
159
+ return option
160
+ .setName('all')
161
+ .setDescription(truncateCommandDescription('Include completed, cancelled, and failed tasks'));
162
+ })
163
+ .setDMPermission(false)
164
+ .toJSON(),
165
+ new SlashCommandBuilder()
166
+ .setName('add-project')
167
+ .setDescription(truncateCommandDescription('Create Discord channels for a project. Use `npx otto project add` for unlisted projects'))
168
+ .addStringOption((option) => {
169
+ option
170
+ .setName('project')
171
+ .setDescription(truncateCommandDescription('Recent OpenCode projects. Use `npx otto project add` if not listed'))
172
+ .setRequired(true)
173
+ .setAutocomplete(true);
174
+ return option;
175
+ })
176
+ .setDMPermission(false)
177
+ .toJSON(),
178
+ new SlashCommandBuilder()
179
+ .setName('remove-project')
180
+ .setDescription(truncateCommandDescription('Remove Discord channels for a project'))
181
+ .addStringOption((option) => {
182
+ option
183
+ .setName('project')
184
+ .setDescription(truncateCommandDescription('Select a project to remove'))
185
+ .setRequired(true)
186
+ .setAutocomplete(true);
187
+ return option;
188
+ })
189
+ .setDMPermission(false)
190
+ .toJSON(),
191
+ new SlashCommandBuilder()
192
+ .setName('create-new-project')
193
+ .setDescription(truncateCommandDescription('Create a new project folder, initialize git, and start a session'))
194
+ .addStringOption((option) => {
195
+ option
196
+ .setName('name')
197
+ .setDescription(truncateCommandDescription('Name for the new project folder'))
198
+ .setRequired(true);
199
+ return option;
200
+ })
201
+ .setDMPermission(false)
202
+ .toJSON(),
203
+ new SlashCommandBuilder()
204
+ .setName('add-dir')
205
+ .setDescription(truncateCommandDescription('Allow the current session to access an extra directory or * for all folders'))
206
+ .addStringOption((option) => {
207
+ option
208
+ .setName('directory')
209
+ .setDescription(truncateCommandDescription('Directory to allow, resolved from the current worktree. Use * for all folders'))
210
+ .setRequired(false);
211
+ return option;
212
+ })
213
+ .setDMPermission(false)
214
+ .toJSON(),
215
+ new SlashCommandBuilder()
216
+ .setName('abort')
217
+ .setDescription(truncateCommandDescription('Abort the current OpenCode request in this thread'))
218
+ .setDMPermission(false)
219
+ .toJSON(),
220
+ new SlashCommandBuilder()
221
+ .setName('compact')
222
+ .setDescription(truncateCommandDescription('Compact the session context by summarizing conversation history'))
223
+ .setDMPermission(false)
224
+ .toJSON(),
225
+ new SlashCommandBuilder()
226
+ .setName('share')
227
+ .setDescription(truncateCommandDescription('Share the current session as a public URL'))
228
+ .setDMPermission(false)
229
+ .toJSON(),
230
+ new SlashCommandBuilder()
231
+ .setName('diff')
232
+ .setDescription(truncateCommandDescription('Show git diff as a shareable URL'))
233
+ .setDMPermission(false)
234
+ .toJSON(),
235
+ new SlashCommandBuilder()
236
+ .setName('fork')
237
+ .setDescription(truncateCommandDescription('Fork the session from a past user message'))
238
+ .setDMPermission(false)
239
+ .toJSON(),
240
+ new SlashCommandBuilder()
241
+ .setName('fork-subagent')
242
+ .setDescription(truncateCommandDescription('Fork a subagent task session into a new thread'))
243
+ .setDMPermission(false)
244
+ .toJSON(),
245
+ new SlashCommandBuilder()
246
+ .setName('btw')
247
+ .setDescription(truncateCommandDescription('Ask something without polluting or blocking the current session'))
248
+ .addStringOption((option) => {
249
+ option
250
+ .setName('prompt')
251
+ .setDescription(truncateCommandDescription('The message to send in the forked session'))
252
+ .setRequired(true);
253
+ return option;
254
+ })
255
+ .setDMPermission(false)
256
+ .toJSON(),
257
+ new SlashCommandBuilder()
258
+ .setName('model')
259
+ .setDescription(truncateCommandDescription('Set the preferred model for this channel or session'))
260
+ .setDMPermission(false)
261
+ .toJSON(),
262
+ new SlashCommandBuilder()
263
+ .setName('model-variant')
264
+ .setDescription(truncateCommandDescription('Change thinking level for current model. Tied to the model; lost when you switch models'))
265
+ .setDMPermission(false)
266
+ .toJSON(),
267
+ new SlashCommandBuilder()
268
+ .setName('unset-model-override')
269
+ .setDescription(truncateCommandDescription('Remove model override and use default instead'))
270
+ .setDMPermission(false)
271
+ .toJSON(),
272
+ new SlashCommandBuilder()
273
+ .setName('login')
274
+ .setDescription(truncateCommandDescription('Authenticate with an AI provider (OAuth or API key). Use this instead of /connect'))
275
+ .setDMPermission(false)
276
+ .toJSON(),
277
+ new SlashCommandBuilder()
278
+ .setName('agent')
279
+ .setDescription(truncateCommandDescription('Set the preferred agent for this channel or session'))
280
+ .setDMPermission(false)
281
+ .toJSON(),
282
+ new SlashCommandBuilder()
283
+ .setName('queue')
284
+ .setDescription(truncateCommandDescription('Queue a message to be sent after the current response finishes'))
285
+ .addStringOption((option) => {
286
+ option
287
+ .setName('message')
288
+ .setDescription(truncateCommandDescription('The message to queue'))
289
+ .setRequired(true);
290
+ return option;
291
+ })
292
+ .setDMPermission(false)
293
+ .toJSON(),
294
+ new SlashCommandBuilder()
295
+ .setName('clear-queue')
296
+ .setDescription(truncateCommandDescription('Clear all queued messages in this thread'))
297
+ .addIntegerOption((option) => {
298
+ option
299
+ .setName('position')
300
+ .setDescription(truncateCommandDescription('1-based queued message position to clear (default: all)'))
301
+ .setMinValue(1);
302
+ return option;
303
+ })
304
+ .setDMPermission(false)
305
+ .toJSON(),
306
+ new SlashCommandBuilder()
307
+ .setName('queue-command')
308
+ .setDescription(truncateCommandDescription('Queue a user command to run after the current response finishes'))
309
+ .addStringOption((option) => {
310
+ option
311
+ .setName('command')
312
+ .setDescription(truncateCommandDescription('The command to run'))
313
+ .setRequired(true)
314
+ .setAutocomplete(true);
315
+ return option;
316
+ })
317
+ .addStringOption((option) => {
318
+ option
319
+ .setName('arguments')
320
+ .setDescription(truncateCommandDescription('Arguments to pass to the command'))
321
+ .setRequired(false);
322
+ return option;
323
+ })
324
+ .setDMPermission(false)
325
+ .toJSON(),
326
+ new SlashCommandBuilder()
327
+ .setName('undo')
328
+ .setDescription(truncateCommandDescription('Undo the last assistant message (revert file changes)'))
329
+ .setDMPermission(false)
330
+ .toJSON(),
331
+ new SlashCommandBuilder()
332
+ .setName('redo')
333
+ .setDescription(truncateCommandDescription('Redo previously undone changes'))
334
+ .setDMPermission(false)
335
+ .toJSON(),
336
+ new SlashCommandBuilder()
337
+ .setName('verbosity')
338
+ .setDescription(truncateCommandDescription('Set output verbosity for this channel'))
339
+ .setDMPermission(false)
340
+ .toJSON(),
341
+ new SlashCommandBuilder()
342
+ .setName('thread-deletion-sync')
343
+ .setDescription(truncateCommandDescription('Set how Discord thread deletion syncs to OpenCode sessions'))
344
+ .addStringOption((option) => {
345
+ option
346
+ .setName('mode')
347
+ .setDescription(truncateCommandDescription('soft, hard, or reset to default soft mode'))
348
+ .addChoices(...THREAD_DELETION_SYNC_CHOICES)
349
+ .setRequired(false);
350
+ return option;
351
+ })
352
+ .setDMPermission(false)
353
+ .toJSON(),
354
+ new SlashCommandBuilder()
355
+ .setName('restart-opencode-server')
356
+ .setDescription(truncateCommandDescription('Restart opencode server and re-register slash commands'))
357
+ .setDMPermission(false)
358
+ .toJSON(),
359
+ new SlashCommandBuilder()
360
+ .setName('run-shell-command')
361
+ .setDescription(truncateCommandDescription('Run a shell command in the project directory. Tip: prefix messages with ! as shortcut'))
362
+ .addStringOption((option) => {
363
+ option
364
+ .setName('command')
365
+ .setDescription(truncateCommandDescription('Command to run'))
366
+ .setRequired(true);
367
+ return option;
368
+ })
369
+ .setDMPermission(false)
370
+ .toJSON(),
371
+ new SlashCommandBuilder()
372
+ .setName('context-usage')
373
+ .setDescription(truncateCommandDescription('Show token usage and context window percentage for this session'))
374
+ .setDMPermission(false)
375
+ .toJSON(),
376
+ new SlashCommandBuilder()
377
+ .setName('session-id')
378
+ .setDescription(truncateCommandDescription('Show current session ID and opencode attach command for this thread'))
379
+ .setDMPermission(false)
380
+ .toJSON(),
381
+ new SlashCommandBuilder()
382
+ .setName('upgrade-and-restart')
383
+ .setDescription(truncateCommandDescription('Upgrade otto to the latest version and restart the bot'))
384
+ .setDMPermission(false)
385
+ .toJSON(),
386
+ new SlashCommandBuilder()
387
+ .setName('transcription-key')
388
+ .setDescription(truncateCommandDescription('Set API key for voice message transcription (OpenAI or Gemini)'))
389
+ .setDMPermission(false)
390
+ .toJSON(),
391
+ new SlashCommandBuilder()
392
+ .setName('mcp')
393
+ .setDescription(truncateCommandDescription('List and manage MCP servers for this project'))
394
+ .setDMPermission(false)
395
+ .toJSON(),
396
+ new SlashCommandBuilder()
397
+ .setName('screenshare')
398
+ .setDescription(truncateCommandDescription('Start screen sharing via VNC tunnel (auto-stops after 30 minutes)'))
399
+ .setDMPermission(false)
400
+ .toJSON(),
401
+ new SlashCommandBuilder()
402
+ .setName('screenshare-stop')
403
+ .setDescription(truncateCommandDescription('Stop screen sharing'))
404
+ .setDMPermission(false)
405
+ .toJSON(),
406
+ new SlashCommandBuilder()
407
+ .setName('vscode')
408
+ .setDescription(truncateCommandDescription('Open VS Code in the browser for this project or worktree (auto-stops after 30 minutes)'))
409
+ .setDMPermission(false)
410
+ .toJSON(),
411
+ ];
412
+ // Dynamic commands are registered in priority order: agents → user commands → skills → MCP prompts.
413
+ // This ordering matters because we slice to MAX_DISCORD_COMMANDS (100) at the end,
414
+ // so lower-priority dynamic commands get trimmed first if the total exceeds the limit.
415
+ // 1. Agent-specific quick commands like /plan-agent, /build-agent
416
+ // Filter to primary/all mode agents (same as /agent command shows), excluding hidden agents
417
+ const primaryAgents = agents.filter((a) => (a.mode === 'primary' || a.mode === 'all') && !a.hidden);
418
+ for (const agent of primaryAgents) {
419
+ const sanitizedName = sanitizeAgentName(agent.name);
420
+ // Skip if sanitized name is empty or would create invalid command name
421
+ // Discord command names must start with a lowercase letter or number
422
+ if (!sanitizedName || !/^[a-z0-9]/.test(sanitizedName)) {
423
+ continue;
424
+ }
425
+ // Truncate base name before appending suffix so the -agent suffix is never
426
+ // lost to Discord's 32-char command name limit.
427
+ const agentSuffix = '-agent';
428
+ const agentBaseName = sanitizedName.slice(0, 32 - agentSuffix.length);
429
+ const commandName = `${agentBaseName}${agentSuffix}`;
430
+ const description = buildQuickAgentCommandDescription({
431
+ agentName: agent.name,
432
+ description: agent.description,
433
+ });
434
+ commands.push(new SlashCommandBuilder()
435
+ .setName(commandName)
436
+ .setDescription(truncateCommandDescription(description))
437
+ .setDMPermission(false)
438
+ .toJSON());
439
+ }
440
+ // 2. User-defined commands, skills, and MCP prompts (ordered by priority)
441
+ // Also populate registeredUserCommands in the store for /queue-command autocomplete
442
+ const newRegisteredCommands = [];
443
+ // Sort: regular commands first, then skills, then MCP prompts
444
+ const sourceOrder = { config: 0, skill: 1, mcp: 2 };
445
+ const sortedUserCommands = [...userCommands].sort((a, b) => {
446
+ return (sourceOrder[a.source || ''] ?? 0) - (sourceOrder[b.source || ''] ?? 0);
447
+ });
448
+ for (const cmd of sortedUserCommands) {
449
+ if (SKIP_USER_COMMANDS.includes(cmd.name)) {
450
+ continue;
451
+ }
452
+ // Sanitize command name: oh-my-opencode uses MCP commands with colons and slashes,
453
+ // which Discord doesn't allow in command names.
454
+ // Discord command names: lowercase, alphanumeric and hyphens only, must start with letter/number.
455
+ const sanitizedName = cmd.name
456
+ .toLowerCase()
457
+ .replace(/[:/]/g, '-') // Replace : and / with hyphens first
458
+ .replace(/[^a-z0-9-]/g, '-') // Replace any other non-alphanumeric chars
459
+ .replace(/-+/g, '-') // Collapse multiple hyphens
460
+ .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
461
+ // Skip if sanitized name is empty - would create invalid command name like "-cmd"
462
+ if (!sanitizedName) {
463
+ continue;
464
+ }
465
+ const commandSuffix = getDiscordCommandSuffix(cmd);
466
+ // Truncate base name before appending suffix so the suffix is never
467
+ // lost to Discord's 32-char command name limit.
468
+ const baseName = sanitizedName.slice(0, 32 - commandSuffix.length);
469
+ const commandName = `${baseName}${commandSuffix}`;
470
+ const description = cmd.description || `Run /${cmd.name} command`;
471
+ newRegisteredCommands.push({
472
+ name: cmd.name,
473
+ discordCommandName: commandName,
474
+ description,
475
+ source: cmd.source,
476
+ });
477
+ commands.push(new SlashCommandBuilder()
478
+ .setName(commandName)
479
+ .setDescription(truncateCommandDescription(description))
480
+ .addStringOption((option) => {
481
+ option
482
+ .setName('arguments')
483
+ .setDescription(truncateCommandDescription('Arguments to pass to the command'))
484
+ .setRequired(false);
485
+ return option;
486
+ })
487
+ .setDMPermission(false)
488
+ .toJSON());
489
+ }
490
+ store.setState({ registeredUserCommands: newRegisteredCommands });
491
+ // Discord allows max 100 guild commands. Slice to stay within the limit,
492
+ // trimming lowest-priority dynamic commands (MCP prompts, then skills) first.
493
+ const MAX_DISCORD_COMMANDS = 100;
494
+ if (commands.length > MAX_DISCORD_COMMANDS) {
495
+ cliLogger.warn(`COMMANDS: ${commands.length} commands exceed Discord limit of ${MAX_DISCORD_COMMANDS}, truncating to ${MAX_DISCORD_COMMANDS}`);
496
+ commands.length = MAX_DISCORD_COMMANDS;
497
+ }
498
+ const rest = createDiscordRest(token);
499
+ const uniqueGuildIds = Array.from(new Set(guildIds.filter((guildId) => guildId)));
500
+ const guildCommandNames = new Set(commands
501
+ .map((command) => {
502
+ return command.name;
503
+ })
504
+ .filter((name) => {
505
+ return typeof name === 'string';
506
+ }));
507
+ if (uniqueGuildIds.length === 0) {
508
+ cliLogger.warn('COMMANDS: No guilds available, skipping slash command registration');
509
+ return;
510
+ }
511
+ try {
512
+ // PUT is a bulk overwrite: Discord matches by name, updates changed fields
513
+ // (description, options, etc.) in place, creates new commands, and deletes
514
+ // any not present in the body. No local diffing needed.
515
+ const results = await Promise.allSettled(uniqueGuildIds.map(async (guildId) => {
516
+ const response = await rest.put(Routes.applicationGuildCommands(appId, guildId), {
517
+ body: commands,
518
+ });
519
+ const registeredCount = Array.isArray(response)
520
+ ? response.length
521
+ : commands.length;
522
+ return { guildId, registeredCount };
523
+ }));
524
+ const failedGuilds = results
525
+ .map((result, index) => {
526
+ if (result.status === 'fulfilled') {
527
+ return null;
528
+ }
529
+ return {
530
+ guildId: uniqueGuildIds[index],
531
+ error: result.reason instanceof Error
532
+ ? result.reason.message
533
+ : String(result.reason),
534
+ };
535
+ })
536
+ .filter((value) => {
537
+ return value !== null;
538
+ });
539
+ if (failedGuilds.length > 0) {
540
+ failedGuilds.forEach((failure) => {
541
+ cliLogger.warn(`COMMANDS: Failed to register slash commands for guild ${failure.guildId}: ${failure.error}`);
542
+ });
543
+ throw new Error(`Failed to register slash commands for ${failedGuilds.length} guild(s)`);
544
+ }
545
+ const successfulGuilds = results.length;
546
+ const firstRegisteredCount = results[0];
547
+ const registeredCommandCount = firstRegisteredCount && firstRegisteredCount.status === 'fulfilled'
548
+ ? firstRegisteredCount.value.registeredCount
549
+ : commands.length;
550
+ // In gateway mode, global application routes (/applications/{app_id}/commands)
551
+ // are denied by the proxy (DeniedWithoutGuild). Legacy global commands only
552
+ // exist for self-hosted bots that previously registered commands globally.
553
+ const isGateway = store.getState().discordBaseUrl !== 'https://discord.com';
554
+ if (!isGateway) {
555
+ await deleteLegacyGlobalCommands({
556
+ rest,
557
+ appId,
558
+ commandNames: guildCommandNames,
559
+ });
560
+ }
561
+ cliLogger.info(`COMMANDS: Successfully registered ${registeredCommandCount} slash commands for ${successfulGuilds} guild(s)`);
562
+ }
563
+ catch (error) {
564
+ cliLogger.error('COMMANDS: Failed to register slash commands: ' + String(error));
565
+ throw error;
566
+ }
567
+ }
@@ -0,0 +1,82 @@
1
+ // Configurable Discord API endpoint URLs.
2
+ // Base URL for REST calls lives in the centralized zustand store (store.ts),
3
+ // replacing the old process.env['DISCORD_REST_BASE_URL'] mutation.
4
+ //
5
+ // DISCORD_GATEWAY_URL: WebSocket gateway URL (default: undefined, auto-discovered via /gateway/bot)
6
+ // discord.js has no direct ws.gateway option — the gateway URL comes from the
7
+ // GET /gateway/bot REST response. If using a proxy, the proxy's /gateway/bot
8
+ // endpoint should return the desired gateway URL. This constant is provided
9
+ // for non-discord.js consumers (e.g. the Rust gateway-proxy config).
10
+ import { REST } from 'discord.js';
11
+ import { store } from './store.js';
12
+ /**
13
+ * Base URL for Discord (default: https://discord.com).
14
+ * All REST API and raw fetch calls derive their URLs from this.
15
+ * Reads from the centralized store so gateway mode can set it via
16
+ * store.setState({ discordBaseUrl }) after startup.
17
+ */
18
+ export function getDiscordRestBaseUrl() {
19
+ return store.getState().discordBaseUrl;
20
+ }
21
+ /**
22
+ * The REST api path that discord.js expects (base + /api).
23
+ * discord.js appends /v10/... to this internally.
24
+ */
25
+ export function getDiscordRestApiUrl() {
26
+ return new URL('/api', getDiscordRestBaseUrl()).toString();
27
+ }
28
+ /**
29
+ * WebSocket gateway URL override (default: undefined = auto-discover).
30
+ * When undefined, discord.js fetches it from GET /gateway/bot via REST.
31
+ * Provided as a constant for external consumers like the Rust gateway-proxy.
32
+ */
33
+ export const DISCORD_GATEWAY_URL = process.env['DISCORD_GATEWAY_URL'] || undefined;
34
+ /**
35
+ * Build a full Discord REST API URL for raw fetch() calls.
36
+ * Uses new URL() for safe path concatenation.
37
+ *
38
+ * Example: discordApiUrl(`/channels/${id}/messages`) →
39
+ * "https://discord.com/api/v10/channels/123/messages"
40
+ */
41
+ export function discordApiUrl(path) {
42
+ return new URL(`/api/v10${path}`, getDiscordRestBaseUrl()).toString();
43
+ }
44
+ /**
45
+ * Create a discord.js REST client pointed at the configured base URL.
46
+ * Centralizes the REST instantiation so all call sites use the override.
47
+ */
48
+ export function createDiscordRest(token) {
49
+ return new REST({ api: getDiscordRestApiUrl() }).setToken(token);
50
+ }
51
+ /**
52
+ * Returns the internet-reachable base URL for this otto instance.
53
+ * When OTTO_INTERNET_REACHABLE_URL (or the legacy OTTO_INTERNET_REACHABLE_URL)
54
+ * is set (e.g. "https://my-otto.fly.dev"), otto binds the hrana server to
55
+ * 0.0.0.0 and exposes a /otto/wake endpoint so the gateway-proxy can wake
56
+ * this instance. Discord traffic still flows through the normal path
57
+ * (gateway-proxy in gateway mode, direct in self-hosted).
58
+ * Returns null when not set (otto only reachable on localhost).
59
+ */
60
+ export function getInternetReachableBaseUrl() {
61
+ return process.env['OTTO_INTERNET_REACHABLE_URL'] || process.env['OTTO_INTERNET_REACHABLE_URL'] || null;
62
+ }
63
+ /**
64
+ * Derive an HTTPS REST base URL from a WebSocket gateway URL.
65
+ * Swaps wss→https and ws→http. Used for gateway mode where the
66
+ * gateway proxy URL doubles as the REST proxy base.
67
+ */
68
+ export function getGatewayProxyRestBaseUrl({ gatewayUrl }) {
69
+ try {
70
+ const parsedUrl = new URL(gatewayUrl);
71
+ if (parsedUrl.protocol === 'wss:') {
72
+ parsedUrl.protocol = 'https:';
73
+ }
74
+ else if (parsedUrl.protocol === 'ws:') {
75
+ parsedUrl.protocol = 'http:';
76
+ }
77
+ return parsedUrl.toString();
78
+ }
79
+ catch {
80
+ return gatewayUrl;
81
+ }
82
+ }