import { withProgress } from "../cli/progress.js"; import { resolveGatewayPort } from "../config/config.js"; import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js"; import { info } from "../globals.js"; import { formatUsageReportLines, loadProviderUsageSummary } from "../infra/provider-usage.js"; import type { RuntimeEnv } from "../runtime.js"; import { runSecurityAudit } from "../security/audit.js"; import { renderTable } from "../terminal/table.js"; import { theme } from "../terminal/theme.js"; import { formatHealthChannelLines, type HealthSummary } from "./health.js"; import { resolveControlUiLinks } from "./onboard-helpers.js"; import { getDaemonStatusSummary } from "./status.daemon.js"; import { formatAge, formatDuration, formatKTokens, formatTokensCompact, shortenText, } from "./status.format.js"; import { resolveGatewayProbeAuth } from "./status.gateway-probe.js"; import { scanStatus } from "./status.scan.js"; import { formatUpdateOneLiner } from "./status.update.js"; import { formatGatewayAuthUsed } from "./status-all/format.js"; import { statusAllCommand } from "./status-all.js"; export async function statusCommand( opts: { json?: boolean; deep?: boolean; usage?: boolean; timeoutMs?: number; verbose?: boolean; all?: boolean; }, runtime: RuntimeEnv, ) { if (opts.all && !opts.json) { await statusAllCommand(runtime, { timeoutMs: opts.timeoutMs }); return; } const scan = await scanStatus( { json: opts.json, timeoutMs: opts.timeoutMs, all: opts.all }, runtime, ); const { cfg, osSummary, tailscaleMode, tailscaleDns, tailscaleHttpsUrl, update, gatewayConnection, remoteUrlMissing, gatewayMode, gatewayProbe, gatewayReachable, gatewaySelf, channelIssues, agentStatus, channels, summary, } = scan; const securityAudit = await withProgress( { label: "Running security audit…", indeterminate: true, enabled: opts.json !== true, }, async () => await runSecurityAudit({ config: cfg, deep: false, includeFilesystem: true, includeChannelSecurity: true, }), ); const usage = opts.usage ? await withProgress( { label: "Fetching usage snapshot…", indeterminate: true, enabled: opts.json !== true, }, async () => await loadProviderUsageSummary({ timeoutMs: opts.timeoutMs }), ) : undefined; const health: HealthSummary | undefined = opts.deep ? await withProgress( { label: "Checking gateway health…", indeterminate: true, enabled: opts.json !== true, }, async () => await callGateway({ method: "health", timeoutMs: opts.timeoutMs, }), ) : undefined; if (opts.json) { runtime.log( JSON.stringify( { ...summary, os: osSummary, update, gateway: { mode: gatewayMode, url: gatewayConnection.url, urlSource: gatewayConnection.urlSource, misconfigured: remoteUrlMissing, reachable: gatewayReachable, connectLatencyMs: gatewayProbe?.connectLatencyMs ?? null, self: gatewaySelf, error: gatewayProbe?.error ?? null, }, agents: agentStatus, securityAudit, ...(health || usage ? { health, usage } : {}), }, null, 2, ), ); return; } const rich = true; const muted = (value: string) => (rich ? theme.muted(value) : value); const ok = (value: string) => (rich ? theme.success(value) : value); const warn = (value: string) => (rich ? theme.warn(value) : value); if (opts.verbose) { const details = buildGatewayConnectionDetails(); runtime.log(info("Gateway connection:")); for (const line of details.message.split("\n")) runtime.log(` ${line}`); runtime.log(""); } const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1); const dashboard = (() => { const controlUiEnabled = cfg.gateway?.controlUi?.enabled ?? true; if (!controlUiEnabled) return "disabled"; const links = resolveControlUiLinks({ port: resolveGatewayPort(cfg), bind: cfg.gateway?.bind, customBindHost: cfg.gateway?.customBindHost, basePath: cfg.gateway?.controlUi?.basePath, }); return links.httpUrl; })(); const gatewayValue = (() => { const target = remoteUrlMissing ? `fallback ${gatewayConnection.url}` : `${gatewayConnection.url}${gatewayConnection.urlSource ? ` (${gatewayConnection.urlSource})` : ""}`; const reach = remoteUrlMissing ? warn("misconfigured (remote.url missing)") : gatewayReachable ? ok(`reachable ${formatDuration(gatewayProbe?.connectLatencyMs)}`) : warn(gatewayProbe?.error ? `unreachable (${gatewayProbe.error})` : "unreachable"); const auth = gatewayReachable && !remoteUrlMissing ? ` · auth ${formatGatewayAuthUsed(resolveGatewayProbeAuth(cfg))}` : ""; const self = gatewaySelf?.host || gatewaySelf?.version || gatewaySelf?.platform ? [ gatewaySelf?.host ? gatewaySelf.host : null, gatewaySelf?.ip ? `(${gatewaySelf.ip})` : null, gatewaySelf?.version ? `app ${gatewaySelf.version}` : null, gatewaySelf?.platform ? gatewaySelf.platform : null, ] .filter(Boolean) .join(" ") : null; const suffix = self ? ` · ${self}` : ""; return `${gatewayMode} · ${target} · ${reach}${auth}${suffix}`; })(); const agentsValue = (() => { const pending = agentStatus.bootstrapPendingCount > 0 ? `${agentStatus.bootstrapPendingCount} bootstrapping` : "no bootstraps"; const def = agentStatus.agents.find((a) => a.id === agentStatus.defaultId); const defActive = def?.lastActiveAgeMs != null ? formatAge(def.lastActiveAgeMs) : "unknown"; const defSuffix = def ? ` · default ${def.id} active ${defActive}` : ""; return `${agentStatus.agents.length} · ${pending} · sessions ${agentStatus.totalSessions}${defSuffix}`; })(); const daemon = await getDaemonStatusSummary(); const daemonValue = (() => { if (daemon.installed === false) return `${daemon.label} not installed`; const installedPrefix = daemon.installed === true ? "installed · " : ""; return `${daemon.label} ${installedPrefix}${daemon.loadedText}${daemon.runtimeShort ? ` · ${daemon.runtimeShort}` : ""}`; })(); const defaults = summary.sessions.defaults; const defaultCtx = defaults.contextTokens ? ` (${formatKTokens(defaults.contextTokens)} ctx)` : ""; const eventsValue = summary.queuedSystemEvents.length > 0 ? `${summary.queuedSystemEvents.length} queued` : "none"; const probesValue = health ? ok("enabled") : muted("skipped (use --deep)"); const overviewRows = [ { Item: "Dashboard", Value: dashboard }, { Item: "OS", Value: `${osSummary.label} · node ${process.versions.node}` }, { Item: "Tailscale", Value: tailscaleMode === "off" ? muted("off") : tailscaleDns && tailscaleHttpsUrl ? `${tailscaleMode} · ${tailscaleDns} · ${tailscaleHttpsUrl}` : warn(`${tailscaleMode} · magicdns unknown`), }, { Item: "Update", Value: formatUpdateOneLiner(update).replace(/^Update:\s*/i, ""), }, { Item: "Gateway", Value: gatewayValue }, { Item: "Daemon", Value: daemonValue }, { Item: "Agents", Value: agentsValue }, { Item: "Probes", Value: probesValue }, { Item: "Events", Value: eventsValue }, { Item: "Heartbeat", Value: `${summary.heartbeatSeconds}s` }, { Item: "Sessions", Value: `${summary.sessions.count} active · default ${defaults.model ?? "unknown"}${defaultCtx} · store ${summary.sessions.path}`, }, ]; runtime.log(theme.heading("Clawdbot status")); runtime.log(""); runtime.log(theme.heading("Overview")); runtime.log( renderTable({ width: tableWidth, columns: [ { key: "Item", header: "Item", minWidth: 12 }, { key: "Value", header: "Value", flex: true, minWidth: 32 }, ], rows: overviewRows, }).trimEnd(), ); runtime.log(""); runtime.log(theme.heading("Security audit")); const fmtSummary = (value: { critical: number; warn: number; info: number }) => { const parts = [ theme.error(`${value.critical} critical`), theme.warn(`${value.warn} warn`), theme.muted(`${value.info} info`), ]; return parts.join(" · "); }; runtime.log(theme.muted(`Summary: ${fmtSummary(securityAudit.summary)}`)); const importantFindings = securityAudit.findings.filter( (f) => f.severity === "critical" || f.severity === "warn", ); if (importantFindings.length === 0) { runtime.log(theme.muted("No critical or warn findings detected.")); } else { const severityLabel = (sev: "critical" | "warn" | "info") => { if (sev === "critical") return theme.error("CRITICAL"); if (sev === "warn") return theme.warn("WARN"); return theme.muted("INFO"); }; const sevRank = (sev: "critical" | "warn" | "info") => sev === "critical" ? 0 : sev === "warn" ? 1 : 2; const sorted = [...importantFindings].sort((a, b) => sevRank(a.severity) - sevRank(b.severity)); const shown = sorted.slice(0, 6); for (const f of shown) { runtime.log(` ${severityLabel(f.severity)} ${f.title}`); runtime.log(` ${shortenText(f.detail.replaceAll("\n", " "), 160)}`); if (f.remediation?.trim()) runtime.log(` ${theme.muted(`Fix: ${f.remediation.trim()}`)}`); } if (sorted.length > shown.length) { runtime.log(theme.muted(`… +${sorted.length - shown.length} more`)); } } runtime.log(theme.muted("Full report: clawdbot security audit")); runtime.log(theme.muted("Deep probe: clawdbot security audit --deep")); runtime.log(""); runtime.log(theme.heading("Channels")); const channelIssuesByChannel = (() => { const map = new Map(); for (const issue of channelIssues) { const key = issue.channel; const list = map.get(key); if (list) list.push(issue); else map.set(key, [issue]); } return map; })(); runtime.log( renderTable({ width: tableWidth, columns: [ { key: "Channel", header: "Channel", minWidth: 10 }, { key: "Enabled", header: "Enabled", minWidth: 7 }, { key: "State", header: "State", minWidth: 8 }, { key: "Detail", header: "Detail", flex: true, minWidth: 24 }, ], rows: channels.rows.map((row) => { const issues = channelIssuesByChannel.get(row.id) ?? []; const effectiveState = row.state === "off" ? "off" : issues.length > 0 ? "warn" : row.state; const issueSuffix = issues.length > 0 ? ` · ${warn(`gateway: ${shortenText(issues[0]?.message ?? "issue", 84)}`)}` : ""; return { Channel: row.label, Enabled: row.enabled ? ok("ON") : muted("OFF"), State: effectiveState === "ok" ? ok("OK") : effectiveState === "warn" ? warn("WARN") : effectiveState === "off" ? muted("OFF") : theme.accentDim("SETUP"), Detail: `${row.detail}${issueSuffix}`, }; }), }).trimEnd(), ); runtime.log(""); runtime.log(theme.heading("Sessions")); runtime.log( renderTable({ width: tableWidth, columns: [ { key: "Key", header: "Key", minWidth: 20, flex: true }, { key: "Kind", header: "Kind", minWidth: 6 }, { key: "Age", header: "Age", minWidth: 9 }, { key: "Model", header: "Model", minWidth: 14 }, { key: "Tokens", header: "Tokens", minWidth: 16 }, ], rows: summary.sessions.recent.length > 0 ? summary.sessions.recent.map((sess) => ({ Key: shortenText(sess.key, 32), Kind: sess.kind, Age: sess.updatedAt ? formatAge(sess.age) : "no activity", Model: sess.model ?? "unknown", Tokens: formatTokensCompact(sess), })) : [ { Key: muted("no sessions yet"), Kind: "", Age: "", Model: "", Tokens: "", }, ], }).trimEnd(), ); if (summary.queuedSystemEvents.length > 0) { runtime.log(""); runtime.log(theme.heading("System events")); runtime.log( renderTable({ width: tableWidth, columns: [{ key: "Event", header: "Event", flex: true, minWidth: 24 }], rows: summary.queuedSystemEvents.slice(0, 5).map((event) => ({ Event: event, })), }).trimEnd(), ); if (summary.queuedSystemEvents.length > 5) { runtime.log(muted(`… +${summary.queuedSystemEvents.length - 5} more`)); } } if (health) { runtime.log(""); runtime.log(theme.heading("Health")); const rows: Array> = []; rows.push({ Item: "Gateway", Status: ok("reachable"), Detail: `${health.durationMs}ms`, }); for (const line of formatHealthChannelLines(health)) { const colon = line.indexOf(":"); if (colon === -1) continue; const item = line.slice(0, colon).trim(); const detail = line.slice(colon + 1).trim(); const normalized = detail.toLowerCase(); const status = (() => { if (normalized.startsWith("ok")) return ok("OK"); if (normalized.startsWith("failed")) return warn("WARN"); if (normalized.startsWith("not configured")) return muted("OFF"); if (normalized.startsWith("configured")) return ok("OK"); if (normalized.startsWith("linked")) return ok("LINKED"); if (normalized.startsWith("not linked")) return warn("UNLINKED"); return warn("WARN"); })(); rows.push({ Item: item, Status: status, Detail: detail }); } runtime.log( renderTable({ width: tableWidth, columns: [ { key: "Item", header: "Item", minWidth: 10 }, { key: "Status", header: "Status", minWidth: 8 }, { key: "Detail", header: "Detail", flex: true, minWidth: 28 }, ], rows, }).trimEnd(), ); } if (usage) { runtime.log(""); runtime.log(theme.heading("Usage")); for (const line of formatUsageReportLines(usage)) { runtime.log(line); } } runtime.log(""); runtime.log("FAQ: https://docs.clawd.bot/faq"); runtime.log("Troubleshooting: https://docs.clawd.bot/troubleshooting"); runtime.log(""); runtime.log("Next steps:"); runtime.log(" Need to share? clawdbot status --all"); runtime.log(" Need to debug live? clawdbot logs --follow"); if (gatewayReachable) { runtime.log(" Need to test channels? clawdbot status --deep"); } else { runtime.log(" Fix reachability first: clawdbot gateway status"); } }