@4pm/cli 1.4.1 → 1.4.2

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 (2) hide show
  1. package/dist/index.js +113 -25
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -33309,9 +33309,47 @@ async function readCgroup(file) {
33309
33309
  return null;
33310
33310
  }
33311
33311
  }
33312
+ function parseKv(text) {
33313
+ const map = /* @__PURE__ */ new Map();
33314
+ if (!text) return map;
33315
+ for (const line of text.split("\n")) {
33316
+ const [k, v] = line.trim().split(/\s+/);
33317
+ if (k && v !== void 0) {
33318
+ const n = Number(v);
33319
+ if (Number.isFinite(n)) map.set(k, n);
33320
+ }
33321
+ }
33322
+ return map;
33323
+ }
33324
+ function parsePressureSome(text) {
33325
+ if (!text) return void 0;
33326
+ const some = text.split("\n").find((l) => l.startsWith("some "));
33327
+ const m = some?.match(/avg10=([\d.]+)/);
33328
+ const n = m ? Number(m[1]) : NaN;
33329
+ return Number.isFinite(n) ? clampPct(n) : void 0;
33330
+ }
33331
+ function parseIoStat(text) {
33332
+ if (!text) return null;
33333
+ let rBytes = 0;
33334
+ let wBytes = 0;
33335
+ for (const line of text.split("\n")) {
33336
+ const r = line.match(/\brbytes=(\d+)/);
33337
+ const w = line.match(/\bwbytes=(\d+)/);
33338
+ if (r) rBytes += Number(r[1]);
33339
+ if (w) wBytes += Number(w[1]);
33340
+ }
33341
+ return { rBytes, wBytes };
33342
+ }
33343
+ function parseMaybeMax(text) {
33344
+ if (text === null) return null;
33345
+ if (text === "max") return null;
33346
+ const n = Number(text);
33347
+ return Number.isFinite(n) ? n : null;
33348
+ }
33312
33349
  function createWorkerMetricsSampler(diskPath) {
33313
33350
  const cgroupV2 = existsSync4(`${CGROUP}/cpu.stat`) && existsSync4(`${CGROUP}/memory.current`);
33314
- let prev = null;
33351
+ let cgPrev = null;
33352
+ let osPrev = null;
33315
33353
  async function cgroupCpuCount() {
33316
33354
  const raw = await readCgroup("cpu.max");
33317
33355
  if (raw) {
@@ -33323,22 +33361,6 @@ function createWorkerMetricsSampler(diskPath) {
33323
33361
  }
33324
33362
  return cpus().length;
33325
33363
  }
33326
- async function cgroupCpuPct(cpuCount) {
33327
- const stat2 = await readCgroup("cpu.stat");
33328
- const m = stat2?.match(/usage_usec\s+(\d+)/);
33329
- const usageUsec = m ? Number(m[1]) : NaN;
33330
- const now = Date.now();
33331
- if (!Number.isFinite(usageUsec)) return 0;
33332
- if (!prev) {
33333
- prev = { busy: usageUsec, ref: now };
33334
- return 0;
33335
- }
33336
- const dUsage = usageUsec - prev.busy;
33337
- const dWall = (now - prev.ref) * 1e3;
33338
- prev = { busy: usageUsec, ref: now };
33339
- if (dWall <= 0) return 0;
33340
- return clampPct(dUsage / (dWall * cpuCount) * 100);
33341
- }
33342
33364
  function osCpuPct() {
33343
33365
  let busy = 0;
33344
33366
  let total = 0;
@@ -33347,13 +33369,13 @@ function createWorkerMetricsSampler(diskPath) {
33347
33369
  busy += t.user + t.nice + t.sys + t.irq;
33348
33370
  total += t.user + t.nice + t.sys + t.irq + t.idle;
33349
33371
  }
33350
- if (!prev) {
33351
- prev = { busy, ref: total };
33372
+ if (!osPrev) {
33373
+ osPrev = { busy, total };
33352
33374
  return 0;
33353
33375
  }
33354
- const dBusy = busy - prev.busy;
33355
- const dTotal = total - prev.ref;
33356
- prev = { busy, ref: total };
33376
+ const dBusy = busy - osPrev.busy;
33377
+ const dTotal = total - osPrev.total;
33378
+ osPrev = { busy, total };
33357
33379
  if (dTotal <= 0) return 0;
33358
33380
  return clampPct(dBusy / dTotal * 100);
33359
33381
  }
@@ -33367,9 +33389,74 @@ function createWorkerMetricsSampler(diskPath) {
33367
33389
  return { usedGb: 0, totalGb: 0 };
33368
33390
  }
33369
33391
  }
33392
+ async function cgroupExtended(cpuCount, now) {
33393
+ const [cpuStatRaw, memStatRaw, ioStatRaw, cpuPsi, memPsi, ioPsi, swapCurRaw, swapMaxRaw, memEventsRaw, pidsCurRaw, pidsMaxRaw] = await Promise.all([
33394
+ readCgroup("cpu.stat"),
33395
+ readCgroup("memory.stat"),
33396
+ readCgroup("io.stat"),
33397
+ readCgroup("cpu.pressure"),
33398
+ readCgroup("memory.pressure"),
33399
+ readCgroup("io.pressure"),
33400
+ readCgroup("memory.swap.current"),
33401
+ readCgroup("memory.swap.max"),
33402
+ readCgroup("memory.events"),
33403
+ readCgroup("pids.current"),
33404
+ readCgroup("pids.max")
33405
+ ]);
33406
+ const cpuStat = parseKv(cpuStatRaw);
33407
+ const usageUsec = cpuStat.get("usage_usec") ?? NaN;
33408
+ const nrThrottled = cpuStat.get("nr_throttled") ?? 0;
33409
+ const nrPeriods = cpuStat.get("nr_periods") ?? 0;
33410
+ const io = parseIoStat(ioStatRaw);
33411
+ const extra = {};
33412
+ extra.cpuPressurePct = parsePressureSome(cpuPsi);
33413
+ extra.memPressurePct = parsePressureSome(memPsi);
33414
+ extra.ioPressurePct = parsePressureSome(ioPsi);
33415
+ const memStat = parseKv(memStatRaw);
33416
+ if (memStat.has("anon")) extra.memAnonMb = Math.round(memStat.get("anon") / MB);
33417
+ if (memStat.has("file")) extra.memCacheMb = Math.round(memStat.get("file") / MB);
33418
+ if (swapCurRaw !== null) extra.swapUsedMb = Math.round(Number(swapCurRaw) / MB);
33419
+ if (swapMaxRaw !== null) {
33420
+ const max = parseMaybeMax(swapMaxRaw);
33421
+ extra.swapTotalMb = max === null ? null : Math.round(max / MB);
33422
+ }
33423
+ const memEvents = parseKv(memEventsRaw);
33424
+ if (memEvents.has("oom_kill")) extra.oomKills = memEvents.get("oom_kill");
33425
+ if (pidsCurRaw !== null) {
33426
+ const n = Number(pidsCurRaw);
33427
+ if (Number.isFinite(n)) extra.pidsCurrent = n;
33428
+ }
33429
+ if (pidsMaxRaw !== null) extra.pidsMax = parseMaybeMax(pidsMaxRaw);
33430
+ let cpuPct = 0;
33431
+ if (Number.isFinite(usageUsec)) {
33432
+ if (cgPrev) {
33433
+ const dWallUsec = (now - cgPrev.wallMs) * 1e3;
33434
+ if (dWallUsec > 0) cpuPct = clampPct((usageUsec - cgPrev.usageUsec) / (dWallUsec * cpuCount) * 100);
33435
+ const dPeriods = nrPeriods - cgPrev.nrPeriods;
33436
+ if (dPeriods > 0) extra.cpuThrottledPct = clampPct((nrThrottled - cgPrev.nrThrottled) / dPeriods * 100);
33437
+ if (io) {
33438
+ const dSec = (now - cgPrev.wallMs) / 1e3;
33439
+ if (dSec > 0) {
33440
+ extra.diskReadMbps = round1((io.rBytes - cgPrev.ioR) / MB / dSec);
33441
+ extra.diskWriteMbps = round1((io.wBytes - cgPrev.ioW) / MB / dSec);
33442
+ }
33443
+ }
33444
+ }
33445
+ cgPrev = {
33446
+ usageUsec,
33447
+ nrThrottled,
33448
+ nrPeriods,
33449
+ ioR: io?.rBytes ?? 0,
33450
+ ioW: io?.wBytes ?? 0,
33451
+ wallMs: now
33452
+ };
33453
+ }
33454
+ return { cpuPct, extra };
33455
+ }
33370
33456
  return {
33371
33457
  async sample() {
33372
- const at = (/* @__PURE__ */ new Date()).toISOString();
33458
+ const now = Date.now();
33459
+ const at = new Date(now).toISOString();
33373
33460
  const { usedGb: diskUsedGb, totalGb: diskTotalGb } = await disk();
33374
33461
  const base = { platform: platform(), arch: arch(), diskUsedGb, diskTotalGb, at };
33375
33462
  if (cgroupV2) {
@@ -33378,14 +33465,15 @@ function createWorkerMetricsSampler(diskPath) {
33378
33465
  const memTotalBytes = memMaxRaw && memMaxRaw !== "max" ? Number(memMaxRaw) : totalmem();
33379
33466
  const memUsedBytes = memCurRaw ? Number(memCurRaw) : totalmem() - freemem();
33380
33467
  const cpuCount = await cgroupCpuCount();
33381
- const cpuPct = await cgroupCpuPct(cpuCount);
33468
+ const { cpuPct, extra } = await cgroupExtended(cpuCount, now);
33382
33469
  return {
33383
33470
  ...base,
33384
33471
  source: "cgroup",
33385
33472
  cpuCount,
33386
33473
  cpuPct,
33387
33474
  memUsedMb: Math.round(memUsedBytes / MB),
33388
- memTotalMb: Math.round(memTotalBytes / MB)
33475
+ memTotalMb: Math.round(memTotalBytes / MB),
33476
+ ...extra
33389
33477
  };
33390
33478
  }
33391
33479
  const total = totalmem();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "private": false,
5
5
  "description": "4PM CLI — drives Claude/Codex CLIs on AI worker machines (Node + TypeScript)",
6
6
  "license": "LicenseRef-4PM-Source-Available",