donsetch 2.1.0 → 2.1.2
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 +443 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "donsetch",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
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,443 @@
|
|
|
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
|
+
* Custom TUI: each tool has clean renderCall/renderResult showing
|
|
18
|
+
* a compact summary card — not the full raw output. The LLM still
|
|
19
|
+
* receives complete content; the user sees a minimal status line +
|
|
20
|
+
* one-line preview. Amber theme matching DonSeTch's identity.
|
|
21
|
+
*/
|
|
22
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { Type } from "typebox";
|
|
24
|
+
import { spawn, execFileSync, type ChildProcess } from "node:child_process";
|
|
25
|
+
import { existsSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { Text, visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
|
|
28
|
+
|
|
29
|
+
// ── Constants ──
|
|
30
|
+
const INIT_TIMEOUT_MS = 10_000;
|
|
31
|
+
const CALL_TIMEOUT_MS = 120_000;
|
|
32
|
+
const SHUTDOWN_GRACE_MS = 2_000;
|
|
33
|
+
|
|
34
|
+
// ── Color palette — DonSeTch amber theme ──
|
|
35
|
+
const C_AMBER = "\x1b[38;2;255;178;0m";
|
|
36
|
+
const C_GREEN = "\x1b[38;2;100;200;100m";
|
|
37
|
+
const C_RED = "\x1b[38;2;229;115;115m";
|
|
38
|
+
const C_DIM = "\x1b[38;2;130;130;140m";
|
|
39
|
+
const C_CREAM = "\x1b[38;2;240;230;210m";
|
|
40
|
+
const RESET = "\x1b[0m";
|
|
41
|
+
|
|
42
|
+
// ── Tool icons ──
|
|
43
|
+
const ICONS: Record<string, string> = {
|
|
44
|
+
web_fetch: "\u{1F310}", // 🌐
|
|
45
|
+
web_search: "\u{1F50E}", // 🔎
|
|
46
|
+
web_crawl: "\u{1F577}\u{FE0F}", // 🕷️
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ── MCP client state ──
|
|
50
|
+
let proc: ChildProcess | null = null;
|
|
51
|
+
let nextId = 1;
|
|
52
|
+
const pending = new Map<
|
|
53
|
+
number,
|
|
54
|
+
{ resolve: (v: any) => void; reject: (e: any) => void; timer: ReturnType<typeof setTimeout> }
|
|
55
|
+
>();
|
|
56
|
+
let initialized = false;
|
|
57
|
+
const toolNames: string[] = [];
|
|
58
|
+
|
|
59
|
+
// ── Binary resolution ──
|
|
60
|
+
|
|
61
|
+
function getBinaryPath(): string {
|
|
62
|
+
const pkgDir = __dirname;
|
|
63
|
+
const binaryName = process.platform === "win32" ? "donsetch.exe" : "donsetch";
|
|
64
|
+
return join(pkgDir, "binaries", binaryName);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ensureBinary(): string {
|
|
68
|
+
const binaryPath = getBinaryPath();
|
|
69
|
+
if (existsSync(binaryPath)) return binaryPath;
|
|
70
|
+
|
|
71
|
+
const installScript = join(__dirname, "install.js");
|
|
72
|
+
if (!existsSync(installScript)) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`donsetch binary not found at ${binaryPath} and install.js is missing. ` +
|
|
75
|
+
`Run \`npm rebuild donsetch\` or \`npm install -g --allow-scripts=donsetch donsetch@latest\`.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
execFileSync("node", [installScript], {
|
|
81
|
+
stdio: "inherit",
|
|
82
|
+
cwd: __dirname,
|
|
83
|
+
timeout: 60_000,
|
|
84
|
+
});
|
|
85
|
+
} catch (err: any) {
|
|
86
|
+
throw new Error(`Failed to download donsetch binary: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!existsSync(binaryPath)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`donsetch binary still missing after install.js ran. ` +
|
|
92
|
+
`Run \`npm install -g --allow-scripts=donsetch donsetch@latest\` manually.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return binaryPath;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── MCP JSON-RPC 2.0 over stdio ──
|
|
100
|
+
|
|
101
|
+
function startServer(): Promise<void> {
|
|
102
|
+
if (proc && initialized) return Promise.resolve();
|
|
103
|
+
if (proc && !initialized) return Promise.reject(new Error("donsetch MCP server is still initializing"));
|
|
104
|
+
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
let binaryPath: string;
|
|
107
|
+
try {
|
|
108
|
+
binaryPath = ensureBinary();
|
|
109
|
+
} catch (err: any) {
|
|
110
|
+
reject(err);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
proc = spawn(binaryPath, ["mcp"], {
|
|
116
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
117
|
+
env: { ...process.env },
|
|
118
|
+
windowsHide: true,
|
|
119
|
+
});
|
|
120
|
+
} catch (err: any) {
|
|
121
|
+
reject(new Error(`Failed to spawn donsetch MCP server: ${err.message}`));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let buffer = "";
|
|
126
|
+
|
|
127
|
+
proc.stdout?.on("data", (chunk: Buffer) => {
|
|
128
|
+
buffer += chunk.toString();
|
|
129
|
+
const lines = buffer.split("\n");
|
|
130
|
+
buffer = lines.pop() || "";
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
if (!line.trim()) continue;
|
|
133
|
+
try {
|
|
134
|
+
const msg = JSON.parse(line);
|
|
135
|
+
if (msg.id != null && pending.has(msg.id)) {
|
|
136
|
+
const entry = pending.get(msg.id)!;
|
|
137
|
+
pending.delete(msg.id);
|
|
138
|
+
clearTimeout(entry.timer);
|
|
139
|
+
if (msg.error) {
|
|
140
|
+
entry.reject(new Error(msg.error.message || "MCP error"));
|
|
141
|
+
} else {
|
|
142
|
+
entry.resolve(msg.result);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
/* ignore non-JSON lines on stdout */
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
proc.stderr?.on("data", (chunk: Buffer) => {
|
|
152
|
+
process.stderr.write(chunk);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
proc.on("error", (err) => {
|
|
156
|
+
proc = null;
|
|
157
|
+
initialized = false;
|
|
158
|
+
for (const [, e] of pending) {
|
|
159
|
+
clearTimeout(e.timer);
|
|
160
|
+
e.reject(err);
|
|
161
|
+
}
|
|
162
|
+
pending.clear();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
proc.on("exit", (code) => {
|
|
166
|
+
proc = null;
|
|
167
|
+
initialized = false;
|
|
168
|
+
for (const [, e] of pending) {
|
|
169
|
+
clearTimeout(e.timer);
|
|
170
|
+
e.reject(new Error(`donsetch MCP server exited (code ${code})`));
|
|
171
|
+
}
|
|
172
|
+
pending.clear();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
sendRequest(
|
|
176
|
+
"initialize",
|
|
177
|
+
{
|
|
178
|
+
protocolVersion: "2024-11-05",
|
|
179
|
+
capabilities: {},
|
|
180
|
+
clientInfo: { name: "pi-donsetch", version: "1.0.0" },
|
|
181
|
+
},
|
|
182
|
+
INIT_TIMEOUT_MS
|
|
183
|
+
)
|
|
184
|
+
.then(() => {
|
|
185
|
+
sendNotification("notifications/initialized", {});
|
|
186
|
+
initialized = true;
|
|
187
|
+
resolve();
|
|
188
|
+
})
|
|
189
|
+
.catch(reject);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function sendRequest(method: string, params: any, timeoutMs = CALL_TIMEOUT_MS): Promise<any> {
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
if (!proc?.stdin?.writable) {
|
|
196
|
+
reject(new Error("donsetch MCP server not running"));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const id = nextId++;
|
|
200
|
+
const timer = setTimeout(() => {
|
|
201
|
+
if (pending.has(id)) {
|
|
202
|
+
pending.delete(id);
|
|
203
|
+
reject(new Error(`MCP request timeout (${timeoutMs}ms): ${method}`));
|
|
204
|
+
}
|
|
205
|
+
}, timeoutMs);
|
|
206
|
+
pending.set(id, { resolve, reject, timer });
|
|
207
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
208
|
+
proc.stdin.write(msg + "\n");
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function sendNotification(method: string, params: any): void {
|
|
213
|
+
if (!proc?.stdin?.writable) return;
|
|
214
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
215
|
+
proc.stdin.write(msg + "\n");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function callMcpTool(name: string, args: any): Promise<any> {
|
|
219
|
+
return sendRequest("tools/call", { name, arguments: args ?? {} });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function killServer(): void {
|
|
223
|
+
if (proc) {
|
|
224
|
+
try {
|
|
225
|
+
proc.stdin?.end();
|
|
226
|
+
proc.kill("SIGTERM");
|
|
227
|
+
const p = proc;
|
|
228
|
+
setTimeout(() => {
|
|
229
|
+
try { p.kill("SIGKILL"); } catch {}
|
|
230
|
+
}, SHUTDOWN_GRACE_MS);
|
|
231
|
+
} catch {}
|
|
232
|
+
proc = null;
|
|
233
|
+
}
|
|
234
|
+
initialized = false;
|
|
235
|
+
toolNames.length = 0;
|
|
236
|
+
for (const [, e] of pending) {
|
|
237
|
+
clearTimeout(e.timer);
|
|
238
|
+
e.reject(new Error("donsetch MCP server killed"));
|
|
239
|
+
}
|
|
240
|
+
pending.clear();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function isAlive(): boolean {
|
|
244
|
+
return proc !== null && !proc.killed && proc.stdin?.writable === true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── TUI helpers ──
|
|
248
|
+
|
|
249
|
+
/** Extract a clean preview line from markdown content. */
|
|
250
|
+
function getPreview(text: string, maxLen = 72): string {
|
|
251
|
+
const lines = text.split("\n");
|
|
252
|
+
for (const line of lines) {
|
|
253
|
+
let clean = line.replace(/^#+\s*/, "").replace(/\*\*([^*]+)\*\*/g, "$1").trim();
|
|
254
|
+
if (clean.length > 0 && !clean.startsWith("{") && !clean.startsWith("[")) {
|
|
255
|
+
return truncateToWidth(clean, maxLen, "\u2026");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Count numbered search results in text. */
|
|
262
|
+
function countSearchResults(text: string): number {
|
|
263
|
+
const matches = text.match(/^\d+\.\s/gm);
|
|
264
|
+
return matches ? matches.length : 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Extract first search result title. */
|
|
268
|
+
function getFirstResultTitle(text: string): string {
|
|
269
|
+
const match = text.match(/^\d+\.\s+\*\*(.+?)\*\*/m);
|
|
270
|
+
return match ? match[1] : "";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Count pages from crawl output (## headings or numbered pages). */
|
|
274
|
+
function countCrawlPages(text: string): number {
|
|
275
|
+
const matches = text.match(/^##\s/gm);
|
|
276
|
+
return matches ? matches.length : 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Extract domain from URL for display. */
|
|
280
|
+
function shortUrl(url: string): string {
|
|
281
|
+
try {
|
|
282
|
+
const u = new URL(url);
|
|
283
|
+
return u.hostname + (u.pathname !== "/" ? u.pathname.slice(0, 30) : "");
|
|
284
|
+
} catch {
|
|
285
|
+
return truncateToWidth(url, 50, "\u2026");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── Extension ──
|
|
290
|
+
|
|
291
|
+
export default function (pi: ExtensionAPI) {
|
|
292
|
+
pi.on("session_start", async () => {
|
|
293
|
+
try {
|
|
294
|
+
await startServer();
|
|
295
|
+
} catch (err: any) {
|
|
296
|
+
process.stderr.write(`[donsetch] failed to start MCP server: ${err.message}\n`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let toolsResult: any;
|
|
301
|
+
try {
|
|
302
|
+
toolsResult = await sendRequest("tools/list", {});
|
|
303
|
+
} catch (err: any) {
|
|
304
|
+
process.stderr.write(`[donsetch] failed to list tools: ${err.message}\n`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const mcpTools: any[] = toolsResult?.tools ?? [];
|
|
309
|
+
if (mcpTools.length === 0) {
|
|
310
|
+
process.stderr.write("[donsetch] no tools discovered from MCP server\n");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
for (const mcpTool of mcpTools) {
|
|
315
|
+
const name = mcpTool.name;
|
|
316
|
+
if (!name) continue;
|
|
317
|
+
toolNames.push(name);
|
|
318
|
+
|
|
319
|
+
const description = mcpTool.description || mcpTool.name;
|
|
320
|
+
const inputSchema = mcpTool.inputSchema || { type: "object", properties: {} };
|
|
321
|
+
const toolName = name;
|
|
322
|
+
const icon = ICONS[toolName] ?? "\u25C6";
|
|
323
|
+
|
|
324
|
+
pi.registerTool({
|
|
325
|
+
name: toolName,
|
|
326
|
+
label: toolName,
|
|
327
|
+
description,
|
|
328
|
+
parameters: Type.Unsafe(inputSchema) as any,
|
|
329
|
+
async execute(_toolCallId, params, _signal) {
|
|
330
|
+
if (!isAlive()) {
|
|
331
|
+
try {
|
|
332
|
+
await startServer();
|
|
333
|
+
} catch (err: any) {
|
|
334
|
+
return {
|
|
335
|
+
content: [{ type: "text", text: `donsetch MCP server crashed and could not restart: ${err.message}` }],
|
|
336
|
+
isError: true,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const result = await callMcpTool(toolName, params);
|
|
343
|
+
const text = result?.content?.[0]?.text ?? "";
|
|
344
|
+
const isErr = result?.isError ?? false;
|
|
345
|
+
|
|
346
|
+
// Build details for TUI rendering
|
|
347
|
+
const details: any = {
|
|
348
|
+
mcpTool: toolName,
|
|
349
|
+
isError: isErr,
|
|
350
|
+
chars: text.length,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
if (toolName === "web_search") {
|
|
354
|
+
details.results = countSearchResults(text);
|
|
355
|
+
details.topResult = getFirstResultTitle(text);
|
|
356
|
+
} else if (toolName === "web_crawl") {
|
|
357
|
+
details.pages = countCrawlPages(text);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// For errors, extract error text
|
|
361
|
+
if (isErr) {
|
|
362
|
+
details.error = getPreview(text, 60);
|
|
363
|
+
} else {
|
|
364
|
+
details.preview = getPreview(text);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
content: result?.content ?? [{ type: "text", text: "No output" }],
|
|
369
|
+
details,
|
|
370
|
+
isError: isErr,
|
|
371
|
+
};
|
|
372
|
+
} catch (err: any) {
|
|
373
|
+
return {
|
|
374
|
+
content: [{ type: "text", text: `donsetch MCP call failed: ${err.message}` }],
|
|
375
|
+
details: { mcpTool: toolName, isError: true, error: err.message },
|
|
376
|
+
isError: true,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
renderCall(args: any, _theme: any) {
|
|
382
|
+
let key = "";
|
|
383
|
+
if (args?.url) {
|
|
384
|
+
key = shortUrl(args.url);
|
|
385
|
+
} else if (args?.query) {
|
|
386
|
+
key = truncateToWidth(`"${args.query}"`, 50, "\u2026");
|
|
387
|
+
}
|
|
388
|
+
return new Text(
|
|
389
|
+
`${C_AMBER}${icon}${RESET} ${C_CREAM}${toolName}${RESET} ${C_DIM}${key}${RESET}`,
|
|
390
|
+
0, 0
|
|
391
|
+
);
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
renderResult(result: any, opts: any, _theme: any) {
|
|
395
|
+
if (opts?.isPartial) {
|
|
396
|
+
return new Text(`${C_AMBER}\u23F3${RESET} ${C_DIM}${toolName} working…${RESET}`, 0, 0);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const isErr = result?.isError || result?.details?.isError;
|
|
400
|
+
const d = result?.details ?? {};
|
|
401
|
+
const glyph = isErr ? "\u2717" : "\u2713";
|
|
402
|
+
const color = isErr ? C_RED : C_GREEN;
|
|
403
|
+
|
|
404
|
+
// Build metadata string per tool
|
|
405
|
+
let meta = "";
|
|
406
|
+
if (toolName === "web_fetch") {
|
|
407
|
+
const chars = d.chars ?? 0;
|
|
408
|
+
meta = `${chars.toLocaleString()} chars`;
|
|
409
|
+
} else if (toolName === "web_search") {
|
|
410
|
+
const count = d.results ?? 0;
|
|
411
|
+
meta = `${count} result${count !== 1 ? "s" : ""}`;
|
|
412
|
+
} else if (toolName === "web_crawl") {
|
|
413
|
+
const pages = d.pages ?? 0;
|
|
414
|
+
meta = `${pages} page${pages !== 1 ? "s" : ""}`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Build line 2: preview or error
|
|
418
|
+
let line2 = "";
|
|
419
|
+
if (isErr) {
|
|
420
|
+
line2 = d.error || "failed";
|
|
421
|
+
} else if (toolName === "web_search" && d.topResult) {
|
|
422
|
+
line2 = truncateToWidth(d.topResult, 70, "\u2026");
|
|
423
|
+
} else if (d.preview) {
|
|
424
|
+
line2 = d.preview;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const line1 = `${color}${glyph}${RESET} ${C_CREAM}${toolName}${RESET} ${C_DIM}\u00B7 ${meta}${RESET}`;
|
|
428
|
+
const output = line2
|
|
429
|
+
? `${line1}\n ${C_DIM}${line2}${RESET}`
|
|
430
|
+
: line1;
|
|
431
|
+
|
|
432
|
+
return new Text(output, 0, 0);
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
process.stderr.write(`[donsetch] ${mcpTools.length} tools registered: ${toolNames.join(", ")}\n`);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
pi.on("session_shutdown", () => {
|
|
441
|
+
killServer();
|
|
442
|
+
});
|
|
443
|
+
}
|