surf-cli 2.7.2 → 2.8.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 +110 -9
- package/native/browser-lock.cjs +169 -0
- package/native/chatgpt-client.cjs +18 -1
- package/native/cli.cjs +670 -279
- package/native/config.cjs +2 -2
- package/native/do-executor.cjs +2 -9
- package/native/do-parser.cjs +12 -0
- package/native/doctor.cjs +583 -0
- package/native/gemini-client.cjs +91 -20
- package/native/grok-client.cjs +270 -170
- package/native/host-helpers.cjs +51 -4
- package/native/host.cjs +22 -7
- package/native/mcp-server.cjs +10 -7
- package/native/socket-path.cjs +46 -0
- package/package.json +5 -5
- package/scripts/install-native-host.cjs +155 -53
- package/scripts/uninstall-native-host.cjs +93 -15
- package/skills/surf/SKILL.md +46 -18
package/README.md
CHANGED
|
@@ -76,10 +76,21 @@ surf install <extension-id> # Chrome (default)
|
|
|
76
76
|
surf install <extension-id> --browser brave # Brave
|
|
77
77
|
surf install <extension-id> --browser helium # Helium
|
|
78
78
|
surf install <extension-id> --browser all # All supported browsers
|
|
79
|
+
surf install <extension-id> --target linux # WSLg/Linux browser from WSL2
|
|
79
80
|
```
|
|
80
81
|
|
|
81
82
|
Supported: `chrome`, `chromium`, `brave`, `edge`, `arc`, `helium`
|
|
82
83
|
|
|
84
|
+
**WSL2 with Windows Chrome**
|
|
85
|
+
When you run `surf install <extension-id>` inside WSL2, Surf detects WSL2 and installs a Windows-side native messaging manifest for Windows Chrome/Brave/Edge by default. The generated Windows wrapper launches the WSL2 host with `wsl.exe`, so `surf` commands run inside WSL2 still connect to the WSL socket.
|
|
86
|
+
|
|
87
|
+
If you use a Linux browser inside WSLg instead, install with:
|
|
88
|
+
```bash
|
|
89
|
+
surf install <extension-id> --target linux
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Restart Windows Chrome after installing. If the extension reports `Access to the specified native messaging host is forbidden`, rerun `surf install <extension-id>` from the same WSL distro and confirm the extension ID was copied from `chrome://extensions`.
|
|
93
|
+
|
|
83
94
|
**Package Manager Installs (Nix, Homebrew, etc.)**
|
|
84
95
|
If surf is installed via a package manager that stores binaries in non-standard locations, set these environment variables before running `surf install`:
|
|
85
96
|
```bash
|
|
@@ -94,6 +105,7 @@ See [Environment Variables](#environment-variables) for details.
|
|
|
94
105
|
```bash
|
|
95
106
|
surf uninstall # Chrome only
|
|
96
107
|
surf uninstall --all # All browsers + wrapper files
|
|
108
|
+
surf uninstall --target linux # Remove WSLg/Linux-browser config from WSL2
|
|
97
109
|
```
|
|
98
110
|
|
|
99
111
|
### Development Setup
|
|
@@ -111,6 +123,7 @@ npm run build
|
|
|
111
123
|
```bash
|
|
112
124
|
surf <command> [args] [options]
|
|
113
125
|
surf --help # Basic help
|
|
126
|
+
surf --llm-context # Compact reference for AI agents
|
|
114
127
|
surf --help-full # All 50+ commands
|
|
115
128
|
surf <command> --help # Command details
|
|
116
129
|
surf --find <query> # Search commands
|
|
@@ -185,7 +198,9 @@ surf click 100 200 # Click by coordinates
|
|
|
185
198
|
surf type "hello" --submit # Type and press Enter
|
|
186
199
|
surf type "email@example.com" --ref e12 # Type into specific element
|
|
187
200
|
surf key Escape # Press key
|
|
188
|
-
surf scroll
|
|
201
|
+
surf scroll down 800 # Scroll down 800px
|
|
202
|
+
surf scroll bottom # Scroll to bottom
|
|
203
|
+
surf scroll.bottom # Dot command form also works
|
|
189
204
|
```
|
|
190
205
|
|
|
191
206
|
### Forms
|
|
@@ -221,6 +236,7 @@ surf screenshot --output /tmp/shot.png # Save to specific path
|
|
|
221
236
|
surf screenshot --full --output /tmp/hd.png # Full resolution (skip resize)
|
|
222
237
|
surf screenshot --annotate # With element labels
|
|
223
238
|
surf screenshot --fullpage # Entire page
|
|
239
|
+
surf screenshot --full-page /tmp/full.png # Entire page, save to path
|
|
224
240
|
surf screenshot --no-save # Return base64 + ID only (no file)
|
|
225
241
|
surf snap # Alias for screenshot
|
|
226
242
|
```
|
|
@@ -246,15 +262,19 @@ surf tab.group --name "Work" --color blue
|
|
|
246
262
|
Keep using your browser while the agent works in a separate window:
|
|
247
263
|
|
|
248
264
|
```bash
|
|
249
|
-
# Create
|
|
265
|
+
# Create a separate window for agent work
|
|
250
266
|
surf window.new "https://example.com"
|
|
251
267
|
# Returns: Window 123456 (tab 789)
|
|
252
268
|
|
|
253
|
-
#
|
|
269
|
+
# Target that window or its tab from later commands
|
|
254
270
|
surf click e5 --window-id 123456
|
|
255
|
-
surf read --
|
|
271
|
+
surf read --tab-id 789
|
|
256
272
|
surf tab.new "https://other.com" --window-id 123456
|
|
257
273
|
|
|
274
|
+
# Name tabs when humans or agents need stable aliases
|
|
275
|
+
surf tab.name dashboard --tab-id 789
|
|
276
|
+
surf tab.switch dashboard
|
|
277
|
+
|
|
258
278
|
# Or manage windows directly
|
|
259
279
|
surf window.list # List all windows
|
|
260
280
|
surf window.list --tabs # Include tab details
|
|
@@ -262,6 +282,19 @@ surf window.focus 123456 # Bring window to front
|
|
|
262
282
|
surf window.close 123456 # Close window
|
|
263
283
|
```
|
|
264
284
|
|
|
285
|
+
`window.new`, `--window-id`, `--tab-id`, and named tabs are Surf's supported coordination tools for parallel workflows. They help agents avoid accidentally driving the same visible tab.
|
|
286
|
+
|
|
287
|
+
Surf also serializes non-streaming browser CLI requests per socket with a file-based lock, so two agents sharing the same native host wait instead of interleaving browser commands. Use `--no-lock` only when you intentionally want to bypass the guard for a command.
|
|
288
|
+
|
|
289
|
+
For hard isolation, run separate browser instances/profiles with separate Surf native hosts and socket paths, then point each shell at the matching socket. Each socket has its own independent lock:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
SURF_SOCKET=/tmp/surf-agent-a.sock surf tab.list
|
|
293
|
+
SURF_SOCKET=/tmp/surf-agent-b.sock surf tab.list
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Surf does not yet provide `session.new`, session IDs, or independent per-agent CDP sessions.
|
|
297
|
+
|
|
265
298
|
### Device Emulation
|
|
266
299
|
|
|
267
300
|
Test responsive designs and mobile layouts:
|
|
@@ -283,6 +316,39 @@ surf emulate.touch --enabled false # Disable touch
|
|
|
283
316
|
|
|
284
317
|
Available devices: iPhone 12-14 (Pro/Max), iPhone SE, iPad (Pro/Mini), Pixel 5-7 (Pro), Galaxy S21-S23, Galaxy Tab S7, Nest Hub (Max).
|
|
285
318
|
|
|
319
|
+
### Animation Recording
|
|
320
|
+
|
|
321
|
+
Capture a screenshot burst and assemble it into an animated GIF with ImageMagick:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
surf record --duration 2000 --fps 10 --output /tmp/anim.gif
|
|
325
|
+
surf record --trigger "click:#btn" --output /tmp/click.gif
|
|
326
|
+
surf record --rect 0,200,1440,800 --output /tmp/region.gif
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`record` defaults to 2000ms at 10fps and writes to `/tmp/surf-record-*.gif` when no output is provided. `--duration` is capped at 10000ms and `--fps` is capped at 30. `--trigger` supports `click:<selector>`, `scroll:up|down|left|right|top|bottom`, and `scroll:<selector>` to scroll a container to the bottom before capture. `--rect` crops the GIF using `x,y,width,height`. ImageMagick must be available as `magick` or `convert`.
|
|
330
|
+
|
|
331
|
+
### Animation Audit
|
|
332
|
+
|
|
333
|
+
Sample matching elements over time and return a bounded JSON timeline for agent inspection:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
surf animate-audit --selector ".thing" --duration 2000 --fps 10
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
The command captures rect, opacity, transform, visibility, display, and a short text snippet for up to 25 matching elements per sample. `--selector` is required. `--duration` defaults to 2000ms and is capped at 10000ms; `--fps` defaults to 10 and is capped at 30. This command returns JSON only and does not record GIF/video output.
|
|
340
|
+
|
|
341
|
+
### Performance Audit
|
|
342
|
+
|
|
343
|
+
Capture layout shift, long animation frame, event timing, long task, and paint entries during a short window:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
surf perf-audit --duration 3000 --trigger "click:.cta" --output /tmp/perf.json
|
|
347
|
+
surf perf-audit --duration 1000 --json
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`perf-audit` defaults to 3000ms and is capped at 10000ms. `--trigger` supports the same `click:<selector>` and `scroll:<target>` forms as `record`. `--output` writes the JSON snapshot to disk.
|
|
351
|
+
|
|
286
352
|
### Performance Tracing
|
|
287
353
|
|
|
288
354
|
Capture performance metrics and traces:
|
|
@@ -324,7 +390,7 @@ surf grok "what are the latest AI agent trends on X" # Search X posts
|
|
|
324
390
|
surf grok "analyze @username recent activity" # Profile analysis
|
|
325
391
|
surf grok "summarize this page" --with-page # Include page context
|
|
326
392
|
surf grok "find viral AI posts" --deep-search # DeepSearch mode
|
|
327
|
-
surf grok "quick question" --model fast # Models: auto, fast, expert,
|
|
393
|
+
surf grok "quick question" --model fast # Models: auto, fast, expert, grok-4.20-beta
|
|
328
394
|
surf grok --validate # Check UI and available models
|
|
329
395
|
surf grok --validate --save-models # Save discovered models to settings
|
|
330
396
|
|
|
@@ -342,7 +408,7 @@ surf aistudio.build "game" --keep-open --timeout 600 # Keep tab open, 1
|
|
|
342
408
|
|
|
343
409
|
Each AI tool uses your existing browser login - no API keys needed. Just be logged into the respective service in Chrome (chatgpt.com, gemini.google.com, perplexity.ai, x.com, or aistudio.google.com).
|
|
344
410
|
|
|
345
|
-
**Grok troubleshooting:** If queries fail, run `surf grok --validate` to check if the UI structure changed. Use `--save-models` to update the model cache in `surf.json`. Default model is
|
|
411
|
+
**Grok troubleshooting:** If queries fail, run `surf grok --validate` to check if the UI structure changed. Use `--save-models` to update the model cache in `surf.json`. Default model is `fast`.
|
|
346
412
|
|
|
347
413
|
### Waiting
|
|
348
414
|
|
|
@@ -357,8 +423,11 @@ surf wait.url "/dashboard" # Wait for URL pattern
|
|
|
357
423
|
|
|
358
424
|
```bash
|
|
359
425
|
surf js "return document.title" # Execute JavaScript
|
|
426
|
+
surf record --duration 2000 --fps 10 --output /tmp/anim.gif # Animated GIF capture
|
|
427
|
+
surf animate-audit --selector ".thing" --duration 2000 --fps 10 # JSON animation timeline
|
|
428
|
+
surf perf-audit --duration 3000 --output /tmp/perf.json # PerformanceObserver snapshot
|
|
360
429
|
surf search "login" # Find text in page
|
|
361
|
-
surf cookie
|
|
430
|
+
surf cookie list # List cookies
|
|
362
431
|
surf zoom 1.5 # Set zoom to 150%
|
|
363
432
|
surf console # Read console messages
|
|
364
433
|
surf network # Read network requests
|
|
@@ -529,6 +598,7 @@ surf workflow.validate ./my-workflow.json
|
|
|
529
598
|
--window-id <id> # Target specific window (isolate agent from your browsing)
|
|
530
599
|
--json # Output raw JSON
|
|
531
600
|
--soft-fail # Warn instead of error (exit 0) on restricted pages
|
|
601
|
+
--no-lock # Bypass the per-socket browser request lock
|
|
532
602
|
--no-screenshot # Skip auto-screenshot after actions
|
|
533
603
|
--full # Full resolution screenshots (skip resize)
|
|
534
604
|
--network-path <path> # Custom path for network logs (default: /tmp/surf, or SURF_NETWORK_PATH env)
|
|
@@ -538,12 +608,14 @@ surf workflow.validate ./my-workflow.json
|
|
|
538
608
|
|
|
539
609
|
```bash
|
|
540
610
|
SURF_NETWORK_PATH # Path for network capture logs (default: /tmp/surf)
|
|
611
|
+
SURF_SOCKET # Socket path or named pipe (default: /tmp/surf.sock, Windows: //./pipe/surf)
|
|
541
612
|
SURF_NODE_PATH # Path to node binary (for native host wrapper)
|
|
542
613
|
SURF_HOST_PATH # Path to native/host.cjs (for native host wrapper)
|
|
543
614
|
SURF_EXTENSION_PATH # Path to extension dist/ directory
|
|
544
615
|
```
|
|
545
616
|
|
|
546
617
|
**Use cases:**
|
|
618
|
+
- `SURF_SOCKET`: Advanced socket override. Set it for both the native host and CLI if you need a non-default socket, including separate sockets for separate browser/profile instances in hard-isolated multi-agent workflows. Each socket gets an independent request lock.
|
|
547
619
|
- `SURF_NODE_PATH` / `SURF_HOST_PATH`: Package manager installs (e.g., Nix) that store binaries in non-standard locations
|
|
548
620
|
- `SURF_EXTENSION_PATH`: Package managers that create stable symlinks instead of changing paths on reinstall
|
|
549
621
|
|
|
@@ -554,9 +626,38 @@ export SURF_HOST_PATH=~/.local/share/surf-cli/native/host.cjs
|
|
|
554
626
|
export SURF_EXTENSION_PATH=~/.local/share/surf-cli/extension
|
|
555
627
|
```
|
|
556
628
|
|
|
629
|
+
## Troubleshooting native host connections
|
|
630
|
+
|
|
631
|
+
If a command fails with `Socket connect failed`, start with:
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
surf doctor
|
|
635
|
+
surf doctor --browser all
|
|
636
|
+
surf doctor --json
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
`doctor` does not require a working browser connection. It checks the socket path, native messaging manifest, manifest `allowed_origins`, and wrapper path, then prints targeted next steps.
|
|
640
|
+
|
|
641
|
+
Read the `Attempted socket:` line first. The CLI and native host must agree on the same socket path. By default this is `/tmp/surf.sock` on macOS/Linux/WSL2 and `//./pipe/surf` on Windows.
|
|
642
|
+
|
|
643
|
+
Common fixes:
|
|
644
|
+
- Restart the browser after `surf install <extension-id>`.
|
|
645
|
+
- Confirm the Surf extension is enabled and the extension ID matches the one passed to `surf install`.
|
|
646
|
+
- On WSL2 with Windows Chrome, run `surf install <extension-id>` from WSL2 and restart Windows Chrome. Use `--target linux` only for a Linux browser running inside WSLg.
|
|
647
|
+
- If `SURF_SOCKET` is set, set the same value for both the browser-launched native host and the shell running `surf`.
|
|
648
|
+
|
|
649
|
+
macOS checklist:
|
|
650
|
+
- Confirm Chrome has a native messaging manifest at `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/surf.browser.host.json`.
|
|
651
|
+
- Confirm the manifest `allowed_origins` entry uses the same extension ID shown on `chrome://extensions` for the Surf extension.
|
|
652
|
+
- Reinstall the manifest with `surf install <extension-id>` after copying a fresh extension build or if the extension ID changed.
|
|
653
|
+
- Fully restart Chrome, then reload the Surf extension on `chrome://extensions`.
|
|
654
|
+
- Open the extension service worker from `chrome://extensions` and check its console for native messaging or socket errors.
|
|
655
|
+
- If `SURF_SOCKET` is set in your shell, make sure Chrome launches the native host with the same value; otherwise both sides should use `/tmp/surf.sock`.
|
|
656
|
+
- Run a simple CLI command such as `surf tab.list`; if it fails, compare its `Attempted socket:` line with the socket expected by the native host.
|
|
657
|
+
|
|
557
658
|
## Socket API
|
|
558
659
|
|
|
559
|
-
For programmatic integration, send JSON to `/tmp/surf.sock
|
|
660
|
+
For programmatic integration, send JSON to `/tmp/surf.sock` by default, or to `SURF_SOCKET` when set:
|
|
560
661
|
|
|
561
662
|
```bash
|
|
562
663
|
echo '{"type":"tool_request","method":"execute_tool","params":{"tool":"tab.list","args":{}},"id":"1"}' | nc -U /tmp/surf.sock
|
|
@@ -614,7 +715,7 @@ echo '{"type":"tool_request","method":"execute_tool","params":{"tool":"tab.list"
|
|
|
614
715
|
| `element.*` | `styles` |
|
|
615
716
|
| `frame.*` | `list`, `switch`, `main`, `js` |
|
|
616
717
|
| `wait.*` | `element`, `network`, `url`, `dom`, `load` |
|
|
617
|
-
| `cookie.*` | `list`, `get`, `set`, `clear` |
|
|
718
|
+
| `cookie` / `cookie.*` | `list`, `get`, `set`, `clear`, `delete` |
|
|
618
719
|
| `bookmark.*` | `add`, `remove`, `list` |
|
|
619
720
|
| `history.*` | `list`, `search` |
|
|
620
721
|
| `dialog.*` | `accept`, `dismiss`, `info` |
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const DEFAULT_STALE_MS = 30000;
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 60000;
|
|
7
|
+
const DEFAULT_MIN_WAIT_MS = 50;
|
|
8
|
+
const DEFAULT_MAX_WAIT_MS = 1000;
|
|
9
|
+
|
|
10
|
+
function sleepSync(ms) {
|
|
11
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getBrowserLockDir(socketPath, tempDir) {
|
|
15
|
+
const hash = crypto.createHash("sha256").update(socketPath).digest("hex").slice(0, 16);
|
|
16
|
+
return path.join(tempDir, `surf-lock-${hash}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createToken() {
|
|
20
|
+
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
21
|
+
return crypto.randomBytes(16).toString("hex");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readLockOwner(lockDir) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(path.join(lockDir, "owner.json"), "utf8"));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error && (error.code === "ENOENT" || error instanceof SyntaxError)) return null;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isProcessAlive(pid) {
|
|
34
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
35
|
+
try {
|
|
36
|
+
process.kill(pid, 0);
|
|
37
|
+
return true;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return Boolean(error && error.code === "EPERM");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeLockOwner(lockDir, socketPath, token) {
|
|
44
|
+
fs.writeFileSync(
|
|
45
|
+
path.join(lockDir, "owner.json"),
|
|
46
|
+
JSON.stringify({ pid: process.pid, token, socketPath, createdAt: new Date().toISOString() }),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function removeLockDir(lockDir) {
|
|
51
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getLockTimestamp(lockDir, owner) {
|
|
55
|
+
const target = owner ? path.join(lockDir, "owner.json") : lockDir;
|
|
56
|
+
try {
|
|
57
|
+
return fs.statSync(target).mtimeMs;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error && error.code === "ENOENT") return 0;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ownerMatches(left, right) {
|
|
65
|
+
if (!left && !right) return true;
|
|
66
|
+
return Boolean(left && right && left.token && left.token === right.token);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function tryCreateStaleClaim(lockDir, staleMs, now = Date.now()) {
|
|
70
|
+
const claimDir = path.join(lockDir, "stale-claim");
|
|
71
|
+
try {
|
|
72
|
+
fs.mkdirSync(claimDir);
|
|
73
|
+
return claimDir;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (!error || error.code !== "EEXIST") throw error;
|
|
76
|
+
try {
|
|
77
|
+
if (now - fs.statSync(claimDir).mtimeMs > staleMs) {
|
|
78
|
+
fs.rmSync(claimDir, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
} catch (claimError) {
|
|
81
|
+
if (!claimError || claimError.code !== "ENOENT") throw claimError;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function claimAndRemoveStaleLock(lockDir, staleMs, now = Date.now()) {
|
|
88
|
+
let lockStats;
|
|
89
|
+
try {
|
|
90
|
+
lockStats = fs.statSync(lockDir);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error && error.code === "ENOENT") return false;
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const inspectedOwner = readLockOwner(lockDir);
|
|
97
|
+
if (inspectedOwner && isProcessAlive(inspectedOwner.pid)) return false;
|
|
98
|
+
if (now - getLockTimestamp(lockDir, inspectedOwner) <= staleMs) return false;
|
|
99
|
+
|
|
100
|
+
const claimDir = tryCreateStaleClaim(lockDir, staleMs, now);
|
|
101
|
+
if (!claimDir) return false;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const currentOwner = readLockOwner(lockDir);
|
|
105
|
+
if (!ownerMatches(inspectedOwner, currentOwner)) return false;
|
|
106
|
+
if (currentOwner && isProcessAlive(currentOwner.pid)) return false;
|
|
107
|
+
if (!currentOwner && Date.now() - lockStats.mtimeMs <= staleMs) return false;
|
|
108
|
+
|
|
109
|
+
removeLockDir(lockDir);
|
|
110
|
+
return true;
|
|
111
|
+
} finally {
|
|
112
|
+
try {
|
|
113
|
+
fs.rmSync(claimDir, { recursive: true, force: true });
|
|
114
|
+
} catch {}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function acquireBrowserLock(socketPath, tempDir, options = {}) {
|
|
119
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
120
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
121
|
+
const sleep = options.sleep ?? sleepSync;
|
|
122
|
+
const lockDir = getBrowserLockDir(socketPath, tempDir);
|
|
123
|
+
const startedAt = Date.now();
|
|
124
|
+
let waitMs = options.minWaitMs ?? DEFAULT_MIN_WAIT_MS;
|
|
125
|
+
const maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
126
|
+
|
|
127
|
+
while (true) {
|
|
128
|
+
const token = createToken();
|
|
129
|
+
try {
|
|
130
|
+
fs.mkdirSync(lockDir, { recursive: false, mode: 0o700 });
|
|
131
|
+
try {
|
|
132
|
+
writeLockOwner(lockDir, socketPath, token);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
removeLockDir(lockDir);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
let released = false;
|
|
138
|
+
return {
|
|
139
|
+
lockDir,
|
|
140
|
+
release() {
|
|
141
|
+
if (released) return;
|
|
142
|
+
released = true;
|
|
143
|
+
const owner = readLockOwner(lockDir);
|
|
144
|
+
if (owner && owner.token === token) removeLockDir(lockDir);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (!error || error.code !== "EEXIST") throw error;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (claimAndRemoveStaleLock(lockDir, staleMs)) continue;
|
|
152
|
+
|
|
153
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Timed out waiting for browser lock after ${Math.round(timeoutMs / 1000)}s. Use --no-lock to bypass.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
sleep(waitMs);
|
|
160
|
+
waitMs = Math.min(Math.round(waitMs * 1.5), maxWaitMs);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
DEFAULT_STALE_MS,
|
|
166
|
+
DEFAULT_TIMEOUT_MS,
|
|
167
|
+
acquireBrowserLock,
|
|
168
|
+
getBrowserLockDir,
|
|
169
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
1
3
|
const CHATGPT_URL = "https://chatgpt.com/";
|
|
2
4
|
|
|
3
5
|
const SELECTORS = {
|
|
@@ -645,6 +647,7 @@ async function query(options) {
|
|
|
645
647
|
closeTab,
|
|
646
648
|
cdpEvaluate,
|
|
647
649
|
cdpCommand,
|
|
650
|
+
uploadFile,
|
|
648
651
|
log = () => {},
|
|
649
652
|
} = options;
|
|
650
653
|
const startTime = Date.now();
|
|
@@ -692,7 +695,21 @@ async function query(options) {
|
|
|
692
695
|
log(`Selected model: ${selectedLabel}`);
|
|
693
696
|
}
|
|
694
697
|
if (file) {
|
|
695
|
-
|
|
698
|
+
if (!uploadFile) {
|
|
699
|
+
throw new Error("ChatGPT file upload unavailable: native host did not provide upload callback");
|
|
700
|
+
}
|
|
701
|
+
const files = Array.isArray(file) ? file : [file];
|
|
702
|
+
const absFiles = files.map((filePath) => path.resolve(process.cwd(), filePath));
|
|
703
|
+
log(`Uploading ${absFiles.length} file(s) to ChatGPT...`);
|
|
704
|
+
const uploadResult = await uploadFile(tabId, absFiles);
|
|
705
|
+
if (uploadResult?.error) {
|
|
706
|
+
throw new Error(`ChatGPT file upload failed: ${uploadResult.error}`);
|
|
707
|
+
}
|
|
708
|
+
if (!uploadResult?.success) {
|
|
709
|
+
throw new Error("ChatGPT file upload failed: upload did not report success");
|
|
710
|
+
}
|
|
711
|
+
log("File uploaded, waiting for ChatGPT attachment processing...");
|
|
712
|
+
await delay(1500);
|
|
696
713
|
}
|
|
697
714
|
await typePrompt(cdp, inputCdp, prompt);
|
|
698
715
|
log("Prompt typed");
|