claude-recall 0.9.2 → 0.10.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/hooks/search_enforcer.py +153 -0
- package/.claude/settings.json +0 -0
- package/.claude/skills/memory-management/SKILL.md +56 -22
- package/README.md +0 -0
- package/dist/cli/claude-recall-cli.js +48 -46
- package/dist/mcp/tools/memory-tools.js +94 -4
- package/dist/memory/database-adapter.js +256 -0
- package/dist/memory/schema.sql +4 -2
- package/dist/memory/storage.js +55 -4
- package/dist/services/config.js +5 -1
- package/dist/services/database-manager.js +26 -5
- package/dist/services/embedding-service.js +183 -0
- package/dist/services/memory.js +41 -0
- package/docs/claude-flow-analysis.md +50 -0
- package/docs/cli.md +207 -36
- package/docs/content-hashing.md +46 -0
- package/docs/faq.md +0 -0
- package/docs/hooks.md +80 -50
- package/docs/installation.md +0 -0
- package/docs/learning-loop.md +0 -0
- package/docs/memory-types.md +0 -0
- package/docs/project-scoping.md +0 -0
- package/docs/quickstart.md +0 -0
- package/docs/security.md +0 -0
- package/docs/troubleshooting.md +0 -0
- package/package.json +1 -3
- package/scripts/postinstall.js +45 -37
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Minimal Search Enforcer for Claude Recall v0.9.3+
|
|
4
|
+
|
|
5
|
+
Ensures memory search is performed before Write/Edit/Bash/Task operations.
|
|
6
|
+
Works alongside native Claude Skills - skill teaches, hook enforces.
|
|
7
|
+
|
|
8
|
+
State file: ~/.claude-recall/hook-state/{session_id}.json
|
|
9
|
+
Exit codes: 0 = allow, 2 = block
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
STATE_DIR = Path.home() / '.claude-recall' / 'hook-state'
|
|
18
|
+
SEARCH_TTL_MS = int(os.environ.get('CLAUDE_RECALL_SEARCH_TTL', 5 * 60 * 1000)) # 5 min default
|
|
19
|
+
ENFORCE_MODE = os.environ.get('CLAUDE_RECALL_ENFORCE_MODE', 'block') # block, warn, off
|
|
20
|
+
|
|
21
|
+
# Tools that count as "search performed"
|
|
22
|
+
SEARCH_TOOLS = [
|
|
23
|
+
'mcp__claude-recall__load_rules',
|
|
24
|
+
'mcp__claude-recall__mcp__claude-recall__load_rules',
|
|
25
|
+
'mcp__claude-recall__search',
|
|
26
|
+
'mcp__claude-recall__mcp__claude-recall__search',
|
|
27
|
+
'mcp__claude-recall__retrieve_memory',
|
|
28
|
+
'mcp__claude-recall__mcp__claude-recall__retrieve_memory',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Tools that require search first
|
|
32
|
+
ENFORCE_TOOLS = ['Write', 'Edit', 'Bash', 'Task']
|
|
33
|
+
|
|
34
|
+
# Read-only bash commands that don't need memory search
|
|
35
|
+
READ_ONLY_BASH = [
|
|
36
|
+
'ls', 'cat', 'head', 'tail', 'less', 'more', 'file', 'stat', 'wc',
|
|
37
|
+
'find', 'locate', 'which', 'whereis', 'type', 'pwd', 'whoami',
|
|
38
|
+
'git status', 'git log', 'git diff', 'git show', 'git branch',
|
|
39
|
+
'git remote', 'git fetch', 'git stash list', 'git tag',
|
|
40
|
+
'npm list', 'npm ls', 'npm view', 'npm outdated', 'npm audit',
|
|
41
|
+
'npm test', 'npm run test', 'npm run build', 'npm run lint',
|
|
42
|
+
'pip list', 'pip show', 'pip freeze',
|
|
43
|
+
'pytest', 'jest', 'cargo test', 'go test', 'tsc --noEmit',
|
|
44
|
+
'grep', 'rg', 'ag', 'awk', 'sed', 'sort', 'uniq', 'diff',
|
|
45
|
+
'tree', 'realpath', 'dirname', 'basename', 'date', 'env', 'echo',
|
|
46
|
+
'ps', 'top', 'df', 'du', 'free', 'uptime', 'hostname',
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_state_file(session_id: str) -> Path:
|
|
51
|
+
safe_id = "".join(c if c.isalnum() or c in '-_' else '_' for c in session_id) or 'default'
|
|
52
|
+
return STATE_DIR / f'{safe_id}.json'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_state(session_id: str) -> dict:
|
|
56
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
state_file = get_state_file(session_id)
|
|
58
|
+
if state_file.exists():
|
|
59
|
+
try:
|
|
60
|
+
return json.load(open(state_file))
|
|
61
|
+
except:
|
|
62
|
+
pass
|
|
63
|
+
return {'lastSearchAt': None, 'searchQuery': None}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def save_state(session_id: str, state: dict):
|
|
67
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
try:
|
|
69
|
+
json.dump(state, open(get_state_file(session_id), 'w'), indent=2)
|
|
70
|
+
except:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def is_search_tool(tool_name: str) -> bool:
|
|
75
|
+
return any(s in tool_name for s in SEARCH_TOOLS)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_read_only_bash(command: str) -> bool:
|
|
79
|
+
if not command:
|
|
80
|
+
return False
|
|
81
|
+
cmd = command.strip().lower()
|
|
82
|
+
# Check direct match or pipe starting with read-only
|
|
83
|
+
for ro in READ_ONLY_BASH:
|
|
84
|
+
if cmd.startswith(ro):
|
|
85
|
+
return True
|
|
86
|
+
if '|' in cmd:
|
|
87
|
+
first = cmd.split('|')[0].strip()
|
|
88
|
+
for ro in READ_ONLY_BASH:
|
|
89
|
+
if first.startswith(ro):
|
|
90
|
+
return True
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def main():
|
|
95
|
+
if ENFORCE_MODE == 'off':
|
|
96
|
+
sys.exit(0)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
data = json.load(sys.stdin)
|
|
100
|
+
except:
|
|
101
|
+
sys.exit(0)
|
|
102
|
+
|
|
103
|
+
tool_name = data.get('tool_name', '')
|
|
104
|
+
tool_input = data.get('tool_input', {})
|
|
105
|
+
session_id = data.get('session_id', '') or 'default'
|
|
106
|
+
|
|
107
|
+
# Track search calls
|
|
108
|
+
if is_search_tool(tool_name):
|
|
109
|
+
state = load_state(session_id)
|
|
110
|
+
state['lastSearchAt'] = int(datetime.now().timestamp() * 1000)
|
|
111
|
+
state['searchQuery'] = tool_input.get('query', '')
|
|
112
|
+
save_state(session_id, state)
|
|
113
|
+
sys.exit(0)
|
|
114
|
+
|
|
115
|
+
# Only enforce on specific tools
|
|
116
|
+
if tool_name not in ENFORCE_TOOLS:
|
|
117
|
+
sys.exit(0)
|
|
118
|
+
|
|
119
|
+
# Skip read-only bash
|
|
120
|
+
if tool_name == 'Bash' and is_read_only_bash(tool_input.get('command', '')):
|
|
121
|
+
sys.exit(0)
|
|
122
|
+
|
|
123
|
+
# Check if search was recent
|
|
124
|
+
state = load_state(session_id)
|
|
125
|
+
last_search = state.get('lastSearchAt')
|
|
126
|
+
|
|
127
|
+
if last_search:
|
|
128
|
+
now = int(datetime.now().timestamp() * 1000)
|
|
129
|
+
if (now - last_search) <= SEARCH_TTL_MS:
|
|
130
|
+
sys.exit(0)
|
|
131
|
+
|
|
132
|
+
# Block or warn
|
|
133
|
+
msg = f"""
|
|
134
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
135
|
+
🔍 LOAD RULES REQUIRED before {tool_name}
|
|
136
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
137
|
+
|
|
138
|
+
Run: mcp__claude-recall__load_rules({{}})
|
|
139
|
+
|
|
140
|
+
Or for a specific lookup:
|
|
141
|
+
mcp__claude-recall__search({{"query": "relevant keywords"}})
|
|
142
|
+
|
|
143
|
+
This ensures you apply user preferences and avoid past mistakes.
|
|
144
|
+
|
|
145
|
+
To disable: CLAUDE_RECALL_ENFORCE_MODE=off
|
|
146
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
147
|
+
"""
|
|
148
|
+
print(msg.strip(), file=sys.stderr)
|
|
149
|
+
sys.exit(0 if ENFORCE_MODE == 'warn' else 2)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == '__main__':
|
|
153
|
+
main()
|
package/.claude/settings.json
CHANGED
|
File without changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memory-management
|
|
3
3
|
description: Persistent memory for Claude across conversations. Use when starting any task, before writing or editing code, before making decisions, when user mentions preferences or conventions, when user corrects your work, or when completing a task that overcame challenges. Ensures Claude never repeats mistakes and always applies learned patterns.
|
|
4
|
-
version: "1.
|
|
4
|
+
version: "1.1.0"
|
|
5
5
|
license: "MIT"
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -11,9 +11,7 @@ Persistent memory system that ensures Claude never repeats mistakes and always a
|
|
|
11
11
|
|
|
12
12
|
## When to Use This Skill
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- **Starting any task** - Check for existing patterns, preferences, past failures
|
|
14
|
+
- **Starting any task** - Load rules for preferences, corrections, past failures
|
|
17
15
|
- **Before writing/editing code** - Apply learned conventions and avoid past mistakes
|
|
18
16
|
- **Before making architectural decisions** - Check for established patterns
|
|
19
17
|
- **When user mentions preferences** - Store for future sessions
|
|
@@ -22,25 +20,30 @@ Invoke memory search in these situations:
|
|
|
22
20
|
|
|
23
21
|
## Key Directives
|
|
24
22
|
|
|
25
|
-
1. **ALWAYS
|
|
26
|
-
2. **
|
|
27
|
-
3. **
|
|
28
|
-
4. **
|
|
29
|
-
5. **
|
|
23
|
+
1. **ALWAYS load rules before acting** - Call `mcp__claude-recall__load_rules` before Write, Edit, or significant Bash operations
|
|
24
|
+
2. **Use `search` for specific lookups** - Mid-task targeted queries ("what did we decide about auth?")
|
|
25
|
+
3. **Apply what you find** - Use retrieved preferences, patterns, and corrections
|
|
26
|
+
4. **Capture corrections immediately** - User fixes are highest priority
|
|
27
|
+
5. **Store learning cycles** - When you fail then succeed, that's valuable knowledge
|
|
28
|
+
6. **Never store secrets** - No API keys, passwords, tokens, or PII
|
|
30
29
|
|
|
31
30
|
## Quick Reference
|
|
32
31
|
|
|
33
|
-
###
|
|
32
|
+
### Load Rules (Before Every Task)
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
mcp__claude-recall__load_rules({})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Returns all active preferences, recent corrections, recent failures, and devops rules in one call. No query needed — deterministic and complete.
|
|
39
|
+
|
|
40
|
+
### Search (For Specific Lookups)
|
|
34
41
|
|
|
35
42
|
```
|
|
36
|
-
mcp__claude-recall__search({ "query": "
|
|
43
|
+
mcp__claude-recall__search({ "query": "authentication jwt session" })
|
|
37
44
|
```
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
- Before tests: `"testing tdd framework location"`
|
|
41
|
-
- Before git: `"git commit branch workflow"`
|
|
42
|
-
- Before deploy: `"deploy docker build ci/cd"`
|
|
43
|
-
- Before coding: `"[language] style conventions preferences"`
|
|
46
|
+
Use when you need targeted information mid-task, not for task-start enforcement.
|
|
44
47
|
|
|
45
48
|
### Store (When Something Important Happens)
|
|
46
49
|
|
|
@@ -107,16 +110,33 @@ Safe to store:
|
|
|
107
110
|
```
|
|
108
111
|
1. User: "Add user authentication"
|
|
109
112
|
|
|
110
|
-
2.
|
|
111
|
-
mcp__claude-
|
|
113
|
+
2. Load rules first:
|
|
114
|
+
mcp__claude-recall__load_rules({})
|
|
112
115
|
|
|
113
|
-
3.
|
|
116
|
+
3. Response includes:
|
|
117
|
+
## Preferences
|
|
118
|
+
- auth_method: JWT with httpOnly cookies
|
|
119
|
+
## Corrections
|
|
120
|
+
- Never use localStorage for auth tokens
|
|
114
121
|
|
|
115
|
-
4. Implement using JWT + httpOnly cookies (not sessions)
|
|
122
|
+
4. Implement using JWT + httpOnly cookies (not sessions, not localStorage)
|
|
116
123
|
|
|
117
124
|
5. User approves → Done (no need to store, just applied existing knowledge)
|
|
118
125
|
```
|
|
119
126
|
|
|
127
|
+
### Mid-Task Specific Lookup
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
1. Working on auth, need to check a specific decision
|
|
131
|
+
|
|
132
|
+
2. Search for it:
|
|
133
|
+
mcp__claude-recall__search({ "query": "oauth provider google azure" })
|
|
134
|
+
|
|
135
|
+
3. Found: "We use Google OAuth, not Azure AD"
|
|
136
|
+
|
|
137
|
+
4. Continue implementation with Google OAuth
|
|
138
|
+
```
|
|
139
|
+
|
|
120
140
|
### User Corrects Your Work
|
|
121
141
|
|
|
122
142
|
```
|
|
@@ -150,15 +170,29 @@ Safe to store:
|
|
|
150
170
|
})
|
|
151
171
|
```
|
|
152
172
|
|
|
173
|
+
## Inline Citations
|
|
174
|
+
|
|
175
|
+
When `load_rules`, `search`, or `retrieve_memory` returns memories, the response includes a `_citationDirective` instructing you to cite any memory you actually apply. When you use a retrieved memory in your work, add a brief inline note:
|
|
176
|
+
|
|
177
|
+
> (applied from memory: always use httpOnly cookies for auth tokens)
|
|
178
|
+
|
|
179
|
+
This gives the user visibility into which stored knowledge is influencing your decisions. Only cite memories you actually use — don't cite every memory returned.
|
|
180
|
+
|
|
181
|
+
To disable citations, set the environment variable `CLAUDE_RECALL_CITE_MEMORIES=false`.
|
|
182
|
+
|
|
153
183
|
## Troubleshooting
|
|
154
184
|
|
|
185
|
+
**Load rules returns nothing:**
|
|
186
|
+
- This may be a new project with no history yet
|
|
187
|
+
- Try `search` with broad keywords to check
|
|
188
|
+
|
|
155
189
|
**Search returns nothing relevant:**
|
|
156
190
|
- Broaden keywords: include domain + task + "preferences patterns"
|
|
157
191
|
- This may be a new project with no history yet
|
|
158
192
|
|
|
159
193
|
**Automatic capture missed something:**
|
|
160
194
|
- Store it manually with appropriate type
|
|
161
|
-
- Future searches will find it
|
|
195
|
+
- Future loads/searches will find it
|
|
162
196
|
|
|
163
197
|
**Check what's been captured:**
|
|
164
198
|
```
|
|
@@ -167,4 +201,4 @@ mcp__claude-recall__get_recent_captures({ "limit": 10 })
|
|
|
167
201
|
|
|
168
202
|
---
|
|
169
203
|
|
|
170
|
-
**The Learning Loop**:
|
|
204
|
+
**The Learning Loop**: Load rules → Apply → Execute → Capture outcomes → Better next time
|
package/README.md
CHANGED
|
File without changes
|
|
@@ -49,7 +49,6 @@ const queue_integration_1 = require("../services/queue-integration");
|
|
|
49
49
|
const memory_evolution_1 = require("../services/memory-evolution");
|
|
50
50
|
const mcp_commands_1 = require("./commands/mcp-commands");
|
|
51
51
|
const project_commands_1 = require("./commands/project-commands");
|
|
52
|
-
const hook_commands_1 = require("./commands/hook-commands");
|
|
53
52
|
const program = new commander_1.Command();
|
|
54
53
|
class ClaudeRecallCLI {
|
|
55
54
|
constructor(options) {
|
|
@@ -565,24 +564,25 @@ async function main() {
|
|
|
565
564
|
}
|
|
566
565
|
}
|
|
567
566
|
}
|
|
568
|
-
// Install skills
|
|
569
|
-
function
|
|
567
|
+
// Install skills + minimal enforcement hook (v0.9.3+ hybrid approach)
|
|
568
|
+
function installSkillsAndHook(force = false) {
|
|
570
569
|
const cwd = process.cwd();
|
|
571
570
|
const projectName = path.basename(cwd);
|
|
572
|
-
console.log('\n📦 Claude Recall v0.9.
|
|
571
|
+
console.log('\n📦 Claude Recall v0.9.3+ Setup\n');
|
|
573
572
|
console.log(`📍 Project: ${projectName}`);
|
|
574
573
|
console.log(`📍 Directory: ${cwd}\n`);
|
|
575
574
|
// Find the package directory (where claude-recall is installed)
|
|
576
575
|
const packageDir = path.resolve(__dirname, '../..');
|
|
577
576
|
const packageSkillsDir = path.join(packageDir, '.claude/skills');
|
|
577
|
+
const packageHooksDir = path.join(packageDir, '.claude/hooks');
|
|
578
578
|
const claudeDir = path.join(cwd, '.claude');
|
|
579
579
|
const hooksDir = path.join(claudeDir, 'hooks');
|
|
580
580
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
581
|
-
// === CLEANUP: Remove
|
|
581
|
+
// === CLEANUP: Remove OLD hooks (not the new search_enforcer.py) ===
|
|
582
582
|
if (fs.existsSync(hooksDir)) {
|
|
583
583
|
const oldHooks = [
|
|
584
|
-
'memory_enforcer.py',
|
|
585
|
-
'
|
|
584
|
+
'memory_enforcer.py', // Old v0.8.x hook
|
|
585
|
+
'search_enforcer.py',
|
|
586
586
|
'mcp_tool_tracker.py',
|
|
587
587
|
'pubnub_pre_tool_hook.py',
|
|
588
588
|
'pubnub_prompt_hook.py',
|
|
@@ -597,49 +597,53 @@ async function main() {
|
|
|
597
597
|
removedCount++;
|
|
598
598
|
}
|
|
599
599
|
}
|
|
600
|
-
// Remove hooks directory if empty
|
|
601
|
-
try {
|
|
602
|
-
const remaining = fs.readdirSync(hooksDir);
|
|
603
|
-
if (remaining.length === 0) {
|
|
604
|
-
fs.rmdirSync(hooksDir);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
catch (e) {
|
|
608
|
-
// Ignore
|
|
609
|
-
}
|
|
610
600
|
if (removedCount > 0) {
|
|
611
|
-
console.log(`🧹 Removed ${removedCount} old hook file(s)
|
|
601
|
+
console.log(`🧹 Removed ${removedCount} old hook file(s)`);
|
|
612
602
|
}
|
|
613
603
|
}
|
|
614
|
-
// ===
|
|
604
|
+
// === INSTALL: New minimal search_enforcer.py ===
|
|
605
|
+
if (!fs.existsSync(hooksDir)) {
|
|
606
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
607
|
+
}
|
|
608
|
+
const hookSource = path.join(packageHooksDir, 'search_enforcer.py');
|
|
609
|
+
const hookDest = path.join(hooksDir, 'search_enforcer.py');
|
|
610
|
+
if (fs.existsSync(hookSource)) {
|
|
611
|
+
fs.copyFileSync(hookSource, hookDest);
|
|
612
|
+
fs.chmodSync(hookDest, 0o755);
|
|
613
|
+
console.log('✅ Installed search_enforcer.py to .claude/hooks/');
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
console.log(`⚠️ Hook not found at: ${hookSource}`);
|
|
617
|
+
}
|
|
618
|
+
// === CONFIGURE: Update settings.json with new hook ===
|
|
615
619
|
let settings = {};
|
|
616
620
|
if (fs.existsSync(settingsPath)) {
|
|
617
621
|
try {
|
|
618
622
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
619
|
-
const hadHooks = settings.hooks && Object.keys(settings.hooks).length > 0;
|
|
620
|
-
// Clear hooks - v0.9.0+ uses Skills instead
|
|
621
|
-
settings.hooks = {};
|
|
622
|
-
settings.hooksVersion = '2.0.0';
|
|
623
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
624
|
-
if (hadHooks) {
|
|
625
|
-
console.log('🧹 Cleared old hook configuration from .claude/settings.json');
|
|
626
|
-
}
|
|
627
623
|
}
|
|
628
624
|
catch (e) {
|
|
629
|
-
|
|
630
|
-
if (!fs.existsSync(claudeDir)) {
|
|
631
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
632
|
-
}
|
|
633
|
-
fs.writeFileSync(settingsPath, JSON.stringify({ hooks: {}, hooksVersion: '2.0.0' }, null, 2));
|
|
625
|
+
settings = {};
|
|
634
626
|
}
|
|
635
627
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
628
|
+
settings.hooksVersion = '3.0.0'; // v3 = hybrid (skill + minimal hook)
|
|
629
|
+
settings.hooks = {
|
|
630
|
+
PreToolUse: [
|
|
631
|
+
{
|
|
632
|
+
matcher: "mcp__claude-recall__.*|Write|Edit|Bash|Task",
|
|
633
|
+
hooks: [
|
|
634
|
+
{
|
|
635
|
+
type: "command",
|
|
636
|
+
command: `python3 ${hookDest}`
|
|
637
|
+
}
|
|
638
|
+
]
|
|
639
|
+
}
|
|
640
|
+
]
|
|
641
|
+
};
|
|
642
|
+
if (!fs.existsSync(claudeDir)) {
|
|
643
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
642
644
|
}
|
|
645
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
646
|
+
console.log('✅ Configured search enforcement hook');
|
|
643
647
|
// === INSTALL: Copy skills directory ===
|
|
644
648
|
if (fs.existsSync(packageSkillsDir)) {
|
|
645
649
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
@@ -650,7 +654,7 @@ async function main() {
|
|
|
650
654
|
console.log(`⚠️ Skills not found at: ${packageSkillsDir}`);
|
|
651
655
|
}
|
|
652
656
|
console.log('\n✅ Setup complete!\n');
|
|
653
|
-
console.log('ℹ️ v0.9.
|
|
657
|
+
console.log('ℹ️ v0.9.3+ uses Skills (guidance) + minimal hook (enforcement).');
|
|
654
658
|
console.log('Restart Claude Code to activate.\n');
|
|
655
659
|
}
|
|
656
660
|
// Setup command - shows activation instructions or installs skills
|
|
@@ -660,8 +664,8 @@ async function main() {
|
|
|
660
664
|
.option('--install', 'Install skills and clean up old hooks')
|
|
661
665
|
.action((options) => {
|
|
662
666
|
if (options.install) {
|
|
663
|
-
// Install skills and
|
|
664
|
-
|
|
667
|
+
// Install skills and enforcement hook
|
|
668
|
+
installSkillsAndHook();
|
|
665
669
|
}
|
|
666
670
|
else {
|
|
667
671
|
// Show activation instructions
|
|
@@ -692,7 +696,7 @@ async function main() {
|
|
|
692
696
|
.description('Clean up old hooks and install skills (v0.9.0+ migration)')
|
|
693
697
|
.option('--force', 'Force overwrite existing configuration')
|
|
694
698
|
.action((options) => {
|
|
695
|
-
|
|
699
|
+
installSkillsAndHook(options.force || false);
|
|
696
700
|
process.exit(0);
|
|
697
701
|
});
|
|
698
702
|
// Check hooks function
|
|
@@ -792,7 +796,7 @@ async function main() {
|
|
|
792
796
|
let searchDir = process.cwd();
|
|
793
797
|
let enforcerPath = null;
|
|
794
798
|
while (searchDir !== path.dirname(searchDir)) {
|
|
795
|
-
const candidate = path.join(searchDir, '.claude/hooks/
|
|
799
|
+
const candidate = path.join(searchDir, '.claude/hooks/search_enforcer.py');
|
|
796
800
|
if (fs.existsSync(candidate)) {
|
|
797
801
|
enforcerPath = candidate;
|
|
798
802
|
break;
|
|
@@ -800,7 +804,7 @@ async function main() {
|
|
|
800
804
|
searchDir = path.dirname(searchDir);
|
|
801
805
|
}
|
|
802
806
|
if (!enforcerPath) {
|
|
803
|
-
console.log('❌ Could not find
|
|
807
|
+
console.log('❌ Could not find search_enforcer.py');
|
|
804
808
|
console.log(' Run: npx claude-recall repair\n');
|
|
805
809
|
return;
|
|
806
810
|
}
|
|
@@ -976,8 +980,6 @@ async function main() {
|
|
|
976
980
|
project_commands_1.ProjectCommands.register(program);
|
|
977
981
|
// Migration commands
|
|
978
982
|
migrate_1.MigrateCommand.register(program);
|
|
979
|
-
// Hook commands (legacy, kept for backwards compatibility)
|
|
980
|
-
hook_commands_1.HookCommands.register(program);
|
|
981
983
|
// Register live test command
|
|
982
984
|
new live_test_1.LiveTestCommand().register(program);
|
|
983
985
|
// Search command
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MemoryTools = void 0;
|
|
4
|
+
const config_1 = require("../../services/config");
|
|
4
5
|
const search_monitor_1 = require("../../services/search-monitor");
|
|
5
6
|
class MemoryTools {
|
|
6
7
|
constructor(memoryService, logger) {
|
|
@@ -10,6 +11,12 @@ class MemoryTools {
|
|
|
10
11
|
this.searchMonitor = search_monitor_1.SearchMonitor.getInstance();
|
|
11
12
|
this.registerTools();
|
|
12
13
|
}
|
|
14
|
+
getCitationDirective() {
|
|
15
|
+
const config = config_1.ConfigService.getInstance();
|
|
16
|
+
if (config.getConfig().citations?.enabled === false)
|
|
17
|
+
return undefined;
|
|
18
|
+
return MemoryTools.CITATION_DIRECTIVE;
|
|
19
|
+
}
|
|
13
20
|
// Claude-flow pattern: Validate input against schema
|
|
14
21
|
validateInput(schema, input) {
|
|
15
22
|
if (schema.required) {
|
|
@@ -227,6 +234,20 @@ class MemoryTools {
|
|
|
227
234
|
}
|
|
228
235
|
},
|
|
229
236
|
handler: this.handleGetRecentCaptures.bind(this)
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'mcp__claude-recall__load_rules',
|
|
240
|
+
description: 'Load all active rules (preferences, corrections, failures, devops) before starting work. No query needed. Use this instead of search at the start of every task.',
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: {
|
|
244
|
+
projectId: {
|
|
245
|
+
type: 'string',
|
|
246
|
+
description: 'Optional project ID override. Defaults to current project.'
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
handler: this.handleLoadRules.bind(this)
|
|
230
251
|
}
|
|
231
252
|
];
|
|
232
253
|
}
|
|
@@ -312,6 +333,7 @@ class MemoryTools {
|
|
|
312
333
|
async handleRetrieveMemory(input, context) {
|
|
313
334
|
try {
|
|
314
335
|
let { query, id, limit = 10, sortBy } = input;
|
|
336
|
+
const citationDirective = this.getCitationDirective();
|
|
315
337
|
// Smart detection: Auto-detect timestamp sorting from query keywords
|
|
316
338
|
if (!sortBy && query) {
|
|
317
339
|
const timeKeywords = ['recent', 'latest', 'newest', 'last', 'new'];
|
|
@@ -337,7 +359,8 @@ class MemoryTools {
|
|
|
337
359
|
}
|
|
338
360
|
return {
|
|
339
361
|
memories: [memory],
|
|
340
|
-
count: 1
|
|
362
|
+
count: 1,
|
|
363
|
+
...(citationDirective && { _citationDirective: citationDirective })
|
|
341
364
|
};
|
|
342
365
|
}
|
|
343
366
|
if (query) {
|
|
@@ -351,7 +374,8 @@ class MemoryTools {
|
|
|
351
374
|
})),
|
|
352
375
|
count: limitedResults.length,
|
|
353
376
|
totalFound: results.length,
|
|
354
|
-
sortBy
|
|
377
|
+
sortBy,
|
|
378
|
+
...(citationDirective && { _citationDirective: citationDirective })
|
|
355
379
|
};
|
|
356
380
|
}
|
|
357
381
|
// Get recent memories from context
|
|
@@ -367,7 +391,8 @@ class MemoryTools {
|
|
|
367
391
|
relevanceScore: r.score
|
|
368
392
|
})),
|
|
369
393
|
count: limitedResults.length,
|
|
370
|
-
sortBy
|
|
394
|
+
sortBy,
|
|
395
|
+
...(citationDirective && { _citationDirective: citationDirective })
|
|
371
396
|
};
|
|
372
397
|
}
|
|
373
398
|
catch (error) {
|
|
@@ -378,6 +403,7 @@ class MemoryTools {
|
|
|
378
403
|
async handleSearch(input, context) {
|
|
379
404
|
try {
|
|
380
405
|
const { query, filters, limit = 20 } = input;
|
|
406
|
+
const citationDirective = this.getCitationDirective();
|
|
381
407
|
if (!query) {
|
|
382
408
|
throw new Error('Query is required');
|
|
383
409
|
}
|
|
@@ -431,7 +457,8 @@ class MemoryTools {
|
|
|
431
457
|
estimatedTokens: resultTokens,
|
|
432
458
|
estimatedTokensSaved: tokensSaved,
|
|
433
459
|
efficiency: tokensSaved > 0 ? `${Math.round((tokensSaved / (tokensSaved + resultTokens)) * 100)}%` : '0%'
|
|
434
|
-
}
|
|
460
|
+
},
|
|
461
|
+
...(citationDirective && { _citationDirective: citationDirective })
|
|
435
462
|
};
|
|
436
463
|
}
|
|
437
464
|
catch (error) {
|
|
@@ -648,8 +675,71 @@ class MemoryTools {
|
|
|
648
675
|
}
|
|
649
676
|
return 0.5; // Default confidence
|
|
650
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* Load all active rules by category - deterministic, no query needed
|
|
680
|
+
*/
|
|
681
|
+
async handleLoadRules(input, context) {
|
|
682
|
+
try {
|
|
683
|
+
const { projectId } = input;
|
|
684
|
+
const citationDirective = this.getCitationDirective();
|
|
685
|
+
const rules = this.memoryService.loadActiveRules(projectId || context.projectId);
|
|
686
|
+
// Format categorized markdown sections
|
|
687
|
+
const sections = [];
|
|
688
|
+
if (rules.preferences.length > 0) {
|
|
689
|
+
sections.push('## Preferences\n' + rules.preferences.map(m => {
|
|
690
|
+
const val = typeof m.value === 'object' ? (m.value.content || m.value.value || JSON.stringify(m.value)) : m.value;
|
|
691
|
+
return `- ${m.preference_key || m.key}: ${val}`;
|
|
692
|
+
}).join('\n'));
|
|
693
|
+
}
|
|
694
|
+
if (rules.corrections.length > 0) {
|
|
695
|
+
sections.push('## Corrections\n' + rules.corrections.map(m => {
|
|
696
|
+
const val = typeof m.value === 'object' ? (m.value.content || m.value.value || JSON.stringify(m.value)) : m.value;
|
|
697
|
+
return `- ${val}`;
|
|
698
|
+
}).join('\n'));
|
|
699
|
+
}
|
|
700
|
+
if (rules.failures.length > 0) {
|
|
701
|
+
sections.push('## Failures\n' + rules.failures.map(m => {
|
|
702
|
+
const val = typeof m.value === 'object' ? (m.value.content || m.value.value || JSON.stringify(m.value)) : m.value;
|
|
703
|
+
return `- ${val}`;
|
|
704
|
+
}).join('\n'));
|
|
705
|
+
}
|
|
706
|
+
if (rules.devops.length > 0) {
|
|
707
|
+
sections.push('## DevOps Rules\n' + rules.devops.map(m => {
|
|
708
|
+
const val = typeof m.value === 'object' ? (m.value.content || m.value.value || JSON.stringify(m.value)) : m.value;
|
|
709
|
+
return `- ${val}`;
|
|
710
|
+
}).join('\n'));
|
|
711
|
+
}
|
|
712
|
+
const totalRules = rules.preferences.length + rules.corrections.length +
|
|
713
|
+
rules.failures.length + rules.devops.length;
|
|
714
|
+
const resultTokens = this.estimateTokens([
|
|
715
|
+
...rules.preferences, ...rules.corrections,
|
|
716
|
+
...rules.failures, ...rules.devops
|
|
717
|
+
]);
|
|
718
|
+
// Record to SearchMonitor so monitoring/stats still work
|
|
719
|
+
this.searchMonitor.recordSearch('load_rules', totalRules, context.sessionId, 'mcp', { tool: 'load_rules', tokenMetrics: { resultTokens, tokensSaved: totalRules > 0 ? totalRules * 200 : 0 } });
|
|
720
|
+
return {
|
|
721
|
+
rules: sections.length > 0 ? sections.join('\n\n') : 'No active rules found. This may be a new project.',
|
|
722
|
+
counts: {
|
|
723
|
+
preferences: rules.preferences.length,
|
|
724
|
+
corrections: rules.corrections.length,
|
|
725
|
+
failures: rules.failures.length,
|
|
726
|
+
devops: rules.devops.length,
|
|
727
|
+
total: totalRules
|
|
728
|
+
},
|
|
729
|
+
summary: rules.summary,
|
|
730
|
+
...(citationDirective && { _citationDirective: citationDirective })
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
this.logger.error('MemoryTools', 'Failed to load rules', error);
|
|
735
|
+
throw error;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
651
738
|
getTools() {
|
|
652
739
|
return this.tools;
|
|
653
740
|
}
|
|
654
741
|
}
|
|
655
742
|
exports.MemoryTools = MemoryTools;
|
|
743
|
+
MemoryTools.CITATION_DIRECTIVE = 'When you apply any of the above rules or memories to your work, ' +
|
|
744
|
+
'add a brief inline citation: "(applied from memory: [one-line summary])". ' +
|
|
745
|
+
'Only cite memories you actually use.';
|