@zeldrisho/pi-web-fetch 0.16.0 → 0.16.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/CHANGELOG.md +8 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cache.ts +20 -0
- package/src/index.ts +14 -8
- package/src/render.ts +34 -9
- package/src/service.ts +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.16.1] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Show focus queries in tool calls, and display expansion and continuation notices with clearer spacing.
|
|
15
|
+
|
|
10
16
|
## [0.16.0] - 2026-10-28
|
|
11
17
|
|
|
12
18
|
### Added
|
|
@@ -295,7 +301,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
295
301
|
|
|
296
302
|
- Add bounded public page fetching ([b979496](https://github.com/zeldrisho/pi-packages/commit/b979496b32de1cead172ba570307e4a4a7b3421d))
|
|
297
303
|
|
|
298
|
-
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-web-fetch-v0.16.
|
|
304
|
+
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-web-fetch-v0.16.1...HEAD
|
|
305
|
+
[0.16.1]: https://github.com/zeldrisho/pi-packages/compare/pi-web-fetch-v0.16.0...pi-web-fetch-v0.16.1
|
|
299
306
|
[0.16.0]: https://github.com/zeldrisho/pi-packages/compare/pi-web-fetch-v0.15.0...pi-web-fetch-v0.16.0
|
|
300
307
|
[0.15.0]: https://github.com/zeldrisho/pi-packages/compare/pi-web-fetch-v0.14.1...pi-web-fetch-v0.15.0
|
|
301
308
|
[0.14.1]: https://github.com/zeldrisho/pi-packages/compare/pi-web-fetch-v0.14.0...pi-web-fetch-v0.14.1
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ The `web_fetch` tool accepts public HTTP and HTTPS URLs and supports HTML, Markd
|
|
|
25
25
|
|
|
26
26
|
Output is bounded to 6,000 characters by default. For long pages, use the returned `nextOffset` to continue. Use `query` to focus on matching sections, or omit it to read the complete document. Pi's preview is collapsed by default; press `Ctrl+O` to expand it.
|
|
27
27
|
|
|
28
|
-
For example, a URL such as `https://github.com/
|
|
28
|
+
For example, a URL such as `https://github.com/zeldrisho/pi-packages/blob/main/packages/pi-web-fetch/README.md#usage` starts at the matching heading. GitHub `blob` links and bare gists are converted to raw source URLs; repository roots use their README. GitHub directory listings are not supported.
|
|
29
29
|
|
|
30
30
|
### Page alternatives and bot challenges
|
|
31
31
|
|
package/package.json
CHANGED
package/src/cache.ts
CHANGED
|
@@ -39,6 +39,26 @@ export function stableKeyHash(key: string): string {
|
|
|
39
39
|
return createHash("sha256").update(key).digest("hex");
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Hash canonical JSON, sorting object keys recursively while preserving array order. */
|
|
43
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- JSON serialization defines the accepted values.
|
|
44
|
+
export function canonicalKey(value: unknown): string {
|
|
45
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- JSON replacer receives arbitrary JSON values.
|
|
46
|
+
const json = JSON.stringify(value, (_key, entry: unknown) => {
|
|
47
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- recursively sort JSON objects.
|
|
48
|
+
if (entry !== null && typeof entry === "object" && !Array.isArray(entry)) {
|
|
49
|
+
return Object.fromEntries(
|
|
50
|
+
Object.entries(entry).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return entry;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (json === undefined) throw new TypeError("Cache key must be JSON serializable");
|
|
58
|
+
|
|
59
|
+
return stableKeyHash(json);
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
/**
|
|
43
63
|
* Resolves a cache entry's on-disk path and refuses to escape the cache
|
|
44
64
|
* directory, guarding against a `keyToPath` that returns `..` segments. The
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import {
|
|
5
5
|
FETCH_MAX_OFFSET_CHARACTERS,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
FETCH_MAX_URL_CHARACTERS,
|
|
8
8
|
} from "./limits";
|
|
9
9
|
import { redactUrlForDisplay } from "./redact";
|
|
10
|
-
import {
|
|
10
|
+
import { renderCollapsibleOutput } from "./render";
|
|
11
11
|
import { executeWebFetch, webFetchOutputSchema, type WebFetchDetails } from "./service";
|
|
12
12
|
|
|
13
13
|
export { ExpiringLruCache } from "./cache";
|
|
@@ -108,7 +108,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
108
108
|
/** Renders the redacted URL, optional focus query, and character offset in the call preview. */
|
|
109
109
|
renderCall(args, theme) {
|
|
110
110
|
return new Text(
|
|
111
|
-
`${theme.fg("toolTitle", theme.bold("web_fetch"))} ${theme.fg("accent", redactUrlForDisplay(args.url))}`,
|
|
111
|
+
`${theme.fg("toolTitle", theme.bold("web_fetch"))} ${theme.fg("accent", redactUrlForDisplay(args.url))}${args.query === undefined ? "" : theme.fg("muted", ` query=${JSON.stringify(args.query)}`)}`,
|
|
112
112
|
0,
|
|
113
113
|
0,
|
|
114
114
|
);
|
|
@@ -122,15 +122,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
122
122
|
* Renders partial text or "Fetching…" while pending. Completed text gets a
|
|
123
123
|
* detail summary when available and a collapsible body; absent text shows "No content".
|
|
124
124
|
*/
|
|
125
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
125
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
126
126
|
const content = result.content.find((item) => item.type === "text");
|
|
127
127
|
|
|
128
128
|
if (isPartial) return new Text(theme.fg("warning", content?.text || "Fetching…"), 0, 0);
|
|
129
129
|
|
|
130
130
|
return content?.type === "text"
|
|
131
|
-
?
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
? renderCollapsibleOutput(
|
|
132
|
+
content.text,
|
|
133
|
+
expanded,
|
|
134
|
+
theme,
|
|
135
|
+
context?.isError,
|
|
136
|
+
result.details?.truncated && result.details.nextOffset !== undefined
|
|
137
|
+
? `[Content truncated. Continue with offset=${result.details.nextOffset} to read the next chunk.]`
|
|
138
|
+
: undefined,
|
|
139
|
+
)
|
|
134
140
|
: new Text(theme.fg("dim", "No content"), 0, 0);
|
|
135
141
|
},
|
|
136
142
|
});
|
package/src/render.ts
CHANGED
|
@@ -1,21 +1,46 @@
|
|
|
1
|
-
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { getMarkdownTheme, keyHint } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
2
3
|
|
|
3
4
|
const COLLAPSED_LINES = 10;
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
export function
|
|
6
|
+
/** Render document content separately from Pi-style interface notices. */
|
|
7
|
+
export function renderCollapsibleOutput(
|
|
8
|
+
output: string,
|
|
9
|
+
expanded: boolean,
|
|
10
|
+
theme: { fg(color: "toolOutput" | "muted" | "warning", text: string): string },
|
|
11
|
+
isError = false,
|
|
12
|
+
warning?: string,
|
|
13
|
+
): Container {
|
|
7
14
|
const lines = output.replace(/\r\n/g, "\n").split("\n");
|
|
8
15
|
|
|
9
16
|
while (lines.at(-1) === "") lines.pop();
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
17
|
+
const visible = expanded ? lines : lines.slice(0, COLLAPSED_LINES);
|
|
18
|
+
const remaining = lines.length - visible.length;
|
|
19
|
+
const component = new Container();
|
|
20
|
+
const body = `\n${visible.join("\n")}`;
|
|
13
21
|
|
|
14
|
-
|
|
22
|
+
component.addChild(
|
|
23
|
+
isError
|
|
24
|
+
? new Text(theme.fg("toolOutput", body), 0, 0)
|
|
25
|
+
: new Markdown(body, 0, 0, getMarkdownTheme(), {
|
|
26
|
+
color: (text) => theme.fg("toolOutput", text),
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
15
29
|
|
|
16
30
|
if (remaining > 0) {
|
|
17
|
-
|
|
31
|
+
component.addChild(
|
|
32
|
+
new Text(
|
|
33
|
+
`${theme.fg("muted", `... (${remaining} more lines, ${lines.length} total,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`,
|
|
34
|
+
0,
|
|
35
|
+
0,
|
|
36
|
+
),
|
|
37
|
+
);
|
|
18
38
|
}
|
|
19
39
|
|
|
20
|
-
|
|
40
|
+
if (warning) {
|
|
41
|
+
component.addChild(new Spacer(1));
|
|
42
|
+
component.addChild(new Text(theme.fg("warning", warning), 0, 0));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return component;
|
|
21
46
|
}
|
package/src/service.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
truncateHead,
|
|
9
9
|
type AgentToolResult,
|
|
10
10
|
} from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { ExpiringLruCache, stableKeyHash, type CachePersistence } from "./cache";
|
|
11
|
+
import { ExpiringLruCache, stableKeyHash, canonicalKey, type CachePersistence } from "./cache";
|
|
12
12
|
import { sliceCompleteDocument, type CompleteDocument } from "./document/content.ts";
|
|
13
13
|
import { revalidateCompleteDocument, type FetchRemoteDependencies } from "./acquisition/fetch.ts";
|
|
14
14
|
import { fetchDocumentWithLlmsTxtSupport } from "./acquisition/llms.ts";
|
|
@@ -430,7 +430,8 @@ export async function executeWebFetch(
|
|
|
430
430
|
|
|
431
431
|
const displayRequestedUrl = redactUrlForDisplay(params.url);
|
|
432
432
|
const sourceUrl = urlWithoutFragment(params.url);
|
|
433
|
-
|
|
433
|
+
const cacheKey = canonicalKey({ url: sourceUrl });
|
|
434
|
+
let document = fetchCache.get(cacheKey);
|
|
434
435
|
const now = Date.now();
|
|
435
436
|
|
|
436
437
|
const fresh = Boolean(
|
|
@@ -459,7 +460,7 @@ export async function executeWebFetch(
|
|
|
459
460
|
const stale = document;
|
|
460
461
|
|
|
461
462
|
const acquisition = await inflightFetches.run(
|
|
462
|
-
|
|
463
|
+
cacheKey,
|
|
463
464
|
async (sharedSignal) => {
|
|
464
465
|
let fetched: CompleteDocument;
|
|
465
466
|
let acquisitionStatus: FetchAcquisition["cacheStatus"] = "miss";
|
|
@@ -479,7 +480,7 @@ export async function executeWebFetch(
|
|
|
479
480
|
}
|
|
480
481
|
|
|
481
482
|
if (!fetched.extractionDiagnostics?.botWall) {
|
|
482
|
-
fetchCache.set(
|
|
483
|
+
fetchCache.set(cacheKey, fetched, Date.now() + CACHE_STALE_RETENTION_MS);
|
|
483
484
|
}
|
|
484
485
|
|
|
485
486
|
return { document: fetched, cacheStatus: acquisitionStatus };
|