surf-cli 2.14.0 → 2.15.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 +62 -33
- package/agents/gpt-pro.md +19 -0
- package/native/browser-scheduler.cjs +348 -0
- package/native/browser-session-store.cjs +271 -0
- package/native/cli.cjs +331 -60
- package/native/do-executor.cjs +5 -0
- package/native/host-helpers.cjs +19 -3
- package/native/host-sessions.cjs +8 -1
- package/native/host.cjs +766 -19
- package/native/playbook-cli.cjs +16 -3
- package/native/surf-error.cjs +47 -0
- package/native/tool-scope.cjs +107 -0
- package/native/workflow-definition.cjs +7 -0
- package/package.json +8 -2
- package/pi-extension/surf.ts +18 -4
- package/skills/surf/SKILL.md +48 -21
package/native/playbook-cli.cjs
CHANGED
|
@@ -36,6 +36,11 @@ async function requestHost(endpoint, tool, args, options = {}) {
|
|
|
36
36
|
try {
|
|
37
37
|
const request = { type: "tool_request", method: "execute_tool", params: { tool, args }, id: `playbook-${Date.now()}-${Math.random()}` };
|
|
38
38
|
if (options.tabId) request.tabId = options.tabId;
|
|
39
|
+
if (options.session) {
|
|
40
|
+
request.session = options.session;
|
|
41
|
+
request.sessionSource = options.sessionSource || "environment";
|
|
42
|
+
}
|
|
43
|
+
if (options.admission) request.admission = options.admission;
|
|
39
44
|
return unwrapResponse(await transport.request(request, options.timeoutMs || 11 * 60 * 1000));
|
|
40
45
|
} finally {
|
|
41
46
|
await transport.close();
|
|
@@ -48,7 +53,7 @@ function runSpec(argv) {
|
|
|
48
53
|
const parsed = parseCommandArgs(argv.slice(offset));
|
|
49
54
|
const [playbook, op] = parsed.positional;
|
|
50
55
|
if (!playbook || !op) throw new Error(direct ? "Usage: surf use <playbook> <op> [--arg value]" : "Usage: surf pb run <playbook> <op> [--arg value]");
|
|
51
|
-
const reserved = new Set(["json", "no-lock", "tab-id", "write", "repeat", "retry-attempt", "override-in-doubt", "pin-built-in", "allow-script"]);
|
|
56
|
+
const reserved = new Set(["json", "no-lock", "no-wait", "session", "tab-id", "write", "repeat", "retry-attempt", "override-in-doubt", "pin-built-in", "allow-script"]);
|
|
52
57
|
const args = Object.fromEntries(Object.entries(parsed.options).filter(([name]) => !reserved.has(name)));
|
|
53
58
|
return { playbook, op, args, options: parsed.options };
|
|
54
59
|
}
|
|
@@ -73,7 +78,7 @@ function playbookCommandNeedsBrowser(argv) {
|
|
|
73
78
|
return subcommand === "record" && ["start", "stop", "discard"].includes(argv[2]);
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
async function handlePlaybookCli(argv, { endpoint, cwd = process.cwd() }) {
|
|
81
|
+
async function handlePlaybookCli(argv, { endpoint, cwd = process.cwd(), session, sessionSource, admission } = {}) {
|
|
77
82
|
if (!["playbook", "pb", "use"].includes(argv[0])) return { handled: false };
|
|
78
83
|
if (argv[0] === "use" || argv[1] === "run") {
|
|
79
84
|
const spec = runSpec(argv);
|
|
@@ -93,6 +98,9 @@ async function handlePlaybookCli(argv, { endpoint, cwd = process.cwd() }) {
|
|
|
93
98
|
};
|
|
94
99
|
const value = await requestHost(endpoint, "playbook.run", args, {
|
|
95
100
|
tabId: spec.options["tab-id"],
|
|
101
|
+
session: spec.options.session || session,
|
|
102
|
+
sessionSource: spec.options.session ? "explicit" : sessionSource,
|
|
103
|
+
admission: spec.options["no-wait"] === true ? { wait: false } : admission,
|
|
96
104
|
timeoutMs: resolveRequestDeadlineMs("playbook.run", args),
|
|
97
105
|
});
|
|
98
106
|
return { handled: true, value, json: spec.options.json === true };
|
|
@@ -118,7 +126,12 @@ async function handlePlaybookCli(argv, { endpoint, cwd = process.cwd() }) {
|
|
|
118
126
|
else if (action === "mark") args = { label: parsed.positional.slice(1).join(" ") };
|
|
119
127
|
else if (action === "stop") args = { draft: parsed.options.draft === true };
|
|
120
128
|
else if (!["status", "pause", "resume", "discard"].includes(action)) throw new Error("Unknown record command");
|
|
121
|
-
const value = await requestHost(endpoint, tool, args, {
|
|
129
|
+
const value = await requestHost(endpoint, tool, args, {
|
|
130
|
+
tabId: parsed.options["tab-id"],
|
|
131
|
+
session: parsed.options.session || session,
|
|
132
|
+
sessionSource: parsed.options.session ? "explicit" : sessionSource,
|
|
133
|
+
admission: parsed.options["no-wait"] === true ? { wait: false } : admission,
|
|
134
|
+
});
|
|
122
135
|
return { handled: true, value, json: parsed.options.json === true };
|
|
123
136
|
}
|
|
124
137
|
if (command === "suggest") return { handled: true, value: suggestions({ since: parsed.options.since || "1h" }), json: parsed.options.json === true };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class SurfError extends Error {
|
|
2
|
+
constructor(code, message, details = {}) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "SurfError";
|
|
5
|
+
this.code = code;
|
|
6
|
+
Object.assign(this, details);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
toJSON() {
|
|
10
|
+
const value = { code: this.code, message: this.message };
|
|
11
|
+
for (const key of [
|
|
12
|
+
"session", "target", "lastUrl", "laneKey", "resourceKeys", "retryable", "recoveryCommand",
|
|
13
|
+
"queue", "reason", "browserEpoch", "expectedBrowserEpoch",
|
|
14
|
+
]) {
|
|
15
|
+
if (this[key] !== undefined) value[key] = this[key];
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function surfError(code, message, details = {}) {
|
|
22
|
+
return new SurfError(code, message, details);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isSurfError(error) {
|
|
26
|
+
return Boolean(error && typeof error === "object" && typeof error.code === "string");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fromExtensionError(result, fallbackCode = "browser_error") {
|
|
30
|
+
if (!result?.error) return null;
|
|
31
|
+
return surfError(result.errorCode || fallbackCode, result.error, result.errorDetails || {});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function recoveryFor(error) {
|
|
35
|
+
if (!error || typeof error !== "object") return null;
|
|
36
|
+
if (typeof error.recoveryCommand === "string" && error.recoveryCommand) return error.recoveryCommand;
|
|
37
|
+
if ((error.code === "tab_gone" || error.code === "session_epoch_stale") && error.session) {
|
|
38
|
+
return `surf session.reopen ${error.session}`;
|
|
39
|
+
}
|
|
40
|
+
if ((error.code === "tab_busy" || error.code === "browser_busy" || error.code === "resource_busy") && error.session) {
|
|
41
|
+
return `surf session.info ${error.session}`;
|
|
42
|
+
}
|
|
43
|
+
if (error.code === "browser_busy") return "surf session.list --refresh";
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { SurfError, surfError, isSurfError, fromExtensionError, recoveryFor };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
const PROVIDER_TOOLS = new Set([
|
|
4
|
+
"chatgpt", "gemini", "perplexity", "grok", "kimi", "aistudio", "aistudio.build",
|
|
5
|
+
"oracle.ask", "oracle.result", "oracle.cancel",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
const HOST_TOOLS = new Set([
|
|
9
|
+
"wait",
|
|
10
|
+
"session.list",
|
|
11
|
+
"session.info",
|
|
12
|
+
"tab.unname", "tabs_unregister", "tab.named", "tabs_list_named",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const BROWSER_READ_TOOLS = new Set([
|
|
16
|
+
"tab.list", "tabs_context", "list_tabs",
|
|
17
|
+
"window.list",
|
|
18
|
+
"history.list", "history.search",
|
|
19
|
+
"bookmark.list",
|
|
20
|
+
"downloads.search",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const BROWSER_WRITE_TOOLS = new Set([
|
|
24
|
+
"session.new", "session.ensure", "session.close", "session.rebind", "session.reopen",
|
|
25
|
+
"tab.new", "new_tab", "tabs_create",
|
|
26
|
+
"tab.move", "tab.switch", "switch_tab",
|
|
27
|
+
"tab.group", "tab.ungroup",
|
|
28
|
+
"window.new", "window.close", "window.focus", "window.resize",
|
|
29
|
+
"smoke",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const BROWSER_WRITE_TARGETED_TOOLS = new Set([
|
|
33
|
+
"cookie.set", "cookie.clear", "cookie.clear-all",
|
|
34
|
+
"bookmark.add", "bookmark.remove",
|
|
35
|
+
"playbook.run",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const TAB_TOOLS = new Set([
|
|
39
|
+
"ai", "computer", "batch", "record", "animate-audit", "perf-audit",
|
|
40
|
+
"navigate", "go", "back", "forward", "reload", "tab.reload",
|
|
41
|
+
"screenshot", "snap", "resize",
|
|
42
|
+
"page.read", "read_page", "page.text", "get_page_text", "page.html", "page.save", "page.state",
|
|
43
|
+
"click", "left_click", "right_click", "double_click", "triple_click", "drag", "hover", "key", "submit",
|
|
44
|
+
"type", "smart_type", "find_and_type", "form_input", "form.fill", "select", "upload", "upload_image",
|
|
45
|
+
"scroll", "scroll.top", "scroll.bottom", "scroll.to", "scroll.info", "scroll_to_position",
|
|
46
|
+
"search", "locate.role", "locate.text", "locate.label", "element.styles",
|
|
47
|
+
"js", "javascript_tool", "eval",
|
|
48
|
+
"wait.element", "wait.url", "wait.network", "wait.dom", "wait.load", "health",
|
|
49
|
+
"frame.list", "frame.switch", "frame.main", "frame.js",
|
|
50
|
+
"dialog.accept", "dialog.dismiss", "dialog.info",
|
|
51
|
+
"console", "network", "network.get", "network.body", "network.curl", "network.path",
|
|
52
|
+
"network.origins", "network.clear", "network.stats", "network.export",
|
|
53
|
+
"emulate.network", "emulate.cpu", "emulate.geo", "emulate.device", "emulate.viewport", "emulate.touch",
|
|
54
|
+
"perf.start", "perf.stop", "perf.metrics",
|
|
55
|
+
"zoom", "cookie.list", "cookie.get",
|
|
56
|
+
"tab.name", "tabs_register",
|
|
57
|
+
"playbook.record.start", "playbook.record.stop", "playbook.record.status", "playbook.record.mark",
|
|
58
|
+
"playbook.record.pause", "playbook.record.resume", "playbook.record.discard",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
function hasExplicitTabCloseTarget(args = {}) {
|
|
62
|
+
return [args.id, args.tab_id, args.tabId, args.ids, args.tab_ids, args.tabIds]
|
|
63
|
+
.some((value) => value !== undefined && value !== null && value !== "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function classifyTool(tool, args = {}) {
|
|
67
|
+
if (PROVIDER_TOOLS.has(tool) || tool.startsWith("oracle.")) {
|
|
68
|
+
const hostOnly = tool === "oracle.result" || tool === "oracle.cancel" || tool === "oracle.status" || tool === "oracle.list";
|
|
69
|
+
return hostOnly
|
|
70
|
+
? { scope: "host", targetUse: "host" }
|
|
71
|
+
: { scope: "provider", targetUse: "default-tab" };
|
|
72
|
+
}
|
|
73
|
+
if (tool === "session.list") {
|
|
74
|
+
return { scope: args.refresh ? "browser-read" : "host", targetUse: "host" };
|
|
75
|
+
}
|
|
76
|
+
if (tool === "session.info") {
|
|
77
|
+
return { scope: args.refresh ? "browser-read" : "host", targetUse: "host" };
|
|
78
|
+
}
|
|
79
|
+
if (tool === "tab.close" || tool === "close_tab") {
|
|
80
|
+
return hasExplicitTabCloseTarget(args)
|
|
81
|
+
? { scope: "browser-write", targetUse: "browser" }
|
|
82
|
+
: { scope: "browser-write", targetUse: "default-tab" };
|
|
83
|
+
}
|
|
84
|
+
if (HOST_TOOLS.has(tool)) return { scope: "host", targetUse: "host" };
|
|
85
|
+
if (BROWSER_READ_TOOLS.has(tool)) return { scope: "browser-read", targetUse: "browser" };
|
|
86
|
+
if (BROWSER_WRITE_TARGETED_TOOLS.has(tool)) {
|
|
87
|
+
return { scope: "browser-write", targetUse: "default-tab" };
|
|
88
|
+
}
|
|
89
|
+
if (BROWSER_WRITE_TOOLS.has(tool)) return { scope: "browser-write", targetUse: "browser" };
|
|
90
|
+
if (TAB_TOOLS.has(tool)) {
|
|
91
|
+
const resourceKeys = [];
|
|
92
|
+
if (tool === "network.export" && typeof args.output === "string") resourceKeys.push(`file:${path.resolve(args.output)}`);
|
|
93
|
+
if (tool.startsWith("playbook.record.")) resourceKeys.push("playbook-recorder");
|
|
94
|
+
return { scope: "tab", targetUse: "default-tab", resourceKeys };
|
|
95
|
+
}
|
|
96
|
+
return { scope: "browser-write", targetUse: "browser", conservative: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
BROWSER_READ_TOOLS,
|
|
101
|
+
BROWSER_WRITE_TARGETED_TOOLS,
|
|
102
|
+
BROWSER_WRITE_TOOLS,
|
|
103
|
+
HOST_TOOLS,
|
|
104
|
+
PROVIDER_TOOLS,
|
|
105
|
+
TAB_TOOLS,
|
|
106
|
+
classifyTool,
|
|
107
|
+
};
|
|
@@ -36,6 +36,13 @@ const COMMANDS = {
|
|
|
36
36
|
"tab.close": { primaryArg: "id", effect: "page-write", argKinds: { id: "tab-id" } },
|
|
37
37
|
"tab.name": { primaryArg: "name", effect: "page-write", argKinds: { name: "name" } },
|
|
38
38
|
"tab.unname": { primaryArg: "name", effect: "page-write", argKinds: { name: "name" } },
|
|
39
|
+
"session.new": { primaryArg: "name", effect: "navigation", recordable: false, argKinds: { name: "name", url: "url" } },
|
|
40
|
+
"session.ensure": { primaryArg: "name", effect: "navigation", recordable: false, argKinds: { name: "name", url: "url" } },
|
|
41
|
+
"session.list": { effect: "read", recordable: false },
|
|
42
|
+
"session.info": { primaryArg: "name", effect: "read", recordable: false, argKinds: { name: "name" } },
|
|
43
|
+
"session.close": { primaryArg: "name", effect: "page-write", recordable: false, argKinds: { name: "name" } },
|
|
44
|
+
"session.rebind": { primaryArg: "name", effect: "page-write", recordable: false, argKinds: { name: "name", tabId: "tab-id" } },
|
|
45
|
+
"session.reopen": { primaryArg: "name", effect: "navigation", recordable: false, argKinds: { name: "name", url: "url" } },
|
|
39
46
|
scroll_to_position: { primaryArg: "position", effect: "page-write", argKinds: { position: "position" } },
|
|
40
47
|
type: { primaryArg: "text", effect: "page-write", argKinds: { selector: "selector", text: "user-input" }, sensitiveArgs: ["text"] },
|
|
41
48
|
smart_type: { primaryArg: "text", effect: "page-write", argKinds: { selector: "selector", text: "user-input" }, sensitiveArgs: ["text"] },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "surf-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chrome",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"playbooks/",
|
|
34
34
|
"scripts/",
|
|
35
35
|
"dist/",
|
|
36
|
+
"agents/",
|
|
36
37
|
"skills/",
|
|
37
38
|
"README.md",
|
|
38
39
|
"LICENSE"
|
|
@@ -81,7 +82,12 @@
|
|
|
81
82
|
],
|
|
82
83
|
"skills": [
|
|
83
84
|
"./skills"
|
|
84
|
-
]
|
|
85
|
+
],
|
|
86
|
+
"subagents": {
|
|
87
|
+
"agents": [
|
|
88
|
+
"./agents"
|
|
89
|
+
]
|
|
90
|
+
}
|
|
85
91
|
},
|
|
86
92
|
"peerDependencies": {
|
|
87
93
|
"typebox": "*"
|
package/pi-extension/surf.ts
CHANGED
|
@@ -308,6 +308,16 @@ function emitFailedOracleJob(error: unknown, emitTerminal: EmitOracleJob) {
|
|
|
308
308
|
emitTerminal({ id: error.jobId, state: "failed" });
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
function oracleOption(input: Record<string, unknown>, key: "model" | "effort"): string | undefined {
|
|
312
|
+
const options = input.options;
|
|
313
|
+
if (options && typeof options === "object" && !Array.isArray(options)) {
|
|
314
|
+
const value = (options as Record<string, unknown>)[key];
|
|
315
|
+
if (typeof value === "string") return value;
|
|
316
|
+
}
|
|
317
|
+
const direct = input[key];
|
|
318
|
+
return typeof direct === "string" ? direct : undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
311
321
|
export function createOracleExternalJobProvider(
|
|
312
322
|
sessionId: string,
|
|
313
323
|
jobIds: Set<string>,
|
|
@@ -325,10 +335,12 @@ export function createOracleExternalJobProvider(
|
|
|
325
335
|
async start(input) {
|
|
326
336
|
const prompt = typeof input.prompt === "string" ? input.prompt : "";
|
|
327
337
|
if (!prompt.trim()) throw new Error("prompt required");
|
|
338
|
+
const model = oracleOption(input, "model");
|
|
339
|
+
const effort = oracleOption(input, "effort");
|
|
328
340
|
const job = await requestOracleJob(request, "oracle.ask", {
|
|
329
341
|
prompt,
|
|
330
|
-
...(
|
|
331
|
-
...(
|
|
342
|
+
...(model !== undefined ? { model } : {}),
|
|
343
|
+
...(effort !== undefined ? { effort } : {}),
|
|
332
344
|
});
|
|
333
345
|
rememberJob(job.id);
|
|
334
346
|
return { ...job, promptDigest: job.promptDigest ?? digestPrompt(prompt) };
|
|
@@ -361,11 +373,13 @@ export function createOracleExternalJobProvider(
|
|
|
361
373
|
},
|
|
362
374
|
async follow(id, message, input = {}) {
|
|
363
375
|
if (!message.trim()) throw new Error("message required");
|
|
376
|
+
const model = oracleOption(input, "model");
|
|
377
|
+
const effort = oracleOption(input, "effort");
|
|
364
378
|
const job = await requestOracleJob(request, "oracle.ask", {
|
|
365
379
|
follow: id,
|
|
366
380
|
prompt: message,
|
|
367
|
-
...(
|
|
368
|
-
...(
|
|
381
|
+
...(model !== undefined ? { model } : {}),
|
|
382
|
+
...(effort !== undefined ? { effort } : {}),
|
|
369
383
|
});
|
|
370
384
|
rememberJob(job.id);
|
|
371
385
|
return { ...job, promptDigest: job.promptDigest ?? digestPrompt(message) };
|
package/skills/surf/SKILL.md
CHANGED
|
@@ -47,6 +47,17 @@ surf --find <term> # Search tools
|
|
|
47
47
|
surf --help-topic <topic> # Topic guide (refs, semantic, frames, devices, windows)
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## First Command for Independent Agents
|
|
51
|
+
|
|
52
|
+
Before the first browser command in each independent agent shell, choose a unique valid session name and ensure its target exists:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
export SURF_SESSION="$(basename "$PWD" | sed 's/[^A-Za-z0-9._-]/-/g')"
|
|
56
|
+
surf session.ensure "$SURF_SESSION" about:blank
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`session.ensure` is idempotent. It creates a missing session, reuses a live binding, and reopens a stale or closed tab. Keep `SURF_SESSION` set for every later tab-scoped command in that shell. Use a distinct worktree/directory name per agent; when agents share one directory, append a stable agent identifier. Use `surf session.info "$SURF_SESSION"` to inspect the target and queue state.
|
|
60
|
+
|
|
50
61
|
## Core Workflow
|
|
51
62
|
|
|
52
63
|
```bash
|
|
@@ -103,7 +114,9 @@ surf oracle result <job-id> --wait --json
|
|
|
103
114
|
|
|
104
115
|
Treat Pro quota as scarce. Oracle never selects Pro implicitly; request it with `--model pro` or `--effort pro`. ChatGPT model aliases include `instant`, `thinking`, `pro`, `gpt-5.5`, and `gpt-5.6-sol`. Accepted `--effort` values are `light`, `standard`, `extended`, `heavy`, and `pro`. Requested model and effort selections are read back before submission, and an unverifiable selection fails with `model_verification_failed` instead of silently continuing. Capacity is one non-terminal oracle job. A `capacity` error includes the in-flight job ID; poll that job or wait for it to finish rather than submitting the same consult again.
|
|
105
116
|
|
|
106
|
-
When loaded as a Pi extension, Surf also registers a `surf-oracle` external-job provider when the runtime exposes that bridge. The provider maps `start`, `status`, `result`, `reattach`, and `follow` to durable Surf Oracle jobs. It returns the conversation URL, requested and verified model and effort, prompt digest, result text, and failure details. `reattach` only harvests an existing job by ID; it never submits the prompt again.
|
|
117
|
+
When loaded as a Pi extension, Surf also registers a `surf-oracle` external-job provider when the runtime exposes that bridge. The provider maps `start`, `status`, `result`, `reattach`, and `follow` to durable Surf Oracle jobs. It honors `options.model` and `options.effort` for starts and follows, so `model: pro` selects ChatGPT GPT-5.6 Sol Pro web mode through the browser. It returns the conversation URL, requested and verified model and effort, prompt digest, result text, and failure details. `reattach` only harvests an existing job by ID; it never submits the prompt again.
|
|
118
|
+
|
|
119
|
+
When Surf is installed as a Pi package, it exposes an optional `gpt-pro` package agent for `pi-subagents`. That profile uses `runner.type: external-job`, provider `surf-oracle`, and `options.model: pro`. Surf remains useful without Pi or `pi-subagents`.
|
|
107
120
|
|
|
108
121
|
Context comes from repeatable `--files` globs. Surf fails closed when a glob matches nothing or a matched file is unreadable, binary, or invalid UTF-8. It also blocks gitignored files and basenames matching `.env*`, `*.pem`, `*.key`, `id_rsa*`, `id_ed25519*`, `*.p12`, `*.pfx`, `credentials*`, or `secrets*`. Use `--allow-sensitive` only after intentionally reviewing those files; it overrides the block rather than redacting content. Context up to 60,000 evidence characters is inserted inline, while larger context becomes one private text attachment. The assembly manifest records each path, byte count, SHA-256, inline or bundle disposition, and deny-list outcome.
|
|
109
122
|
|
|
@@ -247,20 +260,27 @@ surf window.resize --id 123 --width 1920 --height 1080
|
|
|
247
260
|
surf window.resize --id 123 --state maximized # States: normal, minimized, maximized, fullscreen
|
|
248
261
|
```
|
|
249
262
|
|
|
250
|
-
**
|
|
263
|
+
**Concurrent agent sessions:**
|
|
264
|
+
|
|
251
265
|
```bash
|
|
252
|
-
#
|
|
253
|
-
|
|
254
|
-
surf
|
|
255
|
-
|
|
266
|
+
# Required first command rule for each independent agent shell
|
|
267
|
+
export SURF_SESSION="$(basename "$PWD" | sed 's/[^A-Za-z0-9._-]/-/g')"
|
|
268
|
+
surf session.ensure "$SURF_SESSION" about:blank
|
|
269
|
+
|
|
270
|
+
# Explicit form when an environment variable is inconvenient
|
|
271
|
+
surf --session research go "https://example.com"
|
|
272
|
+
surf --session research read
|
|
256
273
|
|
|
257
|
-
#
|
|
258
|
-
surf
|
|
259
|
-
surf
|
|
260
|
-
surf tab.switch agent-a
|
|
274
|
+
# Inspect bindings and scheduler state
|
|
275
|
+
surf session.list --refresh
|
|
276
|
+
surf session.info research --refresh
|
|
261
277
|
```
|
|
262
278
|
|
|
263
|
-
|
|
279
|
+
Each session owns one explicit tab and defaults to a separate unfocused window. Commands for the same tab are FIFO; different session tabs may run concurrently. Browser-wide writers wait for tab lanes to drain. `--no-wait` returns `tab_busy` or `browser_busy` immediately. On `tab_gone` or `session_epoch_stale`, run the exact command printed after `Recovery:`—normally `surf session.reopen <name>`.
|
|
280
|
+
|
|
281
|
+
Browser-login provider commands (`chatgpt`, `gemini`, `perplexity`, `grok`, `kimi`, `aistudio`, and `oracle ask`) take exclusive browser access and print a warning before dispatch. Do not assume Surf is hung while that warning is visible; inspect `surf session.info <name>` from another shell to see the active writer.
|
|
282
|
+
|
|
283
|
+
Sessions share cookies, authentication, same-origin storage, downloads, history, bookmarks, and other Chrome-profile state. Use separate browser/profile instances and `SURF_SOCKET` values only when hard isolation is required. Explicit `--tab-id`, `--window-id`, and named tabs remain available for one-off targeting.
|
|
264
284
|
|
|
265
285
|
## Input Methods
|
|
266
286
|
|
|
@@ -439,14 +459,16 @@ surf upload --ref e5 --files "/path/file1.txt,/path/file2.txt"
|
|
|
439
459
|
|
|
440
460
|
```bash
|
|
441
461
|
surf frame.list # List frames with IDs
|
|
442
|
-
surf frame.switch "
|
|
462
|
+
surf frame.switch --selector "#payment-iframe"
|
|
463
|
+
surf frame.switch --name "checkout"
|
|
464
|
+
surf frame.switch --index 0 # First iframe
|
|
443
465
|
surf frame.main # Return to main frame
|
|
444
|
-
surf frame.js
|
|
466
|
+
surf frame.js "return document.title" --id "FRAME_ID"
|
|
445
467
|
|
|
446
468
|
# After frame.switch, subsequent commands target that frame:
|
|
447
|
-
surf frame.switch "iframe
|
|
469
|
+
surf frame.switch --selector "#payment-iframe"
|
|
448
470
|
surf page.read # Reads iframe content
|
|
449
|
-
surf click
|
|
471
|
+
surf click --selector "#pay" # Clicks in iframe
|
|
450
472
|
surf frame.main # Back to main page
|
|
451
473
|
```
|
|
452
474
|
|
|
@@ -550,6 +572,9 @@ surf do 'go "https://example.com" | click e5 | screenshot'
|
|
|
550
572
|
# Multi-step login flow
|
|
551
573
|
surf do 'go "https://example.com/login" | type "user@example.com" --selector "#email" | type "pass" --selector "#password" | click --selector "button[type=submit]"'
|
|
552
574
|
|
|
575
|
+
# JSON action batch. Uses SURF_SESSION when it is set.
|
|
576
|
+
surf batch --actions '[{"type":"frame.switch","index":0},{"type":"click","selector":"#pay"}]'
|
|
577
|
+
|
|
553
578
|
# Validate without executing
|
|
554
579
|
surf do 'go "url" | click e5' --dry-run
|
|
555
580
|
```
|
|
@@ -683,9 +708,11 @@ surf wait.element ".missing" --auto-capture --timeout 2000
|
|
|
683
708
|
## Common Options
|
|
684
709
|
|
|
685
710
|
```bash
|
|
686
|
-
--
|
|
687
|
-
--
|
|
688
|
-
--
|
|
711
|
+
--session <name> # Target a durable named session (or set SURF_SESSION)
|
|
712
|
+
--tab-id <id> # Target a specific tab
|
|
713
|
+
--window-id <id> # Target a specific window
|
|
714
|
+
--no-wait # Return tab_busy/browser_busy instead of queueing
|
|
715
|
+
--json # Raw JSON including target metadata
|
|
689
716
|
--auto-capture # Screenshot + console on error
|
|
690
717
|
--timeout <ms> # Override default timeout
|
|
691
718
|
```
|
|
@@ -703,12 +730,12 @@ surf wait.element ".missing" --auto-capture --timeout 2000
|
|
|
703
730
|
9. **AI Studio for unrestricted Gemini** - `surf aistudio` gives less filtered responses than `surf gemini` for the same models
|
|
704
731
|
10. **Use `surf do` for multi-step tasks** - Reduces token overhead and improves reliability
|
|
705
732
|
11. **Dry-run workflows first** - `surf do '...' --dry-run` validates without executing
|
|
706
|
-
12. **
|
|
707
|
-
13. **
|
|
733
|
+
12. **Session first** - Set a unique `SURF_SESSION` and run `session.ensure` before the first browser command in every independent agent shell
|
|
734
|
+
13. **Queue diagnostics** - `session.info` distinguishes the session's own tab queue, other active tabs, and browser-wide writers; use `--no-wait` for immediate busy errors
|
|
708
735
|
14. **Native host diagnostics** - If commands fail with socket/native-host errors, run `surf doctor` or `surf doctor --browser all` before guessing at reinstall steps
|
|
709
736
|
15. **HTML export** - Use `surf page.html > artifact.html` to save Claude artifacts or any rendered page as static HTML
|
|
710
737
|
16. **Animation capture** - Use `surf record --duration 2000 --fps 10 --output /tmp/anim.gif` when the agent needs to see motion; use `animate-audit` for numeric timelines and `perf-audit` for jank/layout-shift snapshots
|
|
711
|
-
17. **Hard isolation** -
|
|
738
|
+
17. **Hard isolation** - Sessions share a Chrome profile; use separate browser/profile instances plus separate `SURF_SOCKET` values when profile state must not be shared
|
|
712
739
|
18. **Semantic locators** - `locate.role`, `locate.text`, `locate.label` for more robust element finding
|
|
713
740
|
19. **Frame context** - Use `frame.switch` before interacting with iframe content
|
|
714
741
|
|