@mrclrchtr/supi-web 1.14.2 → 1.15.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 +12 -3
- package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +1 -1
- package/package.json +24 -5
- 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/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
|
}
|