surf-cli 2.17.0 → 2.19.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 +135 -9
- package/agents/gpt-pro.md +2 -2
- package/native/browser-session-store.cjs +30 -2
- package/native/chatgpt-client-selection.cjs +79 -35
- package/native/chatgpt-client-ui.cjs +313 -198
- package/native/chatgpt-client.cjs +7 -2
- package/native/cli.cjs +443 -13
- package/native/doctor.cjs +11 -2
- package/native/endpoint.cjs +121 -36
- package/native/extract.cjs +362 -0
- package/native/file-transfer.cjs +5 -1
- package/native/host-helpers.cjs +40 -3
- package/native/host-sessions.cjs +10 -0
- package/native/host.cjs +451 -26
- package/native/mcp-server.cjs +26 -1
- package/native/oracle-cli.cjs +2 -2
- package/native/script-options.cjs +33 -0
- package/native/socket-permissions.cjs +114 -0
- package/native/stdin-frames.cjs +33 -0
- package/native/tool-scope.cjs +8 -4
- package/native/video-recorder.cjs +444 -0
- package/native/workflow-definition.cjs +5 -0
- package/package.json +6 -6
- package/scripts/install-native-host.cjs +53 -6
- package/skills/surf/SKILL.md +25 -3
package/README.md
CHANGED
|
@@ -135,6 +135,30 @@ SURF_REMOTE_CREDENTIAL=~/.config/surf/agent-macbook.json \
|
|
|
135
135
|
surf tab.list
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
For a TLS-terminating reverse proxy in front of the native host's existing clear-TCP
|
|
139
|
+
listener, opt in on the client:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
surf --remote surf.example.com:443 --remote-tls \
|
|
143
|
+
--remote-credential ~/.config/surf/agent-macbook.json tab.list
|
|
144
|
+
|
|
145
|
+
# Private CA and an IP destination with a DNS certificate identity
|
|
146
|
+
surf --remote 100.101.102.103:443 --remote-tls \
|
|
147
|
+
--remote-tls-ca ~/.config/surf/private-ca.pem \
|
|
148
|
+
--remote-tls-server-name surf.example.com \
|
|
149
|
+
--remote-credential ~/.config/surf/agent-macbook.json tab.list
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`--remote-tls-ca` replaces Node's system roots rather than adding to them. DNS endpoints
|
|
153
|
+
use their hostname for SNI and certificate validation; IP endpoints omit SNI and validate
|
|
154
|
+
the certificate's IP SAN unless `--remote-tls-server-name` supplies a DNS identity. TLS
|
|
155
|
+
certificate validation finishes before the mandatory Ed25519 authentication. Surf has no
|
|
156
|
+
insecure mode, downgrade, or plaintext retry. `SURF_REMOTE_TLS=1` is the only accepted
|
|
157
|
+
environment spelling; unset it to disable TLS. There is no CLI negation for env-enabled TLS.
|
|
158
|
+
CLI values override `SURF_REMOTE_TLS_CA` and `SURF_REMOTE_TLS_SERVER_NAME` independently.
|
|
159
|
+
`SURF_LISTEN` remains a plaintext listener behind the reverse proxy; Surf does not terminate
|
|
160
|
+
TLS on the browser host.
|
|
161
|
+
|
|
138
162
|
Surf performs mutual Ed25519 challenge-response with fresh nonces and checks authorization throughout the connection. A credential grants the same browser and host-file authority as a trusted local Surf user. Give each client its own credential, do not share it, and revoke it immediately if the client or file is lost:
|
|
139
163
|
|
|
140
164
|
```bash
|
|
@@ -146,6 +170,8 @@ surf remote list
|
|
|
146
170
|
|
|
147
171
|
`surf install --listen` persists the explicit Tailnet address in the native-host wrapper. Re-run `surf install` without `--listen` to remove it. The address must be a Tailscale IPv4 or IPv6 address with a port; Surf does not bind every interface. Remote listeners currently require a POSIX browser host and are not supported by Windows native-host wrappers.
|
|
148
172
|
|
|
173
|
+
For advanced local-only sharing, `surf install <extension-id> --socket-mode 660 --socket-group <group>` persists an opt-in group-owned socket. The default remains mode `600`; mode `660` gives every account in that group full Surf authority, so use a dedicated narrow group. Re-run `surf install` without these flags to clear persisted socket settings. Remote Surf credentials remain the revocable per-client alternative.
|
|
174
|
+
|
|
149
175
|
Keep Tailscale policy restrictions as defense in depth. For example:
|
|
150
176
|
|
|
151
177
|
```json
|
|
@@ -160,7 +186,9 @@ Keep Tailscale policy restrictions as defense in depth. For example:
|
|
|
160
186
|
}
|
|
161
187
|
```
|
|
162
188
|
|
|
163
|
-
Adapt tags and ports to your Tailnet. Surf authentication does not replace Tailnet policy
|
|
189
|
+
Adapt tags and ports to your Tailnet. Surf authentication does not replace Tailnet policy.
|
|
190
|
+
Optional outbound remote TLS protects the client-to-proxy connection; Surf does not add an
|
|
191
|
+
SSH tunnel or a TLS listener.
|
|
164
192
|
|
|
165
193
|
**Operations and troubleshooting**
|
|
166
194
|
|
|
@@ -188,7 +216,7 @@ surf --remote "$SURF_REMOTE" --remote-credential "$SURF_REMOTE_CREDENTIAL" \
|
|
|
188
216
|
|
|
189
217
|
Client-local inputs are staged privately on the host and removed after the request. Client-local outputs are downloaded with size/hash verification and atomic destination replacement. `surf js --file` and `perf-audit --output` are handled by the client itself. `network.export` defaults to a generated client-local `.json`, `.jsonl`, or `.har` path. Gemini edits default to client-local `edited.png`. Successful remote actions transfer their automatic screenshot to a generated client-local path; `--auto-capture` on failure remains a separate screenshot and console diagnostic.
|
|
190
218
|
|
|
191
|
-
The remote single-file boundary supports one `upload` file, one ChatGPT attachment, or one Gemini attachment/edit input, plus one screenshot, network export, or Gemini image output. Transfers are limited to 256 MiB per file, 512 MiB and 32 files per connection, with 256 KiB decoded chunks. Remote `record`, `aistudio.build`, smoke screenshot directories, directory transfer, and multi-file inputs are intentionally rejected. A `remote:` path bypasses transfer and gives the trusted client direct authority over that absolute host path.
|
|
219
|
+
The remote single-file boundary supports one `upload` file, one ChatGPT attachment, or one Gemini attachment/edit input, plus one screenshot, network export, or Gemini image output. Transfers are limited to 256 MiB per file, 512 MiB and 32 files per connection, with 256 KiB decoded chunks. Remote `record`, `video`, `aistudio.build`, smoke screenshot directories, directory transfer, and multi-file inputs are intentionally rejected. A `remote:` path bypasses transfer and gives the trusted client direct authority over that absolute host path.
|
|
192
220
|
|
|
193
221
|
### Development Setup
|
|
194
222
|
|
|
@@ -278,6 +306,13 @@ surf locate.role button --action click
|
|
|
278
306
|
surf frame.main # Return to main page
|
|
279
307
|
```
|
|
280
308
|
|
|
309
|
+
When a selector never matches, `frame.diagnose` shows the three frame views side by side (DOM `<iframe>` elements, the extension's frames with content-script reachability, and the CDP frame tree) and explains the mismatches: `srcdoc`/`about:blank` frames (matched to their CDP frame by `name`/`id`), sandboxes without `allow-scripts`, cross-origin frames, out-of-process frames that the CDP tree does not list (`frame.js` cannot reach them; `frame.switch` and `page.read` can when the content script answers), and frames still loading. The DOM inventory walks open shadow roots, so frames rendered by custom elements are listed with their `shadowHost` path. The text report abbreviates long frame URLs; `--json` keeps them whole.
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
surf frame.diagnose # Human-readable report with warnings
|
|
313
|
+
surf frame.diagnose --json # Full inventories
|
|
314
|
+
```
|
|
315
|
+
|
|
281
316
|
### Interaction
|
|
282
317
|
|
|
283
318
|
```bash
|
|
@@ -378,10 +413,14 @@ SURF_SESSION=research surf screenshot # environment selector
|
|
|
378
413
|
surf session.list --refresh # all bindings + queue state
|
|
379
414
|
surf session.info research --refresh # target and scheduler details
|
|
380
415
|
surf session.close research # closes Surf-created target
|
|
416
|
+
surf session.cleanup --idle-after 1h --dry-run # preview forgotten sessions
|
|
417
|
+
surf session.cleanup --idle-after 1h # remove idle bindings
|
|
381
418
|
surf session.rebind research --tab-id 789 # adopt an existing tab
|
|
382
419
|
surf session.reopen research # recreate from last URL
|
|
383
420
|
```
|
|
384
421
|
|
|
422
|
+
`session.cleanup` is an explicit, one-shot cleanup operation; `--idle-after` is required. It inspects current bindings first, removes gone or stale records, and removes live inactive bindings older than the threshold. Only Surf-created targets are closed. Adopted targets remain open while their session binding is removed. Use `--dry-run` to report the exact bindings and target actions without changing the store or browser.
|
|
423
|
+
|
|
385
424
|
Commands for the same session tab run FIFO. Commands for different session tabs can overlap. Browser-wide mutations—such as creating, moving, closing, or focusing tabs/windows and writing cookies—wait for active tab lanes to drain. Add `--no-wait` to return `tab_busy` or `browser_busy` immediately instead of queueing.
|
|
386
425
|
|
|
387
426
|
Recovery errors print an exact command that can be copied directly:
|
|
@@ -440,6 +479,19 @@ surf record --rect 0,200,1440,800 --output /tmp/region.gif
|
|
|
440
479
|
|
|
441
480
|
`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`.
|
|
442
481
|
|
|
482
|
+
### Local Video Recording
|
|
483
|
+
|
|
484
|
+
Capture a local tab as WebM/VP9 with ffmpeg:
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
surf video start ./demo.webm --fps 30
|
|
488
|
+
surf video status
|
|
489
|
+
surf video stop
|
|
490
|
+
surf video restart ./take-2.webm --fps 60
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
`--fps` defaults to 30 and is capped at 60. Video recording is local-only and requires `ffmpeg` on `PATH`.
|
|
494
|
+
|
|
443
495
|
### Animation Audit
|
|
444
496
|
|
|
445
497
|
Sample matching elements over time and return a bounded JSON timeline for agent inspection:
|
|
@@ -526,7 +578,9 @@ surf aistudio.build "game" --keep-open --timeout 600 # Keep tab open, 1
|
|
|
526
578
|
|
|
527
579
|
#### Oracle
|
|
528
580
|
|
|
529
|
-
Use `surf oracle` for a durable, local ChatGPT consult instead of a quick `surf chatgpt` one-shot. It persists jobs by conversation URL, supports repeatable file-context globs, one direct local attachment with `--file`, and verifies requested model and reasoning effort before submission. Add `--github` when the consult needs the ChatGPT Chat tab and connected GitHub tool. ChatGPT model aliases include `
|
|
581
|
+
Use `surf oracle` for a durable, local ChatGPT consult instead of a quick `surf chatgpt` one-shot. It persists jobs by conversation URL, supports repeatable file-context globs, one direct local attachment with `--file`, and verifies requested model and reasoning effort before submission. Add `--github` when the consult needs the ChatGPT Chat tab and connected GitHub tool. ChatGPT model aliases include `gpt-6-astra`, `latest`, `gpt-5.6-sol`, and `gpt-5.5`; `latest` is an explicit floating choice, while `gpt-6-astra` must read back as model 6 before submission. Accepted efforts are `instant`, `medium`, `high`, `xhigh`/`extra-high`, and `pro`. Use `--model gpt-6-astra --effort pro` for GPT-6 Astra with Pro effort.
|
|
582
|
+
|
|
583
|
+
ChatGPT can hide the model version at lower effort settings. Use `--model latest` if you want floating model selection there; an explicit `gpt-6-astra` request fails closed when the UI cannot verify model 6.
|
|
530
584
|
|
|
531
585
|
```bash
|
|
532
586
|
surf oracle ask "review this change" --files "src/**/*.ts" --file ./design.md --model gpt-5.5 --effort pro --github --detach --json
|
|
@@ -548,12 +602,55 @@ surf wait 2 # Wait 2 seconds
|
|
|
548
602
|
surf wait.element ".loaded" # Wait for element
|
|
549
603
|
surf wait.network # Wait for network idle
|
|
550
604
|
surf wait.url "/dashboard" # Wait for URL pattern
|
|
605
|
+
surf wait.ready --selector ".results" # Wait for content, fail fast on a bounce
|
|
606
|
+
surf page.readiness --json # Classify the page once
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
`wait.ready` polls with a bounded budget and reports a typed state instead of timing out silently: `ready`, `empty` (the page showed its own no-results message, `--empty-text`), or one of the negative states `login`, `challenge` (anti-bot interstitial), `not-found`, `error`. A negative state exits non-zero with codes `page_login`, `page_challenge`, `page_not_found`, `page_error`; `page_timeout` reports the last observed state. Pass `--accept login` to return a state to the caller instead. Detection uses visible UI state (a rendered password field, a login-looking route, the page's own wording, `--url-prefix` bounces), never site-specific selectors.
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
surf wait.ready --url-prefix "https://app.example.com/" --empty-text "No results"
|
|
613
|
+
surf wait.ready --accept login --json # {"state":"login","evidence":[...]} instead of an error
|
|
551
614
|
```
|
|
552
615
|
|
|
616
|
+
### Extracting structured data
|
|
617
|
+
|
|
618
|
+
`surf extract` composes existing client-side tools: it opens an owned tab,
|
|
619
|
+
waits for explicit readiness, runs a page script, validates its JSON result,
|
|
620
|
+
and closes the tab. It prints concise Markdown by default or structured JSON
|
|
621
|
+
with `--json`. Return an array, or an object containing a conventional row key
|
|
622
|
+
such as `rows`, `items`, or `results`; use `--rows <key>` for another key.
|
|
623
|
+
|
|
624
|
+
```bash
|
|
625
|
+
surf extract "https://example.com/list" --file rows.js --ready-selector ".item"
|
|
626
|
+
surf extract "https://example.com/search" --file rows.js --options '{"limit":20}' --empty-text "No results" --json
|
|
627
|
+
surf extract --tab-id 42 --code 'return [...document.querySelectorAll("h2")].map(h => ({title: h.textContent}))'
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Owned-tab failures always attempt cleanup. Zero rows retry unless
|
|
631
|
+
`--allow-empty` is set or `--empty-text` identifies the page's accepted empty
|
|
632
|
+
state. Fresh-tab retries are bounded (`--retry`, default 1, maximum 5) and are
|
|
633
|
+
limited to readiness timeouts, zero rows, and lost tab/execution-context
|
|
634
|
+
failures. Login, challenge, not-found, page-error, script/output, and cleanup
|
|
635
|
+
failures do not retry. `--tab-id` and `--session` target an existing page and
|
|
636
|
+
never retry or close it; `--keep-tab` preserves a successfully owned tab.
|
|
637
|
+
|
|
638
|
+
Extract is intended for read-only or otherwise idempotent caller scripts.
|
|
639
|
+
JavaScript is not inherently read-only: a retry can replay the script, so avoid
|
|
640
|
+
mutations or make them idempotent.
|
|
641
|
+
|
|
553
642
|
### Other
|
|
554
643
|
|
|
644
|
+
`js` and `frame.js` accept `--options '{"limit": 20}'` with inline code or
|
|
645
|
+
`--file`. This defines `SURF_OPTIONS` by parsing the JSON and freezing the
|
|
646
|
+
result; use an explicit `return` for the script's result. The freeze is shallow.
|
|
647
|
+
Invalid JSON and non-object values are rejected before sending a request;
|
|
648
|
+
`--options ''` defines an empty object. Without `--options`, code is unchanged.
|
|
649
|
+
|
|
555
650
|
```bash
|
|
556
651
|
surf js "return document.title" # Execute JavaScript
|
|
652
|
+
surf js "piHelpers.setValue(document.querySelector('#q'), 'hello')" # Native value setter + input/change events
|
|
653
|
+
surf js --file script.js --options '{"limit": 20}' # Script reads SURF_OPTIONS.limit
|
|
557
654
|
surf record --duration 2000 --fps 10 --output /tmp/anim.gif # Animated GIF capture
|
|
558
655
|
surf animate-audit --selector ".thing" --duration 2000 --fps 10 # JSON animation timeline
|
|
559
656
|
surf perf-audit --duration 3000 --output /tmp/perf.json # PerformanceObserver snapshot
|
|
@@ -790,12 +887,32 @@ Generated manifests declare provenance and authentication environment inputs. Su
|
|
|
790
887
|
--window-id <id> # Target a specific window
|
|
791
888
|
--no-wait # Return tab_busy/browser_busy instead of queueing
|
|
792
889
|
--json # Raw JSON including resolved target metadata
|
|
793
|
-
--soft-fail #
|
|
890
|
+
--soft-fail # Host tool errors: stderr warning, exit 0, no JSON error output
|
|
794
891
|
--no-lock # Bypass the legacy lock for compound client-side commands
|
|
795
892
|
--no-screenshot # Skip auto-screenshot after actions
|
|
796
893
|
--full # Full resolution screenshots (skip resize)
|
|
797
894
|
```
|
|
798
895
|
|
|
896
|
+
### Host tool-response errors
|
|
897
|
+
|
|
898
|
+
For ordinary socket-backed commands, a host response with a top-level `error`
|
|
899
|
+
exits 1 and prints `Error: ...` on stderr. A supplied code is appended as `[code]`
|
|
900
|
+
to the first line unless already present there; subsequent recovery lines are
|
|
901
|
+
preserved. Without a code, no suffix is added.
|
|
902
|
+
|
|
903
|
+
`--json` additionally writes `{"error":{"code":"...","message":"...","details":{...}}}`
|
|
904
|
+
to stdout, while retaining stderr and exit 1. The JSON code defaults to `"error"`;
|
|
905
|
+
the message uses the host's message, or the first display line if absent. Optional
|
|
906
|
+
details retain the host's fields except redundant `code` and `message` fields.
|
|
907
|
+
|
|
908
|
+
`--soft-fail` takes precedence: the original host display text is printed as a
|
|
909
|
+
stderr warning, without adding a code, stdout stays empty even with `--json`,
|
|
910
|
+
and the command exits 0. This is **not a universal JSON error envelope**: local
|
|
911
|
+
validation, transport/parser failures, compound commands and errors embedded in
|
|
912
|
+
successful result payloads retain their existing behavior. In particular, a
|
|
913
|
+
connection failure still prints stderr, leaves stdout empty and exits 1 with
|
|
914
|
+
`--json`, even with `--soft-fail`.
|
|
915
|
+
|
|
799
916
|
## Environment Variables
|
|
800
917
|
|
|
801
918
|
```bash
|
|
@@ -805,8 +922,13 @@ SURF_SESSION # Default named browser session for tab-scoped command
|
|
|
805
922
|
SURF_SOCKET # Socket path or named pipe (default: /tmp/surf.sock, Windows: //./pipe/surf)
|
|
806
923
|
SURF_REMOTE # Remote Surf endpoint as host:port (overrides SURF_SOCKET)
|
|
807
924
|
SURF_REMOTE_CREDENTIAL # Client Ed25519 credential for the selected remote endpoint
|
|
925
|
+
SURF_REMOTE_TLS # Exactly 1 enables TLS for a selected remote endpoint
|
|
926
|
+
SURF_REMOTE_TLS_CA # Custom CA bundle that replaces system roots
|
|
927
|
+
SURF_REMOTE_TLS_SERVER_NAME # DNS SNI and certificate identity override
|
|
808
928
|
SURF_REMOTE_STATE_DIR # Host identity/authorization directory (default: ~/.surf/remote)
|
|
809
929
|
SURF_LISTEN # Native-host Tailnet bind address as <tailscale-ip>:<port>
|
|
930
|
+
SURF_SOCKET_MODE # Advanced POSIX local socket mode: 600 (default) or 660
|
|
931
|
+
SURF_SOCKET_GROUP # Group name or numeric gid required with mode 660
|
|
810
932
|
SURF_NODE_PATH # Path to node binary (for native host wrapper)
|
|
811
933
|
SURF_HOST_PATH # Path to native/host.cjs (for native host wrapper)
|
|
812
934
|
SURF_EXTENSION_PATH # Path to extension dist/ directory
|
|
@@ -818,8 +940,12 @@ SURF_EXTENSION_PATH # Path to extension dist/ directory
|
|
|
818
940
|
- `SURF_SOCKET`: Advanced socket override. Set it for both the native host and CLI when separate browser/profile instances need hard isolation.
|
|
819
941
|
- `SURF_REMOTE`: Remote client endpoint. `--remote <host>:<port>` overrides it; both override `SURF_SOCKET`.
|
|
820
942
|
- `SURF_REMOTE_CREDENTIAL`: Credential used for mutual remote authentication. `--remote-credential <path>` overrides it.
|
|
943
|
+
- `SURF_REMOTE_TLS`: Set exactly `1` for TLS through a terminating reverse proxy; `--remote-tls` also enables it and cannot negate an env-enabled setting.
|
|
944
|
+
- `SURF_REMOTE_TLS_CA`: CA bundle for remote TLS, replacing system roots. `--remote-tls-ca <path>` overrides it.
|
|
945
|
+
- `SURF_REMOTE_TLS_SERVER_NAME`: DNS SNI and certificate identity override. `--remote-tls-server-name <name>` overrides it.
|
|
821
946
|
- `SURF_REMOTE_STATE_DIR`: Advanced host-side override for the mode-0700 identity and client registry directory.
|
|
822
947
|
- `SURF_LISTEN`: Native-host listener address on the browser machine. Use `surf install ... --listen <tailscale-ip>:<port>` to persist it in that host's wrapper.
|
|
948
|
+
- `SURF_SOCKET_MODE` / `SURF_SOCKET_GROUP`: Advanced POSIX native-host settings. Use `surf install ... --socket-mode 660 --socket-group <group>` to persist group access; mode `660` grants full Surf authority to every member of that group.
|
|
823
949
|
- `SURF_NODE_PATH` / `SURF_HOST_PATH`: Package manager installs (e.g., Nix) that store binaries in non-standard locations
|
|
824
950
|
- `SURF_EXTENSION_PATH`: Package managers that create stable symlinks instead of changing paths on reinstall
|
|
825
951
|
|
|
@@ -914,11 +1040,11 @@ echo '{"type":"tool_request","method":"execute_tool","params":{"tool":"tab.list"
|
|
|
914
1040
|
| `window.*` | `new`, `list`, `focus`, `close`, `resize` |
|
|
915
1041
|
| `tab.*` | `list`, `new`, `switch`, `close`, `name`, `unname`, `named`, `group`, `ungroup`, `groups`, `reload` |
|
|
916
1042
|
| `scroll.*` | `top`, `bottom`, `to`, `info` |
|
|
917
|
-
| `page.*` | `read`, `text`, `state` |
|
|
1043
|
+
| `page.*` | `read`, `text`, `state`, `readiness` |
|
|
918
1044
|
| `locate.*` | `role`, `text`, `label` |
|
|
919
1045
|
| `element.*` | `styles` |
|
|
920
|
-
| `frame.*` | `list`, `switch`, `main`, `js` |
|
|
921
|
-
| `wait.*` | `element`, `network`, `url`, `dom`, `load` |
|
|
1046
|
+
| `frame.*` | `list`, `diagnose`, `switch`, `main`, `js` |
|
|
1047
|
+
| `wait.*` | `element`, `network`, `url`, `dom`, `load`, `ready` |
|
|
922
1048
|
| `cookie` / `cookie.*` | `list`, `get`, `set`, `clear`, `delete` |
|
|
923
1049
|
| `bookmark.*` | `add`, `remove`, `list` |
|
|
924
1050
|
| `history.*` | `list`, `search` |
|
|
@@ -996,9 +1122,9 @@ pi -e /path/to/surf-cli/pi-extension/surf.ts
|
|
|
996
1122
|
|
|
997
1123
|
It registers `surf_read`, `surf_screenshot`, `surf_click`, `surf_type`, `surf_tool`, and the `surf_oracle_*` tools. Browser calls use Surf's native-host socket, not shell commands. If `pi-subagents/background-work` is installed, the extension also reports active oracle jobs started by that Pi session. Pi still loads the browser tools when pi-subagents is not installed.
|
|
998
1124
|
|
|
999
|
-
The extension also registers a `surf-oracle` external-job provider when a Pi runtime exposes that provider bridge. The provider implements pi-subagents' external-job contract: `start`, `status`, `result`, and `reattach` operations that return `providerJobId`, a contract state (`queued`, `running`, `completed`, `failed`), the durable conversation URL, the captured result text as `output`, and failure code and message when present. It reads `options.model`, `options.effort`, `options.file`, and `options.github` for starts and follow-ups, so a Pi profile can request `model: gpt-
|
|
1125
|
+
The extension also registers a `surf-oracle` external-job provider when a Pi runtime exposes that provider bridge. The provider implements pi-subagents' external-job contract: `start`, `status`, `result`, and `reattach` operations that return `providerJobId`, a contract state (`queued`, `running`, `completed`, `failed`), the durable conversation URL, the captured result text as `output`, and failure code and message when present. It reads `options.model`, `options.effort`, `options.file`, and `options.github` for starts and follow-ups, so a Pi profile can request `model: gpt-6-astra` plus `effort: pro` and reach ChatGPT GPT-6 Astra with Pro effort through Surf, while `github: true` requires Chat mode and the connected GitHub tool. Capacity stays fail-closed: Surf returns the blocking job id instead of silently queueing a second ChatGPT job.
|
|
1000
1126
|
|
|
1001
|
-
When Surf is installed as a Pi package, it also exposes an optional `gpt-pro` package agent for `pi-subagents`. That profile uses `runner.type: external-job`, provider `surf-oracle`, `options.model: gpt-
|
|
1127
|
+
When Surf is installed as a Pi package, it also exposes an optional `gpt-pro` package agent for `pi-subagents`. That profile uses `runner.type: external-job`, provider `surf-oracle`, `options.model: gpt-6-astra`, and `options.effort: pro`. Surf remains useful without Pi or `pi-subagents`; the package agent only wires Surf's browser-backed model alias into Pi's agent picker.
|
|
1002
1128
|
|
|
1003
1129
|
Shell-based agents should select a unique session with `SURF_SESSION` and call `surf session.ensure` before their first browser command. The optional Pi extension still uses its existing socket-tool interface; callers that coordinate several Pi workers should pass explicit tab targets until session selection is exposed by that integration.
|
|
1004
1130
|
|
package/agents/gpt-pro.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gpt-pro
|
|
3
|
-
description: Surf GPT Pro advisor through ChatGPT GPT-
|
|
3
|
+
description: Surf GPT Pro advisor through ChatGPT GPT-6 Astra Pro web mode
|
|
4
4
|
runner:
|
|
5
5
|
type: external-job
|
|
6
6
|
provider: surf-oracle
|
|
7
7
|
options:
|
|
8
|
-
model: gpt-
|
|
8
|
+
model: gpt-6-astra
|
|
9
9
|
effort: pro
|
|
10
10
|
async: true
|
|
11
11
|
systemPromptMode: replace
|
|
@@ -9,6 +9,24 @@ const { surfError } = require("./surf-error.cjs");
|
|
|
9
9
|
|
|
10
10
|
const STORE_VERSION = 1;
|
|
11
11
|
const SESSION_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
12
|
+
const SESSION_DURATION_PATTERN = /^(\d+(?:\.\d+)?)(s|m|h|d)?$/i;
|
|
13
|
+
|
|
14
|
+
function parseDurationMs(value) {
|
|
15
|
+
if (typeof value === "boolean" || value === undefined || value === null) {
|
|
16
|
+
throw new Error("--idle-after requires a positive duration such as 30s, 5m, 1h, or 1d");
|
|
17
|
+
}
|
|
18
|
+
const match = String(value).trim().match(SESSION_DURATION_PATTERN);
|
|
19
|
+
if (!match) {
|
|
20
|
+
throw new Error("--idle-after must be a duration such as 30s, 5m, 1h, or 1d (plain seconds are also accepted)");
|
|
21
|
+
}
|
|
22
|
+
const amount = Number(match[1]);
|
|
23
|
+
const multiplier = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[(match[2] || "s").toLowerCase()];
|
|
24
|
+
const milliseconds = amount * multiplier;
|
|
25
|
+
if (!Number.isFinite(milliseconds) || milliseconds <= 0) {
|
|
26
|
+
throw new Error("--idle-after must be a positive duration");
|
|
27
|
+
}
|
|
28
|
+
return Math.max(1, Math.round(milliseconds));
|
|
29
|
+
}
|
|
12
30
|
|
|
13
31
|
function normalizeName(name) {
|
|
14
32
|
return String(name || "").toLowerCase();
|
|
@@ -114,6 +132,7 @@ class BrowserSessionStore {
|
|
|
114
132
|
browserEpoch: identity.browserEpoch,
|
|
115
133
|
createdAt: timestamp,
|
|
116
134
|
updatedAt: timestamp,
|
|
135
|
+
lastAccessedAt: values.lastAccessedAt || timestamp,
|
|
117
136
|
lastValidatedAt: values.lastValidatedAt || timestamp,
|
|
118
137
|
};
|
|
119
138
|
bucket.sessions[key] = record;
|
|
@@ -137,6 +156,7 @@ class BrowserSessionStore {
|
|
|
137
156
|
browserEpoch: identity.browserEpoch,
|
|
138
157
|
createdAt: existing?.createdAt || timestamp,
|
|
139
158
|
updatedAt: timestamp,
|
|
159
|
+
lastAccessedAt: values.lastAccessedAt || timestamp,
|
|
140
160
|
lastValidatedAt: values.lastValidatedAt || timestamp,
|
|
141
161
|
};
|
|
142
162
|
delete record.invalidReason;
|
|
@@ -149,7 +169,13 @@ class BrowserSessionStore {
|
|
|
149
169
|
update(identity, name, patch) {
|
|
150
170
|
const existing = this.get(identity, name);
|
|
151
171
|
if (!existing) throw surfError("session_unknown", `unknown session: ${name}`, { session: name });
|
|
152
|
-
|
|
172
|
+
const timestamp = this.now();
|
|
173
|
+
return this.replace(identity, name, {
|
|
174
|
+
...existing,
|
|
175
|
+
...patch,
|
|
176
|
+
updatedAt: timestamp,
|
|
177
|
+
lastAccessedAt: patch?.lastAccessedAt || timestamp,
|
|
178
|
+
});
|
|
153
179
|
}
|
|
154
180
|
|
|
155
181
|
remove(identity, name) {
|
|
@@ -212,7 +238,8 @@ class BrowserSessionStore {
|
|
|
212
238
|
let changed = false;
|
|
213
239
|
for (const record of Object.values(bucket.sessions || {})) {
|
|
214
240
|
if (record.tabId !== tabId) continue;
|
|
215
|
-
|
|
241
|
+
const timestamp = this.now();
|
|
242
|
+
Object.assign(record, patch, { updatedAt: timestamp, lastAccessedAt: patch?.lastAccessedAt || timestamp });
|
|
216
243
|
changed = true;
|
|
217
244
|
}
|
|
218
245
|
if (changed) this.save(state);
|
|
@@ -266,6 +293,7 @@ module.exports = {
|
|
|
266
293
|
BrowserSessionStore,
|
|
267
294
|
SESSION_NAME_PATTERN,
|
|
268
295
|
STORE_VERSION,
|
|
296
|
+
parseDurationMs,
|
|
269
297
|
normalizeName,
|
|
270
298
|
validateSessionName,
|
|
271
299
|
};
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
const CHATGPT_EFFORT_CHOICES = ["
|
|
1
|
+
const CHATGPT_EFFORT_CHOICES = ["instant", "medium", "high", "xhigh", "pro"];
|
|
2
|
+
const CHATGPT_EFFORT_VALUE = new Map([
|
|
3
|
+
["instant", 0],
|
|
4
|
+
["medium", 1],
|
|
5
|
+
["high", 2],
|
|
6
|
+
["xhigh", 3],
|
|
7
|
+
["pro", 4],
|
|
8
|
+
]);
|
|
9
|
+
|
|
2
10
|
const CHATGPT_MODEL_ALIASES = new Map([
|
|
3
|
-
["
|
|
4
|
-
["
|
|
5
|
-
["
|
|
6
|
-
["
|
|
7
|
-
["
|
|
8
|
-
["
|
|
11
|
+
["latest", "latest"],
|
|
12
|
+
["gpt6", "gpt6astra"],
|
|
13
|
+
["chatgpt6", "gpt6astra"],
|
|
14
|
+
["gpt6astra", "gpt6astra"],
|
|
15
|
+
["chatgpt6astra", "gpt6astra"],
|
|
16
|
+
["6", "gpt6astra"],
|
|
9
17
|
["55", "gpt55"],
|
|
10
18
|
["gpt55", "gpt55"],
|
|
11
19
|
["chatgpt55", "gpt55"],
|
|
@@ -14,6 +22,15 @@ const CHATGPT_MODEL_ALIASES = new Map([
|
|
|
14
22
|
["chatgpt56sol", "gpt56sol"],
|
|
15
23
|
]);
|
|
16
24
|
|
|
25
|
+
const CHATGPT_EFFORT_ALIASES = new Map([
|
|
26
|
+
["instant", "instant"],
|
|
27
|
+
["medium", "medium"],
|
|
28
|
+
["high", "high"],
|
|
29
|
+
["xhigh", "xhigh"],
|
|
30
|
+
["extrahigh", "xhigh"],
|
|
31
|
+
["pro", "pro"],
|
|
32
|
+
]);
|
|
33
|
+
|
|
17
34
|
function normalizeChatGPTModelChoice(desiredModel) {
|
|
18
35
|
const normalized = String(desiredModel || "")
|
|
19
36
|
.toLowerCase()
|
|
@@ -23,43 +40,68 @@ function normalizeChatGPTModelChoice(desiredModel) {
|
|
|
23
40
|
}
|
|
24
41
|
|
|
25
42
|
function normalizeChatGPTEffortChoice(desiredEffort) {
|
|
26
|
-
const normalized = String(desiredEffort || "")
|
|
27
|
-
|
|
43
|
+
const normalized = String(desiredEffort || "")
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^a-z0-9]/g, "");
|
|
46
|
+
return CHATGPT_EFFORT_ALIASES.get(normalized) || null;
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
function normalizedWords(value) {
|
|
31
50
|
return String(value || "")
|
|
32
51
|
.toLowerCase()
|
|
33
|
-
.replace(/[^a-z0-9]+/g, " ")
|
|
52
|
+
.replace(/[^a-z0-9.]+/g, " ")
|
|
34
53
|
.trim()
|
|
35
54
|
.split(/\s+/)
|
|
36
55
|
.filter(Boolean);
|
|
37
56
|
}
|
|
38
57
|
|
|
58
|
+
function normalizedText(value) {
|
|
59
|
+
return normalizedWords(value).join(" ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function modelKeyFromValue(value) {
|
|
63
|
+
const text = normalizedText(value);
|
|
64
|
+
if (!text) return null;
|
|
65
|
+
if (/^latest$/.test(text)) return "latest";
|
|
66
|
+
if (/\b5\.6\b/.test(text) && /\bsol\b/.test(text)) return "gpt56sol";
|
|
67
|
+
if (/\b5\.5\b/.test(text)) return "gpt55";
|
|
68
|
+
if (/\bgpt\s*6\b/.test(text) || /\bchatgpt\s*6\b/.test(text) || /^6(?:\s|$)/.test(text)) {
|
|
69
|
+
return "gpt6astra";
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
39
74
|
function modelCandidateMatches(item, targetModel) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (targetModel.startsWith("gpt") && normalizedValue.includes(targetModel)) return true;
|
|
45
|
-
const variants = ["instant", "thinking", "pro"].filter((variant) =>
|
|
46
|
-
normalizedWords(value).includes(variant),
|
|
47
|
-
);
|
|
48
|
-
return variants.length === 1 && variants[0] === targetModel;
|
|
49
|
-
});
|
|
75
|
+
if (item?.modelKey === targetModel) return true;
|
|
76
|
+
const values = [item?.label, item?.displayLabel, item?.testId?.replace(/^model-switcher-/, "")]
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
return values.some((value) => modelKeyFromValue(value) === targetModel);
|
|
50
79
|
}
|
|
51
80
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (labelVariants.size > 0) {
|
|
57
|
-
return labelVariants.size === 1 && labelVariants.has(targetEffort);
|
|
81
|
+
function modelSelectableCandidateMatches(item, targetModel) {
|
|
82
|
+
if (modelCandidateMatches(item, targetModel)) return true;
|
|
83
|
+
if (targetModel === "gpt6astra") {
|
|
84
|
+
return [item?.label, item?.displayLabel].filter(Boolean).some((value) => modelKeyFromValue(value) === "latest");
|
|
58
85
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function effortKeyFromValue(value) {
|
|
90
|
+
const text = normalizedText(value);
|
|
91
|
+
if (!text) return null;
|
|
92
|
+
if (/\bextra\s+high\b/.test(text) || /\bxhigh\b/.test(text)) return "xhigh";
|
|
93
|
+
if (/\binstant\b/.test(text)) return "instant";
|
|
94
|
+
if (/\bmedium\b/.test(text)) return "medium";
|
|
95
|
+
if (/\bhigh\b/.test(text)) return "high";
|
|
96
|
+
if (/\bpro\b/.test(text)) return "pro";
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function effortCandidateMatches(item, targetEffort) {
|
|
101
|
+
const labelKey = item?.effortKey || effortKeyFromValue(item?.label || item?.displayLabel);
|
|
102
|
+
if (!labelKey || labelKey !== targetEffort) return false;
|
|
103
|
+
if (item?.value === undefined || item?.value === null) return true;
|
|
104
|
+
return Number(item.value) === CHATGPT_EFFORT_VALUE.get(targetEffort);
|
|
63
105
|
}
|
|
64
106
|
|
|
65
107
|
function uniqueMatch(items, matches) {
|
|
@@ -75,7 +117,7 @@ function resolveChatGPTModelMenuOption(items, desiredModel) {
|
|
|
75
117
|
items,
|
|
76
118
|
(item) =>
|
|
77
119
|
["button", "menuitem", "menuitemradio", "radio"].includes(item?.role) &&
|
|
78
|
-
|
|
120
|
+
modelSelectableCandidateMatches(item, targetModel),
|
|
79
121
|
);
|
|
80
122
|
}
|
|
81
123
|
|
|
@@ -85,7 +127,8 @@ function verifyChatGPTModelSelection(items, desiredModel) {
|
|
|
85
127
|
return uniqueMatch(
|
|
86
128
|
items,
|
|
87
129
|
(item) =>
|
|
88
|
-
(
|
|
130
|
+
(!["menuitemradio", "radio"].includes(item?.role) || item.selected === true) &&
|
|
131
|
+
(typeof item?.label === "string" || typeof item?.displayLabel === "string" || typeof item?.testId === "string") &&
|
|
89
132
|
modelCandidateMatches(item, targetModel),
|
|
90
133
|
);
|
|
91
134
|
}
|
|
@@ -96,7 +139,7 @@ function resolveChatGPTEffortMenuOption(items, desiredEffort) {
|
|
|
96
139
|
return uniqueMatch(
|
|
97
140
|
items,
|
|
98
141
|
(item) =>
|
|
99
|
-
["button", "menuitem", "menuitemradio"].includes(item?.role) &&
|
|
142
|
+
["slider", "button", "menuitem", "menuitemradio"].includes(item?.role) &&
|
|
100
143
|
effortCandidateMatches(item, targetEffort),
|
|
101
144
|
);
|
|
102
145
|
}
|
|
@@ -107,7 +150,7 @@ function verifyChatGPTEffortSelection(items, desiredEffort) {
|
|
|
107
150
|
return uniqueMatch(
|
|
108
151
|
items,
|
|
109
152
|
(item) =>
|
|
110
|
-
(typeof item?.label === "string" || typeof item?.testId === "string") &&
|
|
153
|
+
(typeof item?.label === "string" || typeof item?.displayLabel === "string" || typeof item?.testId === "string") &&
|
|
111
154
|
effortCandidateMatches(item, targetEffort),
|
|
112
155
|
);
|
|
113
156
|
}
|
|
@@ -115,7 +158,7 @@ function verifyChatGPTEffortSelection(items, desiredEffort) {
|
|
|
115
158
|
function boundedOptionLabels(items) {
|
|
116
159
|
if (!Array.isArray(items)) return [];
|
|
117
160
|
return items
|
|
118
|
-
.map((item) => String(item?.label || "").replace(/\s+/g, " ").trim().slice(0, 80))
|
|
161
|
+
.map((item) => String(item?.displayLabel || item?.label || "").replace(/\s+/g, " ").trim().slice(0, 80))
|
|
119
162
|
.filter(Boolean)
|
|
120
163
|
.filter((label, index, labels) => labels.indexOf(label) === index)
|
|
121
164
|
.slice(0, 10);
|
|
@@ -123,6 +166,7 @@ function boundedOptionLabels(items) {
|
|
|
123
166
|
|
|
124
167
|
module.exports = {
|
|
125
168
|
CHATGPT_EFFORT_CHOICES,
|
|
169
|
+
CHATGPT_EFFORT_VALUE,
|
|
126
170
|
boundedOptionLabels,
|
|
127
171
|
normalizeChatGPTEffortChoice,
|
|
128
172
|
normalizeChatGPTModelChoice,
|