@su-record/vibe 0.1.2 → 0.1.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.
Files changed (59) hide show
  1. package/README.md +13 -6
  2. package/bin/vibe +20 -2
  3. package/package.json +5 -6
  4. package/scripts/install-mcp.js +31 -5
  5. package/mcp/dist/__tests__/complexity.test.js +0 -126
  6. package/mcp/dist/__tests__/memory.test.js +0 -120
  7. package/mcp/dist/__tests__/python-dart-complexity.test.js +0 -146
  8. package/mcp/dist/index.js +0 -230
  9. package/mcp/dist/lib/ContextCompressor.js +0 -305
  10. package/mcp/dist/lib/MemoryManager.js +0 -334
  11. package/mcp/dist/lib/ProjectCache.js +0 -126
  12. package/mcp/dist/lib/PythonParser.js +0 -241
  13. package/mcp/dist/tools/browser/browserPool.js +0 -76
  14. package/mcp/dist/tools/browser/browserUtils.js +0 -135
  15. package/mcp/dist/tools/browser/inspectNetworkRequests.js +0 -140
  16. package/mcp/dist/tools/browser/monitorConsoleLogs.js +0 -97
  17. package/mcp/dist/tools/convention/analyzeComplexity.js +0 -248
  18. package/mcp/dist/tools/convention/applyQualityRules.js +0 -102
  19. package/mcp/dist/tools/convention/checkCouplingCohesion.js +0 -233
  20. package/mcp/dist/tools/convention/complexityMetrics.js +0 -133
  21. package/mcp/dist/tools/convention/dartComplexity.js +0 -117
  22. package/mcp/dist/tools/convention/getCodingGuide.js +0 -64
  23. package/mcp/dist/tools/convention/languageDetector.js +0 -50
  24. package/mcp/dist/tools/convention/pythonComplexity.js +0 -109
  25. package/mcp/dist/tools/convention/suggestImprovements.js +0 -257
  26. package/mcp/dist/tools/convention/validateCodeQuality.js +0 -177
  27. package/mcp/dist/tools/memory/autoSaveContext.js +0 -79
  28. package/mcp/dist/tools/memory/database.js +0 -123
  29. package/mcp/dist/tools/memory/deleteMemory.js +0 -39
  30. package/mcp/dist/tools/memory/listMemories.js +0 -38
  31. package/mcp/dist/tools/memory/memoryConfig.js +0 -27
  32. package/mcp/dist/tools/memory/memorySQLite.js +0 -138
  33. package/mcp/dist/tools/memory/memoryUtils.js +0 -34
  34. package/mcp/dist/tools/memory/migrate.js +0 -113
  35. package/mcp/dist/tools/memory/prioritizeMemory.js +0 -109
  36. package/mcp/dist/tools/memory/recallMemory.js +0 -40
  37. package/mcp/dist/tools/memory/restoreSessionContext.js +0 -69
  38. package/mcp/dist/tools/memory/saveMemory.js +0 -34
  39. package/mcp/dist/tools/memory/searchMemories.js +0 -37
  40. package/mcp/dist/tools/memory/startSession.js +0 -100
  41. package/mcp/dist/tools/memory/updateMemory.js +0 -46
  42. package/mcp/dist/tools/planning/analyzeRequirements.js +0 -166
  43. package/mcp/dist/tools/planning/createUserStories.js +0 -119
  44. package/mcp/dist/tools/planning/featureRoadmap.js +0 -202
  45. package/mcp/dist/tools/planning/generatePrd.js +0 -156
  46. package/mcp/dist/tools/prompt/analyzePrompt.js +0 -145
  47. package/mcp/dist/tools/prompt/enhancePrompt.js +0 -105
  48. package/mcp/dist/tools/semantic/findReferences.js +0 -195
  49. package/mcp/dist/tools/semantic/findSymbol.js +0 -200
  50. package/mcp/dist/tools/thinking/analyzeProblem.js +0 -50
  51. package/mcp/dist/tools/thinking/breakDownProblem.js +0 -140
  52. package/mcp/dist/tools/thinking/createThinkingChain.js +0 -39
  53. package/mcp/dist/tools/thinking/formatAsPlan.js +0 -73
  54. package/mcp/dist/tools/thinking/stepByStepAnalysis.js +0 -58
  55. package/mcp/dist/tools/thinking/thinkAloudProcess.js +0 -75
  56. package/mcp/dist/tools/time/getCurrentTime.js +0 -61
  57. package/mcp/dist/tools/ui/previewUiAscii.js +0 -232
  58. package/mcp/dist/types/tool.js +0 -2
  59. package/mcp/package.json +0 -53
