borgmcp 4.6.2 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +7 -0
  2. package/dist/assimilate-cmd.d.ts +8 -23
  3. package/dist/assimilate-cmd.d.ts.map +1 -1
  4. package/dist/assimilate-cmd.js +8 -5
  5. package/dist/assimilate-cmd.js.map +1 -1
  6. package/dist/assimilate-deps.d.ts.map +1 -1
  7. package/dist/assimilate-deps.js +1 -2
  8. package/dist/assimilate-deps.js.map +1 -1
  9. package/dist/cli-help.d.ts.map +1 -1
  10. package/dist/cli-help.js +1 -0
  11. package/dist/cli-help.js.map +1 -1
  12. package/dist/codex-launch.d.ts +1 -1
  13. package/dist/codex-launch.d.ts.map +1 -1
  14. package/dist/codex-launch.js.map +1 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +55 -18
  17. package/dist/config.js.map +1 -1
  18. package/dist/cubes.d.ts +0 -7
  19. package/dist/cubes.d.ts.map +1 -1
  20. package/dist/cubes.js +0 -9
  21. package/dist/cubes.js.map +1 -1
  22. package/dist/docs-sections.js +1 -1
  23. package/dist/docs-sections.js.map +1 -1
  24. package/dist/gc-orphan-inboxes.js.map +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +36 -52
  27. package/dist/index.js.map +1 -1
  28. package/dist/log-provenance.d.ts +20 -0
  29. package/dist/log-provenance.d.ts.map +1 -0
  30. package/dist/log-provenance.js +96 -0
  31. package/dist/log-provenance.js.map +1 -0
  32. package/dist/log-stream.d.ts +6 -16
  33. package/dist/log-stream.d.ts.map +1 -1
  34. package/dist/log-stream.js.map +1 -1
  35. package/dist/quickstart-cmd.d.ts.map +1 -1
  36. package/dist/quickstart-cmd.js +9 -2
  37. package/dist/quickstart-cmd.js.map +1 -1
  38. package/dist/regen-format.d.ts +1 -2
  39. package/dist/regen-format.d.ts.map +1 -1
  40. package/dist/regen-format.js +4 -3
  41. package/dist/regen-format.js.map +1 -1
  42. package/dist/regen.js +1 -1
  43. package/dist/regen.js.map +1 -1
  44. package/dist/remote-client.d.ts.map +1 -1
  45. package/dist/remote-client.js +8 -8
  46. package/dist/remote-client.js.map +1 -1
  47. package/dist/repository-cube-init.d.ts +1 -1
  48. package/dist/repository-cube-init.d.ts.map +1 -1
  49. package/dist/repository-cube-init.js +2 -5
  50. package/dist/repository-cube-init.js.map +1 -1
  51. package/dist/repository-identity.d.ts +14 -4
  52. package/dist/repository-identity.d.ts.map +1 -1
  53. package/dist/repository-identity.js +86 -18
  54. package/dist/repository-identity.js.map +1 -1
  55. package/dist/self-path.d.ts.map +1 -1
  56. package/dist/self-path.js.map +1 -1
  57. package/dist/server-handshake.d.ts +1 -1
  58. package/dist/server-handshake.d.ts.map +1 -1
  59. package/dist/server-handshake.js +0 -1
  60. package/dist/server-handshake.js.map +1 -1
  61. package/dist/tool-manifest.d.ts.map +1 -1
  62. package/dist/tool-manifest.js +12 -4
  63. package/dist/tool-manifest.js.map +1 -1
  64. package/dist/unknown-subcommand.d.ts +1 -1
  65. package/dist/unknown-subcommand.d.ts.map +1 -1
  66. package/dist/unknown-subcommand.js +1 -0
  67. package/dist/unknown-subcommand.js.map +1 -1
  68. package/dist/update-cmd.js +1 -1
  69. package/dist/update-cmd.js.map +1 -1
  70. package/docs/RELEASING.md +5 -4
  71. package/package.json +2 -2
  72. package/src/assimilate-cmd.ts +28 -37
  73. package/src/assimilate-deps.ts +0 -4
  74. package/src/cli-help.ts +1 -0
  75. package/src/codex-launch.ts +2 -1
  76. package/src/config.ts +58 -22
  77. package/src/cubes.ts +0 -10
  78. package/src/docs-sections.ts +1 -1
  79. package/src/gc-orphan-inboxes.ts +1 -1
  80. package/src/index.ts +41 -56
  81. package/src/log-provenance.ts +119 -0
  82. package/src/log-stream.ts +12 -17
  83. package/src/quickstart-cmd.ts +10 -2
  84. package/src/regen-format.ts +5 -6
  85. package/src/regen.ts +2 -3
  86. package/src/remote-client.ts +12 -9
  87. package/src/repository-cube-init.ts +3 -6
  88. package/src/repository-identity.ts +125 -20
  89. package/src/self-path.ts +0 -1
  90. package/src/server-handshake.ts +1 -2
  91. package/src/tool-manifest.ts +12 -4
  92. package/src/unknown-subcommand.ts +1 -0
  93. package/src/update-cmd.ts +1 -1
  94. package/dist/postinstall-banner.d.ts +0 -2
  95. package/dist/postinstall-banner.d.ts.map +0 -1
  96. package/dist/postinstall-banner.js +0 -27
  97. package/dist/postinstall-banner.js.map +0 -1
  98. package/dist/postinstall.d.ts +0 -11
  99. package/dist/postinstall.d.ts.map +0 -1
  100. package/dist/postinstall.js +0 -33
  101. package/dist/postinstall.js.map +0 -1
  102. package/dist/types.d.ts +0 -5
  103. package/dist/types.d.ts.map +0 -1
  104. package/dist/types.js +0 -5
  105. package/dist/types.js.map +0 -1
  106. package/src/postinstall-banner.ts +0 -37
  107. package/src/postinstall.ts +0 -36
  108. package/src/types.ts +0 -5
