@otto-assistant/otto 0.1.1 → 0.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (637) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-account-identity.js +62 -0
  7. package/dist/anthropic-account-identity.test.js +38 -0
  8. package/dist/anthropic-auth-plugin.js +917 -0
  9. package/dist/anthropic-auth-state.js +303 -0
  10. package/dist/anthropic-auth-state.test.js +150 -0
  11. package/dist/bin.js +152 -0
  12. package/dist/btw-prefix-detection.js +17 -0
  13. package/dist/btw-prefix-detection.test.js +63 -0
  14. package/dist/channel-management.js +259 -0
  15. package/dist/cli-parsing.test.js +142 -0
  16. package/dist/cli-send-thread.e2e.test.js +353 -0
  17. package/dist/cli-telegram-options.test.js +99 -0
  18. package/dist/cli.js +4210 -568
  19. package/dist/commands/abort.js +65 -0
  20. package/dist/commands/action-buttons.js +245 -0
  21. package/dist/commands/add-dir.js +124 -0
  22. package/dist/commands/add-dir.test.js +126 -0
  23. package/dist/commands/add-project.js +113 -0
  24. package/dist/commands/agent.js +355 -0
  25. package/dist/commands/ask-question.js +320 -0
  26. package/dist/commands/ask-question.test.js +92 -0
  27. package/dist/commands/btw.js +121 -0
  28. package/dist/commands/cli-commands-group-a.test.js +728 -0
  29. package/dist/commands/cli-commands-group-b.test.js +695 -0
  30. package/dist/commands/compact.js +120 -0
  31. package/dist/commands/context-usage.js +140 -0
  32. package/dist/commands/create-new-project.js +130 -0
  33. package/dist/commands/diff.js +63 -0
  34. package/dist/commands/discord-commands-group-a.test.js +621 -0
  35. package/dist/commands/discord-commands-group-b.test.js +595 -0
  36. package/dist/commands/discord-commands-group-c.test.js +739 -0
  37. package/dist/commands/file-upload.js +275 -0
  38. package/dist/commands/fork-subagent.js +177 -0
  39. package/dist/commands/fork.js +262 -0
  40. package/dist/commands/gemini-apikey.js +70 -0
  41. package/dist/commands/login.js +887 -0
  42. package/dist/commands/mcp.js +239 -0
  43. package/dist/commands/memory-snapshot.js +24 -0
  44. package/dist/commands/mention-mode.js +44 -0
  45. package/dist/commands/merge-worktree.js +162 -0
  46. package/dist/commands/model-variant.js +366 -0
  47. package/dist/commands/model.js +794 -0
  48. package/dist/commands/new-worktree.js +465 -0
  49. package/dist/commands/paginated-select.js +57 -0
  50. package/dist/commands/permissions.js +274 -0
  51. package/dist/commands/queue.js +223 -0
  52. package/dist/commands/remove-project.js +115 -0
  53. package/dist/commands/restart-opencode-server.js +127 -0
  54. package/dist/commands/resume.js +149 -0
  55. package/dist/commands/run-command.js +79 -0
  56. package/dist/commands/screenshare.js +303 -0
  57. package/dist/commands/screenshare.test.js +20 -0
  58. package/dist/commands/session-id.js +78 -0
  59. package/dist/commands/session.js +176 -0
  60. package/dist/commands/share.js +80 -0
  61. package/dist/commands/tasks.js +205 -0
  62. package/dist/commands/thread-deletion-sync.js +50 -0
  63. package/dist/commands/types.js +2 -0
  64. package/dist/commands/undo-redo.js +305 -0
  65. package/dist/commands/unset-model.js +139 -0
  66. package/dist/commands/upgrade.js +48 -0
  67. package/dist/commands/user-command.js +155 -0
  68. package/dist/commands/verbosity.js +125 -0
  69. package/dist/commands/vscode.js +269 -0
  70. package/dist/commands/worktree-settings.js +43 -0
  71. package/dist/commands/worktrees.js +468 -0
  72. package/dist/condense-memory.js +33 -0
  73. package/dist/config.js +100 -255
  74. package/dist/context-awareness-plugin.js +340 -0
  75. package/dist/context-awareness-plugin.test.js +126 -0
  76. package/dist/critique-utils.js +95 -0
  77. package/dist/database.js +1355 -0
  78. package/dist/db.js +260 -0
  79. package/dist/db.test.js +138 -0
  80. package/dist/debounce-timeout.js +28 -0
  81. package/dist/debounced-process-flush.js +77 -0
  82. package/dist/discord-bot.js +1124 -0
  83. package/dist/discord-command-registration.js +567 -0
  84. package/dist/discord-urls.js +82 -0
  85. package/dist/discord-utils.js +616 -0
  86. package/dist/discord-utils.test.js +134 -0
  87. package/dist/errors.js +157 -0
  88. package/dist/escape-backticks.test.js +429 -0
  89. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  90. package/dist/eventsource-parser.test.js +327 -0
  91. package/dist/exec-async.js +26 -0
  92. package/dist/external-opencode-sync.js +480 -0
  93. package/dist/format-tables.js +491 -0
  94. package/dist/format-tables.test.js +478 -0
  95. package/dist/forum-sync/config.js +79 -0
  96. package/dist/forum-sync/discord-operations.js +154 -0
  97. package/dist/forum-sync/index.js +5 -0
  98. package/dist/forum-sync/markdown.js +113 -0
  99. package/dist/forum-sync/sync-to-discord.js +417 -0
  100. package/dist/forum-sync/sync-to-files.js +190 -0
  101. package/dist/forum-sync/types.js +53 -0
  102. package/dist/forum-sync/watchers.js +307 -0
  103. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  104. package/dist/gateway-proxy.e2e.test.js +485 -0
  105. package/dist/genai-worker-wrapper.js +111 -0
  106. package/dist/genai-worker.js +311 -0
  107. package/dist/genai.js +232 -0
  108. package/dist/generated/browser.js +17 -0
  109. package/dist/generated/client.js +37 -0
  110. package/dist/generated/commonInputTypes.js +10 -0
  111. package/dist/generated/enums.js +58 -0
  112. package/dist/generated/internal/class.js +49 -0
  113. package/dist/generated/internal/prismaNamespace.js +254 -0
  114. package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
  115. package/dist/generated/models/bot_api_keys.js +1 -0
  116. package/dist/generated/models/bot_tokens.js +1 -0
  117. package/dist/generated/models/channel_agents.js +1 -0
  118. package/dist/generated/models/channel_directories.js +1 -0
  119. package/dist/generated/models/channel_mention_mode.js +1 -0
  120. package/dist/generated/models/channel_models.js +1 -0
  121. package/dist/generated/models/channel_verbosity.js +1 -0
  122. package/dist/generated/models/channel_worktrees.js +1 -0
  123. package/dist/generated/models/forum_sync_configs.js +1 -0
  124. package/dist/generated/models/global_models.js +1 -0
  125. package/dist/generated/models/ipc_requests.js +1 -0
  126. package/dist/generated/models/part_messages.js +1 -0
  127. package/dist/generated/models/scheduled_tasks.js +1 -0
  128. package/dist/generated/models/session_agents.js +1 -0
  129. package/dist/generated/models/session_events.js +1 -0
  130. package/dist/generated/models/session_models.js +1 -0
  131. package/dist/generated/models/session_start_sources.js +1 -0
  132. package/dist/generated/models/thread_sessions.js +1 -0
  133. package/dist/generated/models/thread_worktrees.js +1 -0
  134. package/dist/generated/models.js +1 -0
  135. package/dist/heap-monitor.js +122 -0
  136. package/dist/hrana-server.js +251 -0
  137. package/dist/hrana-server.test.js +370 -0
  138. package/dist/html-actions.js +123 -0
  139. package/dist/html-actions.test.js +70 -0
  140. package/dist/html-components.js +117 -0
  141. package/dist/html-components.test.js +34 -0
  142. package/dist/image-optimizer-plugin.js +153 -0
  143. package/dist/image-utils.js +112 -0
  144. package/dist/interaction-handler.js +420 -0
  145. package/dist/ipc-polling.js +327 -0
  146. package/dist/ipc-tools-plugin.js +193 -0
  147. package/dist/ipc-utils.js +18 -0
  148. package/dist/limit-heading-depth.js +25 -0
  149. package/dist/limit-heading-depth.test.js +105 -0
  150. package/dist/logger.js +171 -0
  151. package/dist/markdown.js +342 -0
  152. package/dist/markdown.test.js +264 -0
  153. package/dist/memory-overview-plugin.js +128 -0
  154. package/dist/message-finish-field.e2e.test.js +168 -0
  155. package/dist/message-formatting.js +415 -0
  156. package/dist/message-formatting.test.js +115 -0
  157. package/dist/message-preprocessing.js +359 -0
  158. package/dist/onboarding-tutorial.js +163 -0
  159. package/dist/onboarding-welcome.js +37 -0
  160. package/dist/openai-realtime.js +224 -0
  161. package/dist/opencode-command-detection.js +65 -0
  162. package/dist/opencode-command-detection.test.js +240 -0
  163. package/dist/opencode-command.js +131 -0
  164. package/dist/opencode-command.test.js +48 -0
  165. package/dist/opencode-interrupt-plugin.js +388 -0
  166. package/dist/opencode-interrupt-plugin.test.js +463 -0
  167. package/dist/opencode.js +1117 -0
  168. package/dist/otto/branding.js +22 -0
  169. package/dist/otto/index.js +21 -0
  170. package/dist/otto-digital-twin.e2e.test.js +161 -0
  171. package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
  172. package/dist/otto-opencode-plugin.js +21 -0
  173. package/dist/otto-opencode-plugin.test.js +98 -0
  174. package/dist/parse-permission-rules.test.js +117 -0
  175. package/dist/patch-text-parser.js +97 -0
  176. package/dist/plugin-logger.js +68 -0
  177. package/dist/privacy-sanitizer.js +105 -0
  178. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  179. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  180. package/dist/queue-advanced-e2e-setup.js +790 -0
  181. package/dist/queue-advanced-footer.e2e.test.js +481 -0
  182. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  183. package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
  184. package/dist/queue-advanced-question.e2e.test.js +261 -0
  185. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  186. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  187. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  188. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  189. package/dist/queue-question-select-drain.e2e.test.js +256 -0
  190. package/dist/runtime-idle-sweeper.js +52 -0
  191. package/dist/runtime-lifecycle.e2e.test.js +514 -0
  192. package/dist/sentry.js +23 -0
  193. package/dist/session-handler/agent-utils.js +67 -0
  194. package/dist/session-handler/event-stream-state.js +475 -0
  195. package/dist/session-handler/event-stream-state.test.js +632 -0
  196. package/dist/session-handler/model-utils.js +147 -0
  197. package/dist/session-handler/opencode-session-event-log.js +94 -0
  198. package/dist/session-handler/thread-runtime-state.js +131 -0
  199. package/dist/session-handler/thread-session-runtime.js +3390 -0
  200. package/dist/session-handler.js +9 -0
  201. package/dist/session-search.js +100 -0
  202. package/dist/session-search.test.js +40 -0
  203. package/dist/session-title-rename.test.js +92 -0
  204. package/dist/skill-filter.js +31 -0
  205. package/dist/skill-filter.test.js +65 -0
  206. package/dist/startup-service.js +153 -0
  207. package/dist/startup-time.e2e.test.js +296 -0
  208. package/dist/store.js +19 -0
  209. package/dist/subagent-rate-limit-plugin.js +175 -0
  210. package/dist/system-message.js +702 -0
  211. package/dist/system-message.test.js +697 -0
  212. package/dist/task-runner.js +530 -0
  213. package/dist/task-schedule.js +213 -0
  214. package/dist/task-schedule.test.js +71 -0
  215. package/dist/test-utils.js +313 -0
  216. package/dist/thinking-utils.js +35 -0
  217. package/dist/thread-message-queue.e2e.test.js +1111 -0
  218. package/dist/tools.js +357 -0
  219. package/dist/undo-redo.e2e.test.js +161 -0
  220. package/dist/unnest-code-blocks.js +146 -0
  221. package/dist/unnest-code-blocks.test.js +673 -0
  222. package/dist/upgrade.js +156 -0
  223. package/dist/utils.js +172 -0
  224. package/dist/utils.test.js +130 -0
  225. package/dist/voice-attachment.js +34 -0
  226. package/dist/voice-handler.js +646 -0
  227. package/dist/voice-message.e2e.test.js +1021 -0
  228. package/dist/voice.js +456 -0
  229. package/dist/voice.test.js +235 -0
  230. package/dist/wait-session.js +171 -0
  231. package/dist/websockify.js +69 -0
  232. package/dist/worker-types.js +4 -0
  233. package/dist/worktree-lifecycle.e2e.test.js +311 -0
  234. package/dist/worktree-utils.js +3 -0
  235. package/dist/worktrees.js +991 -0
  236. package/dist/worktrees.test.js +415 -0
  237. package/dist/xml.js +92 -0
  238. package/dist/xml.test.js +32 -0
  239. package/package.json +90 -38
  240. package/schema.prisma +303 -0
  241. package/skills/batch/SKILL.md +87 -0
  242. package/skills/critique/SKILL.md +112 -0
  243. package/skills/egaki/SKILL.md +100 -0
  244. package/skills/errore/SKILL.md +647 -0
  245. package/skills/event-sourcing-state/SKILL.md +252 -0
  246. package/skills/goke/SKILL.md +38 -0
  247. package/skills/jitter/EDITOR.md +219 -0
  248. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  249. package/skills/jitter/SKILL.md +158 -0
  250. package/skills/jitter/jitter-clipboard.json +1042 -0
  251. package/skills/jitter/package.json +14 -0
  252. package/skills/jitter/tsconfig.json +15 -0
  253. package/skills/jitter/utils/actions.ts +212 -0
  254. package/skills/jitter/utils/export.ts +114 -0
  255. package/skills/jitter/utils/index.ts +141 -0
  256. package/skills/jitter/utils/snapshot.ts +154 -0
  257. package/skills/jitter/utils/traverse.ts +246 -0
  258. package/skills/jitter/utils/types.ts +279 -0
  259. package/skills/jitter/utils/wait.ts +133 -0
  260. package/skills/lintcn/SKILL.md +873 -0
  261. package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
  262. package/skills/new-skill/SKILL.md +237 -0
  263. package/skills/npm-package/SKILL.md +617 -0
  264. package/skills/opensrc/SKILL.md +78 -0
  265. package/skills/otto-publish/SKILL.md +61 -0
  266. package/skills/playwriter/SKILL.md +35 -0
  267. package/skills/profano/SKILL.md +16 -0
  268. package/skills/proxyman/SKILL.md +215 -0
  269. package/skills/security-review/SKILL.md +208 -0
  270. package/skills/sigillo/SKILL.md +101 -0
  271. package/skills/simplify/SKILL.md +58 -0
  272. package/skills/spiceflow/SKILL.md +28 -0
  273. package/skills/termcast/SKILL.md +945 -0
  274. package/skills/tuistory/SKILL.md +98 -0
  275. package/skills/usecomputer/SKILL.md +264 -0
  276. package/skills/x-articles/SKILL.md +554 -0
  277. package/skills/zele/SKILL.md +49 -0
  278. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  279. package/src/agent-model.e2e.test.ts +979 -0
  280. package/src/ai-tool-to-genai.test.ts +296 -0
  281. package/src/ai-tool-to-genai.ts +283 -0
  282. package/src/ai-tool.ts +39 -0
  283. package/src/anthropic-account-identity.test.ts +52 -0
  284. package/src/anthropic-account-identity.ts +77 -0
  285. package/src/anthropic-auth-plugin.ts +1139 -0
  286. package/src/anthropic-auth-state.test.ts +187 -0
  287. package/src/anthropic-auth-state.ts +386 -0
  288. package/src/bin.ts +182 -0
  289. package/src/btw-prefix-detection.test.ts +73 -0
  290. package/src/btw-prefix-detection.ts +23 -0
  291. package/src/channel-management.ts +376 -0
  292. package/src/cli-parsing.test.ts +197 -0
  293. package/src/cli-send-thread.e2e.test.ts +463 -0
  294. package/src/cli-telegram-options.test.ts +114 -0
  295. package/src/cli.ts +5718 -580
  296. package/src/commands/abort.ts +89 -0
  297. package/src/commands/action-buttons.ts +364 -0
  298. package/src/commands/add-dir.test.ts +154 -0
  299. package/src/commands/add-dir.ts +175 -0
  300. package/src/commands/add-project.ts +149 -0
  301. package/src/commands/agent.ts +496 -0
  302. package/src/commands/ask-question.test.ts +111 -0
  303. package/src/commands/ask-question.ts +455 -0
  304. package/src/commands/btw.ts +184 -0
  305. package/src/commands/cli-commands-group-a.test.ts +837 -0
  306. package/src/commands/cli-commands-group-b.test.ts +800 -0
  307. package/src/commands/compact.ts +157 -0
  308. package/src/commands/context-usage.ts +199 -0
  309. package/src/commands/create-new-project.ts +190 -0
  310. package/src/commands/diff.ts +91 -0
  311. package/src/commands/discord-commands-group-a.test.ts +751 -0
  312. package/src/commands/discord-commands-group-b.test.ts +648 -0
  313. package/src/commands/discord-commands-group-c.test.ts +882 -0
  314. package/src/commands/file-upload.ts +389 -0
  315. package/src/commands/fork-subagent.ts +263 -0
  316. package/src/commands/fork.ts +386 -0
  317. package/src/commands/gemini-apikey.ts +104 -0
  318. package/src/commands/login.ts +1175 -0
  319. package/src/commands/mcp.ts +307 -0
  320. package/src/commands/memory-snapshot.ts +30 -0
  321. package/src/commands/mention-mode.ts +68 -0
  322. package/src/commands/merge-worktree.ts +226 -0
  323. package/src/commands/model-variant.ts +485 -0
  324. package/src/commands/model.ts +1078 -0
  325. package/src/commands/new-worktree.ts +645 -0
  326. package/src/commands/paginated-select.ts +81 -0
  327. package/src/commands/permissions.ts +397 -0
  328. package/src/commands/queue.ts +293 -0
  329. package/src/commands/remove-project.ts +155 -0
  330. package/src/commands/restart-opencode-server.ts +162 -0
  331. package/src/commands/resume.ts +230 -0
  332. package/src/commands/run-command.ts +123 -0
  333. package/src/commands/screenshare.test.ts +30 -0
  334. package/src/commands/screenshare.ts +366 -0
  335. package/src/commands/session-id.ts +109 -0
  336. package/src/commands/session.ts +227 -0
  337. package/src/commands/share.ts +106 -0
  338. package/src/commands/tasks.ts +293 -0
  339. package/src/commands/thread-deletion-sync.ts +80 -0
  340. package/src/commands/types.ts +25 -0
  341. package/src/commands/undo-redo.ts +386 -0
  342. package/src/commands/unset-model.ts +174 -0
  343. package/src/commands/upgrade.ts +59 -0
  344. package/src/commands/user-command.ts +198 -0
  345. package/src/commands/verbosity.ts +173 -0
  346. package/src/commands/vscode.ts +342 -0
  347. package/src/commands/worktree-settings.ts +70 -0
  348. package/src/commands/worktrees.ts +645 -0
  349. package/src/condense-memory.ts +36 -0
  350. package/src/config.ts +103 -339
  351. package/src/context-awareness-plugin.test.ts +144 -0
  352. package/src/context-awareness-plugin.ts +469 -0
  353. package/src/critique-utils.ts +139 -0
  354. package/src/database.ts +1949 -0
  355. package/src/db.test.ts +162 -0
  356. package/src/db.ts +295 -0
  357. package/src/debounce-timeout.ts +43 -0
  358. package/src/debounced-process-flush.ts +104 -0
  359. package/src/discord-bot.ts +1505 -0
  360. package/src/discord-command-registration.ts +752 -0
  361. package/src/discord-urls.ts +89 -0
  362. package/src/discord-utils.test.ts +153 -0
  363. package/src/discord-utils.ts +846 -0
  364. package/src/errors.ts +201 -0
  365. package/src/escape-backticks.test.ts +469 -0
  366. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  367. package/src/eventsource-parser.test.ts +351 -0
  368. package/src/exec-async.ts +35 -0
  369. package/src/external-opencode-sync.ts +685 -0
  370. package/src/format-tables.test.ts +515 -0
  371. package/src/format-tables.ts +718 -0
  372. package/src/forum-sync/config.ts +92 -0
  373. package/src/forum-sync/discord-operations.ts +241 -0
  374. package/src/forum-sync/index.ts +9 -0
  375. package/src/forum-sync/markdown.ts +172 -0
  376. package/src/forum-sync/sync-to-discord.ts +595 -0
  377. package/src/forum-sync/sync-to-files.ts +294 -0
  378. package/src/forum-sync/types.ts +175 -0
  379. package/src/forum-sync/watchers.ts +454 -0
  380. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  381. package/src/gateway-proxy.e2e.test.ts +644 -0
  382. package/src/genai-worker-wrapper.ts +164 -0
  383. package/src/genai-worker.ts +386 -0
  384. package/src/genai.ts +321 -0
  385. package/src/generated/browser.ts +114 -0
  386. package/src/generated/client.ts +138 -0
  387. package/src/generated/commonInputTypes.ts +770 -0
  388. package/src/generated/enums.ts +98 -0
  389. package/src/generated/internal/class.ts +384 -0
  390. package/src/generated/internal/prismaNamespace.ts +2394 -0
  391. package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
  392. package/src/generated/models/bot_api_keys.ts +1288 -0
  393. package/src/generated/models/bot_tokens.ts +1700 -0
  394. package/src/generated/models/channel_agents.ts +1256 -0
  395. package/src/generated/models/channel_directories.ts +1859 -0
  396. package/src/generated/models/channel_mention_mode.ts +1300 -0
  397. package/src/generated/models/channel_models.ts +1288 -0
  398. package/src/generated/models/channel_verbosity.ts +1228 -0
  399. package/src/generated/models/channel_worktrees.ts +1300 -0
  400. package/src/generated/models/forum_sync_configs.ts +1452 -0
  401. package/src/generated/models/global_models.ts +1288 -0
  402. package/src/generated/models/ipc_requests.ts +1485 -0
  403. package/src/generated/models/part_messages.ts +1302 -0
  404. package/src/generated/models/scheduled_tasks.ts +2320 -0
  405. package/src/generated/models/session_agents.ts +1086 -0
  406. package/src/generated/models/session_events.ts +1439 -0
  407. package/src/generated/models/session_models.ts +1114 -0
  408. package/src/generated/models/session_start_sources.ts +1408 -0
  409. package/src/generated/models/thread_sessions.ts +1781 -0
  410. package/src/generated/models/thread_worktrees.ts +1356 -0
  411. package/src/generated/models.ts +30 -0
  412. package/src/heap-monitor.ts +152 -0
  413. package/src/hrana-server.test.ts +434 -0
  414. package/src/hrana-server.ts +299 -0
  415. package/src/html-actions.test.ts +87 -0
  416. package/src/html-actions.ts +174 -0
  417. package/src/html-components.test.ts +38 -0
  418. package/src/html-components.ts +181 -0
  419. package/src/image-optimizer-plugin.ts +194 -0
  420. package/src/image-utils.ts +149 -0
  421. package/src/interaction-handler.ts +610 -0
  422. package/src/ipc-polling.ts +427 -0
  423. package/src/ipc-tools-plugin.ts +236 -0
  424. package/src/ipc-utils.ts +29 -0
  425. package/src/limit-heading-depth.test.ts +116 -0
  426. package/src/limit-heading-depth.ts +26 -0
  427. package/src/logger.ts +215 -0
  428. package/src/markdown.test.ts +315 -0
  429. package/src/markdown.ts +410 -0
  430. package/src/memory-overview-plugin.ts +163 -0
  431. package/src/message-finish-field.e2e.test.ts +195 -0
  432. package/src/message-formatting.test.ts +126 -0
  433. package/src/message-formatting.ts +535 -0
  434. package/src/message-preprocessing.ts +488 -0
  435. package/src/onboarding-tutorial.ts +167 -0
  436. package/src/onboarding-welcome.ts +49 -0
  437. package/src/openai-realtime.ts +358 -0
  438. package/src/opencode-command-detection.test.ts +307 -0
  439. package/src/opencode-command-detection.ts +76 -0
  440. package/src/opencode-command.test.ts +70 -0
  441. package/src/opencode-command.ts +191 -0
  442. package/src/opencode-interrupt-plugin.test.ts +682 -0
  443. package/src/opencode-interrupt-plugin.ts +507 -0
  444. package/src/opencode.ts +1453 -0
  445. package/src/otto/branding.ts +23 -0
  446. package/src/otto/index.ts +22 -0
  447. package/src/otto-digital-twin.e2e.test.ts +199 -0
  448. package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
  449. package/src/otto-opencode-plugin.test.ts +108 -0
  450. package/src/otto-opencode-plugin.ts +22 -0
  451. package/src/parse-permission-rules.test.ts +127 -0
  452. package/src/patch-text-parser.ts +107 -0
  453. package/src/plugin-logger.ts +84 -0
  454. package/src/privacy-sanitizer.ts +142 -0
  455. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  456. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  457. package/src/queue-advanced-e2e-setup.ts +877 -0
  458. package/src/queue-advanced-footer.e2e.test.ts +591 -0
  459. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  460. package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
  461. package/src/queue-advanced-question.e2e.test.ts +316 -0
  462. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  463. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  464. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  465. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  466. package/src/queue-question-select-drain.e2e.test.ts +327 -0
  467. package/src/runtime-idle-sweeper.ts +76 -0
  468. package/src/runtime-lifecycle.e2e.test.ts +651 -0
  469. package/src/schema.sql +174 -0
  470. package/src/sentry.ts +26 -0
  471. package/src/session-handler/agent-utils.ts +99 -0
  472. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  473. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  474. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  475. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  476. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  477. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  478. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  479. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  480. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  481. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  482. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  483. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  484. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  485. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  486. package/src/session-handler/event-stream-state.test.ts +717 -0
  487. package/src/session-handler/event-stream-state.ts +706 -0
  488. package/src/session-handler/model-utils.ts +217 -0
  489. package/src/session-handler/opencode-session-event-log.ts +130 -0
  490. package/src/session-handler/thread-runtime-state.ts +247 -0
  491. package/src/session-handler/thread-session-runtime.ts +4440 -0
  492. package/src/session-handler.ts +15 -0
  493. package/src/session-search.test.ts +50 -0
  494. package/src/session-search.ts +148 -0
  495. package/src/session-title-rename.test.ts +130 -0
  496. package/src/skill-filter.test.ts +83 -0
  497. package/src/skill-filter.ts +42 -0
  498. package/src/startup-service.ts +200 -0
  499. package/src/startup-time.e2e.test.ts +373 -0
  500. package/src/store.ts +139 -0
  501. package/src/subagent-rate-limit-plugin.ts +218 -0
  502. package/src/system-message.test.ts +710 -0
  503. package/src/system-message.ts +814 -0
  504. package/src/task-runner.ts +725 -0
  505. package/src/task-schedule.test.ts +84 -0
  506. package/src/task-schedule.ts +317 -0
  507. package/src/test-utils.ts +451 -0
  508. package/src/thinking-utils.ts +61 -0
  509. package/src/thread-message-queue.e2e.test.ts +1350 -0
  510. package/src/tools.ts +430 -0
  511. package/src/undici.d.ts +12 -0
  512. package/src/undo-redo.e2e.test.ts +209 -0
  513. package/src/unnest-code-blocks.test.ts +713 -0
  514. package/src/unnest-code-blocks.ts +185 -0
  515. package/src/upgrade.ts +185 -0
  516. package/src/utils.test.ts +155 -0
  517. package/src/utils.ts +265 -0
  518. package/src/voice-attachment.ts +51 -0
  519. package/src/voice-handler.ts +908 -0
  520. package/src/voice-message.e2e.test.ts +1255 -0
  521. package/src/voice.test.ts +281 -0
  522. package/src/voice.ts +638 -0
  523. package/src/wait-session.ts +273 -0
  524. package/src/websockify.ts +101 -0
  525. package/src/worker-types.ts +64 -0
  526. package/src/worktree-lifecycle.e2e.test.ts +396 -0
  527. package/src/worktree-utils.ts +4 -0
  528. package/src/worktrees.test.ts +489 -0
  529. package/src/worktrees.ts +1370 -0
  530. package/src/xml.test.ts +38 -0
  531. package/src/xml.ts +121 -0
  532. package/dist/cli.d.ts +0 -3
  533. package/dist/cli.d.ts.map +0 -1
  534. package/dist/cli.js.map +0 -1
  535. package/dist/config.d.ts +0 -39
  536. package/dist/config.d.ts.map +0 -1
  537. package/dist/config.js.map +0 -1
  538. package/dist/config.test.d.ts +0 -2
  539. package/dist/config.test.d.ts.map +0 -1
  540. package/dist/config.test.js +0 -202
  541. package/dist/config.test.js.map +0 -1
  542. package/dist/detect.d.ts +0 -9
  543. package/dist/detect.d.ts.map +0 -1
  544. package/dist/detect.js +0 -40
  545. package/dist/detect.js.map +0 -1
  546. package/dist/detect.test.d.ts +0 -2
  547. package/dist/detect.test.d.ts.map +0 -1
  548. package/dist/detect.test.js +0 -26
  549. package/dist/detect.test.js.map +0 -1
  550. package/dist/docker.d.ts +0 -7
  551. package/dist/docker.d.ts.map +0 -1
  552. package/dist/docker.js +0 -17
  553. package/dist/docker.js.map +0 -1
  554. package/dist/docker.test.d.ts +0 -2
  555. package/dist/docker.test.d.ts.map +0 -1
  556. package/dist/docker.test.js +0 -12
  557. package/dist/docker.test.js.map +0 -1
  558. package/dist/health.d.ts +0 -31
  559. package/dist/health.d.ts.map +0 -1
  560. package/dist/health.js +0 -117
  561. package/dist/health.js.map +0 -1
  562. package/dist/health.test.d.ts +0 -2
  563. package/dist/health.test.d.ts.map +0 -1
  564. package/dist/health.test.js +0 -52
  565. package/dist/health.test.js.map +0 -1
  566. package/dist/index.d.ts +0 -20
  567. package/dist/index.d.ts.map +0 -1
  568. package/dist/index.js +0 -15
  569. package/dist/index.js.map +0 -1
  570. package/dist/index.test.d.ts +0 -2
  571. package/dist/index.test.d.ts.map +0 -1
  572. package/dist/index.test.js +0 -8
  573. package/dist/index.test.js.map +0 -1
  574. package/dist/installer.d.ts +0 -10
  575. package/dist/installer.d.ts.map +0 -1
  576. package/dist/installer.js +0 -50
  577. package/dist/installer.js.map +0 -1
  578. package/dist/installer.test.d.ts +0 -2
  579. package/dist/installer.test.d.ts.map +0 -1
  580. package/dist/installer.test.js +0 -43
  581. package/dist/installer.test.js.map +0 -1
  582. package/dist/lifecycle.d.ts +0 -10
  583. package/dist/lifecycle.d.ts.map +0 -1
  584. package/dist/lifecycle.js +0 -45
  585. package/dist/lifecycle.js.map +0 -1
  586. package/dist/lifecycle.test.d.ts +0 -2
  587. package/dist/lifecycle.test.d.ts.map +0 -1
  588. package/dist/lifecycle.test.js +0 -20
  589. package/dist/lifecycle.test.js.map +0 -1
  590. package/dist/manifest.d.ts +0 -18
  591. package/dist/manifest.d.ts.map +0 -1
  592. package/dist/manifest.js +0 -30
  593. package/dist/manifest.js.map +0 -1
  594. package/dist/skills-baseline.d.ts +0 -7
  595. package/dist/skills-baseline.d.ts.map +0 -1
  596. package/dist/skills-baseline.js +0 -9
  597. package/dist/skills-baseline.js.map +0 -1
  598. package/dist/skills.d.ts +0 -110
  599. package/dist/skills.d.ts.map +0 -1
  600. package/dist/skills.js +0 -429
  601. package/dist/skills.js.map +0 -1
  602. package/dist/skills.test.d.ts +0 -2
  603. package/dist/skills.test.d.ts.map +0 -1
  604. package/dist/skills.test.js +0 -416
  605. package/dist/skills.test.js.map +0 -1
  606. package/dist/sync.d.ts +0 -10
  607. package/dist/sync.d.ts.map +0 -1
  608. package/dist/sync.js +0 -39
  609. package/dist/sync.js.map +0 -1
  610. package/dist/tenant.d.ts +0 -13
  611. package/dist/tenant.d.ts.map +0 -1
  612. package/dist/tenant.js +0 -105
  613. package/dist/tenant.js.map +0 -1
  614. package/dist/tenant.test.d.ts +0 -2
  615. package/dist/tenant.test.d.ts.map +0 -1
  616. package/dist/tenant.test.js +0 -37
  617. package/dist/tenant.test.js.map +0 -1
  618. package/src/config.test.ts +0 -237
  619. package/src/detect.test.ts +0 -29
  620. package/src/detect.ts +0 -52
  621. package/src/docker.test.ts +0 -12
  622. package/src/docker.ts +0 -23
  623. package/src/health.test.ts +0 -61
  624. package/src/health.ts +0 -158
  625. package/src/index.test.ts +0 -8
  626. package/src/index.ts +0 -62
  627. package/src/installer.test.ts +0 -52
  628. package/src/installer.ts +0 -62
  629. package/src/lifecycle.test.ts +0 -23
  630. package/src/lifecycle.ts +0 -49
  631. package/src/manifest.ts +0 -42
  632. package/src/skills-baseline.ts +0 -14
  633. package/src/skills.test.ts +0 -503
  634. package/src/skills.ts +0 -512
  635. package/src/sync.ts +0 -53
  636. package/src/tenant.test.ts +0 -49
  637. package/src/tenant.ts +0 -120
