borgmcp 2.14.2 → 2.16.0
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/dist/agent-runtime.d.ts +5 -0
- package/dist/agent-runtime.d.ts.map +1 -1
- package/dist/agent-runtime.js +14 -0
- package/dist/agent-runtime.js.map +1 -1
- package/dist/assimilate-cmd.d.ts +5 -0
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +53 -14
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +19 -4
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/bare-launch-menu.d.ts +8 -0
- package/dist/bare-launch-menu.d.ts.map +1 -1
- package/dist/bare-launch-menu.js +12 -0
- package/dist/bare-launch-menu.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +28 -40
- package/dist/claude.js.map +1 -1
- package/dist/cleanup-cmd.d.ts.map +1 -1
- package/dist/cleanup-cmd.js +2 -2
- package/dist/cleanup-cmd.js.map +1 -1
- package/dist/cli-tool-approval.d.ts.map +1 -1
- package/dist/cli-tool-approval.js +3 -2
- package/dist/cli-tool-approval.js.map +1 -1
- package/dist/codex-remote.d.ts.map +1 -1
- package/dist/codex-remote.js +4 -3
- package/dist/codex-remote.js.map +1 -1
- package/dist/config-utils.d.ts +16 -1
- package/dist/config-utils.d.ts.map +1 -1
- package/dist/config-utils.js +176 -17
- package/dist/config-utils.js.map +1 -1
- package/dist/credential-paths.d.ts.map +1 -1
- package/dist/credential-paths.js +2 -3
- package/dist/credential-paths.js.map +1 -1
- package/dist/cubes.d.ts +6 -1
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +9 -4
- package/dist/cubes.js.map +1 -1
- package/dist/foreign-path-reminder.d.ts +12 -0
- package/dist/foreign-path-reminder.d.ts.map +1 -0
- package/dist/foreign-path-reminder.js +122 -0
- package/dist/foreign-path-reminder.js.map +1 -0
- package/dist/launch-access.d.ts +22 -0
- package/dist/launch-access.d.ts.map +1 -0
- package/dist/launch-access.js +27 -0
- package/dist/launch-access.js.map +1 -0
- package/dist/launch-all-deps.d.ts.map +1 -1
- package/dist/launch-all-deps.js +2 -2
- package/dist/launch-all-deps.js.map +1 -1
- package/dist/lifecycle-log-guard.js +2 -2
- package/dist/lifecycle-log-guard.js.map +1 -1
- package/dist/local-server-cursor.js +2 -2
- package/dist/local-server-cursor.js.map +1 -1
- package/dist/opencode-plugin.js +2 -2
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/parse-assimilate-args.d.ts.map +1 -1
- package/dist/parse-assimilate-args.js +6 -1
- package/dist/parse-assimilate-args.js.map +1 -1
- package/dist/private-root.d.ts +17 -0
- package/dist/private-root.d.ts.map +1 -1
- package/dist/private-root.js +62 -2
- package/dist/private-root.js.map +1 -1
- package/dist/regen-format.js +1 -1
- package/dist/regen-format.js.map +1 -1
- package/dist/resolved-cli-config.d.ts +16 -0
- package/dist/resolved-cli-config.d.ts.map +1 -0
- package/dist/resolved-cli-config.js +26 -0
- package/dist/resolved-cli-config.js.map +1 -0
- package/dist/seat-store.d.ts.map +1 -1
- package/dist/seat-store.js +4 -3
- package/dist/seat-store.js.map +1 -1
- package/dist/seats.d.ts.map +1 -1
- package/dist/seats.js +2 -2
- package/dist/seats.js.map +1 -1
- package/dist/self-path.d.ts +2 -0
- package/dist/self-path.d.ts.map +1 -1
- package/dist/self-path.js +4 -0
- package/dist/self-path.js.map +1 -1
- package/dist/server-trust.d.ts.map +1 -1
- package/dist/server-trust.js +3 -3
- package/dist/server-trust.js.map +1 -1
- package/dist/setup-confirm.d.ts +1 -1
- package/dist/setup-confirm.js +1 -1
- package/dist/setup-selection.d.ts +38 -0
- package/dist/setup-selection.d.ts.map +1 -0
- package/dist/setup-selection.js +51 -0
- package/dist/setup-selection.js.map +1 -0
- package/dist/setup.js +80 -21
- package/dist/setup.js.map +1 -1
- package/dist/stream-owner.js +2 -2
- package/dist/stream-owner.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +3 -3
- package/docs/LOCAL_SERVER.md +23 -0
- package/docs/RELEASING.md +130 -282
- package/package.json +9 -9
- package/src/agent-runtime.ts +17 -0
- package/src/assimilate-cmd.ts +73 -14
- package/src/assimilate-deps.ts +23 -3
- package/src/bare-launch-menu.ts +17 -0
- package/src/claude.ts +31 -37
- package/src/cleanup-cmd.ts +2 -2
- package/src/cli-tool-approval.ts +3 -2
- package/src/codex-remote.ts +4 -2
- package/src/config-utils.ts +186 -18
- package/src/credential-paths.ts +2 -3
- package/src/cubes.ts +9 -4
- package/src/foreign-path-reminder.ts +141 -0
- package/src/launch-access.ts +40 -0
- package/src/launch-all-deps.ts +2 -2
- package/src/lifecycle-log-guard.ts +2 -2
- package/src/local-server-cursor.ts +2 -2
- package/src/opencode-plugin.ts +2 -2
- package/src/parse-assimilate-args.ts +7 -1
- package/src/private-root.ts +62 -2
- package/src/regen-format.ts +1 -1
- package/src/resolved-cli-config.ts +40 -0
- package/src/seat-store.ts +4 -3
- package/src/seats.ts +2 -2
- package/src/self-path.ts +5 -0
- package/src/server-trust.ts +3 -3
- package/src/setup-confirm.ts +1 -1
- package/src/setup-selection.ts +87 -0
- package/src/setup.ts +98 -24
- package/src/stream-owner.ts +2 -2
package/src/private-root.ts
CHANGED
|
@@ -1,8 +1,68 @@
|
|
|
1
|
+
import { lstatSync, realpathSync } from 'node:fs';
|
|
1
2
|
import { chmod, lstat, mkdir } from 'node:fs/promises';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Optional alternate home root for isolated client runs. The value is the
|
|
8
|
+
* replacement home directory, not the `.config/borgmcp` directory itself, so
|
|
9
|
+
* every client-owned path (credentials, seats, worktrees, and agent config)
|
|
10
|
+
* stays under one root.
|
|
11
|
+
*/
|
|
12
|
+
export const BORG_STATE_ROOT_ENV = 'BORG_STATE_ROOT';
|
|
13
|
+
|
|
14
|
+
function invalidStateRoot(): Error {
|
|
15
|
+
return new Error(`${BORG_STATE_ROOT_ENV} must be an absolute canonical path`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Return whether a path and every existing ancestor are free of symlinks. */
|
|
19
|
+
export function isCanonicalPath(root: string): boolean {
|
|
20
|
+
if (!isAbsolute(root) || resolve(root) !== root) return false;
|
|
21
|
+
let candidate = root;
|
|
22
|
+
while (true) {
|
|
23
|
+
try {
|
|
24
|
+
const metadata = lstatSync(candidate);
|
|
25
|
+
return !metadata.isSymbolicLink() && realpathSync(candidate) === candidate;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
28
|
+
const parent = dirname(candidate);
|
|
29
|
+
if (parent === candidate) return false;
|
|
30
|
+
candidate = parent;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function configuredStateRoot(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
36
|
+
const configured = env[BORG_STATE_ROOT_ENV];
|
|
37
|
+
if (configured === undefined) return null;
|
|
38
|
+
if (configured.length === 0 || !isCanonicalPath(configured)) {
|
|
39
|
+
throw invalidStateRoot();
|
|
40
|
+
}
|
|
41
|
+
return configured;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Resolve the effective home root used by all Borg-owned local state. */
|
|
45
|
+
export function borgHomeRoot(env: NodeJS.ProcessEnv = process.env): string {
|
|
46
|
+
return configuredStateRoot(env) ?? realpathSync(homedir());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const borgConfigRoot = (): string => join(borgHomeRoot(), '.config', 'borgmcp');
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Environment used when a native agent CLI registers Borg. The CLI must write
|
|
53
|
+
* its own config under the same effective root that config-utils reads; the
|
|
54
|
+
* eventual MCP child receives BORG_STATE_ROOT separately via its registration.
|
|
55
|
+
*/
|
|
56
|
+
export function borgAgentConfigEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
57
|
+
if (env[BORG_STATE_ROOT_ENV] === undefined) return { ...env };
|
|
58
|
+
const root = borgHomeRoot(env);
|
|
59
|
+
return {
|
|
60
|
+
...env,
|
|
61
|
+
HOME: root,
|
|
62
|
+
CODEX_HOME: join(root, '.codex'),
|
|
63
|
+
XDG_CONFIG_HOME: join(root, '.config'),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
6
66
|
|
|
7
67
|
/** Ensure Borg's local state root exists with owner-only directory permissions. */
|
|
8
68
|
export async function ensurePrivateBorgConfigRoot(root = borgConfigRoot()): Promise<void> {
|
package/src/regen-format.ts
CHANGED
|
@@ -243,7 +243,7 @@ export function markArrivalAnnouncedThisProcess(): void {
|
|
|
243
243
|
export function getDronePlaybook(): string {
|
|
244
244
|
const arrivalInstruction = arrivalAnnouncedThisProcess
|
|
245
245
|
? ''
|
|
246
|
-
: '\n**When this MCP session first starts:** post one `ARRIVAL: <your-label> (<your-role>) online on <hostname
|
|
246
|
+
: '\n**When this MCP session first starts:** post one `ARRIVAL: <your-label> (<your-role>) online on <hostname>` (run `hostname`). After the post succeeds, the client suppresses this instruction until the MCP process restarts; an explicit `/mcp` reconnect may show it again.\n';
|
|
247
247
|
return `## How to operate as a Drone
|
|
248
248
|
|
|
249
249
|
You're a Drone in a Cube. Coordinate with other drones through the activity log.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BorgCli } from './cubes.js';
|
|
2
|
+
|
|
3
|
+
export interface ResolvedCliConfigDeps {
|
|
4
|
+
ensureMcp(cli: BorgCli): void;
|
|
5
|
+
addClaudeProjectSessionStartHook(): void;
|
|
6
|
+
removeClaudeGlobalSessionStartHook(): void;
|
|
7
|
+
addClaudeUserPromptSubmitHook(): void;
|
|
8
|
+
addCodexSessionStartHook(): void;
|
|
9
|
+
addCodexUserPromptSubmitHook(): void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Apply only the integration writes for the CLI the launcher resolved.
|
|
13
|
+
* In particular, a Claude installation must not cause Codex or OpenCode
|
|
14
|
+
* configuration to be repaired as a side effect of a bare `borg` launch.
|
|
15
|
+
*/
|
|
16
|
+
export function configureResolvedCli(
|
|
17
|
+
cli: BorgCli,
|
|
18
|
+
deps: ResolvedCliConfigDeps,
|
|
19
|
+
): void {
|
|
20
|
+
if (cli === 'claude') {
|
|
21
|
+
deps.ensureMcp('claude');
|
|
22
|
+
// The project-local SessionStart hook is the launch-time self-heal for
|
|
23
|
+
// the CLI actually being launched. Remove only the obsolete global hook
|
|
24
|
+
// after the local hook is in place, then keep the Claude prompt hook in
|
|
25
|
+
// its existing global location.
|
|
26
|
+
deps.addClaudeProjectSessionStartHook();
|
|
27
|
+
deps.removeClaudeGlobalSessionStartHook();
|
|
28
|
+
deps.addClaudeUserPromptSubmitHook();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (cli === 'codex') {
|
|
33
|
+
deps.ensureMcp('codex');
|
|
34
|
+
deps.addCodexSessionStartHook();
|
|
35
|
+
deps.addCodexUserPromptSubmitHook();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
deps.ensureMcp('opencode');
|
|
40
|
+
}
|
package/src/seat-store.ts
CHANGED
|
@@ -32,9 +32,10 @@
|
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
import { constants } from 'node:fs';
|
|
35
|
-
import { open, link, lstat, mkdir, readFile,
|
|
35
|
+
import { open, link, lstat, mkdir, readFile, rename, stat, unlink } from 'node:fs/promises';
|
|
36
36
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
37
37
|
import { randomBytes } from 'node:crypto';
|
|
38
|
+
import { isCanonicalPath } from './private-root.js';
|
|
38
39
|
|
|
39
40
|
const LOCK_WAIT_MS = 10;
|
|
40
41
|
const LOCK_ATTEMPTS = 500;
|
|
@@ -58,7 +59,7 @@ async function assertSecureRoot(
|
|
|
58
59
|
root: string,
|
|
59
60
|
rootMode: SecureStoreOptions['rootMode'] = 'private',
|
|
60
61
|
): Promise<void> {
|
|
61
|
-
if (!
|
|
62
|
+
if (!isCanonicalPath(root)) {
|
|
62
63
|
throw new Error(`Borg credential store path ${root} is not canonical`);
|
|
63
64
|
}
|
|
64
65
|
let metadata: Awaited<ReturnType<typeof lstat>>;
|
|
@@ -83,7 +84,7 @@ async function assertSecureRoot(
|
|
|
83
84
|
if (uid !== null && metadata.uid !== uid) {
|
|
84
85
|
throw new Error(`Borg credential store root ${root} is not owned by the current user`);
|
|
85
86
|
}
|
|
86
|
-
if (
|
|
87
|
+
if (!isCanonicalPath(root)) {
|
|
87
88
|
throw new Error(`Borg credential store root ${root} is not canonical or contains a symlink`);
|
|
88
89
|
}
|
|
89
90
|
}
|
package/src/seats.ts
CHANGED
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
* hydration reader; every other observation is digest-only.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import os from 'os';
|
|
21
20
|
import path from 'path';
|
|
22
21
|
import { createHash } from 'node:crypto';
|
|
23
22
|
import { readStoreFile, withStore } from './seat-store.js';
|
|
24
23
|
import { LegacySessionCredentialCollisionError } from './server-errors.js';
|
|
24
|
+
import { borgConfigRoot } from './private-root.js';
|
|
25
25
|
|
|
26
|
-
export const SEATS_FILE = path.join(
|
|
26
|
+
export const SEATS_FILE = path.join(borgConfigRoot(), 'seats.json');
|
|
27
27
|
const SEATS_VERSION = 1 as const;
|
|
28
28
|
const REF_RE = /^borg-server-session:[a-f0-9]{64}$/;
|
|
29
29
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
package/src/self-path.ts
CHANGED
|
@@ -49,3 +49,8 @@ export function resolveClearRewakePath(): string {
|
|
|
49
49
|
export function resolveLogAuditPath(): string {
|
|
50
50
|
return resolveSelfBinPath('log-audit.js');
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/** Absolute path to the optional pre-tool foreign-path reminder hook. */
|
|
54
|
+
export function resolveForeignPathReminderPath(): string {
|
|
55
|
+
return resolveSelfBinPath('foreign-path-reminder.js');
|
|
56
|
+
}
|
package/src/server-trust.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { constants } from 'node:fs';
|
|
|
4
4
|
import { access, lstat, mkdir, open, rename, rm, unlink, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { request as httpsRequest } from 'node:https';
|
|
6
6
|
import { connect as tlsConnect } from 'node:tls';
|
|
7
|
-
import { homedir } from 'node:os';
|
|
8
7
|
import { join, resolve } from 'node:path';
|
|
9
8
|
import { Readable } from 'node:stream';
|
|
10
9
|
import { BorgServerTrustError } from './server-errors.js';
|
|
@@ -27,6 +26,7 @@ import type {
|
|
|
27
26
|
EnrollmentArtifactBinding,
|
|
28
27
|
EnrollmentTrustPointer,
|
|
29
28
|
} from './enrollment-types.js';
|
|
29
|
+
import { borgHomeRoot } from './private-root.js';
|
|
30
30
|
|
|
31
31
|
// CR5 TLS LATTICE: OpenSSL/Node TLS certificate-verification error codes. A raw
|
|
32
32
|
// CA / cert-chain / SAN failure from the pinned transport is a potential MITM and
|
|
@@ -102,12 +102,12 @@ function invalidateOriginTrustCache(origin: string): void {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
function serverDataDirectory(): string {
|
|
105
|
-
return resolve(process.env.BORG_SERVER_DATA_DIR ?? join(
|
|
105
|
+
return resolve(process.env.BORG_SERVER_DATA_DIR ?? join(borgHomeRoot(), '.borg', 'server'));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
function remoteTrustDirectory(origin: string): string {
|
|
109
109
|
const key = createHash('sha256').update(origin).digest('hex');
|
|
110
|
-
return join(
|
|
110
|
+
return join(borgHomeRoot(), '.borg', 'server-trust', key);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
function trustGenerationsDirectory(origin: string): string {
|
package/src/setup-confirm.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Step-1 of the setup wizard writes the user's GLOBAL agent config
|
|
5
5
|
* (registers the borg MCP server + hooks). Before gh#818 this happened
|
|
6
6
|
* silently on first run. This module adds informed-consent disclosure:
|
|
7
|
-
* it lists WHICH files will be written (well-known
|
|
7
|
+
* it lists WHICH files will be written (well-known home-relative paths)
|
|
8
8
|
* and asks to continue before the first mutation.
|
|
9
9
|
*
|
|
10
10
|
* Pure / dep-injected (mirrors `resolveCliChoice` + `setup-action.ts`) so
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { BorgCli } from './cubes.js';
|
|
2
|
+
|
|
3
|
+
export interface SetupAgentChoice {
|
|
4
|
+
title: string;
|
|
5
|
+
value: BorgCli;
|
|
6
|
+
selected: boolean;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type SetupAgentSelection =
|
|
11
|
+
| { kind: 'selected'; agents: BorgCli[] }
|
|
12
|
+
| { kind: 'empty' }
|
|
13
|
+
| { kind: 'cancelled' };
|
|
14
|
+
|
|
15
|
+
export interface EmptySetupOutcome {
|
|
16
|
+
kind: 'empty';
|
|
17
|
+
agentConfigurationChanged: false;
|
|
18
|
+
localServerInitializationStarted: false;
|
|
19
|
+
recovery: {
|
|
20
|
+
kind: 'rerun-setup';
|
|
21
|
+
command: 'borg setup';
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CLI_TITLES: Record<BorgCli, string> = {
|
|
26
|
+
claude: 'Claude Code',
|
|
27
|
+
codex: 'Codex',
|
|
28
|
+
opencode: 'OpenCode',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Build the first-run choices from the CLIs that are actually installed. */
|
|
32
|
+
export function setupAgentChoices(
|
|
33
|
+
detected: readonly BorgCli[],
|
|
34
|
+
alreadyConfigured: ReadonlySet<BorgCli> = new Set(),
|
|
35
|
+
): SetupAgentChoice[] {
|
|
36
|
+
return [...new Set(detected)].map((cli) => ({
|
|
37
|
+
title: alreadyConfigured.has(cli)
|
|
38
|
+
? `${CLI_TITLES[cli]} (already configured)`
|
|
39
|
+
: CLI_TITLES[cli],
|
|
40
|
+
value: cli,
|
|
41
|
+
selected: !alreadyConfigured.has(cli),
|
|
42
|
+
...(alreadyConfigured.has(cli) ? { disabled: true } : {}),
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The agent names whose newly selected setup needs a restart notice. */
|
|
47
|
+
export function setupRestartInstruction(selected: readonly BorgCli[]): string {
|
|
48
|
+
const labels = selected.map((cli) => CLI_TITLES[cli]);
|
|
49
|
+
return `🔄 Restart ${labels.join(' / ')} (or open a new session) for the changes to take effect.`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Describe the accepted zero-selection result and its recovery path. */
|
|
53
|
+
export function emptySetupOutcome(): EmptySetupOutcome {
|
|
54
|
+
return {
|
|
55
|
+
kind: 'empty',
|
|
56
|
+
agentConfigurationChanged: false,
|
|
57
|
+
localServerInitializationStarted: false,
|
|
58
|
+
recovery: {
|
|
59
|
+
kind: 'rerun-setup',
|
|
60
|
+
command: 'borg setup',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Keep only detected agents and return them in detection order. The selected
|
|
67
|
+
* set is invocation-local; it is deliberately never persisted.
|
|
68
|
+
*/
|
|
69
|
+
export function normalizeSetupAgentSelection(
|
|
70
|
+
detected: readonly BorgCli[],
|
|
71
|
+
selected: readonly unknown[] | undefined,
|
|
72
|
+
): BorgCli[] {
|
|
73
|
+
if (!Array.isArray(selected)) return [];
|
|
74
|
+
const selectedSet = new Set(selected);
|
|
75
|
+
return [...new Set(detected)].filter((cli) => selectedSet.has(cli));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Turn the prompt result into the terminal outcome used by the wizard. */
|
|
79
|
+
export function resolveSetupAgentSelection(
|
|
80
|
+
detected: readonly BorgCli[],
|
|
81
|
+
selected: readonly unknown[] | undefined,
|
|
82
|
+
cancelled = false,
|
|
83
|
+
): SetupAgentSelection {
|
|
84
|
+
if (cancelled) return { kind: 'cancelled' };
|
|
85
|
+
const agents = normalizeSetupAgentSelection(detected, selected);
|
|
86
|
+
return agents.length === 0 ? { kind: 'empty' } : { kind: 'selected', agents };
|
|
87
|
+
}
|
package/src/setup.ts
CHANGED
|
@@ -39,6 +39,13 @@ import { initDebugFromArgv } from './debug.js';
|
|
|
39
39
|
import { defaultApprovalIo, setupApprovalWarnings } from './cli-tool-approval.js';
|
|
40
40
|
import { offerFirstRunServerInstall } from './first-run-server.js';
|
|
41
41
|
import { setupNextStepsText } from './cli-help.js';
|
|
42
|
+
import {
|
|
43
|
+
emptySetupOutcome,
|
|
44
|
+
resolveSetupAgentSelection,
|
|
45
|
+
setupAgentChoices,
|
|
46
|
+
setupRestartInstruction,
|
|
47
|
+
} from './setup-selection.js';
|
|
48
|
+
import type { BorgCli } from './cubes.js';
|
|
42
49
|
|
|
43
50
|
/**
|
|
44
51
|
* Main setup wizard
|
|
@@ -69,18 +76,8 @@ async function main() {
|
|
|
69
76
|
process.exit(1);
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// partial setup state behind.
|
|
75
|
-
console.log(chalk.blue('◼ Local Server'));
|
|
76
|
-
const serverInstall = await offerFirstRunServerInstall(undefined, undefined, { initializeServer: true });
|
|
77
|
-
if (serverInstall.kind !== 'present' && serverInstall.kind !== 'installed') {
|
|
78
|
-
process.exit(serverInstall.kind === 'declined' ? 0 : 1);
|
|
79
|
-
}
|
|
80
|
-
console.log('');
|
|
81
|
-
console.log('◼ Local server initialized');
|
|
82
|
-
|
|
83
|
-
// Step 1: Configure every detected agent CLI
|
|
79
|
+
// Step 1: Choose which detected agent CLIs to configure. The choice is
|
|
80
|
+
// first-run/invocation-local; it is never persisted as an opt-out.
|
|
84
81
|
console.log(chalk.blue('◼ Agent CLI Integration'));
|
|
85
82
|
|
|
86
83
|
const yes = parseYesFlag(process.argv);
|
|
@@ -88,6 +85,12 @@ async function main() {
|
|
|
88
85
|
const claudeDetected = claudeCliPath !== null;
|
|
89
86
|
const codexDetected = codexCliPath !== null;
|
|
90
87
|
const opencodeDetected = opencodeCliPath !== null;
|
|
88
|
+
const detectedClis: BorgCli[] = [
|
|
89
|
+
...(claudeDetected ? ['claude' as const] : []),
|
|
90
|
+
...(codexDetected ? ['codex' as const] : []),
|
|
91
|
+
...(opencodeDetected ? ['opencode' as const] : []),
|
|
92
|
+
];
|
|
93
|
+
|
|
91
94
|
const claudeMcpConfigured = isMcpServerConfigured();
|
|
92
95
|
const codexMcpConfigured = isCodexMcpServerConfigured();
|
|
93
96
|
const opencodeMcpConfigured = isOpenCodeMcpServerConfigured();
|
|
@@ -95,14 +98,73 @@ async function main() {
|
|
|
95
98
|
const claudeUpsHookPending = claudeDetected && !isUserPromptSubmitHookRegistered();
|
|
96
99
|
const codexSessionHookPending = codexDetected && !isCodexSessionStartHookRegistered();
|
|
97
100
|
const codexUpsHookPending = codexDetected && !isCodexUserPromptSubmitHookRegistered();
|
|
98
|
-
const
|
|
99
|
-
|
|
101
|
+
const alreadyConfigured = new Set<BorgCli>([
|
|
102
|
+
...(claudeDetected && claudeMcpConfigured && !claudeLegacyHookPending && !claudeUpsHookPending
|
|
103
|
+
? ['claude' as const]
|
|
104
|
+
: []),
|
|
105
|
+
...(codexDetected && codexMcpConfigured && !codexSessionHookPending && !codexUpsHookPending
|
|
106
|
+
? ['codex' as const]
|
|
107
|
+
: []),
|
|
108
|
+
...(opencodeDetected && opencodeMcpConfigured ? ['opencode' as const] : []),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const allDetectedMutationPending = setupMutationPending({
|
|
112
|
+
claude: claudeDetected,
|
|
113
|
+
codex: codexDetected,
|
|
114
|
+
opencode: opencodeDetected,
|
|
115
|
+
claudeMcpConfigured,
|
|
116
|
+
codexMcpConfigured,
|
|
117
|
+
opencodeMcpConfigured,
|
|
118
|
+
claudeHookPending: claudeLegacyHookPending || claudeUpsHookPending,
|
|
119
|
+
codexHookPending: codexSessionHookPending || codexUpsHookPending,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
let selectedClis = detectedClis;
|
|
123
|
+
if (allDetectedMutationPending && !yes && process.stdin.isTTY === true) {
|
|
124
|
+
let cancelled = false;
|
|
125
|
+
const answer = await prompts(
|
|
126
|
+
{
|
|
127
|
+
type: 'multiselect',
|
|
128
|
+
name: 'selected',
|
|
129
|
+
message: 'Which detected agent CLIs should Borg configure?',
|
|
130
|
+
hint: 'Space toggles selection; Enter accepts the checked agents',
|
|
131
|
+
choices: setupAgentChoices(detectedClis, alreadyConfigured),
|
|
132
|
+
instructions: false,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
onCancel: () => {
|
|
136
|
+
cancelled = true;
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
const selection = resolveSetupAgentSelection(detectedClis, answer.selected, cancelled);
|
|
141
|
+
if (selection.kind === 'cancelled') {
|
|
142
|
+
console.log(chalk.yellow('\n◼ Setup cancelled — no changes made.\n'));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (selection.kind === 'empty') {
|
|
146
|
+
const outcome = emptySetupOutcome();
|
|
147
|
+
console.log(chalk.yellow(
|
|
148
|
+
`\n◼ No agent CLIs selected — agent configuration and local-server initialization were skipped. ` +
|
|
149
|
+
`Run \`${outcome.recovery.command}\` again to choose an agent and continue setup.\n`,
|
|
150
|
+
));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
selectedClis = selection.agents;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const selected = new Set(selectedClis);
|
|
157
|
+
const claudeSelected = selected.has('claude');
|
|
158
|
+
const codexSelected = selected.has('codex');
|
|
159
|
+
const opencodeSelected = selected.has('opencode');
|
|
160
|
+
const claudeHookPending = claudeSelected && (claudeLegacyHookPending || claudeUpsHookPending);
|
|
161
|
+
const codexHookPending = codexSelected && (codexSessionHookPending || codexUpsHookPending);
|
|
100
162
|
|
|
101
163
|
if (
|
|
102
164
|
setupMutationPending({
|
|
103
|
-
claude:
|
|
104
|
-
codex:
|
|
105
|
-
opencode:
|
|
165
|
+
claude: claudeSelected,
|
|
166
|
+
codex: codexSelected,
|
|
167
|
+
opencode: opencodeSelected,
|
|
106
168
|
claudeMcpConfigured,
|
|
107
169
|
codexMcpConfigured,
|
|
108
170
|
opencodeMcpConfigured,
|
|
@@ -112,7 +174,7 @@ async function main() {
|
|
|
112
174
|
) {
|
|
113
175
|
console.log(
|
|
114
176
|
formatConfigMutationDisclosure(
|
|
115
|
-
configMutationTargets({ claude:
|
|
177
|
+
configMutationTargets({ claude: claudeSelected, codex: codexSelected, opencode: opencodeSelected })
|
|
116
178
|
)
|
|
117
179
|
);
|
|
118
180
|
const mutationDecision = await confirmConfigMutation({
|
|
@@ -135,7 +197,19 @@ async function main() {
|
|
|
135
197
|
}
|
|
136
198
|
console.log('');
|
|
137
199
|
|
|
138
|
-
|
|
200
|
+
// Resolve the separately published local server only after the complete
|
|
201
|
+
// agent selection and the existing single config-mutation confirmation.
|
|
202
|
+
// Esc/zero-selection/decline therefore leave both agent and server setup
|
|
203
|
+
// untouched.
|
|
204
|
+
console.log(chalk.blue('◼ Local Server'));
|
|
205
|
+
const serverInstall = await offerFirstRunServerInstall(undefined, undefined, { initializeServer: true });
|
|
206
|
+
if (serverInstall.kind !== 'present' && serverInstall.kind !== 'installed') {
|
|
207
|
+
process.exit(serverInstall.kind === 'declined' ? 0 : 1);
|
|
208
|
+
}
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log('◼ Local server initialized');
|
|
211
|
+
|
|
212
|
+
if (claudeSelected) {
|
|
139
213
|
try {
|
|
140
214
|
ensureCliMcpConfigured('claude');
|
|
141
215
|
if (claudeLegacyHookPending) removeSessionStartHook();
|
|
@@ -146,7 +220,7 @@ async function main() {
|
|
|
146
220
|
process.exit(1);
|
|
147
221
|
}
|
|
148
222
|
}
|
|
149
|
-
if (
|
|
223
|
+
if (codexSelected) {
|
|
150
224
|
try {
|
|
151
225
|
ensureCliMcpConfigured('codex');
|
|
152
226
|
if (codexSessionHookPending) addCodexSessionStartHook();
|
|
@@ -157,7 +231,7 @@ async function main() {
|
|
|
157
231
|
process.exit(1);
|
|
158
232
|
}
|
|
159
233
|
}
|
|
160
|
-
if (
|
|
234
|
+
if (opencodeSelected) {
|
|
161
235
|
try {
|
|
162
236
|
ensureCliMcpConfigured('opencode');
|
|
163
237
|
console.log(chalk.green('◼ borg configured for OpenCode'));
|
|
@@ -172,8 +246,8 @@ async function main() {
|
|
|
172
246
|
codexArgs: [],
|
|
173
247
|
});
|
|
174
248
|
for (const warning of await setupApprovalWarnings(approvalIo, {
|
|
175
|
-
codex:
|
|
176
|
-
opencode:
|
|
249
|
+
codex: codexSelected,
|
|
250
|
+
opencode: opencodeSelected,
|
|
177
251
|
})) {
|
|
178
252
|
console.log(chalk.yellow(`warning: ${warning}`));
|
|
179
253
|
}
|
|
@@ -185,7 +259,7 @@ async function main() {
|
|
|
185
259
|
|
|
186
260
|
// Success message
|
|
187
261
|
console.log(chalk.green.bold('\nSetup complete!\n'));
|
|
188
|
-
console.log(chalk.yellow(
|
|
262
|
+
console.log(chalk.yellow(`${setupRestartInstruction(selectedClis)}\n`));
|
|
189
263
|
if (!serverInstall.suppressClientNextSteps) {
|
|
190
264
|
console.log(chalk.gray(setupNextStepsText()));
|
|
191
265
|
}
|
package/src/stream-owner.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import path from 'node:path';
|
|
4
|
+
import { borgConfigRoot } from './private-root.js';
|
|
5
5
|
|
|
6
6
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7
|
-
const STREAM_LOCKS_DIR = path.join(
|
|
7
|
+
const STREAM_LOCKS_DIR = path.join(borgConfigRoot(), 'stream-locks');
|
|
8
8
|
const OWNER_FILE = 'owner.json';
|
|
9
9
|
const TAKEOVER_FILE = 'takeover.json';
|
|
10
10
|
const SCHEMA_VERSION = 1;
|