@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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +448 -0
  3. package/agents/backend-python-expert.md +453 -0
  4. package/agents/database-postgres-expert.md +538 -0
  5. package/agents/frontend-flutter-expert.md +487 -0
  6. package/agents/frontend-react-expert.md +424 -0
  7. package/agents/quality-reviewer.md +542 -0
  8. package/agents/specification-agent.md +505 -0
  9. package/bin/sutory +332 -0
  10. package/bin/vibe +338 -0
  11. package/mcp/dist/__tests__/complexity.test.js +126 -0
  12. package/mcp/dist/__tests__/memory.test.js +120 -0
  13. package/mcp/dist/__tests__/python-dart-complexity.test.js +146 -0
  14. package/mcp/dist/index.js +230 -0
  15. package/mcp/dist/lib/ContextCompressor.js +305 -0
  16. package/mcp/dist/lib/MemoryManager.js +334 -0
  17. package/mcp/dist/lib/ProjectCache.js +126 -0
  18. package/mcp/dist/lib/PythonParser.js +241 -0
  19. package/mcp/dist/tools/browser/browserPool.js +76 -0
  20. package/mcp/dist/tools/browser/browserUtils.js +135 -0
  21. package/mcp/dist/tools/browser/inspectNetworkRequests.js +140 -0
  22. package/mcp/dist/tools/browser/monitorConsoleLogs.js +97 -0
  23. package/mcp/dist/tools/convention/analyzeComplexity.js +248 -0
  24. package/mcp/dist/tools/convention/applyQualityRules.js +102 -0
  25. package/mcp/dist/tools/convention/checkCouplingCohesion.js +233 -0
  26. package/mcp/dist/tools/convention/complexityMetrics.js +133 -0
  27. package/mcp/dist/tools/convention/dartComplexity.js +117 -0
  28. package/mcp/dist/tools/convention/getCodingGuide.js +64 -0
  29. package/mcp/dist/tools/convention/languageDetector.js +50 -0
  30. package/mcp/dist/tools/convention/pythonComplexity.js +109 -0
  31. package/mcp/dist/tools/convention/suggestImprovements.js +257 -0
  32. package/mcp/dist/tools/convention/validateCodeQuality.js +177 -0
  33. package/mcp/dist/tools/memory/autoSaveContext.js +79 -0
  34. package/mcp/dist/tools/memory/database.js +123 -0
  35. package/mcp/dist/tools/memory/deleteMemory.js +39 -0
  36. package/mcp/dist/tools/memory/listMemories.js +38 -0
  37. package/mcp/dist/tools/memory/memoryConfig.js +27 -0
  38. package/mcp/dist/tools/memory/memorySQLite.js +138 -0
  39. package/mcp/dist/tools/memory/memoryUtils.js +34 -0
  40. package/mcp/dist/tools/memory/migrate.js +113 -0
  41. package/mcp/dist/tools/memory/prioritizeMemory.js +109 -0
  42. package/mcp/dist/tools/memory/recallMemory.js +40 -0
  43. package/mcp/dist/tools/memory/restoreSessionContext.js +69 -0
  44. package/mcp/dist/tools/memory/saveMemory.js +34 -0
  45. package/mcp/dist/tools/memory/searchMemories.js +37 -0
  46. package/mcp/dist/tools/memory/startSession.js +100 -0
  47. package/mcp/dist/tools/memory/updateMemory.js +46 -0
  48. package/mcp/dist/tools/planning/analyzeRequirements.js +166 -0
  49. package/mcp/dist/tools/planning/createUserStories.js +119 -0
  50. package/mcp/dist/tools/planning/featureRoadmap.js +202 -0
  51. package/mcp/dist/tools/planning/generatePrd.js +156 -0
  52. package/mcp/dist/tools/prompt/analyzePrompt.js +145 -0
  53. package/mcp/dist/tools/prompt/enhancePrompt.js +105 -0
  54. package/mcp/dist/tools/semantic/findReferences.js +195 -0
  55. package/mcp/dist/tools/semantic/findSymbol.js +200 -0
  56. package/mcp/dist/tools/thinking/analyzeProblem.js +50 -0
  57. package/mcp/dist/tools/thinking/breakDownProblem.js +140 -0
  58. package/mcp/dist/tools/thinking/createThinkingChain.js +39 -0
  59. package/mcp/dist/tools/thinking/formatAsPlan.js +73 -0
  60. package/mcp/dist/tools/thinking/stepByStepAnalysis.js +58 -0
  61. package/mcp/dist/tools/thinking/thinkAloudProcess.js +75 -0
  62. package/mcp/dist/tools/time/getCurrentTime.js +61 -0
  63. package/mcp/dist/tools/ui/previewUiAscii.js +232 -0
  64. package/mcp/dist/types/tool.js +2 -0
  65. package/mcp/package.json +53 -0
  66. package/package.json +49 -0
  67. package/scripts/install-mcp.js +48 -0
  68. package/scripts/install.sh +70 -0
  69. package/skills/core/communication-guide.md +104 -0
  70. package/skills/core/development-philosophy.md +53 -0
  71. package/skills/core/quick-start.md +121 -0
  72. package/skills/languages/dart-flutter.md +509 -0
  73. package/skills/languages/python-fastapi.md +386 -0
  74. package/skills/languages/typescript-nextjs.md +441 -0
  75. package/skills/languages/typescript-react-native.md +446 -0
  76. package/skills/languages/typescript-react.md +525 -0
  77. package/skills/quality/checklist.md +276 -0
  78. package/skills/quality/testing-strategy.md +437 -0
  79. package/skills/standards/anti-patterns.md +369 -0
  80. package/skills/standards/code-structure.md +291 -0
  81. package/skills/standards/complexity-metrics.md +312 -0
  82. package/skills/standards/naming-conventions.md +198 -0
  83. package/skills/tools/mcp-hi-ai-guide.md +665 -0
  84. package/skills/tools/mcp-workflow.md +51 -0
  85. package/templates/constitution-template.md +193 -0
  86. package/templates/plan-template.md +237 -0
  87. package/templates/spec-template.md +142 -0
  88. 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
+ }