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,69 @@
1
+ import { basename } from 'node:path';
2
+
3
+ const MAX_URL_LEN = 2048;
4
+ const CONTROL_CHAR_RE = /[\x00-\x1F\x7F]/;
5
+
6
+ /**
7
+ * Trim + reject control chars + cap length. Returns null on rejection
8
+ * so callers fall through to no-remote derivation.
9
+ */
10
+ export function sanitizeRemoteUrl(raw: string): string | null {
11
+ const trimmed = raw.trim();
12
+ if (trimmed.length === 0 || trimmed.length > MAX_URL_LEN) return null;
13
+ if (CONTROL_CHAR_RE.test(trimmed)) return null;
14
+ return trimmed;
15
+ }
16
+
17
+ /**
18
+ * Extract the repo name from a git remote URL. Handles SSH/HTTPS/git/file
19
+ * forms and embedded credentials. Returns null when nothing parseable
20
+ * is present.
21
+ *
22
+ * Strategy: strip protocol + credentials, then take the last path segment
23
+ * after the final `/` or `:`, stripping a trailing `.git`.
24
+ */
25
+ export function parseGitRemote(url: string): string | null {
26
+ if (!url) return null;
27
+
28
+ let s = url.replace(/^[a-z]+:\/\//, '');
29
+ s = s.replace(/^[^@\/]*@/, '');
30
+
31
+ const lastSep = Math.max(s.lastIndexOf('/'), s.lastIndexOf(':'));
32
+ if (lastSep === -1) return null;
33
+
34
+ let name = s.slice(lastSep + 1);
35
+ name = name.replace(/\.git$/, '');
36
+ return name.length > 0 ? name : null;
37
+ }
38
+
39
+ /**
40
+ * Normalize an arbitrary string into a valid cube name:
41
+ * lowercase, underscores+spaces → hyphens, strip [^a-z0-9-], truncate 64.
42
+ */
43
+ export function normalizeCubeName(raw: string): string {
44
+ return raw
45
+ .toLowerCase()
46
+ .replace(/[_\s]+/g, '-')
47
+ .replace(/[^a-z0-9-]/g, '')
48
+ .slice(0, 64);
49
+ }
50
+
51
+ /**
52
+ * Compose the full derivation: sanitize + parse + normalize, with
53
+ * project-root basename as fallback. Returns null when no valid name
54
+ * can be derived.
55
+ */
56
+ export function deriveCubeName(projectRoot: string, gitRemoteUrl: string | null): string | null {
57
+ if (gitRemoteUrl) {
58
+ const sanitized = sanitizeRemoteUrl(gitRemoteUrl);
59
+ if (sanitized) {
60
+ const repo = parseGitRemote(sanitized);
61
+ if (repo) {
62
+ const normalized = normalizeCubeName(repo);
63
+ if (normalized.length > 0) return normalized;
64
+ }
65
+ }
66
+ }
67
+ const fallback = normalizeCubeName(basename(projectRoot));
68
+ return fallback.length > 0 ? fallback : null;
69
+ }
package/src/cubes.ts ADDED
@@ -0,0 +1,583 @@
1
+ /**
2
+ * Per-project active-cube persistence for Borg MCP client
3
+ *
4
+ * Stores the currently-assimilated cube identity + authority metadata PER
5
+ * PROJECT in ~/.config/borgmcp/cubes.json. The "project"
6
+ * is identified by walking up from cwd to find a .git directory; if none is
7
+ * found, cwd itself is used as the project key.
8
+ *
9
+ * Local-server session tokens never enter this file: only an opaque keychain
10
+ * reference is stored and hydrated at read time. An entry without verified
11
+ * local-server trust can no longer be hydrated.
12
+ *
13
+ * apiUrl is captured at assimilate time so subprocess invocations (e.g. the
14
+ * SessionStart hook firing borg-regen) don't need BORG_API_URL in their env
15
+ * to know which worker to talk to.
16
+ */
17
+
18
+ import { existsSync } from 'node:fs';
19
+ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
20
+ import { homedir } from 'node:os';
21
+ import { dirname, join, resolve } from 'node:path';
22
+ import { pruneDeadWakeTargets } from './codex-wake-resolve.js';
23
+ import {
24
+ getActiveSeatCredential,
25
+ getActiveSeatForWorktree,
26
+ getSeatForWorktree,
27
+ hasSeatForWorktree,
28
+ observeSeat,
29
+ readAllActiveSeats,
30
+ refreshSeatMetadata,
31
+ resetSeatForWorktree,
32
+ seatRef,
33
+ type SeatObservation,
34
+ type SeatOperation,
35
+ type SeatRecord,
36
+ } from './seats.js';
37
+
38
+ // The unified 0600 seat store (seats.ts / seats.json) is now the SOLE home of the
39
+ // per-worktree ACTIVE-CUBE seat map: each seat = credential + worktree binding +
40
+ // display as ONE atomic record. cubes.json no longer holds a seat map — the
41
+ // functions below are thin adapters over seats.ts. Only launch.json,
42
+ // codex-wake-targets.json, and the inbox tree remain owned by this module.
43
+
44
+ /** Re-exported from seats.ts for call-site parity (the retired cross-store name). */
45
+ export type { SeatExpectation as ExpectedBinding } from './seats.js';
46
+
47
+ const CUBES_DIR = join(homedir(), '.config', 'borgmcp');
48
+ const LAUNCH_FILE = join(CUBES_DIR, 'launch.json');
49
+ const CODEX_WAKE_TARGETS_FILE = join(CUBES_DIR, 'codex-wake-targets.json');
50
+ const INBOX_DIR = join(CUBES_DIR, 'inboxes');
51
+
52
+ export type BorgCli = 'claude' | 'codex' | 'opencode';
53
+
54
+ export interface ActiveCube {
55
+ cubeId: string;
56
+ droneId: string;
57
+ name: string;
58
+ sessionToken: string;
59
+ droneLabel: string;
60
+ apiUrl: string;
61
+ /** Verified local-server CA identity; absent until a local server is selected. */
62
+ serverTrustIdentity?: string;
63
+ /** Opaque local-session keychain reference; never a bearer. */
64
+ localSessionCredentialRef?: string;
65
+ localSessionExpiresAt?: string | null;
66
+ // gh#899: the assimilated role, persisted so the connect-time ListTools
67
+ // handler can role-scope the NATIVE tool surface (UX/context only — never an
68
+ // auth boundary). Absent on pre-gh#899 cubes.json entries → the filter
69
+ // defaults to the FULL set (no capability hidden). Regen and a successful
70
+ // self-reassignment refresh this display metadata without changing seat identity.
71
+ roleName?: string;
72
+ roleClass?: 'queen' | 'worker';
73
+ isHumanSeat?: boolean;
74
+ }
75
+
76
+ export type ActiveCubeInput = Omit<ActiveCube, 'sessionToken'> & {
77
+ sessionToken?: string;
78
+ };
79
+
80
+ interface LaunchFile {
81
+ projects: Record<string, { cli: BorgCli }>;
82
+ }
83
+
84
+ export interface CodexWakeTargetRecord {
85
+ threadId: string;
86
+ socketPath: string;
87
+ updatedAt: string;
88
+ }
89
+
90
+ interface CodexWakeTargetsFile {
91
+ targets: Record<string, CodexWakeTargetRecord>;
92
+ }
93
+
94
+ /**
95
+ * Walk up from cwd looking for a .git directory. If found, return that
96
+ * directory. If not found by filesystem root, return the original cwd.
97
+ * The returned absolute path is the "project key" used to scope cube state.
98
+ */
99
+ export function findProjectRoot(cwd: string = process.cwd()): string {
100
+ let dir = resolve(cwd);
101
+ while (true) {
102
+ if (existsSync(join(dir, '.git'))) return dir;
103
+ const parent = dirname(dir);
104
+ if (parent === dir) return resolve(cwd); // hit root, fall back to cwd
105
+ dir = parent;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Per-(cube, drone) inbox file path. Each drone gets its own file so that
111
+ * multiple drones in the same cube don't trample each other's writes when
112
+ * they each receive the same long-poll batch. The file lives under a
113
+ * per-cube subdir keyed by cube ID, then by drone ID (a UUID, globally
114
+ * unique).
115
+ *
116
+ * Validates cubeId/droneId as UUIDs before using them in a filesystem
117
+ * path. The values come from cubes.json (populated from server response),
118
+ * so the input is trusted in normal operation — but a regex guard is
119
+ * cheap defense against a corrupted file or future bug that would
120
+ * otherwise let `../` slip through into the inbox path.
121
+ */
122
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
123
+
124
+ export function inboxPathForDrone(cubeId: string, droneId: string): string {
125
+ if (!UUID_RE.test(cubeId)) throw new Error(`Invalid cubeId: ${cubeId}`);
126
+ if (!UUID_RE.test(droneId)) throw new Error(`Invalid droneId: ${droneId}`);
127
+ return join(INBOX_DIR, cubeId, `${droneId}.log`);
128
+ }
129
+
130
+ // gh#894: crash-safe write. A plain truncate-write (open-for-write then write)
131
+ // leaves a truncated/empty prefs file if the process dies mid-write — losing
132
+ // every stored cube preference. Instead write to a same-directory temp file
133
+ // and rename() it over the target: rename is atomic on POSIX, so a reader
134
+ // always sees either the old complete file or the new complete file, never a
135
+ // partial one. On any failure the original is untouched (we never opened it for
136
+ // write) and the temp is best-effort cleaned. `io` is injectable for tests.
137
+ let atomicTmpCounter = 0;
138
+ export async function atomicWriteFile(
139
+ filePath: string,
140
+ data: string,
141
+ opts: {
142
+ mode?: number;
143
+ io?: {
144
+ writeFile: typeof writeFile;
145
+ rename: typeof rename;
146
+ unlink: typeof unlink;
147
+ };
148
+ } = {}
149
+ ): Promise<void> {
150
+ const io = opts.io ?? { writeFile, rename, unlink };
151
+ const mode = opts.mode ?? 0o600;
152
+ await mkdir(dirname(filePath), { recursive: true });
153
+ // Same-dir temp so rename() stays on one filesystem (atomicity requirement).
154
+ // pid + counter keeps concurrent same-process writes from colliding.
155
+ const tmp = `${filePath}.${process.pid}.${atomicTmpCounter++}.tmp`;
156
+ try {
157
+ await io.writeFile(tmp, data, { mode });
158
+ await io.rename(tmp, filePath);
159
+ } catch (err) {
160
+ try {
161
+ await io.unlink(tmp);
162
+ } catch {
163
+ /* best-effort temp cleanup; never mask the original error */
164
+ }
165
+ throw err;
166
+ }
167
+ }
168
+
169
+ function isLaunchFile(data: any): data is LaunchFile {
170
+ return (
171
+ data !== null &&
172
+ typeof data === 'object' &&
173
+ typeof data.projects === 'object' &&
174
+ data.projects !== null &&
175
+ !Array.isArray(data.projects)
176
+ );
177
+ }
178
+
179
+ async function readLaunchFile(): Promise<LaunchFile | null> {
180
+ let raw: string;
181
+ try {
182
+ raw = await readFile(LAUNCH_FILE, 'utf8');
183
+ } catch (error: any) {
184
+ if (error?.code === 'ENOENT') return null;
185
+ throw error;
186
+ }
187
+ try {
188
+ const parsed = JSON.parse(raw);
189
+ return isLaunchFile(parsed) ? parsed : null;
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ async function writeLaunchFile(data: LaunchFile): Promise<void> {
196
+ // atomicWriteFile handles the mkdir + 0o600 mode, and the temp+rename keeps
197
+ // a concurrent reader from seeing a half-written launch file (gh#894, gh#901).
198
+ await atomicWriteFile(LAUNCH_FILE, JSON.stringify(data, null, 2) + '\n');
199
+ }
200
+
201
+ function codexWakeTargetKey(cubeId: string, droneId: string): string {
202
+ if (!UUID_RE.test(cubeId)) throw new Error(`Invalid cubeId: ${cubeId}`);
203
+ if (!UUID_RE.test(droneId)) throw new Error(`Invalid droneId: ${droneId}`);
204
+ return `${cubeId}:${droneId}`;
205
+ }
206
+
207
+ function isCodexWakeTargetsFile(data: any): data is CodexWakeTargetsFile {
208
+ return (
209
+ data !== null &&
210
+ typeof data === 'object' &&
211
+ typeof data.targets === 'object' &&
212
+ data.targets !== null &&
213
+ !Array.isArray(data.targets)
214
+ );
215
+ }
216
+
217
+ async function readCodexWakeTargetsFile(): Promise<CodexWakeTargetsFile | null> {
218
+ let raw: string;
219
+ try {
220
+ raw = await readFile(CODEX_WAKE_TARGETS_FILE, 'utf8');
221
+ } catch (error: any) {
222
+ if (error?.code === 'ENOENT') return null;
223
+ throw error;
224
+ }
225
+ try {
226
+ const parsed = JSON.parse(raw);
227
+ return isCodexWakeTargetsFile(parsed) ? parsed : null;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+
233
+ async function writeCodexWakeTargetsFile(data: CodexWakeTargetsFile): Promise<void> {
234
+ // atomicWriteFile handles the mkdir + 0o600 mode, and the temp+rename keeps
235
+ // a concurrent reader from seeing a half-written file (gh#894, gh#901).
236
+ await atomicWriteFile(CODEX_WAKE_TARGETS_FILE, JSON.stringify(data, null, 2) + '\n');
237
+ }
238
+
239
+ /**
240
+ * Get the currently-active cube for the current project, or null if not
241
+ * assimilated in this project. Entries written by older client versions
242
+ * that lack the `cubeId` field are treated as absent — re-assimilate to
243
+ * refresh.
244
+ */
245
+ export async function getActiveCube(): Promise<ActiveCube | null> {
246
+ const record = await getActiveSeatForWorktree(findProjectRoot());
247
+ if (!record || !record.cubeId || !record.droneId) return null;
248
+ return hydrateActiveCube(record);
249
+ }
250
+
251
+ /**
252
+ * True iff this worktree has an ACTIVE bound seat in seats.json. In the collapsed
253
+ * single-store model the credential and the worktree binding are one atomic unit,
254
+ * so there is no "binding present but credential lost" partial state to diagnose:
255
+ * an active bound seat always hydrates.
256
+ */
257
+ export async function hasPersistedActiveCube(): Promise<boolean> {
258
+ return hasSeatForWorktree(findProjectRoot());
259
+ }
260
+
261
+ /**
262
+ * Compose an ActiveCube from an ACTIVE SeatRecord, hydrating the session bearer via
263
+ * the SOLE raw-bearer reader (getActiveSeatCredential). Returns null if the bearer
264
+ * can no longer be resolved (record concurrently reset/replaced).
265
+ */
266
+ async function hydrateActiveCube(record: SeatRecord): Promise<ActiveCube | null> {
267
+ const ref = seatRef(record);
268
+ const sessionToken = await getActiveSeatCredential(ref, {
269
+ origin: record.origin,
270
+ trustIdentity: record.trustIdentity,
271
+ cubeId: record.cubeId,
272
+ });
273
+ if (!sessionToken) return null;
274
+ return {
275
+ cubeId: record.cubeId,
276
+ droneId: record.droneId!,
277
+ name: record.name ?? '',
278
+ droneLabel: record.droneLabel ?? '',
279
+ sessionToken,
280
+ apiUrl: record.origin,
281
+ serverTrustIdentity: record.trustIdentity,
282
+ localSessionCredentialRef: ref,
283
+ ...(record.expiresAt !== undefined ? { localSessionExpiresAt: record.expiresAt } : {}),
284
+ ...(record.roleName !== undefined ? { roleName: record.roleName } : {}),
285
+ ...(record.roleClass !== undefined ? { roleClass: record.roleClass } : {}),
286
+ ...(record.isHumanSeat !== undefined ? { isHumanSeat: record.isHumanSeat } : {}),
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Legacy binding-only writer. In the collapsed single-store model an ACTIVE seat is
292
+ * created ONLY by the atomic mint→activate+bind path in seats.ts (driven by the
293
+ * attach FINALIZE); there is no standalone binding write. Retained solely as a
294
+ * fail-closed guard for incomplete session metadata.
295
+ */
296
+ export async function setActiveCube(_active: ActiveCubeInput): Promise<void> {
297
+ throw new Error('local Borg server session metadata is incomplete');
298
+ }
299
+
300
+ export function activeCubeWithFreshRegenIdentity(
301
+ active: ActiveCube,
302
+ result: {
303
+ cube?: { name?: string | null };
304
+ drone?: { label?: string | null };
305
+ role?: {
306
+ name?: string | null;
307
+ role_class?: 'queen' | 'worker' | null;
308
+ is_human_seat?: boolean | null;
309
+ };
310
+ }
311
+ ): ActiveCube {
312
+ const name = result.cube?.name ?? active.name;
313
+ const droneLabel = result.drone?.label ?? active.droneLabel;
314
+ const roleName = result.role?.name ?? active.roleName;
315
+ const roleClass = result.role?.role_class ?? active.roleClass;
316
+ const isHumanSeat = result.role?.is_human_seat ?? active.isHumanSeat;
317
+ if (name === active.name && droneLabel === active.droneLabel &&
318
+ roleName === active.roleName && roleClass === active.roleClass &&
319
+ isHumanSeat === active.isHumanSeat) return active;
320
+ return { ...active, name, droneLabel, roleName, roleClass, isHumanSeat };
321
+ }
322
+
323
+ // The ONLY sanctioned seat-clear is resetLocalSeatBinding → seats.ts
324
+ // resetSeatForWorktree, which under the single store flock re-checks the FULL
325
+ // binding (ref + drone id) + the token-safe observation and DELETES the whole
326
+ // record (credential AND binding together — no cross-store 'partial').
327
+
328
+ export interface LocalSeatSnapshot {
329
+ apiUrl: string;
330
+ serverTrustIdentity: string;
331
+ cubeId: string;
332
+ /** The FULL binding includes the prior drone identity (CR #3): a drone-id
333
+ * change at recheck is a full-binding change and aborts the reset. CR#4: a
334
+ * bound-PENDING record (a sibling whose activation failed) may carry NO drone id
335
+ * yet — the snapshot then omits it and the reset matches on undefined-vs-undefined,
336
+ * still deleting the exact record. */
337
+ droneId?: string;
338
+ credentialRef: string;
339
+ worktree: string;
340
+ /** Token-safe TYPED seat observation: active|pending (with digest) or absent. */
341
+ observation: SeatObservation;
342
+ }
343
+
344
+ export type ResetLocalSeatOutcome =
345
+ // The whole record (credential + binding) was deleted in one commit. In the
346
+ // single store there is no cross-store 'partial'/'repair-required' state.
347
+ | { outcome: 'reset'; credentialRef: string }
348
+ | { outcome: 'no-binding' }
349
+ | { outcome: 'changed' };
350
+
351
+ /**
352
+ * Snapshot this worktree's exact FULL local-seat binding (incl drone id) plus a
353
+ * token-safe TYPED observation (active + digest | absent). Read-only. Returns null
354
+ * when this worktree has no ACTIVE bound seat to reset: an honest no-op.
355
+ */
356
+ export async function snapshotLocalSeat(): Promise<LocalSeatSnapshot | null> {
357
+ const worktree = findProjectRoot();
358
+ // CR#4: discover an ACTIVE seat OR a bound-PENDING record (a sibling whose
359
+ // activation failed, bound to THIS worktree by the attach bind-pending step).
360
+ // getActiveSeatForWorktree would MISS the bound-pending record (it requires
361
+ // state==='active' + a drone id), so `reset-local-seat` would FALSELY report
362
+ // "nothing to reset" (exit 0) while a resumable, server-digest-bound bearer
363
+ // persists at rest — a FALSE-SUCCESS revocation failure. getSeatForWorktree sees
364
+ // both, and the offline reset's exact re-check + delete cover the bound-pending
365
+ // record too.
366
+ const record = await getSeatForWorktree(worktree);
367
+ if (!record || !record.cubeId) return null;
368
+ const ref = seatRef(record);
369
+ const observation = await observeSeat(ref, {
370
+ origin: record.origin,
371
+ trustIdentity: record.trustIdentity,
372
+ cubeId: record.cubeId,
373
+ });
374
+ return {
375
+ apiUrl: record.origin,
376
+ serverTrustIdentity: record.trustIdentity,
377
+ cubeId: record.cubeId,
378
+ ...(record.droneId !== undefined ? { droneId: record.droneId } : {}),
379
+ credentialRef: ref,
380
+ worktree,
381
+ observation,
382
+ };
383
+ }
384
+
385
+ export interface PersistedLocalSeat {
386
+ cubeId: string;
387
+ /** Optional: a bound-PENDING record (a sibling whose activation failed) may carry
388
+ * no drone id yet — the ACTIVE resume path still has it. */
389
+ droneId?: string;
390
+ name: string;
391
+ droneLabel: string;
392
+ apiUrl: string;
393
+ serverTrustIdentity: string;
394
+ localSessionCredentialRef: string;
395
+ localSessionExpiresAt?: string | null;
396
+ roleName?: string;
397
+ roleClass?: 'queen' | 'worker';
398
+ isHumanSeat?: boolean;
399
+ /** The STORED seat operation (projectRoot + kind + operationKey). The resume path
400
+ * re-derives the EXACT seat ref from this: for a bound-PENDING sibling it is the
401
+ * ORIGINAL sibling operation, NOT the rerun worktree's derived seat operation —
402
+ * so the rerun re-mints-or-reuses the identical pending bearer and converges. */
403
+ operation: SeatOperation;
404
+ /** The record state: a bound-PENDING record resumes with an ABSENT/pending-reuse
405
+ * expectation (re-send the identical bearer); an ACTIVE one resumes with EXACT. */
406
+ state: 'pending' | 'active';
407
+ }
408
+
409
+ /**
410
+ * Read the RAW persisted local-server seat bound to the current worktree — ACTIVE
411
+ * or a bound-PENDING record — WITHOUT hydrating its credential. Used by the resume
412
+ * path to recover the seat identity, its stored `operation`, and its `state`.
413
+ *
414
+ * CR#2: a SIBLING attach whose activation failed leaves a PENDING record BOUND to
415
+ * the preserved worktree (via the attach path's bind-pending step). This surfaces it so the
416
+ * rerun-from-that-worktree re-derives the EXACT sibling ref and re-sends the
417
+ * identical bearer (ghost-free convergence). A crash-in-gap PENDING record that was
418
+ * NEVER bound to a worktree still returns null here (it carries no worktree locator)
419
+ * and is resumed by prepareSeat's idempotent mint-or-reuse; a genuine absence is
420
+ * likewise null and a fresh enroll mints correctly.
421
+ */
422
+ export async function readPersistedLocalSeat(): Promise<PersistedLocalSeat | null> {
423
+ const record = await getSeatForWorktree(findProjectRoot());
424
+ if (!record || !record.cubeId) return null;
425
+ return {
426
+ cubeId: record.cubeId,
427
+ ...(record.droneId !== undefined ? { droneId: record.droneId } : {}),
428
+ name: record.name ?? '',
429
+ droneLabel: record.droneLabel ?? '',
430
+ apiUrl: record.origin,
431
+ serverTrustIdentity: record.trustIdentity,
432
+ localSessionCredentialRef: seatRef(record),
433
+ operation: record.operation,
434
+ state: record.state,
435
+ ...(record.expiresAt !== undefined ? { localSessionExpiresAt: record.expiresAt } : {}),
436
+ ...(record.roleName !== undefined ? { roleName: record.roleName } : {}),
437
+ ...(record.roleClass !== undefined ? { roleClass: record.roleClass } : {}),
438
+ ...(record.isHumanSeat !== undefined ? { isHumanSeat: record.isHumanSeat } : {}),
439
+ };
440
+ }
441
+
442
+ /**
443
+ * Reset this worktree's seat: delegate to the single-store resetSeatForWorktree,
444
+ * which under ONE flock re-checks the exact FULL binding (ref + drone id, CR #3)
445
+ * plus the token-safe observation and DELETES the whole record — credential AND
446
+ * binding vanish together in one commit. Any drift / missing / same-ref digest
447
+ * replacement is an honest no-op ('changed'); no cross-store 'partial' exists.
448
+ */
449
+ export async function resetLocalSeatBinding(
450
+ expected: LocalSeatSnapshot,
451
+ ): Promise<ResetLocalSeatOutcome> {
452
+ const outcome = await resetSeatForWorktree({
453
+ worktree: expected.worktree,
454
+ ref: expected.credentialRef,
455
+ droneId: expected.droneId,
456
+ observation: expected.observation,
457
+ });
458
+ if (outcome.outcome === 'reset') return { outcome: 'reset', credentialRef: outcome.ref };
459
+ if (outcome.outcome === 'no-binding') return { outcome: 'no-binding' };
460
+ return { outcome: 'changed' };
461
+ }
462
+
463
+ export type FinalizeServerSeatOutcome =
464
+ // Activate+bind committed atomically (credential ACTIVE and worktree bound).
465
+ | { committed: true }
466
+ // Aborted at PREPARE-time revalidation — nothing minted/bound (safe to roll back
467
+ // a just-spawned worktree; the own pending record was scrubbed). Produced only by
468
+ // the prepare stage (result.prepareAborted); never by the merged activate+bind.
469
+ | { committed: false; reason: 'expectation-mismatch' }
470
+ // The merged activate+bind did not commit (missing/replaced/threw). The PENDING
471
+ // record is the rerunnable locator; the caller must PRESERVE the worktree (CR #5).
472
+ | { committed: false; reason: 'activation-failed' };
473
+
474
+ /**
475
+ * Metadata-only refresh (cube name / drone label / role display) of the CURRENT
476
+ * worktree's ACTIVE seat — delegates to seats.ts refreshSeatMetadata, which CANNOT
477
+ * alter the credential, ref, identity, or worktree binding. A no-op when this
478
+ * worktree has no active seat, so a stale regen identity can never resurrect or
479
+ * mutate a seat ref.
480
+ */
481
+ export async function refreshActiveCubeMetadata(active: ActiveCubeInput): Promise<boolean> {
482
+ if (!active.localSessionCredentialRef) return false;
483
+ return refreshSeatMetadata(findProjectRoot(), {
484
+ credentialRef: active.localSessionCredentialRef,
485
+ cubeId: active.cubeId,
486
+ droneId: active.droneId,
487
+ }, {
488
+ name: active.name,
489
+ droneLabel: active.droneLabel,
490
+ ...(active.roleName !== undefined ? { roleName: active.roleName } : {}),
491
+ ...(active.roleClass !== undefined ? { roleClass: active.roleClass } : {}),
492
+ ...(active.isHumanSeat !== undefined ? { isHumanSeat: active.isHumanSeat } : {}),
493
+ });
494
+ }
495
+
496
+ export async function getProjectCliPreference(): Promise<BorgCli | null> {
497
+ const data = await readLaunchFile();
498
+ if (!data) return null;
499
+ const entry = data.projects[findProjectRoot()];
500
+ return entry?.cli === 'claude' || entry?.cli === 'codex' || entry?.cli === 'opencode' ? entry.cli : null;
501
+ }
502
+
503
+ /**
504
+ * gh#556 Part 2 — like getProjectCliPreference, but keyed on an arbitrary
505
+ * worktree dir (launch-all reads the saved CLI preference for EACH discovered
506
+ * worktree, not just cwd). Returns null if no preference is saved for that path.
507
+ */
508
+ export async function getProjectCliPreferenceForPath(dir: string): Promise<BorgCli | null> {
509
+ const data = await readLaunchFile();
510
+ if (!data) return null;
511
+ const entry = data.projects[findProjectRoot(dir)];
512
+ return entry?.cli === 'claude' || entry?.cli === 'codex' || entry?.cli === 'opencode' ? entry.cli : null;
513
+ }
514
+
515
+ /**
516
+ * gh#556 Part 2 — returns all persisted project identities from the seat store.
517
+ * Used by `borg launch-all` to enumerate drones across all known worktrees.
518
+ * Returns an empty array when no ACTIVE bound seats exist.
519
+ */
520
+ export async function readAllProjectIdentities(): Promise<
521
+ Array<{ projectPath: string; cube: ActiveCube }>
522
+ > {
523
+ const seats = await readAllActiveSeats();
524
+ const hydrated = await Promise.all(
525
+ seats.map(async ({ worktree, record }) => ({
526
+ projectPath: worktree,
527
+ cube: await hydrateActiveCube(record),
528
+ })),
529
+ );
530
+ return hydrated.flatMap(({ projectPath, cube }) =>
531
+ cube === null ? [] : [{ projectPath, cube }],
532
+ );
533
+ }
534
+
535
+ export async function setProjectCliPreference(cli: BorgCli): Promise<void> {
536
+ const existing = (await readLaunchFile()) ?? { projects: {} };
537
+ existing.projects[findProjectRoot()] = { cli };
538
+ await writeLaunchFile(existing);
539
+ }
540
+
541
+ export async function setCodexWakeTarget(
542
+ cubeId: string,
543
+ droneId: string,
544
+ target: Omit<CodexWakeTargetRecord, 'updatedAt'>
545
+ ): Promise<void> {
546
+ const existing = (await readCodexWakeTargetsFile()) ?? { targets: {} };
547
+ existing.targets[codexWakeTargetKey(cubeId, droneId)] = {
548
+ ...target,
549
+ updatedAt: new Date().toISOString(),
550
+ };
551
+ await writeCodexWakeTargetsFile(existing);
552
+ }
553
+
554
+ export async function getCodexWakeTarget(
555
+ cubeId: string,
556
+ droneId: string
557
+ ): Promise<CodexWakeTargetRecord | null> {
558
+ const existing = await readCodexWakeTargetsFile();
559
+ if (!existing) return null;
560
+ const target = existing.targets[codexWakeTargetKey(cubeId, droneId)];
561
+ if (!target || typeof target.threadId !== 'string' || typeof target.socketPath !== 'string') {
562
+ return null;
563
+ }
564
+ return target;
565
+ }
566
+
567
+ /**
568
+ * gh#855: drop wake-target entries whose app-server socket is positively dead,
569
+ * so the file self-heals (stale dead-socket entries from crashed prior launches
570
+ * don't linger and mislead probeCodexBridgeArmed / health-beat). Pure prune
571
+ * decision lives in codex-wake-resolve.ts (false-deaf-avoidance: keeps alive +
572
+ * indeterminate); this is the thin read → prune → write-only-on-change glue.
573
+ * The liveness check is injected (claude.ts wires checkCodexBridgeHealthy) so
574
+ * cubes.ts stays free of the codex-remote dependency.
575
+ */
576
+ export async function pruneDeadCodexWakeTargets(
577
+ socketLiveness: (socketPath: string) => boolean | null
578
+ ): Promise<void> {
579
+ const existing = await readCodexWakeTargetsFile();
580
+ if (!existing) return;
581
+ const { targets, changed } = pruneDeadWakeTargets(existing.targets, socketLiveness);
582
+ if (changed) await writeCodexWakeTargetsFile({ ...existing, targets });
583
+ }