@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,147 @@
1
+ // Wait utilities for polling session completion.
2
+ // Used by `kimaki send --wait` to block until a session finishes,
3
+ // then output the session markdown to stdout.
4
+
5
+ import { getThreadSession } from './database.js'
6
+ import { initializeOpencodeForDirectory } from './opencode.js'
7
+ import { ShareMarkdown } from './markdown.js'
8
+ import { createLogger, LogPrefix } from './logger.js'
9
+
10
+ const waitLogger = createLogger(LogPrefix.SESSION)
11
+
12
+ /**
13
+ * Poll the kimaki database until a session ID appears for the given thread.
14
+ * The bot writes this mapping in session-handler.ts:551 when it picks up
15
+ * the thread and creates/reuses a session.
16
+ */
17
+ export async function waitForSessionId({
18
+ threadId,
19
+ timeoutMs = 120_000,
20
+ }: {
21
+ threadId: string
22
+ timeoutMs?: number
23
+ }): Promise<string> {
24
+ const startTime = Date.now()
25
+ const pollIntervalMs = 2_000
26
+
27
+ while (Date.now() - startTime < timeoutMs) {
28
+ const sessionId = await getThreadSession(threadId)
29
+ if (sessionId) {
30
+ waitLogger.log(`Session ID resolved: ${sessionId}`)
31
+ return sessionId
32
+ }
33
+ await new Promise((resolve) => {
34
+ setTimeout(resolve, pollIntervalMs)
35
+ })
36
+ }
37
+
38
+ throw new Error(
39
+ `Timed out waiting for session ID (thread: ${threadId}, timeout: ${timeoutMs}ms)`,
40
+ )
41
+ }
42
+
43
+ /**
44
+ * Poll the OpenCode SDK until the session's last assistant message
45
+ * has `time.completed` set, meaning the model finished responding.
46
+ */
47
+ export async function waitForSessionComplete({
48
+ projectDirectory,
49
+ sessionId,
50
+ timeoutMs = 30 * 60 * 1000,
51
+ }: {
52
+ projectDirectory: string
53
+ sessionId: string
54
+ timeoutMs?: number
55
+ }): Promise<void> {
56
+ const pollIntervalMs = 3_000
57
+ const startTime = Date.now()
58
+
59
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
60
+ if (getClient instanceof Error) {
61
+ throw new Error(
62
+ `Failed to connect to OpenCode server: ${getClient.message}`,
63
+ {
64
+ cause: getClient,
65
+ },
66
+ )
67
+ }
68
+
69
+ while (Date.now() - startTime < timeoutMs) {
70
+ const messagesResponse = await getClient().session.messages({
71
+ sessionID: sessionId,
72
+ })
73
+ const messages = messagesResponse.data || []
74
+
75
+ // Find the last assistant message
76
+ const lastAssistant = [...messages]
77
+ .reverse()
78
+ .find((m) => m.info.role === 'assistant')
79
+
80
+ if (
81
+ lastAssistant &&
82
+ lastAssistant.info.role === 'assistant' &&
83
+ lastAssistant.info.time.completed
84
+ ) {
85
+ waitLogger.log(`Session ${sessionId} completed`)
86
+ return
87
+ }
88
+
89
+ await new Promise((resolve) => {
90
+ setTimeout(resolve, pollIntervalMs)
91
+ })
92
+ }
93
+
94
+ throw new Error(
95
+ `Timed out waiting for session completion (session: ${sessionId}, timeout: ${timeoutMs}ms)`,
96
+ )
97
+ }
98
+
99
+ /**
100
+ * Wait for session completion and output the session markdown to stdout.
101
+ * Orchestrates the full wait flow: session ID resolution -> completion -> output.
102
+ */
103
+ export async function waitAndOutputSession({
104
+ threadId,
105
+ projectDirectory,
106
+ sessionIdTimeoutMs,
107
+ completionTimeoutMs,
108
+ }: {
109
+ threadId: string
110
+ projectDirectory: string
111
+ sessionIdTimeoutMs?: number
112
+ completionTimeoutMs?: number
113
+ }): Promise<void> {
114
+ waitLogger.log('Waiting for session ID...')
115
+ const sessionId = await waitForSessionId({
116
+ threadId,
117
+ timeoutMs: sessionIdTimeoutMs,
118
+ })
119
+
120
+ waitLogger.log(`Waiting for session ${sessionId} to complete...`)
121
+ await waitForSessionComplete({
122
+ projectDirectory,
123
+ sessionId,
124
+ timeoutMs: completionTimeoutMs,
125
+ })
126
+
127
+ waitLogger.log('Generating session output...')
128
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
129
+ if (getClient instanceof Error) {
130
+ throw new Error(
131
+ `Failed to connect to OpenCode server: ${getClient.message}`,
132
+ {
133
+ cause: getClient,
134
+ },
135
+ )
136
+ }
137
+
138
+ const markdown = new ShareMarkdown(getClient())
139
+ const result = await markdown.generate({ sessionID: sessionId })
140
+ if (result instanceof Error) {
141
+ throw new Error(`Failed to generate session markdown: ${result.message}`, {
142
+ cause: result,
143
+ })
144
+ }
145
+
146
+ process.stdout.write(result)
147
+ }
@@ -0,0 +1,101 @@
1
+ // In-process WebSocket-to-TCP bridge (websockify replacement).
2
+ // Accepts WebSocket connections and pipes raw bytes to/from a TCP target.
3
+ // Used by /screenshare to bridge noVNC (WebSocket) to a VNC server (TCP).
4
+ // Supports the 'binary' subprotocol required by noVNC.
5
+
6
+ import { WebSocketServer, WebSocket } from 'ws'
7
+ import net from 'node:net'
8
+ import { createLogger } from './logger.js'
9
+
10
+ const logger = createLogger('SCREEN')
11
+
12
+ type WebsockifyOptions = {
13
+ /** Port for the WebSocket server (0 = auto-assign) */
14
+ wsPort: number
15
+ /** TCP target host */
16
+ tcpHost: string
17
+ /** TCP target port */
18
+ tcpPort: number
19
+ }
20
+
21
+ type WebsockifyInstance = {
22
+ wss: WebSocketServer
23
+ /** Resolved port (useful when wsPort=0) */
24
+ port: number
25
+ close: () => void
26
+ }
27
+
28
+ export function startWebsockify({
29
+ wsPort,
30
+ tcpHost,
31
+ tcpPort,
32
+ }: WebsockifyOptions): Promise<WebsockifyInstance> {
33
+ return new Promise((resolve, reject) => {
34
+ const wss = new WebSocketServer({
35
+ port: wsPort,
36
+ // noVNC negotiates the 'binary' subprotocol
37
+ handleProtocols: (protocols) => {
38
+ if (protocols.has('binary')) {
39
+ return 'binary'
40
+ }
41
+ return false
42
+ },
43
+ })
44
+
45
+ wss.on('listening', () => {
46
+ const addr = wss.address()
47
+ const port = typeof addr === 'object' && addr ? addr.port : wsPort
48
+ logger.log(`Websockify listening on port ${port} → ${tcpHost}:${tcpPort}`)
49
+ resolve({
50
+ wss,
51
+ port,
52
+ close: () => {
53
+ for (const client of wss.clients) {
54
+ client.close()
55
+ }
56
+ wss.close()
57
+ },
58
+ })
59
+ })
60
+
61
+ wss.on('error', (err) => {
62
+ reject(new Error('Websockify failed to start', { cause: err }))
63
+ })
64
+
65
+ wss.on('connection', (ws) => {
66
+ const tcp = net.createConnection(tcpPort, tcpHost, () => {
67
+ logger.log(`TCP connection established to ${tcpHost}:${tcpPort}`)
68
+ })
69
+
70
+ tcp.on('data', (data) => {
71
+ if (ws.readyState === WebSocket.OPEN) {
72
+ ws.send(data)
73
+ }
74
+ })
75
+
76
+ ws.on('message', (data: Buffer) => {
77
+ if (!tcp.destroyed) {
78
+ tcp.write(data)
79
+ }
80
+ })
81
+
82
+ ws.on('close', () => {
83
+ tcp.destroy()
84
+ })
85
+
86
+ ws.on('error', (err) => {
87
+ logger.error('WebSocket error:', err)
88
+ tcp.destroy()
89
+ })
90
+
91
+ tcp.on('close', () => {
92
+ ws.close()
93
+ })
94
+
95
+ tcp.on('error', (err) => {
96
+ logger.error('TCP connection error:', err)
97
+ ws.close()
98
+ })
99
+ })
100
+ })
101
+ }
@@ -0,0 +1,64 @@
1
+ // Type definitions for worker thread message passing.
2
+ // Defines the protocol between main thread and GenAI worker for
3
+ // audio streaming, tool calls, and session lifecycle management.
4
+
5
+ // Messages sent from main thread to worker
6
+ export type WorkerInMessage =
7
+ | {
8
+ type: 'init'
9
+ directory: string // Project directory for tools
10
+ systemMessage?: string
11
+ guildId: string
12
+ channelId: string
13
+ appId: string
14
+ geminiApiKey?: string | null
15
+ }
16
+ | {
17
+ type: 'sendRealtimeInput'
18
+ audio?: {
19
+ mimeType: string
20
+ data: string // base64
21
+ }
22
+ audioStreamEnd?: boolean
23
+ }
24
+ | {
25
+ type: 'sendTextInput'
26
+ text: string
27
+ }
28
+ | {
29
+ type: 'interrupt'
30
+ }
31
+ | {
32
+ type: 'stop'
33
+ }
34
+
35
+ // Messages sent from worker to main thread via parentPort
36
+ export type WorkerOutMessage =
37
+ | {
38
+ type: 'assistantOpusPacket'
39
+ packet: ArrayBuffer // Opus encoded audio packet
40
+ }
41
+ | {
42
+ type: 'assistantStartSpeaking'
43
+ }
44
+ | {
45
+ type: 'assistantStopSpeaking'
46
+ }
47
+ | {
48
+ type: 'assistantInterruptSpeaking'
49
+ }
50
+ | {
51
+ type: 'toolCallCompleted'
52
+ sessionId: string
53
+ messageId: string
54
+ data?: unknown
55
+ error?: unknown
56
+ markdown?: string
57
+ }
58
+ | {
59
+ type: 'error'
60
+ error: string
61
+ }
62
+ | {
63
+ type: 'ready'
64
+ }
@@ -0,0 +1,391 @@
1
+ // E2e test for worktree lifecycle: /new-worktree inside an existing thread,
2
+ // then verify the session still works after sdkDirectory switches.
3
+ // Validates that handleDirectoryChanged() reconnects the event listener
4
+ // so events from the worktree Instance reach the runtime (PR #75 fix).
5
+ //
6
+ // Uses opencode-deterministic-provider (no real LLM calls).
7
+ // Poll timeouts: 4s max, 100ms interval (except worktree creation which
8
+ // involves real git operations — 10s timeout there).
9
+
10
+ import fs from 'node:fs'
11
+
12
+ import path from 'node:path'
13
+ import url from 'node:url'
14
+ import { describe, beforeAll, afterAll, test, expect } from 'vitest'
15
+ import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js'
16
+ import { DigitalDiscord } from 'discord-digital-twin/src'
17
+ import {
18
+ buildDeterministicOpencodeConfig,
19
+ type DeterministicMatcher,
20
+ } from 'opencode-deterministic-provider'
21
+ import { setDataDir } from './config.js'
22
+ import { store } from './store.js'
23
+ import { startDiscordBot } from './discord-bot.js'
24
+ import { getRuntime } from './session-handler/thread-session-runtime.js'
25
+ import {
26
+ setBotToken,
27
+ initDatabase,
28
+ closeDatabase,
29
+ setChannelDirectory,
30
+ setChannelVerbosity,
31
+ type VerbosityLevel,
32
+ } from './database.js'
33
+ import { startHranaServer, stopHranaServer } from './hrana-server.js'
34
+ import {
35
+ initializeOpencodeForDirectory,
36
+ stopOpencodeServer,
37
+ } from './opencode.js'
38
+ import {
39
+ chooseLockPort,
40
+ cleanupTestSessions,
41
+ waitForBotMessageContaining,
42
+ waitForBotReplyAfterUserMessage,
43
+ } from './test-utils.js'
44
+ import { execAsync } from './worktrees.js'
45
+
46
+ const TEST_USER_ID = '200000000000000901'
47
+ const TEXT_CHANNEL_ID = '200000000000000902'
48
+ // Unique worktree name per run to avoid collisions with leftover worktrees
49
+ const WORKTREE_SUFFIX = Date.now().toString(36).slice(-6)
50
+ const WORKTREE_NAME = `wt-e2e-${WORKTREE_SUFFIX}`
51
+
52
+ function createRunDirectories() {
53
+ const root = path.resolve(process.cwd(), 'tmp', 'worktree-lifecycle-e2e')
54
+ fs.mkdirSync(root, { recursive: true })
55
+ const dataDir = fs.mkdtempSync(path.join(root, 'data-'))
56
+ const projectDirectory = path.join(root, 'project')
57
+ fs.mkdirSync(projectDirectory, { recursive: true })
58
+ return { root, dataDir, projectDirectory }
59
+ }
60
+
61
+ function createDiscordJsClient({ restUrl }: { restUrl: string }) {
62
+ return new Client({
63
+ intents: [
64
+ GatewayIntentBits.Guilds,
65
+ GatewayIntentBits.GuildMessages,
66
+ GatewayIntentBits.MessageContent,
67
+ GatewayIntentBits.GuildVoiceStates,
68
+ ],
69
+ partials: [
70
+ Partials.Channel,
71
+ Partials.Message,
72
+ Partials.User,
73
+ Partials.ThreadMember,
74
+ ],
75
+ rest: {
76
+ api: restUrl,
77
+ version: '10',
78
+ },
79
+ })
80
+ }
81
+
82
+ /** Initialize a git repo with an initial commit so worktrees can be created. */
83
+ async function initGitRepo(directory: string): Promise<void> {
84
+ // Check if already a git repo (directory may persist across runs)
85
+ const isRepo = fs.existsSync(path.join(directory, '.git'))
86
+ if (isRepo) {
87
+ // Commit any new/changed files (opencode.json may have been rewritten)
88
+ await execAsync('git add -A && git diff --cached --quiet || git commit -m "update"', {
89
+ cwd: directory,
90
+ }).catch(() => { return })
91
+ return
92
+ }
93
+ await execAsync('git init -b main', { cwd: directory })
94
+ await execAsync('git config user.email "test@test.com"', { cwd: directory })
95
+ await execAsync('git config user.name "Test"', { cwd: directory })
96
+ await execAsync('git add -A && git commit -m "initial"', { cwd: directory })
97
+ }
98
+
99
+ function createDeterministicMatchers(): DeterministicMatcher[] {
100
+ const userReplyMatcher: DeterministicMatcher = {
101
+ id: 'user-reply',
102
+ priority: 10,
103
+ when: {
104
+ lastMessageRole: 'user',
105
+ rawPromptIncludes: 'Reply with exactly:',
106
+ },
107
+ then: {
108
+ parts: [
109
+ { type: 'stream-start', warnings: [] },
110
+ { type: 'text-start', id: 'default-reply' },
111
+ { type: 'text-delta', id: 'default-reply', delta: 'ok' },
112
+ { type: 'text-end', id: 'default-reply' },
113
+ {
114
+ type: 'finish',
115
+ finishReason: 'stop',
116
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
117
+ },
118
+ ],
119
+ partDelaysMs: [0, 100, 0, 0, 0],
120
+ },
121
+ }
122
+
123
+ return [userReplyMatcher]
124
+ }
125
+
126
+ describe('worktree lifecycle', () => {
127
+ let directories: ReturnType<typeof createRunDirectories>
128
+ let discord: DigitalDiscord
129
+ let botClient: Client
130
+ let previousDefaultVerbosity: VerbosityLevel | null = null
131
+ let testStartTime = Date.now()
132
+
133
+ beforeAll(async () => {
134
+ testStartTime = Date.now()
135
+ directories = createRunDirectories()
136
+ const lockPort = chooseLockPort({ key: TEXT_CHANNEL_ID })
137
+
138
+ process.env['KIMAKI_LOCK_PORT'] = String(lockPort)
139
+ setDataDir(directories.dataDir)
140
+ previousDefaultVerbosity = store.getState().defaultVerbosity
141
+ store.setState({ defaultVerbosity: 'tools_and_text' })
142
+
143
+ const digitalDiscordDbPath = path.join(
144
+ directories.dataDir,
145
+ 'digital-discord.db',
146
+ )
147
+
148
+ discord = new DigitalDiscord({
149
+ guild: {
150
+ name: 'Worktree E2E Guild',
151
+ ownerId: TEST_USER_ID,
152
+ },
153
+ channels: [
154
+ {
155
+ id: TEXT_CHANNEL_ID,
156
+ name: 'worktree-e2e',
157
+ type: ChannelType.GuildText,
158
+ },
159
+ ],
160
+ users: [
161
+ {
162
+ id: TEST_USER_ID,
163
+ username: 'worktree-tester',
164
+ },
165
+ ],
166
+ dbUrl: `file:${digitalDiscordDbPath}`,
167
+ })
168
+
169
+ await discord.start()
170
+
171
+ const providerNpm = url
172
+ .pathToFileURL(
173
+ path.resolve(
174
+ process.cwd(),
175
+ '..',
176
+ 'opencode-deterministic-provider',
177
+ 'src',
178
+ 'index.ts',
179
+ ),
180
+ )
181
+ .toString()
182
+
183
+ const opencodeConfig = buildDeterministicOpencodeConfig({
184
+ providerName: 'deterministic-provider',
185
+ providerNpm,
186
+ model: 'deterministic-v2',
187
+ smallModel: 'deterministic-v2',
188
+ settings: {
189
+ strict: false,
190
+ matchers: createDeterministicMatchers(),
191
+ },
192
+ })
193
+ fs.writeFileSync(
194
+ path.join(directories.projectDirectory, 'opencode.json'),
195
+ JSON.stringify(opencodeConfig, null, 2),
196
+ )
197
+
198
+ // Initialize git repo after writing opencode.json so the initial commit
199
+ // includes it. Worktrees require at least one commit.
200
+ await initGitRepo(directories.projectDirectory)
201
+
202
+ const dbPath = path.join(directories.dataDir, 'discord-sessions.db')
203
+ const hranaResult = await startHranaServer({ dbPath })
204
+ if (hranaResult instanceof Error) {
205
+ throw hranaResult
206
+ }
207
+ process.env['KIMAKI_DB_URL'] = hranaResult
208
+ await initDatabase()
209
+ await setBotToken(discord.botUserId, discord.botToken)
210
+
211
+ await setChannelDirectory({
212
+ channelId: TEXT_CHANNEL_ID,
213
+ directory: directories.projectDirectory,
214
+ channelType: 'text',
215
+ })
216
+ await setChannelVerbosity(TEXT_CHANNEL_ID, 'tools_and_text')
217
+
218
+ botClient = createDiscordJsClient({ restUrl: discord.restUrl })
219
+ await startDiscordBot({
220
+ token: discord.botToken,
221
+ appId: discord.botUserId,
222
+ discordClient: botClient,
223
+ })
224
+
225
+ // Pre-warm the opencode server
226
+ const warmup = await initializeOpencodeForDirectory(
227
+ directories.projectDirectory,
228
+ )
229
+ if (warmup instanceof Error) {
230
+ throw warmup
231
+ }
232
+ }, 60_000)
233
+
234
+ afterAll(async () => {
235
+ if (directories) {
236
+ await cleanupTestSessions({
237
+ projectDirectory: directories.projectDirectory,
238
+ testStartTime,
239
+ })
240
+ }
241
+ if (botClient) {
242
+ botClient.destroy()
243
+ }
244
+ await stopOpencodeServer()
245
+ await Promise.all([
246
+ closeDatabase().catch(() => { return }),
247
+ stopHranaServer().catch(() => { return }),
248
+ discord?.stop().catch(() => { return }),
249
+ ])
250
+ delete process.env['KIMAKI_LOCK_PORT']
251
+ delete process.env['KIMAKI_DB_URL']
252
+ if (previousDefaultVerbosity) {
253
+ store.setState({ defaultVerbosity: previousDefaultVerbosity })
254
+ }
255
+ // Clean up the git worktree created during the test
256
+ if (directories) {
257
+ const worktreeBranch = `opencode/kimaki-${WORKTREE_NAME}`
258
+ await execAsync(
259
+ `git worktree list --porcelain`,
260
+ { cwd: directories.projectDirectory },
261
+ ).then(({ stdout }) => {
262
+ // Find and remove any worktree for our test branch
263
+ const lines = stdout.split('\n')
264
+ let currentPath = ''
265
+ for (const line of lines) {
266
+ if (line.startsWith('worktree ')) {
267
+ currentPath = line.slice('worktree '.length)
268
+ }
269
+ if (line.startsWith('branch ') && line.includes(worktreeBranch) && currentPath) {
270
+ return execAsync(
271
+ `git worktree remove --force ${JSON.stringify(currentPath)}`,
272
+ { cwd: directories.projectDirectory },
273
+ )
274
+ }
275
+ }
276
+ }).catch(() => { return })
277
+ await execAsync(
278
+ `git branch -D ${JSON.stringify(`opencode/kimaki-${WORKTREE_NAME}`)}`,
279
+ { cwd: directories.projectDirectory },
280
+ ).catch(() => { return })
281
+ fs.rmSync(directories.dataDir, { recursive: true, force: true })
282
+ }
283
+ }, 10_000)
284
+
285
+ test(
286
+ 'session responds after /new-worktree switches sdkDirectory in existing thread',
287
+ async () => {
288
+ // 1. Send a message to create a thread and establish a session
289
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
290
+ content: 'Reply with exactly: before-worktree',
291
+ })
292
+
293
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
294
+ timeout: 4_000,
295
+ predicate: (t) => {
296
+ return t.name === 'Reply with exactly: before-worktree'
297
+ },
298
+ })
299
+
300
+ const th = discord.thread(thread.id)
301
+
302
+ // Wait for first run to fully complete (footer appears)
303
+ await waitForBotMessageContaining({
304
+ discord,
305
+ threadId: thread.id,
306
+ userId: TEST_USER_ID,
307
+ text: '*project',
308
+ timeout: 4_000,
309
+ })
310
+
311
+ // Capture runtime — should survive the directory switch
312
+ const runtimeBefore = getRuntime(thread.id)
313
+ expect(runtimeBefore).toBeDefined()
314
+ expect(runtimeBefore!.sdkDirectory).toBe(directories.projectDirectory)
315
+
316
+ // 2. Run /new-worktree inside the thread (in-thread flow).
317
+ // This creates a pending worktree, then background creates the git worktree,
318
+ // then marks it ready. Next message will pick up the worktree directory.
319
+ const { id: interactionId } = await th
320
+ .user(TEST_USER_ID)
321
+ .runSlashCommand({
322
+ name: 'new-worktree',
323
+ options: [{ name: 'name', type: 3, value: WORKTREE_NAME }],
324
+ })
325
+
326
+ // Wait for the slash command ack
327
+ await discord
328
+ .channel(thread.id)
329
+ .waitForInteractionAck({ interactionId, timeout: 4_000 })
330
+
331
+ // 3. Wait for worktree to become ready — the background creation
332
+ // edits the starter message to include the branch name.
333
+ // Git worktree creation involves real git operations, so allow more time.
334
+ await waitForBotMessageContaining({
335
+ discord,
336
+ threadId: thread.id,
337
+ userId: TEST_USER_ID,
338
+ text: 'Branch:',
339
+ timeout: 10_000,
340
+ })
341
+
342
+ // 4. Send a message after the worktree is ready.
343
+ // Without handleDirectoryChanged (PR #75), the event listener is still
344
+ // subscribed to the old project directory's Instance, so this message
345
+ // gets processed but the response events never reach the runtime.
346
+ await th.user(TEST_USER_ID).sendMessage({
347
+ content: 'Reply with exactly: after-worktree',
348
+ })
349
+
350
+ // 5. Verify the bot actually responds — this is the core assertion.
351
+ // If the listener wasn't reconnected, this will time out.
352
+ await waitForBotReplyAfterUserMessage({
353
+ discord,
354
+ threadId: thread.id,
355
+ userId: TEST_USER_ID,
356
+ userMessageIncludes: 'after-worktree',
357
+ timeout: 4_000,
358
+ })
359
+
360
+ // Wait for the footer to confirm full completion
361
+ await waitForBotMessageContaining({
362
+ discord,
363
+ threadId: thread.id,
364
+ userId: TEST_USER_ID,
365
+ text: 'deterministic-v2',
366
+ afterUserMessageIncludes: 'after-worktree',
367
+ timeout: 4_000,
368
+ })
369
+
370
+ // Runtime instance should be the same (not recreated)
371
+ const runtimeAfter = getRuntime(thread.id)
372
+ expect(runtimeAfter).toBe(runtimeBefore)
373
+
374
+ // sdkDirectory should now point to the worktree path
375
+ expect(runtimeAfter!.sdkDirectory).not.toBe(directories.projectDirectory)
376
+ expect(runtimeAfter!.sdkDirectory).toContain(`kimaki-${WORKTREE_NAME}`)
377
+
378
+ // Snapshot uses dynamic worktree name so we verify structure, not exact text
379
+ const text = await th.text()
380
+ expect(text).toContain('Reply with exactly: before-worktree')
381
+ expect(text).toContain('⬥ ok')
382
+ expect(text).toContain('Worktree:')
383
+ expect(text).toContain('Branch:')
384
+ expect(text).toContain('Reply with exactly: after-worktree')
385
+ // The second "⬥ ok" proves the bot responded after the worktree switch
386
+ const okCount = (text.match(/⬥ ok/g) || []).length
387
+ expect(okCount).toBe(2)
388
+ },
389
+ 30_000,
390
+ )
391
+ })
@@ -0,0 +1,4 @@
1
+ // Backward-compatible re-export for worktree helpers.
2
+ // New code should import from worktrees.ts.
3
+
4
+ export * from './worktrees.js'