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.
- package/README.md +96 -230
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +11 -2
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/claude.js +6 -16
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +5 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +49 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/drone-lifecycle.d.ts +8 -0
- package/dist/drone-lifecycle.d.ts.map +1 -1
- package/dist/drone-lifecycle.js +19 -0
- package/dist/drone-lifecycle.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +4 -0
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +28 -4
- package/dist/log-stream.js.map +1 -1
- package/dist/remote-client.d.ts +1 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +42 -7
- package/dist/remote-client.js.map +1 -1
- package/dist/server-errors.d.ts +8 -0
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js +19 -0
- package/dist/server-errors.js.map +1 -1
- package/dist/server-facade.d.ts +4 -0
- package/dist/server-facade.d.ts.map +1 -1
- package/dist/server-facade.js +11 -0
- package/dist/server-facade.js.map +1 -1
- package/dist/server-handshake.d.ts +1 -1
- package/dist/server-trust.d.ts.map +1 -1
- package/dist/server-trust.js +10 -0
- package/dist/server-trust.js.map +1 -1
- package/dist/setup.js +2 -4
- package/dist/setup.js.map +1 -1
- package/dist/tool-manifest.d.ts.map +1 -1
- package/dist/tool-manifest.js +14 -13
- package/dist/tool-manifest.js.map +1 -1
- package/dist/update-cmd.d.ts.map +1 -1
- package/dist/update-cmd.js +122 -16
- package/dist/update-cmd.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +7 -7
- package/docs/LOCAL_SERVER.md +3 -3
- package/docs/RELEASING.md +16 -6
- package/package.json +2 -2
- package/src/assimilate-cmd.ts +13 -2
- package/src/claude.ts +11 -16
- package/src/cli-help.ts +65 -1
- package/src/drone-lifecycle.ts +25 -0
- package/src/index.ts +15 -1
- package/src/log-stream.ts +26 -5
- package/src/remote-client.ts +56 -5
- package/src/server-errors.ts +24 -0
- package/src/server-facade.ts +15 -0
- package/src/server-handshake.ts +1 -1
- package/src/server-trust.ts +11 -0
- package/src/setup.ts +2 -4
- package/src/tool-manifest.ts +14 -13
- 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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
?
|
|
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
|
|
828
|
+
`Server update failed during ${failureStage}: ${errorMessage(error, 'unknown failure')}.\n` +
|
|
722
829
|
renderServerState(client, server, observedStatus, observedUpdate) +
|
|
723
|
-
|
|
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(
|
|
1198
|
+
throw new Error(
|
|
1199
|
+
`server ${command} exited ${result.code}${serverCommandStderr(result.stderr)}`,
|
|
1200
|
+
);
|
|
1072
1201
|
}
|
|
1073
1202
|
return parsed;
|
|
1074
1203
|
},
|