donsetch 2.0.0 → 2.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/package.json +10 -1
- package/pi-extension.ts +310 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "donsetch",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
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",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"url": "https://github.com/dondai44423/donsetch/issues"
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
|
+
"pi-package",
|
|
16
17
|
"mcp",
|
|
17
18
|
"web-fetch",
|
|
18
19
|
"search",
|
|
@@ -25,6 +26,13 @@
|
|
|
25
26
|
"pdf",
|
|
26
27
|
"extraction"
|
|
27
28
|
],
|
|
29
|
+
"pi": {
|
|
30
|
+
"extensions": ["./pi-extension.ts"]
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
34
|
+
"typebox": "*"
|
|
35
|
+
},
|
|
28
36
|
"bin": {
|
|
29
37
|
"donsetch": "bin/donsetch.js"
|
|
30
38
|
},
|
|
@@ -34,6 +42,7 @@
|
|
|
34
42
|
"files": [
|
|
35
43
|
"install.js",
|
|
36
44
|
"bin/donsetch.js",
|
|
45
|
+
"pi-extension.ts",
|
|
37
46
|
"README.md"
|
|
38
47
|
],
|
|
39
48
|
"engines": {
|
package/pi-extension.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DonSeTch pi extension — bridges the donsetch MCP binary into pi.
|
|
3
|
+
*
|
|
4
|
+
* `pi install npm:donsetch` installs this package. At session_start the
|
|
5
|
+
* extension spawns `donsetch mcp`, performs the MCP handshake, discovers
|
|
6
|
+
* tools via tools/list, and registers each one natively with
|
|
7
|
+
* pi.registerTool(). Tool calls are proxied to the binary over stdio.
|
|
8
|
+
*
|
|
9
|
+
* Zero maintenance: tool definitions are fetched dynamically from the
|
|
10
|
+
* binary. When donsetch adds or changes tools, this extension picks
|
|
11
|
+
* them up automatically — no code changes needed here.
|
|
12
|
+
*
|
|
13
|
+
* Auto-download: if the binary is missing (e.g. postinstall was
|
|
14
|
+
* blocked by npm 10+), the extension runs install.js at session_start
|
|
15
|
+
* to fetch it from GitHub Releases.
|
|
16
|
+
*/
|
|
17
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { Type } from "typebox";
|
|
19
|
+
import { spawn, execFileSync, type ChildProcess } from "node:child_process";
|
|
20
|
+
import { existsSync } from "node:fs";
|
|
21
|
+
import { join, dirname } from "node:path";
|
|
22
|
+
|
|
23
|
+
// ── Constants ──
|
|
24
|
+
const INIT_TIMEOUT_MS = 10_000;
|
|
25
|
+
const CALL_TIMEOUT_MS = 120_000; // fetch/crawl can take a while
|
|
26
|
+
const SHUTDOWN_GRACE_MS = 2_000;
|
|
27
|
+
|
|
28
|
+
// ── MCP client state ──
|
|
29
|
+
let proc: ChildProcess | null = null;
|
|
30
|
+
let nextId = 1;
|
|
31
|
+
const pending = new Map<
|
|
32
|
+
number,
|
|
33
|
+
{ resolve: (v: any) => void; reject: (e: any) => void; timer: ReturnType<typeof setTimeout> }
|
|
34
|
+
>();
|
|
35
|
+
let initialized = false;
|
|
36
|
+
const toolNames: string[] = [];
|
|
37
|
+
|
|
38
|
+
// ── Binary resolution ──
|
|
39
|
+
|
|
40
|
+
function getBinaryPath(): string {
|
|
41
|
+
const pkgDir = __dirname;
|
|
42
|
+
const binaryName = process.platform === "win32" ? "donsetch.exe" : "donsetch";
|
|
43
|
+
return join(pkgDir, "binaries", binaryName);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ensureBinary(): string {
|
|
47
|
+
const binaryPath = getBinaryPath();
|
|
48
|
+
if (existsSync(binaryPath)) return binaryPath;
|
|
49
|
+
|
|
50
|
+
// Binary missing — postinstall was likely blocked. Run install.js
|
|
51
|
+
// to download from GitHub Releases.
|
|
52
|
+
const installScript = join(__dirname, "install.js");
|
|
53
|
+
if (!existsSync(installScript)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`donsetch binary not found at ${binaryPath} and install.js is missing. ` +
|
|
56
|
+
`Run \`npm rebuild donsetch\` or \`npm install -g --allow-scripts=donsetch donsetch@latest\`.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
execFileSync("node", [installScript], {
|
|
62
|
+
stdio: "inherit",
|
|
63
|
+
cwd: __dirname,
|
|
64
|
+
timeout: 60_000,
|
|
65
|
+
});
|
|
66
|
+
} catch (err: any) {
|
|
67
|
+
throw new Error(`Failed to download donsetch binary: ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!existsSync(binaryPath)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`donsetch binary still missing after install.js ran. ` +
|
|
73
|
+
`Run \`npm install -g --allow-scripts=donsetch donsetch@latest\` manually.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return binaryPath;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── MCP JSON-RPC 2.0 over stdio ──
|
|
81
|
+
|
|
82
|
+
function startServer(): Promise<void> {
|
|
83
|
+
if (proc && initialized) return Promise.resolve();
|
|
84
|
+
if (proc && !initialized) return Promise.reject(new Error("donsetch MCP server is still initializing"));
|
|
85
|
+
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
let binaryPath: string;
|
|
88
|
+
try {
|
|
89
|
+
binaryPath = ensureBinary();
|
|
90
|
+
} catch (err: any) {
|
|
91
|
+
reject(err);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
proc = spawn(binaryPath, ["mcp"], {
|
|
97
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
98
|
+
env: { ...process.env },
|
|
99
|
+
windowsHide: true,
|
|
100
|
+
});
|
|
101
|
+
} catch (err: any) {
|
|
102
|
+
reject(new Error(`Failed to spawn donsetch MCP server: ${err.message}`));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let buffer = "";
|
|
107
|
+
|
|
108
|
+
proc.stdout?.on("data", (chunk: Buffer) => {
|
|
109
|
+
buffer += chunk.toString();
|
|
110
|
+
const lines = buffer.split("\n");
|
|
111
|
+
buffer = lines.pop() || "";
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
if (!line.trim()) continue;
|
|
114
|
+
try {
|
|
115
|
+
const msg = JSON.parse(line);
|
|
116
|
+
if (msg.id != null && pending.has(msg.id)) {
|
|
117
|
+
const entry = pending.get(msg.id)!;
|
|
118
|
+
pending.delete(msg.id);
|
|
119
|
+
clearTimeout(entry.timer);
|
|
120
|
+
if (msg.error) {
|
|
121
|
+
entry.reject(new Error(msg.error.message || "MCP error"));
|
|
122
|
+
} else {
|
|
123
|
+
entry.resolve(msg.result);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
/* ignore non-JSON lines on stdout */
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Drain stderr to prevent pipe buffer deadlock; route to our stderr for debugging.
|
|
133
|
+
proc.stderr?.on("data", (chunk: Buffer) => {
|
|
134
|
+
process.stderr.write(chunk);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
proc.on("error", (err) => {
|
|
138
|
+
proc = null;
|
|
139
|
+
initialized = false;
|
|
140
|
+
for (const [, e] of pending) {
|
|
141
|
+
clearTimeout(e.timer);
|
|
142
|
+
e.reject(err);
|
|
143
|
+
}
|
|
144
|
+
pending.clear();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
proc.on("exit", (code) => {
|
|
148
|
+
proc = null;
|
|
149
|
+
initialized = false;
|
|
150
|
+
for (const [, e] of pending) {
|
|
151
|
+
clearTimeout(e.timer);
|
|
152
|
+
e.reject(new Error(`donsetch MCP server exited (code ${code})`));
|
|
153
|
+
}
|
|
154
|
+
pending.clear();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// MCP handshake: initialize → notifications/initialized
|
|
158
|
+
sendRequest(
|
|
159
|
+
"initialize",
|
|
160
|
+
{
|
|
161
|
+
protocolVersion: "2024-11-05",
|
|
162
|
+
capabilities: {},
|
|
163
|
+
clientInfo: { name: "pi-donsetch", version: "1.0.0" },
|
|
164
|
+
},
|
|
165
|
+
INIT_TIMEOUT_MS
|
|
166
|
+
)
|
|
167
|
+
.then(() => {
|
|
168
|
+
sendNotification("notifications/initialized", {});
|
|
169
|
+
initialized = true;
|
|
170
|
+
resolve();
|
|
171
|
+
})
|
|
172
|
+
.catch(reject);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sendRequest(method: string, params: any, timeoutMs = CALL_TIMEOUT_MS): Promise<any> {
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
if (!proc?.stdin?.writable) {
|
|
179
|
+
reject(new Error("donsetch MCP server not running"));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const id = nextId++;
|
|
183
|
+
const timer = setTimeout(() => {
|
|
184
|
+
if (pending.has(id)) {
|
|
185
|
+
pending.delete(id);
|
|
186
|
+
reject(new Error(`MCP request timeout (${timeoutMs}ms): ${method}`));
|
|
187
|
+
}
|
|
188
|
+
}, timeoutMs);
|
|
189
|
+
pending.set(id, { resolve, reject, timer });
|
|
190
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
191
|
+
proc.stdin.write(msg + "\n");
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function sendNotification(method: string, params: any): void {
|
|
196
|
+
if (!proc?.stdin?.writable) return;
|
|
197
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
198
|
+
proc.stdin.write(msg + "\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function callMcpTool(name: string, args: any): Promise<any> {
|
|
202
|
+
return sendRequest("tools/call", { name, arguments: args ?? {} });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function killServer(): void {
|
|
206
|
+
if (proc) {
|
|
207
|
+
try {
|
|
208
|
+
proc.stdin?.end();
|
|
209
|
+
proc.kill("SIGTERM");
|
|
210
|
+
const p = proc;
|
|
211
|
+
setTimeout(() => {
|
|
212
|
+
try {
|
|
213
|
+
p.kill("SIGKILL");
|
|
214
|
+
} catch {}
|
|
215
|
+
}, SHUTDOWN_GRACE_MS);
|
|
216
|
+
} catch {}
|
|
217
|
+
proc = null;
|
|
218
|
+
}
|
|
219
|
+
initialized = false;
|
|
220
|
+
toolNames.length = 0;
|
|
221
|
+
for (const [, e] of pending) {
|
|
222
|
+
clearTimeout(e.timer);
|
|
223
|
+
e.reject(new Error("donsetch MCP server killed"));
|
|
224
|
+
}
|
|
225
|
+
pending.clear();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isAlive(): boolean {
|
|
229
|
+
return proc !== null && !proc.killed && proc.stdin?.writable === true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── Extension ──
|
|
233
|
+
|
|
234
|
+
export default function (pi: ExtensionAPI) {
|
|
235
|
+
pi.on("session_start", async () => {
|
|
236
|
+
try {
|
|
237
|
+
await startServer();
|
|
238
|
+
} catch (err: any) {
|
|
239
|
+
process.stderr.write(`[donsetch] failed to start MCP server: ${err.message}\n`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let toolsResult: any;
|
|
244
|
+
try {
|
|
245
|
+
toolsResult = await sendRequest("tools/list", {});
|
|
246
|
+
} catch (err: any) {
|
|
247
|
+
process.stderr.write(`[donsetch] failed to list tools: ${err.message}\n`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const mcpTools: any[] = toolsResult?.tools ?? [];
|
|
252
|
+
if (mcpTools.length === 0) {
|
|
253
|
+
process.stderr.write("[donsetch] no tools discovered from MCP server\n");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const mcpTool of mcpTools) {
|
|
258
|
+
const name = mcpTool.name;
|
|
259
|
+
if (!name) continue;
|
|
260
|
+
toolNames.push(name);
|
|
261
|
+
|
|
262
|
+
const description = mcpTool.description || mcpTool.name;
|
|
263
|
+
const inputSchema = mcpTool.inputSchema || { type: "object", properties: {} };
|
|
264
|
+
|
|
265
|
+
// Capture name for closure
|
|
266
|
+
const toolName = name;
|
|
267
|
+
|
|
268
|
+
pi.registerTool({
|
|
269
|
+
name: toolName,
|
|
270
|
+
label: toolName,
|
|
271
|
+
description,
|
|
272
|
+
parameters: Type.Unsafe(inputSchema) as any,
|
|
273
|
+
async execute(_toolCallId, params, signal) {
|
|
274
|
+
// Check if server is still alive, restart if dead
|
|
275
|
+
if (!isAlive()) {
|
|
276
|
+
try {
|
|
277
|
+
await startServer();
|
|
278
|
+
} catch (err: any) {
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: "text", text: `donsetch MCP server crashed and could not restart: ${err.message}` }],
|
|
281
|
+
isError: true,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const result = await callMcpTool(toolName, params);
|
|
288
|
+
return {
|
|
289
|
+
content: result?.content ?? [{ type: "text", text: "No output" }],
|
|
290
|
+
details: { mcpTool: toolName, isError: result?.isError ?? false },
|
|
291
|
+
isError: result?.isError ?? false,
|
|
292
|
+
};
|
|
293
|
+
} catch (err: any) {
|
|
294
|
+
return {
|
|
295
|
+
content: [{ type: "text", text: `donsetch MCP call failed: ${err.message}` }],
|
|
296
|
+
details: { error: err.message, mcpTool: toolName },
|
|
297
|
+
isError: true,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
process.stderr.write(`[donsetch] ${mcpTools.length} tools registered: ${toolNames.join(", ")}\n`);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
pi.on("session_shutdown", () => {
|
|
308
|
+
killServer();
|
|
309
|
+
});
|
|
310
|
+
}
|