polydev-ai 1.2.3 → 1.2.5
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/README.md +1 -1
- package/lib/cliManager.ts +144 -2
- package/lib/smartCliCache.ts +189 -0
- package/lib/universalMemoryExtractor.js +607 -0
- package/lib/zeroKnowledgeEncryption.js +289 -0
- package/mcp/README.md +165 -0
- package/mcp/manifest.json +8 -8
- package/mcp/package.json +2 -2
- package/mcp/server.js +43 -20
- package/mcp/stdio-wrapper.js +203 -2
- package/package.json +5 -1
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Universal Memory Extraction System
|
|
4
|
+
* Extracts memory from ALL CLI tools with zero-knowledge encryption
|
|
5
|
+
* Preserves existing functionality while adding comprehensive memory support
|
|
6
|
+
*/
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.universalMemoryExtractor = exports.UniversalMemoryExtractor = void 0;
|
|
12
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const os_1 = __importDefault(require("os"));
|
|
15
|
+
const server_1 = require("@/app/utils/supabase/server");
|
|
16
|
+
const zeroKnowledgeEncryption_1 = require("./zeroKnowledgeEncryption");
|
|
17
|
+
class UniversalMemoryExtractor {
|
|
18
|
+
constructor() {
|
|
19
|
+
// Memory location patterns for all supported CLI tools
|
|
20
|
+
this.MEMORY_PATTERNS = {
|
|
21
|
+
claude_code: {
|
|
22
|
+
global: [
|
|
23
|
+
'~/.claude/CLAUDE.md',
|
|
24
|
+
'~/.claude/global_memory.md',
|
|
25
|
+
'~/.claude/config.json'
|
|
26
|
+
],
|
|
27
|
+
project: [
|
|
28
|
+
'CLAUDE.md',
|
|
29
|
+
'.claude/CLAUDE.md',
|
|
30
|
+
'.claude/project_memory.md'
|
|
31
|
+
],
|
|
32
|
+
conversations: [
|
|
33
|
+
'~/.claude/projects/*/conversations/*.jsonl',
|
|
34
|
+
'~/.claude/conversations/*.jsonl'
|
|
35
|
+
],
|
|
36
|
+
config: ['~/.claude/config.json']
|
|
37
|
+
},
|
|
38
|
+
cline: {
|
|
39
|
+
global: [
|
|
40
|
+
'~/.cline/global_memory.md',
|
|
41
|
+
'~/.cline/settings.json',
|
|
42
|
+
'~/.cline/config.json'
|
|
43
|
+
],
|
|
44
|
+
project: [
|
|
45
|
+
'.cline/project_memory.md',
|
|
46
|
+
'.cline/cline_project.json',
|
|
47
|
+
'.cline/memory.md'
|
|
48
|
+
],
|
|
49
|
+
conversations: [
|
|
50
|
+
'.cline/conversations/*.json',
|
|
51
|
+
'.cline/chat_history.json'
|
|
52
|
+
],
|
|
53
|
+
config: ['.cline/cline_config.json', '.cline/settings.json']
|
|
54
|
+
},
|
|
55
|
+
codex_cli: {
|
|
56
|
+
global: [
|
|
57
|
+
'~/.codex/memory.json',
|
|
58
|
+
'~/.codex/config.yaml',
|
|
59
|
+
'~/.codex/global_context.md'
|
|
60
|
+
],
|
|
61
|
+
project: [
|
|
62
|
+
'.codex/project_context.json',
|
|
63
|
+
'.codex/memory.md',
|
|
64
|
+
'.codex/context.json'
|
|
65
|
+
],
|
|
66
|
+
conversations: [
|
|
67
|
+
'~/.codex/conversations/*.json',
|
|
68
|
+
'.codex/history.json'
|
|
69
|
+
],
|
|
70
|
+
config: ['~/.codex/config.json', '~/.codex/config.yaml']
|
|
71
|
+
},
|
|
72
|
+
cursor: {
|
|
73
|
+
global: [
|
|
74
|
+
'~/.cursor/memory.json',
|
|
75
|
+
'~/.cursor/settings.json',
|
|
76
|
+
'~/.cursor/global_context.md'
|
|
77
|
+
],
|
|
78
|
+
project: [
|
|
79
|
+
'.cursor/project.json',
|
|
80
|
+
'.cursor/workspace_context.json',
|
|
81
|
+
'.cursor/memory.md'
|
|
82
|
+
],
|
|
83
|
+
conversations: [
|
|
84
|
+
'.cursor/chat_history.json',
|
|
85
|
+
'.cursor/conversations/*.json'
|
|
86
|
+
],
|
|
87
|
+
config: ['.cursor/settings.json']
|
|
88
|
+
},
|
|
89
|
+
continue: {
|
|
90
|
+
global: [
|
|
91
|
+
'~/.continue/config.json',
|
|
92
|
+
'~/.continue/memory.json',
|
|
93
|
+
'~/.continue/global_context.md'
|
|
94
|
+
],
|
|
95
|
+
project: [
|
|
96
|
+
'.continue/config.json',
|
|
97
|
+
'.continue/memory.md',
|
|
98
|
+
'.continue/project_context.json'
|
|
99
|
+
],
|
|
100
|
+
conversations: [
|
|
101
|
+
'.continue/sessions/*.json',
|
|
102
|
+
'.continue/chat_history.json'
|
|
103
|
+
],
|
|
104
|
+
config: ['.continue/config.json']
|
|
105
|
+
},
|
|
106
|
+
aider: {
|
|
107
|
+
global: [
|
|
108
|
+
'~/.aider.conf.yml',
|
|
109
|
+
'~/.aider/memory.json',
|
|
110
|
+
'~/.aider/global_context.md'
|
|
111
|
+
],
|
|
112
|
+
project: [
|
|
113
|
+
'.aider.conf.yml',
|
|
114
|
+
'.aider/project_memory.md',
|
|
115
|
+
'.aider/context.md'
|
|
116
|
+
],
|
|
117
|
+
conversations: [
|
|
118
|
+
'.aider/chat_history.json',
|
|
119
|
+
'.aider/sessions/*.json'
|
|
120
|
+
],
|
|
121
|
+
config: ['.aider.conf.yml']
|
|
122
|
+
},
|
|
123
|
+
generic: {
|
|
124
|
+
global: [
|
|
125
|
+
'~/.ai/global_memory.md',
|
|
126
|
+
'~/.ai/config.json'
|
|
127
|
+
],
|
|
128
|
+
project: [
|
|
129
|
+
'ai_memory.md',
|
|
130
|
+
'project_context.md',
|
|
131
|
+
'.ai/memory.json',
|
|
132
|
+
'.ai/context.md'
|
|
133
|
+
],
|
|
134
|
+
conversations: [
|
|
135
|
+
'ai_conversations.json',
|
|
136
|
+
'.ai/history.json',
|
|
137
|
+
'chat_history.json'
|
|
138
|
+
],
|
|
139
|
+
config: ['~/.ai/config.json']
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
this.homeDir = os_1.default.homedir();
|
|
143
|
+
this.encryption = new zeroKnowledgeEncryption_1.ZeroKnowledgeEncryption();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Initialize the memory extractor
|
|
147
|
+
*/
|
|
148
|
+
async initialize() {
|
|
149
|
+
await this.encryption.initialize();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Detect all available memory sources for specified CLI tools
|
|
153
|
+
*/
|
|
154
|
+
async detectMemorySources(projectPath = process.cwd(), cliTools = ['all']) {
|
|
155
|
+
const sources = [];
|
|
156
|
+
const toolsToCheck = cliTools.includes('all')
|
|
157
|
+
? Object.keys(this.MEMORY_PATTERNS)
|
|
158
|
+
: cliTools;
|
|
159
|
+
for (const tool of toolsToCheck) {
|
|
160
|
+
const patterns = this.MEMORY_PATTERNS[tool];
|
|
161
|
+
if (!patterns)
|
|
162
|
+
continue;
|
|
163
|
+
// Check global memory sources
|
|
164
|
+
if (patterns.global) {
|
|
165
|
+
for (const pattern of patterns.global) {
|
|
166
|
+
const resolved = this.resolvePath(pattern);
|
|
167
|
+
const exists = await this.fileExists(resolved);
|
|
168
|
+
sources.push({
|
|
169
|
+
cli_tool: tool,
|
|
170
|
+
memory_type: 'global',
|
|
171
|
+
file_path: resolved,
|
|
172
|
+
exists,
|
|
173
|
+
...(exists && await this.getFileStats(resolved))
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Check project memory sources
|
|
178
|
+
if (patterns.project) {
|
|
179
|
+
for (const pattern of patterns.project) {
|
|
180
|
+
const resolved = path_1.default.resolve(projectPath, pattern);
|
|
181
|
+
const exists = await this.fileExists(resolved);
|
|
182
|
+
sources.push({
|
|
183
|
+
cli_tool: tool,
|
|
184
|
+
memory_type: 'project',
|
|
185
|
+
file_path: resolved,
|
|
186
|
+
exists,
|
|
187
|
+
...(exists && await this.getFileStats(resolved))
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Check conversation sources (simplified glob pattern matching)
|
|
192
|
+
if (patterns.conversations) {
|
|
193
|
+
for (const pattern of patterns.conversations) {
|
|
194
|
+
const conversationSources = await this.findConversationFiles(pattern, projectPath);
|
|
195
|
+
sources.push(...conversationSources.map(filePath => ({
|
|
196
|
+
cli_tool: tool,
|
|
197
|
+
memory_type: 'conversation',
|
|
198
|
+
file_path: filePath,
|
|
199
|
+
exists: true
|
|
200
|
+
})));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return sources.filter(source => source.exists);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Extract memory from detected sources
|
|
208
|
+
*/
|
|
209
|
+
async extractMemory(sources, options = {}) {
|
|
210
|
+
const { encryptContent = true, maxFileSizeKB = 500, relevanceThreshold = 0.3 } = options;
|
|
211
|
+
const extractedMemories = [];
|
|
212
|
+
for (const source of sources) {
|
|
213
|
+
if (!source.exists)
|
|
214
|
+
continue;
|
|
215
|
+
try {
|
|
216
|
+
// Skip large files
|
|
217
|
+
if (source.size_bytes && source.size_bytes > maxFileSizeKB * 1024) {
|
|
218
|
+
console.warn(`[Memory Extractor] Skipping large file: ${source.file_path} (${source.size_bytes} bytes)`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const content = await promises_1.default.readFile(source.file_path, 'utf-8');
|
|
222
|
+
const relevanceScore = this.calculateRelevanceScore(content, source);
|
|
223
|
+
// Skip low-relevance content
|
|
224
|
+
if (relevanceScore < relevanceThreshold) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const extractedMemory = {
|
|
228
|
+
source,
|
|
229
|
+
content,
|
|
230
|
+
relevance_score: relevanceScore,
|
|
231
|
+
extraction_method: this.getExtractionMethod(source),
|
|
232
|
+
project_identifier: this.extractProjectIdentifier(source.file_path)
|
|
233
|
+
};
|
|
234
|
+
extractedMemories.push(extractedMemory);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
console.warn(`[Memory Extractor] Failed to read ${source.file_path}:`, error);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return extractedMemories.sort((a, b) => b.relevance_score - a.relevance_score);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Extract recent conversations from CLI tools
|
|
245
|
+
*/
|
|
246
|
+
async extractRecentConversations(sources, options = {}) {
|
|
247
|
+
const { limit = 10, timeRangeHours = 24, queryContext = '' } = options;
|
|
248
|
+
const conversations = [];
|
|
249
|
+
const cutoffTime = new Date(Date.now() - timeRangeHours * 60 * 60 * 1000);
|
|
250
|
+
const conversationSources = sources.filter(s => s.memory_type === 'conversation');
|
|
251
|
+
for (const source of conversationSources) {
|
|
252
|
+
try {
|
|
253
|
+
const content = await promises_1.default.readFile(source.file_path, 'utf-8');
|
|
254
|
+
const extracted = this.parseConversations(content, source.cli_tool, cutoffTime);
|
|
255
|
+
conversations.push(...extracted);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
console.warn(`[Memory Extractor] Failed to parse conversations from ${source.file_path}:`, error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Sort by relevance if query context provided, otherwise by timestamp
|
|
262
|
+
if (queryContext) {
|
|
263
|
+
conversations.forEach(conv => {
|
|
264
|
+
conv.relevance_score = this.calculateConversationRelevance(conv, queryContext);
|
|
265
|
+
});
|
|
266
|
+
conversations.sort((a, b) => (b.relevance_score || 0) - (a.relevance_score || 0));
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
conversations.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
270
|
+
}
|
|
271
|
+
return conversations.slice(0, limit);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Store extracted memory in database with encryption
|
|
275
|
+
*/
|
|
276
|
+
async storeMemory(userId, extractedMemories, useEncryption = true) {
|
|
277
|
+
const supabase = await (0, server_1.createClient)();
|
|
278
|
+
for (const memory of extractedMemories) {
|
|
279
|
+
try {
|
|
280
|
+
let storedContent = memory.content;
|
|
281
|
+
let encryptedContent = null;
|
|
282
|
+
let contentHash;
|
|
283
|
+
if (useEncryption) {
|
|
284
|
+
const encrypted = await this.encryption.encrypt(memory.content);
|
|
285
|
+
encryptedContent = JSON.stringify(encrypted);
|
|
286
|
+
contentHash = await this.encryption.createContentHash(memory.content);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
contentHash = await this.encryption.createContentHash(memory.content);
|
|
290
|
+
}
|
|
291
|
+
const sourcePathHash = await this.encryption.createContentHash(memory.source.file_path);
|
|
292
|
+
// Insert into user_cli_memory_sources
|
|
293
|
+
await supabase
|
|
294
|
+
.from('user_cli_memory_sources')
|
|
295
|
+
.upsert({
|
|
296
|
+
user_id: userId,
|
|
297
|
+
cli_tool: memory.source.cli_tool,
|
|
298
|
+
memory_type: memory.source.memory_type,
|
|
299
|
+
source_path_hash: sourcePathHash,
|
|
300
|
+
encrypted_content: encryptedContent,
|
|
301
|
+
content_hash: contentHash,
|
|
302
|
+
file_count: 1,
|
|
303
|
+
last_modified: memory.source.last_modified?.toISOString(),
|
|
304
|
+
extraction_method: memory.extraction_method,
|
|
305
|
+
relevance_score: memory.relevance_score,
|
|
306
|
+
project_identifier: memory.project_identifier,
|
|
307
|
+
encryption_version: useEncryption ? 1 : 0
|
|
308
|
+
}, {
|
|
309
|
+
onConflict: 'user_id,cli_tool,memory_type,source_path_hash'
|
|
310
|
+
});
|
|
311
|
+
// Log the extraction for audit
|
|
312
|
+
await this.logMemoryAccess(userId, {
|
|
313
|
+
action_type: 'extract',
|
|
314
|
+
cli_tool: memory.source.cli_tool,
|
|
315
|
+
memory_type: memory.source.memory_type,
|
|
316
|
+
sources_processed: 1,
|
|
317
|
+
bytes_processed: memory.content.length,
|
|
318
|
+
privacy_mode: useEncryption
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
console.error(`[Memory Extractor] Failed to store memory for ${memory.source.file_path}:`, error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Store conversations with encryption
|
|
328
|
+
*/
|
|
329
|
+
async storeConversations(userId, conversations, useEncryption = true) {
|
|
330
|
+
const supabase = await (0, server_1.createClient)();
|
|
331
|
+
for (const conv of conversations) {
|
|
332
|
+
try {
|
|
333
|
+
let encryptedUserMessage = null;
|
|
334
|
+
let encryptedAssistantResponse = null;
|
|
335
|
+
let conversationHash;
|
|
336
|
+
const conversationText = `${conv.user_message}\n\n${conv.assistant_response}`;
|
|
337
|
+
if (useEncryption) {
|
|
338
|
+
const encryptedUser = await this.encryption.encrypt(conv.user_message);
|
|
339
|
+
const encryptedAssistant = await this.encryption.encrypt(conv.assistant_response);
|
|
340
|
+
encryptedUserMessage = JSON.stringify(encryptedUser);
|
|
341
|
+
encryptedAssistantResponse = JSON.stringify(encryptedAssistant);
|
|
342
|
+
conversationHash = await this.encryption.createContentHash(conversationText);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
conversationHash = await this.encryption.createContentHash(conversationText);
|
|
346
|
+
}
|
|
347
|
+
// Insert into mcp_conversation_memory (extending existing table)
|
|
348
|
+
await supabase
|
|
349
|
+
.from('mcp_conversation_memory')
|
|
350
|
+
.upsert({
|
|
351
|
+
user_id: userId,
|
|
352
|
+
user_message: useEncryption ? '' : conv.user_message,
|
|
353
|
+
assistant_response: useEncryption ? '' : conv.assistant_response,
|
|
354
|
+
encrypted_user_message: encryptedUserMessage,
|
|
355
|
+
encrypted_assistant_response: encryptedAssistantResponse,
|
|
356
|
+
model_used: conv.model_used || 'unknown',
|
|
357
|
+
tokens_used: conv.tokens_used || 0,
|
|
358
|
+
conversation_hash: conversationHash,
|
|
359
|
+
encryption_version: useEncryption ? 1 : 0,
|
|
360
|
+
privacy_mode: useEncryption,
|
|
361
|
+
created_at: conv.timestamp.toISOString()
|
|
362
|
+
}, {
|
|
363
|
+
onConflict: 'conversation_hash'
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
console.error('[Memory Extractor] Failed to store conversation:', error);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Get comprehensive memory context for injection
|
|
373
|
+
*/
|
|
374
|
+
async getContextForInjection(userId, queryContext = '', options = {}) {
|
|
375
|
+
const supabase = await (0, server_1.createClient)();
|
|
376
|
+
const { maxMemoryKB = 50, maxConversations = 6 } = options;
|
|
377
|
+
let memory = '';
|
|
378
|
+
let conversations = '';
|
|
379
|
+
let totalSize = 0;
|
|
380
|
+
const sourcesUsed = [];
|
|
381
|
+
try {
|
|
382
|
+
// Get relevant memory sources
|
|
383
|
+
const { data: memorySources } = await supabase
|
|
384
|
+
.from('user_cli_memory_sources')
|
|
385
|
+
.select('*')
|
|
386
|
+
.eq('user_id', userId)
|
|
387
|
+
.gte('relevance_score', 0.5)
|
|
388
|
+
.order('relevance_score', { ascending: false })
|
|
389
|
+
.limit(20);
|
|
390
|
+
if (memorySources) {
|
|
391
|
+
for (const source of memorySources) {
|
|
392
|
+
if (totalSize > maxMemoryKB * 1024)
|
|
393
|
+
break;
|
|
394
|
+
let content = '';
|
|
395
|
+
if (source.encrypted_content) {
|
|
396
|
+
try {
|
|
397
|
+
const encryptedData = JSON.parse(source.encrypted_content);
|
|
398
|
+
content = await this.encryption.decrypt(encryptedData);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
console.warn('[Memory Context] Failed to decrypt memory source:', error);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (content) {
|
|
406
|
+
const contextEntry = `[${source.cli_tool}:${source.memory_type}]\n${content}\n\n`;
|
|
407
|
+
if (totalSize + contextEntry.length <= maxMemoryKB * 1024) {
|
|
408
|
+
memory += contextEntry;
|
|
409
|
+
totalSize += contextEntry.length;
|
|
410
|
+
sourcesUsed.push(`${source.cli_tool}:${source.memory_type}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Get relevant conversations
|
|
416
|
+
const { data: recentConversations } = await supabase
|
|
417
|
+
.from('mcp_conversation_memory')
|
|
418
|
+
.select('*')
|
|
419
|
+
.eq('user_id', userId)
|
|
420
|
+
.order('created_at', { ascending: false })
|
|
421
|
+
.limit(maxConversations);
|
|
422
|
+
if (recentConversations) {
|
|
423
|
+
for (const conv of recentConversations.slice(0, maxConversations)) {
|
|
424
|
+
let userMsg = conv.user_message;
|
|
425
|
+
let assistantResp = conv.assistant_response;
|
|
426
|
+
if (conv.encrypted_user_message && conv.encrypted_assistant_response) {
|
|
427
|
+
try {
|
|
428
|
+
const encryptedUser = JSON.parse(conv.encrypted_user_message);
|
|
429
|
+
const encryptedAssistant = JSON.parse(conv.encrypted_assistant_response);
|
|
430
|
+
userMsg = await this.encryption.decrypt(encryptedUser);
|
|
431
|
+
assistantResp = await this.encryption.decrypt(encryptedAssistant);
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
console.warn('[Memory Context] Failed to decrypt conversation:', error);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const convEntry = `[Conversation ${conv.created_at}]\nUser: ${userMsg}\nAssistant: ${assistantResp}\n\n`;
|
|
439
|
+
conversations += convEntry;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
console.error('[Memory Context] Failed to get context:', error);
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
memory: memory.trim(),
|
|
448
|
+
conversations: conversations.trim(),
|
|
449
|
+
totalSizeKB: Math.round(totalSize / 1024),
|
|
450
|
+
sourcesUsed
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
// Private helper methods
|
|
454
|
+
resolvePath(pathPattern) {
|
|
455
|
+
return pathPattern.replace('~', this.homeDir);
|
|
456
|
+
}
|
|
457
|
+
async fileExists(filePath) {
|
|
458
|
+
try {
|
|
459
|
+
await promises_1.default.access(filePath);
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async getFileStats(filePath) {
|
|
467
|
+
const stats = await promises_1.default.stat(filePath);
|
|
468
|
+
return {
|
|
469
|
+
size_bytes: stats.size,
|
|
470
|
+
last_modified: stats.mtime
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
async findConversationFiles(pattern, projectPath) {
|
|
474
|
+
const files = [];
|
|
475
|
+
try {
|
|
476
|
+
const resolved = pattern.includes('~') ? this.resolvePath(pattern) : path_1.default.resolve(projectPath, pattern);
|
|
477
|
+
const dir = path_1.default.dirname(resolved.replace('*', ''));
|
|
478
|
+
if (await this.fileExists(dir)) {
|
|
479
|
+
const entries = await promises_1.default.readdir(dir);
|
|
480
|
+
for (const entry of entries) {
|
|
481
|
+
const fullPath = path_1.default.join(dir, entry);
|
|
482
|
+
const stats = await promises_1.default.stat(fullPath);
|
|
483
|
+
if (stats.isFile() && (entry.endsWith('.json') || entry.endsWith('.jsonl'))) {
|
|
484
|
+
files.push(fullPath);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
// Silently handle directory access errors
|
|
491
|
+
}
|
|
492
|
+
return files;
|
|
493
|
+
}
|
|
494
|
+
calculateRelevanceScore(content, source) {
|
|
495
|
+
let score = 0.5; // Base score
|
|
496
|
+
// Boost for project-level memory
|
|
497
|
+
if (source.memory_type === 'project')
|
|
498
|
+
score += 0.2;
|
|
499
|
+
// Boost for recent conversations
|
|
500
|
+
if (source.memory_type === 'conversation')
|
|
501
|
+
score += 0.1;
|
|
502
|
+
// Boost for certain CLI tools
|
|
503
|
+
if (['claude_code', 'cursor', 'cline'].includes(source.cli_tool))
|
|
504
|
+
score += 0.1;
|
|
505
|
+
// Content quality indicators
|
|
506
|
+
if (content.includes('TODO') || content.includes('FIXME'))
|
|
507
|
+
score += 0.1;
|
|
508
|
+
if (content.includes('pattern') || content.includes('decision'))
|
|
509
|
+
score += 0.1;
|
|
510
|
+
if (content.length > 1000 && content.length < 10000)
|
|
511
|
+
score += 0.1;
|
|
512
|
+
return Math.min(score, 1.0);
|
|
513
|
+
}
|
|
514
|
+
getExtractionMethod(source) {
|
|
515
|
+
const ext = path_1.default.extname(source.file_path).toLowerCase();
|
|
516
|
+
switch (ext) {
|
|
517
|
+
case '.md': return 'markdown_parse';
|
|
518
|
+
case '.json': return 'json_parse';
|
|
519
|
+
case '.jsonl': return 'jsonl_parse';
|
|
520
|
+
case '.yml':
|
|
521
|
+
case '.yaml': return 'yaml_parse';
|
|
522
|
+
default: return 'text_parse';
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
extractProjectIdentifier(filePath) {
|
|
526
|
+
const parts = filePath.split(path_1.default.sep);
|
|
527
|
+
const projectIndex = parts.findIndex(part => !part.startsWith('.') && part !== 'home' && part !== 'Users');
|
|
528
|
+
return projectIndex >= 0 ? parts.slice(projectIndex, projectIndex + 2).join('/') : 'unknown';
|
|
529
|
+
}
|
|
530
|
+
parseConversations(content, cliTool, cutoffTime) {
|
|
531
|
+
const conversations = [];
|
|
532
|
+
try {
|
|
533
|
+
if (cliTool === 'claude_code' && content.includes('"type":"message"')) {
|
|
534
|
+
// Parse Claude Code JSONL format
|
|
535
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
536
|
+
for (const line of lines) {
|
|
537
|
+
try {
|
|
538
|
+
const entry = JSON.parse(line);
|
|
539
|
+
if (entry.type === 'message' && entry.timestamp) {
|
|
540
|
+
const timestamp = new Date(entry.timestamp);
|
|
541
|
+
if (timestamp > cutoffTime) {
|
|
542
|
+
conversations.push({
|
|
543
|
+
timestamp,
|
|
544
|
+
user_message: entry.content || '',
|
|
545
|
+
assistant_response: entry.response || '',
|
|
546
|
+
model_used: entry.model,
|
|
547
|
+
tokens_used: entry.tokens
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch { }
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else if (content.startsWith('[') || content.startsWith('{')) {
|
|
556
|
+
// Parse JSON format conversations
|
|
557
|
+
const data = JSON.parse(content);
|
|
558
|
+
const messages = Array.isArray(data) ? data : data.messages || [];
|
|
559
|
+
for (let i = 0; i < messages.length - 1; i += 2) {
|
|
560
|
+
const userMsg = messages[i];
|
|
561
|
+
const assistantMsg = messages[i + 1];
|
|
562
|
+
if (userMsg && assistantMsg) {
|
|
563
|
+
const timestamp = new Date(userMsg.timestamp || assistantMsg.timestamp || Date.now());
|
|
564
|
+
if (timestamp > cutoffTime) {
|
|
565
|
+
conversations.push({
|
|
566
|
+
timestamp,
|
|
567
|
+
user_message: userMsg.content || userMsg.text || '',
|
|
568
|
+
assistant_response: assistantMsg.content || assistantMsg.text || '',
|
|
569
|
+
model_used: assistantMsg.model,
|
|
570
|
+
tokens_used: assistantMsg.tokens
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
console.warn(`[Memory Extractor] Failed to parse conversations for ${cliTool}:`, error);
|
|
579
|
+
}
|
|
580
|
+
return conversations;
|
|
581
|
+
}
|
|
582
|
+
calculateConversationRelevance(conversation, queryContext) {
|
|
583
|
+
if (!queryContext)
|
|
584
|
+
return 0.5;
|
|
585
|
+
const text = `${conversation.user_message} ${conversation.assistant_response}`.toLowerCase();
|
|
586
|
+
const queryWords = queryContext.toLowerCase().split(/\s+/);
|
|
587
|
+
const matches = queryWords.filter(word => text.includes(word)).length;
|
|
588
|
+
return Math.min(matches / queryWords.length, 1.0);
|
|
589
|
+
}
|
|
590
|
+
async logMemoryAccess(userId, logData) {
|
|
591
|
+
const supabase = await (0, server_1.createClient)();
|
|
592
|
+
try {
|
|
593
|
+
await supabase
|
|
594
|
+
.from('user_memory_audit_log')
|
|
595
|
+
.insert({
|
|
596
|
+
user_id: userId,
|
|
597
|
+
...logData
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
console.error('[Memory Extractor] Failed to log memory access:', error);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
exports.UniversalMemoryExtractor = UniversalMemoryExtractor;
|
|
606
|
+
// Export singleton instance
|
|
607
|
+
exports.universalMemoryExtractor = new UniversalMemoryExtractor();
|