@@ -1,334 +0,0 @@
1
- // SQLite-based memory management system
2
- // Replaces JSON file storage with proper database
3
- import Database from 'better-sqlite3';
4
- import path from 'path';
5
- export class MemoryManager {
6
- db;
7
- static instance = null;
8
- dbPath;
9
- recallStmt = null;
10
- saveStmt = null;
11
- recallSelectStmt = null;
12
- recallUpdateStmt = null;
13
- constructor(customDbPath) {
14
- if (customDbPath) {
15
- this.dbPath = customDbPath;
16
- }
17
- else {
18
- const memoryDir = path.join(process.cwd(), 'memories');
19
- this.dbPath = path.join(memoryDir, 'memories.db');
20
- // Ensure directory exists synchronously (needed for DB init)
21
- try {
22
- require('fs').mkdirSync(memoryDir, { recursive: true });
23
- }
24
- catch (error) {
25
- const nodeError = error;
26
- if (nodeError.code !== 'EEXIST') {
27
- throw new Error(`Failed to create memory directory: ${nodeError.message}`);
28
- }
29
- }
30
- }
31
- this.db = new Database(this.dbPath);
32
- this.initializeDatabase();
33
- // Only migrate if using default path (not for tests)
34
- if (!customDbPath) {
35
- this.migrateFromJSON();
36
- }
37
- }
38
- static cleanupRegistered = false;
39
- static getInstance(customDbPath) {
40
- if (!MemoryManager.instance) {
41
- MemoryManager.instance = new MemoryManager(customDbPath);
42
- // Register cleanup handlers only once
43
- if (!MemoryManager.cleanupRegistered) {
44
- MemoryManager.cleanupRegistered = true;
45
- // Increase max listeners to avoid warnings in test environments
46
- process.setMaxListeners(Math.max(process.getMaxListeners(), 15));
47
- // Register cleanup on process exit to prevent memory leaks
48
- const cleanup = () => {
49
- if (MemoryManager.instance) {
50
- MemoryManager.instance.close();
51
- }
52
- };
53
- process.on('exit', cleanup);
54
- process.on('SIGINT', () => {
55
- cleanup();
56
- process.exit(0);
57
- });
58
- process.on('SIGTERM', () => {
59
- cleanup();
60
- process.exit(0);
61
- });
62
- }
63
- }
64
- return MemoryManager.instance;
65
- }
66
- initializeDatabase() {
67
- // Create memories table
68
- this.db.exec(`
69
- CREATE TABLE IF NOT EXISTS memories (
70
- key TEXT PRIMARY KEY,
71
- value TEXT NOT NULL,
72
- category TEXT NOT NULL DEFAULT 'general',
73
- timestamp TEXT NOT NULL,
74
- lastAccessed TEXT NOT NULL,
75
- priority INTEGER DEFAULT 0
76
- );
77
-
78
- CREATE INDEX IF NOT EXISTS idx_category ON memories(category);
79
- CREATE INDEX IF NOT EXISTS idx_timestamp ON memories(timestamp);
80
- CREATE INDEX IF NOT EXISTS idx_priority ON memories(priority);
81
- CREATE INDEX IF NOT EXISTS idx_lastAccessed ON memories(lastAccessed);
82
- `);
83
- // Enable WAL mode for better concurrency
84
- this.db.pragma('journal_mode = WAL');
85
- // Pre-compile frequently used statements for performance
86
- this.initializePreparedStatements();
87
- }
88
- initializePreparedStatements() {
89
- // Pre-compile recall statement
90
- try {
91
- this.recallStmt = this.db.prepare(`
92
- UPDATE memories SET lastAccessed = ?
93
- WHERE key = ?
94
- RETURNING *
95
- `);
96
- }
97
- catch (error) {
98
- // RETURNING not supported, pre-compile fallback statements
99
- this.recallStmt = null;
100
- this.recallSelectStmt = this.db.prepare(`SELECT * FROM memories WHERE key = ?`);
101
- this.recallUpdateStmt = this.db.prepare(`UPDATE memories SET lastAccessed = ? WHERE key = ?`);
102
- }
103
- // Pre-compile save statement
104
- this.saveStmt = this.db.prepare(`
105
- INSERT OR REPLACE INTO memories (key, value, category, timestamp, lastAccessed, priority)
106
- VALUES (?, ?, ?, ?, ?, ?)
107
- `);
108
- }
109
- /**
110
- * Auto-migrate from JSON to SQLite database
111
- */
112
- migrateFromJSON() {
113
- const jsonPath = this.getJSONPath();
114
- const memories = this.loadJSONMemories(jsonPath);
115
- if (memories.length === 0)
116
- return;
117
- this.importMemories(memories);
118
- this.backupAndCleanup(jsonPath, memories.length);
119
- }
120
- /**
121
- * Get JSON file path
122
- */
123
- getJSONPath() {
124
- return path.join(path.dirname(this.dbPath), 'memories.json');
125
- }
126
- /**
127
- * Load memories from JSON file
128
- */
129
- loadJSONMemories(jsonPath) {
130
- try {
131
- const jsonData = require('fs').readFileSync(jsonPath, 'utf-8');
132
- return JSON.parse(jsonData);
133
- }
134
- catch (error) {
135
- return [];
136
- }
137
- }
138
- /**
139
- * Import memories into SQLite database
140
- */
141
- importMemories(memories) {
142
- const insert = this.db.prepare(`
143
- INSERT OR REPLACE INTO memories (key, value, category, timestamp, lastAccessed, priority)
144
- VALUES (?, ?, ?, ?, ?, ?)
145
- `);
146
- const insertMany = this.db.transaction((items) => {
147
- for (const item of items) {
148
- insert.run(item.key, item.value, item.category || 'general', item.timestamp, item.lastAccessed, item.priority || 0);
149
- }
150
- });
151
- insertMany(memories);
152
- }
153
- /**
154
- * Backup JSON file and log migration
155
- */
156
- backupAndCleanup(jsonPath, count) {
157
- try {
158
- require('fs').renameSync(jsonPath, `${jsonPath}.backup`);
159
- // Migration successful - could add logger here
160
- }
161
- catch (error) {
162
- // Backup failed but migration completed
163
- }
164
- }
165
- /**
166
- * Save or update a memory item
167
- * @param key - Unique identifier for the memory
168
- * @param value - Content to store
169
- * @param category - Category for organization (default: 'general')
170
- * @param priority - Priority level (default: 0)
171
- */
172
- save(key, value, category = 'general', priority = 0) {
173
- const timestamp = new Date().toISOString();
174
- if (this.saveStmt) {
175
- this.saveStmt.run(key, value, category, timestamp, timestamp, priority);
176
- }
177
- else {
178
- // Fallback if prepared statement not available
179
- const stmt = this.db.prepare(`
180
- INSERT OR REPLACE INTO memories (key, value, category, timestamp, lastAccessed, priority)
181
- VALUES (?, ?, ?, ?, ?, ?)
182
- `);
183
- stmt.run(key, value, category, timestamp, timestamp, priority);
184
- }
185
- }
186
- /**
187
- * Recall a memory item by key
188
- * @param key - Memory key to recall
189
- * @returns Memory item or null if not found
190
- */
191
- recall(key) {
192
- const timestamp = new Date().toISOString();
193
- // Use pre-compiled statement if available
194
- if (this.recallStmt) {
195
- const result = this.recallStmt.get(timestamp, key);
196
- return result || null;
197
- }
198
- // Fallback for older SQLite versions (using pre-compiled statements)
199
- if (!this.recallSelectStmt || !this.recallUpdateStmt) {
200
- throw new Error('Fallback recall statements not initialized');
201
- }
202
- const result = this.recallSelectStmt.get(key);
203
- if (result) {
204
- this.recallUpdateStmt.run(timestamp, key);
205
- }
206
- return result || null;
207
- }
208
- /**
209
- * Delete a memory item
210
- * @param key - Memory key to delete
211
- * @returns True if deleted successfully
212
- */
213
- delete(key) {
214
- const stmt = this.db.prepare(`
215
- DELETE FROM memories WHERE key = ?
216
- `);
217
- const result = stmt.run(key);
218
- return result.changes > 0;
219
- }
220
- /**
221
- * Update a memory item's value
222
- * @param key - Memory key to update
223
- * @param value - New value
224
- * @returns True if updated successfully
225
- */
226
- update(key, value) {
227
- const timestamp = new Date().toISOString();
228
- const stmt = this.db.prepare(`
229
- UPDATE memories
230
- SET value = ?, timestamp = ?, lastAccessed = ?
231
- WHERE key = ?
232
- `);
233
- const result = stmt.run(value, timestamp, timestamp, key);
234
- return result.changes > 0;
235
- }
236
- /**
237
- * List all memories or filter by category
238
- * @param category - Optional category filter
239
- * @returns Array of memory items
240
- */
241
- list(category) {
242
- let stmt;
243
- if (category) {
244
- stmt = this.db.prepare(`
245
- SELECT * FROM memories WHERE category = ?
246
- ORDER BY priority DESC, timestamp DESC
247
- `);
248
- return stmt.all(category);
249
- }
250
- else {
251
- stmt = this.db.prepare(`
252
- SELECT * FROM memories
253
- ORDER BY priority DESC, timestamp DESC
254
- `);
255
- return stmt.all();
256
- }
257
- }
258
- /**
259
- * Search memories by keyword
260
- * @param query - Search query string
261
- * @returns Array of matching memory items
262
- */
263
- search(query) {
264
- const stmt = this.db.prepare(`
265
- SELECT * FROM memories
266
- WHERE key LIKE ? OR value LIKE ?
267
- ORDER BY priority DESC, timestamp DESC
268
- `);
269
- const pattern = `%${query}%`;
270
- return stmt.all(pattern, pattern);
271
- }
272
- /**
273
- * Get memories by priority level
274
- * @param priority - Priority level to filter
275
- * @returns Array of memory items with specified priority
276
- */
277
- getByPriority(priority) {
278
- const stmt = this.db.prepare(`
279
- SELECT * FROM memories
280
- WHERE priority = ?
281
- ORDER BY timestamp DESC
282
- `);
283
- return stmt.all(priority);
284
- }
285
- /**
286
- * Update priority of a memory item
287
- * @param key - Memory key
288
- * @param priority - New priority level
289
- * @returns True if updated successfully
290
- */
291
- setPriority(key, priority) {
292
- const stmt = this.db.prepare(`
293
- UPDATE memories SET priority = ? WHERE key = ?
294
- `);
295
- const result = stmt.run(priority, key);
296
- return result.changes > 0;
297
- }
298
- /**
299
- * Get memory statistics (optimized to single query)
300
- * @returns Total count and count by category
301
- */
302
- getStats() {
303
- // Single query with ROLLUP or combined approach
304
- const categories = this.db.prepare(`
305
- SELECT category, COUNT(*) as count
306
- FROM memories
307
- GROUP BY category
308
- `).all();
309
- const byCategory = {};
310
- let total = 0;
311
- categories.forEach(cat => {
312
- byCategory[cat.category] = cat.count;
313
- total += cat.count;
314
- });
315
- return { total, byCategory };
316
- }
317
- /**
318
- * Close database connection
319
- */
320
- close() {
321
- if (this.db) {
322
- this.db.close();
323
- MemoryManager.instance = null;
324
- }
325
- }
326
- /**
327
- * Reset singleton instance (useful for testing and cleanup)
328
- */
329
- static resetInstance() {
330
- if (MemoryManager.instance) {
331
- MemoryManager.instance.close();
332
- }
333
- }
334
- }
@@ -1,126 +0,0 @@
1
- // Project caching utility for ts-morph (v1.3)
2
- // Implements LRU cache to avoid re-parsing on every request
3
- import { Project } from 'ts-morph';
4
- import path from 'path';
5
- export class ProjectCache {
6
- static instance = null;
7
- cache = new Map();
8
- MAX_CACHE_SIZE = 5;
9
- CACHE_TTL = 5 * 60 * 1000; // 5 minutes
10
- MAX_TOTAL_MEMORY_MB = 200; // Max 200MB total cache
11
- MAX_PROJECT_MEMORY_MB = 100; // Max 100MB per project
12
- constructor() { }
13
- static getInstance() {
14
- if (!ProjectCache.instance) {
15
- ProjectCache.instance = new ProjectCache();
16
- }
17
- return ProjectCache.instance;
18
- }
19
- getOrCreate(projectPath) {
20
- // Normalize path and remove trailing slashes
21
- let normalizedPath = path.normalize(projectPath);
22
- if (normalizedPath.endsWith(path.sep) && normalizedPath.length > 1) {
23
- normalizedPath = normalizedPath.slice(0, -1);
24
- }
25
- const now = Date.now();
26
- // Check if cached and not expired
27
- const cached = this.cache.get(normalizedPath);
28
- if (cached && (now - cached.lastAccess) < this.CACHE_TTL) {
29
- cached.lastAccess = now;
30
- return cached.project;
31
- }
32
- // Remove expired entries
33
- this.removeExpired();
34
- // LRU eviction if cache is full
35
- if (this.cache.size >= this.MAX_CACHE_SIZE) {
36
- this.evictLRU();
37
- }
38
- // Create new project
39
- const project = new Project({
40
- useInMemoryFileSystem: false,
41
- compilerOptions: {
42
- allowJs: true,
43
- skipLibCheck: true,
44
- noEmit: true
45
- }
46
- });
47
- // Add source files
48
- const pattern = path.join(normalizedPath, '**/*.{ts,tsx,js,jsx}');
49
- project.addSourceFilesAtPaths(pattern);
50
- const sourceFiles = project.getSourceFiles();
51
- const fileCount = sourceFiles.length;
52
- // Estimate memory usage (rough: 1MB base + 0.5MB per file)
53
- const estimatedMemoryMB = 1 + (fileCount * 0.5);
54
- // Skip caching if project is too large
55
- if (estimatedMemoryMB > this.MAX_PROJECT_MEMORY_MB) {
56
- console.warn(`Project ${normalizedPath} is too large (${estimatedMemoryMB}MB, ${fileCount} files) - not caching`);
57
- return project;
58
- }
59
- // Check total cache memory before adding
60
- const totalMemory = this.getTotalMemoryUsage();
61
- if (totalMemory + estimatedMemoryMB > this.MAX_TOTAL_MEMORY_MB) {
62
- // Evict projects until we have enough space
63
- while (this.getTotalMemoryUsage() + estimatedMemoryMB > this.MAX_TOTAL_MEMORY_MB && this.cache.size > 0) {
64
- this.evictLRU();
65
- }
66
- }
67
- this.cache.set(normalizedPath, {
68
- project,
69
- lastAccess: now,
70
- fileCount,
71
- estimatedMemoryMB
72
- });
73
- return project;
74
- }
75
- invalidate(projectPath) {
76
- const normalizedPath = path.normalize(projectPath);
77
- this.cache.delete(normalizedPath);
78
- }
79
- clear() {
80
- this.cache.clear();
81
- }
82
- getStats() {
83
- const now = Date.now();
84
- const projects = Array.from(this.cache.entries()).map(([path, cached]) => ({
85
- path,
86
- files: cached.fileCount,
87
- memoryMB: cached.estimatedMemoryMB,
88
- age: Math.floor((now - cached.lastAccess) / 1000) // seconds
89
- }));
90
- return {
91
- size: this.cache.size,
92
- totalMemoryMB: this.getTotalMemoryUsage(),
93
- projects
94
- };
95
- }
96
- getTotalMemoryUsage() {
97
- let total = 0;
98
- this.cache.forEach(cached => {
99
- total += cached.estimatedMemoryMB;
100
- });
101
- return total;
102
- }
103
- removeExpired() {
104
- const now = Date.now();
105
- const toRemove = [];
106
- this.cache.forEach((cached, path) => {
107
- if ((now - cached.lastAccess) >= this.CACHE_TTL) {
108
- toRemove.push(path);
109
- }
110
- });
111
- toRemove.forEach(path => this.cache.delete(path));
112
- }
113
- evictLRU() {
114
- let oldestPath = null;
115
- let oldestTime = Date.now();
116
- this.cache.forEach((cached, path) => {
117
- if (cached.lastAccess < oldestTime) {
118
- oldestTime = cached.lastAccess;
119
- oldestPath = path;
120
- }
121
- });
122
- if (oldestPath) {
123
- this.cache.delete(oldestPath);
124
- }
125
- }
126
- }
@@ -1,241 +0,0 @@
1
- // Python code parser utility for v1.3
2
- // Uses Python's ast module via child_process
3
- import { exec } from 'child_process';
4
- import { promisify } from 'util';
5
- import { writeFile, unlink } from 'fs/promises';
6
- import path from 'path';
7
- import os from 'os';
8
- const execAsync = promisify(exec);
9
- export class PythonParser {
10
- static cleanupRegistered = false;
11
- static pythonScript = `
12
- import ast
13
- import sys
14
- import json
15
-
16
- def analyze_code(code):
17
- try:
18
- tree = ast.parse(code)
19
- symbols = []
20
-
21
- for node in ast.walk(tree):
22
- if isinstance(node, ast.FunctionDef):
23
- symbols.append({
24
- 'name': node.name,
25
- 'kind': 'function',
26
- 'line': node.lineno,
27
- 'column': node.col_offset,
28
- 'endLine': node.end_lineno,
29
- 'docstring': ast.get_docstring(node)
30
- })
31
- elif isinstance(node, ast.ClassDef):
32
- symbols.append({
33
- 'name': node.name,
34
- 'kind': 'class',
35
- 'line': node.lineno,
36
- 'column': node.col_offset,
37
- 'endLine': node.end_lineno,
38
- 'docstring': ast.get_docstring(node)
39
- })
40
- elif isinstance(node, ast.Assign):
41
- for target in node.targets:
42
- if isinstance(target, ast.Name):
43
- symbols.append({
44
- 'name': target.id,
45
- 'kind': 'variable',
46
- 'line': node.lineno,
47
- 'column': node.col_offset
48
- })
49
- elif isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom):
50
- for alias in node.names:
51
- symbols.append({
52
- 'name': alias.name,
53
- 'kind': 'import',
54
- 'line': node.lineno,
55
- 'column': node.col_offset
56
- })
57
-
58
- return {'success': True, 'symbols': symbols}
59
- except SyntaxError as e:
60
- return {'success': False, 'error': str(e)}
61
- except Exception as e:
62
- return {'success': False, 'error': str(e)}
63
-
64
- def calculate_complexity(code):
65
- try:
66
- tree = ast.parse(code)
67
-
68
- def cyclomatic_complexity(node):
69
- complexity = 1
70
- for child in ast.walk(node):
71
- if isinstance(child, (ast.If, ast.For, ast.While, ast.And, ast.Or, ast.ExceptHandler)):
72
- complexity += 1
73
- elif isinstance(child, ast.BoolOp):
74
- complexity += len(child.values) - 1
75
- return complexity
76
-
77
- functions = []
78
- classes = []
79
- total_complexity = 1
80
-
81
- for node in ast.walk(tree):
82
- if isinstance(node, ast.FunctionDef):
83
- func_complexity = cyclomatic_complexity(node)
84
- functions.append({
85
- 'name': node.name,
86
- 'complexity': func_complexity,
87
- 'line': node.lineno
88
- })
89
- total_complexity += func_complexity
90
- elif isinstance(node, ast.ClassDef):
91
- method_count = sum(1 for n in node.body if isinstance(n, ast.FunctionDef))
92
- classes.append({
93
- 'name': node.name,
94
- 'methods': method_count,
95
- 'line': node.lineno
96
- })
97
-
98
- return {
99
- 'success': True,
100
- 'cyclomaticComplexity': total_complexity,
101
- 'functions': functions,
102
- 'classes': classes
103
- }
104
- except Exception as e:
105
- return {'success': False, 'error': str(e)}
106
-
107
- if __name__ == '__main__':
108
- code = sys.stdin.read()
109
- action = sys.argv[1] if len(sys.argv) > 1 else 'symbols'
110
-
111
- if action == 'symbols':
112
- result = analyze_code(code)
113
- elif action == 'complexity':
114
- result = calculate_complexity(code)
115
- else:
116
- result = {'success': False, 'error': 'Unknown action'}
117
-
118
- print(json.dumps(result))
119
- `;
120
- // Singleton Python script path to avoid recreating it
121
- static scriptPath = null;
122
- /**
123
- * Register cleanup handlers on first use
124
- */
125
- static registerCleanup() {
126
- if (this.cleanupRegistered) {
127
- return;
128
- }
129
- this.cleanupRegistered = true;
130
- // Cleanup on normal exit
131
- process.on('exit', () => {
132
- if (this.scriptPath) {
133
- try {
134
- const fs = require('fs');
135
- fs.unlinkSync(this.scriptPath);
136
- }
137
- catch (e) {
138
- // Ignore errors during cleanup
139
- }
140
- }
141
- });
142
- // Cleanup on SIGINT (Ctrl+C)
143
- process.on('SIGINT', () => {
144
- this.cleanup().then(() => process.exit(0));
145
- });
146
- // Cleanup on SIGTERM
147
- process.on('SIGTERM', () => {
148
- this.cleanup().then(() => process.exit(0));
149
- });
150
- // Cleanup on uncaught exception
151
- process.on('uncaughtException', (error) => {
152
- console.error('Uncaught exception:', error);
153
- this.cleanup().then(() => process.exit(1));
154
- });
155
- }
156
- /**
157
- * Initialize Python script (singleton pattern)
158
- */
159
- static async ensureScriptExists() {
160
- if (this.scriptPath) {
161
- return this.scriptPath;
162
- }
163
- // Register cleanup handlers on first use
164
- this.registerCleanup();
165
- this.scriptPath = path.join(os.tmpdir(), `hi-ai-parser-${process.pid}.py`);
166
- await writeFile(this.scriptPath, this.pythonScript);
167
- return this.scriptPath;
168
- }
169
- /**
170
- * Execute Python code analysis with improved memory management
171
- */
172
- static async executePython(code, action) {
173
- let codePath = null;
174
- try {
175
- const scriptPath = await this.ensureScriptExists();
176
- // Write code to temp file with unique name
177
- codePath = path.join(os.tmpdir(), `hi-ai-code-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.py`);
178
- await writeFile(codePath, code);
179
- // Execute Python script
180
- const { stdout, stderr } = await execAsync(`python3 "${scriptPath}" ${action} < "${codePath}"`, {
181
- maxBuffer: 10 * 1024 * 1024, // 10MB
182
- timeout: 30000 // 30 second timeout
183
- });
184
- if (stderr && !stderr.includes('DeprecationWarning')) {
185
- console.error('Python stderr:', stderr);
186
- }
187
- const result = JSON.parse(stdout);
188
- if (!result.success) {
189
- throw new Error(result.error || `Python ${action} analysis failed`);
190
- }
191
- return result;
192
- }
193
- catch (error) {
194
- if (error.code === 'ENOENT') {
195
- throw new Error('Python 3 not found. Please install Python 3 to analyze Python code.');
196
- }
197
- throw error;
198
- }
199
- finally {
200
- // Always cleanup code temp file immediately
201
- if (codePath) {
202
- await unlink(codePath).catch(() => { });
203
- }
204
- }
205
- }
206
- static async findSymbols(code) {
207
- const result = await this.executePython(code, 'symbols');
208
- return result.symbols || [];
209
- }
210
- static async analyzeComplexity(code) {
211
- const result = await this.executePython(code, 'complexity');
212
- return {
213
- cyclomaticComplexity: result.cyclomaticComplexity || 1,
214
- functions: result.functions || [],
215
- classes: result.classes || []
216
- };
217
- }
218
- /**
219
- * Cleanup singleton script on process exit
220
- */
221
- static async cleanup() {
222
- if (this.scriptPath) {
223
- await unlink(this.scriptPath).catch(() => { });
224
- this.scriptPath = null;
225
- }
226
- }
227
- static isPythonFile(filePath) {
228
- return filePath.endsWith('.py');
229
- }
230
- static isPythonCode(code) {
231
- // Heuristic: Check for Python-specific patterns
232
- const pythonPatterns = [
233
- /^import\s+\w+/m,
234
- /^from\s+\w+\s+import/m,
235
- /^def\s+\w+\s*\(/m,
236
- /^class\s+\w+/m,
237
- /^if\s+__name__\s*==\s*['"]__main__['"]/m
238
- ];
239
- return pythonPatterns.some(pattern => pattern.test(code));
240
- }
241
- }