@otto-assistant/otto 0.1.2 → 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 (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 +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/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,327 @@
1
+ // IPC polling bridge between the opencode plugin and the Discord bot.
2
+ // The plugin inserts rows into ipc_requests (via Prisma). This module polls
3
+ // that table, claims pending rows atomically, and dispatches them by type.
4
+ // Replaces the old HTTP lock-server approach with DB-based IPC.
5
+ import * as errore from "errore";
6
+ import { createTaggedError } from "errore";
7
+ import { claimPendingIpcRequests, completeIpcRequest, cancelAllPendingIpcRequests, cancelStaleProcessingRequests, } from "./database.js";
8
+ import { showFileUploadButton } from "./commands/file-upload.js";
9
+ import { queueActionButtonsRequest } from "./commands/action-buttons.js";
10
+ import { getOrCreateRuntime } from "./session-handler/thread-session-runtime.js";
11
+ import * as threadState from "./session-handler/thread-runtime-state.js";
12
+ import { createLogger, LogPrefix } from "./logger.js";
13
+ import { notifyError } from "./sentry.js";
14
+ const ipcLogger = createLogger(LogPrefix.IPC);
15
+ // ── Tagged errors ────────────────────────────────────────────────────────
16
+ class IpcDispatchError extends createTaggedError({
17
+ name: "IpcDispatchError",
18
+ message: "IPC dispatch failed for request $requestId: $reason",
19
+ }) {
20
+ }
21
+ // ── Button parsing ───────────────────────────────────────────────────────
22
+ const VALID_COLORS = new Set([
23
+ "white",
24
+ "blue",
25
+ "green",
26
+ "red",
27
+ ]);
28
+ function parseButtons(raw) {
29
+ if (!Array.isArray(raw))
30
+ return [];
31
+ const results = [];
32
+ for (const value of raw) {
33
+ if (!value || typeof value !== "object")
34
+ continue;
35
+ const label = (typeof value.label === "string" ? value.label : "")
36
+ .trim()
37
+ .slice(0, 80);
38
+ if (!label)
39
+ continue;
40
+ const color = typeof value.color === "string" &&
41
+ VALID_COLORS.has(value.color)
42
+ ? value.color
43
+ : undefined;
44
+ results.push({ label, color });
45
+ if (results.length >= 3)
46
+ break;
47
+ }
48
+ return results;
49
+ }
50
+ async function dispatchRequest({ req, discordClient, }) {
51
+ switch (req.type) {
52
+ case "file_upload": {
53
+ const parsed = errore.try({
54
+ try: () => JSON.parse(req.payload),
55
+ catch: (e) => new IpcDispatchError({
56
+ requestId: req.id,
57
+ reason: "Invalid payload JSON",
58
+ cause: e,
59
+ }),
60
+ });
61
+ if (parsed instanceof Error) {
62
+ await completeIpcRequest({
63
+ id: req.id,
64
+ response: JSON.stringify({ error: parsed.message }),
65
+ });
66
+ return parsed;
67
+ }
68
+ const thread = await discordClient.channels.fetch(req.thread_id).catch((e) => new IpcDispatchError({
69
+ requestId: req.id,
70
+ reason: "Thread fetch failed",
71
+ cause: e,
72
+ }));
73
+ if (thread instanceof Error) {
74
+ await completeIpcRequest({
75
+ id: req.id,
76
+ response: JSON.stringify({ error: "Thread not found" }),
77
+ });
78
+ return thread;
79
+ }
80
+ if (!thread?.isThread()) {
81
+ await completeIpcRequest({
82
+ id: req.id,
83
+ response: JSON.stringify({ error: "Thread not found" }),
84
+ });
85
+ return new IpcDispatchError({
86
+ requestId: req.id,
87
+ reason: "Channel is not a thread",
88
+ });
89
+ }
90
+ // Fire-and-forget: showFileUploadButton waits for user interaction
91
+ // (button click + modal + file download) which can take minutes.
92
+ // Don't block the dispatch loop — complete the IPC request asynchronously.
93
+ showFileUploadButton({
94
+ thread,
95
+ sessionId: req.session_id,
96
+ directory: parsed.directory || "",
97
+ prompt: parsed.prompt || "Please upload files",
98
+ maxFiles: Math.min(10, Math.max(1, parsed.maxFiles || 5)),
99
+ })
100
+ .then((filePaths) => {
101
+ return completeIpcRequest({
102
+ id: req.id,
103
+ response: JSON.stringify({ filePaths }),
104
+ });
105
+ })
106
+ .catch((e) => {
107
+ ipcLogger.error("[IPC] File upload error:", e instanceof Error ? e.message : String(e));
108
+ return completeIpcRequest({
109
+ id: req.id,
110
+ response: JSON.stringify({
111
+ error: e instanceof Error ? e.message : "File upload failed",
112
+ }),
113
+ });
114
+ })
115
+ .catch((e) => {
116
+ void notifyError(e, "IPC file upload completion update failed");
117
+ });
118
+ return;
119
+ }
120
+ case "action_buttons": {
121
+ const parsed = errore.try({
122
+ try: () => JSON.parse(req.payload),
123
+ catch: (e) => new IpcDispatchError({
124
+ requestId: req.id,
125
+ reason: "Invalid payload JSON",
126
+ cause: e,
127
+ }),
128
+ });
129
+ if (parsed instanceof Error) {
130
+ await completeIpcRequest({
131
+ id: req.id,
132
+ response: JSON.stringify({ error: parsed.message }),
133
+ });
134
+ return parsed;
135
+ }
136
+ const buttons = parseButtons(parsed.buttons);
137
+ if (buttons.length === 0) {
138
+ await completeIpcRequest({
139
+ id: req.id,
140
+ response: JSON.stringify({ error: "No valid buttons" }),
141
+ });
142
+ return;
143
+ }
144
+ const thread = await discordClient.channels.fetch(req.thread_id).catch((e) => new IpcDispatchError({
145
+ requestId: req.id,
146
+ reason: "Thread fetch failed",
147
+ cause: e,
148
+ }));
149
+ if (thread instanceof Error) {
150
+ await completeIpcRequest({
151
+ id: req.id,
152
+ response: JSON.stringify({ error: "Thread not found" }),
153
+ });
154
+ return thread;
155
+ }
156
+ if (!thread?.isThread()) {
157
+ await completeIpcRequest({
158
+ id: req.id,
159
+ response: JSON.stringify({ error: "Thread not found" }),
160
+ });
161
+ return new IpcDispatchError({
162
+ requestId: req.id,
163
+ reason: "Channel is not a thread",
164
+ });
165
+ }
166
+ queueActionButtonsRequest({
167
+ sessionId: req.session_id,
168
+ threadId: req.thread_id,
169
+ directory: parsed.directory || "",
170
+ buttons,
171
+ });
172
+ await completeIpcRequest({
173
+ id: req.id,
174
+ response: JSON.stringify({ ok: true }),
175
+ });
176
+ return;
177
+ }
178
+ case "start_thread_listener": {
179
+ const parsed = errore.try({
180
+ try: () => JSON.parse(req.payload),
181
+ catch: (e) => new IpcDispatchError({
182
+ requestId: req.id,
183
+ reason: "Invalid payload JSON",
184
+ cause: e,
185
+ }),
186
+ });
187
+ if (parsed instanceof Error) {
188
+ await completeIpcRequest({
189
+ id: req.id,
190
+ response: JSON.stringify({ error: parsed.message }),
191
+ });
192
+ return parsed;
193
+ }
194
+ const thread = await discordClient.channels.fetch(req.thread_id).catch((e) => new IpcDispatchError({
195
+ requestId: req.id,
196
+ reason: "Thread fetch failed",
197
+ cause: e,
198
+ }));
199
+ if (thread instanceof Error) {
200
+ await completeIpcRequest({
201
+ id: req.id,
202
+ response: JSON.stringify({ error: "Thread fetch failed" }),
203
+ });
204
+ return thread;
205
+ }
206
+ if (!thread?.isThread()) {
207
+ await completeIpcRequest({
208
+ id: req.id,
209
+ response: JSON.stringify({ error: "Channel is not a thread" }),
210
+ });
211
+ return new IpcDispatchError({
212
+ requestId: req.id,
213
+ reason: "Channel is not a thread",
214
+ });
215
+ }
216
+ const runtime = getOrCreateRuntime({
217
+ threadId: req.thread_id,
218
+ thread,
219
+ projectDirectory: parsed.projectDirectory,
220
+ sdkDirectory: parsed.projectDirectory,
221
+ channelId: parsed.channelId,
222
+ appId: parsed.appId,
223
+ });
224
+ // Ensure runtime has a session ID before subscribing, otherwise
225
+ // session-scoped events get dropped by the event demux guard.
226
+ threadState.setSessionId(req.thread_id, req.session_id);
227
+ void runtime.startEventListener();
228
+ if (parsed.initialPrompt && parsed.initialPrompt.trim().length > 0) {
229
+ const enqueueResult = await runtime.enqueueIncoming({
230
+ prompt: parsed.initialPrompt,
231
+ userId: parsed.userId || "otto-cli",
232
+ username: parsed.username || "otto-cli",
233
+ sourceMessageId: parsed.sourceMessageId,
234
+ sourceThreadId: parsed.sourceThreadId,
235
+ appId: parsed.appId,
236
+ agent: parsed.agent,
237
+ model: parsed.model,
238
+ permissions: parsed.permissions,
239
+ injectionGuardPatterns: parsed.injectionGuardPatterns,
240
+ });
241
+ if (enqueueResult instanceof Error) {
242
+ await completeIpcRequest({
243
+ id: req.id,
244
+ response: JSON.stringify({ error: enqueueResult.message }),
245
+ });
246
+ return enqueueResult;
247
+ }
248
+ }
249
+ await completeIpcRequest({
250
+ id: req.id,
251
+ response: JSON.stringify({ ok: true }),
252
+ });
253
+ return;
254
+ }
255
+ default: {
256
+ await completeIpcRequest({
257
+ id: req.id,
258
+ response: JSON.stringify({ error: `Unknown IPC type: ${req.type}` }),
259
+ });
260
+ return;
261
+ }
262
+ }
263
+ }
264
+ // ── Polling lifecycle ────────────────────────────────────────────────────
265
+ let pollingInterval = null;
266
+ // Cancel requests stuck in 'processing' longer than 24 hours. Users often
267
+ // come back the next day to click permission/question/file-upload buttons,
268
+ // so we keep IPC rows alive for a full day. Checked every 30 seconds.
269
+ const STALE_TTL_MS = 24 * 60 * 60 * 1000;
270
+ const STALE_CHECK_INTERVAL_MS = 30 * 1000;
271
+ let lastStaleCheck = 0;
272
+ /**
273
+ * Start polling the ipc_requests table for pending requests from the plugin.
274
+ * Claims rows atomically (pending -> processing) to prevent duplicate dispatch.
275
+ * Uses an in-flight guard to prevent overlapping poll ticks.
276
+ */
277
+ export async function startIpcPolling({ discordClient, }) {
278
+ // Clean up stale requests from previous runs before first poll tick
279
+ await cancelAllPendingIpcRequests().catch((e) => {
280
+ ipcLogger.warn("Failed to cancel stale IPC requests:", e.message);
281
+ void notifyError(e, "Failed to cancel stale IPC requests");
282
+ });
283
+ let polling = false;
284
+ pollingInterval = setInterval(async () => {
285
+ if (polling)
286
+ return;
287
+ polling = true;
288
+ // Periodically sweep requests stuck in 'processing' past the TTL
289
+ const now = Date.now();
290
+ if (now - lastStaleCheck > STALE_CHECK_INTERVAL_MS) {
291
+ lastStaleCheck = now;
292
+ await cancelStaleProcessingRequests({ ttlMs: STALE_TTL_MS }).catch((e) => {
293
+ ipcLogger.warn("Stale sweep failed:", e.message);
294
+ void notifyError(e, "IPC stale sweep failed");
295
+ });
296
+ }
297
+ const claimed = await claimPendingIpcRequests().catch((e) => new IpcDispatchError({
298
+ requestId: "poll",
299
+ reason: "Claim failed",
300
+ cause: e,
301
+ }));
302
+ if (claimed instanceof Error) {
303
+ ipcLogger.error("IPC claim failed:", claimed.message);
304
+ void notifyError(claimed, "IPC claim failed");
305
+ polling = false;
306
+ return;
307
+ }
308
+ for (const req of claimed) {
309
+ const result = await dispatchRequest({ req, discordClient }).catch((e) => new IpcDispatchError({
310
+ requestId: req.id,
311
+ reason: "Dispatch threw",
312
+ cause: e,
313
+ }));
314
+ if (result instanceof Error) {
315
+ ipcLogger.error(`IPC dispatch error for ${req.type}:`, result.message);
316
+ void notifyError(result, `IPC dispatch error for ${req.type}`);
317
+ }
318
+ }
319
+ polling = false;
320
+ }, 200);
321
+ }
322
+ export function stopIpcPolling() {
323
+ if (!pollingInterval)
324
+ return;
325
+ clearInterval(pollingInterval);
326
+ pollingInterval = null;
327
+ }
@@ -0,0 +1,193 @@
1
+ // OpenCode plugin that provides IPC-based tools for Discord interaction:
2
+ // - otto_file_upload: prompts the Discord user to upload files via native picker
3
+ // - otto_action_buttons: shows clickable action buttons in the Discord thread
4
+ //
5
+ // Tools communicate with the bot process via IPC rows in SQLite (the plugin
6
+ // runs inside the OpenCode server process, not the bot process).
7
+ //
8
+ // Exported from otto-opencode-plugin.ts — each export is treated as a separate
9
+ // plugin by OpenCode's plugin loader.
10
+ import dedent from 'string-dedent';
11
+ import { z } from 'zod';
12
+ import { setDataDir } from './config.js';
13
+ import { createPluginLogger, setPluginLogFilePath } from './plugin-logger.js';
14
+ import { initSentry } from './sentry.js';
15
+ // Inlined from '@opencode-ai/plugin/tool' because the subpath value import
16
+ // fails at runtime in global npm installs (#35). Opencode loads this plugin
17
+ // file in its own process and resolves modules from otto's install dir,
18
+ // but the '/tool' subpath export isn't found by opencode's module resolver.
19
+ // The type-only imports above are fine (erased at compile time).
20
+ //
21
+ // NOTE: @opencode-ai/plugin bundles its own zod 4.1.x as a hard dependency
22
+ // while goke (used by cli.ts) requires zod 4.3.x. This version skew makes
23
+ // the Plugin return type structurally incompatible with our local tool()
24
+ // even though runtime behavior is identical. ipcToolsPlugin is cast to
25
+ // Plugin via unknown to bypass this purely type-level incompatibility.
26
+ function tool(input) {
27
+ return input;
28
+ }
29
+ const logger = createPluginLogger('OPENCODE');
30
+ const FILE_UPLOAD_TIMEOUT_MS = 6 * 60 * 1000;
31
+ const DEFAULT_FILE_UPLOAD_MAX_FILES = 5;
32
+ const ACTION_BUTTON_TIMEOUT_MS = 30 * 1000;
33
+ async function loadDatabaseModule() {
34
+ // The plugin-loading e2e test boots OpenCode directly without the bot-side
35
+ // Hrana env vars. Lazy-loading avoids pulling Prisma + libsql sqlite mode
36
+ // during plugin startup when no IPC tool is being executed yet.
37
+ return import('./database.js');
38
+ }
39
+ // @opencode-ai/plugin bundles zod 4.1.x as a hard dep; our code uses 4.3.x
40
+ // (required by goke for ~standard.jsonSchema). The Plugin return type is
41
+ // structurally incompatible due to _zod.version.minor skew even though
42
+ // runtime behavior is identical. `any` bypasses the type-level mismatch —
43
+ // opencode's plugin loader doesn't care about the zod version at runtime.
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ const ipcToolsPlugin = async () => {
46
+ initSentry();
47
+ const dataDir = process.env.OTTO_DATA_DIR;
48
+ if (dataDir) {
49
+ setDataDir(dataDir);
50
+ setPluginLogFilePath(dataDir);
51
+ }
52
+ return {
53
+ tool: {
54
+ otto_file_upload: tool({
55
+ description: 'Prompt the Discord user to upload files using a native file picker modal. ' +
56
+ 'The user sees a button, clicks it, and gets a file upload dialog. ' +
57
+ 'Returns the local file paths of downloaded files in the project directory. ' +
58
+ 'Use this when you need the user to provide files (images, documents, configs, etc.). ' +
59
+ 'IMPORTANT: Always call this tool last in your message, after all text parts.',
60
+ args: {
61
+ prompt: z
62
+ .string()
63
+ .describe('Message shown to the user explaining what files to upload'),
64
+ maxFiles: z
65
+ .number()
66
+ .min(1)
67
+ .max(10)
68
+ .optional()
69
+ .describe('Maximum number of files the user can upload (1-10, default 5)'),
70
+ },
71
+ async execute({ prompt, maxFiles }, context) {
72
+ const { getPrisma, createIpcRequest, getIpcRequestById } = await loadDatabaseModule();
73
+ const prisma = await getPrisma();
74
+ const row = await prisma.thread_sessions.findFirst({
75
+ where: { session_id: context.sessionID },
76
+ select: { thread_id: true },
77
+ });
78
+ if (!row?.thread_id) {
79
+ return 'Could not find thread for current session';
80
+ }
81
+ const ipcRow = await createIpcRequest({
82
+ type: 'file_upload',
83
+ sessionId: context.sessionID,
84
+ threadId: row.thread_id,
85
+ payload: JSON.stringify({
86
+ prompt,
87
+ maxFiles: maxFiles || DEFAULT_FILE_UPLOAD_MAX_FILES,
88
+ directory: context.directory,
89
+ }),
90
+ });
91
+ const deadline = Date.now() + FILE_UPLOAD_TIMEOUT_MS;
92
+ const POLL_INTERVAL_MS = 300;
93
+ while (Date.now() < deadline) {
94
+ await new Promise((resolve) => {
95
+ setTimeout(resolve, POLL_INTERVAL_MS);
96
+ });
97
+ const updated = await getIpcRequestById({ id: ipcRow.id });
98
+ if (!updated || updated.status === 'cancelled') {
99
+ return 'File upload was cancelled';
100
+ }
101
+ if (updated.response) {
102
+ const parsed = JSON.parse(updated.response);
103
+ if (parsed.error) {
104
+ return `File upload failed: ${parsed.error}`;
105
+ }
106
+ const filePaths = parsed.filePaths || [];
107
+ if (filePaths.length === 0) {
108
+ return 'No files were uploaded (user may have cancelled or sent a new message)';
109
+ }
110
+ return `Files uploaded successfully:\n${filePaths.join('\n')}`;
111
+ }
112
+ }
113
+ return 'File upload timed out - user did not upload files within the time limit';
114
+ },
115
+ }),
116
+ otto_action_buttons: tool({
117
+ description: dedent `
118
+ Show action buttons in the current Discord thread for quick confirmations.
119
+ Use this when the user can respond by clicking one of up to 3 buttons.
120
+ Prefer a single button whenever possible.
121
+ Default color is white (same visual style as permission deny button).
122
+ If you need more than 3 options, use the question tool instead.
123
+ IMPORTANT: Always call this tool last in your message, after all text parts.
124
+
125
+ Examples:
126
+ - buttons: [{"label":"Yes, proceed"}]
127
+ - buttons: [{"label":"Approve","color":"green"}]
128
+ - buttons: [
129
+ {"label":"Confirm","color":"blue"},
130
+ {"label":"Cancel","color":"white"}
131
+ ]
132
+ `,
133
+ args: {
134
+ buttons: z
135
+ .array(z.object({
136
+ label: z
137
+ .string()
138
+ .min(1)
139
+ .max(80)
140
+ .describe('Button label shown to the user (1-80 chars)'),
141
+ color: z
142
+ .enum(['white', 'blue', 'green', 'red'])
143
+ .optional()
144
+ .describe('Optional button color. white is default and preferred for most confirmations.'),
145
+ }))
146
+ .min(1)
147
+ .max(3)
148
+ .describe('Array of 1-3 action buttons. Prefer one button whenever possible.'),
149
+ },
150
+ async execute({ buttons }, context) {
151
+ const { getPrisma, createIpcRequest, getIpcRequestById } = await loadDatabaseModule();
152
+ const prisma = await getPrisma();
153
+ const row = await prisma.thread_sessions.findFirst({
154
+ where: { session_id: context.sessionID },
155
+ select: { thread_id: true },
156
+ });
157
+ if (!row?.thread_id) {
158
+ return 'Could not find thread for current session';
159
+ }
160
+ const ipcRow = await createIpcRequest({
161
+ type: 'action_buttons',
162
+ sessionId: context.sessionID,
163
+ threadId: row.thread_id,
164
+ payload: JSON.stringify({
165
+ buttons,
166
+ directory: context.directory,
167
+ }),
168
+ });
169
+ const deadline = Date.now() + ACTION_BUTTON_TIMEOUT_MS;
170
+ const POLL_INTERVAL_MS = 200;
171
+ while (Date.now() < deadline) {
172
+ await new Promise((resolve) => {
173
+ setTimeout(resolve, POLL_INTERVAL_MS);
174
+ });
175
+ const updated = await getIpcRequestById({ id: ipcRow.id });
176
+ if (!updated || updated.status === 'cancelled') {
177
+ return 'Action button request was cancelled';
178
+ }
179
+ if (updated.response) {
180
+ const parsed = JSON.parse(updated.response);
181
+ if (parsed.error) {
182
+ return `Action button request failed: ${parsed.error}`;
183
+ }
184
+ return `Action button(s) shown: ${buttons.map((button) => button.label).join(', ')}`;
185
+ }
186
+ }
187
+ return 'Action button request timed out';
188
+ },
189
+ }),
190
+ },
191
+ };
192
+ };
193
+ export { ipcToolsPlugin };
@@ -0,0 +1,18 @@
1
+ import { getIpcRequestById } from './database.js';
2
+ /**
3
+ * Wait for an IPC request to reach "completed" status.
4
+ * Used by both the CLI send flow and the task-runner.
5
+ */
6
+ export async function waitForIpcRequestCompletion({ requestId, timeoutMs = 4_000, pollMs = 100, }) {
7
+ const startedAt = Date.now();
8
+ while (Date.now() - startedAt < timeoutMs) {
9
+ const row = await getIpcRequestById({ id: requestId });
10
+ if (row?.status === 'completed') {
11
+ return true;
12
+ }
13
+ await new Promise((resolve) => {
14
+ setTimeout(resolve, pollMs);
15
+ });
16
+ }
17
+ return new Error(`Timed out waiting for IPC request ${requestId} completion`);
18
+ }
@@ -0,0 +1,25 @@
1
+ // Limit heading depth for Discord.
2
+ // Discord only supports headings up to ### (h3), so this converts
3
+ // ####, #####, etc. to ### to maintain consistent rendering.
4
+ import { Lexer } from 'marked';
5
+ export function limitHeadingDepth(markdown, maxDepth = 3) {
6
+ const lexer = new Lexer();
7
+ const tokens = lexer.lex(markdown);
8
+ let result = '';
9
+ for (const token of tokens) {
10
+ if (token.type === 'heading') {
11
+ const heading = token;
12
+ if (heading.depth > maxDepth) {
13
+ const hashes = '#'.repeat(maxDepth);
14
+ result += hashes + ' ' + heading.text + '\n';
15
+ }
16
+ else {
17
+ result += token.raw;
18
+ }
19
+ }
20
+ else {
21
+ result += token.raw;
22
+ }
23
+ }
24
+ return result;
25
+ }
@@ -0,0 +1,105 @@
1
+ import { expect, test } from 'vitest';
2
+ import { limitHeadingDepth } from './limit-heading-depth.js';
3
+ test('converts h4 to h3', () => {
4
+ const input = '#### Fourth level heading';
5
+ const result = limitHeadingDepth(input);
6
+ expect(result).toMatchInlineSnapshot(`
7
+ "### Fourth level heading
8
+ "
9
+ `);
10
+ });
11
+ test('converts h5 to h3', () => {
12
+ const input = '##### Fifth level heading';
13
+ const result = limitHeadingDepth(input);
14
+ expect(result).toMatchInlineSnapshot(`
15
+ "### Fifth level heading
16
+ "
17
+ `);
18
+ });
19
+ test('converts h6 to h3', () => {
20
+ const input = '###### Sixth level heading';
21
+ const result = limitHeadingDepth(input);
22
+ expect(result).toMatchInlineSnapshot(`
23
+ "### Sixth level heading
24
+ "
25
+ `);
26
+ });
27
+ test('preserves h3 unchanged', () => {
28
+ const input = '### Third level heading';
29
+ const result = limitHeadingDepth(input);
30
+ expect(result).toMatchInlineSnapshot(`"### Third level heading"`);
31
+ });
32
+ test('preserves h2 unchanged', () => {
33
+ const input = '## Second level heading';
34
+ const result = limitHeadingDepth(input);
35
+ expect(result).toMatchInlineSnapshot(`"## Second level heading"`);
36
+ });
37
+ test('preserves h1 unchanged', () => {
38
+ const input = '# First level heading';
39
+ const result = limitHeadingDepth(input);
40
+ expect(result).toMatchInlineSnapshot(`"# First level heading"`);
41
+ });
42
+ test('handles multiple headings in document', () => {
43
+ const input = `# Title
44
+
45
+ Some text
46
+
47
+ ## Section
48
+
49
+ ### Subsection
50
+
51
+ #### Too deep
52
+
53
+ ##### Even deeper
54
+
55
+ Regular paragraph
56
+
57
+ ### Back to normal
58
+ `;
59
+ const result = limitHeadingDepth(input);
60
+ expect(result).toMatchInlineSnapshot(`
61
+ "# Title
62
+
63
+ Some text
64
+
65
+ ## Section
66
+
67
+ ### Subsection
68
+
69
+ ### Too deep
70
+ ### Even deeper
71
+ Regular paragraph
72
+
73
+ ### Back to normal
74
+ "
75
+ `);
76
+ });
77
+ test('preserves heading with inline formatting', () => {
78
+ const input = '#### Heading with **bold** and `code`';
79
+ const result = limitHeadingDepth(input);
80
+ expect(result).toMatchInlineSnapshot(`
81
+ "### Heading with **bold** and \`code\`
82
+ "
83
+ `);
84
+ });
85
+ test('handles empty markdown', () => {
86
+ const result = limitHeadingDepth('');
87
+ expect(result).toMatchInlineSnapshot(`""`);
88
+ });
89
+ test('handles markdown with no headings', () => {
90
+ const input = 'Just some text\n\nAnd more text';
91
+ const result = limitHeadingDepth(input);
92
+ expect(result).toMatchInlineSnapshot(`
93
+ "Just some text
94
+
95
+ And more text"
96
+ `);
97
+ });
98
+ test('allows custom maxDepth', () => {
99
+ const input = '### Third level';
100
+ const result = limitHeadingDepth(input, 2);
101
+ expect(result).toMatchInlineSnapshot(`
102
+ "## Third level
103
+ "
104
+ `);
105
+ });