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 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
+ });