@otto-assistant/bridge 0.4.92

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 (483) 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-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,302 @@
1
+ // Markdown table formatter for Discord.
2
+ // Converts GFM tables to Discord Components V2 (ContainerBuilder with TextDisplay
3
+ // key-value pairs and Separators between row groups). Large tables are split
4
+ // across multiple Container components to stay within the 40-component limit.
5
+ import { Lexer } from 'marked';
6
+ import { ButtonStyle, ComponentType, SeparatorSpacingSize, } from 'discord.js';
7
+ import { parseInlineHtmlRenderables, } from './html-components.js';
8
+ // Max 40 components per message (nested components count toward the limit).
9
+ // Row cost is dynamic now because a table row can render as a plain TextDisplay
10
+ // or as a TextDisplay plus an Action Row holding one or more buttons.
11
+ const MAX_COMPONENTS = 40;
12
+ /**
13
+ * Split markdown into text and table component segments.
14
+ * Tables are rendered as CV2 Container components with bold key-value TextDisplay
15
+ * pairs. Large tables are split across multiple component segments.
16
+ */
17
+ export function splitTablesFromMarkdown(markdown, options = {}) {
18
+ const lexer = new Lexer();
19
+ const tokens = lexer.lex(markdown);
20
+ const segments = [];
21
+ let textBuffer = '';
22
+ for (const token of tokens) {
23
+ if (token.type === 'table') {
24
+ if (textBuffer.trim()) {
25
+ segments.push({ type: 'text', text: textBuffer });
26
+ textBuffer = '';
27
+ }
28
+ const componentSegments = buildTableComponents(token, options);
29
+ segments.push(...componentSegments);
30
+ }
31
+ else {
32
+ textBuffer += token.raw;
33
+ }
34
+ }
35
+ if (textBuffer.trim()) {
36
+ segments.push({ type: 'text', text: textBuffer });
37
+ }
38
+ return segments;
39
+ }
40
+ /**
41
+ * Build CV2 components for a table. Plain rows render as one TextDisplay with
42
+ * bold key-value lines. Rows with resolved button cells render as a TextDisplay
43
+ * plus an Action Row so wide tables do not violate Section's 1-3 text child
44
+ * limit. Large tables are split into multiple Containers using a dynamic
45
+ * component-budget check.
46
+ */
47
+ export function buildTableComponents(table, options = {}) {
48
+ const headers = table.header.map((cell) => {
49
+ return extractCellText(cell.tokens);
50
+ });
51
+ const rows = table.rows.map((row) => {
52
+ return buildRenderedRow({
53
+ headers,
54
+ row,
55
+ options,
56
+ });
57
+ });
58
+ const chunks = chunkRowsByComponentLimit({ rows });
59
+ return chunks.map((chunkRows) => {
60
+ const children = [];
61
+ for (let i = 0; i < chunkRows.length; i++) {
62
+ if (i > 0) {
63
+ children.push({
64
+ type: ComponentType.Separator,
65
+ divider: true,
66
+ spacing: SeparatorSpacingSize.Small,
67
+ });
68
+ }
69
+ children.push(...chunkRows[i].components);
70
+ }
71
+ const container = {
72
+ type: ComponentType.Container,
73
+ components: children,
74
+ };
75
+ return {
76
+ type: 'components',
77
+ components: [container],
78
+ };
79
+ });
80
+ }
81
+ function buildRenderedRow({ headers, row, options, }) {
82
+ const renderedCells = row.map((cell) => {
83
+ return renderTableCell({ cell, options });
84
+ });
85
+ const buttonCellCount = renderedCells.filter((cell) => {
86
+ return cell.type === 'button';
87
+ }).length;
88
+ if (buttonCellCount > 0) {
89
+ return buildButtonRow({
90
+ headers,
91
+ cells: renderedCells,
92
+ });
93
+ }
94
+ return buildTextRow({
95
+ headers,
96
+ cells: renderedCells,
97
+ });
98
+ }
99
+ function buildTextRow({ headers, cells, }) {
100
+ const lines = headers.map((key, index) => {
101
+ const cell = cells[index];
102
+ const value = cell ? getRenderedCellText({ cell }) : '';
103
+ return `**${key}** ${value}`;
104
+ });
105
+ return {
106
+ components: [
107
+ {
108
+ type: ComponentType.TextDisplay,
109
+ content: lines.join('\n'),
110
+ },
111
+ ],
112
+ componentCost: 1,
113
+ };
114
+ }
115
+ function buildButtonRow({ headers, cells, }) {
116
+ const buttonCells = cells.filter((cell) => {
117
+ return cell.type === 'button';
118
+ });
119
+ if (buttonCells.length === 0 || buttonCells.length > 5) {
120
+ return buildTextRow({ headers, cells });
121
+ }
122
+ const lines = headers.flatMap((header, index) => {
123
+ const cell = cells[index];
124
+ if (!cell || cell.type === 'button') {
125
+ return [];
126
+ }
127
+ return [`**${header}** ${cell.text}`];
128
+ });
129
+ if (lines.length === 0) {
130
+ return buildTextRow({ headers, cells });
131
+ }
132
+ const buttons = buttonCells.map((buttonCell) => {
133
+ return {
134
+ type: ComponentType.Button,
135
+ custom_id: buttonCell.customId,
136
+ label: buttonCell.label,
137
+ style: toButtonStyle({ variant: buttonCell.variant }),
138
+ disabled: buttonCell.disabled,
139
+ };
140
+ });
141
+ const actionRow = {
142
+ type: ComponentType.ActionRow,
143
+ components: buttons,
144
+ };
145
+ return {
146
+ components: [
147
+ {
148
+ type: ComponentType.TextDisplay,
149
+ content: lines.join('\n'),
150
+ },
151
+ actionRow,
152
+ ],
153
+ componentCost: 2 + buttons.length,
154
+ };
155
+ }
156
+ function chunkRowsByComponentLimit({ rows, }) {
157
+ const chunks = [];
158
+ let currentChunk = [];
159
+ let currentCost = 1;
160
+ for (const row of rows) {
161
+ const separatorCost = currentChunk.length > 0 ? 1 : 0;
162
+ const nextCost = currentCost + separatorCost + row.componentCost;
163
+ if (currentChunk.length > 0 && nextCost > MAX_COMPONENTS) {
164
+ chunks.push(currentChunk);
165
+ currentChunk = [row];
166
+ currentCost = 1 + row.componentCost;
167
+ continue;
168
+ }
169
+ currentChunk.push(row);
170
+ currentCost = nextCost;
171
+ }
172
+ if (currentChunk.length > 0) {
173
+ chunks.push(currentChunk);
174
+ }
175
+ return chunks;
176
+ }
177
+ function renderTableCell({ cell, options, }) {
178
+ const hasHtmlToken = cell.tokens.some((token) => {
179
+ return token.type === 'html';
180
+ });
181
+ if (!hasHtmlToken) {
182
+ return {
183
+ type: 'text',
184
+ text: extractCellText(cell.tokens),
185
+ };
186
+ }
187
+ const renderables = parseInlineHtmlRenderables({ html: cell.text });
188
+ if (renderables instanceof Error) {
189
+ return {
190
+ type: 'text',
191
+ text: extractRenderableText({ renderables: undefined, fallbackText: cell.text }),
192
+ };
193
+ }
194
+ const buttonRenderables = renderables.filter((renderable) => {
195
+ return renderable.type === 'button';
196
+ });
197
+ if (buttonRenderables.length !== 1) {
198
+ return {
199
+ type: 'text',
200
+ text: extractRenderableText({ renderables, fallbackText: cell.text }),
201
+ };
202
+ }
203
+ const hasNonWhitespaceText = renderables.some((renderable) => {
204
+ if (renderable.type !== 'text') {
205
+ return false;
206
+ }
207
+ return renderable.text.trim().length > 0;
208
+ });
209
+ if (hasNonWhitespaceText) {
210
+ return {
211
+ type: 'text',
212
+ text: extractRenderableText({ renderables, fallbackText: cell.text }),
213
+ };
214
+ }
215
+ const button = buttonRenderables[0];
216
+ const customId = options.resolveButtonCustomId?.({ button });
217
+ if (!customId || customId instanceof Error) {
218
+ return {
219
+ type: 'text',
220
+ text: button.label,
221
+ };
222
+ }
223
+ return {
224
+ type: 'button',
225
+ label: button.label,
226
+ customId,
227
+ variant: button.variant,
228
+ disabled: button.disabled,
229
+ };
230
+ }
231
+ function getRenderedCellText({ cell, }) {
232
+ if (cell.type === 'button') {
233
+ return cell.label;
234
+ }
235
+ return cell.text;
236
+ }
237
+ function extractRenderableText({ renderables, fallbackText, }) {
238
+ if (!renderables) {
239
+ return fallbackText.replace(/\s+/g, ' ').trim();
240
+ }
241
+ const text = renderables
242
+ .map((renderable) => {
243
+ if (renderable.type === 'button') {
244
+ return renderable.label;
245
+ }
246
+ return renderable.text;
247
+ })
248
+ .join(' ')
249
+ .replace(/\s+/g, ' ')
250
+ .trim();
251
+ if (text.length > 0) {
252
+ return text;
253
+ }
254
+ return fallbackText.replace(/\s+/g, ' ').trim();
255
+ }
256
+ function toButtonStyle({ variant, }) {
257
+ if (variant === 'primary') {
258
+ return ButtonStyle.Primary;
259
+ }
260
+ if (variant === 'success') {
261
+ return ButtonStyle.Success;
262
+ }
263
+ if (variant === 'danger') {
264
+ return ButtonStyle.Danger;
265
+ }
266
+ return ButtonStyle.Secondary;
267
+ }
268
+ function extractCellText(tokens) {
269
+ const parts = [];
270
+ for (const token of tokens) {
271
+ parts.push(extractTokenText(token));
272
+ }
273
+ return parts.join('').trim();
274
+ }
275
+ function extractTokenText(token) {
276
+ switch (token.type) {
277
+ case 'text':
278
+ case 'codespan':
279
+ case 'escape':
280
+ return token.text;
281
+ case 'link':
282
+ return token.href;
283
+ case 'image':
284
+ return token.href;
285
+ case 'strong':
286
+ case 'em':
287
+ case 'del':
288
+ return token.tokens ? extractCellText(token.tokens) : token.text;
289
+ case 'br':
290
+ return ' ';
291
+ default: {
292
+ const tokenAny = token;
293
+ if (tokenAny.tokens && Array.isArray(tokenAny.tokens)) {
294
+ return extractCellText(tokenAny.tokens);
295
+ }
296
+ if (typeof tokenAny.text === 'string') {
297
+ return tokenAny.text;
298
+ }
299
+ return '';
300
+ }
301
+ }
302
+ }
@@ -0,0 +1,308 @@
1
+ import { test, expect, describe } from 'vitest';
2
+ import { splitTablesFromMarkdown, buildTableComponents, } from './format-tables.js';
3
+ import { Lexer } from 'marked';
4
+ function parseTable(markdown) {
5
+ const lexer = new Lexer();
6
+ const tokens = lexer.lex(markdown);
7
+ return tokens.find((t) => t.type === 'table');
8
+ }
9
+ /** Extract the first container's children from buildTableComponents result */
10
+ function getContainerChildren(segments) {
11
+ const seg = segments[0];
12
+ if (seg.type !== 'components') {
13
+ throw new Error('Expected components segment');
14
+ }
15
+ const container = seg.components[0];
16
+ return container.components;
17
+ }
18
+ describe('buildTableComponents', () => {
19
+ test('builds container with key-value TextDisplays', () => {
20
+ const table = parseTable(`| Name | Age |
21
+ | --- | --- |
22
+ | Alice | 30 |
23
+ | Bob | 25 |`);
24
+ const result = buildTableComponents(table);
25
+ expect(result).toMatchInlineSnapshot(`
26
+ [
27
+ {
28
+ "components": [
29
+ {
30
+ "components": [
31
+ {
32
+ "content": "**Name** Alice
33
+ **Age** 30",
34
+ "type": 10,
35
+ },
36
+ {
37
+ "divider": true,
38
+ "spacing": 1,
39
+ "type": 14,
40
+ },
41
+ {
42
+ "content": "**Name** Bob
43
+ **Age** 25",
44
+ "type": 10,
45
+ },
46
+ ],
47
+ "type": 17,
48
+ },
49
+ ],
50
+ "type": "components",
51
+ },
52
+ ]
53
+ `);
54
+ });
55
+ test('adds separators between row groups', () => {
56
+ const table = parseTable(`| Key | Value |
57
+ | --- | --- |
58
+ | a | 1 |
59
+ | b | 2 |
60
+ | c | 3 |`);
61
+ const result = buildTableComponents(table);
62
+ const types = getContainerChildren(result).map((c) => c.type);
63
+ // type 10 = TextDisplay, type 14 = Separator
64
+ expect(types).toMatchInlineSnapshot(`
65
+ [
66
+ 10,
67
+ 14,
68
+ 10,
69
+ 14,
70
+ 10,
71
+ ]
72
+ `);
73
+ });
74
+ test('single-row table has one TextDisplay, no separators', () => {
75
+ const table = parseTable(`| Method | Endpoint |
76
+ | --- | --- |
77
+ | GET | /api/users |`);
78
+ const result = buildTableComponents(table);
79
+ const children = getContainerChildren(result);
80
+ expect(children).toHaveLength(1);
81
+ expect(children[0].type).toBe(10);
82
+ expect(children[0].content).toMatchInlineSnapshot(`
83
+ "**Method** GET
84
+ **Endpoint** /api/users"
85
+ `);
86
+ });
87
+ test('splits large table into multiple container segments', () => {
88
+ // 25 rows: exceeds 19 rows per container, so splits into 2 containers
89
+ const headers = '| A | B |';
90
+ const sep = '| --- | --- |';
91
+ const rows = Array.from({ length: 25 }, (_, i) => {
92
+ return `| ${i}a | ${i}b |`;
93
+ }).join('\n');
94
+ const table = parseTable(`${headers}\n${sep}\n${rows}`);
95
+ const result = buildTableComponents(table);
96
+ expect(result).toHaveLength(2);
97
+ expect(result[0].type).toBe('components');
98
+ expect(result[1].type).toBe('components');
99
+ // First container has 20 rows (20 TDs + 19 seps = 39 children)
100
+ const firstChildren = getContainerChildren([result[0]]);
101
+ expect(firstChildren).toHaveLength(20 + 19);
102
+ // Second container has 5 rows (5 TDs + 4 seps = 9 children)
103
+ const secondChildren = getContainerChildren([result[1]]);
104
+ expect(secondChildren).toHaveLength(5 + 4);
105
+ });
106
+ test('strips formatting from cells', () => {
107
+ const table = parseTable(`| Header | Value |
108
+ | --- | --- |
109
+ | **Bold text** | Normal |
110
+ | *Italic* | \`code\` |`);
111
+ const result = buildTableComponents(table);
112
+ const children = getContainerChildren(result);
113
+ expect(children[0].content).toMatchInlineSnapshot(`
114
+ "**Header** Bold text
115
+ **Value** Normal"
116
+ `);
117
+ });
118
+ test('renders button cells as action rows inside the container', () => {
119
+ const table = parseTable(`| Name | Action |
120
+ | --- | --- |
121
+ | feature-a | <button id="delete-a" variant="secondary">Delete</button> |`);
122
+ const result = buildTableComponents(table, {
123
+ resolveButtonCustomId: ({ button }) => {
124
+ return `html_action:${button.id}`;
125
+ },
126
+ });
127
+ expect(result).toMatchInlineSnapshot(`
128
+ [
129
+ {
130
+ "components": [
131
+ {
132
+ "components": [
133
+ {
134
+ "content": "**Name** feature-a",
135
+ "type": 10,
136
+ },
137
+ {
138
+ "components": [
139
+ {
140
+ "custom_id": "html_action:delete-a",
141
+ "disabled": false,
142
+ "label": "Delete",
143
+ "style": 2,
144
+ "type": 2,
145
+ },
146
+ ],
147
+ "type": 1,
148
+ },
149
+ ],
150
+ "type": 17,
151
+ },
152
+ ],
153
+ "type": "components",
154
+ },
155
+ ]
156
+ `);
157
+ });
158
+ test('falls back to button text when no resolver is provided', () => {
159
+ const table = parseTable(`| Name | Action |
160
+ | --- | --- |
161
+ | feature-a | <button id="delete-a" variant="secondary">Delete</button> |`);
162
+ const result = buildTableComponents(table);
163
+ expect(result).toMatchInlineSnapshot(`
164
+ [
165
+ {
166
+ "components": [
167
+ {
168
+ "components": [
169
+ {
170
+ "content": "**Name** feature-a
171
+ **Action** Delete",
172
+ "type": 10,
173
+ },
174
+ ],
175
+ "type": 17,
176
+ },
177
+ ],
178
+ "type": "components",
179
+ },
180
+ ]
181
+ `);
182
+ });
183
+ test('renders wide rows with buttons without using sections', () => {
184
+ const table = parseTable(`| Thread | Name | Status | Created | Folder | Action |
185
+ | --- | --- | --- | --- | --- | --- |
186
+ | thread | feature-a | merged | 1m ago | /tmp/feature-a | <button id="delete-a" variant="secondary">Delete</button> |`);
187
+ const result = buildTableComponents(table, {
188
+ resolveButtonCustomId: ({ button }) => {
189
+ return `html_action:${button.id}`;
190
+ },
191
+ });
192
+ expect(result).toMatchInlineSnapshot(`
193
+ [
194
+ {
195
+ "components": [
196
+ {
197
+ "components": [
198
+ {
199
+ "content": "**Thread** thread
200
+ **Name** feature-a
201
+ **Status** merged
202
+ **Created** 1m ago
203
+ **Folder** /tmp/feature-a",
204
+ "type": 10,
205
+ },
206
+ {
207
+ "components": [
208
+ {
209
+ "custom_id": "html_action:delete-a",
210
+ "disabled": false,
211
+ "label": "Delete",
212
+ "style": 2,
213
+ "type": 2,
214
+ },
215
+ ],
216
+ "type": 1,
217
+ },
218
+ ],
219
+ "type": 17,
220
+ },
221
+ ],
222
+ "type": "components",
223
+ },
224
+ ]
225
+ `);
226
+ });
227
+ });
228
+ describe('splitTablesFromMarkdown', () => {
229
+ test('returns single text segment for content without tables', () => {
230
+ const result = splitTablesFromMarkdown('Just some text.\n\nMore text.');
231
+ expect(result).toHaveLength(1);
232
+ expect(result[0].type).toBe('text');
233
+ });
234
+ test('returns single components segment for table-only content', () => {
235
+ const result = splitTablesFromMarkdown(`| A | B |
236
+ | --- | --- |
237
+ | 1 | 2 |`);
238
+ expect(result).toHaveLength(1);
239
+ expect(result[0].type).toBe('components');
240
+ });
241
+ test('splits text before and after table into separate segments', () => {
242
+ const result = splitTablesFromMarkdown(`Text before.
243
+
244
+ | Key | Value |
245
+ | --- | --- |
246
+ | a | 1 |
247
+
248
+ Text after.`);
249
+ expect(result).toHaveLength(3);
250
+ expect(result[0].type).toBe('text');
251
+ expect(result[1].type).toBe('components');
252
+ expect(result[2].type).toBe('text');
253
+ });
254
+ test('handles multiple tables with text between', () => {
255
+ const result = splitTablesFromMarkdown(`First table:
256
+
257
+ | A | B |
258
+ | --- | --- |
259
+ | 1 | 2 |
260
+
261
+ Middle text.
262
+
263
+ | X | Y |
264
+ | --- | --- |
265
+ | a | b |`);
266
+ expect(result).toHaveLength(4);
267
+ expect(result.map((s) => s.type)).toMatchInlineSnapshot(`
268
+ [
269
+ "text",
270
+ "components",
271
+ "text",
272
+ "components",
273
+ ]
274
+ `);
275
+ });
276
+ test('splits oversized table into multiple component segments', () => {
277
+ const headers = '| A | B |';
278
+ const sep = '| --- | --- |';
279
+ const rows = Array.from({ length: 25 }, (_, i) => {
280
+ return `| ${i}a | ${i}b |`;
281
+ }).join('\n');
282
+ const result = splitTablesFromMarkdown(`${headers}\n${sep}\n${rows}`);
283
+ // 25 rows splits into 2 container segments
284
+ expect(result).toHaveLength(2);
285
+ expect(result.every((s) => s.type === 'components')).toBe(true);
286
+ });
287
+ test('preserves code blocks alongside tables', () => {
288
+ const result = splitTablesFromMarkdown(`Some code:
289
+
290
+ \`\`\`js
291
+ const x = 1
292
+ \`\`\`
293
+
294
+ | Key | Value |
295
+ | --- | --- |
296
+ | a | 1 |
297
+
298
+ Done.`);
299
+ const types = result.map((s) => s.type);
300
+ expect(types).toMatchInlineSnapshot(`
301
+ [
302
+ "text",
303
+ "components",
304
+ "text",
305
+ ]
306
+ `);
307
+ });
308
+ });
@@ -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
+ }