webfox 4.6.0 → 4.8.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/README.md CHANGED
@@ -244,6 +244,95 @@ Ask Pi: **Search the web for the latest Node.js release notes.**
244
244
  Add `contents`, `answer`, or `research` defaults for more tools.
245
245
  Restart Pi after changing defaults.
246
246
 
247
+ ### Compose web calls with codemode
248
+
249
+ With Pi 0.99 or later, you can use its built-in `codemode` tool to chain web
250
+ calls, run independent calls in parallel, and filter results before they reach
251
+ the model. In `~/.pi/agent/settings.json` or your project's `.pi/settings.json`,
252
+ add:
253
+
254
+ ```json
255
+ {
256
+ "defaultTools": ["+codemode"]
257
+ }
258
+ ```
259
+
260
+ This keeps your existing tools and adds codemode. Webfox tools remain directly
261
+ callable; you don't need an MCP server or another extension. In codemode, calls
262
+ such as `tools.web_search(...)` return structured result objects, not text.
263
+ Scripts receive complete results even when the direct tool output is truncated.
264
+ Only the output you emit with `text(...)` or `return` reaches the model.
265
+
266
+ Ask Pi: **Use codemode to search for Node.js and Bun cancellation, fetch up to
267
+ five unique pages, and return a short excerpt from each.** Pi can run a script
268
+ like this, provided you have configured both `search` and `contents` defaults:
269
+
270
+ ```js
271
+ const search = await tools.web_search({
272
+ queries: ["Node.js cancellation", "Bun cancellation"],
273
+ maxResults: 3,
274
+ });
275
+
276
+ // Keep failed inputs visible while using the successful results.
277
+ for (const entry of search.results) {
278
+ if (!entry.ok) text({ input: entry.input, error: entry.error });
279
+ }
280
+
281
+ const urls = [...new Set(
282
+ search.results.flatMap(entry =>
283
+ entry.ok ? entry.value.results.map(hit => hit.url) : []
284
+ )
285
+ )].slice(0, 5);
286
+
287
+ if (!urls.length) exit();
288
+
289
+ const pages = await tools.web_contents({ urls });
290
+ text(pages.results.map(entry =>
291
+ entry.ok
292
+ ? { url: entry.value.url, excerpt: entry.value.content?.slice(0, 3000) }
293
+ : { url: entry.input, error: entry.error }
294
+ ));
295
+ ```
296
+
297
+ For independent tasks, await every call with `Promise.allSettled`. This example
298
+ requires an `answer` default and keeps one rejected call from discarding the
299
+ other call's result:
300
+
301
+ ```js
302
+ const answers = await Promise.allSettled([
303
+ tools.web_answer({ queries: ["What is an AbortSignal?"] }),
304
+ tools.web_answer({ queries: ["How does Bun handle cancellation?"] }),
305
+ ]);
306
+
307
+ for (const answer of answers) {
308
+ if (answer.status === "fulfilled") text(answer.value);
309
+ else text({ error: String(answer.reason) });
310
+ }
311
+ ```
312
+
313
+ A Webfox document with `status: "partial"` still resolves: check each result's
314
+ `ok` field before accessing `value`. Invalid arguments and blocked tool calls
315
+ reject instead. Nested calls still pass through Pi's validation and permission
316
+ hooks.
317
+
318
+ To return less data, select the fields you need rather than emit a whole
319
+ document. For example, this returns only titles and source URLs:
320
+
321
+ ```js
322
+ const search = await tools.web_search({ queries: ["Node.js release notes"] });
323
+ text(search.results.flatMap(entry =>
324
+ entry.ok
325
+ ? entry.value.results.map(({ title, url }) => ({ title, url }))
326
+ : [{ input: entry.input, error: entry.error }]
327
+ ));
328
+ ```
329
+
330
+ Webfox tools belong to the `web` namespace, which you can search from scripts
331
+ with `await searchTools("search", { namespace: "web" })`. To route all callable
332
+ tools through scripts, set `"codemode": { "mode": "only" }` in Pi's settings
333
+ alongside `defaultTools`. This setting affects all callable tools, not only
334
+ Webfox.
335
+
247
336
  ## 📚 Use with TypeScript
248
337
 
