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.
- package/LICENSE +74 -0
- package/README.md +226 -0
- package/dist/cli.js +1037 -0
- package/dist/commands/accept.js +149 -0
- package/dist/commands/adapter-openclaw-import.js +75 -0
- package/dist/commands/add-step.js +35 -0
- package/dist/commands/agent-board.js +106 -0
- package/dist/commands/audit.js +35 -0
- package/dist/commands/bootstrap.js +34 -0
- package/dist/commands/capability.js +104 -0
- package/dist/commands/changes.js +112 -0
- package/dist/commands/check-constraints.js +63 -0
- package/dist/commands/claim-resource.js +54 -0
- package/dist/commands/claim.js +92 -0
- package/dist/commands/complete-step.js +34 -0
- package/dist/commands/constraint.js +44 -0
- package/dist/commands/context-diff.js +32 -0
- package/dist/commands/context.js +63 -0
- package/dist/commands/decision.js +45 -0
- package/dist/commands/delete-plan.js +20 -0
- package/dist/commands/diff.js +99 -0
- package/dist/commands/doctor.js +1275 -0
- package/dist/commands/enable-agent.js +63 -0
- package/dist/commands/env.js +46 -0
- package/dist/commands/estimation-report.js +167 -0
- package/dist/commands/explore.js +47 -0
- package/dist/commands/export.js +381 -0
- package/dist/commands/handoff.js +63 -0
- package/dist/commands/history.js +22 -0
- package/dist/commands/hooks.js +123 -0
- package/dist/commands/init.js +356 -0
- package/dist/commands/install-hooks.js +115 -0
- package/dist/commands/instruction.js +56 -0
- package/dist/commands/list-agents.js +44 -0
- package/dist/commands/list-claims.js +45 -0
- package/dist/commands/list-instructions.js +50 -0
- package/dist/commands/list-plans.js +48 -0
- package/dist/commands/mcp-worker.js +12 -0
- package/dist/commands/mcp.js +2272 -0
- package/dist/commands/memory.js +283 -0
- package/dist/commands/metrics.js +175 -0
- package/dist/commands/plan-resource.js +62 -0
- package/dist/commands/plan.js +76 -0
- package/dist/commands/prune-candidates.js +36 -0
- package/dist/commands/prune.js +48 -0
- package/dist/commands/pull.js +25 -0
- package/dist/commands/push.js +28 -0
- package/dist/commands/rebuild.js +14 -0
- package/dist/commands/reflect-runtime-note.js +74 -0
- package/dist/commands/reflect.js +286 -0
- package/dist/commands/register-agent.js +29 -0
- package/dist/commands/reject.js +52 -0
- package/dist/commands/release-claim.js +41 -0
- package/dist/commands/release-claims.js +67 -0
- package/dist/commands/review.js +242 -0
- package/dist/commands/rollback.js +156 -0
- package/dist/commands/runtime-note.js +144 -0
- package/dist/commands/runtime-status.js +49 -0
- package/dist/commands/search.js +36 -0
- package/dist/commands/session-end.js +187 -0
- package/dist/commands/session-start.js +147 -0
- package/dist/commands/set-trust.js +92 -0
- package/dist/commands/setup.js +446 -0
- package/dist/commands/show-candidate.js +31 -0
- package/dist/commands/star-candidate.js +28 -0
- package/dist/commands/status.js +133 -0
- package/dist/commands/sync.js +159 -0
- package/dist/commands/tool.js +126 -0
- package/dist/commands/trap.js +74 -0
- package/dist/commands/update-handoff.js +23 -0
- package/dist/commands/update-plan.js +37 -0
- package/dist/commands/upgrade.js +382 -0
- package/dist/commands/use-candidate.js +35 -0
- package/dist/commands/version.js +96 -0
- package/dist/commands/watch.js +215 -0
- package/dist/commands/whoami.js +104 -0
- package/dist/core/agent-context.js +340 -0
- package/dist/core/agent-files.js +874 -0
- package/dist/core/agent-integrations.js +135 -0
- package/dist/core/agent-inventory.js +401 -0
- package/dist/core/agent-registry.js +420 -0
- package/dist/core/ai-agent-detection.js +140 -0
- package/dist/core/audit.js +85 -0
- package/dist/core/bootstrap.js +658 -0
- package/dist/core/brainclaw-version.js +433 -0
- package/dist/core/candidates.js +137 -0
- package/dist/core/circuit-breaker.js +118 -0
- package/dist/core/claims.js +72 -0
- package/dist/core/config.js +86 -0
- package/dist/core/context-diff.js +122 -0
- package/dist/core/context.js +1212 -0
- package/dist/core/contradictions.js +270 -0
- package/dist/core/coordination.js +86 -0
- package/dist/core/cross-project.js +99 -0
- package/dist/core/duplicates.js +72 -0
- package/dist/core/event-log.js +152 -0
- package/dist/core/events.js +56 -0
- package/dist/core/execution-context.js +204 -0
- package/dist/core/freshness.js +87 -0
- package/dist/core/global-registry.js +182 -0
- package/dist/core/host.js +10 -0
- package/dist/core/identity.js +151 -0
- package/dist/core/ids.js +56 -0
- package/dist/core/input-validation.js +81 -0
- package/dist/core/instructions.js +117 -0
- package/dist/core/io.js +191 -0
- package/dist/core/json-store.js +63 -0
- package/dist/core/lifecycle.js +45 -0
- package/dist/core/lock.js +129 -0
- package/dist/core/logger.js +49 -0
- package/dist/core/machine-profile.js +332 -0
- package/dist/core/markdown.js +120 -0
- package/dist/core/memory-git.js +133 -0
- package/dist/core/migration.js +247 -0
- package/dist/core/project-registry.js +64 -0
- package/dist/core/reflection-safety.js +21 -0
- package/dist/core/repo-analysis.js +133 -0
- package/dist/core/reputation.js +409 -0
- package/dist/core/runtime.js +134 -0
- package/dist/core/schema.js +580 -0
- package/dist/core/search.js +115 -0
- package/dist/core/security.js +66 -0
- package/dist/core/setup-state.js +50 -0
- package/dist/core/state.js +83 -0
- package/dist/core/store-resolution.js +119 -0
- package/dist/core/sync-remote.js +83 -0
- package/dist/core/traps.js +86 -0
- 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
|