pi-web-kit 0.2.1 → 0.2.3
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/CHANGELOG.md +17 -1
- package/README.md +3 -3
- package/extensions/index.ts +194 -17
- package/package.json +7 -7
- package/src/config.ts +11 -4
- package/src/providers/fallback.ts +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.2.
|
|
3
|
+
## 0.2.3 - 2026-07-17
|
|
4
4
|
|
|
5
5
|
All notable changes to this project will be documented in this file.
|
|
6
6
|
|
|
@@ -8,6 +8,22 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Honor project-local config only after Pi confirms project trust.
|
|
14
|
+
- Keep structured tool output valid JSON under the 50KB limit with continuation metadata.
|
|
15
|
+
- Derive opaque cache scopes from full API keys and hide internal cache keys from tool output.
|
|
16
|
+
- Prevent provider fetch results from being assigned to more than one requested URL.
|
|
17
|
+
- Remove the stale package-local lockfile in favor of the monorepo root lockfile.
|
|
18
|
+
|
|
19
|
+
## [0.2.2] - 2026-07-01
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Update Pi core development dependencies for Pi 0.80 compatibility.
|
|
24
|
+
|
|
25
|
+
## [0.2.1] - 2026-06-14
|
|
26
|
+
|
|
11
27
|
## [0.2.0] - 2026-06-13
|
|
12
28
|
|
|
13
29
|
### Added
|
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ Tool schemas are tailored to the configured providers at startup/reload, so only
|
|
|
89
89
|
|
|
90
90
|
## Configuration
|
|
91
91
|
|
|
92
|
-
Resolution order: defaults < environment variables < global config < project config < CLI flags.
|
|
92
|
+
Resolution order: defaults < environment variables < global config < trusted project config < CLI flags. Project config is ignored unless Pi trusts the current project, including in print, JSON, and RPC modes.
|
|
93
93
|
|
|
94
94
|
### Environment variables
|
|
95
95
|
|
|
@@ -217,7 +217,7 @@ Finds practical code examples, implementation context, setup snippets, migration
|
|
|
217
217
|
| Max `numResults` | 20 |
|
|
218
218
|
| Max URL length | 2048 characters |
|
|
219
219
|
|
|
220
|
-
Cache keys include the provider, canonical URL, fetch-affecting parameters, relevant provider defaults, and
|
|
220
|
+
Cache keys include the provider, canonical URL, fetch-affecting parameters, relevant provider defaults, and an opaque SHA-256 API-key/account scope. Internal cache keys are never returned in tool output. `refresh: true` bypasses and replaces the cached entry.
|
|
221
221
|
|
|
222
222
|
## Privacy and security
|
|
223
223
|
|
|
@@ -236,7 +236,7 @@ Report security issues privately. See [SECURITY.md](SECURITY.md).
|
|
|
236
236
|
| Invalid URL / scheme / credentials error | URL validation rejected the input. | Use an absolute `http:` or `https:` URL without username/password credentials. |
|
|
237
237
|
| Timeout error | Provider request exceeded its timeout. | Retry, reduce URL count, or switch provider. |
|
|
238
238
|
| No content returned | Provider returned no matching content or a redirected/canonicalized response could not be mapped. | Retry with `refresh: true`, fetch a single URL, or switch provider. |
|
|
239
|
-
| Large page is truncated | Tool output is bounded
|
|
239
|
+
| Large page is truncated | Tool output is bounded to valid JSON under 50KB. | Continue with the returned `range.nextOffset`. |
|
|
240
240
|
|
|
241
241
|
## Development
|
|
242
242
|
|
package/extensions/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
4
|
import { Type } from "typebox";
|
|
4
5
|
import { fetchCache, type CachedPage } from "../src/cache.js";
|
|
5
6
|
import { resolveConfig } from "../src/config.js";
|
|
6
7
|
import { DEFAULT_FETCH_LIMIT, DEFAULT_NUM_RESULTS, MAX_LIMIT, MAX_NUM_RESULTS, MAX_OFFSET, MAX_QUERY_COUNT, MAX_URL_COUNT, MULTI_FETCH_LIMIT } from "../src/limits.js";
|
|
7
|
-
import { truncateText } from "../src/http.js";
|
|
8
8
|
import { createCodeSearchProvider, createContext7Provider, createFetchProvider, createSearchProvider } from "../src/providers/index.js";
|
|
9
9
|
import { mapFetchResults } from "../src/providers/fallback.js";
|
|
10
10
|
import type { FetchProviderName, SearchProviderName, WebFetchResult } from "../src/types.js";
|
|
@@ -22,11 +22,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
22
22
|
type: "string",
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
let registeredConfig = "";
|
|
26
|
+
pi.on("session_start", (_event, ctx) => {
|
|
27
|
+
const startupConfig = runtimeConfig(pi, ctx.cwd, projectIsTrusted(ctx));
|
|
28
|
+
const signature = toolConfigSignature(startupConfig);
|
|
29
|
+
if (signature === registeredConfig) return;
|
|
30
|
+
registerTools(pi, startupConfig);
|
|
31
|
+
syncOptionalTools(pi, startupConfig);
|
|
32
|
+
registeredConfig = signature;
|
|
28
33
|
});
|
|
34
|
+
}
|
|
29
35
|
|
|
36
|
+
function registerTools(pi: ExtensionAPI, startupConfig: ReturnType<typeof resolveConfig>) {
|
|
30
37
|
pi.registerTool({
|
|
31
38
|
name: "web_search",
|
|
32
39
|
label: "Web Search",
|
|
@@ -40,7 +47,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
40
47
|
const numResults = parseInteger(params.numResults, DEFAULT_NUM_RESULTS, "numResults", 1, MAX_NUM_RESULTS);
|
|
41
48
|
if (queries.length === 0) throw new Error("web_search requires query or queries.");
|
|
42
49
|
|
|
43
|
-
const config =
|
|
50
|
+
const config = runtimeConfig(pi, ctx.cwd, projectIsTrusted(ctx));
|
|
44
51
|
assertProviderUnchanged("web_search", startupConfig.provider_search, config.provider_search);
|
|
45
52
|
const provider = createSearchProvider(config);
|
|
46
53
|
const grouped = [];
|
|
@@ -80,7 +87,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
80
87
|
throw new Error("web_fetch offset range reads require a single url, not urls.");
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
const config =
|
|
90
|
+
const config = runtimeConfig(pi, ctx.cwd, projectIsTrusted(ctx));
|
|
84
91
|
assertProviderUnchanged("web_fetch", startupConfig.provider_fetch, config.provider_fetch);
|
|
85
92
|
const progress = createProgress("fetch", config.provider_fetch, urls);
|
|
86
93
|
const result = await fetchWithCache(config.provider_fetch, params, urls, signal, config, (event) => {
|
|
@@ -110,7 +117,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
110
117
|
const libraryName = requiredString(params.libraryName, "libraryName");
|
|
111
118
|
const query = optionalString(params.query, "query") ?? libraryName;
|
|
112
119
|
const limit = parseInteger(params.limit, 10, "limit", 1, MAX_NUM_RESULTS);
|
|
113
|
-
const provider = createContext7Provider(runtimeConfig(pi, ctx.cwd));
|
|
120
|
+
const provider = createContext7Provider(runtimeConfig(pi, ctx.cwd, projectIsTrusted(ctx)));
|
|
114
121
|
const result = await provider.searchLibraries({ libraryName, query, fast: params.fast === true, limit }, signal);
|
|
115
122
|
return jsonToolResult(result);
|
|
116
123
|
},
|
|
@@ -127,7 +134,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
127
134
|
const params = rawParams as Record<string, any>;
|
|
128
135
|
const query = requiredString(params.query, "query");
|
|
129
136
|
const limit = parseInteger(params.limit, 10, "limit", 1, MAX_NUM_RESULTS);
|
|
130
|
-
const provider = createContext7Provider(runtimeConfig(pi, ctx.cwd));
|
|
137
|
+
const provider = createContext7Provider(runtimeConfig(pi, ctx.cwd, projectIsTrusted(ctx)));
|
|
131
138
|
let libraryId = optionalString(params.libraryId, "libraryId");
|
|
132
139
|
if (!libraryId) {
|
|
133
140
|
const libraryName = requiredString(params.libraryName, "libraryName");
|
|
@@ -153,7 +160,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
153
160
|
const params = rawParams as Record<string, any>;
|
|
154
161
|
const query = requiredString(params.query, "query");
|
|
155
162
|
const tokensNum = parseTokensNum(params.tokensNum);
|
|
156
|
-
const provider = createCodeSearchProvider(runtimeConfig(pi, ctx.cwd));
|
|
163
|
+
const provider = createCodeSearchProvider(runtimeConfig(pi, ctx.cwd, projectIsTrusted(ctx)));
|
|
157
164
|
const result = await provider.searchCode({ query, tokensNum }, signal);
|
|
158
165
|
return jsonToolResult(result);
|
|
159
166
|
},
|
|
@@ -161,6 +168,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
161
168
|
}
|
|
162
169
|
}
|
|
163
170
|
|
|
171
|
+
const OPTIONAL_TOOLS = ["library_search", "library_docs", "code_search"];
|
|
172
|
+
|
|
173
|
+
function toolConfigSignature(config: ReturnType<typeof resolveConfig>): string {
|
|
174
|
+
return JSON.stringify({
|
|
175
|
+
search: config.provider_search,
|
|
176
|
+
fetch: config.provider_fetch,
|
|
177
|
+
context7: !!config.apiKeys.context7,
|
|
178
|
+
exa: !!config.apiKeys.exa,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function syncOptionalTools(pi: ExtensionAPI, config: ReturnType<typeof resolveConfig>) {
|
|
183
|
+
if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
|
|
184
|
+
const enabled = [
|
|
185
|
+
...(config.apiKeys.context7 ? ["library_search", "library_docs"] : []),
|
|
186
|
+
...(config.apiKeys.exa ? ["code_search"] : []),
|
|
187
|
+
];
|
|
188
|
+
const active = pi.getActiveTools().filter((name) => !OPTIONAL_TOOLS.includes(name));
|
|
189
|
+
pi.setActiveTools([...new Set([...active, ...enabled])]);
|
|
190
|
+
}
|
|
191
|
+
|
|
164
192
|
type ProgressKind = "search" | "fetch";
|
|
165
193
|
type ProgressItem = { label: string; status: "pending" | "current" | "done" | "error"; note?: string; error?: string };
|
|
166
194
|
type WebProgress = { kind: ProgressKind; provider: string; total: number; completed: number; items: ProgressItem[] };
|
|
@@ -320,7 +348,7 @@ export async function fetchWithCache(providerName: FetchProviderName, params: Re
|
|
|
320
348
|
export function pageSlice(page: CachedPage, offset: number, limit: number, cached: boolean, refreshed: boolean) {
|
|
321
349
|
const total = page.content.length;
|
|
322
350
|
const content = page.content.slice(offset, offset + limit);
|
|
323
|
-
return { url: page.requestedUrl ?? page.url, fetchedUrl: page.url, title: page.title, content, format: page.format, cached, refreshed,
|
|
351
|
+
return { url: page.requestedUrl ?? page.url, fetchedUrl: page.url, title: page.title, content, format: page.format, cached, refreshed, range: { offset, limit, returned: content.length, total, truncated: offset > 0 || offset + content.length < total, hasPrevious: offset > 0, hasNext: offset + content.length < total, nextOffset: offset + content.length < total ? offset + content.length : undefined } };
|
|
324
352
|
}
|
|
325
353
|
|
|
326
354
|
export function buildCacheKey(provider: FetchProviderName, url: string, params: Record<string, any>, config?: any): string {
|
|
@@ -335,13 +363,163 @@ export function buildCacheKey(provider: FetchProviderName, url: string, params:
|
|
|
335
363
|
return `${provider}\0${scope}\0${JSON.stringify(affecting)}\0${canonical}`;
|
|
336
364
|
}
|
|
337
365
|
|
|
338
|
-
function
|
|
339
|
-
return
|
|
366
|
+
function projectIsTrusted(ctx: { isProjectTrusted?: () => boolean }): boolean {
|
|
367
|
+
return typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted() === true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function runtimeConfig(pi: ExtensionAPI, cwd: string, projectTrusted: boolean) {
|
|
371
|
+
return resolveConfig(
|
|
372
|
+
{ providerSearch: pi.getFlag("web-provider-search"), providerFetch: pi.getFlag("web-provider-fetch") },
|
|
373
|
+
cwd,
|
|
374
|
+
process.env,
|
|
375
|
+
{ includeProject: projectTrusted },
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const MAX_OUTPUT_BYTES = 50_000;
|
|
380
|
+
|
|
381
|
+
export function jsonToolResult(result: unknown) {
|
|
382
|
+
const bounded = boundStructuredResult(result);
|
|
383
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(bounded) }], details: boundedDetails(bounded) };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function boundStructuredResult(result: unknown): unknown {
|
|
387
|
+
if (Buffer.byteLength(JSON.stringify(result)) <= MAX_OUTPUT_BYTES) return result;
|
|
388
|
+
|
|
389
|
+
if (isFetchResult(result)) {
|
|
390
|
+
const metadataBounded = limitFetchMetadata(result, 1_000);
|
|
391
|
+
let low = 0;
|
|
392
|
+
let high = Math.max(...metadataBounded.results.map((item: any) => typeof item.content === "string" ? item.content.length : 0));
|
|
393
|
+
const emptyContent = fetchResultWithContentLimit(metadataBounded, 0);
|
|
394
|
+
if (Buffer.byteLength(JSON.stringify(emptyContent)) > MAX_OUTPUT_BYTES) {
|
|
395
|
+
return { truncated: true, message: "Fetch metadata exceeded 50KB; retry with fewer URLs." };
|
|
396
|
+
}
|
|
397
|
+
let best: unknown = emptyContent;
|
|
398
|
+
while (low <= high) {
|
|
399
|
+
const middle = Math.floor((low + high) / 2);
|
|
400
|
+
const candidate = fetchResultWithContentLimit(metadataBounded, middle);
|
|
401
|
+
if (Buffer.byteLength(JSON.stringify(candidate)) <= MAX_OUTPUT_BYTES) {
|
|
402
|
+
best = candidate;
|
|
403
|
+
low = middle + 1;
|
|
404
|
+
} else {
|
|
405
|
+
high = middle - 1;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return best;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (isSearchResult(result)) return boundSearchResult(result);
|
|
412
|
+
|
|
413
|
+
const compact = limitStrings(result, 1_000);
|
|
414
|
+
if (Buffer.byteLength(JSON.stringify(compact)) <= MAX_OUTPUT_BYTES) return compact;
|
|
415
|
+
return { truncated: true, message: "Structured result exceeded 50KB; refine the request or use smaller limits." };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function isFetchResult(value: any): value is { provider?: string; results: any[] } {
|
|
419
|
+
return !!value && Array.isArray(value.results) && value.results.some((item: any) => typeof item?.content === "string" && item?.range);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function isSearchResult(value: any): value is { provider?: string; queries: Array<{ query?: string; results?: any[] }> } {
|
|
423
|
+
return !!value && Array.isArray(value.queries) && value.queries.every((group: any) => Array.isArray(group?.results));
|
|
340
424
|
}
|
|
341
425
|
|
|
342
|
-
function
|
|
343
|
-
const
|
|
344
|
-
|
|
426
|
+
function boundSearchResult(value: { provider?: string; queries: Array<{ query?: string; results?: any[] }> }) {
|
|
427
|
+
const compact = limitStrings(value, 1_000) as typeof value;
|
|
428
|
+
const maxSnippet = Math.max(0, ...compact.queries.flatMap((group) => (group.results ?? []).map((item) => typeof item?.snippet === "string" ? item.snippet.length : 0)));
|
|
429
|
+
let low = 0;
|
|
430
|
+
let high = maxSnippet;
|
|
431
|
+
let best = searchResultWithLimits(compact, 0);
|
|
432
|
+
while (low <= high) {
|
|
433
|
+
const middle = Math.floor((low + high) / 2);
|
|
434
|
+
const candidate = searchResultWithLimits(compact, middle);
|
|
435
|
+
if (Buffer.byteLength(JSON.stringify(candidate)) <= MAX_OUTPUT_BYTES) {
|
|
436
|
+
best = candidate;
|
|
437
|
+
low = middle + 1;
|
|
438
|
+
} else {
|
|
439
|
+
high = middle - 1;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (Buffer.byteLength(JSON.stringify(best)) <= MAX_OUTPUT_BYTES) return best;
|
|
443
|
+
|
|
444
|
+
low = 0;
|
|
445
|
+
high = Math.max(0, ...compact.queries.map((group) => group.results?.length ?? 0));
|
|
446
|
+
while (low <= high) {
|
|
447
|
+
const middle = Math.floor((low + high) / 2);
|
|
448
|
+
const candidate = searchResultWithLimits(compact, 0, middle);
|
|
449
|
+
if (Buffer.byteLength(JSON.stringify(candidate)) <= MAX_OUTPUT_BYTES) {
|
|
450
|
+
best = candidate;
|
|
451
|
+
low = middle + 1;
|
|
452
|
+
} else {
|
|
453
|
+
high = middle - 1;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return Buffer.byteLength(JSON.stringify(best)) <= MAX_OUTPUT_BYTES
|
|
457
|
+
? best
|
|
458
|
+
: { truncated: true, message: "Search metadata exceeded 50KB; retry with fewer queries or results." };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function searchResultWithLimits(value: { provider?: string; queries: Array<{ query?: string; results?: any[] }> }, snippetChars: number, resultsPerQuery?: number) {
|
|
462
|
+
return {
|
|
463
|
+
...value,
|
|
464
|
+
queries: value.queries.map((group) => ({
|
|
465
|
+
...group,
|
|
466
|
+
results: (group.results ?? []).slice(0, resultsPerQuery).map((item) => {
|
|
467
|
+
if (typeof item?.snippet !== "string") return item;
|
|
468
|
+
if (snippetChars === 0) {
|
|
469
|
+
const { snippet: _snippet, ...metadata } = item;
|
|
470
|
+
return metadata;
|
|
471
|
+
}
|
|
472
|
+
return { ...item, snippet: safePrefix(item.snippet, snippetChars) };
|
|
473
|
+
}),
|
|
474
|
+
})),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function limitFetchMetadata(value: { provider?: string; results: any[] }, maxChars: number) {
|
|
479
|
+
return {
|
|
480
|
+
...value,
|
|
481
|
+
results: value.results.map((item: any) => {
|
|
482
|
+
if (typeof item.content !== "string") return limitStrings(item, maxChars);
|
|
483
|
+
const content = item.content;
|
|
484
|
+
return { ...(limitStrings(item, maxChars) as Record<string, unknown>), content };
|
|
485
|
+
}),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function fetchResultWithContentLimit(value: { provider?: string; results: any[] }, maxChars: number) {
|
|
490
|
+
return {
|
|
491
|
+
...value,
|
|
492
|
+
results: value.results.map((item: any) => {
|
|
493
|
+
if (typeof item.content !== "string" || !item.range) return item;
|
|
494
|
+
const content = safePrefix(item.content, maxChars);
|
|
495
|
+
const returned = content.length;
|
|
496
|
+
const hasNext = item.range.offset + returned < item.range.total;
|
|
497
|
+
return {
|
|
498
|
+
...item,
|
|
499
|
+
content,
|
|
500
|
+
range: {
|
|
501
|
+
...item.range,
|
|
502
|
+
returned,
|
|
503
|
+
truncated: item.range.offset > 0 || hasNext,
|
|
504
|
+
hasNext,
|
|
505
|
+
nextOffset: hasNext ? item.range.offset + returned : undefined,
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function safePrefix(value: string, maxChars: number): string {
|
|
513
|
+
let end = Math.min(value.length, maxChars);
|
|
514
|
+
if (end > 0 && /[\uD800-\uDBFF]/.test(value[end - 1])) end--;
|
|
515
|
+
return value.slice(0, end);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function limitStrings(value: unknown, maxChars: number): unknown {
|
|
519
|
+
if (typeof value === "string") return safePrefix(value, maxChars);
|
|
520
|
+
if (Array.isArray(value)) return value.map((item) => limitStrings(item, maxChars));
|
|
521
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, limitStrings(item, maxChars)]));
|
|
522
|
+
return value;
|
|
345
523
|
}
|
|
346
524
|
|
|
347
525
|
function parseInteger(value: unknown, defaultValue: number, name: string, min: number, max: number): number {
|
|
@@ -380,7 +558,7 @@ function providerScope(provider: FetchProviderName, config?: any): string {
|
|
|
380
558
|
firecrawl: config?.apiKeys?.firecrawl,
|
|
381
559
|
};
|
|
382
560
|
const key = keyMap[provider];
|
|
383
|
-
return key ? `key:${String(key).
|
|
561
|
+
return key ? `key:${createHash("sha256").update(String(key)).digest("hex")}` : "default";
|
|
384
562
|
}
|
|
385
563
|
|
|
386
564
|
function assertProviderUnchanged(tool: string, startup: string, runtime: string) {
|
|
@@ -449,7 +627,6 @@ function fetchDetails(value: any) {
|
|
|
449
627
|
format: r.format,
|
|
450
628
|
cached: r.cached,
|
|
451
629
|
refreshed: r.refreshed,
|
|
452
|
-
cacheKey: r.cacheKey,
|
|
453
630
|
range: r.range,
|
|
454
631
|
error: r.error,
|
|
455
632
|
})),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Context-efficient web search and fetch tools for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -58,12 +58,12 @@
|
|
|
58
58
|
"typebox": "*"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@earendil-works/pi-ai": "^0.
|
|
62
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
63
|
-
"@earendil-works/pi-tui": "^0.
|
|
64
|
-
"@types/node": "^
|
|
65
|
-
"tsx": "^4.
|
|
66
|
-
"typebox": "^1.
|
|
61
|
+
"@earendil-works/pi-ai": "^0.80.0",
|
|
62
|
+
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
63
|
+
"@earendil-works/pi-tui": "^0.80.0",
|
|
64
|
+
"@types/node": "^26.1.0",
|
|
65
|
+
"tsx": "^4.23.0",
|
|
66
|
+
"typebox": "^1.3.4",
|
|
67
67
|
"typescript": "^6.0.3"
|
|
68
68
|
},
|
|
69
69
|
"publishConfig": {
|
package/src/config.ts
CHANGED
|
@@ -24,7 +24,12 @@ type PartialConfig = Partial<Omit<WebKitConfig, "apiKeys" | "markdownNew">> & {
|
|
|
24
24
|
markdownNew?: Partial<WebKitConfig["markdownNew"]>;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
export function resolveConfig(
|
|
27
|
+
export function resolveConfig(
|
|
28
|
+
flags: { providerSearch?: unknown; providerFetch?: unknown } = {},
|
|
29
|
+
cwd = process.cwd(),
|
|
30
|
+
env = process.env,
|
|
31
|
+
options: { includeProject?: boolean } = {},
|
|
32
|
+
): WebKitConfig {
|
|
28
33
|
let cfg = DEFAULT_CONFIG;
|
|
29
34
|
cfg = merge(cfg, {
|
|
30
35
|
provider_search: env.PI_WEB_KIT_PROVIDER_SEARCH as SearchProviderName | undefined,
|
|
@@ -38,9 +43,11 @@ export function resolveConfig(flags: { providerSearch?: unknown; providerFetch?:
|
|
|
38
43
|
},
|
|
39
44
|
});
|
|
40
45
|
const home = env.HOME ?? homedir();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
const globalConfig = readConfigFile(join(home, ".pi/agent/pi-web-kit.json"));
|
|
47
|
+
if (globalConfig) cfg = merge(cfg, globalConfig);
|
|
48
|
+
if (options.includeProject !== false) {
|
|
49
|
+
const projectConfig = readConfigFile(join(cwd, ".pi-web-kit.json"));
|
|
50
|
+
if (projectConfig) cfg = merge(cfg, projectConfig);
|
|
44
51
|
}
|
|
45
52
|
cfg = merge(cfg, {
|
|
46
53
|
provider_search: flags.providerSearch as SearchProviderName | undefined,
|
|
@@ -69,8 +69,12 @@ export function mapFetchResults(requested: string[], fetched: WebFetchResult): M
|
|
|
69
69
|
const index = remaining.findIndex((item) => urlsMatch(item.url, url));
|
|
70
70
|
if (index >= 0) out.set(url, remaining.splice(index, 1)[0]);
|
|
71
71
|
}
|
|
72
|
-
//
|
|
73
|
-
|
|
72
|
+
// Providers may not echo the requested URL. Assign each unmatched result at
|
|
73
|
+
// most once, after exact canonical matches have consumed their results.
|
|
74
|
+
for (const url of requested) {
|
|
75
|
+
if (out.has(url) || remaining.length === 0) continue;
|
|
76
|
+
out.set(url, remaining.shift()!);
|
|
77
|
+
}
|
|
74
78
|
return out;
|
|
75
79
|
}
|
|
76
80
|
|