surf-cli 2.13.1 → 2.15.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.
@@ -269,7 +269,7 @@ const TOOL_SCHEMAS = {
269
269
  desc: "Ask ChatGPT through the browser session",
270
270
  schema: {
271
271
  query: z.string().describe("Question or prompt"),
272
- model: z.string().optional().describe("ChatGPT model"),
272
+ model: z.string().optional().describe("ChatGPT model: instant, thinking, pro, gpt-5.5, gpt-5.6-sol, or a visible model label"),
273
273
  "with-page": z.boolean().optional().describe("Include current page context"),
274
274
  file: z.string().optional().describe("One attachment path"),
275
275
  timeout: z.number().optional().describe("Timeout in seconds")
@@ -30,8 +30,8 @@ Commands:
30
30
 
31
31
  Ask/follow options:
32
32
  --files <glob> Add context files (repeatable)
33
- --model <model> Select a ChatGPT model
34
- --effort <effort> Select reasoning effort
33
+ --model <model> Select model: instant, thinking, pro, gpt-5.5, gpt-5.6-sol
34
+ --effort <effort> Select effort: light, standard, extended, heavy, pro
35
35
  --detach Return after dispatch
36
36
  --allow-sensitive Allow deny-listed context files
37
37
 
@@ -35,6 +35,24 @@ function codedError(code, message, details = {}) {
35
35
  return error;
36
36
  }
37
37
 
38
+ function promptDigest(prompt) {
39
+ return `sha256:${crypto.createHash("sha256").update(prompt).digest("hex")}`;
40
+ }
41
+
42
+ function hydrateJobMetadata(job, root = getPrivateStateRoot()) {
43
+ const prompt = job.promptDigest ? null : readPrivateFile(path.join(jobDirectory(job.id, root), "request.md"), {
44
+ root,
45
+ encoding: "utf8",
46
+ });
47
+ const legacyModel = job.modelRequested === undefined && job.modelVerified === undefined;
48
+ return {
49
+ ...job,
50
+ modelRequested: job.modelRequested ?? (legacyModel && job.state === "created" ? job.model ?? null : null),
51
+ modelVerified: job.modelVerified ?? (legacyModel && job.state !== "created" ? job.model ?? null : null),
52
+ promptDigest: job.promptDigest ?? promptDigest(prompt),
53
+ };
54
+ }
55
+
38
56
  function readJobs(root = getPrivateStateRoot()) {
39
57
  const base = oracleRoot(root);
40
58
  if (!fs.existsSync(base)) return [];
@@ -44,7 +62,8 @@ function readJobs(root = getPrivateStateRoot()) {
44
62
  .filter((id) => JOB_ID_PATTERN.test(id))
45
63
  .sort((a, b) => b.localeCompare(a))
46
64
  .map((id) => readPrivateJson(path.join(base, id, "job.json"), null, { root }))
47
- .filter(Boolean);
65
+ .filter(Boolean)
66
+ .map((job) => hydrateJobMetadata(job, root));
48
67
  }
49
68
 
50
69
  function createJob({ prompt, contextManifest = {}, model = null, effortRequested = null, follow = null }) {
@@ -84,8 +103,11 @@ function createJob({ prompt, contextManifest = {}, model = null, effortRequested
84
103
  id,
85
104
  state: "created",
86
105
  model,
106
+ modelRequested: model,
107
+ modelVerified: null,
87
108
  effortRequested,
88
109
  effortVerified: null,
110
+ promptDigest: promptDigest(prompt),
89
111
  createdAt: now.toISOString(),
90
112
  dispatchedAt: null,
91
113
  awaitingAt: null,
@@ -110,7 +132,7 @@ function getJob(id) {
110
132
  const root = getPrivateStateRoot();
111
133
  const job = readPrivateJson(path.join(jobDirectory(id, root), "job.json"), null, { root });
112
134
  if (!job) throw codedError("not_found", `oracle job not found: ${id}`);
113
- return job;
135
+ return hydrateJobMetadata(job, root);
114
136
  }
115
137
 
116
138
  function getResponse(id) {
@@ -141,7 +163,7 @@ function markDispatched(id, { tabId, promptEcho, modelVerified, effortVerified }
141
163
  dispatchedAt: new Date().toISOString(),
142
164
  tabId,
143
165
  ...(promptEcho ? { promptEcho } : {}),
144
- ...(modelVerified ? { model: modelVerified } : {}),
166
+ ...(modelVerified ? { model: modelVerified, modelVerified } : {}),
145
167
  ...(effortVerified ? { effortVerified } : {}),
146
168
  });
147
169
  }
@@ -36,6 +36,11 @@ async function requestHost(endpoint, tool, args, options = {}) {
36
36
  try {
37
37
  const request = { type: "tool_request", method: "execute_tool", params: { tool, args }, id: `playbook-${Date.now()}-${Math.random()}` };
38
38
  if (options.tabId) request.tabId = options.tabId;
39
+ if (options.session) {
40
+ request.session = options.session;
41
+ request.sessionSource = options.sessionSource || "environment";
42
+ }
43
+ if (options.admission) request.admission = options.admission;
39
44
  return unwrapResponse(await transport.request(request, options.timeoutMs || 11 * 60 * 1000));
40
45
  } finally {
41
46
  await transport.close();
@@ -48,7 +53,7 @@ function runSpec(argv) {
48
53
  const parsed = parseCommandArgs(argv.slice(offset));
49
54
  const [playbook, op] = parsed.positional;
50
55
  if (!playbook || !op) throw new Error(direct ? "Usage: surf use <playbook> <op> [--arg value]" : "Usage: surf pb run <playbook> <op> [--arg value]");
51
- const reserved = new Set(["json", "no-lock", "tab-id", "write", "repeat", "retry-attempt", "override-in-doubt", "pin-built-in", "allow-script"]);
56
+ const reserved = new Set(["json", "no-lock", "no-wait", "session", "tab-id", "write", "repeat", "retry-attempt", "override-in-doubt", "pin-built-in", "allow-script"]);
52
57
  const args = Object.fromEntries(Object.entries(parsed.options).filter(([name]) => !reserved.has(name)));
53
58
  return { playbook, op, args, options: parsed.options };
54
59
  }
@@ -73,7 +78,7 @@ function playbookCommandNeedsBrowser(argv) {
73
78
  return subcommand === "record" && ["start", "stop", "discard"].includes(argv[2]);
74
79
  }
75
80
 
76
- async function handlePlaybookCli(argv, { endpoint, cwd = process.cwd() }) {
81
+ async function handlePlaybookCli(argv, { endpoint, cwd = process.cwd(), session, sessionSource, admission } = {}) {
77
82
  if (!["playbook", "pb", "use"].includes(argv[0])) return { handled: false };
78
83
  if (argv[0] === "use" || argv[1] === "run") {
79
84
  const spec = runSpec(argv);
@@ -93,6 +98,9 @@ async function handlePlaybookCli(argv, { endpoint, cwd = process.cwd() }) {
93
98
  };
94
99
  const value = await requestHost(endpoint, "playbook.run", args, {
95
100
  tabId: spec.options["tab-id"],
101
+ session: spec.options.session || session,
102
+ sessionSource: spec.options.session ? "explicit" : sessionSource,
103
+ admission: spec.options["no-wait"] === true ? { wait: false } : admission,
96
104
  timeoutMs: resolveRequestDeadlineMs("playbook.run", args),
97
105
  });
98
106
  return { handled: true, value, json: spec.options.json === true };
@@ -118,7 +126,12 @@ async function handlePlaybookCli(argv, { endpoint, cwd = process.cwd() }) {
118
126
  else if (action === "mark") args = { label: parsed.positional.slice(1).join(" ") };
119
127
  else if (action === "stop") args = { draft: parsed.options.draft === true };
120
128
  else if (!["status", "pause", "resume", "discard"].includes(action)) throw new Error("Unknown record command");
121
- const value = await requestHost(endpoint, tool, args, { tabId: parsed.options["tab-id"] });
129
+ const value = await requestHost(endpoint, tool, args, {
130
+ tabId: parsed.options["tab-id"],
131
+ session: parsed.options.session || session,
132
+ sessionSource: parsed.options.session ? "explicit" : sessionSource,
133
+ admission: parsed.options["no-wait"] === true ? { wait: false } : admission,
134
+ });
122
135
  return { handled: true, value, json: parsed.options.json === true };
123
136
  }
124
137
  if (command === "suggest") return { handled: true, value: suggestions({ since: parsed.options.since || "1h" }), json: parsed.options.json === true };
@@ -0,0 +1,47 @@
1
+ class SurfError extends Error {
2
+ constructor(code, message, details = {}) {
3
+ super(message);
4
+ this.name = "SurfError";
5
+ this.code = code;
6
+ Object.assign(this, details);
7
+ }
8
+
9
+ toJSON() {
10
+ const value = { code: this.code, message: this.message };
11
+ for (const key of [
12
+ "session", "target", "lastUrl", "laneKey", "resourceKeys", "retryable", "recoveryCommand",
13
+ "queue", "reason", "browserEpoch", "expectedBrowserEpoch",
14
+ ]) {
15
+ if (this[key] !== undefined) value[key] = this[key];
16
+ }
17
+ return value;
18
+ }
19
+ }
20
+
21
+ function surfError(code, message, details = {}) {
22
+ return new SurfError(code, message, details);
23
+ }
24
+
25
+ function isSurfError(error) {
26
+ return Boolean(error && typeof error === "object" && typeof error.code === "string");
27
+ }
28
+
29
+ function fromExtensionError(result, fallbackCode = "browser_error") {
30
+ if (!result?.error) return null;
31
+ return surfError(result.errorCode || fallbackCode, result.error, result.errorDetails || {});
32
+ }
33
+
34
+ function recoveryFor(error) {
35
+ if (!error || typeof error !== "object") return null;
36
+ if (typeof error.recoveryCommand === "string" && error.recoveryCommand) return error.recoveryCommand;
37
+ if ((error.code === "tab_gone" || error.code === "session_epoch_stale") && error.session) {
38
+ return `surf session.reopen ${error.session}`;
39
+ }
40
+ if ((error.code === "tab_busy" || error.code === "browser_busy" || error.code === "resource_busy") && error.session) {
41
+ return `surf session.info ${error.session}`;
42
+ }
43
+ if (error.code === "browser_busy") return "surf session.list --refresh";
44
+ return null;
45
+ }
46
+
47
+ module.exports = { SurfError, surfError, isSurfError, fromExtensionError, recoveryFor };
@@ -0,0 +1,107 @@
1
+ const path = require("path");
2
+
3
+ const PROVIDER_TOOLS = new Set([
4
+ "chatgpt", "gemini", "perplexity", "grok", "kimi", "aistudio", "aistudio.build",
5
+ "oracle.ask", "oracle.result", "oracle.cancel",
6
+ ]);
7
+
8
+ const HOST_TOOLS = new Set([
9
+ "wait",
10
+ "session.list",
11
+ "session.info",
12
+ "tab.unname", "tabs_unregister", "tab.named", "tabs_list_named",
13
+ ]);
14
+
15
+ const BROWSER_READ_TOOLS = new Set([
16
+ "tab.list", "tabs_context", "list_tabs",
17
+ "window.list",
18
+ "history.list", "history.search",
19
+ "bookmark.list",
20
+ "downloads.search",
21
+ ]);
22
+
23
+ const BROWSER_WRITE_TOOLS = new Set([
24
+ "session.new", "session.ensure", "session.close", "session.rebind", "session.reopen",
25
+ "tab.new", "new_tab", "tabs_create",
26
+ "tab.move", "tab.switch", "switch_tab",
27
+ "tab.group", "tab.ungroup",
28
+ "window.new", "window.close", "window.focus", "window.resize",
29
+ "smoke",
30
+ ]);
31
+
32
+ const BROWSER_WRITE_TARGETED_TOOLS = new Set([
33
+ "cookie.set", "cookie.clear", "cookie.clear-all",
34
+ "bookmark.add", "bookmark.remove",
35
+ "playbook.run",
36
+ ]);
37
+
38
+ const TAB_TOOLS = new Set([
39
+ "ai", "computer", "batch", "record", "animate-audit", "perf-audit",
40
+ "navigate", "go", "back", "forward", "reload", "tab.reload",
41
+ "screenshot", "snap", "resize",
42
+ "page.read", "read_page", "page.text", "get_page_text", "page.html", "page.save", "page.state",
43
+ "click", "left_click", "right_click", "double_click", "triple_click", "drag", "hover", "key", "submit",
44
+ "type", "smart_type", "find_and_type", "form_input", "form.fill", "select", "upload", "upload_image",
45
+ "scroll", "scroll.top", "scroll.bottom", "scroll.to", "scroll.info", "scroll_to_position",
46
+ "search", "locate.role", "locate.text", "locate.label", "element.styles",
47
+ "js", "javascript_tool", "eval",
48
+ "wait.element", "wait.url", "wait.network", "wait.dom", "wait.load", "health",
49
+ "frame.list", "frame.switch", "frame.main", "frame.js",
50
+ "dialog.accept", "dialog.dismiss", "dialog.info",
51
+ "console", "network", "network.get", "network.body", "network.curl", "network.path",
52
+ "network.origins", "network.clear", "network.stats", "network.export",
53
+ "emulate.network", "emulate.cpu", "emulate.geo", "emulate.device", "emulate.viewport", "emulate.touch",
54
+ "perf.start", "perf.stop", "perf.metrics",
55
+ "zoom", "cookie.list", "cookie.get",
56
+ "tab.name", "tabs_register",
57
+ "playbook.record.start", "playbook.record.stop", "playbook.record.status", "playbook.record.mark",
58
+ "playbook.record.pause", "playbook.record.resume", "playbook.record.discard",
59
+ ]);
60
+
61
+ function hasExplicitTabCloseTarget(args = {}) {
62
+ return [args.id, args.tab_id, args.tabId, args.ids, args.tab_ids, args.tabIds]
63
+ .some((value) => value !== undefined && value !== null && value !== "");
64
+ }
65
+
66
+ function classifyTool(tool, args = {}) {
67
+ if (PROVIDER_TOOLS.has(tool) || tool.startsWith("oracle.")) {
68
+ const hostOnly = tool === "oracle.result" || tool === "oracle.cancel" || tool === "oracle.status" || tool === "oracle.list";
69
+ return hostOnly
70
+ ? { scope: "host", targetUse: "host" }
71
+ : { scope: "provider", targetUse: "default-tab" };
72
+ }
73
+ if (tool === "session.list") {
74
+ return { scope: args.refresh ? "browser-read" : "host", targetUse: "host" };
75
+ }
76
+ if (tool === "session.info") {
77
+ return { scope: args.refresh ? "browser-read" : "host", targetUse: "host" };
78
+ }
79
+ if (tool === "tab.close" || tool === "close_tab") {
80
+ return hasExplicitTabCloseTarget(args)
81
+ ? { scope: "browser-write", targetUse: "browser" }
82
+ : { scope: "browser-write", targetUse: "default-tab" };
83
+ }
84
+ if (HOST_TOOLS.has(tool)) return { scope: "host", targetUse: "host" };
85
+ if (BROWSER_READ_TOOLS.has(tool)) return { scope: "browser-read", targetUse: "browser" };
86
+ if (BROWSER_WRITE_TARGETED_TOOLS.has(tool)) {
87
+ return { scope: "browser-write", targetUse: "default-tab" };
88
+ }
89
+ if (BROWSER_WRITE_TOOLS.has(tool)) return { scope: "browser-write", targetUse: "browser" };
90
+ if (TAB_TOOLS.has(tool)) {
91
+ const resourceKeys = [];
92
+ if (tool === "network.export" && typeof args.output === "string") resourceKeys.push(`file:${path.resolve(args.output)}`);
93
+ if (tool.startsWith("playbook.record.")) resourceKeys.push("playbook-recorder");
94
+ return { scope: "tab", targetUse: "default-tab", resourceKeys };
95
+ }
96
+ return { scope: "browser-write", targetUse: "browser", conservative: true };
97
+ }
98
+
99
+ module.exports = {
100
+ BROWSER_READ_TOOLS,
101
+ BROWSER_WRITE_TARGETED_TOOLS,
102
+ BROWSER_WRITE_TOOLS,
103
+ HOST_TOOLS,
104
+ PROVIDER_TOOLS,
105
+ TAB_TOOLS,
106
+ classifyTool,
107
+ };
@@ -36,6 +36,13 @@ const COMMANDS = {
36
36
  "tab.close": { primaryArg: "id", effect: "page-write", argKinds: { id: "tab-id" } },
37
37
  "tab.name": { primaryArg: "name", effect: "page-write", argKinds: { name: "name" } },
38
38
  "tab.unname": { primaryArg: "name", effect: "page-write", argKinds: { name: "name" } },
39
+ "session.new": { primaryArg: "name", effect: "navigation", recordable: false, argKinds: { name: "name", url: "url" } },
40
+ "session.ensure": { primaryArg: "name", effect: "navigation", recordable: false, argKinds: { name: "name", url: "url" } },
41
+ "session.list": { effect: "read", recordable: false },
42
+ "session.info": { primaryArg: "name", effect: "read", recordable: false, argKinds: { name: "name" } },
43
+ "session.close": { primaryArg: "name", effect: "page-write", recordable: false, argKinds: { name: "name" } },
44
+ "session.rebind": { primaryArg: "name", effect: "page-write", recordable: false, argKinds: { name: "name", tabId: "tab-id" } },
45
+ "session.reopen": { primaryArg: "name", effect: "navigation", recordable: false, argKinds: { name: "name", url: "url" } },
39
46
  scroll_to_position: { primaryArg: "position", effect: "page-write", argKinds: { position: "position" } },
40
47
  type: { primaryArg: "text", effect: "page-write", argKinds: { selector: "selector", text: "user-input" }, sensitiveArgs: ["text"] },
41
48
  smart_type: { primaryArg: "text", effect: "page-write", argKinds: { selector: "selector", text: "user-input" }, sensitiveArgs: ["text"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surf-cli",
3
- "version": "2.13.1",
3
+ "version": "2.15.0",
4
4
  "description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
5
5
  "keywords": [
6
6
  "chrome",
@@ -33,6 +33,7 @@
33
33
  "playbooks/",
34
34
  "scripts/",
35
35
  "dist/",
36
+ "agents/",
36
37
  "skills/",
37
38
  "README.md",
38
39
  "LICENSE"
@@ -81,7 +82,12 @@
81
82
  ],
82
83
  "skills": [
83
84
  "./skills"
84
- ]
85
+ ],
86
+ "subagents": {
87
+ "agents": [
88
+ "./agents"
89
+ ]
90
+ }
85
91
  },
86
92
  "peerDependencies": {
87
93
  "typebox": "*"