agent-yes 1.266.0 → 1.267.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/{SUPPORTED_CLIS-AFV5pjSx.js → SUPPORTED_CLIS-BByfWaoz.js} +2 -2
  2. package/dist/{SUPPORTED_CLIS-DRVuSo48.js → SUPPORTED_CLIS-Bob214Yb.js} +3 -3
  3. package/dist/{agentShare-qdFSp9Uq.js → agentShare-TGChdMCD.js} +3 -3
  4. package/dist/{callback-DfNFdZin.js → callback-B2thylEC.js} +3 -3
  5. package/dist/{callback-Da7MRLh6.js → callback-D93yB1xz.js} +3 -3
  6. package/dist/cli.js +11 -11
  7. package/dist/cmdPs-D7NlwNFV.js +577 -0
  8. package/dist/{cwdConflictWarn-CtQhl-vF.js → cwdConflictWarn-BpweoAiy.js} +1 -1
  9. package/dist/{cwdPassthroughHint-apaX_O7p.js → cwdPassthroughHint-BlRuOGo_.js} +1 -1
  10. package/dist/{expose-BXn6My6A.js → expose-DyiZ5AU0.js} +1 -1
  11. package/dist/{forkNested-CDfWPiHw.js → forkNested-B_4c-TQk.js} +1 -1
  12. package/dist/{hist-BpocAe1N.js → hist-BoZPJ2ah.js} +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/{initMsg-C-w4A_eF.js → initMsg-BKqWDYFk.js} +1 -1
  15. package/dist/{initMsg-BjRseolL.js → initMsg-D8cmdY76.js} +1 -1
  16. package/dist/{nodeRuntime-CRoEMWkp.js → nodeRuntime-QihGFQbI.js} +1 -1
  17. package/dist/{notifyDaemon-BFzqcdN1.js → notifyDaemon-B-vCwFi3.js} +3 -3
  18. package/dist/{openBrowser-DhQ9pJRi.js → openBrowser-Cs46NiH6.js} +1 -1
  19. package/dist/{parentLink-B798ASCW.js → parentLink-CQrn998L.js} +1 -1
  20. package/dist/{parentLink-C0cIRjfL.js → parentLink-CTgZZCgw.js} +1 -1
  21. package/dist/{parentPingLoop-BIKxzbPH.js → parentPingLoop-CVD0sREv.js} +4 -4
  22. package/dist/{remotes-iMhdZytp.js → remotes-DdAfueOu.js} +2 -2
  23. package/dist/{remotes-Dg_JscKk.js → remotes-M8FbM92D.js} +1 -1
  24. package/dist/{rustBinary-JGNROWfY.js → rustBinary-Bf9514x8.js} +2 -2
  25. package/dist/{schedule-DbLpjgl5.js → schedule-fgKVzax1.js} +6 -6
  26. package/dist/{serve-ucVXOci1.js → serve-DM5-VJPX.js} +26 -26
  27. package/dist/{setup-De7uYRZb.js → setup-DNuxXwYS.js} +4 -4
  28. package/dist/{spawnGate-BvaZeHE7.js → spawnGate-CZlMs6uV.js} +1 -1
  29. package/dist/{spawnGate-CEXVmTtv.js → spawnGate-DN4wNXHq.js} +2 -2
  30. package/dist/{subcommands-C04exkIM.js → subcommands-3rBreZdT.js} +2 -2
  31. package/dist/{subcommands-DtF31RoW.js → subcommands-ujgVZRT3.js} +24 -24
  32. package/dist/{systemPathLink-7TFPwJna.js → systemPathLink-BHv1I6wh.js} +1 -1
  33. package/dist/{terminal-Dm32SXU-.js → terminal-jJ2YDBDS.js} +2 -2
  34. package/dist/{todoCli-c_R7D7Y-.js → todoCli-BYGZ7_C5.js} +1 -1
  35. package/dist/{tray-DZnGjJBK.js → tray-CzGs9-ch.js} +1 -1
  36. package/dist/trayApp-CPDWlxfV.js +5 -0
  37. package/dist/{trayApp-CfkPG0wy.js → trayApp-DbV2E8dZ.js} +2 -2
  38. package/dist/{ts-CGtoPER5.js → ts-CQIqcW7_.js} +5 -5
  39. package/dist/{versionChecker-CJgW9DHY.js → versionChecker-D49xkrMv.js} +2 -2
  40. package/dist/{webrtcRemote-BDg0jjTs.js → webrtcRemote-Bl8eowma.js} +1 -1
  41. package/dist/{widget-DXtaX3QR.js → widget-CRXwzRP7.js} +3 -3
  42. package/dist/{workspaceConfig-HiWQzq6A.js → workspaceConfig-CBKlgA_q.js} +1 -1
  43. package/dist/{ws-aKRZpI9b.js → ws-B4ZoCy71.js} +3 -3
  44. package/dist/{ws-CqlS5JhN.js → ws-DomYQT_I.js} +3 -3
  45. package/lab/ui/sw.js +10 -1
  46. package/package.json +1 -1
  47. package/ts/cmdPs.spec.ts +389 -0
  48. package/ts/cmdPs.ts +343 -0
  49. package/ts/procStats.spec.ts +506 -0
  50. package/ts/procStats.ts +484 -0
  51. package/ts/subcommands.ts +9 -4
  52. package/dist/trayApp-CZbwLK9t.js +0 -5
