import { formatCliCommand } from "../cli/command-format.js"; import type { OpenClawConfig } from "../config/config.js"; import { readConfigFileSnapshot, resolveGatewayPort, writeConfigFile } from "../config/config.js"; import { logConfigUpdated } from "../config/logging.js"; import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { note } from "../terminal/note.js"; import { resolveUserPath } from "../utils.js"; import { createClackPrompter } from "../wizard/clack-prompter.js"; import { WizardCancelledError } from "../wizard/prompts.js"; import { t } from "../wizard/i18n.js"; import { removeChannelConfigWizard } from "./configure.channels.js"; import { maybeInstallDaemon } from "./configure.daemon.js"; import { promptGatewayConfig } from "./configure.gateway.js"; import { promptAuthConfig } from "./configure.gateway-auth.js"; import type { ChannelsWizardMode, ConfigureWizardParams, WizardSection, } from "./configure.shared.js"; import { CONFIGURE_SECTION_OPTIONS, confirm, intro, outro, select, text, } from "./configure.shared.js"; import { healthCommand } from "./health.js"; import { formatHealthCheckFailure } from "./health-format.js"; import { noteChannelStatus, setupChannels } from "./onboard-channels.js"; import { applyWizardMetadata, DEFAULT_WORKSPACE, ensureWorkspaceAndSessions, guardCancel, printWizardHeader, probeGatewayReachable, resolveControlUiLinks, summarizeExistingConfig, waitForGatewayReachable, } from "./onboard-helpers.js"; import { promptRemoteGatewayConfig } from "./onboard-remote.js"; import { setupSkills } from "./onboard-skills.js"; type ConfigureSectionChoice = WizardSection | "__continue"; async function promptConfigureSection( runtime: RuntimeEnv, hasSelection: boolean, ): Promise { return guardCancel( await select({ message: t("configure.sections.title"), options: [ ...CONFIGURE_SECTION_OPTIONS, { value: "__continue", label: t("configure.sections.continue"), hint: hasSelection ? t("configure.sections.continueHint") : t("configure.sections.skipHint"), }, ], initialValue: CONFIGURE_SECTION_OPTIONS[0]?.value, }), runtime, ); } async function promptChannelMode(runtime: RuntimeEnv): Promise { return guardCancel( await select({ message: t("configure.sections.channels"), options: [ { value: "configure", label: t("configure.channels.configure"), hint: t("configure.channels.configureHint"), }, { value: "remove", label: t("configure.channels.remove"), hint: t("configure.channels.removeHint"), }, ], initialValue: "configure", }), runtime, ) as ChannelsWizardMode; } async function promptWebToolsConfig( nextConfig: OpenClawConfig, runtime: RuntimeEnv, ): Promise { const existingSearch = nextConfig.tools?.web?.search; const existingFetch = nextConfig.tools?.web?.fetch; const hasSearchKey = Boolean(existingSearch?.apiKey); note( [ "Web search lets your agent look things up online using the `web_search` tool.", "It requires a Brave Search API key (you can store it in the config or set BRAVE_API_KEY in the Gateway environment).", "Docs: https://docs.openclaw.ai/tools/web", ].join("\n"), "Web search", ); const enableSearch = guardCancel( await confirm({ message: t("configure.web.enableSearch"), initialValue: existingSearch?.enabled ?? hasSearchKey, }), runtime, ); let nextSearch = { ...existingSearch, enabled: enableSearch, }; if (enableSearch) { const keyInput = guardCancel( await text({ message: hasSearchKey ? t("configure.web.keyPrompt") : t("configure.web.keyPromptEmpty"), placeholder: hasSearchKey ? t("configure.web.placeholderKey") : t("configure.web.placeholderKeyEmpty"), }), runtime, ); const key = String(keyInput ?? "").trim(); if (key) { nextSearch = { ...nextSearch, apiKey: key }; } else if (!hasSearchKey) { note( t("configure.web.noKeyWarning"), t("configure.sections.web"), ); } } const enableFetch = guardCancel( await confirm({ message: t("configure.web.enableFetch"), initialValue: existingFetch?.enabled ?? true, }), runtime, ); const nextFetch = { ...existingFetch, enabled: enableFetch, }; return { ...nextConfig, tools: { ...nextConfig.tools, web: { ...nextConfig.tools?.web, search: nextSearch, fetch: nextFetch, }, }, }; } export async function runConfigureWizard( opts: ConfigureWizardParams, runtime: RuntimeEnv = defaultRuntime, ) { try { printWizardHeader(runtime); intro(opts.command === "update" ? t("configure.updateTitle") : t("configure.title")); const prompter = createClackPrompter(); const snapshot = await readConfigFileSnapshot(); const baseConfig: OpenClawConfig = snapshot.valid ? snapshot.config : {}; if (snapshot.exists) { const title = snapshot.valid ? "Existing config detected" : "Invalid config"; note(summarizeExistingConfig(baseConfig), title); if (!snapshot.valid && snapshot.issues.length > 0) { note( [ ...snapshot.issues.map((iss) => `- ${iss.path}: ${iss.message}`), "", "Docs: https://docs.openclaw.ai/gateway/configuration", ].join("\n"), "Config issues", ); } if (!snapshot.valid) { outro( `Config invalid. Run \`${formatCliCommand("openclaw doctor")}\` to repair it, then re-run configure.`, ); runtime.exit(1); return; } } const localUrl = "ws://127.0.0.1:18789"; const localProbe = await probeGatewayReachable({ url: localUrl, token: baseConfig.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN, password: baseConfig.gateway?.auth?.password ?? process.env.OPENCLAW_GATEWAY_PASSWORD, }); const remoteUrl = baseConfig.gateway?.remote?.url?.trim() ?? ""; const remoteProbe = remoteUrl ? await probeGatewayReachable({ url: remoteUrl, token: baseConfig.gateway?.remote?.token, }) : null; const mode = guardCancel( await select({ message: t("configure.gateway.modeSelect"), options: [ { value: "local", label: t("configure.gateway.local"), hint: localProbe.ok ? t("configure.gateway.localHintReachable", { url: localUrl }) : t("configure.gateway.localHintMissing", { url: localUrl }), }, { value: "remote", label: t("configure.gateway.remote"), hint: !remoteUrl ? t("configure.gateway.remoteHintNoUrl") : remoteProbe?.ok ? t("configure.gateway.remoteHintReachable", { url: remoteUrl }) : t("configure.gateway.remoteHintConfigured", { url: remoteUrl }), }, ], }), runtime, ) as "local" | "remote"; if (mode === "remote") { let remoteConfig = await promptRemoteGatewayConfig(baseConfig, prompter); remoteConfig = applyWizardMetadata(remoteConfig, { command: opts.command, mode, }); await writeConfigFile(remoteConfig); logConfigUpdated(runtime); outro(t("configure.gateway.remoteConfigured")); return; } let nextConfig = { ...baseConfig }; let didSetGatewayMode = false; if (nextConfig.gateway?.mode !== "local") { nextConfig = { ...nextConfig, gateway: { ...nextConfig.gateway, mode: "local", }, }; didSetGatewayMode = true; } let workspaceDir = nextConfig.agents?.defaults?.workspace ?? baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE; let gatewayPort = resolveGatewayPort(baseConfig); let gatewayToken: string | undefined = nextConfig.gateway?.auth?.token ?? baseConfig.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN; const persistConfig = async () => { nextConfig = applyWizardMetadata(nextConfig, { command: opts.command, mode, }); await writeConfigFile(nextConfig); logConfigUpdated(runtime); }; if (opts.sections) { const selected = opts.sections; if (!selected || selected.length === 0) { outro(t("configure.gateway.noChanges")); return; } if (selected.includes("workspace")) { const workspaceInput = guardCancel( await text({ message: "Workspace directory", initialValue: workspaceDir, }), runtime, ); workspaceDir = resolveUserPath(String(workspaceInput ?? "").trim() || DEFAULT_WORKSPACE); nextConfig = { ...nextConfig, agents: { ...nextConfig.agents, defaults: { ...nextConfig.agents?.defaults, workspace: workspaceDir, }, }, }; await ensureWorkspaceAndSessions(workspaceDir, runtime); } if (selected.includes("model")) { nextConfig = await promptAuthConfig(nextConfig, runtime, prompter); } if (selected.includes("web")) { nextConfig = await promptWebToolsConfig(nextConfig, runtime); } if (selected.includes("gateway")) { const gateway = await promptGatewayConfig(nextConfig, runtime); nextConfig = gateway.config; gatewayPort = gateway.port; gatewayToken = gateway.token; } if (selected.includes("channels")) { await noteChannelStatus({ cfg: nextConfig, prompter }); const channelMode = await promptChannelMode(runtime); if (channelMode === "configure") { nextConfig = await setupChannels(nextConfig, runtime, prompter, { allowDisable: true, allowSignalInstall: true, skipConfirm: true, skipStatusNote: true, }); } else { nextConfig = await removeChannelConfigWizard(nextConfig, runtime); } } if (selected.includes("skills")) { const wsDir = resolveUserPath(workspaceDir); nextConfig = await setupSkills(nextConfig, wsDir, runtime, prompter); } await persistConfig(); if (selected.includes("daemon")) { if (!selected.includes("gateway")) { const portInput = guardCancel( await text({ message: "Gateway port for service install", initialValue: String(gatewayPort), validate: (value) => (Number.isFinite(Number(value)) ? undefined : "Invalid port"), }), runtime, ); gatewayPort = Number.parseInt(String(portInput), 10); } await maybeInstallDaemon({ runtime, port: gatewayPort, gatewayToken }); } if (selected.includes("health")) { const localLinks = resolveControlUiLinks({ bind: nextConfig.gateway?.bind ?? "loopback", port: gatewayPort, customBindHost: nextConfig.gateway?.customBindHost, basePath: undefined, }); const remoteUrl = nextConfig.gateway?.remote?.url?.trim(); const wsUrl = nextConfig.gateway?.mode === "remote" && remoteUrl ? remoteUrl : localLinks.wsUrl; const token = nextConfig.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN; const password = nextConfig.gateway?.auth?.password ?? process.env.OPENCLAW_GATEWAY_PASSWORD; await waitForGatewayReachable({ url: wsUrl, token, password, deadlineMs: 15_000, }); try { await healthCommand({ json: false, timeoutMs: 10_000 }, runtime); } catch (err) { runtime.error(formatHealthCheckFailure(err)); note( [ "Docs:", "https://docs.openclaw.ai/gateway/health", "https://docs.openclaw.ai/gateway/troubleshooting", ].join("\n"), "Health check help", ); } } } else { let ranSection = false; let didConfigureGateway = false; while (true) { const choice = await promptConfigureSection(runtime, ranSection); if (choice === "__continue") break; ranSection = true; if (choice === "workspace") { const workspaceInput = guardCancel( await text({ message: "Workspace directory", initialValue: workspaceDir, }), runtime, ); workspaceDir = resolveUserPath(String(workspaceInput ?? "").trim() || DEFAULT_WORKSPACE); nextConfig = { ...nextConfig, agents: { ...nextConfig.agents, defaults: { ...nextConfig.agents?.defaults, workspace: workspaceDir, }, }, }; await ensureWorkspaceAndSessions(workspaceDir, runtime); await persistConfig(); } if (choice === "model") { nextConfig = await promptAuthConfig(nextConfig, runtime, prompter); await persistConfig(); } if (choice === "web") { nextConfig = await promptWebToolsConfig(nextConfig, runtime); await persistConfig(); } if (choice === "gateway") { const gateway = await promptGatewayConfig(nextConfig, runtime); nextConfig = gateway.config; gatewayPort = gateway.port; gatewayToken = gateway.token; didConfigureGateway = true; await persistConfig(); } if (choice === "channels") { await noteChannelStatus({ cfg: nextConfig, prompter }); const channelMode = await promptChannelMode(runtime); if (channelMode === "configure") { nextConfig = await setupChannels(nextConfig, runtime, prompter, { allowDisable: true, allowSignalInstall: true, skipConfirm: true, skipStatusNote: true, }); } else { nextConfig = await removeChannelConfigWizard(nextConfig, runtime); } await persistConfig(); } if (choice === "skills") { const wsDir = resolveUserPath(workspaceDir); nextConfig = await setupSkills(nextConfig, wsDir, runtime, prompter); await persistConfig(); } if (choice === "daemon") { if (!didConfigureGateway) { const portInput = guardCancel( await text({ message: "Gateway port for service install", initialValue: String(gatewayPort), validate: (value) => (Number.isFinite(Number(value)) ? undefined : "Invalid port"), }), runtime, ); gatewayPort = Number.parseInt(String(portInput), 10); } await maybeInstallDaemon({ runtime, port: gatewayPort, gatewayToken, }); } if (choice === "health") { const localLinks = resolveControlUiLinks({ bind: nextConfig.gateway?.bind ?? "loopback", port: gatewayPort, customBindHost: nextConfig.gateway?.customBindHost, basePath: undefined, }); const remoteUrl = nextConfig.gateway?.remote?.url?.trim(); const wsUrl = nextConfig.gateway?.mode === "remote" && remoteUrl ? remoteUrl : localLinks.wsUrl; const token = nextConfig.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN; const password = nextConfig.gateway?.auth?.password ?? process.env.OPENCLAW_GATEWAY_PASSWORD; await waitForGatewayReachable({ url: wsUrl, token, password, deadlineMs: 15_000, }); try { await healthCommand({ json: false, timeoutMs: 10_000 }, runtime); } catch (err) { runtime.error(formatHealthCheckFailure(err)); note( [ "Docs:", "https://docs.openclaw.ai/gateway/health", "https://docs.openclaw.ai/gateway/troubleshooting", ].join("\n"), "Health check help", ); } } } if (!ranSection) { if (didSetGatewayMode) { await persistConfig(); outro(t("configure.gateway.modeSetLocal")); return; } outro(t("configure.gateway.noChanges")); return; } } const controlUiAssets = await ensureControlUiAssetsBuilt(runtime); if (!controlUiAssets.ok && controlUiAssets.message) { runtime.error(controlUiAssets.message); } const bind = nextConfig.gateway?.bind ?? "loopback"; const links = resolveControlUiLinks({ bind, port: gatewayPort, customBindHost: nextConfig.gateway?.customBindHost, basePath: nextConfig.gateway?.controlUi?.basePath, }); // Try both new and old passwords since gateway may still have old config. const newPassword = nextConfig.gateway?.auth?.password ?? process.env.OPENCLAW_GATEWAY_PASSWORD; const oldPassword = baseConfig.gateway?.auth?.password ?? process.env.OPENCLAW_GATEWAY_PASSWORD; const token = nextConfig.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN; let gatewayProbe = await probeGatewayReachable({ url: links.wsUrl, token, password: newPassword, }); // If new password failed and it's different from old password, try old too. if (!gatewayProbe.ok && newPassword !== oldPassword && oldPassword) { gatewayProbe = await probeGatewayReachable({ url: links.wsUrl, token, password: oldPassword, }); } const gatewayStatusLine = gatewayProbe.ok ? "Gateway: reachable" : `Gateway: not detected${gatewayProbe.detail ? ` (${gatewayProbe.detail})` : ""}`; note( [ `Web UI: ${links.httpUrl}`, `Gateway WS: ${links.wsUrl}`, gatewayStatusLine, "Docs: https://docs.openclaw.ai/web/control-ui", ].join("\n"), "Control UI", ); outro(t("configure.gateway.configureComplete")); } catch (err) { if (err instanceof WizardCancelledError) { runtime.exit(0); return; } throw err; } }