249
338
  ```ts
@@ -266,6 +355,25 @@ The client also provides `contents`, `answer`, and `research`. See the
266
355
  [library reference](./docs/reference.md#typescript-library) for request controls,
267
356
  configuration, progress, and errors.
268
357
 
358
+ ## 🧬 Use with Pi Durable
359
+
360
+ A custom Pi Durable host can install the web tools directly:
361
+
362
+ ```ts
363
+ import { createWebfoxExtension } from "webfox/durable";
364
+
365
+ registry.install(createWebfoxExtension({ cwd: process.cwd() }));
366
+ ```
367
+
368
+ The adapter uses the shared Webfox configuration and provider-specific schemas.
369
+ Each call follows its conversation's execution environment, streams progress,
370
+ and preserves partial failures. Large text results are saved to private files.
371
+ Interrupted requests are not replayed automatically, since providers may charge
372
+ for them. Cancelling a request may not cancel a provider's already-running job.
373
+
374
+ The adapter requires Pi Durable 1.0. It does not install a CLI, register standard
375
+ Pi renderers, or provide codemode's structured-result interface.
376
+
269
377
  ## 🩺 Troubleshooting
270
378
 
271
379
  - **No provider selected:** Pass `--provider <id>` or save a default with
@@ -2,11 +2,11 @@ import {
2
2
  SERPBASE_MODES,
3
3
  serpbaseProvider,
4
4
  validateOptions
5
- } from "./chunk-KH54YH7D.js";
6
- import "./chunk-GPACLYNT.js";
5
+ } from "./chunk-QERSNL3J.js";
7
6
  import {
8
7
  trimSnippet
9
8
  } from "./chunk-X6ZBIHZC.js";
9
+ import "./chunk-GPACLYNT.js";
10
10
  import {
11
11
  WebfoxError,
12
12
  httpError
@@ -130,9 +130,21 @@ function createClient(config) {
130
130
  return new PerplexityClient({
131
131
  maxRetries: 0,
132
132
  apiKey,
133
- baseURL: config.baseUrl
133
+ baseURL: config.baseUrl,
134
+ ...shouldSendIntegrationHeader(config.baseUrl) ? { defaultHeaders: { "X-Pplx-Integration": "webfox" } } : {}
134
135
  });
135
136
  }
137
+ function shouldSendIntegrationHeader(baseUrl) {
138
+ const customHeaders = process.env.PERPLEXITY_CUSTOM_HEADERS ?? "";
139
+ if (/^\s*x-pplx-integration\s*:/im.test(customHeaders)) return false;
140
+ const url = baseUrl ?? process.env.PERPLEXITY_BASE_URL?.trim();
141
+ if (!url) return true;
142
+ try {
143
+ return new URL(url).hostname === "api.perplexity.ai";
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
136
148
  function resolveModel(optionModel, fallbackModel) {
137
149
  if (typeof optionModel === "string" && optionModel.trim().length > 0) {
138
150
  return optionModel;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  optionSchema,
3
3
  providers
4
- } from "./chunk-KH54YH7D.js";
4
+ } from "./chunk-QERSNL3J.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.6.0",
12
+ version: "4.8.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: [
@@ -82,10 +82,14 @@ var package_default = {
82
82
  types: "./dist/pi.d.ts",
83
83
  import: "./dist/pi.js"
84
84
  },
85
+ "./durable": {
86
+ types: "./dist/durable.d.ts",
87
+ import: "./dist/durable.js"
88
+ },
85
89
  "./config.schema.json": "./dist/config.schema.json"
86
90
  },
87
91
  scripts: {
88
- build: "rm -rf dist && esbuild index=src/index.ts cli=src/cli.ts pi=src/pi.ts generate-schema=scripts/generate-schema.ts --bundle --splitting --format=esm --platform=node --target=node22 --packages=external --outdir=dist && node dist/generate-schema.js && biome format --write src/config.schema.json >&2 && rm dist/generate-schema.js && tsc --project tsconfig.build.json && cp src/config.schema.json dist/config.schema.json && chmod +x dist/cli.js",
92
+ build: "rm -rf dist && esbuild index=src/index.ts cli=src/cli.ts pi=src/pi.ts durable=src/durable.ts generate-schema=scripts/generate-schema.ts --bundle --splitting --format=esm --platform=node --target=node22 --packages=external --outdir=dist && node dist/generate-schema.js && biome format --write src/config.schema.json >&2 && rm dist/generate-schema.js && tsc --project tsconfig.build.json && cp src/config.schema.json dist/config.schema.json && chmod +x dist/cli.js",
89
93
  prepare: "npm run build",
90
94
  prepack: "npm run build",
91
95
  check: "tsc --noEmit",
@@ -112,20 +116,26 @@ var package_default = {
112
116
  },
113
117
  devDependencies: {
114
118
  "@biomejs/biome": "^2.5.4",
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
+ "@earendil-works/chord": "^1.0.0",
120
+ "@earendil-works/pi-ai": "^1.0.0",
121
+ "@earendil-works/pi-coding-agent": "^1.0.0",
122
+ "@earendil-works/pi-durable": "^1.0.0",
123
+ "@earendil-works/pi-tui": "^1.0.0",
118
124
  "@types/node": "^26.1.1",
119
125
  esbuild: "^0.28.1",
120
126
  typescript: "^7.0.2",
121
127
  vitest: "^4.1.10"
122
128
  },
123
129
  peerDependencies: {
130
+ "@earendil-works/pi-durable": "^1.0.0",
124
131
  "@earendil-works/pi-coding-agent": "*",
125
132
  "@earendil-works/pi-tui": "*",
126
133
  typebox: "*"
127
134
  },
128
135
  peerDependenciesMeta: {
136
+ "@earendil-works/pi-durable": {
137
+ optional: true
138
+ },
129
139
  "@earendil-works/pi-coding-agent": {
130
140
  optional: true
131
141
  },
@@ -6,14 +6,14 @@ import {
6
6
  } from "./chunk-WCTOYZVB.js";
7
7
  import {
8
8
  configurationSchema
9
- } from "./chunk-5TNPVBAZ.js";
9
+ } from "./chunk-G7LMBPW4.js";
10
10
  import {
11
11
  effectiveOptions,
12
12
  optionSchema,
13
13
  providers,
14
14
  selectProvider,
15
15
  validateConfiguredOptions
16
- } from "./chunk-KH54YH7D.js";
16
+ } from "./chunk-QERSNL3J.js";
17
17
  import {
18
18
  cancelProcessGroup,
19
19
  killProcessGroup
@@ -1890,7 +1890,7 @@ var perplexityProvider = defineProvider({
1890
1890
  answer: { options: chatOptions, retrySafe: false },
1891
1891
  research: { options: chatOptions, retrySafe: false }
1892
1892
  },
1893
- load: async () => (await import("./adapter-NEXSOHGT.js")).adapter
1893
+ load: async () => (await import("./adapter-SLTIAAML.js")).adapter
1894
1894
  });
1895
1895
 
1896
1896
  // src/providers/serpbase/definition.ts
@@ -1991,7 +1991,7 @@ var serpbaseProvider = defineProvider({
1991
1991
  retrySafe: true
1992
1992
  }
1993
1993
  },
1994
- load: async () => (await import("./adapter-BQUN3ME2.js")).adapter
1994
+ load: async () => (await import("./adapter-QU2G7O25.js")).adapter
1995
1995
  });
1996
1996
 
1997
1997
  // src/providers/serper/definition.ts
@@ -0,0 +1,112 @@
1
+ // src/tool-definition.ts
2
+ import { Type } from "typebox";
3
+ function webToolParameters(inspection) {
4
+ const capability = inspection.capability;
5
+ const fields = capability === "contents" ? { urls: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }) } : capability === "research" ? { input: Type.String({ minLength: 1 }) } : {
6
+ queries: Type.Array(Type.String({ minLength: 1 }), {
7
+ minItems: 1,
8
+ maxItems: 10
9
+ }),
10
+ ...capability === "search" ? { maxResults: Type.Optional(Type.Integer({ minimum: 1 })) } : {}
11
+ };
12
+ return Type.Object(
13
+ {
14
+ ...fields,
15
+ ...inspection.optionSchema ? {
16
+ options: Type.Optional(
17
+ inspection.optionSchema
18
+ )
19
+ } : {}
20
+ },
21
+ { additionalProperties: false }
22
+ );
23
+ }
24
+ var descriptions = {
25
+ search: "Search up to ten queries and return titles, URLs, and snippets in input order.",
26
+ contents: "Fetch and extract readable contents from web URLs.",
27
+ answer: "Answer up to ten questions using web-grounded evidence.",
28
+ research: "Run foreground multi-step web research and return the final report."
29
+ };
30
+
31
+ // src/pi-validation.ts
32
+ import { stripVTControlCharacters } from "node:util";
33
+ import { Check, Errors } from "typebox/value";
34
+ function isSchema(value) {
35
+ return !!value && typeof value === "object" && !Array.isArray(value);
36
+ }
37
+ function alternatives(schema) {
38
+ return ["anyOf", "oneOf", "allOf"].flatMap(
39
+ (key) => Array.isArray(schema[key]) ? schema[key].filter(isSchema) : []
40
+ );
41
+ }
42
+ function acceptsPath(schema, path) {
43
+ if (schema === true) return true;
44
+ if (!isSchema(schema)) return false;
45
+ if (!path.length) return true;
46
+ if (alternatives(schema).some((branch) => acceptsPath(branch, path)))
47
+ return true;
48
+ const [key, ...rest] = path;
49
+ if (schema.type === "array") return acceptsPath(schema.items, rest);
50
+ if (schema.properties && Object.hasOwn(schema.properties, key))
51
+ return acceptsPath(schema.properties[key], rest);
52
+ if (isSchema(schema.patternProperties)) {
53
+ for (const [pattern, child] of Object.entries(schema.patternProperties))
54
+ if (new RegExp(pattern).test(key) && acceptsPath(child, rest))
55
+ return true;
56
+ }
57
+ if (schema.additionalProperties === false) return false;
58
+ if (schema.type && schema.type !== "object") return false;
59
+ if (alternatives(schema).length && !schema.properties && schema.additionalProperties === void 0)
60
+ return false;
61
+ return schema.additionalProperties === void 0 || acceptsPath(schema.additionalProperties, rest);
62
+ }
63
+ function displayPath(path) {
64
+ return path.map((part, index) => {
65
+ const safe = stripVTControlCharacters(part);
66
+ if (/^[a-zA-Z_][\w-]*$/.test(safe)) return `${index ? "." : ""}${safe}`;
67
+ if (/^\d+$/.test(safe)) return `[${safe}]`;
68
+ return `[${JSON.stringify(safe)}]`;
69
+ }).join("");
70
+ }
71
+ function locations(schema, name, path = []) {
72
+ if (!isSchema(schema)) return [];
73
+ const found = alternatives(schema).flatMap(
74
+ (branch) => locations(branch, name, path)
75
+ );
76
+ if (isSchema(schema.properties)) {
77
+ for (const [key, child] of Object.entries(schema.properties)) {
78
+ const next = [...path, key];
79
+ if (key === name) found.push(next);
80
+ found.push(...locations(child, name, next));
81
+ }
82
+ }
83
+ return found;
84
+ }
85
+ function prepareToolArguments(schema, args) {
86
+ if (Check(schema, args)) return args;
87
+ const messages = /* @__PURE__ */ new Set();
88
+ for (const error of Errors(schema, args)) {
89
+ if (error.keyword !== "additionalProperties") continue;
90
+ const names = error.params.additionalProperties;
91
+ if (!Array.isArray(names)) continue;
92
+ const parent = error.instancePath.split("/").slice(1).map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"));
93
+ for (const name of names) {
94
+ if (typeof name !== "string") continue;
95
+ const path = [...parent, name];
96
+ if (acceptsPath(schema, path)) continue;
97
+ const targets = [...new Set(locations(schema, name).map(displayPath))];
98
+ const suggestion = targets.length === 1 ? ` Use ${targets[0]} instead.` : " Check the tool's parameter schema.";
99
+ messages.add(`Invalid parameter: ${displayPath(path)}.${suggestion}`);
100
+ if (messages.size === 3) break;
101
+ }
102
+ if (messages.size === 3) break;
103
+ }
104
+ if (messages.size) throw new Error([...messages].join("\n"));
105
+ return args;
106
+ }
107
+
108
+ export {
109
+ webToolParameters,
110
+ descriptions,
111
+ prepareToolArguments
112
+ };
package/dist/cli.js CHANGED
@@ -11,14 +11,14 @@ import {
11
11
  redactConfig,
12
12
  resolveConfigPath,
13
13
  setCapabilityDefault
14
- } from "./chunk-QXTTUDLI.js";
14
+ } from "./chunk-IRKSK34I.js";
15
15
  import "./chunk-WCTOYZVB.js";
16
16
  import {
17
17
  PACKAGE_VERSION
18
- } from "./chunk-5TNPVBAZ.js";
18
+ } from "./chunk-G7LMBPW4.js";
19
19
  import {
20
20
  validateConfiguredOptions
21
- } from "./chunk-KH54YH7D.js";
21
+ } from "./chunk-QERSNL3J.js";
22
22
  import "./chunk-ZVTVERW5.js";
23
23
  import {
24
24
  CAPABILITIES,
@@ -0,0 +1,8 @@
1
+ import { type Extension } from "@earendil-works/pi-durable";
2
+ import { type WebfoxClient, type CreateWebfoxOptions } from "./index.js";
3
+ export interface DurableWebfoxOptions extends CreateWebfoxOptions {
4
+ /** Supply a client for an embedded host or deterministic tests. */
5
+ client?: WebfoxClient;
6
+ }
7
+ /** Install alongside CodingTools. Every call uses the conversation's current environment. */
8
+ export declare function createWebfoxExtension(options?: DurableWebfoxOptions): Extension;
@@ -0,0 +1,124 @@
1
+ import {
2
+ descriptions,
3
+ prepareToolArguments,
4
+ webToolParameters
5
+ } from "./chunk-VVOQKYOP.js";
6
+ import {
7
+ renderTextDocument
8
+ } from "./chunk-BTWECET5.js";
9
+ import {
10
+ createWebfox
11
+ } from "./chunk-IRKSK34I.js";
12
+ import "./chunk-WCTOYZVB.js";
13
+ import "./chunk-G7LMBPW4.js";
14
+ import "./chunk-QERSNL3J.js";
15
+ import "./chunk-ZVTVERW5.js";
16
+ import {
17
+ CAPABILITIES
18
+ } from "./chunk-GPACLYNT.js";
19
+ import "./chunk-U4BLULLV.js";
20
+
21
+ // src/durable.ts
22
+ import { mkdtemp, writeFile } from "node:fs/promises";
23
+ import { tmpdir } from "node:os";
24
+ import { join } from "node:path";
25
+ import {
26
+ truncateHead,
27
+ withFileMutationQueue
28
+ } from "@earendil-works/pi-coding-agent";
29
+ import {
30
+ defineExtension,
31
+ defineTool
32
+ } from "@earendil-works/pi-durable";
33
+ function createWebfoxExtension(options = {}) {
34
+ const clients = /* @__PURE__ */ new Map();
35
+ const clientFor = (cwd) => {
36
+ if (options.client) return options.client;
37
+ let client = clients.get(cwd);
38
+ if (!client) {
39
+ client = createWebfox({ ...options, cwd });
40
+ clients.set(cwd, client);
41
+ }
42
+ return client;
43
+ };
44
+ const initialCwd = options.cwd ?? process.cwd();
45
+ const initial = clientFor(initialCwd);
46
+ const inspections = CAPABILITIES.map(
47
+ (capability) => initial.inspectCapability(capability)
48
+ ).filter((inspection) => inspection.provider);
49
+ return defineExtension({
50
+ name: "webfox",
51
+ tools: inspections.map((inspection) => {
52
+ const capability = inspection.capability;
53
+ const parameters = webToolParameters(inspection);
54
+ return defineTool({
55
+ name: `web_${capability}`,
56
+ description: [
57
+ descriptions[capability],
58
+ ...inspection.promptGuidelines ?? [],
59
+ "Output is limited to 2000 lines or 50 KiB; complete results are saved to a private file when truncated."
60
+ ].join("\n"),
61
+ parameters,
62
+ // Web requests may incur charges. A crash reports interruption rather than silently charging again.
63
+ replay: "unsafe",
64
+ prepareArguments: (args) => prepareToolArguments(parameters, args),
65
+ async execute(args, api, context) {
66
+ const client = clientFor(
67
+ api.env?.cwd ?? (await api.agent(context)).cwd ?? initialCwd
68
+ );
69
+ const request = {
70
+ provider: inspection.provider,
71
+ signal: context.abortSignal,
72
+ options: args.options,
73
+ onProgress: (event) => api.output(`${event.message}
74
+ `)
75
+ };
76
+ const result = capability === "search" ? await client.search({
77
+ ...request,
78
+ queries: args.queries,
79
+ maxResults: args.maxResults
80
+ }) : capability === "contents" ? await client.contents({
81
+ ...request,
82
+ urls: args.urls
83
+ }) : capability === "answer" ? await client.answer({
84
+ ...request,
85
+ queries: args.queries
86
+ }) : await client.research({
87
+ ...request,
88
+ input: args.input
89
+ });
90
+ const truncated = truncateHead(renderTextDocument(result));
91
+ let body = truncated.content;
92
+ let fullOutputPath;
93
+ if (truncated.truncated) {
94
+ fullOutputPath = join(
95
+ await mkdtemp(join(tmpdir(), "web-")),
96
+ "result.json"
97
+ );
98
+ await withFileMutationQueue(
99
+ fullOutputPath,
100
+ () => writeFile(fullOutputPath, JSON.stringify(result), {
101
+ mode: 384
102
+ })
103
+ );
104
+ body += `
105
+
106
+ Full results: ${fullOutputPath}`;
107
+ }
108
+ return {
109
+ content: [{ type: "text", text: body }],
110
+ isError: result.status === "partial",
111
+ details: {
112
+ status: result.status,
113
+ capability,
114
+ ...fullOutputPath ? { fullOutputPath } : {}
115
+ }
116
+ };
117
+ }
118
+ });
119
+ })
120
+ });
121
+ }
122
+ export {
123
+ createWebfoxExtension
124
+ };
package/dist/index.js CHANGED
@@ -6,14 +6,14 @@ import {
6
6
  resolveConfigPath,
7
7
  setCapabilityDefault,
8
8
  validateConfig
9
- } from "./chunk-QXTTUDLI.js";
9
+ } from "./chunk-IRKSK34I.js";
10
10
  import "./chunk-WCTOYZVB.js";
11
11
  import {
12
12
  CONFIG_SCHEMA_URL
13
- } from "./chunk-5TNPVBAZ.js";
13
+ } from "./chunk-G7LMBPW4.js";
14
14
  import {
15
15
  validateConfiguredOptions
16
- } from "./chunk-KH54YH7D.js";
16
+ } from "./chunk-QERSNL3J.js";
17
17
  import "./chunk-ZVTVERW5.js";
18
18
  import {
19
19
  CAPABILITIES,
package/dist/pi.js CHANGED
@@ -1,13 +1,18 @@
1
+ import {
2
+ descriptions,
3
+ prepareToolArguments,
4
+ webToolParameters
5
+ } from "./chunk-VVOQKYOP.js";
1
6
  import {
2
7
  renderTextDocument
3
8
  } from "./chunk-BTWECET5.js";
4
9
  import {
5
10
  createWebfox,
6
11
  resolveConfigPath
7
- } from "./chunk-QXTTUDLI.js";
12
+ } from "./chunk-IRKSK34I.js";
8
13
  import "./chunk-WCTOYZVB.js";
9
- import "./chunk-5TNPVBAZ.js";
10
- import "./chunk-KH54YH7D.js";
14
+ import "./chunk-G7LMBPW4.js";
15
+ import "./chunk-QERSNL3J.js";
11
16
  import "./chunk-ZVTVERW5.js";
12
17
  import {
13
18
  CAPABILITIES,
@@ -26,7 +31,6 @@ import {
26
31
  truncateHead,
27
32
  withFileMutationQueue
28
33
  } from "@earendil-works/pi-coding-agent";
29
- import { Type as Type2 } from "typebox";
30
34
 
31
35
  // src/pi-render.ts
32
36
  import { stripVTControlCharacters as stripVTControlCharacters3 } from "node:util";
@@ -342,83 +346,6 @@ function renderWebResult(result, options, theme, isError, capability) {
342
346
  };
343
347
  }
344
348
 
345
- // src/pi-validation.ts
346
- import { stripVTControlCharacters as stripVTControlCharacters4 } from "node:util";
347
- import { Check, Errors } from "typebox/value";
348
- function isSchema(value) {
349
- return !!value && typeof value === "object" && !Array.isArray(value);
350
- }
351
- function alternatives(schema) {
352
- return ["anyOf", "oneOf", "allOf"].flatMap(
353
- (key) => Array.isArray(schema[key]) ? schema[key].filter(isSchema) : []
354
- );
355
- }
356
- function acceptsPath(schema, path) {
357
- if (schema === true) return true;
358
- if (!isSchema(schema)) return false;
359
- if (!path.length) return true;
360
- if (alternatives(schema).some((branch) => acceptsPath(branch, path)))
361
- return true;
362
- const [key, ...rest] = path;
363
- if (schema.type === "array") return acceptsPath(schema.items, rest);
364
- if (schema.properties && Object.hasOwn(schema.properties, key))
365
- return acceptsPath(schema.properties[key], rest);
366
- if (isSchema(schema.patternProperties)) {
367
- for (const [pattern, child] of Object.entries(schema.patternProperties))
368
- if (new RegExp(pattern).test(key) && acceptsPath(child, rest))
369
- return true;
370
- }
371
- if (schema.additionalProperties === false) return false;
372
- if (schema.type && schema.type !== "object") return false;
373
- if (alternatives(schema).length && !schema.properties && schema.additionalProperties === void 0)
374
- return false;
375
- return schema.additionalProperties === void 0 || acceptsPath(schema.additionalProperties, rest);
376
- }
377
- function displayPath(path) {
378
- return path.map((part, index) => {
379
- const safe = stripVTControlCharacters4(part);
380
- if (/^[a-zA-Z_][\w-]*$/.test(safe)) return `${index ? "." : ""}${safe}`;
381
- if (/^\d+$/.test(safe)) return `[${safe}]`;
382
- return `[${JSON.stringify(safe)}]`;
383
- }).join("");
384
- }
385
- function locations(schema, name, path = []) {
386
- if (!isSchema(schema)) return [];
387
- const found = alternatives(schema).flatMap(
388
- (branch) => locations(branch, name, path)
389
- );
390
- if (isSchema(schema.properties)) {
391
- for (const [key, child] of Object.entries(schema.properties)) {
392
- const next = [...path, key];
393
- if (key === name) found.push(next);
394
- found.push(...locations(child, name, next));
395
- }
396
- }
397
- return found;
398
- }
399
- function prepareToolArguments(schema, args) {
400
- if (Check(schema, args)) return args;
401
- const messages = /* @__PURE__ */ new Set();
402
- for (const error of Errors(schema, args)) {
403
- if (error.keyword !== "additionalProperties") continue;
404
- const names = error.params.additionalProperties;
405
- if (!Array.isArray(names)) continue;
406
- const parent = error.instancePath.split("/").slice(1).map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"));
407
- for (const name of names) {
408
- if (typeof name !== "string") continue;
409
- const path = [...parent, name];
410
- if (acceptsPath(schema, path)) continue;
411
- const targets = [...new Set(locations(schema, name).map(displayPath))];
412
- const suggestion = targets.length === 1 ? ` Use ${targets[0]} instead.` : " Check the tool's parameter schema.";
413
- messages.add(`Invalid parameter: ${displayPath(path)}.${suggestion}`);
414
- if (messages.size === 3) break;
415
- }
416
- if (messages.size === 3) break;
417
- }
418
- if (messages.size) throw new Error([...messages].join("\n"));
419
- return args;
420
- }
421
-
422
349
  // src/pi-diagnostics.ts
423
350
  import { homedir } from "node:os";
424
351
  import { sep } from "node:path";
@@ -556,32 +483,19 @@ function webExtension(pi) {
556
483
  for (const inspection of selected) {
557
484
  const capability = inspection.capability;
558
485
  const provider = inspection.provider;
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 }), {
561
- minItems: 1,
562
- maxItems: 10
563
- }),
564
- ...capability === "search" ? { maxResults: Type2.Optional(Type2.Integer({ minimum: 1 })) } : {}
565
- };
566
- const parameters = Type2.Object(
567
- {
568
- ...fields,
569
- ...inspection.optionSchema ? {
570
- options: Type2.Optional(
571
- inspection.optionSchema
572
- )
573
- } : {}
574
- },
575
- { additionalProperties: false }
576
- );
486
+ const parameters = webToolParameters(inspection);
577
487
  pi.registerTool({
578
488
  name: `web_${capability}`,
489
+ namespace: {
490
+ name: "web",
491
+ description: "Search, extract pages, answer, and research the web."
492
+ },
579
493
  label: `Web ${capability[0].toUpperCase()}${capability.slice(1)}`,
580
494
  // Keep provider guidance with the tool schema, including with custom system prompts.
581
495
  description: [
582
496
  descriptions[capability],
583
497
  ...inspection.promptGuidelines ?? [],
584
- "Output is truncated to 2000 lines or 50 KiB; full results are saved to a file when truncated."
498
+ "Model-facing text is truncated to 2000 lines or 50 KiB; full results are saved to a file when truncated. Codemode scripts receive complete structured results."
585
499
  ].join("\n"),
586
500
  parameters,
587
501
  // Let permission extensions and tool loadouts treat web access as a
@@ -676,12 +590,6 @@ Full results: ${fullOutputPath}`;
676
590
  });
