@otto-assistant/bridge 0.4.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,693 @@
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
+
20
+ const cliLogger = createLogger(LogPrefix.CLI)
21
+
22
+ // Commands to skip when registering user commands (reserved names)
23
+ export const SKIP_USER_COMMANDS = ['init']
24
+
25
+ export type AgentInfo = {
26
+ name: string
27
+ description?: string
28
+ mode: string
29
+ hidden?: boolean
30
+ }
31
+
32
+ function getDiscordCommandSuffix(
33
+ command: OpencodeCommand,
34
+ ): '-cmd' | '-skill' | '-mcp-prompt' {
35
+ if (command.source === 'skill') {
36
+ return '-skill'
37
+ }
38
+ if (command.source === 'mcp') {
39
+ return '-mcp-prompt'
40
+ }
41
+ return '-cmd'
42
+ }
43
+
44
+ type DiscordCommandSummary = {
45
+ id: string
46
+ name: string
47
+ }
48
+
49
+ function isDiscordCommandSummary(value: unknown): value is DiscordCommandSummary {
50
+ if (typeof value !== 'object' || value === null) {
51
+ return false
52
+ }
53
+
54
+ const id = Reflect.get(value, 'id')
55
+ const name = Reflect.get(value, 'name')
56
+ return typeof id === 'string' && typeof name === 'string'
57
+ }
58
+
59
+ async function deleteLegacyGlobalCommands({
60
+ rest,
61
+ appId,
62
+ commandNames,
63
+ }: {
64
+ rest: REST
65
+ appId: string
66
+ commandNames: Set<string>
67
+ }) {
68
+ try {
69
+ const response = await rest.get(Routes.applicationCommands(appId))
70
+ if (!Array.isArray(response)) {
71
+ cliLogger.warn(
72
+ 'COMMANDS: Unexpected global command payload while cleaning legacy global commands',
73
+ )
74
+ return
75
+ }
76
+
77
+ const legacyGlobalCommands = response
78
+ .filter(isDiscordCommandSummary)
79
+ .filter((command) => {
80
+ return commandNames.has(command.name)
81
+ })
82
+
83
+ if (legacyGlobalCommands.length === 0) {
84
+ return
85
+ }
86
+
87
+ const deletionResults = await Promise.allSettled(
88
+ legacyGlobalCommands.map(async (command) => {
89
+ await rest.delete(Routes.applicationCommand(appId, command.id))
90
+ return command
91
+ }),
92
+ )
93
+
94
+ const failedDeletions = deletionResults.filter((result) => {
95
+ return result.status === 'rejected'
96
+ })
97
+ if (failedDeletions.length > 0) {
98
+ cliLogger.warn(
99
+ `COMMANDS: Failed to delete ${failedDeletions.length} legacy global command(s)`,
100
+ )
101
+ }
102
+
103
+ const deletedCount = deletionResults.length - failedDeletions.length
104
+ if (deletedCount > 0) {
105
+ cliLogger.info(
106
+ `COMMANDS: Deleted ${deletedCount} legacy global command(s) to avoid guild/global duplicates`,
107
+ )
108
+ }
109
+ } catch (error) {
110
+ cliLogger.warn(
111
+ `COMMANDS: Could not clean legacy global commands: ${error instanceof Error ? error.stack : String(error)}`,
112
+ )
113
+ }
114
+ }
115
+
116
+ // Discord slash command descriptions must be 1-100 chars.
117
+ // Truncate to 100 so @sapphire/shapeshift validation never throws.
118
+ function truncateCommandDescription(description: string): string {
119
+ return description.slice(0, 100)
120
+ }
121
+
122
+ export async function registerCommands({
123
+ token,
124
+ appId,
125
+ guildIds,
126
+ userCommands = [],
127
+ agents = [],
128
+ }: {
129
+ token: string
130
+ appId: string
131
+ guildIds: string[]
132
+ userCommands?: OpencodeCommand[]
133
+ agents?: AgentInfo[]
134
+ }) {
135
+ const commands = [
136
+ new SlashCommandBuilder()
137
+ .setName('resume')
138
+ .setDescription(truncateCommandDescription('Resume an existing OpenCode session'))
139
+ .addStringOption((option) => {
140
+ option
141
+ .setName('session')
142
+ .setDescription(truncateCommandDescription('The session to resume'))
143
+ .setRequired(true)
144
+ .setAutocomplete(true)
145
+
146
+ return option
147
+ })
148
+ .setDMPermission(false)
149
+ .toJSON(),
150
+ new SlashCommandBuilder()
151
+ .setName('new-session')
152
+ .setDescription(truncateCommandDescription('Start a new OpenCode session'))
153
+ .addStringOption((option) => {
154
+ option
155
+ .setName('prompt')
156
+ .setDescription(truncateCommandDescription('Prompt content for the session'))
157
+ .setRequired(true)
158
+
159
+ return option
160
+ })
161
+ .addStringOption((option) => {
162
+ option
163
+ .setName('files')
164
+ .setDescription(
165
+ truncateCommandDescription('Files to mention (comma or space separated; autocomplete)'),
166
+ )
167
+ .setAutocomplete(true)
168
+ .setMaxLength(6000)
169
+
170
+ return option
171
+ })
172
+ .addStringOption((option) => {
173
+ option
174
+ .setName('agent')
175
+ .setDescription(truncateCommandDescription('Agent to use for this session'))
176
+ .setAutocomplete(true)
177
+
178
+ return option
179
+ })
180
+ .setDMPermission(false)
181
+ .toJSON(),
182
+ new SlashCommandBuilder()
183
+ .setName('new-worktree')
184
+ .setDescription(
185
+ truncateCommandDescription('Create a git worktree branch from origin/HEAD (or main). Optionally pick a base branch.'),
186
+ )
187
+ .addStringOption((option) => {
188
+ option
189
+ .setName('name')
190
+ .setDescription(
191
+ truncateCommandDescription('Name for worktree (optional in threads - uses thread name)'),
192
+ )
193
+ .setRequired(false)
194
+
195
+ return option
196
+ })
197
+ .addStringOption((option) => {
198
+ option
199
+ .setName('base-branch')
200
+ .setDescription(
201
+ truncateCommandDescription('Branch to create the worktree from (default: origin/HEAD or main)'),
202
+ )
203
+ .setRequired(false)
204
+ .setAutocomplete(true)
205
+
206
+ return option
207
+ })
208
+ .setDMPermission(false)
209
+ .toJSON(),
210
+ new SlashCommandBuilder()
211
+ .setName('merge-worktree')
212
+ .setDescription(
213
+ truncateCommandDescription('Squash-merge worktree into default branch. Aborts if main has uncommitted changes.'),
214
+ )
215
+ .addStringOption((option) => {
216
+ option
217
+ .setName('target-branch')
218
+ .setDescription(
219
+ truncateCommandDescription('Branch to merge into (default: origin/HEAD or main)'),
220
+ )
221
+ .setRequired(false)
222
+ .setAutocomplete(true)
223
+
224
+ return option
225
+ })
226
+ .setDMPermission(false)
227
+ .toJSON(),
228
+ new SlashCommandBuilder()
229
+ .setName('toggle-worktrees')
230
+ .setDescription(
231
+ truncateCommandDescription('Toggle automatic git worktree creation for new sessions in this channel'),
232
+ )
233
+ .setDMPermission(false)
234
+ .toJSON(),
235
+ new SlashCommandBuilder()
236
+ .setName('worktrees')
237
+ .setDescription(truncateCommandDescription('List all active worktree sessions'))
238
+ .setDMPermission(false)
239
+ .toJSON(),
240
+ new SlashCommandBuilder()
241
+ .setName('tasks')
242
+ .setDescription(truncateCommandDescription('List scheduled tasks created via send --send-at'))
243
+ .addBooleanOption((option) => {
244
+ return option
245
+ .setName('all')
246
+ .setDescription(
247
+ truncateCommandDescription('Include completed, cancelled, and failed tasks'),
248
+ )
249
+ })
250
+ .setDMPermission(false)
251
+ .toJSON(),
252
+
253
+ new SlashCommandBuilder()
254
+ .setName('add-project')
255
+ .setDescription(
256
+ truncateCommandDescription('Create Discord channels for a project. Use `npx kimaki project add` for unlisted projects'),
257
+ )
258
+ .addStringOption((option) => {
259
+ option
260
+ .setName('project')
261
+ .setDescription(
262
+ truncateCommandDescription('Recent OpenCode projects. Use `npx kimaki project add` if not listed'),
263
+ )
264
+ .setRequired(true)
265
+ .setAutocomplete(true)
266
+
267
+ return option
268
+ })
269
+ .setDMPermission(false)
270
+ .toJSON(),
271
+ new SlashCommandBuilder()
272
+ .setName('remove-project')
273
+ .setDescription(truncateCommandDescription('Remove Discord channels for a project'))
274
+ .addStringOption((option) => {
275
+ option
276
+ .setName('project')
277
+ .setDescription(truncateCommandDescription('Select a project to remove'))
278
+ .setRequired(true)
279
+ .setAutocomplete(true)
280
+
281
+ return option
282
+ })
283
+ .setDMPermission(false)
284
+ .toJSON(),
285
+ new SlashCommandBuilder()
286
+ .setName('create-new-project')
287
+ .setDescription(
288
+ truncateCommandDescription('Create a new project folder, initialize git, and start a session'),
289
+ )
290
+ .addStringOption((option) => {
291
+ option
292
+ .setName('name')
293
+ .setDescription(truncateCommandDescription('Name for the new project folder'))
294
+ .setRequired(true)
295
+
296
+ return option
297
+ })
298
+ .setDMPermission(false)
299
+ .toJSON(),
300
+ new SlashCommandBuilder()
301
+ .setName('abort')
302
+ .setDescription(truncateCommandDescription('Abort the current OpenCode request in this thread'))
303
+ .setDMPermission(false)
304
+ .toJSON(),
305
+ new SlashCommandBuilder()
306
+ .setName('compact')
307
+ .setDescription(
308
+ truncateCommandDescription('Compact the session context by summarizing conversation history'),
309
+ )
310
+ .setDMPermission(false)
311
+ .toJSON(),
312
+
313
+ new SlashCommandBuilder()
314
+ .setName('share')
315
+ .setDescription(truncateCommandDescription('Share the current session as a public URL'))
316
+ .setDMPermission(false)
317
+ .toJSON(),
318
+ new SlashCommandBuilder()
319
+ .setName('diff')
320
+ .setDescription(truncateCommandDescription('Show git diff as a shareable URL'))
321
+ .setDMPermission(false)
322
+ .toJSON(),
323
+ new SlashCommandBuilder()
324
+ .setName('fork')
325
+ .setDescription(truncateCommandDescription('Fork the session from a past user message'))
326
+ .setDMPermission(false)
327
+ .toJSON(),
328
+ new SlashCommandBuilder()
329
+ .setName('btw')
330
+ .setDescription(truncateCommandDescription('Ask something without polluting or blocking the current session'))
331
+ .addStringOption((option) => {
332
+ option
333
+ .setName('prompt')
334
+ .setDescription(truncateCommandDescription('The message to send in the forked session'))
335
+ .setRequired(true)
336
+ return option
337
+ })
338
+ .setDMPermission(false)
339
+ .toJSON(),
340
+ new SlashCommandBuilder()
341
+ .setName('model')
342
+ .setDescription(truncateCommandDescription('Set the preferred model for this channel or session'))
343
+ .setDMPermission(false)
344
+ .toJSON(),
345
+ new SlashCommandBuilder()
346
+ .setName('model-variant')
347
+ .setDescription(
348
+ truncateCommandDescription('Quickly change the thinking level variant for the current model'),
349
+ )
350
+ .setDMPermission(false)
351
+ .toJSON(),
352
+ new SlashCommandBuilder()
353
+ .setName('unset-model-override')
354
+ .setDescription(truncateCommandDescription('Remove model override and use default instead'))
355
+ .setDMPermission(false)
356
+ .toJSON(),
357
+ new SlashCommandBuilder()
358
+ .setName('login')
359
+ .setDescription(
360
+ truncateCommandDescription('Authenticate with an AI provider (OAuth or API key). Use this instead of /connect'),
361
+ )
362
+ .setDMPermission(false)
363
+ .toJSON(),
364
+ new SlashCommandBuilder()
365
+ .setName('agent')
366
+ .setDescription(truncateCommandDescription('Set the preferred agent for this channel or session'))
367
+ .setDMPermission(false)
368
+ .toJSON(),
369
+ new SlashCommandBuilder()
370
+ .setName('queue')
371
+ .setDescription(
372
+ truncateCommandDescription('Queue a message to be sent after the current response finishes'),
373
+ )
374
+ .addStringOption((option) => {
375
+ option
376
+ .setName('message')
377
+ .setDescription(truncateCommandDescription('The message to queue'))
378
+ .setRequired(true)
379
+
380
+ return option
381
+ })
382
+ .setDMPermission(false)
383
+ .toJSON(),
384
+ new SlashCommandBuilder()
385
+ .setName('clear-queue')
386
+ .setDescription(truncateCommandDescription('Clear all queued messages in this thread'))
387
+ .setDMPermission(false)
388
+ .toJSON(),
389
+ new SlashCommandBuilder()
390
+ .setName('queue-command')
391
+ .setDescription(
392
+ truncateCommandDescription('Queue a user command to run after the current response finishes'),
393
+ )
394
+ .addStringOption((option) => {
395
+ option
396
+ .setName('command')
397
+ .setDescription(truncateCommandDescription('The command to run'))
398
+ .setRequired(true)
399
+ .setAutocomplete(true)
400
+ return option
401
+ })
402
+ .addStringOption((option) => {
403
+ option
404
+ .setName('arguments')
405
+ .setDescription(truncateCommandDescription('Arguments to pass to the command'))
406
+ .setRequired(false)
407
+ return option
408
+ })
409
+ .setDMPermission(false)
410
+ .toJSON(),
411
+ new SlashCommandBuilder()
412
+ .setName('undo')
413
+ .setDescription(truncateCommandDescription('Undo the last assistant message (revert file changes)'))
414
+ .setDMPermission(false)
415
+ .toJSON(),
416
+ new SlashCommandBuilder()
417
+ .setName('redo')
418
+ .setDescription(truncateCommandDescription('Redo previously undone changes'))
419
+ .setDMPermission(false)
420
+ .toJSON(),
421
+ new SlashCommandBuilder()
422
+ .setName('verbosity')
423
+ .setDescription(truncateCommandDescription('Set output verbosity for this channel'))
424
+ .setDMPermission(false)
425
+ .toJSON(),
426
+ new SlashCommandBuilder()
427
+ .setName('restart-opencode-server')
428
+ .setDescription(
429
+ truncateCommandDescription('Restart opencode server and re-register slash commands'),
430
+ )
431
+ .setDMPermission(false)
432
+ .toJSON(),
433
+ new SlashCommandBuilder()
434
+ .setName('run-shell-command')
435
+ .setDescription(
436
+ truncateCommandDescription('Run a shell command in the project directory. Tip: prefix messages with ! as shortcut'),
437
+ )
438
+ .addStringOption((option) => {
439
+ option
440
+ .setName('command')
441
+ .setDescription(truncateCommandDescription('Command to run'))
442
+ .setRequired(true)
443
+ return option
444
+ })
445
+ .setDMPermission(false)
446
+ .toJSON(),
447
+ new SlashCommandBuilder()
448
+ .setName('context-usage')
449
+ .setDescription(
450
+ truncateCommandDescription('Show token usage and context window percentage for this session'),
451
+ )
452
+ .setDMPermission(false)
453
+ .toJSON(),
454
+ new SlashCommandBuilder()
455
+ .setName('session-id')
456
+ .setDescription(
457
+ truncateCommandDescription('Show current session ID and opencode attach command for this thread'),
458
+ )
459
+ .setDMPermission(false)
460
+ .toJSON(),
461
+
462
+ new SlashCommandBuilder()
463
+ .setName('upgrade-and-restart')
464
+ .setDescription(
465
+ truncateCommandDescription('Upgrade kimaki to the latest version and restart the bot'),
466
+ )
467
+ .setDMPermission(false)
468
+ .toJSON(),
469
+ new SlashCommandBuilder()
470
+ .setName('transcription-key')
471
+ .setDescription(
472
+ truncateCommandDescription('Set API key for voice message transcription (OpenAI or Gemini)'),
473
+ )
474
+ .setDMPermission(false)
475
+ .toJSON(),
476
+ new SlashCommandBuilder()
477
+ .setName('mcp')
478
+ .setDescription(truncateCommandDescription('List and manage MCP servers for this project'))
479
+ .setDMPermission(false)
480
+ .toJSON(),
481
+ new SlashCommandBuilder()
482
+ .setName('screenshare')
483
+ .setDescription(truncateCommandDescription('Start screen sharing via VNC tunnel (auto-stops after 30 minutes)'))
484
+ .setDMPermission(false)
485
+ .toJSON(),
486
+ new SlashCommandBuilder()
487
+ .setName('screenshare-stop')
488
+ .setDescription(truncateCommandDescription('Stop screen sharing'))
489
+ .setDMPermission(false)
490
+ .toJSON(),
491
+ ]
492
+
493
+ // Dynamic commands are registered in priority order: agents → user commands → skills → MCP prompts.
494
+ // This ordering matters because we slice to MAX_DISCORD_COMMANDS (100) at the end,
495
+ // so lower-priority dynamic commands get trimmed first if the total exceeds the limit.
496
+
497
+ // 1. Agent-specific quick commands like /plan-agent, /build-agent
498
+ // Filter to primary/all mode agents (same as /agent command shows), excluding hidden agents
499
+ const primaryAgents = agents.filter(
500
+ (a) => (a.mode === 'primary' || a.mode === 'all') && !a.hidden,
501
+ )
502
+ for (const agent of primaryAgents) {
503
+ const sanitizedName = sanitizeAgentName(agent.name)
504
+ // Skip if sanitized name is empty or would create invalid command name
505
+ // Discord command names must start with a lowercase letter or number
506
+ if (!sanitizedName || !/^[a-z0-9]/.test(sanitizedName)) {
507
+ continue
508
+ }
509
+ // Truncate base name before appending suffix so the -agent suffix is never
510
+ // lost to Discord's 32-char command name limit.
511
+ const agentSuffix = '-agent'
512
+ const agentBaseName = sanitizedName.slice(0, 32 - agentSuffix.length)
513
+ const commandName = `${agentBaseName}${agentSuffix}`
514
+ const description = buildQuickAgentCommandDescription({
515
+ agentName: agent.name,
516
+ description: agent.description,
517
+ })
518
+
519
+ commands.push(
520
+ new SlashCommandBuilder()
521
+ .setName(commandName)
522
+ .setDescription(truncateCommandDescription(description))
523
+ .setDMPermission(false)
524
+ .toJSON(),
525
+ )
526
+ }
527
+
528
+ // 2. User-defined commands, skills, and MCP prompts (ordered by priority)
529
+ // Also populate registeredUserCommands in the store for /queue-command autocomplete
530
+ const newRegisteredCommands: RegisteredUserCommand[] = []
531
+ // Sort: regular commands first, then skills, then MCP prompts
532
+ const sourceOrder: Record<string, number> = { config: 0, skill: 1, mcp: 2 }
533
+ const sortedUserCommands = [...userCommands].sort((a, b) => {
534
+ return (sourceOrder[a.source || ''] ?? 0) - (sourceOrder[b.source || ''] ?? 0)
535
+ })
536
+ for (const cmd of sortedUserCommands) {
537
+ if (SKIP_USER_COMMANDS.includes(cmd.name)) {
538
+ continue
539
+ }
540
+
541
+ // Sanitize command name: oh-my-opencode uses MCP commands with colons and slashes,
542
+ // which Discord doesn't allow in command names.
543
+ // Discord command names: lowercase, alphanumeric and hyphens only, must start with letter/number.
544
+ const sanitizedName = cmd.name
545
+ .toLowerCase()
546
+ .replace(/[:/]/g, '-') // Replace : and / with hyphens first
547
+ .replace(/[^a-z0-9-]/g, '-') // Replace any other non-alphanumeric chars
548
+ .replace(/-+/g, '-') // Collapse multiple hyphens
549
+ .replace(/^-|-$/g, '') // Remove leading/trailing hyphens
550
+
551
+ // Skip if sanitized name is empty - would create invalid command name like "-cmd"
552
+ if (!sanitizedName) {
553
+ continue
554
+ }
555
+
556
+ const commandSuffix = getDiscordCommandSuffix(cmd)
557
+
558
+ // Truncate base name before appending suffix so the suffix is never
559
+ // lost to Discord's 32-char command name limit.
560
+ const baseName = sanitizedName.slice(0, 32 - commandSuffix.length)
561
+ const commandName = `${baseName}${commandSuffix}`
562
+ const description = cmd.description || `Run /${cmd.name} command`
563
+
564
+ newRegisteredCommands.push({
565
+ name: cmd.name,
566
+ discordCommandName: commandName,
567
+ description,
568
+ source: cmd.source,
569
+ })
570
+
571
+ commands.push(
572
+ new SlashCommandBuilder()
573
+ .setName(commandName)
574
+ .setDescription(truncateCommandDescription(description))
575
+ .addStringOption((option) => {
576
+ option
577
+ .setName('arguments')
578
+ .setDescription(truncateCommandDescription('Arguments to pass to the command'))
579
+ .setRequired(false)
580
+ return option
581
+ })
582
+ .setDMPermission(false)
583
+ .toJSON(),
584
+ )
585
+ }
586
+ store.setState({ registeredUserCommands: newRegisteredCommands })
587
+
588
+ // Discord allows max 100 guild commands. Slice to stay within the limit,
589
+ // trimming lowest-priority dynamic commands (MCP prompts, then skills) first.
590
+ const MAX_DISCORD_COMMANDS = 100
591
+ if (commands.length > MAX_DISCORD_COMMANDS) {
592
+ cliLogger.warn(
593
+ `COMMANDS: ${commands.length} commands exceed Discord limit of ${MAX_DISCORD_COMMANDS}, truncating to ${MAX_DISCORD_COMMANDS}`,
594
+ )
595
+ commands.length = MAX_DISCORD_COMMANDS
596
+ }
597
+
598
+ const rest = createDiscordRest(token)
599
+ const uniqueGuildIds = Array.from(new Set(guildIds.filter((guildId) => guildId)))
600
+ const guildCommandNames = new Set(
601
+ commands
602
+ .map((command) => {
603
+ return command.name
604
+ })
605
+ .filter((name): name is string => {
606
+ return typeof name === 'string'
607
+ }),
608
+ )
609
+
610
+ if (uniqueGuildIds.length === 0) {
611
+ cliLogger.warn('COMMANDS: No guilds available, skipping slash command registration')
612
+ return
613
+ }
614
+
615
+ try {
616
+ // PUT is a bulk overwrite: Discord matches by name, updates changed fields
617
+ // (description, options, etc.) in place, creates new commands, and deletes
618
+ // any not present in the body. No local diffing needed.
619
+ const results = await Promise.allSettled(
620
+ uniqueGuildIds.map(async (guildId) => {
621
+ const response = await rest.put(
622
+ Routes.applicationGuildCommands(appId, guildId),
623
+ {
624
+ body: commands,
625
+ },
626
+ )
627
+
628
+ const registeredCount = Array.isArray(response)
629
+ ? response.length
630
+ : commands.length
631
+
632
+ return { guildId, registeredCount }
633
+ }),
634
+ )
635
+
636
+ const failedGuilds = results
637
+ .map((result, index) => {
638
+ if (result.status === 'fulfilled') {
639
+ return null
640
+ }
641
+
642
+ return {
643
+ guildId: uniqueGuildIds[index],
644
+ error:
645
+ result.reason instanceof Error
646
+ ? result.reason.message
647
+ : String(result.reason),
648
+ }
649
+ })
650
+ .filter((value): value is { guildId: string; error: string } => {
651
+ return value !== null
652
+ })
653
+
654
+ if (failedGuilds.length > 0) {
655
+ failedGuilds.forEach((failure) => {
656
+ cliLogger.warn(
657
+ `COMMANDS: Failed to register slash commands for guild ${failure.guildId}: ${failure.error}`,
658
+ )
659
+ })
660
+ throw new Error(
661
+ `Failed to register slash commands for ${failedGuilds.length} guild(s)`,
662
+ )
663
+ }
664
+
665
+ const successfulGuilds = results.length
666
+ const firstRegisteredCount = results[0]
667
+ const registeredCommandCount =
668
+ firstRegisteredCount && firstRegisteredCount.status === 'fulfilled'
669
+ ? firstRegisteredCount.value.registeredCount
670
+ : commands.length
671
+
672
+ // In gateway mode, global application routes (/applications/{app_id}/commands)
673
+ // are denied by the proxy (DeniedWithoutGuild). Legacy global commands only
674
+ // exist for self-hosted bots that previously registered commands globally.
675
+ const isGateway = store.getState().discordBaseUrl !== 'https://discord.com'
676
+ if (!isGateway) {
677
+ await deleteLegacyGlobalCommands({
678
+ rest,
679
+ appId,
680
+ commandNames: guildCommandNames,
681
+ })
682
+ }
683
+
684
+ cliLogger.info(
685
+ `COMMANDS: Successfully registered ${registeredCommandCount} slash commands for ${successfulGuilds} guild(s)`,
686
+ )
687
+ } catch (error) {
688
+ cliLogger.error(
689
+ 'COMMANDS: Failed to register slash commands: ' + String(error),
690
+ )
691
+ throw error
692
+ }
693
+ }