borgmcp 2.7.2 → 2.8.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 (63) hide show
  1. package/README.md +96 -230
  2. package/dist/assimilate-cmd.d.ts.map +1 -1
  3. package/dist/assimilate-cmd.js +11 -2
  4. package/dist/assimilate-cmd.js.map +1 -1
  5. package/dist/claude.js +6 -16
  6. package/dist/claude.js.map +1 -1
  7. package/dist/cli-help.d.ts +5 -0
  8. package/dist/cli-help.d.ts.map +1 -1
  9. package/dist/cli-help.js +49 -1
  10. package/dist/cli-help.js.map +1 -1
  11. package/dist/drone-lifecycle.d.ts +8 -0
  12. package/dist/drone-lifecycle.d.ts.map +1 -1
  13. package/dist/drone-lifecycle.js +19 -0
  14. package/dist/drone-lifecycle.js.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +14 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/log-stream.d.ts +4 -0
  19. package/dist/log-stream.d.ts.map +1 -1
  20. package/dist/log-stream.js +28 -4
  21. package/dist/log-stream.js.map +1 -1
  22. package/dist/remote-client.d.ts +1 -1
  23. package/dist/remote-client.d.ts.map +1 -1
  24. package/dist/remote-client.js +42 -7
  25. package/dist/remote-client.js.map +1 -1
  26. package/dist/server-errors.d.ts +8 -0
  27. package/dist/server-errors.d.ts.map +1 -1
  28. package/dist/server-errors.js +19 -0
  29. package/dist/server-errors.js.map +1 -1
  30. package/dist/server-facade.d.ts +4 -0
  31. package/dist/server-facade.d.ts.map +1 -1
  32. package/dist/server-facade.js +11 -0
  33. package/dist/server-facade.js.map +1 -1
  34. package/dist/server-handshake.d.ts +1 -1
  35. package/dist/server-trust.d.ts.map +1 -1
  36. package/dist/server-trust.js +10 -0
  37. package/dist/server-trust.js.map +1 -1
  38. package/dist/setup.js +2 -4
  39. package/dist/setup.js.map +1 -1
  40. package/dist/tool-manifest.d.ts.map +1 -1
  41. package/dist/tool-manifest.js +14 -13
  42. package/dist/tool-manifest.js.map +1 -1
  43. package/dist/update-cmd.d.ts.map +1 -1
  44. package/dist/update-cmd.js +122 -16
  45. package/dist/update-cmd.js.map +1 -1
  46. package/docs/EXTRACTION_PROVENANCE.md +7 -7
  47. package/docs/LOCAL_SERVER.md +3 -3
  48. package/docs/RELEASING.md +16 -6
  49. package/package.json +2 -2
  50. package/src/assimilate-cmd.ts +13 -2
  51. package/src/claude.ts +11 -16
  52. package/src/cli-help.ts +65 -1
  53. package/src/drone-lifecycle.ts +25 -0
  54. package/src/index.ts +15 -1
  55. package/src/log-stream.ts +26 -5
  56. package/src/remote-client.ts +56 -5
  57. package/src/server-errors.ts +24 -0
  58. package/src/server-facade.ts +15 -0
  59. package/src/server-handshake.ts +1 -1
  60. package/src/server-trust.ts +11 -0
  61. package/src/setup.ts +2 -4
  62. package/src/tool-manifest.ts +14 -13
  63. package/src/update-cmd.ts +145 -16
package/src/update-cmd.ts CHANGED
@@ -7,6 +7,7 @@ import which from 'which';
7
7
  import { updateHelpText } from './cli-help.js';
8
8
  import { preflightBorgServerTag } from './server-handshake.js';
9
9
  import { loadBorgServerTrust } from './server-trust.js';
10
+ import { shellEscape } from './shell-escape.js';
10
11
 
11
12
  const CLIENT_PACKAGE = 'borgmcp';
12
13
  const SERVER_PACKAGE = 'borgmcp-server';
@@ -81,6 +82,7 @@ interface ServerStatus {
81
82
  endpoint: string | null;
82
83
  mode: 'foreground' | 'managed' | 'legacy' | 'stopped';
83
84
  serviceAdapter: 'launchd' | 'systemd' | null;
85
+ serviceRecovery: { command: string[] } | null;
84
86
  dataIdentity: 'available' | 'unavailable';
85
87
  nextAction: string | null;
86
88
  }