677
591
  }
678
592
  }
679
- var descriptions = {
680
- search: "Search up to ten queries and return titles, URLs, and snippets in input order.",
681
- contents: "Fetch and extract readable contents from web URLs.",
682
- answer: "Answer up to ten questions using web-grounded evidence.",
683
- research: "Run foreground multi-step web research and return the final report."
684
- };
685
593
  export {
686
594
  webExtension as default
687
595
  };
@@ -0,0 +1,4 @@
1
+ import { type TObject } from "typebox";
2
+ import type { Capability, WebfoxClient } from "./index.js";
3
+ export declare function webToolParameters(inspection: ReturnType<WebfoxClient["inspectCapability"]>): TObject;
4
+ export declare const descriptions: Record<Capability, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webfox",
3
- "version": "4.6.0",
3
+ "version": "4.8.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": [
@@ -73,10 +73,14 @@
73
73
  "types": "./dist/pi.d.ts",
74
74
  "import": "./dist/pi.js"
75
75
  },
76
+ "./durable": {
77
+ "types": "./dist/durable.d.ts",
78
+ "import": "./dist/durable.js"
79
+ },
76
80
  "./config.schema.json": "./dist/config.schema.json"
77
81
  },
78
82
  "scripts": {
79
- "build": "rm -rf dist && esbuild index=src/index.ts cli=src/cli.ts pi=src/pi.ts generate-schema=scripts/generate-schema.ts --bundle --splitting --format=esm --platform=node --target=node22 --packages=external --outdir=dist && node dist/generate-schema.js && biome format --write src/config.schema.json >&2 && rm dist/generate-schema.js && tsc --project tsconfig.build.json && cp src/config.schema.json dist/config.schema.json && chmod +x dist/cli.js",
83
+ "build": "rm -rf dist && esbuild index=src/index.ts cli=src/cli.ts pi=src/pi.ts durable=src/durable.ts generate-schema=scripts/generate-schema.ts --bundle --splitting --format=esm --platform=node --target=node22 --packages=external --outdir=dist && node dist/generate-schema.js && biome format --write src/config.schema.json >&2 && rm dist/generate-schema.js && tsc --project tsconfig.build.json && cp src/config.schema.json dist/config.schema.json && chmod +x dist/cli.js",
80
84
  "prepare": "npm run build",
81
85
  "prepack": "npm run build",
82
86
  "check": "tsc --noEmit",
@@ -103,20 +107,26 @@
103
107
  },
104
108
  "devDependencies": {
105
109
  "@biomejs/biome": "^2.5.4",
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
+ "@earendil-works/chord": "^1.0.0",
111
+ "@earendil-works/pi-ai": "^1.0.0",
112
+ "@earendil-works/pi-coding-agent": "^1.0.0",
113
+ "@earendil-works/pi-durable": "^1.0.0",
114
+ "@earendil-works/pi-tui": "^1.0.0",
109
115
  "@types/node": "^26.1.1",
110
116
  "esbuild": "^0.28.1",
111
117
  "typescript": "^7.0.2",
112
118
  "vitest": "^4.1.10"
113
119
  },
114
120
  "peerDependencies": {
121
+ "@earendil-works/pi-durable": "^1.0.0",
115
122
  "@earendil-works/pi-coding-agent": "*",
116
123
  "@earendil-works/pi-tui": "*",
117
124
  "typebox": "*"
118
125
  },
119
126
  "peerDependenciesMeta": {
127
+ "@earendil-works/pi-durable": {
128
+ "optional": true
129
+ },
120
130
  "@earendil-works/pi-coding-agent": {
121
131
  "optional": true
122
132
  },