@otto-assistant/otto 0.1.2 → 0.7.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (638) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-account-identity.js +62 -0
  7. package/dist/anthropic-account-identity.test.js +38 -0
  8. package/dist/anthropic-auth-plugin.js +917 -0
  9. package/dist/anthropic-auth-state.js +303 -0
  10. package/dist/anthropic-auth-state.test.js +150 -0
  11. package/dist/bin.js +152 -0
  12. package/dist/btw-prefix-detection.js +17 -0
  13. package/dist/btw-prefix-detection.test.js +63 -0
  14. package/dist/channel-management.js +259 -0
  15. package/dist/cli-parsing.test.js +142 -0
  16. package/dist/cli-send-thread.e2e.test.js +353 -0
  17. package/dist/cli-telegram-options.test.js +99 -0
  18. package/dist/cli.js +4210 -568
  19. package/dist/commands/abort.js +65 -0
  20. package/dist/commands/action-buttons.js +245 -0
  21. package/dist/commands/add-dir.js +124 -0
  22. package/dist/commands/add-dir.test.js +126 -0
  23. package/dist/commands/add-project.js +113 -0
  24. package/dist/commands/agent.js +355 -0
  25. package/dist/commands/ask-question.js +320 -0
  26. package/dist/commands/ask-question.test.js +92 -0
  27. package/dist/commands/btw.js +121 -0
  28. package/dist/commands/cli-commands-group-a.test.js +728 -0
  29. package/dist/commands/cli-commands-group-b.test.js +695 -0
  30. package/dist/commands/compact.js +120 -0
  31. package/dist/commands/context-usage.js +140 -0
  32. package/dist/commands/create-new-project.js +130 -0
  33. package/dist/commands/diff.js +63 -0
  34. package/dist/commands/discord-commands-group-a.test.js +655 -0
  35. package/dist/commands/discord-commands-group-b.test.js +595 -0
  36. package/dist/commands/discord-commands-group-c.test.js +739 -0
  37. package/dist/commands/file-upload.js +275 -0
  38. package/dist/commands/fork-subagent.js +177 -0
  39. package/dist/commands/fork.js +262 -0
  40. package/dist/commands/gemini-apikey.js +70 -0
  41. package/dist/commands/login.js +893 -0
  42. package/dist/commands/mcp.js +239 -0
  43. package/dist/commands/memory-snapshot.js +24 -0
  44. package/dist/commands/mention-mode.js +44 -0
  45. package/dist/commands/merge-worktree.js +162 -0
  46. package/dist/commands/model-variant.js +369 -0
  47. package/dist/commands/model.js +798 -0
  48. package/dist/commands/new-worktree.js +465 -0
  49. package/dist/commands/paginated-select.js +57 -0
  50. package/dist/commands/permissions.js +274 -0
  51. package/dist/commands/queue.js +223 -0
  52. package/dist/commands/remove-project.js +115 -0
  53. package/dist/commands/restart-opencode-server.js +127 -0
  54. package/dist/commands/resume.js +149 -0
  55. package/dist/commands/run-command.js +79 -0
  56. package/dist/commands/screenshare.js +303 -0
  57. package/dist/commands/screenshare.test.js +20 -0
  58. package/dist/commands/session-id.js +78 -0
  59. package/dist/commands/session.js +176 -0
  60. package/dist/commands/share.js +80 -0
  61. package/dist/commands/tasks.js +205 -0
  62. package/dist/commands/thread-deletion-sync.js +50 -0
  63. package/dist/commands/types.js +2 -0
  64. package/dist/commands/undo-redo.js +305 -0
  65. package/dist/commands/unset-model.js +139 -0
  66. package/dist/commands/upgrade.js +48 -0
  67. package/dist/commands/user-command.js +155 -0
  68. package/dist/commands/verbosity.js +125 -0
  69. package/dist/commands/vscode.js +269 -0
  70. package/dist/commands/worktree-settings.js +43 -0
  71. package/dist/commands/worktrees.js +468 -0
  72. package/dist/condense-memory.js +33 -0
  73. package/dist/config.js +100 -255
  74. package/dist/context-awareness-plugin.js +340 -0
  75. package/dist/context-awareness-plugin.test.js +126 -0
  76. package/dist/critique-utils.js +95 -0
  77. package/dist/database.js +1355 -0
  78. package/dist/db.js +260 -0
  79. package/dist/db.test.js +138 -0
  80. package/dist/debounce-timeout.js +28 -0
  81. package/dist/debounced-process-flush.js +77 -0
  82. package/dist/discord-bot.js +1124 -0
  83. package/dist/discord-command-registration.js +567 -0
  84. package/dist/discord-urls.js +82 -0
  85. package/dist/discord-utils.js +616 -0
  86. package/dist/discord-utils.test.js +134 -0
  87. package/dist/errors.js +179 -0
  88. package/dist/escape-backticks.test.js +429 -0
  89. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  90. package/dist/eventsource-parser.test.js +327 -0
  91. package/dist/exec-async.js +26 -0
  92. package/dist/external-opencode-sync.js +480 -0
  93. package/dist/format-tables.js +491 -0
  94. package/dist/format-tables.test.js +478 -0
  95. package/dist/forum-sync/config.js +79 -0
  96. package/dist/forum-sync/discord-operations.js +154 -0
  97. package/dist/forum-sync/index.js +5 -0
  98. package/dist/forum-sync/markdown.js +113 -0
  99. package/dist/forum-sync/sync-to-discord.js +417 -0
  100. package/dist/forum-sync/sync-to-files.js +190 -0
  101. package/dist/forum-sync/types.js +53 -0
  102. package/dist/forum-sync/watchers.js +307 -0
  103. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  104. package/dist/gateway-proxy.e2e.test.js +485 -0
  105. package/dist/genai-worker-wrapper.js +111 -0
  106. package/dist/genai-worker.js +311 -0
  107. package/dist/genai.js +232 -0
  108. package/dist/generated/browser.js +17 -0
  109. package/dist/generated/client.js +37 -0
  110. package/dist/generated/commonInputTypes.js +10 -0
  111. package/dist/generated/enums.js +58 -0
  112. package/dist/generated/internal/class.js +49 -0
  113. package/dist/generated/internal/prismaNamespace.js +254 -0
  114. package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
  115. package/dist/generated/models/bot_api_keys.js +1 -0
  116. package/dist/generated/models/bot_tokens.js +1 -0
  117. package/dist/generated/models/channel_agents.js +1 -0
  118. package/dist/generated/models/channel_directories.js +1 -0
  119. package/dist/generated/models/channel_mention_mode.js +1 -0
  120. package/dist/generated/models/channel_models.js +1 -0
  121. package/dist/generated/models/channel_verbosity.js +1 -0
  122. package/dist/generated/models/channel_worktrees.js +1 -0
  123. package/dist/generated/models/forum_sync_configs.js +1 -0
  124. package/dist/generated/models/global_models.js +1 -0
  125. package/dist/generated/models/ipc_requests.js +1 -0
  126. package/dist/generated/models/part_messages.js +1 -0
  127. package/dist/generated/models/scheduled_tasks.js +1 -0
  128. package/dist/generated/models/session_agents.js +1 -0
  129. package/dist/generated/models/session_events.js +1 -0
  130. package/dist/generated/models/session_models.js +1 -0
  131. package/dist/generated/models/session_start_sources.js +1 -0
  132. package/dist/generated/models/thread_sessions.js +1 -0
  133. package/dist/generated/models/thread_worktrees.js +1 -0
  134. package/dist/generated/models.js +1 -0
  135. package/dist/heap-monitor.js +122 -0
  136. package/dist/hrana-server.js +251 -0
  137. package/dist/hrana-server.test.js +370 -0
  138. package/dist/html-actions.js +123 -0
  139. package/dist/html-actions.test.js +70 -0
  140. package/dist/html-components.js +117 -0
  141. package/dist/html-components.test.js +34 -0
  142. package/dist/image-optimizer-plugin.js +153 -0
  143. package/dist/image-utils.js +112 -0
  144. package/dist/interaction-handler.js +420 -0
  145. package/dist/ipc-polling.js +327 -0
  146. package/dist/ipc-tools-plugin.js +193 -0
  147. package/dist/ipc-utils.js +18 -0
  148. package/dist/limit-heading-depth.js +25 -0
  149. package/dist/limit-heading-depth.test.js +105 -0
  150. package/dist/logger.js +171 -0
  151. package/dist/markdown.js +342 -0
  152. package/dist/markdown.test.js +264 -0
  153. package/dist/memory-overview-plugin.js +128 -0
  154. package/dist/message-finish-field.e2e.test.js +168 -0
  155. package/dist/message-formatting.js +415 -0
  156. package/dist/message-formatting.test.js +115 -0
  157. package/dist/message-preprocessing.js +359 -0
  158. package/dist/onboarding-tutorial.js +163 -0
  159. package/dist/onboarding-welcome.js +37 -0
  160. package/dist/openai-realtime.js +224 -0
  161. package/dist/opencode-command-detection.js +65 -0
  162. package/dist/opencode-command-detection.test.js +240 -0
  163. package/dist/opencode-command.js +131 -0
  164. package/dist/opencode-command.test.js +48 -0
  165. package/dist/opencode-interrupt-plugin.js +388 -0
  166. package/dist/opencode-interrupt-plugin.test.js +463 -0
  167. package/dist/opencode.js +1124 -0
  168. package/dist/otto/branding.js +22 -0
  169. package/dist/otto/index.js +21 -0
  170. package/dist/otto-digital-twin.e2e.test.js +161 -0
  171. package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
  172. package/dist/otto-opencode-plugin.js +21 -0
  173. package/dist/otto-opencode-plugin.test.js +98 -0
  174. package/dist/parse-permission-rules.test.js +117 -0
  175. package/dist/patch-text-parser.js +97 -0
  176. package/dist/plugin-logger.js +68 -0
  177. package/dist/privacy-sanitizer.js +105 -0
  178. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  179. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  180. package/dist/queue-advanced-e2e-setup.js +790 -0
  181. package/dist/queue-advanced-footer.e2e.test.js +481 -0
  182. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  183. package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
  184. package/dist/queue-advanced-question.e2e.test.js +261 -0
  185. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  186. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  187. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  188. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  189. package/dist/queue-question-select-drain.e2e.test.js +256 -0
  190. package/dist/runtime-idle-sweeper.js +52 -0
  191. package/dist/runtime-lifecycle.e2e.test.js +514 -0
  192. package/dist/sentry.js +23 -0
  193. package/dist/session-handler/agent-utils.js +67 -0
  194. package/dist/session-handler/event-stream-state.js +475 -0
  195. package/dist/session-handler/event-stream-state.test.js +632 -0
  196. package/dist/session-handler/model-utils.js +147 -0
  197. package/dist/session-handler/opencode-session-event-log.js +94 -0
  198. package/dist/session-handler/thread-runtime-state.js +131 -0
  199. package/dist/session-handler/thread-session-runtime.js +3390 -0
  200. package/dist/session-handler.js +9 -0
  201. package/dist/session-search.js +100 -0
  202. package/dist/session-search.test.js +40 -0
  203. package/dist/session-title-rename.test.js +92 -0
  204. package/dist/skill-filter.js +31 -0
  205. package/dist/skill-filter.test.js +65 -0
  206. package/dist/startup-service.js +153 -0
  207. package/dist/startup-time.e2e.test.js +296 -0
  208. package/dist/store.js +19 -0
  209. package/dist/subagent-rate-limit-plugin.js +175 -0
  210. package/dist/system-message.js +702 -0
  211. package/dist/system-message.test.js +697 -0
  212. package/dist/task-runner.js +530 -0
  213. package/dist/task-schedule.js +213 -0
  214. package/dist/task-schedule.test.js +71 -0
  215. package/dist/test-utils.js +313 -0
  216. package/dist/thinking-utils.js +35 -0
  217. package/dist/thread-message-queue.e2e.test.js +1111 -0
  218. package/dist/tools.js +357 -0
  219. package/dist/undo-redo.e2e.test.js +161 -0
  220. package/dist/unnest-code-blocks.js +146 -0
  221. package/dist/unnest-code-blocks.test.js +673 -0
  222. package/dist/upgrade.js +156 -0
  223. package/dist/utils.js +172 -0
  224. package/dist/utils.test.js +130 -0
  225. package/dist/voice-attachment.js +34 -0
  226. package/dist/voice-handler.js +646 -0
  227. package/dist/voice-message.e2e.test.js +1021 -0
  228. package/dist/voice.js +456 -0
  229. package/dist/voice.test.js +235 -0
  230. package/dist/wait-session.js +171 -0
  231. package/dist/websockify.js +69 -0
  232. package/dist/worker-types.js +4 -0
  233. package/dist/worktree-lifecycle.e2e.test.js +311 -0
  234. package/dist/worktree-utils.js +3 -0
  235. package/dist/worktrees.js +991 -0
  236. package/dist/worktrees.test.js +415 -0
  237. package/dist/xml.js +92 -0
  238. package/dist/xml.test.js +32 -0
  239. package/package.json +90 -38
  240. package/schema.prisma +303 -0
  241. package/skills/batch/SKILL.md +87 -0
  242. package/skills/critique/SKILL.md +112 -0
  243. package/skills/egaki/SKILL.md +100 -0
  244. package/skills/errore/SKILL.md +647 -0
  245. package/skills/event-sourcing-state/SKILL.md +252 -0
  246. package/skills/goke/SKILL.md +38 -0
  247. package/skills/jitter/EDITOR.md +219 -0
  248. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  249. package/skills/jitter/SKILL.md +158 -0
  250. package/skills/jitter/jitter-clipboard.json +1042 -0
  251. package/skills/jitter/package.json +14 -0
  252. package/skills/jitter/tsconfig.json +15 -0
  253. package/skills/jitter/utils/actions.ts +212 -0
  254. package/skills/jitter/utils/export.ts +114 -0
  255. package/skills/jitter/utils/index.ts +141 -0
  256. package/skills/jitter/utils/snapshot.ts +154 -0
  257. package/skills/jitter/utils/traverse.ts +246 -0
  258. package/skills/jitter/utils/types.ts +279 -0
  259. package/skills/jitter/utils/wait.ts +133 -0
  260. package/skills/lintcn/SKILL.md +873 -0
  261. package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
  262. package/skills/new-skill/SKILL.md +237 -0
  263. package/skills/npm-package/SKILL.md +617 -0
  264. package/skills/opensrc/SKILL.md +78 -0
  265. package/skills/otto-publish/SKILL.md +61 -0
  266. package/skills/playwriter/SKILL.md +35 -0
  267. package/skills/profano/SKILL.md +16 -0
  268. package/skills/proxyman/SKILL.md +215 -0
  269. package/skills/security-review/SKILL.md +208 -0
  270. package/skills/sigillo/SKILL.md +101 -0
  271. package/skills/simplify/SKILL.md +58 -0
  272. package/skills/spiceflow/SKILL.md +28 -0
  273. package/skills/termcast/SKILL.md +945 -0
  274. package/skills/tuistory/SKILL.md +98 -0
  275. package/skills/usecomputer/SKILL.md +264 -0
  276. package/skills/x-articles/SKILL.md +554 -0
  277. package/skills/zele/SKILL.md +49 -0
  278. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  279. package/src/agent-model.e2e.test.ts +979 -0
  280. package/src/ai-tool-to-genai.test.ts +296 -0
  281. package/src/ai-tool-to-genai.ts +283 -0
  282. package/src/ai-tool.ts +39 -0
  283. package/src/anthropic-account-identity.test.ts +52 -0
  284. package/src/anthropic-account-identity.ts +77 -0
  285. package/src/anthropic-auth-plugin.ts +1139 -0
  286. package/src/anthropic-auth-state.test.ts +187 -0
  287. package/src/anthropic-auth-state.ts +386 -0
  288. package/src/bin.ts +182 -0
  289. package/src/btw-prefix-detection.test.ts +73 -0
  290. package/src/btw-prefix-detection.ts +23 -0
  291. package/src/channel-management.ts +376 -0
  292. package/src/cli-parsing.test.ts +197 -0
  293. package/src/cli-send-thread.e2e.test.ts +463 -0
  294. package/src/cli-telegram-options.test.ts +114 -0
  295. package/src/cli.ts +5718 -580
  296. package/src/commands/abort.ts +89 -0
  297. package/src/commands/action-buttons.ts +364 -0
  298. package/src/commands/add-dir.test.ts +154 -0
  299. package/src/commands/add-dir.ts +175 -0
  300. package/src/commands/add-project.ts +149 -0
  301. package/src/commands/agent.ts +496 -0
  302. package/src/commands/ask-question.test.ts +111 -0
  303. package/src/commands/ask-question.ts +455 -0
  304. package/src/commands/btw.ts +184 -0
  305. package/src/commands/cli-commands-group-a.test.ts +837 -0
  306. package/src/commands/cli-commands-group-b.test.ts +800 -0
  307. package/src/commands/compact.ts +157 -0
  308. package/src/commands/context-usage.ts +199 -0
  309. package/src/commands/create-new-project.ts +190 -0
  310. package/src/commands/diff.ts +91 -0
  311. package/src/commands/discord-commands-group-a.test.ts +789 -0
  312. package/src/commands/discord-commands-group-b.test.ts +648 -0
  313. package/src/commands/discord-commands-group-c.test.ts +882 -0
  314. package/src/commands/file-upload.ts +389 -0
  315. package/src/commands/fork-subagent.ts +263 -0
  316. package/src/commands/fork.ts +386 -0
  317. package/src/commands/gemini-apikey.ts +104 -0
  318. package/src/commands/login.ts +1181 -0
  319. package/src/commands/mcp.ts +307 -0
  320. package/src/commands/memory-snapshot.ts +30 -0
  321. package/src/commands/mention-mode.ts +68 -0
  322. package/src/commands/merge-worktree.ts +226 -0
  323. package/src/commands/model-variant.ts +488 -0
  324. package/src/commands/model.ts +1082 -0
  325. package/src/commands/new-worktree.ts +645 -0
  326. package/src/commands/paginated-select.ts +81 -0
  327. package/src/commands/permissions.ts +397 -0
  328. package/src/commands/queue.ts +293 -0
  329. package/src/commands/remove-project.ts +155 -0
  330. package/src/commands/restart-opencode-server.ts +162 -0
  331. package/src/commands/resume.ts +230 -0
  332. package/src/commands/run-command.ts +123 -0
  333. package/src/commands/screenshare.test.ts +30 -0
  334. package/src/commands/screenshare.ts +366 -0
  335. package/src/commands/session-id.ts +109 -0
  336. package/src/commands/session.ts +227 -0
  337. package/src/commands/share.ts +106 -0
  338. package/src/commands/tasks.ts +293 -0
  339. package/src/commands/thread-deletion-sync.ts +80 -0
  340. package/src/commands/types.ts +25 -0
  341. package/src/commands/undo-redo.ts +386 -0
  342. package/src/commands/unset-model.ts +174 -0
  343. package/src/commands/upgrade.ts +59 -0
  344. package/src/commands/user-command.ts +198 -0
  345. package/src/commands/verbosity.ts +173 -0
  346. package/src/commands/vscode.ts +342 -0
  347. package/src/commands/worktree-settings.ts +70 -0
  348. package/src/commands/worktrees.ts +645 -0
  349. package/src/condense-memory.ts +36 -0
  350. package/src/config.ts +103 -339
  351. package/src/context-awareness-plugin.test.ts +144 -0
  352. package/src/context-awareness-plugin.ts +469 -0
  353. package/src/critique-utils.ts +139 -0
  354. package/src/database.ts +1949 -0
  355. package/src/db.test.ts +162 -0
  356. package/src/db.ts +295 -0
  357. package/src/debounce-timeout.ts +43 -0
  358. package/src/debounced-process-flush.ts +104 -0
  359. package/src/discord-bot.ts +1507 -0
  360. package/src/discord-command-registration.ts +752 -0
  361. package/src/discord-urls.ts +89 -0
  362. package/src/discord-utils.test.ts +153 -0
  363. package/src/discord-utils.ts +846 -0
  364. package/src/errors.ts +232 -0
  365. package/src/escape-backticks.test.ts +469 -0
  366. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  367. package/src/eventsource-parser.test.ts +351 -0
  368. package/src/exec-async.ts +35 -0
  369. package/src/external-opencode-sync.ts +685 -0
  370. package/src/format-tables.test.ts +515 -0
  371. package/src/format-tables.ts +718 -0
  372. package/src/forum-sync/config.ts +92 -0
  373. package/src/forum-sync/discord-operations.ts +241 -0
  374. package/src/forum-sync/index.ts +9 -0
  375. package/src/forum-sync/markdown.ts +172 -0
  376. package/src/forum-sync/sync-to-discord.ts +595 -0
  377. package/src/forum-sync/sync-to-files.ts +294 -0
  378. package/src/forum-sync/types.ts +175 -0
  379. package/src/forum-sync/watchers.ts +454 -0
  380. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  381. package/src/gateway-proxy.e2e.test.ts +644 -0
  382. package/src/genai-worker-wrapper.ts +164 -0
  383. package/src/genai-worker.ts +386 -0
  384. package/src/genai.ts +321 -0
  385. package/src/generated/browser.ts +114 -0
  386. package/src/generated/client.ts +138 -0
  387. package/src/generated/commonInputTypes.ts +770 -0
  388. package/src/generated/enums.ts +98 -0
  389. package/src/generated/internal/class.ts +384 -0
  390. package/src/generated/internal/prismaNamespace.ts +2394 -0
  391. package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
  392. package/src/generated/models/bot_api_keys.ts +1288 -0
  393. package/src/generated/models/bot_tokens.ts +1700 -0
  394. package/src/generated/models/channel_agents.ts +1256 -0
  395. package/src/generated/models/channel_directories.ts +1859 -0
  396. package/src/generated/models/channel_mention_mode.ts +1300 -0
  397. package/src/generated/models/channel_models.ts +1288 -0
  398. package/src/generated/models/channel_verbosity.ts +1228 -0
  399. package/src/generated/models/channel_worktrees.ts +1300 -0
  400. package/src/generated/models/forum_sync_configs.ts +1452 -0
  401. package/src/generated/models/global_models.ts +1288 -0
  402. package/src/generated/models/ipc_requests.ts +1485 -0
  403. package/src/generated/models/part_messages.ts +1302 -0
  404. package/src/generated/models/scheduled_tasks.ts +2320 -0
  405. package/src/generated/models/session_agents.ts +1086 -0
  406. package/src/generated/models/session_events.ts +1439 -0
  407. package/src/generated/models/session_models.ts +1114 -0
  408. package/src/generated/models/session_start_sources.ts +1408 -0
  409. package/src/generated/models/thread_sessions.ts +1781 -0
  410. package/src/generated/models/thread_worktrees.ts +1356 -0
  411. package/src/generated/models.ts +30 -0
  412. package/src/heap-monitor.ts +152 -0
  413. package/src/hrana-server.test.ts +434 -0
  414. package/src/hrana-server.ts +299 -0
  415. package/src/html-actions.test.ts +87 -0
  416. package/src/html-actions.ts +174 -0
  417. package/src/html-components.test.ts +38 -0
  418. package/src/html-components.ts +181 -0
  419. package/src/image-optimizer-plugin.ts +194 -0
  420. package/src/image-utils.ts +149 -0
  421. package/src/interaction-handler.ts +610 -0
  422. package/src/ipc-polling.ts +427 -0
  423. package/src/ipc-tools-plugin.ts +236 -0
  424. package/src/ipc-utils.ts +29 -0
  425. package/src/limit-heading-depth.test.ts +116 -0
  426. package/src/limit-heading-depth.ts +26 -0
  427. package/src/logger.ts +215 -0
  428. package/src/markdown.test.ts +315 -0
  429. package/src/markdown.ts +410 -0
  430. package/src/memory-overview-plugin.ts +163 -0
  431. package/src/message-finish-field.e2e.test.ts +195 -0
  432. package/src/message-formatting.test.ts +126 -0
  433. package/src/message-formatting.ts +535 -0
  434. package/src/message-preprocessing.ts +488 -0
  435. package/src/onboarding-tutorial.ts +167 -0
  436. package/src/onboarding-welcome.ts +49 -0
  437. package/src/openai-realtime.ts +358 -0
  438. package/src/opencode-command-detection.test.ts +307 -0
  439. package/src/opencode-command-detection.ts +76 -0
  440. package/src/opencode-command.test.ts +70 -0
  441. package/src/opencode-command.ts +191 -0
  442. package/src/opencode-interrupt-plugin.test.ts +682 -0
  443. package/src/opencode-interrupt-plugin.ts +507 -0
  444. package/src/opencode.ts +1462 -0
  445. package/src/otto/branding.ts +23 -0
  446. package/src/otto/index.ts +22 -0
  447. package/src/otto-digital-twin.e2e.test.ts +199 -0
  448. package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
  449. package/src/otto-opencode-plugin.test.ts +108 -0
  450. package/src/otto-opencode-plugin.ts +22 -0
  451. package/src/parse-permission-rules.test.ts +127 -0
  452. package/src/patch-text-parser.ts +107 -0
  453. package/src/plugin-logger.ts +84 -0
  454. package/src/privacy-sanitizer.ts +142 -0
  455. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  456. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  457. package/src/queue-advanced-e2e-setup.ts +877 -0
  458. package/src/queue-advanced-footer.e2e.test.ts +591 -0
  459. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  460. package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
  461. package/src/queue-advanced-question.e2e.test.ts +316 -0
  462. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  463. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  464. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  465. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  466. package/src/queue-question-select-drain.e2e.test.ts +327 -0
  467. package/src/runtime-idle-sweeper.ts +76 -0
  468. package/src/runtime-lifecycle.e2e.test.ts +651 -0
  469. package/src/schema.sql +174 -0
  470. package/src/sentry.ts +26 -0
  471. package/src/session-handler/agent-utils.ts +99 -0
  472. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  473. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  474. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  475. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  476. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  477. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  478. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  479. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  480. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  481. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  482. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  483. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  484. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  485. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  486. package/src/session-handler/event-stream-state.test.ts +717 -0
  487. package/src/session-handler/event-stream-state.ts +706 -0
  488. package/src/session-handler/model-utils.ts +217 -0
  489. package/src/session-handler/opencode-session-event-log.ts +130 -0
  490. package/src/session-handler/thread-runtime-state.ts +247 -0
  491. package/src/session-handler/thread-session-runtime.ts +4440 -0
  492. package/src/session-handler.ts +15 -0
  493. package/src/session-search.test.ts +50 -0
  494. package/src/session-search.ts +148 -0
  495. package/src/session-title-rename.test.ts +130 -0
  496. package/src/skill-filter.test.ts +83 -0
  497. package/src/skill-filter.ts +42 -0
  498. package/src/startup-service.ts +200 -0
  499. package/src/startup-time.e2e.test.ts +373 -0
  500. package/src/store.ts +139 -0
  501. package/src/subagent-rate-limit-plugin.ts +218 -0
  502. package/src/system-message.test.ts +710 -0
  503. package/src/system-message.ts +814 -0
  504. package/src/task-runner.ts +725 -0
  505. package/src/task-schedule.test.ts +84 -0
  506. package/src/task-schedule.ts +317 -0
  507. package/src/test-utils.ts +451 -0
  508. package/src/thinking-utils.ts +61 -0
  509. package/src/thread-message-queue.e2e.test.ts +1350 -0
  510. package/src/tools.ts +430 -0
  511. package/src/undici.d.ts +12 -0
  512. package/src/undo-redo.e2e.test.ts +209 -0
  513. package/src/unnest-code-blocks.test.ts +713 -0
  514. package/src/unnest-code-blocks.ts +185 -0
  515. package/src/upgrade.ts +185 -0
  516. package/src/utils.test.ts +155 -0
  517. package/src/utils.ts +265 -0
  518. package/src/voice-attachment.ts +51 -0
  519. package/src/voice-handler.ts +908 -0
  520. package/src/voice-message.e2e.test.ts +1255 -0
  521. package/src/voice.test.ts +281 -0
  522. package/src/voice.ts +638 -0
  523. package/src/wait-session.ts +273 -0
  524. package/src/websockify.ts +101 -0
  525. package/src/worker-types.ts +64 -0
  526. package/src/worktree-lifecycle.e2e.test.ts +396 -0
  527. package/src/worktree-utils.ts +4 -0
  528. package/src/worktrees.test.ts +489 -0
  529. package/src/worktrees.ts +1370 -0
  530. package/src/xml.test.ts +38 -0
  531. package/src/xml.ts +121 -0
  532. package/README.md +0 -142
  533. package/dist/cli.d.ts +0 -3
  534. package/dist/cli.d.ts.map +0 -1
  535. package/dist/cli.js.map +0 -1
  536. package/dist/config.d.ts +0 -39
  537. package/dist/config.d.ts.map +0 -1
  538. package/dist/config.js.map +0 -1
  539. package/dist/config.test.d.ts +0 -2
  540. package/dist/config.test.d.ts.map +0 -1
  541. package/dist/config.test.js +0 -202
  542. package/dist/config.test.js.map +0 -1
  543. package/dist/detect.d.ts +0 -9
  544. package/dist/detect.d.ts.map +0 -1
  545. package/dist/detect.js +0 -40
  546. package/dist/detect.js.map +0 -1
  547. package/dist/detect.test.d.ts +0 -2
  548. package/dist/detect.test.d.ts.map +0 -1
  549. package/dist/detect.test.js +0 -26
  550. package/dist/detect.test.js.map +0 -1
  551. package/dist/docker.d.ts +0 -7
  552. package/dist/docker.d.ts.map +0 -1
  553. package/dist/docker.js +0 -17
  554. package/dist/docker.js.map +0 -1
  555. package/dist/docker.test.d.ts +0 -2
  556. package/dist/docker.test.d.ts.map +0 -1
  557. package/dist/docker.test.js +0 -12
  558. package/dist/docker.test.js.map +0 -1
  559. package/dist/health.d.ts +0 -31
  560. package/dist/health.d.ts.map +0 -1
  561. package/dist/health.js +0 -117
  562. package/dist/health.js.map +0 -1
  563. package/dist/health.test.d.ts +0 -2
  564. package/dist/health.test.d.ts.map +0 -1
  565. package/dist/health.test.js +0 -52
  566. package/dist/health.test.js.map +0 -1
  567. package/dist/index.d.ts +0 -20
  568. package/dist/index.d.ts.map +0 -1
  569. package/dist/index.js +0 -15
  570. package/dist/index.js.map +0 -1
  571. package/dist/index.test.d.ts +0 -2
  572. package/dist/index.test.d.ts.map +0 -1
  573. package/dist/index.test.js +0 -8
  574. package/dist/index.test.js.map +0 -1
  575. package/dist/installer.d.ts +0 -10
  576. package/dist/installer.d.ts.map +0 -1
  577. package/dist/installer.js +0 -50
  578. package/dist/installer.js.map +0 -1
  579. package/dist/installer.test.d.ts +0 -2
  580. package/dist/installer.test.d.ts.map +0 -1
  581. package/dist/installer.test.js +0 -43
  582. package/dist/installer.test.js.map +0 -1
  583. package/dist/lifecycle.d.ts +0 -10
  584. package/dist/lifecycle.d.ts.map +0 -1
  585. package/dist/lifecycle.js +0 -45
  586. package/dist/lifecycle.js.map +0 -1
  587. package/dist/lifecycle.test.d.ts +0 -2
  588. package/dist/lifecycle.test.d.ts.map +0 -1
  589. package/dist/lifecycle.test.js +0 -20
  590. package/dist/lifecycle.test.js.map +0 -1
  591. package/dist/manifest.d.ts +0 -18
  592. package/dist/manifest.d.ts.map +0 -1
  593. package/dist/manifest.js +0 -30
  594. package/dist/manifest.js.map +0 -1
  595. package/dist/skills-baseline.d.ts +0 -7
  596. package/dist/skills-baseline.d.ts.map +0 -1
  597. package/dist/skills-baseline.js +0 -9
  598. package/dist/skills-baseline.js.map +0 -1
  599. package/dist/skills.d.ts +0 -110
  600. package/dist/skills.d.ts.map +0 -1
  601. package/dist/skills.js +0 -429
  602. package/dist/skills.js.map +0 -1
  603. package/dist/skills.test.d.ts +0 -2
  604. package/dist/skills.test.d.ts.map +0 -1
  605. package/dist/skills.test.js +0 -416
  606. package/dist/skills.test.js.map +0 -1
  607. package/dist/sync.d.ts +0 -10
  608. package/dist/sync.d.ts.map +0 -1
  609. package/dist/sync.js +0 -39
  610. package/dist/sync.js.map +0 -1
  611. package/dist/tenant.d.ts +0 -13
  612. package/dist/tenant.d.ts.map +0 -1
  613. package/dist/tenant.js +0 -105
  614. package/dist/tenant.js.map +0 -1
  615. package/dist/tenant.test.d.ts +0 -2
  616. package/dist/tenant.test.d.ts.map +0 -1
  617. package/dist/tenant.test.js +0 -37
  618. package/dist/tenant.test.js.map +0 -1
  619. package/src/config.test.ts +0 -237
  620. package/src/detect.test.ts +0 -29
  621. package/src/detect.ts +0 -52
  622. package/src/docker.test.ts +0 -12
  623. package/src/docker.ts +0 -23
  624. package/src/health.test.ts +0 -61
  625. package/src/health.ts +0 -158
  626. package/src/index.test.ts +0 -8
  627. package/src/index.ts +0 -62
  628. package/src/installer.test.ts +0 -52
  629. package/src/installer.ts +0 -62
  630. package/src/lifecycle.test.ts +0 -23
  631. package/src/lifecycle.ts +0 -49
  632. package/src/manifest.ts +0 -42
  633. package/src/skills-baseline.ts +0 -14
  634. package/src/skills.test.ts +0 -503
  635. package/src/skills.ts +0 -512
  636. package/src/sync.ts +0 -53
  637. package/src/tenant.test.ts +0 -49
  638. package/src/tenant.ts +0 -120
