chromex-mcp 1.0.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/package.json +50 -0
  4. package/plugins/chromex/skills/chromex/scripts/chromex.mjs +343 -0
  5. package/plugins/chromex/skills/chromex/scripts/lib/browser.mjs +66 -0
  6. package/plugins/chromex/skills/chromex/scripts/lib/client.mjs +98 -0
  7. package/plugins/chromex/skills/chromex/scripts/lib/commands/console.mjs +37 -0
  8. package/plugins/chromex/skills/chromex/scripts/lib/commands/cookies.mjs +77 -0
  9. package/plugins/chromex/skills/chromex/scripts/lib/commands/coverage.mjs +95 -0
  10. package/plugins/chromex/skills/chromex/scripts/lib/commands/cpu.mjs +14 -0
  11. package/plugins/chromex/skills/chromex/scripts/lib/commands/dialog.mjs +38 -0
  12. package/plugins/chromex/skills/chromex/scripts/lib/commands/domsnapshot.mjs +84 -0
  13. package/plugins/chromex/skills/chromex/scripts/lib/commands/download.mjs +25 -0
  14. package/plugins/chromex/skills/chromex/scripts/lib/commands/drag.mjs +71 -0
  15. package/plugins/chromex/skills/chromex/scripts/lib/commands/emulate.mjs +44 -0
  16. package/plugins/chromex/skills/chromex/scripts/lib/commands/evaluate.mjs +31 -0
  17. package/plugins/chromex/skills/chromex/scripts/lib/commands/form.mjs +163 -0
  18. package/plugins/chromex/skills/chromex/scripts/lib/commands/geo.mjs +37 -0
  19. package/plugins/chromex/skills/chromex/scripts/lib/commands/har.mjs +101 -0
  20. package/plugins/chromex/skills/chromex/scripts/lib/commands/heap.mjs +24 -0
  21. package/plugins/chromex/skills/chromex/scripts/lib/commands/highlight.mjs +36 -0
  22. package/plugins/chromex/skills/chromex/scripts/lib/commands/html.mjs +10 -0
  23. package/plugins/chromex/skills/chromex/scripts/lib/commands/inject.mjs +39 -0
  24. package/plugins/chromex/skills/chromex/scripts/lib/commands/interact.mjs +88 -0
  25. package/plugins/chromex/skills/chromex/scripts/lib/commands/intercept.mjs +99 -0
  26. package/plugins/chromex/skills/chromex/scripts/lib/commands/navigate.mjs +45 -0
  27. package/plugins/chromex/skills/chromex/scripts/lib/commands/network.mjs +13 -0
  28. package/plugins/chromex/skills/chromex/scripts/lib/commands/pdf.mjs +16 -0
  29. package/plugins/chromex/skills/chromex/scripts/lib/commands/perf.mjs +98 -0
  30. package/plugins/chromex/skills/chromex/scripts/lib/commands/refs.mjs +67 -0
  31. package/plugins/chromex/skills/chromex/scripts/lib/commands/screenshot.mjs +54 -0
  32. package/plugins/chromex/skills/chromex/scripts/lib/commands/scroll.mjs +44 -0
  33. package/plugins/chromex/skills/chromex/scripts/lib/commands/snapshot.mjs +100 -0
  34. package/plugins/chromex/skills/chromex/scripts/lib/commands/storage.mjs +47 -0
  35. package/plugins/chromex/skills/chromex/scripts/lib/commands/tab.mjs +31 -0
  36. package/plugins/chromex/skills/chromex/scripts/lib/commands/throttle.mjs +38 -0
  37. package/plugins/chromex/skills/chromex/scripts/lib/commands/touch.mjs +62 -0
  38. package/plugins/chromex/skills/chromex/scripts/lib/commands/trace.mjs +51 -0
  39. package/plugins/chromex/skills/chromex/scripts/lib/commands/upload.mjs +43 -0
  40. package/plugins/chromex/skills/chromex/scripts/lib/commands/wait.mjs +84 -0
  41. package/plugins/chromex/skills/chromex/scripts/lib/commands/webauthn.mjs +47 -0
  42. package/plugins/chromex/skills/chromex/scripts/lib/config.mjs +100 -0
  43. package/plugins/chromex/skills/chromex/scripts/lib/daemon.mjs +368 -0
  44. package/plugins/chromex/skills/chromex/scripts/lib/ipc.mjs +178 -0
  45. package/plugins/chromex/skills/chromex/scripts/lib/launcher.mjs +111 -0
  46. package/plugins/chromex/skills/chromex/scripts/lib/security.mjs +48 -0
  47. package/plugins/chromex/skills/chromex/scripts/lib/utils.mjs +47 -0
  48. package/plugins/chromex/skills/chromex/scripts/mcp-server.mjs +726 -0
