@mrclrchtr/supi-web 1.14.3 → 1.16.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/README.md +8 -4
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +1 -1
- package/package.json +10 -2
- package/src/api.ts +10 -0
- package/src/context7-client.ts +13 -3
- package/src/docs.ts +295 -156
- package/src/fetch.ts +174 -92
- package/src/index.ts +10 -0
- package/src/temp-file.ts +6 -5
- package/src/tool/guidance.ts +34 -0
- package/src/tool/output.ts +50 -0
- package/src/tool/render.ts +63 -0
- package/src/tool/tool-specs.ts +107 -0
- package/src/web.ts +150 -78
- package/src/tool/web-docs-fetch-guidance.ts +0 -11
- package/src/tool/web-docs-search-guidance.ts +0 -10
- package/src/tool/web-fetch-md-guidance.ts +0 -41
package/README.md
CHANGED
|
@@ -26,7 +26,10 @@ pi install ./packages/supi-web
|
|
|
26
26
|
|
|
27
27
|
## What you get
|
|
28
28
|
|
|
29
|
-
After install, pi gets three tools
|
|
29
|
+
After install, pi gets three tools.
|
|
30
|
+
|
|
31
|
+
In PI's TUI, these tools now render a collapsed summary by default; expand the tool row to reveal the full inline output.
|
|
32
|
+
|
|
30
33
|
|
|
31
34
|
| Tool | Purpose |
|
|
32
35
|
|------|---------|
|
|
@@ -50,7 +53,7 @@ Fetches a public URL and returns clean Markdown.
|
|
|
50
53
|
### Output modes
|
|
51
54
|
|
|
52
55
|
- **`auto`** — returns Markdown inline if ≤15,000 characters; otherwise writes to a temporary file and returns the path
|
|
53
|
-
- **`inline`** —
|
|
56
|
+
- **`inline`** — returns Markdown inline, truncated to PI's model-visible output limit when necessary
|
|
54
57
|
- **`file`** — always writes to a temporary file and returns the path
|
|
55
58
|
|
|
56
59
|
### Behavior
|
|
@@ -58,6 +61,7 @@ Fetches a public URL and returns clean Markdown.
|
|
|
58
61
|
- Only accepts real `http://` or `https://` URLs
|
|
59
62
|
- Access-controlled pages (login, paywall) should be skipped — ask the user for an allowed source instead
|
|
60
63
|
- Plain-text responses are wrapped in fenced code blocks
|
|
64
|
+
- Model-visible inline output is truncated to 2,000 lines or 50KB; full truncated output is saved to a temp file
|
|
61
65
|
- Links and images are absolutized by default; set `abs_links: false` to keep them relative
|
|
62
66
|
|
|
63
67
|
## `web_docs_search`
|
|
@@ -71,7 +75,7 @@ Searches Context7 for library IDs before fetching documentation.
|
|
|
71
75
|
| `library_name` | string | ✓ | Library name to search for (e.g. `"react"`, `"next.js"`) |
|
|
72
76
|
| `query` | string | ✓ | What you're trying to do — used for relevance ranking |
|
|
73
77
|
|
|
74
|
-
Results return as a Markdown table with library ID, name, description, trust score, benchmark score, snippet count, and
|
|
78
|
+
Results return as a compact Markdown table with library ID, name, description, trust score, benchmark score, snippet count, and shortened version lists (top 10 matches shown).
|
|
75
79
|
|
|
76
80
|
## `web_docs_fetch`
|
|
77
81
|
|
|
@@ -85,7 +89,7 @@ Retrieves documentation context for a known Context7 library.
|
|
|
85
89
|
| `query` | string | ✓ | — | Specific question about the library |
|
|
86
90
|
| `raw` | boolean | — | `false` | When `true`, returns JSON-serialized snippet objects instead of Markdown |
|
|
87
91
|
|
|
88
|
-
Default mode returns pre-formatted Markdown. Set `raw: true` when you need structured JSON for programmatic use.
|
|
92
|
+
Default mode returns pre-formatted Markdown. Set `raw: true` when you need structured JSON for programmatic use. Large model-visible responses are truncated to 2,000 lines or 50KB with the full response saved to a temp file.
|
|
89
93
|
|
|
90
94
|
## Typical workflow
|
|
91
95
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "SuPi Web extension — fetch web pages as clean Markdown (web_fetch_md) and library docs via Context7 (web_docs_search, web_docs_fetch)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,19 +35,27 @@
|
|
|
35
35
|
"@mozilla/readability": "^0.6.0",
|
|
36
36
|
"turndown": "^7.2.0",
|
|
37
37
|
"turndown-plugin-gfm": "^1.0.2",
|
|
38
|
-
"@mrclrchtr/supi-core": "1.
|
|
38
|
+
"@mrclrchtr/supi-core": "1.16.0"
|
|
39
39
|
},
|
|
40
40
|
"bundledDependencies": [
|
|
41
41
|
"@mrclrchtr/supi-core"
|
|
42
42
|
],
|
|
43
43
|
"peerDependencies": {
|
|
44
|
+
"@earendil-works/pi-ai": "*",
|
|
44
45
|
"@earendil-works/pi-coding-agent": "*",
|
|
46
|
+
"@earendil-works/pi-tui": "*",
|
|
45
47
|
"typebox": "*"
|
|
46
48
|
},
|
|
47
49
|
"peerDependenciesMeta": {
|
|
50
|
+
"@earendil-works/pi-ai": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
48
53
|
"@earendil-works/pi-coding-agent": {
|
|
49
54
|
"optional": true
|
|
50
55
|
},
|
|
56
|
+
"@earendil-works/pi-tui": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
51
59
|
"typebox": {
|
|
52
60
|
"optional": true
|
|
53
61
|
}
|
package/src/api.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* SuPi Web extension — public API exports.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
export type { Context7RequestOptions } from "./context7-client.ts";
|
|
5
6
|
export { htmlToMarkdown, wrapAsCodeBlock } from "./convert.ts";
|
|
6
7
|
export { default as docsExtension } from "./docs.ts";
|
|
7
8
|
export {
|
|
@@ -11,4 +12,13 @@ export {
|
|
|
11
12
|
fetchWithNegotiation,
|
|
12
13
|
isValidHttpUrl,
|
|
13
14
|
} from "./fetch.ts";
|
|
15
|
+
export {
|
|
16
|
+
WEB_FETCH_INLINE_MAX_CHARS,
|
|
17
|
+
WEB_TOOL_NAMES,
|
|
18
|
+
WEB_TOOL_SPECS,
|
|
19
|
+
type WebDocsFetchInput,
|
|
20
|
+
type WebDocsSearchInput,
|
|
21
|
+
type WebFetchMdInput,
|
|
22
|
+
type WebToolName,
|
|
23
|
+
} from "./tool/tool-specs.ts";
|
|
14
24
|
export { default } from "./web.ts";
|
package/src/context7-client.ts
CHANGED
|
@@ -31,6 +31,11 @@ export interface DocSnippet {
|
|
|
31
31
|
source: string;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** Request options shared by Context7 REST calls. */
|
|
35
|
+
export interface Context7RequestOptions {
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
function authHeaders(): Record<string, string> {
|
|
35
40
|
const apiKey = process.env.CONTEXT7_API_KEY;
|
|
36
41
|
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
|
@@ -86,12 +91,16 @@ function mapSearchResult(r: ApiSearchResult): SearchResult {
|
|
|
86
91
|
};
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
export async function searchLibrary(
|
|
94
|
+
export async function searchLibrary(
|
|
95
|
+
query: string,
|
|
96
|
+
libraryName: string,
|
|
97
|
+
options: Context7RequestOptions = {},
|
|
98
|
+
): Promise<SearchResult[]> {
|
|
90
99
|
const url = new URL(`${BASE_URL}/v2/libs/search`);
|
|
91
100
|
url.searchParams.set("query", query);
|
|
92
101
|
url.searchParams.set("libraryName", libraryName);
|
|
93
102
|
|
|
94
|
-
const response = await fetch(url, { headers: authHeaders() });
|
|
103
|
+
const response = await fetch(url, { headers: authHeaders(), signal: options.signal });
|
|
95
104
|
|
|
96
105
|
if (!response.ok) {
|
|
97
106
|
throw new Context7Error(await parseErrorResponse(response));
|
|
@@ -105,12 +114,13 @@ export async function getContext(
|
|
|
105
114
|
query: string,
|
|
106
115
|
libraryId: string,
|
|
107
116
|
raw?: boolean,
|
|
117
|
+
options: Context7RequestOptions = {},
|
|
108
118
|
): Promise<string | DocSnippet[]> {
|
|
109
119
|
const url = new URL(`${BASE_URL}/v2/context`);
|
|
110
120
|
url.searchParams.set("query", query);
|
|
111
121
|
url.searchParams.set("libraryId", libraryId);
|
|
112
122
|
|
|
113
|
-
const response = await fetch(url, { headers: authHeaders() });
|
|
123
|
+
const response = await fetch(url, { headers: authHeaders(), signal: options.signal });
|
|
114
124
|
|
|
115
125
|
if (!response.ok) {
|
|
116
126
|
throw new Context7Error(await parseErrorResponse(response));
|
package/src/docs.ts
CHANGED
|
@@ -1,198 +1,337 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SuPi Web Context7 extension — registers web_docs_search and web_docs_fetch tools.
|
|
3
3
|
*
|
|
4
|
-
* Uses @upstash/context7-sdk for up-to-date library documentation lookups.
|
|
5
4
|
* API key is read automatically from the CONTEXT7_API_KEY environment variable.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
import type {
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import type {
|
|
8
|
+
AgentToolResult,
|
|
9
|
+
AgentToolUpdateCallback,
|
|
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";
|
|
11
18
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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];
|
|
21
43
|
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const FETCH_TOOL_LABEL = "Web Docs Fetch";
|
|
44
|
+
const MAX_SEARCH_RESULTS = 10;
|
|
45
|
+
const MAX_DESCRIPTION_CHARS = 120;
|
|
46
|
+
const MAX_VERSION_COUNT = 5;
|
|
26
47
|
|
|
27
48
|
export default function docsExtension(pi: ExtensionAPI): void {
|
|
49
|
+
const searchSpec = getWebToolSpec(WEB_DOCS_SEARCH_TOOL_NAME);
|
|
50
|
+
const searchSurface = getWebToolPromptSurface(WEB_DOCS_SEARCH_TOOL_NAME);
|
|
28
51
|
pi.registerTool({
|
|
29
|
-
name:
|
|
30
|
-
label:
|
|
31
|
-
description:
|
|
32
|
-
promptSnippet:
|
|
33
|
-
promptGuidelines:
|
|
34
|
-
parameters:
|
|
35
|
-
library_name: Type.String({
|
|
36
|
-
description: "Library name to search for (e.g. react, next.js, fastapi)",
|
|
37
|
-
}),
|
|
38
|
-
query: Type.String({
|
|
39
|
-
description: "What you're trying to do — used for relevance ranking of results",
|
|
40
|
-
}),
|
|
41
|
-
}),
|
|
52
|
+
name: searchSpec.name,
|
|
53
|
+
label: searchSpec.label,
|
|
54
|
+
description: searchSurface.description,
|
|
55
|
+
promptSnippet: searchSurface.promptSnippet,
|
|
56
|
+
promptGuidelines: searchSurface.promptGuidelines,
|
|
57
|
+
parameters: searchSpec.parameters,
|
|
42
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
|
+
},
|
|
43
88
|
});
|
|
44
89
|
|
|
90
|
+
const fetchSpec = getWebToolSpec(WEB_DOCS_FETCH_TOOL_NAME);
|
|
91
|
+
const fetchSurface = getWebToolPromptSurface(WEB_DOCS_FETCH_TOOL_NAME);
|
|
45
92
|
pi.registerTool({
|
|
46
|
-
name:
|
|
47
|
-
label:
|
|
48
|
-
description:
|
|
49
|
-
promptSnippet:
|
|
50
|
-
promptGuidelines:
|
|
51
|
-
parameters:
|
|
52
|
-
library_id: Type.String({
|
|
53
|
-
description:
|
|
54
|
-
"Context7 library ID (e.g. /facebook/react, /vercel/next.js). Find it via web_docs_search.",
|
|
55
|
-
}),
|
|
56
|
-
query: Type.String({
|
|
57
|
-
description: "Specific question about the library",
|
|
58
|
-
}),
|
|
59
|
-
raw: Type.Optional(
|
|
60
|
-
Type.Boolean({
|
|
61
|
-
description:
|
|
62
|
-
"When true, returns JSON-serialized snippet objects instead of plain text Markdown",
|
|
63
|
-
default: false,
|
|
64
|
-
}),
|
|
65
|
-
),
|
|
66
|
-
}),
|
|
93
|
+
name: fetchSpec.name,
|
|
94
|
+
label: fetchSpec.label,
|
|
95
|
+
description: fetchSurface.description,
|
|
96
|
+
promptSnippet: fetchSurface.promptSnippet,
|
|
97
|
+
promptGuidelines: fetchSurface.promptGuidelines,
|
|
98
|
+
parameters: fetchSpec.parameters,
|
|
67
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
|
+
},
|
|
68
128
|
});
|
|
69
129
|
}
|
|
70
130
|
|
|
71
131
|
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
72
132
|
async function runSearch(
|
|
73
133
|
_toolCallId: string,
|
|
74
|
-
params:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
_ctx:
|
|
78
|
-
): Promise<{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}> {
|
|
83
|
-
const libraryName = (params.library_name as string | undefined)?.trim();
|
|
84
|
-
const query = (params.query as string | undefined)?.trim();
|
|
85
|
-
|
|
86
|
-
if (!libraryName) {
|
|
87
|
-
return {
|
|
88
|
-
content: [{ type: "text", text: "Error: 'library_name' parameter is required" }],
|
|
89
|
-
isError: true,
|
|
90
|
-
details: {},
|
|
91
|
-
};
|
|
92
|
-
}
|
|
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();
|
|
93
142
|
|
|
94
|
-
if (!
|
|
95
|
-
|
|
96
|
-
content: [{ type: "text", text: "Error: 'query' parameter is required" }],
|
|
97
|
-
isError: true,
|
|
98
|
-
details: {},
|
|
99
|
-
};
|
|
100
|
-
}
|
|
143
|
+
if (!libraryName) throw new Error("'library_name' parameter is required");
|
|
144
|
+
if (!query) throw new Error("'query' parameter is required");
|
|
101
145
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
content: [
|
|
108
|
-
{
|
|
109
|
-
type: "text",
|
|
110
|
-
text: `No libraries found for "${libraryName}". Try a different search term.`,
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
details: { count: 0, libraryName },
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const rows = results.map(
|
|
118
|
-
(lib) =>
|
|
119
|
-
`| **${escapeMd(lib.name)}** | \`${escapeMd(lib.id)}\` | ${escapeMd(lib.description ?? "")} | ${lib.trustScore ?? ""} | ${lib.benchmarkScore ?? ""} | ${lib.totalSnippets ?? ""} | ${lib.versions ? lib.versions.join(", ") : ""} |`,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
const markdown = [
|
|
123
|
-
`Found **${results.length}** library/libraries matching "${libraryName}":`,
|
|
124
|
-
"",
|
|
125
|
-
"| Name | ID | Description | Trust | Benchmark | Snippets | Versions |",
|
|
126
|
-
"|---|---|---|---|---|---|---|",
|
|
127
|
-
...rows,
|
|
128
|
-
"",
|
|
129
|
-
`> Use \`web_docs_fetch\` with the library ID to retrieve documentation.`,
|
|
130
|
-
].join("\n");
|
|
146
|
+
onUpdate?.({
|
|
147
|
+
content: [{ type: "text", text: `Searching Context7 for ${libraryName}...` }],
|
|
148
|
+
details: { libraryName },
|
|
149
|
+
});
|
|
131
150
|
|
|
151
|
+
const requestOptions = signal ? { signal } : undefined;
|
|
152
|
+
const results = await searchLibrary(query, libraryName, requestOptions);
|
|
153
|
+
|
|
154
|
+
if (results.length === 0) {
|
|
132
155
|
return {
|
|
133
|
-
content: [
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
isError: true,
|
|
141
|
-
details: { libraryName, error: String(err) },
|
|
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 },
|
|
142
163
|
};
|
|
143
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
|
+
};
|
|
144
181
|
}
|
|
145
182
|
|
|
146
183
|
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
147
184
|
async function runFetch(
|
|
148
185
|
_toolCallId: string,
|
|
149
|
-
params:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
_ctx:
|
|
153
|
-
): Promise<{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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");
|
|
168
293
|
}
|
|
169
294
|
|
|
170
|
-
if (
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
};
|
|
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("");
|
|
176
300
|
}
|
|
177
301
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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("");
|
|
181
307
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
details: { libraryId, raw },
|
|
185
|
-
};
|
|
186
|
-
} catch (err) {
|
|
187
|
-
const message = err instanceof Context7Error ? err.message : `Unexpected error: ${String(err)}`;
|
|
188
|
-
return {
|
|
189
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
190
|
-
isError: true,
|
|
191
|
-
details: { libraryId, error: String(err) },
|
|
192
|
-
};
|
|
308
|
+
if (details.truncation?.truncated) {
|
|
309
|
+
summary += theme.fg("warning", " [truncated]");
|
|
193
310
|
}
|
|
311
|
+
|
|
312
|
+
return summary;
|
|
194
313
|
}
|
|
195
314
|
|
|
196
|
-
function
|
|
197
|
-
|
|
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;
|
|
198
337
|
}
|