halo-agent 1.2.0 → 1.3.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.
- package/browser.js +43 -13
- package/index.js +63 -40
- package/package.json +1 -1
package/browser.js
CHANGED
|
@@ -3,10 +3,19 @@
|
|
|
3
3
|
const { chromium } = require('playwright');
|
|
4
4
|
const { execSync, spawnSync, spawn } = require('child_process');
|
|
5
5
|
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
6
8
|
|
|
7
9
|
const CDP_PORT = 9222;
|
|
8
10
|
const CDP_URL = `http://localhost:${CDP_PORT}`;
|
|
9
11
|
|
|
12
|
+
// Dedicated user-data-dir for the agent's Chrome. Fully isolated from the
|
|
13
|
+
// user's real Chrome so it can run side-by-side without profile-lock
|
|
14
|
+
// collisions (which silently disable --remote-debugging-port). The
|
|
15
|
+
// user logs into Workday / LinkedIn here ONCE on first run and the
|
|
16
|
+
// session persists across runs because this dir is on disk, not /tmp.
|
|
17
|
+
const AGENT_PROFILE_DIR = path.join(os.homedir(), '.halo-agent', 'chrome-profile');
|
|
18
|
+
|
|
10
19
|
/**
|
|
11
20
|
* Check if Chrome is already running with remote debugging on CDP_PORT.
|
|
12
21
|
*/
|
|
@@ -42,12 +51,22 @@ function findChromeMac() {
|
|
|
42
51
|
*/
|
|
43
52
|
function launchChrome() {
|
|
44
53
|
const platform = process.platform;
|
|
45
|
-
|
|
54
|
+
|
|
55
|
+
// Ensure the dedicated profile dir exists. Chrome will populate it on
|
|
56
|
+
// first launch (and persist cookies/logins there across runs).
|
|
57
|
+
try { fs.mkdirSync(AGENT_PROFILE_DIR, { recursive: true }); } catch {}
|
|
58
|
+
|
|
59
|
+
// Note: we use --user-data-dir (a full isolated dir), NOT --profile-directory
|
|
60
|
+
// (which is a sub-profile name inside the default user-data-dir and would
|
|
61
|
+
// still collide with the user's running Chrome and silently drop the
|
|
62
|
+
// --remote-debugging-port flag).
|
|
63
|
+
const baseFlags = [
|
|
46
64
|
`--remote-debugging-port=${CDP_PORT}`,
|
|
47
|
-
|
|
65
|
+
`--user-data-dir=${AGENT_PROFILE_DIR}`,
|
|
48
66
|
'--no-first-run',
|
|
49
67
|
'--no-default-browser-check',
|
|
50
|
-
|
|
68
|
+
'--no-default-browser-check',
|
|
69
|
+
];
|
|
51
70
|
|
|
52
71
|
if (platform === 'darwin') {
|
|
53
72
|
const chromePath = findChromeMac();
|
|
@@ -55,9 +74,11 @@ function launchChrome() {
|
|
|
55
74
|
console.error('[halo-agent] Chrome not found. Install Google Chrome from https://www.google.com/chrome/');
|
|
56
75
|
return;
|
|
57
76
|
}
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
// `open -na` forces a fresh app instance (LaunchServices would otherwise
|
|
78
|
+
// reactivate the existing Chrome and drop our flags). Because we point
|
|
79
|
+
// at a dedicated --user-data-dir, this instance does NOT collide with
|
|
80
|
+
// the user's everyday Chrome — they can run side-by-side cleanly.
|
|
81
|
+
spawn('open', ['-na', chromePath, '--args', ...baseFlags], {
|
|
61
82
|
detached: true,
|
|
62
83
|
stdio: 'ignore',
|
|
63
84
|
}).unref();
|
|
@@ -69,10 +90,12 @@ function launchChrome() {
|
|
|
69
90
|
];
|
|
70
91
|
const chromePath = chromePaths.find(p => { try { fs.accessSync(p); return true; } catch { return false; } });
|
|
71
92
|
if (!chromePath) { console.error('[halo-agent] Chrome not found on Windows.'); return; }
|
|
72
|
-
|
|
93
|
+
const winFlags = baseFlags.map(f => f.includes(' ') ? `"${f}"` : f).join(' ');
|
|
94
|
+
spawn(`"${chromePath}" ${winFlags}`, [], { shell: true, detached: true, stdio: 'ignore' }).unref();
|
|
73
95
|
} else {
|
|
74
96
|
// Linux
|
|
75
|
-
|
|
97
|
+
const linuxFlags = baseFlags.map(f => f.includes(' ') ? `"${f}"` : f).join(' ');
|
|
98
|
+
spawn(`google-chrome ${linuxFlags}`, [], { shell: true, detached: true, stdio: 'ignore' }).unref();
|
|
76
99
|
}
|
|
77
100
|
}
|
|
78
101
|
|
|
@@ -81,7 +104,12 @@ function launchChrome() {
|
|
|
81
104
|
* Returns { browser, context, newPage }.
|
|
82
105
|
* All requests use the user's real session — cookies, IP, fingerprint, extensions.
|
|
83
106
|
*/
|
|
84
|
-
async function connectToChrome(retries = 10) {
|
|
107
|
+
async function connectToChrome(retries = 10, opts = {}) {
|
|
108
|
+
// skipLaunch: caller has ALREADY launched Chrome (e.g. via the index.js
|
|
109
|
+
// restart-with-debug-flag flow) and we should JUST wait for CDP to come
|
|
110
|
+
// up. Without this, every retry path also tries to launch Chrome again,
|
|
111
|
+
// which on macOS spawns extra windows via the LaunchServices race.
|
|
112
|
+
const skipLaunch = !!opts.skipLaunch;
|
|
85
113
|
for (let i = 0; i < retries; i++) {
|
|
86
114
|
const debuggable = await isChromeDebuggable();
|
|
87
115
|
if (debuggable) {
|
|
@@ -135,7 +163,7 @@ async function connectToChrome(retries = 10) {
|
|
|
135
163
|
}
|
|
136
164
|
}
|
|
137
165
|
|
|
138
|
-
if (i === 0) {
|
|
166
|
+
if (i === 0 && !skipLaunch) {
|
|
139
167
|
console.log('[halo-agent] Chrome not found on port 9222. Launching...');
|
|
140
168
|
launchChrome();
|
|
141
169
|
console.log('[halo-agent] Waiting for Chrome to start...');
|
|
@@ -148,9 +176,11 @@ async function connectToChrome(retries = 10) {
|
|
|
148
176
|
|
|
149
177
|
console.log(''); // newline after dots
|
|
150
178
|
throw new Error(
|
|
151
|
-
`Could not connect to Chrome after ${retries} attempts.\n` +
|
|
152
|
-
`
|
|
153
|
-
`
|
|
179
|
+
`Could not connect to the agent's Chrome on port ${CDP_PORT} after ${retries} attempts.\n` +
|
|
180
|
+
`The agent uses an isolated Chrome profile at ${AGENT_PROFILE_DIR}.\n` +
|
|
181
|
+
`If a previous run is still alive, kill it:\n\n` +
|
|
182
|
+
` pkill -f "user-data-dir=${AGENT_PROFILE_DIR}"\n\n` +
|
|
183
|
+
`Then run \`halo-agent start\` again.\n`
|
|
154
184
|
);
|
|
155
185
|
}
|
|
156
186
|
|
package/index.js
CHANGED
|
@@ -346,24 +346,56 @@ async function restartChromeWithDebugFlag() {
|
|
|
346
346
|
const { launchChrome, isChromeDebuggable } = require('./browser');
|
|
347
347
|
|
|
348
348
|
console.log('Restarting Chrome…');
|
|
349
|
-
if (process.platform === 'darwin') {
|
|
350
|
-
// AppleScript graceful quit — respects Chrome's restore-tabs behavior.
|
|
351
|
-
try {
|
|
352
|
-
spawnSync('osascript', ['-e', 'tell application "Google Chrome" to quit'], { stdio: 'ignore' });
|
|
353
|
-
} catch { /* fall through to pkill if osascript missing */ }
|
|
354
|
-
} else if (process.platform === 'win32') {
|
|
355
|
-
// Windows: send WM_CLOSE via taskkill (no /F so it's graceful).
|
|
356
|
-
try { spawnSync('taskkill', ['/IM', 'chrome.exe'], { stdio: 'ignore' }); } catch {}
|
|
357
|
-
} else {
|
|
358
|
-
try { spawnSync('pkill', ['-x', 'chrome'], { stdio: 'ignore' }); } catch {}
|
|
359
|
-
}
|
|
360
349
|
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
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
|
+
}
|
|
364
391
|
if (!gone) {
|
|
365
|
-
|
|
366
|
-
console.
|
|
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.');
|
|
367
399
|
process.exit(1);
|
|
368
400
|
}
|
|
369
401
|
|
|
@@ -372,15 +404,19 @@ async function restartChromeWithDebugFlag() {
|
|
|
372
404
|
await new Promise(r => setTimeout(r, 800)); // brief breath
|
|
373
405
|
launchChrome();
|
|
374
406
|
|
|
375
|
-
// Wait for the new Chrome to expose CDP. Up to ~
|
|
376
|
-
|
|
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++) {
|
|
377
412
|
if (await isChromeDebuggable()) {
|
|
378
|
-
console.log('
|
|
413
|
+
console.log('\nChrome is back, debugging enabled.');
|
|
379
414
|
return;
|
|
380
415
|
}
|
|
416
|
+
process.stdout.write('.');
|
|
381
417
|
await new Promise(r => setTimeout(r, 500));
|
|
382
418
|
}
|
|
383
|
-
console.error('
|
|
419
|
+
console.error('\nChrome relaunched but CDP didn’t come up in time.');
|
|
384
420
|
console.error('Try running `halo-agent start` again — Chrome may have just been slow.');
|
|
385
421
|
process.exit(1);
|
|
386
422
|
}
|
|
@@ -417,29 +453,16 @@ async function runStart() {
|
|
|
417
453
|
console.log('\nHALO Agent starting...');
|
|
418
454
|
console.log('Connecting to your Chrome browser...\n');
|
|
419
455
|
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
const { isChromeDebuggable } = require('./browser');
|
|
426
|
-
const alreadyDebuggable = await isChromeDebuggable();
|
|
427
|
-
if (!alreadyDebuggable) {
|
|
428
|
-
const needsRestart = await detectChromeRunningWithoutDebug();
|
|
429
|
-
if (needsRestart) {
|
|
430
|
-
const ok = await offerChromeRestart();
|
|
431
|
-
if (!ok) {
|
|
432
|
-
console.error('\nOK — leaving Chrome alone. Run `halo-agent start` again after you’ve quit Chrome.');
|
|
433
|
-
process.exit(1);
|
|
434
|
-
}
|
|
435
|
-
await restartChromeWithDebugFlag();
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
456
|
+
// The agent runs its own isolated Chrome instance (separate --user-data-dir
|
|
457
|
+
// at ~/.halo-agent/chrome-profile) so it never collides with the user's
|
|
458
|
+
// everyday Chrome. First launch will be a blank profile — the user logs
|
|
459
|
+
// into Workday / LinkedIn ONCE in the agent's Chrome and those sessions
|
|
460
|
+
// persist across runs. No need to detect or restart the user's Chrome.
|
|
439
461
|
let chromeConn;
|
|
440
462
|
try {
|
|
441
|
-
chromeConn = await connectToChrome();
|
|
463
|
+
chromeConn = await connectToChrome(10);
|
|
442
464
|
console.log('\nConnected to Chrome. Polling for queued jobs...');
|
|
465
|
+
console.log('First time? Log into Workday/LinkedIn in this Chrome window — sessions persist.');
|
|
443
466
|
console.log('Go to your HALO dashboard and click "Auto-Apply" on any job.\n');
|
|
444
467
|
} catch (err) {
|
|
445
468
|
console.error('\nCould not connect to Chrome:', err.message);
|