From 518dfd4e42f0ca6c6feebebfccd99b3f26662dbb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 11 Jan 2026 01:05:06 +0100 Subject: [PATCH] fix(status): provider setup vs warn --- CHANGELOG.md | 5 + package.json | 2 +- src/commands/status-all.ts | 15 ++- src/commands/status-all/providers.ts | 181 ++++++++++++++++++++++----- src/commands/status.ts | 15 ++- 5 files changed, 171 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 663bb2e7b..a2aff66da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2026.1.10-2 + +### Fixes +- CLI/Status: provider table uses `SETUP` for missing credentials (not `WARN`); richer per-provider detail (token sources, missing keys). + ## 2026.1.10-1 ### Fixes diff --git a/package.json b/package.json index c07de2d29..dc7554347 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clawdbot", - "version": "2026.1.10-1", + "version": "2026.1.10-2", "description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent", "type": "module", "main": "dist/index.js", diff --git a/src/commands/status-all.ts b/src/commands/status-all.ts index f8d718e5e..977801745 100644 --- a/src/commands/status-all.ts +++ b/src/commands/status-all.ts @@ -360,11 +360,14 @@ export async function statusAllCommand( const providerRows = providers.rows.map((row) => ({ Provider: row.provider, Enabled: row.enabled ? ok("ON") : muted("OFF"), - Configured: row.configured - ? ok("OK") - : row.enabled - ? warn("WARN") - : muted("OFF"), + State: + row.state === "ok" + ? ok("OK") + : row.state === "warn" + ? warn("WARN") + : row.state === "off" + ? muted("OFF") + : theme.accentDim("SETUP"), Detail: row.detail, })); @@ -373,7 +376,7 @@ export async function statusAllCommand( columns: [ { key: "Provider", header: "Provider", minWidth: 10 }, { key: "Enabled", header: "Enabled", minWidth: 7 }, - { key: "Configured", header: "Configured", minWidth: 10 }, + { key: "State", header: "State", minWidth: 8 }, { key: "Detail", header: "Detail", flex: true, minWidth: 28 }, ], rows: providerRows, diff --git a/src/commands/status-all/providers.ts b/src/commands/status-all/providers.ts index 7b2b52cd4..28d9ebdf9 100644 --- a/src/commands/status-all/providers.ts +++ b/src/commands/status-all/providers.ts @@ -1,3 +1,4 @@ +import fs from "node:fs"; import type { ClawdbotConfig } from "../../config/config.js"; import { listDiscordAccountIds, @@ -35,10 +36,36 @@ import { formatAge } from "./format.js"; export type ProviderRow = { provider: string; enabled: boolean; - configured: boolean; + state: "ok" | "setup" | "warn" | "off"; detail: string; }; +function summarizeSources(sources: Array): { + label: string; + parts: string[]; +} { + const counts = new Map(); + for (const s of sources) { + const key = s?.trim() ? s.trim() : "unknown"; + counts.set(key, (counts.get(key) ?? 0) + 1); + } + const parts = [...counts.entries()] + .sort((a, b) => b[1] - a[1]) + .map(([key, n]) => `${key}${n > 1 ? `×${n}` : ""}`); + const label = parts.length > 0 ? parts.join("+") : "unknown"; + return { label, parts }; +} + +function existsSyncMaybe(p: string | undefined): boolean | null { + const path = p?.trim() || ""; + if (!path) return null; + try { + return fs.existsSync(path); + } catch { + return null; + } +} + export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{ rows: ProviderRow[]; details: Array<{ @@ -67,11 +94,11 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{ rows.push({ provider: "WhatsApp", enabled: waEnabled, - configured: waLinked, + state: !waEnabled ? "off" : waLinked ? "ok" : "setup", detail: waEnabled ? waLinked ? `linked${waSelf ? ` ${waSelf}` : ""}${waAuthAgeMs ? ` · auth ${formatAge(waAuthAgeMs)}` : ""} · accounts ${waAccounts.length || 1}` - : "not linked" + : "not linked (run clawdbot login)" : "disabled", }); if (waLinked) { @@ -96,7 +123,7 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{ Account: account.name?.trim() ? `${account.accountId} (${account.name.trim()})` : account.accountId, - Status: account.enabled ? "OK" : "WARN", + Status: account.enabled ? "OK" : "OFF", Notes: notes.join(" · "), }; }), @@ -108,15 +135,43 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{ const tgAccounts = listTelegramAccountIds(cfg).map((accountId) => resolveTelegramAccount({ cfg, accountId }), ); - const tgConfigured = tgAccounts.some((a) => Boolean(a.token?.trim())); + const tgEnabledAccounts = tgAccounts.filter((a) => a.enabled); + const tgTokenAccounts = tgEnabledAccounts.filter((a) => a.token?.trim()); + const tgSources = summarizeSources(tgTokenAccounts.map((a) => a.tokenSource)); + const tgMissingFiles: string[] = []; + const tgGlobalTokenFileExists = existsSyncMaybe(cfg.telegram?.tokenFile); + if ( + tgEnabled && + cfg.telegram?.tokenFile?.trim() && + tgGlobalTokenFileExists === false + ) { + tgMissingFiles.push("telegram.tokenFile"); + } + for (const accountId of listTelegramAccountIds(cfg)) { + const tokenFile = + cfg.telegram?.accounts?.[accountId]?.tokenFile?.trim() || ""; + const ok = existsSyncMaybe(tokenFile); + if (tgEnabled && tokenFile && ok === false) { + tgMissingFiles.push(`telegram.accounts.${accountId}.tokenFile`); + } + } + const tgMisconfigured = tgMissingFiles.length > 0; rows.push({ provider: "Telegram", enabled: tgEnabled, - configured: tgEnabled && tgConfigured, + state: !tgEnabled + ? "off" + : tgMisconfigured + ? "warn" + : tgTokenAccounts.length > 0 + ? "ok" + : "setup", detail: tgEnabled - ? tgConfigured - ? `accounts ${tgAccounts.filter((a) => a.token?.trim()).length}` - : "not configured" + ? tgMisconfigured + ? `token file missing (${tgMissingFiles[0]})` + : tgTokenAccounts.length > 0 + ? `bot token ${tgSources.label} · accounts ${tgTokenAccounts.length}/${tgEnabledAccounts.length || 1}` + : "no bot token (TELEGRAM_BOT_TOKEN / telegram.botToken)" : "disabled", }); @@ -125,15 +180,17 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{ const dcAccounts = listDiscordAccountIds(cfg).map((accountId) => resolveDiscordAccount({ cfg, accountId }), ); - const dcConfigured = dcAccounts.some((a) => Boolean(a.token?.trim())); + const dcEnabledAccounts = dcAccounts.filter((a) => a.enabled); + const dcTokenAccounts = dcEnabledAccounts.filter((a) => a.token?.trim()); + const dcSources = summarizeSources(dcTokenAccounts.map((a) => a.tokenSource)); rows.push({ provider: "Discord", enabled: dcEnabled, - configured: dcEnabled && dcConfigured, + state: !dcEnabled ? "off" : dcTokenAccounts.length > 0 ? "ok" : "setup", detail: dcEnabled - ? dcConfigured - ? `accounts ${dcAccounts.filter((a) => a.token?.trim()).length}` - : "not configured" + ? dcTokenAccounts.length > 0 + ? `bot token ${dcSources.label} · accounts ${dcTokenAccounts.length}/${dcEnabledAccounts.length || 1}` + : "no bot token (DISCORD_BOT_TOKEN / discord.token)" : "disabled", }); @@ -142,17 +199,42 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{ const slAccounts = listSlackAccountIds(cfg).map((accountId) => resolveSlackAccount({ cfg, accountId }), ); - const slConfigured = slAccounts.some( + const slEnabledAccounts = slAccounts.filter((a) => a.enabled); + const slReady = slEnabledAccounts.filter( (a) => Boolean(a.botToken?.trim()) && Boolean(a.appToken?.trim()), ); + const slPartial = slEnabledAccounts.filter( + (a) => + (a.botToken?.trim() && !a.appToken?.trim()) || + (!a.botToken?.trim() && a.appToken?.trim()), + ); + const slHasAnyToken = slEnabledAccounts.some( + (a) => Boolean(a.botToken?.trim()) || Boolean(a.appToken?.trim()), + ); + const slBotSources = summarizeSources( + slReady.map((a) => a.botTokenSource ?? "none"), + ); + const slAppSources = summarizeSources( + slReady.map((a) => a.appTokenSource ?? "none"), + ); rows.push({ provider: "Slack", enabled: slEnabled, - configured: slEnabled && slConfigured, + state: !slEnabled + ? "off" + : slPartial.length > 0 + ? "warn" + : slReady.length > 0 + ? "ok" + : "setup", detail: slEnabled - ? slConfigured - ? `accounts ${slAccounts.filter((a) => a.botToken?.trim() && a.appToken?.trim()).length}` - : "not configured" + ? slPartial.length > 0 + ? `partial tokens (need bot+app) · accounts ${slPartial.length}` + : slReady.length > 0 + ? `tokens ok (bot ${slBotSources.label}, app ${slAppSources.label}) · accounts ${slReady.length}/${slEnabledAccounts.length || 1}` + : slHasAnyToken + ? "tokens incomplete (need bot+app)" + : "no tokens (SLACK_BOT_TOKEN + SLACK_APP_TOKEN)" : "disabled", }); @@ -161,15 +243,20 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{ const siAccounts = listSignalAccountIds(cfg).map((accountId) => resolveSignalAccount({ cfg, accountId }), ); - const siConfigured = siAccounts.some((a) => a.configured); + const siEnabledAccounts = siAccounts.filter((a) => a.enabled); + const siConfiguredAccounts = siEnabledAccounts.filter((a) => a.configured); rows.push({ provider: "Signal", enabled: siEnabled, - configured: siEnabled && siConfigured, + state: !siEnabled + ? "off" + : siConfiguredAccounts.length > 0 + ? "ok" + : "setup", detail: siEnabled - ? siConfigured - ? `accounts ${siAccounts.filter((a) => a.configured).length}` - : "not configured" + ? siConfiguredAccounts.length > 0 + ? `configured · accounts ${siConfiguredAccounts.length}/${siEnabledAccounts.length || 1}` + : "default config (no overrides)" : "disabled", }); @@ -178,29 +265,55 @@ export async function buildProvidersTable(cfg: ClawdbotConfig): Promise<{ const imAccounts = listIMessageAccountIds(cfg).map((accountId) => resolveIMessageAccount({ cfg, accountId }), ); - const imConfigured = imAccounts.some((a) => a.configured); + const imEnabledAccounts = imAccounts.filter((a) => a.enabled); + const imConfiguredAccounts = imEnabledAccounts.filter((a) => a.configured); rows.push({ provider: "iMessage", enabled: imEnabled, - configured: imEnabled && imConfigured, + state: !imEnabled + ? "off" + : imConfiguredAccounts.length > 0 + ? "ok" + : "setup", detail: imEnabled - ? imConfigured - ? `accounts ${imAccounts.length}` - : "not configured" + ? imConfiguredAccounts.length > 0 + ? `configured · accounts ${imConfiguredAccounts.length}/${imEnabledAccounts.length || 1}` + : "default config (no overrides)" : "disabled", }); // MS Teams const msEnabled = cfg.msteams?.enabled !== false; - const msConfigured = Boolean(resolveMSTeamsCredentials(cfg.msteams)); + const msCreds = resolveMSTeamsCredentials(cfg.msteams); + const msAppId = + cfg.msteams?.appId?.trim() || process.env.MSTEAMS_APP_ID?.trim(); + const msAppPassword = + cfg.msteams?.appPassword?.trim() || + process.env.MSTEAMS_APP_PASSWORD?.trim(); + const msTenantId = + cfg.msteams?.tenantId?.trim() || process.env.MSTEAMS_TENANT_ID?.trim(); + const msMissing = [ + !msAppId ? "appId" : null, + !msAppPassword ? "appPassword" : null, + !msTenantId ? "tenantId" : null, + ].filter(Boolean) as string[]; + const msAnyPresent = Boolean(msAppId || msAppPassword || msTenantId); rows.push({ provider: "MS Teams", enabled: msEnabled, - configured: msEnabled && msConfigured, + state: !msEnabled + ? "off" + : msCreds + ? "ok" + : msAnyPresent + ? "warn" + : "setup", detail: msEnabled - ? msConfigured - ? "credentials present" - : "not configured" + ? msCreds + ? "credentials set" + : msAnyPresent + ? `credentials incomplete (missing ${msMissing.join(", ")})` + : "no credentials (MSTEAMS_APP_ID / _PASSWORD / _TENANT_ID)" : "disabled", }); diff --git a/src/commands/status.ts b/src/commands/status.ts index f6e3e14a4..384a99e69 100644 --- a/src/commands/status.ts +++ b/src/commands/status.ts @@ -796,17 +796,20 @@ export async function statusCommand( columns: [ { key: "Provider", header: "Provider", minWidth: 10 }, { key: "Enabled", header: "Enabled", minWidth: 7 }, - { key: "Configured", header: "Configured", minWidth: 10 }, + { key: "State", header: "State", minWidth: 8 }, { key: "Detail", header: "Detail", flex: true, minWidth: 24 }, ], rows: providers.rows.map((row) => ({ Provider: row.provider, Enabled: row.enabled ? ok("ON") : muted("OFF"), - Configured: row.configured - ? ok("OK") - : row.enabled - ? warn("WARN") - : muted("OFF"), + State: + row.state === "ok" + ? ok("OK") + : row.state === "warn" + ? warn("WARN") + : row.state === "off" + ? muted("OFF") + : theme.accentDim("SETUP"), Detail: row.detail, })), }).trimEnd(),