@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,488 @@
1
+ // Message pre-processing pipeline for incoming Discord messages.
2
+ // Extracts prompt text, voice transcription, file/text attachments, and
3
+ // session context from a Discord Message before handing off to the runtime.
4
+ //
5
+ // This module exists so discord-bot.ts stays a thin event router and the
6
+ // expensive async work (voice transcription, context fetch, attachment
7
+ // download) runs inside the runtime's serialized preprocessChain —
8
+ // preserving arrival order without a separate threadIngressQueue.
9
+
10
+ import type { Message, ThreadChannel } from 'discord.js'
11
+ import type { DiscordFileAttachment } from './message-formatting.js'
12
+ import type { PreprocessResult } from './session-handler/thread-session-runtime.js'
13
+ import type { AgentInfo, RepliedMessageContext } from './system-message.js'
14
+ import {
15
+ resolveMentions,
16
+ getFileAttachments,
17
+ getTextAttachments,
18
+ } from './message-formatting.js'
19
+ import { processVoiceAttachment } from './voice-handler.js'
20
+ import { isVoiceAttachment } from './voice-attachment.js'
21
+ import { initializeOpencodeForDirectory } from './opencode.js'
22
+ import { getCompactSessionContext, getLastSessionId } from './markdown.js'
23
+ import { getThreadSession } from './database.js'
24
+ import * as errore from 'errore'
25
+ import { createLogger, LogPrefix } from './logger.js'
26
+ import { notifyError } from './sentry.js'
27
+
28
+ const logger = createLogger(LogPrefix.SESSION)
29
+ const voiceLogger = createLogger(LogPrefix.VOICE)
30
+
31
+ export const VOICE_MESSAGE_TRANSCRIPTION_PREFIX =
32
+ 'Voice message transcription from Discord user:\n'
33
+
34
+ /** Fetch available agents from OpenCode for voice transcription agent selection. */
35
+ async function fetchAvailableAgents(
36
+ getClient: Awaited<ReturnType<typeof initializeOpencodeForDirectory>>,
37
+ directory: string,
38
+ ): Promise<AgentInfo[]> {
39
+ if (getClient instanceof Error) {
40
+ return []
41
+ }
42
+ const result = await errore.tryAsync(() => {
43
+ return getClient().app.agents({ directory })
44
+ })
45
+ if (result instanceof Error) {
46
+ return []
47
+ }
48
+ return (result.data || [])
49
+ .filter((a) => {
50
+ return (a.mode === 'primary' || a.mode === 'all') && !a.hidden
51
+ })
52
+ .map((a) => {
53
+ return { name: a.name, description: a.description }
54
+ })
55
+ }
56
+
57
+ export type { PreprocessResult }
58
+
59
+ // Matches explicit queue markers at the end of a message (case-insensitive).
60
+ // Supported forms:
61
+ // - punctuation + queue: ". queue", "! queue", ". queue.", "!queue."
62
+ // - queue as its own final line: "text\nqueue" or just "queue"
63
+ // When present the suffix is stripped and the message is routed through
64
+ // otto's local queue (same as /queue command).
65
+ const QUEUE_SUFFIX_RE = /(?:[.!?,;:]|^)\s*queue\.?\s*$|\n\s*queue\.?\s*$/i
66
+ const REPLIED_MESSAGE_TEXT_LIMIT = 1_000
67
+ const TRANSPORT_METADATA_KEY_RE = /^(start|username|userId):\s*/i
68
+ const TRANSPORT_START_TRUE_RE = /^start:\s*true\s*$/i
69
+ const DISCORD_USER_TAG_LINE_RE = /^\s*<discord-user\b[^>]*\/>\s*$/i
70
+
71
+ function extractQueueSuffix(prompt: string): { prompt: string; forceQueue: boolean } {
72
+ if (!QUEUE_SUFFIX_RE.test(prompt)) {
73
+ return { prompt, forceQueue: false }
74
+ }
75
+ return { prompt: prompt.replace(QUEUE_SUFFIX_RE, '').trimEnd(), forceQueue: true }
76
+ }
77
+
78
+ export function stripTransportMetadataBlock(prompt: string): string {
79
+ const lines = prompt.split('\n')
80
+ const firstStartIndex = lines.findIndex((line) => {
81
+ return TRANSPORT_START_TRUE_RE.test(line.trim())
82
+ })
83
+
84
+ const linesWithoutMetadataBlock = lines.filter((line, index) => {
85
+ if (firstStartIndex === -1 || index < firstStartIndex) {
86
+ return true
87
+ }
88
+ const trimmed = line.trim()
89
+ if (!trimmed) {
90
+ return true
91
+ }
92
+ return !TRANSPORT_METADATA_KEY_RE.test(trimmed)
93
+ })
94
+
95
+ return linesWithoutMetadataBlock
96
+ .filter((line) => {
97
+ return !DISCORD_USER_TAG_LINE_RE.test(line)
98
+ })
99
+ .join('\n')
100
+ .trim()
101
+ }
102
+
103
+ function shouldSkipEmptyPrompt({
104
+ message,
105
+ prompt,
106
+ images,
107
+ hasVoiceAttachment,
108
+ }: {
109
+ message: Message
110
+ prompt: string
111
+ images?: DiscordFileAttachment[]
112
+ hasVoiceAttachment: boolean
113
+ }): boolean {
114
+ if (prompt.trim()) {
115
+ return false
116
+ }
117
+ if ((images?.length || 0) > 0) {
118
+ return false
119
+ }
120
+
121
+ const inferredVoiceAttachment = message.attachments.some((attachment) => {
122
+ return isVoiceAttachment(attachment)
123
+ })
124
+ if (!hasVoiceAttachment && !inferredVoiceAttachment && message.attachments.size === 0) {
125
+ return false
126
+ }
127
+
128
+ voiceLogger.warn(
129
+ `[INGRESS] Skipping empty prompt after preprocessing attachments=${message.attachments.size} hasVoiceAttachment=${hasVoiceAttachment} inferredVoiceAttachment=${inferredVoiceAttachment}`,
130
+ )
131
+ return true
132
+ }
133
+
134
+ async function getRepliedMessageContext({
135
+ message,
136
+ }: {
137
+ message: Message
138
+ }): Promise<RepliedMessageContext | undefined> {
139
+ if (!message.reference?.messageId) {
140
+ return undefined
141
+ }
142
+
143
+ const referencedMessage = await errore.tryAsync(() => {
144
+ return message.fetchReference()
145
+ })
146
+ if (referencedMessage instanceof Error) {
147
+ logger.warn(
148
+ `[INGRESS] Failed to fetch replied message ${message.reference.messageId} for ${message.id}: ${referencedMessage.message}`,
149
+ )
150
+ return undefined
151
+ }
152
+
153
+ const repliedText = resolveMentions(referencedMessage)
154
+ .trim()
155
+ .slice(0, REPLIED_MESSAGE_TEXT_LIMIT)
156
+ if (!repliedText) {
157
+ return undefined
158
+ }
159
+
160
+ return {
161
+ authorUsername: referencedMessage.author.username,
162
+ text: repliedText,
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Pre-process a message in an existing thread (thread already has a session or
168
+ * needs a new one). Handles voice transcription, text/file attachments, and
169
+ * session context fetching for voice messages.
170
+ *
171
+ * For threads with an existing session, voice transcription is enriched with
172
+ * current + last session context (used by the transcription model to better
173
+ * understand domain-specific terms).
174
+ */
175
+ export async function preprocessExistingThreadMessage({
176
+ message,
177
+ thread,
178
+ projectDirectory,
179
+ channelId,
180
+ isCliInjected,
181
+ hasVoiceAttachment,
182
+ appId,
183
+ }: {
184
+ message: Message
185
+ thread: ThreadChannel
186
+ projectDirectory: string
187
+ channelId: string | undefined
188
+ isCliInjected: boolean
189
+ hasVoiceAttachment: boolean
190
+ appId: string | undefined
191
+ }): Promise<PreprocessResult> {
192
+ const sessionId = await getThreadSession(thread.id)
193
+
194
+ // ── No existing session: new session in an existing thread ──
195
+ if (!sessionId) {
196
+ return preprocessNewSessionMessage({
197
+ message,
198
+ thread,
199
+ projectDirectory,
200
+ hasVoiceAttachment,
201
+ appId,
202
+ })
203
+ }
204
+
205
+ // ── Existing session path ──
206
+ voiceLogger.log(`[SESSION] Found session ${sessionId} for thread ${thread.id}`)
207
+
208
+ let messageContent = isCliInjected
209
+ ? (message.content || '')
210
+ : resolveMentions(message)
211
+ const repliedMessage = await getRepliedMessageContext({ message })
212
+
213
+ // Fetch session context and available agents for voice transcription enrichment
214
+ let currentSessionContext: string | undefined
215
+ let lastSessionContext: string | undefined
216
+ let agents: AgentInfo[] = []
217
+
218
+ if (projectDirectory) {
219
+ try {
220
+ const getClient = await initializeOpencodeForDirectory(
221
+ projectDirectory,
222
+ { channelId },
223
+ )
224
+ if (getClient instanceof Error) {
225
+ voiceLogger.error(
226
+ `[SESSION] Failed to initialize OpenCode client:`,
227
+ getClient.message,
228
+ )
229
+ throw new Error(getClient.message)
230
+ }
231
+ const client = getClient()
232
+
233
+ const [sessionContextResult, lastSessionResult, fetchedAgents] = await Promise.all([
234
+ getCompactSessionContext({
235
+ client,
236
+ sessionId,
237
+ includeSystemPrompt: false,
238
+ maxMessages: 15,
239
+ }),
240
+ getLastSessionId({
241
+ client,
242
+ excludeSessionId: sessionId,
243
+ }),
244
+ fetchAvailableAgents(getClient, projectDirectory),
245
+ ])
246
+
247
+ if (errore.isOk(sessionContextResult)) {
248
+ currentSessionContext = sessionContextResult
249
+ }
250
+ agents = fetchedAgents
251
+
252
+ const lastSessionId = errore.unwrapOr(lastSessionResult, null)
253
+ if (lastSessionId) {
254
+ const result = await getCompactSessionContext({
255
+ client,
256
+ sessionId: lastSessionId,
257
+ includeSystemPrompt: true,
258
+ maxMessages: 10,
259
+ })
260
+ if (errore.isOk(result)) {
261
+ lastSessionContext = result
262
+ }
263
+ }
264
+ } catch (e) {
265
+ voiceLogger.error(`Could not get session context:`, e)
266
+ void notifyError(e, 'Failed to get session context')
267
+ }
268
+ }
269
+
270
+ const voiceResult = await processVoiceAttachment({
271
+ message,
272
+ thread,
273
+ projectDirectory,
274
+ appId,
275
+ currentSessionContext,
276
+ lastSessionContext,
277
+ agents,
278
+ })
279
+ if (voiceResult) {
280
+ messageContent = `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`
281
+ }
282
+
283
+ // Voice transcription failed and no text — drop silently
284
+ if (hasVoiceAttachment && !voiceResult && !messageContent.trim()) {
285
+ return { prompt: '', mode: 'opencode', skip: true }
286
+ }
287
+
288
+ // Extract queue suffix from raw message content BEFORE appending text
289
+ // attachments. Otherwise a text file attachment pushes "? queue" away from
290
+ // the end of the string and the regex fails to match.
291
+ const qs = extractQueueSuffix(messageContent)
292
+
293
+ const fileAttachments = await getFileAttachments(message)
294
+ const textAttachmentsContent = await getTextAttachments(message)
295
+ const prompt = textAttachmentsContent
296
+ ? `${qs.prompt}\n\n${textAttachmentsContent}`
297
+ : qs.prompt
298
+ const cleanedPrompt = stripTransportMetadataBlock(prompt)
299
+
300
+ if (
301
+ shouldSkipEmptyPrompt({
302
+ message,
303
+ prompt: cleanedPrompt,
304
+ images: fileAttachments,
305
+ hasVoiceAttachment,
306
+ })
307
+ ) {
308
+ return { prompt: '', mode: 'opencode', skip: true }
309
+ }
310
+
311
+ return {
312
+ prompt: cleanedPrompt,
313
+ images: fileAttachments.length > 0 ? fileAttachments : undefined,
314
+ repliedMessage,
315
+ mode: qs.forceQueue || voiceResult?.queueMessage ? 'local-queue' : 'opencode',
316
+ agent: voiceResult?.agent,
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Pre-process a message that starts a new session in a thread (no existing
322
+ * session). Handles starter message context, voice transcription, and
323
+ * text/file attachments.
324
+ */
325
+ export async function preprocessNewSessionMessage({
326
+ message,
327
+ thread,
328
+ projectDirectory,
329
+ hasVoiceAttachment,
330
+ appId,
331
+ }: {
332
+ message: Message
333
+ thread: ThreadChannel
334
+ projectDirectory: string
335
+ hasVoiceAttachment: boolean
336
+ appId?: string
337
+ }): Promise<PreprocessResult> {
338
+ logger.log(`No session for thread ${thread.id}, starting new session`)
339
+
340
+ // Fetch available agents only for voice messages to avoid unnecessary SDK
341
+ // roundtrips on plain text messages.
342
+ let agents: AgentInfo[] = []
343
+ if (hasVoiceAttachment && projectDirectory) {
344
+ try {
345
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
346
+ agents = await fetchAvailableAgents(getClient, projectDirectory)
347
+ } catch (e) {
348
+ voiceLogger.error(`Could not fetch agents for voice transcription:`, e)
349
+ }
350
+ }
351
+
352
+ let prompt = resolveMentions(message)
353
+ const repliedMessage = await getRepliedMessageContext({ message })
354
+ const voiceResult = await processVoiceAttachment({
355
+ message,
356
+ thread,
357
+ projectDirectory,
358
+ appId,
359
+ agents,
360
+ })
361
+ if (voiceResult) {
362
+ prompt = `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`
363
+ }
364
+
365
+ // Voice transcription failed and no text — drop silently
366
+ if (hasVoiceAttachment && !voiceResult && !prompt.trim()) {
367
+ return { prompt: '', mode: 'opencode', skip: true }
368
+ }
369
+
370
+ // Fetch starter message for thread context
371
+ const starterMessage = await thread
372
+ .fetchStarterMessage()
373
+ .catch((error) => {
374
+ logger.warn(
375
+ `[SESSION] Failed to fetch starter message for thread ${thread.id}:`,
376
+ error instanceof Error ? error.stack : String(error),
377
+ )
378
+ return null
379
+ })
380
+ if (starterMessage && starterMessage.content !== message.content) {
381
+ const starterTextAttachments = await getTextAttachments(starterMessage)
382
+ const starterContent = resolveMentions(starterMessage)
383
+ const starterText = starterTextAttachments
384
+ ? `${starterContent}\n\n${starterTextAttachments}`
385
+ : starterContent
386
+ if (starterText) {
387
+ prompt = `Context from thread:\n${starterText}\n\nUser request:\n${prompt}`
388
+ }
389
+ }
390
+
391
+ const cleanedPrompt = stripTransportMetadataBlock(prompt)
392
+ const qs = extractQueueSuffix(cleanedPrompt)
393
+ if (
394
+ shouldSkipEmptyPrompt({
395
+ message,
396
+ prompt: qs.prompt,
397
+ hasVoiceAttachment,
398
+ })
399
+ ) {
400
+ return { prompt: '', mode: 'opencode', skip: true }
401
+ }
402
+
403
+ return {
404
+ prompt: qs.prompt,
405
+ repliedMessage,
406
+ mode: qs.forceQueue || voiceResult?.queueMessage ? 'local-queue' : 'opencode',
407
+ agent: voiceResult?.agent,
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Pre-process a message from a text channel (creates a new thread).
413
+ * Handles voice transcription and file/text attachments.
414
+ */
415
+ export async function preprocessNewThreadMessage({
416
+ message,
417
+ thread,
418
+ projectDirectory,
419
+ hasVoiceAttachment,
420
+ appId,
421
+ }: {
422
+ message: Message
423
+ thread: ThreadChannel
424
+ projectDirectory: string
425
+ hasVoiceAttachment: boolean
426
+ appId?: string
427
+ }): Promise<PreprocessResult> {
428
+ // Fetch available agents only for voice messages to avoid unnecessary SDK
429
+ // roundtrips on plain text messages.
430
+ let agents: AgentInfo[] = []
431
+ if (hasVoiceAttachment && projectDirectory) {
432
+ try {
433
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
434
+ agents = await fetchAvailableAgents(getClient, projectDirectory)
435
+ } catch (e) {
436
+ voiceLogger.error(`Could not fetch agents for voice transcription:`, e)
437
+ }
438
+ }
439
+
440
+ let messageContent = resolveMentions(message)
441
+ const repliedMessage = await getRepliedMessageContext({ message })
442
+ const voiceResult = await processVoiceAttachment({
443
+ message,
444
+ thread,
445
+ projectDirectory,
446
+ isNewThread: true,
447
+ appId,
448
+ agents,
449
+ })
450
+ if (voiceResult) {
451
+ messageContent = `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`
452
+ }
453
+
454
+ // Voice transcription failed and no text — drop silently
455
+ if (hasVoiceAttachment && !voiceResult && !messageContent.trim()) {
456
+ return { prompt: '', mode: 'opencode', skip: true }
457
+ }
458
+
459
+ // Extract queue suffix from raw message content BEFORE appending text
460
+ // attachments (same fix as preprocessExistingThreadMessage).
461
+ const qs = extractQueueSuffix(messageContent)
462
+
463
+ const fileAttachments = await getFileAttachments(message)
464
+ const textAttachmentsContent = await getTextAttachments(message)
465
+ const prompt = textAttachmentsContent
466
+ ? `${qs.prompt}\n\n${textAttachmentsContent}`
467
+ : qs.prompt
468
+ const cleanedPrompt = stripTransportMetadataBlock(prompt)
469
+
470
+ if (
471
+ shouldSkipEmptyPrompt({
472
+ message,
473
+ prompt: cleanedPrompt,
474
+ images: fileAttachments,
475
+ hasVoiceAttachment,
476
+ })
477
+ ) {
478
+ return { prompt: '', mode: 'opencode', skip: true }
479
+ }
480
+
481
+ return {
482
+ prompt: cleanedPrompt,
483
+ images: fileAttachments.length > 0 ? fileAttachments : undefined,
484
+ repliedMessage,
485
+ mode: qs.forceQueue || voiceResult?.queueMessage ? 'local-queue' : 'opencode',
486
+ agent: voiceResult?.agent,
487
+ }
488
+ }
@@ -0,0 +1,167 @@
1
+ // Onboarding tutorial system instructions injected by the plugin when the
2
+ // user starts a 3D game tutorial session. The `markdown` tag is a no-op
3
+ // identity function — it exists only for editor syntax highlighting.
4
+ //
5
+ // This file has no discord.js deps so it can be safely imported by both
6
+ // the welcome message (discord side) and the opencode plugin.
7
+
8
+ // Unique text used in the welcome message and detected by the plugin to
9
+ // trigger tutorial instruction injection. Shared constant so they can't
10
+ // drift out of sync.
11
+ export const TUTORIAL_WELCOME_TEXT =
12
+ 'Want to build an example browser game? Respond in this thread.'
13
+
14
+ const markdown = String.raw
15
+ const backticks = '```'
16
+
17
+ export const ONBOARDING_TUTORIAL_INSTRUCTIONS = markdown`
18
+ You are helping a new user try Otto for the first time. The default suggestion is building a 3D game, but if the user asks to build something else, build that instead. Adapt all instructions below to whatever the user wants.
19
+
20
+ ## Prerequisites
21
+
22
+ Before doing anything else, check that these are installed:
23
+
24
+ **Bun** (v1.2 or later) — runtime and bundler:
25
+
26
+ ${backticks}bash
27
+ bun --version
28
+ ${backticks}
29
+
30
+ If missing or below 1.2, tell the user to install it: https://bun.sh — or run:
31
+
32
+ ${backticks}bash
33
+ curl -fsSL https://bun.sh/install | bash
34
+ ${backticks}
35
+
36
+ **tuistory** — needed to run the dev server in the background with otto tunnel:
37
+
38
+ ${backticks}bash
39
+ bunx tuistory --help
40
+ ${backticks}
41
+
42
+ This works without installing it globally because \`bunx\` can run it on demand.
43
+
44
+ Do NOT use Node.js, npm, or npx. Use Bun for everything.
45
+
46
+ ## Goal
47
+
48
+ Build a simple but visually impressive 3D game using Three.js that runs in the browser. The user should be able to play it within a few minutes of starting. If the user asked for something different, build that instead.
49
+
50
+ ## Game idea
51
+
52
+ Build a "Space Dodge" game:
53
+ - The player controls a spaceship that flies forward through space
54
+ - Asteroids/obstacles come toward the player
55
+ - The player dodges left/right/up/down using arrow keys or WASD
56
+ - Touch/swipe controls for mobile — the user is on Discord and may open the link on their phone
57
+ - Score increases over time, speed gradually increases
58
+ - Particle effects for explosions when hit
59
+ - Starfield background for atmosphere
60
+ - Simple start screen and game over screen with score
61
+
62
+ If the game idea doesn't match what the user asked for, adapt to their request instead.
63
+
64
+ ## Project setup
65
+
66
+ Create these files:
67
+
68
+ **package.json** — install three as a dependency:
69
+ ${backticks}json
70
+ {
71
+ "dependencies": {
72
+ "three": "^0.170.0"
73
+ }
74
+ }
75
+ ${backticks}
76
+
77
+ Run bun install after creating it.
78
+
79
+ **tsconfig.json**:
80
+ ${backticks}json
81
+ {
82
+ "compilerOptions": {
83
+ "target": "ESNext",
84
+ "module": "ESNext",
85
+ "moduleResolution": "bundler",
86
+ "strict": true,
87
+ "jsx": "react-jsx",
88
+ "types": ["three"]
89
+ }
90
+ }
91
+ ${backticks}
92
+
93
+ **index.html** — the entry point, references the TypeScript source:
94
+ ${backticks}html
95
+ <!DOCTYPE html>
96
+ <html>
97
+ <head>
98
+ <meta charset="utf-8" />
99
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
100
+ <title>Space Dodge</title>
101
+ <style>
102
+ body { margin: 0; overflow: hidden; }
103
+ canvas { display: block; }
104
+ </style>
105
+ </head>
106
+ <body>
107
+ <script type="module" src="./game.ts"></script>
108
+ </body>
109
+ </html>
110
+ ${backticks}
111
+
112
+ **game.ts** — all game logic in TypeScript, importing from "three":
113
+ ${backticks}ts
114
+ import * as THREE from "three"
115
+ // ... game code here
116
+ ${backticks}
117
+
118
+ Write the full game code in game.ts. Import Three.js with normal imports (Bun bundles it automatically). Add basic mobile touch controls (swipe to move) so it works on phones too.
119
+
120
+ **server.ts** — Bun fullstack dev server (reads port from PORT env var):
121
+ ${backticks}ts
122
+ import homepage from "./index.html"
123
+
124
+ Bun.serve({
125
+ port: Number(process.env.PORT) || 3000,
126
+ routes: { "/": homepage },
127
+ development: true,
128
+ })
129
+ ${backticks}
130
+
131
+ ## Dev server and tunnel
132
+
133
+ After creating all files and running bun install, start the dev server and expose it via otto tunnel so the user can play immediately from their browser or phone. The user is on Discord, not at a terminal — localhost URLs are useless to them.
134
+
135
+ Pick a random port between 3000-9000 to avoid conflicts:
136
+
137
+ ${backticks}bash
138
+ PORT=$((RANDOM % 6000 + 3000))
139
+ bunx tuistory launch "PORT=$PORT otto tunnel -p $PORT -- bun run server.ts" -s game-dev --cwd "$PWD"
140
+ ${backticks}
141
+
142
+ Wait a moment, then get the tunnel URL:
143
+
144
+ ${backticks}bash
145
+ bunx tuistory -s game-dev wait "/tunnel|https?:\/\//i" --timeout 30000
146
+ bunx tuistory read -s game-dev
147
+ ${backticks}
148
+
149
+ If the tunnel URL is not visible yet, run the capture command again — it usually appears within a few seconds.
150
+
151
+ After the tunnel is up, also generate a diff URL so the user can browse the code:
152
+
153
+ ${backticks}bash
154
+ git init && git add -A && critique --url
155
+ ${backticks}
156
+
157
+ Share both URLs prominently so the user can play the game and browse the code. Put each URL on its own line, not buried in text.
158
+
159
+ ## Behavior
160
+
161
+ - Be friendly and casual in the conversation. Sound like a helpful teammate, not a formal tutorial.
162
+ - Be concise and direct. Show the code, start the server, share the URL.
163
+ - After the game is working and the user can play it, ask what they'd like to change by using the question tool. Do not present a manual numbered list in plain text. Offer a few short button/dropdown options like sound effects, ship design, leaderboard, power-ups, and multiplayer, while still allowing a custom answer.
164
+ - Never mention this is a tutorial, onboarding, or demo. Act like a normal coding session.
165
+ - Never apologize or over-explain. Just build and iterate.
166
+ - If the user asks to change something, do it quickly and share the updated URL.
167
+ `
@@ -0,0 +1,49 @@
1
+ // Onboarding welcome message for the default otto channel.
2
+ // Sends a message explaining what Otto is, then creates a thread from it
3
+ // so the user can respond there to start a tutorial session.
4
+ // Sends a smaller follow-up message inside the thread with the installer
5
+ // mention so the notification is less noisy.
6
+ // Posted once when the default channel is first created.
7
+
8
+ import { ThreadAutoArchiveDuration, type TextChannel } from 'discord.js'
9
+ import { createLogger, LogPrefix } from './logger.js'
10
+ import { TUTORIAL_WELCOME_TEXT } from './onboarding-tutorial.js'
11
+
12
+ const logger = createLogger(LogPrefix.CHANNEL)
13
+
14
+ function buildWelcomeText(): string {
15
+ return `**Otto** lets you code from Discord. Send a message in any project channel and an AI agent edits code, runs commands, and searches your codebase — all on your machine.
16
+ **What you can do:**
17
+ - Use \`/add-project\` to create a Discord channel linked to one OpenCode project (git repo)
18
+ - Collaborate with teammates in the same session
19
+ - Upload images and files, the bot can share screenshots back
20
+ ${TUTORIAL_WELCOME_TEXT}`
21
+ }
22
+
23
+ function buildThreadPrompt({ mentionUserId }: { mentionUserId?: string }): string {
24
+ const mentionSuffix = mentionUserId ? ` <@${mentionUserId}>` : ''
25
+ return `Want to build an example browser game? Respond in this thread.${mentionSuffix}`
26
+ }
27
+
28
+ export async function sendWelcomeMessage({
29
+ channel,
30
+ mentionUserId,
31
+ }: {
32
+ channel: TextChannel
33
+ mentionUserId?: string
34
+ }): Promise<void> {
35
+ try {
36
+ const message = await channel.send(buildWelcomeText())
37
+ const thread = await message.startThread({
38
+ name: 'Otto tutorial',
39
+ autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
40
+ reason: 'Onboarding tutorial thread',
41
+ })
42
+ await thread.send(buildThreadPrompt({ mentionUserId }))
43
+ logger.log(`Sent welcome message with thread to #${channel.name}`)
44
+ } catch (error) {
45
+ logger.warn(
46
+ `Failed to send welcome message to #${channel.name}: ${error instanceof Error ? error.stack : String(error)}`,
47
+ )
48
+ }
49
+ }