borgmcp 2.15.0 → 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.
Files changed (46) hide show
  1. package/dist/assimilate-cmd.d.ts +2 -0
  2. package/dist/assimilate-cmd.d.ts.map +1 -1
  3. package/dist/assimilate-cmd.js +20 -7
  4. package/dist/assimilate-cmd.js.map +1 -1
  5. package/dist/assimilate-deps.d.ts.map +1 -1
  6. package/dist/assimilate-deps.js +2 -1
  7. package/dist/assimilate-deps.js.map +1 -1
  8. package/dist/bare-launch-menu.d.ts +8 -0
  9. package/dist/bare-launch-menu.d.ts.map +1 -1
  10. package/dist/bare-launch-menu.js +12 -0
  11. package/dist/bare-launch-menu.js.map +1 -1
  12. package/dist/claude.d.ts.map +1 -1
  13. package/dist/claude.js +26 -39
  14. package/dist/claude.js.map +1 -1
  15. package/dist/cubes.d.ts +6 -1
  16. package/dist/cubes.d.ts.map +1 -1
  17. package/dist/cubes.js +7 -2
  18. package/dist/cubes.js.map +1 -1
  19. package/dist/parse-assimilate-args.d.ts.map +1 -1
  20. package/dist/parse-assimilate-args.js +6 -1
  21. package/dist/parse-assimilate-args.js.map +1 -1
  22. package/dist/regen-format.js +1 -1
  23. package/dist/regen-format.js.map +1 -1
  24. package/dist/resolved-cli-config.d.ts +16 -0
  25. package/dist/resolved-cli-config.d.ts.map +1 -0
  26. package/dist/resolved-cli-config.js +26 -0
  27. package/dist/resolved-cli-config.js.map +1 -0
  28. package/dist/setup-selection.d.ts +38 -0
  29. package/dist/setup-selection.d.ts.map +1 -0
  30. package/dist/setup-selection.js +51 -0
  31. package/dist/setup-selection.js.map +1 -0
  32. package/dist/setup.js +80 -21
  33. package/dist/setup.js.map +1 -1
  34. package/docs/EXTRACTION_PROVENANCE.md +3 -3
  35. package/docs/RELEASING.md +8 -282
  36. package/package.json +8 -9
  37. package/src/assimilate-cmd.ts +24 -7
  38. package/src/assimilate-deps.ts +2 -0
  39. package/src/bare-launch-menu.ts +17 -0
  40. package/src/claude.ts +29 -37
  41. package/src/cubes.ts +7 -2
  42. package/src/parse-assimilate-args.ts +7 -1
  43. package/src/regen-format.ts +1 -1
  44. package/src/resolved-cli-config.ts +40 -0
  45. package/src/setup-selection.ts +87 -0
  46. package/src/setup.ts +98 -24
@@ -60,6 +60,7 @@ import {
60
60
  hasPersistedActiveCube as cubesHasPersistedActive,
61
61
  setActiveCube as cubesSetActive,
62
62
  inboxPathForDrone,
63
+ setProjectCliPreference,
63
64
  setCodexWakeTarget,
64
65
  } from './cubes.js';
65
66
  import {
@@ -575,6 +576,7 @@ export function buildDefaultAssimilateDeps(
575
576
  }
576
577
  }, () => process.stdin.isTTY === true)
577
578
  ),
579
+ setCliPreferenceForWorktree: (cli, worktree) => setProjectCliPreference(cli, worktree),
578
580
  prepareCodexRemoteLaunch: () => prepareCodexRemoteLaunch(defaultCodexRemoteDeps()),
579
581
  setCodexWakeTarget,
580
582
  findLoadedCodexThread,
@@ -39,6 +39,23 @@ export interface LaunchMenuInputs {
39
39
  hasLaunchAllTargets: boolean;
40
40
  }
41
41
 
42
+ /**
43
+ * Resolve and configure the CLI that will actually launch.
44
+ *
45
+ * This callback deliberately runs after the one-shot menu choice. A bare
46
+ * launch may resolve one default CLI first and then launch a different
47
+ * installed CLI for this invocation; only the final CLI may be self-healed.
48
+ */
49
+ export function configureSelectedLaunchCli(
50
+ defaultCli: BorgCli,
51
+ action: LaunchMenuAction | undefined,
52
+ configure: (cli: BorgCli) => void,
53
+ ): BorgCli {
54
+ const cli = action?.kind === 'launch' ? action.cli : defaultCli;
55
+ configure(cli);
56
+ return cli;
57
+ }
58
+
42
59
  const PRETTY: Record<BorgCli, string> = { claude: 'Claude', codex: 'Codex', opencode: 'OpenCode' };
43
60
 
