halo-agent 1.2.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.
Files changed (3) hide show
  1. package/browser.js +27 -7
  2. package/index.js +62 -21
  3. 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
- // Use shell:true so the path with spaces is handled correctly by the shell
59
- spawn(`"${chromePath}" ${flags}`, [], {
60
- shell: true,
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
- `Run this manually in a separate terminal, then try again:\n\n` +
153
- ` "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222 --user-data-dir=/tmp/halo-chrome\n`
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
@@ -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
- // Poll until Chrome is gone (up to 10s). If it lingers, the user has a
362
- // "leave site?" prompt or modal we surface that rather than force-killing.
363
- const gone = await waitForChromeGone(10_000);
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
+ }
364
397
  if (!gone) {
365
- console.error('Chrome didn’t quit (you may have an unsaved-changes prompt).');
366
- console.error('Click through any prompts in Chrome, then run `halo-agent start` again.');
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 ~15s.
376
- for (let i = 0; i < 30; i++) {
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('Chrome is back, debugging enabled.');
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('Chrome relaunched but CDP didn’t come up in time.');
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
  }
@@ -423,6 +459,10 @@ async function runStart() {
423
459
  // restart Chrome gracefully (preserves tabs via Chrome's "Continue where
424
460
  // you left off" setting) and relaunch with the flag.
425
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;
426
466
  const alreadyDebuggable = await isChromeDebuggable();
427
467
  if (!alreadyDebuggable) {
428
468
  const needsRestart = await detectChromeRunningWithoutDebug();
@@ -433,12 +473,13 @@ async function runStart() {
433
473
  process.exit(1);
434
474
  }
435
475
  await restartChromeWithDebugFlag();
476
+ weRestartedIt = true;
436
477
  }
437
478
  }
438
479
 
439
480
  let chromeConn;
440
481
  try {
441
- chromeConn = await connectToChrome();
482
+ chromeConn = await connectToChrome(10, { skipLaunch: weRestartedIt });
442
483
  console.log('\nConnected to Chrome. Polling for queued jobs...');
443
484
  console.log('Go to your HALO dashboard and click "Auto-Apply" on any job.\n');
444
485
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halo-agent",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "HALO local apply agent — auto-fills job applications using your real Chrome session",
5
5
  "main": "index.js",
6
6
  "bin": {