@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,595 @@
1
+ // Filesystem -> Discord sync.
2
+ // Reads markdown files and creates/updates/deletes forum threads to match.
3
+ // Handles upsert logic: new files create threads, existing files update them.
4
+
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+ import { MessageFlags, type Client, type ForumChannel } from 'discord.js'
8
+ import { createLogger } from '../logger.js'
9
+ import {
10
+ appendProjectChannelFooter,
11
+ extractStarterContent,
12
+ getStringValue,
13
+ parseFrontmatter,
14
+ toStringArray,
15
+ } from './markdown.js'
16
+ import { resolveForumChannel } from './discord-operations.js'
17
+ import { syncSingleThreadToFile } from './sync-to-files.js'
18
+ import {
19
+ ForumSyncOperationError,
20
+ shouldIgnorePath,
21
+ type ForumFileSyncResult,
22
+ type ForumRuntimeState,
23
+ type SyncFilesToForumOptions,
24
+ } from './types.js'
25
+
26
+ const forumLogger = createLogger('FORUM')
27
+
28
+ // Fields managed by forum sync that should not be set by external writers (e.g. AI model).
29
+ // If a file has never been synced (no lastSyncedAt), these fields are stripped to prevent
30
+ // model-invented values from causing sync errors (e.g. fake threadId -> fetch fails,
31
+ // future lastSyncedAt -> file permanently skipped).
32
+ const SYSTEM_MANAGED_FIELDS = [
33
+ 'threadId',
34
+ 'forumChannelId',
35
+ 'lastSyncedAt',
36
+ 'lastMessageId',
37
+ 'messageCount',
38
+ 'author',
39
+ 'authorId',
40
+ 'createdAt',
41
+ 'lastUpdated',
42
+ 'project',
43
+ 'projectChannelId',
44
+ ] as const
45
+
46
+ /** Check that a value is a valid ISO date string that isn't in the future. */
47
+ function isValidPastIsoDate({ value }: { value: unknown }): boolean {
48
+ if (typeof value !== 'string') return false
49
+ const parsed = Date.parse(value)
50
+ if (!Number.isFinite(parsed)) return false
51
+ return parsed <= Date.now()
52
+ }
53
+
54
+ function stripSystemFieldsFromUnsyncedFile({
55
+ frontmatter,
56
+ }: {
57
+ frontmatter: Record<string, unknown>
58
+ }): Record<string, unknown> {
59
+ if (isValidPastIsoDate({ value: frontmatter.lastSyncedAt }))
60
+ return frontmatter
61
+ const cleaned = { ...frontmatter }
62
+ for (const field of SYSTEM_MANAGED_FIELDS) {
63
+ delete cleaned[field]
64
+ }
65
+ return cleaned
66
+ }
67
+
68
+ function isValidDiscordSnowflake({ value }: { value: string }) {
69
+ return /^\d{17,20}$/.test(value)
70
+ }
71
+
72
+ async function collectMarkdownEntries({
73
+ dir,
74
+ outputDir,
75
+ }: {
76
+ dir: string
77
+ outputDir: string
78
+ }): Promise<Array<{ filePath: string; subfolder?: string }>> {
79
+ const exists = await fs.promises
80
+ .access(dir)
81
+ .then(() => true)
82
+ .catch(() => false)
83
+ if (!exists) return []
84
+
85
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true })
86
+ const relativeSub = path.relative(outputDir, dir)
87
+ const subfolder = relativeSub && relativeSub !== '.' ? relativeSub : undefined
88
+
89
+ const markdownFiles: Array<{ filePath: string; subfolder?: string }> = entries
90
+ .filter((entry) => {
91
+ return entry.isFile() && entry.name.endsWith('.md')
92
+ })
93
+ .map((entry) => {
94
+ return { filePath: path.join(dir, entry.name), subfolder }
95
+ })
96
+
97
+ const nestedEntries = await Promise.all(
98
+ entries
99
+ .filter((entry) => {
100
+ return entry.isDirectory()
101
+ })
102
+ .map(async (entry) => {
103
+ return await collectMarkdownEntries({
104
+ dir: path.join(dir, entry.name),
105
+ outputDir,
106
+ })
107
+ }),
108
+ )
109
+
110
+ return [...markdownFiles, ...nestedEntries.flat()]
111
+ }
112
+
113
+ function resolveTagIds({
114
+ forumChannel,
115
+ tagNames,
116
+ }: {
117
+ forumChannel: ForumChannel
118
+ tagNames: string[]
119
+ }): string[] {
120
+ if (tagNames.length === 0) return []
121
+ const normalizedWanted = new Set(
122
+ tagNames.map((tag) => tag.toLowerCase().trim()),
123
+ )
124
+ return forumChannel.availableTags
125
+ .filter((tag) => normalizedWanted.has(tag.name.toLowerCase().trim()))
126
+ .map((tag) => tag.id)
127
+ }
128
+
129
+ /** Ensure all requested tag names exist on the forum channel, creating any missing ones. */
130
+ async function ensureForumTags({
131
+ forumChannel,
132
+ tagNames,
133
+ }: {
134
+ forumChannel: ForumChannel
135
+ tagNames: string[]
136
+ }): Promise<void> {
137
+ if (tagNames.length === 0) return
138
+ const existingNames = new Set(
139
+ forumChannel.availableTags.map((tag) => tag.name.toLowerCase().trim()),
140
+ )
141
+ const missing = tagNames.filter(
142
+ (name) => !existingNames.has(name.toLowerCase().trim()),
143
+ )
144
+ if (missing.length === 0) return
145
+ // Discord forums allow up to 20 tags
146
+ const available = forumChannel.availableTags
147
+ if (available.length + missing.length > 20) return
148
+ await forumChannel
149
+ .setAvailableTags(
150
+ [...available, ...missing.map((name) => ({ name }))],
151
+ `Auto-create tags: ${missing.join(', ')}`,
152
+ )
153
+ .catch((cause) => {
154
+ forumLogger.warn(
155
+ `Failed to create forum tags [${missing.join(', ')}]: ${cause instanceof Error ? cause.message : cause}`,
156
+ )
157
+ })
158
+ }
159
+
160
+ function hasTagName({ tags, tagName }: { tags: string[]; tagName: string }) {
161
+ return tags.some(
162
+ (tag) => tag.toLowerCase().trim() === tagName.toLowerCase().trim(),
163
+ )
164
+ }
165
+
166
+ async function upsertThreadFromFile({
167
+ discordClient,
168
+ forumChannel,
169
+ filePath,
170
+ runtimeState,
171
+ subfolder,
172
+ project,
173
+ projectChannelId,
174
+ }: {
175
+ discordClient: Client
176
+ forumChannel: ForumChannel
177
+ filePath: string
178
+ runtimeState?: ForumRuntimeState
179
+ subfolder?: string
180
+ project?: string
181
+ projectChannelId?: string
182
+ }): Promise<'created' | 'updated' | 'skipped' | ForumSyncOperationError> {
183
+ if (!fs.existsSync(filePath)) return 'skipped'
184
+
185
+ const content = await fs.promises
186
+ .readFile(filePath, 'utf8')
187
+ .catch((cause) => {
188
+ return new ForumSyncOperationError({
189
+ forumChannelId: forumChannel.id,
190
+ reason: `failed to read ${filePath}`,
191
+ cause,
192
+ })
193
+ })
194
+ if (content instanceof Error) return content
195
+
196
+ const parsed = parseFrontmatter({ markdown: content })
197
+ const frontmatter = stripSystemFieldsFromUnsyncedFile({
198
+ frontmatter: parsed.frontmatter,
199
+ })
200
+ const rawThreadId = getStringValue({ value: frontmatter.threadId })
201
+ const threadId =
202
+ rawThreadId && isValidDiscordSnowflake({ value: rawThreadId })
203
+ ? rawThreadId
204
+ : ''
205
+ const title =
206
+ getStringValue({ value: frontmatter.title }) ||
207
+ path.basename(filePath, '.md')
208
+ const tags = toStringArray({ value: frontmatter.tags })
209
+ const normalizedSubfolder = subfolder?.replaceAll('\\', '/').toLowerCase()
210
+ const isGlobalSubfolder = Boolean(
211
+ normalizedSubfolder &&
212
+ (normalizedSubfolder === 'global' ||
213
+ normalizedSubfolder.startsWith('global/')),
214
+ )
215
+ const tagsWithScope =
216
+ isGlobalSubfolder && !hasTagName({ tags, tagName: 'global' })
217
+ ? [...tags, 'global']
218
+ : tags
219
+ // Add project name as a forum tag if derived from subfolder
220
+ const allTags =
221
+ project && !hasTagName({ tags: tagsWithScope, tagName: project })
222
+ ? [...tagsWithScope, project]
223
+ : tagsWithScope
224
+ const starterContent = extractStarterContent({ body: parsed.body })
225
+ // Resolve fallback BEFORE appending footer so an empty body doesn't
226
+ // produce a message that is just the channel footer.
227
+ const baseContent = starterContent || title || 'Untitled post'
228
+ const safeStarterContent = appendProjectChannelFooter({
229
+ content: baseContent,
230
+ projectChannelId,
231
+ })
232
+
233
+ const stat = await fs.promises.stat(filePath).catch((cause) => {
234
+ return new ForumSyncOperationError({
235
+ forumChannelId: forumChannel.id,
236
+ reason: `failed to stat ${filePath}`,
237
+ cause,
238
+ })
239
+ })
240
+ if (stat instanceof Error) return stat
241
+
242
+ // Skip if file hasn't been modified since last sync
243
+ const lastSyncedAt = Date.parse(
244
+ getStringValue({ value: frontmatter.lastSyncedAt }),
245
+ )
246
+ if (Number.isFinite(lastSyncedAt) && stat.mtimeMs <= lastSyncedAt)
247
+ return 'skipped'
248
+
249
+ await ensureForumTags({ forumChannel, tagNames: allTags })
250
+ const tagIds = resolveTagIds({ forumChannel, tagNames: allTags })
251
+
252
+ // No threadId in frontmatter -> create a new thread
253
+ if (!threadId) {
254
+ return await createNewThread({
255
+ forumChannel,
256
+ filePath,
257
+ title,
258
+ safeStarterContent,
259
+ tagIds,
260
+ runtimeState,
261
+ subfolder,
262
+ project,
263
+ projectChannelId,
264
+ })
265
+ }
266
+
267
+ // Thread exists -> update it
268
+ return await updateExistingThread({
269
+ discordClient,
270
+ forumChannel,
271
+ filePath,
272
+ threadId,
273
+ title,
274
+ safeStarterContent,
275
+ tagIds,
276
+ runtimeState,
277
+ subfolder,
278
+ project,
279
+ projectChannelId,
280
+ })
281
+ }
282
+
283
+ async function createNewThread({
284
+ forumChannel,
285
+ filePath,
286
+ title,
287
+ safeStarterContent,
288
+ tagIds,
289
+ runtimeState,
290
+ subfolder,
291
+ project,
292
+ projectChannelId,
293
+ }: {
294
+ forumChannel: ForumChannel
295
+ filePath: string
296
+ title: string
297
+ safeStarterContent: string
298
+ tagIds: string[]
299
+ runtimeState?: ForumRuntimeState
300
+ subfolder?: string
301
+ project?: string
302
+ projectChannelId?: string
303
+ }): Promise<'created' | ForumSyncOperationError> {
304
+ const created = await forumChannel.threads
305
+ .create({
306
+ name: title.slice(0, 100) || 'Untitled post',
307
+ message: {
308
+ content: safeStarterContent.slice(0, 2_000),
309
+ flags: MessageFlags.SuppressEmbeds,
310
+ },
311
+ appliedTags: tagIds,
312
+ })
313
+ .catch(
314
+ (cause) =>
315
+ new ForumSyncOperationError({
316
+ forumChannelId: forumChannel.id,
317
+ reason: `failed creating thread from ${filePath}`,
318
+ cause,
319
+ }),
320
+ )
321
+ if (created instanceof Error) return created
322
+
323
+ // Re-sync the file to get the new threadId in frontmatter.
324
+ // outputDir is path.dirname(filePath) which already includes the subfolder,
325
+ // so we don't pass subfolder again to avoid double-nesting.
326
+ const syncResult = await syncSingleThreadToFile({
327
+ thread: created,
328
+ forumChannel,
329
+ outputDir: path.dirname(filePath),
330
+ runtimeState,
331
+ previousFilePath: filePath,
332
+ project,
333
+ projectChannelId,
334
+ })
335
+ if (syncResult instanceof Error) return syncResult
336
+ return 'created'
337
+ }
338
+
339
+ async function updateExistingThread({
340
+ discordClient,
341
+ forumChannel,
342
+ filePath,
343
+ threadId,
344
+ title,
345
+ safeStarterContent,
346
+ tagIds,
347
+ runtimeState,
348
+ subfolder,
349
+ project,
350
+ projectChannelId,
351
+ }: {
352
+ discordClient: Client
353
+ forumChannel: ForumChannel
354
+ filePath: string
355
+ threadId: string
356
+ title: string
357
+ safeStarterContent: string
358
+ tagIds: string[]
359
+ runtimeState?: ForumRuntimeState
360
+ subfolder?: string
361
+ project?: string
362
+ projectChannelId?: string
363
+ }): Promise<'updated' | ForumSyncOperationError> {
364
+ const fetchedChannel = await discordClient.channels.fetch(threadId).catch(
365
+ (cause) =>
366
+ new ForumSyncOperationError({
367
+ forumChannelId: forumChannel.id,
368
+ reason: `failed fetching thread ${threadId}`,
369
+ cause,
370
+ }),
371
+ )
372
+ if (fetchedChannel instanceof Error) return fetchedChannel
373
+
374
+ if (
375
+ !fetchedChannel ||
376
+ !fetchedChannel.isThread() ||
377
+ fetchedChannel.parentId !== forumChannel.id
378
+ ) {
379
+ return new ForumSyncOperationError({
380
+ forumChannelId: forumChannel.id,
381
+ reason: `thread ${threadId} not found in forum`,
382
+ })
383
+ }
384
+
385
+ const updateResult = await fetchedChannel
386
+ .edit({
387
+ name: title.slice(0, 100) || fetchedChannel.name,
388
+ appliedTags: tagIds,
389
+ })
390
+ .catch(
391
+ (cause) =>
392
+ new ForumSyncOperationError({
393
+ forumChannelId: forumChannel.id,
394
+ reason: `failed editing thread ${threadId}`,
395
+ cause,
396
+ }),
397
+ )
398
+ if (updateResult instanceof Error) return updateResult
399
+
400
+ const starterMessage = await fetchedChannel
401
+ .fetchStarterMessage()
402
+ .catch((cause) => {
403
+ return new ForumSyncOperationError({
404
+ forumChannelId: forumChannel.id,
405
+ reason: `failed fetching starter message for ${threadId}`,
406
+ cause,
407
+ })
408
+ })
409
+ if (starterMessage instanceof Error) return starterMessage
410
+
411
+ if (starterMessage && starterMessage.content !== safeStarterContent) {
412
+ const editResult = await starterMessage
413
+ .edit({
414
+ content: safeStarterContent.slice(0, 2_000),
415
+ flags: MessageFlags.SuppressEmbeds,
416
+ })
417
+ .catch(
418
+ (cause) =>
419
+ new ForumSyncOperationError({
420
+ forumChannelId: forumChannel.id,
421
+ reason: `failed editing starter message for ${threadId}`,
422
+ cause,
423
+ }),
424
+ )
425
+ if (editResult instanceof Error) return editResult
426
+ }
427
+
428
+ // Re-sync the file to update frontmatter with latest state.
429
+ // outputDir is path.dirname(filePath) which already includes the subfolder.
430
+ const syncResult = await syncSingleThreadToFile({
431
+ thread: fetchedChannel,
432
+ forumChannel,
433
+ outputDir: path.dirname(filePath),
434
+ runtimeState,
435
+ project,
436
+ projectChannelId,
437
+ })
438
+ if (syncResult instanceof Error) return syncResult
439
+ return 'updated'
440
+ }
441
+
442
+ async function deleteThreadFromFilePath({
443
+ discordClient,
444
+ forumChannel,
445
+ filePath,
446
+ }: {
447
+ discordClient: Client
448
+ forumChannel: ForumChannel
449
+ filePath: string
450
+ }): Promise<void | ForumSyncOperationError> {
451
+ const filename = path.basename(filePath, '.md')
452
+ if (!/^\d+$/.test(filename)) return
453
+
454
+ const threadId = filename
455
+ const fetchedChannel = await discordClient.channels.fetch(threadId).catch(
456
+ (cause) =>
457
+ new ForumSyncOperationError({
458
+ forumChannelId: forumChannel.id,
459
+ reason: `failed fetching deleted thread ${threadId}`,
460
+ cause,
461
+ }),
462
+ )
463
+ if (fetchedChannel instanceof Error) return fetchedChannel
464
+
465
+ if (
466
+ !fetchedChannel ||
467
+ !fetchedChannel.isThread() ||
468
+ fetchedChannel.parentId !== forumChannel.id
469
+ ) {
470
+ return
471
+ }
472
+
473
+ const deleteResult = await fetchedChannel
474
+ .delete('Deleted from forum sync markdown directory')
475
+ .catch(
476
+ (cause) =>
477
+ new ForumSyncOperationError({
478
+ forumChannelId: forumChannel.id,
479
+ reason: `failed deleting thread ${threadId}`,
480
+ cause,
481
+ }),
482
+ )
483
+ if (deleteResult instanceof Error) return deleteResult
484
+ }
485
+
486
+ export async function syncFilesToForum({
487
+ discordClient,
488
+ forumChannelId,
489
+ outputDir,
490
+ runtimeState,
491
+ changedFilePaths,
492
+ deletedFilePaths,
493
+ }: SyncFilesToForumOptions) {
494
+ const forumChannel = await resolveForumChannel({
495
+ discordClient,
496
+ forumChannelId,
497
+ })
498
+ if (forumChannel instanceof Error) return forumChannel
499
+
500
+ // When changedFilePaths is provided (from file watcher), derive subfolder from path.
501
+ // Otherwise, recursively scan all markdown files in outputDir.
502
+ const changedEntries: Array<{ filePath: string; subfolder?: string }> =
503
+ changedFilePaths
504
+ ? changedFilePaths.map((filePath) => {
505
+ const rel = path.relative(outputDir, path.dirname(filePath))
506
+ const subfolder = rel && rel !== '.' ? rel : undefined
507
+ return { filePath, subfolder }
508
+ })
509
+ : await collectMarkdownEntries({ dir: outputDir, outputDir })
510
+
511
+ // Resolve channel names for subfolders (each subfolder name is a Discord channel ID).
512
+ // Cache resolutions to avoid redundant API calls.
513
+ const channelNameCache = new Map<string, string | null>()
514
+ const resolveChannelName = async (
515
+ channelId: string,
516
+ ): Promise<string | null> => {
517
+ if (channelNameCache.has(channelId)) return channelNameCache.get(channelId)!
518
+ const channel = await discordClient.channels
519
+ .fetch(channelId)
520
+ .catch(() => null)
521
+ const name =
522
+ channel && 'name' in channel && typeof channel.name === 'string'
523
+ ? channel.name
524
+ : null
525
+ channelNameCache.set(channelId, name)
526
+ return name
527
+ }
528
+
529
+ const result: ForumFileSyncResult = {
530
+ created: 0,
531
+ updated: 0,
532
+ skipped: 0,
533
+ deleted: 0,
534
+ }
535
+
536
+ for (const { filePath, subfolder } of changedEntries) {
537
+ if (!filePath.endsWith('.md')) continue
538
+ if (runtimeState && shouldIgnorePath({ runtimeState, filePath })) {
539
+ result.skipped += 1
540
+ continue
541
+ }
542
+
543
+ // Derive project info from subfolder (subfolder name is the channel ID).
544
+ // Only use subfolder as channelId if it looks like a valid Discord snowflake
545
+ // to prevent nested paths or arbitrary folder names from being treated as IDs.
546
+ const projectChannelId =
547
+ subfolder && isValidDiscordSnowflake({ value: subfolder })
548
+ ? subfolder
549
+ : undefined
550
+ const project = projectChannelId
551
+ ? (await resolveChannelName(projectChannelId)) || undefined
552
+ : undefined
553
+
554
+ const upsertResult = await upsertThreadFromFile({
555
+ discordClient,
556
+ forumChannel,
557
+ filePath,
558
+ runtimeState,
559
+ subfolder,
560
+ project,
561
+ projectChannelId,
562
+ })
563
+ // Keep syncing other files even if one file has stale/bad metadata
564
+ // (e.g. threadId that no longer exists). A single bad file should not
565
+ // block watcher startup for the whole memory directory.
566
+ if (upsertResult instanceof Error) {
567
+ forumLogger.warn(`Skipping ${filePath}: ${upsertResult.message}`)
568
+ result.skipped += 1
569
+ continue
570
+ }
571
+
572
+ if (upsertResult === 'created') {
573
+ result.created += 1
574
+ } else if (upsertResult === 'updated') {
575
+ result.updated += 1
576
+ } else {
577
+ result.skipped += 1
578
+ }
579
+ }
580
+
581
+ for (const filePath of deletedFilePaths || []) {
582
+ const deleteResult = await deleteThreadFromFilePath({
583
+ discordClient,
584
+ forumChannel,
585
+ filePath,
586
+ })
587
+ if (deleteResult instanceof Error) {
588
+ forumLogger.warn(`Skipping delete ${filePath}: ${deleteResult.message}`)
589
+ continue
590
+ }
591
+ result.deleted += 1
592
+ }
593
+
594
+ return result
595
+ }