44
61
  /**
package/src/claude.ts CHANGED
@@ -24,7 +24,7 @@ import { basename } from 'node:path';
24
24
  import { createInterface } from 'node:readline/promises';
25
25
  import { fileURLToPath } from 'node:url';
26
26
  import chalk from 'chalk';
27
- import { findProjectRoot, getActiveCube, inboxPathForDrone, setCodexWakeTarget, pruneDeadCodexWakeTargets } from './cubes.js';
27
+ import { findProjectRoot, getActiveCube, inboxPathForDrone, setCodexWakeTarget, pruneDeadCodexWakeTargets, type BorgCli } from './cubes.js';
28
28
  import { monitorStateRootForWorktree } from './inbox-monitor.js';
29
29
  import { formatSeatReattachRefusal, inspectLiveInboxMonitor } from './seat-reattach-guard.js';
30
30
  import { handleVersionFlag, getPackageVersion } from './version.js';
@@ -48,10 +48,12 @@ import { runLaunchAll } from './launch-all-cmd.js';
48
48
  import { buildDefaultLaunchAllDeps } from './launch-all-deps.js';
49
49
  import { discoverDroneCandidates } from './launch-all-discovery.js';
50
50
  import {
51
+ configureSelectedLaunchCli,
51
52
  explicitCliLaunchHint,
52
53
  runBareLaunchMenu,
53
54
  shouldResolveExplicitCliLaunchHintTargets,
54
55
  shouldShowLaunchMenu,
56
+ type LaunchMenuAction,
55
57
  } from './bare-launch-menu.js';
56
58
  import { setTerminalTitle } from './terminal-title.js';
57
59
  import { initConsolePrefix, consolePrefix } from './console-prefix.js';
@@ -81,6 +83,7 @@ import {
81
83
  removeSessionStartHook,
82
84
  } from './config-utils.js';
83
85
  import { ensureCliMcpConfigured } from './ensure-mcp-config.js';
86
+ import { configureResolvedCli } from './resolved-cli-config.js';
84
87
  import { installBorgPlugin } from './opencode-plugin.js';
85
88
  import { allocateOpenCodePort, connectOpenCodeDrone, createOpenCodeLaunchKickoff, injectInitialKickoff, openCodeLaunchBinding } from './opencode-drone.js';
86
89
  import { buildOpenCodeLaunchArgs, defaultApprovalIo, resolveLaunchBorgApprovals } from './cli-tool-approval.js';
@@ -322,7 +325,7 @@ async function main() {
322
325
  parsedCli.cli,
323
326
  defaultCliChoiceDeps(prompt, () => process.stdin.isTTY === true)
324
327
  );
325
- ensureDetectedCliConfigured();
328
+ let launchAction: LaunchMenuAction | undefined;
326
329
 
327
330
  // Active cube for this directory — needed for the launch menu's option-3
328
331
  // availability, the terminal title, and the inbox-Monitor clause below.
@@ -383,9 +386,13 @@ async function main() {
383
386
  // option 1 → configured default; option 2 → the other agent, ONE-SHOT
384
387
  // (we deliberately do NOT call setProjectCliPreference — the saved
385
388
  // preference is changed only via `borg --cli <agent>`).
386
- cli = action.cli;
389
+ launchAction = action;
387
390
  }
388
391
 
392
+ // Configure only the CLI that will actually launch. This must follow the
393
+ // one-shot menu: the resolved default can differ from the menu selection.
394
+ cli = configureSelectedLaunchCli(cli, launchAction, ensureResolvedCliConfigured);
395
+
389
396
  if (active && !parsedCli.force) {
390
397
  const inboxPath = inboxPathForDrone(active.cubeId, active.droneId);
391
398
  const stateRoot = monitorStateRootForWorktree(findProjectRoot(process.cwd()));
@@ -616,40 +623,25 @@ async function main() {
616
623
  });
617
624
  }
618
625
 
619
- function ensureDetectedCliConfigured(): void {
620
- const found = detectCliAvailability();
621
- if (found.claude) {
622
- try {
623
- ensureCliMcpConfigured('claude');
624
- // gh#673 P2 (WI-1): the orientation hook lives PROJECT-LOCAL in
625
- // <root>/.claude/settings.local.json ensured on every bare
626
- // `borg` launch so pre-P2 worktrees self-heal. The legacy GLOBAL
627
- // hook is then removed: safe because this ensure precedes every
628
- // borg-launched agent spawn (other projects get their local hook
629
- // at their own next launch/assimilate), and P1's BORG_SESSION
630
- // gate already no-ops the global hook in non-borg sessions.
631
- addProjectSessionStartHook(findProjectRoot(process.cwd()));
632
- removeSessionStartHook();
633
- addUserPromptSubmitHook();
634
- } catch (err: any) {
635
- console.error(`${consolePrefix()}${chalk.yellow(`warning: Claude Code integration check failed: ${err?.message ?? err}`)}`);
636
- }
637
- }
638
- if (found.codex) {
639
- try {
640
- ensureCliMcpConfigured('codex');
641
- addCodexSessionStartHook();
642
- addCodexUserPromptSubmitHook();
643
- } catch (err: any) {
644
- console.error(`${consolePrefix()}${chalk.yellow(`warning: Codex integration check failed: ${err?.message ?? err}`)}`);
645
- }
646
- }
647
- if (found.opencode) {
648
- try {
649
- ensureCliMcpConfigured('opencode');
650
- } catch (err: any) {
651
- console.error(`${consolePrefix()}${chalk.yellow(`warning: OpenCode integration check failed: ${err?.message ?? err}`)}`);
652
- }
626
+ function ensureResolvedCliConfigured(cli: BorgCli): void {
627
+ const label = cli === 'claude' ? 'Claude Code' : cli === 'codex' ? 'Codex' : 'OpenCode';
628
+ try {
629
+ configureResolvedCli(cli, {
630
+ ensureMcp: ensureCliMcpConfigured,
631
+ addClaudeProjectSessionStartHook: () => {
632
+ // gh#673 P2 (WI-1): the orientation hook lives PROJECT-LOCAL in
633
+ // <root>/.claude/settings.local.json ensured on every bare
634
+ // `borg` launch so pre-P2 worktrees self-heal. The legacy GLOBAL
635
+ // hook is then removed after the local hook is in place.
636
+ addProjectSessionStartHook(findProjectRoot(process.cwd()));
637
+ },
638
+ removeClaudeGlobalSessionStartHook: removeSessionStartHook,
639
+ addClaudeUserPromptSubmitHook: addUserPromptSubmitHook,
640
+ addCodexSessionStartHook,
641
+ addCodexUserPromptSubmitHook,
642
+ });
643
+ } catch (err: any) {
644
+ console.error(`${consolePrefix()}${chalk.yellow(`warning: ${label} integration check failed: ${err?.message ?? err}`)}`);
653
645
  }
654
646
  }
655
647
 
package/src/cubes.ts CHANGED
@@ -611,9 +611,14 @@ export async function readAllProjectIdentities(): Promise<
611
611
  );
612
612
  }
613
613
 
614
- export async function setProjectCliPreference(cli: BorgCli): Promise<void> {
614
+ /**
615
+ * Save the CLI preference for the current project, or for an explicitly named
616
+ * worktree. The explicit path is used when assimilate has just created a
617
+ * sibling worktree but the process still began in the invoking checkout.
618
+ */
619
+ export async function setProjectCliPreference(cli: BorgCli, dir?: string): Promise<void> {
615
620
  const existing = (await readLaunchFile()) ?? { projects: {} };
616
- existing.projects[findProjectRoot()] = { cli };
621
+ existing.projects[findProjectRoot(dir)] = { cli };
617
622
  await writeLaunchFile(existing);
618
623
  }
