borgmcp 1.1.15 → 2.0.1

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 (459) 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.map +1 -0
  71. package/dist/codex-app-wake.js +432 -2
  72. package/dist/codex-app-wake.js.map +1 -0
  73. package/dist/codex-launch.d.ts.map +1 -0
  74. package/dist/codex-launch.js +102 -2
  75. package/dist/codex-launch.js.map +1 -0
  76. package/dist/codex-remote.d.ts +6 -1
  77. package/dist/codex-remote.d.ts.map +1 -0
  78. package/dist/codex-remote.js +394 -1
  79. package/dist/codex-remote.js.map +1 -0
  80. package/dist/codex-wake-resolve.d.ts.map +1 -0
  81. package/dist/codex-wake-resolve.js +127 -1
  82. package/dist/codex-wake-resolve.js.map +1 -0
  83. package/dist/config-utils.d.ts +2 -2
  84. package/dist/config-utils.d.ts.map +1 -0
  85. package/dist/config-utils.js +766 -3
  86. package/dist/config-utils.js.map +1 -0
  87. package/dist/config.d.ts +98 -61
  88. package/dist/config.d.ts.map +1 -0
  89. package/dist/config.js +426 -1
  90. package/dist/config.js.map +1 -0
  91. package/dist/console-prefix.d.ts.map +1 -0
  92. package/dist/console-prefix.js +96 -1
  93. package/dist/console-prefix.js.map +1 -0
  94. package/dist/cube-name.d.ts.map +1 -0
  95. package/dist/cube-name.js +67 -1
  96. package/dist/cube-name.js.map +1 -0
  97. package/dist/cubes.d.ts +126 -18
  98. package/dist/cubes.d.ts.map +1 -0
  99. package/dist/cubes.js +412 -4
  100. package/dist/cubes.js.map +1 -0
  101. package/dist/debug.d.ts.map +1 -0
  102. package/dist/debug.js +71 -1
  103. package/dist/debug.js.map +1 -0
  104. package/dist/direct-log.d.ts.map +1 -0
  105. package/dist/direct-log.js +11 -1
  106. package/dist/direct-log.js.map +1 -0
  107. package/dist/docs-sections.d.ts +11 -10
  108. package/dist/docs-sections.d.ts.map +1 -0
  109. package/dist/docs-sections.js +113 -3
  110. package/dist/docs-sections.js.map +1 -0
  111. package/dist/drone-lifecycle.d.ts +5 -21
  112. package/dist/drone-lifecycle.d.ts.map +1 -0
  113. package/dist/drone-lifecycle.js +57 -8
  114. package/dist/drone-lifecycle.js.map +1 -0
  115. package/dist/drone-management.d.ts +29 -0
  116. package/dist/drone-management.d.ts.map +1 -0
  117. package/dist/drone-management.js +109 -0
  118. package/dist/drone-management.js.map +1 -0
  119. package/dist/ensure-mcp-config.d.ts.map +1 -0
  120. package/dist/ensure-mcp-config.js +36 -1
  121. package/dist/ensure-mcp-config.js.map +1 -0
  122. package/dist/evict-drone.d.ts +10 -2
  123. package/dist/evict-drone.d.ts.map +1 -0
  124. package/dist/evict-drone.js +60 -1
  125. package/dist/evict-drone.js.map +1 -0
  126. package/dist/gc-orphan-inboxes.d.ts.map +1 -0
  127. package/dist/gc-orphan-inboxes.js +177 -1
  128. package/dist/gc-orphan-inboxes.js.map +1 -0
  129. package/dist/inbox-monitor.d.ts +1 -1
  130. package/dist/inbox-monitor.d.ts.map +1 -0
  131. package/dist/inbox-monitor.js +856 -3
  132. package/dist/inbox-monitor.js.map +1 -0
  133. package/dist/index.d.ts +6 -4
  134. package/dist/index.d.ts.map +1 -0
  135. package/dist/index.js +1034 -37
  136. package/dist/index.js.map +1 -0
  137. package/dist/launch-all-cmd.d.ts.map +1 -0
  138. package/dist/launch-all-cmd.js +377 -37
  139. package/dist/launch-all-cmd.js.map +1 -0
  140. package/dist/launch-all-command.d.ts.map +1 -0
  141. package/dist/launch-all-command.js +24 -1
  142. package/dist/launch-all-command.js.map +1 -0
  143. package/dist/launch-all-deps.d.ts +3 -8
  144. package/dist/launch-all-deps.d.ts.map +1 -0
  145. package/dist/launch-all-deps.js +99 -1
  146. package/dist/launch-all-deps.js.map +1 -0
  147. package/dist/launch-all-discovery.d.ts +1 -0
  148. package/dist/launch-all-discovery.d.ts.map +1 -0
  149. package/dist/launch-all-discovery.js +90 -4
  150. package/dist/launch-all-discovery.js.map +1 -0
  151. package/dist/launch-all-locks.d.ts.map +1 -0
  152. package/dist/launch-all-locks.js +54 -1
  153. package/dist/launch-all-locks.js.map +1 -0
  154. package/dist/launch-gate.d.ts +2 -2
  155. package/dist/launch-gate.d.ts.map +1 -0
  156. package/dist/launch-gate.js +73 -1
  157. package/dist/launch-gate.js.map +1 -0
  158. package/dist/lifecycle-log-guard.d.ts.map +1 -0
  159. package/dist/lifecycle-log-guard.js +93 -2
  160. package/dist/lifecycle-log-guard.js.map +1 -0
  161. package/dist/list-roles-render.d.ts.map +1 -0
  162. package/dist/list-roles-render.js +40 -6
  163. package/dist/list-roles-render.js.map +1 -0
  164. package/dist/local-log-routing.d.ts +19 -0
  165. package/dist/local-log-routing.d.ts.map +1 -0
  166. package/dist/local-log-routing.js +67 -0
  167. package/dist/local-log-routing.js.map +1 -0
  168. package/dist/local-manage-tool-result.d.ts +8 -0
  169. package/dist/local-manage-tool-result.d.ts.map +1 -0
  170. package/dist/local-manage-tool-result.js +10 -0
  171. package/dist/local-manage-tool-result.js.map +1 -0
  172. package/dist/local-server-cursor.d.ts +34 -0
  173. package/dist/local-server-cursor.d.ts.map +1 -0
  174. package/dist/local-server-cursor.js +151 -0
  175. package/dist/local-server-cursor.js.map +1 -0
  176. package/dist/log-audit.d.ts.map +1 -0
  177. package/dist/log-audit.js +194 -3
  178. package/dist/log-audit.js.map +1 -0
  179. package/dist/log-stream.d.ts +31 -18
  180. package/dist/log-stream.d.ts.map +1 -0
  181. package/dist/log-stream.js +1214 -12
  182. package/dist/log-stream.js.map +1 -0
  183. package/dist/model-presets.d.ts.map +1 -0
  184. package/dist/model-presets.js +19 -1
  185. package/dist/model-presets.js.map +1 -0
  186. package/dist/name-validator.d.ts.map +1 -0
  187. package/dist/name-validator.js +22 -1
  188. package/dist/name-validator.js.map +1 -0
  189. package/dist/opencode-drone.d.ts.map +1 -0
  190. package/dist/opencode-drone.js +438 -4
  191. package/dist/opencode-drone.js.map +1 -0
  192. package/dist/opencode-plugin.d.ts +1 -1
  193. package/dist/opencode-plugin.d.ts.map +1 -0
  194. package/dist/opencode-plugin.js +29 -4
  195. package/dist/opencode-plugin.js.map +1 -0
  196. package/dist/parse-assimilate-args.d.ts +1 -1
  197. package/dist/parse-assimilate-args.d.ts.map +1 -0
  198. package/dist/parse-assimilate-args.js +125 -1
  199. package/dist/parse-assimilate-args.js.map +1 -0
  200. package/dist/parse-launch-all-args.d.ts.map +1 -0
  201. package/dist/parse-launch-all-args.js +70 -1
  202. package/dist/parse-launch-all-args.js.map +1 -0
  203. package/dist/postinstall-banner.d.ts.map +1 -0
  204. package/dist/postinstall-banner.js +27 -2
  205. package/dist/postinstall-banner.js.map +1 -0
  206. package/dist/postinstall.d.ts.map +1 -0
  207. package/dist/postinstall.js +32 -5
  208. package/dist/postinstall.js.map +1 -0
  209. package/dist/readiness-probe.d.ts.map +1 -0
  210. package/dist/readiness-probe.js +9 -1
  211. package/dist/readiness-probe.js.map +1 -0
  212. package/dist/regen-format.d.ts.map +1 -0
  213. package/dist/regen-format.js +545 -13
  214. package/dist/regen-format.js.map +1 -0
  215. package/dist/regen.d.ts.map +1 -0
  216. package/dist/regen.js +141 -5
  217. package/dist/regen.js.map +1 -0
  218. package/dist/remote-client.d.ts +65 -175
  219. package/dist/remote-client.d.ts.map +1 -0
  220. package/dist/remote-client.js +942 -1
  221. package/dist/remote-client.js.map +1 -0
  222. package/dist/reset-local-seat-cmd.d.ts +50 -0
  223. package/dist/reset-local-seat-cmd.d.ts.map +1 -0
  224. package/dist/reset-local-seat-cmd.js +169 -0
  225. package/dist/reset-local-seat-cmd.js.map +1 -0
  226. package/dist/role-match.d.ts.map +1 -0
  227. package/dist/role-match.js +23 -1
  228. package/dist/role-match.js.map +1 -0
  229. package/dist/role-resolver.d.ts.map +1 -0
  230. package/dist/role-resolver.js +60 -1
  231. package/dist/role-resolver.js.map +1 -0
  232. package/dist/roster-render.d.ts.map +1 -0
  233. package/dist/roster-render.js +121 -3
  234. package/dist/roster-render.js.map +1 -0
  235. package/dist/seat-probe.d.ts +32 -13
  236. package/dist/seat-probe.d.ts.map +1 -0
  237. package/dist/seat-probe.js +78 -1
  238. package/dist/seat-probe.js.map +1 -0
  239. package/dist/seat-store.d.ts +76 -0
  240. package/dist/seat-store.d.ts.map +1 -0
  241. package/dist/seat-store.js +309 -0
  242. package/dist/seat-store.js.map +1 -0
  243. package/dist/seats.d.ts +303 -0
  244. package/dist/seats.d.ts.map +1 -0
  245. package/dist/seats.js +551 -0
  246. package/dist/seats.js.map +1 -0
  247. package/dist/self-path.d.ts +27 -0
  248. package/dist/self-path.d.ts.map +1 -0
  249. package/dist/self-path.js +43 -0
  250. package/dist/self-path.js.map +1 -0
  251. package/dist/server-endpoint.d.ts +9 -0
  252. package/dist/server-endpoint.d.ts.map +1 -0
  253. package/dist/server-endpoint.js +38 -0
  254. package/dist/server-endpoint.js.map +1 -0
  255. package/dist/server-errors.d.ts +50 -0
  256. package/dist/server-errors.d.ts.map +1 -0
  257. package/dist/server-errors.js +79 -0
  258. package/dist/server-errors.js.map +1 -0
  259. package/dist/server-handshake.d.ts +197 -0
  260. package/dist/server-handshake.d.ts.map +1 -0
  261. package/dist/server-handshake.js +505 -0
  262. package/dist/server-handshake.js.map +1 -0
  263. package/dist/server-response.d.ts +3 -0
  264. package/dist/server-response.d.ts.map +1 -0
  265. package/dist/server-response.js +67 -0
  266. package/dist/server-response.js.map +1 -0
  267. package/dist/server-trust.d.ts +14 -0
  268. package/dist/server-trust.d.ts.map +1 -0
  269. package/dist/server-trust.js +225 -0
  270. package/dist/server-trust.js.map +1 -0
  271. package/dist/setup-confirm.d.ts +3 -3
  272. package/dist/setup-confirm.d.ts.map +1 -0
  273. package/dist/setup-confirm.js +126 -2
  274. package/dist/setup-confirm.js.map +1 -0
  275. package/dist/setup.d.ts +5 -3
  276. package/dist/setup.d.ts.map +1 -0
  277. package/dist/setup.js +169 -46
  278. package/dist/setup.js.map +1 -0
  279. package/dist/shell-escape.d.ts.map +1 -0
  280. package/dist/shell-escape.js +22 -1
  281. package/dist/shell-escape.js.map +1 -0
  282. package/dist/spawn.d.ts +2 -2
  283. package/dist/spawn.d.ts.map +1 -0
  284. package/dist/spawn.js +29 -10
  285. package/dist/spawn.js.map +1 -0
  286. package/dist/startup-services.d.ts +0 -1
  287. package/dist/startup-services.d.ts.map +1 -0
  288. package/dist/startup-services.js +26 -1
  289. package/dist/startup-services.js.map +1 -0
  290. package/dist/stream-owner.d.ts +6 -2
  291. package/dist/stream-owner.d.ts.map +1 -0
  292. package/dist/stream-owner.js +547 -2
  293. package/dist/stream-owner.js.map +1 -0
  294. package/dist/stream-status.d.ts +1 -1
  295. package/dist/stream-status.d.ts.map +1 -0
  296. package/dist/stream-status.js +268 -3
  297. package/dist/stream-status.js.map +1 -0
  298. package/dist/sync-roles-render.d.ts.map +1 -0
  299. package/dist/sync-roles-render.js +118 -3
  300. package/dist/sync-roles-render.js.map +1 -0
  301. package/dist/sync.d.ts +1 -1
  302. package/dist/sync.d.ts.map +1 -0
  303. package/dist/sync.js +286 -22
  304. package/dist/sync.js.map +1 -0
  305. package/dist/terminal-title.d.ts.map +1 -0
  306. package/dist/terminal-title.js +68 -1
  307. package/dist/terminal-title.js.map +1 -0
  308. package/dist/token-store.d.ts +17 -111
  309. package/dist/token-store.d.ts.map +1 -0
  310. package/dist/token-store.js +81 -1
  311. package/dist/token-store.js.map +1 -0
  312. package/dist/tool-manifest.d.ts +4 -8
  313. package/dist/tool-manifest.d.ts.map +1 -0
  314. package/dist/tool-manifest.js +554 -1
  315. package/dist/tool-manifest.js.map +1 -0
  316. package/dist/tool-scope.d.ts +8 -12
  317. package/dist/tool-scope.d.ts.map +1 -0
  318. package/dist/tool-scope.js +97 -1
  319. package/dist/tool-scope.js.map +1 -0
  320. package/dist/types.d.ts +1 -5
  321. package/dist/types.d.ts.map +1 -0
  322. package/dist/types.js +5 -0
  323. package/dist/types.js.map +1 -0
  324. package/dist/unknown-subcommand.d.ts +1 -1
  325. package/dist/unknown-subcommand.d.ts.map +1 -0
  326. package/dist/unknown-subcommand.js +37 -1
  327. package/dist/unknown-subcommand.js.map +1 -0
  328. package/dist/version.d.ts +1 -1
  329. package/dist/version.d.ts.map +1 -0
  330. package/dist/version.js +78 -2
  331. package/dist/version.js.map +1 -0
  332. package/dist/working-repo.d.ts.map +1 -0
  333. package/dist/working-repo.js +81 -1
  334. package/dist/working-repo.js.map +1 -0
  335. package/dist/worktree-lifecycle.d.ts.map +1 -0
  336. package/dist/worktree-lifecycle.js +205 -2
  337. package/dist/worktree-lifecycle.js.map +1 -0
  338. package/docs/EXTRACTION_PROVENANCE.md +40 -0
  339. package/docs/LOCAL_SERVER.md +103 -0
  340. package/docs/RELEASING.md +120 -0
  341. package/package.json +52 -34
  342. package/src/agent-runtime.ts +69 -0
  343. package/src/assimilate-cmd.ts +1864 -0
  344. package/src/assimilate-deps.ts +444 -0
  345. package/src/assimilate-guard.ts +84 -0
  346. package/src/assimilate-welcome.ts +39 -0
  347. package/src/auth-env.ts +21 -0
  348. package/src/backends/launch-all-pastelist.ts +18 -0
  349. package/src/backends/launch-all-tmux.ts +75 -0
  350. package/src/backends/launch-all-windows.ts +110 -0
  351. package/src/bare-launch-menu.ts +148 -0
  352. package/src/claude-launch-args.ts +35 -0
  353. package/src/claude.ts +561 -0
  354. package/src/cleanup-cmd.ts +581 -0
  355. package/src/clear-rewake-core.ts +34 -0
  356. package/src/clear-rewake.ts +18 -0
  357. package/src/cli-help.ts +116 -0
  358. package/src/cli-platform.ts +127 -0
  359. package/src/cli-tool-approval.ts +716 -0
  360. package/src/codex-app-server.ts +238 -0
  361. package/src/codex-app-wake.ts +512 -0
  362. package/src/codex-launch.ts +140 -0
  363. package/src/codex-remote.ts +500 -0
  364. package/src/codex-wake-resolve.ts +155 -0
  365. package/src/config-utils.ts +811 -0
  366. package/src/config.ts +595 -0
  367. package/src/console-prefix.ts +99 -0
  368. package/src/cube-name.ts +69 -0
  369. package/src/cubes.ts +583 -0
  370. package/src/debug.ts +73 -0
  371. package/src/direct-log.ts +9 -0
  372. package/src/docs-sections.ts +129 -0
  373. package/src/drone-lifecycle.ts +62 -0
  374. package/src/drone-management.ts +162 -0
  375. package/src/ensure-mcp-config.ts +54 -0
  376. package/src/evict-drone.ts +82 -0
  377. package/src/gc-orphan-inboxes.ts +229 -0
  378. package/src/inbox-monitor.ts +1005 -0
  379. package/src/index.ts +1201 -0
  380. package/src/launch-all-cmd.ts +450 -0
  381. package/src/launch-all-command.ts +30 -0
  382. package/src/launch-all-deps.ts +188 -0
  383. package/src/launch-all-discovery.ts +118 -0
  384. package/src/launch-all-locks.ts +74 -0
  385. package/src/launch-gate.ts +80 -0
  386. package/src/lifecycle-log-guard.ts +144 -0
  387. package/src/list-roles-render.ts +52 -0
  388. package/src/local-log-routing.ts +106 -0
  389. package/src/local-manage-tool-result.ts +10 -0
  390. package/src/local-server-cursor.ts +188 -0
  391. package/src/log-audit.ts +207 -0
  392. package/src/log-stream.ts +1505 -0
  393. package/src/model-presets.ts +22 -0
  394. package/src/name-validator.ts +23 -0
  395. package/src/opencode-drone.ts +528 -0
  396. package/src/opencode-plugin.ts +38 -0
  397. package/src/parse-assimilate-args.ts +125 -0
  398. package/src/parse-launch-all-args.ts +100 -0
  399. package/src/postinstall-banner.ts +37 -0
  400. package/src/postinstall.ts +36 -0
  401. package/src/readiness-probe.ts +12 -0
  402. package/src/regen-format.ts +651 -0
  403. package/src/regen.ts +150 -0
  404. package/src/remote-client.ts +1379 -0
  405. package/src/reset-local-seat-cmd.ts +227 -0
  406. package/src/role-match.ts +29 -0
  407. package/src/role-resolver.ts +93 -0
  408. package/src/roster-render.ts +196 -0
  409. package/src/seat-probe.ts +115 -0
  410. package/src/seat-store.ts +333 -0
  411. package/src/seats.ts +741 -0
  412. package/src/self-path.ts +51 -0
  413. package/src/server-endpoint.ts +38 -0
  414. package/src/server-errors.ts +93 -0
  415. package/src/server-handshake.ts +783 -0
  416. package/src/server-response.ts +76 -0
  417. package/src/server-trust.ts +245 -0
  418. package/src/setup-confirm.ts +171 -0
  419. package/src/setup.ts +186 -0
  420. package/src/shell-escape.ts +22 -0
  421. package/src/spawn.ts +31 -0
  422. package/src/startup-services.ts +36 -0
  423. package/src/stream-owner.ts +686 -0
  424. package/src/stream-status.ts +337 -0
  425. package/src/sync-roles-render.ts +163 -0
  426. package/src/sync.ts +363 -0
  427. package/src/terminal-title.ts +76 -0
  428. package/src/token-store.ts +102 -0
  429. package/src/tool-manifest.ts +610 -0
  430. package/src/tool-scope.ts +110 -0
  431. package/src/types.ts +5 -0
  432. package/src/unknown-subcommand.ts +35 -0
  433. package/src/version.ts +82 -0
  434. package/src/working-repo.ts +99 -0
  435. package/src/worktree-lifecycle.ts +254 -0
  436. package/dist/auth-recovery.d.ts +0 -33
  437. package/dist/auth-recovery.js +0 -1
  438. package/dist/auth.d.ts +0 -103
  439. package/dist/auth.js +0 -39
  440. package/dist/device-auth.d.ts +0 -75
  441. package/dist/device-auth.js +0 -1
  442. package/dist/drone-address.d.ts +0 -26
  443. package/dist/drone-address.js +0 -1
  444. package/dist/get-started.d.ts +0 -20
  445. package/dist/get-started.js +0 -2
  446. package/dist/health-beat.d.ts +0 -134
  447. package/dist/health-beat.js +0 -1
  448. package/dist/role-section.d.ts +0 -110
  449. package/dist/role-section.js +0 -8
  450. package/dist/setup-action.d.ts +0 -20
  451. package/dist/setup-action.js +0 -1
  452. package/dist/stale-version-check.d.ts +0 -40
  453. package/dist/stale-version-check.js +0 -1
  454. package/dist/subscription-retry.d.ts +0 -40
  455. package/dist/subscription-retry.js +0 -1
  456. package/dist/templates.d.ts +0 -107
  457. package/dist/templates.js +0 -359
  458. package/dist/token-crypto.d.ts +0 -50
  459. package/dist/token-crypto.js +0 -1
