surf-cli 2.10.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/native/cli.cjs CHANGED
@@ -19,6 +19,11 @@ const {
19
19
  const { executeDoSteps } = require("./do-executor.cjs");
20
20
  const { openClientTransport } = require("./client-transport.cjs");
21
21
  const { version: VERSION } = require("../package.json");
22
+ const {
23
+ formatOracleError,
24
+ formatOracleOutput,
25
+ handleOracleCli,
26
+ } = require("./oracle-cli.cjs");
22
27
  const { formatPlaybookOutput, handlePlaybookCli, playbookCommandNeedsBrowser } = require("./playbook-cli.cjs");
23
28
 
24
29
  const IS_WIN = process.platform === "win32";
@@ -73,6 +78,28 @@ function installBrowserLock({ noLock, timeoutMs }, endpoint) {
73
78
  });
74
79
  }
75
80
 
81
+ async function runWithBrowserLock(lockOptions, endpoint, operation) {
82
+ let releaseBrowserLock = () => {};
83
+ if (!lockOptions.noLock) {
84
+ const lock = acquireBrowserLock(endpoint.key, SURF_TMP, {
85
+ timeoutMs: lockOptions.timeoutMs,
86
+ });
87
+ releaseBrowserLock = lock.release;
88
+ }
89
+ const release = () => {
90
+ const releaseCurrent = releaseBrowserLock;
91
+ releaseBrowserLock = () => {};
92
+ releaseCurrent();
93
+ };
94
+ process.once("exit", release);
95
+ try {
96
+ return await operation();
97
+ } finally {
98
+ process.removeListener("exit", release);
99
+ release();
100
+ }
101
+ }
102
+
76
103
  // Cross-platform image resize (macOS: sips, Linux: ImageMagick)
