claude-recall 0.18.8 → 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.
- package/.claude/skills/auto-corrections/SKILL.md +11 -1
- package/.claude/skills/auto-corrections/manifest.json +13 -3
- package/.claude/skills/auto-preferences/SKILL.md +46 -1
- package/.claude/skills/auto-preferences/manifest.json +48 -3
- package/README.md +11 -7
- package/dist/cli/claude-recall-cli.js +13 -3
- package/dist/cli/commands/hook-commands.js +13 -2
- package/dist/hooks/memory-stop-hook.js +31 -4
- package/dist/hooks/memory-sync-hook.js +163 -77
- package/dist/hooks/tool-outcome-watcher.js +430 -0
- package/dist/mcp/prompts-handler.js +126 -0
- package/dist/mcp/server.js +17 -1
- package/dist/mcp/tools/memory-tools.js +18 -4
- package/dist/services/memory.js +41 -0
- package/dist/services/outcome-storage.js +8 -0
- package/docs/20260323_equipping-agents-for-the-real-world-with-agent-skills.md +569 -0
- package/package.json +1 -1
|
@@ -8,10 +8,20 @@ source: claude-recall
|
|
|
8
8
|
|
|
9
9
|
# Corrections
|
|
10
10
|
|
|
11
|
-
Auto-generated from
|
|
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": "
|
|
4
|
-
"memoryCount":
|
|
5
|
-
"generatedAt": "2026-
|
|
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,55 @@ source: claude-recall
|
|
|
8
8
|
|
|
9
9
|
# Preferences
|
|
10
10
|
|
|
11
|
-
Auto-generated from
|
|
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
|
|
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.
|
|
15
60
|
- Test preference 1774195109742-2
|
|
16
61
|
- Test preference 1774195109742-1
|
|
17
62
|
- Test preference 1774195109742-0
|
|
@@ -1,9 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"topicId": "preferences",
|
|
3
|
-
"sourceHash": "
|
|
4
|
-
"memoryCount":
|
|
5
|
-
"generatedAt": "2026-
|
|
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",
|
|
51
|
+
"memory_1775030829333_64gdk8kql",
|
|
7
52
|
"memory_1774195109780_8ffmflge1",
|
|
8
53
|
"memory_1774195109763_v6olh83ct",
|
|
9
54
|
"memory_1774195109743_i79j9a9rl",
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Claude Recall
|
|
2
2
|
|
|
3
|
-
### Persistent, local memory for Claude Code —
|
|
3
|
+
### Persistent, local memory for Claude Code — learn from every session.
|
|
4
4
|
|
|
5
5
|
Claude Recall is a **local memory engine + MCP server** that gives Claude Code something it's missing by default:
|
|
6
6
|
**the ability to learn from you over time.**
|
|
@@ -14,8 +14,8 @@ 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 (
|
|
18
|
-
- **Skill Crystallization** —
|
|
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
|
+
- **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
|
|
|
21
21
|
---
|
|
@@ -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. **
|
|
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
|
|
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,11 @@ 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
|
+
|
|
118
|
+
### Built on Agent Skills
|
|
119
|
+
|
|
120
|
+
Claude Recall uses Anthropic's [Agent Skills](https://agentskills.io/) open standard to teach Claude when and how to use its memory tools. A core skill (`.claude/skills/memory-management/SKILL.md`) guides Claude's memory behavior using progressive disclosure — metadata loads at startup, full instructions load only when needed. When enough memories accumulate around a topic, Claude Recall auto-generates additional skills (`.claude/skills/auto-*/`) that load natively without MCP tool calls. See Anthropic's [blog post](https://claude.com/blog/equipping-agents-for-the-real-world-with-agent-skills) for more on the Agent Skills architecture.
|
|
117
121
|
|
|
118
122
|
| Tool | Purpose |
|
|
119
123
|
| ---- | ------- |
|
|
@@ -130,7 +134,7 @@ Claude Recall tracks what happens *after* Claude acts — not just what was said
|
|
|
130
134
|
action → outcome event → episode → candidate lesson → promotion → active rule
|
|
131
135
|
```
|
|
132
136
|
|
|
133
|
-
- **Outcome events** capture
|
|
137
|
+
- **Outcome events** capture results from all tool types (Bash, Edit, Write, MCP), test outcomes, user corrections, and reask signals
|
|
134
138
|
- **Episodes** summarize entire sessions with outcome type, severity, and confidence
|
|
135
139
|
- **Candidate lessons** are extracted from failure patterns — deduplicated by Jaccard similarity
|
|
136
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 = '
|
|
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}
|
|
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
|
-
|
|
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,
|
|
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:
|
|
116
|
-
severity:
|
|
117
|
-
outcome_summary: `${stored} memories, ${failures.length}
|
|
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(
|
|
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
|
|
6
|
-
* in Claude Code's auto-memory directory,
|
|
7
|
-
*
|
|
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
|
|
71
|
+
* Extract display value from a memory record.
|
|
68
72
|
*/
|
|
69
|
-
function extractValue(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
*
|
|
94
|
+
* Sanitize a string for use as a filename slug.
|
|
89
95
|
*/
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
*
|
|
127
|
-
* Appends the pointer only if absent. Never overwrites existing content.
|
|
132
|
+
* Generate a unique filename for a rule.
|
|
128
133
|
*/
|
|
129
|
-
function
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
186
|
+
* Clean up stale recall_* files that are no longer in the current sync set.
|
|
142
187
|
*/
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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}`);
|