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 +108 -0
- package/dist/{adapter-BQUN3ME2.js → adapter-QU2G7O25.js} +2 -2
- package/dist/{adapter-NEXSOHGT.js → adapter-SLTIAAML.js} +13 -1
- package/dist/{chunk-5TNPVBAZ.js → chunk-G7LMBPW4.js} +16 -6
- package/dist/{chunk-QXTTUDLI.js → chunk-IRKSK34I.js} +2 -2
- package/dist/{chunk-KH54YH7D.js → chunk-QERSNL3J.js} +2 -2
- package/dist/chunk-VVOQKYOP.js +112 -0
- package/dist/cli.js +3 -3
- package/dist/durable.d.ts +8 -0
- package/dist/durable.js +124 -0
- package/dist/index.js +3 -3
- package/dist/pi.js +14 -106
- package/dist/tool-definition.d.ts +4 -0
- package/package.json +15 -5
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-
|
|
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-
|
|
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.
|
|
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/
|
|
116
|
-
"@earendil-works/pi-
|
|
117
|
-
"@earendil-works/pi-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
14
|
+
} from "./chunk-IRKSK34I.js";
|
|
15
15
|
import "./chunk-WCTOYZVB.js";
|
|
16
16
|
import {
|
|
17
17
|
PACKAGE_VERSION
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-G7LMBPW4.js";
|
|
19
19
|
import {
|
|
20
20
|
validateConfiguredOptions
|
|
21
|
-
} from "./chunk-
|
|
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;
|
package/dist/durable.js
ADDED
|
@@ -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-
|
|
9
|
+
} from "./chunk-IRKSK34I.js";
|
|
10
10
|
import "./chunk-WCTOYZVB.js";
|
|
11
11
|
import {
|
|
12
12
|
CONFIG_SCHEMA_URL
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-G7LMBPW4.js";
|
|
14
14
|
import {
|
|
15
15
|
validateConfiguredOptions
|
|
16
|
-
} from "./chunk-
|
|
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-
|
|
12
|
+
} from "./chunk-IRKSK34I.js";
|
|
8
13
|
import "./chunk-WCTOYZVB.js";
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
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
|
|
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
|
-
"
|
|
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.
|
|
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/
|
|
107
|
-
"@earendil-works/pi-
|
|
108
|
-
"@earendil-works/pi-
|
|
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
|
},
|