@@ -0,0 +1,359 @@
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
+ import { resolveMentions, getFileAttachments, getTextAttachments, } from './message-formatting.js';
10
+ import { processVoiceAttachment } from './voice-handler.js';
11
+ import { isVoiceAttachment } from './voice-attachment.js';
12
+ import { initializeOpencodeForDirectory } from './opencode.js';
13
+ import { getCompactSessionContext, getLastSessionId } from './markdown.js';
14
+ import { getThreadSession } from './database.js';
15
+ import * as errore from 'errore';
16
+ import { createLogger, LogPrefix } from './logger.js';
17
+ import { notifyError } from './sentry.js';
18
+ const logger = createLogger(LogPrefix.SESSION);
19
+ const voiceLogger = createLogger(LogPrefix.VOICE);
20
+ export const VOICE_MESSAGE_TRANSCRIPTION_PREFIX = 'Voice message transcription from Discord user:\n';
21
+ /** Fetch available agents from OpenCode for voice transcription agent selection. */
22
+ async function fetchAvailableAgents(getClient, directory) {
23
+ if (getClient instanceof Error) {
24
+ return [];
25
+ }
26
+ const result = await errore.tryAsync(() => {
27
+ return getClient().app.agents({ directory });
28
+ });
29
+ if (result instanceof Error) {
30
+ return [];
31
+ }
32
+ return (result.data || [])
33
+ .filter((a) => {
34
+ return (a.mode === 'primary' || a.mode === 'all') && !a.hidden;
35
+ })
36
+ .map((a) => {
37
+ return { name: a.name, description: a.description };
38
+ });
39
+ }
40
+ // Matches explicit queue markers at the end of a message (case-insensitive).
41
+ // Supported forms:
42
+ // - punctuation + queue: ". queue", "! queue", ". queue.", "!queue."
43
+ // - queue as its own final line: "text\nqueue" or just "queue"
44
+ // When present the suffix is stripped and the message is routed through
45
+ // otto's local queue (same as /queue command).
46
+ const QUEUE_SUFFIX_RE = /(?:[.!?,;:]|^)\s*queue\.?\s*$|\n\s*queue\.?\s*$/i;
47
+ const REPLIED_MESSAGE_TEXT_LIMIT = 1_000;
48
+ const TRANSPORT_METADATA_KEY_RE = /^(start|username|userId):\s*/i;
49
+ const TRANSPORT_START_TRUE_RE = /^start:\s*true\s*$/i;
50
+ const DISCORD_USER_TAG_LINE_RE = /^\s*<discord-user\b[^>]*\/>\s*$/i;
51
+ function extractQueueSuffix(prompt) {
52
+ if (!QUEUE_SUFFIX_RE.test(prompt)) {
53
+ return { prompt, forceQueue: false };
54
+ }
55
+ return { prompt: prompt.replace(QUEUE_SUFFIX_RE, '').trimEnd(), forceQueue: true };
56
+ }
57
+ export function stripTransportMetadataBlock(prompt) {
58
+ const lines = prompt.split('\n');
59
+ const firstStartIndex = lines.findIndex((line) => {
60
+ return TRANSPORT_START_TRUE_RE.test(line.trim());
61
+ });
62
+ const linesWithoutMetadataBlock = lines.filter((line, index) => {
63
+ if (firstStartIndex === -1 || index < firstStartIndex) {
64
+ return true;
65
+ }
66
+ const trimmed = line.trim();
67
+ if (!trimmed) {
68
+ return true;
69
+ }
70
+ return !TRANSPORT_METADATA_KEY_RE.test(trimmed);
71
+ });
72
+ return linesWithoutMetadataBlock
73
+ .filter((line) => {
74
+ return !DISCORD_USER_TAG_LINE_RE.test(line);
75
+ })
76
+ .join('\n')
77
+ .trim();
78
+ }
79
+ function shouldSkipEmptyPrompt({ message, prompt, images, hasVoiceAttachment, }) {
80
+ if (prompt.trim()) {
81
+ return false;
82
+ }
83
+ if ((images?.length || 0) > 0) {
84
+ return false;
85
+ }
86
+ const inferredVoiceAttachment = message.attachments.some((attachment) => {
87
+ return isVoiceAttachment(attachment);
88
+ });
89
+ if (!hasVoiceAttachment && !inferredVoiceAttachment && message.attachments.size === 0) {
90
+ return false;
91
+ }
92
+ voiceLogger.warn(`[INGRESS] Skipping empty prompt after preprocessing attachments=${message.attachments.size} hasVoiceAttachment=${hasVoiceAttachment} inferredVoiceAttachment=${inferredVoiceAttachment}`);
93
+ return true;
94
+ }
95
+ async function getRepliedMessageContext({ message, }) {
96
+ if (!message.reference?.messageId) {
97
+ return undefined;
98
+ }
99
+ const referencedMessage = await errore.tryAsync(() => {
100
+ return message.fetchReference();
101
+ });
102
+ if (referencedMessage instanceof Error) {
103
+ logger.warn(`[INGRESS] Failed to fetch replied message ${message.reference.messageId} for ${message.id}: ${referencedMessage.message}`);
104
+ return undefined;
105
+ }
106
+ const repliedText = resolveMentions(referencedMessage)
107
+ .trim()
108
+ .slice(0, REPLIED_MESSAGE_TEXT_LIMIT);
109
+ if (!repliedText) {
110
+ return undefined;
111
+ }
112
+ return {
113
+ authorUsername: referencedMessage.author.username,
114
+ text: repliedText,
115
+ };
116
+ }
117
+ /**
118
+ * Pre-process a message in an existing thread (thread already has a session or
119
+ * needs a new one). Handles voice transcription, text/file attachments, and
120
+ * session context fetching for voice messages.
121
+ *
122
+ * For threads with an existing session, voice transcription is enriched with
123
+ * current + last session context (used by the transcription model to better
124
+ * understand domain-specific terms).
125
+ */
126
+ export async function preprocessExistingThreadMessage({ message, thread, projectDirectory, channelId, isCliInjected, hasVoiceAttachment, appId, }) {
127
+ const sessionId = await getThreadSession(thread.id);
128
+ // ── No existing session: new session in an existing thread ──
129
+ if (!sessionId) {
130
+ return preprocessNewSessionMessage({
131
+ message,
132
+ thread,
133
+ projectDirectory,
134
+ hasVoiceAttachment,
135
+ appId,
136
+ });
137
+ }
138
+ // ── Existing session path ──
139
+ voiceLogger.log(`[SESSION] Found session ${sessionId} for thread ${thread.id}`);
140
+ let messageContent = isCliInjected
141
+ ? (message.content || '')
142
+ : resolveMentions(message);
143
+ const repliedMessage = await getRepliedMessageContext({ message });
144
+ // Fetch session context and available agents for voice transcription enrichment
145
+ let currentSessionContext;
146
+ let lastSessionContext;
147
+ let agents = [];
148
+ if (projectDirectory) {
149
+ try {
150
+ const getClient = await initializeOpencodeForDirectory(projectDirectory, { channelId });
151
+ if (getClient instanceof Error) {
152
+ voiceLogger.error(`[SESSION] Failed to initialize OpenCode client:`, getClient.message);
153
+ throw new Error(getClient.message);
154
+ }
155
+ const client = getClient();
156
+ const [sessionContextResult, lastSessionResult, fetchedAgents] = await Promise.all([
157
+ getCompactSessionContext({
158
+ client,
159
+ sessionId,
160
+ includeSystemPrompt: false,
161
+ maxMessages: 15,
162
+ }),
163
+ getLastSessionId({
164
+ client,
165
+ excludeSessionId: sessionId,
166
+ }),
167
+ fetchAvailableAgents(getClient, projectDirectory),
168
+ ]);
169
+ if (errore.isOk(sessionContextResult)) {
170
+ currentSessionContext = sessionContextResult;
171
+ }
172
+ agents = fetchedAgents;
173
+ const lastSessionId = errore.unwrapOr(lastSessionResult, null);
174
+ if (lastSessionId) {
175
+ const result = await getCompactSessionContext({
176
+ client,
177
+ sessionId: lastSessionId,
178
+ includeSystemPrompt: true,
179
+ maxMessages: 10,
180
+ });
181
+ if (errore.isOk(result)) {
182
+ lastSessionContext = result;
183
+ }
184
+ }
185
+ }
186
+ catch (e) {
187
+ voiceLogger.error(`Could not get session context:`, e);
188
+ void notifyError(e, 'Failed to get session context');
189
+ }
190
+ }
191
+ const voiceResult = await processVoiceAttachment({
192
+ message,
193
+ thread,
194
+ projectDirectory,
195
+ appId,
196
+ currentSessionContext,
197
+ lastSessionContext,
198
+ agents,
199
+ });
200
+ if (voiceResult) {
201
+ messageContent = `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`;
202
+ }
203
+ // Voice transcription failed and no text — drop silently
204
+ if (hasVoiceAttachment && !voiceResult && !messageContent.trim()) {
205
+ return { prompt: '', mode: 'opencode', skip: true };
206
+ }
207
+ // Extract queue suffix from raw message content BEFORE appending text
208
+ // attachments. Otherwise a text file attachment pushes "? queue" away from
209
+ // the end of the string and the regex fails to match.
210
+ const qs = extractQueueSuffix(messageContent);
211
+ const fileAttachments = await getFileAttachments(message);
212
+ const textAttachmentsContent = await getTextAttachments(message);
213
+ const prompt = textAttachmentsContent
214
+ ? `${qs.prompt}\n\n${textAttachmentsContent}`
215
+ : qs.prompt;
216
+ const cleanedPrompt = stripTransportMetadataBlock(prompt);
217
+ if (shouldSkipEmptyPrompt({
218
+ message,
219
+ prompt: cleanedPrompt,
220
+ images: fileAttachments,
221
+ hasVoiceAttachment,
222
+ })) {
223
+ return { prompt: '', mode: 'opencode', skip: true };
224
+ }
225
+ return {
226
+ prompt: cleanedPrompt,
227
+ images: fileAttachments.length > 0 ? fileAttachments : undefined,
228
+ repliedMessage,
229
+ mode: qs.forceQueue || voiceResult?.queueMessage ? 'local-queue' : 'opencode',
230
+ agent: voiceResult?.agent,
231
+ };
232
+ }
233
+ /**
234
+ * Pre-process a message that starts a new session in a thread (no existing
235
+ * session). Handles starter message context, voice transcription, and
236
+ * text/file attachments.
237
+ */
238
+ export async function preprocessNewSessionMessage({ message, thread, projectDirectory, hasVoiceAttachment, appId, }) {
239
+ logger.log(`No session for thread ${thread.id}, starting new session`);
240
+ // Fetch available agents only for voice messages to avoid unnecessary SDK
241
+ // roundtrips on plain text messages.
242
+ let agents = [];
243
+ if (hasVoiceAttachment && projectDirectory) {
244
+ try {
245
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
246
+ agents = await fetchAvailableAgents(getClient, projectDirectory);
247
+ }
248
+ catch (e) {
249
+ voiceLogger.error(`Could not fetch agents for voice transcription:`, e);
250
+ }
251
+ }
252
+ let prompt = resolveMentions(message);
253
+ const repliedMessage = await getRepliedMessageContext({ message });
254
+ const voiceResult = await processVoiceAttachment({
255
+ message,
256
+ thread,
257
+ projectDirectory,
258
+ appId,
259
+ agents,
260
+ });
261
+ if (voiceResult) {
262
+ prompt = `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`;
263
+ }
264
+ // Voice transcription failed and no text — drop silently
265
+ if (hasVoiceAttachment && !voiceResult && !prompt.trim()) {
266
+ return { prompt: '', mode: 'opencode', skip: true };
267
+ }
268
+ // Fetch starter message for thread context
269
+ const starterMessage = await thread
270
+ .fetchStarterMessage()
271
+ .catch((error) => {
272
+ logger.warn(`[SESSION] Failed to fetch starter message for thread ${thread.id}:`, error instanceof Error ? error.stack : String(error));
273
+ return null;
274
+ });
275
+ if (starterMessage && starterMessage.content !== message.content) {
276
+ const starterTextAttachments = await getTextAttachments(starterMessage);
277
+ const starterContent = resolveMentions(starterMessage);
278
+ const starterText = starterTextAttachments
279
+ ? `${starterContent}\n\n${starterTextAttachments}`
280
+ : starterContent;
281
+ if (starterText) {
282
+ prompt = `Context from thread:\n${starterText}\n\nUser request:\n${prompt}`;
283
+ }
284
+ }
285
+ const cleanedPrompt = stripTransportMetadataBlock(prompt);
286
+ const qs = extractQueueSuffix(cleanedPrompt);
287
+ if (shouldSkipEmptyPrompt({
288
+ message,
289
+ prompt: qs.prompt,
290
+ hasVoiceAttachment,
291
+ })) {
292
+ return { prompt: '', mode: 'opencode', skip: true };
293
+ }
294
+ return {
295
+ prompt: qs.prompt,
296
+ repliedMessage,
297
+ mode: qs.forceQueue || voiceResult?.queueMessage ? 'local-queue' : 'opencode',
298
+ agent: voiceResult?.agent,
299
+ };
300
+ }
301
+ /**
302
+ * Pre-process a message from a text channel (creates a new thread).
303
+ * Handles voice transcription and file/text attachments.
304
+ */
305
+ export async function preprocessNewThreadMessage({ message, thread, projectDirectory, hasVoiceAttachment, appId, }) {
306
+ // Fetch available agents only for voice messages to avoid unnecessary SDK
307
+ // roundtrips on plain text messages.
308
+ let agents = [];
309
+ if (hasVoiceAttachment && projectDirectory) {
310
+ try {
311
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
312
+ agents = await fetchAvailableAgents(getClient, projectDirectory);
313
+ }
314
+ catch (e) {
315
+ voiceLogger.error(`Could not fetch agents for voice transcription:`, e);
316
+ }
317
+ }
318
+ let messageContent = resolveMentions(message);
319
+ const repliedMessage = await getRepliedMessageContext({ message });
320
+ const voiceResult = await processVoiceAttachment({
321
+ message,
322
+ thread,
323
+ projectDirectory,
324
+ isNewThread: true,
325
+ appId,
326
+ agents,
327
+ });
328
+ if (voiceResult) {
329
+ messageContent = `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`;
330
+ }
331
+ // Voice transcription failed and no text — drop silently
332
+ if (hasVoiceAttachment && !voiceResult && !messageContent.trim()) {
333
+ return { prompt: '', mode: 'opencode', skip: true };
334
+ }
335
+ // Extract queue suffix from raw message content BEFORE appending text
336
+ // attachments (same fix as preprocessExistingThreadMessage).
337
+ const qs = extractQueueSuffix(messageContent);
338
+ const fileAttachments = await getFileAttachments(message);
339
+ const textAttachmentsContent = await getTextAttachments(message);
340
+ const prompt = textAttachmentsContent
341
+ ? `${qs.prompt}\n\n${textAttachmentsContent}`
342
+ : qs.prompt;
343
+ const cleanedPrompt = stripTransportMetadataBlock(prompt);
344
+ if (shouldSkipEmptyPrompt({
345
+ message,
346
+ prompt: cleanedPrompt,
347
+ images: fileAttachments,
348
+ hasVoiceAttachment,
349
+ })) {
350
+ return { prompt: '', mode: 'opencode', skip: true };
351
+ }
352
+ return {
353
+ prompt: cleanedPrompt,
354
+ images: fileAttachments.length > 0 ? fileAttachments : undefined,
355
+ repliedMessage,
356
+ mode: qs.forceQueue || voiceResult?.queueMessage ? 'local-queue' : 'opencode',
357
+ agent: voiceResult?.agent,
358
+ };
359
+ }
@@ -0,0 +1,163 @@
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
+ // Unique text used in the welcome message and detected by the plugin to
8
+ // trigger tutorial instruction injection. Shared constant so they can't
9
+ // drift out of sync.
10
+ export const TUTORIAL_WELCOME_TEXT = 'Want to build an example browser game? Respond in this thread.';
11
+ const markdown = String.raw;
12
+ const backticks = '```';
13
+ export const ONBOARDING_TUTORIAL_INSTRUCTIONS = markdown `
14
+ 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.
15
+
16
+ ## Prerequisites
17
+
18
+ Before doing anything else, check that these are installed:
19
+
20
+ **Bun** (v1.2 or later) — runtime and bundler:
21
+
22
+ ${backticks}bash
23
+ bun --version
24
+ ${backticks}
25
+
26
+ If missing or below 1.2, tell the user to install it: https://bun.sh — or run:
27
+
28
+ ${backticks}bash
29
+ curl -fsSL https://bun.sh/install | bash
30
+ ${backticks}
31
+
32
+ **tuistory** — needed to run the dev server in the background with otto tunnel:
33
+
34
+ ${backticks}bash
35
+ bunx tuistory --help
36
+ ${backticks}
37
+
38
+ This works without installing it globally because \`bunx\` can run it on demand.
39
+
40
+ Do NOT use Node.js, npm, or npx. Use Bun for everything.
41
+
42
+ ## Goal
43
+
44
+ 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.
45
+
46
+ ## Game idea
47
+
48
+ Build a "Space Dodge" game:
49
+ - The player controls a spaceship that flies forward through space
50
+ - Asteroids/obstacles come toward the player
51
+ - The player dodges left/right/up/down using arrow keys or WASD
52
+ - Touch/swipe controls for mobile — the user is on Discord and may open the link on their phone
53
+ - Score increases over time, speed gradually increases
54
+ - Particle effects for explosions when hit
55
+ - Starfield background for atmosphere
56
+ - Simple start screen and game over screen with score
57
+
58
+ If the game idea doesn't match what the user asked for, adapt to their request instead.
59
+
60
+ ## Project setup
61
+
62
+ Create these files:
63
+
64
+ **package.json** — install three as a dependency:
65
+ ${backticks}json
66
+ {
67
+ "dependencies": {
68
+ "three": "^0.170.0"
69
+ }
70
+ }
71
+ ${backticks}
72
+
73
+ Run bun install after creating it.
74
+
75
+ **tsconfig.json**:
76
+ ${backticks}json
77
+ {
78
+ "compilerOptions": {
79
+ "target": "ESNext",
80
+ "module": "ESNext",
81
+ "moduleResolution": "bundler",
82
+ "strict": true,
83
+ "jsx": "react-jsx",
84
+ "types": ["three"]
85
+ }
86
+ }
87
+ ${backticks}
88
+
89
+ **index.html** — the entry point, references the TypeScript source:
90
+ ${backticks}html
91
+ <!DOCTYPE html>
92
+ <html>
93
+ <head>
94
+ <meta charset="utf-8" />
95
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
96
+ <title>Space Dodge</title>
97
+ <style>
98
+ body { margin: 0; overflow: hidden; }
99
+ canvas { display: block; }
100
+ </style>
101
+ </head>
102
+ <body>
103
+ <script type="module" src="./game.ts"></script>
104
+ </body>
105
+ </html>
106
+ ${backticks}
107
+
108
+ **game.ts** — all game logic in TypeScript, importing from "three":
109
+ ${backticks}ts
110
+ import * as THREE from "three"
111
+ // ... game code here
112
+ ${backticks}
113
+
114
+ 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.
115
+
116
+ **server.ts** — Bun fullstack dev server (reads port from PORT env var):
117
+ ${backticks}ts
118
+ import homepage from "./index.html"
119
+
120
+ Bun.serve({
121
+ port: Number(process.env.PORT) || 3000,
122
+ routes: { "/": homepage },
123
+ development: true,
124
+ })
125
+ ${backticks}
126
+
127
+ ## Dev server and tunnel
128
+
129
+ 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.
130
+
131
+ Pick a random port between 3000-9000 to avoid conflicts:
132
+
133
+ ${backticks}bash
134
+ PORT=$((RANDOM % 6000 + 3000))
135
+ bunx tuistory launch "PORT=$PORT otto tunnel -p $PORT -- bun run server.ts" -s game-dev --cwd "$PWD"
136
+ ${backticks}
137
+
138
+ Wait a moment, then get the tunnel URL:
139
+
140
+ ${backticks}bash
141
+ bunx tuistory -s game-dev wait "/tunnel|https?:\/\//i" --timeout 30000
142
+ bunx tuistory read -s game-dev
143
+ ${backticks}
144
+
145
+ If the tunnel URL is not visible yet, run the capture command again — it usually appears within a few seconds.
146
+
147
+ After the tunnel is up, also generate a diff URL so the user can browse the code:
148
+
149
+ ${backticks}bash
150
+ git init && git add -A && critique --url
151
+ ${backticks}
152
+
153
+ 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.
154
+
155
+ ## Behavior
156
+
157
+ - Be friendly and casual in the conversation. Sound like a helpful teammate, not a formal tutorial.
158
+ - Be concise and direct. Show the code, start the server, share the URL.
159
+ - 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.
160
+ - Never mention this is a tutorial, onboarding, or demo. Act like a normal coding session.
161
+ - Never apologize or over-explain. Just build and iterate.
162
+ - If the user asks to change something, do it quickly and share the updated URL.
163
+ `;
@@ -0,0 +1,37 @@
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
+ import { ThreadAutoArchiveDuration } from 'discord.js';
8
+ import { createLogger, LogPrefix } from './logger.js';
9
+ import { TUTORIAL_WELCOME_TEXT } from './onboarding-tutorial.js';
10
+ const logger = createLogger(LogPrefix.CHANNEL);
11
+ function buildWelcomeText() {
12
+ 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.
13
+ **What you can do:**
14
+ - Use \`/add-project\` to create a Discord channel linked to one OpenCode project (git repo)
15
+ - Collaborate with teammates in the same session
16
+ - Upload images and files, the bot can share screenshots back
17
+ ${TUTORIAL_WELCOME_TEXT}`;
18
+ }
19
+ function buildThreadPrompt({ mentionUserId }) {
20
+ const mentionSuffix = mentionUserId ? ` <@${mentionUserId}>` : '';
21
+ return `Want to build an example browser game? Respond in this thread.${mentionSuffix}`;
22
+ }
23
+ export async function sendWelcomeMessage({ channel, mentionUserId, }) {
24
+ try {
25
+ const message = await channel.send(buildWelcomeText());
26
+ const thread = await message.startThread({
27
+ name: 'Otto tutorial',
28
+ autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
29
+ reason: 'Onboarding tutorial thread',
30
+ });
31
+ await thread.send(buildThreadPrompt({ mentionUserId }));
32
+ logger.log(`Sent welcome message with thread to #${channel.name}`);
33
+ }
34
+ catch (error) {
35
+ logger.warn(`Failed to send welcome message to #${channel.name}: ${error instanceof Error ? error.stack : String(error)}`);
36
+ }
37
+ }