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/dist/index.js
CHANGED
|
@@ -1,38 +1,1035 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Borg MCP Client - Main Entry Point
|
|
4
|
+
*
|
|
5
|
+
* stdio MCP server that:
|
|
6
|
+
* 1. Connects to Claude Code via stdio transport
|
|
7
|
+
* 2. Proxies MCP tools to a verified local (self-hosted) Borg server
|
|
8
|
+
* 3. Provides the borg: cube tool surface (assimilate / cube / role /
|
|
9
|
+
* roster / read-log) so Claude can act as a Drone in a hive of
|
|
10
|
+
* collaborating sessions.
|
|
11
|
+
*/
|
|
12
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
13
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
15
|
+
import { assertRoleMatches } from './role-match.js';
|
|
16
|
+
import { getCubeInfo, getRoleInfo, getRoleInfoByName, getRoster, readLog, appendLog, ackLogEntry, recordDecision, removeDecision, listDecisions, regen, listCubes, createCube, updateCube, deleteCube, createRole, updateRole, patchRoleSection, patchTaxonomyClass, deleteRole, getCube, listRoles, syncRoles, applyTemplate, whoami, roleRationale, } from './remote-client.js';
|
|
17
|
+
import { getTemplate, listTemplateNames, resolveCubeDirectiveForCreate, resolveCubeDirectiveForApply, resolveMessageTaxonomyForCreate, } from 'borgmcp-shared/templates';
|
|
18
|
+
import { activeCubeWithFreshRegenIdentity, getActiveCube, refreshActiveCubeMetadata, findProjectRoot, inboxPathForDrone, } from './cubes.js';
|
|
19
|
+
import { isEntryInvocation, monitorStateRootForWorktree } from './inbox-monitor.js';
|
|
20
|
+
import { addSessionStartHook, addUserPromptSubmitHook } from './config-utils.js';
|
|
21
|
+
import { humanAgo, formatLogEntryMarkdown, formatRegenMarkdown, getDronePlaybook, getDronePlaybookChapter, nullTaxonomyTip, regenWakePathDroneLabel, } from './regen-format.js';
|
|
22
|
+
import { startLogStream, getStreamStatus } from './log-stream.js';
|
|
23
|
+
import { isMcpReadinessProbe } from './readiness-probe.js';
|
|
24
|
+
import { runMcpStartupServices } from './startup-services.js';
|
|
25
|
+
import { TOOL_MANIFEST } from './tool-manifest.js';
|
|
26
|
+
import { DOCS_SECTIONS, matchDocsSections, formatDocsIndex } from './docs-sections.js';
|
|
27
|
+
import { renderRoleList } from './list-roles-render.js';
|
|
28
|
+
import { filterToolsForRole } from './tool-scope.js';
|
|
29
|
+
import { getPackageVersion, getOnDiskVersion, handleVersionFlag } from './version.js';
|
|
30
|
+
import { renderStreamStatus, checkInboxMonitorHealthy, formatWakePathPrefix, shouldShowWakePathWarning, } from './stream-status.js';
|
|
31
|
+
import { formatRoleAgentLabel, formatWorkingRepoLabel, renderRoster } from './roster-render.js';
|
|
32
|
+
import { resolveWorkingRepo } from './working-repo.js';
|
|
33
|
+
import { DroneEvictedError, formatEvictedToolResult, } from './drone-lifecycle.js';
|
|
34
|
+
import { classifyInSessionAssimilate, reattachOnlyRefusal, reattachFailureMessage, } from './assimilate-guard.js';
|
|
35
|
+
import { gateAllowsActivation, borgSessionToolNotice } from './launch-gate.js';
|
|
36
|
+
import { renderSyncRolesResult } from './sync-roles-render.js';
|
|
37
|
+
import { initConsolePrefix, consolePrefix } from './console-prefix.js';
|
|
38
|
+
import { resolveSessionAgentKind, } from './codex-app-wake.js';
|
|
39
|
+
import { connectOpenCodeDrone, injectOpenCodeEntry, computeOpenCodePort, } from './opencode-drone.js';
|
|
40
|
+
import { installBorgPlugin } from './opencode-plugin.js';
|
|
41
|
+
import { setModuleInjectOpenCode } from './log-stream.js';
|
|
42
|
+
import { lifecycleSignalForMessage, recordLifecycleLog, shouldSuppressLifecycleLog, } from './lifecycle-log-guard.js';
|
|
43
|
+
import { normalizeDirectLogRecipients, } from './direct-log.js';
|
|
44
|
+
import { formatLocalManageToolResult } from './local-manage-tool-result.js';
|
|
45
|
+
import { runEvictDroneTool, runReassignDroneTool, } from './drone-management.js';
|
|
46
|
+
/**
|
|
47
|
+
* Apply a template's roles + message_taxonomy to a cube.
|
|
48
|
+
*
|
|
49
|
+
* gh#473 PR2 — delegates to the NON-CLOBBERING server route. New roles
|
|
50
|
+
* are inserted; existing template-named roles get ADD fragments (template
|
|
51
|
+
* sections/classes the cube lacks) auto-applied, but EVOLVED (conflicting)
|
|
52
|
+
* fragments are surfaced server-side and KEPT — never silently
|
|
53
|
+
* overwritten. The old per-role blanket `updateRole`/whole-taxonomy
|
|
54
|
+
* `updateCube` overwrite path is removed. Operators who want to take the
|
|
55
|
+
* template version of a conflicting fragment use `borg_sync-roles` with a
|
|
56
|
+
* `decisions` map. Returns `{ created, updated }` for the caller's toast.
|
|
57
|
+
*/
|
|
58
|
+
async function applyTemplateToCube(cubeId, template) {
|
|
59
|
+
return await applyTemplate(cubeId, template.name);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Throw a friendly error if the client has not been assimilated to a cube.
|
|
63
|
+
*/
|
|
64
|
+
async function requireActiveCube() {
|
|
65
|
+
const active = await getActiveCube();
|
|
66
|
+
if (!active) {
|
|
67
|
+
throw new Error('Not assimilated to a cube. Use borg_assimilate <cube-name> first.');
|
|
68
|
+
}
|
|
69
|
+
return active;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Main entry point - MCP stdio server
|
|
73
|
+
*/
|
|
74
|
+
export async function main() {
|
|
75
|
+
// Honor `--version` / `-v` BEFORE any side-effecting work (hooks,
|
|
76
|
+
// readiness checks, stream consumer spawn, MCP handshake). Lets
|
|
77
|
+
// operators run `borg-mcp --version` cleanly to confirm the
|
|
78
|
+
// installed client version.
|
|
79
|
+
handleVersionFlag();
|
|
80
|
+
const readinessProbe = isMcpReadinessProbe();
|
|
81
|
+
await runMcpStartupServices(readinessProbe, {
|
|
82
|
+
// Auto-register the SessionStart hook so existing users get borg-regen
|
|
83
|
+
// auto-orientation on session start without re-running borg setup. Idempotent.
|
|
84
|
+
sessionStartHook: () => {
|
|
85
|
+
addSessionStartHook();
|
|
86
|
+
},
|
|
87
|
+
// Auto-register the UserPromptSubmit audit hook so the drone gets a
|
|
88
|
+
// nudge if the previous assistant span used state-changing tools
|
|
89
|
+
// without calling borg_log. Domain-agnostic — knows nothing about git
|
|
90
|
+
// or any specific convention. Idempotent.
|
|
91
|
+
auditHook: () => {
|
|
92
|
+
addUserPromptSubmitHook();
|
|
93
|
+
},
|
|
94
|
+
// Spawn the SSE log-stream consumer. This gives drones real-time
|
|
95
|
+
// wakeup: when another drone posts to the cube, the worker pushes
|
|
96
|
+
// an `event: log` over SSE, the consumer appends one line to the
|
|
97
|
+
// per-drone inbox file (see inboxPathForDrone in cubes.ts), and the
|
|
98
|
+
// launcher's Monitor wakes the active /loop iteration immediately.
|
|
99
|
+
// Same inbox-file shape as the prior long-poll path — the file is
|
|
100
|
+
// still the harness-side wake primitive — only the wire layer
|
|
101
|
+
// changed. See:
|
|
102
|
+
// docs/superpowers/specs/2026-05-11-server-push-log-subscription.md
|
|
103
|
+
// Failure here is non-fatal — the launcher's fallback heartbeat
|
|
104
|
+
// still keeps things moving.
|
|
105
|
+
sseStream: () => {
|
|
106
|
+
startLogStream();
|
|
107
|
+
},
|
|
108
|
+
// gh#opencode: initialize opencode drone module for autonomous entry
|
|
109
|
+
// injection. Installs the plugin (idempotent) and connects the SDK client
|
|
110
|
+
// when the runtime is opencode (BORG_OPENCODE=1). The module-level
|
|
111
|
+
// injectOpenCode hook routes SSE entries into the TUI's session.
|
|
112
|
+
// Best-effort; never breaks the MCP server.
|
|
113
|
+
openCode: async () => {
|
|
114
|
+
installBorgPlugin();
|
|
115
|
+
const active = await getActiveCube();
|
|
116
|
+
if (active && process.env.BORG_OPENCODE === '1') {
|
|
117
|
+
const port = computeOpenCodePort(active.droneId);
|
|
118
|
+
const serverUrl = `http://127.0.0.1:${port}`;
|
|
119
|
+
await connectOpenCodeDrone({
|
|
120
|
+
serverUrl,
|
|
121
|
+
directory: process.cwd(),
|
|
122
|
+
droneLabel: active.droneLabel,
|
|
123
|
+
cubeName: active.name,
|
|
124
|
+
});
|
|
125
|
+
setModuleInjectOpenCode(injectOpenCodeEntry);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
// Create MCP server. `version` is the installed borgmcp version
|
|
130
|
+
// (T1.4 of 0.6.0): read at runtime from package.json so Claude
|
|
131
|
+
// Code's `/mcp` view shows the real version instead of the
|
|
132
|
+
// long-standing hardcoded "0.1.0".
|
|
133
|
+
const server = new Server({
|
|
134
|
+
name: 'borg-mcp-client',
|
|
135
|
+
version: getPackageVersion(),
|
|
136
|
+
}, {
|
|
137
|
+
capabilities: {
|
|
138
|
+
tools: {},
|
|
139
|
+
prompts: {},
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
// gh#899: tool definitions built once at setup, then role-scoped per caller
|
|
143
|
+
// in the ListTools handler below (the dispatcher reaches deferred tools).
|
|
144
|
+
const allToolDefs = TOOL_MANIFEST;
|
|
145
|
+
// Register tool listing — role-scope the native surface (gh#899). Missing role
|
|
146
|
+
// (old cubes.json / pre-assimilate) → full set; deferred tools stay reachable
|
|
147
|
+
// via borg_tool. Never an auth boundary — live per-client cube grants govern.
|
|
148
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
149
|
+
let scope = null;
|
|
150
|
+
try {
|
|
151
|
+
const active = await getActiveCube();
|
|
152
|
+
if (active) {
|
|
153
|
+
scope = { roleName: active.roleName, roleClass: active.roleClass, isHumanSeat: active.isHumanSeat };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
scope = null; // unreadable cube state → full set (fail-safe)
|
|
158
|
+
}
|
|
159
|
+
return { tools: filterToolsForRole(allToolDefs, scope) };
|
|
160
|
+
});
|
|
161
|
+
// Register tool execution handler
|
|
162
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
163
|
+
let { name, arguments: args } = request.params;
|
|
164
|
+
// gh#899: borg_describe-tool — schema-only, NEVER executes. Returns the
|
|
165
|
+
// named tool's def from allToolDefs so a role-scoped session can learn a
|
|
166
|
+
// deferred tool's arguments before invoking it via borg_tool.
|
|
167
|
+
if (name === 'borg_describe-tool') {
|
|
168
|
+
const target = typeof args?.name === 'string' ? args.name : '';
|
|
169
|
+
const def = allToolDefs.find((t) => t.name === target);
|
|
170
|
+
if (!def) {
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: 'text', text: `Unknown borg tool: ${target || '(none)'}. Pass { name: "<borg_tool>" }.` }],
|
|
173
|
+
isError: true,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: 'text', text: JSON.stringify({ name: def.name, description: def.description, inputSchema: def.inputSchema }, null, 2) }],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// gh#899: borg_tool dispatcher — unwrap to the inner tool and fall through
|
|
181
|
+
// to the SAME switch below (identical activation gate + per-tool auth/Zod
|
|
182
|
+
// validation; no weaker entry — the server's live cube-grant boundary is
|
|
183
|
+
// unchanged). Guards against dispatcher self-reference / recursion.
|
|
184
|
+
if (name === 'borg_tool') {
|
|
185
|
+
const inner = typeof args?.name === 'string' ? args.name : '';
|
|
186
|
+
if (!inner || inner === 'borg_tool' || inner === 'borg_describe-tool') {
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: 'text', text: 'borg_tool: pass { name: "<borg_tool>", arguments: {...} } naming a real borg tool (not the dispatcher itself).' }],
|
|
189
|
+
isError: true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
args = (args?.arguments && typeof args.arguments === 'object')
|
|
193
|
+
? args.arguments
|
|
194
|
+
: {};
|
|
195
|
+
name = inner;
|
|
196
|
+
}
|
|
197
|
+
// gh#673 P1 (WI-5): the borg tool surface activates only in
|
|
198
|
+
// borg-launched sessions — BOTH harnesses ride the same gate. A
|
|
199
|
+
// vanilla `claude`/`codex` gets a non-silent re-launch notice per
|
|
200
|
+
// tool, never a half-activated, un-wakeable drone; ListTools stays
|
|
201
|
+
// intact so the agent learns WHY. Claude children inherit
|
|
202
|
+
// BORG_SESSION from the wrapper's launchEnv; codex children receive
|
|
203
|
+
// it via the per-launch `-c mcp_servers.borg.env.BORG_SESSION="1"`
|
|
204
|
+
// override (codexBorgSessionConfigArgs — inherited env never reaches
|
|
205
|
+
// codex MCP children, V2/V2b probes). ACTIVATION-only per the SR
|
|
206
|
+
// binding — never a security decision (server auth unchanged).
|
|
207
|
+
if (!gateAllowsActivation(`tool ${name}`)) {
|
|
208
|
+
return {
|
|
209
|
+
content: [{ type: 'text', text: borgSessionToolNotice(name) }],
|
|
210
|
+
isError: true,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
switch (name) {
|
|
215
|
+
case 'borg_regen': {
|
|
216
|
+
const active = await getActiveCube();
|
|
217
|
+
if (!active) {
|
|
218
|
+
return {
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: 'text',
|
|
222
|
+
text: 'Not connected to a cube. Use `borg_assimilate cube_name="<name>"` to join one.',
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const since = typeof args?.since === 'string' ? args.since : undefined;
|
|
228
|
+
const mode = args?.mode === 'lite' ? 'lite' : 'full';
|
|
229
|
+
const reportedModel = typeof args?.model === 'string' ? args.model : undefined;
|
|
230
|
+
const result = await regen(active.sessionToken, active.apiUrl, {
|
|
231
|
+
since,
|
|
232
|
+
reportedModel,
|
|
233
|
+
workingRepo: resolveWorkingRepo(),
|
|
234
|
+
serverTrustIdentity: active.serverTrustIdentity,
|
|
235
|
+
});
|
|
236
|
+
const freshActive = activeCubeWithFreshRegenIdentity(active, result);
|
|
237
|
+
if (freshActive !== active) {
|
|
238
|
+
await refreshActiveCubeMetadata(freshActive);
|
|
239
|
+
}
|
|
240
|
+
// Wake-path self-heal (gh#43): SSE delivery to the inbox file
|
|
241
|
+
// is independent from Claude Code waking on file writes. The
|
|
242
|
+
// latter requires a `tail -F` Monitor against the inbox path;
|
|
243
|
+
// if that Monitor dies (or was never armed across a session
|
|
244
|
+
// boundary), the drone misses every incoming entry until the
|
|
245
|
+
// /loop fallback heartbeat. Because regen runs on every /loop
|
|
246
|
+
// iteration, surfacing the breakage here gives self-healing at
|
|
247
|
+
// worst-case latency = the heartbeat interval. Mirrors the
|
|
248
|
+
// State-5 self-arm instruction in stream-status.ts.
|
|
249
|
+
const streamStatus = getStreamStatus();
|
|
250
|
+
const inboxPath = inboxPathForDrone(freshActive.cubeId, freshActive.droneId);
|
|
251
|
+
const monitorStateRoot = monitorStateRootForWorktree(findProjectRoot());
|
|
252
|
+
// Non-Claude CLIs do not use the Claude inbox Monitor. Keep the
|
|
253
|
+
// agent CLI distinction independent from whether Codex's optional
|
|
254
|
+
// remote-wake transport is currently armed.
|
|
255
|
+
const agentKind = resolveSessionAgentKind();
|
|
256
|
+
const inboxMonitorHealthy = agentKind === 'claude'
|
|
257
|
+
? checkInboxMonitorHealthy(inboxPath, monitorStateRoot)
|
|
258
|
+
: true;
|
|
259
|
+
const prefix = shouldShowWakePathWarning(streamStatus, inboxMonitorHealthy)
|
|
260
|
+
? formatWakePathPrefix({
|
|
261
|
+
inboxPath,
|
|
262
|
+
monitorStateRoot,
|
|
263
|
+
droneLabel: regenWakePathDroneLabel(result, freshActive.droneLabel),
|
|
264
|
+
cubeName: freshActive.name,
|
|
265
|
+
})
|
|
266
|
+
: '';
|
|
267
|
+
// gh#285: version-mismatch nudge when on-disk package is newer.
|
|
268
|
+
let versionHeader = '';
|
|
269
|
+
try {
|
|
270
|
+
const running = getPackageVersion();
|
|
271
|
+
const onDisk = getOnDiskVersion();
|
|
272
|
+
if (running !== 'unknown' && onDisk !== 'unknown' && onDisk !== running) {
|
|
273
|
+
const [rMaj, rMin, rPat] = running.split('.').map(Number);
|
|
274
|
+
const [dMaj, dMin, dPat] = onDisk.split('.').map(Number);
|
|
275
|
+
const onDiskNewer = dMaj > rMaj || (dMaj === rMaj && dMin > rMin) || (dMaj === rMaj && dMin === rMin && dPat > rPat);
|
|
276
|
+
if (onDiskNewer) {
|
|
277
|
+
versionHeader = `## 🔄 borgmcp ${onDisk} installed — run /mcp and reconnect (or restart Claude Code) to apply. Currently running ${running}.\n\n`;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch { /* never break regen */ }
|
|
282
|
+
return { content: [{ type: 'text', text: versionHeader + prefix + formatRegenMarkdown(result, { mode }) }] };
|
|
283
|
+
}
|
|
284
|
+
case 'borg_assimilate': {
|
|
285
|
+
const cubeName = args?.cube_name;
|
|
286
|
+
if (!cubeName)
|
|
287
|
+
throw new Error('cube_name is required');
|
|
288
|
+
// gh#780 (Queen ruling 33a62d94): RE-ATTACH-ONLY. The old handler
|
|
289
|
+
// used the retired attach path, which always minted a new drone row —
|
|
290
|
+
// so agents "recovering" from auth blips spawned orphan seats.
|
|
291
|
+
// This tool now re-attaches to the worktree's saved identity or
|
|
292
|
+
// refuses with CLI guidance; it is structurally incapable of
|
|
293
|
+
// minting. Seat creation lives in the CLI (`borg assimilate`).
|
|
294
|
+
const active = await getActiveCube();
|
|
295
|
+
const decision = classifyInSessionAssimilate(active, cubeName);
|
|
296
|
+
if (decision.kind !== 'reattach') {
|
|
297
|
+
return {
|
|
298
|
+
content: [{ type: 'text', text: reattachOnlyRefusal(decision, cubeName) }],
|
|
299
|
+
isError: true,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
// Re-attach = serve the saved identity through the
|
|
303
|
+
// server-validated regen path (SR cond-4: no fabricated success
|
|
304
|
+
// — an evicted/revoked seat FAILS server-side and is surfaced).
|
|
305
|
+
try {
|
|
306
|
+
const result = await regen(active.sessionToken, active.apiUrl, {
|
|
307
|
+
workingRepo: resolveWorkingRepo(),
|
|
308
|
+
serverTrustIdentity: active.serverTrustIdentity,
|
|
309
|
+
});
|
|
310
|
+
const freshActive = activeCubeWithFreshRegenIdentity(active, result);
|
|
311
|
+
if (freshActive !== active) {
|
|
312
|
+
await refreshActiveCubeMetadata(freshActive);
|
|
313
|
+
}
|
|
314
|
+
const header = [
|
|
315
|
+
`# Re-attached to cube: ${freshActive.name}`,
|
|
316
|
+
``,
|
|
317
|
+
`**Drone label:** ${freshActive.droneLabel}`,
|
|
318
|
+
`**Seat:** existing identity reused — no new drone minted (gh#780)`,
|
|
319
|
+
``,
|
|
320
|
+
``,
|
|
321
|
+
].join('\n');
|
|
322
|
+
return {
|
|
323
|
+
content: [{ type: 'text', text: header + formatRegenMarkdown(result, { mode: 'full' }) }],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
const failure = reattachFailureMessage(err ?? {});
|
|
328
|
+
return { content: [{ type: 'text', text: failure }], isError: true };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
case 'borg_version': {
|
|
332
|
+
return { content: [{ type: 'text', text: `borgmcp ${getPackageVersion()}` }] };
|
|
333
|
+
}
|
|
334
|
+
case 'borg_playbook': {
|
|
335
|
+
// gh#912: serve the on-demand operating-playbook chapter (static
|
|
336
|
+
// client-side text — no cube/auth needed; the rule-spine is already
|
|
337
|
+
// inline in regen). Lets the bootstrap regen stay light.
|
|
338
|
+
return { content: [{ type: 'text', text: getDronePlaybookChapter() }] };
|
|
339
|
+
}
|
|
340
|
+
case 'borg_docs': {
|
|
341
|
+
// gh#docs-site B: return the docs index (URLs + summaries) so the agent
|
|
342
|
+
// can WebFetch the right page. No server-side fetch, no search service —
|
|
343
|
+
// the topic match is a lazy keyword filter over DOCS_SECTIONS.
|
|
344
|
+
const topic = typeof args?.topic === 'string' ? args.topic.trim() : '';
|
|
345
|
+
const matched = topic ? matchDocsSections(topic) : [];
|
|
346
|
+
const sections = matched.length > 0 ? matched : DOCS_SECTIONS;
|
|
347
|
+
const header = topic && matched.length > 0
|
|
348
|
+
? `Best-matching docs section(s) for "${topic}" — WebFetch the URL for the full page:`
|
|
349
|
+
: topic
|
|
350
|
+
? `No exact match for "${topic}". Full Borg MCP docs index — WebFetch the URL you need:`
|
|
351
|
+
: `Borg MCP docs index — WebFetch the URL of the section you need:`;
|
|
352
|
+
return { content: [{ type: 'text', text: `${header}\n\n${formatDocsIndex(sections)}` }] };
|
|
353
|
+
}
|
|
354
|
+
case 'borg_whoami': {
|
|
355
|
+
const active = await requireActiveCube();
|
|
356
|
+
const result = await whoami(active.sessionToken, active.apiUrl, active.serverTrustIdentity);
|
|
357
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
358
|
+
}
|
|
359
|
+
case 'borg_cube': {
|
|
360
|
+
// No-cache invariant (T1.2 — 0.7.0): both `getCubeInfo` and
|
|
361
|
+
// `getRoleInfo` MUST fetch fresh per invocation. Never
|
|
362
|
+
// memoize / cache the cube or role payload in process memory
|
|
363
|
+
// or in `cubes.json` — `borg_reassign-drone` changes the
|
|
364
|
+
// calling drone's role in the DB, and subsequent
|
|
365
|
+
// `borg_cube` reads MUST reflect the new role within one
|
|
366
|
+
// round-trip. Locked in by the regression probe at
|
|
367
|
+
// `client/__tests__/integration/01-role-cache-freshness.integration.ts`;
|
|
368
|
+
// a future refactor that introduces caching here will fail
|
|
369
|
+
// that probe. See drone-8's 19:38:56 observation + drone-6's
|
|
370
|
+
// 06:00:31 finding for the original incident trace.
|
|
371
|
+
const active = await requireActiveCube();
|
|
372
|
+
const [{ cube, roles }] = await Promise.all([
|
|
373
|
+
getCubeInfo(active.sessionToken, active.apiUrl, active.serverTrustIdentity),
|
|
374
|
+
getRoleInfo(active.sessionToken, active.apiUrl, active.serverTrustIdentity),
|
|
375
|
+
]);
|
|
376
|
+
const lines = [];
|
|
377
|
+
lines.push(`# Cube: ${cube.name}`);
|
|
378
|
+
lines.push('');
|
|
379
|
+
lines.push('## Cube directive');
|
|
380
|
+
lines.push(cube.cube_directive || '_(none)_');
|
|
381
|
+
lines.push('');
|
|
382
|
+
const taxonomyTip = nullTaxonomyTip(cube.message_taxonomy);
|
|
383
|
+
if (taxonomyTip) {
|
|
384
|
+
lines.push(taxonomyTip);
|
|
385
|
+
lines.push('');
|
|
386
|
+
}
|
|
387
|
+
lines.push('## Roles in this cube');
|
|
388
|
+
if (!roles.length) {
|
|
389
|
+
lines.push('_(no roles defined)_');
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
for (const r of roles) {
|
|
393
|
+
const tags = [
|
|
394
|
+
r.role_class === 'queen' ? 'Queen' : null,
|
|
395
|
+
r.is_human_seat ? 'human-seat' : null,
|
|
396
|
+
r.is_default ? 'default' : null,
|
|
397
|
+
r.is_mandatory ? 'mandatory' : null,
|
|
398
|
+
].filter(Boolean).join(', ');
|
|
399
|
+
const marker = tags ? ` (${tags})` : '';
|
|
400
|
+
const desc = r.short_description || '_(no description)_';
|
|
401
|
+
lines.push(`- **${r.name}**${marker} — ${desc}`);
|
|
402
|
+
}
|
|
403
|
+
// Sprint 6 / gh#153 discoverability nudge per drone-7 UX-FEEDBACK:
|
|
404
|
+
// point Coordinator-class readers at the tool that surfaces role IDs.
|
|
405
|
+
lines.push('');
|
|
406
|
+
lines.push('_(Coordinator-class drones can fetch role IDs via `borg_list-roles` for use with `borg_reassign-drone`.)_');
|
|
407
|
+
}
|
|
408
|
+
lines.push('');
|
|
409
|
+
lines.push(getDronePlaybook());
|
|
410
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
411
|
+
}
|
|
412
|
+
case 'borg_role': {
|
|
413
|
+
// No-cache invariant (T1.2 — 0.7.0): role reads MUST fetch
|
|
414
|
+
// fresh per invocation. See 01-role-cache-freshness.integration.ts.
|
|
415
|
+
const active = await requireActiveCube();
|
|
416
|
+
const requested = typeof args?.role === 'string' ? args.role.trim() : '';
|
|
417
|
+
if (requested) {
|
|
418
|
+
const { role } = await getRoleInfoByName(active.sessionToken, active.apiUrl, requested, active.serverTrustIdentity);
|
|
419
|
+
assertRoleMatches(requested, role);
|
|
420
|
+
const text = [
|
|
421
|
+
`# Role: ${role.name}`,
|
|
422
|
+
``,
|
|
423
|
+
role.detailed_description || '_(no detailed description set)_',
|
|
424
|
+
].join('\n');
|
|
425
|
+
return { content: [{ type: 'text', text }] };
|
|
426
|
+
}
|
|
427
|
+
const { role } = await getRoleInfo(active.sessionToken, active.apiUrl, active.serverTrustIdentity);
|
|
428
|
+
const text = [
|
|
429
|
+
`# Your role: ${role.name}`,
|
|
430
|
+
``,
|
|
431
|
+
role.detailed_description || '_(no detailed description set)_',
|
|
432
|
+
].join('\n');
|
|
433
|
+
return { content: [{ type: 'text', text }] };
|
|
434
|
+
}
|
|
435
|
+
case 'borg_role-rationale': {
|
|
436
|
+
const active = await requireActiveCube();
|
|
437
|
+
const role = typeof args?.role === 'string' ? args.role : '';
|
|
438
|
+
const section = typeof args?.section === 'string' ? args.section : '';
|
|
439
|
+
const result = await roleRationale(active.sessionToken, active.apiUrl, role, section, active.serverTrustIdentity);
|
|
440
|
+
const text = [
|
|
441
|
+
`# Role rationale: ${result.role} — ${result.section}`,
|
|
442
|
+
'',
|
|
443
|
+
result.body || '_(empty)_',
|
|
444
|
+
].join('\n');
|
|
445
|
+
return { content: [{ type: 'text', text }] };
|
|
446
|
+
}
|
|
447
|
+
case 'borg_roster': {
|
|
448
|
+
const active = await requireActiveCube();
|
|
449
|
+
const since = typeof args?.since === 'string' ? args.since : undefined;
|
|
450
|
+
const { drones, roles, since: resolvedSince } = await getRoster(active.sessionToken, active.apiUrl, since, active.serverTrustIdentity);
|
|
451
|
+
const text = renderRoster({
|
|
452
|
+
cubeName: active.name,
|
|
453
|
+
drones,
|
|
454
|
+
roles,
|
|
455
|
+
resolvedSince: resolvedSince ?? null,
|
|
456
|
+
humanAgo,
|
|
457
|
+
});
|
|
458
|
+
return { content: [{ type: 'text', text }] };
|
|
459
|
+
}
|
|
460
|
+
case 'borg_stream-status': {
|
|
461
|
+
// Probe the in-process SSE consumer state. Does NOT require
|
|
462
|
+
// an active cube — if the consumer hasn't started or is
|
|
463
|
+
// between cubes, the snapshot still reports current values.
|
|
464
|
+
// Also probes wake-path completeness (T1.2): is anyone tailing
|
|
465
|
+
// the inbox file? Without that, SSE delivery still works but
|
|
466
|
+
// the harness `/loop` never wakes on the file write.
|
|
467
|
+
const status = getStreamStatus();
|
|
468
|
+
const active = await getActiveCube();
|
|
469
|
+
const inboxPath = active
|
|
470
|
+
? inboxPathForDrone(active.cubeId, active.droneId)
|
|
471
|
+
: null;
|
|
472
|
+
const monitorStateRoot = active
|
|
473
|
+
? monitorStateRootForWorktree(findProjectRoot())
|
|
474
|
+
: null;
|
|
475
|
+
// Non-Claude CLIs have their own wake mechanism, so this Claude-only
|
|
476
|
+
// Monitor diagnostic must not be inferred from Codex transport state.
|
|
477
|
+
const nonClaudeSession = active && resolveSessionAgentKind() !== 'claude';
|
|
478
|
+
const inboxMonitorHealthy = active
|
|
479
|
+
? nonClaudeSession
|
|
480
|
+
? true
|
|
481
|
+
: checkInboxMonitorHealthy(inboxPath, monitorStateRoot)
|
|
482
|
+
: null;
|
|
483
|
+
let silentInertWarning = '';
|
|
484
|
+
if (status.runLoopHealth === 'silent-inert') {
|
|
485
|
+
silentInertWarning = '## ⚠ SSE stream loop silent-inert — run /mcp and reconnect to restart\n\n' +
|
|
486
|
+
'The log-stream consumer started but never connected. This drone will not receive real-time cube events.\n\n';
|
|
487
|
+
}
|
|
488
|
+
const text = renderStreamStatus({
|
|
489
|
+
status,
|
|
490
|
+
inboxMonitorHealthy,
|
|
491
|
+
inboxPath,
|
|
492
|
+
monitorStateRoot,
|
|
493
|
+
droneLabel: active?.droneLabel ?? null,
|
|
494
|
+
cubeName: active?.name ?? null,
|
|
495
|
+
humanAgo,
|
|
496
|
+
});
|
|
497
|
+
return { content: [{ type: 'text', text: silentInertWarning + text }] };
|
|
498
|
+
}
|
|
499
|
+
case 'borg_read-log': {
|
|
500
|
+
const active = await requireActiveCube();
|
|
501
|
+
const since = typeof args?.since === 'string' ? args.since : undefined;
|
|
502
|
+
const limit = typeof args?.limit === 'number' ? args.limit : undefined;
|
|
503
|
+
const unreadOnly = args?.unread_only === true || args?.unread_only === 'true';
|
|
504
|
+
const { entries, drones, roles, behind_by, has_more } = await readLog(active.sessionToken, active.apiUrl, {
|
|
505
|
+
since,
|
|
506
|
+
limit,
|
|
507
|
+
unreadOnly,
|
|
508
|
+
serverTrustIdentity: active.serverTrustIdentity,
|
|
509
|
+
});
|
|
510
|
+
const droneById = new Map();
|
|
511
|
+
for (const d of drones)
|
|
512
|
+
droneById.set(d.id, d);
|
|
513
|
+
const roleById = new Map();
|
|
514
|
+
for (const r of roles)
|
|
515
|
+
roleById.set(r.id, r);
|
|
516
|
+
const lines = [];
|
|
517
|
+
lines.push(`# Activity log: ${active.name}`);
|
|
518
|
+
lines.push('');
|
|
519
|
+
if (!entries.length) {
|
|
520
|
+
lines.push('_(no entries)_');
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
for (const e of entries) {
|
|
524
|
+
lines.push(formatLogEntryMarkdown(e, droneById, roleById));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// gh#709 part B: nudge a behind drone to drain. `behind_by` is the
|
|
528
|
+
// count of entries VISIBLE to this drone still unread AFTER this read
|
|
529
|
+
// advanced the watermark — if > 0 you under-read (limit-capped or a
|
|
530
|
+
// non-cursor read) and will skip messages unless you keep reading.
|
|
531
|
+
if (has_more === true) {
|
|
532
|
+
lines.push('');
|
|
533
|
+
lines.push(`⚠ has_more: true — call \`borg_read-log unread_only=true\` again until has_more=false so you finish draining unread entries.`);
|
|
534
|
+
}
|
|
535
|
+
else if (typeof behind_by === 'number' && behind_by > 0) {
|
|
536
|
+
lines.push('');
|
|
537
|
+
lines.push(`⚠ behind_by: ${behind_by} more unread ${behind_by === 1 ? 'entry' : 'entries'} addressed to you — call \`borg_read-log unread_only=true\` again until behind_by=0 so you don't skip messages.`);
|
|
538
|
+
}
|
|
539
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
540
|
+
}
|
|
541
|
+
case 'borg_log': {
|
|
542
|
+
const message = args?.message;
|
|
543
|
+
if (!message || typeof message !== 'string')
|
|
544
|
+
throw new Error('message is required');
|
|
545
|
+
const active = await getActiveCube();
|
|
546
|
+
if (!active)
|
|
547
|
+
throw new Error('Not assimilated to a cube. Use borg_assimilate <cube-name> first.');
|
|
548
|
+
if (lifecycleSignalForMessage(message)) {
|
|
549
|
+
const decision = await shouldSuppressLifecycleLog(active, message);
|
|
550
|
+
if (decision.suppress) {
|
|
551
|
+
await recordLifecycleLog(active, message);
|
|
552
|
+
return {
|
|
553
|
+
content: [
|
|
554
|
+
{
|
|
555
|
+
type: 'text',
|
|
556
|
+
text: `Suppressed duplicate ${decision.signal?.toUpperCase()} lifecycle log for ${active.droneLabel}; recent cube log already contains this signal.`,
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const hasTo = Object.prototype.hasOwnProperty.call(args ?? {}, 'to');
|
|
563
|
+
const recipients = hasTo ? normalizeDirectLogRecipients(args?.to) : undefined;
|
|
564
|
+
const explicitClass = typeof args?.class === 'string' ? args.class : undefined;
|
|
565
|
+
const visibility = args?.visibility === 'broadcast' || args?.visibility === 'direct'
|
|
566
|
+
? args.visibility
|
|
567
|
+
: undefined;
|
|
568
|
+
if (!active.serverTrustIdentity) {
|
|
569
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
570
|
+
}
|
|
571
|
+
const appendOpts = {
|
|
572
|
+
...(explicitClass ? { class: explicitClass } : {}),
|
|
573
|
+
...(hasTo ? { to: recipients ?? [] } : {}),
|
|
574
|
+
...(visibility ? { visibility } : {}),
|
|
575
|
+
serverTrustIdentity: active.serverTrustIdentity,
|
|
576
|
+
};
|
|
577
|
+
const result = await appendLog(active.sessionToken, active.apiUrl, message, appendOpts);
|
|
578
|
+
await recordLifecycleLog(active, message);
|
|
579
|
+
const echo = result.routing?.message ? `\n${result.routing.message}` : '';
|
|
580
|
+
// gh#534: surface to the SENDER which directed recipients are
|
|
581
|
+
// currently unreachable via the wake path. The message is delivered
|
|
582
|
+
// regardless (persisted server-side); they read it when they return.
|
|
583
|
+
const unreachable = result.unreachableRecipients?.length
|
|
584
|
+
? `\n⚠ ${result.unreachableRecipients.length} directed recipient(s) currently unreachable (wake-path:deaf): ${result.unreachableRecipients
|
|
585
|
+
.map((r) => r.label)
|
|
586
|
+
.join(', ')}. Message delivered — they'll read it when they return.`
|
|
587
|
+
: '';
|
|
588
|
+
const text = `Logged to cube "${active.name}" as ${active.droneLabel}. (entry id: ${result.entry.id})${echo}${unreachable}`;
|
|
589
|
+
return { content: [{ type: 'text', text }] };
|
|
590
|
+
}
|
|
591
|
+
case 'borg_ack': {
|
|
592
|
+
const entryId = args?.entry_id;
|
|
593
|
+
if (!entryId || typeof entryId !== 'string') {
|
|
594
|
+
throw new Error('entry_id is required');
|
|
595
|
+
}
|
|
596
|
+
// gh#418: default 'ack'. Only 'claim' is the other allowed kind; the
|
|
597
|
+
// worker re-validates at the Zod boundary so an unknown value is
|
|
598
|
+
// rejected server-side, but normalize here to keep the wire clean.
|
|
599
|
+
const kind = args?.kind === 'claim' ? 'claim' : 'ack';
|
|
600
|
+
const active = await requireActiveCube();
|
|
601
|
+
await ackLogEntry(active.sessionToken, active.apiUrl, entryId, kind, active.serverTrustIdentity);
|
|
602
|
+
return {
|
|
603
|
+
content: [
|
|
604
|
+
{
|
|
605
|
+
type: 'text',
|
|
606
|
+
text: kind === 'claim'
|
|
607
|
+
? `Claimed entry ${entryId} in cube "${active.name}" (advisory — merge stays keyed on REVIEW-APPROVED).`
|
|
608
|
+
: `Acked entry ${entryId} in cube "${active.name}".`,
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
case 'borg_decide': {
|
|
614
|
+
const topic = args?.topic;
|
|
615
|
+
const decision = args?.decision;
|
|
616
|
+
if (!topic || typeof topic !== 'string')
|
|
617
|
+
throw new Error('topic is required');
|
|
618
|
+
if (!decision || typeof decision !== 'string')
|
|
619
|
+
throw new Error('decision is required');
|
|
620
|
+
const rationale = typeof args?.rationale === 'string' ? args.rationale : undefined;
|
|
621
|
+
const active = await requireActiveCube();
|
|
622
|
+
const { decision: row } = await recordDecision(active.sessionToken, active.apiUrl, {
|
|
623
|
+
topic,
|
|
624
|
+
decision,
|
|
625
|
+
...(rationale !== undefined ? { rationale } : {}),
|
|
626
|
+
}, active.serverTrustIdentity);
|
|
627
|
+
const superseded = row?.supersedes ? ' (superseded the prior decision on this topic)' : '';
|
|
628
|
+
return {
|
|
629
|
+
content: [
|
|
630
|
+
{
|
|
631
|
+
type: 'text',
|
|
632
|
+
text: `Recorded ratified decision on "${topic}" in cube "${active.name}"${superseded}. Cite it via borg_decisions; it surfaces in borg_regen.`,
|
|
633
|
+
},
|
|
634
|
+
],
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
case 'borg_decisions': {
|
|
638
|
+
const topic = typeof args?.topic === 'string' ? args.topic : undefined;
|
|
639
|
+
const active = await requireActiveCube();
|
|
640
|
+
const { decisions } = await listDecisions(active.sessionToken, active.apiUrl, topic, active.serverTrustIdentity);
|
|
641
|
+
const text = decisions.length === 0
|
|
642
|
+
? topic
|
|
643
|
+
? `No active ratified decision on "${topic}" in cube "${active.name}".`
|
|
644
|
+
: `No active ratified decisions in cube "${active.name}".`
|
|
645
|
+
: decisions
|
|
646
|
+
.map((d) => `**${d.topic}:** ${d.decision}${d.rationale ? ` — ${d.rationale}` : ''}`)
|
|
647
|
+
.join('\n');
|
|
648
|
+
return { content: [{ type: 'text', text }] };
|
|
649
|
+
}
|
|
650
|
+
case 'borg_remove-decision': {
|
|
651
|
+
const topic = typeof args?.topic === 'string' ? args.topic : undefined;
|
|
652
|
+
const decisionId = typeof args?.decision_id === 'string' ? args.decision_id : undefined;
|
|
653
|
+
if (Number(topic !== undefined) + Number(decisionId !== undefined) !== 1) {
|
|
654
|
+
throw new Error('provide exactly one of topic or decision_id');
|
|
655
|
+
}
|
|
656
|
+
const active = await requireActiveCube();
|
|
657
|
+
const selector = topic !== undefined ? { topic } : { decision_id: decisionId };
|
|
658
|
+
const { decision } = await removeDecision(active.sessionToken, active.apiUrl, selector, active.serverTrustIdentity);
|
|
659
|
+
return {
|
|
660
|
+
content: [{
|
|
661
|
+
type: 'text',
|
|
662
|
+
text: `Removed the active ratified decision on "${decision.topic}" from cube "${active.name}".`,
|
|
663
|
+
}],
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
case 'borg_list-cubes': {
|
|
667
|
+
const { cubes } = await listCubes();
|
|
668
|
+
if (!cubes.length) {
|
|
669
|
+
return { content: [{ type: 'text', text: 'No cubes yet. Use borg_create-cube to make your first one.' }] };
|
|
670
|
+
}
|
|
671
|
+
const lines = cubes.map((c) => `- **${c.name}** (id: ${c.id})\n ${(c.cube_directive || '_(no directive set)_').split('\n')[0].slice(0, 120)}`);
|
|
672
|
+
return { content: [{ type: 'text', text: `Your cubes (${cubes.length}):\n\n${lines.join('\n\n')}` }] };
|
|
673
|
+
}
|
|
674
|
+
case 'borg_create-cube': {
|
|
675
|
+
const name = args?.name;
|
|
676
|
+
const cubeDirective = args?.cube_directive;
|
|
677
|
+
const templateName = args?.template;
|
|
678
|
+
if (!name)
|
|
679
|
+
throw new Error('name is required');
|
|
680
|
+
if (cubeDirective === undefined)
|
|
681
|
+
throw new Error('cube_directive is required (pass empty string if none)');
|
|
682
|
+
// Resolve template (validates name early so the cube isn't
|
|
683
|
+
// created in a partial state if the template name is wrong).
|
|
684
|
+
let template = null;
|
|
685
|
+
if (templateName) {
|
|
686
|
+
template = getTemplate(templateName);
|
|
687
|
+
if (!template) {
|
|
688
|
+
throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// Sprint 14: template cube_directive fills empty operator input.
|
|
692
|
+
// Operator-supplied text takes precedence — templates fill
|
|
693
|
+
// the blank, never stomp.
|
|
694
|
+
const resolvedCubeDirective = resolveCubeDirectiveForCreate(cubeDirective, template);
|
|
695
|
+
// v0.9.2: createCube now returns the flat shape directly
|
|
696
|
+
// (see remote-client unwrap). `cube.id` / `cube.name` work
|
|
697
|
+
// verbatim on the returned object.
|
|
698
|
+
const resolvedMessageTaxonomy = resolveMessageTaxonomyForCreate(undefined, template);
|
|
699
|
+
const cube = await createCube(name, resolvedCubeDirective, {
|
|
700
|
+
message_taxonomy: resolvedMessageTaxonomy,
|
|
701
|
+
});
|
|
702
|
+
// Apply template roles if requested. Merges by name: any role the
|
|
703
|
+
// server auto-seeded (e.g. "Drone") that the template doesn't
|
|
704
|
+
// also include stays put; templated roles upsert.
|
|
705
|
+
if (template) {
|
|
706
|
+
const summary = await applyTemplateToCube(cube.id, template);
|
|
707
|
+
const cubeDirectiveNote = resolvedCubeDirective !== cubeDirective
|
|
708
|
+
? ' Template cube directive applied (operator passed empty).'
|
|
709
|
+
: '';
|
|
710
|
+
const text = `Created cube **${cube.name}** (id: ${cube.id}) with template **${templateName}** applied — ${summary.created} role(s) created, ${summary.updated} updated.${cubeDirectiveNote} Use borg_assimilate ${cube.name} to join as a drone.`;
|
|
711
|
+
return { content: [{ type: 'text', text }] };
|
|
712
|
+
}
|
|
713
|
+
const text = `Created cube **${cube.name}** (id: ${cube.id}). A default "Drone" role was seeded — rename or replace it via borg_update-role / borg_create-role / borg_delete-role. Use borg_assimilate ${cube.name} to join as a drone.`;
|
|
714
|
+
return { content: [{ type: 'text', text }] };
|
|
715
|
+
}
|
|
716
|
+
case 'borg_update-cube': {
|
|
717
|
+
const cubeId = args?.cube_id;
|
|
718
|
+
if (!cubeId)
|
|
719
|
+
throw new Error('cube_id is required');
|
|
720
|
+
const updates = {};
|
|
721
|
+
if (typeof args?.name === 'string')
|
|
722
|
+
updates.name = args.name;
|
|
723
|
+
if (typeof args?.cube_directive === 'string')
|
|
724
|
+
updates.cube_directive = args.cube_directive;
|
|
725
|
+
if (Array.isArray(args?.message_taxonomy))
|
|
726
|
+
updates.message_taxonomy = args.message_taxonomy;
|
|
727
|
+
if (Object.keys(updates).length === 0)
|
|
728
|
+
throw new Error('Pass at least one of: name, cube_directive, message_taxonomy.');
|
|
729
|
+
const { cube } = await updateCube(cubeId, updates);
|
|
730
|
+
return { content: [{ type: 'text', text: `Updated cube **${cube.name}** (id: ${cube.id}).` }] };
|
|
731
|
+
}
|
|
732
|
+
case 'borg_patch-taxonomy-class': {
|
|
733
|
+
const cubeId = args?.cube_id;
|
|
734
|
+
if (!cubeId)
|
|
735
|
+
throw new Error('cube_id is required');
|
|
736
|
+
const action = args?.action;
|
|
737
|
+
if (action !== 'add' && action !== 'replace' && action !== 'remove') {
|
|
738
|
+
throw new Error('action must be one of: add, replace, remove.');
|
|
739
|
+
}
|
|
740
|
+
let cube;
|
|
741
|
+
let label;
|
|
742
|
+
if (action === 'remove') {
|
|
743
|
+
const className = args?.class;
|
|
744
|
+
if (!className)
|
|
745
|
+
throw new Error('class is required for remove.');
|
|
746
|
+
({ cube } = await patchTaxonomyClass(cubeId, { action, class: className }));
|
|
747
|
+
label = className;
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
const classDef = args?.class_def;
|
|
751
|
+
if (classDef == null || typeof classDef !== 'object' || Array.isArray(classDef)) {
|
|
752
|
+
throw new Error('class_def (object) is required for add/replace.');
|
|
753
|
+
}
|
|
754
|
+
({ cube } = await patchTaxonomyClass(cubeId, { action, class_def: classDef }));
|
|
755
|
+
label = String(classDef.class ?? '');
|
|
756
|
+
}
|
|
757
|
+
const verb = action === 'add' ? 'Added' : action === 'replace' ? 'Replaced' : 'Removed';
|
|
758
|
+
return { content: [{ type: 'text', text: `${verb} taxonomy class **${label}** in cube **${cube.name}** (id: ${cube.id}).` }] };
|
|
759
|
+
}
|
|
760
|
+
case 'borg_delete-cube': {
|
|
761
|
+
const cubeId = args?.cube_id;
|
|
762
|
+
if (!cubeId)
|
|
763
|
+
throw new Error('cube_id is required');
|
|
764
|
+
await deleteCube(cubeId);
|
|
765
|
+
return { content: [{ type: 'text', text: `Deleted cube ${cubeId} (and all its roles, drones, log entries).` }] };
|
|
766
|
+
}
|
|
767
|
+
case 'borg_create-role': {
|
|
768
|
+
const cubeId = args?.cube_id;
|
|
769
|
+
const name = args?.name;
|
|
770
|
+
const shortDesc = args?.short_description;
|
|
771
|
+
const detailedDesc = args?.detailed_description;
|
|
772
|
+
if (!cubeId)
|
|
773
|
+
throw new Error('cube_id is required');
|
|
774
|
+
if (!name)
|
|
775
|
+
throw new Error('name is required');
|
|
776
|
+
if (shortDesc === undefined)
|
|
777
|
+
throw new Error('short_description is required (pass empty string if none)');
|
|
778
|
+
if (detailedDesc === undefined)
|
|
779
|
+
throw new Error('detailed_description is required (pass empty string if none)');
|
|
780
|
+
const isDefault = args?.is_default === true;
|
|
781
|
+
const isMandatory = args?.is_mandatory === true;
|
|
782
|
+
const isHumanSeat = args?.is_human_seat === true;
|
|
783
|
+
const canBroadcast = args?.can_broadcast === true;
|
|
784
|
+
const receivesAllDirect = args?.receives_all_direct === true;
|
|
785
|
+
const { role } = await createRole(cubeId, {
|
|
786
|
+
name,
|
|
787
|
+
short_description: shortDesc,
|
|
788
|
+
detailed_description: detailedDesc,
|
|
789
|
+
is_default: isDefault,
|
|
790
|
+
is_mandatory: isMandatory,
|
|
791
|
+
is_human_seat: isHumanSeat,
|
|
792
|
+
can_broadcast: canBroadcast,
|
|
793
|
+
receives_all_direct: receivesAllDirect,
|
|
794
|
+
...(typeof args?.default_model === 'string' ? { default_model: args.default_model } : {}),
|
|
795
|
+
});
|
|
796
|
+
const tags = [
|
|
797
|
+
role.role_class === 'queen' ? 'Queen' : null,
|
|
798
|
+
role.is_human_seat ? 'human-seat' : null,
|
|
799
|
+
role.is_default ? 'default' : null,
|
|
800
|
+
role.is_mandatory ? 'mandatory' : null,
|
|
801
|
+
].filter(Boolean).join(', ');
|
|
802
|
+
const tag = tags ? ` (${tags})` : '';
|
|
803
|
+
return { content: [{ type: 'text', text: `Created role **${role.name}**${tag} (id: ${role.id}) in cube ${cubeId}.` }] };
|
|
804
|
+
}
|
|
805
|
+
case 'borg_update-role': {
|
|
806
|
+
const roleId = args?.role_id;
|
|
807
|
+
if (!roleId)
|
|
808
|
+
throw new Error('role_id is required');
|
|
809
|
+
const updates = {};
|
|
810
|
+
if (typeof args?.name === 'string')
|
|
811
|
+
updates.name = args.name;
|
|
812
|
+
if (typeof args?.short_description === 'string')
|
|
813
|
+
updates.short_description = args.short_description;
|
|
814
|
+
if (typeof args?.detailed_description === 'string')
|
|
815
|
+
updates.detailed_description = args.detailed_description;
|
|
816
|
+
if (typeof args?.is_default === 'boolean')
|
|
817
|
+
updates.is_default = args.is_default;
|
|
818
|
+
if (typeof args?.is_mandatory === 'boolean')
|
|
819
|
+
updates.is_mandatory = args.is_mandatory;
|
|
820
|
+
if (typeof args?.is_human_seat === 'boolean')
|
|
821
|
+
updates.is_human_seat = args.is_human_seat;
|
|
822
|
+
if (typeof args?.can_broadcast === 'boolean')
|
|
823
|
+
updates.can_broadcast = args.can_broadcast;
|
|
824
|
+
if (typeof args?.receives_all_direct === 'boolean')
|
|
825
|
+
updates.receives_all_direct = args.receives_all_direct;
|
|
826
|
+
if (typeof args?.default_model === 'string')
|
|
827
|
+
updates.default_model = args.default_model;
|
|
828
|
+
if (Object.keys(updates).length === 0)
|
|
829
|
+
throw new Error('Pass at least one of: name, short_description, detailed_description, is_default, is_mandatory, is_human_seat, can_broadcast, receives_all_direct.');
|
|
830
|
+
const { role } = await updateRole(roleId, updates);
|
|
831
|
+
const tags = [
|
|
832
|
+
role.role_class === 'queen' ? 'Queen' : null,
|
|
833
|
+
role.is_human_seat ? 'human-seat' : null,
|
|
834
|
+
role.is_default ? 'default' : null,
|
|
835
|
+
role.is_mandatory ? 'mandatory' : null,
|
|
836
|
+
].filter(Boolean).join(', ');
|
|
837
|
+
const tag = tags ? ` (${tags})` : '';
|
|
838
|
+
return { content: [{ type: 'text', text: `Updated role **${role.name}**${tag} (id: ${role.id}).` }] };
|
|
839
|
+
}
|
|
840
|
+
case 'borg_patch-role-section': {
|
|
841
|
+
const roleId = args?.role_id;
|
|
842
|
+
if (!roleId)
|
|
843
|
+
throw new Error('role_id is required');
|
|
844
|
+
const action = args?.action;
|
|
845
|
+
if (action !== 'replace' && action !== 'insert' && action !== 'delete') {
|
|
846
|
+
throw new Error('action must be one of: replace, insert, delete.');
|
|
847
|
+
}
|
|
848
|
+
const heading = args?.heading;
|
|
849
|
+
if (!heading)
|
|
850
|
+
throw new Error('heading is required');
|
|
851
|
+
let role;
|
|
852
|
+
if (action === 'delete') {
|
|
853
|
+
({ role } = await patchRoleSection(roleId, { action, heading }));
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
const body = args?.body;
|
|
857
|
+
if (typeof body !== 'string') {
|
|
858
|
+
throw new Error('body is required for replace/insert (pass empty string for an empty section).');
|
|
859
|
+
}
|
|
860
|
+
if (action === 'insert') {
|
|
861
|
+
const after = (typeof args?.after === 'string' ? args.after : null);
|
|
862
|
+
({ role } = await patchRoleSection(roleId, { action, heading, body, after }));
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
({ role } = await patchRoleSection(roleId, { action, heading, body }));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
const verb = action === 'replace' ? 'Replaced' : action === 'insert' ? 'Inserted' : 'Deleted';
|
|
869
|
+
return { content: [{ type: 'text', text: `${verb} section **${heading}** in role **${role.name}** (id: ${role.id}).` }] };
|
|
870
|
+
}
|
|
871
|
+
case 'borg_delete-role': {
|
|
872
|
+
const roleId = args?.role_id;
|
|
873
|
+
if (!roleId)
|
|
874
|
+
throw new Error('role_id is required');
|
|
875
|
+
await deleteRole(roleId);
|
|
876
|
+
return { content: [{ type: 'text', text: `Deleted role ${roleId}.` }] };
|
|
877
|
+
}
|
|
878
|
+
case 'borg_reassign-drone': {
|
|
879
|
+
return {
|
|
880
|
+
content: [{ type: 'text', text: await runReassignDroneTool({
|
|
881
|
+
droneId: args?.drone_id,
|
|
882
|
+
roleId: args?.role_id,
|
|
883
|
+
}) }],
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
case 'borg_evict-drone': {
|
|
887
|
+
return {
|
|
888
|
+
content: [{ type: 'text', text: await runEvictDroneTool(args) }],
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
case 'borg_list-drones': {
|
|
892
|
+
const cubeId = args?.cube_id;
|
|
893
|
+
if (!cubeId)
|
|
894
|
+
throw new Error('cube_id is required');
|
|
895
|
+
const { drones, roles } = await getCube(cubeId);
|
|
896
|
+
if (!drones.length) {
|
|
897
|
+
return { content: [{ type: 'text', text: 'No drones in this cube yet.' }] };
|
|
898
|
+
}
|
|
899
|
+
const rolesById = new Map(roles.map((r) => [r.id, r]));
|
|
900
|
+
const lines = drones.map((d) => {
|
|
901
|
+
const r = rolesById.get(d.role_id);
|
|
902
|
+
const roleLabel = formatRoleAgentLabel(r?.name ?? '?', d.agent_kind);
|
|
903
|
+
const wakeClass = d.wake_path_alert_class && d.wake_path_alert_class !== 'independent'
|
|
904
|
+
? ` — wake-path-class: ${d.wake_path_alert_class}`
|
|
905
|
+
: '';
|
|
906
|
+
const reportedModel = d.reported_model
|
|
907
|
+
? ` — Reported model: ${d.reported_model}`
|
|
908
|
+
: ' — Reported model: not reported';
|
|
909
|
+
const workingRepo = formatWorkingRepoLabel(d);
|
|
910
|
+
const workingRepoText = workingRepo ? ` — ${workingRepo}` : '';
|
|
911
|
+
return `- **${d.label}** (id: ${d.id}) — role: ${roleLabel} (${d.role_id}) — last seen ${d.last_seen}${wakeClass}${reportedModel}${workingRepoText}`;
|
|
912
|
+
});
|
|
913
|
+
return { content: [{ type: 'text', text: `Drones in cube ${cubeId} (${drones.length}):\n\n${lines.join('\n')}` }] };
|
|
914
|
+
}
|
|
915
|
+
case 'borg_list-roles': {
|
|
916
|
+
// Sprint 6 / gh#153: surface role IDs to Coordinator-class drones
|
|
917
|
+
// for use with borg_reassign-drone (e.g. promoting a drone to Queen).
|
|
918
|
+
// Uses the same cube-grant-scoped GET /api/cubes/:id endpoint as
|
|
919
|
+
// borg_list-drones — data is already accessible to the cube owner
|
|
920
|
+
// over the local server; this tool just makes role IDs
|
|
921
|
+
// discoverable inside the MCP tool namespace per drone-7's UX-FEEDBACK.
|
|
922
|
+
// Render logic extracted to list-roles-render.ts for testability
|
|
923
|
+
// per drone-3 QA-FAIL 2026-05-18T13:27:53Z.
|
|
924
|
+
const cubeId = args?.cube_id;
|
|
925
|
+
if (!cubeId)
|
|
926
|
+
throw new Error('cube_id is required');
|
|
927
|
+
const roles = await listRoles(cubeId);
|
|
928
|
+
return { content: [{ type: 'text', text: renderRoleList(roles, cubeId) }] };
|
|
929
|
+
}
|
|
930
|
+
case 'borg_list-templates': {
|
|
931
|
+
const names = listTemplateNames();
|
|
932
|
+
const lines = names.map((n) => {
|
|
933
|
+
const t = getTemplate(n);
|
|
934
|
+
return `- **${n}**: ${t.description}`;
|
|
935
|
+
});
|
|
936
|
+
return { content: [{ type: 'text', text: `Available templates:\n\n${lines.join('\n')}` }] };
|
|
937
|
+
}
|
|
938
|
+
case 'borg_sync-roles': {
|
|
939
|
+
const cubeId = args?.cube_id;
|
|
940
|
+
const templateName = args?.template_name || 'software-dev';
|
|
941
|
+
const apply = args?.apply === true;
|
|
942
|
+
// gh#473 PR2 — per-conflict-fragment accept/reject decisions.
|
|
943
|
+
// Keyed on the stable fragment key surfaced by the dry-run (e.g.
|
|
944
|
+
// `role:Builder:section:Workflow`). Unspecified conflicts default
|
|
945
|
+
// to REJECT (keep the cube's evolved text).
|
|
946
|
+
const decisions = args?.decisions && typeof args.decisions === 'object'
|
|
947
|
+
? args.decisions
|
|
948
|
+
: undefined;
|
|
949
|
+
if (!cubeId)
|
|
950
|
+
throw new Error('cube_id is required');
|
|
951
|
+
const result = await syncRoles(cubeId, templateName, apply, decisions);
|
|
952
|
+
return { content: [{ type: 'text', text: renderSyncRolesResult(result, templateName) }] };
|
|
953
|
+
}
|
|
954
|
+
case 'borg_apply-template': {
|
|
955
|
+
const cubeId = args?.cube_id;
|
|
956
|
+
const templateName = args?.template_name;
|
|
957
|
+
if (!cubeId)
|
|
958
|
+
throw new Error('cube_id is required');
|
|
959
|
+
if (!templateName)
|
|
960
|
+
throw new Error('template_name is required');
|
|
961
|
+
const template = getTemplate(templateName);
|
|
962
|
+
if (!template) {
|
|
963
|
+
throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
|
|
964
|
+
}
|
|
965
|
+
const summary = await applyTemplateToCube(cubeId, template);
|
|
966
|
+
// Sprint 14: optionally write template's cube_directive to the
|
|
967
|
+
// cube. No-clobber discipline — only fills empty directives,
|
|
968
|
+
// never overwrites operator-customized text.
|
|
969
|
+
let cubeDirectiveNote = '';
|
|
970
|
+
const cubeForRules = await getCube(cubeId);
|
|
971
|
+
const newCubeDirective = resolveCubeDirectiveForApply(cubeForRules.cube_directive, template);
|
|
972
|
+
if (newCubeDirective !== null) {
|
|
973
|
+
await updateCube(cubeId, { cube_directive: newCubeDirective });
|
|
974
|
+
cubeDirectiveNote = ' Template cube directive applied (cube directive was empty).';
|
|
975
|
+
}
|
|
976
|
+
return { content: [{ type: 'text', text: `Applied template **${templateName}** to cube ${cubeId} — ${summary.created} role(s) created, ${summary.updated} updated.${cubeDirectiveNote}` }] };
|
|
977
|
+
}
|
|
978
|
+
default:
|
|
979
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
catch (error) {
|
|
983
|
+
// gh#877: the drone-lifecycle verdict returns a RECOGNIZABLE tool RESULT
|
|
984
|
+
// (not a generic "Error: ..." the agent retries) so the /loop + role
|
|
985
|
+
// playbook can branch deterministically. Checked FIRST: an evicted seat is
|
|
986
|
+
// a lifecycle terminal — it gets its own recognizable result rather than
|
|
987
|
+
// the generic error rendering below.
|
|
988
|
+
if (error instanceof DroneEvictedError) {
|
|
989
|
+
const active = await getActiveCube();
|
|
990
|
+
return {
|
|
991
|
+
content: [{ type: 'text', text: formatEvictedToolResult(active?.name) }],
|
|
992
|
+
isError: true,
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
const localManageResult = formatLocalManageToolResult(error);
|
|
996
|
+
if (localManageResult)
|
|
997
|
+
return localManageResult;
|
|
998
|
+
return {
|
|
999
|
+
content: [
|
|
1000
|
+
{
|
|
1001
|
+
type: 'text',
|
|
1002
|
+
text: `Error: ${error.message}`,
|
|
1003
|
+
},
|
|
1004
|
+
],
|
|
1005
|
+
isError: true,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
// Register prompts listing — the client exposes no prompts.
|
|
1010
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
1011
|
+
return { prompts: [] };
|
|
1012
|
+
});
|
|
1013
|
+
// Register prompt getter
|
|
1014
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1015
|
+
const { name } = request.params;
|
|
1016
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
1017
|
+
});
|
|
1018
|
+
// Create stdio transport
|
|
1019
|
+
const transport = new StdioServerTransport();
|
|
1020
|
+
// Connect server to transport
|
|
1021
|
+
await server.connect(transport);
|
|
1022
|
+
// Resolve drone self-identification prefix before any console output
|
|
1023
|
+
// (gh#25). Falls back to `[unassimilated · <repo>]` if no cube cached.
|
|
1024
|
+
await initConsolePrefix();
|
|
1025
|
+
console.error(`${consolePrefix()}◼ Borg MCP Client started`);
|
|
1026
|
+
console.error(`${consolePrefix()}◼ Use borg_assimilate <cube-name> to join a cube as a drone`);
|
|
1027
|
+
console.error(`${consolePrefix()}◼ Manage your cubes with borg --help`);
|
|
1028
|
+
}
|
|
1029
|
+
if (isEntryInvocation(process.argv[1], import.meta.url)) {
|
|
1030
|
+
main().catch((error) => {
|
|
1031
|
+
console.error(`${consolePrefix()}Fatal error:`, error);
|
|
1032
|
+
process.exit(1);
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
//# sourceMappingURL=index.js.map
|