@pi-unipi/web-api 2.14.0 → 2.14.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/web-api",
3
- "version": "2.14.0",
3
+ "version": "2.14.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",
@@ -31,7 +31,7 @@
31
31
  "README.md"
32
32
  ],
33
33
  "dependencies": {
34
- "@pi-unipi/core": "2.14.0",
34
+ "@pi-unipi/core": "2.14.2",
35
35
  "defuddle": "^0.18.1",
36
36
  "linkedom": "^0.18.12",
37
37
  "wreq-js": "^2.3.0"
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @unipi/web-api — error serialization
3
+ *
4
+ * Engines and providers throw a mix of Error instances and plain objects
5
+ * (FetchError-shaped payloads, wreq-js native errors, provider API bodies).
6
+ * `String()` on a plain object yields "[object Object]", which masked the
7
+ * real diagnosis behind "Read failed: [object Object]". Everything that
8
+ * surfaces an error to the agent must go through describeError().
9
+ */
10
+
11
+ /** Best-effort human-readable message for any thrown value. */
12
+ export function describeError(error: unknown): string {
13
+ if (error instanceof Error) return error.message || error.toString();
14
+ if (typeof error === "string") return error;
15
+ if (error && typeof error === "object") {
16
+ const obj = error as Record<string, unknown>;
17
+ for (const key of ["message", "error", "reason", "detail"]) {
18
+ const v = obj[key];
19
+ if (typeof v === "string" && v.trim()) return v;
20
+ }
21
+ try {
22
+ return JSON.stringify(error) ?? String(error);
23
+ } catch {
24
+ // circular or otherwise unserializable — describe by shape
25
+ const keys = Object.getOwnPropertyNames(obj).join(",");
26
+ return keys ? `[${keys}]` : String(error);
27
+ }
28
+ }
29
+ return String(error);
30
+ }
@@ -25,6 +25,7 @@ import {
25
25
  } from "./constants.js";
26
26
  import { resolveBrowserProfile, resolveOSProfile } from "./profiles.js";
27
27
  import { getWreq, getDefuddle } from "./dependencies.js";
28
+ import { describeError } from "./errors.js";
28
29
  import { parseHTML, extractTextContent, elementToMarkdown } from "./dom.js";
29
30
  import { truncateContent, formatContent } from "./format.js";
30
31
 
@@ -68,21 +69,22 @@ function validateUrl(url: string): URL {
68
69
 
69
70
  /**
70
71
  * Create a FetchError object.
72
+ *
73
+ * Returns a REAL Error instance carrying the FetchError fields (.error,
74
+ * .code, .phase, .retryable) so boundary catches surface `message` instead
75
+ * of serializing a plain object to "[object Object]".
71
76
  */
72
- function createError(
77
+ export function createError(
73
78
  code: FetchError["code"],
74
79
  phase: FetchError["phase"],
75
80
  message: string,
76
81
  retryable: boolean,
77
82
  extra: Partial<FetchError> = {}
78
83
  ): FetchError {
79
- return {
80
- error: message,
81
- code,
82
- phase,
83
- retryable,
84
- ...extra,
85
- };
84
+ const err = new Error(message) as Error & FetchError;
85
+ err.name = "FetchError";
86
+ Object.assign(err, { error: message, code, phase, retryable, ...extra });
87
+ return err;
86
88
  }
87
89
 
88
90
  /**
@@ -401,25 +403,26 @@ export async function defuddleFetch(
401
403
  throw error;
402
404
  }
403
405
 
404
- const err = error as Error;
406
+ // Classify error describeError() is safe on plain objects from the
407
+ // native wreq binding that carry no .message of their own.
408
+ const message = describeError(error);
405
409
 
406
- // Classify error
407
- if (err.message.includes("timeout")) {
408
- throw createError("timeout", "waiting", err.message, true, {
410
+ if (message.includes("timeout")) {
411
+ throw createError("timeout", "waiting", message, true, {
409
412
  url,
410
413
  finalUrl,
411
414
  timeoutMs,
412
415
  });
413
416
  }
414
417
 
415
- if (err.message.includes("network") || err.message.includes("ECONNREFUSED")) {
416
- throw createError("network_error", "connecting", err.message, true, {
418
+ if (message.includes("network") || message.includes("ECONNREFUSED")) {
419
+ throw createError("network_error", "connecting", message, true, {
417
420
  url,
418
421
  finalUrl,
419
422
  });
420
423
  }
421
424
 
422
- throw createError("unexpected_response", "loading", err.message, false, {
425
+ throw createError("unexpected_response", "loading", message, false, {
423
426
  url,
424
427
  finalUrl,
425
428
  });
@@ -15,6 +15,8 @@
15
15
  * module-level singleton, closed on session shutdown.
16
16
  */
17
17
 
18
+ import { describeError } from "../engine/errors.js";
19
+
18
20
  /** Minimal structural types — avoids a type-level dependency on the AGPL SDK. */
19
21
  interface WigoloSearchResponse {
20
22
  results?: unknown[];
@@ -112,7 +114,7 @@ export async function getWigoloClient(): Promise<WigoloClientLike> {
112
114
  try {
113
115
  return await sdk.createLocalClient();
114
116
  } catch (error) {
115
- const detail = error instanceof Error ? error.message : String(error);
117
+ const detail = describeError(error);
116
118
  throw new WigoloUnavailableError(`${NOT_RUNNING_MESSAGE}\n→ Cause: ${detail}`);
117
119
  }
118
120
  })();
package/src/tools.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  } from "./engine/extract.js";
29
29
  import type { FetchOptions, FetchResult, BatchFetchResult } from "./engine/types.js";
30
30
  import { formatSingleResult, formatBatchResult } from "./engine/format.js";
31
+ import { describeError } from "./engine/errors.js";
31
32
 
32
33
  /** Tool names */
33
34
  export const WEB_TOOLS = {
@@ -124,7 +125,7 @@ export async function withProviderFallthrough<T>(
124
125
  try {
125
126
  return await attempt(provider);
126
127
  } catch (error) {
127
- const message = error instanceof Error ? error.message : String(error);
128
+ const message = describeError(error);
128
129
  failures.push(`${provider.name}: ${message}`);
129
130
  }
130
131
  }
@@ -339,7 +340,7 @@ export function registerWebTools(pi: ExtensionAPI): void {
339
340
  };
340
341
  } catch (error) {
341
342
  const message =
342
- error instanceof Error ? error.message : String(error);
343
+ describeError(error);
343
344
  return {
344
345
  content: [{ type: "text", text: `Search failed: ${message}` }],
345
346
  isError: true,
@@ -503,7 +504,7 @@ export function registerWebTools(pi: ExtensionAPI): void {
503
504
  return {
504
505
  url,
505
506
  status: "error",
506
- error: error instanceof Error ? error.message : String(error),
507
+ error: describeError(error),
507
508
  };
508
509
  }
509
510
  })
@@ -560,7 +561,7 @@ export function registerWebTools(pi: ExtensionAPI): void {
560
561
  };
561
562
  } catch (error) {
562
563
  const message =
563
- error instanceof Error ? error.message : String(error);
564
+ describeError(error);
564
565
  return {
565
566
  content: [{ type: "text", text: `Read failed: ${message}` }],
566
567
  isError: true,
@@ -623,7 +624,7 @@ export function registerWebTools(pi: ExtensionAPI): void {
623
624
  };
624
625
  } catch (error) {
625
626
  const message =
626
- error instanceof Error ? error.message : String(error);
627
+ describeError(error);
627
628
  return {
628
629
  content: [{ type: "text", text: `Summarize failed: ${message}` }],
629
630
  isError: true,