brainclaw 1.28.6 → 1.28.8

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.
Binary file
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { Command } from 'commander';
5
5
  import { getInstalledBrainclawVersion } from './core/brainclaw-version.js';
6
- import { cleanOrphanFiles, memoryDir } from './core/io.js';
6
+ import { cleanOrphanFiles, isSafeSessionId, memoryDir } from './core/io.js';
7
7
  import { initLogLevel, logger } from './core/logger.js';
8
8
  import { resolveEffectiveCwd } from './core/store-resolution.js';
9
9
  import { resolveProjectCwd } from './core/cross-project.js';
@@ -82,6 +82,38 @@ function trailingGlobalOptionError(argv, actionCommand) {
82
82
  }
83
83
  return undefined;
84
84
  }
85
+ /**
86
+ * Hook subprocesses do not reliably inherit the MCP process environment. The
87
+ * agent integrations do, however, provide their session identity in the JSON
88
+ * event on stdin. Hydrate it before preAction resolves the effective project;
89
+ * otherwise a session-scoped switch silently falls back to the workspace root.
90
+ *
91
+ * This is deliberately best-effort: hooks are advisory, stdin may be empty or
92
+ * non-JSON, and an explicitly exported session id remains authoritative.
93
+ */
94
+ function hydrateHookSessionFromStdin(argv) {
95
+ if (!argv.includes('--hook') || process.stdin.isTTY || process.env.BRAINCLAW_SESSION_ID)
96
+ return;
97
+ try {
98
+ const raw = fs.readFileSync(0, 'utf8').trim();
99
+ if (!raw)
100
+ return;
101
+ const event = JSON.parse(raw);
102
+ const sessionId = typeof event.session_id === 'string'
103
+ ? event.session_id.trim()
104
+ : typeof event.sessionId === 'string'
105
+ ? event.sessionId.trim()
106
+ : typeof event.metadata?.session === 'string'
107
+ ? event.metadata.session.trim()
108
+ : '';
109
+ if (sessionId && isSafeSessionId(sessionId)) {
110
+ process.env.BRAINCLAW_SESSION_ID = sessionId;
111
+ }
112
+ }
113
+ catch {
114
+ // Invalid hook input must never break the agent's prompt loop.
115
+ }
116
+ }
85
117
  /**
86
118
  * Resolve the (possibly nested) subcommand named in argv without parsing.
87
119
  * Used to run the trailing-global-option guard BEFORE Commander parses:
@@ -122,7 +154,9 @@ program
122
154
  .option('--cwd <path>', 'Override working directory for this invocation')
123
155
  .option('--project <name>', 'Run the command against a linked project (cross_project_links or workspace store-chain child). Resolves via resolveProjectCwd; mutually exclusive with --cwd.')
124
156
  .hook('preAction', (_thisCommand, actionCommand) => {
125
- const root = parseLeadingGlobalOptions(process.argv.slice(2));
157
+ const argv = process.argv.slice(2);
158
+ hydrateHookSessionFromStdin(argv);
159
+ const root = parseLeadingGlobalOptions(argv);
126
160
  initLogLevel({ verbose: root.verbose, debug: root.debug });
127
161
  // Skip effective cwd resolution for commands that create the store
128
162
  const cmdName = actionCommand.name();
@@ -146,14 +180,18 @@ program
146
180
  }
147
181
  if (!skipResolution) {
148
182
  // Resolve effective cwd (explicit > BRAINCLAW_PROJECT > active-project > process.cwd)
183
+ const resolutionStarted = performance.now();
149
184
  const effectiveCwd = resolveEffectiveCwd({ explicitCwd });
185
+ logger.debug(`Startup project resolution: ${(performance.now() - resolutionStarted).toFixed(1)}ms`);
150
186
  if (effectiveCwd !== process.cwd()) {
151
187
  // Change process.cwd() so all commands resolve the correct store
152
188
  // without needing individual --cwd plumbing
153
189
  process.chdir(effectiveCwd);
154
190
  logger.info(`Resolved effective cwd: ${effectiveCwd}`);
155
191
  }
192
+ const cleanupStarted = performance.now();
156
193
  const removed = cleanOrphanFiles(memoryDir());
194
+ logger.debug(`Startup orphan cleanup: ${(performance.now() - cleanupStarted).toFixed(1)}ms`);
157
195
  if (removed > 0) {
158
196
  logger.info(`Cleaned ${removed} orphan lock/tmp file(s) in ${memoryDir()}`);
159
197
  }
package/dist/core/io.js CHANGED
@@ -519,30 +519,32 @@ export function writeFileAtomic(filepath, content, options = {}) {
519
519
  * Call once at CLI startup. Returns count of removed files.
520
520
  */
