halo-agent 2.1.0 → 2.2.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 (4) hide show
  1. package/index.js +73 -47
  2. package/package.json +2 -1
  3. package/poller.js +11 -7
  4. package/ui.js +85 -0
package/index.js CHANGED
@@ -17,6 +17,7 @@ const { loadConfig, saveConfig } = require('./config');
17
17
  const { connectToChrome } = require('./browser');
18
18
  const { startPolling } = require('./poller');
19
19
  const { startLocalServer } = require('./localServer');
20
+ const ui = require('./ui');
20
21
 
21
22
  const [,, command, ...cmdArgs] = process.argv;
22
23
 
@@ -34,13 +35,18 @@ async function main() {
34
35
  } else if (command === 'uninstall-autostart') {
35
36
  await runUninstallAutostart();
36
37
  } else {
37
- console.log('Usage:');
38
- console.log(' halo-agent pair <code> — one-click pair with the dashboard');
39
- console.log(' halo-agent start — start the agent');
40
- console.log(' halo-agent install-autostart — auto-start at login');
41
- console.log(' halo-agent uninstall-autostart — remove login auto-start');
42
- console.log(' halo-agent init — first-time setup (legacy: token paste)');
43
- console.log(' halo-agent token <value> — update auth token');
38
+ let v = '';
39
+ try { v = require('./package.json').version; } catch {}
40
+ ui.banner(v);
41
+ 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`);
49
+ console.log('');
44
50
  process.exit(1);
45
51
  }
46
52
  }
@@ -59,7 +65,7 @@ async function runUpdateToken(newToken) {
59
65
  const config = loadConfig() || {};
60
66
  config.token = resolved.trim();
61
67
  saveConfig(config);
62
- console.log('Token updated.');
68
+ ui.success('Token updated');
63
69
  }
64
70
 
65
71
  // One-click pairing. The user generates a 6-char code in the dashboard
@@ -68,8 +74,11 @@ async function runUpdateToken(newToken) {
68
74
  // the user to copy-paste an API token.
69
75
  async function runPair(code) {
70
76
  if (!code) {
71
- console.error('Usage: halo-agent pair <code>');
72
- console.error('Get a code from the HALO dashboard: Profile -> Connect agent.');
77
+ ui.fail('Missing pairing code.');
78
+ console.log('');
79
+ console.log(` ${ui.bold('Usage')} ${ui.cyan('halo-agent pair')} ${ui.gray('<code>')}`);
80
+ console.log(` ${ui.gray('Get a code from the HALO dashboard: Profile')} ${ui.gray(ui.sym.arrow)} ${ui.gray('Connect agent.')}`);
81
+ console.log('');
73
82
  process.exit(1);
74
83
  }
75
84
  const trimmed = String(code).trim().toUpperCase();
@@ -77,7 +86,8 @@ async function runPair(code) {
77
86
  // Default API URL — overridable via env for self-hosted Workers.
78
87
  const apiUrl = process.env.HALO_API_URL || 'https://halo-apply-os.amoghi2tb.workers.dev';
79
88
 
80
- console.log(`\nPairing with ${apiUrl}...`);
89
+ console.log('');
90
+ const spin = ui.spinner(`Pairing with ${ui.gray(apiUrl)}`);
81
91
  let res;
82
92
  try {
83
93
  res = await fetch(`${apiUrl}/agent-pair/claim`, {
@@ -86,19 +96,19 @@ async function runPair(code) {
86
96
  body: JSON.stringify({ code: trimmed }),
87
97
  });
88
98
  } catch (e) {
89
- console.error('Network error reaching HALO:', e.message);
99
+ spin.stop(`Network error reaching HALO: ${e.message}`, false);
90
100
  process.exit(1);
91
101
  }
92
102
 
93
103
  if (!res.ok) {
94
104
  let msg = `HTTP ${res.status}`;
95
105
  try { const j = await res.json(); msg = j.error || msg; } catch {}
96
- console.error(`Pair failed: ${msg}`);
97
- console.error('Codes expire 5 minutes after they are generated get a fresh one if needed.');
106
+ spin.stop(`Pair failed: ${msg}`, false);
107
+ ui.muted('Codes expire 5 minutes after they are generated. Get a fresh one if needed.');
98
108
  process.exit(1);
99
109
  }
100
110
  const data = await res.json();
101
- if (!data.token) { console.error('Pair response missing token.'); process.exit(1); }
111
+ if (!data.token) { spin.stop('Pair response missing token.', false); process.exit(1); }
102
112
 
103
113
  const { loadConfig, saveConfig } = require('./config');
104
114
  const existing = loadConfig() || {};
@@ -110,8 +120,11 @@ async function runPair(code) {
110
120
  };
111
121
  saveConfig(config);
112
122
 
113
- console.log('\nPaired. Token saved to ~/.halo-agent/config.json');
114
- console.log('Run `halo-agent start` to connect the agent.\n');
123
+ spin.stop(`Paired with HALO`);
124
+ ui.muted(` Token saved to ~/.halo-agent/config.json`);
125
+ console.log('');
126
+ console.log(` ${ui.gray('Next:')} ${ui.cyan('halo-agent start')}`);
127
+ console.log('');
115
128
  }
116
129
 
117
130
  // Auto-start at login. The user picked "always on" — once installed, the
@@ -166,10 +179,12 @@ async function runInstallAutostart() {
166
179
  spawnSync('launchctl', ['unload', plistPath], { stdio: 'ignore' });
167
180
  spawnSync('launchctl', ['load', plistPath], { stdio: 'ignore' });
168
181
  } catch { /* user can `launchctl load` manually */ }
169
- console.log(`\nInstalled login auto-start (macOS LaunchAgent).`);
170
- console.log(` plist: ${plistPath}`);
171
- console.log(` logs: ${logPath}`);
172
- console.log(`The agent will start automatically at login. It's running now.`);
182
+ console.log('');
183
+ ui.success('Login auto-start installed (macOS LaunchAgent)');
184
+ ui.muted(` plist: ${plistPath}`);
185
+ ui.muted(` logs: ${logPath}`);
186
+ ui.muted(' The agent will start automatically at login. It is running now.');
187
+ console.log('');
173
188
  return;
174
189
  }
