borgmcp 4.2.3 → 4.3.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/index.d.ts.map +1 -1
- package/dist/index.js +21 -6
- package/dist/index.js.map +1 -1
- package/dist/remote-client.d.ts +23 -19
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +70 -6
- package/dist/remote-client.js.map +1 -1
- package/dist/tool-manifest.d.ts.map +1 -1
- package/dist/tool-manifest.js +17 -4
- package/dist/tool-manifest.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +26 -6
- package/src/remote-client.ts +99 -10
- package/src/tool-manifest.ts +17 -4
package/src/remote-client.ts
CHANGED
|
@@ -51,11 +51,15 @@ import {
|
|
|
51
51
|
type EvictDroneResult,
|
|
52
52
|
type ReassignDroneResult,
|
|
53
53
|
type RoleRationaleResult,
|
|
54
|
+
decodeCreateCubeResponse,
|
|
54
55
|
type PutDocumentResult,
|
|
55
56
|
type GetDocumentResult,
|
|
56
57
|
type ListDocumentsResult,
|
|
57
58
|
type RemoveDocumentResult,
|
|
59
|
+
type CreateCubeRepository,
|
|
58
60
|
} from 'borgmcp-shared/protocol';
|
|
61
|
+
import { Buffer } from 'node:buffer';
|
|
62
|
+
import { canonicalizeWorkingRepoIdentity } from './working-repo.js';
|
|
59
63
|
import { consolePrefix } from './console-prefix.js';
|
|
60
64
|
import { debugLog } from './debug.js';
|
|
61
65
|
import { assertUuidShape } from './evict-drone.js';
|
|
@@ -1526,31 +1530,116 @@ export async function listCubes(connection?: RemoteConnection): Promise<{ cubes:
|
|
|
1526
1530
|
* orchestrator pick a default role without a follow-up `getCube` call.
|
|
1527
1531
|
* Existing callers that read `body.cube` keep working (forward-compat).
|
|
1528
1532
|
*/
|
|
1533
|
+
const REPOSITORY_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1534
|
+
// The wire's working_repo_name rule (borgmcp-shared decodeWorkingRepositoryName,
|
|
1535
|
+
// which the package does not export): 1-120 UTF-8 bytes, must start with a
|
|
1536
|
+
// letter or digit, then letters/digits/spaces/dots/underscores/hyphens.
|
|
1537
|
+
const WORKING_REPO_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/;
|
|
1538
|
+
|
|
1539
|
+
function assertValidWorkingRepoName(name: string): string {
|
|
1540
|
+
const bytes = Buffer.byteLength(name, 'utf8');
|
|
1541
|
+
if (bytes < 1 || bytes > 120 || !WORKING_REPO_NAME_RE.test(name)) {
|
|
1542
|
+
throw new Error(
|
|
1543
|
+
'working_repo_name must start with a letter or digit and contain only letters, digits, spaces, dots, underscores, or hyphens (1-120 UTF-8 bytes).',
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
return name;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
export interface NormalizedCreateCubeRepository {
|
|
1550
|
+
repository: CreateCubeRepository;
|
|
1551
|
+
workingRepoName: string;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* client#499: normalize the EXPLICIT repository argument into the wire's
|
|
1556
|
+
* `{ repository, working_repo_name }` pair — no cwd inference. A canonical git
|
|
1557
|
+
* remote URL becomes an `origin` identity (reusing the shared canonicalizer,
|
|
1558
|
+
* the same encoding the CLI create path uses); a UUID becomes a `local`
|
|
1559
|
+
* identity (the server requires a UUID for local repositories). The optional
|
|
1560
|
+
* working-repo display name defaults to the origin's repository segment.
|
|
1561
|
+
*/
|
|
1562
|
+
export function normalizeExplicitRepository(
|
|
1563
|
+
repositoryArg: unknown,
|
|
1564
|
+
workingRepoNameArg?: unknown,
|
|
1565
|
+
): NormalizedCreateCubeRepository {
|
|
1566
|
+
if (typeof repositoryArg !== 'string' || repositoryArg.trim().length === 0) {
|
|
1567
|
+
throw new Error(
|
|
1568
|
+
'repository is required: pass a canonical git remote URL (e.g. https://github.com/owner/repo) or a UUID identifying a local repository.',
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
// client#499 CR: a PRESENT working_repo_name must be a string; reject a
|
|
1572
|
+
// present non-string rather than silently coercing it to the derived name.
|
|
1573
|
+
if (workingRepoNameArg !== undefined && workingRepoNameArg !== null && typeof workingRepoNameArg !== 'string') {
|
|
1574
|
+
throw new Error('working_repo_name must be a string when provided.');
|
|
1575
|
+
}
|
|
1576
|
+
const repoInput = repositoryArg.trim();
|
|
1577
|
+
const nameArg = typeof workingRepoNameArg === 'string' ? workingRepoNameArg.trim() : '';
|
|
1578
|
+
|
|
1579
|
+
const canonical = canonicalizeWorkingRepoIdentity(repoInput);
|
|
1580
|
+
if (canonical?.origin && canonical.name) {
|
|
1581
|
+
const derivedName = canonical.name.split('/').pop() || canonical.name;
|
|
1582
|
+
// Validate the FINAL name (explicit or derived) against the wire rule
|
|
1583
|
+
// before any network use, for both repository kinds — fail closed.
|
|
1584
|
+
return {
|
|
1585
|
+
repository: { kind: 'origin', value: canonical.origin },
|
|
1586
|
+
workingRepoName: assertValidWorkingRepoName(nameArg || derivedName),
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
if (REPOSITORY_UUID_RE.test(repoInput)) {
|
|
1590
|
+
if (!nameArg) {
|
|
1591
|
+
throw new Error(
|
|
1592
|
+
'working_repo_name is required when repository is a local UUID — there is no origin URL to derive a name from.',
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
return { repository: { kind: 'local', value: repoInput }, workingRepoName: assertValidWorkingRepoName(nameArg) };
|
|
1596
|
+
}
|
|
1597
|
+
throw new Error(
|
|
1598
|
+
'repository must be a canonical git remote URL (e.g. https://github.com/owner/repo) or a UUID identifying a local repository.',
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1529
1602
|
export async function createCube(
|
|
1530
1603
|
name: string | undefined,
|
|
1531
1604
|
cubeDirective: string,
|
|
1532
|
-
opts?: {
|
|
1605
|
+
opts?: {
|
|
1606
|
+
template?: string;
|
|
1607
|
+
message_taxonomy?: MessageTaxonomy | null;
|
|
1608
|
+
// client#499: the explicit repository binding (no cwd inference). Required.
|
|
1609
|
+
repository?: CreateCubeRepository;
|
|
1610
|
+
workingRepoName?: string;
|
|
1611
|
+
},
|
|
1533
1612
|
connection?: RemoteConnection,
|
|
1534
|
-
): Promise<{ id: string; name: string; cube_directive?: string; roles: any[]; drones?: any[]; [k: string]: any }> {
|
|
1613
|
+
): Promise<{ result: 'created' | 'resolved'; cube: { id: string; name: string; cube_directive?: string; roles: any[]; drones?: any[]; [k: string]: any } }> {
|
|
1535
1614
|
if (!name?.trim()) throw new Error('Local Borg server cube creation requires a cube name');
|
|
1536
1615
|
if (opts?.template !== undefined && opts.template !== 'default') {
|
|
1537
1616
|
throw new Error('Local Borg server supports only the default cube seed');
|
|
1538
1617
|
}
|
|
1618
|
+
if (!opts?.repository || !opts?.workingRepoName) {
|
|
1619
|
+
throw new Error('Local Borg server cube creation requires an explicit repository identity');
|
|
1620
|
+
}
|
|
1539
1621
|
const resolved = await localOwnerConnection(connection);
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1622
|
+
// client#499 CR: strictly decode the response against the shared
|
|
1623
|
+
// CreateCubeResponse contract — `result` MUST be 'created' or 'resolved'. A
|
|
1624
|
+
// missing/unknown result FAILS CLOSED (throws) rather than falling through to
|
|
1625
|
+
// 'created' and PATCHing an existing cube's directive.
|
|
1626
|
+
const created = decodeCreateCubeResponse(await localConnectionMutation<unknown>(resolved, '/api/cubes', 'POST', {
|
|
1545
1627
|
retry_key: randomUUID(),
|
|
1546
1628
|
name: name.trim(),
|
|
1629
|
+
working_repo_name: opts.workingRepoName,
|
|
1630
|
+
repository: opts.repository,
|
|
1547
1631
|
template: 'default',
|
|
1548
|
-
});
|
|
1549
|
-
|
|
1632
|
+
}));
|
|
1633
|
+
// client#499: the server homes one cube per repository. A 'resolved' result
|
|
1634
|
+
// means this repository already has a cube — report it honestly and DO NOT
|
|
1635
|
+
// PATCH its directive over the existing settings (the round-1 stomp defect).
|
|
1636
|
+
if (created.result === 'resolved') {
|
|
1637
|
+
return { result: 'resolved', cube: await getCube(created.cube_id, resolved) };
|
|
1638
|
+
}
|
|
1550
1639
|
const patch: Record<string, unknown> = { cube_directive: cubeDirective };
|
|
1551
1640
|
if (opts?.message_taxonomy !== undefined) patch.message_taxonomy = opts.message_taxonomy;
|
|
1552
1641
|
await localConnectionMutation(resolved, `/api/cubes/${created.cube_id}`, 'PATCH', patch);
|
|
1553
|
-
return getCube(created.cube_id, resolved);
|
|
1642
|
+
return { result: 'created', cube: await getCube(created.cube_id, resolved) };
|
|
1554
1643
|
}
|
|
1555
1644
|
|
|
1556
1645
|
/**
|
package/src/tool-manifest.ts
CHANGED
|
@@ -452,7 +452,7 @@ const BASE_TOOL_MANIFEST: ToolManifestEntry[] = [
|
|
|
452
452
|
{
|
|
453
453
|
name: 'borg_create-cube',
|
|
454
454
|
description:
|
|
455
|
-
'Create a new cube. The server seeds a default "Drone" role atomically so
|
|
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 a default "Drone" role atomically so a newly-created cube is assimilatable immediately. ' +
|
|
456
456
|
'Pass an optional `template` name to apply a richer role set instead (see borg_list-templates / borg_apply-template).',
|
|
457
457
|
inputSchema: {
|
|
458
458
|
type: 'object',
|
|
@@ -464,12 +464,22 @@ const BASE_TOOL_MANIFEST: ToolManifestEntry[] = [
|
|
|
464
464
|
maxLength: 120,
|
|
465
465
|
},
|
|
466
466
|
cube_directive: { type: 'string', description: 'Project-specific Markdown shown to every drone when it refreshes cube context.' },
|
|
467
|
+
repository: {
|
|
468
|
+
type: 'string',
|
|
469
|
+
description: 'The repository this cube binds to (explicit — not inferred from the working directory). Pass a canonical git remote URL (e.g. https://github.com/owner/repo) for a hosted repository, or a UUID identifying a local (no-remote) repository. The cube is homed to this repository; if it already has one, that existing cube is reported and its directive is left unchanged.',
|
|
470
|
+
},
|
|
471
|
+
working_repo_name: {
|
|
472
|
+
type: 'string',
|
|
473
|
+
description: 'Optional short display name for the repository (starts with a letter or digit; letters, digits, spaces, dots, underscores, or hyphens; max 120 bytes). Defaults to the repository segment of the URL; required when `repository` is a local UUID.',
|
|
474
|
+
pattern: '^[A-Za-z0-9][A-Za-z0-9 ._-]*$',
|
|
475
|
+
maxLength: 120,
|
|
476
|
+
},
|
|
467
477
|
template: {
|
|
468
478
|
type: 'string',
|
|
469
|
-
description: 'Optional template name to apply after cube creation (e.g. "software-dev"). Roles are merged by name; the default Drone role gets overwritten by the template if a same-named role is in the template.',
|
|
479
|
+
description: 'Optional template name to apply after cube creation (e.g. "software-dev"). Roles are merged by name; the default Drone role gets overwritten by the template if a same-named role is in the template. Only applied when a cube is newly created — never to an already-existing repository cube.',
|
|
470
480
|
},
|
|
471
481
|
},
|
|
472
|
-
required: ['name', 'cube_directive'],
|
|
482
|
+
required: ['name', 'cube_directive', 'repository'],
|
|
473
483
|
},
|
|
474
484
|
},
|
|
475
485
|
{
|
|
@@ -1063,11 +1073,14 @@ export const TOOL_OUTPUT_SCHEMAS: Record<string, OutputSchema> = {
|
|
|
1063
1073
|
type: 'object',
|
|
1064
1074
|
properties: {
|
|
1065
1075
|
cube: CUBE_OUTPUT,
|
|
1076
|
+
// client#499: 'created' = a new cube; 'resolved' = the repository already
|
|
1077
|
+
// had a cube (reported, directive left unchanged).
|
|
1078
|
+
result: { type: 'string', enum: ['created', 'resolved'] },
|
|
1066
1079
|
template: { type: ['string', 'null'] },
|
|
1067
1080
|
roles_created: { type: ['number', 'null'] },
|
|
1068
1081
|
roles_updated: { type: ['number', 'null'] },
|
|
1069
1082
|
},
|
|
1070
|
-
required: ['cube', 'template'],
|
|
1083
|
+
required: ['cube', 'result', 'template'],
|
|
1071
1084
|
},
|
|
1072
1085
|
'borg_update-cube': {
|
|
1073
1086
|
type: 'object',
|