surf-cli 2.9.0 → 2.11.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 +61 -4
- package/dist/content/accessibility-tree.js +11 -0
- package/dist/content/accessibility-tree.js.map +1 -0
- package/dist/content/visual-indicator.js +111 -0
- package/dist/content/visual-indicator.js.map +1 -0
- package/dist/manifest.json +11 -2
- package/dist/options/options.js +3 -3
- package/dist/options/options.js.map +1 -1
- package/dist/service-worker/index.js +61 -261
- package/dist/service-worker/index.js.map +1 -1
- package/native/activity-journal.cjs +55 -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 +254 -664
- package/native/cli.cjs +100 -273
- package/native/do-executor.cjs +52 -475
- package/native/do-parser.cjs +8 -249
- package/native/host-helpers.cjs +32 -15
- package/native/host-sessions.cjs +6 -1
- package/native/host.cjs +228 -6
- package/native/network-export.cjs +20 -17
- package/native/network-store.cjs +38 -58
- 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-authoring.cjs +44 -0
- package/native/playbook-cli.cjs +157 -0
- package/native/playbook-client.cjs +259 -0
- package/native/playbook-receipts.cjs +109 -0
- package/native/playbook-records.cjs +208 -0
- package/native/playbook-runtime.cjs +177 -0
- package/native/playbooks.cjs +235 -0
- package/native/private-state.cjs +156 -0
- package/native/redaction.cjs +104 -0
- package/native/workflow-definition.cjs +369 -0
- package/native/workflow-runtime.cjs +225 -0
- package/package.json +2 -1
- package/playbooks/page/ops/read.json +22 -0
- package/playbooks/page/playbook.json +7 -0
- package/skills/surf/SKILL.md +72 -1
- package/dist/content/index.js +0 -116
- package/dist/content/index.js.map +0 -1
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
const { openClientTransport } = require("./client-transport.cjs");
|
|
2
|
+
const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
|
|
3
|
+
const { assembleContext } = require("./oracle-context.cjs");
|
|
4
|
+
|
|
5
|
+
const RESULT_TIMEOUT_SECONDS = 20;
|
|
6
|
+
const POLL_DELAYS_MS = [5000, 10000, 20000, 40000, 60000];
|
|
7
|
+
const ORACLE_ERROR_CODES = new Set([
|
|
8
|
+
"auth",
|
|
9
|
+
"capacity",
|
|
10
|
+
"cloudflare",
|
|
11
|
+
"context_incomplete",
|
|
12
|
+
"dispatch_failed",
|
|
13
|
+
"harvest_failed",
|
|
14
|
+
"invalid_transition",
|
|
15
|
+
"model_verification_failed",
|
|
16
|
+
"not_found",
|
|
17
|
+
"rate_limit",
|
|
18
|
+
"remote_unsupported",
|
|
19
|
+
"sensitive_blocked",
|
|
20
|
+
"timeout",
|
|
21
|
+
]);
|
|
22
|
+
const HELP = `Usage: surf oracle <ask|status|result|follow|list>
|
|
23
|
+
|
|
24
|
+
Commands:
|
|
25
|
+
ask <prompt> Start a consult and wait for its response
|
|
26
|
+
follow <id> <prompt> Continue a captured consult
|
|
27
|
+
status [id] Show a job (newest when id is omitted)
|
|
28
|
+
result <id> [--wait] Try to capture a result, optionally keep waiting
|
|
29
|
+
list List jobs newest-first
|
|
30
|
+
|
|
31
|
+
Ask/follow options:
|
|
32
|
+
--files <glob> Add context files (repeatable)
|
|
33
|
+
--model <model> Select a ChatGPT model
|
|
34
|
+
--effort <effort> Select reasoning effort
|
|
35
|
+
--detach Return after dispatch
|
|
36
|
+
--allow-sensitive Allow deny-listed context files
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--json Output machine-readable JSON
|
|
40
|
+
--no-lock Bypass the browser request lock`;
|
|
41
|
+
|
|
42
|
+
function codedError(code, message, details = {}) {
|
|
43
|
+
const error = new Error(message);
|
|
44
|
+
error.code = code;
|
|
45
|
+
Object.assign(error, details);
|
|
46
|
+
return error;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function requireOptionValue(argv, index, name) {
|
|
50
|
+
const value = argv[index + 1];
|
|
51
|
+
if (!value || value.startsWith("--")) {
|
|
52
|
+
throw codedError("invalid_transition", `--${name} requires a value`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseOptions(argv) {
|
|
58
|
+
const positional = [];
|
|
59
|
+
const options = { files: [] };
|
|
60
|
+
const valueOptions = new Set(["files", "model", "effort"]);
|
|
61
|
+
const booleanOptions = new Set([
|
|
62
|
+
"allow-sensitive",
|
|
63
|
+
"detach",
|
|
64
|
+
"json",
|
|
65
|
+
"no-lock",
|
|
66
|
+
"wait",
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
70
|
+
const value = argv[index];
|
|
71
|
+
if (!value.startsWith("--")) {
|
|
72
|
+
positional.push(value);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const name = value.slice(2);
|
|
76
|
+
if (valueOptions.has(name)) {
|
|
77
|
+
const optionValue = requireOptionValue(argv, index, name);
|
|
78
|
+
if (name === "files") options.files.push(optionValue);
|
|
79
|
+
else options[name] = optionValue;
|
|
80
|
+
index += 1;
|
|
81
|
+
} else if (booleanOptions.has(name)) {
|
|
82
|
+
options[name] = true;
|
|
83
|
+
} else {
|
|
84
|
+
throw codedError("invalid_transition", `Unknown oracle option: --${name}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { positional, options };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function assertAllowedOptions(command, options) {
|
|
91
|
+
const common = new Set(["json", "no-lock"]);
|
|
92
|
+
const ask = new Set([
|
|
93
|
+
...common,
|
|
94
|
+
"allow-sensitive",
|
|
95
|
+
"detach",
|
|
96
|
+
"effort",
|
|
97
|
+
"files",
|
|
98
|
+
"model",
|
|
99
|
+
]);
|
|
100
|
+
const allowed = command === "ask" || command === "follow"
|
|
101
|
+
? ask
|
|
102
|
+
: command === "result" ? new Set([...common, "wait"]) : common;
|
|
103
|
+
for (const [name, value] of Object.entries(options)) {
|
|
104
|
+
if (name === "files" && value.length === 0) continue;
|
|
105
|
+
if (value !== undefined && !allowed.has(name)) {
|
|
106
|
+
throw codedError("invalid_transition", `--${name} is not supported by oracle ${command}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseOracleCommand(argv) {
|
|
112
|
+
if (argv[0] !== "oracle") return { handled: false };
|
|
113
|
+
const command = argv[1];
|
|
114
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
115
|
+
return { handled: true, command: "help", json: false };
|
|
116
|
+
}
|
|
117
|
+
if (!["ask", "follow", "status", "result", "list"].includes(command)) {
|
|
118
|
+
throw codedError("invalid_transition", `Unknown oracle command: ${command}`);
|
|
119
|
+
}
|
|
120
|
+
const parsed = parseOptions(argv.slice(2));
|
|
121
|
+
assertAllowedOptions(command, parsed.options);
|
|
122
|
+
const json = parsed.options.json === true;
|
|
123
|
+
|
|
124
|
+
if (command === "ask") {
|
|
125
|
+
const prompt = parsed.positional.join(" ");
|
|
126
|
+
if (!prompt.trim()) {
|
|
127
|
+
throw codedError("dispatch_failed", "Usage: surf oracle ask <prompt> [options]");
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
handled: true,
|
|
131
|
+
command,
|
|
132
|
+
prompt,
|
|
133
|
+
files: parsed.options.files,
|
|
134
|
+
model: parsed.options.model,
|
|
135
|
+
effort: parsed.options.effort,
|
|
136
|
+
detach: parsed.options.detach === true,
|
|
137
|
+
allowSensitive: parsed.options["allow-sensitive"] === true,
|
|
138
|
+
json,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (command === "follow") {
|
|
143
|
+
const id = parsed.positional[0];
|
|
144
|
+
const prompt = parsed.positional.slice(1).join(" ");
|
|
145
|
+
if (!id?.trim() || !prompt.trim()) {
|
|
146
|
+
throw codedError("dispatch_failed", "Usage: surf oracle follow <id> <prompt> [options]");
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
handled: true,
|
|
150
|
+
command,
|
|
151
|
+
id,
|
|
152
|
+
prompt,
|
|
153
|
+
files: parsed.options.files,
|
|
154
|
+
model: parsed.options.model,
|
|
155
|
+
effort: parsed.options.effort,
|
|
156
|
+
detach: parsed.options.detach === true,
|
|
157
|
+
allowSensitive: parsed.options["allow-sensitive"] === true,
|
|
158
|
+
json,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (command === "status") {
|
|
163
|
+
if (parsed.positional.length > 1 || parsed.positional[0] === "") {
|
|
164
|
+
throw codedError("not_found", "Usage: surf oracle status [id]");
|
|
165
|
+
}
|
|
166
|
+
return { handled: true, command, id: parsed.positional[0], json };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (command === "result") {
|
|
170
|
+
if (parsed.positional.length !== 1 || !parsed.positional[0].trim()) {
|
|
171
|
+
throw codedError("not_found", "Usage: surf oracle result <id> [--wait]");
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
handled: true,
|
|
175
|
+
command,
|
|
176
|
+
id: parsed.positional[0],
|
|
177
|
+
wait: parsed.options.wait === true,
|
|
178
|
+
json,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (parsed.positional.length > 0) {
|
|
183
|
+
throw codedError("invalid_transition", "Usage: surf oracle list");
|
|
184
|
+
}
|
|
185
|
+
return { handled: true, command, json };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function composeAskRequest(spec, context) {
|
|
189
|
+
const prompt = context ? `${spec.prompt}\n\n${context.envelope}` : spec.prompt;
|
|
190
|
+
return {
|
|
191
|
+
prompt,
|
|
192
|
+
...(spec.model ? { model: spec.model } : {}),
|
|
193
|
+
...(spec.effort ? { effort: spec.effort } : {}),
|
|
194
|
+
...(context ? { contextManifest: context.manifest } : {}),
|
|
195
|
+
...(context?.bundlePath ? { bundlePath: context.bundlePath } : {}),
|
|
196
|
+
...(spec.id ? { follow: spec.id } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function unwrapResponse(response) {
|
|
201
|
+
if (response?.error) {
|
|
202
|
+
const message = response.error.message
|
|
203
|
+
|| response.error.content?.[0]?.text
|
|
204
|
+
|| JSON.stringify(response.error);
|
|
205
|
+
throw codedError(response.error.code || "timeout", message, {
|
|
206
|
+
...(response.error.jobId ? { jobId: response.error.jobId } : {}),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const text = response?.result?.content?.[0]?.text;
|
|
210
|
+
if (text === undefined) return response?.result;
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(text);
|
|
213
|
+
} catch {
|
|
214
|
+
return text;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function requestHost(endpoint, tool, args, withBrowserLock) {
|
|
219
|
+
const execute = async () => {
|
|
220
|
+
const timeoutMs = resolveRequestDeadlineMs(tool, args);
|
|
221
|
+
const transport = await openClientTransport(endpoint, { requestTimeoutMs: timeoutMs });
|
|
222
|
+
try {
|
|
223
|
+
const request = {
|
|
224
|
+
type: "tool_request",
|
|
225
|
+
method: "execute_tool",
|
|
226
|
+
params: { tool, args },
|
|
227
|
+
id: `oracle-${Date.now()}-${Math.random()}`,
|
|
228
|
+
};
|
|
229
|
+
return unwrapResponse(await transport.request(request, timeoutMs));
|
|
230
|
+
} finally {
|
|
231
|
+
await transport.close();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
return withBrowserLock ? withBrowserLock(execute) : execute();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function recoveryHint(id) {
|
|
238
|
+
return `Recover with: surf oracle result ${id}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function classifyError(error, fallbackCode) {
|
|
242
|
+
if (!ORACLE_ERROR_CODES.has(error?.code)) error.code = fallbackCode;
|
|
243
|
+
return error;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function shapeOracleError(error, fallbackCode = "timeout") {
|
|
247
|
+
const jobId = typeof error?.jobId === "string" ? error.jobId : undefined;
|
|
248
|
+
let message = error?.message || String(error);
|
|
249
|
+
if (error?.recoverable && jobId && !message.includes("Recover with:")) {
|
|
250
|
+
message = `${message}\n${recoveryHint(jobId)}`;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
error: {
|
|
254
|
+
code: ORACLE_ERROR_CODES.has(error?.code) ? error.code : fallbackCode,
|
|
255
|
+
message,
|
|
256
|
+
},
|
|
257
|
+
...(jobId ? { jobId } : {}),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function formatOracleError(error, json = false) {
|
|
262
|
+
const shaped = shapeOracleError(error);
|
|
263
|
+
if (json) return JSON.stringify(shaped, null, 2);
|
|
264
|
+
return `Error: ${shaped.error.message}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function formatOracleOutput(value, json = false) {
|
|
268
|
+
if (json) return JSON.stringify(value, null, 2);
|
|
269
|
+
if (typeof value === "string") return value;
|
|
270
|
+
if (Array.isArray(value)) {
|
|
271
|
+
if (value.length === 0) return "No oracle jobs.";
|
|
272
|
+
return value.map((job) => `${job.id}\t${job.state}`).join("\n");
|
|
273
|
+
}
|
|
274
|
+
const lines = [value.id, value.state];
|
|
275
|
+
if (value.response !== undefined) lines.push(value.response);
|
|
276
|
+
return lines.filter((line) => line !== undefined).join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function waitForResult(job, spec, io) {
|
|
280
|
+
const startedAt = Date.now();
|
|
281
|
+
const interrupt = () => {
|
|
282
|
+
const message = `Interrupted. ${recoveryHint(job.id)}`;
|
|
283
|
+
if (spec.json) {
|
|
284
|
+
io.stderr.write(`${JSON.stringify({
|
|
285
|
+
error: { code: "timeout", message },
|
|
286
|
+
jobId: job.id,
|
|
287
|
+
}, null, 2)}\n`);
|
|
288
|
+
} else {
|
|
289
|
+
io.stderr.write(`${recoveryHint(job.id)}\n`);
|
|
290
|
+
}
|
|
291
|
+
process.exit(130);
|
|
292
|
+
};
|
|
293
|
+
process.once("SIGINT", interrupt);
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
let current = job;
|
|
297
|
+
let pollIndex = 0;
|
|
298
|
+
while (current.state !== "captured") {
|
|
299
|
+
try {
|
|
300
|
+
current = await requestHost(
|
|
301
|
+
io.endpoint,
|
|
302
|
+
"oracle.result",
|
|
303
|
+
{
|
|
304
|
+
id: current.id,
|
|
305
|
+
timeout: RESULT_TIMEOUT_SECONDS,
|
|
306
|
+
},
|
|
307
|
+
io.withBrowserLock,
|
|
308
|
+
);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
error.jobId ||= current.id;
|
|
311
|
+
error.recoverable = true;
|
|
312
|
+
throw classifyError(error, "timeout");
|
|
313
|
+
}
|
|
314
|
+
if (!spec.json && io.stderr.isTTY) {
|
|
315
|
+
const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000);
|
|
316
|
+
io.stderr.write(`[${elapsedSeconds}s] ${current.state}\n`);
|
|
317
|
+
}
|
|
318
|
+
if (current.state === "failed") {
|
|
319
|
+
throw codedError(
|
|
320
|
+
current.error?.code || "harvest_failed",
|
|
321
|
+
current.error?.message || `oracle job ${current.id} failed`,
|
|
322
|
+
{ jobId: current.id },
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (current.state !== "captured") {
|
|
326
|
+
const delayMs = POLL_DELAYS_MS[Math.min(pollIndex, POLL_DELAYS_MS.length - 1)];
|
|
327
|
+
pollIndex += 1;
|
|
328
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return current;
|
|
332
|
+
} finally {
|
|
333
|
+
process.removeListener("SIGINT", interrupt);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function handleOracleCli(argv, {
|
|
338
|
+
endpoint,
|
|
339
|
+
cwd = process.cwd(),
|
|
340
|
+
stderr = process.stderr,
|
|
341
|
+
withBrowserLock,
|
|
342
|
+
} = {}) {
|
|
343
|
+
const spec = parseOracleCommand(argv);
|
|
344
|
+
if (!spec.handled) return spec;
|
|
345
|
+
if (spec.command === "help") return { handled: true, value: HELP, json: false };
|
|
346
|
+
if (endpoint?.kind === "remote") {
|
|
347
|
+
throw codedError(
|
|
348
|
+
"remote_unsupported",
|
|
349
|
+
"oracle commands are not supported with remote endpoints",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const io = { endpoint, stderr, withBrowserLock };
|
|
354
|
+
if (spec.command === "status") {
|
|
355
|
+
try {
|
|
356
|
+
const value = await requestHost(endpoint, "oracle.status", spec.id ? { id: spec.id } : {});
|
|
357
|
+
return { handled: true, value, json: spec.json };
|
|
358
|
+
} catch (error) {
|
|
359
|
+
throw classifyError(error, "timeout");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (spec.command === "list") {
|
|
363
|
+
try {
|
|
364
|
+
const value = await requestHost(endpoint, "oracle.list", {});
|
|
365
|
+
return { handled: true, value, json: spec.json };
|
|
366
|
+
} catch (error) {
|
|
367
|
+
throw classifyError(error, "timeout");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (spec.command === "result") {
|
|
371
|
+
if (spec.wait) {
|
|
372
|
+
const value = await waitForResult({ id: spec.id, state: "created" }, spec, io);
|
|
373
|
+
return { handled: true, value, json: spec.json };
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
const value = await requestHost(
|
|
377
|
+
endpoint,
|
|
378
|
+
"oracle.result",
|
|
379
|
+
{
|
|
380
|
+
id: spec.id,
|
|
381
|
+
timeout: RESULT_TIMEOUT_SECONDS,
|
|
382
|
+
},
|
|
383
|
+
withBrowserLock,
|
|
384
|
+
);
|
|
385
|
+
return { handled: true, value, json: spec.json };
|
|
386
|
+
} catch (error) {
|
|
387
|
+
error.jobId ||= spec.id;
|
|
388
|
+
throw classifyError(error, "timeout");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const context = spec.files.length > 0
|
|
393
|
+
? await assembleContext({
|
|
394
|
+
files: spec.files,
|
|
395
|
+
cwd,
|
|
396
|
+
allowSensitive: spec.allowSensitive,
|
|
397
|
+
})
|
|
398
|
+
: null;
|
|
399
|
+
const request = composeAskRequest(spec, context);
|
|
400
|
+
const dispatchInterrupt = () => {
|
|
401
|
+
stderr.write(
|
|
402
|
+
"Interrupted during dispatch. A job may already have been created. Run surf oracle status or surf oracle list to find it.\n",
|
|
403
|
+
);
|
|
404
|
+
process.exit(130);
|
|
405
|
+
};
|
|
406
|
+
process.once("SIGINT", dispatchInterrupt);
|
|
407
|
+
let value;
|
|
408
|
+
try {
|
|
409
|
+
value = await requestHost(endpoint, "oracle.ask", request, withBrowserLock);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
throw classifyError(error, "dispatch_failed");
|
|
412
|
+
} finally {
|
|
413
|
+
process.removeListener("SIGINT", dispatchInterrupt);
|
|
414
|
+
}
|
|
415
|
+
if (spec.detach || value.state === "captured") {
|
|
416
|
+
if (spec.detach && value.state === "dispatched") {
|
|
417
|
+
stderr.write(
|
|
418
|
+
`Warning: the durable conversation URL is not yet captured. Run surf oracle result ${value.id} promptly.\n`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return { handled: true, value, json: spec.json };
|
|
422
|
+
}
|
|
423
|
+
value = await waitForResult(value, spec, io);
|
|
424
|
+
return { handled: true, value, json: spec.json };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
module.exports = {
|
|
428
|
+
composeAskRequest,
|
|
429
|
+
formatOracleError,
|
|
430
|
+
formatOracleOutput,
|
|
431
|
+
handleOracleCli,
|
|
432
|
+
parseOracleCommand,
|
|
433
|
+
shapeOracleError,
|
|
434
|
+
};
|