@otto-assistant/otto 0.1.2 → 0.7.15

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