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.
Files changed (42) hide show
  1. package/README.md +137 -11
  2. package/bin/agent-browser-darwin-arm64 +0 -0
  3. package/bin/agent-browser-darwin-x64 +0 -0
  4. package/bin/agent-browser-linux-arm64 +0 -0
  5. package/bin/agent-browser-linux-x64 +0 -0
  6. package/bin/agent-browser-win32-x64.exe +0 -0
  7. package/dist/actions.d.ts.map +1 -1
  8. package/dist/actions.js +191 -4
  9. package/dist/actions.js.map +1 -1
  10. package/dist/browser.d.ts +36 -3
  11. package/dist/browser.d.ts.map +1 -1
  12. package/dist/browser.js +230 -8
  13. package/dist/browser.js.map +1 -1
  14. package/dist/daemon.d.ts.map +1 -1
  15. package/dist/daemon.js +120 -2
  16. package/dist/daemon.js.map +1 -1
  17. package/dist/encryption.d.ts +50 -0
  18. package/dist/encryption.d.ts.map +1 -0
  19. package/dist/encryption.js +85 -0
  20. package/dist/encryption.js.map +1 -0
  21. package/dist/protocol.d.ts.map +1 -1
  22. package/dist/protocol.js +56 -2
  23. package/dist/protocol.js.map +1 -1
  24. package/dist/snapshot.d.ts.map +1 -1
  25. package/dist/snapshot.js +18 -5
  26. package/dist/snapshot.js.map +1 -1
  27. package/dist/state-utils.d.ts +77 -0
  28. package/dist/state-utils.d.ts.map +1 -0
  29. package/dist/state-utils.js +178 -0
  30. package/dist/state-utils.js.map +1 -0
  31. package/dist/stream-server.d.ts.map +1 -1
  32. package/dist/stream-server.js +4 -0
  33. package/dist/stream-server.js.map +1 -1
  34. package/dist/types.d.ts +51 -4
  35. package/dist/types.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/skills/agent-browser/SKILL.md +87 -1
  38. package/skills/agent-browser/references/commands.md +4 -0
  39. package/skills/agent-browser/references/profiling.md +120 -0
  40. package/skills/agent-browser/references/proxy-support.md +10 -4
  41. package/skills/agent-browser/references/snapshot-refs.md +2 -2
  42. 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
- ### npm (recommended)
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`, `check`, `hover`, `text`
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
- | `--ignore-https-errors` | Ignore HTTPS certificate errors (useful for self-signed certs) |
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 - just tell your agent to use it:
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
@@ -1 +1 @@
1
- {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpE,OAAO,KAAK,EACV,OAAO,EACP,QAAQ,EAqHT,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,CAmQjG"}
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
- // Storage state is loaded at context creation
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
- note: 'Storage state must be loaded at browser launch. Use --state flag.',
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();