webfox 4.4.2 → 4.6.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.
@@ -2,7 +2,7 @@ import {
2
2
  SERPBASE_MODES,
3
3
  serpbaseProvider,
4
4
  validateOptions
5
- } from "./chunk-VYMIR35R.js";
5
+ } from "./chunk-KH54YH7D.js";
6
6
  import "./chunk-GPACLYNT.js";
7
7
  import {
8
8
  trimSnippet
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  optionSchema,
3
3
  providers
4
- } from "./chunk-VYMIR35R.js";
4
+ } from "./chunk-KH54YH7D.js";
5
5
  import {
6
6
  CAPABILITIES
7
7
  } from "./chunk-GPACLYNT.js";
@@ -9,7 +9,7 @@ import {
9
9
  // package.json
10
10
  var package_default = {
11
11
  name: "webfox",
12
- version: "4.4.2",
12
+ version: "4.6.0",
13
13
  description: "Search the web, extract pages, get grounded answers, and run deep research with the web CLI, a TypeScript library, and a pi extension. Bring your own providers and API keys.",
14
14
  type: "module",
15
15
  files: [
@@ -107,15 +107,14 @@ var package_default = {
107
107
  openai: "^7.10.0",
108
108
  "parallel-web": "^1.3.3",
109
109
  picocolors: "^1.1.1",
110
- typebox: "^1.3.6",
111
110
  "valyu-js": "^2.10.1",
112
111
  yaml: "^2.9.0"
113
112
  },
114
113
  devDependencies: {
115
114
  "@biomejs/biome": "^2.5.4",
116
- "@earendil-works/pi-ai": "^0.85.1",
117
- "@earendil-works/pi-coding-agent": "^0.85.1",
118
- "@earendil-works/pi-tui": "^0.85.1",
115
+ "@earendil-works/pi-ai": "^0.99.0",
116
+ "@earendil-works/pi-coding-agent": "^0.99.0",
117
+ "@earendil-works/pi-tui": "^0.99.0",
119
118
  "@types/node": "^26.1.1",
120
119
  esbuild: "^0.28.1",
121
120
  typescript: "^7.0.2",
@@ -123,7 +122,8 @@ var package_default = {
123
122
  },
124
123
  peerDependencies: {
125
124
  "@earendil-works/pi-coding-agent": "*",
126
- "@earendil-works/pi-tui": "*"
125
+ "@earendil-works/pi-tui": "*",
126
+ typebox: "*"
127
127
  },
128
128
  peerDependenciesMeta: {
129
129
  "@earendil-works/pi-coding-agent": {
@@ -637,6 +637,11 @@ var customProvider = defineProvider({
637
637
  });
638
638
 
639
639
  // src/providers/exa/definition.ts
640
+ var snapshotAsOf = {
641
+ type: "string",
642
+ minLength: 1,
643
+ description: "ISO datetime cutoff, such as '2026-07-01T00:00:00Z'. Return the newest stored page version at or before this instant; pages without an eligible version are omitted. Do not combine with maxAgeHours, livecrawlTimeout, or subpages."
644
+ };
640
645
  var exaProvider = defineProvider({
641
646
  id: "exa",
642
647
  label: "Exa",
@@ -844,6 +849,7 @@ var exaProvider = defineProvider({
844
849
  ],
845
850
  description: "Include AI-generated summary."
846
851
  },
852
+ snapshotAsOf,
847
853
  livecrawlTimeout: {
848
854
  type: "integer",
849
855
  minimum: 1,
@@ -897,7 +903,7 @@ var exaProvider = defineProvider({
897
903
  }
898
904
  },
899
905
  additionalProperties: false,
900
- description: "Content extraction and freshness controls. Put text, highlights, summary, maxAgeHours, and livecrawlTimeout here, not directly in options."
906
+ description: "Content extraction, freshness, and historical snapshot controls. Put text, highlights, summary, snapshotAsOf, maxAgeHours, and livecrawlTimeout here, not directly in options."
901
907
  }
902
908
  },
903
909
  description: "Exa search options."
@@ -908,12 +914,27 @@ var exaProvider = defineProvider({
908
914
  "Set includeDomains or excludeDomains when the task names preferred sources, requires primary sources, or needs noisy domains filtered out.",
909
915
  "For fresh Exa search content, use options.contents.maxAgeHours=0; for cache-only retrieval use -1. Never put maxAgeHours at the top level of options.",
910
916
  "Do not send deprecated livecrawl, even alongside maxAgeHours. Remove livecrawl rather than moving it into contents. Exa ignores startCrawlDate/endCrawlDate; use startPublishedDate/endPublishedDate to filter publication dates, not cache freshness.",
917
+ "For historical page versions, set options.contents.snapshotAsOf to an ISO datetime. Use type auto, fast, or instant; omit category, maxAgeHours, livecrawl, livecrawlTimeout, and subpages. Snapshot bounds content, not ranking: Exa still discovers URLs using current retrieval signals. Snapshot is a research preview with limited access and lookback.",
911
918
  "Use includeText/excludeText for short required or forbidden phrases in page text.",
912
919
  "Request contents.text, contents.highlights, or contents.summary only when snippets are insufficient and richer source context is needed directly in search results."
913
920
  ],
914
921
  retrySafe: true
915
922
  },
916
923
  contents: {
924
+ options: {
925
+ type: "object",
926
+ properties: {
927
+ snapshotAsOf,
928
+ text: {
929
+ type: "boolean",
930
+ description: "Include text content."
931
+ }
932
+ },
933
+ description: "Exa contents options."
934
+ },
935
+ promptGuidelines: [
936
+ "For historical page versions, set options.snapshotAsOf to an ISO datetime (not nested inside contents). Exa returns the newest stored version at or before that instant and omits pages without an eligible version. Snapshot is a research preview with limited access and lookback."
937
+ ],
917
938
  retrySafe: true
918
939
  },
919
940
  answer: {
@@ -1970,7 +1991,7 @@ var serpbaseProvider = defineProvider({
1970
1991
  retrySafe: true
1971
1992
  }
1972
1993
  },
1973
- load: async () => (await import("./adapter-RL7JNB22.js")).adapter
1994
+ load: async () => (await import("./adapter-BQUN3ME2.js")).adapter
1974
1995
  });
1975
1996
 
1976
1997
  // src/providers/serper/definition.ts
@@ -6,14 +6,14 @@ import {
6
6
  } from "./chunk-WCTOYZVB.js";
7
7
  import {
8
8
  configurationSchema
9
- } from "./chunk-MNN3BEPJ.js";
9
+ } from "./chunk-5TNPVBAZ.js";
10
10
  import {
11
11
  effectiveOptions,
12
12
  optionSchema,
13
13
  providers,
14
14
  selectProvider,
15
15
  validateConfiguredOptions
16
- } from "./chunk-VYMIR35R.js";
16
+ } from "./chunk-KH54YH7D.js";
17
17
  import {
18
18
  cancelProcessGroup,
19
19
  killProcessGroup
package/dist/cli.js CHANGED
@@ -11,14 +11,14 @@ import {
11
11
  redactConfig,
12
12
  resolveConfigPath,
13
13
  setCapabilityDefault
14
- } from "./chunk-J3QO44LO.js";
14
+ } from "./chunk-QXTTUDLI.js";
15
15
  import "./chunk-WCTOYZVB.js";
16
16
  import {
17
17
  PACKAGE_VERSION
18
- } from "./chunk-MNN3BEPJ.js";
18
+ } from "./chunk-5TNPVBAZ.js";
19
19
  import {
20
20
  validateConfiguredOptions
21
- } from "./chunk-VYMIR35R.js";
21
+ } from "./chunk-KH54YH7D.js";
22
22
  import "./chunk-ZVTVERW5.js";
23
23
  import {
24
24
  CAPABILITIES,
@@ -1388,6 +1388,11 @@
1388
1388
  ],
1389
1389
  "description": "Include AI-generated summary."
1390
1390
  },
1391
+ "snapshotAsOf": {
1392
+ "type": "string",
1393
+ "minLength": 1,
1394
+ "description": "ISO datetime cutoff, such as '2026-07-01T00:00:00Z'. Return the newest stored page version at or before this instant; pages without an eligible version are omitted. Do not combine with maxAgeHours, livecrawlTimeout, or subpages."
1395
+ },
1391
1396
  "livecrawlTimeout": {
1392
1397
  "type": "integer",
1393
1398
  "minimum": 1,
@@ -1441,7 +1446,7 @@
1441
1446
  }
1442
1447
  },
