bladebro 3.0.19 → 3.0.21
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 +5 -5
- package/pi-extension.ts +215 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bladebro",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.21",
|
|
4
4
|
"description": "Stealthy and efficient agentic browser driver for AI agents. Few tools, full control, real stealth, max token efficiency.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"LICENSE"
|
|
45
45
|
],
|
|
46
46
|
"optionalDependencies": {
|
|
47
|
-
"bladebro-linux-x64": "3.0.
|
|
48
|
-
"bladebro-darwin-x64": "3.0.
|
|
49
|
-
"bladebro-darwin-arm64": "3.0.
|
|
50
|
-
"bladebro-windows-x64": "3.0.
|
|
47
|
+
"bladebro-linux-x64": "3.0.21",
|
|
48
|
+
"bladebro-darwin-x64": "3.0.21",
|
|
49
|
+
"bladebro-darwin-arm64": "3.0.21",
|
|
50
|
+
"bladebro-windows-x64": "3.0.21"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": ">=14"
|
package/pi-extension.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
26
26
|
import { Type } from "typebox";
|
|
27
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
27
28
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
28
29
|
import { existsSync } from "node:fs";
|
|
29
30
|
import { dirname, join } from "node:path";
|
|
@@ -53,6 +54,185 @@ function resolveBinary(): string | null {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
// ── TUI rendering helpers ─────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/** Extract text from MCP tool result content blocks. */
|
|
60
|
+
function getResultText(result: any): string {
|
|
61
|
+
if (!result?.content) return "";
|
|
62
|
+
return result.content
|
|
63
|
+
.filter((c: any) => c.type === "text")
|
|
64
|
+
.map((c: any) => c.text)
|
|
65
|
+
.join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Format the `act` tool call display. */
|
|
69
|
+
function formatActCall(args: any, theme: any): string {
|
|
70
|
+
const action = args.action || "?";
|
|
71
|
+
const bold = (s: string) => theme.bold(s);
|
|
72
|
+
const accent = (s: string) => theme.fg("accent", s);
|
|
73
|
+
const muted = (s: string) => theme.fg("muted", s);
|
|
74
|
+
const title = bold("act");
|
|
75
|
+
|
|
76
|
+
switch (action) {
|
|
77
|
+
case "navigate": {
|
|
78
|
+
const url = args.url || "";
|
|
79
|
+
return `${title} ${accent("navigate")} ${muted("→")} ${accent(url)}`;
|
|
80
|
+
}
|
|
81
|
+
case "click": {
|
|
82
|
+
const target = args.ref || args.text || args.label || "";
|
|
83
|
+
return `${title} ${accent("click")} ${muted(target)}`;
|
|
84
|
+
}
|
|
85
|
+
case "type": {
|
|
86
|
+
const target = args.ref || args.label || "";
|
|
87
|
+
const text = args.text || "";
|
|
88
|
+
const press = args.press ? ` ${muted("+ " + args.press)}` : "";
|
|
89
|
+
return `${title} ${accent("type")} ${muted(target ? `${target} ` : "")}${muted(`"${text}"`)}${press}`;
|
|
90
|
+
}
|
|
91
|
+
case "scroll": {
|
|
92
|
+
const dy = args.dy || 0;
|
|
93
|
+
const dx = args.dx || 0;
|
|
94
|
+
const arrow = dy > 0 ? "↓" : dy < 0 ? "↑" : dx > 0 ? "→" : "←";
|
|
95
|
+
const mag = Math.abs(dy || dx);
|
|
96
|
+
return `${title} ${accent("scroll")} ${muted(`${arrow}${mag}`)}`;
|
|
97
|
+
}
|
|
98
|
+
case "fill": {
|
|
99
|
+
const n = args.fields?.length || 0;
|
|
100
|
+
const submit = args.submit ? ` ${muted("+ submit")}` : "";
|
|
101
|
+
return `${title} ${accent("fill")} ${muted(`(${n} field${n !== 1 ? "s" : ""})`)}${submit}`;
|
|
102
|
+
}
|
|
103
|
+
case "batch": {
|
|
104
|
+
const n = args.steps?.length || 0;
|
|
105
|
+
return `${title} ${accent("batch")} ${muted(`(${n} step${n !== 1 ? "s" : ""})`)}`;
|
|
106
|
+
}
|
|
107
|
+
case "hover": {
|
|
108
|
+
const target = args.ref || args.text || args.label || "";
|
|
109
|
+
return `${title} ${accent("hover")} ${muted(target)}`;
|
|
110
|
+
}
|
|
111
|
+
case "press": {
|
|
112
|
+
return `${title} ${accent("press")} ${muted(args.key || "")}`;
|
|
113
|
+
}
|
|
114
|
+
case "eval": {
|
|
115
|
+
const js = (args.js || "").slice(0, 50);
|
|
116
|
+
return `${title} ${accent("eval")} ${muted(`"${js}${js.length >= 50 ? "…" : ""}"`)}`;
|
|
117
|
+
}
|
|
118
|
+
case "wait": {
|
|
119
|
+
const cond = args.condition || "settle";
|
|
120
|
+
const t = args.timeout || 10;
|
|
121
|
+
return `${title} ${accent("wait")} ${muted(`${cond} (${t}s)`)}`;
|
|
122
|
+
}
|
|
123
|
+
case "read": {
|
|
124
|
+
return `${title} ${accent("read")} ${muted(args.ref || "")}`;
|
|
125
|
+
}
|
|
126
|
+
case "back": return `${title} ${accent("back")}`;
|
|
127
|
+
case "forward": return `${title} ${accent("forward")}`;
|
|
128
|
+
case "reload": return `${title} ${accent("reload")}`;
|
|
129
|
+
case "select": {
|
|
130
|
+
const target = args.ref || "";
|
|
131
|
+
const opt = args.option || "";
|
|
132
|
+
return `${title} ${accent("select")} ${muted(`${target} "${opt}"`)}`;
|
|
133
|
+
}
|
|
134
|
+
case "pdf": return `${title} ${accent("pdf")}`;
|
|
135
|
+
case "download": return `${title} ${accent("download")}`;
|
|
136
|
+
case "collect": return `${title} ${accent("collect")}`;
|
|
137
|
+
case "clear": return `${title} ${accent("clear")} ${muted(args.ref || "")}`;
|
|
138
|
+
case "upload": return `${title} ${accent("upload")} ${muted(args.ref || "")}`;
|
|
139
|
+
default: return `${title} ${accent(action)}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Format the `see` tool call display. */
|
|
144
|
+
function formatSeeCall(args: any, theme: any): string {
|
|
145
|
+
const bold = (s: string) => theme.bold(s);
|
|
146
|
+
const accent = (s: string) => theme.fg("accent", s);
|
|
147
|
+
const muted = (s: string) => theme.fg("muted", s);
|
|
148
|
+
const title = bold("see");
|
|
149
|
+
|
|
150
|
+
const mode = args.mode;
|
|
151
|
+
if (mode === "content") return `${title} ${accent("content")}`;
|
|
152
|
+
if (mode === "outline") return `${title} ${accent("outline")}`;
|
|
153
|
+
if (mode === "model") return `${title} ${accent("model")}`;
|
|
154
|
+
|
|
155
|
+
const find = args.find;
|
|
156
|
+
if (find) return `${title} ${accent("find")} ${muted(`"${find}"`)}`;
|
|
157
|
+
|
|
158
|
+
const extract = args.extract;
|
|
159
|
+
if (extract) return `${title} ${accent(`extract=${extract}`)}`;
|
|
160
|
+
|
|
161
|
+
const scope = args.scope;
|
|
162
|
+
if (scope) return `${title} ${accent("scope")} ${muted(scope)}`;
|
|
163
|
+
|
|
164
|
+
const filter = args.filter;
|
|
165
|
+
if (filter) return `${title} ${accent("model")} ${muted(`[${filter}]`)}`;
|
|
166
|
+
|
|
167
|
+
const logs = args.logs;
|
|
168
|
+
if (logs) return `${title} ${accent("logs")} ${muted(logs)}`;
|
|
169
|
+
|
|
170
|
+
return `${title} ${accent("model")}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Format the `state` tool call display. */
|
|
174
|
+
function formatStateCall(args: any, theme: any): string {
|
|
175
|
+
const bold = (s: string) => theme.bold(s);
|
|
176
|
+
const accent = (s: string) => theme.fg("accent", s);
|
|
177
|
+
const muted = (s: string) => theme.fg("muted", s);
|
|
178
|
+
const title = bold("state");
|
|
179
|
+
|
|
180
|
+
const op = args.op || "?";
|
|
181
|
+
const extra = args.url || args.name || "";
|
|
182
|
+
return `${title} ${accent(op)}${extra ? ` ${muted(extra)}` : ""}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Format the `run` tool call display. */
|
|
186
|
+
function formatRunCall(args: any, theme: any): string {
|
|
187
|
+
const bold = (s: string) => theme.bold(s);
|
|
188
|
+
const title = bold("run");
|
|
189
|
+
return title;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Format the `vision` tool call display. */
|
|
193
|
+
function formatVisionCall(args: any, theme: any): string {
|
|
194
|
+
const bold = (s: string) => theme.bold(s);
|
|
195
|
+
const title = bold("vision");
|
|
196
|
+
return title;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Format tool result for display. */
|
|
200
|
+
function formatResult(text: string, expanded: boolean, isError: boolean, theme: any): string {
|
|
201
|
+
const out = (s: string) => theme.fg("toolOutput", s);
|
|
202
|
+
const warn = (s: string) => theme.fg("warning", s);
|
|
203
|
+
const muted = (s: string) => theme.fg("muted", s);
|
|
204
|
+
const accent = (s: string) => theme.fg("accent", s);
|
|
205
|
+
|
|
206
|
+
if (!text) return muted("(no output)");
|
|
207
|
+
|
|
208
|
+
const lines = text.split("\n");
|
|
209
|
+
|
|
210
|
+
if (isError) {
|
|
211
|
+
// Error: show all lines in warning color
|
|
212
|
+
return lines.map((l) => warn(l)).join("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Truncate if not expanded
|
|
216
|
+
const maxLines = expanded ? 9999 : 15;
|
|
217
|
+
const visible = lines.slice(0, maxLines);
|
|
218
|
+
const skipped = lines.length - visible.length;
|
|
219
|
+
|
|
220
|
+
const formatted = visible.map((line) => {
|
|
221
|
+
// Outcome lines → accent
|
|
222
|
+
if (line.startsWith("outcome:")) return accent(line);
|
|
223
|
+
// Warning lines (⚠) → warning color
|
|
224
|
+
if (line.includes("\u{26a0}")) return warn(line);
|
|
225
|
+
// Verdict lines with arrow → toolOutput
|
|
226
|
+
return out(line);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (skipped > 0) {
|
|
230
|
+
formatted.push(muted(` … ${skipped} more line${skipped !== 1 ? "s" : ""} (expand to view)`));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return formatted.join("\n");
|
|
234
|
+
}
|
|
235
|
+
|
|
56
236
|
// ── Minimal MCP stdio client ──────────────────────────────────────────
|
|
57
237
|
|
|
58
238
|
interface PendingReq {
|
|
@@ -92,11 +272,16 @@ class McpStdio {
|
|
|
92
272
|
|
|
93
273
|
this.proc.stdout!.on("data", (data: Buffer) => this.onData(data));
|
|
94
274
|
this.proc.stderr!.on("data", (data: Buffer) => {
|
|
95
|
-
|
|
96
|
-
|
|
275
|
+
const text = data.toString();
|
|
276
|
+
for (const line of text.split("\n")) {
|
|
277
|
+
const trimmed = line.trim();
|
|
278
|
+
if (!trimmed) continue;
|
|
279
|
+
if (/error|panic|fatal|warn|fail/i.test(trimmed)) {
|
|
280
|
+
process.stderr.write(line + "\n");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
97
283
|
});
|
|
98
284
|
|
|
99
|
-
// MCP initialize handshake, then signal ready.
|
|
100
285
|
this.request("initialize", {
|
|
101
286
|
protocolVersion: "2025-06-18",
|
|
102
287
|
capabilities: {},
|
|
@@ -167,8 +352,6 @@ class McpStdio {
|
|
|
167
352
|
this.alive = false;
|
|
168
353
|
try { this.proc.stdin?.end(); } catch {}
|
|
169
354
|
this.proc.kill("SIGTERM");
|
|
170
|
-
// Wait up to 5s for graceful exit (Chrome shutdown takes up to 3s),
|
|
171
|
-
// then SIGKILL. The orphan reaper handles any leaked Chrome.
|
|
172
355
|
await new Promise<void>((resolve) => {
|
|
173
356
|
const t = setTimeout(() => {
|
|
174
357
|
this.proc?.kill("SIGKILL");
|
|
@@ -182,12 +365,17 @@ class McpStdio {
|
|
|
182
365
|
|
|
183
366
|
// ── Extension ─────────────────────────────────────────────────────────
|
|
184
367
|
|
|
368
|
+
const TOOL_FORMATTERS: Record<string, (args: any, theme: any) => string> = {
|
|
369
|
+
act: formatActCall,
|
|
370
|
+
see: formatSeeCall,
|
|
371
|
+
state: formatStateCall,
|
|
372
|
+
run: formatRunCall,
|
|
373
|
+
vision: formatVisionCall,
|
|
374
|
+
};
|
|
375
|
+
|
|
185
376
|
export default function bladebroExtension(pi: ExtensionAPI) {
|
|
186
377
|
let client: McpStdio | null = null;
|
|
187
378
|
let binaryPath: string | null = null;
|
|
188
|
-
// Restart lock: prevents concurrent restarts from spawning
|
|
189
|
-
// multiple binaries. When two parallel tool calls both see
|
|
190
|
-
// isAlive() === false, they share the same restart promise.
|
|
191
379
|
let restartPromise: Promise<void> | null = null;
|
|
192
380
|
|
|
193
381
|
async function ensureClient(): Promise<McpStdio> {
|
|
@@ -230,7 +418,6 @@ export default function bladebroExtension(pi: ExtensionAPI) {
|
|
|
230
418
|
return;
|
|
231
419
|
}
|
|
232
420
|
|
|
233
|
-
// Discover tools from the binary and register them natively.
|
|
234
421
|
let tools: any[];
|
|
235
422
|
try {
|
|
236
423
|
tools = await client!.listTools();
|
|
@@ -240,14 +427,30 @@ export default function bladebroExtension(pi: ExtensionAPI) {
|
|
|
240
427
|
}
|
|
241
428
|
|
|
242
429
|
for (const tool of tools) {
|
|
430
|
+
const formatter = TOOL_FORMATTERS[tool.name];
|
|
431
|
+
|
|
243
432
|
pi.registerTool({
|
|
244
433
|
name: tool.name,
|
|
245
434
|
label: tool.name,
|
|
246
435
|
description: tool.description,
|
|
247
436
|
parameters: Type.Unsafe(tool.inputSchema),
|
|
437
|
+
renderShell: "default",
|
|
438
|
+
renderCall(args: any, theme: any, context: any) {
|
|
439
|
+
const text = new Text("", 0, 0);
|
|
440
|
+
if (formatter) {
|
|
441
|
+
text.setText(formatter(args, theme));
|
|
442
|
+
} else {
|
|
443
|
+
text.setText(theme.bold(tool.name));
|
|
444
|
+
}
|
|
445
|
+
return text;
|
|
446
|
+
},
|
|
447
|
+
renderResult(result: any, options: any, theme: any, context: any) {
|
|
448
|
+
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
449
|
+
const body = getResultText(result);
|
|
450
|
+
text.setText(formatResult(body, options.expanded, result.isError, theme));
|
|
451
|
+
return text;
|
|
452
|
+
},
|
|
248
453
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
249
|
-
// Ensure the binary is alive — restart if it died.
|
|
250
|
-
// ensureClient handles the restart race condition.
|
|
251
454
|
const c = await ensureClient();
|
|
252
455
|
const result = await c.callTool(tool.name, params);
|
|
253
456
|
return {
|
|
@@ -259,7 +462,7 @@ export default function bladebroExtension(pi: ExtensionAPI) {
|
|
|
259
462
|
});
|
|
260
463
|
}
|
|
261
464
|
|
|
262
|
-
ctx.ui.notify(`Bladebro
|
|
465
|
+
ctx.ui.notify(`Bladebro ready (${tools.length} tools)`, "info");
|
|
263
466
|
});
|
|
264
467
|
|
|
265
468
|
pi.on("session_shutdown", async () => {
|