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.
@@ -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
- * Update MEMORY.md with pointers to recall files.
245
- * Replaces any existing "## Claude Recall" section, preserves everything else.
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 updateMemoryMdIndex(memoryDir, files) {
248
- const memoryMdPath = path.join(memoryDir, 'MEMORY.md');
249
- let existing = '';
250
- if (fs.existsSync(memoryMdPath)) {
251
- existing = fs.readFileSync(memoryMdPath, 'utf-8');
252
- }
253
- // Remove existing Claude Recall section (everything from ## Claude Recall to next ## or end)
254
- const sectionRegex = /\n?## Claude Recall\n[\s\S]*?(?=\n## |\n*$)/;
255
- const cleaned = existing.replace(sectionRegex, '').trimEnd();
256
- // Build new section
257
- const recallLines = ['', '## Claude Recall'];
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
- recallLines.push('- No recall rules synced');
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
- recallLines.push(`- [${f.name}](${f.filename}) — ${hook}`);
282
+ lines.push(`- [${f.name}](${f.filename}) — ${hook}`);
265
283
  }
266
284
  }
267
- recallLines.push('');
268
- const newContent = cleaned + recallLines.join('\n');
269
- fs.writeFileSync(memoryMdPath, newContent);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-recall",
3
- "version": "0.38.0",
3
+ "version": "0.38.1",
4
4
  "description": "Persistent memory for Claude Code and Pi with native Skills integration, automatic capture, failure learning, and project scoping",
5
5
  "main": "dist/index.js",
6
6
  "bin": {