import { CLAUDE_CLI_PROFILE_ID, CODEX_CLI_PROFILE_ID, loadAuthProfileStore, } from "../agents/auth-profiles.js"; import { withProgress } from "../cli/progress.js"; import { type ClawdbotConfig, readConfigFileSnapshot, writeConfigFile, } from "../config/config.js"; import { listDiscordAccountIds, resolveDiscordAccount, } from "../discord/accounts.js"; import { callGateway } from "../gateway/call.js"; import { listIMessageAccountIds, resolveIMessageAccount, } from "../imessage/accounts.js"; import { formatUsageReportLines, loadProviderUsageSummary, } from "../infra/provider-usage.js"; import { type ChatProviderId, getChatProviderMeta, listChatProviders, normalizeChatProviderId, } from "../providers/registry.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId, } from "../routing/session-key.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { listSignalAccountIds, resolveSignalAccount, } from "../signal/accounts.js"; import { listSlackAccountIds, resolveSlackAccount } from "../slack/accounts.js"; import { listTelegramAccountIds, resolveTelegramAccount, } from "../telegram/accounts.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; import { listWhatsAppAccountIds, resolveWhatsAppAuthDir, } from "../web/accounts.js"; import { webAuthExists } from "../web/session.js"; import { createClackPrompter } from "../wizard/clack-prompter.js"; import { setupProviders } from "./onboard-providers.js"; import type { ProviderChoice } from "./onboard-types.js"; type ChatProvider = ChatProviderId; type ProvidersListOptions = { json?: boolean; usage?: boolean; }; type ProvidersStatusOptions = { json?: boolean; probe?: boolean; timeout?: string; }; export type ProvidersAddOptions = { provider?: string; account?: string; name?: string; token?: string; tokenFile?: string; botToken?: string; appToken?: string; signalNumber?: string; cliPath?: string; dbPath?: string; service?: "imessage" | "sms" | "auto"; region?: string; authDir?: string; httpUrl?: string; httpHost?: string; httpPort?: string; useEnv?: boolean; }; export type ProvidersRemoveOptions = { provider?: string; account?: string; delete?: boolean; }; async function requireValidConfig( runtime: RuntimeEnv, ): Promise { const snapshot = await readConfigFileSnapshot(); if (snapshot.exists && !snapshot.valid) { const issues = snapshot.issues.length > 0 ? snapshot.issues .map((issue) => `- ${issue.path}: ${issue.message}`) .join("\n") : "Unknown validation issue."; runtime.error(`Config invalid:\n${issues}`); runtime.error("Fix the config or run clawdbot doctor."); runtime.exit(1); return null; } return snapshot.config; } function formatAccountLabel(params: { accountId: string; name?: string }) { const base = params.accountId || DEFAULT_ACCOUNT_ID; if (params.name?.trim()) return `${base} (${params.name.trim()})`; return base; } const providerLabel = (provider: ChatProvider) => getChatProviderMeta(provider).label; const colorValue = (value: string) => { if (value === "none") return theme.error(value); if (value === "env") return theme.accent(value); return theme.success(value); }; function formatEnabled(value: boolean | undefined): string { return value === false ? theme.error("disabled") : theme.success("enabled"); } function formatConfigured(value: boolean): string { return value ? theme.success("configured") : theme.warn("not configured"); } function formatTokenSource(source?: string): string { const value = source || "none"; return `token=${colorValue(value)}`; } function formatSource(label: string, source?: string): string { const value = source || "none"; return `${label}=${colorValue(value)}`; } function formatLinked(value: boolean): string { return value ? theme.success("linked") : theme.warn("not linked"); } function shouldUseWizard(params?: { hasFlags?: boolean }) { return params?.hasFlags === false; } function providerHasAccounts(cfg: ClawdbotConfig, provider: ChatProvider) { if (provider === "whatsapp") return true; const base = (cfg as Record)[provider] as | { accounts?: Record } | undefined; return Boolean(base?.accounts && Object.keys(base.accounts).length > 0); } function shouldStoreNameInAccounts( cfg: ClawdbotConfig, provider: ChatProvider, accountId: string, ): boolean { if (provider === "whatsapp") return true; if (accountId !== DEFAULT_ACCOUNT_ID) return true; return providerHasAccounts(cfg, provider); } function migrateBaseNameToDefaultAccount( cfg: ClawdbotConfig, provider: ChatProvider, ): ClawdbotConfig { if (provider === "whatsapp") return cfg; const base = (cfg as Record)[provider] as | { name?: string; accounts?: Record> } | undefined; const baseName = base?.name?.trim(); if (!baseName) return cfg; const accounts: Record> = { ...base?.accounts, }; const defaultAccount = accounts[DEFAULT_ACCOUNT_ID] ?? {}; if (!defaultAccount.name) { accounts[DEFAULT_ACCOUNT_ID] = { ...defaultAccount, name: baseName }; } const { name: _ignored, ...rest } = base ?? {}; return { ...cfg, [provider]: { ...rest, accounts, }, } as ClawdbotConfig; } function applyAccountName(params: { cfg: ClawdbotConfig; provider: ChatProvider; accountId: string; name?: string; }): ClawdbotConfig { const trimmed = params.name?.trim(); if (!trimmed) return params.cfg; const accountId = normalizeAccountId(params.accountId); if (params.provider === "whatsapp") { return { ...params.cfg, whatsapp: { ...params.cfg.whatsapp, accounts: { ...params.cfg.whatsapp?.accounts, [accountId]: { ...params.cfg.whatsapp?.accounts?.[accountId], name: trimmed, }, }, }, }; } const key = params.provider; const useAccounts = shouldStoreNameInAccounts(params.cfg, key, accountId); if (!useAccounts && accountId === DEFAULT_ACCOUNT_ID) { const baseConfig = (params.cfg as Record)[key]; const safeBase = typeof baseConfig === "object" && baseConfig ? (baseConfig as Record) : {}; return { ...params.cfg, [key]: { ...safeBase, name: trimmed, }, } as ClawdbotConfig; } const base = (params.cfg as Record)[key] as | { name?: string; accounts?: Record> } | undefined; const baseAccounts: Record< string, Record > = base?.accounts ?? {}; const existingAccount = baseAccounts[accountId] ?? {}; const baseWithoutName = accountId === DEFAULT_ACCOUNT_ID ? (({ name: _ignored, ...rest }) => rest)(base ?? {}) : (base ?? {}); return { ...params.cfg, [key]: { ...baseWithoutName, accounts: { ...baseAccounts, [accountId]: { ...existingAccount, name: trimmed, }, }, }, } as ClawdbotConfig; } function applyProviderAccountConfig(params: { cfg: ClawdbotConfig; provider: ChatProvider; accountId: string; name?: string; token?: string; tokenFile?: string; botToken?: string; appToken?: string; signalNumber?: string; cliPath?: string; dbPath?: string; service?: "imessage" | "sms" | "auto"; region?: string; authDir?: string; httpUrl?: string; httpHost?: string; httpPort?: string; useEnv?: boolean; }): ClawdbotConfig { const accountId = normalizeAccountId(params.accountId); const name = params.name?.trim() || undefined; const namedConfig = applyAccountName({ cfg: params.cfg, provider: params.provider, accountId, name, }); const next = accountId !== DEFAULT_ACCOUNT_ID ? migrateBaseNameToDefaultAccount(namedConfig, params.provider) : namedConfig; if (params.provider === "whatsapp") { const entry = { ...next.whatsapp?.accounts?.[accountId], ...(params.authDir ? { authDir: params.authDir } : {}), enabled: true, }; return { ...next, whatsapp: { ...next.whatsapp, accounts: { ...next.whatsapp?.accounts, [accountId]: entry, }, }, }; } if (params.provider === "telegram") { if (accountId === DEFAULT_ACCOUNT_ID) { return { ...next, telegram: { ...next.telegram, enabled: true, ...(params.useEnv ? {} : params.tokenFile ? { tokenFile: params.tokenFile } : params.token ? { botToken: params.token } : {}), }, }; } return { ...next, telegram: { ...next.telegram, enabled: true, accounts: { ...next.telegram?.accounts, [accountId]: { ...next.telegram?.accounts?.[accountId], enabled: true, ...(params.tokenFile ? { tokenFile: params.tokenFile } : params.token ? { botToken: params.token } : {}), }, }, }, }; } if (params.provider === "discord") { if (accountId === DEFAULT_ACCOUNT_ID) { return { ...next, discord: { ...next.discord, enabled: true, ...(params.useEnv ? {} : params.token ? { token: params.token } : {}), }, }; } return { ...next, discord: { ...next.discord, enabled: true, accounts: { ...next.discord?.accounts, [accountId]: { ...next.discord?.accounts?.[accountId], enabled: true, ...(params.token ? { token: params.token } : {}), }, }, }, }; } if (params.provider === "slack") { if (accountId === DEFAULT_ACCOUNT_ID) { return { ...next, slack: { ...next.slack, enabled: true, ...(params.useEnv ? {} : { ...(params.botToken ? { botToken: params.botToken } : {}), ...(params.appToken ? { appToken: params.appToken } : {}), }), }, }; } return { ...next, slack: { ...next.slack, enabled: true, accounts: { ...next.slack?.accounts, [accountId]: { ...next.slack?.accounts?.[accountId], enabled: true, ...(params.botToken ? { botToken: params.botToken } : {}), ...(params.appToken ? { appToken: params.appToken } : {}), }, }, }, }; } if (params.provider === "signal") { if (accountId === DEFAULT_ACCOUNT_ID) { return { ...next, signal: { ...next.signal, enabled: true, ...(params.signalNumber ? { account: params.signalNumber } : {}), ...(params.cliPath ? { cliPath: params.cliPath } : {}), ...(params.httpUrl ? { httpUrl: params.httpUrl } : {}), ...(params.httpHost ? { httpHost: params.httpHost } : {}), ...(params.httpPort ? { httpPort: Number(params.httpPort) } : {}), }, }; } return { ...next, signal: { ...next.signal, enabled: true, accounts: { ...next.signal?.accounts, [accountId]: { ...next.signal?.accounts?.[accountId], enabled: true, ...(params.signalNumber ? { account: params.signalNumber } : {}), ...(params.cliPath ? { cliPath: params.cliPath } : {}), ...(params.httpUrl ? { httpUrl: params.httpUrl } : {}), ...(params.httpHost ? { httpHost: params.httpHost } : {}), ...(params.httpPort ? { httpPort: Number(params.httpPort) } : {}), }, }, }, }; } if (params.provider === "imessage") { if (accountId === DEFAULT_ACCOUNT_ID) { return { ...next, imessage: { ...next.imessage, enabled: true, ...(params.cliPath ? { cliPath: params.cliPath } : {}), ...(params.dbPath ? { dbPath: params.dbPath } : {}), ...(params.service ? { service: params.service } : {}), ...(params.region ? { region: params.region } : {}), }, }; } return { ...next, imessage: { ...next.imessage, enabled: true, accounts: { ...next.imessage?.accounts, [accountId]: { ...next.imessage?.accounts?.[accountId], enabled: true, ...(params.cliPath ? { cliPath: params.cliPath } : {}), ...(params.dbPath ? { dbPath: params.dbPath } : {}), ...(params.service ? { service: params.service } : {}), ...(params.region ? { region: params.region } : {}), }, }, }, }; } return next; } export async function providersListCommand( opts: ProvidersListOptions, runtime: RuntimeEnv = defaultRuntime, ) { const cfg = await requireValidConfig(runtime); if (!cfg) return; const includeUsage = opts.usage !== false; const whatsappAccounts = listWhatsAppAccountIds(cfg); const telegramAccounts = listTelegramAccountIds(cfg); const discordAccounts = listDiscordAccountIds(cfg); const slackAccounts = listSlackAccountIds(cfg); const signalAccounts = listSignalAccountIds(cfg); const imessageAccounts = listIMessageAccountIds(cfg); const authStore = loadAuthProfileStore(); const authProfiles = Object.entries(authStore.profiles).map( ([profileId, profile]) => ({ id: profileId, provider: profile.provider, type: profile.type, isExternal: profileId === CLAUDE_CLI_PROFILE_ID || profileId === CODEX_CLI_PROFILE_ID, }), ); if (opts.json) { const usage = includeUsage ? await loadProviderUsageSummary() : undefined; const payload = { chat: { whatsapp: whatsappAccounts, telegram: telegramAccounts, discord: discordAccounts, slack: slackAccounts, signal: signalAccounts, imessage: imessageAccounts, }, auth: authProfiles, ...(usage ? { usage } : {}), }; runtime.log(JSON.stringify(payload, null, 2)); return; } const lines: string[] = []; lines.push(theme.heading("Chat providers:")); for (const accountId of telegramAccounts) { const account = resolveTelegramAccount({ cfg, accountId }); lines.push( `- ${theme.accent(providerLabel("telegram"))} ${theme.heading( formatAccountLabel({ accountId, name: account.name, }), )}: ${formatConfigured(Boolean(account.token))}, ${formatTokenSource( account.tokenSource, )}, ${formatEnabled(account.enabled)}`, ); } for (const accountId of whatsappAccounts) { const { authDir } = resolveWhatsAppAuthDir({ cfg, accountId }); const linked = await webAuthExists(authDir); const name = cfg.whatsapp?.accounts?.[accountId]?.name; lines.push( `- ${theme.accent(providerLabel("whatsapp"))} ${theme.heading( formatAccountLabel({ accountId, name, }), )}: ${formatLinked(linked)}, ${formatEnabled( cfg.whatsapp?.accounts?.[accountId]?.enabled ?? cfg.web?.enabled ?? true, )}`, ); } for (const accountId of discordAccounts) { const account = resolveDiscordAccount({ cfg, accountId }); lines.push( `- ${theme.accent(providerLabel("discord"))} ${theme.heading( formatAccountLabel({ accountId, name: account.name, }), )}: ${formatConfigured(Boolean(account.token))}, ${formatTokenSource( account.tokenSource, )}, ${formatEnabled(account.enabled)}`, ); } for (const accountId of slackAccounts) { const account = resolveSlackAccount({ cfg, accountId }); const configured = Boolean(account.botToken && account.appToken); lines.push( `- ${theme.accent(providerLabel("slack"))} ${theme.heading( formatAccountLabel({ accountId, name: account.name, }), )}: ${formatConfigured(configured)}, ${formatSource( "bot", account.botTokenSource, )}, ${formatSource("app", account.appTokenSource)}, ${formatEnabled( account.enabled, )}`, ); } for (const accountId of signalAccounts) { const account = resolveSignalAccount({ cfg, accountId }); lines.push( `- ${theme.accent(providerLabel("signal"))} ${theme.heading( formatAccountLabel({ accountId, name: account.name, }), )}: ${formatConfigured(account.configured)}, base=${theme.muted( account.baseUrl, )}, ${formatEnabled(account.enabled)}`, ); } for (const accountId of imessageAccounts) { const account = resolveIMessageAccount({ cfg, accountId }); lines.push( `- ${theme.accent(providerLabel("imessage"))} ${theme.heading( formatAccountLabel({ accountId, name: account.name, }), )}: ${formatEnabled(account.enabled)}`, ); } lines.push(""); lines.push(theme.heading("Auth providers (OAuth + API keys):")); if (authProfiles.length === 0) { lines.push(theme.muted("- none")); } else { for (const profile of authProfiles) { const external = profile.isExternal ? theme.muted(" (synced)") : ""; lines.push( `- ${theme.accent(profile.id)} (${theme.success(profile.type)}${external})`, ); } } runtime.log(lines.join("\n")); if (includeUsage) { runtime.log(""); const usage = await loadUsageWithProgress(runtime); if (usage) { const usageLines = formatUsageReportLines(usage); if (usageLines.length > 0) { usageLines[0] = theme.accent(usageLines[0]); runtime.log(usageLines.join("\n")); } } } runtime.log(""); runtime.log( `Docs: ${formatDocsLink("/gateway/configuration", "gateway/configuration")}`, ); } async function loadUsageWithProgress( runtime: RuntimeEnv, ): Promise> | null> { try { return await withProgress( { label: "Fetching usage snapshot…", indeterminate: true, enabled: true }, async () => await loadProviderUsageSummary(), ); } catch (err) { runtime.error(String(err)); return null; } } export function formatGatewayProvidersStatusLines( payload: Record, ): string[] { const lines: string[] = []; lines.push(theme.success("Gateway reachable.")); const accountLines = ( label: string, accounts: Array>, ) => accounts.map((account) => { const bits: string[] = []; if (typeof account.enabled === "boolean") { bits.push(account.enabled ? "enabled" : "disabled"); } if (typeof account.configured === "boolean") { bits.push(account.configured ? "configured" : "not configured"); } if (typeof account.linked === "boolean") { bits.push(account.linked ? "linked" : "not linked"); } if (typeof account.running === "boolean") { bits.push(account.running ? "running" : "stopped"); } const probe = account.probe as { ok?: boolean } | undefined; if (probe && typeof probe.ok === "boolean") { bits.push(probe.ok ? "works" : "probe failed"); } const accountId = typeof account.accountId === "string" ? account.accountId : "default"; const name = typeof account.name === "string" ? account.name.trim() : ""; const labelText = `${label} ${formatAccountLabel({ accountId, name: name || undefined, })}`; return `- ${labelText}: ${bits.join(", ")}`; }); const accountPayloads: Partial< Record>> > = { whatsapp: Array.isArray(payload.whatsappAccounts) ? (payload.whatsappAccounts as Array>) : undefined, telegram: Array.isArray(payload.telegramAccounts) ? (payload.telegramAccounts as Array>) : undefined, discord: Array.isArray(payload.discordAccounts) ? (payload.discordAccounts as Array>) : undefined, slack: Array.isArray(payload.slackAccounts) ? (payload.slackAccounts as Array>) : undefined, signal: Array.isArray(payload.signalAccounts) ? (payload.signalAccounts as Array>) : undefined, imessage: Array.isArray(payload.imessageAccounts) ? (payload.imessageAccounts as Array>) : undefined, }; for (const meta of listChatProviders()) { const accounts = accountPayloads[meta.id]; if (accounts && accounts.length > 0) { lines.push(...accountLines(meta.label, accounts)); } } lines.push(""); lines.push( `Tip: ${formatDocsLink("/cli#status", "status --deep")} runs local probes without a gateway.`, ); return lines; } export async function providersStatusCommand( opts: ProvidersStatusOptions, runtime: RuntimeEnv = defaultRuntime, ) { const timeoutMs = Number(opts.timeout ?? 10_000); try { const payload = await withProgress( { label: "Checking provider status…", indeterminate: true, enabled: opts.json !== true, }, async () => await callGateway({ method: "providers.status", params: { probe: Boolean(opts.probe), timeoutMs }, timeoutMs, }), ); if (opts.json) { runtime.log(JSON.stringify(payload, null, 2)); return; } runtime.log( formatGatewayProvidersStatusLines( payload as Record, ).join("\n"), ); } catch (err) { runtime.error(`Gateway not reachable: ${String(err)}`); runtime.exit(1); } } export async function providersAddCommand( opts: ProvidersAddOptions, runtime: RuntimeEnv = defaultRuntime, params?: { hasFlags?: boolean }, ) { const cfg = await requireValidConfig(runtime); if (!cfg) return; const useWizard = shouldUseWizard(params); if (useWizard) { const prompter = createClackPrompter(); let selection: ProviderChoice[] = []; const accountIds: Partial> = {}; await prompter.intro("Provider setup"); let nextConfig = await setupProviders(cfg, runtime, prompter, { allowDisable: false, allowSignalInstall: true, promptAccountIds: true, onSelection: (value) => { selection = value; }, onAccountId: (provider, accountId) => { accountIds[provider] = accountId; }, }); if (selection.length === 0) { await prompter.outro("No providers selected."); return; } const wantsNames = await prompter.confirm({ message: "Add display names for these accounts? (optional)", initialValue: false, }); if (wantsNames) { for (const provider of selection) { const accountId = accountIds[provider] ?? DEFAULT_ACCOUNT_ID; const existingName = provider === "whatsapp" ? nextConfig.whatsapp?.accounts?.[accountId]?.name : provider === "telegram" ? (nextConfig.telegram?.accounts?.[accountId]?.name ?? nextConfig.telegram?.name) : provider === "discord" ? (nextConfig.discord?.accounts?.[accountId]?.name ?? nextConfig.discord?.name) : provider === "slack" ? (nextConfig.slack?.accounts?.[accountId]?.name ?? nextConfig.slack?.name) : provider === "signal" ? (nextConfig.signal?.accounts?.[accountId]?.name ?? nextConfig.signal?.name) : provider === "imessage" ? (nextConfig.imessage?.accounts?.[accountId]?.name ?? nextConfig.imessage?.name) : undefined; const name = await prompter.text({ message: `${provider} account name (${accountId})`, initialValue: existingName, }); if (name?.trim()) { nextConfig = applyAccountName({ cfg: nextConfig, provider, accountId, name, }); } } } await writeConfigFile(nextConfig); await prompter.outro("Providers updated."); return; } const provider = normalizeChatProviderId(opts.provider); if (!provider) { runtime.error(`Unknown provider: ${String(opts.provider ?? "")}`); runtime.exit(1); return; } const accountId = normalizeAccountId(opts.account); const useEnv = opts.useEnv === true; if (provider === "telegram") { if (useEnv && accountId !== DEFAULT_ACCOUNT_ID) { runtime.error( "TELEGRAM_BOT_TOKEN can only be used for the default account.", ); runtime.exit(1); return; } if (!useEnv && !opts.token && !opts.tokenFile) { runtime.error( "Telegram requires --token or --token-file (or --use-env).", ); runtime.exit(1); return; } } if (provider === "discord") { if (useEnv && accountId !== DEFAULT_ACCOUNT_ID) { runtime.error( "DISCORD_BOT_TOKEN can only be used for the default account.", ); runtime.exit(1); return; } if (!useEnv && !opts.token) { runtime.error("Discord requires --token (or --use-env)."); runtime.exit(1); return; } } if (provider === "slack") { if (useEnv && accountId !== DEFAULT_ACCOUNT_ID) { runtime.error( "Slack env tokens can only be used for the default account.", ); runtime.exit(1); return; } if (!useEnv && (!opts.botToken || !opts.appToken)) { runtime.error( "Slack requires --bot-token and --app-token (or --use-env).", ); runtime.exit(1); return; } } if (provider === "signal") { if ( !opts.signalNumber && !opts.httpUrl && !opts.httpHost && !opts.httpPort && !opts.cliPath ) { runtime.error( "Signal requires --signal-number or --http-url/--http-host/--http-port/--cli-path.", ); runtime.exit(1); return; } } const nextConfig = applyProviderAccountConfig({ cfg, provider, accountId, name: opts.name, token: opts.token, tokenFile: opts.tokenFile, botToken: opts.botToken, appToken: opts.appToken, signalNumber: opts.signalNumber, cliPath: opts.cliPath, dbPath: opts.dbPath, service: opts.service, region: opts.region, authDir: opts.authDir, httpUrl: opts.httpUrl, httpHost: opts.httpHost, httpPort: opts.httpPort, useEnv, }); await writeConfigFile(nextConfig); runtime.log(`Added ${providerLabel(provider)} account "${accountId}".`); } export async function providersRemoveCommand( opts: ProvidersRemoveOptions, runtime: RuntimeEnv = defaultRuntime, params?: { hasFlags?: boolean }, ) { const cfg = await requireValidConfig(runtime); if (!cfg) return; const useWizard = shouldUseWizard(params); const prompter = useWizard ? createClackPrompter() : null; let provider = normalizeChatProviderId(opts.provider); let accountId = normalizeAccountId(opts.account); const deleteConfig = Boolean(opts.delete); if (useWizard && prompter) { await prompter.intro("Remove provider account"); provider = (await prompter.select({ message: "Provider", options: listChatProviders().map((meta) => ({ value: meta.id, label: meta.label, })), })) as ChatProvider; const listAccounts = provider === "whatsapp" ? listWhatsAppAccountIds : provider === "telegram" ? listTelegramAccountIds : provider === "discord" ? listDiscordAccountIds : provider === "slack" ? listSlackAccountIds : provider === "signal" ? listSignalAccountIds : listIMessageAccountIds; accountId = await (async () => { const ids = listAccounts(cfg); const choice = (await prompter.select({ message: "Account", options: ids.map((id) => ({ value: id, label: id === DEFAULT_ACCOUNT_ID ? "default (primary)" : id, })), initialValue: ids[0] ?? DEFAULT_ACCOUNT_ID, })) as string; return normalizeAccountId(choice); })(); const wantsDisable = await prompter.confirm({ message: `Disable ${providerLabel(provider)} account "${accountId}"? (keeps config)`, initialValue: true, }); if (!wantsDisable) { await prompter.outro("Cancelled."); return; } } else { if (!provider) { runtime.error("Provider is required. Use --provider ."); runtime.exit(1); return; } if (!deleteConfig) { const confirm = createClackPrompter(); const ok = await confirm.confirm({ message: `Disable ${providerLabel(provider)} account "${accountId}"? (keeps config)`, initialValue: true, }); if (!ok) { return; } } } let next = { ...cfg }; const accountKey = accountId || DEFAULT_ACCOUNT_ID; const setAccountEnabled = (key: ChatProvider, enabled: boolean) => { if (key === "whatsapp") { next = { ...next, whatsapp: { ...next.whatsapp, accounts: { ...next.whatsapp?.accounts, [accountKey]: { ...next.whatsapp?.accounts?.[accountKey], enabled, }, }, }, }; return; } const base = (next as Record)[key] as | { accounts?: Record>; enabled?: boolean; } | undefined; const baseAccounts: Record< string, Record > = base?.accounts ?? {}; const existingAccount = baseAccounts[accountKey] ?? {}; if (accountKey === DEFAULT_ACCOUNT_ID && !base?.accounts) { next = { ...next, [key]: { ...base, enabled, }, } as ClawdbotConfig; return; } next = { ...next, [key]: { ...base, accounts: { ...baseAccounts, [accountKey]: { ...existingAccount, enabled, }, }, }, } as ClawdbotConfig; }; const deleteAccount = (key: ChatProvider) => { if (key === "whatsapp") { const accounts = { ...next.whatsapp?.accounts }; delete accounts[accountKey]; next = { ...next, whatsapp: { ...next.whatsapp, accounts: Object.keys(accounts).length ? accounts : undefined, }, }; return; } const base = (next as Record)[key] as | { accounts?: Record>; enabled?: boolean; } | undefined; if (accountKey !== DEFAULT_ACCOUNT_ID) { const accounts = { ...base?.accounts }; delete accounts[accountKey]; next = { ...next, [key]: { ...base, accounts: Object.keys(accounts).length ? accounts : undefined, }, } as ClawdbotConfig; return; } if (base?.accounts && Object.keys(base.accounts).length > 0) { const accounts = { ...base.accounts }; delete accounts[accountKey]; next = { ...next, [key]: { ...base, accounts: Object.keys(accounts).length ? accounts : undefined, ...(key === "telegram" ? { botToken: undefined, tokenFile: undefined, name: undefined } : key === "discord" ? { token: undefined, name: undefined } : key === "slack" ? { botToken: undefined, appToken: undefined, name: undefined } : key === "signal" ? { account: undefined, httpUrl: undefined, httpHost: undefined, httpPort: undefined, cliPath: undefined, name: undefined, } : key === "imessage" ? { cliPath: undefined, dbPath: undefined, service: undefined, region: undefined, name: undefined, } : {}), }, } as ClawdbotConfig; return; } // No accounts map: remove entire provider section. const clone = { ...next } as Record; delete clone[key]; next = clone as ClawdbotConfig; }; if (deleteConfig) { deleteAccount(provider); } else { setAccountEnabled(provider, false); } await writeConfigFile(next); if (useWizard && prompter) { await prompter.outro( deleteConfig ? `Deleted ${providerLabel(provider)} account "${accountKey}".` : `Disabled ${providerLabel(provider)} account "${accountKey}".`, ); } else { runtime.log( deleteConfig ? `Deleted ${providerLabel(provider)} account "${accountKey}".` : `Disabled ${providerLabel(provider)} account "${accountKey}".`, ); } }