@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,929 @@
1
+ // Worktree service and git helpers.
2
+ // Provides reusable, Discord-agnostic worktree creation/merge logic,
3
+ // submodule initialization, and git diff transfer utilities.
4
+ import crypto from 'node:crypto';
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import * as errore from 'errore';
9
+ import { execAsync } from './exec-async.js';
10
+ import { createLogger, LogPrefix } from './logger.js';
11
+ export { execAsync } from './exec-async.js';
12
+ const SUBMODULE_INIT_TIMEOUT_MS = 20 * 60_000;
13
+ const INSTALL_TIMEOUT_MS = 60_000;
14
+ const logger = createLogger(LogPrefix.WORKTREE);
15
+ const LOCKFILE_TO_INSTALL_COMMAND = [
16
+ ['pnpm-lock.yaml', 'pnpm install'],
17
+ ['bun.lock', 'bun install'],
18
+ ['bun.lockb', 'bun install'],
19
+ ['yarn.lock', 'yarn install'],
20
+ ['package-lock.json', 'npm install'],
21
+ ];
22
+ function detectInstallCommand(directory) {
23
+ for (const [lockfile, command] of LOCKFILE_TO_INSTALL_COMMAND) {
24
+ if (fs.existsSync(path.join(directory, lockfile))) {
25
+ return command;
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ /**
31
+ * Run the detected package manager install in a worktree directory.
32
+ * Non-fatal: returns Error on failure/timeout so callers can log and continue.
33
+ * The 60s timeout kills the process if install hangs.
34
+ */
35
+ export async function runDependencyInstall({ directory, }) {
36
+ const installCommand = detectInstallCommand(directory);
37
+ if (!installCommand) {
38
+ return;
39
+ }
40
+ logger.log(`Running "${installCommand}" in ${directory} (timeout=${INSTALL_TIMEOUT_MS}ms)`);
41
+ try {
42
+ await execAsync(installCommand, {
43
+ cwd: directory,
44
+ timeout: INSTALL_TIMEOUT_MS,
45
+ });
46
+ logger.log(`Dependencies installed in ${directory}`);
47
+ }
48
+ catch (e) {
49
+ return new Error(`Install failed: ${formatCommandError(e)}`, { cause: e });
50
+ }
51
+ }
52
+ function formatCommandError(error) {
53
+ if (!(error instanceof Error)) {
54
+ return String(error);
55
+ }
56
+ const commandError = error;
57
+ const details = [commandError.message];
58
+ if (commandError.cmd) {
59
+ details.push(`cmd=${commandError.cmd}`);
60
+ }
61
+ if (commandError.signal) {
62
+ details.push(`signal=${commandError.signal}`);
63
+ }
64
+ if (commandError.killed) {
65
+ details.push('process=killed');
66
+ }
67
+ if (commandError.stderr?.trim()) {
68
+ details.push(`stderr=${commandError.stderr.trim()}`);
69
+ }
70
+ if (commandError.stdout?.trim()) {
71
+ details.push(`stdout=${commandError.stdout.trim()}`);
72
+ }
73
+ return details.join(' | ');
74
+ }
75
+ export function parseGitmodulesFileContent(gitmodulesContent) {
76
+ const lines = gitmodulesContent.split('\n');
77
+ const configs = [];
78
+ let currentName = null;
79
+ let currentPath = null;
80
+ let currentUrl = null;
81
+ const flushCurrent = () => {
82
+ if (!currentName) {
83
+ return;
84
+ }
85
+ if (!currentPath) {
86
+ return new Error(`Submodule ${currentName} is missing path in .gitmodules`);
87
+ }
88
+ configs.push({
89
+ name: currentName,
90
+ path: currentPath,
91
+ url: currentUrl,
92
+ });
93
+ };
94
+ for (const rawLine of lines) {
95
+ const line = rawLine.trim();
96
+ if (!line || line.startsWith('#') || line.startsWith(';')) {
97
+ continue;
98
+ }
99
+ const sectionMatch = line.match(/^\[submodule\s+"([^"]+)"\]$/);
100
+ if (sectionMatch?.[1]) {
101
+ const flushError = flushCurrent();
102
+ if (flushError instanceof Error) {
103
+ return flushError;
104
+ }
105
+ currentName = sectionMatch[1];
106
+ currentPath = null;
107
+ currentUrl = null;
108
+ continue;
109
+ }
110
+ if (!currentName) {
111
+ continue;
112
+ }
113
+ const keyValueMatch = line.match(/^([^=\s]+)\s*=\s*(.*)$/);
114
+ const key = keyValueMatch?.[1];
115
+ const value = keyValueMatch?.[2];
116
+ if (!key || value === undefined) {
117
+ continue;
118
+ }
119
+ if (key === 'path') {
120
+ currentPath = value;
121
+ continue;
122
+ }
123
+ if (key === 'url') {
124
+ currentUrl = value;
125
+ }
126
+ }
127
+ const flushError = flushCurrent();
128
+ if (flushError instanceof Error) {
129
+ return flushError;
130
+ }
131
+ return configs;
132
+ }
133
+ async function readSubmoduleConfigs(directory) {
134
+ const gitmodulesPath = path.join(directory, '.gitmodules');
135
+ const gitmodulesExists = await fs.promises
136
+ .access(gitmodulesPath)
137
+ .then(() => {
138
+ return true;
139
+ })
140
+ .catch(() => {
141
+ return false;
142
+ });
143
+ if (!gitmodulesExists) {
144
+ return [];
145
+ }
146
+ const gitmodulesContent = await errore.tryAsync({
147
+ try: () => fs.promises.readFile(gitmodulesPath, 'utf-8'),
148
+ catch: (e) => new Error(`Failed to read ${gitmodulesPath}`, {
149
+ cause: e,
150
+ }),
151
+ });
152
+ if (gitmodulesContent instanceof Error) {
153
+ return gitmodulesContent;
154
+ }
155
+ const parsed = parseGitmodulesFileContent(gitmodulesContent);
156
+ if (parsed instanceof Error) {
157
+ return new Error(`Failed to parse ${gitmodulesPath}: ${parsed.message}`, {
158
+ cause: parsed,
159
+ });
160
+ }
161
+ return parsed;
162
+ }
163
+ export function buildSubmoduleReferencePlan({ sourceDirectory, submodulePaths, existingSourceSubmoduleDirectories, }) {
164
+ return submodulePaths.map((submodulePath) => {
165
+ const sourceSubmoduleDirectory = path.resolve(sourceDirectory, submodulePath);
166
+ if (existingSourceSubmoduleDirectories.has(sourceSubmoduleDirectory)) {
167
+ return {
168
+ path: submodulePath,
169
+ referenceDirectory: sourceSubmoduleDirectory,
170
+ };
171
+ }
172
+ return {
173
+ path: submodulePath,
174
+ referenceDirectory: null,
175
+ };
176
+ });
177
+ }
178
+ function buildGitCommand(args) {
179
+ const quotedArgs = args.map((arg) => {
180
+ return JSON.stringify(arg);
181
+ });
182
+ return `git ${quotedArgs.join(' ')}`;
183
+ }
184
+ export function buildSubmoduleUpdateCommandArgs({ path: submodulePath, referenceDirectory, }) {
185
+ if (referenceDirectory) {
186
+ return [
187
+ '-c',
188
+ 'protocol.file.allow=always',
189
+ 'submodule',
190
+ 'update',
191
+ '--init',
192
+ '--recursive',
193
+ '--reference',
194
+ referenceDirectory,
195
+ '--',
196
+ submodulePath,
197
+ ];
198
+ }
199
+ return [
200
+ '-c',
201
+ 'protocol.file.allow=always',
202
+ 'submodule',
203
+ 'update',
204
+ '--init',
205
+ '--recursive',
206
+ '--',
207
+ submodulePath,
208
+ ];
209
+ }
210
+ async function hasSubmoduleGitMetadata(directory) {
211
+ const gitPath = path.join(directory, '.git');
212
+ return fs.promises
213
+ .access(gitPath)
214
+ .then(() => {
215
+ return true;
216
+ })
217
+ .catch(() => {
218
+ return false;
219
+ });
220
+ }
221
+ async function initializeSubmodulesWithLocalReferences({ sourceDirectory, worktreeDirectory, }) {
222
+ const submoduleConfigs = await readSubmoduleConfigs(worktreeDirectory);
223
+ if (submoduleConfigs instanceof Error) {
224
+ return submoduleConfigs;
225
+ }
226
+ if (submoduleConfigs.length === 0) {
227
+ return;
228
+ }
229
+ const sourceDirectories = submoduleConfigs.map(({ path: submodulePath }) => {
230
+ return path.resolve(sourceDirectory, submodulePath);
231
+ });
232
+ const sourceDirectoryChecks = await Promise.all(sourceDirectories.map(async (sourceSubmoduleDirectory) => {
233
+ const exists = await hasSubmoduleGitMetadata(sourceSubmoduleDirectory);
234
+ return { sourceSubmoduleDirectory, exists };
235
+ }));
236
+ const existingSourceSubmoduleDirectories = new Set(sourceDirectoryChecks
237
+ .filter(({ exists }) => {
238
+ return exists;
239
+ })
240
+ .map(({ sourceSubmoduleDirectory }) => {
241
+ return sourceSubmoduleDirectory;
242
+ }));
243
+ const submodulePlan = buildSubmoduleReferencePlan({
244
+ sourceDirectory,
245
+ submodulePaths: submoduleConfigs.map(({ path: submodulePath }) => {
246
+ return submodulePath;
247
+ }),
248
+ existingSourceSubmoduleDirectories,
249
+ });
250
+ for (const planItem of submodulePlan) {
251
+ const commandArgs = buildSubmoduleUpdateCommandArgs(planItem);
252
+ const command = buildGitCommand(commandArgs);
253
+ const result = await errore.tryAsync({
254
+ try: () => execAsync(command, {
255
+ cwd: worktreeDirectory,
256
+ timeout: SUBMODULE_INIT_TIMEOUT_MS,
257
+ }),
258
+ catch: (e) => new Error(`git ${commandArgs.join(' ')} failed for ${planItem.path}: ${formatCommandError(e)}`, { cause: e }),
259
+ });
260
+ if (result instanceof Error) {
261
+ // Non-fatal: broken .gitmodules entries (e.g. path listed but not in tree)
262
+ // should not block worktree creation. Log and continue with remaining submodules.
263
+ logger.warn(`Skipping submodule ${planItem.path}: ${result.message}`);
264
+ }
265
+ }
266
+ }
267
+ /**
268
+ * Get submodule paths from .gitmodules file.
269
+ * Returns empty array if no submodules or on error.
270
+ */
271
+ async function getSubmodulePaths(directory) {
272
+ const submoduleConfigs = await readSubmoduleConfigs(directory);
273
+ if (submoduleConfigs instanceof Error) {
274
+ logger.warn(`Failed reading submodules from ${directory}: ${submoduleConfigs.message}`);
275
+ return [];
276
+ }
277
+ return submoduleConfigs.map(({ path: submodulePath }) => {
278
+ return submodulePath;
279
+ });
280
+ }
281
+ /**
282
+ * Remove broken submodule stubs created by git worktree.
283
+ * When git worktree add runs on a repo with submodules, it creates submodule
284
+ * directories with .git files pointing to ../.git/worktrees/<name>/modules/<submodule>
285
+ * but that path only has a config file, missing HEAD/objects/refs.
286
+ * This causes git commands to fail with "fatal: not a git repository".
287
+ */
288
+ async function removeBrokenSubmoduleStubs(directory) {
289
+ const submodulePaths = await getSubmodulePaths(directory);
290
+ for (const subPath of submodulePaths) {
291
+ const fullPath = path.join(directory, subPath);
292
+ const gitFile = path.join(fullPath, '.git');
293
+ try {
294
+ const stat = await fs.promises.stat(gitFile);
295
+ if (!stat.isFile()) {
296
+ continue;
297
+ }
298
+ // Read .git file to get gitdir path
299
+ const content = await fs.promises.readFile(gitFile, 'utf-8');
300
+ const match = content.match(/^gitdir:\s*(.+)$/m);
301
+ if (!match || !match[1]) {
302
+ continue;
303
+ }
304
+ const gitdir = path.resolve(fullPath, match[1].trim());
305
+ const headFile = path.join(gitdir, 'HEAD');
306
+ // If HEAD doesn't exist, this is a broken stub
307
+ const headExists = await fs.promises
308
+ .access(headFile)
309
+ .then(() => {
310
+ return true;
311
+ })
312
+ .catch(() => {
313
+ return false;
314
+ });
315
+ if (!headExists) {
316
+ logger.log(`Removing broken submodule stub: ${subPath}`);
317
+ await fs.promises.rm(fullPath, { recursive: true, force: true });
318
+ }
319
+ }
320
+ catch {
321
+ // Directory doesn't exist or other error, skip
322
+ }
323
+ }
324
+ }
325
+ function parseSubmoduleGitdir(gitFileContent) {
326
+ const match = gitFileContent.match(/^gitdir:\s*(.+)$/m);
327
+ const gitdir = match?.[1]?.trim();
328
+ if (!gitdir) {
329
+ return new Error('Missing gitdir pointer');
330
+ }
331
+ return gitdir;
332
+ }
333
+ async function validateSubmodulePointers(directory) {
334
+ const submodulePaths = await getSubmodulePaths(directory);
335
+ if (submodulePaths.length === 0) {
336
+ return;
337
+ }
338
+ const validationIssues = [];
339
+ await Promise.all(submodulePaths.map(async (submodulePath) => {
340
+ const submoduleDir = path.join(directory, submodulePath);
341
+ const submoduleGitFile = path.join(submoduleDir, '.git');
342
+ const gitFileExists = await fs.promises
343
+ .access(submoduleGitFile)
344
+ .then(() => {
345
+ return true;
346
+ })
347
+ .catch(() => {
348
+ return false;
349
+ });
350
+ if (!gitFileExists) {
351
+ validationIssues.push(`${submodulePath}: missing .git file`);
352
+ return;
353
+ }
354
+ const gitFileContentResult = await errore.tryAsync({
355
+ try: () => fs.promises.readFile(submoduleGitFile, 'utf-8'),
356
+ catch: (e) => new Error(`Failed to read .git for ${submodulePath}`, { cause: e }),
357
+ });
358
+ if (gitFileContentResult instanceof Error) {
359
+ validationIssues.push(`${submodulePath}: ${gitFileContentResult.message}`);
360
+ return;
361
+ }
362
+ const parsedGitdir = parseSubmoduleGitdir(gitFileContentResult);
363
+ if (parsedGitdir instanceof Error) {
364
+ validationIssues.push(`${submodulePath}: ${parsedGitdir.message}`);
365
+ return;
366
+ }
367
+ const resolvedGitdir = path.resolve(submoduleDir, parsedGitdir);
368
+ const headPath = path.join(resolvedGitdir, 'HEAD');
369
+ const headExists = await fs.promises
370
+ .access(headPath)
371
+ .then(() => {
372
+ return true;
373
+ })
374
+ .catch(() => {
375
+ return false;
376
+ });
377
+ if (!headExists) {
378
+ validationIssues.push(`${submodulePath}: gitdir missing HEAD (${resolvedGitdir})`);
379
+ }
380
+ }));
381
+ const submoduleStatusResult = await errore.tryAsync({
382
+ try: () => execAsync('git submodule status --recursive', {
383
+ cwd: directory,
384
+ timeout: SUBMODULE_INIT_TIMEOUT_MS,
385
+ }),
386
+ catch: (e) => new Error('git submodule status --recursive failed', { cause: e }),
387
+ });
388
+ if (submoduleStatusResult instanceof Error) {
389
+ validationIssues.push(submoduleStatusResult.message);
390
+ }
391
+ if (validationIssues.length === 0) {
392
+ return;
393
+ }
394
+ return new Error(`Submodule validation failed: ${validationIssues.join('; ')}`);
395
+ }
396
+ async function resolveDefaultWorktreeTarget(directory) {
397
+ const remoteHead = await execAsync('git symbolic-ref refs/remotes/origin/HEAD', {
398
+ cwd: directory,
399
+ }).catch(() => {
400
+ return null;
401
+ });
402
+ const remoteRef = remoteHead?.stdout.trim();
403
+ if (remoteRef?.startsWith('refs/remotes/')) {
404
+ return remoteRef.replace('refs/remotes/', '');
405
+ }
406
+ const hasMain = await execAsync('git show-ref --verify --quiet refs/heads/main', {
407
+ cwd: directory,
408
+ })
409
+ .then(() => {
410
+ return true;
411
+ })
412
+ .catch(() => {
413
+ return false;
414
+ });
415
+ if (hasMain) {
416
+ return 'main';
417
+ }
418
+ const hasMaster = await execAsync('git show-ref --verify --quiet refs/heads/master', {
419
+ cwd: directory,
420
+ })
421
+ .then(() => {
422
+ return true;
423
+ })
424
+ .catch(() => {
425
+ return false;
426
+ });
427
+ if (hasMaster) {
428
+ return 'master';
429
+ }
430
+ return 'HEAD';
431
+ }
432
+ function getManagedWorktreeDirectory({ directory, name, }) {
433
+ const projectHash = crypto.createHash('sha1').update(directory).digest('hex');
434
+ const safeName = name.replaceAll('/', '-');
435
+ return path.join(os.homedir(), '.local', 'share', 'opencode', 'worktree', projectHash, safeName);
436
+ }
437
+ /**
438
+ * Create a worktree using git and initialize git submodules.
439
+ * This wrapper ensures submodules are properly set up in new worktrees.
440
+ */
441
+ export async function createWorktreeWithSubmodules({ directory, name, baseBranch, onProgress, }) {
442
+ // 1. Create worktree via git (checked out immediately).
443
+ const worktreeDir = getManagedWorktreeDirectory({ directory, name });
444
+ const targetRef = baseBranch || (await resolveDefaultWorktreeTarget(directory));
445
+ if (fs.existsSync(worktreeDir)) {
446
+ return new Error(`Worktree directory already exists: ${worktreeDir}`);
447
+ }
448
+ await fs.promises.mkdir(path.dirname(worktreeDir), { recursive: true });
449
+ const createCommand = `git worktree add ${JSON.stringify(worktreeDir)} -B ${JSON.stringify(name)} ${JSON.stringify(targetRef)}`;
450
+ const createResult = await errore.tryAsync({
451
+ try: () => execAsync(createCommand, {
452
+ cwd: directory,
453
+ timeout: SUBMODULE_INIT_TIMEOUT_MS,
454
+ }),
455
+ catch: (e) => new Error(`git worktree add failed: ${formatCommandError(e)}`, {
456
+ cause: e,
457
+ }),
458
+ });
459
+ if (createResult instanceof Error) {
460
+ return createResult;
461
+ }
462
+ // 2. Remove broken submodule stubs before init
463
+ // git worktree creates stub directories with .git files pointing to incomplete gitdirs
464
+ await removeBrokenSubmoduleStubs(worktreeDir);
465
+ // 4. Init submodules in new worktree.
466
+ // For each submodule we use git's built-in --reference mechanism when the
467
+ // source checkout already has that submodule cloned. This preserves commit
468
+ // pinning while allowing local-only submodule commits to resolve reliably.
469
+ logger.log(`Initializing submodules in ${worktreeDir} (timeout=${SUBMODULE_INIT_TIMEOUT_MS}ms)`);
470
+ const submoduleInitResult = await initializeSubmodulesWithLocalReferences({
471
+ sourceDirectory: directory,
472
+ worktreeDirectory: worktreeDir,
473
+ });
474
+ if (submoduleInitResult instanceof Error) {
475
+ // Non-fatal: log and continue. The worktree itself is already created,
476
+ // only submodule init had issues (e.g. stale .gitmodules entries).
477
+ logger.error('Submodule initialization failed (non-fatal)', {
478
+ worktreeDir,
479
+ timeoutMs: SUBMODULE_INIT_TIMEOUT_MS,
480
+ command: 'git submodule update --init --recursive [--reference ...]',
481
+ error: submoduleInitResult.message,
482
+ });
483
+ }
484
+ else {
485
+ logger.log(`Submodules initialized in ${worktreeDir}`);
486
+ }
487
+ // 4.5 Validate submodule pointers and git metadata.
488
+ // Non-fatal: stale .gitmodules entries (path listed but removed from tree)
489
+ // should not block worktree creation.
490
+ const submoduleValidationError = await validateSubmodulePointers(worktreeDir);
491
+ if (submoduleValidationError instanceof Error) {
492
+ logger.error('Submodule validation issues (non-fatal)', {
493
+ worktreeDir,
494
+ error: submoduleValidationError.message,
495
+ });
496
+ }
497
+ // 5. Dependency install (non-fatal, 60s timeout).
498
+ // Runs the detected package manager install so workspace packages with
499
+ // `prepare` scripts get built (e.g. errore → dist/).
500
+ onProgress?.('Installing dependencies...');
501
+ const installResult = await runDependencyInstall({ directory: worktreeDir });
502
+ if (installResult instanceof Error) {
503
+ logger.error('Dependency install failed (non-fatal)', {
504
+ worktreeDir,
505
+ error: installResult.message,
506
+ });
507
+ }
508
+ return { directory: worktreeDir, branch: name };
509
+ }
510
+ // ─── Worktree merge ──────────────────────────────────────────────────────────
511
+ // Merge pipeline (preserves all worktree commits, no squash):
512
+ // 1. Reject if uncommitted changes exist
513
+ // 2. Rebase worktree commits onto target (default branch)
514
+ // 3. Fast-forward push to target via local git push
515
+ // 4. Switch to detached HEAD, delete branch
516
+ //
517
+ // Uses `git push <git-common-dir> HEAD:<target>` with
518
+ // `receive.denyCurrentBranch=updateInstead` to fast-forward the target
519
+ // WITHOUT checking it out in the main repo.
520
+ //
521
+ // Returns MergeWorktreeErrors | MergeSuccess. All errors are tagged via errore.
522
+ // - DirtyWorktreeError → git untouched
523
+ // - NothingToMergeError → git untouched
524
+ // - RebaseConflictError → git left mid-rebase for AI/user resolution
525
+ // - RebaseError → rebase not in progress; temp branch cleaned
526
+ // - NotFastForwardError → source intact; no push
527
+ // - ConflictingFilesError → no push; lists overlapping files
528
+ // - PushError → source rebased but target unchanged
529
+ // - GitCommandError → catch-all for unexpected git failures
530
+ import { DirtyWorktreeError, NothingToMergeError, RebaseConflictError, RebaseError, NotFastForwardError, ConflictingFilesError, PushError, GitCommandError, } from './errors.js';
531
+ export async function git(dir, args, opts) {
532
+ const result = await errore.tryAsync({
533
+ try: () => execAsync(`git -C "${dir}" ${args}`, opts ? { timeout: opts.timeout } : undefined),
534
+ catch: (e) => new GitCommandError({ command: args, cause: e }),
535
+ });
536
+ if (result instanceof Error) {
537
+ return result;
538
+ }
539
+ return result.stdout.trim();
540
+ }
541
+ export async function getDefaultBranch(repoDir, opts) {
542
+ const ref = await git(repoDir, 'symbolic-ref refs/remotes/origin/HEAD', opts);
543
+ if (ref instanceof Error) {
544
+ return 'main';
545
+ }
546
+ return ref.replace(/^refs\/remotes\/origin\//, '') || 'main';
547
+ }
548
+ export async function deleteWorktree({ projectDirectory, worktreeDirectory, worktreeName, }) {
549
+ let removeResult = await git(projectDirectory, `worktree remove ${JSON.stringify(worktreeDirectory)}`, {
550
+ timeout: SUBMODULE_INIT_TIMEOUT_MS,
551
+ });
552
+ // git refuses to remove worktrees with submodule entries:
553
+ // "fatal: working trees containing submodules cannot be moved or removed"
554
+ // Retry with --force which bypasses this guard. This is safe because
555
+ // canDeleteWorktree already verified the worktree is clean and merged.
556
+ if (removeResult instanceof Error) {
557
+ const stderr = removeResult.cause?.stderr ?? '';
558
+ if (stderr.includes('containing submodules')) {
559
+ removeResult = await git(projectDirectory, `worktree remove --force ${JSON.stringify(worktreeDirectory)}`, { timeout: SUBMODULE_INIT_TIMEOUT_MS });
560
+ }
561
+ }
562
+ if (removeResult instanceof Error) {
563
+ return new Error(`Failed to remove worktree ${worktreeName}`, {
564
+ cause: removeResult,
565
+ });
566
+ }
567
+ const deleteBranchResult = await git(projectDirectory, `branch -d ${JSON.stringify(worktreeName)}`);
568
+ if (deleteBranchResult instanceof Error) {
569
+ return new Error(`Failed to delete branch ${worktreeName}`, {
570
+ cause: deleteBranchResult,
571
+ });
572
+ }
573
+ const pruneResult = await git(projectDirectory, 'worktree prune');
574
+ if (pruneResult instanceof Error) {
575
+ logger.warn(`Failed to prune worktrees after deleting ${worktreeName}`);
576
+ }
577
+ }
578
+ export async function isDirty(dir, opts) {
579
+ const status = await git(dir, 'status --porcelain', opts);
580
+ if (status instanceof Error) {
581
+ return false;
582
+ }
583
+ return status.length > 0;
584
+ }
585
+ async function getGitCommonDir(dir) {
586
+ const commonDir = await git(dir, 'rev-parse --git-common-dir');
587
+ if (commonDir instanceof Error) {
588
+ return commonDir;
589
+ }
590
+ if (path.isAbsolute(commonDir)) {
591
+ return commonDir;
592
+ }
593
+ return path.resolve(dir, commonDir);
594
+ }
595
+ async function isAncestor(dir, ref1, ref2) {
596
+ const result = await git(dir, `merge-base --is-ancestor "${ref1}" "${ref2}"`);
597
+ return !(result instanceof Error);
598
+ }
599
+ async function isRebasedOnto(dir, target) {
600
+ const mergeBase = await git(dir, `merge-base HEAD "${target}"`);
601
+ if (mergeBase instanceof Error) {
602
+ return false;
603
+ }
604
+ const targetSha = await git(dir, `rev-parse "${target}"`);
605
+ if (targetSha instanceof Error) {
606
+ return false;
607
+ }
608
+ return mergeBase === targetSha;
609
+ }
610
+ async function getChangedFiles(dir, ref1, ref2) {
611
+ const result = await git(dir, `diff --name-only "${ref1}" "${ref2}"`);
612
+ if (result instanceof Error) {
613
+ return [];
614
+ }
615
+ return result.split('\n').filter(Boolean);
616
+ }
617
+ /**
618
+ * Get dirty files using porcelain -z format.
619
+ * Handles rename/copy entries which emit two NUL-separated paths.
620
+ */
621
+ async function getDirtyFiles(dir) {
622
+ const result = await git(dir, 'status --porcelain -z');
623
+ if (result instanceof Error) {
624
+ return [];
625
+ }
626
+ const files = [];
627
+ const parts = result.split('\0');
628
+ let i = 0;
629
+ while (i < parts.length) {
630
+ const entry = parts[i];
631
+ if (!entry || entry.length < 3) {
632
+ i++;
633
+ continue;
634
+ }
635
+ const status = entry.slice(0, 2);
636
+ const filePath = entry.slice(3);
637
+ if (filePath) {
638
+ files.push(filePath);
639
+ }
640
+ if (status[0] === 'R' ||
641
+ status[0] === 'C' ||
642
+ status[1] === 'R' ||
643
+ status[1] === 'C') {
644
+ i++;
645
+ const oldPath = parts[i];
646
+ if (oldPath) {
647
+ files.push(oldPath);
648
+ }
649
+ }
650
+ i++;
651
+ }
652
+ return files;
653
+ }
654
+ /**
655
+ * Check if target worktree has dirty files overlapping with the push range.
656
+ * updateInstead only modifies the working tree when pushing to the currently
657
+ * checked-out branch. If the main repo is on a different branch, the push
658
+ * won't touch the working tree at all, so there's nothing to conflict with.
659
+ */
660
+ async function checkTargetWorktreeConflicts({ targetDir, sourceDir, targetBranch, }) {
661
+ // Only check for conflicts if the main repo has the target branch checked out.
662
+ // updateInstead only updates the working tree for the currently checked-out
663
+ // branch — if the main repo is on a different branch, the push to targetBranch
664
+ // won't touch the working tree at all.
665
+ const currentBranch = await git(targetDir, 'symbolic-ref --short HEAD');
666
+ if (currentBranch instanceof Error || currentBranch !== targetBranch) {
667
+ return null;
668
+ }
669
+ if (!(await isDirty(targetDir))) {
670
+ return null;
671
+ }
672
+ const pushFiles = await getChangedFiles(sourceDir, targetBranch, 'HEAD');
673
+ const dirtyFiles = await getDirtyFiles(targetDir);
674
+ const overlapping = pushFiles.filter((f) => {
675
+ return dirtyFiles.includes(f);
676
+ });
677
+ return overlapping.length > 0 ? overlapping : null;
678
+ }
679
+ /**
680
+ * Check if git is mid-rebase by looking for rebase-merge or rebase-apply dirs.
681
+ */
682
+ async function isRebaseInProgress(dir) {
683
+ for (const rebaseDir of ['rebase-merge', 'rebase-apply']) {
684
+ const gitPath = await git(dir, `rev-parse --git-path ${rebaseDir}`);
685
+ if (gitPath instanceof Error) {
686
+ continue;
687
+ }
688
+ const resolvedPath = path.isAbsolute(gitPath)
689
+ ? gitPath
690
+ : path.resolve(dir, gitPath);
691
+ const exists = await fs.promises
692
+ .access(resolvedPath)
693
+ .then(() => {
694
+ return true;
695
+ })
696
+ .catch(() => {
697
+ return false;
698
+ });
699
+ if (exists) {
700
+ return true;
701
+ }
702
+ }
703
+ return false;
704
+ }
705
+ /**
706
+ * Merge a worktree branch into the default branch by rebasing all commits
707
+ * onto target, then fast-forward pushing. Preserves every worktree commit.
708
+ * Returns MergeWorktreeErrors | MergeSuccess.
709
+ */
710
+ export async function mergeWorktree({ worktreeDir, mainRepoDir, worktreeName, targetBranch, onProgress, }) {
711
+ const log = (msg) => {
712
+ logger.log(msg);
713
+ onProgress?.(msg);
714
+ };
715
+ // Resolve current branch. If detached, create a temp branch.
716
+ let branchName;
717
+ let tempBranch = null;
718
+ const branchResult = await git(worktreeDir, 'symbolic-ref --short HEAD');
719
+ if (branchResult instanceof Error) {
720
+ tempBranch = `kimaki-merge-${Date.now()}`;
721
+ const createResult = await git(worktreeDir, `checkout -b "${tempBranch}"`);
722
+ if (createResult instanceof Error) {
723
+ return createResult;
724
+ }
725
+ branchName = tempBranch;
726
+ }
727
+ else {
728
+ branchName = branchResult || worktreeName;
729
+ }
730
+ const defaultBranch = targetBranch || (await getDefaultBranch(mainRepoDir));
731
+ log(`Merging ${branchName} into ${defaultBranch}`);
732
+ // Best-effort cleanup of temp branch on error paths
733
+ const cleanupTempBranch = async () => {
734
+ if (!tempBranch) {
735
+ return;
736
+ }
737
+ const detachResult = await git(worktreeDir, 'checkout --detach');
738
+ if (detachResult instanceof Error) {
739
+ logger.warn(`[MERGE CLEANUP] Failed to detach HEAD before deleting temp branch: ${detachResult.message}`);
740
+ }
741
+ const deleteTempBranchResult = await git(worktreeDir, `branch -D "${tempBranch}"`);
742
+ if (deleteTempBranchResult instanceof Error) {
743
+ logger.warn(`[MERGE CLEANUP] Failed to delete temp branch ${tempBranch}: ${deleteTempBranchResult.message}`);
744
+ }
745
+ };
746
+ // ── Step 1: If a rebase is already paused mid-flight, surface it ──
747
+ // This happens when the user reruns /merge-worktree while the model is
748
+ // still resolving conflicts. With multi-commit rebases, each conflict
749
+ // leaves staged conflict markers (isDirty would say yes) AND merge-base
750
+ // may already equal target (isRebasedOnto would say yes), so neither
751
+ // of those checks is safe to run first. We must detect the in-progress
752
+ // rebase explicitly and route back to the AI-resolve flow.
753
+ if (await isRebaseInProgress(worktreeDir)) {
754
+ return new RebaseConflictError({ target: defaultBranch });
755
+ }
756
+ // ── Step 2: Reject uncommitted changes ──
757
+ if (await isDirty(worktreeDir)) {
758
+ await cleanupTempBranch();
759
+ return new DirtyWorktreeError();
760
+ }
761
+ // ── Step 3: Rebase worktree commits onto target ──
762
+ // If already rebased onto target AND no rebase is in progress, skip
763
+ // rebase entirely. The in-progress check above guarantees the second
764
+ // half; we keep it implicit here.
765
+ const alreadyRebased = await isRebasedOnto(worktreeDir, defaultBranch);
766
+ const mergeBaseResult = await git(worktreeDir, `merge-base HEAD "${defaultBranch}"`);
767
+ const mergeBase = mergeBaseResult instanceof Error ? defaultBranch : mergeBaseResult;
768
+ const commitCountResult = await git(worktreeDir, `rev-list --count "${mergeBase}..HEAD"`);
769
+ if (commitCountResult instanceof Error) {
770
+ await cleanupTempBranch();
771
+ return commitCountResult;
772
+ }
773
+ const commitCount = parseInt(commitCountResult, 10);
774
+ if (commitCount === 0) {
775
+ await cleanupTempBranch();
776
+ return new NothingToMergeError({ target: defaultBranch });
777
+ }
778
+ if (!alreadyRebased) {
779
+ // Rebase all worktree commits onto target, preserving each commit.
780
+ log(commitCount > 1
781
+ ? `Rebasing ${commitCount} commits onto ${defaultBranch}...`
782
+ : `Rebasing onto ${defaultBranch}...`);
783
+ const rebaseResult = await git(worktreeDir, `rebase "${defaultBranch}"`, {
784
+ timeout: 60_000,
785
+ });
786
+ if (rebaseResult instanceof Error) {
787
+ if (await isRebaseInProgress(worktreeDir)) {
788
+ return new RebaseConflictError({
789
+ target: defaultBranch,
790
+ cause: rebaseResult,
791
+ });
792
+ }
793
+ await cleanupTempBranch();
794
+ return new RebaseError({ target: defaultBranch, cause: rebaseResult });
795
+ }
796
+ }
797
+ else {
798
+ log('Already rebased onto target');
799
+ }
800
+ // ── Step 4: Fast-forward push via local git push ──
801
+ if (!(await isAncestor(worktreeDir, defaultBranch, 'HEAD'))) {
802
+ await cleanupTempBranch();
803
+ return new NotFastForwardError({ target: defaultBranch });
804
+ }
805
+ const overlappingFiles = await checkTargetWorktreeConflicts({
806
+ targetDir: mainRepoDir,
807
+ sourceDir: worktreeDir,
808
+ targetBranch: defaultBranch,
809
+ });
810
+ if (overlappingFiles) {
811
+ await cleanupTempBranch();
812
+ return new ConflictingFilesError({ target: defaultBranch });
813
+ }
814
+ const gitCommonDir = await getGitCommonDir(worktreeDir);
815
+ if (gitCommonDir instanceof Error) {
816
+ await cleanupTempBranch();
817
+ return gitCommonDir;
818
+ }
819
+ log(`Pushing to ${defaultBranch}...`);
820
+ const pushResult = await git(worktreeDir, `push --receive-pack="git -c receive.denyCurrentBranch=updateInstead receive-pack" "${gitCommonDir}" "HEAD:${defaultBranch}"`, { timeout: 30_000 });
821
+ if (pushResult instanceof Error) {
822
+ await cleanupTempBranch();
823
+ return new PushError({ target: defaultBranch, cause: pushResult });
824
+ }
825
+ // Get short SHA for display
826
+ const shortSha = await git(worktreeDir, 'rev-parse --short HEAD');
827
+ if (shortSha instanceof Error) {
828
+ // Push succeeded but can't get SHA -- non-fatal, use placeholder
829
+ logger.warn('Failed to get short SHA after push');
830
+ }
831
+ // ── Step 5: Clean up -- detach HEAD and delete branch ──
832
+ log('Cleaning up worktree...');
833
+ const detachResult = await git(worktreeDir, `checkout --detach "${defaultBranch}"`);
834
+ if (detachResult instanceof Error) {
835
+ logger.warn(`[MERGE CLEANUP] Failed to detach worktree HEAD after push: ${detachResult.message}`);
836
+ }
837
+ const deleteBranchResult = await git(worktreeDir, `branch -D "${branchName}"`);
838
+ if (deleteBranchResult instanceof Error) {
839
+ logger.warn(`[MERGE CLEANUP] Failed to delete branch ${branchName}: ${deleteBranchResult.message}`);
840
+ }
841
+ if (branchName !== worktreeName && worktreeName) {
842
+ const deleteWorktreeBranchResult = await git(worktreeDir, `branch -D "${worktreeName}"`);
843
+ if (deleteWorktreeBranchResult instanceof Error) {
844
+ logger.warn(`[MERGE CLEANUP] Failed to delete worktree branch ${worktreeName}: ${deleteWorktreeBranchResult.message}`);
845
+ }
846
+ }
847
+ return {
848
+ defaultBranch,
849
+ branchName: worktreeName || branchName,
850
+ commitCount,
851
+ shortSha: shortSha instanceof Error ? 'unknown' : shortSha,
852
+ };
853
+ }
854
+ /**
855
+ * List branches sorted by most recent commit date.
856
+ * Returns branch short names (e.g. "main", "origin/feature-x").
857
+ * Filters by optional query string (case-insensitive substring match).
858
+ * Limited to 25 results for Discord autocomplete.
859
+ *
860
+ * @param includeRemote - When true (default), includes remote tracking branches (`-a` flag).
861
+ * Set to false for merge targets where only local branches make sense.
862
+ */
863
+ export async function listBranchesByLastCommit({ directory, query, includeRemote = true, }) {
864
+ const branchFlag = includeRemote ? '-a' : '';
865
+ const result = await git(directory, `branch ${branchFlag} --sort=-committerdate --format=%(refname:short)`);
866
+ if (result instanceof Error) {
867
+ return [];
868
+ }
869
+ const lowerQuery = query?.toLowerCase() || '';
870
+ return result
871
+ .split('\n')
872
+ .map((line) => {
873
+ return line.trim();
874
+ })
875
+ .filter((name) => {
876
+ if (!name) {
877
+ return false;
878
+ }
879
+ // Skip HEAD pointer entries like "origin/HEAD -> origin/main"
880
+ if (name.includes('->')) {
881
+ return false;
882
+ }
883
+ if (!lowerQuery) {
884
+ return true;
885
+ }
886
+ return name.toLowerCase().includes(lowerQuery);
887
+ })
888
+ .slice(0, 25);
889
+ }
890
+ /**
891
+ * Validate that a branch name is safe for use in git commands.
892
+ * Uses `git check-ref-format --branch` which rejects names with shell metacharacters,
893
+ * double dots, trailing dots/locks, etc. Returns the normalized name or an Error.
894
+ */
895
+ export async function validateBranchRef({ directory, ref, }) {
896
+ const result = await git(directory, `check-ref-format --branch ${JSON.stringify(ref)}`);
897
+ if (result instanceof Error) {
898
+ return new Error(`Invalid branch name: ${ref}`);
899
+ }
900
+ return result;
901
+ }
902
+ /**
903
+ * Validate that a directory is a git worktree of the given project.
904
+ * Parses `git worktree list --porcelain` from the project directory and
905
+ * checks that the candidate path appears as one of the listed worktrees.
906
+ * Returns the resolved absolute path on success, or an Error on failure.
907
+ */
908
+ export async function validateWorktreeDirectory({ projectDirectory, candidatePath, }) {
909
+ const absoluteCandidate = path.resolve(candidatePath);
910
+ if (!fs.existsSync(absoluteCandidate)) {
911
+ return new Error(`Directory does not exist: ${absoluteCandidate}`);
912
+ }
913
+ const result = await git(projectDirectory, 'worktree list --porcelain');
914
+ if (result instanceof Error) {
915
+ return new Error('Failed to list git worktrees', { cause: result });
916
+ }
917
+ const worktreePaths = result
918
+ .split('\n')
919
+ .filter((line) => {
920
+ return line.startsWith('worktree ');
921
+ })
922
+ .map((line) => {
923
+ return line.slice('worktree '.length);
924
+ });
925
+ if (!worktreePaths.includes(absoluteCandidate)) {
926
+ return new Error(`Directory is not a git worktree of ${projectDirectory}: ${absoluteCandidate}`);
927
+ }
928
+ return absoluteCandidate;
929
+ }