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.
@@ -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()
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.0.0"
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
- Invoke memory search in these situations:
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 search before acting** - Call `mcp__claude-recall__search` before Write, Edit, or significant Bash operations
26
- 2. **Apply what you find** - Use retrieved preferences, patterns, and corrections
27
- 3. **Capture corrections immediately** - User fixes are highest priority
28
- 4. **Store learning cycles** - When you fail then succeed, that's valuable knowledge
29
- 5. **Never store secrets** - No API keys, passwords, tokens, or PII
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
- ### Search (Before Every Task)
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": "[task] [domain] preferences patterns" })
43
+ mcp__claude-recall__search({ "query": "authentication jwt session" })
37
44
  ```
38
45
 
39
- Examples:
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. Search first:
111
- mcp__claude-recall__search({ "query": "authentication auth jwt session preferences" })
113
+ 2. Load rules first:
114
+ mcp__claude-recall__load_rules({})
112
115
 
113
- 3. Found: "We use JWT for auth, store tokens in httpOnly cookies"
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**: Search → Apply → Execute → Capture outcomes → Better next time
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 and clean up old hooks (v0.9.0+ uses Skills, not hooks)
569
- function installSkillsAndCleanupHooks(force = false) {
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.x Setup\n');
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 old hooks (v0.9.0+ doesn't use hooks) ===
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
- 'pre_tool_search_enforcer.py',
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) from .claude/hooks/`);
601
+ console.log(`🧹 Removed ${removedCount} old hook file(s)`);
612
602
  }
613
603
  }
614
- // === CLEANUP: Clear hook configuration from settings.json ===
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
- // If settings.json is invalid, create fresh one
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
- else {
637
- // Create new settings.json without hooks
638
- if (!fs.existsSync(claudeDir)) {
639
- fs.mkdirSync(claudeDir, { recursive: true });
640
- }
641
- fs.writeFileSync(settingsPath, JSON.stringify({ hooks: {}, hooksVersion: '2.0.0' }, null, 2));
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.0+ uses native Claude Skills instead of hooks.');
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 clean up old hooks
664
- installSkillsAndCleanupHooks();
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
- installSkillsAndCleanupHooks(options.force || false);
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/pre_tool_search_enforcer.py');
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 pre_tool_search_enforcer.py');
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.';