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
@@ -1 +1,942 @@
1
- import{getIdToken as f,getRefreshToken as m,clearTokens as h}from"./config.js";import{refreshIdToken as S,RefreshTokenInvalidError as g,RefreshTransientError as T}from"./auth.js";import{consolePrefix as j}from"./console-prefix.js";import{debugLog as E}from"./debug.js";import{assertUuidShape as x}from"./evict-drone.js";import{DroneEvictedError as k,DroneFrozenError as C,DRONE_EVICTED_CODE as P,DRONE_FROZEN_CODE as O,errorCodeFromBody as I}from"./drone-lifecycle.js";import{canonicalizeWorkingRepoIdentity as D}from"./working-repo.js";const L=/^(claude|ollama):[A-Za-z0-9._:\/-]+$/,A=process.env.BORG_API_URL||"https://api.borgmcp.ai",G=3,J=6e4;let l=null;function _(e){return l||(l=S(e).finally(()=>{l=null}),l)}function M(e){if(e==null)return null;const t=e.trim();return/^\d+$/.test(t)?parseInt(t,10)*1e3:null}function v(e,t,n=J,o=()=>Math.random()*500){const r=e??1e3*(t+1);return Math.min(r,n)+o()}function U(e){const t=(n,o)=>`${n}${/[.:!?]$/.test(n)?"":":"} ${o}`;try{const n=JSON.parse(e);if(typeof n?.error=="string")return typeof n.details=="string"?t(n.error,n.details):n.error;if(n?.error&&typeof n.error=="object"){const o=n.error.message,r=n.error.details??n.details;if(typeof o=="string"&&typeof r=="string")return t(o,r);if(typeof o=="string")return o}if(typeof n?.message=="string"&&typeof n?.details=="string")return t(n.message,n.details);if(typeof n?.message=="string")return n.message}catch{}return e}async function N(e,t,n){const o=n.maxRetries??G;let r=e,i=0;for(;r.status===429&&i<o;){const p=v(M(r.headers.get("Retry-After")),i,n.capMs,n.jitter);n.log?.(`rate limited (429); retrying in ${Math.round(p)}ms (attempt ${i+1}/${o})`),await n.sleep(p),i++,r=await t()}return r}function q(e){return new Promise(t=>setTimeout(t,e))}async function B(){let e=await f();if(!e){const t=await m(),n=t!=null;if(t)try{await _(t),e=await f()}catch(o){if(o instanceof g)await h();else throw o instanceof T?o:new T(`Token refresh failed unexpectedly (your saved login was NOT cleared \u2014 retry; if it persists, restart the borg session): ${o?.message??"unknown"}`)}if(!e)throw new Error(n?"Authentication expired \u2014 your saved login has expired. Run: borg setup":"Authentication required \u2014 you are not signed in. Run: borg setup")}return e}async function F(){const e=await m();if(!e)return null;try{return await _(e),await f()}catch(t){if(t instanceof g&&await h(),t instanceof T)throw t;return null}}async function Q(){if(await f())return"valid";const t=await m();if(!t)return"dead";try{return await _(t),await f()?"valid":"transient"}catch(n){return n instanceof g?(await h(),"dead"):"transient"}}async function s(e,t={}){let n=await B();const{droneSession:o,apiUrl:r,headers:i,...p}=t,$=r??A,y=(p.method??"GET").toUpperCase(),w=async c=>{const d={Authorization:`Bearer ${c}`,...i};o&&(d["X-Drone-Session"]=o),E(`\u2192 ${y} ${e}`);const u=await fetch(`${$}${e}`,{...p,headers:d});return E(`\u2190 ${u.status} ${y} ${e}`),u};let a=await w(n);if(a.status===401){const c=await F();c&&(n=c,a=await w(n))}if(a.status===401)throw new Error("Authentication required. Run: borg setup");if(a.status===429&&(a=await N(a,()=>w(n),{sleep:q,log:c=>console.error(`${j()}${c}`)})),!a.ok){const c=await a.text();E(`\u2717 ${a.status} ${y} ${e}: ${c}`);const d=U(c),u=I(c);if(a.status===410&&u===P)throw new k(d);if(a.status===423&&u===O)throw new C(d);if(a.status===429){const b=a.headers.get("Retry-After"),R=b?` (retry after ${b}s)`:"";throw new Error(`HTTP 429: rate limited${R}: ${d}`)}throw new Error(`HTTP ${a.status}: ${d}`)}return a}async function K(e,t,n,o){const r={hostname:n??null};if((o==="claude"||o==="codex"||o==="opencode")&&(r.agent_kind=o),typeof e=="string")r.cube_name=e;else if(e.cube_id&&(r.cube_id=e.cube_id),e.cube_name&&(r.cube_name=e.cube_name),e.role_id&&(r.role_id=e.role_id),e.role_name&&(r.role_name=e.role_name),e.prior_drone_id&&(r.prior_drone_id=e.prior_drone_id),e.model!=null){if(!L.test(e.model))throw new Error(`Invalid model descriptor: "${e.model}" (expected "<claude|ollama>:<model>").`);r.model=e.model}return await(await s("/api/assimilate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r),apiUrl:t})).json()}async function ee(e,t){return await(await s("/api/drone/cube",{method:"GET",droneSession:e,apiUrl:t})).json()}async function ne(e,t){return await(await s("/api/drone/role",{method:"GET",droneSession:e,apiUrl:t})).json()}async function te(e,t,n){const o=new URLSearchParams({role:n});return await(await s(`/api/drone/role?${o.toString()}`,{method:"GET",droneSession:e,apiUrl:t})).json()}async function oe(e,t){return await(await s("/api/drone/whoami",{method:"GET",droneSession:e,apiUrl:t})).json()}async function re(e,t,n){const o=n?`?since=${encodeURIComponent(n)}`:"";return await(await s(`/api/drone/roster${o}`,{method:"GET",droneSession:e,apiUrl:t})).json()}async function se(e,t,n={}){const o=new URLSearchParams;n.since&&o.set("since",n.since),n.limit!==void 0&&o.set("limit",String(n.limit)),n.unreadOnly&&o.set("unread_only","true");const r=o.toString();return await(await s(`/api/drone/log${r?`?${r}`:""}`,{method:"GET",droneSession:e,apiUrl:t})).json()}async function ie(e,t,n,o="ack"){await s(`/api/drone/log/${n}/ack`,{method:"POST",body:JSON.stringify({kind:o}),droneSession:e,apiUrl:t})}async function ae(e,t,n){return await(await s("/api/drone/decide",{method:"POST",body:JSON.stringify(n),droneSession:e,apiUrl:t})).json()}async function ce(e,t,n){const o=n?`?topic=${encodeURIComponent(n)}`:"";return await(await s(`/api/drone/decisions${o}`,{method:"GET",droneSession:e,apiUrl:t})).json()}async function pe(e,t,n){return await(await s("/api/drone/decisions",{method:"DELETE",body:JSON.stringify(n),droneSession:e,apiUrl:t})).json()}async function de(e,t,n={}){const o=new URLSearchParams;if(n.since&&o.set("since",n.since),n.reportedModel&&o.set("reported_model",n.reportedModel),n.workingRepo){o.set("working_repo_reported","1");const p=n.workingRepo.origin?D(n.workingRepo.origin):null;p&&o.set("working_repo_origin",p)}const r=o.toString();return await(await s(`/api/drone/regen${r?`?${r}`:""}`,{method:"GET",droneSession:e,apiUrl:t})).json()}async function ue(e,t,n,o){const r=new URLSearchParams({role:n,section:o});return await(await s(`/api/drone/role-rationale?${r.toString()}`,{method:"GET",droneSession:e,apiUrl:t})).json()}async function fe(e,t,n,o={}){const r={message:n,...o.visibility?{visibility:o.visibility}:{},...o.recipientDroneIds?{recipientDroneIds:o.recipientDroneIds}:{},...o.class?{class:o.class}:{},...o.to?{to:o.to}:{}};return await(await s("/api/drone/log",{method:"POST",headers:{"Content-Type":"application/json"},droneSession:e,apiUrl:t,body:JSON.stringify(r)})).json()}async function le(e,t,n){const o={kind:n.kind??"friction",message:n.message,...n.metadata?{metadata:n.metadata}:{}};return await(await s("/api/drone/report",{method:"POST",headers:{"Content-Type":"application/json"},droneSession:e,apiUrl:t,body:JSON.stringify(o)})).json()}async function ye(){const e=await s("/api/reports",{method:"GET"});if(e.status===403)return{forbidden:!0};if(!e.ok)throw new Error(`Failed to fetch reports: ${e.status}`);return{forbidden:!1,reports:(await e.json()).reports}}async function we(){return await(await s("/api/cubes",{method:"GET"})).json()}async function me(){return await(await s("/api/templates",{method:"GET"})).json()}async function he(e,t,n){const o={cube_directive:t};e&&(o.name=e),n?.template&&(o.template=n.template),n&&Object.prototype.hasOwnProperty.call(n,"message_taxonomy")&&(o.message_taxonomy=n.message_taxonomy??null);const i=await(await s("/api/cubes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)})).json();return i.cube?{...i.cube,roles:i.roles??[],drones:i.drones??[]}:i}async function ge(e,t){return await(await s(`/api/cubes/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Te(e,t){return await(await s(`/api/cubes/${e}/taxonomy-patch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Ee(e){await s(`/api/cubes/${e}`,{method:"DELETE"})}async function _e(e,t){return await(await s(`/api/cubes/${e}/roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function be(e,t){return await(await s(`/api/roles/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function xe(e,t){return await(await s(`/api/roles/${e}/section-patch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function $e(e){await s(`/api/roles/${e}`,{method:"DELETE"})}async function Re(e,t){return x(e,"drone_id"),await(await s(`/api/drones/${e}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({role_id:t})})).json()}async function Se(e){x(e,"drone_id"),await s(`/api/drones/${e}`,{method:"DELETE"})}async function je(e){const n=await(await s(`/api/cubes/${e}`,{method:"GET"})).json();return n.cube?{...n.cube,roles:n.roles??[],drones:n.drones??[]}:n}async function ke(e,t){return await(await s(`/api/cubes/${e}/apply-template`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({template_name:t})})).json()}async function Ce(){return await(await s("/api/subscription/status",{method:"GET"})).json()}async function Pe(e,t="software-dev",n=!1,o){return await(await s(`/api/cubes/${e}/sync-roles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({template_name:t,apply:n,...o?{decisions:o}:{}})})).json()}async function Oe(){const t=await(await s("/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"}})).json();if(!t.checkout_url)throw new Error("No checkout URL in response");return t.checkout_url}async function Ie(){const t=await(await s("/api/subscription/portal",{method:"POST",headers:{"Content-Type":"application/json"}})).json();if(!t.portal_url)throw new Error(t.message||"No portal URL in response");return t.portal_url}export{A as API_URL,ie as ackLogEntry,fe as appendLog,ke as applyTemplate,K as assimilate,Ce as checkSubscriptionStatus,Ie as createBillingPortalSession,he as createCube,_e as createRole,Oe as createSubscription,Ee as deleteCube,$e as deleteRole,Se as evictDrone,U as extractHttpErrorMessage,ye as fetchReports,je as getCube,ee as getCubeInfo,ne as getRoleInfo,te as getRoleInfoByName,re as getRoster,B as getValidToken,we as listCubes,ce as listDecisions,me as listTemplates,M as parseRetryAfterMs,xe as patchRoleSection,Te as patchTaxonomyClass,Q as probeSession,v as rateLimitWaitMs,se as readLog,Re as reassignDrone,ae as recordDecision,de as regen,pe as removeDecision,N as retryOn429,ue as roleRationale,le as submitReport,Pe as syncRoles,ge as updateCube,be as updateRole,oe as whoami};
1
+ /**
2
+ * HTTP client for a verified local (self-hosted) Borg server.
3
+ *
4
+ * Handles:
5
+ * - Pinned-TLS requests to the selected local server
6
+ * - Drone-session / enrollment-credential injection
7
+ * - Network failure handling with retry + exponential backoff
8
+ *
9
+ * There is no hosted-authority path: every request must carry verified local
10
+ * server trust or it fails closed before any network or credential use.
11
+ */
12
+ import { getServerCredential, } from './config.js';
13
+ import { randomUUID } from 'node:crypto';
14
+ import { createProtocolEnvelope, decodeEvictDroneResult, decodeProtocolEnvelope, decodeProtocolErrorEnvelope, decodeReassignDroneResult, ErrorCode, } from 'borgmcp-shared/protocol';
15
+ import { debugLog } from './debug.js';
16
+ import { assertUuidShape } from './evict-drone.js';
17
+ import { DroneEvictedError, DRONE_EVICTED_CODE } from './drone-lifecycle.js';
18
+ import { loadBorgServerTrust } from './server-trust.js';
19
+ import { BorgServerError, BorgServerHttpError, BorgServerTrustError, BorgServerUnreachableError, LocalManageCredentialUnavailableError, LocalManageRequiredError, } from './server-errors.js';
20
+ import { getActiveCube } from './cubes.js';
21
+ import { advanceLocalServerCursor, getLocalServerCursor, } from './local-server-cursor.js';
22
+ import { readBoundedResponseBody } from './server-response.js';
23
+ import { resolveLocalLogRecipients } from './local-log-routing.js';
24
+ // gh#330: honor the server's Retry-After on 429 instead of failing the
25
+ // (often required) coordination signal outright. Bounded so a CLI call
26
+ // never blocks unboundedly; capped per attempt so a large window-reset
27
+ // retryAfter can't wedge the call.
28
+ const RATE_LIMIT_MAX_RETRIES = 3;
29
+ const RATE_LIMIT_MAX_WAIT_MS = 60_000; // cap a single Retry-After honor
30
+ export const LOCAL_SERVER_RESPONSE_LIMIT_BYTES = 32 * 1024 * 1024;
31
+ // A typed auth-error envelope is tiny; anything larger is hostile and the
32
+ // bounded read throws → the 401 fails closed to non-destructive CREDENTIAL_REJECTED.
33
+ const AUTH_ERROR_ENVELOPE_LIMIT_BYTES = 64 * 1024;
34
+ export const LOCAL_SERVER_REQUEST_TIMEOUT_MS = 5_000;
35
+ const LOCAL_SERVER_RESPONSE_LIMIT_MESSAGE = 'Local Borg server response exceeded the response limit';
36
+ /**
37
+ * Parse a `Retry-After` header (delta-seconds form, which the worker
38
+ * emits — mcp-server.ts:382/583) into milliseconds. Returns null when
39
+ * absent or not a non-negative integer count of seconds. (The HTTP-date
40
+ * form is not emitted by the worker, so it is intentionally unhandled.)
41
+ */
42
+ export function parseRetryAfterMs(headerValue) {
43
+ if (headerValue == null)
44
+ return null;
45
+ const trimmed = headerValue.trim();
46
+ if (!/^\d+$/.test(trimmed))
47
+ return null;
48
+ return parseInt(trimmed, 10) * 1000;
49
+ }
50
+ /**
51
+ * How long to wait before the next 429 retry. Honors the server's
52
+ * Retry-After when present (capped at `capMs` so a full-window reset
53
+ * can't wedge a CLI call); falls back to an escalating 1s·(attempt+1)
54
+ * when absent. Adds jitter (injected for tests) so co-located sibling
55
+ * drones sharing one per-IP bucket don't retry in lockstep.
56
+ */
57
+ export function rateLimitWaitMs(retryAfterMs, attempt, capMs = RATE_LIMIT_MAX_WAIT_MS, jitter = () => Math.random() * 500) {
58
+ const base = retryAfterMs != null ? retryAfterMs : 1000 * (attempt + 1);
59
+ return Math.min(base, capMs) + jitter();
60
+ }
61
+ /**
62
+ * Given an ALREADY-OBTAINED response, while it is a 429 and retries
63
+ * remain, wait per `rateLimitWaitMs` (honoring the CURRENT response's
64
+ * Retry-After) and THEN re-run `doRequest`. Takes `initialResponse`
65
+ * (not a first request) because the caller has already made the request
66
+ * and read its status — re-fetching first would ignore the first 429's
67
+ * Retry-After and double-fire an immediate extra request (CR blocker
68
+ * d3a564f5). Returns the last Response (200-class on success, or a final
69
+ * 429 if retries exhaust — the caller surfaces that). `sleep` is
70
+ * injected for deterministic tests; no fetch-global mocking required.
71
+ */
72
+ export async function retryOn429(initialResponse, doRequest, opts) {
73
+ const maxRetries = opts.maxRetries ?? RATE_LIMIT_MAX_RETRIES;
74
+ let response = initialResponse;
75
+ let attempt = 0;
76
+ while (response.status === 429 && attempt < maxRetries) {
77
+ // Honor THIS 429's Retry-After BEFORE issuing the next request.
78
+ const waitMs = rateLimitWaitMs(parseRetryAfterMs(response.headers.get('Retry-After')), attempt, opts.capMs, opts.jitter);
79
+ opts.log?.(`rate limited (429); retrying in ${Math.round(waitMs)}ms (attempt ${attempt + 1}/${maxRetries})`);
80
+ await opts.sleep(waitMs);
81
+ attempt++;
82
+ response = await doRequest();
83
+ }
84
+ return response;
85
+ }
86
+ async function localAuthorityContext(sessionToken, apiUrl, expectedServerTrustIdentity) {
87
+ const active = await getActiveCube();
88
+ const matched = active?.serverTrustIdentity !== undefined &&
89
+ active.apiUrl === apiUrl &&
90
+ active.sessionToken === sessionToken
91
+ ? active
92
+ : null;
93
+ if (!matched) {
94
+ throw new Error('Selected Borg server authority state is missing or unreadable');
95
+ }
96
+ assertUuidShape(matched.cubeId, 'cube_id');
97
+ assertUuidShape(matched.droneId, 'drone_id');
98
+ if (expectedServerTrustIdentity !== undefined) {
99
+ if (matched.serverTrustIdentity !== expectedServerTrustIdentity) {
100
+ throw new Error('Selected Borg server authority state is missing or unreadable');
101
+ }
102
+ return matched;
103
+ }
104
+ // Only a hydrated local ActiveCube carrying the verified trust anchor
105
+ // authorizes the request. cubes.json is mutable local state and a
106
+ // legacy-looking sessionToken proves nothing — fail closed before any
107
+ // network use when no verified local authority is present.
108
+ return matched;
109
+ }
110
+ function localUnsupported(capability) {
111
+ throw new Error(`Local Borg server does not support ${capability}`);
112
+ }
113
+ function waitForLocalRequest(promise, signal) {
114
+ if (signal.aborted)
115
+ return Promise.reject(signal.reason);
116
+ return new Promise((resolve, reject) => {
117
+ const onAbort = () => reject(signal.reason);
118
+ signal.addEventListener('abort', onAbort, { once: true });
119
+ promise.then(resolve, reject).finally(() => {
120
+ signal.removeEventListener('abort', onAbort);
121
+ });
122
+ });
123
+ }
124
+ async function decodeLocalProtocolResponse(request, allowNoContent, decodePayload = (value) => value) {
125
+ const controller = new AbortController();
126
+ const timeout = setTimeout(() => {
127
+ controller.abort(new Error('Local Borg server request timed out'));
128
+ }, LOCAL_SERVER_REQUEST_TIMEOUT_MS);
129
+ try {
130
+ const response = await waitForLocalRequest(request(controller.signal), controller.signal);
131
+ if (response.status === 204 && allowNoContent)
132
+ return null;
133
+ const encoded = await readBoundedResponseBody(response, LOCAL_SERVER_RESPONSE_LIMIT_BYTES, LOCAL_SERVER_RESPONSE_LIMIT_MESSAGE, controller.signal);
134
+ let body;
135
+ try {
136
+ body = JSON.parse(encoded);
137
+ }
138
+ catch {
139
+ throw new Error('Local Borg server returned an invalid protocol envelope');
140
+ }
141
+ return decodeProtocolEnvelope(body, decodePayload).payload;
142
+ }
143
+ catch (error) {
144
+ if (controller.signal.aborted) {
145
+ // CR5: a TYPED transport-timeout verdict (message kept for call-site parity).
146
+ throw new BorgServerUnreachableError('Local Borg server request timed out');
147
+ }
148
+ throw error;
149
+ }
150
+ finally {
151
+ clearTimeout(timeout);
152
+ }
153
+ }
154
+ async function localServerRequest(active, path, method, payload) {
155
+ return decodeLocalProtocolResponse((signal) => authedFetch(path, {
156
+ method,
157
+ signal,
158
+ droneSession: active.sessionToken,
159
+ apiUrl: active.apiUrl,
160
+ serverTrustIdentity: active.serverTrustIdentity,
161
+ redirect: 'error',
162
+ ...(payload === undefined
163
+ ? { headers: { Accept: 'application/json' } }
164
+ : {
165
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
166
+ body: JSON.stringify(createProtocolEnvelope(randomUUID(), payload)),
167
+ }),
168
+ }), true);
169
+ }
170
+ function manageCopyValue(value) {
171
+ return JSON.stringify(value);
172
+ }
173
+ async function localManageConnection(active, operation) {
174
+ const trustIdentity = active.serverTrustIdentity;
175
+ if (!trustIdentity)
176
+ throw new Error('Selected Borg server authority state is missing or unreadable');
177
+ let authToken;
178
+ try {
179
+ authToken = await getServerCredential(active.apiUrl, trustIdentity);
180
+ }
181
+ catch {
182
+ throw new LocalManageCredentialUnavailableError(operation.operation, operation.cubeName, operation.noMutation);
183
+ }
184
+ if (!authToken) {
185
+ throw new LocalManageCredentialUnavailableError(operation.operation, operation.cubeName, operation.noMutation);
186
+ }
187
+ return { apiUrl: active.apiUrl, authToken, serverTrustIdentity: trustIdentity };
188
+ }
189
+ async function localManageRequest(active, path, method, operation, payload, decodePayload) {
190
+ const connection = await localManageConnection(active, operation);
191
+ try {
192
+ return await decodeLocalProtocolResponse((signal) => authedFetch(path, {
193
+ method,
194
+ signal,
195
+ apiUrl: connection.apiUrl,
196
+ authToken: connection.authToken,
197
+ serverTrustIdentity: connection.serverTrustIdentity,
198
+ redirect: 'error',
199
+ ...(payload === undefined
200
+ ? { headers: { Accept: 'application/json' } }
201
+ : {
202
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
203
+ body: JSON.stringify(createProtocolEnvelope(randomUUID(), payload)),
204
+ }),
205
+ }), true, decodePayload);
206
+ }
207
+ catch (error) {
208
+ if (error instanceof BorgServerHttpError &&
209
+ error.status === 403 &&
210
+ error.code === ErrorCode.ACCESS_DENIED) {
211
+ throw new LocalManageRequiredError(operation.operation, operation.cubeName, operation.noMutation);
212
+ }
213
+ throw error;
214
+ }
215
+ }
216
+ async function localConnectionRequest(connection, path) {
217
+ return decodeLocalProtocolResponse((signal) => authedFetch(path, {
218
+ method: 'GET',
219
+ signal,
220
+ apiUrl: connection.apiUrl,
221
+ authToken: connection.authToken,
222
+ serverTrustIdentity: connection.serverTrustIdentity,
223
+ redirect: 'error',
224
+ headers: { Accept: 'application/json' },
225
+ }), false);
226
+ }
227
+ async function localOwnerConnection(connection) {
228
+ if (connection)
229
+ return connection;
230
+ const active = await getActiveCube();
231
+ if (!active?.serverTrustIdentity) {
232
+ throw new Error('Selected Borg server authority state is missing or unreadable');
233
+ }
234
+ const authToken = await getServerCredential(active.apiUrl, active.serverTrustIdentity);
235
+ if (!authToken)
236
+ throw new Error('No credential is stored for the selected Borg server identity');
237
+ return {
238
+ apiUrl: active.apiUrl,
239
+ authToken,
240
+ serverTrustIdentity: active.serverTrustIdentity,
241
+ };
242
+ }
243
+ async function localCubeComposition(active) {
244
+ const base = `/api/cubes/${active.cubeId}`;
245
+ const [cubePayload, rolePayload, dronePayload] = await Promise.all([
246
+ localServerRequest(active, base, 'GET'),
247
+ localServerRequest(active, `${base}/roles`, 'GET'),
248
+ localServerRequest(active, `${base}/drones`, 'GET'),
249
+ ]);
250
+ if (!cubePayload || !rolePayload || !dronePayload) {
251
+ throw new Error('Local Borg server returned an incomplete cube response');
252
+ }
253
+ const drone = dronePayload.drones.find((candidate) => candidate.id === active.droneId);
254
+ const role = rolePayload.roles.find((candidate) => candidate.id === drone?.role_id);
255
+ if (!drone || !role)
256
+ throw new Error('Local Borg server no longer recognizes this drone seat');
257
+ return {
258
+ cube: cubePayload.cube,
259
+ roles: rolePayload.roles,
260
+ drones: dronePayload.drones,
261
+ role,
262
+ drone,
263
+ };
264
+ }
265
+ function localCursorBinding(active) {
266
+ return {
267
+ origin: active.apiUrl,
268
+ trustIdentity: active.serverTrustIdentity,
269
+ cubeId: active.cubeId,
270
+ droneId: active.droneId,
271
+ };
272
+ }
273
+ async function localReadLogPage(active, opts = {}) {
274
+ const payload = await localServerRequest(active, `/api/cubes/${active.cubeId}/logs`, 'PUT', {
275
+ cursor: opts.cursor ?? null,
276
+ ...(opts.limit === undefined ? {} : { limit: opts.limit }),
277
+ });
278
+ if (!payload)
279
+ throw new Error('Local Borg server returned an empty log response');
280
+ return payload;
281
+ }
282
+ async function resolveLocalLogCursor(active, since) {
283
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
284
+ .test(since);
285
+ const timestamp = isUuid ? null : Date.parse(since);
286
+ if (!isUuid && (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== since)) {
287
+ throw new Error('Invalid local Borg server log cursor');
288
+ }
289
+ let scanCursor = null;
290
+ let timestampCursor = null;
291
+ for (;;) {
292
+ const page = await localReadLogPage(active, { cursor: scanCursor, limit: 500 });
293
+ for (const entry of page.entries) {
294
+ if (isUuid && entry.id === since) {
295
+ return { id: entry.id, created_at: entry.created_at };
296
+ }
297
+ if (!isUuid) {
298
+ if (entry.created_at > since)
299
+ return timestampCursor;
300
+ timestampCursor = { id: entry.id, created_at: entry.created_at };
301
+ }
302
+ }
303
+ if (!page.has_more || !page.cursor) {
304
+ if (isUuid)
305
+ throw new Error('Local Borg server log cursor was not found');
306
+ return timestampCursor;
307
+ }
308
+ scanCursor = page.cursor;
309
+ }
310
+ }
311
+ /**
312
+ * Sleep for specified milliseconds
313
+ */
314
+ function sleep(ms) {
315
+ return new Promise(resolve => setTimeout(resolve, ms));
316
+ }
317
+ /**
318
+ * Authenticated fetch helper.
319
+ *
320
+ * Adds the Bearer token + optional drone-session header, parses errors
321
+ * consistently, and surfaces a re-auth recovery message ("Run: borg setup")
322
+ * on auth failure (gh#780/#794 — never `borg assimilate`, which rides the same
323
+ * broken Bearer).
324
+ *
325
+ * Accepts an optional `apiUrl` override so already-assimilated callers can
326
+ * route to the worker that issued their drone session token, regardless of
327
+ * what BORG_API_URL was set to when this process started.
328
+ */
329
+ async function authedFetch(path, init = {}) {
330
+ const { droneSession, apiUrl, authToken, serverTrustIdentity: suppliedTrustIdentity, headers, ...rest } = init;
331
+ if (apiUrl === undefined) {
332
+ throw new Error('Selected Borg server authority state is missing or unreadable');
333
+ }
334
+ const baseUrl = apiUrl;
335
+ if (suppliedTrustIdentity === undefined) {
336
+ throw new Error('Selected Borg server authority state is missing or unreadable');
337
+ }
338
+ const serverTrustIdentity = suppliedTrustIdentity;
339
+ if (!/^\/api\/cubes(?:\/|$)/.test(path)) {
340
+ localUnsupported(`the ${path} capability`);
341
+ }
342
+ let requestFetch;
343
+ let token;
344
+ {
345
+ const trust = await loadBorgServerTrust(baseUrl);
346
+ if (trust.identity !== serverTrustIdentity) {
347
+ // CR5: a TYPED terminal trust verdict — never inferred from error text.
348
+ throw new BorgServerTrustError('Borg server trust identity changed; refusing the connection');
349
+ }
350
+ requestFetch = trust.fetchImpl;
351
+ if (droneSession !== undefined) {
352
+ // Local attach credentials are already cube/drone-scoped Bearers. The
353
+ // server authenticates this single narrower principal directly.
354
+ token = droneSession;
355
+ }
356
+ else if (authToken !== undefined) {
357
+ token = authToken;
358
+ }
359
+ else {
360
+ const stored = await getServerCredential(baseUrl, serverTrustIdentity);
361
+ if (!stored) {
362
+ throw new Error('No credential is stored for the selected Borg server identity');
363
+ }
364
+ token = stored;
365
+ }
366
+ }
367
+ const method = (rest.method ?? 'GET').toUpperCase();
368
+ const buildRequest = async (tok) => {
369
+ const finalHeaders = {
370
+ 'Authorization': `Bearer ${tok}`,
371
+ ...headers,
372
+ };
373
+ // --debug / BORG_DEBUG: trace every HTTP attempt. Logs method/path/status
374
+ // ONLY — never the Authorization header or any token material.
375
+ debugLog(`→ ${method} ${path}`);
376
+ const res = await requestFetch(`${baseUrl}${path}`, {
377
+ ...rest,
378
+ headers: finalHeaders,
379
+ });
380
+ debugLog(`← ${res.status} ${method} ${path}`);
381
+ return res;
382
+ };
383
+ const response = await buildRequest(token);
384
+ if (response.status === 401) {
385
+ // Reached only after pinned-TLS trust is verified (localAuthorityContext
386
+ // fails closed otherwise). The DESTRUCTIVE worktree-seat reset is permitted
387
+ // ONLY when BOTH hold: (a) this request used the drone SESSION bearer, and
388
+ // (b) the server's bounded-decoded shared-v2 error envelope carries the EXACT
389
+ // typed code SESSION_REJECTED. A bare 401 is never sufficient. Any other or
390
+ // absent/malformed/oversized code — or a parent enrollment/client credential
391
+ // (authToken/stored) — is CREDENTIAL_REJECTED → non-destructive re-enroll
392
+ // recovery, never a seat reset. The body is read (and thus consumed) here,
393
+ // bounded to reject oversized hostile payloads (fail closed to non-reset).
394
+ let rejectedCode;
395
+ try {
396
+ const body = await readBoundedResponseBody(response, AUTH_ERROR_ENVELOPE_LIMIT_BYTES, 'Local Borg server auth error response exceeded the response limit');
397
+ rejectedCode = decodeProtocolErrorEnvelope(JSON.parse(body)).error.code;
398
+ }
399
+ catch {
400
+ rejectedCode = undefined;
401
+ }
402
+ if (droneSession !== undefined && rejectedCode === ErrorCode.SESSION_REJECTED) {
403
+ throw new BorgServerError('SESSION_REJECTED', 'the selected Borg server rejected this worktree session (revoked or taken over)');
404
+ }
405
+ throw new BorgServerError('CREDENTIAL_REJECTED', 'the selected Borg server rejected the credential');
406
+ }
407
+ if (!response.ok) {
408
+ // Do not copy a server response body into errors or debug output: a malicious or
409
+ // misconfigured server could reflect bearer/invitation material or inject
410
+ // terminal controls. Decode only the bounded protocol error code for typed
411
+ // branching; never surface the server-provided message or details.
412
+ let code;
413
+ try {
414
+ const body = await readBoundedResponseBody(response, AUTH_ERROR_ENVELOPE_LIMIT_BYTES, 'Local Borg server error response exceeded the response limit');
415
+ code = decodeProtocolErrorEnvelope(JSON.parse(body)).error.code;
416
+ }
417
+ catch {
418
+ code = undefined;
419
+ }
420
+ debugLog(`✗ ${response.status} ${method} ${path}`);
421
+ if (droneSession !== undefined && response.status === 410 && code === DRONE_EVICTED_CODE) {
422
+ throw new DroneEvictedError();
423
+ }
424
+ throw new BorgServerHttpError(response.status, `Borg server request failed (HTTP ${response.status})`, code);
425
+ }
426
+ return response;
427
+ }
428
+ /**
429
+ * Get the active cube's directive + role registry.
430
+ */
431
+ export async function getCubeInfo(sessionToken, apiUrl, serverTrustIdentity) {
432
+ const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
433
+ const composed = await localCubeComposition(local);
434
+ return { cube: composed.cube, roles: composed.roles };
435
+ }
436
+ /**
437
+ * Get this drone's assigned role (with detailed_description).
438
+ */
439
+ export async function getRoleInfo(sessionToken, apiUrl, serverTrustIdentity) {
440
+ const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
441
+ return { role: (await localCubeComposition(local)).role };
442
+ }
443
+ /**
444
+ * Get a named role's full playbook (detailed_description). Any drone in
445
+ * the cube may read any role. `role` is a role name (case-insensitive)
446
+ * or role id.
447
+ */
448
+ export async function getRoleInfoByName(sessionToken, apiUrl, role, serverTrustIdentity) {
449
+ const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
450
+ const roles = (await localCubeComposition(local)).roles;
451
+ const matched = roles.find((candidate) => candidate.id === role || candidate.name.toLowerCase() === role.toLowerCase());
452
+ if (!matched)
453
+ throw new Error(`Local Borg server has no role named ${JSON.stringify(role)}`);
454
+ return { role: matched };
455
+ }
456
+ export async function whoami(sessionToken, apiUrl, serverTrustIdentity) {
457
+ const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
458
+ const composed = await localCubeComposition(local);
459
+ return {
460
+ cube_id: composed.cube.id,
461
+ cube_name: composed.cube.name,
462
+ drone_id: composed.drone.id,
463
+ drone_label: composed.drone.label,
464
+ role_id: composed.role.id,
465
+ role_name: composed.role.name,
466
+ };
467
+ }
468
+ /**
469
+ * List all currently-connected drones in this cube.
470
+ *
471
+ * Optional `since` is the T2.1 sender-side liveness probe — pass either
472
+ * an activity_log entry id (UUID; server resolves to its `created_at`)
473
+ * OR an ISO-8601 timestamp. When provided, the response includes:
474
+ * - per-drone `seen_since: boolean` — true iff that drone's
475
+ * `last_seen` is strictly after the resolved timestamp
476
+ * - top-level `since: ISO-string | null` — the resolved timestamp
477
+ * (echoed back so the renderer can label the column accurately
478
+ * even when the caller passed an entry-id)
479
+ */
480
+ export async function getRoster(sessionToken, apiUrl, since, serverTrustIdentity) {
481
+ const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
482
+ if (since !== undefined)
483
+ localUnsupported('roster liveness filtering');
484
+ const composed = await localCubeComposition(local);
485
+ return { drones: composed.drones, roles: composed.roles, message_taxonomy: null };
486
+ }
487
+ /**
488
+ * Read recent log entries for the cube.
489
+ */
490
+ export async function readLog(sessionToken, apiUrl, opts = {}) {
491
+ const local = await localAuthorityContext(sessionToken, apiUrl, opts.serverTrustIdentity);
492
+ let cursor = null;
493
+ if (opts.unreadOnly)
494
+ cursor = await getLocalServerCursor(localCursorBinding(local));
495
+ if (opts.since !== undefined)
496
+ cursor = await resolveLocalLogCursor(local, opts.since);
497
+ const page = await localReadLogPage(local, { cursor, limit: opts.limit });
498
+ if (opts.unreadOnly && page.cursor) {
499
+ await advanceLocalServerCursor(localCursorBinding(local), page.cursor);
500
+ }
501
+ const composed = await localCubeComposition(local);
502
+ return {
503
+ entries: page.entries,
504
+ drones: composed.drones,
505
+ roles: composed.roles,
506
+ behind_by: page.behind_by,
507
+ has_more: page.has_more,
508
+ };
509
+ }
510
+ /**
511
+ * Sprint 25 log substrate refactor: explicit ack on a log entry.
512
+ *
513
+ * Replaces in-band `ACK: <dispatch-id>` log entries with a DB-backed
514
+ * flag on activity_log_acks. Idempotent — the server INSERT uses ON
515
+ * CONFLICT DO NOTHING. 204 No Content on success.
516
+ */
517
+ // 'claim' is advisory review-gate ownership; 'ack' preserves the original wire default.
518
+ export async function ackLogEntry(sessionToken, apiUrl, entryId, kind = 'ack', serverTrustIdentity) {
519
+ const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
520
+ await localServerRequest(local, `/api/cubes/${local.cubeId}/acks`, 'POST', { entry_id: entryId, kind });
521
+ }
522
+ /** Record a ratified cube decision using the local client's cube-manage grant. */
523
+ export async function recordDecision(sessionToken, apiUrl, input, serverTrustIdentity) {
524
+ const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
525
+ const payload = await localManageRequest(local, `/api/cubes/${local.cubeId}/decisions`, 'POST', {
526
+ operation: `record a decision in cube ${manageCopyValue(local.name)}`,
527
+ cubeName: local.name,
528
+ noMutation: 'Nothing was recorded.',
529
+ }, input);
530
+ if (!payload)
531
+ throw new Error('Local Borg server returned an empty decision response');
532
+ return payload;
533
+ }
534
+ /**
535
+ * gh#740: list active ratified decisions for the cube (any member). With
536
+ * `topic`, returns that topic's active decision.
537
+ */
538
+ export async function listDecisions(sessionToken, apiUrl, topic, serverTrustIdentity) {
539
+ const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
540
+ const payload = await localServerRequest(local, `/api/cubes/${local.cubeId}/decisions`, 'PUT', {});
541
+ if (!payload)
542
+ throw new Error('Local Borg server returned an empty decisions response');
543
+ return {
544
+ decisions: topic === undefined
545
+ ? payload.decisions
546
+ : payload.decisions.filter((decision) => decision.topic === topic),
547
+ };
548
+ }
549
+ /** Remove one active ratified decision. The worker enforces the seat gate. */
550
+ export async function removeDecision(sessionToken, apiUrl, selector, serverTrustIdentity) {
551
+ void sessionToken;
552
+ void apiUrl;
553
+ void selector;
554
+ void serverTrustIdentity;
555
+ localUnsupported('decision removal');
556
+ }
557
+ /**
558
+ * Regen: one-shot composite of everything a drone needs to be oriented.
559
+ *
560
+ * Returns the active cube's directive, the drone's own role with full
561
+ * detailed_description, the public role registry (no detailed_description
562
+ * leakage for OTHER roles), the drone roster, and the caller's unread-log
563
+ * COUNT (behind_by). gh#886: the recent-log PAYLOAD is no longer rendered
564
+ * client-side — the drone gets the count and drains via borg_read-log. Use
565
+ * on session start and before each new task to stay in sync.
566
+ *
567
+ * gh#29 Sprint C / Q3a: optional `since` cursor (entry-id UUID or
568
+ * ISO-8601 timestamp). The worker still ships `recentLog` for rollout-compat
569
+ * (a pre-gh#886 client renders it; `since` trims it to entries strictly after
570
+ * the anchor) — but the current client renders the unread COUNT, not the
571
+ * payload, so `since` no longer affects what this client shows.
572
+ */
573
+ export async function regen(sessionToken, apiUrl, opts = {}) {
574
+ const local = await localAuthorityContext(sessionToken, apiUrl, opts.serverTrustIdentity);
575
+ const composed = await localCubeComposition(local);
576
+ const cursor = opts.since === undefined
577
+ ? await getLocalServerCursor(localCursorBinding(local))
578
+ : await resolveLocalLogCursor(local, opts.since);
579
+ const page = await localReadLogPage(local, { cursor, limit: 1 });
580
+ let decisions = [];
581
+ try {
582
+ decisions = (await listDecisions(sessionToken, apiUrl, undefined, opts.serverTrustIdentity)).decisions;
583
+ }
584
+ catch (error) {
585
+ console.warn(`Local regen: failed to fetch ratified decisions (${error instanceof Error ? error.message : String(error)}); continuing without them.`);
586
+ }
587
+ return {
588
+ cube: composed.cube,
589
+ role: composed.role,
590
+ drone: composed.drone,
591
+ roles: composed.roles,
592
+ drones: composed.drones,
593
+ recentLog: [],
594
+ behind_by: page.entries.length + page.behind_by,
595
+ decisions,
596
+ };
597
+ }
598
+ export async function roleRationale(sessionToken, apiUrl, role, section, serverTrustIdentity) {
599
+ void sessionToken;
600
+ void apiUrl;
601
+ void role;
602
+ void section;
603
+ void serverTrustIdentity;
604
+ localUnsupported('role rationale sections');
605
+ }
606
+ /**
607
+ * Append a message to the cube's shared activity log.
608
+ */
609
+ export async function appendLog(sessionToken, apiUrl, message, opts = {}) {
610
+ if (opts.visibility === 'broadcast' && (opts.to?.length ?? 0) > 0) {
611
+ throw new Error("Invalid input: visibility:'broadcast' cannot be combined with non-empty to:. " +
612
+ 'Remove visibility to direct to recipients, or remove to: to broadcast.');
613
+ }
614
+ const local = await localAuthorityContext(sessionToken, apiUrl, opts.serverTrustIdentity);
615
+ let visibility = opts.visibility;
616
+ let recipientDroneIds = opts.recipientDroneIds;
617
+ if (visibility !== 'broadcast' &&
618
+ (!recipientDroneIds || recipientDroneIds.length === 0) &&
619
+ opts.to !== undefined) {
620
+ const base = `/api/cubes/${local.cubeId}`;
621
+ const [rolePayload, dronePayload] = await Promise.all([
622
+ localServerRequest(local, `${base}/roles`, 'GET'),
623
+ localServerRequest(local, `${base}/drones`, 'GET'),
624
+ ]);
625
+ if (!rolePayload || !dronePayload) {
626
+ throw new Error('Local Borg server returned an incomplete cube roster');
627
+ }
628
+ recipientDroneIds = resolveLocalLogRecipients(opts.to, dronePayload.drones, rolePayload.roles);
629
+ visibility = 'direct';
630
+ }
631
+ else if (visibility === undefined && recipientDroneIds !== undefined) {
632
+ visibility = 'direct';
633
+ }
634
+ const payload = await localServerRequest(local, `/api/cubes/${local.cubeId}/logs`, 'POST', {
635
+ message,
636
+ ...(visibility ? { visibility } : {}),
637
+ ...(visibility === 'direct' && recipientDroneIds
638
+ ? { recipientDroneIds }
639
+ : {}),
640
+ // server#48 append-time taxonomy routing: forward the requested class
641
+ // so the server can classify/route. It is honored only when no explicit
642
+ // visibility/recipients override it (server resolveMessageRouting).
643
+ ...(opts.class ? { class: opts.class } : {}),
644
+ });
645
+ if (!payload)
646
+ throw new Error('Local Borg server returned an empty log response');
647
+ return { entry: payload.entry };
648
+ }
649
+ /**
650
+ * List cubes readable by the local client's live grants.
651
+ */
652
+ export async function listCubes(connection) {
653
+ return localConnectionRequest(await localOwnerConnection(connection), '/api/cubes');
654
+ }
655
+ /**
656
+ * List bundled cube templates. Used by the `borg assimilate` orchestrator
657
+ * to surface the interactive template prompt on first-drone bootstrap.
658
+ */
659
+ /**
660
+ * Create a new cube. Server-side seeds a default "Drone" role atomically
661
+ * so the cube is assimilatable immediately, OR applies the named template
662
+ * atomically when `opts.template` is set (single-withUserId transaction —
663
+ * skips the auto-Drone insert to avoid is_default partial-index conflict).
664
+ *
665
+ * Returns `{ cube, roles }` — the roles array lets the assimilate
666
+ * orchestrator pick a default role without a follow-up `getCube` call.
667
+ * Existing callers that read `body.cube` keep working (forward-compat).
668
+ */
669
+ export async function createCube(name, cubeDirective, opts, connection) {
670
+ void name;
671
+ void cubeDirective;
672
+ void opts;
673
+ void connection;
674
+ localUnsupported('cube creation');
675
+ }
676
+ /**
677
+ * Update a cube's name and/or cube_directive. Both fields are optional;
678
+ * pass only what changes.
679
+ */
680
+ export async function updateCube(cubeId, updates) {
681
+ assertUuidShape(cubeId, 'cube_id');
682
+ const active = await getActiveCube();
683
+ if (!active?.serverTrustIdentity)
684
+ throw new Error('Selected Borg server authority state is missing or unreadable');
685
+ if (updates.name !== undefined)
686
+ localUnsupported('cube rename');
687
+ const payload = {};
688
+ if (updates.cube_directive !== undefined)
689
+ payload.cube_directive = updates.cube_directive;
690
+ if (Object.prototype.hasOwnProperty.call(updates, 'message_taxonomy')) {
691
+ payload.message_taxonomy = updates.message_taxonomy ?? null;
692
+ }
693
+ const result = await localManageRequest(active, `/api/cubes/${cubeId}`, 'PATCH', {
694
+ operation: `update cube settings in cube ${manageCopyValue(cubeId === active.cubeId ? active.name : cubeId)}`,
695
+ cubeName: cubeId === active.cubeId ? active.name : cubeId,
696
+ noMutation: 'No cube settings were changed.',
697
+ }, payload);
698
+ if (!result)
699
+ throw new Error('Local Borg server returned an empty cube response');
700
+ return result;
701
+ }
702
+ /**
703
+ * gh#473 PR1 — granular per-class taxonomy patch. Add / replace-by-name
704
+ * / remove a single class within the cube's message_taxonomy, leaving
705
+ * other classes unchanged. The server re-validates the full resulting
706
+ * array and requires the selected local client's live cube-manage grant.
707
+ */
708
+ export async function patchTaxonomyClass(cubeId, op) {
709
+ assertUuidShape(cubeId, 'cube_id');
710
+ const active = await getActiveCube();
711
+ if (!active?.serverTrustIdentity)
712
+ throw new Error('Selected Borg server authority state is missing or unreadable');
713
+ const className = op.action === 'remove' ? op.class : op.class_def.class;
714
+ const preposition = op.action === 'add' ? 'to' : op.action === 'replace' ? 'in' : 'from';
715
+ const pastTense = op.action === 'add' ? 'added' : op.action === 'replace' ? 'replaced' : 'removed';
716
+ const cubeName = cubeId === active.cubeId ? active.name : cubeId;
717
+ const result = await localManageRequest(active, `/api/cubes/${cubeId}/taxonomy-patch`, 'POST', {
718
+ operation: `${op.action} message class ${manageCopyValue(className)} ${preposition} cube ${manageCopyValue(cubeName)}`,
719
+ cubeName,
720
+ noMutation: `No message class was ${pastTense}.`,
721
+ }, op);
722
+ if (!result)
723
+ throw new Error('Local Borg server returned an empty taxonomy response');
724
+ return result;
725
+ }
726
+ /**
727
+ * Delete a cube. Cascade-deletes all roles, drones, and log entries.
728
+ * Requires a live cube-manage grant on the selected local client.
729
+ */
730
+ export async function deleteCube(cubeId) {
731
+ void cubeId;
732
+ localUnsupported('cube deletion');
733
+ }
734
+ /**
735
+ * Create a role inside a cube. is_default=true demotes the previous
736
+ * default role; the cube always has exactly one default.
737
+ */
738
+ export async function createRole(cubeId, data) {
739
+ assertUuidShape(cubeId, 'cube_id');
740
+ const active = await getActiveCube();
741
+ if (!active?.serverTrustIdentity)
742
+ throw new Error('Selected Borg server authority state is missing or unreadable');
743
+ if (data.default_model !== undefined)
744
+ localUnsupported('per-role default model');
745
+ const result = await localManageRequest(active, `/api/cubes/${cubeId}/roles`, 'POST', {
746
+ operation: `create role ${manageCopyValue(data.name)} in cube ${manageCopyValue(cubeId === active.cubeId ? active.name : cubeId)}`,
747
+ cubeName: cubeId === active.cubeId ? active.name : cubeId,
748
+ noMutation: 'No role was created.',
749
+ }, buildLocalRoleFields(data));
750
+ if (!result)
751
+ throw new Error('Local Borg server returned an empty role response');
752
+ return result;
753
+ }
754
+ /**
755
+ * Update a role. All fields optional; pass only what changes.
756
+ */
757
+ export async function updateRole(roleId, updates) {
758
+ assertUuidShape(roleId, 'role_id');
759
+ const active = await getActiveCube();
760
+ if (!active?.serverTrustIdentity)
761
+ throw new Error('Selected Borg server authority state is missing or unreadable');
762
+ assertUuidShape(active.cubeId, 'cube_id');
763
+ if (updates.default_model !== undefined)
764
+ localUnsupported('per-role default model');
765
+ const result = await localManageRequest(active, `/api/cubes/${active.cubeId}/roles/${roleId}`, 'PATCH', {
766
+ operation: `update role ${manageCopyValue(roleId)} in cube ${manageCopyValue(active.name)}`,
767
+ cubeName: active.name,
768
+ noMutation: 'No role was updated.',
769
+ }, buildLocalRoleFields(updates));
770
+ if (!result)
771
+ throw new Error('Local Borg server returned an empty role response');
772
+ return result;
773
+ }
774
+ /**
775
+ * Project a create/update-role field bag onto the exact snake_case keys the
776
+ * self-hosted coordination API accepts, dropping undefined entries and the
777
+ * unsupported default_model (rejected before this call). name is included only
778
+ * when present so a partial update PATCHes just the supplied fields.
779
+ */
780
+ function buildLocalRoleFields(fields) {
781
+ const payload = {};
782
+ for (const key of [
783
+ 'name',
784
+ 'short_description',
785
+ 'detailed_description',
786
+ 'is_default',
787
+ 'is_mandatory',
788
+ 'is_human_seat',
789
+ 'can_broadcast',
790
+ 'receives_all_direct',
791
+ 'role_class',
792
+ ]) {
793
+ if (fields[key] !== undefined)
794
+ payload[key] = fields[key];
795
+ }
796
+ return payload;
797
+ }
798
+ /**
799
+ * gh#473 PR1 — granular role-text section patch. Replace / insert /
800
+ * delete a single named section of a role's detailed_description,
801
+ * leaving the rest of the field byte-identical. Requires the selected local
802
+ * client's live cube-manage grant. Sections are delimited by plain-label lines (e.g.
803
+ * `Workflow:`), NOT markdown headings.
804
+ */
805
+ export async function patchRoleSection(roleId, op) {
806
+ assertUuidShape(roleId, 'role_id');
807
+ const active = await getActiveCube();
808
+ if (!active?.serverTrustIdentity)
809
+ throw new Error('Selected Borg server authority state is missing or unreadable');
810
+ assertUuidShape(active.cubeId, 'cube_id');
811
+ const result = await localManageRequest(active, `/api/cubes/${active.cubeId}/roles/${roleId}/section-patch`, 'POST', {
812
+ operation: `${op.action} section ${manageCopyValue(op.heading)} ${op.action === 'delete' ? 'from' : 'in'} role ${manageCopyValue(roleId)} in cube ${manageCopyValue(active.name)}`,
813
+ cubeName: active.name,
814
+ noMutation: `No role section was ${op.action === 'insert' ? 'inserted' : op.action === 'replace' ? 'replaced' : 'deleted'}.`,
815
+ }, { ...op });
816
+ if (!result)
817
+ throw new Error('Local Borg server returned an empty role response');
818
+ return result;
819
+ }
820
+ /**
821
+ * Delete a role. Worker refuses if any drone is still assigned to it
822
+ * (reassign or evict those drones first).
823
+ */
824
+ export async function deleteRole(roleId) {
825
+ void roleId;
826
+ localUnsupported('role deletion');
827
+ }
828
+ /**
829
+ * Reassign a drone to a different role within the same cube.
830
+ * Queen-class seat cardinality is enforced server-side — attempting
831
+ * to assign to a queen-class role when another drone already holds
832
+ * the seat returns an error. The class-hierarchy guard also rejects
833
+ * direct promotion from non-human-seat roles.
834
+ */
835
+ export async function reassignDrone(droneId, roleId, activeOverride) {
836
+ // Validate both identifiers before credential lookup or network access.
837
+ assertUuidShape(droneId, 'drone_id');
838
+ assertUuidShape(roleId, 'role_id');
839
+ const active = activeOverride ?? await getActiveCube();
840
+ if (!active?.serverTrustIdentity)
841
+ throw new Error('Selected Borg server authority state is missing or unreadable');
842
+ assertUuidShape(active.cubeId, 'cube_id');
843
+ const result = await localManageRequest(active, `/api/cubes/${active.cubeId}/drones/${droneId}`, 'PATCH', {
844
+ operation: `reassign drone ${manageCopyValue(droneId)} to role ${manageCopyValue(roleId)} in cube ${manageCopyValue(active.name)}`,
845
+ cubeName: active.name,
846
+ noMutation: 'No drone was reassigned.',
847
+ }, { role_id: roleId }, decodeReassignDroneResult);
848
+ if (!result)
849
+ throw new Error('Local Borg server returned an empty drone reassignment response');
850
+ return result;
851
+ }
852
+ export async function evictDrone(droneId, options = {}) {
853
+ assertUuidShape(droneId, 'drone_id');
854
+ if (options.cubeId !== undefined)
855
+ assertUuidShape(options.cubeId, 'cube_id');
856
+ const active = options.active ?? await getActiveCube();
857
+ if (!active?.serverTrustIdentity)
858
+ throw new Error('Selected Borg server authority state is missing or unreadable');
859
+ const cubeId = options.cubeId ?? active.cubeId;
860
+ assertUuidShape(cubeId, 'cube_id');
861
+ const cubeName = options.cubeName ?? (cubeId === active.cubeId ? active.name : cubeId);
862
+ const targetReference = options.targetReference ?? droneId;
863
+ const result = await localManageRequest(active, `/api/cubes/${cubeId}/drones/${droneId}`, 'DELETE', {
864
+ operation: `remove ${manageCopyValue(targetReference)} from cube ${manageCopyValue(cubeName)}`,
865
+ cubeName,
866
+ noMutation: 'No drone was removed.',
867
+ }, {}, decodeEvictDroneResult);
868
+ if (!result)
869
+ throw new Error('Local Borg server returned an empty drone eviction response');
870
+ return result;
871
+ }
872
+ export async function listRoles(cubeId) {
873
+ assertUuidShape(cubeId, 'cube_id');
874
+ const active = await getActiveCube();
875
+ if (!active?.serverTrustIdentity)
876
+ throw new Error('Selected Borg server authority state is missing or unreadable');
877
+ const result = await localServerRequest(active, `/api/cubes/${cubeId}/roles`, 'GET');
878
+ if (!result || !Array.isArray(result.roles)) {
879
+ throw new Error('Local Borg server returned an invalid roles response');
880
+ }
881
+ return result.roles;
882
+ }
883
+ export async function getCubeForManagement(cubeId, operation, activeOverride) {
884
+ assertUuidShape(cubeId, 'cube_id');
885
+ const active = activeOverride ?? await getActiveCube();
886
+ if (!active?.serverTrustIdentity)
887
+ throw new Error('Selected Borg server authority state is missing or unreadable');
888
+ return getCube(cubeId, await localManageConnection(active, operation));
889
+ }
890
+ /**
891
+ * Fetch a cube's full detail: directive, roles (with detailed
892
+ * descriptions), and drones. Access is enforced by the local client's live
893
+ * per-cube grant.
894
+ */
895
+ export async function getCube(cubeId, connection) {
896
+ assertUuidShape(cubeId, 'cube_id');
897
+ const resolved = await localOwnerConnection(connection);
898
+ const base = `/api/cubes/${cubeId}`;
899
+ const [cubePayload, rolePayload, dronePayload] = await Promise.all([
900
+ localConnectionRequest(resolved, base),
901
+ localConnectionRequest(resolved, `${base}/roles`),
902
+ localConnectionRequest(resolved, `${base}/drones`),
903
+ ]);
904
+ return {
905
+ ...cubePayload.cube,
906
+ roles: rolePayload.roles,
907
+ drones: dronePayload.drones,
908
+ };
909
+ }
910
+ /**
911
+ * gh#473 PR2 — apply a named template to an existing cube via the
912
+ * NON-CLOBBERING server route. New roles are inserted; existing
913
+ * template-named roles get ADD fragments auto-applied (template
914
+ * sections/classes the cube lacks) but their EVOLVED (conflicting)
915
+ * fragments are surfaced server-side and KEPT, never overwritten. Returns
916
+ * `{ created, updated }` counts. To selectively take template versions of
917
+ * conflicting fragments, use `syncRoles` with a `decisions` map instead.
918
+ */
919
+ export async function applyTemplate(cubeId, templateName) {
920
+ void cubeId;
921
+ void templateName;
922
+ localUnsupported('template application');
923
+ }
924
+ /**
925
+ * gh#473 PR2 — NON-CLOBBERING sync of a cube's roles + message_taxonomy
926
+ * against the current built-in template. Dry-run by default classifies
927
+ * each fragment (role-text SECTION / short_description / flags / taxonomy
928
+ * CLASS) as ADD / UNCHANGED / CONFLICT. Pass apply=true to commit:
929
+ * ADD fragments auto-apply (zero clobber risk); CONFLICT fragments apply
930
+ * ONLY when their stable key appears in `decisions` as 'accept'.
931
+ * Unspecified conflicts DEFAULT TO REJECT — the cube's evolved text is
932
+ * never silently overwritten. Custom roles (names not in template) are
933
+ * never touched. Returns a NonClobberSyncResult.
934
+ */
935
+ export async function syncRoles(cubeId, templateName = 'software-dev', apply = false, decisions) {
936
+ void cubeId;
937
+ void templateName;
938
+ void apply;
939
+ void decisions;
940
+ localUnsupported('role synchronization');
941
+ }
942
+ //# sourceMappingURL=remote-client.js.map