donsetch 4.1.1 → 4.2.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/pi-extension.ts CHANGED
@@ -27,10 +27,37 @@ import { join } from "node:path";
27
27
  import { Text } from "@earendil-works/pi-tui";
28
28
 
29
29
  // ── Constants ──
30
- const INIT_TIMEOUT_MS = 10_000;
30
+ const INIT_TIMEOUT_MS = 30_000;
31
31
  const CALL_TIMEOUT_MS = 120_000;
32
32
  const SHUTDOWN_GRACE_MS = 2_000;
33
33
 
34
+ // The client timeout must outlive the server's OWN budget, or a call the
35
+ // binary still considers legal gets killed here first. web_crawl accepts
36
+ // deadline_s up to 600 and web_fetch deadline_ms up to 600000, so a fixed
37
+ // 120s client cap cut legal crawls off at exactly the binary's default
38
+ // deadline (deadline_s defaults to 120 : the two raced). Derive it from
39
+ // the call's own budget plus slack instead, capped at the largest budget
40
+ // the schemas allow so a hostile arg cannot hold a slot forever.
41
+ const CALL_DEADLINE_SLACK_MS = 20_000;
42
+ const MAX_CALL_TIMEOUT_MS = 620_000; // 600s budget + slack
43
+
44
+ function num(v: unknown): number | null {
45
+ const n = typeof v === "string" ? Number(v) : typeof v === "number" ? v : NaN;
46
+ return Number.isFinite(n) && n > 0 ? n : null;
47
+ }
48
+
49
+ function callTimeoutFor(name: string, args: any): number {
50
+ let budgetMs: number | null = null;
51
+ if (name === "web_crawl") {
52
+ const secs = num(args?.deadline_s);
53
+ budgetMs = secs !== null ? secs * 1000 : 120_000; // the binary's default
54
+ } else if (name === "web_fetch") {
55
+ budgetMs = num(args?.deadline_ms);
56
+ }
57
+ if (budgetMs === null) return CALL_TIMEOUT_MS;
58
+ return Math.min(budgetMs + CALL_DEADLINE_SLACK_MS, MAX_CALL_TIMEOUT_MS);
59
+ }
60
+
34
61
  // A raw process.stderr.write bypasses pi's TUI paint cycle and
35
62
  // corrupts the viewport (worse across parallel agents). Gate it here.
36
63
  const DEBUG_MODE =
@@ -56,6 +83,7 @@ const ICONS: Record<string, string> = {
56
83
  web_fetch: "\u{1F310}", // 🌐
57
84
  web_search: "\u{1F50E}", // 🔎
58
85
  web_crawl: "\u{1F577}\u{FE0F}", // 🕷️
86
+ web_screenshot: "\u{1F4F7}", // 📷
59
87
  };
60
88
 
61
89
  // ── MCP client state ──
@@ -89,10 +117,10 @@ function ensureBinary(): string {
89
117
  }
90
118
 
91
119
  try {
92
- execFileSync("node", [installScript], {
120
+ execFileSync(process.execPath, [installScript], {
93
121
  stdio: "inherit",
94
122
  cwd: __dirname,
95
- timeout: 60_000,
123
+ timeout: 300_000,
96
124
  });
97
125
  } catch (err: any) {
98
126
  throw new Error(`Failed to download donsetch binary: ${err.message}`);
@@ -312,8 +340,13 @@ function sendNotification(method: string, params: any): void {
312
340
  }
313
341
  }
314
342
 
315
- async function callMcpTool(name: string, args: any, signal?: AbortSignal): Promise<any> {
316
- return sendRequest("tools/call", { name, arguments: args ?? {} }, CALL_TIMEOUT_MS, signal);
343
+ async function callMcpTool(
344
+ name: string,
345
+ args: any,
346
+ signal?: AbortSignal,
347
+ timeoutMs = CALL_TIMEOUT_MS
348
+ ): Promise<any> {
349
+ return sendRequest("tools/call", { name, arguments: args ?? {} }, timeoutMs, signal);
317
350
  }
318
351
 
319
352
  function killServer(): void {
@@ -433,7 +466,11 @@ export default function (pi: ExtensionAPI) {
433
466
  try {
434
467
  await startServer();
435
468
  } catch (err: any) {
436
- process.stderr.write(`[donsetch] failed to start MCP server: ${err.message}\n`);
469
+ const message = String(err.message || err);
470
+ const hint = message.includes("initialize") && message.includes("timeout")
471
+ ? " Run `donsetch doctor` to diagnose the installation and runtime."
472
+ : "";
473
+ process.stderr.write(`[donsetch] failed to start MCP server: ${message}.${hint}\n`);
437
474
  return;
438
475
  }
439
476
 
@@ -473,13 +510,21 @@ export default function (pi: ExtensionAPI) {
473
510
  } catch (err: any) {
474
511
  return {
475
512
  content: [{ type: "text", text: `donsetch MCP server crashed and could not restart: ${err.message}` }],
513
+ // Every other result path carries `details`; pi's
514
+ // AgentToolResult requires it, and renderResult reads it.
515
+ details: { mcpTool: toolName, isError: true, error: err.message },
476
516
  isError: true,
477
517
  };
478
518
  }
479
519
  }
480
520
 
481
521
  try {
482
- const result = await callMcpTool(toolName, params, _signal);
522
+ const result = await callMcpTool(
523
+ toolName,
524
+ params,
525
+ _signal,
526
+ callTimeoutFor(toolName, params)
527
+ );
483
528
  // Join all content text blocks, skipping [meta] blocks.
484
529
  // [meta] blocks contain compact metadata for clients
485
530
  // (Claude Code, VSCode) that drop text when