halo-agent 2.2.2 → 2.2.4

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 CHANGED
@@ -135,7 +135,7 @@ async function connectToChrome(retries = 10, opts = {}) {
135
135
  ctx = fresh.contexts()[0] || await fresh.newContext();
136
136
  conn.context = ctx;
137
137
  }
138
- const page = await ctx.newPage();
138
+ let page = await ctx.newPage();
139
139
  if (url) {
140
140
  try {
141
141
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
@@ -153,6 +153,39 @@ async function connectToChrome(retries = 10, opts = {}) {
153
153
  await page.waitForTimeout(8000);
154
154
  }
155
155
  }
156
+
157
+ // VERIFY-AFTER-GOTO. connectOverCDP against the user's REAL Chrome
158
+ // has a tab-handle race: ctx.newPage() may hand back a different
159
+ // tab than the one that actually navigated (e.g. Chrome already
160
+ // had a restored New Tab open). Symptom: the agent then scans
161
+ // about:blank / chrome://newtab forever while the target URL is
162
+ // loaded in a sibling tab. Self-correct: if our handle isn't on
163
+ // the target host, find the tab that IS and adopt it.
164
+ try {
165
+ const here = page.url();
166
+ const targetHost = new URL(url).hostname;
167
+ const onTarget = (u) => { try { return new URL(u).hostname === targetHost; } catch { return false; } };
168
+ if (!onTarget(here)) {
169
+ // Look across all pages in this context for the one that navigated.
170
+ const pages = ctx.pages();
171
+ const match = pages.find((p) => onTarget(p.url()));
172
+ if (match && match !== page) {
173
+ console.warn(`[halo-agent] newPage handle was on "${here.slice(0, 40)}" but target loaded in another tab — adopting it.`);
174
+ // Close the stray blank tab we opened so it doesn't litter.
175
+ if (/^about:blank$|^chrome:\/\/newtab/i.test(here)) {
176
+ try { await page.close(); } catch {}
177
+ }
178
+ page = match;
179
+ await page.bringToFront().catch(() => {});
180
+ } else if (/^about:blank$|^chrome:\/\/newtab/i.test(here)) {
181
+ // Our tab is genuinely blank and nothing else navigated —
182
+ // the goto silently no-op'd. Retry the navigation on THIS tab.
183
+ console.warn('[halo-agent] Tab still blank after goto — retrying navigation on this handle...');
184
+ await page.goto(url, { waitUntil: 'commit', timeout: 30000 }).catch(() => {});
185
+ await page.waitForTimeout(4000);
186
+ }
187
+ }
188
+ } catch { /* best-effort self-correction; fall through */ }
156
189
  }
157
190
  return page;
158
191
  },
package/index.js CHANGED
@@ -38,14 +38,18 @@ async function main() {
38
38
  let v = '';
39
39
  try { v = require('./package.json').version; } catch {}
40
40
  ui.banner(v);
41
+ console.log(` ${ui.bold('Usage')} ${ui.gray('halo-agent <command>')}`);
41
42
  console.log('');
42
- console.log(` ${ui.bold('Usage')}`);
43
- console.log(` ${ui.cyan('halo-agent pair')} ${ui.gray('<code>')} One-click pair with the dashboard`);
44
- console.log(` ${ui.cyan('halo-agent start')} Start the agent`);
45
- console.log(` ${ui.cyan('halo-agent install-autostart')} Auto-start at login`);
46
- console.log(` ${ui.cyan('halo-agent uninstall-autostart')} Remove login auto-start`);
47
- console.log(` ${ui.cyan('halo-agent init')} First-time setup (legacy: token paste)`);
48
- console.log(` ${ui.cyan('halo-agent token')} ${ui.gray('<value>')} Update auth token`);
43
+ ui.commandTable([
44
+ ['pair <code>', 'One-click pair with the HALO dashboard'],
45
+ ['start', 'Connect to Chrome and run the agent'],
46
+ ['install-autostart', 'Run automatically at every login'],
47
+ ['uninstall-autostart', 'Remove the login auto-start'],
48
+ ['init', 'First-time setup (legacy token paste)'],
49
+ ['token <value>', 'Update the auth token'],
50
+ ]);
51
+ console.log('');
52
+ ui.hint('Get a pairing code: HALO dashboard → Profile → Auto-apply agent → Get pairing code.');
49
53
  console.log('');
50
54
  process.exit(1);
51
55
  }
