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,274 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.MemoryStorage = void 0;
40
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ class MemoryStorage {
44
+ constructor(dbPath) {
45
+ this.db = new better_sqlite3_1.default(dbPath);
46
+ this.initialize();
47
+ }
48
+ initialize() {
49
+ // Check if the database is already initialized
50
+ try {
51
+ const tableExists = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories'").get();
52
+ if (tableExists) {
53
+ // Database is already initialized, skip schema execution
54
+ return;
55
+ }
56
+ }
57
+ catch (error) {
58
+ // If checking fails, proceed with initialization
59
+ }
60
+ // Initialize the database schema
61
+ try {
62
+ const schemaPath = path.join(__dirname, 'schema.sql');
63
+ const schema = fs.readFileSync(schemaPath, 'utf-8');
64
+ this.db.exec(schema);
65
+ }
66
+ catch (error) {
67
+ // Log error but don't throw - database might already be initialized
68
+ console.error('Error initializing database schema:', error);
69
+ // Verify that the memories table exists
70
+ const tableExists = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories'").get();
71
+ if (!tableExists) {
72
+ // Re-throw the error if the table doesn't exist
73
+ throw new Error(`Failed to initialize database: ${error}`);
74
+ }
75
+ }
76
+ }
77
+ save(memory) {
78
+ const stmt = this.db.prepare(`
79
+ INSERT OR REPLACE INTO memories
80
+ (key, value, type, project_id, file_path, timestamp, relevance_score, access_count,
81
+ preference_key, is_active, superseded_by, superseded_at, confidence_score)
82
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
83
+ `);
84
+ stmt.run(memory.key, JSON.stringify(memory.value), memory.type, memory.project_id || null, memory.file_path || null, memory.timestamp || Date.now(), memory.relevance_score || 1.0, memory.access_count || 0, memory.preference_key || null, memory.is_active !== undefined ? (memory.is_active ? 1 : 0) : 1, memory.superseded_by || null, memory.superseded_at || null, memory.confidence_score || null);
85
+ }
86
+ retrieve(key) {
87
+ const stmt = this.db.prepare('SELECT * FROM memories WHERE key = ?');
88
+ const row = stmt.get(key);
89
+ if (row) {
90
+ this.updateAccessCount(key);
91
+ // Fetch updated row after incrementing access count
92
+ const updatedRow = stmt.get(key);
93
+ return this.rowToMemory(updatedRow);
94
+ }
95
+ return null;
96
+ }
97
+ updateAccessCount(key) {
98
+ const stmt = this.db.prepare(`
99
+ UPDATE memories
100
+ SET access_count = access_count + 1,
101
+ last_accessed = ?
102
+ WHERE key = ?
103
+ `);
104
+ stmt.run(Date.now(), key);
105
+ }
106
+ rowToMemory(row) {
107
+ return {
108
+ id: row.id,
109
+ key: row.key,
110
+ value: JSON.parse(row.value),
111
+ type: row.type,
112
+ project_id: row.project_id,
113
+ file_path: row.file_path,
114
+ timestamp: row.timestamp,
115
+ access_count: row.access_count,
116
+ last_accessed: row.last_accessed,
117
+ relevance_score: row.relevance_score,
118
+ preference_key: row.preference_key,
119
+ is_active: row.is_active === 1,
120
+ superseded_by: row.superseded_by,
121
+ superseded_at: row.superseded_at,
122
+ confidence_score: row.confidence_score
123
+ };
124
+ }
125
+ searchByContext(context) {
126
+ let query = 'SELECT * FROM memories WHERE 1=1';
127
+ const params = [];
128
+ if (context.project_id) {
129
+ query += ' AND project_id = ?';
130
+ params.push(context.project_id);
131
+ }
132
+ if (context.file_path) {
133
+ query += ' AND file_path = ?';
134
+ params.push(context.file_path);
135
+ }
136
+ if (context.type) {
137
+ query += ' AND type = ?';
138
+ params.push(context.type);
139
+ }
140
+ // Add keyword search in value field
141
+ if (context.keywords && context.keywords.length > 0) {
142
+ const keywordConditions = context.keywords.map(() => 'value LIKE ?').join(' OR ');
143
+ query += ` AND (${keywordConditions})`;
144
+ // Add parameters for each keyword
145
+ for (const keyword of context.keywords) {
146
+ params.push(`%${keyword}%`);
147
+ }
148
+ }
149
+ // Order by type priority and relevance
150
+ query += ` ORDER BY
151
+ CASE type
152
+ WHEN 'project-knowledge' THEN 1
153
+ WHEN 'preference' THEN 2
154
+ WHEN 'tool-use' THEN 3
155
+ ELSE 4
156
+ END,
157
+ relevance_score DESC,
158
+ timestamp DESC`;
159
+ const stmt = this.db.prepare(query);
160
+ const rows = stmt.all(...params);
161
+ return rows.map(row => this.rowToMemory(row));
162
+ }
163
+ search(query) {
164
+ const stmt = this.db.prepare(`
165
+ SELECT * FROM memories
166
+ WHERE key LIKE ? OR value LIKE ?
167
+ ORDER BY relevance_score DESC
168
+ LIMIT 20
169
+ `);
170
+ const searchPattern = `%${query}%`;
171
+ const rows = stmt.all(searchPattern, searchPattern);
172
+ return rows.map(row => this.rowToMemory(row));
173
+ }
174
+ getStats() {
175
+ const totalStmt = this.db.prepare('SELECT COUNT(*) as count FROM memories');
176
+ const total = totalStmt.get().count;
177
+ const byTypeStmt = this.db.prepare('SELECT type, COUNT(*) as count FROM memories GROUP BY type');
178
+ const byTypeRows = byTypeStmt.all();
179
+ const byType = {};
180
+ for (const row of byTypeRows) {
181
+ byType[row.type] = row.count;
182
+ }
183
+ return { total, byType };
184
+ }
185
+ /**
186
+ * Update a memory record by key
187
+ */
188
+ update(key, updates) {
189
+ const fields = Object.keys(updates).filter(k => k !== 'key'); // Don't update key
190
+ const setClause = fields.map(field => `${field} = ?`).join(', ');
191
+ const values = fields.map(field => {
192
+ const value = updates[field];
193
+ if (field === 'value') {
194
+ return JSON.stringify(value);
195
+ }
196
+ else if (field === 'is_active') {
197
+ return value ? 1 : 0;
198
+ }
199
+ else {
200
+ return value;
201
+ }
202
+ });
203
+ const stmt = this.db.prepare(`UPDATE memories SET ${setClause} WHERE key = ?`);
204
+ stmt.run(...values, key);
205
+ }
206
+ /**
207
+ * Get preferences by preference key
208
+ */
209
+ getByPreferenceKey(preferenceKey, projectId) {
210
+ let query = 'SELECT * FROM memories WHERE preference_key = ? AND type = ?';
211
+ const params = [preferenceKey, 'preference'];
212
+ if (projectId) {
213
+ query += ' AND project_id = ?';
214
+ params.push(projectId);
215
+ }
216
+ query += ' ORDER BY timestamp DESC';
217
+ const stmt = this.db.prepare(query);
218
+ const rows = stmt.all(...params);
219
+ return rows.map(row => this.rowToMemory(row));
220
+ }
221
+ /**
222
+ * Get preferences by context with active filtering
223
+ */
224
+ getPreferencesByContext(context) {
225
+ let query = 'SELECT * FROM memories WHERE type = ?';
226
+ const params = ['preference'];
227
+ if (context.project_id) {
228
+ query += ' AND project_id = ?';
229
+ params.push(context.project_id);
230
+ }
231
+ if (context.file_path) {
232
+ query += ' AND file_path = ?';
233
+ params.push(context.file_path);
234
+ }
235
+ // Order by preference key, then by timestamp desc to get latest per key
236
+ query += ' ORDER BY preference_key, timestamp DESC';
237
+ const stmt = this.db.prepare(query);
238
+ const rows = stmt.all(...params);
239
+ return rows.map(row => this.rowToMemory(row));
240
+ }
241
+ /**
242
+ * Mark a memory as superseded
243
+ */
244
+ markSuperseded(key, supersededBy) {
245
+ const stmt = this.db.prepare(`
246
+ UPDATE memories
247
+ SET is_active = 0, superseded_by = ?, superseded_at = ?
248
+ WHERE key = ?
249
+ `);
250
+ stmt.run(supersededBy, Date.now(), key);
251
+ }
252
+ /**
253
+ * Get active preferences only
254
+ */
255
+ getActivePreferences(projectId) {
256
+ let query = 'SELECT * FROM memories WHERE type = ? AND is_active = 1';
257
+ const params = ['preference'];
258
+ if (projectId) {
259
+ query += ' AND project_id = ?';
260
+ params.push(projectId);
261
+ }
262
+ query += ' ORDER BY preference_key, timestamp DESC';
263
+ const stmt = this.db.prepare(query);
264
+ const rows = stmt.all(...params);
265
+ return rows.map(row => this.rowToMemory(row));
266
+ }
267
+ getDatabase() {
268
+ return this.db;
269
+ }
270
+ close() {
271
+ this.db.close();
272
+ }
273
+ }
274
+ exports.MemoryStorage = MemoryStorage;
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActionPatternDetector = void 0;
4
+ const logging_1 = require("./logging");
5
+ /**
6
+ * ActionPatternDetector - Detects patterns in Claude Code's behavior
7
+ *
8
+ * Instead of analyzing user input with API calls, this service detects
9
+ * patterns in how Claude Code acts, allowing us to learn preferences
10
+ * from behavior rather than redundant text analysis.
11
+ */
12
+ class ActionPatternDetector {
13
+ constructor() {
14
+ this.logger = logging_1.LoggingService.getInstance();
15
+ this.actionHistory = [];
16
+ // Pattern detection thresholds
17
+ this.PATTERN_THRESHOLD = 2; // How many times before we consider it a pattern
18
+ this.RECENT_WINDOW = 10; // Number of recent actions to consider
19
+ this.logger.info('ActionPatternDetector', 'Initialized behavioral pattern detector');
20
+ }
21
+ /**
22
+ * Detect patterns from tool usage
23
+ */
24
+ detectToolAction(toolName, toolInput) {
25
+ const timestamp = Date.now();
26
+ // Detect file creation patterns
27
+ if (toolName === 'Write' || toolName === 'MultiEdit') {
28
+ const filePath = toolInput.file_path || toolInput.filePath;
29
+ if (filePath) {
30
+ const action = this.detectFileCreationPattern(filePath, timestamp);
31
+ if (action) {
32
+ this.recordAction(action);
33
+ return action;
34
+ }
35
+ }
36
+ }
37
+ // Detect specific tool preferences (e.g., always using axios for HTTP)
38
+ if (toolName === 'Write' && toolInput.content) {
39
+ const toolPreference = this.detectToolPreference(toolInput.content, timestamp);
40
+ if (toolPreference) {
41
+ this.recordAction(toolPreference);
42
+ return toolPreference;
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+ /**
48
+ * Detect patterns from Claude's response content
49
+ */
50
+ detectResponsePattern(responseContent) {
51
+ const timestamp = Date.now();
52
+ // Detect when Claude mentions preferences
53
+ const preferenceMentions = [
54
+ /I'll use (\w+) for (\w+)/gi,
55
+ /I'm using (\w+) instead of (\w+)/gi,
56
+ /I'll save (?:the )?(\w+) in ([\w\-\/]+)/gi,
57
+ /I'll create (?:the )?(\w+) in ([\w\-\/]+)/gi,
58
+ /Using (\w+) as (?:the )?(\w+)/gi
59
+ ];
60
+ for (const pattern of preferenceMentions) {
61
+ const match = pattern.exec(responseContent);
62
+ if (match) {
63
+ const action = {
64
+ type: 'preference_mention',
65
+ pattern: match[0],
66
+ context: {
67
+ content: responseContent.substring(Math.max(0, match.index - 50), match.index + match[0].length + 50),
68
+ timestamp
69
+ },
70
+ preference: {
71
+ key: this.inferPreferenceKey(match[2] || 'tool'),
72
+ value: match[1],
73
+ confidence: 0.8,
74
+ raw: match[0],
75
+ isOverride: false,
76
+ overrideSignals: []
77
+ }
78
+ };
79
+ this.recordAction(action);
80
+ return action;
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+ /**
86
+ * Get learned patterns based on repeated behavior
87
+ */
88
+ getLearnedPatterns() {
89
+ const preferences = [];
90
+ const recentActions = this.actionHistory.slice(-this.RECENT_WINDOW);
91
+ // Group actions by type and pattern
92
+ const patternGroups = new Map();
93
+ for (const action of recentActions) {
94
+ if (action.preference) {
95
+ const key = `${action.preference.key}:${action.preference.value}`;
96
+ if (!patternGroups.has(key)) {
97
+ patternGroups.set(key, []);
98
+ }
99
+ patternGroups.get(key).push(action);
100
+ }
101
+ }
102
+ // Convert repeated patterns to preferences
103
+ for (const [key, actions] of patternGroups) {
104
+ if (actions.length >= this.PATTERN_THRESHOLD) {
105
+ const latestAction = actions[actions.length - 1];
106
+ if (latestAction.preference) {
107
+ preferences.push({
108
+ ...latestAction.preference,
109
+ confidence: Math.min(0.9, 0.5 + (actions.length * 0.1)), // Increase confidence with repetition
110
+ raw: `Behavioral pattern: ${latestAction.preference.raw} (observed ${actions.length} times)`
111
+ });
112
+ }
113
+ }
114
+ }
115
+ return preferences;
116
+ }
117
+ detectFileCreationPattern(filePath, timestamp) {
118
+ const pathParts = filePath.split('/');
119
+ // Detect test file patterns
120
+ if (filePath.includes('test') || filePath.includes('spec')) {
121
+ const testDir = this.findTestDirectory(pathParts);
122
+ if (testDir) {
123
+ return {
124
+ type: 'file_creation',
125
+ pattern: `test_files_in_${testDir}`,
126
+ context: { filePath, directory: testDir, timestamp },
127
+ preference: {
128
+ key: 'test_location',
129
+ value: testDir,
130
+ confidence: 0.85,
131
+ raw: `Tests created in ${testDir}`,
132
+ isOverride: false,
133
+ overrideSignals: []
134
+ }
135
+ };
136
+ }
137
+ }
138
+ // Detect config file patterns
139
+ if (pathParts.includes('configs') || pathParts.includes('config')) {
140
+ const configDir = pathParts.includes('configs') ? 'configs' : 'config';
141
+ return {
142
+ type: 'file_creation',
143
+ pattern: `config_files_in_${configDir}`,
144
+ context: { filePath, directory: configDir, timestamp },
145
+ preference: {
146
+ key: 'config_location',
147
+ value: configDir,
148
+ confidence: 0.8,
149
+ raw: `Config files saved in ${configDir}`,
150
+ isOverride: false,
151
+ overrideSignals: []
152
+ }
153
+ };
154
+ }
155
+ return null;
156
+ }
157
+ detectToolPreference(content, timestamp) {
158
+ // Detect HTTP client preferences
159
+ if (content.includes('axios') && (content.includes('http') || content.includes('request'))) {
160
+ return {
161
+ type: 'tool_preference',
162
+ pattern: 'axios_for_http',
163
+ context: { content: content.substring(0, 200), timestamp },
164
+ preference: {
165
+ key: 'http_client',
166
+ value: 'axios',
167
+ confidence: 0.75,
168
+ raw: 'Using axios for HTTP requests',
169
+ isOverride: false,
170
+ overrideSignals: []
171
+ }
172
+ };
173
+ }
174
+ // Detect indentation preferences
175
+ if (content.includes('\t')) {
176
+ return {
177
+ type: 'pattern_usage',
178
+ pattern: 'tabs_indentation',
179
+ context: { content: 'File uses tab indentation', timestamp },
180
+ preference: {
181
+ key: 'indentation',
182
+ value: 'tabs',
183
+ confidence: 0.7,
184
+ raw: 'Using tabs for indentation',
185
+ isOverride: false,
186
+ overrideSignals: []
187
+ }
188
+ };
189
+ }
190
+ else if (content.match(/^ /m)) {
191
+ return {
192
+ type: 'pattern_usage',
193
+ pattern: '2_space_indentation',
194
+ context: { content: 'File uses 2-space indentation', timestamp },
195
+ preference: {
196
+ key: 'indentation',
197
+ value: '2_spaces',
198
+ confidence: 0.7,
199
+ raw: 'Using 2 spaces for indentation',
200
+ isOverride: false,
201
+ overrideSignals: []
202
+ }
203
+ };
204
+ }
205
+ return null;
206
+ }
207
+ findTestDirectory(pathParts) {
208
+ // Look for test-related directories
209
+ for (let i = 0; i < pathParts.length; i++) {
210
+ const part = pathParts[i];
211
+ if (part.includes('test') || part.includes('spec')) {
212
+ // Check if it's a custom test directory
213
+ if (part.startsWith('tests-') || part.startsWith('test-')) {
214
+ return part;
215
+ }
216
+ // Otherwise return the standard test directory
217
+ return part;
218
+ }
219
+ }
220
+ return null;
221
+ }
222
+ inferPreferenceKey(context) {
223
+ const normalized = context.toLowerCase();
224
+ if (normalized.includes('indent'))
225
+ return 'indentation';
226
+ if (normalized.includes('http') || normalized.includes('request'))
227
+ return 'http_client';
228
+ if (normalized.includes('test'))
229
+ return 'test_framework';
230
+ if (normalized.includes('build'))
231
+ return 'build_tool';
232
+ if (normalized.includes('ui') || normalized.includes('frontend'))
233
+ return 'ui_framework';
234
+ if (normalized.includes('database') || normalized.includes('db'))
235
+ return 'database';
236
+ return context.toLowerCase().replace(/\s+/g, '_');
237
+ }
238
+ recordAction(action) {
239
+ this.actionHistory.push(action);
240
+ // Keep history size manageable
241
+ if (this.actionHistory.length > 100) {
242
+ this.actionHistory = this.actionHistory.slice(-50);
243
+ }
244
+ this.logger.debug('ActionPatternDetector', 'Recorded action', {
245
+ type: action.type,
246
+ pattern: action.pattern,
247
+ preference: action.preference
248
+ });
249
+ }
250
+ }
251
+ exports.ActionPatternDetector = ActionPatternDetector;