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,56 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { resolveEventSessionId } from './identity.js';
|
|
4
|
+
import { RuntimeEventSchema } from './schema.js';
|
|
5
|
+
import { readFileSync, resolveEntityDir } from './io.js';
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
|
+
function runtimeDir(cwd) {
|
|
8
|
+
return resolveEntityDir('runtime', cwd ?? process.cwd(), 'read');
|
|
9
|
+
}
|
|
10
|
+
function collectJsonFiles(dir) {
|
|
11
|
+
const files = [];
|
|
12
|
+
if (!fs.existsSync(dir))
|
|
13
|
+
return files;
|
|
14
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
15
|
+
const full = path.join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
files.push(...collectJsonFiles(full));
|
|
18
|
+
}
|
|
19
|
+
else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
20
|
+
files.push(full);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return files;
|
|
24
|
+
}
|
|
25
|
+
export function listRuntimeEvents(cwd) {
|
|
26
|
+
const base = runtimeDir(cwd);
|
|
27
|
+
if (!fs.existsSync(base))
|
|
28
|
+
return [];
|
|
29
|
+
const events = [];
|
|
30
|
+
for (const file of collectJsonFiles(base)) {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(readFileSync(file));
|
|
33
|
+
if (Array.isArray(parsed)) {
|
|
34
|
+
for (const item of parsed) {
|
|
35
|
+
events.push(RuntimeEventSchema.parse(item));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.events)) {
|
|
39
|
+
for (const item of parsed.events) {
|
|
40
|
+
events.push(RuntimeEventSchema.parse(item));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
events.push(RuntimeEventSchema.parse(parsed));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
logger.debug('Ignoring malformed runtime event file:', file, err);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return events.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
52
|
+
}
|
|
53
|
+
export function listRuntimeEventsBySession(session, cwd) {
|
|
54
|
+
return listRuntimeEvents(cwd).filter((event) => resolveEventSessionId(event) === session);
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
const TOOLCHAINS = [
|
|
4
|
+
{ name: 'node', command: 'node', args: ['--version'] },
|
|
5
|
+
{ name: 'npm', command: 'npm', args: ['--version'] },
|
|
6
|
+
{ name: 'pnpm', command: 'pnpm', args: ['--version'] },
|
|
7
|
+
{ name: 'python', command: 'python', args: ['--version'] },
|
|
8
|
+
{ name: 'pip', command: 'pip', args: ['--version'] },
|
|
9
|
+
{ name: 'cargo', command: 'cargo', args: ['--version'] },
|
|
10
|
+
{ name: 'go', command: 'go', args: ['version'] },
|
|
11
|
+
];
|
|
12
|
+
const ENV_WHITELIST = [
|
|
13
|
+
'BRAINCLAW_AGENT',
|
|
14
|
+
'BRAINCLAW_HOST_ID',
|
|
15
|
+
'BRAINCLAW_SESSION_ID',
|
|
16
|
+
'OPENCLAW_AGENT',
|
|
17
|
+
'OPENCLAW_SESSION_ID',
|
|
18
|
+
'CI',
|
|
19
|
+
'NODE_ENV',
|
|
20
|
+
'VIRTUAL_ENV',
|
|
21
|
+
'CONDA_DEFAULT_ENV',
|
|
22
|
+
'npm_config_user_agent',
|
|
23
|
+
];
|
|
24
|
+
let cachedToolchains;
|
|
25
|
+
export function buildExecutionContext(options = {}) {
|
|
26
|
+
const cwd = options.cwd ?? process.cwd();
|
|
27
|
+
const env = options.env ?? process.env;
|
|
28
|
+
const runner = options.runner ?? defaultRunner;
|
|
29
|
+
const workspaceRoot = detectWorkspaceRoot(cwd, runner);
|
|
30
|
+
const branch = detectGitBranch(cwd, runner);
|
|
31
|
+
const gitStatus = detectGitStatus(cwd, runner);
|
|
32
|
+
const hasRemote = detectGitRemote(cwd, runner);
|
|
33
|
+
return {
|
|
34
|
+
platform: process.platform,
|
|
35
|
+
shell: detectShell(env),
|
|
36
|
+
cwd,
|
|
37
|
+
workspace_root: workspaceRoot,
|
|
38
|
+
branch,
|
|
39
|
+
git_status: gitStatus,
|
|
40
|
+
has_remote: hasRemote,
|
|
41
|
+
toolchains: detectToolchains(cwd, runner),
|
|
42
|
+
env_signals: captureEnvSignals(env),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function compactExecutionContext(snapshot) {
|
|
46
|
+
return {
|
|
47
|
+
platform: snapshot.platform,
|
|
48
|
+
shell: snapshot.shell,
|
|
49
|
+
workspace_root: snapshot.workspace_root,
|
|
50
|
+
branch: snapshot.branch,
|
|
51
|
+
git_status: snapshot.git_status,
|
|
52
|
+
has_remote: snapshot.has_remote,
|
|
53
|
+
toolchains: snapshot.toolchains.filter((tool) => tool.available),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function renderExecutionContextSummary(snapshot, includeEnvSignals = false) {
|
|
57
|
+
const lines = [];
|
|
58
|
+
lines.push(`Platform: ${snapshot.platform}`);
|
|
59
|
+
if (snapshot.shell) {
|
|
60
|
+
lines.push(`Shell: ${snapshot.shell}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push(`Workspace: ${snapshot.workspace_root}`);
|
|
63
|
+
if (snapshot.branch) {
|
|
64
|
+
lines.push(`Git branch: ${snapshot.branch}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push(`Git status: ${snapshot.git_status}`);
|
|
67
|
+
lines.push(`Git remote: ${snapshot.has_remote ? 'configured' : 'none'}`);
|
|
68
|
+
const availableToolchains = snapshot.toolchains.filter((tool) => tool.available);
|
|
69
|
+
if (availableToolchains.length > 0) {
|
|
70
|
+
lines.push(`Toolchains: ${availableToolchains.map((tool) => `${tool.name}${tool.version ? ` ${tool.version}` : ''}`).join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
lines.push('Toolchains: none detected');
|
|
74
|
+
}
|
|
75
|
+
if (includeEnvSignals && 'env_signals' in snapshot && snapshot.env_signals.length > 0) {
|
|
76
|
+
lines.push('Environment signals:');
|
|
77
|
+
for (const signal of snapshot.env_signals) {
|
|
78
|
+
lines.push(`- ${signal.name}=${signal.value}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
function detectWorkspaceRoot(cwd, runner) {
|
|
84
|
+
const result = runner('git', ['rev-parse', '--show-toplevel'], cwd);
|
|
85
|
+
if (result.status === 0) {
|
|
86
|
+
return result.stdout.trim();
|
|
87
|
+
}
|
|
88
|
+
return cwd;
|
|
89
|
+
}
|
|
90
|
+
function detectGitBranch(cwd, runner) {
|
|
91
|
+
const result = runner('git', ['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
92
|
+
if (result.status !== 0) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const branch = result.stdout.trim();
|
|
96
|
+
return branch && branch !== 'HEAD' ? branch : undefined;
|
|
97
|
+
}
|
|
98
|
+
function detectGitStatus(cwd, runner) {
|
|
99
|
+
const result = runner('git', ['status', '--porcelain'], cwd);
|
|
100
|
+
if (result.status !== 0) {
|
|
101
|
+
return 'unavailable';
|
|
102
|
+
}
|
|
103
|
+
return result.stdout.trim().length > 0 ? 'dirty' : 'clean';
|
|
104
|
+
}
|
|
105
|
+
function detectGitRemote(cwd, runner) {
|
|
106
|
+
const result = runner('git', ['remote'], cwd);
|
|
107
|
+
if (result.status !== 0) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).length > 0;
|
|
111
|
+
}
|
|
112
|
+
function detectToolchains(cwd, runner) {
|
|
113
|
+
if (runner === defaultRunner && cachedToolchains) {
|
|
114
|
+
return cachedToolchains;
|
|
115
|
+
}
|
|
116
|
+
const detected = TOOLCHAINS.map((tool) => {
|
|
117
|
+
const result = runner(tool.command, tool.args, cwd);
|
|
118
|
+
if (result.status !== 0) {
|
|
119
|
+
return { name: tool.name, available: false };
|
|
120
|
+
}
|
|
121
|
+
const version = firstNonEmptyLine(result.stdout || result.stderr);
|
|
122
|
+
return {
|
|
123
|
+
name: tool.name,
|
|
124
|
+
available: true,
|
|
125
|
+
version,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
if (runner === defaultRunner) {
|
|
129
|
+
cachedToolchains = detected;
|
|
130
|
+
}
|
|
131
|
+
return detected;
|
|
132
|
+
}
|
|
133
|
+
function captureEnvSignals(env) {
|
|
134
|
+
const signals = [];
|
|
135
|
+
for (const name of ENV_WHITELIST) {
|
|
136
|
+
const raw = env[name]?.trim();
|
|
137
|
+
if (!raw) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
signals.push({
|
|
141
|
+
name,
|
|
142
|
+
value: normalizeEnvValue(name, raw),
|
|
143
|
+
redacted: isRedactedEnv(name),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return signals;
|
|
147
|
+
}
|
|
148
|
+
function normalizeEnvValue(name, value) {
|
|
149
|
+
if (name === 'CI') {
|
|
150
|
+
return value.toLowerCase() === 'true' ? 'true' : 'set';
|
|
151
|
+
}
|
|
152
|
+
if (name === 'VIRTUAL_ENV') {
|
|
153
|
+
return path.basename(value);
|
|
154
|
+
}
|
|
155
|
+
if (name === 'BRAINCLAW_SESSION_ID' || name === 'OPENCLAW_SESSION_ID') {
|
|
156
|
+
return redactValue(value);
|
|
157
|
+
}
|
|
158
|
+
if (name === 'npm_config_user_agent') {
|
|
159
|
+
return value.split(' ')[0] ?? value;
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
function isRedactedEnv(name) {
|
|
164
|
+
return name === 'BRAINCLAW_SESSION_ID' || name === 'OPENCLAW_SESSION_ID';
|
|
165
|
+
}
|
|
166
|
+
function redactValue(value) {
|
|
167
|
+
if (value.length <= 10) {
|
|
168
|
+
return 'set';
|
|
169
|
+
}
|
|
170
|
+
return `${value.slice(0, 5)}...${value.slice(-3)}`;
|
|
171
|
+
}
|
|
172
|
+
function detectShell(env) {
|
|
173
|
+
const shell = env.SHELL?.trim();
|
|
174
|
+
if (shell) {
|
|
175
|
+
return path.basename(shell).replace(/\.exe$/i, '');
|
|
176
|
+
}
|
|
177
|
+
if (env.PSModulePath) {
|
|
178
|
+
return 'powershell';
|
|
179
|
+
}
|
|
180
|
+
const comspec = env.ComSpec?.trim() ?? env.COMSPEC?.trim();
|
|
181
|
+
if (comspec) {
|
|
182
|
+
return path.basename(comspec).replace(/\.exe$/i, '');
|
|
183
|
+
}
|
|
184
|
+
return process.platform === 'win32' ? 'unknown-windows-shell' : 'unknown-shell';
|
|
185
|
+
}
|
|
186
|
+
function firstNonEmptyLine(value) {
|
|
187
|
+
return value
|
|
188
|
+
.split(/\r?\n/)
|
|
189
|
+
.map((line) => line.trim())
|
|
190
|
+
.find(Boolean);
|
|
191
|
+
}
|
|
192
|
+
function defaultRunner(command, args, cwd) {
|
|
193
|
+
const result = spawnSync(command, args, {
|
|
194
|
+
cwd,
|
|
195
|
+
encoding: 'utf-8',
|
|
196
|
+
timeout: 5000,
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
status: result.status,
|
|
200
|
+
stdout: result.stdout ?? '',
|
|
201
|
+
stderr: result.stderr ?? '',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=execution-context.js.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { resolveCurrentHostId } from './host.js';
|
|
5
|
+
import { memoryPath, readFileSync, writeFileAtomic, resolveEntityDir } from './io.js';
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
|
+
const SHARED_PATHS = [
|
|
8
|
+
'config.yaml',
|
|
9
|
+
'project.md',
|
|
10
|
+
'constraints',
|
|
11
|
+
'decisions',
|
|
12
|
+
'traps',
|
|
13
|
+
'handoffs',
|
|
14
|
+
'plans',
|
|
15
|
+
'instructions',
|
|
16
|
+
'claims',
|
|
17
|
+
'runtime',
|
|
18
|
+
'inbox',
|
|
19
|
+
];
|
|
20
|
+
export function getVisibleMemoryVersion(options = {}) {
|
|
21
|
+
const entries = [];
|
|
22
|
+
const cwd = options.cwd ?? process.cwd();
|
|
23
|
+
// Scan entity-aligned paths (with legacy fallback via resolveEntityDir)
|
|
24
|
+
for (const relativePath of SHARED_PATHS) {
|
|
25
|
+
const resolved = relativePath.includes('.') ? memoryPath(relativePath, cwd) : resolveEntityDir(relativePath, cwd, 'read');
|
|
26
|
+
collectFileStats(resolved, relativePath, entries);
|
|
27
|
+
}
|
|
28
|
+
if (options.allHosts) {
|
|
29
|
+
collectFileStats(resolveEntityDir('runtime-hosts', cwd, 'read'), 'runtime-hosts', entries);
|
|
30
|
+
collectFileStats(resolveEntityDir('runtime-private', cwd, 'read'), 'runtime-private', entries);
|
|
31
|
+
collectFileStats(resolveEntityDir('traps-hosts', cwd, 'read'), 'traps-hosts', entries);
|
|
32
|
+
collectFileStats(resolveEntityDir('traps-private', cwd, 'read'), 'traps-private', entries);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const hostId = options.hostId ?? resolveCurrentHostId();
|
|
36
|
+
collectFileStats(path.join(resolveEntityDir('runtime-hosts', cwd, 'read'), hostId), path.join('runtime-hosts', hostId), entries);
|
|
37
|
+
collectFileStats(path.join(resolveEntityDir('runtime-private', cwd, 'read'), hostId), path.join('runtime-private', hostId), entries);
|
|
38
|
+
collectFileStats(path.join(resolveEntityDir('traps-hosts', cwd, 'read'), hostId), path.join('traps-hosts', hostId), entries);
|
|
39
|
+
collectFileStats(path.join(resolveEntityDir('traps-private', cwd, 'read'), hostId), path.join('traps-private', hostId), entries);
|
|
40
|
+
}
|
|
41
|
+
const hash = crypto.createHash('sha1');
|
|
42
|
+
for (const entry of entries.sort()) {
|
|
43
|
+
hash.update(entry);
|
|
44
|
+
hash.update('\n');
|
|
45
|
+
}
|
|
46
|
+
return hash.digest('hex');
|
|
47
|
+
}
|
|
48
|
+
export function readContextMarker(cwd) {
|
|
49
|
+
const markerPath = memoryPath('.last-context', cwd);
|
|
50
|
+
if (!fs.existsSync(markerPath)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const raw = readFileSync(markerPath).trim();
|
|
54
|
+
if (!raw) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (raw.startsWith('{')) {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(raw);
|
|
60
|
+
if (parsed && typeof parsed.read_at === 'string') {
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
logger.debug('Failed to parse context marker:', err);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { read_at: raw };
|
|
70
|
+
}
|
|
71
|
+
export function writeContextMarker(marker, cwd) {
|
|
72
|
+
writeFileAtomic(memoryPath('.last-context', cwd), JSON.stringify(marker, null, 2) + '\n');
|
|
73
|
+
}
|
|
74
|
+
function collectFileStats(absolutePath, relativePath, entries) {
|
|
75
|
+
if (!fs.existsSync(absolutePath)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const stat = fs.statSync(absolutePath);
|
|
79
|
+
if (stat.isDirectory()) {
|
|
80
|
+
for (const child of fs.readdirSync(absolutePath).sort()) {
|
|
81
|
+
collectFileStats(path.join(absolutePath, child), `${relativePath}/${child}`, entries);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
entries.push(`${relativePath}:${stat.size}:${stat.mtimeMs}`);
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=freshness.js.map
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
8
|
+
function runGit(args, cwd) {
|
|
9
|
+
try {
|
|
10
|
+
const r = spawnSync('git', args, { encoding: 'utf-8', timeout: 5000, cwd, windowsHide: true });
|
|
11
|
+
return r.status === 0 ? r.stdout.trim() : undefined;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function readJsonSafe(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function readYamlSafe(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
return yaml.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ── Scan a project directory ───────────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* Extract registry metadata from a single brainclaw-initialized project.
|
|
36
|
+
*/
|
|
37
|
+
export function scanProject(projectPath) {
|
|
38
|
+
const brainclawDir = path.join(projectPath, MEMORY_DIR);
|
|
39
|
+
if (!fs.existsSync(brainclawDir))
|
|
40
|
+
return undefined;
|
|
41
|
+
const identity = readJsonSafe(path.join(brainclawDir, 'project.identity.json'));
|
|
42
|
+
const config = readYamlSafe(path.join(brainclawDir, 'config.yaml'));
|
|
43
|
+
if (!identity && !config)
|
|
44
|
+
return undefined;
|
|
45
|
+
const projectId = (identity?.project_id ?? config?.project_id ?? 'unknown');
|
|
46
|
+
const projectName = (identity?.project_name ?? config?.project_name ?? path.basename(projectPath));
|
|
47
|
+
// Git remote
|
|
48
|
+
const gitRemote = runGit(['remote', 'get-url', 'origin'], projectPath);
|
|
49
|
+
// Git user for this repo
|
|
50
|
+
const gitUsers = [];
|
|
51
|
+
const userName = runGit(['config', 'user.name'], projectPath);
|
|
52
|
+
const userEmail = runGit(['config', 'user.email'], projectPath);
|
|
53
|
+
if (userName && userEmail) {
|
|
54
|
+
gitUsers.push({ name: userName, email: userEmail });
|
|
55
|
+
}
|
|
56
|
+
// Agents seen
|
|
57
|
+
const agentsSeen = [];
|
|
58
|
+
const agentsDir = path.join(brainclawDir, 'agents');
|
|
59
|
+
if (fs.existsSync(agentsDir)) {
|
|
60
|
+
try {
|
|
61
|
+
for (const f of fs.readdirSync(agentsDir).filter(f => f.endsWith('.json'))) {
|
|
62
|
+
const agent = readJsonSafe(path.join(agentsDir, f));
|
|
63
|
+
const name = agent?.agent_name;
|
|
64
|
+
if (name && !agentsSeen.includes(name))
|
|
65
|
+
agentsSeen.push(name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch { /* non-fatal */ }
|
|
69
|
+
}
|
|
70
|
+
// Last activity from audit.log mtime
|
|
71
|
+
let lastActivity;
|
|
72
|
+
try {
|
|
73
|
+
const auditLog = path.join(brainclawDir, 'audit.log');
|
|
74
|
+
if (fs.existsSync(auditLog)) {
|
|
75
|
+
lastActivity = fs.statSync(auditLog).mtime.toISOString();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { /* non-fatal */ }
|
|
79
|
+
return { project_id: projectId, project_name: projectName, path: projectPath, git_remote: gitRemote, git_users: gitUsers, last_activity: lastActivity, agents_seen: agentsSeen };
|
|
80
|
+
}
|
|
81
|
+
// ── Registry CRUD ──────────────────────────────────────────────────────────────
|
|
82
|
+
export function globalRegistryPath() {
|
|
83
|
+
return path.join(os.homedir(), MEMORY_DIR, 'projects.yaml');
|
|
84
|
+
}
|
|
85
|
+
export function loadGlobalRegistry() {
|
|
86
|
+
const filePath = globalRegistryPath();
|
|
87
|
+
if (!fs.existsSync(filePath))
|
|
88
|
+
return undefined;
|
|
89
|
+
try {
|
|
90
|
+
return yaml.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function saveGlobalRegistry(registry) {
|
|
97
|
+
const filePath = globalRegistryPath();
|
|
98
|
+
const dir = path.dirname(filePath);
|
|
99
|
+
if (!fs.existsSync(dir))
|
|
100
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
101
|
+
fs.writeFileSync(filePath, yaml.stringify(registry, { lineWidth: 120 }), 'utf-8');
|
|
102
|
+
return filePath;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Register or update a single project in the global registry (upsert by project_id).
|
|
106
|
+
*/
|
|
107
|
+
export function upsertProject(entry) {
|
|
108
|
+
const registry = loadGlobalRegistry() ?? { schema_version: 1, updated_at: new Date().toISOString(), projects: [] };
|
|
109
|
+
const idx = registry.projects.findIndex(p => p.project_id === entry.project_id);
|
|
110
|
+
if (idx >= 0) {
|
|
111
|
+
registry.projects[idx] = entry;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
registry.projects.push(entry);
|
|
115
|
+
}
|
|
116
|
+
registry.updated_at = new Date().toISOString();
|
|
117
|
+
saveGlobalRegistry(registry);
|
|
118
|
+
return registry;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Scan directories for brainclaw projects and update the global registry.
|
|
122
|
+
*/
|
|
123
|
+
export function scanAndRegister(roots) {
|
|
124
|
+
const registry = loadGlobalRegistry() ?? { schema_version: 1, updated_at: new Date().toISOString(), projects: [] };
|
|
125
|
+
for (const root of roots) {
|
|
126
|
+
const resolved = path.resolve(root);
|
|
127
|
+
// Check root itself
|
|
128
|
+
const rootEntry = scanProject(resolved);
|
|
129
|
+
if (rootEntry) {
|
|
130
|
+
const idx = registry.projects.findIndex(p => p.project_id === rootEntry.project_id);
|
|
131
|
+
if (idx >= 0)
|
|
132
|
+
registry.projects[idx] = rootEntry;
|
|
133
|
+
else
|
|
134
|
+
registry.projects.push(rootEntry);
|
|
135
|
+
}
|
|
136
|
+
// Scan immediate subdirectories
|
|
137
|
+
if (!fs.existsSync(resolved))
|
|
138
|
+
continue;
|
|
139
|
+
try {
|
|
140
|
+
for (const e of fs.readdirSync(resolved, { withFileTypes: true })) {
|
|
141
|
+
if (!e.isDirectory() || e.name.startsWith('.') || e.name === 'node_modules')
|
|
142
|
+
continue;
|
|
143
|
+
const entry = scanProject(path.join(resolved, e.name));
|
|
144
|
+
if (entry) {
|
|
145
|
+
const idx = registry.projects.findIndex(p => p.project_id === entry.project_id);
|
|
146
|
+
if (idx >= 0)
|
|
147
|
+
registry.projects[idx] = entry;
|
|
148
|
+
else
|
|
149
|
+
registry.projects.push(entry);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch { /* non-fatal */ }
|
|
154
|
+
}
|
|
155
|
+
registry.updated_at = new Date().toISOString();
|
|
156
|
+
saveGlobalRegistry(registry);
|
|
157
|
+
return registry;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Render human-readable summary of the global project registry.
|
|
161
|
+
*/
|
|
162
|
+
export function renderGlobalRegistrySummary(registry) {
|
|
163
|
+
const lines = [];
|
|
164
|
+
lines.push(`Projects registered: ${registry.projects.length}`);
|
|
165
|
+
lines.push('');
|
|
166
|
+
for (const p of registry.projects) {
|
|
167
|
+
lines.push(`● ${p.project_name} (${p.project_id})`);
|
|
168
|
+
lines.push(` Path: ${p.path}`);
|
|
169
|
+
if (p.git_remote)
|
|
170
|
+
lines.push(` Remote: ${p.git_remote}`);
|
|
171
|
+
if (p.git_users.length > 0)
|
|
172
|
+
lines.push(` Git user: ${p.git_users.map(u => `${u.name} <${u.email}>`).join(', ')}`);
|
|
173
|
+
if (p.agents_seen.length > 0)
|
|
174
|
+
lines.push(` Agents: ${p.agents_seen.join(', ')}`);
|
|
175
|
+
if (p.last_activity)
|
|
176
|
+
lines.push(` Last activity: ${p.last_activity}`);
|
|
177
|
+
lines.push('');
|
|
178
|
+
}
|
|
179
|
+
lines.push(`Registry updated: ${registry.updated_at}`);
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=global-registry.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
const HOST_ID_PATTERN = /[^a-z0-9._-]+/g;
|
|
3
|
+
export function sanitizeHostId(value) {
|
|
4
|
+
const normalized = value.trim().toLowerCase().replace(HOST_ID_PATTERN, '-').replace(/-+/g, '-');
|
|
5
|
+
return normalized.replace(/^-|-$/g, '') || 'unknown-host';
|
|
6
|
+
}
|
|
7
|
+
export function resolveCurrentHostId(explicitHostId) {
|
|
8
|
+
return sanitizeHostId(explicitHostId ?? process.env.BRAINCLAW_HOST_ID ?? os.hostname());
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=host.js.map
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { requireRegisteredAgentIdentity } from './agent-registry.js';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
import { resolveCurrentHostId } from './host.js';
|
|
7
|
+
import { memoryDir } from './io.js';
|
|
8
|
+
import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
|
|
9
|
+
import { CurrentSessionStateSchema } from './schema.js';
|
|
10
|
+
const CURRENT_SESSION_FILE = '.current-session';
|
|
11
|
+
export function resolveCurrentSessionId(env = process.env, cwd, options = {}) {
|
|
12
|
+
const value = env.BRAINCLAW_SESSION_ID?.trim()
|
|
13
|
+
|| env.OPENCLAW_SESSION_ID?.trim()
|
|
14
|
+
|| env.CLAUDE_SESSION_ID?.trim()
|
|
15
|
+
|| env.COPILOT_SESSION_ID?.trim();
|
|
16
|
+
if (value && value.length > 0) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
const agentName = options.agentName?.trim();
|
|
20
|
+
const agentId = options.agentId?.trim();
|
|
21
|
+
const hostId = options.hostId?.trim();
|
|
22
|
+
if (!agentName || !agentId || !hostId) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const implicit = resolveImplicitSession(cwd, {
|
|
26
|
+
agentName,
|
|
27
|
+
agentId,
|
|
28
|
+
hostId,
|
|
29
|
+
preferredSessionId: options.preferredSessionId,
|
|
30
|
+
persistImplicit: options.persistImplicit,
|
|
31
|
+
});
|
|
32
|
+
return implicit?.session_id;
|
|
33
|
+
}
|
|
34
|
+
export function buildOperationalIdentity(agentName, cwd, options = {}) {
|
|
35
|
+
const actor = requireRegisteredAgentIdentity({
|
|
36
|
+
agentName,
|
|
37
|
+
agentId: options.agentId,
|
|
38
|
+
cwd,
|
|
39
|
+
allowCurrent: true,
|
|
40
|
+
allowEnv: true,
|
|
41
|
+
});
|
|
42
|
+
const config = loadConfig(cwd);
|
|
43
|
+
const hostId = resolveCurrentHostId();
|
|
44
|
+
return {
|
|
45
|
+
agent: actor.agent_name,
|
|
46
|
+
agent_id: actor.agent_id,
|
|
47
|
+
project_id: config.project_id,
|
|
48
|
+
host_id: hostId,
|
|
49
|
+
session_id: resolveCurrentSessionId(process.env, cwd, {
|
|
50
|
+
agentName: actor.agent_name,
|
|
51
|
+
agentId: actor.agent_id,
|
|
52
|
+
hostId,
|
|
53
|
+
preferredSessionId: options.sessionId,
|
|
54
|
+
persistImplicit: options.persistImplicitSession,
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function resolveEventSessionId(event) {
|
|
59
|
+
if (typeof event.session_id === 'string' && event.session_id.trim().length > 0) {
|
|
60
|
+
return event.session_id;
|
|
61
|
+
}
|
|
62
|
+
const metadataSession = event.metadata?.session;
|
|
63
|
+
return typeof metadataSession === 'string' && metadataSession.trim().length > 0
|
|
64
|
+
? metadataSession
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
67
|
+
export function loadCurrentSession(cwd) {
|
|
68
|
+
const filepath = currentSessionPath(cwd);
|
|
69
|
+
if (!fs.existsSync(filepath)) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return CurrentSessionStateSchema.parse(loadVersionedJsonFile('current_session', filepath).document);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function saveCurrentSession(session, cwd) {
|
|
80
|
+
saveVersionedJsonFile('current_session', currentSessionPath(cwd), CurrentSessionStateSchema.parse(session));
|
|
81
|
+
}
|
|
82
|
+
export function clearCurrentSession(cwd, sessionId) {
|
|
83
|
+
const filepath = currentSessionPath(cwd);
|
|
84
|
+
if (!fs.existsSync(filepath)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (sessionId) {
|
|
88
|
+
const current = loadCurrentSession(cwd);
|
|
89
|
+
if (!current || current.session_id !== sessionId) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
fs.unlinkSync(filepath);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Ignore cleanup races.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function currentSessionPath(cwd) {
|
|
101
|
+
return path.join(memoryDir(cwd), CURRENT_SESSION_FILE);
|
|
102
|
+
}
|
|
103
|
+
function resolveImplicitSession(cwd, options) {
|
|
104
|
+
const current = loadCurrentSession(cwd);
|
|
105
|
+
const persistImplicit = options.persistImplicit ?? true;
|
|
106
|
+
const ttlMs = parseDurationToMs(loadConfig(cwd).implicit_session_ttl ?? '4h');
|
|
107
|
+
const now = new Date();
|
|
108
|
+
if (current
|
|
109
|
+
&& current.agent === options.agentName
|
|
110
|
+
&& current.agent_id === options.agentId
|
|
111
|
+
&& current.host_id === options.hostId
|
|
112
|
+
&& now.getTime() - Date.parse(current.last_seen_at) <= ttlMs) {
|
|
113
|
+
const refreshed = {
|
|
114
|
+
...current,
|
|
115
|
+
last_seen_at: now.toISOString(),
|
|
116
|
+
};
|
|
117
|
+
if (persistImplicit) {
|
|
118
|
+
saveCurrentSession(refreshed, cwd);
|
|
119
|
+
}
|
|
120
|
+
return refreshed;
|
|
121
|
+
}
|
|
122
|
+
const created = {
|
|
123
|
+
session_id: options.preferredSessionId ?? generateImplicitSessionId(),
|
|
124
|
+
started_at: now.toISOString(),
|
|
125
|
+
last_seen_at: now.toISOString(),
|
|
126
|
+
agent: options.agentName,
|
|
127
|
+
agent_id: options.agentId,
|
|
128
|
+
host_id: options.hostId,
|
|
129
|
+
};
|
|
130
|
+
if (persistImplicit) {
|
|
131
|
+
saveCurrentSession(created, cwd);
|
|
132
|
+
}
|
|
133
|
+
return created;
|
|
134
|
+
}
|
|
135
|
+
function parseDurationToMs(value) {
|
|
136
|
+
const match = /^(\d+)([mhd])$/i.exec(value.trim());
|
|
137
|
+
if (!match) {
|
|
138
|
+
return 4 * 60 * 60 * 1000;
|
|
139
|
+
}
|
|
140
|
+
const amount = parseInt(match[1], 10);
|
|
141
|
+
const unit = match[2].toLowerCase();
|
|
142
|
+
if (unit === 'm')
|
|
143
|
+
return amount * 60_000;
|
|
144
|
+
if (unit === 'h')
|
|
145
|
+
return amount * 3_600_000;
|
|
146
|
+
return amount * 86_400_000;
|
|
147
|
+
}
|
|
148
|
+
function generateImplicitSessionId() {
|
|
149
|
+
return `sess_${crypto.randomBytes(4).toString('hex')}`;
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=identity.js.map
|