brainclaw 0.19.10 → 0.19.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/cli.js +1 -0
- package/dist/commands/bootstrap.js +17 -1
- package/dist/commands/env.js +7 -1
- package/dist/commands/init.js +1 -0
- package/dist/commands/mcp.js +76 -3
- package/dist/commands/version.js +3 -1
- package/dist/commands/whoami.js +11 -1
- package/dist/core/bootstrap.js +416 -60
- package/dist/core/brainclaw-version.js +303 -62
- package/dist/core/schema.js +28 -0
- package/docs/cli.md +15 -1
- package/docs/integrations/mcp.md +29 -7
- package/docs/mcp-schema-changelog.md +6 -0
- package/docs/quickstart.md +22 -1
- package/package.json +1 -1
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { BrainclawLocalReleaseManifestSchema, } from './schema.js';
|
|
6
7
|
export const DEFAULT_LOCAL_RELEASES_DIR = '.releases';
|
|
7
8
|
export const DEFAULT_LOCAL_RELEASE_MANIFEST_PATH = `${DEFAULT_LOCAL_RELEASES_DIR}/brainclaw-local.json`;
|
|
9
|
+
export const DEFAULT_NPM_UPDATE_PACKAGE = 'brainclaw';
|
|
10
|
+
export const DEFAULT_NPM_UPDATE_DIST_TAG = 'latest';
|
|
11
|
+
export const DEFAULT_INSTALLABLE_UPDATE_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
8
12
|
const SEMVER_RE = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
13
|
+
const DEFAULT_NPM_UPDATE_SOURCE = {
|
|
14
|
+
type: 'npm',
|
|
15
|
+
package_name: DEFAULT_NPM_UPDATE_PACKAGE,
|
|
16
|
+
dist_tag: DEFAULT_NPM_UPDATE_DIST_TAG,
|
|
17
|
+
};
|
|
9
18
|
let cachedCliVersion;
|
|
10
19
|
export function getInstalledBrainclawVersion() {
|
|
11
20
|
if (cachedCliVersion) {
|
|
@@ -87,8 +96,10 @@ export function assessBrainclawVersion(config) {
|
|
|
87
96
|
message,
|
|
88
97
|
};
|
|
89
98
|
}
|
|
90
|
-
export function checkBrainclawInstallableUpdate(config, cwd) {
|
|
91
|
-
const source = config?.brainclaw_update_source
|
|
99
|
+
export function checkBrainclawInstallableUpdate(config, cwd, options = {}) {
|
|
100
|
+
const source = config?.brainclaw_update_source
|
|
101
|
+
?? (options.useDefaultNpmSource ? DEFAULT_NPM_UPDATE_SOURCE : undefined);
|
|
102
|
+
const defaultSource = !config?.brainclaw_update_source && source?.type === 'npm';
|
|
92
103
|
if (!source) {
|
|
93
104
|
return {
|
|
94
105
|
checked: false,
|
|
@@ -103,22 +114,181 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
|
|
|
103
114
|
};
|
|
104
115
|
}
|
|
105
116
|
if (source.type === 'npm') {
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
return checkNpmInstallableUpdate(source, config, cwd, options, defaultSource);
|
|
118
|
+
}
|
|
119
|
+
return checkLocalPackInstallableUpdate(source.manifest_path, config, cwd);
|
|
120
|
+
}
|
|
121
|
+
export function renderBrainclawInstallableUpdateNotice(updateCheck) {
|
|
122
|
+
if (!updateCheck || updateCheck.status !== 'update_available') {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const lines = [updateCheck.message];
|
|
126
|
+
if (updateCheck.install_command) {
|
|
127
|
+
lines.push(`Install: ${updateCheck.install_command}`);
|
|
128
|
+
}
|
|
129
|
+
if (updateCheck.release_notes) {
|
|
130
|
+
lines.push(`Why update: ${updateCheck.release_notes}`);
|
|
131
|
+
}
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
134
|
+
export function publishLocalBrainclawRelease(cwd, options = {}) {
|
|
135
|
+
const workspacePackage = readWorkspaceBrainclawPackage(cwd);
|
|
136
|
+
const outputDir = path.resolve(cwd, options.outputDir ?? DEFAULT_LOCAL_RELEASES_DIR);
|
|
137
|
+
const manifestPath = path.resolve(cwd, options.manifestPath ?? DEFAULT_LOCAL_RELEASE_MANIFEST_PATH);
|
|
138
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
139
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
140
|
+
const packResult = spawnSync(resolveNpmCommand(), resolveNpmPackArgs(outputDir), {
|
|
141
|
+
cwd,
|
|
142
|
+
encoding: 'utf-8',
|
|
143
|
+
timeout: 120000,
|
|
144
|
+
});
|
|
145
|
+
if (packResult.error) {
|
|
146
|
+
throw new Error(`Failed to run npm pack: ${packResult.error.message}`);
|
|
147
|
+
}
|
|
148
|
+
if (packResult.status !== 0) {
|
|
149
|
+
throw new Error(firstNonEmptyLine(packResult.stderr) ?? firstNonEmptyLine(packResult.stdout) ?? 'npm pack failed');
|
|
150
|
+
}
|
|
151
|
+
const artifactFilename = parsePackedFilename(packResult.stdout);
|
|
152
|
+
if (!artifactFilename) {
|
|
153
|
+
throw new Error('npm pack did not report the generated tarball filename.');
|
|
154
|
+
}
|
|
155
|
+
const artifactAbsolutePath = path.join(outputDir, artifactFilename);
|
|
156
|
+
if (!fs.existsSync(artifactAbsolutePath)) {
|
|
157
|
+
throw new Error(`npm pack reported ${artifactFilename}, but the tarball was not found in ${outputDir}.`);
|
|
158
|
+
}
|
|
159
|
+
const manifestArtifactPath = toManifestRelativePath(path.relative(path.dirname(manifestPath), artifactAbsolutePath));
|
|
160
|
+
const projectArtifactPath = toManifestRelativePath(path.relative(cwd, artifactAbsolutePath));
|
|
161
|
+
const projectManifestPath = toPortablePath(path.relative(cwd, manifestPath));
|
|
162
|
+
const installCommand = `npm install -g "${projectArtifactPath}"`;
|
|
163
|
+
const releaseNotes = options.releaseNotes?.trim() || null;
|
|
164
|
+
const manifest = BrainclawLocalReleaseManifestSchema.parse({
|
|
165
|
+
version: 1,
|
|
166
|
+
channel: 'local-pack',
|
|
167
|
+
package_name: workspacePackage.name,
|
|
168
|
+
latest_installable_version: workspacePackage.version,
|
|
169
|
+
published_at: new Date().toISOString(),
|
|
170
|
+
artifact_path: manifestArtifactPath,
|
|
171
|
+
install_command: installCommand,
|
|
172
|
+
release_notes: releaseNotes ?? undefined,
|
|
173
|
+
});
|
|
174
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
|
|
175
|
+
return {
|
|
176
|
+
package_name: workspacePackage.name,
|
|
177
|
+
workspace_version: workspacePackage.version,
|
|
178
|
+
manifest_path: projectManifestPath,
|
|
179
|
+
artifact_path: projectArtifactPath,
|
|
180
|
+
install_command: installCommand,
|
|
181
|
+
release_notes: releaseNotes,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function checkNpmInstallableUpdate(source, config, cwd, options, defaultSource) {
|
|
185
|
+
const packageName = source.package_name?.trim() || DEFAULT_NPM_UPDATE_PACKAGE;
|
|
186
|
+
const distTag = source.dist_tag?.trim() || DEFAULT_NPM_UPDATE_DIST_TAG;
|
|
187
|
+
if (packageName.length === 0) {
|
|
108
188
|
return {
|
|
109
189
|
checked: false,
|
|
110
190
|
source_type: 'npm',
|
|
111
|
-
source_description:
|
|
191
|
+
source_description: null,
|
|
112
192
|
latest_installable_version: null,
|
|
113
193
|
artifact_path: null,
|
|
114
194
|
install_command: null,
|
|
115
195
|
release_notes: null,
|
|
116
|
-
status: '
|
|
117
|
-
message: '
|
|
196
|
+
status: 'invalid_config',
|
|
197
|
+
message: 'brainclaw_update_source.package_name must not be empty.',
|
|
198
|
+
default_source: defaultSource,
|
|
118
199
|
};
|
|
119
200
|
}
|
|
120
|
-
|
|
121
|
-
|
|
201
|
+
if (distTag.length === 0) {
|
|
202
|
+
return {
|
|
203
|
+
checked: false,
|
|
204
|
+
source_type: 'npm',
|
|
205
|
+
source_description: packageName,
|
|
206
|
+
latest_installable_version: null,
|
|
207
|
+
artifact_path: null,
|
|
208
|
+
install_command: null,
|
|
209
|
+
release_notes: null,
|
|
210
|
+
status: 'invalid_config',
|
|
211
|
+
message: 'brainclaw_update_source.dist_tag must not be empty.',
|
|
212
|
+
default_source: defaultSource,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const lookup = (options.npmLookup ?? lookupNpmDistTags)(packageName, {
|
|
217
|
+
cwd,
|
|
218
|
+
now: options.now,
|
|
219
|
+
cacheTtlMs: options.cacheTtlMs,
|
|
220
|
+
});
|
|
221
|
+
const latestVersion = normalizeConfiguredVersion(lookup.dist_tags[distTag]);
|
|
222
|
+
if (!latestVersion) {
|
|
223
|
+
return {
|
|
224
|
+
checked: true,
|
|
225
|
+
source_type: 'npm',
|
|
226
|
+
source_description: formatNpmSourceDescription(packageName, distTag, defaultSource),
|
|
227
|
+
latest_installable_version: null,
|
|
228
|
+
artifact_path: null,
|
|
229
|
+
install_command: null,
|
|
230
|
+
release_notes: config?.brainclaw_upgrade_message?.trim() || null,
|
|
231
|
+
status: 'check_failed',
|
|
232
|
+
message: `The npm channel ${packageName}@${distTag} did not resolve to a valid semver release.`,
|
|
233
|
+
checked_at: lookup.checked_at,
|
|
234
|
+
cached: lookup.cached,
|
|
235
|
+
default_source: defaultSource,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const installCommand = config?.brainclaw_upgrade_command?.trim()
|
|
239
|
+
|| `npm install -g ${packageName}@${latestVersion}`;
|
|
240
|
+
const releaseNotes = config?.brainclaw_upgrade_message?.trim() || null;
|
|
241
|
+
const installedVersion = getInstalledBrainclawVersion();
|
|
242
|
+
if (compareVersions(installedVersion, latestVersion) < 0) {
|
|
243
|
+
return {
|
|
244
|
+
checked: true,
|
|
245
|
+
source_type: 'npm',
|
|
246
|
+
source_description: formatNpmSourceDescription(packageName, distTag, defaultSource),
|
|
247
|
+
latest_installable_version: latestVersion,
|
|
248
|
+
artifact_path: null,
|
|
249
|
+
install_command: installCommand,
|
|
250
|
+
release_notes: releaseNotes,
|
|
251
|
+
status: 'update_available',
|
|
252
|
+
message: `A newer installable brainclaw build is available from npm: ${latestVersion} (installed ${installedVersion}).`,
|
|
253
|
+
checked_at: lookup.checked_at,
|
|
254
|
+
cached: lookup.cached,
|
|
255
|
+
default_source: defaultSource,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
checked: true,
|
|
260
|
+
source_type: 'npm',
|
|
261
|
+
source_description: formatNpmSourceDescription(packageName, distTag, defaultSource),
|
|
262
|
+
latest_installable_version: latestVersion,
|
|
263
|
+
artifact_path: null,
|
|
264
|
+
install_command: installCommand,
|
|
265
|
+
release_notes: releaseNotes,
|
|
266
|
+
status: 'up_to_date',
|
|
267
|
+
message: `Installed brainclaw ${installedVersion} is up to date for npm channel ${packageName}@${distTag}.`,
|
|
268
|
+
checked_at: lookup.checked_at,
|
|
269
|
+
cached: lookup.cached,
|
|
270
|
+
default_source: defaultSource,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
275
|
+
return {
|
|
276
|
+
checked: true,
|
|
277
|
+
source_type: 'npm',
|
|
278
|
+
source_description: formatNpmSourceDescription(packageName, distTag, defaultSource),
|
|
279
|
+
latest_installable_version: null,
|
|
280
|
+
artifact_path: null,
|
|
281
|
+
install_command: null,
|
|
282
|
+
release_notes: config?.brainclaw_upgrade_message?.trim() || null,
|
|
283
|
+
status: 'check_failed',
|
|
284
|
+
message: `Failed to check npm installable updates: ${message}`,
|
|
285
|
+
default_source: defaultSource,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function checkLocalPackInstallableUpdate(manifestPath, config, cwd) {
|
|
290
|
+
const trimmedManifestPath = manifestPath.trim();
|
|
291
|
+
if (trimmedManifestPath.length === 0) {
|
|
122
292
|
return {
|
|
123
293
|
checked: false,
|
|
124
294
|
source_type: 'local-pack',
|
|
@@ -129,11 +299,12 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
|
|
|
129
299
|
release_notes: null,
|
|
130
300
|
status: 'invalid_config',
|
|
131
301
|
message: 'brainclaw_update_source.manifest_path must not be empty.',
|
|
302
|
+
default_source: false,
|
|
132
303
|
};
|
|
133
304
|
}
|
|
134
|
-
const resolvedManifestPath = path.isAbsolute(
|
|
135
|
-
?
|
|
136
|
-
: path.resolve(cwd,
|
|
305
|
+
const resolvedManifestPath = path.isAbsolute(trimmedManifestPath)
|
|
306
|
+
? trimmedManifestPath
|
|
307
|
+
: path.resolve(cwd, trimmedManifestPath);
|
|
137
308
|
if (!fs.existsSync(resolvedManifestPath)) {
|
|
138
309
|
return {
|
|
139
310
|
checked: true,
|
|
@@ -145,6 +316,7 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
|
|
|
145
316
|
release_notes: null,
|
|
146
317
|
status: 'check_failed',
|
|
147
318
|
message: `The configured local-pack manifest was not found: ${resolvedManifestPath}`,
|
|
319
|
+
default_source: false,
|
|
148
320
|
};
|
|
149
321
|
}
|
|
150
322
|
try {
|
|
@@ -162,6 +334,7 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
|
|
|
162
334
|
release_notes: manifest.release_notes?.trim() || config?.brainclaw_upgrade_message?.trim() || null,
|
|
163
335
|
status: 'check_failed',
|
|
164
336
|
message: `The local-pack manifest has an invalid latest_installable_version: ${manifest.latest_installable_version}`,
|
|
337
|
+
default_source: false,
|
|
165
338
|
};
|
|
166
339
|
}
|
|
167
340
|
const artifactPath = manifest.artifact_path
|
|
@@ -182,6 +355,7 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
|
|
|
182
355
|
release_notes: releaseNotes,
|
|
183
356
|
status: 'update_available',
|
|
184
357
|
message: `A newer installable brainclaw build is available: ${latestVersion} (installed ${installedVersion}).`,
|
|
358
|
+
default_source: false,
|
|
185
359
|
};
|
|
186
360
|
}
|
|
187
361
|
return {
|
|
@@ -194,6 +368,7 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
|
|
|
194
368
|
release_notes: releaseNotes,
|
|
195
369
|
status: 'up_to_date',
|
|
196
370
|
message: `Installed brainclaw ${installedVersion} is up to date for the configured local-pack channel.`,
|
|
371
|
+
default_source: false,
|
|
197
372
|
};
|
|
198
373
|
}
|
|
199
374
|
catch (error) {
|
|
@@ -208,59 +383,10 @@ export function checkBrainclawInstallableUpdate(config, cwd) {
|
|
|
208
383
|
release_notes: null,
|
|
209
384
|
status: 'check_failed',
|
|
210
385
|
message: `Failed to read the configured local-pack manifest: ${message}`,
|
|
386
|
+
default_source: false,
|
|
211
387
|
};
|
|
212
388
|
}
|
|
213
389
|
}
|
|
214
|
-
export function publishLocalBrainclawRelease(cwd, options = {}) {
|
|
215
|
-
const workspacePackage = readWorkspaceBrainclawPackage(cwd);
|
|
216
|
-
const outputDir = path.resolve(cwd, options.outputDir ?? DEFAULT_LOCAL_RELEASES_DIR);
|
|
217
|
-
const manifestPath = path.resolve(cwd, options.manifestPath ?? DEFAULT_LOCAL_RELEASE_MANIFEST_PATH);
|
|
218
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
219
|
-
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
220
|
-
const packResult = spawnSync(resolveNpmCommand(), resolveNpmPackArgs(outputDir), {
|
|
221
|
-
cwd,
|
|
222
|
-
encoding: 'utf-8',
|
|
223
|
-
timeout: 120000,
|
|
224
|
-
});
|
|
225
|
-
if (packResult.error) {
|
|
226
|
-
throw new Error(`Failed to run npm pack: ${packResult.error.message}`);
|
|
227
|
-
}
|
|
228
|
-
if (packResult.status !== 0) {
|
|
229
|
-
throw new Error(firstNonEmptyLine(packResult.stderr) ?? firstNonEmptyLine(packResult.stdout) ?? 'npm pack failed');
|
|
230
|
-
}
|
|
231
|
-
const artifactFilename = parsePackedFilename(packResult.stdout);
|
|
232
|
-
if (!artifactFilename) {
|
|
233
|
-
throw new Error('npm pack did not report the generated tarball filename.');
|
|
234
|
-
}
|
|
235
|
-
const artifactAbsolutePath = path.join(outputDir, artifactFilename);
|
|
236
|
-
if (!fs.existsSync(artifactAbsolutePath)) {
|
|
237
|
-
throw new Error(`npm pack reported ${artifactFilename}, but the tarball was not found in ${outputDir}.`);
|
|
238
|
-
}
|
|
239
|
-
const manifestArtifactPath = toManifestRelativePath(path.relative(path.dirname(manifestPath), artifactAbsolutePath));
|
|
240
|
-
const projectArtifactPath = toManifestRelativePath(path.relative(cwd, artifactAbsolutePath));
|
|
241
|
-
const projectManifestPath = toPortablePath(path.relative(cwd, manifestPath));
|
|
242
|
-
const installCommand = `npm install -g "${projectArtifactPath}"`;
|
|
243
|
-
const releaseNotes = options.releaseNotes?.trim() || null;
|
|
244
|
-
const manifest = BrainclawLocalReleaseManifestSchema.parse({
|
|
245
|
-
version: 1,
|
|
246
|
-
channel: 'local-pack',
|
|
247
|
-
package_name: workspacePackage.name,
|
|
248
|
-
latest_installable_version: workspacePackage.version,
|
|
249
|
-
published_at: new Date().toISOString(),
|
|
250
|
-
artifact_path: manifestArtifactPath,
|
|
251
|
-
install_command: installCommand,
|
|
252
|
-
release_notes: releaseNotes ?? undefined,
|
|
253
|
-
});
|
|
254
|
-
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
|
|
255
|
-
return {
|
|
256
|
-
package_name: workspacePackage.name,
|
|
257
|
-
workspace_version: workspacePackage.version,
|
|
258
|
-
manifest_path: projectManifestPath,
|
|
259
|
-
artifact_path: projectArtifactPath,
|
|
260
|
-
install_command: installCommand,
|
|
261
|
-
release_notes: releaseNotes,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
390
|
function resolveNpmCommand() {
|
|
265
391
|
return process.platform === 'win32' ? (process.env.ComSpec?.trim() || 'cmd.exe') : 'npm';
|
|
266
392
|
}
|
|
@@ -270,6 +396,12 @@ function resolveNpmPackArgs(outputDir) {
|
|
|
270
396
|
}
|
|
271
397
|
return ['pack', '--json', '--pack-destination', outputDir];
|
|
272
398
|
}
|
|
399
|
+
function resolveNpmViewArgs(packageName) {
|
|
400
|
+
if (process.platform === 'win32') {
|
|
401
|
+
return ['/d', '/s', '/c', 'npm', 'view', packageName, 'dist-tags', '--json'];
|
|
402
|
+
}
|
|
403
|
+
return ['view', packageName, 'dist-tags', '--json'];
|
|
404
|
+
}
|
|
273
405
|
function findOwnPackageJson() {
|
|
274
406
|
let currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
275
407
|
while (true) {
|
|
@@ -331,6 +463,115 @@ function parsePackedFilename(stdout) {
|
|
|
331
463
|
return firstNonEmptyLine(stdout);
|
|
332
464
|
}
|
|
333
465
|
}
|
|
466
|
+
function lookupNpmDistTags(packageName, options) {
|
|
467
|
+
const now = options.now ?? new Date();
|
|
468
|
+
const cachePath = resolveNpmUpdateCachePath(packageName);
|
|
469
|
+
const ttlMs = options.cacheTtlMs ?? DEFAULT_INSTALLABLE_UPDATE_CACHE_TTL_MS;
|
|
470
|
+
const cached = readCachedNpmDistTags(cachePath, packageName, now, ttlMs);
|
|
471
|
+
if (cached) {
|
|
472
|
+
return cached;
|
|
473
|
+
}
|
|
474
|
+
const viewResult = spawnSync(resolveNpmCommand(), resolveNpmViewArgs(packageName), {
|
|
475
|
+
cwd: options.cwd,
|
|
476
|
+
encoding: 'utf-8',
|
|
477
|
+
timeout: 15000,
|
|
478
|
+
});
|
|
479
|
+
if (viewResult.error) {
|
|
480
|
+
throw new Error(`npm view failed: ${viewResult.error.message}`);
|
|
481
|
+
}
|
|
482
|
+
if (viewResult.status !== 0) {
|
|
483
|
+
throw new Error(firstNonEmptyLine(viewResult.stderr) ?? firstNonEmptyLine(viewResult.stdout) ?? 'npm view failed');
|
|
484
|
+
}
|
|
485
|
+
const distTags = parseNpmDistTags(viewResult.stdout);
|
|
486
|
+
const result = {
|
|
487
|
+
dist_tags: distTags,
|
|
488
|
+
checked_at: now.toISOString(),
|
|
489
|
+
cached: false,
|
|
490
|
+
};
|
|
491
|
+
writeCachedNpmDistTags(cachePath, {
|
|
492
|
+
version: 1,
|
|
493
|
+
package_name: packageName,
|
|
494
|
+
fetched_at: result.checked_at,
|
|
495
|
+
dist_tags: distTags,
|
|
496
|
+
});
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
function parseNpmDistTags(stdout) {
|
|
500
|
+
let parsed;
|
|
501
|
+
try {
|
|
502
|
+
parsed = JSON.parse(stdout);
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
506
|
+
throw new Error(`npm view returned invalid JSON: ${message}`);
|
|
507
|
+
}
|
|
508
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
509
|
+
throw new Error('npm view did not return a dist-tag object.');
|
|
510
|
+
}
|
|
511
|
+
const distTags = {};
|
|
512
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
513
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
514
|
+
distTags[key] = value.trim();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (Object.keys(distTags).length === 0) {
|
|
518
|
+
throw new Error('npm view returned an empty dist-tag object.');
|
|
519
|
+
}
|
|
520
|
+
return distTags;
|
|
521
|
+
}
|
|
522
|
+
function resolveNpmUpdateCachePath(packageName) {
|
|
523
|
+
const safeName = packageName.replace(/[^a-z0-9._-]+/gi, '_').replace(/^_+|_+$/g, '') || DEFAULT_NPM_UPDATE_PACKAGE;
|
|
524
|
+
return path.join(os.homedir(), '.brainclaw', 'cache', 'installable-updates', `${safeName}.json`);
|
|
525
|
+
}
|
|
526
|
+
function readCachedNpmDistTags(cachePath, packageName, now, ttlMs) {
|
|
527
|
+
if (!fs.existsSync(cachePath)) {
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
const parsed = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
|
532
|
+
if (parsed.version !== 1
|
|
533
|
+
|| parsed.package_name !== packageName
|
|
534
|
+
|| typeof parsed.fetched_at !== 'string'
|
|
535
|
+
|| !parsed.dist_tags
|
|
536
|
+
|| typeof parsed.dist_tags !== 'object'
|
|
537
|
+
|| Array.isArray(parsed.dist_tags)) {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
const fetchedAt = Date.parse(parsed.fetched_at);
|
|
541
|
+
if (!Number.isFinite(fetchedAt) || now.getTime() - fetchedAt > ttlMs) {
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
const distTags = {};
|
|
545
|
+
for (const [key, value] of Object.entries(parsed.dist_tags)) {
|
|
546
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
547
|
+
distTags[key] = value.trim();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (Object.keys(distTags).length === 0) {
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
dist_tags: distTags,
|
|
555
|
+
checked_at: parsed.fetched_at,
|
|
556
|
+
cached: true,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function writeCachedNpmDistTags(cachePath, document) {
|
|
564
|
+
try {
|
|
565
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
566
|
+
fs.writeFileSync(cachePath, `${JSON.stringify(document, null, 2)}\n`, 'utf-8');
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
// Cache writes are best-effort only.
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function formatNpmSourceDescription(packageName, distTag, defaultSource) {
|
|
573
|
+
return defaultSource ? `${packageName}@${distTag} (default npm channel)` : `${packageName}@${distTag}`;
|
|
574
|
+
}
|
|
334
575
|
function firstNonEmptyLine(value) {
|
|
335
576
|
return value
|
|
336
577
|
.split(/\r?\n/)
|
package/dist/core/schema.js
CHANGED
|
@@ -485,6 +485,7 @@ export const BootstrapProfileDocumentSchema = z.object({
|
|
|
485
485
|
seed_count: z.number().int().nonnegative(),
|
|
486
486
|
target: z.string().optional(),
|
|
487
487
|
workspace_kind: z.enum(['empty', 'existing']).optional(),
|
|
488
|
+
onboarding_mode: z.enum(['empty_workspace', 'existing_documented', 'existing_sparse']).optional(),
|
|
488
489
|
confidence: MemorySeedConfidenceSchema.optional(),
|
|
489
490
|
native_instruction_files: z.array(z.string()).default([]),
|
|
490
491
|
gaps: z.array(z.string()).default([]),
|
|
@@ -503,6 +504,9 @@ export const BootstrapSuggestionDocumentSchema = z.object({
|
|
|
503
504
|
scope: z.string().optional(),
|
|
504
505
|
tags: z.array(z.string()).default([]),
|
|
505
506
|
related_paths: z.array(z.string()).optional(),
|
|
507
|
+
category: ConstraintCategorySchema.optional(),
|
|
508
|
+
outcome: DecisionOutcomeSchema.optional(),
|
|
509
|
+
severity: SeveritySchema.optional(),
|
|
506
510
|
reversible: z.boolean().default(true),
|
|
507
511
|
});
|
|
508
512
|
export const BootstrapInterviewAudienceSchema = z.enum(['cli', 'ide_chat', 'any']);
|
|
@@ -514,6 +518,7 @@ export const BootstrapInterviewQuestionSchema = z.object({
|
|
|
514
518
|
audience: BootstrapInterviewAudienceSchema.default('any'),
|
|
515
519
|
response_kind: z.enum(['short_text', 'long_text', 'boolean', 'list']).default('short_text'),
|
|
516
520
|
gap_keys: z.array(z.string()).default([]),
|
|
521
|
+
target_hints: z.array(BootstrapSuggestionTargetSchema).default([]),
|
|
517
522
|
});
|
|
518
523
|
export const BootstrapInterviewPlanSchema = z.object({
|
|
519
524
|
schema_version: z.number().int().positive().optional(),
|
|
@@ -524,15 +529,38 @@ export const BootstrapInterviewPlanSchema = z.object({
|
|
|
524
529
|
question_count: z.number().int().nonnegative(),
|
|
525
530
|
questions: z.array(BootstrapInterviewQuestionSchema).default([]),
|
|
526
531
|
});
|
|
532
|
+
export const BootstrapInterviewAnswerSuggestionSchema = z.object({
|
|
533
|
+
target: BootstrapSuggestionTargetSchema,
|
|
534
|
+
text: z.string(),
|
|
535
|
+
rationale: z.string().optional(),
|
|
536
|
+
confidence: MemorySeedConfidenceSchema.optional(),
|
|
537
|
+
layer: z.enum(['global', 'project', 'agent']).optional(),
|
|
538
|
+
scope: z.string().optional(),
|
|
539
|
+
tags: z.array(z.string()).default([]),
|
|
540
|
+
related_paths: z.array(z.string()).optional(),
|
|
541
|
+
category: ConstraintCategorySchema.optional(),
|
|
542
|
+
outcome: DecisionOutcomeSchema.optional(),
|
|
543
|
+
severity: SeveritySchema.optional(),
|
|
544
|
+
});
|
|
545
|
+
export const BootstrapInterviewAnswerSchema = z.object({
|
|
546
|
+
question_id: z.string(),
|
|
547
|
+
response_text: z.string().optional(),
|
|
548
|
+
response_items: z.array(z.string()).default([]),
|
|
549
|
+
response_boolean: z.boolean().optional(),
|
|
550
|
+
suggestions: z.array(BootstrapInterviewAnswerSuggestionSchema).default([]),
|
|
551
|
+
});
|
|
527
552
|
export const BootstrapImportPlanDocumentSchema = z.object({
|
|
528
553
|
schema_version: z.number().int().positive().optional(),
|
|
529
554
|
derived_at: z.string(),
|
|
530
555
|
target: z.string().optional(),
|
|
531
556
|
workspace_kind: z.enum(['empty', 'existing']).optional(),
|
|
557
|
+
onboarding_mode: z.enum(['empty_workspace', 'existing_documented', 'existing_sparse']).optional(),
|
|
532
558
|
confidence: MemorySeedConfidenceSchema.optional(),
|
|
533
559
|
summary: z.string(),
|
|
534
560
|
requires_confirmation: z.boolean().default(true),
|
|
535
561
|
gaps: z.array(z.string()).default([]),
|
|
562
|
+
confirmed_suggestion_count: z.number().int().nonnegative().default(0),
|
|
563
|
+
interview_answer_count: z.number().int().nonnegative().default(0),
|
|
536
564
|
suggestion_count: z.number().int().nonnegative(),
|
|
537
565
|
suggestions: z.array(BootstrapSuggestionDocumentSchema).default([]),
|
|
538
566
|
interview: BootstrapInterviewPlanSchema.optional(),
|
package/docs/cli.md
CHANGED
|
@@ -115,6 +115,7 @@ For capable agents, the MCP equivalent is `bclaw_bootstrap`. Use the CLI when a
|
|
|
115
115
|
| `--refresh` | Force refresh even if bootstrap data is recent |
|
|
116
116
|
| `--interview` | Render adaptive interview prompts instead of the bootstrap summary |
|
|
117
117
|
| `--audience <audience>` | Filter interview prompts for `cli`, `ide_chat`, or `any` |
|
|
118
|
+
| `--answers-file <path>` | Load structured interview answers from JSON and enrich the import proposal before preview/apply |
|
|
118
119
|
| `--apply` | Import the current bootstrap proposal into canonical memory |
|
|
119
120
|
| `--uninstall` | Deactivate the last bootstrap-managed import |
|
|
120
121
|
| `-y, --yes` | Skip confirmation prompts for apply/uninstall |
|
|
@@ -123,12 +124,16 @@ For capable agents, the MCP equivalent is `bclaw_bootstrap`. Use the CLI when a
|
|
|
123
124
|
brainclaw bootstrap --for copilot
|
|
124
125
|
brainclaw bootstrap --for claude --refresh --json
|
|
125
126
|
brainclaw bootstrap --interview --audience cli
|
|
127
|
+
brainclaw bootstrap --answers-file ./bootstrap-answers.json --json
|
|
128
|
+
brainclaw bootstrap --answers-file ./bootstrap-answers.json --apply -y
|
|
126
129
|
brainclaw bootstrap --apply -y
|
|
127
130
|
```
|
|
128
131
|
|
|
132
|
+
`--answers-file` expects a JSON array keyed by interview question IDs. Each entry can provide free-form answers (`response_text`, `response_items`, `response_boolean`) and optional explicit `suggestions` to confirm durable imports such as `decision`, `constraint`, `instruction`, or `trap`.
|
|
133
|
+
|
|
129
134
|
### `brainclaw env`
|
|
130
135
|
|
|
131
|
-
Display environment
|
|
136
|
+
Display environment, tooling detection, and installable Brainclaw update information.
|
|
132
137
|
|
|
133
138
|
| Option | Description |
|
|
134
139
|
|---|---|
|
|
@@ -141,6 +146,8 @@ brainclaw env --json
|
|
|
141
146
|
brainclaw env --agent-tooling
|
|
142
147
|
```
|
|
143
148
|
|
|
149
|
+
When the project does not pin a local release channel, `brainclaw env` checks the public npm `latest` channel for installable updates. Projects can override that with `brainclaw_update_source`, for example to use `prelaunch` or a local-pack manifest.
|
|
150
|
+
|
|
144
151
|
---
|
|
145
152
|
|
|
146
153
|
## Memory Management
|
|
@@ -1248,3 +1255,10 @@ brainclaw version
|
|
|
1248
1255
|
brainclaw version --check
|
|
1249
1256
|
brainclaw version --publish-local --release-notes "Add estimation-report command"
|
|
1250
1257
|
```
|
|
1258
|
+
|
|
1259
|
+
`brainclaw version --check` now follows this order:
|
|
1260
|
+
|
|
1261
|
+
- if `brainclaw_update_source` is configured, use it
|
|
1262
|
+
- otherwise, fall back to the public npm channel `brainclaw@latest`
|
|
1263
|
+
|
|
1264
|
+
This keeps end-user installs aware of published npm releases without requiring a local tarball channel. To keep beta testers on a different channel, set `brainclaw_update_source` to `type: npm` with a different `dist_tag`, such as `prelaunch`.
|
package/docs/integrations/mcp.md
CHANGED
|
@@ -16,16 +16,19 @@ MCP matters because Brainclaw's value is mostly in dynamic state:
|
|
|
16
16
|
|
|
17
17
|
Static files still help, but they age immediately. MCP is the stronger path for live coordination.
|
|
18
18
|
|
|
19
|
+
That now also includes Brainclaw's own install channel state: `bclaw_get_execution_context` surfaces whether a newer npm or local-pack build is available, so the agent can notice upgrades without relying on a human to run `brainclaw version --check`.
|
|
20
|
+
|
|
19
21
|
## Recommended Agent Pattern
|
|
20
22
|
|
|
21
23
|
The default dynamic workflow is:
|
|
22
24
|
|
|
23
25
|
1. `bclaw_session_start` to open work and get the current board/context
|
|
24
|
-
2. `
|
|
25
|
-
3. `
|
|
26
|
-
4. `
|
|
27
|
-
5. `
|
|
28
|
-
6. `
|
|
26
|
+
2. `bclaw_get_execution_context` early in the session when the agent needs local tooling signals or package update visibility
|
|
27
|
+
3. `bclaw_get_context` when the target path or task changes
|
|
28
|
+
4. `bclaw_list_plans` and `bclaw_list_claims` to inspect active work
|
|
29
|
+
5. `bclaw_claim` before editing
|
|
30
|
+
6. `bclaw_write_note` for runtime observations
|
|
31
|
+
7. `bclaw_session_end` to close cleanly and hand work off
|
|
29
32
|
|
|
30
33
|
This keeps session continuity inside Brainclaw instead of pushing the agent back to manual CLI usage.
|
|
31
34
|
|
|
@@ -34,8 +37,8 @@ This keeps session continuity inside Brainclaw instead of pushing the agent back
|
|
|
34
37
|
| Tool | Purpose |
|
|
35
38
|
|---|---|
|
|
36
39
|
| `bclaw_get_context` | Ranked prompt-ready context, supports `digest: true` |
|
|
37
|
-
| `bclaw_bootstrap` | Derive brownfield bootstrap signals, adaptive interview prompts,
|
|
38
|
-
| `bclaw_get_execution_context` | Inspect local execution context and agent tooling |
|
|
40
|
+
| `bclaw_bootstrap` | Derive brownfield bootstrap signals, return adaptive interview prompts, accept structured interview answers, and preview/apply a selective import proposal |
|
|
41
|
+
| `bclaw_get_execution_context` | Inspect local execution context, installable update status, and agent tooling |
|
|
39
42
|
| `bclaw_write_note` | Record a runtime note, supports `autoReflect: true` |
|
|
40
43
|
| `bclaw_read_handoff` | Read active handoffs |
|
|
41
44
|
| `bclaw_get_agent_board` | Coordination snapshot |
|
|
@@ -65,6 +68,25 @@ brainclaw mcp
|
|
|
65
68
|
|
|
66
69
|
In practice, most agents pick this up through generated MCP config such as `.mcp.json`, `~/.cursor/mcp.json`, or other agent-specific config files written by `brainclaw setup`, `brainclaw init`, or `brainclaw export`.
|
|
67
70
|
|
|
71
|
+
By default, installable update checks use the public npm channel `brainclaw@latest`. Projects that need a different channel can override `brainclaw_update_source`, for example with `type: npm` and `dist_tag: prelaunch`, or with `type: local-pack` for local tarball workflows.
|
|
72
|
+
|
|
73
|
+
## Bootstrap Through MCP
|
|
74
|
+
|
|
75
|
+
For agent-first onboarding, `bclaw_bootstrap` is the nominal path:
|
|
76
|
+
|
|
77
|
+
1. call `bclaw_bootstrap` to get the current `import_plan` and adaptive interview questions
|
|
78
|
+
2. collect answers in the agent surface
|
|
79
|
+
3. call `bclaw_bootstrap` again with `interviewAnswers` to preview confirmed `decision`, `constraint`, `instruction`, or `trap` suggestions
|
|
80
|
+
4. call `bclaw_bootstrap` with `apply: true` to create canonical memory
|
|
81
|
+
5. call `bclaw_bootstrap` with `uninstall: true` to revert the last bootstrap-managed import
|
|
82
|
+
|
|
83
|
+
Interview answers are keyed by question ID and may contain:
|
|
84
|
+
|
|
85
|
+
- `response_text`
|
|
86
|
+
- `response_items`
|
|
87
|
+
- `response_boolean`
|
|
88
|
+
- optional explicit `suggestions` when the agent wants to confirm exact canonical memory items
|
|
89
|
+
|
|
68
90
|
## Important Rule
|
|
69
91
|
|
|
70
92
|
If the agent has MCP available, do not treat the CLI as the primary runtime interface.
|
|
@@ -27,6 +27,12 @@ This document tracks all breaking and notable changes to the brainclaw MCP serve
|
|
|
27
27
|
- `bclaw_bootstrap` now returns adaptive interview prompts alongside the import proposal when bootstrap confidence is incomplete
|
|
28
28
|
- `structuredContent.import_plan.interview` exposes `summary`, `question_count`, and audience-tagged questions
|
|
29
29
|
- Questions can be targeted to `cli`, `ide_chat`, or `any`
|
|
30
|
+
- Interview questions now expose stable IDs and `target_hints`
|
|
31
|
+
- `structuredContent.onboarding_mode` distinguishes `empty_workspace`, `existing_documented`, and `existing_sparse`
|
|
32
|
+
- `structuredContent.import_plan.confirmed_suggestion_count` reports how many suggestions were confirmed by interview answers
|
|
33
|
+
- `bclaw_bootstrap` now accepts `interviewAnswers`, `apply`, `uninstall`, `audience`, and `interview`
|
|
34
|
+
- capable agents can preview confirmed selective imports through MCP before applying them
|
|
35
|
+
- bootstrap apply/uninstall now covers selective canonical memory imports beyond instructions
|
|
30
36
|
|
|
31
37
|
**Changed**
|
|
32
38
|
- MCP schema version bumped to 0.6.0 to reflect new metadata discovery capabilities
|