@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,591 @@
1
+ // Discord-specific utility functions.
2
+ // Handles markdown splitting for Discord's 2000-char limit, code block escaping,
3
+ // thread message sending, and channel metadata extraction from topic tags.
4
+ import { ChannelType, GuildMember, MessageFlags, PermissionsBitField, } from 'discord.js';
5
+ import { REST, Routes } from 'discord.js';
6
+ import { discordApiUrl } from './discord-urls.js';
7
+ import { Lexer } from 'marked';
8
+ import { splitTablesFromMarkdown } from './format-tables.js';
9
+ import { getChannelDirectory, getThreadWorktree } from './database.js';
10
+ import { limitHeadingDepth } from './limit-heading-depth.js';
11
+ import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js';
12
+ import { createLogger, LogPrefix } from './logger.js';
13
+ import * as errore from 'errore';
14
+ import mime from 'mime';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ const discordLogger = createLogger(LogPrefix.DISCORD);
18
+ /**
19
+ * Centralized permission check for Kimaki bot access.
20
+ * Returns true if the member has permission to use the bot:
21
+ * - Server owner, Administrator, Manage Server, or "Kimaki" role (case-insensitive).
22
+ * Returns false if member is null or has the "no-kimaki" role (overrides all).
23
+ */
24
+ export function hasKimakiBotPermission(member, guild) {
25
+ if (!member) {
26
+ return false;
27
+ }
28
+ const hasNoKimakiRole = hasRoleByName(member, 'no-kimaki', guild);
29
+ if (hasNoKimakiRole) {
30
+ return false;
31
+ }
32
+ const memberPermissions = member instanceof GuildMember
33
+ ? member.permissions
34
+ : new PermissionsBitField(BigInt(member.permissions));
35
+ const ownerId = member instanceof GuildMember ? member.guild.ownerId : guild?.ownerId;
36
+ const memberId = member instanceof GuildMember ? member.id : member.user.id;
37
+ const isOwner = ownerId ? memberId === ownerId : false;
38
+ const isAdmin = memberPermissions.has(PermissionsBitField.Flags.Administrator);
39
+ const canManageServer = memberPermissions.has(PermissionsBitField.Flags.ManageGuild);
40
+ const hasKimakiRole = hasRoleByName(member, 'kimaki', guild);
41
+ return isOwner || isAdmin || canManageServer || hasKimakiRole;
42
+ }
43
+ function hasRoleByName(member, roleName, guild) {
44
+ const target = roleName.toLowerCase();
45
+ if (member instanceof GuildMember) {
46
+ return member.roles.cache.some((role) => role.name.toLowerCase() === target);
47
+ }
48
+ if (!guild) {
49
+ return false;
50
+ }
51
+ const roleIds = Array.isArray(member.roles) ? member.roles : [];
52
+ for (const roleId of roleIds) {
53
+ const role = guild.roles.cache.get(roleId);
54
+ if (role?.name.toLowerCase() === target) {
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+ /**
61
+ * Check if the member has the "no-kimaki" role that blocks bot access.
62
+ * Separate from hasKimakiBotPermission so callers can show a specific error message.
63
+ */
64
+ export function hasNoKimakiRole(member) {
65
+ if (!member?.roles?.cache) {
66
+ return false;
67
+ }
68
+ return member.roles.cache.some((role) => role.name.toLowerCase() === 'no-kimaki');
69
+ }
70
+ /**
71
+ * React to a thread's starter message with an emoji.
72
+ * Thread ID equals the starter message ID in Discord.
73
+ */
74
+ export async function reactToThread({ rest, threadId, channelId, emoji, }) {
75
+ const parentChannelId = await (async () => {
76
+ if (channelId) {
77
+ return channelId;
78
+ }
79
+ // Fetch the thread to get its parent channel ID
80
+ const threadResult = await errore.tryAsync(() => {
81
+ return rest.get(Routes.channel(threadId));
82
+ });
83
+ if (threadResult instanceof Error) {
84
+ discordLogger.warn(`Failed to fetch thread ${threadId}:`, threadResult.message);
85
+ return null;
86
+ }
87
+ return threadResult.parent_id || null;
88
+ })();
89
+ if (!parentChannelId) {
90
+ discordLogger.warn(`Could not resolve parent channel for thread ${threadId}`);
91
+ return;
92
+ }
93
+ // React to the thread starter message in the parent channel.
94
+ // Thread ID equals the starter message ID for threads created from messages.
95
+ const result = await errore.tryAsync(() => {
96
+ return rest.put(Routes.channelMessageOwnReaction(parentChannelId, threadId, encodeURIComponent(emoji)));
97
+ });
98
+ if (result instanceof Error) {
99
+ discordLogger.warn(`Failed to react to thread ${threadId} with ${emoji}:`, result.message);
100
+ }
101
+ }
102
+ export async function archiveThread({ rest, threadId, parentChannelId, sessionId, client, archiveDelay = 0, }) {
103
+ await reactToThread({
104
+ rest,
105
+ threadId,
106
+ channelId: parentChannelId,
107
+ emoji: '📁',
108
+ });
109
+ if (client && sessionId) {
110
+ const updateResult = await errore.tryAsync({
111
+ try: async () => {
112
+ const sessionResponse = await client.session.get({
113
+ sessionID: sessionId,
114
+ });
115
+ if (!sessionResponse.data) {
116
+ return;
117
+ }
118
+ const currentTitle = sessionResponse.data.title || '';
119
+ const newTitle = currentTitle.startsWith('📁')
120
+ ? currentTitle
121
+ : `📁 ${currentTitle}`.trim();
122
+ await client.session.update({
123
+ sessionID: sessionId,
124
+ title: newTitle,
125
+ });
126
+ },
127
+ catch: (e) => new Error('Failed to update session title', { cause: e }),
128
+ });
129
+ if (updateResult instanceof Error) {
130
+ discordLogger.warn(`[archive-thread] ${updateResult.message}`);
131
+ }
132
+ const abortResult = await errore.tryAsync({
133
+ try: async () => {
134
+ await client.session.abort({ sessionID: sessionId });
135
+ },
136
+ catch: (e) => new Error('Failed to abort session', { cause: e }),
137
+ });
138
+ if (abortResult instanceof Error) {
139
+ discordLogger.warn(`[archive-thread] ${abortResult.message}`);
140
+ }
141
+ }
142
+ if (archiveDelay > 0) {
143
+ await new Promise((resolve) => {
144
+ setTimeout(() => {
145
+ resolve();
146
+ }, archiveDelay);
147
+ });
148
+ }
149
+ await rest.patch(Routes.channel(threadId), {
150
+ body: { archived: true },
151
+ });
152
+ }
153
+ /** Remove Discord mentions from text so they don't appear in thread titles */
154
+ export function stripMentions(text) {
155
+ return text
156
+ .replace(/<@!?\d+>/g, '') // user mentions
157
+ .replace(/<@&\d+>/g, '') // role mentions
158
+ .replace(/<#\d+>/g, '') // channel mentions
159
+ .replace(/\s+/g, ' ')
160
+ .trim();
161
+ }
162
+ export const SILENT_MESSAGE_FLAGS = 4 | 4096;
163
+ // Same as SILENT but without SuppressNotifications - triggers badge/notification
164
+ export const NOTIFY_MESSAGE_FLAGS = 4;
165
+ export function escapeBackticksInCodeBlocks(markdown) {
166
+ const lexer = new Lexer();
167
+ const tokens = lexer.lex(markdown);
168
+ let result = '';
169
+ for (const token of tokens) {
170
+ if (token.type === 'code') {
171
+ const escapedCode = token.text.replace(/`/g, '\\`');
172
+ result += '```' + (token.lang || '') + '\n' + escapedCode + '\n```\n';
173
+ }
174
+ else {
175
+ result += token.raw;
176
+ }
177
+ }
178
+ return result;
179
+ }
180
+ export function splitMarkdownForDiscord({ content, maxLength, }) {
181
+ if (content.length <= maxLength) {
182
+ return [content];
183
+ }
184
+ const lexer = new Lexer();
185
+ const tokens = lexer.lex(content);
186
+ const lines = [];
187
+ const ensureNewlineBeforeCode = () => {
188
+ const last = lines[lines.length - 1];
189
+ if (!last) {
190
+ return;
191
+ }
192
+ if (last.text.endsWith('\n')) {
193
+ return;
194
+ }
195
+ lines.push({
196
+ text: '\n',
197
+ inCodeBlock: false,
198
+ lang: '',
199
+ isOpeningFence: false,
200
+ isClosingFence: false,
201
+ });
202
+ };
203
+ for (const token of tokens) {
204
+ if (token.type === 'code') {
205
+ ensureNewlineBeforeCode();
206
+ const lang = token.lang || '';
207
+ lines.push({
208
+ text: '```' + lang + '\n',
209
+ inCodeBlock: false,
210
+ lang,
211
+ isOpeningFence: true,
212
+ isClosingFence: false,
213
+ });
214
+ const codeLines = token.text.split('\n');
215
+ for (const codeLine of codeLines) {
216
+ lines.push({
217
+ text: codeLine + '\n',
218
+ inCodeBlock: true,
219
+ lang,
220
+ isOpeningFence: false,
221
+ isClosingFence: false,
222
+ });
223
+ }
224
+ lines.push({
225
+ text: '```\n',
226
+ inCodeBlock: false,
227
+ lang: '',
228
+ isOpeningFence: false,
229
+ isClosingFence: true,
230
+ });
231
+ }
232
+ else {
233
+ const rawLines = token.raw.split('\n');
234
+ for (let i = 0; i < rawLines.length; i++) {
235
+ const isLast = i === rawLines.length - 1;
236
+ const text = isLast ? rawLines[i] : rawLines[i] + '\n';
237
+ if (text) {
238
+ lines.push({
239
+ text,
240
+ inCodeBlock: false,
241
+ lang: '',
242
+ isOpeningFence: false,
243
+ isClosingFence: false,
244
+ });
245
+ }
246
+ }
247
+ }
248
+ }
249
+ const chunks = [];
250
+ let currentChunk = '';
251
+ let currentLang = null;
252
+ // helper to split a long line into smaller pieces at word boundaries or hard breaks
253
+ const splitLongLine = (text, available, inCode) => {
254
+ const pieces = [];
255
+ let remaining = text;
256
+ while (remaining.length > available) {
257
+ let splitAt = available;
258
+ // for non-code, try to split at word boundary
259
+ if (!inCode) {
260
+ const lastSpace = remaining.lastIndexOf(' ', available);
261
+ if (lastSpace > available * 0.5) {
262
+ splitAt = lastSpace + 1;
263
+ }
264
+ }
265
+ pieces.push(remaining.slice(0, splitAt));
266
+ remaining = remaining.slice(splitAt);
267
+ }
268
+ if (remaining) {
269
+ pieces.push(remaining);
270
+ }
271
+ return pieces;
272
+ };
273
+ const closingFence = '```\n';
274
+ for (const line of lines) {
275
+ // openingFenceSize accounts for the fence text when starting a fresh chunk
276
+ const openingFenceSize = currentChunk.length === 0 && (line.inCodeBlock || line.isOpeningFence)
277
+ ? ('```' + line.lang + '\n').length
278
+ : 0;
279
+ // When opening fence starts a fresh chunk, its size is in openingFenceSize.
280
+ // Otherwise count it normally so the overflow check doesn't miss the fence text.
281
+ const lineLength = line.isOpeningFence && currentChunk.length === 0 ? 0 : line.text.length;
282
+ const activeFenceOverhead = currentLang !== null || openingFenceSize > 0 ? closingFence.length : 0;
283
+ const wouldExceed = currentChunk.length +
284
+ openingFenceSize +
285
+ lineLength +
286
+ activeFenceOverhead >
287
+ maxLength;
288
+ if (wouldExceed) {
289
+ // handle case where single line is longer than maxLength
290
+ if (line.text.length > maxLength) {
291
+ // first, flush current chunk if any
292
+ if (currentChunk) {
293
+ if (currentLang !== null) {
294
+ currentChunk += '```\n';
295
+ }
296
+ chunks.push(currentChunk);
297
+ currentChunk = '';
298
+ }
299
+ // calculate overhead for code block markers
300
+ const codeBlockOverhead = line.inCodeBlock
301
+ ? ('```' + line.lang + '\n').length + '```\n'.length
302
+ : 0;
303
+ // ensure at least 10 chars available, even if maxLength is very small
304
+ const availablePerChunk = Math.max(10, maxLength - codeBlockOverhead - 50);
305
+ const pieces = splitLongLine(line.text, availablePerChunk, line.inCodeBlock);
306
+ for (let i = 0; i < pieces.length; i++) {
307
+ const piece = pieces[i];
308
+ if (line.inCodeBlock) {
309
+ chunks.push('```' + line.lang + '\n' + piece + '```\n');
310
+ }
311
+ else {
312
+ chunks.push(piece);
313
+ }
314
+ }
315
+ currentLang = null;
316
+ continue;
317
+ }
318
+ // normal case: line fits in a chunk but current chunk would overflow
319
+ if (currentChunk) {
320
+ if (currentLang !== null) {
321
+ currentChunk += '```\n';
322
+ }
323
+ chunks.push(currentChunk);
324
+ if (line.isClosingFence && currentLang !== null) {
325
+ currentChunk = '';
326
+ currentLang = null;
327
+ continue;
328
+ }
329
+ if (line.inCodeBlock || line.isOpeningFence) {
330
+ const lang = line.lang;
331
+ currentChunk = '```' + lang + '\n';
332
+ if (!line.isOpeningFence) {
333
+ currentChunk += line.text;
334
+ }
335
+ currentLang = lang;
336
+ }
337
+ else {
338
+ currentChunk = line.text;
339
+ currentLang = null;
340
+ }
341
+ }
342
+ else {
343
+ // currentChunk is empty but line still exceeds - shouldn't happen after above check
344
+ const openingFence = line.inCodeBlock || line.isOpeningFence;
345
+ const openingFenceSize = openingFence
346
+ ? ('```' + line.lang + '\n').length
347
+ : 0;
348
+ if (line.text.length + openingFenceSize + activeFenceOverhead >
349
+ maxLength) {
350
+ const fencedOverhead = openingFence
351
+ ? ('```' + line.lang + '\n').length + closingFence.length
352
+ : 0;
353
+ const availablePerChunk = Math.max(10, maxLength - fencedOverhead - 50);
354
+ const pieces = splitLongLine(line.text, availablePerChunk, line.inCodeBlock);
355
+ for (const piece of pieces) {
356
+ if (openingFence) {
357
+ chunks.push('```' + line.lang + '\n' + piece + closingFence);
358
+ }
359
+ else {
360
+ chunks.push(piece);
361
+ }
362
+ }
363
+ currentChunk = '';
364
+ currentLang = null;
365
+ }
366
+ else {
367
+ if (openingFence) {
368
+ currentChunk = '```' + line.lang + '\n';
369
+ if (!line.isOpeningFence) {
370
+ currentChunk += line.text;
371
+ }
372
+ currentLang = line.lang;
373
+ }
374
+ else {
375
+ currentChunk = line.text;
376
+ currentLang = null;
377
+ }
378
+ }
379
+ }
380
+ }
381
+ else {
382
+ currentChunk += line.text;
383
+ if (line.inCodeBlock || line.isOpeningFence) {
384
+ currentLang = line.lang;
385
+ }
386
+ else if (line.isClosingFence) {
387
+ currentLang = null;
388
+ }
389
+ }
390
+ }
391
+ if (currentChunk) {
392
+ if (currentLang !== null) {
393
+ currentChunk += closingFence;
394
+ }
395
+ chunks.push(currentChunk);
396
+ }
397
+ return chunks;
398
+ }
399
+ export async function sendThreadMessage(thread, content, options) {
400
+ const MAX_LENGTH = 2000;
401
+ // Split content into text and CV2 component segments (tables → Container components)
402
+ const segments = splitTablesFromMarkdown(content);
403
+ const baseFlags = options?.flags ?? SILENT_MESSAGE_FLAGS;
404
+ let firstMessage;
405
+ for (const segment of segments) {
406
+ if (segment.type === 'components') {
407
+ const message = await thread.send({
408
+ components: segment.components,
409
+ flags: MessageFlags.IsComponentsV2 | baseFlags,
410
+ });
411
+ if (!firstMessage) {
412
+ firstMessage = message;
413
+ }
414
+ continue;
415
+ }
416
+ // Apply text transformations to text segments
417
+ let text = segment.text;
418
+ text = unnestCodeBlocksFromLists(text);
419
+ text = limitHeadingDepth(text);
420
+ text = escapeBackticksInCodeBlocks(text);
421
+ if (!text.trim()) {
422
+ continue;
423
+ }
424
+ const sendFlags = options?.flags ?? SILENT_MESSAGE_FLAGS;
425
+ const chunks = splitMarkdownForDiscord({
426
+ content: text,
427
+ maxLength: MAX_LENGTH,
428
+ });
429
+ if (chunks.length > 1) {
430
+ discordLogger.log(`MESSAGE: Splitting ${text.length} chars into ${chunks.length} messages`);
431
+ }
432
+ for (let chunk of chunks) {
433
+ if (!chunk) {
434
+ continue;
435
+ }
436
+ // Safety net: hard-truncate if splitting still produced an oversized chunk
437
+ if (chunk.length > MAX_LENGTH) {
438
+ chunk = chunk.slice(0, MAX_LENGTH - 4) + '...';
439
+ }
440
+ const message = await thread.send({ content: chunk, flags: sendFlags });
441
+ if (!firstMessage) {
442
+ firstMessage = message;
443
+ }
444
+ }
445
+ }
446
+ return firstMessage;
447
+ }
448
+ export async function resolveTextChannel(channel) {
449
+ if (!channel) {
450
+ return null;
451
+ }
452
+ if (channel.type === ChannelType.GuildText) {
453
+ return channel;
454
+ }
455
+ if (channel.type === ChannelType.PublicThread ||
456
+ channel.type === ChannelType.PrivateThread ||
457
+ channel.type === ChannelType.AnnouncementThread) {
458
+ const parentId = channel.parentId;
459
+ if (parentId) {
460
+ const parent = await channel.guild.channels.fetch(parentId);
461
+ if (parent?.type === ChannelType.GuildText) {
462
+ return parent;
463
+ }
464
+ }
465
+ }
466
+ return null;
467
+ }
468
+ export function escapeDiscordFormatting(text) {
469
+ return text.replace(/```/g, '\\`\\`\\`').replace(/````/g, '\\`\\`\\`\\`');
470
+ }
471
+ export async function getKimakiMetadata(textChannel) {
472
+ if (!textChannel) {
473
+ return {};
474
+ }
475
+ const channelConfig = await getChannelDirectory(textChannel.id);
476
+ if (!channelConfig) {
477
+ return {};
478
+ }
479
+ return {
480
+ projectDirectory: channelConfig.directory,
481
+ };
482
+ }
483
+ /**
484
+ * Resolve project directory from an autocomplete interaction.
485
+ * Uses interaction.channelId (always available from raw payload) instead of
486
+ * interaction.channel (cache-based getter, often null with gateway-proxy).
487
+ * Checks the channel ID directly in DB, then tries thread worktree lookup,
488
+ * then falls back to fetching the channel to resolve thread parent.
489
+ */
490
+ export async function resolveProjectDirectoryFromAutocomplete(interaction) {
491
+ const channelId = interaction.channelId;
492
+ // Direct channel lookup — works when the command is run from a project text channel
493
+ const channelConfig = await getChannelDirectory(channelId);
494
+ if (channelConfig) {
495
+ return channelConfig.directory;
496
+ }
497
+ // If we're in a thread, try worktree info first (has project_directory)
498
+ const worktreeInfo = await getThreadWorktree(channelId);
499
+ if (worktreeInfo?.project_directory) {
500
+ return worktreeInfo.project_directory;
501
+ }
502
+ // Thread fallback: resolve parent channel ID and look up its directory.
503
+ // Try cached channel first, then fetch if cache misses (gateway-proxy scenario).
504
+ const cachedParentId = interaction.channel?.isThread() ? interaction.channel.parentId : null;
505
+ if (cachedParentId) {
506
+ const parentConfig = await getChannelDirectory(cachedParentId);
507
+ if (parentConfig) {
508
+ return parentConfig.directory;
509
+ }
510
+ }
511
+ // Last resort: fetch the channel from Discord API to get parentId for threads
512
+ // when the channel isn't cached at all (common with gateway-proxy).
513
+ if (!cachedParentId) {
514
+ const fetched = await errore.tryAsync({
515
+ try: () => { return interaction.client.channels.fetch(channelId); },
516
+ catch: (e) => { return e; },
517
+ });
518
+ if (!(fetched instanceof Error) && fetched?.isThread() && fetched.parentId) {
519
+ const parentConfig = await getChannelDirectory(fetched.parentId);
520
+ if (parentConfig) {
521
+ return parentConfig.directory;
522
+ }
523
+ }
524
+ }
525
+ return undefined;
526
+ }
527
+ /**
528
+ * Resolve the working directory for a channel or thread.
529
+ * Returns both the base project directory (for server init) and the working directory
530
+ * (worktree directory if in a worktree thread, otherwise same as projectDirectory).
531
+ * This prevents commands from accidentally running in the base project dir when a
532
+ * worktree is active — the bug that caused /diff, /compact, etc. to use wrong cwd.
533
+ */
534
+ export async function resolveWorkingDirectory({ channel, }) {
535
+ const isThread = [
536
+ ChannelType.PublicThread,
537
+ ChannelType.PrivateThread,
538
+ ChannelType.AnnouncementThread,
539
+ ].includes(channel.type);
540
+ const textChannel = isThread
541
+ ? await resolveTextChannel(channel)
542
+ : channel;
543
+ const metadata = await getKimakiMetadata(textChannel);
544
+ if (!metadata.projectDirectory) {
545
+ return undefined;
546
+ }
547
+ let workingDirectory = metadata.projectDirectory;
548
+ if (isThread) {
549
+ const worktreeInfo = await getThreadWorktree(channel.id);
550
+ if (worktreeInfo?.status === 'ready' && worktreeInfo.worktree_directory) {
551
+ workingDirectory = worktreeInfo.worktree_directory;
552
+ }
553
+ }
554
+ return {
555
+ projectDirectory: metadata.projectDirectory,
556
+ workingDirectory,
557
+ };
558
+ }
559
+ /**
560
+ * Upload files to a Discord thread/channel in a single message.
561
+ * Sending all files in one message causes Discord to display images in a grid layout.
562
+ */
563
+ export async function uploadFilesToDiscord({ threadId, botToken, files, }) {
564
+ if (files.length === 0) {
565
+ return;
566
+ }
567
+ // Build attachments array for all files
568
+ const attachments = files.map((file, index) => ({
569
+ id: index,
570
+ filename: path.basename(file),
571
+ }));
572
+ const formData = new FormData();
573
+ formData.append('payload_json', JSON.stringify({ attachments }));
574
+ // Append each file with its array index, with correct MIME type for grid display
575
+ files.forEach((file, index) => {
576
+ const buffer = fs.readFileSync(file);
577
+ const mimeType = mime.getType(file) || 'application/octet-stream';
578
+ formData.append(`files[${index}]`, new Blob([buffer], { type: mimeType }), path.basename(file));
579
+ });
580
+ const response = await fetch(discordApiUrl(`/channels/${threadId}/messages`), {
581
+ method: 'POST',
582
+ headers: {
583
+ Authorization: `Bot ${botToken}`,
584
+ },
585
+ body: formData,
586
+ });
587
+ if (!response.ok) {
588
+ const error = await response.text();
589
+ throw new Error(`Discord API error: ${response.status} - ${error}`);
590
+ }
591
+ }