polydev-ai 1.2.2 → 1.2.4
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.js +11 -0
- package/lib/universalMemoryExtractor.js +607 -0
- package/lib/zeroKnowledgeEncryption.js +289 -0
- package/mcp/manifest.json +9 -9
- package/mcp/server.js +50 -21
- package/mcp/stdio-wrapper.js +7 -2
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Polydev AI Website
|
|
2
2
|
|
|
3
|
-
Advanced Model Context Protocol platform with comprehensive multi-LLM
|
|
3
|
+
Advanced Model Context Protocol platform with comprehensive multi-LLM integrations, subscription-based CLI access, OAuth bridges, and advanced tooling for AI development.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
package/lib/cliManager.js
CHANGED
|
@@ -350,6 +350,12 @@ This is a known issue with @google/gemini-cli@0.3.4 and older Node.js versions.`
|
|
|
350
350
|
if (timeoutMs === null) {
|
|
351
351
|
timeoutMs = providerId === 'claude_code' ? 60000 : 30000; // 60s for Claude Code, 30s for others
|
|
352
352
|
}
|
|
353
|
+
|
|
354
|
+
// Ensure timeoutMs is valid (not undefined, null, Infinity, or negative)
|
|
355
|
+
if (!timeoutMs || timeoutMs === Infinity || timeoutMs < 1 || timeoutMs > 300000) {
|
|
356
|
+
timeoutMs = 30000 // Default to 30 seconds
|
|
357
|
+
}
|
|
358
|
+
|
|
353
359
|
const startTime = Date.now();
|
|
354
360
|
|
|
355
361
|
try {
|
|
@@ -434,6 +440,11 @@ This is a known issue with @google/gemini-cli@0.3.4 and older Node.js versions.`
|
|
|
434
440
|
}
|
|
435
441
|
|
|
436
442
|
async executeCliCommand(command, args, mode = 'args', timeoutMs = 30000, stdinInput) {
|
|
443
|
+
// Ensure timeoutMs is valid (not undefined, null, Infinity, or negative)
|
|
444
|
+
if (!timeoutMs || timeoutMs === Infinity || timeoutMs < 1 || timeoutMs > 300000) {
|
|
445
|
+
timeoutMs = 30000 // Default to 30 seconds
|
|
446
|
+
}
|
|
447
|
+
|
|
437
448
|
return new Promise((resolve, reject) => {
|
|
438
449
|
if (process.env.POLYDEV_CLI_DEBUG) {
|
|
439
450
|
console.log(`[CLI Debug] Executing: ${command} ${args.join(' ')} (mode: ${mode})`);
|
|
@@ -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();
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Zero-Knowledge Client-Side Encryption System
|
|
4
|
+
* All encryption keys never leave the user's browser
|
|
5
|
+
* Enterprise-grade AES-256-GCM encryption with automatic key rotation
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.zkEncryption = exports.ZeroKnowledgeEncryption = void 0;
|
|
9
|
+
class ZeroKnowledgeEncryption {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.db = null;
|
|
12
|
+
this.activeKey = null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Initialize the encryption system
|
|
16
|
+
*/
|
|
17
|
+
async initialize() {
|
|
18
|
+
await this.initializeIndexedDB();
|
|
19
|
+
await this.loadOrCreateActiveKey();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Initialize IndexedDB for local key storage
|
|
23
|
+
*/
|
|
24
|
+
async initializeIndexedDB() {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const request = indexedDB.open(ZeroKnowledgeEncryption.DB_NAME, ZeroKnowledgeEncryption.DB_VERSION);
|
|
27
|
+
request.onerror = () => reject(request.error);
|
|
28
|
+
request.onsuccess = () => {
|
|
29
|
+
this.db = request.result;
|
|
30
|
+
resolve();
|
|
31
|
+
};
|
|
32
|
+
request.onupgradeneeded = (event) => {
|
|
33
|
+
const db = event.target.result;
|
|
34
|
+
// Store for encryption keys (never synchronized)
|
|
35
|
+
if (!db.objectStoreNames.contains('keys')) {
|
|
36
|
+
const keyStore = db.createObjectStore('keys', { keyPath: 'keyId' });
|
|
37
|
+
keyStore.createIndex('created', 'created', { unique: false });
|
|
38
|
+
}
|
|
39
|
+
// Cache for encrypted data
|
|
40
|
+
if (!db.objectStoreNames.contains('cache')) {
|
|
41
|
+
const cacheStore = db.createObjectStore('cache', { keyPath: 'id' });
|
|
42
|
+
cacheStore.createIndex('expires', 'expires', { unique: false });
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Generate new AES-256-GCM encryption key
|
|
49
|
+
*/
|
|
50
|
+
async generateKey() {
|
|
51
|
+
const keyId = this.generateKeyId();
|
|
52
|
+
const key = await window.crypto.subtle.generateKey({
|
|
53
|
+
name: ZeroKnowledgeEncryption.ALGORITHM,
|
|
54
|
+
length: ZeroKnowledgeEncryption.KEY_LENGTH,
|
|
55
|
+
}, true, // extractable for storage
|
|
56
|
+
['encrypt', 'decrypt']);
|
|
57
|
+
const now = new Date();
|
|
58
|
+
const rotationDue = new Date(now.getTime() + (ZeroKnowledgeEncryption.KEY_ROTATION_DAYS * 24 * 60 * 60 * 1000));
|
|
59
|
+
return {
|
|
60
|
+
keyId,
|
|
61
|
+
key,
|
|
62
|
+
created: now,
|
|
63
|
+
rotationDue
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Load active key or create new one
|
|
68
|
+
*/
|
|
69
|
+
async loadOrCreateActiveKey() {
|
|
70
|
+
if (!this.db)
|
|
71
|
+
throw new Error('Database not initialized');
|
|
72
|
+
const transaction = this.db.transaction(['keys'], 'readonly');
|
|
73
|
+
const store = transaction.objectStore('keys');
|
|
74
|
+
// Get the most recent key
|
|
75
|
+
const index = store.index('created');
|
|
76
|
+
const request = index.openCursor(null, 'prev');
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
request.onsuccess = async (event) => {
|
|
79
|
+
const cursor = event.target.result;
|
|
80
|
+
if (cursor) {
|
|
81
|
+
const keyData = cursor.value;
|
|
82
|
+
// Check if key needs rotation
|
|
83
|
+
if (new Date() > new Date(keyData.rotationDue)) {
|
|
84
|
+
console.log('[ZK Encryption] Key rotation needed, generating new key');
|
|
85
|
+
await this.createAndStoreKey();
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Import existing key
|
|
89
|
+
const keyBuffer = new Uint8Array(keyData.keyBuffer);
|
|
90
|
+
this.activeKey = {
|
|
91
|
+
keyId: keyData.keyId,
|
|
92
|
+
key: await window.crypto.subtle.importKey('raw', keyBuffer, { name: ZeroKnowledgeEncryption.ALGORITHM }, true, ['encrypt', 'decrypt']),
|
|
93
|
+
created: new Date(keyData.created),
|
|
94
|
+
rotationDue: new Date(keyData.rotationDue)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// No keys exist, create first one
|
|
100
|
+
await this.createAndStoreKey();
|
|
101
|
+
}
|
|
102
|
+
resolve();
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Create and store new key
|
|
108
|
+
*/
|
|
109
|
+
async createAndStoreKey() {
|
|
110
|
+
this.activeKey = await this.generateKey();
|
|
111
|
+
await this.storeKey(this.activeKey);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Store key in IndexedDB
|
|
115
|
+
*/
|
|
116
|
+
async storeKey(userKey) {
|
|
117
|
+
if (!this.db)
|
|
118
|
+
throw new Error('Database not initialized');
|
|
119
|
+
const keyBuffer = await window.crypto.subtle.exportKey('raw', userKey.key);
|
|
120
|
+
const transaction = this.db.transaction(['keys'], 'readwrite');
|
|
121
|
+
const store = transaction.objectStore('keys');
|
|
122
|
+
await store.put({
|
|
123
|
+
keyId: userKey.keyId,
|
|
124
|
+
keyBuffer: Array.from(new Uint8Array(keyBuffer)),
|
|
125
|
+
created: userKey.created.toISOString(),
|
|
126
|
+
rotationDue: userKey.rotationDue.toISOString()
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Encrypt sensitive data
|
|
131
|
+
*/
|
|
132
|
+
async encrypt(plaintext) {
|
|
133
|
+
if (!this.activeKey)
|
|
134
|
+
throw new Error('Encryption key not available');
|
|
135
|
+
const encoder = new TextEncoder();
|
|
136
|
+
const data = encoder.encode(plaintext);
|
|
137
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(ZeroKnowledgeEncryption.IV_LENGTH));
|
|
138
|
+
const encrypted = await window.crypto.subtle.encrypt({
|
|
139
|
+
name: ZeroKnowledgeEncryption.ALGORITHM,
|
|
140
|
+
iv: iv,
|
|
141
|
+
}, this.activeKey.key, data);
|
|
142
|
+
const encryptedArray = new Uint8Array(encrypted);
|
|
143
|
+
const ciphertext = encryptedArray.slice(0, -ZeroKnowledgeEncryption.AUTH_TAG_LENGTH);
|
|
144
|
+
const authTag = encryptedArray.slice(-ZeroKnowledgeEncryption.AUTH_TAG_LENGTH);
|
|
145
|
+
return {
|
|
146
|
+
ciphertext: this.arrayBufferToBase64(ciphertext.buffer),
|
|
147
|
+
iv: this.arrayBufferToBase64(iv.buffer),
|
|
148
|
+
authTag: this.arrayBufferToBase64(authTag.buffer),
|
|
149
|
+
keyId: this.activeKey.keyId,
|
|
150
|
+
version: 1
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Decrypt sensitive data
|
|
155
|
+
*/
|
|
156
|
+
async decrypt(encryptedData) {
|
|
157
|
+
const key = await this.getKeyById(encryptedData.keyId);
|
|
158
|
+
if (!key)
|
|
159
|
+
throw new Error(`Encryption key ${encryptedData.keyId} not found`);
|
|
160
|
+
const ciphertext = this.base64ToArrayBuffer(encryptedData.ciphertext);
|
|
161
|
+
const iv = this.base64ToArrayBuffer(encryptedData.iv);
|
|
162
|
+
const authTag = this.base64ToArrayBuffer(encryptedData.authTag);
|
|
163
|
+
// Combine ciphertext and auth tag for Web Crypto API
|
|
164
|
+
const combined = new Uint8Array(ciphertext.byteLength + authTag.byteLength);
|
|
165
|
+
combined.set(new Uint8Array(ciphertext));
|
|
166
|
+
combined.set(new Uint8Array(authTag), ciphertext.byteLength);
|
|
167
|
+
const decrypted = await window.crypto.subtle.decrypt({
|
|
168
|
+
name: ZeroKnowledgeEncryption.ALGORITHM,
|
|
169
|
+
iv: new Uint8Array(iv),
|
|
170
|
+
}, key, combined);
|
|
171
|
+
const decoder = new TextDecoder();
|
|
172
|
+
return decoder.decode(decrypted);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get encryption key by ID
|
|
176
|
+
*/
|
|
177
|
+
async getKeyById(keyId) {
|
|
178
|
+
if (!this.db)
|
|
179
|
+
return null;
|
|
180
|
+
const transaction = this.db.transaction(['keys'], 'readonly');
|
|
181
|
+
const store = transaction.objectStore('keys');
|
|
182
|
+
const request = store.get(keyId);
|
|
183
|
+
return new Promise((resolve) => {
|
|
184
|
+
request.onsuccess = async () => {
|
|
185
|
+
if (request.result) {
|
|
186
|
+
const keyBuffer = new Uint8Array(request.result.keyBuffer);
|
|
187
|
+
const key = await window.crypto.subtle.importKey('raw', keyBuffer, { name: ZeroKnowledgeEncryption.ALGORITHM }, true, ['encrypt', 'decrypt']);
|
|
188
|
+
resolve(key);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
resolve(null);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
request.onerror = () => resolve(null);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Create SHA-256 hash for content deduplication
|
|
199
|
+
*/
|
|
200
|
+
async createContentHash(content) {
|
|
201
|
+
const encoder = new TextEncoder();
|
|
202
|
+
const data = encoder.encode(content);
|
|
203
|
+
const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
|
|
204
|
+
return this.arrayBufferToHex(hashBuffer);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Check if key rotation is needed
|
|
208
|
+
*/
|
|
209
|
+
isRotationDue() {
|
|
210
|
+
return this.activeKey ? new Date() > this.activeKey.rotationDue : true;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Force key rotation
|
|
214
|
+
*/
|
|
215
|
+
async rotateKeys() {
|
|
216
|
+
await this.createAndStoreKey();
|
|
217
|
+
console.log('[ZK Encryption] Key rotation completed');
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Generate unique key ID
|
|
221
|
+
*/
|
|
222
|
+
generateKeyId() {
|
|
223
|
+
const timestamp = Date.now().toString(36);
|
|
224
|
+
const random = Math.random().toString(36).substr(2, 9);
|
|
225
|
+
return `zk_${timestamp}_${random}`;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Utility: ArrayBuffer to Base64
|
|
229
|
+
*/
|
|
230
|
+
arrayBufferToBase64(buffer) {
|
|
231
|
+
const bytes = new Uint8Array(buffer);
|
|
232
|
+
let binary = '';
|
|
233
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
234
|
+
binary += String.fromCharCode(bytes[i]);
|
|
235
|
+
}
|
|
236
|
+
return btoa(binary);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Utility: Base64 to ArrayBuffer
|
|
240
|
+
*/
|
|
241
|
+
base64ToArrayBuffer(base64) {
|
|
242
|
+
const binary = atob(base64);
|
|
243
|
+
const bytes = new Uint8Array(binary.length);
|
|
244
|
+
for (let i = 0; i < binary.length; i++) {
|
|
245
|
+
bytes[i] = binary.charCodeAt(i);
|
|
246
|
+
}
|
|
247
|
+
return bytes.buffer;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Utility: ArrayBuffer to Hex
|
|
251
|
+
*/
|
|
252
|
+
arrayBufferToHex(buffer) {
|
|
253
|
+
const bytes = new Uint8Array(buffer);
|
|
254
|
+
return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Get encryption status for dashboard
|
|
258
|
+
*/
|
|
259
|
+
getEncryptionStatus() {
|
|
260
|
+
return {
|
|
261
|
+
keyId: this.activeKey?.keyId || null,
|
|
262
|
+
created: this.activeKey?.created || null,
|
|
263
|
+
rotationDue: this.activeKey?.rotationDue || null,
|
|
264
|
+
rotationNeeded: this.isRotationDue()
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Clear all keys (for user logout/reset)
|
|
269
|
+
*/
|
|
270
|
+
async clearAllKeys() {
|
|
271
|
+
if (!this.db)
|
|
272
|
+
return;
|
|
273
|
+
const transaction = this.db.transaction(['keys', 'cache'], 'readwrite');
|
|
274
|
+
await transaction.objectStore('keys').clear();
|
|
275
|
+
await transaction.objectStore('cache').clear();
|
|
276
|
+
this.activeKey = null;
|
|
277
|
+
console.log('[ZK Encryption] All keys cleared');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
exports.ZeroKnowledgeEncryption = ZeroKnowledgeEncryption;
|
|
281
|
+
ZeroKnowledgeEncryption.ALGORITHM = 'AES-GCM';
|
|
282
|
+
ZeroKnowledgeEncryption.KEY_LENGTH = 256;
|
|
283
|
+
ZeroKnowledgeEncryption.IV_LENGTH = 12;
|
|
284
|
+
ZeroKnowledgeEncryption.AUTH_TAG_LENGTH = 16;
|
|
285
|
+
ZeroKnowledgeEncryption.KEY_ROTATION_DAYS = 30;
|
|
286
|
+
ZeroKnowledgeEncryption.DB_NAME = 'PolydevZKMemory';
|
|
287
|
+
ZeroKnowledgeEncryption.DB_VERSION = 1;
|
|
288
|
+
// Export singleton instance
|
|
289
|
+
exports.zkEncryption = new ZeroKnowledgeEncryption();
|
package/mcp/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polydev-perspectives",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Agentic workflow assistant - get diverse perspectives from multiple LLMs when stuck or need enhanced reasoning",
|
|
5
5
|
"author": "Polydev AI",
|
|
6
6
|
"license": "MIT",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
]
|
|
199
199
|
},
|
|
200
200
|
{
|
|
201
|
-
"name": "
|
|
201
|
+
"name": "force_cli_detection",
|
|
202
202
|
"description": "Force detection and status update for CLI tools (Claude Code, Codex CLI, Gemini CLI). Updates local cache and reports status to MCP server via Supabase.",
|
|
203
203
|
"inputSchema": {
|
|
204
204
|
"type": "object",
|
|
@@ -290,7 +290,7 @@
|
|
|
290
290
|
]
|
|
291
291
|
},
|
|
292
292
|
{
|
|
293
|
-
"name": "
|
|
293
|
+
"name": "get_cli_status",
|
|
294
294
|
"description": "Get current CLI status with caching support. Returns cached results if available and fresh, otherwise performs new detection.",
|
|
295
295
|
"inputSchema": {
|
|
296
296
|
"type": "object",
|
|
@@ -347,7 +347,7 @@
|
|
|
347
347
|
}
|
|
348
348
|
},
|
|
349
349
|
{
|
|
350
|
-
"name": "
|
|
350
|
+
"name": "send_cli_prompt",
|
|
351
351
|
"description": "Send a prompt to a CLI provider and get response. Supports both stdin and argument modes for maximum compatibility.",
|
|
352
352
|
"inputSchema": {
|
|
353
353
|
"type": "object",
|
|
@@ -443,7 +443,7 @@
|
|
|
443
443
|
]
|
|
444
444
|
},
|
|
445
445
|
{
|
|
446
|
-
"name": "
|
|
446
|
+
"name": "detect_memory_sources",
|
|
447
447
|
"description": "Detect all available memory sources across CLI tools (Claude Code, Cline, Codex, Cursor, Continue, Aider). Returns file paths for global memory, project memory, and recent conversations.",
|
|
448
448
|
"inputSchema": {
|
|
449
449
|
"type": "object",
|
|
@@ -503,7 +503,7 @@
|
|
|
503
503
|
}
|
|
504
504
|
},
|
|
505
505
|
{
|
|
506
|
-
"name": "
|
|
506
|
+
"name": "extract_memory",
|
|
507
507
|
"description": "Extract and optionally encrypt memory content from detected CLI tool sources. Provides TF-IDF relevance scoring and content analysis.",
|
|
508
508
|
"inputSchema": {
|
|
509
509
|
"type": "object",
|
|
@@ -550,7 +550,7 @@
|
|
|
550
550
|
}
|
|
551
551
|
},
|
|
552
552
|
{
|
|
553
|
-
"name": "
|
|
553
|
+
"name": "get_recent_conversations",
|
|
554
554
|
"description": "Get recent conversations from CLI tools with TF-IDF relevance scoring against query context. Supports encrypted storage and retrieval.",
|
|
555
555
|
"inputSchema": {
|
|
556
556
|
"type": "object",
|
|
@@ -590,7 +590,7 @@
|
|
|
590
590
|
}
|
|
591
591
|
},
|
|
592
592
|
{
|
|
593
|
-
"name": "
|
|
593
|
+
"name": "get_memory_context",
|
|
594
594
|
"description": "Get formatted memory and conversation context for injection into prompts. Automatically handles decryption and relevance scoring.",
|
|
595
595
|
"inputSchema": {
|
|
596
596
|
"type": "object",
|
|
@@ -631,7 +631,7 @@
|
|
|
631
631
|
}
|
|
632
632
|
},
|
|
633
633
|
{
|
|
634
|
-
"name": "
|
|
634
|
+
"name": "manage_memory_preferences",
|
|
635
635
|
"description": "Configure memory extraction and privacy preferences. Control which CLI tools and memory types are enabled, encryption settings, and context injection behavior.",
|
|
636
636
|
"inputSchema": {
|
|
637
637
|
"type": "object",
|
package/mcp/server.js
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Register ts-node for TypeScript support
|
|
4
|
+
try {
|
|
5
|
+
require('ts-node/register');
|
|
6
|
+
} catch (e) {
|
|
7
|
+
// ts-node not available, proceed without it
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
const fs = require('fs');
|
|
4
11
|
const path = require('path');
|
|
5
12
|
const CLIManager = require('../lib/cliManager').default;
|
|
6
|
-
|
|
13
|
+
|
|
14
|
+
let UniversalMemoryExtractor;
|
|
15
|
+
try {
|
|
16
|
+
UniversalMemoryExtractor = require('../src/lib/universalMemoryExtractor').UniversalMemoryExtractor;
|
|
17
|
+
} catch (e) {
|
|
18
|
+
// Fallback if TypeScript module is not available
|
|
19
|
+
console.warn('UniversalMemoryExtractor not available, memory features will be disabled');
|
|
20
|
+
UniversalMemoryExtractor = class {
|
|
21
|
+
async detectMemorySources() { return []; }
|
|
22
|
+
async extractMemory() { return {}; }
|
|
23
|
+
async getRecentConversations() { return []; }
|
|
24
|
+
async getRelevantContext() { return ''; }
|
|
25
|
+
async getPreferences() { return {}; }
|
|
26
|
+
async updatePreferences() { return {}; }
|
|
27
|
+
async resetPreferences() { return {}; }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
7
30
|
|
|
8
31
|
class MCPServer {
|
|
9
32
|
constructor() {
|
|
@@ -126,35 +149,35 @@ class MCPServer {
|
|
|
126
149
|
result = await this.callPerspectivesAPI(args);
|
|
127
150
|
break;
|
|
128
151
|
|
|
129
|
-
case '
|
|
152
|
+
case 'force_cli_detection':
|
|
130
153
|
result = await this.forceCliDetection(args);
|
|
131
154
|
break;
|
|
132
155
|
|
|
133
|
-
case '
|
|
156
|
+
case 'get_cli_status':
|
|
134
157
|
result = await this.getCliStatus(args);
|
|
135
158
|
break;
|
|
136
159
|
|
|
137
|
-
case '
|
|
160
|
+
case 'send_cli_prompt':
|
|
138
161
|
result = await this.sendCliPrompt(args);
|
|
139
162
|
break;
|
|
140
163
|
|
|
141
|
-
case '
|
|
164
|
+
case 'detect_memory_sources':
|
|
142
165
|
result = await this.detectMemorySources(args);
|
|
143
166
|
break;
|
|
144
167
|
|
|
145
|
-
case '
|
|
168
|
+
case 'extract_memory':
|
|
146
169
|
result = await this.extractMemory(args);
|
|
147
170
|
break;
|
|
148
171
|
|
|
149
|
-
case '
|
|
172
|
+
case 'get_recent_conversations':
|
|
150
173
|
result = await this.getRecentConversations(args);
|
|
151
174
|
break;
|
|
152
175
|
|
|
153
|
-
case '
|
|
176
|
+
case 'get_memory_context':
|
|
154
177
|
result = await this.getMemoryContext(args);
|
|
155
178
|
break;
|
|
156
179
|
|
|
157
|
-
case '
|
|
180
|
+
case 'manage_memory_preferences':
|
|
158
181
|
result = await this.manageMemoryPreferences(args);
|
|
159
182
|
break;
|
|
160
183
|
|
|
@@ -298,20 +321,20 @@ class MCPServer {
|
|
|
298
321
|
case 'get_perspectives':
|
|
299
322
|
return this.formatPerspectivesResponse(result);
|
|
300
323
|
|
|
301
|
-
case '
|
|
324
|
+
case 'force_cli_detection':
|
|
302
325
|
return this.formatCliDetectionResponse(result);
|
|
303
|
-
|
|
304
|
-
case '
|
|
326
|
+
|
|
327
|
+
case 'get_cli_status':
|
|
305
328
|
return this.formatCliStatusResponse(result);
|
|
306
|
-
|
|
307
|
-
case '
|
|
329
|
+
|
|
330
|
+
case 'send_cli_prompt':
|
|
308
331
|
return this.formatCliPromptResponse(result);
|
|
309
|
-
|
|
310
|
-
case '
|
|
311
|
-
case '
|
|
312
|
-
case '
|
|
313
|
-
case '
|
|
314
|
-
case '
|
|
332
|
+
|
|
333
|
+
case 'detect_memory_sources':
|
|
334
|
+
case 'extract_memory':
|
|
335
|
+
case 'get_recent_conversations':
|
|
336
|
+
case 'get_memory_context':
|
|
337
|
+
case 'manage_memory_preferences':
|
|
315
338
|
return this.formatMemoryResponse(result);
|
|
316
339
|
|
|
317
340
|
default:
|
|
@@ -557,6 +580,12 @@ class MCPServer {
|
|
|
557
580
|
try {
|
|
558
581
|
const { provider_id, prompt, mode = 'args', timeout_ms = 30000, user_id } = args;
|
|
559
582
|
|
|
583
|
+
// Ensure timeout_ms is valid (not undefined, null, Infinity, or negative)
|
|
584
|
+
let validTimeout = timeout_ms;
|
|
585
|
+
if (!validTimeout || validTimeout === Infinity || validTimeout < 1 || validTimeout > 300000) {
|
|
586
|
+
validTimeout = 30000 // Default to 30 seconds
|
|
587
|
+
}
|
|
588
|
+
|
|
560
589
|
if (!provider_id || !prompt) {
|
|
561
590
|
throw new Error('provider_id and prompt are required');
|
|
562
591
|
}
|
|
@@ -566,7 +595,7 @@ class MCPServer {
|
|
|
566
595
|
provider_id,
|
|
567
596
|
prompt,
|
|
568
597
|
mode,
|
|
569
|
-
|
|
598
|
+
validTimeout
|
|
570
599
|
);
|
|
571
600
|
|
|
572
601
|
// CLI usage is tracked locally - no database integration needed
|
package/mcp/stdio-wrapper.js
CHANGED
|
@@ -279,6 +279,11 @@ class StdioMCPWrapper {
|
|
|
279
279
|
try {
|
|
280
280
|
let { provider_id, prompt, mode = 'args', timeout_ms = 30000 } = args;
|
|
281
281
|
|
|
282
|
+
// Ensure timeout_ms is valid (not undefined, null, Infinity, or negative)
|
|
283
|
+
if (!timeout_ms || timeout_ms === Infinity || timeout_ms < 1 || timeout_ms > 300000) {
|
|
284
|
+
timeout_ms = 30000 // Default to 30 seconds
|
|
285
|
+
}
|
|
286
|
+
|
|
282
287
|
if (!prompt) {
|
|
283
288
|
throw new Error('prompt is required');
|
|
284
289
|
}
|
|
@@ -289,8 +294,8 @@ class StdioMCPWrapper {
|
|
|
289
294
|
console.error(`[Stdio Wrapper] Auto-selected provider: ${provider_id}`);
|
|
290
295
|
}
|
|
291
296
|
|
|
292
|
-
// Use
|
|
293
|
-
const gracefulTimeout = Math.min(timeout_ms,
|
|
297
|
+
// Use reasonable timeout for CLI responses (15 seconds instead of 5)
|
|
298
|
+
const gracefulTimeout = Math.min(timeout_ms, 15000);
|
|
294
299
|
|
|
295
300
|
// Start both operations concurrently for better performance
|
|
296
301
|
const [localResult, perspectivesResult] = await Promise.allSettled([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polydev-ai",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Agentic workflow assistant with CLI integration - get diverse perspectives from multiple LLMs when stuck or need enhanced reasoning",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"next": "15.0.0",
|
|
62
62
|
"polydev-ai": "^1.2.0",
|
|
63
63
|
"posthog-js": "^1.157.2",
|
|
64
|
+
"prismjs": "^1.30.0",
|
|
64
65
|
"react": "^18.3.1",
|
|
65
66
|
"react-dom": "^18.3.1",
|
|
66
67
|
"resend": "^6.0.2",
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"sonner": "^2.0.7",
|
|
69
70
|
"stripe": "^18.5.0",
|
|
70
71
|
"tailwind-merge": "^3.3.1",
|
|
72
|
+
"ts-node": "^10.9.2",
|
|
71
73
|
"which": "^5.0.0"
|
|
72
74
|
},
|
|
73
75
|
"devDependencies": {
|