borgmcp 2.2.0 → 2.4.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 +36 -0
- package/dist/assimilate-cmd.d.ts +6 -0
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +31 -7
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +2 -0
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +14 -0
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +19 -0
- package/dist/cli-help.js.map +1 -1
- package/dist/cli-platform.d.ts +1 -0
- package/dist/cli-platform.d.ts.map +1 -1
- package/dist/cli-platform.js +5 -1
- package/dist/cli-platform.js.map +1 -1
- package/dist/console-prefix.d.ts +9 -10
- package/dist/console-prefix.d.ts.map +1 -1
- package/dist/console-prefix.js +23 -20
- package/dist/console-prefix.js.map +1 -1
- package/dist/cubes.d.ts +11 -0
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +10 -0
- package/dist/cubes.js.map +1 -1
- package/dist/display-identity.d.ts +34 -0
- package/dist/display-identity.d.ts.map +1 -0
- package/dist/display-identity.js +88 -0
- package/dist/display-identity.js.map +1 -0
- package/dist/docs-sections.d.ts.map +1 -1
- package/dist/docs-sections.js +9 -0
- package/dist/docs-sections.js.map +1 -1
- package/dist/drone-management.d.ts.map +1 -1
- package/dist/drone-management.js +6 -0
- package/dist/drone-management.js.map +1 -1
- package/dist/inbox-monitor.d.ts.map +1 -1
- package/dist/inbox-monitor.js +12 -0
- package/dist/inbox-monitor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -16
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +41 -15
- package/dist/log-stream.js.map +1 -1
- package/dist/parse-assimilate-args.d.ts +1 -1
- package/dist/parse-assimilate-args.d.ts.map +1 -1
- package/dist/parse-assimilate-args.js +5 -2
- package/dist/parse-assimilate-args.js.map +1 -1
- package/dist/regen-format.d.ts +2 -2
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +6 -5
- package/dist/regen-format.js.map +1 -1
- package/dist/regen.js +6 -2
- package/dist/regen.js.map +1 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +8 -1
- package/dist/remote-client.js.map +1 -1
- package/dist/reset-local-seat-cmd.d.ts +8 -0
- package/dist/reset-local-seat-cmd.d.ts.map +1 -1
- package/dist/reset-local-seat-cmd.js +21 -2
- package/dist/reset-local-seat-cmd.js.map +1 -1
- package/dist/seat-reattach-guard.d.ts +21 -0
- package/dist/seat-reattach-guard.d.ts.map +1 -0
- package/dist/seat-reattach-guard.js +83 -0
- package/dist/seat-reattach-guard.js.map +1 -0
- package/dist/seats.d.ts +6 -2
- package/dist/seats.d.ts.map +1 -1
- package/dist/seats.js +37 -14
- package/dist/seats.js.map +1 -1
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -1
- package/dist/unknown-subcommand.js +1 -0
- package/dist/unknown-subcommand.js.map +1 -1
- package/dist/update-cmd.d.ts +58 -0
- package/dist/update-cmd.d.ts.map +1 -0
- package/dist/update-cmd.js +801 -0
- package/dist/update-cmd.js.map +1 -0
- package/docs/EXTRACTION_PROVENANCE.md +3 -3
- package/docs/LOCAL_SERVER.md +43 -0
- package/docs/RELEASING.md +145 -4
- package/docs/SEAT_LIFECYCLE.md +242 -0
- package/package.json +6 -3
- package/src/assimilate-cmd.ts +45 -7
- package/src/assimilate-deps.ts +2 -0
- package/src/claude.ts +15 -0
- package/src/cli-help.ts +22 -0
- package/src/cli-platform.ts +5 -2
- package/src/console-prefix.ts +25 -18
- package/src/cubes.ts +16 -0
- package/src/display-identity.ts +130 -0
- package/src/docs-sections.ts +9 -0
- package/src/drone-management.ts +6 -0
- package/src/inbox-monitor.ts +19 -0
- package/src/index.ts +66 -20
- package/src/log-stream.ts +38 -14
- package/src/parse-assimilate-args.ts +4 -2
- package/src/regen-format.ts +6 -5
- package/src/regen.ts +11 -2
- package/src/remote-client.ts +8 -0
- package/src/reset-local-seat-cmd.ts +26 -1
- package/src/seat-reattach-guard.ts +113 -0
- package/src/seats.ts +36 -14
- package/src/unknown-subcommand.ts +1 -0
- package/src/update-cmd.ts +1006 -0
package/src/assimilate-cmd.ts
CHANGED
|
@@ -28,6 +28,11 @@ import {
|
|
|
28
28
|
import type { BorgCli } from './cubes.js';
|
|
29
29
|
import { inboxPathForDrone } from './cubes.js';
|
|
30
30
|
import { monitorStateRootForWorktree } from './inbox-monitor.js';
|
|
31
|
+
import {
|
|
32
|
+
formatSeatReattachRefusal,
|
|
33
|
+
inspectLiveInboxMonitor,
|
|
34
|
+
type LiveInboxMonitor,
|
|
35
|
+
} from './seat-reattach-guard.js';
|
|
31
36
|
import { resolveLaunchEnv } from './model-presets.js';
|
|
32
37
|
import { unlinkSync } from 'node:fs';
|
|
33
38
|
import {
|
|
@@ -89,6 +94,7 @@ export interface AssimilateFlags {
|
|
|
89
94
|
model?: string;
|
|
90
95
|
server?: string;
|
|
91
96
|
enroll?: boolean;
|
|
97
|
+
force?: boolean;
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
export interface AssimilateArgs {
|
|
@@ -154,6 +160,8 @@ export interface ActiveCube {
|
|
|
154
160
|
/** Verified local-server CA identity; absent until a local server is selected. */
|
|
155
161
|
serverTrustIdentity?: string;
|
|
156
162
|
localSessionCredentialRef?: string;
|
|
163
|
+
/** Durable operation that produced this exact seat binding. */
|
|
164
|
+
operation?: ServerSessionOperation;
|
|
157
165
|
// gh#899: assimilated role, persisted for connect-time tool-surface scoping
|
|
158
166
|
// (mirrors cubes.ts ActiveCube; optional → backward-compatible).
|
|
159
167
|
roleName?: string;
|
|
@@ -202,6 +210,11 @@ export interface AssimilateDeps {
|
|
|
202
210
|
// The scaffold previously declared them sync; that would silently
|
|
203
211
|
// mis-await in Phase F wiring. Promise<...> matches the real shape.
|
|
204
212
|
getActiveCube: () => Promise<ActiveCube | null>;
|
|
213
|
+
/** Read-only relaunch guard for the saved seat's inbox monitor. */
|
|
214
|
+
inspectLiveInboxMonitor?: (
|
|
215
|
+
inboxPath: string,
|
|
216
|
+
monitorStateRoot: string,
|
|
217
|
+
) => LiveInboxMonitor | null;
|
|
205
218
|
hasPersistedActiveCube: () => Promise<boolean>;
|
|
206
219
|
/** Read the RAW persisted local seat for this worktree WITHOUT hydrating its
|
|
207
220
|
* keychain credential — used to recover a crash-in-gap PENDING seat when
|
|
@@ -848,6 +861,12 @@ export async function runAssimilate(
|
|
|
848
861
|
: `named-sibling:${args.flags.worktree}`)
|
|
849
862
|
: 'current-worktree',
|
|
850
863
|
};
|
|
864
|
+
// A selected sibling can be the surviving live seat for this worktree (#63).
|
|
865
|
+
// `--here` must re-send that seat's durable operation, not reconstruct the
|
|
866
|
+
// in-place operation and then fail PREPARE forever on the wrong ref.
|
|
867
|
+
if (existing && args.flags.here && existing.operation) {
|
|
868
|
+
sessionOperation = existing.operation;
|
|
869
|
+
}
|
|
851
870
|
let reattachPriorId: string | undefined;
|
|
852
871
|
let remintInvalidPrior = false;
|
|
853
872
|
let savedLocalRole: Role | undefined;
|
|
@@ -1083,6 +1102,16 @@ export async function runAssimilate(
|
|
|
1083
1102
|
reattachPriorId = existing.droneId;
|
|
1084
1103
|
}
|
|
1085
1104
|
|
|
1105
|
+
if (existing && reattachPriorId !== undefined && !args.flags.force) {
|
|
1106
|
+
const inboxPath = deps.getInboxPath(existing.cubeId, existing.droneId);
|
|
1107
|
+
const stateRoot = monitorStateRootForWorktree(projectRoot);
|
|
1108
|
+
const holder = (deps.inspectLiveInboxMonitor ?? inspectLiveInboxMonitor)(inboxPath, stateRoot);
|
|
1109
|
+
if (holder !== null) {
|
|
1110
|
+
deps.stderr(formatSeatReattachRefusal(holder, 'borg assimilate --here --force'));
|
|
1111
|
+
return 1;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1086
1115
|
// ----- Step 5: Role resolution -----
|
|
1087
1116
|
let resolvedRole: Role | undefined;
|
|
1088
1117
|
if (savedLocalRole) {
|
|
@@ -1545,14 +1574,23 @@ export async function runAssimilate(
|
|
|
1545
1574
|
return 1;
|
|
1546
1575
|
}
|
|
1547
1576
|
// missing / replaced / threw / unavailable: the worktree owns NO durable
|
|
1548
|
-
// locator
|
|
1549
|
-
//
|
|
1577
|
+
// locator. The server may already have accepted the seat, while the client
|
|
1578
|
+
// has no protocol operation id or cleanup endpoint with which to prove reuse
|
|
1579
|
+
// or remove it. State the exact local outcome and do not prescribe a retry
|
|
1580
|
+
// that can silently create a duplicate server seat (#35).
|
|
1581
|
+
const bindFailure =
|
|
1582
|
+
bindOutcome === 'missing'
|
|
1583
|
+
? 'the exact pending seat record went missing locally before it could be bound'
|
|
1584
|
+
: bindOutcome === 'replaced'
|
|
1585
|
+
? 'the exact pending seat record was replaced locally before it could be bound; the replacement was left untouched'
|
|
1586
|
+
: bindOutcome === 'threw'
|
|
1587
|
+
? 'the local seat store could not be read or written while preserving the pending seat'
|
|
1588
|
+
: 'this client did not receive a pending-seat preservation handle';
|
|
1550
1589
|
deps.stderr(
|
|
1551
|
-
`This worktree's secure session on ${auth.apiUrl} did not finish activating
|
|
1552
|
-
|
|
1553
|
-
'
|
|
1554
|
-
|
|
1555
|
-
`${localAssimilateCommand(auth.apiUrl)} to attach against the current state.\n`,
|
|
1590
|
+
`This worktree's secure session on ${auth.apiUrl} did not finish activating: ` +
|
|
1591
|
+
`${bindFailure}. The spawned worktree will be removed. No client-only command can ` +
|
|
1592
|
+
'prove reuse or safely clear the possibly accepted server-side seat; ask the server ' +
|
|
1593
|
+
'operator to inspect that seat before retrying.\n',
|
|
1556
1594
|
);
|
|
1557
1595
|
rollbackWorktree();
|
|
1558
1596
|
return 1;
|
package/src/assimilate-deps.ts
CHANGED
|
@@ -17,6 +17,7 @@ import prompts from 'prompts';
|
|
|
17
17
|
import { readinessProbeEnv } from './readiness-probe.js';
|
|
18
18
|
import { resolveMcpBinaryPath } from './self-path.js';
|
|
19
19
|
import { buildRuntimeMetadataReport } from './runtime-metadata.js';
|
|
20
|
+
import { inspectLiveInboxMonitor } from './seat-reattach-guard.js';
|
|
20
21
|
|
|
21
22
|
import type { AssimilateDeps } from './assimilate-cmd.js';
|
|
22
23
|
import {
|
|
@@ -170,6 +171,7 @@ export function buildDefaultAssimilateDeps(
|
|
|
170
171
|
},
|
|
171
172
|
|
|
172
173
|
getActiveCube: () => cubesGetActive(),
|
|
174
|
+
inspectLiveInboxMonitor,
|
|
173
175
|
hasPersistedActiveCube: () => cubesHasPersistedActive(),
|
|
174
176
|
readPersistedLocalSeat: () => readPersistedLocalSeat(),
|
|
175
177
|
peekServerSessionRecord: async (credentialRef, binding) =>
|
package/src/claude.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
26
26
|
import chalk from 'chalk';
|
|
27
27
|
import { findProjectRoot, getActiveCube, inboxPathForDrone, setCodexWakeTarget, pruneDeadCodexWakeTargets } from './cubes.js';
|
|
28
28
|
import { monitorStateRootForWorktree } from './inbox-monitor.js';
|
|
29
|
+
import { formatSeatReattachRefusal, inspectLiveInboxMonitor } from './seat-reattach-guard.js';
|
|
29
30
|
import { handleVersionFlag, getPackageVersion } from './version.js';
|
|
30
31
|
import { isHelpFlag, setupHelpText, topLevelHelpText, assimilateHelpText, resetLocalSeatHelpText } from './cli-help.js';
|
|
31
32
|
import { runSpawn } from './spawn.js';
|
|
@@ -82,6 +83,7 @@ import { installBorgPlugin } from './opencode-plugin.js';
|
|
|
82
83
|
import { connectOpenCodeDrone, computeOpenCodePort, createOpenCodeLaunchKickoff, injectInitialKickoff } from './opencode-drone.js';
|
|
83
84
|
import { buildOpenCodeLaunchArgs, defaultApprovalIo, resolveLaunchBorgApprovals } from './cli-tool-approval.js';
|
|
84
85
|
import { runEarlyServerFacade } from './server-facade.js';
|
|
86
|
+
import { runEarlyUpdate } from './update-cmd.js';
|
|
85
87
|
|
|
86
88
|
export type AssimilateDepsBuilder = typeof buildDefaultAssimilateDeps;
|
|
87
89
|
|
|
@@ -101,6 +103,9 @@ export async function runAssimilateEntry(
|
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
async function main() {
|
|
106
|
+
const updateExitCode = await runEarlyUpdate(process.argv);
|
|
107
|
+
if (updateExitCode !== null) process.exit(updateExitCode);
|
|
108
|
+
|
|
104
109
|
const serverExitCode = await runEarlyServerFacade(process.argv);
|
|
105
110
|
if (serverExitCode !== null) process.exit(serverExitCode);
|
|
106
111
|
|
|
@@ -306,6 +311,16 @@ async function main() {
|
|
|
306
311
|
cli = action.cli;
|
|
307
312
|
}
|
|
308
313
|
|
|
314
|
+
if (active && !parsedCli.force) {
|
|
315
|
+
const inboxPath = inboxPathForDrone(active.cubeId, active.droneId);
|
|
316
|
+
const stateRoot = monitorStateRootForWorktree(findProjectRoot(process.cwd()));
|
|
317
|
+
const holder = inspectLiveInboxMonitor(inboxPath, stateRoot);
|
|
318
|
+
if (holder !== null) {
|
|
319
|
+
process.stderr.write(formatSeatReattachRefusal(holder, 'borg --force'));
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
309
324
|
// client#20: inspect only the SELECTED harness after the one-shot launch
|
|
310
325
|
// menu choice. Explicit consent enables a narrow per-process override;
|
|
311
326
|
// Borg never rewrites the user's approval policy here.
|
package/src/cli-help.ts
CHANGED
|
@@ -27,6 +27,7 @@ export function topLevelHelpText(version: string): string {
|
|
|
27
27
|
`Usage:\n` +
|
|
28
28
|
` borg Launch your agent CLI; in a TTY, bare borg may show the launch menu\n` +
|
|
29
29
|
` borg setup Set up borg MCP server + agent CLI integration\n` +
|
|
30
|
+
` borg update Update the client and installed local server together\n` +
|
|
30
31
|
` borg assimilate [role] Join or create a cube\n` +
|
|
31
32
|
` borg assimilate --host <host> Join or create on an explicit server\n` +
|
|
32
33
|
` borg assimilate --worktree <name> Spawn a worktree drone (in ~/.borg/worktrees/<repo>/<name>)\n` +
|
|
@@ -44,6 +45,25 @@ export function topLevelHelpText(version: string): string {
|
|
|
44
45
|
);
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/** Help for the whole-product, npm-owned update journey. */
|
|
49
|
+
export function updateHelpText(version: string): string {
|
|
50
|
+
return (
|
|
51
|
+
`borg update${version ? ` (borgmcp ${version})` : ''} — update Borg's client and local server together\n\n` +
|
|
52
|
+
`Usage:\n` +
|
|
53
|
+
` borg update Confirm interactively, then update\n` +
|
|
54
|
+
` borg update --yes Update without prompting (required outside a TTY)\n` +
|
|
55
|
+
` borg update --help Show this help\n\n` +
|
|
56
|
+
`Before changing anything, Borg reads the exact published client and server manifests,\n` +
|
|
57
|
+
`requires matching exact borgmcp-shared pins, and verifies canonical npm ownership. The client is\n` +
|
|
58
|
+
`installed first and the update continues under that new client before the server controller\n` +
|
|
59
|
+
`and runtime are updated. Alternate registries and unsupported or ambiguous package-manager\n` +
|
|
60
|
+
`provenance fail closed with manual-update guidance.\n\n` +
|
|
61
|
+
`If no local server is installed, the server phase is skipped. A failure after the client\n` +
|
|
62
|
+
`succeeds is reported as partial completion with the safe retry command. Borg never starts a\n` +
|
|
63
|
+
`stopped server and never restarts agent processes; restart active agent sessions yourself.\n`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
/** Product Design-approved client-owned copy for `borg server --help`. */
|
|
48
68
|
export function serverHelpText(): string {
|
|
49
69
|
return (
|
|
@@ -89,12 +109,14 @@ export function assimilateHelpText(version: string): string {
|
|
|
89
109
|
` borg assimilate [role] --worktree <name> Spawn the drone in an isolated git worktree\n` +
|
|
90
110
|
` (~/.borg/worktrees/<repo>/<name>)\n` +
|
|
91
111
|
` borg assimilate --here Assimilate in the current worktree (no sibling spawn)\n` +
|
|
112
|
+
` borg assimilate --here --force Reattach despite a still-live inbox monitor\n` +
|
|
92
113
|
` borg assimilate --host <host> Join an authorized self-hosted cube\n` +
|
|
93
114
|
` borg assimilate --host <host> --enroll Operator-terminal enrollment, then create/join (preview)\n` +
|
|
94
115
|
` borg assimilate --help Show this help\n\n` +
|
|
95
116
|
`Flags:\n` +
|
|
96
117
|
` --worktree <name> Create + launch the drone in a sibling git worktree\n` +
|
|
97
118
|
` --here Stay in the current worktree (no sibling spawn)\n` +
|
|
119
|
+
` --force Override a live-seat guard for a wedged monitor\n` +
|
|
98
120
|
` --cube-name <name> Repository cube name (otherwise edit the proposed name)\n` +
|
|
99
121
|
` --host <host> Borg server host or URL (bare hosts default to HTTPS)\n` +
|
|
100
122
|
` --enroll Prompt for a hidden enrollment invitation in the operator terminal\n` +
|
package/src/cli-platform.ts
CHANGED
|
@@ -101,9 +101,10 @@ export function defaultCliChoiceDeps(prompt: (message: string) => Promise<string
|
|
|
101
101
|
|
|
102
102
|
const VALID_CLIS: readonly BorgCli[] = ['claude', 'codex', 'opencode'];
|
|
103
103
|
|
|
104
|
-
export function parseCliFlag(args: string[]): { cli?: BorgCli; rest: string[]; error?: string } {
|
|
104
|
+
export function parseCliFlag(args: string[]): { cli?: BorgCli; force?: boolean; rest: string[]; error?: string } {
|
|
105
105
|
const rest: string[] = [];
|
|
106
106
|
let cli: BorgCli | undefined;
|
|
107
|
+
let force = false;
|
|
107
108
|
for (let i = 0; i < args.length; i++) {
|
|
108
109
|
const arg = args[i];
|
|
109
110
|
if (arg === '--cli') {
|
|
@@ -119,9 +120,11 @@ export function parseCliFlag(args: string[]): { cli?: BorgCli; rest: string[]; e
|
|
|
119
120
|
return { rest, error: `--cli requires one of: ${VALID_CLIS.join(', ')}` };
|
|
120
121
|
}
|
|
121
122
|
cli = value as BorgCli;
|
|
123
|
+
} else if (arg === '--force') {
|
|
124
|
+
force = true;
|
|
122
125
|
} else {
|
|
123
126
|
rest.push(arg);
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
|
-
return { cli, rest };
|
|
129
|
+
return { ...(cli ? { cli } : {}), ...(force ? { force: true } : {}), rest };
|
|
127
130
|
}
|
package/src/console-prefix.ts
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* insufficient — the Queen reads the active terminal's output stream,
|
|
10
10
|
* not its title bar.
|
|
11
11
|
*
|
|
12
|
-
* This module exports a one-shot initializer that
|
|
13
|
-
* from the
|
|
14
|
-
*
|
|
12
|
+
* This module exports a one-shot initializer that seeds the process-local
|
|
13
|
+
* display identity from the selected seat, plus a synchronous getter that
|
|
14
|
+
* follows later server confirmations and wraps each console.error.
|
|
15
15
|
*
|
|
16
16
|
* Format (matches the terminal-title.ts middle-dot convention so
|
|
17
17
|
* surfaces stay internally consistent):
|
|
@@ -27,8 +27,13 @@
|
|
|
27
27
|
import { basename } from 'node:path';
|
|
28
28
|
import chalk from 'chalk';
|
|
29
29
|
import { getActiveCube } from './cubes.js';
|
|
30
|
+
import {
|
|
31
|
+
_resetDisplayIdentityForTests,
|
|
32
|
+
currentDisplayIdentity,
|
|
33
|
+
seedDisplayIdentity,
|
|
34
|
+
} from './display-identity.js';
|
|
30
35
|
|
|
31
|
-
let
|
|
36
|
+
let initialized = false;
|
|
32
37
|
|
|
33
38
|
/** Neutral prefix for a not-yet-assimilated session (gh#818 P1). */
|
|
34
39
|
function unassimilatedPrefix(): string {
|
|
@@ -36,34 +41,35 @@ function unassimilatedPrefix(): string {
|
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
/**
|
|
39
|
-
* Resolve the drone-self-identification prefix from cube state and
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* blocked.
|
|
44
|
+
* Resolve the drone-self-identification prefix from cube state and seed the
|
|
45
|
+
* shared display source. Idempotent — later calls do not re-read the store,
|
|
46
|
+
* while the synchronous prefix still follows server-confirmed display changes.
|
|
47
|
+
* Falls back silently on any read error so console emission is never blocked.
|
|
44
48
|
*/
|
|
45
49
|
export async function initConsolePrefix(): Promise<string> {
|
|
46
|
-
if (
|
|
50
|
+
if (initialized) return droneIdPrefix();
|
|
47
51
|
try {
|
|
48
52
|
const active = await getActiveCube();
|
|
49
53
|
if (active?.droneLabel && active?.name) {
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
seedDisplayIdentity(active);
|
|
55
|
+
initialized = true;
|
|
56
|
+
return droneIdPrefix();
|
|
52
57
|
}
|
|
53
58
|
} catch {
|
|
54
59
|
// Fall through to unassimilated fallback.
|
|
55
60
|
}
|
|
56
|
-
|
|
57
|
-
return
|
|
61
|
+
initialized = true;
|
|
62
|
+
return unassimilatedPrefix();
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
/**
|
|
61
|
-
* Synchronous prefix getter. Returns the
|
|
62
|
-
* otherwise the unassimilated fallback — safe to call before
|
|
66
|
+
* Synchronous prefix getter. Returns the current process-local display value
|
|
67
|
+
* if initialized, otherwise the unassimilated fallback — safe to call before
|
|
63
68
|
* initConsolePrefix() resolves.
|
|
64
69
|
*/
|
|
65
70
|
export function droneIdPrefix(): string {
|
|
66
|
-
|
|
71
|
+
const identity = currentDisplayIdentity();
|
|
72
|
+
if (initialized && identity) return `[${identity.droneLabel} · ${identity.cubeName}]`;
|
|
67
73
|
return unassimilatedPrefix();
|
|
68
74
|
}
|
|
69
75
|
|
|
@@ -95,5 +101,6 @@ export function cerr(...args: any[]): void {
|
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
export function _resetCachedPrefixForTests(): void {
|
|
98
|
-
|
|
104
|
+
initialized = false;
|
|
105
|
+
_resetDisplayIdentityForTests();
|
|
99
106
|
}
|
package/src/cubes.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface ActiveCube {
|
|
|
62
62
|
serverTrustIdentity?: string;
|
|
63
63
|
/** Opaque local-session keychain reference; never a bearer. */
|
|
64
64
|
localSessionCredentialRef?: string;
|
|
65
|
+
/** Durable seat operation that produced this exact local binding. */
|
|
66
|
+
operation?: SeatOperation;
|
|
65
67
|
// gh#899: the assimilated role, persisted so the connect-time ListTools
|
|
66
68
|
// handler can role-scope the NATIVE tool surface (UX/context only — never an
|
|
67
69
|
// auth boundary). Absent on pre-gh#899 cubes.json entries → the filter
|
|
@@ -279,12 +281,26 @@ async function hydrateActiveCube(record: SeatRecord): Promise<ActiveCube | null>
|
|
|
279
281
|
apiUrl: record.origin,
|
|
280
282
|
serverTrustIdentity: record.trustIdentity,
|
|
281
283
|
localSessionCredentialRef: ref,
|
|
284
|
+
operation: record.operation,
|
|
282
285
|
...(record.roleName !== undefined ? { roleName: record.roleName } : {}),
|
|
283
286
|
...(record.roleClass !== undefined ? { roleClass: record.roleClass } : {}),
|
|
284
287
|
...(record.isHumanSeat !== undefined ? { isHumanSeat: record.isHumanSeat } : {}),
|
|
285
288
|
};
|
|
286
289
|
}
|
|
287
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Token-free lookup used after an offline reset. A surviving seat is only
|
|
293
|
+
* described as saved local state; the caller must still revalidate it with the
|
|
294
|
+
* server before launch.
|
|
295
|
+
*/
|
|
296
|
+
export async function findRemainingActiveSeatForWorktree(worktree: string): Promise<{
|
|
297
|
+
apiUrl: string;
|
|
298
|
+
operation: SeatOperation;
|
|
299
|
+
} | null> {
|
|
300
|
+
const record = await getActiveSeatForWorktree(worktree);
|
|
301
|
+
return record ? { apiUrl: record.origin, operation: record.operation } : null;
|
|
302
|
+
}
|
|
303
|
+
|
|
288
304
|
/**
|
|
289
305
|
* Legacy binding-only writer. In the collapsed single-store model an ACTIVE seat is
|
|
290
306
|
* created ONLY by the atomic mint→activate+bind path in seats.ts (driven by the
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ActiveCube } from './cubes.js';
|
|
2
|
+
|
|
3
|
+
const LAST_CONFIRMED = ' (last confirmed)';
|
|
4
|
+
|
|
5
|
+
type DisplayField = {
|
|
6
|
+
value: string | null;
|
|
7
|
+
uncertain: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type DisplayState = {
|
|
11
|
+
seatKey: string;
|
|
12
|
+
cubeName: DisplayField;
|
|
13
|
+
droneLabel: DisplayField;
|
|
14
|
+
roleName: DisplayField;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type DisplayIdentity = {
|
|
18
|
+
cubeName: string;
|
|
19
|
+
droneLabel: string;
|
|
20
|
+
roleName: string | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ServerDisplayIdentity = Partial<DisplayIdentity>;
|
|
24
|
+
|
|
25
|
+
let state: DisplayState | null = null;
|
|
26
|
+
|
|
27
|
+
function seatKey(active: Pick<ActiveCube, 'cubeId' | 'droneId'>): string {
|
|
28
|
+
return `${active.cubeId}\0${active.droneId}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function initialState(active: ActiveCube): DisplayState {
|
|
32
|
+
return {
|
|
33
|
+
seatKey: seatKey(active),
|
|
34
|
+
cubeName: { value: active.name, uncertain: false },
|
|
35
|
+
droneLabel: { value: active.droneLabel, uncertain: false },
|
|
36
|
+
roleName: { value: active.roleName ?? null, uncertain: false },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ensureState(active: ActiveCube): DisplayState {
|
|
41
|
+
const key = seatKey(active);
|
|
42
|
+
if (state === null || state.seatKey !== key) {
|
|
43
|
+
state = initialState(active);
|
|
44
|
+
}
|
|
45
|
+
return state;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function confirmField(field: DisplayField, value: string | null | undefined): void {
|
|
49
|
+
if (value === undefined || value === null) return;
|
|
50
|
+
field.value = value;
|
|
51
|
+
field.uncertain = false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderField(field: DisplayField): string | null {
|
|
55
|
+
if (field.value === null) return null;
|
|
56
|
+
return field.uncertain ? `${field.value}${LAST_CONFIRMED}` : field.value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Seed the invocation-local display source from the exact seat selected by #63. */
|
|
60
|
+
export function seedDisplayIdentity(active: ActiveCube): void {
|
|
61
|
+
ensureState(active);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Apply server-authoritative fields and clear uncertainty only for those fields. */
|
|
65
|
+
export function confirmDisplayIdentity(
|
|
66
|
+
active: ActiveCube,
|
|
67
|
+
identity: ServerDisplayIdentity,
|
|
68
|
+
): DisplayIdentity {
|
|
69
|
+
const current = ensureState(active);
|
|
70
|
+
confirmField(current.cubeName, identity.cubeName);
|
|
71
|
+
confirmField(current.droneLabel, identity.droneLabel);
|
|
72
|
+
confirmField(current.roleName, identity.roleName);
|
|
73
|
+
return renderDisplayIdentity(active);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Mark the current seat identity as last-confirmed after an identity read fails. */
|
|
77
|
+
export function markDisplayIdentityReadFailed(active: ActiveCube): void {
|
|
78
|
+
const current = ensureState(active);
|
|
79
|
+
current.cubeName.uncertain = true;
|
|
80
|
+
current.droneLabel.uncertain = true;
|
|
81
|
+
if (current.roleName.value !== null) current.roleName.uncertain = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderDisplayIdentity(active: ActiveCube): DisplayIdentity {
|
|
85
|
+
const current = ensureState(active);
|
|
86
|
+
return {
|
|
87
|
+
cubeName: renderField(current.cubeName)!,
|
|
88
|
+
droneLabel: renderField(current.droneLabel)!,
|
|
89
|
+
roleName: renderField(current.roleName),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Synchronous view for console-prefix after initConsolePrefix seeds the seat. */
|
|
94
|
+
export function currentDisplayIdentity(): DisplayIdentity | null {
|
|
95
|
+
if (state === null) return null;
|
|
96
|
+
return {
|
|
97
|
+
cubeName: renderField(state.cubeName)!,
|
|
98
|
+
droneLabel: renderField(state.droneLabel)!,
|
|
99
|
+
roleName: renderField(state.roleName),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function identityFromRegen(result: {
|
|
104
|
+
cube?: { name?: string | null };
|
|
105
|
+
drone?: { label?: string | null };
|
|
106
|
+
role?: { name?: string | null };
|
|
107
|
+
}): ServerDisplayIdentity {
|
|
108
|
+
return {
|
|
109
|
+
cubeName: result.cube?.name ?? undefined,
|
|
110
|
+
droneLabel: result.drone?.label ?? undefined,
|
|
111
|
+
roleName: result.role?.name ?? undefined,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function withRenderedRegenIdentity<T extends {
|
|
116
|
+
cube?: Record<string, unknown>;
|
|
117
|
+
drone?: Record<string, unknown>;
|
|
118
|
+
role?: Record<string, unknown>;
|
|
119
|
+
}>(result: T, identity: DisplayIdentity): T {
|
|
120
|
+
return {
|
|
121
|
+
...result,
|
|
122
|
+
cube: { ...result.cube, name: identity.cubeName },
|
|
123
|
+
drone: { ...result.drone, label: identity.droneLabel },
|
|
124
|
+
role: { ...result.role, name: identity.roleName },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function _resetDisplayIdentityForTests(): void {
|
|
129
|
+
state = null;
|
|
130
|
+
}
|
package/src/docs-sections.ts
CHANGED
|
@@ -16,6 +16,7 @@ export const DOCS_BASE_URL = "https://github.com/Byte-Ventures/borg-mcp-client";
|
|
|
16
16
|
|
|
17
17
|
const README_URL = `${DOCS_BASE_URL}#readme`;
|
|
18
18
|
const LOCAL_SERVER_URL = `${DOCS_BASE_URL}/blob/main/docs/LOCAL_SERVER.md`;
|
|
19
|
+
const SEAT_LIFECYCLE_URL = `${DOCS_BASE_URL}/blob/main/docs/SEAT_LIFECYCLE.md`;
|
|
19
20
|
|
|
20
21
|
export interface DocsSection {
|
|
21
22
|
/** logical topic key */
|
|
@@ -71,6 +72,14 @@ export const DOCS_SECTIONS: DocsSection[] = [
|
|
|
71
72
|
summary: "Connect a client to a self-hosted server: invitations, assimilate --host --enroll, credentials.",
|
|
72
73
|
keywords: ["enroll", "enrollment", "invitation", "invite", "assimilate", "--host", "credential", "credentials", "owner", "join server"],
|
|
73
74
|
},
|
|
75
|
+
{
|
|
76
|
+
slug: "seat-lifecycle",
|
|
77
|
+
title: "Seat lifecycle and recovery",
|
|
78
|
+
url: SEAT_LIFECYCLE_URL,
|
|
79
|
+
page: "docs/SEAT_LIFECYCLE.md",
|
|
80
|
+
summary: "Saved-seat states, re-attach and reset recovery, duplicate-session guards, and deterministic multi-seat selection.",
|
|
81
|
+
keywords: ["seat", "lifecycle", "reattach", "re-attach", "reset-local-seat", "evicted", "revoked", "superseded", "inbox monitor", "multiple seats", "silent deafness"],
|
|
82
|
+
},
|
|
74
83
|
{
|
|
75
84
|
slug: "self-hosting",
|
|
76
85
|
title: "Self-hosting operations",
|
package/src/drone-management.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
reassignDrone,
|
|
18
18
|
type LocalManageOperation,
|
|
19
19
|
} from './remote-client.js';
|
|
20
|
+
import { confirmDisplayIdentity } from './display-identity.js';
|
|
20
21
|
|
|
21
22
|
interface ManagedCube {
|
|
22
23
|
id: string;
|
|
@@ -95,6 +96,11 @@ export async function runReassignDroneTool(
|
|
|
95
96
|
|
|
96
97
|
let warning = '';
|
|
97
98
|
if (drone.id === active.droneId) {
|
|
99
|
+
confirmDisplayIdentity(active, {
|
|
100
|
+
cubeName: cube.name,
|
|
101
|
+
droneLabel: drone.label,
|
|
102
|
+
roleName: role.name,
|
|
103
|
+
});
|
|
98
104
|
try {
|
|
99
105
|
const refreshed = await deps.refreshActiveCubeMetadata(activeCubeWithFreshRegenIdentity(active, {
|
|
100
106
|
cube,
|
package/src/inbox-monitor.ts
CHANGED
|
@@ -753,6 +753,23 @@ export function parseMonitorInvocation(argv: string[]): MonitorInvocation | null
|
|
|
753
753
|
return null;
|
|
754
754
|
}
|
|
755
755
|
|
|
756
|
+
function liveMonitorYieldLine(
|
|
757
|
+
inboxPath: string,
|
|
758
|
+
holderPidfilePath: string,
|
|
759
|
+
deps: Pick<InboxLockDeps, 'read' | 'isAlive'>
|
|
760
|
+
): string {
|
|
761
|
+
const raw = deps.read(holderPidfilePath);
|
|
762
|
+
const parsed = raw === null ? null : parsePidfileContent(raw.trim());
|
|
763
|
+
const livePid = parsed && Number.isSafeInteger(parsed.pid) && parsed.pid > 0 && deps.isAlive(parsed.pid)
|
|
764
|
+
? parsed.pid
|
|
765
|
+
: null;
|
|
766
|
+
const holder = livePid === null ? 'another monitor instance' : `a live instance (pid ${livePid})`;
|
|
767
|
+
return (
|
|
768
|
+
`borg-inbox-monitor: seat inbox ${JSON.stringify(inboxPath)} is already monitored by ${holder}; ` +
|
|
769
|
+
'yielding — another session likely holds this seat.'
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
756
773
|
function main(): void {
|
|
757
774
|
const invocation = parseMonitorInvocation(process.argv.slice(2));
|
|
758
775
|
if (!invocation) {
|
|
@@ -812,6 +829,7 @@ function main(): void {
|
|
|
812
829
|
if (claimResult === 'legacy-live') {
|
|
813
830
|
// An already-running old monitor still owns the wake path; yield without
|
|
814
831
|
// touching its inbox-adjacent state.
|
|
832
|
+
console.log(liveMonitorYieldLine(inboxPath, legacyPidfilePathFor(inboxPath), lockDeps));
|
|
815
833
|
process.exit(0);
|
|
816
834
|
}
|
|
817
835
|
if (claimResult === 'legacy-blocked') {
|
|
@@ -822,6 +840,7 @@ function main(): void {
|
|
|
822
840
|
process.exit(1);
|
|
823
841
|
}
|
|
824
842
|
if (claimResult === 'modern-live') {
|
|
843
|
+
console.log(liveMonitorYieldLine(inboxPath, pidfilePath, lockDeps));
|
|
825
844
|
process.exit(0);
|
|
826
845
|
}
|
|
827
846
|
|