bladebro 3.0.20 → 3.0.22
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 +207 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bladebro",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.22",
|
|
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.22",
|
|
48
|
+
"bladebro-darwin-x64": "3.0.22",
|
|
49
|
+
"bladebro-darwin-arm64": "3.0.22",
|
|
50
|
+
"bladebro-windows-x64": "3.0.22"
|
|
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,9 +272,6 @@ 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
|
-
// Forward only errors/warnings to pi stderr. Info lines like
|
|
96
|
-
// "[bladebro] MCP server ready" are suppressed to keep the
|
|
97
|
-
// agent TUI clean on startup.
|
|
98
275
|
const text = data.toString();
|
|
99
276
|
for (const line of text.split("\n")) {
|
|
100
277
|
const trimmed = line.trim();
|
|
@@ -105,7 +282,6 @@ class McpStdio {
|
|
|
105
282
|
}
|
|
106
283
|
});
|
|
107
284
|
|
|
108
|
-
// MCP initialize handshake, then signal ready.
|
|
109
285
|
this.request("initialize", {
|
|
110
286
|
protocolVersion: "2025-06-18",
|
|
111
287
|
capabilities: {},
|
|
@@ -176,8 +352,6 @@ class McpStdio {
|
|
|
176
352
|
this.alive = false;
|
|
177
353
|
try { this.proc.stdin?.end(); } catch {}
|
|
178
354
|
this.proc.kill("SIGTERM");
|
|
179
|
-
// Wait up to 5s for graceful exit (Chrome shutdown takes up to 3s),
|
|
180
|
-
// then SIGKILL. The orphan reaper handles any leaked Chrome.
|
|
181
355
|
await new Promise<void>((resolve) => {
|
|
182
356
|
const t = setTimeout(() => {
|
|
183
357
|
this.proc?.kill("SIGKILL");
|
|
@@ -191,12 +365,17 @@ class McpStdio {
|
|
|
191
365
|
|
|
192
366
|
// ── Extension ─────────────────────────────────────────────────────────
|
|
193
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
|
+
|
|
194
376
|
export default function bladebroExtension(pi: ExtensionAPI) {
|
|
195
377
|
let client: McpStdio | null = null;
|
|
196
378
|
let binaryPath: string | null = null;
|
|
197
|
-
// Restart lock: prevents concurrent restarts from spawning
|
|
198
|
-
// multiple binaries. When two parallel tool calls both see
|
|
199
|
-
// isAlive() === false, they share the same restart promise.
|
|
200
379
|
let restartPromise: Promise<void> | null = null;
|
|
201
380
|
|
|
202
381
|
async function ensureClient(): Promise<McpStdio> {
|
|
@@ -239,7 +418,6 @@ export default function bladebroExtension(pi: ExtensionAPI) {
|
|
|
239
418
|
return;
|
|
240
419
|
}
|
|
241
420
|
|
|
242
|
-
// Discover tools from the binary and register them natively.
|
|
243
421
|
let tools: any[];
|
|
244
422
|
try {
|
|
245
423
|
tools = await client!.listTools();
|
|
@@ -249,14 +427,30 @@ export default function bladebroExtension(pi: ExtensionAPI) {
|
|
|
249
427
|
}
|
|
250
428
|
|
|
251
429
|
for (const tool of tools) {
|
|
430
|
+
const formatter = TOOL_FORMATTERS[tool.name];
|
|
431
|
+
|
|
252
432
|
pi.registerTool({
|
|
253
433
|
name: tool.name,
|
|
254
434
|
label: tool.name,
|
|
255
435
|
description: tool.description,
|
|
256
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
|
+
},
|
|
257
453
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
258
|
-
// Ensure the binary is alive — restart if it died.
|
|
259
|
-
// ensureClient handles the restart race condition.
|
|
260
454
|
const c = await ensureClient();
|
|
261
455
|
const result = await c.callTool(tool.name, params);
|
|
262
456
|
return {
|
|
@@ -268,7 +462,7 @@ export default function bladebroExtension(pi: ExtensionAPI) {
|
|
|
268
462
|
});
|
|
269
463
|
}
|
|
270
464
|
|
|
271
|
-
ctx.ui.notify(`Bladebro
|
|
465
|
+
ctx.ui.notify(`Bladebro ready (${tools.length} tools)`, "info");
|
|
272
466
|
});
|
|
273
467
|
|
|
274
468
|
pi.on("session_shutdown", async () => {
|