@@ -0,0 +1,484 @@
1
+ /**
2
+ * Per-process-TREE resource rollup, for `ay ps`.
3
+ *
4
+ * Why this exists: `ay ls` lists WRAPPER pids, but a single ay-managed agent is
5
+ * a whole subtree — the wrapper, the CLI, its version process, `bg-pty-host`,
6
+ * `bg-spare`, the daemon, plus whatever the agent itself spawned. On a busy box
7
+ * one session can be 30+ processes. `ps`/`htop`/`glances` see those as
8
+ * unrelated rows and cannot attribute them to an agent; only ay knows the
9
+ * wrapper pids, so only ay can roll the cost up to "this session costs 612 MiB".
10
+ *
11
+ * Two things here are deliberately NOT the obvious approach:
12
+ *
13
+ * 1. CPU is a SAMPLED DELTA, never `ps`'s lifetime average. A session alive for
14
+ * 7 days shows a meaningless smeared percentage; "is this agent busy right
15
+ * now" needs two reads of the cumulative counter a moment apart. That's why
16
+ * the caller must await a sampling window — see `sampleTrees`.
17
+ *
18
+ * 2. Rollup walks the FULL descendant closure, not direct children. Agents spawn
19
+ * subagents that spawn shells; a depth-1 sum undercounts badly.
20
+ *
21
+ * BEST-EFFORT by contract, like `procStartTime.ts`: every reader returns null /
22
+ * empty when it cannot answer (no /proc, unreadable pid, a process that exited
23
+ * mid-sample). Callers render "-" rather than failing — this is an inspection
24
+ * command, it must never be the thing that breaks.
25
+ */
26
+
27
+ import { execFile } from "node:child_process";
28
+ import { readdir, readFile } from "node:fs/promises";
29
+ import { promisify } from "node:util";
30
+
31
+ const execFileAsync = promisify(execFile);
32
+
33
+ /** One process, as sampled. `cpuSeconds` is CUMULATIVE since its start. */
34
+ export interface ProcSample {
35
+ pid: number;
36
+ ppid: number;
37
+ /** Resident set size in bytes. */
38
+ rss: number;
39
+ /** Cumulative CPU (user+sys) in seconds since process start. */
40
+ cpuSeconds: number;
41
+ /** Single-letter state: R, S, D, Z, T… */
42
+ state: string;
43
+ /**
44
+ * Opaque "when did this pid start" token, for detecting pid REUSE between the
45
+ * two samples. Empty when unknowable (the non-Linux `ps` fallback), which
46
+ * callers must read as "no opinion", never as a mismatch.
47
+ */
48
+ startToken: string;
49
+ }
50
+
51
+ /**
52
+ * USER_HZ. The kernel reports utime/stime in clock ticks, and this is 100 on
53
+ * every Linux ABI we run on — it is fixed at the syscall boundary, NOT the
54
+ * configurable CONFIG_HZ. There is no cheap way to query it from Node.
55
+ */
56
+ const USER_HZ = 100;
57
+
58
+ /** Assumed page size when we cannot measure the real one. */
59
+ const DEFAULT_PAGE_SIZE = 4096;
60
+
61
+ /**
62
+ * Real page size for the `rss` field of /proc/<pid>/stat, which is in PAGES.
63
+ *
64
+ * Hardcoding 4 KiB under-reports by 4× on a 16 KiB-page arm64 kernel, so measure
65
+ * it instead — for free, with no `getconf` spawn: /proc/self/status reports our
66
+ * own VmRSS in kB while /proc/self/stat reports the same figure in pages, and
67
+ * the ratio is the page size. Snapped to the nearest supported power of two
68
+ * because VmRSS is kB-rounded. Measured once per process, then cached.
69
+ */
70
+ let cachedPageSize: number | null = null;
71
+
72
+ export function derivePageSize(statLine: string, statusText: string): number {
73
+ const close = statLine.lastIndexOf(")");
74
+ if (close < 0) return DEFAULT_PAGE_SIZE;
75
+ const pages = Number(
76
+ statLine
77
+ .slice(close + 1)
78
+ .trim()
79
+ .split(/\s+/)[21],
80
+ );
81
+ const kb = Number(/^VmRSS:\s+(\d+)/m.exec(statusText)?.[1]);
82
+ if (!Number.isFinite(pages) || !Number.isFinite(kb) || pages <= 0 || kb <= 0) {
83
+ return DEFAULT_PAGE_SIZE;
84
+ }
85
+ const raw = (kb * 1024) / pages;
86
+ let best = DEFAULT_PAGE_SIZE;
87
+ for (const candidate of [4096, 8192, 16384, 65536]) {
88
+ if (Math.abs(raw - candidate) < Math.abs(raw - best)) best = candidate;
89
+ }
90
+ return best;
91
+ }
92
+
93
+ async function pageSize(deps?: ProcReaders): Promise<number> {
94
+ if (cachedPageSize !== null) return cachedPageSize;
95
+ const readSys = deps?.readSys ?? defaultReadSys;
96
+ const [stat, status] = await Promise.all([readSys("self/stat"), readSys("self/status")]);
97
+ cachedPageSize = stat && status ? derivePageSize(stat, status) : DEFAULT_PAGE_SIZE;
98
+ return cachedPageSize;
99
+ }
100
+
101
+ /**
102
+ * Parse the fields of a /proc/<pid>/stat line that we need.
103
+ *
104
+ * As in `parseLinuxStartTime`, the parse MUST start after the last `)`: field 2
105
+ * is the comm and may contain spaces and parens, so naive splitting mis-indexes
106
+ * everything after it. Post-`)` fields begin at field 3, so field N is at index
107
+ * N-3: state=3→0, ppid=4→1, utime=14→11, stime=15→12, starttime=22→19, rss=24→21.
108
+ */
109
+ export function parseProcStat(
110
+ pid: number,
111
+ stat: string,
112
+ pageSizeBytes: number = DEFAULT_PAGE_SIZE,
113
+ ): ProcSample | null {
114
+ const close = stat.lastIndexOf(")");
115
+ if (close < 0) return null;
116
+ const f = stat
117
+ .slice(close + 1)
118
+ .trim()
119
+ .split(/\s+/);
120
+ const state = f[0];
121
+ const ppid = Number(f[1]);
122
+ const utime = Number(f[11]);
123
+ const stime = Number(f[12]);
124
+ const rssPages = Number(f[21]);
125
+ if (!state || !Number.isFinite(ppid) || !Number.isFinite(utime) || !Number.isFinite(stime)) {
126
+ return null;
127
+ }
128
+ return {
129
+ pid,
130
+ ppid,
131
+ rss: Number.isFinite(rssPages) ? rssPages * pageSizeBytes : 0,
132
+ cpuSeconds: (utime + stime) / USER_HZ,
133
+ state,
134
+ startToken: f[19] ?? "",
135
+ };
136
+ }
137
+
138
+ /** Injection seam so specs can drive every path without a real /proc. */
139
+ export interface ProcReaders {
140
+ platform?: string;
141
+ /** Numeric pid entries of /proc. */
142
+ listPids?: () => Promise<number[]>;
143
+ /** Contents of /proc/<pid>/stat, or null when gone. */
144
+ readStat?: (pid: number) => Promise<string | null>;
145
+ /** Whole-file read of /proc/loadavg, /proc/meminfo, … or null. */
146
+ readSys?: (name: string) => Promise<string | null>;
147
+ /** `ps -eo pid=,ppid=,rss=,time=,state=` output, for the non-Linux fallback. */
148
+ readPsTable?: () => Promise<string | null>;
149
+ }
150
+
151
+ export async function defaultListPids(): Promise<number[]> {
152
+ try {
153
+ const entries = await readdir("/proc");
154
+ const out: number[] = [];
155
+ for (const e of entries) {
156
+ // /proc holds plenty of non-pid entries (self, meminfo, sys…); the
157
+ // all-digits test is the standard way to pick out the processes.
158
+ if (/^\d+$/.test(e)) out.push(Number(e));
159
+ }
160
+ return out;
161
+ } catch {
162
+ return [];
163
+ }
164
+ }
165
+
166
+ export async function defaultReadStat(pid: number): Promise<string | null> {
167
+ try {
168
+ return await readFile(`/proc/${pid}/stat`, "utf8");
169
+ } catch {
170
+ // Racing a process that exited between readdir and read is NORMAL on a busy
171
+ // box, not an error worth surfacing.
172
+ return null;
173
+ }
174
+ }
175
+
176
+ export async function defaultReadSys(name: string): Promise<string | null> {
177
+ try {
178
+ return await readFile(`/proc/${name}`, "utf8");
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+
184
+ export async function defaultReadPsTable(): Promise<string | null> {
185
+ try {
186
+ const { stdout } = await execFileAsync("ps", ["-eo", "pid=,ppid=,rss=,time=,state="], {
187
+ encoding: "utf8",
188
+ timeout: 3000,
189
+ maxBuffer: 16 * 1024 * 1024,
190
+ });
191
+ return stdout;
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+
197
+ /** Parse `[[dd-]hh:]mm:ss` (ps `time=`) into seconds. */
198
+ export function parsePsTime(raw: string): number {
199
+ const [dayPart, clockPart] = raw.includes("-") ? raw.split("-", 2) : [null, raw];
200
+ const parts = (clockPart ?? "").split(":").map(Number);
201
+ if (parts.some((n) => !Number.isFinite(n))) return 0;
202
+ let seconds = 0;
203
+ for (const p of parts) seconds = seconds * 60 + p;
204
+ if (dayPart !== null) {
205
+ const days = Number(dayPart);
206
+ if (Number.isFinite(days)) seconds += days * 86400;
207
+ }
208
+ return seconds;
209
+ }
210
+
211
+ /** Parse the `ps -eo pid=,ppid=,rss=,time=,state=` fallback table. */
212
+ export function parsePsTable(out: string): Map<number, ProcSample> {
213
+ const procs = new Map<number, ProcSample>();
214
+ for (const line of out.split("\n")) {
215
+ const m = line.trim().split(/\s+/);
216
+ if (m.length < 5) continue;
217
+ const [pidS, ppidS, rssS, timeS, state] = m;
218
+ const pid = Number(pidS);
219
+ const ppid = Number(ppidS);
220
+ const rssKib = Number(rssS);
221
+ if (!Number.isFinite(pid) || !Number.isFinite(ppid)) continue;
222
+ procs.set(pid, {
223
+ pid,
224
+ ppid,
225
+ // ps reports RSS in KiB; /proc gives bytes. Normalize to bytes so both
226
+ // backends feed identical numbers into the rollup.
227
+ rss: Number.isFinite(rssKib) ? rssKib * 1024 : 0,
228
+ cpuSeconds: parsePsTime(timeS ?? ""),
229
+ state: state ?? "?",
230
+ // No start token from this table: `ps -o lstart=` contains spaces and
231
+ // would break the column split, and `etimes` changes between samples so
232
+ // it cannot serve as a stable identity. The pid-reuse guard is therefore
233
+ // Linux-only; elsewhere we fall back to matching on pid alone.
234
+ startToken: "",
235
+ });
236
+ }
237
+ return procs;
238
+ }
239
+
240
+ /** One snapshot of every visible process, keyed by pid. */
241
+ export async function snapshotProcs(deps?: ProcReaders): Promise<Map<number, ProcSample>> {
242
+ const platform = deps?.platform ?? process.platform;
243
+ if (platform !== "linux" && !deps?.listPids) {
244
+ const table = await (deps?.readPsTable ?? defaultReadPsTable)();
245
+ return table ? parsePsTable(table) : new Map();
246
+ }
247
+ const pids = await (deps?.listPids ?? defaultListPids)();
248
+ const readStat = deps?.readStat ?? defaultReadStat;
249
+ const bytesPerPage = await pageSize(deps);
250
+ const procs = new Map<number, ProcSample>();
251
+ const samples = await Promise.all(
252
+ pids.map(async (pid) => {
253
+ const stat = await readStat(pid);
254
+ return stat ? parseProcStat(pid, stat, bytesPerPage) : null;
255
+ }),
256
+ );
257
+ for (const s of samples) if (s) procs.set(s.pid, s);
258
+ return procs;
259
+ }
260
+
261
+ /** ppid → children index, for descendant walks. */
262
+ export function buildChildIndex(procs: Map<number, ProcSample>): Map<number, number[]> {
263
+ const kids = new Map<number, number[]>();
264
+ for (const p of procs.values()) {
265
+ const list = kids.get(p.ppid);
266
+ if (list) list.push(p.pid);
267
+ else kids.set(p.ppid, [p.pid]);
268
+ }
269
+ return kids;
270
+ }
271
+
272
+ /**
273
+ * Every pid in the subtree rooted at `root`, inclusive.
274
+ *
275
+ * Iterative with a seen-set: a corrupt/racing snapshot can present a parent
276
+ * cycle, and recursion would blow the stack rather than degrade.
277
+ */
278
+ export function descendantsOf(
279
+ root: number,
280
+ kids: Map<number, number[]>,
281
+ seen = new Set<number>(),
282
+ ): Set<number> {
283
+ const out = new Set<number>();
284
+ const stack = [root];
285
+ while (stack.length > 0) {
286
+ const pid = stack.pop() as number;
287
+ if (out.has(pid) || seen.has(pid)) continue;
288
+ out.add(pid);
289
+ for (const child of kids.get(pid) ?? []) stack.push(child);
290
+ }
291
+ return out;
292
+ }
293
+
294
+ /** Rolled-up cost of one agent's whole process subtree. */
295
+ export interface TreeStats {
296
+ /** The wrapper pid this tree is rooted at. */
297
+ pid: number;
298
+ /** Summed RSS in bytes across the subtree. */
299
+ rss: number;
300
+ /** Percent of ONE core, summed across the subtree (may exceed 100). */
301
+ cpuPercent: number;
302
+ /** Number of live processes in the subtree, inclusive of the root. */
303
+ procs: number;
304
+ }
305
+
306
+ function rollup(
307
+ root: number,
308
+ members: Set<number>,
309
+ first: Map<number, ProcSample>,
310
+ second: Map<number, ProcSample>,
311
+ elapsedSeconds: number,
312
+ ): TreeStats {
313
+ let rss = 0;
314
+ let cpuDelta = 0;
315
+ let count = 0;
316
+ for (const pid of members) {
317
+ const now = second.get(pid);
318
+ if (!now) continue; // exited during the window
319
+ count++;
320
+ rss += now.rss;
321
+ const before = first.get(pid);
322
+ // Two reasons to skip the delta:
323
+ //
324
+ // - No baseline: the process appeared DURING the window. Counting its full
325
+ // cumulative CPU would credit a long-lived child's entire history to this
326
+ // one-second window and spike the row to nonsense, so a newcomer
327
+ // contributes 0 until the next sample. It under-reports a genuinely new
328
+ // busy child for one tick, which beats a wildly wrong number.
329
+ //
330
+ // - PID REUSE: the pid is the same but the process behind it is not, so the
331
+ // two counters belong to unrelated processes and subtracting them is
332
+ // meaningless. An empty token means "no opinion" (the ps fallback), which
333
+ // must not be read as a mismatch.
334
+ const reused =
335
+ before !== undefined &&
336
+ before.startToken !== "" &&
337
+ now.startToken !== "" &&
338
+ before.startToken !== now.startToken;
339
+ if (before && !reused) cpuDelta += Math.max(0, now.cpuSeconds - before.cpuSeconds);
340
+ }
341
+ return {
342
+ pid: root,
343
+ rss,
344
+ cpuPercent: elapsedSeconds > 0 ? (cpuDelta / elapsedSeconds) * 100 : 0,
345
+ procs: count,
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Sample every root's subtree over a real time window.
351
+ *
352
+ * Roots are attributed in the order given, and a pid is claimed by the FIRST
353
+ * root that reaches it — so a nested agent whose wrapper is itself listed as a
354
+ * root is never double-counted into its parent's total.
355
+ *
356
+ * Pass DEEPEST-FIRST (children before parents). A nested agent must claim its
357
+ * own subtree before its parent walks through it; parent-first instead makes the
358
+ * parent absorb everything and renders the child as a bogus 0-proc row.
359
+ */
360
+ export async function sampleTrees(
361
+ roots: number[],
362
+ opts: { windowMs?: number; deps?: ProcReaders } = {},
363
+ ): Promise<{ trees: Map<number, TreeStats>; unattributed: TreeStats }> {
364
+ const windowMs = Math.max(100, opts.windowMs ?? 1000);
365
+ // Measure the ACTUAL elapsed time, not the requested window: a loaded box (the
366
+ // exact case this command is for) can oversleep by hundreds of ms, and
367
+ // dividing by the nominal window would over-report every row.
368
+ //
369
+ // MIDPOINT to MIDPOINT, not end-of-first to end-of-second: a snapshot of ~1400
370
+ // processes is not instantaneous, so each pid's counter is read at some moment
371
+ // *inside* each pass. Anchoring on the ends charges the second pass's whole
372
+ // duration to the window while ignoring the first's, which systematically
373
+ // under-reports CPU% by exactly the scan time — worst on the loaded boxes this
374
+ // command exists for. The midpoints are the unbiased estimate of when the
375
+ // average counter was actually read.
376
+ const firstStart = Date.now();
377
+ const first = await snapshotProcs(opts.deps);
378
+ const firstMid = (firstStart + Date.now()) / 2;
379
+ await new Promise((r) => setTimeout(r, windowMs));
380
+ const secondStart = Date.now();
381
+ const second = await snapshotProcs(opts.deps);
382
+ const secondMid = (secondStart + Date.now()) / 2;
383
+ const elapsedSeconds = Math.max(0.001, (secondMid - firstMid) / 1000);
384
+
385
+ const kids = buildChildIndex(second);
386
+ const claimed = new Set<number>();
387
+ const trees = new Map<number, TreeStats>();
388
+ for (const root of roots) {
389
+ const members = descendantsOf(root, kids, claimed);
390
+ for (const pid of members) claimed.add(pid);
391
+ trees.set(root, rollup(root, members, first, second, elapsedSeconds));
392
+ }
393
+
394
+ // Everything ay does NOT manage, as one row. This is the answer to "is there
395
+ // anything safe to reap?" — without it you cannot tell a fully-accounted-for
396
+ // box from one quietly hoarding orphans.
397
+ const rest = new Set<number>();
398
+ for (const pid of second.keys()) if (!claimed.has(pid)) rest.add(pid);
399
+ return { trees, unattributed: rollup(0, rest, first, second, elapsedSeconds) };
400
+ }
401
+
402
+ /** Whole-box vitals for the header line. */
403
+ export interface SystemStats {
404
+ load: [number, number, number] | null;
405
+ ncpu: number;
406
+ memTotalBytes: number | null;
407
+ memAvailableBytes: number | null;
408
+ swapTotalBytes: number | null;
409
+ swapFreeBytes: number | null;
410
+ zombies: number;
411
+ }
412
+
413
+ /** Parse /proc/meminfo (`MemTotal: 16316360 kB`) into a kB-valued map. */
414
+ export function parseMeminfo(raw: string): Map<string, number> {
415
+ const out = new Map<string, number>();
416
+ for (const line of raw.split("\n")) {
417
+ const m = /^(\w+):\s+(\d+)/.exec(line);
418
+ if (m?.[1] && m[2]) out.set(m[1], Number(m[2]) * 1024);
419
+ }
420
+ return out;
421
+ }
422
+
423
+ export async function systemStats(
424
+ procs: Map<number, ProcSample>,
425
+ deps?: ProcReaders,
426
+ ): Promise<SystemStats> {
427
+ const readSys = deps?.readSys ?? defaultReadSys;
428
+ const [loadRaw, memRaw] = await Promise.all([readSys("loadavg"), readSys("meminfo")]);
429
+ const loadFields = loadRaw?.trim().split(/\s+/) ?? [];
430
+ const load =
431
+ loadFields.length >= 3
432
+ ? ([Number(loadFields[0]), Number(loadFields[1]), Number(loadFields[2])] as [
433
+ number,
434
+ number,
435
+ number,
436
+ ])
437
+ : null;
438
+ const mem = memRaw ? parseMeminfo(memRaw) : new Map<string, number>();
439
+ let zombies = 0;
440
+ for (const p of procs.values()) if (p.state === "Z") zombies++;
441
+
442
+ // Off Linux there is no /proc, so the header would otherwise be entirely
443
+ // blank. node:os supplies load and memory on macOS/BSD — no swap breakdown,
444
+ // and loadavg is always [0,0,0] on Windows, so both stay null there.
445
+ const os = await import("node:os");
446
+ const osLoad = os.loadavg();
447
+ const loadFallback =
448
+ process.platform !== "win32" && osLoad.some((n) => n > 0)
449
+ ? ([osLoad[0], osLoad[1], osLoad[2]] as [number, number, number])
450
+ : null;
451
+
452
+ return {
453
+ load: load && load.every(Number.isFinite) ? load : loadFallback,
454
+ ncpu: os.cpus().length || 1,
455
+ memTotalBytes: mem.get("MemTotal") ?? os.totalmem() ?? null,
456
+ // MemAvailable is the kernel's own estimate of what a new allocation can
457
+ // actually get; "free" is misleading on any box with a warm page cache — but
458
+ // os.freemem() is the only portable stand-in.
459
+ memAvailableBytes: mem.get("MemAvailable") ?? os.freemem() ?? null,
460
+ swapTotalBytes: mem.get("SwapTotal") ?? null,
461
+ swapFreeBytes: mem.get("SwapFree") ?? null,
462
+ zombies,
463
+ };
464
+ }
465
+
466
+ /** `612Mi`, `1.4Gi` — fixed-width-ish, no trailing noise. */
467
+ export function humanBytes(bytes: number | null): string {
468
+ if (bytes === null || !Number.isFinite(bytes)) return "-";
469
+ const units: [number, string][] = [
470
+ [1024 ** 3, "Gi"],
471
+ [1024 ** 2, "Mi"],
472
+ [1024, "Ki"],
473
+ ];
474
+ for (const [scale, suffix] of units) {
475
+ if (bytes >= scale) {
476
+ const v = bytes / scale;
477
+ // Keep one decimal up to 3 significant digits: at Gi scale a bare integer
478
+ // rounds 15.6Gi to "16Gi", which visibly disagrees with `free -h` and
479
+ // makes the header look wrong.
480
+ return `${v >= 100 ? Math.round(v) : v.toFixed(1)}${suffix}`;
481
+ }
482
+ }
483
+ return `${Math.round(bytes)}B`;
484
+ }
package/ts/subcommands.ts CHANGED
@@ -584,8 +584,11 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
584
584
  switch (sub) {
585
585
  case "ls":
586
586
  case "list":
587
- case "ps":
588
587
  return await cmdLs(rest);
588
+ // `ps` was an alias for `ls`; it is now the RESOURCE view — same agents,
589
+ // rolled up per process tree with box vitals. See ts/cmdPs.ts.
590
+ case "ps":
591
+ return await (await import("./cmdPs.ts")).cmdPs(rest);
589
592
  case "status":
590
593
  return await cmdStatus(rest);
591
594
  case "whoami":
@@ -832,6 +835,8 @@ export async function cmdHelp(managerCommands = true): Promise<number> {
832
835
  `\n` +
833
836
  `Management:\n` +
834
837
  ` ay ls [keyword] list running agents\n` +
838
+ ` ay ps [keyword] per-agent CPU/RSS, rolled up over each\n` +
839
+ ` agent's whole process tree, + box vitals\n` +
835
840
  ` ay tail [-f] [-n N] <keyword> last N lines (96), -f to follow\n` +
836
841
  ` ay read <keyword> [page opts] paginate: --last/--head N, --range A:B,\n` +
837
842
  ` --before-line L [--limit N]\n` +
@@ -1822,9 +1827,9 @@ async function cmdLs(rest: string[]): Promise<number> {
1822
1827
  const y = yargs(rest)
1823
1828
  .usage(
1824
1829
  "Usage: ay ls [keyword] [options]\n" +
1825
- " ay list [keyword] [options]\n" +
1826
- " ay ps [keyword] [options]\n\n" +
1827
- "List running agents. Optionally filter by keyword (pid, cwd substring, or prompt substring).",
1830
+ " ay list [keyword] [options]\n\n" +
1831
+ "List running agents. Optionally filter by keyword (pid, cwd substring, or prompt substring).\n" +
1832
+ "For per-agent CPU/memory usage, see `ay ps`.",
1828
1833
  )
1829
1834
  .option("all", {
1830
1835
  type: "boolean",
@@ -1,5 +0,0 @@
1
- import "./versionChecker-CJgW9DHY.js";
2
- import "./rustBinary-JGNROWfY.js";
3
- import { a as trayHiddenMarker, i as launchTray, n as hasDesktop, r as isTrayHidden, t as cmdTray } from "./trayApp-CfkPG0wy.js";
4
-
5
- export { cmdTray };