donsetch 2.1.1 → 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 +1 -1
- package/pi-extension.ts +149 -16
package/package.json
CHANGED
package/pi-extension.ts
CHANGED
|
@@ -13,18 +13,39 @@
|
|
|
13
13
|
* Auto-download: if the binary is missing (e.g. postinstall was
|
|
14
14
|
* blocked by npm 10+), the extension runs install.js at session_start
|
|
15
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.
|
|
16
21
|
*/
|
|
17
22
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
23
|
import { Type } from "typebox";
|
|
19
24
|
import { spawn, execFileSync, type ChildProcess } from "node:child_process";
|
|
20
25
|
import { existsSync } from "node:fs";
|
|
21
|
-
import { join
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { Text, visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
|
|
22
28
|
|
|
23
29
|
// ── Constants ──
|
|
24
30
|
const INIT_TIMEOUT_MS = 10_000;
|
|
25
|
-
const CALL_TIMEOUT_MS = 120_000;
|
|
31
|
+
const CALL_TIMEOUT_MS = 120_000;
|
|
26
32
|
const SHUTDOWN_GRACE_MS = 2_000;
|
|
27
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
|
+
|
|
28
49
|
// ── MCP client state ──
|
|
29
50
|
let proc: ChildProcess | null = null;
|
|
30
51
|
let nextId = 1;
|
|
@@ -47,8 +68,6 @@ function ensureBinary(): string {
|
|
|
47
68
|
const binaryPath = getBinaryPath();
|
|
48
69
|
if (existsSync(binaryPath)) return binaryPath;
|
|
49
70
|
|
|
50
|
-
// Binary missing — postinstall was likely blocked. Run install.js
|
|
51
|
-
// to download from GitHub Releases.
|
|
52
71
|
const installScript = join(__dirname, "install.js");
|
|
53
72
|
if (!existsSync(installScript)) {
|
|
54
73
|
throw new Error(
|
|
@@ -129,7 +148,6 @@ function startServer(): Promise<void> {
|
|
|
129
148
|
}
|
|
130
149
|
});
|
|
131
150
|
|
|
132
|
-
// Drain stderr to prevent pipe buffer deadlock; route to our stderr for debugging.
|
|
133
151
|
proc.stderr?.on("data", (chunk: Buffer) => {
|
|
134
152
|
process.stderr.write(chunk);
|
|
135
153
|
});
|
|
@@ -154,7 +172,6 @@ function startServer(): Promise<void> {
|
|
|
154
172
|
pending.clear();
|
|
155
173
|
});
|
|
156
174
|
|
|
157
|
-
// MCP handshake: initialize → notifications/initialized
|
|
158
175
|
sendRequest(
|
|
159
176
|
"initialize",
|
|
160
177
|
{
|
|
@@ -209,9 +226,7 @@ function killServer(): void {
|
|
|
209
226
|
proc.kill("SIGTERM");
|
|
210
227
|
const p = proc;
|
|
211
228
|
setTimeout(() => {
|
|
212
|
-
try {
|
|
213
|
-
p.kill("SIGKILL");
|
|
214
|
-
} catch {}
|
|
229
|
+
try { p.kill("SIGKILL"); } catch {}
|
|
215
230
|
}, SHUTDOWN_GRACE_MS);
|
|
216
231
|
} catch {}
|
|
217
232
|
proc = null;
|
|
@@ -229,6 +244,48 @@ function isAlive(): boolean {
|
|
|
229
244
|
return proc !== null && !proc.killed && proc.stdin?.writable === true;
|
|
230
245
|
}
|
|
231
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
|
+
|
|
232
289
|
// ── Extension ──
|
|
233
290
|
|
|
234
291
|
export default function (pi: ExtensionAPI) {
|
|
@@ -261,17 +318,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
261
318
|
|
|
262
319
|
const description = mcpTool.description || mcpTool.name;
|
|
263
320
|
const inputSchema = mcpTool.inputSchema || { type: "object", properties: {} };
|
|
264
|
-
|
|
265
|
-
// Capture name for closure
|
|
266
321
|
const toolName = name;
|
|
322
|
+
const icon = ICONS[toolName] ?? "\u25C6";
|
|
267
323
|
|
|
268
324
|
pi.registerTool({
|
|
269
325
|
name: toolName,
|
|
270
326
|
label: toolName,
|
|
271
327
|
description,
|
|
272
328
|
parameters: Type.Unsafe(inputSchema) as any,
|
|
273
|
-
async execute(_toolCallId, params,
|
|
274
|
-
// Check if server is still alive, restart if dead
|
|
329
|
+
async execute(_toolCallId, params, _signal) {
|
|
275
330
|
if (!isAlive()) {
|
|
276
331
|
try {
|
|
277
332
|
await startServer();
|
|
@@ -285,19 +340,97 @@ export default function (pi: ExtensionAPI) {
|
|
|
285
340
|
|
|
286
341
|
try {
|
|
287
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
|
+
|
|
288
367
|
return {
|
|
289
368
|
content: result?.content ?? [{ type: "text", text: "No output" }],
|
|
290
|
-
details
|
|
291
|
-
isError:
|
|
369
|
+
details,
|
|
370
|
+
isError: isErr,
|
|
292
371
|
};
|
|
293
372
|
} catch (err: any) {
|
|
294
373
|
return {
|
|
295
374
|
content: [{ type: "text", text: `donsetch MCP call failed: ${err.message}` }],
|
|
296
|
-
details: { error: err.message
|
|
375
|
+
details: { mcpTool: toolName, isError: true, error: err.message },
|
|
297
376
|
isError: true,
|
|
298
377
|
};
|
|
299
378
|
}
|
|
300
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
|
+
},
|
|
301
434
|
});
|
|
302
435
|
}
|
|
303
436
|
|