fix(status): improve diagnostics and output

This commit is contained in:
Peter Steinberger
2026-01-11 02:42:14 +01:00
parent 2e2f05a0e1
commit e824b3514b
18 changed files with 632 additions and 82 deletions

View File

@@ -1,5 +1,11 @@
export type ProviderStatusIssue = {
provider: "discord" | "telegram" | "whatsapp";
provider:
| "discord"
| "telegram"
| "whatsapp"
| "slack"
| "signal"
| "imessage";
accountId: string;
kind: "intent" | "permissions" | "config" | "auth" | "runtime";
message: string;
@@ -40,12 +46,37 @@ type WhatsAppAccountStatus = {
lastError?: unknown;
};
type RuntimeAccountStatus = {
accountId?: unknown;
enabled?: unknown;
configured?: unknown;
running?: unknown;
lastError?: unknown;
};
function asString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0
? value.trim()
: undefined;
}
function formatValue(value: unknown): string | undefined {
const s = asString(value);
if (s) return s;
if (value == null) return undefined;
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function shorten(message: string, maxLen = 140): string {
const cleaned = message.replace(/\s+/g, " ").trim();
if (cleaned.length <= maxLen) return cleaned;
return `${cleaned.slice(0, Math.max(0, maxLen - 1))}`;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
@@ -191,6 +222,17 @@ function readWhatsAppAccountStatus(
};
}
function readRuntimeAccountStatus(value: unknown): RuntimeAccountStatus | null {
if (!isRecord(value)) return null;
return {
accountId: value.accountId,
enabled: value.enabled,
configured: value.configured,
running: value.running,
lastError: value.lastError,
};
}
export function collectProvidersStatusIssues(
payload: Record<string, unknown>,
): ProviderStatusIssue[] {
@@ -342,5 +384,68 @@ export function collectProvidersStatusIssues(
}
}
const slackAccountsRaw = payload.slackAccounts;
if (Array.isArray(slackAccountsRaw)) {
for (const entry of slackAccountsRaw) {
const account = readRuntimeAccountStatus(entry);
if (!account) continue;
const accountId = asString(account.accountId) ?? "default";
const enabled = account.enabled !== false;
const configured = account.configured === true;
if (!enabled || !configured) continue;
const lastError = formatValue(account.lastError);
if (!lastError) continue;
issues.push({
provider: "slack",
accountId,
kind: "runtime",
message: `Provider error: ${shorten(lastError)}`,
fix: "Check gateway logs (`clawdbot logs --follow`) and re-auth/restart if needed.",
});
}
}
const signalAccountsRaw = payload.signalAccounts;
if (Array.isArray(signalAccountsRaw)) {
for (const entry of signalAccountsRaw) {
const account = readRuntimeAccountStatus(entry);
if (!account) continue;
const accountId = asString(account.accountId) ?? "default";
const enabled = account.enabled !== false;
const configured = account.configured === true;
if (!enabled || !configured) continue;
const lastError = formatValue(account.lastError);
if (!lastError) continue;
issues.push({
provider: "signal",
accountId,
kind: "runtime",
message: `Provider error: ${shorten(lastError)}`,
fix: "Check gateway logs (`clawdbot logs --follow`) and verify signal CLI/service setup.",
});
}
}
const imessageAccountsRaw = payload.imessageAccounts;
if (Array.isArray(imessageAccountsRaw)) {
for (const entry of imessageAccountsRaw) {
const account = readRuntimeAccountStatus(entry);
if (!account) continue;
const accountId = asString(account.accountId) ?? "default";
const enabled = account.enabled !== false;
const configured = account.configured === true;
if (!enabled || !configured) continue;
const lastError = formatValue(account.lastError);
if (!lastError) continue;
issues.push({
provider: "imessage",
accountId,
kind: "runtime",
message: `Provider error: ${shorten(lastError)}`,
fix: "Check macOS permissions/TCC and gateway logs (`clawdbot logs --follow`).",
});
}
}
return issues;
}

View File

@@ -12,6 +12,16 @@ import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { colorize, isRich, theme } from "../terminal/theme.js";
import { ensureBinary } from "./binaries.js";
function parsePossiblyNoisyJsonObject(stdout: string): Record<string, unknown> {
const trimmed = stdout.trim();
const start = trimmed.indexOf("{");
const end = trimmed.lastIndexOf("}");
if (start >= 0 && end > start) {
return JSON.parse(trimmed.slice(start, end + 1)) as Record<string, unknown>;
}
return JSON.parse(trimmed) as Record<string, unknown>;
}
export async function getTailnetHostname(exec: typeof runExec = runExec) {
// Derive tailnet hostname (or IP fallback) from tailscale status JSON.
const candidates = [
@@ -24,9 +34,7 @@ export async function getTailnetHostname(exec: typeof runExec = runExec) {
if (candidate.startsWith("/") && !existsSync(candidate)) continue;
try {
const { stdout } = await exec(candidate, ["status", "--json"]);
const parsed = stdout
? (JSON.parse(stdout) as Record<string, unknown>)
: {};
const parsed = stdout ? parsePossiblyNoisyJsonObject(stdout) : {};
const self =
typeof parsed.Self === "object" && parsed.Self !== null
? (parsed.Self as Record<string, unknown>)
@@ -49,6 +57,17 @@ export async function getTailnetHostname(exec: typeof runExec = runExec) {
throw lastError ?? new Error("Could not determine Tailscale DNS or IP");
}
export async function readTailscaleStatusJson(
exec: typeof runExec = runExec,
opts?: { timeoutMs?: number },
): Promise<Record<string, unknown>> {
const { stdout } = await exec("tailscale", ["status", "--json"], {
timeoutMs: opts?.timeoutMs ?? 5000,
maxBuffer: 400_000,
});
return stdout ? parsePossiblyNoisyJsonObject(stdout) : {};
}
export async function ensureGoInstalled(
exec: typeof runExec = runExec,
prompt: typeof promptYesNo = promptYesNo,