// Setup finalize helpers write onboarding output and follow-up state. import fs from "node:fs/promises"; import path from "node:path"; import { restoreTerminalState } from "../../packages/terminal-core/src/restore.js"; import { resolveDefaultAgentDir } from "../agents/agent-scope-config.js"; import { describeCodexNativeWebSearch } from "../agents/codex-native-web-search.shared.js"; import { hasAuthProfileForProvider } from "../agents/tools/model-config.helpers.js"; import { DEFAULT_BOOTSTRAP_FILENAME } from "../agents/workspace.js"; import { formatCliCommand } from "../cli/command-format.js"; import { buildGatewayInstallPlan, gatewayInstallErrorHint, } from "../commands/daemon-install-helpers.js"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME, GATEWAY_DAEMON_RUNTIME_OPTIONS, } from "../commands/daemon-runtime.js"; import { resolveGatewayInstallToken } from "../commands/gateway-install-token.js"; import { formatHealthCheckFailure } from "../commands/health-format.js"; import { healthCommand } from "../commands/health.js"; import { detectBrowserOpenSupport, formatControlUiSshHint, openUrl, probeGatewayReachable, waitForGatewayReachable, resolveAdvertisedControlUiLinks, resolveLocalControlUiProbeLinks, } from "../commands/onboard-helpers.js"; import type { OnboardOptions } from "../commands/onboard-types.js"; import type { GatewayAuthConfig } from "../config/types.gateway.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { describeGatewayServiceRestart, resolveGatewayService } from "../daemon/service.js"; import { isSystemdUserServiceAvailable } from "../daemon/systemd.js"; import { isContainerEnvironment } from "../infra/container-environment.js"; import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js"; import { formatErrorMessage } from "../infra/errors.js"; import { formatWindowsGatewayFirewallGuidance } from "../infra/windows-gateway-firewall-diagnostics.js"; import type { RuntimeEnv } from "../runtime.js"; import { createLazyRuntimeModule } from "../shared/lazy-runtime.js"; import { launchTuiCli } from "../tui/tui-launch.js"; import { resolveUserPath } from "../utils.js"; import { listConfiguredWebSearchProviders } from "../web-search/runtime.js"; import { t } from "./i18n/index.js"; import type { WizardPrompter } from "./prompts.js"; import { setupWizardShellCompletion } from "./setup.completion.js"; import { resolveSetupSecretInputString } from "./setup.secret-input.js"; import type { GatewayWizardSettings, WizardFlow } from "./setup.types.js"; type FinalizeOnboardingOptions = { flow: WizardFlow; opts: OnboardOptions; baseConfig: OpenClawConfig; hadExistingConfig?: boolean; nextConfig: OpenClawConfig; workspaceDir: string; settings: GatewayWizardSettings; prompter: WizardPrompter; runtime: RuntimeEnv; }; const HATCH_TUI_TIMEOUT_MS = 5 * 60 * 1000; function buildSessionGatewayAuthOverride(params: { nextConfig: OpenClawConfig; settings: GatewayWizardSettings; resolvedGatewayPassword: string; }): GatewayAuthConfig | undefined { if (params.settings.authMode === "token" && params.settings.gatewayToken) { return { ...params.nextConfig.gateway?.auth, mode: "token", token: params.settings.gatewayToken, }; } if (params.settings.authMode === "password" && params.resolvedGatewayPassword) { return { ...params.nextConfig.gateway?.auth, mode: "password", password: params.resolvedGatewayPassword, }; } return params.nextConfig.gateway?.auth; } async function startSessionGatewayForOnboarding(params: { nextConfig: OpenClawConfig; settings: GatewayWizardSettings; resolvedGatewayPassword: string; prompter: WizardPrompter; }): Promise { const progress = params.prompter.progress(t("wizard.finalize.sessionGatewayStarting")); try { const { startGatewayServer } = await import("../gateway/server.js"); const server = await startGatewayServer(params.settings.port, { bind: params.settings.bind, ...(params.settings.bind === "custom" && params.settings.customBindHost ? { host: params.settings.customBindHost } : {}), auth: buildSessionGatewayAuthOverride({ nextConfig: params.nextConfig, settings: params.settings, resolvedGatewayPassword: params.resolvedGatewayPassword, }), tailscale: params.nextConfig.gateway?.tailscale, }); progress.stop(t("wizard.finalize.sessionGatewayStarted")); return server; } catch (error) { progress.stop(t("wizard.finalize.sessionGatewayStartFailed")); await params.prompter.note( [ t("wizard.finalize.sessionGatewayStartFailed"), formatErrorMessage(error), t("wizard.finalize.startGatewayNow", { command: formatCliCommand("openclaw gateway run"), }), ].join("\n"), "Gateway", ); return undefined; } } async function closeSessionGatewayForOnboarding(params: { sessionGateway: import("../gateway/server.js").GatewayServer; runtime: RuntimeEnv; reason: string; }): Promise { await params.sessionGateway.close({ reason: params.reason }).catch((error: unknown) => { params.runtime.error(formatErrorMessage(error)); }); } async function showControlUiDashboardNote(params: { prompter: WizardPrompter; settings: GatewayWizardSettings; authedUrl: string; controlUiBasePath: string | undefined; hintToken: string | undefined; }): Promise<{ opened: boolean }> { let opened = false; let openHint: string | undefined; const browserSupport = await detectBrowserOpenSupport(); if (browserSupport.ok) { opened = await openUrl(params.authedUrl); if (!opened) { openHint = formatControlUiSshHint({ port: params.settings.port, basePath: params.controlUiBasePath, token: params.hintToken, }); } } else { openHint = formatControlUiSshHint({ port: params.settings.port, basePath: params.controlUiBasePath, token: params.hintToken, }); } await params.prompter.note( [ t("wizard.finalize.dashboardLinkWithToken", { url: params.authedUrl }), opened ? t("wizard.finalize.dashboardOpened") : t("wizard.finalize.dashboardCopyPaste"), openHint, ] .filter(Boolean) .join("\n"), t("wizard.finalize.dashboardReady"), ); return { opened }; } function getLocalizedGatewayDaemonRuntimeOptions() { return GATEWAY_DAEMON_RUNTIME_OPTIONS.map((option) => ({ hint: option.value === "node" ? t("wizard.finalize.daemonRuntimeNodeHint") : (option.hint ?? undefined), label: option.value === "node" ? t("wizard.finalize.daemonRuntimeNode") : option.label, value: option.value, })); } const loadOnboardSearchModule = createLazyRuntimeModule( () => import("../commands/onboard-search.js"), ); /** * Ensure the gateway service matches the onboarding decision: prompt/decide * whether to install the daemon, then install/restart/reinstall it. Shared by * the classic wizard finalize and the bootstrap onboarding flow. */ export async function ensureGatewayServiceForOnboarding(params: { flow: WizardFlow; opts: Pick; nextConfig: OpenClawConfig; settings: Pick; prompter: WizardPrompter; runtime: RuntimeEnv; /** Pre-answer the "service already installed" prompt (conversational flows). */ loadedAction?: "restart"; }): Promise<{ installDaemon: boolean; containerWithoutUserSystemd: boolean }> { const { flow, opts, nextConfig, settings, prompter, runtime } = params; const withWizardProgress = async ( label: string, optionsLocal: { doneMessage?: string | (() => string | undefined) }, work: (progress: { update: (message: string) => void }) => Promise, ): Promise => { const progress = prompter.progress(label); try { return await work(progress); } finally { progress.stop( typeof optionsLocal.doneMessage === "function" ? optionsLocal.doneMessage() : optionsLocal.doneMessage, ); } }; const systemdAvailable = process.platform === "linux" ? await isSystemdUserServiceAvailable() : true; const linuxWithoutUserSystemd = process.platform === "linux" && !systemdAvailable; const containerWithoutUserSystemd = linuxWithoutUserSystemd && isContainerEnvironment(); if (linuxWithoutUserSystemd) { await prompter.note( t( containerWithoutUserSystemd ? "wizard.finalize.containerSystemdUnavailable" : "wizard.finalize.systemdUnavailable", ), containerWithoutUserSystemd ? t("wizard.finalize.containerRuntimeTitle") : "Systemd", ); } if (process.platform === "linux" && systemdAvailable) { const { ensureSystemdUserLingerInteractive } = await import("../commands/systemd-linger.js"); await ensureSystemdUserLingerInteractive({ runtime, prompter: { confirm: prompter.confirm, note: prompter.note, }, reason: t("wizard.finalize.systemdLingerReason"), requireConfirm: false, }); } const explicitInstallDaemon = typeof opts.installDaemon === "boolean" ? opts.installDaemon : undefined; let installDaemon: boolean; if (explicitInstallDaemon !== undefined) { installDaemon = explicitInstallDaemon; } else if (linuxWithoutUserSystemd) { installDaemon = false; } else if (flow === "quickstart") { installDaemon = true; } else { installDaemon = await prompter.confirm({ message: t("wizard.finalize.installGateway"), initialValue: true, }); } if (linuxWithoutUserSystemd && installDaemon) { await prompter.note( t("wizard.finalize.systemdInstallSkipped"), t("wizard.finalize.gatewayService"), ); installDaemon = false; } if (installDaemon) { const daemonRuntime = flow === "quickstart" ? DEFAULT_GATEWAY_DAEMON_RUNTIME : await prompter.select({ message: t("wizard.finalize.daemonRuntime"), options: getLocalizedGatewayDaemonRuntimeOptions(), initialValue: opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME, }); if (flow === "quickstart") { await prompter.note( t("wizard.finalize.quickstartNodeRuntime"), t("wizard.finalize.daemonRuntime"), ); } const service = resolveGatewayService(); const loaded = await service.isLoaded({ env: process.env }); let restartWasScheduled = false; if (loaded) { const action = params.loadedAction ?? (await prompter.select({ message: t("wizard.finalize.alreadyInstalled"), options: [ { value: "restart", label: t("wizard.finalize.restart") }, { value: "reinstall", label: t("wizard.finalize.reinstall") }, { value: "skip", label: t("common.skip") }, ], })); if (action === "restart") { let restartDoneMessage = t("wizard.finalize.gatewayServiceRestarted"); await withWizardProgress( t("wizard.finalize.gatewayService"), { doneMessage: () => restartDoneMessage }, async (progress) => { progress.update(t("wizard.finalize.gatewayServiceRestarting")); const restartResult = await service.restart({ env: process.env, stdout: process.stdout, }); const restartStatus = describeGatewayServiceRestart("Gateway", restartResult); restartDoneMessage = restartStatus.scheduled ? t("wizard.finalize.gatewayServiceRestartScheduled") : t("wizard.finalize.gatewayServiceRestarted"); restartWasScheduled = restartStatus.scheduled; }, ); } else if (action === "reinstall") { await withWizardProgress( t("wizard.finalize.gatewayService"), { doneMessage: t("wizard.finalize.gatewayServiceUninstalled") }, async (progress) => { progress.update(t("wizard.finalize.gatewayServiceUninstalling")); await service.uninstall({ env: process.env, stdout: process.stdout }); }, ); } } if ( !loaded || (!restartWasScheduled && loaded && !(await service.isLoaded({ env: process.env }))) ) { const progress = prompter.progress(t("wizard.finalize.gatewayService")); let installError: string | null = null; const installWarnings: Array<{ message: string; title?: string }> = []; const flushInstallWarnings = async () => { let warning: (typeof installWarnings)[number] | undefined; // Remove before awaiting so a rejected note is not replayed when the // outer catch drains warnings that remain in the planner's queue. while ((warning = installWarnings.shift()) !== undefined) { await prompter.note(warning.message, warning.title); } }; try { progress.update(t("wizard.finalize.gatewayServicePreparing")); const tokenResolution = await resolveGatewayInstallToken({ config: nextConfig, env: process.env, }); for (const warning of tokenResolution.warnings) { await prompter.note(warning, "Gateway service"); } if (tokenResolution.unavailableReason) { installError = [ t("wizard.finalize.gatewayInstallBlocked"), tokenResolution.unavailableReason, t("wizard.finalize.gatewayInstallFixAuth"), ].join(" "); } else { const { programArguments, workingDirectory, environment, environmentValueSources } = await buildGatewayInstallPlan({ env: process.env, port: settings.port, runtime: daemonRuntime, warn: (message, title) => { installWarnings.push({ message, title }); }, config: nextConfig, }); await flushInstallWarnings(); progress.update(t("wizard.finalize.gatewayServiceInstalling")); await service.install({ env: process.env, stdout: process.stdout, programArguments, workingDirectory, environment, environmentValueSources, }); } } catch (err) { await flushInstallWarnings(); installError = formatErrorMessage(err); } finally { progress.stop( installError ? t("wizard.finalize.gatewayServiceInstallFailed") : t("wizard.finalize.gatewayServiceInstalled"), ); } if (installError) { await prompter.note( t("wizard.finalize.gatewayServiceInstallFailedWithError", { error: installError }), "Gateway", ); await prompter.note(gatewayInstallErrorHint(), "Gateway"); } } } return { installDaemon, containerWithoutUserSystemd }; } export async function finalizeSetupWizard( options: FinalizeOnboardingOptions, ): Promise<{ launchedTui: boolean }> { const { flow, opts, baseConfig, nextConfig, settings, prompter, runtime } = options; const suppressGatewayTokenOutput = opts.suppressGatewayTokenOutput === true; let gatewayProbe: { ok: boolean; detail?: string } = { ok: true }; let resolvedGatewayPassword = ""; let sessionGateway: import("../gateway/server.js").GatewayServer | undefined; const { installDaemon, containerWithoutUserSystemd } = await ensureGatewayServiceForOnboarding({ flow, opts, nextConfig, settings, prompter, runtime, }); if (settings.authMode === "password") { try { resolvedGatewayPassword = (await resolveSetupSecretInputString({ config: nextConfig, value: nextConfig.gateway?.auth?.password, path: "gateway.auth.password", env: process.env, })) ?? ""; } catch (error) { await prompter.note( [ t("wizard.finalize.secretRefAuthFailed", { field: "gateway.auth.password" }), formatErrorMessage(error), ].join("\n"), t("wizard.gateway.auth"), ); } } if (containerWithoutUserSystemd && !opts.skipUi) { sessionGateway = await startSessionGatewayForOnboarding({ nextConfig, settings, resolvedGatewayPassword, prompter, }); } try { if (!opts.skipHealth) { const probeLinks = resolveLocalControlUiProbeLinks({ bind: nextConfig.gateway?.bind ?? "loopback", port: settings.port, customBindHost: nextConfig.gateway?.customBindHost, basePath: undefined, tlsEnabled: nextConfig.gateway?.tls?.enabled === true, }); // Daemon install/restart can briefly flap the WS; wait a bit so health check doesn't false-fail. gatewayProbe = await waitForGatewayReachable({ url: probeLinks.wsUrl, token: settings.authMode === "token" ? settings.gatewayToken : undefined, password: settings.authMode === "password" ? resolvedGatewayPassword : undefined, deadlineMs: 15_000, }); if (gatewayProbe.ok) { try { const healthConfig: OpenClawConfig = settings.authMode === "token" && settings.gatewayToken ? { ...nextConfig, gateway: { ...nextConfig.gateway, auth: { ...nextConfig.gateway?.auth, mode: "token", token: settings.gatewayToken, }, }, } : nextConfig; await healthCommand( { json: false, timeoutMs: 10_000, config: healthConfig, token: settings.authMode === "token" ? settings.gatewayToken : undefined, password: settings.authMode === "password" ? resolvedGatewayPassword : undefined, }, runtime, ); } catch (err) { runtime.error(formatHealthCheckFailure(err)); await prompter.note( [ t("common.docs"), "https://docs.openclaw.ai/gateway/health", "https://docs.openclaw.ai/gateway/troubleshooting", ].join("\n"), t("wizard.finalize.healthCheckHelp"), ); } } else if (installDaemon) { runtime.error( formatHealthCheckFailure( new Error( gatewayProbe.detail ?? `gateway did not become reachable at ${probeLinks.wsUrl}`, ), ), ); await prompter.note( [ t("common.docs"), "https://docs.openclaw.ai/gateway/health", "https://docs.openclaw.ai/gateway/troubleshooting", ].join("\n"), t("wizard.finalize.healthCheckHelp"), ); } else { await prompter.note( [ t("wizard.finalize.gatewayNotDetected"), t("wizard.finalize.noBackgroundGatewayExpected"), t("wizard.finalize.startGatewayNow", { command: formatCliCommand("openclaw gateway run"), }), t("wizard.finalize.rerunInstallDaemon", { command: formatCliCommand("openclaw onboard --install-daemon"), }), t("wizard.finalize.skipHealthNextTime", { command: formatCliCommand("openclaw onboard --skip-health"), }), ].join("\n"), "Gateway", ); } } const controlUiEnabled = nextConfig.gateway?.controlUi?.enabled ?? baseConfig.gateway?.controlUi?.enabled ?? true; if (!opts.skipUi && controlUiEnabled) { const controlUiAssets = await ensureControlUiAssetsBuilt(runtime); if (!controlUiAssets.ok && controlUiAssets.message) { runtime.error(controlUiAssets.message); } } await prompter.note( [ t("wizard.finalize.addNodes"), `- ${t("wizard.finalize.nodeMac")}`, `- ${t("wizard.finalize.nodeIos")}`, `- ${t("wizard.finalize.nodeAndroid")}`, ].join("\n"), t("wizard.finalize.optionalApps"), ); const controlUiBasePath = nextConfig.gateway?.controlUi?.basePath ?? baseConfig.gateway?.controlUi?.basePath; const displayLinks = await resolveAdvertisedControlUiLinks({ bind: settings.bind, port: settings.port, customBindHost: settings.customBindHost, basePath: controlUiBasePath, tlsEnabled: nextConfig.gateway?.tls?.enabled === true, }); const probeLinks = resolveLocalControlUiProbeLinks({ bind: settings.bind, port: settings.port, customBindHost: settings.customBindHost, basePath: controlUiBasePath, tlsEnabled: nextConfig.gateway?.tls?.enabled === true, }); const authedUrl = settings.authMode === "token" && settings.gatewayToken && !suppressGatewayTokenOutput ? `${displayLinks.httpUrl}#token=${encodeURIComponent(settings.gatewayToken)}` : displayLinks.httpUrl; if (opts.skipHealth || !gatewayProbe.ok) { gatewayProbe = await probeGatewayReachable({ url: probeLinks.wsUrl, token: settings.authMode === "token" ? settings.gatewayToken : undefined, password: settings.authMode === "password" ? resolvedGatewayPassword : "", }); } const gatewayStatusLine = gatewayProbe.ok ? t("wizard.finalize.gatewayReachable") : t("wizard.finalize.gatewayNotDetectedStatus", { detail: gatewayProbe.detail ? ` (${gatewayProbe.detail})` : "", }); const windowsFirewallLines = formatWindowsGatewayFirewallGuidance({ bind: settings.bind, }); const bootstrapPath = path.join( resolveUserPath(options.workspaceDir), DEFAULT_BOOTSTRAP_FILENAME, ); const hasBootstrap = await fs .access(bootstrapPath) .then(() => true) .catch(() => false); const agentDir = resolveDefaultAgentDir(nextConfig); // Without model credentials the seeded first message is guaranteed to fail // with a provider auth error, so hatch quietly and explain instead. const { resolveDefaultModelAuthStatus } = await import("../commands/auth-choice.js"); const modelAuthStatus = resolveDefaultModelAuthStatus(nextConfig, { agentDir }); const shouldSeedBootstrapHatch = hasBootstrap && options.hadExistingConfig !== true && modelAuthStatus.hasAuth; await prompter.note( [ t("wizard.finalize.webUiUrl", { url: displayLinks.httpUrl }), settings.authMode === "token" && settings.gatewayToken && !suppressGatewayTokenOutput ? t("wizard.finalize.webUiWithTokenUrl", { url: authedUrl }) : undefined, t("wizard.finalize.gatewayWsUrl", { url: displayLinks.wsUrl }), gatewayStatusLine, ...windowsFirewallLines, t("wizard.finalize.controlUiDocs"), ] .filter(Boolean) .join("\n"), "Control UI", ); let controlUiOpened = false; const seededInBackground = false; let launchedTui = false; const shouldLaunchTui = !opts.skipUi; if (shouldLaunchTui) { if (hasBootstrap) { await prompter.note( [ t("wizard.finalize.workspaceReady"), ...(shouldSeedBootstrapHatch ? [t("wizard.finalize.firstTerminalChat")] : []), t("wizard.finalize.editBootstrap"), ].join("\n"), t("wizard.finalize.hatchYourAgent"), ); } if (!modelAuthStatus.hasAuth) { await prompter.note( [ t("wizard.finalize.noModelAuth", { provider: modelAuthStatus.provider }), t("wizard.finalize.noModelAuthNext", { command: formatCliCommand("openclaw configure --section model"), }), ].join("\n"), t("wizard.finalize.noModelAuthTitle"), ); } if (gatewayProbe.ok) { const tokenNotes = [ t("wizard.finalize.gatewayTokenShared"), t("wizard.finalize.gatewayTokenStored"), t("wizard.finalize.gatewayTokenView", { command: formatCliCommand("openclaw config get gateway.auth.token"), }), t("wizard.finalize.gatewayTokenGenerate", { command: formatCliCommand("openclaw doctor --generate-gateway-token"), }), suppressGatewayTokenOutput ? undefined : t("wizard.finalize.dashboardTokenMemory"), t("wizard.finalize.dashboardOpenAnytime", { command: formatCliCommand("openclaw dashboard --no-open"), }), suppressGatewayTokenOutput ? undefined : t("wizard.finalize.dashboardTokenPrompt"), ].filter(Boolean); await prompter.note(tokenNotes.join("\n"), "Token"); } } else if (opts.skipUi) { await prompter.note(t("wizard.finalize.skipControlUi"), t("wizard.finalize.controlUiTitle")); } await prompter.note( [t("wizard.finalize.backupWorkspace"), t("wizard.finalize.workspaceDocs")].join("\n"), t("wizard.finalize.workspaceBackupTitle"), ); await prompter.note(t("wizard.finalize.securityReminder"), t("wizard.security.title")); await setupWizardShellCompletion({ flow, prompter }); const shouldOpenControlUi = !opts.skipUi && gatewayProbe.ok && settings.authMode === "token" && Boolean(settings.gatewayToken) && !suppressGatewayTokenOutput && !shouldLaunchTui; if (shouldOpenControlUi) { const dashboard = await showControlUiDashboardNote({ prompter, settings, authedUrl, controlUiBasePath, hintToken: settings.gatewayToken, }); controlUiOpened = dashboard.opened; } const codexNativeSummary = describeCodexNativeWebSearch(nextConfig); const webSearchProvider = nextConfig.tools?.web?.search?.provider; const webSearchEnabled = nextConfig.tools?.web?.search?.enabled; const configuredSearchProviders = listConfiguredWebSearchProviders({ config: nextConfig }); if (webSearchProvider) { const { resolveExistingKey, hasExistingKey, hasKeyInEnv } = await loadOnboardSearchModule(); const entry = configuredSearchProviders.find((e) => e.id === webSearchProvider); const label = entry?.label ?? webSearchProvider; const storedKey = entry ? resolveExistingKey(nextConfig, webSearchProvider) : undefined; const keyConfigured = entry ? hasExistingKey(nextConfig, webSearchProvider) : false; const envAvailable = entry ? hasKeyInEnv(entry) : false; const hasKey = keyConfigured || envAvailable; const authProviderId = entry?.authProviderId?.trim(); const authProviderLabel = authProviderId === "xai" ? "xAI" : authProviderId; const providerAuthProfileAvailable = authProviderId ? hasAuthProfileForProvider({ provider: authProviderId, agentDir, }) : false; const oauthAuthProfileAvailable = authProviderId && providerAuthProfileAvailable ? hasAuthProfileForProvider({ provider: authProviderId, agentDir, type: "oauth", }) : false; const hasCredential = hasKey || providerAuthProfileAvailable; const keySource = storedKey ? t("wizard.finalize.webSearchKeyStored") : keyConfigured ? t("wizard.finalize.webSearchKeyRef") : envAvailable ? t("wizard.finalize.webSearchKeyEnv", { env: entry?.envVars.join(" / ") ?? "" }) : oauthAuthProfileAvailable && authProviderLabel ? t("wizard.finalize.webSearchOAuthProfile", { provider: authProviderLabel }) : providerAuthProfileAvailable && authProviderLabel ? t("wizard.finalize.webSearchAuthProfile", { provider: authProviderLabel }) : undefined; if (!entry) { await prompter.note( [ t("wizard.finalize.webSearchProviderUnavailable", { provider: label }), t("wizard.finalize.webSearchUnavailableAction"), ` ${formatCliCommand("openclaw configure --section web")}`, "", t("wizard.finalize.webDocs"), ].join("\n"), t("wizard.finalize.webSearchTitle"), ); } else if (webSearchEnabled !== false && entry.requiresCredential === false) { // Keyless providers (e.g. Parallel Search (Free), DuckDuckGo, Ollama) need // no API key — report ready rather than the credential-required warning. await prompter.note( [ t("wizard.finalize.webSearchKeyFree"), "", t("wizard.finalize.webSearchProvider", { provider: label }), t("wizard.finalize.webDocs"), ].join("\n"), t("wizard.finalize.webSearchTitle"), ); } else if (webSearchEnabled !== false && hasCredential) { await prompter.note( [ t("wizard.finalize.webSearchEnabled"), "", t("wizard.finalize.webSearchProvider", { provider: label }), ...(keySource ? [keySource] : []), t("wizard.finalize.webDocs"), ].join("\n"), t("wizard.finalize.webSearchTitle"), ); } else if (entry.requiresCredential !== false && !hasCredential) { await prompter.note( [ t("wizard.finalize.webSearchNoKey", { provider: label }), t("wizard.finalize.webSearchNeedsKey"), ` ${formatCliCommand("openclaw configure --section web")}`, "", t("wizard.finalize.webSearchGetKey", { url: entry?.signupUrl ?? "https://docs.openclaw.ai/tools/web", }), t("wizard.finalize.webDocs"), ].join("\n"), t("wizard.finalize.webSearchTitle"), ); } else { await prompter.note( [ t("wizard.finalize.webSearchDisabled", { provider: label }), t("wizard.finalize.webSearchReenable", { command: formatCliCommand("openclaw configure --section web"), }), "", t("wizard.finalize.webDocs"), ].join("\n"), t("wizard.finalize.webSearchTitle"), ); } } else { // Legacy configs may have a working key (e.g. apiKey or BRAVE_API_KEY) without // an explicit provider. Runtime auto-detects these, so avoid saying "skipped". const { hasExistingKey, hasKeyInEnv } = await loadOnboardSearchModule(); const legacyDetected = configuredSearchProviders.find( (e) => hasExistingKey(nextConfig, e.id) || hasKeyInEnv(e), ); if (legacyDetected) { await prompter.note( [ t("wizard.finalize.webSearchAutoDetected", { provider: legacyDetected.label }), t("wizard.finalize.webDocs"), ].join("\n"), t("wizard.finalize.webSearchTitle"), ); } else if (codexNativeSummary) { await prompter.note( [ t("wizard.finalize.managedWebSearchSkipped"), codexNativeSummary, t("wizard.finalize.webDocs"), ].join("\n"), t("wizard.finalize.webSearchTitle"), ); } else { await prompter.note( [ t("wizard.finalize.webSearchSkipped"), ` ${formatCliCommand("openclaw configure --section web")}`, "", t("wizard.finalize.webDocs"), ].join("\n"), t("wizard.finalize.webSearchTitle"), ); } } if (codexNativeSummary) { await prompter.note( [ codexNativeSummary, t("wizard.finalize.codexNativeSearchOnly"), t("wizard.finalize.webDocs"), ].join("\n"), t("wizard.finalize.codexNativeSearchTitle"), ); } await prompter.note(t("wizard.finalize.whatNow"), t("wizard.finalize.whatNowTitle")); await prompter.outro( controlUiOpened ? t("wizard.finalize.outroDashboardOpened") : seededInBackground ? t("wizard.finalize.outroSeeded") : t("wizard.finalize.outroDashboardLink"), ); if (shouldLaunchTui) { restoreTerminalState("pre-setup tui", { resumeStdinIfPaused: false }); try { await launchTuiCli( { ...(gatewayProbe.ok ? {} : { local: true }), deliver: false, message: shouldSeedBootstrapHatch ? t("wizard.finalize.bootstrapHatchMessage") : undefined, timeoutMs: HATCH_TUI_TIMEOUT_MS, }, gatewayProbe.ok ? { gatewayUrl: displayLinks.wsUrl, authSource: "config" } : {}, ); } finally { restoreTerminalState("post-setup tui", { resumeStdinIfPaused: false }); if (sessionGateway) { await closeSessionGatewayForOnboarding({ sessionGateway, runtime, reason: "onboarding tui exited", }); sessionGateway = undefined; } } launchedTui = true; } return { launchedTui }; } finally { if (sessionGateway) { await closeSessionGatewayForOnboarding({ sessionGateway, runtime, reason: "onboarding finalize exited", }); } } }