@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
126 lines (125 loc) • 5.37 kB
TypeScript
import { type NotLinkedComputer } from "./hand-off.js";
import { type HookPeer } from "./peers.js";
import { type Json } from "./protocol.js";
import { NEAR_LIMIT_LEFT, type AccountUsage, type Subscription, type UsageWindow } from "./usage.js";
export { NEAR_LIMIT_LEFT };
/**
* Every computer's agent usage merged by account, for a conductor choosing
* where to send work: `account_usage` and `phren dispatch usage`. Read like
* `live_sessions`: this Hook, then each linked Hook over its pinned pipe, with
* unreachable and unlinked computers listed apart.
*
* The merge follows the phone's cards: one account is one allowance whichever
* computers report it, so the freshest whole report stands. Claude and Codex
* rows merge by their account key; a Claude login with no identity
* (`claude:home:<id>`) stays per computer, since two unknown logins are not
* one account. A window whose reset time has passed says `reset` and has no
* percent, and a report older than STALE_AFTER_MS is flagged stale.
*
* `nearLimit` is information: the owner often wants quota used before it
* resets. Only `exhausted` (a window at 100% or refusing requests) rules an
* account out.
*/
/** A report older than this is flagged: the Hook reads each account at most once a minute. */
export declare const STALE_AFTER_MS: number;
export interface AccountWindow {
id: string;
name: string;
usedPercent?: number;
leftPercent?: number;
resetsAt?: string;
resetsIn?: string;
/** Reset since its report: the allowance is back, but no new percent is known. */
reset?: true;
/** The service is refusing requests on this window now. */
limited?: true;
/** At 100% or refused: no quota on this window until it resets. */
exhausted?: true;
usedUSD?: number;
limitUSD?: number;
}
export interface AccountUsageRow {
/** `source` or `source|key`, as the phone keys its cards. */
id: string;
harness: string;
name: string;
/** The login's email, else the account's label, when the Hook names one. */
account?: string;
windows: AccountWindow[];
/** Least room on any window with a percent; `limited` counts as 0. */
leftPercent?: number;
/** Under 20% left on some window. Information, not a reason to avoid the account. */
nearLimit: boolean;
/** A window is at 100% or refusing requests: never dispatch to it until `availableIn`. */
exhausted: boolean;
/** When the last exhausted window resets, as "3h 10m". */
availableIn?: string;
spend?: {
amountUSD: number;
period: string;
};
subscription?: Subscription;
updatedAt?: string;
/** How old the standing report is, as "4m" or "2d 3h". */
age?: string;
stale: boolean;
/** The computer whose report stands. */
from?: string;
/** Where it is signed in, with the Claude account id dispatch takes there. */
computers: Array<{
name: string;
account?: string;
}>;
message?: string;
}
export interface AccountUsageView {
accounts: AccountUsageRow[];
/** Harnesses no computer reported numbers for, with what each Hook said. */
noData: Array<{
harness: string;
name: string;
computers: string[];
message?: string;
}>;
/** The computers that answered, this one first. */
computers: string[];
unreachable: Array<{
computer: string;
error: string;
code?: string;
}>;
/** Registered in the store but not linked in hooks.yaml: their usage is unknown, not zero. */
notLinked: NotLinkedComputer[];
enrolled: number;
peerError?: string;
}
export interface ComputerUsage {
computer: string;
accounts: AccountUsage[];
}
export declare function ageText(ms: number): string;
/** The phone's card key, except that a Claude login with no identity stays on its own computer. */
export declare function accountIdentity(usage: AccountUsage, computer: string): string;
/** Percent used and left, with a passed reset reported as reset rather than as its old percent. */
export declare function settleWindow(window: UsageWindow, now: number): AccountWindow;
/** Least room across an account's windows, undefined when none has a percent. */
export declare function roomLeft(windows: readonly AccountWindow[]): number | undefined;
/** One row per account across every computer's report. */
export declare function mergeAccountUsage(reports: readonly ComputerUsage[], now?: number): Pick<AccountUsageView, "accounts" | "noData">;
export interface ReadAccountUsageOptions {
/** This computer's Hook; tests pass the fixture's socket. */
hook?: (route: string) => Promise<Json>;
peers?: () => Promise<{
peers: HookPeer[];
peerError?: string;
}>;
peer?: (peer: HookPeer, route: string) => Promise<Json>;
store?: string | null;
now?: number;
}
/** This Hook's usage and every linked Hook's, merged by account. */
export declare function readAccountUsage(options?: ReadAccountUsageOptions): Promise<AccountUsageView>;
/** The one-line answer a conductor reads first: how many, and which accounts are tight or stale. */
export declare function usageSummary(view: AccountUsageView): string;
/** `phren dispatch usage` without --json. */
export declare function formatAccountUsage(view: AccountUsageView): string;