borgmcp 2.8.0 → 2.9.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 (57) hide show
  1. package/dist/assimilate-cmd.d.ts +1 -0
  2. package/dist/assimilate-cmd.d.ts.map +1 -1
  3. package/dist/assimilate-cmd.js +18 -4
  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 +3 -0
  7. package/dist/assimilate-deps.js.map +1 -1
  8. package/dist/cli-help.d.ts.map +1 -1
  9. package/dist/cli-help.js +2 -3
  10. package/dist/cli-help.js.map +1 -1
  11. package/dist/config.d.ts +1 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +3 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/first-run-server.d.ts +10 -2
  16. package/dist/first-run-server.d.ts.map +1 -1
  17. package/dist/first-run-server.js +54 -7
  18. package/dist/first-run-server.js.map +1 -1
  19. package/dist/remote-client.d.ts +1 -1
  20. package/dist/remote-client.d.ts.map +1 -1
  21. package/dist/remote-client.js +1 -1
  22. package/dist/remote-client.js.map +1 -1
  23. package/dist/runtime-metadata.d.ts +1 -1
  24. package/dist/runtime-metadata.d.ts.map +1 -1
  25. package/dist/runtime-metadata.js +3 -3
  26. package/dist/runtime-metadata.js.map +1 -1
  27. package/dist/server-errors.d.ts +1 -1
  28. package/dist/server-errors.d.ts.map +1 -1
  29. package/dist/server-errors.js.map +1 -1
  30. package/dist/server-handshake.d.ts +4 -0
  31. package/dist/server-handshake.d.ts.map +1 -1
  32. package/dist/server-handshake.js +18 -0
  33. package/dist/server-handshake.js.map +1 -1
  34. package/dist/setup.js +5 -2
  35. package/dist/setup.js.map +1 -1
  36. package/dist/tool-manifest.d.ts.map +1 -1
  37. package/dist/tool-manifest.js +5 -3
  38. package/dist/tool-manifest.js.map +1 -1
  39. package/dist/update-cmd.d.ts +1 -0
  40. package/dist/update-cmd.d.ts.map +1 -1
  41. package/dist/update-cmd.js +8 -3
  42. package/dist/update-cmd.js.map +1 -1
  43. package/docs/EXTRACTION_PROVENANCE.md +3 -3
  44. package/docs/RELEASING.md +6 -1
  45. package/package.json +1 -1
  46. package/src/assimilate-cmd.ts +25 -6
  47. package/src/assimilate-deps.ts +3 -0
  48. package/src/cli-help.ts +2 -3
  49. package/src/config.ts +4 -0
  50. package/src/first-run-server.ts +71 -8
  51. package/src/remote-client.ts +2 -2
  52. package/src/runtime-metadata.ts +4 -3
  53. package/src/server-errors.ts +1 -0
  54. package/src/server-handshake.ts +29 -0
  55. package/src/setup.ts +5 -2
  56. package/src/tool-manifest.ts +5 -3
  57. package/src/update-cmd.ts +8 -3
package/docs/RELEASING.md CHANGED
@@ -173,8 +173,13 @@ Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.7.3
173
173
  `83fadd1b308f2b770ce0d8da6a3b61183ec94f8d`. Workflow run `30618377765`, attempt 1,
174
174
  successfully published that exact source as `borgmcp@2.7.3`; the same-run artifact report records integrity
175
175
  `sha512-Jb+Kg/+H3cPU34JX7eFV3gEO/PzdfJEruNU1mJwYlQZKG7TMfrLJzwghDzxmlMupHJHL3rzAVZ224wioM/pclg==`.
