borgmcp 2.0.9 → 2.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +2 -2
  2. package/SECURITY.md +1 -1
  3. package/dist/agent-runtime.d.ts +2 -0
  4. package/dist/agent-runtime.d.ts.map +1 -1
  5. package/dist/agent-runtime.js +5 -1
  6. package/dist/agent-runtime.js.map +1 -1
  7. package/dist/assimilate-cmd.d.ts +2 -0
  8. package/dist/assimilate-cmd.d.ts.map +1 -1
  9. package/dist/assimilate-cmd.js +2 -0
  10. package/dist/assimilate-cmd.js.map +1 -1
  11. package/dist/assimilate-deps.d.ts.map +1 -1
  12. package/dist/assimilate-deps.js +6 -0
  13. package/dist/assimilate-deps.js.map +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +18 -17
  16. package/dist/index.js.map +1 -1
  17. package/dist/local-manage-tool-result.d.ts +10 -0
  18. package/dist/local-manage-tool-result.d.ts.map +1 -1
  19. package/dist/local-manage-tool-result.js +36 -0
  20. package/dist/local-manage-tool-result.js.map +1 -1
  21. package/dist/regen-format.d.ts.map +1 -1
  22. package/dist/regen-format.js +6 -4
  23. package/dist/regen-format.js.map +1 -1
  24. package/dist/regen.js +2 -0
  25. package/dist/regen.js.map +1 -1
  26. package/dist/remote-client.d.ts +18 -6
  27. package/dist/remote-client.d.ts.map +1 -1
  28. package/dist/remote-client.js +99 -16
  29. package/dist/remote-client.js.map +1 -1
  30. package/dist/roster-render.d.ts +11 -0
  31. package/dist/roster-render.d.ts.map +1 -1
  32. package/dist/roster-render.js +41 -8
  33. package/dist/roster-render.js.map +1 -1
  34. package/dist/runtime-metadata.d.ts +13 -0
  35. package/dist/runtime-metadata.d.ts.map +1 -0
  36. package/dist/runtime-metadata.js +52 -0
  37. package/dist/runtime-metadata.js.map +1 -0
  38. package/dist/server-handshake.d.ts +2 -1
  39. package/dist/server-handshake.d.ts.map +1 -1
  40. package/dist/server-handshake.js +3 -0
  41. package/dist/server-handshake.js.map +1 -1
  42. package/dist/tool-manifest.js +2 -2
  43. package/dist/tool-manifest.js.map +1 -1
  44. package/dist/working-repo.d.ts +5 -7
  45. package/dist/working-repo.d.ts.map +1 -1
  46. package/dist/working-repo.js +30 -40
  47. package/dist/working-repo.js.map +1 -1
  48. package/docs/EXTRACTION_PROVENANCE.md +8 -8
  49. package/docs/LOCAL_SERVER.md +1 -1
  50. package/docs/RELEASING.md +25 -13
  51. package/package.json +2 -2
  52. package/src/agent-runtime.ts +8 -1
  53. package/src/assimilate-cmd.ts +3 -1
  54. package/src/assimilate-deps.ts +10 -4
  55. package/src/index.ts +26 -21
  56. package/src/local-manage-tool-result.ts +47 -0
  57. package/src/regen-format.ts +12 -5
  58. package/src/regen.ts +2 -0
  59. package/src/remote-client.ts +124 -24
  60. package/src/roster-render.ts +58 -8
  61. package/src/runtime-metadata.ts +67 -0
  62. package/src/server-handshake.ts +7 -2
  63. package/src/tool-manifest.ts +2 -2
  64. package/src/working-repo.ts +30 -41
