surf-cli 2.12.0 → 2.13.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 +6 -0
- package/native/cli.cjs +33 -1
- package/native/host-helpers.cjs +33 -2
- package/native/host-sessions.cjs +1 -0
- package/native/host.cjs +109 -6
- package/native/kimi-client.cjs +912 -0
- package/native/mcp-server.cjs +10 -0
- package/native/workflow-definition.cjs +1 -0
- package/package.json +1 -1
- package/pi-extension/surf.ts +71 -9
- package/skills/README.md +6 -0
package/native/mcp-server.cjs
CHANGED
|
@@ -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: {
|
|
@@ -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
package/pi-extension/surf.ts
CHANGED
|
@@ -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
|
|
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
|
|
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: [
|
|
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
|
-
|
|
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
|
-
//
|
|
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