1443
1448
  "additionalProperties": false,
1444
- "description": "Content extraction and freshness controls. Put text, highlights, summary, maxAgeHours, and livecrawlTimeout here, not directly in options."
1449
+ "description": "Content extraction, freshness, and historical snapshot controls. Put text, highlights, summary, snapshotAsOf, maxAgeHours, and livecrawlTimeout here, not directly in options."
1445
1450
  }
1446
1451
  },
1447
1452
  "description": "Exa search options.",
@@ -1449,6 +1454,18 @@
1449
1454
  },
1450
1455
  "contents": {
1451
1456
  "type": "object",
1457
+ "properties": {
1458
+ "snapshotAsOf": {
1459
+ "type": "string",
1460
+ "minLength": 1,
1461
+ "description": "ISO datetime cutoff, such as '2026-07-01T00:00:00Z'. Return the newest stored page version at or before this instant; pages without an eligible version are omitted. Do not combine with maxAgeHours, livecrawlTimeout, or subpages."
1462
+ },
1463
+ "text": {
1464
+ "type": "boolean",
1465
+ "description": "Include text content."
1466
+ }
1467
+ },
1468
+ "description": "Exa contents options.",
1452
1469
  "additionalProperties": false
1453
1470
  },
1454
1471
  "answer": {
@@ -0,0 +1,114 @@
1
+ import { Type, type Static } from "typebox";
2
+ import { type AnswerDocument, type ContentsDocument, type ResearchDocument, type SearchDocument } from "./domain.js";
3
+ /**
4
+ * JSON Schema of each capability's result document. This is the document the
5
+ * CLI prints with `--format json` and the library returns.
6
+ */
7
+ export declare const DOCUMENT_SCHEMAS: {
8
+ readonly search: Type.TObject<{
9
+ schemaVersion: Type.TLiteral<1>;
10
+ capability: Type.TLiteral<"search">;
11
+ provider: Type.TEnum<["brave", "cloudflare", "custom", "exa", "firecrawl", "gemini", "linkup", "ollama", "openai", "parallel", "perplexity", "serpbase", "serper", "tavily", "valyu", "youcom"]>;
12
+ status: Type.TEnum<["ok", "partial"]>;
13
+ results: Type.TArray<Type.TUnion<[Type.TObject<{
14
+ input: Type.TString;
15
+ ok: Type.TLiteral<true>;
16
+ value: Type.TObject<{
17
+ results: Type.TArray<Type.TObject<{
18
+ title: Type.TString;
19
+ url: Type.TString;
20
+ snippet: Type.TString;
21
+ score: Type.TOptional<Type.TNumber>;
22
+ metadata: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
23
+ }>>;
24
+ }>;
25
+ }>, Type.TObject<{
26
+ input: Type.TString;
27
+ ok: Type.TLiteral<false>;
28
+ error: Type.TObject<{
29
+ code: Type.TEnum<["INVALID_CONFIG", "INVALID_INPUT", "PROVIDER_UNAVAILABLE", "PROVIDER_FAILURE", "PARTIAL_BATCH", "TIMEOUT", "CANCELLED"]>;
30
+ message: Type.TString;
31
+ retryable: Type.TOptional<Type.TBoolean>;
32
+ }>;
33
+ }>]>>;
34
+ }>;
35
+ readonly contents: Type.TObject<{
36
+ schemaVersion: Type.TLiteral<1>;
37
+ capability: Type.TLiteral<"contents">;
38
+ provider: Type.TEnum<["brave", "cloudflare", "custom", "exa", "firecrawl", "gemini", "linkup", "ollama", "openai", "parallel", "perplexity", "serpbase", "serper", "tavily", "valyu", "youcom"]>;
39
+ status: Type.TEnum<["ok", "partial"]>;
40
+ results: Type.TArray<Type.TUnion<[Type.TObject<{
41
+ input: Type.TString;
42
+ ok: Type.TLiteral<true>;
43
+ value: Type.TObject<{
44
+ url: Type.TString;
45
+ content: Type.TOptional<Type.TString>;
46
+ summary: Type.TOptional<Type.TUnknown>;
47
+ metadata: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
48
+ }>;
49
+ }>, Type.TObject<{
50
+ input: Type.TString;
51
+ ok: Type.TLiteral<false>;
52
+ error: Type.TObject<{
53
+ code: Type.TEnum<["INVALID_CONFIG", "INVALID_INPUT", "PROVIDER_UNAVAILABLE", "PROVIDER_FAILURE", "PARTIAL_BATCH", "TIMEOUT", "CANCELLED"]>;
54
+ message: Type.TString;
55
+ retryable: Type.TOptional<Type.TBoolean>;
56
+ }>;
57
+ }>]>>;
58
+ }>;
59
+ readonly answer: Type.TObject<{
60
+ schemaVersion: Type.TLiteral<1>;
61
+ capability: Type.TLiteral<"answer">;
62
+ provider: Type.TEnum<["brave", "cloudflare", "custom", "exa", "firecrawl", "gemini", "linkup", "ollama", "openai", "parallel", "perplexity", "serpbase", "serper", "tavily", "valyu", "youcom"]>;
63
+ status: Type.TEnum<["ok", "partial"]>;
64
+ results: Type.TArray<Type.TUnion<[Type.TObject<{
65
+ input: Type.TString;
66
+ ok: Type.TLiteral<true>;
67
+ value: Type.TObject<{
68
+ text: Type.TString;
69
+ itemCount: Type.TOptional<Type.TInteger>;
70
+ metadata: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
71
+ }>;
72
+ }>, Type.TObject<{
73
+ input: Type.TString;
74
+ ok: Type.TLiteral<false>;
75
+ error: Type.TObject<{
76
+ code: Type.TEnum<["INVALID_CONFIG", "INVALID_INPUT", "PROVIDER_UNAVAILABLE", "PROVIDER_FAILURE", "PARTIAL_BATCH", "TIMEOUT", "CANCELLED"]>;
77
+ message: Type.TString;
78
+ retryable: Type.TOptional<Type.TBoolean>;
79
+ }>;
80
+ }>]>>;
81
+ }>;
82
+ readonly research: Type.TObject<{
83
+ schemaVersion: Type.TLiteral<1>;
84
+ capability: Type.TLiteral<"research">;
85
+ provider: Type.TEnum<["brave", "cloudflare", "custom", "exa", "firecrawl", "gemini", "linkup", "ollama", "openai", "parallel", "perplexity", "serpbase", "serper", "tavily", "valyu", "youcom"]>;
86
+ status: Type.TEnum<["ok", "partial"]>;
87
+ results: Type.TArray<Type.TUnion<[Type.TObject<{
88
+ input: Type.TString;
89
+ ok: Type.TLiteral<true>;
90
+ value: Type.TObject<{
91
+ text: Type.TString;
92
+ itemCount: Type.TOptional<Type.TInteger>;
93
+ metadata: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
94
+ }>;
95
+ }>, Type.TObject<{
96
+ input: Type.TString;
97
+ ok: Type.TLiteral<false>;
98
+ error: Type.TObject<{
99
+ code: Type.TEnum<["INVALID_CONFIG", "INVALID_INPUT", "PROVIDER_UNAVAILABLE", "PROVIDER_FAILURE", "PARTIAL_BATCH", "TIMEOUT", "CANCELLED"]>;
100
+ message: Type.TString;
101
+ retryable: Type.TOptional<Type.TBoolean>;
102
+ }>;
103
+ }>]>>;
104
+ }>;
105
+ };
106
+ type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
107
+ type Assert<T extends true> = T;
108
+ export type DocumentSchemaMatchesDomain = [
109
+ Assert<Same<Static<typeof DOCUMENT_SCHEMAS.search>, SearchDocument>>,
110
+ Assert<Same<Static<typeof DOCUMENT_SCHEMAS.contents>, ContentsDocument>>,
111
+ Assert<Same<Static<typeof DOCUMENT_SCHEMAS.answer>, AnswerDocument>>,
112
+ Assert<Same<Static<typeof DOCUMENT_SCHEMAS.research>, ResearchDocument>>
113
+ ];
114
+ export {};
package/dist/index.js CHANGED
@@ -6,14 +6,14 @@ import {
6
6
  resolveConfigPath,
7
7
  setCapabilityDefault,
8
8
  validateConfig
9
- } from "./chunk-J3QO44LO.js";
9
+ } from "./chunk-QXTTUDLI.js";
10
10
  import "./chunk-WCTOYZVB.js";