@@ -0,0 +1,100 @@
1
+ // Configuração centralizada -- paths, defaults, load/save
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { homedir } from 'os';
5
+
6
+ const CONFIG_DIR_NEW = resolve(homedir(), '.chromex');
7
+ const CONFIG_DIR_LEGACY = resolve(homedir(), '.config/cdp-skill');
8
+
9
+ export function getConfigDir() {
10
+ // Preferir novo path; fallback para legado se existir
11
+ if (existsSync(resolve(CONFIG_DIR_NEW, 'config.json'))) return CONFIG_DIR_NEW;
12
+ if (existsSync(resolve(CONFIG_DIR_LEGACY, 'config.json'))) return CONFIG_DIR_LEGACY;
13
+ return CONFIG_DIR_NEW;
14
+ }
15
+
16
+ export function getAuditLogPath(configDir) {
17
+ return resolve(configDir, 'audit.log');
18
+ }
19
+
20
+ export function getSocketDir(configDir) {
21
+ const dir = process.env.XDG_RUNTIME_DIR
22
+ ? resolve(process.env.XDG_RUNTIME_DIR, 'chromex')
23
+ : resolve(configDir, 'run');
24
+ ensureDir(dir);
25
+ return dir;
26
+ }
27
+
28
+ export function getTokenPath(socketDir) {
29
+ return resolve(socketDir, '.token');
30
+ }
31
+
32
+ export function getPagesCachePath(socketDir) {
33
+ return resolve(socketDir, 'pages.json');
34
+ }
35
+
36
+ export function ensureDir(dir) {
37
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
38
+ }
39
+
40
+ const DEFAULTS = {
41
+ commandTimeout: 15000,
42
+ navigationTimeout: 30000,
43
+ idleTimeout: 20 * 60 * 1000,
44
+ allowedDomains: [],
45
+ blockedDomains: [],
46
+ blockedCdpMethods: [
47
+ 'Network.enable',
48
+ 'Network.setRequestInterception',
49
+ 'Network.setCacheDisabled',
50
+ 'Page.setDocumentContent',
51
+ 'Security.disable',
52
+ 'Security.setIgnoreCertificateErrors',
53
+ 'Fetch.enable',
54
+ 'Fetch.fulfillRequest',
55
+ 'Fetch.continueRequest',
56
+ 'Browser.close',
57
+ 'Browser.crashGpuProcess',
58
+ 'Target.disposeBrowserContext',
59
+ 'SystemInfo.getProcessInfo',
60
+ 'Storage.clearDataForOrigin',
61
+ 'Storage.getCookies',
62
+ 'IndexedDB.requestData',
63
+ ],
64
+ auditLog: true,
65
+ socketAuth: true,
66
+ defaultScreenshotPath: '/tmp/screenshot.png',
67
+ };
68
+
69
+ export function loadConfig() {
70
+ const configDir = getConfigDir();
71
+ ensureDir(configDir);
72
+ const configPath = resolve(configDir, 'config.json');
73
+
74
+ let userConfig = {};
75
+ if (existsSync(configPath)) {
76
+ try {
77
+ userConfig = JSON.parse(readFileSync(configPath, 'utf8'));
78
+ } catch { /* config corrompida, usar defaults */ }
79
+ } else {
80
+ // Migrar config legada se existir
81
+ const legacyPath = resolve(CONFIG_DIR_LEGACY, 'config.json');
82
+ if (configDir === CONFIG_DIR_NEW && existsSync(legacyPath)) {
83
+ try {
84
+ copyFileSync(legacyPath, configPath);
85
+ userConfig = JSON.parse(readFileSync(configPath, 'utf8'));
86
+ } catch { /* falha na migração, usar defaults */ }
87
+ } else {
88
+ // Primeira execução: gerar config default
89
+ writeFileSync(configPath, JSON.stringify(DEFAULTS, null, 2));
90
+ }
91
+ }
92
+
93
+ const config = { ...DEFAULTS, ...userConfig };
94
+ config._configDir = configDir;
95
+ config._socketDir = getSocketDir(configDir);
96
+ config._tokenPath = getTokenPath(config._socketDir);
97
+ config._pagesCachePath = getPagesCachePath(config._socketDir);
98
+ config._auditLogPath = getAuditLogPath(configDir);
99
+ return config;
100
+ }
@@ -0,0 +1,368 @@
1
+ // Per-tab daemon: mantém sessão CDP aberta, recebe comandos via Unix socket
2
+ import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'fs';
3
+ import { randomBytes } from 'crypto';
4
+ import net from 'net';
5
+ import { CDP } from './client.mjs';
6
+ import { getWsUrl, getPages, formatPageList } from './browser.mjs';
7
+ import { audit } from './security.mjs';
8
+ import { sockPath } from './utils.mjs';
9
+
10
+ // Importar todos os comandos
11
+ import { snapshotStr } from './commands/snapshot.mjs';
12
+ import { evalStr, evalRawStr } from './commands/evaluate.mjs';
13
+ import { shotStr } from './commands/screenshot.mjs';
14
+ import { navStr } from './commands/navigate.mjs';
15
+ import { htmlStr } from './commands/html.mjs';
16
+ import { netStr } from './commands/network.mjs';
17
+ import { clickStr, clickXyStr, typeStr, loadAllStr, waitForStr } from './commands/interact.mjs';
18
+ import { fillStr, clearStr, selectStr, checkStr, formStr } from './commands/form.mjs';
19
+ import { scrollStr } from './commands/scroll.mjs';
20
+ import { cookiesStr } from './commands/cookies.mjs';
21
+ import { pdfStr } from './commands/pdf.mjs';
22
+ import { consoleStr } from './commands/console.mjs';
23
+ import { storageStr } from './commands/storage.mjs';
24
+ import { emulateStr } from './commands/emulate.mjs';
25
+ import { perfStr } from './commands/perf.mjs';
26
+ // Tier 1
27
+ import { waitLifecycleStr } from './commands/wait.mjs';
28
+ import { openTabStr, closeTabStr, focusTabStr } from './commands/tab.mjs';
29
+ import { dialogStr, setupAutoDialog } from './commands/dialog.mjs';
30
+ import { uploadStr } from './commands/upload.mjs';
31
+ import { geoStr, timezoneStr, localeStr } from './commands/geo.mjs';
32
+ import { throttleStr } from './commands/throttle.mjs';
33
+ import { cpuStr } from './commands/cpu.mjs';
34
+ import { injectStr } from './commands/inject.mjs';
35
+ import { downloadStr } from './commands/download.mjs';
36
+ // Tier 2
37
+ import { interceptStr } from './commands/intercept.mjs';
38
+ import { harStr } from './commands/har.mjs';
39
+ import { coverageStr } from './commands/coverage.mjs';
40
+ // Tier 3
41
+ import { traceStr } from './commands/trace.mjs';
42
+ import { heapStr } from './commands/heap.mjs';
43
+ import { webauthnStr } from './commands/webauthn.mjs';
44
+ import { dragStr } from './commands/drag.mjs';
45
+ import { touchStr } from './commands/touch.mjs';
46
+ import { domsnapshotStr } from './commands/domsnapshot.mjs';
47
+ import { parseRef, clickRefStr, hoverRefStr, fillRefStr } from './commands/refs.mjs';
48
+ import { highlightStr } from './commands/highlight.mjs';
49
+
50
+ export function getOrCreateToken(config) {
51
+ if (!config.socketAuth) return null;
52
+ const tokenPath = config._tokenPath;
53
+ if (existsSync(tokenPath)) {
54
+ return readFileSync(tokenPath, 'utf8').trim();
55
+ }
56
+ const token = randomBytes(32).toString('hex');
57
+ writeFileSync(tokenPath, token, { mode: 0o600 });
58
+ return token;
59
+ }
60
+
61
+ export async function runDaemon(targetId, config) {
62
+ const sp = sockPath(config._socketDir, targetId);
63
+ const authToken = getOrCreateToken(config);
64
+
65
+ const cdp = new CDP(config.commandTimeout);
66
+ try {
67
+ await cdp.connect(getWsUrl());
68
+ } catch (e) {
69
+ process.stderr.write(`Daemon: cannot connect to Chrome: ${e.message}\n`);
70
+ process.exit(1);
71
+ }
72
+
73
+ let sessionId;
74
+ try {
75
+ const res = await cdp.send('Target.attachToTarget', { targetId, flatten: true });
76
+ sessionId = res.sessionId;
77
+ } catch (e) {
78
+ process.stderr.write(`Daemon: attach failed: ${e.message}\n`);
79
+ cdp.close();
80
+ process.exit(1);
81
+ }
82
+
83
+ let alive = true;
84
+ function shutdown() {
85
+ if (!alive) return;
86
+ alive = false;
87
+ server.close();
88
+ try { unlinkSync(sp); } catch { /* socket já removido */ }
89
+ cdp.close();
90
+ process.exit(0);
91
+ }
92
+
93
+ cdp.onEvent('Target.targetDestroyed', (params) => {
94
+ if (params.targetId === targetId) shutdown();
95
+ });
96
+ cdp.onEvent('Target.detachedFromTarget', (params) => {
97
+ if (params.sessionId === sessionId) shutdown();
98
+ });
99
+ cdp.onClose(() => shutdown());
100
+ process.on('SIGTERM', shutdown);
101
+ process.on('SIGINT', shutdown);
102
+
103
+ let idleTimer = setTimeout(shutdown, config.idleTimeout);
104
+ function resetIdle() {
105
+ clearTimeout(idleTimer);
106
+ idleTimer = setTimeout(shutdown, config.idleTimeout);
107
+ }
108
+
109
+ // Ref-based selection state: stores mapping from @eN -> {backendNodeId, role, name}
110
+ let currentRefMap = new Map();
111
+
112
+ async function handleCommand({ cmd, args }) {
113
+ resetIdle();
114
+ const auditResult = { ok: true };
115
+ try {
116
+ // Ref-based dispatch: click @e5, fill @e3 "value", hover @e12
117
+ if (args[0] && parseRef(args[0]) !== null) {
118
+ const refNum = parseRef(args[0]);
119
+ let result;
120
+ if (cmd === 'click') {
121
+ result = await clickRefStr(cdp, sessionId, currentRefMap, refNum);
122
+ } else if (cmd === 'fill') {
123
+ result = await fillRefStr(cdp, sessionId, currentRefMap, refNum, args.slice(1).join(' '));
124
+ } else if (cmd === 'hover') {
125
+ result = await hoverRefStr(cdp, sessionId, currentRefMap, refNum);
126
+ } else {
127
+ throw new Error(`Ref @e${refNum} not supported for command "${cmd}". Use with: click, fill, hover.`);
128
+ }
129
+ audit(cmd, targetId, args, auditResult, config);
130
+ return { ok: true, result };
131
+ }
132
+
133
+ let result;
134
+ switch (cmd) {
135
+ // --- Comandos originais ---
136
+ case 'list': {
137
+ const pages = await getPages(cdp);
138
+ result = formatPageList(pages, config);
139
+ break;
140
+ }
141
+ case 'list_raw': {
142
+ const pages = await getPages(cdp);
143
+ result = JSON.stringify(pages);
144
+ break;
145
+ }
146
+ case 'snap': case 'snapshot': {
147
+ const useRefs = args.includes('--refs') || args.includes('-i');
148
+ const snapResult = await snapshotStr(cdp, sessionId, true, useRefs);
149
+ result = snapResult.text;
150
+ if (useRefs && snapResult.refMap.size > 0) {
151
+ currentRefMap = snapResult.refMap;
152
+ }
153
+ break;
154
+ }
155
+ case 'eval':
156
+ result = await evalStr(cdp, sessionId, args[0]);
157
+ break;
158
+ case 'shot': case 'screenshot': {
159
+ const full = args.includes('--full');
160
+ const file = args.find(a => a && a !== '--full');
161
+ result = await shotStr(cdp, sessionId, file, full, config);
162
+ break;
163
+ }
164
+ case 'html':
165
+ result = await htmlStr(cdp, sessionId, args[0]);
166
+ break;
167
+ case 'nav': case 'navigate':
168
+ result = await navStr(cdp, sessionId, args[0], config);
169
+ break;
170
+ case 'net': case 'network':
171
+ result = await netStr(cdp, sessionId);
172
+ break;
173
+ case 'click':
174
+ result = await clickStr(cdp, sessionId, args[0]);
175
+ break;
176
+ case 'clickxy':
177
+ result = await clickXyStr(cdp, sessionId, args[0], args[1]);
178
+ break;
179
+ case 'type':
180
+ result = await typeStr(cdp, sessionId, args[0]);
181
+ break;
182
+ case 'loadall':
183
+ result = await loadAllStr(cdp, sessionId, args[0], args[1] ? parseInt(args[1]) : 1500);
184
+ break;
185
+ case 'evalraw':
186
+ result = await evalRawStr(cdp, sessionId, args[0], args[1], config);
187
+ break;
188
+ case 'waitfor':
189
+ result = await waitForStr(cdp, sessionId, args[0], args[1] ? parseInt(args[1]) : undefined, config);
190
+ break;
191
+ // --- Novos comandos ---
192
+ case 'fill':
193
+ result = await fillStr(cdp, sessionId, args[0], args.slice(1).join(' '));
194
+ break;
195
+ case 'clear':
196
+ result = await clearStr(cdp, sessionId, args[0]);
197
+ break;
198
+ case 'select':
199
+ result = await selectStr(cdp, sessionId, args[0], args.slice(1).join(' '));
200
+ break;
201
+ case 'check':
202
+ result = await checkStr(cdp, sessionId, args[0], args[1] !== 'false');
203
+ break;
204
+ case 'form':
205
+ result = await formStr(cdp, sessionId, args[0]);
206
+ break;
207
+ case 'scroll':
208
+ result = await scrollStr(cdp, sessionId, args[0], args[1]);
209
+ break;
210
+ case 'cookies':
211
+ result = await cookiesStr(cdp, sessionId, args[0], args.slice(1).join(' ') || undefined);
212
+ break;
213
+ case 'pdf':
214
+ result = await pdfStr(cdp, sessionId, args[0]);
215
+ break;
216
+ case 'console':
217
+ result = await consoleStr(cdp, sessionId, args[0]);
218
+ break;
219
+ case 'storage':
220
+ result = await storageStr(cdp, sessionId, args[0]);
221
+ break;
222
+ case 'emulate':
223
+ result = await emulateStr(cdp, sessionId, args[0]);
224
+ break;
225
+ case 'perf':
226
+ result = await perfStr(cdp, sessionId);
227
+ break;
228
+ // --- Tier 1: Quick Wins ---
229
+ case 'wait':
230
+ result = await waitLifecycleStr(cdp, sessionId, args[0], args[1], config);
231
+ break;
232
+ case 'open':
233
+ result = await openTabStr(cdp, args[0]);
234
+ break;
235
+ case 'close':
236
+ result = await closeTabStr(cdp, args[0]);
237
+ break;
238
+ case 'focus':
239
+ result = await focusTabStr(cdp, args[0]);
240
+ break;
241
+ case 'dialog': {
242
+ const dialogResult = await dialogStr(cdp, sessionId, args[0], args.slice(1).join(' ') || undefined);
243
+ if (dialogResult === '__AUTO_DIALOG__') {
244
+ result = setupAutoDialog(cdp, sessionId);
245
+ } else {
246
+ result = dialogResult;
247
+ }
248
+ break;
249
+ }
250
+ case 'upload':
251
+ result = await uploadStr(cdp, sessionId, args[0], ...args.slice(1));
252
+ break;
253
+ case 'geo':
254
+ result = await geoStr(cdp, sessionId, args[0], args[1], args[2]);
255
+ break;
256
+ case 'timezone':
257
+ result = await timezoneStr(cdp, sessionId, args[0]);
258
+ break;
259
+ case 'locale':
260
+ result = await localeStr(cdp, sessionId, args[0]);
261
+ break;
262
+ case 'throttle':
263
+ result = await throttleStr(cdp, sessionId, args[0], ...args.slice(1));
264
+ break;
265
+ case 'cpu':
266
+ result = await cpuStr(cdp, sessionId, args[0]);
267
+ break;
268
+ case 'inject':
269
+ result = await injectStr(cdp, sessionId, args[0], args.slice(1).join(' ') || undefined);
270
+ break;
271
+ case 'download':
272
+ result = await downloadStr(cdp, sessionId, args[0], args[1]);
273
+ break;
274
+ // --- Tier 2: Game Changers ---
275
+ case 'intercept':
276
+ result = await interceptStr(cdp, sessionId, args[0], args[1], args.slice(2).join(' ') || undefined);
277
+ break;
278
+ case 'har':
279
+ result = await harStr(cdp, sessionId, args[0], args[1]);
280
+ break;
281
+ case 'coverage':
282
+ result = await coverageStr(cdp, sessionId, args[0]);
283
+ break;
284
+ // --- Tier 3: Pro Features ---
285
+ case 'trace':
286
+ result = await traceStr(cdp, sessionId, args[0], args[1]);
287
+ break;
288
+ case 'heap':
289
+ result = await heapStr(cdp, sessionId, args[0], args[1]);
290
+ break;
291
+ case 'webauthn':
292
+ result = await webauthnStr(cdp, sessionId, args[0]);
293
+ break;
294
+ case 'drag':
295
+ result = await dragStr(cdp, sessionId, args[0], args[1]);
296
+ break;
297
+ case 'touch':
298
+ result = await touchStr(cdp, sessionId, args[0], ...args.slice(1));
299
+ break;
300
+ case 'domsnapshot':
301
+ result = await domsnapshotStr(cdp, sessionId, args.includes('--styles'));
302
+ break;
303
+ case 'highlight':
304
+ result = await highlightStr(cdp, sessionId, args[0]);
305
+ break;
306
+ case 'hover':
307
+ throw new Error('hover requires a ref (@eN). Run "snap --refs" first, then "hover @e5".');
308
+ case 'stop': {
309
+ audit(cmd, targetId, args, auditResult, config);
310
+ return { ok: true, result: '', stopAfter: true };
311
+ }
312
+ default: {
313
+ auditResult.ok = false;
314
+ audit(cmd, targetId, args, auditResult, config);
315
+ return { ok: false, error: `Unknown command: ${cmd}` };
316
+ }
317
+ }
318
+ audit(cmd, targetId, args, auditResult, config);
319
+ return { ok: true, result: result ?? '' };
320
+ } catch (e) {
321
+ auditResult.ok = false;
322
+ audit(cmd, targetId, args, auditResult, config);
323
+ return { ok: false, error: e.message };
324
+ }
325
+ }
326
+
327
+ // Unix socket server com autenticação
328
+ const server = net.createServer((conn) => {
329
+ let buf = '';
330
+ let authenticated = !config.socketAuth;
331
+
332
+ conn.on('data', (chunk) => {
333
+ buf += chunk.toString();
334
+ const lines = buf.split('\n');
335
+ buf = lines.pop();
336
+ for (const line of lines) {
337
+ if (!line.trim()) continue;
338
+ let req;
339
+ try {
340
+ req = JSON.parse(line);
341
+ } catch {
342
+ conn.write(JSON.stringify({ ok: false, error: 'Invalid JSON request', id: null }) + '\n');
343
+ continue;
344
+ }
345
+
346
+ if (!authenticated) {
347
+ if (req.auth === authToken) {
348
+ authenticated = true;
349
+ conn.write(JSON.stringify({ ok: true, id: req.id || 0 }) + '\n');
350
+ } else {
351
+ conn.write(JSON.stringify({ ok: false, error: 'Authentication failed', id: req.id || 0 }) + '\n');
352
+ conn.end();
353
+ }
354
+ continue;
355
+ }
356
+
357
+ handleCommand(req).then((res) => {
358
+ const payload = JSON.stringify({ ...res, id: req.id }) + '\n';
359
+ if (res.stopAfter) conn.end(payload, shutdown);
360
+ else conn.write(payload);
361
+ });
362
+ }
363
+ });
364
+ });
365
+
366
+ try { unlinkSync(sp); } catch { /* socket não existe */ }
367
+ server.listen(sp);
368
+ }
@@ -0,0 +1,178 @@
1
+ // Comunicação CLI <-> daemon via Unix sockets
2
+ import { unlinkSync, readFileSync, existsSync } from 'fs';
3
+ import { checkDomain } from './security.mjs';
4
+ import { spawn } from 'child_process';
5
+ import net from 'net';
6
+ import { sleep, resolvePrefix, listDaemonSockets, sockPath } from './utils.mjs';
7
+ import { getOrCreateToken } from './daemon.mjs';
8
+
9
+ const DAEMON_CONNECT_RETRIES = 20;
10
+ const DAEMON_CONNECT_DELAY = 300;
11
+
12
+ function connectToSocket(sp) {
13
+ return new Promise((resolve, reject) => {
14
+ const conn = net.connect(sp);
15
+ conn.on('connect', () => resolve(conn));
16
+ conn.on('error', reject);
17
+ });
18
+ }
19
+
20
+ async function authenticateConnection(conn, authToken) {
21
+ if (!authToken) return true;
22
+
23
+ return new Promise((resolve, reject) => {
24
+ let buf = '';
25
+ const onData = (chunk) => {
26
+ buf += chunk.toString();
27
+ const idx = buf.indexOf('\n');
28
+ if (idx === -1) return;
29
+ conn.off('data', onData);
30
+ conn.off('error', onError);
31
+ try {
32
+ const resp = JSON.parse(buf.slice(0, idx));
33
+ resolve(resp.ok === true);
34
+ } catch {
35
+ resolve(false);
36
+ }
37
+ };
38
+ const onError = (e) => {
39
+ conn.off('data', onData);
40
+ reject(e);
41
+ };
42
+ conn.on('data', onData);
43
+ conn.on('error', onError);
44
+ conn.write(JSON.stringify({ auth: authToken, id: 0 }) + '\n');
45
+ });
46
+ }
47
+
48
+ async function connectAndAuth(sp, authToken) {
49
+ const conn = await connectToSocket(sp);
50
+ const ok = await authenticateConnection(conn, authToken);
51
+ if (!ok) {
52
+ conn.end();
53
+ throw new Error('Socket authentication failed');
54
+ }
55
+ return conn;
56
+ }
57
+
58
+ export async function getOrStartTabDaemon(targetId, config) {
59
+ const sp = sockPath(config._socketDir, targetId);
60
+ const authToken = getOrCreateToken(config);
61
+
62
+ // Tentar daemon existente
63
+ try { return await connectAndAuth(sp, authToken); } catch { /* daemon não existe */ }
64
+
65
+ // Limpar socket stale
66
+ try { unlinkSync(sp); } catch { /* não existe */ }
67
+
68
+ // Spawnar daemon -- usa o mesmo script com _daemon como primeiro arg
69
+ const scriptPath = new URL('../chromex.mjs', import.meta.url).pathname;
70
+ const child = spawn(process.execPath, [scriptPath, '_daemon', targetId], {
71
+ detached: true,
72
+ stdio: 'ignore',
73
+ });
74
+ child.unref();
75
+
76
+ // Aguardar socket (inclui tempo do usuário clicar Allow)
77
+ for (let i = 0; i < DAEMON_CONNECT_RETRIES; i++) {
78
+ await sleep(DAEMON_CONNECT_DELAY);
79
+ try { return await connectAndAuth(sp, authToken); } catch { /* aguardando */ }
80
+ }
81
+ throw new Error('Daemon failed to start — did you click Allow in Chrome?');
82
+ }
83
+
84
+ export function sendCommand(conn, req) {
85
+ return new Promise((resolve, reject) => {
86
+ let buf = '';
87
+ let settled = false;
88
+
89
+ const cleanup = () => {
90
+ conn.off('data', onData);
91
+ conn.off('error', onError);
92
+ conn.off('end', onEnd);
93
+ conn.off('close', onClose);
94
+ };
95
+
96
+ const onData = (chunk) => {
97
+ buf += chunk.toString();
98
+ const idx = buf.indexOf('\n');
99
+ if (idx === -1) return;
100
+ settled = true;
101
+ cleanup();
102
+ resolve(JSON.parse(buf.slice(0, idx)));
103
+ conn.end();
104
+ };
105
+
106
+ const onError = (error) => {
107
+ if (settled) return;
108
+ settled = true;
109
+ cleanup();
110
+ reject(error);
111
+ };
112
+
113
+ const onEnd = () => {
114
+ if (settled) return;
115
+ settled = true;
116
+ cleanup();
117
+ reject(new Error('Connection closed before response'));
118
+ };
119
+
120
+ const onClose = () => {
121
+ if (settled) return;
122
+ settled = true;
123
+ cleanup();
124
+ reject(new Error('Connection closed before response'));
125
+ };
126
+
127
+ conn.on('data', onData);
128
+ conn.on('error', onError);
129
+ conn.on('end', onEnd);
130
+ conn.on('close', onClose);
131
+ req.id = 1;
132
+ conn.write(JSON.stringify(req) + '\n');
133
+ });
134
+ }
135
+
136
+ export async function stopDaemons(targetPrefix, config) {
137
+ const authToken = getOrCreateToken(config);
138
+ const daemons = listDaemonSockets(config._socketDir);
139
+
140
+ if (targetPrefix) {
141
+ const targetId = resolvePrefix(targetPrefix, daemons.map(d => d.targetId), 'daemon');
142
+ const daemon = daemons.find(d => d.targetId === targetId);
143
+ try {
144
+ const conn = await connectAndAuth(daemon.socketPath, authToken);
145
+ await sendCommand(conn, { cmd: 'stop' });
146
+ } catch {
147
+ try { unlinkSync(daemon.socketPath); } catch { /* já removido */ }
148
+ }
149
+ return;
150
+ }
151
+
152
+ for (const daemon of daemons) {
153
+ try {
154
+ const conn = await connectAndAuth(daemon.socketPath, authToken);
155
+ await sendCommand(conn, { cmd: 'stop' });
156
+ } catch {
157
+ try { unlinkSync(daemon.socketPath); } catch { /* já removido */ }
158
+ }
159
+ }
160
+ }
161
+
162
+ export function findAnyDaemonSocket(config) {
163
+ return listDaemonSockets(config._socketDir)[0]?.socketPath || null;
164
+ }
165
+
166
+ export function checkTargetDomain(targetId, config) {
167
+ if (!existsSync(config._pagesCachePath)) return;
168
+ try {
169
+ const pages = JSON.parse(readFileSync(config._pagesCachePath, 'utf8'));
170
+ const page = pages.find(p => p.targetId === targetId);
171
+ if (page) {
172
+ const error = checkDomain(page.url, config);
173
+ if (error) throw new Error(`${error}\nTab: ${page.title} (${page.url})`);
174
+ }
175
+ } catch (e) {
176
+ if (e.message.includes('blocked') || e.message.includes('allowedDomains')) throw e;
177
+ }
178
+ }