import type { ClawdbotConfig } from "../config/config.js"; import { CONFIG_PATH_CLAWDBOT, readConfigFileSnapshot, resolveGatewayPort, writeConfigFile, } from "../config/config.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, sleep } from "../utils.js"; import { createClackPrompter } from "../wizard/clack-prompter.js"; import { WizardCancelledError } from "../wizard/prompts.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, intro, outro, select, text, } from "./configure.shared.js"; import { healthCommand } from "./health.js"; import { formatHealthCheckFailure } from "./health-format.js"; import { setupChannels } from "./onboard-channels.js"; import { applyWizardMetadata, DEFAULT_WORKSPACE, ensureWorkspaceAndSessions, guardCancel, printWizardHeader, probeGatewayReachable, resolveControlUiLinks, summarizeExistingConfig, } 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: "Select sections to configure", options: [ ...CONFIGURE_SECTION_OPTIONS, { value: "__continue", label: "Continue", hint: hasSelection ? "Done" : "Skip for now", }, ], initialValue: CONFIGURE_SECTION_OPTIONS[0]?.value, }), runtime, ); } async function promptChannelMode( runtime: RuntimeEnv, ): Promise { return guardCancel( await select({ message: "Channels", options: [ { value: "configure", label: "Configure/link", hint: "Add/update channels; disable unselected accounts", }, { value: "remove", label: "Remove channel config", hint: "Delete channel tokens/settings from clawdbot.json", }, ], initialValue: "configure", }), runtime, ) as ChannelsWizardMode; } export async function runConfigureWizard( opts: ConfigureWizardParams, runtime: RuntimeEnv = defaultRuntime, ) { try { printWizardHeader(runtime); intro( opts.command === "update" ? "Clawdbot update wizard" : "Clawdbot configure", ); const prompter = createClackPrompter(); const snapshot = await readConfigFileSnapshot(); const baseConfig: ClawdbotConfig = 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.clawd.bot/gateway/configuration", ].join("\n"), "Config issues", ); } if (!snapshot.valid) { outro( "Config invalid. Run `clawdbot 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.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 = guardCancel( await select({ message: "Where will the Gateway run?", options: [ { value: "local", label: "Local (this machine)", hint: localProbe.ok ? `Gateway reachable (${localUrl})` : `No gateway detected (${localUrl})`, }, { value: "remote", label: "Remote (info-only)", hint: !remoteUrl ? "No remote URL configured yet" : remoteProbe?.ok ? `Gateway reachable (${remoteUrl})` : `Configured but unreachable (${remoteUrl})`, }, ], }), runtime, ) as "local" | "remote"; if (mode === "remote") { let remoteConfig = await promptRemoteGatewayConfig(baseConfig, prompter); remoteConfig = applyWizardMetadata(remoteConfig, { command: opts.command, mode, }); await writeConfigFile(remoteConfig); runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`); outro("Remote gateway configured."); return; } let nextConfig = { ...baseConfig }; 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.CLAWDBOT_GATEWAY_TOKEN; const persistConfig = async () => { nextConfig = applyWizardMetadata(nextConfig, { command: opts.command, mode, }); await writeConfigFile(nextConfig); runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`); }; if (opts.sections) { const selected = opts.sections; if (!selected || selected.length === 0) { outro("No changes selected."); 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("gateway")) { const gateway = await promptGatewayConfig(nextConfig, runtime); nextConfig = gateway.config; gatewayPort = gateway.port; gatewayToken = gateway.token; } if (selected.includes("channels")) { const channelMode = await promptChannelMode(runtime); if (channelMode === "configure") { nextConfig = await setupChannels(nextConfig, runtime, prompter, { allowDisable: true, allowSignalInstall: 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 daemon 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")) { await sleep(1000); try { await healthCommand({ json: false, timeoutMs: 10_000 }, runtime); } catch (err) { runtime.error(formatHealthCheckFailure(err)); note( [ "Docs:", "https://docs.clawd.bot/gateway/health", "https://docs.clawd.bot/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 === "gateway") { const gateway = await promptGatewayConfig(nextConfig, runtime); nextConfig = gateway.config; gatewayPort = gateway.port; gatewayToken = gateway.token; didConfigureGateway = true; await persistConfig(); } if (choice === "channels") { const channelMode = await promptChannelMode(runtime); if (channelMode === "configure") { nextConfig = await setupChannels(nextConfig, runtime, prompter, { allowDisable: true, allowSignalInstall: 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 daemon 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") { await sleep(1000); try { await healthCommand({ json: false, timeoutMs: 10_000 }, runtime); } catch (err) { runtime.error(formatHealthCheckFailure(err)); note( [ "Docs:", "https://docs.clawd.bot/gateway/health", "https://docs.clawd.bot/gateway/troubleshooting", ].join("\n"), "Health check help", ); } } } if (!ranSection) { outro("No changes selected."); 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.CLAWDBOT_GATEWAY_PASSWORD; const oldPassword = baseConfig.gateway?.auth?.password ?? process.env.CLAWDBOT_GATEWAY_PASSWORD; const token = nextConfig.gateway?.auth?.token ?? process.env.CLAWDBOT_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.clawd.bot/web/control-ui", ].join("\n"), "Control UI", ); outro("Configure complete."); } catch (err) { if (err instanceof WizardCancelledError) { runtime.exit(0); return; } throw err; } }