@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,358 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ /* istanbul ignore file */
3
+ // @ts-nocheck
4
+
5
+ import { RealtimeClient } from '@openai/realtime-api-beta'
6
+ import { writeFile } from 'fs'
7
+ import { createLogger, LogPrefix } from './logger.js'
8
+
9
+ const openaiLogger = createLogger(LogPrefix.OPENAI)
10
+
11
+ // Export the session type for reuse
12
+ export interface OpenAIRealtimeSession {
13
+ send: (audioData: ArrayBuffer) => void
14
+ sendText: (text: string) => void
15
+ close: () => void
16
+ }
17
+
18
+ // Type definitions based on @openai/realtime-api-beta
19
+ interface ConversationItem {
20
+ id: string
21
+ object: string
22
+ type: 'message' | 'function_call' | 'function_call_output'
23
+ status: 'in_progress' | 'completed' | 'incomplete'
24
+ role?: 'user' | 'assistant' | 'system'
25
+ content?: Array<{
26
+ type: string
27
+ text?: string
28
+ audio?: string
29
+ transcript?: string | null
30
+ }>
31
+ formatted: {
32
+ audio?: Int16Array
33
+ text?: string
34
+ transcript?: string
35
+ tool?: {
36
+ type: 'function'
37
+ name: string
38
+ call_id: string
39
+ arguments: string
40
+ }
41
+ output?: string
42
+ }
43
+ }
44
+
45
+ interface ConversationEventDelta {
46
+ audio?: Int16Array
47
+ text?: string
48
+ transcript?: string
49
+ arguments?: string
50
+ }
51
+
52
+ const audioParts: Buffer[] = []
53
+
54
+ function saveBinaryFile(fileName: string, content: Buffer) {
55
+ writeFile(fileName, content, 'utf8', (err) => {
56
+ if (err) {
57
+ openaiLogger.error(`Error writing file ${fileName}:`, err)
58
+ return
59
+ }
60
+ openaiLogger.log(`Appending stream content to file ${fileName}.`)
61
+ })
62
+ }
63
+
64
+ interface WavConversionOptions {
65
+ numChannels: number
66
+ sampleRate: number
67
+ bitsPerSample: number
68
+ }
69
+
70
+ function convertToWav(rawData: Buffer[], mimeType: string) {
71
+ const options = parseMimeType(mimeType)
72
+ const dataLength = rawData.reduce((a, b) => a + b.length, 0)
73
+ const wavHeader = createWavHeader(dataLength, options)
74
+ const buffer = Buffer.concat(rawData)
75
+
76
+ return Buffer.concat([wavHeader, buffer])
77
+ }
78
+
79
+ function parseMimeType(mimeType: string) {
80
+ const [fileType, ...params] = mimeType.split(';').map((s) => s.trim())
81
+ const [_, format] = fileType?.split('/') || []
82
+
83
+ const options: Partial<WavConversionOptions> = {
84
+ numChannels: 1,
85
+ bitsPerSample: 16,
86
+ }
87
+
88
+ if (format && format.startsWith('L')) {
89
+ const bits = parseInt(format.slice(1), 10)
90
+ if (!isNaN(bits)) {
91
+ options.bitsPerSample = bits
92
+ }
93
+ }
94
+
95
+ for (const param of params) {
96
+ const [key, value] = param.split('=').map((s) => s.trim())
97
+ if (key === 'rate') {
98
+ options.sampleRate = parseInt(value || '', 10)
99
+ }
100
+ }
101
+
102
+ return options as WavConversionOptions
103
+ }
104
+
105
+ function createWavHeader(dataLength: number, options: WavConversionOptions) {
106
+ const { numChannels, sampleRate, bitsPerSample } = options
107
+
108
+ // http://soundfile.sapp.org/doc/WaveFormat
109
+
110
+ const byteRate = (sampleRate * numChannels * bitsPerSample) / 8
111
+ const blockAlign = (numChannels * bitsPerSample) / 8
112
+ const buffer = Buffer.alloc(44)
113
+
114
+ buffer.write('RIFF', 0) // ChunkID
115
+ buffer.writeUInt32LE(36 + dataLength, 4) // ChunkSize
116
+ buffer.write('WAVE', 8) // Format
117
+ buffer.write('fmt ', 12) // Subchunk1ID
118
+ buffer.writeUInt32LE(16, 16) // Subchunk1Size (PCM)
119
+ buffer.writeUInt16LE(1, 20) // AudioFormat (1 = PCM)
120
+ buffer.writeUInt16LE(numChannels, 22) // NumChannels
121
+ buffer.writeUInt32LE(sampleRate, 24) // SampleRate
122
+ buffer.writeUInt32LE(byteRate, 28) // ByteRate
123
+ buffer.writeUInt16LE(blockAlign, 32) // BlockAlign
124
+ buffer.writeUInt16LE(bitsPerSample, 34) // BitsPerSample
125
+ buffer.write('data', 36) // Subchunk2ID
126
+ buffer.writeUInt32LE(dataLength, 40) // Subchunk2Size
127
+
128
+ return buffer
129
+ }
130
+
131
+ function defaultAudioChunkHandler({
132
+ data,
133
+ mimeType,
134
+ }: {
135
+ data: Buffer
136
+ mimeType: string
137
+ }) {
138
+ audioParts.push(data)
139
+ const fileName = 'audio.wav'
140
+ const buffer = convertToWav(audioParts, mimeType)
141
+ saveBinaryFile(fileName, buffer)
142
+ }
143
+
144
+ export interface GenAISessionResult {
145
+ session: OpenAIRealtimeSession
146
+ stop: () => void
147
+ }
148
+
149
+ export async function startGenAiSession({
150
+ onAssistantAudioChunk,
151
+ onAssistantStartSpeaking,
152
+ onAssistantStopSpeaking,
153
+ onAssistantInterruptSpeaking,
154
+ systemMessage,
155
+ tools,
156
+ }: {
157
+ onAssistantAudioChunk?: (args: { data: Buffer; mimeType: string }) => void
158
+ onAssistantStartSpeaking?: () => void
159
+ onAssistantStopSpeaking?: () => void
160
+ onAssistantInterruptSpeaking?: () => void
161
+ systemMessage?: string
162
+ // Accept tools but use structural typing to avoid variance issues
163
+ tools?: Record<
164
+ string,
165
+ {
166
+ description?: string
167
+ inputSchema?: unknown
168
+ execute?: Function
169
+ }
170
+ >
171
+ } = {}): Promise<GenAISessionResult> {
172
+ if (!process.env.OPENAI_API_KEY) {
173
+ throw new Error('OPENAI_API_KEY environment variable is required')
174
+ }
175
+
176
+ const client = new RealtimeClient({
177
+ apiKey: process.env.OPENAI_API_KEY,
178
+ })
179
+
180
+ const audioChunkHandler = onAssistantAudioChunk || defaultAudioChunkHandler
181
+ let isAssistantSpeaking = false
182
+
183
+ // Configure session with 24kHz sample rate
184
+ client.updateSession({
185
+ instructions: systemMessage || '',
186
+ voice: 'alloy',
187
+ input_audio_format: 'pcm16',
188
+ output_audio_format: 'pcm16',
189
+ input_audio_transcription: { model: 'whisper-1' },
190
+ turn_detection: { type: 'server_vad' },
191
+ modalities: ['text', 'audio'],
192
+ temperature: 0.8,
193
+ })
194
+
195
+ // Add tools if provided
196
+ if (tools) {
197
+ for (const [name, tool] of Object.entries(tools)) {
198
+ // Convert AI SDK tool to OpenAI Realtime format
199
+ // The tool.inputSchema is a Zod schema, we need to convert it to JSON Schema
200
+ let parameters: Record<string, unknown> = {
201
+ type: 'object',
202
+ properties: {},
203
+ required: [],
204
+ }
205
+
206
+ // If the tool has a Zod schema, we can try to extract basic structure
207
+ // For now, we'll use a simple placeholder
208
+ if (tool.description?.includes('session')) {
209
+ parameters = {
210
+ type: 'object',
211
+ properties: {
212
+ sessionId: { type: 'string', description: 'The session ID' },
213
+ message: { type: 'string', description: 'The message text' },
214
+ },
215
+ required: ['sessionId'],
216
+ }
217
+ }
218
+
219
+ client.addTool(
220
+ {
221
+ type: 'function',
222
+ name,
223
+ description: tool.description || '',
224
+ parameters,
225
+ },
226
+ async (params: Record<string, unknown>) => {
227
+ try {
228
+ if (!tool.execute || typeof tool.execute !== 'function') {
229
+ return { error: 'Tool execute function not found' }
230
+ }
231
+ // Call the execute function with params
232
+ // The Tool type from 'ai' expects (input, options) but we need to handle this safely
233
+ const result = await tool.execute(params, {
234
+ abortSignal: new AbortController().signal,
235
+ toolCallId: '',
236
+ messages: [],
237
+ })
238
+ return result
239
+ } catch (error) {
240
+ openaiLogger.error(`Tool ${name} execution error:`, error)
241
+ return { error: String(error) }
242
+ }
243
+ },
244
+ )
245
+ }
246
+ }
247
+
248
+ // Set up event handlers
249
+ client.on(
250
+ 'conversation.item.created',
251
+ ({ item }: { item: ConversationItem }) => {
252
+ if (
253
+ item.role === 'assistant' &&
254
+ item.type === 'message'
255
+ ) {
256
+ // Check if this is the first audio content
257
+ const hasAudio =
258
+ Array.isArray(item.content) &&
259
+ item.content.some((c) => c.type === 'audio')
260
+ if (hasAudio && !isAssistantSpeaking && onAssistantStartSpeaking) {
261
+ isAssistantSpeaking = true
262
+ onAssistantStartSpeaking()
263
+ }
264
+ }
265
+ },
266
+ )
267
+
268
+ client.on(
269
+ 'conversation.updated',
270
+ ({
271
+ item,
272
+ delta,
273
+ }: {
274
+ item: ConversationItem
275
+ delta: ConversationEventDelta | null
276
+ }) => {
277
+ // Handle audio chunks
278
+ if (delta?.audio && item.role === 'assistant') {
279
+ if (!isAssistantSpeaking && onAssistantStartSpeaking) {
280
+ isAssistantSpeaking = true
281
+ onAssistantStartSpeaking()
282
+ }
283
+
284
+ // OpenAI provides audio as Int16Array or base64
285
+ let audioBuffer: Buffer
286
+ if (delta.audio instanceof Int16Array) {
287
+ audioBuffer = Buffer.from(delta.audio.buffer)
288
+ } else {
289
+ // Assume base64 string
290
+ audioBuffer = Buffer.from(delta.audio, 'base64')
291
+ }
292
+
293
+ // OpenAI uses 24kHz PCM16 format
294
+ audioChunkHandler({
295
+ data: audioBuffer,
296
+ mimeType: 'audio/pcm;rate=24000',
297
+ })
298
+ }
299
+
300
+ // Handle transcriptions
301
+ if (delta?.transcript) {
302
+ if (item.role === 'user') {
303
+ openaiLogger.log('User transcription:', delta.transcript)
304
+ } else if (item.role === 'assistant') {
305
+ openaiLogger.log('Assistant transcription:', delta.transcript)
306
+ }
307
+ }
308
+ },
309
+ )
310
+
311
+ client.on(
312
+ 'conversation.item.completed',
313
+ ({ item }: { item: ConversationItem }) => {
314
+ if (
315
+ 'role' in item &&
316
+ item.role === 'assistant' &&
317
+ isAssistantSpeaking &&
318
+ onAssistantStopSpeaking
319
+ ) {
320
+ isAssistantSpeaking = false
321
+ onAssistantStopSpeaking()
322
+ }
323
+ },
324
+ )
325
+
326
+ client.on('conversation.interrupted', () => {
327
+ openaiLogger.log('Assistant was interrupted')
328
+ if (isAssistantSpeaking && onAssistantInterruptSpeaking) {
329
+ isAssistantSpeaking = false
330
+ onAssistantInterruptSpeaking()
331
+ }
332
+ })
333
+
334
+ // Connect to the Realtime API
335
+ await client.connect()
336
+
337
+ const sessionResult: GenAISessionResult = {
338
+ session: {
339
+ send: (audioData: ArrayBuffer) => {
340
+ // Convert ArrayBuffer to Int16Array for OpenAI
341
+ const int16Data = new Int16Array(audioData)
342
+ client.appendInputAudio(int16Data)
343
+ },
344
+ sendText: (text: string) => {
345
+ // Send text message to OpenAI
346
+ client.sendUserMessageContent([{ type: 'input_text', text }])
347
+ },
348
+ close: () => {
349
+ client.disconnect()
350
+ },
351
+ },
352
+ stop: () => {
353
+ client.disconnect()
354
+ },
355
+ }
356
+
357
+ return sessionResult
358
+ }
@@ -0,0 +1,307 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { extractLeadingOpencodeCommand } from './opencode-command-detection.js'
3
+ import type { RegisteredUserCommand } from './store.js'
4
+
5
+ const fixtures: RegisteredUserCommand[] = [
6
+ {
7
+ name: 'build',
8
+ discordCommandName: 'build-cmd',
9
+ description: 'build the project',
10
+ source: 'command',
11
+ },
12
+ {
13
+ name: 'namespace:foo',
14
+ discordCommandName: 'namespace-foo-cmd',
15
+ description: 'namespaced',
16
+ source: 'command',
17
+ },
18
+ {
19
+ name: 'review',
20
+ discordCommandName: 'review-skill',
21
+ description: 'review skill',
22
+ source: 'skill',
23
+ },
24
+ {
25
+ name: 'plan',
26
+ discordCommandName: 'plan-mcp-prompt',
27
+ description: 'plan via mcp',
28
+ source: 'mcp',
29
+ },
30
+ ]
31
+
32
+ describe('extractLeadingOpencodeCommand', () => {
33
+ test('plain /build with args', () => {
34
+ expect(
35
+ extractLeadingOpencodeCommand('/build foo bar', fixtures),
36
+ ).toMatchInlineSnapshot(`
37
+ {
38
+ "command": {
39
+ "arguments": "foo bar",
40
+ "name": "build",
41
+ },
42
+ }
43
+ `)
44
+ })
45
+
46
+ test('plain /build no args', () => {
47
+ expect(extractLeadingOpencodeCommand('/build', fixtures))
48
+ .toMatchInlineSnapshot(`
49
+ {
50
+ "command": {
51
+ "arguments": "",
52
+ "name": "build",
53
+ },
54
+ }
55
+ `)
56
+ })
57
+
58
+ test('/build-cmd suffix resolves to build', () => {
59
+ expect(
60
+ extractLeadingOpencodeCommand('/build-cmd hello world', fixtures),
61
+ ).toMatchInlineSnapshot(`
62
+ {
63
+ "command": {
64
+ "arguments": "hello world",
65
+ "name": "build",
66
+ },
67
+ }
68
+ `)
69
+ })
70
+
71
+ test('-skill suffix', () => {
72
+ expect(
73
+ extractLeadingOpencodeCommand('/review-skill a b', fixtures),
74
+ ).toMatchInlineSnapshot(`
75
+ {
76
+ "command": {
77
+ "arguments": "a b",
78
+ "name": "review",
79
+ },
80
+ }
81
+ `)
82
+ })
83
+
84
+ test('-mcp-prompt suffix', () => {
85
+ expect(
86
+ extractLeadingOpencodeCommand('/plan-mcp-prompt go', fixtures),
87
+ ).toMatchInlineSnapshot(`
88
+ {
89
+ "command": {
90
+ "arguments": "go",
91
+ "name": "plan",
92
+ },
93
+ }
94
+ `)
95
+ })
96
+
97
+ test('original namespaced name with colon', () => {
98
+ expect(
99
+ extractLeadingOpencodeCommand('/namespace:foo arg', fixtures),
100
+ ).toMatchInlineSnapshot(`
101
+ {
102
+ "command": {
103
+ "arguments": "arg",
104
+ "name": "namespace:foo",
105
+ },
106
+ }
107
+ `)
108
+ })
109
+
110
+ test('discord-sanitized namespaced name', () => {
111
+ expect(
112
+ extractLeadingOpencodeCommand('/namespace-foo-cmd arg', fixtures),
113
+ ).toMatchInlineSnapshot(`
114
+ {
115
+ "command": {
116
+ "arguments": "arg",
117
+ "name": "namespace:foo",
118
+ },
119
+ }
120
+ `)
121
+ })
122
+
123
+ test('kimaki-cli prefix on its own line', () => {
124
+ expect(
125
+ extractLeadingOpencodeCommand(
126
+ '» **kimaki-cli:**\n/build foo bar',
127
+ fixtures,
128
+ ),
129
+ ).toMatchInlineSnapshot(`
130
+ {
131
+ "command": {
132
+ "arguments": "foo bar",
133
+ "name": "build",
134
+ },
135
+ }
136
+ `)
137
+ })
138
+
139
+ test('queue-style user prefix on its own line', () => {
140
+ expect(
141
+ extractLeadingOpencodeCommand('» **Tommy:**\n/build hey', fixtures),
142
+ ).toMatchInlineSnapshot(`
143
+ {
144
+ "command": {
145
+ "arguments": "hey",
146
+ "name": "build",
147
+ },
148
+ }
149
+ `)
150
+ })
151
+
152
+ test('username containing asterisk on its own line', () => {
153
+ expect(
154
+ extractLeadingOpencodeCommand('» **A*B:**\n/build hi', fixtures),
155
+ ).toMatchInlineSnapshot(`
156
+ {
157
+ "command": {
158
+ "arguments": "hi",
159
+ "name": "build",
160
+ },
161
+ }
162
+ `)
163
+ })
164
+
165
+ test('Context from thread wrapping still detects command', () => {
166
+ const wrapped =
167
+ 'Context from thread:\nsome starter text\n\nUser request:\n/build foo'
168
+ expect(extractLeadingOpencodeCommand(wrapped, fixtures))
169
+ .toMatchInlineSnapshot(`
170
+ {
171
+ "command": {
172
+ "arguments": "foo",
173
+ "name": "build",
174
+ },
175
+ }
176
+ `)
177
+ })
178
+
179
+ test('unknown command returns null', () => {
180
+ expect(
181
+ extractLeadingOpencodeCommand('/nothing here', fixtures),
182
+ ).toMatchInlineSnapshot(`null`)
183
+ })
184
+
185
+ test('no leading slash on any line returns null', () => {
186
+ expect(
187
+ extractLeadingOpencodeCommand('hello /build\nmore text', fixtures),
188
+ ).toMatchInlineSnapshot(`null`)
189
+ })
190
+
191
+ test('just slash returns null', () => {
192
+ expect(extractLeadingOpencodeCommand('/', fixtures)).toMatchInlineSnapshot(
193
+ `null`,
194
+ )
195
+ })
196
+
197
+ test('empty string returns null', () => {
198
+ expect(extractLeadingOpencodeCommand('', fixtures)).toMatchInlineSnapshot(
199
+ `null`,
200
+ )
201
+ })
202
+
203
+ test('empty registry returns null for tokens without Discord suffix', () => {
204
+ expect(extractLeadingOpencodeCommand('/build foo', [])).toMatchInlineSnapshot(
205
+ `null`,
206
+ )
207
+ })
208
+
209
+ test('empty registry fallback: -cmd suffix strips and returns base name', () => {
210
+ expect(
211
+ extractLeadingOpencodeCommand('/hello-test-cmd', []),
212
+ ).toMatchInlineSnapshot(`
213
+ {
214
+ "command": {
215
+ "arguments": "",
216
+ "name": "hello-test",
217
+ },
218
+ }
219
+ `)
220
+ })
221
+
222
+ test('empty registry fallback: -skill suffix with args', () => {
223
+ expect(
224
+ extractLeadingOpencodeCommand('/review-skill check auth', []),
225
+ ).toMatchInlineSnapshot(`
226
+ {
227
+ "command": {
228
+ "arguments": "check auth",
229
+ "name": "review",
230
+ },
231
+ }
232
+ `)
233
+ })
234
+
235
+ test('empty registry fallback skips non-suffixed, matches suffixed on next line', () => {
236
+ expect(
237
+ extractLeadingOpencodeCommand('/unknown\n/deploy-cmd now', []),
238
+ ).toMatchInlineSnapshot(`
239
+ {
240
+ "command": {
241
+ "arguments": "now",
242
+ "name": "deploy",
243
+ },
244
+ }
245
+ `)
246
+ })
247
+
248
+ test('leading whitespace before slash still matches', () => {
249
+ expect(
250
+ extractLeadingOpencodeCommand(' /build foo', fixtures),
251
+ ).toMatchInlineSnapshot(`
252
+ {
253
+ "command": {
254
+ "arguments": "foo",
255
+ "name": "build",
256
+ },
257
+ }
258
+ `)
259
+ })
260
+
261
+ test('first matching line wins', () => {
262
+ const prompt = 'noise line\n/build first args\n/review second args'
263
+ expect(extractLeadingOpencodeCommand(prompt, fixtures))
264
+ .toMatchInlineSnapshot(`
265
+ {
266
+ "command": {
267
+ "arguments": "first args",
268
+ "name": "build",
269
+ },
270
+ }
271
+ `)
272
+ })
273
+
274
+ test('unknown command on one line, known on next', () => {
275
+ const prompt = '/unknown foo\n/build bar'
276
+ expect(extractLeadingOpencodeCommand(prompt, fixtures))
277
+ .toMatchInlineSnapshot(`
278
+ {
279
+ "command": {
280
+ "arguments": "bar",
281
+ "name": "build",
282
+ },
283
+ }
284
+ `)
285
+ })
286
+
287
+ test('suffix strip does not clobber a command whose name happens to end in -cmd', () => {
288
+ const custom: RegisteredUserCommand[] = [
289
+ {
290
+ name: 'deploy-cmd',
291
+ discordCommandName: 'deploy-cmd-cmd',
292
+ description: '',
293
+ source: 'command',
294
+ },
295
+ ]
296
+ expect(
297
+ extractLeadingOpencodeCommand('/deploy-cmd now', custom),
298
+ ).toMatchInlineSnapshot(`
299
+ {
300
+ "command": {
301
+ "arguments": "now",
302
+ "name": "deploy-cmd",
303
+ },
304
+ }
305
+ `)
306
+ })
307
+ })