175
190
 
@@ -183,10 +198,12 @@ async function runInstallAutostart() {
183
198
  WshShell.Run """${nodePath.replace(/"/g, '""')}"" ""${agentPath.replace(/"/g, '""')}"" start", 0, False
184
199
  Set WshShell = Nothing`;
185
200
  fs.writeFileSync(vbsPath, vbs);
186
- console.log(`\nInstalled login auto-start (Windows Startup folder).`);
187
- console.log(` vbs: ${vbsPath}`);
188
- console.log(` logs: ${logPath} (agent appends its own output)`);
189
- console.log(`The agent will start at your next login. Run \`halo-agent start\` now to begin this session.`);
201
+ console.log('');
202
+ ui.success('Login auto-start installed (Windows Startup folder)');
203
+ ui.muted(` vbs: ${vbsPath}`);
204
+ ui.muted(` logs: ${logPath} (agent appends its own output)`);
205
+ ui.muted(` Starts at next login. Run ${ui.cyan('halo-agent start')} now to begin this session.`);
206
+ console.log('');
190
207
  return;
191
208
  }
192
209
 
@@ -217,10 +234,12 @@ WantedBy=default.target
217
234
  // Linger so the unit survives logout — best effort.
218
235
  spawnSync('loginctl', ['enable-linger', os.userInfo().username], { stdio: 'ignore' });
219
236
  } catch { /* user can enable manually */ }
220
- console.log(`\nInstalled login auto-start (systemd --user).`);
221
- console.log(` unit: ${unitPath}`);
222
- console.log(` logs: ${logPath}`);
223
- console.log(`The agent should be running now: systemctl --user status halo-agent`);
237
+ console.log('');
238
+ ui.success('Login auto-start installed (systemd --user)');
239
+ ui.muted(` unit: ${unitPath}`);
240
+ ui.muted(` logs: ${logPath}`);
241
+ ui.muted(` Check status: ${ui.cyan('systemctl --user status halo-agent')}`);
242
+ console.log('');
224
243
  }
225
244
 
226
245
  async function runUninstallAutostart() {
@@ -235,13 +254,13 @@ async function runUninstallAutostart() {
235
254
  spawnSync('launchctl', ['unload', plistPath], { stdio: 'ignore' });
236
255
  } catch {}
237
256
  if (fs.existsSync(plistPath)) fs.unlinkSync(plistPath);
238
- console.log('Removed macOS LaunchAgent.');
257
+ ui.success('Removed macOS LaunchAgent');
239
258
  return;
240
259
  }
241
260
  if (process.platform === 'win32') {
242
261
  const vbsPath = path.join(process.env.APPDATA || os.homedir(), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', 'halo-agent.vbs');
243
262
  if (fs.existsSync(vbsPath)) fs.unlinkSync(vbsPath);
244
- console.log('Removed Windows Startup entry.');
263
+ ui.success('Removed Windows Startup entry');
245
264
  return;
246
265
  }
247
266
  try {
@@ -250,7 +269,7 @@ async function runUninstallAutostart() {
250
269
  } catch {}
251
270
  const unitPath = path.join(os.homedir(), '.config', 'systemd', 'user', 'halo-agent.service');
252
271
  if (fs.existsSync(unitPath)) fs.unlinkSync(unitPath);
253
- console.log('Removed systemd --user unit.');
272
+ ui.success('Removed systemd --user unit');
254
273
  }
255
274
 
256
275
  async function runInit() {
@@ -287,8 +306,10 @@ async function runInit() {
287
306
  };
288
307
 
289
308
  saveConfig(config);
290
- console.log('\nConfig saved to ~/.halo-agent/config.json');
291
- console.log('Run "halo-agent start" to begin.\n');
309
+ console.log('');
310
+ ui.success('Config saved to ~/.halo-agent/config.json');
311
+ console.log(` ${ui.gray('Next:')} ${ui.cyan('halo-agent start')}`);
312
+ console.log('');
292
313
  }
293
314
 
294
315
  // Is Chrome running without the --remote-debugging-port=9222 flag?
@@ -325,13 +346,13 @@ async function detectChromeRunningWithoutDebug() {
325
346
  function offerChromeRestart() {
326
347
  return new Promise((resolve) => {
327
348
  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").');
349
+ ui.warn('Chrome is running, but without the debug flag the agent needs.');
350
+ ui.muted(' I can restart it for you. Your tabs reopen automatically');
351
+ ui.muted(' (if your "On startup" setting is "Continue where you left off").');
331
352
  console.log('');
332
353
  const readline = require('readline');
333
354
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
334
- rl.question('Restart Chrome now? [Y/n] ', (ans) => {
355
+ rl.question(`${ui.cyan(ui.sym.arrow)} Restart Chrome now? ${ui.gray('[Y/n]')} `, (ans) => {
335
356
  rl.close();
336
357
  const a = (ans || '').trim().toLowerCase();
337
358
  resolve(a === '' || a === 'y' || a === 'yes');
@@ -446,12 +467,14 @@ async function waitForChromeGone(timeoutMs) {
446
467
  async function runStart() {
447
468
  const config = loadConfig();
448
469
  if (!config || !config.token) {
449
- console.error('No config found. Run "halo-agent init" first.');
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.')}`);
450
472
  process.exit(1);
451
473
  }
452
474
 
453
- console.log('\nHALO Agent starting...');
454
- console.log('Connecting to your Chrome browser...\n');
475
+ let version = '';
476
+ try { version = require('./package.json').version; } catch {}
477
+ ui.banner(version);
455
478
 
456
479
  // The agent runs its own isolated Chrome instance (separate --user-data-dir
457
480
  // at ~/.halo-agent/chrome-profile) so it never collides with the user's
@@ -459,22 +482,25 @@ async function runStart() {
459
482
  // into Workday / LinkedIn ONCE in the agent's Chrome and those sessions
460
483
  // persist across runs. No need to detect or restart the user's Chrome.
461
484
  let chromeConn;
485
+ const spin = ui.spinner('Launching Chrome');
462
486
  try {
463
487
  chromeConn = await connectToChrome(10);
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.');
466
- console.log('Go to your HALO dashboard and click "Auto-Apply" on any job.\n');
488
+ spin.stop('Connected to Chrome');
467
489
  } catch (err) {
468
- console.error('\nCould not connect to Chrome:', err.message);
490
+ spin.stop(`Could not connect to Chrome: ${err.message}`, false);
469
491
  process.exit(1);
470
492
  }
493
+ ui.muted(` First time? Sign into Workday / LinkedIn once in this Chrome window — sessions persist.`);
494
+ ui.muted(` Then queue a job from your HALO dashboard.`);
495
+ console.log('');
471
496
 
472
497
  // Start the local HTTP server so the HALO dashboard can trigger Manus automation
473
498
  startLocalServer();
474
499
 
475
500
  // Graceful shutdown
476
501
  process.on('SIGINT', async () => {
477
- console.log('\nShutting down...');
502
+ console.log('');
503
+ ui.muted('Shutting down…');
478
504
  try { await chromeConn.browser.close(); } catch {}
479
505
  process.exit(0);
480
506
  });
@@ -483,6 +509,6 @@ async function runStart() {
483
509
  }
484
510
 
485
511
  main().catch(err => {
486
- console.error('Fatal error:', err.message);
512
+ ui.fail(`Fatal: ${err.message}`);
487
513
  process.exit(1);
488
514
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halo-agent",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "HALO local apply agent — auto-fills job applications using your real Chrome session",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -28,6 +28,7 @@
28
28
  "captcha.js",
29
29
  "vision.js",
30
30
  "manusAutomate.js",
31
+ "ui.js",
31
32
  "README.md"
32
33
  ],
33
34
  "keywords": [
package/poller.js CHANGED
@@ -8,12 +8,15 @@
8
8
 
9
9
  const { runJob } = require('./orchestrator');
10
10
  const { cfAccessHeaders } = require('./config');
11
+ const ui = require('./ui');
11
12
 
12
13
  let active = false;
13
14
  let sessionId = null;
14
15
 
15
16
  async function startPolling(chromeConn, config) {
16
- console.log('[poller] Started. Polling for queued jobs every 5s...');
17
+ ui.success('Listening for jobs');
18
+ ui.muted(` Polling every 5s. ${ui.cyan('Ctrl-C')} to stop.`);
19
+ console.log('');
17
20
 
18
21
  // Register agent session with backend
19
22
  sessionId = await registerSession(config);
@@ -28,7 +31,8 @@ async function startPolling(chromeConn, config) {
28
31
  if (!item) continue;
29
32
 
30
33
  active = true;
31
- console.log(`[poller] Picked up job: ${item.company} - ${item.title} (${item.id})`);
34
+ console.log('');
35
+ ui.info(`${ui.bold(item.company)} ${ui.gray(ui.sym.dot)} ${item.title}`);
32
36
 
33
37
  // Update session status
34
38
  await heartbeat(config, sessionId, 'filling', item.job_id);
@@ -37,7 +41,7 @@ async function startPolling(chromeConn, config) {
37
41
  try {
38
42
  await runJob(item, chromeConn, config, reportStatus);
39
43
  } catch (err) {
40
- console.error('[poller] runJob threw:', err.message);
44
+ ui.fail(`Job error: ${err.message}`);
41
45
  // Last-resort safety net — orchestrator usually classifies, but if a
42
46
  // throw escaped without classification, fall back to 'generic'.
43
47
  await reportStatus('NEEDS_ATTENTION', {
@@ -80,7 +84,7 @@ function makeReporter(config, queueId, sessionId) {
80
84
  body: JSON.stringify({ status, ...extra }),
81
85
  });
82
86
  } catch (e) {
83
- console.warn('[poller] Could not report status:', e.message);
87
+ ui.warn(`Could not report status: ${e.message}`);
84
88
  }
85
89
 
86
90
  // Granular step PATCH — drives the live SSE feed. Fire-and-forget; if the
@@ -142,14 +146,14 @@ async function registerSession(config) {
142
146
  });
143
147
  if (!res.ok) {
144
148
  const text = await res.text().catch(() => res.status);
145
- console.warn(`[poller] Session registration failed (${res.status}):`, text);
149
+ ui.warn(`Session registration failed (${res.status}): ${text}`);
146
150
  return null;
147
151
  }
148
152
  const data = await res.json();
149
- console.log('[poller] Session registered:', data.session_id);
153
+ ui.muted(` Session ${ui.gray(data.session_id)}`);
150
154
  return data.session_id;
151
155
  } catch (e) {
152
- console.warn('[poller] Session registration error:', e.message);
156
+ ui.warn(`Session registration error: ${e.message}`);
153
157
  return null;
154
158
  }
155
159
  }
package/ui.js ADDED
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Tiny zero-dep terminal styling helpers. ANSI escapes only — no chalk/ora,
5
+ * because the agent is CommonJS and those packages are ESM-only in v5/v8.
6
+ *
7
+ * Auto-disables color when stdout isn't a TTY (e.g. piped to a file, or
8
+ * the LaunchAgent/systemd log) or when NO_COLOR is set (https://no-color.org).
9
+ *
10
+ * Used everywhere we want the agent to look like a modern CLI on camera
11
+ * (Vercel, Bun, pnpm) instead of plain console.log output.
12
+ */
13
+
14
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
15
+
16
+ const wrap = (open, close) => (s) => useColor ? `\x1b[${open}m${s}\x1b[${close}m` : String(s);
17
+
18
+ const bold = wrap(1, 22);
19
+ const dim = wrap(2, 22);
20
+ const red = wrap(31, 39);
21
+ const green = wrap(32, 39);
22
+ const yellow = wrap(33, 39);
23
+ const blue = wrap(34, 39);
24
+ const magenta = wrap(35, 39);
25
+ const cyan = wrap(36, 39);
26
+ const gray = wrap(90, 39);
27
+
28
+ // HALO's brand color is ember; render as bright orange when supported.
29
+ const ember = (s) => useColor ? `\x1b[38;5;208m${s}\x1b[39m` : String(s);
30
+
31
+ // Status glyphs — match what modern CLIs do. ASCII fallbacks for terminals
32
+ // that can't render the unicode (rare in 2026, but cheap insurance).
33
+ const sym = process.platform === 'win32' && !process.env.WT_SESSION
34
+ ? { tick: '√', cross: '×', bullet: '*', arrow: '→', dot: '·' }
35
+ : { tick: '✓', cross: '✗', bullet: '•', arrow: '→', dot: '·' };
36
+
37
+ const success = (msg) => console.log(`${green(sym.tick)} ${msg}`);
38
+ const fail = (msg) => console.log(`${red(sym.cross)} ${msg}`);
39
+ const info = (msg) => console.log(`${cyan(sym.arrow)} ${msg}`);
40
+ const warn = (msg) => console.log(`${yellow('!')} ${msg}`);
41
+ const muted = (msg) => console.log(gray(msg));
42
+ const step = (msg) => console.log(`${gray(sym.dot)} ${msg}`);
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.
46
+ function banner(version) {
47
+ const v = version ? gray(`v${version}`) : '';
48
+ const line = gray('─'.repeat(40));
49
+ 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}`);
53
+ }
54
+
55
+ // Lightweight spinner for waiting states ("Connecting to Chrome..."). Frames
56
+ // match the standard braille spinner most users have seen. Returns a stop()
57
+ // fn — call it with a final message + status to render a success/fail line
58
+ // in place of the spinner.
59
+ function spinner(message) {
60
+ if (!useColor) {
61
+ process.stdout.write(`${gray(sym.dot)} ${message}\n`);
62
+ return { stop: (finalMsg, ok = true) => (ok ? success(finalMsg) : fail(finalMsg)) };
63
+ }
64
+ const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
65
+ let i = 0;
66
+ const render = () => {
67
+ process.stdout.write(`\r${cyan(frames[i = (i + 1) % frames.length])} ${message} `);
68
+ };
69
+ const handle = setInterval(render, 80);
70
+ render();
71
+ return {
72
+ update: (newMessage) => { message = newMessage; },
73
+ stop: (finalMsg, ok = true) => {
74
+ clearInterval(handle);
75
+ process.stdout.write('\r\x1b[K'); // clear the spinner line
76
+ if (ok) success(finalMsg);
77
+ else fail(finalMsg);
78
+ },
79
+ };
80
+ }
81
+
82
+ module.exports = {
83
+ bold, dim, red, green, yellow, blue, magenta, cyan, gray, ember,
84
+ sym, success, fail, info, warn, muted, step, banner, spinner,
85
+ };