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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Polydev AI Website
2
2
 
3
- Advanced Model Context Protocol platform with comprehensive multi-LLM integration, subscription-based CLI access, OAuth bridges, and advanced tooling for AI development.
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.0.0",
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": "polydev.force_cli_detection",
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": "polydev.get_cli_status",
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": "polydev.send_cli_prompt",
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": "polydev.detect_memory_sources",
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": "polydev.extract_memory",
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": "polydev.get_recent_conversations",
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": "polydev.get_memory_context",
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": "polydev.manage_memory_preferences",
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
- const UniversalMemoryExtractor = require('../src/lib/universalMemoryExtractor').UniversalMemoryExtractor;
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 'polydev.force_cli_detection':
152
+ case 'force_cli_detection':
130
153
  result = await this.forceCliDetection(args);
131
154
  break;
132
155
 
133
- case 'polydev.get_cli_status':
156
+ case 'get_cli_status':
134
157
  result = await this.getCliStatus(args);
135
158
  break;
136
159
 
137
- case 'polydev.send_cli_prompt':
160
+ case 'send_cli_prompt':
138
161
  result = await this.sendCliPrompt(args);
139
162
  break;
140
163
 
141
- case 'polydev.detect_memory_sources':
164
+ case 'detect_memory_sources':
142
165
  result = await this.detectMemorySources(args);
143
166
  break;
144
167
 
145
- case 'polydev.extract_memory':
168
+ case 'extract_memory':
146
169
  result = await this.extractMemory(args);
147
170
  break;
148
171
 
149
- case 'polydev.get_recent_conversations':
172
+ case 'get_recent_conversations':
150
173
  result = await this.getRecentConversations(args);
151
174
  break;
152
175
 
153
- case 'polydev.get_memory_context':
176
+ case 'get_memory_context':
154
177
  result = await this.getMemoryContext(args);
155
178
  break;
156
179
 
157
- case 'polydev.manage_memory_preferences':
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 'polydev.force_cli_detection':
324
+ case 'force_cli_detection':
302
325
  return this.formatCliDetectionResponse(result);
303
-
304
- case 'polydev.get_cli_status':
326
+
327
+ case 'get_cli_status':
305
328
  return this.formatCliStatusResponse(result);
306
-
307
- case 'polydev.send_cli_prompt':
329
+
330
+ case 'send_cli_prompt':
308
331
  return this.formatCliPromptResponse(result);
309
-
310
- case 'polydev.detect_memory_sources':
311
- case 'polydev.extract_memory':
312
- case 'polydev.get_recent_conversations':
313
- case 'polydev.get_memory_context':
314
- case 'polydev.manage_memory_preferences':
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
- timeout_ms
598
+ validTimeout
570
599
  );
571
600
 
572
601
  // CLI usage is tracked locally - no database integration needed
@@ -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 shorter timeout for faster fallback (5 seconds instead of 30)
293
- const gracefulTimeout = Math.min(timeout_ms, 5000);
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.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": {