@@ -0,0 +1,811 @@
1
+ /**
2
+ * MCP Settings Configuration Utilities
3
+ *
4
+ * Handles adding borg-mcp to Claude Code via the claude CLI
5
+ */
6
+
7
+ import { execSync } from 'child_process';
8
+ import fs from 'fs';
9
+ import os from 'os';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { dirname } from 'path';
13
+ import {
14
+ BORG_AGENT_KIND_ENV,
15
+ BORG_CODEX_REMOTE_WAKE_ENV,
16
+ withAgentRuntimeEnv,
17
+ } from './agent-runtime.js';
18
+ import {
19
+ resolveMcpBinaryPath,
20
+ resolveRegenPath,
21
+ resolveClearRewakePath,
22
+ resolveLogAuditPath,
23
+ } from './self-path.js';
24
+ import { shellEscape } from './shell-escape.js';
25
+
26
+ // Get __dirname equivalent in ESM
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = dirname(__filename);
29
+
30
+ // gh#client#18: canonical hook commands stored in JSON are shell-escaped
31
+ // absolute paths. Bare names and stale/other-install absolute paths are
32
+ // migrated to this form on every hook write.
33
+ const HOOK_COMMAND = shellEscape(resolveRegenPath());
34
+ const CLEAR_REWAKE_HOOK_COMMAND = shellEscape(resolveClearRewakePath());
35
+ const AUDIT_HOOK_COMMAND = shellEscape(resolveLogAuditPath());
36
+ const MCP_BINARY = resolveMcpBinaryPath();
37
+
38
+ /**
39
+ * Claude Code CLI config path. The CLI reads `mcpServers.<name>` from
40
+ * this file to discover registered MCP servers; `addMcpServer()` (below)
41
+ * writes to it via the `claude mcp add --scope user borg borg-mcp` shell
42
+ * command. Server name is `borg` (not `borgmcp` — `borg-mcp` is the
43
+ * binary that backs it). NOTE: distinct from
44
+ * `~/Library/Application Support/Claude/claude_desktop_config.json`,
45
+ * which is the Claude Desktop app's config (different product).
46
+ */
47
+ const CLAUDE_CONFIG_PATH = path.join(os.homedir(), '.claude.json');
48
+ const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
49
+ const CODEX_HOOKS_PATH = path.join(os.homedir(), '.codex', 'hooks.json');
50
+ const OPENCODE_CONFIG_PATH = path.join(os.homedir(), '.config', 'opencode', 'opencode.json');
51
+ const MCP_SERVER_NAME = 'borg';
52
+
53
+ function settingsPath(): string {
54
+ return path.join(os.homedir(), '.claude', 'settings.json');
55
+ }
56
+
57
+ function readSettings(): any {
58
+ const p = settingsPath();
59
+ if (!fs.existsSync(p)) return {};
60
+ const text = fs.readFileSync(p, 'utf-8');
61
+ if (!text.trim()) return {};
62
+ return JSON.parse(text);
63
+ }
64
+
65
+ function writeSettings(settings: any): void {
66
+ const p = settingsPath();
67
+ fs.mkdirSync(path.dirname(p), { recursive: true });
68
+ fs.writeFileSync(p, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
69
+ }
70
+
71
+ function readJsonFile(p: string): any {
72
+ if (!fs.existsSync(p)) return {};
73
+ const text = fs.readFileSync(p, 'utf-8');
74
+ if (!text.trim()) return {};
75
+ return JSON.parse(text);
76
+ }
77
+
78
+ function writeJsonFile(p: string, data: any): void {
79
+ fs.mkdirSync(path.dirname(p), { recursive: true });
80
+ fs.writeFileSync(p, JSON.stringify(data, null, 2) + '\n', 'utf-8');
81
+ }
82
+
83
+ /**
84
+ * Register a Claude Code SessionStart hook that runs `borg-regen` at the
85
+ * start of every session. Idempotent: re-running won't add duplicates.
86
+ *
87
+ * Returns true if a change was made, false otherwise (already present, or
88
+ * settings.json could not be parsed).
89
+ */
90
+ export function addSessionStartHook(): boolean {
91
+ return addSessionStartHookAt(settingsPath());
92
+ }
93
+
94
+ /**
95
+ * gh#673 P2 (WI-1): install the borg-regen SessionStart hook into a
96
+ * PROJECT-LOCAL `<projectRoot>/.claude/settings.local.json` instead of
97
+ * the global ~/.claude/settings.json. The .local variant is
98
+ * user-authored and uncommitted, so it neither imposes borg-regen on
99
+ * collaborators nor trips committed-project-hook trust prompts
100
+ * (V1-probed: Claude Code merges + fires hooks from it). Written by
101
+ * `borg assimilate` (incl. into freshly spawned sibling worktrees) and
102
+ * ensured on every bare `borg` launch. Idempotent; preserves unrelated
103
+ * settings content; refuses to clobber an unparseable file.
104
+ */
105
+ export function addProjectSessionStartHook(projectRoot: string): boolean {
106
+ return addSessionStartHookAt(projectSettingsPath(projectRoot), true);
107
+ }
108
+
109
+ /** Peek variant of addProjectSessionStartHook — no mutation. */
110
+ export function isProjectSessionStartHookRegistered(projectRoot: string): boolean {
111
+ return sessionStartHookRegisteredAt(projectSettingsPath(projectRoot), true);
112
+ }
113
+
114
+ function projectSettingsPath(projectRoot: string): string {
115
+ return path.join(projectRoot, '.claude', 'settings.local.json');
116
+ }
117
+
118
+ function addSessionStartHookAt(settingsFile: string, includeClearRewake = false): boolean {
119
+ let settings: any;
120
+ try {
121
+ settings = readJsonFile(settingsFile);
122
+ } catch (err: any) {
123
+ console.error(`⚠ Could not parse ${settingsFile}: ${err.message}. Skipping hook registration; you can add it manually.`);
124
+ return false;
125
+ }
126
+
127
+ settings.hooks ??= {};
128
+ settings.hooks.SessionStart ??= [];
129
+
130
+ let changed = false;
131
+
132
+ // gh#client#18: migrate owned hooks to canonical form and deduplicate
133
+ // (exactly one canonical hook per owned command, preserving unrelated
134
+ // siblings and entry metadata).
135
+ changed = migrateAndDedupOwnedHooks(settings.hooks.SessionStart) || changed;
136
+
137
+ if (!hasCommandHook(settings.hooks.SessionStart, HOOK_COMMAND)) {
138
+ settings.hooks.SessionStart.push({
139
+ matcher: '*',
140
+ hooks: [{ type: 'command', command: HOOK_COMMAND }],
141
+ });
142
+ changed = true;
143
+ }
144
+
145
+ // Claude Code's normal orientation hook is intentionally left intact. A
146
+ // second project-local handler matches only `/clear` and uses asyncRewake +
147
+ // exit 2 to force one recovery turn when the otherwise-quiet SessionStart
148
+ // event would leave the interactive session idle.
149
+ if (includeClearRewake) {
150
+ const normalized = normalizeClearRewakeHook(settings.hooks.SessionStart);
151
+ if (normalized) {
152
+ settings.hooks.SessionStart = normalized;
153
+ changed = true;
154
+ }
155
+ }
156
+
157
+ if (!changed) return false;
158
+
159
+ writeJsonFile(settingsFile, settings);
160
+ return true;
161
+ }
162
+
163
+ // gh#client#18: match bare names (old configs), stale absolute paths (other
164
+ // installations), shell-escaped canonical paths, and unescaped canonical paths.
165
+ function commandMatches(entryCommand: string, bareName: string, absolutePath: string): boolean {
166
+ const escaped = shellEscape(absolutePath);
167
+ if (entryCommand === escaped || entryCommand === absolutePath || entryCommand === bareName) return true;
168
+ // gh#client#18: stale prior-install absolute paths (e.g.
169
+ // /old/.../dist/regen.js) end with the same basename. Only match
170
+ // when the command IS an absolute path ending with the bare name to
171
+ // avoid false positives on unrelated commands like "run regen.js".
172
+ if (entryCommand.startsWith('/') && entryCommand.endsWith(`/${bareName}`)) return true;
173
+ return false;
174
+ }
175
+
176
+ /** gh#client#18: map an owned hook command to its canonical shell-escaped form.
177
+ * Returns null for non-owned commands. Ownership requires EXACTLY ONE of:
178
+ * (a) Exact bare bin name (borg-regen, borg-clear-rewake, borg-log-audit)
179
+ * (b) Exact match (raw or shell-escaped) of THIS installation's canonical
180
+ * absolute command — always owned, no marker required (fixes neutral-path
181
+ * false negatives that broke idempotency)
182
+ * (c) Foreign-install heuristic: absolute path ending in an owned basename
183
+ * AND containing a borg package marker (borgmcp|borg-mcp) in the path
184
+ * This prevents false-positive ownership of unrelated scripts that happen
185
+ * to share a basename (e.g. /opt/custom-tool/regen.js). */
186
+ function ownedCanonical(command: string): string | null {
187
+ const stripped = command.replace(/^'|'$/g, '');
188
+
189
+ // (a) Exact bare name — always owned
190
+ if (stripped === BARE_BORG_REGEN) return HOOK_COMMAND;
191
+ if (stripped === BARE_CLEAR_REWAKE) return CLEAR_REWAKE_HOOK_COMMAND;
192
+ if (stripped === BARE_LOG_AUDIT) return AUDIT_HOOK_COMMAND;
193
+
194
+ // (b) Exact match of THIS installation's canonical command (raw or escaped)
195
+ // — always owned, no marker check required
196
+ if (command === HOOK_COMMAND || stripped === resolveRegenPath()) return HOOK_COMMAND;
197
+ if (command === CLEAR_REWAKE_HOOK_COMMAND || stripped === resolveClearRewakePath()) return CLEAR_REWAKE_HOOK_COMMAND;
198
+ if (command === AUDIT_HOOK_COMMAND || stripped === resolveLogAuditPath()) return AUDIT_HOOK_COMMAND;
199
+
200
+ // (c) Foreign-install heuristic: absolute path + owned basename + borg marker
201
+ if (stripped.startsWith('/') && (stripped.includes('borgmcp') || stripped.includes('borg-mcp'))) {
202
+ const name = stripped.split('/').pop() ?? '';
203
+ if (name === 'regen.js') return HOOK_COMMAND;
204
+ if (name === 'clear-rewake.js') return CLEAR_REWAKE_HOOK_COMMAND;
205
+ if (name === 'log-audit.js') return AUDIT_HOOK_COMMAND;
206
+ }
207
+
208
+ return null;
209
+ }
210
+
211
+ /**
212
+ * gh#client#18: migrate owned hooks to canonical form and deduplicate:
213
+ * exactly one canonical hook per owned command. Removes only duplicate owned
214
+ * hook objects within entries, preserving unrelated siblings and entry metadata.
215
+ * Mutates the entries array in place. Returns true if any change was made.
216
+ */
217
+ function migrateAndDedupOwnedHooks(entries: any[]): boolean {
218
+ let changed = false;
219
+
220
+ // Phase 1: Migrate owned hooks to canonical form
221
+ for (const entry of entries) {
222
+ if (!Array.isArray(entry?.hooks)) continue;
223
+ for (const hook of entry.hooks) {
224
+ if (hook?.type !== 'command' || typeof hook?.command !== 'string') continue;
225
+ const canonical = ownedCanonical(hook.command);
226
+ if (canonical && hook.command !== canonical) {
227
+ hook.command = canonical;
228
+ changed = true;
229
+ }
230
+ }
231
+ }
232
+
233
+ // Phase 2: Deduplicate — for each canonical command, keep exactly one hook
234
+ // object across all entries. Remove only the duplicate hook objects, not
235
+ // entire entries (preserving unrelated siblings and entry metadata).
236
+ const seenCanonicals = new Set<string>();
237
+ for (const entry of entries) {
238
+ if (!Array.isArray(entry?.hooks)) continue;
239
+ const before = entry.hooks.length;
240
+ entry.hooks = entry.hooks.filter((hook: any) => {
241
+ if (hook?.type !== 'command' || typeof hook?.command !== 'string') return true;
242
+ const canonical = ownedCanonical(hook.command);
243
+ if (!canonical) return true;
244
+ if (seenCanonicals.has(canonical)) {
245
+ changed = true;
246
+ return false;
247
+ }
248
+ seenCanonicals.add(canonical);
249
+ return true;
250
+ });
251
+ if (entry.hooks.length !== before) changed = true;
252
+ }
253
+
254
+ // Phase 3: Remove entries that became empty after dedup
255
+ for (let i = entries.length - 1; i >= 0; i--) {
256
+ if (!Array.isArray(entries[i]?.hooks) || entries[i].hooks.length === 0) {
257
+ entries.splice(i, 1);
258
+ changed = true;
259
+ }
260
+ }
261
+
262
+ return changed;
263
+ }
264
+
265
+ /** Strict canonical match: only the shell-escaped canonical form.
266
+ * gh#client#18: raw unescaped paths are NOT canonical — a path with spaces
267
+ * or metacharacters would break at shell-fire time if not escaped. */
268
+ function isCanonicalCommand(entryCommand: string, canonical: string): boolean {
269
+ return entryCommand === canonical;
270
+ }
271
+
272
+ const BARE_BORG_REGEN = 'borg-regen';
273
+ const BARE_CLEAR_REWAKE = 'borg-clear-rewake';
274
+ const BARE_LOG_AUDIT = 'borg-log-audit';
275
+
276
+ function hasCommandHook(entries: any[], command: string): boolean {
277
+ return entries.some((entry: any) =>
278
+ Array.isArray(entry?.hooks) &&
279
+ entry.hooks.some((h: any) => {
280
+ if (h?.type !== 'command' || typeof h?.command !== 'string') return false;
281
+ // gh#client#18: for known owned commands, check if this hook's command
282
+ // maps to the SAME canonical as the target. This correctly distinguishes
283
+ // regen from clear-rewake from audit — a clear-rewake hook does NOT
284
+ // satisfy the dedup check for a regen target, and vice versa.
285
+ if (command === HOOK_COMMAND || command === CLEAR_REWAKE_HOOK_COMMAND || command === AUDIT_HOOK_COMMAND) {
286
+ return ownedCanonical(h.command) === command;
287
+ }
288
+ return h.command === command;
289
+ })
290
+ );
291
+ }
292
+
293
+ /** Strict: only the shell-escaped canonical form (no bare-name fallback). */
294
+ function hasCanonicalCommandHook(entries: any[], command: string): boolean {
295
+ return entries.some((entry: any) =>
296
+ Array.isArray(entry?.hooks) &&
297
+ entry.hooks.some((h: any) => {
298
+ if (h?.type !== 'command' || typeof h?.command !== 'string') return false;
299
+ return isCanonicalCommand(h.command, command);
300
+ })
301
+ );
302
+ }
303
+
304
+ function isClearRewakeCommand(hook: any): boolean {
305
+ if (hook?.type !== 'command' || typeof hook?.command !== 'string') return false;
306
+ return commandMatches(hook.command, BARE_CLEAR_REWAKE, resolveClearRewakePath());
307
+ }
308
+
309
+ function isCanonicalClearRewakeEntry(entry: any): boolean {
310
+ return entry?.matcher === 'clear' &&
311
+ Array.isArray(entry?.hooks) &&
312
+ entry.hooks.length === 1 &&
313
+ isCanonicalCommand(entry.hooks[0].command, CLEAR_REWAKE_HOOK_COMMAND) &&
314
+ entry.hooks[0].asyncRewake === true;
315
+ }
316
+
317
+ /**
318
+ * Ensure one dedicated clear-only async-rewake handler. If a user has a
319
+ * malformed or duplicate entry for our command, remove only that owned
320
+ * command, preserve any unrelated sibling hooks, and append the canonical
321
+ * entry once.
322
+ */
323
+ function normalizeClearRewakeHook(entries: any[]): any[] | null {
324
+ let migrated = false;
325
+ const result = entries.map((entry: any) => {
326
+ if (!Array.isArray(entry?.hooks)) return entry;
327
+ const hooks = entry.hooks.map((hook: any) => {
328
+ if (isClearRewakeCommand(hook) && !isCanonicalCommand(hook.command, CLEAR_REWAKE_HOOK_COMMAND)) {
329
+ migrated = true;
330
+ return { ...hook, command: CLEAR_REWAKE_HOOK_COMMAND };
331
+ }
332
+ return hook;
333
+ });
334
+ return hooks === entry.hooks ? entry : { ...entry, hooks };
335
+ });
336
+
337
+ const commandCount = result.reduce(
338
+ (count, entry) => count + (
339
+ Array.isArray(entry?.hooks) ? entry.hooks.filter(isClearRewakeCommand).length : 0
340
+ ),
341
+ 0
342
+ );
343
+ if (commandCount === 1 && result.some(isCanonicalClearRewakeEntry)) {
344
+ return migrated ? result : null;
345
+ }
346
+
347
+ const withoutOwnedCommand = result.flatMap((entry: any) => {
348
+ if (!Array.isArray(entry?.hooks)) return [entry];
349
+ const hooks = entry.hooks.filter((hook: any) => !isClearRewakeCommand(hook));
350
+ return hooks.length > 0 ? [{ ...entry, hooks }] : [];
351
+ });
352
+ withoutOwnedCommand.push({
353
+ matcher: 'clear',
354
+ hooks: [{ type: 'command', command: CLEAR_REWAKE_HOOK_COMMAND, asyncRewake: true }],
355
+ });
356
+ return withoutOwnedCommand;
357
+ }
358
+
359
+ function sessionStartHookRegisteredAt(settingsFile: string, includeClearRewake = false): boolean {
360
+ let settings: any;
361
+ try {
362
+ settings = readJsonFile(settingsFile);
363
+ } catch {
364
+ return false;
365
+ }
366
+ const arr = settings?.hooks?.SessionStart;
367
+ if (!Array.isArray(arr)) return false;
368
+ return hasCanonicalCommandHook(arr, HOOK_COMMAND) &&
369
+ (!includeClearRewake || normalizeClearRewakeHook(arr) === null);
370
+ }
371
+
372
+ /**
373
+ * Peek whether the borg-regen SessionStart hook is already registered, without
374
+ * mutating settings. Returns false on any read error (safe-default).
375
+ */
376
+ export function isSessionStartHookRegistered(): boolean {
377
+ let settings: any;
378
+ try {
379
+ settings = readSettings();
380
+ } catch {
381
+ return false;
382
+ }
383
+ const arr = settings?.hooks?.SessionStart;
384
+ if (!Array.isArray(arr)) return false;
385
+ return arr.some((entry: any) =>
386
+ Array.isArray(entry?.hooks) &&
387
+ entry.hooks.some((h: any) => {
388
+ if (h?.type !== 'command' || typeof h?.command !== 'string') return false;
389
+ return isCanonicalCommand(h.command, HOOK_COMMAND);
390
+ })
391
+ );
392
+ }
393
+
394
+ /**
395
+ * Peek: true iff the Claude UserPromptSubmit audit hook (`borg-log-audit`) is
396
+ * registered. Non-mutating mirror of addUserPromptSubmitHook's idempotency
397
+ * check; used by isClaudeHookConfigPending (gh#844).
398
+ */
399
+ export function isUserPromptSubmitHookRegistered(): boolean {
400
+ let settings: any;
401
+ try {
402
+ settings = readSettings();
403
+ } catch {
404
+ return false;
405
+ }
406
+ const arr = settings?.hooks?.UserPromptSubmit;
407
+ if (!Array.isArray(arr)) return false;
408
+ return arr.some((entry: any) =>
409
+ Array.isArray(entry?.hooks) &&
410
+ entry.hooks.some((h: any) => {
411
+ if (h?.type !== 'command' || typeof h?.command !== 'string') return false;
412
+ return isCanonicalCommand(h.command, AUDIT_HOOK_COMMAND);
413
+ })
414
+ );
415
+ }
416
+
417
+ /**
418
+ * Inverse of addSessionStartHook: remove any SessionStart hook entry whose
419
+ * inner hooks array contains a `borg-regen` command. If multiple commands
420
+ * share an entry, only the borg-regen command is removed; otherwise the
421
+ * entire entry is dropped. Empty containers are cleaned up.
422
+ *
423
+ * Returns true if a change was made, false otherwise.
424
+ */
425
+ export function removeSessionStartHook(): boolean {
426
+ let settings: any;
427
+ try {
428
+ settings = readSettings();
429
+ } catch {
430
+ return false;
431
+ }
432
+ if (!settings?.hooks?.SessionStart) return false;
433
+
434
+ let changed = false;
435
+ settings.hooks.SessionStart = settings.hooks.SessionStart
436
+ .map((entry: any) => {
437
+ if (!Array.isArray(entry?.hooks)) return entry;
438
+ const filtered = entry.hooks.filter((h: any) => {
439
+ if (h?.type !== 'command' || typeof h?.command !== 'string') return true;
440
+ return !commandMatches(h.command, BARE_BORG_REGEN, HOOK_COMMAND);
441
+ });
442
+ if (filtered.length !== entry.hooks.length) {
443
+ changed = true;
444
+ return { ...entry, hooks: filtered };
445
+ }
446
+ return entry;
447
+ })
448
+ .filter((entry: any) => Array.isArray(entry?.hooks) && entry.hooks.length > 0);
449
+
450
+ if (settings.hooks.SessionStart.length === 0) {
451
+ delete settings.hooks.SessionStart;
452
+ }
453
+ if (Object.keys(settings.hooks).length === 0) {
454
+ delete settings.hooks;
455
+ }
456
+
457
+ if (changed) writeSettings(settings);
458
+ return changed;
459
+ }
460
+
461
+ /**
462
+ * Register a Claude Code UserPromptSubmit hook that runs `borg-log-audit`
463
+ * before each user prompt. The audit script nudges the drone if the
464
+ * previous assistant span used state-changing tools without calling
465
+ * `borg_log`. Idempotent: re-running won't add duplicates.
466
+ *
467
+ * Returns true if a change was made, false otherwise.
468
+ */
469
+ export function addUserPromptSubmitHook(): boolean {
470
+ let settings: any;
471
+ try {
472
+ settings = readSettings();
473
+ } catch (err: any) {
474
+ console.error(`⚠ Could not parse ${settingsPath()}: ${err.message}. Skipping audit hook registration.`);
475
+ return false;
476
+ }
477
+
478
+ settings.hooks ??= {};
479
+ settings.hooks.UserPromptSubmit ??= [];
480
+
481
+ // gh#client#18: migrate owned hooks to canonical form and deduplicate
482
+ // (exactly one canonical hook per owned command, preserving unrelated
483
+ // siblings and entry metadata).
484
+ let changed = migrateAndDedupOwnedHooks(settings.hooks.UserPromptSubmit);
485
+
486
+ if (!hasCanonicalCommandHook(settings.hooks.UserPromptSubmit, AUDIT_HOOK_COMMAND)) {
487
+ settings.hooks.UserPromptSubmit.push({
488
+ matcher: '*',
489
+ hooks: [{ type: 'command', command: AUDIT_HOOK_COMMAND }],
490
+ });
491
+ changed = true;
492
+ }
493
+
494
+ if (changed) writeSettings(settings);
495
+ return changed;
496
+ }
497
+
498
+ /**
499
+ * Inverse of addUserPromptSubmitHook: remove any UserPromptSubmit hook
500
+ * entry that runs `borg-log-audit`. Symmetric cleanup to
501
+ * removeSessionStartHook.
502
+ */
503
+ export function removeUserPromptSubmitHook(): boolean {
504
+ let settings: any;
505
+ try {
506
+ settings = readSettings();
507
+ } catch {
508
+ return false;
509
+ }
510
+ if (!settings?.hooks?.UserPromptSubmit) return false;
511
+
512
+ let changed = false;
513
+ settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit
514
+ .map((entry: any) => {
515
+ if (!Array.isArray(entry?.hooks)) return entry;
516
+ const filtered = entry.hooks.filter((h: any) => {
517
+ if (h?.type !== 'command' || typeof h?.command !== 'string') return true;
518
+ return !commandMatches(h.command, BARE_LOG_AUDIT, AUDIT_HOOK_COMMAND);
519
+ });
520
+ if (filtered.length !== entry.hooks.length) {
521
+ changed = true;
522
+ return { ...entry, hooks: filtered };
523
+ }
524
+ return entry;
525
+ })
526
+ .filter((entry: any) => Array.isArray(entry?.hooks) && entry.hooks.length > 0);
527
+
528
+ if (settings.hooks.UserPromptSubmit.length === 0) {
529
+ delete settings.hooks.UserPromptSubmit;
530
+ }
531
+ if (Object.keys(settings.hooks).length === 0) {
532
+ delete settings.hooks;
533
+ }
534
+
535
+ if (changed) writeSettings(settings);
536
+ return changed;
537
+ }
538
+
539
+ /**
540
+ * Detect whether the borg MCP server is already registered in the Claude
541
+ * Code CLI config (`~/.claude.json` `mcpServers.borg`).
542
+ *
543
+ * Per gh#79: when a user re-runs `borg setup` (the canonical re-run
544
+ * reason), the setup wizard's "Add borg to Claude Code?"
545
+ * prompt is redundant — the answer is deterministic ("already
546
+ * configured"). This detect lets the wizard silently skip Step 1 entirely
547
+ * when borg is present. Per the dispatch's Queen-implicit anti-scope,
548
+ * "silent means silent" — callers must not log an "already configured"
549
+ * notice when this returns true.
550
+ *
551
+ * Safe-default contract: any read error (file missing, malformed JSON,
552
+ * permission denied, empty file, unexpected shape) returns `false` so
553
+ * the caller still prompts. The dispatch's edge-case framing is "if
554
+ * indeterminate → prompt fires" — never silent-skip when state is
555
+ * ambiguous. The prompt is the safe path; silent-skip is the
556
+ * optimization layered on top of a verified-present signal.
557
+ *
558
+ * @param configPath Override the config-file path; primarily for tests.
559
+ */
560
+ export function isMcpServerConfigured(
561
+ configPath: string = CLAUDE_CONFIG_PATH
562
+ ): boolean {
563
+ try {
564
+ if (!fs.existsSync(configPath)) return false;
565
+ const text = fs.readFileSync(configPath, 'utf-8');
566
+ if (!text.trim()) return false;
567
+ const parsed = JSON.parse(text);
568
+ if (!parsed || typeof parsed !== 'object') return false;
569
+ const servers = (parsed as any).mcpServers;
570
+ if (!servers || typeof servers !== 'object' || Array.isArray(servers)) return false;
571
+ return MCP_SERVER_NAME in servers;
572
+ } catch {
573
+ return false;
574
+ }
575
+ }
576
+
577
+ export function isCodexMcpServerConfigured(
578
+ configPath: string = CODEX_CONFIG_PATH
579
+ ): boolean {
580
+ try {
581
+ if (!fs.existsSync(configPath)) return false;
582
+ const text = fs.readFileSync(configPath, 'utf-8');
583
+ const hasBorgServer = /^\s*\[mcp_servers\.borg\]\s*$/m.test(text);
584
+ const hasPinnedCodexIdentity = new RegExp(
585
+ `^\\s*${BORG_AGENT_KIND_ENV}\\s*=\\s*"codex"\\s*$`,
586
+ 'm'
587
+ ).test(text);
588
+ // gh#968 compatibility: older installations encoded the Codex CLI
589
+ // identity using the remote-wake transport marker. Keep recognizing that
590
+ // static shape so setup does not overwrite a working legacy config merely
591
+ // to migrate its marker.
592
+ const hasLegacyRemoteWakeIdentity = new RegExp(
593
+ `^\\s*${BORG_CODEX_REMOTE_WAKE_ENV}\\s*=\\s*"1"\\s*$`,
594
+ 'm'
595
+ ).test(text);
596
+ return hasBorgServer && (hasPinnedCodexIdentity || hasLegacyRemoteWakeIdentity);
597
+ } catch {
598
+ return false;
599
+ }
600
+ }
601
+
602
+ /**
603
+ * Get absolute path to borg index.js
604
+ * Returns the actual index.js file, not the npm symlink
605
+ */
606
+ export function getBinaryPath(): string {
607
+ // In production: dist/index.js is in the same directory as this file
608
+ // In development: same
609
+ return path.join(__dirname, 'index.js');
610
+ }
611
+
612
+ /**
613
+ * Add borg MCP server to Claude Code using claude CLI
614
+ * First removes any existing borg configuration, then adds fresh one
615
+ * Runs: claude mcp remove --scope user borg && claude mcp add --scope user borg borg-mcp
616
+ */
617
+ export function addMcpServer(): void {
618
+ try {
619
+ // First, remove any existing borg configuration (ignore errors if not found)
620
+ try {
621
+ execSync('claude mcp remove --scope user borg', { stdio: 'ignore' });
622
+ } catch {
623
+ // Ignore - server might not exist yet
624
+ }
625
+
626
+ // gh#client#18: use absolute path to THIS installation's binary so the
627
+ // registered server always matches the running client version.
628
+ const command = `claude mcp add --scope user borg ${shellQuote(MCP_BINARY)}`;
629
+
630
+ execSync(command, {
631
+ stdio: 'inherit', // Show output to user
632
+ // No hosted-URL injection: BORG_API_URL passes through from the
633
+ // environment only when the operator has explicitly set it.
634
+ env: process.env,
635
+ });
636
+ } catch (error: any) {
637
+ if (error.message?.includes('command not found')) {
638
+ throw new Error('Claude CLI not found. Please install Claude Code first.');
639
+ }
640
+ throw new Error(`Failed to add MCP server: ${error.message}`);
641
+ }
642
+ }
643
+
644
+ export function addCodexMcpServer(): void {
645
+ try {
646
+ try {
647
+ execSync('codex mcp remove borg', { stdio: 'ignore' });
648
+ } catch {
649
+ // Ignore - server might not exist yet.
650
+ }
651
+
652
+ // No hosted-URL fallback: only forward BORG_API_URL into the generated
653
+ // Codex MCP config when the operator has explicitly set it.
654
+ const apiUrl = process.env.BORG_API_URL;
655
+ // Identity is durable configuration; remote wake is a per-launch
656
+ // transport capability. Do not persist a transport marker here: a future
657
+ // Codex child may launch without a live --remote socket.
658
+ // gh#client#18: use absolute path to THIS installation's binary.
659
+ const codexConfigEnv = withAgentRuntimeEnv(process.env, 'codex');
660
+ const apiUrlEnvArg = apiUrl ? ` --env BORG_API_URL=${shellQuote(apiUrl)}` : '';
661
+ execSync('codex mcp add borg' +
662
+ apiUrlEnvArg +
663
+ ` --env ${BORG_AGENT_KIND_ENV}=codex` +
664
+ ` -- ${shellQuote(MCP_BINARY)}`, {
665
+ stdio: 'inherit',
666
+ env: codexConfigEnv,
667
+ });
668
+ } catch (error: any) {
669
+ if (error.message?.includes('command not found')) {
670
+ throw new Error('Codex CLI not found. Please install Codex first.');
671
+ }
672
+ throw new Error(`Failed to add MCP server to Codex: ${error.message}`);
673
+ }
674
+ }
675
+
676
+ function shellQuote(value: string): string {
677
+ return `'${value.replace(/'/g, `'\\''`)}'`;
678
+ }
679
+
680
+ function addCodexHook(eventName: 'SessionStart' | 'UserPromptSubmit', command: string, options: { matcher?: string; timeout?: number } = {}): boolean {
681
+ let hooksFile: any;
682
+ try {
683
+ hooksFile = readJsonFile(CODEX_HOOKS_PATH);
684
+ } catch (err: any) {
685
+ console.error(`⚠ Could not parse ${CODEX_HOOKS_PATH}: ${err.message}. Skipping Codex hook registration.`);
686
+ return false;
687
+ }
688
+
689
+ hooksFile.hooks ??= {};
690
+ hooksFile.hooks[eventName] ??= [];
691
+ const entries = hooksFile.hooks[eventName];
692
+ if (!Array.isArray(entries)) return false;
693
+
694
+ // gh#client#18: migrate owned hooks to canonical form and deduplicate
695
+ // (exactly one canonical hook per owned command, preserving unrelated
696
+ // siblings and entry metadata).
697
+ let changed = migrateAndDedupOwnedHooks(entries);
698
+
699
+ if (!hasCanonicalCommandHook(entries, command)) {
700
+ const entry: any = {
701
+ hooks: [{ type: 'command', command }],
702
+ };
703
+ if (options.matcher) entry.matcher = options.matcher;
704
+ if (typeof options.timeout === 'number') entry.hooks[0].timeout = options.timeout;
705
+ entries.push(entry);
706
+ changed = true;
707
+ }
708
+ if (changed) writeJsonFile(CODEX_HOOKS_PATH, hooksFile);
709
+ return changed;
710
+ }
711
+
712
+ export function addCodexSessionStartHook(): boolean {
713
+ return addCodexHook('SessionStart', HOOK_COMMAND, { matcher: 'startup|resume', timeout: 30 });
714
+ }
715
+
716
+ export function addCodexUserPromptSubmitHook(): boolean {
717
+ return addCodexHook('UserPromptSubmit', AUDIT_HOOK_COMMAND, { timeout: 10 });
718
+ }
719
+
720
+ export function isCodexHookRegistered(
721
+ eventName: 'SessionStart' | 'UserPromptSubmit' | 'Stop',
722
+ command: string,
723
+ hooksPath: string = CODEX_HOOKS_PATH
724
+ ): boolean {
725
+ try {
726
+ const parsed = readJsonFile(hooksPath);
727
+ const arr = parsed?.hooks?.[eventName];
728
+ if (!Array.isArray(arr)) return false;
729
+ return arr.some((entry: any) =>
730
+ Array.isArray(entry?.hooks) &&
731
+ entry.hooks.some((h: any) => {
732
+ if (h?.type !== 'command' || typeof h?.command !== 'string') return false;
733
+ if (command === HOOK_COMMAND) return isCanonicalCommand(h.command, command);
734
+ if (command === AUDIT_HOOK_COMMAND) return isCanonicalCommand(h.command, command);
735
+ return h.command === command;
736
+ })
737
+ );
738
+ } catch {
739
+ return false;
740
+ }
741
+ }
742
+
743
+ /**
744
+ * Peek: true iff the Codex SessionStart orientation hook (`borg-regen`) is
745
+ * registered. Non-mutating mirror of addCodexSessionStartHook; used to gate
746
+ * that writer + the gh#844 disclosure on whether it would actually mutate.
747
+ */
748
+ export function isCodexSessionStartHookRegistered(hooksPath: string = CODEX_HOOKS_PATH): boolean {
749
+ return isCodexHookRegistered('SessionStart', HOOK_COMMAND, hooksPath);
750
+ }
751
+
752
+ /**
753
+ * Peek: true iff the Codex UserPromptSubmit audit hook (`borg-log-audit`) is
754
+ * registered. Non-mutating mirror of addCodexUserPromptSubmitHook.
755
+ */
756
+ export function isCodexUserPromptSubmitHookRegistered(hooksPath: string = CODEX_HOOKS_PATH): boolean {
757
+ return isCodexHookRegistered('UserPromptSubmit', AUDIT_HOOK_COMMAND, hooksPath);
758
+ }
759
+
760
+ // ─── OpenCode MCP integration ────────────────────────────────────────────
761
+
762
+ /**
763
+ * Detect whether the borg MCP server is already registered in the opencode
764
+ * config (`~/.config/opencode/opencode.json` `mcp.borg`).
765
+ *
766
+ * Reads the config as JSON and checks for a `mcp.borg` entry with
767
+ * `type: "local"`. Safe-default: any read error returns `false`.
768
+ */
769
+ export function isOpenCodeMcpServerConfigured(
770
+ configPath: string = OPENCODE_CONFIG_PATH
771
+ ): boolean {
772
+ try {
773
+ if (!fs.existsSync(configPath)) return false;
774
+ const text = fs.readFileSync(configPath, 'utf-8');
775
+ if (!text.trim()) return false;
776
+ const parsed = JSON.parse(text);
777
+ if (!parsed || typeof parsed !== 'object') return false;
778
+ const borgServer = (parsed as any).mcp?.borg;
779
+ if (!borgServer || typeof borgServer !== 'object') return false;
780
+ return borgServer.type === 'local';
781
+ } catch {
782
+ return false;
783
+ }
784
+ }
785
+
786
+ /**
787
+ * Add borg MCP server to OpenCode using `opencode mcp add` CLI.
788
+ * Pins BORG_SESSION=1, BORG_AGENT_KIND=opencode, the legacy BORG_OPENCODE=1,
789
+ * and BORG_API_URL in the server environment so the MCP child inherits the
790
+ * activation gate + explicit agent-kind signal (same approach as Codex's
791
+ * pinned env — OpenCode MCP children only see pinned env, not parent process
792
+ * env). Existing configs with BORG_OPENCODE remain supported by the runtime
793
+ * fallback.
794
+ */
795
+ export function addOpenCodeMcpServer(): void {
796
+ try {
797
+ // No hosted-URL fallback: only forward BORG_API_URL when explicitly set.
798
+ const apiUrl = process.env.BORG_API_URL;
799
+ const apiUrlEnvArg = apiUrl ? ` --env BORG_API_URL=${shellQuote(apiUrl)}` : '';
800
+ // gh#client#18: use absolute path to THIS installation's binary.
801
+ execSync(
802
+ `opencode mcp add borg --env BORG_SESSION=1 --env BORG_AGENT_KIND=opencode --env BORG_OPENCODE=1${apiUrlEnvArg} -- ${shellQuote(MCP_BINARY)}`,
803
+ { stdio: 'inherit' }
804
+ );
805
+ } catch (error: any) {
806
+ if (error.message?.includes('command not found')) {
807
+ throw new Error('opencode CLI not found. Please install opencode first.');
808
+ }
809
+ throw new Error(`Failed to add MCP server to opencode: ${error.message}`);
810
+ }
811
+ }