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
package/dist/cli.js ADDED
@@ -0,0 +1,641 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ formatDuration
4
+ } from "./chunk-I2UIACY3.js";
5
+ import {
6
+ renderTextDocument
7
+ } from "./chunk-QRHDXM2L.js";
8
+ import {
9
+ createWebfox,
10
+ loadConfig,
11
+ redactConfig,
12
+ resolveConfigPath,
13
+ setCapabilityDefault
14
+ } from "./chunk-MVCCEPDF.js";
15
+ import "./chunk-WCTOYZVB.js";
16
+ import {
17
+ PACKAGE_VERSION,
18
+ validateConfiguredOptions
19
+ } from "./chunk-GAWPRXAU.js";
20
+ import "./chunk-ZVTVERW5.js";
21
+ import {
22
+ CAPABILITIES,
23
+ PROVIDER_IDS
24
+ } from "./chunk-G2YAQEET.js";
25
+ import {
26
+ WebfoxError
27
+ } from "./chunk-U4BLULLV.js";
28
+
29
+ // src/cli.ts
30
+ import { realpathSync } from "node:fs";
31
+ import { readFile } from "node:fs/promises";
32
+ import { resolve } from "node:path";
33
+ import { pathToFileURL } from "node:url";
34
+ import {
35
+ Command,
36
+ CommanderError,
37
+ InvalidArgumentError as InvalidArgumentError2,
38
+ Option
39
+ } from "commander";
40
+ import pc from "picocolors";
41
+ import { stringify } from "yaml";
42
+
43
+ // src/cli/diagnostics.ts
44
+ function createDiagnostics(stderr, colors) {
45
+ const write = (glyph, message) => stderr.write(`${glyph} ${message.trimEnd()}
46
+ `);
47
+ return {
48
+ success: (message) => write(colors.green("\u2714\uFE0E"), message),
49
+ progress: (message) => write(colors.yellow("\u25B6\uFE0E"), message),
50
+ error(error, input) {
51
+ const glyph = error.code === "CANCELLED" ? colors.dim("\u25A0") : colors.red("\u2718\uFE0E");
52
+ write(
53
+ glyph,
54
+ `${input === void 0 ? "" : `${input}: `}${error.code}: ${error.message}`
55
+ );
56
+ }
57
+ };
58
+ }
59
+
60
+ // src/cli/flags.ts
61
+ import { InvalidArgumentError } from "commander";
62
+ var RESERVED_FLAGS = /* @__PURE__ */ new Set([
63
+ "provider",
64
+ "config",
65
+ "cwd",
66
+ "timeout",
67
+ "format",
68
+ "options-json",
69
+ "quiet",
70
+ "help",
71
+ "max-results",
72
+ "version",
73
+ "color",
74
+ "no-color"
75
+ ]);
76
+ function buildOptionFlags(schema) {
77
+ const candidates = [];
78
+ walkSchema(schema, [], candidates);
79
+ const counts = /* @__PURE__ */ new Map();
80
+ for (const flag of candidates) {
81
+ for (const name of [flag.flag, flag.negativeFlag].filter(
82
+ (name2) => !!name2
83
+ ))
84
+ counts.set(name, (counts.get(name) ?? 0) + 1);
85
+ }
86
+ return candidates.filter(
87
+ (entry) => counts.get(entry.flag) === 1 && (!entry.negativeFlag || counts.get(entry.negativeFlag) === 1) && !entry.flag.startsWith("--no-") && !RESERVED_FLAGS.has(entry.flag.slice(2))
88
+ );
89
+ }
90
+ function walkSchema(schema, path, output) {
91
+ const properties = schema.properties;
92
+ if (properties && typeof properties === "object" && !Array.isArray(properties)) {
93
+ for (const [name, child] of Object.entries(properties)) {
94
+ const childSchema = child;
95
+ const next = [...path, name];
96
+ if (childSchema.type === "object" && childSchema.properties) {
97
+ walkSchema(childSchema, next, output);
98
+ continue;
99
+ }
100
+ const descriptor = flagForSchema(childSchema, next);
101
+ if (descriptor) output.push(descriptor);
102
+ }
103
+ }
104
+ }
105
+ function flagForSchema(schema, path) {
106
+ const flag = `--${path.map(kebab).join("-")}`;
107
+ const enumValues = readEnum(schema);
108
+ const kind = schema.type ?? inferEnumKind(enumValues);
109
+ if (["string", "number", "integer", "boolean"].includes(kind)) {
110
+ return {
111
+ flag,
112
+ ...kind === "boolean" ? { negativeFlag: `--no-${flag.slice(2)}` } : {},
113
+ path,
114
+ kind,
115
+ ...enumValues ? { enumValues } : {},
116
+ ...schema.description ? { description: schema.description } : {}
117
+ };
118
+ }
119
+ if (kind === "array" && schema.items && ["string", "number", "integer", "boolean"].includes(
120
+ schema.items.type ?? inferEnumKind(readEnum(schema.items))
121
+ )) {
122
+ const itemKind = schema.items.type ?? inferEnumKind(readEnum(schema.items));
123
+ return {
124
+ flag,
125
+ path,
126
+ kind: "array",
127
+ itemKind,
128
+ ...readEnum(schema.items) ? { enumValues: readEnum(schema.items) } : {},
129
+ ...schema.description ? { description: schema.description } : {}
130
+ };
131
+ }
132
+ return void 0;
133
+ }
134
+ function readEnum(schema) {
135
+ if (Array.isArray(schema.enum)) return schema.enum;
136
+ if (Array.isArray(schema.anyOf) && schema.anyOf.every((entry) => "const" in entry)) {
137
+ return schema.anyOf.map((entry) => entry.const);
138
+ }
139
+ return void 0;
140
+ }
141
+ function inferEnumKind(values) {
142
+ const kind = typeof values?.[0];
143
+ return kind === "string" || kind === "number" || kind === "boolean" ? kind : void 0;
144
+ }
145
+ function parseTypedValue(raw, descriptor) {
146
+ const itemKind = descriptor.kind === "array" ? inferArrayKind(descriptor) : descriptor.kind;
147
+ let value = raw;
148
+ if (itemKind === "number" || itemKind === "integer") {
149
+ value = Number(raw);
150
+ if (!Number.isFinite(value) || itemKind === "integer" && !Number.isInteger(value))
151
+ throw new InvalidArgumentError(
152
+ `${descriptor.flag} requires a ${itemKind}`
153
+ );
154
+ } else if (itemKind === "boolean") {
155
+ if (raw !== "true" && raw !== "false")
156
+ throw new InvalidArgumentError(
157
+ `${descriptor.flag} requires true or false`
158
+ );
159
+ value = raw === "true";
160
+ }
161
+ if (descriptor.enumValues && !descriptor.enumValues.includes(value)) {
162
+ throw new InvalidArgumentError(
163
+ `${descriptor.flag} must be one of: ${descriptor.enumValues.join(", ")}`
164
+ );
165
+ }
166
+ return value;
167
+ }
168
+ function inferArrayKind(descriptor) {
169
+ if (descriptor.itemKind) return descriptor.itemKind;
170
+ if (descriptor.enumValues?.length) {
171
+ const type = typeof descriptor.enumValues[0];
172
+ if (type === "number") return "number";
173
+ if (type === "boolean") return "boolean";
174
+ }
175
+ return "string";
176
+ }
177
+ function setPath(target, path, value) {
178
+ let cursor = target;
179
+ for (const part of path.slice(0, -1)) {
180
+ const existing = cursor[part];
181
+ cursor[part] = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
182
+ cursor = cursor[part];
183
+ }
184
+ cursor[path.at(-1)] = value;
185
+ }
186
+ function kebab(value) {
187
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
188
+ }
189
+
190
+ // src/cli.ts
191
+ var summaries = {
192
+ search: "Search the public web",
193
+ contents: "Extract readable pages",
194
+ answer: "Answer questions using web sources",
195
+ research: "Run research in the foreground"
196
+ };
197
+ async function runCli(argv, io = {
198
+ stdin: process.stdin,
199
+ stdout: process.stdout,
200
+ stderr: process.stderr,
201
+ env: process.env,
202
+ cwd: process.cwd()
203
+ }) {
204
+ const beforeSeparator = argv.slice(
205
+ 0,
206
+ argv.indexOf("--") < 0 ? argv.length : argv.indexOf("--")
207
+ );
208
+ const colors = pc.createColors(
209
+ Boolean(io.stderr.isTTY) && !("NO_COLOR" in io.env) && !beforeSeparator.includes("--no-color")
210
+ );
211
+ const helpColors = pc.createColors(
212
+ Boolean(io.stdout.isTTY) && !("NO_COLOR" in io.env) && !beforeSeparator.includes("--no-color")
213
+ );
214
+ const diagnostics = createDiagnostics(io.stderr, colors);
215
+ const controller = new AbortController();
216
+ const abort = () => controller.abort(new WebfoxError("CANCELLED", "Operation cancelled."));
217
+ const signals = io.signalSource ?? process;
218
+ signals.once("SIGINT", abort);
219
+ signals.once("SIGTERM", abort);
220
+ let exitCode = 0;
221
+ const output = (command) => command.exitOverride().configureHelp({
222
+ styleTitle: helpColors.bold,
223
+ styleCommandText: helpColors.cyan,
224
+ styleSubcommandText: helpColors.cyan,
225
+ styleOptionText: helpColors.cyan,
226
+ styleArgumentText: helpColors.yellow
227
+ }).configureOutput({
228
+ getOutHasColors: () => helpColors.isColorSupported,
229
+ getErrHasColors: () => colors.isColorSupported,
230
+ writeOut: (value) => io.stdout.write(value),
231
+ writeErr: (value) => io.stderr.write(value),
232
+ outputError: (value) => diagnostics.error({
233
+ code: "INVALID_INPUT",
234
+ message: value.replace(/^error: /, "")
235
+ })
236
+ });
237
+ try {
238
+ while (argv[0] === "--no-color") argv = argv.slice(1);
239
+ if (argv[0] === "help" && CAPABILITIES.includes(argv[1]))
240
+ argv = [argv[1], "--help", ...argv.slice(2)];
241
+ const root = output(new Command("web")).description(
242
+ "Webfox: search, extract pages, answer questions, and research with your chosen provider."
243
+ ).version(PACKAGE_VERSION, "--version").helpCommand(false);
244
+ root.addHelpText(
245
+ "after",
246
+ [
247
+ `
248
+ ${helpColors.bold("Examples:")}`,
249
+ ` ${helpColors.cyan("web search")} ${helpColors.yellow('"Node.js release notes"')} ${helpColors.cyan("--provider")} ${helpColors.yellow("brave")}`,
250
+ ` ${helpColors.cyan("web config default")} ${helpColors.yellow("search brave")}`,
251
+ ` ${helpColors.cyan("web search --help")}`,
252
+ ` ${helpColors.cyan("web search --provider")} ${helpColors.yellow("brave")} ${helpColors.cyan("--help")}`
253
+ ].join("\n")
254
+ );
255
+ for (const capability of CAPABILITIES) {
256
+ const command = root.command(capability).description(summaries[capability]).argument(
257
+ capability === "research" ? "[brief]" : capability === "contents" ? "[urls...]" : "[queries...]"
258
+ ).allowExcessArguments(false).helpOption(false);
259
+ addControls(command, capability);
260
+ command.addOption(new Option("-h, --help", "Show help"));
261
+ if (argv[0] !== capability) continue;
262
+ const route = output(new Command()).allowUnknownOption(true).helpOption(false);
263
+ addControls(route, capability);
264
+ route.addOption(new Option("-h, --help"));
265
+ route.parseOptions(argv.slice(1));
266
+ const controls = route.opts();
267
+ const cwd = resolve(io.cwd, controls.cwd ?? ".");
268
+ const help = controls.help;
269
+ const client = help && !controls.provider ? void 0 : createWebfox({
270
+ configPath: controls.config && resolve(io.cwd, controls.config),
271
+ cwd,
272
+ env: io.env
273
+ });
274
+ const inspection = client?.inspectCapability(
275
+ capability,
276
+ controls.provider
277
+ );
278
+ const flags = inspection?.optionSchema ? buildOptionFlags(inspection.optionSchema) : [];
279
+ for (const flag of flags)
280
+ addProviderOption(command, flag, !controls.provider && !!help);
281
+ command.addHelpText("after", capabilityHelp(capability, helpColors));
282
+ if (help) {
283
+ command.action(() => command.outputHelp());
284
+ } else
285
+ command.action(async function() {
286
+ const parsed = this.opts();
287
+ const values = this.processedArgs.flatMap(
288
+ (value) => Array.isArray(value) ? value : value === void 0 ? [] : [String(value)]
289
+ );
290
+ const inputs = await readInputs(
291
+ capability,
292
+ values,
293
+ io.stdin,
294
+ controller.signal
295
+ );
296
+ const providerOptions = await readOptions(parsed.optionsJson, cwd);
297
+ for (const flag of flags) {
298
+ const option = this.options.find(
299
+ (option2) => option2.long === flag.flag
300
+ );
301
+ if (this.getOptionValueSource(option.attributeName()) === "cli")
302
+ setPath(
303
+ providerOptions,
304
+ flag.path,
305
+ this.getOptionValue(option.attributeName())
306
+ );
307
+ }
308
+ const request = {
309
+ provider: inspection?.provider,
310
+ options: providerOptions,
311
+ timeoutMs: parsed.timeout,
312
+ signal: controller.signal,
313
+ onProgress: parsed.quiet ? void 0 : (event) => {
314
+ if (!event.state) diagnostics.progress(event.message);
315
+ }
316
+ };
317
+ const startedAt = Date.now();
318
+ const result = capability === "search" ? await client.search({
319
+ ...request,
320
+ queries: inputs,
321
+ maxResults: parsed.maxResults
322
+ }) : capability === "contents" ? await client.contents({ ...request, urls: inputs }) : capability === "answer" ? await client.answer({ ...request, queries: inputs }) : await client.research({ ...request, input: inputs[0] });
323
+ for (const entry of result.results) {
324
+ if (!entry.ok) diagnostics.error(entry.error, entry.input);
325
+ else if (!parsed.quiet)
326
+ diagnostics.success(
327
+ capability === "research" ? `Research via ${client.getProvider(result.provider).label} completed in ${formatDuration(Date.now() - startedAt)}.` : entry.input
328
+ );
329
+ }
330
+ const rendered = parsed.format === "json" ? JSON.stringify(result) : renderTextDocument(result, { includeErrors: false });
331
+ if (rendered) io.stdout.write(`${rendered}
332
+ `);
333
+ exitCode = result.results.some(
334
+ (entry) => !entry.ok && entry.error.code === "CANCELLED"
335
+ ) ? 130 : result.status === "partial" ? 1 : 0;
336
+ });
337
+ }
338
+ root.command("providers").description(
339
+ "Inspect supported, configured, and selected capabilities (not verified)"
340
+ ).argument("[id]").option("--config <path>", "Advanced: read this configuration file").action((id, controls) => {
341
+ const client = createWebfox({
342
+ configPath: controls.config && resolve(io.cwd, controls.config),
343
+ cwd: io.cwd,
344
+ env: io.env
345
+ });
346
+ const entries = id ? [client.getProvider(parseProvider(id))] : client.listProviders().filter(
347
+ (entry) => entry.id !== "custom" || entry.configured.length > 0 || entry.selectedDefaults.length > 0
348
+ );
349
+ const rows = [
350
+ ["", "Provider", ...CAPABILITIES],
351
+ ...entries.map((entry) => [
352
+ entry.configured.length > 0 ? "\u2605" : "\u2606",
353
+ entry.id,
354
+ ...CAPABILITIES.map(
355
+ (capability) => entry.selectedDefaults.includes(capability) ? "\u25C9" : (entry.id === "custom" ? entry.configured : entry.capabilities).includes(capability) ? "\u2714\uFE0E" : "\u2718\uFE0E"
356
+ )
357
+ ])
358
+ ];
359
+ const width = (value) => value.replaceAll("\uFE0E", "").length;
360
+ const widths = rows[0].map(
361
+ (_, index) => Math.max(...rows.map((row) => width(row[index])))
362
+ );
363
+ io.stdout.write(
364
+ rows.map(
365
+ (row, rowIndex) => row.map((value, i) => {
366
+ const left = rowIndex > 0 && i > 1 ? Math.floor((widths[i] - width(value)) / 2) : 0;
367
+ const padded = " ".repeat(left) + value + (i === row.length - 1 ? "" : " ".repeat(widths[i] - left - width(value)));
368
+ if (rowIndex === 0) return helpColors.bold(padded);
369
+ return padded.replace("\u2714\uFE0E", helpColors.green("\u2714\uFE0E")).replace("\u2718\uFE0E", helpColors.dim("\u2718\uFE0E")).replace("\u25C9", helpColors.cyan("\u25C9")).replace("\u2605", helpColors.yellow("\u2605")).replace("\u2606", helpColors.dim("\u2606"));
370
+ }).join(" ").trimEnd()
371
+ ).join("\n") + "\n\n\u2606 Unconfigured \u2605 Configured \u2714\uFE0E Supported \u2718\uFE0E Unsupported \u25C9 Selected default\n"
372
+ );
373
+ if (id) {
374
+ const entry = entries[0];
375
+ if (entry.id === "custom") {
376
+ io.stdout.write(
377
+ "\nConfigure providers.custom.commands.<capability>.argv with the executable and arguments for each capability. Only configured commands are marked supported; \u25C9 marks selection independently.\n"
378
+ );
379
+ }
380
+ io.stdout.write(
381
+ `
382
+ ${entry.docsUrl}
383
+ ${entry.credentials.map((c) => `${c.name}: ${c.environmentVariable}${c.optional ? " (optional)" : ""}`).join("\n")}
384
+ `
385
+ );
386
+ for (const [key, env] of Object.entries(
387
+ entry.configurationRequirements
388
+ ))
389
+ io.stdout.write(`${key}: ${env}
390
+ `);
391
+ for (const capability of entry.capabilities)
392
+ io.stdout.write(
393
+ `
394
+ ${capability} defaults: ${JSON.stringify(client.inspectCapability(capability, entry.id).defaults)}
395
+ Options: web ${capability} --provider ${entry.id} --help
396
+ `
397
+ );
398
+ }
399
+ });
400
+ const config = root.command("config").description("Save provider choices or inspect YAML configuration");
401
+ config.command("default").description("Save one capability\u2019s provider choice").argument("<capability>").argument("<provider>").option("--config <path>", "Advanced: update this configuration file").action(
402
+ async (capability, provider, controls) => {
403
+ await setCapabilityDefault(
404
+ capability,
405
+ parseProvider(provider),
406
+ { configPath: controls.config, cwd: io.cwd, env: io.env }
407
+ );
408
+ diagnostics.success(`Saved ${capability} default: ${provider}`);
409
+ }
410
+ );
411
+ for (const action of ["path", "show", "validate"])
412
+ config.command(action).description(
413
+ {
414
+ path: "Print the configuration path",
415
+ show: "Show configuration with secrets redacted",
416
+ validate: "Validate without credentials or network requests"
417
+ }[action]
418
+ ).option("--config <path>", "Use this configuration file").action(async (controls) => {
419
+ const options = {
420
+ configPath: controls.config,
421
+ cwd: io.cwd,
422
+ env: io.env
423
+ };
424
+ if (action === "path")
425
+ io.stdout.write(`${resolveConfigPath(options)}
426
+ `);
427
+ else {
428
+ const value = await loadConfig(options);
429
+ if (action === "show")
430
+ io.stdout.write(stringify(redactConfig(value)));
431
+ else {
432
+ validateConfiguredOptions(value);
433
+ diagnostics.success(
434
+ "Configuration is valid. Credentials and connectivity have not been verified."
435
+ );
436
+ }
437
+ }
438
+ });
439
+ config.action(() => config.outputHelp());
440
+ if (!argv.length) {
441
+ root.outputHelp();
442
+ return 0;
443
+ }
444
+ await root.parseAsync(argv, { from: "user" });
445
+ return exitCode;
446
+ } catch (error) {
447
+ if (error instanceof CommanderError && !(error instanceof InvalidArgumentError2))
448
+ return error.exitCode === 0 ? 0 : 2;
449
+ const normalized = error instanceof WebfoxError ? error : new WebfoxError(
450
+ "INVALID_INPUT",
451
+ error instanceof Error ? error.message : String(error)
452
+ );
453
+ diagnostics.error(normalized);
454
+ return normalized.code === "CANCELLED" ? 130 : ["INVALID_INPUT", "INVALID_CONFIG"].includes(normalized.code) ? 2 : 1;
455
+ } finally {
456
+ signals.removeListener("SIGINT", abort);
457
+ signals.removeListener("SIGTERM", abort);
458
+ }
459
+ }
460
+ function addControls(command, capability) {
461
+ command.addOption(
462
+ new Option(
463
+ "--provider <id>",
464
+ `Override the saved ${capability} provider`
465
+ ).argParser(parseProvider)
466
+ );
467
+ if (capability === "search")
468
+ command.addOption(
469
+ new Option("--max-results <n>", "Maximum results per query").argParser(
470
+ (value) => positiveInteger(value, "--max-results")
471
+ )
472
+ );
473
+ command.addOption(
474
+ new Option("--format <text|json>", "Result format (text even when piped)").choices(["text", "json"]).default("text")
475
+ );
476
+ command.addOption(
477
+ new Option(
478
+ "--timeout <duration>",
479
+ "Overall deadline including retries (30s, 20m)"
480
+ ).argParser(parseDuration)
481
+ );
482
+ command.option("--quiet", "Suppress progress and success notices on stderr");
483
+ command.addOption(new Option("--no-color", "Disable terminal colors"));
484
+ command.addOption(
485
+ new Option("--config <path>", "Read this YAML configuration file")
486
+ );
487
+ command.addOption(
488
+ new Option(
489
+ "--cwd <path>",
490
+ "Working directory for custom providers and option files"
491
+ )
492
+ );
493
+ command.addOption(
494
+ new Option(
495
+ "--options-json <json|@file>",
496
+ "Complex provider options; typed flags take precedence"
497
+ )
498
+ );
499
+ }
500
+ function addProviderOption(command, flag, hidden) {
501
+ const description = `${flag.description ?? flag.path.join(".")}${flag.enumValues ? ` (${flag.enumValues.join(" | ")})` : ""}`;
502
+ const option = new Option(
503
+ flag.kind === "boolean" ? flag.flag : `${flag.flag} <value>`,
504
+ description
505
+ ).helpGroup("Provider options").hideHelp(hidden);
506
+ if (flag.kind === "array")
507
+ option.argParser((value, previous = []) => [
508
+ ...previous,
509
+ parseTypedValue(value, flag)
510
+ ]);
511
+ else if (flag.kind !== "boolean")
512
+ option.argParser((value) => parseTypedValue(value, flag));
513
+ command.addOption(option);
514
+ if (flag.negativeFlag)
515
+ command.addOption(
516
+ new Option(flag.negativeFlag, `Disable ${flag.path.join(".")}`).helpGroup("Provider options").hideHelp(hidden)
517
+ );
518
+ }
519
+ function capabilityHelp(capability, colors) {
520
+ const examples = {
521
+ search: {
522
+ input: '"Node.js cancellation" "Bun cancellation"',
523
+ provider: "brave"
524
+ },
525
+ contents: { input: "https://example.com", provider: "tavily" },
526
+ answer: { input: '"What is MCP?" "What is A2A?"', provider: "openai" },
527
+ research: { input: '"Compare databases"', provider: "gemini" }
528
+ };
529
+ const { input, provider } = examples[capability];
530
+ const command = colors.cyan(`web ${capability}`);
531
+ const providerFlag = `${colors.cyan("--provider")} ${colors.yellow(provider)}`;
532
+ return [
533
+ "",
534
+ capability === "contents" ? "Omit URLs to read newline-separated URLs from piped or redirected stdin." : capability === "research" ? "Provide exactly one brief, or omit it to read piped or redirected stdin." : "Quote each independent input (up to ten), or omit inputs to read piped or redirected stdin.",
535
+ "Results preserve input order. Progress and errors go to stderr.",
536
+ "",
537
+ colors.bold("Examples:"),
538
+ ` ${command} ${colors.yellow(input)} ${providerFlag}${capability === "research" ? ` ${colors.cyan("--timeout")} ${colors.yellow("20m")}` : ""}`,
539
+ ` ${command} ${providerFlag} ${colors.cyan("--help")}`
540
+ ].join("\n");
541
+ }
542
+ function parseDuration(value) {
543
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/.exec(value);
544
+ const ms = match ? Number(match[1]) * { ms: 1, s: 1e3, m: 6e4, h: 36e5 }[match[2]] : NaN;
545
+ if (!Number.isSafeInteger(ms) || ms < 1 || ms > 2147483647)
546
+ throw new InvalidArgumentError2(
547
+ "Use a positive duration with ms, s, m, or h (for example 30s or 20m)."
548
+ );
549
+ return ms;
550
+ }
551
+ function positiveInteger(value, flag) {
552
+ const parsed = Number(value);
553
+ if (!Number.isSafeInteger(parsed) || parsed < 1)
554
+ throw new InvalidArgumentError2(`${flag} requires a positive integer`);
555
+ return parsed;
556
+ }
557
+ function parseProvider(value) {
558
+ if (!PROVIDER_IDS.includes(value))
559
+ throw new InvalidArgumentError2(
560
+ `Unknown provider '${value}'. See web providers.`
561
+ );
562
+ return value;
563
+ }
564
+ async function readInputs(capability, inputs, stdin, signal) {
565
+ if (!inputs.length && stdin.isTTY || capability === "research" && inputs.length > 1)
566
+ throw new WebfoxError(
567
+ "INVALID_INPUT",
568
+ `${capability} requires ${capability === "research" ? "exactly one brief" : "one or more inputs"}. Supply arguments or pipe input to stdin.`
569
+ );
570
+ if (inputs.length && !inputs.includes("-")) return inputs;
571
+ if (inputs.includes("-") && inputs.length !== 1)
572
+ throw new WebfoxError("INVALID_INPUT", "Use '-' by itself for stdin.");
573
+ const text = await new Promise((resolvePromise, reject) => {
574
+ let text2 = "";
575
+ const cleanup = () => {
576
+ stdin.removeListener("data", data);
577
+ stdin.removeListener("end", end);
578
+ stdin.removeListener("error", error);
579
+ signal.removeEventListener("abort", abort);
580
+ };
581
+ const data = (chunk) => {
582
+ text2 += chunk;
583
+ };
584
+ const end = () => {
585
+ cleanup();
586
+ resolvePromise(text2.trim());
587
+ };
588
+ const error = (error2) => {
589
+ cleanup();
590
+ reject(error2);
591
+ };
592
+ const abort = () => {
593
+ cleanup();
594
+ stdin.pause();
595
+ reject(signal.reason);
596
+ };
597
+ if (signal.aborted) {
598
+ abort();
599
+ return;
600
+ }
601
+ if (stdin.readableEnded) {
602
+ end();
603
+ return;
604
+ }
605
+ stdin.setEncoding?.("utf8");
606
+ stdin.on("data", data);
607
+ stdin.once("end", end);
608
+ stdin.once("error", error);
609
+ signal.addEventListener("abort", abort, { once: true });
610
+ });
611
+ if (!text)
612
+ throw new WebfoxError(
613
+ "INVALID_INPUT",
614
+ "Stdin must contain non-empty input."
615
+ );
616
+ return capability === "contents" ? text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [text];
617
+ }
618
+ async function readOptions(value, cwd) {
619
+ if (!value) return {};
620
+ try {
621
+ const parsed = JSON.parse(
622
+ value.startsWith("@") ? await readFile(resolve(cwd, value.slice(1)), "utf8") : value
623
+ );
624
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
625
+ throw new Error();
626
+ return parsed;
627
+ } catch {
628
+ throw new WebfoxError(
629
+ "INVALID_INPUT",
630
+ "--options-json must contain a JSON object or name a readable @file."
631
+ );
632
+ }
633
+ }
634
+ var invokedPath = process.argv[1] ? pathToFileURL(realpathSync(resolve(process.argv[1]))).href : void 0;
635
+ if (invokedPath === import.meta.url)
636
+ process.exitCode = await runCli(process.argv.slice(2));
637
+ export {
638
+ buildOptionFlags,
639
+ parseDuration,
640
+ runCli
641
+ };