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.
- 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-GAWPRXAU.js +3051 -0
- package/dist/chunk-I2UIACY3.js +12 -0
- package/dist/chunk-MVCCEPDF.js +762 -0
- package/dist/chunk-QRHDXM2L.js +31 -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
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
|
+
};
|