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/README.md +43 -0
- package/browser.js +157 -0
- package/captcha.js +217 -0
- package/config.js +37 -0
- package/filler.js +987 -0
- package/index.js +360 -0
- package/localServer.js +270 -0
- package/manusAutomate.js +349 -0
- package/orchestrator.js +1122 -0
- package/package.json +49 -0
- package/poller.js +172 -0
- package/scanPage.js +606 -0
- package/vision.js +398 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# halo-agent
|
|
2
|
+
|
|
3
|
+
Local apply agent for [HALO](https://halo-apply-os.amoghi2tb.workers.dev). Auto-fills job applications in **your real Chrome session** — your cookies, your IP, your logged-in tabs — so it bypasses the bot-detection that blocks cloud headless browsers on sites like Workday and LinkedIn.
|
|
4
|
+
|
|
5
|
+
## Install + pair
|
|
6
|
+
|
|
7
|
+
In the HALO dashboard, open **Profile → Auto-apply agent → Get pairing code**. Then on your machine:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npx halo-agent pair <CODE>
|
|
11
|
+
npx halo-agent install-autostart # optional: run continuously at every login
|
|
12
|
+
npx halo-agent start # connect now
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That's it. The dashboard's "Agent connected" pill should flip on within 30 seconds.
|
|
16
|
+
|
|
17
|
+
## How it works
|
|
18
|
+
|
|
19
|
+
- Polls `GET /apply-queue/next` for queued jobs.
|
|
20
|
+
- Launches your real Chrome via Playwright (CDP on port 9222, your default profile) — *not* a headless browser.
|
|
21
|
+
- Fills the application with materials you prepped + signed off on in HALO (tailored resume PDF, reviewed cover letter, per-field answers).
|
|
22
|
+
- Defaults to **review-first**: agent fills, then pauses for your sign-off. Per-job auto-submit toggle in HALO if you want it to submit directly.
|
|
23
|
+
- CAPTCHA: CapSolver if configured, vision fallback via Anthropic if not.
|
|
24
|
+
- Heartbeats every 15s so the dashboard knows you're online.
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
| | |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `halo-agent pair <code>` | One-click pair with HALO (recommended) |
|
|
31
|
+
| `halo-agent start` | Connect + start polling |
|
|
32
|
+
| `halo-agent install-autostart` | Run at every login (macOS / Windows / Linux) |
|
|
33
|
+
| `halo-agent uninstall-autostart` | Remove the autostart entry |
|
|
34
|
+
| `halo-agent init` | Legacy: manual token paste |
|
|
35
|
+
| `halo-agent token <value>` | Update the saved auth token |
|
|
36
|
+
|
|
37
|
+
## Config
|
|
38
|
+
|
|
39
|
+
Lives at `~/.halo-agent/config.json`. Set `HALO_API_URL` to point at a self-hosted Worker.
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
package/browser.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { chromium } = require('playwright');
|
|
4
|
+
const { execSync, spawnSync, spawn } = require('child_process');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const CDP_PORT = 9222;
|
|
8
|
+
const CDP_URL = `http://localhost:${CDP_PORT}`;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if Chrome is already running with remote debugging on CDP_PORT.
|
|
12
|
+
*/
|
|
13
|
+
async function isChromeDebuggable() {
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(`${CDP_URL}/json/version`, { signal: AbortSignal.timeout(1500) });
|
|
16
|
+
return res.ok;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Find the Chrome executable path on macOS.
|
|
24
|
+
* Checks multiple common install locations.
|
|
25
|
+
*/
|
|
26
|
+
function findChromeMac() {
|
|
27
|
+
const paths = [
|
|
28
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
29
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
30
|
+
`${process.env.HOME}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
|
|
31
|
+
];
|
|
32
|
+
for (const p of paths) {
|
|
33
|
+
if (fs.existsSync(p)) return p;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Launch Chrome with remote debugging enabled, preserving the user's real profile.
|
|
40
|
+
* Uses shell: true on macOS to correctly handle spaces in the path.
|
|
41
|
+
* navigator.webdriver is NOT set because Playwright did not launch the browser.
|
|
42
|
+
*/
|
|
43
|
+
function launchChrome() {
|
|
44
|
+
const platform = process.platform;
|
|
45
|
+
const flags = [
|
|
46
|
+
`--remote-debugging-port=${CDP_PORT}`,
|
|
47
|
+
'--profile-directory=Default',
|
|
48
|
+
'--no-first-run',
|
|
49
|
+
'--no-default-browser-check',
|
|
50
|
+
].join(' ');
|
|
51
|
+
|
|
52
|
+
if (platform === 'darwin') {
|
|
53
|
+
const chromePath = findChromeMac();
|
|
54
|
+
if (!chromePath) {
|
|
55
|
+
console.error('[halo-agent] Chrome not found. Install Google Chrome from https://www.google.com/chrome/');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Use shell:true so the path with spaces is handled correctly by the shell
|
|
59
|
+
spawn(`"${chromePath}" ${flags}`, [], {
|
|
60
|
+
shell: true,
|
|
61
|
+
detached: true,
|
|
62
|
+
stdio: 'ignore',
|
|
63
|
+
}).unref();
|
|
64
|
+
} else if (platform === 'win32') {
|
|
65
|
+
const chromePaths = [
|
|
66
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
67
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
68
|
+
`${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
69
|
+
];
|
|
70
|
+
const chromePath = chromePaths.find(p => { try { fs.accessSync(p); return true; } catch { return false; } });
|
|
71
|
+
if (!chromePath) { console.error('[halo-agent] Chrome not found on Windows.'); return; }
|
|
72
|
+
spawn(`"${chromePath}" ${flags}`, [], { shell: true, detached: true, stdio: 'ignore' }).unref();
|
|
73
|
+
} else {
|
|
74
|
+
// Linux
|
|
75
|
+
spawn(`google-chrome ${flags}`, [], { shell: true, detached: true, stdio: 'ignore' }).unref();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Connect to the user's real Chrome via CDP.
|
|
81
|
+
* Returns { browser, context, newPage }.
|
|
82
|
+
* All requests use the user's real session — cookies, IP, fingerprint, extensions.
|
|
83
|
+
*/
|
|
84
|
+
async function connectToChrome(retries = 10) {
|
|
85
|
+
for (let i = 0; i < retries; i++) {
|
|
86
|
+
const debuggable = await isChromeDebuggable();
|
|
87
|
+
if (debuggable) {
|
|
88
|
+
try {
|
|
89
|
+
const browser = await chromium.connectOverCDP(CDP_URL);
|
|
90
|
+
const context = browser.contexts()[0] || await browser.newContext();
|
|
91
|
+
console.log('[halo-agent] Connected to Chrome.');
|
|
92
|
+
|
|
93
|
+
const conn = {
|
|
94
|
+
browser,
|
|
95
|
+
context,
|
|
96
|
+
async newPage(url) {
|
|
97
|
+
// Re-fetch context in case it went stale
|
|
98
|
+
let ctx;
|
|
99
|
+
try {
|
|
100
|
+
ctx = conn.browser.contexts()[0];
|
|
101
|
+
if (!ctx) throw new Error('no context');
|
|
102
|
+
} catch {
|
|
103
|
+
// Reconnect entirely
|
|
104
|
+
console.warn('[halo-agent] Chrome context lost — reconnecting...');
|
|
105
|
+
const fresh = await chromium.connectOverCDP(CDP_URL);
|
|
106
|
+
conn.browser = fresh;
|
|
107
|
+
ctx = fresh.contexts()[0] || await fresh.newContext();
|
|
108
|
+
conn.context = ctx;
|
|
109
|
+
}
|
|
110
|
+
const page = await ctx.newPage();
|
|
111
|
+
if (url) {
|
|
112
|
+
try {
|
|
113
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
114
|
+
} catch (e) {
|
|
115
|
+
if (!e.message.includes('Timeout')) throw e;
|
|
116
|
+
// Slow page — try commit (just wait for server response, no JS wait)
|
|
117
|
+
console.warn('[halo-agent] Navigation slow — falling back to commit wait...');
|
|
118
|
+
try {
|
|
119
|
+
await page.goto(url, { waitUntil: 'commit', timeout: 60000 });
|
|
120
|
+
await page.waitForTimeout(5000); // give JS time to render
|
|
121
|
+
} catch (e2) {
|
|
122
|
+
if (!e2.message.includes('Timeout')) throw e2;
|
|
123
|
+
// Last resort — page may already be partially loaded, just wait
|
|
124
|
+
console.warn('[halo-agent] Still slow — waiting for current page state...');
|
|
125
|
+
await page.waitForTimeout(8000);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return page;
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
return conn;
|
|
133
|
+
} catch (e) {
|
|
134
|
+
console.warn(`[halo-agent] CDP connect attempt ${i + 1} failed: ${e.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (i === 0) {
|
|
139
|
+
console.log('[halo-agent] Chrome not found on port 9222. Launching...');
|
|
140
|
+
launchChrome();
|
|
141
|
+
console.log('[halo-agent] Waiting for Chrome to start...');
|
|
142
|
+
} else {
|
|
143
|
+
process.stdout.write('.');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(''); // newline after dots
|
|
150
|
+
throw new Error(
|
|
151
|
+
`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`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = { connectToChrome, isChromeDebuggable };
|
package/captcha.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CAPTCHA detection and solving via CapSolver API.
|
|
5
|
+
* API key stored locally in ~/.halo-agent/config.json.
|
|
6
|
+
*
|
|
7
|
+
* Supported: reCAPTCHA v2, hCAPTCHA, Cloudflare Turnstile
|
|
8
|
+
*
|
|
9
|
+
* Key fix: Ashby and many modern ATS load reCAPTCHA inside nested iframes.
|
|
10
|
+
* We search all frames on the page, not just document.querySelector on the top frame.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const CAPSOLVER_API = 'https://api.capsolver.com';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect if a CAPTCHA is present anywhere on the page (including nested iframes).
|
|
17
|
+
* Returns { detected, type, sitekey, pageUrl }
|
|
18
|
+
*/
|
|
19
|
+
async function detectCaptcha(page) {
|
|
20
|
+
// First try the top-level frame via evaluate
|
|
21
|
+
const topResult = await page.evaluate(() => {
|
|
22
|
+
// reCAPTCHA v2 via iframe src
|
|
23
|
+
const rcFrame = document.querySelector('iframe[src*="recaptcha/api2"], iframe[src*="google.com/recaptcha"]');
|
|
24
|
+
if (rcFrame) {
|
|
25
|
+
const match = (rcFrame.src || '').match(/[?&]k=([^&]+)/);
|
|
26
|
+
return { detected: true, type: 'recaptcha_v2', sitekey: match?.[1] || null, pageUrl: location.href };
|
|
27
|
+
}
|
|
28
|
+
// reCAPTCHA via data-sitekey
|
|
29
|
+
const rcEl = document.querySelector('.g-recaptcha[data-sitekey], [data-sitekey]:not(.h-captcha)');
|
|
30
|
+
if (rcEl) {
|
|
31
|
+
return { detected: true, type: 'recaptcha_v2', sitekey: rcEl.getAttribute('data-sitekey'), pageUrl: location.href };
|
|
32
|
+
}
|
|
33
|
+
// hCAPTCHA
|
|
34
|
+
const hcEl = document.querySelector('.h-captcha[data-sitekey]');
|
|
35
|
+
if (hcEl) {
|
|
36
|
+
return { detected: true, type: 'hcaptcha', sitekey: hcEl.getAttribute('data-sitekey'), pageUrl: location.href };
|
|
37
|
+
}
|
|
38
|
+
const hcFrame = document.querySelector('iframe[src*="hcaptcha.com"]');
|
|
39
|
+
if (hcFrame) {
|
|
40
|
+
const match = (hcFrame.src || '').match(/[?&]sitekey=([^&]+)/);
|
|
41
|
+
return { detected: true, type: 'hcaptcha', sitekey: match?.[1] || null, pageUrl: location.href };
|
|
42
|
+
}
|
|
43
|
+
// Cloudflare
|
|
44
|
+
if (document.getElementById('cf-challenge-running') || document.querySelector('.cf-browser-verification')) {
|
|
45
|
+
return { detected: true, type: 'cloudflare', sitekey: null, pageUrl: location.href };
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (topResult) return topResult;
|
|
51
|
+
|
|
52
|
+
// Search all frames for reCAPTCHA (Ashby loads it inside a sandboxed iframe)
|
|
53
|
+
const pageUrl = page.url();
|
|
54
|
+
for (const frame of page.frames()) {
|
|
55
|
+
if (frame === page.mainFrame()) continue;
|
|
56
|
+
const frameSrc = frame.url();
|
|
57
|
+
|
|
58
|
+
// If the frame itself is a reCAPTCHA anchor frame, extract sitekey from its URL
|
|
59
|
+
if (frameSrc.includes('recaptcha/api2/anchor') || frameSrc.includes('recaptcha/enterprise/anchor')) {
|
|
60
|
+
const match = frameSrc.match(/[?&]k=([^&]+)/);
|
|
61
|
+
if (match) {
|
|
62
|
+
return { detected: true, type: 'recaptcha_v2', sitekey: match[1], pageUrl };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check inside the frame's DOM
|
|
67
|
+
try {
|
|
68
|
+
const frameResult = await frame.evaluate(() => {
|
|
69
|
+
const rcEl = document.querySelector('.g-recaptcha[data-sitekey], [data-sitekey]:not(.h-captcha)');
|
|
70
|
+
if (rcEl) return { sitekey: rcEl.getAttribute('data-sitekey'), type: 'recaptcha_v2' };
|
|
71
|
+
const rcFrame = document.querySelector('iframe[src*="recaptcha"]');
|
|
72
|
+
if (rcFrame) {
|
|
73
|
+
const match = (rcFrame.src || '').match(/[?&]k=([^&]+)/);
|
|
74
|
+
return match ? { sitekey: match[1], type: 'recaptcha_v2' } : null;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}).catch(() => null);
|
|
78
|
+
|
|
79
|
+
if (frameResult?.sitekey) {
|
|
80
|
+
return { detected: true, type: frameResult.type, sitekey: frameResult.sitekey, pageUrl };
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { detected: false, type: null, sitekey: null, pageUrl };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Solve a CAPTCHA using CapSolver API.
|
|
90
|
+
* Returns the solution token string, or null if solving failed.
|
|
91
|
+
*/
|
|
92
|
+
async function solveCaptcha(captchaInfo, apiKey) {
|
|
93
|
+
if (!apiKey) return null;
|
|
94
|
+
if (captchaInfo.type === 'cloudflare') return null;
|
|
95
|
+
if (!captchaInfo.sitekey) {
|
|
96
|
+
console.warn('[captcha] No sitekey found — cannot solve automatically');
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const taskType = captchaInfo.type === 'hcaptcha'
|
|
101
|
+
? 'HCaptchaTaskProxyless'
|
|
102
|
+
: 'ReCaptchaV2TaskProxyless';
|
|
103
|
+
|
|
104
|
+
console.log(`[captcha] Submitting ${taskType} task to CapSolver (sitekey: ${captchaInfo.sitekey.slice(0, 12)}...)`);
|
|
105
|
+
|
|
106
|
+
let taskId;
|
|
107
|
+
try {
|
|
108
|
+
const createRes = await fetch(`${CAPSOLVER_API}/createTask`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
clientKey: apiKey,
|
|
113
|
+
task: {
|
|
114
|
+
type: taskType,
|
|
115
|
+
websiteURL: captchaInfo.pageUrl,
|
|
116
|
+
websiteKey: captchaInfo.sitekey,
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
const createData = await createRes.json();
|
|
121
|
+
if (createData.errorId !== 0) {
|
|
122
|
+
console.error('[captcha] CapSolver create error:', createData.errorDescription);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
taskId = createData.taskId;
|
|
126
|
+
console.log(`[captcha] Task created: ${taskId}`);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.error('[captcha] CapSolver request failed:', e.message);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Poll for result (up to 90 seconds)
|
|
133
|
+
for (let i = 0; i < 45; i++) {
|
|
134
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
135
|
+
try {
|
|
136
|
+
const resultRes = await fetch(`${CAPSOLVER_API}/getTaskResult`, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'Content-Type': 'application/json' },
|
|
139
|
+
body: JSON.stringify({ clientKey: apiKey, taskId }),
|
|
140
|
+
});
|
|
141
|
+
const resultData = await resultRes.json();
|
|
142
|
+
if (resultData.status === 'ready') {
|
|
143
|
+
const token = resultData.solution?.gRecaptchaResponse || resultData.solution?.token || null;
|
|
144
|
+
console.log(`[captcha] Solved! Token length: ${token?.length || 0}`);
|
|
145
|
+
return token;
|
|
146
|
+
}
|
|
147
|
+
if (resultData.status === 'failed') {
|
|
148
|
+
console.error('[captcha] CapSolver solve failed:', resultData.errorDescription);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
} catch {}
|
|
152
|
+
}
|
|
153
|
+
console.warn('[captcha] CapSolver timed out after 90s');
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Inject a solved CAPTCHA token into the page and trigger the framework callback.
|
|
159
|
+
* Handles reCAPTCHA v2 in main frame and nested iframes.
|
|
160
|
+
*/
|
|
161
|
+
async function injectCaptchaToken(page, token, type) {
|
|
162
|
+
// Try injecting in the main frame first
|
|
163
|
+
await page.evaluate(({ token, type }) => {
|
|
164
|
+
const injectIntoDoc = (doc) => {
|
|
165
|
+
const responseEl = doc.querySelector(
|
|
166
|
+
'[name="g-recaptcha-response"], [name="h-captcha-response"], textarea[id*="captcha"]'
|
|
167
|
+
);
|
|
168
|
+
if (responseEl) {
|
|
169
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
|
170
|
+
if (setter) setter.call(responseEl, token);
|
|
171
|
+
else responseEl.value = token;
|
|
172
|
+
responseEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
173
|
+
responseEl.dispatchEvent(new Event('change', { bubbles: true }));
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
injectIntoDoc(document);
|
|
178
|
+
|
|
179
|
+
// Trigger reCAPTCHA callback
|
|
180
|
+
try {
|
|
181
|
+
if (type === 'recaptcha_v2') {
|
|
182
|
+
if (window.___grecaptcha_cfg?.clients) {
|
|
183
|
+
for (const k of Object.keys(window.___grecaptcha_cfg.clients)) {
|
|
184
|
+
const client = window.___grecaptcha_cfg.clients[k];
|
|
185
|
+
for (const ck of Object.keys(client)) {
|
|
186
|
+
if (client[ck] && typeof client[ck].callback === 'function') {
|
|
187
|
+
client[ck].callback(token);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Also try direct grecaptcha callback
|
|
193
|
+
if (typeof window.grecaptcha?.execute === 'function') {
|
|
194
|
+
// v3 style — set token in all response textareas
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch {}
|
|
198
|
+
}, { token, type });
|
|
199
|
+
|
|
200
|
+
// Also try injecting into child frames that contain the captcha widget
|
|
201
|
+
for (const frame of page.frames()) {
|
|
202
|
+
if (frame === page.mainFrame()) continue;
|
|
203
|
+
try {
|
|
204
|
+
await frame.evaluate(({ token }) => {
|
|
205
|
+
const responseEl = document.querySelector(
|
|
206
|
+
'[name="g-recaptcha-response"], [name="h-captcha-response"]'
|
|
207
|
+
);
|
|
208
|
+
if (responseEl) {
|
|
209
|
+
responseEl.value = token;
|
|
210
|
+
responseEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
211
|
+
}
|
|
212
|
+
}, { token }).catch(() => {});
|
|
213
|
+
} catch {}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = { detectCaptcha, solveCaptcha, injectCaptchaToken };
|
package/config.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const CONFIG_DIR = path.join(os.homedir(), '.halo-agent');
|
|
9
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
10
|
+
|
|
11
|
+
function loadConfig() {
|
|
12
|
+
if (!fs.existsSync(CONFIG_FILE)) return null;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function saveConfig(config) {
|
|
21
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
22
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Build headers required for Cloudflare Access service token auth.
|
|
26
|
+
// Returns empty object if no CF Access credentials are configured.
|
|
27
|
+
function cfAccessHeaders(config) {
|
|
28
|
+
if (config && config.cfAccessClientId && config.cfAccessClientSecret) {
|
|
29
|
+
return {
|
|
30
|
+
'CF-Access-Client-Id': config.cfAccessClientId,
|
|
31
|
+
'CF-Access-Client-Secret': config.cfAccessClientSecret,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { loadConfig, saveConfig, CONFIG_DIR, cfAccessHeaders };
|