brainclaw 0.19.11 → 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 CHANGED
@@ -129,6 +129,7 @@ After that, the agent should stay on Brainclaw's MCP path for live state:
129
129
 
130
130
  ```text
131
131
  bclaw_session_start -> open session + return board/context
132
+ bclaw_get_execution_context -> inspect local tooling + notice Brainclaw package updates
132
133
  bclaw_get_context -> fetch fresh prompt-ready context for a path
133
134
  bclaw_list_plans -> inspect shared work
134
135
  bclaw_claim -> claim scope before editing
@@ -177,6 +178,8 @@ If you want a machine-level CLI for operator workflows, debugging, or repeated l
177
178
  npm install -g brainclaw
178
179
  ```
179
180
 
181
+ By default, Brainclaw's update checks for end-user installs follow the public npm `latest` channel. Projects that need a different track can override `brainclaw_update_source`, for example to use `prelaunch` or a local tarball channel.
182
+
180
183
  If you are working from source while developing Brainclaw itself:
181
184
 
182
185
  ```bash
@@ -1,7 +1,7 @@
1
1
  import { memoryExists } from '../core/io.js';
2
2
  import { loadConfig } from '../core/config.js';
3
3
  import { assessAgentIntegrationReadiness } from '../core/agent-integrations.js';
4
- import { assessBrainclawVersion } from '../core/brainclaw-version.js';
4
+ import { assessBrainclawVersion, checkBrainclawInstallableUpdate, renderBrainclawInstallableUpdateNotice, } from '../core/brainclaw-version.js';
5
5
  import { buildExecutionContext, compactExecutionContext, renderExecutionContextSummary } from '../core/execution-context.js';
6
6
  import { buildAgentToolingContext, renderAgentToolingSummary } from '../core/agent-context.js';
7
7
  export function runEnv(options = {}) {
@@ -14,11 +14,14 @@ export function runEnv(options = {}) {
14
14
  const config = loadConfig(cwd);
15
15
  const integrationReadiness = assessAgentIntegrationReadiness(config, cwd);
16
16
  const brainclawVersion = assessBrainclawVersion(config);
17
+ const installableUpdate = checkBrainclawInstallableUpdate(config, cwd, { useDefaultNpmSource: true });
18
+ const installableUpdateNotice = renderBrainclawInstallableUpdateNotice(installableUpdate);
17
19
  const agentTooling = options.agentTooling ? buildAgentToolingContext({ cwd }) : undefined;
18
20
  if (options.json) {
19
21
  console.log(JSON.stringify({
20
22
  execution_context: executionContext,
21
23
  brainclaw_version: brainclawVersion,
24
+ installable_update: installableUpdate,
22
25
  declared_agent_integrations: config.agent_integrations,
23
26
  integration_readiness: integrationReadiness,
24
27
  ...(agentTooling ? { agent_tooling: agentTooling } : {}),
@@ -33,6 +36,9 @@ export function runEnv(options = {}) {
33
36
  console.log(`Upgrade benefits: ${brainclawVersion.upgrade_message}`);
34
37
  }
35
38
  }
39
+ if (installableUpdateNotice) {
40
+ console.log(installableUpdateNotice);
41
+ }
36
42
  console.log(`Declared agent integrations: ${config.agent_integrations.declarations.length}`);
37
43
  const missingDeclarations = integrationReadiness.filter((entry) => !entry.ready);
38
44
  if (missingDeclarations.length > 0) {
@@ -7,6 +7,7 @@ import { buildAgentToolingContext, renderAgentToolingSummary } from '../core/age
7
7
  import { buildCoordinationSnapshot } from '../core/coordination.js';
8
8
  import { buildContext, renderContextMarkdown, renderContextPromptTemplate } from '../core/context.js';
9
9
  import { buildExecutionContext, renderExecutionContextSummary } from '../core/execution-context.js';
10
+ import { checkBrainclawInstallableUpdate, renderBrainclawInstallableUpdateNotice } from '../core/brainclaw-version.js';
10
11
  import { loadConfig } from '../core/config.js';
11
12
  import { loadState, persistState } from '../core/state.js';
12
13
  import { memoryExists } from '../core/io.js';
@@ -82,7 +83,7 @@ export const MCP_READ_TOOLS = [
82
83
  },
83
84
  {
84
85
  name: 'bclaw_get_execution_context',
85
- description: 'Inspect the local execution environment and optionally agent tooling signals.',
86
+ description: 'Inspect the local execution environment, installable Brainclaw update channel, and optionally agent tooling signals.',
86
87
  inputSchema: {
87
88
  type: 'object',
88
89
  properties: {
@@ -1133,15 +1134,20 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1133
1134
  }
1134
1135
  if (name === 'bclaw_get_execution_context') {
1135
1136
  const executionContext = buildExecutionContext({ cwd });
1137
+ const config = loadConfig(cwd);
1138
+ const installableUpdate = checkBrainclawInstallableUpdate(config, cwd, { useDefaultNpmSource: true });
1139
+ const installableUpdateNotice = renderBrainclawInstallableUpdateNotice(installableUpdate);
1136
1140
  const agentTooling = args.includeAgentTooling ? buildAgentToolingContext({ cwd }) : undefined;
1137
1141
  const text = [
1138
1142
  renderExecutionContextSummary(executionContext, true),
1143
+ ...(installableUpdateNotice ? ['', installableUpdateNotice] : []),
1139
1144
  ...(agentTooling ? ['', renderAgentToolingSummary(agentTooling)] : []),
1140
1145
  ].join('\n');
1141
1146
  return {
1142
1147
  content: [{ type: 'text', text }],
1143
1148
  structuredContent: {
1144
1149
  execution_context: executionContext,
1150
+ installable_update: installableUpdate,
1145
1151
  ...(agentTooling ? { agent_tooling: agentTooling } : {}),
1146
1152
  },
1147
1153
  };
@@ -31,7 +31,9 @@ export function runVersion(options = {}) {
31
31
  }
32
32
  }
33
33
  const assessment = assessBrainclawVersion(config);
34
- const updateCheck = options.check ? checkBrainclawInstallableUpdate(config, cwd) : undefined;
34
+ const updateCheck = options.check
35
+ ? checkBrainclawInstallableUpdate(config, cwd, { useDefaultNpmSource: true })
36
+ : undefined;
35
37
  const result = {
36
38
  initialized,
37
39
  ...assessment,
@@ -4,7 +4,7 @@ import { loadConfig } from '../core/config.js';
4
4
  import { resolveCurrentHostId } from '../core/host.js';
5
5
  import { buildOperationalIdentity } from '../core/identity.js';
6
6
  import { assessAgentIntegrationReadiness } from '../core/agent-integrations.js';
7
- import { assessBrainclawVersion } from '../core/brainclaw-version.js';
7
+ import { assessBrainclawVersion, checkBrainclawInstallableUpdate, renderBrainclawInstallableUpdateNotice, } from '../core/brainclaw-version.js';
8
8
  import { buildExecutionContext, compactExecutionContext } from '../core/execution-context.js';
9
9
  import { buildAgentToolingContext } from '../core/agent-context.js';
10
10
  export function runWhoami(options = {}) {
@@ -18,6 +18,8 @@ export function runWhoami(options = {}) {
18
18
  const executionContext = compactExecutionContext(buildExecutionContext({ cwd }));
19
19
  const integrationReadiness = assessAgentIntegrationReadiness(config, cwd);
20
20
  const brainclawVersion = assessBrainclawVersion(config);
21
+ const installableUpdate = checkBrainclawInstallableUpdate(config, cwd, { useDefaultNpmSource: true });
22
+ const installableUpdateNotice = renderBrainclawInstallableUpdateNotice(installableUpdate);
21
23
  const agentTooling = buildAgentToolingContext({ cwd });
22
24
  let identity;
23
25
  try {
@@ -45,6 +47,7 @@ export function runWhoami(options = {}) {
45
47
  env_session: process.env.BRAINCLAW_SESSION_ID ?? null,
46
48
  env_host: process.env.BRAINCLAW_HOST_ID ?? null,
47
49
  brainclaw_version: brainclawVersion,
50
+ installable_update: installableUpdate,
48
51
  declared_agent_integrations: config.agent_integrations,
49
52
  integration_readiness: integrationReadiness,
50
53
  execution_context: executionContext,
@@ -80,6 +83,13 @@ export function runWhoami(options = {}) {
80
83
  if (result.brainclaw_version.status !== 'ok') {
81
84
  console.log(` Version : ${result.brainclaw_version.message}`);
82
85
  }
86
+ if (installableUpdateNotice) {
87
+ const lines = installableUpdateNotice.split('\n');
88
+ console.log(` Update : ${lines[0]}`);
89
+ for (const line of lines.slice(1)) {
90
+ console.log(` ${line}`);
91
+ }
92
+ }
83
93
  console.log(` Declared integrations: ${result.declared_agent_integrations.declarations.length}`);
84
94
  const missingIntegrations = result.integration_readiness.filter((entry) => !entry.ready);
85
95
  if (missingIntegrations.length > 0) {
@@ -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
- const packageName = source.package_name?.trim() || 'brainclaw';
107
- const distTag = source.dist_tag?.trim() || 'latest';
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: `${packageName}@${distTag}`,
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: 'unsupported_source',
117
- message: 'The npm update source is modeled in config but is not implemented yet in this build.',
196
+ status: 'invalid_config',
197
+ message: 'brainclaw_update_source.package_name must not be empty.',
198
+ default_source: defaultSource,
118
199
  };
119
200
  }
120
- const manifestPath = source.manifest_path.trim();
121
- if (manifestPath.length === 0) {
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(manifestPath)
135
- ? manifestPath
136
- : path.resolve(cwd, manifestPath);
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/docs/cli.md CHANGED
@@ -133,7 +133,7 @@ brainclaw bootstrap --apply -y
133
133
 
134
134
  ### `brainclaw env`
135
135
 
136
- Display environment and tooling detection information.
136
+ Display environment, tooling detection, and installable Brainclaw update information.
137
137
 
138
138
  | Option | Description |
139
139
  |---|---|
@@ -146,6 +146,8 @@ brainclaw env --json
146
146
  brainclaw env --agent-tooling
147
147
  ```
