@ulpi/browse 0.7.5 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ulpi/browse",
3
- "version": "0.7.5",
3
+ "version": "1.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ulpi-io/browse"
@@ -8,20 +8,20 @@
8
8
  "dependencies": {
9
9
  "@lightpanda/browser": "^1.2.0",
10
10
  "@ulpi/browse": "^0.3.0",
11
+ "better-sqlite3": "^11.0.0",
11
12
  "diff": "^7.0.0",
12
13
  "playwright": "^1.58.2",
13
14
  "playwright-core": "^1.58.2"
14
15
  },
15
16
  "bin": {
16
- "browse": "bin/browse.ts"
17
+ "browse": "dist/browse.mjs"
17
18
  },
18
19
  "description": "Fast headless browser CLI — persistent Chromium daemon via Playwright.",
19
20
  "engines": {
20
- "bun": ">=1.0.0"
21
+ "node": ">=18.0.0"
21
22
  },
22
23
  "files": [
23
- "bin/",
24
- "src/",
24
+ "dist/",
25
25
  "skill/",
26
26
  "LICENSE",
27
27
  "README.md",
@@ -41,19 +41,23 @@
41
41
  "access": "public"
42
42
  },
43
43
  "scripts": {
44
- "build": "bun build --compile --external electron --external chromium-bidi src/cli.ts --outfile dist/browse && rm -f .*.bun-build",
44
+ "build": "esbuild src/cli.ts --bundle --format=esm --platform=node --target=node18 --outfile=dist/browse.mjs --external:playwright --external:playwright-core --external:better-sqlite3 --external:electron --external:chromium-bidi --banner:js='#!/usr/bin/env node'",
45
45
  "build:all": "bash scripts/build-all.sh",
46
- "dev": "bun run src/cli.ts",
47
- "server": "bun run src/server.ts",
48
- "test": "bun test",
49
- "start": "bun run src/server.ts",
50
- "postinstall": "bunx playwright install chromium",
51
- "benchmark": "bun run benchmark.ts"
46
+ "dev": "tsx src/cli.ts",
47
+ "server": "tsx src/server.ts",
48
+ "test": "vitest run",
49
+ "start": "tsx src/server.ts",
50
+ "postinstall": "npx playwright install chromium",
51
+ "benchmark": "tsx benchmark.ts"
52
52
  },
53
53
  "type": "module",
54
54
  "devDependencies": {
55
+ "@types/better-sqlite3": "^7.0.0",
55
56
  "@types/node": "^25.5.0",
56
- "typescript": "^5.9.3"
57
+ "esbuild": "^0.25.0",
58
+ "tsx": "^4.0.0",
59
+ "typescript": "^5.9.3",
60
+ "vitest": "^3.0.0"
57
61
  },
