donsetch 2.5.0 → 3.1.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/pi-extension.ts +68 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "donsetch",
3
- "version": "2.5.0",
3
+ "version": "3.1.0",
4
4
  "description": "Web fetch, search and crawl for AI agents. Zero API keys. Chrome-true TLS.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Bishesh Bhandari",
package/pi-extension.ts CHANGED
@@ -118,7 +118,11 @@ function startServer(): Promise<void> {
118
118
  }
119
119
 
120
120
  try {
121
- proc = spawn(binaryPath, ["mcp"], {
121
+ // --supervised: crash-only daemon. If the MCP server is SIGKILLed
122
+ // (OOM, crash), the supervisor respawns it and replays in-flight
123
+ // requests — pi users never see a dead tool until the process
124
+ // itself dies.
125
+ proc = spawn(binaryPath, ["mcp", "--supervised"], {
122
126
  stdio: ["pipe", "pipe", "pipe"],
123
127
  env: { ...process.env },
124
128
  windowsHide: true,
@@ -196,20 +200,58 @@ function startServer(): Promise<void> {
196
200
  });
197
201
  }
198
202
 
199
- function sendRequest(method: string, params: any, timeoutMs = CALL_TIMEOUT_MS): Promise<any> {
203
+ function sendRequest(
204
+ method: string,
205
+ params: any,
206
+ timeoutMs = CALL_TIMEOUT_MS,
207
+ signal?: AbortSignal
208
+ ): Promise<any> {
200
209
  return new Promise((resolve, reject) => {
201
210
  if (!proc?.stdin?.writable) {
202
211
  reject(new Error("donsetch MCP server not running"));
203
212
  return;
204
213
  }
205
214
  const id = nextId++;
215
+ let settled = false;
216
+ const finish = (fn: (v: any) => void, v: any) => {
217
+ if (settled) return;
218
+ settled = true;
219
+ signal?.removeEventListener("abort", onAbort);
220
+ fn(v);
221
+ };
206
222
  const timer = setTimeout(() => {
207
- if (pending.has(id)) {
208
- pending.delete(id);
209
- reject(new Error(`MCP request timeout (${timeoutMs}ms): ${method}`));
223
+ if (pending.delete(id)) {
224
+ finish(reject, new Error(`MCP request timeout (${timeoutMs}ms): ${method}`));
210
225
  }
211
226
  }, timeoutMs);
212
- pending.set(id, { resolve, reject, timer });
227
+ // v3 real MCP cancellation: forward pi's abort to the server so the
228
+ // in-flight fetch/crawl actually stops server-side, then settle
229
+ // locally. The caller maps this to a graceful "Cancelled" result.
230
+ const onAbort = () => {
231
+ if (pending.delete(id)) {
232
+ clearTimeout(timer);
233
+ sendNotification("notifications/cancelled", { id, reason: "client aborted" });
234
+ finish(reject, new Error("cancelled"));
235
+ }
236
+ };
237
+ if (signal) {
238
+ if (signal.aborted) {
239
+ onAbort();
240
+ return;
241
+ }
242
+ signal.addEventListener("abort", onAbort, { once: true });
243
+ }
244
+ pending.set(id, {
245
+ resolve: (v: any) => {
246
+ clearTimeout(timer);
247
+ finish(resolve, v);
248
+ },
249
+ reject: (e: any) => {
250
+ clearTimeout(timer);
251
+ finish(reject, e);
252
+ },
253
+ timer,
254
+ });
213
255
  const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params });
214
256
  proc.stdin.write(msg + "\n");
215
257
  });
@@ -221,8 +263,8 @@ function sendNotification(method: string, params: any): void {
221
263
  proc.stdin.write(msg + "\n");
222
264
  }
223
265
 
224
- async function callMcpTool(name: string, args: any): Promise<any> {
225
- return sendRequest("tools/call", { name, arguments: args ?? {} });
266
+ async function callMcpTool(name: string, args: any, signal?: AbortSignal): Promise<any> {
267
+ return sendRequest("tools/call", { name, arguments: args ?? {} }, CALL_TIMEOUT_MS, signal);
226
268
  }
227
269
 
228
270
  function killServer(): void {
@@ -384,7 +426,7 @@ export default function (pi: ExtensionAPI) {
384
426
  }
385
427
 
386
428
  try {
387
- const result = await callMcpTool(toolName, params);
429
+ const result = await callMcpTool(toolName, params, _signal);
388
430
  // Join all content text blocks, skipping [meta] blocks.
389
431
  // [meta] blocks contain compact metadata for clients
390
432
  // (Claude Code, VSCode) that drop text when
@@ -411,13 +453,15 @@ export default function (pi: ExtensionAPI) {
411
453
  } else if (toolName === "web_fetch") {
412
454
  details.source = getFetchSource(sc);
413
455
  details.status = getFetchStatus(sc);
456
+ if (sc?.stitched) details.stitched = sc.stitched;
414
457
  } else if (toolName === "web_crawl") {
415
458
  details.pages = countCrawlPages(text);
416
459
  }
417
460
 
418
- // For errors, extract error text
461
+ // For errors, extract error text + v3 stable error code
419
462
  if (isErr) {
420
463
  details.error = getPreview(text, 60);
464
+ if (sc?.code) details.code = sc.code;
421
465
  } else {
422
466
  details.preview = getPreview(text);
423
467
  }
@@ -428,6 +472,15 @@ export default function (pi: ExtensionAPI) {
428
472
  isError: isErr,
429
473
  };
430
474
  } catch (err: any) {
475
+ // User pressed Esc in pi: we already told the server to stop
476
+ // (notifications/cancelled). Graceful non-error result, per
477
+ // pi's extension contract for aborted calls.
478
+ if (err.message === "cancelled") {
479
+ return {
480
+ content: [{ type: "text", text: "Cancelled" }],
481
+ details: { mcpTool: toolName, cancelled: true },
482
+ };
483
+ }
431
484
  return {
432
485
  content: [{ type: "text", text: `donsetch MCP call failed: ${err.message}` }],
433
486
  details: { mcpTool: toolName, isError: true, error: err.message },
@@ -469,6 +522,7 @@ export default function (pi: ExtensionAPI) {
469
522
  else if (d.source === "ghost") parts.push("via ghost");
470
523
  if (d.status === "blocked") parts.push("blocked");
471
524
  else if (d.status === "thin") parts.push("thin");
525
+ if (d.stitched) parts.push(`stitched \u00D7${d.stitched}`);
472
526
  meta = parts.join(" \u00B7 ");
473
527
  } else if (toolName === "web_search") {
474
528
  const count = d.results ?? 0;
@@ -481,8 +535,10 @@ export default function (pi: ExtensionAPI) {
481
535
 
482
536
  // Build line 2: preview or error
483
537
  let line2 = "";
484
- if (isErr) {
485
- line2 = d.error || "failed";
538
+ if (d.cancelled) {
539
+ line2 = "cancelled";
540
+ } else if (isErr) {
541
+ line2 = d.code ? `[${d.code}] ${d.error || "failed"}` : d.error || "failed";
486
542
  } else if (toolName === "web_search" && d.topResult) {
487
543
  line2 = truncate(d.topResult, 70);
488
544
  } else if (d.preview) {