521
521
  export function cleanOrphanFiles(dirPath) {
522
+ return cleanOrphanDirectory(dirPath, true);
523
+ }
524
+ function cleanOrphanDirectory(dirPath, root) {
522
525
  let removed = 0;
523
- if (!fs.existsSync(dirPath))
524
- return 0;
525
- // Clean .tmp files (residual from crashed writeFileAtomic)
526
+ // Historical/derived trees are not startup-maintenance targets. Their writers
527
+ // reclaim stale locks when acquiring them; scanning their contents here makes
528
+ // connection latency proportional to Git history and Code Map size.
529
+ const excluded = new Set(['.git', 'code', 'archive', 'gc-backups', 'migration-backups', 'recovery-backups']);
526
530
  try {
527
- for (const entry of fs.readdirSync(dirPath)) {
528
- const full = path.join(dirPath, entry);
529
- let stat;
530
- try {
531
- stat = fs.statSync(full);
531
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
532
+ const full = path.join(dirPath, entry.name);
533
+ // Dirents avoid a stat per entity and do not follow symlinks/junctions.
534
+ if (entry.isDirectory()) {
535
+ const name = entry.name.toLowerCase();
536
+ if (name !== '.git' && !(root && excluded.has(name)))
537
+ removed += cleanOrphanDirectory(full, false);
532
538
  }
533
- catch {
534
- continue;
535
- }
536
- if (entry.endsWith('.tmp') && stat.isFile() && shouldRemoveTmp(entry, stat)) {
539
+ else if (entry.isFile() && tempOwnerPid(entry.name)) {
537
540
  try {
538
- fs.unlinkSync(full);
539
- removed++;
541
+ const stat = fs.lstatSync(full);
542
+ if (stat.isFile() && shouldRemoveTmp(entry.name, stat)) {
543
+ fs.unlinkSync(full);
544
+ removed++;
545
+ }
540
546
  }
541
- catch { /* already gone */ }
542
- }
543
- // Recurse into subdirectories
544
- if (stat.isDirectory()) {
545
- removed += cleanOrphanFiles(full);
547
+ catch { /* disappeared or unreadable — skip */ }
546
548
  }
547
549
  }
548
550
  }
package/dist/facts.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // Generated by scripts/emit-site-facts.mjs at build time. Do not edit manually.
2
- // Source: brainclaw v1.28.6 on 2026-08-28T22:03:41.224Z
2
+ // Source: brainclaw v1.28.8 on 2026-09-09T12:16:44.520Z
3
3
  export const FACTS = {
4
- "version": "1.28.6",
5
- "generated_at": "2026-08-28T22:03:41.224Z",
4
+ "version": "1.28.8",
5
+ "generated_at": "2026-09-09T12:16:44.520Z",
6
6
  "tools": {
7
7
  "count": 71,
8
8
  "published_count": 69,
@@ -479,8 +479,8 @@ export const FACTS = {
479
479
  },
480
480
  "bench": {
481
481
  "schema": "brainclaw.bench.v1",
482
- "generated_at": "2026-08-28T22:03:39.012Z",
483
- "node_version": "v24.19.0",
482
+ "generated_at": "2026-09-09T12:16:42.258Z",
483
+ "node_version": "v24.20.0",
484
484
  "platform": "linux-x64",
485
485
  "repeats": 3,
486
486
  "scenarios": [
@@ -488,7 +488,7 @@ export const FACTS = {
488
488
  "name": "cold_onboard",
489
489
  "volume": "empty",
490
490
  "description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
491
- "duration_ms_median": 77,
491
+ "duration_ms_median": 88,
492
492
  "payload_chars_median": 1640,
493
493
  "payload_tokens_est_median": 410
494
494
  },
@@ -496,7 +496,7 @@ export const FACTS = {
496
496
  "name": "warm_work",
497
497
  "volume": "medium",
498
498
  "description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
499
- "duration_ms_median": 129,
499
+ "duration_ms_median": 136,
500
500
  "payload_chars_median": 2626,
501
501
  "payload_tokens_est_median": 657
502
502
  },
package/dist/facts.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.28.6",
3
- "generated_at": "2026-08-28T22:03:41.224Z",
2
+ "version": "1.28.8",
3
+ "generated_at": "2026-09-09T12:16:44.520Z",
4
4
  "tools": {
5
5
  "count": 71,
6
6
  "published_count": 69,
@@ -477,8 +477,8 @@
477
477
  },
478
478
  "bench": {
479
479
  "schema": "brainclaw.bench.v1",
480
- "generated_at": "2026-08-28T22:03:39.012Z",
481
- "node_version": "v24.19.0",
480
+ "generated_at": "2026-09-09T12:16:42.258Z",
481
+ "node_version": "v24.20.0",
482
482
  "platform": "linux-x64",
483
483
  "repeats": 3,
484
484
  "scenarios": [
@@ -486,7 +486,7 @@
486
486
  "name": "cold_onboard",
487
487
  "volume": "empty",
488
488
  "description": "fresh machine → init → first useful context. Baseline for time-to-first-value.",
489
- "duration_ms_median": 77,
489
+ "duration_ms_median": 88,
490
490
  "payload_chars_median": 1640,
491
491
  "payload_tokens_est_median": 410
492
492
  },
@@ -494,7 +494,7 @@
494
494
  "name": "warm_work",
495
495
  "volume": "medium",
496
496
  "description": "bclaw_work consult over a real-shaped store (~200 plans / 500 handoffs / 450 claims).",
497
- "duration_ms_median": 129,
497
+ "duration_ms_median": 136,
498
498
  "payload_chars_median": 2626,
499
499
  "payload_tokens_est_median": 657
500
500
  },
@@ -564,4 +564,19 @@ The CLI remains valuable for:
564
564
  - release and packaging
565
565
  - debugging and fallback access
566
566
 
567
+ ### Diagnose MCP startup
568
+
569
+ Measure startup with a fresh subprocess rather than timing a tool call on an
570
+ existing connection:
571
+
572
+ ```bash
573
+ node scripts/bench-mcp-startup.mjs --cwd . --cli dist/cli.js --repeats 3 --budget-ms 20000
574
+ ```
575
+
576
+ The benchmark covers process launch, `initialize`, the initialized
577
+ notification, and `tools/list`. It requires an explicit project directory
578
+ because normal CLI startup maintenance may update that store. Pass `--debug`
579
+ before the `mcp` command to report project-resolution and orphan-cleanup
580
+ durations on stderr without corrupting JSON-RPC stdout.
581
+
567
582
  But for capable agents, MCP is the first-class path for both reads and writes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainclaw",
3
- "version": "1.28.6",
3
+ "version": "1.28.8",
4
4
  "description": "Shared project memory for humans and coding agents.",
5
5
  "type": "module",
6
6
  "repository": {