bladebro 4.0.3 → 4.1.1

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 CHANGED
@@ -55,6 +55,7 @@ bladebro help --json
55
55
 
56
56
  - Node.js >= 14
57
57
  - Chrome/Chromium installed (Bladebro finds it automatically)
58
+ - Linux prebuilt binaries: glibc >= 2.28
58
59
 
59
60
  ## Platforms
60
61
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bladebro",
3
- "version": "4.0.3",
3
+ "version": "4.1.1",
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": {
@@ -48,11 +48,11 @@
48
48
  "LICENSE"
49
49
  ],
50
50
  "optionalDependencies": {
51
- "bladebro-linux-x64": "4.0.3",
52
- "bladebro-linux-arm64": "4.0.3",
53
- "bladebro-darwin-x64": "4.0.3",
54
- "bladebro-darwin-arm64": "4.0.3",
55
- "bladebro-windows-x64": "4.0.3"
51
+ "bladebro-linux-x64": "4.1.1",
52
+ "bladebro-linux-arm64": "4.1.1",
53
+ "bladebro-darwin-x64": "4.1.1",
54
+ "bladebro-darwin-arm64": "4.1.1",
55
+ "bladebro-windows-x64": "4.1.1"
56
56
  },
57
57
  "engines": {
58
58
  "node": ">=14"
package/pi-extension.ts CHANGED
@@ -25,6 +25,7 @@
25
25
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
26
26
  import { Type } from "typebox";
27
27
  import { Text } from "@earendil-works/pi-tui";
28
+ import { StringDecoder } from "node:string_decoder";
28
29
  import { spawn, type ChildProcess } from "node:child_process";
29
30
  import { existsSync } from "node:fs";
30
31
  import { dirname, join } from "node:path";
@@ -247,16 +248,21 @@ class McpStdio {
247
248
  private pending = new Map<number, PendingReq>();
248
249
  private buffer = "";
249
250
  private alive = false;
251
+ private decoder = new StringDecoder("utf8");
250
252
 
251
253
  start(binaryPath: string): Promise<void> {
252
254
  return new Promise((resolve, reject) => {
253
- this.proc = spawn(binaryPath, ["mcp"], {
255
+ this.buffer = "";
256
+ this.decoder = new StringDecoder("utf8");
257
+ const proc = this.proc = spawn(binaryPath, ["mcp"], {
254
258
  stdio: ["pipe", "pipe", "pipe"],
255
259
  env: { ...process.env },
256
260
  });
257
261
 
258
262
  const onExit = () => {
263
+ if (this.proc !== proc) return;
259
264
  this.alive = false;
265
+ this.proc = null;
260
266
  for (const [, p] of this.pending) {
261
267
  clearTimeout(p.timer);
262
268
  p.reject(new Error("bladebro process exited"));
@@ -264,7 +270,12 @@ class McpStdio {
264
270
  this.pending.clear();
265
271
  };
266
272
 
267
- this.proc.on("exit", onExit);
273
+ proc.on("exit", onExit);
274
+ proc.stdin!.on("error", (err) => {
275
+ onExit();
276
+ proc.kill();
277
+ reject(err);
278
+ });
268
279
  this.proc.on("error", (err) => {
269
280
  onExit();
270
281
  reject(err);
@@ -292,12 +303,25 @@ class McpStdio {
292
303
  this.alive = true;
293
304
  resolve();
294
305
  })
295
- .catch(reject);
306
+ .catch((err) => {
307
+ onExit();
308
+ proc.kill();
309
+ reject(err);
310
+ });
296
311
  });
297
312
  }
298
313
 
299
314
  private onData(data: Buffer): void {
300
- this.buffer += data.toString();
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
+ }
301
325
  let idx: number;
302
326
  while ((idx = this.buffer.indexOf("\n")) >= 0) {
303
327
  const line = this.buffer.slice(0, idx).trim();
@@ -308,18 +332,35 @@ class McpStdio {
308
332
 
309
333
  private onMessage(line: string): void {
310
334
  let msg: any;
311
- try { msg = JSON.parse(line); } catch { return; }
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
+ }
312
348
  if (msg.id !== undefined && this.pending.has(msg.id)) {
313
349
  const entry = this.pending.get(msg.id)!;
314
350
  this.pending.delete(msg.id);
315
351
  clearTimeout(entry.timer);
316
- if (msg.error) entry.reject(msg.error);
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"));
317
354
  else entry.resolve(msg.result);
318
355
  }
319
356
  }
320
357
 
321
358
  private request(method: string, params: any, timeoutMs = 180000): Promise<any> {
322
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
+ }
323
364
  const id = this.nextId++;
324
365
  const timer = setTimeout(() => {
325
366
  this.pending.delete(id);
@@ -348,18 +389,23 @@ class McpStdio {
348
389
  isAlive(): boolean { return this.alive && this.proc !== null; }
349
390
 
350
391
  async stop(): Promise<void> {
351
- if (!this.proc) return;
392
+ const proc = this.proc;
393
+ if (!proc) return;
352
394
  this.alive = false;
353
- try { this.proc.stdin?.end(); } catch {}
354
- this.proc.kill("SIGTERM");
355
- await new Promise<void>((resolve) => {
356
- const t = setTimeout(() => {
357
- this.proc?.kill("SIGKILL");
358
- resolve();
359
- }, 5000);
360
- this.proc?.on("exit", () => { clearTimeout(t); resolve(); });
361
- });
362
- this.proc = null;
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;
363
409
  }
364
410
  }
365
411
 
@@ -452,6 +498,7 @@ export default function bladebroExtension(pi: ExtensionAPI) {
452
498
  },
453
499
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
454
500
  const c = await ensureClient();
501
+ if (_signal?.aborted) throw new Error("Bladebro tool call cancelled before dispatch");
455
502
  const result = await c.callTool(tool.name, params);
456
503
  return {
457
504
  content: result.content || [],