chrome-agent 0.3.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/LICENSE +21 -0
- package/README.md +339 -0
- package/bin/chrome-agent.js +71 -0
- package/package.json +34 -0
- package/scripts/postinstall.js +100 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stephane Derosiaux
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# chrome-agent
|
|
2
|
+
|
|
3
|
+
Browser automation for AI agents. Single Rust binary, zero runtime dependencies, talks CDP directly to Chrome.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Existing tools (Playwright, Puppeteer, Selenium) carry heavy runtimes and weren't designed for agents. Agents need:
|
|
8
|
+
- **Minimum tokens** — a11y tree snapshots instead of raw HTML (~50 tokens vs ~2000)
|
|
9
|
+
- **Minimum round-trips** — `--inspect` returns updated page state with every action
|
|
10
|
+
- **Zero setup** — single binary, headless by default, no npm/Node required
|
|
11
|
+
- **Persistent sessions** — login once, stay logged in across invocations
|
|
12
|
+
- **Stable UIDs** — element identifiers based on `backendNodeId`, survive between inspects
|
|
13
|
+
- **3 targeting modes** — uid from accessibility tree, CSS selectors, or coordinates
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
### For AI agents (recommended)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Install the skill — your agent learns chrome-agent automatically
|
|
21
|
+
npx skills add sderosiaux/chrome-agent
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This installs a `SKILL.md` that teaches your agent (Claude Code, Cursor, Copilot, etc.) how to use chrome-agent, including the workflow, commands, and best practices.
|
|
25
|
+
|
|
26
|
+
### CLI binary
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# npm (downloads prebuilt binary)
|
|
30
|
+
npm install -g chrome-agent
|
|
31
|
+
|
|
32
|
+
# or with npx (no install needed)
|
|
33
|
+
npx chrome-agent --help
|
|
34
|
+
|
|
35
|
+
# or with Cargo (builds from source)
|
|
36
|
+
cargo install chrome-agent
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Navigate and inspect the page in one call
|
|
43
|
+
chrome-agent goto https://example.com --inspect
|
|
44
|
+
# → https://example.com — Example Domain
|
|
45
|
+
# → uid=n1 RootWebArea "Example Domain"
|
|
46
|
+
# → uid=n9 heading "Example Domain" level=1
|
|
47
|
+
# → uid=n10 paragraph "This domain is for..."
|
|
48
|
+
# → uid=n12 link "Learn more"
|
|
49
|
+
|
|
50
|
+
# Click by uid, get updated page state
|
|
51
|
+
chrome-agent click n12 --inspect
|
|
52
|
+
|
|
53
|
+
# Fill a form field
|
|
54
|
+
chrome-agent fill --uid n20 "user@test.com"
|
|
55
|
+
|
|
56
|
+
# Or target by CSS selector (when uids aren't practical)
|
|
57
|
+
chrome-agent click --selector "button.submit"
|
|
58
|
+
chrome-agent fill --selector "input[name=email]" "hello@test.com"
|
|
59
|
+
|
|
60
|
+
# Extract article content (Mozilla Readability — reader mode)
|
|
61
|
+
chrome-agent read
|
|
62
|
+
|
|
63
|
+
# Extract full visible text (use --selector to scope, --truncate to cap)
|
|
64
|
+
chrome-agent text --selector "main" --truncate 500
|
|
65
|
+
|
|
66
|
+
# Evaluate JavaScript
|
|
67
|
+
chrome-agent eval "document.title"
|
|
68
|
+
|
|
69
|
+
# Screenshot (returns file path, not binary data)
|
|
70
|
+
chrome-agent screenshot
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## How It Works
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
chrome-agent v0.2.0 (Rust, ~5.3K lines, 2.9 MB binary)
|
|
77
|
+
│
|
|
78
|
+
│ WebSocket (Chrome DevTools Protocol)
|
|
79
|
+
▼
|
|
80
|
+
Chrome / Chromium (headless by default)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
No Node.js. No Playwright. No daemon required. Headless by default — `--headed` for debugging.
|
|
84
|
+
|
|
85
|
+
UIDs are stable across inspects (based on Chrome's `backendNodeId`). The agent inspects, picks a uid, acts — even minutes later. When a11y tree isn't practical, CSS selectors and coordinates work as fallbacks. Click auto-falls back to JS `.click()` when the element has no box model.
|
|
86
|
+
|
|
87
|
+
## Commands
|
|
88
|
+
|
|
89
|
+
| Command | Description |
|
|
90
|
+
|---------|------------|
|
|
91
|
+
| `goto <url> [--inspect] [--max-depth N]` | Navigate to URL |
|
|
92
|
+
| `inspect [--verbose] [--max-depth N] [--uid nN] [--filter "role,role"]` | Accessibility tree with stable uids |
|
|
93
|
+
| `click <uid> [--inspect] [--max-depth N]` | Click by uid (JS fallback if no box model) |
|
|
94
|
+
| `click --selector "css" [--inspect]` | Click by CSS selector |
|
|
95
|
+
| `click --xy 100,200` | Click by coordinates |
|
|
96
|
+
| `fill --uid <uid> <value> [--inspect]` | Fill input by uid |
|
|
97
|
+
| `fill --selector "css" <value>` | Fill by CSS selector |
|
|
98
|
+
| `fill-form <uid=val>...` | Batch fill multiple fields |
|
|
99
|
+
| `read [--html] [--truncate N]` | Extract main content (Mozilla Readability) |
|
|
100
|
+
| `text [uid] [--selector "css"] [--truncate N]` | Extract visible text (page or element) |
|
|
101
|
+
| `eval <expression> [--selector "css"]` | Run JS in page context (`el` = matched element) |
|
|
102
|
+
| `network [--filter "pattern"] [--body] [--live N]` | Capture network requests / API responses |
|
|
103
|
+
| `console [--level error] [--clear]` | Show captured console.log/warn/error + JS exceptions |
|
|
104
|
+
| `pipe` | Persistent connection: JSON stdin → JSON stdout |
|
|
105
|
+
| `wait <text\|url\|selector> <pattern>` | Wait for condition |
|
|
106
|
+
| `type <text> [--selector "css"]` | Type into focused/selected element |
|
|
107
|
+
| `press <key>` | Press Enter, Tab, Escape, etc. |
|
|
108
|
+
| `scroll <down\|up\|uid>` | Scroll page or element into view |
|
|
109
|
+
| `hover <uid>` | Hover over element |
|
|
110
|
+
| `back` | Navigate back in history |
|
|
111
|
+
| `screenshot [--filename name]` | Capture screenshot → file path |
|
|
112
|
+
| `tabs` | List open browser tabs |
|
|
113
|
+
| `close [--purge]` | Close browser (--purge deletes profile/cookies) |
|
|
114
|
+
| `status` | Show session info |
|
|
115
|
+
| `stop` | Stop background daemon |
|
|
116
|
+
|
|
117
|
+
## Global Flags
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
--browser <name> Named browser profile (default: "default")
|
|
121
|
+
--page <name> Named page/tab (default: "default")
|
|
122
|
+
--connect [url] Connect to running Chrome (auto or explicit)
|
|
123
|
+
--headed Show browser window (default is headless)
|
|
124
|
+
--stealth Bypass bot detection (Cloudflare, Turnstile)
|
|
125
|
+
--timeout <seconds> Command timeout (default: 30)
|
|
126
|
+
--max-depth <N> Limit inspect tree depth (works with --inspect on any command)
|
|
127
|
+
--ignore-https-errors Accept self-signed certificates
|
|
128
|
+
--json Structured JSON output for all commands
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## The Inspect → Act → Inspect Loop
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# 1. Navigate and inspect
|
|
135
|
+
chrome-agent goto https://app.com/login --inspect
|
|
136
|
+
# → uid=n47 heading "Login" level=1
|
|
137
|
+
# uid=n52 textbox "Email" focusable
|
|
138
|
+
# uid=n58 textbox "Password" focusable
|
|
139
|
+
# uid=n63 button "Sign In" focusable
|
|
140
|
+
|
|
141
|
+
# 2. Act
|
|
142
|
+
chrome-agent fill --uid n52 "user@test.com"
|
|
143
|
+
chrome-agent fill --uid n58 "password123"
|
|
144
|
+
|
|
145
|
+
# 3. Click with --inspect to get result + new state in one call
|
|
146
|
+
chrome-agent click n63 --inspect
|
|
147
|
+
# → Clicked uid=n63
|
|
148
|
+
# → uid=n101 heading "Dashboard" level=1
|
|
149
|
+
# → uid=n105 navigation "Main menu"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
UIDs (n47, n52, etc.) are stable — they won't change between inspects as long as the DOM node exists.
|
|
153
|
+
|
|
154
|
+
## Network Capture
|
|
155
|
+
|
|
156
|
+
Extract API data directly instead of DOM scraping:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# Show resources loaded by the page (stealth-safe, uses Performance API)
|
|
160
|
+
chrome-agent network --filter "api"
|
|
161
|
+
|
|
162
|
+
# Capture live traffic with response bodies (5 seconds)
|
|
163
|
+
chrome-agent network --live 5 --body --filter "graphql"
|
|
164
|
+
|
|
165
|
+
# JSON output for structured extraction
|
|
166
|
+
chrome-agent --json network --body --filter "api" --limit 10
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Console Capture
|
|
170
|
+
|
|
171
|
+
See what the page logs — useful for debugging and error detection:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
chrome-agent console # all messages
|
|
175
|
+
chrome-agent console --level error # errors + exceptions only
|
|
176
|
+
chrome-agent console --clear # read and clear buffer
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Stealth-safe: uses injected interceptor, not `Runtime.enable`.
|
|
180
|
+
|
|
181
|
+
## Pipe Mode
|
|
182
|
+
|
|
183
|
+
Persistent connection for high-performance agent workflows:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Start pipe (one connection, reads JSON from stdin)
|
|
187
|
+
echo '{"cmd":"goto","url":"https://example.com","inspect":true}
|
|
188
|
+
{"cmd":"click","uid":"n12","inspect":true}
|
|
189
|
+
{"cmd":"read"}' | chrome-agent pipe
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Each command returns one JSON line: `{"ok":true,...}` or `{"ok":false,"error":"..."}`. 10x faster than spawning chrome-agent per command.
|
|
193
|
+
|
|
194
|
+
## Content Extraction
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Article content (Readability — like Firefox Reader Mode)
|
|
198
|
+
chrome-agent read
|
|
199
|
+
# → # Article Title
|
|
200
|
+
# → Clean article text without nav, footer, sidebar...
|
|
201
|
+
|
|
202
|
+
# Full page text (scoped by selector)
|
|
203
|
+
chrome-agent text --selector "[role=main]" --truncate 1000
|
|
204
|
+
|
|
205
|
+
# Structured data via JS
|
|
206
|
+
chrome-agent eval "JSON.stringify([...document.querySelectorAll('h2')].map(e => e.textContent))"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Stealth Mode
|
|
210
|
+
|
|
211
|
+
Many sites (Cloudflare, Turnstile) block headless Chrome. `--stealth` patches 7 automation fingerprints via CDP:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
chrome-agent --stealth goto https://protected-site.com --inspect
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
What it patches:
|
|
218
|
+
- `navigator.webdriver` → `undefined`
|
|
219
|
+
- `chrome.runtime` → mocked (headless doesn't have it)
|
|
220
|
+
- Permissions API → consistent with real browser
|
|
221
|
+
- WebGL renderer → masks ANGLE/headless fingerprint
|
|
222
|
+
- User-Agent → removes "HeadlessChrome"
|
|
223
|
+
- Input `screenX`/`pageX` leak → random offset added
|
|
224
|
+
- `Runtime.enable` → skipped (the #1 CDP detection vector)
|
|
225
|
+
|
|
226
|
+
All patches are CDP-level (`Page.addScriptToEvaluateOnNewDocument`). No fake Chrome flags.
|
|
227
|
+
|
|
228
|
+
### Heavy bot protection (DataDome, Kasada)
|
|
229
|
+
|
|
230
|
+
Some sites (Leboncoin, etc.) use advanced fingerprinting that detects bundled Chromium regardless of CDP patches. For these, connect to your real installed Chrome instead:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
# Launch your real Chrome with debugging enabled
|
|
234
|
+
google-chrome --remote-debugging-port=9222 &
|
|
235
|
+
|
|
236
|
+
# Connect chrome-agent to it
|
|
237
|
+
chrome-agent --connect http://127.0.0.1:9222 goto https://www.leboncoin.fr --inspect
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Real Chrome has genuine canvas/audio/codec fingerprints that Chromium lacks.
|
|
241
|
+
|
|
242
|
+
| Protection Level | Solution |
|
|
243
|
+
|---|---|
|
|
244
|
+
| None | `chrome-agent goto ...` |
|
|
245
|
+
| Cloudflare/Turnstile | `chrome-agent --stealth goto ...` |
|
|
246
|
+
| DataDome/Kasada | `chrome-agent --connect` to real Chrome |
|
|
247
|
+
|
|
248
|
+
## JSON Mode
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
chrome-agent --json goto https://example.com --inspect
|
|
252
|
+
# → {"ok":true,"url":"...","title":"...","snapshot":"uid=n1 heading..."}
|
|
253
|
+
|
|
254
|
+
chrome-agent --json eval "1+1"
|
|
255
|
+
# → {"ok":true,"result":2}
|
|
256
|
+
|
|
257
|
+
chrome-agent --json read
|
|
258
|
+
# → {"ok":true,"title":"...","text":"...","excerpt":"...","byline":"..."}
|
|
259
|
+
|
|
260
|
+
# Errors also structured (exit 0 for agent parsing):
|
|
261
|
+
chrome-agent --json click n99
|
|
262
|
+
# → {"ok":false,"error":"Element uid=n99 not found.","hint":"Run 'chrome-agent inspect'"}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Multi-Tab
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
chrome-agent --page main goto https://app.com
|
|
269
|
+
chrome-agent --page docs goto https://docs.app.com
|
|
270
|
+
chrome-agent --page main eval "document.title" # → "App"
|
|
271
|
+
chrome-agent --page docs eval "document.title" # → "Docs"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Parallel Agents
|
|
275
|
+
|
|
276
|
+
Multiple agents sharing the same browser corrupt each other's sessions. Isolate with `--browser`:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# Agent 1
|
|
280
|
+
chrome-agent --browser agent1 goto https://example.com
|
|
281
|
+
|
|
282
|
+
# Agent 2 (separate Chrome instance)
|
|
283
|
+
chrome-agent --browser agent2 goto https://other.com
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Using with AI Agents
|
|
287
|
+
|
|
288
|
+
### Skill (recommended)
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
npx skills add sderosiaux/chrome-agent
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
This installs a SKILL.md that teaches your agent the full chrome-agent workflow, commands, and tips. Works with Claude Code, Cursor, Copilot, and any agent that reads skill files.
|
|
295
|
+
|
|
296
|
+
### Manual
|
|
297
|
+
|
|
298
|
+
Tell your agent to run `chrome-agent --help` — the help output includes a complete LLM usage guide.
|
|
299
|
+
|
|
300
|
+
### Claude Code permissions
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"permissions": {
|
|
305
|
+
"allow": ["Bash(chrome-agent *)"]
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Connect to Your Browser
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
chrome-agent --connect inspect # auto-discover Chrome with debugging
|
|
314
|
+
google-chrome --remote-debugging-port=9222 # or launch manually
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Comparison
|
|
318
|
+
|
|
319
|
+
| | chrome-agent | dev-browser | chrome-devtools-mcp | Playwright MCP |
|
|
320
|
+
|---|---|---|---|---|
|
|
321
|
+
| Language | Rust | Rust + Node.js | TypeScript | TypeScript |
|
|
322
|
+
| Runtime deps | none | Node.js + npm + Playwright + QuickJS | Node.js + Puppeteer | Node.js + Playwright |
|
|
323
|
+
| Binary size | ~3 MB | ~3 MB (CLI) + ~200 MB (daemon + deps) | npm package | npm package |
|
|
324
|
+
| CLI startup (reuse session) | ~10ms | ~500ms (daemon check) | N/A (MCP server) | N/A (MCP server) |
|
|
325
|
+
| Element targeting | uid + CSS selector + coordinates | CSS selectors + snapshotForAI | uid (sequential) | CSS selectors |
|
|
326
|
+
| UID stability | backendNodeId (stable across inspects) | N/A | sequential (reassigned each snapshot) | N/A |
|
|
327
|
+
| Action + observe | `--inspect` flag (1 call) | 1 script (batched) | 1 MCP call per action | 1 MCP call per action |
|
|
328
|
+
| Script batching | No (atomic commands + eval) | Full JS scripts in QuickJS sandbox | No | No |
|
|
329
|
+
| Stealth mode | 7 CDP patches + Runtime.enable skip | No | No | No |
|
|
330
|
+
| Reader mode | `read` (Mozilla Readability) | No | No | No |
|
|
331
|
+
| Sandbox | Chrome sandbox | QuickJS WASM sandbox | Chrome sandbox | No |
|
|
332
|
+
| Network capture | Retroactive + live | No | No | Metadata only (no bodies) |
|
|
333
|
+
| Console capture | Stealth-safe interceptor | No | Console messages | No |
|
|
334
|
+
| Pipe mode | JSON stdin/stdout | No | No | No |
|
|
335
|
+
| Code | ~5.3K lines | ~76K lines (69K Playwright fork) | ~12K lines | Playwright |
|
|
336
|
+
|
|
337
|
+
## License
|
|
338
|
+
|
|
339
|
+
MIT
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { accessSync, chmodSync, constants, existsSync, readFileSync } from 'fs';
|
|
5
|
+
import { arch, platform } from 'os';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
11
|
+
const version = packageJson.version;
|
|
12
|
+
const repoSlug = 'sderosiaux/chrome-agent';
|
|
13
|
+
|
|
14
|
+
const supportedTargets = Object.freeze({
|
|
15
|
+
'darwin-arm64': 'chrome-agent-darwin-arm64',
|
|
16
|
+
'darwin-x64': 'chrome-agent-darwin-x64',
|
|
17
|
+
'linux-arm64': 'chrome-agent-linux-arm64',
|
|
18
|
+
'linux-x64': 'chrome-agent-linux-x64',
|
|
19
|
+
'win32-x64': 'chrome-agent-windows-x64.exe',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function getTargetKey() {
|
|
23
|
+
const p = platform();
|
|
24
|
+
const a = arch();
|
|
25
|
+
if (p === 'darwin') return a === 'arm64' ? 'darwin-arm64' : a === 'x64' ? 'darwin-x64' : null;
|
|
26
|
+
if (p === 'linux') return a === 'x64' ? 'linux-x64' : a === 'arm64' ? 'linux-arm64' : null;
|
|
27
|
+
if (p === 'win32') return a === 'x64' ? 'win32-x64' : null;
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function main() {
|
|
32
|
+
const targetKey = getTargetKey();
|
|
33
|
+
const binaryName = targetKey ? supportedTargets[targetKey] : null;
|
|
34
|
+
|
|
35
|
+
if (!binaryName) {
|
|
36
|
+
console.error(`Error: Unsupported platform: ${platform()}-${arch()}`);
|
|
37
|
+
console.error(`Supported: ${Object.keys(supportedTargets).join(', ')}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const binaryPath = join(__dirname, binaryName);
|
|
42
|
+
|
|
43
|
+
if (!existsSync(binaryPath)) {
|
|
44
|
+
const url = `https://github.com/${repoSlug}/releases/download/v${version}/${binaryName}`;
|
|
45
|
+
console.error(`Error: Native binary not found at ${binaryPath}`);
|
|
46
|
+
console.error('The postinstall step downloads it from GitHub releases.');
|
|
47
|
+
console.error(`Reinstall the package, or download manually from: ${url}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (platform() !== 'win32') {
|
|
52
|
+
try { accessSync(binaryPath, constants.X_OK); } catch { chmodSync(binaryPath, 0o755); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
56
|
+
stdio: 'inherit',
|
|
57
|
+
windowsHide: false,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
child.on('error', (error) => {
|
|
61
|
+
console.error(`Error: ${error.message}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
child.on('exit', (code, signal) => {
|
|
66
|
+
if (signal) { process.kill(process.pid, signal); return; }
|
|
67
|
+
process.exit(code ?? 1);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chrome-agent",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Browser automation for AI agents. Single binary, zero dependencies, CDP direct.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"chrome-agent": "./bin/chrome-agent.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node scripts/postinstall.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"scripts/",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"browser",
|
|
20
|
+
"automation",
|
|
21
|
+
"ai",
|
|
22
|
+
"agent",
|
|
23
|
+
"cdp",
|
|
24
|
+
"chrome",
|
|
25
|
+
"headless",
|
|
26
|
+
"cli"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/sderosiaux/chrome-agent.git"
|
|
32
|
+
},
|
|
33
|
+
"author": "Stephane Derosiaux"
|
|
34
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync } from 'fs';
|
|
4
|
+
import { get } from 'https';
|
|
5
|
+
import { arch, platform } from 'os';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const projectRoot = join(__dirname, '..');
|
|
11
|
+
const binDir = join(projectRoot, 'bin');
|
|
12
|
+
const packageJson = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
|
|
13
|
+
const version = packageJson.version;
|
|
14
|
+
const repoSlug = 'sderosiaux/chrome-agent';
|
|
15
|
+
|
|
16
|
+
const supportedTargets = Object.freeze({
|
|
17
|
+
'darwin-arm64': 'chrome-agent-darwin-arm64',
|
|
18
|
+
'darwin-x64': 'chrome-agent-darwin-x64',
|
|
19
|
+
'linux-arm64': 'chrome-agent-linux-arm64',
|
|
20
|
+
'linux-x64': 'chrome-agent-linux-x64',
|
|
21
|
+
'win32-x64': 'chrome-agent-windows-x64.exe',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function getTargetKey() {
|
|
25
|
+
const p = platform();
|
|
26
|
+
const a = arch();
|
|
27
|
+
if (p === 'darwin') return a === 'arm64' ? 'darwin-arm64' : a === 'x64' ? 'darwin-x64' : null;
|
|
28
|
+
if (p === 'linux') return a === 'x64' ? 'linux-x64' : a === 'arm64' ? 'linux-arm64' : null;
|
|
29
|
+
if (p === 'win32') return a === 'x64' ? 'win32-x64' : null;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function downloadFile(url, destination) {
|
|
34
|
+
const tempPath = `${destination}.download`;
|
|
35
|
+
rmSync(tempPath, { force: true });
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const request = (currentUrl, redirects = 10) => {
|
|
39
|
+
get(currentUrl, {
|
|
40
|
+
headers: { Accept: 'application/octet-stream', 'User-Agent': `chrome-agent/${version}` },
|
|
41
|
+
}, (response) => {
|
|
42
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
43
|
+
response.resume();
|
|
44
|
+
if (redirects === 0) { reject(new Error('Too many redirects')); return; }
|
|
45
|
+
request(new URL(response.headers.location, currentUrl), redirects - 1);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (response.statusCode !== 200) {
|
|
49
|
+
response.resume();
|
|
50
|
+
reject(new Error(`HTTP ${response.statusCode} from ${currentUrl}`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const file = createWriteStream(tempPath);
|
|
54
|
+
response.pipe(file);
|
|
55
|
+
file.on('finish', () => file.close(() => {
|
|
56
|
+
try { renameSync(tempPath, destination); resolve(); }
|
|
57
|
+
catch (e) { reject(e); }
|
|
58
|
+
}));
|
|
59
|
+
file.on('error', reject);
|
|
60
|
+
response.on('error', reject);
|
|
61
|
+
}).on('error', reject).setTimeout(30_000, function() { this.destroy(new Error('Timeout')); });
|
|
62
|
+
};
|
|
63
|
+
request(url);
|
|
64
|
+
}).catch((error) => { rmSync(tempPath, { force: true }); throw error; });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function main() {
|
|
68
|
+
const targetKey = getTargetKey();
|
|
69
|
+
const binaryName = targetKey ? supportedTargets[targetKey] : null;
|
|
70
|
+
|
|
71
|
+
if (!binaryName) {
|
|
72
|
+
// Not a fatal error during local dev
|
|
73
|
+
if (existsSync(join(projectRoot, '.git'))) {
|
|
74
|
+
console.warn(`Warning: No prebuilt binary for ${platform()}-${arch()}. Build from source with: cargo build --release`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Unsupported platform: ${platform()}-${arch()}. Supported: ${Object.keys(supportedTargets).join(', ')}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
mkdirSync(binDir, { recursive: true });
|
|
81
|
+
const binaryPath = join(binDir, binaryName);
|
|
82
|
+
|
|
83
|
+
if (existsSync(binaryPath)) {
|
|
84
|
+
if (platform() !== 'win32') chmodSync(binaryPath, 0o755);
|
|
85
|
+
console.log(`chrome-agent: native binary already present (${binaryName})`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const url = `https://github.com/${repoSlug}/releases/download/v${version}/${binaryName}`;
|
|
90
|
+
console.log(`chrome-agent: downloading native binary for ${platform()}-${arch()}...`);
|
|
91
|
+
|
|
92
|
+
await downloadFile(url, binaryPath);
|
|
93
|
+
if (platform() !== 'win32') chmodSync(binaryPath, 0o755);
|
|
94
|
+
console.log(`chrome-agent: installed ${binaryName}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
main().catch((error) => {
|
|
98
|
+
console.error(`chrome-agent postinstall failed: ${error.message}`);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
});
|