58
62
  "optionalDependencies": {
59
63
  "rebrowser-playwright": "^1.52.0"
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: browse
3
- version: 2.5.1
3
+ version: 2.8.0
4
4
  description: |
5
5
  Fast web browsing for AI coding agents via persistent headless Chromium daemon. Navigate to any URL,
6
6
  read page content, click elements, fill forms, run JavaScript, take screenshots,
@@ -31,8 +31,7 @@ fi
31
31
 
32
32
  If `NEEDS_INSTALL`:
33
33
  1. Tell the user: "browse needs a one-time install via npm. OK to proceed?"
34
- 2. If they approve: `bun install -g @ulpi/browse`
35
- 3. If `bun` is not installed: `curl -fsSL https://bun.sh/install | bash`
34
+ 2. If they approve: `npm install -g @ulpi/browse`
36
35
 
37
36
  ### Permissions check
38
37
 
@@ -75,13 +74,21 @@ If the file is missing or does not contain browse permission rules in `permissio
75
74
  "Bash(browse frame:*)",
76
75
  "Bash(browse sessions:*)", "Bash(browse session-close:*)",
77
76
  "Bash(browse state:*)", "Bash(browse auth:*)", "Bash(browse har:*)", "Bash(browse video:*)",
77
+ "Bash(browse record:*)",
78
78
  "Bash(browse route:*)", "Bash(browse offline:*)",
79
79
  "Bash(browse status:*)", "Bash(browse stop:*)", "Bash(browse restart:*)",
80
80
  "Bash(browse cookie:*)", "Bash(browse header:*)",
81
81
  "Bash(browse useragent:*)",
82
82
  "Bash(browse clipboard:*)", "Bash(browse screenshot-diff:*)",
83
83
  "Bash(browse find:*)", "Bash(browse inspect:*)",
84
- "Bash(browse instances:*)", "Bash(browse --headed:*)"
84
+ "Bash(browse instances:*)", "Bash(browse --headed:*)",
85
+ "Bash(browse rightclick:*)", "Bash(browse tap:*)",
86
+ "Bash(browse swipe:*)", "Bash(browse mouse:*)",
87
+ "Bash(browse keyboard:*)", "Bash(browse scrollinto:*)",
88
+ "Bash(browse scrollintoview:*)", "Bash(browse set:*)",
89
+ "Bash(browse box:*)", "Bash(browse errors:*)",
90
+ "Bash(browse doctor:*)", "Bash(browse upgrade:*)",
91
+ "Bash(browse --max-output:*)"
85
92
  ```
86
93
 
87
94
  ## IMPORTANT
@@ -189,6 +196,28 @@ browse --allowed-domains example.com,*.cdn.example.com goto https://example.com
189
196
  # State persistence
190
197
  browse state save mysite
191
198
  browse state load mysite
199
+ browse state clean # delete states older than 7 days
200
+ browse state clean --older-than 30 # custom threshold
201
+
202
+ # Cookie management
203
+ browse cookie clear # clear all cookies
204
+ browse cookie set auth token --domain .example.com # set with options
205
+ browse cookie export ./cookies.json # export to file
206
+ browse cookie import ./cookies.json # import from file
207
+
208
+ # Cookie import from real browsers (macOS — Chrome, Arc, Brave, Edge)
209
+ browse cookie-import --list # show installed browsers
210
+ browse cookie-import chrome --domain .example.com # import cookies for a domain
211
+ browse cookie-import arc --domain .github.com # import from Arc
212
+ browse cookie-import chrome --profile "Profile 1" --domain .site.com # specific Chrome profile
213
+
214
+ # Session auto-persistence (named sessions survive restarts)
215
+ browse --session myapp goto https://app.com/login # login...
216
+ browse session-close myapp # state auto-saved (encrypted if BROWSE_ENCRYPTION_KEY set)
217
+ browse --session myapp goto https://app.com/dashboard # cookies auto-restored
218
+
219
+ # Load state at launch
220
+ browse --state auth.json goto https://app.com # load cookies before first command
192
221
 
193
222
  # Auth vault (credentials never visible to LLM)
194
223
  browse auth save github https://github.com/login user pass123
@@ -199,11 +228,31 @@ browse har start
199
228
  browse goto https://example.com
200
229
  browse har stop ./recording.har
201
230
 
202
- # Video recording
231
+ # Video recording (watch a .webm of the session)
232
+ browse video start ./videos
233
+ browse goto https://example.com
234
+ browse click @e3
235
+ browse video stop
236
+
237
+ # Command recording (export replayable scripts)
238
+ browse record start
239
+ browse goto https://example.com
240
+ browse click "a"
241
+ browse fill "[id=search]" "test query"
242
+ browse record stop
243
+ browse record export replay ./recording.json # replay with: npx @puppeteer/replay ./recording.json
244
+ browse record export browse ./steps.json # replay with: cat steps.json | browse chain
245
+
246
+ # Both together (video + replayable script)
203
247
  browse video start ./videos
248
+ browse record start
204
249
  browse goto https://example.com
250
+ browse snapshot -i
205
251
  browse click @e3
252
+ browse fill "[id=email]" "user@test.com"
253
+ browse record stop
206
254
  browse video stop
255
+ browse record export replay ./recording.json
207
256
 
208
257
  # Device emulation
209
258
  browse emulate iphone
@@ -286,11 +335,13 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
286
335
  ```
287
336
  browse click <selector> Click element (CSS selector or @ref)
288
337
  browse click <x>,<y> Click at page coordinates (e.g. 590,461)
338
+ browse rightclick <selector> Right-click element (context menu)
289
339
  browse dblclick <selector> Double-click element
290
340
  browse fill <selector> <value> Fill input field
291
341
  browse select <selector> <val> Select dropdown value
292
342
  browse hover <selector> Hover over element
293
343
  browse focus <selector> Focus element
344
+ browse tap <selector> Tap element (requires touch context via emulate)
294
345
  browse check <selector> Check checkbox
295
346
  browse uncheck <selector> Uncheck checkbox
296
347
  browse drag <src> <tgt> Drag source to target
@@ -298,8 +349,24 @@ browse type <text> Type into focused element
298
349
  browse press <key> Press key (Enter, Tab, Escape, etc.)
299
350
  browse keydown <key> Hold key down
300
351
  browse keyup <key> Release key
352
+ browse keyboard inserttext <t> Insert text without key events
301
353
  browse scroll [sel|up|down] Scroll element/viewport/bottom
302
- browse wait <sel|--url|--network-idle> Wait for element, URL, or network
354
+ browse scrollinto <sel> Scroll element into view (explicit)
355
+ browse swipe <dir> [px] Swipe up/down/left/right (touch events)
356
+ browse mouse move <x> <y> Move mouse to coordinates
357
+ browse mouse down [button] Press mouse button (left/right/middle)
358
+ browse mouse up [button] Release mouse button
359
+ browse mouse wheel <dy> [dx] Scroll wheel
360
+ browse wait <sel> Wait for element to appear
361
+ browse wait <sel> --state hidden Wait for element to disappear
362
+ browse wait <ms> Wait for milliseconds
363
+ browse wait --text "..." Wait for text to appear in page
364
+ browse wait --fn "expr" Wait for JavaScript condition
365
+ browse wait --load <state> Wait for load state
366
+ browse wait --url <pattern> Wait for URL match
367
+ browse wait --network-idle Wait for network idle
368
+ browse set geo <lat> <lng> Set geolocation
369
+ browse set media <scheme> Set color scheme (dark/light/no-preference)
303
370
  browse viewport <WxH> Set viewport size (e.g. 375x812)
304
371
  browse upload <sel> <files> Upload file(s) to a file input
305
372
  browse highlight <selector> Highlight element (visual debugging)
@@ -327,8 +394,10 @@ browse attrs <selector> Get element attributes as JSON
327
394
  browse element-state <selector> Element state (visible/enabled/checked/focused)
328
395
  browse value <selector> Get input field value
329
396
  browse count <selector> Count matching elements
397
+ browse box <selector> Get bounding box as JSON {x, y, width, height}
330
398
  browse dialog Last dialog info or "(no dialog detected)"
331
399
  browse console [--clear] View/clear console messages
400
+ browse errors [--clear] View/clear page errors (filtered from console)
332
401
  browse network [--clear] View/clear network requests
333
402
  browse cookies Dump all cookies as JSON
334
403
  browse storage [set <k> <v>] View/set localStorage
@@ -342,6 +411,8 @@ browse clipboard write <text> Write text to system clipboard
342
411
  ```
343
412
  browse screenshot [path] Viewport screenshot (default: .browse/sessions/{id}/screenshot.png)
344
413
  browse screenshot --full [path] Full-page screenshot (entire scrollable page)
414
+ browse screenshot <sel|@ref> [path] Screenshot specific element
415
+ browse screenshot --clip x,y,w,h [path] Screenshot clipped region
345
416
  browse screenshot --annotate [path] Screenshot with numbered badges + legend
346
417
  browse pdf [path] Save as PDF
347
418
  browse responsive [prefix] Screenshots at mobile/tablet/desktop
@@ -360,6 +431,11 @@ browse find text <query> Find elements by text content
360
431
  browse find label <query> Find elements by label
361
432
  browse find placeholder <query> Find elements by placeholder
362
433
  browse find testid <query> Find elements by test ID
434
+ browse find alt <query> Find elements by alt text
435
+ browse find title <query> Find elements by title attribute
436
+ browse find first <sel> First matching element
437
+ browse find last <sel> Last matching element
438
+ browse find nth <n> <sel> Nth matching element (0-indexed)
363
439
  ```
364
440
 
365
441
  ### Compare
@@ -394,6 +470,15 @@ browse state save [name] Save cookies + localStorage (all origins)
394
470
  browse state load [name] Restore saved state
395
471
  browse state list List saved states
396
472
  browse state show [name] Show contents of saved state
473
+ browse state clean Delete states older than 7 days
474
+ browse state clean --older-than N Custom age threshold (days)
475
+ ```
476
+
477
+ ### Cookie import (macOS — borrow auth from real browsers)
478
+ ```
479
+ browse cookie-import --list List installed browsers
480
+ browse cookie-import <browser> --domain <d> Import cookies for a domain
481
+ browse cookie-import <browser> --profile <p> --domain <d> Specific Chrome profile
397
482
  ```
398
483
 
399
484
  ### Auth vault
@@ -417,11 +502,22 @@ browse video stop Stop recording and save video files
417
502
  browse video status Check if recording is active
418
503
  ```
419
504
 
505
+ ### Command recording & export
506
+ ```
507
+ browse record start Start recording commands
508
+ browse record stop Stop recording, keep steps for export
509
+ browse record status Recording state and step count
510
+ browse record export browse [path] Export as chain-compatible JSON (replay with browse chain)
511
+ browse record export replay [path] Export as Chrome DevTools Recorder (Playwright/Puppeteer)
512
+ ```
513
+
420
514
  ### Server management
421
515
  ```
422
516
  browse status Server health, uptime, session count
423
517
  browse instances List all running browse servers (instance, PID, port, status)
424
518
  browse version Print CLI version
519
+ browse doctor System check (Bun, Playwright, Chromium)
520
+ browse upgrade Self-update via npm
425
521
  browse stop Shutdown server
426
522
  browse restart Kill + restart server
427
523
  browse inspect Open DevTools (requires BROWSE_DEBUG_PORT)
@@ -431,10 +527,12 @@ browse inspect Open DevTools (requires BROWSE_DEBUG_PORT)
431
527
 
432
528
  | Flag | Description |
433
529
  |------|-------------|
434
- | `--session <id>` | Named session (isolates tabs, refs, cookies) |
530
+ | `--session <id>` | Named session (isolates tabs, refs, cookies — auto-persists on close) |
531
+ | `--state <path>` | Load state file (cookies/storage) before first command |
435
532
  | `--json` | Wrap output as `{success, data, command}` |
436
533
  | `--content-boundaries` | Wrap page content in nonce-delimited markers (prompt injection defense) |
437
534
  | `--allowed-domains <d,d>` | Block navigation/resources outside allowlist |
535
+ | `--max-output <n>` | Truncate output to N characters |
438
536
  | `--headed` | Run browser in headed (visible) mode |
439
537
  | `--runtime <name>` | Browser engine: playwright (default), rebrowser (stealth) |
440
538
 
@@ -480,6 +578,7 @@ browse inspect Open DevTools (requires BROWSE_DEBUG_PORT)
480
578
  | Auto-login | `auth save gh https://github.com/login user pass` → `auth login gh` |
481
579
  | Record network | `har start` → browse around → `har stop ./out.har` |
482
580
  | Record video | `video start ./vids` → browse around → `video stop` |
581
+ | Export automation script | `record start` → browse around → `record export replay ./recording.json` |
483
582
  | Parallel agents | `--session agent-a <cmd>` / `--session agent-b <cmd>` |
484
583
  | Multi-step flow | `echo '[...]' \| browse chain` |
485
584
  | Secure browsing | `--allowed-domains example.com goto https://example.com` |
@@ -489,6 +588,14 @@ browse inspect Open DevTools (requires BROWSE_DEBUG_PORT)
489
588
  | Find by accessibility | `find role button` / `find text "Submit"` |
490
589
  | Visual regression | `screenshot-diff baseline.png` |
491
590
  | Debug with DevTools | `inspect` (set BROWSE_DEBUG_PORT first) |
591
+ | Get element position | `box @e3` |
592
+ | Check page errors | `errors` |
593
+ | Right-click context menu | `rightclick @e3` |
594
+ | Test mobile gestures | `emulate iphone` → `tap @e1` / `swipe down` |
595
+ | Set dark mode | `set media dark` |
596
+ | Test geolocation | `set geo 37.7 -122.4` → verify in page |
597
+ | Export/import cookies | `cookie export ./cookies.json` / `cookie import ./cookies.json` |
598
+ | Limit output size | `--max-output 5000 text` |
492
599
  | See the browser | `browse --headed goto <url>` |
493
600
  | Bypass bot detection | `--runtime rebrowser goto <url>` |
494
601
 
package/bin/browse.ts DELETED
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { main } from '../src/cli';
3
-
4
- if (process.env.__BROWSE_SERVER_MODE === '1') {
5
- import('../src/server');
6
- } else {
7
- main().catch((err) => {
8
- console.error(`[browse] ${err.message}`);
9
- process.exit(1);
10
- });
11
- }
package/src/auth-vault.ts DELETED
@@ -1,244 +0,0 @@
1
- /**
2
- * Credential vault — AES-256-GCM encrypted credential storage
3
- *
4
- * Encryption key: BROWSE_ENCRYPTION_KEY env var (64-char hex)
5
- * or auto-generated at .browse/.encryption-key
6
- *
7
- * Storage: .browse/auth/<name>.json (mode 0o600)
8
- * Password never returned in list/get — only hasPassword: true
9
- */
10
-
11
- import * as crypto from 'crypto';
12
- import * as fs from 'fs';
13
- import * as path from 'path';
14
- import type { BrowserManager } from './browser-manager';
15
- import { DEFAULTS } from './constants';
16
- import { sanitizeName } from './sanitize';
17
-
18
- interface StoredCredential {
19
- name: string;
20
- url: string;
21
- username: string;
22
- encrypted: true;
23
- iv: string; // base64
24
- authTag: string; // base64
25
- data: string; // base64 (encrypted password)
26
- usernameSelector?: string;
27
- passwordSelector?: string;
28
- submitSelector?: string;
29
- createdAt: string;
30
- updatedAt: string;
31
- }
32
-
33
- export interface CredentialInfo {
34
- name: string;
35
- url: string;
36
- username: string;
37
- hasPassword: boolean;
38
- createdAt: string;
39
- }
40
-
41
- export class AuthVault {
42
- private authDir: string;
43
- private encryptionKey: Buffer;
44
-
45
- constructor(localDir: string) {
46
- this.authDir = path.join(localDir, 'auth');
47
- this.encryptionKey = this.resolveKey(localDir);
48
- }
49
-
50
- private resolveKey(localDir: string): Buffer {
51
- // 1. Env var (64-char hex = 32 bytes)
52
- const envKey = process.env.BROWSE_ENCRYPTION_KEY;
53
- if (envKey) {
54
- if (envKey.length !== 64) {
55
- throw new Error('BROWSE_ENCRYPTION_KEY must be 64 hex characters (32 bytes)');
56
- }
57
- return Buffer.from(envKey, 'hex');
58
- }
59
-
60
- // 2. Key file
61
- const keyPath = path.join(localDir, '.encryption-key');
62
- if (fs.existsSync(keyPath)) {
63
- const hex = fs.readFileSync(keyPath, 'utf-8').trim();
64
- return Buffer.from(hex, 'hex');
65
- }
66
-
67
- // 3. Auto-generate
68
- const key = crypto.randomBytes(32);
69
- fs.writeFileSync(keyPath, key.toString('hex') + '\n', { mode: 0o600 });
70
- return key;
71
- }
72
-
73
- private encrypt(plaintext: string): { ciphertext: string; iv: string; authTag: string } {
74
- const iv = crypto.randomBytes(12);
75
- const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, iv);
76
- const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
77
- return {
78
- ciphertext: encrypted.toString('base64'),
79
- iv: iv.toString('base64'),
80
- authTag: cipher.getAuthTag().toString('base64'),
81
- };
82
- }
83
-
84
- private decrypt(ciphertext: string, iv: string, authTag: string): string {
85
- const decipher = crypto.createDecipheriv(
86
- 'aes-256-gcm',
87
- this.encryptionKey,
88
- Buffer.from(iv, 'base64'),
89
- );
90
- decipher.setAuthTag(Buffer.from(authTag, 'base64'));
91
- const decrypted = Buffer.concat([
92
- decipher.update(Buffer.from(ciphertext, 'base64')),
93
- decipher.final(),
94
- ]);
95
- return decrypted.toString('utf-8');
96
- }
97
-
98
- save(
99
- name: string,
100
- url: string,
101
- username: string,
102
- password: string,
103
- selectors?: { username?: string; password?: string; submit?: string },
104
- ): void {
105
- fs.mkdirSync(this.authDir, { recursive: true });
106
-
107
- const { ciphertext, iv, authTag } = this.encrypt(password);
108
- const now = new Date().toISOString();
109
-
110
- const credential: StoredCredential = {
111
- name,
112
- url,
113
- username,
114
- encrypted: true,
115
- iv,
116
- authTag,
117
- data: ciphertext,
118
- usernameSelector: selectors?.username,
119
- passwordSelector: selectors?.password,
120
- submitSelector: selectors?.submit,
121
- createdAt: now,
122
- updatedAt: now,
123
- };
124
-
125
- const filePath = path.join(this.authDir, `${sanitizeName(name)}.json`);
126
- fs.writeFileSync(filePath, JSON.stringify(credential, null, 2), { mode: 0o600 });
127
- }
128
-
129
- private load(name: string): StoredCredential {
130
- const filePath = path.join(this.authDir, `${sanitizeName(name)}.json`);
131
- if (!fs.existsSync(filePath)) {
132
- throw new Error(`Credential "${name}" not found. Run "browse auth list" to see saved credentials.`);
133
- }
134
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
135
- }
136
-
137
- async login(name: string, bm: BrowserManager): Promise<string> {
138
- const cred = this.load(name);
139
- const password = this.decrypt(cred.data, cred.iv, cred.authTag);
140
- const page = bm.getPage();
141
-
142
- // Navigate to login URL
143
- await page.goto(cred.url, {
144
- waitUntil: 'domcontentloaded',
145
- timeout: DEFAULTS.COMMAND_TIMEOUT_MS,
146
- });
147
-
148
- // Resolve selectors: use stored or auto-detect
149
- const userSel = cred.usernameSelector || await autoDetectSelector(page, 'username');
150
- const passSel = cred.passwordSelector || await autoDetectSelector(page, 'password');
151
- const submitSel = cred.submitSelector || await autoDetectSelector(page, 'submit');
152
-
153
- // Fill credentials
154
- await page.fill(userSel, cred.username, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
155
- await page.fill(passSel, password, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
156
-
157
- // Submit
158
- await page.click(submitSel, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
159
- await page.waitForLoadState('domcontentloaded').catch(() => {});
160
-
161
- return `Logged in as ${cred.username} at ${page.url()}`;
162
- }
163
-
164
- list(): CredentialInfo[] {
165
- if (!fs.existsSync(this.authDir)) return [];
166
-
167
- const files = fs.readdirSync(this.authDir).filter(f => f.endsWith('.json'));
168
- return files.map(f => {
169
- try {
170
- const data = JSON.parse(fs.readFileSync(path.join(this.authDir, f), 'utf-8'));
171
- return {
172
- name: data.name,
173
- url: data.url,
174
- username: data.username,
175
- hasPassword: true,
176
- createdAt: data.createdAt,
177
- };
178
- } catch {
179
- return null;
180
- }
181
- }).filter(Boolean) as CredentialInfo[];
182
- }
183
-
184
- delete(name: string): void {
185
- const filePath = path.join(this.authDir, `${sanitizeName(name)}.json`);
186
- if (!fs.existsSync(filePath)) {
187
- throw new Error(`Credential "${name}" not found.`);
188
- }
189
- fs.unlinkSync(filePath);
190
- }
191
- }
192
-
193
- /**
194
- * Auto-detect login form selectors by common patterns.
195
- */
196
- async function autoDetectSelector(page: any, field: 'username' | 'password' | 'submit'): Promise<string> {
197
- if (field === 'username') {
198
- const candidates = [
199
- 'input[type="email"]',
200
- 'input[name="email"]',
201
- 'input[name="username"]',
202
- 'input[name="user"]',
203
- 'input[name="login"]',
204
- 'input[autocomplete="username"]',
205
- 'input[autocomplete="email"]',
206
- 'input[type="text"]:first-of-type',
207
- ];
208
- for (const sel of candidates) {
209
- const count = await page.locator(sel).count();
210
- if (count > 0) return sel;
211
- }
212
- throw new Error('Could not auto-detect username field. Save with explicit selectors: browse auth save <name> <url> <user> <pass> --user-sel <sel> --pass-sel <sel> --submit-sel <sel>');
213
- }
214
-
215
- if (field === 'password') {
216
- const candidates = [
217
- 'input[type="password"]',
218
- 'input[name="password"]',
219
- 'input[name="pass"]',
220
- 'input[autocomplete="current-password"]',
221
- ];
222
- for (const sel of candidates) {
223
- const count = await page.locator(sel).count();
224
- if (count > 0) return sel;
225
- }
226
- throw new Error('Could not auto-detect password field.');
227
- }
228
-
229
- // submit
230
- const candidates = [
231
- 'button[type="submit"]',
232
- 'input[type="submit"]',
233
- 'form button',
234
- 'button:has-text("Log in")',
235
- 'button:has-text("Sign in")',
236
- 'button:has-text("Login")',
237
- 'button:has-text("Submit")',
238
- ];
239
- for (const sel of candidates) {
240
- const count = await page.locator(sel).count();
241
- if (count > 0) return sel;
242
- }
243
- throw new Error('Could not auto-detect submit button.');
244
- }