@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,185 @@
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
+
5
+ import { Lexer, type Token, type Tokens } from 'marked'
6
+
7
+ type Segment =
8
+ | { type: 'list-item'; prefix: string; content: string }
9
+ | { type: 'code'; content: string }
10
+
11
+ export function unnestCodeBlocksFromLists(markdown: string): string {
12
+ const lexer = new Lexer()
13
+ const tokens = lexer.lex(markdown)
14
+
15
+ const result: string[] = []
16
+
17
+ for (let i = 0; i < tokens.length; i++) {
18
+ const token = tokens[i]!
19
+ const next = tokens[i + 1]
20
+
21
+ const chunk = (() => {
22
+ if (token.type === 'list') {
23
+ const segments = processListToken(token as Tokens.List)
24
+ return renderSegments(segments)
25
+ }
26
+ return token.raw
27
+ })()
28
+
29
+ if (!chunk) {
30
+ continue
31
+ }
32
+
33
+ const nextRaw = next?.raw ?? ''
34
+ const needsNewline =
35
+ nextRaw &&
36
+ !chunk.endsWith('\n') &&
37
+ typeof nextRaw === 'string' &&
38
+ !nextRaw.startsWith('\n')
39
+
40
+ result.push(needsNewline ? chunk + '\n' : chunk)
41
+ }
42
+ return result.join('')
43
+ }
44
+
45
+ function processListToken(list: Tokens.List): Segment[] {
46
+ const segments: Segment[] = []
47
+ const start =
48
+ typeof list.start === 'number' ? list.start : parseInt(list.start, 10) || 1
49
+ const prefix = list.ordered ? (i: number) => `${start + i}. ` : () => '- '
50
+
51
+ for (let i = 0; i < list.items.length; i++) {
52
+ const item = list.items[i]!
53
+ const itemSegments = processListItem(item, prefix(i))
54
+ segments.push(...itemSegments)
55
+ }
56
+
57
+ return segments
58
+ }
59
+
60
+ function processListItem(item: Tokens.ListItem, prefix: string): Segment[] {
61
+ const segments: Segment[] = []
62
+ let currentText: string[] = []
63
+ // Track if we've seen a code block - text after code uses continuation prefix
64
+ let seenCodeBlock = false
65
+
66
+ const taskMarker = item.task ? (item.checked ? '[x] ' : '[ ] ') : ''
67
+ let wroteFirstListItem = false
68
+
69
+ const flushText = (): void => {
70
+ const rawText = currentText.join('')
71
+ const text = rawText.trimEnd()
72
+ if (text.trim()) {
73
+ // After a code block, use '-' as continuation prefix to avoid repeating numbers
74
+ const effectivePrefix = seenCodeBlock ? '- ' : prefix
75
+ const marker = !wroteFirstListItem ? taskMarker : ''
76
+ const normalizedText = normalizeListItemText({
77
+ text,
78
+ isTaskItem: item.task,
79
+ })
80
+ segments.push({
81
+ type: 'list-item',
82
+ prefix: effectivePrefix,
83
+ content: marker + normalizedText,
84
+ })
85
+ wroteFirstListItem = true
86
+ }
87
+ currentText = []
88
+ }
89
+
90
+ for (const token of item.tokens) {
91
+ if (token.type === 'code') {
92
+ flushText()
93
+ const codeToken = token as Tokens.Code
94
+ const lang = codeToken.lang || ''
95
+ segments.push({
96
+ type: 'code',
97
+ content: '```' + lang + '\n' + codeToken.text + '\n```\n',
98
+ })
99
+ seenCodeBlock = true
100
+ continue
101
+ }
102
+
103
+ if (token.type === 'list') {
104
+ flushText()
105
+ // Recursively process nested list - segments bubble up
106
+ const nestedSegments = processListToken(token as Tokens.List)
107
+ segments.push(...nestedSegments)
108
+ continue
109
+ }
110
+
111
+ currentText.push(extractText(token))
112
+ }
113
+
114
+ flushText()
115
+
116
+ // If no segments were created (empty item), return empty
117
+ if (segments.length === 0) {
118
+ return []
119
+ }
120
+
121
+ // If item had no code blocks (all segments are list-items from this level),
122
+ // return original raw to preserve formatting
123
+ const hasCode = segments.some((s) => s.type === 'code')
124
+ if (!hasCode) {
125
+ return [{ type: 'list-item', prefix: '', content: item.raw }]
126
+ }
127
+
128
+ return segments
129
+ }
130
+
131
+ function extractText(token: Token): string {
132
+ // Prefer raw to preserve newlines and markdown markers.
133
+ if ('raw' in token && typeof token.raw === 'string') {
134
+ return token.raw
135
+ }
136
+
137
+ if (token.type === 'text') {
138
+ return (token as Tokens.Text).text
139
+ }
140
+
141
+ return ''
142
+ }
143
+
144
+ function normalizeListItemText({
145
+ text,
146
+ isTaskItem,
147
+ }: {
148
+ text: string
149
+ isTaskItem: boolean
150
+ }): string {
151
+ const withoutIndent = text.replace(/^\s+/, '')
152
+ if (!isTaskItem) {
153
+ return withoutIndent
154
+ }
155
+ return withoutIndent.replace(/^\[(?: |x|X)\]\s+/, '')
156
+ }
157
+
158
+ function renderSegments(segments: Segment[]): string {
159
+ const result: string[] = []
160
+
161
+ for (let i = 0; i < segments.length; i++) {
162
+ const segment = segments[i]!
163
+ const prev = segments[i - 1]
164
+
165
+ if (segment.type === 'code') {
166
+ // Add newline before code if previous was a list item
167
+ if (prev && prev.type === 'list-item') {
168
+ result.push('\n')
169
+ }
170
+ result.push(segment.content)
171
+ } else {
172
+ // list-item
173
+ if (segment.prefix) {
174
+ result.push(segment.prefix + segment.content + '\n')
175
+ } else {
176
+ // Raw content (no prefix means it's original raw)
177
+ // Ensure raw ends with newline for proper separation from next segment
178
+ const raw = segment.content.trimEnd()
179
+ result.push(raw + '\n')
180
+ }
181
+ }
182
+ }
183
+
184
+ return result.join('').trimEnd()
185
+ }
package/src/upgrade.ts ADDED
@@ -0,0 +1,127 @@
1
+ // Kimaki self-upgrade utilities.
2
+ // Detects the package manager used to install kimaki, checks npm for newer versions,
3
+ // and runs the global upgrade command. Used by both CLI `kimaki upgrade` and
4
+ // the Discord `/upgrade-and-restart` command, plus background auto-upgrade on startup.
5
+
6
+ import fs from 'node:fs'
7
+ import { createRequire } from 'node:module'
8
+ import { createLogger, LogPrefix } from './logger.js'
9
+ import { execAsync } from './worktrees.js'
10
+
11
+ const logger = createLogger(LogPrefix.CLI)
12
+
13
+ type Pm = 'bun' | 'pnpm' | 'npm'
14
+
15
+ // Detects which package manager globally installed kimaki, used to run the
16
+ // correct `<pm> i -g kimaki@latest` upgrade command.
17
+ //
18
+ // Detection order:
19
+ // 1. npm_config_user_agent — set by npx/bunx/pnpm dlx, reliable for those cases
20
+ // 2. Realpath of the running script — resolve symlinks and check if the path
21
+ // lives under a known PM global directory (e.g. ~/.bun, ~/Library/pnpm,
22
+ // /usr/local/lib/node_modules). Inspired by sindresorhus/global-directory.
23
+ // 3. process.versions.bun — if the runtime itself is Bun, likely bun ecosystem
24
+ // 4. Default to npm — safest fallback since npm is the most common global installer
25
+ export function detectPm(): Pm {
26
+ const ua = process.env.npm_config_user_agent
27
+ if (ua?.startsWith('bun/')) {
28
+ return 'bun'
29
+ }
30
+ if (ua?.startsWith('pnpm/')) {
31
+ return 'pnpm'
32
+ }
33
+ if (ua?.startsWith('npm/')) {
34
+ return 'npm'
35
+ }
36
+
37
+ const scriptPath = resolveScriptRealpath()
38
+ if (scriptPath) {
39
+ const p = scriptPath.toLowerCase()
40
+ // bun global installs live under ~/.bun or $BUN_INSTALL
41
+ if (p.includes('.bun/') || p.includes('/bun/install/')) {
42
+ return 'bun'
43
+ }
44
+ // pnpm global installs live under ~/Library/pnpm, ~/.local/share/pnpm, or $PNPM_HOME
45
+ if (p.includes('/pnpm/')) {
46
+ return 'pnpm'
47
+ }
48
+ // npm global installs typically live under lib/node_modules/kimaki without
49
+ // any pnpm or bun path segments, so if we reach here it's likely npm
50
+ }
51
+
52
+ if (process.versions.bun) {
53
+ return 'bun'
54
+ }
55
+
56
+ return 'npm'
57
+ }
58
+
59
+ function resolveScriptRealpath(): string | null {
60
+ try {
61
+ const script = process.argv[1]
62
+ if (!script) {
63
+ return null
64
+ }
65
+ return fs.realpathSync(script)
66
+ } catch {
67
+ return null
68
+ }
69
+ }
70
+
71
+ export function getCurrentVersion(): string {
72
+ const require = createRequire(import.meta.url)
73
+ const pkg = require('../package.json') as { version: string }
74
+ return pkg.version
75
+ }
76
+
77
+ export async function getLatestNpmVersion(): Promise<string | null> {
78
+ try {
79
+ const res = await fetch('https://registry.npmjs.org/kimaki/latest', {
80
+ signal: AbortSignal.timeout(15_000),
81
+ })
82
+ if (!res.ok) {
83
+ return null
84
+ }
85
+ const data = (await res.json()) as { version: string }
86
+ return data.version
87
+ } catch {
88
+ return null
89
+ }
90
+ }
91
+
92
+ // Returns the new version string if upgraded, null if already up to date.
93
+ export async function upgrade(): Promise<string | null> {
94
+ const current = getCurrentVersion()
95
+ const latest = await getLatestNpmVersion()
96
+ if (!latest) {
97
+ throw new Error('Failed to check latest version from npm')
98
+ }
99
+ if (current === latest) {
100
+ return null
101
+ }
102
+
103
+ const pm = detectPm()
104
+ logger.log(`Upgrading kimaki from v${current} to v${latest} using ${pm}...`)
105
+ await execAsync(`${pm} i -g kimaki@latest`, { timeout: 120_000 })
106
+
107
+ return latest
108
+ }
109
+
110
+ // Fire-and-forget background upgrade check on bot startup.
111
+ // Only upgrades if a newer version is available. Errors are silently ignored.
112
+ export async function backgroundUpgradeKimaki(): Promise<void> {
113
+ try {
114
+ const current = getCurrentVersion()
115
+ const latest = await getLatestNpmVersion()
116
+ if (!latest || current === latest) {
117
+ return
118
+ }
119
+
120
+ const pm = detectPm()
121
+ logger.debug(`Background kimaki upgrade started: v${current} -> v${latest}`)
122
+ await execAsync(`${pm} i -g kimaki@latest`, { timeout: 120_000 })
123
+ logger.debug(`Background kimaki upgrade completed: v${latest}`)
124
+ } catch {
125
+ // silently ignored, non-critical
126
+ }
127
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,212 @@
1
+ // General utility functions for the bot.
2
+ // Includes Discord OAuth URL generation, array deduplication,
3
+ // abort error detection, and date/time formatting helpers.
4
+
5
+ import os from 'node:os'
6
+ import { PermissionsBitField } from 'discord.js'
7
+ import type { BotMode } from './database.js'
8
+ import * as errore from 'errore'
9
+
10
+ type GenerateInstallUrlOptions = {
11
+ clientId: string
12
+ permissions?: bigint[]
13
+ scopes?: string[]
14
+ guildId?: string
15
+ disableGuildSelect?: boolean
16
+ state?: string
17
+ redirectUri?: string
18
+ responseType?: string
19
+ }
20
+
21
+ export function generateBotInstallUrl({
22
+ clientId,
23
+ permissions = [
24
+ PermissionsBitField.Flags.ViewChannel,
25
+ PermissionsBitField.Flags.ManageChannels,
26
+ PermissionsBitField.Flags.SendMessages,
27
+ PermissionsBitField.Flags.SendMessagesInThreads,
28
+ PermissionsBitField.Flags.CreatePublicThreads,
29
+ PermissionsBitField.Flags.ManageThreads,
30
+ PermissionsBitField.Flags.ReadMessageHistory,
31
+ PermissionsBitField.Flags.AddReactions,
32
+ PermissionsBitField.Flags.ManageMessages,
33
+ PermissionsBitField.Flags.UseExternalEmojis,
34
+ PermissionsBitField.Flags.AttachFiles,
35
+ PermissionsBitField.Flags.Connect,
36
+ PermissionsBitField.Flags.Speak,
37
+ PermissionsBitField.Flags.ManageRoles,
38
+ PermissionsBitField.Flags.ManageEvents,
39
+ PermissionsBitField.Flags.CreateEvents,
40
+ ],
41
+ scopes = ['bot', 'applications.commands', 'identify', 'email'],
42
+ guildId,
43
+ disableGuildSelect = false,
44
+ state,
45
+ redirectUri,
46
+ responseType,
47
+ }: GenerateInstallUrlOptions): string {
48
+ const permissionsBitField = new PermissionsBitField(permissions)
49
+ const permissionsValue = permissionsBitField.bitfield.toString()
50
+
51
+ const url = new URL('https://discord.com/api/oauth2/authorize')
52
+ url.searchParams.set('client_id', clientId)
53
+ url.searchParams.set('permissions', permissionsValue)
54
+ url.searchParams.set('scope', scopes.join(' '))
55
+
56
+ if (guildId) {
57
+ url.searchParams.set('guild_id', guildId)
58
+ }
59
+
60
+ if (disableGuildSelect) {
61
+ url.searchParams.set('disable_guild_select', 'true')
62
+ }
63
+
64
+ if (state) {
65
+ url.searchParams.set('state', state)
66
+ }
67
+
68
+ if (redirectUri) {
69
+ url.searchParams.set('redirect_uri', redirectUri)
70
+ }
71
+
72
+ if (responseType) {
73
+ url.searchParams.set('response_type', responseType)
74
+ }
75
+
76
+ return url.toString()
77
+ }
78
+
79
+ export const KIMAKI_GATEWAY_APP_ID =
80
+ process.env.KIMAKI_GATEWAY_APP_ID || '1477605701202481173'
81
+ export const KIMAKI_WEBSITE_URL = process.env.KIMAKI_WEBSITE_URL || 'https://kimaki.xyz'
82
+
83
+ export function generateDiscordInstallUrlForBot({
84
+ appId,
85
+ mode,
86
+ clientId,
87
+ clientSecret,
88
+ gatewayCallbackUrl,
89
+ reachableUrl,
90
+ }: {
91
+ appId: string
92
+ mode: BotMode
93
+ clientId: string | null
94
+ clientSecret: string | null
95
+ /** Optional external URL to redirect to after OAuth completes instead of the
96
+ * default success page. The website appends ?guild_id=<id> before redirecting. */
97
+ gatewayCallbackUrl?: string
98
+ /** When set (KIMAKI_INTERNET_REACHABLE_URL), the website stores this URL in
99
+ * gateway_clients.reachable_url so the gateway-proxy connects outbound. */
100
+ reachableUrl?: string
101
+ }): Error | string {
102
+ if (mode !== 'gateway') {
103
+ return generateBotInstallUrl({ clientId: appId })
104
+ }
105
+
106
+ if (!clientId || !clientSecret) {
107
+ return new Error('Gateway credentials are missing from local database')
108
+ }
109
+
110
+ // In gateway mode, redirect to the website's /discord-install route.
111
+ // This initiates the better-auth OAuth flow with clientId/clientSecret
112
+ // as additionalData, which better-auth stores in its verification table
113
+ // and recovers after Discord redirects back to the callback.
114
+ // Use a kimaki-specific callback field name to avoid ambiguity with
115
+ // better-auth's own callbackURL state field.
116
+ const url = new URL(`${KIMAKI_WEBSITE_URL}/discord-install`)
117
+ url.searchParams.set('clientId', clientId)
118
+ url.searchParams.set('clientSecret', clientSecret)
119
+ if (gatewayCallbackUrl) {
120
+ url.searchParams.set('kimakiCallbackUrl', gatewayCallbackUrl)
121
+ }
122
+ if (reachableUrl) {
123
+ url.searchParams.set('reachableUrl', reachableUrl)
124
+ }
125
+ return url.toString()
126
+ }
127
+
128
+ export function deduplicateByKey<T, K>(arr: T[], keyFn: (item: T) => K): T[] {
129
+ const seen = new Set<K>()
130
+ return arr.filter((item) => {
131
+ const key = keyFn(item)
132
+ if (seen.has(key)) {
133
+ return false
134
+ }
135
+ seen.add(key)
136
+ return true
137
+ })
138
+ }
139
+
140
+ // Delegates to errore.isAbortError (walks cause chain for AbortError instances),
141
+ // then falls back to opencode server-specific abort patterns that aren't
142
+ // errore.AbortError but still represent aborted operations.
143
+ export function isAbortError(error: unknown): error is Error {
144
+ if (errore.isAbortError(error)) return true
145
+ if (!(error instanceof Error)) return false
146
+ return (
147
+ error.name === 'MessageAbortedError' ||
148
+ error.message?.includes('aborted') === true
149
+ )
150
+ }
151
+
152
+ const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
153
+
154
+ const TIME_DIVISIONS: Array<{
155
+ amount: number
156
+ name: Intl.RelativeTimeFormatUnit
157
+ }> = [
158
+ { amount: 60, name: 'seconds' },
159
+ { amount: 60, name: 'minutes' },
160
+ { amount: 24, name: 'hours' },
161
+ { amount: 7, name: 'days' },
162
+ { amount: 4.34524, name: 'weeks' },
163
+ { amount: 12, name: 'months' },
164
+ { amount: Number.POSITIVE_INFINITY, name: 'years' },
165
+ ]
166
+
167
+ export function formatDistanceToNow(date: Date): string {
168
+ let duration = (date.getTime() - Date.now()) / 1000
169
+
170
+ for (const division of TIME_DIVISIONS) {
171
+ if (Math.abs(duration) < division.amount) {
172
+ return rtf.format(Math.round(duration), division.name)
173
+ }
174
+ duration /= division.amount
175
+ }
176
+ return rtf.format(Math.round(duration), 'years')
177
+ }
178
+
179
+ const dtf = new Intl.DateTimeFormat('en-US', {
180
+ month: 'short',
181
+ day: 'numeric',
182
+ year: 'numeric',
183
+ hour: 'numeric',
184
+ minute: '2-digit',
185
+ hour12: true,
186
+ })
187
+
188
+ export function formatDateTime(date: Date): string {
189
+ return dtf.format(date)
190
+ }
191
+
192
+ // Comprehensive ANSI escape sequence regex covering CSI, OSC, and related sequences.
193
+ // Valid string terminator sequences are BEL, ESC\, and 0x9c.
194
+ const ANSI_REGEX = (() => {
195
+ const ST = '(?:\\u0007|\\u001B\\u005C|\\u009C)'
196
+ const osc = `(?:\\u001B\\][\\s\\S]*?${ST})`
197
+ const csi =
198
+ '[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]'
199
+ return new RegExp(`${osc}|${csi}`, 'g')
200
+ })()
201
+
202
+ export function stripAnsi(str: string): string {
203
+ return str.replace(ANSI_REGEX, '')
204
+ }
205
+
206
+ export function abbreviatePath(fullPath: string): string {
207
+ const home = os.homedir()
208
+ if (fullPath.startsWith(home)) {
209
+ return '~' + fullPath.slice(home.length)
210
+ }
211
+ return fullPath
212
+ }
@@ -0,0 +1,51 @@
1
+ // Voice attachment detection helpers.
2
+ // Normalizes Discord attachment heuristics for voice-message detection so
3
+ // message routing, transcription, and empty-prompt guards all agree even when
4
+ // Discord omits contentType on uploaded audio attachments.
5
+
6
+ import path from 'node:path'
7
+
8
+ const VOICE_ATTACHMENT_EXTENSIONS = new Set<string>([
9
+ '.m4a',
10
+ '.mp3',
11
+ '.mp4',
12
+ '.oga',
13
+ '.ogg',
14
+ '.opus',
15
+ '.wav',
16
+ ])
17
+
18
+ export type VoiceAttachmentLike = {
19
+ contentType?: string | null
20
+ name?: string | null
21
+ duration?: number | null
22
+ waveform?: string | null
23
+ }
24
+
25
+ export function getVoiceAttachmentMatchReason(
26
+ attachment: VoiceAttachmentLike,
27
+ ): string | null {
28
+ const contentType = attachment.contentType?.trim().toLowerCase() || ''
29
+ if (contentType.startsWith('audio/')) {
30
+ return `contentType:${contentType}`
31
+ }
32
+
33
+ if (typeof attachment.duration === 'number' && attachment.duration > 0) {
34
+ return `duration:${attachment.duration}`
35
+ }
36
+
37
+ if (attachment.waveform?.trim()) {
38
+ return 'waveform'
39
+ }
40
+
41
+ const extension = path.extname(attachment.name || '').toLowerCase()
42
+ if (VOICE_ATTACHMENT_EXTENSIONS.has(extension)) {
43
+ return `extension:${extension}`
44
+ }
45
+
46
+ return null
47
+ }
48
+
49
+ export function isVoiceAttachment(attachment: VoiceAttachmentLike): boolean {
50
+ return getVoiceAttachmentMatchReason(attachment) !== null
51
+ }