fix(status): improve diagnostics and output
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user