@pi-unipi/web-api 2.6.1 → 2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/web-api",
3
- "version": "2.6.1",
3
+ "version": "2.9.0",
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",
@@ -31,32 +31,32 @@
31
31
  "README.md"
32
32
  ],
33
33
  "dependencies": {
34
- "@pi-unipi/core": "2.6.1",
34
+ "@pi-unipi/core": "2.9.0",
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": {
42
40
  "wigolo-sdk": "^0.2.1"
43
41
  },
44
42
  "peerDependencies": {
45
- "@earendil-works/pi-coding-agent": "^0.80.0",
46
- "@earendil-works/pi-tui": "^0.80.0",
43
+ "@earendil-works/pi-coding-agent": "^0.84.0",
44
+ "@earendil-works/pi-tui": "^0.84.0",
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
- "skills": [],
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
- * Lazy-loaded dependencies for the smart-fetch engine.
5
- * Uses dynamic imports to handle optional native binding failures gracefully.
4
+ * wreq-js and defuddle are hard dependencies (always installed).
5
+ * Static imports replace the former lazy-loader theater.
6
6
  */
7
7
 
8
- let wreqModule: Record<string, unknown> | null = null;
9
- let defuddleModule: Record<string, unknown> | null = null;
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
- * Get the wreq-js module.
15
- * Throws a helpful error if the module is not available.
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
- * Get the mime-types module.
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
- * Check if all required dependencies are available.
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
  }
@@ -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, getMimeTypes } from "./dependencies.js";
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 = await getWreq();
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 = await getDefuddle();
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
- progress[index] = {
535
- url,
536
- status: "connecting",
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(url, fetchOptions, {
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
 
@@ -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
- }
@@ -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 = await checkDependencies();
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
@@ -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
  }
@@ -146,7 +146,6 @@ const duckduckgoProvider: WebProvider = {
146
146
  read: 0,
147
147
  summarize: 0,
148
148
  },
149
- config: {},
150
149
 
151
150
  async search(query: string, _config?: ProviderConfig): Promise<SearchResult[]> {
152
151
  return searchDDG(query);
@@ -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 */
@@ -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
@@ -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
  }
@@ -71,7 +71,6 @@ const wigoloProvider: WebProvider = {
71
71
  read: 1,
72
72
  summarize: 0,
73
73
  },
74
- config: {},
75
74
 
76
75
  async search(query: string, config?: ProviderConfig): Promise<SearchResult[]> {
77
76
  const client = await getWigoloClient();
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: "chrome_145",
58
- os: "windows",
59
- maxChars: 50000,
60
- timeoutMs: 15000,
61
- batchConcurrency: 8,
62
- removeImages: false,
63
- includeReplies: "extractors",
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, formatErrorResult } from "./engine/format.js";
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 defaults = loadSmartFetchSettings();
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
- browser: options.browser || defaults.browser,
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
 
@@ -13,7 +13,6 @@ import {
13
13
  removeApiKey,
14
14
  isProviderEnabled,
15
15
  setProviderEnabled,
16
- validateApiKeyFormat,
17
16
  loadSmartFetchSettings,
18
17
  saveSmartFetchSettings,
19
18
  resetSmartFetchSettings,
@@ -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 };