webfox 0.0.0-test.0 → 4.0.1

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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +256 -3
  3. package/assets/webfox-lockup-dark.png +0 -0
  4. package/assets/webfox-lockup.png +0 -0
  5. package/dist/adapter-2YBFCBCZ.js +161 -0
  6. package/dist/adapter-67TIISDW.js +753 -0
  7. package/dist/adapter-AB7LKV7N.js +324 -0
  8. package/dist/adapter-AUYLTDPM.js +73 -0
  9. package/dist/adapter-HITGYUXN.js +494 -0
  10. package/dist/adapter-HPTMLH75.js +334 -0
  11. package/dist/adapter-I6OUKBZQ.js +492 -0
  12. package/dist/adapter-ISZQTZDP.js +166 -0
  13. package/dist/adapter-MAG7X663.js +230 -0
  14. package/dist/adapter-NEXSOHGT.js +232 -0
  15. package/dist/adapter-PADXKW43.js +162 -0
  16. package/dist/adapter-SDQX2533.js +123 -0
  17. package/dist/adapter-UAZNLDSS.js +237 -0
  18. package/dist/adapter-XBAQSK7I.js +282 -0
  19. package/dist/application-types.d.ts +34 -0
  20. package/dist/application.d.ts +2 -0
  21. package/dist/chunk-EF4VMDZL.js +85 -0
  22. package/dist/chunk-G2YAQEET.js +38 -0
  23. package/dist/chunk-GAWPRXAU.js +3051 -0
  24. package/dist/chunk-I2UIACY3.js +12 -0
  25. package/dist/chunk-MVCCEPDF.js +762 -0
  26. package/dist/chunk-QRHDXM2L.js +31 -0
  27. package/dist/chunk-U4BLULLV.js +52 -0
  28. package/dist/chunk-WCTOYZVB.js +85 -0
  29. package/dist/chunk-X6ZBIHZC.js +21 -0
  30. package/dist/chunk-YZXM4U2C.js +21 -0
  31. package/dist/chunk-ZVTVERW5.js +20 -0
  32. package/dist/cli.js +641 -0
  33. package/dist/config.schema.json +4270 -0
  34. package/dist/configuration/defaults.d.ts +3 -0
  35. package/dist/configuration/file.d.ts +17 -0
  36. package/dist/configuration/options-schema.d.ts +2 -0
  37. package/dist/configuration/planning.d.ts +10 -0
  38. package/dist/configuration/schema.d.ts +9 -0
  39. package/dist/configuration/types.d.ts +37 -0
  40. package/dist/contents.d.ts +19 -0
  41. package/dist/domain.d.ts +89 -0
  42. package/dist/errors.d.ts +17 -0
  43. package/dist/index.d.ts +8 -0
  44. package/dist/index.js +38 -0
  45. package/dist/package-metadata.d.ts +3 -0
  46. package/dist/pi-keybindings.d.ts +1 -0
  47. package/dist/pi-params.d.ts +3 -0
  48. package/dist/pi-render.d.ts +50 -0
  49. package/dist/pi-search-render.d.ts +4 -0
  50. package/dist/pi-text.d.ts +4 -0
  51. package/dist/pi-validation.d.ts +4 -0
  52. package/dist/pi.d.ts +2 -0
  53. package/dist/pi.js +563 -0
  54. package/dist/providers/brave/adapter.d.ts +7 -0
  55. package/dist/providers/brave/definition.d.ts +1 -0
  56. package/dist/providers/brave/types.d.ts +4 -0
  57. package/dist/providers/cli-json.d.ts +9 -0
  58. package/dist/providers/cloudflare/adapter.d.ts +5 -0
  59. package/dist/providers/cloudflare/definition.d.ts +1 -0
  60. package/dist/providers/cloudflare/types.d.ts +4 -0
  61. package/dist/providers/contract.d.ts +88 -0
  62. package/dist/providers/custom/adapter.d.ts +9 -0
  63. package/dist/providers/custom/definition.d.ts +1 -0
  64. package/dist/providers/custom/types.d.ts +9 -0
  65. package/dist/providers/definition.d.ts +32 -0
  66. package/dist/providers/exa/adapter.d.ts +8 -0
  67. package/dist/providers/exa/definition.d.ts +1 -0
  68. package/dist/providers/exa/types.d.ts +4 -0
  69. package/dist/providers/firecrawl/adapter.d.ts +7 -0
  70. package/dist/providers/firecrawl/definition.d.ts +1 -0
  71. package/dist/providers/firecrawl/types.d.ts +4 -0
  72. package/dist/providers/gemini/adapter.d.ts +14 -0
  73. package/dist/providers/gemini/definition.d.ts +1 -0
  74. package/dist/providers/gemini/types.d.ts +3 -0
  75. package/dist/providers/linkup/adapter.d.ts +7 -0
  76. package/dist/providers/linkup/definition.d.ts +1 -0
  77. package/dist/providers/linkup/types.d.ts +4 -0
  78. package/dist/providers/metadata.d.ts +15 -0
  79. package/dist/providers/ollama/adapter.d.ts +6 -0
  80. package/dist/providers/ollama/definition.d.ts +1 -0
  81. package/dist/providers/ollama/types.d.ts +4 -0
  82. package/dist/providers/openai/adapter.d.ts +7 -0
  83. package/dist/providers/openai/definition.d.ts +1 -0
  84. package/dist/providers/openai/types.d.ts +28 -0
  85. package/dist/providers/parallel/adapter.d.ts +6 -0
  86. package/dist/providers/parallel/definition.d.ts +1 -0
  87. package/dist/providers/parallel/types.d.ts +4 -0
  88. package/dist/providers/perplexity/adapter.d.ts +7 -0
  89. package/dist/providers/perplexity/definition.d.ts +1 -0
  90. package/dist/providers/perplexity/types.d.ts +4 -0
  91. package/dist/providers/registry.d.ts +16 -0
  92. package/dist/providers/serper/adapter.d.ts +5 -0
  93. package/dist/providers/serper/definition.d.ts +1 -0
  94. package/dist/providers/serper/types.d.ts +21 -0
  95. package/dist/providers/shared.d.ts +4 -0
  96. package/dist/providers/tavily/adapter.d.ts +6 -0
  97. package/dist/providers/tavily/definition.d.ts +1 -0
  98. package/dist/providers/tavily/types.d.ts +4 -0
  99. package/dist/providers/valyu/adapter.d.ts +8 -0
  100. package/dist/providers/valyu/definition.d.ts +1 -0
  101. package/dist/providers/valyu/types.d.ts +4 -0
  102. package/dist/render.d.ts +5 -0
  103. package/dist/runtime/credentials.d.ts +15 -0
  104. package/dist/runtime/duration.d.ts +2 -0
  105. package/dist/runtime/execute.d.ts +20 -0
  106. package/dist/runtime/lifecycle.d.ts +8 -0
  107. package/dist/runtime/outward.d.ts +11 -0
  108. package/dist/runtime/polling.d.ts +13 -0
  109. package/dist/runtime/process.d.ts +4 -0
  110. package/docs/cli-experience.md +177 -0
  111. package/docs/provider.md +177 -0
  112. package/docs/reference.md +181 -0
  113. package/example-config.yaml +36 -0
  114. package/examples/custom/README.md +57 -0
  115. package/examples/custom/provider.mjs +54 -0
  116. package/examples/custom/webfox.json +18 -0
  117. package/package.json +122 -8
  118. 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-GAWPRXAU.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
+ };