borgmcp 2.8.0 → 2.10.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 (118) hide show
  1. package/dist/assimilate-cmd.d.ts +14 -0
  2. package/dist/assimilate-cmd.d.ts.map +1 -1
  3. package/dist/assimilate-cmd.js +187 -14
  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 +29 -1
  7. package/dist/assimilate-deps.js.map +1 -1
  8. package/dist/assimilate-welcome.d.ts.map +1 -1
  9. package/dist/assimilate-welcome.js +1 -0
  10. package/dist/assimilate-welcome.js.map +1 -1
  11. package/dist/claude.d.ts.map +1 -1
  12. package/dist/claude.js +27 -0
  13. package/dist/claude.js.map +1 -1
  14. package/dist/cli-help.d.ts +1 -0
  15. package/dist/cli-help.d.ts.map +1 -1
  16. package/dist/cli-help.js +13 -3
  17. package/dist/cli-help.js.map +1 -1
  18. package/dist/config.d.ts +12 -1
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +170 -11
  21. package/dist/config.js.map +1 -1
  22. package/dist/first-run-server.d.ts +10 -2
  23. package/dist/first-run-server.d.ts.map +1 -1
  24. package/dist/first-run-server.js +54 -7
  25. package/dist/first-run-server.js.map +1 -1
  26. package/dist/invitation-artifact.d.ts +33 -0
  27. package/dist/invitation-artifact.d.ts.map +1 -0
  28. package/dist/invitation-artifact.js +89 -0
  29. package/dist/invitation-artifact.js.map +1 -0
  30. package/dist/launch-gate.d.ts +5 -4
  31. package/dist/launch-gate.d.ts.map +1 -1
  32. package/dist/launch-gate.js +7 -8
  33. package/dist/launch-gate.js.map +1 -1
  34. package/dist/parse-assimilate-args.d.ts.map +1 -1
  35. package/dist/parse-assimilate-args.js +9 -3
  36. package/dist/parse-assimilate-args.js.map +1 -1
  37. package/dist/recover-enrollment-cmd.d.ts +17 -0
  38. package/dist/recover-enrollment-cmd.d.ts.map +1 -0
  39. package/dist/recover-enrollment-cmd.js +62 -0
  40. package/dist/recover-enrollment-cmd.js.map +1 -0
  41. package/dist/regen-format.d.ts +9 -0
  42. package/dist/regen-format.d.ts.map +1 -1
  43. package/dist/regen-format.js +15 -0
  44. package/dist/regen-format.js.map +1 -1
  45. package/dist/regen.js +27 -3
  46. package/dist/regen.js.map +1 -1
  47. package/dist/remote-client.d.ts +1 -1
  48. package/dist/remote-client.d.ts.map +1 -1
  49. package/dist/remote-client.js +1 -1
  50. package/dist/remote-client.js.map +1 -1
  51. package/dist/runtime-metadata.d.ts +1 -1
  52. package/dist/runtime-metadata.d.ts.map +1 -1
  53. package/dist/runtime-metadata.js +3 -3
  54. package/dist/runtime-metadata.js.map +1 -1
  55. package/dist/seat-store.d.ts.map +1 -1
  56. package/dist/seat-store.js +7 -0
  57. package/dist/seat-store.js.map +1 -1
  58. package/dist/server-errors.d.ts +1 -1
  59. package/dist/server-errors.d.ts.map +1 -1
  60. package/dist/server-errors.js.map +1 -1
  61. package/dist/server-facade.d.ts +1 -1
  62. package/dist/server-facade.d.ts.map +1 -1
  63. package/dist/server-facade.js +2 -2
  64. package/dist/server-facade.js.map +1 -1
  65. package/dist/server-handshake.d.ts +23 -3
  66. package/dist/server-handshake.d.ts.map +1 -1
  67. package/dist/server-handshake.js +102 -7
  68. package/dist/server-handshake.js.map +1 -1
  69. package/dist/server-trust.d.ts +8 -0
  70. package/dist/server-trust.d.ts.map +1 -1
  71. package/dist/server-trust.js +165 -5
  72. package/dist/server-trust.js.map +1 -1
  73. package/dist/setup.js +5 -2
  74. package/dist/setup.js.map +1 -1
  75. package/dist/token-store.d.ts +3 -0
  76. package/dist/token-store.d.ts.map +1 -1
  77. package/dist/token-store.js +2 -0
  78. package/dist/token-store.js.map +1 -1
  79. package/dist/tool-manifest.d.ts.map +1 -1
  80. package/dist/tool-manifest.js +5 -3
  81. package/dist/tool-manifest.js.map +1 -1
  82. package/dist/unknown-subcommand.d.ts +1 -1
  83. package/dist/unknown-subcommand.d.ts.map +1 -1
  84. package/dist/unknown-subcommand.js +1 -0
  85. package/dist/unknown-subcommand.js.map +1 -1
  86. package/dist/update-cmd.d.ts +1 -0
  87. package/dist/update-cmd.d.ts.map +1 -1
  88. package/dist/update-cmd.js +8 -3
  89. package/dist/update-cmd.js.map +1 -1
  90. package/docs/EXTRACTION_PROVENANCE.md +11 -7
  91. package/docs/LOCAL_SERVER.md +12 -3
  92. package/docs/RELEASING.md +17 -6
  93. package/package.json +2 -2
  94. package/src/assimilate-cmd.ts +234 -20
  95. package/src/assimilate-deps.ts +29 -0
  96. package/src/assimilate-welcome.ts +1 -0
  97. package/src/claude.ts +22 -0
  98. package/src/cli-help.ts +16 -3
  99. package/src/config.ts +167 -8
  100. package/src/first-run-server.ts +71 -8
  101. package/src/invitation-artifact.ts +116 -0
  102. package/src/launch-gate.ts +7 -8
  103. package/src/parse-assimilate-args.ts +11 -4
  104. package/src/recover-enrollment-cmd.ts +63 -0
  105. package/src/regen-format.ts +25 -0
  106. package/src/regen.ts +30 -2
  107. package/src/remote-client.ts +2 -2
  108. package/src/runtime-metadata.ts +4 -3
  109. package/src/seat-store.ts +6 -0
  110. package/src/server-errors.ts +1 -0
  111. package/src/server-facade.ts +2 -2
  112. package/src/server-handshake.ts +140 -4
  113. package/src/server-trust.ts +180 -5
  114. package/src/setup.ts +5 -2
  115. package/src/token-store.ts +5 -0
  116. package/src/tool-manifest.ts +5 -3
  117. package/src/unknown-subcommand.ts +1 -0
  118. package/src/update-cmd.ts +8 -3
