borgmcp 1.1.15 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.map +1 -0
- package/dist/codex-app-wake.js +432 -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.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 +31 -18
- package/dist/log-stream.d.ts.map +1 -0
- package/dist/log-stream.js +1214 -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 +65 -175
- package/dist/remote-client.d.ts.map +1 -0
- package/dist/remote-client.js +942 -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 +303 -0
- package/dist/seats.d.ts.map +1 -0
- package/dist/seats.js +551 -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 +505 -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/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 +40 -0
- package/docs/LOCAL_SERVER.md +103 -0
- package/docs/RELEASING.md +120 -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 +512 -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 +1505 -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 +1379 -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 +741 -0
- package/src/self-path.ts +51 -0
- package/src/server-endpoint.ts +38 -0
- package/src/server-errors.ts +93 -0
- package/src/server-handshake.ts +783 -0
- package/src/server-response.ts +76 -0
- package/src/server-trust.ts +245 -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
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Settings Configuration Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles adding borg-mcp to Claude Code via the claude CLI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { dirname } from 'path';
|
|
13
|
+
import {
|
|
14
|
+
BORG_AGENT_KIND_ENV,
|
|
15
|
+
BORG_CODEX_REMOTE_WAKE_ENV,
|
|
16
|
+
withAgentRuntimeEnv,
|
|
17
|
+
} from './agent-runtime.js';
|
|
18
|
+
import {
|
|
19
|
+
resolveMcpBinaryPath,
|
|
20
|
+
resolveRegenPath,
|
|
21
|
+
resolveClearRewakePath,
|
|
22
|
+
resolveLogAuditPath,
|
|
23
|
+
} from './self-path.js';
|
|
24
|
+
import { shellEscape } from './shell-escape.js';
|
|
25
|
+
|
|
26
|
+
// Get __dirname equivalent in ESM
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = dirname(__filename);
|
|
29
|
+
|
|
30
|
+
// gh#client#18: canonical hook commands stored in JSON are shell-escaped
|
|
31
|
+
// absolute paths. Bare names and stale/other-install absolute paths are
|
|
32
|
+
// migrated to this form on every hook write.
|
|
33
|
+
const HOOK_COMMAND = shellEscape(resolveRegenPath());
|
|
34
|
+
const CLEAR_REWAKE_HOOK_COMMAND = shellEscape(resolveClearRewakePath());
|
|
35
|
+
const AUDIT_HOOK_COMMAND = shellEscape(resolveLogAuditPath());
|
|
36
|
+
const MCP_BINARY = resolveMcpBinaryPath();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Claude Code CLI config path. The CLI reads `mcpServers.<name>` from
|
|
40
|
+
* this file to discover registered MCP servers; `addMcpServer()` (below)
|
|
41
|
+
* writes to it via the `claude mcp add --scope user borg borg-mcp` shell
|
|
42
|
+
* command. Server name is `borg` (not `borgmcp` — `borg-mcp` is the
|
|
43
|
+
* binary that backs it). NOTE: distinct from
|
|
44
|
+
* `~/Library/Application Support/Claude/claude_desktop_config.json`,
|
|
45
|
+
* which is the Claude Desktop app's config (different product).
|
|
46
|
+
*/
|
|
47
|
+
const CLAUDE_CONFIG_PATH = path.join(os.homedir(), '.claude.json');
|
|
48
|
+
const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
|
|
49
|
+
const CODEX_HOOKS_PATH = path.join(os.homedir(), '.codex', 'hooks.json');
|
|
50
|
+
const OPENCODE_CONFIG_PATH = path.join(os.homedir(), '.config', 'opencode', 'opencode.json');
|
|
51
|
+
const MCP_SERVER_NAME = 'borg';
|
|
52
|
+
|
|
53
|
+
function settingsPath(): string {
|
|
54
|
+
return path.join(os.homedir(), '.claude', 'settings.json');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readSettings(): any {
|
|
58
|
+
const p = settingsPath();
|
|
59
|
+
if (!fs.existsSync(p)) return {};
|
|
60
|
+
const text = fs.readFileSync(p, 'utf-8');
|
|
61
|
+
if (!text.trim()) return {};
|
|
62
|
+
return JSON.parse(text);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function writeSettings(settings: any): void {
|
|
66
|
+
const p = settingsPath();
|
|
67
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
68
|
+
fs.writeFileSync(p, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readJsonFile(p: string): any {
|
|
72
|
+
if (!fs.existsSync(p)) return {};
|
|
73
|
+
const text = fs.readFileSync(p, 'utf-8');
|
|
74
|
+
if (!text.trim()) return {};
|
|
75
|
+
return JSON.parse(text);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function writeJsonFile(p: string, data: any): void {
|
|
79
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
80
|
+
fs.writeFileSync(p, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Register a Claude Code SessionStart hook that runs `borg-regen` at the
|
|
85
|
+
* start of every session. Idempotent: re-running won't add duplicates.
|
|
86
|
+
*
|
|
87
|
+
* Returns true if a change was made, false otherwise (already present, or
|
|
88
|
+
* settings.json could not be parsed).
|
|
89
|
+
*/
|
|
90
|
+
export function addSessionStartHook(): boolean {
|
|
91
|
+
return addSessionStartHookAt(settingsPath());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* gh#673 P2 (WI-1): install the borg-regen SessionStart hook into a
|
|
96
|
+
* PROJECT-LOCAL `<projectRoot>/.claude/settings.local.json` instead of
|
|
97
|
+
* the global ~/.claude/settings.json. The .local variant is
|
|
98
|
+
* user-authored and uncommitted, so it neither imposes borg-regen on
|
|
99
|
+
* collaborators nor trips committed-project-hook trust prompts
|
|
100
|
+
* (V1-probed: Claude Code merges + fires hooks from it). Written by
|
|
101
|
+
* `borg assimilate` (incl. into freshly spawned sibling worktrees) and
|
|
102
|
+
* ensured on every bare `borg` launch. Idempotent; preserves unrelated
|
|
103
|
+
* settings content; refuses to clobber an unparseable file.
|
|
104
|
+
*/
|
|
105
|
+
export function addProjectSessionStartHook(projectRoot: string): boolean {
|
|
106
|
+
return addSessionStartHookAt(projectSettingsPath(projectRoot), true);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Peek variant of addProjectSessionStartHook — no mutation. */
|
|
110
|
+
export function isProjectSessionStartHookRegistered(projectRoot: string): boolean {
|
|
111
|
+
return sessionStartHookRegisteredAt(projectSettingsPath(projectRoot), true);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function projectSettingsPath(projectRoot: string): string {
|
|
115
|
+
return path.join(projectRoot, '.claude', 'settings.local.json');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function addSessionStartHookAt(settingsFile: string, includeClearRewake = false): boolean {
|
|
119
|
+
let settings: any;
|
|
120
|
+
try {
|
|
121
|
+
settings = readJsonFile(settingsFile);
|
|
122
|
+
} catch (err: any) {
|
|
123
|
+
console.error(`⚠ Could not parse ${settingsFile}: ${err.message}. Skipping hook registration; you can add it manually.`);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
settings.hooks ??= {};
|
|
128
|
+
settings.hooks.SessionStart ??= [];
|
|
129
|
+
|
|
130
|
+
let changed = false;
|
|
131
|
+
|
|
132
|
+
// gh#client#18: migrate owned hooks to canonical form and deduplicate
|
|
133
|
+
// (exactly one canonical hook per owned command, preserving unrelated
|
|
134
|
+
// siblings and entry metadata).
|
|
135
|
+
changed = migrateAndDedupOwnedHooks(settings.hooks.SessionStart) || changed;
|
|
136
|
+
|
|
137
|
+
if (!hasCommandHook(settings.hooks.SessionStart, HOOK_COMMAND)) {
|
|
138
|
+
settings.hooks.SessionStart.push({
|
|
139
|
+
matcher: '*',
|
|
140
|
+
hooks: [{ type: 'command', command: HOOK_COMMAND }],
|
|
141
|
+
});
|
|
142
|
+
changed = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Claude Code's normal orientation hook is intentionally left intact. A
|
|
146
|
+
// second project-local handler matches only `/clear` and uses asyncRewake +
|
|
147
|
+
// exit 2 to force one recovery turn when the otherwise-quiet SessionStart
|
|
148
|
+
// event would leave the interactive session idle.
|
|
149
|
+
if (includeClearRewake) {
|
|
150
|
+
const normalized = normalizeClearRewakeHook(settings.hooks.SessionStart);
|
|
151
|
+
if (normalized) {
|
|
152
|
+
settings.hooks.SessionStart = normalized;
|
|
153
|
+
changed = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!changed) return false;
|
|
158
|
+
|
|
159
|
+
writeJsonFile(settingsFile, settings);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// gh#client#18: match bare names (old configs), stale absolute paths (other
|
|
164
|
+
// installations), shell-escaped canonical paths, and unescaped canonical paths.
|
|
165
|
+
function commandMatches(entryCommand: string, bareName: string, absolutePath: string): boolean {
|
|
166
|
+
const escaped = shellEscape(absolutePath);
|
|
167
|
+
if (entryCommand === escaped || entryCommand === absolutePath || entryCommand === bareName) return true;
|
|
168
|
+
// gh#client#18: stale prior-install absolute paths (e.g.
|
|
169
|
+
// /old/.../dist/regen.js) end with the same basename. Only match
|
|
170
|
+
// when the command IS an absolute path ending with the bare name to
|
|
171
|
+
// avoid false positives on unrelated commands like "run regen.js".
|
|
172
|
+
if (entryCommand.startsWith('/') && entryCommand.endsWith(`/${bareName}`)) return true;
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** gh#client#18: map an owned hook command to its canonical shell-escaped form.
|
|
177
|
+
* Returns null for non-owned commands. Ownership requires EXACTLY ONE of:
|
|
178
|
+
* (a) Exact bare bin name (borg-regen, borg-clear-rewake, borg-log-audit)
|
|
179
|
+
* (b) Exact match (raw or shell-escaped) of THIS installation's canonical
|
|
180
|
+
* absolute command — always owned, no marker required (fixes neutral-path
|
|
181
|
+
* false negatives that broke idempotency)
|
|
182
|
+
* (c) Foreign-install heuristic: absolute path ending in an owned basename
|
|
183
|
+
* AND containing a borg package marker (borgmcp|borg-mcp) in the path
|
|
184
|
+
* This prevents false-positive ownership of unrelated scripts that happen
|
|
185
|
+
* to share a basename (e.g. /opt/custom-tool/regen.js). */
|
|
186
|
+
function ownedCanonical(command: string): string | null {
|
|
187
|
+
const stripped = command.replace(/^'|'$/g, '');
|
|
188
|
+
|
|
189
|
+
// (a) Exact bare name — always owned
|
|
190
|
+
if (stripped === BARE_BORG_REGEN) return HOOK_COMMAND;
|
|
191
|
+
if (stripped === BARE_CLEAR_REWAKE) return CLEAR_REWAKE_HOOK_COMMAND;
|
|
192
|
+
if (stripped === BARE_LOG_AUDIT) return AUDIT_HOOK_COMMAND;
|
|
193
|
+
|
|
194
|
+
// (b) Exact match of THIS installation's canonical command (raw or escaped)
|
|
195
|
+
// — always owned, no marker check required
|
|
196
|
+
if (command === HOOK_COMMAND || stripped === resolveRegenPath()) return HOOK_COMMAND;
|
|
197
|
+
if (command === CLEAR_REWAKE_HOOK_COMMAND || stripped === resolveClearRewakePath()) return CLEAR_REWAKE_HOOK_COMMAND;
|
|
198
|
+
if (command === AUDIT_HOOK_COMMAND || stripped === resolveLogAuditPath()) return AUDIT_HOOK_COMMAND;
|
|
199
|
+
|
|
200
|
+
// (c) Foreign-install heuristic: absolute path + owned basename + borg marker
|
|
201
|
+
if (stripped.startsWith('/') && (stripped.includes('borgmcp') || stripped.includes('borg-mcp'))) {
|
|
202
|
+
const name = stripped.split('/').pop() ?? '';
|
|
203
|
+
if (name === 'regen.js') return HOOK_COMMAND;
|
|
204
|
+
if (name === 'clear-rewake.js') return CLEAR_REWAKE_HOOK_COMMAND;
|
|
205
|
+
if (name === 'log-audit.js') return AUDIT_HOOK_COMMAND;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* gh#client#18: migrate owned hooks to canonical form and deduplicate:
|
|
213
|
+
* exactly one canonical hook per owned command. Removes only duplicate owned
|
|
214
|
+
* hook objects within entries, preserving unrelated siblings and entry metadata.
|
|
215
|
+
* Mutates the entries array in place. Returns true if any change was made.
|
|
216
|
+
*/
|
|
217
|
+
function migrateAndDedupOwnedHooks(entries: any[]): boolean {
|
|
218
|
+
let changed = false;
|
|
219
|
+
|
|
220
|
+
// Phase 1: Migrate owned hooks to canonical form
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
if (!Array.isArray(entry?.hooks)) continue;
|
|
223
|
+
for (const hook of entry.hooks) {
|
|
224
|
+
if (hook?.type !== 'command' || typeof hook?.command !== 'string') continue;
|
|
225
|
+
const canonical = ownedCanonical(hook.command);
|
|
226
|
+
if (canonical && hook.command !== canonical) {
|
|
227
|
+
hook.command = canonical;
|
|
228
|
+
changed = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Phase 2: Deduplicate — for each canonical command, keep exactly one hook
|
|
234
|
+
// object across all entries. Remove only the duplicate hook objects, not
|
|
235
|
+
// entire entries (preserving unrelated siblings and entry metadata).
|
|
236
|
+
const seenCanonicals = new Set<string>();
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
if (!Array.isArray(entry?.hooks)) continue;
|
|
239
|
+
const before = entry.hooks.length;
|
|
240
|
+
entry.hooks = entry.hooks.filter((hook: any) => {
|
|
241
|
+
if (hook?.type !== 'command' || typeof hook?.command !== 'string') return true;
|
|
242
|
+
const canonical = ownedCanonical(hook.command);
|
|
243
|
+
if (!canonical) return true;
|
|
244
|
+
if (seenCanonicals.has(canonical)) {
|
|
245
|
+
changed = true;
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
seenCanonicals.add(canonical);
|
|
249
|
+
return true;
|
|
250
|
+
});
|
|
251
|
+
if (entry.hooks.length !== before) changed = true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Phase 3: Remove entries that became empty after dedup
|
|
255
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
256
|
+
if (!Array.isArray(entries[i]?.hooks) || entries[i].hooks.length === 0) {
|
|
257
|
+
entries.splice(i, 1);
|
|
258
|
+
changed = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return changed;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Strict canonical match: only the shell-escaped canonical form.
|
|
266
|
+
* gh#client#18: raw unescaped paths are NOT canonical — a path with spaces
|
|
267
|
+
* or metacharacters would break at shell-fire time if not escaped. */
|
|
268
|
+
function isCanonicalCommand(entryCommand: string, canonical: string): boolean {
|
|
269
|
+
return entryCommand === canonical;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const BARE_BORG_REGEN = 'borg-regen';
|
|
273
|
+
const BARE_CLEAR_REWAKE = 'borg-clear-rewake';
|
|
274
|
+
const BARE_LOG_AUDIT = 'borg-log-audit';
|
|
275
|
+
|
|
276
|
+
function hasCommandHook(entries: any[], command: string): boolean {
|
|
277
|
+
return entries.some((entry: any) =>
|
|
278
|
+
Array.isArray(entry?.hooks) &&
|
|
279
|
+
entry.hooks.some((h: any) => {
|
|
280
|
+
if (h?.type !== 'command' || typeof h?.command !== 'string') return false;
|
|
281
|
+
// gh#client#18: for known owned commands, check if this hook's command
|
|
282
|
+
// maps to the SAME canonical as the target. This correctly distinguishes
|
|
283
|
+
// regen from clear-rewake from audit — a clear-rewake hook does NOT
|
|
284
|
+
// satisfy the dedup check for a regen target, and vice versa.
|
|
285
|
+
if (command === HOOK_COMMAND || command === CLEAR_REWAKE_HOOK_COMMAND || command === AUDIT_HOOK_COMMAND) {
|
|
286
|
+
return ownedCanonical(h.command) === command;
|
|
287
|
+
}
|
|
288
|
+
return h.command === command;
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Strict: only the shell-escaped canonical form (no bare-name fallback). */
|
|
294
|
+
function hasCanonicalCommandHook(entries: any[], command: string): boolean {
|
|
295
|
+
return entries.some((entry: any) =>
|
|
296
|
+
Array.isArray(entry?.hooks) &&
|
|
297
|
+
entry.hooks.some((h: any) => {
|
|
298
|
+
if (h?.type !== 'command' || typeof h?.command !== 'string') return false;
|
|
299
|
+
return isCanonicalCommand(h.command, command);
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function isClearRewakeCommand(hook: any): boolean {
|
|
305
|
+
if (hook?.type !== 'command' || typeof hook?.command !== 'string') return false;
|
|
306
|
+
return commandMatches(hook.command, BARE_CLEAR_REWAKE, resolveClearRewakePath());
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isCanonicalClearRewakeEntry(entry: any): boolean {
|
|
310
|
+
return entry?.matcher === 'clear' &&
|
|
311
|
+
Array.isArray(entry?.hooks) &&
|
|
312
|
+
entry.hooks.length === 1 &&
|
|
313
|
+
isCanonicalCommand(entry.hooks[0].command, CLEAR_REWAKE_HOOK_COMMAND) &&
|
|
314
|
+
entry.hooks[0].asyncRewake === true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Ensure one dedicated clear-only async-rewake handler. If a user has a
|
|
319
|
+
* malformed or duplicate entry for our command, remove only that owned
|
|
320
|
+
* command, preserve any unrelated sibling hooks, and append the canonical
|
|
321
|
+
* entry once.
|
|
322
|
+
*/
|
|
323
|
+
function normalizeClearRewakeHook(entries: any[]): any[] | null {
|
|
324
|
+
let migrated = false;
|
|
325
|
+
const result = entries.map((entry: any) => {
|
|
326
|
+
if (!Array.isArray(entry?.hooks)) return entry;
|
|
327
|
+
const hooks = entry.hooks.map((hook: any) => {
|
|
328
|
+
if (isClearRewakeCommand(hook) && !isCanonicalCommand(hook.command, CLEAR_REWAKE_HOOK_COMMAND)) {
|
|
329
|
+
migrated = true;
|
|
330
|
+
return { ...hook, command: CLEAR_REWAKE_HOOK_COMMAND };
|
|
331
|
+
}
|
|
332
|
+
return hook;
|
|
333
|
+
});
|
|
334
|
+
return hooks === entry.hooks ? entry : { ...entry, hooks };
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const commandCount = result.reduce(
|
|
338
|
+
(count, entry) => count + (
|
|
339
|
+
Array.isArray(entry?.hooks) ? entry.hooks.filter(isClearRewakeCommand).length : 0
|
|
340
|
+
),
|
|
341
|
+
0
|
|
342
|
+
);
|
|
343
|
+
if (commandCount === 1 && result.some(isCanonicalClearRewakeEntry)) {
|
|
344
|
+
return migrated ? result : null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const withoutOwnedCommand = result.flatMap((entry: any) => {
|
|
348
|
+
if (!Array.isArray(entry?.hooks)) return [entry];
|
|
349
|
+
const hooks = entry.hooks.filter((hook: any) => !isClearRewakeCommand(hook));
|
|
350
|
+
return hooks.length > 0 ? [{ ...entry, hooks }] : [];
|
|
351
|
+
});
|
|
352
|
+
withoutOwnedCommand.push({
|
|
353
|
+
matcher: 'clear',
|
|
354
|
+
hooks: [{ type: 'command', command: CLEAR_REWAKE_HOOK_COMMAND, asyncRewake: true }],
|
|
355
|
+
});
|
|
356
|
+
return withoutOwnedCommand;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function sessionStartHookRegisteredAt(settingsFile: string, includeClearRewake = false): boolean {
|
|
360
|
+
let settings: any;
|
|
361
|
+
try {
|
|
362
|
+
settings = readJsonFile(settingsFile);
|
|
363
|
+
} catch {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
const arr = settings?.hooks?.SessionStart;
|
|
367
|
+
if (!Array.isArray(arr)) return false;
|
|
368
|
+
return hasCanonicalCommandHook(arr, HOOK_COMMAND) &&
|
|
369
|
+
(!includeClearRewake || normalizeClearRewakeHook(arr) === null);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Peek whether the borg-regen SessionStart hook is already registered, without
|
|
374
|
+
* mutating settings. Returns false on any read error (safe-default).
|
|
375
|
+
*/
|
|
376
|
+
export function isSessionStartHookRegistered(): boolean {
|
|
377
|
+
let settings: any;
|
|
378
|
+
try {
|
|
379
|
+
settings = readSettings();
|
|
380
|
+
} catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
const arr = settings?.hooks?.SessionStart;
|
|
384
|
+
if (!Array.isArray(arr)) return false;
|
|
385
|
+
return arr.some((entry: any) =>
|
|
386
|
+
Array.isArray(entry?.hooks) &&
|
|
387
|
+
entry.hooks.some((h: any) => {
|
|
388
|
+
if (h?.type !== 'command' || typeof h?.command !== 'string') return false;
|
|
389
|
+
return isCanonicalCommand(h.command, HOOK_COMMAND);
|
|
390
|
+
})
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Peek: true iff the Claude UserPromptSubmit audit hook (`borg-log-audit`) is
|
|
396
|
+
* registered. Non-mutating mirror of addUserPromptSubmitHook's idempotency
|
|
397
|
+
* check; used by isClaudeHookConfigPending (gh#844).
|
|
398
|
+
*/
|
|
399
|
+
export function isUserPromptSubmitHookRegistered(): boolean {
|
|
400
|
+
let settings: any;
|
|
401
|
+
try {
|
|
402
|
+
settings = readSettings();
|
|
403
|
+
} catch {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
const arr = settings?.hooks?.UserPromptSubmit;
|
|
407
|
+
if (!Array.isArray(arr)) return false;
|
|
408
|
+
return arr.some((entry: any) =>
|
|
409
|
+
Array.isArray(entry?.hooks) &&
|
|
410
|
+
entry.hooks.some((h: any) => {
|
|
411
|
+
if (h?.type !== 'command' || typeof h?.command !== 'string') return false;
|
|
412
|
+
return isCanonicalCommand(h.command, AUDIT_HOOK_COMMAND);
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Inverse of addSessionStartHook: remove any SessionStart hook entry whose
|
|
419
|
+
* inner hooks array contains a `borg-regen` command. If multiple commands
|
|
420
|
+
* share an entry, only the borg-regen command is removed; otherwise the
|
|
421
|
+
* entire entry is dropped. Empty containers are cleaned up.
|
|
422
|
+
*
|
|
423
|
+
* Returns true if a change was made, false otherwise.
|
|
424
|
+
*/
|
|
425
|
+
export function removeSessionStartHook(): boolean {
|
|
426
|
+
let settings: any;
|
|
427
|
+
try {
|
|
428
|
+
settings = readSettings();
|
|
429
|
+
} catch {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
if (!settings?.hooks?.SessionStart) return false;
|
|
433
|
+
|
|
434
|
+
let changed = false;
|
|
435
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart
|
|
436
|
+
.map((entry: any) => {
|
|
437
|
+
if (!Array.isArray(entry?.hooks)) return entry;
|
|
438
|
+
const filtered = entry.hooks.filter((h: any) => {
|
|
439
|
+
if (h?.type !== 'command' || typeof h?.command !== 'string') return true;
|
|
440
|
+
return !commandMatches(h.command, BARE_BORG_REGEN, HOOK_COMMAND);
|
|
441
|
+
});
|
|
442
|
+
if (filtered.length !== entry.hooks.length) {
|
|
443
|
+
changed = true;
|
|
444
|
+
return { ...entry, hooks: filtered };
|
|
445
|
+
}
|
|
446
|
+
return entry;
|
|
447
|
+
})
|
|
448
|
+
.filter((entry: any) => Array.isArray(entry?.hooks) && entry.hooks.length > 0);
|
|
449
|
+
|
|
450
|
+
if (settings.hooks.SessionStart.length === 0) {
|
|
451
|
+
delete settings.hooks.SessionStart;
|
|
452
|
+
}
|
|
453
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
454
|
+
delete settings.hooks;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (changed) writeSettings(settings);
|
|
458
|
+
return changed;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Register a Claude Code UserPromptSubmit hook that runs `borg-log-audit`
|
|
463
|
+
* before each user prompt. The audit script nudges the drone if the
|
|
464
|
+
* previous assistant span used state-changing tools without calling
|
|
465
|
+
* `borg_log`. Idempotent: re-running won't add duplicates.
|
|
466
|
+
*
|
|
467
|
+
* Returns true if a change was made, false otherwise.
|
|
468
|
+
*/
|
|
469
|
+
export function addUserPromptSubmitHook(): boolean {
|
|
470
|
+
let settings: any;
|
|
471
|
+
try {
|
|
472
|
+
settings = readSettings();
|
|
473
|
+
} catch (err: any) {
|
|
474
|
+
console.error(`⚠ Could not parse ${settingsPath()}: ${err.message}. Skipping audit hook registration.`);
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
settings.hooks ??= {};
|
|
479
|
+
settings.hooks.UserPromptSubmit ??= [];
|
|
480
|
+
|
|
481
|
+
// gh#client#18: migrate owned hooks to canonical form and deduplicate
|
|
482
|
+
// (exactly one canonical hook per owned command, preserving unrelated
|
|
483
|
+
// siblings and entry metadata).
|
|
484
|
+
let changed = migrateAndDedupOwnedHooks(settings.hooks.UserPromptSubmit);
|
|
485
|
+
|
|
486
|
+
if (!hasCanonicalCommandHook(settings.hooks.UserPromptSubmit, AUDIT_HOOK_COMMAND)) {
|
|
487
|
+
settings.hooks.UserPromptSubmit.push({
|
|
488
|
+
matcher: '*',
|
|
489
|
+
hooks: [{ type: 'command', command: AUDIT_HOOK_COMMAND }],
|
|
490
|
+
});
|
|
491
|
+
changed = true;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (changed) writeSettings(settings);
|
|
495
|
+
return changed;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Inverse of addUserPromptSubmitHook: remove any UserPromptSubmit hook
|
|
500
|
+
* entry that runs `borg-log-audit`. Symmetric cleanup to
|
|
501
|
+
* removeSessionStartHook.
|
|
502
|
+
*/
|
|
503
|
+
export function removeUserPromptSubmitHook(): boolean {
|
|
504
|
+
let settings: any;
|
|
505
|
+
try {
|
|
506
|
+
settings = readSettings();
|
|
507
|
+
} catch {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
if (!settings?.hooks?.UserPromptSubmit) return false;
|
|
511
|
+
|
|
512
|
+
let changed = false;
|
|
513
|
+
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit
|
|
514
|
+
.map((entry: any) => {
|
|
515
|
+
if (!Array.isArray(entry?.hooks)) return entry;
|
|
516
|
+
const filtered = entry.hooks.filter((h: any) => {
|
|
517
|
+
if (h?.type !== 'command' || typeof h?.command !== 'string') return true;
|
|
518
|
+
return !commandMatches(h.command, BARE_LOG_AUDIT, AUDIT_HOOK_COMMAND);
|
|
519
|
+
});
|
|
520
|
+
if (filtered.length !== entry.hooks.length) {
|
|
521
|
+
changed = true;
|
|
522
|
+
return { ...entry, hooks: filtered };
|
|
523
|
+
}
|
|
524
|
+
return entry;
|
|
525
|
+
})
|
|
526
|
+
.filter((entry: any) => Array.isArray(entry?.hooks) && entry.hooks.length > 0);
|
|
527
|
+
|
|
528
|
+
if (settings.hooks.UserPromptSubmit.length === 0) {
|
|
529
|
+
delete settings.hooks.UserPromptSubmit;
|
|
530
|
+
}
|
|
531
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
532
|
+
delete settings.hooks;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (changed) writeSettings(settings);
|
|
536
|
+
return changed;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Detect whether the borg MCP server is already registered in the Claude
|
|
541
|
+
* Code CLI config (`~/.claude.json` `mcpServers.borg`).
|
|
542
|
+
*
|
|
543
|
+
* Per gh#79: when a user re-runs `borg setup` (the canonical re-run
|
|
544
|
+
* reason), the setup wizard's "Add borg to Claude Code?"
|
|
545
|
+
* prompt is redundant — the answer is deterministic ("already
|
|
546
|
+
* configured"). This detect lets the wizard silently skip Step 1 entirely
|
|
547
|
+
* when borg is present. Per the dispatch's Queen-implicit anti-scope,
|
|
548
|
+
* "silent means silent" — callers must not log an "already configured"
|
|
549
|
+
* notice when this returns true.
|
|
550
|
+
*
|
|
551
|
+
* Safe-default contract: any read error (file missing, malformed JSON,
|
|
552
|
+
* permission denied, empty file, unexpected shape) returns `false` so
|
|
553
|
+
* the caller still prompts. The dispatch's edge-case framing is "if
|
|
554
|
+
* indeterminate → prompt fires" — never silent-skip when state is
|
|
555
|
+
* ambiguous. The prompt is the safe path; silent-skip is the
|
|
556
|
+
* optimization layered on top of a verified-present signal.
|
|
557
|
+
*
|
|
558
|
+
* @param configPath Override the config-file path; primarily for tests.
|
|
559
|
+
*/
|
|
560
|
+
export function isMcpServerConfigured(
|
|
561
|
+
configPath: string = CLAUDE_CONFIG_PATH
|
|
562
|
+
): boolean {
|
|
563
|
+
try {
|
|
564
|
+
if (!fs.existsSync(configPath)) return false;
|
|
565
|
+
const text = fs.readFileSync(configPath, 'utf-8');
|
|
566
|
+
if (!text.trim()) return false;
|
|
567
|
+
const parsed = JSON.parse(text);
|
|
568
|
+
if (!parsed || typeof parsed !== 'object') return false;
|
|
569
|
+
const servers = (parsed as any).mcpServers;
|
|
570
|
+
if (!servers || typeof servers !== 'object' || Array.isArray(servers)) return false;
|
|
571
|
+
return MCP_SERVER_NAME in servers;
|
|
572
|
+
} catch {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function isCodexMcpServerConfigured(
|
|
578
|
+
configPath: string = CODEX_CONFIG_PATH
|
|
579
|
+
): boolean {
|
|
580
|
+
try {
|
|
581
|
+
if (!fs.existsSync(configPath)) return false;
|
|
582
|
+
const text = fs.readFileSync(configPath, 'utf-8');
|
|
583
|
+
const hasBorgServer = /^\s*\[mcp_servers\.borg\]\s*$/m.test(text);
|
|
584
|
+
const hasPinnedCodexIdentity = new RegExp(
|
|
585
|
+
`^\\s*${BORG_AGENT_KIND_ENV}\\s*=\\s*"codex"\\s*$`,
|
|
586
|
+
'm'
|
|
587
|
+
).test(text);
|
|
588
|
+
// gh#968 compatibility: older installations encoded the Codex CLI
|
|
589
|
+
// identity using the remote-wake transport marker. Keep recognizing that
|
|
590
|
+
// static shape so setup does not overwrite a working legacy config merely
|
|
591
|
+
// to migrate its marker.
|
|
592
|
+
const hasLegacyRemoteWakeIdentity = new RegExp(
|
|
593
|
+
`^\\s*${BORG_CODEX_REMOTE_WAKE_ENV}\\s*=\\s*"1"\\s*$`,
|
|
594
|
+
'm'
|
|
595
|
+
).test(text);
|
|
596
|
+
return hasBorgServer && (hasPinnedCodexIdentity || hasLegacyRemoteWakeIdentity);
|
|
597
|
+
} catch {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Get absolute path to borg index.js
|
|
604
|
+
* Returns the actual index.js file, not the npm symlink
|
|
605
|
+
*/
|
|
606
|
+
export function getBinaryPath(): string {
|
|
607
|
+
// In production: dist/index.js is in the same directory as this file
|
|
608
|
+
// In development: same
|
|
609
|
+
return path.join(__dirname, 'index.js');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Add borg MCP server to Claude Code using claude CLI
|
|
614
|
+
* First removes any existing borg configuration, then adds fresh one
|
|
615
|
+
* Runs: claude mcp remove --scope user borg && claude mcp add --scope user borg borg-mcp
|
|
616
|
+
*/
|
|
617
|
+
export function addMcpServer(): void {
|
|
618
|
+
try {
|
|
619
|
+
// First, remove any existing borg configuration (ignore errors if not found)
|
|
620
|
+
try {
|
|
621
|
+
execSync('claude mcp remove --scope user borg', { stdio: 'ignore' });
|
|
622
|
+
} catch {
|
|
623
|
+
// Ignore - server might not exist yet
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// gh#client#18: use absolute path to THIS installation's binary so the
|
|
627
|
+
// registered server always matches the running client version.
|
|
628
|
+
const command = `claude mcp add --scope user borg ${shellQuote(MCP_BINARY)}`;
|
|
629
|
+
|
|
630
|
+
execSync(command, {
|
|
631
|
+
stdio: 'inherit', // Show output to user
|
|
632
|
+
// No hosted-URL injection: BORG_API_URL passes through from the
|
|
633
|
+
// environment only when the operator has explicitly set it.
|
|
634
|
+
env: process.env,
|
|
635
|
+
});
|
|
636
|
+
} catch (error: any) {
|
|
637
|
+
if (error.message?.includes('command not found')) {
|
|
638
|
+
throw new Error('Claude CLI not found. Please install Claude Code first.');
|
|
639
|
+
}
|
|
640
|
+
throw new Error(`Failed to add MCP server: ${error.message}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export function addCodexMcpServer(): void {
|
|
645
|
+
try {
|
|
646
|
+
try {
|
|
647
|
+
execSync('codex mcp remove borg', { stdio: 'ignore' });
|
|
648
|
+
} catch {
|
|
649
|
+
// Ignore - server might not exist yet.
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// No hosted-URL fallback: only forward BORG_API_URL into the generated
|
|
653
|
+
// Codex MCP config when the operator has explicitly set it.
|
|
654
|
+
const apiUrl = process.env.BORG_API_URL;
|
|
655
|
+
// Identity is durable configuration; remote wake is a per-launch
|
|
656
|
+
// transport capability. Do not persist a transport marker here: a future
|
|
657
|
+
// Codex child may launch without a live --remote socket.
|
|
658
|
+
// gh#client#18: use absolute path to THIS installation's binary.
|
|
659
|
+
const codexConfigEnv = withAgentRuntimeEnv(process.env, 'codex');
|
|
660
|
+
const apiUrlEnvArg = apiUrl ? ` --env BORG_API_URL=${shellQuote(apiUrl)}` : '';
|
|
661
|
+
execSync('codex mcp add borg' +
|
|
662
|
+
apiUrlEnvArg +
|
|
663
|
+
` --env ${BORG_AGENT_KIND_ENV}=codex` +
|
|
664
|
+
` -- ${shellQuote(MCP_BINARY)}`, {
|
|
665
|
+
stdio: 'inherit',
|
|
666
|
+
env: codexConfigEnv,
|
|
667
|
+
});
|
|
668
|
+
} catch (error: any) {
|
|
669
|
+
if (error.message?.includes('command not found')) {
|
|
670
|
+
throw new Error('Codex CLI not found. Please install Codex first.');
|
|
671
|
+
}
|
|
672
|
+
throw new Error(`Failed to add MCP server to Codex: ${error.message}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function shellQuote(value: string): string {
|
|
677
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function addCodexHook(eventName: 'SessionStart' | 'UserPromptSubmit', command: string, options: { matcher?: string; timeout?: number } = {}): boolean {
|
|
681
|
+
let hooksFile: any;
|
|
682
|
+
try {
|
|
683
|
+
hooksFile = readJsonFile(CODEX_HOOKS_PATH);
|
|
684
|
+
} catch (err: any) {
|
|
685
|
+
console.error(`⚠ Could not parse ${CODEX_HOOKS_PATH}: ${err.message}. Skipping Codex hook registration.`);
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
hooksFile.hooks ??= {};
|
|
690
|
+
hooksFile.hooks[eventName] ??= [];
|
|
691
|
+
const entries = hooksFile.hooks[eventName];
|
|
692
|
+
if (!Array.isArray(entries)) return false;
|
|
693
|
+
|
|
694
|
+
// gh#client#18: migrate owned hooks to canonical form and deduplicate
|
|
695
|
+
// (exactly one canonical hook per owned command, preserving unrelated
|
|
696
|
+
// siblings and entry metadata).
|
|
697
|
+
let changed = migrateAndDedupOwnedHooks(entries);
|
|
698
|
+
|
|
699
|
+
if (!hasCanonicalCommandHook(entries, command)) {
|
|
700
|
+
const entry: any = {
|
|
701
|
+
hooks: [{ type: 'command', command }],
|
|
702
|
+
};
|
|
703
|
+
if (options.matcher) entry.matcher = options.matcher;
|
|
704
|
+
if (typeof options.timeout === 'number') entry.hooks[0].timeout = options.timeout;
|
|
705
|
+
entries.push(entry);
|
|
706
|
+
changed = true;
|
|
707
|
+
}
|
|
708
|
+
if (changed) writeJsonFile(CODEX_HOOKS_PATH, hooksFile);
|
|
709
|
+
return changed;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export function addCodexSessionStartHook(): boolean {
|
|
713
|
+
return addCodexHook('SessionStart', HOOK_COMMAND, { matcher: 'startup|resume', timeout: 30 });
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export function addCodexUserPromptSubmitHook(): boolean {
|
|
717
|
+
return addCodexHook('UserPromptSubmit', AUDIT_HOOK_COMMAND, { timeout: 10 });
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function isCodexHookRegistered(
|
|
721
|
+
eventName: 'SessionStart' | 'UserPromptSubmit' | 'Stop',
|
|
722
|
+
command: string,
|
|
723
|
+
hooksPath: string = CODEX_HOOKS_PATH
|
|
724
|
+
): boolean {
|
|
725
|
+
try {
|
|
726
|
+
const parsed = readJsonFile(hooksPath);
|
|
727
|
+
const arr = parsed?.hooks?.[eventName];
|
|
728
|
+
if (!Array.isArray(arr)) return false;
|
|
729
|
+
return arr.some((entry: any) =>
|
|
730
|
+
Array.isArray(entry?.hooks) &&
|
|
731
|
+
entry.hooks.some((h: any) => {
|
|
732
|
+
if (h?.type !== 'command' || typeof h?.command !== 'string') return false;
|
|
733
|
+
if (command === HOOK_COMMAND) return isCanonicalCommand(h.command, command);
|
|
734
|
+
if (command === AUDIT_HOOK_COMMAND) return isCanonicalCommand(h.command, command);
|
|
735
|
+
return h.command === command;
|
|
736
|
+
})
|
|
737
|
+
);
|
|
738
|
+
} catch {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Peek: true iff the Codex SessionStart orientation hook (`borg-regen`) is
|
|
745
|
+
* registered. Non-mutating mirror of addCodexSessionStartHook; used to gate
|
|
746
|
+
* that writer + the gh#844 disclosure on whether it would actually mutate.
|
|
747
|
+
*/
|
|
748
|
+
export function isCodexSessionStartHookRegistered(hooksPath: string = CODEX_HOOKS_PATH): boolean {
|
|
749
|
+
return isCodexHookRegistered('SessionStart', HOOK_COMMAND, hooksPath);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Peek: true iff the Codex UserPromptSubmit audit hook (`borg-log-audit`) is
|
|
754
|
+
* registered. Non-mutating mirror of addCodexUserPromptSubmitHook.
|
|
755
|
+
*/
|
|
756
|
+
export function isCodexUserPromptSubmitHookRegistered(hooksPath: string = CODEX_HOOKS_PATH): boolean {
|
|
757
|
+
return isCodexHookRegistered('UserPromptSubmit', AUDIT_HOOK_COMMAND, hooksPath);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ─── OpenCode MCP integration ────────────────────────────────────────────
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Detect whether the borg MCP server is already registered in the opencode
|
|
764
|
+
* config (`~/.config/opencode/opencode.json` `mcp.borg`).
|
|
765
|
+
*
|
|
766
|
+
* Reads the config as JSON and checks for a `mcp.borg` entry with
|
|
767
|
+
* `type: "local"`. Safe-default: any read error returns `false`.
|
|
768
|
+
*/
|
|
769
|
+
export function isOpenCodeMcpServerConfigured(
|
|
770
|
+
configPath: string = OPENCODE_CONFIG_PATH
|
|
771
|
+
): boolean {
|
|
772
|
+
try {
|
|
773
|
+
if (!fs.existsSync(configPath)) return false;
|
|
774
|
+
const text = fs.readFileSync(configPath, 'utf-8');
|
|
775
|
+
if (!text.trim()) return false;
|
|
776
|
+
const parsed = JSON.parse(text);
|
|
777
|
+
if (!parsed || typeof parsed !== 'object') return false;
|
|
778
|
+
const borgServer = (parsed as any).mcp?.borg;
|
|
779
|
+
if (!borgServer || typeof borgServer !== 'object') return false;
|
|
780
|
+
return borgServer.type === 'local';
|
|
781
|
+
} catch {
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Add borg MCP server to OpenCode using `opencode mcp add` CLI.
|
|
788
|
+
* Pins BORG_SESSION=1, BORG_AGENT_KIND=opencode, the legacy BORG_OPENCODE=1,
|
|
789
|
+
* and BORG_API_URL in the server environment so the MCP child inherits the
|
|
790
|
+
* activation gate + explicit agent-kind signal (same approach as Codex's
|
|
791
|
+
* pinned env — OpenCode MCP children only see pinned env, not parent process
|
|
792
|
+
* env). Existing configs with BORG_OPENCODE remain supported by the runtime
|
|
793
|
+
* fallback.
|
|
794
|
+
*/
|
|
795
|
+
export function addOpenCodeMcpServer(): void {
|
|
796
|
+
try {
|
|
797
|
+
// No hosted-URL fallback: only forward BORG_API_URL when explicitly set.
|
|
798
|
+
const apiUrl = process.env.BORG_API_URL;
|
|
799
|
+
const apiUrlEnvArg = apiUrl ? ` --env BORG_API_URL=${shellQuote(apiUrl)}` : '';
|
|
800
|
+
// gh#client#18: use absolute path to THIS installation's binary.
|
|
801
|
+
execSync(
|
|
802
|
+
`opencode mcp add borg --env BORG_SESSION=1 --env BORG_AGENT_KIND=opencode --env BORG_OPENCODE=1${apiUrlEnvArg} -- ${shellQuote(MCP_BINARY)}`,
|
|
803
|
+
{ stdio: 'inherit' }
|
|
804
|
+
);
|
|
805
|
+
} catch (error: any) {
|
|
806
|
+
if (error.message?.includes('command not found')) {
|
|
807
|
+
throw new Error('opencode CLI not found. Please install opencode first.');
|
|
808
|
+
}
|
|
809
|
+
throw new Error(`Failed to add MCP server to opencode: ${error.message}`);
|
|
810
|
+
}
|
|
811
|
+
}
|