@4pm/cli 1.3.2 → 1.4.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 (2) hide show
  1. package/dist/index.js +253 -61
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -32954,7 +32954,11 @@ var issueProvisioningTokenRequestSchema = external_exports.object({
32954
32954
  ttlSec: external_exports.number().int().min(0).max(2592e3).optional()
32955
32955
  });
32956
32956
  var listWorkersQuerySchema = baseRequestSchema.extend({
32957
- status: external_exports.enum(["online", "offline"]).optional()
32957
+ status: external_exports.enum(["online", "offline"]).optional(),
32958
+ /** Only workers whose live CPU/memory/disk usage % ≥ the given threshold (0–100). */
32959
+ minCpuPct: external_exports.coerce.number().min(0).max(100).optional(),
32960
+ minMemPct: external_exports.coerce.number().min(0).max(100).optional(),
32961
+ minDiskPct: external_exports.coerce.number().min(0).max(100).optional()
32958
32962
  });
32959
32963
  var updateWorkerRequestSchema = external_exports.object({
32960
32964
  name: external_exports.string().min(1).max(100).optional()
@@ -33290,6 +33294,118 @@ var activityHeartbeatSchema = external_exports.object({
33290
33294
  projectId: external_exports.string().uuid().optional()
33291
33295
  });
33292
33296
 
33297
+ // src/core/worker-metrics.ts
33298
+ import { statfs, readFile } from "fs/promises";
33299
+ import { existsSync as existsSync4 } from "fs";
33300
+ import { totalmem, freemem, cpus, platform, arch } from "os";
33301
+ var CGROUP = "/sys/fs/cgroup";
33302
+ var MB = 1024 * 1024;
33303
+ var GB = 1024 * 1024 * 1024;
33304
+ async function readCgroup(file) {
33305
+ try {
33306
+ return (await readFile(`${CGROUP}/${file}`, "utf8")).trim();
33307
+ } catch {
33308
+ return null;
33309
+ }
33310
+ }
33311
+ function createWorkerMetricsSampler(diskPath) {
33312
+ const cgroupV2 = existsSync4(`${CGROUP}/cpu.stat`) && existsSync4(`${CGROUP}/memory.current`);
33313
+ let prev = null;
33314
+ async function cgroupCpuCount() {
33315
+ const raw = await readCgroup("cpu.max");
33316
+ if (raw) {
33317
+ const [quota, period] = raw.split(/\s+/);
33318
+ if (quota && quota !== "max" && period) {
33319
+ const n = Number(quota) / Number(period);
33320
+ if (n > 0) return Math.max(1, Math.round(n * 10) / 10);
33321
+ }
33322
+ }
33323
+ return cpus().length;
33324
+ }
33325
+ async function cgroupCpuPct(cpuCount) {
33326
+ const stat2 = await readCgroup("cpu.stat");
33327
+ const m = stat2?.match(/usage_usec\s+(\d+)/);
33328
+ const usageUsec = m ? Number(m[1]) : NaN;
33329
+ const now = Date.now();
33330
+ if (!Number.isFinite(usageUsec)) return 0;
33331
+ if (!prev) {
33332
+ prev = { busy: usageUsec, ref: now };
33333
+ return 0;
33334
+ }
33335
+ const dUsage = usageUsec - prev.busy;
33336
+ const dWall = (now - prev.ref) * 1e3;
33337
+ prev = { busy: usageUsec, ref: now };
33338
+ if (dWall <= 0) return 0;
33339
+ return clampPct(dUsage / (dWall * cpuCount) * 100);
33340
+ }
33341
+ function osCpuPct() {
33342
+ let busy = 0;
33343
+ let total = 0;
33344
+ for (const c of cpus()) {
33345
+ const t = c.times;
33346
+ busy += t.user + t.nice + t.sys + t.irq;
33347
+ total += t.user + t.nice + t.sys + t.irq + t.idle;
33348
+ }
33349
+ if (!prev) {
33350
+ prev = { busy, ref: total };
33351
+ return 0;
33352
+ }
33353
+ const dBusy = busy - prev.busy;
33354
+ const dTotal = total - prev.ref;
33355
+ prev = { busy, ref: total };
33356
+ if (dTotal <= 0) return 0;
33357
+ return clampPct(dBusy / dTotal * 100);
33358
+ }
33359
+ async function disk() {
33360
+ try {
33361
+ const fs2 = await statfs(diskPath);
33362
+ const total = fs2.blocks * fs2.bsize;
33363
+ const free = fs2.bavail * fs2.bsize;
33364
+ return { usedGb: round1((total - free) / GB), totalGb: round1(total / GB) };
33365
+ } catch {
33366
+ return { usedGb: 0, totalGb: 0 };
33367
+ }
33368
+ }
33369
+ return {
33370
+ async sample() {
33371
+ const at = (/* @__PURE__ */ new Date()).toISOString();
33372
+ const { usedGb: diskUsedGb, totalGb: diskTotalGb } = await disk();
33373
+ const base = { platform: platform(), arch: arch(), diskUsedGb, diskTotalGb, at };
33374
+ if (cgroupV2) {
33375
+ const memMaxRaw = await readCgroup("memory.max");
33376
+ const memCurRaw = await readCgroup("memory.current");
33377
+ const memTotalBytes = memMaxRaw && memMaxRaw !== "max" ? Number(memMaxRaw) : totalmem();
33378
+ const memUsedBytes = memCurRaw ? Number(memCurRaw) : totalmem() - freemem();
33379
+ const cpuCount = await cgroupCpuCount();
33380
+ const cpuPct = await cgroupCpuPct(cpuCount);
33381
+ return {
33382
+ ...base,
33383
+ source: "cgroup",
33384
+ cpuCount,
33385
+ cpuPct,
33386
+ memUsedMb: Math.round(memUsedBytes / MB),
33387
+ memTotalMb: Math.round(memTotalBytes / MB)
33388
+ };
33389
+ }
33390
+ const total = totalmem();
33391
+ return {
33392
+ ...base,
33393
+ source: "os",
33394
+ cpuCount: cpus().length,
33395
+ cpuPct: osCpuPct(),
33396
+ memUsedMb: Math.round((total - freemem()) / MB),
33397
+ memTotalMb: Math.round(total / MB)
33398
+ };
33399
+ }
33400
+ };
33401
+ }
33402
+ function clampPct(n) {
33403
+ return round1(Math.min(100, Math.max(0, n)));
33404
+ }
33405
+ function round1(n) {
33406
+ return Math.round(n * 10) / 10;
33407
+ }
33408
+
33293
33409
  // vendor/@4pm/ws/batcher.ts
33294
33410
  var OutputBatcher = class {
33295
33411
  opts;
@@ -33432,6 +33548,12 @@ var WsChannels = {
33432
33548
  // whether a web viewer is attached) so an unwatched cli syncs nothing.
33433
33549
  CONSOLE_SYNC: "console.sync",
33434
33550
  CONSOLE_WATCH: "console.watch",
33551
+ // worker resource live-metrics (ADR-0214): the cli streams live CPU/RAM/disk (cgroup-accurate)
33552
+ // only while `metrics.watch` (server → cli: whether a web viewer has the Workers tab open) is on,
33553
+ // so an unwatched cli samples nothing. Buffered ephemerally in Redis (never the DB), separate from
33554
+ // the low-frequency, DB-authoritative `machine.status`.
33555
+ METRICS_WATCH: "metrics.watch",
33556
+ MACHINE_METRICS: "machine.metrics",
33435
33557
  // AI spec-assist (suggest/review/compose) moved off dedicated ai.* channels to
33436
33558
  // command.dispatch({ ai:true }) → the profile-failover AI path (ADR-0100).
33437
33559
  // quota & usage (ADR-0020)
@@ -33449,6 +33571,10 @@ var WsChannels = {
33449
33571
  // server → cli (reply): read a finished command's captured output on demand (ADR-0115) —
33450
33572
  // cli stores command-output/{commandId}.log; returns null when pruned/unavailable.
33451
33573
  COMMAND_OUTPUT_READ: "command.output-read",
33574
+ // server → cli (reply): flush pending data before a rented machine is scrubbed (ADR-0210) —
33575
+ // the worker uploads its pending log tail (+ any queued history) then acks, so the release
33576
+ // sweep can finalise without waiting the full 5-minute timeout when the worker is online.
33577
+ RENTAL_FLUSH: "rental.flush",
33452
33578
  // outbound review (ADR-0082): machine→server request, server→outbound evaluate, result back
33453
33579
  REVIEW_REQUEST: "review.request",
33454
33580
  REVIEW_EVALUATE: "review.evaluate",
@@ -33514,7 +33640,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
33514
33640
  }
33515
33641
 
33516
33642
  // src/core/ws-client.ts
33517
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, rmSync as rmSync5 } from "fs";
33643
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, rmSync as rmSync5 } from "fs";
33518
33644
  import { randomUUID as randomCommandId } from "crypto";
33519
33645
 
33520
33646
  // src/core/update-scheduler.ts
@@ -33920,14 +34046,14 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
33920
34046
  }
33921
34047
 
33922
34048
  // src/core/ai-profile-state.ts
33923
- import { existsSync as existsSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
34049
+ import { existsSync as existsSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
33924
34050
  import { join as join7 } from "path";
33925
34051
  function statePath(profileDir2) {
33926
34052
  return join7(profileDir2, "ai-profiles.json");
33927
34053
  }
33928
34054
  function readState(profileDir2) {
33929
34055
  const path = statePath(profileDir2);
33930
- if (!existsSync4(path)) return {};
34056
+ if (!existsSync5(path)) return {};
33931
34057
  try {
33932
34058
  return JSON.parse(readFileSync7(path, "utf8"));
33933
34059
  } catch {
@@ -34393,7 +34519,7 @@ async function evaluateReview(payload) {
34393
34519
  }
34394
34520
 
34395
34521
  // src/core/command-history.ts
34396
- import { existsSync as existsSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
34522
+ import { existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
34397
34523
  import { join as join10 } from "path";
34398
34524
  var MAX_ENTRIES = 1e3;
34399
34525
  var filePath = null;
@@ -34457,7 +34583,7 @@ async function uploadHistory(cfg) {
34457
34583
  }
34458
34584
  function initCommandHistory(profileDir2, profileName, uploadMinutes = 10) {
34459
34585
  filePath = join10(profileDir2, "command-history.json");
34460
- if (existsSync5(filePath)) {
34586
+ if (existsSync6(filePath)) {
34461
34587
  try {
34462
34588
  entries = JSON.parse(readFileSync10(filePath, "utf8"));
34463
34589
  } catch {
@@ -34515,7 +34641,7 @@ function finishCommand(commandId, exitCode, usage) {
34515
34641
  // src/core/command-output-store.ts
34516
34642
  import {
34517
34643
  appendFileSync,
34518
- existsSync as existsSync6,
34644
+ existsSync as existsSync7,
34519
34645
  mkdirSync as mkdirSync3,
34520
34646
  readFileSync as readFileSync11,
34521
34647
  readdirSync as readdirSync3,
@@ -34543,7 +34669,7 @@ function appendCommandOutput(commandId, text) {
34543
34669
  function readCommandOutput(commandId) {
34544
34670
  if (!dir) return null;
34545
34671
  const path = join11(dir, `${commandId}.log`);
34546
- if (!existsSync6(path)) return null;
34672
+ if (!existsSync7(path)) return null;
34547
34673
  try {
34548
34674
  return readFileSync11(path, "utf8");
34549
34675
  } catch {
@@ -34594,13 +34720,13 @@ async function listDir(path, root) {
34594
34720
  }
34595
34721
 
34596
34722
  // src/core/fs-read.ts
34597
- import { readFile } from "fs/promises";
34723
+ import { readFile as readFile2 } from "fs/promises";
34598
34724
  import { resolve as resolve2 } from "path";
34599
34725
  var MAX_BYTES = 256 * 1024;
34600
34726
  async function readWorkerFile(path) {
34601
34727
  const target = resolve2(path);
34602
34728
  try {
34603
- const buf = await readFile(target);
34729
+ const buf = await readFile2(target);
34604
34730
  const truncated = buf.byteLength > MAX_BYTES;
34605
34731
  const content = buf.subarray(0, MAX_BYTES).toString("utf8");
34606
34732
  return { path: target, content, truncated };
@@ -34641,7 +34767,7 @@ async function writeWorkerFile(root, relPath, content) {
34641
34767
  import { spawn as spawn4 } from "child_process";
34642
34768
  import { execFile as execFile2 } from "child_process";
34643
34769
  import { createHash as createHash3 } from "crypto";
34644
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
34770
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
34645
34771
  import { join as join12 } from "path";
34646
34772
  import { promisify as promisify2 } from "util";
34647
34773
  var execFileAsync = promisify2(execFile2);
@@ -34662,7 +34788,7 @@ function authedUrl(url, token) {
34662
34788
  }
34663
34789
  async function ensureRepo(profileDir2, repo) {
34664
34790
  const dir2 = repoDir(profileDir2, repo.url, repo.branch);
34665
- if (existsSync7(join12(dir2, ".git"))) return dir2;
34791
+ if (existsSync8(join12(dir2, ".git"))) return dir2;
34666
34792
  const url = authedUrl(repo.url, repo.token);
34667
34793
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
34668
34794
  mkdirSync4(kbRoot(profileDir2), { recursive: true });
@@ -34673,11 +34799,11 @@ async function ensureRepo(profileDir2, repo) {
34673
34799
  }
34674
34800
  async function refreshSupportKb(profileDir2) {
34675
34801
  const root = kbRoot(profileDir2);
34676
- if (!existsSync7(root)) return;
34802
+ if (!existsSync8(root)) return;
34677
34803
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
34678
34804
  for (const entry of readdirSync4(root)) {
34679
34805
  const dir2 = join12(root, entry);
34680
- if (!existsSync7(join12(dir2, ".git"))) continue;
34806
+ if (!existsSync8(join12(dir2, ".git"))) continue;
34681
34807
  try {
34682
34808
  await execFileAsync("git", ["-C", dir2, "pull", "--ff-only"], { env: gitEnv });
34683
34809
  } catch (err) {
@@ -34853,7 +34979,7 @@ async function runKnowledgeCompose(req, ai, physicRoot) {
34853
34979
  }
34854
34980
 
34855
34981
  // src/core/autonomous.ts
34856
- import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
34982
+ import { readFile as readFile3, writeFile as writeFile2, chmod } from "fs/promises";
34857
34983
  import { readFileSync as readFileSync13 } from "fs";
34858
34984
  import { execFile as execFile3, spawn as spawn6 } from "child_process";
34859
34985
  import { join as join13 } from "path";
@@ -34874,7 +35000,7 @@ var BOOK_FILES = {
34874
35000
  var DEFAULT_CRON = "*/10 * * * *";
34875
35001
  async function readText(path, fallback = "") {
34876
35002
  try {
34877
- return await readFile2(path, "utf8");
35003
+ return await readFile3(path, "utf8");
34878
35004
  } catch {
34879
35005
  return fallback;
34880
35006
  }
@@ -35011,7 +35137,7 @@ ${req.content.trim()}
35011
35137
  }
35012
35138
 
35013
35139
  // src/core/agents.ts
35014
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3, readdir as readdir3, rm } from "fs/promises";
35140
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3, readdir as readdir3, rm } from "fs/promises";
35015
35141
  import { join as join14 } from "path";
35016
35142
  var AGENTS_REL = ".claude/agents";
35017
35143
  var SKILLS_REL = ".claude/skills";
@@ -35021,7 +35147,7 @@ function safeName(name) {
35021
35147
  }
35022
35148
  async function readText2(path, fallback = "") {
35023
35149
  try {
35024
- return await readFile3(path, "utf8");
35150
+ return await readFile4(path, "utf8");
35025
35151
  } catch {
35026
35152
  return fallback;
35027
35153
  }
@@ -35090,7 +35216,7 @@ async function writeAgent(root, req) {
35090
35216
 
35091
35217
  // src/core/packages.ts
35092
35218
  import { createHash as createHash4 } from "crypto";
35093
- import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir4, readdir as readdir4, rm as rm2, stat } from "fs/promises";
35219
+ import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir4, readdir as readdir4, rm as rm2, stat } from "fs/promises";
35094
35220
  import { dirname as dirname4, join as join15, relative } from "path";
35095
35221
  var AGENTS_REL2 = ".claude/agents";
35096
35222
  var SKILLS_REL2 = ".claude/skills";
@@ -35100,7 +35226,7 @@ function safeName2(name) {
35100
35226
  }
35101
35227
  async function readMaybe(path) {
35102
35228
  try {
35103
- return await readFile4(path);
35229
+ return await readFile5(path);
35104
35230
  } catch {
35105
35231
  return null;
35106
35232
  }
@@ -35324,21 +35450,21 @@ function applyGitAuth(method) {
35324
35450
  }
35325
35451
 
35326
35452
  // src/core/secrets.ts
35327
- import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
35453
+ import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
35328
35454
  import { join as join18 } from "path";
35329
35455
  var SECURITY_REL = "AI_SECURITY.md";
35330
35456
  var PLACEHOLDER_REL = "AI_PLACEHOLDER.md";
35331
35457
  var SECRETS_REL = "project.secrets.json";
35332
35458
  async function readText3(path, fallback = "") {
35333
35459
  try {
35334
- return await readFile5(path, "utf8");
35460
+ return await readFile6(path, "utf8");
35335
35461
  } catch {
35336
35462
  return fallback;
35337
35463
  }
35338
35464
  }
35339
35465
  async function readSecretsObject(root) {
35340
35466
  try {
35341
- const v = JSON.parse(await readFile5(join18(root, SECRETS_REL), "utf8"));
35467
+ const v = JSON.parse(await readFile6(join18(root, SECRETS_REL), "utf8"));
35342
35468
  return v && typeof v === "object" ? v : {};
35343
35469
  } catch {
35344
35470
  return {};
@@ -35387,7 +35513,7 @@ async function writeSecrets(root, req) {
35387
35513
  }
35388
35514
 
35389
35515
  // src/core/agent-tools.ts
35390
- import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
35516
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
35391
35517
  import { dirname as dirname5, join as join19 } from "path";
35392
35518
  var SHARED_REL = ".claude/settings.json";
35393
35519
  var LOCAL_REL = ".claude/settings.local.json";
@@ -35398,7 +35524,7 @@ function fileFor(root, scope) {
35398
35524
  }
35399
35525
  async function readSettingsObject(path) {
35400
35526
  try {
35401
- const v = JSON.parse(await readFile6(path, "utf8"));
35527
+ const v = JSON.parse(await readFile7(path, "utf8"));
35402
35528
  return v && typeof v === "object" ? v : {};
35403
35529
  } catch {
35404
35530
  return {};
@@ -35430,7 +35556,7 @@ async function writeAgentTools(root, scope, permissions) {
35430
35556
  }
35431
35557
 
35432
35558
  // src/core/graph.ts
35433
- import { readFile as readFile7, readdir as readdir5 } from "fs/promises";
35559
+ import { readFile as readFile8, readdir as readdir5 } from "fs/promises";
35434
35560
  import { join as join20, relative as relative2, dirname as dirname6, resolve as resolve4, extname, basename as basename2 } from "path";
35435
35561
  var MAX_FILES = 2e3;
35436
35562
  var MAX_NODES = 3e3;
@@ -35479,7 +35605,7 @@ async function buildDocs(root) {
35479
35605
  const rel = relative2(root, f).split("\\").join("/");
35480
35606
  let text = "";
35481
35607
  try {
35482
- text = await readFile7(f, "utf8");
35608
+ text = await readFile8(f, "utf8");
35483
35609
  } catch {
35484
35610
  continue;
35485
35611
  }
@@ -35518,7 +35644,7 @@ async function buildCode(root) {
35518
35644
  const rel = relative2(root, f).split("\\").join("/");
35519
35645
  let text = "";
35520
35646
  try {
35521
- text = await readFile7(f, "utf8");
35647
+ text = await readFile8(f, "utf8");
35522
35648
  } catch {
35523
35649
  continue;
35524
35650
  }
@@ -35570,10 +35696,10 @@ async function buildGraph(root, mode) {
35570
35696
  }
35571
35697
 
35572
35698
  // src/core/rag.ts
35573
- import { readFile as readFile8, writeFile as writeFile7, statfs, mkdir as mkdir6 } from "fs/promises";
35574
- import { existsSync as existsSync8 } from "fs";
35699
+ import { readFile as readFile9, writeFile as writeFile7, statfs as statfs2, mkdir as mkdir6 } from "fs/promises";
35700
+ import { existsSync as existsSync9 } from "fs";
35575
35701
  import { execFile as execFile4, spawn as spawn7 } from "child_process";
35576
- import { totalmem, cpus } from "os";
35702
+ import { totalmem as totalmem2, cpus as cpus2 } from "os";
35577
35703
  import { join as join21 } from "path";
35578
35704
  import { promisify as promisify4 } from "util";
35579
35705
  var run3 = promisify4(execFile4);
@@ -35595,7 +35721,7 @@ async function detectPython() {
35595
35721
  }
35596
35722
  async function installLog(root) {
35597
35723
  try {
35598
- const text = await readFile8(join21(root, RAG_REL, "install.log"), "utf8");
35724
+ const text = await readFile9(join21(root, RAG_REL, "install.log"), "utf8");
35599
35725
  return text.split("\n").filter((l) => l.length > 0).slice(-200);
35600
35726
  } catch {
35601
35727
  return [];
@@ -35643,28 +35769,28 @@ async function ragStatus(root) {
35643
35769
  ]) : [false, false, false];
35644
35770
  let diskFreeGB = 0;
35645
35771
  try {
35646
- const s = await statfs(root);
35772
+ const s = await statfs2(root);
35647
35773
  diskFreeGB = Math.round(s.bavail * s.bsize / 1e9 * 10) / 10;
35648
35774
  } catch {
35649
35775
  diskFreeGB = 0;
35650
35776
  }
35651
35777
  const gpu = (await tryRun("nvidia-smi", ["-L"])).ok;
35652
35778
  const machine = {
35653
- ramGB: Math.round(totalmem() / 1e9 * 10) / 10,
35654
- cpus: cpus().length,
35779
+ ramGB: Math.round(totalmem2() / 1e9 * 10) / 10,
35780
+ cpus: cpus2().length,
35655
35781
  diskFreeGB,
35656
35782
  gpu
35657
35783
  };
35658
35784
  let modelName = "";
35659
35785
  try {
35660
- modelName = (await readFile8(join21(root, RAG_REL, ".installed"), "utf8")).trim();
35786
+ modelName = (await readFile9(join21(root, RAG_REL, ".installed"), "utf8")).trim();
35661
35787
  } catch {
35662
35788
  modelName = "";
35663
35789
  }
35664
35790
  const modelPresent = modelName.length > 0;
35665
35791
  return {
35666
35792
  installed: fastembed && sqliteVec && modelPresent,
35667
- installing: existsSync8(join21(root, RAG_REL, ".installing")),
35793
+ installing: existsSync9(join21(root, RAG_REL, ".installing")),
35668
35794
  python: { found: py.found, version: py.version },
35669
35795
  deps: { pip, fastembed, sqliteVec },
35670
35796
  model: { present: modelPresent, name: modelName },
@@ -35675,9 +35801,9 @@ async function ragStatus(root) {
35675
35801
  };
35676
35802
  }
35677
35803
  async function readIndexMeta(root) {
35678
- const indexing = existsSync8(join21(root, RAG_REL, ".indexing"));
35804
+ const indexing = existsSync9(join21(root, RAG_REL, ".indexing"));
35679
35805
  try {
35680
- const m = JSON.parse(await readFile8(join21(root, RAG_REL, ".index.json"), "utf8"));
35806
+ const m = JSON.parse(await readFile9(join21(root, RAG_REL, ".index.json"), "utf8"));
35681
35807
  return {
35682
35808
  present: true,
35683
35809
  indexing,
@@ -35750,8 +35876,8 @@ async function ensureEngine(root) {
35750
35876
  async function ragReindex(root) {
35751
35877
  const py = await detectPython();
35752
35878
  if (!py.found) return { ok: false, started: false, error: "python3 not found" };
35753
- if (!existsSync8(join21(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
35754
- if (existsSync8(join21(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
35879
+ if (!existsSync9(join21(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
35880
+ if (existsSync9(join21(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
35755
35881
  await ensureEngine(root);
35756
35882
  const script = `
35757
35883
  set +e
@@ -35771,7 +35897,7 @@ rm -f "${RAG_REL}/.indexing"
35771
35897
  async function ragQuery(root, queryText, k = 8) {
35772
35898
  const py = await detectPython();
35773
35899
  if (!py.found) return { results: [], error: "python3 not found" };
35774
- if (!existsSync8(join21(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
35900
+ if (!existsSync9(join21(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
35775
35901
  await ensureEngine(root);
35776
35902
  try {
35777
35903
  const { stdout } = await run3(
@@ -35788,7 +35914,7 @@ async function ragQuery(root, queryText, k = 8) {
35788
35914
  async function ragInstall(root, model) {
35789
35915
  const py = await detectPython();
35790
35916
  if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
35791
- if (existsSync8(join21(root, RAG_REL, ".installing"))) return { ok: true, started: true };
35917
+ if (existsSync9(join21(root, RAG_REL, ".installing"))) return { ok: true, started: true };
35792
35918
  try {
35793
35919
  await mkdir6(join21(root, RAG_REL), { recursive: true });
35794
35920
  } catch {
@@ -35817,7 +35943,7 @@ rm -f "${RAG_REL}/.installing"
35817
35943
  }
35818
35944
 
35819
35945
  // src/core/git-diff.ts
35820
- import { readFile as readFile9 } from "fs/promises";
35946
+ import { readFile as readFile10 } from "fs/promises";
35821
35947
  import { execFile as execFile5 } from "child_process";
35822
35948
  import { basename as basename3, dirname as dirname7, resolve as resolve5 } from "path";
35823
35949
  import { promisify as promisify5 } from "util";
@@ -35828,7 +35954,7 @@ async function gitDiff(path) {
35828
35954
  const base = basename3(target);
35829
35955
  let newContent = "";
35830
35956
  try {
35831
- newContent = await readFile9(target, "utf8");
35957
+ newContent = await readFile10(target, "utf8");
35832
35958
  } catch {
35833
35959
  }
35834
35960
  let oldContent = "";
@@ -35877,7 +36003,7 @@ ${stderr}`;
35877
36003
 
35878
36004
  // src/core/git-ssh-key.ts
35879
36005
  import { execFile as execFile7 } from "child_process";
35880
- import { readFile as readFile10, rm as rm3, mkdir as mkdir7 } from "fs/promises";
36006
+ import { readFile as readFile11, rm as rm3, mkdir as mkdir7 } from "fs/promises";
35881
36007
  import { join as join22 } from "path";
35882
36008
  import { promisify as promisify7 } from "util";
35883
36009
  var run6 = promisify7(execFile7);
@@ -35888,7 +36014,7 @@ function keyPaths(profileDir2) {
35888
36014
  }
35889
36015
  async function readPublicKey(pub) {
35890
36016
  try {
35891
- return (await readFile10(pub, "utf8")).trim();
36017
+ return (await readFile11(pub, "utf8")).trim();
35892
36018
  } catch {
35893
36019
  return null;
35894
36020
  }
@@ -36197,12 +36323,12 @@ async function addProject(payload, profileDir2, onProgress) {
36197
36323
  }
36198
36324
 
36199
36325
  // src/core/config-sync.ts
36200
- import { existsSync as existsSync9, readFileSync as readFileSync14 } from "fs";
36326
+ import { existsSync as existsSync10, readFileSync as readFileSync14 } from "fs";
36201
36327
  import { join as join24 } from "path";
36202
36328
  var SERVER_MANAGED_KEYS = ["physicPath", "sessionSwitchPct", "perPromptTokenLimit"];
36203
36329
  function readConfigText(profileDir2) {
36204
36330
  const path = join24(profileDir2, "config.json");
36205
- if (existsSync9(path)) {
36331
+ if (existsSync10(path)) {
36206
36332
  try {
36207
36333
  return readFileSync14(path, "utf8");
36208
36334
  } catch {
@@ -36363,6 +36489,8 @@ var CLOSE_DRAINING = 4001;
36363
36489
  var OFFLINE_BUFFER_MAX_BYTES = 512 * 1024;
36364
36490
  var CONSOLE_SNAPSHOT_INTERVAL_MS = 3e4;
36365
36491
  var CONSOLE_WATCH_LEASE_MS = 45e3;
36492
+ var METRICS_SAMPLE_INTERVAL_MS = 5e3;
36493
+ var METRICS_WATCH_LEASE_MS = 45e3;
36366
36494
  function toDtoEntry(e) {
36367
36495
  return { id: e.id, source: e.source, kind: e.kind, text: e.text, level: e.level, resultKind: e.resultKind };
36368
36496
  }
@@ -36455,6 +36583,16 @@ var WsClient = class {
36455
36583
  consoleSnapshotTimer = null;
36456
36584
  /** Lease expiry: stop syncing if the server does not renew `console.watch` in time. */
36457
36585
  consoleWatchExpiry = null;
36586
+ /**
36587
+ * Worker-metrics state (ADR-0214). `metricsWatching` is set by `metrics.watch` (a viewer has the
36588
+ * Workers tab open); only then does the cli sample + emit `machine.metrics`. The sampler holds the
36589
+ * CPU-delta baseline; both timers are lease-driven like console-sync and reset on reconnect.
36590
+ */
36591
+ metricsWatching = false;
36592
+ metricsTimer = null;
36593
+ metricsWatchExpiry = null;
36594
+ /** Lazily created on the first `metrics.watch` (keeps the CPU-delta baseline across samples). */
36595
+ metricsSampler = null;
36458
36596
  /** cli → server requests awaiting a reply (quota.check — ADR-0020). */
36459
36597
  pending = /* @__PURE__ */ new Map();
36460
36598
  /** Daily scheduled cli auto-update (ADR-0074); policy fed from each ws_token. */
@@ -36690,6 +36828,7 @@ var WsClient = class {
36690
36828
  }
36691
36829
  logger.info("ws.connected", { version: CLI_VERSION });
36692
36830
  this.stopConsoleSync();
36831
+ this.stopMetricsSync();
36693
36832
  this.bus.setStatus("connected");
36694
36833
  this.finishReconnect();
36695
36834
  this.sendMachineStatus();
@@ -37152,6 +37291,11 @@ var WsClient = class {
37152
37291
  this.send(WsChannels.COMMAND_OUTPUT_READ, { output }, message.id);
37153
37292
  break;
37154
37293
  }
37294
+ case WsChannels.RENTAL_FLUSH: {
37295
+ this.uploadLog();
37296
+ this.send(WsChannels.RENTAL_FLUSH, { ok: true }, message.id);
37297
+ break;
37298
+ }
37155
37299
  case WsChannels.PROJECT_CREATE:
37156
37300
  void scaffoldProject(
37157
37301
  payload,
@@ -37176,6 +37320,9 @@ var WsClient = class {
37176
37320
  case WsChannels.CONSOLE_WATCH:
37177
37321
  this.setConsoleWatching(payload.on);
37178
37322
  break;
37323
+ case WsChannels.METRICS_WATCH:
37324
+ this.setMetricsWatching(payload.on);
37325
+ break;
37179
37326
  default:
37180
37327
  this.bus.log(`Unsupported channel: ${message.channel}`, "warn");
37181
37328
  }
@@ -37605,6 +37752,50 @@ var WsClient = class {
37605
37752
  this.consoleWatchExpiry = null;
37606
37753
  }
37607
37754
  }
37755
+ /**
37756
+ * Handle `metrics.watch` (ADR-0214) — a renewable lease mirroring `console.watch`. `on` renews it:
37757
+ * on the first assertion the cli samples immediately + starts the ~5s sampler; later renewals just
37758
+ * extend the lease. No renewal within `METRICS_WATCH_LEASE_MS` (or an explicit `on:false`) ⇒ stop.
37759
+ */
37760
+ setMetricsWatching(on) {
37761
+ if (!on) {
37762
+ this.stopMetricsSync();
37763
+ return;
37764
+ }
37765
+ if (this.metricsWatchExpiry) clearTimeout(this.metricsWatchExpiry);
37766
+ this.metricsWatchExpiry = setTimeout(() => this.stopMetricsSync(), METRICS_WATCH_LEASE_MS);
37767
+ this.metricsWatchExpiry.unref?.();
37768
+ if (this.metricsWatching) return;
37769
+ this.metricsWatching = true;
37770
+ void this.sendMachineMetrics();
37771
+ this.metricsTimer = setInterval(() => void this.sendMachineMetrics(), METRICS_SAMPLE_INTERVAL_MS);
37772
+ this.metricsTimer.unref?.();
37773
+ }
37774
+ /** Stop sampling worker resources (lease expired / viewer left / reconnect) — clears both timers. */
37775
+ stopMetricsSync() {
37776
+ this.metricsWatching = false;
37777
+ if (this.metricsTimer) {
37778
+ clearInterval(this.metricsTimer);
37779
+ this.metricsTimer = null;
37780
+ }
37781
+ if (this.metricsWatchExpiry) {
37782
+ clearTimeout(this.metricsWatchExpiry);
37783
+ this.metricsWatchExpiry = null;
37784
+ }
37785
+ }
37786
+ /** Sample the worker's live CPU/RAM/disk and push a `machine.metrics` frame (ADR-0214). */
37787
+ async sendMachineMetrics() {
37788
+ if (!this.sessionKey) return;
37789
+ try {
37790
+ if (!this.metricsSampler) this.metricsSampler = createWorkerMetricsSampler(this.context.profileDir);
37791
+ const resources = await this.metricsSampler.sample();
37792
+ this.send(WsChannels.MACHINE_METRICS, {
37793
+ fingerprint: this.context.machine.fingerprint,
37794
+ resources
37795
+ });
37796
+ } catch {
37797
+ }
37798
+ }
37608
37799
  /**
37609
37800
  * Send a cli → server request and await the reply by envelope id (15s timeout).
37610
37801
  * Used for quota.check before spawning an AI cli (ADR-0020).
@@ -37677,7 +37868,7 @@ var WsClient = class {
37677
37868
  /** Create the physic folder at an absolute path if missing (best-effort). */
37678
37869
  ensurePhysicFolderPath(folder) {
37679
37870
  try {
37680
- if (!existsSync10(folder)) {
37871
+ if (!existsSync11(folder)) {
37681
37872
  mkdirSync5(folder, { recursive: true });
37682
37873
  this.bus.log(`Created physic project folder: ${folder}`);
37683
37874
  }
@@ -37706,7 +37897,7 @@ var WsClient = class {
37706
37897
  projects: [],
37707
37898
  version: CLI_VERSION,
37708
37899
  physicPath,
37709
- physicPathExists: physicPath ? existsSync10(physicPath) : void 0,
37900
+ physicPathExists: physicPath ? existsSync11(physicPath) : void 0,
37710
37901
  // Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
37711
37902
  autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false
37712
37903
  });
@@ -37729,6 +37920,7 @@ var WsClient = class {
37729
37920
  this.logUploadTimer = null;
37730
37921
  if (this.kbRefreshTimer) clearInterval(this.kbRefreshTimer);
37731
37922
  this.kbRefreshTimer = null;
37923
+ this.stopMetricsSync();
37732
37924
  }
37733
37925
  /**
37734
37926
  * Poll the Claude subscription usage API and push a `machine.usage` snapshot (ADR-0072).
@@ -38415,12 +38607,12 @@ var isWebWorker = typeof WorkerGlobalScope !== "undefined" && globalThis instanc
38415
38607
  var isDedicatedWorker = typeof DedicatedWorkerGlobalScope !== "undefined" && globalThis instanceof DedicatedWorkerGlobalScope;
38416
38608
  var isSharedWorker = typeof SharedWorkerGlobalScope !== "undefined" && globalThis instanceof SharedWorkerGlobalScope;
38417
38609
  var isServiceWorker = typeof ServiceWorkerGlobalScope !== "undefined" && globalThis instanceof ServiceWorkerGlobalScope;
38418
- var platform = globalThis.navigator?.userAgentData?.platform;
38419
- var isMacOs = platform === "macOS" || globalThis.navigator?.platform === "MacIntel" || globalThis.navigator?.userAgent?.includes(" Mac ") === true || globalThis.process?.platform === "darwin";
38420
- var isWindows = platform === "Windows" || globalThis.navigator?.platform === "Win32" || globalThis.process?.platform === "win32";
38421
- var isLinux = platform === "Linux" || globalThis.navigator?.platform?.startsWith("Linux") === true || globalThis.navigator?.userAgent?.includes(" Linux ") === true || globalThis.process?.platform === "linux";
38422
- var isIos = platform === "iOS" || globalThis.navigator?.platform === "MacIntel" && globalThis.navigator?.maxTouchPoints > 1 || /iPad|iPhone|iPod/.test(globalThis.navigator?.platform);
38423
- var isAndroid = platform === "Android" || globalThis.navigator?.platform === "Android" || globalThis.navigator?.userAgent?.includes(" Android ") === true || globalThis.process?.platform === "android";
38610
+ var platform2 = globalThis.navigator?.userAgentData?.platform;
38611
+ var isMacOs = platform2 === "macOS" || globalThis.navigator?.platform === "MacIntel" || globalThis.navigator?.userAgent?.includes(" Mac ") === true || globalThis.process?.platform === "darwin";
38612
+ var isWindows = platform2 === "Windows" || globalThis.navigator?.platform === "Win32" || globalThis.process?.platform === "win32";
38613
+ var isLinux = platform2 === "Linux" || globalThis.navigator?.platform?.startsWith("Linux") === true || globalThis.navigator?.userAgent?.includes(" Linux ") === true || globalThis.process?.platform === "linux";
38614
+ var isIos = platform2 === "iOS" || globalThis.navigator?.platform === "MacIntel" && globalThis.navigator?.maxTouchPoints > 1 || /iPad|iPhone|iPod/.test(globalThis.navigator?.platform);
38615
+ var isAndroid = platform2 === "Android" || globalThis.navigator?.platform === "Android" || globalThis.navigator?.userAgent?.includes(" Android ") === true || globalThis.process?.platform === "android";
38424
38616
 
38425
38617
  // ../../node_modules/.pnpm/ansi-escapes@7.3.0/node_modules/ansi-escapes/base.js
38426
38618
  var ESC = "\x1B[";
@@ -43874,7 +44066,7 @@ var import_react21 = __toESM(require_react(), 1);
43874
44066
  var import_react26 = __toESM(require_react(), 1);
43875
44067
 
43876
44068
  // src/core/input-history.ts
43877
- import { existsSync as existsSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "fs";
44069
+ import { existsSync as existsSync13, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "fs";
43878
44070
  import { join as join28 } from "path";
43879
44071
  var MAX_HISTORY = 200;
43880
44072
  function historyPath(profileDir2) {
@@ -43882,7 +44074,7 @@ function historyPath(profileDir2) {
43882
44074
  }
43883
44075
  function loadInputHistory(profileDir2) {
43884
44076
  const path = historyPath(profileDir2);
43885
- if (!existsSync12(path)) return [];
44077
+ if (!existsSync13(path)) return [];
43886
44078
  try {
43887
44079
  const data = JSON.parse(readFileSync17(path, "utf8"));
43888
44080
  return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
@@ -44144,7 +44336,7 @@ function TranscriptLine({ line }) {
44144
44336
  var import_react24 = __toESM(require_react(), 1);
44145
44337
 
44146
44338
  // src/ui/slash-commands.ts
44147
- import { existsSync as existsSync13 } from "fs";
44339
+ import { existsSync as existsSync14 } from "fs";
44148
44340
  import { join as join29 } from "path";
44149
44341
  var CONFIG_KEYS = [
44150
44342
  "aiCli",
@@ -44207,7 +44399,7 @@ function runConfig(ctx) {
44207
44399
  ctx.print(`\u2714 ${verb} default config: ${path}`);
44208
44400
  ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
44209
44401
  };
44210
- if (existsSync13(path)) {
44402
+ if (existsSync14(path)) {
44211
44403
  ctx.confirm(
44212
44404
  `config exists: ${path} \u2014 replace it with defaults? (y/N)`,
44213
44405
  () => writeDefaults("replaced with")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
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",