176
+ Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.8.0` tag object
177
+ `576e2401a83537662ef0d5f7b0d03d6101b15c54` peels to protected-main commit
178
+ `ad40e93ee9b1ff2801119c1c763ad526974ae2a6`. Workflow run `30639790368`, attempt 1,
179
+ successfully published that exact source as `borgmcp@2.8.0`; the same-run artifact report records integrity
180
+ `sha512-hKNyWwVg/8WJ/qcqaNjZVylMw2UzgFEhE7sAJmWy/BjNU4lEa6ox8N7SdbEt8hhKsySMxt+YTFbNFitRtf9u3g==`.
176
181
  Never move, replace, reuse, or rerun that tag or workflow. The next candidate
177
- uses the unused `v2.8.0` identity from a fresh reviewed protected-main commit
182
+ uses the unused `v2.9.0` identity from a fresh reviewed protected-main commit
178
183
  and requires the complete release gate again.
179
184
 
180
185
  ## Release Prerequisites
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "Coordinate AI coding agents in shared cubes. Works with Claude Code, Codex, and OpenCode.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -277,7 +277,7 @@ export interface AssimilateDeps {
277
277
  detectLocalServer: () => Promise<string | null>;
278
278
  connectServer: (
279
279
  apiUrl: string,
280
- enrollment?: { invitation: string },
280
+ enrollment?: { invitation: string; confirmReplacement?: () => Promise<boolean> },
281
281
  ) => Promise<{
282
282
  token: string;
283
283
  trustIdentity: string;
@@ -361,6 +361,11 @@ function affirmative(answer: string): boolean {
361
361
  return normalized === '' || normalized === 'y' || normalized === 'yes';
362
362
  }
363
363
 
364
+ function strictAffirmative(answer: string): boolean {
365
+ const normalized = answer.trim().toLowerCase();
366
+ return normalized === 'y' || normalized === 'yes';
367
+ }
368
+
364
369
  async function selectAssimilationAuthority(
365
370
  flags: AssimilateFlags,
366
371
  deps: AssimilateDeps,
@@ -475,6 +480,14 @@ function reportServerFailure(
475
480
  );
476
481
  return 1;
477
482
  }
483
+ if (error instanceof BorgServerError && error.code === 'LOCAL_CREDENTIAL_EXISTS') {
484
+ deps.stderr(
485
+ `A local enrollment for ${apiUrl} already exists and was not replaced. ` +
486
+ `Re-run ${localAssimilateCommand(apiUrl, true, mode)} and explicitly confirm replacement ` +
487
+ 'only if the first enrolled client should be abandoned.\n',
488
+ );
489
+ return 1;
490
+ }
478
491
  if (error instanceof LegacySessionCredentialCollisionError) {
479
492
  const recovery = mode === 'cube-init'
480
493
  ? localAssimilateCommand(error.origin, true, mode)
@@ -729,8 +742,14 @@ export async function runAssimilate(
729
742
  );
730
743
  return 1;
731
744
  }
732
- try {
733
- serverAuth = await deps.connectServer(authority.apiUrl, { invitation });
745
+ try {
746
+ serverAuth = await deps.connectServer(authority.apiUrl, {
747
+ invitation,
748
+ confirmReplacement: async () => strictAffirmative(await deps.prompt(
749
+ `A local enrollment for ${authority.apiUrl} already exists. Replacing it will orphan ` +
750
+ 'the first enrolled client. Replace it? [y/N]: ',
751
+ )),
752
+ });
734
753
  } finally {
735
754
  // Strings cannot be zeroized in JavaScript, but drop this command's
736
755
  // reference immediately after the exchange instead of retaining the
@@ -1492,7 +1511,7 @@ export async function runAssimilate(
1492
1511
  deps.stderr(
1493
1512
  'Borg could not enumerate this repository’s existing worktrees, so it did not risk creating a colliding sibling.\n' +
1494
1513
  'Run `git worktree list` from this repository and resolve the reported Git error, then rerun `borg assimilate`.\n' +
1495
- 'A seat was reserved and remains pending; rerunning after fixing the worktree issue resumes that seat.\n',
1514
+ 'A local drone reservation was created and remains pending; rerunning after fixing the worktree issue resumes that reservation.\n',
1496
1515
  );
1497
1516
  return 1;
1498
1517
  }
@@ -1565,13 +1584,13 @@ export async function runAssimilate(
1565
1584
  ? `Git left branch ${residualBranch} without a registered worktree; Borg preserved it.\n`
1566
1585
  : '') +
1567
1586
  'Run `git worktree list` and `git status` to inspect repository state, resolve the reported Git error, then rerun `borg assimilate`.\n' +
1568
- 'A seat was reserved and remains pending; rerunning after fixing the worktree issue resumes that seat.\n',
1587
+ 'A local drone reservation was created and remains pending; rerunning after fixing the worktree issue resumes that reservation.\n',
1569
1588
  );
1570
1589
  return 1;
1571
1590
  }
1572
1591
  deps.stderr(
1573
1592
  `spawned sibling worktree at ${candidate} on branch ${wtBranch} (${startRef}); ` +
1574
- `the original dir keeps its active seat — run \`borg reset-local-seat\` there if that binding is stale.\n`
1593
+ `the original dir keeps its active drone binding — run \`borg reset-local-seat\` there if that binding is stale.\n`
1575
1594
  );
