import { ensureAuthProfileStore } from "../agents/auth-profiles.js"; import { listChannelPlugins } from "../channels/plugins/index.js"; import { applyAuthChoice, resolvePreferredProviderForAuthChoice, warnIfModelConfigLooksOff, } from "../commands/auth-choice.js"; import { promptAuthChoiceGrouped } from "../commands/auth-choice-prompt.js"; import { applyPrimaryModel, promptDefaultModel } from "../commands/model-picker.js"; import { setupChannels } from "../commands/onboard-channels.js"; import { applyWizardMetadata, DEFAULT_WORKSPACE, ensureWorkspaceAndSessions, handleReset, printWizardHeader, probeGatewayReachable, summarizeExistingConfig, } from "../commands/onboard-helpers.js"; import { promptRemoteGatewayConfig } from "../commands/onboard-remote.js"; import { setupSkills } from "../commands/onboard-skills.js"; import { setupInternalHooks } from "../commands/onboard-hooks.js"; import type { GatewayAuthChoice, OnboardMode, OnboardOptions, ResetScope, } from "../commands/onboard-types.js"; import type { ClawdbotConfig } from "../config/config.js"; import { CONFIG_PATH_CLAWDBOT, DEFAULT_GATEWAY_PORT, readConfigFileSnapshot, resolveGatewayPort, writeConfigFile, } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { resolveUserPath } from "../utils.js"; import { finalizeOnboardingWizard } from "./onboarding.finalize.js"; import { configureGatewayForOnboarding } from "./onboarding.gateway-config.js"; import type { QuickstartGatewayDefaults, WizardFlow } from "./onboarding.types.js"; import { WizardCancelledError, type WizardPrompter } from "./prompts.js"; async function requireRiskAcknowledgement(params: { opts: OnboardOptions; prompter: WizardPrompter; }) { if (params.opts.acceptRisk === true) return; await params.prompter.note( [ "Please read: https://docs.clawd.bot/security", "", "Clawdbot agents can run commands, read/write files, and act through any tools you enable. They can only send messages on channels you configure (for example, an account you log in on this machine, or a bot account like Slack/Discord).", "", "If you’re new to this, start with the sandbox and least privilege. It helps limit what an agent can do if it’s tricked or makes a mistake.", "Learn more: https://docs.clawd.bot/sandboxing", ].join("\n"), "Security", ); const ok = await params.prompter.confirm({ message: "I understand this is powerful and inherently risky. Continue?", initialValue: false, }); if (!ok) { throw new WizardCancelledError("risk not accepted"); } } export async function runOnboardingWizard( opts: OnboardOptions, runtime: RuntimeEnv = defaultRuntime, prompter: WizardPrompter, ) { printWizardHeader(runtime); await prompter.intro("Clawdbot onboarding"); await requireRiskAcknowledgement({ opts, prompter }); const snapshot = await readConfigFileSnapshot(); let baseConfig: ClawdbotConfig = snapshot.valid ? snapshot.config : {}; if (snapshot.exists) { const title = snapshot.valid ? "Existing config detected" : "Invalid config"; await prompter.note(summarizeExistingConfig(baseConfig), title); if (!snapshot.valid && snapshot.issues.length > 0) { await prompter.note( [ ...snapshot.issues.map((iss) => `- ${iss.path}: ${iss.message}`), "", "Docs: https://docs.clawd.bot/gateway/configuration", ].join("\n"), "Config issues", ); } if (!snapshot.valid) { await prompter.outro( "Config invalid. Run `clawdbot doctor` to repair it, then re-run onboarding.", ); runtime.exit(1); return; } const action = (await prompter.select({ message: "Config handling", options: [ { value: "keep", label: "Use existing values" }, { value: "modify", label: "Update values" }, { value: "reset", label: "Reset" }, ], })) as "keep" | "modify" | "reset"; if (action === "reset") { const workspaceDefault = baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE; const resetScope = (await prompter.select({ message: "Reset scope", options: [ { value: "config", label: "Config only" }, { value: "config+creds+sessions", label: "Config + creds + sessions", }, { value: "full", label: "Full reset (config + creds + sessions + workspace)", }, ], })) as ResetScope; await handleReset(resetScope, resolveUserPath(workspaceDefault), runtime); baseConfig = {}; } } const quickstartHint = "Configure details later via clawdbot configure."; const advancedHint = "Configure port, network, Tailscale, and auth options."; const explicitFlowRaw = opts.flow?.trim(); if (explicitFlowRaw && explicitFlowRaw !== "quickstart" && explicitFlowRaw !== "advanced") { runtime.error("Invalid --flow (use quickstart or advanced)."); runtime.exit(1); return; } const explicitFlow: WizardFlow | undefined = explicitFlowRaw === "quickstart" || explicitFlowRaw === "advanced" ? explicitFlowRaw : undefined; let flow: WizardFlow = explicitFlow ?? ((await prompter.select({ message: "Onboarding mode", options: [ { value: "quickstart", label: "QuickStart", hint: quickstartHint }, { value: "advanced", label: "Advanced", hint: advancedHint }, ], initialValue: "quickstart", })) as "quickstart" | "advanced"); if (opts.mode === "remote" && flow === "quickstart") { await prompter.note( "QuickStart only supports local gateways. Switching to Advanced mode.", "QuickStart", ); flow = "advanced"; } const quickstartGateway: QuickstartGatewayDefaults = (() => { const hasExisting = typeof baseConfig.gateway?.port === "number" || baseConfig.gateway?.bind !== undefined || baseConfig.gateway?.auth?.mode !== undefined || baseConfig.gateway?.auth?.token !== undefined || baseConfig.gateway?.auth?.password !== undefined || baseConfig.gateway?.customBindHost !== undefined || baseConfig.gateway?.tailscale?.mode !== undefined; const bindRaw = baseConfig.gateway?.bind; const bind = bindRaw === "loopback" || bindRaw === "lan" || bindRaw === "auto" || bindRaw === "custom" ? bindRaw : "loopback"; let authMode: GatewayAuthChoice = "token"; if ( baseConfig.gateway?.auth?.mode === "token" || baseConfig.gateway?.auth?.mode === "password" ) { authMode = baseConfig.gateway.auth.mode; } else if (baseConfig.gateway?.auth?.token) { authMode = "token"; } else if (baseConfig.gateway?.auth?.password) { authMode = "password"; } const tailscaleRaw = baseConfig.gateway?.tailscale?.mode; const tailscaleMode = tailscaleRaw === "off" || tailscaleRaw === "serve" || tailscaleRaw === "funnel" ? tailscaleRaw : "off"; return { hasExisting, port: resolveGatewayPort(baseConfig), bind, authMode, tailscaleMode, token: baseConfig.gateway?.auth?.token, password: baseConfig.gateway?.auth?.password, customBindHost: baseConfig.gateway?.customBindHost, tailscaleResetOnExit: baseConfig.gateway?.tailscale?.resetOnExit ?? false, }; })(); if (flow === "quickstart") { const formatBind = (value: "loopback" | "lan" | "auto" | "custom") => { if (value === "loopback") return "Loopback (127.0.0.1)"; if (value === "lan") return "LAN"; if (value === "custom") return "Custom IP"; return "Auto"; }; const formatAuth = (value: GatewayAuthChoice) => { if (value === "off") return "Off (loopback only)"; if (value === "token") return "Token (default)"; return "Password"; }; const formatTailscale = (value: "off" | "serve" | "funnel") => { if (value === "off") return "Off"; if (value === "serve") return "Serve"; return "Funnel"; }; const quickstartLines = quickstartGateway.hasExisting ? [ "Keeping your current gateway settings:", `Gateway port: ${quickstartGateway.port}`, `Gateway bind: ${formatBind(quickstartGateway.bind)}`, ...(quickstartGateway.bind === "custom" && quickstartGateway.customBindHost ? [`Gateway custom IP: ${quickstartGateway.customBindHost}`] : []), `Gateway auth: ${formatAuth(quickstartGateway.authMode)}`, `Tailscale exposure: ${formatTailscale(quickstartGateway.tailscaleMode)}`, "Direct to chat channels.", ] : [ `Gateway port: ${DEFAULT_GATEWAY_PORT}`, "Gateway bind: Loopback (127.0.0.1)", "Gateway auth: Token (default)", "Tailscale exposure: Off", "Direct to chat channels.", ]; await prompter.note(quickstartLines.join("\n"), "QuickStart"); } const localPort = resolveGatewayPort(baseConfig); const localUrl = `ws://127.0.0.1:${localPort}`; const localProbe = await probeGatewayReachable({ url: localUrl, token: baseConfig.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN, password: baseConfig.gateway?.auth?.password ?? process.env.CLAWDBOT_GATEWAY_PASSWORD, }); const remoteUrl = baseConfig.gateway?.remote?.url?.trim() ?? ""; const remoteProbe = remoteUrl ? await probeGatewayReachable({ url: remoteUrl, token: baseConfig.gateway?.remote?.token, }) : null; const mode = opts.mode ?? (flow === "quickstart" ? "local" : ((await prompter.select({ message: "What do you want to set up?", options: [ { value: "local", label: "Local gateway (this machine)", hint: localProbe.ok ? `Gateway reachable (${localUrl})` : `No gateway detected (${localUrl})`, }, { value: "remote", label: "Remote gateway (info-only)", hint: !remoteUrl ? "No remote URL configured yet" : remoteProbe?.ok ? `Gateway reachable (${remoteUrl})` : `Configured but unreachable (${remoteUrl})`, }, ], })) as OnboardMode)); if (mode === "remote") { let nextConfig = await promptRemoteGatewayConfig(baseConfig, prompter); nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode }); await writeConfigFile(nextConfig); runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`); await prompter.outro("Remote gateway configured."); return; } const workspaceInput = opts.workspace ?? (flow === "quickstart" ? (baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE) : await prompter.text({ message: "Workspace directory", initialValue: baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE, })); const workspaceDir = resolveUserPath(workspaceInput.trim() || DEFAULT_WORKSPACE); let nextConfig: ClawdbotConfig = { ...baseConfig, agents: { ...baseConfig.agents, defaults: { ...baseConfig.agents?.defaults, workspace: workspaceDir, }, }, gateway: { ...baseConfig.gateway, mode: "local", }, }; const authStore = ensureAuthProfileStore(undefined, { allowKeychainPrompt: false, }); const authChoiceFromPrompt = opts.authChoice === undefined; const authChoice = opts.authChoice ?? (await promptAuthChoiceGrouped({ prompter, store: authStore, includeSkip: true, includeClaudeCliIfMissing: true, })); const authResult = await applyAuthChoice({ authChoice, config: nextConfig, prompter, runtime, setDefaultModel: true, }); nextConfig = authResult.config; if (authChoiceFromPrompt) { const modelSelection = await promptDefaultModel({ config: nextConfig, prompter, allowKeep: true, ignoreAllowlist: true, preferredProvider: resolvePreferredProviderForAuthChoice(authChoice), }); if (modelSelection.model) { nextConfig = applyPrimaryModel(nextConfig, modelSelection.model); } } await warnIfModelConfigLooksOff(nextConfig, prompter); const gateway = await configureGatewayForOnboarding({ flow, baseConfig, nextConfig, localPort, quickstartGateway, prompter, runtime, }); nextConfig = gateway.nextConfig; const settings = gateway.settings; if (opts.skipChannels ?? opts.skipProviders) { await prompter.note("Skipping channel setup.", "Channels"); } else { const quickstartAllowFromChannels = flow === "quickstart" ? listChannelPlugins() .filter((plugin) => plugin.meta.quickstartAllowFrom) .map((plugin) => plugin.id) : []; nextConfig = await setupChannels(nextConfig, runtime, prompter, { allowSignalInstall: true, forceAllowFromChannels: quickstartAllowFromChannels, skipDmPolicyPrompt: flow === "quickstart", skipConfirm: flow === "quickstart", quickstartDefaults: flow === "quickstart", }); } await writeConfigFile(nextConfig); runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`); await ensureWorkspaceAndSessions(workspaceDir, runtime, { skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap), }); if (opts.skipSkills) { await prompter.note("Skipping skills setup.", "Skills"); } else { nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter); } // Setup hooks (session memory on /new) nextConfig = await setupInternalHooks(nextConfig, runtime, prompter); nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode }); await writeConfigFile(nextConfig); await finalizeOnboardingWizard({ flow, opts, baseConfig, nextConfig, workspaceDir, settings, prompter, runtime, }); }