dig-burrow 1.2.0
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/.claude/burrow/VERSION +1 -0
- package/.claude/burrow/burrow-tools.cjs +490 -0
- package/.claude/burrow/lib/core.cjs +26 -0
- package/.claude/burrow/lib/init.cjs +95 -0
- package/.claude/burrow/lib/installer.cjs +389 -0
- package/.claude/burrow/lib/mongoose.cjs +461 -0
- package/.claude/burrow/lib/render.cjs +330 -0
- package/.claude/burrow/lib/version.cjs +137 -0
- package/.claude/burrow/lib/warren.cjs +168 -0
- package/.claude/burrow/workflows/burrow.md +184 -0
- package/.claude/commands/burrow/add.md +12 -0
- package/.claude/commands/burrow/archive.md +12 -0
- package/.claude/commands/burrow/dump.md +12 -0
- package/.claude/commands/burrow/edit.md +12 -0
- package/.claude/commands/burrow/help.md +29 -0
- package/.claude/commands/burrow/move.md +12 -0
- package/.claude/commands/burrow/read.md +12 -0
- package/.claude/commands/burrow/remove.md +12 -0
- package/.claude/commands/burrow/unarchive.md +12 -0
- package/.claude/commands/burrow/update.md +15 -0
- package/.claude/commands/burrow.md +16 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/install.cjs +430 -0
- package/package.json +31 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
// ── Sentinel markers ──────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const SENTINEL_START = '<!-- burrow:start -->';
|
|
9
|
+
const SENTINEL_END = '<!-- burrow:end -->';
|
|
10
|
+
|
|
11
|
+
// ── CLAUDE.md snippet ─────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const CLAUDE_MD_SNIPPET = `## Burrow — Agent Memory
|
|
14
|
+
|
|
15
|
+
Burrow is your persistent memory. On every session start, silently read \`.planning/burrow/cards.json\` using the Read tool to load project context, tasks, and notes.
|
|
16
|
+
|
|
17
|
+
**When the user says "remember", "don't forget", "always do X", or any instruction meant to persist across sessions:**
|
|
18
|
+
- Store it as a Burrow card: \`node .claude/burrow/burrow-tools.cjs add --title "<summary>" --body "<details>"\`
|
|
19
|
+
- Organize under relevant parent cards (create parents if needed)
|
|
20
|
+
- Do NOT write to loose markdown files or other ad-hoc storage — Burrow is the single source of truth
|
|
21
|
+
|
|
22
|
+
**Privacy:** Burrow data is meant to be committed to git. Anything stored in cards is visible to anyone with repo access. Avoid storing secrets, credentials, or sensitive personal information.
|
|
23
|
+
|
|
24
|
+
All mutations go through the CLI — NEVER edit cards.json directly.
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
// ── Core file paths (relative to target) ─────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Core paths used for detection.
|
|
31
|
+
* Note: .claude/commands/burrow is a directory (no extension).
|
|
32
|
+
*/
|
|
33
|
+
const CORE_PATHS = [
|
|
34
|
+
'.claude/burrow/burrow-tools.cjs',
|
|
35
|
+
'.claude/commands/burrow.md',
|
|
36
|
+
'.claude/commands/burrow',
|
|
37
|
+
'.planning/burrow/cards.json',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function ensureDir(dir) {
|
|
43
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function copyDirSync(src, dest) {
|
|
47
|
+
ensureDir(dest);
|
|
48
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
49
|
+
const srcPath = path.join(src, entry.name);
|
|
50
|
+
const destPath = path.join(dest, entry.name);
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
copyDirSync(srcPath, destPath);
|
|
53
|
+
} else {
|
|
54
|
+
fs.copyFileSync(srcPath, destPath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Detect the line ending convention of existing content.
|
|
61
|
+
* Returns '\r\n' if CRLF is predominant, '\n' otherwise.
|
|
62
|
+
*/
|
|
63
|
+
function detectLineEnding(content) {
|
|
64
|
+
const crlfCount = (content.match(/\r\n/g) || []).length;
|
|
65
|
+
const lfCount = (content.match(/(?<!\r)\n/g) || []).length;
|
|
66
|
+
return crlfCount > lfCount ? '\r\n' : '\n';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convert a snippet to use the target line ending.
|
|
71
|
+
*/
|
|
72
|
+
function normaliseLineEndings(text, eol) {
|
|
73
|
+
// Normalise to LF first, then convert
|
|
74
|
+
const lf = text.replace(/\r\n/g, '\n');
|
|
75
|
+
if (eol === '\r\n') {
|
|
76
|
+
return lf.replace(/\n/g, '\r\n');
|
|
77
|
+
}
|
|
78
|
+
return lf;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Ensure .gitignore at targetDir contains the given entry.
|
|
83
|
+
* Returns 'created' | 'updated' | 'unchanged'.
|
|
84
|
+
*/
|
|
85
|
+
function ensureGitignoreEntry(targetDir, entry) {
|
|
86
|
+
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
87
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
88
|
+
fs.writeFileSync(gitignorePath, `${entry}\n`);
|
|
89
|
+
return 'created';
|
|
90
|
+
}
|
|
91
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
92
|
+
if (content.includes(entry)) return 'unchanged';
|
|
93
|
+
const trimmed = content.replace(/\n$/, '');
|
|
94
|
+
fs.writeFileSync(gitignorePath, `${trimmed}\n${entry}\n`);
|
|
95
|
+
return 'updated';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── detect() ─────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Scan targetDir and determine installer mode.
|
|
102
|
+
*
|
|
103
|
+
* Returns:
|
|
104
|
+
* { mode: 'fresh', hasSentinel, hasLegacyClaude }
|
|
105
|
+
* { mode: 'upgrade', version: string|null, hasSentinel, hasLegacyClaude }
|
|
106
|
+
* { mode: 'repair', missing: string[], hasSentinel, hasLegacyClaude }
|
|
107
|
+
*/
|
|
108
|
+
function detect(targetDir) {
|
|
109
|
+
const present = [];
|
|
110
|
+
const missing = [];
|
|
111
|
+
|
|
112
|
+
for (const rel of CORE_PATHS) {
|
|
113
|
+
const full = path.join(targetDir, rel);
|
|
114
|
+
if (fs.existsSync(full)) {
|
|
115
|
+
present.push(rel);
|
|
116
|
+
} else {
|
|
117
|
+
missing.push(rel);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// CLAUDE.md sentinel/legacy detection
|
|
122
|
+
const claudeMdPath = path.join(targetDir, 'CLAUDE.md');
|
|
123
|
+
let hasSentinel = false;
|
|
124
|
+
let hasLegacyClaude = false;
|
|
125
|
+
|
|
126
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
127
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
128
|
+
hasSentinel = content.includes(SENTINEL_START);
|
|
129
|
+
// Legacy = has "## Burrow" heading but no sentinel markers
|
|
130
|
+
hasLegacyClaude = !hasSentinel && content.includes('## Burrow');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (present.length === 0) {
|
|
134
|
+
return { mode: 'fresh', hasSentinel, hasLegacyClaude };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (missing.length === 0) {
|
|
138
|
+
// Read version from burrow-tools.cjs or package.json if available
|
|
139
|
+
let version = null;
|
|
140
|
+
const versionFile = path.join(targetDir, '.claude', 'burrow', 'VERSION');
|
|
141
|
+
const pkgFile = path.join(targetDir, '.claude', 'burrow', 'package.json');
|
|
142
|
+
if (fs.existsSync(versionFile)) {
|
|
143
|
+
version = fs.readFileSync(versionFile, 'utf-8').trim();
|
|
144
|
+
} else if (fs.existsSync(pkgFile)) {
|
|
145
|
+
try {
|
|
146
|
+
const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf-8'));
|
|
147
|
+
version = pkg.version || null;
|
|
148
|
+
} catch (_) {
|
|
149
|
+
// ignore
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { mode: 'upgrade', version, hasSentinel, hasLegacyClaude };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { mode: 'repair', missing, hasSentinel, hasLegacyClaude };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── writeSentinelBlock() ──────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Insert or replace a sentinel-wrapped block in claudeMdPath.
|
|
162
|
+
* Creates the file if it does not exist.
|
|
163
|
+
* Preserves all content outside sentinel markers.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} claudeMdPath - Absolute path to CLAUDE.md
|
|
166
|
+
* @param {string} blockContent - Content to wrap with sentinel markers
|
|
167
|
+
*/
|
|
168
|
+
function writeSentinelBlock(claudeMdPath, blockContent) {
|
|
169
|
+
let existingContent = '';
|
|
170
|
+
let eol = '\n';
|
|
171
|
+
|
|
172
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
173
|
+
existingContent = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
174
|
+
eol = detectLineEnding(existingContent);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Normalise block content to use file's line ending
|
|
178
|
+
const block = normaliseLineEndings(blockContent, eol);
|
|
179
|
+
const sentinelStart = SENTINEL_START;
|
|
180
|
+
const sentinelEnd = SENTINEL_END;
|
|
181
|
+
|
|
182
|
+
const newBlock = `${sentinelStart}${eol}${block}${sentinelEnd}`;
|
|
183
|
+
|
|
184
|
+
if (existingContent.includes(sentinelStart)) {
|
|
185
|
+
// Replace existing sentinel block (content between start and end markers)
|
|
186
|
+
const startIdx = existingContent.indexOf(sentinelStart);
|
|
187
|
+
const endIdx = existingContent.indexOf(sentinelEnd, startIdx);
|
|
188
|
+
if (endIdx !== -1) {
|
|
189
|
+
const before = existingContent.slice(0, startIdx);
|
|
190
|
+
const after = existingContent.slice(endIdx + sentinelEnd.length);
|
|
191
|
+
fs.writeFileSync(claudeMdPath, `${before}${newBlock}${after}`);
|
|
192
|
+
} else {
|
|
193
|
+
// Malformed: start without end — replace from start to EOF
|
|
194
|
+
const before = existingContent.slice(0, startIdx);
|
|
195
|
+
fs.writeFileSync(claudeMdPath, `${before}${newBlock}`);
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
// Append to end
|
|
199
|
+
const trimmed = existingContent.trimEnd();
|
|
200
|
+
const separator = trimmed.length > 0 ? `${eol}${eol}` : '';
|
|
201
|
+
fs.writeFileSync(claudeMdPath, `${trimmed}${separator}${newBlock}${eol}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── removeSentinelBlock() ─────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Remove the sentinel-wrapped block from claudeMdPath.
|
|
209
|
+
* No-op if file does not exist or no sentinels found.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} claudeMdPath - Absolute path to CLAUDE.md
|
|
212
|
+
*/
|
|
213
|
+
function removeSentinelBlock(claudeMdPath) {
|
|
214
|
+
if (!fs.existsSync(claudeMdPath)) return;
|
|
215
|
+
|
|
216
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
217
|
+
if (!content.includes(SENTINEL_START)) return;
|
|
218
|
+
|
|
219
|
+
const startIdx = content.indexOf(SENTINEL_START);
|
|
220
|
+
const endIdx = content.indexOf(SENTINEL_END, startIdx);
|
|
221
|
+
|
|
222
|
+
if (endIdx === -1) {
|
|
223
|
+
// Malformed — remove from start marker to end of file
|
|
224
|
+
const before = content.slice(0, startIdx).trimEnd();
|
|
225
|
+
fs.writeFileSync(claudeMdPath, before.length > 0 ? `${before}\n` : '');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const before = content.slice(0, startIdx);
|
|
230
|
+
const after = content.slice(endIdx + SENTINEL_END.length);
|
|
231
|
+
|
|
232
|
+
// Collapse any double blank lines left over from removal
|
|
233
|
+
const joined = `${before}${after}`;
|
|
234
|
+
// Trim leading/trailing blank lines that were separators
|
|
235
|
+
const result = joined.replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
236
|
+
fs.writeFileSync(claudeMdPath, result.length > 0 ? `${result}\n` : '');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── performInstall() ──────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Perform a fresh install: copy all source files, create data dir, add .gitignore entry.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} sourceDir - Path to burrow repo root (where .claude/burrow exists)
|
|
245
|
+
* @param {string} targetDir - Path to target project root
|
|
246
|
+
* @param {object} [opts] - Optional flags (reserved for future use)
|
|
247
|
+
* @returns {object} - Results map: { burrowDir, commandFile, commandDir, cardsJson, gitignore }
|
|
248
|
+
*/
|
|
249
|
+
function performInstall(sourceDir, targetDir, opts = {}) {
|
|
250
|
+
const results = {};
|
|
251
|
+
|
|
252
|
+
// 1. Copy .claude/burrow/
|
|
253
|
+
const srcBurrow = path.join(sourceDir, '.claude', 'burrow');
|
|
254
|
+
const destBurrow = path.join(targetDir, '.claude', 'burrow');
|
|
255
|
+
copyDirSync(srcBurrow, destBurrow);
|
|
256
|
+
results.burrowDir = 'copied';
|
|
257
|
+
|
|
258
|
+
// 2. Copy .claude/commands/burrow.md
|
|
259
|
+
ensureDir(path.join(targetDir, '.claude', 'commands'));
|
|
260
|
+
const srcCommandFile = path.join(sourceDir, '.claude', 'commands', 'burrow.md');
|
|
261
|
+
const destCommandFile = path.join(targetDir, '.claude', 'commands', 'burrow.md');
|
|
262
|
+
fs.copyFileSync(srcCommandFile, destCommandFile);
|
|
263
|
+
results.commandFile = 'copied';
|
|
264
|
+
|
|
265
|
+
// 3. Copy .claude/commands/burrow/
|
|
266
|
+
const srcCommandDir = path.join(sourceDir, '.claude', 'commands', 'burrow');
|
|
267
|
+
const destCommandDir = path.join(targetDir, '.claude', 'commands', 'burrow');
|
|
268
|
+
copyDirSync(srcCommandDir, destCommandDir);
|
|
269
|
+
results.commandDir = 'copied';
|
|
270
|
+
|
|
271
|
+
// 4. Create .planning/burrow/ and empty cards.json
|
|
272
|
+
const dataDir = path.join(targetDir, '.planning', 'burrow');
|
|
273
|
+
ensureDir(dataDir);
|
|
274
|
+
const cardsPath = path.join(dataDir, 'cards.json');
|
|
275
|
+
if (!fs.existsSync(cardsPath)) {
|
|
276
|
+
fs.writeFileSync(cardsPath, JSON.stringify({ version: 2, cards: [] }) + '\n');
|
|
277
|
+
results.cardsJson = 'created';
|
|
278
|
+
} else {
|
|
279
|
+
results.cardsJson = 'preserved';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 5. .gitignore entry
|
|
283
|
+
results.gitignore = ensureGitignoreEntry(targetDir, '.planning/burrow/cards.json.bak');
|
|
284
|
+
|
|
285
|
+
return results;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── performUpgrade() ──────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Perform an upgrade: unconditionally replace source files, preserve cards.json.
|
|
292
|
+
*
|
|
293
|
+
* @param {string} sourceDir - Path to burrow repo root
|
|
294
|
+
* @param {string} targetDir - Path to target project root
|
|
295
|
+
* @param {object} [opts] - Optional flags (reserved for future use)
|
|
296
|
+
* @returns {object} - Results map
|
|
297
|
+
*/
|
|
298
|
+
function performUpgrade(sourceDir, targetDir, opts = {}) {
|
|
299
|
+
const results = {};
|
|
300
|
+
|
|
301
|
+
// 1. Replace .claude/burrow/ (unconditional)
|
|
302
|
+
const srcBurrow = path.join(sourceDir, '.claude', 'burrow');
|
|
303
|
+
const destBurrow = path.join(targetDir, '.claude', 'burrow');
|
|
304
|
+
copyDirSync(srcBurrow, destBurrow);
|
|
305
|
+
results.burrowDir = 'replaced';
|
|
306
|
+
|
|
307
|
+
// 2. Replace .claude/commands/burrow.md
|
|
308
|
+
ensureDir(path.join(targetDir, '.claude', 'commands'));
|
|
309
|
+
const srcCommandFile = path.join(sourceDir, '.claude', 'commands', 'burrow.md');
|
|
310
|
+
const destCommandFile = path.join(targetDir, '.claude', 'commands', 'burrow.md');
|
|
311
|
+
fs.copyFileSync(srcCommandFile, destCommandFile);
|
|
312
|
+
results.commandFile = 'replaced';
|
|
313
|
+
|
|
314
|
+
// 3. Replace .claude/commands/burrow/
|
|
315
|
+
const srcCommandDir = path.join(sourceDir, '.claude', 'commands', 'burrow');
|
|
316
|
+
const destCommandDir = path.join(targetDir, '.claude', 'commands', 'burrow');
|
|
317
|
+
copyDirSync(srcCommandDir, destCommandDir);
|
|
318
|
+
results.commandDir = 'replaced';
|
|
319
|
+
|
|
320
|
+
// 4. cards.json is NEVER touched on upgrade
|
|
321
|
+
results.cardsJson = 'preserved';
|
|
322
|
+
|
|
323
|
+
// 5. .gitignore entry (idempotent)
|
|
324
|
+
results.gitignore = ensureGitignoreEntry(targetDir, '.planning/burrow/cards.json.bak');
|
|
325
|
+
|
|
326
|
+
return results;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── performRepair() ───────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Perform a repair: copy only the files listed in `missingFiles`.
|
|
333
|
+
* Does not touch any files that already exist at target.
|
|
334
|
+
*
|
|
335
|
+
* @param {string} sourceDir - Path to burrow repo root
|
|
336
|
+
* @param {string} targetDir - Path to target project root
|
|
337
|
+
* @param {string[]} missingFiles - Relative paths (from detect().missing)
|
|
338
|
+
* @returns {object} - Results map: { [rel]: 'copied' }
|
|
339
|
+
*/
|
|
340
|
+
function performRepair(sourceDir, targetDir, missingFiles) {
|
|
341
|
+
const results = {};
|
|
342
|
+
|
|
343
|
+
for (const rel of missingFiles) {
|
|
344
|
+
const srcFull = path.join(sourceDir, rel);
|
|
345
|
+
const destFull = path.join(targetDir, rel);
|
|
346
|
+
|
|
347
|
+
if (!fs.existsSync(srcFull)) {
|
|
348
|
+
results[rel] = 'source-missing';
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const stat = fs.statSync(srcFull);
|
|
353
|
+
if (stat.isDirectory()) {
|
|
354
|
+
copyDirSync(srcFull, destFull);
|
|
355
|
+
results[rel] = 'copied-dir';
|
|
356
|
+
} else {
|
|
357
|
+
ensureDir(path.dirname(destFull));
|
|
358
|
+
fs.copyFileSync(srcFull, destFull);
|
|
359
|
+
results[rel] = 'copied';
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Handle cards.json specially: if missing, create empty one
|
|
364
|
+
const cardsRel = '.planning/burrow/cards.json';
|
|
365
|
+
if (missingFiles.includes(cardsRel)) {
|
|
366
|
+
const cardsPath = path.join(targetDir, cardsRel);
|
|
367
|
+
if (!fs.existsSync(cardsPath)) {
|
|
368
|
+
ensureDir(path.dirname(cardsPath));
|
|
369
|
+
fs.writeFileSync(cardsPath, JSON.stringify({ version: 2, cards: [] }) + '\n');
|
|
370
|
+
results[cardsRel] = 'created';
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return results;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── Exports ───────────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
module.exports = {
|
|
380
|
+
SENTINEL_START,
|
|
381
|
+
SENTINEL_END,
|
|
382
|
+
CLAUDE_MD_SNIPPET,
|
|
383
|
+
detect,
|
|
384
|
+
performInstall,
|
|
385
|
+
performUpgrade,
|
|
386
|
+
performRepair,
|
|
387
|
+
writeSentinelBlock,
|
|
388
|
+
removeSentinelBlock,
|
|
389
|
+
};
|