@@ -56,6 +56,17 @@ import {
56
56
  RepositoryAssociationOutcomeUnknownError,
57
57
  RepositoryAssociationResolutionError,
58
58
  } from './server-errors.js';
59
+ import {
60
+ decodeAndVerifyInvitationArtifact,
61
+ InvitationArtifactCompatibilityError,
62
+ InvitationArtifactEndpointMismatchError,
63
+ InvitationArtifactFormatError,
64
+ InvitationArtifactLegacyError,
65
+ InvitationArtifactRecoveryError,
66
+ InvitationArtifactStorageError,
67
+ InvitationArtifactTransportError,
68
+ InvitationArtifactTrustError,
69
+ } from './invitation-artifact.js';
59
70
  import type { SeatStatus } from './seat-probe.js';
60
71
  import type { ServerSessionOperation } from './config.js';
61
72
  import type { ExpectedBinding, FinalizeServerSeatOutcome, PersistedLocalSeat } from './cubes.js';
@@ -67,6 +78,7 @@ import type {
67
78
  AssociateRepositoryCubeResponse,
68
79
  CreateCubeRepository,
69
80
  CubeTemplate,
81
+ InvitationArtifact,
70
82
  } from 'borgmcp-shared/protocol';
71
83
  import {
72
84
  initializeRepositoryCube,
@@ -277,7 +289,11 @@ export interface AssimilateDeps {
277
289
  detectLocalServer: () => Promise<string | null>;
278
290
  connectServer: (
279
291
  apiUrl: string,
280
- enrollment?: { invitation: string },
292
+ enrollment?: {
293
+ invitation: string;
294
+ artifact?: import('borgmcp-shared/protocol').InvitationArtifact;
295
+ confirmReplacement?: () => Promise<boolean>;
296
+ },
281
297
  ) => Promise<{
282
298
  token: string;
283
299
  trustIdentity: string;
@@ -291,6 +307,17 @@ export interface AssimilateDeps {
291
307
  trustIdentity: string;
292
308
  serverCapabilities?: readonly string[];
293
309
  } | null>;
310
+ /** Read-only pending-artifact peek used to validate an explicit host before
311
+ * the normal resume path performs trust/credential work. */
312
+ peekPendingServerEnrollment?: () => Promise<{ origin: string; invitation: string } | null>;
313
+ resumePendingServerEnrollment?: (
314
+ onPending?: () => void,
315
+ ) => Promise<{
316
+ token: string;
317
+ apiUrl?: string;
318
+ trustIdentity: string;
319
+ serverCapabilities?: readonly string[];
320
+ } | null>;
294
321
 
295
322
  resolveRepositoryCube: (
296
323
  apiUrl: string,
@@ -361,6 +388,11 @@ function affirmative(answer: string): boolean {
361
388
  return normalized === '' || normalized === 'y' || normalized === 'yes';
362
389
  }
363
390
 
391
+ function strictAffirmative(answer: string): boolean {
392
+ const normalized = answer.trim().toLowerCase();
393
+ return normalized === 'y' || normalized === 'yes';
394
+ }
395
+
364
396
  async function selectAssimilationAuthority(
365
397
  flags: AssimilateFlags,
366
398
  deps: AssimilateDeps,
@@ -377,6 +409,11 @@ async function selectAssimilationAuthority(
377
409
 
378
410
  // Only a local self-hosted server authority exists. Non-TTY and --yes must
379
411
  // NOT infer an authority — fail closed with actionable guidance.
412
+ if (flags.enroll && deps.isTTY()) {
413
+ // The v2 enrollment artifact is itself the authority selector. Use the
414
+ // local origin only as a placeholder until the hidden artifact is decoded.
415
+ return { kind: 'server', apiUrl: DEFAULT_LOCAL_SERVER_ORIGIN };
416
+ }
380
417
  if (!deps.isTTY() || flags.yes) {
381
418
  if (deps.defaultAuthority) return deps.defaultAuthority;
382
419
  const command = mode === 'cube-init'
@@ -475,6 +512,14 @@ function reportServerFailure(
475
512
  );
476
513
  return 1;
477
514
  }
515
+ if (error instanceof BorgServerError && error.code === 'LOCAL_CREDENTIAL_EXISTS') {
516
+ deps.stderr(
517
+ `A local enrollment for ${apiUrl} already exists and was not replaced. ` +
518
+ `Re-run ${localAssimilateCommand(apiUrl, true, mode)} and explicitly confirm replacement ` +
519
+ 'only if the first enrolled client should be abandoned.\n',
520
+ );
521
+ return 1;
522
+ }
478
523
  if (error instanceof LegacySessionCredentialCollisionError) {
479
524
  const recovery = mode === 'cube-init'
480
525
  ? localAssimilateCommand(error.origin, true, mode)
@@ -508,6 +553,38 @@ function reportServerFailure(
508
553
  );
509
554
  return 1;
510
555
  }
556
+ if (error instanceof InvitationArtifactCompatibilityError) {
557
+ deps.stderr(`${error.message}\n`);
558
+ return 1;
559
+ }
560
+ if (error instanceof InvitationArtifactLegacyError) {
561
+ deps.stderr(`${error.message}\n`);
562
+ return 1;
563
+ }
564
+ if (error instanceof InvitationArtifactFormatError) {
565
+ deps.stderr(`${error.message}\n`);
566
+ return 1;
567
+ }
568
+ if (error instanceof InvitationArtifactEndpointMismatchError) {
569
+ deps.stderr(`${error.message}\n`);
570
+ return 1;
571
+ }
572
+ if (error instanceof InvitationArtifactTransportError) {
573
+ deps.stderr(`${error.message}\n`);
574
+ return 1;
575
+ }
576
+ if (error instanceof InvitationArtifactStorageError) {
577
+ deps.stderr(`${error.message}\n`);
578
+ return 1;
579
+ }
580
+ if (error instanceof InvitationArtifactRecoveryError) {
581
+ deps.stderr(`${error.message}\n`);
582
+ return 1;
583
+ }
584
+ if (error instanceof InvitationArtifactTrustError) {
585
+ deps.stderr(`${error.message}\n`);
586
+ return 1;
587
+ }
511
588
  if (/HTTP 40[13]|auth(?:entication|orization)|credential.*(?:invalid|rejected)/i.test(message)) {
512
589
  deps.stderr(
513
590
  `The saved enrollment for ${apiUrl} was rejected. Re-run ` +
@@ -649,7 +726,117 @@ export async function runAssimilate(
649
726
  return 1;
650
727
  }
651
728
 
652
- if (args.flags.server === undefined && deps.defaultAuthority === undefined) {
729
+ const artifactOnlyEnrollment = args.flags.enroll === true &&
730
+ args.flags.server === undefined && deps.defaultAuthority === undefined && deps.isTTY();
731
+ let preResumeAttempted = false;
732
+ let preResumedEnrollment: {
733
+ token: string;
734
+ trustIdentity: string;
735
+ serverCapabilities?: readonly string[];
736
+ } | null = null;
737
+
738
+ let prefetchedInvitation: string | undefined;
739
+ let prefetchedArtifact: InvitationArtifact | undefined;
740
+
741
+ // An explicit --host plus a new invitation must be rejected on the pure
742
+ // input path. Decode and compare the operator-presented artifact before any
743
+ // pending-enrollment lookup: the lookup enumerates the credential backend,
744
+ // so it must not precede this contradiction check. A matching artifact may
745
+ // then take the published pending-resume path until client#267 lands.
746
+ if (args.flags.enroll && args.flags.server !== undefined && deps.isTTY()) {
747
+ let preResumeOrigin: string;
748
+ try {
749
+ preResumeOrigin = normalizeServerEndpoint(args.flags.server);
750
+ } catch (error) {
751
+ deps.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
752
+ return 1;
753
+ }
754
+
755
+ prefetchedInvitation = await deps.promptSecret('Enrollment invitation (single-use; hidden input):');
756
+ if (!prefetchedInvitation) {
757
+ deps.stderr('No enrollment invitation was entered. Ask the server operator for one, then retry.\n');
758
+ return 1;
759
+ }
760
+ try {
761
+ prefetchedArtifact = decodeAndVerifyInvitationArtifact(prefetchedInvitation);
762
+ if (prefetchedArtifact.endpoint !== preResumeOrigin) {
763
+ throw new InvitationArtifactEndpointMismatchError(
764
+ preResumeOrigin,
765
+ prefetchedArtifact.endpoint,
766
+ );
767
+ }
768
+ } catch (error) {
769
+ deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
770
+ prefetchedInvitation = undefined;
771
+ prefetchedArtifact = undefined;
772
+ return 1;
773
+ }
774
+
775
+ let pendingForHost = false;
776
+ if (deps.peekPendingServerEnrollment !== undefined) {
777
+ let pending: { origin: string; invitation: string } | null = null;
778
+ try {
779
+ pending = await deps.peekPendingServerEnrollment();
780
+ } catch {
781
+ pending = null;
782
+ }
783
+ if (pending?.origin === preResumeOrigin) {
784
+ try {
785
+ const pendingArtifact = decodeAndVerifyInvitationArtifact(pending.invitation);
786
+ if (pendingArtifact.endpoint !== preResumeOrigin) {
787
+ throw new InvitationArtifactEndpointMismatchError(
788
+ preResumeOrigin,
789
+ pendingArtifact.endpoint,
790
+ );
791
+ }
792
+ pendingForHost = true;
793
+ } catch (error) {
794
+ deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
795
+ return 1;
796
+ }
797
+ }
798
+ }
799
+
800
+ if (pendingForHost) {
801
+ try {
802
+ preResumeAttempted = true;
803
+ preResumedEnrollment = await deps.resumeServerEnrollment(preResumeOrigin, () => {
804
+ deps.stderr(
805
+ `Resuming the pending enrollment for \`${preResumeOrigin}\`; ` +
806
+ 'do not enter another invitation unless the server certificate was reissued; ' +
807
+ 'if it was, request a current invitation and rerun this command.\n',
808
+ );
809
+ });
810
+ } catch {
811
+ preResumeAttempted = false;
812
+ }
813
+ }
814
+ }
815
+ if (artifactOnlyEnrollment ||
816
+ preResumeAttempted && preResumedEnrollment === null) {
817
+ if (artifactOnlyEnrollment && deps.resumePendingServerEnrollment) {
818
+ preResumedEnrollment = await deps.resumePendingServerEnrollment(() => {
819
+ deps.stderr('Resuming the pending enrollment; no new invitation is required.\n');
820
+ });
821
+ }
822
+ if (artifactOnlyEnrollment && preResumedEnrollment) {
823
+ // The exact pending tuple was already redeemed or is being resumed.
824
+ } else {
825
+ prefetchedInvitation = await deps.promptSecret('Enrollment invitation (single-use; hidden input):');
826
+ if (!prefetchedInvitation) {
827
+ deps.stderr('No enrollment invitation was entered. Ask the server operator for one, then retry.\n');
828
+ return 1;
829
+ }
830
+ try {
831
+ prefetchedArtifact = decodeAndVerifyInvitationArtifact(prefetchedInvitation);
832
+ } catch (error) {
833
+ deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
834
+ prefetchedInvitation = undefined;
835
+ return 1;
836
+ }
837
+ }
838
+ }
839
+ if (!artifactOnlyEnrollment && args.flags.server === undefined && deps.defaultAuthority === undefined) {
653
840
  const connectCommand = mode === 'cube-init'
654
841
  ? 'borg server cube init --host <host>'
655
842
  : 'borg assimilate --host <host>';
@@ -686,8 +873,9 @@ export async function runAssimilate(
686
873
  }
687
874
 
688
875
  // ----- Step 1: Select and authenticate the local server -----
689
- const authority = await selectAssimilationAuthority(args.flags, deps, mode);
690
- if (!authority) return 1;
876
+ const selectedAuthority = await selectAssimilationAuthority(args.flags, deps, mode);
877
+ if (!selectedAuthority) return 1;
878
+ let authority: AssimilationAuthority = selectedAuthority;
691
879
  if (localSeatReadError !== undefined) {
692
880
  return reportServerFailure(deps, authority.apiUrl, localSeatReadError, false, mode);
693
881
  }
@@ -710,27 +898,48 @@ export async function runAssimilate(
710
898
  );
711
899
  return 1;
712
900
  }
713
- const resumed = await deps.resumeServerEnrollment(authority.apiUrl, () => {
714
- deps.stderr(
715
- `Resuming the pending enrollment for \`${authority.apiUrl}\`; ` +
716
- 'do not enter another invitation.\n',
717
- );
718
- });
901
+ let resumed = preResumedEnrollment;
902
+ if (!resumed && prefetchedArtifact === undefined && !preResumeAttempted && !artifactOnlyEnrollment) {
903
+ resumed = await deps.resumeServerEnrollment(authority.apiUrl, () => {
904
+ deps.stderr(
905
+ `Resuming the pending enrollment for \`${authority.apiUrl}\`; ` +
906
+ 'do not enter another invitation unless the server certificate was reissued; ' +
907
+ 'if it was, request a current invitation and rerun this command.\n',
908
+ );
909
+ });
910
+ }
719
911
  if (resumed) {
912
+ if ('apiUrl' in resumed && typeof resumed.apiUrl === 'string') {
913
+ authority = { kind: 'server', apiUrl: resumed.apiUrl };
914
+ }
720
915
  serverAuth = resumed;
721
916
  } else {
722
- let invitation = await deps.promptSecret(
723
- `Enrollment invitation for \`${authority.apiUrl}\` (single-use; hidden input):`,
917
+ let invitation = prefetchedInvitation ?? await deps.promptSecret(
918
+ artifactOnlyEnrollment
919
+ ? 'Enrollment invitation (single-use; hidden input):'
920
+ : `Enrollment invitation for \`${authority.apiUrl}\` (single-use; hidden input):`,
724
921
  );
725
922
  if (!invitation) {
726
- deps.stderr(
727
- `No enrollment invitation was entered for ${authority.apiUrl}. ` +
728
- `Ask the server operator for one, then rerun ${localAssimilateCommand(authority.apiUrl, true, mode)}.\n`,
729
- );
923
+ deps.stderr(artifactOnlyEnrollment
924
+ ? 'No enrollment invitation was entered. Ask the server operator for one, then rerun `borg assimilate --enroll`.\n'
925
+ : `No enrollment invitation was entered for ${authority.apiUrl}. ` +
926
+ `Ask the server operator for one, then rerun ${localAssimilateCommand(authority.apiUrl, true, mode)}.\n`);
730
927
  return 1;
731
928
  }
732
929
  try {
733
- serverAuth = await deps.connectServer(authority.apiUrl, { invitation });
930
+ const artifact = prefetchedArtifact ?? decodeAndVerifyInvitationArtifact(invitation);
931
+ if (args.flags.server !== undefined && authority.apiUrl !== artifact.endpoint) {
932
+ throw new InvitationArtifactEndpointMismatchError(authority.apiUrl, artifact.endpoint);
933
+ }
934
+ authority = { kind: 'server', apiUrl: artifact.endpoint };
935
+ serverAuth = await deps.connectServer(authority.apiUrl, {
936
+ invitation,
937
+ artifact,
938
+ confirmReplacement: async () => strictAffirmative(await deps.prompt(
939
+ `A local enrollment for ${authority.apiUrl} already exists. Replacing it will orphan ` +
940
+ 'the first enrolled client. Replace it? [y/N]: ',
941
+ )),
942
+ });
734
943
  } finally {
735
944
  // Strings cannot be zeroized in JavaScript, but drop this command's
736
945
  // reference immediately after the exchange instead of retaining the
@@ -758,6 +967,11 @@ export async function runAssimilate(
758
967
  serverTrustIdentity: serverAuth.trustIdentity,
759
968
  serverCapabilities: serverAuth.serverCapabilities ?? [],
760
969
  };
970
+ if (args.flags.enroll) {
971
+ deps.stderr(
972
+ `This machine (${deps.getHostname()}) is enrolled with Borg server \`${authority.apiUrl}\`.\n`,
973
+ );
974
+ }
761
975
  } catch (error) {
762
976
  return reportServerFailure(deps, authority.apiUrl, error, args.flags.enroll === true, mode);
763
977
  }
@@ -1492,7 +1706,7 @@ export async function runAssimilate(
1492
1706
  deps.stderr(
1493
1707
  'Borg could not enumerate this repository’s existing worktrees, so it did not risk creating a colliding sibling.\n' +
1494
1708
  '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',
1709
+ 'A local drone reservation was created and remains pending; rerunning after fixing the worktree issue resumes that reservation.\n',
1496
1710
  );
1497
1711
  return 1;
1498
1712
  }
@@ -1565,13 +1779,13 @@ export async function runAssimilate(
1565
1779
  ? `Git left branch ${residualBranch} without a registered worktree; Borg preserved it.\n`
1566
1780
  : '') +
1567
1781
  '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',
1782
+ 'A local drone reservation was created and remains pending; rerunning after fixing the worktree issue resumes that reservation.\n',
1569
1783
  );
1570
1784
  return 1;
1571
1785
  }
1572
1786
  deps.stderr(
1573
1787
  `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`
1788
+ `the original dir keeps its active drone binding — run \`borg reset-local-seat\` there if that binding is stale.\n`
1575
1789
  );
1576
1790
  deps.chdir(candidate);
1577
1791
  deps.stderr(renderWorktreeSteeringNote(candidate, wtBranch, projectRoot));
@@ -34,6 +34,7 @@ import {
34
34
  connectLocalBorgServer,
35
35
  createLocalBorgServerCube,
36
36
  enrollLocalBorgServer,
37
+ enrollLocalBorgServerArtifact,
37
38
  probeLocalBorgServer,
38
39
  resolveLocalBorgServerRepositoryCube,
39
40
  resumeLocalBorgServerEnrollment,
@@ -62,6 +63,7 @@ import {
62
63
  setCodexWakeTarget,
63
64
  } from './cubes.js';
64
65
  import { addProjectSessionStartHook } from './config-utils.js';
66
+ import { findPendingServerEnrollment } from './config.js';
65
67
  import { setTerminalTitle as setTitle } from './terminal-title.js';
66
68
  import { defaultCliChoiceDeps, resolveCliChoice } from './cli-platform.js';
67
69
  import { prepareCodexRemoteLaunch, defaultCodexRemoteDeps } from './codex-remote.js';
@@ -240,7 +242,19 @@ export function buildDefaultAssimilateDeps(
240
242
  : null,
241
243
  connectServer: async (apiUrl, enrollment) => {
242
244
  if (enrollment) {
245
+ if (enrollment.artifact) {
246
+ return enrollLocalBorgServerArtifact(enrollment.artifact, {
247
+ invitation: enrollment.invitation,
248
+ ...(enrollment.confirmReplacement === undefined
249
+ ? {}
250
+ : { confirmReplacement: enrollment.confirmReplacement }),
251
+ clientName: osHostname(),
252
+ });
253
+ }
243
254
  return enrollLocalBorgServer(apiUrl, enrollment.invitation, {
255
+ ...(enrollment.confirmReplacement === undefined
256
+ ? {}
257
+ : { confirmReplacement: enrollment.confirmReplacement }),
244
258
  clientName: osHostname(),
245
259
  });
246
260
  }
@@ -250,6 +264,21 @@ export function buildDefaultAssimilateDeps(
250
264
  resumeLocalBorgServerEnrollment(apiUrl, {
251
265
  ...(onPending === undefined ? {} : { onPending }),
252
266
  }),
267
+ peekPendingServerEnrollment: async () => {
268
+ const pending = await findPendingServerEnrollment();
269
+ return pending === null
270
+ ? null
271
+ : { origin: pending.origin, invitation: pending.invitation };
272
+ },
273
+ resumePendingServerEnrollment: async (onPending) =>
274
+ (async () => {
275
+ const pending = await findPendingServerEnrollment();
276
+ if (!pending) return null;
277
+ const result = await resumeLocalBorgServerEnrollment(pending.origin, {
278
+ ...(onPending === undefined ? {} : { onPending }),
279
+ });
280
+ return result ? { ...result, apiUrl: pending.origin } : null;
281
+ })(),
253
282
 
254
283
  resolveRepositoryCube: async (apiUrl, token, input, serverTrustIdentity) => {
255
284
  if (serverTrustIdentity === undefined) {
@@ -33,6 +33,7 @@ export function renderAssimilationWelcome(
33
33
  ``,
34
34
  `In the launched agent, run \`borg_whoami\` and \`borg_roster\` to verify the seat`,
35
35
  `and begin coordinating.`,
36
+ `Launch future sessions in this repository with \`borg\`.`,
36
37
  ...teammateLines,
37
38
  ``,
38
39
  ].join('\n');
package/src/claude.ts CHANGED
@@ -43,6 +43,7 @@ import {
43
43
  } from './reset-local-seat-cmd.js';
44
44
  import { parseLaunchAllArgs } from './parse-launch-all-args.js';
45
45
  import { unknownSubcommand } from './unknown-subcommand.js';
46
+ import { parseRecoverEnrollmentArgs, runRecoverEnrollment } from './recover-enrollment-cmd.js';
46
47
  import { runLaunchAll } from './launch-all-cmd.js';
47
48
  import { buildDefaultLaunchAllDeps } from './launch-all-deps.js';
48
49
  import { discoverDroneCandidates } from './launch-all-discovery.js';
@@ -173,6 +174,27 @@ async function main() {
173
174
  const code = await runResetLocalSeat(parsed.flags, buildDefaultResetLocalSeatDeps());
174
175
  process.exit(code);
175
176
  }
177
+ if (process.argv[2] === 'recover-enrollment') {
178
+ const parsed = parseRecoverEnrollmentArgs(process.argv.slice(3));
179
+ if (!parsed.ok) {
180
+ if (parsed.error === 'help') {
181
+ process.stdout.write(clientSubcommandHelpText('recover-enrollment', [], getPackageVersion()) ?? '');
182
+ process.exit(0);
183
+ }
184
+ process.stderr.write(chalk.red(`${consolePrefix()}◼ borg recover-enrollment: ${parsed.error}\n`));
185
+ process.exit(1);
186
+ }
187
+ const prompt = async (message: string): Promise<string> => {
188
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
189
+ try { return await rl.question(message); } finally { rl.close(); }
190
+ };
191
+ const code = await runRecoverEnrollment(parsed.flags, {
192
+ prompt,
193
+ stderr: (line) => process.stderr.write(line),
194
+ stdout: (line) => process.stdout.write(line),
195
+ });
196
+ process.exit(code);
197
+ }
176
198
  if (process.argv[2] === 'spawn') {
177
199
  // Deprecated; the stub prints a redirect message and exits 2.
178
200
  const code = await runSpawn();
package/src/cli-help.ts CHANGED
@@ -64,6 +64,7 @@ export function clientSubcommandHelpText(
64
64
  case 'setup': return setupHelpText(version);
65
65
  case 'assimilate': return assimilateHelpText(version);
66
66
  case 'reset-local-seat': return resetLocalSeatHelpText(version);
67
+ case 'recover-enrollment': return recoverEnrollmentHelpText(version);
67
68
  case 'sync': return syncHelpText(version);
68
69
  case 'cleanup': return cleanupHelpText(version);
69
70
  case 'launch-all': return launchAllHelpText(version);
@@ -74,9 +75,8 @@ export function clientSubcommandHelpText(
74
75
  export function setupNextStepsText(): string {
75
76
  return (
76
77
  `◼ 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`
78
+ `1. Run \`borg server start\` and leave that terminal open.\n` +
79
+ `2. In a second terminal, cd into your project's Git repository and run \`borg assimilate\`.\n`
80
80
  );
81
81
  }
82
82
 
@@ -102,6 +102,7 @@ export function topLevelHelpText(version: string): string {
102
102
  ` borg assimilate --worktree <name> Spawn a worktree drone (in ~/.borg/worktrees/<repo>/<name>)\n` +
103
103
  ` borg server cube init Initialize this repository's cube without creating a drone\n` +
104
104
  ` borg reset-local-seat Clear ONLY this worktree's saved local seat (offline; after a rejection)\n` +
105
+ ` borg recover-enrollment Clear ONLY one failed server enrollment transaction\n` +
105
106
  ` borg sync [--prune] Sync this worktree's branch to origin/main\n` +
106
107
  ` borg cleanup [--prune] Report (or --prune) worktrees orphaned by evicted drones\n` +
107
108
  ` borg launch-all [cube] Launch all drone worktrees of a cube (default: active cube)\n` +
@@ -114,6 +115,17 @@ export function topLevelHelpText(version: string): string {
114
115
  );
115
116
  }
116
117
 
118
+ export function recoverEnrollmentHelpText(version: string): string {
119
+ return (
120
+ `borg recover-enrollment (borgmcp ${version}) — clear one failed server enrollment\n\n` +
121
+ `Usage:\n` +
122
+ ` borg recover-enrollment [--host <host>] [--yes]\n` +
123
+ ` borg recover-enrollment --help\n\n` +
124
+ `This operation clears only the failed enrollment transaction for one server.\n` +
125
+ `It does not touch other server enrollments or accounts. It never requires manual file editing.\n`
126
+ );
127
+ }
128
+
117
129
  /** Help for the whole-product, npm-owned update journey. */
118
130
  export function updateHelpText(version: string): string {
119
131
  return (
@@ -144,6 +156,7 @@ export function serverHelpText(): string {
144
156
  ` status Report verified runtime evidence.\n` +
145
157
  ` update Verify and activate a local server artifact.\n` +
146
158
  ` invite Create a single-use invitation in an interactive terminal.\n` +
159
+ ` cert-reissue Widen the server certificate to cover another address without replacing the CA.\n` +
147
160
  ` dashboard View the running local server dashboard.\n` +
148
161
  ` cube init Initialize this Git repository's cube; does not create a drone.\n\n` +
149
162
  `Run borg server <command> --help for server command options.\n`