@@ -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 { getTemplate, type Template, type TemplateRole } from 'borgmcp-shared/templates';
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 a default "Drone" role atomically
1525
- * so the cube is assimilatable immediately, OR applies the named template
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
- if (opts?.template !== undefined && opts.template !== 'default') {
1616
- throw new Error('Local Borg server supports only the default cube seed');
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: 'default',
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
- LEGACY_DEFAULT_TEMPLATE_LABEL,
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 = Exclude<CubeTemplate, 'default'>;
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
- if (template === 'default') {
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[]) => { status: number | null; stdout?: string | null };
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, { cwd, encoding: 'utf8' });
47
- return { status: result.status, stdout: result.stdout };
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: { status: number | null; stdout?: string | null }): string | null {
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 rootRaw = output(runGit(cwd, ['rev-parse', '--show-toplevel']));
62
- if (!rootRaw || !isAbsolute(rootRaw)) return null;
63
- const bare = output(runGit(cwd, ['rev-parse', '--is-bare-repository']));
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
- const commonRaw = output(runGit(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir']));
66
- if (!commonRaw || !isAbsolute(commonRaw)) return null;
67
- const root = await canonicalPath(rootRaw);
68
- const commonDir = await canonicalPath(commonRaw);
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 originRaw = output(runGit(root, ['config', '--get', 'remote.origin.url']));
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
- for (const [key, value] of Object.entries(parsed.associations)) {
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 state = parseState(await readStoreFile(statePath, options));
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
@@ -9,7 +9,6 @@
9
9
  * under `dist/`. All bin targets are siblings in the same `dist/` directory.
10
10
  */
11
11
 
12
- import fs from 'fs';
13
12
  import path from 'path';
14
13
  import { fileURLToPath } from 'url';
15
14
 
@@ -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 v12.
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
@@ -416,7 +416,7 @@ const BASE_TOOL_MANIFEST: ToolManifestEntry[] = [
416
416
  {
417
417
  name: 'borg_log',
418
418
  description:
419
- 'Append a message to the cube\'s activity log with an explicit audience. Every call must pass `to: "broadcast"` for all drones or a non-empty selector array for direct delivery. Selectors accept an exact drone label, drone id, the stable 8-hex `id:` token shown in roster/read-log, role name, or role slug. Message text and taxonomy classes never choose the audience; optional `class` records classification/lifecycle metadata only. Cite durable cube documents through `documents`; citations carry current metadata but do not inline document content. Direct routing controls delivery and wakes, not read confidentiality inside the cube.',
419
+ 'Append a message to the cube\'s activity log with an explicit audience. Every call must pass `to: "broadcast"` for all drones or a non-empty selector array for direct delivery. Selectors accept an exact drone label, drone id, the stable 8-hex `id:` token shown in roster/read-log, role name, or role slug. Pass local Git refs through `refs` to append their mechanically resolved commit SHAs; REVIEW-READY messages require refs. Message text and taxonomy classes never choose the audience; optional `class` records classification/lifecycle metadata only. Cite durable cube documents through `documents`; citations carry current metadata but do not inline document content. Direct routing controls delivery and wakes, not read confidentiality inside the cube.',
420
420
  inputSchema: {
421
421
  type: 'object',
422
422
  properties: {
@@ -440,6 +440,14 @@ const BASE_TOOL_MANIFEST: ToolManifestEntry[] = [
440
440
  uniqueItems: true,
441
441
  description: 'Optional full opaque ids of 1-100 same-cube documents to cite atomically. Unknown, duplicate, or foreign ids are refused.',
442
442
  },
443
+ refs: {
444
+ type: 'array',
445
+ items: { type: 'string' },
446
+ minItems: 1,
447
+ maxItems: 8,
448
+ uniqueItems: true,
449
+ description: 'Optional local Git refs to resolve mechanically and append as `<ref> = <40-hex>` provenance. Required for REVIEW-READY messages.',
450
+ },
443
451
  },
444
452
  required: ['message', 'to'],
445
453
  },
@@ -452,8 +460,8 @@ const BASE_TOOL_MANIFEST: ToolManifestEntry[] = [
452
460
  {
453
461
  name: 'borg_create-cube',
454
462
  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 a default "Drone" role atomically so a newly-created cube is assimilatable immediately. ' +
456
- 'Pass an optional `template` name to apply a richer role set instead (see borg_list-templates / borg_apply-template).',
463
+ '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. ' +
464
+ 'Pass an optional `template` name to select another role set (see borg_list-templates / borg_apply-template).',
457
465
  inputSchema: {
458
466
  type: 'object',
459
467
  properties: {
@@ -476,7 +484,7 @@ const BASE_TOOL_MANIFEST: ToolManifestEntry[] = [
476
484
  },
477
485
  template: {
478
486
  type: 'string',
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.',
487
+ 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
488
  },
481
489
  },
482
490
  required: ['name', 'cube_directive', 'repository'],
@@ -15,6 +15,7 @@
15
15
  export const KNOWN_SUBCOMMANDS = [
16
16
  'setup',
17
17
  'update',
18
+ 'upgrade',
18
19
  'doctor',
19
20
  'clone',
20
21
  'quickstart',
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,2 +0,0 @@
1
- export declare function composeInstallBanner(hasAgentCli: boolean): string;
2
- //# sourceMappingURL=postinstall-banner.d.ts.map
@@ -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"}
@@ -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
@@ -1 +0,0 @@
1
- {"version":3,"file":"postinstall.d.ts","sourceRoot":"","sources":["../src/postinstall.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG"}
@@ -1,33 +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
- import { detectCliAvailability, installedCliNames } from './cli-platform.js';
11
- import { composeInstallBanner } from './postinstall-banner.js';
12
- // Check if this is a local install (npm_config_global is not set)
13
- const isGlobal = process.env.npm_config_global === 'true';
14
- if (!isGlobal) {
15
- console.error('\n◼ Error: borg must be installed globally\n');
16
- console.error('Please install with:');
17
- console.error(' npm install -g borgmcp\n');
18
- console.error('Local installation is not supported.\n');
19
- process.exit(1);
20
- }
21
- // Global install — show instructions. Detect agent-CLI presence so the banner
22
- // never points a user with no Claude Code / Codex at `borg setup` (which would
23
- // dead-end). On any detection error, default to assuming an agent CLI is
24
- // present so we never emit a false "install an agent CLI" warning.
25
- let hasAgentCli = true;
26
- try {
27
- hasAgentCli = installedCliNames(detectCliAvailability()).length > 0;
28
- }
29
- catch {
30
- hasAgentCli = true;
31
- }
32
- console.log(composeInstallBanner(hasAgentCli));
33
- //# sourceMappingURL=postinstall.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"postinstall.js","sourceRoot":"","sources":["../src/postinstall.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG;AAEH,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,kEAAkE;AAClE,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,KAAK,MAAM,CAAC;AAE1D,IAAI,CAAC,QAAQ,EAAE,CAAC;IACd,OAAO,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAC9D,OAAO,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;IACtC,OAAO,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAC5C,OAAO,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAC;IACxD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,8EAA8E;AAC9E,+EAA+E;AAC/E,yEAAyE;AACzE,mEAAmE;AACnE,IAAI,WAAW,GAAG,IAAI,CAAC;AACvB,IAAI,CAAC;IACH,WAAW,GAAG,iBAAiB,CAAC,qBAAqB,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;AACtE,CAAC;AAAC,MAAM,CAAC;IACP,WAAW,GAAG,IAAI,CAAC;AACrB,CAAC;AAED,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,WAAW,CAAC,CAAC,CAAC"}
package/dist/types.d.ts DELETED
@@ -1,5 +0,0 @@
1
- /**
2
- * Type definitions for Borg MCP Client
3
- */
4
- export {};
5
- //# sourceMappingURL=types.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC"}
package/dist/types.js DELETED
@@ -1,5 +0,0 @@
1
- /**
2
- * Type definitions for Borg MCP Client
3
- */
4
- export {};
5
- //# sourceMappingURL=types.js.map
package/dist/types.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -1,37 +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
-
12
- export function composeInstallBanner(hasAgentCli: boolean): string {
13
- const lines = [
14
- '',
15
- '╔════════════════════════════════════╗',
16
- '║ ◼ Borg MCP Installed ◼ ║',
17
- '╚════════════════════════════════════╝',
18
- '',
19
- ];
20
-
21
- if (!hasAgentCli) {
22
- lines.push(
23
- '⚠ No agent CLI detected. Borg runs on top of Claude Code or Codex —',
24
- ' install one first:',
25
- ' Claude Code: https://claude.ai/download',
26
- ' Codex: https://developers.openai.com/codex',
27
- '',
28
- 'Then run:',
29
- ' borg setup',
30
- '',
31
- );
32
- } else {
33
- lines.push('Next step:', ' borg setup', '');
34
- }
35
-
36
- return lines.join('\n');
37
- }
@@ -1,36 +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
-
11
- import { detectCliAvailability, installedCliNames } from './cli-platform.js';
12
- import { composeInstallBanner } from './postinstall-banner.js';
13
-
14
- // Check if this is a local install (npm_config_global is not set)
15
- const isGlobal = process.env.npm_config_global === 'true';
16
-
17
- if (!isGlobal) {
18
- console.error('\n◼ Error: borg must be installed globally\n');
19
- console.error('Please install with:');
20
- console.error(' npm install -g borgmcp\n');
21
- console.error('Local installation is not supported.\n');
22
- process.exit(1);
23
- }
24
-
25
- // Global install — show instructions. Detect agent-CLI presence so the banner
26
- // never points a user with no Claude Code / Codex at `borg setup` (which would
27
- // dead-end). On any detection error, default to assuming an agent CLI is
28
- // present so we never emit a false "install an agent CLI" warning.
29
- let hasAgentCli = true;
30
- try {
31
- hasAgentCli = installedCliNames(detectCliAvailability()).length > 0;
32
- } catch {
33
- hasAgentCli = true;
34
- }
35
-
36
- console.log(composeInstallBanner(hasAgentCli));
package/src/types.ts DELETED
@@ -1,5 +0,0 @@
1
- /**
2
- * Type definitions for Borg MCP Client
3
- */
4
-
5
- export {};