1576
1595
  deps.chdir(candidate);
1577
1596
  deps.stderr(renderWorktreeSteeringNote(candidate, wtBranch, projectRoot));
@@ -241,6 +241,9 @@ export function buildDefaultAssimilateDeps(
241
241
  connectServer: async (apiUrl, enrollment) => {
242
242
  if (enrollment) {
243
243
  return enrollLocalBorgServer(apiUrl, enrollment.invitation, {
244
+ ...(enrollment.confirmReplacement === undefined
245
+ ? {}
246
+ : { confirmReplacement: enrollment.confirmReplacement }),
244
247
  clientName: osHostname(),
245
248
  });
246
249
  }
package/src/cli-help.ts CHANGED
@@ -74,9 +74,8 @@ export function clientSubcommandHelpText(
74
74
  export function setupNextStepsText(): string {
75
75
  return (
76
76
  `◼ Next steps:\n` +
77
- `1. Run \`borg server setup\` to prepare the local server.\n` +
78
- `2. Run \`borg server start\` and leave that terminal open.\n` +
79
- `3. In a second terminal, cd into your project and run \`borg assimilate\`.\n`
77
+ `1. Run \`borg server start\` and leave that terminal open.\n` +
78
+ `2. In a second terminal, cd into your project's Git repository and run \`borg assimilate\`.\n`
80
79
  );
81
80
  }
82
81
 
package/src/config.ts CHANGED
@@ -447,6 +447,7 @@ export async function activatePendingServerEnrollment(
447
447
  credential: string;
448
448
  clientId: string;
449
449
  serverCapabilities: ServerCapability[];
450
+ allowReplacement?: boolean;
450
451
  },
451
452
  ): Promise<void> {
452
453
  validateServerCredentialBinding(input.origin, input.trustIdentity);
@@ -471,6 +472,9 @@ export async function activatePendingServerEnrollment(
471
472
  } catch {
472
473
  throw new Error('pending Borg server enrollment does not match the verified response');
473
474
  }
475
+ if (!input.allowReplacement && await backend.get(serverCredentialAccount(input.origin, input.trustIdentity))) {
476
+ throw new Error('local Borg server enrollment already exists');
477
+ }
474
478
  // Already inside the single store lock — use the UNLOCKED write body so we do
475
479
  // not re-acquire (and self-deadlock on) CREDENTIALS_LOCK (CR3b).
476
480
  await writeServerCredentialRecord(backend, {
@@ -1,3 +1,4 @@
1
+ import { spawn } from 'node:child_process';
1
2
  import { readFileSync } from 'node:fs';
2
3
  import { dirname, join } from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
@@ -14,10 +15,12 @@ const CLIENT_PACKAGE = 'borgmcp';
14
15
  const SERVER_PACKAGE = 'borgmcp-server';
15
16
  const SHARED_PACKAGE = 'borgmcp-shared';
16
17
  const DEFAULT_CONNECT_COMMAND = 'borg assimilate --host <host>';
18
+ const CLIENT_ONBOARDING_ENV = 'BORG_CLIENT_ONBOARDING';
19
+ export const HINT_SUPPORTED_FROM = '0.8.1';
17
20
 
18
21
  export type FirstRunServerInstallResult =
19
- | { kind: 'present'; server: InstalledPackage }
20
- | { kind: 'installed'; server: InstalledPackage }
22
+ | { kind: 'present'; server: InstalledPackage; suppressClientNextSteps?: boolean }
23
+ | { kind: 'installed'; server: InstalledPackage; suppressClientNextSteps?: boolean }
21
24
  | { kind: 'declined' | 'non-interactive' | 'failed' };
22
25
 
23
26
  export interface FirstRunServerInstallDeps {
@@ -32,7 +35,8 @@ export interface FirstRunServerInstallDeps {
32
35
  version: string,
33
36
  options?: { ignoreScripts?: boolean },
34
37
  ): Promise<void>;
35
- confirm(message: string): Promise<'yes' | 'no' | 'eof' | 'interrupted'>;
38
+ runServerSetup(server: InstalledPackage, options?: { onboardingHint?: boolean }): Promise<number>;
39
+ confirm(message: string, defaultYes?: boolean): Promise<'yes' | 'no' | 'eof' | 'interrupted'>;
36
40
  isTTY(): boolean;
37
41
  stdout(text: string): void;
38
42
  stderr(text: string): void;
@@ -84,6 +88,18 @@ export function buildDefaultFirstRunServerInstallDeps(): FirstRunServerInstallDe
84
88
  publishedPackage: update.publishedPackage,
85
89
  publishedVersions: update.publishedVersions,
86
90
  installGlobal: update.installGlobal,
91
+ runServerSetup: (server, options = {}) => new Promise((resolve) => {
92
+ const childEnv = { ...process.env };
93
+ delete childEnv[CLIENT_ONBOARDING_ENV];
94
+ if (options.onboardingHint) childEnv[CLIENT_ONBOARDING_ENV] = '1';
95
+ const child = spawn(server.binPath, ['setup'], {
96
+ stdio: 'inherit',
97
+ shell: false,
98
+ env: childEnv,
99
+ });
100
+ child.once('error', () => resolve(1));
101
+ child.once('exit', (code) => resolve(code ?? 1));
102
+ }),
87
103
  confirm: update.confirm,
88
104
  isTTY: update.isTTY,
89
105
  stdout: update.stdout,
@@ -100,6 +116,7 @@ export function buildDefaultFirstRunServerInstallDeps(): FirstRunServerInstallDe
100
116
  export async function offerFirstRunServerInstall(
101
117
  deps: FirstRunServerInstallDeps = buildDefaultFirstRunServerInstallDeps(),
102
118
  connectCommand = DEFAULT_CONNECT_COMMAND,
119
+ options: { initializeServer?: boolean } = {},
103
120
  ): Promise<FirstRunServerInstallResult> {
104
121
  let installed: InstalledPackage | null;
105
122
  try {
@@ -112,7 +129,15 @@ export async function offerFirstRunServerInstall(
112
129
  );
113
130
  return { kind: 'failed' };
114
131
  }
115
- if (installed) return { kind: 'present', server: installed };
132
+ if (installed) {
133
+ return initializeServerIfRequested(
134
+ installed,
135
+ deps,
136
+ options.initializeServer,
137
+ 'present',
138
+ shouldSuppressClientNextSteps(installed.version),
139
+ );
140
+ }
116
141
 
117
142
  if (!deps.isTTY()) {
118
143
  deps.stderr(
@@ -140,9 +165,11 @@ export async function offerFirstRunServerInstall(
140
165
  const command = exactInstallCommand(target.version);
141
166
  const decision = await deps.confirm(
142
167
  `No local ${SERVER_PACKAGE} installation was found. ` +
143
- `Install ${SERVER_PACKAGE}@${target.version}, which uses ${SHARED_PACKAGE}@${target.sharedVersion}, now?\n` +
168
+ `Install ${SERVER_PACKAGE}@${target.version}, the local coordination server, ` +
169
+ `which uses ${SHARED_PACKAGE}@${target.sharedVersion}, now?\n` +
144
170
  `Command: ${command}\n` +
145
- `Continue? [y/N] `,
171
+ `Install it now? [Y/n] `,
172
+ true,
146
173
  );
147
174
  if (decision !== 'yes') {
148
175
  const reason = decision === 'interrupted'
@@ -172,7 +199,43 @@ export async function offerFirstRunServerInstall(
172
199
 
173
200
  deps.stdout(
174
201
  `Installed ${SERVER_PACKAGE}@${target.version} with ${SHARED_PACKAGE}@${target.sharedVersion}.\n` +
175
- `Next, run \`borg server setup\`, then run \`borg server start\` in a terminal you keep open.\n`,
202
+ (options.initializeServer
203
+ ? `The local server package is ready.\n`
204
+ : `Next, run \`borg server setup\`, then run \`borg server start\` in a terminal you keep open.\n`),
205
+ );
206
+ return initializeServerIfRequested(installed, deps, options.initializeServer, 'installed');
207
+ }
208
+
209
+ async function initializeServerIfRequested(
210
+ server: InstalledPackage,
211
+ deps: FirstRunServerInstallDeps,
212
+ requested: boolean | undefined,
213
+ kind: 'present' | 'installed',
214
+ suppressClientNextSteps = false,
215
+ ): Promise<FirstRunServerInstallResult> {
216
+ if (!requested) return { kind, server, ...(suppressClientNextSteps ? { suppressClientNextSteps: true } : {}) };
217
+ try {
218
+ const exitCode = await deps.runServerSetup(server, { onboardingHint: !suppressClientNextSteps });
219
+ if (exitCode === 0) {
220
+ return { kind, server, ...(suppressClientNextSteps ? { suppressClientNextSteps: true } : {}) };
221
+ }
222
+ } catch (error) {
223
+ deps.stderr(`Local server setup could not be completed. ${error instanceof Error ? error.message : String(error)}\n`);
224
+ }
225
+ deps.stderr(
226
+ 'Run `borg server setup` to retry local server initialization, then run `borg server start`.\n',
176
227
  );
177
- return { kind: 'installed', server: installed };
228
+ return { kind: 'failed' };
229
+ }
230
+
231
+ function shouldSuppressClientNextSteps(version: unknown): boolean {
232
+ if (!isExactSemver(version)) return false;
233
+ const installedParts = version.split('.').slice(0, 3).map(Number);
234
+ const supportedParts = HINT_SUPPORTED_FROM.split('.').map(Number);
235
+ for (let index = 0; index < 3; index += 1) {
236
+ if (installedParts[index] !== supportedParts[index]) {
237
+ return installedParts[index] < supportedParts[index];
238
+ }
239
+ }
240
+ return false;
178
241
  }
@@ -1041,7 +1041,7 @@ export async function regen(
1041
1041
  opts: {
1042
1042
  since?: string;
1043
1043
  /** Advisory self-report from the running agent; never model-routing config. */
1044
- reportedModel?: string;
1044
+ reportedModel?: string | null;
1045
1045
  /** Positively identified running Agent CLI; null means explicitly unknown. */
1046
1046
  agentKind?: AgentKind | null;
1047
1047
  /** Current cwd-derived identity; refreshed each regen to avoid stale routing data. */
@@ -1077,7 +1077,7 @@ export async function regen(
1077
1077
  ) {
1078
1078
  const patch = buildRuntimeMetadataPatch({
1079
1079
  agentKind: opts.agentKind ?? null,
1080
- reportedModel: opts.reportedModel,
1080
+ reportedModel: opts.reportedModel ?? null,
1081
1081
  workingRepo: opts.workingRepo,
1082
1082
  });
1083
1083
  try {
@@ -53,13 +53,14 @@ export function buildRuntimeMetadataReport(input: {
53
53
 
54
54
  export function buildRuntimeMetadataPatch(input: {
55
55
  agentKind: AgentKind | null;
56
- reportedModel?: string;
56
+ reportedModel?: string | null;
57
57
  workingRepo?: WorkingRepo;
58
58
  }): DroneRuntimeMetadataPatch {
59
59
  const patch: DroneRuntimeMetadataPatch = { agent_kind: input.agentKind };
60
60
  if (input.reportedModel !== undefined) {
61
- const model = safeReportedModel(input.reportedModel);
62
- if (model !== null) patch.reported_model = model;
61
+ patch.reported_model = input.reportedModel === null
62
+ ? null
63
+ : validateReportedModel(input.reportedModel);
63
64
  }
64
65
  const repository = reportableRepository(input.workingRepo);
65
66
  if (repository) Object.assign(patch, repository);
@@ -2,6 +2,7 @@ export type BorgServerErrorCode =
2
2
  | 'NOT_ENROLLED'
3
3
  | 'CREDENTIAL_REJECTED'
4
4
  | 'INVITATION_REJECTED'
5
+ | 'LOCAL_CREDENTIAL_EXISTS'
5
6
  | 'CREATE_CUBE_DENIED'
6
7
  | 'ATTACH_CONFLICT'
7
8
  | 'SESSION_REJECTED'
@@ -452,6 +452,8 @@ export async function enrollBorgServer(
452
452
  prepareEnrollment?: typeof getOrCreatePendingServerEnrollment;
453
453
  activateEnrollment?: typeof activatePendingServerEnrollment;
454
454
  clearPendingEnrollment?: typeof clearPendingServerEnrollment;
455
+ loadCredentialRecord?: typeof getServerCredentialRecord;
456
+ confirmReplacement?: () => Promise<boolean>;
455
457
  clientName?: string;
456
458
  } = {},
457
459
  ): Promise<NewServerEnrollment> {
@@ -461,6 +463,24 @@ export async function enrollBorgServer(
461
463
  // persisted and before any invitation/secret-bearing request is sent.
462
464
  const protocol = await preflightBorgServerTag(origin, fetchImpl);
463
465
 
466
+ const existingCredential = await (deps.loadCredentialRecord ?? getServerCredentialRecord)(
467
+ origin,
468
+ trustIdentity,
469
+ );
470
+ let replacementConfirmed = false;
471
+ if (existingCredential) {
472
+ const confirmed = deps.confirmReplacement !== undefined
473
+ ? await deps.confirmReplacement()
474
+ : false;
475
+ if (!confirmed) {
476
+ throw new BorgServerError(
477
+ 'LOCAL_CREDENTIAL_EXISTS',
478
+ 'a local enrollment already exists for this server and was not replaced',
479
+ );
480
+ }
481
+ replacementConfirmed = true;
482
+ }
483
+
464
484
  const pending = await (deps.prepareEnrollment ?? getOrCreatePendingServerEnrollment)({
465
485
  origin,
466
486
  trustIdentity,
@@ -528,6 +548,7 @@ export async function enrollBorgServer(
528
548
  credential: pending.credential,
529
549
  clientId: decoded.payload.client_id,
530
550
  serverCapabilities: decoded.payload.server_capabilities,
551
+ ...(replacementConfirmed ? { allowReplacement: true } : {}),
531
552
  });
532
553
  return {
533
554
  token: pending.credential,
@@ -1011,6 +1032,8 @@ export async function enrollLocalBorgServer(
1011
1032
  prepareEnrollment?: typeof getOrCreatePendingServerEnrollment;
1012
1033
  activateEnrollment?: typeof activatePendingServerEnrollment;
1013
1034
  clearPendingEnrollment?: typeof clearPendingServerEnrollment;
1035
+ loadCredentialRecord?: typeof getServerCredentialRecord;
1036
+ confirmReplacement?: () => Promise<boolean>;
1014
1037
  clientName?: string;
1015
1038
  } = {},
1016
1039
  ): Promise<NewServerEnrollment> {
@@ -1022,6 +1045,12 @@ export async function enrollLocalBorgServer(
1022
1045
  ...(deps.clearPendingEnrollment === undefined
1023
1046
  ? {}
1024
1047
  : { clearPendingEnrollment: deps.clearPendingEnrollment }),
1048
+ ...(deps.loadCredentialRecord === undefined
1049
+ ? {}
1050
+ : { loadCredentialRecord: deps.loadCredentialRecord }),
1051
+ ...(deps.confirmReplacement === undefined
1052
+ ? {}
1053
+ : { confirmReplacement: deps.confirmReplacement }),
1025
1054
  ...(deps.clientName === undefined ? {} : { clientName: deps.clientName }),
1026
1055
  });
1027
1056
  }
package/src/setup.ts CHANGED
@@ -73,11 +73,12 @@ async function main() {
73
73
  // configuration. Decline/non-interactive/failure paths therefore leave no
74
74
  // partial setup state behind.
75
75
  console.log(chalk.blue('◼ Local Server'));
76
- const serverInstall = await offerFirstRunServerInstall();
76
+ const serverInstall = await offerFirstRunServerInstall(undefined, undefined, { initializeServer: true });
77
77
  if (serverInstall.kind !== 'present' && serverInstall.kind !== 'installed') {
78
78
  process.exit(serverInstall.kind === 'declined' ? 0 : 1);
79
79
  }
80
80
  console.log('');
81
+ console.log('◼ Local server initialized');
81
82
 
82
83
  // Step 1: Configure every detected agent CLI
83
84
  console.log(chalk.blue('◼ Agent CLI Integration'));
@@ -185,7 +186,9 @@ async function main() {
185
186
  // Success message
186
187
  console.log(chalk.green.bold('\nSetup complete!\n'));
187
188
  console.log(chalk.yellow('🔄 Restart Claude Code / Codex / OpenCode (or open a new session) for the changes to take effect.\n'));
188
- console.log(chalk.gray(setupNextStepsText()));
189
+ if (!serverInstall.suppressClientNextSteps) {
190
+ console.log(chalk.gray(setupNextStepsText()));
191
+ }
189
192
  }
190
193
 
191
194
  // Run wizard
@@ -30,7 +30,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
30
30
  'Optional `since` (entry-id UUID or ISO-8601 timestamp) trims the recent-log section ' +
31
31
  'to entries strictly after the anchor — pass your last-seen entry id to skip ' +
32
32
  'already-processed history on each refresh. If you know the current session model, pass ' +
33
- 'optional `model` to self-report it as advisory metadata; unrecognized model strings are accepted.',
33
+ 'optional `model` to self-report its printable identifier as advisory metadata; model names are not allowlisted.',
34
34
  inputSchema: {
35
35
  type: 'object',
36
36
  properties: {
@@ -47,9 +47,11 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
47
47
  },
48
48
  model: {
49
49
  type: 'string',
50
- maxLength: 256,
50
+ minLength: 1,
51
+ maxLength: 160,
52
+ pattern: '^[A-Za-z0-9][A-Za-z0-9._/@:+-]*$',
51
53
  description:
52
- 'Optional advisory self-report of the model running this agent session. Use the model identifier you know from session context; the server does not infer or validate it against an allowlist.',
54
+ 'Optional advisory self-report of the model running this agent session. Use a printable model identifier of 1-160 ASCII characters; model names are not allowlisted.',
53
55
  },
54
56
  },
55
57
  required: [],
package/src/update-cmd.ts CHANGED
@@ -1128,7 +1128,13 @@ async function defaultPublishedVersions(
1128
1128
  }
1129
1129
  }
1130
1130
 
1131
- async function defaultConfirm(message: string): Promise<'yes' | 'no' | 'eof' | 'interrupted'> {
1131
+ export function parseConfirmationAnswer(answer: string, defaultYes = false): 'yes' | 'no' {
1132
+ const normalized = answer.trim().toLowerCase();
1133
+ if (normalized === '') return defaultYes ? 'yes' : 'no';
1134
+ return normalized === 'y' || normalized === 'yes' ? 'yes' : 'no';
1135
+ }
1136
+
1137
+ async function defaultConfirm(message: string, defaultYes = false): Promise<'yes' | 'no' | 'eof' | 'interrupted'> {
1132
1138
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1133
1139
  let interrupted = false;
1134
1140
  rl.once('SIGINT', () => {
@@ -1136,8 +1142,7 @@ async function defaultConfirm(message: string): Promise<'yes' | 'no' | 'eof' | '
1136
1142
  rl.close();
1137
1143
  });
1138
1144
  try {
1139
- const answer = (await rl.question(message)).trim().toLowerCase();
1140
- return answer === 'y' || answer === 'yes' ? 'yes' : 'no';
1145
+ return parseConfirmationAnswer(await rl.question(message), defaultYes);
1141
1146
  } catch (error) {
1142
1147
  if (interrupted) return 'interrupted';
1143
1148
  if ((error as NodeJS.ErrnoException).code === 'ERR_USE_AFTER_CLOSE') return 'eof';