surf-cli 2.12.0 → 2.13.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.
@@ -290,6 +290,16 @@ const TOOL_SCHEMAS = {
290
290
  timeout: z.number().optional().describe("Timeout in seconds")
291
291
  }
292
292
  },
293
+ kimi: {
294
+ desc: "Ask Kimi AI (kimi.com, Moonshot K-series) through the browser session",
295
+ schema: {
296
+ query: z.string().optional().describe("Question or prompt"),
297
+ model: z.string().optional().describe("Model: instant (default), thinking, high, or any label in kimi.com's picker"),
298
+ "with-page": z.boolean().optional().describe("Include current page context"),
299
+ timeout: z.number().optional().describe("Timeout in seconds"),
300
+ validate: z.boolean().optional().describe("Check kimi.com UI and list available models")
301
+ }
302
+ },
293
303
  "network.export": {
294
304
  desc: "Export captured network requests",
295
305
  schema: {
@@ -132,7 +132,7 @@ function readPrivateFile(filePath, options = {}) {
132
132
  const stat = assertNotSymlink(resolved, options.allowMissing === true);
133
133
  if (!stat) return options.fallback;
134
134
  if (!stat.isFile()) throw new Error(`private state path is not a file: ${resolved}`);
135
- if ((stat.mode & 0o077) !== 0) throw new Error(`private state file permissions are too broad: ${resolved}`);
135
+ if (process.platform !== "win32" && (stat.mode & 0o077) !== 0) throw new Error(`private state file permissions are too broad: ${resolved}`);
136
136
  return fs.readFileSync(resolved, options.encoding || null);
137
137
  }
138
138
 
@@ -7,6 +7,7 @@ const COMMANDS = {
7
7
  ai: { primaryArg: "query", effect: "read", argKinds: { query: "user-input" }, sensitiveArgs: ["query"] },
8
8
  gemini: { primaryArg: "query", effect: "page-write", argKinds: { query: "user-input" }, sensitiveArgs: ["query"] },
9
9
  chatgpt: { primaryArg: "query", effect: "page-write", argKinds: { query: "user-input" }, sensitiveArgs: ["query"] },
10
+ kimi: { primaryArg: "query", effect: "page-write", argKinds: { query: "user-input" }, sensitiveArgs: ["query"] },
10
11
  "oracle.ask": { primaryArg: "prompt", effect: "page-write", argKinds: { prompt: "user-input" }, sensitiveArgs: ["prompt"] },
11
12
  perplexity: { primaryArg: "query", effect: "page-write", argKinds: { query: "user-input" }, sensitiveArgs: ["query"] },
12
13
  grok: { primaryArg: "query", effect: "page-write", argKinds: { query: "user-input" }, sensitiveArgs: ["query"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surf-cli",
3
- "version": "2.12.0",
3
+ "version": "2.13.1",
4
4
  "description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
5
5
  "keywords": [
6
6
  "chrome",
@@ -69,7 +69,7 @@
69
69
  "@types/node": "^26.1.2",
70
70
  "@vitest/coverage-v8": "^4.1.9",
71
71
  "@vitest/ui": "^4.1.9",
72
- "puppeteer": "25.4.0",
72
+ "puppeteer": "25.5.0",
73
73
  "typescript": "^7.0.2",
74
74
  "vite": "^8.1.4",
75
75
  "vitest": "^4.1.9",
@@ -21,12 +21,16 @@ const { prepareRemoteTool, validateLocalToolPaths } = require("../native/file-tr
21
21
 
22
22
  const MAX_OUTPUT_CHARS = 20_000;
23
23
  const ORACLE_ACTIVE_STATES = new Set(["created", "dispatched", "awaiting"]);
24
+ const ORACLE_TERMINAL_STATES = new Set(["captured", "failed"]);
25
+ const ORACLE_FINISHED_CHANNEL = "surf-oracle:finished";
24
26
  const BACKGROUND_WORK_PROTOCOL_VERSION = 1;
25
27
  const BACKGROUND_WORK_REGISTRY_KEY = "pi-subagents.background-work.v1";
28
+ const BACKGROUND_WORK_MODULE_SPECIFIER = "pi-subagents/background-work";
26
29
 
27
30
  type Pi = {
28
31
  registerTool(tool: Record<string, unknown>): void;
29
32
  on(event: "session_start" | "session_shutdown", handler: (event: unknown, ctx: unknown) => void | Promise<void>): void;
33
+ events?: { emit(event: string, data: unknown): void };
30
34
  };
31
35
 
32
36
  type SurfEndpoint = { kind?: string };
@@ -39,6 +43,12 @@ type BackgroundWorkProvider = {
39
43
  listActiveWork(): Array<{ id: string; sessionId: string }>;
40
44
  };
41
45
 
46
+ type RegisterBackgroundWorkProvider = (provider: BackgroundWorkProvider) => () => void;
47
+
48
+ type BackgroundWorkModule = {
49
+ registerBackgroundWorkProvider?: unknown;
50
+ };
51
+
42
52
  type BackgroundWorkRegistry = {
43
53
  version: typeof BACKGROUND_WORK_PROTOCOL_VERSION;
44
54
  providers: Map<string, BackgroundWorkProvider>;
@@ -52,21 +62,21 @@ function textResult(value: unknown, isError = false): ToolResult {
52
62
  return { content: [{ type: "text", text: bounded }], ...(isError ? { isError: true } : {}) };
53
63
  }
54
64
 
55
- function resultFromHost(response: Record<string, unknown>): ToolResult {
65
+ export function resultFromHost(response: Record<string, unknown>): ToolResult {
56
66
  const error = response.error as { content?: Array<{ text?: string }> } | undefined;
57
67
  if (error) return textResult(error.content?.map((item) => item.text ?? "").join("\n") || "Surf request failed", true);
58
68
  const result = response.result as { content?: ToolResult["content"] } | undefined;
59
69
  if (!result?.content) return textResult(result ?? "OK");
60
- const content = result.content.map((item) => item.type === "text" && item.text && item.text.length > MAX_OUTPUT_CHARS
61
- ? { ...item, text: `${item.text.slice(0, MAX_OUTPUT_CHARS)}\n\n[Surf output truncated at ${MAX_OUTPUT_CHARS} characters]` }
62
- : item);
63
- const text = content.find((item) => item.type === "text")?.text;
70
+ const text = result.content.find((item) => item.type === "text")?.text;
64
71
  let details: unknown;
65
72
  try {
66
73
  details = text ? JSON.parse(text) : undefined;
67
74
  } catch {
68
75
  details = undefined;
69
76
  }
77
+ const content = result.content.map((item) => item.type === "text" && item.text && item.text.length > MAX_OUTPUT_CHARS
78
+ ? { ...item, text: `${item.text.slice(0, MAX_OUTPUT_CHARS)}\n\n[Surf output truncated at ${MAX_OUTPUT_CHARS} characters]` }
79
+ : item);
70
80
  return { content, details };
71
81
  }
72
82
 
@@ -128,10 +138,24 @@ export function registerGlobalBackgroundProvider(provider: BackgroundWorkProvide
128
138
  };
129
139
  }
130
140
 
131
- export function registerOptionalBackgroundProvider(sessionId: string, jobIds: Set<string>, listJobs: () => Array<{ id: string; state: string }>, register: (provider: BackgroundWorkProvider) => () => void) {
141
+ export async function resolveBackgroundWorkRegister(
142
+ loadModule: () => Promise<BackgroundWorkModule> = () => import(BACKGROUND_WORK_MODULE_SPECIFIER) as Promise<BackgroundWorkModule>,
143
+ ): Promise<RegisterBackgroundWorkProvider> {
144
+ try {
145
+ const module = await loadModule();
146
+ if (typeof module.registerBackgroundWorkProvider === "function") {
147
+ return module.registerBackgroundWorkProvider as RegisterBackgroundWorkProvider;
148
+ }
149
+ } catch {
150
+ // The Pi bridge is optional. Surf also runs in other coding-agent harnesses and as a direct CLI.
151
+ }
152
+ return registerGlobalBackgroundProvider;
153
+ }
154
+
155
+ export function registerOptionalBackgroundProvider(sessionId: string, jobIds: Set<string>, listJobs: () => Array<{ id: string; state: string }>, register: RegisterBackgroundWorkProvider) {
132
156
  return register({
133
157
  name: "surf-oracle",
134
- wakeChannels: ["surf-oracle:finished"],
158
+ wakeChannels: [ORACLE_FINISHED_CHANNEL],
135
159
  listActiveWork: () => listJobs()
136
160
  .filter((job) => jobIds.has(job.id) && ORACLE_ACTIVE_STATES.has(job.state))
137
161
  .map((job) => ({ id: job.id, sessionId })),
@@ -144,6 +168,15 @@ export function rememberOracleJobForSession(jobIds: Set<string>, jobId: unknown,
144
168
  return true;
145
169
  }
146
170
 
171
+ export function emitOracleFinished(pi: Pi, job: unknown): boolean {
172
+ if (!job || typeof job !== "object" || Array.isArray(job)) return false;
173
+ const { id, state } = job as { id?: unknown; state?: unknown };
174
+ if (typeof id !== "string" || typeof state !== "string" || !ORACLE_TERMINAL_STATES.has(state)) return false;
175
+ if (!pi.events) return false;
176
+ pi.events.emit(ORACLE_FINISHED_CHANNEL, { id, state });
177
+ return true;
178
+ }
179
+
147
180
  function registerTool(pi: Pi, name: string, description: string, parameters: unknown, map: (args: Record<string, unknown>) => [string, Record<string, unknown>, number | undefined]) {
148
181
  pi.registerTool({
149
182
  name,
@@ -178,7 +211,21 @@ export default function surfExtension(pi: Pi) {
178
211
  tool: Type.String(), args: Type.Optional(Type.Record(Type.String(), Type.Unknown())), tabId: Type.Optional(Type.Number()),
179
212
  }), (args) => [args.tool as string, (args.args as Record<string, unknown>) ?? {}, args.tabId as number | undefined]);
180
213
  registerTool(pi, "surf_oracle_status", "Get the status of a Surf oracle job, or the newest job.", Type.Object({ id: Type.Optional(Type.String()) }), (args) => ["oracle.status", args, undefined]);
181
- registerTool(pi, "surf_oracle_result", "Capture the result of a Surf oracle job.", Type.Object({ id: Type.String(), timeout: Type.Optional(Type.Number()) }), (args) => ["oracle.result", args, undefined]);
214
+ pi.registerTool({
215
+ name: "surf_oracle_result",
216
+ label: "surf_oracle_result",
217
+ description: "Capture the result of a Surf oracle job.",
218
+ parameters: Type.Object({ id: Type.String(), timeout: Type.Optional(Type.Number()) }),
219
+ async execute(_id: string, args: Record<string, unknown>) {
220
+ try {
221
+ const result = await requestSurf("oracle.result", args);
222
+ emitOracleFinished(pi, result.details ?? { id: args.id, state: result.isError ? "failed" : undefined });
223
+ return result;
224
+ } catch (error) {
225
+ return textResult(error instanceof Error ? error.message : String(error), true);
226
+ }
227
+ },
228
+ });
182
229
 
183
230
  const oracleJobIds = new Set<string>();
184
231
  let sessionGeneration = 0;
@@ -204,6 +251,7 @@ export default function surfExtension(pi: Pi) {
204
251
  let dispose: (() => void) | undefined;
205
252
  pi.on("session_start", (_event, ctx) => {
206
253
  sessionGeneration++;
254
+ const generation = sessionGeneration;
207
255
  sessionActive = false;
208
256
  dispose?.();
209
257
  dispose = undefined;
@@ -216,8 +264,22 @@ export default function surfExtension(pi: Pi) {
216
264
  const jobs = require("../native/oracle-jobs.cjs") as { listJobs(): Array<{ id: string; state: string }> };
217
265
  dispose = registerOptionalBackgroundProvider(sessionId, oracleJobIds, jobs.listJobs, registerGlobalBackgroundProvider);
218
266
  sessionActive = true;
267
+ void resolveBackgroundWorkRegister().then((register) => {
268
+ try {
269
+ if (register === registerGlobalBackgroundProvider || generation !== sessionGeneration) return;
270
+ const nextDispose = registerOptionalBackgroundProvider(sessionId, oracleJobIds, jobs.listJobs, register);
271
+ if (generation !== sessionGeneration) {
272
+ nextDispose();
273
+ return;
274
+ }
275
+ dispose?.();
276
+ dispose = nextDispose;
277
+ } catch {
278
+ // Keep the already-registered fallback provider.
279
+ }
280
+ });
219
281
  } catch {
220
- // pi-subagents is optional. Browser tools work without it.
282
+ // The Pi bridge is optional. Browser tools work without pi-subagents.
221
283
  }
222
284
  });
223
285
  pi.on("session_shutdown", () => {
package/skills/README.md CHANGED
@@ -1,3 +1,9 @@
1
+ ---
2
+ name: surf-skills
3
+ description: Package index for Surf skills. Use when you need install or layout notes for the Surf skill bundle.
4
+ disable-model-invocation: true
5
+ ---
6
+
1
7
  # Surf Skills
2
8
 
3
9
  This directory contains skill files for AI coding agents: