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,56 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { withLock } from './lock.js';
5
+ const PREFIXES = {
6
+ active_constraints: 'cst',
7
+ bootstrap_seeds: 'bsd',
8
+ recent_decisions: 'dec',
9
+ known_traps: 'trp',
10
+ open_handoffs: 'hnd',
11
+ plan_items: 'pln',
12
+ plan_steps: 'stp',
13
+ instruction_entries: 'ins',
14
+ };
15
+ const ID_COUNTER_FILE = '.id-counter.json';
16
+ function counterPath(cwd) {
17
+ return path.join(cwd ?? process.cwd(), '.brainclaw', ID_COUNTER_FILE);
18
+ }
19
+ /** Generate a concurrence-safe prefixed ID using 4 random bytes. */
20
+ export function generateId(section) {
21
+ const prefix = PREFIXES[section] ?? section.slice(0, 3);
22
+ const rand = crypto.randomBytes(4).toString('hex');
23
+ return `${prefix}_${rand}`;
24
+ }
25
+ /**
26
+ * Atomically increment the per-prefix counter and return the next short label.
27
+ * Best-effort: if the counter file is unavailable the call still succeeds.
28
+ */
29
+ export function getNextShortLabel(prefix, cwd) {
30
+ const fp = counterPath(cwd);
31
+ return withLock(fp, () => {
32
+ let counter = {};
33
+ try {
34
+ counter = JSON.parse(fs.readFileSync(fp, 'utf-8'));
35
+ }
36
+ catch { /* first use or missing file */ }
37
+ const next = (counter[prefix] ?? 0) + 1;
38
+ counter[prefix] = next;
39
+ fs.writeFileSync(fp, JSON.stringify(counter), 'utf-8');
40
+ return `${prefix}#${next}`;
41
+ });
42
+ }
43
+ /**
44
+ * Generate both a concurrence-safe hash ID and a human-readable short label.
45
+ * The hash ID is the canonical storage key; the short label is for display and aliased lookups.
46
+ */
47
+ export function generateIdWithLabel(section, cwd) {
48
+ const id = generateId(section);
49
+ const prefix = PREFIXES[section] ?? section.slice(0, 3);
50
+ const short_label = getNextShortLabel(prefix, cwd);
51
+ return { id, short_label };
52
+ }
53
+ export function nowISO() {
54
+ return new Date().toISOString();
55
+ }
56
+ //# sourceMappingURL=ids.js.map
@@ -0,0 +1,81 @@
1
+ import { z } from 'zod';
2
+ export const TEXT_MAX_LENGTH = 2000;
3
+ export const TAG_MAX_LENGTH = 50;
4
+ export const TAG_MAX_COUNT = 20;
5
+ export const TextInputSchema = z.string().min(1, 'Text cannot be empty').max(TEXT_MAX_LENGTH, `Text cannot exceed ${TEXT_MAX_LENGTH} characters`);
6
+ export const TagArraySchema = z
7
+ .array(z.string().min(1, 'Tag cannot be empty').max(TAG_MAX_LENGTH, `Tag cannot exceed ${TAG_MAX_LENGTH} characters`))
8
+ .max(TAG_MAX_COUNT, `Cannot have more than ${TAG_MAX_COUNT} tags`);
9
+ export const TTL_PATTERN = /^(\d+)([mhd])$/;
10
+ export const TtlSchema = z
11
+ .string()
12
+ .regex(TTL_PATTERN, 'Invalid TTL format. Use <number><unit> where unit is m (minutes), h (hours), or d (days). Example: 30m, 2h, 7d');
13
+ function formatZodError(error, field) {
14
+ return error.errors.map((e) => ({ field, message: e.message }));
15
+ }
16
+ /**
17
+ * Validate CLI mutation inputs. On failure: prints error and calls process.exit(1).
18
+ */
19
+ export function validateCliInput(text, tags) {
20
+ const errors = [];
21
+ const textResult = TextInputSchema.safeParse(text);
22
+ if (!textResult.success) {
23
+ errors.push(...formatZodError(textResult.error, 'text'));
24
+ }
25
+ if (tags !== undefined && tags.length > 0) {
26
+ const tagsResult = TagArraySchema.safeParse(tags);
27
+ if (!tagsResult.success) {
28
+ errors.push(...formatZodError(tagsResult.error, 'tags'));
29
+ }
30
+ }
31
+ if (errors.length > 0) {
32
+ for (const e of errors) {
33
+ console.error(`Error: ${e.message}`);
34
+ }
35
+ process.exit(1);
36
+ }
37
+ }
38
+ /**
39
+ * Validate CLI TTL argument. On failure: prints error and calls process.exit(1).
40
+ */
41
+ export function validateCliTtl(ttl) {
42
+ const result = TtlSchema.safeParse(ttl);
43
+ if (!result.success) {
44
+ console.error(`Error: ${result.error.errors[0]?.message ?? 'Invalid TTL'}`);
45
+ process.exit(1);
46
+ }
47
+ }
48
+ /**
49
+ * Validate MCP mutation inputs. Returns { ok: true } or { ok: false, errors }.
50
+ * Never calls process.exit — callers must handle the error response.
51
+ */
52
+ export function validateMcpInput(text, tags) {
53
+ const errors = [];
54
+ const textResult = TextInputSchema.safeParse(text);
55
+ if (!textResult.success) {
56
+ errors.push(...formatZodError(textResult.error, 'text'));
57
+ }
58
+ if (tags !== undefined && tags.length > 0) {
59
+ const tagsResult = TagArraySchema.safeParse(tags);
60
+ if (!tagsResult.success) {
61
+ errors.push(...formatZodError(tagsResult.error, 'tags'));
62
+ }
63
+ }
64
+ if (errors.length > 0) {
65
+ return { ok: false, errors };
66
+ }
67
+ return { ok: true };
68
+ }
69
+ /**
70
+ * Validate a non-empty string field for MCP (scope, description, from, to).
71
+ */
72
+ export function validateMcpField(value, fieldName) {
73
+ if (!value.trim()) {
74
+ return { ok: false, message: `${fieldName} cannot be empty` };
75
+ }
76
+ if (value.length > TEXT_MAX_LENGTH) {
77
+ return { ok: false, message: `${fieldName} cannot exceed ${TEXT_MAX_LENGTH} characters` };
78
+ }
79
+ return { ok: true };
80
+ }
81
+ //# sourceMappingURL=input-validation.js.map
@@ -0,0 +1,117 @@
1
+ import fs from 'node:fs';
2
+ import { resolveEntityDir } from './io.js';
3
+ import { generateId } from './ids.js';
4
+ import { InstructionEntrySchema } from './schema.js';
5
+ import { JsonStore } from './json-store.js';
6
+ function instructionStore(cwd, mode = 'read') {
7
+ return new JsonStore({
8
+ dirPath: resolveEntityDir('instructions', cwd ?? process.cwd(), mode),
9
+ documentType: 'instruction',
10
+ getId: (entry) => entry.id,
11
+ sort: (a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id),
12
+ });
13
+ }
14
+ export function loadInstructions(cwd) {
15
+ const dir = resolveEntityDir('instructions', cwd ?? process.cwd(), 'read');
16
+ if (!fs.existsSync(dir)) {
17
+ return [];
18
+ }
19
+ return instructionStore(cwd).list();
20
+ }
21
+ export function saveInstruction(entry, cwd) {
22
+ instructionStore(cwd, 'write').save(InstructionEntrySchema.parse(entry));
23
+ }
24
+ export function createInstruction(text, options, cwd) {
25
+ const entries = loadInstructions(cwd);
26
+ const timestamp = new Date().toISOString();
27
+ const entry = {
28
+ schema_version: 2,
29
+ id: generateId('instruction_entries'),
30
+ layer: options.layer,
31
+ scope: options.scope,
32
+ text,
33
+ created_at: timestamp,
34
+ updated_at: timestamp,
35
+ author: options.author,
36
+ tags: options.tags ?? [],
37
+ active: true,
38
+ supersedes: options.supersedes,
39
+ };
40
+ saveInstruction(entry, cwd);
41
+ return entry;
42
+ }
43
+ export function resolveInstructions(entries, options = {}) {
44
+ const superseded = new Set(entries
45
+ .filter((entry) => entry.active && entry.supersedes)
46
+ .map((entry) => entry.supersedes));
47
+ const active = entries.filter((entry) => entry.active && !superseded.has(entry.id));
48
+ const scoped = active.filter((entry) => {
49
+ if (entry.layer === 'global') {
50
+ return true;
51
+ }
52
+ if (entry.layer === 'project') {
53
+ return entry.scope === options.project;
54
+ }
55
+ return entry.scope === options.agent;
56
+ });
57
+ const latestByScope = new Map();
58
+ for (const entry of scoped) {
59
+ const key = `${entry.layer}:${entry.scope ?? '*'}`;
60
+ const current = latestByScope.get(key);
61
+ if (!current || current.updated_at.localeCompare(entry.updated_at) <= 0) {
62
+ latestByScope.set(key, entry);
63
+ }
64
+ }
65
+ return [...latestByScope.values()].sort((a, b) => {
66
+ const rank = layerOrder(a.layer) - layerOrder(b.layer);
67
+ if (rank !== 0)
68
+ return rank;
69
+ return a.updated_at.localeCompare(b.updated_at) || a.id.localeCompare(b.id);
70
+ });
71
+ }
72
+ export function inferProjectFromTarget(target, config) {
73
+ const trimmed = target?.trim();
74
+ if (!trimmed) {
75
+ return undefined;
76
+ }
77
+ const normalized = trimmed.replace(/\\/g, '/');
78
+ const known = config.projects?.known ?? [];
79
+ if (known.includes(normalized)) {
80
+ return normalized;
81
+ }
82
+ return known.find((project) => normalized === project || normalized.startsWith(`${project}/`));
83
+ }
84
+ export function findInstructionConflicts(entries) {
85
+ const superseded = new Set(entries
86
+ .filter((entry) => entry.active && entry.supersedes)
87
+ .map((entry) => entry.supersedes));
88
+ const groups = new Map();
89
+ for (const entry of entries.filter((item) => item.active && !superseded.has(item.id))) {
90
+ const key = `${entry.layer}:${entry.scope ?? ''}`;
91
+ const bucket = groups.get(key) ?? [];
92
+ bucket.push(entry);
93
+ groups.set(key, bucket);
94
+ }
95
+ const conflicts = [];
96
+ for (const group of groups.values()) {
97
+ if (group.length > 1) {
98
+ conflicts.push({
99
+ layer: group[0].layer,
100
+ scope: group[0].scope,
101
+ ids: group.map((entry) => entry.id),
102
+ });
103
+ }
104
+ }
105
+ return conflicts;
106
+ }
107
+ function layerOrder(layer) {
108
+ switch (layer) {
109
+ case 'global':
110
+ return 0;
111
+ case 'project':
112
+ return 1;
113
+ case 'agent':
114
+ return 2;
115
+ }
116
+ }
117
+ //# sourceMappingURL=instructions.js.map
@@ -0,0 +1,191 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { withLock, cleanStaleLocks } from './lock.js';
4
+ export const MEMORY_DIR = '.brainclaw';
5
+ const RETRYABLE_RENAME_ERROR_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
6
+ const DEFAULT_RENAME_RETRY_ATTEMPTS = 6;
7
+ const DEFAULT_RENAME_RETRY_DELAY_MS = 25;
8
+ /**
9
+ * Entity-aligned directory mapping.
10
+ * Maps legacy flat directory names to their entity-partitioned paths.
11
+ * Used by resolveEntityDir() for backward-compatible reads and forward writes.
12
+ */
13
+ const ENTITY_DIR_MAP = {
14
+ // memory/ — Project entity: durable knowledge
15
+ 'constraints': 'memory/constraints',
16
+ 'decisions': 'memory/decisions',
17
+ 'traps': 'memory/traps',
18
+ 'traps-hosts': 'memory/traps-hosts',
19
+ 'traps-private': 'memory/traps-private',
20
+ 'instructions': 'memory/instructions',
21
+ // coordination/ — Agent↔Project: active work state
22
+ 'plans': 'coordination/plans',
23
+ 'claims': 'coordination/claims',
24
+ 'handoffs': 'coordination/handoffs',
25
+ 'sessions': 'coordination/sessions',
26
+ 'inbox': 'coordination/inbox',
27
+ 'inbox/accepted': 'coordination/inbox/accepted',
28
+ 'inbox/rejected': 'coordination/inbox/rejected',
29
+ 'runtime': 'coordination/runtime',
30
+ 'runtime-hosts': 'coordination/runtime-hosts',
31
+ 'runtime-private': 'coordination/runtime-private',
32
+ // discovery/ — Project entity: what's available
33
+ 'bootstrap': 'discovery/bootstrap',
34
+ 'bootstrap/seeds': 'discovery/bootstrap/seeds',
35
+ // agents/ — stays at top level (already entity-aligned)
36
+ 'agents': 'agents',
37
+ };
38
+ /**
39
+ * Resolve a subdirectory path with entity-model awareness.
40
+ *
41
+ * For READS: tries the new entity path first, falls back to legacy flat path.
42
+ * For WRITES: always uses the new entity path (creates parent dirs as needed).
43
+ *
44
+ * @param subdir Legacy subdirectory name (e.g. 'constraints', 'claims')
45
+ * @param cwd Project root
46
+ * @param mode 'read' checks both paths, 'write' uses new path only
47
+ */
48
+ export function resolveEntityDir(subdir, cwd = process.cwd(), mode = 'read', preferredDirName) {
49
+ const base = memoryDir(cwd, preferredDirName);
50
+ const newPath = ENTITY_DIR_MAP[subdir];
51
+ if (!newPath) {
52
+ // Unknown subdirectory — use as-is
53
+ return path.join(base, subdir);
54
+ }
55
+ const entityPath = path.join(base, newPath);
56
+ const legacyPath = path.join(base, subdir);
57
+ if (mode === 'write') {
58
+ // Always write to new entity path
59
+ return entityPath;
60
+ }
61
+ // Read: prefer entity path if it has content, fall back to legacy
62
+ if (fs.existsSync(entityPath) && hasContent(entityPath))
63
+ return entityPath;
64
+ if (fs.existsSync(legacyPath))
65
+ return legacyPath;
66
+ // Neither exists — return entity path (caller will handle missing dir)
67
+ return entityPath;
68
+ }
69
+ export function memoryDir(cwd = process.cwd(), preferredDirName) {
70
+ return path.join(cwd, preferredDirName ?? MEMORY_DIR);
71
+ }
72
+ export function memoryPath(filename, cwd, preferredDirName) {
73
+ return path.join(memoryDir(cwd, preferredDirName), filename);
74
+ }
75
+ export function memoryExists(cwd, preferredDirName) {
76
+ return fs.existsSync(memoryDir(cwd, preferredDirName));
77
+ }
78
+ export function ensureMemoryDir(cwd, preferredDirName) {
79
+ const dir = memoryDir(cwd, preferredDirName);
80
+ if (!fs.existsSync(dir)) {
81
+ fs.mkdirSync(dir, { recursive: true });
82
+ }
83
+ // Ensure entity-aligned subdirectories exist
84
+ const entityDirs = [
85
+ 'memory/constraints', 'memory/decisions', 'memory/traps', 'memory/instructions',
86
+ 'coordination/plans', 'coordination/claims', 'coordination/handoffs', 'coordination/sessions',
87
+ 'coordination/inbox',
88
+ 'discovery',
89
+ 'agents',
90
+ ];
91
+ for (const subdir of entityDirs) {
92
+ const p = path.join(dir, subdir);
93
+ if (!fs.existsSync(p)) {
94
+ fs.mkdirSync(p, { recursive: true });
95
+ }
96
+ }
97
+ }
98
+ /** Check if a path is a file, or a directory with at least one entry. */
99
+ function hasContent(p) {
100
+ try {
101
+ const stat = fs.statSync(p);
102
+ if (stat.isFile())
103
+ return true;
104
+ if (stat.isDirectory())
105
+ return fs.readdirSync(p).length > 0;
106
+ return false;
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
112
+ export function readFileSync(filepath) {
113
+ return fs.readFileSync(filepath, 'utf-8');
114
+ }
115
+ function syncSleep(ms) {
116
+ if (ms <= 0)
117
+ return;
118
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
119
+ }
120
+ function makeTempPath(filepath) {
121
+ const dir = path.dirname(filepath);
122
+ const base = path.basename(filepath);
123
+ const unique = `${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 10)}`;
124
+ return path.join(dir, `.${base}.${unique}.tmp`);
125
+ }
126
+ function isRetryableRenameError(error) {
127
+ if (!(error instanceof Error) || !('code' in error))
128
+ return false;
129
+ const code = error.code;
130
+ return code ? RETRYABLE_RENAME_ERROR_CODES.has(code) : false;
131
+ }
132
+ function renameWithRetry(tmpPath, targetPath, options) {
133
+ const { fsImpl, maxRenameAttempts, retryDelayMs, sleep } = options;
134
+ for (let attempt = 0; attempt < maxRenameAttempts; attempt++) {
135
+ try {
136
+ fsImpl.renameSync(tmpPath, targetPath);
137
+ return;
138
+ }
139
+ catch (error) {
140
+ if (!isRetryableRenameError(error) || attempt === maxRenameAttempts - 1) {
141
+ throw error;
142
+ }
143
+ sleep(retryDelayMs * (attempt + 1));
144
+ }
145
+ }
146
+ }
147
+ /** Atomic write with advisory file locking: acquire lock, write to a temp file, then rename. */
148
+ export function writeFileAtomic(filepath, content, options = {}) {
149
+ withLock(filepath, () => {
150
+ const fsImpl = options.fsImpl ?? fs;
151
+ const tmp = makeTempPath(filepath);
152
+ fsImpl.writeFileSync(tmp, content, 'utf-8');
153
+ renameWithRetry(tmp, filepath, {
154
+ fsImpl,
155
+ maxRenameAttempts: options.maxRenameAttempts ?? DEFAULT_RENAME_RETRY_ATTEMPTS,
156
+ retryDelayMs: options.retryDelayMs ?? DEFAULT_RENAME_RETRY_DELAY_MS,
157
+ sleep: options.sleep ?? syncSleep,
158
+ });
159
+ });
160
+ }
161
+ /**
162
+ * Remove orphan .tmp and .lock files left by crashed processes.
163
+ * Call once at CLI startup. Returns count of removed files.
164
+ */
165
+ export function cleanOrphanFiles(dirPath) {
166
+ let removed = 0;
167
+ if (!fs.existsSync(dirPath))
168
+ return 0;
169
+ // Clean .tmp files (residual from crashed writeFileAtomic)
170
+ try {
171
+ for (const entry of fs.readdirSync(dirPath)) {
172
+ const full = path.join(dirPath, entry);
173
+ if (entry.endsWith('.tmp') && fs.statSync(full).isFile()) {
174
+ try {
175
+ fs.unlinkSync(full);
176
+ removed++;
177
+ }
178
+ catch { /* already gone */ }
179
+ }
180
+ // Recurse into subdirectories
181
+ if (fs.statSync(full).isDirectory()) {
182
+ removed += cleanOrphanFiles(full);
183
+ }
184
+ }
185
+ }
186
+ catch { /* dir unreadable — skip */ }
187
+ // Clean stale .lock files
188
+ removed += cleanStaleLocks(dirPath);
189
+ return removed;
190
+ }
191
+ //# sourceMappingURL=io.js.map
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { logger } from './logger.js';
4
+ import { loadVersionedJsonFile, saveVersionedJsonFile } from './migration.js';
5
+ export class JsonStore {
6
+ dirPath;
7
+ documentType;
8
+ getId;
9
+ sort;
10
+ constructor(options) {
11
+ this.dirPath = options.dirPath;
12
+ this.documentType = options.documentType;
13
+ this.getId = options.getId;
14
+ this.sort = options.sort;
15
+ }
16
+ list() {
17
+ if (!fs.existsSync(this.dirPath)) {
18
+ return [];
19
+ }
20
+ const items = [];
21
+ for (const file of fs.readdirSync(this.dirPath).filter((entry) => entry.endsWith('.json')).sort()) {
22
+ try {
23
+ items.push(this.load(file.replace(/\.json$/i, '')));
24
+ }
25
+ catch (error) {
26
+ logger.debug(`Skipping malformed ${this.documentType} file:`, file, error);
27
+ }
28
+ }
29
+ if (this.sort) {
30
+ items.sort(this.sort);
31
+ }
32
+ return items;
33
+ }
34
+ load(id) {
35
+ const filepath = this.pathFor(id);
36
+ if (!fs.existsSync(filepath)) {
37
+ throw new Error(`${this.documentType} '${id}' not found`);
38
+ }
39
+ return loadVersionedJsonFile(this.documentType, filepath).document;
40
+ }
41
+ save(item) {
42
+ this.ensureDir();
43
+ saveVersionedJsonFile(this.documentType, this.pathFor(this.getId(item)), item);
44
+ }
45
+ delete(id) {
46
+ const filepath = this.pathFor(id);
47
+ if (fs.existsSync(filepath)) {
48
+ fs.unlinkSync(filepath);
49
+ }
50
+ }
51
+ exists(id) {
52
+ return fs.existsSync(this.pathFor(id));
53
+ }
54
+ ensureDir() {
55
+ if (!fs.existsSync(this.dirPath)) {
56
+ fs.mkdirSync(this.dirPath, { recursive: true });
57
+ }
58
+ }
59
+ pathFor(id) {
60
+ return path.join(this.dirPath, `${id}.json`);
61
+ }
62
+ }
63
+ //# sourceMappingURL=json-store.js.map
@@ -0,0 +1,45 @@
1
+ import { loadState } from './state.js';
2
+ import { loadInstructions } from './instructions.js';
3
+ /**
4
+ * Returns all memory items tagged with the given trigger tag.
5
+ * Supports: traps, constraints, decisions, instructions.
6
+ *
7
+ * Convention: trigger tags take the form "trigger:<event>"
8
+ * e.g. "trigger:post-claim", "trigger:pre-session-end", "trigger:post-session-start"
9
+ */
10
+ export function getTriggeredItems(tag, cwd) {
11
+ const items = [];
12
+ const state = loadState(cwd);
13
+ for (const trap of state.known_traps) {
14
+ if (trap.tags.includes(tag)) {
15
+ items.push({ type: 'trap', id: trap.id, text: trap.text });
16
+ }
17
+ }
18
+ for (const constraint of state.active_constraints) {
19
+ if (constraint.tags.includes(tag)) {
20
+ items.push({ type: 'constraint', id: constraint.id, text: constraint.text });
21
+ }
22
+ }
23
+ for (const decision of state.recent_decisions) {
24
+ if (decision.tags.includes(tag)) {
25
+ items.push({ type: 'decision', id: decision.id, text: decision.text });
26
+ }
27
+ }
28
+ for (const instruction of loadInstructions(cwd)) {
29
+ if (instruction.tags.includes(tag)) {
30
+ items.push({ type: 'instruction', id: instruction.id, text: instruction.text });
31
+ }
32
+ }
33
+ return items;
34
+ }
35
+ /**
36
+ * Renders triggered items as a text block for inclusion in MCP responses.
37
+ * Returns empty string if no items found.
38
+ */
39
+ export function renderTriggeredItems(items) {
40
+ if (items.length === 0)
41
+ return '';
42
+ const lines = items.map((item) => `⚡ [${item.type}] ${item.text}`);
43
+ return lines.join('\n');
44
+ }
45
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1,129 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const DEFAULT_TIMEOUT_MS = 2000;
4
+ const LOCK_RETRY_INTERVAL_MS = 50;
5
+ const LOCK_EXPIRY_MS = 5000;
6
+ function lockFilePath(targetPath) {
7
+ return targetPath + '.lock';
8
+ }
9
+ function syncSleep(ms) {
10
+ if (ms <= 0)
11
+ return;
12
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
13
+ }
14
+ function isProcessAlive(pid) {
15
+ try {
16
+ process.kill(pid, 0);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ function readLockData(lockPath) {
24
+ try {
25
+ const raw = fs.readFileSync(lockPath, 'utf-8');
26
+ return JSON.parse(raw);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ function tryCreateLock(lockPath) {
33
+ const data = { pid: process.pid, timestamp: Date.now() };
34
+ try {
35
+ fs.writeFileSync(lockPath, JSON.stringify(data), { encoding: 'utf-8', flag: 'wx' });
36
+ return true;
37
+ }
38
+ catch (err) {
39
+ if (err instanceof Error && 'code' in err && err.code === 'EEXIST')
40
+ return false;
41
+ throw err;
42
+ }
43
+ }
44
+ function tryBreakLock(lockPath) {
45
+ const data = readLockData(lockPath);
46
+ if (!data)
47
+ return false;
48
+ const expired = Date.now() - data.timestamp > LOCK_EXPIRY_MS;
49
+ const ownerDead = !isProcessAlive(data.pid);
50
+ if (!expired && !ownerDead)
51
+ return false;
52
+ try {
53
+ fs.unlinkSync(lockPath);
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ return tryCreateLock(lockPath);
59
+ }
60
+ export function acquireLock(targetPath, timeoutMs = DEFAULT_TIMEOUT_MS) {
61
+ const lockPath = lockFilePath(targetPath);
62
+ const deadline = Date.now() + timeoutMs;
63
+ const dir = path.dirname(lockPath);
64
+ if (!fs.existsSync(dir)) {
65
+ fs.mkdirSync(dir, { recursive: true });
66
+ }
67
+ while (Date.now() < deadline) {
68
+ if (tryCreateLock(lockPath))
69
+ return true;
70
+ if (tryBreakLock(lockPath))
71
+ return true;
72
+ syncSleep(Math.min(LOCK_RETRY_INTERVAL_MS, deadline - Date.now()));
73
+ }
74
+ return false;
75
+ }
76
+ export function releaseLock(targetPath) {
77
+ const lockPath = lockFilePath(targetPath);
78
+ try {
79
+ if (fs.existsSync(lockPath)) {
80
+ fs.unlinkSync(lockPath);
81
+ }
82
+ }
83
+ catch {
84
+ // Ignore errors during release
85
+ }
86
+ }
87
+ export function withLock(targetPath, fn, timeoutMs = DEFAULT_TIMEOUT_MS) {
88
+ const acquired = acquireLock(targetPath, timeoutMs);
89
+ if (!acquired) {
90
+ throw new Error(`Could not acquire lock on ${path.basename(targetPath)} after ${timeoutMs}ms`);
91
+ }
92
+ try {
93
+ return fn();
94
+ }
95
+ finally {
96
+ releaseLock(targetPath);
97
+ }
98
+ }
99
+ export function cleanStaleLocks(dirPath) {
100
+ let removed = 0;
101
+ let entries;
102
+ try {
103
+ entries = fs.readdirSync(dirPath);
104
+ }
105
+ catch {
106
+ return 0;
107
+ }
108
+ for (const entry of entries) {
109
+ if (!entry.endsWith('.lock'))
110
+ continue;
111
+ const lockPath = path.join(dirPath, entry);
112
+ const data = readLockData(lockPath);
113
+ if (!data)
114
+ continue;
115
+ const expired = Date.now() - data.timestamp > LOCK_EXPIRY_MS;
116
+ const ownerDead = !isProcessAlive(data.pid);
117
+ if (expired || ownerDead) {
118
+ try {
119
+ fs.unlinkSync(lockPath);
120
+ removed++;
121
+ }
122
+ catch {
123
+ // Another process may have already cleaned it
124
+ }
125
+ }
126
+ }
127
+ return removed;
128
+ }
129
+ //# sourceMappingURL=lock.js.map