brainclaw 0.19.2

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.
Files changed (128) hide show
  1. package/LICENSE +74 -0
  2. package/README.md +226 -0
  3. package/dist/cli.js +1037 -0
  4. package/dist/commands/accept.js +149 -0
  5. package/dist/commands/adapter-openclaw-import.js +75 -0
  6. package/dist/commands/add-step.js +35 -0
  7. package/dist/commands/agent-board.js +106 -0
  8. package/dist/commands/audit.js +35 -0
  9. package/dist/commands/bootstrap.js +34 -0
  10. package/dist/commands/capability.js +104 -0
  11. package/dist/commands/changes.js +112 -0
  12. package/dist/commands/check-constraints.js +63 -0
  13. package/dist/commands/claim-resource.js +54 -0
  14. package/dist/commands/claim.js +92 -0
  15. package/dist/commands/complete-step.js +34 -0
  16. package/dist/commands/constraint.js +44 -0
  17. package/dist/commands/context-diff.js +32 -0
  18. package/dist/commands/context.js +63 -0
  19. package/dist/commands/decision.js +45 -0
  20. package/dist/commands/delete-plan.js +20 -0
  21. package/dist/commands/diff.js +99 -0
  22. package/dist/commands/doctor.js +1275 -0
  23. package/dist/commands/enable-agent.js +63 -0
  24. package/dist/commands/env.js +46 -0
  25. package/dist/commands/estimation-report.js +167 -0
  26. package/dist/commands/explore.js +47 -0
  27. package/dist/commands/export.js +381 -0
  28. package/dist/commands/handoff.js +63 -0
  29. package/dist/commands/history.js +22 -0
  30. package/dist/commands/hooks.js +123 -0
  31. package/dist/commands/init.js +356 -0
  32. package/dist/commands/install-hooks.js +115 -0
  33. package/dist/commands/instruction.js +56 -0
  34. package/dist/commands/list-agents.js +44 -0
  35. package/dist/commands/list-claims.js +45 -0
  36. package/dist/commands/list-instructions.js +50 -0
  37. package/dist/commands/list-plans.js +48 -0
  38. package/dist/commands/mcp-worker.js +12 -0
  39. package/dist/commands/mcp.js +2272 -0
  40. package/dist/commands/memory.js +283 -0
  41. package/dist/commands/metrics.js +175 -0
  42. package/dist/commands/plan-resource.js +62 -0
  43. package/dist/commands/plan.js +76 -0
  44. package/dist/commands/prune-candidates.js +36 -0
  45. package/dist/commands/prune.js +48 -0
  46. package/dist/commands/pull.js +25 -0
  47. package/dist/commands/push.js +28 -0
  48. package/dist/commands/rebuild.js +14 -0
  49. package/dist/commands/reflect-runtime-note.js +74 -0
  50. package/dist/commands/reflect.js +286 -0
  51. package/dist/commands/register-agent.js +29 -0
  52. package/dist/commands/reject.js +52 -0
  53. package/dist/commands/release-claim.js +41 -0
  54. package/dist/commands/release-claims.js +67 -0
  55. package/dist/commands/review.js +242 -0
  56. package/dist/commands/rollback.js +156 -0
  57. package/dist/commands/runtime-note.js +144 -0
  58. package/dist/commands/runtime-status.js +49 -0
  59. package/dist/commands/search.js +36 -0
  60. package/dist/commands/session-end.js +187 -0
  61. package/dist/commands/session-start.js +147 -0
  62. package/dist/commands/set-trust.js +92 -0
  63. package/dist/commands/setup.js +446 -0
  64. package/dist/commands/show-candidate.js +31 -0
  65. package/dist/commands/star-candidate.js +28 -0
  66. package/dist/commands/status.js +133 -0
  67. package/dist/commands/sync.js +159 -0
  68. package/dist/commands/tool.js +126 -0
  69. package/dist/commands/trap.js +74 -0
  70. package/dist/commands/update-handoff.js +23 -0
  71. package/dist/commands/update-plan.js +37 -0
  72. package/dist/commands/upgrade.js +382 -0
  73. package/dist/commands/use-candidate.js +35 -0
  74. package/dist/commands/version.js +96 -0
  75. package/dist/commands/watch.js +215 -0
  76. package/dist/commands/whoami.js +104 -0
  77. package/dist/core/agent-context.js +340 -0
  78. package/dist/core/agent-files.js +874 -0
  79. package/dist/core/agent-integrations.js +135 -0
  80. package/dist/core/agent-inventory.js +401 -0
  81. package/dist/core/agent-registry.js +420 -0
  82. package/dist/core/ai-agent-detection.js +140 -0
  83. package/dist/core/audit.js +85 -0
  84. package/dist/core/bootstrap.js +658 -0
  85. package/dist/core/brainclaw-version.js +433 -0
  86. package/dist/core/candidates.js +137 -0
  87. package/dist/core/circuit-breaker.js +118 -0
  88. package/dist/core/claims.js +72 -0
  89. package/dist/core/config.js +86 -0
  90. package/dist/core/context-diff.js +122 -0
  91. package/dist/core/context.js +1212 -0
  92. package/dist/core/contradictions.js +270 -0
  93. package/dist/core/coordination.js +86 -0
  94. package/dist/core/cross-project.js +99 -0
  95. package/dist/core/duplicates.js +72 -0
  96. package/dist/core/event-log.js +152 -0
  97. package/dist/core/events.js +56 -0
  98. package/dist/core/execution-context.js +204 -0
  99. package/dist/core/freshness.js +87 -0
  100. package/dist/core/global-registry.js +182 -0
  101. package/dist/core/host.js +10 -0
  102. package/dist/core/identity.js +151 -0
  103. package/dist/core/ids.js +56 -0
  104. package/dist/core/input-validation.js +81 -0
  105. package/dist/core/instructions.js +117 -0
  106. package/dist/core/io.js +191 -0
  107. package/dist/core/json-store.js +63 -0
  108. package/dist/core/lifecycle.js +45 -0
  109. package/dist/core/lock.js +129 -0
  110. package/dist/core/logger.js +49 -0
  111. package/dist/core/machine-profile.js +332 -0
  112. package/dist/core/markdown.js +120 -0
  113. package/dist/core/memory-git.js +133 -0
  114. package/dist/core/migration.js +247 -0
  115. package/dist/core/project-registry.js +64 -0
  116. package/dist/core/reflection-safety.js +21 -0
  117. package/dist/core/repo-analysis.js +133 -0
  118. package/dist/core/reputation.js +409 -0
  119. package/dist/core/runtime.js +134 -0
  120. package/dist/core/schema.js +580 -0
  121. package/dist/core/search.js +115 -0
  122. package/dist/core/security.js +66 -0
  123. package/dist/core/setup-state.js +50 -0
  124. package/dist/core/state.js +83 -0
  125. package/dist/core/store-resolution.js +119 -0
  126. package/dist/core/sync-remote.js +83 -0
  127. package/dist/core/traps.js +86 -0
  128. package/package.json +60 -0