148
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
+
149
151
  ---
150
152
 
151
153
  ## Memory Management
@@ -1253,3 +1255,10 @@ brainclaw version
1253
1255
  brainclaw version --check
1254
1256
  brainclaw version --publish-local --release-notes "Add estimation-report command"
1255
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`.
@@ -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. `bclaw_get_context` when the target path or task changes
25
- 3. `bclaw_list_plans` and `bclaw_list_claims` to inspect active work
26
- 4. `bclaw_claim` before editing
27
- 5. `bclaw_write_note` for runtime observations
28
- 6. `bclaw_session_end` to close cleanly and hand work off
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
 
@@ -35,7 +38,7 @@ This keeps session continuity inside Brainclaw instead of pushing the agent back
35
38
  |---|---|
36
39
  | `bclaw_get_context` | Ranked prompt-ready context, supports `digest: true` |
37
40
  | `bclaw_bootstrap` | Derive brownfield bootstrap signals, return adaptive interview prompts, accept structured interview answers, and preview/apply a selective import proposal |
38
- | `bclaw_get_execution_context` | Inspect local execution context and agent tooling |
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,8 @@ 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
+
68
73
  ## Bootstrap Through MCP
69
74
 
70
75
  For agent-first onboarding, `bclaw_bootstrap` is the nominal path:
@@ -39,6 +39,7 @@ After the workspace is initialized, the nominal flow is:
39
39
 
40
40
  ```text
41
41
  bclaw_session_start -> open a session and return current board/context
42
+ bclaw_get_execution_context -> inspect local tooling and notice package updates
42
43
  bclaw_get_context -> fetch fresh prompt-ready context for the target path
43
44
  bclaw_list_plans -> inspect active work
44
45
  bclaw_claim -> claim scope before editing
@@ -48,6 +49,8 @@ bclaw_session_end -> close session cleanly and hand work off
48
49
 
49
50
  Use native agent files such as `AGENTS.md`, `CLAUDE.md`, or Cursor rules as local workflow guidance, not as the only source of live state.
50
51
 
52
+ Unless the project overrides `brainclaw_update_source`, `bclaw_get_execution_context` checks the public npm `latest` channel so the agent can notice when a newer Brainclaw release is available.
53
+
51
54
  ## Path 2: CLI-Oriented Agent Or Fallback Workflow
52
55
 
53
56
  Use this path when the agent does not have a good MCP integration yet, or when a human needs to drive the workflow directly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainclaw",
3
- "version": "0.19.11",
3
+ "version": "0.19.12",
4
4
  "description": "Shared project memory for humans and coding agents.",
5
5
  "type": "module",
6
6
  "bin": {