UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

828 lines (827 loc) 32.7 kB
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { d as resolveGatewayPort } from "./paths-mvMm5bYV.js"; import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js"; import { t as restoreTerminalState } from "./restore-BWpek1U9.js"; import { p as resolveUserPath } from "./utils-CCC-BEJH.js"; import { l as isValidEnvSecretRefId, m as resolveSecretInputRef } from "./types.secrets-_0JOMGE5.js"; import { n as defaultRuntime, r as writeRuntimeJson } from "./runtime-B4lgFmsS.js"; import { t as assertSupportedRuntime } from "./runtime-guard-DFX4PxCB.js"; import { s as resolveDefaultSecretProviderAlias } from "./ref-contract-BAa3qHwr.js"; import "./daemon-runtime-C76za6vm.js"; import { t as resolveConfiguredSecretInputString } from "./resolve-configured-secret-input-string-9uwQlTwC.js"; import { t as resolveGatewayAuthToken } from "./auth-token-resolution-AiDzF8d9.js"; import { u as readConfigFileSnapshot } from "./io-Gi7-pyU-.js"; import { i as replaceConfigFile } from "./config-C9RxTsn1.js"; import { t as randomToken } from "./random-token-B1woZa_H.js"; import { r as formatInvalidPortOption } from "./error-format-COoNML-C.js"; import { t as resolveControlUiLinks } from "./control-ui-links-HaNDPHaQ.js"; import { t as WizardCancelledError } from "./prompts-E0FayNjJ.js"; import { t as createClackPrompter } from "./clack-prompter-fc9v8HJv.js"; import { r as logConfigUpdated } from "./logging-Cr7I5jF_.js"; import { a as stripPendingPluginInstallRecords, i as hasPendingPluginInstallRecords, n as commitConfigWriteWithPendingPluginInstalls, s as unchangedPendingPluginInstallRecordIds } from "./plugins-install-record-commit--Zbbdxlm.js"; import { c as normalizeGatewayTokenInput, m as waitForGatewayReachable, n as applyWizardMetadata, o as handleReset, r as ensureWorkspaceAndSessions, t as DEFAULT_WORKSPACE } from "./onboard-helpers-DXyUiHQ-.js"; import { i as resolveDeprecatedAuthChoiceReplacement, n as isDeprecatedAuthChoice, r as normalizeLegacyOnboardAuthChoice, t as formatDeprecatedNonInteractiveAuthChoiceError } from "./auth-choice-legacy-B19jXK1Y.js"; import { i as applySkipBootstrapConfig, r as applyLocalSetupWorkspaceConfig } from "./onboard-config-oAXDZ2yl.js"; import { t as createNonInteractiveLoggingPrompter } from "./non-interactive-prompter-DLCGsi38.js"; import { t as runSetupWizard } from "./setup-DJSGE6lO.js"; //#region src/commands/onboard-interactive.ts /** * Interactive onboarding command entrypoint. * * It wires the Clack prompter to the setup wizard and restores terminal state * on every exit path so canceled setup cannot leave stdin paused. */ /** Runs the interactive setup wizard and maps user cancellation to exit code 1. */ async function runInteractiveSetup(opts, runtime = defaultRuntime) { const prompter = createClackPrompter(); let exitCode = null; try { await runSetupWizard(opts, runtime, prompter); } catch (err) { if (err instanceof WizardCancelledError) { exitCode = 1; return; } throw err; } finally { restoreTerminalState("setup finish", { resumeStdinIfPaused: false }); if (exitCode !== null) runtime.exit(exitCode); } } //#endregion //#region src/commands/onboard-non-interactive/config-write.ts /** * Config write commit helper for non-interactive onboarding. * * It preserves pending plugin install records before replacing the user config, * which lets setup reruns avoid dropping plugin-owned state accidentally. */ /** Commits a non-interactive onboard config update with pending plugin records handled first. */ async function commitNonInteractiveOnboardConfig(params) { const allowConfigSizeDrop = params.reset === true; let writeBaseHash = params.baseHash; let nextConfig = params.nextConfig; if (!allowConfigSizeDrop && hasPendingPluginInstallRecords(params.baseConfig)) { writeBaseHash = (await commitConfigWriteWithPendingPluginInstalls({ nextConfig: params.baseConfig, writeOptions: { allowConfigSizeDrop: true }, commit: async (config, writeOptions) => { return await replaceConfigFile({ nextConfig: config, ...writeBaseHash !== void 0 ? { baseHash: writeBaseHash } : {}, ...writeOptions ? { writeOptions } : {} }); } })).persistedHash ?? void 0; nextConfig = stripPendingPluginInstallRecords(nextConfig, unchangedPendingPluginInstallRecordIds(nextConfig, params.baseConfig)); } return (await commitConfigWriteWithPendingPluginInstalls({ nextConfig, writeOptions: { allowConfigSizeDrop }, commit: async (config, writeOptions) => { return await replaceConfigFile({ nextConfig: config, ...writeBaseHash !== void 0 ? { baseHash: writeBaseHash } : {}, ...writeOptions ? { writeOptions } : {} }); } })).config; } //#endregion //#region src/commands/onboard-non-interactive/local/gateway-config.ts /** * Gateway config mutation for local non-interactive onboarding. * * This module owns port/bind/auth validation and token/ref preservation before * the final config write happens. */ /** Applies gateway CLI options to the pending config and returns normalized runtime settings. */ function applyNonInteractiveGatewayConfig(params) { const { opts, runtime } = params; const gatewayPort = opts.gatewayPort; if (gatewayPort !== void 0 && (!Number.isFinite(gatewayPort) || gatewayPort <= 0 || gatewayPort > 65535)) { runtime.error(formatInvalidPortOption("--gateway-port")); runtime.exit(1); return null; } const port = gatewayPort ?? params.defaultPort; let bind = opts.gatewayBind ?? "loopback"; const authModeRaw = opts.gatewayAuth ?? "token"; if (authModeRaw !== "token" && authModeRaw !== "password") { runtime.error("Invalid --gateway-auth. Use \"token\" or \"password\"."); runtime.exit(1); return null; } let authMode = authModeRaw; const tailscaleMode = opts.tailscale ?? "off"; const tailscaleResetOnExit = Boolean(opts.tailscaleResetOnExit); if (tailscaleMode !== "off" && bind !== "loopback") bind = "loopback"; if (tailscaleMode === "funnel" && authMode !== "password") authMode = "password"; let nextConfig = params.nextConfig; const explicitGatewayToken = normalizeGatewayTokenInput(opts.gatewayToken); const envGatewayToken = normalizeGatewayTokenInput(process.env.OPENCLAW_GATEWAY_TOKEN); const existingTokenInput = nextConfig.gateway?.auth?.token; const existingTokenRef = resolveSecretInputRef({ value: existingTokenInput, defaults: nextConfig.secrets?.defaults }).ref; const existingPlaintextToken = normalizeGatewayTokenInput(existingTokenInput); let gatewayToken = explicitGatewayToken || existingPlaintextToken || envGatewayToken || void 0; const gatewayTokenRefEnv = normalizeOptionalString(opts.gatewayTokenRefEnv ?? "") ?? ""; if (authMode === "token") if (gatewayTokenRefEnv) { if (!isValidEnvSecretRefId(gatewayTokenRefEnv)) { runtime.error("Invalid --gateway-token-ref-env. Use an environment variable name like OPENCLAW_GATEWAY_TOKEN."); runtime.exit(1); return null; } if (explicitGatewayToken) { runtime.error("Use either --gateway-token or --gateway-token-ref-env, not both. Prefer --gateway-token-ref-env to avoid writing plaintext tokens."); runtime.exit(1); return null; } if (!process.env[gatewayTokenRefEnv]?.trim()) { runtime.error(`Environment variable "${gatewayTokenRefEnv}" is missing or empty. Export it first, then rerun ${formatCliCommand("openclaw onboard --non-interactive")}.`); runtime.exit(1); return null; } nextConfig = { ...nextConfig, gateway: { ...nextConfig.gateway, auth: { ...nextConfig.gateway?.auth, mode: "token", token: { source: "env", provider: resolveDefaultSecretProviderAlias(nextConfig, "env", { preferFirstProviderForSource: true }), id: gatewayTokenRefEnv } } } }; } else if (!explicitGatewayToken && existingTokenRef) nextConfig = { ...nextConfig, gateway: { ...nextConfig.gateway, auth: { ...nextConfig.gateway?.auth, mode: "token" } } }; else { if (!gatewayToken) gatewayToken = randomToken(); nextConfig = { ...nextConfig, gateway: { ...nextConfig.gateway, auth: { ...nextConfig.gateway?.auth, mode: "token", token: gatewayToken } } }; } if (authMode === "password") { const password = opts.gatewayPassword?.trim(); if (!password) { runtime.error("Missing --gateway-password for password auth. Pass --gateway-password or use --gateway-auth token."); runtime.exit(1); return null; } nextConfig = { ...nextConfig, gateway: { ...nextConfig.gateway, auth: { ...nextConfig.gateway?.auth, mode: "password", password } } }; } nextConfig = { ...nextConfig, gateway: { ...nextConfig.gateway, port, bind, tailscale: { ...nextConfig.gateway?.tailscale, mode: tailscaleMode, resetOnExit: tailscaleResetOnExit } } }; return { nextConfig, port, bind, authMode, tailscaleMode, tailscaleResetOnExit }; } //#endregion //#region src/commands/onboard-non-interactive/local/output.ts /** * Output helpers for non-interactive onboarding. * * JSON success/failure payloads and human-readable gateway health diagnostics * are kept here so local and remote setup report failures consistently. */ /** Emits the JSON success payload for non-interactive onboarding when requested. */ function logNonInteractiveOnboardingJson(params) { if (!params.opts.json) return; writeRuntimeJson(params.runtime, { ok: true, mode: params.mode, workspace: params.workspaceDir, authChoice: params.authChoice, gateway: params.gateway, installDaemon: Boolean(params.installDaemon), daemonInstall: params.daemonInstall, daemonRuntime: params.daemonRuntime, skipSkills: Boolean(params.skipSkills), skipHealth: Boolean(params.skipHealth) }); } function formatGatewayRuntimeSummary(diagnostics) { const service = diagnostics?.service; if (!service?.runtimeStatus) return; const parts = [service.runtimeStatus]; if (typeof service.pid === "number") parts.push(`pid ${service.pid}`); if (service.state) parts.push(`state ${service.state}`); if (typeof service.lastExitStatus === "number") parts.push(`last exit ${service.lastExitStatus}`); if (service.lastExitReason) parts.push(`reason ${service.lastExitReason}`); return parts.join(", "); } function hasConnectionRefusedDetail(detail) { return /\b(?:econnrefused|connection refused|connect refused)\b/i.test(detail); } function classifyGatewayHealthFailure(params) { const detail = params.detail ?? ""; const lastGatewayError = params.diagnostics?.lastGatewayError ?? ""; const combined = `${detail}\n${lastGatewayError}`; if (/\b(?:unauthorized|forbidden|invalid token|invalid password|auth mismatch)\b/i.test(combined)) return "auth-mismatch"; if (/\b(?:runtime[- ]deps?|runtime dependencies|cannot find module|sqlite-vec|loadextension)\b/i.test(combined)) return "module-missing"; if (params.diagnostics?.service?.loaded === false && hasConnectionRefusedDetail(detail)) return "service-missing"; const runtimeStatus = params.diagnostics?.service?.runtimeStatus; if (runtimeStatus && runtimeStatus !== "running" && runtimeStatus !== "active" && hasConnectionRefusedDetail(detail)) return "service-stopped"; if (lastGatewayError.trim()) return "startup-blocked"; if (hasConnectionRefusedDetail(detail)) return "not-listening"; } function recoveryHintForGatewayHealthFailure(classification) { switch (classification) { case "auth-mismatch": return "Fix: run `openclaw doctor --fix`."; case "module-missing": return "Fix: run `openclaw doctor --fix`."; case "service-missing": return "Fix: run `openclaw gateway install --force`."; case "service-stopped": return "Fix: run `openclaw gateway restart`."; case "startup-blocked": return "Fix: run `openclaw gateway status --deep`."; case "not-listening": return "Fix: start `openclaw gateway run`, or run `openclaw gateway restart` for a managed gateway."; default: return; } } /** Emits JSON or human-readable failure output for non-interactive onboarding. */ function logNonInteractiveOnboardingFailure(params) { const classification = classifyGatewayHealthFailure({ detail: params.detail, diagnostics: params.diagnostics }); const recoveryHint = recoveryHintForGatewayHealthFailure(classification); const hints = [...recoveryHint ? [recoveryHint] : [], ...params.hints?.filter(Boolean) ?? []]; const gatewayRuntime = formatGatewayRuntimeSummary(params.diagnostics); if (params.opts.json) { writeRuntimeJson(params.runtime, { ok: false, mode: params.mode, phase: params.phase, message: params.message, classification, detail: params.detail, gateway: params.gateway, installDaemon: Boolean(params.installDaemon), daemonInstall: params.daemonInstall, daemonRuntime: params.daemonRuntime, diagnostics: params.diagnostics, hints: hints.length > 0 ? hints : void 0 }); return; } const lines = [ params.message, classification ? `Classification: ${classification}` : void 0, params.detail ? `Last probe: ${params.detail}` : void 0, params.diagnostics?.service ? `Service: ${params.diagnostics.service.label} (${params.diagnostics.service.loaded ? params.diagnostics.service.loadedText : "not loaded"})` : void 0, gatewayRuntime ? `Runtime: ${gatewayRuntime}` : void 0, params.diagnostics?.lastGatewayError ? `Last gateway error: ${params.diagnostics.lastGatewayError}` : void 0, params.diagnostics?.inspectError ? `Diagnostics warning: ${params.diagnostics.inspectError}` : void 0, hints.length > 0 ? hints.join("\n") : void 0 ].filter(Boolean).join("\n"); params.runtime.error(lines); } //#endregion //#region src/commands/onboard-non-interactive/local/skills-config.ts /** Applies the non-interactive skills install options to the pending config. */ function applyNonInteractiveSkillsConfig(params) { const { nextConfig, opts, runtime } = params; if (opts.skipSkills) return nextConfig; const nodeManager = opts.nodeManager ?? "npm"; if (![ "npm", "pnpm", "bun" ].includes(nodeManager)) { runtime.error("Invalid --node-manager. Use \"npm\", \"pnpm\", or \"bun\"."); runtime.exit(1); return nextConfig; } return { ...nextConfig, skills: { ...nextConfig.skills, install: { ...nextConfig.skills?.install, nodeManager } } }; } //#endregion //#region src/commands/onboard-non-interactive/local/workspace.ts /** Resolves the workspace directory used by local non-interactive setup. */ function resolveNonInteractiveWorkspaceDir(params) { return resolveUserPath((params.opts.workspace ?? params.baseConfig.agents?.defaults?.workspace ?? params.defaultWorkspaceDir).trim()); } //#endregion //#region src/commands/onboard-non-interactive/local.ts /** * Local non-interactive onboarding orchestration. * * This entrypoint applies config changes, optionally installs the gateway * daemon, verifies health, and emits machine-readable setup output. */ const INSTALL_DAEMON_HEALTH_DEADLINE_MS = 45e3; const ATTACH_EXISTING_GATEWAY_HEALTH_DEADLINE_MS = 15e3; const INSTALL_DAEMON_HEALTH_PROBE_TIMEOUT_MS = 1e4; const WINDOWS_INSTALL_DAEMON_HEALTH_DEADLINE_MS = 9e4; const WINDOWS_INSTALL_DAEMON_HEALTH_PROBE_TIMEOUT_MS = 15e3; const INSTALL_DAEMON_HEALTH_COMMAND_TIMEOUT_MS = 1e4; const WINDOWS_INSTALL_DAEMON_HEALTH_COMMAND_TIMEOUT_MS = 9e4; /** Returns platform-specific health timing for managed daemon installs. */ function resolveInstallDaemonGatewayHealthTiming(platform = process.platform) { if (platform === "win32") return { deadlineMs: WINDOWS_INSTALL_DAEMON_HEALTH_DEADLINE_MS, probeTimeoutMs: WINDOWS_INSTALL_DAEMON_HEALTH_PROBE_TIMEOUT_MS, healthCommandTimeoutMs: WINDOWS_INSTALL_DAEMON_HEALTH_COMMAND_TIMEOUT_MS }; return { deadlineMs: INSTALL_DAEMON_HEALTH_DEADLINE_MS, probeTimeoutMs: INSTALL_DAEMON_HEALTH_PROBE_TIMEOUT_MS, healthCommandTimeoutMs: INSTALL_DAEMON_HEALTH_COMMAND_TIMEOUT_MS }; } async function collectGatewayHealthFailureDiagnostics() { const diagnostics = {}; try { const { resolveGatewayService } = await import("./service-T_yX9_lb.js"); const service = resolveGatewayService(); const env = process.env; const [loaded, runtime] = await Promise.all([service.isLoaded({ env }).catch(() => false), service.readRuntime(env).catch(() => void 0)]); diagnostics.service = { label: service.label, loaded, loadedText: service.loadedText, runtimeStatus: runtime?.status, state: runtime?.state, pid: runtime?.pid, lastExitStatus: runtime?.lastExitStatus, lastExitReason: runtime?.lastExitReason }; } catch (err) { diagnostics.inspectError = `service diagnostics failed: ${String(err)}`; } try { const { readLastGatewayErrorLine } = await import("./diagnostics-s1BstpDV.js"); diagnostics.lastGatewayError = await readLastGatewayErrorLine(process.env) ?? void 0; } catch (err) { diagnostics.inspectError = diagnostics.inspectError ? `${diagnostics.inspectError}; log diagnostics failed: ${String(err)}` : `log diagnostics failed: ${String(err)}`; } return diagnostics.service || diagnostics.lastGatewayError || diagnostics.inspectError ? diagnostics : void 0; } /** Resolves the auth material used by the post-setup gateway health probe. */ async function resolveGatewayHealthProbeToken(nextConfig) { if (nextConfig.gateway?.auth?.mode === "password") { const resolved = await resolveConfiguredSecretInputString({ config: nextConfig, env: process.env, value: nextConfig.gateway.auth.password, path: "gateway.auth.password", unresolvedReasonStyle: "detailed" }); return { password: resolved.value, unresolvedRefReason: resolved.unresolvedRefReason }; } const resolved = await resolveGatewayAuthToken({ cfg: nextConfig, env: process.env, envFallback: "no-secret-ref", unresolvedReasonStyle: "detailed" }); const probeAuth = {}; if (resolved.token) probeAuth.token = resolved.token; if (resolved.unresolvedRefReason) probeAuth.unresolvedRefReason = resolved.unresolvedRefReason; return probeAuth; } function formatGatewayHealthFailureDetail(params) { return [params.probeDetail, params.unresolvedRefReason].filter(Boolean).join("\n") || void 0; } /** Runs local non-interactive setup from config mutation through health verification. */ async function runNonInteractiveLocalSetup(params) { const { opts, runtime, baseConfig, baseHash } = params; const mode = "local"; const workspaceDir = resolveNonInteractiveWorkspaceDir({ opts, baseConfig, defaultWorkspaceDir: DEFAULT_WORKSPACE }); let nextConfig = applyLocalSetupWorkspaceConfig(baseConfig, workspaceDir); if (opts.skipBootstrap) nextConfig = applySkipBootstrapConfig(nextConfig); const inferredAuthChoice = opts.authChoice ? void 0 : (await import("./auth-choice-inference-BploMKSs.js")).inferAuthChoiceFromFlags(opts, { config: nextConfig, workspaceDir, env: process.env }); if (!opts.authChoice && inferredAuthChoice && inferredAuthChoice.matches.length > 1) { runtime.error([ "Multiple API key flags were provided for non-interactive setup.", "Use a single provider flag or pass --auth-choice explicitly.", `Flags: ${inferredAuthChoice.matches.map((match) => match.label).join(", ")}` ].join("\n")); runtime.exit(1); return; } const authChoice = opts.authChoice ?? inferredAuthChoice?.choice ?? "skip"; if (authChoice !== "skip") { const { applyNonInteractiveAuthChoice } = await import("./auth-choice-D-RgcQyK.js"); const nextConfigAfterAuth = await applyNonInteractiveAuthChoice({ nextConfig, authChoice, opts, runtime, baseConfig }); if (!nextConfigAfterAuth) return; nextConfig = nextConfigAfterAuth; } const gatewayBasePort = resolveGatewayPort(baseConfig); const gatewayResult = applyNonInteractiveGatewayConfig({ nextConfig, opts, runtime, defaultPort: gatewayBasePort }); if (!gatewayResult) return; nextConfig = gatewayResult.nextConfig; nextConfig = applyNonInteractiveSkillsConfig({ nextConfig, opts, runtime }); nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode }); nextConfig = await commitNonInteractiveOnboardConfig({ nextConfig, baseConfig, baseHash, reset: opts.reset }); logConfigUpdated(runtime); await ensureWorkspaceAndSessions(workspaceDir, runtime, { skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap), skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles }); const daemonRuntimeRaw = opts.daemonRuntime ?? "node"; let daemonInstallStatus; if (opts.installDaemon) { const { installGatewayDaemonNonInteractive } = await import("./daemon-install-DRK4wHXs.js"); const daemonInstall = await installGatewayDaemonNonInteractive({ nextConfig, opts, runtime, port: gatewayResult.port }); daemonInstallStatus = daemonInstall.installed ? { requested: true, installed: true } : { requested: true, installed: false, skippedReason: daemonInstall.skippedReason }; if (!daemonInstall.installed && !opts.skipHealth) { logNonInteractiveOnboardingFailure({ opts, runtime, mode, phase: "daemon-install", message: daemonInstall.skippedReason === "systemd-user-unavailable" ? "Gateway service install is unavailable because systemd user services are not reachable in this Linux session." : "Gateway service install did not complete successfully.", installDaemon: true, daemonInstall: { requested: true, installed: false, skippedReason: daemonInstall.skippedReason }, daemonRuntime: daemonRuntimeRaw, hints: daemonInstall.skippedReason === "systemd-user-unavailable" ? ["Fix: rerun without `--install-daemon` for one-shot setup, or enable a working user-systemd session and retry.", "If your auth profile uses env-backed refs, keep those env vars set in the shell that runs `openclaw gateway run` or `openclaw agent --local`."] : [`Run \`${formatCliCommand("openclaw gateway status --deep")}\` for more detail.`] }); runtime.exit(1); return; } } if (!opts.skipHealth) { const { healthCommand } = await import("./health-B9v-C93Q.js"); const links = resolveControlUiLinks({ bind: gatewayResult.bind, port: gatewayResult.port, customBindHost: nextConfig.gateway?.customBindHost, basePath: void 0, tlsEnabled: nextConfig.gateway?.tls?.enabled === true }); const installDaemonGatewayHealthTiming = resolveInstallDaemonGatewayHealthTiming(); const probeAuth = await resolveGatewayHealthProbeToken(nextConfig); const probe = await waitForGatewayReachable({ url: links.wsUrl, token: probeAuth.token, password: probeAuth.password, deadlineMs: opts.installDaemon ? installDaemonGatewayHealthTiming.deadlineMs : ATTACH_EXISTING_GATEWAY_HEALTH_DEADLINE_MS, probeTimeoutMs: opts.installDaemon ? installDaemonGatewayHealthTiming.probeTimeoutMs : void 0 }); if (!probe.ok) { const detail = formatGatewayHealthFailureDetail({ probeDetail: probe.detail, unresolvedRefReason: probeAuth.unresolvedRefReason }); const diagnostics = opts.installDaemon ? await collectGatewayHealthFailureDiagnostics() : void 0; logNonInteractiveOnboardingFailure({ opts, runtime, mode, phase: "gateway-health", message: `Gateway did not become reachable at ${links.wsUrl}.`, detail, gateway: { wsUrl: links.wsUrl, httpUrl: links.httpUrl }, installDaemon: Boolean(opts.installDaemon), daemonInstall: daemonInstallStatus, daemonRuntime: opts.installDaemon ? daemonRuntimeRaw : void 0, diagnostics, hints: !opts.installDaemon ? [ "Non-interactive local setup only waits for an already-running gateway unless you pass --install-daemon.", `Fix: start \`${formatCliCommand("openclaw gateway run")}\`, re-run with \`--install-daemon\`, or use \`--skip-health\`.`, process.platform === "win32" ? "Native Windows managed gateway install tries Scheduled Tasks first and falls back to a per-user Startup-folder login item when task creation is denied." : void 0 ].filter((value) => Boolean(value)) : [`Run \`${formatCliCommand("openclaw gateway status --deep")}\` for more detail.`] }); runtime.exit(1); return; } await healthCommand({ json: false, timeoutMs: opts.installDaemon ? installDaemonGatewayHealthTiming.healthCommandTimeoutMs : 1e4, config: nextConfig, token: probeAuth.token, password: probeAuth.password }, runtime); } logNonInteractiveOnboardingJson({ opts, runtime, mode, workspaceDir, authChoice, gateway: { port: gatewayResult.port, bind: gatewayResult.bind, authMode: gatewayResult.authMode, tailscaleMode: gatewayResult.tailscaleMode }, installDaemon: Boolean(opts.installDaemon), daemonInstall: daemonInstallStatus, daemonRuntime: opts.installDaemon ? daemonRuntimeRaw : void 0, skipSkills: Boolean(opts.skipSkills), skipHealth: Boolean(opts.skipHealth) }); if (!opts.json) runtime.log(`Tip: run \`${formatCliCommand("openclaw configure --section web")}\` to store your Brave API key for web_search. Docs: https://docs.openclaw.ai/tools/web`); } //#endregion //#region src/commands/onboard-non-interactive/remote.ts /** * Remote non-interactive onboarding orchestration. * * It writes gateway.remote config without local gateway setup, preserving the * same config commit path as local onboarding. */ /** Runs non-interactive setup for clients that connect to an existing remote gateway. */ async function runNonInteractiveRemoteSetup(params) { const { opts, runtime, baseConfig, baseHash } = params; const mode = "remote"; const remoteUrl = normalizeOptionalString(opts.remoteUrl); if (!remoteUrl) { runtime.error(`Missing --remote-url for remote mode. Example: ${formatCliCommand("openclaw onboard --non-interactive --mode remote --remote-url ws://127.0.0.1:3000")}.`); runtime.exit(1); return; } let nextConfig = { ...baseConfig, gateway: { ...baseConfig.gateway, mode: "remote", remote: { url: remoteUrl, token: normalizeOptionalString(opts.remoteToken) } } }; if (opts.skipBootstrap) nextConfig = applySkipBootstrapConfig(nextConfig); nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode }); await commitNonInteractiveOnboardConfig({ nextConfig, baseConfig, baseHash, reset: opts.reset }); logConfigUpdated(runtime); const payload = { mode, remoteUrl, auth: opts.remoteToken ? "token" : "none" }; if (opts.json) writeRuntimeJson(runtime, payload); else { runtime.log(`Remote gateway: ${remoteUrl}`); runtime.log(`Auth: ${payload.auth}`); runtime.log(`Tip: run \`${formatCliCommand("openclaw configure --section web")}\` to store your Brave API key for web_search. Docs: https://docs.openclaw.ai/tools/web`); } } //#endregion //#region src/commands/onboard-non-interactive.ts /** * Non-interactive onboarding command dispatcher. * * This module validates the existing config snapshot, routes local/remote * setup, and handles explicit migration imports without interactive prompts. */ /** Runs a setup migration import with non-interactive prompt failures. */ async function runNonInteractiveMigrationImport(params) { const providerId = params.opts.importFrom?.trim(); if (!providerId) { params.runtime.error(`--import-from is required for non-interactive migration import. Run ${formatCliCommand("openclaw migrate list")} to choose a provider.`); params.runtime.exit(1); return; } const { detectSetupMigrationSources, runSetupMigrationImport } = await import("./setup.migration-import-DjJCW0n1.js"); const detections = await detectSetupMigrationSources({ config: params.baseConfig, runtime: params.runtime }); await runSetupMigrationImport({ opts: { ...params.opts, importFrom: providerId, nonInteractive: true }, baseConfig: params.baseConfig, detections, prompter: createNonInteractiveLoggingPrompter(params.runtime, (message) => `Non-interactive migration import needs explicit flags before prompting: ${message}`), runtime: params.runtime, async commitConfigFile(config) { await replaceConfigFile({ nextConfig: config, ...params.baseHash !== void 0 ? { baseHash: params.baseHash } : {}, writeOptions: { allowConfigSizeDrop: true } }); logConfigUpdated(params.runtime); return config; } }); } /** Runs non-interactive onboarding in local, remote, or migration-import mode. */ async function runNonInteractiveSetup(opts, runtime = defaultRuntime) { const snapshot = await readConfigFileSnapshot(); if (snapshot.exists && !snapshot.valid) { runtime.error(`Config invalid. Run \`${formatCliCommand("openclaw doctor")}\` to repair it, then re-run setup.`); runtime.exit(1); return; } const baseConfig = snapshot.valid ? snapshot.exists ? snapshot.sourceConfig ?? snapshot.config : {} : {}; const mode = opts.mode ?? "local"; if (mode !== "local" && mode !== "remote") { runtime.error(`Invalid --mode "${String(mode)}". Use "local" or "remote", or run ${formatCliCommand("openclaw onboard")} for interactive setup.`); runtime.exit(1); return; } if (opts.importFrom || opts.importSource || opts.importSecrets || opts.flow === "import") { await runNonInteractiveMigrationImport({ opts, runtime, baseConfig, baseHash: snapshot.hash }); return; } if (mode === "remote") { await runNonInteractiveRemoteSetup({ opts, runtime, baseConfig, baseHash: snapshot.hash }); return; } await runNonInteractiveLocalSetup({ opts, runtime, baseConfig, baseHash: snapshot.hash }); } //#endregion //#region src/commands/onboard.ts /** * Top-level `openclaw onboard` command entrypoint. * * It validates global setup flags, performs optional reset handling, and then * routes to interactive or non-interactive onboarding. */ const VALID_RESET_SCOPES = new Set([ "config", "config+creds+sessions", "full" ]); /** Runs the onboard command after normalizing legacy flags and setup mode. */ async function setupWizardCommand(opts, runtime = defaultRuntime) { assertSupportedRuntime(runtime); const originalAuthChoice = opts.authChoice; const normalizedAuthChoice = normalizeLegacyOnboardAuthChoice(originalAuthChoice, { env: process.env }); if (opts.nonInteractive && isDeprecatedAuthChoice(originalAuthChoice, { env: process.env })) { runtime.error(formatDeprecatedNonInteractiveAuthChoiceError(originalAuthChoice, { env: process.env })); runtime.exit(1); return; } if (isDeprecatedAuthChoice(originalAuthChoice, { env: process.env })) runtime.log(resolveDeprecatedAuthChoiceReplacement(originalAuthChoice, { env: process.env }).message); const flow = opts.flow === "manual" ? "advanced" : opts.flow; const normalizedOpts = normalizedAuthChoice === opts.authChoice && flow === opts.flow ? opts : { ...opts, authChoice: normalizedAuthChoice, flow }; if (normalizedOpts.secretInputMode && normalizedOpts.secretInputMode !== "plaintext" && normalizedOpts.secretInputMode !== "ref") { runtime.error(`Invalid --secret-input-mode. Use "plaintext" or "ref", or run ${formatCliCommand("openclaw onboard")} for the interactive setup.`); runtime.exit(1); return; } if (normalizedOpts.resetScope && !VALID_RESET_SCOPES.has(normalizedOpts.resetScope)) { runtime.error(`Invalid --reset-scope. Use "config", "config+creds+sessions", or "full". Run ${formatCliCommand("openclaw onboard --reset --reset-scope config")} for a config-only reset.`); runtime.exit(1); return; } if (normalizedOpts.nonInteractive && normalizedOpts.acceptRisk !== true) { runtime.error([ "Non-interactive setup requires explicit risk acknowledgement.", "Read: https://docs.openclaw.ai/security", `Re-run with: ${formatCliCommand("openclaw onboard --non-interactive --accept-risk ...")}` ].join("\n")); runtime.exit(1); return; } if (normalizedOpts.reset) { const snapshot = await readConfigFileSnapshot(); const baseConfig = snapshot.valid ? snapshot.sourceConfig ?? snapshot.config : {}; const workspaceDefault = normalizedOpts.workspace ?? baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE; await handleReset(normalizedOpts.resetScope ?? "config+creds+sessions", resolveUserPath(workspaceDefault), runtime); } if (process.platform === "win32") runtime.log([ "Windows detected - OpenClaw runs great on WSL2!", "Native Windows might be trickier.", "Quick setup: wsl --install (one command, one reboot)", "Guide: https://docs.openclaw.ai/windows" ].join("\n")); if (normalizedOpts.nonInteractive) { await runNonInteractiveSetup(normalizedOpts, runtime); return; } await runInteractiveSetup(normalizedOpts, runtime); } //#endregion export { setupWizardCommand };