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.
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/cli/claude-recall-cli.js +345 -0
- package/dist/cli/commands/migrate.js +245 -0
- package/dist/core/pattern-detector.js +242 -0
- package/dist/core/patterns.js +56 -0
- package/dist/core/retrieval.js +108 -0
- package/dist/mcp/rate-limiter.js +114 -0
- package/dist/mcp/server.js +283 -0
- package/dist/mcp/session-manager.js +161 -0
- package/dist/mcp/tools/memory-tools.js +358 -0
- package/dist/mcp/transports/stdio.js +246 -0
- package/dist/memory/pattern-store.js +66 -0
- package/dist/memory/schema.sql +24 -0
- package/dist/memory/storage.js +274 -0
- package/dist/services/action-pattern-detector.js +251 -0
- package/dist/services/claude-json-watcher.js +243 -0
- package/dist/services/config.js +149 -0
- package/dist/services/database-manager.js +332 -0
- package/dist/services/logging.js +124 -0
- package/dist/services/memory-enhancer.js +148 -0
- package/dist/services/memory.js +334 -0
- package/dist/services/pattern-service.js +172 -0
- package/dist/services/preference-extractor.js +286 -0
- package/dist/services/semantic-preference-extractor.js +432 -0
- package/docs/MCP_API_REFERENCE.md +257 -0
- package/docs/MCP_USER_GUIDE.md +115 -0
- package/docs/release-process.md +86 -0
- package/package.json +89 -0
- package/scripts/postinstall.js +76 -0
|
@@ -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;
|