@otto-assistant/otto 0.1.1 → 0.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (637) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-account-identity.js +62 -0
  7. package/dist/anthropic-account-identity.test.js +38 -0
  8. package/dist/anthropic-auth-plugin.js +917 -0
  9. package/dist/anthropic-auth-state.js +303 -0
  10. package/dist/anthropic-auth-state.test.js +150 -0
  11. package/dist/bin.js +152 -0
  12. package/dist/btw-prefix-detection.js +17 -0
  13. package/dist/btw-prefix-detection.test.js +63 -0
  14. package/dist/channel-management.js +259 -0
  15. package/dist/cli-parsing.test.js +142 -0
  16. package/dist/cli-send-thread.e2e.test.js +353 -0
  17. package/dist/cli-telegram-options.test.js +99 -0
  18. package/dist/cli.js +4210 -568
  19. package/dist/commands/abort.js +65 -0
  20. package/dist/commands/action-buttons.js +245 -0
  21. package/dist/commands/add-dir.js +124 -0
  22. package/dist/commands/add-dir.test.js +126 -0
  23. package/dist/commands/add-project.js +113 -0
  24. package/dist/commands/agent.js +355 -0
  25. package/dist/commands/ask-question.js +320 -0
  26. package/dist/commands/ask-question.test.js +92 -0
  27. package/dist/commands/btw.js +121 -0
  28. package/dist/commands/cli-commands-group-a.test.js +728 -0
  29. package/dist/commands/cli-commands-group-b.test.js +695 -0
  30. package/dist/commands/compact.js +120 -0
  31. package/dist/commands/context-usage.js +140 -0
  32. package/dist/commands/create-new-project.js +130 -0
  33. package/dist/commands/diff.js +63 -0
  34. package/dist/commands/discord-commands-group-a.test.js +621 -0
  35. package/dist/commands/discord-commands-group-b.test.js +595 -0
  36. package/dist/commands/discord-commands-group-c.test.js +739 -0
  37. package/dist/commands/file-upload.js +275 -0
  38. package/dist/commands/fork-subagent.js +177 -0
  39. package/dist/commands/fork.js +262 -0
  40. package/dist/commands/gemini-apikey.js +70 -0
  41. package/dist/commands/login.js +887 -0
  42. package/dist/commands/mcp.js +239 -0
  43. package/dist/commands/memory-snapshot.js +24 -0
  44. package/dist/commands/mention-mode.js +44 -0
  45. package/dist/commands/merge-worktree.js +162 -0
  46. package/dist/commands/model-variant.js +366 -0
  47. package/dist/commands/model.js +794 -0
  48. package/dist/commands/new-worktree.js +465 -0
  49. package/dist/commands/paginated-select.js +57 -0
  50. package/dist/commands/permissions.js +274 -0
  51. package/dist/commands/queue.js +223 -0
  52. package/dist/commands/remove-project.js +115 -0
  53. package/dist/commands/restart-opencode-server.js +127 -0
  54. package/dist/commands/resume.js +149 -0
  55. package/dist/commands/run-command.js +79 -0
  56. package/dist/commands/screenshare.js +303 -0
  57. package/dist/commands/screenshare.test.js +20 -0
  58. package/dist/commands/session-id.js +78 -0
  59. package/dist/commands/session.js +176 -0
  60. package/dist/commands/share.js +80 -0
  61. package/dist/commands/tasks.js +205 -0
  62. package/dist/commands/thread-deletion-sync.js +50 -0
  63. package/dist/commands/types.js +2 -0
  64. package/dist/commands/undo-redo.js +305 -0
  65. package/dist/commands/unset-model.js +139 -0
  66. package/dist/commands/upgrade.js +48 -0
  67. package/dist/commands/user-command.js +155 -0
  68. package/dist/commands/verbosity.js +125 -0
  69. package/dist/commands/vscode.js +269 -0
  70. package/dist/commands/worktree-settings.js +43 -0
  71. package/dist/commands/worktrees.js +468 -0
  72. package/dist/condense-memory.js +33 -0
  73. package/dist/config.js +100 -255
  74. package/dist/context-awareness-plugin.js +340 -0
  75. package/dist/context-awareness-plugin.test.js +126 -0
  76. package/dist/critique-utils.js +95 -0
  77. package/dist/database.js +1355 -0
  78. package/dist/db.js +260 -0
  79. package/dist/db.test.js +138 -0
  80. package/dist/debounce-timeout.js +28 -0
  81. package/dist/debounced-process-flush.js +77 -0
  82. package/dist/discord-bot.js +1124 -0
  83. package/dist/discord-command-registration.js +567 -0
  84. package/dist/discord-urls.js +82 -0
  85. package/dist/discord-utils.js +616 -0
  86. package/dist/discord-utils.test.js +134 -0
  87. package/dist/errors.js +157 -0
  88. package/dist/escape-backticks.test.js +429 -0
  89. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  90. package/dist/eventsource-parser.test.js +327 -0
  91. package/dist/exec-async.js +26 -0
  92. package/dist/external-opencode-sync.js +480 -0
  93. package/dist/format-tables.js +491 -0
  94. package/dist/format-tables.test.js +478 -0
  95. package/dist/forum-sync/config.js +79 -0
  96. package/dist/forum-sync/discord-operations.js +154 -0
  97. package/dist/forum-sync/index.js +5 -0
  98. package/dist/forum-sync/markdown.js +113 -0
  99. package/dist/forum-sync/sync-to-discord.js +417 -0
  100. package/dist/forum-sync/sync-to-files.js +190 -0
  101. package/dist/forum-sync/types.js +53 -0
  102. package/dist/forum-sync/watchers.js +307 -0
  103. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  104. package/dist/gateway-proxy.e2e.test.js +485 -0
  105. package/dist/genai-worker-wrapper.js +111 -0
  106. package/dist/genai-worker.js +311 -0
  107. package/dist/genai.js +232 -0
  108. package/dist/generated/browser.js +17 -0
  109. package/dist/generated/client.js +37 -0
  110. package/dist/generated/commonInputTypes.js +10 -0
  111. package/dist/generated/enums.js +58 -0
  112. package/dist/generated/internal/class.js +49 -0
  113. package/dist/generated/internal/prismaNamespace.js +254 -0
  114. package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
  115. package/dist/generated/models/bot_api_keys.js +1 -0
  116. package/dist/generated/models/bot_tokens.js +1 -0
  117. package/dist/generated/models/channel_agents.js +1 -0
  118. package/dist/generated/models/channel_directories.js +1 -0
  119. package/dist/generated/models/channel_mention_mode.js +1 -0
  120. package/dist/generated/models/channel_models.js +1 -0
  121. package/dist/generated/models/channel_verbosity.js +1 -0
  122. package/dist/generated/models/channel_worktrees.js +1 -0
  123. package/dist/generated/models/forum_sync_configs.js +1 -0
  124. package/dist/generated/models/global_models.js +1 -0
  125. package/dist/generated/models/ipc_requests.js +1 -0
  126. package/dist/generated/models/part_messages.js +1 -0
  127. package/dist/generated/models/scheduled_tasks.js +1 -0
  128. package/dist/generated/models/session_agents.js +1 -0
  129. package/dist/generated/models/session_events.js +1 -0
  130. package/dist/generated/models/session_models.js +1 -0
  131. package/dist/generated/models/session_start_sources.js +1 -0
  132. package/dist/generated/models/thread_sessions.js +1 -0
  133. package/dist/generated/models/thread_worktrees.js +1 -0
  134. package/dist/generated/models.js +1 -0
  135. package/dist/heap-monitor.js +122 -0
  136. package/dist/hrana-server.js +251 -0
  137. package/dist/hrana-server.test.js +370 -0
  138. package/dist/html-actions.js +123 -0
  139. package/dist/html-actions.test.js +70 -0
  140. package/dist/html-components.js +117 -0
  141. package/dist/html-components.test.js +34 -0
  142. package/dist/image-optimizer-plugin.js +153 -0
  143. package/dist/image-utils.js +112 -0
  144. package/dist/interaction-handler.js +420 -0
  145. package/dist/ipc-polling.js +327 -0
  146. package/dist/ipc-tools-plugin.js +193 -0
  147. package/dist/ipc-utils.js +18 -0
  148. package/dist/limit-heading-depth.js +25 -0
  149. package/dist/limit-heading-depth.test.js +105 -0
  150. package/dist/logger.js +171 -0
  151. package/dist/markdown.js +342 -0
  152. package/dist/markdown.test.js +264 -0
  153. package/dist/memory-overview-plugin.js +128 -0
  154. package/dist/message-finish-field.e2e.test.js +168 -0
  155. package/dist/message-formatting.js +415 -0
  156. package/dist/message-formatting.test.js +115 -0
  157. package/dist/message-preprocessing.js +359 -0
  158. package/dist/onboarding-tutorial.js +163 -0
  159. package/dist/onboarding-welcome.js +37 -0
  160. package/dist/openai-realtime.js +224 -0
  161. package/dist/opencode-command-detection.js +65 -0
  162. package/dist/opencode-command-detection.test.js +240 -0
  163. package/dist/opencode-command.js +131 -0
  164. package/dist/opencode-command.test.js +48 -0
  165. package/dist/opencode-interrupt-plugin.js +388 -0
  166. package/dist/opencode-interrupt-plugin.test.js +463 -0
  167. package/dist/opencode.js +1117 -0
  168. package/dist/otto/branding.js +22 -0
  169. package/dist/otto/index.js +21 -0
  170. package/dist/otto-digital-twin.e2e.test.js +161 -0
  171. package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
  172. package/dist/otto-opencode-plugin.js +21 -0
  173. package/dist/otto-opencode-plugin.test.js +98 -0
  174. package/dist/parse-permission-rules.test.js +117 -0
  175. package/dist/patch-text-parser.js +97 -0
  176. package/dist/plugin-logger.js +68 -0
  177. package/dist/privacy-sanitizer.js +105 -0
  178. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  179. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  180. package/dist/queue-advanced-e2e-setup.js +790 -0
  181. package/dist/queue-advanced-footer.e2e.test.js +481 -0
  182. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  183. package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
  184. package/dist/queue-advanced-question.e2e.test.js +261 -0
  185. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  186. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  187. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  188. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  189. package/dist/queue-question-select-drain.e2e.test.js +256 -0
  190. package/dist/runtime-idle-sweeper.js +52 -0
  191. package/dist/runtime-lifecycle.e2e.test.js +514 -0
  192. package/dist/sentry.js +23 -0
  193. package/dist/session-handler/agent-utils.js +67 -0
  194. package/dist/session-handler/event-stream-state.js +475 -0
  195. package/dist/session-handler/event-stream-state.test.js +632 -0
  196. package/dist/session-handler/model-utils.js +147 -0
  197. package/dist/session-handler/opencode-session-event-log.js +94 -0
  198. package/dist/session-handler/thread-runtime-state.js +131 -0
  199. package/dist/session-handler/thread-session-runtime.js +3390 -0
  200. package/dist/session-handler.js +9 -0
  201. package/dist/session-search.js +100 -0
  202. package/dist/session-search.test.js +40 -0
  203. package/dist/session-title-rename.test.js +92 -0
  204. package/dist/skill-filter.js +31 -0
  205. package/dist/skill-filter.test.js +65 -0
  206. package/dist/startup-service.js +153 -0
  207. package/dist/startup-time.e2e.test.js +296 -0
  208. package/dist/store.js +19 -0
  209. package/dist/subagent-rate-limit-plugin.js +175 -0
  210. package/dist/system-message.js +702 -0
  211. package/dist/system-message.test.js +697 -0
  212. package/dist/task-runner.js +530 -0
  213. package/dist/task-schedule.js +213 -0
  214. package/dist/task-schedule.test.js +71 -0
  215. package/dist/test-utils.js +313 -0
  216. package/dist/thinking-utils.js +35 -0
  217. package/dist/thread-message-queue.e2e.test.js +1111 -0
  218. package/dist/tools.js +357 -0
  219. package/dist/undo-redo.e2e.test.js +161 -0
  220. package/dist/unnest-code-blocks.js +146 -0
  221. package/dist/unnest-code-blocks.test.js +673 -0
  222. package/dist/upgrade.js +156 -0
  223. package/dist/utils.js +172 -0
  224. package/dist/utils.test.js +130 -0
  225. package/dist/voice-attachment.js +34 -0
  226. package/dist/voice-handler.js +646 -0
  227. package/dist/voice-message.e2e.test.js +1021 -0
  228. package/dist/voice.js +456 -0
  229. package/dist/voice.test.js +235 -0
  230. package/dist/wait-session.js +171 -0
  231. package/dist/websockify.js +69 -0
  232. package/dist/worker-types.js +4 -0
  233. package/dist/worktree-lifecycle.e2e.test.js +311 -0
  234. package/dist/worktree-utils.js +3 -0
  235. package/dist/worktrees.js +991 -0
  236. package/dist/worktrees.test.js +415 -0
  237. package/dist/xml.js +92 -0
  238. package/dist/xml.test.js +32 -0
  239. package/package.json +90 -38
  240. package/schema.prisma +303 -0
  241. package/skills/batch/SKILL.md +87 -0
  242. package/skills/critique/SKILL.md +112 -0
  243. package/skills/egaki/SKILL.md +100 -0
  244. package/skills/errore/SKILL.md +647 -0
  245. package/skills/event-sourcing-state/SKILL.md +252 -0
  246. package/skills/goke/SKILL.md +38 -0
  247. package/skills/jitter/EDITOR.md +219 -0
  248. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  249. package/skills/jitter/SKILL.md +158 -0
  250. package/skills/jitter/jitter-clipboard.json +1042 -0
  251. package/skills/jitter/package.json +14 -0
  252. package/skills/jitter/tsconfig.json +15 -0
  253. package/skills/jitter/utils/actions.ts +212 -0
  254. package/skills/jitter/utils/export.ts +114 -0
  255. package/skills/jitter/utils/index.ts +141 -0
  256. package/skills/jitter/utils/snapshot.ts +154 -0
  257. package/skills/jitter/utils/traverse.ts +246 -0
  258. package/skills/jitter/utils/types.ts +279 -0
  259. package/skills/jitter/utils/wait.ts +133 -0
  260. package/skills/lintcn/SKILL.md +873 -0
  261. package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
  262. package/skills/new-skill/SKILL.md +237 -0
  263. package/skills/npm-package/SKILL.md +617 -0
  264. package/skills/opensrc/SKILL.md +78 -0
  265. package/skills/otto-publish/SKILL.md +61 -0
  266. package/skills/playwriter/SKILL.md +35 -0
  267. package/skills/profano/SKILL.md +16 -0
  268. package/skills/proxyman/SKILL.md +215 -0
  269. package/skills/security-review/SKILL.md +208 -0
  270. package/skills/sigillo/SKILL.md +101 -0
  271. package/skills/simplify/SKILL.md +58 -0
  272. package/skills/spiceflow/SKILL.md +28 -0
  273. package/skills/termcast/SKILL.md +945 -0
  274. package/skills/tuistory/SKILL.md +98 -0
  275. package/skills/usecomputer/SKILL.md +264 -0
  276. package/skills/x-articles/SKILL.md +554 -0
  277. package/skills/zele/SKILL.md +49 -0
  278. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  279. package/src/agent-model.e2e.test.ts +979 -0
  280. package/src/ai-tool-to-genai.test.ts +296 -0
  281. package/src/ai-tool-to-genai.ts +283 -0
  282. package/src/ai-tool.ts +39 -0
  283. package/src/anthropic-account-identity.test.ts +52 -0
  284. package/src/anthropic-account-identity.ts +77 -0
  285. package/src/anthropic-auth-plugin.ts +1139 -0
  286. package/src/anthropic-auth-state.test.ts +187 -0
  287. package/src/anthropic-auth-state.ts +386 -0
  288. package/src/bin.ts +182 -0
  289. package/src/btw-prefix-detection.test.ts +73 -0
  290. package/src/btw-prefix-detection.ts +23 -0
  291. package/src/channel-management.ts +376 -0
  292. package/src/cli-parsing.test.ts +197 -0
  293. package/src/cli-send-thread.e2e.test.ts +463 -0
  294. package/src/cli-telegram-options.test.ts +114 -0
  295. package/src/cli.ts +5718 -580
  296. package/src/commands/abort.ts +89 -0
  297. package/src/commands/action-buttons.ts +364 -0
  298. package/src/commands/add-dir.test.ts +154 -0
  299. package/src/commands/add-dir.ts +175 -0
  300. package/src/commands/add-project.ts +149 -0
  301. package/src/commands/agent.ts +496 -0
  302. package/src/commands/ask-question.test.ts +111 -0
  303. package/src/commands/ask-question.ts +455 -0
  304. package/src/commands/btw.ts +184 -0
  305. package/src/commands/cli-commands-group-a.test.ts +837 -0
  306. package/src/commands/cli-commands-group-b.test.ts +800 -0
  307. package/src/commands/compact.ts +157 -0
  308. package/src/commands/context-usage.ts +199 -0
  309. package/src/commands/create-new-project.ts +190 -0
  310. package/src/commands/diff.ts +91 -0
  311. package/src/commands/discord-commands-group-a.test.ts +751 -0
  312. package/src/commands/discord-commands-group-b.test.ts +648 -0
  313. package/src/commands/discord-commands-group-c.test.ts +882 -0
  314. package/src/commands/file-upload.ts +389 -0
  315. package/src/commands/fork-subagent.ts +263 -0
  316. package/src/commands/fork.ts +386 -0
  317. package/src/commands/gemini-apikey.ts +104 -0
  318. package/src/commands/login.ts +1175 -0
  319. package/src/commands/mcp.ts +307 -0
  320. package/src/commands/memory-snapshot.ts +30 -0
  321. package/src/commands/mention-mode.ts +68 -0
  322. package/src/commands/merge-worktree.ts +226 -0
  323. package/src/commands/model-variant.ts +485 -0
  324. package/src/commands/model.ts +1078 -0
  325. package/src/commands/new-worktree.ts +645 -0
  326. package/src/commands/paginated-select.ts +81 -0
  327. package/src/commands/permissions.ts +397 -0
  328. package/src/commands/queue.ts +293 -0
  329. package/src/commands/remove-project.ts +155 -0
  330. package/src/commands/restart-opencode-server.ts +162 -0
  331. package/src/commands/resume.ts +230 -0
  332. package/src/commands/run-command.ts +123 -0
  333. package/src/commands/screenshare.test.ts +30 -0
  334. package/src/commands/screenshare.ts +366 -0
  335. package/src/commands/session-id.ts +109 -0
  336. package/src/commands/session.ts +227 -0
  337. package/src/commands/share.ts +106 -0
  338. package/src/commands/tasks.ts +293 -0
  339. package/src/commands/thread-deletion-sync.ts +80 -0
  340. package/src/commands/types.ts +25 -0
  341. package/src/commands/undo-redo.ts +386 -0
  342. package/src/commands/unset-model.ts +174 -0
  343. package/src/commands/upgrade.ts +59 -0
  344. package/src/commands/user-command.ts +198 -0
  345. package/src/commands/verbosity.ts +173 -0
  346. package/src/commands/vscode.ts +342 -0
  347. package/src/commands/worktree-settings.ts +70 -0
  348. package/src/commands/worktrees.ts +645 -0
  349. package/src/condense-memory.ts +36 -0
  350. package/src/config.ts +103 -339
  351. package/src/context-awareness-plugin.test.ts +144 -0
  352. package/src/context-awareness-plugin.ts +469 -0
  353. package/src/critique-utils.ts +139 -0
  354. package/src/database.ts +1949 -0
  355. package/src/db.test.ts +162 -0
  356. package/src/db.ts +295 -0
  357. package/src/debounce-timeout.ts +43 -0
  358. package/src/debounced-process-flush.ts +104 -0
  359. package/src/discord-bot.ts +1505 -0
  360. package/src/discord-command-registration.ts +752 -0
  361. package/src/discord-urls.ts +89 -0
  362. package/src/discord-utils.test.ts +153 -0
  363. package/src/discord-utils.ts +846 -0
  364. package/src/errors.ts +201 -0
  365. package/src/escape-backticks.test.ts +469 -0
  366. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  367. package/src/eventsource-parser.test.ts +351 -0
  368. package/src/exec-async.ts +35 -0
  369. package/src/external-opencode-sync.ts +685 -0
  370. package/src/format-tables.test.ts +515 -0
  371. package/src/format-tables.ts +718 -0
  372. package/src/forum-sync/config.ts +92 -0
  373. package/src/forum-sync/discord-operations.ts +241 -0
  374. package/src/forum-sync/index.ts +9 -0
  375. package/src/forum-sync/markdown.ts +172 -0
  376. package/src/forum-sync/sync-to-discord.ts +595 -0
  377. package/src/forum-sync/sync-to-files.ts +294 -0
  378. package/src/forum-sync/types.ts +175 -0
  379. package/src/forum-sync/watchers.ts +454 -0
  380. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  381. package/src/gateway-proxy.e2e.test.ts +644 -0
  382. package/src/genai-worker-wrapper.ts +164 -0
  383. package/src/genai-worker.ts +386 -0
  384. package/src/genai.ts +321 -0
  385. package/src/generated/browser.ts +114 -0
  386. package/src/generated/client.ts +138 -0
  387. package/src/generated/commonInputTypes.ts +770 -0
  388. package/src/generated/enums.ts +98 -0
  389. package/src/generated/internal/class.ts +384 -0
  390. package/src/generated/internal/prismaNamespace.ts +2394 -0
  391. package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
  392. package/src/generated/models/bot_api_keys.ts +1288 -0
  393. package/src/generated/models/bot_tokens.ts +1700 -0
  394. package/src/generated/models/channel_agents.ts +1256 -0
  395. package/src/generated/models/channel_directories.ts +1859 -0
  396. package/src/generated/models/channel_mention_mode.ts +1300 -0
  397. package/src/generated/models/channel_models.ts +1288 -0
  398. package/src/generated/models/channel_verbosity.ts +1228 -0
  399. package/src/generated/models/channel_worktrees.ts +1300 -0
  400. package/src/generated/models/forum_sync_configs.ts +1452 -0
  401. package/src/generated/models/global_models.ts +1288 -0
  402. package/src/generated/models/ipc_requests.ts +1485 -0
  403. package/src/generated/models/part_messages.ts +1302 -0
  404. package/src/generated/models/scheduled_tasks.ts +2320 -0
  405. package/src/generated/models/session_agents.ts +1086 -0
  406. package/src/generated/models/session_events.ts +1439 -0
  407. package/src/generated/models/session_models.ts +1114 -0
  408. package/src/generated/models/session_start_sources.ts +1408 -0
  409. package/src/generated/models/thread_sessions.ts +1781 -0
  410. package/src/generated/models/thread_worktrees.ts +1356 -0
  411. package/src/generated/models.ts +30 -0
  412. package/src/heap-monitor.ts +152 -0
  413. package/src/hrana-server.test.ts +434 -0
  414. package/src/hrana-server.ts +299 -0
  415. package/src/html-actions.test.ts +87 -0
  416. package/src/html-actions.ts +174 -0
  417. package/src/html-components.test.ts +38 -0
  418. package/src/html-components.ts +181 -0
  419. package/src/image-optimizer-plugin.ts +194 -0
  420. package/src/image-utils.ts +149 -0
  421. package/src/interaction-handler.ts +610 -0
  422. package/src/ipc-polling.ts +427 -0
  423. package/src/ipc-tools-plugin.ts +236 -0
  424. package/src/ipc-utils.ts +29 -0
  425. package/src/limit-heading-depth.test.ts +116 -0
  426. package/src/limit-heading-depth.ts +26 -0
  427. package/src/logger.ts +215 -0
  428. package/src/markdown.test.ts +315 -0
  429. package/src/markdown.ts +410 -0
  430. package/src/memory-overview-plugin.ts +163 -0
  431. package/src/message-finish-field.e2e.test.ts +195 -0
  432. package/src/message-formatting.test.ts +126 -0
  433. package/src/message-formatting.ts +535 -0
  434. package/src/message-preprocessing.ts +488 -0
  435. package/src/onboarding-tutorial.ts +167 -0
  436. package/src/onboarding-welcome.ts +49 -0
  437. package/src/openai-realtime.ts +358 -0
  438. package/src/opencode-command-detection.test.ts +307 -0
  439. package/src/opencode-command-detection.ts +76 -0
  440. package/src/opencode-command.test.ts +70 -0
  441. package/src/opencode-command.ts +191 -0
  442. package/src/opencode-interrupt-plugin.test.ts +682 -0
  443. package/src/opencode-interrupt-plugin.ts +507 -0
  444. package/src/opencode.ts +1453 -0
  445. package/src/otto/branding.ts +23 -0
  446. package/src/otto/index.ts +22 -0
  447. package/src/otto-digital-twin.e2e.test.ts +199 -0
  448. package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
  449. package/src/otto-opencode-plugin.test.ts +108 -0
  450. package/src/otto-opencode-plugin.ts +22 -0
  451. package/src/parse-permission-rules.test.ts +127 -0
  452. package/src/patch-text-parser.ts +107 -0
  453. package/src/plugin-logger.ts +84 -0
  454. package/src/privacy-sanitizer.ts +142 -0
  455. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  456. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  457. package/src/queue-advanced-e2e-setup.ts +877 -0
  458. package/src/queue-advanced-footer.e2e.test.ts +591 -0
  459. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  460. package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
  461. package/src/queue-advanced-question.e2e.test.ts +316 -0
  462. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  463. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  464. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  465. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  466. package/src/queue-question-select-drain.e2e.test.ts +327 -0
  467. package/src/runtime-idle-sweeper.ts +76 -0
  468. package/src/runtime-lifecycle.e2e.test.ts +651 -0
  469. package/src/schema.sql +174 -0
  470. package/src/sentry.ts +26 -0
  471. package/src/session-handler/agent-utils.ts +99 -0
  472. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  473. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  474. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  475. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  476. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  477. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  478. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  479. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  480. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  481. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  482. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  483. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  484. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  485. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  486. package/src/session-handler/event-stream-state.test.ts +717 -0
  487. package/src/session-handler/event-stream-state.ts +706 -0
  488. package/src/session-handler/model-utils.ts +217 -0
  489. package/src/session-handler/opencode-session-event-log.ts +130 -0
  490. package/src/session-handler/thread-runtime-state.ts +247 -0
  491. package/src/session-handler/thread-session-runtime.ts +4440 -0
  492. package/src/session-handler.ts +15 -0
  493. package/src/session-search.test.ts +50 -0
  494. package/src/session-search.ts +148 -0
  495. package/src/session-title-rename.test.ts +130 -0
  496. package/src/skill-filter.test.ts +83 -0
  497. package/src/skill-filter.ts +42 -0
  498. package/src/startup-service.ts +200 -0
  499. package/src/startup-time.e2e.test.ts +373 -0
  500. package/src/store.ts +139 -0
  501. package/src/subagent-rate-limit-plugin.ts +218 -0
  502. package/src/system-message.test.ts +710 -0
  503. package/src/system-message.ts +814 -0
  504. package/src/task-runner.ts +725 -0
  505. package/src/task-schedule.test.ts +84 -0
  506. package/src/task-schedule.ts +317 -0
  507. package/src/test-utils.ts +451 -0
  508. package/src/thinking-utils.ts +61 -0
  509. package/src/thread-message-queue.e2e.test.ts +1350 -0
  510. package/src/tools.ts +430 -0
  511. package/src/undici.d.ts +12 -0
  512. package/src/undo-redo.e2e.test.ts +209 -0
  513. package/src/unnest-code-blocks.test.ts +713 -0
  514. package/src/unnest-code-blocks.ts +185 -0
  515. package/src/upgrade.ts +185 -0
  516. package/src/utils.test.ts +155 -0
  517. package/src/utils.ts +265 -0
  518. package/src/voice-attachment.ts +51 -0
  519. package/src/voice-handler.ts +908 -0
  520. package/src/voice-message.e2e.test.ts +1255 -0
  521. package/src/voice.test.ts +281 -0
  522. package/src/voice.ts +638 -0
  523. package/src/wait-session.ts +273 -0
  524. package/src/websockify.ts +101 -0
  525. package/src/worker-types.ts +64 -0
  526. package/src/worktree-lifecycle.e2e.test.ts +396 -0
  527. package/src/worktree-utils.ts +4 -0
  528. package/src/worktrees.test.ts +489 -0
  529. package/src/worktrees.ts +1370 -0
  530. package/src/xml.test.ts +38 -0
  531. package/src/xml.ts +121 -0
  532. package/dist/cli.d.ts +0 -3
  533. package/dist/cli.d.ts.map +0 -1
  534. package/dist/cli.js.map +0 -1
  535. package/dist/config.d.ts +0 -39
  536. package/dist/config.d.ts.map +0 -1
  537. package/dist/config.js.map +0 -1
  538. package/dist/config.test.d.ts +0 -2
  539. package/dist/config.test.d.ts.map +0 -1
  540. package/dist/config.test.js +0 -202
  541. package/dist/config.test.js.map +0 -1
  542. package/dist/detect.d.ts +0 -9
  543. package/dist/detect.d.ts.map +0 -1
  544. package/dist/detect.js +0 -40
  545. package/dist/detect.js.map +0 -1
  546. package/dist/detect.test.d.ts +0 -2
  547. package/dist/detect.test.d.ts.map +0 -1
  548. package/dist/detect.test.js +0 -26
  549. package/dist/detect.test.js.map +0 -1
  550. package/dist/docker.d.ts +0 -7
  551. package/dist/docker.d.ts.map +0 -1
  552. package/dist/docker.js +0 -17
  553. package/dist/docker.js.map +0 -1
  554. package/dist/docker.test.d.ts +0 -2
  555. package/dist/docker.test.d.ts.map +0 -1
  556. package/dist/docker.test.js +0 -12
  557. package/dist/docker.test.js.map +0 -1
  558. package/dist/health.d.ts +0 -31
  559. package/dist/health.d.ts.map +0 -1
  560. package/dist/health.js +0 -117
  561. package/dist/health.js.map +0 -1
  562. package/dist/health.test.d.ts +0 -2
  563. package/dist/health.test.d.ts.map +0 -1
  564. package/dist/health.test.js +0 -52
  565. package/dist/health.test.js.map +0 -1
  566. package/dist/index.d.ts +0 -20
  567. package/dist/index.d.ts.map +0 -1
  568. package/dist/index.js +0 -15
  569. package/dist/index.js.map +0 -1
  570. package/dist/index.test.d.ts +0 -2
  571. package/dist/index.test.d.ts.map +0 -1
  572. package/dist/index.test.js +0 -8
  573. package/dist/index.test.js.map +0 -1
  574. package/dist/installer.d.ts +0 -10
  575. package/dist/installer.d.ts.map +0 -1
  576. package/dist/installer.js +0 -50
  577. package/dist/installer.js.map +0 -1
  578. package/dist/installer.test.d.ts +0 -2
  579. package/dist/installer.test.d.ts.map +0 -1
  580. package/dist/installer.test.js +0 -43
  581. package/dist/installer.test.js.map +0 -1
  582. package/dist/lifecycle.d.ts +0 -10
  583. package/dist/lifecycle.d.ts.map +0 -1
  584. package/dist/lifecycle.js +0 -45
  585. package/dist/lifecycle.js.map +0 -1
  586. package/dist/lifecycle.test.d.ts +0 -2
  587. package/dist/lifecycle.test.d.ts.map +0 -1
  588. package/dist/lifecycle.test.js +0 -20
  589. package/dist/lifecycle.test.js.map +0 -1
  590. package/dist/manifest.d.ts +0 -18
  591. package/dist/manifest.d.ts.map +0 -1
  592. package/dist/manifest.js +0 -30
  593. package/dist/manifest.js.map +0 -1
  594. package/dist/skills-baseline.d.ts +0 -7
  595. package/dist/skills-baseline.d.ts.map +0 -1
  596. package/dist/skills-baseline.js +0 -9
  597. package/dist/skills-baseline.js.map +0 -1
  598. package/dist/skills.d.ts +0 -110
  599. package/dist/skills.d.ts.map +0 -1
  600. package/dist/skills.js +0 -429
  601. package/dist/skills.js.map +0 -1
  602. package/dist/skills.test.d.ts +0 -2
  603. package/dist/skills.test.d.ts.map +0 -1
  604. package/dist/skills.test.js +0 -416
  605. package/dist/skills.test.js.map +0 -1
  606. package/dist/sync.d.ts +0 -10
  607. package/dist/sync.d.ts.map +0 -1
  608. package/dist/sync.js +0 -39
  609. package/dist/sync.js.map +0 -1
  610. package/dist/tenant.d.ts +0 -13
  611. package/dist/tenant.d.ts.map +0 -1
  612. package/dist/tenant.js +0 -105
  613. package/dist/tenant.js.map +0 -1
  614. package/dist/tenant.test.d.ts +0 -2
  615. package/dist/tenant.test.d.ts.map +0 -1
  616. package/dist/tenant.test.js +0 -37
  617. package/dist/tenant.test.js.map +0 -1
  618. package/src/config.test.ts +0 -237
  619. package/src/detect.test.ts +0 -29
  620. package/src/detect.ts +0 -52
  621. package/src/docker.test.ts +0 -12
  622. package/src/docker.ts +0 -23
  623. package/src/health.test.ts +0 -61
  624. package/src/health.ts +0 -158
  625. package/src/index.test.ts +0 -8
  626. package/src/index.ts +0 -62
  627. package/src/installer.test.ts +0 -52
  628. package/src/installer.ts +0 -62
  629. package/src/lifecycle.test.ts +0 -23
  630. package/src/lifecycle.ts +0 -49
  631. package/src/manifest.ts +0 -42
  632. package/src/skills-baseline.ts +0 -14
  633. package/src/skills.test.ts +0 -503
  634. package/src/skills.ts +0 -512
  635. package/src/sync.ts +0 -53
  636. package/src/tenant.test.ts +0 -49
  637. package/src/tenant.ts +0 -120
