surf-cli 2.15.0 → 2.15.1
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 +2 -2
- 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/package.json +5 -4
- package/pi-extension/surf.ts +66 -52
- package/skills/surf/SKILL.md +2 -2
|
@@ -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 = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "surf-cli",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.1",
|
|
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
|
|
@@ -114,7 +114,7 @@ surf oracle result <job-id> --wait --json
|
|
|
114
114
|
|
|
115
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.
|
|
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: pro` selects ChatGPT GPT-5.6 Sol Pro web mode through the browser. `reattach` only harvests an existing job by ID; it never submits the prompt again.
|
|
118
118
|
|
|
119
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`.
|
|
120
120
|
|