import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import type { Command } from "commander"; import { resolveAgentDir } from "../../agents/agent-scope.js"; import { getRuntimeConfig } from "../../config/config.js"; import { defaultRuntime } from "../../runtime.js"; import { isWebFetchProviderConfigured, listWebFetchProviders, resolveWebFetchDefinition, } from "../../web-fetch/runtime.js"; import { isWebSearchProviderConfigured, listWebSearchProviders, runWebSearch, } from "../../web-search/runtime.js"; import { runCommandWithRuntime } from "../cli-utils.js"; import { getCapabilityWebFetchCommandSecretTargets, getCapabilityWebSearchCommandSecretTargets, } from "../command-secret-targets.js"; import { exitCliAfterOutput } from "../one-shot-exit.js"; import type { CapabilityEnvelope } from "./metadata.js"; import { emitJsonOrText, formatEnvelopeForText } from "./output.js"; import { parseOptionalPositiveInteger, registerLocalProvidersCommand, resolveLocalCapabilityRuntimeConfig, } from "./shared.js"; function describeWebResultFailure(result: Record): string | undefined { const statusCode = typeof result.statusCode === "number" && Number.isFinite(result.statusCode) ? result.statusCode : undefined; const error = result.error; const errorMessage = typeof error === "string" ? error : error && typeof error === "object" && typeof (error as { message?: unknown }).message === "string" ? (error as { message: string }).message : undefined; if (result.ok !== false && (statusCode === undefined || statusCode < 400) && !errorMessage) { return undefined; } return ( errorMessage ?? (statusCode ? `provider returned status ${statusCode}` : "provider reported failure") ); } async function runWebSearchCommand(params: { query: string; provider?: string; limit?: number }) { const rawConfig = getRuntimeConfig(); const scopedTargets = getCapabilityWebSearchCommandSecretTargets(rawConfig, { providerId: params.provider, }); const cfg = await resolveLocalCapabilityRuntimeConfig({ commandName: "infer web search", ...scopedTargets, config: rawConfig, }); const result = await runWebSearch({ config: cfg, providerId: params.provider, args: { query: params.query, count: params.limit, limit: params.limit, }, }); const error = describeWebResultFailure(result.result); return { ok: error === undefined, capability: "web.search", transport: "local" as const, provider: result.provider, attempts: [], outputs: [{ result: result.result }], ...(error ? { error } : {}), } satisfies CapabilityEnvelope; } async function runWebFetchCommand(params: { url: string; provider?: string; format?: string }) { const rawConfig = getRuntimeConfig(); const scopedTargets = getCapabilityWebFetchCommandSecretTargets(rawConfig, { providerId: params.provider, }); const cfg = await resolveLocalCapabilityRuntimeConfig({ commandName: "infer web fetch", ...scopedTargets, config: rawConfig, }); const resolved = resolveWebFetchDefinition({ config: cfg, providerId: params.provider, }); if (!resolved) { throw new Error("web.fetch is disabled or no provider is available."); } const result = await resolved.definition.execute({ url: params.url, format: params.format, }); const error = describeWebResultFailure(result); return { ok: error === undefined, capability: "web.fetch", transport: "local" as const, provider: resolved.provider.id, attempts: [], outputs: [{ result }], ...(error ? { error } : {}), } satisfies CapabilityEnvelope; } export function registerWebCapabilityCommands(capability: Command): void { const web = capability.command("web").description("Web capabilities"); web .command("search") .description("Run web search") .requiredOption("--query ", "Search query") .option("--provider ", "Provider id") .option("--limit ", "Result limit") .option("--json", "Output JSON", false) .action(async (opts) => { await runCommandWithRuntime(defaultRuntime, async () => { const result = await runWebSearchCommand({ query: String(opts.query), provider: opts.provider as string | undefined, limit: parseOptionalPositiveInteger(opts.limit, "--limit"), }); emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText); if (!result.ok) { exitCliAfterOutput(defaultRuntime, 1); } }); }); web .command("fetch") .description("Fetch one URL") .requiredOption("--url ", "URL") .option("--provider ", "Provider id") .option("--format ", "Format hint") .option("--json", "Output JSON", false) .action(async (opts) => { await runCommandWithRuntime(defaultRuntime, async () => { const result = await runWebFetchCommand({ url: String(opts.url), provider: opts.provider as string | undefined, format: opts.format as string | undefined, }); emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText); if (!result.ok) { exitCliAfterOutput(defaultRuntime, 1); } }); }); registerLocalProvidersCommand( web, "List web providers", (cfg, agentId) => { const agentDir = resolveAgentDir(cfg, agentId); const selectedSearchProvider = typeof cfg.tools?.web?.search?.provider === "string" ? normalizeLowercaseStringOrEmpty(cfg.tools.web.search.provider) : ""; const selectedFetchProvider = typeof cfg.tools?.web?.fetch?.provider === "string" ? normalizeLowercaseStringOrEmpty(cfg.tools.web.fetch.provider) : ""; return { search: listWebSearchProviders({ config: cfg }).map((provider) => ({ available: true, configured: isWebSearchProviderConfigured({ provider, config: cfg, agentDir }), selected: provider.id === selectedSearchProvider, id: provider.id, envVars: provider.envVars, })), fetch: listWebFetchProviders({ config: cfg }).map((provider) => ({ available: true, configured: isWebFetchProviderConfigured({ provider, config: cfg }), selected: provider.id === selectedFetchProvider, id: provider.id, envVars: provider.envVars, })), }; }, (value) => JSON.stringify(value, null, 2), ); }