donsetch 2.4.1 → 3.0.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/bin/donsetch.js CHANGED
@@ -57,7 +57,10 @@ child.on('exit', (code, signal) => {
57
57
  if (signal) {
58
58
  // Re-raise the signal so the parent's exit reflects it
59
59
  try { process.kill(process.pid, signal); } catch (_) {}
60
- process.exit(128 + 15); // 128 + SIGTERM
60
+ // Fallback: exit code for the signal we actually received
61
+ // (SIGINT=2, SIGTERM=15, SIGHUP=1), not always SIGTERM's 143.
62
+ const sigNum = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGTERM: 15 }[signal] || 15;
63
+ process.exit(128 + sigNum);
61
64
  } else {
62
65
  process.exit(code || 0);
63
66
  }
package/install.js CHANGED
@@ -10,9 +10,10 @@
10
10
  // uses the same release artifacts that manual users download.
11
11
  //
12
12
  // Supported platforms:
13
- // linux-x64 Linux x86_64
14
- // linux-arm64 Linux ARM64 (aarch64)
13
+ // linux-x64 Linux x86_64 (glibc)
14
+ // linux-arm64 Linux ARM64 (glibc)
15
15
  // darwin-arm64 macOS Apple Silicon
16
+ // darwin-x64 macOS Intel
16
17
  // win32-x64 Windows x86_64
17
18
 
18
19
  const https = require('https');
@@ -30,6 +31,7 @@ const PLATFORMS = {
30
31
  'linux-x64': { asset: 'donsetch-linux-x64.tar.gz', binary: 'donsetch' },
31
32
  'linux-arm64': { asset: 'donsetch-linux-arm64.tar.gz', binary: 'donsetch' },
32
33
  'darwin-arm64': { asset: 'donsetch-darwin-arm64.tar.gz', binary: 'donsetch' },
34
+ 'darwin-x64': { asset: 'donsetch-darwin-x64.tar.gz', binary: 'donsetch' },
33
35
  'win32-x64': { asset: 'donsetch-win32-x64.tar.gz', binary: 'donsetch.exe' },
34
36
  };
35
37
 
@@ -37,25 +39,56 @@ const platKey = `${process.platform}-${process.arch}`;
37
39
  const plat = PLATFORMS[platKey];
38
40
 
39
41
  if (!plat) {
40
- console.error(`donsetch: unsupported platform ${platKey}`);
42
+ const known = {
43
+ 'darwin-x64': 'prebuilt binaries exist — update donsetch to a version that ships one',
44
+ 'win32-arm64': 'no prebuilt binary yet — build from source (see below)',
45
+ }[platKey];
46
+ console.error(`donsetch: unsupported platform ${platKey}${known ? ` (${known})` : ''}`);
41
47
  console.error('');
42
48
  console.error('Supported platforms:');
43
- console.error(' linux-x64 Linux x86_64');
44
- console.error(' linux-arm64 Linux ARM64 (aarch64)');
49
+ console.error(' linux-x64 Linux x86_64 (glibc)');
50
+ console.error(' linux-arm64 Linux ARM64 (glibc)');
45
51
  console.error(' darwin-arm64 macOS Apple Silicon');
52
+ console.error(' darwin-x64 macOS Intel');
46
53
  console.error(' win32-x64 Windows x86_64');
47
54
  console.error('');
48
55
  console.error('Build from source: https://github.com/' + REPO);
49
56
  process.exit(1);
50
57
  }
51
58
 
59
+ // ── musl detection (Alpine etc.) ────────────────────────────────
60
+ // The Linux binaries are glibc-linked. On musl systems they install
61
+ // fine but can never exec (missing ld-linux loader) — fail HERE with
62
+ // the actual cause instead of a cryptic spawn error on first run.
63
+ if (process.platform === 'linux') {
64
+ const isMusl = fs.existsSync('/lib/ld-musl-x86_64.so.1')
65
+ || fs.existsSync('/lib/ld-musl-aarch64.so.1');
66
+ if (isMusl) {
67
+ console.error('donsetch: musl libc detected (Alpine?).');
68
+ console.error('The prebuilt Linux binaries are glibc-linked and will not run.');
69
+ console.error('');
70
+ console.error('Options:');
71
+ console.error(' - build from source: git clone https://github.com/' + REPO + ' && cargo build --release');
72
+ console.error(' - use a glibc-based image/dist (debian, ubuntu, fedora)');
73
+ process.exit(1);
74
+ }
75
+ }
76
+
52
77
  const binDir = path.join(__dirname, 'binaries');
53
78
  const binaryPath = path.join(binDir, plat.binary);
54
79
 
55
- // Skip if already installed (npm cache reuse, reinstall, etc.)
80
+ // Skip if already installed (npm cache reuse, reinstall, etc.).
81
+ // A plausibility check first: a stale 0-byte or truncated leftover
82
+ // (killed install, crashed download) must not shadow a fresh one.
56
83
  if (fs.existsSync(binaryPath)) {
57
- console.log(`donsetch: binary already present (${plat.binary})`);
58
- process.exit(0);
84
+ let size = 0;
85
+ try { size = fs.statSync(binaryPath).size; } catch (_) {}
86
+ if (size > 1024 * 1024) {
87
+ console.log(`donsetch: binary already present (${plat.binary})`);
88
+ process.exit(0);
89
+ }
90
+ console.log(`donsetch: leftover ${plat.binary} is ${size} bytes — re-downloading`);
91
+ try { fs.unlinkSync(binaryPath); } catch (_) {}
59
92
  }
60
93
 
61
94
  fs.mkdirSync(binDir, { recursive: true });
@@ -110,6 +143,19 @@ async function main() {
110
143
  const tarball = path.join(binDir, plat.asset);
111
144
  const checksumFile = path.join(binDir, 'checksum.sha256');
112
145
 
146
+ // 0. Windows: tar ships with Windows 10 1803+; older boxes lack it.
147
+ // Detect BEFORE downloading so the error names the real problem.
148
+ if (process.platform === 'win32') {
149
+ let hasTar = false;
150
+ try { execFileSync('tar', ['--version'], { stdio: 'ignore' }); hasTar = true; } catch (_) {}
151
+ if (!hasTar) {
152
+ console.error('donsetch: `tar` not found on this Windows system.');
153
+ console.error('tar ships with Windows 10 1803+. Update Windows, or extract manually');
154
+ console.error('after downloading ' + assetUrl);
155
+ process.exit(1);
156
+ }
157
+ }
158
+
113
159
  // 1. Download the binary tarball
114
160
  console.log(`donsetch: downloading ${plat.asset} from ${TAG}...`);
115
161
  await download(assetUrl, tarball);
@@ -142,12 +188,8 @@ async function main() {
142
188
  try { fs.unlinkSync(tarball); } catch (_) {}
143
189
  try { fs.unlinkSync(checksumFile); } catch (_) {}
144
190
 
145
- // 6. Make executable (Unix only)
146
- if (process.platform !== 'win32') {
147
- fs.chmodSync(binaryPath, 0o755);
148
- }
149
-
150
- // 7. Verify the binary exists after extraction
191
+ // 6. Verify the binary exists after extraction (BEFORE chmod —
192
+ // chmod on a missing file throws an opaque error).
151
193
  if (!fs.existsSync(binaryPath)) {
152
194
  console.error(`donsetch: expected ${plat.binary} not found after extraction`);
153
195
  console.error(` looked at: ${binaryPath}`);
@@ -155,6 +197,11 @@ async function main() {
155
197
  process.exit(1);
156
198
  }
157
199
 
200
+ // 7. Make executable (Unix only)
201
+ if (process.platform !== 'win32') {
202
+ fs.chmodSync(binaryPath, 0o755);
203
+ }
204
+
158
205
  console.log(`donsetch: installed ${plat.binary} to ${binaryPath}`);
159
206
  console.log(`donsetch: run \`donsetch\` to see available commands.`);
160
207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "donsetch",
3
- "version": "2.4.1",
3
+ "version": "3.0.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) {