borgmcp 4.6.2 → 4.7.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/dist/assimilate-cmd.d.ts +8 -23
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +8 -5
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +1 -2
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +1 -0
- package/dist/cli-help.js.map +1 -1
- package/dist/codex-launch.d.ts +1 -1
- package/dist/codex-launch.d.ts.map +1 -1
- package/dist/codex-launch.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +55 -18
- package/dist/config.js.map +1 -1
- package/dist/cubes.d.ts +0 -7
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +0 -9
- package/dist/cubes.js.map +1 -1
- package/dist/gc-orphan-inboxes.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -46
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +6 -16
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js.map +1 -1
- package/dist/quickstart-cmd.d.ts.map +1 -1
- package/dist/quickstart-cmd.js +9 -2
- package/dist/quickstart-cmd.js.map +1 -1
- package/dist/regen-format.d.ts +1 -2
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js.map +1 -1
- package/dist/regen.js +1 -1
- package/dist/regen.js.map +1 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +8 -8
- package/dist/remote-client.js.map +1 -1
- package/dist/repository-cube-init.d.ts +1 -1
- package/dist/repository-cube-init.d.ts.map +1 -1
- package/dist/repository-cube-init.js +2 -5
- package/dist/repository-cube-init.js.map +1 -1
- package/dist/repository-identity.d.ts +14 -4
- package/dist/repository-identity.d.ts.map +1 -1
- package/dist/repository-identity.js +86 -18
- package/dist/repository-identity.js.map +1 -1
- package/dist/self-path.d.ts.map +1 -1
- package/dist/self-path.js.map +1 -1
- package/dist/server-handshake.d.ts +1 -1
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +0 -1
- package/dist/server-handshake.js.map +1 -1
- package/dist/tool-manifest.js +3 -3
- package/dist/tool-manifest.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.js +1 -1
- package/dist/update-cmd.js.map +1 -1
- package/docs/RELEASING.md +5 -4
- package/package.json +2 -2
- package/src/assimilate-cmd.ts +28 -37
- package/src/assimilate-deps.ts +0 -4
- package/src/cli-help.ts +1 -0
- package/src/codex-launch.ts +2 -1
- package/src/config.ts +58 -22
- package/src/cubes.ts +0 -10
- package/src/gc-orphan-inboxes.ts +1 -1
- package/src/index.ts +17 -50
- package/src/log-stream.ts +12 -17
- package/src/quickstart-cmd.ts +10 -2
- package/src/regen-format.ts +1 -3
- package/src/regen.ts +2 -3
- package/src/remote-client.ts +12 -9
- package/src/repository-cube-init.ts +3 -6
- package/src/repository-identity.ts +125 -20
- package/src/self-path.ts +0 -1
- package/src/server-handshake.ts +1 -2
- package/src/tool-manifest.ts +3 -3
- package/src/unknown-subcommand.ts +1 -0
- package/src/update-cmd.ts +1 -1
- package/dist/postinstall-banner.d.ts +0 -2
- package/dist/postinstall-banner.d.ts.map +0 -1
- package/dist/postinstall-banner.js +0 -27
- package/dist/postinstall-banner.js.map +0 -1
- package/dist/postinstall.d.ts +0 -11
- package/dist/postinstall.d.ts.map +0 -1
- package/dist/postinstall.js +0 -33
- package/dist/postinstall.js.map +0 -1
- package/dist/types.d.ts +0 -5
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/src/postinstall-banner.ts +0 -37
- package/src/postinstall.ts +0 -36
- package/src/types.ts +0 -5
package/src/index.ts
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
formatDocumentMetadata,
|
|
68
68
|
} from './document-render.js';
|
|
69
69
|
import {
|
|
70
|
+
NEW_CUBE_TEMPLATE_PRESENTATIONS,
|
|
70
71
|
getTemplate,
|
|
71
72
|
listTemplateNames,
|
|
72
73
|
resolveCubeDirectiveForCreate,
|
|
@@ -176,27 +177,6 @@ import {
|
|
|
176
177
|
|
|
177
178
|
const OPEN_CODE_IDENTITY_HANDSHAKE_TIMEOUT_MS = 5_000;
|
|
178
179
|
|
|
179
|
-
/**
|
|
180
|
-
* Apply a template's roles + message_taxonomy to a cube.
|
|
181
|
-
*
|
|
182
|
-
* Client-orchestrated through the server's non-clobbering role and taxonomy
|
|
183
|
-
* primitives. New roles are inserted; existing template-named roles get ADD
|
|
184
|
-
* fragments (template sections/classes the cube lacks) auto-applied, but
|
|
185
|
-
* EVOLVED (conflicting) fragments are kept — never silently overwritten. The
|
|
186
|
-
* old per-role blanket `updateRole`/whole-taxonomy `updateCube` overwrite path
|
|
187
|
-
* is removed. Operators who want to take the template version of a conflicting
|
|
188
|
-
* fragment use `borg_sync-roles` with a `decisions` map. Primitive operations
|
|
189
|
-
* are sequential, so a later failure can leave earlier changes committed.
|
|
190
|
-
* Returns `{ created, updated }` for the caller's toast.
|
|
191
|
-
*/
|
|
192
|
-
async function applyTemplateToCube(
|
|
193
|
-
cubeId: string,
|
|
194
|
-
template: Template,
|
|
195
|
-
authority?: LocalManageAuthority,
|
|
196
|
-
): Promise<{ created: number; updated: number }> {
|
|
197
|
-
return await applyTemplate(cubeId, template.name, authority);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
180
|
export async function runApplyTemplateTool(
|
|
201
181
|
cubeId: string,
|
|
202
182
|
template: Template,
|
|
@@ -1257,18 +1237,15 @@ export async function main() {
|
|
|
1257
1237
|
case 'borg_create-cube': {
|
|
1258
1238
|
const name = args?.name as string;
|
|
1259
1239
|
const cubeDirective = args?.cube_directive as string;
|
|
1260
|
-
const templateName = args?.template as string | undefined;
|
|
1240
|
+
const templateName = (args?.template as string | undefined) ?? NEW_CUBE_TEMPLATE_PRESENTATIONS[0].name;
|
|
1261
1241
|
if (!name) throw new Error('name is required');
|
|
1262
1242
|
if (cubeDirective === undefined) throw new Error('cube_directive is required (pass empty string if none)');
|
|
1263
1243
|
|
|
1264
1244
|
// Resolve template (validates name early so the cube isn't
|
|
1265
1245
|
// created in a partial state if the template name is wrong).
|
|
1266
|
-
|
|
1267
|
-
if (
|
|
1268
|
-
template
|
|
1269
|
-
if (!template) {
|
|
1270
|
-
throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
|
|
1271
|
-
}
|
|
1246
|
+
const template = getTemplate(templateName);
|
|
1247
|
+
if (!template) {
|
|
1248
|
+
throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
|
|
1272
1249
|
}
|
|
1273
1250
|
|
|
1274
1251
|
// client#499: the cube binds to an EXPLICIT repository (no cwd
|
|
@@ -1286,6 +1263,7 @@ export async function main() {
|
|
|
1286
1263
|
const resolvedCubeDirective = resolveCubeDirectiveForCreate(cubeDirective, template);
|
|
1287
1264
|
const resolvedMessageTaxonomy = resolveMessageTaxonomyForCreate(undefined, template);
|
|
1288
1265
|
const { result, cube } = await createCube(name, resolvedCubeDirective, {
|
|
1266
|
+
template: template.name,
|
|
1289
1267
|
message_taxonomy: resolvedMessageTaxonomy,
|
|
1290
1268
|
repository,
|
|
1291
1269
|
workingRepoName,
|
|
@@ -1302,30 +1280,19 @@ export async function main() {
|
|
|
1302
1280
|
};
|
|
1303
1281
|
}
|
|
1304
1282
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
const summary = await applyTemplateToCube(cube.id, template);
|
|
1310
|
-
const cubeDirectiveNote = resolvedCubeDirective !== cubeDirective
|
|
1311
|
-
? ' Template cube directive applied (operator passed empty).'
|
|
1312
|
-
: '';
|
|
1313
|
-
const text = `Created cube **${cube.name}** (id: ${cube.id}) with template **${templateName}** applied — ${summary.created} role(s) created, ${summary.updated} updated.${cubeDirectiveNote} Use borg_assimilate ${cube.name} to join as a drone.`;
|
|
1314
|
-
return {
|
|
1315
|
-
content: [{ type: 'text', text }],
|
|
1316
|
-
structuredContent: {
|
|
1317
|
-
cube,
|
|
1318
|
-
result: 'created',
|
|
1319
|
-
template: templateName,
|
|
1320
|
-
roles_created: summary.created,
|
|
1321
|
-
roles_updated: summary.updated,
|
|
1322
|
-
},
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1325
|
-
const text = `Created cube **${cube.name}** (id: ${cube.id}). A default "Drone" role was seeded — rename or replace it via borg_update-role / borg_create-role / borg_delete-role. Use borg_assimilate ${cube.name} to join as a drone.`;
|
|
1283
|
+
const cubeDirectiveNote = resolvedCubeDirective !== cubeDirective
|
|
1284
|
+
? ' Template cube directive applied (operator passed empty).'
|
|
1285
|
+
: '';
|
|
1286
|
+
const text = `Created cube **${cube.name}** (id: ${cube.id}) with template **${templateName}** applied atomically — ${cube.roles.length} role(s) created.${cubeDirectiveNote} Use borg_assimilate ${cube.name} to join as a drone.`;
|
|
1326
1287
|
return {
|
|
1327
1288
|
content: [{ type: 'text', text }],
|
|
1328
|
-
structuredContent: {
|
|
1289
|
+
structuredContent: {
|
|
1290
|
+
cube,
|
|
1291
|
+
result: 'created',
|
|
1292
|
+
template: templateName,
|
|
1293
|
+
roles_created: cube.roles.length,
|
|
1294
|
+
roles_updated: 0,
|
|
1295
|
+
},
|
|
1329
1296
|
};
|
|
1330
1297
|
}
|
|
1331
1298
|
|
package/src/log-stream.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
ErrorCode,
|
|
35
35
|
type DocumentCitation,
|
|
36
36
|
} from 'borgmcp-shared/protocol';
|
|
37
|
-
import { getActiveCube, inboxPathForDrone } from './cubes.js';
|
|
37
|
+
import { getActiveCube, inboxPathForDrone, type ActiveCube } from './cubes.js';
|
|
38
38
|
import { assertUuidShape } from './evict-drone.js';
|
|
39
39
|
import { loadBorgServerTrust } from './server-trust.js';
|
|
40
40
|
import {
|
|
@@ -56,7 +56,6 @@ import {
|
|
|
56
56
|
CODEX_HEARTBEAT_CADENCE_MS,
|
|
57
57
|
fireCodexHeartbeatTick,
|
|
58
58
|
formatCodexWakePrompt,
|
|
59
|
-
resolveSessionAgentKind,
|
|
60
59
|
startCodexHeartbeat,
|
|
61
60
|
wakeCodexViaAppServer,
|
|
62
61
|
} from './codex-app-wake.js';
|
|
@@ -420,7 +419,7 @@ export interface StreamDeps {
|
|
|
420
419
|
isSourcePending?: () => Promise<boolean>,
|
|
421
420
|
) => Promise<boolean>;
|
|
422
421
|
/** Inspect whether one retry source remains beyond the durable unread cursor. */
|
|
423
|
-
hasPendingWakeEntry?: (active:
|
|
422
|
+
hasPendingWakeEntry?: (active: StreamActiveCube, entryId: string) => Promise<boolean>;
|
|
424
423
|
/** Clear in-process retry diagnostics after the durable entry is consumed. */
|
|
425
424
|
settleOpenCodeEntry?: (sourceEntryId: string) => void;
|
|
426
425
|
}
|
|
@@ -671,21 +670,17 @@ export function __runLoopForTest(testDeps: RunLoopTestDeps): Promise<void> {
|
|
|
671
670
|
// Single SSE session (exported for tests)
|
|
672
671
|
// ------------------------------------------------------------------
|
|
673
672
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
sessionToken
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
localSessionCredentialRef
|
|
681
|
-
|
|
682
|
-
worktree?: string;
|
|
683
|
-
droneLabel?: string;
|
|
684
|
-
name?: string;
|
|
685
|
-
}
|
|
673
|
+
/** Direct stream controls may omit metadata or omit trust to exercise fail-closed validation. */
|
|
674
|
+
export type StreamActiveCube = Pick<
|
|
675
|
+
ActiveCube,
|
|
676
|
+
'cubeId' | 'droneId' | 'sessionToken' | 'apiUrl'
|
|
677
|
+
> & Partial<Pick<
|
|
678
|
+
ActiveCube,
|
|
679
|
+
'serverTrustIdentity' | 'localSessionCredentialRef' | 'worktree' | 'droneLabel' | 'name'
|
|
680
|
+
>>;
|
|
686
681
|
|
|
687
682
|
export async function streamOnce(
|
|
688
|
-
active:
|
|
683
|
+
active: StreamActiveCube,
|
|
689
684
|
lastEventId: string | null,
|
|
690
685
|
onEventId: (id: string) => void,
|
|
691
686
|
deps: StreamDeps = {}
|
|
@@ -1271,7 +1266,7 @@ export async function streamOnce(
|
|
|
1271
1266
|
}
|
|
1272
1267
|
|
|
1273
1268
|
export async function streamOnceIfOwner(
|
|
1274
|
-
active:
|
|
1269
|
+
active: StreamActiveCube,
|
|
1275
1270
|
lastEventId: string | null,
|
|
1276
1271
|
onEventId: (id: string) => void,
|
|
1277
1272
|
deps: StreamDeps = {}
|
package/src/quickstart-cmd.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { LAUNCH_ALL_NO_DISPATCH_EXIT_CODE, runLaunchAll } from './launch-all-cmd
|
|
|
13
13
|
import type { QuickstartArgs } from './parse-quickstart-args.js';
|
|
14
14
|
import { roleSlug, type Role } from './role-resolver.js';
|
|
15
15
|
import { DEFAULT_LOCAL_SERVER_ORIGIN } from './server-handshake.js';
|
|
16
|
+
import { repositoryDiscoveryFailureMessage } from './repository-identity.js';
|
|
16
17
|
|
|
17
18
|
interface PlannedRole {
|
|
18
19
|
name: string;
|
|
@@ -178,8 +179,15 @@ export async function runQuickstart(
|
|
|
178
179
|
let context;
|
|
179
180
|
try {
|
|
180
181
|
context = await assimilate.resolveRepositoryContext(assimilate.cwd());
|
|
181
|
-
} catch {
|
|
182
|
-
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error instanceof Error && error.message === 'BARE_REPOSITORY') {
|
|
184
|
+
context = null;
|
|
185
|
+
} else {
|
|
186
|
+
deps.stderr(
|
|
187
|
+
`borg quickstart: could not inspect this Git repository: ${repositoryDiscoveryFailureMessage(error)}\n`,
|
|
188
|
+
);
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
183
191
|
}
|
|
184
192
|
if (!context) {
|
|
185
193
|
deps.stderr('borg quickstart: run this command inside a non-bare Git repository.\n');
|
package/src/regen-format.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { formatDocumentCitations } from './document-render.js';
|
|
|
21
21
|
import { shellEscape } from './shell-escape.js';
|
|
22
22
|
import { OPENCODE_WAKE_PATH_GUIDANCE } from './opencode-wake-copy.js';
|
|
23
23
|
import { isBorgSession } from './launch-gate.js';
|
|
24
|
+
import type { AgentKind } from './agent-runtime.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Extract the SessionStart `source` from a Claude Code hook payload (gh#926).
|
|
@@ -72,9 +73,6 @@ export function shouldRelayPlainSessionReminder(args: {
|
|
|
72
73
|
);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
/** The agent runtime a session runs under — drives the wake-path branch. */
|
|
76
|
-
export type AgentKind = 'claude' | 'codex' | 'opencode';
|
|
77
|
-
|
|
78
76
|
/**
|
|
79
77
|
* The agent-branched WAKE-PATH ARMING sub-block (gh#929/gh#927) — the single
|
|
80
78
|
* shared "re-establish your wake path" instruction reused by the launch
|
package/src/regen.ts
CHANGED
|
@@ -24,10 +24,9 @@ import {
|
|
|
24
24
|
formatLeanOrientation,
|
|
25
25
|
formatPlainSessionReminder,
|
|
26
26
|
shouldRelayPlainSessionReminder,
|
|
27
|
-
type AgentKind,
|
|
28
27
|
} from './regen-format.js';
|
|
29
28
|
import { resolveSessionAgentKind } from './codex-app-wake.js';
|
|
30
|
-
import { resolveReportableSessionAgentKind } from './agent-runtime.js';
|
|
29
|
+
import { resolveReportableSessionAgentKind, type AgentKind } from './agent-runtime.js';
|
|
31
30
|
import { handleVersionFlag } from './version.js';
|
|
32
31
|
import {
|
|
33
32
|
BORG_LAUNCH_REMINDER_DISABLED_ENV,
|
|
@@ -177,7 +176,7 @@ async function emitUnassimilatedNotice(): Promise<void> {
|
|
|
177
176
|
lines.push('');
|
|
178
177
|
lines.push('## Tools you can call once the user has chosen');
|
|
179
178
|
lines.push('- Join an existing cube: `borg_assimilate cube_name="<their choice>"`');
|
|
180
|
-
lines.push('- Create a new cube: `borg_create-cube name="<name>" cube_directive="<markdown>"` (
|
|
179
|
+
lines.push('- Create a new cube: `borg_create-cube name="<name>" cube_directive="<markdown>"` (defaults to `template="software-dev"`)');
|
|
181
180
|
lines.push('- See available templates: `borg_list-templates`');
|
|
182
181
|
process.stdout.write(lines.join('\n') + '\n');
|
|
183
182
|
}
|
package/src/remote-client.ts
CHANGED
|
@@ -60,7 +60,6 @@ import {
|
|
|
60
60
|
} from 'borgmcp-shared/protocol';
|
|
61
61
|
import { Buffer } from 'node:buffer';
|
|
62
62
|
import { canonicalizeWorkingRepoIdentity } from './working-repo.js';
|
|
63
|
-
import { consolePrefix } from './console-prefix.js';
|
|
64
63
|
import { debugLog } from './debug.js';
|
|
65
64
|
import { assertUuidShape } from './evict-drone.js';
|
|
66
65
|
import {
|
|
@@ -70,7 +69,11 @@ import {
|
|
|
70
69
|
DRONE_EVICTED_CODE,
|
|
71
70
|
} from './drone-lifecycle.js';
|
|
72
71
|
import type { MessageTaxonomy, MessageTaxonomyClass } from 'borgmcp-shared/templates';
|
|
73
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
NEW_CUBE_TEMPLATE_PRESENTATIONS,
|
|
74
|
+
getTemplate,
|
|
75
|
+
type TemplateRole,
|
|
76
|
+
} from 'borgmcp-shared/templates';
|
|
74
77
|
import { parseRoleSections } from 'borgmcp-shared/role-section';
|
|
75
78
|
import type { FragmentView, NonClobberSyncResult } from './sync-roles-render.js';
|
|
76
79
|
import type { WorkingRepo } from './working-repo.js';
|
|
@@ -1521,10 +1524,8 @@ export async function listCubes(connection?: RemoteConnection): Promise<{ cubes:
|
|
|
1521
1524
|
* to surface the interactive template prompt on first-drone bootstrap.
|
|
1522
1525
|
*/
|
|
1523
1526
|
/**
|
|
1524
|
-
* Create a new cube. Server-side seeds
|
|
1525
|
-
* so the cube is assimilatable immediately
|
|
1526
|
-
* atomically when `opts.template` is set (single-withUserId transaction —
|
|
1527
|
-
* skips the auto-Drone insert to avoid is_default partial-index conflict).
|
|
1527
|
+
* Create a new cube. Server-side seeds the selected named template atomically
|
|
1528
|
+
* so the cube is assimilatable immediately.
|
|
1528
1529
|
*
|
|
1529
1530
|
* Returns `{ cube, roles }` — the roles array lets the assimilate
|
|
1530
1531
|
* orchestrator pick a default role without a follow-up `getCube` call.
|
|
@@ -1612,8 +1613,10 @@ export async function createCube(
|
|
|
1612
1613
|
connection?: RemoteConnection,
|
|
1613
1614
|
): Promise<{ result: 'created' | 'resolved'; cube: { id: string; name: string; cube_directive?: string; roles: any[]; drones?: any[]; [k: string]: any } }> {
|
|
1614
1615
|
if (!name?.trim()) throw new Error('Local Borg server cube creation requires a cube name');
|
|
1615
|
-
|
|
1616
|
-
|
|
1616
|
+
const templateName = opts?.template ?? NEW_CUBE_TEMPLATE_PRESENTATIONS[0].name;
|
|
1617
|
+
const template = getTemplate(templateName);
|
|
1618
|
+
if (!template) {
|
|
1619
|
+
throw new Error(`Unknown template "${templateName}". Available: ${NEW_CUBE_TEMPLATE_PRESENTATIONS.map(({ name }) => name).join(', ')}`);
|
|
1617
1620
|
}
|
|
1618
1621
|
if (!opts?.repository || !opts?.workingRepoName) {
|
|
1619
1622
|
throw new Error('Local Borg server cube creation requires an explicit repository identity');
|
|
@@ -1628,7 +1631,7 @@ export async function createCube(
|
|
|
1628
1631
|
name: name.trim(),
|
|
1629
1632
|
working_repo_name: opts.workingRepoName,
|
|
1630
1633
|
repository: opts.repository,
|
|
1631
|
-
template:
|
|
1634
|
+
template: template.name,
|
|
1632
1635
|
}));
|
|
1633
1636
|
// client#499: the server homes one cube per repository. A 'resolved' result
|
|
1634
1637
|
// means this repository already has a cube — report it honestly and DO NOT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NEW_CUBE_TEMPLATE_PRESENTATIONS,
|
|
3
|
-
|
|
3
|
+
getTemplate,
|
|
4
4
|
} from 'borgmcp-shared/templates';
|
|
5
5
|
import type {
|
|
6
6
|
AssociateRepositoryCubeResponse,
|
|
@@ -33,7 +33,7 @@ export interface RepositoryCubeCreation {
|
|
|
33
33
|
cube: RepositoryCubeDetail;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
type NewCubeTemplate =
|
|
36
|
+
type NewCubeTemplate = CubeTemplate;
|
|
37
37
|
|
|
38
38
|
const NEW_CUBE_TEMPLATE_NAMES: readonly NewCubeTemplate[] =
|
|
39
39
|
NEW_CUBE_TEMPLATE_PRESENTATIONS.map(({ name }) => name);
|
|
@@ -110,10 +110,7 @@ export function validRepositoryCubeName(value: string): boolean {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
function presentation(template: CubeTemplate) {
|
|
113
|
-
|
|
114
|
-
return { name: template, label: LEGACY_DEFAULT_TEMPLATE_LABEL };
|
|
115
|
-
}
|
|
116
|
-
return NEW_CUBE_TEMPLATE_PRESENTATIONS.find((candidate) => candidate.name === template)!;
|
|
113
|
+
return getTemplate(template)!;
|
|
117
114
|
}
|
|
118
115
|
|
|
119
116
|
async function ask(
|
|
@@ -37,39 +37,132 @@ interface RepositoryIdentityState {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export interface RepositoryIdentityDeps {
|
|
40
|
-
runGit?: (cwd: string, args: string[]) =>
|
|
40
|
+
runGit?: (cwd: string, args: string[]) => GitCommandResult;
|
|
41
41
|
canonicalPath?: (path: string) => Promise<string>;
|
|
42
42
|
root?: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
interface GitCommandResult {
|
|
46
|
+
status: number | null;
|
|
47
|
+
stdout?: string | null;
|
|
48
|
+
stderr?: string | null;
|
|
49
|
+
error?: Error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class RepositoryDiscoveryError extends Error {
|
|
53
|
+
readonly name = 'RepositoryDiscoveryError';
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
readonly kind: 'git-execution' | 'git-query' | 'canonical-path',
|
|
57
|
+
message: string,
|
|
58
|
+
options?: ErrorOptions,
|
|
59
|
+
) {
|
|
60
|
+
super(message, options);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function repositoryDiscoveryFailureMessage(error: unknown): string {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
return message.replace(/[\x00-\x1F\x7F]/g, '').trim() || 'Unknown repository discovery failure';
|
|
67
|
+
}
|
|
68
|
+
|
|
45
69
|
function defaultRunGit(cwd: string, args: string[]) {
|
|
46
|
-
const result = spawnSync('git', args, {
|
|
47
|
-
|
|
70
|
+
const result = spawnSync('git', args, {
|
|
71
|
+
cwd,
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
env: { ...process.env, LC_ALL: 'C' },
|
|
74
|
+
});
|
|
75
|
+
return { status: result.status, stdout: result.stdout, stderr: result.stderr, error: result.error };
|
|
48
76
|
}
|
|
49
77
|
|
|
50
|
-
function output(result:
|
|
78
|
+
function output(result: GitCommandResult): string | null {
|
|
51
79
|
const value = result.status === 0 ? result.stdout?.trim() : null;
|
|
52
80
|
return value || null;
|
|
53
81
|
}
|
|
54
82
|
|
|
83
|
+
function runGitForDiscovery(
|
|
84
|
+
runGit: NonNullable<RepositoryIdentityDeps['runGit']>,
|
|
85
|
+
cwd: string,
|
|
86
|
+
args: string[],
|
|
87
|
+
): GitCommandResult {
|
|
88
|
+
let result: GitCommandResult;
|
|
89
|
+
try {
|
|
90
|
+
result = runGit(cwd, args);
|
|
91
|
+
} catch (cause) {
|
|
92
|
+
throw new RepositoryDiscoveryError(
|
|
93
|
+
'git-execution',
|
|
94
|
+
`Git repository discovery could not run: ${repositoryDiscoveryFailureMessage(cause)}`,
|
|
95
|
+
{ cause },
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (result.error) {
|
|
99
|
+
throw new RepositoryDiscoveryError(
|
|
100
|
+
'git-execution',
|
|
101
|
+
`Git repository discovery could not run: ${repositoryDiscoveryFailureMessage(result.error)}`,
|
|
102
|
+
{ cause: result.error },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function gitQueryFailure(label: string, result: GitCommandResult): RepositoryDiscoveryError {
|
|
109
|
+
const detail = (result.stderr ?? '').replace(/[\x00-\x1F\x7F]/g, '').trim();
|
|
110
|
+
const suffix = detail ? `: ${detail}` : ` (Git exited with status ${result.status ?? 'unknown'})`;
|
|
111
|
+
return new RepositoryDiscoveryError(
|
|
112
|
+
'git-query',
|
|
113
|
+
`Git repository discovery failed while querying ${label}${suffix}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function canonicalizeRepositoryPath(
|
|
118
|
+
canonicalPath: NonNullable<RepositoryIdentityDeps['canonicalPath']>,
|
|
119
|
+
path: string,
|
|
120
|
+
label: string,
|
|
121
|
+
): Promise<string> {
|
|
122
|
+
try {
|
|
123
|
+
return await canonicalPath(path);
|
|
124
|
+
} catch (cause) {
|
|
125
|
+
throw new RepositoryDiscoveryError(
|
|
126
|
+
'canonical-path',
|
|
127
|
+
`Could not resolve the ${label}: ${repositoryDiscoveryFailureMessage(cause)}`,
|
|
128
|
+
{ cause },
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
55
133
|
export async function resolveGitRepositoryContext(
|
|
56
134
|
cwd: string,
|
|
57
135
|
deps: RepositoryIdentityDeps = {},
|
|
58
136
|
): Promise<GitRepositoryContext | null> {
|
|
59
137
|
const runGit = deps.runGit ?? defaultRunGit;
|
|
60
138
|
const canonicalPath = deps.canonicalPath ?? realpath;
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
|
|
139
|
+
const bareResult = runGitForDiscovery(runGit, cwd, ['rev-parse', '--is-bare-repository']);
|
|
140
|
+
if (bareResult.status !== 0) {
|
|
141
|
+
if (/not a git repository/i.test(bareResult.stderr ?? '')) return null;
|
|
142
|
+
throw gitQueryFailure('whether the repository is bare', bareResult);
|
|
143
|
+
}
|
|
144
|
+
const bare = output(bareResult);
|
|
64
145
|
if (bare === 'true') throw new Error('BARE_REPOSITORY');
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
const
|
|
146
|
+
if (bare !== 'false') throw gitQueryFailure('whether the repository is bare', bareResult);
|
|
147
|
+
|
|
148
|
+
const rootResult = runGitForDiscovery(runGit, cwd, ['rev-parse', '--show-toplevel']);
|
|
149
|
+
const rootRaw = output(rootResult);
|
|
150
|
+
if (!rootRaw || !isAbsolute(rootRaw)) throw gitQueryFailure('the repository root', rootResult);
|
|
151
|
+
|
|
152
|
+
const commonResult = runGitForDiscovery(runGit, cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
153
|
+
const commonRaw = output(commonResult);
|
|
154
|
+
if (!commonRaw || !isAbsolute(commonRaw)) throw gitQueryFailure('the Git common directory', commonResult);
|
|
155
|
+
|
|
156
|
+
const root = await canonicalizeRepositoryPath(canonicalPath, rootRaw, 'repository root path');
|
|
157
|
+
const commonDir = await canonicalizeRepositoryPath(canonicalPath, commonRaw, 'Git common directory path');
|
|
69
158
|
const derivedName = normalizeCubeName(basename(root));
|
|
70
159
|
if (!derivedName) return null;
|
|
71
160
|
|
|
72
|
-
const
|
|
161
|
+
const originResult = runGitForDiscovery(runGit, root, ['config', '--get', 'remote.origin.url']);
|
|
162
|
+
if (originResult.status !== 0 && (originResult.status !== 1 || originResult.stderr?.trim())) {
|
|
163
|
+
throw gitQueryFailure('the origin remote', originResult);
|
|
164
|
+
}
|
|
165
|
+
const originRaw = output(originResult);
|
|
73
166
|
const canonical = originRaw ? canonicalizeWorkingRepoIdentity(originRaw) : null;
|
|
74
167
|
return {
|
|
75
168
|
root,
|
|
@@ -94,8 +187,8 @@ function emptyState(): RepositoryIdentityState {
|
|
|
94
187
|
return { version: STORE_VERSION, localIdentities: {}, associations: {} };
|
|
95
188
|
}
|
|
96
189
|
|
|
97
|
-
function parseState(raw: string | null): RepositoryIdentityState {
|
|
98
|
-
if (raw === null) return emptyState();
|
|
190
|
+
function parseState(raw: string | null): { state: RepositoryIdentityState; migrated: boolean } {
|
|
191
|
+
if (raw === null) return { state: emptyState(), migrated: false };
|
|
99
192
|
const parsed = JSON.parse(raw) as Partial<RepositoryIdentityState>;
|
|
100
193
|
if (
|
|
101
194
|
parsed.version !== STORE_VERSION ||
|
|
@@ -109,7 +202,9 @@ function parseState(raw: string | null): RepositoryIdentityState {
|
|
|
109
202
|
throw new Error('Borg repository identity store is malformed or unsupported');
|
|
110
203
|
}
|
|
111
204
|
}
|
|
112
|
-
|
|
205
|
+
let migrated = false;
|
|
206
|
+
for (const [key, rawAssociation] of Object.entries(parsed.associations)) {
|
|
207
|
+
const value = rawAssociation as Omit<Partial<RepositoryAssociation>, 'template'> & { template?: unknown };
|
|
113
208
|
if (
|
|
114
209
|
!/^[a-f0-9]{64}$/.test(key) ||
|
|
115
210
|
!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
@@ -117,13 +212,22 @@ function parseState(raw: string | null): RepositoryIdentityState {
|
|
|
117
212
|
typeof value.name !== 'string' || Buffer.byteLength(value.name, 'utf8') > 120 ||
|
|
118
213
|
!DISPLAY_NAME_RE.test(value.name) ||
|
|
119
214
|
typeof value.workingRepoName !== 'string' || Buffer.byteLength(value.workingRepoName, 'utf8') > 120 ||
|
|
120
|
-
!DISPLAY_NAME_RE.test(value.workingRepoName)
|
|
121
|
-
!CUBE_TEMPLATES.some((template) => template === value.template)
|
|
215
|
+
!DISPLAY_NAME_RE.test(value.workingRepoName)
|
|
122
216
|
) {
|
|
123
217
|
throw new Error('Borg repository identity store is malformed or unsupported');
|
|
124
218
|
}
|
|
219
|
+
// Tag 12 persisted the removed legacy seed. Dropping only this valid
|
|
220
|
+
// association makes the caller re-resolve authoritative state from tag 13.
|
|
221
|
+
if (value.template === 'default') {
|
|
222
|
+
delete parsed.associations[key];
|
|
223
|
+
migrated = true;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (!CUBE_TEMPLATES.some((template) => template === value.template)) {
|
|
227
|
+
throw new Error('Borg repository identity store is malformed or unsupported');
|
|
228
|
+
}
|
|
125
229
|
}
|
|
126
|
-
return parsed as RepositoryIdentityState;
|
|
230
|
+
return { state: parsed as RepositoryIdentityState, migrated };
|
|
127
231
|
}
|
|
128
232
|
|
|
129
233
|
function digest(secret: Buffer, purpose: string, value: string): string {
|
|
@@ -145,9 +249,10 @@ async function withIdentityState<T>(
|
|
|
145
249
|
await atomicWrite0600(secretPath, `${secret.toString('hex')}\n`, options);
|
|
146
250
|
}
|
|
147
251
|
const statePath = join(root, STATE_FILE);
|
|
148
|
-
const
|
|
252
|
+
const parsed = parseState(await readStoreFile(statePath, options));
|
|
253
|
+
const state = parsed.state;
|
|
149
254
|
const outcome = await operation(secret, state);
|
|
150
|
-
if (outcome.changed) {
|
|
255
|
+
if (parsed.migrated || outcome.changed) {
|
|
151
256
|
await atomicWrite0600(statePath, `${JSON.stringify(state, null, 2)}\n`, options);
|
|
152
257
|
}
|
|
153
258
|
return outcome.result;
|
package/src/self-path.ts
CHANGED
package/src/server-handshake.ts
CHANGED
|
@@ -89,7 +89,6 @@ import type { EnrollmentArtifactBinding } from './enrollment-types.js';
|
|
|
89
89
|
const HANDSHAKE_BODY_LIMIT = 64 * 1024;
|
|
90
90
|
const HANDSHAKE_TIMEOUT_MS = 5_000;
|
|
91
91
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
92
|
-
const CLIENT_ATTACH_PATH = '/api/client/attach';
|
|
93
92
|
export const DEFAULT_LOCAL_SERVER_ORIGIN = 'https://127.0.0.1:7091' as const;
|
|
94
93
|
|
|
95
94
|
type FetchLike = typeof fetch;
|
|
@@ -215,7 +214,7 @@ export interface ServerAttachResult {
|
|
|
215
214
|
}
|
|
216
215
|
|
|
217
216
|
/**
|
|
218
|
-
* Attach an enrolled client principal to one granted cube/role over protocol
|
|
217
|
+
* Attach an enrolled client principal to one granted cube/role over protocol v13.
|
|
219
218
|
* The client CSPRNG-generates the session bearer and persists it PENDING in the
|
|
220
219
|
* local 0600 credential store (keyed by the stable per-seat identity) BEFORE
|
|
221
220
|
* this request, so an interrupted/lost response is recovered by re-sending the
|
package/src/tool-manifest.ts
CHANGED
|
@@ -452,8 +452,8 @@ const BASE_TOOL_MANIFEST: ToolManifestEntry[] = [
|
|
|
452
452
|
{
|
|
453
453
|
name: 'borg_create-cube',
|
|
454
454
|
description:
|
|
455
|
-
'Create a new cube bound to an explicit repository. The server homes ONE cube per repository: if the given repository already has a cube, this reports that existing cube and leaves its directive unchanged (it never overwrites it). The server seeds
|
|
456
|
-
'Pass an optional `template` name to
|
|
455
|
+
'Create a new cube bound to an explicit repository. The server homes ONE cube per repository: if the given repository already has a cube, this reports that existing cube and leaves its directive unchanged (it never overwrites it). The server seeds the selected named template atomically; `software-dev` is selected by default. ' +
|
|
456
|
+
'Pass an optional `template` name to select another role set (see borg_list-templates / borg_apply-template).',
|
|
457
457
|
inputSchema: {
|
|
458
458
|
type: 'object',
|
|
459
459
|
properties: {
|
|
@@ -476,7 +476,7 @@ const BASE_TOOL_MANIFEST: ToolManifestEntry[] = [
|
|
|
476
476
|
},
|
|
477
477
|
template: {
|
|
478
478
|
type: 'string',
|
|
479
|
-
description: 'Optional template
|
|
479
|
+
description: 'Optional named template (default: "software-dev"). The server seeds its roles atomically when a cube is newly created; it is never applied to an already-existing repository cube.',
|
|
480
480
|
},
|
|
481
481
|
},
|
|
482
482
|
required: ['name', 'cube_directive', 'repository'],
|
package/src/update-cmd.ts
CHANGED
|
@@ -1370,7 +1370,7 @@ export async function runEarlyUpdate(
|
|
|
1370
1370
|
argv: readonly string[],
|
|
1371
1371
|
deps?: UpdateDeps,
|
|
1372
1372
|
): Promise<number | null> {
|
|
1373
|
-
if (argv[2] !== 'update') return null;
|
|
1373
|
+
if (argv[2] !== 'update' && argv[2] !== 'upgrade') return null;
|
|
1374
1374
|
const parsed = parseUpdateArgs(argv.slice(3), process.env[REENTRY_ENV] === '1');
|
|
1375
1375
|
const resolvedDeps = deps ?? buildDefaultUpdateDeps(parsed.ok ? parsed.registry : undefined);
|
|
1376
1376
|
if (!parsed.ok) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"postinstall-banner.d.ts","sourceRoot":"","sources":["../src/postinstall-banner.ts"],"names":[],"mappings":"AAWA,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,OAAO,GAAG,MAAM,CAyBjE"}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// Pure builder for the post-install banner text. Kept separate from
|
|
2
|
-
// postinstall.ts (which is a side-effectful entry script that runs detection
|
|
3
|
-
// + console output + process.exit on import) so the message content is unit-
|
|
4
|
-
// testable without triggering those side effects.
|
|
5
|
-
//
|
|
6
|
-
// gh#653 B1: a non-expert who installs borgmcp WITHOUT an agent CLI (Claude
|
|
7
|
-
// Code / Codex) present would otherwise be sent straight to "Next step: borg
|
|
8
|
-
// setup", which dead-ends at "No supported agent CLI found". When no agent CLI
|
|
9
|
-
// is detected, surface the install-an-agent-CLI step FIRST so the banner never
|
|
10
|
-
// points the user into a wall.
|
|
11
|
-
export function composeInstallBanner(hasAgentCli) {
|
|
12
|
-
const lines = [
|
|
13
|
-
'',
|
|
14
|
-
'╔════════════════════════════════════╗',
|
|
15
|
-
'║ ◼ Borg MCP Installed ◼ ║',
|
|
16
|
-
'╚════════════════════════════════════╝',
|
|
17
|
-
'',
|
|
18
|
-
];
|
|
19
|
-
if (!hasAgentCli) {
|
|
20
|
-
lines.push('⚠ No agent CLI detected. Borg runs on top of Claude Code or Codex —', ' install one first:', ' Claude Code: https://claude.ai/download', ' Codex: https://developers.openai.com/codex', '', 'Then run:', ' borg setup', '');
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
lines.push('Next step:', ' borg setup', '');
|
|
24
|
-
}
|
|
25
|
-
return lines.join('\n');
|
|
26
|
-
}
|
|
27
|
-
//# sourceMappingURL=postinstall-banner.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"postinstall-banner.js","sourceRoot":"","sources":["../src/postinstall-banner.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,6EAA6E;AAC7E,6EAA6E;AAC7E,kDAAkD;AAClD,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAC/E,+BAA+B;AAE/B,MAAM,UAAU,oBAAoB,CAAC,WAAoB;IACvD,MAAM,KAAK,GAAG;QACZ,EAAE;QACF,wCAAwC;QACxC,wCAAwC;QACxC,wCAAwC;QACxC,EAAE;KACH,CAAC;IAEF,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,KAAK,CAAC,IAAI,CACR,qEAAqE,EACrE,sBAAsB,EACtB,6CAA6C,EAC7C,sDAAsD,EACtD,EAAE,EACF,WAAW,EACX,cAAc,EACd,EAAE,CACH,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,YAAY,EAAE,cAAc,EAAE,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
|
package/dist/postinstall.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Post-install script
|
|
4
|
-
*
|
|
5
|
-
* Detects local vs global installation and rejects local installs.
|
|
6
|
-
* gh#653 B1: also detects whether an agent CLI (Claude Code / Codex) is present
|
|
7
|
-
* and adjusts the "next step" banner so a user with no agent CLI is told to
|
|
8
|
-
* install one FIRST rather than being sent into `borg setup`'s dead-end.
|
|
9
|
-
*/
|
|
10
|
-
export {};
|
|
11
|
-
//# sourceMappingURL=postinstall.d.ts.map
|