halo-agent 1.1.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/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "halo-agent",
3
+ "version": "1.1.0",
4
+ "description": "HALO local apply agent — auto-fills job applications using your real Chrome session",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "halo-agent": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js start",
11
+ "init": "node index.js init",
12
+ "pair": "node index.js pair",
13
+ "install-autostart": "node index.js install-autostart",
14
+ "uninstall-autostart": "node index.js uninstall-autostart"
15
+ },
16
+ "files": [
17
+ "index.js",
18
+ "config.js",
19
+ "browser.js",
20
+ "poller.js",
21
+ "orchestrator.js",
22
+ "localServer.js",
23
+ "filler.js",
24
+ "scanPage.js",
25
+ "captcha.js",
26
+ "vision.js",
27
+ "manusAutomate.js",
28
+ "README.md"
29
+ ],
30
+ "keywords": [
31
+ "halo",
32
+ "job-application",
33
+ "auto-apply",
34
+ "playwright",
35
+ "cli"
36
+ ],
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "playwright": "^1.44.0",
40
+ "ws": "^8.17.0",
41
+ "node-fetch": "^3.3.2",
42
+ "ora": "^8.0.1",
43
+ "inquirer": "^9.2.22",
44
+ "chalk": "^5.3.0"
45
+ },
46
+ "engines": {
47
+ "node": ">=18"
48
+ }
49
+ }
package/poller.js ADDED
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Polls HALO backend every 5 seconds for queued jobs.
5
+ * When a job is found, hands it to the orchestrator.
6
+ * Runs sequentially — one job at a time to avoid Chrome chaos.
7
+ */
8
+
9
+ const { runJob } = require('./orchestrator');
10
+ const { cfAccessHeaders } = require('./config');
11
+
12
+ let active = false;
13
+ let sessionId = null;
14
+
15
+ async function startPolling(chromeConn, config) {
16
+ console.log('[poller] Started. Polling for queued jobs every 5s...');
17
+
18
+ // Register agent session with backend
19
+ sessionId = await registerSession(config);
20
+
21
+ const heartbeatInterval = setInterval(() => heartbeat(config, sessionId), 15000);
22
+
23
+ while (true) {
24
+ await new Promise(r => setTimeout(r, 5000));
25
+ if (active) continue; // already processing a job
26
+
27
+ const item = await fetchNextJob(config);
28
+ if (!item) continue;
29
+
30
+ active = true;
31
+ console.log(`[poller] Picked up job: ${item.company} - ${item.title} (${item.id})`);
32
+
33
+ // Update session status
34
+ await heartbeat(config, sessionId, 'filling', item.job_id);
35
+
36
+ const reportStatus = makeReporter(config, item.id, sessionId);
37
+ try {
38
+ await runJob(item, chromeConn, config, reportStatus);
39
+ } catch (err) {
40
+ console.error('[poller] runJob threw:', err.message);
41
+ // Last-resort safety net — orchestrator usually classifies, but if a
42
+ // throw escaped without classification, fall back to 'generic'.
43
+ await reportStatus('NEEDS_ATTENTION', {
44
+ needs_attention_reason: err.message,
45
+ intervention_type: 'generic',
46
+ step: 'NEEDS_ATTENTION',
47
+ step_detail: err.message?.slice(0, 120),
48
+ }).catch(() => {});
49
+ } finally {
50
+ active = false;
51
+ await heartbeat(config, sessionId, 'idle', null);
52
+ }
53
+ }
54
+ }
55
+
56
+ // Maps the coarse status the orchestrator already emits to the fine-grained
57
+ // step value the SSE feed renders. The orchestrator can override either by
58
+ // passing { step, step_detail } explicitly in extra.
59
+ const STATUS_TO_STEP = {
60
+ PENDING: 'ANALYZING',
61
+ IN_PROGRESS: 'FILLING',
62
+ REVIEWING: 'REVIEWING',
63
+ DONE: 'DONE',
64
+ NEEDS_ATTENTION: 'NEEDS_ATTENTION',
65
+ CANCELLED: 'DONE',
66
+ };
67
+
68
+ function makeReporter(config, queueId, sessionId) {
69
+ return async function reportStatus(status, extra = {}) {
70
+ // Existing coarse-status PATCH — preserves all current backend behaviour
71
+ // (auto-create submission on DONE, etc). Untouched by the SSE work.
72
+ try {
73
+ await fetch(`${config.apiUrl}/apply-queue/${queueId}`, {
74
+ method: 'PATCH',
75
+ headers: {
76
+ Authorization: `Bearer ${config.token}`,
77
+ 'Content-Type': 'application/json',
78
+ ...cfAccessHeaders(config),
79
+ },
80
+ body: JSON.stringify({ status, ...extra }),
81
+ });
82
+ } catch (e) {
83
+ console.warn('[poller] Could not report status:', e.message);
84
+ }
85
+
86
+ // Granular step PATCH — drives the live SSE feed. Fire-and-forget; if the
87
+ // backend is on an older version that doesn't have /agent yet, this 404s
88
+ // silently and the user just sees the coarse status as before.
89
+ const step = extra.step || STATUS_TO_STEP[status] || status;
90
+ const detail = extra.step_detail || extra.detail || extra.needs_attention_reason || null;
91
+ const fields_filled = typeof extra.fields_filled === 'number' ? extra.fields_filled : undefined;
92
+
93
+ try {
94
+ await fetch(`${config.apiUrl}/agent/queue/${queueId}/step`, {
95
+ method: 'PATCH',
96
+ headers: {
97
+ Authorization: `Bearer ${config.token}`,
98
+ 'Content-Type': 'application/json',
99
+ ...cfAccessHeaders(config),
100
+ },
101
+ body: JSON.stringify({
102
+ step,
103
+ detail,
104
+ ...(fields_filled !== undefined ? { fields_filled } : {}),
105
+ ...(sessionId ? { session_id: sessionId } : {}),
106
+ }),
107
+ });
108
+ } catch { /* fire-and-forget; non-critical */ }
109
+ };
110
+ }
111
+
112
+ async function fetchNextJob(config) {
113
+ try {
114
+ const res = await fetch(`${config.apiUrl}/apply-queue/next`, {
115
+ headers: {
116
+ Authorization: `Bearer ${config.token}`,
117
+ ...cfAccessHeaders(config),
118
+ },
119
+ });
120
+ if (!res.ok) return null;
121
+ const data = await res.json();
122
+ return data.item || null;
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ async function registerSession(config) {
129
+ try {
130
+ const res = await fetch(`${config.apiUrl}/apply-queue/agent/sessions`, {
131
+ method: 'POST',
132
+ headers: {
133
+ Authorization: `Bearer ${config.token}`,
134
+ 'Content-Type': 'application/json',
135
+ ...cfAccessHeaders(config),
136
+ },
137
+ body: JSON.stringify({
138
+ platform: process.platform,
139
+ agent_version: require('./package.json').version,
140
+ status: 'idle',
141
+ }),
142
+ });
143
+ if (!res.ok) {
144
+ const text = await res.text().catch(() => res.status);
145
+ console.warn(`[poller] Session registration failed (${res.status}):`, text);
146
+ return null;
147
+ }
148
+ const data = await res.json();
149
+ console.log('[poller] Session registered:', data.session_id);
150
+ return data.session_id;
151
+ } catch (e) {
152
+ console.warn('[poller] Session registration error:', e.message);
153
+ return null;
154
+ }
155
+ }
156
+
157
+ async function heartbeat(config, sessionId, status = 'idle', currentJobId = null) {
158
+ if (!sessionId) return;
159
+ try {
160
+ await fetch(`${config.apiUrl}/apply-queue/agent/sessions`, {
161
+ method: 'POST',
162
+ headers: {
163
+ Authorization: `Bearer ${config.token}`,
164
+ 'Content-Type': 'application/json',
165
+ ...cfAccessHeaders(config),
166
+ },
167
+ body: JSON.stringify({ id: sessionId, status, current_job_id: currentJobId }),
168
+ });
169
+ } catch {}
170
+ }
171
+
172
+ module.exports = { startPolling };