brainclaw 0.19.11 → 0.19.14

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.
@@ -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/)
@@ -0,0 +1,224 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { memoryExists } from './io.js';
6
+ const CLAUDE_DESKTOP_TOOLS = [
7
+ { name: 'bclaw_session_start', description: 'Open a Brainclaw session and surface Claude-targeted tasks plus compact execution context.' },
8
+ { name: 'bclaw_get_context', description: 'Retrieve ranked Brainclaw project context for the current task or path.' },
9
+ { name: 'bclaw_list_surface_tasks', description: 'List queued or completed Brainclaw tasks delegated to Claude Desktop.' },
10
+ { name: 'bclaw_update_surface_task', description: 'Mark a delegated Claude Desktop task as in progress or completed.' },
11
+ ];
12
+ export function buildClaudeDesktopExtension(options = {}) {
13
+ const cwd = path.resolve(options.cwd ?? process.cwd());
14
+ if (!memoryExists(cwd)) {
15
+ throw new Error('Project memory not initialized. Run `brainclaw init` first.');
16
+ }
17
+ const workspaceDir = path.resolve(options.workspaceDir ?? path.join(cwd, 'internal-docs', 'desktop-extensions', 'claude-desktop-brainclaw'));
18
+ const outputFile = path.resolve(options.outputFile ?? path.join(cwd, 'internal-docs', 'desktop-extensions', 'brainclaw-claude-desktop.mcpb'));
19
+ if (outputFile.startsWith(`${workspaceDir}${path.sep}`) || outputFile === workspaceDir) {
20
+ throw new Error('The .mcpb output file must live outside the extension workspace directory.');
21
+ }
22
+ const projectRoot = path.resolve(options.projectRoot ?? cwd);
23
+ const runtimeRoot = path.resolve(options.runtimeRootOverride ?? resolveRuntimeRoot());
24
+ const packageRoot = path.resolve(options.packageRootOverride ?? findPackageRoot(runtimeRoot));
25
+ const metadata = readPackageMetadata(packageRoot);
26
+ const copiedDependencies = options.dependenciesOverride ?? resolveRuntimeDependencies(packageRoot);
27
+ fs.rmSync(workspaceDir, { recursive: true, force: true });
28
+ fs.mkdirSync(workspaceDir, { recursive: true });
29
+ fs.cpSync(runtimeRoot, path.join(workspaceDir, 'runtime'), { recursive: true });
30
+ fs.mkdirSync(path.join(workspaceDir, 'server'), { recursive: true });
31
+ fs.mkdirSync(path.dirname(outputFile), { recursive: true });
32
+ for (const dep of copiedDependencies) {
33
+ const sourceDir = path.join(packageRoot, 'node_modules', dep);
34
+ if (!fs.existsSync(sourceDir)) {
35
+ throw new Error(`Missing runtime dependency for Claude Desktop extension: ${dep}`);
36
+ }
37
+ fs.cpSync(sourceDir, path.join(workspaceDir, 'node_modules', dep), { recursive: true });
38
+ }
39
+ const version = metadata.version ?? '0.0.0';
40
+ const manifestPath = path.join(workspaceDir, 'manifest.json');
41
+ const entryPointPath = path.join(workspaceDir, 'server', 'index.js');
42
+ const packageJsonPath = path.join(workspaceDir, 'package.json');
43
+ fs.writeFileSync(entryPointPath, buildServerEntryPoint(), 'utf-8');
44
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify({
45
+ name: 'brainclaw-claude-desktop-extension',
46
+ private: true,
47
+ type: 'module',
48
+ version,
49
+ }, null, 2)}\n`, 'utf-8');
50
+ fs.writeFileSync(manifestPath, `${JSON.stringify(buildManifest(metadata, version, projectRoot), null, 2)}\n`, 'utf-8');
51
+ const packed = options.pack !== false ? packClaudeDesktopExtension(workspaceDir, outputFile) : false;
52
+ return {
53
+ workspaceDir,
54
+ outputFile,
55
+ packed,
56
+ manifestPath,
57
+ entryPointPath,
58
+ packageRoot,
59
+ runtimeRoot,
60
+ projectRoot,
61
+ copiedDependencies,
62
+ };
63
+ }
64
+ function buildServerEntryPoint() {
65
+ return `import process from 'node:process';
66
+
67
+ const projectRoot = process.env.BRAINCLAW_PROJECT_ROOT?.trim();
68
+ if (projectRoot) {
69
+ process.chdir(projectRoot);
70
+ }
71
+
72
+ const { runMcp } = await import('../runtime/commands/mcp.js');
73
+ runMcp();
74
+ `;
75
+ }
76
+ function buildManifest(metadata, version, projectRoot) {
77
+ return {
78
+ manifest_version: '0.3',
79
+ name: 'brainclaw-claude-desktop',
80
+ display_name: 'Brainclaw Project Memory',
81
+ version,
82
+ description: 'Brainclaw local project memory and delegated task inbox for Claude Desktop.',
83
+ author: {
84
+ name: 'Brainclaw',
85
+ ...(metadata.homepage ? { url: metadata.homepage } : {}),
86
+ },
87
+ ...(typeof metadata.repository === 'string'
88
+ ? { repository: { type: 'git', url: metadata.repository } }
89
+ : metadata.repository
90
+ ? { repository: metadata.repository }
91
+ : {}),
92
+ ...(metadata.homepage ? { homepage: metadata.homepage } : {}),
93
+ server: {
94
+ type: 'node',
95
+ entry_point: 'server/index.js',
96
+ mcp_config: {
97
+ command: 'node',
98
+ args: ['${__dirname}/server/index.js'],
99
+ env: {
100
+ BRAINCLAW_PROJECT_ROOT: '${user_config.project_root}',
101
+ BRAINCLAW_SKIP_SETUP_REQUIREMENT: '1',
102
+ },
103
+ },
104
+ },
105
+ tools: CLAUDE_DESKTOP_TOOLS,
106
+ compatibility: {
107
+ platforms: ['win32', 'darwin'],
108
+ runtimes: {
109
+ node: '>=20.0.0',
110
+ },
111
+ },
112
+ user_config: {
113
+ project_root: {
114
+ type: 'directory',
115
+ title: 'Project Root',
116
+ description: 'Brainclaw project root that Claude Desktop should operate on.',
117
+ required: true,
118
+ default: projectRoot,
119
+ },
120
+ },
121
+ };
122
+ }
123
+ function resolveRuntimeRoot() {
124
+ return path.resolve(fileURLToPath(new URL('..', import.meta.url)));
125
+ }
126
+ function findPackageRoot(startDir) {
127
+ let current = startDir;
128
+ while (true) {
129
+ const candidate = path.join(current, 'package.json');
130
+ if (fs.existsSync(candidate)) {
131
+ return current;
132
+ }
133
+ const parent = path.dirname(current);
134
+ if (parent === current) {
135
+ throw new Error(`Could not locate package.json from ${startDir}`);
136
+ }
137
+ current = parent;
138
+ }
139
+ }
140
+ function readPackageMetadata(packageRoot) {
141
+ const packageJsonPath = path.join(packageRoot, 'package.json');
142
+ return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
143
+ }
144
+ function resolveRuntimeDependencies(packageRoot) {
145
+ const metadata = readPackageMetadata(packageRoot);
146
+ return Object.keys(metadata.dependencies ?? {});
147
+ }
148
+ function packClaudeDesktopExtension(workspaceDir, outputFile) {
149
+ fs.rmSync(outputFile, { force: true });
150
+ const pythonCommand = resolveAvailableCommand(process.platform === 'win32'
151
+ ? ['python', 'py', 'python3']
152
+ : ['python3', 'python', 'py']);
153
+ if (pythonCommand) {
154
+ const script = [
155
+ 'import os, sys, zipfile',
156
+ 'src, dest = sys.argv[1], sys.argv[2]',
157
+ 'with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as zf:',
158
+ ' for root, _, files in os.walk(src):',
159
+ ' for name in files:',
160
+ ' full = os.path.join(root, name)',
161
+ ' rel = os.path.relpath(full, src)',
162
+ ' zf.write(full, rel)',
163
+ ].join('; ');
164
+ const result = spawnSync(pythonCommand, ['-c', script, workspaceDir, outputFile], {
165
+ encoding: 'utf-8',
166
+ });
167
+ if (result.status === 0) {
168
+ return true;
169
+ }
170
+ }
171
+ if (process.platform === 'win32') {
172
+ const shell = resolveAvailableCommand(['pwsh', 'powershell']);
173
+ if (!shell) {
174
+ throw new Error('Could not find Python or PowerShell to create the Claude Desktop .mcpb archive.');
175
+ }
176
+ const command = `Compress-Archive -Path (Join-Path '${escapePowerShellPath(workspaceDir)}' '*') -DestinationPath '${escapePowerShellPath(outputFile)}' -Force`;
177
+ const result = spawnSync(shell, ['-NoProfile', '-Command', command], {
178
+ encoding: 'utf-8',
179
+ });
180
+ if (result.status === 0) {
181
+ return true;
182
+ }
183
+ }
184
+ const zipCommand = resolveAvailableCommand(['zip']);
185
+ if (zipCommand) {
186
+ const result = spawnSync(zipCommand, ['-qr', outputFile, '.'], {
187
+ cwd: workspaceDir,
188
+ encoding: 'utf-8',
189
+ });
190
+ if (result.status === 0) {
191
+ return true;
192
+ }
193
+ }
194
+ throw new Error('Failed to create a .mcpb archive. Install Python 3, PowerShell, or zip.');
195
+ }
196
+ function resolveAvailableCommand(candidates) {
197
+ for (const candidate of candidates) {
198
+ const result = spawnSync(candidate, ['--version'], { encoding: 'utf-8' });
199
+ if (result.status === 0) {
200
+ return candidate;
201
+ }
202
+ }
203
+ return undefined;
204
+ }
205
+ function escapePowerShellPath(value) {
206
+ return value.replace(/'/g, "''");
207
+ }
208
+ export function renderClaudeDesktopExtensionSummary(result) {
209
+ const lines = [
210
+ 'Claude Desktop extension scaffold ready.',
211
+ `Workspace: ${path.relative(process.cwd(), result.workspaceDir) || result.workspaceDir}`,
212
+ `Manifest: ${path.relative(process.cwd(), result.manifestPath) || result.manifestPath}`,
213
+ `Server entry: ${path.relative(process.cwd(), result.entryPointPath) || result.entryPointPath}`,
214
+ ];
215
+ if (result.packed) {
216
+ lines.push(`Package: ${path.relative(process.cwd(), result.outputFile) || result.outputFile}`);
217
+ lines.push('Install in Claude Desktop via Developer -> Extensions -> Install Extension.');
218
+ }
219
+ else {
220
+ lines.push('Archive packing skipped (--no-pack).');
221
+ }
222
+ return lines.join('\n');
223
+ }
224
+ //# sourceMappingURL=claude-desktop-extension.js.map
package/dist/core/ids.js CHANGED
@@ -11,6 +11,7 @@ const PREFIXES = {
11
11
  plan_items: 'pln',
12
12
  plan_steps: 'stp',
13
13
  instruction_entries: 'ins',
14
+ ai_surface_tasks: 'ast',
14
15
  };
15
16
  const ID_COUNTER_FILE = '.id-counter.json';
16
17
  function counterPath(cwd) {