import { intro as clackIntro, outro as clackOutro } from "@clack/prompts"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; import { getModelRefStatus, resolveConfiguredModelRef, resolveHooksGmailModel, } from "../agents/model-selection.js"; import type { ClawdbotConfig } from "../config/config.js"; import { CONFIG_PATH_CLAWDBOT, writeConfigFile } from "../config/config.js"; import { resolveGatewayService } from "../daemon/service.js"; import { buildGatewayConnectionDetails } from "../gateway/call.js"; import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { note } from "../terminal/note.js"; import { stylePromptTitle } from "../terminal/prompt-style.js"; import { maybeRepairAnthropicOAuthProfileId, noteAuthProfileHealth } from "./doctor-auth.js"; import { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js"; import { maybeRepairGatewayDaemon } from "./doctor-gateway-daemon-flow.js"; import { checkGatewayHealth } from "./doctor-gateway-health.js"; import { maybeMigrateLegacyGatewayService, maybeRepairGatewayServiceConfig, maybeScanExtraGatewayServices, } from "./doctor-gateway-services.js"; import { noteSourceInstallIssues } from "./doctor-install.js"; import { noteMacLaunchAgentOverrides } from "./doctor-platform-notes.js"; import { createDoctorPrompter, type DoctorOptions } from "./doctor-prompter.js"; import { maybeRepairSandboxImages, noteSandboxScopeWarnings } from "./doctor-sandbox.js"; import { noteSecurityWarnings } from "./doctor-security.js"; import { noteStateIntegrity, noteWorkspaceBackupTip } from "./doctor-state-integrity.js"; import { detectLegacyStateMigrations, runLegacyStateMigrations, } from "./doctor-state-migrations.js"; import { maybeRepairUiProtocolFreshness } from "./doctor-ui.js"; import { maybeOfferUpdateBeforeDoctor } from "./doctor-update.js"; import { MEMORY_SYSTEM_PROMPT, shouldSuggestMemorySystem } from "./doctor-workspace.js"; import { noteWorkspaceStatus } from "./doctor-workspace-status.js"; import { applyWizardMetadata, printWizardHeader, randomToken } from "./onboard-helpers.js"; import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js"; const intro = (message: string) => clackIntro(stylePromptTitle(message) ?? message); const outro = (message: string) => clackOutro(stylePromptTitle(message) ?? message); function resolveMode(cfg: ClawdbotConfig): "local" | "remote" { return cfg.gateway?.mode === "remote" ? "remote" : "local"; } export async function doctorCommand( runtime: RuntimeEnv = defaultRuntime, options: DoctorOptions = {}, ) { const prompter = createDoctorPrompter({ runtime, options }); printWizardHeader(runtime); intro("Clawdbot doctor"); const root = await resolveClawdbotPackageRoot({ moduleUrl: import.meta.url, argv1: process.argv[1], cwd: process.cwd(), }); const updateResult = await maybeOfferUpdateBeforeDoctor({ runtime, options, root, confirm: (p) => prompter.confirm(p), outro, }); if (updateResult.handled) return; await maybeRepairUiProtocolFreshness(runtime, prompter); noteSourceInstallIssues(root); const configResult = await loadAndMaybeMigrateDoctorConfig({ options, confirm: (p) => prompter.confirm(p), }); let cfg: ClawdbotConfig = configResult.cfg; cfg = await maybeRepairAnthropicOAuthProfileId(cfg, prompter); await noteAuthProfileHealth({ cfg, prompter, allowKeychainPrompt: options.nonInteractive !== true && Boolean(process.stdin.isTTY), }); const gatewayDetails = buildGatewayConnectionDetails({ config: cfg }); if (gatewayDetails.remoteFallbackNote) { note(gatewayDetails.remoteFallbackNote, "Gateway"); } if (resolveMode(cfg) === "local") { const authMode = cfg.gateway?.auth?.mode; const token = typeof cfg.gateway?.auth?.token === "string" ? cfg.gateway?.auth?.token.trim() : ""; const needsToken = authMode !== "password" && (authMode !== "token" || !token); if (needsToken) { note( "Gateway auth is off or missing a token. Token auth is now the recommended default (including loopback).", "Gateway auth", ); const shouldSetToken = options.generateGatewayToken === true ? true : options.nonInteractive === true ? false : await prompter.confirmRepair({ message: "Generate and configure a gateway token now?", initialValue: true, }); if (shouldSetToken) { const nextToken = randomToken(); cfg = { ...cfg, gateway: { ...cfg.gateway, auth: { ...cfg.gateway?.auth, mode: "token", token: nextToken, }, }, }; note("Gateway token configured.", "Gateway auth"); } } } const legacyState = await detectLegacyStateMigrations({ cfg }); if (legacyState.preview.length > 0) { note(legacyState.preview.join("\n"), "Legacy state detected"); const migrate = options.nonInteractive === true ? true : await prompter.confirm({ message: "Migrate legacy state (sessions/agent/WhatsApp auth) now?", initialValue: true, }); if (migrate) { const migrated = await runLegacyStateMigrations({ detected: legacyState, }); if (migrated.changes.length > 0) { note(migrated.changes.join("\n"), "Doctor changes"); } if (migrated.warnings.length > 0) { note(migrated.warnings.join("\n"), "Doctor warnings"); } } } await noteStateIntegrity(cfg, prompter, configResult.path ?? CONFIG_PATH_CLAWDBOT); cfg = await maybeRepairSandboxImages(cfg, runtime, prompter); noteSandboxScopeWarnings(cfg); await maybeMigrateLegacyGatewayService(cfg, resolveMode(cfg), runtime, prompter); await maybeScanExtraGatewayServices(options); await maybeRepairGatewayServiceConfig(cfg, resolveMode(cfg), runtime, prompter); await noteMacLaunchAgentOverrides(); await noteSecurityWarnings(cfg); if (cfg.hooks?.gmail?.model?.trim()) { const hooksModelRef = resolveHooksGmailModel({ cfg, defaultProvider: DEFAULT_PROVIDER, }); if (!hooksModelRef) { note(`- hooks.gmail.model "${cfg.hooks.gmail.model}" could not be resolved`, "Hooks"); } else { const { provider: defaultProvider, model: defaultModel } = resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); const catalog = await loadModelCatalog({ config: cfg }); const status = getModelRefStatus({ cfg, catalog, ref: hooksModelRef, defaultProvider, defaultModel, }); const warnings: string[] = []; if (!status.allowed) { warnings.push( `- hooks.gmail.model "${status.key}" not in agents.defaults.models allowlist (will use primary instead)`, ); } if (!status.inCatalog) { warnings.push( `- hooks.gmail.model "${status.key}" not in the model catalog (may fail at runtime)`, ); } if (warnings.length > 0) { note(warnings.join("\n"), "Hooks"); } } } if ( options.nonInteractive !== true && process.platform === "linux" && resolveMode(cfg) === "local" ) { const service = resolveGatewayService(); let loaded = false; try { loaded = await service.isLoaded({ env: process.env }); } catch { loaded = false; } if (loaded) { await ensureSystemdUserLingerInteractive({ runtime, prompter: { confirm: async (p) => prompter.confirm(p), note, }, reason: "Gateway runs as a systemd user service. Without lingering, systemd stops the user session on logout/idle and kills the Gateway.", requireConfirm: true, }); } } noteWorkspaceStatus(cfg); const { healthOk } = await checkGatewayHealth({ runtime, cfg }); await maybeRepairGatewayDaemon({ cfg, runtime, prompter, options, gatewayDetailsMessage: gatewayDetails.message, healthOk, }); cfg = applyWizardMetadata(cfg, { command: "doctor", mode: resolveMode(cfg) }); await writeConfigFile(cfg); runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`); if (options.workspaceSuggestions !== false) { const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)); noteWorkspaceBackupTip(workspaceDir); if (await shouldSuggestMemorySystem(workspaceDir)) { note(MEMORY_SYSTEM_PROMPT, "Workspace"); } } outro("Doctor complete."); }