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