bladebro 4.5.1 → 4.5.2
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/package.json +6 -9
- package/pi-extension.ts +29 -487
- package/skills/bladebro/SKILL.md +5 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bladebro",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.2",
|
|
4
4
|
"description": "Stealthy and efficient agentic browser driver for AI agents. Few tools, full control, real stealth, max token efficiency.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -37,9 +37,6 @@
|
|
|
37
37
|
"peerDependenciesMeta": {
|
|
38
38
|
"@earendil-works/pi-coding-agent": {
|
|
39
39
|
"optional": true
|
|
40
|
-
},
|
|
41
|
-
"typebox": {
|
|
42
|
-
"optional": true
|
|
43
40
|
}
|
|
44
41
|
},
|
|
45
42
|
"bin": {
|
|
@@ -56,11 +53,11 @@
|
|
|
56
53
|
"LICENSE"
|
|
57
54
|
],
|
|
58
55
|
"optionalDependencies": {
|
|
59
|
-
"bladebro-linux-x64": "4.5.
|
|
60
|
-
"bladebro-linux-arm64": "4.5.
|
|
61
|
-
"bladebro-darwin-x64": "4.5.
|
|
62
|
-
"bladebro-darwin-arm64": "4.5.
|
|
63
|
-
"bladebro-windows-x64": "4.5.
|
|
56
|
+
"bladebro-linux-x64": "4.5.2",
|
|
57
|
+
"bladebro-linux-arm64": "4.5.2",
|
|
58
|
+
"bladebro-darwin-x64": "4.5.2",
|
|
59
|
+
"bladebro-darwin-arm64": "4.5.2",
|
|
60
|
+
"bladebro-windows-x64": "4.5.2"
|
|
64
61
|
},
|
|
65
62
|
"engines": {
|
|
66
63
|
"node": ">=14"
|
package/pi-extension.ts
CHANGED
|
@@ -1,37 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bladebro pi extension —
|
|
2
|
+
* Bladebro pi extension — the bladebro MCP server, registered with pi.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* This is the whole extension. It resolves the bladebro binary this package
|
|
5
|
+
* installed and hands it to pi as a stdio MCP server; pi's own MCP client does
|
|
6
|
+
* everything else — spawn, handshake, tool-call proxying, shutdown. The five
|
|
7
|
+
* tools arrive with the binary's full definitions (descriptions, schemas and
|
|
8
|
+
* the server's `initialize` instructions), so no meaning is copied here and
|
|
9
|
+
* there is nothing that can drift when the Rust side changes.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
+
* `exposure: "direct"` declares the tools to the model like any other pi tool,
|
|
12
|
+
* and the session waits for this server before the first prompt. The timeout
|
|
13
|
+
* covers the driver's long operations (run/batch/collect), not pi's 60s default.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - tool call → proxy to binary via tools/call, return result
|
|
15
|
-
* - session_shutdown → kill binary (which kills Chrome)
|
|
16
|
-
*
|
|
17
|
-
* Chrome launches lazily inside the binary (first tools/call only).
|
|
18
|
-
* The binary process itself starts in milliseconds.
|
|
19
|
-
*
|
|
20
|
-
* Auto-adapt: tool descriptions, schemas, new tools, removed tools,
|
|
21
|
-
* changed parameters — all fetched from the binary at session start.
|
|
22
|
-
* The extension never hardcodes tool definitions.
|
|
15
|
+
* No protocol code, no copied schemas, no rendering: a troubleshooting step
|
|
16
|
+
* belongs at the source — the binary — never in this shim.
|
|
23
17
|
*/
|
|
24
|
-
|
|
25
18
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
|
-
import { Type } from "typebox";
|
|
27
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
28
|
-
import { StringDecoder } from "node:string_decoder";
|
|
29
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
30
19
|
import { existsSync } from "node:fs";
|
|
31
20
|
import { dirname, join } from "node:path";
|
|
32
21
|
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
/** npm platform packages → the binary each one carries. */
|
|
35
23
|
const PLATFORM_MAP: Record<string, string> = {
|
|
36
24
|
"linux-x64": "bladebro-linux-x64",
|
|
37
25
|
"linux-arm64": "bladebro-linux-arm64",
|
|
@@ -40,482 +28,36 @@ const PLATFORM_MAP: Record<string, string> = {
|
|
|
40
28
|
"win32-x64": "bladebro-windows-x64",
|
|
41
29
|
};
|
|
42
30
|
|
|
31
|
+
/** Absolute path of this platform's binary, or null when it is not installed. */
|
|
43
32
|
function resolveBinary(): string | null {
|
|
44
|
-
const
|
|
45
|
-
const pkgName = PLATFORM_MAP[key];
|
|
33
|
+
const pkgName = PLATFORM_MAP[`${process.platform}-${process.arch}`];
|
|
46
34
|
if (!pkgName) return null;
|
|
47
35
|
try {
|
|
48
36
|
const pkgJsonPath = require.resolve(`${pkgName}/package.json`);
|
|
49
37
|
const pkgJson = require(pkgJsonPath);
|
|
50
|
-
const
|
|
51
|
-
const binPath = join(dirname(pkgJsonPath), binName);
|
|
38
|
+
const binPath = join(dirname(pkgJsonPath), pkgJson.main || "bladebro");
|
|
52
39
|
return existsSync(binPath) ? binPath : null;
|
|
53
40
|
} catch {
|
|
54
41
|
return null;
|
|
55
42
|
}
|
|
56
43
|
}
|
|
57
44
|
|
|
58
|
-
// ── TUI rendering helpers ─────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
/** Extract text from MCP tool result content blocks. */
|
|
61
|
-
function getResultText(result: any): string {
|
|
62
|
-
if (!result?.content) return "";
|
|
63
|
-
return result.content
|
|
64
|
-
.filter((c: any) => c.type === "text")
|
|
65
|
-
.map((c: any) => c.text)
|
|
66
|
-
.join("\n");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Format the `act` tool call display. */
|
|
70
|
-
function formatActCall(args: any, theme: any): string {
|
|
71
|
-
const action = args.action || "?";
|
|
72
|
-
const bold = (s: string) => theme.bold(s);
|
|
73
|
-
const accent = (s: string) => theme.fg("accent", s);
|
|
74
|
-
const muted = (s: string) => theme.fg("muted", s);
|
|
75
|
-
const title = bold("act");
|
|
76
|
-
|
|
77
|
-
switch (action) {
|
|
78
|
-
case "navigate": {
|
|
79
|
-
const url = args.url || "";
|
|
80
|
-
return `${title} ${accent("navigate")} ${muted("→")} ${accent(url)}`;
|
|
81
|
-
}
|
|
82
|
-
case "click": {
|
|
83
|
-
const target = args.ref || args.text || args.label || "";
|
|
84
|
-
return `${title} ${accent("click")} ${muted(target)}`;
|
|
85
|
-
}
|
|
86
|
-
case "type": {
|
|
87
|
-
const target = args.ref || args.label || "";
|
|
88
|
-
const text = args.text || "";
|
|
89
|
-
const press = args.press ? ` ${muted("+ " + args.press)}` : "";
|
|
90
|
-
return `${title} ${accent("type")} ${muted(target ? `${target} ` : "")}${muted(`"${text}"`)}${press}`;
|
|
91
|
-
}
|
|
92
|
-
case "scroll": {
|
|
93
|
-
const dy = args.dy || 0;
|
|
94
|
-
const dx = args.dx || 0;
|
|
95
|
-
const arrow = dy > 0 ? "↓" : dy < 0 ? "↑" : dx > 0 ? "→" : "←";
|
|
96
|
-
const mag = Math.abs(dy || dx);
|
|
97
|
-
return `${title} ${accent("scroll")} ${muted(`${arrow}${mag}`)}`;
|
|
98
|
-
}
|
|
99
|
-
case "fill": {
|
|
100
|
-
const n = args.fields?.length || 0;
|
|
101
|
-
const submit = args.submit ? ` ${muted("+ submit")}` : "";
|
|
102
|
-
return `${title} ${accent("fill")} ${muted(`(${n} field${n !== 1 ? "s" : ""})`)}${submit}`;
|
|
103
|
-
}
|
|
104
|
-
case "batch": {
|
|
105
|
-
const n = args.steps?.length || 0;
|
|
106
|
-
return `${title} ${accent("batch")} ${muted(`(${n} step${n !== 1 ? "s" : ""})`)}`;
|
|
107
|
-
}
|
|
108
|
-
case "hover": {
|
|
109
|
-
const target = args.ref || args.text || args.label || "";
|
|
110
|
-
return `${title} ${accent("hover")} ${muted(target)}`;
|
|
111
|
-
}
|
|
112
|
-
case "press": {
|
|
113
|
-
return `${title} ${accent("press")} ${muted(args.key || "")}`;
|
|
114
|
-
}
|
|
115
|
-
case "eval": {
|
|
116
|
-
const js = (args.js || "").slice(0, 50);
|
|
117
|
-
return `${title} ${accent("eval")} ${muted(`"${js}${js.length >= 50 ? "…" : ""}"`)}`;
|
|
118
|
-
}
|
|
119
|
-
case "wait": {
|
|
120
|
-
const cond = args.condition || "settle";
|
|
121
|
-
const t = args.timeout || 10;
|
|
122
|
-
return `${title} ${accent("wait")} ${muted(`${cond} (${t}s)`)}`;
|
|
123
|
-
}
|
|
124
|
-
case "read": {
|
|
125
|
-
return `${title} ${accent("read")} ${muted(args.ref || "")}`;
|
|
126
|
-
}
|
|
127
|
-
case "back": return `${title} ${accent("back")}`;
|
|
128
|
-
case "forward": return `${title} ${accent("forward")}`;
|
|
129
|
-
case "reload": return `${title} ${accent("reload")}`;
|
|
130
|
-
case "select": {
|
|
131
|
-
const target = args.ref || "";
|
|
132
|
-
const opt = args.option || "";
|
|
133
|
-
return `${title} ${accent("select")} ${muted(`${target} "${opt}"`)}`;
|
|
134
|
-
}
|
|
135
|
-
case "pdf": return `${title} ${accent("pdf")}`;
|
|
136
|
-
case "download": return `${title} ${accent("download")}`;
|
|
137
|
-
case "collect": return `${title} ${accent("collect")}`;
|
|
138
|
-
case "clear": return `${title} ${accent("clear")} ${muted(args.ref || "")}`;
|
|
139
|
-
case "upload": return `${title} ${accent("upload")} ${muted(args.ref || "")}`;
|
|
140
|
-
default: return `${title} ${accent(action)}`;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Format the `see` tool call display. */
|
|
145
|
-
function formatSeeCall(args: any, theme: any): string {
|
|
146
|
-
const bold = (s: string) => theme.bold(s);
|
|
147
|
-
const accent = (s: string) => theme.fg("accent", s);
|
|
148
|
-
const muted = (s: string) => theme.fg("muted", s);
|
|
149
|
-
const title = bold("see");
|
|
150
|
-
|
|
151
|
-
const mode = args.mode;
|
|
152
|
-
if (mode === "content") return `${title} ${accent("content")}`;
|
|
153
|
-
if (mode === "outline") return `${title} ${accent("outline")}`;
|
|
154
|
-
if (mode === "model") return `${title} ${accent("model")}`;
|
|
155
|
-
|
|
156
|
-
const find = args.find;
|
|
157
|
-
if (find) return `${title} ${accent("find")} ${muted(`"${find}"`)}`;
|
|
158
|
-
|
|
159
|
-
const extract = args.extract;
|
|
160
|
-
if (extract) return `${title} ${accent(`extract=${extract}`)}`;
|
|
161
|
-
|
|
162
|
-
const scope = args.scope;
|
|
163
|
-
if (scope) return `${title} ${accent("scope")} ${muted(scope)}`;
|
|
164
|
-
|
|
165
|
-
const filter = args.filter;
|
|
166
|
-
if (filter) return `${title} ${accent("model")} ${muted(`[${filter}]`)}`;
|
|
167
|
-
|
|
168
|
-
const logs = args.logs;
|
|
169
|
-
if (logs) return `${title} ${accent("logs")} ${muted(logs)}`;
|
|
170
|
-
|
|
171
|
-
return `${title} ${accent("model")}`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Format the `state` tool call display. */
|
|
175
|
-
function formatStateCall(args: any, theme: any): string {
|
|
176
|
-
const bold = (s: string) => theme.bold(s);
|
|
177
|
-
const accent = (s: string) => theme.fg("accent", s);
|
|
178
|
-
const muted = (s: string) => theme.fg("muted", s);
|
|
179
|
-
const title = bold("state");
|
|
180
|
-
|
|
181
|
-
const op = args.op || "?";
|
|
182
|
-
const extra = args.url || args.name || "";
|
|
183
|
-
return `${title} ${accent(op)}${extra ? ` ${muted(extra)}` : ""}`;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** Format the `run` tool call display. */
|
|
187
|
-
function formatRunCall(args: any, theme: any): string {
|
|
188
|
-
const bold = (s: string) => theme.bold(s);
|
|
189
|
-
const title = bold("run");
|
|
190
|
-
return title;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Format the `vision` tool call display. */
|
|
194
|
-
function formatVisionCall(args: any, theme: any): string {
|
|
195
|
-
const bold = (s: string) => theme.bold(s);
|
|
196
|
-
const title = bold("vision");
|
|
197
|
-
return title;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** Format tool result for display. */
|
|
201
|
-
function formatResult(text: string, expanded: boolean, isError: boolean, theme: any): string {
|
|
202
|
-
const out = (s: string) => theme.fg("toolOutput", s);
|
|
203
|
-
const warn = (s: string) => theme.fg("warning", s);
|
|
204
|
-
const muted = (s: string) => theme.fg("muted", s);
|
|
205
|
-
const accent = (s: string) => theme.fg("accent", s);
|
|
206
|
-
|
|
207
|
-
if (!text) return muted("(no output)");
|
|
208
|
-
|
|
209
|
-
const lines = text.split("\n");
|
|
210
|
-
|
|
211
|
-
if (isError) {
|
|
212
|
-
// Error: show all lines in warning color
|
|
213
|
-
return lines.map((l) => warn(l)).join("\n");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Truncate if not expanded
|
|
217
|
-
const maxLines = expanded ? 9999 : 15;
|
|
218
|
-
const visible = lines.slice(0, maxLines);
|
|
219
|
-
const skipped = lines.length - visible.length;
|
|
220
|
-
|
|
221
|
-
const formatted = visible.map((line) => {
|
|
222
|
-
// Outcome lines → accent
|
|
223
|
-
if (line.startsWith("outcome:")) return accent(line);
|
|
224
|
-
// Warning lines (⚠) → warning color
|
|
225
|
-
if (line.includes("\u{26a0}")) return warn(line);
|
|
226
|
-
// Verdict lines with arrow → toolOutput
|
|
227
|
-
return out(line);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
if (skipped > 0) {
|
|
231
|
-
formatted.push(muted(` … ${skipped} more line${skipped !== 1 ? "s" : ""} (expand to view)`));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return formatted.join("\n");
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ── Minimal MCP stdio client ──────────────────────────────────────────
|
|
238
|
-
|
|
239
|
-
interface PendingReq {
|
|
240
|
-
resolve: (v: any) => void;
|
|
241
|
-
reject: (e: any) => void;
|
|
242
|
-
timer: ReturnType<typeof setTimeout>;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
class McpStdio {
|
|
246
|
-
private proc: ChildProcess | null = null;
|
|
247
|
-
private nextId = 1;
|
|
248
|
-
private pending = new Map<number, PendingReq>();
|
|
249
|
-
private buffer = "";
|
|
250
|
-
private alive = false;
|
|
251
|
-
private decoder = new StringDecoder("utf8");
|
|
252
|
-
|
|
253
|
-
start(binaryPath: string): Promise<void> {
|
|
254
|
-
return new Promise((resolve, reject) => {
|
|
255
|
-
this.buffer = "";
|
|
256
|
-
this.decoder = new StringDecoder("utf8");
|
|
257
|
-
const proc = this.proc = spawn(binaryPath, ["mcp"], {
|
|
258
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
259
|
-
env: { ...process.env },
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
const onExit = () => {
|
|
263
|
-
if (this.proc !== proc) return;
|
|
264
|
-
this.alive = false;
|
|
265
|
-
this.proc = null;
|
|
266
|
-
for (const [, p] of this.pending) {
|
|
267
|
-
clearTimeout(p.timer);
|
|
268
|
-
p.reject(new Error("bladebro process exited"));
|
|
269
|
-
}
|
|
270
|
-
this.pending.clear();
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
proc.on("exit", onExit);
|
|
274
|
-
proc.stdin!.on("error", (err) => {
|
|
275
|
-
onExit();
|
|
276
|
-
proc.kill();
|
|
277
|
-
reject(err);
|
|
278
|
-
});
|
|
279
|
-
this.proc.on("error", (err) => {
|
|
280
|
-
onExit();
|
|
281
|
-
reject(err);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
this.proc.stdout!.on("data", (data: Buffer) => this.onData(data));
|
|
285
|
-
this.proc.stderr!.on("data", (data: Buffer) => {
|
|
286
|
-
const text = data.toString();
|
|
287
|
-
for (const line of text.split("\n")) {
|
|
288
|
-
const trimmed = line.trim();
|
|
289
|
-
if (!trimmed) continue;
|
|
290
|
-
if (/error|panic|fatal|warn|fail/i.test(trimmed)) {
|
|
291
|
-
process.stderr.write(line + "\n");
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
this.request("initialize", {
|
|
297
|
-
protocolVersion: "2025-06-18",
|
|
298
|
-
capabilities: {},
|
|
299
|
-
clientInfo: { name: "pi-bladebro", version: "1.0.0" },
|
|
300
|
-
}, 10000)
|
|
301
|
-
.then(() => {
|
|
302
|
-
this.notify("notifications/initialized", {});
|
|
303
|
-
this.alive = true;
|
|
304
|
-
resolve();
|
|
305
|
-
})
|
|
306
|
-
.catch((err) => {
|
|
307
|
-
onExit();
|
|
308
|
-
proc.kill();
|
|
309
|
-
reject(err);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
private onData(data: Buffer): void {
|
|
315
|
-
this.buffer += this.decoder.write(data);
|
|
316
|
-
if (Buffer.byteLength(this.buffer, "utf8") > 64 * 1024 * 1024) {
|
|
317
|
-
for (const [, p] of this.pending) {
|
|
318
|
-
clearTimeout(p.timer);
|
|
319
|
-
p.reject(new Error("bladebro MCP response exceeds 64 MiB"));
|
|
320
|
-
}
|
|
321
|
-
this.pending.clear();
|
|
322
|
-
this.proc?.kill();
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
let idx: number;
|
|
326
|
-
while ((idx = this.buffer.indexOf("\n")) >= 0) {
|
|
327
|
-
const line = this.buffer.slice(0, idx).trim();
|
|
328
|
-
this.buffer = this.buffer.slice(idx + 1);
|
|
329
|
-
if (line) this.onMessage(line);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
private onMessage(line: string): void {
|
|
334
|
-
let msg: any;
|
|
335
|
-
try { msg = JSON.parse(line); } catch {
|
|
336
|
-
for (const [, p] of this.pending) {
|
|
337
|
-
clearTimeout(p.timer);
|
|
338
|
-
p.reject(new Error("Invalid MCP JSON from bladebro"));
|
|
339
|
-
}
|
|
340
|
-
this.pending.clear();
|
|
341
|
-
this.proc?.kill();
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
if (!msg || typeof msg !== "object" || Array.isArray(msg)) {
|
|
345
|
-
this.proc?.kill();
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
349
|
-
const entry = this.pending.get(msg.id)!;
|
|
350
|
-
this.pending.delete(msg.id);
|
|
351
|
-
clearTimeout(entry.timer);
|
|
352
|
-
if (msg.error) entry.reject(new Error(msg.error.message || "MCP request failed"));
|
|
353
|
-
else if (!("result" in msg)) entry.reject(new Error("MCP response missing result"));
|
|
354
|
-
else entry.resolve(msg.result);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
private request(method: string, params: any, timeoutMs = 180000): Promise<any> {
|
|
359
|
-
return new Promise((resolve, reject) => {
|
|
360
|
-
if (!this.proc || this.proc.exitCode !== null || this.proc.signalCode !== null || !this.proc.stdin?.writable) {
|
|
361
|
-
reject(new Error("bladebro process is not running"));
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
const id = this.nextId++;
|
|
365
|
-
const timer = setTimeout(() => {
|
|
366
|
-
this.pending.delete(id);
|
|
367
|
-
reject(new Error(`MCP timeout: ${method} (${timeoutMs}ms)`));
|
|
368
|
-
}, timeoutMs);
|
|
369
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
370
|
-
const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
371
|
-
this.proc?.stdin?.write(msg + "\n");
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
private notify(method: string, params: any): void {
|
|
376
|
-
const msg = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
377
|
-
this.proc?.stdin?.write(msg + "\n");
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async listTools(): Promise<any[]> {
|
|
381
|
-
const result = await this.request("tools/list", {}, 10000);
|
|
382
|
-
return result.tools || [];
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
async callTool(name: string, args: any): Promise<any> {
|
|
386
|
-
return this.request("tools/call", { name, arguments: args });
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
isAlive(): boolean { return this.alive && this.proc !== null; }
|
|
390
|
-
|
|
391
|
-
async stop(): Promise<void> {
|
|
392
|
-
const proc = this.proc;
|
|
393
|
-
if (!proc) return;
|
|
394
|
-
this.alive = false;
|
|
395
|
-
for (const [, p] of this.pending) {
|
|
396
|
-
clearTimeout(p.timer);
|
|
397
|
-
p.reject(new Error("bladebro session stopped"));
|
|
398
|
-
}
|
|
399
|
-
this.pending.clear();
|
|
400
|
-
if (proc.exitCode === null && proc.signalCode === null) {
|
|
401
|
-
await new Promise<void>((resolve) => {
|
|
402
|
-
const t = setTimeout(() => { proc.kill("SIGKILL"); resolve(); }, 5000);
|
|
403
|
-
proc.once("exit", () => { clearTimeout(t); resolve(); });
|
|
404
|
-
proc.stdin?.end();
|
|
405
|
-
proc.kill("SIGTERM");
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
if (this.proc === proc) this.proc = null;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// ── Extension ─────────────────────────────────────────────────────────
|
|
413
|
-
|
|
414
|
-
const TOOL_FORMATTERS: Record<string, (args: any, theme: any) => string> = {
|
|
415
|
-
act: formatActCall,
|
|
416
|
-
see: formatSeeCall,
|
|
417
|
-
state: formatStateCall,
|
|
418
|
-
run: formatRunCall,
|
|
419
|
-
vision: formatVisionCall,
|
|
420
|
-
};
|
|
421
|
-
|
|
422
45
|
export default function bladebroExtension(pi: ExtensionAPI) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
let restartPromise: Promise<void> | null = null;
|
|
46
|
+
const key = `${process.platform}-${process.arch}`;
|
|
47
|
+
const binary = resolveBinary();
|
|
426
48
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (!binaryPath) throw new Error("Bladebro binary not available");
|
|
434
|
-
const p = (async () => {
|
|
435
|
-
const c = new McpStdio();
|
|
436
|
-
await c.start(binaryPath!);
|
|
437
|
-
client = c;
|
|
438
|
-
})();
|
|
439
|
-
restartPromise = p;
|
|
440
|
-
try {
|
|
441
|
-
await p;
|
|
442
|
-
} finally {
|
|
443
|
-
restartPromise = null;
|
|
444
|
-
}
|
|
445
|
-
return client!;
|
|
49
|
+
if (!binary || typeof pi.registerMcpServer !== "function") {
|
|
50
|
+
const why = !binary
|
|
51
|
+
? `no binary for ${key} — run: npm install bladebro`
|
|
52
|
+
: "this pi build has no MCP registration API — update pi";
|
|
53
|
+
pi.on("session_start", (_event, ctx) => ctx.ui.notify(`Bladebro: ${why}`, "error"));
|
|
54
|
+
return;
|
|
446
55
|
}
|
|
447
56
|
|
|
448
|
-
pi.
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
`Bladebro: no binary for ${key}. Run: npm install bladebro`,
|
|
454
|
-
"error",
|
|
455
|
-
);
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
await ensureClient();
|
|
461
|
-
} catch (err: any) {
|
|
462
|
-
ctx.ui.notify(`Bladebro: failed to start: ${err.message}`, "error");
|
|
463
|
-
client = null;
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
let tools: any[];
|
|
468
|
-
try {
|
|
469
|
-
tools = await client!.listTools();
|
|
470
|
-
} catch (err: any) {
|
|
471
|
-
ctx.ui.notify(`Bladebro: tools/list failed: ${err.message}`, "error");
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
for (const tool of tools) {
|
|
476
|
-
const formatter = TOOL_FORMATTERS[tool.name];
|
|
477
|
-
|
|
478
|
-
pi.registerTool({
|
|
479
|
-
name: tool.name,
|
|
480
|
-
label: tool.name,
|
|
481
|
-
description: tool.description,
|
|
482
|
-
parameters: Type.Unsafe(tool.inputSchema),
|
|
483
|
-
renderShell: "default",
|
|
484
|
-
renderCall(args: any, theme: any, context: any) {
|
|
485
|
-
const text = new Text("", 0, 0);
|
|
486
|
-
if (formatter) {
|
|
487
|
-
text.setText(formatter(args, theme));
|
|
488
|
-
} else {
|
|
489
|
-
text.setText(theme.bold(tool.name));
|
|
490
|
-
}
|
|
491
|
-
return text;
|
|
492
|
-
},
|
|
493
|
-
renderResult(result: any, options: any, theme: any, context: any) {
|
|
494
|
-
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
495
|
-
const body = getResultText(result);
|
|
496
|
-
text.setText(formatResult(body, options.expanded, result.isError, theme));
|
|
497
|
-
return text;
|
|
498
|
-
},
|
|
499
|
-
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
500
|
-
const c = await ensureClient();
|
|
501
|
-
if (_signal?.aborted) throw new Error("Bladebro tool call cancelled before dispatch");
|
|
502
|
-
const result = await c.callTool(tool.name, params);
|
|
503
|
-
return {
|
|
504
|
-
content: result.content || [],
|
|
505
|
-
details: {},
|
|
506
|
-
isError: result.isError || false,
|
|
507
|
-
};
|
|
508
|
-
},
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
ctx.ui.notify(`Bladebro ready (${tools.length} tools)`, "info");
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
pi.on("session_shutdown", async () => {
|
|
516
|
-
if (client) {
|
|
517
|
-
await client.stop();
|
|
518
|
-
client = null;
|
|
519
|
-
}
|
|
57
|
+
pi.registerMcpServer("bladebro", {
|
|
58
|
+
command: binary,
|
|
59
|
+
args: ["mcp"],
|
|
60
|
+
exposure: "direct",
|
|
61
|
+
timeout: 180,
|
|
520
62
|
});
|
|
521
63
|
}
|
package/skills/bladebro/SKILL.md
CHANGED
|
@@ -1,45 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bladebro
|
|
3
|
-
description:
|
|
3
|
+
description: Optional house rules for bladebro. Bladebro itself needs no skill — the package registers its MCP server with pi and the five tools arrive complete. Add your own preferences here if you want the agent to use bladebro in a particular way.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# No skill added by the user.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Bladebro works without this file: the npm package registers the bladebro MCP server with pi, and the tools come with their full definitions — descriptions, schemas, and the driver's own `initialize` instructions. Nothing needs repeating here.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
This file exists so you can tell your agent to use bladebro a specific way — sites to prefer, extraction shapes, pacing, things to avoid. Whatever you write below is read as guidance.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
2. Address by text when you can: `act { "action": "click", "text": "Sign in" }` — no `see` needed. Labels work for form fields.
|
|
14
|
-
3. Refs are page-scoped and short-lived: after a navigation or tab switch use the fresh refs from THAT response. Within a page they self-heal.
|
|
15
|
-
4. Every action returns an observation (verdict + what changed). A `see` after acting is usually unnecessary — read the observation.
|
|
16
|
-
5. Collapse round-trips: `fill` for whole forms, `batch`/`run` for sequences (branching, loops, inline reads).
|
|
17
|
-
|
|
18
|
-
## Reading
|
|
19
|
-
|
|
20
|
-
- List, search, product, profile pages: `see { "extract": "auto" }` FIRST — one call returns structured items.
|
|
21
|
-
- Articles and docs: `see { "mode": "content" }` (clean markdown). Cheapest "what's here": `mode: "outline"`.
|
|
22
|
-
- One value or a custom shape: `act { "action": "eval", "js": "…" }`.
|
|
23
|
-
- Machine-readable output: add `format: "json"` — the result is pure JSON. `act eval` in text mode is one line, `result: <compact JSON>`; if the script returned a string containing JSON, a note says so (parse the value once more).
|
|
24
|
-
|
|
25
|
-
## Trust what the result tells you
|
|
26
|
-
|
|
27
|
-
- `page_total` with `partial`/`paged` in an extract: the page claims more results than the DOM holds — follow the pagination, or `act { "action": "collect", "url": "…" }` to scroll and dedupe the whole feed.
|
|
28
|
-
- `sponsored: true` items are paid placements — flagged per item and counted in the envelope, left in page order.
|
|
29
|
-
- A note that the URL's range filter does not match the page's own prices means the site ignored its filter — treat those results as unfiltered.
|
|
30
|
-
- `duplicates_removed` / `redirects_unwrapped` explain what was cleaned up; item URLs are canonical, not tracking interstitials.
|
|
31
|
-
- Oversized payloads are written to an artifact file and read back with `see { "artifact": "…" }` (paged). Nothing is truncated silently — a `… (+N chars truncated)` marker always says so.
|
|
32
|
-
|
|
33
|
-
## Forms, sequences, dialogs
|
|
34
|
-
|
|
35
|
-
- Whole forms in ONE call: `act { "action": "fill", "fields": [ { "label": "Email", "text": "…" } ], "submit": "Sign in" }`.
|
|
36
|
-
- Multi-step or branching work: `run { "steps": [ … ] }` — `if`/`while`, inline `see`/`extract` steps, one round-trip.
|
|
37
|
-
- A click that dispatches but changes nothing reads "clicked (no observable DOM change)" — it fired ONCE; verify state before re-clicking. "no-effect" means nothing was dispatched, so a retry is safe there.
|
|
38
|
-
- Dialogs (confirm/prompt/alert): arm `act { "action": "dialog", "expect": "confirm" }` BEFORE the action that triggers it.
|
|
39
|
-
|
|
40
|
-
## When something goes wrong
|
|
41
|
-
|
|
42
|
-
- Errors carry the current page state — read it instead of guessing, and never blind-retry a submit.
|
|
43
|
-
- `trusted:false` = text landed through an untrusted fallback; treat it as not typed. `degraded:true` = the browser is unhealthy (retry or restart it) — not a page problem.
|
|
44
|
-
- An unknown action name answers with the valid list and the correct step.
|
|
45
|
-
- CAPTCHA / bot walls: the verdict says `blocked:` and the remediation ladder is in the ambient notes. Do not try to solve image grids.
|
|
12
|
+
<!-- Add your rules below this line. -->
|