fix(status): full-width tables + better diagnosis
This commit is contained in:
@@ -70,6 +70,7 @@
|
|||||||
- When answering questions, respond with high-confidence answers only: verify in code; do not guess.
|
- When answering questions, respond with high-confidence answers only: verify in code; do not guess.
|
||||||
- Never update the Carbon dependency.
|
- Never update the Carbon dependency.
|
||||||
- CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); don’t hand-roll spinners/bars.
|
- CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); don’t hand-roll spinners/bars.
|
||||||
|
- Status output: keep `clawdbot status` table-based (`src/terminal/table.ts`, flex fills width) + `status --all` log tail summarized/pasteable.
|
||||||
- Gateway currently runs only as the menubar app; there is no separate LaunchAgent/helper label installed. Restart via the Clawdbot Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep clawdbot` rather than assuming a fixed label. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.**
|
- Gateway currently runs only as the menubar app; there is no separate LaunchAgent/helper label installed. Restart via the Clawdbot Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep clawdbot` rather than assuming a fixed label. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.**
|
||||||
- macOS logs: use `./scripts/clawlog.sh` (aka `vtlog`) to query unified logs for the Clawdbot subsystem; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`.
|
- macOS logs: use `./scripts/clawlog.sh` (aka `vtlog`) to query unified logs for the Clawdbot subsystem; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`.
|
||||||
- If shared guardrails are available locally, review them; otherwise follow this repo's guidance.
|
- If shared guardrails are available locally, review them; otherwise follow this repo's guidance.
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026.1.10-1
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- CLI/Status: expand tables to full terminal width; improve update + daemon summary lines; keep `status --all` gateway log tail pasteable.
|
||||||
|
|
||||||
## 2026.1.10
|
## 2026.1.10
|
||||||
|
|
||||||
### New Features and Changes
|
### New Features and Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clawdbot",
|
"name": "clawdbot",
|
||||||
"version": "2026.1.10",
|
"version": "2026.1.10-1",
|
||||||
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
|
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,177 @@ export async function readFileTailLines(
|
|||||||
.filter((line) => line.trim().length > 0);
|
.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): {
|
export function pickGatewaySelfPresence(presence: unknown): {
|
||||||
host?: string;
|
host?: string;
|
||||||
ip?: string;
|
ip?: string;
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ vi.mock("../daemon/service.js", () => ({
|
|||||||
notLoadedText: "not loaded",
|
notLoadedText: "not loaded",
|
||||||
isLoaded: async () => true,
|
isLoaded: async () => true,
|
||||||
readRuntime: async () => ({ status: "running", pid: 1234 }),
|
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();
|
(runtime.log as vi.Mock).mockClear();
|
||||||
await statusCommand({}, runtime as never);
|
await statusCommand({}, runtime as never);
|
||||||
const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0]));
|
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("Clawdbot status"))).toBe(true);
|
||||||
expect(logs.some((l) => l.includes("Active sessions"))).toBe(true);
|
expect(logs.some((l) => l.includes("Overview"))).toBe(true);
|
||||||
expect(logs.some((l) => l.includes("Default model"))).toBe(true);
|
expect(logs.some((l) => l.includes("Dashboard"))).toBe(true);
|
||||||
expect(logs.some((l) => l.includes("tokens:"))).toBe(true);
|
expect(logs.some((l) => l.includes("macos 14.0 (arm64)"))).toBe(true);
|
||||||
expect(logs.some((l) => l.includes("Daemon:"))).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("FAQ:"))).toBe(true);
|
||||||
expect(logs.some((l) => l.includes("Troubleshooting:"))).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,
|
DEFAULT_PROVIDER,
|
||||||
} from "../agents/defaults.js";
|
} from "../agents/defaults.js";
|
||||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||||
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
|
|
||||||
import { withProgress } from "../cli/progress.js";
|
import { withProgress } from "../cli/progress.js";
|
||||||
import {
|
import { loadConfig, resolveGatewayPort } from "../config/config.js";
|
||||||
loadConfig,
|
|
||||||
readConfigFileSnapshot,
|
|
||||||
resolveGatewayPort,
|
|
||||||
} from "../config/config.js";
|
|
||||||
import {
|
import {
|
||||||
loadSessionStore,
|
loadSessionStore,
|
||||||
resolveMainSessionKey,
|
resolveMainSessionKey,
|
||||||
resolveStorePath,
|
resolveStorePath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
|
|
||||||
import { resolveGatewayService } from "../daemon/service.js";
|
import { resolveGatewayService } from "../daemon/service.js";
|
||||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||||
import { probeGateway } from "../gateway/probe.js";
|
import { probeGateway } from "../gateway/probe.js";
|
||||||
@@ -30,17 +24,11 @@ import { listAgentsForGateway } from "../gateway/session-utils.js";
|
|||||||
import { info } from "../globals.js";
|
import { info } from "../globals.js";
|
||||||
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
|
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
|
||||||
import { resolveOsSummary } from "../infra/os-summary.js";
|
import { resolveOsSummary } from "../infra/os-summary.js";
|
||||||
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
|
|
||||||
import { buildProviderSummary } from "../infra/provider-summary.js";
|
import { buildProviderSummary } from "../infra/provider-summary.js";
|
||||||
import {
|
import {
|
||||||
formatUsageReportLines,
|
formatUsageReportLines,
|
||||||
loadProviderUsageSummary,
|
loadProviderUsageSummary,
|
||||||
} from "../infra/provider-usage.js";
|
} 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 { peekSystemEvents } from "../infra/system-events.js";
|
||||||
import {
|
import {
|
||||||
checkUpdateStatus,
|
checkUpdateStatus,
|
||||||
@@ -48,17 +36,15 @@ import {
|
|||||||
type UpdateCheckResult,
|
type UpdateCheckResult,
|
||||||
} from "../infra/update-check.js";
|
} from "../infra/update-check.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { renderTable } from "../terminal/table.js";
|
||||||
import { theme } from "../terminal/theme.js";
|
import { theme } from "../terminal/theme.js";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import { resolveWhatsAppAccount } from "../web/accounts.js";
|
import { resolveWhatsAppAccount } from "../web/accounts.js";
|
||||||
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
||||||
import {
|
import { getWebAuthAgeMs, webAuthExists } from "../web/session.js";
|
||||||
getWebAuthAgeMs,
|
|
||||||
logWebSelfId,
|
|
||||||
webAuthExists,
|
|
||||||
} from "../web/session.js";
|
|
||||||
import type { HealthSummary } from "./health.js";
|
import type { HealthSummary } from "./health.js";
|
||||||
import { resolveControlUiLinks } from "./onboard-helpers.js";
|
import { resolveControlUiLinks } from "./onboard-helpers.js";
|
||||||
|
import { buildProvidersTable } from "./status-all/providers.js";
|
||||||
import { statusAllCommand } from "./status-all.js";
|
import { statusAllCommand } from "./status-all.js";
|
||||||
|
|
||||||
export type SessionStatus = {
|
export type SessionStatus = {
|
||||||
@@ -206,19 +192,20 @@ const formatDuration = (ms: number | null | undefined) => {
|
|||||||
return `${(ms / 1000).toFixed(1)}s`;
|
return `${(ms / 1000).toFixed(1)}s`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatContextUsage = (
|
const shortenText = (value: string, maxLen: number) => {
|
||||||
total: number | null | undefined,
|
const chars = Array.from(value);
|
||||||
contextTokens: number | null | undefined,
|
if (chars.length <= maxLen) return value;
|
||||||
remaining: number | null | undefined,
|
return `${chars.slice(0, Math.max(0, maxLen - 1)).join("")}…`;
|
||||||
pct: number | null | undefined,
|
};
|
||||||
|
|
||||||
|
const formatTokensCompact = (
|
||||||
|
sess: Pick<SessionStatus, "totalTokens" | "contextTokens" | "percentUsed">,
|
||||||
) => {
|
) => {
|
||||||
const used = total ?? 0;
|
const used = sess.totalTokens ?? 0;
|
||||||
if (!contextTokens) {
|
const ctx = sess.contextTokens;
|
||||||
return `tokens: ${formatKTokens(used)} used (ctx unknown)`;
|
if (!ctx) return `${formatKTokens(used)} used`;
|
||||||
}
|
const pctLabel = sess.percentUsed != null ? `${sess.percentUsed}%` : "?%";
|
||||||
const left = remaining ?? Math.max(0, contextTokens - used);
|
return `${formatKTokens(used)}/${formatKTokens(ctx)} (${pctLabel})`;
|
||||||
const pctLabel = pct != null ? `${pct}%` : "?%";
|
|
||||||
return `tokens: ${formatKTokens(used)} used, ${formatKTokens(left)} left of ${formatKTokens(contextTokens)} (${pctLabel})`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const classifyKey = (
|
const classifyKey = (
|
||||||
@@ -243,6 +230,7 @@ const formatDaemonRuntimeShort = (runtime?: {
|
|||||||
pid?: number;
|
pid?: number;
|
||||||
state?: string;
|
state?: string;
|
||||||
detail?: string;
|
detail?: string;
|
||||||
|
missingUnit?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
if (!runtime) return null;
|
if (!runtime) return null;
|
||||||
const status = runtime.status ?? "unknown";
|
const status = runtime.status ?? "unknown";
|
||||||
@@ -251,22 +239,38 @@ const formatDaemonRuntimeShort = (runtime?: {
|
|||||||
if (runtime.state && runtime.state.toLowerCase() !== status) {
|
if (runtime.state && runtime.state.toLowerCase() !== status) {
|
||||||
details.push(`state ${runtime.state}`);
|
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;
|
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 {
|
try {
|
||||||
const service = resolveGatewayService();
|
const service = resolveGatewayService();
|
||||||
const [loaded, runtime] = await Promise.all([
|
const [loaded, runtime, command] = await Promise.all([
|
||||||
service.isLoaded({ env: process.env }).catch(() => false),
|
service.isLoaded({ env: process.env }).catch(() => false),
|
||||||
service.readRuntime(process.env).catch(() => undefined),
|
service.readRuntime(process.env).catch(() => undefined),
|
||||||
|
service.readCommand(process.env).catch(() => null),
|
||||||
]);
|
]);
|
||||||
|
const installed = command != null;
|
||||||
const loadedText = loaded ? service.loadedText : service.notLoadedText;
|
const loadedText = loaded ? service.loadedText : service.notLoadedText;
|
||||||
const runtimeShort = formatDaemonRuntimeShort(runtime);
|
const runtimeShort = formatDaemonRuntimeShort(runtime);
|
||||||
return `Daemon: ${service.label} ${loadedText}${runtimeShort ? `, ${runtimeShort}` : ""}. Details: clawdbot daemon status`;
|
return { label: service.label, installed, loadedText, runtimeShort };
|
||||||
} catch {
|
} 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.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 {
|
} else {
|
||||||
parts.push(
|
parts.push(
|
||||||
update.packageManager !== "unknown" ? update.packageManager : "pkg",
|
update.packageManager !== "unknown" ? update.packageManager : "pkg",
|
||||||
);
|
);
|
||||||
if (update.registry?.latestVersion) {
|
if (update.registry?.latestVersion) {
|
||||||
const cmp = compareSemverStrings(VERSION, 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) {
|
else if (cmp != null && cmp < 0) {
|
||||||
parts.push(`update available ${update.registry.latestVersion}`);
|
parts.push(`npm update ${update.registry.latestVersion}`);
|
||||||
} else {
|
} else {
|
||||||
parts.push(`latest ${update.registry.latestVersion}`);
|
parts.push(`npm latest ${update.registry.latestVersion} (local newer)`);
|
||||||
}
|
}
|
||||||
} else if (update.registry?.error) {
|
} 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(" · ")}`;
|
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 buildFlags = (entry: SessionEntry): string[] => {
|
||||||
const flags: string[] = [];
|
const flags: string[] = [];
|
||||||
const think = entry?.thinkingLevel;
|
const think = entry?.thinkingLevel;
|
||||||
@@ -535,7 +530,7 @@ export async function statusCommand(
|
|||||||
const scan = await withProgress(
|
const scan = await withProgress(
|
||||||
{
|
{
|
||||||
label: "Scanning status…",
|
label: "Scanning status…",
|
||||||
total: 6,
|
total: 7,
|
||||||
enabled: opts.json !== true,
|
enabled: opts.json !== true,
|
||||||
},
|
},
|
||||||
async (progress) => {
|
async (progress) => {
|
||||||
@@ -582,6 +577,10 @@ export async function statusCommand(
|
|||||||
: null;
|
: null;
|
||||||
progress.tick();
|
progress.tick();
|
||||||
|
|
||||||
|
progress.setLabel("Summarizing providers…");
|
||||||
|
const providers = await buildProvidersTable(cfg);
|
||||||
|
progress.tick();
|
||||||
|
|
||||||
progress.setLabel("Reading sessions…");
|
progress.setLabel("Reading sessions…");
|
||||||
const summary = await getStatusSummary();
|
const summary = await getStatusSummary();
|
||||||
progress.tick();
|
progress.tick();
|
||||||
@@ -600,6 +599,7 @@ export async function statusCommand(
|
|||||||
gatewayReachable,
|
gatewayReachable,
|
||||||
gatewaySelf,
|
gatewaySelf,
|
||||||
agentStatus,
|
agentStatus,
|
||||||
|
providers,
|
||||||
summary,
|
summary,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -616,6 +616,7 @@ export async function statusCommand(
|
|||||||
gatewayReachable,
|
gatewayReachable,
|
||||||
gatewaySelf,
|
gatewaySelf,
|
||||||
agentStatus,
|
agentStatus,
|
||||||
|
providers,
|
||||||
summary,
|
summary,
|
||||||
} = scan;
|
} = scan;
|
||||||
const usage = opts.usage
|
const usage = opts.usage
|
||||||
@@ -671,40 +672,44 @@ export async function statusCommand(
|
|||||||
return;
|
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();
|
const details = buildGatewayConnectionDetails();
|
||||||
runtime.log(info("Gateway connection:"));
|
runtime.log(info("Gateway connection:"));
|
||||||
for (const line of details.message.split("\n")) {
|
for (const line of details.message.split("\n")) runtime.log(` ${line}`);
|
||||||
runtime.log(` ${line}`);
|
runtime.log("");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const controlUiEnabled = cfg.gateway?.controlUi?.enabled ?? true;
|
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
|
||||||
if (!controlUiEnabled) {
|
|
||||||
runtime.log(info("Dashboard: disabled"));
|
const dashboard = (() => {
|
||||||
} else {
|
const controlUiEnabled = cfg.gateway?.controlUi?.enabled ?? true;
|
||||||
|
if (!controlUiEnabled) return "disabled";
|
||||||
const links = resolveControlUiLinks({
|
const links = resolveControlUiLinks({
|
||||||
port: resolveGatewayPort(cfg),
|
port: resolveGatewayPort(cfg),
|
||||||
bind: cfg.gateway?.bind,
|
bind: cfg.gateway?.bind,
|
||||||
basePath: cfg.gateway?.controlUi?.basePath,
|
basePath: cfg.gateway?.controlUi?.basePath,
|
||||||
});
|
});
|
||||||
runtime.log(info(`Dashboard: ${links.httpUrl}`));
|
return links.httpUrl;
|
||||||
}
|
})();
|
||||||
|
|
||||||
runtime.log(info(`OS: ${osSummary.label} · node ${process.versions.node}`));
|
const gatewayValue = (() => {
|
||||||
runtime.log(info(formatUpdateOneLiner(update)));
|
|
||||||
|
|
||||||
const gatewayLine = (() => {
|
|
||||||
const target = remoteUrlMissing
|
const target = remoteUrlMissing
|
||||||
? "(missing gateway.remote.url)"
|
? `fallback ${gatewayConnection.url}`
|
||||||
: gatewayConnection.url;
|
: `${gatewayConnection.url}${gatewayConnection.urlSource ? ` (${gatewayConnection.urlSource})` : ""}`;
|
||||||
const reach = remoteUrlMissing
|
const reach = remoteUrlMissing
|
||||||
? "misconfigured (missing gateway.remote.url)"
|
? warn("misconfigured (remote.url missing)")
|
||||||
: gatewayReachable
|
: gatewayReachable
|
||||||
? `reachable (${formatDuration(gatewayProbe?.connectLatencyMs)})`
|
? ok(`reachable ${formatDuration(gatewayProbe?.connectLatencyMs)}`)
|
||||||
: gatewayProbe?.error
|
: warn(
|
||||||
? `unreachable (${gatewayProbe.error})`
|
gatewayProbe?.error
|
||||||
: "unreachable";
|
? `unreachable (${gatewayProbe.error})`
|
||||||
|
: "unreachable",
|
||||||
|
);
|
||||||
const self =
|
const self =
|
||||||
gatewaySelf?.host || gatewaySelf?.version || gatewaySelf?.platform
|
gatewaySelf?.host || gatewaySelf?.version || gatewaySelf?.platform
|
||||||
? [
|
? [
|
||||||
@@ -717,11 +722,10 @@ export async function statusCommand(
|
|||||||
.join(" ")
|
.join(" ")
|
||||||
: null;
|
: null;
|
||||||
const suffix = self ? ` · ${self}` : "";
|
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 =
|
const pending =
|
||||||
agentStatus.bootstrapPendingCount > 0
|
agentStatus.bootstrapPendingCount > 0
|
||||||
? `${agentStatus.bootstrapPendingCount} bootstrapping`
|
? `${agentStatus.bootstrapPendingCount} bootstrapping`
|
||||||
@@ -730,300 +734,192 @@ export async function statusCommand(
|
|||||||
const defActive =
|
const defActive =
|
||||||
def?.lastActiveAgeMs != null ? formatAge(def.lastActiveAgeMs) : "unknown";
|
def?.lastActiveAgeMs != null ? formatAge(def.lastActiveAgeMs) : "unknown";
|
||||||
const defSuffix = def ? ` · default ${def.id} active ${defActive}` : "";
|
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(
|
const daemon = await getDaemonStatusSummary();
|
||||||
`Web session: ${summary.web.linked ? "linked" : "not linked"}${summary.web.linked ? ` (last refreshed ${formatAge(summary.web.authAgeMs)})` : ""}`,
|
const daemonValue = (() => {
|
||||||
);
|
if (daemon.installed === false) return `${daemon.label} not installed`;
|
||||||
if (summary.web.linked) {
|
const installedPrefix = daemon.installed === true ? "installed · " : "";
|
||||||
const account = resolveWhatsAppAccount({ cfg });
|
return `${daemon.label} ${installedPrefix}${daemon.loadedText}${daemon.runtimeShort ? ` · ${daemon.runtimeShort}` : ""}`;
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 defaults = summary.sessions.defaults;
|
||||||
const defaultCtx = defaults.contextTokens
|
const defaultCtx = defaults.contextTokens
|
||||||
? ` (${formatKTokens(defaults.contextTokens)} ctx)`
|
? ` (${formatKTokens(defaults.contextTokens)} ctx)`
|
||||||
: "";
|
: "";
|
||||||
runtime.log(
|
const eventsValue =
|
||||||
info(`Default model: ${defaults.model ?? "unknown"}${defaultCtx}`),
|
summary.queuedSystemEvents.length > 0
|
||||||
);
|
? `${summary.queuedSystemEvents.length} queued`
|
||||||
runtime.log(info(`Active sessions: ${summary.sessions.count}`));
|
: "none";
|
||||||
if (summary.sessions.recent.length > 0) {
|
|
||||||
runtime.log("Recent sessions:");
|
const probesValue = health ? ok("enabled") : muted("skipped (use --deep)");
|
||||||
for (const r of summary.sessions.recent) {
|
|
||||||
runtime.log(
|
const overviewRows = [
|
||||||
`- ${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(", ")}` : ""}`,
|
{ Item: "Dashboard", Value: dashboard },
|
||||||
);
|
{ Item: "OS", Value: `${osSummary.label} · node ${process.versions.node}` },
|
||||||
}
|
{
|
||||||
} else {
|
Item: "Update",
|
||||||
runtime.log("No session activity yet.");
|
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("");
|
||||||
|
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) {
|
if (usage) {
|
||||||
|
runtime.log("");
|
||||||
|
runtime.log(theme.heading("Usage"));
|
||||||
for (const line of formatUsageReportLines(usage)) {
|
for (const line of formatUsageReportLines(usage)) {
|
||||||
runtime.log(line);
|
runtime.log(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runtime.log("");
|
||||||
runtime.log("FAQ: https://docs.clawd.bot/faq");
|
runtime.log("FAQ: https://docs.clawd.bot/faq");
|
||||||
runtime.log("Troubleshooting: https://docs.clawd.bot/troubleshooting");
|
runtime.log("Troubleshooting: https://docs.clawd.bot/troubleshooting");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { visibleWidth } from "./ansi.js";
|
||||||
import { renderTable } from "./table.js";
|
import { renderTable } from "./table.js";
|
||||||
|
|
||||||
describe("renderTable", () => {
|
describe("renderTable", () => {
|
||||||
@@ -16,4 +17,19 @@ describe("renderTable", () => {
|
|||||||
expect(out).toContain("Dashboard");
|
expect(out).toContain("Dashboard");
|
||||||
expect(out).toMatch(/│ Dashboard\s+│/);
|
expect(out).toMatch(/│ Dashboard\s+│/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("expands flex columns to fill available width", () => {
|
||||||
|
const width = 60;
|
||||||
|
const out = renderTable({
|
||||||
|
width,
|
||||||
|
columns: [
|
||||||
|
{ key: "Item", header: "Item", minWidth: 10 },
|
||||||
|
{ key: "Value", header: "Value", flex: true, minWidth: 24 },
|
||||||
|
],
|
||||||
|
rows: [{ Item: "OS", Value: "macos 26.2 (arm64)" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstLine = out.trimEnd().split("\n")[0] ?? "";
|
||||||
|
expect(visibleWidth(firstLine)).toBe(width);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -196,6 +196,39 @@ export function renderTable(opts: RenderTableOptions): string {
|
|||||||
shrink(nonFlexOrder, absoluteMinWidths);
|
shrink(nonFlexOrder, absoluteMinWidths);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have room and any flex columns, expand them to fill the available width.
|
||||||
|
// This keeps tables from looking "clipped" and reduces wrapping in wide terminals.
|
||||||
|
if (maxWidth) {
|
||||||
|
const sepCount = columns.length + 1;
|
||||||
|
const currentTotal = widths.reduce((a, b) => a + b, 0) + sepCount;
|
||||||
|
let extra = maxWidth - currentTotal;
|
||||||
|
if (extra > 0) {
|
||||||
|
const flexCols = columns
|
||||||
|
.map((c, i) => ({ c, i }))
|
||||||
|
.filter(({ c }) => Boolean(c.flex))
|
||||||
|
.map(({ i }) => i);
|
||||||
|
if (flexCols.length > 0) {
|
||||||
|
const caps = columns.map((c) =>
|
||||||
|
typeof c.maxWidth === "number" && c.maxWidth > 0
|
||||||
|
? Math.floor(c.maxWidth)
|
||||||
|
: Number.POSITIVE_INFINITY,
|
||||||
|
);
|
||||||
|
while (extra > 0) {
|
||||||
|
let progressed = false;
|
||||||
|
for (const i of flexCols) {
|
||||||
|
if ((widths[i] ?? 0) >= (caps[i] ?? Number.POSITIVE_INFINITY))
|
||||||
|
continue;
|
||||||
|
widths[i] = (widths[i] ?? 0) + 1;
|
||||||
|
extra -= 1;
|
||||||
|
progressed = true;
|
||||||
|
if (extra <= 0) break;
|
||||||
|
}
|
||||||
|
if (!progressed) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const box =
|
const box =
|
||||||
border === "ascii"
|
border === "ascii"
|
||||||
? {
|
? {
|
||||||
|
|||||||
Reference in New Issue
Block a user