@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
180 lines (179 loc) • 8.37 kB
TypeScript
import { type CleanupReport, type JobSource, type ProcessLike, type TrackedJob } from "./job-registry.js";
/**
* This computer's live resources for `GET /v1/resources`, the overview
* stream's `resources` frames and the memory-free `phren computers` surface:
* load against cores, memory, free disk on the home volume, battery, uptime
* and the heavy jobs (simulators, emulators, xcodebuild, Gradle, agent
* workers) with the pane that started them where one did; any other program
* holding half a core or more across its processes shows as `busy`. macOS reads
* sysctl, pmset and ps; Linux reads /proc, /sys and ps. Nothing here needs a
* permission the Hook does not already have.
*/
export declare const RESOURCES_MAX_AGE_MS: number;
/** Below this much free disk, or above twice the cores in load, a computer is stressed. */
export declare const LOW_DISK_BYTES: number;
export type HeavyKind = "simulator" | "emulator" | "xcodebuild" | "gradle" | "java" | "codex" | "opencode" | "claude" | "busy";
export interface PaneRef {
server: string;
workspace?: string;
pane: string;
agent?: string;
label?: string;
}
export interface HeavyProcess {
kind: HeavyKind;
name: string;
pid: number;
/** OS processes, including helpers; never a count of agents or working sessions. */
processes: number;
cpuPercent: number;
memoryBytes: number;
/** Why this process group is listed; resource use does not establish session activity. */
resourceReason?: "cpu" | "memory" | "tracked";
/** The harness that owns the job, when a pane or a registered group says so. */
agent?: string;
/** True when the group is one the Hook registered for a dispatched worker. */
tracked?: boolean;
pane?: PaneRef;
}
export interface ComputerResources {
collectedAt: string;
platform: string;
uptimeSeconds: number;
cpu: {
cores: number;
load1: number;
load5: number;
load15: number;
loadPerCore: number;
};
memory: {
totalBytes: number;
availablePercent?: number;
pressure?: "normal" | "warn" | "critical";
swapUsedBytes?: number;
};
disk?: {
path: string;
totalBytes: number;
freeBytes: number;
};
battery?: {
percent: number;
charging: boolean;
onAC: boolean;
};
hardware?: ComputerHardware;
heavy: HeavyProcess[];
/** 0 idle to 1 saturated, per resource and overall (the highest), so every client fills its gauge the same way. */
pressure: {
cpu: number;
memory: number;
disk: number;
overall: number;
};
level: "ok" | "busy" | "stressed";
warnings: Array<"load-high" | "memory-low" | "disk-low" | "battery-low">;
}
/** What the computer is, read once per Hook run: "MacBook Pro", "Apple M1 Max",
* 32 GB; on Linux the DMI vendor and product ("Dell XPS 13 9310") and the CPU's
* model name. Never a serial number or any other identifier. */
export interface ComputerHardware {
model?: string;
chip?: string;
memoryBytes: number;
cores: number;
}
/** `system_profiler SPHardwareDataType -json` → model and chip. Intel Macs name
* the processor in `cpu_type` instead of `chip_type`. */
export declare function parseMacHardware(json: string): Pick<ComputerHardware, "model" | "chip">;
/** DMI vendor and product → "Dell XPS 13 9310", "Lenovo ThinkPad X1 Carbon";
* placeholder strings firmware leaves in are dropped. */
export declare function linuxModel(vendor: string | undefined, product: string | undefined, version?: string): string | undefined;
/** /proc/cpuinfo's first `model name`, without the (R)/(TM) marks, "CPU" and clock. */
export declare function linuxChip(cpuinfo: string): string | undefined;
/** Read once; the answer can't change while the Hook runs. */
export declare function computerHardware(platform?: NodeJS.Platform): Promise<ComputerHardware>;
/** A `ps` row; `pgid` is the process group the job registry attributes work by. */
export interface ProcessRow extends ProcessLike {
cpu: number;
rssKB: number;
command: string;
args: string;
}
/** `ps -axo pid=,ppid=,pgid=,pcpu=,rss=,comm=` paired with the `args=` listing by
* pid. The older five-column stats (no pgid) still parse, with pgid 0. */
export declare function parsePs(stats: string, args: string): ProcessRow[];
/** What kind of heavy job a process is, if any. */
export declare function heavyKind(row: ProcessRow): {
kind: HeavyKind;
name: string;
} | undefined;
/**
* The heavy jobs in a process table. Every process counts toward its nearest
* heavy ancestor-or-self, so a Codex worker running xcodebuild shows both,
* each with only its own share. Any other program shows as `busy` when its
* processes together hold half a core. Known programs below both a core's
* tenth and 200 MiB are left out, including simulators and emulators.
* A memory-only row can be an idle session or helper; it is not evidence of
* agent activity. Counts include OS helpers, not just the named executable.
* On Linux, ps pcpu is a process lifetime average, so these cutoffs can miss
* daemons that spike briefly.
*/
export declare function heavyProcesses(rows: ProcessRow[], owners?: Map<number, PaneRef>, jobs?: readonly TrackedJob[], limit?: number): HeavyProcess[];
/** `pmset -g batt`: "Now drawing from 'AC Power'" then " -InternalBattery-0 (id=…) 87%; charging; …". */
export declare function parsePmset(output: string): ComputerResources["battery"];
/** `sysctl vm.swapusage`: "total = 2048.00M used = 798.50M free = …". */
export declare function parseSwap(output: string): number | undefined;
/** /proc/meminfo: MemTotal, MemAvailable and swap, in kB. */
export declare function parseMeminfo(text: string): ComputerResources["memory"];
/** Gauge fill and warnings from the numbers. */
export declare function assess(value: Omit<ComputerResources, "pressure" | "level" | "warnings">): Pick<ComputerResources, "pressure" | "level" | "warnings">;
/** Seconds from `ps`'s `etime` (`[[dd-]hh:]mm:ss`), or undefined. */
export declare function parseEtime(value: string): number | undefined;
/** Each row's start time (epoch ms, to about a second) from a `pid=,etime=`
* listing. The job registry checks a process group's leader by it, so a group
* id the system reused for another program is never signalled. */
export declare function withStartTimes(rows: ProcessRow[], elapsed: string, now: number): ProcessRow[];
/** Every pane of every terminal server this computer knows, or undefined when
* any server's snapshot could not be read. Cleanup ends a job only when its
* pane is missing from a complete listing: a pane that merely failed to
* answer, or sat past a display cap, must never look gone. */
export declare function livePanes(): Promise<{
panes: Set<string>;
servers: Set<string>;
} | undefined>;
/** Every pid a Herdr or tmux pane holds (its shell and foreground), for naming a job's pane. */
export declare function paneOwners(): Promise<Map<number, PaneRef>>;
export interface ResourceDeps {
platform: NodeJS.Platform;
now: () => number;
home: () => string;
processes: () => Promise<ProcessRow[]>;
owners: () => Promise<Map<number, PaneRef>>;
/** Every live pane and the servers listed, or undefined when the listing is incomplete (see `livePanes`). */
panes: () => Promise<{
panes: Set<string>;
servers: Set<string>;
} | undefined>;
hardware?: () => Promise<ComputerHardware | undefined>;
jobs?: JobSource;
}
export declare function collectResources(deps: ResourceDeps): Promise<ComputerResources>;
/** One collection at a time, reused for `RESOURCES_MAX_AGE_MS`. */
export declare class ResourceMonitor {
private maxAgeMs;
private cached?;
private pending?;
private readonly deps;
private readonly registry;
constructor(deps?: Partial<ResourceDeps>, maxAgeMs?: number);
read(): Promise<ComputerResources>;
/**
* End only the registered process groups whose owning pane is gone. The
* registry (job-registry.ts) does the safety checks; a failed pane read ends
* nothing. Never the Hook's own service, never an owner's session, never an
* unregistered process.
*/
cleanupJobs(): Promise<CleanupReport>;
}