@su-record/vibe 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +448 -0
- package/agents/backend-python-expert.md +453 -0
- package/agents/database-postgres-expert.md +538 -0
- package/agents/frontend-flutter-expert.md +487 -0
- package/agents/frontend-react-expert.md +424 -0
- package/agents/quality-reviewer.md +542 -0
- package/agents/specification-agent.md +505 -0
- package/bin/sutory +332 -0
- package/bin/vibe +338 -0
- package/mcp/dist/__tests__/complexity.test.js +126 -0
- package/mcp/dist/__tests__/memory.test.js +120 -0
- package/mcp/dist/__tests__/python-dart-complexity.test.js +146 -0
- package/mcp/dist/index.js +230 -0
- package/mcp/dist/lib/ContextCompressor.js +305 -0
- package/mcp/dist/lib/MemoryManager.js +334 -0
- package/mcp/dist/lib/ProjectCache.js +126 -0
- package/mcp/dist/lib/PythonParser.js +241 -0
- package/mcp/dist/tools/browser/browserPool.js +76 -0
- package/mcp/dist/tools/browser/browserUtils.js +135 -0
- package/mcp/dist/tools/browser/inspectNetworkRequests.js +140 -0
- package/mcp/dist/tools/browser/monitorConsoleLogs.js +97 -0
- package/mcp/dist/tools/convention/analyzeComplexity.js +248 -0
- package/mcp/dist/tools/convention/applyQualityRules.js +102 -0
- package/mcp/dist/tools/convention/checkCouplingCohesion.js +233 -0
- package/mcp/dist/tools/convention/complexityMetrics.js +133 -0
- package/mcp/dist/tools/convention/dartComplexity.js +117 -0
- package/mcp/dist/tools/convention/getCodingGuide.js +64 -0
- package/mcp/dist/tools/convention/languageDetector.js +50 -0
- package/mcp/dist/tools/convention/pythonComplexity.js +109 -0
- package/mcp/dist/tools/convention/suggestImprovements.js +257 -0
- package/mcp/dist/tools/convention/validateCodeQuality.js +177 -0
- package/mcp/dist/tools/memory/autoSaveContext.js +79 -0
- package/mcp/dist/tools/memory/database.js +123 -0
- package/mcp/dist/tools/memory/deleteMemory.js +39 -0
- package/mcp/dist/tools/memory/listMemories.js +38 -0
- package/mcp/dist/tools/memory/memoryConfig.js +27 -0
- package/mcp/dist/tools/memory/memorySQLite.js +138 -0
- package/mcp/dist/tools/memory/memoryUtils.js +34 -0
- package/mcp/dist/tools/memory/migrate.js +113 -0
- package/mcp/dist/tools/memory/prioritizeMemory.js +109 -0
- package/mcp/dist/tools/memory/recallMemory.js +40 -0
- package/mcp/dist/tools/memory/restoreSessionContext.js +69 -0
- package/mcp/dist/tools/memory/saveMemory.js +34 -0
- package/mcp/dist/tools/memory/searchMemories.js +37 -0
- package/mcp/dist/tools/memory/startSession.js +100 -0
- package/mcp/dist/tools/memory/updateMemory.js +46 -0
- package/mcp/dist/tools/planning/analyzeRequirements.js +166 -0
- package/mcp/dist/tools/planning/createUserStories.js +119 -0
- package/mcp/dist/tools/planning/featureRoadmap.js +202 -0
- package/mcp/dist/tools/planning/generatePrd.js +156 -0
- package/mcp/dist/tools/prompt/analyzePrompt.js +145 -0
- package/mcp/dist/tools/prompt/enhancePrompt.js +105 -0
- package/mcp/dist/tools/semantic/findReferences.js +195 -0
- package/mcp/dist/tools/semantic/findSymbol.js +200 -0
- package/mcp/dist/tools/thinking/analyzeProblem.js +50 -0
- package/mcp/dist/tools/thinking/breakDownProblem.js +140 -0
- package/mcp/dist/tools/thinking/createThinkingChain.js +39 -0
- package/mcp/dist/tools/thinking/formatAsPlan.js +73 -0
- package/mcp/dist/tools/thinking/stepByStepAnalysis.js +58 -0
- package/mcp/dist/tools/thinking/thinkAloudProcess.js +75 -0
- package/mcp/dist/tools/time/getCurrentTime.js +61 -0
- package/mcp/dist/tools/ui/previewUiAscii.js +232 -0
- package/mcp/dist/types/tool.js +2 -0
- package/mcp/package.json +53 -0
- package/package.json +49 -0
- package/scripts/install-mcp.js +48 -0
- package/scripts/install.sh +70 -0
- package/skills/core/communication-guide.md +104 -0
- package/skills/core/development-philosophy.md +53 -0
- package/skills/core/quick-start.md +121 -0
- package/skills/languages/dart-flutter.md +509 -0
- package/skills/languages/python-fastapi.md +386 -0
- package/skills/languages/typescript-nextjs.md +441 -0
- package/skills/languages/typescript-react-native.md +446 -0
- package/skills/languages/typescript-react.md +525 -0
- package/skills/quality/checklist.md +276 -0
- package/skills/quality/testing-strategy.md +437 -0
- package/skills/standards/anti-patterns.md +369 -0
- package/skills/standards/code-structure.md +291 -0
- package/skills/standards/complexity-metrics.md +312 -0
- package/skills/standards/naming-conventions.md +198 -0
- package/skills/tools/mcp-hi-ai-guide.md +665 -0
- package/skills/tools/mcp-workflow.md +51 -0
- package/templates/constitution-template.md +193 -0
- package/templates/plan-template.md +237 -0
- package/templates/spec-template.md +142 -0
- package/templates/tasks-template.md +132 -0
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Browser instance pool for efficient resource management
|
|
2
|
+
import puppeteer from 'puppeteer-core';
|
|
3
|
+
import { getBrowserLaunchOptions } from './browserUtils.js';
|
|
4
|
+
class BrowserPool {
|
|
5
|
+
browser = null;
|
|
6
|
+
lastUsed = 0;
|
|
7
|
+
IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
8
|
+
cleanupTimer = null;
|
|
9
|
+
/**
|
|
10
|
+
* Get or create browser instance
|
|
11
|
+
*/
|
|
12
|
+
async getBrowser() {
|
|
13
|
+
// If browser exists and is connected, return it
|
|
14
|
+
if (this.browser && this.browser.isConnected()) {
|
|
15
|
+
this.lastUsed = Date.now();
|
|
16
|
+
this.scheduleCleanup();
|
|
17
|
+
return this.browser;
|
|
18
|
+
}
|
|
19
|
+
// Create new browser instance
|
|
20
|
+
const options = getBrowserLaunchOptions();
|
|
21
|
+
this.browser = await puppeteer.launch(options);
|
|
22
|
+
this.lastUsed = Date.now();
|
|
23
|
+
this.scheduleCleanup();
|
|
24
|
+
return this.browser;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Schedule automatic cleanup of idle browser
|
|
28
|
+
*/
|
|
29
|
+
scheduleCleanup() {
|
|
30
|
+
// Clear existing timer
|
|
31
|
+
if (this.cleanupTimer) {
|
|
32
|
+
clearTimeout(this.cleanupTimer);
|
|
33
|
+
}
|
|
34
|
+
// Set new timer
|
|
35
|
+
this.cleanupTimer = setTimeout(async () => {
|
|
36
|
+
const idleTime = Date.now() - this.lastUsed;
|
|
37
|
+
if (idleTime >= this.IDLE_TIMEOUT) {
|
|
38
|
+
await this.closeBrowser();
|
|
39
|
+
}
|
|
40
|
+
}, this.IDLE_TIMEOUT);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Manually close browser instance
|
|
44
|
+
*/
|
|
45
|
+
async closeBrowser() {
|
|
46
|
+
if (this.cleanupTimer) {
|
|
47
|
+
clearTimeout(this.cleanupTimer);
|
|
48
|
+
this.cleanupTimer = null;
|
|
49
|
+
}
|
|
50
|
+
if (this.browser && this.browser.isConnected()) {
|
|
51
|
+
await this.browser.close();
|
|
52
|
+
this.browser = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get a new page from the browser pool
|
|
57
|
+
*/
|
|
58
|
+
async getPage() {
|
|
59
|
+
const browser = await this.getBrowser();
|
|
60
|
+
return await browser.newPage();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Singleton instance
|
|
64
|
+
export const browserPool = new BrowserPool();
|
|
65
|
+
// Cleanup on process exit
|
|
66
|
+
process.on('exit', () => {
|
|
67
|
+
browserPool.closeBrowser();
|
|
68
|
+
});
|
|
69
|
+
process.on('SIGINT', async () => {
|
|
70
|
+
await browserPool.closeBrowser();
|
|
71
|
+
process.exit(0);
|
|
72
|
+
});
|
|
73
|
+
process.on('SIGTERM', async () => {
|
|
74
|
+
await browserPool.closeBrowser();
|
|
75
|
+
process.exit(0);
|
|
76
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Browser utility functions for finding Chrome/Chromium executables
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { platform } from 'os';
|
|
5
|
+
/**
|
|
6
|
+
* Finds Chrome or Chromium executable path on the system
|
|
7
|
+
*/
|
|
8
|
+
export function findChromePath() {
|
|
9
|
+
const platformName = platform();
|
|
10
|
+
// Platform-specific paths for Chrome
|
|
11
|
+
const chromePaths = {
|
|
12
|
+
win32: [
|
|
13
|
+
process.env.LOCALAPPDATA + '\\Google\\Chrome\\Application\\chrome.exe',
|
|
14
|
+
process.env.PROGRAMFILES + '\\Google\\Chrome\\Application\\chrome.exe',
|
|
15
|
+
process.env['PROGRAMFILES(X86)'] + '\\Google\\Chrome\\Application\\chrome.exe',
|
|
16
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
17
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
18
|
+
],
|
|
19
|
+
darwin: [
|
|
20
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
21
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
22
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
23
|
+
],
|
|
24
|
+
linux: [
|
|
25
|
+
'/usr/bin/google-chrome-stable',
|
|
26
|
+
'/usr/bin/google-chrome',
|
|
27
|
+
'/usr/bin/chromium-browser',
|
|
28
|
+
'/usr/bin/chromium',
|
|
29
|
+
'/snap/bin/chromium',
|
|
30
|
+
]
|
|
31
|
+
};
|
|
32
|
+
// Platform-specific paths for Edge (as fallback)
|
|
33
|
+
const edgePaths = {
|
|
34
|
+
win32: [
|
|
35
|
+
process.env['PROGRAMFILES(X86)'] + '\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
36
|
+
process.env.PROGRAMFILES + '\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
37
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
38
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
39
|
+
],
|
|
40
|
+
darwin: [
|
|
41
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
42
|
+
],
|
|
43
|
+
linux: [
|
|
44
|
+
'/usr/bin/microsoft-edge',
|
|
45
|
+
'/usr/bin/microsoft-edge-stable',
|
|
46
|
+
]
|
|
47
|
+
};
|
|
48
|
+
// Platform-specific paths for Brave (as fallback)
|
|
49
|
+
const bravePaths = {
|
|
50
|
+
win32: [
|
|
51
|
+
process.env.LOCALAPPDATA + '\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
|
|
52
|
+
process.env.PROGRAMFILES + '\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
|
|
53
|
+
process.env['PROGRAMFILES(X86)'] + '\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
|
|
54
|
+
],
|
|
55
|
+
darwin: [
|
|
56
|
+
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
57
|
+
],
|
|
58
|
+
linux: [
|
|
59
|
+
'/usr/bin/brave-browser',
|
|
60
|
+
'/usr/bin/brave',
|
|
61
|
+
'/snap/bin/brave',
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
// Check user-specified path first
|
|
65
|
+
if (process.env.CHROME_PATH && existsSync(process.env.CHROME_PATH)) {
|
|
66
|
+
return process.env.CHROME_PATH;
|
|
67
|
+
}
|
|
68
|
+
// Get paths for current platform
|
|
69
|
+
const currentPlatform = platformName === 'win32' ? 'win32' :
|
|
70
|
+
platformName === 'darwin' ? 'darwin' : 'linux';
|
|
71
|
+
const allPaths = [
|
|
72
|
+
...(chromePaths[currentPlatform] || []),
|
|
73
|
+
...(edgePaths[currentPlatform] || []),
|
|
74
|
+
...(bravePaths[currentPlatform] || [])
|
|
75
|
+
];
|
|
76
|
+
// Find first existing path
|
|
77
|
+
for (const path of allPaths) {
|
|
78
|
+
if (path && existsSync(path)) {
|
|
79
|
+
return path;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Try to find Chrome using 'which' command on Unix systems
|
|
83
|
+
if (platformName !== 'win32') {
|
|
84
|
+
try {
|
|
85
|
+
const chromePath = execSync('which google-chrome || which chromium || which chromium-browser', {
|
|
86
|
+
encoding: 'utf8'
|
|
87
|
+
}).trim();
|
|
88
|
+
if (chromePath && existsSync(chromePath)) {
|
|
89
|
+
return chromePath;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Command failed, continue to next method
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Try to find Chrome using 'where' command on Windows
|
|
97
|
+
if (platformName === 'win32') {
|
|
98
|
+
try {
|
|
99
|
+
const chromePath = execSync('where chrome', { encoding: 'utf8' }).trim().split('\n')[0];
|
|
100
|
+
if (chromePath && existsSync(chromePath)) {
|
|
101
|
+
return chromePath;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Command failed, continue
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get launch options for puppeteer with proper browser configuration
|
|
112
|
+
*/
|
|
113
|
+
export function getBrowserLaunchOptions(additionalOptions = {}) {
|
|
114
|
+
const executablePath = findChromePath();
|
|
115
|
+
if (!executablePath) {
|
|
116
|
+
throw new Error('Chrome/Chromium browser not found. Please install Chrome or set CHROME_PATH environment variable.\n' +
|
|
117
|
+
'Download Chrome from: https://www.google.com/chrome/\n' +
|
|
118
|
+
'Or set environment variable: export CHROME_PATH="/path/to/chrome"');
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
headless: true,
|
|
122
|
+
executablePath,
|
|
123
|
+
args: [
|
|
124
|
+
'--no-sandbox',
|
|
125
|
+
'--disable-setuid-sandbox',
|
|
126
|
+
'--disable-dev-shm-usage',
|
|
127
|
+
'--disable-accelerated-2d-canvas',
|
|
128
|
+
'--no-first-run',
|
|
129
|
+
'--no-zygote',
|
|
130
|
+
'--single-process', // For Windows compatibility
|
|
131
|
+
'--disable-gpu'
|
|
132
|
+
],
|
|
133
|
+
...additionalOptions
|
|
134
|
+
};
|
|
135
|
+
}
|