@@ -0,0 +1,49 @@
1
+ const LEVEL_ORDER = {
2
+ silent: 0,
3
+ error: 1,
4
+ warn: 2,
5
+ info: 3,
6
+ debug: 4,
7
+ };
8
+ let currentLevel = 'warn';
9
+ export function setLogLevel(level) {
10
+ currentLevel = level;
11
+ }
12
+ export function getLogLevel() {
13
+ return currentLevel;
14
+ }
15
+ /** Initialise log level from --verbose/--debug flags or BRAINCLAW_LOG_LEVEL env. */
16
+ export function initLogLevel(opts) {
17
+ if (opts.debug) {
18
+ currentLevel = 'debug';
19
+ }
20
+ else if (opts.verbose) {
21
+ currentLevel = 'info';
22
+ }
23
+ else {
24
+ const env = process.env['BRAINCLAW_LOG_LEVEL'];
25
+ if (env && env in LEVEL_ORDER) {
26
+ currentLevel = env;
27
+ }
28
+ }
29
+ }
30
+ /** All output goes to stderr so it never pollutes stdout (MCP / JSON). */
31
+ export const logger = {
32
+ error(...args) {
33
+ if (LEVEL_ORDER['error'] <= LEVEL_ORDER[currentLevel])
34
+ console.error('[brainclaw:error]', ...args);
35
+ },
36
+ warn(...args) {
37
+ if (LEVEL_ORDER['warn'] <= LEVEL_ORDER[currentLevel])
38
+ console.error('[brainclaw:warn]', ...args);
39
+ },
40
+ info(...args) {
41
+ if (LEVEL_ORDER['info'] <= LEVEL_ORDER[currentLevel])
42
+ console.error('[brainclaw:info]', ...args);
43
+ },
44
+ debug(...args) {
45
+ if (LEVEL_ORDER['debug'] <= LEVEL_ORDER[currentLevel])
46
+ console.error('[brainclaw:debug]', ...args);
47
+ },
48
+ };
49
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1,332 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import yaml from 'yaml';
6
+ import { MEMORY_DIR } from './io.js';
7
+ // ── Detection Functions ────────────────────────────────────────────────────────
8
+ const TOOLCHAINS = [
9
+ { name: 'node', command: 'node', versionArgs: ['--version'] },
10
+ { name: 'npm', command: 'npm', versionArgs: ['--version'] },
11
+ { name: 'pnpm', command: 'pnpm', versionArgs: ['--version'] },
12
+ { name: 'python', command: 'python', versionArgs: ['--version'] },
13
+ { name: 'pip', command: 'pip', versionArgs: ['--version'] },
14
+ { name: 'cargo', command: 'cargo', versionArgs: ['--version'] },
15
+ { name: 'go', command: 'go', versionArgs: ['version'] },
16
+ { name: 'docker', command: 'docker', versionArgs: ['--version'] },
17
+ { name: 'java', command: 'java', versionArgs: ['-version'] },
18
+ { name: 'mvn', command: 'mvn', versionArgs: ['--version'] },
19
+ ];
20
+ function run(command, args, timeout = 5000) {
21
+ try {
22
+ const result = spawnSync(command, args, { encoding: 'utf-8', timeout, windowsHide: true });
23
+ return { ok: result.status === 0, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
24
+ }
25
+ catch {
26
+ return { ok: false, stdout: '', stderr: '' };
27
+ }
28
+ }
29
+ function firstLine(text) {
30
+ return text.split(/\r?\n/).map(l => l.trim()).find(Boolean);
31
+ }
32
+ function detectOsVariant() {
33
+ if (process.platform === 'darwin')
34
+ return 'macos';
35
+ if (process.platform === 'linux')
36
+ return 'linux';
37
+ if (process.platform === 'win32') {
38
+ // Check if WSL2 is available
39
+ const wsl = run('wsl', ['--list', '--quiet'], 3000);
40
+ if (wsl.ok && wsl.stdout.trim().length > 0)
41
+ return 'windows+wsl2';
42
+ return 'windows';
43
+ }
44
+ return 'linux'; // fallback
45
+ }
46
+ function detectShells() {
47
+ const shells = [];
48
+ const defaultShell = process.env.SHELL ?? process.env.ComSpec ?? '';
49
+ if (process.platform === 'win32') {
50
+ // Windows shells
51
+ const cmd = process.env.ComSpec ?? 'C:\\WINDOWS\\system32\\cmd.exe';
52
+ if (fs.existsSync(cmd)) {
53
+ shells.push({ name: 'cmd', path: cmd, default: cmd === defaultShell });
54
+ }
55
+ // PowerShell
56
+ const ps7 = run('pwsh', ['--version'], 3000);
57
+ if (ps7.ok)
58
+ shells.push({ name: 'pwsh', path: 'pwsh', default: false });
59
+ const ps5 = run('powershell', ['-Command', '$PSVersionTable.PSVersion.ToString()'], 3000);
60
+ if (ps5.ok)
61
+ shells.push({ name: 'powershell', path: 'powershell', default: false });
62
+ // Git Bash
63
+ const gitBash = 'C:\\Program Files\\Git\\bin\\bash.exe';
64
+ if (fs.existsSync(gitBash))
65
+ shells.push({ name: 'bash (git)', path: gitBash, default: false });
66
+ }
67
+ else {
68
+ // Unix shells
69
+ const unixShells = [
70
+ { name: 'bash', command: 'bash' },
71
+ { name: 'zsh', command: 'zsh' },
72
+ { name: 'fish', command: 'fish' },
73
+ { name: 'sh', command: 'sh' },
74
+ ];
75
+ for (const s of unixShells) {
76
+ const which = run('which', [s.command], 2000);
77
+ if (which.ok) {
78
+ const shellPath = which.stdout.trim();
79
+ shells.push({ name: s.name, path: shellPath, default: defaultShell.endsWith(s.command) });
80
+ }
81
+ }
82
+ }
83
+ return shells;
84
+ }
85
+ function detectGitUsers() {
86
+ const users = [];
87
+ // Global git user
88
+ const globalName = run('git', ['config', '--global', 'user.name']);
89
+ const globalEmail = run('git', ['config', '--global', 'user.email']);
90
+ if (globalName.ok && globalEmail.ok) {
91
+ const name = globalName.stdout.trim();
92
+ const email = globalEmail.stdout.trim();
93
+ if (name && email) {
94
+ users.push({ name, email, scope: 'global' });
95
+ }
96
+ }
97
+ // Try to detect host-specific users from ~/.gitconfig includes
98
+ // (e.g. [includeIf "hasconfig:remote.*.url:git@github.com:*/**"])
99
+ // This is best-effort — we just report the global user for now
100
+ // Local per-repo users are detected during project init, not machine profile
101
+ return users;
102
+ }
103
+ function detectSshKeys() {
104
+ const keys = [];
105
+ const sshDir = path.join(os.homedir(), '.ssh');
106
+ if (!fs.existsSync(sshDir))
107
+ return keys;
108
+ try {
109
+ const files = fs.readdirSync(sshDir);
110
+ const pubKeys = files.filter(f => f.endsWith('.pub'));
111
+ for (const pubFile of pubKeys) {
112
+ const pubPath = path.join(sshDir, pubFile);
113
+ try {
114
+ const content = fs.readFileSync(pubPath, 'utf-8').trim();
115
+ const parts = content.split(/\s+/);
116
+ const keyType = parts[0] ?? 'unknown';
117
+ const keyName = pubFile.replace(/\.pub$/, '');
118
+ const keyInfo = { name: keyName, path: pubPath, type: keyType };
119
+ // Try to find configured host in ~/.ssh/config
120
+ const configuredHost = findSshConfigHost(sshDir, keyName);
121
+ if (configuredHost)
122
+ keyInfo.configured_host = configuredHost;
123
+ keys.push(keyInfo);
124
+ }
125
+ catch {
126
+ // Skip unreadable key files
127
+ }
128
+ }
129
+ }
130
+ catch {
131
+ // Skip if directory not readable
132
+ }
133
+ return keys;
134
+ }
135
+ function findSshConfigHost(sshDir, keyName) {
136
+ const configPath = path.join(sshDir, 'config');
137
+ if (!fs.existsSync(configPath))
138
+ return undefined;
139
+ try {
140
+ const content = fs.readFileSync(configPath, 'utf-8');
141
+ const lines = content.split(/\r?\n/);
142
+ let currentHost;
143
+ for (const line of lines) {
144
+ const trimmed = line.trim();
145
+ const hostMatch = trimmed.match(/^Host\s+(.+)/i);
146
+ if (hostMatch) {
147
+ currentHost = hostMatch[1].trim();
148
+ continue;
149
+ }
150
+ const identityMatch = trimmed.match(/^IdentityFile\s+(.+)/i);
151
+ if (identityMatch && currentHost) {
152
+ const identityPath = identityMatch[1].trim().replace(/^~/, os.homedir());
153
+ if (identityPath.includes(keyName)) {
154
+ return currentHost;
155
+ }
156
+ }
157
+ }
158
+ }
159
+ catch {
160
+ // Non-fatal
161
+ }
162
+ return undefined;
163
+ }
164
+ function detectToolchains() {
165
+ return TOOLCHAINS.map(tool => {
166
+ const result = run(tool.command, tool.versionArgs);
167
+ if (!result.ok) {
168
+ return { name: tool.name, available: false };
169
+ }
170
+ const versionLine = firstLine(result.stdout || result.stderr) ?? '';
171
+ // Extract version number from various formats
172
+ const versionMatch = versionLine.match(/v?(\d+\.\d+[\w.-]*)/);
173
+ const version = versionMatch ? versionMatch[1] : versionLine;
174
+ // Try to get path
175
+ let toolPath;
176
+ if (process.platform === 'win32') {
177
+ const where = run('where', [tool.command], 3000);
178
+ if (where.ok)
179
+ toolPath = firstLine(where.stdout);
180
+ }
181
+ else {
182
+ const which = run('which', [tool.command], 2000);
183
+ if (which.ok)
184
+ toolPath = which.stdout.trim();
185
+ }
186
+ return { name: tool.name, available: true, version, path: toolPath };
187
+ });
188
+ }
189
+ function detectWslDistros() {
190
+ if (process.platform !== 'win32')
191
+ return [];
192
+ const result = run('wsl', ['--list', '--verbose'], 5000);
193
+ if (!result.ok)
194
+ return [];
195
+ const distros = [];
196
+ const lines = result.stdout.split(/\r?\n/).filter(l => l.trim());
197
+ // Skip header line (NAME STATE VERSION)
198
+ for (let i = 1; i < lines.length; i++) {
199
+ const line = lines[i];
200
+ // Clean null bytes (common in wsl --list output on Windows)
201
+ const cleaned = line.replace(/\0/g, '').trim();
202
+ if (!cleaned)
203
+ continue;
204
+ const isDefault = cleaned.startsWith('*');
205
+ const parts = cleaned.replace(/^\*\s*/, '').trim().split(/\s+/);
206
+ const name = parts[0];
207
+ if (!name || name === 'NAME')
208
+ continue;
209
+ const distro = { name, default: isDefault };
210
+ // Detect node inside WSL
211
+ const nodeResult = run('wsl', ['-d', name, '--', 'node', '--version'], 5000);
212
+ if (nodeResult.ok) {
213
+ const ver = firstLine(nodeResult.stdout);
214
+ distro.node_version = ver?.replace(/^v/, '');
215
+ // Get node path inside WSL
216
+ const whichNode = run('wsl', ['-d', name, '--', 'which', 'node'], 3000);
217
+ if (whichNode.ok)
218
+ distro.node_path = whichNode.stdout.trim();
219
+ }
220
+ distros.push(distro);
221
+ }
222
+ return distros;
223
+ }
224
+ // ── Public API ─────────────────────────────────────────────────────────────────
225
+ /**
226
+ * Build a complete machine profile by detecting all system capabilities.
227
+ */
228
+ export function buildMachineProfile() {
229
+ return {
230
+ schema_version: 1,
231
+ generated_at: new Date().toISOString(),
232
+ hostname: os.hostname(),
233
+ os_user: os.userInfo().username,
234
+ home_dir: os.homedir(),
235
+ os_variant: detectOsVariant(),
236
+ platform: process.platform,
237
+ os_release: os.release(),
238
+ arch: os.arch(),
239
+ shells: detectShells(),
240
+ git_users: detectGitUsers(),
241
+ ssh_keys: detectSshKeys(),
242
+ toolchains: detectToolchains(),
243
+ wsl_distros: detectWslDistros(),
244
+ };
245
+ }
246
+ /**
247
+ * Path to the machine profile file.
248
+ */
249
+ export function machineProfilePath() {
250
+ return path.join(os.homedir(), MEMORY_DIR, 'machine.yaml');
251
+ }
252
+ /**
253
+ * Save a machine profile to ~/.brainclaw/machine.yaml.
254
+ */
255
+ export function saveMachineProfile(profile) {
256
+ const filePath = machineProfilePath();
257
+ const dir = path.dirname(filePath);
258
+ if (!fs.existsSync(dir)) {
259
+ fs.mkdirSync(dir, { recursive: true });
260
+ }
261
+ const content = yaml.stringify(profile, { lineWidth: 120 });
262
+ fs.writeFileSync(filePath, content, 'utf-8');
263
+ return filePath;
264
+ }
265
+ /**
266
+ * Load the machine profile from ~/.brainclaw/machine.yaml.
267
+ * Returns undefined if no profile exists.
268
+ */
269
+ export function loadMachineProfile() {
270
+ const filePath = machineProfilePath();
271
+ if (!fs.existsSync(filePath))
272
+ return undefined;
273
+ try {
274
+ const content = fs.readFileSync(filePath, 'utf-8');
275
+ return yaml.parse(content);
276
+ }
277
+ catch {
278
+ return undefined;
279
+ }
280
+ }
281
+ /**
282
+ * Render a human-readable summary of the machine profile.
283
+ */
284
+ export function renderMachineProfileSummary(profile) {
285
+ const lines = [];
286
+ lines.push(`Machine: ${profile.hostname} (user: ${profile.os_user})`);
287
+ lines.push(`Home: ${profile.home_dir}`);
288
+ lines.push(`OS: ${profile.os_variant} (${profile.platform} ${profile.os_release}, ${profile.arch})`);
289
+ // Shells
290
+ const defaultShell = profile.shells.find(s => s.default);
291
+ const otherShells = profile.shells.filter(s => !s.default);
292
+ if (defaultShell) {
293
+ lines.push(`Default shell: ${defaultShell.name}${defaultShell.path ? ` (${defaultShell.path})` : ''}`);
294
+ }
295
+ if (otherShells.length > 0) {
296
+ lines.push(`Other shells: ${otherShells.map(s => s.name).join(', ')}`);
297
+ }
298
+ // Git users
299
+ if (profile.git_users.length > 0) {
300
+ lines.push(`Git users:`);
301
+ for (const u of profile.git_users) {
302
+ lines.push(` - ${u.name} <${u.email}> (${u.scope}${u.host ? `, ${u.host}` : ''})`);
303
+ }
304
+ }
305
+ // SSH keys
306
+ if (profile.ssh_keys.length > 0) {
307
+ lines.push(`SSH keys:`);
308
+ for (const k of profile.ssh_keys) {
309
+ lines.push(` - ${k.name} (${k.type}${k.configured_host ? `, host: ${k.configured_host}` : ''})`);
310
+ }
311
+ }
312
+ // Toolchains
313
+ const available = profile.toolchains.filter(t => t.available);
314
+ const unavailable = profile.toolchains.filter(t => !t.available);
315
+ if (available.length > 0) {
316
+ lines.push(`Toolchains: ${available.map(t => `${t.name} ${t.version ?? ''}`).join(', ').trim()}`);
317
+ }
318
+ if (unavailable.length > 0) {
319
+ lines.push(`Not installed: ${unavailable.map(t => t.name).join(', ')}`);
320
+ }
321
+ // WSL distros
322
+ if (profile.wsl_distros.length > 0) {
323
+ lines.push(`WSL distros:`);
324
+ for (const d of profile.wsl_distros) {
325
+ const nodeInfo = d.node_version ? `node ${d.node_version} at ${d.node_path}` : 'no node';
326
+ lines.push(` - ${d.name}${d.default ? ' (default)' : ''}: ${nodeInfo}`);
327
+ }
328
+ }
329
+ lines.push(`Profile generated: ${profile.generated_at}`);
330
+ return lines.join('\n');
331
+ }
332
+ //# sourceMappingURL=machine-profile.js.map
@@ -0,0 +1,120 @@
1
+ import { listClaims } from './claims.js';
2
+ import { loadInstructions } from './instructions.js';
3
+ import { isTrapActive } from './traps.js';
4
+ export function generateMarkdown(state, cwd) {
5
+ const lines = ['# Project Memory', ''];
6
+ const instructions = loadInstructions(cwd).filter((entry) => entry.active);
7
+ const claims = listClaims(cwd).filter((claim) => claim.status === 'active');
8
+ lines.push('## Shared instructions');
9
+ if (instructions.length === 0) {
10
+ lines.push('- (none)');
11
+ }
12
+ else {
13
+ for (const entry of instructions) {
14
+ const scope = entry.scope ? `:${entry.scope}` : '';
15
+ const tags = entry.tags.length ? ` [${entry.tags.join(', ')}]` : '';
16
+ lines.push(`- **[${entry.id}]** <${entry.layer}${scope}> ${entry.text}${tags}`);
17
+ }
18
+ }
19
+ lines.push('');
20
+ lines.push('## Active claims');
21
+ if (claims.length === 0) {
22
+ lines.push('- (none)');
23
+ }
24
+ else {
25
+ for (const claim of claims) {
26
+ const meta = [claim.scope];
27
+ if (claim.plan_id)
28
+ meta.push(`plan: ${claim.plan_id}`);
29
+ if (claim.project)
30
+ meta.push(`project: ${claim.project}`);
31
+ lines.push(`- **[${claim.id}]** ${claim.agent} → ${claim.description} _(${meta.join(', ')})_`);
32
+ }
33
+ }
34
+ lines.push('');
35
+ lines.push('## Shared plan');
36
+ const activePlans = state.plan_items.filter((plan) => plan.status !== 'done' && plan.status !== 'dropped');
37
+ if (activePlans.length === 0) {
38
+ lines.push('- (none)');
39
+ }
40
+ else {
41
+ for (const plan of activePlans) {
42
+ const meta = [plan.status, plan.priority];
43
+ if (plan.assignee)
44
+ meta.push(`assignee: ${plan.assignee}`);
45
+ if (plan.project)
46
+ meta.push(`project: ${plan.project}`);
47
+ if (plan.steps && plan.steps.length > 0) {
48
+ const done = plan.steps.filter((s) => s.status === 'done').length;
49
+ meta.push(`${done}/${plan.steps.length} steps`);
50
+ }
51
+ const tags = plan.tags.length ? ` [${plan.tags.join(', ')}]` : '';
52
+ const paths = plan.related_paths?.length ? ` → ${plan.related_paths.join(', ')}` : '';
53
+ lines.push(`- **[${plan.id}]** ${plan.text} _(${meta.join(', ')})_${paths}${tags}`);
54
+ if (plan.steps && plan.steps.length > 0) {
55
+ for (const step of plan.steps) {
56
+ const check = step.status === 'done' ? 'x' : ' ';
57
+ const assign = step.assignee ? ` _(${step.assignee})_` : '';
58
+ lines.push(` - [${check}] [${step.id}] ${step.text}${assign}`);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ lines.push('');
64
+ lines.push('## Active constraints');
65
+ if (state.active_constraints.length === 0) {
66
+ lines.push('- (none)');
67
+ }
68
+ else {
69
+ for (const c of state.active_constraints) {
70
+ const tags = c.tags.length ? ` [${c.tags.join(', ')}]` : '';
71
+ const paths = c.related_paths?.length ? ` → ${c.related_paths.join(', ')}` : '';
72
+ lines.push(`- **[${c.id}]** ${c.text} _(${c.status})_${paths}${tags}`);
73
+ }
74
+ }
75
+ lines.push('');
76
+ lines.push('## Recent decisions');
77
+ if (state.recent_decisions.length === 0) {
78
+ lines.push('- (none)');
79
+ }
80
+ else {
81
+ for (const d of state.recent_decisions) {
82
+ const tags = d.tags.length ? ` [${d.tags.join(', ')}]` : '';
83
+ const paths = d.related_paths?.length ? ` → ${d.related_paths.join(', ')}` : '';
84
+ lines.push(`- **[${d.id}]** ${d.text}${paths}${tags}`);
85
+ }
86
+ }
87
+ lines.push('');
88
+ lines.push('## Known traps');
89
+ const activeTraps = state.known_traps.filter((trap) => isTrapActive(trap));
90
+ if (activeTraps.length === 0) {
91
+ lines.push('- (none)');
92
+ }
93
+ else {
94
+ for (const t of activeTraps) {
95
+ const tags = t.tags.length ? ` [${t.tags.join(', ')}]` : '';
96
+ const paths = t.related_paths?.length ? ` → ${t.related_paths.join(', ')}` : '';
97
+ lines.push(`- **[${t.id}]** ${t.text} _(${t.severity})_${paths}${tags}`);
98
+ }
99
+ }
100
+ lines.push('');
101
+ lines.push('## Open handoffs');
102
+ if (state.open_handoffs.length === 0) {
103
+ lines.push('- (none)');
104
+ }
105
+ else {
106
+ for (const h of state.open_handoffs) {
107
+ const tags = h.tags.length ? ` [${h.tags.join(', ')}]` : '';
108
+ const paths = h.related_paths?.length ? ` → ${h.related_paths.join(', ')}` : '';
109
+ const meta = [h.status];
110
+ if (h.plan_id)
111
+ meta.push(`plan: ${h.plan_id}`);
112
+ if (h.project)
113
+ meta.push(`project: ${h.project}`);
114
+ lines.push(`- **[${h.id}]** ${h.from} → ${h.to}: ${h.text} _(${meta.join(', ')})_${paths}${tags}`);
115
+ }
116
+ }
117
+ lines.push('');
118
+ return lines.join('\n');
119
+ }
120
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1,133 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { memoryDir } from './io.js';
5
+ import { logger } from './logger.js';
6
+ const GIT_DIR_NAME = '.git';
7
+ /**
8
+ * Check if the memory directory has an internal git repo.
9
+ */
10
+ export function hasMemoryRepo(cwd) {
11
+ const gitDir = path.join(memoryDir(cwd), GIT_DIR_NAME);
12
+ return fs.existsSync(gitDir);
13
+ }
14
+ /**
15
+ * Initialize a git repo inside .brainclaw/ for memory versioning.
16
+ * Idempotent — skips if already initialized.
17
+ * Returns true if a new repo was created.
18
+ */
19
+ export function initMemoryRepo(cwd) {
20
+ const dir = memoryDir(cwd);
21
+ if (!fs.existsSync(dir))
22
+ return false;
23
+ if (hasMemoryRepo(cwd))
24
+ return false;
25
+ try {
26
+ git(dir, ['init', '--quiet']);
27
+ // Configure the repo to avoid user identity errors on commit
28
+ git(dir, ['config', 'user.name', 'brainclaw']);
29
+ git(dir, ['config', 'user.email', 'brainclaw@local']);
30
+ // Ignore temp files and locks
31
+ const gitignore = path.join(dir, '.gitignore');
32
+ if (!fs.existsSync(gitignore)) {
33
+ fs.writeFileSync(gitignore, ['*.tmp', '*.lock', ''].join('\n'), 'utf-8');
34
+ }
35
+ // Initial commit
36
+ git(dir, ['add', '-A']);
37
+ git(dir, ['commit', '--quiet', '--allow-empty', '-m', 'brainclaw: initial memory snapshot']);
38
+ return true;
39
+ }
40
+ catch (err) {
41
+ logger.debug('Failed to initialize memory git repo:', err);
42
+ return false;
43
+ }
44
+ }
45
+ /**
46
+ * Commit all pending changes in the memory repo.
47
+ * Called after write operations (saveState, saveRuntimeNote, etc.).
48
+ *
49
+ * No-op if:
50
+ * - No memory repo exists
51
+ * - No changes to commit
52
+ *
53
+ * Returns true if a commit was created.
54
+ */
55
+ export function commitMemoryChange(message, cwd) {
56
+ if (!hasMemoryRepo(cwd))
57
+ return false;
58
+ const dir = memoryDir(cwd);
59
+ try {
60
+ // Stage all changes
61
+ git(dir, ['add', '-A']);
62
+ // Check if there's anything to commit
63
+ const status = git(dir, ['status', '--porcelain']);
64
+ if (!status.trim())
65
+ return false;
66
+ git(dir, ['commit', '--quiet', '-m', message]);
67
+ return true;
68
+ }
69
+ catch (err) {
70
+ logger.debug('Failed to commit memory change:', err);
71
+ return false;
72
+ }
73
+ }
74
+ /**
75
+ * Get the short log of recent memory commits.
76
+ */
77
+ export function getMemoryLog(limit = 20, cwd) {
78
+ if (!hasMemoryRepo(cwd))
79
+ return [];
80
+ const dir = memoryDir(cwd);
81
+ try {
82
+ const output = git(dir, ['log', '--oneline', `-${limit}`]);
83
+ return output.trim().split('\n').filter(Boolean);
84
+ }
85
+ catch {
86
+ return [];
87
+ }
88
+ }
89
+ /**
90
+ * Rollback the memory to a previous commit.
91
+ * Returns true if successful.
92
+ */
93
+ export function rollbackMemory(ref, cwd) {
94
+ if (!hasMemoryRepo(cwd))
95
+ return false;
96
+ const dir = memoryDir(cwd);
97
+ try {
98
+ // Remove all tracked files, then restore from the target ref
99
+ // This ensures files added after the ref are also removed
100
+ git(dir, ['rm', '-rf', '--quiet', '--ignore-unmatch', '.']);
101
+ git(dir, ['checkout', ref, '--', '.']);
102
+ git(dir, ['clean', '-fd', '--quiet']);
103
+ git(dir, ['add', '-A']);
104
+ git(dir, ['commit', '--quiet', '--allow-empty', '-m', `brainclaw: rollback to ${ref}`]);
105
+ return true;
106
+ }
107
+ catch (err) {
108
+ logger.debug('Failed to rollback memory:', err);
109
+ return false;
110
+ }
111
+ }
112
+ /**
113
+ * Get the current HEAD short hash.
114
+ */
115
+ export function getMemoryHead(cwd) {
116
+ if (!hasMemoryRepo(cwd))
117
+ return undefined;
118
+ try {
119
+ return git(memoryDir(cwd), ['rev-parse', '--short', 'HEAD']).trim();
120
+ }
121
+ catch {
122
+ return undefined;
123
+ }
124
+ }
125
+ function git(cwd, args) {
126
+ return execFileSync('git', args, {
127
+ cwd,
128
+ encoding: 'utf-8',
129
+ timeout: 10_000,
130
+ stdio: ['pipe', 'pipe', 'pipe'],
131
+ });
132
+ }
133
+ //# sourceMappingURL=memory-git.js.map