borgmcp 1.1.15 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (466) hide show
  1. package/CONTRIBUTING.md +15 -0
  2. package/LICENSE +201 -33
  3. package/NOTICE +2 -0
  4. package/README.md +85 -36
  5. package/SECURITY.md +7 -0
  6. package/dist/agent-runtime.d.ts.map +1 -0
  7. package/dist/agent-runtime.js +63 -1
  8. package/dist/agent-runtime.js.map +1 -0
  9. package/dist/assimilate-cmd.d.ts +96 -17
  10. package/dist/assimilate-cmd.d.ts.map +1 -0
  11. package/dist/assimilate-cmd.js +1396 -48
  12. package/dist/assimilate-cmd.js.map +1 -0
  13. package/dist/assimilate-deps.d.ts.map +1 -0
  14. package/dist/assimilate-deps.js +384 -3
  15. package/dist/assimilate-deps.js.map +1 -0
  16. package/dist/assimilate-guard.d.ts +5 -7
  17. package/dist/assimilate-guard.d.ts.map +1 -0
  18. package/dist/assimilate-guard.js +60 -2
  19. package/dist/assimilate-guard.js.map +1 -0
  20. package/dist/assimilate-welcome.d.ts +1 -1
  21. package/dist/assimilate-welcome.d.ts.map +1 -0
  22. package/dist/assimilate-welcome.js +32 -2
  23. package/dist/assimilate-welcome.js.map +1 -0
  24. package/dist/auth-env.d.ts +7 -50
  25. package/dist/auth-env.d.ts.map +1 -0
  26. package/dist/auth-env.js +22 -1
  27. package/dist/auth-env.js.map +1 -0
  28. package/dist/backends/launch-all-pastelist.d.ts.map +1 -0
  29. package/dist/backends/launch-all-pastelist.js +11 -4
  30. package/dist/backends/launch-all-pastelist.js.map +1 -0
  31. package/dist/backends/launch-all-tmux.d.ts.map +1 -0
  32. package/dist/backends/launch-all-tmux.js +49 -1
  33. package/dist/backends/launch-all-tmux.js.map +1 -0
  34. package/dist/backends/launch-all-windows.d.ts.map +1 -0
  35. package/dist/backends/launch-all-windows.js +86 -12
  36. package/dist/backends/launch-all-windows.js.map +1 -0
  37. package/dist/bare-launch-menu.d.ts.map +1 -0
  38. package/dist/bare-launch-menu.js +83 -6
  39. package/dist/bare-launch-menu.js.map +1 -0
  40. package/dist/claude-launch-args.d.ts.map +1 -0
  41. package/dist/claude-launch-args.js +34 -1
  42. package/dist/claude-launch-args.js.map +1 -0
  43. package/dist/claude.d.ts.map +1 -0
  44. package/dist/claude.js +486 -14
  45. package/dist/claude.js.map +1 -0
  46. package/dist/cleanup-cmd.d.ts +5 -5
  47. package/dist/cleanup-cmd.d.ts.map +1 -0
  48. package/dist/cleanup-cmd.js +453 -14
  49. package/dist/cleanup-cmd.js.map +1 -0
  50. package/dist/clear-rewake-core.d.ts.map +1 -0
  51. package/dist/clear-rewake-core.js +23 -2
  52. package/dist/clear-rewake-core.js.map +1 -0
  53. package/dist/clear-rewake.d.ts.map +1 -0
  54. package/dist/clear-rewake.js +19 -1
  55. package/dist/clear-rewake.js.map +1 -0
  56. package/dist/cli-help.d.ts +5 -0
  57. package/dist/cli-help.d.ts.map +1 -0
  58. package/dist/cli-help.js +104 -56
  59. package/dist/cli-help.js.map +1 -0
  60. package/dist/cli-platform.d.ts.map +1 -0
  61. package/dist/cli-platform.js +106 -4
  62. package/dist/cli-platform.js.map +1 -0
  63. package/dist/cli-tool-approval.d.ts +84 -0
  64. package/dist/cli-tool-approval.d.ts.map +1 -0
  65. package/dist/cli-tool-approval.js +583 -0
  66. package/dist/cli-tool-approval.js.map +1 -0
  67. package/dist/codex-app-server.d.ts.map +1 -0
  68. package/dist/codex-app-server.js +228 -4
  69. package/dist/codex-app-server.js.map +1 -0
  70. package/dist/codex-app-wake.d.ts +9 -11
  71. package/dist/codex-app-wake.d.ts.map +1 -0
  72. package/dist/codex-app-wake.js +436 -2
  73. package/dist/codex-app-wake.js.map +1 -0
  74. package/dist/codex-launch.d.ts.map +1 -0
  75. package/dist/codex-launch.js +102 -2
  76. package/dist/codex-launch.js.map +1 -0
  77. package/dist/codex-remote.d.ts +6 -1
  78. package/dist/codex-remote.d.ts.map +1 -0
  79. package/dist/codex-remote.js +394 -1
  80. package/dist/codex-remote.js.map +1 -0
  81. package/dist/codex-wake-resolve.d.ts +4 -4
  82. package/dist/codex-wake-resolve.d.ts.map +1 -0
  83. package/dist/codex-wake-resolve.js +127 -1
  84. package/dist/codex-wake-resolve.js.map +1 -0
  85. package/dist/config-utils.d.ts +2 -2
  86. package/dist/config-utils.d.ts.map +1 -0
  87. package/dist/config-utils.js +766 -3
  88. package/dist/config-utils.js.map +1 -0
  89. package/dist/config.d.ts +98 -61
  90. package/dist/config.d.ts.map +1 -0
  91. package/dist/config.js +426 -1
  92. package/dist/config.js.map +1 -0
  93. package/dist/console-prefix.d.ts.map +1 -0
  94. package/dist/console-prefix.js +96 -1
  95. package/dist/console-prefix.js.map +1 -0
  96. package/dist/cube-name.d.ts.map +1 -0
  97. package/dist/cube-name.js +67 -1
  98. package/dist/cube-name.js.map +1 -0
  99. package/dist/cubes.d.ts +126 -18
  100. package/dist/cubes.d.ts.map +1 -0
  101. package/dist/cubes.js +412 -4
  102. package/dist/cubes.js.map +1 -0
  103. package/dist/debug.d.ts.map +1 -0
  104. package/dist/debug.js +71 -1
  105. package/dist/debug.js.map +1 -0
  106. package/dist/direct-log.d.ts.map +1 -0
  107. package/dist/direct-log.js +11 -1
  108. package/dist/direct-log.js.map +1 -0
  109. package/dist/docs-sections.d.ts +11 -10
  110. package/dist/docs-sections.d.ts.map +1 -0
  111. package/dist/docs-sections.js +113 -3
  112. package/dist/docs-sections.js.map +1 -0
  113. package/dist/drone-lifecycle.d.ts +5 -21
  114. package/dist/drone-lifecycle.d.ts.map +1 -0
  115. package/dist/drone-lifecycle.js +57 -8
  116. package/dist/drone-lifecycle.js.map +1 -0
  117. package/dist/drone-management.d.ts +29 -0
  118. package/dist/drone-management.d.ts.map +1 -0
  119. package/dist/drone-management.js +109 -0
  120. package/dist/drone-management.js.map +1 -0
  121. package/dist/ensure-mcp-config.d.ts.map +1 -0
  122. package/dist/ensure-mcp-config.js +36 -1
  123. package/dist/ensure-mcp-config.js.map +1 -0
  124. package/dist/evict-drone.d.ts +10 -2
  125. package/dist/evict-drone.d.ts.map +1 -0
  126. package/dist/evict-drone.js +60 -1
  127. package/dist/evict-drone.js.map +1 -0
  128. package/dist/gc-orphan-inboxes.d.ts.map +1 -0
  129. package/dist/gc-orphan-inboxes.js +177 -1
  130. package/dist/gc-orphan-inboxes.js.map +1 -0
  131. package/dist/inbox-monitor.d.ts +1 -1
  132. package/dist/inbox-monitor.d.ts.map +1 -0
  133. package/dist/inbox-monitor.js +856 -3
  134. package/dist/inbox-monitor.js.map +1 -0
  135. package/dist/index.d.ts +6 -4
  136. package/dist/index.d.ts.map +1 -0
  137. package/dist/index.js +1034 -37
  138. package/dist/index.js.map +1 -0
  139. package/dist/launch-all-cmd.d.ts.map +1 -0
  140. package/dist/launch-all-cmd.js +377 -37
  141. package/dist/launch-all-cmd.js.map +1 -0
  142. package/dist/launch-all-command.d.ts.map +1 -0
  143. package/dist/launch-all-command.js +24 -1
  144. package/dist/launch-all-command.js.map +1 -0
  145. package/dist/launch-all-deps.d.ts +3 -8
  146. package/dist/launch-all-deps.d.ts.map +1 -0
  147. package/dist/launch-all-deps.js +99 -1
  148. package/dist/launch-all-deps.js.map +1 -0
  149. package/dist/launch-all-discovery.d.ts +1 -0
  150. package/dist/launch-all-discovery.d.ts.map +1 -0
  151. package/dist/launch-all-discovery.js +90 -4
  152. package/dist/launch-all-discovery.js.map +1 -0
  153. package/dist/launch-all-locks.d.ts.map +1 -0
  154. package/dist/launch-all-locks.js +54 -1
  155. package/dist/launch-all-locks.js.map +1 -0
  156. package/dist/launch-gate.d.ts +2 -2
  157. package/dist/launch-gate.d.ts.map +1 -0
  158. package/dist/launch-gate.js +73 -1
  159. package/dist/launch-gate.js.map +1 -0
  160. package/dist/lifecycle-log-guard.d.ts.map +1 -0
  161. package/dist/lifecycle-log-guard.js +93 -2
  162. package/dist/lifecycle-log-guard.js.map +1 -0
  163. package/dist/list-roles-render.d.ts.map +1 -0
  164. package/dist/list-roles-render.js +40 -6
  165. package/dist/list-roles-render.js.map +1 -0
  166. package/dist/local-log-routing.d.ts +19 -0
  167. package/dist/local-log-routing.d.ts.map +1 -0
  168. package/dist/local-log-routing.js +67 -0
  169. package/dist/local-log-routing.js.map +1 -0
  170. package/dist/local-manage-tool-result.d.ts +8 -0
  171. package/dist/local-manage-tool-result.d.ts.map +1 -0
  172. package/dist/local-manage-tool-result.js +10 -0
  173. package/dist/local-manage-tool-result.js.map +1 -0
  174. package/dist/local-server-cursor.d.ts +34 -0
  175. package/dist/local-server-cursor.d.ts.map +1 -0
  176. package/dist/local-server-cursor.js +151 -0
  177. package/dist/local-server-cursor.js.map +1 -0
  178. package/dist/log-audit.d.ts.map +1 -0
  179. package/dist/log-audit.js +194 -3
  180. package/dist/log-audit.js.map +1 -0
  181. package/dist/log-stream.d.ts +35 -18
  182. package/dist/log-stream.d.ts.map +1 -0
  183. package/dist/log-stream.js +1270 -12
  184. package/dist/log-stream.js.map +1 -0
  185. package/dist/model-presets.d.ts.map +1 -0
  186. package/dist/model-presets.js +19 -1
  187. package/dist/model-presets.js.map +1 -0
  188. package/dist/name-validator.d.ts.map +1 -0
  189. package/dist/name-validator.js +22 -1
  190. package/dist/name-validator.js.map +1 -0
  191. package/dist/opencode-drone.d.ts.map +1 -0
  192. package/dist/opencode-drone.js +438 -4
  193. package/dist/opencode-drone.js.map +1 -0
  194. package/dist/opencode-plugin.d.ts +1 -1
  195. package/dist/opencode-plugin.d.ts.map +1 -0
  196. package/dist/opencode-plugin.js +29 -4
  197. package/dist/opencode-plugin.js.map +1 -0
  198. package/dist/parse-assimilate-args.d.ts +1 -1
  199. package/dist/parse-assimilate-args.d.ts.map +1 -0
  200. package/dist/parse-assimilate-args.js +125 -1
  201. package/dist/parse-assimilate-args.js.map +1 -0
  202. package/dist/parse-launch-all-args.d.ts.map +1 -0
  203. package/dist/parse-launch-all-args.js +70 -1
  204. package/dist/parse-launch-all-args.js.map +1 -0
  205. package/dist/postinstall-banner.d.ts.map +1 -0
  206. package/dist/postinstall-banner.js +27 -2
  207. package/dist/postinstall-banner.js.map +1 -0
  208. package/dist/postinstall.d.ts.map +1 -0
  209. package/dist/postinstall.js +32 -5
  210. package/dist/postinstall.js.map +1 -0
  211. package/dist/readiness-probe.d.ts.map +1 -0
  212. package/dist/readiness-probe.js +9 -1
  213. package/dist/readiness-probe.js.map +1 -0
  214. package/dist/regen-format.d.ts.map +1 -0
  215. package/dist/regen-format.js +545 -13
  216. package/dist/regen-format.js.map +1 -0
  217. package/dist/regen.d.ts.map +1 -0
  218. package/dist/regen.js +141 -5
  219. package/dist/regen.js.map +1 -0
  220. package/dist/remote-client.d.ts +88 -173
  221. package/dist/remote-client.d.ts.map +1 -0
  222. package/dist/remote-client.js +997 -1
  223. package/dist/remote-client.js.map +1 -0
  224. package/dist/reset-local-seat-cmd.d.ts +50 -0
  225. package/dist/reset-local-seat-cmd.d.ts.map +1 -0
  226. package/dist/reset-local-seat-cmd.js +169 -0
  227. package/dist/reset-local-seat-cmd.js.map +1 -0
  228. package/dist/role-match.d.ts.map +1 -0
  229. package/dist/role-match.js +23 -1
  230. package/dist/role-match.js.map +1 -0
  231. package/dist/role-resolver.d.ts.map +1 -0
  232. package/dist/role-resolver.js +60 -1
  233. package/dist/role-resolver.js.map +1 -0
  234. package/dist/roster-render.d.ts.map +1 -0
  235. package/dist/roster-render.js +121 -3
  236. package/dist/roster-render.js.map +1 -0
  237. package/dist/seat-probe.d.ts +32 -13
  238. package/dist/seat-probe.d.ts.map +1 -0
  239. package/dist/seat-probe.js +78 -1
  240. package/dist/seat-probe.js.map +1 -0
  241. package/dist/seat-store.d.ts +76 -0
  242. package/dist/seat-store.d.ts.map +1 -0
  243. package/dist/seat-store.js +309 -0
  244. package/dist/seat-store.js.map +1 -0
  245. package/dist/seats.d.ts +375 -0
  246. package/dist/seats.d.ts.map +1 -0
  247. package/dist/seats.js +670 -0
  248. package/dist/seats.js.map +1 -0
  249. package/dist/self-path.d.ts +27 -0
  250. package/dist/self-path.d.ts.map +1 -0
  251. package/dist/self-path.js +43 -0
  252. package/dist/self-path.js.map +1 -0
  253. package/dist/server-endpoint.d.ts +9 -0
  254. package/dist/server-endpoint.d.ts.map +1 -0
  255. package/dist/server-endpoint.js +38 -0
  256. package/dist/server-endpoint.js.map +1 -0
  257. package/dist/server-errors.d.ts +50 -0
  258. package/dist/server-errors.d.ts.map +1 -0
  259. package/dist/server-errors.js +79 -0
  260. package/dist/server-errors.js.map +1 -0
  261. package/dist/server-handshake.d.ts +197 -0
  262. package/dist/server-handshake.d.ts.map +1 -0
  263. package/dist/server-handshake.js +533 -0
  264. package/dist/server-handshake.js.map +1 -0
  265. package/dist/server-response.d.ts +3 -0
  266. package/dist/server-response.d.ts.map +1 -0
  267. package/dist/server-response.js +67 -0
  268. package/dist/server-response.js.map +1 -0
  269. package/dist/server-trust.d.ts +14 -0
  270. package/dist/server-trust.d.ts.map +1 -0
  271. package/dist/server-trust.js +225 -0
  272. package/dist/server-trust.js.map +1 -0
  273. package/dist/session-continuity.d.ts +33 -0
  274. package/dist/session-continuity.d.ts.map +1 -0
  275. package/dist/session-continuity.js +220 -0
  276. package/dist/session-continuity.js.map +1 -0
  277. package/dist/setup-confirm.d.ts +3 -3
  278. package/dist/setup-confirm.d.ts.map +1 -0
  279. package/dist/setup-confirm.js +126 -2
  280. package/dist/setup-confirm.js.map +1 -0
  281. package/dist/setup.d.ts +5 -3
  282. package/dist/setup.d.ts.map +1 -0
  283. package/dist/setup.js +169 -46
  284. package/dist/setup.js.map +1 -0
  285. package/dist/shell-escape.d.ts.map +1 -0
  286. package/dist/shell-escape.js +22 -1
  287. package/dist/shell-escape.js.map +1 -0
  288. package/dist/spawn.d.ts +2 -2
  289. package/dist/spawn.d.ts.map +1 -0
  290. package/dist/spawn.js +29 -10
  291. package/dist/spawn.js.map +1 -0
  292. package/dist/startup-services.d.ts +0 -1
  293. package/dist/startup-services.d.ts.map +1 -0
  294. package/dist/startup-services.js +26 -1
  295. package/dist/startup-services.js.map +1 -0
  296. package/dist/stream-owner.d.ts +6 -2
  297. package/dist/stream-owner.d.ts.map +1 -0
  298. package/dist/stream-owner.js +547 -2
  299. package/dist/stream-owner.js.map +1 -0
  300. package/dist/stream-status.d.ts +1 -1
  301. package/dist/stream-status.d.ts.map +1 -0
  302. package/dist/stream-status.js +268 -3
  303. package/dist/stream-status.js.map +1 -0
  304. package/dist/sync-roles-render.d.ts.map +1 -0
  305. package/dist/sync-roles-render.js +118 -3
  306. package/dist/sync-roles-render.js.map +1 -0
  307. package/dist/sync.d.ts +1 -1
  308. package/dist/sync.d.ts.map +1 -0
  309. package/dist/sync.js +286 -22
  310. package/dist/sync.js.map +1 -0
  311. package/dist/terminal-title.d.ts.map +1 -0
  312. package/dist/terminal-title.js +68 -1
  313. package/dist/terminal-title.js.map +1 -0
  314. package/dist/token-store.d.ts +17 -111
  315. package/dist/token-store.d.ts.map +1 -0
  316. package/dist/token-store.js +81 -1
  317. package/dist/token-store.js.map +1 -0
  318. package/dist/tool-manifest.d.ts +4 -8
  319. package/dist/tool-manifest.d.ts.map +1 -0
  320. package/dist/tool-manifest.js +554 -1
  321. package/dist/tool-manifest.js.map +1 -0
  322. package/dist/tool-scope.d.ts +8 -12
  323. package/dist/tool-scope.d.ts.map +1 -0
  324. package/dist/tool-scope.js +97 -1
  325. package/dist/tool-scope.js.map +1 -0
  326. package/dist/types.d.ts +1 -5
  327. package/dist/types.d.ts.map +1 -0
  328. package/dist/types.js +5 -0
  329. package/dist/types.js.map +1 -0
  330. package/dist/unknown-subcommand.d.ts +1 -1
  331. package/dist/unknown-subcommand.d.ts.map +1 -0
  332. package/dist/unknown-subcommand.js +37 -1
  333. package/dist/unknown-subcommand.js.map +1 -0
  334. package/dist/version.d.ts +1 -1
  335. package/dist/version.d.ts.map +1 -0
  336. package/dist/version.js +78 -2
  337. package/dist/version.js.map +1 -0
  338. package/dist/working-repo.d.ts.map +1 -0
  339. package/dist/working-repo.js +81 -1
  340. package/dist/working-repo.js.map +1 -0
  341. package/dist/worktree-lifecycle.d.ts.map +1 -0
  342. package/dist/worktree-lifecycle.js +205 -2
  343. package/dist/worktree-lifecycle.js.map +1 -0
  344. package/docs/EXTRACTION_PROVENANCE.md +42 -0
  345. package/docs/LOCAL_SERVER.md +103 -0
  346. package/docs/RELEASING.md +129 -0
  347. package/package.json +52 -34
  348. package/src/agent-runtime.ts +69 -0
  349. package/src/assimilate-cmd.ts +1864 -0
  350. package/src/assimilate-deps.ts +444 -0
  351. package/src/assimilate-guard.ts +84 -0
  352. package/src/assimilate-welcome.ts +39 -0
  353. package/src/auth-env.ts +21 -0
  354. package/src/backends/launch-all-pastelist.ts +18 -0
  355. package/src/backends/launch-all-tmux.ts +75 -0
  356. package/src/backends/launch-all-windows.ts +110 -0
  357. package/src/bare-launch-menu.ts +148 -0
  358. package/src/claude-launch-args.ts +35 -0
  359. package/src/claude.ts +561 -0
  360. package/src/cleanup-cmd.ts +581 -0
  361. package/src/clear-rewake-core.ts +34 -0
  362. package/src/clear-rewake.ts +18 -0
  363. package/src/cli-help.ts +116 -0
  364. package/src/cli-platform.ts +127 -0
  365. package/src/cli-tool-approval.ts +716 -0
  366. package/src/codex-app-server.ts +238 -0
  367. package/src/codex-app-wake.ts +524 -0
  368. package/src/codex-launch.ts +140 -0
  369. package/src/codex-remote.ts +500 -0
  370. package/src/codex-wake-resolve.ts +155 -0
  371. package/src/config-utils.ts +811 -0
  372. package/src/config.ts +595 -0
  373. package/src/console-prefix.ts +99 -0
  374. package/src/cube-name.ts +69 -0
  375. package/src/cubes.ts +583 -0
  376. package/src/debug.ts +73 -0
  377. package/src/direct-log.ts +9 -0
  378. package/src/docs-sections.ts +129 -0
  379. package/src/drone-lifecycle.ts +62 -0
  380. package/src/drone-management.ts +162 -0
  381. package/src/ensure-mcp-config.ts +54 -0
  382. package/src/evict-drone.ts +82 -0
  383. package/src/gc-orphan-inboxes.ts +229 -0
  384. package/src/inbox-monitor.ts +1005 -0
  385. package/src/index.ts +1201 -0
  386. package/src/launch-all-cmd.ts +450 -0
  387. package/src/launch-all-command.ts +30 -0
  388. package/src/launch-all-deps.ts +188 -0
  389. package/src/launch-all-discovery.ts +118 -0
  390. package/src/launch-all-locks.ts +74 -0
  391. package/src/launch-gate.ts +80 -0
  392. package/src/lifecycle-log-guard.ts +144 -0
  393. package/src/list-roles-render.ts +52 -0
  394. package/src/local-log-routing.ts +106 -0
  395. package/src/local-manage-tool-result.ts +10 -0
  396. package/src/local-server-cursor.ts +188 -0
  397. package/src/log-audit.ts +207 -0
  398. package/src/log-stream.ts +1567 -0
  399. package/src/model-presets.ts +22 -0
  400. package/src/name-validator.ts +23 -0
  401. package/src/opencode-drone.ts +528 -0
  402. package/src/opencode-plugin.ts +38 -0
  403. package/src/parse-assimilate-args.ts +125 -0
  404. package/src/parse-launch-all-args.ts +100 -0
  405. package/src/postinstall-banner.ts +37 -0
  406. package/src/postinstall.ts +36 -0
  407. package/src/readiness-probe.ts +12 -0
  408. package/src/regen-format.ts +651 -0
  409. package/src/regen.ts +150 -0
  410. package/src/remote-client.ts +1457 -0
  411. package/src/reset-local-seat-cmd.ts +227 -0
  412. package/src/role-match.ts +29 -0
  413. package/src/role-resolver.ts +93 -0
  414. package/src/roster-render.ts +196 -0
  415. package/src/seat-probe.ts +115 -0
  416. package/src/seat-store.ts +333 -0
  417. package/src/seats.ts +911 -0
  418. package/src/self-path.ts +51 -0
  419. package/src/server-endpoint.ts +38 -0
  420. package/src/server-errors.ts +95 -0
  421. package/src/server-handshake.ts +816 -0
  422. package/src/server-response.ts +76 -0
  423. package/src/server-trust.ts +245 -0
  424. package/src/session-continuity.ts +280 -0
  425. package/src/setup-confirm.ts +171 -0
  426. package/src/setup.ts +186 -0
  427. package/src/shell-escape.ts +22 -0
  428. package/src/spawn.ts +31 -0
  429. package/src/startup-services.ts +36 -0
  430. package/src/stream-owner.ts +686 -0
  431. package/src/stream-status.ts +337 -0
  432. package/src/sync-roles-render.ts +163 -0
  433. package/src/sync.ts +363 -0
  434. package/src/terminal-title.ts +76 -0
  435. package/src/token-store.ts +102 -0
  436. package/src/tool-manifest.ts +610 -0
  437. package/src/tool-scope.ts +110 -0
  438. package/src/types.ts +5 -0
  439. package/src/unknown-subcommand.ts +35 -0
  440. package/src/version.ts +82 -0
  441. package/src/working-repo.ts +99 -0
  442. package/src/worktree-lifecycle.ts +254 -0
  443. package/dist/auth-recovery.d.ts +0 -33
  444. package/dist/auth-recovery.js +0 -1
  445. package/dist/auth.d.ts +0 -103
  446. package/dist/auth.js +0 -39
  447. package/dist/device-auth.d.ts +0 -75
  448. package/dist/device-auth.js +0 -1
  449. package/dist/drone-address.d.ts +0 -26
  450. package/dist/drone-address.js +0 -1
  451. package/dist/get-started.d.ts +0 -20
  452. package/dist/get-started.js +0 -2
  453. package/dist/health-beat.d.ts +0 -134
  454. package/dist/health-beat.js +0 -1
  455. package/dist/role-section.d.ts +0 -110
  456. package/dist/role-section.js +0 -8
  457. package/dist/setup-action.d.ts +0 -20
  458. package/dist/setup-action.js +0 -1
  459. package/dist/stale-version-check.d.ts +0 -40
  460. package/dist/stale-version-check.js +0 -1
  461. package/dist/subscription-retry.d.ts +0 -40
  462. package/dist/subscription-retry.js +0 -1
  463. package/dist/templates.d.ts +0 -107
  464. package/dist/templates.js +0 -359
  465. package/dist/token-crypto.d.ts +0 -50
  466. package/dist/token-crypto.js +0 -1
