@ulpi/browse 0.7.5 → 0.10.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 +1 -1
- package/README.md +444 -300
- package/package.json +1 -1
- package/skill/SKILL.md +113 -5
- package/src/auth-vault.ts +4 -52
- package/src/browser-manager.ts +20 -5
- package/src/bun.d.ts +15 -20
- package/src/chrome-discover.ts +73 -0
- package/src/cli.ts +110 -10
- package/src/commands/meta.ts +247 -9
- package/src/commands/read.ts +28 -0
- package/src/commands/write.ts +236 -16
- package/src/config.ts +0 -1
- package/src/cookie-import.ts +410 -0
- package/src/encryption.ts +48 -0
- package/src/record-export.ts +98 -0
- package/src/server.ts +43 -2
- package/src/session-manager.ts +48 -0
- package/src/session-persist.ts +192 -0
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: browse
|
|
3
|
-
version: 2.
|
|
3
|
+
version: 2.7.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,
|
|
@@ -75,13 +75,21 @@ If the file is missing or does not contain browse permission rules in `permissio
|
|
|
75
75
|
"Bash(browse frame:*)",
|
|
76
76
|
"Bash(browse sessions:*)", "Bash(browse session-close:*)",
|
|
77
77
|
"Bash(browse state:*)", "Bash(browse auth:*)", "Bash(browse har:*)", "Bash(browse video:*)",
|
|
78
|
+
"Bash(browse record:*)",
|
|
78
79
|
"Bash(browse route:*)", "Bash(browse offline:*)",
|
|
79
80
|
"Bash(browse status:*)", "Bash(browse stop:*)", "Bash(browse restart:*)",
|
|
80
81
|
"Bash(browse cookie:*)", "Bash(browse header:*)",
|
|
81
82
|
"Bash(browse useragent:*)",
|
|
82
83
|
"Bash(browse clipboard:*)", "Bash(browse screenshot-diff:*)",
|
|
83
84
|
"Bash(browse find:*)", "Bash(browse inspect:*)",
|
|
84
|
-
"Bash(browse instances:*)", "Bash(browse --headed:*)"
|
|
85
|
+
"Bash(browse instances:*)", "Bash(browse --headed:*)",
|
|
86
|
+
"Bash(browse rightclick:*)", "Bash(browse tap:*)",
|
|
87
|
+
"Bash(browse swipe:*)", "Bash(browse mouse:*)",
|
|
88
|
+
"Bash(browse keyboard:*)", "Bash(browse scrollinto:*)",
|
|
89
|
+
"Bash(browse scrollintoview:*)", "Bash(browse set:*)",
|
|
90
|
+
"Bash(browse box:*)", "Bash(browse errors:*)",
|
|
91
|
+
"Bash(browse doctor:*)", "Bash(browse upgrade:*)",
|
|
92
|
+
"Bash(browse --max-output:*)"
|
|
85
93
|
```
|
|
86
94
|
|
|
87
95
|
## IMPORTANT
|
|
@@ -189,6 +197,28 @@ browse --allowed-domains example.com,*.cdn.example.com goto https://example.com
|
|
|
189
197
|
# State persistence
|
|
190
198
|
browse state save mysite
|
|
191
199
|
browse state load mysite
|
|
200
|
+
browse state clean # delete states older than 7 days
|
|
201
|
+
browse state clean --older-than 30 # custom threshold
|
|
202
|
+
|
|
203
|
+
# Cookie management
|
|
204
|
+
browse cookie clear # clear all cookies
|
|
205
|
+
browse cookie set auth token --domain .example.com # set with options
|
|
206
|
+
browse cookie export ./cookies.json # export to file
|
|
207
|
+
browse cookie import ./cookies.json # import from file
|
|
208
|
+
|
|
209
|
+
# Cookie import from real browsers (macOS — Chrome, Arc, Brave, Edge)
|
|
210
|
+
browse cookie-import --list # show installed browsers
|
|
211
|
+
browse cookie-import chrome --domain .example.com # import cookies for a domain
|
|
212
|
+
browse cookie-import arc --domain .github.com # import from Arc
|
|
213
|
+
browse cookie-import chrome --profile "Profile 1" --domain .site.com # specific Chrome profile
|
|
214
|
+
|
|
215
|
+
# Session auto-persistence (named sessions survive restarts)
|
|
216
|
+
browse --session myapp goto https://app.com/login # login...
|
|
217
|
+
browse session-close myapp # state auto-saved (encrypted if BROWSE_ENCRYPTION_KEY set)
|
|
218
|
+
browse --session myapp goto https://app.com/dashboard # cookies auto-restored
|
|
219
|
+
|
|
220
|
+
# Load state at launch
|
|
221
|
+
browse --state auth.json goto https://app.com # load cookies before first command
|
|
192
222
|
|
|
193
223
|
# Auth vault (credentials never visible to LLM)
|
|
194
224
|
browse auth save github https://github.com/login user pass123
|
|
@@ -199,11 +229,31 @@ browse har start
|
|
|
199
229
|
browse goto https://example.com
|
|
200
230
|
browse har stop ./recording.har
|
|
201
231
|
|
|
202
|
-
# Video recording
|
|
232
|
+
# Video recording (watch a .webm of the session)
|
|
233
|
+
browse video start ./videos
|
|
234
|
+
browse goto https://example.com
|
|
235
|
+
browse click @e3
|
|
236
|
+
browse video stop
|
|
237
|
+
|
|
238
|
+
# Command recording (export replayable scripts)
|
|
239
|
+
browse record start
|
|
240
|
+
browse goto https://example.com
|
|
241
|
+
browse click "a"
|
|
242
|
+
browse fill "[id=search]" "test query"
|
|
243
|
+
browse record stop
|
|
244
|
+
browse record export replay ./recording.json # replay with: npx @puppeteer/replay ./recording.json
|
|
245
|
+
browse record export browse ./steps.json # replay with: cat steps.json | browse chain
|
|
246
|
+
|
|
247
|
+
# Both together (video + replayable script)
|
|
203
248
|
browse video start ./videos
|
|
249
|
+
browse record start
|
|
204
250
|
browse goto https://example.com
|
|
251
|
+
browse snapshot -i
|
|
205
252
|
browse click @e3
|
|
253
|
+
browse fill "[id=email]" "user@test.com"
|
|
254
|
+
browse record stop
|
|
206
255
|
browse video stop
|
|
256
|
+
browse record export replay ./recording.json
|
|
207
257
|
|
|
208
258
|
# Device emulation
|
|
209
259
|
browse emulate iphone
|
|
@@ -286,11 +336,13 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`.
|
|
|
286
336
|
```
|
|
287
337
|
browse click <selector> Click element (CSS selector or @ref)
|
|
288
338
|
browse click <x>,<y> Click at page coordinates (e.g. 590,461)
|
|
339
|
+
browse rightclick <selector> Right-click element (context menu)
|
|
289
340
|
browse dblclick <selector> Double-click element
|
|
290
341
|
browse fill <selector> <value> Fill input field
|
|
291
342
|
browse select <selector> <val> Select dropdown value
|
|
292
343
|
browse hover <selector> Hover over element
|
|
293
344
|
browse focus <selector> Focus element
|
|
345
|
+
browse tap <selector> Tap element (requires touch context via emulate)
|
|
294
346
|
browse check <selector> Check checkbox
|
|
295
347
|
browse uncheck <selector> Uncheck checkbox
|
|
296
348
|
browse drag <src> <tgt> Drag source to target
|
|
@@ -298,8 +350,24 @@ browse type <text> Type into focused element
|
|
|
298
350
|
browse press <key> Press key (Enter, Tab, Escape, etc.)
|
|
299
351
|
browse keydown <key> Hold key down
|
|
300
352
|
browse keyup <key> Release key
|
|
353
|
+
browse keyboard inserttext <t> Insert text without key events
|
|
301
354
|
browse scroll [sel|up|down] Scroll element/viewport/bottom
|
|
302
|
-
browse
|
|
355
|
+
browse scrollinto <sel> Scroll element into view (explicit)
|
|
356
|
+
browse swipe <dir> [px] Swipe up/down/left/right (touch events)
|
|
357
|
+
browse mouse move <x> <y> Move mouse to coordinates
|
|
358
|
+
browse mouse down [button] Press mouse button (left/right/middle)
|
|
359
|
+
browse mouse up [button] Release mouse button
|
|
360
|
+
browse mouse wheel <dy> [dx] Scroll wheel
|
|
361
|
+
browse wait <sel> Wait for element to appear
|
|
362
|
+
browse wait <sel> --state hidden Wait for element to disappear
|
|
363
|
+
browse wait <ms> Wait for milliseconds
|
|
364
|
+
browse wait --text "..." Wait for text to appear in page
|
|
365
|
+
browse wait --fn "expr" Wait for JavaScript condition
|
|
366
|
+
browse wait --load <state> Wait for load state
|
|
367
|
+
browse wait --url <pattern> Wait for URL match
|
|
368
|
+
browse wait --network-idle Wait for network idle
|
|
369
|
+
browse set geo <lat> <lng> Set geolocation
|
|
370
|
+
browse set media <scheme> Set color scheme (dark/light/no-preference)
|
|
303
371
|
browse viewport <WxH> Set viewport size (e.g. 375x812)
|
|
304
372
|
browse upload <sel> <files> Upload file(s) to a file input
|
|
305
373
|
browse highlight <selector> Highlight element (visual debugging)
|
|
@@ -327,8 +395,10 @@ browse attrs <selector> Get element attributes as JSON
|
|
|
327
395
|
browse element-state <selector> Element state (visible/enabled/checked/focused)
|
|
328
396
|
browse value <selector> Get input field value
|
|
329
397
|
browse count <selector> Count matching elements
|
|
398
|
+
browse box <selector> Get bounding box as JSON {x, y, width, height}
|
|
330
399
|
browse dialog Last dialog info or "(no dialog detected)"
|
|
331
400
|
browse console [--clear] View/clear console messages
|
|
401
|
+
browse errors [--clear] View/clear page errors (filtered from console)
|
|
332
402
|
browse network [--clear] View/clear network requests
|
|
333
403
|
browse cookies Dump all cookies as JSON
|
|
334
404
|
browse storage [set <k> <v>] View/set localStorage
|
|
@@ -342,6 +412,8 @@ browse clipboard write <text> Write text to system clipboard
|
|
|
342
412
|
```
|
|
343
413
|
browse screenshot [path] Viewport screenshot (default: .browse/sessions/{id}/screenshot.png)
|
|
344
414
|
browse screenshot --full [path] Full-page screenshot (entire scrollable page)
|
|
415
|
+
browse screenshot <sel|@ref> [path] Screenshot specific element
|
|
416
|
+
browse screenshot --clip x,y,w,h [path] Screenshot clipped region
|
|
345
417
|
browse screenshot --annotate [path] Screenshot with numbered badges + legend
|
|
346
418
|
browse pdf [path] Save as PDF
|
|
347
419
|
browse responsive [prefix] Screenshots at mobile/tablet/desktop
|
|
@@ -360,6 +432,11 @@ browse find text <query> Find elements by text content
|
|
|
360
432
|
browse find label <query> Find elements by label
|
|
361
433
|
browse find placeholder <query> Find elements by placeholder
|
|
362
434
|
browse find testid <query> Find elements by test ID
|
|
435
|
+
browse find alt <query> Find elements by alt text
|
|
436
|
+
browse find title <query> Find elements by title attribute
|
|
437
|
+
browse find first <sel> First matching element
|
|
438
|
+
browse find last <sel> Last matching element
|
|
439
|
+
browse find nth <n> <sel> Nth matching element (0-indexed)
|
|
363
440
|
```
|
|
364
441
|
|
|
365
442
|
### Compare
|
|
@@ -394,6 +471,15 @@ browse state save [name] Save cookies + localStorage (all origins)
|
|
|
394
471
|
browse state load [name] Restore saved state
|
|
395
472
|
browse state list List saved states
|
|
396
473
|
browse state show [name] Show contents of saved state
|
|
474
|
+
browse state clean Delete states older than 7 days
|
|
475
|
+
browse state clean --older-than N Custom age threshold (days)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Cookie import (macOS — borrow auth from real browsers)
|
|
479
|
+
```
|
|
480
|
+
browse cookie-import --list List installed browsers
|
|
481
|
+
browse cookie-import <browser> --domain <d> Import cookies for a domain
|
|
482
|
+
browse cookie-import <browser> --profile <p> --domain <d> Specific Chrome profile
|
|
397
483
|
```
|
|
398
484
|
|
|
399
485
|
### Auth vault
|
|
@@ -417,11 +503,22 @@ browse video stop Stop recording and save video files
|
|
|
417
503
|
browse video status Check if recording is active
|
|
418
504
|
```
|
|
419
505
|
|
|
506
|
+
### Command recording & export
|
|
507
|
+
```
|
|
508
|
+
browse record start Start recording commands
|
|
509
|
+
browse record stop Stop recording, keep steps for export
|
|
510
|
+
browse record status Recording state and step count
|
|
511
|
+
browse record export browse [path] Export as chain-compatible JSON (replay with browse chain)
|
|
512
|
+
browse record export replay [path] Export as Chrome DevTools Recorder (Playwright/Puppeteer)
|
|
513
|
+
```
|
|
514
|
+
|
|
420
515
|
### Server management
|
|
421
516
|
```
|
|
422
517
|
browse status Server health, uptime, session count
|
|
423
518
|
browse instances List all running browse servers (instance, PID, port, status)
|
|
424
519
|
browse version Print CLI version
|
|
520
|
+
browse doctor System check (Bun, Playwright, Chromium)
|
|
521
|
+
browse upgrade Self-update via npm
|
|
425
522
|
browse stop Shutdown server
|
|
426
523
|
browse restart Kill + restart server
|
|
427
524
|
browse inspect Open DevTools (requires BROWSE_DEBUG_PORT)
|
|
@@ -431,10 +528,12 @@ browse inspect Open DevTools (requires BROWSE_DEBUG_PORT)
|
|
|
431
528
|
|
|
432
529
|
| Flag | Description |
|
|
433
530
|
|------|-------------|
|
|
434
|
-
| `--session <id>` | Named session (isolates tabs, refs, cookies) |
|
|
531
|
+
| `--session <id>` | Named session (isolates tabs, refs, cookies — auto-persists on close) |
|
|
532
|
+
| `--state <path>` | Load state file (cookies/storage) before first command |
|
|
435
533
|
| `--json` | Wrap output as `{success, data, command}` |
|
|
436
534
|
| `--content-boundaries` | Wrap page content in nonce-delimited markers (prompt injection defense) |
|
|
437
535
|
| `--allowed-domains <d,d>` | Block navigation/resources outside allowlist |
|
|
536
|
+
| `--max-output <n>` | Truncate output to N characters |
|
|
438
537
|
| `--headed` | Run browser in headed (visible) mode |
|
|
439
538
|
| `--runtime <name>` | Browser engine: playwright (default), rebrowser (stealth) |
|
|
440
539
|
|
|
@@ -480,6 +579,7 @@ browse inspect Open DevTools (requires BROWSE_DEBUG_PORT)
|
|
|
480
579
|
| Auto-login | `auth save gh https://github.com/login user pass` → `auth login gh` |
|
|
481
580
|
| Record network | `har start` → browse around → `har stop ./out.har` |
|
|
482
581
|
| Record video | `video start ./vids` → browse around → `video stop` |
|
|
582
|
+
| Export automation script | `record start` → browse around → `record export replay ./recording.json` |
|
|
483
583
|
| Parallel agents | `--session agent-a <cmd>` / `--session agent-b <cmd>` |
|
|
484
584
|
| Multi-step flow | `echo '[...]' \| browse chain` |
|
|
485
585
|
| Secure browsing | `--allowed-domains example.com goto https://example.com` |
|
|
@@ -489,6 +589,14 @@ browse inspect Open DevTools (requires BROWSE_DEBUG_PORT)
|
|
|
489
589
|
| Find by accessibility | `find role button` / `find text "Submit"` |
|
|
490
590
|
| Visual regression | `screenshot-diff baseline.png` |
|
|
491
591
|
| Debug with DevTools | `inspect` (set BROWSE_DEBUG_PORT first) |
|
|
592
|
+
| Get element position | `box @e3` |
|
|
593
|
+
| Check page errors | `errors` |
|
|
594
|
+
| Right-click context menu | `rightclick @e3` |
|
|
595
|
+
| Test mobile gestures | `emulate iphone` → `tap @e1` / `swipe down` |
|
|
596
|
+
| Set dark mode | `set media dark` |
|
|
597
|
+
| Test geolocation | `set geo 37.7 -122.4` → verify in page |
|
|
598
|
+
| Export/import cookies | `cookie export ./cookies.json` / `cookie import ./cookies.json` |
|
|
599
|
+
| Limit output size | `--max-output 5000 text` |
|
|
492
600
|
| See the browser | `browse --headed goto <url>` |
|
|
493
601
|
| Bypass bot detection | `--runtime rebrowser goto <url>` |
|
|
494
602
|
|
package/src/auth-vault.ts
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* Password never returned in list/get — only hasPassword: true
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import * as crypto from 'crypto';
|
|
12
11
|
import * as fs from 'fs';
|
|
13
12
|
import * as path from 'path';
|
|
14
13
|
import type { BrowserManager } from './browser-manager';
|
|
15
14
|
import { DEFAULTS } from './constants';
|
|
15
|
+
import { resolveEncryptionKey, encrypt, decrypt } from './encryption';
|
|
16
16
|
import { sanitizeName } from './sanitize';
|
|
17
17
|
|
|
18
18
|
interface StoredCredential {
|
|
@@ -44,55 +44,7 @@ export class AuthVault {
|
|
|
44
44
|
|
|
45
45
|
constructor(localDir: string) {
|
|
46
46
|
this.authDir = path.join(localDir, 'auth');
|
|
47
|
-
this.encryptionKey =
|
|
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');
|
|
47
|
+
this.encryptionKey = resolveEncryptionKey(localDir);
|
|
96
48
|
}
|
|
97
49
|
|
|
98
50
|
save(
|
|
@@ -104,7 +56,7 @@ export class AuthVault {
|
|
|
104
56
|
): void {
|
|
105
57
|
fs.mkdirSync(this.authDir, { recursive: true });
|
|
106
58
|
|
|
107
|
-
const { ciphertext, iv, authTag } =
|
|
59
|
+
const { ciphertext, iv, authTag } = encrypt(password, this.encryptionKey);
|
|
108
60
|
const now = new Date().toISOString();
|
|
109
61
|
|
|
110
62
|
const credential: StoredCredential = {
|
|
@@ -136,7 +88,7 @@ export class AuthVault {
|
|
|
136
88
|
|
|
137
89
|
async login(name: string, bm: BrowserManager): Promise<string> {
|
|
138
90
|
const cred = this.load(name);
|
|
139
|
-
const password =
|
|
91
|
+
const password = decrypt(cred.data, cred.iv, cred.authTag, this.encryptionKey);
|
|
140
92
|
const page = bm.getPage();
|
|
141
93
|
|
|
142
94
|
// Navigate to login URL
|
package/src/browser-manager.ts
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
* We do NOT try to self-heal — don't hide failure.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { devices as playwrightDevices, type Browser, type BrowserContext, type Page, type Locator, type Frame, type FrameLocator, type Request as PlaywrightRequest } from 'playwright';
|
|
11
|
-
import { getRuntime } from './runtime';
|
|
10
|
+
import { chromium, devices as playwrightDevices, type Browser, type BrowserContext, type Page, type Locator, type Frame, type FrameLocator, type Request as PlaywrightRequest } from 'playwright';
|
|
12
11
|
import { SessionBuffers, type LogEntry, type NetworkEntry } from './buffers';
|
|
13
12
|
import type { HarRecording } from './har';
|
|
14
13
|
import type { DomainFilter } from './domain-filter';
|
|
@@ -203,9 +202,8 @@ export class BrowserManager {
|
|
|
203
202
|
* Launch a new Chromium browser (single-session / multi-process mode).
|
|
204
203
|
* This instance owns the browser and will close it on close().
|
|
205
204
|
*/
|
|
206
|
-
async launch(onCrash?: () => void
|
|
207
|
-
|
|
208
|
-
this.browser = await runtime.chromium.launch({ headless: true });
|
|
205
|
+
async launch(onCrash?: () => void) {
|
|
206
|
+
this.browser = await chromium.launch({ headless: true });
|
|
209
207
|
this.ownsBrowser = true;
|
|
210
208
|
|
|
211
209
|
// Chromium crash → notify caller (server uses this to exit; tests ignore it)
|
|
@@ -485,6 +483,23 @@ export class BrowserManager {
|
|
|
485
483
|
return { selector };
|
|
486
484
|
}
|
|
487
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Resolve a ref with staleness detection. Throws immediately if the ref's
|
|
488
|
+
* element no longer exists in the DOM, instead of waiting for action timeout.
|
|
489
|
+
*/
|
|
490
|
+
async resolveRefChecked(selector: string): Promise<{ locator: Locator } | { selector: string }> {
|
|
491
|
+
const resolved = this.resolveRef(selector);
|
|
492
|
+
if ('locator' in resolved) {
|
|
493
|
+
const count = await resolved.locator.count();
|
|
494
|
+
if (count === 0) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`Ref ${selector} is stale (element no longer exists). Re-run 'snapshot' to get fresh refs.`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return resolved;
|
|
501
|
+
}
|
|
502
|
+
|
|
488
503
|
getRefCount(): number {
|
|
489
504
|
return this.refMap.size;
|
|
490
505
|
}
|
package/src/bun.d.ts
CHANGED
|
@@ -16,21 +16,9 @@ declare module 'bun' {
|
|
|
16
16
|
stdio?: Array<'ignore' | 'pipe' | 'inherit'>;
|
|
17
17
|
stdout?: 'pipe' | 'ignore' | 'inherit';
|
|
18
18
|
stderr?: 'pipe' | 'ignore' | 'inherit';
|
|
19
|
-
stdin?: 'pipe' | 'ignore' | 'inherit';
|
|
20
19
|
env?: Record<string, string | undefined>;
|
|
21
20
|
}): BunSubprocess;
|
|
22
21
|
|
|
23
|
-
export function listen(options: {
|
|
24
|
-
hostname: string;
|
|
25
|
-
port: number;
|
|
26
|
-
socket: {
|
|
27
|
-
data: (...args: any[]) => void;
|
|
28
|
-
open?: (...args: any[]) => void;
|
|
29
|
-
close?: (...args: any[]) => void;
|
|
30
|
-
error?: (...args: any[]) => void;
|
|
31
|
-
};
|
|
32
|
-
}): BunTCPServer;
|
|
33
|
-
|
|
34
22
|
export function sleep(ms: number): Promise<void>;
|
|
35
23
|
|
|
36
24
|
export const stdin: { text(): Promise<string> };
|
|
@@ -40,26 +28,33 @@ declare module 'bun' {
|
|
|
40
28
|
stop(): void;
|
|
41
29
|
}
|
|
42
30
|
|
|
43
|
-
interface BunTCPServer {
|
|
44
|
-
port: number;
|
|
45
|
-
stop(): void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
31
|
interface BunSubprocess {
|
|
49
32
|
pid: number;
|
|
50
|
-
exitCode: number | null;
|
|
51
33
|
stderr: ReadableStream<Uint8Array> | null;
|
|
52
34
|
stdout: ReadableStream<Uint8Array> | null;
|
|
53
35
|
exited: Promise<number>;
|
|
54
|
-
kill(signal?: number): void;
|
|
55
36
|
unref(): void;
|
|
37
|
+
kill(signal?: number): void;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
declare module 'bun:sqlite' {
|
|
42
|
+
export class Database {
|
|
43
|
+
constructor(filename: string, options?: { readonly?: boolean; create?: boolean });
|
|
44
|
+
query(sql: string): Statement;
|
|
45
|
+
close(): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Statement {
|
|
49
|
+
all(...params: any[]): any[];
|
|
50
|
+
get(...params: any[]): any;
|
|
51
|
+
run(...params: any[]): void;
|
|
56
52
|
}
|
|
57
53
|
}
|
|
58
54
|
|
|
59
55
|
declare var Bun: {
|
|
60
56
|
serve: typeof import('bun').serve;
|
|
61
57
|
spawn: typeof import('bun').spawn;
|
|
62
|
-
listen: typeof import('bun').listen;
|
|
63
58
|
sleep: typeof import('bun').sleep;
|
|
64
59
|
stdin: typeof import('bun').stdin;
|
|
65
60
|
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover a running Chrome instance for CDP connection.
|
|
3
|
+
*
|
|
4
|
+
* Strategy (first match wins):
|
|
5
|
+
* 1. Read DevToolsActivePort file from known browser profile paths
|
|
6
|
+
* 2. Probe well-known CDP ports (9222, 9229)
|
|
7
|
+
* 3. Return null if nothing found
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as os from 'os';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
|
|
14
|
+
const PROFILE_PATHS = [
|
|
15
|
+
'Google/Chrome',
|
|
16
|
+
'Arc/User Data',
|
|
17
|
+
'BraveSoftware/Brave-Browser',
|
|
18
|
+
'Microsoft Edge',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const PROBE_PORTS = [9222, 9229];
|
|
22
|
+
|
|
23
|
+
/** Fetch the CDP WebSocket URL from a Chrome /json/version endpoint. */
|
|
24
|
+
async function fetchWsUrl(port: number): Promise<string | null> {
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
|
|
27
|
+
signal: AbortSignal.timeout(2000),
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok) return null;
|
|
30
|
+
const data = (await res.json()) as { webSocketDebuggerUrl?: string };
|
|
31
|
+
return data.webSocketDebuggerUrl ?? null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Read a DevToolsActivePort file and extract the port number. */
|
|
38
|
+
function readDevToolsPort(filePath: string): number | null {
|
|
39
|
+
try {
|
|
40
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
41
|
+
const port = parseInt(content.split('\n')[0], 10);
|
|
42
|
+
return Number.isFinite(port) && port > 0 ? port : null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Discover a running Chrome instance and return its CDP WebSocket URL.
|
|
50
|
+
* Returns null if no reachable Chrome is found.
|
|
51
|
+
*/
|
|
52
|
+
export async function discoverChrome(): Promise<string | null> {
|
|
53
|
+
const home = os.homedir();
|
|
54
|
+
|
|
55
|
+
// 1. Try DevToolsActivePort files
|
|
56
|
+
for (const profile of PROFILE_PATHS) {
|
|
57
|
+
const filePath = path.join(home, 'Library', 'Application Support', profile, 'DevToolsActivePort');
|
|
58
|
+
const port = readDevToolsPort(filePath);
|
|
59
|
+
if (port) {
|
|
60
|
+
const wsUrl = await fetchWsUrl(port);
|
|
61
|
+
if (wsUrl) return wsUrl;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. Probe well-known ports
|
|
66
|
+
for (const port of PROBE_PORTS) {
|
|
67
|
+
const wsUrl = await fetchWsUrl(port);
|
|
68
|
+
if (wsUrl) return wsUrl;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Nothing found
|
|
72
|
+
return null;
|
|
73
|
+
}
|