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,66 @@
1
+ /**
2
+ * Scan a text string for sensitive patterns defined in the config.
3
+ * Returns a list of warnings.
4
+ */
5
+ export function scanText(text, config) {
6
+ const warnings = [];
7
+ if (!config.redaction.enabled)
8
+ return warnings;
9
+ const isStrict = config.security?.strict_redaction ?? false;
10
+ const level = isStrict ? 'block' : 'warn';
11
+ for (const pattern of config.redaction.patterns) {
12
+ try {
13
+ // Strip Python-style inline flags (?i) etc. since we always use 'i' flag
14
+ const cleanPattern = pattern.replace(/^\(\?[gimsuy]+\)/g, '');
15
+ const re = new RegExp(cleanPattern, 'i');
16
+ if (re.test(text)) {
17
+ warnings.push({
18
+ level,
19
+ message: `Possible sensitive content matching pattern '${pattern}' found in text`,
20
+ });
21
+ }
22
+ }
23
+ catch {
24
+ // skip invalid regex patterns
25
+ }
26
+ }
27
+ const blockPaths = config.security?.block_sensitive_paths ?? true;
28
+ if (blockPaths) {
29
+ for (const sp of config.sensitive_paths) {
30
+ if (text.includes(sp)) {
31
+ warnings.push({
32
+ level: 'warn',
33
+ message: `Sensitive path '${sp}' mentioned in text`,
34
+ });
35
+ }
36
+ }
37
+ }
38
+ return warnings;
39
+ }
40
+ /**
41
+ * Run doctor checks on the full state.
42
+ */
43
+ export function doctorCheck(state, config) {
44
+ const warnings = [];
45
+ const sections = [
46
+ { name: 'active_constraints', items: state.active_constraints },
47
+ { name: 'recent_decisions', items: state.recent_decisions },
48
+ { name: 'known_traps', items: state.known_traps },
49
+ { name: 'open_handoffs', items: state.open_handoffs },
50
+ ];
51
+ for (const section of sections) {
52
+ for (let i = 0; i < section.items.length; i++) {
53
+ const item = section.items[i];
54
+ const text = 'text' in item ? item.text : '';
55
+ const textWarnings = scanText(text, config);
56
+ for (const w of textWarnings) {
57
+ warnings.push({
58
+ level: w.level,
59
+ message: `${w.message} in ${section.name}[${i}] (${item.id})`,
60
+ });
61
+ }
62
+ }
63
+ }
64
+ return warnings;
65
+ }
66
+ //# sourceMappingURL=security.js.map
@@ -0,0 +1,50 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { writeFileAtomic } from './io.js';
4
+ export function resolveHomeDir(env = process.env) {
5
+ return env.HOME?.trim() || env.USERPROFILE?.trim() || undefined;
6
+ }
7
+ export function setupStatePath(env = process.env) {
8
+ const home = resolveHomeDir(env);
9
+ if (!home)
10
+ return undefined;
11
+ return path.join(home, '.brainclaw', 'setup.json');
12
+ }
13
+ export function userStoreConfigPath(env = process.env) {
14
+ const home = resolveHomeDir(env);
15
+ if (!home)
16
+ return undefined;
17
+ return path.join(home, '.brainclaw', 'config.yaml');
18
+ }
19
+ export function readSetupState(env = process.env) {
20
+ const statePath = setupStatePath(env);
21
+ if (!statePath)
22
+ return undefined;
23
+ try {
24
+ if (!fs.existsSync(statePath))
25
+ return undefined;
26
+ const raw = fs.readFileSync(statePath, 'utf-8');
27
+ return JSON.parse(raw);
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
33
+ export function writeSetupState(state, env = process.env) {
34
+ const statePath = setupStatePath(env);
35
+ if (!statePath)
36
+ return;
37
+ const dir = path.dirname(statePath);
38
+ if (!fs.existsSync(dir)) {
39
+ fs.mkdirSync(dir, { recursive: true });
40
+ }
41
+ writeFileAtomic(statePath, JSON.stringify(state, null, 2) + '\n');
42
+ }
43
+ export function hasCompletedSetup(env = process.env) {
44
+ if (readSetupState(env)) {
45
+ return true;
46
+ }
47
+ const configPath = userStoreConfigPath(env);
48
+ return configPath ? fs.existsSync(configPath) : false;
49
+ }
50
+ //# sourceMappingURL=setup-state.js.map
@@ -0,0 +1,83 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ConstraintSchema, DecisionSchema, TrapSchema, HandoffSchema, PlanItemSchema } from './schema.js';
4
+ import { ensureMemoryDir, resolveEntityDir } from './io.js';
5
+ import { commitMemoryChange } from './memory-git.js';
6
+ import { appendEvent } from './event-log.js';
7
+ import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
8
+ export function emptyState() {
9
+ return {
10
+ version: 1,
11
+ write_version: 1,
12
+ active_constraints: [],
13
+ recent_decisions: [],
14
+ known_traps: [],
15
+ open_handoffs: [],
16
+ plan_items: [],
17
+ };
18
+ }
19
+ function loadDirectoryItems(dirPath, schema, documentType) {
20
+ if (!fs.existsSync(dirPath))
21
+ return [];
22
+ const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
23
+ const items = [];
24
+ for (const file of files) {
25
+ try {
26
+ items.push(schema.parse(loadVersionedJsonFile(documentType, path.join(dirPath, file)).document));
27
+ }
28
+ catch {
29
+ // skip invalid files
30
+ }
31
+ }
32
+ return items;
33
+ }
34
+ export function loadState(cwd) {
35
+ // Load from entity-aligned directories (with legacy fallback)
36
+ const effectiveCwd = cwd ?? process.cwd();
37
+ const state = emptyState();
38
+ state.active_constraints = loadDirectoryItems(resolveEntityDir('constraints', effectiveCwd, 'read'), ConstraintSchema, 'constraint');
39
+ state.recent_decisions = loadDirectoryItems(resolveEntityDir('decisions', effectiveCwd, 'read'), DecisionSchema, 'decision');
40
+ state.known_traps = loadDirectoryItems(resolveEntityDir('traps', effectiveCwd, 'read'), TrapSchema, 'trap');
41
+ state.open_handoffs = loadDirectoryItems(resolveEntityDir('handoffs', effectiveCwd, 'read'), HandoffSchema, 'handoff');
42
+ state.plan_items = loadDirectoryItems(resolveEntityDir('plans', effectiveCwd, 'read'), PlanItemSchema, 'plan');
43
+ // Sort them by creation date for consistency
44
+ state.active_constraints.sort((a, b) => a.created_at.localeCompare(b.created_at));
45
+ state.recent_decisions.sort((a, b) => a.created_at.localeCompare(b.created_at));
46
+ state.known_traps.sort((a, b) => a.created_at.localeCompare(b.created_at));
47
+ state.open_handoffs.sort((a, b) => a.created_at.localeCompare(b.created_at));
48
+ state.plan_items.sort((a, b) => a.created_at.localeCompare(b.created_at));
49
+ return state;
50
+ }
51
+ function syncDirectory(dirPath, items, documentType) {
52
+ if (!fs.existsSync(dirPath)) {
53
+ fs.mkdirSync(dirPath, { recursive: true });
54
+ }
55
+ // Write all current items
56
+ const currentIds = new Set();
57
+ for (const item of items) {
58
+ currentIds.add(item.id);
59
+ const filepath = path.join(dirPath, `${item.id}.json`);
60
+ saveVersionedJsonFile(documentType, filepath, item);
61
+ }
62
+ // Remove files that are no longer in the state (e.g. if deleted/pruned)
63
+ const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
64
+ for (const file of files) {
65
+ const id = file.replace('.json', '');
66
+ if (!currentIds.has(id)) {
67
+ fs.unlinkSync(path.join(dirPath, file));
68
+ }
69
+ }
70
+ }
71
+ export function saveState(state, cwd) {
72
+ ensureMemoryDir(cwd);
73
+ const effectiveCwd = cwd ?? process.cwd();
74
+ // Write to entity-aligned directories
75
+ syncDirectory(resolveEntityDir('constraints', effectiveCwd, 'write'), state.active_constraints, 'constraint');
76
+ syncDirectory(resolveEntityDir('decisions', effectiveCwd, 'write'), state.recent_decisions, 'decision');
77
+ syncDirectory(resolveEntityDir('traps', effectiveCwd, 'write'), state.known_traps, 'trap');
78
+ syncDirectory(resolveEntityDir('handoffs', effectiveCwd, 'write'), state.open_handoffs, 'handoff');
79
+ syncDirectory(resolveEntityDir('plans', effectiveCwd, 'write'), state.plan_items, 'plan');
80
+ appendEvent({ action: 'update', item_type: 'state', agent: 'system' }, cwd);
81
+ commitMemoryChange('state update', cwd);
82
+ }
83
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1,119 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { MEMORY_DIR } from './io.js';
5
+ /**
6
+ * Walk up the filesystem from `cwd`, collecting every `.brainclaw/` directory
7
+ * found along the way, up to (and including) `boundary`.
8
+ *
9
+ * The returned array is ordered from closest to farthest (index 0 = highest
10
+ * priority). Returns an empty array when no store is found.
11
+ */
12
+ export function resolveStoreChain(cwd = process.cwd(), options = {}) {
13
+ const dirName = options.dirName ?? MEMORY_DIR;
14
+ const boundary = options.boundary ?? process.env.BRAINCLAW_STORE_BOUNDARY ?? os.homedir();
15
+ const includePartial = options.includePartial ?? false;
16
+ const results = [];
17
+ let current = path.resolve(cwd);
18
+ const boundaryResolved = path.resolve(boundary);
19
+ let depth = 0;
20
+ while (true) {
21
+ const candidate = path.join(current, dirName);
22
+ if (fs.existsSync(candidate)) {
23
+ const configPath = path.join(candidate, 'config.yaml');
24
+ const hasConfig = fs.existsSync(configPath);
25
+ if (hasConfig || includePartial) {
26
+ results.push({
27
+ storePath: candidate,
28
+ cwd: current,
29
+ depth,
30
+ role: inferRole(candidate, configPath, hasConfig),
31
+ });
32
+ }
33
+ }
34
+ // Stop at boundary (inclusive — we already checked it above if applicable)
35
+ if (current === boundaryResolved)
36
+ break;
37
+ const parent = path.dirname(current);
38
+ // Stop if we've hit the filesystem root (dirname returns same path)
39
+ if (parent === current)
40
+ break;
41
+ // Stop if we'd go above the boundary
42
+ if (!isAtOrBelow(parent, boundaryResolved))
43
+ break;
44
+ current = parent;
45
+ depth++;
46
+ }
47
+ return results;
48
+ }
49
+ /**
50
+ * Return the single "primary" store for a given cwd — the closest one.
51
+ * Returns undefined when no store exists in the chain.
52
+ */
53
+ export function resolvePrimaryStore(cwd = process.cwd(), options = {}) {
54
+ return resolveStoreChain(cwd, options)[0];
55
+ }
56
+ /**
57
+ * Resolve the effective cwd for a write operation targeting a specific store level.
58
+ *
59
+ * - `local` → the closest store (default, current behaviour)
60
+ * - `repo` → the first store with role='repo' in the chain; falls back to closest
61
+ * - `workspace` → the first store with role='workspace', or the farthest store found
62
+ * - `user` → the first store with role='user' in the chain; falls back to os.homedir()
63
+ *
64
+ * Returns the original cwd unchanged when no chain exists or when target='local'.
65
+ */
66
+ export function resolveTargetStore(cwd = process.cwd(), target = 'local', options = {}) {
67
+ if (target === 'local')
68
+ return cwd;
69
+ const chain = resolveStoreChain(cwd, options);
70
+ if (chain.length === 0)
71
+ return cwd;
72
+ if (target === 'repo') {
73
+ const match = chain.find((s) => s.role === 'repo');
74
+ return match?.cwd ?? chain[0].cwd;
75
+ }
76
+ if (target === 'workspace') {
77
+ // workspace: prefer declared role, otherwise take farthest
78
+ const match = chain.find((s) => s.role === 'workspace');
79
+ return match?.cwd ?? chain[chain.length - 1].cwd;
80
+ }
81
+ // user: prefer declared role, otherwise os.homedir()
82
+ const match = chain.find((s) => s.role === 'user');
83
+ return match?.cwd ?? os.homedir();
84
+ }
85
+ /**
86
+ * Return true if `dir` is at or below `ancestor` in the filesystem hierarchy.
87
+ */
88
+ function isAtOrBelow(dir, ancestor) {
89
+ const rel = path.relative(ancestor, dir);
90
+ // If relative path starts with '..', dir is above ancestor
91
+ return !rel.startsWith('..');
92
+ }
93
+ /**
94
+ * Infer the store role from config.yaml store_type field, or fall back to
95
+ * heuristics (presence of .git sibling = repo, no parent store = workspace).
96
+ */
97
+ function inferRole(storePath, configPath, hasConfig) {
98
+ if (hasConfig) {
99
+ try {
100
+ const raw = fs.readFileSync(configPath, 'utf-8');
101
+ const match = raw.match(/store_type:\s*(\S+)/);
102
+ if (match) {
103
+ const val = match[1].trim();
104
+ if (val === 'workspace' || val === 'repo' || val === 'service' || val === 'user') {
105
+ return val;
106
+ }
107
+ }
108
+ }
109
+ catch {
110
+ // non-fatal — fall through to heuristics
111
+ }
112
+ }
113
+ // Heuristic: if a .git directory lives alongside .brainclaw/, treat as repo
114
+ const siblingGit = path.join(path.dirname(storePath), '.git');
115
+ if (fs.existsSync(siblingGit))
116
+ return 'repo';
117
+ return 'unknown';
118
+ }
119
+ //# sourceMappingURL=store-resolution.js.map
@@ -0,0 +1,83 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { memoryDir } from './io.js';
4
+ function runGit(args, cwd) {
5
+ const result = spawnSync('git', args, { cwd, encoding: 'utf-8' });
6
+ return {
7
+ ok: result.status === 0,
8
+ stdout: result.stdout?.trim() ?? '',
9
+ stderr: result.stderr?.trim() ?? '',
10
+ };
11
+ }
12
+ function hasGitRemote(remoteName, cwd) {
13
+ const result = runGit(['remote', 'get-url', remoteName], cwd);
14
+ return result.ok;
15
+ }
16
+ export function pullRemoteMemory(options = {}) {
17
+ const cwd = options.cwd ?? process.cwd();
18
+ const remote = options.remote ?? 'origin';
19
+ if (!hasGitRemote(remote, cwd)) {
20
+ return { success: false, message: `Git remote '${remote}' not found. Add it with: git remote add ${remote} <url>` };
21
+ }
22
+ const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
23
+ if (!branch.ok) {
24
+ return { success: false, message: 'Could not determine current branch.' };
25
+ }
26
+ const result = runGit(['pull', '--no-rebase', remote, branch.stdout], cwd);
27
+ if (!result.ok) {
28
+ return { success: false, message: `git pull failed: ${result.stderr}`, details: result.stderr };
29
+ }
30
+ return { success: true, message: `Pulled from ${remote}/${branch.stdout}`, details: result.stdout };
31
+ }
32
+ export function pushRemoteMemory(options = {}) {
33
+ const cwd = options.cwd ?? process.cwd();
34
+ const remote = options.remote ?? 'origin';
35
+ const memDir = memoryDir(cwd);
36
+ const relativeMemDir = path.relative(cwd, memDir);
37
+ if (!hasGitRemote(remote, cwd)) {
38
+ return { success: false, message: `Git remote '${remote}' not found.` };
39
+ }
40
+ const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
41
+ if (!branch.ok) {
42
+ return { success: false, message: 'Could not determine current branch.' };
43
+ }
44
+ // Stage only the memory directory (shared content only — machine/private-specific dirs excluded)
45
+ const addResult = runGit(['add', relativeMemDir], cwd);
46
+ if (!addResult.ok) {
47
+ return { success: false, message: `git add failed: ${addResult.stderr}` };
48
+ }
49
+ // Check if there's anything to commit
50
+ const statusResult = runGit(['diff', '--cached', '--quiet'], cwd);
51
+ if (statusResult.ok) {
52
+ return { success: true, message: 'Nothing to push — memory is already in sync with last commit.' };
53
+ }
54
+ const commitMsg = options.message ?? 'chore: sync brainclaw memory';
55
+ const commitResult = runGit(['commit', '-m', commitMsg], cwd);
56
+ if (!commitResult.ok) {
57
+ return { success: false, message: `git commit failed: ${commitResult.stderr}` };
58
+ }
59
+ const pushResult = runGit(['push', remote, branch.stdout], cwd);
60
+ if (!pushResult.ok) {
61
+ return { success: false, message: `git push failed: ${pushResult.stderr}`, details: pushResult.stderr };
62
+ }
63
+ return { success: true, message: `Pushed to ${remote}/${branch.stdout}`, details: pushResult.stdout };
64
+ }
65
+ export function diffRemoteMemory(options = {}) {
66
+ const cwd = options.cwd ?? process.cwd();
67
+ const remote = options.remote ?? 'origin';
68
+ if (!hasGitRemote(remote, cwd)) {
69
+ return { success: false, message: `Git remote '${remote}' not found.` };
70
+ }
71
+ // Fetch to update remote refs without merging
72
+ runGit(['fetch', remote], cwd);
73
+ const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
74
+ const diffResult = runGit(['diff', `${remote}/${branch.stdout}`, '--', memoryDir(cwd)], cwd);
75
+ if (!diffResult.ok) {
76
+ return { success: false, message: `git diff failed: ${diffResult.stderr}` };
77
+ }
78
+ if (!diffResult.stdout) {
79
+ return { success: true, message: 'Memory is in sync with remote — no differences.' };
80
+ }
81
+ return { success: true, message: 'Differences found with remote:', details: diffResult.stdout };
82
+ }
83
+ //# sourceMappingURL=sync-remote.js.map
@@ -0,0 +1,86 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolveCurrentHostId, sanitizeHostId } from './host.js';
4
+ import { generateId, generateIdWithLabel } from './ids.js';
5
+ import { resolveEntityDir } from './io.js';
6
+ import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
7
+ import { TrapSchema } from './schema.js';
8
+ export function isTrapExpired(trap, nowIso = new Date().toISOString()) {
9
+ return trap.status === 'expired' || (!!trap.expires_at && trap.expires_at < nowIso);
10
+ }
11
+ export function isTrapActive(trap, nowIso = new Date().toISOString()) {
12
+ return trap.status === 'active' && !isTrapExpired(trap, nowIso);
13
+ }
14
+ function machineTrapsDir(cwd) {
15
+ return resolveEntityDir('traps-hosts', cwd ?? process.cwd(), 'read');
16
+ }
17
+ function privateTrapsDir(cwd) {
18
+ return resolveEntityDir('traps-private', cwd ?? process.cwd(), 'read');
19
+ }
20
+ function hostTrapDir(visibility, hostId, cwd) {
21
+ const baseDir = visibility === 'machine' ? machineTrapsDir(cwd) : privateTrapsDir(cwd);
22
+ return path.join(baseDir, sanitizeHostId(hostId));
23
+ }
24
+ function ensureHostTrapDir(visibility, hostId, cwd) {
25
+ const dir = hostTrapDir(visibility, hostId, cwd);
26
+ if (!fs.existsSync(dir)) {
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ }
29
+ }
30
+ function readTrapFiles(dir) {
31
+ if (!fs.existsSync(dir))
32
+ return [];
33
+ const traps = [];
34
+ for (const file of fs.readdirSync(dir).filter((entry) => entry.endsWith('.json')).sort()) {
35
+ try {
36
+ traps.push(loadVersionedJsonFile('trap', path.join(dir, file)).document);
37
+ }
38
+ catch {
39
+ // Ignore malformed files.
40
+ }
41
+ }
42
+ return traps;
43
+ }
44
+ function resolveHostIds(rootDir, options) {
45
+ if (!fs.existsSync(rootDir))
46
+ return [];
47
+ if (options.includeAllHosts) {
48
+ return fs.readdirSync(rootDir)
49
+ .filter((entry) => fs.statSync(path.join(rootDir, entry)).isDirectory())
50
+ .map((entry) => sanitizeHostId(entry));
51
+ }
52
+ return [sanitizeHostId(options.hostId ?? resolveCurrentHostId())];
53
+ }
54
+ export function listOperationalTraps(options = {}, cwd) {
55
+ const visibility = options.visibility;
56
+ const traps = [];
57
+ if (!visibility || visibility === 'machine' || visibility === 'all') {
58
+ for (const hostId of resolveHostIds(machineTrapsDir(cwd), options)) {
59
+ traps.push(...readTrapFiles(hostTrapDir('machine', hostId, cwd)));
60
+ }
61
+ }
62
+ if (visibility === 'private' || visibility === 'all') {
63
+ for (const hostId of resolveHostIds(privateTrapsDir(cwd), options)) {
64
+ traps.push(...readTrapFiles(hostTrapDir('private', hostId, cwd)));
65
+ }
66
+ }
67
+ return traps.sort((a, b) => a.created_at.localeCompare(b.created_at));
68
+ }
69
+ export function saveOperationalTrap(trap, cwd) {
70
+ const visibility = trap.visibility === 'private' ? 'private' : 'machine';
71
+ const hostId = sanitizeHostId(trap.host_id ?? resolveCurrentHostId());
72
+ ensureHostTrapDir(visibility, hostId, cwd);
73
+ const persisted = {
74
+ ...trap,
75
+ visibility,
76
+ host_id: hostId,
77
+ };
78
+ saveVersionedJsonFile('trap', path.join(hostTrapDir(visibility, hostId, cwd), `${trap.id}.json`), TrapSchema.parse(persisted));
79
+ }
80
+ export function generateTrapId() {
81
+ return generateId('known_traps');
82
+ }
83
+ export function generateTrapIdWithLabel(cwd) {
84
+ return generateIdWithLabel('known_traps', cwd);
85
+ }
86
+ //# sourceMappingURL=traps.js.map
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "brainclaw",
3
+ "version": "0.19.2",
4
+ "description": "Shared project memory for humans and coding agents.",
5
+ "type": "module",
6
+ "bin": {
7
+ "brainclaw": "dist/cli.js",
8
+ "bclaw": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist/**/*.js",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsc && node dist/cli.js",
18
+ "build:test": "tsc -p tsconfig.test.json",
19
+ "pack:check": "node scripts/check-pack.mjs",
20
+ "test": "npm run build:test && node scripts/run-tests.mjs default",
21
+ "test:unit": "npm run build:test && node scripts/run-tests.mjs unit",
22
+ "test:smoke": "npm run build:test && node scripts/run-tests.mjs smoke",
23
+ "test:e2e": "npm run build:test && node scripts/run-tests.mjs e2e",
24
+ "test:all": "npm run build:test && node scripts/run-tests.mjs all",
25
+ "test:coverage": "npm run build:test && c8 --all --src dist-test/src --reporter=text-summary --reporter=lcov --exclude=dist-test/tests/** --exclude=dist-test/scripts/** node scripts/run-tests.mjs default",
26
+ "test:coverage:check": "npm run build:test && c8 --all --check-coverage --lines 55 --functions 60 --branches 65 --statements 55 --src dist-test/src --reporter=text-summary --exclude=dist-test/tests/** --exclude=dist-test/scripts/** node scripts/run-tests.mjs default",
27
+ "prepublishOnly": "npm run build && npm run pack:check"
28
+ },
29
+ "keywords": [
30
+ "cli",
31
+ "memory",
32
+ "agents",
33
+ "coding-agents",
34
+ "project-context",
35
+ "handoff",
36
+ "local-first"
37
+ ],
38
+ "license": "BUSL-1.1",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/jberdah/brainclaw.git"
42
+ },
43
+ "homepage": "https://github.com/jberdah/brainclaw",
44
+ "bugs": {
45
+ "url": "https://github.com/jberdah/brainclaw/issues"
46
+ },
47
+ "engines": {
48
+ "node": ">=20"
49
+ },
50
+ "dependencies": {
51
+ "commander": "^13.1.0",
52
+ "yaml": "^2.7.1",
53
+ "zod": "^3.24.4"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^22.15.3",
57
+ "c8": "^11.0.0",
58
+ "typescript": "^5.8.3"
59
+ }
60
+ }