UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

180 lines (179 loc) • 8.37 kB
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>; }