@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
package/dist/tools.js ADDED
@@ -0,0 +1,357 @@
1
+ // Voice assistant tool definitions for the GenAI worker.
2
+ // Provides tools for managing OpenCode sessions (create, submit, abort),
3
+ // listing chats, searching files, and reading session messages.
4
+ import { tool } from './ai-tool.js';
5
+ import { z } from 'zod';
6
+ import { spawn } from 'node:child_process';
7
+ import net from 'node:net';
8
+ import {} from '@opencode-ai/sdk/v2';
9
+ import { createLogger, LogPrefix } from './logger.js';
10
+ import * as errore from 'errore';
11
+ const toolsLogger = createLogger(LogPrefix.TOOLS);
12
+ import { ShareMarkdown } from './markdown.js';
13
+ import { formatDistanceToNow } from './utils.js';
14
+ import pc from 'picocolors';
15
+ import { initializeOpencodeForDirectory, getOpencodeSystemMessage, } from './discord-bot.js';
16
+ export async function getTools({ onMessageCompleted, directory, }) {
17
+ const getClient = await initializeOpencodeForDirectory(directory);
18
+ if (getClient instanceof Error) {
19
+ throw new Error(getClient.message);
20
+ }
21
+ const client = getClient();
22
+ const markdownRenderer = new ShareMarkdown(client);
23
+ const providersResponse = await client.config.providers();
24
+ const providers = providersResponse.data?.providers || [];
25
+ // Helper: get last assistant model for a session (non-summary)
26
+ const getSessionModel = async (sessionId) => {
27
+ const res = await getClient().session.messages({ sessionID: sessionId });
28
+ const data = res.data;
29
+ if (!data || data.length === 0)
30
+ return undefined;
31
+ for (let i = data.length - 1; i >= 0; i--) {
32
+ const info = data?.[i]?.info;
33
+ if (info?.role === 'assistant') {
34
+ const ai = info;
35
+ if (!ai.summary && ai.providerID && ai.modelID) {
36
+ return { providerID: ai.providerID, modelID: ai.modelID };
37
+ }
38
+ }
39
+ }
40
+ return undefined;
41
+ };
42
+ const tools = {
43
+ submitMessage: tool({
44
+ description: 'Submit a message to an existing chat session. Does not wait for the message to complete',
45
+ inputSchema: z.object({
46
+ sessionId: z.string().describe('The session ID to send message to'),
47
+ message: z.string().describe('The message text to send'),
48
+ }),
49
+ execute: async ({ sessionId, message }) => {
50
+ const sessionModel = await getSessionModel(sessionId);
51
+ // do not await
52
+ getClient()
53
+ .session.promptAsync({
54
+ sessionID: sessionId,
55
+ parts: [{ type: 'text', text: message }],
56
+ model: sessionModel,
57
+ system: getOpencodeSystemMessage({ sessionId }),
58
+ })
59
+ .then(async (response) => {
60
+ const markdownResult = await markdownRenderer.generate({
61
+ sessionID: sessionId,
62
+ lastAssistantOnly: true,
63
+ });
64
+ onMessageCompleted?.({
65
+ sessionId,
66
+ messageId: '',
67
+ markdown: errore.unwrapOr(markdownResult, ''),
68
+ });
69
+ })
70
+ .catch((error) => {
71
+ onMessageCompleted?.({
72
+ sessionId,
73
+ messageId: '',
74
+ error,
75
+ });
76
+ });
77
+ return {
78
+ success: true,
79
+ sessionId,
80
+ directive: 'Tell user that message has been sent successfully',
81
+ };
82
+ },
83
+ }),
84
+ createNewChat: tool({
85
+ description: 'Start a new chat session with an initial message. Does not wait for the message to complete',
86
+ inputSchema: z.object({
87
+ message: z
88
+ .string()
89
+ .describe('The initial message to start the chat with'),
90
+ title: z.string().optional().describe('Optional title for the session'),
91
+ model: z
92
+ .object({
93
+ providerId: z
94
+ .string()
95
+ .describe('The provider ID (e.g., "anthropic", "openai")'),
96
+ modelId: z
97
+ .string()
98
+ .describe('The model ID (e.g., "claude-opus-4-20250514", "gpt-5")'),
99
+ })
100
+ .optional()
101
+ .describe('Optional model to use for this session'),
102
+ }),
103
+ execute: async ({ message, title }) => {
104
+ if (!message.trim()) {
105
+ throw new Error(`message must be a non empty string`);
106
+ }
107
+ try {
108
+ const session = await getClient().session.create({
109
+ ...(title ? { title } : {}),
110
+ });
111
+ if (!session.data) {
112
+ throw new Error('Failed to create session');
113
+ }
114
+ // do not await
115
+ getClient()
116
+ .session.promptAsync({
117
+ sessionID: session.data.id,
118
+ parts: [{ type: 'text', text: message }],
119
+ system: getOpencodeSystemMessage({ sessionId: session.data.id }),
120
+ })
121
+ .then(async (response) => {
122
+ const markdownResult = await markdownRenderer.generate({
123
+ sessionID: session.data.id,
124
+ lastAssistantOnly: true,
125
+ });
126
+ onMessageCompleted?.({
127
+ sessionId: session.data.id,
128
+ messageId: '',
129
+ markdown: errore.unwrapOr(markdownResult, ''),
130
+ });
131
+ })
132
+ .catch((error) => {
133
+ onMessageCompleted?.({
134
+ sessionId: session.data.id,
135
+ messageId: '',
136
+ error,
137
+ });
138
+ });
139
+ return {
140
+ success: true,
141
+ sessionId: session.data.id,
142
+ title: session.data.title,
143
+ };
144
+ }
145
+ catch (error) {
146
+ return {
147
+ success: false,
148
+ error: error instanceof Error
149
+ ? error.message
150
+ : 'Failed to create chat session',
151
+ };
152
+ }
153
+ },
154
+ }),
155
+ listChats: tool({
156
+ description: 'Get a list of available chat sessions sorted by most recent',
157
+ inputSchema: z.object({}),
158
+ execute: async () => {
159
+ toolsLogger.log(`Listing opencode sessions`);
160
+ const sessions = await getClient().session.list();
161
+ if (!sessions.data) {
162
+ return { success: false, error: 'No sessions found' };
163
+ }
164
+ const sortedSessions = [...sessions.data]
165
+ .sort((a, b) => {
166
+ return b.time.updated - a.time.updated;
167
+ })
168
+ .slice(0, 20);
169
+ const sessionList = sortedSessions.map(async (session) => {
170
+ const finishedAt = session.time.updated;
171
+ const status = await (async () => {
172
+ if (session.revert)
173
+ return 'error';
174
+ const messagesResponse = await getClient().session.messages({
175
+ sessionID: session.id,
176
+ });
177
+ const messages = messagesResponse.data || [];
178
+ const lastMessage = messages[messages.length - 1];
179
+ if (lastMessage?.info.role === 'assistant' &&
180
+ !lastMessage.info.time.completed) {
181
+ return 'in_progress';
182
+ }
183
+ return 'finished';
184
+ })();
185
+ return {
186
+ id: session.id,
187
+ folder: session.directory,
188
+ status,
189
+ finishedAt: formatDistanceToNow(new Date(finishedAt)),
190
+ title: session.title,
191
+ prompt: session.title,
192
+ };
193
+ });
194
+ const resolvedList = await Promise.all(sessionList);
195
+ return {
196
+ success: true,
197
+ sessions: resolvedList,
198
+ };
199
+ },
200
+ }),
201
+ searchFiles: tool({
202
+ description: 'Search for files in a folder',
203
+ inputSchema: z.object({
204
+ folder: z
205
+ .string()
206
+ .optional()
207
+ .describe('The folder path to search in, optional. only use if user specifically asks for it'),
208
+ query: z.string().describe('The search query for files'),
209
+ }),
210
+ execute: async ({ folder, query }) => {
211
+ const results = await getClient().find.files({
212
+ query,
213
+ directory: folder,
214
+ });
215
+ return {
216
+ success: true,
217
+ files: results.data || [],
218
+ };
219
+ },
220
+ }),
221
+ readSessionMessages: tool({
222
+ description: 'Read messages from a chat session',
223
+ inputSchema: z.object({
224
+ sessionId: z.string().describe('The session ID to read messages from'),
225
+ lastAssistantOnly: z
226
+ .boolean()
227
+ .optional()
228
+ .describe('Only read the last assistant message'),
229
+ }),
230
+ execute: async ({ sessionId, lastAssistantOnly = false }) => {
231
+ if (lastAssistantOnly) {
232
+ const messages = await getClient().session.messages({
233
+ sessionID: sessionId,
234
+ });
235
+ if (!messages.data) {
236
+ return { success: false, error: 'No messages found' };
237
+ }
238
+ const assistantMessages = messages.data.filter((m) => m.info.role === 'assistant');
239
+ if (assistantMessages.length === 0) {
240
+ return {
241
+ success: false,
242
+ error: 'No assistant messages found',
243
+ };
244
+ }
245
+ const lastMessage = assistantMessages[assistantMessages.length - 1];
246
+ const status = 'completed' in lastMessage.info.time &&
247
+ lastMessage.info.time.completed
248
+ ? 'completed'
249
+ : 'in_progress';
250
+ const markdownResult = await markdownRenderer.generate({
251
+ sessionID: sessionId,
252
+ lastAssistantOnly: true,
253
+ });
254
+ if (markdownResult instanceof Error) {
255
+ throw new Error(markdownResult.message);
256
+ }
257
+ return {
258
+ success: true,
259
+ markdown: markdownResult,
260
+ status,
261
+ };
262
+ }
263
+ else {
264
+ const markdownResult = await markdownRenderer.generate({
265
+ sessionID: sessionId,
266
+ });
267
+ if (markdownResult instanceof Error) {
268
+ throw new Error(markdownResult.message);
269
+ }
270
+ const messages = await getClient().session.messages({
271
+ sessionID: sessionId,
272
+ });
273
+ const lastMessage = messages.data?.[messages.data.length - 1];
274
+ const status = lastMessage?.info.role === 'assistant' &&
275
+ lastMessage?.info.time &&
276
+ 'completed' in lastMessage.info.time &&
277
+ !lastMessage.info.time.completed
278
+ ? 'in_progress'
279
+ : 'completed';
280
+ return {
281
+ success: true,
282
+ markdown: markdownResult,
283
+ status,
284
+ };
285
+ }
286
+ },
287
+ }),
288
+ abortChat: tool({
289
+ description: 'Abort/stop an in-progress chat session',
290
+ inputSchema: z.object({
291
+ sessionId: z.string().describe('The session ID to abort'),
292
+ }),
293
+ execute: async ({ sessionId }) => {
294
+ try {
295
+ toolsLogger.log(`[ABORT] reason=voice-tool sessionId=${sessionId} - user requested abort via voice assistant tool`);
296
+ const result = await getClient().session.abort({
297
+ sessionID: sessionId,
298
+ });
299
+ if (!result.data) {
300
+ return {
301
+ success: false,
302
+ error: 'Failed to abort session',
303
+ };
304
+ }
305
+ return {
306
+ success: true,
307
+ sessionId,
308
+ message: 'Session aborted successfully',
309
+ };
310
+ }
311
+ catch (error) {
312
+ return {
313
+ success: false,
314
+ error: error instanceof Error ? error.message : 'Unknown error occurred',
315
+ };
316
+ }
317
+ },
318
+ }),
319
+ getModels: tool({
320
+ description: 'Get all available AI models from all providers',
321
+ inputSchema: z.object({}),
322
+ execute: async () => {
323
+ try {
324
+ const providersResponse = await getClient().config.providers();
325
+ const providers = providersResponse.data?.providers || [];
326
+ const models = [];
327
+ providers.forEach((provider) => {
328
+ if (provider.models && typeof provider.models === 'object') {
329
+ Object.entries(provider.models).forEach(([modelId, model]) => {
330
+ models.push({
331
+ providerId: provider.id,
332
+ modelId: modelId,
333
+ });
334
+ });
335
+ }
336
+ });
337
+ return {
338
+ success: true,
339
+ models,
340
+ totalCount: models.length,
341
+ };
342
+ }
343
+ catch (error) {
344
+ return {
345
+ success: false,
346
+ error: error instanceof Error ? error.message : 'Failed to fetch models',
347
+ models: [],
348
+ };
349
+ }
350
+ },
351
+ }),
352
+ };
353
+ return {
354
+ tools,
355
+ providers,
356
+ };
357
+ }
@@ -0,0 +1,161 @@
1
+ // E2e test for /undo command.
2
+ // Validates that:
3
+ // 1. After /undo, session.revert state is set (files reverted, revert boundary marked)
4
+ // 2. Messages are NOT deleted yet (they stay until next prompt cleans them up)
5
+ // 3. On the next user message, reverted messages are cleaned up by OpenCode's
6
+ // SessionRevert.cleanup() and the model only sees pre-revert messages
7
+ //
8
+ // This matches the OpenCode TUI behavior (use-session-commands.tsx):
9
+ // - Pass the user message ID (not assistant ID)
10
+ // - Don't delete messages — just mark session as reverted
11
+ // - Cleanup happens automatically on next promptAsync()
12
+ //
13
+ // Uses opencode-deterministic-provider (no real LLM calls).
14
+ // Poll timeouts: 4s max, 100ms interval.
15
+ import { describe, test, expect } from 'vitest';
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
19
+ import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
20
+ import { getThreadSession } from './database.js';
21
+ import { initializeOpencodeForDirectory } from './opencode.js';
22
+ const TEXT_CHANNEL_ID = '200000000000001200';
23
+ const e2eTest = describe;
24
+ e2eTest('/undo sets revert state and cleans up on next prompt', () => {
25
+ const ctx = setupQueueAdvancedSuite({
26
+ channelId: TEXT_CHANNEL_ID,
27
+ channelName: 'qa-undo-e2e',
28
+ dirName: 'qa-undo-e2e',
29
+ username: 'undo-tester',
30
+ });
31
+ test('undo sets revert state, next message cleans up reverted messages', async () => {
32
+ const markerPath = path.join(ctx.directories.projectDirectory, 'tmp', 'undo-marker.txt');
33
+ // 1. Send a message and wait for complete session (footer)
34
+ await ctx.discord
35
+ .channel(TEXT_CHANNEL_ID)
36
+ .user(TEST_USER_ID)
37
+ .sendMessage({
38
+ content: 'UNDO_FILE_MARKER',
39
+ });
40
+ const thread = await ctx.discord
41
+ .channel(TEXT_CHANNEL_ID)
42
+ .waitForThread({
43
+ timeout: 8_000,
44
+ predicate: (t) => {
45
+ return t.name === 'UNDO_FILE_MARKER';
46
+ },
47
+ });
48
+ const th = ctx.discord.thread(thread.id);
49
+ await th.waitForBotReply({ timeout: 8_000 });
50
+ await waitForFooterMessage({
51
+ discord: ctx.discord,
52
+ threadId: thread.id,
53
+ timeout: 8_000,
54
+ });
55
+ // 2. Get session ID and verify it has messages
56
+ const sessionId = await getThreadSession(thread.id);
57
+ expect(sessionId).toBeTruthy();
58
+ const getClient = await initializeOpencodeForDirectory(ctx.directories.projectDirectory);
59
+ if (getClient instanceof Error) {
60
+ throw getClient;
61
+ }
62
+ const beforeMessages = await getClient().session.messages({
63
+ sessionID: sessionId,
64
+ directory: ctx.directories.projectDirectory,
65
+ });
66
+ const beforeCount = (beforeMessages.data || []).length;
67
+ expect(beforeCount).toBeGreaterThan(0);
68
+ const beforeUserMessages = (beforeMessages.data || []).filter((m) => {
69
+ return m.info.role === 'user';
70
+ });
71
+ const beforeAssistantMessages = (beforeMessages.data || []).filter((m) => {
72
+ return m.info.role === 'assistant';
73
+ });
74
+ expect(beforeUserMessages.length).toBeGreaterThan(0);
75
+ expect(beforeAssistantMessages.length).toBeGreaterThan(0);
76
+ expect(fs.existsSync(markerPath)).toBe(true);
77
+ // Verify no revert state yet
78
+ const beforeSession = await getClient().session.get({
79
+ sessionID: sessionId,
80
+ });
81
+ expect(beforeSession.data?.revert).toBeFalsy();
82
+ // 3. Run /undo command
83
+ const { id: undoInteractionId } = await th
84
+ .user(TEST_USER_ID)
85
+ .runSlashCommand({ name: 'undo' });
86
+ const undoAck = await th.waitForInteractionAck({
87
+ interactionId: undoInteractionId,
88
+ timeout: 4_000,
89
+ });
90
+ expect(undoAck).toBeDefined();
91
+ await waitForBotMessageContaining({
92
+ discord: ctx.discord,
93
+ threadId: thread.id,
94
+ text: 'Undone - reverted last assistant message',
95
+ timeout: 8_000,
96
+ });
97
+ // 4. Verify session now has revert state set
98
+ const afterSession = await getClient().session.get({
99
+ sessionID: sessionId,
100
+ });
101
+ expect(afterSession.data?.revert).toBeTruthy();
102
+ expect(afterSession.data?.revert?.messageID).toBeTruthy();
103
+ // Messages should still exist (not deleted — cleanup happens on next prompt)
104
+ const afterMessages = await getClient().session.messages({
105
+ sessionID: sessionId,
106
+ directory: ctx.directories.projectDirectory,
107
+ });
108
+ expect((afterMessages.data || []).length).toBe(beforeCount);
109
+ // 5. Send a new message — this triggers SessionRevert.cleanup()
110
+ // which removes reverted messages before processing the new prompt
111
+ await th.user(TEST_USER_ID).sendMessage({
112
+ content: 'Reply with exactly: after-undo-message',
113
+ });
114
+ await waitForFooterMessage({
115
+ discord: ctx.discord,
116
+ threadId: thread.id,
117
+ timeout: 8_000,
118
+ afterMessageIncludes: 'after-undo-message',
119
+ });
120
+ // 6. Verify reverted messages were cleaned up
121
+ const finalMessages = await getClient().session.messages({
122
+ sessionID: sessionId,
123
+ directory: ctx.directories.projectDirectory,
124
+ });
125
+ const finalAssistantMessages = (finalMessages.data || []).filter((m) => {
126
+ return m.info.role === 'assistant';
127
+ });
128
+ // The original assistant message should have been cleaned up,
129
+ // only the new one (from after-undo-message) should remain
130
+ const originalAssistantStillExists = finalAssistantMessages.some((m) => {
131
+ return m.parts.some((p) => {
132
+ return p.type === 'text' && 'text' in p && p.text === 'ok';
133
+ });
134
+ });
135
+ // The first "ok" response was reverted and should be cleaned up.
136
+ // The new response for "after-undo-message" should produce a fresh "ok".
137
+ // We verify the total count dropped: the original user+assistant pair
138
+ // was removed, and replaced by just the new user+assistant pair.
139
+ expect(finalAssistantMessages.length).toBeLessThanOrEqual(beforeAssistantMessages.length);
140
+ // Revert state should be cleared after cleanup
141
+ const finalSession = await getClient().session.get({
142
+ sessionID: sessionId,
143
+ });
144
+ expect(finalSession.data?.revert).toBeFalsy();
145
+ // 7. Snapshot the Discord thread
146
+ expect(await th.text()).toMatchInlineSnapshot(`
147
+ "--- from: user (undo-tester)
148
+ UNDO_FILE_MARKER
149
+ --- from: assistant (TestBot)
150
+ ⬥ creating undo file
151
+ ⬥ undo file created
152
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
153
+ Undone - reverted last assistant message
154
+ --- from: user (undo-tester)
155
+ Reply with exactly: after-undo-message
156
+ --- from: assistant (TestBot)
157
+ ⬥ ok
158
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
159
+ `);
160
+ }, 20_000);
161
+ });
@@ -0,0 +1,146 @@
1
+ // Unnest code blocks from list items for Discord.
2
+ // Discord doesn't render code blocks inside lists, so this hoists them
3
+ // to root level while preserving list structure.
4
+ import { Lexer } from 'marked';
5
+ export function unnestCodeBlocksFromLists(markdown) {
6
+ const lexer = new Lexer();
7
+ const tokens = lexer.lex(markdown);
8
+ const result = [];
9
+ for (let i = 0; i < tokens.length; i++) {
10
+ const token = tokens[i];
11
+ const next = tokens[i + 1];
12
+ const chunk = (() => {
13
+ if (token.type === 'list') {
14
+ const segments = processListToken(token);
15
+ return renderSegments(segments);
16
+ }
17
+ return token.raw;
18
+ })();
19
+ if (!chunk) {
20
+ continue;
21
+ }
22
+ const nextRaw = next?.raw ?? '';
23
+ const needsNewline = nextRaw &&
24
+ !chunk.endsWith('\n') &&
25
+ typeof nextRaw === 'string' &&
26
+ !nextRaw.startsWith('\n');
27
+ result.push(needsNewline ? chunk + '\n' : chunk);
28
+ }
29
+ return result.join('');
30
+ }
31
+ function processListToken(list) {
32
+ const segments = [];
33
+ const start = typeof list.start === 'number' ? list.start : parseInt(list.start, 10) || 1;
34
+ const prefix = list.ordered ? (i) => `${start + i}. ` : () => '- ';
35
+ for (let i = 0; i < list.items.length; i++) {
36
+ const item = list.items[i];
37
+ const itemSegments = processListItem(item, prefix(i));
38
+ segments.push(...itemSegments);
39
+ }
40
+ return segments;
41
+ }
42
+ function processListItem(item, prefix) {
43
+ const segments = [];
44
+ let currentText = [];
45
+ // Track if we've seen a code block - text after code uses continuation prefix
46
+ let seenCodeBlock = false;
47
+ const taskMarker = item.task ? (item.checked ? '[x] ' : '[ ] ') : '';
48
+ let wroteFirstListItem = false;
49
+ const flushText = () => {
50
+ const rawText = currentText.join('');
51
+ const text = rawText.trimEnd();
52
+ if (text.trim()) {
53
+ // After a code block, use '-' as continuation prefix to avoid repeating numbers
54
+ const effectivePrefix = seenCodeBlock ? '- ' : prefix;
55
+ const marker = !wroteFirstListItem ? taskMarker : '';
56
+ const normalizedText = normalizeListItemText({
57
+ text,
58
+ isTaskItem: item.task,
59
+ });
60
+ segments.push({
61
+ type: 'list-item',
62
+ prefix: effectivePrefix,
63
+ content: marker + normalizedText,
64
+ });
65
+ wroteFirstListItem = true;
66
+ }
67
+ currentText = [];
68
+ };
69
+ for (const token of item.tokens) {
70
+ if (token.type === 'code') {
71
+ flushText();
72
+ const codeToken = token;
73
+ const lang = codeToken.lang || '';
74
+ segments.push({
75
+ type: 'code',
76
+ content: '```' + lang + '\n' + codeToken.text + '\n```\n',
77
+ });
78
+ seenCodeBlock = true;
79
+ continue;
80
+ }
81
+ if (token.type === 'list') {
82
+ flushText();
83
+ // Recursively process nested list - segments bubble up
84
+ const nestedSegments = processListToken(token);
85
+ segments.push(...nestedSegments);
86
+ continue;
87
+ }
88
+ currentText.push(extractText(token));
89
+ }
90
+ flushText();
91
+ // If no segments were created (empty item), return empty
92
+ if (segments.length === 0) {
93
+ return [];
94
+ }
95
+ // If item had no code blocks (all segments are list-items from this level),
96
+ // return original raw to preserve formatting
97
+ const hasCode = segments.some((s) => s.type === 'code');
98
+ if (!hasCode) {
99
+ return [{ type: 'list-item', prefix: '', content: item.raw }];
100
+ }
101
+ return segments;
102
+ }
103
+ function extractText(token) {
104
+ // Prefer raw to preserve newlines and markdown markers.
105
+ if ('raw' in token && typeof token.raw === 'string') {
106
+ return token.raw;
107
+ }
108
+ if (token.type === 'text') {
109
+ return token.text;
110
+ }
111
+ return '';
112
+ }
113
+ function normalizeListItemText({ text, isTaskItem, }) {
114
+ const withoutIndent = text.replace(/^\s+/, '');
115
+ if (!isTaskItem) {
116
+ return withoutIndent;
117
+ }
118
+ return withoutIndent.replace(/^\[(?: |x|X)\]\s+/, '');
119
+ }
120
+ function renderSegments(segments) {
121
+ const result = [];
122
+ for (let i = 0; i < segments.length; i++) {
123
+ const segment = segments[i];
124
+ const prev = segments[i - 1];
125
+ if (segment.type === 'code') {
126
+ // Add newline before code if previous was a list item
127
+ if (prev && prev.type === 'list-item') {
128
+ result.push('\n');
129
+ }
130
+ result.push(segment.content);
131
+ }
132
+ else {
133
+ // list-item
134
+ if (segment.prefix) {
135
+ result.push(segment.prefix + segment.content + '\n');
136
+ }
137
+ else {
138
+ // Raw content (no prefix means it's original raw)
139
+ // Ensure raw ends with newline for proper separation from next segment
140
+ const raw = segment.content.trimEnd();
141
+ result.push(raw + '\n');
142
+ }
143
+ }
144
+ }
145
+ return result.join('').trimEnd();
146
+ }