@@ -0,0 +1,478 @@
1
+ import { test, expect, describe } from 'vitest';
2
+ import { splitTablesFromMarkdown, buildTableComponents, } from './format-tables.js';
3
+ import { Lexer } from 'marked';
4
+ import { ComponentType } from 'discord.js';
5
+ function isTableToken(token) {
6
+ return (token.type === 'table' &&
7
+ Object.hasOwn(token, 'header') &&
8
+ Object.hasOwn(token, 'rows'));
9
+ }
10
+ function parseTable(markdown) {
11
+ const lexer = new Lexer();
12
+ const tokens = lexer.lex(markdown);
13
+ const table = tokens.find((token) => {
14
+ return isTableToken(token);
15
+ });
16
+ if (!table || !isTableToken(table)) {
17
+ throw new Error('Expected markdown to contain a table token');
18
+ }
19
+ return table;
20
+ }
21
+ /** Extract the first container's children from buildTableComponents result */
22
+ function getContainerChildren(segments) {
23
+ const seg = segments[0];
24
+ if (seg.type !== 'components') {
25
+ throw new Error('Expected components segment');
26
+ }
27
+ const container = seg.components[0];
28
+ if (!container || container.type !== ComponentType.Container) {
29
+ throw new Error('Expected first top-level component to be a container');
30
+ }
31
+ return container.components.map((component) => {
32
+ const content = component.type === ComponentType.TextDisplay ? component.content : undefined;
33
+ const divider = component.type === ComponentType.Separator ? component.divider : undefined;
34
+ const spacing = component.type === ComponentType.Separator ? component.spacing : undefined;
35
+ return {
36
+ type: component.type,
37
+ content,
38
+ divider,
39
+ spacing,
40
+ };
41
+ });
42
+ }
43
+ describe('buildTableComponents', () => {
44
+ test('builds container with key-value TextDisplays', () => {
45
+ const table = parseTable(`| Name | Age |
46
+ | --- | --- |
47
+ | Alice | 30 |
48
+ | Bob | 25 |`);
49
+ const result = buildTableComponents(table);
50
+ expect(result).toMatchInlineSnapshot(`
51
+ [
52
+ {
53
+ "components": [
54
+ {
55
+ "components": [
56
+ {
57
+ "content": "**Name** Alice
58
+ **Age** 30",
59
+ "type": 10,
60
+ },
61
+ {
62
+ "divider": true,
63
+ "spacing": 1,
64
+ "type": 14,
65
+ },
66
+ {
67
+ "content": "**Name** Bob
68
+ **Age** 25",
69
+ "type": 10,
70
+ },
71
+ ],
72
+ "type": 17,
73
+ },
74
+ ],
75
+ "type": "components",
76
+ },
77
+ ]
78
+ `);
79
+ });
80
+ test('adds separators between row groups', () => {
81
+ const table = parseTable(`| Key | Value |
82
+ | --- | --- |
83
+ | a | 1 |
84
+ | b | 2 |
85
+ | c | 3 |`);
86
+ const result = buildTableComponents(table);
87
+ const types = getContainerChildren(result).map((c) => c.type);
88
+ // type 10 = TextDisplay, type 14 = Separator
89
+ expect(types).toMatchInlineSnapshot(`
90
+ [
91
+ 10,
92
+ 14,
93
+ 10,
94
+ 14,
95
+ 10,
96
+ ]
97
+ `);
98
+ });
99
+ test('single-row table has one TextDisplay, no separators', () => {
100
+ const table = parseTable(`| Method | Endpoint |
101
+ | --- | --- |
102
+ | GET | /api/users |`);
103
+ const result = buildTableComponents(table);
104
+ const children = getContainerChildren(result);
105
+ expect(children).toHaveLength(1);
106
+ expect(children[0].type).toBe(10);
107
+ expect(children[0].content).toMatchInlineSnapshot(`
108
+ "**Method** GET
109
+ **Endpoint** /api/users"
110
+ `);
111
+ });
112
+ test('splits large table into multiple container segments', () => {
113
+ // 25 rows: exceeds 19 rows per container, so splits into 2 containers
114
+ const headers = '| A | B |';
115
+ const sep = '| --- | --- |';
116
+ const rows = Array.from({ length: 25 }, (_, i) => {
117
+ return `| ${i}a | ${i}b |`;
118
+ }).join('\n');
119
+ const table = parseTable(`${headers}\n${sep}\n${rows}`);
120
+ const result = buildTableComponents(table);
121
+ expect(result).toHaveLength(2);
122
+ expect(result[0].type).toBe('components');
123
+ expect(result[1].type).toBe('components');
124
+ // First container has 20 rows (20 TDs + 19 seps = 39 children)
125
+ const firstChildren = getContainerChildren([result[0]]);
126
+ expect(firstChildren).toHaveLength(20 + 19);
127
+ // Second container has 5 rows (5 TDs + 4 seps = 9 children)
128
+ const secondChildren = getContainerChildren([result[1]]);
129
+ expect(secondChildren).toHaveLength(5 + 4);
130
+ });
131
+ test('strips formatting from cells', () => {
132
+ const table = parseTable(`| Header | Value |
133
+ | --- | --- |
134
+ | **Bold text** | Normal |
135
+ | *Italic* | \`code\` |`);
136
+ const result = buildTableComponents(table);
137
+ const children = getContainerChildren(result);
138
+ expect(children[0].content).toMatchInlineSnapshot(`
139
+ "**Header** Bold text
140
+ **Value** Normal"
141
+ `);
142
+ });
143
+ test('renders button cells as action rows inside the container', () => {
144
+ const table = parseTable(`| Name | Action |
145
+ | --- | --- |
146
+ | feature-a | <button id="delete-a" variant="secondary">Delete</button> |`);
147
+ const result = buildTableComponents(table, {
148
+ resolveButtonCustomId: ({ button }) => {
149
+ return `html_action:${button.id}`;
150
+ },
151
+ });
152
+ expect(result).toMatchInlineSnapshot(`
153
+ [
154
+ {
155
+ "components": [
156
+ {
157
+ "components": [
158
+ {
159
+ "content": "**Name** feature-a",
160
+ "type": 10,
161
+ },
162
+ {
163
+ "components": [
164
+ {
165
+ "custom_id": "html_action:delete-a",
166
+ "disabled": false,
167
+ "label": "Delete",
168
+ "style": 2,
169
+ "type": 2,
170
+ },
171
+ ],
172
+ "type": 1,
173
+ },
174
+ ],
175
+ "type": 17,
176
+ },
177
+ ],
178
+ "type": "components",
179
+ },
180
+ ]
181
+ `);
182
+ });
183
+ test('falls back to button text when no resolver is provided', () => {
184
+ const table = parseTable(`| Name | Action |
185
+ | --- | --- |
186
+ | feature-a | <button id="delete-a" variant="secondary">Delete</button> |`);
187
+ const result = buildTableComponents(table);
188
+ expect(result).toMatchInlineSnapshot(`
189
+ [
190
+ {
191
+ "components": [
192
+ {
193
+ "components": [
194
+ {
195
+ "content": "**Name** feature-a
196
+ **Action** Delete",
197
+ "type": 10,
198
+ },
199
+ ],
200
+ "type": 17,
201
+ },
202
+ ],
203
+ "type": "components",
204
+ },
205
+ ]
206
+ `);
207
+ });
208
+ test('renders wide rows with buttons without using sections', () => {
209
+ const table = parseTable(`| Thread | Name | Status | Created | Folder | Action |
210
+ | --- | --- | --- | --- | --- | --- |
211
+ | thread | feature-a | merged | 1m ago | /tmp/feature-a | <button id="delete-a" variant="secondary">Delete</button> |`);
212
+ const result = buildTableComponents(table, {
213
+ resolveButtonCustomId: ({ button }) => {
214
+ return `html_action:${button.id}`;
215
+ },
216
+ });
217
+ expect(result).toMatchInlineSnapshot(`
218
+ [
219
+ {
220
+ "components": [
221
+ {
222
+ "components": [
223
+ {
224
+ "content": "**Thread** thread
225
+ **Name** feature-a
226
+ **Status** merged
227
+ **Created** 1m ago
228
+ **Folder** /tmp/feature-a",
229
+ "type": 10,
230
+ },
231
+ {
232
+ "components": [
233
+ {
234
+ "custom_id": "html_action:delete-a",
235
+ "disabled": false,
236
+ "label": "Delete",
237
+ "style": 2,
238
+ "type": 2,
239
+ },
240
+ ],
241
+ "type": 1,
242
+ },
243
+ ],
244
+ "type": 17,
245
+ },
246
+ ],
247
+ "type": "components",
248
+ },
249
+ ]
250
+ `);
251
+ });
252
+ });
253
+ describe('splitTablesFromMarkdown', () => {
254
+ test('returns single text segment for content without tables', () => {
255
+ const result = splitTablesFromMarkdown('Just some text.\n\nMore text.');
256
+ expect(result).toHaveLength(1);
257
+ expect(result[0].type).toBe('text');
258
+ });
259
+ test('returns single components segment for table-only content', () => {
260
+ const result = splitTablesFromMarkdown(`| A | B |
261
+ | --- | --- |
262
+ | 1 | 2 |`);
263
+ expect(result).toHaveLength(1);
264
+ expect(result[0].type).toBe('components');
265
+ });
266
+ test('splits text before and after table into separate segments', () => {
267
+ const result = splitTablesFromMarkdown(`Text before.
268
+
269
+ | Key | Value |
270
+ | --- | --- |
271
+ | a | 1 |
272
+
273
+ Text after.`);
274
+ expect(result).toHaveLength(3);
275
+ expect(result[0].type).toBe('text');
276
+ expect(result[1].type).toBe('components');
277
+ expect(result[2].type).toBe('text');
278
+ });
279
+ test('handles multiple tables with text between', () => {
280
+ const result = splitTablesFromMarkdown(`First table:
281
+
282
+ | A | B |
283
+ | --- | --- |
284
+ | 1 | 2 |
285
+
286
+ Middle text.
287
+
288
+ | X | Y |
289
+ | --- | --- |
290
+ | a | b |`);
291
+ expect(result).toHaveLength(4);
292
+ expect(result.map((s) => s.type)).toMatchInlineSnapshot(`
293
+ [
294
+ "text",
295
+ "components",
296
+ "text",
297
+ "components",
298
+ ]
299
+ `);
300
+ });
301
+ test('splits oversized table into multiple component segments', () => {
302
+ const headers = '| A | B |';
303
+ const sep = '| --- | --- |';
304
+ const rows = Array.from({ length: 25 }, (_, i) => {
305
+ return `| ${i}a | ${i}b |`;
306
+ }).join('\n');
307
+ const result = splitTablesFromMarkdown(`${headers}\n${sep}\n${rows}`);
308
+ // 25 rows splits into 2 container segments
309
+ expect(result).toHaveLength(2);
310
+ expect(result.every((s) => s.type === 'components')).toBe(true);
311
+ });
312
+ test('preserves code blocks alongside tables', () => {
313
+ const result = splitTablesFromMarkdown(`Some code:
314
+
315
+ \`\`\`js
316
+ const x = 1
317
+ \`\`\`
318
+
319
+ | Key | Value |
320
+ | --- | --- |
321
+ | a | 1 |
322
+
323
+ Done.`);
324
+ const types = result.map((s) => s.type);
325
+ expect(types).toMatchInlineSnapshot(`
326
+ [
327
+ "text",
328
+ "components",
329
+ "text",
330
+ ]
331
+ `);
332
+ });
333
+ test('renders callout text inside an accented container', () => {
334
+ const result = splitTablesFromMarkdown(`<callout accent="#2b7fff">
335
+ ## Important
336
+
337
+ Read this first.
338
+ </callout>`);
339
+ expect(result).toMatchInlineSnapshot(`
340
+ [
341
+ {
342
+ "components": [
343
+ {
344
+ "accent_color": 2850815,
345
+ "components": [
346
+ {
347
+ "content": "## Important
348
+
349
+ Read this first.",
350
+ "type": 10,
351
+ },
352
+ ],
353
+ "type": 17,
354
+ },
355
+ ],
356
+ "type": "components",
357
+ },
358
+ ]
359
+ `);
360
+ });
361
+ test('renders tables inside callouts recursively', () => {
362
+ const result = splitTablesFromMarkdown(`<callout accent="#2b7fff">
363
+ ## Important
364
+
365
+ | Key | Value |
366
+ | --- | --- |
367
+ | a | 1 |
368
+ </callout>`);
369
+ expect(result).toMatchInlineSnapshot(`
370
+ [
371
+ {
372
+ "components": [
373
+ {
374
+ "accent_color": 2850815,
375
+ "components": [
376
+ {
377
+ "content": "## Important",
378
+ "type": 10,
379
+ },
380
+ {
381
+ "content": "**Key** a
382
+ **Value** 1",
383
+ "type": 10,
384
+ },
385
+ ],
386
+ "type": 17,
387
+ },
388
+ ],
389
+ "type": "components",
390
+ },
391
+ ]
392
+ `);
393
+ });
394
+ test('renders button rows inside callouts recursively', () => {
395
+ const result = splitTablesFromMarkdown(`<callout accent="#2b7fff">
396
+ ## Actions
397
+
398
+ | Name | Action |
399
+ | --- | --- |
400
+ | feature-a | <button id="delete-a" variant="secondary">Delete</button> |
401
+ </callout>`, {
402
+ resolveButtonCustomId: ({ button }) => {
403
+ return `html_action:${button.id}`;
404
+ },
405
+ });
406
+ expect(result).toMatchInlineSnapshot(`
407
+ [
408
+ {
409
+ "components": [
410
+ {
411
+ "accent_color": 2850815,
412
+ "components": [
413
+ {
414
+ "content": "## Actions",
415
+ "type": 10,
416
+ },
417
+ {
418
+ "content": "**Name** feature-a",
419
+ "type": 10,
420
+ },
421
+ {
422
+ "components": [
423
+ {
424
+ "custom_id": "html_action:delete-a",
425
+ "disabled": false,
426
+ "label": "Delete",
427
+ "style": 2,
428
+ "type": 2,
429
+ },
430
+ ],
431
+ "type": 1,
432
+ },
433
+ ],
434
+ "type": 17,
435
+ },
436
+ ],
437
+ "type": "components",
438
+ },
439
+ ]
440
+ `);
441
+ });
442
+ test('renders callout that was prefixed with ⬥ as plain text (regression)', () => {
443
+ // Before the fix, formatPart would add ⬥ prefix to callout lines,
444
+ // breaking the callout parser. Now formatPart skips the prefix for callouts.
445
+ const result = splitTablesFromMarkdown(`⬥ <callout accent="#ef4444">
446
+ ## Top priority
447
+ - **Stripe dispute** deadline
448
+ </callout>`);
449
+ expect(result).toMatchInlineSnapshot(`
450
+ [
451
+ {
452
+ "text": "⬥ <callout accent="#ef4444">
453
+ ## Top priority
454
+ - **Stripe dispute** deadline
455
+ </callout>",
456
+ "type": "text",
457
+ },
458
+ ]
459
+ `);
460
+ });
461
+ test('falls back to plain text when a callout is not closed', () => {
462
+ const result = splitTablesFromMarkdown(`<callout accent="#2b7fff">
463
+ ## Important
464
+
465
+ Still open`);
466
+ expect(result).toMatchInlineSnapshot(`
467
+ [
468
+ {
469
+ "text": "<callout accent="#2b7fff">
470
+ ## Important
471
+
472
+ Still open",
473
+ "type": "text",
474
+ },
475
+ ]
476
+ `);
477
+ });
478
+ });
@@ -0,0 +1,79 @@
1
+ // Forum sync configuration from SQLite database.
2
+ // Reads forum_sync_configs table and resolves relative output dirs.
3
+ // On first run, migrates any existing forum-sync.json into the DB.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import YAML from 'yaml';
7
+ import { getDataDir } from '../config.js';
8
+ import { getForumSyncConfigs, upsertForumSyncConfig } from '../database.js';
9
+ import { createLogger } from '../logger.js';
10
+ const forumLogger = createLogger('FORUM');
11
+ const LEGACY_CONFIG_FILE = 'forum-sync.json';
12
+ function isForumSyncDirection(value) {
13
+ return value === 'discord-to-files' || value === 'bidirectional';
14
+ }
15
+ function resolveOutputDir(outputDir) {
16
+ if (path.isAbsolute(outputDir))
17
+ return outputDir;
18
+ return path.resolve(getDataDir(), outputDir);
19
+ }
20
+ /**
21
+ * One-time migration: if the legacy forum-sync.json exists, import its entries
22
+ * into the DB and rename the file so it's not re-imported on next startup.
23
+ */
24
+ async function migrateLegacyConfig({ appId }) {
25
+ const configPath = path.join(getDataDir(), LEGACY_CONFIG_FILE);
26
+ if (!fs.existsSync(configPath))
27
+ return;
28
+ forumLogger.log(`Migrating legacy ${LEGACY_CONFIG_FILE} into database...`);
29
+ const raw = fs.readFileSync(configPath, 'utf8');
30
+ let parsed;
31
+ try {
32
+ parsed = YAML.parse(raw);
33
+ }
34
+ catch {
35
+ forumLogger.warn(`Failed to parse legacy ${LEGACY_CONFIG_FILE}, skipping migration`);
36
+ return;
37
+ }
38
+ if (!parsed || typeof parsed !== 'object')
39
+ return;
40
+ const forums = parsed.forums;
41
+ if (!Array.isArray(forums))
42
+ return;
43
+ for (const item of forums) {
44
+ if (!item || typeof item !== 'object')
45
+ continue;
46
+ const entry = item;
47
+ const forumChannelId = typeof entry.forumChannelId === 'string' ? entry.forumChannelId : '';
48
+ const outputDir = typeof entry.outputDir === 'string' ? entry.outputDir : '';
49
+ const direction = isForumSyncDirection(entry.direction)
50
+ ? entry.direction
51
+ : 'bidirectional';
52
+ if (!forumChannelId || !outputDir)
53
+ continue;
54
+ await upsertForumSyncConfig({
55
+ appId,
56
+ forumChannelId,
57
+ outputDir: resolveOutputDir(outputDir),
58
+ direction,
59
+ });
60
+ }
61
+ // Rename so we don't re-import next time
62
+ const backupPath = configPath + '.migrated';
63
+ fs.renameSync(configPath, backupPath);
64
+ forumLogger.log(`Legacy config migrated and renamed to ${path.basename(backupPath)}`);
65
+ }
66
+ export async function readForumSyncConfig({ appId }) {
67
+ if (!appId)
68
+ return [];
69
+ // Migrate legacy JSON file on first run
70
+ await migrateLegacyConfig({ appId });
71
+ const rows = await getForumSyncConfigs({ appId });
72
+ return rows.map((row) => ({
73
+ forumChannelId: row.forumChannelId,
74
+ outputDir: resolveOutputDir(row.outputDir),
75
+ direction: isForumSyncDirection(row.direction)
76
+ ? row.direction
77
+ : 'bidirectional',
78
+ }));
79
+ }
@@ -0,0 +1,154 @@
1
+ // Discord API operations for forum sync.
2
+ // Resolves forum channels, fetches threads (active + archived) with pagination,
3
+ // fetches thread messages, loads existing forum files from disk, and ensures directories.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { ChannelType, } from 'discord.js';
7
+ import { createLogger } from '../logger.js';
8
+ import { parseFrontmatter, getStringValue } from './markdown.js';
9
+ import { DEFAULT_RATE_LIMIT_DELAY_MS, ForumChannelResolveError, ForumSyncOperationError, delay, } from './types.js';
10
+ const forumLogger = createLogger('FORUM');
11
+ export function getCanonicalThreadFilePath({ outputDir, threadId, subfolder, }) {
12
+ if (subfolder) {
13
+ return path.join(outputDir, subfolder, `${threadId}.md`);
14
+ }
15
+ return path.join(outputDir, `${threadId}.md`);
16
+ }
17
+ export async function ensureDirectory({ directory }) {
18
+ const result = await fs.promises.mkdir(directory, { recursive: true }).catch((cause) => new ForumSyncOperationError({
19
+ forumChannelId: 'unknown',
20
+ reason: directory,
21
+ cause,
22
+ }));
23
+ if (result instanceof Error)
24
+ return result;
25
+ }
26
+ export async function resolveForumChannel({ discordClient, forumChannelId, }) {
27
+ const channel = await discordClient.channels
28
+ .fetch(forumChannelId)
29
+ .catch((cause) => new ForumChannelResolveError({ forumChannelId, cause }));
30
+ if (channel instanceof Error)
31
+ return channel;
32
+ if (!channel || channel.type !== ChannelType.GuildForum) {
33
+ return new ForumChannelResolveError({ forumChannelId });
34
+ }
35
+ return channel;
36
+ }
37
+ export async function fetchForumThreads({ forumChannel, }) {
38
+ const byId = new Map();
39
+ const active = await forumChannel.threads.fetchActive().catch((cause) => new ForumSyncOperationError({
40
+ forumChannelId: forumChannel.id,
41
+ reason: 'fetchActive failed',
42
+ cause,
43
+ }));
44
+ if (active instanceof Error)
45
+ return active;
46
+ for (const [id, thread] of active.threads) {
47
+ byId.set(id, thread);
48
+ }
49
+ let before;
50
+ while (true) {
51
+ const archived = await forumChannel.threads
52
+ .fetchArchived({ type: 'public', limit: 100, before })
53
+ .catch((cause) => new ForumSyncOperationError({
54
+ forumChannelId: forumChannel.id,
55
+ reason: 'fetchArchived failed',
56
+ cause,
57
+ }));
58
+ if (archived instanceof Error)
59
+ return archived;
60
+ const threads = Array.from(archived.threads.values());
61
+ for (const thread of threads) {
62
+ byId.set(thread.id, thread);
63
+ }
64
+ if (!archived.hasMore || threads.length === 0)
65
+ break;
66
+ const timestamps = threads
67
+ .map((thread) => thread.archiveTimestamp ?? thread.createdTimestamp)
68
+ .filter((value) => value !== null);
69
+ const oldestTimestamp = Math.min(...timestamps);
70
+ if (!Number.isFinite(oldestTimestamp))
71
+ break;
72
+ before = new Date(oldestTimestamp - 1);
73
+ await delay({ ms: DEFAULT_RATE_LIMIT_DELAY_MS });
74
+ }
75
+ return Array.from(byId.values());
76
+ }
77
+ export async function fetchThreadMessages({ thread, }) {
78
+ const byId = new Map();
79
+ let before;
80
+ while (true) {
81
+ const fetched = await thread.messages.fetch({ limit: 100, before }).catch((cause) => new ForumSyncOperationError({
82
+ forumChannelId: thread.parentId || 'unknown',
83
+ reason: `message fetch failed for thread ${thread.id}`,
84
+ cause,
85
+ }));
86
+ if (fetched instanceof Error)
87
+ return fetched;
88
+ const messages = Array.from(fetched.values());
89
+ for (const message of messages) {
90
+ byId.set(message.id, message);
91
+ }
92
+ if (messages.length < 100 || messages.length === 0)
93
+ break;
94
+ // Find oldest message for cursor - messages are sorted by Discord, last is oldest
95
+ const oldest = messages[messages.length - 1];
96
+ if (!oldest)
97
+ break;
98
+ before = oldest.id;
99
+ await delay({ ms: DEFAULT_RATE_LIMIT_DELAY_MS });
100
+ }
101
+ return Array.from(byId.values()).sort((a, b) => a.createdTimestamp - b.createdTimestamp);
102
+ }
103
+ /**
104
+ * Recursively walks a directory collecting all .md files with their relative subfolder path.
105
+ */
106
+ async function collectMarkdownFiles({ dir, outputDir, }) {
107
+ if (!fs.existsSync(dir))
108
+ return [];
109
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
110
+ const relativeSub = path.relative(outputDir, dir);
111
+ const subfolder = relativeSub && relativeSub !== '.' ? relativeSub : undefined;
112
+ const mdFiles = entries
113
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
114
+ .map((entry) => ({ filePath: path.join(dir, entry.name), subfolder }));
115
+ const subdirs = entries.filter((entry) => entry.isDirectory());
116
+ const nestedResults = await Promise.all(subdirs.map((subdir) => collectMarkdownFiles({
117
+ dir: path.join(dir, subdir.name),
118
+ outputDir,
119
+ })));
120
+ return [...mdFiles, ...nestedResults.flat()];
121
+ }
122
+ export async function loadExistingForumFiles({ outputDir, }) {
123
+ const markdownEntries = await collectMarkdownFiles({
124
+ dir: outputDir,
125
+ outputDir,
126
+ });
127
+ const loaded = await Promise.all(markdownEntries.map(async ({ filePath, subfolder }) => {
128
+ const content = await fs.promises
129
+ .readFile(filePath, 'utf8')
130
+ .catch((cause) => {
131
+ forumLogger.warn(`Failed to read forum file ${filePath}:`, cause);
132
+ return null;
133
+ });
134
+ if (content === null)
135
+ return null;
136
+ const parsed = parseFrontmatter({ markdown: content });
137
+ const threadIdFromFrontmatter = getStringValue({
138
+ value: parsed.frontmatter.threadId,
139
+ });
140
+ const threadIdFromFilename = path.basename(filePath, '.md');
141
+ const threadId = threadIdFromFrontmatter ||
142
+ (/^\d+$/.test(threadIdFromFilename) ? threadIdFromFilename : '');
143
+ if (!threadId)
144
+ return null;
145
+ const result = {
146
+ filePath,
147
+ threadId,
148
+ frontmatter: parsed.frontmatter,
149
+ subfolder,
150
+ };
151
+ return result;
152
+ }));
153
+ return loaded.filter((item) => item !== null);
154
+ }