@zeldrisho/pi-web-fetch 0.8.2 → 0.9.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/CHANGELOG.md +8 -1
- package/README.md +1 -1
- package/package.json +4 -6
- package/src/abort.ts +3 -0
- package/src/cache.ts +50 -0
- package/src/content.ts +16 -0
- package/src/evidence.ts +10 -0
- package/src/extract.ts +183 -1
- package/src/fetch.ts +54 -0
- package/src/focus.ts +17 -0
- package/src/index.ts +10 -1
- package/src/inflight.ts +10 -0
- package/src/network-policy.ts +17 -0
- package/src/network-redirects.ts +35 -0
- package/src/network-transport.ts +27 -0
- package/src/network.ts +1 -0
- package/src/outline.ts +38 -0
- package/src/redact.ts +7 -0
- package/src/render.ts +3 -0
- package/src/service.ts +156 -17
package/src/outline.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const FENCE = /^[ \t]{0,3}(`{3,}|~{3,})/;
|
|
2
|
+
|
|
2
3
|
const MAX_FALLBACK_HEADING_CHARACTERS = 60;
|
|
4
|
+
|
|
3
5
|
export const MAX_OUTLINE_HEADINGS = 12;
|
|
6
|
+
|
|
4
7
|
export const MAX_OUTLINE_HEADING_CHARACTERS = 120;
|
|
5
8
|
|
|
6
9
|
/** One bounded heading exposed as untrusted document-shape metadata. */
|
|
@@ -41,15 +44,20 @@ interface MarkdownFence {
|
|
|
41
44
|
length: number;
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
/** Tracks whether the current Markdown line opens or closes a fenced code block. */
|
|
44
48
|
function updateFence(line: string, fence: MarkdownFence | undefined): MarkdownFence | undefined {
|
|
45
49
|
const match = FENCE.exec(line);
|
|
50
|
+
|
|
46
51
|
if (!match) return fence;
|
|
47
52
|
const marker = match[1];
|
|
53
|
+
|
|
48
54
|
if (!fence) return { character: marker[0], length: marker.length };
|
|
55
|
+
|
|
49
56
|
const closes =
|
|
50
57
|
marker[0] === fence.character &&
|
|
51
58
|
marker.length >= fence.length &&
|
|
52
59
|
!line.slice(match[0].length).trim();
|
|
60
|
+
|
|
53
61
|
return closes ? undefined : fence;
|
|
54
62
|
}
|
|
55
63
|
|
|
@@ -66,15 +74,20 @@ function isSpaceOrTab(character: string | undefined): boolean {
|
|
|
66
74
|
/** Parses the bounded ATX prefix with a linear scan to avoid backtracking on remote input. */
|
|
67
75
|
function parseAtxHeading(line: string): ParsedAtxHeading | undefined {
|
|
68
76
|
let index = 0;
|
|
77
|
+
|
|
69
78
|
while (index < 3 && isSpaceOrTab(line[index])) index += 1;
|
|
79
|
+
|
|
70
80
|
if (isSpaceOrTab(line[index])) return undefined;
|
|
71
81
|
|
|
72
82
|
const markerStart = index;
|
|
83
|
+
|
|
73
84
|
while (line[index] === "#") index += 1;
|
|
74
85
|
const level = index - markerStart;
|
|
86
|
+
|
|
75
87
|
if (level < 1 || level > 6 || !isSpaceOrTab(line[index])) return undefined;
|
|
76
88
|
|
|
77
89
|
while (isSpaceOrTab(line[index])) index += 1;
|
|
90
|
+
|
|
78
91
|
return { level, text: line.slice(index) };
|
|
79
92
|
}
|
|
80
93
|
|
|
@@ -86,6 +99,7 @@ function parseAtxHeading(line: string): ParsedAtxHeading | undefined {
|
|
|
86
99
|
*/
|
|
87
100
|
function countWords(value: string): number {
|
|
88
101
|
const trimmed = value.trim();
|
|
102
|
+
|
|
89
103
|
return trimmed ? trimmed.split(/\s+/).length : 0;
|
|
90
104
|
}
|
|
91
105
|
|
|
@@ -97,10 +111,12 @@ function countWords(value: string): number {
|
|
|
97
111
|
*/
|
|
98
112
|
function countDocumentWords(lines: string[]): number {
|
|
99
113
|
let fence: MarkdownFence | undefined;
|
|
114
|
+
|
|
100
115
|
return lines.reduce((total, line) => {
|
|
101
116
|
const wasInsideFence = Boolean(fence);
|
|
102
117
|
fence = updateFence(line, fence);
|
|
103
118
|
const heading = wasInsideFence || fence ? undefined : parseAtxHeading(line);
|
|
119
|
+
|
|
104
120
|
return total + countWords(heading ? cleanHeading(heading.text) : line);
|
|
105
121
|
}, 0);
|
|
106
122
|
}
|
|
@@ -113,12 +129,16 @@ function countDocumentWords(lines: string[]): number {
|
|
|
113
129
|
*/
|
|
114
130
|
function cleanHeading(value: string): string {
|
|
115
131
|
let contentEnd = value.length;
|
|
132
|
+
|
|
116
133
|
while (contentEnd > 0 && isSpaceOrTab(value[contentEnd - 1])) contentEnd -= 1;
|
|
117
134
|
|
|
118
135
|
let markerStart = contentEnd;
|
|
136
|
+
|
|
119
137
|
while (markerStart > 0 && value[markerStart - 1] === "#") markerStart -= 1;
|
|
138
|
+
|
|
120
139
|
if (markerStart < contentEnd && markerStart > 0 && isSpaceOrTab(value[markerStart - 1])) {
|
|
121
140
|
contentEnd = markerStart - 1;
|
|
141
|
+
|
|
122
142
|
while (contentEnd > 0 && isSpaceOrTab(value[contentEnd - 1])) contentEnd -= 1;
|
|
123
143
|
}
|
|
124
144
|
|
|
@@ -139,16 +159,21 @@ function collectAtxHeadings(lines: string[]): HeadingCollection {
|
|
|
139
159
|
for (const [lineNumber, line] of lines.entries()) {
|
|
140
160
|
const wasInsideFence = Boolean(fence);
|
|
141
161
|
fence = updateFence(line, fence);
|
|
162
|
+
|
|
142
163
|
if (wasInsideFence || fence) continue;
|
|
143
164
|
const heading = parseAtxHeading(line);
|
|
165
|
+
|
|
144
166
|
if (!heading) continue;
|
|
145
167
|
const text = cleanHeading(heading.text);
|
|
168
|
+
|
|
146
169
|
if (!text) continue;
|
|
147
170
|
total += 1;
|
|
171
|
+
|
|
148
172
|
if (locations.length <= MAX_OUTLINE_HEADINGS) {
|
|
149
173
|
locations.push({ level: heading.level, text, line: lineNumber, inferred: false });
|
|
150
174
|
}
|
|
151
175
|
}
|
|
176
|
+
|
|
152
177
|
return { locations, total };
|
|
153
178
|
}
|
|
154
179
|
|
|
@@ -162,6 +187,7 @@ function collectAtxHeadings(lines: string[]): HeadingCollection {
|
|
|
162
187
|
function looksLikeFallbackHeading(line: string, nextLine: string): boolean {
|
|
163
188
|
if (/^\s{4,}/.test(line)) return false;
|
|
164
189
|
const candidate = line.trim();
|
|
190
|
+
|
|
165
191
|
if (
|
|
166
192
|
!candidate ||
|
|
167
193
|
candidate.length > MAX_FALLBACK_HEADING_CHARACTERS ||
|
|
@@ -173,8 +199,10 @@ function looksLikeFallbackHeading(line: string, nextLine: string): boolean {
|
|
|
173
199
|
) {
|
|
174
200
|
return false;
|
|
175
201
|
}
|
|
202
|
+
|
|
176
203
|
if (!/^[A-Z]/.test(candidate) && !/[`()._]/.test(candidate)) return false;
|
|
177
204
|
const following = nextLine.trim();
|
|
205
|
+
|
|
178
206
|
return following.length > MAX_FALLBACK_HEADING_CHARACTERS || /[.!?]$/.test(following);
|
|
179
207
|
}
|
|
180
208
|
|
|
@@ -190,25 +218,32 @@ function collectFallbackHeadings(lines: string[]): HeadingCollection {
|
|
|
190
218
|
const nextNonEmptyIndexes = new Int32Array(lines.length);
|
|
191
219
|
nextNonEmptyIndexes.fill(-1);
|
|
192
220
|
let nextNonEmptyIndex = -1;
|
|
221
|
+
|
|
193
222
|
for (let lineNumber = lines.length - 1; lineNumber >= 0; lineNumber -= 1) {
|
|
194
223
|
nextNonEmptyIndexes[lineNumber] = nextNonEmptyIndex;
|
|
224
|
+
|
|
195
225
|
if (lines[lineNumber].trim()) nextNonEmptyIndex = lineNumber;
|
|
196
226
|
}
|
|
227
|
+
|
|
197
228
|
let fence: MarkdownFence | undefined;
|
|
198
229
|
|
|
199
230
|
for (const [lineNumber, line] of lines.entries()) {
|
|
200
231
|
const wasInsideFence = Boolean(fence);
|
|
201
232
|
fence = updateFence(line, fence);
|
|
233
|
+
|
|
202
234
|
if (wasInsideFence || fence) continue;
|
|
203
235
|
const followingIndex = nextNonEmptyIndexes[lineNumber];
|
|
204
236
|
const following = followingIndex === -1 ? "" : lines[followingIndex];
|
|
237
|
+
|
|
205
238
|
if (looksLikeFallbackHeading(line, following)) {
|
|
206
239
|
total += 1;
|
|
240
|
+
|
|
207
241
|
if (locations.length <= MAX_OUTLINE_HEADINGS) {
|
|
208
242
|
locations.push({ level: 2, text: cleanHeading(line), line: lineNumber, inferred: true });
|
|
209
243
|
}
|
|
210
244
|
}
|
|
211
245
|
}
|
|
246
|
+
|
|
212
247
|
return { locations, total };
|
|
213
248
|
}
|
|
214
249
|
|
|
@@ -222,8 +257,10 @@ export function createDocumentOutline(markdown: string): DocumentOutline {
|
|
|
222
257
|
const lines = markdown.split("\n");
|
|
223
258
|
const explicit = collectAtxHeadings(lines);
|
|
224
259
|
const collection = explicit.total > 0 ? explicit : collectFallbackHeadings(lines);
|
|
260
|
+
|
|
225
261
|
const headings = collection.locations.slice(0, MAX_OUTLINE_HEADINGS).map((heading, index) => {
|
|
226
262
|
const next = collection.locations[index + 1]?.line ?? lines.length;
|
|
263
|
+
|
|
227
264
|
return {
|
|
228
265
|
level: heading.level,
|
|
229
266
|
text: heading.text,
|
|
@@ -231,6 +268,7 @@ export function createDocumentOutline(markdown: string): DocumentOutline {
|
|
|
231
268
|
inferred: heading.inferred,
|
|
232
269
|
};
|
|
233
270
|
});
|
|
271
|
+
|
|
234
272
|
return {
|
|
235
273
|
totalWords: countDocumentWords(lines),
|
|
236
274
|
totalHeadings: collection.total,
|
package/src/redact.ts
CHANGED
|
@@ -17,14 +17,17 @@ const SENSITIVE_QUERY_KEYS = new Set([
|
|
|
17
17
|
"token",
|
|
18
18
|
]);
|
|
19
19
|
|
|
20
|
+
/** Replaces sensitive query-parameter values and reports whether anything changed. */
|
|
20
21
|
function redactSensitiveParams(params: URLSearchParams): boolean {
|
|
21
22
|
let redacted = false;
|
|
23
|
+
|
|
22
24
|
for (const key of params.keys()) {
|
|
23
25
|
if (SENSITIVE_QUERY_KEYS.has(key.toLowerCase())) {
|
|
24
26
|
params.set(key, "REDACTED");
|
|
25
27
|
redacted = true;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
30
|
+
|
|
28
31
|
return redacted;
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -37,6 +40,7 @@ function redactSensitiveParams(params: URLSearchParams): boolean {
|
|
|
37
40
|
*/
|
|
38
41
|
export function redactUrlForDisplay(value: string | URL): string {
|
|
39
42
|
let url: URL;
|
|
43
|
+
|
|
40
44
|
try {
|
|
41
45
|
url = value instanceof URL ? new URL(value.href) : new URL(value);
|
|
42
46
|
} catch {
|
|
@@ -46,9 +50,12 @@ export function redactUrlForDisplay(value: string | URL): string {
|
|
|
46
50
|
url.username = "";
|
|
47
51
|
url.password = "";
|
|
48
52
|
redactSensitiveParams(url.searchParams);
|
|
53
|
+
|
|
49
54
|
if (url.hash.length > 1) {
|
|
50
55
|
const fragmentParams = new URLSearchParams(url.hash.slice(1));
|
|
56
|
+
|
|
51
57
|
if (redactSensitiveParams(fragmentParams)) url.hash = fragmentParams.toString();
|
|
52
58
|
}
|
|
59
|
+
|
|
53
60
|
return url.href;
|
|
54
61
|
}
|
package/src/render.ts
CHANGED
|
@@ -13,14 +13,17 @@ export function formatCollapsibleOutput(
|
|
|
13
13
|
theme: RenderTheme,
|
|
14
14
|
): string {
|
|
15
15
|
const lines = output.replace(/\r\n/g, "\n").split("\n");
|
|
16
|
+
|
|
16
17
|
while (lines.at(-1) === "") lines.pop();
|
|
17
18
|
const totalLines = lines.length;
|
|
18
19
|
const visibleLines = expanded ? lines : lines.slice(0, COLLAPSED_LINES);
|
|
19
20
|
let text = visibleLines.map((line) => theme.fg("toolOutput", line)).join("\n");
|
|
20
21
|
|
|
21
22
|
const remaining = totalLines - visibleLines.length;
|
|
23
|
+
|
|
22
24
|
if (remaining > 0) {
|
|
23
25
|
text += `${theme.fg("muted", `\n... (${remaining} more lines, ${totalLines} total,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
|
|
24
26
|
}
|
|
27
|
+
|
|
25
28
|
return `\n${text}`;
|
|
26
29
|
}
|