webfox 4.5.0 → 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.
@@ -9,7 +9,7 @@ import {
9
9
  // package.json
10
10
  var package_default = {
11
11
  name: "webfox",
12
- version: "4.5.0",
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": {
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-WCTOYZVB.js";
7
7
  import {
8
8
  configurationSchema
9
- } from "./chunk-VI3473TU.js";
9
+ } from "./chunk-5TNPVBAZ.js";
10
10
  import {
11
11
  effectiveOptions,
12
12
  optionSchema,
package/dist/cli.js CHANGED
@@ -11,11 +11,11 @@ import {
11
11
  redactConfig,
12
12
  resolveConfigPath,
13
13
  setCapabilityDefault
14
- } from "./chunk-D4HCJRBE.js";
14
+ } from "./chunk-QXTTUDLI.js";
15
15
  import "./chunk-WCTOYZVB.js";
16
16
  import {
17
17
  PACKAGE_VERSION
18
- } from "./chunk-VI3473TU.js";
18
+ } from "./chunk-5TNPVBAZ.js";
19
19
  import {
20
20
  validateConfiguredOptions
21
21
  } from "./chunk-KH54YH7D.js";
@@ -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,11 +6,11 @@ import {
6
6
  resolveConfigPath,
7
7
  setCapabilityDefault,
8
8
  validateConfig
9
- } from "./chunk-D4HCJRBE.js";
9
+ } from "./chunk-QXTTUDLI.js";
10
10
  import "./chunk-WCTOYZVB.js";
11
11
  import {
12
12
  CONFIG_SCHEMA_URL
13
- } from "./chunk-VI3473TU.js";
13
+ } from "./chunk-5TNPVBAZ.js";
14
14
  import {
15
15
  validateConfiguredOptions
16
16
  } from "./chunk-KH54YH7D.js";
package/dist/pi.js CHANGED
@@ -4,13 +4,15 @@ import {
4
4
  import {
5
5
  createWebfox,
6
6
  resolveConfigPath
7
- } from "./chunk-D4HCJRBE.js";
7
+ } from "./chunk-QXTTUDLI.js";
8
8
  import "./chunk-WCTOYZVB.js";
9
- import "./chunk-VI3473TU.js";
9
+ import "./chunk-5TNPVBAZ.js";
10
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",
@@ -304,8 +306,8 @@ var WebCall = class {
304
306
  };
305
307
  function renderWebResult(result, options, theme, isError, capability) {
306
308
  const text = result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join("\n");
307
- const metadata = result.details;
308
- const isSearch = (capability ?? metadata?.capability ?? metadata?.result?.capability) === "search";
309
+ const metadata2 = result.details;
310
+ const isSearch = (capability ?? metadata2?.capability ?? metadata2?.result?.capability) === "search";
309
311
  const body = () => isSearch ? renderSearchResult(result.details, text, theme) : new Markdown(text, 0, 0, getMarkdownTheme());
310
312
  const rows = statusRows(result.details);
311
313
  if (rows?.length) {
@@ -435,6 +437,85 @@ function configurationDiagnostic(error) {
435
437
  ].join("\n");
436
438
  }
437
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
+
438
519
  // src/pi.ts
439
520
  function webExtension(pi) {
440
521
  const clients = /* @__PURE__ */ new Map();
@@ -475,18 +556,18 @@ function webExtension(pi) {
475
556
  for (const inspection of selected) {
476
557
  const capability = inspection.capability;
477
558
  const provider = inspection.provider;
478
- const fields = capability === "contents" ? { urls: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }) } : capability === "research" ? { input: Type.String({ minLength: 1 }) } : {
479
- 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 }), {
480
561
  minItems: 1,
481
562
  maxItems: 10
482
563
  }),
483
- ...capability === "search" ? { maxResults: Type.Optional(Type.Integer({ minimum: 1 })) } : {}
564
+ ...capability === "search" ? { maxResults: Type2.Optional(Type2.Integer({ minimum: 1 })) } : {}
484
565
  };
485
- const parameters = Type.Object(
566
+ const parameters = Type2.Object(
486
567
  {
487
568
  ...fields,
488
569
  ...inspection.optionSchema ? {
489
- options: Type.Optional(
570
+ options: Type2.Optional(
490
571
  inspection.optionSchema
491
572
  )
492
573
  } : {}
@@ -503,6 +584,12 @@ function webExtension(pi) {
503
584
  "Output is truncated to 2000 lines or 50 KiB; full results are saved to a file when truncated."
504
585
  ].join("\n"),
505
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],
506
593
  prepareArguments(args) {
507
594
  return prepareToolArguments(parameters, args);
508
595
  },
@@ -550,6 +637,7 @@ function webExtension(pi) {
550
637
  queries: params.queries,
551
638
  maxResults: params.maxResults
552
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);
553
641
  const text = renderTextDocument(result);
554
642
  const truncated = truncateHead(text);
555
643
  let body = truncated.content;
@@ -561,7 +649,7 @@ function webExtension(pi) {
561
649
  );
562
650
  await withFileMutationQueue(
563
651
  fullOutputPath,
564
- () => writeFile(fullOutputPath, JSON.stringify(result), { mode: 384 })
652
+ () => writeFile(fullOutputPath, json, { mode: 384 })
565
653
  );
566
654
  body += `
567
655
 
@@ -569,8 +657,11 @@ Full results: ${fullOutputPath}`;
569
657
  }
570
658
  return {
571
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",
572
664
  details: {
573
- webProviderResult: true,
574
665
  status: result.status,
575
666
  webInputStatus: true,
576
667
  capability,
@@ -584,10 +675,6 @@ Full results: ${fullOutputPath}`;
584
675
  }
585
676
  });
586
677
  }
587
- pi.on("tool_result", (event) => {
588
- if (event.details && typeof event.details === "object" && "webProviderResult" in event.details && event.details.webProviderResult === true && "status" in event.details && event.details.status === "partial")
589
- return { isError: true };
590
- });
591
678
  }
592
679
  var descriptions = {
593
680
  search: "Search up to ten queries and return titles, URLs, and snippets in input order.",
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.5.0",
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": {