@@ -0,0 +1,197 @@
1
+ // Regression tests for CLI argument parsing around Discord ID string preservation.
2
+ import { describe, expect, test } from 'vitest'
3
+ import { execAsync } from './exec-async.js'
4
+
5
+ async function parseWithGoke(argv: string[]) {
6
+ const script = [
7
+ "import { goke } from 'goke'",
8
+ 'const cli = goke(\'otto\')',
9
+ "cli.command('send', 'Send a message').option('-c, --channel <channelId>', 'Discord channel ID').option('--thread <threadId>', 'Thread ID').option('--session <sessionId>', 'Session ID').option('--send-at <schedule>', 'Schedule').option('--gateway-platform <platform>', 'Gateway platform').option('--telegram-bot-token <token>', 'Telegram bot token').option('--telegram-chat-id <id>', 'Telegram chat ID')",
10
+ "cli.command('session archive <threadId>', 'Archive a thread')",
11
+ "cli.command('session search <query>', 'Search sessions').option('--channel <channelId>', 'Discord channel ID').option('--project <path>', 'Project path')",
12
+ "cli.command('session export-events-jsonl', 'Export in-memory events to JSONL').option('--session <sessionId>', 'Session ID').option('--out <file>', 'Output path')",
13
+ "cli.command('add-project', 'Add a project').option('-g, --guild <guildId>', 'Discord guild/server ID')",
14
+ "cli.command('task delete <id>', 'Delete task')",
15
+ "cli.command('anthropic-accounts list', 'List stored Anthropic accounts')",
16
+ "cli.command('anthropic-accounts remove <indexOrEmail>', 'Remove stored Anthropic account')",
17
+ `const result = cli.parse(${JSON.stringify(argv)}, { run: false })`,
18
+ 'process.stdout.write(JSON.stringify({ args: result.args, options: result.options }))',
19
+ ].join(';')
20
+
21
+ const { stdout } = await execAsync(`node --input-type=module -e ${JSON.stringify(script)}`, {
22
+ cwd: import.meta.dirname,
23
+ timeout: 10_000,
24
+ })
25
+ return JSON.parse(stdout) as {
26
+ args: string[]
27
+ options: Record<string, string>
28
+ }
29
+ }
30
+
31
+ async function getHelpOutput() {
32
+ const script = [
33
+ "import { goke } from 'goke'",
34
+ 'const stdout = { text: \'\', write(data) { this.text += String(data) } }',
35
+ "const cli = goke('otto', { stdout })",
36
+ "cli.command('send', 'Send a message')",
37
+ "cli.command('anthropic-accounts list', 'List stored Anthropic accounts')",
38
+ 'cli.help()',
39
+ "cli.parse(['node', 'otto', '--help'], { run: false })",
40
+ 'process.stdout.write(stdout.text)',
41
+ ].join(';')
42
+
43
+ const { stdout } = await execAsync(`node --input-type=module -e ${JSON.stringify(script)}`, {
44
+ cwd: import.meta.dirname,
45
+ timeout: 10_000,
46
+ })
47
+ return stdout
48
+ }
49
+
50
+ describe('goke CLI ID parsing', () => {
51
+ test('keeps large Discord IDs as strings', async () => {
52
+ const channelId = '1234567890123456789'
53
+ const threadId = '9876543210987654321'
54
+ const sessionId = '1111222233334444555'
55
+
56
+ const channelResult = await parseWithGoke(
57
+ ['node', 'otto', 'send', '--channel', channelId],
58
+ )
59
+ expect(channelResult.options.channel).toBe(channelId)
60
+ expect(typeof channelResult.options.channel).toBe('string')
61
+
62
+ const threadResult = await parseWithGoke(
63
+ ['node', 'otto', 'send', '--thread', threadId],
64
+ )
65
+ expect(threadResult.options.thread).toBe(threadId)
66
+ expect(typeof threadResult.options.thread).toBe('string')
67
+
68
+ const sessionResult = await parseWithGoke(
69
+ ['node', 'otto', 'send', '--session', sessionId],
70
+ )
71
+ expect(sessionResult.options.session).toBe(sessionId)
72
+ expect(typeof sessionResult.options.session).toBe('string')
73
+ })
74
+
75
+ test('preserves leading zeros in Discord IDs', async () => {
76
+ const guildId = '001230045600789'
77
+
78
+ const result = await parseWithGoke(
79
+ ['node', 'otto', 'add-project', '--guild', guildId],
80
+ )
81
+
82
+ expect(result.options.guild).toBe(guildId)
83
+ expect(typeof result.options.guild).toBe('string')
84
+ })
85
+
86
+ test('keeps session archive thread ID as string', async () => {
87
+ const threadId = '0098765432109876543'
88
+
89
+ const result = await parseWithGoke(
90
+ ['node', 'otto', 'session', 'archive', threadId],
91
+ )
92
+
93
+ expect(result.args[0]).toBe(threadId)
94
+ expect(typeof result.args[0]).toBe('string')
95
+ })
96
+
97
+ test('keeps session search regex and channel ID as strings', async () => {
98
+ const channelId = '0012345678901234567'
99
+ const query = '/error\\s+42/i'
100
+
101
+ const result = await parseWithGoke(
102
+ ['node', 'otto', 'session', 'search', query, '--channel', channelId],
103
+ )
104
+
105
+ expect(result.args[0]).toBe(query)
106
+ expect(typeof result.args[0]).toBe('string')
107
+ expect(result.options.channel).toBe(channelId)
108
+ expect(typeof result.options.channel).toBe('string')
109
+ })
110
+
111
+ test('keeps session export options as strings', async () => {
112
+ const sessionId = '001111222233334444'
113
+ const outPath = './tmp/session-events.jsonl'
114
+
115
+ const result = await parseWithGoke(
116
+ [
117
+ 'node',
118
+ 'otto',
119
+ 'session',
120
+ 'export-events-jsonl',
121
+ '--session',
122
+ sessionId,
123
+ '--out',
124
+ outPath,
125
+ ],
126
+ )
127
+
128
+ expect(result.options.session).toBe(sessionId)
129
+ expect(typeof result.options.session).toBe('string')
130
+ expect(result.options.out).toBe(outPath)
131
+ expect(typeof result.options.out).toBe('string')
132
+ })
133
+
134
+ test('keeps --send-at cron string intact', async () => {
135
+ const cron = '0 9 * * 1'
136
+
137
+ const result = await parseWithGoke(['node', 'otto', 'send', '--send-at', cron])
138
+
139
+ expect(result.options.sendAt).toBe(cron)
140
+ expect(typeof result.options.sendAt).toBe('string')
141
+ })
142
+
143
+ test('keeps gateway onboarding options as strings', async () => {
144
+ const platform = 'telegram'
145
+ const botToken = '123456:ABCDEF'
146
+ const chatId = '-1001234567890'
147
+
148
+ const result = await parseWithGoke([
149
+ 'node',
150
+ 'otto',
151
+ 'send',
152
+ '--gateway-platform',
153
+ platform,
154
+ '--telegram-bot-token',
155
+ botToken,
156
+ `--telegram-chat-id=${chatId}`,
157
+ ])
158
+
159
+ expect(result.options.gatewayPlatform).toBe(platform)
160
+ expect(typeof result.options.gatewayPlatform).toBe('string')
161
+ expect(result.options.telegramBotToken).toBe(botToken)
162
+ expect(typeof result.options.telegramBotToken).toBe('string')
163
+ expect(result.options.telegramChatId).toBe(chatId)
164
+ expect(typeof result.options.telegramChatId).toBe('string')
165
+ })
166
+
167
+ test('keeps task delete ID as string before validation', async () => {
168
+ const taskId = '0012345'
169
+
170
+ const result = await parseWithGoke(['node', 'otto', 'task', 'delete', taskId])
171
+
172
+ expect(result.args[0]).toBe(taskId)
173
+ expect(typeof result.args[0]).toBe('string')
174
+ })
175
+
176
+ test('anthropic account remove parses index and email as strings', async () => {
177
+ const indexResult = await parseWithGoke(
178
+ ['node', 'otto', 'anthropic-accounts', 'remove', '2'],
179
+ )
180
+
181
+ const emailResult = await parseWithGoke(
182
+ ['node', 'otto', 'anthropic-accounts', 'remove', 'user@example.com'],
183
+ )
184
+
185
+ expect(indexResult.args[0]).toBe('2')
186
+ expect(typeof indexResult.args[0]).toBe('string')
187
+ expect(emailResult.args[0]).toBe('user@example.com')
188
+ expect(typeof emailResult.args[0]).toBe('string')
189
+ })
190
+
191
+ test('anthropic account commands are included in help output', async () => {
192
+ const stdout = await getHelpOutput()
193
+
194
+ expect(stdout).toContain('send')
195
+ expect(stdout).toContain('anthropic-accounts')
196
+ })
197
+ })
@@ -0,0 +1,463 @@
1
+ // E2e test for `otto send --channel` flow.
2
+ // Reproduces the race condition where the bot's MessageCreate GuildText handler
3
+ // tries to call startThread() on the same message that the CLI already created
4
+ // a thread for via REST, causing DiscordAPIError[160004].
5
+ //
6
+ // The test simulates the exact flow: bot posts a starter message with a
7
+ // `start: true` embed marker, then creates a thread on that message via REST.
8
+ // The ThreadCreate handler should pick it up and start a session. The
9
+ // MessageCreate handler must NOT try to startThread() on the same message.
10
+ //
11
+ // Uses opencode-deterministic-provider (no real LLM calls).
12
+ // Poll timeouts: 4s max, 100ms interval.
13
+
14
+ import fs from 'node:fs'
15
+ import path from 'node:path'
16
+ import url from 'node:url'
17
+ import { describe, beforeAll, afterAll, test, expect } from 'vitest'
18
+ import {
19
+ ChannelType,
20
+ Client,
21
+ GatewayIntentBits,
22
+ Partials,
23
+ Routes,
24
+ } from 'discord.js'
25
+ import { DigitalDiscord } from 'discord-digital-twin/src'
26
+ import {
27
+ buildDeterministicOpencodeConfig,
28
+ type DeterministicMatcher,
29
+ } from 'opencode-deterministic-provider'
30
+ import { setDataDir } from './config.js'
31
+ import { store } from './store.js'
32
+ import { startDiscordBot } from './discord-bot.js'
33
+ import {
34
+ setBotToken,
35
+ initDatabase,
36
+ closeDatabase,
37
+ setChannelDirectory,
38
+ setChannelVerbosity,
39
+ type VerbosityLevel,
40
+ } from './database.js'
41
+ import { startHranaServer, stopHranaServer } from './hrana-server.js'
42
+ import {
43
+ initializeOpencodeForDirectory,
44
+ stopOpencodeServer,
45
+ } from './opencode.js'
46
+ import {
47
+ chooseLockPort,
48
+ cleanupTestSessions,
49
+ initTestGitRepo,
50
+ waitForBotMessageContaining,
51
+ waitForFooterMessage,
52
+ } from './test-utils.js'
53
+ import YAML from 'yaml'
54
+ import type { ThreadStartMarker } from './system-message.js'
55
+
56
+ const TEST_USER_ID = '200000000000000830'
57
+ const TEXT_CHANNEL_ID = '200000000000000831'
58
+ const BOT_USER_ID = '200000000000000832'
59
+
60
+ function createRunDirectories() {
61
+ const root = path.resolve(process.cwd(), 'tmp', 'cli-send-thread-e2e')
62
+ fs.mkdirSync(root, { recursive: true })
63
+ const dataDir = fs.mkdtempSync(path.join(root, 'data-'))
64
+ const projectDirectory = path.join(root, 'project')
65
+ fs.mkdirSync(projectDirectory, { recursive: true })
66
+ initTestGitRepo(projectDirectory)
67
+ return { root, dataDir, projectDirectory }
68
+ }
69
+
70
+ function createDiscordJsClient({ restUrl }: { restUrl: string }) {
71
+ return new Client({
72
+ intents: [
73
+ GatewayIntentBits.Guilds,
74
+ GatewayIntentBits.GuildMessages,
75
+ GatewayIntentBits.MessageContent,
76
+ GatewayIntentBits.GuildVoiceStates,
77
+ ],
78
+ partials: [
79
+ Partials.Channel,
80
+ Partials.Message,
81
+ Partials.User,
82
+ Partials.ThreadMember,
83
+ ],
84
+ rest: {
85
+ api: restUrl,
86
+ version: '10',
87
+ },
88
+ })
89
+ }
90
+
91
+ function createDeterministicMatchers(): DeterministicMatcher[] {
92
+ const userReplyMatcher: DeterministicMatcher = {
93
+ id: 'user-reply',
94
+ priority: 10,
95
+ when: {
96
+ lastMessageRole: 'user',
97
+ latestUserTextIncludes: 'Reply with exactly:',
98
+ },
99
+ then: {
100
+ parts: [
101
+ { type: 'stream-start', warnings: [] },
102
+ { type: 'text-start', id: 'default-reply' },
103
+ { type: 'text-delta', id: 'default-reply', delta: 'ok' },
104
+ { type: 'text-end', id: 'default-reply' },
105
+ {
106
+ type: 'finish',
107
+ finishReason: 'stop',
108
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
109
+ },
110
+ ],
111
+ partDelaysMs: [0, 100, 0, 0, 0],
112
+ },
113
+ }
114
+
115
+ // Catch-all: any user message gets a reply
116
+ const catchAll: DeterministicMatcher = {
117
+ id: 'catch-all',
118
+ priority: 0,
119
+ when: { lastMessageRole: 'user' },
120
+ then: {
121
+ parts: [
122
+ { type: 'stream-start', warnings: [] },
123
+ { type: 'text-start', id: 'catch' },
124
+ { type: 'text-delta', id: 'catch', delta: 'caught-by-model' },
125
+ { type: 'text-end', id: 'catch' },
126
+ {
127
+ type: 'finish',
128
+ finishReason: 'stop',
129
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
130
+ },
131
+ ],
132
+ },
133
+ }
134
+
135
+ return [userReplyMatcher, catchAll]
136
+ }
137
+
138
+ describe('otto send --channel thread creation', () => {
139
+ let directories: ReturnType<typeof createRunDirectories>
140
+ let discord: DigitalDiscord
141
+ let botClient: Client
142
+ let previousDefaultVerbosity: VerbosityLevel | null = null
143
+ let testStartTime = Date.now()
144
+
145
+ beforeAll(async () => {
146
+ testStartTime = Date.now()
147
+ directories = createRunDirectories()
148
+ const lockPort = chooseLockPort({ key: TEXT_CHANNEL_ID })
149
+
150
+ process.env['OTTO_LOCK_PORT'] = String(lockPort)
151
+ setDataDir(directories.dataDir)
152
+ previousDefaultVerbosity = store.getState().defaultVerbosity
153
+ store.setState({ defaultVerbosity: 'tools_and_text' })
154
+
155
+ const digitalDiscordDbPath = path.join(
156
+ directories.dataDir,
157
+ 'digital-discord.db',
158
+ )
159
+
160
+ discord = new DigitalDiscord({
161
+ botUser: { id: BOT_USER_ID },
162
+ guild: {
163
+ name: 'CLI Send E2E Guild',
164
+ // Use bot as guild owner so bot-authored messages pass
165
+ // hasOttoBotPermission (owner check). This matches production where
166
+ // the bot typically has admin or is the app owner. Without this, the
167
+ // MessageCreate handler drops bot messages before reaching the GuildText
168
+ // path, hiding the race condition we're testing.
169
+ ownerId: BOT_USER_ID,
170
+ },
171
+ channels: [
172
+ {
173
+ id: TEXT_CHANNEL_ID,
174
+ name: 'cli-send-e2e',
175
+ type: ChannelType.GuildText,
176
+ },
177
+ ],
178
+ users: [
179
+ {
180
+ id: TEST_USER_ID,
181
+ username: 'cli-send-tester',
182
+ },
183
+ ],
184
+ dbUrl: `file:${digitalDiscordDbPath}`,
185
+ })
186
+
187
+ await discord.start()
188
+
189
+ const providerNpm = url
190
+ .pathToFileURL(
191
+ path.resolve(
192
+ process.cwd(),
193
+ '..',
194
+ 'opencode-deterministic-provider',
195
+ 'src',
196
+ 'index.ts',
197
+ ),
198
+ )
199
+ .toString()
200
+
201
+ const opencodeConfig = buildDeterministicOpencodeConfig({
202
+ providerName: 'deterministic-provider',
203
+ providerNpm,
204
+ model: 'deterministic-v2',
205
+ smallModel: 'deterministic-v2',
206
+ settings: {
207
+ strict: false,
208
+ matchers: createDeterministicMatchers(),
209
+ },
210
+ })
211
+ fs.writeFileSync(
212
+ path.join(directories.projectDirectory, 'opencode.json'),
213
+ JSON.stringify(opencodeConfig, null, 2),
214
+ )
215
+
216
+ const dbPath = path.join(directories.dataDir, 'discord-sessions.db')
217
+ const hranaResult = await startHranaServer({ dbPath })
218
+ if (hranaResult instanceof Error) {
219
+ throw hranaResult
220
+ }
221
+ process.env['OTTO_DB_URL'] = hranaResult
222
+ await initDatabase()
223
+ await setBotToken(discord.botUserId, discord.botToken)
224
+
225
+ await setChannelDirectory({
226
+ channelId: TEXT_CHANNEL_ID,
227
+ directory: directories.projectDirectory,
228
+ channelType: 'text',
229
+ })
230
+ await setChannelVerbosity(TEXT_CHANNEL_ID, 'tools_and_text')
231
+
232
+ botClient = createDiscordJsClient({ restUrl: discord.restUrl })
233
+ await startDiscordBot({
234
+ token: discord.botToken,
235
+ appId: discord.botUserId,
236
+ discordClient: botClient,
237
+ })
238
+
239
+ // Pre-warm the opencode server
240
+ const warmup = await initializeOpencodeForDirectory(
241
+ directories.projectDirectory,
242
+ )
243
+ if (warmup instanceof Error) {
244
+ throw warmup
245
+ }
246
+ }, 20_000)
247
+
248
+ afterAll(async () => {
249
+ if (directories) {
250
+ await cleanupTestSessions({
251
+ projectDirectory: directories.projectDirectory,
252
+ testStartTime,
253
+ })
254
+ }
255
+ if (botClient) {
256
+ botClient.destroy()
257
+ }
258
+ await stopOpencodeServer()
259
+ await Promise.all([
260
+ closeDatabase().catch(() => {
261
+ return
262
+ }),
263
+ stopHranaServer().catch(() => {
264
+ return
265
+ }),
266
+ discord?.stop().catch(() => {
267
+ return
268
+ }),
269
+ ])
270
+ delete process.env['OTTO_LOCK_PORT']
271
+ delete process.env['OTTO_DB_URL']
272
+ if (previousDefaultVerbosity) {
273
+ store.setState({ defaultVerbosity: previousDefaultVerbosity })
274
+ }
275
+ if (directories) {
276
+ fs.rmSync(directories.dataDir, { recursive: true, force: true })
277
+ }
278
+ }, 5_000)
279
+
280
+ test(
281
+ 'otto send --prompt "/hello-test-cmd" falls through as text when registeredUserCommands is empty (repro #97)',
282
+ async () => {
283
+ // Reproduce GitHub #97: when registeredUserCommands is empty (gateway mode
284
+ // startup race, or backgroundInit not complete), the prompt "/hello-test-cmd"
285
+ // is NOT detected as a command and is sent to the model as plain text.
286
+
287
+ const prevCommands = store.getState().registeredUserCommands
288
+ // Ensure store is empty — this is the bug condition
289
+ store.setState({ registeredUserCommands: [] })
290
+
291
+ try {
292
+ const prompt = '/hello-test-cmd'
293
+ const embedMarker: ThreadStartMarker = {
294
+ start: true,
295
+ username: 'cli-send-tester',
296
+ userId: TEST_USER_ID,
297
+ }
298
+
299
+ const starterMessage = (await botClient.rest.post(
300
+ Routes.channelMessages(TEXT_CHANNEL_ID),
301
+ {
302
+ body: {
303
+ content: prompt,
304
+ embeds: [
305
+ { color: 0x2b2d31, footer: { text: YAML.stringify(embedMarker) } },
306
+ ],
307
+ },
308
+ },
309
+ )) as { id: string }
310
+
311
+ await new Promise((resolve) => {
312
+ setTimeout(resolve, 200)
313
+ })
314
+
315
+ const threadData = (await botClient.rest.post(
316
+ Routes.threads(TEXT_CHANNEL_ID, starterMessage.id),
317
+ {
318
+ body: { name: 'cmd-detection-test', auto_archive_duration: 1440 },
319
+ },
320
+ )) as { id: string }
321
+
322
+ await botClient.rest.put(
323
+ Routes.threadMembers(threadData.id, TEST_USER_ID),
324
+ )
325
+
326
+ // Wait for any bot reply AFTER the starter message
327
+ await waitForBotMessageContaining({
328
+ discord,
329
+ threadId: threadData.id,
330
+ userId: discord.botUserId,
331
+ text: '',
332
+ afterMessageId: starterMessage.id,
333
+ timeout: 4_000,
334
+ })
335
+
336
+ const messages = await discord.thread(threadData.id).getMessages()
337
+ const botReplies = messages.filter((m) => {
338
+ return m.author.id === discord.botUserId && m.id !== starterMessage.id
339
+ })
340
+
341
+ const allContent = botReplies.map((m) => {
342
+ return m.content
343
+ })
344
+ expect(
345
+ allContent.some((content) => {
346
+ return content.includes('Command not found: "hello-test"')
347
+ }),
348
+ ).toBe(true)
349
+ } finally {
350
+ store.setState({ registeredUserCommands: prevCommands })
351
+ }
352
+ },
353
+ 15_000,
354
+ )
355
+
356
+ test(
357
+ 'bot-posted starter message with start marker creates thread without DiscordAPIError[160004]',
358
+ async () => {
359
+ // Simulate what `otto send --channel` does:
360
+ // 1. Bot posts a starter message with `start: true` embed marker
361
+ // 2. Bot creates a thread on that message via REST
362
+ // The ThreadCreate handler should pick it up. The MessageCreate GuildText
363
+ // handler must NOT try to startThread() on the same message (race).
364
+
365
+ const prompt = 'Reply with exactly: cli-send-test'
366
+ const embedMarker: ThreadStartMarker = {
367
+ start: true,
368
+ username: 'cli-send-tester',
369
+ userId: TEST_USER_ID,
370
+ }
371
+
372
+ // Step 1: Bot posts the starter message (same as CLI's sendDiscordMessageWithOptionalAttachment)
373
+ const starterMessage = (await botClient.rest.post(
374
+ Routes.channelMessages(TEXT_CHANNEL_ID),
375
+ {
376
+ body: {
377
+ content: prompt,
378
+ embeds: [
379
+ { color: 0x2b2d31, footer: { text: YAML.stringify(embedMarker) } },
380
+ ],
381
+ },
382
+ },
383
+ )) as { id: string }
384
+
385
+ // Give the bot's MessageCreate handler time to process the starter
386
+ // message. Without the fix, the handler enters the GuildText path and
387
+ // tries to startThread() on this message, which races the CLI's thread
388
+ // creation below. The digital twin enforces Discord's 160004 uniqueness
389
+ // constraint, so the second startThread call fails.
390
+ await new Promise((resolve) => {
391
+ setTimeout(resolve, 200)
392
+ })
393
+
394
+ // Verify the MessageCreate handler did NOT create a thread on this
395
+ // message. If the handler ignored the start marker (correct behavior),
396
+ // no thread exists yet and the REST call below succeeds.
397
+ const threadsBeforeCliCreate = await discord
398
+ .channel(TEXT_CHANNEL_ID)
399
+ .getThreads()
400
+ const preExistingThread = threadsBeforeCliCreate.find((t) => {
401
+ return t.name?.includes('cli-send-test')
402
+ })
403
+ // This is the core regression assertion: without the fix in discord-bot.ts
404
+ // (skipping start markers in the GuildText handler), the MessageCreate
405
+ // handler would create a thread here, and the CLI's REST call below would
406
+ // fail with 160004.
407
+ expect(preExistingThread).toBeUndefined()
408
+
409
+ // Step 2: Bot creates a thread on the starter message (same as CLI's Routes.threads call)
410
+ const threadData = (await botClient.rest.post(
411
+ Routes.threads(TEXT_CHANNEL_ID, starterMessage.id),
412
+ {
413
+ body: {
414
+ name: 'cli-send-test',
415
+ auto_archive_duration: 1440,
416
+ },
417
+ },
418
+ )) as { id: string; name: string }
419
+
420
+ // Add test user to thread
421
+ await botClient.rest.put(
422
+ Routes.threadMembers(threadData.id, TEST_USER_ID),
423
+ )
424
+
425
+ // Wait for the bot to reply with the ⬥ prefix (proves ThreadCreate
426
+ // handler picked up the starter message and started a session)
427
+ await waitForBotMessageContaining({
428
+ discord,
429
+ threadId: threadData.id,
430
+ userId: discord.botUserId,
431
+ text: '⬥',
432
+ timeout: 4_000,
433
+ })
434
+
435
+ // Wait for footer message (proves session completed successfully)
436
+ await waitForFooterMessage({
437
+ discord,
438
+ threadId: threadData.id,
439
+ timeout: 4_000,
440
+ afterMessageIncludes: '⬥',
441
+ afterAuthorId: discord.botUserId,
442
+ })
443
+
444
+ // Verify no DiscordAPIError[160004] or other errors in the thread.
445
+ // Before the fix, the MessageCreate GuildText handler would race the
446
+ // CLI's thread creation and produce an error message here.
447
+ const messages = await discord.thread(threadData.id).getMessages()
448
+ const errorMessages = messages.filter((m) => {
449
+ return m.content.includes('Error:') || m.content.includes('160004')
450
+ })
451
+ expect(errorMessages).toHaveLength(0)
452
+
453
+ // Verify at least one ⬥ reply exists (session produced output)
454
+ const botReplies = messages.filter((m) => {
455
+ return (
456
+ m.author.id === discord.botUserId && m.content.startsWith('⬥')
457
+ )
458
+ })
459
+ expect(botReplies.length).toBeGreaterThanOrEqual(1)
460
+ },
461
+ 15_000,
462
+ )
463
+ })