package/dist/seats.js ADDED
@@ -0,0 +1,670 @@
1
+ /**
2
+ * The unified 0600 seat store (Queen rescope, scope A) — `seats.json`.
3
+ *
4
+ * Collapses the per-seat drone-session CREDENTIAL and its worktree BINDING into
5
+ * ONE atomically-written record, keyed by the deterministic seat ref
6
+ * (origin+trust+cube+role+operation). Because a record's `state` and its
7
+ * `worktree` binding are written together in a single `withStore` commit, the
8
+ * invariant "an ACTIVE credential without a binding" is UNREACHABLE BY
9
+ * CONSTRUCTION: PREPARE mints `{credential, state:'pending'}` with NO worktree;
10
+ * FINALIZE sets `state:'active' + server metadata + worktree + display` in one
11
+ * commit; a reset deletes the whole record so credential+binding vanish together
12
+ * (there is no cross-store 'partial').
13
+ *
14
+ * The single store flock (seat-store.withStore) serializes every read-compare-
15
+ * write; there is no second lock and no composite. The raw bearer rests ONLY in
16
+ * the 0600 file and is never returned past this module except by the sole
17
+ * hydration reader; every other observation is digest-only.
18
+ */
19
+ import os from 'os';
20
+ import path from 'path';
21
+ import { createHash } from 'node:crypto';
22
+ import { readStoreFile, withStore } from './seat-store.js';
23
+ export const SEATS_FILE = path.join(os.homedir(), '.config', 'borgmcp', 'seats.json');
24
+ const SEATS_VERSION = 1;
25
+ const REF_RE = /^borg-server-session:[a-f0-9]{64}$/;
26
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
27
+ /** The deterministic per-seat ref (identical algorithm to the retired keychain account). */
28
+ export function seatRef(input) {
29
+ const binding = createHash('sha256')
30
+ .update(input.origin)
31
+ .update('\0')
32
+ .update(input.trustIdentity)
33
+ .update('\0')
34
+ .update(input.cubeId)
35
+ .update('\0')
36
+ .update(input.roleId)
37
+ .update('\0')
38
+ .update(input.operation.projectRoot)
39
+ .update('\0')
40
+ .update(input.operation.kind)
41
+ .update('\0')
42
+ .update(input.operation.operationKey)
43
+ .digest('hex');
44
+ return `borg-server-session:${binding}`;
45
+ }
46
+ function digestOf(bearer) {
47
+ return createHash('sha256').update(bearer).digest('hex');
48
+ }
49
+ function emptyStore() {
50
+ return { version: SEATS_VERSION, seats: {} };
51
+ }
52
+ const ROLE_CLASSES = new Set(['queen', 'worker']);
53
+ const OPERATION_KINDS = new Set(['seat', 'sibling']);
54
+ function isNonEmptyString(value) {
55
+ return typeof value === 'string' && value.length > 0;
56
+ }
57
+ function isValidOperation(value) {
58
+ if (value === null || typeof value !== 'object')
59
+ return false;
60
+ const op = value;
61
+ return (isNonEmptyString(op.projectRoot) &&
62
+ typeof op.kind === 'string' &&
63
+ OPERATION_KINDS.has(op.kind) &&
64
+ isNonEmptyString(op.operationKey));
65
+ }
66
+ /**
67
+ * CR#2: FULL per-entry validation. Every key/value/invariant of a seat record is
68
+ * checked — the ref is well-formed and self-consistent (the map key equals the
69
+ * record's derived ref), state ∈ {pending,active}, the credential is a non-empty
70
+ * string, the operation is well-shaped, an ACTIVE record carries ALL its required
71
+ * server + binding fields, and a PENDING record carries NO active-only fields
72
+ * (session id / expiry). A single invalid entry ⇒ the whole store is rejected
73
+ * (fail closed at the caller, bytes preserved) — never a silent cast.
74
+ */
75
+ function isValidSeatRecord(ref, value) {
76
+ if (!REF_RE.test(ref))
77
+ return false;
78
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
79
+ return false;
80
+ const r = value;
81
+ // Identity + credential + operation.
82
+ if (!isNonEmptyString(r.origin) ||
83
+ !isNonEmptyString(r.trustIdentity) ||
84
+ !isNonEmptyString(r.cubeId) ||
85
+ !isNonEmptyString(r.roleId) ||
86
+ !isValidOperation(r.operation) ||
87
+ !isNonEmptyString(r.credential)) {
88
+ return false;
89
+ }
90
+ if (r.state !== 'pending' && r.state !== 'active')
91
+ return false;
92
+ // Optional display/typed fields, validated when present.
93
+ if (r.name !== undefined && typeof r.name !== 'string')
94
+ return false;
95
+ if (r.droneLabel !== undefined && typeof r.droneLabel !== 'string')
96
+ return false;
97
+ if (r.roleName !== undefined && typeof r.roleName !== 'string')
98
+ return false;
99
+ if (r.roleClass !== undefined && (typeof r.roleClass !== 'string' || !ROLE_CLASSES.has(r.roleClass)))
100
+ return false;
101
+ if (r.isHumanSeat !== undefined && typeof r.isHumanSeat !== 'boolean')
102
+ return false;
103
+ if (r.worktree !== undefined && typeof r.worktree !== 'string')
104
+ return false;
105
+ if (r.droneId !== undefined && (typeof r.droneId !== 'string' || !UUID_RE.test(r.droneId)))
106
+ return false;
107
+ if (r.sessionId !== undefined && (typeof r.sessionId !== 'string' || !UUID_RE.test(r.sessionId)))
108
+ return false;
109
+ if (r.expiresAt !== undefined &&
110
+ (typeof r.expiresAt !== 'string' || !Number.isFinite(Date.parse(r.expiresAt)))) {
111
+ return false;
112
+ }
113
+ if (r.replacement !== undefined) {
114
+ if (r.state !== 'active' ||
115
+ r.replacement === null ||
116
+ typeof r.replacement !== 'object' ||
117
+ Array.isArray(r.replacement) ||
118
+ Object.keys(r.replacement).length !== 1 ||
119
+ typeof r.replacement.credential !== 'string' ||
120
+ !/^[A-Za-z0-9_-]{43}$/.test(r.replacement.credential)) {
121
+ return false;
122
+ }
123
+ }
124
+ // State-consistency invariants (no inconsistent active|pending).
125
+ if (r.state === 'active') {
126
+ // An ACTIVE record MUST carry its full server session + worktree binding.
127
+ if (typeof r.droneId !== 'string' ||
128
+ typeof r.sessionId !== 'string' ||
129
+ typeof r.expiresAt !== 'string' ||
130
+ typeof r.worktree !== 'string' ||
131
+ typeof r.name !== 'string' ||
132
+ typeof r.droneLabel !== 'string') {
133
+ return false;
134
+ }
135
+ }
136
+ else {
137
+ // A PENDING record must NOT carry active-only server session fields.
138
+ if (r.sessionId !== undefined || r.expiresAt !== undefined)
139
+ return false;
140
+ }
141
+ // The map key must equal the record's derived ref (no cross-key aliasing).
142
+ return seatRef(value) === ref;
143
+ }
144
+ /**
145
+ * CR4 + CR#2: parse + FULL version/schema/per-entry validation. Returns null (→ fail
146
+ * closed at the caller, bytes preserved) for any malformed JSON, wrong version, or
147
+ * ANY invalid entry; never throws and never coerces an invalid file into a
148
+ * valid-looking or empty store.
149
+ */
150
+ function parseStore(raw) {
151
+ let parsed;
152
+ try {
153
+ parsed = JSON.parse(raw);
154
+ }
155
+ catch {
156
+ return null;
157
+ }
158
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
159
+ const candidate = parsed;
160
+ if (candidate.version === SEATS_VERSION &&
161
+ candidate.seats &&
162
+ typeof candidate.seats === 'object' &&
163
+ !Array.isArray(candidate.seats)) {
164
+ const seats = candidate.seats;
165
+ for (const [ref, record] of Object.entries(seats)) {
166
+ if (!isValidSeatRecord(ref, record))
167
+ return null;
168
+ }
169
+ return { version: SEATS_VERSION, seats: seats };
170
+ }
171
+ }
172
+ return null;
173
+ }
174
+ async function readStore() {
175
+ const raw = await readStoreFile(SEATS_FILE);
176
+ // CR4 fail-closed: ENOENT alone initializes empty. A present-but-malformed /
177
+ // wrong-version / schema-invalid store MUST NOT read as empty (a following commit
178
+ // would erase every seat); throw so the on-disk bytes are preserved.
179
+ if (raw === null)
180
+ return emptyStore();
181
+ const parsed = parseStore(raw);
182
+ if (parsed === null) {
183
+ throw new Error('Borg seat store is malformed or has an unsupported version; refusing to read it');
184
+ }
185
+ return parsed;
186
+ }
187
+ /** True iff `record` is a well-formed seat for `ref` + `binding` (origin/trust/cube). */
188
+ function recordMatches(record, ref, binding) {
189
+ return (record !== undefined &&
190
+ record.origin === binding.origin &&
191
+ record.trustIdentity === binding.trustIdentity &&
192
+ record.cubeId === binding.cubeId &&
193
+ (record.state === 'pending' || record.state === 'active') &&
194
+ typeof record.credential === 'string' &&
195
+ seatRef(record) === ref);
196
+ }
197
+ /**
198
+ * TYPED, token-safe observation of the record at `ref` (CR#3). Lock-free read
199
+ * (atomic rename → complete file); the authoritative delete/activate re-reads
200
+ * under the flock. Returns only a digest + drone id, never the raw bearer.
201
+ */
202
+ export async function observeSeat(ref, binding) {
203
+ if (!REF_RE.test(ref))
204
+ return { state: 'absent' };
205
+ const store = await readStore();
206
+ const record = store.seats[ref];
207
+ if (!recordMatches(record, ref, binding))
208
+ return { state: 'absent' };
209
+ const digest = digestOf(record.credential);
210
+ if (record.state === 'active') {
211
+ return { state: 'active', digest, droneId: typeof record.droneId === 'string' ? record.droneId : '' };
212
+ }
213
+ return { state: 'pending', digest };
214
+ }
215
+ /**
216
+ * The SOLE raw-bearer reader (SR-seven #5). Returns the ACTIVE bearer for `ref`
217
+ * iff the record is active AND the binding matches — used only to hydrate the
218
+ * live seat for authenticated requests. Every other caller observes digest-only.
219
+ */
220
+ export async function getActiveSeatCredential(ref, binding) {
221
+ if (!REF_RE.test(ref))
222
+ return null;
223
+ const store = await readStore();
224
+ const record = store.seats[ref];
225
+ if (!recordMatches(record, ref, binding) || record.state !== 'active')
226
+ return null;
227
+ return record.credential;
228
+ }
229
+ /** Exact ACTIVE record for the internal continuity coordinator. */
230
+ export async function getActiveSeat(ref, binding) {
231
+ if (!REF_RE.test(ref))
232
+ return null;
233
+ const store = await readStore();
234
+ const record = store.seats[ref];
235
+ return recordMatches(record, ref, binding) && record.state === 'active' ? record : null;
236
+ }
237
+ // ─── PREPARE / mint (no worktree yet) ────────────────────────────────────────
238
+ /**
239
+ * Mint the client bearer for one seat, or return the existing record (pending or
240
+ * active) so a lost-response retry re-sends the identical bearer. The minted
241
+ * record has NO worktree binding — FINALIZE adds it atomically with activation,
242
+ * so a pending record is never a live binding.
243
+ */
244
+ export async function mintPendingSeat(input) {
245
+ const ref = seatRef(input);
246
+ return withStore(SEATS_FILE, emptyStore, parseStore, async (txn) => {
247
+ const existing = txn.data.seats[ref];
248
+ if (recordMatches(existing, ref, { origin: input.origin, trustIdentity: input.trustIdentity, cubeId: input.cubeId })) {
249
+ return existing;
250
+ }
251
+ const record = {
252
+ origin: input.origin,
253
+ trustIdentity: input.trustIdentity,
254
+ cubeId: input.cubeId,
255
+ roleId: input.roleId,
256
+ operation: input.operation,
257
+ credential: input.credential,
258
+ state: 'pending',
259
+ };
260
+ txn.data.seats[ref] = record;
261
+ await txn.commit();
262
+ return record;
263
+ });
264
+ }
265
+ /**
266
+ * CR#1 PREPARE-time abort in the single-store model. Under ONE store flock:
267
+ * REVALIDATE the typed expectation against the record currently at the seat ref
268
+ * (EXACT: the exact prior ACTIVE record must still hold — ref, optional drone id,
269
+ * optional live-bearer digest; ABSENT: no ACTIVE record may hold this seat), then
270
+ * — only if it holds — MINT the pending record in the SAME lock hold. A pre-existing
271
+ * valid record (pending from a lost-response retry, or the active record being
272
+ * reattached) is REUSED so the identical bearer is re-sent. `scrubBeforeMint`
273
+ * discards a known-invalid saved record (eviction remint) before minting, still
274
+ * under the one flock. On a mismatch NOTHING is minted or scrubbed (abort).
275
+ */
276
+ export async function prepareSeat(input) {
277
+ const { expected, revalidate = true, scrubBeforeMint = false, seed } = input;
278
+ const ref = seatRef(seed);
279
+ const binding = { origin: seed.origin, trustIdentity: seed.trustIdentity, cubeId: seed.cubeId };
280
+ return withStore(SEATS_FILE, emptyStore, parseStore, async (txn) => {
281
+ if (revalidate) {
282
+ const prior = txn.data.seats[ref];
283
+ let mismatch;
284
+ if (expected.kind === 'exact') {
285
+ const holds = recordMatches(prior, ref, binding) &&
286
+ prior.state === 'active' &&
287
+ ref === expected.credentialRef &&
288
+ (expected.droneId === undefined || prior.droneId === expected.droneId) &&
289
+ (expected.sessionDigest === undefined || digestOf(prior.credential) === expected.sessionDigest);
290
+ mismatch = !holds;
291
+ }
292
+ else {
293
+ // ABSENT: an ACTIVE record holding this seat is a mismatch. A PENDING record
294
+ // (a lost-response retry / crash-in-gap) is NOT a live binding and is reused
295
+ // below so the identical bearer is re-sent.
296
+ mismatch = recordMatches(prior, ref, binding) && prior.state === 'active';
297
+ }
298
+ if (mismatch)
299
+ return { ok: false, reason: 'expectation-mismatch' };
300
+ }
301
+ if (scrubBeforeMint) {
302
+ delete txn.data.seats[ref];
303
+ }
304
+ const existing = txn.data.seats[ref];
305
+ if (recordMatches(existing, ref, binding)) {
306
+ // Idempotent reuse: re-send the exact bearer the server already digest-bound
307
+ // (a lost-response retry, a crash-in-gap pending, or an active reattach).
308
+ return { ok: true, record: existing };
309
+ }
310
+ const record = {
311
+ origin: seed.origin,
312
+ trustIdentity: seed.trustIdentity,
313
+ cubeId: seed.cubeId,
314
+ roleId: seed.roleId,
315
+ operation: seed.operation,
316
+ credential: seed.credential,
317
+ state: 'pending',
318
+ };
319
+ txn.data.seats[ref] = record;
320
+ await txn.commit();
321
+ return { ok: true, record };
322
+ });
323
+ }
324
+ /**
325
+ * ATOMIC compare-and-activate + bind (CR#2 + the scope-A collapse). Under ONE
326
+ * flock: the exact pending/active record whose bearer digest matches
327
+ * `expectedPendingDigest` is stamped ACTIVE with the server metadata AND the
328
+ * worktree binding + display, in a single commit. A same-ref replacement or a
329
+ * missing record fails closed (`replaced`/`missing`) — server metadata for bearer
330
+ * A is never bound onto bearer B, and the digest guard holds even on the
331
+ * no-expectation-digest paths (the SENT bearer's digest is always pinned).
332
+ * Because `state:'active'` and `worktree` land together, ACTIVE-without-binding
333
+ * is unreachable by construction.
334
+ */
335
+ export async function activateAndBindSeat(input) {
336
+ if (!UUID_RE.test(input.droneId) || !UUID_RE.test(input.sessionId)) {
337
+ throw new Error('invalid Borg server session identity');
338
+ }
339
+ if (typeof input.expiresAt !== 'string' || !Number.isFinite(Date.parse(input.expiresAt))) {
340
+ throw new Error('invalid Borg server session expiry');
341
+ }
342
+ const ref = seatRef(input);
343
+ const binding = { origin: input.origin, trustIdentity: input.trustIdentity, cubeId: input.cubeId };
344
+ return withStore(SEATS_FILE, emptyStore, parseStore, async (txn) => {
345
+ const record = txn.data.seats[ref];
346
+ if (!recordMatches(record, ref, binding))
347
+ return 'missing';
348
+ if (digestOf(record.credential) !== input.expectedPendingDigest)
349
+ return 'replaced';
350
+ txn.data.seats[ref] = {
351
+ ...record,
352
+ state: 'active',
353
+ droneId: input.droneId,
354
+ sessionId: input.sessionId,
355
+ expiresAt: input.expiresAt,
356
+ worktree: input.worktree,
357
+ name: input.name,
358
+ droneLabel: input.droneLabel,
359
+ ...(input.roleName !== undefined ? { roleName: input.roleName } : {}),
360
+ ...(input.roleClass !== undefined ? { roleClass: input.roleClass } : {}),
361
+ ...(input.isHumanSeat !== undefined ? { isHumanSeat: input.isHumanSeat } : {}),
362
+ };
363
+ await txn.commit();
364
+ return 'activated';
365
+ });
366
+ }
367
+ /**
368
+ * Preserve the exact ACTIVE seat while preparing one durable replacement bearer.
369
+ * A lost response or process restart reuses the stored replacement; a changed
370
+ * authority, drone, or active bearer fails before any mutation.
371
+ */
372
+ export async function prepareSeatReplacement(input) {
373
+ if (!REF_RE.test(input.ref) || !/^[A-Za-z0-9_-]{43}$/.test(input.replacementCredential)) {
374
+ return { ok: false, reason: 'expectation-mismatch' };
375
+ }
376
+ return withStore(SEATS_FILE, emptyStore, parseStore, async (txn) => {
377
+ const record = txn.data.seats[input.ref];
378
+ if (!recordMatches(record, input.ref, input.binding) ||
379
+ record.state !== 'active' ||
380
+ record.droneId !== input.expectedDroneId ||
381
+ digestOf(record.credential) !== input.expectedActiveDigest) {
382
+ return { ok: false, reason: 'expectation-mismatch' };
383
+ }
384
+ if (record.replacement) {
385
+ return {
386
+ ok: true,
387
+ credential: record.replacement.credential,
388
+ digest: digestOf(record.replacement.credential),
389
+ };
390
+ }
391
+ record.replacement = { credential: input.replacementCredential };
392
+ await txn.commit();
393
+ return {
394
+ ok: true,
395
+ credential: input.replacementCredential,
396
+ digest: digestOf(input.replacementCredential),
397
+ };
398
+ });
399
+ }
400
+ /** Update only the committed expiry for an exact same-bearer session renewal. */
401
+ export async function refreshActiveSeatSession(input) {
402
+ if (!UUID_RE.test(input.expectedDroneId) || !UUID_RE.test(input.expectedSessionId)) {
403
+ throw new Error('invalid Borg server session identity');
404
+ }
405
+ if (!Number.isFinite(Date.parse(input.expiresAt))) {
406
+ throw new Error('invalid Borg server session expiry');
407
+ }
408
+ return withStore(SEATS_FILE, emptyStore, parseStore, async (txn) => {
409
+ const record = txn.data.seats[input.ref];
410
+ if (!recordMatches(record, input.ref, input.binding) ||
411
+ record.state !== 'active' ||
412
+ record.droneId !== input.expectedDroneId ||
413
+ record.sessionId !== input.expectedSessionId ||
414
+ digestOf(record.credential) !== input.expectedActiveDigest) {
415
+ return false;
416
+ }
417
+ record.expiresAt = input.expiresAt;
418
+ await txn.commit();
419
+ return true;
420
+ });
421
+ }
422
+ /** Atomically promote only the response-bound replacement onto the same ACTIVE seat. */
423
+ export async function promoteSeatReplacement(input) {
424
+ if (!UUID_RE.test(input.expectedDroneId) || !UUID_RE.test(input.sessionId)) {
425
+ throw new Error('invalid Borg server session identity');
426
+ }
427
+ if (!Number.isFinite(Date.parse(input.expiresAt))) {
428
+ throw new Error('invalid Borg server session expiry');
429
+ }
430
+ return withStore(SEATS_FILE, emptyStore, parseStore, async (txn) => {
431
+ const record = txn.data.seats[input.ref];
432
+ if (!recordMatches(record, input.ref, input.binding) || record.state !== 'active')
433
+ return 'missing';
434
+ if (record.droneId !== input.expectedDroneId ||
435
+ digestOf(record.credential) !== input.expectedActiveDigest ||
436
+ !record.replacement ||
437
+ digestOf(record.replacement.credential) !== input.expectedReplacementDigest) {
438
+ return 'replaced';
439
+ }
440
+ const { replacement, ...active } = record;
441
+ txn.data.seats[input.ref] = {
442
+ ...active,
443
+ credential: replacement.credential,
444
+ sessionId: input.sessionId,
445
+ expiresAt: input.expiresAt,
446
+ };
447
+ await txn.commit();
448
+ return 'promoted';
449
+ });
450
+ }
451
+ /** Remove only the caller's exact pending replacement, never the ACTIVE seat. */
452
+ export async function scrubSeatReplacement(input) {
453
+ return withStore(SEATS_FILE, emptyStore, parseStore, async (txn) => {
454
+ const record = txn.data.seats[input.ref];
455
+ if (!recordMatches(record, input.ref, input.binding) ||
456
+ record.state !== 'active' ||
457
+ digestOf(record.credential) !== input.expectedActiveDigest ||
458
+ !record.replacement ||
459
+ digestOf(record.replacement.credential) !== input.expectedReplacementDigest) {
460
+ return false;
461
+ }
462
+ delete record.replacement;
463
+ await txn.commit();
464
+ return true;
465
+ });
466
+ }
467
+ /**
468
+ * CR#2: bind an existing PENDING record to a worktree WITHOUT activating it. On a
469
+ * SIBLING attach whose atomic activate+bind failed, the spawned worktree is
470
+ * preserved for an operator-driven rerun (CR#5). This stamps the worktree locator +
471
+ * display (and, if known, the drone id) onto the EXACT digest-matched PENDING record
472
+ * so the rerun FROM that worktree can DISCOVER and RESUME it — re-sending the
473
+ * identical bearer the server already digest-bound, converging on the SAME seat (no
474
+ * ghost). The record STAYS `state:'pending'`: it is non-hydratable as a live seat
475
+ * (getActiveSeatForWorktree still requires state==='active'), so this introduces no
476
+ * ACTIVE-without-activation state. Under ONE store flock (CR#3), fail-closed (CR#4):
477
+ * a missing / already-ACTIVE / same-ref-replaced record is a typed no-op — never
478
+ * binds a worktree onto a different bearer, never mutates an ACTIVE record.
479
+ */
480
+ export async function bindPendingSeatToWorktree(input) {
481
+ if (input.droneId !== undefined && !UUID_RE.test(input.droneId)) {
482
+ throw new Error('invalid Borg server session identity');
483
+ }
484
+ const ref = seatRef(input);
485
+ const binding = { origin: input.origin, trustIdentity: input.trustIdentity, cubeId: input.cubeId };
486
+ return withStore(SEATS_FILE, emptyStore, parseStore, async (txn) => {
487
+ const record = txn.data.seats[ref];
488
+ if (!recordMatches(record, ref, binding))
489
+ return 'missing';
490
+ // Never touch an ACTIVE record, and never bind a worktree onto a same-ref
491
+ // replacement — the record's live digest must still equal the sent bearer's.
492
+ if (record.state !== 'pending' || digestOf(record.credential) !== input.expectedPendingDigest) {
493
+ return 'replaced';
494
+ }
495
+ txn.data.seats[ref] = {
496
+ ...record,
497
+ // state STAYS 'pending' — NOT activated. Only the worktree locator + display
498
+ // (+ drone id) land, so the preserved worktree owns a discoverable, resumable
499
+ // pending record without becoming a live/hydratable binding.
500
+ worktree: input.worktree,
501
+ name: input.name,
502
+ droneLabel: input.droneLabel,
503
+ ...(input.droneId !== undefined ? { droneId: input.droneId } : {}),
504
+ ...(input.roleName !== undefined ? { roleName: input.roleName } : {}),
505
+ ...(input.roleClass !== undefined ? { roleClass: input.roleClass } : {}),
506
+ ...(input.isHumanSeat !== undefined ? { isHumanSeat: input.isHumanSeat } : {}),
507
+ };
508
+ await txn.commit();
509
+ return 'bound';
510
+ });
511
+ }
512
+ // ─── Hydration / enumeration (scan by worktree) ──────────────────────────────
513
+ /** The exact ACTIVE seat bound to `worktree`, or null. A pending record (no
514
+ * worktree, or non-active) is NEVER surfaced as a live binding. */
515
+ export async function getActiveSeatForWorktree(worktree) {
516
+ const store = await readStore();
517
+ for (const [ref, record] of Object.entries(store.seats)) {
518
+ if (record.state === 'active' && record.worktree === worktree && seatRef(record) === ref) {
519
+ return record;
520
+ }
521
+ }
522
+ return null;
523
+ }
524
+ /**
525
+ * CR#2: the seat bound to `worktree` regardless of state — an ACTIVE seat OR a
526
+ * bound-PENDING record (a sibling whose activation failed, bound to its preserved
527
+ * worktree by `bindPendingSeatToWorktree`). Used ONLY by the resume path to recover
528
+ * a bound-pending record's stored `operation` + `state` so the rerun can re-derive
529
+ * the EXACT seat ref and re-send the identical bearer. A bound-pending record is
530
+ * still NON-hydratable as a live seat — `getActiveSeatForWorktree` (and hence
531
+ * `getActiveCube`) still require state==='active', so this reader does NOT weaken
532
+ * the no-ACTIVE-without-binding / non-hydratable-pending invariants.
533
+ */
534
+ export async function getSeatForWorktree(worktree) {
535
+ const store = await readStore();
536
+ for (const [ref, record] of Object.entries(store.seats)) {
537
+ if (record.worktree === worktree && seatRef(record) === ref) {
538
+ return record;
539
+ }
540
+ }
541
+ return null;
542
+ }
543
+ /**
544
+ * CR#3: find an in-flight IMPLICIT-sibling attempt for `binding` — a PENDING,
545
+ * kind==='sibling' record whose operation.projectRoot is the source repo and which
546
+ * has NO worktree binding yet (unbound). Such a record is the persisted, collision-
547
+ * safe attempt identity left when a crash struck AFTER the server accepted the attach
548
+ * but BEFORE the worktree bind: its per-invocation-unique operationKey would otherwise
549
+ * be undiscoverable, so a rerun would mint a NEW bearer and the server (digest-
550
+ * correlating) would create a GHOST seat. Recovering it lets the rerun re-derive the
551
+ * EXACT ref and re-send the identical bearer (the server reuses its seat). A BOUND
552
+ * pending sibling (already discoverable by its worktree) and an ACTIVE record are NOT
553
+ * returned — so a COMPLETED sibling frees the source-repo key and the next distinct
554
+ * sibling mints a fresh identity. Deterministic first match.
555
+ */
556
+ export async function findIncompleteSiblingAttempt(binding) {
557
+ const store = await readStore();
558
+ for (const [ref, record] of Object.entries(store.seats)) {
559
+ if (record.state === 'pending' &&
560
+ record.worktree === undefined &&
561
+ record.operation.kind === 'sibling' &&
562
+ record.origin === binding.origin &&
563
+ record.trustIdentity === binding.trustIdentity &&
564
+ record.cubeId === binding.cubeId &&
565
+ record.operation.projectRoot === binding.projectRoot &&
566
+ seatRef(record) === ref) {
567
+ return record;
568
+ }
569
+ }
570
+ return null;
571
+ }
572
+ /** True iff this worktree has ANY persisted seat record (active OR a bound
573
+ * pending), so a crash-in-gap PENDING seat is discoverable (not mislabeled). */
574
+ export async function hasSeatForWorktree(worktree) {
575
+ const store = await readStore();
576
+ return Object.values(store.seats).some((r) => r.worktree === worktree);
577
+ }
578
+ /** All ACTIVE bound seats — {worktree, record}. */
579
+ export async function readAllActiveSeats() {
580
+ const store = await readStore();
581
+ const out = [];
582
+ for (const [ref, record] of Object.entries(store.seats)) {
583
+ if (record.state === 'active' && typeof record.worktree === 'string' && seatRef(record) === ref) {
584
+ out.push({ worktree: record.worktree, record });
585
+ }
586
+ }
587
+ return out;
588
+ }
589
+ /**
590
+ * Reset the seat bound to `worktree`: under ONE flock, re-check the exact FULL
591
+ * binding (ref + drone id) and the token-safe observation, then DELETE the whole
592
+ * record — credential AND binding vanish together in one commit (no 'partial',
593
+ * no cross-store skew). Any drift (missing / different ref or drone / same-ref
594
+ * digest replacement) is an honest no-op.
595
+ */
596
+ export async function resetSeatForWorktree(expected) {
597
+ return withStore(SEATS_FILE, emptyStore, parseStore, async (txn) => {
598
+ const record = txn.data.seats[expected.ref];
599
+ if (record === undefined ||
600
+ record.worktree !== expected.worktree ||
601
+ record.droneId !== expected.droneId ||
602
+ seatRef(record) !== expected.ref) {
603
+ // Look for whether ANY record still binds this worktree — if not, no-op.
604
+ const anyBound = Object.values(txn.data.seats).some((r) => r.worktree === expected.worktree);
605
+ return anyBound ? { outcome: 'changed' } : { outcome: 'no-binding' };
606
+ }
607
+ // Same-ref replacement guard: the live digest must still match the snapshot.
608
+ const expDigest = expected.observation.state === 'absent' ? null : expected.observation.digest;
609
+ if (expDigest !== null && digestOf(record.credential) !== expDigest) {
610
+ return { outcome: 'changed' };
611
+ }
612
+ delete txn.data.seats[expected.ref];
613
+ await txn.commit();
614
+ return { outcome: 'reset', ref: expected.ref };
615
+ });
616
+ }
617
+ /** Abort-scrub of the caller's OWN pending record only (CR#2 finalize abort):
618
+ * delete iff still pending AND digest matches — never an active record, never a
619
+ * same-ref replacement. */
620
+ export async function scrubPendingSeat(ref, binding, expectedPendingDigest) {
621
+ if (!REF_RE.test(ref))
622
+ return false;
623
+ return withStore(SEATS_FILE, emptyStore, parseStore, async (txn) => {
624
+ const record = txn.data.seats[ref];
625
+ if (!recordMatches(record, ref, binding) || record.state !== 'pending')
626
+ return false;
627
+ if (digestOf(record.credential) !== expectedPendingDigest)
628
+ return false;
629
+ delete txn.data.seats[ref];
630
+ await txn.commit();
631
+ return true;
632
+ });
633
+ }
634
+ /** Discard any record for one seat ref (eviction remint before a fresh mint). */
635
+ export async function clearSeat(ref) {
636
+ if (!REF_RE.test(ref))
637
+ throw new Error('invalid Borg server session credential reference');
638
+ await withStore(SEATS_FILE, emptyStore, parseStore, async (txn) => {
639
+ if (txn.data.seats[ref] !== undefined) {
640
+ delete txn.data.seats[ref];
641
+ await txn.commit();
642
+ }
643
+ });
644
+ }
645
+ /** Metadata-only refresh of one exact ACTIVE seat. The expected tuple prevents
646
+ * a delayed response from updating a replacement seat that reused the worktree. */
647
+ export async function refreshSeatMetadata(worktree, expected, display) {
648
+ if (!REF_RE.test(expected.credentialRef))
649
+ return false;
650
+ return withStore(SEATS_FILE, emptyStore, parseStore, async (txn) => {
651
+ const record = txn.data.seats[expected.credentialRef];
652
+ if (record?.state !== 'active' || record.worktree !== worktree ||
653
+ record.cubeId !== expected.cubeId || record.droneId !== expected.droneId ||
654
+ seatRef(record) !== expected.credentialRef)
655
+ return false;
656
+ txn.data.seats[expected.credentialRef] = {
657
+ ...record,
658
+ name: display.name,
659
+ droneLabel: display.droneLabel,
660
+ ...(display.roleName !== undefined ? { roleName: display.roleName } : {}),
661
+ ...(display.roleClass !== undefined ? { roleClass: display.roleClass } : {}),
662
+ ...(display.isHumanSeat !== undefined ? { isHumanSeat: display.isHumanSeat } : {}),
663
+ };
664
+ await txn.commit();
665
+ return true;
666
+ });
667
+ }
668
+ /** @internal Test-only: point the store at a fixture path is done via HOME; this
669
+ * clears the module cache (there is none — every op reads fresh under the lock). */
670
+ //# sourceMappingURL=seats.js.map