validpilot-oss 1.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 (97) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +196 -0
  3. package/bin/validpilot.js +173 -0
  4. package/brain/error_aggregator.js +203 -0
  5. package/core/artifacts.js +44 -0
  6. package/core/config.js +37 -0
  7. package/core/redaction.js +39 -0
  8. package/core/report.js +42 -0
  9. package/core/result.js +29 -0
  10. package/core/security.js +57 -0
  11. package/engines/chrome_mcp_adapter.js +319 -0
  12. package/engines/playwright_adapter.js +421 -0
  13. package/examples/demo/README.md +58 -0
  14. package/examples/demo/diagnostic-error-flow.json +22 -0
  15. package/examples/demo/diagnostic-error.html +29 -0
  16. package/examples/demo/flow.json +27 -0
  17. package/examples/demo/index.html +29 -0
  18. package/hands/browser_operator.js +67 -0
  19. package/hands/evidence_collector.js +97 -0
  20. package/package.json +55 -0
  21. package/rules/suggested-rules.json +237 -0
  22. package/server.js +5376 -0
  23. package/standalone-start.js +43 -0
  24. package/start-http.js +45 -0
  25. package/tools/ai_debug_investigate.json +30 -0
  26. package/tools/benchmark_run.json +37 -0
  27. package/tools/browser_a11y_check.json +21 -0
  28. package/tools/browser_artifacts.json +8 -0
  29. package/tools/browser_artifacts_clear.json +11 -0
  30. package/tools/browser_assert.json +16 -0
  31. package/tools/browser_batch.json +61 -0
  32. package/tools/browser_click.json +11 -0
  33. package/tools/browser_console.json +26 -0
  34. package/tools/browser_cookies.json +38 -0
  35. package/tools/browser_debug_report.json +11 -0
  36. package/tools/browser_diagnose.json +23 -0
  37. package/tools/browser_dom.json +11 -0
  38. package/tools/browser_element_status.json +26 -0
  39. package/tools/browser_errors.json +17 -0
  40. package/tools/browser_errors_aggregate.json +12 -0
  41. package/tools/browser_errors_clear.json +8 -0
  42. package/tools/browser_eval.json +11 -0
  43. package/tools/browser_events.json +15 -0
  44. package/tools/browser_events_clear.json +8 -0
  45. package/tools/browser_find_element.json +30 -0
  46. package/tools/browser_find_page.json +22 -0
  47. package/tools/browser_flow.json +38 -0
  48. package/tools/browser_har_export.json +17 -0
  49. package/tools/browser_highlight.json +18 -0
  50. package/tools/browser_hover.json +14 -0
  51. package/tools/browser_instrument.json +10 -0
  52. package/tools/browser_links.json +21 -0
  53. package/tools/browser_locator_suggest.json +16 -0
  54. package/tools/browser_locator_validate.json +12 -0
  55. package/tools/browser_network.json +16 -0
  56. package/tools/browser_network_detail.json +17 -0
  57. package/tools/browser_open.json +12 -0
  58. package/tools/browser_performance_check.json +25 -0
  59. package/tools/browser_press_key.json +18 -0
  60. package/tools/browser_quick_fix.json +29 -0
  61. package/tools/browser_screenshot.json +15 -0
  62. package/tools/browser_scroll.json +31 -0
  63. package/tools/browser_select.json +26 -0
  64. package/tools/browser_session_close.json +12 -0
  65. package/tools/browser_session_create.json +17 -0
  66. package/tools/browser_session_switch.json +12 -0
  67. package/tools/browser_sessions.json +8 -0
  68. package/tools/browser_snapshot.json +8 -0
  69. package/tools/browser_step.json +18 -0
  70. package/tools/browser_storage.json +10 -0
  71. package/tools/browser_trace_start.json +14 -0
  72. package/tools/browser_trace_stop.json +10 -0
  73. package/tools/browser_traverse_menu.json +25 -0
  74. package/tools/browser_type.json +12 -0
  75. package/tools/browser_verify_fix.json +39 -0
  76. package/tools/browser_visual_baseline.json +19 -0
  77. package/tools/browser_visual_compare.json +20 -0
  78. package/tools/browser_visual_report.json +8 -0
  79. package/tools/browser_wait.json +18 -0
  80. package/tools/debug_investigate.json +17 -0
  81. package/tools/error_fix_suggestion.json +13 -0
  82. package/tools/error_summary_md.json +11 -0
  83. package/tools/fix_verify.json +13 -0
  84. package/tools/mcp_health_check.json +8 -0
  85. package/tools/mcp_self_test.json +12 -0
  86. package/tools/screenshot_diff.json +16 -0
  87. package/tools/validation_check.json +20 -0
  88. package/tools/validation_decision.json +24 -0
  89. package/tools/validation_element.json +13 -0
  90. package/tools/validation_flow.json +12 -0
  91. package/tools/validation_matrix.json +27 -0
  92. package/tools/validation_quick_run.json +13 -0
  93. package/tools/validation_report.json +10 -0
  94. package/tools/validation_report_export.json +8 -0
  95. package/tools/validation_run.json +35 -0
  96. package/tools/validation_start.json +12 -0
  97. package/tools/validation_suite_run.json +17 -0
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { loadConfig } = require('./config');
6
+
7
+ function ensureDir(dirPath = loadConfig().artifactDir) {
8
+ fs.mkdirSync(dirPath, { recursive: true });
9
+ return dirPath;
10
+ }
11
+
12
+ function listArtifacts(rootDir = loadConfig().artifactDir) {
13
+ if (!fs.existsSync(rootDir)) return [];
14
+ const entries = [];
15
+ for (const name of fs.readdirSync(rootDir)) {
16
+ const fullPath = path.join(rootDir, name);
17
+ const stat = fs.statSync(fullPath);
18
+ if (stat.isDirectory()) {
19
+ entries.push(...listArtifacts(fullPath));
20
+ } else {
21
+ entries.push({ path: fullPath, size: stat.size, mtimeMs: stat.mtimeMs });
22
+ }
23
+ }
24
+ return entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
25
+ }
26
+
27
+ function cleanupOld(rootDir = loadConfig().artifactDir, retentionDays = 7) {
28
+ if (!fs.existsSync(rootDir)) return { removed: 0, files: [] };
29
+ const cutoff = Date.now() - Number(retentionDays) * 24 * 60 * 60 * 1000;
30
+ const removed = [];
31
+ for (const artifact of listArtifacts(rootDir)) {
32
+ if (artifact.mtimeMs < cutoff) {
33
+ fs.unlinkSync(artifact.path);
34
+ removed.push(artifact.path);
35
+ }
36
+ }
37
+ return { removed: removed.length, files: removed };
38
+ }
39
+
40
+ module.exports = {
41
+ ensureDir,
42
+ listArtifacts,
43
+ cleanupOld
44
+ };
package/core/config.js ADDED
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const DEFAULT_CONFIG = {
6
+ artifactDir: path.join(__dirname, '..', 'artifacts'),
7
+ redaction: true,
8
+ allowlist: ['localhost', '127.0.0.1', '::1'],
9
+ blockedHosts: [],
10
+ headless: true
11
+ };
12
+
13
+ function parseList(value) {
14
+ if (!value) return [];
15
+ return String(value).split(',').map(item => item.trim()).filter(Boolean);
16
+ }
17
+
18
+ function parseBool(value, fallback) {
19
+ if (value === undefined) return fallback;
20
+ return !/^(false|0|no)$/i.test(String(value));
21
+ }
22
+
23
+ function loadConfig(overrides = {}) {
24
+ const envConfig = {
25
+ artifactDir: process.env.VALIDPILOT_ARTIFACT_DIR || DEFAULT_CONFIG.artifactDir,
26
+ redaction: parseBool(process.env.VALIDPILOT_REDACTION, DEFAULT_CONFIG.redaction),
27
+ allowlist: parseList(process.env.VALIDPILOT_ALLOWLIST).length ? parseList(process.env.VALIDPILOT_ALLOWLIST) : DEFAULT_CONFIG.allowlist,
28
+ blockedHosts: parseList(process.env.VALIDPILOT_BLOCKED_HOSTS),
29
+ headless: parseBool(process.env.VALIDPILOT_HEADLESS, DEFAULT_CONFIG.headless)
30
+ };
31
+ return { ...DEFAULT_CONFIG, ...envConfig, ...overrides };
32
+ }
33
+
34
+ module.exports = {
35
+ DEFAULT_CONFIG,
36
+ loadConfig
37
+ };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const SENSITIVE_KEY_RE = /(password|passwd|pwd|token|secret|authorization|cookie|apikey|api_key|api-key|key)$/i;
4
+ const SENSITIVE_TEXT_PATTERNS = [
5
+ /Bearer\s+[A-Za-z0-9._~+\/-]+=*/gi,
6
+ /ark-[A-Za-z0-9-]{20,}/gi,
7
+ /(api[_-]?key\s*[:=]\s*)[A-Za-z0-9._~+\/-]{8,}/gi,
8
+ /(token\s*[:=]\s*)[A-Za-z0-9._~+\/-]{8,}/gi
9
+ ];
10
+
11
+ function redactString(value) {
12
+ let text = String(value ?? '');
13
+ for (const pattern of SENSITIVE_TEXT_PATTERNS) {
14
+ text = text.replace(pattern, match => {
15
+ const prefix = match.match(/^(api[_-]?key\s*[:=]\s*|token\s*[:=]\s*)/i)?.[0] || '';
16
+ return `${prefix}******`;
17
+ });
18
+ }
19
+ return text;
20
+ }
21
+
22
+ function isSensitiveKey(key = '') {
23
+ return SENSITIVE_KEY_RE.test(String(key));
24
+ }
25
+
26
+ function redact(value, key = '') {
27
+ if (value == null) return value;
28
+ if (isSensitiveKey(key)) return '******';
29
+ if (typeof value === 'string') return redactString(value);
30
+ if (typeof value !== 'object') return value;
31
+ if (Array.isArray(value)) return value.map(item => redact(item));
32
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redact(v, k)]));
33
+ }
34
+
35
+ module.exports = {
36
+ redactString,
37
+ isSensitiveKey,
38
+ redact
39
+ };
package/core/report.js ADDED
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const { redact } = require('./redaction');
4
+
5
+ function buildJsonReport(data = {}) {
6
+ return {
7
+ generatedAt: new Date().toISOString(),
8
+ ok: data.ok ?? data.pass ?? data.passed ?? false,
9
+ passed: data.passed ?? data.pass ?? data.ok ?? false,
10
+ summary: data.summary || '',
11
+ data: redact(data.data || data),
12
+ artifacts: Array.isArray(data.artifacts) ? data.artifacts : [],
13
+ errors: Array.isArray(data.errors) ? data.errors : []
14
+ };
15
+ }
16
+
17
+ function escapeHtml(value) {
18
+ return String(value ?? '')
19
+ .replace(/&/g, '&amp;')
20
+ .replace(/</g, '&lt;')
21
+ .replace(/>/g, '&gt;')
22
+ .replace(/"/g, '&quot;');
23
+ }
24
+
25
+ function buildHtmlReport(data = {}) {
26
+ const report = buildJsonReport(data);
27
+ return `<!doctype html>
28
+ <html lang="zh-CN">
29
+ <head><meta charset="utf-8"><title>ValidPilot Report</title></head>
30
+ <body>
31
+ <h1>ValidPilot Report</h1>
32
+ <p>Status: ${report.passed ? 'pass' : 'fail'}</p>
33
+ <p>${escapeHtml(report.summary)}</p>
34
+ <pre>${escapeHtml(JSON.stringify(report, null, 2))}</pre>
35
+ </body>
36
+ </html>`;
37
+ }
38
+
39
+ module.exports = {
40
+ buildJsonReport,
41
+ buildHtmlReport
42
+ };
package/core/result.js ADDED
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ function buildResult(options = {}) {
4
+ const ok = options.ok ?? options.passed ?? false;
5
+ const passed = options.passed ?? ok;
6
+ return {
7
+ ok: Boolean(ok),
8
+ passed: Boolean(passed),
9
+ summary: options.summary || (passed ? 'pass' : 'fail'),
10
+ data: options.data || null,
11
+ artifacts: Array.isArray(options.artifacts) ? options.artifacts : [],
12
+ errors: Array.isArray(options.errors) ? options.errors : []
13
+ };
14
+ }
15
+
16
+ function pass(summary = 'pass', data = null, artifacts = []) {
17
+ return buildResult({ ok: true, passed: true, summary, data, artifacts });
18
+ }
19
+
20
+ function fail(summary = 'fail', errors = [], data = null, artifacts = []) {
21
+ return buildResult({ ok: false, passed: false, summary, errors: Array.isArray(errors) ? errors : [errors], data, artifacts });
22
+ }
23
+
24
+ module.exports = {
25
+ buildResult,
26
+ result: buildResult,
27
+ pass,
28
+ fail
29
+ };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const { loadConfig } = require('./config');
4
+
5
+ function hostMatches(hostname, pattern) {
6
+ const host = String(hostname || '').toLowerCase();
7
+ const rule = String(pattern || '').toLowerCase();
8
+ if (!rule) return false;
9
+ return host === rule || host.endsWith(`.${rule}`);
10
+ }
11
+
12
+ function checkUrl(url, config = loadConfig()) {
13
+ let parsed;
14
+ try {
15
+ parsed = new URL(url);
16
+ } catch (error) {
17
+ return { allowed: false, reason: `invalid url: ${error.message}` };
18
+ }
19
+
20
+ if (!['http:', 'https:', 'file:'].includes(parsed.protocol)) {
21
+ return { allowed: false, reason: `unsupported protocol: ${parsed.protocol}` };
22
+ }
23
+
24
+ if (parsed.protocol === 'file:') {
25
+ return { allowed: true, reason: 'file protocol allowed' };
26
+ }
27
+
28
+ const hostname = parsed.hostname;
29
+ if ((config.blockedHosts || []).some(host => hostMatches(hostname, host))) {
30
+ return { allowed: false, reason: `blocked host: ${hostname}` };
31
+ }
32
+
33
+ const allowlist = config.allowlist || [];
34
+ if (allowlist.length && !allowlist.some(host => hostMatches(hostname, host))) {
35
+ return { allowed: false, reason: `host not in allowlist: ${hostname}` };
36
+ }
37
+
38
+ return { allowed: true, reason: 'allowed' };
39
+ }
40
+
41
+ function safeUrlLog(url) {
42
+ try {
43
+ const parsed = new URL(url);
44
+ parsed.username = '';
45
+ parsed.password = '';
46
+ parsed.search = parsed.search ? '?…' : '';
47
+ parsed.hash = '';
48
+ return parsed.toString();
49
+ } catch (_) {
50
+ return String(url || '').slice(0, 200);
51
+ }
52
+ }
53
+
54
+ module.exports = {
55
+ checkUrl,
56
+ safeUrlLog
57
+ };
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ class ChromeMCPAdapter {
8
+ constructor(options = {}) {
9
+ this.browser = null;
10
+ this.pages = new Map();
11
+ this.defaultPage = null;
12
+ this._engine = null; // 'puppeteer' or 'playwright'
13
+ this.options = {
14
+ headless: options.headless !== false,
15
+ executablePath: options.executablePath || null,
16
+ cdpPort: options.cdpPort || 9222,
17
+ ...options
18
+ };
19
+ }
20
+
21
+ async launch(options = {}) {
22
+ const mergedOptions = { ...this.options, ...options };
23
+
24
+ // Try puppeteer-core first
25
+ try {
26
+ const puppeteer = require('puppeteer-core');
27
+ this.browser = await puppeteer.launch({
28
+ headless: mergedOptions.headless !== false ? 'new' : false,
29
+ executablePath: mergedOptions.executablePath || undefined,
30
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
31
+ });
32
+ this._engine = 'puppeteer';
33
+ return this.browser;
34
+ } catch (e) {
35
+ // Fall through to Playwright approach
36
+ }
37
+
38
+ // Fall back to Playwright Chromium (already available as a dependency)
39
+ try {
40
+ const { chromium } = require('playwright');
41
+ this.browser = await chromium.launch({
42
+ headless: mergedOptions.headless !== false,
43
+ executablePath: mergedOptions.executablePath || undefined
44
+ });
45
+ this._engine = 'playwright';
46
+ return this.browser;
47
+ } catch (e) {
48
+ throw new Error(
49
+ `ChromeMCPAdapter: Cannot launch browser. ` +
50
+ `Install puppeteer-core or ensure Playwright is available. Error: ${e.message}`
51
+ );
52
+ }
53
+ }
54
+
55
+ async newPage(options = {}) {
56
+ if (!this.browser) {
57
+ throw new Error('ChromeMCPAdapter: Browser not launched. Call launch() first.');
58
+ }
59
+
60
+ let page;
61
+
62
+ if (this._engine === 'puppeteer') {
63
+ page = await this.browser.newPage();
64
+ if (options.viewport) {
65
+ await page.setViewport(options.viewport);
66
+ }
67
+ } else {
68
+ // Playwright
69
+ page = await this.browser.newPage({
70
+ viewport: options.viewport || { width: 1440, height: 900 }
71
+ });
72
+ }
73
+
74
+ // Generate a unique page name if none provided
75
+ const pageName = options.name || `page_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
76
+ this.pages.set(pageName, { page, name: pageName, createdAt: new Date().toISOString() });
77
+
78
+ if (!this.defaultPage) {
79
+ this.defaultPage = page;
80
+ }
81
+
82
+ return page;
83
+ }
84
+
85
+ async close() {
86
+ // Close all pages
87
+ for (const [name, entry] of this.pages) {
88
+ try {
89
+ if (entry.page && typeof entry.page.close === 'function') {
90
+ await entry.page.close();
91
+ }
92
+ } catch (e) {
93
+ // Ignore errors closing individual pages
94
+ }
95
+ }
96
+ this.pages.clear();
97
+ this.defaultPage = null;
98
+
99
+ // Close browser
100
+ if (this.browser && typeof this.browser.close === 'function') {
101
+ try {
102
+ await this.browser.close();
103
+ } catch (e) {
104
+ // Ignore errors closing browser
105
+ }
106
+ }
107
+ this.browser = null;
108
+ this._engine = null;
109
+ }
110
+
111
+ getPage(name = null) {
112
+ if (!this.browser) {
113
+ throw new Error('ChromeMCPAdapter: Browser not launched. Call launch() first.');
114
+ }
115
+
116
+ if (name === null) {
117
+ if (!this.defaultPage) {
118
+ throw new Error('ChromeMCPAdapter: No default page. Call newPage() first.');
119
+ }
120
+ return this.defaultPage;
121
+ }
122
+
123
+ const entry = this.pages.get(name);
124
+ if (!entry || !entry.page) {
125
+ throw new Error(`ChromeMCPAdapter: Page '${name}' not found.`);
126
+ }
127
+ return entry.page;
128
+ }
129
+
130
+ async goto(url, options = {}) {
131
+ const page = this.getPage();
132
+ const waitUntil = options.waitUntil || 'domcontentloaded';
133
+
134
+ if (this._engine === 'puppeteer') {
135
+ await page.goto(url, { waitUntil, timeout: options.timeout || 30000 });
136
+ } else {
137
+ await page.goto(url, { waitUntil, timeout: options.timeout || 30000 });
138
+ }
139
+
140
+ return page;
141
+ }
142
+
143
+ async screenshot(options = {}) {
144
+ const page = this.getPage();
145
+ const screenshotOptions = { fullPage: options.fullPage !== false, type: options.type || 'png' };
146
+
147
+ if (options.path) {
148
+ screenshotOptions.path = options.path;
149
+ }
150
+
151
+ if (this._engine === 'puppeteer') {
152
+ return await page.screenshot(screenshotOptions);
153
+ } else {
154
+ // Playwright returns Buffer
155
+ return await page.screenshot(screenshotOptions);
156
+ }
157
+ }
158
+
159
+ async evaluate(pageFunction, ...args) {
160
+ const page = this.getPage();
161
+
162
+ if (this._engine === 'puppeteer') {
163
+ return await page.evaluate(pageFunction, ...args);
164
+ } else {
165
+ return await page.evaluate(pageFunction, ...args);
166
+ }
167
+ }
168
+
169
+ async waitForSelector(selector, options = {}) {
170
+ const page = this.getPage();
171
+ const timeout = options.timeout || 10000;
172
+
173
+ if (this._engine === 'puppeteer') {
174
+ return await page.waitForSelector(selector, { timeout, visible: options.visible !== false });
175
+ } else {
176
+ return await page.waitForSelector(selector, {
177
+ timeout,
178
+ state: options.visible !== false ? 'visible' : 'attached'
179
+ });
180
+ }
181
+ }
182
+
183
+ async click(selector, options = {}) {
184
+ const page = this.getPage();
185
+
186
+ if (this._engine === 'puppeteer') {
187
+ await page.waitForSelector(selector, { timeout: options.timeout || 10000 });
188
+ return await page.click(selector);
189
+ } else {
190
+ await page.waitForSelector(selector, {
191
+ timeout: options.timeout || 10000,
192
+ state: 'visible'
193
+ });
194
+ return await page.click(selector);
195
+ }
196
+ }
197
+
198
+ async type(selector, text, options = {}) {
199
+ const page = this.getPage();
200
+
201
+ if (this._engine === 'puppeteer') {
202
+ await page.waitForSelector(selector, { timeout: options.timeout || 10000 });
203
+ await page.click(selector, { clickCount: 3 }); // Select all existing text
204
+ return await page.type(selector, text, { delay: options.delay || 0 });
205
+ } else {
206
+ await page.waitForSelector(selector, {
207
+ timeout: options.timeout || 10000,
208
+ state: 'visible'
209
+ });
210
+ await page.click(selector, { clickCount: 3 }); // Select all existing text
211
+ return await page.type(selector, text, { delay: options.delay || 0 });
212
+ }
213
+ }
214
+
215
+ isConnected() {
216
+ if (!this.browser) {
217
+ return false;
218
+ }
219
+
220
+ try {
221
+ // Playwright: browser.isConnected()
222
+ if (typeof this.browser.isConnected === 'function') {
223
+ return this.browser.isConnected();
224
+ }
225
+ // Puppeteer: browser.isConnected()
226
+ if (typeof this.browser.isConnected === 'function') {
227
+ return this.browser.isConnected();
228
+ }
229
+ // Fallback: if browser exists and no error thrown, assume connected
230
+ return true;
231
+ } catch (e) {
232
+ return false;
233
+ }
234
+ }
235
+
236
+ static detectChromePath() {
237
+ const platform = os.platform();
238
+
239
+ if (platform === 'win32') {
240
+ const winPaths = [
241
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
242
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
243
+ path.join(process.env.LOCALAPPDATA || '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
244
+ 'C:\\Program Files\\Chromium\\Application\\chrome.exe'
245
+ ];
246
+ for (const p of winPaths) {
247
+ if (fs.existsSync(p)) {
248
+ return p;
249
+ }
250
+ }
251
+
252
+ // Try registry lookup
253
+ try {
254
+ const { execSync } = require('child_process');
255
+ const regQuery = execSync(
256
+ 'reg query "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe" /ve 2>nul',
257
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
258
+ );
259
+ const match = regQuery.match(/([A-Z]:\\[^\r\n]+\.exe)/i);
260
+ if (match && fs.existsSync(match[1])) {
261
+ return match[1];
262
+ }
263
+ } catch (e) {
264
+ // Registry query failed
265
+ }
266
+ }
267
+
268
+ if (platform === 'darwin') {
269
+ const macPaths = [
270
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
271
+ '/Applications/Chromium.app/Contents/MacOS/Chromium'
272
+ ];
273
+ for (const p of macPaths) {
274
+ if (fs.existsSync(p)) {
275
+ return p;
276
+ }
277
+ }
278
+ }
279
+
280
+ if (platform === 'linux') {
281
+ const linuxPaths = [
282
+ '/usr/bin/google-chrome',
283
+ '/usr/bin/chromium-browser',
284
+ '/usr/bin/chromium',
285
+ '/snap/bin/chromium'
286
+ ];
287
+ for (const p of linuxPaths) {
288
+ if (fs.existsSync(p)) {
289
+ return p;
290
+ }
291
+ }
292
+
293
+ // Try which command
294
+ try {
295
+ const { execSync } = require('child_process');
296
+ const names = ['google-chrome', 'chromium-browser', 'chromium'];
297
+ for (const name of names) {
298
+ try {
299
+ const result = execSync(`which ${name} 2>/dev/null`, {
300
+ encoding: 'utf8',
301
+ stdio: ['pipe', 'pipe', 'pipe']
302
+ }).trim();
303
+ if (result && fs.existsSync(result)) {
304
+ return result;
305
+ }
306
+ } catch (e) {
307
+ // Command not found
308
+ }
309
+ }
310
+ } catch (e) {
311
+ // which command failed
312
+ }
313
+ }
314
+
315
+ return null;
316
+ }
317
+ }
318
+
319
+ module.exports = { ChromeMCPAdapter };