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 +4 -1
- package/install.js +61 -14
- package/package.json +1 -1
- package/pi-extension.ts +68 -12
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
58
|
-
|
|
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.
|
|
146
|
-
|
|
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
package/pi-extension.ts
CHANGED
|
@@ -118,7 +118,11 @@ function startServer(): Promise<void> {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
try {
|
|
121
|
-
|
|
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(
|
|
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.
|
|
208
|
-
|
|
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
|
-
|
|
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 (
|
|
485
|
-
line2 =
|
|
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) {
|