@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
package/src/db.test.ts ADDED
@@ -0,0 +1,162 @@
1
+ // Tests for Prisma client initialization and schema migration.
2
+ // Auto-isolated via VITEST guards in config.ts (temp data dir) and db.ts (clears OTTO_DB_URL).
3
+
4
+ import { afterAll, describe, expect, test } from 'vitest'
5
+ import { getPrisma, closePrisma } from './db.js'
6
+ import {
7
+ appendSessionEventsSinceLastTimestamp,
8
+ createPendingWorktree,
9
+ getSessionEventSnapshot,
10
+ } from './database.js'
11
+
12
+ afterAll(async () => {
13
+ await closePrisma()
14
+ })
15
+
16
+ describe('getPrisma', () => {
17
+ test('creates sqlite file and migrates schema automatically', async () => {
18
+ const prisma = await getPrisma()
19
+
20
+ const session = await prisma.thread_sessions.create({
21
+ data: { thread_id: 'test-thread-123', session_id: 'test-session-456' },
22
+ })
23
+ expect(session.thread_id).toBe('test-thread-123')
24
+ expect(session.created_at).toBeInstanceOf(Date)
25
+
26
+ const found = await prisma.thread_sessions.findUnique({
27
+ where: { thread_id: session.thread_id },
28
+ })
29
+ expect(found?.session_id).toBe('test-session-456')
30
+
31
+ // Cleanup test data
32
+ await prisma.thread_sessions.delete({
33
+ where: { thread_id: 'test-thread-123' },
34
+ })
35
+ })
36
+
37
+ test('createPendingWorktree creates parent and child rows', async () => {
38
+ const prisma = await getPrisma()
39
+ const threadId = `test-worktree-${Date.now()}`
40
+
41
+ await createPendingWorktree({
42
+ threadId,
43
+ worktreeName: 'regression-worktree',
44
+ projectDirectory: '/tmp/regression-project',
45
+ })
46
+
47
+ const session = await prisma.thread_sessions.findUnique({
48
+ where: { thread_id: threadId },
49
+ })
50
+ expect(session).toBeTruthy()
51
+ expect(session?.session_id).toBe('')
52
+
53
+ const worktree = await prisma.thread_worktrees.findUnique({
54
+ where: { thread_id: threadId },
55
+ })
56
+ expect(worktree).toBeTruthy()
57
+ expect(worktree?.worktree_name).toBe('regression-worktree')
58
+ expect(worktree?.project_directory).toBe('/tmp/regression-project')
59
+ expect(worktree?.status).toBe('pending')
60
+
61
+ await prisma.thread_worktrees.delete({ where: { thread_id: threadId } })
62
+ await prisma.thread_sessions.delete({ where: { thread_id: threadId } })
63
+ })
64
+
65
+ test('session event persistence uses (timestamp, event_index) ordering for deterministic same-ms replay', async () => {
66
+ const prisma = await getPrisma()
67
+ const threadId = 'test-session-events-thread'
68
+ const sessionId = 'test-session-events-session'
69
+
70
+ await prisma.session_events.deleteMany({ where: { session_id: sessionId } })
71
+ await prisma.thread_sessions.deleteMany({ where: { thread_id: threadId } })
72
+
73
+ await prisma.thread_sessions.create({
74
+ data: { thread_id: threadId, session_id: sessionId },
75
+ })
76
+
77
+ const baseTimestamp = 1_700_000_000_000n
78
+
79
+ const inserted1 = await appendSessionEventsSinceLastTimestamp({
80
+ sessionId,
81
+ events: [
82
+ {
83
+ session_id: sessionId,
84
+ thread_id: threadId,
85
+ timestamp: baseTimestamp,
86
+ event_index: 2,
87
+ event_json: JSON.stringify({ id: 'e2' }),
88
+ },
89
+ {
90
+ session_id: sessionId,
91
+ thread_id: threadId,
92
+ timestamp: baseTimestamp,
93
+ event_index: 0,
94
+ event_json: JSON.stringify({ id: 'e0' }),
95
+ },
96
+ {
97
+ session_id: sessionId,
98
+ thread_id: threadId,
99
+ timestamp: baseTimestamp,
100
+ event_index: 1,
101
+ event_json: JSON.stringify({ id: 'e1' }),
102
+ },
103
+ ],
104
+ })
105
+
106
+ const inserted2 = await appendSessionEventsSinceLastTimestamp({
107
+ sessionId,
108
+ events: [
109
+ {
110
+ session_id: sessionId,
111
+ thread_id: threadId,
112
+ timestamp: baseTimestamp,
113
+ event_index: 0,
114
+ event_json: JSON.stringify({ id: 'e0' }),
115
+ },
116
+ {
117
+ session_id: sessionId,
118
+ thread_id: threadId,
119
+ timestamp: baseTimestamp,
120
+ event_index: 1,
121
+ event_json: JSON.stringify({ id: 'e1' }),
122
+ },
123
+ {
124
+ session_id: sessionId,
125
+ thread_id: threadId,
126
+ timestamp: baseTimestamp,
127
+ event_index: 2,
128
+ event_json: JSON.stringify({ id: 'e2' }),
129
+ },
130
+ {
131
+ session_id: sessionId,
132
+ thread_id: threadId,
133
+ timestamp: baseTimestamp,
134
+ event_index: 3,
135
+ event_json: JSON.stringify({ id: 'e3' }),
136
+ },
137
+ ],
138
+ })
139
+
140
+ const rows = await getSessionEventSnapshot({ sessionId })
141
+ const orderedIds = rows.map((row) => {
142
+ const parsed = JSON.parse(row.event_json) as { id: string }
143
+ return parsed.id
144
+ })
145
+
146
+ expect({ inserted1, inserted2, orderedIds }).toMatchInlineSnapshot(`
147
+ {
148
+ "inserted1": 3,
149
+ "inserted2": 1,
150
+ "orderedIds": [
151
+ "e0",
152
+ "e1",
153
+ "e2",
154
+ "e3",
155
+ ],
156
+ }
157
+ `)
158
+
159
+ await prisma.session_events.deleteMany({ where: { session_id: sessionId } })
160
+ await prisma.thread_sessions.deleteMany({ where: { thread_id: threadId } })
161
+ })
162
+ })
package/src/db.ts ADDED
@@ -0,0 +1,295 @@
1
+ // Prisma client initialization with libsql adapter.
2
+ // Uses OTTO_DB_URL env var when set (plugin process → Hrana HTTP),
3
+ // otherwise falls back to direct file: access (bot process, CLI subcommands).
4
+
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+ import crypto from 'node:crypto'
8
+ import Database from 'libsql'
9
+ import { PrismaLibSql } from '@prisma/adapter-libsql'
10
+ import { PrismaClient, Prisma } from './generated/client.js'
11
+ import { getDataDir } from './config.js'
12
+ import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js'
13
+ import { fileURLToPath } from 'node:url'
14
+
15
+ const __filename = fileURLToPath(import.meta.url)
16
+ const __dirname = path.dirname(__filename)
17
+
18
+ export type { Prisma }
19
+ export { PrismaClient }
20
+
21
+ // Under vitest, clear any inherited OTTO_DB_URL from the parent bot process
22
+ // so tests default to file-based access using the auto-isolated temp data dir.
23
+ // Tests that need Hrana (like the e2e test) can set OTTO_DB_URL explicitly
24
+ // after import — getDbUrl() reads process.env dynamically on each call.
25
+ if (process.env.OTTO_VITEST) {
26
+ delete process.env['OTTO_DB_URL']
27
+ }
28
+
29
+ const dbLogger = createLogger(LogPrefix.DB)
30
+
31
+ let prismaInstance: PrismaClient | null = null
32
+ let initPromise: Promise<PrismaClient> | null = null
33
+
34
+ /**
35
+ * Get the singleton Prisma client instance.
36
+ * Initializes the database on first call, running schema setup if needed.
37
+ */
38
+ export function getPrisma(): Promise<PrismaClient> {
39
+ if (prismaInstance) {
40
+ return Promise.resolve(prismaInstance)
41
+ }
42
+ if (initPromise) {
43
+ return initPromise
44
+ }
45
+ initPromise = initializePrisma()
46
+ return initPromise
47
+ }
48
+
49
+ /**
50
+ * Build the libsql connection URL.
51
+ * OTTO_DB_URL is set by the bot when spawning opencode plugin processes,
52
+ * pointing them at the in-process Hrana HTTP server. Future-proof for remote
53
+ * opencode processes on different machines.
54
+ * Without the env var (bot process, CLI subcommands), uses direct file: access.
55
+ */
56
+ function getDbUrl(): string {
57
+ if (process.env.OTTO_DB_URL) {
58
+ return process.env.OTTO_DB_URL
59
+ }
60
+ const dataDir = getDataDir()
61
+ const dbPath = path.join(dataDir, 'discord-sessions.db')
62
+ return `file:${dbPath}`
63
+ }
64
+
65
+ function getDbAuthToken(): string | undefined {
66
+ const token = process.env.OTTO_DB_AUTH_TOKEN
67
+ if (!token) {
68
+ return undefined
69
+ }
70
+ return token
71
+ }
72
+
73
+ async function initializePrisma(): Promise<PrismaClient> {
74
+ const dbUrl = getDbUrl()
75
+ const isFileMode = dbUrl.startsWith('file:')
76
+
77
+ if (isFileMode) {
78
+ const dataDir = getDataDir()
79
+ try {
80
+ fs.mkdirSync(dataDir, { recursive: true })
81
+ } catch (e) {
82
+ dbLogger.error(
83
+ `Failed to create data directory ${dataDir}:`,
84
+ (e as Error).message,
85
+ )
86
+ }
87
+ }
88
+
89
+ dbLogger.log(`Opening database via: ${dbUrl}`)
90
+
91
+ const dbAuthToken = getDbAuthToken()
92
+ const adapter = new PrismaLibSql({
93
+ url: dbUrl,
94
+ ...(dbAuthToken && { authToken: dbAuthToken }),
95
+ })
96
+ const prisma = new PrismaClient({ adapter })
97
+
98
+ try {
99
+ if (isFileMode) {
100
+ // WAL mode allows concurrent reads while writing instead of blocking.
101
+ // busy_timeout makes SQLite retry for 5s instead of immediately failing with SQLITE_BUSY.
102
+ // The Hrana server (serving the plugin process) sets the same pragmas on its own connection.
103
+ // PRAGMAs are skipped for HTTP connections — they're connection-scoped and the Hrana
104
+ // server already configures them on its own libsql Database handle.
105
+ await prisma.$executeRawUnsafe('PRAGMA journal_mode = WAL')
106
+ await prisma.$executeRawUnsafe('PRAGMA busy_timeout = 5000')
107
+ }
108
+
109
+ // Always run migrations - schema.sql uses IF NOT EXISTS so it's idempotent
110
+ dbLogger.log('Running schema migrations...')
111
+ await migrateSchema(prisma)
112
+ dbLogger.log('Schema migration complete')
113
+ } catch (error) {
114
+ dbLogger.error('Prisma init failed:', formatErrorWithStack(error))
115
+ throw error
116
+ }
117
+
118
+ prismaInstance = prisma
119
+ return prisma
120
+ }
121
+
122
+ async function migrateSchema(prisma: PrismaClient): Promise<void> {
123
+
124
+ // Helper: ALTER TABLE ADD COLUMN that bypasses Prisma's libsql adapter.
125
+ // Prisma wraps $executeRawUnsafe in implicit transactions; when a DDL
126
+ // statement fails (e.g. "duplicate column name") the transaction is
127
+ // marked as failed and subsequent statements in the same implicit
128
+ // transaction also fail — even if they would succeed independently.
129
+ // Direct libsql execution avoids this problem.
130
+ const alterTableAddColumn = (table: string, column: string, extra?: string) => {
131
+ const dbPath = (() => {
132
+ const url = getDbUrl()
133
+ if (url.startsWith('file:')) {
134
+ return url.slice('file:'.length)
135
+ }
136
+ return null
137
+ })()
138
+ if (!dbPath) {
139
+ // HTTP mode (Hrana) — fall back to Prisma and hope for the best
140
+ const sql = `ALTER TABLE ${table} ADD COLUMN ${column}${extra ? ` ${extra}` : ''}`
141
+ return prisma.$executeRawUnsafe(sql).catch(() => {})
142
+ }
143
+ const db = new Database(dbPath)
144
+ try {
145
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all()
146
+ const exists = (cols as Array<{ name: string }>).some((c) => {
147
+ return c.name === column
148
+ })
149
+ if (exists) {
150
+ return
151
+ }
152
+ const sql = `ALTER TABLE ${table} ADD COLUMN ${column}${extra ? ` ${extra}` : ''}`
153
+ db.exec(sql)
154
+ dbLogger.log(`[MIGRATION] ${sql}`)
155
+ } catch (e) {
156
+ const msg = e instanceof Error ? e.message : String(e)
157
+ dbLogger.warn(`[MIGRATION] ALTER TABLE ${table} ADD COLUMN ${column} failed: ${msg}`)
158
+ } finally {
159
+ db.close()
160
+ }
161
+ }
162
+
163
+ const schemaPath = path.join(__dirname, '../src/schema.sql')
164
+ const sql = fs.readFileSync(schemaPath, 'utf-8')
165
+ const statements = sql
166
+ .split(';')
167
+ .map((s) =>
168
+ s
169
+ .split('\n')
170
+ .filter((line) => !line.trimStart().startsWith('--'))
171
+ .join('\n')
172
+ .trim(),
173
+ )
174
+ .filter(
175
+ (s) =>
176
+ s.length > 0 &&
177
+ !/^CREATE\s+TABLE\s+["']?sqlite_sequence["']?\s*\(/i.test(s),
178
+ )
179
+ // Make CREATE INDEX idempotent
180
+ .map((s) =>
181
+ s
182
+ .replace(
183
+ /^CREATE\s+UNIQUE\s+INDEX\b(?!\s+IF)/i,
184
+ 'CREATE UNIQUE INDEX IF NOT EXISTS',
185
+ )
186
+ .replace(/^CREATE\s+INDEX\b(?!\s+IF)/i, 'CREATE INDEX IF NOT EXISTS'),
187
+ )
188
+ for (const statement of statements) {
189
+ await prisma.$executeRawUnsafe(statement)
190
+ }
191
+
192
+ // Migration: add variant column to model tables (for thinking/reasoning level).
193
+ alterTableAddColumn('channel_models', 'variant', 'TEXT')
194
+ alterTableAddColumn('session_models', 'variant', 'TEXT')
195
+ alterTableAddColumn('global_models', 'variant', 'TEXT')
196
+
197
+ // Migration: add openai_api_key column to bot_api_keys.
198
+ alterTableAddColumn('bot_api_keys', 'openai_api_key', 'TEXT')
199
+
200
+ // Migration: add gateway bot mode columns to bot_tokens.
201
+ alterTableAddColumn('bot_tokens', 'bot_mode', "TEXT DEFAULT 'self_hosted'")
202
+ alterTableAddColumn('bot_tokens', 'thread_deletion_sync_mode', 'TEXT')
203
+ alterTableAddColumn('bot_tokens', 'client_id', 'TEXT')
204
+ alterTableAddColumn('bot_tokens', 'client_secret', 'TEXT')
205
+ alterTableAddColumn('bot_tokens', 'proxy_url', 'TEXT')
206
+ alterTableAddColumn('bot_tokens', 'last_used_at', 'DATETIME')
207
+
208
+ alterTableAddColumn('thread_sessions', 'source', "TEXT DEFAULT 'otto'")
209
+
210
+ // Migration: move session_thinking data into session_models.variant.
211
+ // session_thinking table is left in place (not dropped) so older otto versions
212
+ // that still reference it won't crash on the same database.
213
+ try {
214
+ // For sessions that already have a model row, copy the thinking value
215
+ await prisma.$executeRawUnsafe(`
216
+ UPDATE session_models SET variant = (
217
+ SELECT thinking_value FROM session_thinking
218
+ WHERE session_thinking.session_id = session_models.session_id
219
+ ) WHERE variant IS NULL AND EXISTS (
220
+ SELECT 1 FROM session_thinking WHERE session_thinking.session_id = session_models.session_id
221
+ )
222
+ `)
223
+ } catch {
224
+ // session_thinking table may not exist in fresh installs
225
+ }
226
+
227
+ // Migration: rename hyphenated verbosity values to underscored for Prisma enum.
228
+ // Old DBs have 'tools-and-text', 'text-and-essential-tools', 'text-only'.
229
+ const verbosityRenames = [
230
+ "UPDATE channel_verbosity SET verbosity = 'tools_and_text' WHERE verbosity = 'tools-and-text'",
231
+ "UPDATE channel_verbosity SET verbosity = 'text_and_essential_tools' WHERE verbosity = 'text-and-essential-tools'",
232
+ "UPDATE channel_verbosity SET verbosity = 'text_only' WHERE verbosity = 'text-only'",
233
+ ]
234
+ for (const stmt of verbosityRenames) {
235
+ try {
236
+ await prisma.$executeRawUnsafe(stmt)
237
+ } catch {
238
+ // Table may not exist on first run
239
+ }
240
+ }
241
+
242
+ // Defensive migration: rename legacy 'self-hosted' bot_mode to 'self_hosted'.
243
+ // Also fix NULL worktree status rows that predate the required enum.
244
+ const defensiveMigrations = [
245
+ "UPDATE bot_tokens SET bot_mode = 'self_hosted' WHERE bot_mode = 'self-hosted'",
246
+ "UPDATE bot_tokens SET proxy_url = REPLACE(proxy_url, 'discord-gateway.otto.xyz', 'discord-gateway.otto.dev') WHERE bot_mode = 'gateway' AND proxy_url LIKE '%discord-gateway.otto.xyz%'",
247
+ "UPDATE thread_worktrees SET status = 'pending' WHERE status IS NULL",
248
+ ]
249
+ for (const stmt of defensiveMigrations) {
250
+ try {
251
+ await prisma.$executeRawUnsafe(stmt)
252
+ } catch {
253
+ // Table may not exist on first run
254
+ }
255
+ }
256
+
257
+ // Migration: ensure every bot row has service auth credentials.
258
+ // These credentials are used for local/internet control-plane auth.
259
+ try {
260
+ const botRows = await prisma.bot_tokens.findMany({
261
+ select: {
262
+ app_id: true,
263
+ client_id: true,
264
+ client_secret: true,
265
+ },
266
+ })
267
+ for (const botRow of botRows) {
268
+ if (botRow.client_id && botRow.client_secret) {
269
+ continue
270
+ }
271
+ await prisma.bot_tokens.update({
272
+ where: { app_id: botRow.app_id },
273
+ data: {
274
+ client_id: crypto.randomUUID(),
275
+ client_secret: crypto.randomBytes(32).toString('hex'),
276
+ },
277
+ })
278
+ }
279
+ } catch {
280
+ // Defensive migration only; ignore if table shape is not ready yet.
281
+ }
282
+
283
+ }
284
+
285
+ /**
286
+ * Close the Prisma connection.
287
+ */
288
+ export async function closePrisma(): Promise<void> {
289
+ if (prismaInstance) {
290
+ await prismaInstance.$disconnect()
291
+ prismaInstance = null
292
+ initPromise = null
293
+ dbLogger.log('Prisma connection closed')
294
+ }
295
+ }
@@ -0,0 +1,43 @@
1
+ // Reusable debounce helper for timeout-based callbacks.
2
+ // Encapsulates the timer handle and exposes trigger/clear/isPending so callers
3
+ // can batch clustered events without leaking timeout state into domain logic.
4
+
5
+ export function createDebouncedTimeout({
6
+ delayMs,
7
+ callback,
8
+ }: {
9
+ delayMs: number
10
+ callback: () => void
11
+ }): {
12
+ trigger: () => void
13
+ clear: () => void
14
+ isPending: () => boolean
15
+ } {
16
+ let timeout: ReturnType<typeof setTimeout> | null = null
17
+
18
+ function clear(): void {
19
+ if (!timeout) {
20
+ return
21
+ }
22
+ clearTimeout(timeout)
23
+ timeout = null
24
+ }
25
+
26
+ function trigger(): void {
27
+ clear()
28
+ timeout = setTimeout(() => {
29
+ timeout = null
30
+ callback()
31
+ }, delayMs)
32
+ }
33
+
34
+ function isPending(): boolean {
35
+ return timeout !== null
36
+ }
37
+
38
+ return {
39
+ trigger,
40
+ clear,
41
+ isPending,
42
+ }
43
+ }
@@ -0,0 +1,104 @@
1
+ // Debounced async callback with centralized shutdown flushing.
2
+ // Used for persistence paths that should batch writes during runtime
3
+ // while allowing the bot's single SIGTERM/SIGINT handler to flush all callbacks.
4
+
5
+ type FlushCallback = () => Promise<void>
6
+
7
+ const processFlushCallbacks = new Set<FlushCallback>()
8
+
9
+ export async function flushDebouncedProcessCallbacks(): Promise<void> {
10
+ const callbacks = [...processFlushCallbacks]
11
+ await Promise.allSettled(
12
+ callbacks.map((callback) => {
13
+ return callback()
14
+ }),
15
+ )
16
+ }
17
+
18
+ export function createDebouncedProcessFlush({
19
+ waitMs,
20
+ callback,
21
+ onError,
22
+ }: {
23
+ waitMs: number
24
+ callback: () => Promise<void>
25
+ onError?: (error: Error) => void
26
+ }): {
27
+ trigger: () => void
28
+ flush: () => Promise<void>
29
+ dispose: () => Promise<void>
30
+ } {
31
+ let timeout: ReturnType<typeof setTimeout> | undefined
32
+ let inFlight: Promise<void> | undefined
33
+ let dirty = false
34
+
35
+ async function run(): Promise<void> {
36
+ if (!dirty) {
37
+ return
38
+ }
39
+ if (inFlight) {
40
+ await inFlight
41
+ if (!dirty) {
42
+ return
43
+ }
44
+ }
45
+
46
+ dirty = false
47
+ const current = Promise.resolve()
48
+ .then(() => {
49
+ return callback()
50
+ })
51
+ .catch((error) => {
52
+ if (onError) {
53
+ const wrappedError =
54
+ error instanceof Error
55
+ ? error
56
+ : new Error('Debounced process flush failed', { cause: error })
57
+ onError(wrappedError)
58
+ }
59
+ })
60
+ inFlight = current
61
+ await current
62
+ if (inFlight === current) {
63
+ inFlight = undefined
64
+ }
65
+ if (dirty) {
66
+ await run()
67
+ }
68
+ }
69
+
70
+ function trigger(): void {
71
+ dirty = true
72
+ if (timeout) {
73
+ return
74
+ }
75
+ timeout = setTimeout(() => {
76
+ timeout = undefined
77
+ void run()
78
+ }, waitMs)
79
+ }
80
+
81
+ async function flush(): Promise<void> {
82
+ if (timeout) {
83
+ clearTimeout(timeout)
84
+ timeout = undefined
85
+ }
86
+ await run()
87
+ }
88
+
89
+ const processFlushCallback: FlushCallback = async () => {
90
+ await flush()
91
+ }
92
+ processFlushCallbacks.add(processFlushCallback)
93
+
94
+ async function dispose(): Promise<void> {
95
+ processFlushCallbacks.delete(processFlushCallback)
96
+ await flush()
97
+ }
98
+
99
+ return {
100
+ trigger,
101
+ flush,
102
+ dispose,
103
+ }
104
+ }