@selesai/code 0.13.3 → 0.13.4
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 +11 -0
- package/dist/defaults/models.json +17 -1
- package/dist/extensions/cost-reconcile.test.ts +24 -5
- package/dist/extensions/cost-reconcile.ts +14 -7
- package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
- package/dist/extensions/pi-intercom/README.md +4 -5
- package/dist/extensions/pi-intercom/config.test.ts +3 -31
- package/dist/extensions/pi-intercom/config.ts +0 -15
- package/dist/extensions/pi-intercom/index.ts +9 -46
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +49 -57
- package/dist/extensions/pi-intercom/package.json +1 -1
- package/dist/extensions/pi-intercom/reply-tracker.test.ts +20 -0
- package/dist/extensions/pi-intercom/reply-tracker.ts +8 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +27 -0
- package/dist/extensions/pi-subagents/docs/tool-reference.md +4 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +2 -2
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +48 -243
- package/dist/extensions/pi-subagents/skills/council-mode/references/pass-contracts.md +150 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +87 -37
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +49 -8
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +34 -27
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +157 -28
- package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -0
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +1 -0
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +45 -2
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +3 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +13 -2
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +5 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +10 -2
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +11 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +98 -1
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +138 -4
- package/dist/extensions/pi-subagents/src/runs/shared/background-process-options.ts +9 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
- package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +52 -3
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +47 -1
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +45 -18
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +21 -2
- package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +15 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +34 -1
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +11 -3
- package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +31 -0
- package/dist/extensions/pi-subagents/src/tui/render.ts +597 -112
- package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +40 -1
- package/dist/extensions/pi-subagents/src/workflows/host-command.ts +6 -1
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +53 -2
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +55 -6
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +111 -1
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +206 -38
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +522 -31
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +123 -0
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +57 -1
- package/dist/extensions/pi-subagents/test/unit/background-process-options.test.ts +17 -0
- package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +44 -4
- package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +0 -1
- package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +20 -3
- package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +93 -17
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +103 -26
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +58 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +22 -2
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +13 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +54 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +48 -1
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +21 -6
- package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +11 -0
- package/dist/extensions/pi-web-agent/CHANGELOG.md +410 -0
- package/dist/extensions/pi-web-agent/README.md +131 -0
- package/dist/extensions/pi-web-agent/package.json +4 -2
- package/dist/extensions/pi-web-agent/src/backends/config.ts +62 -5
- package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +96 -6
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +183 -47
- package/dist/extensions/pi-web-agent/src/extension.ts +62 -25
- package/dist/extensions/pi-web-agent/src/extract/readability.ts +19 -11
- package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +5 -4
- package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +2 -25
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +5 -2
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +2 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +72 -7
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +8 -1
- package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +28 -3
- package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +4 -0
- package/dist/extensions/pi-web-agent/src/orchestration/url.ts +35 -0
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +18 -6
- package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +14 -2
- package/dist/extensions/pi-web-agent/src/readers/github-reader.ts +150 -0
- package/dist/extensions/pi-web-agent/src/readers/limits.ts +3 -0
- package/dist/extensions/pi-web-agent/src/readers/pdf-reader.ts +87 -0
- package/dist/extensions/pi-web-agent/src/readers/resolver.ts +25 -0
- package/dist/extensions/pi-web-agent/src/readers/types.ts +11 -0
- package/dist/extensions/pi-web-agent/src/readers/youtube-reader.ts +79 -0
- package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +32 -5
- package/dist/extensions/pi-web-agent/src/search/exa.ts +109 -0
- package/dist/extensions/pi-web-agent/src/search/fanout.ts +154 -0
- package/dist/extensions/pi-web-agent/src/search/tavily.ts +113 -0
- package/dist/extensions/pi-web-agent/src/search/youcom.ts +109 -0
- package/dist/extensions/pi-web-agent/src/tools/web-search.ts +22 -9
- package/dist/extensions/pi-web-agent/src/types.ts +19 -4
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WebFetchHeadlessResponse, WebFetchResponse } from '../types.js';
|
|
1
|
+
import type { SearchProviderName, WebFetchHeadlessResponse, WebFetchResponse } from '../types.js';
|
|
2
2
|
import { rankEvidence } from './evidence-ranker.js';
|
|
3
3
|
import { planSearchQueries } from './query-planner.js';
|
|
4
4
|
import { classifySourceProfile } from './source-profile.js';
|
|
@@ -25,6 +25,10 @@ function summarizeText(text: string, maxLength = 180): string {
|
|
|
25
25
|
return text.replace(/\s+/g, ' ').trim().slice(0, maxLength);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function isReaderMethod(method: string): boolean {
|
|
29
|
+
return method === 'github' || method === 'pdf' || method === 'youtube';
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
function isBotCheckContent({ title = '', text }: { title?: string; text: string }) {
|
|
29
33
|
return /performing security verification|security service|verify you are not a bot|just a moment|checking your browser/i.test(
|
|
30
34
|
`${title}\n${text}`
|
|
@@ -35,6 +39,17 @@ function evidenceFromFetch(result: WebFetchResponse): ResearchEvidence | null {
|
|
|
35
39
|
if (result.status !== 'ok' || !result.content?.text.trim()) return null;
|
|
36
40
|
if (isBotCheckContent({ title: result.content.title, text: result.content.text })) return null;
|
|
37
41
|
|
|
42
|
+
if (isReaderMethod(result.metadata.method)) {
|
|
43
|
+
return {
|
|
44
|
+
title: result.content.title ?? result.url,
|
|
45
|
+
url: result.url,
|
|
46
|
+
sourceKind: 'primary-content',
|
|
47
|
+
method: result.metadata.method,
|
|
48
|
+
summary: result.content.text,
|
|
49
|
+
supports: [result.content.text]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
return {
|
|
39
54
|
title: result.content.title ?? result.url,
|
|
40
55
|
url: result.url,
|
|
@@ -101,7 +116,9 @@ function buildMetadata({
|
|
|
101
116
|
allLowValueOutcomes,
|
|
102
117
|
headlessAttempts,
|
|
103
118
|
exhaustedBudget,
|
|
104
|
-
caveatReasons = []
|
|
119
|
+
caveatReasons = [],
|
|
120
|
+
fanoutProviders,
|
|
121
|
+
fanoutSkipped
|
|
105
122
|
}: {
|
|
106
123
|
previousQueries: string[];
|
|
107
124
|
allEvidence: ResearchEvidence[];
|
|
@@ -110,13 +127,17 @@ function buildMetadata({
|
|
|
110
127
|
headlessAttempts: number;
|
|
111
128
|
exhaustedBudget: boolean;
|
|
112
129
|
caveatReasons?: EvidenceCaveatReason[];
|
|
130
|
+
fanoutProviders?: SearchProviderName[];
|
|
131
|
+
fanoutSkipped?: SearchProviderName[];
|
|
113
132
|
}) {
|
|
114
133
|
return {
|
|
115
134
|
searchPasses: previousQueries.length,
|
|
116
135
|
fetchedPages: allEvidence.length + allGaps.length + allLowValueOutcomes.length,
|
|
117
136
|
headlessAttempts,
|
|
118
137
|
exhaustedBudget,
|
|
119
|
-
caveatReasons
|
|
138
|
+
caveatReasons,
|
|
139
|
+
fanoutProviders,
|
|
140
|
+
fanoutSkipped
|
|
120
141
|
};
|
|
121
142
|
}
|
|
122
143
|
|
|
@@ -170,6 +191,14 @@ export function createResearchOrchestrator({
|
|
|
170
191
|
const suggestedHeadlessUrls: string[] = [];
|
|
171
192
|
let headlessAttempts = 0;
|
|
172
193
|
let lastPass: ResearchWorkerResult | undefined;
|
|
194
|
+
const fanoutProvidersSeen = new Set<SearchProviderName>();
|
|
195
|
+
const fanoutSkippedSeen = new Set<SearchProviderName>();
|
|
196
|
+
|
|
197
|
+
function fanoutSnapshot() {
|
|
198
|
+
const providers = fanoutProvidersSeen.size ? [...fanoutProvidersSeen] : undefined;
|
|
199
|
+
const skipped = [...fanoutSkippedSeen].filter((p) => !fanoutProvidersSeen.has(p));
|
|
200
|
+
return { fanoutProviders: providers, fanoutSkipped: skipped.length ? skipped : undefined };
|
|
201
|
+
}
|
|
173
202
|
|
|
174
203
|
if (fetchDirect) {
|
|
175
204
|
for (const url of extractDirectUrls(query).slice(0, 3)) {
|
|
@@ -204,6 +233,36 @@ export function createResearchOrchestrator({
|
|
|
204
233
|
}
|
|
205
234
|
}
|
|
206
235
|
|
|
236
|
+
if (allEvidence.some((item) => item.sourceKind === 'primary-content')) {
|
|
237
|
+
const ranked = rankEvidence(allEvidence.filter((item) => item.sourceKind !== 'package-page'));
|
|
238
|
+
const quality = analyzeEvidenceQuality({
|
|
239
|
+
evidence: ranked,
|
|
240
|
+
gaps: allGaps,
|
|
241
|
+
lowValueOutcomes: allLowValueOutcomes
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
decision: decisionForAnswer({ action: 'answer', query, ranked, exhaustedBudget: false }),
|
|
245
|
+
evidence: ranked,
|
|
246
|
+
workerPass: combinedWorkerPass({
|
|
247
|
+
lastPass,
|
|
248
|
+
previousQueries,
|
|
249
|
+
allGaps,
|
|
250
|
+
allLowValueOutcomes,
|
|
251
|
+
exhaustedBudget: false
|
|
252
|
+
}),
|
|
253
|
+
metadata: buildMetadata({
|
|
254
|
+
previousQueries,
|
|
255
|
+
allEvidence,
|
|
256
|
+
allGaps,
|
|
257
|
+
allLowValueOutcomes,
|
|
258
|
+
headlessAttempts,
|
|
259
|
+
exhaustedBudget: false,
|
|
260
|
+
caveatReasons: quality.caveatReasons,
|
|
261
|
+
...fanoutSnapshot()
|
|
262
|
+
})
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
207
266
|
for (let passIndex = 0; passIndex < DEFAULT_MAX_PASSES; passIndex++) {
|
|
208
267
|
const queries = planSearchQueries({
|
|
209
268
|
originalQuery: query,
|
|
@@ -221,6 +280,8 @@ export function createResearchOrchestrator({
|
|
|
221
280
|
});
|
|
222
281
|
|
|
223
282
|
lastPass = pass;
|
|
283
|
+
pass.fanoutProviders?.forEach((p) => fanoutProvidersSeen.add(p));
|
|
284
|
+
pass.fanoutSkipped?.forEach((p) => fanoutSkippedSeen.add(p));
|
|
224
285
|
allEvidence.push(...pass.evidence);
|
|
225
286
|
allGaps.push(...pass.gaps);
|
|
226
287
|
allLowValueOutcomes.push(...pass.lowValueOutcomes);
|
|
@@ -287,7 +348,8 @@ export function createResearchOrchestrator({
|
|
|
287
348
|
allLowValueOutcomes,
|
|
288
349
|
headlessAttempts,
|
|
289
350
|
exhaustedBudget,
|
|
290
|
-
caveatReasons: updatedQuality.caveatReasons
|
|
351
|
+
caveatReasons: updatedQuality.caveatReasons,
|
|
352
|
+
...fanoutSnapshot()
|
|
291
353
|
})
|
|
292
354
|
};
|
|
293
355
|
}
|
|
@@ -314,7 +376,8 @@ export function createResearchOrchestrator({
|
|
|
314
376
|
allLowValueOutcomes,
|
|
315
377
|
headlessAttempts,
|
|
316
378
|
exhaustedBudget: false,
|
|
317
|
-
caveatReasons: quality.caveatReasons
|
|
379
|
+
caveatReasons: quality.caveatReasons,
|
|
380
|
+
...fanoutSnapshot()
|
|
318
381
|
})
|
|
319
382
|
};
|
|
320
383
|
}
|
|
@@ -338,7 +401,8 @@ export function createResearchOrchestrator({
|
|
|
338
401
|
allLowValueOutcomes,
|
|
339
402
|
headlessAttempts,
|
|
340
403
|
exhaustedBudget,
|
|
341
|
-
caveatReasons: quality.caveatReasons
|
|
404
|
+
caveatReasons: quality.caveatReasons,
|
|
405
|
+
...fanoutSnapshot()
|
|
342
406
|
})
|
|
343
407
|
};
|
|
344
408
|
}
|
|
@@ -368,7 +432,8 @@ export function createResearchOrchestrator({
|
|
|
368
432
|
allLowValueOutcomes,
|
|
369
433
|
headlessAttempts,
|
|
370
434
|
exhaustedBudget: true,
|
|
371
|
-
caveatReasons: quality.caveatReasons
|
|
435
|
+
caveatReasons: quality.caveatReasons,
|
|
436
|
+
...fanoutSnapshot()
|
|
372
437
|
})
|
|
373
438
|
};
|
|
374
439
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { SearchProviderName } from '../types.js';
|
|
2
|
+
|
|
1
3
|
export type ResearchSourceKind =
|
|
4
|
+
| 'primary-content'
|
|
2
5
|
| 'official-docs'
|
|
3
6
|
| 'official-api'
|
|
4
7
|
| 'official-discussion'
|
|
@@ -7,7 +10,7 @@ export type ResearchSourceKind =
|
|
|
7
10
|
| 'package-page'
|
|
8
11
|
| 'other';
|
|
9
12
|
|
|
10
|
-
export type ResearchMethod = 'search' | 'http' | 'headless' | 'firecrawl';
|
|
13
|
+
export type ResearchMethod = 'search' | 'http' | 'headless' | 'firecrawl' | 'github' | 'pdf' | 'youtube';
|
|
11
14
|
|
|
12
15
|
export type ResearchEvidence = {
|
|
13
16
|
title: string;
|
|
@@ -36,6 +39,8 @@ export type ResearchWorkerResult = {
|
|
|
36
39
|
lowValueOutcomes: ResearchLowValueOutcome[];
|
|
37
40
|
suggestedHeadlessUrl?: string;
|
|
38
41
|
exhaustedBudget: boolean;
|
|
42
|
+
fanoutProviders?: SearchProviderName[];
|
|
43
|
+
fanoutSkipped?: SearchProviderName[];
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
export type ResearchRunMetadata = {
|
|
@@ -43,6 +48,8 @@ export type ResearchRunMetadata = {
|
|
|
43
48
|
fetchedPages: number;
|
|
44
49
|
headlessAttempts: number;
|
|
45
50
|
exhaustedBudget: boolean;
|
|
51
|
+
fanoutProviders?: SearchProviderName[];
|
|
52
|
+
fanoutSkipped?: SearchProviderName[];
|
|
46
53
|
};
|
|
47
54
|
|
|
48
55
|
export type ResearchOrchestratorDecision =
|
|
@@ -17,6 +17,10 @@ function summarizeText(text: string, maxLength = 180): string {
|
|
|
17
17
|
return text.replace(/\s+/g, ' ').trim().slice(0, maxLength);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function isReaderMethod(method: string): boolean {
|
|
21
|
+
return method === 'github' || method === 'pdf' || method === 'youtube';
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
function isBotCheckContent({ title = '', text }: { title?: string; text: string }) {
|
|
21
25
|
return /performing security verification|security service|verify you are not a bot|just a moment|checking your browser/i.test(
|
|
22
26
|
`${title}\n${text}`
|
|
@@ -28,6 +32,19 @@ function evidenceFromFetch(fetched: WebFetchResponse, fallbackTitle: string) {
|
|
|
28
32
|
if (fetched.status !== 'ok' || !content) return null;
|
|
29
33
|
if (isBotCheckContent({ title: content.title, text: content.text })) return null;
|
|
30
34
|
|
|
35
|
+
// A successful reader read with usable text is primary content, exempt from the
|
|
36
|
+
// package-page filter below.
|
|
37
|
+
if (isReaderMethod(fetched.metadata.method) && content.text.trim()) {
|
|
38
|
+
return {
|
|
39
|
+
title: content.title ?? fallbackTitle,
|
|
40
|
+
url: fetched.url,
|
|
41
|
+
sourceKind: 'primary-content',
|
|
42
|
+
method: fetched.metadata.method,
|
|
43
|
+
summary: content.text,
|
|
44
|
+
supports: [content.text]
|
|
45
|
+
} satisfies ResearchEvidence;
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
const sourceKind = classifySource(fetched.url);
|
|
32
49
|
if (sourceKind === 'package-page') {
|
|
33
50
|
return null;
|
|
@@ -98,6 +115,8 @@ export function createResearchWorker({
|
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
const searchResult = await search({ query });
|
|
118
|
+
const fanoutProviders = searchResult.metadata.fanout?.providers;
|
|
119
|
+
const fanoutSkipped = searchResult.metadata.fanout?.skipped;
|
|
101
120
|
if (searchResult.status !== 'ok') {
|
|
102
121
|
return {
|
|
103
122
|
searchQueries,
|
|
@@ -110,7 +129,9 @@ export function createResearchWorker({
|
|
|
110
129
|
],
|
|
111
130
|
lowValueOutcomes,
|
|
112
131
|
suggestedHeadlessUrl,
|
|
113
|
-
exhaustedBudget: false
|
|
132
|
+
exhaustedBudget: false,
|
|
133
|
+
fanoutProviders,
|
|
134
|
+
fanoutSkipped
|
|
114
135
|
};
|
|
115
136
|
}
|
|
116
137
|
|
|
@@ -126,7 +147,9 @@ export function createResearchWorker({
|
|
|
126
147
|
}
|
|
127
148
|
],
|
|
128
149
|
suggestedHeadlessUrl,
|
|
129
|
-
exhaustedBudget: false
|
|
150
|
+
exhaustedBudget: false,
|
|
151
|
+
fanoutProviders,
|
|
152
|
+
fanoutSkipped
|
|
130
153
|
};
|
|
131
154
|
}
|
|
132
155
|
|
|
@@ -174,7 +197,9 @@ export function createResearchWorker({
|
|
|
174
197
|
gaps,
|
|
175
198
|
lowValueOutcomes,
|
|
176
199
|
suggestedHeadlessUrl,
|
|
177
|
-
exhaustedBudget: false
|
|
200
|
+
exhaustedBudget: false,
|
|
201
|
+
fanoutProviders,
|
|
202
|
+
fanoutSkipped
|
|
178
203
|
};
|
|
179
204
|
}
|
|
180
205
|
};
|
|
@@ -32,6 +32,10 @@ function parseUrl(rawUrl: string): URL | undefined {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export function hostOf(rawUrl: string): string | undefined {
|
|
36
|
+
return parseUrl(rawUrl)?.hostname.toLowerCase().replace(/^www\./, '');
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
function isOfficialApi(host: string, path: string) {
|
|
36
40
|
return (
|
|
37
41
|
(host === 'playwright.dev' && path.startsWith('/docs/api/')) ||
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const TRACKING_PARAMS = new Set([
|
|
2
|
+
'utm_source',
|
|
3
|
+
'utm_medium',
|
|
4
|
+
'utm_campaign',
|
|
5
|
+
'utm_term',
|
|
6
|
+
'utm_content',
|
|
7
|
+
'utm_name',
|
|
8
|
+
'fbclid',
|
|
9
|
+
'gclid'
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
/** Canonical form for dedupe/comparison. Returns undefined for non-http(s) or unparseable input. */
|
|
13
|
+
export function canonicalizeUrl(raw: string, options: { canonicalizeHost?: boolean } = {}): string | undefined {
|
|
14
|
+
const { canonicalizeHost = true } = options;
|
|
15
|
+
let url: URL;
|
|
16
|
+
try {
|
|
17
|
+
url = new URL(raw);
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return undefined;
|
|
22
|
+
|
|
23
|
+
if (canonicalizeHost) {
|
|
24
|
+
url.hostname = url.hostname.replace(/^www\./, '');
|
|
25
|
+
}
|
|
26
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
27
|
+
if (TRACKING_PARAMS.has(key.toLowerCase())) {
|
|
28
|
+
url.searchParams.delete(key);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
url.hash = '';
|
|
32
|
+
return url.toString().replace(/\/$/, '');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { TRACKING_PARAMS };
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { WebExploreResponse } from '../types.js';
|
|
1
|
+
import type { FetchMethod, WebExploreResponse } from '../types.js';
|
|
2
2
|
import type { PresentationEnvelope } from './types.js';
|
|
3
3
|
|
|
4
|
-
function internalReaderLabel(method?:
|
|
4
|
+
function internalReaderLabel(method?: FetchMethod) {
|
|
5
5
|
if (method === 'headless') return 'web_fetch_headless';
|
|
6
6
|
if (method === 'firecrawl') return 'firecrawl';
|
|
7
7
|
if (method === 'http') return 'web_fetch';
|
|
8
|
+
if (method === 'github') return 'github';
|
|
9
|
+
if (method === 'pdf') return 'pdf';
|
|
10
|
+
if (method === 'youtube') return 'youtube';
|
|
8
11
|
return 'web_explore';
|
|
9
12
|
}
|
|
10
13
|
|
|
@@ -18,8 +21,15 @@ export function buildExplorePresentation(result: WebExploreResponse): Presentati
|
|
|
18
21
|
};
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
const fanoutProviders = result.metadata?.fanoutProviders;
|
|
25
|
+
const fanoutSkipped = result.metadata?.fanoutSkipped;
|
|
26
|
+
const fanoutNote = fanoutProviders?.length
|
|
27
|
+
? ` (fanout: ${fanoutProviders.join(', ')}${fanoutSkipped?.length ? `; skipped: ${fanoutSkipped.join(', ')}` : ''})`
|
|
28
|
+
: fanoutSkipped?.length
|
|
29
|
+
? ` (fanout; skipped: ${fanoutSkipped.join(', ')})`
|
|
30
|
+
: '';
|
|
21
31
|
const internalSummary = result.metadata
|
|
22
|
-
? `Internal research: web_search ×${result.metadata.searchPasses}, web_fetch ×${result.metadata.fetchedPages}, web_fetch_headless ×${result.metadata.headlessAttempts}`
|
|
32
|
+
? `Internal research: web_search ×${result.metadata.searchPasses}${fanoutNote}, web_fetch ×${result.metadata.fetchedPages}, web_fetch_headless ×${result.metadata.headlessAttempts}`
|
|
23
33
|
: undefined;
|
|
24
34
|
const hasEvidence = result.findings.length > 0 || result.sources.length > 0;
|
|
25
35
|
const evidenceLines = hasEvidence
|
|
@@ -43,12 +53,14 @@ export function buildExplorePresentation(result: WebExploreResponse): Presentati
|
|
|
43
53
|
.filter((line) => line !== undefined)
|
|
44
54
|
.join('\n');
|
|
45
55
|
|
|
56
|
+
const compact = hasEvidence
|
|
57
|
+
? `Reviewed ${result.sources.length} sources · synthesized answer with ${result.findings.length} findings`
|
|
58
|
+
: 'No usable evidence found';
|
|
59
|
+
|
|
46
60
|
return {
|
|
47
61
|
mode: 'compact',
|
|
48
62
|
views: {
|
|
49
|
-
compact
|
|
50
|
-
? `Reviewed ${result.sources.length} sources · synthesized answer with ${result.findings.length} findings`
|
|
51
|
-
: 'No usable evidence found',
|
|
63
|
+
compact,
|
|
52
64
|
preview,
|
|
53
65
|
verbose
|
|
54
66
|
},
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
import type { WebSearchResponse } from '../types.js';
|
|
2
2
|
import type { PresentationEnvelope } from './types.js';
|
|
3
3
|
|
|
4
|
+
function fanoutNote(result: WebSearchResponse): string {
|
|
5
|
+
const f = result.metadata.fanout;
|
|
6
|
+
if (!f) return '';
|
|
7
|
+
if (f.providers.length) {
|
|
8
|
+
return ` (fanout: ${f.providers.join(', ')}${f.skipped?.length ? `; skipped: ${f.skipped.join(', ')}` : ''})`;
|
|
9
|
+
}
|
|
10
|
+
if (f.skipped?.length) {
|
|
11
|
+
return ` (fanout; skipped: ${f.skipped.join(', ')})`;
|
|
12
|
+
}
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
|
|
4
16
|
function formatCompact(result: WebSearchResponse): string {
|
|
5
17
|
const fallbackPrefix = result.metadata.fallbackFrom
|
|
6
18
|
? `${result.metadata.fallbackFrom} failed; used ${result.metadata.backend} fallback. `
|
|
7
19
|
: '';
|
|
8
20
|
|
|
9
21
|
if (result.status === 'error') {
|
|
10
|
-
return `${fallbackPrefix}Search failed: ${result.error?.message ?? 'Unknown search failure.'}`;
|
|
22
|
+
return `${fallbackPrefix}Search failed: ${result.error?.message ?? 'Unknown search failure.'}${fanoutNote(result)}`;
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
const suffix = result.results.length === 1 ? 'result' : 'results';
|
|
14
|
-
return `${fallbackPrefix}Found ${result.results.length} ${suffix}`;
|
|
26
|
+
return `${fallbackPrefix}Found ${result.results.length} ${suffix}${fanoutNote(result)}`;
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
export function buildSearchPresentation(result: WebSearchResponse): PresentationEnvelope {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { WebFetchResponse } from '../types.js';
|
|
2
|
+
import type { SpecialContentReader } from './types.js';
|
|
3
|
+
import { READER_TEXT_CAP } from './limits.js';
|
|
4
|
+
|
|
5
|
+
type GithubReaderDeps = {
|
|
6
|
+
fetchImpl?: typeof fetch;
|
|
7
|
+
token?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function parseGithub(url: string): URL | undefined {
|
|
11
|
+
try {
|
|
12
|
+
return new URL(url);
|
|
13
|
+
} catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type GithubShape = 'blob' | 'issue' | 'pull' | 'repo-root';
|
|
19
|
+
|
|
20
|
+
function classifyGithubShape(url: string): { shape: GithubShape; owner: string; repo: string; ref?: string; path?: string; num?: string } | undefined {
|
|
21
|
+
const parsed = parseGithub(url);
|
|
22
|
+
if (!parsed || parsed.hostname.toLowerCase() !== 'github.com') {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
27
|
+
const [owner, repo, type, ...rest] = segments;
|
|
28
|
+
|
|
29
|
+
// blob: [owner, repo, 'blob', ref, ...path] with rest.length >= 2
|
|
30
|
+
if (owner && repo && type === 'blob' && rest.length >= 2) {
|
|
31
|
+
const [ref, ...pathParts] = rest;
|
|
32
|
+
return { shape: 'blob', owner, repo, ref, path: pathParts.join('/') };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// issue: [owner, repo, 'issues', N]
|
|
36
|
+
if (owner && repo && type === 'issues' && rest[0]) {
|
|
37
|
+
return { shape: 'issue', owner, repo, num: rest[0] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// pull: [owner, repo, 'pull', N]
|
|
41
|
+
if (owner && repo && type === 'pull' && rest[0]) {
|
|
42
|
+
return { shape: 'pull', owner, repo, num: rest[0] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// repo-root: exactly [owner, repo]
|
|
46
|
+
if (owner && repo && !type) {
|
|
47
|
+
return { shape: 'repo-root', owner, repo };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function encodeSegment(segment: string): string {
|
|
54
|
+
let decoded: string;
|
|
55
|
+
try {
|
|
56
|
+
decoded = decodeURIComponent(segment);
|
|
57
|
+
} catch {
|
|
58
|
+
decoded = segment;
|
|
59
|
+
}
|
|
60
|
+
return encodeURIComponent(decoded);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fail(url: string, message: string): WebFetchResponse {
|
|
64
|
+
return {
|
|
65
|
+
status: 'error',
|
|
66
|
+
url,
|
|
67
|
+
metadata: { method: 'github', cacheHit: false },
|
|
68
|
+
error: { code: 'GITHUB_FETCH_FAILED', message }
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function okResponse(url: string, title: string, text: string): WebFetchResponse {
|
|
73
|
+
const capped = text.slice(0, READER_TEXT_CAP);
|
|
74
|
+
return {
|
|
75
|
+
status: 'ok',
|
|
76
|
+
url,
|
|
77
|
+
content: { title, text: capped },
|
|
78
|
+
metadata: { method: 'github', cacheHit: false, truncated: text.length >= READER_TEXT_CAP }
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createGithubReader({ fetchImpl = fetch, token = process.env.GITHUB_TOKEN }: GithubReaderDeps = {}): SpecialContentReader {
|
|
83
|
+
function headers(json: boolean): Record<string, string> {
|
|
84
|
+
const h: Record<string, string> = { 'User-Agent': 'pi-web-agent' };
|
|
85
|
+
if (json) h.Accept = 'application/vnd.github+json';
|
|
86
|
+
if (token) h.Authorization = `Bearer ${token}`;
|
|
87
|
+
return h;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function getText(target: string): Promise<string> {
|
|
91
|
+
const res = await fetchImpl(target, { headers: headers(false) });
|
|
92
|
+
if (!res.ok) throw new Error(`GitHub returned ${res.status} for ${target}`);
|
|
93
|
+
return res.text();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function getJson<T>(target: string): Promise<T> {
|
|
97
|
+
const res = await fetchImpl(target, { headers: headers(true) });
|
|
98
|
+
if (!res.ok) throw new Error(`GitHub API returned ${res.status} for ${target}`);
|
|
99
|
+
return res.json() as Promise<T>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function readBlob(url: string, owner: string, repo: string, ref: string, path: string): Promise<WebFetchResponse> {
|
|
103
|
+
const encodedPath = path.split('/').map(encodeSegment).join('/');
|
|
104
|
+
const raw = `https://raw.githubusercontent.com/${owner}/${repo}/${encodeSegment(ref)}/${encodedPath}`;
|
|
105
|
+
const text = await getText(raw);
|
|
106
|
+
return okResponse(url, `${owner}/${repo}/${path}`, text);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readThread(url: string, owner: string, repo: string, kind: 'issues' | 'pulls', num: string): Promise<WebFetchResponse> {
|
|
110
|
+
const base = `https://api.github.com/repos/${owner}/${repo}/${kind}/${num}`;
|
|
111
|
+
const item = await getJson<{ title?: string; body?: string }>(base);
|
|
112
|
+
const comments = await getJson<Array<{ body?: string }>>(`${base}/comments`);
|
|
113
|
+
const body = [item.body ?? '', ...comments.map((c) => c.body ?? '')].filter(Boolean).join('\n\n---\n\n');
|
|
114
|
+
return okResponse(url, item.title ?? `${owner}/${repo} ${kind} #${num}`, body);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function readRepoRoot(url: string, owner: string, repo: string): Promise<WebFetchResponse> {
|
|
118
|
+
const readmeMeta = await getJson<{ download_url?: string }>(`https://api.github.com/repos/${owner}/${repo}/readme`);
|
|
119
|
+
const readme = readmeMeta.download_url ? await getText(readmeMeta.download_url) : '';
|
|
120
|
+
const tree = await getJson<Array<{ name: string; type: string }>>(`https://api.github.com/repos/${owner}/${repo}/contents`);
|
|
121
|
+
const listing = tree.map((entry) => `${entry.type === 'dir' ? '[dir] ' : ''}${entry.name}`).join('\n');
|
|
122
|
+
return okResponse(url, `${owner}/${repo}`, `${readme}\n\nTop-level contents:\n${listing}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
name: 'github',
|
|
127
|
+
canHandle(url: string): boolean {
|
|
128
|
+
return classifyGithubShape(url) !== undefined;
|
|
129
|
+
},
|
|
130
|
+
async read(url: string): Promise<WebFetchResponse> {
|
|
131
|
+
const shape = classifyGithubShape(url);
|
|
132
|
+
if (!shape) return fail(url, 'Unsupported GitHub URL shape for the reader.');
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
switch (shape.shape) {
|
|
136
|
+
case 'blob':
|
|
137
|
+
return await readBlob(url, shape.owner, shape.repo, shape.ref!, shape.path!);
|
|
138
|
+
case 'issue':
|
|
139
|
+
return await readThread(url, shape.owner, shape.repo, 'issues', shape.num!);
|
|
140
|
+
case 'pull':
|
|
141
|
+
return await readThread(url, shape.owner, shape.repo, 'pulls', shape.num!);
|
|
142
|
+
case 'repo-root':
|
|
143
|
+
return await readRepoRoot(url, shape.owner, shape.repo);
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return fail(url, err instanceof Error ? err.message : 'GitHub read failed.');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { extractText, getDocumentProxy, getMeta } from 'unpdf';
|
|
2
|
+
import type { WebFetchResponse } from '../types.js';
|
|
3
|
+
import type { SpecialContentReader } from './types.js';
|
|
4
|
+
import { READER_TEXT_CAP } from './limits.js';
|
|
5
|
+
|
|
6
|
+
type PdfReaderDeps = {
|
|
7
|
+
fetchImpl?: typeof fetch;
|
|
8
|
+
/** Injectable for tests; defaults to unpdf. */
|
|
9
|
+
extractPdfText?: (bytes: Uint8Array) => Promise<{ text: string; title?: string }>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async function defaultExtract(bytes: Uint8Array): Promise<{ text: string; title?: string }> {
|
|
13
|
+
const pdf = await getDocumentProxy(bytes);
|
|
14
|
+
const [{ text }, meta] = await Promise.all([
|
|
15
|
+
extractText(pdf, { mergePages: true }),
|
|
16
|
+
getMeta(pdf).catch(() => ({ info: undefined }))
|
|
17
|
+
]);
|
|
18
|
+
const info = (meta as { info?: { Title?: string } }).info;
|
|
19
|
+
const rawTitle = info?.Title?.trim();
|
|
20
|
+
return { text, title: rawTitle ? rawTitle : undefined };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isPdfUrl(url: string): boolean {
|
|
24
|
+
try {
|
|
25
|
+
return new URL(url).pathname.toLowerCase().endsWith('.pdf');
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function filenameFromUrl(url: string): string {
|
|
32
|
+
try {
|
|
33
|
+
const last = new URL(url).pathname.split('/').filter(Boolean).pop() ?? 'PDF';
|
|
34
|
+
return decodeURIComponent(last);
|
|
35
|
+
} catch {
|
|
36
|
+
return 'PDF';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createPdfReader({ fetchImpl = fetch, extractPdfText = defaultExtract }: PdfReaderDeps = {}): SpecialContentReader {
|
|
41
|
+
return {
|
|
42
|
+
name: 'pdf',
|
|
43
|
+
canHandle: isPdfUrl,
|
|
44
|
+
canHandleContentType(contentType: string): boolean {
|
|
45
|
+
return contentType.toLowerCase().includes('application/pdf');
|
|
46
|
+
},
|
|
47
|
+
async read(url: string): Promise<WebFetchResponse> {
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetchImpl(url);
|
|
50
|
+
if (!('ok' in response) || !response.ok) {
|
|
51
|
+
return {
|
|
52
|
+
status: 'error',
|
|
53
|
+
url,
|
|
54
|
+
metadata: { method: 'pdf', cacheHit: false },
|
|
55
|
+
error: { code: 'PDF_READ_FAILED', message: `Fetching the PDF failed.` }
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
59
|
+
const extracted = await extractPdfText(bytes);
|
|
60
|
+
const text = extracted.text.trim();
|
|
61
|
+
|
|
62
|
+
if (text.length === 0) {
|
|
63
|
+
return {
|
|
64
|
+
status: 'unsupported',
|
|
65
|
+
url,
|
|
66
|
+
metadata: { method: 'pdf', cacheHit: false, contentType: 'application/pdf' },
|
|
67
|
+
error: { code: 'PDF_NO_TEXT', message: 'This looks like a scanned PDF with no extractable text.' }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
status: 'ok',
|
|
73
|
+
url,
|
|
74
|
+
content: { title: extracted.title ?? filenameFromUrl(url), text: text.slice(0, READER_TEXT_CAP) },
|
|
75
|
+
metadata: { method: 'pdf', cacheHit: false, contentType: 'application/pdf', truncated: text.length >= READER_TEXT_CAP }
|
|
76
|
+
};
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return {
|
|
79
|
+
status: 'error',
|
|
80
|
+
url,
|
|
81
|
+
metadata: { method: 'pdf', cacheHit: false },
|
|
82
|
+
error: { code: 'PDF_READ_FAILED', message: err instanceof Error ? err.message : 'PDF read failed.' }
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { WebFetchResponse } from '../types.js';
|
|
2
|
+
import type { SpecialContentReader } from './types.js';
|
|
3
|
+
|
|
4
|
+
type ResolverDeps = {
|
|
5
|
+
readers: SpecialContentReader[];
|
|
6
|
+
fallback: (input: { url: string }) => Promise<WebFetchResponse>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function createSpecialContentResolver({ readers, fallback }: ResolverDeps) {
|
|
10
|
+
return async function resolve(input: { url: string }): Promise<WebFetchResponse> {
|
|
11
|
+
const matched = readers.find((reader) => reader.canHandle(input.url));
|
|
12
|
+
if (matched) {
|
|
13
|
+
return matched.read(input.url);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const response = await fallback(input);
|
|
17
|
+
if (response.status === 'unsupported' && response.metadata.contentType) {
|
|
18
|
+
const byContentType = readers.find((reader) => reader.canHandleContentType?.(response.metadata.contentType!));
|
|
19
|
+
if (byContentType) {
|
|
20
|
+
return byContentType.read(input.url);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return response;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WebFetchResponse } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export type SpecialContentReader = {
|
|
4
|
+
name: string;
|
|
5
|
+
/** Cheap URL-shape check. No network. */
|
|
6
|
+
canHandle(url: string): boolean;
|
|
7
|
+
/** Optional: claim a response by content-type after a normal fetch (e.g. application/pdf). */
|
|
8
|
+
canHandleContentType?(contentType: string): boolean;
|
|
9
|
+
/** Produce a normal WebFetchResponse. Must never throw. */
|
|
10
|
+
read(url: string): Promise<WebFetchResponse>;
|
|
11
|
+
};
|