619
624
 
@@ -115,7 +115,13 @@ export function parseAssimilateArgs(rawArgs: string[]): ParseResult {
115
115
  if (enrollmentRequested) {
116
116
  return { ok: false, error: ENROLLMENT_POSITIONAL_INPUT_ERROR };
117
117
  }
118
- return { ok: false, error: `unexpected extra argument: ${arg} (already have role "${role}")` };
118
+ return {
119
+ ok: false,
120
+ error:
121
+ `unexpected extra argument: ${arg} (already have role "${role}"). ` +
122
+ 'Multi-word role names must be quoted (for example, `borg assimilate "Code Reviewer"`) ' +
123
+ 'or written with hyphens.',
124
+ };
119
125
  }
120
126
  if (enrollmentRequested && !validateName(arg).ok) {
121
127
  return { ok: false, error: ENROLLMENT_POSITIONAL_INPUT_ERROR };
@@ -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> at <project-path>` (run `hostname`; use cwd for the path). After the post succeeds, the client suppresses this instruction until the MCP process restarts; an explicit `/mcp` reconnect may show it again.\n';
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
+ }
@@ -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
- // Resolve the separately published local server before setup writes agent
73
- // configuration. Decline/non-interactive/failure paths therefore leave no
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 claudeHookPending = claudeLegacyHookPending || claudeUpsHookPending;
99
- const codexHookPending = codexSessionHookPending || codexUpsHookPending;
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: claudeDetected,
104
- codex: codexDetected,
105
- opencode: opencodeDetected,
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: claudeDetected, codex: codexDetected, opencode: opencodeDetected })
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
- if (claudeCliPath) {
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 (codexCliPath) {
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 (opencodeCliPath) {
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: codexDetected,
176
- opencode: opencodeDetected,
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('🔄 Restart Claude Code / Codex / OpenCode (or open a new session) for the changes to take effect.\n'));
262
+ console.log(chalk.yellow(`${setupRestartInstruction(selectedClis)}\n`));
189
263
  if (!serverInstall.suppressClientNextSteps) {
190
264
  console.log(chalk.gray(setupNextStepsText()));
191
265
  }