@@ -103,6 +105,15 @@ interface ServerUpdateFailure {
103
105
  }
104
106
 
105
107
  type ServerUpdateResult = ServerUpdateSuccess | ServerUpdateFailure;
108
+ type ServerUpdateFailureStage =
109
+ | 'initial server status check'
110
+ | 'server controller identity check'
111
+ | 'server runtime activation'
112
+ | 'post-update server status check'
113
+ | 'final server state verification'
114
+ | 'final package verification'
115
+ | 'managed service continuity check'
116
+ | 'running server protocol verification';
106
117
 
107
118
  interface NpmContext {
108
119
  commandPath: string;
@@ -119,9 +130,13 @@ function errorMessage(error: unknown, fallback: string): string {
119
130
  return error instanceof Error ? error.message : fallback;
120
131
  }
121
132
 
133
+ function hasErrorCode(error: unknown, code: string): boolean {
134
+ return error instanceof Error && (error as NodeJS.ErrnoException).code === code;
135
+ }
136
+
122
137
  function renderReentryPreflightFailure(error: unknown, target: UpdateTarget): string {
123
138
  return (
124
- `${errorMessage(error, 'Update preflight failed')}\n` +
139
+ `Update preflight failed: ${errorMessage(error, 'unknown failure')}\n` +
125
140
  `Observed update state:\n` +
126
141
  ` client: ${CLIENT_PACKAGE}@${target.clientVersion} installed and verified before re-entry\n` +
127
142
  ` server controller: not changed by this continuation\n` +
@@ -346,6 +361,18 @@ function isNextAction(value: unknown): value is string | null {
346
361
  );
347
362
  }
348
363
 
364
+ function decodeManagedServiceRecovery(value: unknown): { command: string[] } | null {
365
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
366
+ const record = value as Record<string, unknown>;
367
+ if (record.kind !== 'run-platform-command' || !Array.isArray(record.command) ||
368
+ record.command.length === 0 || record.command.length > 32 ||
369
+ record.command.some((arg) => typeof arg !== 'string' || arg.length === 0 ||
370
+ Array.from(arg).length > 1024 || /\p{Cc}/u.test(arg))) {
371
+ return null;
372
+ }
373
+ return { command: record.command as string[] };
374
+ }
375
+
349
376
  function decodeServerStatus(value: unknown): ServerStatus {
350
377
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
351
378
  throw new Error('server returned invalid JSON status');
@@ -379,13 +406,19 @@ function decodeServerStatus(value: unknown): ServerStatus {
379
406
  record.running_integrity !== null ||
380
407
  record.build_identity !== null ||
381
408
  record.endpoint !== null ||
382
- record.mode !== 'stopped' ||
383
- record.service_adapter !== null
409
+ record.mode !== 'stopped'
384
410
  )) ||
385
411
  (record.status === 'running' && record.mode === 'stopped')
386
412
  ) {
387
413
  throw new Error('server returned inconsistent JSON status');
388
414
  }
415
+ const managedRecovery = record.status === 'stopped' && record.service_adapter !== null
416
+ ? decodeManagedServiceRecovery(record.service_recovery)
417
+ : null;
418
+ if (record.status === 'stopped' && record.service_adapter !== null &&
419
+ (record.service_state !== 'inactive' || managedRecovery === null)) {
420
+ throw new Error('server returned invalid managed-service recovery status');
421
+ }
389
422
  return {
390
423
  state: record.status,
391
424
  installedController: record.installed_controller,
@@ -397,6 +430,7 @@ function decodeServerStatus(value: unknown): ServerStatus {
397
430
  endpoint: record.endpoint,
398
431
  mode: record.mode as ServerStatus['mode'],
399
432
  serviceAdapter: record.service_adapter,
433
+ serviceRecovery: managedRecovery,
400
434
  dataIdentity: record.data_identity,
401
435
  nextAction: record.next_action,
402
436
  };
@@ -472,7 +506,6 @@ function verifyServerStatus(status: ServerStatus, target: PublishedPackage): 'ru
472
506
  status.buildIdentity !== null ||
473
507
  status.endpoint !== null ||
474
508
  status.mode !== 'stopped' ||
475
- status.serviceAdapter !== null ||
476
509
  status.dataIdentity !== 'available'
477
510
  ) {
478
511
  throw new Error('final server verification failed: stopped server reported a running runtime');
@@ -492,6 +525,37 @@ function verifyServerStatus(status: ServerStatus, target: PublishedPackage): 'ru
492
525
  return 'running';
493
526
  }
494
527
 
528
+ function renderServerFailureRecovery(
529
+ status: ServerStatus | null,
530
+ updateAttempted: boolean,
531
+ retryCommand: 'borg update --yes' | 'borg server status' | 'borg server update' | 'borg server start',
532
+ ): string {
533
+ let text = '';
534
+ if (status?.state === 'stopped') {
535
+ text += renderStoppedServiceRecovery(status);
536
+ } else if (updateAttempted && status === null) {
537
+ text += (
538
+ `Local server service state could not be verified; it may be stopped.\n` +
539
+ `Check it with: borg server status\n` +
540
+ `If it is stopped, run the recovery command reported by borg server status.\n`
541
+ );
542
+ }
543
+ if (retryCommand !== 'borg server start') {
544
+ text += status?.state === 'stopped'
545
+ ? `Then retry the failed stage with: ${retryCommand}\n`
546
+ : `Next: ${retryCommand}\n`;
547
+ }
548
+ return text;
549
+ }
550
+
551
+ function renderStoppedServiceRecovery(status: ServerStatus): string {
552
+ if (status.serviceRecovery !== null) {
553
+ const command = status.serviceRecovery.command.map(shellEscape).join(' ');
554
+ return `Local server service is stopped.\nRestart it with: ${command}\n`;
555
+ }
556
+ return `Local server service is stopped.\nStart it with: borg server start\n`;
557
+ }
558
+
495
559
  export async function runUpdate(options: UpdateOptions, deps: UpdateDeps): Promise<number> {
496
560
  if (options.help) {
497
561
  deps.stdout(updateHelpText(''));
@@ -512,7 +576,7 @@ export async function runUpdate(options: UpdateOptions, deps: UpdateDeps): Promi
512
576
  deps.stderr(options.target
513
577
  ? renderReentryPreflightFailure(error, options.target)
514
578
  : (
515
- `${errorMessage(error, 'Update preflight failed')}\n` +
579
+ `Update preflight failed: ${errorMessage(error, 'unknown failure')}\n` +
516
580
  `Observed update state:\n` +
517
581
  ` client: not inspected (registry preflight incomplete)\n` +
518
582
  ` server controller: not inspected (registry preflight incomplete)\n` +
@@ -540,7 +604,7 @@ export async function runUpdate(options: UpdateOptions, deps: UpdateDeps): Promi
540
604
  }
541
605
 
542
606
  deps.stdout(
543
- `Published update plan (npm registry):\n` +
607
+ `Published update plan (${CANONICAL_NPM_REGISTRY}):\n` +
544
608
  ` client: ${CLIENT_PACKAGE}@${client.version} -> ${CLIENT_PACKAGE}@${pair.client.version}\n` +
545
609
  ` target integrity: ${pair.client.integrity}\n` +
546
610
  ` server: ${discoveredServer ? `${SERVER_PACKAGE}@${discoveredServer.version}` : 'not installed'} -> ${SERVER_PACKAGE}@${pair.server.version}\n` +
@@ -624,7 +688,8 @@ export async function runUpdate(options: UpdateOptions, deps: UpdateDeps): Promi
624
688
  : 'not installed'}\n` +
625
689
  ` prepared runtime: not inspected\n` +
626
690
  ` running runtime: not inspected\n` +
627
- `Server mutation was not attempted.\n`,
691
+ `Server mutation was not attempted.\n` +
692
+ `Next: reinstall ${CLIENT_PACKAGE}@${pair.client.version} from ${CANONICAL_NPM_REGISTRY}, then rerun borg update --yes.\n`,
628
693
  );
629
694
  return interrupted ?? 1;
630
695
  }
@@ -671,19 +736,39 @@ export async function runUpdate(options: UpdateOptions, deps: UpdateDeps): Promi
671
736
 
672
737
  let observedStatus: ServerStatus | null = null;
673
738
  let observedUpdate: ServerUpdateResult | null = null;
739
+ let initialServerState: ServerStatus['state'] | null = null;
740
+ let updateAttempted = false;
741
+ let recoveryStatusAttempted = false;
742
+ let failureStage: ServerUpdateFailureStage = 'initial server status check';
743
+ let retryCommand: Parameters<typeof renderServerFailureRecovery>[2] = 'borg server status';
744
+ const observeStatusAfterFailure = async (): Promise<void> => {
745
+ recoveryStatusAttempted = true;
746
+ try {
747
+ observedStatus = decodeServerStatus(await deps.serverJson(server.binPath, 'status'));
748
+ } catch {
749
+ observedStatus = null;
750
+ }
751
+ };
674
752
  try {
675
753
  let status = decodeServerStatus(await deps.serverJson(server.binPath, 'status'));
676
754
  observedStatus = status;
755
+ initialServerState = status.state;
677
756
  if (status.installedController !== exactServerIdentity(pair.server.version)) {
757
+ failureStage = 'server controller identity check';
758
+ retryCommand = 'borg update --yes';
678
759
  throw new Error('server status contradicted the verified controller identity');
679
760
  }
680
761
  try {
681
762
  verifyServerStatus(status, pair.server);
682
763
  } catch {
683
764
  observedStatus = null;
765
+ updateAttempted = true;
766
+ failureStage = 'server runtime activation';
767
+ retryCommand = 'borg server update';
684
768
  const update = decodeServerUpdate(await deps.serverJson(server.binPath, 'update'));
685
769
  observedUpdate = update;
686
770
  if (update.status === 'failed') {
771
+ await observeStatusAfterFailure();
687
772
  throw new Error(`server update failed: ${update.errorCode} (${update.recovery})`);
688
773
  }
689
774
  const serverIdentity = exactServerIdentity(pair.server.version);
@@ -696,10 +781,17 @@ export async function runUpdate(options: UpdateOptions, deps: UpdateDeps): Promi
696
781
  ) {
697
782
  throw new Error('server update result did not reach the target artifact');
698
783
  }
784
+ failureStage = 'post-update server status check';
785
+ retryCommand = 'borg server status';
786
+ recoveryStatusAttempted = true;
699
787
  status = decodeServerStatus(await deps.serverJson(server.binPath, 'status'));
700
788
  observedStatus = status;
701
789
  }
790
+ failureStage = 'final server state verification';
791
+ retryCommand = 'borg server update';
702
792
  const state = verifyServerStatus(status, pair.server);
793
+ failureStage = 'final package verification';
794
+ retryCommand = 'borg update --yes';
703
795
  const [finalClient, finalServer] = await Promise.all([
704
796
  deps.currentClient(),
705
797
  deps.currentServer(),
@@ -707,20 +799,35 @@ export async function runUpdate(options: UpdateOptions, deps: UpdateDeps): Promi
707
799
  assertInstalled(finalClient, pair.client);
708
800
  if (!finalServer) throw new Error('server controller disappeared during final verification');
709
801
  assertInstalled(finalServer, pair.server);
710
- if (state === 'running') await deps.verifyRunningProtocol(status.endpoint!);
802
+ if (initialServerState === 'running' && state === 'stopped') {
803
+ failureStage = 'managed service continuity check';
804
+ retryCommand = 'borg server start';
805
+ throw new Error('a previously running local server is now stopped');
806
+ }
807
+ if (state === 'running') {
808
+ failureStage = 'running server protocol verification';
809
+ retryCommand = 'borg server status';
810
+ await deps.verifyRunningProtocol(status.endpoint!);
811
+ }
711
812
  deps.stdout(
712
813
  state === 'stopped'
713
- ? `Updated ${CLIENT_PACKAGE}@${pair.client.version} and ${SERVER_PACKAGE}@${pair.server.version}: prepared; still stopped.\n`
814
+ ? (
815
+ `Updated ${CLIENT_PACKAGE}@${pair.client.version} and ${SERVER_PACKAGE}@${pair.server.version}: prepared.\n` +
816
+ renderStoppedServiceRecovery(status)
817
+ )
714
818
  : `Updated ${CLIENT_PACKAGE}@${pair.client.version} and ${SERVER_PACKAGE}@${pair.server.version}; running identities and protocol verified.\n`,
715
819
  );
716
820
  deps.stdout('Restart active agent sessions to load the updated client.\n');
717
821
  return 0;
718
822
  } catch (error) {
719
823
  const interrupted = signalExitCode(error);
824
+ if (updateAttempted && !recoveryStatusAttempted && observedStatus === null) {
825
+ await observeStatusAfterFailure();
826
+ }
720
827
  deps.stderr(
721
- `Server update or final verification failed: ${errorMessage(error, 'unknown failure')}.\n` +
828
+ `Server update failed during ${failureStage}: ${errorMessage(error, 'unknown failure')}.\n` +
722
829
  renderServerState(client, server, observedStatus, observedUpdate) +
723
- `Retry with: borg update --yes\n`,
830
+ renderServerFailureRecovery(observedStatus, updateAttempted, retryCommand),
724
831
  );
725
832
  return interrupted ?? 1;
726
833
  }
@@ -735,8 +842,8 @@ interface CommandResult {
735
842
  class CommandSignalError extends Error {
736
843
  readonly exitCode: number;
737
844
 
738
- constructor(signal: NodeJS.Signals) {
739
- super(`command stopped by ${signal}`);
845
+ constructor(signal: NodeJS.Signals, stderr = '') {
846
+ super(`command stopped by ${signal}${serverCommandStderr(stderr)}`);
740
847
  this.name = 'CommandSignalError';
741
848
  this.exitCode = 128 + (constants.signals[signal] ?? 1);
742
849
  }
@@ -775,7 +882,7 @@ function runCommand(
775
882
  child.once('exit', (code, signal) => {
776
883
  if (settled) return;
777
884
  if (signal) {
778
- fail(new CommandSignalError(signal));
885
+ fail(new CommandSignalError(signal, stderr));
779
886
  return;
780
887
  }
781
888
  settled = true;
@@ -800,6 +907,14 @@ function singleLine(text: string, label: string): string {
800
907
  return value;
801
908
  }
802
909
 
910
+ function serverCommandStderr(stderr: string): string {
911
+ const detail = stderr.trim();
912
+ if (detail === '') return '';
913
+ const bounded = Array.from(detail).slice(-4096).join('')
914
+ .replace(/\p{Cc}/gu, (character) => character === '\n' || character === '\t' ? character : '?');
915
+ return `\nServer command stderr:\n${bounded}`;
916
+ }
917
+
803
918
  async function npmText(commandPath: string, args: readonly string[], label: string): Promise<string> {
804
919
  const result = await runCommand(commandPath, args);
805
920
  if (result.code !== 0) throw new Error(`npm ${label} lookup failed`);
@@ -925,6 +1040,18 @@ async function inspectNpmPackage(
925
1040
  if (!required) return null;
926
1041
  throw new Error(`${binName} is not available on PATH`);
927
1042
  }
1043
+ try {
1044
+ await realpath(join(context.root, name));
1045
+ } catch (error) {
1046
+ if (hasErrorCode(error, 'ENOENT')) {
1047
+ throw new Error(
1048
+ `${binName} is on PATH from a different npm global prefix. ` +
1049
+ `borg update only manages packages under the active npm prefix. ` +
1050
+ `Run npm prefix --global to inspect it, then update the other installation with its package manager.`,
1051
+ );
1052
+ }
1053
+ throw error;
1054
+ }
928
1055
  return inspectNpmPackageAt({
929
1056
  name,
930
1057
  binName,
@@ -1065,10 +1192,12 @@ export function buildDefaultUpdateDeps(): UpdateDeps {
1065
1192
  try {
1066
1193
  parsed = JSON.parse(result.stdout);
1067
1194
  } catch {
1068
- throw new Error(`server ${command} returned invalid JSON`);
1195
+ throw new Error(`server ${command} returned invalid JSON${serverCommandStderr(result.stderr)}`);
1069
1196
  }
1070
1197
  if (result.code !== 0 && command !== 'update') {
1071
- throw new Error(`server ${command} exited ${result.code}`);
1198
+ throw new Error(
1199
+ `server ${command} exited ${result.code}${serverCommandStderr(result.stderr)}`,
1200
+ );
1072
1201
  }
1073
1202
  return parsed;
1074
1203
  },