11
11
  import {
12
12
  CONFIG_SCHEMA_URL
13
- } from "./chunk-MNN3BEPJ.js";
13
+ } from "./chunk-5TNPVBAZ.js";
14
14
  import {
15
15
  validateConfiguredOptions
16
- } from "./chunk-VYMIR35R.js";
16
+ } from "./chunk-KH54YH7D.js";
17
17
  import "./chunk-ZVTVERW5.js";
18
18
  import {
19
19
  CAPABILITIES,
package/dist/pi.js CHANGED
@@ -4,13 +4,15 @@ import {
4
4
  import {
5
5
  createWebfox,
6
6
  resolveConfigPath
7
- } from "./chunk-J3QO44LO.js";
7
+ } from "./chunk-QXTTUDLI.js";
8
8
  import "./chunk-WCTOYZVB.js";
9
- import "./chunk-MNN3BEPJ.js";
10
- import "./chunk-VYMIR35R.js";
9
+ import "./chunk-5TNPVBAZ.js";
10
+ import "./chunk-KH54YH7D.js";
11
11
  import "./chunk-ZVTVERW5.js";
12
12
  import {
13
- CAPABILITIES
13
+ CAPABILITIES,
14
+ PROVIDER_IDS,
15
+ WEBFOX_ERROR_CODES
14
16
  } from "./chunk-GPACLYNT.js";
15
17
  import {
16
18
  WebfoxError
@@ -24,7 +26,7 @@ import {
24
26
  truncateHead,
25
27
  withFileMutationQueue
26
28
  } from "@earendil-works/pi-coding-agent";
27
- import { Type } from "typebox";
29
+ import { Type as Type2 } from "typebox";
28
30
 
29
31
  // src/pi-render.ts
30
32
  import { stripVTControlCharacters as stripVTControlCharacters3 } from "node:util";
@@ -186,11 +188,11 @@ function link(url, theme) {
186
188
  return label;
187
189
  }
188
190
  function renderSearchResult(details, fallback, theme) {
189
- const document = isRecord2(details) && searchDocument(details.result) ? details.result : void 0;
191
+ const document2 = isRecord2(details) && searchDocument(details.result) ? details.result : void 0;
190
192
  return {
191
193
  render(width) {
192
194
  if (width <= 0) return [];
193
- if (!document) {
195
+ if (!document2) {
194
196
  const text = plainText(fallback).split("\n").map((line) => {
195
197
  const urlLine = /^(\s*)(https?:\/\/\S+)(\s*)$/.exec(line);
196
198
  return urlLine ? urlLine[1] + link(urlLine[2], theme) + urlLine[3] : theme.fg("toolOutput", line);
@@ -209,9 +211,9 @@ function renderSearchResult(details, fallback, theme) {
209
211
  )
210
212
  );
211
213
  };
212
- document.results.forEach((entry, index) => {
214
+ document2.results.forEach((entry, index) => {
213
215
  if (index) lines.push("");
214
- if (document.results.length > 1) {
216
+ if (document2.results.length > 1) {
215
217
  append(
216
218
  theme.fg(
217
219
  "mdHeading",
@@ -288,11 +290,7 @@ var WebCall = class {
288
290
  const { args, theme, capability } = this;
289
291
  const title = theme.fg("toolTitle", theme.bold(`web ${capability}`));
290
292
  let text = title;
291
- const parameters = callParameters(
292
- capability,
293
- args,
294
- (key2) => theme.bold(key2)
295
- );
293
+ const parameters = callParameters(capability, args);
296
294
  if (parameters) text += theme.fg("dim", ` ${parameters}`);
297
295
  if (this.expanded)
298
296
  return visibleWidth2(text) <= width ? [text] : new Text2(text, 0, 0).render(width).map((line) => truncateLine(line, width));
@@ -308,8 +306,8 @@ var WebCall = class {
308
306
  };
309
307
  function renderWebResult(result, options, theme, isError, capability) {
310
308
  const text = result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join("\n");
311
- const metadata = result.details;
312
- const isSearch = (capability ?? metadata?.capability ?? metadata?.result?.capability) === "search";
309
+ const metadata2 = result.details;
310
+ const isSearch = (capability ?? metadata2?.capability ?? metadata2?.result?.capability) === "search";
313
311
  const body = () => isSearch ? renderSearchResult(result.details, text, theme) : new Markdown(text, 0, 0, getMarkdownTheme());
314
312
  const rows = statusRows(result.details);
315
313
  if (rows?.length) {
@@ -439,6 +437,85 @@ function configurationDiagnostic(error) {
439
437
  ].join("\n");
440
438
  }
441
439
 
440
+ // src/document-schema.ts
441
+ import { Type } from "typebox";
442
+ var closed = { additionalProperties: false };
443
+ var metadata = Type.Record(Type.String(), Type.Unknown(), {
444
+ description: "Provider-specific fields."
445
+ });
446
+ var searchResult = Type.Object(
447
+ {
448
+ title: Type.String(),
449
+ url: Type.String(),
450
+ snippet: Type.String(),
451
+ score: Type.Optional(Type.Number()),
452
+ metadata: Type.Optional(metadata)
453
+ },
454
+ closed
455
+ );
456
+ var contentsAnswer = Type.Object(
457
+ {
458
+ url: Type.String({ description: "Final URL after redirects." }),
459
+ content: Type.Optional(Type.String()),
460
+ summary: Type.Optional(Type.Unknown()),
461
+ metadata: Type.Optional(metadata)
462
+ },
463
+ closed
464
+ );
465
+ var textAnswer = Type.Object(
466
+ {
467
+ text: Type.String(),
468
+ itemCount: Type.Optional(Type.Integer()),
469
+ metadata: Type.Optional(metadata)
470
+ },
471
+ closed
472
+ );
473
+ var serializedError = Type.Object(
474
+ {
475
+ code: Type.Enum(WEBFOX_ERROR_CODES),
476
+ message: Type.String(),
477
+ retryable: Type.Optional(Type.Boolean())
478
+ },
479
+ closed
480
+ );
481
+ function inputResult(value) {
482
+ const input = Type.String({
483
+ description: "The query, URL, or brief this entry answers."
484
+ });
485
+ return Type.Union([
486
+ Type.Object({ input, ok: Type.Literal(true), value }, closed),
487
+ Type.Object(
488
+ { input, ok: Type.Literal(false), error: serializedError },
489
+ closed
490
+ )
491
+ ]);
492
+ }
493
+ function document(capability, value) {
494
+ return Type.Object(
495
+ {
496
+ schemaVersion: Type.Literal(1),
497
+ capability: Type.Literal(capability),
498
+ provider: Type.Enum(PROVIDER_IDS),
499
+ status: Type.Enum(["ok", "partial"], {
500
+ description: "`partial` when at least one input failed."
501
+ }),
502
+ results: Type.Array(inputResult(value), {
503
+ description: "One entry per input, in input order."
504
+ })
505
+ },
506
+ closed
507
+ );
508
+ }
509
+ var DOCUMENT_SCHEMAS = {
510
+ search: document(
511
+ "search",
512
+ Type.Object({ results: Type.Array(searchResult) }, closed)
513
+ ),
514
+ contents: document("contents", contentsAnswer),
515
+ answer: document("answer", textAnswer),
516
+ research: document("research", textAnswer)
517
+ };
518
+
442
519
  // src/pi.ts
443
520
  function webExtension(pi) {
444
521
  const clients = /* @__PURE__ */ new Map();
@@ -479,18 +556,18 @@ function webExtension(pi) {
479
556
  for (const inspection of selected) {
480
557
  const capability = inspection.capability;
481
558
  const provider = inspection.provider;
482
- const fields = capability === "contents" ? { urls: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }) } : capability === "research" ? { input: Type.String({ minLength: 1 }) } : {
483
- queries: Type.Array(Type.String({ minLength: 1 }), {
559
+ const fields = capability === "contents" ? { urls: Type2.Array(Type2.String({ minLength: 1 }), { minItems: 1 }) } : capability === "research" ? { input: Type2.String({ minLength: 1 }) } : {
560
+ queries: Type2.Array(Type2.String({ minLength: 1 }), {
484
561
  minItems: 1,
485
562
  maxItems: 10
486
563
  }),
487
- ...capability === "search" ? { maxResults: Type.Optional(Type.Integer({ minimum: 1 })) } : {}
564
+ ...capability === "search" ? { maxResults: Type2.Optional(Type2.Integer({ minimum: 1 })) } : {}
488
565
  };
489
- const parameters = Type.Object(
566
+ const parameters = Type2.Object(
490
567
  {
491
568
  ...fields,
492
569
  ...inspection.optionSchema ? {
493
- options: Type.Optional(
570
+ options: Type2.Optional(
494
571
  inspection.optionSchema
495
572
  )
496
573
  } : {}
@@ -507,6 +584,12 @@ function webExtension(pi) {
507
584
  "Output is truncated to 2000 lines or 50 KiB; full results are saved to a file when truncated."
508
585
  ].join("\n"),
509
586
  parameters,
587
+ // Let permission extensions and tool loadouts treat web access as a
588
+ // read-only call that reaches the open web.
589
+ annotations: { readOnlyHint: true, openWorldHint: true },
590
+ // Programmatic callers such as codemode scripts receive the result
591
+ // document itself instead of parsing the rendered text.
592
+ outputSchema: DOCUMENT_SCHEMAS[capability],
510
593
  prepareArguments(args) {
511
594
  return prepareToolArguments(parameters, args);
512
595
  },
@@ -554,6 +637,7 @@ function webExtension(pi) {
554
637
  queries: params.queries,
555
638
  maxResults: params.maxResults
556
639
  }) : capability === "answer" ? await client.answer({ ...request, queries: params.queries }) : capability === "contents" ? await client.contents({ ...request, urls: params.urls }) : await client.research({ ...request, input: params.input });
640
+ const json = JSON.stringify(result);
557
641
  const text = renderTextDocument(result);
558
642
  const truncated = truncateHead(text);
559
643
  let body = truncated.content;
@@ -565,7 +649,7 @@ function webExtension(pi) {
565
649
  );
566
650
  await withFileMutationQueue(
567
651
  fullOutputPath,
568
- () => writeFile(fullOutputPath, JSON.stringify(result), { mode: 384 })
652
+ () => writeFile(fullOutputPath, json, { mode: 384 })
569
653
  );
570
654
  body += `
571
655
 
@@ -573,8 +657,11 @@ Full results: ${fullOutputPath}`;
573
657
  }
574
658
  return {
575
659
  content: [{ type: "text", text: body }],
660
+ // Pi keeps this out of the model's context and the session, so the
661
+ // text truncation above does not apply. Scripts get every result.
662
+ structuredContent: JSON.parse(json),
663
+ isError: result.status === "partial",
576
664
  details: {
577
- webProviderResult: true,
578
665
  status: result.status,
579
666
  webInputStatus: true,
580
667
  capability,
@@ -588,10 +675,6 @@ Full results: ${fullOutputPath}`;
588
675
  }
589
676
  });
590
677
  }
591
- pi.on("tool_result", (event) => {
592
- if (event.details && typeof event.details === "object" && "webProviderResult" in event.details && event.details.webProviderResult === true && "status" in event.details && event.details.status === "partial")
593
- return { isError: true };
594
- });
595
678
  }
596
679
  var descriptions = {
597
680
  search: "Search up to ten queries and return titles, URLs, and snippets in input order.",
package/docs/provider.md CHANGED
@@ -74,6 +74,26 @@ web search "Node.js release notes" --provider exa
74
74
  web research "Compare databases for an analytics service" --provider exa --timeout 20m
75
75
  ```
76
76
 
77
+ Use [Exa Snapshot](https://exa.ai/docs/search/snapshot) to retrieve the newest
78
+ stored page version at or before an ISO datetime. For search, set
79
+ `options.contents.snapshotAsOf`; for page extraction, set `options.snapshotAsOf`.
80
+ Both options are also exposed to the model when Exa is selected in pi.
81
+
82
+ ```sh
83
+ web search "Python release notes" --provider exa \
84
+ --options-json '{"contents":{"snapshotAsOf":"2026-07-01T00:00:00Z"}}'
85
+ web contents https://docs.python.org/3/whatsnew/ --provider exa \
86
+ --options-json '{"snapshotAsOf":"2026-07-01T00:00:00Z","text":true}'
87
+ ```
88
+
89
+ Snapshot search supports `auto`, `fast`, and `instant`, without `category`.
90
+ Do not combine snapshots with `maxAgeHours`, `livecrawl`, `livecrawlTimeout`, or
91
+ `subpages`, including saved defaults. Pages without an eligible snapshot are
92
+ omitted by Exa; page extraction reports those URLs as failures. The cutoff bounds
93
+ page content, not ranking: Exa still discovers candidate URLs using current
94
+ retrieval signals. Snapshot is a research preview with limited access and
95
+ lookback; consult Exa's documentation for current limits.
96
+
77
97
  ## Firecrawl
78
98
 
79
99
  Supports search, page extraction, and page-scoped answers. Set `FIRECRAWL_API_KEY`
package/docs/reference.md CHANGED
@@ -179,3 +179,10 @@ extension forwards cancellation and progress, marks partial results as errors,
179
179
  and truncates tool output at 2,000 lines or 50 KiB, with full results saved to a
180
180
  temporary file. It needs no globally installed `web` command and starts no
181
181
  background research jobs.
182
+
183
+ Each tool declares the [JSON result document](./cli-experience.md#json-results)
184
+ as its output schema and returns that document as structured content. Scripts
185
+ that call the tools through Pi's codemode receive it as an object instead of
186
+ parsing text, including the completed inputs of a partial result. Structured
187
+ content is never truncated, and Pi neither sends it to the model nor stores it
188
+ in the session.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webfox",
3
- "version": "4.4.2",
3
+ "version": "4.6.0",
4
4
  "description": "Search the web, extract pages, get grounded answers, and run deep research with the web CLI, a TypeScript library, and a pi extension. Bring your own providers and API keys.",
5
5
  "type": "module",
6
6
  "files": [
@@ -98,15 +98,14 @@
98
98
  "openai": "^7.10.0",
99
99
  "parallel-web": "^1.3.3",
100
100
  "picocolors": "^1.1.1",
101
- "typebox": "^1.3.6",
102
101
  "valyu-js": "^2.10.1",
103
102
  "yaml": "^2.9.0"
104
103
  },
105
104
  "devDependencies": {
106
105
  "@biomejs/biome": "^2.5.4",
107
- "@earendil-works/pi-ai": "^0.85.1",
108
- "@earendil-works/pi-coding-agent": "^0.85.1",
109
- "@earendil-works/pi-tui": "^0.85.1",
106
+ "@earendil-works/pi-ai": "^0.99.0",
107
+ "@earendil-works/pi-coding-agent": "^0.99.0",
108
+ "@earendil-works/pi-tui": "^0.99.0",
110
109
  "@types/node": "^26.1.1",
111
110
  "esbuild": "^0.28.1",
112
111
  "typescript": "^7.0.2",
@@ -114,7 +113,8 @@
114
113
  },
115
114
  "peerDependencies": {
116
115
  "@earendil-works/pi-coding-agent": "*",
117
- "@earendil-works/pi-tui": "*"
116
+ "@earendil-works/pi-tui": "*",
117
+ "typebox": "*"
118
118
  },
119
119
  "peerDependenciesMeta": {
120
120
  "@earendil-works/pi-coding-agent": {