brainclaw 0.25.3 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -81,6 +81,9 @@ import { cleanOrphanFiles, memoryDir } from './core/io.js';
81
81
  import { initLogLevel, logger } from './core/logger.js';
82
82
  import { resolveEffectiveCwd } from './core/store-resolution.js';
83
83
  import { runSwitch } from './commands/switch.js';
84
+ import { runCheckEvents } from './commands/check-events.js';
85
+ import { runDiscover } from './commands/discover.js';
86
+ import { runMigrate } from './commands/migrate.js';
84
87
  const program = new Command();
85
88
  function collect(value, previous) {
86
89
  return [...previous, value];
@@ -994,8 +997,8 @@ program
994
997
  // --- hooks ---
995
998
  program
996
999
  .command('hooks')
997
- .description('Write deterministic session-trigger hooks for Cursor (.cursor/rules/brainclaw-session.mdc) and Windsurf (.windsurfrules)')
998
- .option('--target <target>', 'Which hooks to write: cursor, windsurf, all (default: all)')
1000
+ .description('Write deterministic session-trigger hooks for Cursor, Windsurf, and Claude Code (PostToolUse event check)')
1001
+ .option('--target <target>', 'Which hooks to write: cursor, windsurf, claude-code, all (default: all)')
999
1002
  .action((options) => {
1000
1003
  runHooks(options);
1001
1004
  });
@@ -1009,6 +1012,15 @@ program
1009
1012
  .action((options) => {
1010
1013
  runWatch({ ...options, autoClaim: options.autoClaim });
1011
1014
  });
1015
+ // --- check-events ---
1016
+ program
1017
+ .command('check-events')
1018
+ .description('Show unseen events from the event bus (events.jsonl) for the current agent')
1019
+ .option('--agent <name>', 'Agent name for cursor lookup (default: auto-detected)')
1020
+ .option('--json', 'Output as JSON')
1021
+ .action((options) => {
1022
+ runCheckEvents(options);
1023
+ });
1012
1024
  // --- metrics ---
1013
1025
  program
1014
1026
  .command('metrics')
@@ -1112,6 +1124,24 @@ program
1112
1124
  .action((options) => {
1113
1125
  runExplore({ query: options.query });
1114
1126
  });
1127
+ // --- discover ---
1128
+ program
1129
+ .command('discover')
1130
+ .description('Scan workspace for MCP configs, instruction files, skills, hooks, and agent integrations')
1131
+ .option('--json', 'Output as JSON')
1132
+ .option('--no-save', 'Do not persist discovery profile to .brainclaw/discovery/')
1133
+ .action((options) => {
1134
+ runDiscover({ json: options.json, save: options.save });
1135
+ });
1136
+ // --- migrate ---
1137
+ program
1138
+ .command('migrate')
1139
+ .description('Migrate memory items between stores (e.g. promote machine-scoped items to user store)')
1140
+ .option('--promote-machine-items', 'Move items with scope:machine from project store to user store (~/.brainclaw/)')
1141
+ .option('--dry-run', 'Show what would be moved without actually moving')
1142
+ .action((options) => {
1143
+ runMigrate({ promoteMachineItems: options.promoteMachineItems, dryRun: options.dryRun });
1144
+ });
1115
1145
  program
1116
1146
  .command('switch [project]')
1117
1147
  .description('Set the active project for subsequent commands')
@@ -1,11 +1,10 @@
1
- import { loadState, persistState } from '../core/state.js';
2
1
  import { resolveCurrentAgentName } from '../core/agent-registry.js';
3
2
  import { memoryExists } from '../core/io.js';
4
- import { generateIdWithLabel, nowISO } from '../core/ids.js';
5
3
  import { loadConfig } from '../core/config.js';
6
4
  import { scanText } from '../core/security.js';
7
5
  import { validateCliInput } from '../core/input-validation.js';
8
6
  import { resolveTargetStore } from '../core/store-resolution.js';
7
+ import { listCapabilities, createCapability } from '../core/registries.js';
9
8
  export function runCapability(subcommand, args, options = {}) {
10
9
  const cwd = resolveTargetStore(options.cwd ?? process.cwd(), options.store ?? 'local');
11
10
  if (!memoryExists(cwd)) {
@@ -38,10 +37,7 @@ export function runCapability(subcommand, args, options = {}) {
38
37
  }
39
38
  }
40
39
  function runCapabilityList(cwd) {
41
- const state = loadState(cwd);
42
- const capabilities = state.active_constraints
43
- .filter((c) => c.tags.includes('capability'))
44
- .map((c) => ({ id: c.id, name: c.text.split('\n')[0], tags: c.tags }));
40
+ const capabilities = listCapabilities(cwd);
45
41
  if (capabilities.length === 0) {
46
42
  console.log('No capabilities registered yet.');
47
43
  return;
@@ -49,8 +45,8 @@ function runCapabilityList(cwd) {
49
45
  console.log(`\n${capabilities.length} capability(ies):\n`);
50
46
  capabilities.forEach((cap) => {
51
47
  console.log(` [${cap.id}] ${cap.name}`);
52
- if (cap.tags.length > 1) {
53
- console.log(` tags: ${cap.tags.filter((t) => t !== 'capability').join(', ')}`);
48
+ if (cap.tags.length > 0) {
49
+ console.log(` tags: ${cap.tags.join(', ')}`);
54
50
  }
55
51
  });
56
52
  console.log('');
@@ -66,36 +62,29 @@ function runCapabilityAdd(name, description, options, cwd) {
66
62
  process.exit(1);
67
63
  }
68
64
  }
69
- const state = loadState(cwd);
70
- const { id, short_label } = generateIdWithLabel('recent_decisions');
71
- const entry = {
72
- id,
73
- short_label,
74
- text: name,
75
- created_at: nowISO(),
65
+ const cap = createCapability({
66
+ name,
67
+ description,
68
+ tags: options.tag,
76
69
  author: options.author ?? resolveCurrentAgentName(cwd),
77
- tags: ['capability', ...(options.tag ?? [])],
78
- };
79
- // For now, store as decision to avoid schema migration
80
- // Will migrate to separate capability storage in v0.16
81
- state.recent_decisions.push(entry);
82
- persistState(state, cwd);
83
- console.log(`✔ Capability added: [${id}] ${name}`);
84
- console.log(' (Stored in decisions for now; will move to dedicated registry in v0.16)');
70
+ }, cwd);
71
+ console.log(`✔ Capability added: [${cap.id}] ${name}`);
85
72
  }
86
73
  function runCapabilityDescribe(capId, cwd) {
87
- const state = loadState(cwd);
88
- const decision = state.recent_decisions.find((d) => d.id === capId || d.short_label === capId);
89
- if (!decision) {
74
+ const capabilities = listCapabilities(cwd);
75
+ const cap = capabilities.find((c) => c.id === capId || c.id.startsWith(capId));
76
+ if (!cap) {
90
77
  console.error(`Error: capability '${capId}' not found`);
91
78
  process.exit(1);
92
79
  }
93
- console.log(`\nCapability: ${decision.text}`);
94
- console.log(`ID: ${decision.id}`);
95
- console.log(`Author: ${decision.author}`);
96
- console.log(`Created: ${decision.created_at}`);
97
- if (decision.tags.length > 1) {
98
- console.log(`Tags: ${decision.tags.filter((t) => t !== 'capability').join(', ')}`);
80
+ console.log(`\nCapability: ${cap.name}`);
81
+ console.log(`Description: ${cap.description}`);
82
+ console.log(`ID: ${cap.id}`);
83
+ console.log(`Category: ${cap.category}`);
84
+ console.log(`Author: ${cap.author}`);
85
+ console.log(`Created: ${cap.created_at}`);
86
+ if (cap.tags.length > 0) {
87
+ console.log(`Tags: ${cap.tags.join(', ')}`);
99
88
  }
100
89
  console.log('');
101
90
  }
@@ -0,0 +1,36 @@
1
+ import { memoryExists } from '../core/io.js';
2
+ import { readUnseenEvents, buildNotificationSummary } from '../core/event-log.js';
3
+ import { resolveCurrentAgentName } from '../core/agent-registry.js';
4
+ export function runCheckEvents(options = {}) {
5
+ if (!memoryExists()) {
6
+ console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
7
+ process.exit(1);
8
+ }
9
+ const agent = options.agent ?? resolveCurrentAgentName();
10
+ const events = readUnseenEvents(agent);
11
+ if (events.length === 0) {
12
+ if (options.json) {
13
+ console.log(JSON.stringify({ agent, unseen: 0 }));
14
+ }
15
+ else {
16
+ console.log('No unseen events.');
17
+ }
18
+ return;
19
+ }
20
+ const summary = buildNotificationSummary(events) ?? {};
21
+ if (options.json) {
22
+ console.log(JSON.stringify({ agent, unseen: events.length, summary, events }));
23
+ return;
24
+ }
25
+ console.log(`${events.length} unseen event(s) since last read:\n`);
26
+ for (const [key, count] of Object.entries(summary)) {
27
+ console.log(` ${key}: ${count}`);
28
+ }
29
+ console.log('');
30
+ for (const evt of events) {
31
+ const id = evt.item_id ? ` [${evt.item_id.slice(0, 12)}]` : '';
32
+ const sum = evt.summary ? ` — ${evt.summary}` : '';
33
+ console.log(` ${evt.ts} ${evt.agent} ${evt.action}:${evt.item_type}${id}${sum}`);
34
+ }
35
+ }
36
+ //# sourceMappingURL=check-events.js.map
@@ -0,0 +1,21 @@
1
+ import { memoryExists } from '../core/io.js';
2
+ import { buildProjectDiscovery, saveDiscoveryProfile, renderDiscoverySummary, } from '../core/project-discovery.js';
3
+ export function runDiscover(options = {}) {
4
+ const cwd = options.cwd ?? process.cwd();
5
+ if (!memoryExists(cwd)) {
6
+ console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
7
+ process.exit(1);
8
+ }
9
+ const profile = buildProjectDiscovery({ cwd });
10
+ // Save by default (non-destructive refresh)
11
+ if (options.save !== false) {
12
+ saveDiscoveryProfile(profile, cwd);
13
+ }
14
+ if (options.json) {
15
+ console.log(JSON.stringify(profile, null, 2));
16
+ }
17
+ else {
18
+ console.log(renderDiscoverySummary(profile));
19
+ }
20
+ }
21
+ //# sourceMappingURL=discover.js.map
@@ -1,4 +1,5 @@
1
1
  import { listAgentIdentities, resolveCurrentAgentIdentity } from '../core/agent-registry.js';
2
+ import { listCapabilities as listRegistryCapabilities, listTools as listRegistryTools } from '../core/registries.js';
2
3
  import { buildReputationSummary } from '../core/reputation.js';
3
4
  import { buildCircuitBreakerSnapshot } from '../core/circuit-breaker.js';
4
5
  import { loadState } from '../core/state.js';
@@ -1195,28 +1196,26 @@ export function runDoctor(options = {}) {
1195
1196
  console.log('✔ Cross-level duplicates: no duplicates across store levels');
1196
1197
  }
1197
1198
  }
1198
- // Metadata consistency checks
1199
- const capabilities = state.recent_decisions.filter((d) => d.tags.includes('capability'));
1200
- const tools = state.recent_decisions.filter((d) => d.tags.includes('tool'));
1199
+ // Metadata consistency checks (capabilities/tools from dedicated registries)
1200
+ const capabilities = listRegistryCapabilities(options.cwd);
1201
+ const tools = listRegistryTools(options.cwd);
1201
1202
  const metadataIssues = [];
1202
1203
  // Check capabilities completeness
1203
1204
  capabilities.forEach((cap) => {
1204
- const category = cap.tags.find((t) => t !== 'capability');
1205
- if (!category) {
1205
+ if (!cap.category) {
1206
1206
  metadataIssues.push(`Capability [${cap.id}] missing category`);
1207
1207
  }
1208
- if (!cap.text || cap.text.trim().length === 0) {
1209
- metadataIssues.push(`Capability [${cap.id}] has empty description`);
1208
+ if (!cap.name || cap.name.trim().length === 0) {
1209
+ metadataIssues.push(`Capability [${cap.id}] has empty name`);
1210
1210
  }
1211
1211
  });
1212
1212
  // Check tools completeness
1213
1213
  tools.forEach((tool) => {
1214
- const type = tool.tags.find((t) => t !== 'tool');
1215
- if (!type) {
1214
+ if (!tool.type) {
1216
1215
  metadataIssues.push(`Tool [${tool.id}] missing type`);
1217
1216
  }
1218
- if (!tool.text || tool.text.trim().length === 0) {
1219
- metadataIssues.push(`Tool [${tool.id}] has empty description`);
1217
+ if (!tool.name || tool.name.trim().length === 0) {
1218
+ metadataIssues.push(`Tool [${tool.id}] has empty name`);
1220
1219
  }
1221
1220
  });
1222
1221
  if (metadataIssues.length > 0) {
@@ -1245,6 +1244,63 @@ export function runDoctor(options = {}) {
1245
1244
  }
1246
1245
  }
1247
1246
  catch { /* non-fatal */ }
1247
+ // Check for machine-scoped items in project store (should be in user store)
1248
+ try {
1249
+ const machineInProject = [
1250
+ ...state.active_constraints.filter((c) => c.scope === 'machine'),
1251
+ ...state.recent_decisions.filter((d) => d.scope === 'machine'),
1252
+ ...state.known_traps.filter((t) => t.scope === 'machine'),
1253
+ ];
1254
+ if (machineInProject.length > 0) {
1255
+ const ids = machineInProject.map((i) => i.id).slice(0, 5);
1256
+ checks.push({
1257
+ name: 'machine_scope_placement',
1258
+ status: 'warn',
1259
+ message: `${machineInProject.length} machine-scoped item(s) in project store — consider promoting to user store with 'brainclaw migrate --promote-machine-items'`,
1260
+ details: ids,
1261
+ });
1262
+ if (!options.json) {
1263
+ console.warn(`⚠ ${machineInProject.length} machine-scoped item(s) in project store: ${ids.join(', ')}`);
1264
+ }
1265
+ }
1266
+ else {
1267
+ checks.push({ name: 'machine_scope_placement', status: 'ok', message: 'No machine-scoped items misplaced in project store' });
1268
+ if (!options.json) {
1269
+ console.log('✔ Machine-scope placement: no misplaced items');
1270
+ }
1271
+ }
1272
+ }
1273
+ catch { /* non-fatal */ }
1274
+ // Workflow hygiene check (Phase 9)
1275
+ try {
1276
+ const activeClaims = listClaims(options.cwd).filter((c) => c.status === 'active');
1277
+ const inProgressPlans = state.plan_items.filter((p) => p.status === 'in_progress');
1278
+ const workflowIssues = [];
1279
+ if (activeClaims.length > 3) {
1280
+ workflowIssues.push(`${activeClaims.length} active claims — consider releasing finished ones`);
1281
+ }
1282
+ const unclaimedInProgress = inProgressPlans.filter((p) => !activeClaims.some((c) => c.plan_id === p.id));
1283
+ if (unclaimedInProgress.length > 0) {
1284
+ workflowIssues.push(`${unclaimedInProgress.length} in-progress plan(s) without a claim`);
1285
+ }
1286
+ if (workflowIssues.length > 0) {
1287
+ checks.push({
1288
+ name: 'workflow_hygiene',
1289
+ status: 'warn',
1290
+ message: `Workflow hygiene: ${workflowIssues.join('; ')}`,
1291
+ details: workflowIssues,
1292
+ });
1293
+ if (!options.json) {
1294
+ console.warn(`⚠ Workflow hygiene: ${workflowIssues.join('; ')}`);
1295
+ }
1296
+ }
1297
+ else {
1298
+ checks.push({ name: 'workflow_hygiene', status: 'ok', message: 'Workflow hygiene OK' });
1299
+ if (!options.json)
1300
+ console.log('✔ Workflow hygiene: OK');
1301
+ }
1302
+ }
1303
+ catch { /* non-fatal */ }
1248
1304
  }
1249
1305
  catch { /* non-fatal */ }
1250
1306
  if (options.json) {
@@ -1,14 +1,13 @@
1
- import { loadState } from '../core/state.js';
2
1
  import { memoryExists } from '../core/io.js';
2
+ import { listCapabilities, listTools } from '../core/registries.js';
3
3
  export function runExplore(options = {}) {
4
4
  const cwd = options.cwd ?? process.cwd();
5
5
  if (!memoryExists(cwd)) {
6
6
  console.error('Error: .brainclaw/ not found. Run `brainclaw init` first.');
7
7
  process.exit(1);
8
8
  }
9
- const state = loadState(cwd);
10
- const capabilities = state.recent_decisions.filter((d) => d.tags.includes('capability'));
11
- const tools = state.recent_decisions.filter((d) => d.tags.includes('tool'));
9
+ const capabilities = listCapabilities(cwd);
10
+ const tools = listTools(cwd);
12
11
  console.log('\n╔════════════════════════════════════════════════════════╗');
13
12
  console.log('║ Project Capabilities & Tools ║');
14
13
  console.log('╚════════════════════════════════════════════════════════╝\n');
@@ -18,9 +17,8 @@ export function runExplore(options = {}) {
18
17
  }
19
18
  else {
20
19
  capabilities.forEach((cap) => {
21
- const category = cap.tags.find((t) => t !== 'capability') || 'general';
22
- console.log(` [${cap.id}] ${cap.text.split('\n')[0]}`);
23
- console.log(` Category: ${category}\n`);
20
+ console.log(` [${cap.id}] ${cap.name}`);
21
+ console.log(` Category: ${cap.category}\n`);
24
22
  });
25
23
  }
26
24
  console.log(`🔧 Tools (${tools.length}):\n`);
@@ -29,9 +27,8 @@ export function runExplore(options = {}) {
29
27
  }
30
28
  else {
31
29
  tools.forEach((tool) => {
32
- const type = tool.tags.find((t) => t !== 'tool') || 'utility';
33
- console.log(` [${tool.id}] ${tool.text.split('\n')[0]}`);
34
- console.log(` Type: ${type}\n`);
30
+ console.log(` [${tool.id}] ${tool.name}`);
31
+ console.log(` Type: ${tool.type}\n`);
35
32
  });
36
33
  }
37
34
  if (capabilities.length === 0 && tools.length === 0) {
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { memoryExists } from '../core/io.js';
4
4
  import { loadConfig } from '../core/config.js';
5
- import { BRAINCLAW_SECTION_START, BRAINCLAW_SECTION_END, upsertBrainclawSection } from '../core/agent-files.js';
5
+ import { BRAINCLAW_SECTION_START, BRAINCLAW_SECTION_END, upsertBrainclawSection, ensureClaudeCodeSettings } from '../core/agent-files.js';
6
6
  /**
7
7
  * Generate the Cursor MDC hook file content.
8
8
  * Uses MDC frontmatter with `alwaysApply: true` so Cursor injects it
@@ -25,14 +25,14 @@ brainclaw context
25
25
  This loads the shared project memory: active constraints, recent decisions, known traps,
26
26
  open plan items, active claims, and the last handoff.
27
27
 
28
- **Before finishing any session:**
29
-
30
- \`\`\`bash
31
- brainclaw claim release <id> # release claims you opened
32
- brainclaw plan update <id> --status done # close plan items you completed
33
- # or in one shot:
34
- brainclaw session-end --auto-release
35
- \`\`\`
28
+ **Before finishing any session:**
29
+
30
+ \`\`\`bash
31
+ brainclaw claim release <id> # release claims you opened
32
+ brainclaw plan update <id> --status done # close plan items you completed
33
+ # or in one shot:
34
+ brainclaw session-end --auto-release
35
+ \`\`\`
36
36
  `;
37
37
  }
38
38
  /**
@@ -55,14 +55,14 @@ brainclaw context
55
55
  This gives you: active constraints, recent decisions, known traps, open plans, active claims,
56
56
  and the last handoff note. Do not skip this step.
57
57
 
58
- ## SESSION END (before finishing)
59
-
60
- \`\`\`bash
61
- brainclaw claim release <id> # for each claim you hold
62
- brainclaw plan update <id> --status done # for each plan item you completed
63
- # or:
64
- brainclaw session-end --auto-release
65
- \`\`\`
58
+ ## SESSION END (before finishing)
59
+
60
+ \`\`\`bash
61
+ brainclaw claim release <id> # for each claim you hold
62
+ brainclaw plan update <id> --status done # for each plan item you completed
63
+ # or:
64
+ brainclaw session-end --auto-release
65
+ \`\`\`
66
66
  `;
67
67
  }
68
68
  export function writeHook(content, relativePath, cwd) {
@@ -100,6 +100,14 @@ export function runHooks(options = {}) {
100
100
  const content = generateWindsurfHook(config.project_name);
101
101
  results.push(writeHook(content, '.windsurfrules', cwd));
102
102
  }
103
+ if (target === 'claude-code' || target === 'all') {
104
+ const autoResult = ensureClaudeCodeSettings(cwd);
105
+ results.push({
106
+ target: 'claude-code',
107
+ relativePath: autoResult.relativePath ?? '.claude/settings.local.json',
108
+ created: autoResult.created,
109
+ });
110
+ }
103
111
  for (const r of results) {
104
112
  console.log(`✔ Hook written to ${r.relativePath} (${r.created ? 'created' : 'updated'})`);
105
113
  }
@@ -118,6 +126,14 @@ export function writeDetectedAgentHooks(agentName, projectName, cwd) {
118
126
  const content = generateWindsurfHook(projectName);
119
127
  results.push(writeHook(content, '.windsurfrules', cwd));
120
128
  }
129
+ if (agentName === 'claude-code') {
130
+ const autoResult = ensureClaudeCodeSettings(cwd);
131
+ results.push({
132
+ target: 'claude-code',
133
+ relativePath: autoResult.relativePath ?? '.claude/settings.local.json',
134
+ created: autoResult.created,
135
+ });
136
+ }
121
137
  return results;
122
138
  }
123
139
  //# sourceMappingURL=hooks.js.map