borgmcp 1.1.14 → 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 +86 -37
- 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,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temporary compatibility helpers for the legacy Borg-managed Claude model
|
|
3
|
+
* selector. Model/provider configuration belongs to the launched agent CLI;
|
|
4
|
+
* the remaining selector is retired with the assimilation model wire cleanup.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const MODEL_DESCRIPTOR_REGEX = /^claude:[A-Za-z0-9._:\/-]+$/;
|
|
8
|
+
|
|
9
|
+
export function parseModel(descriptor: string): { kind: 'claude'; model: string } {
|
|
10
|
+
if (!MODEL_DESCRIPTOR_REGEX.test(descriptor)) {
|
|
11
|
+
throw new Error(`invalid model descriptor: ${descriptor} (expected claude:<model>)`);
|
|
12
|
+
}
|
|
13
|
+
return { kind: 'claude', model: descriptor.slice('claude:'.length) };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveLaunchEnv(
|
|
17
|
+
descriptor: string | null
|
|
18
|
+
): { set: Record<string, string>; unset: string[] } {
|
|
19
|
+
if (!descriptor) return { set: {}, unset: [] };
|
|
20
|
+
const { model } = parseModel(descriptor);
|
|
21
|
+
return { set: { ANTHROPIC_MODEL: model }, unset: [] };
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates an identifier used in worktree paths and other safety-critical
|
|
3
|
+
* positions (must survive shell-arg parsing without escaping).
|
|
4
|
+
*
|
|
5
|
+
* Allowed: lowercase ASCII letters, digits, hyphens, underscores.
|
|
6
|
+
* Length 1–48. Anchored. No leading hyphen (would parse as a flag).
|
|
7
|
+
*/
|
|
8
|
+
const NAME_RE = /^[a-z0-9_][a-z0-9_-]{0,47}$/;
|
|
9
|
+
|
|
10
|
+
export function validateName(name: string): { ok: true } | { ok: false; error: string } {
|
|
11
|
+
if (name.length === 0) {
|
|
12
|
+
return { ok: false, error: `name must not be empty` };
|
|
13
|
+
}
|
|
14
|
+
if (!NAME_RE.test(name)) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
error:
|
|
18
|
+
`invalid name "${name}". Use lowercase letters, digits, hyphens, or ` +
|
|
19
|
+
`underscores; max 48 chars; must not start with a hyphen.`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return { ok: true };
|
|
23
|
+
}
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
|
|
2
|
+
import { createHash, randomUUID } from 'crypto';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
|
|
6
|
+
const LOG_FILE = join(tmpdir(), 'borg-opencode-drone.log');
|
|
7
|
+
function log(msg: string) {
|
|
8
|
+
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
9
|
+
try { appendFileSync(LOG_FILE, line); } catch {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface OpenCodeDroneState {
|
|
13
|
+
serverUrl: string;
|
|
14
|
+
sessionId: string | null;
|
|
15
|
+
sessionCreatedAt: number | null;
|
|
16
|
+
knownRootSessionIds: string[];
|
|
17
|
+
directory: string;
|
|
18
|
+
droneLabel: string;
|
|
19
|
+
cubeName: string;
|
|
20
|
+
connected: boolean;
|
|
21
|
+
totalEntriesInjected: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let state: OpenCodeDroneState | null = null;
|
|
25
|
+
|
|
26
|
+
interface ConnectDeps {
|
|
27
|
+
serverUrl: string;
|
|
28
|
+
directory: string;
|
|
29
|
+
droneLabel: string;
|
|
30
|
+
cubeName: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface OCSession {
|
|
34
|
+
id: string;
|
|
35
|
+
directory: string;
|
|
36
|
+
time: { created: number };
|
|
37
|
+
parentID?: string;
|
|
38
|
+
agent?: string;
|
|
39
|
+
model?: { providerID: string; modelID: string };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface OCMessage {
|
|
43
|
+
info?: {
|
|
44
|
+
role?: string;
|
|
45
|
+
time?: { created?: number };
|
|
46
|
+
};
|
|
47
|
+
parts?: Array<{
|
|
48
|
+
type?: string;
|
|
49
|
+
text?: string;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface SessionBinding {
|
|
54
|
+
version: 2;
|
|
55
|
+
sessionId: string;
|
|
56
|
+
sessionCreatedAt: number;
|
|
57
|
+
knownRootSessionIds: string[];
|
|
58
|
+
serverUrl: string;
|
|
59
|
+
directory: string;
|
|
60
|
+
droneLabel: string;
|
|
61
|
+
cubeName: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface OpenCodeLaunchKickoff {
|
|
65
|
+
prompt: string;
|
|
66
|
+
nonce: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// This is correlation metadata, intentionally not an instruction to the
|
|
70
|
+
// launched agent. A markdown comment keeps it benign in the user-visible
|
|
71
|
+
// kickoff while preserving it in OpenCode's stored message text.
|
|
72
|
+
const OPEN_CODE_LAUNCH_NONCE_MARKER = 'borg-opencode-correlation:';
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Add a launch-unique identity to the OpenCode-only copy of the shared
|
|
76
|
+
* kickoff. The prompt is what OpenCode records as its first user message, so
|
|
77
|
+
* the launcher can later bind the MCP child to this precise launch instead of
|
|
78
|
+
* guessing from a repeated kickoff's text or timestamp.
|
|
79
|
+
*/
|
|
80
|
+
export function createOpenCodeLaunchKickoff(
|
|
81
|
+
kickoff: string,
|
|
82
|
+
nonce: string = randomUUID(),
|
|
83
|
+
): OpenCodeLaunchKickoff {
|
|
84
|
+
return {
|
|
85
|
+
prompt: `${kickoff}\n\n<!-- ${OPEN_CODE_LAUNCH_NONCE_MARKER}${nonce} -->`,
|
|
86
|
+
nonce,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const bindingPathsForTests = new Set<string>();
|
|
91
|
+
|
|
92
|
+
export async function connectOpenCodeDrone(deps: ConnectDeps): Promise<void> {
|
|
93
|
+
state = {
|
|
94
|
+
serverUrl: deps.serverUrl,
|
|
95
|
+
sessionId: null,
|
|
96
|
+
sessionCreatedAt: null,
|
|
97
|
+
knownRootSessionIds: [],
|
|
98
|
+
directory: deps.directory,
|
|
99
|
+
droneLabel: deps.droneLabel,
|
|
100
|
+
cubeName: deps.cubeName,
|
|
101
|
+
connected: true,
|
|
102
|
+
totalEntriesInjected: 0,
|
|
103
|
+
};
|
|
104
|
+
log(`connected url=${deps.serverUrl} dir=${deps.directory}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Raw fetch wrappers
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
function apiUrl(path: string): string {
|
|
112
|
+
const base = state!.serverUrl.replace(/\/+$/, '');
|
|
113
|
+
return `${base}${path}${path.includes('?') ? '&' : '?'}directory=${encodeURIComponent(state!.directory)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const FETCH_TIMEOUT = 10_000;
|
|
117
|
+
|
|
118
|
+
async function rawGet(path: string): Promise<{ status: number; body: string }> {
|
|
119
|
+
const url = apiUrl(path);
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
|
122
|
+
try {
|
|
123
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
124
|
+
const body = await res.text();
|
|
125
|
+
return { status: res.status, body };
|
|
126
|
+
} finally {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function rawPost(path: string, bodyObj: unknown): Promise<{ status: number; body: string }> {
|
|
132
|
+
const url = apiUrl(path);
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
|
135
|
+
try {
|
|
136
|
+
const res = await fetch(url, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'Content-Type': 'application/json' },
|
|
139
|
+
signal: controller.signal,
|
|
140
|
+
body: JSON.stringify(bodyObj),
|
|
141
|
+
});
|
|
142
|
+
const body = await res.text();
|
|
143
|
+
return { status: res.status, body };
|
|
144
|
+
} finally {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function listSessions(): Promise<OCSession[]> {
|
|
150
|
+
const { status, body } = await rawGet('/session');
|
|
151
|
+
if (status !== 200) throw new Error(`OpenCode sessions request failed (${status})`);
|
|
152
|
+
return JSON.parse(body);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function getSession(id: string): Promise<OCSession | null> {
|
|
156
|
+
try {
|
|
157
|
+
const { status, body } = await rawGet(`/session/${id}`);
|
|
158
|
+
if (status !== 200) return null;
|
|
159
|
+
return JSON.parse(body);
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function listSessionMessages(id: string): Promise<OCMessage[]> {
|
|
166
|
+
const { status, body } = await rawGet(`/session/${id}/message`);
|
|
167
|
+
if (status !== 200) throw new Error(`OpenCode session messages request failed (${status})`);
|
|
168
|
+
return JSON.parse(body);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function promptSession(id: string, bodyObj: Record<string, unknown>): Promise<boolean> {
|
|
172
|
+
const { status } = await rawPost(`/session/${id}/prompt_async`, bodyObj);
|
|
173
|
+
return status === 200 || status === 204;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Persist the launch-selected session for the separately spawned MCP child.
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
function bindingPath(): string {
|
|
181
|
+
const current = state!;
|
|
182
|
+
const key = [current.serverUrl, current.directory, current.cubeName, current.droneLabel].join('\0');
|
|
183
|
+
const digest = createHash('sha256').update(key).digest('hex').slice(0, 24);
|
|
184
|
+
const path = join(tmpdir(), `borg-opencode-session-${digest}.json`);
|
|
185
|
+
bindingPathsForTests.add(path);
|
|
186
|
+
return path;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function bindingMatchesState(binding: SessionBinding): boolean {
|
|
190
|
+
const current = state!;
|
|
191
|
+
return binding.version === 2
|
|
192
|
+
&& binding.serverUrl === current.serverUrl
|
|
193
|
+
&& binding.directory === current.directory
|
|
194
|
+
&& binding.droneLabel === current.droneLabel
|
|
195
|
+
&& binding.cubeName === current.cubeName
|
|
196
|
+
&& typeof binding.sessionId === 'string'
|
|
197
|
+
&& typeof binding.sessionCreatedAt === 'number'
|
|
198
|
+
&& Array.isArray(binding.knownRootSessionIds)
|
|
199
|
+
&& binding.knownRootSessionIds.every((id) => typeof id === 'string');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function readBinding(): SessionBinding | null {
|
|
203
|
+
try {
|
|
204
|
+
const path = bindingPath();
|
|
205
|
+
if (!existsSync(path)) return null;
|
|
206
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8')) as SessionBinding;
|
|
207
|
+
return bindingMatchesState(parsed) ? parsed : null;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function clearBinding(): void {
|
|
214
|
+
if (!state) return;
|
|
215
|
+
const path = bindingPath();
|
|
216
|
+
state.sessionId = null;
|
|
217
|
+
state.sessionCreatedAt = null;
|
|
218
|
+
state.knownRootSessionIds = [];
|
|
219
|
+
try {
|
|
220
|
+
unlinkSync(path);
|
|
221
|
+
} catch {
|
|
222
|
+
// The file may have already been removed by the launch process.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function saveBinding(session: OCSession, knownRootSessionIds: string[]): void {
|
|
227
|
+
const current = state!;
|
|
228
|
+
const binding: SessionBinding = {
|
|
229
|
+
version: 2,
|
|
230
|
+
sessionId: session.id,
|
|
231
|
+
sessionCreatedAt: session.time.created,
|
|
232
|
+
knownRootSessionIds,
|
|
233
|
+
serverUrl: current.serverUrl,
|
|
234
|
+
directory: current.directory,
|
|
235
|
+
droneLabel: current.droneLabel,
|
|
236
|
+
cubeName: current.cubeName,
|
|
237
|
+
};
|
|
238
|
+
current.sessionId = binding.sessionId;
|
|
239
|
+
current.sessionCreatedAt = binding.sessionCreatedAt;
|
|
240
|
+
current.knownRootSessionIds = binding.knownRootSessionIds;
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const path = bindingPath();
|
|
244
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
245
|
+
writeFileSync(temporary, JSON.stringify(binding), { mode: 0o600 });
|
|
246
|
+
renameSync(temporary, path);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
log(`session binding write failed: ${err}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function restoreBinding(): SessionBinding | null {
|
|
253
|
+
if (!state) return null;
|
|
254
|
+
if (state.sessionId && state.sessionCreatedAt !== null) {
|
|
255
|
+
return {
|
|
256
|
+
version: 2,
|
|
257
|
+
sessionId: state.sessionId,
|
|
258
|
+
sessionCreatedAt: state.sessionCreatedAt,
|
|
259
|
+
knownRootSessionIds: state.knownRootSessionIds,
|
|
260
|
+
serverUrl: state.serverUrl,
|
|
261
|
+
directory: state.directory,
|
|
262
|
+
droneLabel: state.droneLabel,
|
|
263
|
+
cubeName: state.cubeName,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const binding = readBinding();
|
|
268
|
+
if (!binding) return null;
|
|
269
|
+
state.sessionId = binding.sessionId;
|
|
270
|
+
state.sessionCreatedAt = binding.sessionCreatedAt;
|
|
271
|
+
state.knownRootSessionIds = binding.knownRootSessionIds;
|
|
272
|
+
return binding;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isBoundSession(session: OCSession, binding: SessionBinding): boolean {
|
|
276
|
+
return session.id === binding.sessionId && session.directory === state!.directory;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isTopLevelSession(session: OCSession): boolean {
|
|
280
|
+
return !session.parentID;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function findUnseenTopLevelSession(knownRootSessionIds: string[]): Promise<{
|
|
284
|
+
session: OCSession;
|
|
285
|
+
knownRootSessionIds: string[];
|
|
286
|
+
} | null> {
|
|
287
|
+
try {
|
|
288
|
+
const sessions = await listSessions();
|
|
289
|
+
const roots = sessions.filter(
|
|
290
|
+
(session) => session.directory === state!.directory
|
|
291
|
+
&& isTopLevelSession(session),
|
|
292
|
+
);
|
|
293
|
+
const matched = roots.filter((session) => !knownRootSessionIds.includes(session.id));
|
|
294
|
+
if (matched.length === 0) return null;
|
|
295
|
+
const best = matched.reduce((a, b) =>
|
|
296
|
+
a.time.created > b.time.created ? a : b,
|
|
297
|
+
);
|
|
298
|
+
return { session: best, knownRootSessionIds: roots.map((session) => session.id) };
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function kickoffMessageTime(messages: OCMessage[], nonce: string): number | null {
|
|
305
|
+
let latest: number | null = null;
|
|
306
|
+
for (const message of messages) {
|
|
307
|
+
if (message.info?.role && message.info.role !== 'user') continue;
|
|
308
|
+
const matchesLaunchNonce = message.parts?.some(
|
|
309
|
+
(part) => part.type === 'text' && part.text?.includes(`${OPEN_CODE_LAUNCH_NONCE_MARKER}${nonce}`),
|
|
310
|
+
);
|
|
311
|
+
if (!matchesLaunchNonce) continue;
|
|
312
|
+
const created = message.info?.time?.created ?? 0;
|
|
313
|
+
latest = latest === null ? created : Math.max(latest, created);
|
|
314
|
+
}
|
|
315
|
+
return latest;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The launch process is the only place allowed to discover a session from the
|
|
320
|
+
* server. It chooses the session that contains this launch's unique nonce,
|
|
321
|
+
* rather than choosing by repeated kickoff text or session creation time. A
|
|
322
|
+
* fork is therefore allowed only when it was explicitly selected for this
|
|
323
|
+
* launch and received the nonce-bearing kickoff.
|
|
324
|
+
*/
|
|
325
|
+
async function findLaunchSession(nonce: string): Promise<{
|
|
326
|
+
session: OCSession;
|
|
327
|
+
knownRootSessionIds: string[];
|
|
328
|
+
} | null> {
|
|
329
|
+
try {
|
|
330
|
+
const sessions = (await listSessions()).filter(
|
|
331
|
+
(session) => session.directory === state!.directory,
|
|
332
|
+
);
|
|
333
|
+
const knownRootSessionIds = sessions
|
|
334
|
+
.filter(isTopLevelSession)
|
|
335
|
+
.map((session) => session.id);
|
|
336
|
+
const candidates = await Promise.all(sessions.map(async (session) => {
|
|
337
|
+
try {
|
|
338
|
+
const messageTime = kickoffMessageTime(
|
|
339
|
+
await listSessionMessages(session.id),
|
|
340
|
+
nonce,
|
|
341
|
+
);
|
|
342
|
+
return messageTime === null ? null : { session, messageTime };
|
|
343
|
+
} catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}));
|
|
347
|
+
const matched = candidates.filter(
|
|
348
|
+
(candidate): candidate is { session: OCSession; messageTime: number } => candidate !== null,
|
|
349
|
+
);
|
|
350
|
+
if (matched.length === 0) return null;
|
|
351
|
+
const session = matched.reduce((best, candidate) =>
|
|
352
|
+
candidate.messageTime > best.messageTime ? candidate : best,
|
|
353
|
+
).session;
|
|
354
|
+
return { session, knownRootSessionIds };
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function resolveInjectionSession(): Promise<OCSession | null> {
|
|
361
|
+
const binding = restoreBinding();
|
|
362
|
+
if (!binding) return null;
|
|
363
|
+
|
|
364
|
+
const bound = await getSession(binding.sessionId);
|
|
365
|
+
if (!bound || !isBoundSession(bound, binding)) {
|
|
366
|
+
clearBinding();
|
|
367
|
+
const replacement = await findUnseenTopLevelSession(binding.knownRootSessionIds);
|
|
368
|
+
if (!replacement) return null;
|
|
369
|
+
saveBinding(replacement.session, replacement.knownRootSessionIds);
|
|
370
|
+
return replacement.session;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// `/new` creates an unseen top-level session. Keep the launch-time root
|
|
374
|
+
// snapshot so an old, unrelated root is never mistaken for a user switch.
|
|
375
|
+
// Children never supersede the bound root.
|
|
376
|
+
const switched = await findUnseenTopLevelSession(binding.knownRootSessionIds);
|
|
377
|
+
if (switched) {
|
|
378
|
+
saveBinding(switched.session, switched.knownRootSessionIds);
|
|
379
|
+
return switched.session;
|
|
380
|
+
}
|
|
381
|
+
return bound;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function rebindAfterFailure(knownRootSessionIds: string[]): Promise<OCSession | null> {
|
|
385
|
+
const replacement = await findUnseenTopLevelSession(knownRootSessionIds);
|
|
386
|
+
if (!replacement) return null;
|
|
387
|
+
saveBinding(replacement.session, replacement.knownRootSessionIds);
|
|
388
|
+
return replacement.session;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// Public API
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Wait for the OpenCode HTTP server, then capture the session that received
|
|
397
|
+
* this launch's nonce-bearing `--prompt` kickoff. The binding survives the separate
|
|
398
|
+
* MCP-child process, which must never fall back to a newest-session heuristic.
|
|
399
|
+
*/
|
|
400
|
+
export async function injectInitialKickoff(launch: OpenCodeLaunchKickoff): Promise<boolean> {
|
|
401
|
+
if (!state?.connected) { log('kickoff: not connected'); return false; }
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
// Wait for the server.
|
|
405
|
+
for (let i = 0; i < 30; i++) {
|
|
406
|
+
try {
|
|
407
|
+
await listSessions();
|
|
408
|
+
log(`kickoff: server ready (attempt ${i + 1})`);
|
|
409
|
+
break;
|
|
410
|
+
} catch {
|
|
411
|
+
// not ready yet
|
|
412
|
+
}
|
|
413
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Capture the launch-selected session, including explicit resume/fork
|
|
417
|
+
// targets. Unrelated sessions do not contain this launch's nonce.
|
|
418
|
+
for (let i = 0; i < 30; i++) {
|
|
419
|
+
const binding = await findLaunchSession(launch.nonce);
|
|
420
|
+
if (binding) {
|
|
421
|
+
saveBinding(binding.session, binding.knownRootSessionIds);
|
|
422
|
+
log(`kickoff: bound session ${binding.session.id.slice(0, 8)}…`);
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
log('kickoff: no session found');
|
|
429
|
+
return false;
|
|
430
|
+
} catch (err) {
|
|
431
|
+
log(`kickoff error: ${err}`);
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Inject a silent context entry (noReply) into our session.
|
|
438
|
+
* Falls through silently — caller falls back to inbox write.
|
|
439
|
+
*/
|
|
440
|
+
export async function injectOpenCodeEntry(text: string): Promise<boolean> {
|
|
441
|
+
if (!state?.connected) return false;
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const target = await resolveInjectionSession();
|
|
445
|
+
if (!target) return false;
|
|
446
|
+
|
|
447
|
+
const body = {
|
|
448
|
+
parts: [{ type: 'text', text }],
|
|
449
|
+
};
|
|
450
|
+
if (await promptSession(target.id, body)) {
|
|
451
|
+
state.totalEntriesInjected++;
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// A failed prompt means the cached target is no longer trustworthy. Clear
|
|
456
|
+
// it before considering only a newer root created by `/new`; never fall
|
|
457
|
+
// through to a child or arbitrary newest session.
|
|
458
|
+
const knownRootSessionIds = [...state.knownRootSessionIds];
|
|
459
|
+
clearBinding();
|
|
460
|
+
const replacement = await rebindAfterFailure(knownRootSessionIds);
|
|
461
|
+
if (!replacement) return false;
|
|
462
|
+
if (await promptSession(replacement.id, body)) {
|
|
463
|
+
state.totalEntriesInjected++;
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
clearBinding();
|
|
467
|
+
return false;
|
|
468
|
+
} catch (err) {
|
|
469
|
+
log(`entry error: ${err}`);
|
|
470
|
+
clearBinding();
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export async function probeOpenCodeDroneArmed(): Promise<boolean | null> {
|
|
476
|
+
if (!state?.connected) return null;
|
|
477
|
+
const binding = restoreBinding();
|
|
478
|
+
if (!binding) return false;
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
const session = await getSession(binding.sessionId);
|
|
482
|
+
if (session && isBoundSession(session, binding)) return true;
|
|
483
|
+
clearBinding();
|
|
484
|
+
return false;
|
|
485
|
+
} catch {
|
|
486
|
+
clearBinding();
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function disconnectOpenCodeDrone(): void {
|
|
492
|
+
state = null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function getOpenCodeConnectionState(): {
|
|
496
|
+
connected: boolean;
|
|
497
|
+
sessionId: string | null;
|
|
498
|
+
totalEntriesInjected: number;
|
|
499
|
+
} {
|
|
500
|
+
return {
|
|
501
|
+
connected: state?.connected ?? false,
|
|
502
|
+
sessionId: state?.sessionId ?? null,
|
|
503
|
+
totalEntriesInjected: state?.totalEntriesInjected ?? 0,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function computeOpenCodePort(droneId: string, base: number = 14096): number {
|
|
508
|
+
let hash = 0;
|
|
509
|
+
for (let i = 0; i < droneId.length; i++) {
|
|
510
|
+
const char = droneId.charCodeAt(i);
|
|
511
|
+
hash = ((hash << 5) - hash) + char;
|
|
512
|
+
hash |= 0;
|
|
513
|
+
}
|
|
514
|
+
return base + (Math.abs(hash) % 1024);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Test-only cleanup for module state and the local cross-process binding. */
|
|
518
|
+
export function __resetOpenCodeDroneForTests(): void {
|
|
519
|
+
state = null;
|
|
520
|
+
for (const path of bindingPathsForTests) {
|
|
521
|
+
try {
|
|
522
|
+
unlinkSync(path);
|
|
523
|
+
} catch {
|
|
524
|
+
// Already removed.
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
bindingPathsForTests.clear();
|
|
528
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Plugin source that preserves borg cube context across session compaction.
|
|
7
|
+
* The initial kickoff and ongoing entry injection are handled by the launcher
|
|
8
|
+
* and MCP client via the SDK, not by the plugin.
|
|
9
|
+
*/
|
|
10
|
+
export const BORG_PLUGIN_SOURCE = `
|
|
11
|
+
// Borg MCP context-preservation plugin — installed by borg assimilate
|
|
12
|
+
export default function () {
|
|
13
|
+
return {
|
|
14
|
+
'experimental.session.compacting': async (_input, output) => {
|
|
15
|
+
output.context.push(
|
|
16
|
+
'## Borg Cube\\nYou are in a Borg MCP multi-agent coordination cube. ' +
|
|
17
|
+
'Use MCP tool borg_regen to get full context and recent activity.'
|
|
18
|
+
);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const PLUGIN_REL_PATH = path.join('.config', 'opencode', 'plugins', 'borg-orient.js');
|
|
25
|
+
|
|
26
|
+
export function installBorgPlugin(): void {
|
|
27
|
+
const pluginPath = path.join(os.homedir(), PLUGIN_REL_PATH);
|
|
28
|
+
try {
|
|
29
|
+
if (fs.existsSync(pluginPath)) {
|
|
30
|
+
const existing = fs.readFileSync(pluginPath, 'utf-8');
|
|
31
|
+
if (existing === BORG_PLUGIN_SOURCE) return;
|
|
32
|
+
}
|
|
33
|
+
fs.mkdirSync(path.dirname(pluginPath), { recursive: true });
|
|
34
|
+
fs.writeFileSync(pluginPath, BORG_PLUGIN_SOURCE, 'utf-8');
|
|
35
|
+
} catch {
|
|
36
|
+
// Best-effort — plugin is an optimization, not a requirement.
|
|
37
|
+
}
|
|
38
|
+
}
|