chrometools-mcp 2.5.0 → 3.1.2

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 (48) hide show
  1. package/CHANGELOG.md +420 -0
  2. package/COMPONENT_MAPPING_SPEC.md +1217 -0
  3. package/README.md +406 -38
  4. package/bridge/bridge-client.js +472 -0
  5. package/bridge/bridge-service.js +399 -0
  6. package/bridge/install.js +241 -0
  7. package/browser/browser-manager.js +107 -2
  8. package/browser/page-manager.js +226 -69
  9. package/docs/CHROME_EXTENSION.md +219 -0
  10. package/docs/PAGE_OBJECT_MODEL_CONCEPT.md +1756 -0
  11. package/extension/background.js +643 -0
  12. package/extension/content.js +715 -0
  13. package/extension/icons/create-icons.js +164 -0
  14. package/extension/icons/icon128.png +0 -0
  15. package/extension/icons/icon16.png +0 -0
  16. package/extension/icons/icon48.png +0 -0
  17. package/extension/manifest.json +58 -0
  18. package/extension/popup/popup.css +437 -0
  19. package/extension/popup/popup.html +102 -0
  20. package/extension/popup/popup.js +415 -0
  21. package/extension/recorder-overlay.css +93 -0
  22. package/index.js +3347 -2901
  23. package/models/BaseInputModel.js +93 -0
  24. package/models/CheckboxGroupModel.js +199 -0
  25. package/models/CheckboxModel.js +103 -0
  26. package/models/ColorInputModel.js +53 -0
  27. package/models/DateInputModel.js +67 -0
  28. package/models/RadioGroupModel.js +126 -0
  29. package/models/RangeInputModel.js +60 -0
  30. package/models/SelectModel.js +97 -0
  31. package/models/TextInputModel.js +34 -0
  32. package/models/TextareaModel.js +59 -0
  33. package/models/TimeInputModel.js +49 -0
  34. package/models/index.js +122 -0
  35. package/package.json +3 -2
  36. package/pom/apom-converter.js +267 -0
  37. package/pom/apom-tree-converter.js +515 -0
  38. package/pom/element-id-generator.js +175 -0
  39. package/recorder/page-object-generator.js +16 -0
  40. package/recorder/scenario-executor.js +80 -2
  41. package/server/tool-definitions.js +839 -713
  42. package/server/tool-groups.js +1 -1
  43. package/server/tool-schemas.js +367 -326
  44. package/server/websocket-bridge.js +447 -0
  45. package/utils/selector-resolver.js +186 -0
  46. package/utils/ui-framework-detector.js +392 -0
  47. package/RELEASE_NOTES_v2.5.0.md +0 -109
  48. package/npm_publish_output.txt +0 -0
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bridge-service.js
4
+ *
5
+ * Native Messaging Host that acts as a persistent bridge between
6
+ * Chrome Extension and Claude/MCP clients.
7
+ *
8
+ * - Launched by Chrome when Extension starts
9
+ * - Maintains state (tabs, recordings, events)
10
+ * - Accepts 0-8 WebSocket clients
11
+ * - Clients get full state on connect + real-time updates
12
+ */
13
+
14
+ import { WebSocketServer } from 'ws';
15
+ import { createRequire } from 'module';
16
+ import { saveScenario } from '../recorder/scenario-storage.js';
17
+ import { urlToProjectId } from '../utils/url-to-project.js';
18
+
19
+ // For Native Messaging protocol
20
+ const require = createRequire(import.meta.url);
21
+
22
+ // ============================================
23
+ // Configuration
24
+ // ============================================
25
+
26
+ const WS_PORT = 9223;
27
+ const MAX_CLIENTS = 8;
28
+ const HOST_NAME = 'com.chrometools.bridge';
29
+
30
+ // ============================================
31
+ // State Store
32
+ // ============================================
33
+
34
+ const state = {
35
+ tabs: new Map(), // tabId -> {url, title, active, ...}
36
+ recordings: [], // Array of recorded actions
37
+ recorderState: {
38
+ isRecording: false,
39
+ isPaused: false,
40
+ metadata: null,
41
+ secrets: {}
42
+ },
43
+ eventLog: [], // Ring buffer of recent events (max 1000)
44
+ extensionConnected: false
45
+ };
46
+
47
+ const MAX_EVENT_LOG = 1000;
48
+
49
+ // ============================================
50
+ // WebSocket Server for Claude/MCP Clients
51
+ // ============================================
52
+
53
+ let wss = null;
54
+ const clients = new Set();
55
+
56
+ function startWebSocketServer() {
57
+ wss = new WebSocketServer({ port: WS_PORT, host: '127.0.0.1' });
58
+
59
+ wss.on('listening', () => {
60
+ log(`WebSocket server listening on port ${WS_PORT}`);
61
+ });
62
+
63
+ wss.on('connection', (ws) => {
64
+ if (clients.size >= MAX_CLIENTS) {
65
+ log(`Max clients (${MAX_CLIENTS}) reached, rejecting connection`);
66
+ ws.close(1013, 'Max clients reached');
67
+ return;
68
+ }
69
+
70
+ clients.add(ws);
71
+ log(`Client connected (${clients.size}/${MAX_CLIENTS})`);
72
+
73
+ // Send full state immediately on connect
74
+ sendToClient(ws, {
75
+ type: 'initial_state',
76
+ payload: getFullState()
77
+ });
78
+
79
+ ws.on('message', (data) => {
80
+ try {
81
+ const message = JSON.parse(data.toString());
82
+ handleClientMessage(ws, message);
83
+ } catch (error) {
84
+ log(`Failed to parse client message: ${error.message}`);
85
+ }
86
+ });
87
+
88
+ ws.on('close', () => {
89
+ clients.delete(ws);
90
+ log(`Client disconnected (${clients.size}/${MAX_CLIENTS})`);
91
+ });
92
+
93
+ ws.on('error', (error) => {
94
+ log(`Client error: ${error.message}`);
95
+ });
96
+ });
97
+
98
+ wss.on('error', (error) => {
99
+ log(`WebSocket server error: ${error.message}`);
100
+ });
101
+ }
102
+
103
+ function getFullState() {
104
+ return {
105
+ tabs: Array.from(state.tabs.values()),
106
+ recordings: state.recordings,
107
+ recorderState: state.recorderState,
108
+ extensionConnected: state.extensionConnected,
109
+ timestamp: Date.now()
110
+ };
111
+ }
112
+
113
+ function sendToClient(ws, message) {
114
+ if (ws.readyState === 1) { // OPEN
115
+ ws.send(JSON.stringify(message));
116
+ }
117
+ }
118
+
119
+ function broadcastToClients(message) {
120
+ const data = JSON.stringify(message);
121
+ for (const client of clients) {
122
+ if (client.readyState === 1) {
123
+ client.send(data);
124
+ }
125
+ }
126
+ }
127
+
128
+ function handleClientMessage(ws, message) {
129
+ log(`Client message: ${message.type}`);
130
+
131
+ switch (message.type) {
132
+ case 'get_state':
133
+ sendToClient(ws, {
134
+ type: 'state',
135
+ payload: getFullState(),
136
+ requestId: message.requestId
137
+ });
138
+ break;
139
+
140
+ case 'get_tabs':
141
+ sendToClient(ws, {
142
+ type: 'tabs',
143
+ payload: { tabs: Array.from(state.tabs.values()) },
144
+ requestId: message.requestId
145
+ });
146
+ break;
147
+
148
+ case 'get_recorder_state':
149
+ sendToClient(ws, {
150
+ type: 'recorder_state',
151
+ payload: state.recorderState,
152
+ requestId: message.requestId
153
+ });
154
+ break;
155
+
156
+ // Commands to forward to extension
157
+ case 'start_recording':
158
+ case 'stop_recording':
159
+ case 'pause_recording':
160
+ case 'switch_tab':
161
+ case 'recorder_start':
162
+ case 'recorder_stop':
163
+ case 'recorder_pause':
164
+ // Forward to extension via Native Messaging
165
+ sendToExtension({
166
+ type: message.type,
167
+ payload: message.payload,
168
+ requestId: message.requestId
169
+ });
170
+ break;
171
+
172
+ default:
173
+ log(`Unknown client message type: ${message.type}`);
174
+ }
175
+ }
176
+
177
+ // ============================================
178
+ // Native Messaging (communication with Extension)
179
+ // ============================================
180
+
181
+ let nativePort = null;
182
+
183
+ function initNativeMessaging() {
184
+ // Native Messaging uses stdin/stdout with length-prefixed JSON
185
+ process.stdin.on('readable', () => {
186
+ let chunk;
187
+ while ((chunk = process.stdin.read(4)) !== null) {
188
+ const length = chunk.readUInt32LE(0);
189
+ const data = process.stdin.read(length);
190
+ if (data) {
191
+ try {
192
+ const message = JSON.parse(data.toString());
193
+ handleExtensionMessage(message);
194
+ } catch (error) {
195
+ log(`Failed to parse extension message: ${error.message}`);
196
+ }
197
+ }
198
+ }
199
+ });
200
+
201
+ process.stdin.on('end', () => {
202
+ log('Extension disconnected (stdin closed)');
203
+ state.extensionConnected = false;
204
+ broadcastToClients({ type: 'extension_disconnected' });
205
+ // Chrome closed the connection, we should exit
206
+ setTimeout(() => process.exit(0), 100);
207
+ });
208
+
209
+ state.extensionConnected = true;
210
+ log('Native Messaging initialized');
211
+ }
212
+
213
+ function sendToExtension(message) {
214
+ const json = JSON.stringify(message);
215
+ const buffer = Buffer.alloc(4 + json.length);
216
+ buffer.writeUInt32LE(json.length, 0);
217
+ buffer.write(json, 4);
218
+ process.stdout.write(buffer);
219
+ }
220
+
221
+ function handleExtensionMessage(message) {
222
+ log(`Extension message: ${message.type}`);
223
+ addToEventLog(message);
224
+
225
+ switch (message.type) {
226
+ case 'tabs_sync':
227
+ // Full tabs sync
228
+ state.tabs.clear();
229
+ if (message.payload?.tabs) {
230
+ message.payload.tabs.forEach(tab => {
231
+ state.tabs.set(tab.tabId, tab);
232
+ });
233
+ }
234
+ broadcastToClients({
235
+ type: 'tabs_sync',
236
+ payload: { tabs: Array.from(state.tabs.values()) }
237
+ });
238
+ break;
239
+
240
+ case 'tab_created':
241
+ state.tabs.set(message.payload.tabId, message.payload);
242
+ broadcastToClients({ type: 'tab_created', payload: message.payload });
243
+ break;
244
+
245
+ case 'tab_closed':
246
+ state.tabs.delete(message.payload.tabId);
247
+ broadcastToClients({ type: 'tab_closed', payload: message.payload });
248
+ break;
249
+
250
+ case 'tab_activated':
251
+ // Update active status
252
+ for (const [id, tab] of state.tabs) {
253
+ tab.active = (id === message.payload.tabId);
254
+ }
255
+ broadcastToClients({ type: 'tab_activated', payload: message.payload });
256
+ break;
257
+
258
+ case 'tab_updated':
259
+ if (message.payload.tab) {
260
+ state.tabs.set(message.payload.tabId, message.payload.tab);
261
+ }
262
+ broadcastToClients({ type: 'tab_updated', payload: message.payload });
263
+ break;
264
+
265
+ case 'recorder_state_changed':
266
+ state.recorderState = { ...state.recorderState, ...message.payload };
267
+ broadcastToClients({ type: 'recorder_state_changed', payload: message.payload });
268
+ break;
269
+
270
+ case 'action_recorded':
271
+ state.recordings.push(message.payload);
272
+ broadcastToClients({ type: 'action_recorded', payload: message.payload });
273
+ break;
274
+
275
+ case 'recordings_cleared':
276
+ state.recordings = [];
277
+ broadcastToClients({ type: 'recordings_cleared' });
278
+ break;
279
+
280
+ case 'scenario_save':
281
+ // Handle scenario save from extension popup
282
+ handleScenarioSave(message.payload, message.requestId);
283
+ break;
284
+
285
+ // Responses to forward to requesting client
286
+ case 'recorder_started':
287
+ case 'recorder_stopped':
288
+ case 'recorder_paused':
289
+ case 'scenario_saved':
290
+ case 'scenario_list_response':
291
+ broadcastToClients(message);
292
+ break;
293
+
294
+ case 'pong':
295
+ broadcastToClients({ type: 'pong' });
296
+ break;
297
+
298
+ default:
299
+ // Forward unknown messages to clients
300
+ broadcastToClients(message);
301
+ }
302
+ }
303
+
304
+ function addToEventLog(event) {
305
+ state.eventLog.push({
306
+ ...event,
307
+ timestamp: Date.now()
308
+ });
309
+ // Keep ring buffer bounded
310
+ if (state.eventLog.length > MAX_EVENT_LOG) {
311
+ state.eventLog.shift();
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Handle scenario save from extension popup
317
+ */
318
+ async function handleScenarioSave(scenario, requestId) {
319
+ try {
320
+ // Determine project ID from entry URL
321
+ const entryUrl = scenario.metadata?.entryUrl || scenario.entryUrl || '';
322
+ const projectId = urlToProjectId(entryUrl);
323
+
324
+ log(`Saving scenario "${scenario.name}" to project "${projectId}"`);
325
+
326
+ // Convert actions to chain format if needed (popup sends 'actions', storage expects 'chain')
327
+ const scenarioToSave = {
328
+ ...scenario,
329
+ chain: scenario.chain || scenario.actions || []
330
+ };
331
+
332
+ const result = await saveScenario(scenarioToSave, projectId);
333
+
334
+ // Send response back to extension
335
+ sendToExtension({
336
+ type: 'scenario_saved',
337
+ payload: {
338
+ success: result.success,
339
+ filePath: result.filePath,
340
+ error: result.error
341
+ },
342
+ requestId
343
+ });
344
+
345
+ // Also notify clients
346
+ broadcastToClients({
347
+ type: 'scenario_saved',
348
+ payload: {
349
+ success: result.success,
350
+ name: scenario.name,
351
+ projectId
352
+ },
353
+ requestId
354
+ });
355
+
356
+ log(`Scenario saved: ${scenario.name}`);
357
+ } catch (error) {
358
+ log(`Failed to save scenario: ${error.message}`);
359
+
360
+ sendToExtension({
361
+ type: 'scenario_saved',
362
+ payload: {
363
+ success: false,
364
+ error: error.message
365
+ },
366
+ requestId
367
+ });
368
+ }
369
+ }
370
+
371
+ // ============================================
372
+ // Logging
373
+ // ============================================
374
+
375
+ function log(message) {
376
+ // Write to stderr (Native Messaging uses stdout for protocol)
377
+ console.error(`[bridge] ${new Date().toISOString()} ${message}`);
378
+ }
379
+
380
+ // ============================================
381
+ // Main
382
+ // ============================================
383
+
384
+ function main() {
385
+ log(`Starting ${HOST_NAME} bridge service`);
386
+
387
+ // Start WebSocket server for clients
388
+ startWebSocketServer();
389
+
390
+ // Initialize Native Messaging with extension
391
+ initNativeMessaging();
392
+
393
+ // Send ready message to extension
394
+ sendToExtension({ type: 'bridge_ready' });
395
+
396
+ log('Bridge service ready');
397
+ }
398
+
399
+ main();
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * install.js
4
+ *
5
+ * Installs the Native Messaging Host for ChromeTools Bridge.
6
+ * Run via: npx chrometools-mcp --install-bridge
7
+ */
8
+
9
+ import { execSync } from 'child_process';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import os from 'os';
13
+ import { fileURLToPath } from 'url';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+
18
+ // ============================================
19
+ // Configuration
20
+ // ============================================
21
+
22
+ const HOST_NAME = 'com.chrometools.bridge';
23
+ const EXTENSION_ID = 'dmehkibmncgphijnigkahhlekgajhpbl';
24
+
25
+ // Where to install bridge files
26
+ const BRIDGE_DIR = path.join(os.homedir(), '.chrometools');
27
+
28
+ // ============================================
29
+ // Installation
30
+ // ============================================
31
+
32
+ export async function installBridge(options = {}) {
33
+ const { silent = false } = options;
34
+
35
+ const log = silent ? () => {} : console.log;
36
+ const error = console.error;
37
+
38
+ try {
39
+ log('Installing ChromeTools Bridge Service...\n');
40
+
41
+ // 1. Create bridge directory
42
+ log(`Creating directory: ${BRIDGE_DIR}`);
43
+ fs.mkdirSync(BRIDGE_DIR, { recursive: true });
44
+
45
+ // 2. Store path to project directory (where node_modules lives)
46
+ const projectDir = path.resolve(__dirname, '..');
47
+ const bridgeSrc = path.join(__dirname, 'bridge-service.js');
48
+
49
+ // Save project path for reference
50
+ const configPath = path.join(BRIDGE_DIR, 'config.json');
51
+ fs.writeFileSync(configPath, JSON.stringify({
52
+ projectDir,
53
+ bridgeScript: bridgeSrc,
54
+ installedAt: new Date().toISOString()
55
+ }, null, 2));
56
+ log(`Saved config: ${configPath}`);
57
+
58
+ // 3. Create launcher script that runs from project directory
59
+ let launcherPath;
60
+ if (process.platform === 'win32') {
61
+ launcherPath = path.join(BRIDGE_DIR, 'bridge.cmd');
62
+ const nodePath = process.execPath;
63
+ // Change to project directory before running (so node_modules is available)
64
+ const cmd = `@echo off\r\ncd /d "${projectDir}"\r\n"${nodePath}" "${bridgeSrc}"\r\n`;
65
+ fs.writeFileSync(launcherPath, cmd);
66
+ log(`Created launcher: ${launcherPath}`);
67
+ } else {
68
+ launcherPath = path.join(BRIDGE_DIR, 'bridge.sh');
69
+ const sh = `#!/bin/bash\ncd "${projectDir}"\nexec node "${bridgeSrc}"\n`;
70
+ fs.writeFileSync(launcherPath, sh, { mode: 0o755 });
71
+ log(`Created launcher: ${launcherPath}`);
72
+ }
73
+
74
+ // 4. Create Native Messaging manifest
75
+ const manifest = {
76
+ name: HOST_NAME,
77
+ description: 'ChromeTools Bridge Service - Connects Chrome Extension with MCP clients',
78
+ path: launcherPath,
79
+ type: 'stdio',
80
+ allowed_origins: [`chrome-extension://${EXTENSION_ID}/`]
81
+ };
82
+
83
+ const manifestPath = path.join(BRIDGE_DIR, 'native-manifest.json');
84
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
85
+ log(`Created manifest: ${manifestPath}`);
86
+
87
+ // 5. Register in system
88
+ if (process.platform === 'win32') {
89
+ await registerWindows(manifestPath, log, error);
90
+ } else if (process.platform === 'darwin') {
91
+ await registerMacOS(manifestPath, log);
92
+ } else {
93
+ await registerLinux(manifestPath, log);
94
+ }
95
+
96
+ log('\n✅ Bridge installed successfully!\n');
97
+ log('Next steps:');
98
+ log('1. Reload the ChromeTools extension in Chrome (chrome://extensions)');
99
+ log('2. The bridge will start automatically when Chrome opens\n');
100
+
101
+ return { success: true, bridgeDir: BRIDGE_DIR };
102
+
103
+ } catch (err) {
104
+ error(`\n❌ Installation failed: ${err.message}`);
105
+ return { success: false, error: err.message };
106
+ }
107
+ }
108
+
109
+ async function registerWindows(manifestPath, log, error) {
110
+ const regPath = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`;
111
+
112
+ log(`Registering in Windows registry: ${regPath}`);
113
+
114
+ try {
115
+ // Use reg.exe to add the registry key
116
+ execSync(`reg add "${regPath}" /ve /t REG_SZ /d "${manifestPath}" /f`, {
117
+ stdio: 'pipe'
118
+ });
119
+ log('Registry entry created successfully');
120
+ } catch (err) {
121
+ error(`Registry error: ${err.message}`);
122
+ throw new Error('Failed to register Native Messaging Host in Windows registry');
123
+ }
124
+ }
125
+
126
+ async function registerMacOS(manifestPath, log) {
127
+ // Chrome looks in ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/
128
+ const chromeHostsDir = path.join(
129
+ os.homedir(),
130
+ 'Library/Application Support/Google/Chrome/NativeMessagingHosts'
131
+ );
132
+
133
+ fs.mkdirSync(chromeHostsDir, { recursive: true });
134
+
135
+ const destPath = path.join(chromeHostsDir, `${HOST_NAME}.json`);
136
+ fs.copyFileSync(manifestPath, destPath);
137
+ log(`Registered at: ${destPath}`);
138
+ }
139
+
140
+ async function registerLinux(manifestPath, log) {
141
+ // Chrome looks in ~/.config/google-chrome/NativeMessagingHosts/
142
+ const chromeHostsDir = path.join(
143
+ os.homedir(),
144
+ '.config/google-chrome/NativeMessagingHosts'
145
+ );
146
+
147
+ fs.mkdirSync(chromeHostsDir, { recursive: true });
148
+
149
+ const destPath = path.join(chromeHostsDir, `${HOST_NAME}.json`);
150
+ fs.copyFileSync(manifestPath, destPath);
151
+ log(`Registered at: ${destPath}`);
152
+ }
153
+
154
+ // ============================================
155
+ // Uninstallation
156
+ // ============================================
157
+
158
+ export async function uninstallBridge(options = {}) {
159
+ const { silent = false } = options;
160
+ const log = silent ? () => {} : console.log;
161
+
162
+ try {
163
+ log('Uninstalling ChromeTools Bridge Service...\n');
164
+
165
+ // 1. Remove registry entry (Windows)
166
+ if (process.platform === 'win32') {
167
+ const regPath = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`;
168
+ try {
169
+ execSync(`reg delete "${regPath}" /f`, { stdio: 'pipe' });
170
+ log('Removed registry entry');
171
+ } catch {
172
+ // Key might not exist
173
+ }
174
+ }
175
+
176
+ // 2. Remove Chrome config (macOS/Linux)
177
+ const configPaths = [
178
+ path.join(os.homedir(), 'Library/Application Support/Google/Chrome/NativeMessagingHosts', `${HOST_NAME}.json`),
179
+ path.join(os.homedir(), '.config/google-chrome/NativeMessagingHosts', `${HOST_NAME}.json`)
180
+ ];
181
+
182
+ for (const p of configPaths) {
183
+ if (fs.existsSync(p)) {
184
+ fs.unlinkSync(p);
185
+ log(`Removed: ${p}`);
186
+ }
187
+ }
188
+
189
+ // 3. Remove bridge directory
190
+ if (fs.existsSync(BRIDGE_DIR)) {
191
+ fs.rmSync(BRIDGE_DIR, { recursive: true });
192
+ log(`Removed: ${BRIDGE_DIR}`);
193
+ }
194
+
195
+ log('\n✅ Bridge uninstalled successfully!');
196
+ return { success: true };
197
+
198
+ } catch (err) {
199
+ console.error(`\n❌ Uninstallation failed: ${err.message}`);
200
+ return { success: false, error: err.message };
201
+ }
202
+ }
203
+
204
+ // ============================================
205
+ // Check Installation
206
+ // ============================================
207
+
208
+ export function isBridgeInstalled() {
209
+ // Check if manifest exists in expected location
210
+ if (process.platform === 'win32') {
211
+ try {
212
+ const result = execSync(
213
+ `reg query "HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}" /ve`,
214
+ { stdio: 'pipe', encoding: 'utf8' }
215
+ );
216
+ return result.includes(HOST_NAME);
217
+ } catch {
218
+ return false;
219
+ }
220
+ } else {
221
+ const configPaths = [
222
+ path.join(os.homedir(), 'Library/Application Support/Google/Chrome/NativeMessagingHosts', `${HOST_NAME}.json`),
223
+ path.join(os.homedir(), '.config/google-chrome/NativeMessagingHosts', `${HOST_NAME}.json`)
224
+ ];
225
+ return configPaths.some(p => fs.existsSync(p));
226
+ }
227
+ }
228
+
229
+ // ============================================
230
+ // CLI
231
+ // ============================================
232
+
233
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
234
+ const args = process.argv.slice(2);
235
+
236
+ if (args.includes('--uninstall')) {
237
+ uninstallBridge();
238
+ } else {
239
+ installBridge();
240
+ }
241
+ }