@mrclrchtr/supi-web 5.0.0 → 6.0.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/README.md +2 -0
- package/package.json +1 -1
- package/src/docs.ts +5 -328
- package/src/tool/tool-specs.ts +74 -81
- package/src/tool/web_docs_fetch/execute.ts +41 -0
- package/src/tool/web_docs_fetch/guidance.ts +9 -0
- package/src/tool/web_docs_fetch/register.ts +16 -0
- package/src/tool/web_docs_fetch/render.ts +70 -0
- package/src/tool/web_docs_fetch/result.ts +31 -0
- package/src/tool/web_docs_fetch/spec.ts +32 -0
- package/src/tool/web_docs_search/execute.ts +50 -0
- package/src/tool/web_docs_search/guidance.ts +7 -0
- package/src/tool/web_docs_search/register.ts +16 -0
- package/src/tool/web_docs_search/render.ts +75 -0
- package/src/tool/web_docs_search/result.ts +103 -0
- package/src/tool/web_docs_search/spec.ts +28 -0
- package/src/tool/web_fetch_md/execute.ts +69 -0
- package/src/tool/web_fetch_md/guidance.ts +32 -0
- package/src/tool/web_fetch_md/input.ts +39 -0
- package/src/tool/web_fetch_md/register.ts +17 -0
- package/src/tool/web_fetch_md/render.ts +74 -0
- package/src/tool/web_fetch_md/result.ts +52 -0
- package/src/tool/web_fetch_md/spec.ts +21 -0
- package/src/web.ts +3 -178
- package/src/tool/guidance.ts +0 -34
- /package/src/tool/{output.ts → result.ts} +0 -0
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
# @mrclrchtr/supi-web — Web Fetch and Context7 for Pi
|
|
8
8
|
|
|
9
|
+
[](https://github.com/mrclrchtr/supi/stargazers) [](https://www.npmjs.com/package/@mrclrchtr/supi-web)
|
|
10
|
+
|
|
9
11
|
Adds web fetch and Context7 documentation tools to the [Pi coding agent](https://github.com/earendil-works/pi), without making you paste sources into chat.
|
|
10
12
|
|
|
11
13
|
## What your agent gets
|
package/package.json
CHANGED
package/src/docs.ts
CHANGED
|
@@ -4,334 +4,11 @@
|
|
|
4
4
|
* API key is read automatically from the CONTEXT7_API_KEY environment variable.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
ExtensionAPI,
|
|
11
|
-
ExtensionContext,
|
|
12
|
-
TruncationResult,
|
|
13
|
-
} from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { getContext, searchLibrary } from "./context7-client.ts";
|
|
15
|
-
import { getWebToolPromptSurface } from "./tool/guidance.ts";
|
|
16
|
-
import { limitModelVisibleOutput } from "./tool/output.ts";
|
|
17
|
-
import { renderCollapsibleTextResult, renderToolCall } from "./tool/render.ts";
|
|
18
|
-
import {
|
|
19
|
-
getWebToolSpec,
|
|
20
|
-
WEB_DOCS_FETCH_TOOL_NAME,
|
|
21
|
-
WEB_DOCS_SEARCH_TOOL_NAME,
|
|
22
|
-
type WebDocsFetchInput,
|
|
23
|
-
type WebDocsSearchInput,
|
|
24
|
-
} from "./tool/tool-specs.ts";
|
|
25
|
-
|
|
26
|
-
interface SearchDetails extends Record<string, unknown> {
|
|
27
|
-
count: number;
|
|
28
|
-
libraryName: string;
|
|
29
|
-
truncation?: TruncationResult;
|
|
30
|
-
fullOutputPath?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface FetchDetails extends Record<string, unknown> {
|
|
34
|
-
libraryId: string;
|
|
35
|
-
raw: boolean;
|
|
36
|
-
chars: number;
|
|
37
|
-
lines: number;
|
|
38
|
-
truncation?: TruncationResult;
|
|
39
|
-
fullOutputPath?: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
type SearchLibraryResult = Awaited<ReturnType<typeof searchLibrary>>[number];
|
|
43
|
-
|
|
44
|
-
const MAX_SEARCH_RESULTS = 10;
|
|
45
|
-
const MAX_DESCRIPTION_CHARS = 120;
|
|
46
|
-
const MAX_VERSION_COUNT = 5;
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { registerWebDocsFetchTool } from "./tool/web_docs_fetch/register.ts";
|
|
9
|
+
import { registerWebDocsSearchTool } from "./tool/web_docs_search/register.ts";
|
|
47
10
|
|
|
48
11
|
export default function docsExtension(pi: ExtensionAPI): void {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
pi.registerTool({
|
|
52
|
-
name: searchSpec.name,
|
|
53
|
-
label: searchSpec.label,
|
|
54
|
-
description: searchSurface.description,
|
|
55
|
-
promptSnippet: searchSurface.promptSnippet,
|
|
56
|
-
promptGuidelines: searchSurface.promptGuidelines,
|
|
57
|
-
parameters: searchSpec.parameters,
|
|
58
|
-
execute: runSearch,
|
|
59
|
-
renderCall(args, theme) {
|
|
60
|
-
const input = (args ?? {}) as WebDocsSearchInput;
|
|
61
|
-
const libraryName = typeof input.library_name === "string" ? input.library_name : "";
|
|
62
|
-
const query = typeof input.query === "string" ? truncatePreview(input.query) : undefined;
|
|
63
|
-
return renderToolCall(searchSpec.name, libraryName, theme, query);
|
|
64
|
-
},
|
|
65
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
66
|
-
if (isPartial) {
|
|
67
|
-
return renderCollapsibleTextResult({
|
|
68
|
-
summary: theme.fg("warning", "Searching Context7..."),
|
|
69
|
-
expanded,
|
|
70
|
-
theme,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const details = result.details as SearchDetails | undefined;
|
|
75
|
-
const summary = buildSearchSummary(details, theme);
|
|
76
|
-
const content = result.content.find((item) => item.type === "text");
|
|
77
|
-
const body =
|
|
78
|
-
details?.count === 0 ? undefined : content?.type === "text" ? content.text : undefined;
|
|
79
|
-
|
|
80
|
-
return renderCollapsibleTextResult({
|
|
81
|
-
summary,
|
|
82
|
-
body,
|
|
83
|
-
expanded,
|
|
84
|
-
theme,
|
|
85
|
-
fullOutputPath: details?.fullOutputPath,
|
|
86
|
-
});
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const fetchSpec = getWebToolSpec(WEB_DOCS_FETCH_TOOL_NAME);
|
|
91
|
-
const fetchSurface = getWebToolPromptSurface(WEB_DOCS_FETCH_TOOL_NAME);
|
|
92
|
-
pi.registerTool({
|
|
93
|
-
name: fetchSpec.name,
|
|
94
|
-
label: fetchSpec.label,
|
|
95
|
-
description: fetchSurface.description,
|
|
96
|
-
promptSnippet: fetchSurface.promptSnippet,
|
|
97
|
-
promptGuidelines: fetchSurface.promptGuidelines,
|
|
98
|
-
parameters: fetchSpec.parameters,
|
|
99
|
-
execute: runFetch,
|
|
100
|
-
renderCall(args, theme) {
|
|
101
|
-
const input = (args ?? {}) as WebDocsFetchInput;
|
|
102
|
-
const libraryId = typeof input.library_id === "string" ? input.library_id : "";
|
|
103
|
-
const query = typeof input.query === "string" ? truncatePreview(input.query) : undefined;
|
|
104
|
-
return renderToolCall(fetchSpec.name, libraryId, theme, query);
|
|
105
|
-
},
|
|
106
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
107
|
-
if (isPartial) {
|
|
108
|
-
return renderCollapsibleTextResult({
|
|
109
|
-
summary: theme.fg("warning", "Fetching Context7 docs..."),
|
|
110
|
-
expanded,
|
|
111
|
-
theme,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const details = result.details as FetchDetails | undefined;
|
|
116
|
-
const summary = buildFetchSummary(details, theme);
|
|
117
|
-
const content = result.content.find((item) => item.type === "text");
|
|
118
|
-
const body = content?.type === "text" ? content.text : undefined;
|
|
119
|
-
|
|
120
|
-
return renderCollapsibleTextResult({
|
|
121
|
-
summary,
|
|
122
|
-
body,
|
|
123
|
-
expanded,
|
|
124
|
-
theme,
|
|
125
|
-
fullOutputPath: details?.fullOutputPath,
|
|
126
|
-
});
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
132
|
-
async function runSearch(
|
|
133
|
-
_toolCallId: string,
|
|
134
|
-
params: unknown,
|
|
135
|
-
signal: AbortSignal | undefined,
|
|
136
|
-
onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined,
|
|
137
|
-
_ctx: ExtensionContext,
|
|
138
|
-
): Promise<AgentToolResult<SearchDetails>> {
|
|
139
|
-
const input = (params ?? {}) as WebDocsSearchInput;
|
|
140
|
-
const libraryName = input.library_name?.trim();
|
|
141
|
-
const query = input.query?.trim();
|
|
142
|
-
|
|
143
|
-
if (!libraryName) throw new Error("'library_name' parameter is required");
|
|
144
|
-
if (!query) throw new Error("'query' parameter is required");
|
|
145
|
-
|
|
146
|
-
onUpdate?.({
|
|
147
|
-
content: [{ type: "text", text: `Searching Context7 for ${libraryName}...` }],
|
|
148
|
-
details: { libraryName },
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const requestOptions = signal ? { signal } : undefined;
|
|
152
|
-
const results = await searchLibrary(query, libraryName, requestOptions);
|
|
153
|
-
|
|
154
|
-
if (results.length === 0) {
|
|
155
|
-
return {
|
|
156
|
-
content: [
|
|
157
|
-
{
|
|
158
|
-
type: "text",
|
|
159
|
-
text: `No libraries found for "${libraryName}". Try a different search term.`,
|
|
160
|
-
},
|
|
161
|
-
],
|
|
162
|
-
details: { count: 0, libraryName },
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const markdown = formatSearchResults(libraryName, results);
|
|
167
|
-
const output = await limitModelVisibleOutput(markdown, {
|
|
168
|
-
tempPrefix: "web-docs-search",
|
|
169
|
-
suffix: ".md",
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
content: [{ type: "text", text: output.text }],
|
|
174
|
-
details: {
|
|
175
|
-
count: results.length,
|
|
176
|
-
libraryName,
|
|
177
|
-
truncation: output.truncation,
|
|
178
|
-
fullOutputPath: output.fullOutputPath,
|
|
179
|
-
},
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
184
|
-
async function runFetch(
|
|
185
|
-
_toolCallId: string,
|
|
186
|
-
params: unknown,
|
|
187
|
-
signal: AbortSignal | undefined,
|
|
188
|
-
onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined,
|
|
189
|
-
_ctx: ExtensionContext,
|
|
190
|
-
): Promise<AgentToolResult<FetchDetails>> {
|
|
191
|
-
const input = (params ?? {}) as WebDocsFetchInput;
|
|
192
|
-
const libraryId = input.library_id?.trim();
|
|
193
|
-
const query = input.query?.trim();
|
|
194
|
-
const raw = Boolean(input.raw);
|
|
195
|
-
|
|
196
|
-
if (!libraryId) throw new Error("'library_id' parameter is required");
|
|
197
|
-
if (!query) throw new Error("'query' parameter is required");
|
|
198
|
-
|
|
199
|
-
onUpdate?.({
|
|
200
|
-
content: [{ type: "text", text: `Fetching Context7 docs for ${libraryId}...` }],
|
|
201
|
-
details: { libraryId, raw },
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const requestOptions = signal ? { signal } : undefined;
|
|
205
|
-
const content = await getContext(query, libraryId, raw, requestOptions);
|
|
206
|
-
const textContent = typeof content === "string" ? content : JSON.stringify(content, null, 2);
|
|
207
|
-
const output = await limitModelVisibleOutput(textContent, {
|
|
208
|
-
tempPrefix: "web-docs-fetch",
|
|
209
|
-
suffix: raw ? ".json" : ".md",
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
content: [{ type: "text", text: output.text }],
|
|
214
|
-
details: {
|
|
215
|
-
libraryId,
|
|
216
|
-
raw,
|
|
217
|
-
chars: textContent.length,
|
|
218
|
-
lines: textContent.split("\n").length,
|
|
219
|
-
truncation: output.truncation,
|
|
220
|
-
fullOutputPath: output.fullOutputPath,
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function formatSearchResults(
|
|
226
|
-
libraryName: string,
|
|
227
|
-
results: Awaited<ReturnType<typeof searchLibrary>>,
|
|
228
|
-
): string {
|
|
229
|
-
const visibleResults = results.slice(0, MAX_SEARCH_RESULTS);
|
|
230
|
-
const hiddenCount = results.length - visibleResults.length;
|
|
231
|
-
const rows = visibleResults.map(formatSearchRow);
|
|
232
|
-
const noun = results.length === 1 ? "library" : "libraries";
|
|
233
|
-
const hiddenNote =
|
|
234
|
-
hiddenCount > 0
|
|
235
|
-
? [`_${hiddenCount} more omitted; refine \`library_name\` or \`query\` if needed._`, ""]
|
|
236
|
-
: [];
|
|
237
|
-
|
|
238
|
-
return [
|
|
239
|
-
`Found ${results.length} Context7 ${noun} for "${libraryName}"${hiddenCount > 0 ? `; showing top ${visibleResults.length}` : ""}:`,
|
|
240
|
-
"",
|
|
241
|
-
"| ID | Name | Trust | Bench | Snips | Versions | Description |",
|
|
242
|
-
"|---|---|---|---|---|---|---|",
|
|
243
|
-
...rows,
|
|
244
|
-
"",
|
|
245
|
-
...hiddenNote,
|
|
246
|
-
"> Use `web_docs_fetch` with the chosen ID.",
|
|
247
|
-
].join("\n");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function formatSearchRow(lib: SearchLibraryResult): string {
|
|
251
|
-
const cells = [
|
|
252
|
-
`\`${escapeMd(lib.id)}\``,
|
|
253
|
-
escapeMd(lib.name),
|
|
254
|
-
String(lib.trustScore ?? ""),
|
|
255
|
-
String(lib.benchmarkScore ?? ""),
|
|
256
|
-
String(lib.totalSnippets ?? ""),
|
|
257
|
-
escapeMd(formatVersions(lib.versions)),
|
|
258
|
-
escapeMd(truncateCell(lib.description ?? "", MAX_DESCRIPTION_CHARS)),
|
|
259
|
-
];
|
|
260
|
-
|
|
261
|
-
return `| ${cells.join(" | ")} |`;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function formatVersions(versions?: string[]): string {
|
|
265
|
-
if (!versions?.length) return "";
|
|
266
|
-
const visibleVersions = versions.slice(0, MAX_VERSION_COUNT);
|
|
267
|
-
const hiddenCount = versions.length - visibleVersions.length;
|
|
268
|
-
return `${visibleVersions.join(", ")}${hiddenCount > 0 ? `, +${hiddenCount}` : ""}`;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function truncateCell(text: string, maxChars: number): string {
|
|
272
|
-
const compact = text.replace(/\s+/g, " ").trim();
|
|
273
|
-
if (compact.length <= maxChars) return compact;
|
|
274
|
-
return `${compact.slice(0, maxChars - 1).trimEnd()}…`;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function escapeMd(text: string): string {
|
|
278
|
-
return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function truncatePreview(text: string, maxChars = 48): string {
|
|
282
|
-
const compact = text.replace(/\s+/g, " ").trim();
|
|
283
|
-
if (compact.length <= maxChars) return compact;
|
|
284
|
-
return `${compact.slice(0, maxChars - 1).trimEnd()}…`;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function buildSearchSummary(
|
|
288
|
-
details: SearchDetails | undefined,
|
|
289
|
-
theme: { fg: (color: "success" | "warning" | "dim", text: string) => string },
|
|
290
|
-
): string {
|
|
291
|
-
if (!details) {
|
|
292
|
-
return theme.fg("success", "Context7 search finished");
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (details.count === 0) {
|
|
296
|
-
return [
|
|
297
|
-
theme.fg("warning", "No libraries found"),
|
|
298
|
-
theme.fg("dim", ` for ${JSON.stringify(details.libraryName)}`),
|
|
299
|
-
].join("");
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const noun = details.count === 1 ? "library" : "libraries";
|
|
303
|
-
let summary = [
|
|
304
|
-
theme.fg("success", `Found ${details.count} ${noun}`),
|
|
305
|
-
theme.fg("dim", ` for ${JSON.stringify(details.libraryName)}`),
|
|
306
|
-
].join("");
|
|
307
|
-
|
|
308
|
-
if (details.truncation?.truncated) {
|
|
309
|
-
summary += theme.fg("warning", " [truncated]");
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return summary;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function buildFetchSummary(
|
|
316
|
-
details: FetchDetails | undefined,
|
|
317
|
-
theme: { fg: (color: "success" | "warning" | "dim", text: string) => string },
|
|
318
|
-
): string {
|
|
319
|
-
if (!details) {
|
|
320
|
-
return theme.fg("success", "Fetched Context7 docs");
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const format = details.raw ? "raw JSON" : "Markdown";
|
|
324
|
-
let summary = [
|
|
325
|
-
theme.fg("success", `Fetched ${format}`),
|
|
326
|
-
theme.fg(
|
|
327
|
-
"dim",
|
|
328
|
-
` for ${details.libraryId} (${details.chars.toLocaleString()} chars, ${details.lines.toLocaleString()} lines)`,
|
|
329
|
-
),
|
|
330
|
-
].join("");
|
|
331
|
-
|
|
332
|
-
if (details.truncation?.truncated) {
|
|
333
|
-
summary += theme.fg("warning", " [truncated]");
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return summary;
|
|
12
|
+
registerWebDocsSearchTool(pi);
|
|
13
|
+
registerWebDocsFetchTool(pi);
|
|
337
14
|
}
|
package/src/tool/tool-specs.ts
CHANGED
|
@@ -1,11 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
import { type Static, type TSchema, Type } from "typebox";
|
|
3
|
-
import { FETCH_TIMEOUT_MAX_MS } from "../fetch.ts";
|
|
4
|
-
import { MODEL_OUTPUT_LIMIT_DESCRIPTION } from "./output.ts";
|
|
1
|
+
// Compatibility aggregator for the per-tool web specs and prompt surfaces.
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import type { TSchema } from "typebox";
|
|
4
|
+
import {
|
|
5
|
+
toolDescription as docsFetchDescription,
|
|
6
|
+
promptGuidelines as docsFetchGuidelines,
|
|
7
|
+
promptSnippet as docsFetchSnippet,
|
|
8
|
+
} from "./web_docs_fetch/guidance.ts";
|
|
9
|
+
import {
|
|
10
|
+
WEB_DOCS_FETCH_TOOL_LABEL,
|
|
11
|
+
WEB_DOCS_FETCH_TOOL_NAME,
|
|
12
|
+
webDocsFetchParameters,
|
|
13
|
+
} from "./web_docs_fetch/spec.ts";
|
|
14
|
+
import {
|
|
15
|
+
toolDescription as searchDescription,
|
|
16
|
+
promptGuidelines as searchGuidelines,
|
|
17
|
+
promptSnippet as searchSnippet,
|
|
18
|
+
} from "./web_docs_search/guidance.ts";
|
|
19
|
+
import {
|
|
20
|
+
WEB_DOCS_SEARCH_TOOL_LABEL,
|
|
21
|
+
WEB_DOCS_SEARCH_TOOL_NAME,
|
|
22
|
+
webDocsSearchParameters,
|
|
23
|
+
} from "./web_docs_search/spec.ts";
|
|
24
|
+
import {
|
|
25
|
+
toolDescription as fetchDescription,
|
|
26
|
+
promptGuidelines as fetchGuidelines,
|
|
27
|
+
promptSnippet as fetchSnippet,
|
|
28
|
+
getWebFetchPromptSurface,
|
|
29
|
+
} from "./web_fetch_md/guidance.ts";
|
|
30
|
+
import {
|
|
31
|
+
WEB_FETCH_MD_TOOL_LABEL,
|
|
32
|
+
WEB_FETCH_MD_TOOL_NAME,
|
|
33
|
+
webFetchMdParameters,
|
|
34
|
+
} from "./web_fetch_md/spec.ts";
|
|
35
|
+
|
|
36
|
+
export type { WebDocsFetchInput } from "./web_docs_fetch/spec.ts";
|
|
37
|
+
export { WEB_DOCS_FETCH_TOOL_NAME } from "./web_docs_fetch/spec.ts";
|
|
38
|
+
export type { WebDocsSearchInput } from "./web_docs_search/spec.ts";
|
|
39
|
+
export { WEB_DOCS_SEARCH_TOOL_NAME } from "./web_docs_search/spec.ts";
|
|
40
|
+
export type { WebFetchMdInput, WebFetchOutputMode } from "./web_fetch_md/spec.ts";
|
|
41
|
+
export { WEB_FETCH_INLINE_MAX_CHARS, WEB_FETCH_MD_TOOL_NAME } from "./web_fetch_md/spec.ts";
|
|
9
42
|
|
|
10
43
|
export const WEB_TOOL_NAMES = [
|
|
11
44
|
WEB_FETCH_MD_TOOL_NAME,
|
|
@@ -14,63 +47,12 @@ export const WEB_TOOL_NAMES = [
|
|
|
14
47
|
] as const;
|
|
15
48
|
export type WebToolName = (typeof WEB_TOOL_NAMES)[number];
|
|
16
49
|
|
|
17
|
-
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
description: "auto, inline, or file output",
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const WebFetchMdParameters = Type.Object(
|
|
27
|
-
{
|
|
28
|
-
url: Type.String({ description: "Public http(s) URL" }),
|
|
29
|
-
output_mode: Type.Optional(OutputModeEnum),
|
|
30
|
-
abs_links: Type.Optional(Type.Boolean({ description: "Absolute links/images", default: true })),
|
|
31
|
-
timeout_ms: Type.Optional(
|
|
32
|
-
Type.Integer({
|
|
33
|
-
description: "Fetch timeout (ms)",
|
|
34
|
-
default: 30_000,
|
|
35
|
-
minimum: 0,
|
|
36
|
-
maximum: FETCH_TIMEOUT_MAX_MS,
|
|
37
|
-
}),
|
|
38
|
-
),
|
|
39
|
-
},
|
|
40
|
-
{ additionalProperties: false },
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const WebDocsSearchParameters = Type.Object(
|
|
44
|
-
{
|
|
45
|
-
library_name: Type.String({
|
|
46
|
-
description: "Library name (e.g. react, next.js, fastapi)",
|
|
47
|
-
}),
|
|
48
|
-
query: Type.String({
|
|
49
|
-
description: "Task/question for relevance ranking",
|
|
50
|
-
}),
|
|
51
|
-
},
|
|
52
|
-
{ additionalProperties: false },
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const WebDocsFetchParameters = Type.Object(
|
|
56
|
-
{
|
|
57
|
-
library_id: Type.String({
|
|
58
|
-
description: "Context7 ID (e.g. /facebook/react); search first if unknown",
|
|
59
|
-
}),
|
|
60
|
-
query: Type.String({ description: "Specific docs question" }),
|
|
61
|
-
raw: Type.Optional(
|
|
62
|
-
Type.Boolean({
|
|
63
|
-
description: "Return JSON snippets instead of Markdown",
|
|
64
|
-
default: false,
|
|
65
|
-
}),
|
|
66
|
-
),
|
|
67
|
-
},
|
|
68
|
-
{ additionalProperties: false },
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
export type WebFetchMdInput = Static<typeof WebFetchMdParameters>;
|
|
72
|
-
export type WebDocsSearchInput = Static<typeof WebDocsSearchParameters>;
|
|
73
|
-
export type WebDocsFetchInput = Static<typeof WebDocsFetchParameters>;
|
|
50
|
+
/** Prompt metadata sent to pi for a single web tool. */
|
|
51
|
+
export interface WebToolPromptSurface {
|
|
52
|
+
description: string;
|
|
53
|
+
promptSnippet: string;
|
|
54
|
+
promptGuidelines: string[];
|
|
55
|
+
}
|
|
74
56
|
|
|
75
57
|
export interface WebToolSpec {
|
|
76
58
|
name: WebToolName;
|
|
@@ -84,29 +66,27 @@ export interface WebToolSpec {
|
|
|
84
66
|
export const WEB_TOOL_SPECS = [
|
|
85
67
|
{
|
|
86
68
|
name: WEB_FETCH_MD_TOOL_NAME,
|
|
87
|
-
label:
|
|
88
|
-
description:
|
|
89
|
-
promptSnippet:
|
|
90
|
-
promptGuidelines:
|
|
91
|
-
parameters:
|
|
69
|
+
label: WEB_FETCH_MD_TOOL_LABEL,
|
|
70
|
+
description: fetchDescription,
|
|
71
|
+
promptSnippet: fetchSnippet,
|
|
72
|
+
promptGuidelines: fetchGuidelines,
|
|
73
|
+
parameters: webFetchMdParameters,
|
|
92
74
|
},
|
|
93
75
|
{
|
|
94
76
|
name: WEB_DOCS_SEARCH_TOOL_NAME,
|
|
95
|
-
label:
|
|
96
|
-
description:
|
|
97
|
-
promptSnippet:
|
|
98
|
-
promptGuidelines:
|
|
99
|
-
parameters:
|
|
77
|
+
label: WEB_DOCS_SEARCH_TOOL_LABEL,
|
|
78
|
+
description: searchDescription,
|
|
79
|
+
promptSnippet: searchSnippet,
|
|
80
|
+
promptGuidelines: searchGuidelines,
|
|
81
|
+
parameters: webDocsSearchParameters,
|
|
100
82
|
},
|
|
101
83
|
{
|
|
102
84
|
name: WEB_DOCS_FETCH_TOOL_NAME,
|
|
103
|
-
label:
|
|
104
|
-
description:
|
|
105
|
-
promptSnippet:
|
|
106
|
-
promptGuidelines:
|
|
107
|
-
|
|
108
|
-
],
|
|
109
|
-
parameters: WebDocsFetchParameters,
|
|
85
|
+
label: WEB_DOCS_FETCH_TOOL_LABEL,
|
|
86
|
+
description: docsFetchDescription,
|
|
87
|
+
promptSnippet: docsFetchSnippet,
|
|
88
|
+
promptGuidelines: docsFetchGuidelines,
|
|
89
|
+
parameters: webDocsFetchParameters,
|
|
110
90
|
},
|
|
111
91
|
] as const satisfies readonly WebToolSpec[];
|
|
112
92
|
|
|
@@ -115,3 +95,16 @@ export function getWebToolSpec(name: WebToolName): WebToolSpec {
|
|
|
115
95
|
if (!spec) throw new Error(`Unknown web tool: ${name}`);
|
|
116
96
|
return spec;
|
|
117
97
|
}
|
|
98
|
+
|
|
99
|
+
/** Runtime prompt surface, including environment-dependent guidance. */
|
|
100
|
+
export function getWebToolPromptSurface(name: WebToolName): WebToolPromptSurface {
|
|
101
|
+
if (name === WEB_FETCH_MD_TOOL_NAME) {
|
|
102
|
+
return getWebFetchPromptSurface();
|
|
103
|
+
}
|
|
104
|
+
const spec = getWebToolSpec(name);
|
|
105
|
+
return {
|
|
106
|
+
description: spec.description,
|
|
107
|
+
promptSnippet: spec.promptSnippet,
|
|
108
|
+
promptGuidelines: [...spec.promptGuidelines],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolResult,
|
|
3
|
+
AgentToolUpdateCallback,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { getContext } from "../../context7-client.ts";
|
|
7
|
+
import { limitModelVisibleOutput } from "../result.ts";
|
|
8
|
+
import { buildFetchResult, type FetchDetails } from "./result.ts";
|
|
9
|
+
import type { WebDocsFetchInput } from "./spec.ts";
|
|
10
|
+
|
|
11
|
+
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
12
|
+
export async function runFetch(
|
|
13
|
+
_toolCallId: string,
|
|
14
|
+
params: unknown,
|
|
15
|
+
signal: AbortSignal | undefined,
|
|
16
|
+
onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined,
|
|
17
|
+
_ctx: ExtensionContext,
|
|
18
|
+
): Promise<AgentToolResult<FetchDetails>> {
|
|
19
|
+
const input = (params ?? {}) as WebDocsFetchInput;
|
|
20
|
+
const libraryId = input.library_id?.trim();
|
|
21
|
+
const query = input.query?.trim();
|
|
22
|
+
const raw = Boolean(input.raw);
|
|
23
|
+
|
|
24
|
+
if (!libraryId) throw new Error("'library_id' parameter is required");
|
|
25
|
+
if (!query) throw new Error("'query' parameter is required");
|
|
26
|
+
|
|
27
|
+
onUpdate?.({
|
|
28
|
+
content: [{ type: "text", text: `Fetching Context7 docs for ${libraryId}...` }],
|
|
29
|
+
details: { libraryId, raw },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const requestOptions = signal ? { signal } : undefined;
|
|
33
|
+
const content = await getContext(query, libraryId, raw, requestOptions);
|
|
34
|
+
const textContent = typeof content === "string" ? content : JSON.stringify(content, null, 2);
|
|
35
|
+
const output = await limitModelVisibleOutput(textContent, {
|
|
36
|
+
tempPrefix: "web-docs-fetch",
|
|
37
|
+
suffix: raw ? ".json" : ".md",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return buildFetchResult(libraryId, raw, textContent, output);
|
|
41
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { MODEL_OUTPUT_LIMIT_DESCRIPTION } from "../result.ts";
|
|
2
|
+
|
|
3
|
+
export const toolDescription = `Fetch focused Context7 docs for a known Context7 library_id. Markdown by default; raw=true returns JSON snippets. Search first if unknown. ${MODEL_OUTPUT_LIMIT_DESCRIPTION}`;
|
|
4
|
+
|
|
5
|
+
export const promptSnippet = "web_docs_fetch: focused Context7 docs";
|
|
6
|
+
|
|
7
|
+
export const promptGuidelines = [
|
|
8
|
+
"Use web_docs_fetch with a known library_id and narrow query; raw only for JSON.",
|
|
9
|
+
];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { promptGuidelines, promptSnippet, toolDescription } from "./guidance.ts";
|
|
3
|
+
import { renderFetchCall, renderFetchResult } from "./render.ts";
|
|
4
|
+
import { webDocsFetchSpec } from "./spec.ts";
|
|
5
|
+
|
|
6
|
+
/** Register the web_docs_fetch tool. */
|
|
7
|
+
export function registerWebDocsFetchTool(pi: ExtensionAPI): void {
|
|
8
|
+
pi.registerTool({
|
|
9
|
+
...webDocsFetchSpec,
|
|
10
|
+
description: toolDescription,
|
|
11
|
+
promptSnippet,
|
|
12
|
+
promptGuidelines: [...promptGuidelines],
|
|
13
|
+
renderCall: renderFetchCall,
|
|
14
|
+
renderResult: renderFetchResult,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { renderCollapsibleTextResult, renderToolCall } from "../render.ts";
|
|
3
|
+
import type { FetchDetails } from "./result.ts";
|
|
4
|
+
import { WEB_DOCS_FETCH_TOOL_NAME, type WebDocsFetchInput } from "./spec.ts";
|
|
5
|
+
|
|
6
|
+
/** Transcript renderer for web_docs_fetch tool calls. */
|
|
7
|
+
export function renderFetchCall(args: unknown, theme: Theme) {
|
|
8
|
+
const input = (args ?? {}) as WebDocsFetchInput;
|
|
9
|
+
const libraryId = typeof input.library_id === "string" ? input.library_id : "";
|
|
10
|
+
const query = typeof input.query === "string" ? truncatePreview(input.query) : undefined;
|
|
11
|
+
return renderToolCall(WEB_DOCS_FETCH_TOOL_NAME, libraryId, theme, query);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Transcript renderer for web_docs_fetch tool results. */
|
|
15
|
+
export function renderFetchResult(
|
|
16
|
+
result: { content: Array<{ type: string; text?: string }>; details?: unknown },
|
|
17
|
+
{ expanded, isPartial }: { expanded: boolean; isPartial: boolean },
|
|
18
|
+
theme: Theme,
|
|
19
|
+
) {
|
|
20
|
+
if (isPartial) {
|
|
21
|
+
return renderCollapsibleTextResult({
|
|
22
|
+
summary: theme.fg("warning", "Fetching Context7 docs..."),
|
|
23
|
+
expanded,
|
|
24
|
+
theme,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const details = result.details as FetchDetails | undefined;
|
|
29
|
+
const summary = buildFetchSummary(details, theme);
|
|
30
|
+
const content = result.content.find((item) => item.type === "text");
|
|
31
|
+
const body = content?.type === "text" ? content.text : undefined;
|
|
32
|
+
|
|
33
|
+
return renderCollapsibleTextResult({
|
|
34
|
+
summary,
|
|
35
|
+
body,
|
|
36
|
+
expanded,
|
|
37
|
+
theme,
|
|
38
|
+
fullOutputPath: details?.fullOutputPath,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function truncatePreview(text: string, maxChars = 48): string {
|
|
43
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
44
|
+
if (compact.length <= maxChars) return compact;
|
|
45
|
+
return `${compact.slice(0, maxChars - 1).trimEnd()}…`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildFetchSummary(
|
|
49
|
+
details: FetchDetails | undefined,
|
|
50
|
+
theme: { fg: (color: "success" | "warning" | "dim", text: string) => string },
|
|
51
|
+
): string {
|
|
52
|
+
if (!details) {
|
|
53
|
+
return theme.fg("success", "Fetched Context7 docs");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const format = details.raw ? "raw JSON" : "Markdown";
|
|
57
|
+
let summary = [
|
|
58
|
+
theme.fg("success", `Fetched ${format}`),
|
|
59
|
+
theme.fg(
|
|
60
|
+
"dim",
|
|
61
|
+
` for ${details.libraryId} (${details.chars.toLocaleString()} chars, ${details.lines.toLocaleString()} lines)`,
|
|
62
|
+
),
|
|
63
|
+
].join("");
|
|
64
|
+
|
|
65
|
+
if (details.truncation?.truncated) {
|
|
66
|
+
summary += theme.fg("warning", " [truncated]");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return summary;
|
|
70
|
+
}
|