webfox 4.5.0 → 4.7.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-IDKJASTU.js} +2 -2
- package/dist/{chunk-D4HCJRBE.js → chunk-3LK52VNJ.js} +2 -2
- package/dist/{chunk-VI3473TU.js → chunk-MLRWLHC2.js} +18 -8
- package/dist/{chunk-KH54YH7D.js → chunk-NOBEWI5W.js} +1 -1
- package/dist/chunk-VVOQKYOP.js +112 -0
- package/dist/cli.js +3 -3
- package/dist/document-schema.d.ts +114 -0
- package/dist/durable.d.ts +8 -0
- package/dist/durable.js +124 -0
- package/dist/index.js +3 -3
- package/dist/pi.js +114 -119
- package/dist/tool-definition.d.ts +4 -0
- package/docs/reference.md +7 -0
- package/package.json +17 -7
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-NOBEWI5W.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
|
|
@@ -6,14 +6,14 @@ import {
|
|
|
6
6
|
} from "./chunk-WCTOYZVB.js";
|
|
7
7
|
import {
|
|
8
8
|
configurationSchema
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-MLRWLHC2.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-NOBEWI5W.js";
|
|
17
17
|
import {
|
|
18
18
|
cancelProcessGroup,
|
|
19
19
|
killProcessGroup
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
optionSchema,
|
|
3
3
|
providers
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NOBEWI5W.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.7.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",
|
|
@@ -107,25 +111,31 @@ var package_default = {
|
|
|
107
111
|
openai: "^7.10.0",
|
|
108
112
|
"parallel-web": "^1.3.3",
|
|
109
113
|
picocolors: "^1.1.1",
|
|
110
|
-
typebox: "^1.3.6",
|
|
111
114
|
"valyu-js": "^2.10.1",
|
|
112
115
|
yaml: "^2.9.0"
|
|
113
116
|
},
|
|
114
117
|
devDependencies: {
|
|
115
118
|
"@biomejs/biome": "^2.5.4",
|
|
116
|
-
"@earendil-works/
|
|
117
|
-
"@earendil-works/pi-
|
|
118
|
-
"@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",
|
|
119
124
|
"@types/node": "^26.1.1",
|
|
120
125
|
esbuild: "^0.28.1",
|
|
121
126
|
typescript: "^7.0.2",
|
|
122
127
|
vitest: "^4.1.10"
|
|
123
128
|
},
|
|
124
129
|
peerDependencies: {
|
|
130
|
+
"@earendil-works/pi-durable": "^1.0.0",
|
|
125
131
|
"@earendil-works/pi-coding-agent": "*",
|
|
126
|
-
"@earendil-works/pi-tui": "*"
|
|
132
|
+
"@earendil-works/pi-tui": "*",
|
|
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
|
},
|
|
@@ -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-IDKJASTU.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-3LK52VNJ.js";
|
|
15
15
|
import "./chunk-WCTOYZVB.js";
|
|
16
16
|
import {
|
|
17
17
|
PACKAGE_VERSION
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-MLRWLHC2.js";
|
|
19
19
|
import {
|
|
20
20
|
validateConfiguredOptions
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-NOBEWI5W.js";
|
|
22
22
|
import "./chunk-ZVTVERW5.js";
|
|
23
23
|
import {
|
|
24
24
|
CAPABILITIES,
|
|
@@ -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 {};
|
|
@@ -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-3LK52VNJ.js";
|
|
12
|
+
import "./chunk-WCTOYZVB.js";
|
|
13
|
+
import "./chunk-MLRWLHC2.js";
|
|
14
|
+
import "./chunk-NOBEWI5W.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-3LK52VNJ.js";
|
|
10
10
|
import "./chunk-WCTOYZVB.js";
|
|
11
11
|
import {
|
|
12
12
|
CONFIG_SCHEMA_URL
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-MLRWLHC2.js";
|
|
14
14
|
import {
|
|
15
15
|
validateConfiguredOptions
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-NOBEWI5W.js";
|
|
17
17
|
import "./chunk-ZVTVERW5.js";
|
|
18
18
|
import {
|
|
19
19
|
CAPABILITIES,
|
package/dist/pi.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
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-3LK52VNJ.js";
|
|
8
13
|
import "./chunk-WCTOYZVB.js";
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-MLRWLHC2.js";
|
|
15
|
+
import "./chunk-NOBEWI5W.js";
|
|
11
16
|
import "./chunk-ZVTVERW5.js";
|
|
12
17
|
import {
|
|
13
|
-
CAPABILITIES
|
|
18
|
+
CAPABILITIES,
|
|
19
|
+
PROVIDER_IDS,
|
|
20
|
+
WEBFOX_ERROR_CODES
|
|
14
21
|
} from "./chunk-GPACLYNT.js";
|
|
15
22
|
import {
|
|
16
23
|
WebfoxError
|
|
@@ -24,7 +31,6 @@ import {
|
|
|
24
31
|
truncateHead,
|
|
25
32
|
withFileMutationQueue
|
|
26
33
|
} from "@earendil-works/pi-coding-agent";
|
|
27
|
-
import { Type } from "typebox";
|
|
28
34
|
|
|
29
35
|
// src/pi-render.ts
|
|
30
36
|
import { stripVTControlCharacters as stripVTControlCharacters3 } from "node:util";
|
|
@@ -186,11 +192,11 @@ function link(url, theme) {
|
|
|
186
192
|
return label;
|
|
187
193
|
}
|
|
188
194
|
function renderSearchResult(details, fallback, theme) {
|
|
189
|
-
const
|
|
195
|
+
const document2 = isRecord2(details) && searchDocument(details.result) ? details.result : void 0;
|
|
190
196
|
return {
|
|
191
197
|
render(width) {
|
|
192
198
|
if (width <= 0) return [];
|
|
193
|
-
if (!
|
|
199
|
+
if (!document2) {
|
|
194
200
|
const text = plainText(fallback).split("\n").map((line) => {
|
|
195
201
|
const urlLine = /^(\s*)(https?:\/\/\S+)(\s*)$/.exec(line);
|
|
196
202
|
return urlLine ? urlLine[1] + link(urlLine[2], theme) + urlLine[3] : theme.fg("toolOutput", line);
|
|
@@ -209,9 +215,9 @@ function renderSearchResult(details, fallback, theme) {
|
|
|
209
215
|
)
|
|
210
216
|
);
|
|
211
217
|
};
|
|
212
|
-
|
|
218
|
+
document2.results.forEach((entry, index) => {
|
|
213
219
|
if (index) lines.push("");
|
|
214
|
-
if (
|
|
220
|
+
if (document2.results.length > 1) {
|
|
215
221
|
append(
|
|
216
222
|
theme.fg(
|
|
217
223
|
"mdHeading",
|
|
@@ -304,8 +310,8 @@ var WebCall = class {
|
|
|
304
310
|
};
|
|
305
311
|
function renderWebResult(result, options, theme, isError, capability) {
|
|
306
312
|
const text = result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join("\n");
|
|
307
|
-
const
|
|
308
|
-
const isSearch = (capability ??
|
|
313
|
+
const metadata2 = result.details;
|
|
314
|
+
const isSearch = (capability ?? metadata2?.capability ?? metadata2?.result?.capability) === "search";
|
|
309
315
|
const body = () => isSearch ? renderSearchResult(result.details, text, theme) : new Markdown(text, 0, 0, getMarkdownTheme());
|
|
310
316
|
const rows = statusRows(result.details);
|
|
311
317
|
if (rows?.length) {
|
|
@@ -340,83 +346,6 @@ function renderWebResult(result, options, theme, isError, capability) {
|
|
|
340
346
|
};
|
|
341
347
|
}
|
|
342
348
|
|
|
343
|
-
// src/pi-validation.ts
|
|
344
|
-
import { stripVTControlCharacters as stripVTControlCharacters4 } from "node:util";
|
|
345
|
-
import { Check, Errors } from "typebox/value";
|
|
346
|
-
function isSchema(value) {
|
|
347
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
348
|
-
}
|
|
349
|
-
function alternatives(schema) {
|
|
350
|
-
return ["anyOf", "oneOf", "allOf"].flatMap(
|
|
351
|
-
(key) => Array.isArray(schema[key]) ? schema[key].filter(isSchema) : []
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
function acceptsPath(schema, path) {
|
|
355
|
-
if (schema === true) return true;
|
|
356
|
-
if (!isSchema(schema)) return false;
|
|
357
|
-
if (!path.length) return true;
|
|
358
|
-
if (alternatives(schema).some((branch) => acceptsPath(branch, path)))
|
|
359
|
-
return true;
|
|
360
|
-
const [key, ...rest] = path;
|
|
361
|
-
if (schema.type === "array") return acceptsPath(schema.items, rest);
|
|
362
|
-
if (schema.properties && Object.hasOwn(schema.properties, key))
|
|
363
|
-
return acceptsPath(schema.properties[key], rest);
|
|
364
|
-
if (isSchema(schema.patternProperties)) {
|
|
365
|
-
for (const [pattern, child] of Object.entries(schema.patternProperties))
|
|
366
|
-
if (new RegExp(pattern).test(key) && acceptsPath(child, rest))
|
|
367
|
-
return true;
|
|
368
|
-
}
|
|
369
|
-
if (schema.additionalProperties === false) return false;
|
|
370
|
-
if (schema.type && schema.type !== "object") return false;
|
|
371
|
-
if (alternatives(schema).length && !schema.properties && schema.additionalProperties === void 0)
|
|
372
|
-
return false;
|
|
373
|
-
return schema.additionalProperties === void 0 || acceptsPath(schema.additionalProperties, rest);
|
|
374
|
-
}
|
|
375
|
-
function displayPath(path) {
|
|
376
|
-
return path.map((part, index) => {
|
|
377
|
-
const safe = stripVTControlCharacters4(part);
|
|
378
|
-
if (/^[a-zA-Z_][\w-]*$/.test(safe)) return `${index ? "." : ""}${safe}`;
|
|
379
|
-
if (/^\d+$/.test(safe)) return `[${safe}]`;
|
|
380
|
-
return `[${JSON.stringify(safe)}]`;
|
|
381
|
-
}).join("");
|
|
382
|
-
}
|
|
383
|
-
function locations(schema, name, path = []) {
|
|
384
|
-
if (!isSchema(schema)) return [];
|
|
385
|
-
const found = alternatives(schema).flatMap(
|
|
386
|
-
(branch) => locations(branch, name, path)
|
|
387
|
-
);
|
|
388
|
-
if (isSchema(schema.properties)) {
|
|
389
|
-
for (const [key, child] of Object.entries(schema.properties)) {
|
|
390
|
-
const next = [...path, key];
|
|
391
|
-
if (key === name) found.push(next);
|
|
392
|
-
found.push(...locations(child, name, next));
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return found;
|
|
396
|
-
}
|
|
397
|
-
function prepareToolArguments(schema, args) {
|
|
398
|
-
if (Check(schema, args)) return args;
|
|
399
|
-
const messages = /* @__PURE__ */ new Set();
|
|
400
|
-
for (const error of Errors(schema, args)) {
|
|
401
|
-
if (error.keyword !== "additionalProperties") continue;
|
|
402
|
-
const names = error.params.additionalProperties;
|
|
403
|
-
if (!Array.isArray(names)) continue;
|
|
404
|
-
const parent = error.instancePath.split("/").slice(1).map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
405
|
-
for (const name of names) {
|
|
406
|
-
if (typeof name !== "string") continue;
|
|
407
|
-
const path = [...parent, name];
|
|
408
|
-
if (acceptsPath(schema, path)) continue;
|
|
409
|
-
const targets = [...new Set(locations(schema, name).map(displayPath))];
|
|
410
|
-
const suggestion = targets.length === 1 ? ` Use ${targets[0]} instead.` : " Check the tool's parameter schema.";
|
|
411
|
-
messages.add(`Invalid parameter: ${displayPath(path)}.${suggestion}`);
|
|
412
|
-
if (messages.size === 3) break;
|
|
413
|
-
}
|
|
414
|
-
if (messages.size === 3) break;
|
|
415
|
-
}
|
|
416
|
-
if (messages.size) throw new Error([...messages].join("\n"));
|
|
417
|
-
return args;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
349
|
// src/pi-diagnostics.ts
|
|
421
350
|
import { homedir } from "node:os";
|
|
422
351
|
import { sep } from "node:path";
|
|
@@ -435,6 +364,85 @@ function configurationDiagnostic(error) {
|
|
|
435
364
|
].join("\n");
|
|
436
365
|
}
|
|
437
366
|
|
|
367
|
+
// src/document-schema.ts
|
|
368
|
+
import { Type } from "typebox";
|
|
369
|
+
var closed = { additionalProperties: false };
|
|
370
|
+
var metadata = Type.Record(Type.String(), Type.Unknown(), {
|
|
371
|
+
description: "Provider-specific fields."
|
|
372
|
+
});
|
|
373
|
+
var searchResult = Type.Object(
|
|
374
|
+
{
|
|
375
|
+
title: Type.String(),
|
|
376
|
+
url: Type.String(),
|
|
377
|
+
snippet: Type.String(),
|
|
378
|
+
score: Type.Optional(Type.Number()),
|
|
379
|
+
metadata: Type.Optional(metadata)
|
|
380
|
+
},
|
|
381
|
+
closed
|
|
382
|
+
);
|
|
383
|
+
var contentsAnswer = Type.Object(
|
|
384
|
+
{
|
|
385
|
+
url: Type.String({ description: "Final URL after redirects." }),
|
|
386
|
+
content: Type.Optional(Type.String()),
|
|
387
|
+
summary: Type.Optional(Type.Unknown()),
|
|
388
|
+
metadata: Type.Optional(metadata)
|
|
389
|
+
},
|
|
390
|
+
closed
|
|
391
|
+
);
|
|
392
|
+
var textAnswer = Type.Object(
|
|
393
|
+
{
|
|
394
|
+
text: Type.String(),
|
|
395
|
+
itemCount: Type.Optional(Type.Integer()),
|
|
396
|
+
metadata: Type.Optional(metadata)
|
|
397
|
+
},
|
|
398
|
+
closed
|
|
399
|
+
);
|
|
400
|
+
var serializedError = Type.Object(
|
|
401
|
+
{
|
|
402
|
+
code: Type.Enum(WEBFOX_ERROR_CODES),
|
|
403
|
+
message: Type.String(),
|
|
404
|
+
retryable: Type.Optional(Type.Boolean())
|
|
405
|
+
},
|
|
406
|
+
closed
|
|
407
|
+
);
|
|
408
|
+
function inputResult(value) {
|
|
409
|
+
const input = Type.String({
|
|
410
|
+
description: "The query, URL, or brief this entry answers."
|
|
411
|
+
});
|
|
412
|
+
return Type.Union([
|
|
413
|
+
Type.Object({ input, ok: Type.Literal(true), value }, closed),
|
|
414
|
+
Type.Object(
|
|
415
|
+
{ input, ok: Type.Literal(false), error: serializedError },
|
|
416
|
+
closed
|
|
417
|
+
)
|
|
418
|
+
]);
|
|
419
|
+
}
|
|
420
|
+
function document(capability, value) {
|
|
421
|
+
return Type.Object(
|
|
422
|
+
{
|
|
423
|
+
schemaVersion: Type.Literal(1),
|
|
424
|
+
capability: Type.Literal(capability),
|
|
425
|
+
provider: Type.Enum(PROVIDER_IDS),
|
|
426
|
+
status: Type.Enum(["ok", "partial"], {
|
|
427
|
+
description: "`partial` when at least one input failed."
|
|
428
|
+
}),
|
|
429
|
+
results: Type.Array(inputResult(value), {
|
|
430
|
+
description: "One entry per input, in input order."
|
|
431
|
+
})
|
|
432
|
+
},
|
|
433
|
+
closed
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
var DOCUMENT_SCHEMAS = {
|
|
437
|
+
search: document(
|
|
438
|
+
"search",
|
|
439
|
+
Type.Object({ results: Type.Array(searchResult) }, closed)
|
|
440
|
+
),
|
|
441
|
+
contents: document("contents", contentsAnswer),
|
|
442
|
+
answer: document("answer", textAnswer),
|
|
443
|
+
research: document("research", textAnswer)
|
|
444
|
+
};
|
|
445
|
+
|
|
438
446
|
// src/pi.ts
|
|
439
447
|
function webExtension(pi) {
|
|
440
448
|
const clients = /* @__PURE__ */ new Map();
|
|
@@ -475,34 +483,27 @@ function webExtension(pi) {
|
|
|
475
483
|
for (const inspection of selected) {
|
|
476
484
|
const capability = inspection.capability;
|
|
477
485
|
const provider = inspection.provider;
|
|
478
|
-
const
|
|
479
|
-
queries: Type.Array(Type.String({ minLength: 1 }), {
|
|
480
|
-
minItems: 1,
|
|
481
|
-
maxItems: 10
|
|
482
|
-
}),
|
|
483
|
-
...capability === "search" ? { maxResults: Type.Optional(Type.Integer({ minimum: 1 })) } : {}
|
|
484
|
-
};
|
|
485
|
-
const parameters = Type.Object(
|
|
486
|
-
{
|
|
487
|
-
...fields,
|
|
488
|
-
...inspection.optionSchema ? {
|
|
489
|
-
options: Type.Optional(
|
|
490
|
-
inspection.optionSchema
|
|
491
|
-
)
|
|
492
|
-
} : {}
|
|
493
|
-
},
|
|
494
|
-
{ additionalProperties: false }
|
|
495
|
-
);
|
|
486
|
+
const parameters = webToolParameters(inspection);
|
|
496
487
|
pi.registerTool({
|
|
497
488
|
name: `web_${capability}`,
|
|
489
|
+
namespace: {
|
|
490
|
+
name: "web",
|
|
491
|
+
description: "Search, extract pages, answer, and research the web."
|
|
492
|
+
},
|
|
498
493
|
label: `Web ${capability[0].toUpperCase()}${capability.slice(1)}`,
|
|
499
494
|
// Keep provider guidance with the tool schema, including with custom system prompts.
|
|
500
495
|
description: [
|
|
501
496
|
descriptions[capability],
|
|
502
497
|
...inspection.promptGuidelines ?? [],
|
|
503
|
-
"
|
|
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."
|
|
504
499
|
].join("\n"),
|
|
505
500
|
parameters,
|
|
501
|
+
// Let permission extensions and tool loadouts treat web access as a
|
|
502
|
+
// read-only call that reaches the open web.
|
|
503
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
504
|
+
// Programmatic callers such as codemode scripts receive the result
|
|
505
|
+
// document itself instead of parsing the rendered text.
|
|
506
|
+
outputSchema: DOCUMENT_SCHEMAS[capability],
|
|
506
507
|
prepareArguments(args) {
|
|
507
508
|
return prepareToolArguments(parameters, args);
|
|
508
509
|
},
|
|
@@ -550,6 +551,7 @@ function webExtension(pi) {
|
|
|
550
551
|
queries: params.queries,
|
|
551
552
|
maxResults: params.maxResults
|
|
552
553
|
}) : 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 });
|
|
554
|
+
const json = JSON.stringify(result);
|
|
553
555
|
const text = renderTextDocument(result);
|
|
554
556
|
const truncated = truncateHead(text);
|
|
555
557
|
let body = truncated.content;
|
|
@@ -561,7 +563,7 @@ function webExtension(pi) {
|
|
|
561
563
|
);
|
|
562
564
|
await withFileMutationQueue(
|
|
563
565
|
fullOutputPath,
|
|
564
|
-
() => writeFile(fullOutputPath,
|
|
566
|
+
() => writeFile(fullOutputPath, json, { mode: 384 })
|
|
565
567
|
);
|
|
566
568
|
body += `
|
|
567
569
|
|
|
@@ -569,8 +571,11 @@ Full results: ${fullOutputPath}`;
|
|
|
569
571
|
}
|
|
570
572
|
return {
|
|
571
573
|
content: [{ type: "text", text: body }],
|
|
574
|
+
// Pi keeps this out of the model's context and the session, so the
|
|
575
|
+
// text truncation above does not apply. Scripts get every result.
|
|
576
|
+
structuredContent: JSON.parse(json),
|
|
577
|
+
isError: result.status === "partial",
|
|
572
578
|
details: {
|
|
573
|
-
webProviderResult: true,
|
|
574
579
|
status: result.status,
|
|
575
580
|
webInputStatus: true,
|
|
576
581
|
capability,
|
|
@@ -584,17 +589,7 @@ Full results: ${fullOutputPath}`;
|
|
|
584
589
|
}
|
|
585
590
|
});
|
|
586
591
|
}
|
|
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
592
|
}
|
|
592
|
-
var descriptions = {
|
|
593
|
-
search: "Search up to ten queries and return titles, URLs, and snippets in input order.",
|
|
594
|
-
contents: "Fetch and extract readable contents from web URLs.",
|
|
595
|
-
answer: "Answer up to ten questions using web-grounded evidence.",
|
|
596
|
-
research: "Run foreground multi-step web research and return the final report."
|
|
597
|
-
};
|
|
598
593
|
export {
|
|
599
594
|
webExtension as default
|
|
600
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/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.
|
|
3
|
+
"version": "4.7.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",
|
|
@@ -98,25 +102,31 @@
|
|
|
98
102
|
"openai": "^7.10.0",
|
|
99
103
|
"parallel-web": "^1.3.3",
|
|
100
104
|
"picocolors": "^1.1.1",
|
|
101
|
-
"typebox": "^1.3.6",
|
|
102
105
|
"valyu-js": "^2.10.1",
|
|
103
106
|
"yaml": "^2.9.0"
|
|
104
107
|
},
|
|
105
108
|
"devDependencies": {
|
|
106
109
|
"@biomejs/biome": "^2.5.4",
|
|
107
|
-
"@earendil-works/
|
|
108
|
-
"@earendil-works/pi-
|
|
109
|
-
"@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",
|
|
110
115
|
"@types/node": "^26.1.1",
|
|
111
116
|
"esbuild": "^0.28.1",
|
|
112
117
|
"typescript": "^7.0.2",
|
|
113
118
|
"vitest": "^4.1.10"
|
|
114
119
|
},
|
|
115
120
|
"peerDependencies": {
|
|
121
|
+
"@earendil-works/pi-durable": "^1.0.0",
|
|
116
122
|
"@earendil-works/pi-coding-agent": "*",
|
|
117
|
-
"@earendil-works/pi-tui": "*"
|
|
123
|
+
"@earendil-works/pi-tui": "*",
|
|
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
|
},
|