@tekmidian/pai 0.40.0 → 0.41.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 +1 -0
- package/dist/browser-mcp/index.d.mts +1 -0
- package/dist/browser-mcp/index.mjs +352 -0
- package/dist/browser-mcp/index.mjs.map +1 -0
- package/dist/{chain-DhVHVnmT.mjs → chain-zMjCDW-R.mjs} +40 -8
- package/dist/chain-zMjCDW-R.mjs.map +1 -0
- package/dist/cli/index.mjs +3 -3
- package/dist/cli/program.mjs +3 -3
- package/dist/daemon-mcp/index.mjs +12 -2
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{fallback-CdXv-Np0.mjs → fallback-CgQ_x4I7.mjs} +2 -2
- package/dist/{fallback-CdXv-Np0.mjs.map → fallback-CgQ_x4I7.mjs.map} +1 -1
- package/dist/hooks/worker-status-line.mjs.map +2 -2
- package/dist/{planner-Cm3g6fWH.mjs → planner-DsmMRIZR.mjs} +2 -2
- package/dist/{planner-Cm3g6fWH.mjs.map → planner-DsmMRIZR.mjs.map} +1 -1
- package/dist/{program-AR0qlYRg.mjs → program-Bed-Pd-8.mjs} +67 -55
- package/dist/program-Bed-Pd-8.mjs.map +1 -0
- package/dist/skills/Worker/SKILL.md +10 -0
- package/docs/commands/README.md +2 -2
- package/docs/commands/mcp.md +4 -4
- package/docs/commands/worker.md +3 -0
- package/docs/provider-independence.md +8 -21
- package/package.json +1 -1
- package/statusline-command.sh +65 -73
- package/dist/chain-DhVHVnmT.mjs.map +0 -1
- package/dist/program-AR0qlYRg.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -112,6 +112,7 @@ pai worker replay <id> # transcript of a finished or run
|
|
|
112
112
|
pai worker say <id> <text> # message a running worker mid-run
|
|
113
113
|
pai worker handoff '<json>' # from inside a worker: report to the parent
|
|
114
114
|
pai worker merge <id> # merge the worker's branch back, drop the worktree
|
|
115
|
+
pai worker wait <id>... # block until workers finish (never sleep-loop)
|
|
115
116
|
pai worker watch # ps refreshed every 2 seconds
|
|
116
117
|
```
|
|
117
118
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { i as number, o as string, r as boolean } from "../schemas-De1F-ktH.mjs";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
//#region src/browser-mcp/bridge-client.ts
|
|
7
|
+
/**
|
|
8
|
+
* WebSocket client for the browser bridge.
|
|
9
|
+
*
|
|
10
|
+
* Talks to the native host's hand-rolled WS server (ws://127.0.0.1:8756) with
|
|
11
|
+
* the global WebSocket — node >= 22, no external dependency. Requests carry an
|
|
12
|
+
* id; replies are correlated by it. The socket is connected lazily per call
|
|
13
|
+
* and reconnected on demand, so the server survives Chrome being closed.
|
|
14
|
+
*/
|
|
15
|
+
const BRIDGE_PORT = Number(process.env.PAI_BROWSER_BRIDGE_PORT || 8756);
|
|
16
|
+
const BRIDGE_URL = `ws://127.0.0.1:${BRIDGE_PORT}`;
|
|
17
|
+
const NOT_CONNECTED_MESSAGE = "Chrome bridge not connected — start Chrome (the PAI browser-bridge extension must be running) and try again.";
|
|
18
|
+
const globalWebSocketFactory = (url) => {
|
|
19
|
+
const WS = globalThis.WebSocket;
|
|
20
|
+
if (!WS) throw new Error("this node runtime has no global WebSocket — pai-browser-mcp needs node >= 22");
|
|
21
|
+
return new WS(url);
|
|
22
|
+
};
|
|
23
|
+
var NotConnectedError = class extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super(NOT_CONNECTED_MESSAGE);
|
|
26
|
+
this.name = "NotConnectedError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const CONNECT_TIMEOUT_MS = 5e3;
|
|
30
|
+
const REQUEST_TIMEOUT_MS = 5e3;
|
|
31
|
+
const OPEN = 1;
|
|
32
|
+
var BridgeClient = class {
|
|
33
|
+
socket = null;
|
|
34
|
+
connecting = null;
|
|
35
|
+
nextId = 1;
|
|
36
|
+
pending = /* @__PURE__ */ new Map();
|
|
37
|
+
constructor(url = BRIDGE_URL, socketFactory = globalWebSocketFactory) {
|
|
38
|
+
this.url = url;
|
|
39
|
+
this.socketFactory = socketFactory;
|
|
40
|
+
}
|
|
41
|
+
/** True when the underlying socket is open. */
|
|
42
|
+
get connected() {
|
|
43
|
+
return this.socket !== null && this.socket.readyState === OPEN;
|
|
44
|
+
}
|
|
45
|
+
ensureSocket() {
|
|
46
|
+
if (this.socket && this.socket.readyState === OPEN) return Promise.resolve(this.socket);
|
|
47
|
+
if (this.connecting) return this.connecting;
|
|
48
|
+
this.dropSocket();
|
|
49
|
+
const attempt = new Promise((resolve, reject) => {
|
|
50
|
+
let socket;
|
|
51
|
+
try {
|
|
52
|
+
socket = this.socketFactory(this.url);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
reject(e);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
reject(new NotConnectedError());
|
|
59
|
+
socket.onopen = null;
|
|
60
|
+
socket.onclose = null;
|
|
61
|
+
socket.onerror = null;
|
|
62
|
+
socket.onmessage = null;
|
|
63
|
+
try {
|
|
64
|
+
socket.close();
|
|
65
|
+
} catch {}
|
|
66
|
+
}, CONNECT_TIMEOUT_MS);
|
|
67
|
+
socket.onopen = () => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
this.socket = socket;
|
|
70
|
+
resolve(socket);
|
|
71
|
+
};
|
|
72
|
+
socket.onclose = () => {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
this.dropSocket();
|
|
75
|
+
reject(new NotConnectedError());
|
|
76
|
+
};
|
|
77
|
+
socket.onerror = () => {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
this.dropSocket();
|
|
80
|
+
reject(new NotConnectedError());
|
|
81
|
+
};
|
|
82
|
+
socket.onmessage = (ev) => this.onMessage(ev);
|
|
83
|
+
});
|
|
84
|
+
this.connecting = attempt;
|
|
85
|
+
const clear = () => {
|
|
86
|
+
if (this.connecting === attempt) this.connecting = null;
|
|
87
|
+
};
|
|
88
|
+
attempt.then(clear, clear);
|
|
89
|
+
return attempt;
|
|
90
|
+
}
|
|
91
|
+
dropSocket() {
|
|
92
|
+
if (this.socket) {
|
|
93
|
+
this.socket.onopen = null;
|
|
94
|
+
this.socket.onclose = null;
|
|
95
|
+
this.socket.onerror = null;
|
|
96
|
+
this.socket.onmessage = null;
|
|
97
|
+
try {
|
|
98
|
+
this.socket.close();
|
|
99
|
+
} catch {}
|
|
100
|
+
this.socket = null;
|
|
101
|
+
}
|
|
102
|
+
for (const [, p] of this.pending) {
|
|
103
|
+
clearTimeout(p.timer);
|
|
104
|
+
p.reject(new NotConnectedError());
|
|
105
|
+
}
|
|
106
|
+
this.pending.clear();
|
|
107
|
+
}
|
|
108
|
+
onMessage(ev) {
|
|
109
|
+
let reply;
|
|
110
|
+
try {
|
|
111
|
+
reply = JSON.parse(ev.data);
|
|
112
|
+
} catch {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const p = reply?.id !== void 0 ? this.pending.get(reply.id) : void 0;
|
|
116
|
+
if (!p) return;
|
|
117
|
+
this.pending.delete(reply.id);
|
|
118
|
+
clearTimeout(p.timer);
|
|
119
|
+
p.resolve(reply);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Sends one command and awaits its correlated reply. Connection problems
|
|
123
|
+
* and timeouts both surface as NotConnectedError with the user-facing text.
|
|
124
|
+
*/
|
|
125
|
+
async send(command, params = {}) {
|
|
126
|
+
await this.ensureSocket();
|
|
127
|
+
const id = this.nextId++;
|
|
128
|
+
const frame = JSON.stringify({
|
|
129
|
+
id,
|
|
130
|
+
command,
|
|
131
|
+
...params
|
|
132
|
+
});
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const timer = setTimeout(() => {
|
|
135
|
+
this.pending.delete(id);
|
|
136
|
+
reject(new NotConnectedError());
|
|
137
|
+
}, REQUEST_TIMEOUT_MS);
|
|
138
|
+
this.pending.set(id, {
|
|
139
|
+
resolve: (reply) => reply.ok ? resolve(reply.result) : reject(new Error(reply.error ?? "bridge error")),
|
|
140
|
+
reject,
|
|
141
|
+
timer
|
|
142
|
+
});
|
|
143
|
+
try {
|
|
144
|
+
this.socket.send(frame);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
this.pending.delete(id);
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
reject(new NotConnectedError());
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
close() {
|
|
153
|
+
this.dropSocket();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/browser-mcp/tools.ts
|
|
159
|
+
const tabSchema = number().int().describe("Tab id, as returned by tabs_list.");
|
|
160
|
+
function asRecord(v) {
|
|
161
|
+
return v ?? {};
|
|
162
|
+
}
|
|
163
|
+
function formatTabs(result) {
|
|
164
|
+
const tabs = Array.isArray(result) ? result : asRecord(result).tabs;
|
|
165
|
+
if (!Array.isArray(tabs) || tabs.length === 0) return "(no open tabs)";
|
|
166
|
+
return tabs.map((t) => `${t.id}${t.active ? " *" : " "} ${String(t.title ?? "")}\n ${String(t.url ?? "")}`).join("\n");
|
|
167
|
+
}
|
|
168
|
+
function formatSnapshot(result) {
|
|
169
|
+
return String(asRecord(result).yaml ?? "");
|
|
170
|
+
}
|
|
171
|
+
function formatLogs(result) {
|
|
172
|
+
const entries = asRecord(result).entries;
|
|
173
|
+
if (!Array.isArray(entries) || entries.length === 0) return "(no console output captured)";
|
|
174
|
+
return entries.map((e) => {
|
|
175
|
+
return `[${e.source === "console" ? String(e.type ?? "log") : String(e.level ?? "log")}] ${String(e.text ?? "")}`;
|
|
176
|
+
}).join("\n");
|
|
177
|
+
}
|
|
178
|
+
function formatEval(result) {
|
|
179
|
+
const value = asRecord(result).value;
|
|
180
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
181
|
+
}
|
|
182
|
+
function formatScreenshot(result) {
|
|
183
|
+
return String(asRecord(result).base64 ?? "");
|
|
184
|
+
}
|
|
185
|
+
const BROWSER_TOOLS = [
|
|
186
|
+
{
|
|
187
|
+
name: "tabs_list",
|
|
188
|
+
description: "List the tabs of the user's real running Chrome (id, title, url, active).",
|
|
189
|
+
shape: {},
|
|
190
|
+
command: "list_tabs",
|
|
191
|
+
toParams: () => ({}),
|
|
192
|
+
format: formatTabs
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "tab_open",
|
|
196
|
+
description: "Open a new tab in the user's Chrome and return its tab id.",
|
|
197
|
+
shape: {
|
|
198
|
+
url: string().describe("URL to open."),
|
|
199
|
+
active: boolean().optional().describe("Focus the new tab (default true).")
|
|
200
|
+
},
|
|
201
|
+
command: "open_tab",
|
|
202
|
+
toParams: (a) => ({
|
|
203
|
+
url: a.url,
|
|
204
|
+
...a.active !== void 0 ? { active: a.active } : {}
|
|
205
|
+
})
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "tab_select",
|
|
209
|
+
description: "Bring a tab to the front (activate it and focus its window).",
|
|
210
|
+
shape: { tab: tabSchema },
|
|
211
|
+
command: "select_tab",
|
|
212
|
+
toParams: (a) => ({ tabId: a.tab })
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "tab_close",
|
|
216
|
+
description: "Close a tab.",
|
|
217
|
+
shape: { tab: tabSchema },
|
|
218
|
+
command: "close_tab",
|
|
219
|
+
toParams: (a) => ({ tabId: a.tab })
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "dom_snapshot",
|
|
223
|
+
description: "Snapshot a tab's DOM as an accessibility-tree YAML with [ref=sN] ids for interactive elements. Take a snapshot before dom_click/dom_type; refs stay valid until the next snapshot or navigation.",
|
|
224
|
+
shape: { tab: tabSchema },
|
|
225
|
+
command: "snapshot",
|
|
226
|
+
toParams: (a) => ({ tabId: a.tab }),
|
|
227
|
+
format: formatSnapshot
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: "dom_click",
|
|
231
|
+
description: "Click an element by its [ref=sN] from the latest dom_snapshot (trusted CDP event).",
|
|
232
|
+
shape: {
|
|
233
|
+
tab: tabSchema,
|
|
234
|
+
ref: string().describe("Ref id from dom_snapshot, e.g. s3.")
|
|
235
|
+
},
|
|
236
|
+
command: "click",
|
|
237
|
+
toParams: (a) => ({
|
|
238
|
+
tabId: a.tab,
|
|
239
|
+
ref: a.ref
|
|
240
|
+
})
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: "dom_type",
|
|
244
|
+
description: "Focus an element by its [ref=sN] and type text into it (inserts at the caret, works in SPAs).",
|
|
245
|
+
shape: {
|
|
246
|
+
tab: tabSchema,
|
|
247
|
+
ref: string().describe("Ref id from dom_snapshot, e.g. s5."),
|
|
248
|
+
text: string().describe("Text to type.")
|
|
249
|
+
},
|
|
250
|
+
command: "type",
|
|
251
|
+
toParams: (a) => ({
|
|
252
|
+
tabId: a.tab,
|
|
253
|
+
ref: a.ref,
|
|
254
|
+
text: a.text
|
|
255
|
+
})
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: "page_text",
|
|
259
|
+
description: "Read a tab's visible text (document.body.innerText).",
|
|
260
|
+
shape: { tab: tabSchema },
|
|
261
|
+
command: "eval",
|
|
262
|
+
toParams: () => ({ code: "document.body.innerText" }),
|
|
263
|
+
format: formatEval
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: "eval_js",
|
|
267
|
+
description: "Evaluate JavaScript in a tab and return the JSON-safe result. Awaits promises.",
|
|
268
|
+
shape: {
|
|
269
|
+
tab: tabSchema,
|
|
270
|
+
code: string().describe("JavaScript expression to evaluate.")
|
|
271
|
+
},
|
|
272
|
+
command: "eval",
|
|
273
|
+
toParams: (a) => ({ code: a.code }),
|
|
274
|
+
format: formatEval
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: "tab_screenshot",
|
|
278
|
+
description: "Screenshot a tab (PNG, base64).",
|
|
279
|
+
shape: { tab: tabSchema },
|
|
280
|
+
command: "screenshot",
|
|
281
|
+
toParams: (a) => ({ tabId: a.tab }),
|
|
282
|
+
format: formatScreenshot
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: "console_logs",
|
|
286
|
+
description: "Read the console output captured for a tab since its last snapshot.",
|
|
287
|
+
shape: { tab: tabSchema },
|
|
288
|
+
command: "console_logs",
|
|
289
|
+
toParams: (a) => ({ tabId: a.tab }),
|
|
290
|
+
format: formatLogs
|
|
291
|
+
}
|
|
292
|
+
];
|
|
293
|
+
/** Runs one tool against the bridge; returns MCP content + isError. */
|
|
294
|
+
async function runBrowserTool(def, args, bridge) {
|
|
295
|
+
try {
|
|
296
|
+
const result = await bridge.send(def.command, def.toParams(args));
|
|
297
|
+
return { content: [{
|
|
298
|
+
type: "text",
|
|
299
|
+
text: def.format ? def.format(result) : JSON.stringify(result, null, 2)
|
|
300
|
+
}] };
|
|
301
|
+
} catch (e) {
|
|
302
|
+
return {
|
|
303
|
+
content: [{
|
|
304
|
+
type: "text",
|
|
305
|
+
text: e instanceof NotConnectedError ? e.message : e instanceof Error ? e.message : String(e)
|
|
306
|
+
}],
|
|
307
|
+
isError: true
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** Registers every browser tool on an MCP server. */
|
|
312
|
+
function registerBrowserTools(server, bridge) {
|
|
313
|
+
for (const def of BROWSER_TOOLS) server.tool(def.name, def.description, def.shape, (args) => runBrowserTool(def, args, bridge));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/browser-mcp/index.ts
|
|
318
|
+
/**
|
|
319
|
+
* pai-browser-mcp — stdio MCP server for the PAI browser bridge.
|
|
320
|
+
*
|
|
321
|
+
* The provider-independent claude-in-chrome replacement: drives the user's
|
|
322
|
+
* real running Chrome (tabs + DOM) through the browser-bridge extension's
|
|
323
|
+
* native messaging host, over ws://127.0.0.1:8756. Any MCP client works —
|
|
324
|
+
* nothing here is Anthropic-specific.
|
|
325
|
+
*
|
|
326
|
+
* provider → MCP (this server) → WebSocket → native host → extension → Chrome
|
|
327
|
+
*
|
|
328
|
+
* If Chrome is not running (or the extension is not loaded), every tool
|
|
329
|
+
* returns a clear "start Chrome" error; the server itself stays alive and
|
|
330
|
+
* reconnects lazily per call.
|
|
331
|
+
*/
|
|
332
|
+
async function main() {
|
|
333
|
+
if (globalThis.WebSocket === void 0) {
|
|
334
|
+
process.stderr.write("pai-browser-mcp: this node runtime has no global WebSocket — run with node >= 22\n");
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
const server = new McpServer({
|
|
338
|
+
name: "pai-browser",
|
|
339
|
+
version: "0.1.0"
|
|
340
|
+
});
|
|
341
|
+
registerBrowserTools(server, new BridgeClient(void 0, globalWebSocketFactory));
|
|
342
|
+
const transport = new StdioServerTransport();
|
|
343
|
+
await server.connect(transport);
|
|
344
|
+
}
|
|
345
|
+
main().catch((e) => {
|
|
346
|
+
process.stderr.write(`pai-browser-mcp fatal error: ${String(e)}\n`);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
//#endregion
|
|
351
|
+
export { };
|
|
352
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["z.number","z.string","z.boolean"],"sources":["../../src/browser-mcp/bridge-client.ts","../../src/browser-mcp/tools.ts","../../src/browser-mcp/index.ts"],"sourcesContent":["/**\n * WebSocket client for the browser bridge.\n *\n * Talks to the native host's hand-rolled WS server (ws://127.0.0.1:8756) with\n * the global WebSocket — node >= 22, no external dependency. Requests carry an\n * id; replies are correlated by it. The socket is connected lazily per call\n * and reconnected on demand, so the server survives Chrome being closed.\n */\n\nexport const BRIDGE_PORT = Number(process.env.PAI_BROWSER_BRIDGE_PORT || 8756);\nexport const BRIDGE_URL = `ws://127.0.0.1:${BRIDGE_PORT}`;\n\nexport const NOT_CONNECTED_MESSAGE =\n \"Chrome bridge not connected — start Chrome (the PAI browser-bridge extension must be running) and try again.\";\n\n/** Minimal shape of the pieces of WebSocket this client uses. */\nexport interface WebSocketLike {\n readyState: number;\n send(data: string): void;\n close(): void;\n onopen: (() => void) | null;\n onclose: (() => void) | null;\n onerror: (() => void) | null;\n onmessage: ((ev: { data: string }) => void) | null;\n}\n\nexport type WebSocketFactory = (url: string) => WebSocketLike;\n\nexport const globalWebSocketFactory: WebSocketFactory = (url) => {\n const WS = (globalThis as { WebSocket?: new (url: string) => WebSocketLike }).WebSocket;\n if (!WS) {\n throw new Error(\n \"this node runtime has no global WebSocket — pai-browser-mcp needs node >= 22\"\n );\n }\n return new WS(url);\n};\n\nexport class NotConnectedError extends Error {\n constructor() {\n super(NOT_CONNECTED_MESSAGE);\n this.name = \"NotConnectedError\";\n }\n}\n\nexport interface BridgeReply {\n id: number;\n ok: boolean;\n result?: unknown;\n error?: string;\n}\n\nconst CONNECT_TIMEOUT_MS = 5_000;\nconst REQUEST_TIMEOUT_MS = 5_000;\nconst OPEN = 1;\n\nexport class BridgeClient {\n private socket: WebSocketLike | null = null;\n private connecting: Promise<WebSocketLike> | null = null;\n private nextId = 1;\n private readonly pending = new Map<\n number,\n { resolve: (r: BridgeReply) => void; reject: (e: Error) => void; timer: NodeJS.Timeout }\n >();\n\n constructor(\n private readonly url: string = BRIDGE_URL,\n private readonly socketFactory: WebSocketFactory = globalWebSocketFactory\n ) {}\n\n /** True when the underlying socket is open. */\n get connected(): boolean {\n return this.socket !== null && this.socket.readyState === OPEN;\n }\n\n private ensureSocket(): Promise<WebSocketLike> {\n if (this.socket && this.socket.readyState === OPEN) return Promise.resolve(this.socket);\n if (this.connecting) return this.connecting; // concurrent sends share one socket\n this.dropSocket();\n const attempt = new Promise<WebSocketLike>((resolve, reject) => {\n let socket: WebSocketLike;\n try {\n socket = this.socketFactory(this.url);\n } catch (e) {\n reject(e);\n return;\n }\n const timer = setTimeout(() => {\n reject(new NotConnectedError());\n socket.onopen = null;\n socket.onclose = null;\n socket.onerror = null;\n socket.onmessage = null;\n try {\n socket.close();\n } catch {\n /* already gone */\n }\n }, CONNECT_TIMEOUT_MS);\n socket.onopen = () => {\n clearTimeout(timer);\n this.socket = socket;\n resolve(socket);\n };\n socket.onclose = () => {\n clearTimeout(timer);\n this.dropSocket();\n reject(new NotConnectedError());\n };\n socket.onerror = () => {\n clearTimeout(timer);\n this.dropSocket();\n reject(new NotConnectedError());\n };\n socket.onmessage = (ev) => this.onMessage(ev);\n });\n this.connecting = attempt;\n const clear = () => {\n if (this.connecting === attempt) this.connecting = null;\n };\n attempt.then(clear, clear);\n return attempt;\n }\n\n private dropSocket(): void {\n if (this.socket) {\n this.socket.onopen = null;\n this.socket.onclose = null;\n this.socket.onerror = null;\n this.socket.onmessage = null;\n try {\n this.socket.close();\n } catch {\n /* already gone */\n }\n this.socket = null;\n }\n // Fail everything still waiting — the peer is gone.\n for (const [, p] of this.pending) {\n clearTimeout(p.timer);\n p.reject(new NotConnectedError());\n }\n this.pending.clear();\n }\n\n private onMessage(ev: { data: string }): void {\n let reply: BridgeReply;\n try {\n reply = JSON.parse(ev.data) as BridgeReply;\n } catch {\n return; // bridge noise (keepalive pings echo nothing back); ignore\n }\n const p = reply?.id !== undefined ? this.pending.get(reply.id) : undefined;\n if (!p) return;\n this.pending.delete(reply.id);\n clearTimeout(p.timer);\n p.resolve(reply);\n }\n\n /**\n * Sends one command and awaits its correlated reply. Connection problems\n * and timeouts both surface as NotConnectedError with the user-facing text.\n */\n async send(command: string, params: Record<string, unknown> = {}): Promise<unknown> {\n await this.ensureSocket();\n const id = this.nextId++;\n const frame = JSON.stringify({ id, command, ...params });\n return new Promise<unknown>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pending.delete(id);\n reject(new NotConnectedError());\n }, REQUEST_TIMEOUT_MS);\n this.pending.set(id, {\n resolve: (reply) => (reply.ok ? resolve(reply.result) : reject(new Error(reply.error ?? \"bridge error\"))),\n reject,\n timer,\n });\n try {\n this.socket!.send(frame);\n } catch (e) {\n this.pending.delete(id);\n clearTimeout(timer);\n reject(new NotConnectedError());\n }\n });\n }\n\n close(): void {\n this.dropSocket();\n }\n}\n","/**\n * pai-browser MCP tool table.\n *\n * One definition per tool: MCP-facing name/schema, the wire command it maps\n * to on the browser bridge, param translation, and result formatting. Kept as\n * a data table so tests can drive every mapping without a transport, and so\n * registerBrowserTools stays a five-line loop.\n */\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport { NotConnectedError } from \"./bridge-client.js\";\n\nexport interface BridgeLike {\n send(command: string, params?: Record<string, unknown>): Promise<unknown>;\n}\n\nexport interface BrowserToolDef {\n name: string;\n description: string;\n /** zod raw shape for server.tool() */\n shape: z.ZodRawShape;\n /** wire command the extension handles */\n command: string;\n /** MCP args → wire params */\n toParams: (args: Record<string, unknown>) => Record<string, unknown>;\n /** wire result → text shown to the model */\n format?: (result: unknown) => string;\n}\n\nconst tabSchema = z.number().int().describe(\"Tab id, as returned by tabs_list.\");\n\nfunction asRecord(v: unknown): Record<string, unknown> {\n return (v ?? {}) as Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Result formatters\n// ---------------------------------------------------------------------------\n\nfunction formatTabs(result: unknown): string {\n const tabs = (Array.isArray(result) ? result : asRecord(result).tabs) as Array<\n Record<string, unknown>\n >;\n if (!Array.isArray(tabs) || tabs.length === 0) return \"(no open tabs)\";\n return tabs\n .map(\n (t) =>\n `${t.id}${t.active ? \" *\" : \" \"} ${String(t.title ?? \"\")}\\n ${String(t.url ?? \"\")}`\n )\n .join(\"\\n\");\n}\n\nfunction formatSnapshot(result: unknown): string {\n return String(asRecord(result).yaml ?? \"\");\n}\n\nfunction formatLogs(result: unknown): string {\n const entries = asRecord(result).entries as Array<Record<string, unknown>>;\n if (!Array.isArray(entries) || entries.length === 0) return \"(no console output captured)\";\n return entries\n .map((e) => {\n const kind = e.source === \"console\" ? String(e.type ?? \"log\") : String(e.level ?? \"log\");\n return `[${kind}] ${String(e.text ?? \"\")}`;\n })\n .join(\"\\n\");\n}\n\nfunction formatEval(result: unknown): string {\n const value = asRecord(result).value;\n return typeof value === \"string\" ? value : JSON.stringify(value, null, 2);\n}\n\nfunction formatScreenshot(result: unknown): string {\n // Raw base64 PNG — the tool contract is \"base64 png\", not prose.\n return String(asRecord(result).base64 ?? \"\");\n}\n\n// ---------------------------------------------------------------------------\n// Tool table: MCP name → wire command\n// ---------------------------------------------------------------------------\n\nexport const BROWSER_TOOLS: BrowserToolDef[] = [\n {\n name: \"tabs_list\",\n description: \"List the tabs of the user's real running Chrome (id, title, url, active).\",\n shape: {},\n command: \"list_tabs\",\n toParams: () => ({}),\n format: formatTabs,\n },\n {\n name: \"tab_open\",\n description: \"Open a new tab in the user's Chrome and return its tab id.\",\n shape: {\n url: z.string().describe(\"URL to open.\"),\n active: z.boolean().optional().describe(\"Focus the new tab (default true).\"),\n },\n command: \"open_tab\",\n toParams: (a) => ({ url: a.url, ...(a.active !== undefined ? { active: a.active } : {}) }),\n },\n {\n name: \"tab_select\",\n description: \"Bring a tab to the front (activate it and focus its window).\",\n shape: { tab: tabSchema },\n command: \"select_tab\",\n toParams: (a) => ({ tabId: a.tab }),\n },\n {\n name: \"tab_close\",\n description: \"Close a tab.\",\n shape: { tab: tabSchema },\n command: \"close_tab\",\n toParams: (a) => ({ tabId: a.tab }),\n },\n {\n name: \"dom_snapshot\",\n description:\n \"Snapshot a tab's DOM as an accessibility-tree YAML with [ref=sN] ids for interactive elements. \" +\n \"Take a snapshot before dom_click/dom_type; refs stay valid until the next snapshot or navigation.\",\n shape: { tab: tabSchema },\n command: \"snapshot\",\n toParams: (a) => ({ tabId: a.tab }),\n format: formatSnapshot,\n },\n {\n name: \"dom_click\",\n description: \"Click an element by its [ref=sN] from the latest dom_snapshot (trusted CDP event).\",\n shape: { tab: tabSchema, ref: z.string().describe(\"Ref id from dom_snapshot, e.g. s3.\") },\n command: \"click\",\n toParams: (a) => ({ tabId: a.tab, ref: a.ref }),\n },\n {\n name: \"dom_type\",\n description:\n \"Focus an element by its [ref=sN] and type text into it (inserts at the caret, works in SPAs).\",\n shape: {\n tab: tabSchema,\n ref: z.string().describe(\"Ref id from dom_snapshot, e.g. s5.\"),\n text: z.string().describe(\"Text to type.\"),\n },\n command: \"type\",\n toParams: (a) => ({ tabId: a.tab, ref: a.ref, text: a.text }),\n },\n {\n name: \"page_text\",\n description: \"Read a tab's visible text (document.body.innerText).\",\n shape: { tab: tabSchema },\n command: \"eval\",\n toParams: () => ({ code: \"document.body.innerText\" }),\n format: formatEval,\n },\n {\n name: \"eval_js\",\n description: \"Evaluate JavaScript in a tab and return the JSON-safe result. Awaits promises.\",\n shape: {\n tab: tabSchema,\n code: z.string().describe(\"JavaScript expression to evaluate.\"),\n },\n command: \"eval\",\n toParams: (a) => ({ code: a.code }),\n format: formatEval,\n },\n {\n name: \"tab_screenshot\",\n description: \"Screenshot a tab (PNG, base64).\",\n shape: { tab: tabSchema },\n command: \"screenshot\",\n toParams: (a) => ({ tabId: a.tab }),\n format: formatScreenshot,\n },\n {\n name: \"console_logs\",\n description: \"Read the console output captured for a tab since its last snapshot.\",\n shape: { tab: tabSchema },\n command: \"console_logs\",\n toParams: (a) => ({ tabId: a.tab }),\n format: formatLogs,\n },\n];\n\n/** Runs one tool against the bridge; returns MCP content + isError. */\nexport async function runBrowserTool(\n def: BrowserToolDef,\n args: Record<string, unknown>,\n bridge: BridgeLike\n): Promise<{ content: Array<{ type: \"text\"; text: string }>; isError?: boolean }> {\n try {\n const result = await bridge.send(def.command, def.toParams(args));\n return { content: [{ type: \"text\", text: def.format ? def.format(result) : JSON.stringify(result, null, 2) }] };\n } catch (e) {\n const msg =\n e instanceof NotConnectedError\n ? e.message\n : e instanceof Error\n ? e.message\n : String(e);\n return { content: [{ type: \"text\", text: msg }], isError: true };\n }\n}\n\n/** Registers every browser tool on an MCP server. */\nexport function registerBrowserTools(server: McpServer, bridge: BridgeLike): void {\n for (const def of BROWSER_TOOLS) {\n server.tool(def.name, def.description, def.shape, (args) => runBrowserTool(def, args, bridge));\n }\n}\n","#!/usr/bin/env node\n/**\n * pai-browser-mcp — stdio MCP server for the PAI browser bridge.\n *\n * The provider-independent claude-in-chrome replacement: drives the user's\n * real running Chrome (tabs + DOM) through the browser-bridge extension's\n * native messaging host, over ws://127.0.0.1:8756. Any MCP client works —\n * nothing here is Anthropic-specific.\n *\n * provider → MCP (this server) → WebSocket → native host → extension → Chrome\n *\n * If Chrome is not running (or the extension is not loaded), every tool\n * returns a clear \"start Chrome\" error; the server itself stays alive and\n * reconnects lazily per call.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { BridgeClient, globalWebSocketFactory } from \"./bridge-client.js\";\nimport { registerBrowserTools } from \"./tools.js\";\n\nasync function main(): Promise<void> {\n if (globalThis.WebSocket === undefined) {\n // Node < 22 has no global WebSocket client; say so instead of crashing\n // with an opaque TypeError when the first tool call connects.\n process.stderr.write(\n \"pai-browser-mcp: this node runtime has no global WebSocket — run with node >= 22\\n\"\n );\n process.exit(1);\n }\n\n const server = new McpServer({\n name: \"pai-browser\",\n version: \"0.1.0\",\n });\n\n const bridge = new BridgeClient(undefined, globalWebSocketFactory);\n registerBrowserTools(server, bridge);\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch((e) => {\n process.stderr.write(`pai-browser-mcp fatal error: ${String(e)}\\n`);\n process.exit(1);\n});\n"],"mappings":"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"}
|
|
@@ -17,6 +17,7 @@ function parseRunnerArgs(argv) {
|
|
|
17
17
|
let callerMcpConfig = false;
|
|
18
18
|
let callerSystemPrompt = false;
|
|
19
19
|
const mcp = [];
|
|
20
|
+
const allowedTools = [];
|
|
20
21
|
let i = 0;
|
|
21
22
|
while (i < argv.length) {
|
|
22
23
|
const a = argv[i];
|
|
@@ -42,7 +43,18 @@ function parseRunnerArgs(argv) {
|
|
|
42
43
|
i += 1;
|
|
43
44
|
}
|
|
44
45
|
} else if (a.startsWith("--mcp=")) mcp.push(a.slice(6));
|
|
45
|
-
else {
|
|
46
|
+
else if (a === "--allowedTools") {
|
|
47
|
+
rest.push(a);
|
|
48
|
+
const v = argv[i + 1];
|
|
49
|
+
if (v !== void 0 && !v.startsWith("-")) {
|
|
50
|
+
allowedTools.push(v);
|
|
51
|
+
rest.push(v);
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
54
|
+
} else if (a.startsWith("--allowedTools=")) {
|
|
55
|
+
allowedTools.push(a.slice(15));
|
|
56
|
+
rest.push(a);
|
|
57
|
+
} else {
|
|
46
58
|
if (a === "--model") callerModel = true;
|
|
47
59
|
if (a.startsWith("--model=")) callerModel = true;
|
|
48
60
|
if (a === "--mcp-config") callerMcpConfig = true;
|
|
@@ -62,7 +74,8 @@ function parseRunnerArgs(argv) {
|
|
|
62
74
|
callerModel,
|
|
63
75
|
callerMcpConfig,
|
|
64
76
|
callerSystemPrompt,
|
|
65
|
-
mcp
|
|
77
|
+
mcp,
|
|
78
|
+
allowedTools
|
|
66
79
|
};
|
|
67
80
|
}
|
|
68
81
|
/**
|
|
@@ -1607,9 +1620,11 @@ function relShort(p, cwd) {
|
|
|
1607
1620
|
* Headless workers start with NO MCP servers: every server definition is a
|
|
1608
1621
|
* prompt-time tool inventory the model pays to know, and a worker that only
|
|
1609
1622
|
* reads and edits files needs none of it. A run may opt in with `--mcp
|
|
1610
|
-
* name[,name…]
|
|
1611
|
-
*
|
|
1612
|
-
*
|
|
1623
|
+
* name[,name…]`, a role carrying `"mcp": [...]`, or implicitly by naming
|
|
1624
|
+
* `mcp__server__tool` in --allowedTools (a grant without its server loaded is
|
|
1625
|
+
* a dead letter). Names may be single servers from ~/.claude.json's
|
|
1626
|
+
* `mcpServers` or `workers.mcpSets` set names, which expand to their member
|
|
1627
|
+
* list. The filtered config lands in
|
|
1613
1628
|
* `<logDir>/<id>.mcp.json` and is passed with `--strict-mcp-config
|
|
1614
1629
|
* --mcp-config` so exactly those servers load. MCP servers are chosen at
|
|
1615
1630
|
* launch only — a mid-run `say` cannot add any.
|
|
@@ -1646,6 +1661,22 @@ function expandMcpNames(names, config, claudeJson = CLAUDE_JSON) {
|
|
|
1646
1661
|
}
|
|
1647
1662
|
return out;
|
|
1648
1663
|
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Derive server names from tool grants: every `mcp__<server>__<tool>` (or bare
|
|
1666
|
+
* `mcp__<server>`, or `mcp__<server>__*`) in an --allowedTools list names a
|
|
1667
|
+
* server the run expects to be loaded. A grant is a dead letter unless its
|
|
1668
|
+
* server is in the filtered config, so the runner treats these as implicit
|
|
1669
|
+
* --mcp names — and only these; non-mcp grants load nothing.
|
|
1670
|
+
*/
|
|
1671
|
+
function mcpServersFromToolGrants(tools) {
|
|
1672
|
+
const out = [];
|
|
1673
|
+
for (const entry of tools) for (const name of entry.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
1674
|
+
if (!name.startsWith("mcp__")) continue;
|
|
1675
|
+
const server = name.slice(5).split("__")[0];
|
|
1676
|
+
if (server && !out.includes(server)) out.push(server);
|
|
1677
|
+
}
|
|
1678
|
+
return out;
|
|
1679
|
+
}
|
|
1649
1680
|
/** Path of a run's filtered MCP config. */
|
|
1650
1681
|
function runMcpConfigPath(logDir, id) {
|
|
1651
1682
|
return join(logDir, `${id}.mcp.json`);
|
|
@@ -1961,7 +1992,7 @@ async function runWorker(opts) {
|
|
|
1961
1992
|
const logDir = workersLogDir(config);
|
|
1962
1993
|
mkdirSync(logDir, { recursive: true });
|
|
1963
1994
|
if (opts.className === "plan" && !opts._planner) {
|
|
1964
|
-
const { runPlanner } = await import("./planner-
|
|
1995
|
+
const { runPlanner } = await import("./planner-DsmMRIZR.mjs");
|
|
1965
1996
|
return runPlanner(opts);
|
|
1966
1997
|
}
|
|
1967
1998
|
const parent = launchParent(opts.parent);
|
|
@@ -2093,7 +2124,8 @@ async function executeRun(a) {
|
|
|
2093
2124
|
const wanted = [
|
|
2094
2125
|
...a.mcpFlag ? [a.mcpFlag] : [],
|
|
2095
2126
|
...parsed.mcp,
|
|
2096
|
-
...target.classMcp ?? []
|
|
2127
|
+
...target.classMcp ?? [],
|
|
2128
|
+
...mcpServersFromToolGrants(parsed.allowedTools)
|
|
2097
2129
|
];
|
|
2098
2130
|
if (wanted.length) mcpArgs = [
|
|
2099
2131
|
"--strict-mcp-config",
|
|
@@ -2794,4 +2826,4 @@ async function runChain(opts, deps = {}) {
|
|
|
2794
2826
|
|
|
2795
2827
|
//#endregion
|
|
2796
2828
|
export { appendLedger as A, ageOf as C, loadStatus as D, contextPercent as E, parseRunnerArgs as M, shortText as N, loadStatuses as O, UNLABELED as S, contextLabel as T, openPaneForWorker as _, testProvider as a, sessionTag as b, renderReport as c, handoffFromInside as d, readInbox as f, openFollowPane as g, checkPaneForWorker as h, runWorker as i, ledgerSummary as j, newWorkerId as k, discardWorker as l, workerDepth as m, swapPromptArg as n, describeMcp as o, sayToWorker as p, printResult as r, parseWorkerReport as s, runChain as t, mergeWorker as u, currentTabKey as v, alive as w, workerInScope as x, resolveSession as y };
|
|
2797
|
-
//# sourceMappingURL=chain-
|
|
2829
|
+
//# sourceMappingURL=chain-zMjCDW-R.mjs.map
|