agent-browser 0.9.4 → 0.11.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 +137 -11
- package/bin/agent-browser-darwin-arm64 +0 -0
- package/bin/agent-browser-darwin-x64 +0 -0
- package/bin/agent-browser-linux-arm64 +0 -0
- package/bin/agent-browser-linux-x64 +0 -0
- package/bin/agent-browser-win32-x64.exe +0 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +191 -4
- package/dist/actions.js.map +1 -1
- package/dist/browser.d.ts +36 -3
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +230 -8
- package/dist/browser.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +120 -2
- package/dist/daemon.js.map +1 -1
- package/dist/encryption.d.ts +50 -0
- package/dist/encryption.d.ts.map +1 -0
- package/dist/encryption.js +85 -0
- package/dist/encryption.js.map +1 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +56 -2
- package/dist/protocol.js.map +1 -1
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/snapshot.js +18 -5
- package/dist/snapshot.js.map +1 -1
- package/dist/state-utils.d.ts +77 -0
- package/dist/state-utils.d.ts.map +1 -0
- package/dist/state-utils.js +178 -0
- package/dist/state-utils.js.map +1 -0
- package/dist/stream-server.d.ts.map +1 -1
- package/dist/stream-server.js +4 -0
- package/dist/stream-server.js.map +1 -1
- package/dist/types.d.ts +51 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/agent-browser/SKILL.md +87 -1
- package/skills/agent-browser/references/commands.md +4 -0
- package/skills/agent-browser/references/profiling.md +120 -0
- package/skills/agent-browser/references/proxy-support.md +10 -4
- package/skills/agent-browser/references/snapshot-refs.md +2 -2
- package/skills/agent-browser/templates/authenticated-session.sh +12 -9
package/README.md
CHANGED
|
@@ -4,13 +4,43 @@ Headless browser automation CLI for AI agents. Fast Rust CLI with Node.js fallba
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### Global Installation (recommended)
|
|
8
|
+
|
|
9
|
+
Installs the native Rust binary for maximum performance:
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
12
|
npm install -g agent-browser
|
|
11
13
|
agent-browser install # Download Chromium
|
|
12
14
|
```
|
|
13
15
|
|
|
16
|
+
This is the fastest option -- commands run through the native Rust CLI directly with sub-millisecond parsing overhead.
|
|
17
|
+
|
|
18
|
+
### Quick Start (no install)
|
|
19
|
+
|
|
20
|
+
Run directly with `npx` if you want to try it without installing globally:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx agent-browser install # Download Chromium (first time only)
|
|
24
|
+
npx agent-browser open example.com
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> **Note:** `npx` routes through Node.js before reaching the Rust CLI, so it is noticeably slower than a global install. For regular use, install globally.
|
|
28
|
+
|
|
29
|
+
### Project Installation (local dependency)
|
|
30
|
+
|
|
31
|
+
For projects that want to pin the version in `package.json`:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install agent-browser
|
|
35
|
+
npx agent-browser install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then use via `npx` or `package.json` scripts:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx agent-browser open example.com
|
|
42
|
+
```
|
|
43
|
+
|
|
14
44
|
### Homebrew (macOS)
|
|
15
45
|
|
|
16
46
|
```bash
|
|
@@ -65,7 +95,7 @@ agent-browser find role button click --name "Submit"
|
|
|
65
95
|
|
|
66
96
|
```bash
|
|
67
97
|
agent-browser open <url> # Navigate to URL (aliases: goto, navigate)
|
|
68
|
-
agent-browser click <sel> # Click element
|
|
98
|
+
agent-browser click <sel> # Click element (--new-tab to open in new tab)
|
|
69
99
|
agent-browser dblclick <sel> # Double-click element
|
|
70
100
|
agent-browser focus <sel> # Focus element
|
|
71
101
|
agent-browser type <sel> <text> # Type into element
|
|
@@ -100,6 +130,7 @@ agent-browser get title # Get page title
|
|
|
100
130
|
agent-browser get url # Get current URL
|
|
101
131
|
agent-browser get count <sel> # Count matching elements
|
|
102
132
|
agent-browser get box <sel> # Get bounding box
|
|
133
|
+
agent-browser get styles <sel> # Get computed styles
|
|
103
134
|
```
|
|
104
135
|
|
|
105
136
|
### Check State
|
|
@@ -125,7 +156,9 @@ agent-browser find last <sel> <action> [value] # Last match
|
|
|
125
156
|
agent-browser find nth <n> <sel> <action> [value] # Nth match
|
|
126
157
|
```
|
|
127
158
|
|
|
128
|
-
**Actions:** `click`, `fill`, `
|
|
159
|
+
**Actions:** `click`, `fill`, `type`, `hover`, `focus`, `check`, `uncheck`, `text`
|
|
160
|
+
|
|
161
|
+
**Options:** `--name <name>` (filter role by accessible name), `--exact` (require exact text match)
|
|
129
162
|
|
|
130
163
|
**Examples:**
|
|
131
164
|
```bash
|
|
@@ -225,6 +258,8 @@ agent-browser dialog dismiss # Dismiss
|
|
|
225
258
|
```bash
|
|
226
259
|
agent-browser trace start [path] # Start recording trace
|
|
227
260
|
agent-browser trace stop [path] # Stop and save trace
|
|
261
|
+
agent-browser profiler start # Start Chrome DevTools profiling
|
|
262
|
+
agent-browser profiler stop [path] # Stop and save profile (.json)
|
|
228
263
|
agent-browser console # View console messages (log, error, warn, info)
|
|
229
264
|
agent-browser console --clear # Clear console
|
|
230
265
|
agent-browser errors # View page errors (uncaught JavaScript exceptions)
|
|
@@ -232,6 +267,12 @@ agent-browser errors --clear # Clear errors
|
|
|
232
267
|
agent-browser highlight <sel> # Highlight element
|
|
233
268
|
agent-browser state save <path> # Save auth state
|
|
234
269
|
agent-browser state load <path> # Load auth state
|
|
270
|
+
agent-browser state list # List saved state files
|
|
271
|
+
agent-browser state show <file> # Show state summary
|
|
272
|
+
agent-browser state rename <old> <new> # Rename state file
|
|
273
|
+
agent-browser state clear [name] # Clear states for session
|
|
274
|
+
agent-browser state clear --all # Clear all saved states
|
|
275
|
+
agent-browser state clean --older-than <days> # Delete old states
|
|
235
276
|
```
|
|
236
277
|
|
|
237
278
|
### Navigation
|
|
@@ -302,6 +343,40 @@ The profile directory stores:
|
|
|
302
343
|
|
|
303
344
|
**Tip**: Use different profile paths for different projects to keep their browser state isolated.
|
|
304
345
|
|
|
346
|
+
## Session Persistence
|
|
347
|
+
|
|
348
|
+
Alternatively, use `--session-name` to automatically save and restore cookies and localStorage across browser restarts:
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
# Auto-save/load state for "twitter" session
|
|
352
|
+
agent-browser --session-name twitter open twitter.com
|
|
353
|
+
|
|
354
|
+
# Login once, then state persists automatically
|
|
355
|
+
# State files stored in ~/.agent-browser/sessions/
|
|
356
|
+
|
|
357
|
+
# Or via environment variable
|
|
358
|
+
export AGENT_BROWSER_SESSION_NAME=twitter
|
|
359
|
+
agent-browser open twitter.com
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### State Encryption
|
|
363
|
+
|
|
364
|
+
Encrypt saved session data at rest with AES-256-GCM:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# Generate key: openssl rand -hex 32
|
|
368
|
+
export AGENT_BROWSER_ENCRYPTION_KEY=<64-char-hex-key>
|
|
369
|
+
|
|
370
|
+
# State files are now encrypted automatically
|
|
371
|
+
agent-browser --session-name secure open example.com
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
| Variable | Description |
|
|
375
|
+
|----------|-------------|
|
|
376
|
+
| `AGENT_BROWSER_SESSION_NAME` | Auto-save/load state persistence name |
|
|
377
|
+
| `AGENT_BROWSER_ENCRYPTION_KEY` | 64-char hex key for AES-256-GCM encryption |
|
|
378
|
+
| `AGENT_BROWSER_STATE_EXPIRE_DAYS` | Auto-delete states older than N days (default: 30) |
|
|
379
|
+
|
|
305
380
|
## Snapshot Options
|
|
306
381
|
|
|
307
382
|
The `snapshot` command supports filtering to reduce output size:
|
|
@@ -331,25 +406,66 @@ The `-C` flag is useful for modern web apps that use custom clickable elements (
|
|
|
331
406
|
| Option | Description |
|
|
332
407
|
|--------|-------------|
|
|
333
408
|
| `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) |
|
|
409
|
+
| `--session-name <name>` | Auto-save/restore session state (or `AGENT_BROWSER_SESSION_NAME` env) |
|
|
334
410
|
| `--profile <path>` | Persistent browser profile directory (or `AGENT_BROWSER_PROFILE` env) |
|
|
411
|
+
| `--state <path>` | Load storage state from JSON file (or `AGENT_BROWSER_STATE` env) |
|
|
335
412
|
| `--headers <json>` | Set HTTP headers scoped to the URL's origin |
|
|
336
413
|
| `--executable-path <path>` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) |
|
|
414
|
+
| `--extension <path>` | Load browser extension (repeatable; or `AGENT_BROWSER_EXTENSIONS` env) |
|
|
337
415
|
| `--args <args>` | Browser launch args, comma or newline separated (or `AGENT_BROWSER_ARGS` env) |
|
|
338
416
|
| `--user-agent <ua>` | Custom User-Agent string (or `AGENT_BROWSER_USER_AGENT` env) |
|
|
339
417
|
| `--proxy <url>` | Proxy server URL with optional auth (or `AGENT_BROWSER_PROXY` env) |
|
|
340
418
|
| `--proxy-bypass <hosts>` | Hosts to bypass proxy (or `AGENT_BROWSER_PROXY_BYPASS` env) |
|
|
419
|
+
| `--ignore-https-errors` | Ignore HTTPS certificate errors (useful for self-signed certs) |
|
|
420
|
+
| `--allow-file-access` | Allow file:// URLs to access local files (Chromium only) |
|
|
341
421
|
| `-p, --provider <name>` | Cloud browser provider (or `AGENT_BROWSER_PROVIDER` env) |
|
|
422
|
+
| `--device <name>` | iOS device name, e.g. "iPhone 15 Pro" (or `AGENT_BROWSER_IOS_DEVICE` env) |
|
|
342
423
|
| `--json` | JSON output (for agents) |
|
|
343
424
|
| `--full, -f` | Full page screenshot |
|
|
344
|
-
| `--name, -n` | Locator name filter |
|
|
345
|
-
| `--exact` | Exact text match |
|
|
346
425
|
| `--headed` | Show browser window (not headless) |
|
|
347
|
-
| `--cdp <port>` | Connect via Chrome DevTools Protocol |
|
|
426
|
+
| `--cdp <port\|url>` | Connect via Chrome DevTools Protocol (port or WebSocket URL) |
|
|
348
427
|
| `--auto-connect` | Auto-discover and connect to running Chrome (or `AGENT_BROWSER_AUTO_CONNECT` env) |
|
|
349
|
-
| `--
|
|
350
|
-
| `--allow-file-access` | Allow file:// URLs to access local files (Chromium only) |
|
|
428
|
+
| `--config <path>` | Use a custom config file (or `AGENT_BROWSER_CONFIG` env) |
|
|
351
429
|
| `--debug` | Debug output |
|
|
352
430
|
|
|
431
|
+
## Configuration
|
|
432
|
+
|
|
433
|
+
Create an `agent-browser.json` file to set persistent defaults instead of repeating flags on every command.
|
|
434
|
+
|
|
435
|
+
**Locations (lowest to highest priority):**
|
|
436
|
+
|
|
437
|
+
1. `~/.agent-browser/config.json` -- user-level defaults
|
|
438
|
+
2. `./agent-browser.json` -- project-level overrides (in working directory)
|
|
439
|
+
3. `AGENT_BROWSER_*` environment variables override config file values
|
|
440
|
+
4. CLI flags override everything
|
|
441
|
+
|
|
442
|
+
**Example `agent-browser.json`:**
|
|
443
|
+
|
|
444
|
+
```json
|
|
445
|
+
{
|
|
446
|
+
"headed": true,
|
|
447
|
+
"proxy": "http://localhost:8080",
|
|
448
|
+
"profile": "./browser-data",
|
|
449
|
+
"userAgent": "my-agent/1.0",
|
|
450
|
+
"ignoreHttpsErrors": true
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Use `--config <path>` or `AGENT_BROWSER_CONFIG` to load a specific config file instead of the defaults:
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
agent-browser --config ./ci-config.json open example.com
|
|
458
|
+
AGENT_BROWSER_CONFIG=./ci-config.json agent-browser open example.com
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
All options from the table above can be set in the config file using camelCase keys (e.g., `--executable-path` becomes `"executablePath"`, `--proxy-bypass` becomes `"proxyBypass"`). Unknown keys are ignored for forward compatibility.
|
|
462
|
+
|
|
463
|
+
Boolean flags accept an optional `true`/`false` value to override config settings. For example, `--headed false` disables `"headed": true` from config. A bare `--headed` is equivalent to `--headed true`.
|
|
464
|
+
|
|
465
|
+
Auto-discovered config files that are missing are silently ignored. If `--config <path>` points to a missing or invalid file, agent-browser exits with an error. Extensions from user and project configs are merged (concatenated), not replaced.
|
|
466
|
+
|
|
467
|
+
> **Tip:** If your project-level `agent-browser.json` contains environment-specific values (paths, proxies), consider adding it to `.gitignore`.
|
|
468
|
+
|
|
353
469
|
## Selectors
|
|
354
470
|
|
|
355
471
|
### Refs (Recommended for AI)
|
|
@@ -711,7 +827,7 @@ The daemon starts automatically on first command and persists between commands f
|
|
|
711
827
|
|
|
712
828
|
### Just ask the agent
|
|
713
829
|
|
|
714
|
-
The simplest approach
|
|
830
|
+
The simplest approach -- just tell your agent to use it:
|
|
715
831
|
|
|
716
832
|
```
|
|
717
833
|
Use agent-browser to test the login flow. Run agent-browser --help to see available commands.
|
|
@@ -719,7 +835,7 @@ Use agent-browser to test the login flow. Run agent-browser --help to see availa
|
|
|
719
835
|
|
|
720
836
|
The `--help` output is comprehensive and most agents can figure it out from there.
|
|
721
837
|
|
|
722
|
-
### AI Coding Assistants
|
|
838
|
+
### AI Coding Assistants (recommended)
|
|
723
839
|
|
|
724
840
|
Add the skill to your AI coding assistant for richer context:
|
|
725
841
|
|
|
@@ -727,7 +843,17 @@ Add the skill to your AI coding assistant for richer context:
|
|
|
727
843
|
npx skills add vercel-labs/agent-browser
|
|
728
844
|
```
|
|
729
845
|
|
|
730
|
-
This works with Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Goose, OpenCode, and Windsurf.
|
|
846
|
+
This works with Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Goose, OpenCode, and Windsurf. The skill is fetched from the repository, so it stays up to date automatically -- do not copy `SKILL.md` from `node_modules` as it will become stale.
|
|
847
|
+
|
|
848
|
+
### Claude Code
|
|
849
|
+
|
|
850
|
+
Install as a Claude Code skill:
|
|
851
|
+
|
|
852
|
+
```bash
|
|
853
|
+
npx skills add vercel-labs/agent-browser
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
This adds the skill to `.claude/skills/agent-browser/SKILL.md` in your project. The skill teaches Claude Code the full agent-browser workflow, including the snapshot-ref interaction pattern, session management, and timeout handling.
|
|
731
857
|
|
|
732
858
|
### AGENTS.md / CLAUDE.md
|
|
733
859
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/actions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAUpE,OAAO,KAAK,EACV,OAAO,EACP,QAAQ,EA4HT,MAAM,YAAY,CAAC;AAMpB;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC,GAAG,IAAI,GAClD,IAAI,CAEN;AAQD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAqDzE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CAiRjG"}
|
package/dist/actions.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
1
3
|
import { mkdirSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
4
|
import { getAppDir } from './daemon.js';
|
|
5
|
+
import { getSessionsDir, readStateFile, isValidSessionName, isEncryptedPayload, listStateFiles, cleanupExpiredStates, } from './state-utils.js';
|
|
4
6
|
import { successResponse, errorResponse } from './protocol.js';
|
|
5
7
|
// Callback for screencast frames - will be set by the daemon when streaming is active
|
|
6
8
|
let screencastFrameCallback = null;
|
|
@@ -188,6 +190,10 @@ export async function executeCommand(command, browser) {
|
|
|
188
190
|
return await handleTraceStart(command, browser);
|
|
189
191
|
case 'trace_stop':
|
|
190
192
|
return await handleTraceStop(command, browser);
|
|
193
|
+
case 'profiler_start':
|
|
194
|
+
return await handleProfilerStart(command, browser);
|
|
195
|
+
case 'profiler_stop':
|
|
196
|
+
return await handleProfilerStop(command, browser);
|
|
191
197
|
case 'har_start':
|
|
192
198
|
return await handleHarStart(command, browser);
|
|
193
199
|
case 'har_stop':
|
|
@@ -196,6 +202,16 @@ export async function executeCommand(command, browser) {
|
|
|
196
202
|
return await handleStateSave(command, browser);
|
|
197
203
|
case 'state_load':
|
|
198
204
|
return await handleStateLoad(command, browser);
|
|
205
|
+
case 'state_list':
|
|
206
|
+
return await handleStateList(command);
|
|
207
|
+
case 'state_clear':
|
|
208
|
+
return await handleStateClear(command);
|
|
209
|
+
case 'state_show':
|
|
210
|
+
return await handleStateShow(command);
|
|
211
|
+
case 'state_clean':
|
|
212
|
+
return await handleStateClean(command);
|
|
213
|
+
case 'state_rename':
|
|
214
|
+
return await handleStateRename(command);
|
|
199
215
|
case 'console':
|
|
200
216
|
return await handleConsole(command, browser);
|
|
201
217
|
case 'errors':
|
|
@@ -336,6 +352,27 @@ async function handleClick(command, browser) {
|
|
|
336
352
|
// Support both refs (@e1) and regular selectors
|
|
337
353
|
const locator = browser.getLocator(command.selector);
|
|
338
354
|
try {
|
|
355
|
+
// If --new-tab flag is set, get the href and open in a new tab
|
|
356
|
+
if (command.newTab) {
|
|
357
|
+
const fullUrl = await locator.evaluate((el) => {
|
|
358
|
+
const href = el.getAttribute('href');
|
|
359
|
+
// URL and document.baseURI are available in the browser context
|
|
360
|
+
return href
|
|
361
|
+
? new globalThis.URL(href, globalThis.document.baseURI).toString()
|
|
362
|
+
: '';
|
|
363
|
+
});
|
|
364
|
+
if (!fullUrl) {
|
|
365
|
+
throw new Error(`Element '${command.selector}' does not have an href attribute. --new-tab only works on links.`);
|
|
366
|
+
}
|
|
367
|
+
await browser.newTab();
|
|
368
|
+
const newPage = browser.getPage();
|
|
369
|
+
await newPage.goto(fullUrl);
|
|
370
|
+
return successResponse(command.id, {
|
|
371
|
+
clicked: true,
|
|
372
|
+
newTab: true,
|
|
373
|
+
url: fullUrl,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
339
376
|
await locator.click({
|
|
340
377
|
button: command.button,
|
|
341
378
|
clickCount: command.clickCount,
|
|
@@ -980,7 +1017,24 @@ async function handleTraceStart(command, browser) {
|
|
|
980
1017
|
}
|
|
981
1018
|
async function handleTraceStop(command, browser) {
|
|
982
1019
|
await browser.stopTracing(command.path);
|
|
983
|
-
return successResponse(command.id, { path: command.path });
|
|
1020
|
+
return successResponse(command.id, command.path ? { path: command.path } : { traceStopped: true });
|
|
1021
|
+
}
|
|
1022
|
+
async function handleProfilerStart(command, browser) {
|
|
1023
|
+
await browser.startProfiling({ categories: command.categories });
|
|
1024
|
+
return successResponse(command.id, { started: true });
|
|
1025
|
+
}
|
|
1026
|
+
async function handleProfilerStop(command, browser) {
|
|
1027
|
+
let outputPath = command.path;
|
|
1028
|
+
if (!outputPath) {
|
|
1029
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1030
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
1031
|
+
const filename = `profile-${timestamp}-${random}.json`;
|
|
1032
|
+
const profileDir = path.join(getAppDir(), 'tmp', 'profiles');
|
|
1033
|
+
mkdirSync(profileDir, { recursive: true });
|
|
1034
|
+
outputPath = path.join(profileDir, filename);
|
|
1035
|
+
}
|
|
1036
|
+
const result = await browser.stopProfiling(outputPath);
|
|
1037
|
+
return successResponse(command.id, result);
|
|
984
1038
|
}
|
|
985
1039
|
async function handleHarStart(command, browser) {
|
|
986
1040
|
await browser.startHarRecording();
|
|
@@ -1001,12 +1055,145 @@ async function handleStateSave(command, browser) {
|
|
|
1001
1055
|
return successResponse(command.id, { path: command.path });
|
|
1002
1056
|
}
|
|
1003
1057
|
async function handleStateLoad(command, browser) {
|
|
1004
|
-
|
|
1058
|
+
if (browser.isLaunched()) {
|
|
1059
|
+
return errorResponse(command.id, 'Cannot load state while browser is running. Close browser first, then relaunch with loaded state.');
|
|
1060
|
+
}
|
|
1061
|
+
if (!fs.existsSync(command.path)) {
|
|
1062
|
+
return errorResponse(command.id, `State file not found: ${command.path}`);
|
|
1063
|
+
}
|
|
1064
|
+
await browser.launch({
|
|
1065
|
+
id: command.id,
|
|
1066
|
+
action: 'launch',
|
|
1067
|
+
headless: true,
|
|
1068
|
+
autoStateFilePath: command.path,
|
|
1069
|
+
});
|
|
1005
1070
|
return successResponse(command.id, {
|
|
1006
|
-
|
|
1071
|
+
loaded: true,
|
|
1007
1072
|
path: command.path,
|
|
1008
1073
|
});
|
|
1009
1074
|
}
|
|
1075
|
+
async function handleStateList(command) {
|
|
1076
|
+
const sessionsDir = getSessionsDir();
|
|
1077
|
+
const files = listStateFiles();
|
|
1078
|
+
if (files.length === 0) {
|
|
1079
|
+
return successResponse(command.id, { files: [], directory: sessionsDir });
|
|
1080
|
+
}
|
|
1081
|
+
const stateFiles = files
|
|
1082
|
+
.map((filename) => {
|
|
1083
|
+
const filepath = path.join(sessionsDir, filename);
|
|
1084
|
+
const stats = fs.statSync(filepath);
|
|
1085
|
+
let encrypted = false;
|
|
1086
|
+
try {
|
|
1087
|
+
const content = fs.readFileSync(filepath, 'utf-8');
|
|
1088
|
+
const parsed = JSON.parse(content);
|
|
1089
|
+
encrypted = isEncryptedPayload(parsed);
|
|
1090
|
+
}
|
|
1091
|
+
catch {
|
|
1092
|
+
// Ignore parse errors
|
|
1093
|
+
}
|
|
1094
|
+
return {
|
|
1095
|
+
filename,
|
|
1096
|
+
path: filepath,
|
|
1097
|
+
size: stats.size,
|
|
1098
|
+
modified: stats.mtime.toISOString(),
|
|
1099
|
+
encrypted,
|
|
1100
|
+
};
|
|
1101
|
+
})
|
|
1102
|
+
.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
1103
|
+
return successResponse(command.id, { files: stateFiles, directory: sessionsDir });
|
|
1104
|
+
}
|
|
1105
|
+
async function handleStateClear(command) {
|
|
1106
|
+
const sessionsDir = getSessionsDir();
|
|
1107
|
+
if (command.sessionName && !isValidSessionName(command.sessionName)) {
|
|
1108
|
+
return errorResponse(command.id, 'Invalid session name. Use only letters, numbers, dashes, and underscores.');
|
|
1109
|
+
}
|
|
1110
|
+
const files = listStateFiles();
|
|
1111
|
+
if (files.length === 0) {
|
|
1112
|
+
return successResponse(command.id, { cleared: 0, deleted: [] });
|
|
1113
|
+
}
|
|
1114
|
+
const deleted = [];
|
|
1115
|
+
if (command.all) {
|
|
1116
|
+
for (const file of files) {
|
|
1117
|
+
fs.unlinkSync(path.join(sessionsDir, file));
|
|
1118
|
+
deleted.push(file);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
else if (command.sessionName) {
|
|
1122
|
+
for (const file of files) {
|
|
1123
|
+
if (file.startsWith(`${command.sessionName}-`)) {
|
|
1124
|
+
fs.unlinkSync(path.join(sessionsDir, file));
|
|
1125
|
+
deleted.push(file);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return successResponse(command.id, { cleared: deleted.length, deleted });
|
|
1130
|
+
}
|
|
1131
|
+
async function handleStateShow(command) {
|
|
1132
|
+
const sessionsDir = getSessionsDir();
|
|
1133
|
+
const baseName = command.filename.replace(/\.json$/, '');
|
|
1134
|
+
if (!command.filename.endsWith('.json') || !isValidSessionName(baseName)) {
|
|
1135
|
+
return errorResponse(command.id, 'Invalid filename. Use only letters, numbers, dashes, and underscores (with .json extension).');
|
|
1136
|
+
}
|
|
1137
|
+
const filepath = path.join(sessionsDir, command.filename);
|
|
1138
|
+
if (!fs.existsSync(filepath)) {
|
|
1139
|
+
return errorResponse(command.id, `State file not found: ${command.filename}`);
|
|
1140
|
+
}
|
|
1141
|
+
try {
|
|
1142
|
+
const { data: state, wasEncrypted } = readStateFile(filepath);
|
|
1143
|
+
const stats = fs.statSync(filepath);
|
|
1144
|
+
const stateObj = state;
|
|
1145
|
+
const cookies = stateObj.cookies?.length || 0;
|
|
1146
|
+
const origins = stateObj.origins?.length || 0;
|
|
1147
|
+
const domains = [...new Set((stateObj.cookies || []).map((c) => c.domain))];
|
|
1148
|
+
return successResponse(command.id, {
|
|
1149
|
+
filename: command.filename,
|
|
1150
|
+
path: filepath,
|
|
1151
|
+
size: stats.size,
|
|
1152
|
+
modified: stats.mtime.toISOString(),
|
|
1153
|
+
encrypted: wasEncrypted,
|
|
1154
|
+
summary: {
|
|
1155
|
+
cookies,
|
|
1156
|
+
origins,
|
|
1157
|
+
domains,
|
|
1158
|
+
},
|
|
1159
|
+
state,
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
catch (e) {
|
|
1163
|
+
return errorResponse(command.id, `Failed to parse state file: ${e.message}`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
async function handleStateClean(command) {
|
|
1167
|
+
const deleted = cleanupExpiredStates(command.days);
|
|
1168
|
+
const keptCount = listStateFiles().length;
|
|
1169
|
+
return successResponse(command.id, {
|
|
1170
|
+
cleaned: deleted.length,
|
|
1171
|
+
deleted,
|
|
1172
|
+
keptCount,
|
|
1173
|
+
days: command.days,
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
async function handleStateRename(command) {
|
|
1177
|
+
const sessionsDir = getSessionsDir();
|
|
1178
|
+
if (!isValidSessionName(command.oldName) || !isValidSessionName(command.newName)) {
|
|
1179
|
+
return errorResponse(command.id, 'Invalid name. Use only letters, numbers, dashes, and underscores.');
|
|
1180
|
+
}
|
|
1181
|
+
const oldPath = path.join(sessionsDir, `${command.oldName}.json`);
|
|
1182
|
+
const newPath = path.join(sessionsDir, `${command.newName}.json`);
|
|
1183
|
+
if (!fs.existsSync(oldPath)) {
|
|
1184
|
+
return errorResponse(command.id, `State file not found: ${command.oldName}.json`);
|
|
1185
|
+
}
|
|
1186
|
+
if (fs.existsSync(newPath)) {
|
|
1187
|
+
return errorResponse(command.id, `Destination already exists: ${command.newName}.json`);
|
|
1188
|
+
}
|
|
1189
|
+
fs.renameSync(oldPath, newPath);
|
|
1190
|
+
return successResponse(command.id, {
|
|
1191
|
+
renamed: true,
|
|
1192
|
+
oldName: `${command.oldName}.json`,
|
|
1193
|
+
newName: `${command.newName}.json`,
|
|
1194
|
+
path: newPath,
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1010
1197
|
async function handleConsole(command, browser) {
|
|
1011
1198
|
if (command.clear) {
|
|
1012
1199
|
browser.clearConsoleMessages();
|