halo-agent 1.1.0 → 1.2.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.
- package/browser.js +27 -7
- package/index.js +171 -25
- package/package.json +1 -1
package/browser.js
CHANGED
|
@@ -55,9 +55,22 @@ function launchChrome() {
|
|
|
55
55
|
console.error('[halo-agent] Chrome not found. Install Google Chrome from https://www.google.com/chrome/');
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
// CRITICAL: on macOS, spawning the Chrome executable directly while
|
|
59
|
+
// LaunchServices still has a Chrome instance registered (recent quit,
|
|
60
|
+
// dock icon still showing, etc.) causes the new launch to REACTIVATE
|
|
61
|
+
// the existing instance and silently DROP all --flags. Symptom: Chrome
|
|
62
|
+
// opens but without --remote-debugging-port, so the agent can never
|
|
63
|
+
// see it. `open -na` forces a fresh app instance and `--args` passes
|
|
64
|
+
// flags through reliably — this is the only correct way to launch
|
|
65
|
+
// Chrome with custom flags on macOS.
|
|
66
|
+
const splitFlags = [
|
|
67
|
+
`--remote-debugging-port=${CDP_PORT}`,
|
|
68
|
+
'--profile-directory=Default',
|
|
69
|
+
'--no-first-run',
|
|
70
|
+
'--no-default-browser-check',
|
|
71
|
+
'--restore-last-session', // bring back the windows the user just had
|
|
72
|
+
];
|
|
73
|
+
spawn('open', ['-na', chromePath, '--args', ...splitFlags], {
|
|
61
74
|
detached: true,
|
|
62
75
|
stdio: 'ignore',
|
|
63
76
|
}).unref();
|
|
@@ -81,7 +94,12 @@ function launchChrome() {
|
|
|
81
94
|
* Returns { browser, context, newPage }.
|
|
82
95
|
* All requests use the user's real session — cookies, IP, fingerprint, extensions.
|
|
83
96
|
*/
|
|
84
|
-
async function connectToChrome(retries = 10) {
|
|
97
|
+
async function connectToChrome(retries = 10, opts = {}) {
|
|
98
|
+
// skipLaunch: caller has ALREADY launched Chrome (e.g. via the index.js
|
|
99
|
+
// restart-with-debug-flag flow) and we should JUST wait for CDP to come
|
|
100
|
+
// up. Without this, every retry path also tries to launch Chrome again,
|
|
101
|
+
// which on macOS spawns extra windows via the LaunchServices race.
|
|
102
|
+
const skipLaunch = !!opts.skipLaunch;
|
|
85
103
|
for (let i = 0; i < retries; i++) {
|
|
86
104
|
const debuggable = await isChromeDebuggable();
|
|
87
105
|
if (debuggable) {
|
|
@@ -135,7 +153,7 @@ async function connectToChrome(retries = 10) {
|
|
|
135
153
|
}
|
|
136
154
|
}
|
|
137
155
|
|
|
138
|
-
if (i === 0) {
|
|
156
|
+
if (i === 0 && !skipLaunch) {
|
|
139
157
|
console.log('[halo-agent] Chrome not found on port 9222. Launching...');
|
|
140
158
|
launchChrome();
|
|
141
159
|
console.log('[halo-agent] Waiting for Chrome to start...');
|
|
@@ -149,8 +167,10 @@ async function connectToChrome(retries = 10) {
|
|
|
149
167
|
console.log(''); // newline after dots
|
|
150
168
|
throw new Error(
|
|
151
169
|
`Could not connect to Chrome after ${retries} attempts.\n` +
|
|
152
|
-
`
|
|
153
|
-
`
|
|
170
|
+
`Chrome may have launched without the --remote-debugging-port=9222 flag\n` +
|
|
171
|
+
`(macOS reactivates an existing Chrome instance and drops flags).\n` +
|
|
172
|
+
`Try: fully quit Chrome (Cmd+Q in every window + check the dock), then run:\n\n` +
|
|
173
|
+
` halo-agent start\n`
|
|
154
174
|
);
|
|
155
175
|
}
|
|
156
176
|
|
package/index.js
CHANGED
|
@@ -291,6 +291,158 @@ async function runInit() {
|
|
|
291
291
|
console.log('Run "halo-agent start" to begin.\n');
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// Is Chrome running without the --remote-debugging-port=9222 flag?
|
|
295
|
+
// Returns true ONLY when we're certain a restart is needed.
|
|
296
|
+
async function detectChromeRunningWithoutDebug() {
|
|
297
|
+
const { execSync } = require('child_process');
|
|
298
|
+
try {
|
|
299
|
+
if (process.platform === 'darwin') {
|
|
300
|
+
execSync('pgrep -x "Google Chrome"', { stdio: 'ignore' });
|
|
301
|
+
const psOut = execSync('ps aux', { encoding: 'utf8' });
|
|
302
|
+
return !psOut.includes('--remote-debugging-port=9222');
|
|
303
|
+
}
|
|
304
|
+
if (process.platform === 'win32') {
|
|
305
|
+
const out = execSync('tasklist /FI "IMAGENAME eq chrome.exe"', { encoding: 'utf8' });
|
|
306
|
+
if (!/chrome\.exe/i.test(out)) return false;
|
|
307
|
+
// Windows tasklist doesn't show full cmdline; use wmic as a fallback.
|
|
308
|
+
try {
|
|
309
|
+
const wmic = execSync('wmic process where "name=\'chrome.exe\'" get CommandLine', { encoding: 'utf8' });
|
|
310
|
+
return !wmic.includes('--remote-debugging-port=9222');
|
|
311
|
+
} catch { return true; } // can't introspect — safer to offer restart
|
|
312
|
+
}
|
|
313
|
+
// Linux
|
|
314
|
+
try { execSync('pgrep -x chrome', { stdio: 'ignore' }); } catch { return false; }
|
|
315
|
+
try {
|
|
316
|
+
const ps = execSync('ps -eo args', { encoding: 'utf8' });
|
|
317
|
+
return !ps.includes('--remote-debugging-port=9222');
|
|
318
|
+
} catch { return true; }
|
|
319
|
+
} catch {
|
|
320
|
+
return false; // pgrep failed → Chrome not running
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Terminal Y/n prompt for the consent before we touch Chrome.
|
|
325
|
+
function offerChromeRestart() {
|
|
326
|
+
return new Promise((resolve) => {
|
|
327
|
+
console.log('');
|
|
328
|
+
console.log('Chrome is running, but without the debug flag the agent needs.');
|
|
329
|
+
console.log('I can restart Chrome for you — your tabs will reopen automatically');
|
|
330
|
+
console.log('(if your "On startup" setting is "Continue where you left off").');
|
|
331
|
+
console.log('');
|
|
332
|
+
const readline = require('readline');
|
|
333
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
334
|
+
rl.question('Restart Chrome now? [Y/n] ', (ans) => {
|
|
335
|
+
rl.close();
|
|
336
|
+
const a = (ans || '').trim().toLowerCase();
|
|
337
|
+
resolve(a === '' || a === 'y' || a === 'yes');
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Graceful quit (preserves tabs), wait until the process is actually gone,
|
|
343
|
+
// then relaunch with the debug flag. Cross-platform.
|
|
344
|
+
async function restartChromeWithDebugFlag() {
|
|
345
|
+
const { execSync, spawnSync } = require('child_process');
|
|
346
|
+
const { launchChrome, isChromeDebuggable } = require('./browser');
|
|
347
|
+
|
|
348
|
+
console.log('Restarting Chrome…');
|
|
349
|
+
|
|
350
|
+
// Escalation ladder: try the friendliest quit first, escalate if needed.
|
|
351
|
+
// On macOS the "warn before quitting" dialog can intercept AppleScript
|
|
352
|
+
// quit, so we follow up with SIGTERM (still graceful — Chrome handles it
|
|
353
|
+
// and saves session state). SIGKILL is last resort and very rarely needed.
|
|
354
|
+
const tryGraceful = () => {
|
|
355
|
+
if (process.platform === 'darwin') {
|
|
356
|
+
try { spawnSync('osascript', ['-e', 'tell application "Google Chrome" to quit'], { stdio: 'ignore' }); } catch {}
|
|
357
|
+
} else if (process.platform === 'win32') {
|
|
358
|
+
try { spawnSync('taskkill', ['/IM', 'chrome.exe'], { stdio: 'ignore' }); } catch {}
|
|
359
|
+
} else {
|
|
360
|
+
try { spawnSync('pkill', ['-x', 'chrome'], { stdio: 'ignore' }); } catch {}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const trySigterm = () => {
|
|
364
|
+
if (process.platform === 'darwin') {
|
|
365
|
+
try { spawnSync('pkill', ['-TERM', '-x', 'Google Chrome'], { stdio: 'ignore' }); } catch {}
|
|
366
|
+
} else if (process.platform === 'win32') {
|
|
367
|
+
// Already taskkill above; no escalation step worth running before /F
|
|
368
|
+
} else {
|
|
369
|
+
try { spawnSync('pkill', ['-TERM', '-x', 'chrome'], { stdio: 'ignore' }); } catch {}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
const tryForce = () => {
|
|
373
|
+
if (process.platform === 'darwin') {
|
|
374
|
+
try { spawnSync('pkill', ['-9', '-x', 'Google Chrome'], { stdio: 'ignore' }); } catch {}
|
|
375
|
+
} else if (process.platform === 'win32') {
|
|
376
|
+
try { spawnSync('taskkill', ['/F', '/IM', 'chrome.exe'], { stdio: 'ignore' }); } catch {}
|
|
377
|
+
} else {
|
|
378
|
+
try { spawnSync('pkill', ['-9', '-x', 'chrome'], { stdio: 'ignore' }); } catch {}
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
tryGraceful();
|
|
383
|
+
let gone = await waitForChromeGone(4_000);
|
|
384
|
+
if (!gone) {
|
|
385
|
+
// AppleScript blocked (likely by "warn before quitting" dialog). SIGTERM
|
|
386
|
+
// bypasses the dialog but still lets Chrome shut down cleanly.
|
|
387
|
+
console.log('Sending SIGTERM (Chrome had a quit prompt) …');
|
|
388
|
+
trySigterm();
|
|
389
|
+
gone = await waitForChromeGone(4_000);
|
|
390
|
+
}
|
|
391
|
+
if (!gone) {
|
|
392
|
+
// Last resort.
|
|
393
|
+
console.log('Force-quitting Chrome …');
|
|
394
|
+
tryForce();
|
|
395
|
+
gone = await waitForChromeGone(3_000);
|
|
396
|
+
}
|
|
397
|
+
if (!gone) {
|
|
398
|
+
console.error('Could not stop Chrome. Quit it manually (Cmd+Q in every window) and run `halo-agent start` again.');
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Relaunch with the debug flag + Default profile. Uses the existing
|
|
403
|
+
// browser.js logic so the path-finding stays in one place.
|
|
404
|
+
await new Promise(r => setTimeout(r, 800)); // brief breath
|
|
405
|
+
launchChrome();
|
|
406
|
+
|
|
407
|
+
// Wait for the new Chrome to expose CDP. Up to ~25s — with
|
|
408
|
+
// --restore-last-session Chrome has to re-open every tab from before,
|
|
409
|
+
// which on a heavy session can take 10-15s before the CDP server binds.
|
|
410
|
+
process.stdout.write('Waiting for Chrome to expose CDP');
|
|
411
|
+
for (let i = 0; i < 50; i++) {
|
|
412
|
+
if (await isChromeDebuggable()) {
|
|
413
|
+
console.log('\nChrome is back, debugging enabled.');
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
process.stdout.write('.');
|
|
417
|
+
await new Promise(r => setTimeout(r, 500));
|
|
418
|
+
}
|
|
419
|
+
console.error('\nChrome relaunched but CDP didn’t come up in time.');
|
|
420
|
+
console.error('Try running `halo-agent start` again — Chrome may have just been slow.');
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function waitForChromeGone(timeoutMs) {
|
|
425
|
+
const { execSync } = require('child_process');
|
|
426
|
+
const probe = () => {
|
|
427
|
+
try {
|
|
428
|
+
if (process.platform === 'darwin') execSync('pgrep -x "Google Chrome"', { stdio: 'ignore' });
|
|
429
|
+
else if (process.platform === 'win32') {
|
|
430
|
+
const out = execSync('tasklist /FI "IMAGENAME eq chrome.exe"', { encoding: 'utf8' });
|
|
431
|
+
if (!/chrome\.exe/i.test(out)) return false;
|
|
432
|
+
} else execSync('pgrep -x chrome', { stdio: 'ignore' });
|
|
433
|
+
return true; // pgrep succeeded → still running
|
|
434
|
+
} catch {
|
|
435
|
+
return false; // pgrep failed → process gone
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const start = Date.now();
|
|
439
|
+
while (Date.now() - start < timeoutMs) {
|
|
440
|
+
if (!probe()) return true;
|
|
441
|
+
await new Promise(r => setTimeout(r, 300));
|
|
442
|
+
}
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
|
|
294
446
|
async function runStart() {
|
|
295
447
|
const config = loadConfig();
|
|
296
448
|
if (!config || !config.token) {
|
|
@@ -301,39 +453,33 @@ async function runStart() {
|
|
|
301
453
|
console.log('\nHALO Agent starting...');
|
|
302
454
|
console.log('Connecting to your Chrome browser...\n');
|
|
303
455
|
|
|
304
|
-
// Pre-check:
|
|
456
|
+
// Pre-check: Chrome must be running with --remote-debugging-port=9222 for
|
|
457
|
+
// CDP to work. If it's running WITHOUT that flag, Chrome can't have it
|
|
458
|
+
// added retroactively — it's a Chrome architecture limit. We offer to
|
|
459
|
+
// restart Chrome gracefully (preserves tabs via Chrome's "Continue where
|
|
460
|
+
// you left off" setting) and relaunch with the flag.
|
|
305
461
|
const { isChromeDebuggable } = require('./browser');
|
|
462
|
+
// weRestartedIt: tracks whether THIS process launched/restarted Chrome.
|
|
463
|
+
// When true, connectToChrome must NOT launch again — that's the double-
|
|
464
|
+
// launch bug that was spawning extra Chrome windows.
|
|
465
|
+
let weRestartedIt = false;
|
|
306
466
|
const alreadyDebuggable = await isChromeDebuggable();
|
|
307
467
|
if (!alreadyDebuggable) {
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const psOut = execSync('ps aux', { encoding: 'utf8' });
|
|
318
|
-
chromeHasDebugFlag = psOut.includes('--remote-debugging-port=9222');
|
|
319
|
-
} catch {}
|
|
320
|
-
}
|
|
321
|
-
if (chromeRunning && !chromeHasDebugFlag) {
|
|
322
|
-
console.error('Chrome is already running WITHOUT remote debugging enabled.\n');
|
|
323
|
-
console.error('You need to fully quit Chrome first, then relaunch it with the debug flag.\n');
|
|
324
|
-
console.error('Run these commands:\n');
|
|
325
|
-
console.error(' pkill -a -i "Google Chrome"');
|
|
326
|
-
console.error(' sleep 2');
|
|
327
|
-
console.error(' "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222 --profile-directory=Default &');
|
|
328
|
-
console.error(' sleep 4');
|
|
329
|
-
console.error(' node index.js start\n');
|
|
330
|
-
process.exit(1);
|
|
468
|
+
const needsRestart = await detectChromeRunningWithoutDebug();
|
|
469
|
+
if (needsRestart) {
|
|
470
|
+
const ok = await offerChromeRestart();
|
|
471
|
+
if (!ok) {
|
|
472
|
+
console.error('\nOK — leaving Chrome alone. Run `halo-agent start` again after you’ve quit Chrome.');
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
await restartChromeWithDebugFlag();
|
|
476
|
+
weRestartedIt = true;
|
|
331
477
|
}
|
|
332
478
|
}
|
|
333
479
|
|
|
334
480
|
let chromeConn;
|
|
335
481
|
try {
|
|
336
|
-
chromeConn = await connectToChrome();
|
|
482
|
+
chromeConn = await connectToChrome(10, { skipLaunch: weRestartedIt });
|
|
337
483
|
console.log('\nConnected to Chrome. Polling for queued jobs...');
|
|
338
484
|
console.log('Go to your HALO dashboard and click "Auto-Apply" on any job.\n');
|
|
339
485
|
} catch (err) {
|