surf-cli 2.11.0 → 2.13.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 +39 -0
- package/native/cli.cjs +66 -4
- package/native/host-helpers.cjs +45 -2
- package/native/host-sessions.cjs +1 -0
- package/native/host.cjs +110 -6
- package/native/kimi-client.cjs +912 -0
- package/native/mcp-server.cjs +10 -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 +292 -0
- package/skills/README.md +6 -0
- package/skills/surf/SKILL.md +24 -6
|
@@ -0,0 +1,292 @@
|
|
|
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 ORACLE_TERMINAL_STATES = new Set(["captured", "failed"]);
|
|
25
|
+
const ORACLE_FINISHED_CHANNEL = "surf-oracle:finished";
|
|
26
|
+
const BACKGROUND_WORK_PROTOCOL_VERSION = 1;
|
|
27
|
+
const BACKGROUND_WORK_REGISTRY_KEY = "pi-subagents.background-work.v1";
|
|
28
|
+
const BACKGROUND_WORK_MODULE_SPECIFIER = "pi-subagents/background-work";
|
|
29
|
+
|
|
30
|
+
type Pi = {
|
|
31
|
+
registerTool(tool: Record<string, unknown>): void;
|
|
32
|
+
on(event: "session_start" | "session_shutdown", handler: (event: unknown, ctx: unknown) => void | Promise<void>): void;
|
|
33
|
+
events?: { emit(event: string, data: unknown): void };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type SurfEndpoint = { kind?: string };
|
|
37
|
+
|
|
38
|
+
type ToolResult = { content: Array<{ type: "text" | "image"; text?: string; data?: string; mimeType?: string }>; details?: unknown; isError?: boolean };
|
|
39
|
+
|
|
40
|
+
type BackgroundWorkProvider = {
|
|
41
|
+
name: string;
|
|
42
|
+
wakeChannels: string[];
|
|
43
|
+
listActiveWork(): Array<{ id: string; sessionId: string }>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type RegisterBackgroundWorkProvider = (provider: BackgroundWorkProvider) => () => void;
|
|
47
|
+
|
|
48
|
+
type BackgroundWorkModule = {
|
|
49
|
+
registerBackgroundWorkProvider?: unknown;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type BackgroundWorkRegistry = {
|
|
53
|
+
version: typeof BACKGROUND_WORK_PROTOCOL_VERSION;
|
|
54
|
+
providers: Map<string, BackgroundWorkProvider>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function textResult(value: unknown, isError = false): ToolResult {
|
|
58
|
+
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
59
|
+
const bounded = text.length > MAX_OUTPUT_CHARS
|
|
60
|
+
? `${text.slice(0, MAX_OUTPUT_CHARS)}\n\n[Surf output truncated at ${MAX_OUTPUT_CHARS} characters]`
|
|
61
|
+
: text;
|
|
62
|
+
return { content: [{ type: "text", text: bounded }], ...(isError ? { isError: true } : {}) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resultFromHost(response: Record<string, unknown>): ToolResult {
|
|
66
|
+
const error = response.error as { content?: Array<{ text?: string }> } | undefined;
|
|
67
|
+
if (error) return textResult(error.content?.map((item) => item.text ?? "").join("\n") || "Surf request failed", true);
|
|
68
|
+
const result = response.result as { content?: ToolResult["content"] } | undefined;
|
|
69
|
+
if (!result?.content) return textResult(result ?? "OK");
|
|
70
|
+
const text = result.content.find((item) => item.type === "text")?.text;
|
|
71
|
+
let details: unknown;
|
|
72
|
+
try {
|
|
73
|
+
details = text ? JSON.parse(text) : undefined;
|
|
74
|
+
} catch {
|
|
75
|
+
details = undefined;
|
|
76
|
+
}
|
|
77
|
+
const content = result.content.map((item) => item.type === "text" && item.text && item.text.length > MAX_OUTPUT_CHARS
|
|
78
|
+
? { ...item, text: `${item.text.slice(0, MAX_OUTPUT_CHARS)}\n\n[Surf output truncated at ${MAX_OUTPUT_CHARS} characters]` }
|
|
79
|
+
: item);
|
|
80
|
+
return { content, details };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createToolRequest(tool: string, args: Record<string, unknown>, tabId?: number) {
|
|
84
|
+
return {
|
|
85
|
+
type: "tool_request",
|
|
86
|
+
method: "execute_tool",
|
|
87
|
+
params: { tool, args, ...(tabId === undefined ? {} : { tabId }) },
|
|
88
|
+
id: `pi-surf-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function prepareRequest(endpoint: SurfEndpoint, tool: string, args: Record<string, unknown>) {
|
|
93
|
+
if (endpoint.kind === "remote") return prepareRemoteTool(tool, args);
|
|
94
|
+
return { args: validateLocalToolPaths(tool, args), uploads: [], downloads: [], pathRefs: [] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function requestSurf(tool: string, args: Record<string, unknown>, tabId?: number): Promise<ToolResult> {
|
|
98
|
+
const { endpoint } = selectEndpoint([], process.env);
|
|
99
|
+
const timeoutMs = resolveRequestDeadlineMs(tool, args);
|
|
100
|
+
const transport = await openClientTransport(endpoint, { requestTimeoutMs: timeoutMs });
|
|
101
|
+
try {
|
|
102
|
+
const prepared = prepareRequest(endpoint, tool, args);
|
|
103
|
+
const response = await transport.request(createToolRequest(tool, prepared.args, tabId), timeoutMs, prepared);
|
|
104
|
+
return resultFromHost(response);
|
|
105
|
+
} finally {
|
|
106
|
+
await transport.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function surfRequest(tool: string, args: Record<string, unknown>, tabId?: number) {
|
|
111
|
+
return requestSurf(tool, args, tabId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function registerGlobalBackgroundProvider(provider: BackgroundWorkProvider): () => void {
|
|
115
|
+
const key = Symbol.for(BACKGROUND_WORK_REGISTRY_KEY);
|
|
116
|
+
const globalObject = globalThis as Record<PropertyKey, unknown>;
|
|
117
|
+
const existing = globalObject[key];
|
|
118
|
+
let registry: BackgroundWorkRegistry;
|
|
119
|
+
|
|
120
|
+
if (existing === undefined) {
|
|
121
|
+
registry = { version: BACKGROUND_WORK_PROTOCOL_VERSION, providers: new Map() };
|
|
122
|
+
globalObject[key] = registry;
|
|
123
|
+
} else if (
|
|
124
|
+
existing &&
|
|
125
|
+
typeof existing === "object" &&
|
|
126
|
+
!Array.isArray(existing) &&
|
|
127
|
+
(existing as Partial<BackgroundWorkRegistry>).version === BACKGROUND_WORK_PROTOCOL_VERSION &&
|
|
128
|
+
(existing as Partial<BackgroundWorkRegistry>).providers instanceof Map
|
|
129
|
+
) {
|
|
130
|
+
registry = existing as BackgroundWorkRegistry;
|
|
131
|
+
} else {
|
|
132
|
+
throw new Error(`Unsupported background-work registry at Symbol.for("${BACKGROUND_WORK_REGISTRY_KEY}").`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
registry.providers.set(provider.name, provider);
|
|
136
|
+
return () => {
|
|
137
|
+
if (registry.providers.get(provider.name) === provider) registry.providers.delete(provider.name);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function resolveBackgroundWorkRegister(
|
|
142
|
+
loadModule: () => Promise<BackgroundWorkModule> = () => import(BACKGROUND_WORK_MODULE_SPECIFIER) as Promise<BackgroundWorkModule>,
|
|
143
|
+
): Promise<RegisterBackgroundWorkProvider> {
|
|
144
|
+
try {
|
|
145
|
+
const module = await loadModule();
|
|
146
|
+
if (typeof module.registerBackgroundWorkProvider === "function") {
|
|
147
|
+
return module.registerBackgroundWorkProvider as RegisterBackgroundWorkProvider;
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// The Pi bridge is optional. Surf also runs in other coding-agent harnesses and as a direct CLI.
|
|
151
|
+
}
|
|
152
|
+
return registerGlobalBackgroundProvider;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function registerOptionalBackgroundProvider(sessionId: string, jobIds: Set<string>, listJobs: () => Array<{ id: string; state: string }>, register: RegisterBackgroundWorkProvider) {
|
|
156
|
+
return register({
|
|
157
|
+
name: "surf-oracle",
|
|
158
|
+
wakeChannels: [ORACLE_FINISHED_CHANNEL],
|
|
159
|
+
listActiveWork: () => listJobs()
|
|
160
|
+
.filter((job) => jobIds.has(job.id) && ORACLE_ACTIVE_STATES.has(job.state))
|
|
161
|
+
.map((job) => ({ id: job.id, sessionId })),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function rememberOracleJobForSession(jobIds: Set<string>, jobId: unknown, requestGeneration: number, currentGeneration: number, sessionActive: boolean): boolean {
|
|
166
|
+
if (typeof jobId !== "string" || !sessionActive || requestGeneration !== currentGeneration) return false;
|
|
167
|
+
jobIds.add(jobId);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function emitOracleFinished(pi: Pi, job: unknown): boolean {
|
|
172
|
+
if (!job || typeof job !== "object" || Array.isArray(job)) return false;
|
|
173
|
+
const { id, state } = job as { id?: unknown; state?: unknown };
|
|
174
|
+
if (typeof id !== "string" || typeof state !== "string" || !ORACLE_TERMINAL_STATES.has(state)) return false;
|
|
175
|
+
if (!pi.events) return false;
|
|
176
|
+
pi.events.emit(ORACLE_FINISHED_CHANNEL, { id, state });
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function registerTool(pi: Pi, name: string, description: string, parameters: unknown, map: (args: Record<string, unknown>) => [string, Record<string, unknown>, number | undefined]) {
|
|
181
|
+
pi.registerTool({
|
|
182
|
+
name,
|
|
183
|
+
label: name,
|
|
184
|
+
description,
|
|
185
|
+
parameters,
|
|
186
|
+
async execute(_id: string, args: Record<string, unknown>) {
|
|
187
|
+
try {
|
|
188
|
+
const [tool, toolArgs, tabId] = map(args);
|
|
189
|
+
return await requestSurf(tool, toolArgs, tabId);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
return textResult(error instanceof Error ? error.message : String(error), true);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export default function surfExtension(pi: Pi) {
|
|
198
|
+
registerTool(pi, "surf_read", "Read the current Surf browser page. Read tools are safer for parallel scouts than browser actions.", Type.Object({
|
|
199
|
+
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()),
|
|
200
|
+
}), (args) => ["page.read", { filter: args.filter, depth: args.depth, ref: args.ref, compact: args.compact, "max-bytes": args.maxBytes }, args.tabId as number | undefined]);
|
|
201
|
+
registerTool(pi, "surf_screenshot", "Capture a bounded Surf browser screenshot.", Type.Object({
|
|
202
|
+
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()),
|
|
203
|
+
}), (args) => ["screenshot", { output: args.output, fullpage: args.fullpage, annotate: args.annotate, "max-size": args.maxSize }, args.tabId as number | undefined]);
|
|
204
|
+
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({
|
|
205
|
+
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" })),
|
|
206
|
+
}), (args) => [args.button === "right" ? "right_click" : args.button === "double" ? "double_click" : "click", args, args.tabId as number | undefined]);
|
|
207
|
+
registerTool(pi, "surf_type", "Type text in the Surf browser. This can interfere with other agents in the shared browser session.", Type.Object({
|
|
208
|
+
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()),
|
|
209
|
+
}), (args) => ["type", args, args.tabId as number | undefined]);
|
|
210
|
+
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({
|
|
211
|
+
tool: Type.String(), args: Type.Optional(Type.Record(Type.String(), Type.Unknown())), tabId: Type.Optional(Type.Number()),
|
|
212
|
+
}), (args) => [args.tool as string, (args.args as Record<string, unknown>) ?? {}, args.tabId as number | undefined]);
|
|
213
|
+
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]);
|
|
214
|
+
pi.registerTool({
|
|
215
|
+
name: "surf_oracle_result",
|
|
216
|
+
label: "surf_oracle_result",
|
|
217
|
+
description: "Capture the result of a Surf oracle job.",
|
|
218
|
+
parameters: Type.Object({ id: Type.String(), timeout: Type.Optional(Type.Number()) }),
|
|
219
|
+
async execute(_id: string, args: Record<string, unknown>) {
|
|
220
|
+
try {
|
|
221
|
+
const result = await requestSurf("oracle.result", args);
|
|
222
|
+
emitOracleFinished(pi, result.details ?? { id: args.id, state: result.isError ? "failed" : undefined });
|
|
223
|
+
return result;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return textResult(error instanceof Error ? error.message : String(error), true);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const oracleJobIds = new Set<string>();
|
|
231
|
+
let sessionGeneration = 0;
|
|
232
|
+
let sessionActive = false;
|
|
233
|
+
pi.registerTool({
|
|
234
|
+
name: "surf_oracle_ask",
|
|
235
|
+
label: "surf_oracle_ask",
|
|
236
|
+
description: "Start a durable local Surf ChatGPT oracle job.",
|
|
237
|
+
parameters: Type.Object({ prompt: Type.String(), model: Type.Optional(Type.String()), effort: Type.Optional(Type.String()), follow: Type.Optional(Type.String()) }),
|
|
238
|
+
async execute(_id: string, args: Record<string, unknown>) {
|
|
239
|
+
const requestGeneration = sessionGeneration;
|
|
240
|
+
try {
|
|
241
|
+
const result = await requestSurf("oracle.ask", args);
|
|
242
|
+
const job = result.details as { id?: string } | undefined;
|
|
243
|
+
rememberOracleJobForSession(oracleJobIds, job?.id, requestGeneration, sessionGeneration, sessionActive);
|
|
244
|
+
return result;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return textResult(error instanceof Error ? error.message : String(error), true);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
let dispose: (() => void) | undefined;
|
|
252
|
+
pi.on("session_start", (_event, ctx) => {
|
|
253
|
+
sessionGeneration++;
|
|
254
|
+
const generation = sessionGeneration;
|
|
255
|
+
sessionActive = false;
|
|
256
|
+
dispose?.();
|
|
257
|
+
dispose = undefined;
|
|
258
|
+
oracleJobIds.clear();
|
|
259
|
+
|
|
260
|
+
const session = ctx as { sessionManager?: { getSessionId?: () => string }; sessionId?: string };
|
|
261
|
+
const sessionId = session.sessionId ?? session.sessionManager?.getSessionId?.();
|
|
262
|
+
if (!sessionId) return;
|
|
263
|
+
try {
|
|
264
|
+
const jobs = require("../native/oracle-jobs.cjs") as { listJobs(): Array<{ id: string; state: string }> };
|
|
265
|
+
dispose = registerOptionalBackgroundProvider(sessionId, oracleJobIds, jobs.listJobs, registerGlobalBackgroundProvider);
|
|
266
|
+
sessionActive = true;
|
|
267
|
+
void resolveBackgroundWorkRegister().then((register) => {
|
|
268
|
+
try {
|
|
269
|
+
if (register === registerGlobalBackgroundProvider || generation !== sessionGeneration) return;
|
|
270
|
+
const nextDispose = registerOptionalBackgroundProvider(sessionId, oracleJobIds, jobs.listJobs, register);
|
|
271
|
+
if (generation !== sessionGeneration) {
|
|
272
|
+
nextDispose();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
dispose?.();
|
|
276
|
+
dispose = nextDispose;
|
|
277
|
+
} catch {
|
|
278
|
+
// Keep the already-registered fallback provider.
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
} catch {
|
|
282
|
+
// The Pi bridge is optional. Browser tools work without pi-subagents.
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
pi.on("session_shutdown", () => {
|
|
286
|
+
sessionGeneration++;
|
|
287
|
+
sessionActive = false;
|
|
288
|
+
dispose?.();
|
|
289
|
+
dispose = undefined;
|
|
290
|
+
oracleJobIds.clear();
|
|
291
|
+
});
|
|
292
|
+
}
|
package/skills/README.md
CHANGED
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
|
|
@@ -292,9 +292,26 @@ surf page.read --depth 3 # Limit tree depth
|
|
|
292
292
|
surf page.read --compact # Minimal output for LLM efficiency
|
|
293
293
|
surf page.read --max-bytes 2000 # Cap visible text at a UTF-8 byte boundary
|
|
294
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
|
|
295
297
|
surf page.state # Modals, loading state, scroll info
|
|
296
298
|
```
|
|
297
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
|
+
|
|
298
315
|
## Semantic Element Location
|
|
299
316
|
|
|
300
317
|
Find and act on elements by role, text, or label instead of refs:
|
|
@@ -687,10 +704,11 @@ surf wait.element ".missing" --auto-capture --timeout 2000
|
|
|
687
704
|
12. **Window isolation** - Use `window.new` + `--window-id` or `--tab-id` to keep agent work separate from your browsing
|
|
688
705
|
13. **Request lock** - Non-streaming browser CLI requests serialize per socket; use `--no-lock` only when you intentionally want to bypass it
|
|
689
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
|
|
690
|
-
15. **
|
|
691
|
-
16. **
|
|
692
|
-
17. **
|
|
693
|
-
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
|
|
694
712
|
|
|
695
713
|
## Socket API
|
|
696
714
|
|