claude-recall 0.2.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,432 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SemanticPreferenceExtractor = void 0;
4
+ const logging_1 = require("./logging");
5
+ class SemanticPreferenceExtractor {
6
+ constructor() {
7
+ this.logger = logging_1.LoggingService.getInstance();
8
+ // Semantic patterns for intent identification
9
+ this.INTENT_PATTERNS = [
10
+ // Location preferences - very flexible patterns
11
+ {
12
+ pattern: /(?:let'?s?\s+)?(?:put|save|store|place|keep|create)\s+(.+?)\s+(?:in|into|at|under|within)\s+([^\s].+?)(?:\s+from\s+now\s+on|\s+going\s+forward|\s+moving\s+forward)?$/i,
13
+ category: 'location',
14
+ extractor: (match) => {
15
+ let location = match[2].trim();
16
+ // Remove trailing temporal phrases
17
+ location = location.replace(/\s+(from\s+now\s+on|going\s+forward|moving\s+forward)$/i, '');
18
+ return {
19
+ action: 'save_location',
20
+ entities: [match[1].trim(), location]
21
+ };
22
+ }
23
+ },
24
+ {
25
+ pattern: /(.+?)\s+(?:should\s+)?(?:go|goes?|belong|live)(?:es)?\s+(?:in|into|at|under)\s+([^\s].+?)(?:\s+going\s+forward)?$/i,
26
+ category: 'location',
27
+ extractor: (match) => {
28
+ let location = match[2].trim();
29
+ // Remove trailing temporal phrases
30
+ location = location.replace(/\s+(going\s+forward|from\s+now\s+on)$/i, '');
31
+ return {
32
+ action: 'location_preference',
33
+ entities: [match[1].trim(), location]
34
+ };
35
+ }
36
+ },
37
+ {
38
+ pattern: /(?:i\s+)?(?:think|believe|want)\s+(.+?)\s+(?:should\s+)?(?:be|go)\s+(?:in|at)\s+(.+)/i,
39
+ category: 'location',
40
+ extractor: (match) => ({
41
+ action: 'location_opinion',
42
+ entities: [match[1].trim(), match[2].trim()]
43
+ })
44
+ },
45
+ {
46
+ pattern: /(?:actually|you\s+know\s+what),?\s*(?:let'?s?\s+)?(?:use|put|save)\s+(.+?)\s+(?:in|for)\s+(.+)/i,
47
+ category: 'location',
48
+ extractor: (match) => ({
49
+ action: 'location_change',
50
+ entities: [match[1].trim(), match[2].trim()]
51
+ })
52
+ },
53
+ // More flexible patterns for casual language
54
+ {
55
+ pattern: /(?:oh\s+)?(?:btw|by\s+the\s+way)?,?\s*(.+?)\s+should\s+(?:be|go)\s+in\s+([^\s].+?)(?:\s+moving\s+forward)?$/i,
56
+ category: 'location',
57
+ extractor: (match) => {
58
+ let location = match[2].trim();
59
+ location = location.replace(/\s+(moving\s+forward|from\s+now\s+on)$/i, '');
60
+ return {
61
+ action: 'location_preference',
62
+ entities: [match[1].trim(), location]
63
+ };
64
+ }
65
+ },
66
+ {
67
+ pattern: /(?:let'?s?\s+)?(?:start\s+)?(?:putting|placing)\s+(?:all\s+)?(.+?)\s+in\s+([^\s].+)/i,
68
+ category: 'location',
69
+ extractor: (match) => ({
70
+ action: 'save_location',
71
+ entities: [match[1].trim(), match[2].trim()]
72
+ })
73
+ },
74
+ // Tool preferences
75
+ {
76
+ pattern: /(?:let'?s?\s+)?use\s+(.+?)\s+(?:for|to|when)\s+(.+)/i,
77
+ category: 'tool',
78
+ extractor: (match) => ({
79
+ action: 'tool_preference',
80
+ entities: [match[1].trim(), match[2].trim()]
81
+ })
82
+ },
83
+ {
84
+ pattern: /prefer\s+(.+?)\s+(?:over|instead\s+of|rather\s+than)\s+(.+)/i,
85
+ category: 'tool',
86
+ extractor: (match) => ({
87
+ action: 'tool_comparison',
88
+ entities: [match[1].trim(), match[2].trim()]
89
+ })
90
+ },
91
+ // Style preferences
92
+ {
93
+ pattern: /(?:use\s+)?(\d+)\s*spaces?\s+(?:for\s+)?(?:indent|indentation)/i,
94
+ category: 'style',
95
+ extractor: (match) => ({
96
+ action: 'indentation_style',
97
+ entities: [`${match[1]}_spaces`]
98
+ })
99
+ },
100
+ {
101
+ pattern: /(?:use\s+)?tabs?\s+(?:for\s+)?(?:indent|indentation)/i,
102
+ category: 'style',
103
+ extractor: () => ({
104
+ action: 'indentation_style',
105
+ entities: ['tabs']
106
+ })
107
+ },
108
+ {
109
+ pattern: /(?:actually,?\s+)?(?:let'?s?\s+)?use\s+tabs\s+(?:instead|now)?/i,
110
+ category: 'style',
111
+ extractor: () => ({
112
+ action: 'indentation_style',
113
+ entities: ['tabs']
114
+ })
115
+ },
116
+ // Process preferences
117
+ {
118
+ pattern: /(?:always|never)\s+(.+)/i,
119
+ category: 'process',
120
+ extractor: (match, text) => ({
121
+ action: text.toLowerCase().includes('always') ? 'always_do' : 'never_do',
122
+ entities: [match[1].trim()]
123
+ })
124
+ }
125
+ ];
126
+ // Context indicators that boost confidence
127
+ this.CONTEXT_BOOSTERS = {
128
+ temporal: ['from now on', 'going forward', 'moving forward', 'henceforth', 'starting now'],
129
+ change: ['actually', 'instead', 'rather', 'changed my mind', 'on second thought'],
130
+ emphasis: ['definitely', 'absolutely', 'really', 'certainly', 'please'],
131
+ casual: ['hey', 'oh', 'btw', 'by the way', 'fyi']
132
+ };
133
+ // Word variations and synonyms for entity normalization
134
+ this.ENTITY_SYNONYMS = {
135
+ 'tests': ['test', 'testing', 'specs', 'spec', 'test files', 'test file'],
136
+ 'config': ['configuration', 'configs', 'settings', 'conf'],
137
+ 'docs': ['documentation', 'documents', 'doc'],
138
+ 'src': ['source', 'sources', 'code'],
139
+ 'lib': ['library', 'libraries', 'libs'],
140
+ 'dist': ['distribution', 'build', 'output', 'out']
141
+ };
142
+ }
143
+ /**
144
+ * Extract preferences using semantic understanding
145
+ */
146
+ extractPreference(prompt, context) {
147
+ try {
148
+ // Step 1: Identify intent
149
+ const intent = this.identifyIntent(prompt);
150
+ if (!intent)
151
+ return null;
152
+ // Step 2: Extract components based on intent
153
+ const components = this.extractComponents(prompt, intent);
154
+ if (!components)
155
+ return null;
156
+ // Step 3: Build structured preference
157
+ return this.buildPreference(intent, components, prompt);
158
+ }
159
+ catch (error) {
160
+ this.logger.logServiceError('SemanticPreferenceExtractor', 'extractPreference', error);
161
+ return null;
162
+ }
163
+ }
164
+ /**
165
+ * Extract multiple preferences from text
166
+ */
167
+ extractAllPreferences(text) {
168
+ const preferences = [];
169
+ const sentences = this.splitIntoMeaningfulChunks(text);
170
+ for (const sentence of sentences) {
171
+ const preference = this.extractPreference(sentence);
172
+ if (preference && preference.confidence > 0.5) {
173
+ preferences.push(preference);
174
+ }
175
+ }
176
+ return preferences;
177
+ }
178
+ /**
179
+ * Identify the intent category and extract basic information
180
+ */
181
+ identifyIntent(prompt) {
182
+ let bestMatch = null;
183
+ let highestConfidence = 0;
184
+ for (const pattern of this.INTENT_PATTERNS) {
185
+ const match = prompt.match(pattern.pattern);
186
+ if (match) {
187
+ const extracted = pattern.extractor(match, prompt);
188
+ const baseConfidence = this.calculateBaseConfidence(prompt, match[0]);
189
+ const contextBoost = this.calculateContextBoost(prompt);
190
+ const intent = {
191
+ category: pattern.category,
192
+ action: extracted.action || 'unknown',
193
+ entities: extracted.entities || [],
194
+ confidence: Math.min(baseConfidence + contextBoost, 1.0)
195
+ };
196
+ if (intent.confidence > highestConfidence) {
197
+ highestConfidence = intent.confidence;
198
+ bestMatch = intent;
199
+ }
200
+ }
201
+ }
202
+ return bestMatch;
203
+ }
204
+ /**
205
+ * Extract detailed components based on intent
206
+ */
207
+ extractComponents(prompt, intent) {
208
+ switch (intent.category) {
209
+ case 'location':
210
+ return this.extractLocationComponents(prompt, intent);
211
+ case 'tool':
212
+ return this.extractToolComponents(prompt, intent);
213
+ case 'style':
214
+ return this.extractStyleComponents(prompt, intent);
215
+ case 'process':
216
+ return this.extractProcessComponents(prompt, intent);
217
+ default:
218
+ return { raw: intent };
219
+ }
220
+ }
221
+ /**
222
+ * Build the final preference object
223
+ */
224
+ buildPreference(intent, components, rawText) {
225
+ const overrideSignals = this.detectOverrideSignals(rawText);
226
+ return {
227
+ key: components.key,
228
+ value: components.value,
229
+ confidence: intent.confidence,
230
+ rawText: rawText.trim(),
231
+ intent,
232
+ isOverride: overrideSignals.length > 0,
233
+ overrideSignals
234
+ };
235
+ }
236
+ /**
237
+ * Extract location-specific components
238
+ */
239
+ extractLocationComponents(prompt, intent) {
240
+ if (intent.entities.length < 2)
241
+ return null;
242
+ const [subject, location] = intent.entities;
243
+ const normalizedSubject = this.normalizeEntity(subject);
244
+ const normalizedLocation = this.normalizeLocation(location);
245
+ // Determine the preference key based on the subject
246
+ let key = 'file_location'; // default
247
+ if (normalizedSubject.match(/test|spec/i)) {
248
+ key = 'test_location';
249
+ }
250
+ else if (normalizedSubject.match(/config|settings/i)) {
251
+ key = 'config_location';
252
+ }
253
+ else if (normalizedSubject.match(/doc|documentation/i)) {
254
+ key = 'docs_location';
255
+ }
256
+ return {
257
+ key,
258
+ value: normalizedLocation
259
+ };
260
+ }
261
+ /**
262
+ * Extract tool-specific components
263
+ */
264
+ extractToolComponents(prompt, intent) {
265
+ if (intent.entities.length === 0)
266
+ return null;
267
+ const tool = intent.entities[0];
268
+ const purpose = intent.entities[1] || 'general';
269
+ // Map tools to preference keys
270
+ const toolMappings = {
271
+ 'axios': 'http_client',
272
+ 'fetch': 'http_client',
273
+ 'jest': 'test_framework',
274
+ 'vitest': 'test_framework',
275
+ 'mocha': 'test_framework',
276
+ 'webpack': 'build_tool',
277
+ 'vite': 'build_tool',
278
+ 'typescript': 'language',
279
+ 'javascript': 'language',
280
+ 'tabs': 'indentation',
281
+ '2 spaces': 'indentation',
282
+ '4 spaces': 'indentation'
283
+ };
284
+ const key = toolMappings[tool.toLowerCase()] || 'tool_preference';
285
+ return {
286
+ key,
287
+ value: tool.toLowerCase()
288
+ };
289
+ }
290
+ /**
291
+ * Extract style-specific components
292
+ */
293
+ extractStyleComponents(prompt, intent) {
294
+ if (intent.entities.length === 0)
295
+ return null;
296
+ const style = intent.entities[0];
297
+ // Handle different style preferences
298
+ if (intent.action === 'style_preference' && intent.entities.length >= 2) {
299
+ const [value, type] = intent.entities;
300
+ if (type.includes('indent')) {
301
+ return {
302
+ key: 'indentation',
303
+ value: value
304
+ };
305
+ }
306
+ }
307
+ return {
308
+ key: 'indentation',
309
+ value: style
310
+ };
311
+ }
312
+ /**
313
+ * Extract process-specific components
314
+ */
315
+ extractProcessComponents(prompt, intent) {
316
+ if (intent.entities.length === 0)
317
+ return null;
318
+ const process = intent.entities[0];
319
+ const action = intent.action;
320
+ return {
321
+ key: `${action}_rule`,
322
+ value: process
323
+ };
324
+ }
325
+ /**
326
+ * Calculate base confidence based on match quality
327
+ */
328
+ calculateBaseConfidence(fullText, matchedText) {
329
+ // Start with base confidence
330
+ let confidence = 0.6;
331
+ // Boost for match coverage
332
+ const coverage = matchedText.length / fullText.length;
333
+ confidence += coverage * 0.2;
334
+ // Boost for sentence position (preferences often at start or end)
335
+ const position = fullText.indexOf(matchedText) / fullText.length;
336
+ if (position < 0.2 || position > 0.8) {
337
+ confidence += 0.1;
338
+ }
339
+ return confidence;
340
+ }
341
+ /**
342
+ * Calculate confidence boost from context
343
+ */
344
+ calculateContextBoost(text) {
345
+ let boost = 0;
346
+ const lowerText = text.toLowerCase();
347
+ // Check for context boosters
348
+ for (const [category, phrases] of Object.entries(this.CONTEXT_BOOSTERS)) {
349
+ for (const phrase of phrases) {
350
+ if (lowerText.includes(phrase)) {
351
+ switch (category) {
352
+ case 'temporal':
353
+ boost += 0.15;
354
+ break;
355
+ case 'change':
356
+ boost += 0.12;
357
+ break;
358
+ case 'emphasis':
359
+ boost += 0.08;
360
+ break;
361
+ case 'casual':
362
+ boost += 0.05;
363
+ break;
364
+ }
365
+ }
366
+ }
367
+ }
368
+ return Math.min(boost, 0.3); // Cap total boost
369
+ }
370
+ /**
371
+ * Normalize entity names using synonyms
372
+ */
373
+ normalizeEntity(entity) {
374
+ const lower = entity.toLowerCase().trim();
375
+ // Check synonyms
376
+ for (const [normalized, synonyms] of Object.entries(this.ENTITY_SYNONYMS)) {
377
+ if (synonyms.some(syn => lower.includes(syn))) {
378
+ return normalized;
379
+ }
380
+ }
381
+ return entity.trim();
382
+ }
383
+ /**
384
+ * Normalize location paths
385
+ */
386
+ normalizeLocation(location) {
387
+ // Remove quotes and extra spaces
388
+ let normalized = location.trim().replace(/["']/g, '');
389
+ // Remove temporal phrases and punctuation that might have been captured
390
+ normalized = normalized.replace(/\s+(from\s+now\s+on|going\s+forward|moving\s+forward)[\?\!]*$/i, '');
391
+ normalized = normalized.replace(/[\?\!]+$/, ''); // Remove trailing punctuation
392
+ // Don't add ./ prefix to already valid paths or simple directory names
393
+ if (!normalized.startsWith('/') &&
394
+ !normalized.startsWith('.') &&
395
+ !normalized.match(/^[a-zA-Z0-9_-]+$/) &&
396
+ !normalized.match(/^__[a-zA-Z0-9_-]+__$/)) { // Handle __dirname__ style
397
+ // Only add ./ if it contains spaces or special chars
398
+ normalized = `./${normalized}`;
399
+ }
400
+ // Normalize separators
401
+ normalized = normalized.replace(/\\/g, '/');
402
+ return normalized;
403
+ }
404
+ /**
405
+ * Detect override signals in text
406
+ */
407
+ detectOverrideSignals(text) {
408
+ const signals = [];
409
+ const lowerText = text.toLowerCase();
410
+ for (const [category, phrases] of Object.entries(this.CONTEXT_BOOSTERS)) {
411
+ if (category === 'temporal' || category === 'change') {
412
+ for (const phrase of phrases) {
413
+ if (lowerText.includes(phrase)) {
414
+ signals.push(phrase);
415
+ }
416
+ }
417
+ }
418
+ }
419
+ return signals;
420
+ }
421
+ /**
422
+ * Split text into meaningful chunks for analysis
423
+ */
424
+ splitIntoMeaningfulChunks(text) {
425
+ // Split by punctuation and conjunctions
426
+ const chunks = text.split(/[.!?;]|\s+(?:and|but|also|then)\s+/i);
427
+ return chunks
428
+ .map(chunk => chunk.trim())
429
+ .filter(chunk => chunk.length > 10); // Filter short fragments
430
+ }
431
+ }
432
+ exports.SemanticPreferenceExtractor = SemanticPreferenceExtractor;
@@ -0,0 +1,257 @@
1
+ # Claude Recall MCP API Reference
2
+
3
+ ## Protocol Version
4
+ - JSON-RPC 2.0
5
+ - MCP Protocol Version: 2024-11-05
6
+
7
+ ## Tools
8
+
9
+ ### mcp__claude-recall__store_memory
10
+
11
+ Stores a memory in the Claude Recall database.
12
+
13
+ **Input Schema:**
14
+ ```json
15
+ {
16
+ "type": "object",
17
+ "properties": {
18
+ "content": {
19
+ "type": "string",
20
+ "description": "Memory content to store"
21
+ },
22
+ "metadata": {
23
+ "type": "object",
24
+ "description": "Optional metadata for the memory"
25
+ }
26
+ },
27
+ "required": ["content"]
28
+ }
29
+ ```
30
+
31
+ **Output:**
32
+ ```json
33
+ {
34
+ "id": "memory_1234567890_abc123",
35
+ "success": true,
36
+ "message": "Memory stored successfully"
37
+ }
38
+ ```
39
+
40
+ ### mcp__claude-recall__search
41
+
42
+ Searches memories using natural language query.
43
+
44
+ **Input Schema:**
45
+ ```json
46
+ {
47
+ "type": "object",
48
+ "properties": {
49
+ "query": {
50
+ "type": "string",
51
+ "description": "Search query"
52
+ },
53
+ "limit": {
54
+ "type": "number",
55
+ "description": "Maximum number of results",
56
+ "default": 10
57
+ },
58
+ "threshold": {
59
+ "type": "number",
60
+ "description": "Minimum similarity score (0-1)",
61
+ "default": 0.7
62
+ }
63
+ },
64
+ "required": ["query"]
65
+ }
66
+ ```
67
+
68
+ **Output:**
69
+ ```json
70
+ {
71
+ "results": [
72
+ {
73
+ "id": "memory_1234567890_abc123",
74
+ "content": "Memory content",
75
+ "metadata": {},
76
+ "timestamp": "2024-01-01T00:00:00Z",
77
+ "score": 0.95
78
+ }
79
+ ],
80
+ "total": 1,
81
+ "query": "search query"
82
+ }
83
+ ```
84
+
85
+ ### mcp__claude-recall__retrieve_memory
86
+
87
+ Retrieves specific memories by ID or recent memories.
88
+
89
+ **Input Schema:**
90
+ ```json
91
+ {
92
+ "type": "object",
93
+ "properties": {
94
+ "id": {
95
+ "type": "string",
96
+ "description": "Memory ID to retrieve"
97
+ },
98
+ "limit": {
99
+ "type": "number",
100
+ "description": "Number of recent memories to retrieve",
101
+ "default": 10
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ **Output:**
108
+ ```json
109
+ {
110
+ "memories": [
111
+ {
112
+ "id": "memory_1234567890_abc123",
113
+ "content": "Memory content",
114
+ "metadata": {},
115
+ "timestamp": "2024-01-01T00:00:00Z",
116
+ "sessionId": "session_123"
117
+ }
118
+ ],
119
+ "total": 1
120
+ }
121
+ ```
122
+
123
+ ### mcp__claude-recall__get_stats
124
+
125
+ Returns memory usage statistics.
126
+
127
+ **Input Schema:**
128
+ ```json
129
+ {
130
+ "type": "object",
131
+ "properties": {}
132
+ }
133
+ ```
134
+
135
+ **Output:**
136
+ ```json
137
+ {
138
+ "totalMemories": 100,
139
+ "sessionMemories": 25,
140
+ "memoryTypes": {
141
+ "general": 50,
142
+ "code": 30,
143
+ "conversation": 20
144
+ },
145
+ "lastMemoryTimestamp": "2024-01-01T00:00:00Z",
146
+ "sessionStartTime": "2024-01-01T00:00:00Z",
147
+ "databaseSize": "1.5 MB"
148
+ }
149
+ ```
150
+
151
+ ### mcp__claude-recall__clear_context
152
+
153
+ Clears session-specific context memories.
154
+
155
+ **Input Schema:**
156
+ ```json
157
+ {
158
+ "type": "object",
159
+ "properties": {
160
+ "confirm": {
161
+ "type": "boolean",
162
+ "description": "Confirmation required to clear context"
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ **Output:**
169
+ ```json
170
+ {
171
+ "success": true,
172
+ "message": "Context cleared successfully",
173
+ "memoriesCleared": 10
174
+ }
175
+ ```
176
+
177
+ ## Health Check
178
+
179
+ ### Method: health/check
180
+
181
+ Returns server health information.
182
+
183
+ **Response:**
184
+ ```json
185
+ {
186
+ "status": "healthy",
187
+ "version": "1.0.0",
188
+ "uptime": 3600,
189
+ "memory": {
190
+ "heapUsed": 50331648,
191
+ "heapTotal": 68157440,
192
+ "external": 2097152,
193
+ "rss": 104857600
194
+ },
195
+ "sessions": {
196
+ "total": 10,
197
+ "active": 2
198
+ },
199
+ "toolsRegistered": 5,
200
+ "database": "connected",
201
+ "rateLimiter": {
202
+ "requestsPerMinute": 100,
203
+ "currentRequests": 15
204
+ }
205
+ }
206
+ ```
207
+
208
+ ## Error Responses
209
+
210
+ All errors follow the JSON-RPC 2.0 error format:
211
+
212
+ ```json
213
+ {
214
+ "jsonrpc": "2.0",
215
+ "id": 1,
216
+ "error": {
217
+ "code": -32603,
218
+ "message": "Internal error",
219
+ "data": {
220
+ "details": "Additional error information"
221
+ }
222
+ }
223
+ }
224
+ ```
225
+
226
+ ### Common Error Codes
227
+ - `-32700`: Parse error
228
+ - `-32600`: Invalid request
229
+ - `-32601`: Method not found
230
+ - `-32602`: Invalid params
231
+ - `-32603`: Internal error
232
+ - `-32000`: Tool not found
233
+ - `-32001`: Rate limit exceeded
234
+ - `-32002`: Database error
235
+
236
+ ## Transport
237
+
238
+ The MCP server uses stdio transport:
239
+ - Input: stdin (JSON-RPC requests)
240
+ - Output: stdout (JSON-RPC responses)
241
+ - Errors: stderr (logging and diagnostics)
242
+
243
+ ## Session Management
244
+
245
+ Sessions are automatically managed by the server:
246
+ - Session ID is generated on first connection
247
+ - Sessions persist across server restarts
248
+ - Session data stored in `~/.claude-recall/sessions.json`
249
+ - Memories are associated with sessions for context
250
+
251
+ ## Rate Limiting
252
+
253
+ Default rate limiting configuration:
254
+ - 100 requests per minute per session
255
+ - Sliding window algorithm
256
+ - Rate limit resets after 60 seconds
257
+ - Custom limits via `CLAUDE_RECALL_RATE_LIMIT` environment variable