fix(status): full-width tables + better diagnosis
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,177 @@ export async function readFileTailLines(
|
||||
.filter((line) => line.trim().length > 0);
|
||||
}
|
||||
|
||||
function countMatches(haystack: string, needle: string): number {
|
||||
if (!haystack || !needle) return 0;
|
||||
return haystack.split(needle).length - 1;
|
||||
}
|
||||
|
||||
function shorten(message: string, maxLen: number): string {
|
||||
const cleaned = message.replace(/\s+/g, " ").trim();
|
||||
if (cleaned.length <= maxLen) return cleaned;
|
||||
return `${cleaned.slice(0, Math.max(0, maxLen - 1))}…`;
|
||||
}
|
||||
|
||||
function normalizeGwsLine(line: string): string {
|
||||
return line
|
||||
.replace(/\s+runId=[^\s]+/g, "")
|
||||
.replace(/\s+conn=[^\s]+/g, "")
|
||||
.replace(/\s+id=[^\s]+/g, "")
|
||||
.replace(/\s+error=Error:.*$/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function consumeJsonBlock(
|
||||
lines: string[],
|
||||
startIndex: number,
|
||||
): { json: string; endIndex: number } | null {
|
||||
const startLine = lines[startIndex] ?? "";
|
||||
const braceAt = startLine.indexOf("{");
|
||||
if (braceAt < 0) return null;
|
||||
|
||||
const parts: string[] = [startLine.slice(braceAt)];
|
||||
let depth =
|
||||
countMatches(parts[0] ?? "", "{") - countMatches(parts[0] ?? "", "}");
|
||||
let i = startIndex;
|
||||
while (depth > 0 && i + 1 < lines.length) {
|
||||
i += 1;
|
||||
const next = lines[i] ?? "";
|
||||
parts.push(next);
|
||||
depth += countMatches(next, "{") - countMatches(next, "}");
|
||||
}
|
||||
return { json: parts.join("\n"), endIndex: i };
|
||||
}
|
||||
|
||||
export function summarizeLogTail(
|
||||
rawLines: string[],
|
||||
opts?: { maxLines?: number },
|
||||
): string[] {
|
||||
const maxLines = Math.max(6, opts?.maxLines ?? 26);
|
||||
|
||||
const out: string[] = [];
|
||||
const groups = new Map<
|
||||
string,
|
||||
{ count: number; index: number; base: string }
|
||||
>();
|
||||
|
||||
const addGroup = (key: string, base: string) => {
|
||||
const existing = groups.get(key);
|
||||
if (existing) {
|
||||
existing.count += 1;
|
||||
return;
|
||||
}
|
||||
groups.set(key, { count: 1, index: out.length, base });
|
||||
out.push(base);
|
||||
};
|
||||
|
||||
const addLine = (line: string) => {
|
||||
const trimmed = line.trimEnd();
|
||||
if (!trimmed) return;
|
||||
out.push(trimmed);
|
||||
};
|
||||
|
||||
const lines = rawLines.map((line) => line.trimEnd()).filter(Boolean);
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const line = lines[i] ?? "";
|
||||
const trimmedStart = line.trimStart();
|
||||
if (
|
||||
(trimmedStart.startsWith('"') ||
|
||||
trimmedStart === "}" ||
|
||||
trimmedStart === "{" ||
|
||||
trimmedStart.startsWith("}") ||
|
||||
trimmedStart.startsWith("{")) &&
|
||||
!trimmedStart.startsWith("[") &&
|
||||
!trimmedStart.startsWith("#")
|
||||
) {
|
||||
// Tail can cut in the middle of a JSON blob; drop orphaned JSON fragments.
|
||||
continue;
|
||||
}
|
||||
|
||||
// "[openai-codex] Token refresh failed: 401 { ...json... }"
|
||||
const tokenRefresh = line.match(
|
||||
/^\[([^\]]+)\]\s+Token refresh failed:\s*(\d+)\s*(\{)?\s*$/,
|
||||
);
|
||||
if (tokenRefresh) {
|
||||
const tag = tokenRefresh[1] ?? "unknown";
|
||||
const status = tokenRefresh[2] ?? "unknown";
|
||||
const block = consumeJsonBlock(lines, i);
|
||||
if (block) {
|
||||
i = block.endIndex;
|
||||
const parsed = (() => {
|
||||
try {
|
||||
return JSON.parse(block.json) as {
|
||||
error?: { code?: string; message?: string };
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
const code = parsed?.error?.code?.trim() || null;
|
||||
const msg = parsed?.error?.message?.trim() || null;
|
||||
const msgShort = msg
|
||||
? msg.toLowerCase().includes("signing in again")
|
||||
? "re-auth required"
|
||||
: shorten(msg, 52)
|
||||
: null;
|
||||
const base = `[${tag}] token refresh ${status}${code ? ` ${code}` : ""}${msgShort ? ` · ${msgShort}` : ""}`;
|
||||
addGroup(
|
||||
`token:${tag}:${status}:${code ?? ""}:${msgShort ?? ""}`,
|
||||
base,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// "Embedded agent failed before reply: OAuth token refresh failed for openai-codex: ..."
|
||||
const embedded = line.match(
|
||||
/^Embedded agent failed before reply:\s+OAuth token refresh failed for ([^:]+):/,
|
||||
);
|
||||
if (embedded) {
|
||||
const provider = embedded[1]?.trim() || "unknown";
|
||||
addGroup(
|
||||
`embedded:${provider}`,
|
||||
`Embedded agent: OAuth token refresh failed (${provider})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// "[gws] ⇄ res ✗ agent ... errorCode=UNAVAILABLE errorMessage=Error: OAuth token refresh failed ... runId=..."
|
||||
if (
|
||||
line.startsWith("[gws]") &&
|
||||
line.includes("errorCode=UNAVAILABLE") &&
|
||||
line.includes("OAuth token refresh failed")
|
||||
) {
|
||||
const normalized = normalizeGwsLine(line);
|
||||
addGroup(`gws:${normalized}`, normalized);
|
||||
continue;
|
||||
}
|
||||
|
||||
addLine(line);
|
||||
}
|
||||
|
||||
for (const g of groups.values()) {
|
||||
if (g.count <= 1) continue;
|
||||
out[g.index] = `${g.base} ×${g.count}`;
|
||||
}
|
||||
|
||||
const deduped: string[] = [];
|
||||
for (const line of out) {
|
||||
if (deduped[deduped.length - 1] === line) continue;
|
||||
deduped.push(line);
|
||||
}
|
||||
|
||||
if (deduped.length <= maxLines) return deduped;
|
||||
|
||||
const head = Math.min(6, Math.floor(maxLines / 3));
|
||||
const tail = Math.max(1, maxLines - head - 1);
|
||||
const kept = [
|
||||
...deduped.slice(0, head),
|
||||
`… ${deduped.length - head - tail} lines omitted …`,
|
||||
...deduped.slice(-tail),
|
||||
];
|
||||
return kept;
|
||||
}
|
||||
|
||||
export function pickGatewaySelfPresence(presence: unknown): {
|
||||
host?: string;
|
||||
ip?: string;
|
||||
|
||||
@@ -104,6 +104,10 @@ vi.mock("../daemon/service.js", () => ({
|
||||
notLoadedText: "not loaded",
|
||||
isLoaded: async () => true,
|
||||
readRuntime: async () => ({ status: "running", pid: 1234 }),
|
||||
readCommand: async () => ({
|
||||
programArguments: ["node", "dist/entry.js", "gateway"],
|
||||
sourcePath: "/tmp/Library/LaunchAgents/com.clawdbot.gateway.plist",
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -133,16 +137,17 @@ describe("statusCommand", () => {
|
||||
(runtime.log as vi.Mock).mockClear();
|
||||
await statusCommand({}, runtime as never);
|
||||
const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0]));
|
||||
expect(logs.some((l) => l.includes("Web session"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Active sessions"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Default model"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("tokens:"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Daemon:"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Clawdbot status"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Overview"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Dashboard"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("macos 14.0 (arm64)"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Providers"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Telegram"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Sessions"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("+1000"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("50%"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("LaunchAgent"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("FAQ:"))).toBe(true);
|
||||
expect(logs.some((l) => l.includes("Troubleshooting:"))).toBe(true);
|
||||
expect(
|
||||
logs.some((l) => l.includes("flags:") && l.includes("verbose:on")),
|
||||
).toBe(true);
|
||||
expect(mocks.logWebSelfId).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,20 +9,14 @@ import {
|
||||
DEFAULT_PROVIDER,
|
||||
} from "../agents/defaults.js";
|
||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
|
||||
import { withProgress } from "../cli/progress.js";
|
||||
import {
|
||||
loadConfig,
|
||||
readConfigFileSnapshot,
|
||||
resolveGatewayPort,
|
||||
} from "../config/config.js";
|
||||
import { loadConfig, resolveGatewayPort } from "../config/config.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveMainSessionKey,
|
||||
resolveStorePath,
|
||||
type SessionEntry,
|
||||
} from "../config/sessions.js";
|
||||
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||
import { probeGateway } from "../gateway/probe.js";
|
||||
@@ -30,17 +24,11 @@ import { listAgentsForGateway } from "../gateway/session-utils.js";
|
||||
import { info } from "../globals.js";
|
||||
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
|
||||
import { resolveOsSummary } from "../infra/os-summary.js";
|
||||
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
|
||||
import { buildProviderSummary } from "../infra/provider-summary.js";
|
||||
import {
|
||||
formatUsageReportLines,
|
||||
loadProviderUsageSummary,
|
||||
} from "../infra/provider-usage.js";
|
||||
import { collectProvidersStatusIssues } from "../infra/providers-status-issues.js";
|
||||
import {
|
||||
readRestartSentinel,
|
||||
summarizeRestartSentinel,
|
||||
} from "../infra/restart-sentinel.js";
|
||||
import { peekSystemEvents } from "../infra/system-events.js";
|
||||
import {
|
||||
checkUpdateStatus,
|
||||
@@ -48,17 +36,15 @@ import {
|
||||
type UpdateCheckResult,
|
||||
} from "../infra/update-check.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import { resolveWhatsAppAccount } from "../web/accounts.js";
|
||||
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
||||
import {
|
||||
getWebAuthAgeMs,
|
||||
logWebSelfId,
|
||||
webAuthExists,
|
||||
} from "../web/session.js";
|
||||
import { getWebAuthAgeMs, webAuthExists } from "../web/session.js";
|
||||
import type { HealthSummary } from "./health.js";
|
||||
import { resolveControlUiLinks } from "./onboard-helpers.js";
|
||||
import { buildProvidersTable } from "./status-all/providers.js";
|
||||
import { statusAllCommand } from "./status-all.js";
|
||||
|
||||
export type SessionStatus = {
|
||||
@@ -206,19 +192,20 @@ const formatDuration = (ms: number | null | undefined) => {
|
||||
return `${(ms / 1000).toFixed(1)}s`;
|
||||
};
|
||||
|
||||
const formatContextUsage = (
|
||||
total: number | null | undefined,
|
||||
contextTokens: number | null | undefined,
|
||||
remaining: number | null | undefined,
|
||||
pct: number | null | undefined,
|
||||
const shortenText = (value: string, maxLen: number) => {
|
||||
const chars = Array.from(value);
|
||||
if (chars.length <= maxLen) return value;
|
||||
return `${chars.slice(0, Math.max(0, maxLen - 1)).join("")}…`;
|
||||
};
|
||||
|
||||
const formatTokensCompact = (
|
||||
sess: Pick<SessionStatus, "totalTokens" | "contextTokens" | "percentUsed">,
|
||||
) => {
|
||||
const used = total ?? 0;
|
||||
if (!contextTokens) {
|
||||
return `tokens: ${formatKTokens(used)} used (ctx unknown)`;
|
||||
}
|
||||
const left = remaining ?? Math.max(0, contextTokens - used);
|
||||
const pctLabel = pct != null ? `${pct}%` : "?%";
|
||||
return `tokens: ${formatKTokens(used)} used, ${formatKTokens(left)} left of ${formatKTokens(contextTokens)} (${pctLabel})`;
|
||||
const used = sess.totalTokens ?? 0;
|
||||
const ctx = sess.contextTokens;
|
||||
if (!ctx) return `${formatKTokens(used)} used`;
|
||||
const pctLabel = sess.percentUsed != null ? `${sess.percentUsed}%` : "?%";
|
||||
return `${formatKTokens(used)}/${formatKTokens(ctx)} (${pctLabel})`;
|
||||
};
|
||||
|
||||
const classifyKey = (
|
||||
@@ -243,6 +230,7 @@ const formatDaemonRuntimeShort = (runtime?: {
|
||||
pid?: number;
|
||||
state?: string;
|
||||
detail?: string;
|
||||
missingUnit?: boolean;
|
||||
}) => {
|
||||
if (!runtime) return null;
|
||||
const status = runtime.status ?? "unknown";
|
||||
@@ -251,22 +239,38 @@ const formatDaemonRuntimeShort = (runtime?: {
|
||||
if (runtime.state && runtime.state.toLowerCase() !== status) {
|
||||
details.push(`state ${runtime.state}`);
|
||||
}
|
||||
if (runtime.detail) details.push(runtime.detail);
|
||||
const detail = runtime.detail?.replace(/\s+/g, " ").trim() || "";
|
||||
const noisyLaunchctlDetail =
|
||||
runtime.missingUnit === true &&
|
||||
detail.toLowerCase().includes("could not find service");
|
||||
if (detail && !noisyLaunchctlDetail) details.push(detail);
|
||||
return details.length > 0 ? `${status} (${details.join(", ")})` : status;
|
||||
};
|
||||
|
||||
async function getDaemonShortLine(): Promise<string | null> {
|
||||
async function getDaemonStatusSummary(): Promise<{
|
||||
label: string;
|
||||
installed: boolean | null;
|
||||
loadedText: string;
|
||||
runtimeShort: string | null;
|
||||
}> {
|
||||
try {
|
||||
const service = resolveGatewayService();
|
||||
const [loaded, runtime] = await Promise.all([
|
||||
const [loaded, runtime, command] = await Promise.all([
|
||||
service.isLoaded({ env: process.env }).catch(() => false),
|
||||
service.readRuntime(process.env).catch(() => undefined),
|
||||
service.readCommand(process.env).catch(() => null),
|
||||
]);
|
||||
const installed = command != null;
|
||||
const loadedText = loaded ? service.loadedText : service.notLoadedText;
|
||||
const runtimeShort = formatDaemonRuntimeShort(runtime);
|
||||
return `Daemon: ${service.label} ${loadedText}${runtimeShort ? `, ${runtimeShort}` : ""}. Details: clawdbot daemon status`;
|
||||
return { label: service.label, installed, loadedText, runtimeShort };
|
||||
} catch {
|
||||
return "Daemon: unknown. Details: clawdbot daemon status";
|
||||
return {
|
||||
label: "Daemon",
|
||||
installed: null,
|
||||
loadedText: "unknown",
|
||||
runtimeShort: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,20 +453,31 @@ function formatUpdateOneLiner(update: UpdateCheckResult): string {
|
||||
}
|
||||
}
|
||||
if (update.git.fetchOk === false) parts.push("fetch failed");
|
||||
|
||||
if (update.registry?.latestVersion) {
|
||||
const cmp = compareSemverStrings(VERSION, update.registry.latestVersion);
|
||||
if (cmp === 0) parts.push(`npm latest ${update.registry.latestVersion}`);
|
||||
else if (cmp != null && cmp < 0)
|
||||
parts.push(`npm update ${update.registry.latestVersion}`);
|
||||
else
|
||||
parts.push(`npm latest ${update.registry.latestVersion} (local newer)`);
|
||||
} else if (update.registry?.error) {
|
||||
parts.push("npm latest unknown");
|
||||
}
|
||||
} else {
|
||||
parts.push(
|
||||
update.packageManager !== "unknown" ? update.packageManager : "pkg",
|
||||
);
|
||||
if (update.registry?.latestVersion) {
|
||||
const cmp = compareSemverStrings(VERSION, update.registry.latestVersion);
|
||||
if (cmp === 0) parts.push(`latest ${update.registry.latestVersion}`);
|
||||
if (cmp === 0) parts.push(`npm latest ${update.registry.latestVersion}`);
|
||||
else if (cmp != null && cmp < 0) {
|
||||
parts.push(`update available ${update.registry.latestVersion}`);
|
||||
parts.push(`npm update ${update.registry.latestVersion}`);
|
||||
} else {
|
||||
parts.push(`latest ${update.registry.latestVersion}`);
|
||||
parts.push(`npm latest ${update.registry.latestVersion} (local newer)`);
|
||||
}
|
||||
} else if (update.registry?.error) {
|
||||
parts.push("latest unknown");
|
||||
parts.push("npm latest unknown");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,26 +489,6 @@ function formatUpdateOneLiner(update: UpdateCheckResult): string {
|
||||
return `Update: ${parts.join(" · ")}`;
|
||||
}
|
||||
|
||||
function formatCheckLine(params: {
|
||||
ok: boolean;
|
||||
label: string;
|
||||
detail?: string | null;
|
||||
warn?: boolean;
|
||||
}) {
|
||||
const symbol = params.ok
|
||||
? theme.success("\u2713")
|
||||
: params.warn
|
||||
? theme.warn("!")
|
||||
: theme.error("\u2717");
|
||||
const label = params.ok
|
||||
? theme.success(params.label)
|
||||
: params.warn
|
||||
? theme.warn(params.label)
|
||||
: theme.error(params.label);
|
||||
const detail = params.detail?.trim() ? ` ${theme.muted(params.detail)}` : "";
|
||||
return `${symbol} ${label}${detail}`;
|
||||
}
|
||||
|
||||
const buildFlags = (entry: SessionEntry): string[] => {
|
||||
const flags: string[] = [];
|
||||
const think = entry?.thinkingLevel;
|
||||
@@ -535,7 +530,7 @@ export async function statusCommand(
|
||||
const scan = await withProgress(
|
||||
{
|
||||
label: "Scanning status…",
|
||||
total: 6,
|
||||
total: 7,
|
||||
enabled: opts.json !== true,
|
||||
},
|
||||
async (progress) => {
|
||||
@@ -582,6 +577,10 @@ export async function statusCommand(
|
||||
: null;
|
||||
progress.tick();
|
||||
|
||||
progress.setLabel("Summarizing providers…");
|
||||
const providers = await buildProvidersTable(cfg);
|
||||
progress.tick();
|
||||
|
||||
progress.setLabel("Reading sessions…");
|
||||
const summary = await getStatusSummary();
|
||||
progress.tick();
|
||||
@@ -600,6 +599,7 @@ export async function statusCommand(
|
||||
gatewayReachable,
|
||||
gatewaySelf,
|
||||
agentStatus,
|
||||
providers,
|
||||
summary,
|
||||
};
|
||||
},
|
||||
@@ -616,6 +616,7 @@ export async function statusCommand(
|
||||
gatewayReachable,
|
||||
gatewaySelf,
|
||||
agentStatus,
|
||||
providers,
|
||||
summary,
|
||||
} = scan;
|
||||
const usage = opts.usage
|
||||
@@ -671,40 +672,44 @@ export async function statusCommand(
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.verbose || opts.all) {
|
||||
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}`);
|
||||
}
|
||||
for (const line of details.message.split("\n")) runtime.log(` ${line}`);
|
||||
runtime.log("");
|
||||
}
|
||||
|
||||
const controlUiEnabled = cfg.gateway?.controlUi?.enabled ?? true;
|
||||
if (!controlUiEnabled) {
|
||||
runtime.log(info("Dashboard: disabled"));
|
||||
} else {
|
||||
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,
|
||||
basePath: cfg.gateway?.controlUi?.basePath,
|
||||
});
|
||||
runtime.log(info(`Dashboard: ${links.httpUrl}`));
|
||||
}
|
||||
return links.httpUrl;
|
||||
})();
|
||||
|
||||
runtime.log(info(`OS: ${osSummary.label} · node ${process.versions.node}`));
|
||||
runtime.log(info(formatUpdateOneLiner(update)));
|
||||
|
||||
const gatewayLine = (() => {
|
||||
const gatewayValue = (() => {
|
||||
const target = remoteUrlMissing
|
||||
? "(missing gateway.remote.url)"
|
||||
: gatewayConnection.url;
|
||||
? `fallback ${gatewayConnection.url}`
|
||||
: `${gatewayConnection.url}${gatewayConnection.urlSource ? ` (${gatewayConnection.urlSource})` : ""}`;
|
||||
const reach = remoteUrlMissing
|
||||
? "misconfigured (missing gateway.remote.url)"
|
||||
? warn("misconfigured (remote.url missing)")
|
||||
: gatewayReachable
|
||||
? `reachable (${formatDuration(gatewayProbe?.connectLatencyMs)})`
|
||||
: gatewayProbe?.error
|
||||
? `unreachable (${gatewayProbe.error})`
|
||||
: "unreachable";
|
||||
? ok(`reachable ${formatDuration(gatewayProbe?.connectLatencyMs)}`)
|
||||
: warn(
|
||||
gatewayProbe?.error
|
||||
? `unreachable (${gatewayProbe.error})`
|
||||
: "unreachable",
|
||||
);
|
||||
const self =
|
||||
gatewaySelf?.host || gatewaySelf?.version || gatewaySelf?.platform
|
||||
? [
|
||||
@@ -717,11 +722,10 @@ export async function statusCommand(
|
||||
.join(" ")
|
||||
: null;
|
||||
const suffix = self ? ` · ${self}` : "";
|
||||
return `Gateway: ${gatewayMode} · ${target} · ${reach}${suffix}`;
|
||||
return `${gatewayMode} · ${target} · ${reach}${suffix}`;
|
||||
})();
|
||||
runtime.log(info(gatewayLine));
|
||||
|
||||
const agentLine = (() => {
|
||||
const agentsValue = (() => {
|
||||
const pending =
|
||||
agentStatus.bootstrapPendingCount > 0
|
||||
? `${agentStatus.bootstrapPendingCount} bootstrapping`
|
||||
@@ -730,300 +734,192 @@ export async function statusCommand(
|
||||
const defActive =
|
||||
def?.lastActiveAgeMs != null ? formatAge(def.lastActiveAgeMs) : "unknown";
|
||||
const defSuffix = def ? ` · default ${def.id} active ${defActive}` : "";
|
||||
return `Agents: ${agentStatus.agents.length} · ${pending} · sessions ${agentStatus.totalSessions}${defSuffix}`;
|
||||
return `${agentStatus.agents.length} · ${pending} · sessions ${agentStatus.totalSessions}${defSuffix}`;
|
||||
})();
|
||||
runtime.log(info(agentLine));
|
||||
|
||||
runtime.log(
|
||||
`Web session: ${summary.web.linked ? "linked" : "not linked"}${summary.web.linked ? ` (last refreshed ${formatAge(summary.web.authAgeMs)})` : ""}`,
|
||||
);
|
||||
if (summary.web.linked) {
|
||||
const account = resolveWhatsAppAccount({ cfg });
|
||||
logWebSelfId(account.authDir, runtime, true);
|
||||
}
|
||||
runtime.log("");
|
||||
runtime.log(info("System:"));
|
||||
for (const line of summary.providerSummary) {
|
||||
runtime.log(` ${line}`);
|
||||
}
|
||||
const daemonLine = await getDaemonShortLine();
|
||||
if (daemonLine) {
|
||||
runtime.log(info(daemonLine));
|
||||
}
|
||||
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}` : ""}`;
|
||||
})();
|
||||
|
||||
if (opts.all) {
|
||||
runtime.log("");
|
||||
runtime.log(theme.heading("Diagnosis (read-only):"));
|
||||
|
||||
const snap = await readConfigFileSnapshot().catch(() => null);
|
||||
if (snap) {
|
||||
runtime.log(
|
||||
formatCheckLine({
|
||||
ok: Boolean(snap.exists && snap.valid),
|
||||
warn: Boolean(snap.exists && !snap.valid),
|
||||
label: `Config: ${snap.path ?? "(unknown)"}`,
|
||||
detail: snap.exists
|
||||
? snap.valid
|
||||
? "valid"
|
||||
: `invalid (${snap.issues.length} issues)`
|
||||
: "missing",
|
||||
}),
|
||||
);
|
||||
const issues = [...(snap.legacyIssues ?? []), ...(snap.issues ?? [])];
|
||||
const uniqueIssues = issues.filter(
|
||||
(issue, index) =>
|
||||
issues.findIndex(
|
||||
(x) => x.path === issue.path && x.message === issue.message,
|
||||
) === index,
|
||||
);
|
||||
for (const issue of uniqueIssues.slice(0, 12)) {
|
||||
runtime.log(` - ${issue.path}: ${issue.message}`);
|
||||
}
|
||||
if (uniqueIssues.length > 12) {
|
||||
runtime.log(theme.muted(` … +${uniqueIssues.length - 12} more`));
|
||||
}
|
||||
} else {
|
||||
runtime.log(
|
||||
formatCheckLine({
|
||||
ok: false,
|
||||
label: "Config: unknown",
|
||||
detail: "read failed",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const sentinel = await readRestartSentinel().catch(() => null);
|
||||
if (sentinel?.payload) {
|
||||
runtime.log(
|
||||
formatCheckLine({
|
||||
ok: true,
|
||||
label: "Restart sentinel",
|
||||
detail: `${summarizeRestartSentinel(sentinel.payload)} · ${formatAge(Date.now() - sentinel.payload.ts)}`,
|
||||
warn: true,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
runtime.log(
|
||||
formatCheckLine({
|
||||
ok: true,
|
||||
label: "Restart sentinel",
|
||||
detail: "none",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const lastErr = await readLastGatewayErrorLine(process.env).catch(
|
||||
() => null,
|
||||
);
|
||||
if (lastErr) {
|
||||
runtime.log(
|
||||
formatCheckLine({
|
||||
ok: true,
|
||||
warn: true,
|
||||
label: "Gateway last log line",
|
||||
detail: lastErr,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
runtime.log(
|
||||
formatCheckLine({
|
||||
ok: true,
|
||||
label: "Gateway last log line",
|
||||
detail: "none",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const port = resolveGatewayPort(cfg);
|
||||
const portUsage = await inspectPortUsage(port).catch(() => null);
|
||||
if (portUsage) {
|
||||
const ok = portUsage.listeners.length === 0;
|
||||
runtime.log(
|
||||
formatCheckLine({
|
||||
ok,
|
||||
warn: !ok,
|
||||
label: `Port ${port}`,
|
||||
detail: ok ? "free" : "in use",
|
||||
}),
|
||||
);
|
||||
if (!ok) {
|
||||
for (const line of formatPortDiagnostics(portUsage)) {
|
||||
runtime.log(` ${line}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultWorkspace =
|
||||
agentStatus.agents.find((a) => a.id === agentStatus.defaultId)
|
||||
?.workspaceDir ??
|
||||
agentStatus.agents[0]?.workspaceDir ??
|
||||
null;
|
||||
const skillStatus =
|
||||
defaultWorkspace != null
|
||||
? (() => {
|
||||
try {
|
||||
return buildWorkspaceSkillStatus(defaultWorkspace, {
|
||||
config: cfg,
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})()
|
||||
: null;
|
||||
if (skillStatus) {
|
||||
const eligible = skillStatus.skills.filter((s) => s.eligible).length;
|
||||
const missing = skillStatus.skills.filter(
|
||||
(s) => s.eligible && Object.values(s.missing).some((arr) => arr.length),
|
||||
).length;
|
||||
runtime.log(
|
||||
formatCheckLine({
|
||||
ok: missing === 0,
|
||||
warn: missing > 0,
|
||||
label: "Skills",
|
||||
detail: `${eligible} eligible · ${missing} missing requirements · ${skillStatus.workspaceDir}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
runtime.log("");
|
||||
runtime.log(theme.heading("Agents:"));
|
||||
for (const agent of agentStatus.agents) {
|
||||
const name = agent.name ? ` (${agent.name})` : "";
|
||||
const bootstrap =
|
||||
agent.bootstrapPending === true
|
||||
? theme.warn("BOOTSTRAP.md pending")
|
||||
: agent.bootstrapPending === false
|
||||
? theme.success("bootstrapped")
|
||||
: theme.muted("bootstrap unknown");
|
||||
const active =
|
||||
agent.lastActiveAgeMs != null
|
||||
? formatAge(agent.lastActiveAgeMs)
|
||||
: "unknown";
|
||||
runtime.log(
|
||||
`- ${theme.info(agent.id)}${name} · ${bootstrap} · sessions ${agent.sessionsCount} · active ${active}`,
|
||||
);
|
||||
if (agent.workspaceDir)
|
||||
runtime.log(theme.muted(` workspace: ${agent.workspaceDir}`));
|
||||
runtime.log(theme.muted(` sessions: ${agent.sessionsPath}`));
|
||||
}
|
||||
|
||||
if (gatewayReachable) {
|
||||
const providersStatus = await callGateway<Record<string, unknown>>({
|
||||
method: "providers.status",
|
||||
params: { probe: false, timeoutMs: opts.timeoutMs ?? 10_000 },
|
||||
timeoutMs: Math.min(8000, opts.timeoutMs ?? 10_000),
|
||||
}).catch(() => null);
|
||||
if (providersStatus) {
|
||||
const issues = collectProvidersStatusIssues(providersStatus);
|
||||
runtime.log(
|
||||
formatCheckLine({
|
||||
ok: issues.length === 0,
|
||||
warn: issues.length > 0,
|
||||
label: "Provider config/runtime issues",
|
||||
detail: issues.length ? String(issues.length) : "none",
|
||||
}),
|
||||
);
|
||||
for (const issue of issues.slice(0, 8)) {
|
||||
runtime.log(
|
||||
` - ${issue.provider}[${issue.accountId}] ${issue.kind}: ${issue.message}`,
|
||||
);
|
||||
if (issue.fix) runtime.log(theme.muted(` fix: ${issue.fix}`));
|
||||
}
|
||||
if (issues.length > 8) {
|
||||
runtime.log(theme.muted(` … +${issues.length - 8} more`));
|
||||
}
|
||||
} else {
|
||||
runtime.log(
|
||||
formatCheckLine({
|
||||
ok: false,
|
||||
warn: true,
|
||||
label: "Provider config/runtime issues",
|
||||
detail: "skipped (gateway query failed)",
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
runtime.log(
|
||||
formatCheckLine({
|
||||
ok: false,
|
||||
warn: true,
|
||||
label: "Provider config/runtime issues",
|
||||
detail: "skipped (gateway unreachable)",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
runtime.log("");
|
||||
runtime.log(
|
||||
theme.muted(
|
||||
"Tip: This output is safe to paste for debugging (no tokens).",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
runtime.log("");
|
||||
if (health) {
|
||||
runtime.log(info("Gateway health: reachable"));
|
||||
|
||||
const tgLine = health.telegram.configured
|
||||
? health.telegram.probe?.ok
|
||||
? info(
|
||||
`Telegram: ok${health.telegram.probe.bot?.username ? ` (@${health.telegram.probe.bot.username})` : ""} (${health.telegram.probe.elapsedMs}ms)` +
|
||||
(health.telegram.probe.webhook?.url
|
||||
? ` - webhook ${health.telegram.probe.webhook.url}`
|
||||
: ""),
|
||||
)
|
||||
: `Telegram: failed (${health.telegram.probe?.status ?? "unknown"})${health.telegram.probe?.error ? ` - ${health.telegram.probe.error}` : ""}`
|
||||
: info("Telegram: not configured");
|
||||
runtime.log(tgLine);
|
||||
|
||||
const discordLine = health.discord.configured
|
||||
? health.discord.probe?.ok
|
||||
? info(
|
||||
`Discord: ok${health.discord.probe.bot?.username ? ` (@${health.discord.probe.bot.username})` : ""} (${health.discord.probe.elapsedMs}ms)`,
|
||||
)
|
||||
: `Discord: failed (${health.discord.probe?.status ?? "unknown"})${health.discord.probe?.error ? ` - ${health.discord.probe.error}` : ""}`
|
||||
: info("Discord: not configured");
|
||||
runtime.log(discordLine);
|
||||
} else {
|
||||
runtime.log(info("Provider probes: skipped (use --deep)"));
|
||||
}
|
||||
runtime.log("");
|
||||
if (summary.queuedSystemEvents.length > 0) {
|
||||
const preview = summary.queuedSystemEvents.slice(0, 3).join(" | ");
|
||||
runtime.log(
|
||||
info(
|
||||
`Queued system events (${summary.queuedSystemEvents.length}): ${preview}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
runtime.log(info(`Heartbeat: ${summary.heartbeatSeconds}s`));
|
||||
runtime.log(info(`Session store: ${summary.sessions.path}`));
|
||||
const defaults = summary.sessions.defaults;
|
||||
const defaultCtx = defaults.contextTokens
|
||||
? ` (${formatKTokens(defaults.contextTokens)} ctx)`
|
||||
: "";
|
||||
runtime.log(
|
||||
info(`Default model: ${defaults.model ?? "unknown"}${defaultCtx}`),
|
||||
);
|
||||
runtime.log(info(`Active sessions: ${summary.sessions.count}`));
|
||||
if (summary.sessions.recent.length > 0) {
|
||||
runtime.log("Recent sessions:");
|
||||
for (const r of summary.sessions.recent) {
|
||||
runtime.log(
|
||||
`- ${r.key} [${r.kind}] | ${r.updatedAt ? formatAge(r.age) : "no activity"} | model ${r.model ?? "unknown"} | ${formatContextUsage(r.totalTokens, r.contextTokens, r.remainingTokens, r.percentUsed)}${r.flags.length ? ` | flags: ${r.flags.join(", ")}` : ""}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
runtime.log("No session activity yet.");
|
||||
}
|
||||
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: "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("Providers"));
|
||||
runtime.log(
|
||||
renderTable({
|
||||
width: tableWidth,
|
||||
columns: [
|
||||
{ key: "Provider", header: "Provider", minWidth: 10 },
|
||||
{ key: "Enabled", header: "Enabled", minWidth: 7 },
|
||||
{ key: "Configured", header: "Configured", minWidth: 10 },
|
||||
{ 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"),
|
||||
Detail: row.detail,
|
||||
})),
|
||||
}).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<Record<string, string>> = [];
|
||||
rows.push({
|
||||
Provider: "Gateway",
|
||||
Status: ok("reachable"),
|
||||
Detail: `${health.durationMs}ms`,
|
||||
});
|
||||
rows.push({
|
||||
Provider: "Telegram",
|
||||
Status: health.telegram.configured
|
||||
? health.telegram.probe?.ok
|
||||
? ok("OK")
|
||||
: warn("WARN")
|
||||
: muted("OFF"),
|
||||
Detail: health.telegram.configured
|
||||
? health.telegram.probe?.ok
|
||||
? `@${health.telegram.probe.bot?.username ?? "unknown"} · ${health.telegram.probe.elapsedMs}ms`
|
||||
: (health.telegram.probe?.error ?? "probe failed")
|
||||
: "not configured",
|
||||
});
|
||||
rows.push({
|
||||
Provider: "Discord",
|
||||
Status: health.discord.configured
|
||||
? health.discord.probe?.ok
|
||||
? ok("OK")
|
||||
: warn("WARN")
|
||||
: muted("OFF"),
|
||||
Detail: health.discord.configured
|
||||
? health.discord.probe?.ok
|
||||
? `@${health.discord.probe.bot?.username ?? "unknown"} · ${health.discord.probe.elapsedMs}ms`
|
||||
: (health.discord.probe?.error ?? "probe failed")
|
||||
: "not configured",
|
||||
});
|
||||
|
||||
runtime.log(
|
||||
renderTable({
|
||||
width: tableWidth,
|
||||
columns: [
|
||||
{ key: "Provider", header: "Provider", 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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user