77
104
  function resizeImage(filePath, maxSize) {
78
105
  const platform = process.platform;
@@ -153,6 +180,28 @@ try {
153
180
  process.exit(1);
154
181
  }
155
182
 
183
+ if (args[0] === "oracle") {
184
+ handleOracleCli(args, {
185
+ endpoint,
186
+ cwd: process.cwd(),
187
+ withBrowserLock: (operation) => runWithBrowserLock(
188
+ parseBrowserLockOptions(args.includes("--no-lock")),
189
+ endpoint,
190
+ operation,
191
+ ),
192
+ })
193
+ .then((result) => {
194
+ if (!result.handled) throw new Error("Oracle command was not handled");
195
+ if (result.value !== undefined) console.log(formatOracleOutput(result.value, result.json));
196
+ process.exit(0);
197
+ })
198
+ .catch((error) => {
199
+ console.error(formatOracleError(error, args.includes("--json")));
200
+ process.exit(1);
201
+ });
202
+ return;
203
+ }
204
+
156
205
  if (["playbook", "pb", "use"].includes(args[0])) {
157
206
  if (playbookCommandNeedsBrowser(args)) {
158
207
  installBrowserLock(parseBrowserLockOptions(args.includes("--no-lock")), endpoint);
@@ -1496,6 +1545,7 @@ Common Commands:
1496
1545
  search <term> Search for text in page (alias: find)
1497
1546
  window.new <url> Create isolated browser window
1498
1547
  doctor Diagnose native host/socket setup
1548
+ oracle ask <prompt> Start a durable ChatGPT consult
1499
1549
  wait <seconds> Wait N seconds
1500
1550
 
1501
1551
  Quick Examples:
@@ -1555,6 +1605,9 @@ const showFullHelp = () => {
1555
1605
 
1556
1606
  Usage: surf <command> [args] [options]
1557
1607
 
1608
+ Oracle:
1609
+ surf oracle <ask|status|result|follow|list>
1610
+
1558
1611
  Playbooks:
1559
1612
  surf playbook|pb <list|show|ops|run|record|suggest|save|client|trace|export|import>
1560
1613
  surf use <playbook> <op> [--arg value]
@@ -13,6 +13,21 @@ function normalizeModelString(model) {
13
13
  return String(model || "").trim().toLowerCase();
14
14
  }
15
15
 
16
+ function formatToolError(error) {
17
+ const message = error instanceof Error
18
+ ? error.message
19
+ : typeof error === "string"
20
+ ? error
21
+ : error?.message || String(error);
22
+ const result = { content: [{ type: "text", text: message }] };
23
+ if (error && typeof error === "object") {
24
+ result.message = message;
25
+ if (typeof error.code === "string") result.code = error.code;
26
+ if (typeof error.jobId === "string") result.jobId = error.jobId;
27
+ }
28
+ return result;
29
+ }
30
+
16
31
  /**
17
32
  * Format tool result content for MCP response
18
33
  * @param {*} result - The result object from the extension
@@ -1082,6 +1097,16 @@ function mapToolToMessage(tool, args, tabId) {
1082
1097
  case "history.search":
1083
1098
  if (!a.query) throw new Error("query required");
1084
1099
  return { type: "HISTORY_SEARCH", query: a.query, limit: a.limit !== undefined ? parseInt(a.limit, 10) : 20 };
1100
+ case "oracle.ask":
1101
+ if (!a.prompt) throw new Error("prompt required");
1102
+ return { ...a, type: "ORACLE_ASK" };
1103
+ case "oracle.status":
1104
+ return { ...a, type: "ORACLE_STATUS" };
1105
+ case "oracle.result":
1106
+ if (!a.id) throw new Error("id required");
1107
+ return { ...a, type: "ORACLE_RESULT" };
1108
+ case "oracle.list":
1109
+ return { type: "ORACLE_LIST" };
1085
1110
  case "chatgpt":
1086
1111
  if (!a.query) throw new Error("query required");
1087
1112
  return {
@@ -1196,4 +1221,4 @@ function mapToolToMessage(tool, args, tabId) {
1196
1221
  }
1197
1222
  }
1198
1223
 
1199
- module.exports = { mapToolToMessage, mapComputerAction, formatToolContent, buildProviderUploadMessage };
1224
+ module.exports = { mapToolToMessage, mapComputerAction, formatToolContent, formatToolError, buildProviderUploadMessage };
@@ -23,6 +23,7 @@ const PROVIDER_DEFAULT_TIMEOUT_SECONDS = {
23
23
  gemini: 300,
24
24
  grok: 300,
25
25
  perplexity: 120,
26
+ "oracle.result": 300,
26
27
  "playbook.run": 600,
27
28
  };
28
29
 
package/native/host.cjs CHANGED
@@ -14,7 +14,8 @@ const perplexityClient = require("./perplexity-client.cjs");
14
14
  const grokClient = require("./grok-client.cjs");
15
15
  const aistudioClient = require("./aistudio-client.cjs");
16
16
  const aistudioBuild = require("./aistudio-build.cjs");
17
- const { mapToolToMessage, mapComputerAction, formatToolContent, buildProviderUploadMessage } = require("./host-helpers.cjs");
17
+ const { mapToolToMessage, mapComputerAction, formatToolContent, formatToolError, buildProviderUploadMessage } = require("./host-helpers.cjs");
18
+ const { createOracleHost } = require("./oracle-host.cjs");
18
19
 
19
20
  const IS_WIN = process.platform === "win32";
20
21
  const { SOCKET_PATH, SURF_TMP } = require("./socket-path.cjs");
@@ -402,6 +403,15 @@ aiQueue = new BoundedAiQueue({
402
403
  : handler(),
403
404
  });
404
405
 
406
+ const oracleHost = createOracleHost({
407
+ queueAiRequest,
408
+ requestCallExtension,
409
+ buildProviderUploadMessage,
410
+ log,
411
+ });
412
+ const adoptedOracleJobs = oracleHost.adoptOrphans();
413
+ log(`Oracle adoption: ${adoptedOracleJobs.length} job(s); ids=${adoptedOracleJobs.map((job) => job.id).join(",") || "none"}`);
414
+
405
415
  function sendSocket(socket, value, options = {}) {
406
416
  const writer = socketWriters.get(socket);
407
417
  return writer ? writer.send(value, options) : writeFrame(socket, value);
@@ -672,8 +682,14 @@ function sendToolResponse(socket, id, result, error) {
672
682
  } catch (transferFailure) {
673
683
  finalError = transferFailure.message;
674
684
  }
675
- if (finalError && request) {
676
- finalError = rewriteTransferPaths(finalError, request.pathRewrites || []);
685
+ const formattedError = finalError ? formatToolError(finalError) : null;
686
+ if (formattedError && request) {
687
+ const rewrittenMessage = rewriteTransferPaths(
688
+ formattedError.content[0].text,
689
+ request.pathRewrites || [],
690
+ );
691
+ formattedError.content[0].text = rewrittenMessage;
692
+ if (formattedError.message) formattedError.message = rewrittenMessage;
677
693
  }
678
694
  if (request?.tool && !request.tool.startsWith("playbook.")) {
679
695
  const metadata = commandMetadata(request.tool);
@@ -697,7 +713,7 @@ function sendToolResponse(socket, id, result, error) {
697
713
  : finalError ? "error" : "completed";
698
714
  await completeOwnedRequest(context, id, outcome);
699
715
  const response = { type: "tool_response", id };
700
- if (finalError) response.error = { content: [{ type: "text", text: finalError }] };
716
+ if (formattedError) response.error = formattedError;
701
717
  else response.result = { content: formatToolContent(output, log, { suppressImages: Boolean(context?.isRemote) }) };
702
718
  if (!context?.closed) await sendSocket(socket, response);
703
719
  })().catch((sendError) => log(`Error sending tool_response: ${sendError.message}`));
@@ -800,6 +816,13 @@ function handleToolRequest(msg, socket, requestContext = requestStorage.getStore
800
816
  .catch((error) => sendToolResponse(socket, originalId, null, error.message));
801
817
  return;
802
818
  }
819
+
820
+ if (extensionMsg.type.startsWith("ORACLE_")) {
821
+ Promise.resolve(oracleHost.handle(requestContext, extensionMsg))
822
+ .then((result) => sendToolResponse(socket, originalId, result, null))
823
+ .catch((error) => sendToolResponse(socket, originalId, null, error));
824
+ return;
825
+ }
803
826
 
804
827
  if (extensionMsg.type === "BATCH_EXECUTE") {
805
828
  executeBatch(extensionMsg.actions, extensionMsg.tabId, socket, originalId, requestContext);
@@ -1948,6 +1971,7 @@ const handleClient = (socket) => {
1948
1971
  }
1949
1972
  log(`Handling tool_request: ${msg.method} ${tool}${principal ? ` for ${principal.label}` : ""}`);
1950
1973
  try {
1974
+ if (tool.startsWith("oracle.")) oracleHost.assertLocal(request);
1951
1975
  if (isRemote) {
1952
1976
  await applyRequestTransfers(msg, request, transferState, ensureTransferState);
1953
1977
  }
@@ -1955,7 +1979,7 @@ const handleClient = (socket) => {
1955
1979
  requestStorage.run(request, () => handleToolRequest(msg, socket, request));
1956
1980
  } catch (e) {
1957
1981
  await discardRequestTransfers(msg, transferState);
1958
- sendToolResponse(socket, msg.id || null, null, e.message || "Request failed");
1982
+ sendToolResponse(socket, msg.id || null, null, e?.code ? e : e.message || "Request failed");
1959
1983
  }
1960
1984
  return;
1961
1985
  }
@@ -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
+ };