@@ -0,0 +1,67 @@
1
+ import type {
2
+ AgentKind,
3
+ DroneRuntimeMetadata,
4
+ DroneRuntimeMetadataPatch,
5
+ } from 'borgmcp-shared/protocol';
6
+ import {
7
+ canonicalizeRepositoryIdentity,
8
+ validateReportedModel,
9
+ validateRuntimeMetadata,
10
+ validateRuntimeMetadataPatch,
11
+ } from 'borgmcp-shared/runtime-metadata';
12
+ import type { WorkingRepo } from './working-repo.js';
13
+
14
+ function safeReportedModel(value: string | null | undefined): string | null {
15
+ if (value == null) return null;
16
+ try {
17
+ return validateReportedModel(value);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ function reportableRepository(repo: WorkingRepo | undefined) {
24
+ if (!repo || repo.state === 'unavailable' || repo.state === 'rejected') return null;
25
+ if (repo.name === null && repo.origin === null) {
26
+ return { working_repo_name: null, working_repo_origin: null };
27
+ }
28
+ if (repo.name === null || repo.origin === null) return null;
29
+ try {
30
+ const canonical = canonicalizeRepositoryIdentity(repo.origin, repo.name);
31
+ return {
32
+ working_repo_name: canonical.working_repo_name,
33
+ working_repo_origin: canonical.working_repo_origin,
34
+ };
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ export function buildRuntimeMetadataReport(input: {
41
+ agentKind: AgentKind | null | undefined;
42
+ reportedModel?: string | null;
43
+ workingRepo?: WorkingRepo;
44
+ }): DroneRuntimeMetadata {
45
+ const repository = reportableRepository(input.workingRepo);
46
+ return validateRuntimeMetadata({
47
+ agent_kind: input.agentKind ?? null,
48
+ reported_model: safeReportedModel(input.reportedModel),
49
+ working_repo_name: repository?.working_repo_name ?? null,
50
+ working_repo_origin: repository?.working_repo_origin ?? null,
51
+ });
52
+ }
53
+
54
+ export function buildRuntimeMetadataPatch(input: {
55
+ agentKind: AgentKind | null;
56
+ reportedModel?: string;
57
+ workingRepo?: WorkingRepo;
58
+ }): DroneRuntimeMetadataPatch {
59
+ const patch: DroneRuntimeMetadataPatch = { agent_kind: input.agentKind };
60
+ if (input.reportedModel !== undefined) {
61
+ const model = safeReportedModel(input.reportedModel);
62
+ if (model !== null) patch.reported_model = model;
63
+ }
64
+ const repository = reportableRepository(input.workingRepo);
65
+ if (repository) Object.assign(patch, repository);
66
+ return validateRuntimeMetadataPatch(patch);
67
+ }
@@ -15,6 +15,7 @@ import {
15
15
  decodeProtocolTagPreflight,
16
16
  ErrorCode,
17
17
  type CreateCubeResponse,
18
+ type DroneRuntimeMetadata,
18
19
  type ProtocolTagPreflight,
19
20
  type ServerCapability,
20
21
  } from 'borgmcp-shared/protocol';
@@ -235,6 +236,7 @@ export async function sendBorgServerAttach(
235
236
  roleId: string;
236
237
  operation: ServerSessionOperation;
237
238
  priorDroneId?: string;
239
+ runtimeMetadata?: DroneRuntimeMetadata;
238
240
  },
239
241
  pendingBearer: string,
240
242
  deps: {
@@ -274,9 +276,12 @@ export async function sendBorgServerAttach(
274
276
  cube_id: request.cubeId,
275
277
  role_id: request.roleId,
276
278
  session_credential: pending.credential,
277
- ...(request.priorDroneId === undefined
279
+ ...(request.priorDroneId === undefined
280
+ ? {}
281
+ : { prior_drone_id: request.priorDroneId }),
282
+ ...(request.runtimeMetadata === undefined
278
283
  ? {}
279
- : { prior_drone_id: request.priorDroneId }),
284
+ : { runtime_metadata: request.runtimeMetadata }),
280
285
  })),
281
286
  });
282
287
  } catch (error) {
@@ -551,7 +551,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
551
551
  {
552
552
  name: 'borg_apply-template',
553
553
  description:
554
- 'Apply a named template to an existing cube, NON-CLOBBERINGLY. Roles are merged by name: new roles are created; existing template-named roles get template sections/classes the cube LACKS auto-applied, but EVOLVED (conflicting) text is preserved, never overwritten. Use this to retrofit an existing cube with a richer role set (e.g. add Coordinator/Reviewer/UX Expert). To review + selectively accept conflicting fragments, use borg_sync-roles (which surfaces each conflict + takes per-fragment accept decisions).',
554
+ 'A client-orchestrated, non-clobbering application of a named template through role and taxonomy primitives. Requires the selected local client\'s live cube-manage grant. Roles are merged by name: new roles are created; existing template-named roles get missing sections/classes applied, while conflicting text is preserved. Operations are sequential, not atomic: if a later primitive fails, earlier operations may already be committed. Use borg_sync-roles to review and selectively accept conflicts.',
555
555
  inputSchema: {
556
556
  type: 'object',
557
557
  properties: {
@@ -564,7 +564,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
564
564
  {
565
565
  name: 'borg_sync-roles',
566
566
  description:
567
- 'Non-clobbering sync of a cube\'s roles + message_taxonomy against the built-in template. Dry-run (default) classifies each fragment (role-text section, short_description, role flags, taxonomy class) as ADD (cube lacks it — safe auto-apply), UNCHANGED, or CONFLICT (cube has EVOLVED text). On apply, ADDs auto-apply; CONFLICTs apply ONLY via an explicit `decisions` accept (keyed on the dry-run fragment key, e.g. `role:Builder:section:Workflow`); unspecified conflicts default to reject evolved text is NEVER silently overwritten. Custom roles untouched.',
567
+ 'A client-orchestrated, non-clobbering sync through role and taxonomy primitives. Requires the selected local client\'s live cube-manage grant. Dry-run (default) classifies each fragment as ADD, UNCHANGED, or CONFLICT. On apply, ADDs apply automatically; conflicts apply only through an explicit `decisions` accept, and unspecified conflicts remain unchanged. Operations are sequential, not atomic: if a later primitive fails, earlier operations may already be committed. Custom roles remain untouched.',
568
568
  inputSchema: {
569
569
  type: 'object',
570
570
  properties: {
@@ -7,10 +7,12 @@
7
7
  */
8
8
 
9
9
  import { spawnSync } from 'node:child_process';
10
+ import { canonicalizeRepositoryIdentity } from 'borgmcp-shared/runtime-metadata';
10
11
  export interface WorkingRepo {
11
12
  name: string | null;
12
- /** Canonical host/path identity, never a raw Git remote URL. */
13
+ /** Canonical public HTTPS identity, never a raw Git remote URL. */
13
14
  origin: string | null;
15
+ state?: 'known' | 'unknown' | 'unavailable' | 'rejected';
14
16
  }
15
17
 
16
18
  export interface WorkingRepoDeps {
@@ -30,43 +32,19 @@ function trimmed(value: string | null | undefined): string | null {
30
32
  return normalized ? normalized : null;
31
33
  }
32
34
 
33
- function nameFromIdentity(identity: string): string | null {
34
- const lastPathSegment = identity.replace(/\/$/, '').split('/').pop();
35
- const name = lastPathSegment?.replace(/\.git$/i, '').trim();
36
- return name || null;
37
- }
38
-
39
35
  /**
40
- * Convert a Git remote to a non-secret `host/org/repo` identity.
41
- *
42
- * URL userinfo, query strings, fragments, scheme, and SCP-style user prefixes
43
- * are deliberately discarded. Inputs that cannot identify a host and path are
44
- * treated as unreportable rather than forwarded verbatim.
36
+ * Convert a Git remote to the shared canonical public repository identity.
37
+ * Hostile or credential-bearing inputs are rejected rather than sanitized.
45
38
  */
46
- export function canonicalizeWorkingRepoIdentity(origin: string): string | null {
47
- const raw = origin.trim();
48
- if (!raw) return null;
49
- // Remote clients send this canonical form on subsequent lifecycle calls.
50
- const canonical = raw.match(/^([A-Za-z0-9.-]+)\/([^?#\s]+)$/);
51
- if (canonical) {
52
- const host = canonical[1].toLowerCase();
53
- const path = canonical[2].replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '');
54
- return host && path ? `${host}/${path}` : null;
55
- }
39
+ export function canonicalizeWorkingRepoIdentity(origin: string): WorkingRepo | null {
56
40
  try {
57
- const url = new URL(origin);
58
- if (!['http:', 'https:', 'ssh:', 'git:'].includes(url.protocol)) return null;
59
- const path = url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '');
60
- return url.hostname && path ? `${url.hostname.toLowerCase()}/${path}` : null;
41
+ const canonical = canonicalizeRepositoryIdentity(origin.trim());
42
+ return {
43
+ name: canonical.working_repo_name,
44
+ origin: canonical.working_repo_origin,
45
+ state: 'known',
46
+ };
61
47
  } catch {
62
- // SCP-style SSH remote: discard its optional user prefix and URL-like
63
- // query/fragment suffix before accepting only host + repository path.
64
- const match = raw.match(/^(?:[^@\s/:]+@)?([A-Za-z0-9.-]+):\/?([^?#\s]+)(?:[?#].*)?$/);
65
- if (match) {
66
- const host = match[1].toLowerCase();
67
- const path = match[2].replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '');
68
- return host && path ? `${host}/${path}` : null;
69
- }
70
48
  return null;
71
49
  }
72
50
  }
@@ -83,17 +61,28 @@ export function resolveWorkingRepo(
83
61
  deps: WorkingRepoDeps = {}
84
62
  ): WorkingRepo {
85
63
  const runGit = deps.runGit ?? defaultRunGit;
86
- const rootResult = runGit(cwd, ['rev-parse', '--show-toplevel']);
64
+ let rootResult;
65
+ try {
66
+ rootResult = runGit(cwd, ['rev-parse', '--show-toplevel']);
67
+ } catch {
68
+ return { name: null, origin: null, state: 'unavailable' };
69
+ }
87
70
  const root = rootResult.status === 0 ? trimmed(rootResult.stdout) : null;
88
71
  if (!root) {
89
- return { name: null, origin: null };
72
+ return { name: null, origin: null, state: 'unknown' };
90
73
  }
91
74
 
92
- const originResult = runGit(cwd, ['config', '--get', 'remote.origin.url']);
75
+ let originResult;
76
+ try {
77
+ originResult = runGit(cwd, ['config', '--get', 'remote.origin.url']);
78
+ } catch {
79
+ return { name: null, origin: null, state: 'unavailable' };
80
+ }
93
81
  const originRaw = originResult.status === 0 ? trimmed(originResult.stdout) : null;
94
- const origin = originRaw ? canonicalizeWorkingRepoIdentity(originRaw) : null;
95
- return {
96
- name: origin ? nameFromIdentity(origin) : null,
97
- origin,
82
+ if (!originRaw) return { name: null, origin: null, state: 'unknown' };
83
+ return canonicalizeWorkingRepoIdentity(originRaw) ?? {
84
+ name: null,
85
+ origin: null,
86
+ state: 'rejected',
98
87
  };
99
88
  }