@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,199 @@
1
+ // End-to-end test using discord-digital-twin + real Kimaki bot runtime.
2
+ // Verifies onboarding channel creation, message -> thread creation, and assistant reply.
3
+
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+ import { expect, test } from 'vitest'
7
+ import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js'
8
+ import { DigitalDiscord } from 'discord-digital-twin/src'
9
+ import { CachedOpencodeProviderProxy } from 'opencode-cached-provider'
10
+ import { setDataDir } from './config.js'
11
+ import { startDiscordBot } from './discord-bot.js'
12
+ import {
13
+ setBotToken,
14
+ initDatabase,
15
+ closeDatabase,
16
+ setChannelDirectory,
17
+ } from './database.js'
18
+ import { startHranaServer, stopHranaServer } from './hrana-server.js'
19
+ import { cleanupTestSessions, chooseLockPort, initTestGitRepo } from './test-utils.js'
20
+ import { stopOpencodeServer } from './opencode.js'
21
+
22
+ const geminiApiKey =
23
+ process.env['GEMINI_API_KEY'] ||
24
+ process.env['GOOGLE_GENERATIVE_AI_API_KEY'] ||
25
+ ''
26
+ const geminiModel = process.env['GEMINI_FLASH_MODEL'] || 'gemini-2.5-flash'
27
+ const e2eTest = geminiApiKey.length > 0 ? test : test.skip
28
+
29
+ function createRunDirectories() {
30
+ const root = path.resolve(process.cwd(), 'tmp', 'kimaki-digital-twin-e2e')
31
+ fs.mkdirSync(root, { recursive: true })
32
+
33
+ const dataDir = fs.mkdtempSync(path.join(root, 'data-'))
34
+ const projectDirectory = path.join(root, 'project')
35
+ const providerCacheDbPath = path.join(root, 'provider-cache.db')
36
+ fs.mkdirSync(projectDirectory, { recursive: true })
37
+ initTestGitRepo(projectDirectory)
38
+
39
+ return {
40
+ root,
41
+ dataDir,
42
+ projectDirectory,
43
+ providerCacheDbPath,
44
+ }
45
+ }
46
+
47
+ function createDiscordJsClient({ restUrl }: { restUrl: string }) {
48
+ return new Client({
49
+ intents: [
50
+ GatewayIntentBits.Guilds,
51
+ GatewayIntentBits.GuildMessages,
52
+ GatewayIntentBits.MessageContent,
53
+ GatewayIntentBits.GuildVoiceStates,
54
+ ],
55
+ partials: [
56
+ Partials.Channel,
57
+ Partials.Message,
58
+ Partials.User,
59
+ Partials.ThreadMember,
60
+ ],
61
+ rest: {
62
+ api: restUrl,
63
+ version: '10',
64
+ },
65
+ })
66
+ }
67
+
68
+ e2eTest(
69
+ 'onboarding then message creates thread and assistant reply via digital twin',
70
+ async () => {
71
+ const testStartTime = Date.now()
72
+ const directories = createRunDirectories()
73
+ const lockPort = chooseLockPort({ key: 'kimaki-digital-twin-e2e' })
74
+
75
+ process.env['KIMAKI_LOCK_PORT'] = String(lockPort)
76
+ setDataDir(directories.dataDir)
77
+
78
+ const proxy = new CachedOpencodeProviderProxy({
79
+ cacheDbPath: directories.providerCacheDbPath,
80
+ targetBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',
81
+ apiKey: geminiApiKey,
82
+ cacheMethods: ['POST'],
83
+ })
84
+
85
+ const testUserId = '100000000000000777'
86
+ const textChannelId = '100000000000000778'
87
+ const digitalDiscordDbPath = path.join(
88
+ directories.dataDir,
89
+ 'digital-discord.db',
90
+ )
91
+ const discord = new DigitalDiscord({
92
+ guild: {
93
+ name: 'Kimaki E2E Guild',
94
+ ownerId: testUserId,
95
+ },
96
+ channels: [
97
+ {
98
+ id: textChannelId,
99
+ name: 'kimaki-e2e',
100
+ type: ChannelType.GuildText,
101
+ },
102
+ ],
103
+ users: [
104
+ {
105
+ id: testUserId,
106
+ username: 'e2e-user',
107
+ },
108
+ ],
109
+ dbUrl: `file:${digitalDiscordDbPath}`,
110
+ })
111
+
112
+ let botClient: Client | null = null
113
+
114
+ try {
115
+ await Promise.all([proxy.start(), discord.start()])
116
+
117
+ const opencodeConfig = proxy.buildOpencodeConfig({
118
+ providerName: 'cached-google',
119
+ providerNpm: '@ai-sdk/google',
120
+ model: geminiModel,
121
+ smallModel: geminiModel,
122
+ })
123
+ fs.writeFileSync(
124
+ path.join(directories.projectDirectory, 'opencode.json'),
125
+ JSON.stringify(opencodeConfig, null, 2),
126
+ )
127
+
128
+ const dbPath = path.join(directories.dataDir, 'discord-sessions.db')
129
+ const hranaResult = await startHranaServer({ dbPath })
130
+ if (hranaResult instanceof Error) {
131
+ throw hranaResult
132
+ }
133
+ process.env['KIMAKI_DB_URL'] = hranaResult
134
+ await initDatabase()
135
+ await setBotToken(discord.botUserId, discord.botToken)
136
+
137
+ await setChannelDirectory({
138
+ channelId: textChannelId,
139
+ directory: directories.projectDirectory,
140
+ channelType: 'text',
141
+ })
142
+
143
+ botClient = createDiscordJsClient({ restUrl: discord.restUrl })
144
+ await startDiscordBot({
145
+ token: discord.botToken,
146
+ appId: discord.botUserId,
147
+ discordClient: botClient,
148
+ })
149
+
150
+ await discord.channel(textChannelId).user(testUserId).sendMessage({
151
+ content: 'Reply with exactly: kimaki digital twin ok',
152
+ })
153
+
154
+ const createdThread = await discord.channel(textChannelId).waitForThread({
155
+ timeout: 60_000,
156
+ predicate: (thread) => {
157
+ return thread.name === 'Reply with exactly: kimaki digital twin ok'
158
+ },
159
+ })
160
+
161
+ const botReply = await discord.thread(createdThread.id).waitForBotReply({
162
+ timeout: 120_000,
163
+ })
164
+
165
+ expect(createdThread.id.length).toBeGreaterThan(0)
166
+ expect(botReply.content.trim().length).toBeGreaterThan(0)
167
+ } finally {
168
+ await cleanupTestSessions({
169
+ projectDirectory: directories.projectDirectory,
170
+ testStartTime,
171
+ })
172
+
173
+ if (botClient) {
174
+ botClient.destroy()
175
+ }
176
+
177
+ await stopOpencodeServer()
178
+ await Promise.all([
179
+ closeDatabase().catch(() => {
180
+ return
181
+ }),
182
+ stopHranaServer().catch(() => {
183
+ return
184
+ }),
185
+ proxy.stop().catch(() => {
186
+ return
187
+ }),
188
+ discord.stop().catch(() => {
189
+ return
190
+ }),
191
+ ])
192
+
193
+ delete process.env['KIMAKI_LOCK_PORT']
194
+ delete process.env['KIMAKI_DB_URL']
195
+ fs.rmSync(directories.dataDir, { recursive: true, force: true })
196
+ }
197
+ },
198
+ 360_000,
199
+ )
@@ -0,0 +1,109 @@
1
+ // E2e test for OpenCode plugin loading.
2
+ // Spawns `opencode serve` directly with our plugin in OPENCODE_CONFIG_CONTENT,
3
+ // waits for the health endpoint, then checks stderr for plugin errors.
4
+ // No Discord infrastructure needed — just the OpenCode server process.
5
+
6
+ import { spawn, type ChildProcess } from 'node:child_process'
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+ import { test, expect } from 'vitest'
11
+ import { resolveOpencodeCommand } from './opencode.js'
12
+ import { getSpawnCommandAndArgs } from './opencode-command.js'
13
+ import { chooseLockPort } from './test-utils.js'
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
16
+
17
+ async function waitForHealth({
18
+ port,
19
+ maxAttempts = 30,
20
+ }: {
21
+ port: number
22
+ maxAttempts?: number
23
+ }): Promise<boolean> {
24
+ for (let i = 0; i < maxAttempts; i++) {
25
+ try {
26
+ const response = await fetch(`http://127.0.0.1:${port}/api/health`)
27
+ if (response.status < 500) {
28
+ return true
29
+ }
30
+ } catch {
31
+ // connection refused, retry
32
+ }
33
+ await new Promise((resolve) => {
34
+ setTimeout(resolve, 1000)
35
+ })
36
+ }
37
+ return false
38
+ }
39
+
40
+ test(
41
+ 'opencode server loads plugin without errors',
42
+ async () => {
43
+ const projectDir = path.resolve(process.cwd(), 'tmp', 'plugin-loading-e2e')
44
+ fs.mkdirSync(projectDir, { recursive: true })
45
+
46
+ const port = chooseLockPort({ key: 'opencode-plugin-loading-e2e' })
47
+ const pluginPath = new URL('../src/kimaki-opencode-plugin.ts', import.meta.url).href
48
+ const stderrLines: string[] = []
49
+ const isolatedOpencodeRoot = path.join(projectDir, 'opencode-test-home')
50
+
51
+ const {
52
+ command,
53
+ args,
54
+ windowsVerbatimArguments,
55
+ } = getSpawnCommandAndArgs({
56
+ resolvedCommand: resolveOpencodeCommand(),
57
+ baseArgs: ['serve', '--port', port.toString(), '--print-logs', '--log-level', 'DEBUG'],
58
+ })
59
+
60
+ const serverProcess: ChildProcess = spawn(command, args, {
61
+ stdio: 'pipe',
62
+ cwd: projectDir,
63
+ windowsVerbatimArguments,
64
+ env: {
65
+ ...process.env,
66
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({
67
+ $schema: 'https://opencode.ai/config.json',
68
+ lsp: false,
69
+ formatter: false,
70
+ plugin: [pluginPath],
71
+ }),
72
+ OPENCODE_TEST_HOME: isolatedOpencodeRoot,
73
+ OPENCODE_CONFIG_DIR: path.join(isolatedOpencodeRoot, '.opencode-kimaki'),
74
+ XDG_CONFIG_HOME: path.join(isolatedOpencodeRoot, '.config'),
75
+ XDG_DATA_HOME: path.join(isolatedOpencodeRoot, '.local', 'share'),
76
+ XDG_CACHE_HOME: path.join(isolatedOpencodeRoot, '.cache'),
77
+ XDG_STATE_HOME: path.join(isolatedOpencodeRoot, '.local', 'state'),
78
+ },
79
+ })
80
+
81
+ serverProcess.stderr?.on('data', (data) => {
82
+ stderrLines.push(...data.toString().split('\n').filter(Boolean))
83
+ })
84
+
85
+ try {
86
+ const healthy = await waitForHealth({ port })
87
+ expect(healthy).toBe(true)
88
+
89
+ // Check no plugin-related errors in stderr
90
+ const pluginErrorPatterns = [
91
+ /plugin.*error/i,
92
+ /failed to load plugin/i,
93
+ /cannot find module/i,
94
+ /ERR_MODULE_NOT_FOUND/i,
95
+ /plugin.*failed/i,
96
+ /plugin.*crash/i,
97
+ ]
98
+ const errorLines = stderrLines.filter((line) => {
99
+ return pluginErrorPatterns.some((pattern) => {
100
+ return pattern.test(line)
101
+ })
102
+ })
103
+ expect(errorLines).toEqual([])
104
+ } finally {
105
+ serverProcess.kill('SIGTERM')
106
+ }
107
+ },
108
+ 60_000,
109
+ )
@@ -0,0 +1,108 @@
1
+ import { test, expect, describe } from 'vitest'
2
+ import { condenseMemoryMd } from './condense-memory.js'
3
+
4
+ describe('condenseMemoryMd', () => {
5
+ test('multiple headings with body content', () => {
6
+ const content = [
7
+ '# Project Overview',
8
+ '',
9
+ 'This is a big project with many things.',
10
+ 'It does X, Y, and Z.',
11
+ '',
12
+ '## Auth Architecture',
13
+ '',
14
+ 'JWT tokens with 15min expiry.',
15
+ 'Refresh tokens in httpOnly cookies.',
16
+ 'Session stored in Redis.',
17
+ '',
18
+ '## User Preferences',
19
+ '',
20
+ '- kebab-case filenames',
21
+ '- errore-style errors',
22
+ '- no emojis',
23
+ '',
24
+ '### API Conventions',
25
+ '',
26
+ 'All routes return { data, error }.',
27
+ 'Use spiceflow for the server.',
28
+ '',
29
+ ].join('\n')
30
+
31
+ expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
32
+ "1: # Project Overview
33
+ ...
34
+ 6: ## Auth Architecture
35
+ ...
36
+ 12: ## User Preferences
37
+ ...
38
+ 18: ### API Conventions
39
+ ..."
40
+ `)
41
+ })
42
+
43
+ test('body text before first heading', () => {
44
+ const content = [
45
+ 'Some preamble notes.',
46
+ '',
47
+ '# First Heading',
48
+ '',
49
+ 'Content here.',
50
+ '',
51
+ ].join('\n')
52
+
53
+ expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
54
+ "...
55
+ 3: # First Heading
56
+ ..."
57
+ `)
58
+ })
59
+
60
+ test('no headings at all', () => {
61
+ const content = 'Just some notes.\nMore notes.\n'
62
+ expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`"..."`)
63
+ })
64
+
65
+ test('empty content', () => {
66
+ expect(condenseMemoryMd('')).toMatchInlineSnapshot(`""`)
67
+ })
68
+
69
+ test('consecutive headings without body', () => {
70
+ const content = [
71
+ '# H1',
72
+ '## H2',
73
+ '### H3',
74
+ '',
75
+ 'Some body.',
76
+ '',
77
+ ].join('\n')
78
+
79
+ expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
80
+ "1: # H1
81
+ 2: ## H2
82
+ 3: ### H3
83
+ ..."
84
+ `)
85
+ })
86
+
87
+ test('heading with code block body', () => {
88
+ const content = [
89
+ '# Config',
90
+ '',
91
+ '```json',
92
+ '{ "key": "value" }',
93
+ '```',
94
+ '',
95
+ '## Notes',
96
+ '',
97
+ 'Some text.',
98
+ '',
99
+ ].join('\n')
100
+
101
+ expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
102
+ "1: # Config
103
+ ...
104
+ 7: ## Notes
105
+ ..."
106
+ `)
107
+ })
108
+ })
@@ -0,0 +1,18 @@
1
+ // OpenCode plugin entry point for Kimaki Discord bot.
2
+ // Each export is treated as a separate plugin by OpenCode's plugin loader.
3
+ // CRITICAL: never export utility functions from this file — only plugin
4
+ // initializer functions. OpenCode calls every export as a plugin.
5
+ //
6
+ // Plugins are split into focused modules:
7
+ // - ipc-tools-plugin: file upload + action buttons (IPC-based Discord tools)
8
+ // - context-awareness-plugin: branch, pwd, memory, time gap, onboarding tutorial
9
+ // - opencode-interrupt-plugin: interrupt queued messages at step boundaries
10
+ // - kitty-graphics-plugin: extract Kitty Graphics Protocol images from bash output
11
+
12
+ export { ipcToolsPlugin } from './ipc-tools-plugin.js'
13
+ export { contextAwarenessPlugin } from './context-awareness-plugin.js'
14
+ export { interruptOpencodeSessionOnUserMessage } from './opencode-interrupt-plugin.js'
15
+ export { anthropicAuthPlugin } from './anthropic-auth-plugin.js'
16
+ export { imageOptimizerPlugin } from './image-optimizer-plugin.js'
17
+ export { kittyGraphicsPlugin } from 'kitty-graphics-agent'
18
+ export { injectionGuardInternal as injectionGuard } from 'opencode-injection-guard'
@@ -0,0 +1,116 @@
1
+ import { expect, test } from 'vitest'
2
+ import { limitHeadingDepth } from './limit-heading-depth.js'
3
+
4
+ test('converts h4 to h3', () => {
5
+ const input = '#### Fourth level heading'
6
+ const result = limitHeadingDepth(input)
7
+ expect(result).toMatchInlineSnapshot(`
8
+ "### Fourth level heading
9
+ "
10
+ `)
11
+ })
12
+
13
+ test('converts h5 to h3', () => {
14
+ const input = '##### Fifth level heading'
15
+ const result = limitHeadingDepth(input)
16
+ expect(result).toMatchInlineSnapshot(`
17
+ "### Fifth level heading
18
+ "
19
+ `)
20
+ })
21
+
22
+ test('converts h6 to h3', () => {
23
+ const input = '###### Sixth level heading'
24
+ const result = limitHeadingDepth(input)
25
+ expect(result).toMatchInlineSnapshot(`
26
+ "### Sixth level heading
27
+ "
28
+ `)
29
+ })
30
+
31
+ test('preserves h3 unchanged', () => {
32
+ const input = '### Third level heading'
33
+ const result = limitHeadingDepth(input)
34
+ expect(result).toMatchInlineSnapshot(`"### Third level heading"`)
35
+ })
36
+
37
+ test('preserves h2 unchanged', () => {
38
+ const input = '## Second level heading'
39
+ const result = limitHeadingDepth(input)
40
+ expect(result).toMatchInlineSnapshot(`"## Second level heading"`)
41
+ })
42
+
43
+ test('preserves h1 unchanged', () => {
44
+ const input = '# First level heading'
45
+ const result = limitHeadingDepth(input)
46
+ expect(result).toMatchInlineSnapshot(`"# First level heading"`)
47
+ })
48
+
49
+ test('handles multiple headings in document', () => {
50
+ const input = `# Title
51
+
52
+ Some text
53
+
54
+ ## Section
55
+
56
+ ### Subsection
57
+
58
+ #### Too deep
59
+
60
+ ##### Even deeper
61
+
62
+ Regular paragraph
63
+
64
+ ### Back to normal
65
+ `
66
+ const result = limitHeadingDepth(input)
67
+ expect(result).toMatchInlineSnapshot(`
68
+ "# Title
69
+
70
+ Some text
71
+
72
+ ## Section
73
+
74
+ ### Subsection
75
+
76
+ ### Too deep
77
+ ### Even deeper
78
+ Regular paragraph
79
+
80
+ ### Back to normal
81
+ "
82
+ `)
83
+ })
84
+
85
+ test('preserves heading with inline formatting', () => {
86
+ const input = '#### Heading with **bold** and `code`'
87
+ const result = limitHeadingDepth(input)
88
+ expect(result).toMatchInlineSnapshot(`
89
+ "### Heading with **bold** and \`code\`
90
+ "
91
+ `)
92
+ })
93
+
94
+ test('handles empty markdown', () => {
95
+ const result = limitHeadingDepth('')
96
+ expect(result).toMatchInlineSnapshot(`""`)
97
+ })
98
+
99
+ test('handles markdown with no headings', () => {
100
+ const input = 'Just some text\n\nAnd more text'
101
+ const result = limitHeadingDepth(input)
102
+ expect(result).toMatchInlineSnapshot(`
103
+ "Just some text
104
+
105
+ And more text"
106
+ `)
107
+ })
108
+
109
+ test('allows custom maxDepth', () => {
110
+ const input = '### Third level'
111
+ const result = limitHeadingDepth(input, 2)
112
+ expect(result).toMatchInlineSnapshot(`
113
+ "## Third level
114
+ "
115
+ `)
116
+ })
@@ -0,0 +1,26 @@
1
+ // Limit heading depth for Discord.
2
+ // Discord only supports headings up to ### (h3), so this converts
3
+ // ####, #####, etc. to ### to maintain consistent rendering.
4
+
5
+ import { Lexer, type Tokens } from 'marked'
6
+
7
+ export function limitHeadingDepth(markdown: string, maxDepth = 3): string {
8
+ const lexer = new Lexer()
9
+ const tokens = lexer.lex(markdown)
10
+
11
+ let result = ''
12
+ for (const token of tokens) {
13
+ if (token.type === 'heading') {
14
+ const heading = token as Tokens.Heading
15
+ if (heading.depth > maxDepth) {
16
+ const hashes = '#'.repeat(maxDepth)
17
+ result += hashes + ' ' + heading.text + '\n'
18
+ } else {
19
+ result += token.raw
20
+ }
21
+ } else {
22
+ result += token.raw
23
+ }
24
+ }
25
+ return result
26
+ }