surf-cli 2.15.0 → 2.15.2
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 +4 -4
- package/agents/gpt-pro.md +2 -1
- package/dist/service-worker/index.js +14 -14
- package/dist/service-worker/index.js.map +1 -1
- package/native/aistudio-parser.cjs +5 -2
- package/native/chatgpt-client-selection.cjs +1 -1
- package/native/chatgpt-client-ui.cjs +20 -3
- package/package.json +5 -4
- package/pi-extension/surf.ts +66 -52
- package/skills/surf/SKILL.md +4 -4
|
@@ -327,8 +327,11 @@ function parseAiStudioGenerateContentText(rawText) {
|
|
|
327
327
|
let parsed;
|
|
328
328
|
try {
|
|
329
329
|
parsed = JSON.parse(normalized);
|
|
330
|
-
} catch (
|
|
331
|
-
|
|
330
|
+
} catch (error) {
|
|
331
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
332
|
+
throw new Error(`Invalid GenerateContent JSON (${normalized.length} chars): ${message}`, {
|
|
333
|
+
cause: error,
|
|
334
|
+
});
|
|
332
335
|
}
|
|
333
336
|
|
|
334
337
|
const segments = [];
|
|
@@ -191,11 +191,22 @@ async function readPicker(cdp, kind, click = false) {
|
|
|
191
191
|
`(() => {
|
|
192
192
|
${buildClickDispatcher()}
|
|
193
193
|
const kind = ${JSON.stringify(kind)};
|
|
194
|
-
|
|
194
|
+
let nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).filter((node) => {
|
|
195
195
|
const value = ((node.getAttribute?.('aria-label') || '') + ' ' + (node.textContent || '')).toLowerCase();
|
|
196
|
-
if (kind
|
|
197
|
-
return value.includes('thinking') || value.includes('
|
|
196
|
+
if (kind !== 'model') return value.includes('thinking') || value.includes('pro');
|
|
197
|
+
return value.includes('gpt') || value.includes('thinking') || value.includes('instant');
|
|
198
198
|
});
|
|
199
|
+
if (kind === 'model' && nodes.length === 0) {
|
|
200
|
+
nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).filter((node) => {
|
|
201
|
+
const value = ((node.getAttribute?.('aria-label') || '') + ' ' + (node.textContent || '')).toLowerCase();
|
|
202
|
+
return value.includes('pro');
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (kind === 'model' && nodes.length === 0) {
|
|
206
|
+
nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).filter((node) =>
|
|
207
|
+
node.getAttribute?.('aria-haspopup') === 'menu' || node.getAttribute?.('aria-expanded') !== null
|
|
208
|
+
);
|
|
209
|
+
}
|
|
199
210
|
const items = nodes.map((node) => {
|
|
200
211
|
const text = (node.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
201
212
|
const aria = (node.getAttribute?.('aria-label') || '').replace(/\\s+/g, ' ').trim();
|
|
@@ -329,6 +340,12 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000, signal) {
|
|
|
329
340
|
if (picker?.items?.length !== 1) throw verificationError("model", desiredModel);
|
|
330
341
|
await delay(300, signal);
|
|
331
342
|
const menu = await waitForMenu(cdp, "model", timeoutMs, signal);
|
|
343
|
+
const currentAdvancedModel = verifyChatGPTModelSelection(
|
|
344
|
+
menu.items.filter((item) => /\bmodel\b/i.test(String(item?.label || ""))),
|
|
345
|
+
desiredModel,
|
|
346
|
+
);
|
|
347
|
+
if (currentAdvancedModel) return currentAdvancedModel.displayLabel || currentAdvancedModel.label;
|
|
348
|
+
|
|
332
349
|
const match = resolveChatGPTModelMenuOption(menu.items, desiredModel);
|
|
333
350
|
if (!match || !(await clickMenuItem(cdp, "model", match))) {
|
|
334
351
|
throw verificationError("model", desiredModel, menu.items);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "surf-cli",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.2",
|
|
4
4
|
"description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chrome",
|
|
@@ -70,11 +70,12 @@
|
|
|
70
70
|
"@types/node": "^26.1.2",
|
|
71
71
|
"@vitest/coverage-v8": "^4.1.9",
|
|
72
72
|
"@vitest/ui": "^4.1.9",
|
|
73
|
-
"
|
|
73
|
+
"pi-subagents": "^0.52.0",
|
|
74
|
+
"puppeteer": "25.6.0",
|
|
75
|
+
"typebox": "^1.3.11",
|
|
74
76
|
"typescript": "^7.0.2",
|
|
75
77
|
"vite": "^8.1.4",
|
|
76
|
-
"vitest": "^4.1.9"
|
|
77
|
-
"typebox": "^1.3.11"
|
|
78
|
+
"vitest": "^4.1.9"
|
|
78
79
|
},
|
|
79
80
|
"pi": {
|
|
80
81
|
"extensions": [
|
package/pi-extension/surf.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
2
|
import { Type } from "typebox";
|
|
4
3
|
|
|
5
4
|
const require = createRequire(import.meta.url);
|
|
@@ -73,29 +72,31 @@ type BackgroundWorkRegistry = {
|
|
|
73
72
|
};
|
|
74
73
|
|
|
75
74
|
type OracleExternalJob = {
|
|
76
|
-
provider: "surf-oracle";
|
|
77
75
|
id: string;
|
|
78
76
|
state: string;
|
|
79
77
|
conversationUrl: string | null;
|
|
80
|
-
requestedModel: string | null;
|
|
81
|
-
verifiedModel: string | null;
|
|
82
|
-
requestedEffort: string | null;
|
|
83
|
-
verifiedEffort: string | null;
|
|
84
|
-
promptDigest: string | null;
|
|
85
78
|
resultText?: string;
|
|
86
|
-
resultArtifact?: { kind: "inline-text"; bytes: number };
|
|
87
79
|
failure?: { code: string; message: string };
|
|
88
80
|
};
|
|
89
81
|
|
|
82
|
+
type PiExternalJobState = "queued" | "running" | "completed" | "failed";
|
|
83
|
+
|
|
84
|
+
type PiExternalJobHandle = {
|
|
85
|
+
providerJobId: string;
|
|
86
|
+
state: PiExternalJobState;
|
|
87
|
+
conversationUrl?: string;
|
|
88
|
+
failureCode?: string;
|
|
89
|
+
failureMessage?: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type PiExternalJobResult = PiExternalJobHandle & { output?: string };
|
|
93
|
+
|
|
90
94
|
type OracleExternalJobProvider = {
|
|
91
95
|
name: "surf-oracle";
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
result(id: string, input?: Record<string, unknown>): Promise<OracleExternalJob>;
|
|
97
|
-
reattach(id: string, input?: Record<string, unknown>): Promise<OracleExternalJob>;
|
|
98
|
-
follow(id: string, message: string, input?: Record<string, unknown>): Promise<OracleExternalJob>;
|
|
96
|
+
start(input: Record<string, unknown>): Promise<PiExternalJobHandle>;
|
|
97
|
+
status(id: string): Promise<PiExternalJobHandle>;
|
|
98
|
+
result(id: string): Promise<PiExternalJobResult>;
|
|
99
|
+
reattach(id: string): Promise<PiExternalJobHandle>;
|
|
99
100
|
};
|
|
100
101
|
|
|
101
102
|
type RegisterExternalJobProvider = (provider: OracleExternalJobProvider) => () => void;
|
|
@@ -251,15 +252,11 @@ export async function resolveExternalJobProviderRegister(
|
|
|
251
252
|
return module.registerExternalJobProvider as RegisterExternalJobProvider;
|
|
252
253
|
}
|
|
253
254
|
} catch {
|
|
254
|
-
// The
|
|
255
|
+
// The Pi bridge is optional. Surf also runs in other coding-agent harnesses and as a direct CLI.
|
|
255
256
|
}
|
|
256
257
|
return registerGlobalExternalJobProvider;
|
|
257
258
|
}
|
|
258
259
|
|
|
259
|
-
function digestPrompt(prompt: string) {
|
|
260
|
-
return `sha256:${createHash("sha256").update(prompt).digest("hex")}`;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
260
|
function asOracleJob(value: unknown): OracleJob {
|
|
264
261
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Surf oracle response did not include job metadata");
|
|
265
262
|
const job = value as Partial<OracleJob>;
|
|
@@ -272,22 +269,53 @@ function oracleExternalJob(job: OracleJob): OracleExternalJob {
|
|
|
272
269
|
const failure = job.error
|
|
273
270
|
? { code: job.error.code || "failed", message: job.error.message || "Surf oracle job failed" }
|
|
274
271
|
: undefined;
|
|
275
|
-
const requestedModel = Object.hasOwn(job, "modelRequested") ? job.modelRequested ?? null : job.model ?? null;
|
|
276
272
|
return {
|
|
277
|
-
provider: "surf-oracle",
|
|
278
273
|
id: job.id,
|
|
279
274
|
state: job.state,
|
|
280
275
|
conversationUrl: job.conversationUrl ?? null,
|
|
281
|
-
|
|
282
|
-
verifiedModel: job.modelVerified ?? null,
|
|
283
|
-
requestedEffort: job.effortRequested ?? null,
|
|
284
|
-
verifiedEffort: job.effortVerified ?? null,
|
|
285
|
-
promptDigest: job.promptDigest ?? null,
|
|
286
|
-
...(resultText === undefined ? {} : { resultText, resultArtifact: { kind: "inline-text", bytes: Buffer.byteLength(resultText, "utf8") } }),
|
|
276
|
+
...(resultText === undefined ? {} : { resultText }),
|
|
287
277
|
...(failure ? { failure } : {}),
|
|
288
278
|
};
|
|
289
279
|
}
|
|
290
280
|
|
|
281
|
+
// pi-subagents' external-job contract rejects unknown fields, null values,
|
|
282
|
+
// untrimmed strings, and non-contract states, so map oracle payloads at this
|
|
283
|
+
// boundary instead of passing them through.
|
|
284
|
+
const PI_STATE_BY_ORACLE_STATE: Record<string, PiExternalJobState> = {
|
|
285
|
+
created: "queued",
|
|
286
|
+
dispatched: "running",
|
|
287
|
+
awaiting: "running",
|
|
288
|
+
captured: "completed",
|
|
289
|
+
failed: "failed",
|
|
290
|
+
};
|
|
291
|
+
const PI_MAX_FAILURE_CODE_CHARS = 128;
|
|
292
|
+
const PI_MAX_FAILURE_MESSAGE_CHARS = 4_096;
|
|
293
|
+
const PI_MAX_OUTPUT_CHARS = 1024 * 1024;
|
|
294
|
+
|
|
295
|
+
function piBounded(value: string, maxChars: number): string {
|
|
296
|
+
return value.slice(0, maxChars).trim();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function piExternalJobHandle(job: OracleExternalJob): PiExternalJobHandle {
|
|
300
|
+
const state = PI_STATE_BY_ORACLE_STATE[job.state];
|
|
301
|
+
if (!state) throw new Error(`Surf oracle job ${job.id} reported unknown state '${job.state}'`);
|
|
302
|
+
const conversationUrl = job.conversationUrl ?? undefined;
|
|
303
|
+
const failureCode = job.failure ? piBounded(job.failure.code, PI_MAX_FAILURE_CODE_CHARS) : "";
|
|
304
|
+
const failureMessage = job.failure ? piBounded(job.failure.message, PI_MAX_FAILURE_MESSAGE_CHARS) : "";
|
|
305
|
+
return {
|
|
306
|
+
providerJobId: job.id,
|
|
307
|
+
state,
|
|
308
|
+
...(conversationUrl ? { conversationUrl } : {}),
|
|
309
|
+
...(failureCode ? { failureCode } : {}),
|
|
310
|
+
...(failureMessage ? { failureMessage } : {}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function piExternalJobResult(job: OracleExternalJob): PiExternalJobResult {
|
|
315
|
+
const output = job.resultText === undefined ? "" : piBounded(job.resultText, PI_MAX_OUTPUT_CHARS);
|
|
316
|
+
return { ...piExternalJobHandle(job), ...(output ? { output } : {}) };
|
|
317
|
+
}
|
|
318
|
+
|
|
291
319
|
async function requestOracleJob(request: typeof requestSurf, tool: string, args: Record<string, unknown>) {
|
|
292
320
|
const result = await request(tool, args);
|
|
293
321
|
if (result.isError) {
|
|
@@ -298,6 +326,7 @@ async function requestOracleJob(request: typeof requestSurf, tool: string, args:
|
|
|
298
326
|
const error = new Error(message);
|
|
299
327
|
if (typeof details?.code === "string") Object.assign(error, { code: details.code });
|
|
300
328
|
if (typeof details?.jobId === "string") Object.assign(error, { jobId: details.jobId });
|
|
329
|
+
if (details?.code === "capacity" && typeof details?.jobId === "string") Object.assign(error, { blockingJobId: details.jobId });
|
|
301
330
|
throw error;
|
|
302
331
|
}
|
|
303
332
|
return oracleExternalJob(asOracleJob(result.details));
|
|
@@ -330,8 +359,6 @@ export function createOracleExternalJobProvider(
|
|
|
330
359
|
): OracleExternalJobProvider {
|
|
331
360
|
return {
|
|
332
361
|
name: "surf-oracle",
|
|
333
|
-
kind: "external-job",
|
|
334
|
-
wakeChannels: [ORACLE_FINISHED_CHANNEL],
|
|
335
362
|
async start(input) {
|
|
336
363
|
const prompt = typeof input.prompt === "string" ? input.prompt : "";
|
|
337
364
|
if (!prompt.trim()) throw new Error("prompt required");
|
|
@@ -343,47 +370,34 @@ export function createOracleExternalJobProvider(
|
|
|
343
370
|
...(effort !== undefined ? { effort } : {}),
|
|
344
371
|
});
|
|
345
372
|
rememberJob(job.id);
|
|
346
|
-
return
|
|
373
|
+
return piExternalJobHandle(job);
|
|
347
374
|
},
|
|
348
|
-
status(id) {
|
|
349
|
-
return requestOracleJob(request, "oracle.status", { id });
|
|
375
|
+
async status(id) {
|
|
376
|
+
return piExternalJobHandle(await requestOracleJob(request, "oracle.status", { id }));
|
|
350
377
|
},
|
|
351
|
-
result(id
|
|
352
|
-
return requestOracleJob(request, "oracle.result", { id
|
|
378
|
+
result(id) {
|
|
379
|
+
return requestOracleJob(request, "oracle.result", { id })
|
|
353
380
|
.then((job) => {
|
|
354
381
|
emitTerminal({ id: job.id, state: job.state });
|
|
355
|
-
return job;
|
|
382
|
+
return piExternalJobResult(job);
|
|
356
383
|
})
|
|
357
384
|
.catch((error) => {
|
|
358
385
|
emitFailedOracleJob(error, emitTerminal);
|
|
359
386
|
throw error;
|
|
360
387
|
});
|
|
361
388
|
},
|
|
362
|
-
reattach(id
|
|
363
|
-
return requestOracleJob(request, "oracle.result", { id
|
|
389
|
+
reattach(id) {
|
|
390
|
+
return requestOracleJob(request, "oracle.result", { id })
|
|
364
391
|
.then((job) => {
|
|
365
392
|
rememberJob(job.id);
|
|
366
393
|
emitTerminal({ id: job.id, state: job.state });
|
|
367
|
-
return job;
|
|
394
|
+
return piExternalJobHandle(job);
|
|
368
395
|
})
|
|
369
396
|
.catch((error) => {
|
|
370
397
|
emitFailedOracleJob(error, emitTerminal);
|
|
371
398
|
throw error;
|
|
372
399
|
});
|
|
373
400
|
},
|
|
374
|
-
async follow(id, message, input = {}) {
|
|
375
|
-
if (!message.trim()) throw new Error("message required");
|
|
376
|
-
const model = oracleOption(input, "model");
|
|
377
|
-
const effort = oracleOption(input, "effort");
|
|
378
|
-
const job = await requestOracleJob(request, "oracle.ask", {
|
|
379
|
-
follow: id,
|
|
380
|
-
prompt: message,
|
|
381
|
-
...(model !== undefined ? { model } : {}),
|
|
382
|
-
...(effort !== undefined ? { effort } : {}),
|
|
383
|
-
});
|
|
384
|
-
rememberJob(job.id);
|
|
385
|
-
return { ...job, promptDigest: job.promptDigest ?? digestPrompt(message) };
|
|
386
|
-
},
|
|
387
401
|
};
|
|
388
402
|
}
|
|
389
403
|
|
package/skills/surf/SKILL.md
CHANGED
|
@@ -40,7 +40,7 @@ Remote paths are client-local by default. `local:./file` is explicit client-loca
|
|
|
40
40
|
## CLI Quick Reference
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
surf --help #
|
|
43
|
+
surf --help # Basic help
|
|
44
44
|
surf <group> # Group help (tab, scroll, page, wait, dialog, emulate, form, perf, ai)
|
|
45
45
|
surf --help-full # All commands
|
|
46
46
|
surf --find <term> # Search tools
|
|
@@ -112,11 +112,11 @@ surf oracle result <job-id> --wait --json
|
|
|
112
112
|
|
|
113
113
|
`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.
|
|
114
114
|
|
|
115
|
-
Treat Pro quota as scarce. Oracle never selects Pro implicitly; request it with `--
|
|
115
|
+
Treat Pro quota as scarce. Oracle never selects Pro effort implicitly; request it with `--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`. Use `--model gpt-5.6-sol --effort pro` for GPT-5.6 Sol with Pro effort. 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.
|
|
116
116
|
|
|
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`,
|
|
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`, and `reattach` to durable Surf Oracle jobs and returns pi-subagents' external-job contract shape: `providerJobId`, a contract state (`queued`, `running`, `completed`, `failed`), the conversation URL, the captured result text as `output`, and failure code and message. It honors `options.model` and `options.effort` for starts, so `model: gpt-5.6-sol` plus `effort: pro` selects ChatGPT GPT-5.6 Sol with Pro effort through the browser. `reattach` only harvests an existing job by ID; it never submits the prompt again.
|
|
118
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.
|
|
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`, `options.model: gpt-5.6-sol`, and `options.effort: pro`. Surf remains useful without Pi or `pi-subagents`.
|
|
120
120
|
|
|
121
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.
|
|
122
122
|
|