@4pm/cli 1.3.2 → 1.4.1

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