claude-recall 0.18.9 → 0.19.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.
@@ -8,10 +8,20 @@ source: claude-recall
8
8
 
9
9
  # Corrections
10
10
 
11
- Auto-generated from 20 memories. Last updated: 2026-03-22.
11
+ Auto-generated from 30 memories. Last updated: 2026-04-02.
12
12
 
13
13
  ## Rules
14
14
 
15
+ - CORRECTION: Memory with complex metadata
16
+ - CORRECTION: Memory with complex metadata
17
+ - CORRECTION: Memory with complex metadata
18
+ - CORRECTION: Memory with complex metadata
19
+ - CORRECTION: Memory with complex metadata
20
+ - CORRECTION: Memory with complex metadata
21
+ - CORRECTION: Memory with complex metadata
22
+ - CORRECTION: Memory with complex metadata
23
+ - CORRECTION: Memory with complex metadata
24
+ - CORRECTION: Memory with complex metadata
15
25
  - CORRECTION: Memory with complex metadata
16
26
  - CORRECTION: Memory with complex metadata
17
27
  - CORRECTION: Memory with complex metadata
@@ -1,9 +1,19 @@
1
1
  {
2
2
  "topicId": "corrections",
3
- "sourceHash": "be94895a3ad6a5999508be2c149a1a2742178fff9f9adc97b5da6185c9a30398",
4
- "memoryCount": 20,
5
- "generatedAt": "2026-03-22T15:58:29.733Z",
3
+ "sourceHash": "e522ab0f108ce0a51b1dab005a2db51518edfdc431927275ba0c623a033d440e",
4
+ "memoryCount": 30,
5
+ "generatedAt": "2026-04-02T18:06:30.565Z",
6
6
  "memoryKeys": [
7
+ "memory_1775153190549_s9iwgl8lp",
8
+ "memory_1775152793092_g9wy80yom",
9
+ "memory_1775152560585_6yo07v2f4",
10
+ "memory_1775152548944_95j0vkg8u",
11
+ "memory_1775152517032_jvetpszkr",
12
+ "memory_1775152503075_3bncrxz1d",
13
+ "memory_1775152493186_ecq01e1gx",
14
+ "memory_1775152395533_qhhh4p5md",
15
+ "memory_1775152294113_jgwg4rw1q",
16
+ "memory_1775152031817_1vsksjun4",
7
17
  "memory_1774195109722_gij039r4m",
8
18
  "memory_1774191618092_vuxyvq3mw",
9
19
  "memory_1774106331272_kg5w7ztfj",
@@ -8,10 +8,54 @@ source: claude-recall
8
8
 
9
9
  # Preferences
10
10
 
11
- Auto-generated from 22 memories. Last updated: 2026-04-01.
11
+ Auto-generated from 66 memories. Last updated: 2026-04-02.
12
12
 
13
13
  ## Rules
14
14
 
15
+ - Session test preference 1775153190803
16
+ - Test preference 1775153190591-2
17
+ - Test preference 1775153190591-1
18
+ - Test preference 1775153190591-0
19
+ - Test memory content
20
+ - Session test preference 1775152793337
21
+ - Test preference 1775152793133-2
22
+ - Test preference 1775152793133-1
23
+ - Test preference 1775152793133-0
24
+ - Test memory content
25
+ - Session test preference 1775152560667
26
+ - Test preference 1775152560598-2
27
+ - Test preference 1775152560598-1
28
+ - Test preference 1775152560598-0
29
+ - Test memory content
30
+ - Session test preference 1775152549026
31
+ - Test preference 1775152548959-2
32
+ - Test preference 1775152548959-1
33
+ - Test preference 1775152548959-0
34
+ - Test memory content
35
+ - Test preference 1775152517049-2
36
+ - Test preference 1775152517049-1
37
+ - Test preference 1775152517049-0
38
+ - Test memory content
39
+ - Test preference 1775152503089-2
40
+ - Test preference 1775152503089-1
41
+ - Test preference 1775152503089-0
42
+ - Test memory content
43
+ - Test preference 1775152493202-2
44
+ - Test preference 1775152493202-1
45
+ - Test preference 1775152493202-0
46
+ - Test memory content
47
+ - Test preference 1775152395578-2
48
+ - Test preference 1775152395578-1
49
+ - Test preference 1775152395578-0
50
+ - Test memory content
51
+ - Test preference 1775152294130-2
52
+ - Test preference 1775152294130-1
53
+ - Test preference 1775152294130-0
54
+ - Test memory content
55
+ - Test preference 1775152031832-2
56
+ - Test preference 1775152031832-1
57
+ - Test preference 1775152031832-0
58
+ - Test memory content
15
59
  - axios npm package was compromised in a supply chain attack (axios@1.14.1 pulled in malicious plain-crypto-js@4.2.1). Claude Recall is NOT affected — does not use axios. Verified 2026-04-01. If axios is ever added as a dependency, pin the version and audit lockfiles.
16
60
  - Test preference 1774195109742-2
17
61
  - Test preference 1774195109742-1
@@ -1,9 +1,53 @@
1
1
  {
2
2
  "topicId": "preferences",
3
- "sourceHash": "ad9e64fb51202f15dbb49b534b20617f88a6a83e890c45c794b81578a7c1374d",
4
- "memoryCount": 22,
5
- "generatedAt": "2026-04-01T08:07:09.378Z",
3
+ "sourceHash": "72956f94499dd1ea22ae6348d86c5bb0199d3eec080cabc3960af1a57ea86dcb",
4
+ "memoryCount": 66,
5
+ "generatedAt": "2026-04-02T18:06:30.831Z",
6
6
  "memoryKeys": [
7
+ "memory_1775153190805_cvmwqk3fz",
8
+ "memory_1775153190715_23rgmq122",
9
+ "memory_1775153190682_arpcd1klk",
10
+ "memory_1775153190594_6rf5m5mo4",
11
+ "memory_1775153190422_owulv4b0l",
12
+ "memory_1775152793339_z2gjzo21u",
13
+ "memory_1775152793214_vz5vqxtqt",
14
+ "memory_1775152793177_m1zrfowu2",
15
+ "memory_1775152793135_0cxpxnzzt",
16
+ "memory_1775152792982_kvzpym0nj",
17
+ "memory_1775152560669_z96m74mo0",
18
+ "memory_1775152560632_g1m6exf5d",
19
+ "memory_1775152560615_bzsl0vevj",
20
+ "memory_1775152560599_j4nxggnvj",
21
+ "memory_1775152560543_2tlrz7uuw",
22
+ "memory_1775152549028_vdh5d8l1z",
23
+ "memory_1775152548991_s796whgbi",
24
+ "memory_1775152548974_8wu7oxnht",
25
+ "memory_1775152548960_6xn5qg1ed",
26
+ "memory_1775152548897_i892zk4er",
27
+ "memory_1775152517086_h1qtehxg5",
28
+ "memory_1775152517069_7iyt7gtd2",
29
+ "memory_1775152517050_jyndlwzm3",
30
+ "memory_1775152516970_9jsac5vef",
31
+ "memory_1775152503124_36fasrgv4",
32
+ "memory_1775152503106_rmjhhcaqy",
33
+ "memory_1775152503090_1vgaf4xvt",
34
+ "memory_1775152503030_czylzojcv",
35
+ "memory_1775152493236_kge25b20q",
36
+ "memory_1775152493220_1xpmtploo",
37
+ "memory_1775152493203_cck14tmil",
38
+ "memory_1775152493142_fdxvwri6p",
39
+ "memory_1775152395670_69if6leyw",
40
+ "memory_1775152395636_iatgy1qym",
41
+ "memory_1775152395579_00j66922k",
42
+ "memory_1775152395397_c28bq57a0",
43
+ "memory_1775152294164_6jc19cw4j",
44
+ "memory_1775152294147_0b50nxbf8",
45
+ "memory_1775152294131_isvbcko3z",
46
+ "memory_1775152294061_93dm7kgkv",
47
+ "memory_1775152031897_3owwqu9z2",
48
+ "memory_1775152031871_p5fdk67r0",
49
+ "memory_1775152031833_qtjc1ng2t",
50
+ "memory_1775152031740_5lnk9fdzg",
7
51
  "memory_1775030829333_64gdk8kql",
8
52
  "memory_1774195109780_8ffmflge1",
9
53
  "memory_1774195109763_v6olh83ct",
package/README.md CHANGED
@@ -14,7 +14,7 @@ Your preferences, project structure, workflows, corrections, and coding style ar
14
14
  - **Smart Memory Capture** — LLM-powered classification (via Claude Haiku) detects preferences and corrections from natural language, with silent regex fallback
15
15
  - **Project-Scoped Knowledge** — each project gets its own memory namespace; switch projects and Claude switches context automatically
16
16
  - **Failure Learning** — captures what failed, why, and what to do instead — so Claude doesn't repeat mistakes
17
- - **Outcome-Aware Learning** — tracks action outcomes (bash results, test cycles, user corrections), synthesizes candidate lessons, and promotes validated patterns into active rules automatically
17
+ - **Outcome-Aware Learning** — tracks action outcomes (all tool results, test cycles, user corrections), synthesizes candidate lessons, and promotes validated patterns into active rules automatically
18
18
  - **Skill Crystallization** — auto-generates `.claude/skills/auto-*/` files from accumulated memories, using Anthropic's [Agent Skills](https://agentskills.io/) open standard
19
19
  - **Local-Only** — SQLite on your machine, no telemetry, no cloud, works fully offline
20
20
 
@@ -93,10 +93,10 @@ Once installed, Claude Recall works automatically in the background:
93
93
  1. **First prompt** — the `search_enforcer` hook ensures Claude loads your stored rules before taking any action
94
94
  2. **As you work** — the `correction-detector` hook classifies every prompt you type. Natural statements like *"we use tabs here"* or *"no, put tests in `__tests__/`"* are detected and stored automatically
95
95
  3. **End of turn** — the `memory-stop` hook scans recent transcript entries for corrections, preferences, failures, and devops patterns. It also creates **episodes** summarizing the session outcome, generates **candidate lessons** from detected failures, and runs a **promotion cycle** to graduate validated patterns into active rules
96
- 4. **Bash failures** — the `bash-failure-watcher` hook captures command failures in real-time, pairs successful fixes, and writes **outcome events** for the learning pipeline
96
+ 4. **Tool outcomes** — the `tool-outcome-watcher` hook captures outcomes from all tools (Bash, Edit, Write, MCP tools) in real-time. Bash failures are paired with successful fixes. A separate `PostToolUseFailure` hook captures structured error details for any tool failure
97
97
  5. **Reask detection** — the `correction-detector` hook detects user frustration signals ("still broken", "that didn't work") and records them as outcome events
98
98
  6. **Before context compression** — the `precompact-preserve` hook sweeps up to 50 entries so nothing important is lost when the context window shrinks
99
- 7. **Rules sync to auto-memory** — the `memory-sync` hook exports active rules to `~/.claude/projects/{project}/memory/recall-rules.md` so they're available even when the MCP server is down
99
+ 7. **Rules sync to auto-memory** — the `memory-sync` hook exports the top 30 rules as individual typed `.md` files with YAML frontmatter to `~/.claude/projects/{project}/memory/`, matching Claude Code's native memory format. Rules are ranked by citation count, load frequency, and recency
100
100
 
101
101
  All classification uses Claude Haiku (via `ANTHROPIC_API_KEY` from your Claude Code session) with silent regex fallback. No configuration needed.
102
102
 
@@ -113,7 +113,7 @@ claude-recall search "preference"
113
113
 
114
114
  ## How It Works
115
115
 
116
- Claude Recall runs as an MCP server exposing four tools, backed by a local SQLite database with WAL mode, content-hash deduplication, and automatic compaction.
116
+ Claude Recall runs as an MCP server exposing four tools and seven prompts, backed by a local SQLite database with WAL mode, content-hash deduplication, and automatic compaction. The MCP prompts (including `load-rules` and `session-review`) are discoverable by Claude Code's skill system.
117
117
 
118
118
  ### Built on Agent Skills
119
119
 
@@ -134,7 +134,7 @@ Claude Recall tracks what happens *after* Claude acts — not just what was said
134
134
  action → outcome event → episode → candidate lesson → promotion → active rule
135
135
  ```
136
136
 
137
- - **Outcome events** capture bash results, test outcomes, user corrections, and reask signals
137
+ - **Outcome events** capture results from all tool types (Bash, Edit, Write, MCP), test outcomes, user corrections, and reask signals
138
138
  - **Episodes** summarize entire sessions with outcome type, severity, and confidence
139
139
  - **Candidate lessons** are extracted from failure patterns — deduplicated by Jaccard similarity
140
140
  - **Promotion engine** graduates lessons into active rules after 2+ observations (or immediately for high-severity failures), and demotes never-helpful memories
@@ -693,15 +693,25 @@ async function main() {
693
693
  // This avoids registry lookups on every hook invocation.
694
694
  const cliScript = path.join(packageDir, 'dist', 'cli', 'claude-recall-cli.js');
695
695
  const hookCmd = `node ${cliScript} hook run`;
696
- settings.hooksVersion = '8.0.0'; // v8 = add bash-failure-watcher PostToolUse hook
696
+ settings.hooksVersion = '10.0.0'; // v10 = add PostToolUseFailure for explicit error capture
697
697
  settings.hooks = {
698
698
  PostToolUse: [
699
699
  {
700
- matcher: "Bash",
701
700
  hooks: [
702
701
  {
703
702
  type: "command",
704
- command: `${hookCmd} bash-failure-watcher`,
703
+ command: `${hookCmd} tool-outcome-watcher`,
704
+ timeout: 3
705
+ }
706
+ ]
707
+ }
708
+ ],
709
+ PostToolUseFailure: [
710
+ {
711
+ hooks: [
712
+ {
713
+ type: "command",
714
+ command: `${hookCmd} tool-failure`,
705
715
  timeout: 3
706
716
  }
707
717
  ]
@@ -75,14 +75,25 @@ class HookCommands {
75
75
  await handleMemorySync(input);
76
76
  break;
77
77
  }
78
+ case 'tool-outcome-watcher': {
79
+ const { handleToolOutcomeWatcher } = await Promise.resolve().then(() => __importStar(require('../../hooks/tool-outcome-watcher')));
80
+ await handleToolOutcomeWatcher(input);
81
+ break;
82
+ }
83
+ case 'tool-failure': {
84
+ const { handleToolFailure } = await Promise.resolve().then(() => __importStar(require('../../hooks/tool-outcome-watcher')));
85
+ await handleToolFailure(input);
86
+ break;
87
+ }
78
88
  case 'bash-failure-watcher': {
79
- const { handleBashFailureWatcher } = await Promise.resolve().then(() => __importStar(require('../../hooks/bash-failure-watcher')));
89
+ // Backward compat alias routes to tool-outcome-watcher
90
+ const { handleBashFailureWatcher } = await Promise.resolve().then(() => __importStar(require('../../hooks/tool-outcome-watcher')));
80
91
  await handleBashFailureWatcher(input);
81
92
  break;
82
93
  }
83
94
  default:
84
95
  console.error(`Unknown hook: ${name}`);
85
- console.error('Available: correction-detector, memory-stop, precompact-preserve, memory-sync, bash-failure-watcher');
96
+ console.error('Available: correction-detector, memory-stop, precompact-preserve, memory-sync, tool-outcome-watcher');
86
97
  }
87
98
  }
88
99
  catch {
@@ -111,13 +111,16 @@ async function handleMemoryStop(input) {
111
111
  scanForCitations(transcriptPath);
112
112
  // Scan transcript for failure signals (non-zero exits, test cycles, backtracking, etc.)
113
113
  const failures = detectAndStoreFailures(transcriptPath, episodeId);
114
+ // Incorporate structured tool_failure events captured by PostToolUseFailure hook
115
+ const toolFailures = getToolFailureEvents(outcomeStorage);
116
+ const allFailures = [...failures, ...toolFailures];
114
117
  outcomeStorage.updateEpisode(episodeId, {
115
- outcome_type: failures.length > 0 ? 'failure' : 'success',
116
- severity: failures.length > 0 ? 'medium' : 'low',
117
- outcome_summary: `${stored} memories, ${failures.length} failures`,
118
+ outcome_type: allFailures.length > 0 ? 'failure' : 'success',
119
+ severity: allFailures.length > 0 ? 'medium' : 'low',
120
+ outcome_summary: `${stored} memories, ${allFailures.length} failures (${toolFailures.length} from tool events)`,
118
121
  });
119
122
  // Generate candidate lessons from high-confidence failures
120
- generateCandidateLessons(failures, episodeId, projectId);
123
+ generateCandidateLessons(allFailures, episodeId, projectId);
121
124
  // Run promotion cycle
122
125
  try {
123
126
  const { PromotionEngine } = await Promise.resolve().then(() => __importStar(require('../services/promotion-engine')));
@@ -312,6 +315,30 @@ function detectAndStoreFailures(transcriptPath, episodeId) {
312
315
  return [];
313
316
  }
314
317
  }
318
+ /**
319
+ * Convert structured tool_failure outcome events into DetectedFailure format
320
+ * so they feed into the candidate lessons pipeline.
321
+ */
322
+ function getToolFailureEvents(outcomeStorage) {
323
+ try {
324
+ const events = outcomeStorage.getEventsByType('tool_failure', 1); // last 1 hour
325
+ return events.slice(0, 5).map(e => ({
326
+ signal: 'tool_failure',
327
+ confidence: 0.8,
328
+ content: {
329
+ what_failed: e.action_summary || 'Tool failure',
330
+ why_failed: e.next_state_summary,
331
+ what_should_do: 'Check inputs and prerequisites before retrying',
332
+ context: `Captured by PostToolUseFailure hook`,
333
+ preventative_checks: ['Verify tool inputs are correct'],
334
+ },
335
+ }));
336
+ }
337
+ catch (err) {
338
+ (0, shared_1.hookLog)('memory-stop', `Tool failure events error: ${(0, shared_1.safeErrorMessage)(err)}`);
339
+ return [];
340
+ }
341
+ }
315
342
  /**
316
343
  * Generate candidate lessons from high-confidence failures.
317
344
  * Deduplicates against existing lessons and increments evidence count for similar ones.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * memory-sync-hook — fires on Stop and PreCompact events.
4
4
  *
5
- * Exports active rules from Claude Recall's SQLite database to a topic file
6
- * in Claude Code's auto-memory directory, so rules are available even when
7
- * the MCP server is down.
5
+ * Exports active rules from Claude Recall's SQLite database to individual
6
+ * typed .md files in Claude Code's auto-memory directory, using CC's native
7
+ * YAML frontmatter format so rules participate in CC's memory retrieval.
8
8
  *
9
9
  * Input: { session_id, cwd, hook_event_name }
10
10
  */
@@ -50,6 +50,10 @@ const os = __importStar(require("os"));
50
50
  const shared_1 = require("./shared");
51
51
  const memory_1 = require("../services/memory");
52
52
  const config_1 = require("../services/config");
53
+ /** Max number of recall files to write (leave room for CC's own memory files) */
54
+ const MAX_SYNC_FILES = 30;
55
+ /** Prefix for all recall memory files — prevents namespace collisions */
56
+ const FILE_PREFIX = 'recall_';
53
57
  /** Keys that look like test data */
54
58
  const TEST_KEY_PATTERNS = [/^Test /i, /^Session test /i, /^test_/i];
55
59
  /** Values that may contain secrets */
@@ -64,13 +68,15 @@ function deriveAutoMemoryPath(cwd, homedir) {
64
68
  return path.join(home, '.claude', 'projects', sanitized, 'memory');
65
69
  }
66
70
  /**
67
- * Extract display value from a memory record, matching memory-tools.ts formatting.
71
+ * Extract display value from a memory record.
68
72
  */
69
- function extractValue(m) {
70
- const val = typeof m.value === 'object'
71
- ? (m.value.content || m.value.value || JSON.stringify(m.value))
72
- : m.value;
73
- return String(val);
73
+ function extractValue(value) {
74
+ if (typeof value === 'string')
75
+ return value;
76
+ if (typeof value === 'object' && value !== null) {
77
+ return value.content || value.value || JSON.stringify(value);
78
+ }
79
+ return String(value ?? '');
74
80
  }
75
81
  /**
76
82
  * Check if a memory key matches test data patterns.
@@ -85,71 +91,130 @@ function containsSecret(value) {
85
91
  return SECRET_PATTERNS.some(p => p.test(value));
86
92
  }
87
93
  /**
88
- * Format rules into markdown sections.
94
+ * Sanitize a string for use as a filename slug.
89
95
  */
90
- function formatRulesMarkdown(rules) {
91
- const sections = [];
92
- sections.push('<!-- Auto-generated by Claude Recall — overwritten each session -->');
93
- sections.push('# Claude Recall Rules');
94
- if (rules.preferences.length > 0) {
95
- sections.push('\n## Preferences');
96
- for (const m of rules.preferences) {
97
- const val = extractValue(m);
98
- const key = m.preference_key || m.key || '';
99
- const isAutoKey = key.startsWith('memory_') || key.startsWith('auto_') || key.startsWith('pref_') || key.startsWith('hook_');
100
- sections.push(isAutoKey ? `- ${val}` : `- ${key}: ${val}`);
101
- }
102
- }
103
- if (rules.corrections.length > 0) {
104
- sections.push('\n## Corrections');
105
- for (const m of rules.corrections) {
106
- sections.push(`- ${extractValue(m)}`);
107
- }
108
- }
109
- if (rules.failures.length > 0) {
110
- sections.push('\n## Failures');
111
- for (const m of rules.failures) {
112
- sections.push(`- ${extractValue(m)}`);
113
- }
114
- }
115
- if (rules.devops.length > 0) {
116
- sections.push('\n## DevOps Rules');
117
- for (const m of rules.devops) {
118
- sections.push(`- ${extractValue(m)}`);
119
- }
96
+ function slugify(s) {
97
+ return s
98
+ .toLowerCase()
99
+ .replace(/[^a-z0-9]+/g, '-')
100
+ .replace(/^-+|-+$/g, '')
101
+ .substring(0, 50);
102
+ }
103
+ /**
104
+ * Generate a descriptive name from a rule's key and value.
105
+ */
106
+ function generateName(rule) {
107
+ const val = extractValue(rule.value);
108
+ // Use the key if it's human-readable (not auto-generated)
109
+ const isAutoKey = rule.key.startsWith('memory_') || rule.key.startsWith('auto_') ||
110
+ rule.key.startsWith('pref_') || rule.key.startsWith('hook_');
111
+ if (!isAutoKey && rule.key.length > 3 && rule.key.length < 60) {
112
+ return rule.key.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
120
113
  }
121
- sections.push(`\n---\n*Last synced: ${new Date().toISOString()}*`);
122
- return sections.join('\n') + '\n';
114
+ // Derive from value content
115
+ const firstSentence = val.split(/[.\n]/)[0].trim();
116
+ return firstSentence.length > 60 ? firstSentence.substring(0, 57) + '...' : firstSentence;
117
+ }
118
+ /**
119
+ * Generate a one-line description for CC's relevance selector.
120
+ */
121
+ function generateDescription(rule) {
122
+ const val = extractValue(rule.value);
123
+ const typeLabel = rule.crType === 'correction' ? 'Correction'
124
+ : rule.crType === 'failure' ? 'Failure lesson'
125
+ : rule.crType === 'preference' ? 'User preference'
126
+ : rule.crType === 'devops' ? 'DevOps convention'
127
+ : 'Project knowledge';
128
+ const snippet = val.length > 80 ? val.substring(0, 77) + '...' : val;
129
+ return `${typeLabel}: ${snippet}`;
123
130
  }
124
- const MEMORY_MD_POINTER = `\n## Claude Recall\n- See [recall-rules.md](recall-rules.md) for learned preferences, corrections, and failure lessons\n`;
125
131
  /**
126
- * Ensure MEMORY.md contains a pointer to recall-rules.md.
127
- * Appends the pointer only if absent. Never overwrites existing content.
132
+ * Generate a unique filename for a rule.
128
133
  */
129
- function ensureMemoryMdPointer(memoryDir) {
134
+ function generateFilename(rule, index) {
135
+ const slug = slugify(extractValue(rule.value).substring(0, 40)) || `rule-${index}`;
136
+ return `${FILE_PREFIX}${rule.ccType}_${slug}.md`;
137
+ }
138
+ /**
139
+ * Render a single memory file with CC-compatible YAML frontmatter.
140
+ */
141
+ function renderMemoryFile(rule) {
142
+ const name = generateName(rule);
143
+ const description = generateDescription(rule);
144
+ const val = extractValue(rule.value);
145
+ const lines = [
146
+ '---',
147
+ `name: ${name}`,
148
+ `description: ${description}`,
149
+ `type: ${rule.ccType}`,
150
+ '---',
151
+ '',
152
+ val,
153
+ '',
154
+ ];
155
+ return lines.join('\n');
156
+ }
157
+ /**
158
+ * Update MEMORY.md with pointers to recall files.
159
+ * Replaces any existing "## Claude Recall" section, preserves everything else.
160
+ */
161
+ function updateMemoryMdIndex(memoryDir, files) {
130
162
  const memoryMdPath = path.join(memoryDir, 'MEMORY.md');
131
163
  let existing = '';
132
164
  if (fs.existsSync(memoryMdPath)) {
133
165
  existing = fs.readFileSync(memoryMdPath, 'utf-8');
134
166
  }
135
- if (existing.includes('recall-rules.md')) {
136
- return; // Pointer already present
167
+ // Remove existing Claude Recall section (everything from ## Claude Recall to next ## or end)
168
+ const sectionRegex = /\n?## Claude Recall\n[\s\S]*?(?=\n## |\n*$)/;
169
+ const cleaned = existing.replace(sectionRegex, '').trimEnd();
170
+ // Build new section
171
+ const recallLines = ['', '## Claude Recall'];
172
+ if (files.length === 0) {
173
+ recallLines.push('- No recall rules synced');
137
174
  }
138
- fs.appendFileSync(memoryMdPath, MEMORY_MD_POINTER);
175
+ else {
176
+ for (const f of files) {
177
+ const hook = f.description.length > 80 ? f.description.substring(0, 77) + '...' : f.description;
178
+ recallLines.push(`- [${f.name}](${f.filename}) — ${hook}`);
179
+ }
180
+ }
181
+ recallLines.push('');
182
+ const newContent = cleaned + recallLines.join('\n');
183
+ fs.writeFileSync(memoryMdPath, newContent);
139
184
  }
140
185
  /**
141
- * Filter out test data and secrets from rules.
186
+ * Clean up stale recall_* files that are no longer in the current sync set.
142
187
  */
143
- function filterRules(memories) {
144
- return memories.filter(m => {
145
- const key = m.preference_key || m.key || '';
146
- if (isTestData(key))
147
- return false;
148
- const val = extractValue(m);
149
- if (containsSecret(val))
150
- return false;
151
- return true;
152
- });
188
+ function cleanupStaleFiles(memoryDir, currentFilenames) {
189
+ let removed = 0;
190
+ try {
191
+ const files = fs.readdirSync(memoryDir);
192
+ for (const f of files) {
193
+ if (f.startsWith(FILE_PREFIX) && f.endsWith('.md') && !currentFilenames.has(f)) {
194
+ fs.unlinkSync(path.join(memoryDir, f));
195
+ removed++;
196
+ }
197
+ }
198
+ }
199
+ catch {
200
+ // Ignore cleanup errors
201
+ }
202
+ return removed;
203
+ }
204
+ /**
205
+ * Remove old recall-rules.md if it exists (migration from v0.18.x).
206
+ */
207
+ function removeOldRulesFile(memoryDir) {
208
+ const oldPath = path.join(memoryDir, 'recall-rules.md');
209
+ try {
210
+ if (fs.existsSync(oldPath)) {
211
+ fs.unlinkSync(oldPath);
212
+ (0, shared_1.hookLog)('memory-sync', 'Removed old recall-rules.md');
213
+ }
214
+ }
215
+ catch {
216
+ // Ignore
217
+ }
153
218
  }
154
219
  async function handleMemorySync(input) {
155
220
  const cwd = input?.cwd ?? '';
@@ -160,28 +225,49 @@ async function handleMemorySync(input) {
160
225
  try {
161
226
  const projectId = config_1.ConfigService.getInstance().getProjectId();
162
227
  const memoryService = memory_1.MemoryService.getInstance();
163
- const rules = memoryService.loadActiveRules(projectId);
164
- // Filter each category
165
- const filtered = {
166
- preferences: filterRules(rules.preferences),
167
- corrections: filterRules(rules.corrections),
168
- failures: filterRules(rules.failures),
169
- devops: filterRules(rules.devops),
170
- };
171
- const totalRules = filtered.preferences.length + filtered.corrections.length +
172
- filtered.failures.length + filtered.devops.length;
228
+ // Get top rules ranked for sync
229
+ const rules = memoryService.getTopRulesForSync(projectId, MAX_SYNC_FILES);
230
+ // Filter out test data and secrets
231
+ const filtered = rules.filter(r => {
232
+ if (isTestData(r.key))
233
+ return false;
234
+ const val = extractValue(r.value);
235
+ if (containsSecret(val))
236
+ return false;
237
+ return true;
238
+ });
173
239
  // Derive auto-memory path and ensure directory exists
174
240
  const memoryDir = deriveAutoMemoryPath(cwd);
175
241
  if (!fs.existsSync(memoryDir)) {
176
242
  fs.mkdirSync(memoryDir, { recursive: true });
177
243
  }
178
- // Write recall-rules.md (full overwrite)
179
- const markdown = formatRulesMarkdown(filtered);
180
- const rulesPath = path.join(memoryDir, 'recall-rules.md');
181
- fs.writeFileSync(rulesPath, markdown);
182
- // Ensure MEMORY.md has pointer
183
- ensureMemoryMdPointer(memoryDir);
184
- (0, shared_1.hookLog)('memory-sync', `Synced ${totalRules} rules to ${rulesPath}`);
244
+ // Remove old flat rules file (v0.18.x migration)
245
+ removeOldRulesFile(memoryDir);
246
+ // Write individual files
247
+ const writtenFiles = [];
248
+ const currentFilenames = new Set();
249
+ for (let i = 0; i < filtered.length; i++) {
250
+ const rule = filtered[i];
251
+ const filename = generateFilename(rule, i);
252
+ const content = renderMemoryFile(rule);
253
+ // Deduplicate filenames (in case two rules produce the same slug)
254
+ let uniqueFilename = filename;
255
+ if (currentFilenames.has(uniqueFilename)) {
256
+ uniqueFilename = uniqueFilename.replace('.md', `-${i}.md`);
257
+ }
258
+ fs.writeFileSync(path.join(memoryDir, uniqueFilename), content);
259
+ currentFilenames.add(uniqueFilename);
260
+ writtenFiles.push({
261
+ filename: uniqueFilename,
262
+ name: generateName(rule),
263
+ description: generateDescription(rule),
264
+ });
265
+ }
266
+ // Clean up stale recall_* files from previous syncs
267
+ const removed = cleanupStaleFiles(memoryDir, currentFilenames);
268
+ // Update MEMORY.md index
269
+ updateMemoryMdIndex(memoryDir, writtenFiles);
270
+ (0, shared_1.hookLog)('memory-sync', `Synced ${writtenFiles.length} files to ${memoryDir} (removed ${removed} stale)`);
185
271
  }
186
272
  catch (error) {
187
273
  (0, shared_1.hookLog)('memory-sync', `Error: ${error.message}`);