borgmcp 1.1.15 → 2.0.2

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 (466) hide show
  1. package/CONTRIBUTING.md +15 -0
  2. package/LICENSE +201 -33
  3. package/NOTICE +2 -0
  4. package/README.md +85 -36
  5. package/SECURITY.md +7 -0
  6. package/dist/agent-runtime.d.ts.map +1 -0
  7. package/dist/agent-runtime.js +63 -1
  8. package/dist/agent-runtime.js.map +1 -0
  9. package/dist/assimilate-cmd.d.ts +96 -17
  10. package/dist/assimilate-cmd.d.ts.map +1 -0
  11. package/dist/assimilate-cmd.js +1396 -48
  12. package/dist/assimilate-cmd.js.map +1 -0
  13. package/dist/assimilate-deps.d.ts.map +1 -0
  14. package/dist/assimilate-deps.js +384 -3
  15. package/dist/assimilate-deps.js.map +1 -0
  16. package/dist/assimilate-guard.d.ts +5 -7
  17. package/dist/assimilate-guard.d.ts.map +1 -0
  18. package/dist/assimilate-guard.js +60 -2
  19. package/dist/assimilate-guard.js.map +1 -0
  20. package/dist/assimilate-welcome.d.ts +1 -1
  21. package/dist/assimilate-welcome.d.ts.map +1 -0
  22. package/dist/assimilate-welcome.js +32 -2
  23. package/dist/assimilate-welcome.js.map +1 -0
  24. package/dist/auth-env.d.ts +7 -50
  25. package/dist/auth-env.d.ts.map +1 -0
  26. package/dist/auth-env.js +22 -1
  27. package/dist/auth-env.js.map +1 -0
  28. package/dist/backends/launch-all-pastelist.d.ts.map +1 -0
  29. package/dist/backends/launch-all-pastelist.js +11 -4
  30. package/dist/backends/launch-all-pastelist.js.map +1 -0
  31. package/dist/backends/launch-all-tmux.d.ts.map +1 -0
  32. package/dist/backends/launch-all-tmux.js +49 -1
  33. package/dist/backends/launch-all-tmux.js.map +1 -0
  34. package/dist/backends/launch-all-windows.d.ts.map +1 -0
  35. package/dist/backends/launch-all-windows.js +86 -12
  36. package/dist/backends/launch-all-windows.js.map +1 -0
  37. package/dist/bare-launch-menu.d.ts.map +1 -0
  38. package/dist/bare-launch-menu.js +83 -6
  39. package/dist/bare-launch-menu.js.map +1 -0
  40. package/dist/claude-launch-args.d.ts.map +1 -0
  41. package/dist/claude-launch-args.js +34 -1
  42. package/dist/claude-launch-args.js.map +1 -0
  43. package/dist/claude.d.ts.map +1 -0
  44. package/dist/claude.js +486 -14
  45. package/dist/claude.js.map +1 -0
  46. package/dist/cleanup-cmd.d.ts +5 -5
  47. package/dist/cleanup-cmd.d.ts.map +1 -0
  48. package/dist/cleanup-cmd.js +453 -14
  49. package/dist/cleanup-cmd.js.map +1 -0
  50. package/dist/clear-rewake-core.d.ts.map +1 -0
  51. package/dist/clear-rewake-core.js +23 -2
  52. package/dist/clear-rewake-core.js.map +1 -0
  53. package/dist/clear-rewake.d.ts.map +1 -0
  54. package/dist/clear-rewake.js +19 -1
  55. package/dist/clear-rewake.js.map +1 -0
  56. package/dist/cli-help.d.ts +5 -0
  57. package/dist/cli-help.d.ts.map +1 -0
  58. package/dist/cli-help.js +104 -56
  59. package/dist/cli-help.js.map +1 -0
  60. package/dist/cli-platform.d.ts.map +1 -0
  61. package/dist/cli-platform.js +106 -4
  62. package/dist/cli-platform.js.map +1 -0
  63. package/dist/cli-tool-approval.d.ts +84 -0
  64. package/dist/cli-tool-approval.d.ts.map +1 -0
  65. package/dist/cli-tool-approval.js +583 -0
  66. package/dist/cli-tool-approval.js.map +1 -0
  67. package/dist/codex-app-server.d.ts.map +1 -0
  68. package/dist/codex-app-server.js +228 -4
  69. package/dist/codex-app-server.js.map +1 -0
  70. package/dist/codex-app-wake.d.ts +9 -11
  71. package/dist/codex-app-wake.d.ts.map +1 -0
  72. package/dist/codex-app-wake.js +436 -2
  73. package/dist/codex-app-wake.js.map +1 -0
  74. package/dist/codex-launch.d.ts.map +1 -0
  75. package/dist/codex-launch.js +102 -2
  76. package/dist/codex-launch.js.map +1 -0
  77. package/dist/codex-remote.d.ts +6 -1
  78. package/dist/codex-remote.d.ts.map +1 -0
  79. package/dist/codex-remote.js +394 -1
  80. package/dist/codex-remote.js.map +1 -0
  81. package/dist/codex-wake-resolve.d.ts +4 -4
  82. package/dist/codex-wake-resolve.d.ts.map +1 -0
  83. package/dist/codex-wake-resolve.js +127 -1
  84. package/dist/codex-wake-resolve.js.map +1 -0
  85. package/dist/config-utils.d.ts +2 -2
  86. package/dist/config-utils.d.ts.map +1 -0
  87. package/dist/config-utils.js +766 -3
  88. package/dist/config-utils.js.map +1 -0
  89. package/dist/config.d.ts +98 -61
  90. package/dist/config.d.ts.map +1 -0
  91. package/dist/config.js +426 -1
  92. package/dist/config.js.map +1 -0
  93. package/dist/console-prefix.d.ts.map +1 -0
  94. package/dist/console-prefix.js +96 -1
  95. package/dist/console-prefix.js.map +1 -0
  96. package/dist/cube-name.d.ts.map +1 -0
  97. package/dist/cube-name.js +67 -1
  98. package/dist/cube-name.js.map +1 -0
  99. package/dist/cubes.d.ts +126 -18
  100. package/dist/cubes.d.ts.map +1 -0
  101. package/dist/cubes.js +412 -4
  102. package/dist/cubes.js.map +1 -0
  103. package/dist/debug.d.ts.map +1 -0
  104. package/dist/debug.js +71 -1
  105. package/dist/debug.js.map +1 -0
  106. package/dist/direct-log.d.ts.map +1 -0
  107. package/dist/direct-log.js +11 -1
  108. package/dist/direct-log.js.map +1 -0
  109. package/dist/docs-sections.d.ts +11 -10
  110. package/dist/docs-sections.d.ts.map +1 -0
  111. package/dist/docs-sections.js +113 -3
  112. package/dist/docs-sections.js.map +1 -0
  113. package/dist/drone-lifecycle.d.ts +5 -21
  114. package/dist/drone-lifecycle.d.ts.map +1 -0
  115. package/dist/drone-lifecycle.js +57 -8
  116. package/dist/drone-lifecycle.js.map +1 -0
  117. package/dist/drone-management.d.ts +29 -0
  118. package/dist/drone-management.d.ts.map +1 -0
  119. package/dist/drone-management.js +109 -0
  120. package/dist/drone-management.js.map +1 -0
  121. package/dist/ensure-mcp-config.d.ts.map +1 -0
  122. package/dist/ensure-mcp-config.js +36 -1
  123. package/dist/ensure-mcp-config.js.map +1 -0
  124. package/dist/evict-drone.d.ts +10 -2
  125. package/dist/evict-drone.d.ts.map +1 -0
  126. package/dist/evict-drone.js +60 -1
  127. package/dist/evict-drone.js.map +1 -0
  128. package/dist/gc-orphan-inboxes.d.ts.map +1 -0
  129. package/dist/gc-orphan-inboxes.js +177 -1
  130. package/dist/gc-orphan-inboxes.js.map +1 -0
  131. package/dist/inbox-monitor.d.ts +1 -1
  132. package/dist/inbox-monitor.d.ts.map +1 -0
  133. package/dist/inbox-monitor.js +856 -3
  134. package/dist/inbox-monitor.js.map +1 -0
  135. package/dist/index.d.ts +6 -4
  136. package/dist/index.d.ts.map +1 -0
  137. package/dist/index.js +1034 -37
  138. package/dist/index.js.map +1 -0
  139. package/dist/launch-all-cmd.d.ts.map +1 -0
  140. package/dist/launch-all-cmd.js +377 -37
  141. package/dist/launch-all-cmd.js.map +1 -0
  142. package/dist/launch-all-command.d.ts.map +1 -0
  143. package/dist/launch-all-command.js +24 -1
  144. package/dist/launch-all-command.js.map +1 -0
  145. package/dist/launch-all-deps.d.ts +3 -8
  146. package/dist/launch-all-deps.d.ts.map +1 -0
  147. package/dist/launch-all-deps.js +99 -1
  148. package/dist/launch-all-deps.js.map +1 -0
  149. package/dist/launch-all-discovery.d.ts +1 -0
  150. package/dist/launch-all-discovery.d.ts.map +1 -0
  151. package/dist/launch-all-discovery.js +90 -4
  152. package/dist/launch-all-discovery.js.map +1 -0
  153. package/dist/launch-all-locks.d.ts.map +1 -0
  154. package/dist/launch-all-locks.js +54 -1
  155. package/dist/launch-all-locks.js.map +1 -0
  156. package/dist/launch-gate.d.ts +2 -2
  157. package/dist/launch-gate.d.ts.map +1 -0
  158. package/dist/launch-gate.js +73 -1
  159. package/dist/launch-gate.js.map +1 -0
  160. package/dist/lifecycle-log-guard.d.ts.map +1 -0
  161. package/dist/lifecycle-log-guard.js +93 -2
  162. package/dist/lifecycle-log-guard.js.map +1 -0
  163. package/dist/list-roles-render.d.ts.map +1 -0
  164. package/dist/list-roles-render.js +40 -6
  165. package/dist/list-roles-render.js.map +1 -0
  166. package/dist/local-log-routing.d.ts +19 -0
  167. package/dist/local-log-routing.d.ts.map +1 -0
  168. package/dist/local-log-routing.js +67 -0
  169. package/dist/local-log-routing.js.map +1 -0
  170. package/dist/local-manage-tool-result.d.ts +8 -0
  171. package/dist/local-manage-tool-result.d.ts.map +1 -0
  172. package/dist/local-manage-tool-result.js +10 -0
  173. package/dist/local-manage-tool-result.js.map +1 -0
  174. package/dist/local-server-cursor.d.ts +34 -0
  175. package/dist/local-server-cursor.d.ts.map +1 -0
  176. package/dist/local-server-cursor.js +151 -0
  177. package/dist/local-server-cursor.js.map +1 -0
  178. package/dist/log-audit.d.ts.map +1 -0
  179. package/dist/log-audit.js +194 -3
  180. package/dist/log-audit.js.map +1 -0
  181. package/dist/log-stream.d.ts +35 -18
  182. package/dist/log-stream.d.ts.map +1 -0
  183. package/dist/log-stream.js +1270 -12
  184. package/dist/log-stream.js.map +1 -0
  185. package/dist/model-presets.d.ts.map +1 -0
  186. package/dist/model-presets.js +19 -1
  187. package/dist/model-presets.js.map +1 -0
  188. package/dist/name-validator.d.ts.map +1 -0
  189. package/dist/name-validator.js +22 -1
  190. package/dist/name-validator.js.map +1 -0
  191. package/dist/opencode-drone.d.ts.map +1 -0
  192. package/dist/opencode-drone.js +438 -4
  193. package/dist/opencode-drone.js.map +1 -0
  194. package/dist/opencode-plugin.d.ts +1 -1
  195. package/dist/opencode-plugin.d.ts.map +1 -0
  196. package/dist/opencode-plugin.js +29 -4
  197. package/dist/opencode-plugin.js.map +1 -0
  198. package/dist/parse-assimilate-args.d.ts +1 -1
  199. package/dist/parse-assimilate-args.d.ts.map +1 -0
  200. package/dist/parse-assimilate-args.js +125 -1
  201. package/dist/parse-assimilate-args.js.map +1 -0
  202. package/dist/parse-launch-all-args.d.ts.map +1 -0
  203. package/dist/parse-launch-all-args.js +70 -1
  204. package/dist/parse-launch-all-args.js.map +1 -0
  205. package/dist/postinstall-banner.d.ts.map +1 -0
  206. package/dist/postinstall-banner.js +27 -2
  207. package/dist/postinstall-banner.js.map +1 -0
  208. package/dist/postinstall.d.ts.map +1 -0
  209. package/dist/postinstall.js +32 -5
  210. package/dist/postinstall.js.map +1 -0
  211. package/dist/readiness-probe.d.ts.map +1 -0
  212. package/dist/readiness-probe.js +9 -1
  213. package/dist/readiness-probe.js.map +1 -0
  214. package/dist/regen-format.d.ts.map +1 -0
  215. package/dist/regen-format.js +545 -13
  216. package/dist/regen-format.js.map +1 -0
  217. package/dist/regen.d.ts.map +1 -0
  218. package/dist/regen.js +141 -5
  219. package/dist/regen.js.map +1 -0
  220. package/dist/remote-client.d.ts +88 -173
  221. package/dist/remote-client.d.ts.map +1 -0
  222. package/dist/remote-client.js +997 -1
  223. package/dist/remote-client.js.map +1 -0
  224. package/dist/reset-local-seat-cmd.d.ts +50 -0
  225. package/dist/reset-local-seat-cmd.d.ts.map +1 -0
  226. package/dist/reset-local-seat-cmd.js +169 -0
  227. package/dist/reset-local-seat-cmd.js.map +1 -0
  228. package/dist/role-match.d.ts.map +1 -0
  229. package/dist/role-match.js +23 -1
  230. package/dist/role-match.js.map +1 -0
  231. package/dist/role-resolver.d.ts.map +1 -0
  232. package/dist/role-resolver.js +60 -1
  233. package/dist/role-resolver.js.map +1 -0
  234. package/dist/roster-render.d.ts.map +1 -0
  235. package/dist/roster-render.js +121 -3
  236. package/dist/roster-render.js.map +1 -0
  237. package/dist/seat-probe.d.ts +32 -13
  238. package/dist/seat-probe.d.ts.map +1 -0
  239. package/dist/seat-probe.js +78 -1
  240. package/dist/seat-probe.js.map +1 -0
  241. package/dist/seat-store.d.ts +76 -0
  242. package/dist/seat-store.d.ts.map +1 -0
  243. package/dist/seat-store.js +309 -0
  244. package/dist/seat-store.js.map +1 -0
  245. package/dist/seats.d.ts +375 -0
  246. package/dist/seats.d.ts.map +1 -0
  247. package/dist/seats.js +670 -0
  248. package/dist/seats.js.map +1 -0
  249. package/dist/self-path.d.ts +27 -0
  250. package/dist/self-path.d.ts.map +1 -0
  251. package/dist/self-path.js +43 -0
  252. package/dist/self-path.js.map +1 -0
  253. package/dist/server-endpoint.d.ts +9 -0
  254. package/dist/server-endpoint.d.ts.map +1 -0
  255. package/dist/server-endpoint.js +38 -0
  256. package/dist/server-endpoint.js.map +1 -0
  257. package/dist/server-errors.d.ts +50 -0
  258. package/dist/server-errors.d.ts.map +1 -0
  259. package/dist/server-errors.js +79 -0
  260. package/dist/server-errors.js.map +1 -0
  261. package/dist/server-handshake.d.ts +197 -0
  262. package/dist/server-handshake.d.ts.map +1 -0
  263. package/dist/server-handshake.js +533 -0
  264. package/dist/server-handshake.js.map +1 -0
  265. package/dist/server-response.d.ts +3 -0
  266. package/dist/server-response.d.ts.map +1 -0
  267. package/dist/server-response.js +67 -0
  268. package/dist/server-response.js.map +1 -0
  269. package/dist/server-trust.d.ts +14 -0
  270. package/dist/server-trust.d.ts.map +1 -0
  271. package/dist/server-trust.js +225 -0
  272. package/dist/server-trust.js.map +1 -0
  273. package/dist/session-continuity.d.ts +33 -0
  274. package/dist/session-continuity.d.ts.map +1 -0
  275. package/dist/session-continuity.js +220 -0
  276. package/dist/session-continuity.js.map +1 -0
  277. package/dist/setup-confirm.d.ts +3 -3
  278. package/dist/setup-confirm.d.ts.map +1 -0
  279. package/dist/setup-confirm.js +126 -2
  280. package/dist/setup-confirm.js.map +1 -0
  281. package/dist/setup.d.ts +5 -3
  282. package/dist/setup.d.ts.map +1 -0
  283. package/dist/setup.js +169 -46
  284. package/dist/setup.js.map +1 -0
  285. package/dist/shell-escape.d.ts.map +1 -0
  286. package/dist/shell-escape.js +22 -1
  287. package/dist/shell-escape.js.map +1 -0
  288. package/dist/spawn.d.ts +2 -2
  289. package/dist/spawn.d.ts.map +1 -0
  290. package/dist/spawn.js +29 -10
  291. package/dist/spawn.js.map +1 -0
  292. package/dist/startup-services.d.ts +0 -1
  293. package/dist/startup-services.d.ts.map +1 -0
  294. package/dist/startup-services.js +26 -1
  295. package/dist/startup-services.js.map +1 -0
  296. package/dist/stream-owner.d.ts +6 -2
  297. package/dist/stream-owner.d.ts.map +1 -0
  298. package/dist/stream-owner.js +547 -2
  299. package/dist/stream-owner.js.map +1 -0
  300. package/dist/stream-status.d.ts +1 -1
  301. package/dist/stream-status.d.ts.map +1 -0
  302. package/dist/stream-status.js +268 -3
  303. package/dist/stream-status.js.map +1 -0
  304. package/dist/sync-roles-render.d.ts.map +1 -0
  305. package/dist/sync-roles-render.js +118 -3
  306. package/dist/sync-roles-render.js.map +1 -0
  307. package/dist/sync.d.ts +1 -1
  308. package/dist/sync.d.ts.map +1 -0
  309. package/dist/sync.js +286 -22
  310. package/dist/sync.js.map +1 -0
  311. package/dist/terminal-title.d.ts.map +1 -0
  312. package/dist/terminal-title.js +68 -1
  313. package/dist/terminal-title.js.map +1 -0
  314. package/dist/token-store.d.ts +17 -111
  315. package/dist/token-store.d.ts.map +1 -0
  316. package/dist/token-store.js +81 -1
  317. package/dist/token-store.js.map +1 -0
  318. package/dist/tool-manifest.d.ts +4 -8
  319. package/dist/tool-manifest.d.ts.map +1 -0
  320. package/dist/tool-manifest.js +554 -1
  321. package/dist/tool-manifest.js.map +1 -0
  322. package/dist/tool-scope.d.ts +8 -12
  323. package/dist/tool-scope.d.ts.map +1 -0
  324. package/dist/tool-scope.js +97 -1
  325. package/dist/tool-scope.js.map +1 -0
  326. package/dist/types.d.ts +1 -5
  327. package/dist/types.d.ts.map +1 -0
  328. package/dist/types.js +5 -0
  329. package/dist/types.js.map +1 -0
  330. package/dist/unknown-subcommand.d.ts +1 -1
  331. package/dist/unknown-subcommand.d.ts.map +1 -0
  332. package/dist/unknown-subcommand.js +37 -1
  333. package/dist/unknown-subcommand.js.map +1 -0
  334. package/dist/version.d.ts +1 -1
  335. package/dist/version.d.ts.map +1 -0
  336. package/dist/version.js +78 -2
  337. package/dist/version.js.map +1 -0
  338. package/dist/working-repo.d.ts.map +1 -0
  339. package/dist/working-repo.js +81 -1
  340. package/dist/working-repo.js.map +1 -0
  341. package/dist/worktree-lifecycle.d.ts.map +1 -0
  342. package/dist/worktree-lifecycle.js +205 -2
  343. package/dist/worktree-lifecycle.js.map +1 -0
  344. package/docs/EXTRACTION_PROVENANCE.md +42 -0
  345. package/docs/LOCAL_SERVER.md +103 -0
  346. package/docs/RELEASING.md +129 -0
  347. package/package.json +52 -34
  348. package/src/agent-runtime.ts +69 -0
  349. package/src/assimilate-cmd.ts +1864 -0
  350. package/src/assimilate-deps.ts +444 -0
  351. package/src/assimilate-guard.ts +84 -0
  352. package/src/assimilate-welcome.ts +39 -0
  353. package/src/auth-env.ts +21 -0
  354. package/src/backends/launch-all-pastelist.ts +18 -0
  355. package/src/backends/launch-all-tmux.ts +75 -0
  356. package/src/backends/launch-all-windows.ts +110 -0
  357. package/src/bare-launch-menu.ts +148 -0
  358. package/src/claude-launch-args.ts +35 -0
  359. package/src/claude.ts +561 -0
  360. package/src/cleanup-cmd.ts +581 -0
  361. package/src/clear-rewake-core.ts +34 -0
  362. package/src/clear-rewake.ts +18 -0
  363. package/src/cli-help.ts +116 -0
  364. package/src/cli-platform.ts +127 -0
  365. package/src/cli-tool-approval.ts +716 -0
  366. package/src/codex-app-server.ts +238 -0
  367. package/src/codex-app-wake.ts +524 -0
  368. package/src/codex-launch.ts +140 -0
  369. package/src/codex-remote.ts +500 -0
  370. package/src/codex-wake-resolve.ts +155 -0
  371. package/src/config-utils.ts +811 -0
  372. package/src/config.ts +595 -0
  373. package/src/console-prefix.ts +99 -0
  374. package/src/cube-name.ts +69 -0
  375. package/src/cubes.ts +583 -0
  376. package/src/debug.ts +73 -0
  377. package/src/direct-log.ts +9 -0
  378. package/src/docs-sections.ts +129 -0
  379. package/src/drone-lifecycle.ts +62 -0
  380. package/src/drone-management.ts +162 -0
  381. package/src/ensure-mcp-config.ts +54 -0
  382. package/src/evict-drone.ts +82 -0
  383. package/src/gc-orphan-inboxes.ts +229 -0
  384. package/src/inbox-monitor.ts +1005 -0
  385. package/src/index.ts +1201 -0
  386. package/src/launch-all-cmd.ts +450 -0
  387. package/src/launch-all-command.ts +30 -0
  388. package/src/launch-all-deps.ts +188 -0
  389. package/src/launch-all-discovery.ts +118 -0
  390. package/src/launch-all-locks.ts +74 -0
  391. package/src/launch-gate.ts +80 -0
  392. package/src/lifecycle-log-guard.ts +144 -0
  393. package/src/list-roles-render.ts +52 -0
  394. package/src/local-log-routing.ts +106 -0
  395. package/src/local-manage-tool-result.ts +10 -0
  396. package/src/local-server-cursor.ts +188 -0
  397. package/src/log-audit.ts +207 -0
  398. package/src/log-stream.ts +1567 -0
  399. package/src/model-presets.ts +22 -0
  400. package/src/name-validator.ts +23 -0
  401. package/src/opencode-drone.ts +528 -0
  402. package/src/opencode-plugin.ts +38 -0
  403. package/src/parse-assimilate-args.ts +125 -0
  404. package/src/parse-launch-all-args.ts +100 -0
  405. package/src/postinstall-banner.ts +37 -0
  406. package/src/postinstall.ts +36 -0
  407. package/src/readiness-probe.ts +12 -0
  408. package/src/regen-format.ts +651 -0
  409. package/src/regen.ts +150 -0
  410. package/src/remote-client.ts +1457 -0
  411. package/src/reset-local-seat-cmd.ts +227 -0
  412. package/src/role-match.ts +29 -0
  413. package/src/role-resolver.ts +93 -0
  414. package/src/roster-render.ts +196 -0
  415. package/src/seat-probe.ts +115 -0
  416. package/src/seat-store.ts +333 -0
  417. package/src/seats.ts +911 -0
  418. package/src/self-path.ts +51 -0
  419. package/src/server-endpoint.ts +38 -0
  420. package/src/server-errors.ts +95 -0
  421. package/src/server-handshake.ts +816 -0
  422. package/src/server-response.ts +76 -0
  423. package/src/server-trust.ts +245 -0
  424. package/src/session-continuity.ts +280 -0
  425. package/src/setup-confirm.ts +171 -0
  426. package/src/setup.ts +186 -0
  427. package/src/shell-escape.ts +22 -0
  428. package/src/spawn.ts +31 -0
  429. package/src/startup-services.ts +36 -0
  430. package/src/stream-owner.ts +686 -0
  431. package/src/stream-status.ts +337 -0
  432. package/src/sync-roles-render.ts +163 -0
  433. package/src/sync.ts +363 -0
  434. package/src/terminal-title.ts +76 -0
  435. package/src/token-store.ts +102 -0
  436. package/src/tool-manifest.ts +610 -0
  437. package/src/tool-scope.ts +110 -0
  438. package/src/types.ts +5 -0
  439. package/src/unknown-subcommand.ts +35 -0
  440. package/src/version.ts +82 -0
  441. package/src/working-repo.ts +99 -0
  442. package/src/worktree-lifecycle.ts +254 -0
  443. package/dist/auth-recovery.d.ts +0 -33
  444. package/dist/auth-recovery.js +0 -1
  445. package/dist/auth.d.ts +0 -103
  446. package/dist/auth.js +0 -39
  447. package/dist/device-auth.d.ts +0 -75
  448. package/dist/device-auth.js +0 -1
  449. package/dist/drone-address.d.ts +0 -26
  450. package/dist/drone-address.js +0 -1
  451. package/dist/get-started.d.ts +0 -20
  452. package/dist/get-started.js +0 -2
  453. package/dist/health-beat.d.ts +0 -134
  454. package/dist/health-beat.js +0 -1
  455. package/dist/role-section.d.ts +0 -110
  456. package/dist/role-section.js +0 -8
  457. package/dist/setup-action.d.ts +0 -20
  458. package/dist/setup-action.js +0 -1
  459. package/dist/stale-version-check.d.ts +0 -40
  460. package/dist/stale-version-check.js +0 -1
  461. package/dist/subscription-retry.d.ts +0 -40
  462. package/dist/subscription-retry.js +0 -1
  463. package/dist/templates.d.ts +0 -107
  464. package/dist/templates.js +0 -359
  465. package/dist/token-crypto.d.ts +0 -50
  466. package/dist/token-crypto.js +0 -1
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Temporary compatibility helpers for the legacy Borg-managed Claude model
3
+ * selector. Model/provider configuration belongs to the launched agent CLI;
4
+ * the remaining selector is retired with the assimilation model wire cleanup.
5
+ */
6
+
7
+ export const MODEL_DESCRIPTOR_REGEX = /^claude:[A-Za-z0-9._:\/-]+$/;
8
+
9
+ export function parseModel(descriptor: string): { kind: 'claude'; model: string } {
10
+ if (!MODEL_DESCRIPTOR_REGEX.test(descriptor)) {
11
+ throw new Error(`invalid model descriptor: ${descriptor} (expected claude:<model>)`);
12
+ }
13
+ return { kind: 'claude', model: descriptor.slice('claude:'.length) };
14
+ }
15
+
16
+ export function resolveLaunchEnv(
17
+ descriptor: string | null
18
+ ): { set: Record<string, string>; unset: string[] } {
19
+ if (!descriptor) return { set: {}, unset: [] };
20
+ const { model } = parseModel(descriptor);
21
+ return { set: { ANTHROPIC_MODEL: model }, unset: [] };
22
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Validates an identifier used in worktree paths and other safety-critical
3
+ * positions (must survive shell-arg parsing without escaping).
4
+ *
5
+ * Allowed: lowercase ASCII letters, digits, hyphens, underscores.
6
+ * Length 1–48. Anchored. No leading hyphen (would parse as a flag).
7
+ */
8
+ const NAME_RE = /^[a-z0-9_][a-z0-9_-]{0,47}$/;
9
+
10
+ export function validateName(name: string): { ok: true } | { ok: false; error: string } {
11
+ if (name.length === 0) {
12
+ return { ok: false, error: `name must not be empty` };
13
+ }
14
+ if (!NAME_RE.test(name)) {
15
+ return {
16
+ ok: false,
17
+ error:
18
+ `invalid name "${name}". Use lowercase letters, digits, hyphens, or ` +
19
+ `underscores; max 48 chars; must not start with a hyphen.`,
20
+ };
21
+ }
22
+ return { ok: true };
23
+ }
@@ -0,0 +1,528 @@
1
+ import { appendFileSync, existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
2
+ import { createHash, randomUUID } from 'crypto';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+
6
+ const LOG_FILE = join(tmpdir(), 'borg-opencode-drone.log');
7
+ function log(msg: string) {
8
+ const line = `[${new Date().toISOString()}] ${msg}\n`;
9
+ try { appendFileSync(LOG_FILE, line); } catch {}
10
+ }
11
+
12
+ interface OpenCodeDroneState {
13
+ serverUrl: string;
14
+ sessionId: string | null;
15
+ sessionCreatedAt: number | null;
16
+ knownRootSessionIds: string[];
17
+ directory: string;
18
+ droneLabel: string;
19
+ cubeName: string;
20
+ connected: boolean;
21
+ totalEntriesInjected: number;
22
+ }
23
+
24
+ let state: OpenCodeDroneState | null = null;
25
+
26
+ interface ConnectDeps {
27
+ serverUrl: string;
28
+ directory: string;
29
+ droneLabel: string;
30
+ cubeName: string;
31
+ }
32
+
33
+ interface OCSession {
34
+ id: string;
35
+ directory: string;
36
+ time: { created: number };
37
+ parentID?: string;
38
+ agent?: string;
39
+ model?: { providerID: string; modelID: string };
40
+ }
41
+
42
+ interface OCMessage {
43
+ info?: {
44
+ role?: string;
45
+ time?: { created?: number };
46
+ };
47
+ parts?: Array<{
48
+ type?: string;
49
+ text?: string;
50
+ }>;
51
+ }
52
+
53
+ interface SessionBinding {
54
+ version: 2;
55
+ sessionId: string;
56
+ sessionCreatedAt: number;
57
+ knownRootSessionIds: string[];
58
+ serverUrl: string;
59
+ directory: string;
60
+ droneLabel: string;
61
+ cubeName: string;
62
+ }
63
+
64
+ export interface OpenCodeLaunchKickoff {
65
+ prompt: string;
66
+ nonce: string;
67
+ }
68
+
69
+ // This is correlation metadata, intentionally not an instruction to the
70
+ // launched agent. A markdown comment keeps it benign in the user-visible
71
+ // kickoff while preserving it in OpenCode's stored message text.
72
+ const OPEN_CODE_LAUNCH_NONCE_MARKER = 'borg-opencode-correlation:';
73
+
74
+ /**
75
+ * Add a launch-unique identity to the OpenCode-only copy of the shared
76
+ * kickoff. The prompt is what OpenCode records as its first user message, so
77
+ * the launcher can later bind the MCP child to this precise launch instead of
78
+ * guessing from a repeated kickoff's text or timestamp.
79
+ */
80
+ export function createOpenCodeLaunchKickoff(
81
+ kickoff: string,
82
+ nonce: string = randomUUID(),
83
+ ): OpenCodeLaunchKickoff {
84
+ return {
85
+ prompt: `${kickoff}\n\n<!-- ${OPEN_CODE_LAUNCH_NONCE_MARKER}${nonce} -->`,
86
+ nonce,
87
+ };
88
+ }
89
+
90
+ const bindingPathsForTests = new Set<string>();
91
+
92
+ export async function connectOpenCodeDrone(deps: ConnectDeps): Promise<void> {
93
+ state = {
94
+ serverUrl: deps.serverUrl,
95
+ sessionId: null,
96
+ sessionCreatedAt: null,
97
+ knownRootSessionIds: [],
98
+ directory: deps.directory,
99
+ droneLabel: deps.droneLabel,
100
+ cubeName: deps.cubeName,
101
+ connected: true,
102
+ totalEntriesInjected: 0,
103
+ };
104
+ log(`connected url=${deps.serverUrl} dir=${deps.directory}`);
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Raw fetch wrappers
109
+ // ---------------------------------------------------------------------------
110
+
111
+ function apiUrl(path: string): string {
112
+ const base = state!.serverUrl.replace(/\/+$/, '');
113
+ return `${base}${path}${path.includes('?') ? '&' : '?'}directory=${encodeURIComponent(state!.directory)}`;
114
+ }
115
+
116
+ const FETCH_TIMEOUT = 10_000;
117
+
118
+ async function rawGet(path: string): Promise<{ status: number; body: string }> {
119
+ const url = apiUrl(path);
120
+ const controller = new AbortController();
121
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
122
+ try {
123
+ const res = await fetch(url, { signal: controller.signal });
124
+ const body = await res.text();
125
+ return { status: res.status, body };
126
+ } finally {
127
+ clearTimeout(timer);
128
+ }
129
+ }
130
+
131
+ async function rawPost(path: string, bodyObj: unknown): Promise<{ status: number; body: string }> {
132
+ const url = apiUrl(path);
133
+ const controller = new AbortController();
134
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
135
+ try {
136
+ const res = await fetch(url, {
137
+ method: 'POST',
138
+ headers: { 'Content-Type': 'application/json' },
139
+ signal: controller.signal,
140
+ body: JSON.stringify(bodyObj),
141
+ });
142
+ const body = await res.text();
143
+ return { status: res.status, body };
144
+ } finally {
145
+ clearTimeout(timer);
146
+ }
147
+ }
148
+
149
+ async function listSessions(): Promise<OCSession[]> {
150
+ const { status, body } = await rawGet('/session');
151
+ if (status !== 200) throw new Error(`OpenCode sessions request failed (${status})`);
152
+ return JSON.parse(body);
153
+ }
154
+
155
+ async function getSession(id: string): Promise<OCSession | null> {
156
+ try {
157
+ const { status, body } = await rawGet(`/session/${id}`);
158
+ if (status !== 200) return null;
159
+ return JSON.parse(body);
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ async function listSessionMessages(id: string): Promise<OCMessage[]> {
166
+ const { status, body } = await rawGet(`/session/${id}/message`);
167
+ if (status !== 200) throw new Error(`OpenCode session messages request failed (${status})`);
168
+ return JSON.parse(body);
169
+ }
170
+
171
+ async function promptSession(id: string, bodyObj: Record<string, unknown>): Promise<boolean> {
172
+ const { status } = await rawPost(`/session/${id}/prompt_async`, bodyObj);
173
+ return status === 200 || status === 204;
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Persist the launch-selected session for the separately spawned MCP child.
178
+ // ---------------------------------------------------------------------------
179
+
180
+ function bindingPath(): string {
181
+ const current = state!;
182
+ const key = [current.serverUrl, current.directory, current.cubeName, current.droneLabel].join('\0');
183
+ const digest = createHash('sha256').update(key).digest('hex').slice(0, 24);
184
+ const path = join(tmpdir(), `borg-opencode-session-${digest}.json`);
185
+ bindingPathsForTests.add(path);
186
+ return path;
187
+ }
188
+
189
+ function bindingMatchesState(binding: SessionBinding): boolean {
190
+ const current = state!;
191
+ return binding.version === 2
192
+ && binding.serverUrl === current.serverUrl
193
+ && binding.directory === current.directory
194
+ && binding.droneLabel === current.droneLabel
195
+ && binding.cubeName === current.cubeName
196
+ && typeof binding.sessionId === 'string'
197
+ && typeof binding.sessionCreatedAt === 'number'
198
+ && Array.isArray(binding.knownRootSessionIds)
199
+ && binding.knownRootSessionIds.every((id) => typeof id === 'string');
200
+ }
201
+
202
+ function readBinding(): SessionBinding | null {
203
+ try {
204
+ const path = bindingPath();
205
+ if (!existsSync(path)) return null;
206
+ const parsed = JSON.parse(readFileSync(path, 'utf-8')) as SessionBinding;
207
+ return bindingMatchesState(parsed) ? parsed : null;
208
+ } catch {
209
+ return null;
210
+ }
211
+ }
212
+
213
+ function clearBinding(): void {
214
+ if (!state) return;
215
+ const path = bindingPath();
216
+ state.sessionId = null;
217
+ state.sessionCreatedAt = null;
218
+ state.knownRootSessionIds = [];
219
+ try {
220
+ unlinkSync(path);
221
+ } catch {
222
+ // The file may have already been removed by the launch process.
223
+ }
224
+ }
225
+
226
+ function saveBinding(session: OCSession, knownRootSessionIds: string[]): void {
227
+ const current = state!;
228
+ const binding: SessionBinding = {
229
+ version: 2,
230
+ sessionId: session.id,
231
+ sessionCreatedAt: session.time.created,
232
+ knownRootSessionIds,
233
+ serverUrl: current.serverUrl,
234
+ directory: current.directory,
235
+ droneLabel: current.droneLabel,
236
+ cubeName: current.cubeName,
237
+ };
238
+ current.sessionId = binding.sessionId;
239
+ current.sessionCreatedAt = binding.sessionCreatedAt;
240
+ current.knownRootSessionIds = binding.knownRootSessionIds;
241
+
242
+ try {
243
+ const path = bindingPath();
244
+ const temporary = `${path}.${process.pid}.tmp`;
245
+ writeFileSync(temporary, JSON.stringify(binding), { mode: 0o600 });
246
+ renameSync(temporary, path);
247
+ } catch (err) {
248
+ log(`session binding write failed: ${err}`);
249
+ }
250
+ }
251
+
252
+ function restoreBinding(): SessionBinding | null {
253
+ if (!state) return null;
254
+ if (state.sessionId && state.sessionCreatedAt !== null) {
255
+ return {
256
+ version: 2,
257
+ sessionId: state.sessionId,
258
+ sessionCreatedAt: state.sessionCreatedAt,
259
+ knownRootSessionIds: state.knownRootSessionIds,
260
+ serverUrl: state.serverUrl,
261
+ directory: state.directory,
262
+ droneLabel: state.droneLabel,
263
+ cubeName: state.cubeName,
264
+ };
265
+ }
266
+
267
+ const binding = readBinding();
268
+ if (!binding) return null;
269
+ state.sessionId = binding.sessionId;
270
+ state.sessionCreatedAt = binding.sessionCreatedAt;
271
+ state.knownRootSessionIds = binding.knownRootSessionIds;
272
+ return binding;
273
+ }
274
+
275
+ function isBoundSession(session: OCSession, binding: SessionBinding): boolean {
276
+ return session.id === binding.sessionId && session.directory === state!.directory;
277
+ }
278
+
279
+ function isTopLevelSession(session: OCSession): boolean {
280
+ return !session.parentID;
281
+ }
282
+
283
+ async function findUnseenTopLevelSession(knownRootSessionIds: string[]): Promise<{
284
+ session: OCSession;
285
+ knownRootSessionIds: string[];
286
+ } | null> {
287
+ try {
288
+ const sessions = await listSessions();
289
+ const roots = sessions.filter(
290
+ (session) => session.directory === state!.directory
291
+ && isTopLevelSession(session),
292
+ );
293
+ const matched = roots.filter((session) => !knownRootSessionIds.includes(session.id));
294
+ if (matched.length === 0) return null;
295
+ const best = matched.reduce((a, b) =>
296
+ a.time.created > b.time.created ? a : b,
297
+ );
298
+ return { session: best, knownRootSessionIds: roots.map((session) => session.id) };
299
+ } catch {
300
+ return null;
301
+ }
302
+ }
303
+
304
+ function kickoffMessageTime(messages: OCMessage[], nonce: string): number | null {
305
+ let latest: number | null = null;
306
+ for (const message of messages) {
307
+ if (message.info?.role && message.info.role !== 'user') continue;
308
+ const matchesLaunchNonce = message.parts?.some(
309
+ (part) => part.type === 'text' && part.text?.includes(`${OPEN_CODE_LAUNCH_NONCE_MARKER}${nonce}`),
310
+ );
311
+ if (!matchesLaunchNonce) continue;
312
+ const created = message.info?.time?.created ?? 0;
313
+ latest = latest === null ? created : Math.max(latest, created);
314
+ }
315
+ return latest;
316
+ }
317
+
318
+ /**
319
+ * The launch process is the only place allowed to discover a session from the
320
+ * server. It chooses the session that contains this launch's unique nonce,
321
+ * rather than choosing by repeated kickoff text or session creation time. A
322
+ * fork is therefore allowed only when it was explicitly selected for this
323
+ * launch and received the nonce-bearing kickoff.
324
+ */
325
+ async function findLaunchSession(nonce: string): Promise<{
326
+ session: OCSession;
327
+ knownRootSessionIds: string[];
328
+ } | null> {
329
+ try {
330
+ const sessions = (await listSessions()).filter(
331
+ (session) => session.directory === state!.directory,
332
+ );
333
+ const knownRootSessionIds = sessions
334
+ .filter(isTopLevelSession)
335
+ .map((session) => session.id);
336
+ const candidates = await Promise.all(sessions.map(async (session) => {
337
+ try {
338
+ const messageTime = kickoffMessageTime(
339
+ await listSessionMessages(session.id),
340
+ nonce,
341
+ );
342
+ return messageTime === null ? null : { session, messageTime };
343
+ } catch {
344
+ return null;
345
+ }
346
+ }));
347
+ const matched = candidates.filter(
348
+ (candidate): candidate is { session: OCSession; messageTime: number } => candidate !== null,
349
+ );
350
+ if (matched.length === 0) return null;
351
+ const session = matched.reduce((best, candidate) =>
352
+ candidate.messageTime > best.messageTime ? candidate : best,
353
+ ).session;
354
+ return { session, knownRootSessionIds };
355
+ } catch {
356
+ return null;
357
+ }
358
+ }
359
+
360
+ async function resolveInjectionSession(): Promise<OCSession | null> {
361
+ const binding = restoreBinding();
362
+ if (!binding) return null;
363
+
364
+ const bound = await getSession(binding.sessionId);
365
+ if (!bound || !isBoundSession(bound, binding)) {
366
+ clearBinding();
367
+ const replacement = await findUnseenTopLevelSession(binding.knownRootSessionIds);
368
+ if (!replacement) return null;
369
+ saveBinding(replacement.session, replacement.knownRootSessionIds);
370
+ return replacement.session;
371
+ }
372
+
373
+ // `/new` creates an unseen top-level session. Keep the launch-time root
374
+ // snapshot so an old, unrelated root is never mistaken for a user switch.
375
+ // Children never supersede the bound root.
376
+ const switched = await findUnseenTopLevelSession(binding.knownRootSessionIds);
377
+ if (switched) {
378
+ saveBinding(switched.session, switched.knownRootSessionIds);
379
+ return switched.session;
380
+ }
381
+ return bound;
382
+ }
383
+
384
+ async function rebindAfterFailure(knownRootSessionIds: string[]): Promise<OCSession | null> {
385
+ const replacement = await findUnseenTopLevelSession(knownRootSessionIds);
386
+ if (!replacement) return null;
387
+ saveBinding(replacement.session, replacement.knownRootSessionIds);
388
+ return replacement.session;
389
+ }
390
+
391
+ // ---------------------------------------------------------------------------
392
+ // Public API
393
+ // ---------------------------------------------------------------------------
394
+
395
+ /**
396
+ * Wait for the OpenCode HTTP server, then capture the session that received
397
+ * this launch's nonce-bearing `--prompt` kickoff. The binding survives the separate
398
+ * MCP-child process, which must never fall back to a newest-session heuristic.
399
+ */
400
+ export async function injectInitialKickoff(launch: OpenCodeLaunchKickoff): Promise<boolean> {
401
+ if (!state?.connected) { log('kickoff: not connected'); return false; }
402
+
403
+ try {
404
+ // Wait for the server.
405
+ for (let i = 0; i < 30; i++) {
406
+ try {
407
+ await listSessions();
408
+ log(`kickoff: server ready (attempt ${i + 1})`);
409
+ break;
410
+ } catch {
411
+ // not ready yet
412
+ }
413
+ await new Promise((r) => setTimeout(r, 1000));
414
+ }
415
+
416
+ // Capture the launch-selected session, including explicit resume/fork
417
+ // targets. Unrelated sessions do not contain this launch's nonce.
418
+ for (let i = 0; i < 30; i++) {
419
+ const binding = await findLaunchSession(launch.nonce);
420
+ if (binding) {
421
+ saveBinding(binding.session, binding.knownRootSessionIds);
422
+ log(`kickoff: bound session ${binding.session.id.slice(0, 8)}…`);
423
+ return true;
424
+ }
425
+ await new Promise((r) => setTimeout(r, 1000));
426
+ }
427
+
428
+ log('kickoff: no session found');
429
+ return false;
430
+ } catch (err) {
431
+ log(`kickoff error: ${err}`);
432
+ return false;
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Inject a silent context entry (noReply) into our session.
438
+ * Falls through silently — caller falls back to inbox write.
439
+ */
440
+ export async function injectOpenCodeEntry(text: string): Promise<boolean> {
441
+ if (!state?.connected) return false;
442
+
443
+ try {
444
+ const target = await resolveInjectionSession();
445
+ if (!target) return false;
446
+
447
+ const body = {
448
+ parts: [{ type: 'text', text }],
449
+ };
450
+ if (await promptSession(target.id, body)) {
451
+ state.totalEntriesInjected++;
452
+ return true;
453
+ }
454
+
455
+ // A failed prompt means the cached target is no longer trustworthy. Clear
456
+ // it before considering only a newer root created by `/new`; never fall
457
+ // through to a child or arbitrary newest session.
458
+ const knownRootSessionIds = [...state.knownRootSessionIds];
459
+ clearBinding();
460
+ const replacement = await rebindAfterFailure(knownRootSessionIds);
461
+ if (!replacement) return false;
462
+ if (await promptSession(replacement.id, body)) {
463
+ state.totalEntriesInjected++;
464
+ return true;
465
+ }
466
+ clearBinding();
467
+ return false;
468
+ } catch (err) {
469
+ log(`entry error: ${err}`);
470
+ clearBinding();
471
+ return false;
472
+ }
473
+ }
474
+
475
+ export async function probeOpenCodeDroneArmed(): Promise<boolean | null> {
476
+ if (!state?.connected) return null;
477
+ const binding = restoreBinding();
478
+ if (!binding) return false;
479
+
480
+ try {
481
+ const session = await getSession(binding.sessionId);
482
+ if (session && isBoundSession(session, binding)) return true;
483
+ clearBinding();
484
+ return false;
485
+ } catch {
486
+ clearBinding();
487
+ return false;
488
+ }
489
+ }
490
+
491
+ export function disconnectOpenCodeDrone(): void {
492
+ state = null;
493
+ }
494
+
495
+ export function getOpenCodeConnectionState(): {
496
+ connected: boolean;
497
+ sessionId: string | null;
498
+ totalEntriesInjected: number;
499
+ } {
500
+ return {
501
+ connected: state?.connected ?? false,
502
+ sessionId: state?.sessionId ?? null,
503
+ totalEntriesInjected: state?.totalEntriesInjected ?? 0,
504
+ };
505
+ }
506
+
507
+ export function computeOpenCodePort(droneId: string, base: number = 14096): number {
508
+ let hash = 0;
509
+ for (let i = 0; i < droneId.length; i++) {
510
+ const char = droneId.charCodeAt(i);
511
+ hash = ((hash << 5) - hash) + char;
512
+ hash |= 0;
513
+ }
514
+ return base + (Math.abs(hash) % 1024);
515
+ }
516
+
517
+ /** Test-only cleanup for module state and the local cross-process binding. */
518
+ export function __resetOpenCodeDroneForTests(): void {
519
+ state = null;
520
+ for (const path of bindingPathsForTests) {
521
+ try {
522
+ unlinkSync(path);
523
+ } catch {
524
+ // Already removed.
525
+ }
526
+ }
527
+ bindingPathsForTests.clear();
528
+ }
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ /**
6
+ * Plugin source that preserves borg cube context across session compaction.
7
+ * The initial kickoff and ongoing entry injection are handled by the launcher
8
+ * and MCP client via the SDK, not by the plugin.
9
+ */
10
+ export const BORG_PLUGIN_SOURCE = `
11
+ // Borg MCP context-preservation plugin — installed by borg assimilate
12
+ export default function () {
13
+ return {
14
+ 'experimental.session.compacting': async (_input, output) => {
15
+ output.context.push(
16
+ '## Borg Cube\\nYou are in a Borg MCP multi-agent coordination cube. ' +
17
+ 'Use MCP tool borg_regen to get full context and recent activity.'
18
+ );
19
+ },
20
+ };
21
+ }
22
+ `;
23
+
24
+ const PLUGIN_REL_PATH = path.join('.config', 'opencode', 'plugins', 'borg-orient.js');
25
+
26
+ export function installBorgPlugin(): void {
27
+ const pluginPath = path.join(os.homedir(), PLUGIN_REL_PATH);
28
+ try {
29
+ if (fs.existsSync(pluginPath)) {
30
+ const existing = fs.readFileSync(pluginPath, 'utf-8');
31
+ if (existing === BORG_PLUGIN_SOURCE) return;
32
+ }
33
+ fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
34
+ fs.writeFileSync(pluginPath, BORG_PLUGIN_SOURCE, 'utf-8');
35
+ } catch {
36
+ // Best-effort — plugin is an optimization, not a requirement.
37
+ }
38
+ }