surf-cli 2.10.0 → 2.12.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 +46 -0
- package/native/chatgpt-client-response.cjs +336 -0
- package/native/chatgpt-client-selection.cjs +119 -0
- package/native/chatgpt-client-ui.cjs +481 -0
- package/native/chatgpt-client.cjs +252 -664
- package/native/cli.cjs +86 -3
- package/native/host-helpers.cjs +38 -1
- package/native/host-sessions.cjs +1 -0
- package/native/host.cjs +30 -5
- package/native/oracle-cli.cjs +434 -0
- package/native/oracle-context.cjs +311 -0
- package/native/oracle-host.cjs +301 -0
- package/native/oracle-jobs.cjs +253 -0
- package/native/playbook-cli.cjs +3 -2
- package/native/playbook-runtime.cjs +29 -2
- package/native/playbooks.cjs +13 -0
- package/native/workflow-definition.cjs +1 -0
- package/native/workflow-script-runtime.cjs +294 -0
- package/package.json +20 -5
- package/pi-extension/surf.ts +230 -0
- package/skills/surf/SKILL.md +55 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "surf-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chrome",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"agent",
|
|
11
11
|
"cli",
|
|
12
12
|
"cdp",
|
|
13
|
-
"devtools"
|
|
13
|
+
"devtools",
|
|
14
|
+
"pi-package"
|
|
14
15
|
],
|
|
15
16
|
"author": "Nico Bailon",
|
|
16
17
|
"license": "MIT",
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
},
|
|
29
30
|
"files": [
|
|
30
31
|
"native/",
|
|
32
|
+
"pi-extension/",
|
|
31
33
|
"playbooks/",
|
|
32
34
|
"scripts/",
|
|
33
35
|
"dist/",
|
|
@@ -38,7 +40,7 @@
|
|
|
38
40
|
"scripts": {
|
|
39
41
|
"dev": "vite build --watch --mode development",
|
|
40
42
|
"build": "vite build",
|
|
41
|
-
"check": "tsc --noEmit",
|
|
43
|
+
"check": "tsc --noEmit && tsc --noEmit -p tsconfig.pi-extension.json",
|
|
42
44
|
"lint": "biome check .",
|
|
43
45
|
"lint:fix": "biome check --write .",
|
|
44
46
|
"lint:test": "biome check test/",
|
|
@@ -64,11 +66,24 @@
|
|
|
64
66
|
"devDependencies": {
|
|
65
67
|
"@biomejs/biome": "^2.5.4",
|
|
66
68
|
"@types/chrome": "^0.2.2",
|
|
69
|
+
"@types/node": "^26.1.2",
|
|
67
70
|
"@vitest/coverage-v8": "^4.1.9",
|
|
68
71
|
"@vitest/ui": "^4.1.9",
|
|
69
|
-
"puppeteer": "25.
|
|
72
|
+
"puppeteer": "25.4.0",
|
|
70
73
|
"typescript": "^7.0.2",
|
|
71
74
|
"vite": "^8.1.4",
|
|
72
|
-
"vitest": "^4.1.9"
|
|
75
|
+
"vitest": "^4.1.9",
|
|
76
|
+
"typebox": "^1.3.11"
|
|
77
|
+
},
|
|
78
|
+
"pi": {
|
|
79
|
+
"extensions": [
|
|
80
|
+
"./pi-extension/surf.ts"
|
|
81
|
+
],
|
|
82
|
+
"skills": [
|
|
83
|
+
"./skills"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"peerDependencies": {
|
|
87
|
+
"typebox": "*"
|
|
73
88
|
}
|
|
74
89
|
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const { openClientTransport } = require("../native/client-transport.cjs") as {
|
|
6
|
+
openClientTransport(endpoint: SurfEndpoint, options?: { requestTimeoutMs?: number }): Promise<{
|
|
7
|
+
request(message: Record<string, unknown>, timeoutMs?: number, transferPlan?: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
}>;
|
|
10
|
+
};
|
|
11
|
+
const { selectEndpoint } = require("../native/endpoint.cjs") as {
|
|
12
|
+
selectEndpoint(args: string[], env?: Record<string, string | undefined>): { endpoint: SurfEndpoint };
|
|
13
|
+
};
|
|
14
|
+
const { resolveRequestDeadlineMs } = require("../native/host-sessions.cjs") as {
|
|
15
|
+
resolveRequestDeadlineMs(tool: string, args: Record<string, unknown>): number;
|
|
16
|
+
};
|
|
17
|
+
const { prepareRemoteTool, validateLocalToolPaths } = require("../native/file-transfer.cjs") as {
|
|
18
|
+
prepareRemoteTool(tool: string, args: Record<string, unknown>): { args: Record<string, unknown>; uploads?: unknown[]; downloads?: unknown[]; pathRefs?: unknown[] };
|
|
19
|
+
validateLocalToolPaths(tool: string, args: Record<string, unknown>): Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const MAX_OUTPUT_CHARS = 20_000;
|
|
23
|
+
const ORACLE_ACTIVE_STATES = new Set(["created", "dispatched", "awaiting"]);
|
|
24
|
+
const BACKGROUND_WORK_PROTOCOL_VERSION = 1;
|
|
25
|
+
const BACKGROUND_WORK_REGISTRY_KEY = "pi-subagents.background-work.v1";
|
|
26
|
+
|
|
27
|
+
type Pi = {
|
|
28
|
+
registerTool(tool: Record<string, unknown>): void;
|
|
29
|
+
on(event: "session_start" | "session_shutdown", handler: (event: unknown, ctx: unknown) => void | Promise<void>): void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type SurfEndpoint = { kind?: string };
|
|
33
|
+
|
|
34
|
+
type ToolResult = { content: Array<{ type: "text" | "image"; text?: string; data?: string; mimeType?: string }>; details?: unknown; isError?: boolean };
|
|
35
|
+
|
|
36
|
+
type BackgroundWorkProvider = {
|
|
37
|
+
name: string;
|
|
38
|
+
wakeChannels: string[];
|
|
39
|
+
listActiveWork(): Array<{ id: string; sessionId: string }>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type BackgroundWorkRegistry = {
|
|
43
|
+
version: typeof BACKGROUND_WORK_PROTOCOL_VERSION;
|
|
44
|
+
providers: Map<string, BackgroundWorkProvider>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function textResult(value: unknown, isError = false): ToolResult {
|
|
48
|
+
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
49
|
+
const bounded = text.length > MAX_OUTPUT_CHARS
|
|
50
|
+
? `${text.slice(0, MAX_OUTPUT_CHARS)}\n\n[Surf output truncated at ${MAX_OUTPUT_CHARS} characters]`
|
|
51
|
+
: text;
|
|
52
|
+
return { content: [{ type: "text", text: bounded }], ...(isError ? { isError: true } : {}) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resultFromHost(response: Record<string, unknown>): ToolResult {
|
|
56
|
+
const error = response.error as { content?: Array<{ text?: string }> } | undefined;
|
|
57
|
+
if (error) return textResult(error.content?.map((item) => item.text ?? "").join("\n") || "Surf request failed", true);
|
|
58
|
+
const result = response.result as { content?: ToolResult["content"] } | undefined;
|
|
59
|
+
if (!result?.content) return textResult(result ?? "OK");
|
|
60
|
+
const content = result.content.map((item) => item.type === "text" && item.text && item.text.length > MAX_OUTPUT_CHARS
|
|
61
|
+
? { ...item, text: `${item.text.slice(0, MAX_OUTPUT_CHARS)}\n\n[Surf output truncated at ${MAX_OUTPUT_CHARS} characters]` }
|
|
62
|
+
: item);
|
|
63
|
+
const text = content.find((item) => item.type === "text")?.text;
|
|
64
|
+
let details: unknown;
|
|
65
|
+
try {
|
|
66
|
+
details = text ? JSON.parse(text) : undefined;
|
|
67
|
+
} catch {
|
|
68
|
+
details = undefined;
|
|
69
|
+
}
|
|
70
|
+
return { content, details };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createToolRequest(tool: string, args: Record<string, unknown>, tabId?: number) {
|
|
74
|
+
return {
|
|
75
|
+
type: "tool_request",
|
|
76
|
+
method: "execute_tool",
|
|
77
|
+
params: { tool, args, ...(tabId === undefined ? {} : { tabId }) },
|
|
78
|
+
id: `pi-surf-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function prepareRequest(endpoint: SurfEndpoint, tool: string, args: Record<string, unknown>) {
|
|
83
|
+
if (endpoint.kind === "remote") return prepareRemoteTool(tool, args);
|
|
84
|
+
return { args: validateLocalToolPaths(tool, args), uploads: [], downloads: [], pathRefs: [] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function requestSurf(tool: string, args: Record<string, unknown>, tabId?: number): Promise<ToolResult> {
|
|
88
|
+
const { endpoint } = selectEndpoint([], process.env);
|
|
89
|
+
const timeoutMs = resolveRequestDeadlineMs(tool, args);
|
|
90
|
+
const transport = await openClientTransport(endpoint, { requestTimeoutMs: timeoutMs });
|
|
91
|
+
try {
|
|
92
|
+
const prepared = prepareRequest(endpoint, tool, args);
|
|
93
|
+
const response = await transport.request(createToolRequest(tool, prepared.args, tabId), timeoutMs, prepared);
|
|
94
|
+
return resultFromHost(response);
|
|
95
|
+
} finally {
|
|
96
|
+
await transport.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function surfRequest(tool: string, args: Record<string, unknown>, tabId?: number) {
|
|
101
|
+
return requestSurf(tool, args, tabId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function registerGlobalBackgroundProvider(provider: BackgroundWorkProvider): () => void {
|
|
105
|
+
const key = Symbol.for(BACKGROUND_WORK_REGISTRY_KEY);
|
|
106
|
+
const globalObject = globalThis as Record<PropertyKey, unknown>;
|
|
107
|
+
const existing = globalObject[key];
|
|
108
|
+
let registry: BackgroundWorkRegistry;
|
|
109
|
+
|
|
110
|
+
if (existing === undefined) {
|
|
111
|
+
registry = { version: BACKGROUND_WORK_PROTOCOL_VERSION, providers: new Map() };
|
|
112
|
+
globalObject[key] = registry;
|
|
113
|
+
} else if (
|
|
114
|
+
existing &&
|
|
115
|
+
typeof existing === "object" &&
|
|
116
|
+
!Array.isArray(existing) &&
|
|
117
|
+
(existing as Partial<BackgroundWorkRegistry>).version === BACKGROUND_WORK_PROTOCOL_VERSION &&
|
|
118
|
+
(existing as Partial<BackgroundWorkRegistry>).providers instanceof Map
|
|
119
|
+
) {
|
|
120
|
+
registry = existing as BackgroundWorkRegistry;
|
|
121
|
+
} else {
|
|
122
|
+
throw new Error(`Unsupported background-work registry at Symbol.for("${BACKGROUND_WORK_REGISTRY_KEY}").`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
registry.providers.set(provider.name, provider);
|
|
126
|
+
return () => {
|
|
127
|
+
if (registry.providers.get(provider.name) === provider) registry.providers.delete(provider.name);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function registerOptionalBackgroundProvider(sessionId: string, jobIds: Set<string>, listJobs: () => Array<{ id: string; state: string }>, register: (provider: BackgroundWorkProvider) => () => void) {
|
|
132
|
+
return register({
|
|
133
|
+
name: "surf-oracle",
|
|
134
|
+
wakeChannels: ["surf-oracle:finished"],
|
|
135
|
+
listActiveWork: () => listJobs()
|
|
136
|
+
.filter((job) => jobIds.has(job.id) && ORACLE_ACTIVE_STATES.has(job.state))
|
|
137
|
+
.map((job) => ({ id: job.id, sessionId })),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function rememberOracleJobForSession(jobIds: Set<string>, jobId: unknown, requestGeneration: number, currentGeneration: number, sessionActive: boolean): boolean {
|
|
142
|
+
if (typeof jobId !== "string" || !sessionActive || requestGeneration !== currentGeneration) return false;
|
|
143
|
+
jobIds.add(jobId);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function registerTool(pi: Pi, name: string, description: string, parameters: unknown, map: (args: Record<string, unknown>) => [string, Record<string, unknown>, number | undefined]) {
|
|
148
|
+
pi.registerTool({
|
|
149
|
+
name,
|
|
150
|
+
label: name,
|
|
151
|
+
description,
|
|
152
|
+
parameters,
|
|
153
|
+
async execute(_id: string, args: Record<string, unknown>) {
|
|
154
|
+
try {
|
|
155
|
+
const [tool, toolArgs, tabId] = map(args);
|
|
156
|
+
return await requestSurf(tool, toolArgs, tabId);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return textResult(error instanceof Error ? error.message : String(error), true);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default function surfExtension(pi: Pi) {
|
|
165
|
+
registerTool(pi, "surf_read", "Read the current Surf browser page. Read tools are safer for parallel scouts than browser actions.", Type.Object({
|
|
166
|
+
tabId: Type.Optional(Type.Number()), filter: Type.Optional(Type.String()), depth: Type.Optional(Type.Number()), ref: Type.Optional(Type.String()), compact: Type.Optional(Type.Boolean()), maxBytes: Type.Optional(Type.Number()),
|
|
167
|
+
}), (args) => ["page.read", { filter: args.filter, depth: args.depth, ref: args.ref, compact: args.compact, "max-bytes": args.maxBytes }, args.tabId as number | undefined]);
|
|
168
|
+
registerTool(pi, "surf_screenshot", "Capture a bounded Surf browser screenshot.", Type.Object({
|
|
169
|
+
tabId: Type.Optional(Type.Number()), output: Type.Optional(Type.String()), fullpage: Type.Optional(Type.Boolean()), annotate: Type.Optional(Type.Boolean()), maxSize: Type.Optional(Type.Number()),
|
|
170
|
+
}), (args) => ["screenshot", { output: args.output, fullpage: args.fullpage, annotate: args.annotate, "max-size": args.maxSize }, args.tabId as number | undefined]);
|
|
171
|
+
registerTool(pi, "surf_click", "Click a Surf browser element by ref, selector, or coordinates. This can interfere with other agents in the shared browser session.", Type.Object({
|
|
172
|
+
tabId: Type.Optional(Type.Number()), ref: Type.Optional(Type.String()), selector: Type.Optional(Type.String()), x: Type.Optional(Type.Number()), y: Type.Optional(Type.Number()), button: Type.Optional(Type.String({ description: "left, right, or double" })),
|
|
173
|
+
}), (args) => [args.button === "right" ? "right_click" : args.button === "double" ? "double_click" : "click", args, args.tabId as number | undefined]);
|
|
174
|
+
registerTool(pi, "surf_type", "Type text in the Surf browser. This can interfere with other agents in the shared browser session.", Type.Object({
|
|
175
|
+
tabId: Type.Optional(Type.Number()), text: Type.String(), ref: Type.Optional(Type.String()), selector: Type.Optional(Type.String()), clear: Type.Optional(Type.Boolean()), submit: Type.Optional(Type.Boolean()),
|
|
176
|
+
}), (args) => ["type", args, args.tabId as number | undefined]);
|
|
177
|
+
registerTool(pi, "surf_tool", "Run one existing Surf browser tool through the native host. Prefer the dedicated read, screenshot, click, and type tools when they fit.", Type.Object({
|
|
178
|
+
tool: Type.String(), args: Type.Optional(Type.Record(Type.String(), Type.Unknown())), tabId: Type.Optional(Type.Number()),
|
|
179
|
+
}), (args) => [args.tool as string, (args.args as Record<string, unknown>) ?? {}, args.tabId as number | undefined]);
|
|
180
|
+
registerTool(pi, "surf_oracle_status", "Get the status of a Surf oracle job, or the newest job.", Type.Object({ id: Type.Optional(Type.String()) }), (args) => ["oracle.status", args, undefined]);
|
|
181
|
+
registerTool(pi, "surf_oracle_result", "Capture the result of a Surf oracle job.", Type.Object({ id: Type.String(), timeout: Type.Optional(Type.Number()) }), (args) => ["oracle.result", args, undefined]);
|
|
182
|
+
|
|
183
|
+
const oracleJobIds = new Set<string>();
|
|
184
|
+
let sessionGeneration = 0;
|
|
185
|
+
let sessionActive = false;
|
|
186
|
+
pi.registerTool({
|
|
187
|
+
name: "surf_oracle_ask",
|
|
188
|
+
label: "surf_oracle_ask",
|
|
189
|
+
description: "Start a durable local Surf ChatGPT oracle job.",
|
|
190
|
+
parameters: Type.Object({ prompt: Type.String(), model: Type.Optional(Type.String()), effort: Type.Optional(Type.String()), follow: Type.Optional(Type.String()) }),
|
|
191
|
+
async execute(_id: string, args: Record<string, unknown>) {
|
|
192
|
+
const requestGeneration = sessionGeneration;
|
|
193
|
+
try {
|
|
194
|
+
const result = await requestSurf("oracle.ask", args);
|
|
195
|
+
const job = result.details as { id?: string } | undefined;
|
|
196
|
+
rememberOracleJobForSession(oracleJobIds, job?.id, requestGeneration, sessionGeneration, sessionActive);
|
|
197
|
+
return result;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return textResult(error instanceof Error ? error.message : String(error), true);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
let dispose: (() => void) | undefined;
|
|
205
|
+
pi.on("session_start", (_event, ctx) => {
|
|
206
|
+
sessionGeneration++;
|
|
207
|
+
sessionActive = false;
|
|
208
|
+
dispose?.();
|
|
209
|
+
dispose = undefined;
|
|
210
|
+
oracleJobIds.clear();
|
|
211
|
+
|
|
212
|
+
const session = ctx as { sessionManager?: { getSessionId?: () => string }; sessionId?: string };
|
|
213
|
+
const sessionId = session.sessionId ?? session.sessionManager?.getSessionId?.();
|
|
214
|
+
if (!sessionId) return;
|
|
215
|
+
try {
|
|
216
|
+
const jobs = require("../native/oracle-jobs.cjs") as { listJobs(): Array<{ id: string; state: string }> };
|
|
217
|
+
dispose = registerOptionalBackgroundProvider(sessionId, oracleJobIds, jobs.listJobs, registerGlobalBackgroundProvider);
|
|
218
|
+
sessionActive = true;
|
|
219
|
+
} catch {
|
|
220
|
+
// pi-subagents is optional. Browser tools work without it.
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
pi.on("session_shutdown", () => {
|
|
224
|
+
sessionGeneration++;
|
|
225
|
+
sessionActive = false;
|
|
226
|
+
dispose?.();
|
|
227
|
+
dispose = undefined;
|
|
228
|
+
oracleJobIds.clear();
|
|
229
|
+
});
|
|
230
|
+
}
|
package/skills/surf/SKILL.md
CHANGED
|
@@ -63,8 +63,8 @@ surf click --x 100 --y 200
|
|
|
63
63
|
# 4. Type text
|
|
64
64
|
surf type --text "hello"
|
|
65
65
|
|
|
66
|
-
# 5.
|
|
67
|
-
surf screenshot --output /tmp/shot.png
|
|
66
|
+
# 5. Full-page screenshot
|
|
67
|
+
surf screenshot --full-page --output /tmp/shot.png
|
|
68
68
|
|
|
69
69
|
# Inspect animation/style changes as JSON
|
|
70
70
|
surf animate-audit --selector ".thing" --duration 2000 --fps 10
|
|
@@ -82,6 +82,37 @@ surf chatgpt "review" --model gpt-4o # Specify model
|
|
|
82
82
|
surf chatgpt "analyze" --file document.pdf # With file attachment
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
### Oracle
|
|
86
|
+
|
|
87
|
+
Use `surf chatgpt` for quick one-shot questions. Use `surf oracle` for long-running or Pro coding consults that need a durable job, explicit model and effort selection, file context, recovery, or follow-up turns. Oracle is local-only.
|
|
88
|
+
|
|
89
|
+
For agent workflows, detach after dispatch and keep the returned `.id`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
surf oracle ask "Review this change and identify release risks" \
|
|
93
|
+
--files "src/**/*.ts" --files "package.json" \
|
|
94
|
+
--model pro --effort extended --detach --json
|
|
95
|
+
|
|
96
|
+
surf oracle status <job-id> --json
|
|
97
|
+
surf oracle result <job-id> --json
|
|
98
|
+
# Or let Surf keep polling until capture:
|
|
99
|
+
surf oracle result <job-id> --wait --json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`status` reads persisted state without touching Chrome. `result` attempts to harvest the answer and returns the job object with `response` once its state is `captured`. A Ctrl-C during waiting exits with status 130 and prints `Recover with: surf oracle result <id>`. Once the job is `awaiting`, the persisted ChatGPT conversation URL is its durable key, so `surf oracle result <id>` can recover after CLI exit, native-host restart, or Chrome restart by reopening that conversation.
|
|
103
|
+
|
|
104
|
+
Treat Pro quota as scarce. Oracle never selects Pro implicitly; request it with `--model pro`. Accepted `--effort` values are `light`, `standard`, `extended`, and `heavy`. 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
|
+
|
|
106
|
+
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.
|
|
107
|
+
|
|
108
|
+
Continue a captured consult with `follow`. Use the ID returned by each turn for the next turn:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
surf oracle follow <job-id> "Challenge your recommendation. What could invalidate it?" --detach --json
|
|
112
|
+
surf oracle result <follow-job-id> --wait --json
|
|
113
|
+
surf oracle follow <follow-job-id> "Give the final decision and concrete next steps." --detach --json
|
|
114
|
+
```
|
|
115
|
+
|
|
85
116
|
### Gemini
|
|
86
117
|
```bash
|
|
87
118
|
surf gemini "explain quantum computing"
|
|
@@ -261,9 +292,26 @@ surf page.read --depth 3 # Limit tree depth
|
|
|
261
292
|
surf page.read --compact # Minimal output for LLM efficiency
|
|
262
293
|
surf page.read --max-bytes 2000 # Cap visible text at a UTF-8 byte boundary
|
|
263
294
|
surf page.text # Plain text content only
|
|
295
|
+
surf page.html --strip-scripts # Rendered HTML without scripts
|
|
296
|
+
surf page.save --selector "#artifact" --strip-scripts --output page.html # Save one static element
|
|
264
297
|
surf page.state # Modals, loading state, scroll info
|
|
265
298
|
```
|
|
266
299
|
|
|
300
|
+
### Export Rendered HTML
|
|
301
|
+
|
|
302
|
+
Use `page.html` when the user wants a static copy of the current rendered DOM. This works for Claude artifact pages and ordinary web pages.
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
# Save the active page as HTML.
|
|
306
|
+
surf page.save --output page.html
|
|
307
|
+
|
|
308
|
+
# Save a Claude artifact or other preview page after it loads, without scripts.
|
|
309
|
+
surf wait.dom --stable 500
|
|
310
|
+
surf page.html --selector "#artifact" --strip-scripts > artifact.html
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Use `--selector <css>` to export its matching element only. A selector miss fails with an error. `--strip-scripts` removes scripts from exported markup without changing the page. Without `--selector`, `page.html` exports the whole document with its doctype. `page.html` exports the selected frame when `frame.switch` is active. Use `page.read` first when you need refs or visible text.
|
|
314
|
+
|
|
267
315
|
## Semantic Element Location
|
|
268
316
|
|
|
269
317
|
Find and act on elements by role, text, or label instead of refs:
|
|
@@ -656,10 +704,11 @@ surf wait.element ".missing" --auto-capture --timeout 2000
|
|
|
656
704
|
12. **Window isolation** - Use `window.new` + `--window-id` or `--tab-id` to keep agent work separate from your browsing
|
|
657
705
|
13. **Request lock** - Non-streaming browser CLI requests serialize per socket; use `--no-lock` only when you intentionally want to bypass it
|
|
658
706
|
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
|
|
659
|
-
15. **
|
|
660
|
-
16. **
|
|
661
|
-
17. **
|
|
662
|
-
18. **
|
|
707
|
+
15. **HTML export** - Use `surf page.html > artifact.html` to save Claude artifacts or any rendered page as static HTML
|
|
708
|
+
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
|
|
709
|
+
17. **Hard isolation** - Use separate browser/profile instances plus separate `SURF_SOCKET` values when agents must not share a host or target
|
|
710
|
+
18. **Semantic locators** - `locate.role`, `locate.text`, `locate.label` for more robust element finding
|
|
711
|
+
19. **Frame context** - Use `frame.switch` before interacting with iframe content
|
|
663
712
|
|
|
664
713
|
## Socket API
|
|
665
714
|
|