webfox 0.0.0-test.0 → 4.0.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/LICENSE +21 -0
- package/README.md +256 -3
- package/assets/webfox-lockup-dark.png +0 -0
- package/assets/webfox-lockup.png +0 -0
- package/dist/adapter-2YBFCBCZ.js +161 -0
- package/dist/adapter-67TIISDW.js +753 -0
- package/dist/adapter-AB7LKV7N.js +324 -0
- package/dist/adapter-AUYLTDPM.js +73 -0
- package/dist/adapter-HITGYUXN.js +494 -0
- package/dist/adapter-HPTMLH75.js +334 -0
- package/dist/adapter-I6OUKBZQ.js +492 -0
- package/dist/adapter-ISZQTZDP.js +166 -0
- package/dist/adapter-MAG7X663.js +230 -0
- package/dist/adapter-NEXSOHGT.js +232 -0
- package/dist/adapter-PADXKW43.js +162 -0
- package/dist/adapter-SDQX2533.js +123 -0
- package/dist/adapter-UAZNLDSS.js +237 -0
- package/dist/adapter-XBAQSK7I.js +282 -0
- package/dist/application-types.d.ts +34 -0
- package/dist/application.d.ts +2 -0
- package/dist/chunk-EF4VMDZL.js +85 -0
- package/dist/chunk-G2YAQEET.js +38 -0
- package/dist/chunk-I2UIACY3.js +12 -0
- package/dist/chunk-L425U55P.js +3051 -0
- package/dist/chunk-QRHDXM2L.js +31 -0
- package/dist/chunk-SMF5ALX7.js +762 -0
- package/dist/chunk-U4BLULLV.js +52 -0
- package/dist/chunk-WCTOYZVB.js +85 -0
- package/dist/chunk-X6ZBIHZC.js +21 -0
- package/dist/chunk-YZXM4U2C.js +21 -0
- package/dist/chunk-ZVTVERW5.js +20 -0
- package/dist/cli.js +641 -0
- package/dist/config.schema.json +4270 -0
- package/dist/configuration/defaults.d.ts +3 -0
- package/dist/configuration/file.d.ts +17 -0
- package/dist/configuration/options-schema.d.ts +2 -0
- package/dist/configuration/planning.d.ts +10 -0
- package/dist/configuration/schema.d.ts +9 -0
- package/dist/configuration/types.d.ts +37 -0
- package/dist/contents.d.ts +19 -0
- package/dist/domain.d.ts +89 -0
- package/dist/errors.d.ts +17 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +38 -0
- package/dist/package-metadata.d.ts +3 -0
- package/dist/pi-keybindings.d.ts +1 -0
- package/dist/pi-params.d.ts +3 -0
- package/dist/pi-render.d.ts +50 -0
- package/dist/pi-search-render.d.ts +4 -0
- package/dist/pi-text.d.ts +4 -0
- package/dist/pi-validation.d.ts +4 -0
- package/dist/pi.d.ts +2 -0
- package/dist/pi.js +563 -0
- package/dist/providers/brave/adapter.d.ts +7 -0
- package/dist/providers/brave/definition.d.ts +1 -0
- package/dist/providers/brave/types.d.ts +4 -0
- package/dist/providers/cli-json.d.ts +9 -0
- package/dist/providers/cloudflare/adapter.d.ts +5 -0
- package/dist/providers/cloudflare/definition.d.ts +1 -0
- package/dist/providers/cloudflare/types.d.ts +4 -0
- package/dist/providers/contract.d.ts +88 -0
- package/dist/providers/custom/adapter.d.ts +9 -0
- package/dist/providers/custom/definition.d.ts +1 -0
- package/dist/providers/custom/types.d.ts +9 -0
- package/dist/providers/definition.d.ts +32 -0
- package/dist/providers/exa/adapter.d.ts +8 -0
- package/dist/providers/exa/definition.d.ts +1 -0
- package/dist/providers/exa/types.d.ts +4 -0
- package/dist/providers/firecrawl/adapter.d.ts +7 -0
- package/dist/providers/firecrawl/definition.d.ts +1 -0
- package/dist/providers/firecrawl/types.d.ts +4 -0
- package/dist/providers/gemini/adapter.d.ts +14 -0
- package/dist/providers/gemini/definition.d.ts +1 -0
- package/dist/providers/gemini/types.d.ts +3 -0
- package/dist/providers/linkup/adapter.d.ts +7 -0
- package/dist/providers/linkup/definition.d.ts +1 -0
- package/dist/providers/linkup/types.d.ts +4 -0
- package/dist/providers/metadata.d.ts +15 -0
- package/dist/providers/ollama/adapter.d.ts +6 -0
- package/dist/providers/ollama/definition.d.ts +1 -0
- package/dist/providers/ollama/types.d.ts +4 -0
- package/dist/providers/openai/adapter.d.ts +7 -0
- package/dist/providers/openai/definition.d.ts +1 -0
- package/dist/providers/openai/types.d.ts +28 -0
- package/dist/providers/parallel/adapter.d.ts +6 -0
- package/dist/providers/parallel/definition.d.ts +1 -0
- package/dist/providers/parallel/types.d.ts +4 -0
- package/dist/providers/perplexity/adapter.d.ts +7 -0
- package/dist/providers/perplexity/definition.d.ts +1 -0
- package/dist/providers/perplexity/types.d.ts +4 -0
- package/dist/providers/registry.d.ts +16 -0
- package/dist/providers/serper/adapter.d.ts +5 -0
- package/dist/providers/serper/definition.d.ts +1 -0
- package/dist/providers/serper/types.d.ts +21 -0
- package/dist/providers/shared.d.ts +4 -0
- package/dist/providers/tavily/adapter.d.ts +6 -0
- package/dist/providers/tavily/definition.d.ts +1 -0
- package/dist/providers/tavily/types.d.ts +4 -0
- package/dist/providers/valyu/adapter.d.ts +8 -0
- package/dist/providers/valyu/definition.d.ts +1 -0
- package/dist/providers/valyu/types.d.ts +4 -0
- package/dist/render.d.ts +5 -0
- package/dist/runtime/credentials.d.ts +15 -0
- package/dist/runtime/duration.d.ts +2 -0
- package/dist/runtime/execute.d.ts +20 -0
- package/dist/runtime/lifecycle.d.ts +8 -0
- package/dist/runtime/outward.d.ts +11 -0
- package/dist/runtime/polling.d.ts +13 -0
- package/dist/runtime/process.d.ts +4 -0
- package/docs/cli-experience.md +177 -0
- package/docs/provider.md +177 -0
- package/docs/reference.md +181 -0
- package/example-config.yaml +36 -0
- package/examples/custom/README.md +57 -0
- package/examples/custom/provider.mjs +54 -0
- package/examples/custom/webfox.json +18 -0
- package/package.json +122 -8
- package/index.js +0 -2
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deadline,
|
|
3
|
+
orderedMap,
|
|
4
|
+
sleep,
|
|
5
|
+
withSignal
|
|
6
|
+
} from "./chunk-WCTOYZVB.js";
|
|
7
|
+
import {
|
|
8
|
+
configurationSchema,
|
|
9
|
+
effectiveOptions,
|
|
10
|
+
optionSchema,
|
|
11
|
+
providers,
|
|
12
|
+
selectProvider,
|
|
13
|
+
validateConfiguredOptions
|
|
14
|
+
} from "./chunk-L425U55P.js";
|
|
15
|
+
import {
|
|
16
|
+
cancelProcessGroup,
|
|
17
|
+
killProcessGroup
|
|
18
|
+
} from "./chunk-ZVTVERW5.js";
|
|
19
|
+
import {
|
|
20
|
+
CAPABILITIES
|
|
21
|
+
} from "./chunk-G2YAQEET.js";
|
|
22
|
+
import {
|
|
23
|
+
WebfoxError,
|
|
24
|
+
asWebfoxError
|
|
25
|
+
} from "./chunk-U4BLULLV.js";
|
|
26
|
+
|
|
27
|
+
// src/application.ts
|
|
28
|
+
import { resolve as resolve3 } from "node:path";
|
|
29
|
+
|
|
30
|
+
// src/configuration/file.ts
|
|
31
|
+
import { readFile } from "node:fs/promises";
|
|
32
|
+
import { readFileSync } from "node:fs";
|
|
33
|
+
import { homedir } from "node:os";
|
|
34
|
+
import { join, resolve } from "node:path";
|
|
35
|
+
import { parseDocument, visit, isAlias, isMap, isScalar } from "yaml";
|
|
36
|
+
import { Check, Errors } from "typebox/value";
|
|
37
|
+
function resolveConfigPath(options = {}) {
|
|
38
|
+
const env = options.env ?? process.env;
|
|
39
|
+
const cwd = options.cwd ?? process.cwd();
|
|
40
|
+
if (options.configPath) return resolve(cwd, options.configPath);
|
|
41
|
+
if (env.WEBFOX_CONFIG) return resolve(cwd, env.WEBFOX_CONFIG);
|
|
42
|
+
if ((options.platform ?? process.platform) === "win32" && env.APPDATA)
|
|
43
|
+
return join(env.APPDATA, "webfox", "config.yaml");
|
|
44
|
+
return join(
|
|
45
|
+
env.XDG_CONFIG_HOME ?? join(options.home ?? homedir(), ".config"),
|
|
46
|
+
"webfox",
|
|
47
|
+
"config.yaml"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
async function loadConfig(options = {}) {
|
|
51
|
+
const path = resolveConfigPath(options);
|
|
52
|
+
try {
|
|
53
|
+
return parseConfig(await readFile(path, "utf8"), path);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return readFailure(error, options, path);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function loadConfigSync(options = {}) {
|
|
59
|
+
const path = resolveConfigPath(options);
|
|
60
|
+
try {
|
|
61
|
+
return parseConfig(readFileSync(path, "utf8"), path);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return readFailure(error, options, path);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function readFailure(error, options, path) {
|
|
67
|
+
if (error.code === "ENOENT" && !options.configPath && !(options.env ?? process.env).WEBFOX_CONFIG)
|
|
68
|
+
return {};
|
|
69
|
+
if (error instanceof WebfoxError) throw error;
|
|
70
|
+
throw new WebfoxError(
|
|
71
|
+
"INVALID_CONFIG",
|
|
72
|
+
`Could not read configuration: ${path}. Check the path and file permissions.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
function parseConfigDocument(text, source = "config.yaml") {
|
|
76
|
+
try {
|
|
77
|
+
const document = parseDocument(text, { version: "1.2", schema: "core" });
|
|
78
|
+
if (document.errors.length || document.warnings.length || document.directives?.yaml.version !== "1.2")
|
|
79
|
+
throw new Error("Invalid YAML 1.2");
|
|
80
|
+
visit(document, (_key, node) => {
|
|
81
|
+
if (isAlias(node) || node && typeof node === "object" && "tag" in node && node.tag)
|
|
82
|
+
throw new Error("Aliases and explicit tags are not supported");
|
|
83
|
+
if (isMap(node) && node.items.some(
|
|
84
|
+
({ key }) => !isScalar(key) || typeof key.value !== "string"
|
|
85
|
+
))
|
|
86
|
+
throw new Error("Mapping keys must be strings");
|
|
87
|
+
if (isScalar(node) && typeof node.value === "number" && !Number.isFinite(node.value))
|
|
88
|
+
throw new Error("Numbers must be finite");
|
|
89
|
+
});
|
|
90
|
+
return document;
|
|
91
|
+
} catch {
|
|
92
|
+
throw new WebfoxError(
|
|
93
|
+
"INVALID_CONFIG",
|
|
94
|
+
`Invalid YAML in ${source}. Use one YAML 1.2 document with unique keys, with string keys and finite numbers, without aliases or explicit tags.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function parseConfig(text, source = "config.yaml") {
|
|
99
|
+
const document = parseConfigDocument(text, source);
|
|
100
|
+
return validateConfig(
|
|
101
|
+
document.contents === null ? {} : document.toJS({ maxAliasCount: 0 }),
|
|
102
|
+
source
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
function validateConfig(value, source = "configuration") {
|
|
106
|
+
const schema = configurationSchema;
|
|
107
|
+
if (!Check(schema, value)) {
|
|
108
|
+
const detail = Errors(schema, value).slice(0, 3).map((error) => `${error.instancePath || "/"}: ${error.message}`).join("; ");
|
|
109
|
+
throw new WebfoxError(
|
|
110
|
+
"INVALID_CONFIG",
|
|
111
|
+
`Invalid ${source}: ${detail}. Provider options belong under providers.<id>.options.<capability>.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return structuredClone(value);
|
|
115
|
+
}
|
|
116
|
+
function redactConfig(config) {
|
|
117
|
+
const visit2 = (entry) => {
|
|
118
|
+
if (Array.isArray(entry)) return entry.map(visit2);
|
|
119
|
+
if (!entry || typeof entry !== "object") return entry;
|
|
120
|
+
if ("value" in entry) return { value: "[redacted]" };
|
|
121
|
+
if ("command" in entry) return { command: ["[redacted]"] };
|
|
122
|
+
return Object.fromEntries(
|
|
123
|
+
Object.entries(entry).map(([key, child]) => [key, visit2(child)])
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
return visit2(config);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/runtime/credentials.ts
|
|
130
|
+
import { spawn } from "node:child_process";
|
|
131
|
+
import { resolve as resolve2 } from "node:path";
|
|
132
|
+
var CredentialResolver = class {
|
|
133
|
+
constructor(cwd, env) {
|
|
134
|
+
this.cwd = cwd;
|
|
135
|
+
this.env = env;
|
|
136
|
+
}
|
|
137
|
+
cwd;
|
|
138
|
+
env;
|
|
139
|
+
cache = /* @__PURE__ */ new Map();
|
|
140
|
+
async prepare(definition, stored, capability, signal, outward) {
|
|
141
|
+
const {
|
|
142
|
+
credentials: sources,
|
|
143
|
+
commands,
|
|
144
|
+
options: _options,
|
|
145
|
+
accountId,
|
|
146
|
+
...settings
|
|
147
|
+
} = stored ?? {};
|
|
148
|
+
const credentials = {};
|
|
149
|
+
for (const requirement of definition.credentials) {
|
|
150
|
+
if (requirement.capabilities && !requirement.capabilities.includes(capability))
|
|
151
|
+
continue;
|
|
152
|
+
const source = sources?.[requirement.name] ?? {
|
|
153
|
+
env: requirement.environmentVariable
|
|
154
|
+
};
|
|
155
|
+
const value = await this.resolve(source, signal, outward);
|
|
156
|
+
if (!value && !requirement.optional) {
|
|
157
|
+
throw new WebfoxError(
|
|
158
|
+
"PROVIDER_UNAVAILABLE",
|
|
159
|
+
`${definition.label} ${capability} needs a credential. Set ${"env" in source ? source.env : `providers.${definition.id}.credentials.${requirement.name}`} and try again.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (value) credentials[requirement.name] = value;
|
|
163
|
+
}
|
|
164
|
+
const extra = {};
|
|
165
|
+
for (const [key, fallback] of Object.entries(
|
|
166
|
+
definition.credentialDefaults
|
|
167
|
+
)) {
|
|
168
|
+
const value = await this.resolve(
|
|
169
|
+
key === "accountId" ? accountId ?? fallback : fallback,
|
|
170
|
+
signal,
|
|
171
|
+
outward
|
|
172
|
+
);
|
|
173
|
+
if (!value)
|
|
174
|
+
throw new WebfoxError(
|
|
175
|
+
"PROVIDER_UNAVAILABLE",
|
|
176
|
+
`${definition.label} requires ${key}. Set ${"env" in fallback ? fallback.env : key} and try again.`
|
|
177
|
+
);
|
|
178
|
+
extra[key] = value;
|
|
179
|
+
}
|
|
180
|
+
const command = commands?.[capability];
|
|
181
|
+
if (definition.fields.includes("commands") && !command)
|
|
182
|
+
throw new WebfoxError(
|
|
183
|
+
"PROVIDER_UNAVAILABLE",
|
|
184
|
+
`Configure providers.${definition.id}.commands.${capability}.argv before executing ${capability}.`
|
|
185
|
+
);
|
|
186
|
+
const resolvedCommands = command ? {
|
|
187
|
+
[capability]: {
|
|
188
|
+
...command,
|
|
189
|
+
cwd: resolve2(this.cwd, command.cwd ?? "."),
|
|
190
|
+
env: await this.resolveMap(command.env, signal, outward)
|
|
191
|
+
}
|
|
192
|
+
} : void 0;
|
|
193
|
+
return {
|
|
194
|
+
...settings,
|
|
195
|
+
...extra,
|
|
196
|
+
credentials,
|
|
197
|
+
...resolvedCommands ? { commands: resolvedCommands } : {}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async resolveMap(sources, signal, outward) {
|
|
201
|
+
const result = {};
|
|
202
|
+
for (const [key, source] of Object.entries(sources ?? {})) {
|
|
203
|
+
const value = await this.resolve(source, signal, outward);
|
|
204
|
+
if (value !== void 0) result[key] = value;
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
async resolve(source, signal, outward) {
|
|
209
|
+
signal.throwIfAborted();
|
|
210
|
+
let value;
|
|
211
|
+
if ("value" in source) value = source.value;
|
|
212
|
+
else if ("env" in source) value = this.env[source.env];
|
|
213
|
+
else {
|
|
214
|
+
const key = JSON.stringify(source.command);
|
|
215
|
+
value = this.cache.get(key);
|
|
216
|
+
if (value === void 0) {
|
|
217
|
+
value = await credentialCommand(
|
|
218
|
+
source.command,
|
|
219
|
+
this.cwd,
|
|
220
|
+
this.env,
|
|
221
|
+
signal
|
|
222
|
+
);
|
|
223
|
+
this.cache.set(key, value);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (value) outward.addSecret(value);
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
function credentialCommand(argv, cwd, env, signal) {
|
|
231
|
+
signal.throwIfAborted();
|
|
232
|
+
return new Promise((resolvePromise, reject) => {
|
|
233
|
+
const child = spawn(argv[0], argv.slice(1), {
|
|
234
|
+
cwd,
|
|
235
|
+
env,
|
|
236
|
+
detached: process.platform !== "win32",
|
|
237
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
238
|
+
});
|
|
239
|
+
let output = "";
|
|
240
|
+
const abort = () => {
|
|
241
|
+
cancelProcessGroup(child);
|
|
242
|
+
reject(signal.reason);
|
|
243
|
+
};
|
|
244
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
245
|
+
child.stdout.setEncoding("utf8");
|
|
246
|
+
child.stdout.on("data", (chunk) => {
|
|
247
|
+
output += chunk;
|
|
248
|
+
if (Buffer.byteLength(output) > 64 * 1024) {
|
|
249
|
+
killProcessGroup(child, "SIGKILL");
|
|
250
|
+
reject(
|
|
251
|
+
new WebfoxError(
|
|
252
|
+
"INVALID_CONFIG",
|
|
253
|
+
"Credential command output exceeded 64 KiB. Return only the credential."
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
child.on(
|
|
259
|
+
"error",
|
|
260
|
+
() => reject(
|
|
261
|
+
new WebfoxError(
|
|
262
|
+
"INVALID_CONFIG",
|
|
263
|
+
"Could not start credential command. Check its executable and working directory."
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
);
|
|
267
|
+
child.on("close", (code) => {
|
|
268
|
+
signal.removeEventListener("abort", abort);
|
|
269
|
+
if (signal.aborted) reject(signal.reason);
|
|
270
|
+
else if (code !== 0)
|
|
271
|
+
reject(
|
|
272
|
+
new WebfoxError(
|
|
273
|
+
"INVALID_CONFIG",
|
|
274
|
+
`Credential command failed (exit ${code}). Check the credential command independently.`
|
|
275
|
+
)
|
|
276
|
+
);
|
|
277
|
+
else resolvePromise(output.trim());
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/runtime/outward.ts
|
|
283
|
+
var OutwardBoundary = class {
|
|
284
|
+
secrets = /* @__PURE__ */ new Set();
|
|
285
|
+
addSecret(secret) {
|
|
286
|
+
if (secret) this.secrets.add(secret);
|
|
287
|
+
}
|
|
288
|
+
text(value) {
|
|
289
|
+
return [...this.secrets].sort((a, b) => b.length - a.length).reduce((text, secret) => text.split(secret).join("[redacted]"), value);
|
|
290
|
+
}
|
|
291
|
+
value(value) {
|
|
292
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
293
|
+
const visit2 = (entry) => {
|
|
294
|
+
if (typeof entry === "string") return this.text(entry);
|
|
295
|
+
if (!entry || typeof entry !== "object") return entry;
|
|
296
|
+
if (seen.has(entry)) return "[Circular]";
|
|
297
|
+
seen.add(entry);
|
|
298
|
+
const result = Array.isArray(entry) ? entry.map(visit2) : Object.fromEntries(
|
|
299
|
+
Object.entries(entry).map(([key, child]) => [
|
|
300
|
+
this.text(key),
|
|
301
|
+
/authorization|api[-_]?key|token|credential|password|secret|headers?/i.test(
|
|
302
|
+
key
|
|
303
|
+
) ? "[redacted]" : visit2(child)
|
|
304
|
+
])
|
|
305
|
+
);
|
|
306
|
+
seen.delete(entry);
|
|
307
|
+
return result;
|
|
308
|
+
};
|
|
309
|
+
return visit2(value);
|
|
310
|
+
}
|
|
311
|
+
error(error) {
|
|
312
|
+
return this.value(asWebfoxError(error).toJSON());
|
|
313
|
+
}
|
|
314
|
+
exception(error) {
|
|
315
|
+
const safe = this.error(error);
|
|
316
|
+
return new WebfoxError(safe.code, safe.message, {
|
|
317
|
+
retryable: safe.retryable
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// src/runtime/execute.ts
|
|
323
|
+
var ExecutionRuntime = class {
|
|
324
|
+
constructor(cwd, env) {
|
|
325
|
+
this.cwd = cwd;
|
|
326
|
+
this.env = env;
|
|
327
|
+
this.credentials = new CredentialResolver(cwd, env);
|
|
328
|
+
}
|
|
329
|
+
cwd;
|
|
330
|
+
env;
|
|
331
|
+
credentials;
|
|
332
|
+
async execute(plan, request) {
|
|
333
|
+
const { capability, definition, inputs } = plan;
|
|
334
|
+
const timeoutMs = request.timeoutMs ?? (capability === "research" ? plan.policy.researchTimeoutMs ?? 18e5 : plan.policy.timeoutMs ?? 3e4);
|
|
335
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647)
|
|
336
|
+
throw new WebfoxError(
|
|
337
|
+
"INVALID_INPUT",
|
|
338
|
+
"timeoutMs must be a positive integer no larger than 2147483647."
|
|
339
|
+
);
|
|
340
|
+
const scope = deadline(timeoutMs, request.signal);
|
|
341
|
+
const outward = new OutwardBoundary();
|
|
342
|
+
let active = true;
|
|
343
|
+
const notify = (event) => {
|
|
344
|
+
if (!active) return;
|
|
345
|
+
try {
|
|
346
|
+
request.onProgress?.(outward.value(event));
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
const inputState = (input, inputIndex, state) => notify({
|
|
351
|
+
capability,
|
|
352
|
+
provider: definition.id,
|
|
353
|
+
message: `${state}: ${input}`,
|
|
354
|
+
input,
|
|
355
|
+
inputIndex,
|
|
356
|
+
state
|
|
357
|
+
});
|
|
358
|
+
const context = {
|
|
359
|
+
cwd: this.cwd,
|
|
360
|
+
env: this.env,
|
|
361
|
+
signal: scope.signal,
|
|
362
|
+
retryPolicy: {
|
|
363
|
+
retries: plan.policy.retries ?? 0,
|
|
364
|
+
delayMs: plan.policy.retryDelayMs ?? 2e3
|
|
365
|
+
},
|
|
366
|
+
onProgress: (message) => {
|
|
367
|
+
if (!scope.signal.aborted)
|
|
368
|
+
notify({ capability, provider: definition.id, message });
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
const fail = (input, error) => ({
|
|
372
|
+
input,
|
|
373
|
+
ok: false,
|
|
374
|
+
error: outward.error(scope.signal.aborted ? scope.signal.reason : error)
|
|
375
|
+
});
|
|
376
|
+
try {
|
|
377
|
+
scope.signal.throwIfAborted();
|
|
378
|
+
const config = await this.credentials.prepare(
|
|
379
|
+
definition,
|
|
380
|
+
plan.stored,
|
|
381
|
+
capability,
|
|
382
|
+
scope.signal,
|
|
383
|
+
outward
|
|
384
|
+
);
|
|
385
|
+
const adapter = await withSignal(
|
|
386
|
+
definition.load(),
|
|
387
|
+
scope.signal
|
|
388
|
+
);
|
|
389
|
+
const run = async (input) => {
|
|
390
|
+
try {
|
|
391
|
+
if (capability === "contents") {
|
|
392
|
+
const response = await this.run(
|
|
393
|
+
adapter,
|
|
394
|
+
config,
|
|
395
|
+
{
|
|
396
|
+
capability: "contents",
|
|
397
|
+
urls: [input],
|
|
398
|
+
options: plan.options
|
|
399
|
+
},
|
|
400
|
+
plan,
|
|
401
|
+
context
|
|
402
|
+
);
|
|
403
|
+
if (response.answers.length !== 1 || response.answers[0].inputIndex !== 0)
|
|
404
|
+
throw new WebfoxError(
|
|
405
|
+
"PROVIDER_FAILURE",
|
|
406
|
+
"Provider returned missing, duplicate, or invalid contents input indexes."
|
|
407
|
+
);
|
|
408
|
+
const answer = response.answers[0];
|
|
409
|
+
if (answer.error)
|
|
410
|
+
throw new WebfoxError(answer.error.code, answer.error.message, {
|
|
411
|
+
retryable: answer.error.retryable
|
|
412
|
+
});
|
|
413
|
+
const { inputIndex: _index, error: _error, ...value2 } = answer;
|
|
414
|
+
return { input, ok: true, value: value2 };
|
|
415
|
+
}
|
|
416
|
+
const operation = capability === "search" ? {
|
|
417
|
+
capability,
|
|
418
|
+
query: input,
|
|
419
|
+
maxResults: plan.maxResults,
|
|
420
|
+
options: plan.options
|
|
421
|
+
} : capability === "answer" ? { capability, query: input, options: plan.options } : { capability: "research", input, options: plan.options };
|
|
422
|
+
const result = await this.run(
|
|
423
|
+
adapter,
|
|
424
|
+
config,
|
|
425
|
+
operation,
|
|
426
|
+
plan,
|
|
427
|
+
context
|
|
428
|
+
);
|
|
429
|
+
const value = capability === "search" ? {
|
|
430
|
+
results: result.results.slice(
|
|
431
|
+
0,
|
|
432
|
+
plan.maxResults
|
|
433
|
+
)
|
|
434
|
+
} : textValue(result);
|
|
435
|
+
return { input, ok: true, value };
|
|
436
|
+
} catch (error) {
|
|
437
|
+
return fail(input, error);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
inputs.forEach((input, index) => inputState(input, index, "queued"));
|
|
441
|
+
const results = await orderedMap(
|
|
442
|
+
inputs,
|
|
443
|
+
plan.policy.concurrency ?? 4,
|
|
444
|
+
async (input, inputIndex) => {
|
|
445
|
+
if (!scope.signal.aborted) inputState(input, inputIndex, "running");
|
|
446
|
+
const result = await run(input);
|
|
447
|
+
inputState(
|
|
448
|
+
input,
|
|
449
|
+
inputIndex,
|
|
450
|
+
result.ok ? "done" : result.error.code === "CANCELLED" ? "cancelled" : "failed"
|
|
451
|
+
);
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
return outward.value({
|
|
456
|
+
schemaVersion: 1,
|
|
457
|
+
capability,
|
|
458
|
+
provider: definition.id,
|
|
459
|
+
status: results.every((result) => result.ok) ? "ok" : "partial",
|
|
460
|
+
results
|
|
461
|
+
});
|
|
462
|
+
} catch (error) {
|
|
463
|
+
if (scope.signal.aborted)
|
|
464
|
+
return outward.value({
|
|
465
|
+
schemaVersion: 1,
|
|
466
|
+
capability,
|
|
467
|
+
provider: definition.id,
|
|
468
|
+
status: "partial",
|
|
469
|
+
results: inputs.map((input) => fail(input, error))
|
|
470
|
+
});
|
|
471
|
+
throw outward.exception(error);
|
|
472
|
+
} finally {
|
|
473
|
+
active = false;
|
|
474
|
+
scope.dispose();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async run(adapter, config, request, plan, context) {
|
|
478
|
+
const execute = adapter[request.capability];
|
|
479
|
+
if (!execute)
|
|
480
|
+
throw new WebfoxError(
|
|
481
|
+
"PROVIDER_UNAVAILABLE",
|
|
482
|
+
`Provider does not implement ${request.capability}.`
|
|
483
|
+
);
|
|
484
|
+
const retries = plan.definition.capabilities[request.capability]?.retrySafe ? context.retryPolicy?.retries ?? 0 : 0;
|
|
485
|
+
for (let attempt = 0; ; attempt++) {
|
|
486
|
+
context.signal.throwIfAborted();
|
|
487
|
+
try {
|
|
488
|
+
const result = await withSignal(
|
|
489
|
+
execute(request, config, context),
|
|
490
|
+
context.signal
|
|
491
|
+
);
|
|
492
|
+
if (request.capability === "contents") {
|
|
493
|
+
const page = result.answers[0];
|
|
494
|
+
if (page?.error)
|
|
495
|
+
throw new WebfoxError(page.error.code, page.error.message, {
|
|
496
|
+
retryable: page.error.retryable
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
return result;
|
|
500
|
+
} catch (error) {
|
|
501
|
+
if (context.signal.aborted) throw context.signal.reason;
|
|
502
|
+
const normalized = asWebfoxError(error);
|
|
503
|
+
if (!normalized.options.retryable || attempt >= retries)
|
|
504
|
+
throw normalized;
|
|
505
|
+
const delay = Math.min(
|
|
506
|
+
(context.retryPolicy?.delayMs ?? 2e3) * 2 ** attempt,
|
|
507
|
+
3e4
|
|
508
|
+
);
|
|
509
|
+
context.onProgress?.(
|
|
510
|
+
`Retrying ${request.capability} in ${delay}ms (attempt ${attempt + 2}).`
|
|
511
|
+
);
|
|
512
|
+
await sleep(delay, context.signal);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
function textValue(value) {
|
|
518
|
+
return {
|
|
519
|
+
text: value.text,
|
|
520
|
+
...value.itemCount === void 0 ? {} : { itemCount: value.itemCount },
|
|
521
|
+
...value.metadata === void 0 ? {} : { metadata: value.metadata }
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/application.ts
|
|
526
|
+
function createWebfox(options = {}) {
|
|
527
|
+
const cwd = resolve3(options.cwd ?? process.cwd());
|
|
528
|
+
const env = { ...options.env ?? process.env };
|
|
529
|
+
const config = options.config ? validateConfig(structuredClone(options.config), "createWebfox config") : loadConfigSync({ configPath: options.configPath, env, cwd });
|
|
530
|
+
validateConfiguredOptions(config);
|
|
531
|
+
const runtime = new ExecutionRuntime(cwd, env);
|
|
532
|
+
const outward = new OutwardBoundary();
|
|
533
|
+
const collect = (value) => {
|
|
534
|
+
if (!value || typeof value !== "object") return;
|
|
535
|
+
if ("value" in value && typeof value.value === "string")
|
|
536
|
+
outward.addSecret(value.value);
|
|
537
|
+
if ("env" in value && typeof value.env === "string" && env[value.env])
|
|
538
|
+
outward.addSecret(env[value.env]);
|
|
539
|
+
for (const child of Object.values(value)) collect(child);
|
|
540
|
+
};
|
|
541
|
+
collect(config.providers);
|
|
542
|
+
for (const definition of Object.values(providers))
|
|
543
|
+
for (const credential of definition.credentials)
|
|
544
|
+
if (env[credential.environmentVariable])
|
|
545
|
+
outward.addSecret(env[credential.environmentVariable]);
|
|
546
|
+
const configured = (definition, capability) => {
|
|
547
|
+
const stored = config.providers?.[definition.id];
|
|
548
|
+
if (!definition.capabilities[capability]) return false;
|
|
549
|
+
if (definition.fields.includes("commands"))
|
|
550
|
+
return !!stored?.commands?.[capability];
|
|
551
|
+
if (definition.credentials.every((c) => c.optional))
|
|
552
|
+
return !!stored || definition.credentials.some((c) => !!env[c.environmentVariable]);
|
|
553
|
+
const present = (source) => !!source && typeof source === "object" && ("env" in source ? !!env[String(source.env)] : true);
|
|
554
|
+
return definition.credentials.filter(
|
|
555
|
+
(c) => !c.optional && (!c.capabilities || c.capabilities.includes(capability))
|
|
556
|
+
).every(
|
|
557
|
+
(c) => present(
|
|
558
|
+
stored?.credentials?.[c.name] ?? { env: c.environmentVariable }
|
|
559
|
+
)
|
|
560
|
+
) && Object.entries(definition.credentialDefaults).every(
|
|
561
|
+
([key, source]) => present(key === "accountId" ? stored?.accountId ?? source : source)
|
|
562
|
+
);
|
|
563
|
+
};
|
|
564
|
+
const inspectProvider = (definition) => ({
|
|
565
|
+
id: definition.id,
|
|
566
|
+
label: definition.label,
|
|
567
|
+
docsUrl: definition.docsUrl,
|
|
568
|
+
local: definition.local,
|
|
569
|
+
configurationRequirements: Object.fromEntries(
|
|
570
|
+
Object.entries(definition.credentialDefaults).flatMap(
|
|
571
|
+
([key, source]) => "env" in source ? [[key, source.env]] : []
|
|
572
|
+
)
|
|
573
|
+
),
|
|
574
|
+
capabilities: CAPABILITIES.filter((c) => !!definition.capabilities[c]),
|
|
575
|
+
credentials: structuredClone(definition.credentials),
|
|
576
|
+
configured: CAPABILITIES.filter((c) => configured(definition, c)),
|
|
577
|
+
selectedDefaults: CAPABILITIES.filter(
|
|
578
|
+
(c) => config.defaults?.[c]?.provider === definition.id
|
|
579
|
+
)
|
|
580
|
+
});
|
|
581
|
+
const execute = (capability, inputs, request, maxResults) => {
|
|
582
|
+
const definition = selectProvider(config, capability, request.provider);
|
|
583
|
+
const options2 = effectiveOptions(
|
|
584
|
+
config,
|
|
585
|
+
definition,
|
|
586
|
+
capability,
|
|
587
|
+
request.options
|
|
588
|
+
);
|
|
589
|
+
return runtime.execute(
|
|
590
|
+
{
|
|
591
|
+
capability,
|
|
592
|
+
definition,
|
|
593
|
+
inputs,
|
|
594
|
+
options: options2,
|
|
595
|
+
maxResults,
|
|
596
|
+
stored: config.providers?.[definition.id],
|
|
597
|
+
policy: config.execution ?? {}
|
|
598
|
+
},
|
|
599
|
+
request
|
|
600
|
+
);
|
|
601
|
+
};
|
|
602
|
+
return {
|
|
603
|
+
async search(request) {
|
|
604
|
+
const inputs = batch(request.queries, "queries", 10);
|
|
605
|
+
const maxResults = request.maxResults ?? config.defaults?.search?.maxResults ?? 5;
|
|
606
|
+
if (!Number.isSafeInteger(maxResults) || maxResults < 1)
|
|
607
|
+
throw new WebfoxError(
|
|
608
|
+
"INVALID_INPUT",
|
|
609
|
+
"maxResults must be a positive integer."
|
|
610
|
+
);
|
|
611
|
+
return execute("search", inputs, request, maxResults);
|
|
612
|
+
},
|
|
613
|
+
async contents(request) {
|
|
614
|
+
const urls = batch(request.urls, "urls");
|
|
615
|
+
for (const url of urls) {
|
|
616
|
+
try {
|
|
617
|
+
if (!["http:", "https:"].includes(new URL(url).protocol))
|
|
618
|
+
throw new Error();
|
|
619
|
+
} catch {
|
|
620
|
+
throw new WebfoxError(
|
|
621
|
+
"INVALID_INPUT",
|
|
622
|
+
"contents requires HTTP or HTTPS URLs."
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return execute("contents", urls, request);
|
|
627
|
+
},
|
|
628
|
+
async answer(request) {
|
|
629
|
+
return execute("answer", batch(request.queries, "queries", 10), request);
|
|
630
|
+
},
|
|
631
|
+
async research(request) {
|
|
632
|
+
return execute("research", batch([request.input], "input", 1), request);
|
|
633
|
+
},
|
|
634
|
+
listProviders: () => Object.values(providers).map(inspectProvider),
|
|
635
|
+
getProvider: (id) => Object.hasOwn(providers, id) ? inspectProvider(providers[id]) : void 0,
|
|
636
|
+
inspectCapability(capability, requested) {
|
|
637
|
+
const selected = requested ?? config.defaults?.[capability]?.provider;
|
|
638
|
+
if (!selected)
|
|
639
|
+
return {
|
|
640
|
+
capability,
|
|
641
|
+
configured: false,
|
|
642
|
+
defaults: {
|
|
643
|
+
options: {},
|
|
644
|
+
...capability === "search" ? { maxResults: config.defaults?.search?.maxResults ?? 5 } : {}
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
const definition = selectProvider(config, capability, selected);
|
|
648
|
+
return outward.value({
|
|
649
|
+
capability,
|
|
650
|
+
provider: selected,
|
|
651
|
+
configured: configured(definition, capability),
|
|
652
|
+
optionSchema: optionSchema(
|
|
653
|
+
definition,
|
|
654
|
+
capability,
|
|
655
|
+
"defaults"
|
|
656
|
+
),
|
|
657
|
+
defaults: {
|
|
658
|
+
options: effectiveOptions(
|
|
659
|
+
config,
|
|
660
|
+
definition,
|
|
661
|
+
capability,
|
|
662
|
+
{},
|
|
663
|
+
"defaults"
|
|
664
|
+
),
|
|
665
|
+
...capability === "search" ? { maxResults: config.defaults?.search?.maxResults ?? 5 } : {}
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function batch(values, name, max = Infinity) {
|
|
672
|
+
if (!Array.isArray(values) || values.length === 0 || values.length > max)
|
|
673
|
+
throw new WebfoxError(
|
|
674
|
+
"INVALID_INPUT",
|
|
675
|
+
`${name} requires ${max === Infinity ? "one or more inputs" : `between 1 and ${max} inputs`}.`
|
|
676
|
+
);
|
|
677
|
+
return values.map((value) => {
|
|
678
|
+
if (typeof value !== "string" || !value.trim())
|
|
679
|
+
throw new WebfoxError(
|
|
680
|
+
"INVALID_INPUT",
|
|
681
|
+
`${name} must contain non-empty strings.`
|
|
682
|
+
);
|
|
683
|
+
return value.trim();
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/configuration/defaults.ts
|
|
688
|
+
import { readFile as readFile2, mkdir, writeFile, rename, rm } from "node:fs/promises";
|
|
689
|
+
import { dirname } from "node:path";
|
|
690
|
+
import { randomUUID } from "node:crypto";
|
|
691
|
+
var updates = /* @__PURE__ */ new Map();
|
|
692
|
+
async function setCapabilityDefault(capability, provider, options = {}) {
|
|
693
|
+
if (!CAPABILITIES.includes(capability))
|
|
694
|
+
throw new WebfoxError(
|
|
695
|
+
"INVALID_INPUT",
|
|
696
|
+
`Unknown capability '${capability}'. Choose ${CAPABILITIES.join(", ")}.`
|
|
697
|
+
);
|
|
698
|
+
try {
|
|
699
|
+
selectProvider({}, capability, provider);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
throw new WebfoxError(
|
|
702
|
+
"INVALID_INPUT",
|
|
703
|
+
error instanceof WebfoxError ? error.message : "Invalid provider selection."
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
const path = resolveConfigPath(options);
|
|
707
|
+
const previous = updates.get(path) ?? Promise.resolve();
|
|
708
|
+
const operation = previous.catch(() => {
|
|
709
|
+
}).then(async () => {
|
|
710
|
+
let config = {};
|
|
711
|
+
let text = "";
|
|
712
|
+
try {
|
|
713
|
+
text = await readFile2(path, "utf8");
|
|
714
|
+
config = parseConfig(text, path);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
if (error.code !== "ENOENT") throw error;
|
|
717
|
+
}
|
|
718
|
+
config.defaults = {
|
|
719
|
+
...config.defaults,
|
|
720
|
+
[capability]: { ...config.defaults?.[capability], provider }
|
|
721
|
+
};
|
|
722
|
+
validateConfiguredOptions(config);
|
|
723
|
+
const document = parseConfigDocument(text, path);
|
|
724
|
+
document.setIn(["defaults", capability, "provider"], provider);
|
|
725
|
+
const serialized = document.toString();
|
|
726
|
+
parseConfig(serialized, path);
|
|
727
|
+
await mkdir(dirname(path), { recursive: true });
|
|
728
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
729
|
+
try {
|
|
730
|
+
await writeFile(temporary, serialized, {
|
|
731
|
+
mode: 384,
|
|
732
|
+
flag: "wx"
|
|
733
|
+
});
|
|
734
|
+
await rename(temporary, path);
|
|
735
|
+
} finally {
|
|
736
|
+
await rm(temporary, { force: true });
|
|
737
|
+
}
|
|
738
|
+
return path;
|
|
739
|
+
});
|
|
740
|
+
updates.set(path, operation);
|
|
741
|
+
try {
|
|
742
|
+
return await operation;
|
|
743
|
+
} catch (error) {
|
|
744
|
+
if (error instanceof WebfoxError) throw error;
|
|
745
|
+
throw new WebfoxError(
|
|
746
|
+
"INVALID_CONFIG",
|
|
747
|
+
`Could not save provider default to ${path}. Check the path and file permissions.`
|
|
748
|
+
);
|
|
749
|
+
} finally {
|
|
750
|
+
if (updates.get(path) === operation) updates.delete(path);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export {
|
|
755
|
+
resolveConfigPath,
|
|
756
|
+
loadConfig,
|
|
757
|
+
parseConfig,
|
|
758
|
+
validateConfig,
|
|
759
|
+
redactConfig,
|
|
760
|
+
createWebfox,
|
|
761
|
+
setCapabilityDefault
|
|
762
|
+
};
|