@pi-unipi/web-api 2.6.1 → 2.6.2
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/package.json +7 -7
- package/src/engine/dependencies.ts +13 -134
- package/src/engine/extract.ts +9 -99
- package/src/engine/format.ts +0 -75
- package/src/engine/types.ts +0 -39
- package/src/index.ts +1 -2
- package/src/providers/base.ts +0 -10
- package/src/providers/duckduckgo.ts +0 -1
- package/src/providers/firecrawl.ts +0 -9
- package/src/providers/jina-reader.ts +0 -9
- package/src/providers/jina-search.ts +0 -9
- package/src/providers/perplexity.ts +0 -9
- package/src/providers/registry.ts +0 -80
- package/src/providers/serpapi.ts +0 -9
- package/src/providers/tavily.ts +0 -9
- package/src/providers/wigolo-client.ts +0 -30
- package/src/providers/wigolo.ts +0 -1
- package/src/settings.ts +17 -77
- package/src/tools.ts +21 -24
- package/src/tui/settings-dialog.ts +0 -1
- package/src/providers/llm-summarize.ts +0 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/web-api",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "Web search, read, and summarize tools with provider-based backend selection for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,8 +34,6 @@
|
|
|
34
34
|
"@pi-unipi/core": "2.6.1",
|
|
35
35
|
"defuddle": "^0.18.1",
|
|
36
36
|
"linkedom": "^0.18.12",
|
|
37
|
-
"lodash": "^4.17.21",
|
|
38
|
-
"mime-types": "^2.1.35",
|
|
39
37
|
"wreq-js": "^2.3.0"
|
|
40
38
|
},
|
|
41
39
|
"optionalDependencies": {
|
|
@@ -47,16 +45,18 @@
|
|
|
47
45
|
"typebox": "^1.1.38"
|
|
48
46
|
},
|
|
49
47
|
"devDependencies": {
|
|
50
|
-
"@types/lodash": "^4.17.24",
|
|
51
|
-
"@types/mime-types": "^3.0.1",
|
|
52
48
|
"@types/node": "^25.6.0"
|
|
53
49
|
},
|
|
54
50
|
"scripts": {
|
|
55
51
|
"test": "npx tsx --test tests/**/*.test.ts"
|
|
56
52
|
},
|
|
57
53
|
"pi": {
|
|
58
|
-
"extensions": [
|
|
59
|
-
|
|
54
|
+
"extensions": [
|
|
55
|
+
"./src/index.ts"
|
|
56
|
+
],
|
|
57
|
+
"skills": [
|
|
58
|
+
"./skills"
|
|
59
|
+
],
|
|
60
60
|
"prompts": [],
|
|
61
61
|
"themes": []
|
|
62
62
|
}
|
|
@@ -1,145 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @unipi/web-api — Runtime Dependencies
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* wreq-js and defuddle are hard dependencies (always installed).
|
|
5
|
+
* Static imports replace the former lazy-loader theater.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
let lodashModule: Record<string, unknown> | null = null;
|
|
11
|
-
let mimeTypesModule: typeof import("mime-types") | null = null;
|
|
8
|
+
import * as wreq from "wreq-js";
|
|
9
|
+
import * as defuddle from "defuddle";
|
|
12
10
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
*
|
|
17
|
-
* @returns wreq-js module
|
|
18
|
-
*/
|
|
19
|
-
export async function getWreq(): Promise<any> {
|
|
20
|
-
if (wreqModule) {
|
|
21
|
-
return wreqModule;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
// Use dynamic import for ESM compatibility
|
|
26
|
-
wreqModule = await import("wreq-js");
|
|
27
|
-
return wreqModule;
|
|
28
|
-
} catch (error) {
|
|
29
|
-
throw new Error(
|
|
30
|
-
`wreq-js is not available. ` +
|
|
31
|
-
`This is required for browser-grade TLS fingerprinting. ` +
|
|
32
|
-
`Run: npm install wreq-js\n` +
|
|
33
|
-
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Get the defuddle module.
|
|
40
|
-
* Throws a helpful error if the module is not available.
|
|
41
|
-
*
|
|
42
|
-
* @returns defuddle module
|
|
43
|
-
*/
|
|
44
|
-
export async function getDefuddle(): Promise<any> {
|
|
45
|
-
if (defuddleModule) {
|
|
46
|
-
return defuddleModule;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
defuddleModule = await import("defuddle");
|
|
51
|
-
return defuddleModule;
|
|
52
|
-
} catch (error) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`defuddle is not available. ` +
|
|
55
|
-
`This is required for intelligent content extraction. ` +
|
|
56
|
-
`Run: npm install defuddle\n` +
|
|
57
|
-
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Get the lodash module.
|
|
64
|
-
*
|
|
65
|
-
* @returns lodash module
|
|
66
|
-
*/
|
|
67
|
-
export async function getLodash(): Promise<any> {
|
|
68
|
-
if (lodashModule) {
|
|
69
|
-
return lodashModule;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
lodashModule = await import("lodash");
|
|
74
|
-
return lodashModule;
|
|
75
|
-
} catch (error) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
`lodash is not available. ` +
|
|
78
|
-
`Run: npm install lodash\n` +
|
|
79
|
-
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
80
|
-
);
|
|
81
|
-
}
|
|
11
|
+
/** Get the wreq-js module. */
|
|
12
|
+
export function getWreq(): any {
|
|
13
|
+
return wreq;
|
|
82
14
|
}
|
|
83
15
|
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
* @returns mime-types module
|
|
88
|
-
*/
|
|
89
|
-
export async function getMimeTypes(): Promise<any> {
|
|
90
|
-
if (mimeTypesModule) {
|
|
91
|
-
return mimeTypesModule;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
mimeTypesModule = await import("mime-types");
|
|
96
|
-
return mimeTypesModule;
|
|
97
|
-
} catch (error) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`mime-types is not available. ` +
|
|
100
|
-
`Run: npm install mime-types\n` +
|
|
101
|
-
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
102
|
-
);
|
|
103
|
-
}
|
|
16
|
+
/** Get the defuddle module. */
|
|
17
|
+
export function getDefuddle(): any {
|
|
18
|
+
return defuddle;
|
|
104
19
|
}
|
|
105
20
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
* @returns true if all deps are available
|
|
110
|
-
*/
|
|
111
|
-
export async function checkDependencies(): Promise<{
|
|
112
|
-
available: boolean;
|
|
113
|
-
missing: string[];
|
|
114
|
-
}> {
|
|
115
|
-
const missing: string[] = [];
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
await getWreq();
|
|
119
|
-
} catch {
|
|
120
|
-
missing.push("wreq-js");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
await getDefuddle();
|
|
125
|
-
} catch {
|
|
126
|
-
missing.push("defuddle");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
await getLodash();
|
|
131
|
-
} catch {
|
|
132
|
-
missing.push("lodash");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
await getMimeTypes();
|
|
137
|
-
} catch {
|
|
138
|
-
missing.push("mime-types");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
available: missing.length === 0,
|
|
143
|
-
missing,
|
|
144
|
-
};
|
|
21
|
+
/** Check if all required dependencies are available. */
|
|
22
|
+
export function checkDependencies(): { available: boolean; missing: string[] } {
|
|
23
|
+
return { available: true, missing: [] };
|
|
145
24
|
}
|
package/src/engine/extract.ts
CHANGED
|
@@ -9,11 +9,8 @@ import type {
|
|
|
9
9
|
FetchResult,
|
|
10
10
|
FetchError,
|
|
11
11
|
FetchOptions,
|
|
12
|
-
FetchProgress,
|
|
13
|
-
FetchExecutionHooks,
|
|
14
12
|
BatchFetchResult,
|
|
15
13
|
BatchFetchItemResult,
|
|
16
|
-
FetchProgressStatus,
|
|
17
14
|
} from "./types.js";
|
|
18
15
|
import {
|
|
19
16
|
DEFAULT_BROWSER,
|
|
@@ -27,7 +24,7 @@ import {
|
|
|
27
24
|
DEFAULT_BATCH_CONCURRENCY,
|
|
28
25
|
} from "./constants.js";
|
|
29
26
|
import { resolveBrowserProfile, resolveOSProfile } from "./profiles.js";
|
|
30
|
-
import { getWreq, getDefuddle
|
|
27
|
+
import { getWreq, getDefuddle } from "./dependencies.js";
|
|
31
28
|
import { parseHTML, extractTextContent, elementToMarkdown } from "./dom.js";
|
|
32
29
|
import { truncateContent, formatContent } from "./format.js";
|
|
33
30
|
|
|
@@ -224,13 +221,11 @@ function detectContentType(
|
|
|
224
221
|
*
|
|
225
222
|
* @param url - URL to fetch
|
|
226
223
|
* @param options - Fetch options
|
|
227
|
-
* @param hooks - Execution hooks for progress
|
|
228
224
|
* @returns Fetch result or throws FetchError
|
|
229
225
|
*/
|
|
230
226
|
export async function defuddleFetch(
|
|
231
227
|
url: string,
|
|
232
228
|
options: FetchOptions = {},
|
|
233
|
-
hooks?: FetchExecutionHooks
|
|
234
229
|
): Promise<FetchResult> {
|
|
235
230
|
const {
|
|
236
231
|
browser = DEFAULT_BROWSER,
|
|
@@ -244,29 +239,10 @@ export async function defuddleFetch(
|
|
|
244
239
|
headers: customHeaders,
|
|
245
240
|
} = options;
|
|
246
241
|
|
|
247
|
-
// Track progress
|
|
248
|
-
const updateProgress = (
|
|
249
|
-
status: FetchProgressStatus,
|
|
250
|
-
percent: number = 0,
|
|
251
|
-
phase: string = "",
|
|
252
|
-
bytesLoaded: number = 0,
|
|
253
|
-
bytesTotal: number = 0
|
|
254
|
-
) => {
|
|
255
|
-
hooks?.onProgress?.({
|
|
256
|
-
url,
|
|
257
|
-
status,
|
|
258
|
-
percent,
|
|
259
|
-
bytesLoaded,
|
|
260
|
-
bytesTotal,
|
|
261
|
-
phase,
|
|
262
|
-
});
|
|
263
|
-
};
|
|
264
|
-
|
|
265
242
|
let finalUrl = url;
|
|
266
243
|
let redirectCount = 0;
|
|
267
244
|
|
|
268
245
|
// Validate URL
|
|
269
|
-
updateProgress("connecting", 0, "validation");
|
|
270
246
|
try {
|
|
271
247
|
validateUrl(url);
|
|
272
248
|
} catch (error) {
|
|
@@ -279,7 +255,7 @@ export async function defuddleFetch(
|
|
|
279
255
|
}
|
|
280
256
|
|
|
281
257
|
// Get wreq-js
|
|
282
|
-
const wreq =
|
|
258
|
+
const wreq = getWreq();
|
|
283
259
|
|
|
284
260
|
// Build request options
|
|
285
261
|
const resolvedBrowser = resolveBrowserProfile(browser);
|
|
@@ -292,7 +268,6 @@ export async function defuddleFetch(
|
|
|
292
268
|
|
|
293
269
|
// Main fetch loop (handles meta refresh redirects)
|
|
294
270
|
while (redirectCount < MAX_REDIRECTS) {
|
|
295
|
-
updateProgress("connecting", 10, "connecting");
|
|
296
271
|
|
|
297
272
|
try {
|
|
298
273
|
// wreq-js request
|
|
@@ -304,8 +279,6 @@ export async function defuddleFetch(
|
|
|
304
279
|
headers: requestHeaders,
|
|
305
280
|
});
|
|
306
281
|
|
|
307
|
-
updateProgress("waiting", 30, "waiting");
|
|
308
|
-
|
|
309
282
|
// Check HTTP status
|
|
310
283
|
if (!response.ok) {
|
|
311
284
|
throw createError(
|
|
@@ -322,21 +295,16 @@ export async function defuddleFetch(
|
|
|
322
295
|
);
|
|
323
296
|
}
|
|
324
297
|
|
|
325
|
-
updateProgress("loading", 40, "loading");
|
|
326
|
-
|
|
327
298
|
// Get response body
|
|
328
299
|
const buffer = await response.arrayBuffer();
|
|
329
300
|
const contentLength = response.headers.get("content-length");
|
|
330
301
|
const bytesTotal = contentLength ? parseInt(contentLength, 10) : buffer.byteLength;
|
|
331
302
|
|
|
332
|
-
updateProgress("loading", 60, "loading", buffer.byteLength, bytesTotal);
|
|
333
|
-
|
|
334
303
|
// Detect content type
|
|
335
304
|
const { mimeType, isBinary } = detectContentType(response, buffer);
|
|
336
305
|
|
|
337
306
|
// Handle binary content
|
|
338
307
|
if (isBinary) {
|
|
339
|
-
updateProgress("processing", 80, "processing");
|
|
340
308
|
|
|
341
309
|
// For binary files, return a placeholder with metadata
|
|
342
310
|
return createResult(url, finalUrl, `[Binary file: ${mimeType}]`, {
|
|
@@ -347,7 +315,6 @@ export async function defuddleFetch(
|
|
|
347
315
|
|
|
348
316
|
// Handle JSON
|
|
349
317
|
if (mimeType === "application/json") {
|
|
350
|
-
updateProgress("processing", 80, "processing");
|
|
351
318
|
const text = new TextDecoder().decode(buffer);
|
|
352
319
|
const json = JSON.parse(text);
|
|
353
320
|
const content = JSON.stringify(json, null, 2);
|
|
@@ -361,7 +328,6 @@ export async function defuddleFetch(
|
|
|
361
328
|
|
|
362
329
|
// Handle plain text
|
|
363
330
|
if (mimeType.startsWith("text/plain")) {
|
|
364
|
-
updateProgress("processing", 80, "processing");
|
|
365
331
|
const text = new TextDecoder().decode(buffer);
|
|
366
332
|
const truncated = truncateContent(text, maxChars);
|
|
367
333
|
|
|
@@ -372,7 +338,6 @@ export async function defuddleFetch(
|
|
|
372
338
|
}
|
|
373
339
|
|
|
374
340
|
// Handle HTML
|
|
375
|
-
updateProgress("processing", 70, "processing");
|
|
376
341
|
|
|
377
342
|
const html = new TextDecoder().decode(buffer);
|
|
378
343
|
const { document, window } = parseHTML(html);
|
|
@@ -391,7 +356,7 @@ export async function defuddleFetch(
|
|
|
391
356
|
let metadata: Partial<FetchResult> = {};
|
|
392
357
|
|
|
393
358
|
try {
|
|
394
|
-
const defuddle =
|
|
359
|
+
const defuddle = getDefuddle();
|
|
395
360
|
|
|
396
361
|
// defuddle expects a window object with document
|
|
397
362
|
const defuddleOptions = {
|
|
@@ -425,8 +390,6 @@ export async function defuddleFetch(
|
|
|
425
390
|
maxChars
|
|
426
391
|
);
|
|
427
392
|
|
|
428
|
-
updateProgress("done", 100, "done", bytesTotal, bytesTotal);
|
|
429
|
-
|
|
430
393
|
return createResult(url, finalUrl, formattedContent, {
|
|
431
394
|
...metadata,
|
|
432
395
|
mimeType,
|
|
@@ -504,13 +467,11 @@ function fallbackExtraction(document: Document): string {
|
|
|
504
467
|
*
|
|
505
468
|
* @param urls - URLs to fetch
|
|
506
469
|
* @param options - Fetch options
|
|
507
|
-
* @param hooks - Execution hooks
|
|
508
470
|
* @returns Batch fetch result
|
|
509
471
|
*/
|
|
510
472
|
export async function defuddleFetchMultiple(
|
|
511
473
|
urls: string[],
|
|
512
474
|
options: FetchOptions & { batchConcurrency?: number } = {},
|
|
513
|
-
hooks?: FetchExecutionHooks
|
|
514
475
|
): Promise<BatchFetchResult> {
|
|
515
476
|
const {
|
|
516
477
|
batchConcurrency = DEFAULT_BATCH_CONCURRENCY,
|
|
@@ -518,76 +479,28 @@ export async function defuddleFetchMultiple(
|
|
|
518
479
|
} = options;
|
|
519
480
|
|
|
520
481
|
const items: BatchFetchItemResult[] = new Array(urls.length);
|
|
521
|
-
const progress: FetchProgress[] = urls.map((url) => ({
|
|
522
|
-
url,
|
|
523
|
-
status: "queued" as FetchProgressStatus,
|
|
524
|
-
percent: 0,
|
|
525
|
-
bytesLoaded: 0,
|
|
526
|
-
bytesTotal: 0,
|
|
527
|
-
phase: "queued",
|
|
528
|
-
}));
|
|
529
|
-
|
|
530
|
-
// Worker function
|
|
531
|
-
const fetchWorker = async (index: number): Promise<void> => {
|
|
532
|
-
const url = urls[index];
|
|
533
482
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
percent: 0,
|
|
538
|
-
bytesLoaded: 0,
|
|
539
|
-
bytesTotal: 0,
|
|
540
|
-
phase: "connecting",
|
|
541
|
-
};
|
|
542
|
-
hooks?.onUpdate?.([...progress]);
|
|
483
|
+
// Bounded concurrency
|
|
484
|
+
let nextIndex = 0;
|
|
485
|
+
const workers: Promise<void>[] = [];
|
|
543
486
|
|
|
487
|
+
const fetchWorker = async (index: number): Promise<void> => {
|
|
544
488
|
try {
|
|
545
|
-
const result = await defuddleFetch(
|
|
546
|
-
onProgress: (p) => {
|
|
547
|
-
progress[index] = p;
|
|
548
|
-
hooks?.onUpdate?.([...progress]);
|
|
549
|
-
},
|
|
550
|
-
});
|
|
551
|
-
|
|
489
|
+
const result = await defuddleFetch(urls[index], fetchOptions);
|
|
552
490
|
items[index] = { status: "done", result };
|
|
553
|
-
progress[index] = {
|
|
554
|
-
url,
|
|
555
|
-
status: "done",
|
|
556
|
-
percent: 100,
|
|
557
|
-
bytesLoaded: progress[index].bytesTotal,
|
|
558
|
-
bytesTotal: progress[index].bytesTotal,
|
|
559
|
-
phase: "done",
|
|
560
|
-
};
|
|
561
491
|
} catch (error) {
|
|
562
492
|
const fetchError = (error as FetchError).code
|
|
563
493
|
? (error as FetchError)
|
|
564
|
-
: createError("processing_error", "unknown", (error as Error).message, false, { url });
|
|
565
|
-
|
|
494
|
+
: createError("processing_error", "unknown", (error as Error).message, false, { url: urls[index] });
|
|
566
495
|
items[index] = { status: "error", error: fetchError };
|
|
567
|
-
progress[index] = {
|
|
568
|
-
url,
|
|
569
|
-
status: "error",
|
|
570
|
-
percent: 0,
|
|
571
|
-
bytesLoaded: 0,
|
|
572
|
-
bytesTotal: 0,
|
|
573
|
-
phase: "error",
|
|
574
|
-
error: fetchError,
|
|
575
|
-
};
|
|
576
496
|
}
|
|
577
|
-
|
|
578
|
-
hooks?.onUpdate?.([...progress]);
|
|
579
497
|
};
|
|
580
498
|
|
|
581
|
-
// Bounded concurrency
|
|
582
|
-
let nextIndex = 0;
|
|
583
|
-
const workers: Promise<void>[] = [];
|
|
584
|
-
|
|
585
499
|
const startWorker = (): void => {
|
|
586
500
|
if (nextIndex >= urls.length) return;
|
|
587
501
|
const index = nextIndex++;
|
|
588
502
|
workers.push(
|
|
589
503
|
fetchWorker(index).then(() => {
|
|
590
|
-
// Start next worker after completion
|
|
591
504
|
if (nextIndex < urls.length) {
|
|
592
505
|
startWorker();
|
|
593
506
|
}
|
|
@@ -595,15 +508,12 @@ export async function defuddleFetchMultiple(
|
|
|
595
508
|
);
|
|
596
509
|
};
|
|
597
510
|
|
|
598
|
-
// Start initial workers
|
|
599
511
|
for (let i = 0; i < Math.min(batchConcurrency, urls.length); i++) {
|
|
600
512
|
startWorker();
|
|
601
513
|
}
|
|
602
514
|
|
|
603
|
-
// Wait for all workers to complete
|
|
604
515
|
await Promise.all(workers);
|
|
605
516
|
|
|
606
|
-
// Calculate statistics
|
|
607
517
|
const succeeded = items.filter((item) => item.status === "done").length;
|
|
608
518
|
const failed = items.filter((item) => item.status === "error").length;
|
|
609
519
|
|
package/src/engine/format.ts
CHANGED
|
@@ -136,51 +136,6 @@ function stripMarkdown(markdown: string): string {
|
|
|
136
136
|
* @param error - Fetch error
|
|
137
137
|
* @returns Human-readable error string
|
|
138
138
|
*/
|
|
139
|
-
export function buildErrorText(error: FetchError): string {
|
|
140
|
-
const parts: string[] = [];
|
|
141
|
-
|
|
142
|
-
// Main error message
|
|
143
|
-
parts.push(error.error);
|
|
144
|
-
|
|
145
|
-
// Code and phase context
|
|
146
|
-
parts.push(`(${error.code} during ${error.phase})`);
|
|
147
|
-
|
|
148
|
-
// URL context
|
|
149
|
-
if (error.url) {
|
|
150
|
-
if (error.finalUrl && error.finalUrl !== error.url) {
|
|
151
|
-
parts.push(`URL: ${error.url} → ${error.finalUrl}`);
|
|
152
|
-
} else {
|
|
153
|
-
parts.push(`URL: ${error.url}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// HTTP status
|
|
158
|
-
if (error.statusCode) {
|
|
159
|
-
parts.push(`Status: ${error.statusCode}${error.statusText ? ` ${error.statusText}` : ""}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Network details
|
|
163
|
-
if (error.mimeType) {
|
|
164
|
-
parts.push(`Content-Type: ${error.mimeType}`);
|
|
165
|
-
}
|
|
166
|
-
if (error.contentLength !== undefined) {
|
|
167
|
-
const sizeKB = Math.round(error.contentLength / 1024);
|
|
168
|
-
parts.push(`Size: ${sizeKB} KB`);
|
|
169
|
-
}
|
|
170
|
-
if (error.downloadedBytes !== undefined && error.contentLength) {
|
|
171
|
-
const percent = Math.round((error.downloadedBytes / error.contentLength) * 100);
|
|
172
|
-
parts.push(`Downloaded: ${percent}%`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Retry hint
|
|
176
|
-
if (error.retryable) {
|
|
177
|
-
parts.push("This error may be retried.");
|
|
178
|
-
} else {
|
|
179
|
-
parts.push("This error is not retryable.");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return parts.join("\n");
|
|
183
|
-
}
|
|
184
139
|
|
|
185
140
|
/**
|
|
186
141
|
* Format a single FetchResult for display.
|
|
@@ -274,33 +229,3 @@ export function formatBatchResult(result: BatchFetchResult): string {
|
|
|
274
229
|
* @param error - Fetch error
|
|
275
230
|
* @returns Formatted error string
|
|
276
231
|
*/
|
|
277
|
-
export function formatErrorResult(error: FetchError): string {
|
|
278
|
-
const lines: string[] = [];
|
|
279
|
-
|
|
280
|
-
lines.push(`# Fetch Error`);
|
|
281
|
-
lines.push("");
|
|
282
|
-
lines.push(`**${error.error}**`);
|
|
283
|
-
lines.push("");
|
|
284
|
-
lines.push(`Code: \`${error.code}\``);
|
|
285
|
-
lines.push(`Phase: \`${error.phase}\``);
|
|
286
|
-
|
|
287
|
-
if (error.url) {
|
|
288
|
-
lines.push("");
|
|
289
|
-
lines.push(`URL: ${error.url}`);
|
|
290
|
-
if (error.finalUrl && error.finalUrl !== error.url) {
|
|
291
|
-
lines.push(`Final URL: ${error.finalUrl}`);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (error.statusCode) {
|
|
296
|
-
lines.push("");
|
|
297
|
-
lines.push(`HTTP Status: ${error.statusCode}${error.statusText ? ` ${error.statusText}` : ""}`);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (error.retryable) {
|
|
301
|
-
lines.push("");
|
|
302
|
-
lines.push(`*This error may be retried.*`);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return lines.join("\n");
|
|
306
|
-
}
|
package/src/engine/types.ts
CHANGED
|
@@ -128,42 +128,3 @@ export interface BatchFetchResult {
|
|
|
128
128
|
items: BatchFetchItemResult[];
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
// ─── Progress Types ────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
/** Status of a single URL in the fetch pipeline */
|
|
134
|
-
export type FetchProgressStatus =
|
|
135
|
-
| "queued"
|
|
136
|
-
| "connecting"
|
|
137
|
-
| "waiting"
|
|
138
|
-
| "loading"
|
|
139
|
-
| "processing"
|
|
140
|
-
| "done"
|
|
141
|
-
| "error";
|
|
142
|
-
|
|
143
|
-
/** Progress update for a single URL */
|
|
144
|
-
export interface FetchProgress {
|
|
145
|
-
/** URL being fetched */
|
|
146
|
-
url: string;
|
|
147
|
-
/** Current pipeline status */
|
|
148
|
-
status: FetchProgressStatus;
|
|
149
|
-
/** Progress percentage (0-100) */
|
|
150
|
-
percent: number;
|
|
151
|
-
/** Bytes loaded so far */
|
|
152
|
-
bytesLoaded: number;
|
|
153
|
-
/** Total bytes expected */
|
|
154
|
-
bytesTotal: number;
|
|
155
|
-
/** Current phase label */
|
|
156
|
-
phase: string;
|
|
157
|
-
/** Error details (if status is "error") */
|
|
158
|
-
error?: FetchError;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ─── Execution Hooks ───────────────────────────────────────────
|
|
162
|
-
|
|
163
|
-
/** Hooks for observing fetch execution progress */
|
|
164
|
-
export interface FetchExecutionHooks {
|
|
165
|
-
/** Called with progress updates for a single URL fetch */
|
|
166
|
-
onProgress?: (progress: FetchProgress) => void;
|
|
167
|
-
/** Called with full progress snapshot for batch fetches */
|
|
168
|
-
onUpdate?: (progress: FetchProgress[]) => void;
|
|
169
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -28,7 +28,6 @@ import "./providers/serpapi.js";
|
|
|
28
28
|
import "./providers/tavily.js";
|
|
29
29
|
import "./providers/firecrawl.js";
|
|
30
30
|
import "./providers/perplexity.js";
|
|
31
|
-
import "./providers/llm-summarize.js";
|
|
32
31
|
|
|
33
32
|
/** Package version */
|
|
34
33
|
const VERSION = getPackageVersion(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -91,7 +90,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
91
90
|
).length;
|
|
92
91
|
|
|
93
92
|
// Check smart-fetch engine availability
|
|
94
|
-
const deps =
|
|
93
|
+
const deps = checkDependencies();
|
|
95
94
|
const smartFetchStatus = deps.available ? "✓ Ready" : `Missing: ${deps.missing.join(", ")}`;
|
|
96
95
|
|
|
97
96
|
// wigolo status — only probe when the user has it enabled, so a
|
package/src/providers/base.ts
CHANGED
|
@@ -79,9 +79,6 @@ export interface WebProvider {
|
|
|
79
79
|
*/
|
|
80
80
|
ranking: ProviderRanking;
|
|
81
81
|
|
|
82
|
-
/** Provider-specific configuration */
|
|
83
|
-
config: Record<string, unknown>;
|
|
84
|
-
|
|
85
82
|
/**
|
|
86
83
|
* Search the web.
|
|
87
84
|
* @param query - Search query string
|
|
@@ -106,11 +103,4 @@ export interface WebProvider {
|
|
|
106
103
|
* @returns Summarized content
|
|
107
104
|
*/
|
|
108
105
|
summarize?(url: string, prompt?: string, config?: ProviderConfig): Promise<SummarizeResult>;
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Validate API key (optional).
|
|
112
|
-
* @param apiKey - API key to validate
|
|
113
|
-
* @returns true if valid
|
|
114
|
-
*/
|
|
115
|
-
validateApiKey?(apiKey: string): Promise<boolean>;
|
|
116
106
|
}
|
|
@@ -79,7 +79,6 @@ const firecrawlProvider: WebProvider = {
|
|
|
79
79
|
read: 3,
|
|
80
80
|
summarize: 0,
|
|
81
81
|
},
|
|
82
|
-
config: {},
|
|
83
82
|
|
|
84
83
|
async read(url: string, config?: ProviderConfig): Promise<ReadResult> {
|
|
85
84
|
const apiKey = config?.apiKey || process.env.FIRECRAWL_API_KEY;
|
|
@@ -89,14 +88,6 @@ const firecrawlProvider: WebProvider = {
|
|
|
89
88
|
return readFirecrawl(url, apiKey);
|
|
90
89
|
},
|
|
91
90
|
|
|
92
|
-
async validateApiKey(apiKey: string): Promise<boolean> {
|
|
93
|
-
try {
|
|
94
|
-
await readFirecrawl("https://example.com", apiKey);
|
|
95
|
-
return true;
|
|
96
|
-
} catch {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
91
|
};
|
|
101
92
|
|
|
102
93
|
// Register provider
|
|
@@ -66,21 +66,12 @@ const jinaReaderProvider: WebProvider = {
|
|
|
66
66
|
read: 2,
|
|
67
67
|
summarize: 0,
|
|
68
68
|
},
|
|
69
|
-
config: {},
|
|
70
69
|
|
|
71
70
|
async read(url: string, config?: ProviderConfig): Promise<ReadResult> {
|
|
72
71
|
const apiKey = config?.apiKey || process.env.JINA_API_KEY;
|
|
73
72
|
return readJina(url, apiKey);
|
|
74
73
|
},
|
|
75
74
|
|
|
76
|
-
async validateApiKey(apiKey: string): Promise<boolean> {
|
|
77
|
-
try {
|
|
78
|
-
await readJina("https://example.com", apiKey);
|
|
79
|
-
return true;
|
|
80
|
-
} catch {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
75
|
};
|
|
85
76
|
|
|
86
77
|
// Register provider
|
|
@@ -65,21 +65,12 @@ const jinaSearchProvider: WebProvider = {
|
|
|
65
65
|
read: 0,
|
|
66
66
|
summarize: 0,
|
|
67
67
|
},
|
|
68
|
-
config: {},
|
|
69
68
|
|
|
70
69
|
async search(query: string, config?: ProviderConfig): Promise<SearchResult[]> {
|
|
71
70
|
const apiKey = config?.apiKey || process.env.JINA_API_KEY;
|
|
72
71
|
return searchJina(query, apiKey);
|
|
73
72
|
},
|
|
74
73
|
|
|
75
|
-
async validateApiKey(apiKey: string): Promise<boolean> {
|
|
76
|
-
try {
|
|
77
|
-
const results = await searchJina("test", apiKey);
|
|
78
|
-
return Array.isArray(results);
|
|
79
|
-
} catch {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
74
|
};
|
|
84
75
|
|
|
85
76
|
// Register provider
|
|
@@ -142,7 +142,6 @@ const perplexityProvider: WebProvider = {
|
|
|
142
142
|
read: 4,
|
|
143
143
|
summarize: 1,
|
|
144
144
|
},
|
|
145
|
-
config: {},
|
|
146
145
|
|
|
147
146
|
async search(query: string, config?: ProviderConfig): Promise<SearchResult[]> {
|
|
148
147
|
const apiKey = config?.apiKey || process.env.PERPLEXITY_API_KEY;
|
|
@@ -175,14 +174,6 @@ const perplexityProvider: WebProvider = {
|
|
|
175
174
|
return summarizePerplexity(url, prompt, apiKey);
|
|
176
175
|
},
|
|
177
176
|
|
|
178
|
-
async validateApiKey(apiKey: string): Promise<boolean> {
|
|
179
|
-
try {
|
|
180
|
-
await searchPerplexity("test", apiKey);
|
|
181
|
-
return true;
|
|
182
|
-
} catch {
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
177
|
};
|
|
187
178
|
|
|
188
179
|
// Register provider
|
|
@@ -8,25 +8,14 @@
|
|
|
8
8
|
import type {
|
|
9
9
|
WebProvider,
|
|
10
10
|
WebCapability,
|
|
11
|
-
ProviderConfig,
|
|
12
11
|
} from "./base.js";
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* ProviderRegistry manages all registered web providers.
|
|
16
|
-
*
|
|
17
|
-
* Provides methods to:
|
|
18
|
-
* - Register providers
|
|
19
|
-
* - Retrieve providers by ID
|
|
20
|
-
* - Get providers for a specific capability
|
|
21
|
-
* - Get ranked providers for smart selection
|
|
22
15
|
*/
|
|
23
16
|
export class ProviderRegistry {
|
|
24
17
|
private providers: Map<string, WebProvider> = new Map();
|
|
25
18
|
|
|
26
|
-
/**
|
|
27
|
-
* Register a provider.
|
|
28
|
-
* @param provider - Provider to register
|
|
29
|
-
*/
|
|
30
19
|
register(provider: WebProvider): void {
|
|
31
20
|
if (this.providers.has(provider.id)) {
|
|
32
21
|
throw new Error(`Provider "${provider.id}" is already registered`);
|
|
@@ -34,94 +23,25 @@ export class ProviderRegistry {
|
|
|
34
23
|
this.providers.set(provider.id, provider);
|
|
35
24
|
}
|
|
36
25
|
|
|
37
|
-
/**
|
|
38
|
-
* Unregister a provider.
|
|
39
|
-
* @param providerId - Provider ID to unregister
|
|
40
|
-
*/
|
|
41
|
-
unregister(providerId: string): void {
|
|
42
|
-
this.providers.delete(providerId);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Get a provider by ID.
|
|
47
|
-
* @param providerId - Provider ID
|
|
48
|
-
* @returns Provider or undefined
|
|
49
|
-
*/
|
|
50
26
|
getProvider(providerId: string): WebProvider | undefined {
|
|
51
27
|
return this.providers.get(providerId);
|
|
52
28
|
}
|
|
53
29
|
|
|
54
|
-
/**
|
|
55
|
-
* Get all registered providers.
|
|
56
|
-
* @returns Array of all providers
|
|
57
|
-
*/
|
|
58
30
|
getAllProviders(): WebProvider[] {
|
|
59
31
|
return Array.from(this.providers.values());
|
|
60
32
|
}
|
|
61
33
|
|
|
62
|
-
/**
|
|
63
|
-
* Get providers that support a specific capability.
|
|
64
|
-
* @param capability - Capability to filter by
|
|
65
|
-
* @returns Array of providers with the capability
|
|
66
|
-
*/
|
|
67
34
|
getProvidersForCapability(capability: WebCapability): WebProvider[] {
|
|
68
35
|
return this.getAllProviders().filter((p) =>
|
|
69
36
|
p.capabilities.includes(capability)
|
|
70
37
|
);
|
|
71
38
|
}
|
|
72
39
|
|
|
73
|
-
/**
|
|
74
|
-
* Get ranked providers for a specific capability.
|
|
75
|
-
* Sorted by ranking (lower = better/simpler/cheaper).
|
|
76
|
-
* @param capability - Capability to rank by
|
|
77
|
-
* @returns Array of providers sorted by ranking
|
|
78
|
-
*/
|
|
79
40
|
getRankedProviders(capability: WebCapability): WebProvider[] {
|
|
80
41
|
return this.getProvidersForCapability(capability)
|
|
81
42
|
.filter((p) => p.ranking[capability] > 0)
|
|
82
43
|
.sort((a, b) => a.ranking[capability] - b.ranking[capability]);
|
|
83
44
|
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Get the best provider for a capability (lowest rank).
|
|
87
|
-
* @param capability - Capability to find best provider for
|
|
88
|
-
* @returns Best provider or undefined
|
|
89
|
-
*/
|
|
90
|
-
getBestProvider(capability: WebCapability): WebProvider | undefined {
|
|
91
|
-
const ranked = this.getRankedProviders(capability);
|
|
92
|
-
return ranked[0];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Get a provider by rank for a capability.
|
|
97
|
-
* @param capability - Capability to search
|
|
98
|
-
* @param rank - Desired rank (1-based)
|
|
99
|
-
* @returns Provider at that rank or undefined
|
|
100
|
-
*/
|
|
101
|
-
getProviderByRank(capability: WebCapability, rank: number): WebProvider | undefined {
|
|
102
|
-
const ranked = this.getRankedProviders(capability);
|
|
103
|
-
return ranked.find((p) => p.ranking[capability] === rank);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Get enabled providers based on configuration.
|
|
108
|
-
* @param configMap - Map of provider ID to config
|
|
109
|
-
* @returns Array of enabled providers
|
|
110
|
-
*/
|
|
111
|
-
getEnabledProviders(configMap: Map<string, ProviderConfig>): WebProvider[] {
|
|
112
|
-
return this.getAllProviders().filter((p) => {
|
|
113
|
-
const config = configMap.get(p.id);
|
|
114
|
-
return config?.enabled !== false;
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Get provider count.
|
|
120
|
-
* @returns Number of registered providers
|
|
121
|
-
*/
|
|
122
|
-
get count(): number {
|
|
123
|
-
return this.providers.size;
|
|
124
|
-
}
|
|
125
45
|
}
|
|
126
46
|
|
|
127
47
|
/** Singleton registry instance */
|
package/src/providers/serpapi.ts
CHANGED
|
@@ -60,7 +60,6 @@ const serpapiProvider: WebProvider = {
|
|
|
60
60
|
read: 0,
|
|
61
61
|
summarize: 0,
|
|
62
62
|
},
|
|
63
|
-
config: {},
|
|
64
63
|
|
|
65
64
|
async search(query: string, config?: ProviderConfig): Promise<SearchResult[]> {
|
|
66
65
|
const apiKey = config?.apiKey || process.env.SERPAPI_KEY;
|
|
@@ -70,14 +69,6 @@ const serpapiProvider: WebProvider = {
|
|
|
70
69
|
return searchSerpAPI(query, apiKey);
|
|
71
70
|
},
|
|
72
71
|
|
|
73
|
-
async validateApiKey(apiKey: string): Promise<boolean> {
|
|
74
|
-
try {
|
|
75
|
-
const results = await searchSerpAPI("test", apiKey);
|
|
76
|
-
return Array.isArray(results);
|
|
77
|
-
} catch {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
72
|
};
|
|
82
73
|
|
|
83
74
|
// Register provider
|
package/src/providers/tavily.ts
CHANGED
|
@@ -69,7 +69,6 @@ const tavilyProvider: WebProvider = {
|
|
|
69
69
|
read: 0,
|
|
70
70
|
summarize: 0,
|
|
71
71
|
},
|
|
72
|
-
config: {},
|
|
73
72
|
|
|
74
73
|
async search(query: string, config?: ProviderConfig): Promise<SearchResult[]> {
|
|
75
74
|
const apiKey = config?.apiKey || process.env.TAVILY_API_KEY;
|
|
@@ -79,14 +78,6 @@ const tavilyProvider: WebProvider = {
|
|
|
79
78
|
return searchTavily(query, apiKey);
|
|
80
79
|
},
|
|
81
80
|
|
|
82
|
-
async validateApiKey(apiKey: string): Promise<boolean> {
|
|
83
|
-
try {
|
|
84
|
-
const results = await searchTavily("test", apiKey);
|
|
85
|
-
return Array.isArray(results);
|
|
86
|
-
} catch {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
81
|
};
|
|
91
82
|
|
|
92
83
|
// Register provider
|
|
@@ -79,7 +79,6 @@ let clientPromise: Promise<WigoloLocalClient> | null = null;
|
|
|
79
79
|
let clientOverride: WigoloClientLike | null = null;
|
|
80
80
|
|
|
81
81
|
/** Last known availability, for the settings TUI and info screen. */
|
|
82
|
-
let lastError: string | null = null;
|
|
83
82
|
|
|
84
83
|
/** Load the optional SDK. Returns null when it is not installed. */
|
|
85
84
|
async function loadSdk(): Promise<
|
|
@@ -127,10 +126,8 @@ export async function getWigoloClient(): Promise<WigoloClientLike> {
|
|
|
127
126
|
|
|
128
127
|
try {
|
|
129
128
|
const local = await clientPromise;
|
|
130
|
-
lastError = null;
|
|
131
129
|
return local.client;
|
|
132
130
|
} catch (error) {
|
|
133
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
134
131
|
throw error;
|
|
135
132
|
}
|
|
136
133
|
}
|
|
@@ -149,29 +146,6 @@ export async function closeWigoloClient(): Promise<void> {
|
|
|
149
146
|
}
|
|
150
147
|
|
|
151
148
|
/** Availability for the settings TUI / info screen. Never throws. */
|
|
152
|
-
export async function checkWigoloHealth(): Promise<{
|
|
153
|
-
available: boolean;
|
|
154
|
-
status: string;
|
|
155
|
-
detail?: string;
|
|
156
|
-
}> {
|
|
157
|
-
const sdk = await loadSdk();
|
|
158
|
-
if (!sdk) {
|
|
159
|
-
return { available: false, status: "not installed", detail: NOT_INSTALLED_MESSAGE };
|
|
160
|
-
}
|
|
161
|
-
try {
|
|
162
|
-
const client = await getWigoloClient();
|
|
163
|
-
const health = await client.health();
|
|
164
|
-
const status = typeof health?.status === "string" ? health.status : "unknown";
|
|
165
|
-
// wigolo reports 200 "ok" when up and 503 with a body when degraded.
|
|
166
|
-
return { available: status === "ok" || status === "healthy", status };
|
|
167
|
-
} catch (error) {
|
|
168
|
-
return {
|
|
169
|
-
available: false,
|
|
170
|
-
status: "unreachable",
|
|
171
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
149
|
|
|
176
150
|
/** Whether the SDK is importable, without starting a daemon. Never throws. */
|
|
177
151
|
export async function isWigoloInstalled(): Promise<boolean> {
|
|
@@ -179,9 +153,6 @@ export async function isWigoloInstalled(): Promise<boolean> {
|
|
|
179
153
|
}
|
|
180
154
|
|
|
181
155
|
/** Last recorded failure, for diagnostics. */
|
|
182
|
-
export function getWigoloLastError(): string | null {
|
|
183
|
-
return lastError;
|
|
184
|
-
}
|
|
185
156
|
|
|
186
157
|
/** Inject a stub daemon client. Test-only. */
|
|
187
158
|
export function __setWigoloClientForTests(client: WigoloClientLike | null): void {
|
|
@@ -192,5 +163,4 @@ export function __setWigoloClientForTests(client: WigoloClientLike | null): void
|
|
|
192
163
|
export function __resetWigoloClientForTests(): void {
|
|
193
164
|
clientPromise = null;
|
|
194
165
|
clientOverride = null;
|
|
195
|
-
lastError = null;
|
|
196
166
|
}
|
package/src/providers/wigolo.ts
CHANGED
package/src/settings.ts
CHANGED
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import * as os from "node:os";
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_BROWSER,
|
|
13
|
+
DEFAULT_OS,
|
|
14
|
+
DEFAULT_MAX_CHARS,
|
|
15
|
+
DEFAULT_TIMEOUT_MS,
|
|
16
|
+
DEFAULT_BATCH_CONCURRENCY,
|
|
17
|
+
DEFAULT_REMOVE_IMAGES,
|
|
18
|
+
DEFAULT_INCLUDE_REPLIES,
|
|
19
|
+
} from "./engine/constants.js";
|
|
11
20
|
|
|
12
21
|
/** Auth storage structure (API keys) */
|
|
13
22
|
export interface WebApiAuth {
|
|
@@ -21,12 +30,6 @@ export interface ProviderSettings {
|
|
|
21
30
|
[key: string]: unknown;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
|
-
/** Cache configuration */
|
|
25
|
-
export interface CacheSettings {
|
|
26
|
-
enabled: boolean;
|
|
27
|
-
ttlMs: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
33
|
/** Smart-fetch default settings */
|
|
31
34
|
export interface SmartFetchSettings {
|
|
32
35
|
/** TLS fingerprint browser profile */
|
|
@@ -48,19 +51,18 @@ export interface SmartFetchSettings {
|
|
|
48
51
|
/** Config storage structure */
|
|
49
52
|
export interface WebApiConfig {
|
|
50
53
|
providers: Record<string, ProviderSettings>;
|
|
51
|
-
cache: CacheSettings;
|
|
52
54
|
smartFetch?: Partial<SmartFetchSettings>;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
/** Default smart-fetch settings */
|
|
57
|
+
/** Default smart-fetch settings — values from engine/constants.ts */
|
|
56
58
|
const DEFAULT_SMART_FETCH_SETTINGS: SmartFetchSettings = {
|
|
57
|
-
browser:
|
|
58
|
-
os:
|
|
59
|
-
maxChars:
|
|
60
|
-
timeoutMs:
|
|
61
|
-
batchConcurrency:
|
|
62
|
-
removeImages:
|
|
63
|
-
includeReplies:
|
|
59
|
+
browser: DEFAULT_BROWSER,
|
|
60
|
+
os: DEFAULT_OS,
|
|
61
|
+
maxChars: DEFAULT_MAX_CHARS,
|
|
62
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
63
|
+
batchConcurrency: DEFAULT_BATCH_CONCURRENCY,
|
|
64
|
+
removeImages: DEFAULT_REMOVE_IMAGES,
|
|
65
|
+
includeReplies: DEFAULT_INCLUDE_REPLIES,
|
|
64
66
|
};
|
|
65
67
|
|
|
66
68
|
/** Default configuration */
|
|
@@ -76,10 +78,6 @@ const DEFAULT_CONFIG: WebApiConfig = {
|
|
|
76
78
|
perplexity: { enabled: false },
|
|
77
79
|
"llm-summarize": { enabled: true },
|
|
78
80
|
},
|
|
79
|
-
cache: {
|
|
80
|
-
enabled: true,
|
|
81
|
-
ttlMs: 3600000, // 1 hour
|
|
82
|
-
},
|
|
83
81
|
smartFetch: {},
|
|
84
82
|
};
|
|
85
83
|
|
|
@@ -159,10 +157,6 @@ export function loadConfig(): WebApiConfig {
|
|
|
159
157
|
...DEFAULT_CONFIG.providers,
|
|
160
158
|
...config.providers,
|
|
161
159
|
},
|
|
162
|
-
cache: {
|
|
163
|
-
...DEFAULT_CONFIG.cache,
|
|
164
|
-
...config.cache,
|
|
165
|
-
},
|
|
166
160
|
};
|
|
167
161
|
}
|
|
168
162
|
} catch {
|
|
@@ -237,62 +231,8 @@ export function setProviderEnabled(providerId: string, enabled: boolean): void {
|
|
|
237
231
|
saveConfig(config);
|
|
238
232
|
}
|
|
239
233
|
|
|
240
|
-
/**
|
|
241
|
-
* Get cache settings.
|
|
242
|
-
* @returns Cache configuration
|
|
243
|
-
*/
|
|
244
|
-
export function getCacheSettings(): CacheSettings {
|
|
245
|
-
const config = loadConfig();
|
|
246
|
-
return config.cache;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Update cache settings.
|
|
251
|
-
* @param cache - New cache settings
|
|
252
|
-
*/
|
|
253
|
-
export function updateCacheSettings(cache: Partial<CacheSettings>): void {
|
|
254
|
-
const config = loadConfig();
|
|
255
|
-
config.cache = {
|
|
256
|
-
...config.cache,
|
|
257
|
-
...cache,
|
|
258
|
-
};
|
|
259
|
-
saveConfig(config);
|
|
260
|
-
}
|
|
261
234
|
|
|
262
|
-
/**
|
|
263
|
-
* Validate API key format (basic validation).
|
|
264
|
-
* @param providerId - Provider ID
|
|
265
|
-
* @param apiKey - API key to validate
|
|
266
|
-
* @returns true if format looks valid
|
|
267
|
-
*/
|
|
268
|
-
export function validateApiKeyFormat(providerId: string, apiKey: string): boolean {
|
|
269
|
-
if (!apiKey || apiKey.trim().length === 0) {
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
235
|
|
|
273
|
-
// Provider-specific format checks
|
|
274
|
-
switch (providerId) {
|
|
275
|
-
case "serpapi":
|
|
276
|
-
// SerpAPI keys are typically 64 characters
|
|
277
|
-
return apiKey.length >= 32;
|
|
278
|
-
case "tavily":
|
|
279
|
-
// Tavily keys start with "tvly-"
|
|
280
|
-
return apiKey.startsWith("tvly-") && apiKey.length >= 10;
|
|
281
|
-
case "firecrawl":
|
|
282
|
-
// Firecrawl keys are typically longer
|
|
283
|
-
return apiKey.length >= 20;
|
|
284
|
-
case "perplexity":
|
|
285
|
-
// Perplexity keys are typically longer
|
|
286
|
-
return apiKey.length >= 20;
|
|
287
|
-
case "jina-search":
|
|
288
|
-
case "jina-reader":
|
|
289
|
-
// Jina keys are typically longer
|
|
290
|
-
return apiKey.length >= 10;
|
|
291
|
-
default:
|
|
292
|
-
// Generic validation
|
|
293
|
-
return apiKey.length >= 8;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
236
|
|
|
297
237
|
/**
|
|
298
238
|
* Load smart-fetch settings.
|
package/src/tools.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
defuddleFetchMultiple,
|
|
28
28
|
} from "./engine/extract.js";
|
|
29
29
|
import type { FetchOptions, FetchResult, BatchFetchResult } from "./engine/types.js";
|
|
30
|
-
import { formatSingleResult, formatBatchResult
|
|
30
|
+
import { formatSingleResult, formatBatchResult } from "./engine/format.js";
|
|
31
31
|
|
|
32
32
|
/** Tool names */
|
|
33
33
|
export const WEB_TOOLS = {
|
|
@@ -219,6 +219,22 @@ function generateSmartFetchKey(
|
|
|
219
219
|
return parts.join(":");
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
/** Build a complete FetchOptions from partial user input + loaded defaults. */
|
|
223
|
+
function resolveFetchOptions(options: Partial<FetchOptions>): FetchOptions {
|
|
224
|
+
const defaults = loadSmartFetchSettings();
|
|
225
|
+
return {
|
|
226
|
+
browser: options.browser || defaults.browser,
|
|
227
|
+
os: options.os || defaults.os,
|
|
228
|
+
format: options.format || "markdown",
|
|
229
|
+
maxChars: options.maxChars || defaults.maxChars,
|
|
230
|
+
timeoutMs: options.timeoutMs || defaults.timeoutMs,
|
|
231
|
+
removeImages: options.removeImages ?? defaults.removeImages,
|
|
232
|
+
includeReplies: options.includeReplies ?? defaults.includeReplies,
|
|
233
|
+
proxy: options.proxy,
|
|
234
|
+
headers: options.headers,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
/**
|
|
223
239
|
* Execute smart-fetch read (single URL).
|
|
224
240
|
*/
|
|
@@ -233,19 +249,8 @@ async function executeSmartFetchRead(
|
|
|
233
249
|
return cached as FetchResult;
|
|
234
250
|
}
|
|
235
251
|
|
|
236
|
-
// Load defaults
|
|
237
|
-
const
|
|
238
|
-
const fetchOptions: FetchOptions = {
|
|
239
|
-
browser: options.browser || defaults.browser,
|
|
240
|
-
os: options.os || defaults.os,
|
|
241
|
-
format: options.format || "markdown",
|
|
242
|
-
maxChars: options.maxChars || defaults.maxChars,
|
|
243
|
-
timeoutMs: options.timeoutMs || defaults.timeoutMs,
|
|
244
|
-
removeImages: options.removeImages ?? defaults.removeImages,
|
|
245
|
-
includeReplies: options.includeReplies ?? defaults.includeReplies,
|
|
246
|
-
proxy: options.proxy,
|
|
247
|
-
headers: options.headers,
|
|
248
|
-
};
|
|
252
|
+
// Load defaults and build fetch options
|
|
253
|
+
const fetchOptions = resolveFetchOptions(options);
|
|
249
254
|
|
|
250
255
|
// Execute fetch
|
|
251
256
|
const result = await defuddleFetch(url, fetchOptions);
|
|
@@ -263,18 +268,10 @@ async function executeSmartFetchBatch(
|
|
|
263
268
|
urls: string[],
|
|
264
269
|
options: Partial<FetchOptions> & { batchConcurrency?: number } = {}
|
|
265
270
|
): Promise<BatchFetchResult> {
|
|
266
|
-
// Load defaults
|
|
271
|
+
// Load defaults and build fetch options
|
|
267
272
|
const defaults = loadSmartFetchSettings();
|
|
268
273
|
const fetchOptions: FetchOptions & { batchConcurrency?: number } = {
|
|
269
|
-
|
|
270
|
-
os: options.os || defaults.os,
|
|
271
|
-
format: options.format || "markdown",
|
|
272
|
-
maxChars: options.maxChars || defaults.maxChars,
|
|
273
|
-
timeoutMs: options.timeoutMs || defaults.timeoutMs,
|
|
274
|
-
removeImages: options.removeImages ?? defaults.removeImages,
|
|
275
|
-
includeReplies: options.includeReplies ?? defaults.includeReplies,
|
|
276
|
-
proxy: options.proxy,
|
|
277
|
-
headers: options.headers,
|
|
274
|
+
...resolveFetchOptions(options),
|
|
278
275
|
batchConcurrency: options.batchConcurrency || defaults.batchConcurrency,
|
|
279
276
|
};
|
|
280
277
|
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @unipi/web-api — LLM Summarize provider
|
|
3
|
-
*
|
|
4
|
-
* Summarization provider using pi's existing LLM.
|
|
5
|
-
* No external API key required - uses the LLM already configured in pi.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
WebProvider,
|
|
10
|
-
SummarizeResult,
|
|
11
|
-
ProviderConfig,
|
|
12
|
-
} from "./base.js";
|
|
13
|
-
import { registry } from "./registry.js";
|
|
14
|
-
|
|
15
|
-
/** Default summarization prompt */
|
|
16
|
-
const DEFAULT_SUMMARY_PROMPT = `Summarize the following web content concisely, highlighting the key points.
|
|
17
|
-
Focus on:
|
|
18
|
-
1. Main topic and purpose
|
|
19
|
-
2. Key facts and findings
|
|
20
|
-
3. Important conclusions or recommendations
|
|
21
|
-
|
|
22
|
-
Provide a clear, well-structured summary.`;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Summarize content using LLM.
|
|
26
|
-
* This provider delegates to pi's built-in LLM for summarization.
|
|
27
|
-
* The actual LLM call happens in the tool execution, not here.
|
|
28
|
-
*/
|
|
29
|
-
function createLLMSummarizeResult(
|
|
30
|
-
url: string,
|
|
31
|
-
content: string,
|
|
32
|
-
prompt?: string
|
|
33
|
-
): SummarizeResult {
|
|
34
|
-
// Return a placeholder - actual LLM call happens in tool execution
|
|
35
|
-
return {
|
|
36
|
-
url: url,
|
|
37
|
-
summary: `[LLM Summary placeholder for ${url}]`,
|
|
38
|
-
prompt: prompt || DEFAULT_SUMMARY_PROMPT,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** LLM Summarize provider implementation */
|
|
43
|
-
const llmSummarizeProvider: WebProvider = {
|
|
44
|
-
id: "llm-summarize",
|
|
45
|
-
name: "LLM Summarize",
|
|
46
|
-
capabilities: ["summarize"],
|
|
47
|
-
requiresApiKey: false,
|
|
48
|
-
ranking: {
|
|
49
|
-
search: 0,
|
|
50
|
-
read: 0,
|
|
51
|
-
summarize: 2,
|
|
52
|
-
},
|
|
53
|
-
config: {
|
|
54
|
-
defaultPrompt: DEFAULT_SUMMARY_PROMPT,
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
async summarize(url: string, prompt?: string, _config?: ProviderConfig): Promise<SummarizeResult> {
|
|
58
|
-
// This is a placeholder - actual implementation will be in the tool
|
|
59
|
-
// The tool will:
|
|
60
|
-
// 1. Fetch content using a read provider
|
|
61
|
-
// 2. Send to LLM with the prompt
|
|
62
|
-
// 3. Return the LLM's summary
|
|
63
|
-
|
|
64
|
-
return createLLMSummarizeResult(url, "", prompt);
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// Register provider
|
|
69
|
-
registry.register(llmSummarizeProvider);
|
|
70
|
-
|
|
71
|
-
export { llmSummarizeProvider, DEFAULT_SUMMARY_PROMPT };
|