feat: refresh CLI output styling and progress
This commit is contained in:
@@ -16,6 +16,12 @@ If you change the CLI code, update this doc.
|
|||||||
- `--profile <name>`: isolate state under `~/.clawdbot-<name>`.
|
- `--profile <name>`: isolate state under `~/.clawdbot-<name>`.
|
||||||
- `-V`, `--version`, `-v`: print version and exit.
|
- `-V`, `--version`, `-v`: print version and exit.
|
||||||
|
|
||||||
|
## Output styling
|
||||||
|
|
||||||
|
- ANSI colors and progress indicators only render in TTY sessions.
|
||||||
|
- `--json` (and `--plain` where supported) disables styling for clean output.
|
||||||
|
- Long-running commands show a progress indicator (OSC 9;4 when supported).
|
||||||
|
|
||||||
## Command tree
|
## Command tree
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -321,7 +327,7 @@ Options:
|
|||||||
- `--json`
|
- `--json`
|
||||||
|
|
||||||
#### `agents add [name]`
|
#### `agents add [name]`
|
||||||
Add a new isolated agent. If `--workspace` is omitted, runs the guided wizard.
|
Add a new isolated agent. Runs the guided wizard unless flags are passed; `--workspace` is required in non-interactive mode.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
- `--workspace <dir>`
|
- `--workspace <dir>`
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
"grammy": "^1.39.2",
|
"grammy": "^1.39.2",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"long": "5.3.2",
|
"long": "5.3.2",
|
||||||
|
"osc-progress": "^0.2.0",
|
||||||
"playwright-core": "1.57.0",
|
"playwright-core": "1.57.0",
|
||||||
"proper-lockfile": "^4.1.2",
|
"proper-lockfile": "^4.1.2",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"qrcode-terminal": "^0.12.0",
|
||||||
|
|||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -112,6 +112,9 @@ importers:
|
|||||||
long:
|
long:
|
||||||
specifier: 5.3.2
|
specifier: 5.3.2
|
||||||
version: 5.3.2
|
version: 5.3.2
|
||||||
|
osc-progress:
|
||||||
|
specifier: ^0.2.0
|
||||||
|
version: 0.2.0
|
||||||
playwright-core:
|
playwright-core:
|
||||||
specifier: 1.57.0
|
specifier: 1.57.0
|
||||||
version: 1.57.0(patch_hash=66f1f266424dbe354068aaa5bba87bfb0e1d7d834a938c25dd70d43cdf1c1b02)
|
version: 1.57.0(patch_hash=66f1f266424dbe354068aaa5bba87bfb0e1d7d834a938c25dd70d43cdf1c1b02)
|
||||||
@@ -2428,6 +2431,10 @@ packages:
|
|||||||
opus-decoder@0.7.11:
|
opus-decoder@0.7.11:
|
||||||
resolution: {integrity: sha512-+e+Jz3vGQLxRTBHs8YJQPRPc1Tr+/aC6coV/DlZylriA29BdHQAYXhvNRKtjftof17OFng0+P4wsFIqQu3a48A==}
|
resolution: {integrity: sha512-+e+Jz3vGQLxRTBHs8YJQPRPc1Tr+/aC6coV/DlZylriA29BdHQAYXhvNRKtjftof17OFng0+P4wsFIqQu3a48A==}
|
||||||
|
|
||||||
|
osc-progress@0.2.0:
|
||||||
|
resolution: {integrity: sha512-GJR9XnS8dQ+sAdbhX90RA4WbmEyrso7X9aHMws4MaQ2GRpfEjnOUSZIdOXJQfnIfBoy9oCc7US/MNFCyuJQzjg==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
oxlint-tsgolint@0.10.1:
|
oxlint-tsgolint@0.10.1:
|
||||||
resolution: {integrity: sha512-EEHNdo5cW2w1xwYdBQ7d3IXDqWAtMkfVFrh+9gQ4kYbYJwygY4QXSh1eH80/xVipZdVKujAwBgg/nNNHk56kxQ==}
|
resolution: {integrity: sha512-EEHNdo5cW2w1xwYdBQ7d3IXDqWAtMkfVFrh+9gQ4kYbYJwygY4QXSh1eH80/xVipZdVKujAwBgg/nNNHk56kxQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -5294,6 +5301,8 @@ snapshots:
|
|||||||
'@wasm-audio-decoders/common': 9.0.7
|
'@wasm-audio-decoders/common': 9.0.7
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
osc-progress@0.2.0: {}
|
||||||
|
|
||||||
oxlint-tsgolint@0.10.1:
|
oxlint-tsgolint@0.10.1:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@oxlint-tsgolint/darwin-arm64': 0.10.1
|
'@oxlint-tsgolint/darwin-arm64': 0.10.1
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
|
import { withProgress } from "./progress.js";
|
||||||
import { writeBase64ToFile } from "./nodes-camera.js";
|
import { writeBase64ToFile } from "./nodes-camera.js";
|
||||||
import {
|
import {
|
||||||
canvasSnapshotTempPath,
|
canvasSnapshotTempPath,
|
||||||
@@ -80,15 +81,23 @@ const callGatewayCli = async (
|
|||||||
opts: CanvasOpts,
|
opts: CanvasOpts,
|
||||||
params?: unknown,
|
params?: unknown,
|
||||||
) =>
|
) =>
|
||||||
callGateway({
|
withProgress(
|
||||||
url: opts.url,
|
{
|
||||||
token: opts.token,
|
label: `Canvas ${method}`,
|
||||||
method,
|
indeterminate: true,
|
||||||
params,
|
enabled: opts.json !== true,
|
||||||
timeoutMs: Number(opts.timeout ?? 10_000),
|
},
|
||||||
clientName: "cli",
|
async () =>
|
||||||
mode: "cli",
|
await callGateway({
|
||||||
});
|
url: opts.url,
|
||||||
|
token: opts.token,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
timeoutMs: Number(opts.timeout ?? 10_000),
|
||||||
|
clientName: "cli",
|
||||||
|
mode: "cli",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
function parseNodeList(value: unknown): NodeListNode[] {
|
function parseNodeList(value: unknown): NodeListNode[] {
|
||||||
const obj =
|
const obj =
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import chalk from "chalk";
|
|
||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import type { CronJob, CronSchedule } from "../cron/types.js";
|
import type { CronJob, CronSchedule } from "../cron/types.js";
|
||||||
import { danger } from "../globals.js";
|
import { danger } from "../globals.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
|
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||||
import type { GatewayRpcOpts } from "./gateway-rpc.js";
|
import type { GatewayRpcOpts } from "./gateway-rpc.js";
|
||||||
import { addGatewayClientOptions, callGatewayFromCli } from "./gateway-rpc.js";
|
import { addGatewayClientOptions, callGatewayFromCli } from "./gateway-rpc.js";
|
||||||
|
|
||||||
@@ -69,8 +69,6 @@ const CRON_LAST_PAD = 10;
|
|||||||
const CRON_STATUS_PAD = 9;
|
const CRON_STATUS_PAD = 9;
|
||||||
const CRON_TARGET_PAD = 9;
|
const CRON_TARGET_PAD = 9;
|
||||||
|
|
||||||
const isRich = () => Boolean(process.stdout.isTTY && chalk.level > 0);
|
|
||||||
|
|
||||||
const pad = (value: string, width: number) => value.padEnd(width);
|
const pad = (value: string, width: number) => value.padEnd(width);
|
||||||
|
|
||||||
const truncate = (value: string, width: number) => {
|
const truncate = (value: string, width: number) => {
|
||||||
@@ -122,12 +120,6 @@ const formatStatus = (job: CronJob) => {
|
|||||||
return job.state.lastStatus ?? "idle";
|
return job.state.lastStatus ?? "idle";
|
||||||
};
|
};
|
||||||
|
|
||||||
const colorize = (
|
|
||||||
rich: boolean,
|
|
||||||
color: (msg: string) => string,
|
|
||||||
msg: string,
|
|
||||||
) => (rich ? color(msg) : msg);
|
|
||||||
|
|
||||||
function printCronList(jobs: CronJob[], runtime = defaultRuntime) {
|
function printCronList(jobs: CronJob[], runtime = defaultRuntime) {
|
||||||
if (jobs.length === 0) {
|
if (jobs.length === 0) {
|
||||||
runtime.log("No cron jobs.");
|
runtime.log("No cron jobs.");
|
||||||
@@ -145,7 +137,7 @@ function printCronList(jobs: CronJob[], runtime = defaultRuntime) {
|
|||||||
pad("Target", CRON_TARGET_PAD),
|
pad("Target", CRON_TARGET_PAD),
|
||||||
].join(" ");
|
].join(" ");
|
||||||
|
|
||||||
runtime.log(rich ? chalk.bold(header) : header);
|
runtime.log(rich ? theme.heading(header) : header);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
for (const job of jobs) {
|
for (const job of jobs) {
|
||||||
@@ -168,26 +160,28 @@ function printCronList(jobs: CronJob[], runtime = defaultRuntime) {
|
|||||||
const targetLabel = pad(job.sessionTarget, CRON_TARGET_PAD);
|
const targetLabel = pad(job.sessionTarget, CRON_TARGET_PAD);
|
||||||
|
|
||||||
const coloredStatus = (() => {
|
const coloredStatus = (() => {
|
||||||
if (statusRaw === "ok") return colorize(rich, chalk.green, statusLabel);
|
if (statusRaw === "ok")
|
||||||
if (statusRaw === "error") return colorize(rich, chalk.red, statusLabel);
|
return colorize(rich, theme.success, statusLabel);
|
||||||
|
if (statusRaw === "error")
|
||||||
|
return colorize(rich, theme.error, statusLabel);
|
||||||
if (statusRaw === "running")
|
if (statusRaw === "running")
|
||||||
return colorize(rich, chalk.yellow, statusLabel);
|
return colorize(rich, theme.warn, statusLabel);
|
||||||
if (statusRaw === "skipped")
|
if (statusRaw === "skipped")
|
||||||
return colorize(rich, chalk.gray, statusLabel);
|
return colorize(rich, theme.muted, statusLabel);
|
||||||
return colorize(rich, chalk.gray, statusLabel);
|
return colorize(rich, theme.muted, statusLabel);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const coloredTarget =
|
const coloredTarget =
|
||||||
job.sessionTarget === "isolated"
|
job.sessionTarget === "isolated"
|
||||||
? colorize(rich, chalk.magenta, targetLabel)
|
? colorize(rich, theme.accentBright, targetLabel)
|
||||||
: colorize(rich, chalk.cyan, targetLabel);
|
: colorize(rich, theme.accent, targetLabel);
|
||||||
|
|
||||||
const line = [
|
const line = [
|
||||||
colorize(rich, chalk.cyan, idLabel),
|
colorize(rich, theme.accent, idLabel),
|
||||||
colorize(rich, chalk.white, nameLabel),
|
colorize(rich, theme.info, nameLabel),
|
||||||
colorize(rich, chalk.white, scheduleLabel),
|
colorize(rich, theme.info, scheduleLabel),
|
||||||
colorize(rich, chalk.gray, nextLabel),
|
colorize(rich, theme.muted, nextLabel),
|
||||||
colorize(rich, chalk.gray, lastLabel),
|
colorize(rich, theme.muted, lastLabel),
|
||||||
coloredStatus,
|
coloredStatus,
|
||||||
coloredTarget,
|
coloredTarget,
|
||||||
].join(" ");
|
].join(" ");
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
} from "../infra/ports.js";
|
} from "../infra/ports.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { createDefaultDeps } from "./deps.js";
|
import { createDefaultDeps } from "./deps.js";
|
||||||
|
import { withProgress } from "./progress.js";
|
||||||
|
|
||||||
type DaemonStatus = {
|
type DaemonStatus = {
|
||||||
service: {
|
service: {
|
||||||
@@ -74,6 +75,7 @@ export type GatewayRpcOpts = {
|
|||||||
token?: string;
|
token?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
timeout?: string;
|
timeout?: string;
|
||||||
|
json?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DaemonStatusOptions = {
|
export type DaemonStatusOptions = {
|
||||||
@@ -104,15 +106,23 @@ function parsePort(raw: unknown): number | null {
|
|||||||
|
|
||||||
async function probeGatewayStatus(opts: GatewayRpcOpts) {
|
async function probeGatewayStatus(opts: GatewayRpcOpts) {
|
||||||
try {
|
try {
|
||||||
await callGateway({
|
await withProgress(
|
||||||
url: opts.url,
|
{
|
||||||
token: opts.token,
|
label: "Checking gateway status...",
|
||||||
password: opts.password,
|
indeterminate: true,
|
||||||
method: "status",
|
enabled: opts.json !== true,
|
||||||
timeoutMs: Number(opts.timeout ?? 10_000),
|
},
|
||||||
clientName: "cli",
|
async () =>
|
||||||
mode: "cli",
|
await callGateway({
|
||||||
});
|
url: opts.url,
|
||||||
|
token: opts.token,
|
||||||
|
password: opts.password,
|
||||||
|
method: "status",
|
||||||
|
timeoutMs: Number(opts.timeout ?? 10_000),
|
||||||
|
clientName: "cli",
|
||||||
|
mode: "cli",
|
||||||
|
}),
|
||||||
|
);
|
||||||
return { ok: true } as const;
|
return { ok: true } as const;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
} from "./daemon-cli.js";
|
} from "./daemon-cli.js";
|
||||||
import { createDefaultDeps } from "./deps.js";
|
import { createDefaultDeps } from "./deps.js";
|
||||||
import { forceFreePortAndWait } from "./ports.js";
|
import { forceFreePortAndWait } from "./ports.js";
|
||||||
|
import { withProgress } from "./progress.js";
|
||||||
|
|
||||||
type GatewayRpcOpts = {
|
type GatewayRpcOpts = {
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -211,17 +212,25 @@ const callGatewayCli = async (
|
|||||||
opts: GatewayRpcOpts,
|
opts: GatewayRpcOpts,
|
||||||
params?: unknown,
|
params?: unknown,
|
||||||
) =>
|
) =>
|
||||||
callGateway({
|
withProgress(
|
||||||
url: opts.url,
|
{
|
||||||
token: opts.token,
|
label: `Gateway ${method}`,
|
||||||
password: opts.password,
|
indeterminate: true,
|
||||||
method,
|
enabled: true,
|
||||||
params,
|
},
|
||||||
expectFinal: Boolean(opts.expectFinal),
|
async () =>
|
||||||
timeoutMs: Number(opts.timeout ?? 10_000),
|
await callGateway({
|
||||||
clientName: "cli",
|
url: opts.url,
|
||||||
mode: "cli",
|
token: opts.token,
|
||||||
});
|
password: opts.password,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
expectFinal: Boolean(opts.expectFinal),
|
||||||
|
timeoutMs: Number(opts.timeout ?? 10_000),
|
||||||
|
clientName: "cli",
|
||||||
|
mode: "cli",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export function registerGatewayCli(program: Command) {
|
export function registerGatewayCli(program: Command) {
|
||||||
program
|
program
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { callGateway } from "../gateway/call.js";
|
import { callGateway } from "../gateway/call.js";
|
||||||
|
import { withProgress } from "./progress.js";
|
||||||
|
|
||||||
export type GatewayRpcOpts = {
|
export type GatewayRpcOpts = {
|
||||||
url?: string;
|
url?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
timeout?: string;
|
timeout?: string;
|
||||||
expectFinal?: boolean;
|
expectFinal?: boolean;
|
||||||
|
json?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function addGatewayClientOptions(cmd: Command) {
|
export function addGatewayClientOptions(cmd: Command) {
|
||||||
@@ -25,14 +27,22 @@ export async function callGatewayFromCli(
|
|||||||
params?: unknown,
|
params?: unknown,
|
||||||
extra?: { expectFinal?: boolean },
|
extra?: { expectFinal?: boolean },
|
||||||
) {
|
) {
|
||||||
return await callGateway({
|
return await withProgress(
|
||||||
url: opts.url,
|
{
|
||||||
token: opts.token,
|
label: `Gateway ${method}`,
|
||||||
method,
|
indeterminate: true,
|
||||||
params,
|
enabled: opts.json !== true,
|
||||||
expectFinal: extra?.expectFinal ?? Boolean(opts.expectFinal),
|
},
|
||||||
timeoutMs: Number(opts.timeout ?? 10_000),
|
async () =>
|
||||||
clientName: "cli",
|
await callGateway({
|
||||||
mode: "cli",
|
url: opts.url,
|
||||||
});
|
token: opts.token,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
expectFinal: extra?.expectFinal ?? Boolean(opts.expectFinal),
|
||||||
|
timeoutMs: Number(opts.timeout ?? 10_000),
|
||||||
|
clientName: "cli",
|
||||||
|
mode: "cli",
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
|
import { withProgress } from "./progress.js";
|
||||||
import {
|
import {
|
||||||
type CameraFacing,
|
type CameraFacing,
|
||||||
cameraTempPath,
|
cameraTempPath,
|
||||||
@@ -117,15 +118,23 @@ const callGatewayCli = async (
|
|||||||
opts: NodesRpcOpts,
|
opts: NodesRpcOpts,
|
||||||
params?: unknown,
|
params?: unknown,
|
||||||
) =>
|
) =>
|
||||||
callGateway({
|
withProgress(
|
||||||
url: opts.url,
|
{
|
||||||
token: opts.token,
|
label: `Nodes ${method}`,
|
||||||
method,
|
indeterminate: true,
|
||||||
params,
|
enabled: opts.json !== true,
|
||||||
timeoutMs: Number(opts.timeout ?? 10_000),
|
},
|
||||||
clientName: "cli",
|
async () =>
|
||||||
mode: "cli",
|
await callGateway({
|
||||||
});
|
url: opts.url,
|
||||||
|
token: opts.token,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
timeoutMs: Number(opts.timeout ?? 10_000),
|
||||||
|
clientName: "cli",
|
||||||
|
mode: "cli",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
function formatAge(msAgo: number) {
|
function formatAge(msAgo: number) {
|
||||||
const s = Math.max(0, Math.floor(msAgo / 1000));
|
const s = Math.max(0, Math.floor(msAgo / 1000));
|
||||||
|
|||||||
138
src/cli/progress.ts
Normal file
138
src/cli/progress.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { spinner } from "@clack/prompts";
|
||||||
|
import {
|
||||||
|
createOscProgressController,
|
||||||
|
supportsOscProgress,
|
||||||
|
} from "osc-progress";
|
||||||
|
|
||||||
|
import { theme } from "../terminal/theme.js";
|
||||||
|
|
||||||
|
const DEFAULT_DELAY_MS = 300;
|
||||||
|
let activeProgress = 0;
|
||||||
|
|
||||||
|
type ProgressOptions = {
|
||||||
|
label: string;
|
||||||
|
indeterminate?: boolean;
|
||||||
|
total?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
delayMs?: number;
|
||||||
|
stream?: NodeJS.WriteStream;
|
||||||
|
fallback?: "spinner" | "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProgressReporter = {
|
||||||
|
setLabel: (label: string) => void;
|
||||||
|
setPercent: (percent: number) => void;
|
||||||
|
tick: (delta?: number) => void;
|
||||||
|
done: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const noopReporter: ProgressReporter = {
|
||||||
|
setLabel: () => {},
|
||||||
|
setPercent: () => {},
|
||||||
|
tick: () => {},
|
||||||
|
done: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createCliProgress(options: ProgressOptions): ProgressReporter {
|
||||||
|
if (options.enabled === false) return noopReporter;
|
||||||
|
if (activeProgress > 0) return noopReporter;
|
||||||
|
|
||||||
|
const stream = options.stream ?? process.stderr;
|
||||||
|
if (!stream.isTTY) return noopReporter;
|
||||||
|
|
||||||
|
const delayMs =
|
||||||
|
typeof options.delayMs === "number" ? options.delayMs : DEFAULT_DELAY_MS;
|
||||||
|
const canOsc = supportsOscProgress(process.env, stream.isTTY);
|
||||||
|
const allowSpinner =
|
||||||
|
!canOsc && (options.fallback === undefined || options.fallback === "spinner");
|
||||||
|
|
||||||
|
let started = false;
|
||||||
|
let label = options.label;
|
||||||
|
let total = options.total ?? null;
|
||||||
|
let completed = 0;
|
||||||
|
let percent = 0;
|
||||||
|
let indeterminate =
|
||||||
|
options.indeterminate ??
|
||||||
|
(options.total === undefined || options.total === null);
|
||||||
|
|
||||||
|
activeProgress += 1;
|
||||||
|
|
||||||
|
const controller = canOsc
|
||||||
|
? createOscProgressController({
|
||||||
|
env: process.env,
|
||||||
|
isTty: stream.isTTY,
|
||||||
|
write: (chunk) => stream.write(chunk),
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const spin = allowSpinner ? spinner() : null;
|
||||||
|
let timer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
const applyState = () => {
|
||||||
|
if (!started) return;
|
||||||
|
if (controller) {
|
||||||
|
if (indeterminate) controller.setIndeterminate(label);
|
||||||
|
else controller.setPercent(label, percent);
|
||||||
|
} else if (spin) {
|
||||||
|
spin.message(theme.accent(label));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = () => {
|
||||||
|
if (started) return;
|
||||||
|
started = true;
|
||||||
|
if (spin) {
|
||||||
|
spin.start(theme.accent(label));
|
||||||
|
}
|
||||||
|
applyState();
|
||||||
|
};
|
||||||
|
|
||||||
|
timer = setTimeout(start, delayMs);
|
||||||
|
|
||||||
|
const setLabel = (next: string) => {
|
||||||
|
label = next;
|
||||||
|
applyState();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPercent = (nextPercent: number) => {
|
||||||
|
percent = Math.max(0, Math.min(100, Math.round(nextPercent)));
|
||||||
|
indeterminate = false;
|
||||||
|
applyState();
|
||||||
|
};
|
||||||
|
|
||||||
|
const tick = (delta = 1) => {
|
||||||
|
if (!total) return;
|
||||||
|
completed = Math.min(total, completed + delta);
|
||||||
|
const nextPercent =
|
||||||
|
total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||||
|
setPercent(nextPercent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const done = () => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
if (!started) {
|
||||||
|
activeProgress = Math.max(0, activeProgress - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (controller) controller.clear();
|
||||||
|
if (spin) spin.stop();
|
||||||
|
activeProgress = Math.max(0, activeProgress - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { setLabel, setPercent, tick, done };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function withProgress<T>(
|
||||||
|
options: ProgressOptions,
|
||||||
|
work: (progress: ProgressReporter) => Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
|
const progress = createCliProgress(options);
|
||||||
|
try {
|
||||||
|
return await work(progress);
|
||||||
|
} finally {
|
||||||
|
progress.done();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { withProgress } from "../cli/progress.js";
|
||||||
import { normalizeMessageProvider } from "../utils/message-provider.js";
|
import { normalizeMessageProvider } from "../utils/message-provider.js";
|
||||||
import { agentCommand } from "./agent.js";
|
import { agentCommand } from "./agent.js";
|
||||||
|
|
||||||
@@ -125,26 +126,34 @@ export async function agentViaGatewayCommand(
|
|||||||
const provider = normalizeMessageProvider(opts.provider) ?? "whatsapp";
|
const provider = normalizeMessageProvider(opts.provider) ?? "whatsapp";
|
||||||
const idempotencyKey = opts.runId?.trim() || randomIdempotencyKey();
|
const idempotencyKey = opts.runId?.trim() || randomIdempotencyKey();
|
||||||
|
|
||||||
const response = await callGateway<GatewayAgentResponse>({
|
const response = await withProgress(
|
||||||
method: "agent",
|
{
|
||||||
params: {
|
label: "Waiting for agent reply…",
|
||||||
message: body,
|
indeterminate: true,
|
||||||
to: opts.to,
|
enabled: opts.json !== true,
|
||||||
sessionId: opts.sessionId,
|
|
||||||
sessionKey,
|
|
||||||
thinking: opts.thinking,
|
|
||||||
deliver: Boolean(opts.deliver),
|
|
||||||
provider,
|
|
||||||
timeout: timeoutSeconds,
|
|
||||||
lane: opts.lane,
|
|
||||||
extraSystemPrompt: opts.extraSystemPrompt,
|
|
||||||
idempotencyKey,
|
|
||||||
},
|
},
|
||||||
expectFinal: true,
|
async () =>
|
||||||
timeoutMs: gatewayTimeoutMs,
|
await callGateway<GatewayAgentResponse>({
|
||||||
clientName: "cli",
|
method: "agent",
|
||||||
mode: "cli",
|
params: {
|
||||||
});
|
message: body,
|
||||||
|
to: opts.to,
|
||||||
|
sessionId: opts.sessionId,
|
||||||
|
sessionKey,
|
||||||
|
thinking: opts.thinking,
|
||||||
|
deliver: Boolean(opts.deliver),
|
||||||
|
provider,
|
||||||
|
timeout: timeoutSeconds,
|
||||||
|
lane: opts.lane,
|
||||||
|
extraSystemPrompt: opts.extraSystemPrompt,
|
||||||
|
idempotencyKey,
|
||||||
|
},
|
||||||
|
expectFinal: true,
|
||||||
|
timeoutMs: gatewayTimeoutMs,
|
||||||
|
clientName: "cli",
|
||||||
|
mode: "cli",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
runtime.log(JSON.stringify(response, null, 2));
|
runtime.log(JSON.stringify(response, null, 2));
|
||||||
|
|||||||
58
src/commands/agents.add.test.ts
Normal file
58
src/commands/agents.add.test.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
|
||||||
|
const configMocks = vi.hoisted(() => ({
|
||||||
|
readConfigFileSnapshot: vi.fn(),
|
||||||
|
writeConfigFile: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../config/config.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
readConfigFileSnapshot: configMocks.readConfigFileSnapshot,
|
||||||
|
writeConfigFile: configMocks.writeConfigFile,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { agentsAddCommand } from "./agents.js";
|
||||||
|
|
||||||
|
const runtime: RuntimeEnv = {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseSnapshot = {
|
||||||
|
path: "/tmp/clawdbot.json",
|
||||||
|
exists: true,
|
||||||
|
raw: "{}",
|
||||||
|
parsed: {},
|
||||||
|
valid: true,
|
||||||
|
config: {},
|
||||||
|
issues: [],
|
||||||
|
legacyIssues: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("agents add command", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
configMocks.readConfigFileSnapshot.mockReset();
|
||||||
|
configMocks.writeConfigFile.mockClear();
|
||||||
|
runtime.log.mockClear();
|
||||||
|
runtime.error.mockClear();
|
||||||
|
runtime.exit.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires --workspace when flags are present", async () => {
|
||||||
|
configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseSnapshot });
|
||||||
|
|
||||||
|
await agentsAddCommand({ name: "Work" }, runtime, { hasFlags: true });
|
||||||
|
|
||||||
|
expect(runtime.error).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("--workspace"),
|
||||||
|
);
|
||||||
|
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||||
|
expect(configMocks.writeConfigFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -343,12 +343,22 @@ function buildProviderBindings(params: {
|
|||||||
export async function agentsAddCommand(
|
export async function agentsAddCommand(
|
||||||
opts: AgentsAddOptions,
|
opts: AgentsAddOptions,
|
||||||
runtime: RuntimeEnv = defaultRuntime,
|
runtime: RuntimeEnv = defaultRuntime,
|
||||||
|
params?: { hasFlags?: boolean },
|
||||||
) {
|
) {
|
||||||
const cfg = await requireValidConfig(runtime);
|
const cfg = await requireValidConfig(runtime);
|
||||||
if (!cfg) return;
|
if (!cfg) return;
|
||||||
|
|
||||||
const workspaceFlag = opts.workspace?.trim();
|
const workspaceFlag = opts.workspace?.trim();
|
||||||
const nameInput = opts.name?.trim();
|
const nameInput = opts.name?.trim();
|
||||||
|
const hasFlags = params?.hasFlags === true;
|
||||||
|
|
||||||
|
if (hasFlags && !workspaceFlag) {
|
||||||
|
runtime.error(
|
||||||
|
"Non-interactive mode requires --workspace. Re-run without flags to use the wizard.",
|
||||||
|
);
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (workspaceFlag) {
|
if (workspaceFlag) {
|
||||||
if (!nameInput) {
|
if (!nameInput) {
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ import {
|
|||||||
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
|
||||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||||
import { resolveGatewayService } from "../daemon/service.js";
|
import { resolveGatewayService } from "../daemon/service.js";
|
||||||
|
import { createCliProgress } from "../cli/progress.js";
|
||||||
import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
|
import { theme } from "../terminal/theme.js";
|
||||||
import { resolveUserPath, sleep } from "../utils.js";
|
import { resolveUserPath, sleep } from "../utils.js";
|
||||||
import { createClackPrompter } from "../wizard/clack-prompter.js";
|
import { createClackPrompter } from "../wizard/clack-prompter.js";
|
||||||
import {
|
import {
|
||||||
@@ -90,6 +92,27 @@ type ConfigureWizardParams = {
|
|||||||
sections?: WizardSection[];
|
sections?: WizardSection[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startOscSpinner = (label: string) => {
|
||||||
|
const spin = spinner();
|
||||||
|
spin.start(theme.accent(label));
|
||||||
|
const osc = createCliProgress({
|
||||||
|
label,
|
||||||
|
indeterminate: true,
|
||||||
|
enabled: true,
|
||||||
|
fallback: "none",
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
update: (message: string) => {
|
||||||
|
spin.message(theme.accent(message));
|
||||||
|
osc.setLabel(message);
|
||||||
|
},
|
||||||
|
stop: (message: string) => {
|
||||||
|
osc.done();
|
||||||
|
spin.stop(message);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
async function promptGatewayConfig(
|
async function promptGatewayConfig(
|
||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
@@ -283,8 +306,7 @@ async function promptAuthConfig(
|
|||||||
"Browser will open. Paste the code shown after login (code#state).",
|
"Browser will open. Paste the code shown after login (code#state).",
|
||||||
"Anthropic OAuth",
|
"Anthropic OAuth",
|
||||||
);
|
);
|
||||||
const spin = spinner();
|
const spin = startOscSpinner("Waiting for authorization…");
|
||||||
spin.start("Waiting for authorization…");
|
|
||||||
let oauthCreds: OAuthCredentials | null = null;
|
let oauthCreds: OAuthCredentials | null = null;
|
||||||
try {
|
try {
|
||||||
oauthCreds = await loginAnthropic(
|
oauthCreds = await loginAnthropic(
|
||||||
@@ -341,21 +363,20 @@ async function promptAuthConfig(
|
|||||||
].join("\n"),
|
].join("\n"),
|
||||||
"OpenAI Codex OAuth",
|
"OpenAI Codex OAuth",
|
||||||
);
|
);
|
||||||
const spin = spinner();
|
const spin = startOscSpinner("Starting OAuth flow…");
|
||||||
spin.start("Starting OAuth flow…");
|
|
||||||
let manualCodePromise: Promise<string> | undefined;
|
let manualCodePromise: Promise<string> | undefined;
|
||||||
try {
|
try {
|
||||||
const creds = await loginOpenAICodex({
|
const creds = await loginOpenAICodex({
|
||||||
onAuth: async ({ url }) => {
|
onAuth: async ({ url }) => {
|
||||||
if (isRemote) {
|
if (isRemote) {
|
||||||
spin.message("OAuth URL ready (see below)…");
|
spin.update("OAuth URL ready (see below)…");
|
||||||
runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${url}\n`);
|
runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${url}\n`);
|
||||||
manualCodePromise = text({
|
manualCodePromise = text({
|
||||||
message: "Paste the redirect URL (or authorization code)",
|
message: "Paste the redirect URL (or authorization code)",
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||||
}).then((value) => String(guardCancel(value, runtime)));
|
}).then((value) => String(guardCancel(value, runtime)));
|
||||||
} else {
|
} else {
|
||||||
spin.message("Complete sign-in in browser…");
|
spin.update("Complete sign-in in browser…");
|
||||||
await openUrl(url);
|
await openUrl(url);
|
||||||
runtime.log(`Open: ${url}`);
|
runtime.log(`Open: ${url}`);
|
||||||
}
|
}
|
||||||
@@ -372,7 +393,7 @@ async function promptAuthConfig(
|
|||||||
);
|
);
|
||||||
return String(code);
|
return String(code);
|
||||||
},
|
},
|
||||||
onProgress: (msg) => spin.message(msg),
|
onProgress: (msg) => spin.update(msg),
|
||||||
});
|
});
|
||||||
spin.stop("OpenAI OAuth complete");
|
spin.stop("OpenAI OAuth complete");
|
||||||
if (creds) {
|
if (creds) {
|
||||||
@@ -429,8 +450,7 @@ async function promptAuthConfig(
|
|||||||
].join("\n"),
|
].join("\n"),
|
||||||
"Google Antigravity OAuth",
|
"Google Antigravity OAuth",
|
||||||
);
|
);
|
||||||
const spin = spinner();
|
const spin = startOscSpinner("Starting OAuth flow…");
|
||||||
spin.start("Starting OAuth flow…");
|
|
||||||
let oauthCreds: OAuthCredentials | null = null;
|
let oauthCreds: OAuthCredentials | null = null;
|
||||||
try {
|
try {
|
||||||
oauthCreds = await loginAntigravityVpsAware(
|
oauthCreds = await loginAntigravityVpsAware(
|
||||||
@@ -439,12 +459,12 @@ async function promptAuthConfig(
|
|||||||
spin.stop("OAuth URL ready");
|
spin.stop("OAuth URL ready");
|
||||||
runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${url}\n`);
|
runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${url}\n`);
|
||||||
} else {
|
} else {
|
||||||
spin.message("Complete sign-in in browser…");
|
spin.update("Complete sign-in in browser…");
|
||||||
await openUrl(url);
|
await openUrl(url);
|
||||||
runtime.log(`Open: ${url}`);
|
runtime.log(`Open: ${url}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(msg) => spin.message(msg),
|
(msg) => spin.update(msg),
|
||||||
);
|
);
|
||||||
spin.stop("Antigravity OAuth complete");
|
spin.stop("Antigravity OAuth complete");
|
||||||
if (oauthCreds) {
|
if (oauthCreds) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { type DiscordProbe, probeDiscord } from "../discord/probe.js";
|
|||||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||||
import { info } from "../globals.js";
|
import { info } from "../globals.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { withProgress } from "../cli/progress.js";
|
||||||
import { probeTelegram, type TelegramProbe } from "../telegram/probe.js";
|
import { probeTelegram, type TelegramProbe } from "../telegram/probe.js";
|
||||||
import { resolveTelegramToken } from "../telegram/token.js";
|
import { resolveTelegramToken } from "../telegram/token.js";
|
||||||
import { resolveWhatsAppAccount } from "../web/accounts.js";
|
import { resolveWhatsAppAccount } from "../web/accounts.js";
|
||||||
@@ -114,10 +115,18 @@ export async function healthCommand(
|
|||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
) {
|
) {
|
||||||
// Always query the running gateway; do not open a direct Baileys socket here.
|
// Always query the running gateway; do not open a direct Baileys socket here.
|
||||||
const summary = await callGateway<HealthSummary>({
|
const summary = await withProgress(
|
||||||
method: "health",
|
{
|
||||||
timeoutMs: opts.timeoutMs,
|
label: "Checking gateway health…",
|
||||||
});
|
indeterminate: true,
|
||||||
|
enabled: opts.json !== true,
|
||||||
|
},
|
||||||
|
async () =>
|
||||||
|
await callGateway<HealthSummary>({
|
||||||
|
method: "health",
|
||||||
|
timeoutMs: opts.timeoutMs,
|
||||||
|
}),
|
||||||
|
);
|
||||||
// Gateway reachability defines success; provider issues are reported but not fatal here.
|
// Gateway reachability defines success; provider issues are reported but not fatal here.
|
||||||
const fatal = false;
|
const fatal = false;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
discoverAuthStorage,
|
discoverAuthStorage,
|
||||||
discoverModels,
|
discoverModels,
|
||||||
} from "@mariozechner/pi-coding-agent";
|
} from "@mariozechner/pi-coding-agent";
|
||||||
import chalk from "chalk";
|
|
||||||
|
|
||||||
import { resolveClawdbotAgentDir } from "../../agents/agent-paths.js";
|
import { resolveClawdbotAgentDir } from "../../agents/agent-paths.js";
|
||||||
import {
|
import {
|
||||||
@@ -36,6 +35,7 @@ import {
|
|||||||
shouldEnableShellEnvFallback,
|
shouldEnableShellEnvFallback,
|
||||||
} from "../../infra/shell-env.js";
|
} from "../../infra/shell-env.js";
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
|
import { colorize, isRich as isRichTerminal, theme } from "../../terminal/theme.js";
|
||||||
import { shortenHomePath } from "../../utils.js";
|
import { shortenHomePath } from "../../utils.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
@@ -52,43 +52,36 @@ const LOCAL_PAD = 5;
|
|||||||
const AUTH_PAD = 5;
|
const AUTH_PAD = 5;
|
||||||
|
|
||||||
const isRich = (opts?: { json?: boolean; plain?: boolean }) =>
|
const isRich = (opts?: { json?: boolean; plain?: boolean }) =>
|
||||||
Boolean(
|
Boolean(isRichTerminal() && !opts?.json && !opts?.plain);
|
||||||
process.stdout.isTTY && chalk.level > 0 && !opts?.json && !opts?.plain,
|
|
||||||
);
|
|
||||||
|
|
||||||
const pad = (value: string, size: number) => value.padEnd(size);
|
const pad = (value: string, size: number) => value.padEnd(size);
|
||||||
|
|
||||||
const colorize = (
|
|
||||||
rich: boolean,
|
|
||||||
color: (value: string) => string,
|
|
||||||
value: string,
|
|
||||||
) => (rich ? color(value) : value);
|
|
||||||
|
|
||||||
const formatKey = (key: string, rich: boolean) =>
|
const formatKey = (key: string, rich: boolean) =>
|
||||||
colorize(rich, chalk.yellow, key);
|
colorize(rich, theme.warn, key);
|
||||||
|
|
||||||
const formatValue = (value: string, rich: boolean) =>
|
const formatValue = (value: string, rich: boolean) =>
|
||||||
colorize(rich, chalk.white, value);
|
colorize(rich, theme.info, value);
|
||||||
|
|
||||||
const formatKeyValue = (
|
const formatKeyValue = (
|
||||||
key: string,
|
key: string,
|
||||||
value: string,
|
value: string,
|
||||||
rich: boolean,
|
rich: boolean,
|
||||||
valueColor: (value: string) => string = chalk.white,
|
valueColor: (value: string) => string = theme.info,
|
||||||
) => `${formatKey(key, rich)}=${colorize(rich, valueColor, value)}`;
|
) => `${formatKey(key, rich)}=${colorize(rich, valueColor, value)}`;
|
||||||
|
|
||||||
const formatSeparator = (rich: boolean) => colorize(rich, chalk.gray, " | ");
|
const formatSeparator = (rich: boolean) =>
|
||||||
|
colorize(rich, theme.muted, " | ");
|
||||||
|
|
||||||
const formatTag = (tag: string, rich: boolean) => {
|
const formatTag = (tag: string, rich: boolean) => {
|
||||||
if (!rich) return tag;
|
if (!rich) return tag;
|
||||||
if (tag === "default") return chalk.greenBright(tag);
|
if (tag === "default") return theme.success(tag);
|
||||||
if (tag === "image") return chalk.magentaBright(tag);
|
if (tag === "image") return theme.accentBright(tag);
|
||||||
if (tag === "configured") return chalk.cyan(tag);
|
if (tag === "configured") return theme.accent(tag);
|
||||||
if (tag === "missing") return chalk.red(tag);
|
if (tag === "missing") return theme.error(tag);
|
||||||
if (tag.startsWith("fallback#")) return chalk.yellow(tag);
|
if (tag.startsWith("fallback#")) return theme.warn(tag);
|
||||||
if (tag.startsWith("img-fallback#")) return chalk.yellowBright(tag);
|
if (tag.startsWith("img-fallback#")) return theme.warn(tag);
|
||||||
if (tag.startsWith("alias:")) return chalk.blue(tag);
|
if (tag.startsWith("alias:")) return theme.accentDim(tag);
|
||||||
return chalk.gray(tag);
|
return theme.muted(tag);
|
||||||
};
|
};
|
||||||
|
|
||||||
const truncate = (value: string, max: number) => {
|
const truncate = (value: string, max: number) => {
|
||||||
@@ -450,7 +443,7 @@ function printModelTable(
|
|||||||
pad("Auth", AUTH_PAD),
|
pad("Auth", AUTH_PAD),
|
||||||
"Tags",
|
"Tags",
|
||||||
].join(" ");
|
].join(" ");
|
||||||
runtime.log(rich ? chalk.bold(header) : header);
|
runtime.log(rich ? theme.heading(header) : header);
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const keyLabel = pad(truncate(row.key, MODEL_PAD), MODEL_PAD);
|
const keyLabel = pad(truncate(row.key, MODEL_PAD), MODEL_PAD);
|
||||||
@@ -470,26 +463,26 @@ function printModelTable(
|
|||||||
|
|
||||||
const coloredInput = colorize(
|
const coloredInput = colorize(
|
||||||
rich,
|
rich,
|
||||||
row.input.includes("image") ? chalk.magenta : chalk.white,
|
row.input.includes("image") ? theme.accentBright : theme.info,
|
||||||
inputLabel,
|
inputLabel,
|
||||||
);
|
);
|
||||||
const coloredLocal = colorize(
|
const coloredLocal = colorize(
|
||||||
rich,
|
rich,
|
||||||
row.local === null ? chalk.gray : row.local ? chalk.green : chalk.gray,
|
row.local === null ? theme.muted : row.local ? theme.success : theme.muted,
|
||||||
localLabel,
|
localLabel,
|
||||||
);
|
);
|
||||||
const coloredAuth = colorize(
|
const coloredAuth = colorize(
|
||||||
rich,
|
rich,
|
||||||
row.available === null
|
row.available === null
|
||||||
? chalk.gray
|
? theme.muted
|
||||||
: row.available
|
: row.available
|
||||||
? chalk.green
|
? theme.success
|
||||||
: chalk.red,
|
: theme.error,
|
||||||
authLabel,
|
authLabel,
|
||||||
);
|
);
|
||||||
|
|
||||||
const line = [
|
const line = [
|
||||||
rich ? chalk.cyan(keyLabel) : keyLabel,
|
rich ? theme.accent(keyLabel) : keyLabel,
|
||||||
coloredInput,
|
coloredInput,
|
||||||
ctxLabel,
|
ctxLabel,
|
||||||
coloredLocal,
|
coloredLocal,
|
||||||
@@ -762,71 +755,72 @@ export async function modelsStatusCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rich = isRich(opts);
|
const rich = isRich(opts);
|
||||||
const label = (value: string) => colorize(rich, chalk.cyan, value.padEnd(14));
|
const label = (value: string) =>
|
||||||
|
colorize(rich, theme.accent, value.padEnd(14));
|
||||||
const displayDefault =
|
const displayDefault =
|
||||||
rawModel && rawModel !== resolvedLabel
|
rawModel && rawModel !== resolvedLabel
|
||||||
? `${resolvedLabel} (from ${rawModel})`
|
? `${resolvedLabel} (from ${rawModel})`
|
||||||
: resolvedLabel;
|
: resolvedLabel;
|
||||||
|
|
||||||
runtime.log(
|
runtime.log(
|
||||||
`${label("Config")}${colorize(rich, chalk.gray, ":")} ${colorize(rich, chalk.white, CONFIG_PATH_CLAWDBOT)}`,
|
`${label("Config")}${colorize(rich, theme.muted, ":")} ${colorize(rich, theme.info, CONFIG_PATH_CLAWDBOT)}`,
|
||||||
);
|
);
|
||||||
runtime.log(
|
runtime.log(
|
||||||
`${label("Agent dir")}${colorize(rich, chalk.gray, ":")} ${colorize(
|
`${label("Agent dir")}${colorize(rich, theme.muted, ":")} ${colorize(
|
||||||
rich,
|
rich,
|
||||||
chalk.white,
|
theme.info,
|
||||||
shortenHomePath(agentDir),
|
shortenHomePath(agentDir),
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
runtime.log(
|
runtime.log(
|
||||||
`${label("Default")}${colorize(rich, chalk.gray, ":")} ${colorize(
|
`${label("Default")}${colorize(rich, theme.muted, ":")} ${colorize(
|
||||||
rich,
|
rich,
|
||||||
chalk.green,
|
theme.success,
|
||||||
displayDefault,
|
displayDefault,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
runtime.log(
|
runtime.log(
|
||||||
`${label(`Fallbacks (${fallbacks.length || 0})`)}${colorize(
|
`${label(`Fallbacks (${fallbacks.length || 0})`)}${colorize(
|
||||||
rich,
|
rich,
|
||||||
chalk.gray,
|
theme.muted,
|
||||||
":",
|
":",
|
||||||
)} ${colorize(
|
)} ${colorize(
|
||||||
rich,
|
rich,
|
||||||
fallbacks.length ? chalk.yellow : chalk.gray,
|
fallbacks.length ? theme.warn : theme.muted,
|
||||||
fallbacks.length ? fallbacks.join(", ") : "-",
|
fallbacks.length ? fallbacks.join(", ") : "-",
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
runtime.log(
|
runtime.log(
|
||||||
`${label("Image model")}${colorize(rich, chalk.gray, ":")} ${colorize(
|
`${label("Image model")}${colorize(rich, theme.muted, ":")} ${colorize(
|
||||||
rich,
|
rich,
|
||||||
imageModel ? chalk.magenta : chalk.gray,
|
imageModel ? theme.accentBright : theme.muted,
|
||||||
imageModel || "-",
|
imageModel || "-",
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
runtime.log(
|
runtime.log(
|
||||||
`${label(`Image fallbacks (${imageFallbacks.length || 0})`)}${colorize(
|
`${label(`Image fallbacks (${imageFallbacks.length || 0})`)}${colorize(
|
||||||
rich,
|
rich,
|
||||||
chalk.gray,
|
theme.muted,
|
||||||
":",
|
":",
|
||||||
)} ${colorize(
|
)} ${colorize(
|
||||||
rich,
|
rich,
|
||||||
imageFallbacks.length ? chalk.magentaBright : chalk.gray,
|
imageFallbacks.length ? theme.accentBright : theme.muted,
|
||||||
imageFallbacks.length ? imageFallbacks.join(", ") : "-",
|
imageFallbacks.length ? imageFallbacks.join(", ") : "-",
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
runtime.log(
|
runtime.log(
|
||||||
`${label(`Aliases (${Object.keys(aliases).length || 0})`)}${colorize(
|
`${label(`Aliases (${Object.keys(aliases).length || 0})`)}${colorize(
|
||||||
rich,
|
rich,
|
||||||
chalk.gray,
|
theme.muted,
|
||||||
":",
|
":",
|
||||||
)} ${colorize(
|
)} ${colorize(
|
||||||
rich,
|
rich,
|
||||||
Object.keys(aliases).length ? chalk.cyan : chalk.gray,
|
Object.keys(aliases).length ? theme.accent : theme.muted,
|
||||||
Object.keys(aliases).length
|
Object.keys(aliases).length
|
||||||
? Object.entries(aliases)
|
? Object.entries(aliases)
|
||||||
.map(([alias, target]) =>
|
.map(([alias, target]) =>
|
||||||
rich
|
rich
|
||||||
? `${chalk.blue(alias)} ${chalk.gray("->")} ${chalk.white(
|
? `${theme.accentDim(alias)} ${theme.muted("->")} ${theme.info(
|
||||||
target,
|
target,
|
||||||
)}`
|
)}`
|
||||||
: `${alias} -> ${target}`,
|
: `${alias} -> ${target}`,
|
||||||
@@ -838,41 +832,41 @@ export async function modelsStatusCommand(
|
|||||||
runtime.log(
|
runtime.log(
|
||||||
`${label(`Configured models (${allowed.length || 0})`)}${colorize(
|
`${label(`Configured models (${allowed.length || 0})`)}${colorize(
|
||||||
rich,
|
rich,
|
||||||
chalk.gray,
|
theme.muted,
|
||||||
":",
|
":",
|
||||||
)} ${colorize(
|
)} ${colorize(
|
||||||
rich,
|
rich,
|
||||||
allowed.length ? chalk.white : chalk.gray,
|
allowed.length ? theme.info : theme.muted,
|
||||||
allowed.length ? allowed.join(", ") : "all",
|
allowed.length ? allowed.join(", ") : "all",
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
runtime.log("");
|
runtime.log("");
|
||||||
runtime.log(colorize(rich, chalk.bold, "Auth overview"));
|
runtime.log(colorize(rich, theme.heading, "Auth overview"));
|
||||||
runtime.log(
|
runtime.log(
|
||||||
`${label("Auth store")}${colorize(rich, chalk.gray, ":")} ${colorize(
|
`${label("Auth store")}${colorize(rich, theme.muted, ":")} ${colorize(
|
||||||
rich,
|
rich,
|
||||||
chalk.white,
|
theme.info,
|
||||||
shortenHomePath(resolveAuthStorePathForDisplay()),
|
shortenHomePath(resolveAuthStorePathForDisplay()),
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
runtime.log(
|
runtime.log(
|
||||||
`${label("Shell env")}${colorize(rich, chalk.gray, ":")} ${colorize(
|
`${label("Shell env")}${colorize(rich, theme.muted, ":")} ${colorize(
|
||||||
rich,
|
rich,
|
||||||
shellFallbackEnabled ? chalk.green : chalk.gray,
|
shellFallbackEnabled ? theme.success : theme.muted,
|
||||||
shellFallbackEnabled ? "on" : "off",
|
shellFallbackEnabled ? "on" : "off",
|
||||||
)}${
|
)}${
|
||||||
applied.length
|
applied.length
|
||||||
? colorize(rich, chalk.gray, ` (applied: ${applied.join(", ")})`)
|
? colorize(rich, theme.muted, ` (applied: ${applied.join(", ")})`)
|
||||||
: ""
|
: ""
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
runtime.log(
|
runtime.log(
|
||||||
`${label(
|
`${label(
|
||||||
`Providers w/ OAuth (${providersWithOauth.length || 0})`,
|
`Providers w/ OAuth (${providersWithOauth.length || 0})`,
|
||||||
)}${colorize(rich, chalk.gray, ":")} ${colorize(
|
)}${colorize(rich, theme.muted, ":")} ${colorize(
|
||||||
rich,
|
rich,
|
||||||
providersWithOauth.length ? chalk.white : chalk.gray,
|
providersWithOauth.length ? theme.info : theme.muted,
|
||||||
providersWithOauth.length ? providersWithOauth.join(", ") : "-",
|
providersWithOauth.length ? providersWithOauth.join(", ") : "-",
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
@@ -883,9 +877,9 @@ export async function modelsStatusCommand(
|
|||||||
bits.push(
|
bits.push(
|
||||||
formatKeyValue(
|
formatKeyValue(
|
||||||
"effective",
|
"effective",
|
||||||
`${colorize(rich, chalk.magenta, entry.effective.kind)}:${colorize(
|
`${colorize(rich, theme.accentBright, entry.effective.kind)}:${colorize(
|
||||||
rich,
|
rich,
|
||||||
chalk.gray,
|
theme.muted,
|
||||||
entry.effective.detail,
|
entry.effective.detail,
|
||||||
)}`,
|
)}`,
|
||||||
rich,
|
rich,
|
||||||
@@ -930,6 +924,6 @@ export async function modelsStatusCommand(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
runtime.log(`- ${chalk.bold(entry.provider)} ${bits.join(separator)}`);
|
runtime.log(`- ${theme.heading(entry.provider)} ${bits.join(separator)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "../infra/outbound/format.js";
|
} from "../infra/outbound/format.js";
|
||||||
import { normalizePollInput, type PollInput } from "../polls.js";
|
import { normalizePollInput, type PollInput } from "../polls.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { withProgress } from "../cli/progress.js";
|
||||||
|
|
||||||
function parseIntOption(value: unknown, label: string): number | undefined {
|
function parseIntOption(value: unknown, label: string): number | undefined {
|
||||||
if (value === undefined || value === null) return undefined;
|
if (value === undefined || value === null) return undefined;
|
||||||
@@ -57,25 +58,33 @@ export async function pollCommand(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await callGateway<{
|
const result = await withProgress(
|
||||||
messageId: string;
|
{
|
||||||
toJid?: string;
|
label: `Sending poll via ${provider}…`,
|
||||||
channelId?: string;
|
indeterminate: true,
|
||||||
}>({
|
enabled: opts.json !== true,
|
||||||
method: "poll",
|
|
||||||
params: {
|
|
||||||
to: opts.to,
|
|
||||||
question: normalized.question,
|
|
||||||
options: normalized.options,
|
|
||||||
maxSelections: normalized.maxSelections,
|
|
||||||
durationHours: normalized.durationHours,
|
|
||||||
provider,
|
|
||||||
idempotencyKey: randomIdempotencyKey(),
|
|
||||||
},
|
},
|
||||||
timeoutMs: 10_000,
|
async () =>
|
||||||
clientName: "cli",
|
await callGateway<{
|
||||||
mode: "cli",
|
messageId: string;
|
||||||
});
|
toJid?: string;
|
||||||
|
channelId?: string;
|
||||||
|
}>({
|
||||||
|
method: "poll",
|
||||||
|
params: {
|
||||||
|
to: opts.to,
|
||||||
|
question: normalized.question,
|
||||||
|
options: normalized.options,
|
||||||
|
maxSelections: normalized.maxSelections,
|
||||||
|
durationHours: normalized.durationHours,
|
||||||
|
provider,
|
||||||
|
idempotencyKey: randomIdempotencyKey(),
|
||||||
|
},
|
||||||
|
timeoutMs: 10_000,
|
||||||
|
clientName: "cli",
|
||||||
|
mode: "cli",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
runtime.log(
|
runtime.log(
|
||||||
success(
|
success(
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { spinner } from "@clack/prompts";
|
|
||||||
import chalk from "chalk";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CLAUDE_CLI_PROFILE_ID,
|
CLAUDE_CLI_PROFILE_ID,
|
||||||
CODEX_CLI_PROFILE_ID,
|
CODEX_CLI_PROFILE_ID,
|
||||||
loadAuthProfileStore,
|
loadAuthProfileStore,
|
||||||
} from "../agents/auth-profiles.js";
|
} from "../agents/auth-profiles.js";
|
||||||
|
import { withProgress } from "../cli/progress.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
|
import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
@@ -36,6 +34,7 @@ import {
|
|||||||
listTelegramAccountIds,
|
listTelegramAccountIds,
|
||||||
resolveTelegramAccount,
|
resolveTelegramAccount,
|
||||||
} from "../telegram/accounts.js";
|
} from "../telegram/accounts.js";
|
||||||
|
import { theme } from "../terminal/theme.js";
|
||||||
import { formatTerminalLink } from "../utils.js";
|
import { formatTerminalLink } from "../utils.js";
|
||||||
import {
|
import {
|
||||||
listWhatsAppAccountIds,
|
listWhatsAppAccountIds,
|
||||||
@@ -61,7 +60,7 @@ type ChatProvider = (typeof CHAT_PROVIDERS)[number];
|
|||||||
|
|
||||||
function docsLink(path: string, label?: string): string {
|
function docsLink(path: string, label?: string): string {
|
||||||
const url = `${DOCS_ROOT}${path}`;
|
const url = `${DOCS_ROOT}${path}`;
|
||||||
return formatTerminalLink(url, label ?? url);
|
return formatTerminalLink(label ?? url, url, { fallback: url });
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProvidersListOptions = {
|
type ProvidersListOptions = {
|
||||||
@@ -136,17 +135,17 @@ function formatAccountLabel(params: { accountId: string; name?: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const colorValue = (value: string) => {
|
const colorValue = (value: string) => {
|
||||||
if (value === "none") return chalk.red(value);
|
if (value === "none") return theme.error(value);
|
||||||
if (value === "env") return chalk.cyan(value);
|
if (value === "env") return theme.accent(value);
|
||||||
return chalk.green(value);
|
return theme.success(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatEnabled(value: boolean | undefined): string {
|
function formatEnabled(value: boolean | undefined): string {
|
||||||
return value === false ? chalk.red("disabled") : chalk.green("enabled");
|
return value === false ? theme.error("disabled") : theme.success("enabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatConfigured(value: boolean): string {
|
function formatConfigured(value: boolean): string {
|
||||||
return value ? chalk.green("configured") : chalk.yellow("not configured");
|
return value ? theme.success("configured") : theme.warn("not configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTokenSource(source?: string): string {
|
function formatTokenSource(source?: string): string {
|
||||||
@@ -160,7 +159,7 @@ function formatSource(label: string, source?: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatLinked(value: boolean): string {
|
function formatLinked(value: boolean): string {
|
||||||
return value ? chalk.green("linked") : chalk.yellow("not linked");
|
return value ? theme.success("linked") : theme.warn("not linked");
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyAccountName(params: {
|
function applyAccountName(params: {
|
||||||
@@ -501,14 +500,14 @@ export async function providersListCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push(chalk.bold("Chat providers:"));
|
lines.push(theme.heading("Chat providers:"));
|
||||||
|
|
||||||
for (const accountId of whatsappAccounts) {
|
for (const accountId of whatsappAccounts) {
|
||||||
const { authDir } = resolveWhatsAppAuthDir({ cfg, accountId });
|
const { authDir } = resolveWhatsAppAuthDir({ cfg, accountId });
|
||||||
const linked = await webAuthExists(authDir);
|
const linked = await webAuthExists(authDir);
|
||||||
const name = cfg.whatsapp?.accounts?.[accountId]?.name;
|
const name = cfg.whatsapp?.accounts?.[accountId]?.name;
|
||||||
lines.push(
|
lines.push(
|
||||||
`- ${chalk.cyan("WhatsApp")} ${chalk.bold(
|
`- ${theme.accent("WhatsApp")} ${theme.heading(
|
||||||
formatAccountLabel({
|
formatAccountLabel({
|
||||||
accountId,
|
accountId,
|
||||||
name,
|
name,
|
||||||
@@ -524,7 +523,7 @@ export async function providersListCommand(
|
|||||||
for (const accountId of telegramAccounts) {
|
for (const accountId of telegramAccounts) {
|
||||||
const account = resolveTelegramAccount({ cfg, accountId });
|
const account = resolveTelegramAccount({ cfg, accountId });
|
||||||
lines.push(
|
lines.push(
|
||||||
`- ${chalk.cyan("Telegram")} ${chalk.bold(
|
`- ${theme.accent("Telegram")} ${theme.heading(
|
||||||
formatAccountLabel({
|
formatAccountLabel({
|
||||||
accountId,
|
accountId,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
@@ -538,7 +537,7 @@ export async function providersListCommand(
|
|||||||
for (const accountId of discordAccounts) {
|
for (const accountId of discordAccounts) {
|
||||||
const account = resolveDiscordAccount({ cfg, accountId });
|
const account = resolveDiscordAccount({ cfg, accountId });
|
||||||
lines.push(
|
lines.push(
|
||||||
`- ${chalk.cyan("Discord")} ${chalk.bold(
|
`- ${theme.accent("Discord")} ${theme.heading(
|
||||||
formatAccountLabel({
|
formatAccountLabel({
|
||||||
accountId,
|
accountId,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
@@ -553,7 +552,7 @@ export async function providersListCommand(
|
|||||||
const account = resolveSlackAccount({ cfg, accountId });
|
const account = resolveSlackAccount({ cfg, accountId });
|
||||||
const configured = Boolean(account.botToken && account.appToken);
|
const configured = Boolean(account.botToken && account.appToken);
|
||||||
lines.push(
|
lines.push(
|
||||||
`- ${chalk.cyan("Slack")} ${chalk.bold(
|
`- ${theme.accent("Slack")} ${theme.heading(
|
||||||
formatAccountLabel({
|
formatAccountLabel({
|
||||||
accountId,
|
accountId,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
@@ -570,12 +569,12 @@ export async function providersListCommand(
|
|||||||
for (const accountId of signalAccounts) {
|
for (const accountId of signalAccounts) {
|
||||||
const account = resolveSignalAccount({ cfg, accountId });
|
const account = resolveSignalAccount({ cfg, accountId });
|
||||||
lines.push(
|
lines.push(
|
||||||
`- ${chalk.cyan("Signal")} ${chalk.bold(
|
`- ${theme.accent("Signal")} ${theme.heading(
|
||||||
formatAccountLabel({
|
formatAccountLabel({
|
||||||
accountId,
|
accountId,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
}),
|
}),
|
||||||
)}: ${formatConfigured(account.configured)}, base=${chalk.dim(
|
)}: ${formatConfigured(account.configured)}, base=${theme.muted(
|
||||||
account.baseUrl,
|
account.baseUrl,
|
||||||
)}, ${formatEnabled(account.enabled)}`,
|
)}, ${formatEnabled(account.enabled)}`,
|
||||||
);
|
);
|
||||||
@@ -584,7 +583,7 @@ export async function providersListCommand(
|
|||||||
for (const accountId of imessageAccounts) {
|
for (const accountId of imessageAccounts) {
|
||||||
const account = resolveIMessageAccount({ cfg, accountId });
|
const account = resolveIMessageAccount({ cfg, accountId });
|
||||||
lines.push(
|
lines.push(
|
||||||
`- ${chalk.cyan("iMessage")} ${chalk.bold(
|
`- ${theme.accent("iMessage")} ${theme.heading(
|
||||||
formatAccountLabel({
|
formatAccountLabel({
|
||||||
accountId,
|
accountId,
|
||||||
name: account.name,
|
name: account.name,
|
||||||
@@ -594,14 +593,14 @@ export async function providersListCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(chalk.bold("Auth providers (OAuth + API keys):"));
|
lines.push(theme.heading("Auth providers (OAuth + API keys):"));
|
||||||
if (authProfiles.length === 0) {
|
if (authProfiles.length === 0) {
|
||||||
lines.push(chalk.dim("- none"));
|
lines.push(theme.muted("- none"));
|
||||||
} else {
|
} else {
|
||||||
for (const profile of authProfiles) {
|
for (const profile of authProfiles) {
|
||||||
const external = profile.isExternal ? chalk.dim(" (synced)") : "";
|
const external = profile.isExternal ? theme.muted(" (synced)") : "";
|
||||||
lines.push(
|
lines.push(
|
||||||
`- ${chalk.cyan(profile.id)} (${chalk.green(profile.type)}${external})`,
|
`- ${theme.accent(profile.id)} (${theme.success(profile.type)}${external})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -610,11 +609,11 @@ export async function providersListCommand(
|
|||||||
|
|
||||||
if (includeUsage) {
|
if (includeUsage) {
|
||||||
runtime.log("");
|
runtime.log("");
|
||||||
const usage = await loadUsageWithSpinner(runtime);
|
const usage = await loadUsageWithProgress(runtime);
|
||||||
if (usage) {
|
if (usage) {
|
||||||
const usageLines = formatUsageReportLines(usage);
|
const usageLines = formatUsageReportLines(usage);
|
||||||
if (usageLines.length > 0) {
|
if (usageLines.length > 0) {
|
||||||
usageLines[0] = chalk.cyan(usageLines[0]);
|
usageLines[0] = theme.accent(usageLines[0]);
|
||||||
runtime.log(usageLines.join("\n"));
|
runtime.log(usageLines.join("\n"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -628,27 +627,15 @@ export async function providersListCommand(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadUsageWithSpinner(
|
async function loadUsageWithProgress(
|
||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
): Promise<Awaited<ReturnType<typeof loadProviderUsageSummary>> | null> {
|
): Promise<Awaited<ReturnType<typeof loadProviderUsageSummary>> | null> {
|
||||||
const rich = Boolean(process.stdout.isTTY);
|
|
||||||
if (!rich) {
|
|
||||||
try {
|
|
||||||
return await loadProviderUsageSummary();
|
|
||||||
} catch (err) {
|
|
||||||
runtime.error(String(err));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const spin = spinner();
|
|
||||||
spin.start(chalk.cyan("Fetching usage snapshot…"));
|
|
||||||
try {
|
try {
|
||||||
const usage = await loadProviderUsageSummary();
|
return await withProgress(
|
||||||
spin.stop(chalk.green("Usage snapshot ready"));
|
{ label: "Fetching usage snapshot…", indeterminate: true, enabled: true },
|
||||||
return usage;
|
async () => await loadProviderUsageSummary(),
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
spin.stop(chalk.red("Usage snapshot failed"));
|
|
||||||
runtime.error(String(err));
|
runtime.error(String(err));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -660,18 +647,26 @@ export async function providersStatusCommand(
|
|||||||
) {
|
) {
|
||||||
const timeoutMs = Number(opts.timeout ?? 10_000);
|
const timeoutMs = Number(opts.timeout ?? 10_000);
|
||||||
try {
|
try {
|
||||||
const payload = await callGateway({
|
const payload = await withProgress(
|
||||||
method: "providers.status",
|
{
|
||||||
params: { probe: Boolean(opts.probe), timeoutMs },
|
label: "Checking provider status…",
|
||||||
timeoutMs,
|
indeterminate: true,
|
||||||
});
|
enabled: opts.json !== true,
|
||||||
|
},
|
||||||
|
async () =>
|
||||||
|
await callGateway({
|
||||||
|
method: "providers.status",
|
||||||
|
params: { probe: Boolean(opts.probe), timeoutMs },
|
||||||
|
timeoutMs,
|
||||||
|
}),
|
||||||
|
);
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
runtime.log(JSON.stringify(payload, null, 2));
|
runtime.log(JSON.stringify(payload, null, 2));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = payload as Record<string, unknown>;
|
const data = payload as Record<string, unknown>;
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push(chalk.green("Gateway reachable."));
|
lines.push(theme.success("Gateway reachable."));
|
||||||
const accountLines = (
|
const accountLines = (
|
||||||
label: string,
|
label: string,
|
||||||
accounts: Array<Record<string, unknown>>,
|
accounts: Array<Record<string, unknown>>,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "../infra/outbound/format.js";
|
} from "../infra/outbound/format.js";
|
||||||
import { resolveOutboundTarget } from "../infra/outbound/targets.js";
|
import { resolveOutboundTarget } from "../infra/outbound/targets.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { withProgress } from "../cli/progress.js";
|
||||||
import { normalizeMessageProvider } from "../utils/message-provider.js";
|
import { normalizeMessageProvider } from "../utils/message-provider.js";
|
||||||
|
|
||||||
export async function sendCommand(
|
export async function sendCommand(
|
||||||
@@ -50,20 +51,28 @@ export async function sendCommand(
|
|||||||
if (!resolvedTarget.ok) {
|
if (!resolvedTarget.ok) {
|
||||||
throw resolvedTarget.error;
|
throw resolvedTarget.error;
|
||||||
}
|
}
|
||||||
const results = await deliverOutboundPayloads({
|
const results = await withProgress(
|
||||||
cfg: loadConfig(),
|
{
|
||||||
provider,
|
label: `Sending via ${provider}…`,
|
||||||
to: resolvedTarget.to,
|
indeterminate: true,
|
||||||
payloads: [{ text: opts.message, mediaUrl: opts.media }],
|
enabled: opts.json !== true,
|
||||||
deps: {
|
|
||||||
sendWhatsApp: deps.sendMessageWhatsApp,
|
|
||||||
sendTelegram: deps.sendMessageTelegram,
|
|
||||||
sendDiscord: deps.sendMessageDiscord,
|
|
||||||
sendSlack: deps.sendMessageSlack,
|
|
||||||
sendSignal: deps.sendMessageSignal,
|
|
||||||
sendIMessage: deps.sendMessageIMessage,
|
|
||||||
},
|
},
|
||||||
});
|
async () =>
|
||||||
|
await deliverOutboundPayloads({
|
||||||
|
cfg: loadConfig(),
|
||||||
|
provider,
|
||||||
|
to: resolvedTarget.to,
|
||||||
|
payloads: [{ text: opts.message, mediaUrl: opts.media }],
|
||||||
|
deps: {
|
||||||
|
sendWhatsApp: deps.sendMessageWhatsApp,
|
||||||
|
sendTelegram: deps.sendMessageTelegram,
|
||||||
|
sendDiscord: deps.sendMessageDiscord,
|
||||||
|
sendSlack: deps.sendMessageSlack,
|
||||||
|
sendSignal: deps.sendMessageSignal,
|
||||||
|
sendIMessage: deps.sendMessageIMessage,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
const last = results.at(-1);
|
const last = results.at(-1);
|
||||||
const summary = formatOutboundDeliverySummary(provider, last);
|
const summary = formatOutboundDeliverySummary(provider, last);
|
||||||
runtime.log(success(summary));
|
runtime.log(success(summary));
|
||||||
@@ -105,7 +114,14 @@ export async function sendCommand(
|
|||||||
mode: "cli",
|
mode: "cli",
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await sendViaGateway();
|
const result = await withProgress(
|
||||||
|
{
|
||||||
|
label: `Sending via ${provider}…`,
|
||||||
|
indeterminate: true,
|
||||||
|
enabled: opts.json !== true,
|
||||||
|
},
|
||||||
|
async () => await sendViaGateway(),
|
||||||
|
);
|
||||||
|
|
||||||
runtime.log(
|
runtime.log(
|
||||||
success(
|
success(
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import chalk from "chalk";
|
|
||||||
|
|
||||||
import { lookupContextTokens } from "../agents/context.js";
|
import { lookupContextTokens } from "../agents/context.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_CONTEXT_TOKENS,
|
DEFAULT_CONTEXT_TOKENS,
|
||||||
@@ -15,6 +13,7 @@ import {
|
|||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import { info } from "../globals.js";
|
import { info } from "../globals.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { isRich, theme } from "../terminal/theme.js";
|
||||||
|
|
||||||
type SessionRow = {
|
type SessionRow = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -41,8 +40,6 @@ const AGE_PAD = 9;
|
|||||||
const MODEL_PAD = 14;
|
const MODEL_PAD = 14;
|
||||||
const TOKENS_PAD = 20;
|
const TOKENS_PAD = 20;
|
||||||
|
|
||||||
const isRich = () => Boolean(process.stdout.isTTY && chalk.level > 0);
|
|
||||||
|
|
||||||
const formatKTokens = (value: number) =>
|
const formatKTokens = (value: number) =>
|
||||||
`${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k`;
|
`${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k`;
|
||||||
|
|
||||||
@@ -54,10 +51,10 @@ const truncateKey = (key: string) => {
|
|||||||
|
|
||||||
const colorByPct = (label: string, pct: number | null, rich: boolean) => {
|
const colorByPct = (label: string, pct: number | null, rich: boolean) => {
|
||||||
if (!rich || pct === null) return label;
|
if (!rich || pct === null) return label;
|
||||||
if (pct >= 95) return chalk.red(label);
|
if (pct >= 95) return theme.error(label);
|
||||||
if (pct >= 80) return chalk.yellow(label);
|
if (pct >= 80) return theme.warn(label);
|
||||||
if (pct >= 60) return chalk.green(label);
|
if (pct >= 60) return theme.success(label);
|
||||||
return chalk.gray(label);
|
return theme.muted(label);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTokensCell = (
|
const formatTokensCell = (
|
||||||
@@ -79,21 +76,21 @@ const formatTokensCell = (
|
|||||||
const formatKindCell = (kind: SessionRow["kind"], rich: boolean) => {
|
const formatKindCell = (kind: SessionRow["kind"], rich: boolean) => {
|
||||||
const label = kind.padEnd(KIND_PAD);
|
const label = kind.padEnd(KIND_PAD);
|
||||||
if (!rich) return label;
|
if (!rich) return label;
|
||||||
if (kind === "group") return chalk.magenta(label);
|
if (kind === "group") return theme.accentBright(label);
|
||||||
if (kind === "global") return chalk.yellow(label);
|
if (kind === "global") return theme.warn(label);
|
||||||
if (kind === "direct") return chalk.cyan(label);
|
if (kind === "direct") return theme.accent(label);
|
||||||
return chalk.gray(label);
|
return theme.muted(label);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatAgeCell = (updatedAt: number | null | undefined, rich: boolean) => {
|
const formatAgeCell = (updatedAt: number | null | undefined, rich: boolean) => {
|
||||||
const ageLabel = updatedAt ? formatAge(Date.now() - updatedAt) : "unknown";
|
const ageLabel = updatedAt ? formatAge(Date.now() - updatedAt) : "unknown";
|
||||||
const padded = ageLabel.padEnd(AGE_PAD);
|
const padded = ageLabel.padEnd(AGE_PAD);
|
||||||
return rich ? chalk.gray(padded) : padded;
|
return rich ? theme.muted(padded) : padded;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatModelCell = (model: string | null | undefined, rich: boolean) => {
|
const formatModelCell = (model: string | null | undefined, rich: boolean) => {
|
||||||
const label = (model ?? "unknown").padEnd(MODEL_PAD);
|
const label = (model ?? "unknown").padEnd(MODEL_PAD);
|
||||||
return rich ? chalk.white(label) : label;
|
return rich ? theme.info(label) : label;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatFlagsCell = (row: SessionRow, rich: boolean) => {
|
const formatFlagsCell = (row: SessionRow, rich: boolean) => {
|
||||||
@@ -107,7 +104,7 @@ const formatFlagsCell = (row: SessionRow, rich: boolean) => {
|
|||||||
row.sessionId ? `id:${row.sessionId}` : null,
|
row.sessionId ? `id:${row.sessionId}` : null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
const label = flags.join(" ");
|
const label = flags.join(" ");
|
||||||
return label.length === 0 ? "" : rich ? chalk.gray(label) : label;
|
return label.length === 0 ? "" : rich ? theme.muted(label) : label;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatAge = (ms: number | null | undefined) => {
|
const formatAge = (ms: number | null | undefined) => {
|
||||||
@@ -240,7 +237,7 @@ export async function sessionsCommand(
|
|||||||
"Flags",
|
"Flags",
|
||||||
].join(" ");
|
].join(" ");
|
||||||
|
|
||||||
runtime.log(rich ? chalk.bold(header) : header);
|
runtime.log(rich ? theme.heading(header) : header);
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const model = row.model ?? configModel;
|
const model = row.model ?? configModel;
|
||||||
@@ -251,7 +248,7 @@ export async function sessionsCommand(
|
|||||||
const total = row.totalTokens ?? input + output;
|
const total = row.totalTokens ?? input + output;
|
||||||
|
|
||||||
const keyLabel = truncateKey(row.key).padEnd(KEY_PAD);
|
const keyLabel = truncateKey(row.key).padEnd(KEY_PAD);
|
||||||
const keyCell = rich ? chalk.cyan(keyLabel) : keyLabel;
|
const keyCell = rich ? theme.accent(keyLabel) : keyLabel;
|
||||||
|
|
||||||
const line = [
|
const line = [
|
||||||
formatKindCell(row.kind, rich),
|
formatKindCell(row.kind, rich),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
formatUsageReportLines,
|
formatUsageReportLines,
|
||||||
loadProviderUsageSummary,
|
loadProviderUsageSummary,
|
||||||
} from "../infra/provider-usage.js";
|
} from "../infra/provider-usage.js";
|
||||||
|
import { withProgress } from "../cli/progress.js";
|
||||||
import { peekSystemEvents } from "../infra/system-events.js";
|
import { peekSystemEvents } from "../infra/system-events.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { resolveWhatsAppAccount } from "../web/accounts.js";
|
import { resolveWhatsAppAccount } from "../web/accounts.js";
|
||||||
@@ -233,13 +234,29 @@ export async function statusCommand(
|
|||||||
) {
|
) {
|
||||||
const summary = await getStatusSummary();
|
const summary = await getStatusSummary();
|
||||||
const usage = opts.usage
|
const usage = opts.usage
|
||||||
? await loadProviderUsageSummary({ timeoutMs: opts.timeoutMs })
|
? await withProgress(
|
||||||
|
{
|
||||||
|
label: "Fetching usage snapshot…",
|
||||||
|
indeterminate: true,
|
||||||
|
enabled: opts.json !== true,
|
||||||
|
},
|
||||||
|
async () =>
|
||||||
|
await loadProviderUsageSummary({ timeoutMs: opts.timeoutMs }),
|
||||||
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
const health: HealthSummary | undefined = opts.deep
|
const health: HealthSummary | undefined = opts.deep
|
||||||
? await callGateway<HealthSummary>({
|
? await withProgress(
|
||||||
method: "health",
|
{
|
||||||
timeoutMs: opts.timeoutMs,
|
label: "Checking gateway health…",
|
||||||
})
|
indeterminate: true,
|
||||||
|
enabled: opts.json !== true,
|
||||||
|
},
|
||||||
|
async () =>
|
||||||
|
await callGateway<HealthSummary>({
|
||||||
|
method: "health",
|
||||||
|
timeoutMs: opts.timeoutMs,
|
||||||
|
}),
|
||||||
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import chalk from "chalk";
|
|
||||||
import { getLogger, isFileLogLevelEnabled } from "./logging.js";
|
import { getLogger, isFileLogLevelEnabled } from "./logging.js";
|
||||||
|
import { theme } from "./terminal/theme.js";
|
||||||
|
|
||||||
let globalVerbose = false;
|
let globalVerbose = false;
|
||||||
let globalYes = false;
|
let globalYes = false;
|
||||||
@@ -24,12 +24,12 @@ export function logVerbose(message: string) {
|
|||||||
// ignore logger failures to avoid breaking verbose printing
|
// ignore logger failures to avoid breaking verbose printing
|
||||||
}
|
}
|
||||||
if (!globalVerbose) return;
|
if (!globalVerbose) return;
|
||||||
console.log(chalk.gray(message));
|
console.log(theme.muted(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logVerboseConsole(message: string) {
|
export function logVerboseConsole(message: string) {
|
||||||
if (!globalVerbose) return;
|
if (!globalVerbose) return;
|
||||||
console.log(chalk.gray(message));
|
console.log(theme.muted(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setYes(v: boolean) {
|
export function setYes(v: boolean) {
|
||||||
@@ -40,7 +40,7 @@ export function isYes() {
|
|||||||
return globalYes;
|
return globalYes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const success = chalk.green;
|
export const success = theme.success;
|
||||||
export const warn = chalk.yellow;
|
export const warn = theme.warn;
|
||||||
export const info = chalk.cyan;
|
export const info = theme.info;
|
||||||
export const danger = chalk.red;
|
export const danger = theme.error;
|
||||||
|
|||||||
36
src/terminal/theme.ts
Normal file
36
src/terminal/theme.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
export const LOBSTER_PALETTE = {
|
||||||
|
accent: "#FF5A2D",
|
||||||
|
accentBright: "#FF7A3D",
|
||||||
|
accentDim: "#D14A22",
|
||||||
|
info: "#FF8A5B",
|
||||||
|
success: "#2FBF71",
|
||||||
|
warn: "#FFB020",
|
||||||
|
error: "#E23D2D",
|
||||||
|
muted: "#8B7F77",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const hex = (value: string) => chalk.hex(value);
|
||||||
|
|
||||||
|
export const theme = {
|
||||||
|
accent: hex(LOBSTER_PALETTE.accent),
|
||||||
|
accentBright: hex(LOBSTER_PALETTE.accentBright),
|
||||||
|
accentDim: hex(LOBSTER_PALETTE.accentDim),
|
||||||
|
info: hex(LOBSTER_PALETTE.info),
|
||||||
|
success: hex(LOBSTER_PALETTE.success),
|
||||||
|
warn: hex(LOBSTER_PALETTE.warn),
|
||||||
|
error: hex(LOBSTER_PALETTE.error),
|
||||||
|
muted: hex(LOBSTER_PALETTE.muted),
|
||||||
|
heading: chalk.bold.hex(LOBSTER_PALETTE.accent),
|
||||||
|
command: hex(LOBSTER_PALETTE.accentBright),
|
||||||
|
option: hex(LOBSTER_PALETTE.warn),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const isRich = () => Boolean(process.stdout.isTTY && chalk.level > 0);
|
||||||
|
|
||||||
|
export const colorize = (
|
||||||
|
rich: boolean,
|
||||||
|
color: (value: string) => string,
|
||||||
|
value: string,
|
||||||
|
) => (rich ? color(value) : value);
|
||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
|
|
||||||
import type { WizardProgress, WizardPrompter } from "./prompts.js";
|
import type { WizardProgress, WizardPrompter } from "./prompts.js";
|
||||||
import { WizardCancelledError } from "./prompts.js";
|
import { WizardCancelledError } from "./prompts.js";
|
||||||
|
import { createCliProgress } from "../cli/progress.js";
|
||||||
|
import { theme } from "../terminal/theme.js";
|
||||||
|
|
||||||
function guardCancel<T>(value: T | symbol): T {
|
function guardCancel<T>(value: T | symbol): T {
|
||||||
if (isCancel(value)) {
|
if (isCancel(value)) {
|
||||||
@@ -74,10 +76,22 @@ export function createClackPrompter(): WizardPrompter {
|
|||||||
),
|
),
|
||||||
progress: (label: string): WizardProgress => {
|
progress: (label: string): WizardProgress => {
|
||||||
const spin = spinner();
|
const spin = spinner();
|
||||||
spin.start(label);
|
spin.start(theme.accent(label));
|
||||||
|
const osc = createCliProgress({
|
||||||
|
label,
|
||||||
|
indeterminate: true,
|
||||||
|
enabled: true,
|
||||||
|
fallback: "none",
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
update: (message) => spin.message(message),
|
update: (message) => {
|
||||||
stop: (message) => spin.stop(message),
|
spin.message(theme.accent(message));
|
||||||
|
osc.setLabel(message);
|
||||||
|
},
|
||||||
|
stop: (message) => {
|
||||||
|
osc.done();
|
||||||
|
spin.stop(message);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user