diff --git a/src/cli/progress.ts b/src/cli/progress.ts index e7e6c5862..a974d2853 100644 --- a/src/cli/progress.ts +++ b/src/cli/progress.ts @@ -23,6 +23,12 @@ export type ProgressReporter = { done: () => void; }; +export type ProgressTotalsUpdate = { + completed: number; + total: number; + label?: string; +}; + const noopReporter: ProgressReporter = { setLabel: () => {}, setPercent: () => {}, @@ -133,3 +139,20 @@ export async function withProgress( progress.done(); } } + +export async function withProgressTotals( + options: ProgressOptions, + work: ( + update: (update: ProgressTotalsUpdate) => void, + progress: ProgressReporter, + ) => Promise, +): Promise { + return await withProgress(options, async (progress) => { + const update = ({ completed, total, label }: ProgressTotalsUpdate) => { + if (label) progress.setLabel(label); + if (!Number.isFinite(total) || total <= 0) return; + progress.setPercent((completed / total) * 100); + }; + return await work(update, progress); + }); +} diff --git a/src/cli/tagline.ts b/src/cli/tagline.ts index 20b49ebc0..2ab25feeb 100644 --- a/src/cli/tagline.ts +++ b/src/cli/tagline.ts @@ -1,5 +1,4 @@ -const DEFAULT_TAGLINE = - "Send, receive, and auto-reply on WhatsApp (web) and Telegram (bot)."; +const DEFAULT_TAGLINE = "All your chats, one ClawdBot."; const TAGLINES: string[] = []; diff --git a/src/commands/models/scan.ts b/src/commands/models/scan.ts index 14993f40b..1c2380d08 100644 --- a/src/commands/models/scan.ts +++ b/src/commands/models/scan.ts @@ -4,7 +4,7 @@ import { type ModelScanResult, scanOpenRouterModels, } from "../../agents/model-scan.js"; -import { withProgress } from "../../cli/progress.js"; +import { withProgressTotals } from "../../cli/progress.js"; import { CONFIG_PATH_CLAWDBOT, loadConfig } from "../../config/config.js"; import type { RuntimeEnv } from "../../runtime.js"; import { formatMs, formatTokenK, updateConfig } from "./shared.js"; @@ -189,13 +189,13 @@ export async function modelsScanCommand( storedKey = undefined; } } - const results = await withProgress( + const results = await withProgressTotals( { label: "Scanning OpenRouter models...", indeterminate: false, enabled: opts.json !== true, }, - async (progress) => + async (update) => await scanOpenRouterModels({ apiKey: storedKey ?? undefined, minParamB: minParams, @@ -206,10 +206,12 @@ export async function modelsScanCommand( probe, onProgress: ({ phase, completed, total }) => { if (phase !== "probe") return; - if (total <= 0) return; const labelBase = probe ? "Probing models" : "Scanning models"; - progress.setLabel(`${labelBase} (${completed}/${total})`); - progress.setPercent((completed / total) * 100); + update({ + completed, + total, + label: `${labelBase} (${completed}/${total})`, + }); }, }), ); diff --git a/src/infra/provider-summary.ts b/src/infra/provider-summary.ts index 93baf56c8..349925e02 100644 --- a/src/infra/provider-summary.ts +++ b/src/infra/provider-summary.ts @@ -1,6 +1,6 @@ -import chalk from "chalk"; import { type ClawdbotConfig, loadConfig } from "../config/config.js"; import { resolveTelegramToken } from "../telegram/token.js"; +import { theme } from "../terminal/theme.js"; import { normalizeE164 } from "../utils.js"; import { getWebAuthAgeMs, @@ -30,7 +30,7 @@ export async function buildProviderSummary( const webEnabled = effective.web?.enabled !== false; if (!webEnabled) { - lines.push(tint("WhatsApp: disabled", chalk.cyan)); + lines.push(tint("WhatsApp: disabled", theme.muted)); } else { const webLinked = await webAuthExists(); const authAgeMs = getWebAuthAgeMs(); @@ -40,28 +40,28 @@ export async function buildProviderSummary( webLinked ? tint( `WhatsApp: linked${e164 ? ` ${e164}` : ""}${authAge}`, - chalk.green, + theme.success, ) - : tint("WhatsApp: not linked", chalk.red), + : tint("WhatsApp: not linked", theme.error), ); } const telegramEnabled = effective.telegram?.enabled !== false; if (!telegramEnabled) { - lines.push(tint("Telegram: disabled", chalk.cyan)); + lines.push(tint("Telegram: disabled", theme.muted)); } else { const { token: telegramToken } = resolveTelegramToken(effective); const telegramConfigured = Boolean(telegramToken?.trim()); lines.push( telegramConfigured - ? tint("Telegram: configured", chalk.green) - : tint("Telegram: not configured", chalk.cyan), + ? tint("Telegram: configured", theme.success) + : tint("Telegram: not configured", theme.muted), ); } const signalEnabled = effective.signal?.enabled !== false; if (!signalEnabled) { - lines.push(tint("Signal: disabled", chalk.cyan)); + lines.push(tint("Signal: disabled", theme.muted)); } else { const signalConfigured = Boolean(effective.signal) && @@ -75,20 +75,20 @@ export async function buildProviderSummary( ); lines.push( signalConfigured - ? tint("Signal: configured", chalk.green) - : tint("Signal: not configured", chalk.cyan), + ? tint("Signal: configured", theme.success) + : tint("Signal: not configured", theme.muted), ); } const imessageEnabled = effective.imessage?.enabled !== false; if (!imessageEnabled) { - lines.push(tint("iMessage: disabled", chalk.cyan)); + lines.push(tint("iMessage: disabled", theme.muted)); } else { const imessageConfigured = Boolean(effective.imessage); lines.push( imessageConfigured - ? tint("iMessage: configured", chalk.green) - : tint("iMessage: not configured", chalk.cyan), + ? tint("iMessage: configured", theme.success) + : tint("iMessage: not configured", theme.muted), ); } @@ -97,7 +97,7 @@ export async function buildProviderSummary( ? effective.whatsapp.allowFrom.map(normalizeE164).filter(Boolean) : []; if (allowFrom.length) { - lines.push(tint(`AllowFrom: ${allowFrom.join(", ")}`, chalk.cyan)); + lines.push(tint(`AllowFrom: ${allowFrom.join(", ")}`, theme.muted)); } } diff --git a/src/infra/tailscale.ts b/src/infra/tailscale.ts index 3bd7ff9ad..1dc2ab2e0 100644 --- a/src/infra/tailscale.ts +++ b/src/infra/tailscale.ts @@ -1,5 +1,4 @@ import { existsSync } from "node:fs"; -import chalk from "chalk"; import { promptYesNo } from "../cli/prompt.js"; import { danger, @@ -8,6 +7,7 @@ import { shouldLogVerbose, warn, } from "../globals.js"; +import { colorize, isRich, theme } from "../terminal/theme.js"; import { runExec } from "../process/exec.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { ensureBinary } from "./binaries.js"; @@ -180,8 +180,17 @@ export async function ensureFunnel( ), ); if (shouldLogVerbose()) { - if (stdout.trim()) runtime.error(chalk.gray(`stdout: ${stdout.trim()}`)); - if (stderr.trim()) runtime.error(chalk.gray(`stderr: ${stderr.trim()}`)); + const rich = isRich(); + if (stdout.trim()) { + runtime.error( + colorize(rich, theme.muted, `stdout: ${stdout.trim()}`), + ); + } + if (stderr.trim()) { + runtime.error( + colorize(rich, theme.muted, `stderr: ${stderr.trim()}`), + ); + } runtime.error(err as Error); } runtime.exit(1); diff --git a/src/terminal/theme.ts b/src/terminal/theme.ts index e084db5cf..c11a28d93 100644 --- a/src/terminal/theme.ts +++ b/src/terminal/theme.ts @@ -1,5 +1,6 @@ import chalk from "chalk"; +// Semantic palette for CLI output. Keep in sync with docs/cli/index.md. export const LOBSTER_PALETTE = { accent: "#FF5A2D", accentBright: "#FF7A3D",