@@ -123,7 +127,8 @@ async function runPair(code) {
123
127
  spin.stop(`Paired with HALO`);
124
128
  ui.muted(` Token saved to ~/.halo-agent/config.json`);
125
129
  console.log('');
126
- console.log(` ${ui.gray('Next:')} ${ui.cyan('halo-agent start')}`);
130
+ ui.nextStep('halo-agent start', 'connect Chrome and start applying');
131
+ ui.hint('Or `halo-agent install-autostart` to run it at every login.');
127
132
  console.log('');
128
133
  }
129
134
 
@@ -308,7 +313,7 @@ async function runInit() {
308
313
  saveConfig(config);
309
314
  console.log('');
310
315
  ui.success('Config saved to ~/.halo-agent/config.json');
311
- console.log(` ${ui.gray('Next:')} ${ui.cyan('halo-agent start')}`);
316
+ ui.nextStep('halo-agent start', 'connect Chrome and start applying');
312
317
  console.log('');
313
318
  }
314
319
 
@@ -467,14 +472,14 @@ async function waitForChromeGone(timeoutMs) {
467
472
  async function runStart() {
468
473
  const config = loadConfig();
469
474
  if (!config || !config.token) {
470
- ui.fail('No config found.');
471
- console.log(` ${ui.gray('Run')} ${ui.cyan('halo-agent pair <code>')} ${ui.gray('first. Get a code from the HALO dashboard.')}`);
475
+ ui.fail('Not paired yet — no config found.');
476
+ ui.nextStep('halo-agent pair <code>', 'get a code from the HALO dashboard');
472
477
  process.exit(1);
473
478
  }
474
479
 
475
480
  let version = '';
476
481
  try { version = require('./package.json').version; } catch {}
477
- ui.banner(version);
482
+ ui.bigBanner(version); // hero frame — the one you'd screenshot
478
483
 
479
484
  // The agent runs its own isolated Chrome instance (separate --user-data-dir
480
485
  // at ~/.halo-agent/chrome-profile) so it never collides with the user's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halo-agent",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "description": "HALO local apply agent — auto-fills job applications using your real Chrome session",
5
5
  "main": "index.js",
6
6
  "bin": {
package/poller.js CHANGED
@@ -15,7 +15,8 @@ let sessionId = null;
15
15
 
16
16
  async function startPolling(chromeConn, config) {
17
17
  ui.success('Listening for jobs');
18
- ui.muted(` Polling every 5s. ${ui.cyan('Ctrl-C')} to stop.`);
18
+ ui.hint(`Queue one from HALO: a job's prep page → Approve & send to agent.`);
19
+ ui.muted(` Polling every 5s · ${ui.cyan('Ctrl-C')} to stop`);
19
20
  console.log('');
20
21
 
21
22
  // Register agent session with backend
package/ui.js CHANGED
@@ -41,15 +41,76 @@ const warn = (msg) => console.log(`${yellow('!')} ${msg}`);
41
41
  const muted = (msg) => console.log(gray(msg));
42
42
  const step = (msg) => console.log(`${gray(sym.dot)} ${msg}`);
43
43
 
44
- // Banner shown on `start`. Three lines so it has presence on camera without
45
- // being a giant ASCII-art block that ages badly.
44
+ // Strip ANSI so box math measures the VISIBLE width, not the escape-padded
45
+ // string length. Without this, colored content overflows the box border.
46
+ const STRIP_ANSI = /\x1b\[[0-9;]*m/g;
47
+ const visLen = (s) => String(s).replace(STRIP_ANSI, '').length;
48
+
49
+ // Box-drawing glyphs (rounded). ASCII fallback on legacy Windows terminals.
50
+ const box = (process.platform === 'win32' && !process.env.WT_SESSION)
51
+ ? { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|' }
52
+ : { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' };
53
+
54
+ // The compact boxed wordmark. Ember mark + tagline + version, in a rounded
55
+ // box — the Vercel/Bun look. Reads well on camera and in a small terminal,
56
+ // and ages better than big ASCII art. The ⬢ is HALO's hex mark.
46
57
  function banner(version) {
47
- const v = version ? gray(`v${version}`) : '';
48
- const line = gray(''.repeat(40));
58
+ const mark = `${ember('⬢')} ${ember(bold('HALO'))}`;
59
+ const ver = version ? `${gray('agent')} ${gray('v' + version)}` : gray('agent');
60
+ // Two content lines.
61
+ const line1 = `${mark} ${gray('·')} ${ver}`;
62
+ const line2 = gray('auto-apply, in your own Chrome');
63
+ const inner = Math.max(visLen(line1), visLen(line2)) + 2; // 1 space padding each side
64
+ const pad = (s) => ' ' + s + ' '.repeat(inner - visLen(s) - 1);
65
+ const top = gray(box.tl + box.h.repeat(inner) + box.tr);
66
+ const bot = gray(box.bl + box.h.repeat(inner) + box.br);
67
+ const bar = gray(box.v);
68
+ console.log('');
69
+ console.log(` ${top}`);
70
+ console.log(` ${bar}${pad(line1)}${bar}`);
71
+ console.log(` ${bar}${pad(line2)}${bar}`);
72
+ console.log(` ${bot}`);
73
+ console.log('');
74
+ }
75
+
76
+ // Big block-letter wordmark for the hero moment (`start`) — the frame you'd
77
+ // screenshot for a LinkedIn post. Five-row ASCII HALO in ember, tagline +
78
+ // version underneath. Falls back to the compact boxed banner when color is
79
+ // off (logs/CI) so the auto-start service log doesn't get a wall of blocks.
80
+ const HALO_ART = [
81
+ '██ ██ █████ ██ ██████ ',
82
+ '██ ██ ██ ██ ██ ██ ██ ',
83
+ '███████ ███████ ██ ██ ██ ',
84
+ '██ ██ ██ ██ ██ ██ ██ ',
85
+ '██ ██ ██ ██ ███████ ██████ ',
86
+ ];
87
+ function bigBanner(version) {
88
+ if (!useColor) { banner(version); return; }
89
+ const ver = version ? gray('· agent v' + version) : gray('· agent');
90
+ console.log('');
91
+ for (const row of HALO_ART) console.log(' ' + ember(row));
92
+ console.log(' ' + gray('auto-apply, in your own Chrome') + ' ' + ver);
49
93
  console.log('');
50
- console.log(` ${ember(bold('HALO'))} ${bold('Agent')} ${v}`);
51
- console.log(` ${gray('Auto-apply, in your own Chrome.')}`);
52
- console.log(` ${line}`);
94
+ }
95
+
96
+ // Affordance helper: the obvious next command, rendered consistently so the
97
+ // user always knows what to type next. Shown after pair/init/etc.
98
+ function nextStep(cmd, note) {
99
+ const tail = note ? ` ${gray(note)}` : '';
100
+ console.log(` ${gray('Next')} ${gray(sym.arrow)} ${cyan(cmd)}${tail}`);
101
+ }
102
+
103
+ // A hint line — softer than nextStep, for "you can also…" affordances.
104
+ function hint(msg) {
105
+ console.log(` ${gray(sym.dot)} ${gray(msg)}`);
106
+ }
107
+
108
+ // Aligned command table for the usage screen. rows: [[cmd, desc], ...].
109
+ function commandTable(rows) {
110
+ const w = Math.max(...rows.map(([c]) => c.length));
111
+ for (const [cmd, desc] of rows) {
112
+ console.log(` ${cyan(cmd.padEnd(w))} ${gray(desc)}`);
113
+ }
53
114
  }
54
115
 
55
116
  // Lightweight spinner for waiting states ("Connecting to Chrome..."). Frames
@@ -81,5 +142,6 @@ function spinner(message) {
81
142
 
82
143
  module.exports = {
83
144
  bold, dim, red, green, yellow, blue, magenta, cyan, gray, ember,
84
- sym, success, fail, info, warn, muted, step, banner, spinner,
145
+ sym, success, fail, info, warn, muted, step,
146
+ banner, bigBanner, spinner, nextStep, hint, commandTable, visLen,
85
147
  };