claude-recall 0.38.0 → 0.38.1
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/dist/hooks/memory-sync-hook.js +74 -18
- package/package.json +1 -1
|
@@ -54,6 +54,17 @@ const config_1 = require("../services/config");
|
|
|
54
54
|
const MAX_SYNC_FILES = 30;
|
|
55
55
|
/** Prefix for all recall memory files — prevents namespace collisions */
|
|
56
56
|
const FILE_PREFIX = 'recall_';
|
|
57
|
+
/**
|
|
58
|
+
* Marker comments fencing the managed "## Claude Recall" section in MEMORY.md.
|
|
59
|
+
* Everything between these markers is owned by this hook and rewritten in place;
|
|
60
|
+
* everything outside them is left byte-for-byte untouched. Before these markers
|
|
61
|
+
* existed, the section was re-appended to the end of the file on every sync, and
|
|
62
|
+
* the strip regex ran to EOF — so any content a user (or Claude) appended below
|
|
63
|
+
* the section was silently deleted on the next sync. The fence makes that
|
|
64
|
+
* impossible: the managed region no longer depends on being last in the file.
|
|
65
|
+
*/
|
|
66
|
+
const RECALL_BEGIN = '<!-- BEGIN CLAUDE RECALL (auto-generated — do not edit inside this block) -->';
|
|
67
|
+
const RECALL_END = '<!-- END CLAUDE RECALL -->';
|
|
57
68
|
/** Keys that look like test data */
|
|
58
69
|
const TEST_KEY_PATTERNS = [/^Test /i, /^Session test /i, /^test_/i];
|
|
59
70
|
/** Values that may contain secrets */
|
|
@@ -240,33 +251,78 @@ function renderMemoryFile(rule) {
|
|
|
240
251
|
];
|
|
241
252
|
return lines.join('\n');
|
|
242
253
|
}
|
|
254
|
+
/** Escape a literal string for use inside a RegExp. */
|
|
255
|
+
function escapeRegExp(s) {
|
|
256
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
257
|
+
}
|
|
243
258
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
259
|
+
* Lines the hook is allowed to remove when migrating a legacy (pre-fence)
|
|
260
|
+
* "## Claude Recall" section: blank lines, the empty sentinel, and pointer
|
|
261
|
+
* lines targeting a recall_*.md file this hook wrote. Anything else — including
|
|
262
|
+
* a hand-written pointer to a non-recall file — is NOT owned and stops the scan,
|
|
263
|
+
* so content that followed the old section survives the migration.
|
|
246
264
|
*/
|
|
247
|
-
function
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
265
|
+
function isOwnedRecallLine(line) {
|
|
266
|
+
const t = line.trim();
|
|
267
|
+
if (t === '')
|
|
268
|
+
return true;
|
|
269
|
+
if (t === '- No recall rules synced')
|
|
270
|
+
return true;
|
|
271
|
+
return /^- \[[^\]]*\]\(recall_[^)]*\.md\)/.test(t);
|
|
272
|
+
}
|
|
273
|
+
/** Build the marker-fenced managed block. */
|
|
274
|
+
function buildRecallBlock(files) {
|
|
275
|
+
const lines = [RECALL_BEGIN, '## Claude Recall'];
|
|
258
276
|
if (files.length === 0) {
|
|
259
|
-
|
|
277
|
+
lines.push('- No recall rules synced');
|
|
260
278
|
}
|
|
261
279
|
else {
|
|
262
280
|
for (const f of files) {
|
|
263
281
|
const hook = f.description.length > 80 ? f.description.substring(0, 77) + '...' : f.description;
|
|
264
|
-
|
|
282
|
+
lines.push(`- [${f.name}](${f.filename}) — ${hook}`);
|
|
265
283
|
}
|
|
266
284
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
285
|
+
lines.push(RECALL_END);
|
|
286
|
+
return lines.join('\n');
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Update MEMORY.md with pointers to recall files.
|
|
290
|
+
*
|
|
291
|
+
* The managed section is fenced between RECALL_BEGIN/RECALL_END marker comments
|
|
292
|
+
* and is the ONLY region this hook ever rewrites — everything outside the fence,
|
|
293
|
+
* anywhere in the file, is preserved exactly. Three cases:
|
|
294
|
+
* 1. Fence present → replace the block in place, between the markers.
|
|
295
|
+
* 2. Legacy unfenced "## Claude Recall" section → replace only the heading and
|
|
296
|
+
* the lines this hook owns (recall_* pointers / the empty sentinel),
|
|
297
|
+
* stopping at the first line it does not own so any hand-written content
|
|
298
|
+
* that followed the old section survives the one-time migration.
|
|
299
|
+
* 3. No section at all → append a fenced block at the end.
|
|
300
|
+
*/
|
|
301
|
+
function updateMemoryMdIndex(memoryDir, files) {
|
|
302
|
+
const memoryMdPath = path.join(memoryDir, 'MEMORY.md');
|
|
303
|
+
const existing = fs.existsSync(memoryMdPath) ? fs.readFileSync(memoryMdPath, 'utf-8') : '';
|
|
304
|
+
const block = buildRecallBlock(files);
|
|
305
|
+
// Case 1: fenced block already present — splice it in place, touching nothing else.
|
|
306
|
+
const fence = new RegExp(`${escapeRegExp(RECALL_BEGIN)}[\\s\\S]*?${escapeRegExp(RECALL_END)}`);
|
|
307
|
+
if (fence.test(existing)) {
|
|
308
|
+
fs.writeFileSync(memoryMdPath, existing.replace(fence, block));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const lines = existing.split('\n');
|
|
312
|
+
const headingIdx = lines.findIndex(l => l.trim() === '## Claude Recall');
|
|
313
|
+
// Case 2: legacy unfenced section — remove only the lines we own, keep the rest.
|
|
314
|
+
if (headingIdx !== -1) {
|
|
315
|
+
let end = headingIdx + 1;
|
|
316
|
+
while (end < lines.length && isOwnedRecallLine(lines[end]))
|
|
317
|
+
end++;
|
|
318
|
+
const merged = [...lines.slice(0, headingIdx), ...block.split('\n'), ...lines.slice(end)].join('\n');
|
|
319
|
+
fs.writeFileSync(memoryMdPath, merged);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Case 3: no section — append a fenced block at the end.
|
|
323
|
+
const base = existing.replace(/\s+$/, '');
|
|
324
|
+
const sep = base ? '\n\n' : '';
|
|
325
|
+
fs.writeFileSync(memoryMdPath, base + sep + block + '\n');
|
|
270
326
|
}
|
|
271
327
|
/**
|
|
272
328
|
* Clean up stale recall_* files that are no longer in the current sync set.
|
package/package.json
CHANGED