@4pm/cli 1.4.2 → 1.4.3

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 +174 -102
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28550,7 +28550,7 @@ ${INSECURE_URL_WARNING}
28550
28550
 
28551
28551
  // src/commands/start.ts
28552
28552
  import { spawn as spawn9 } from "child_process";
28553
- import { join as join30 } from "path";
28553
+ import { join as join29 } from "path";
28554
28554
 
28555
28555
  // src/core/update.ts
28556
28556
  import { execSync as execSync2 } from "child_process";
@@ -28723,7 +28723,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
28723
28723
  // src/core/ws-client.ts
28724
28724
  init_wrapper();
28725
28725
  import { randomUUID } from "crypto";
28726
- import { basename as basename4, join as join25 } from "path";
28726
+ import { basename as basename4, join as join24 } from "path";
28727
28727
 
28728
28728
  // vendor/@4pm/utils/helpers.ts
28729
28729
  function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
@@ -33087,6 +33087,19 @@ var createProjectRequestSchema = external_exports.object({
33087
33087
  var copyProjectRequestSchema = external_exports.object({
33088
33088
  name: external_exports.string().min(1).max(100)
33089
33089
  });
33090
+ var PROJECT_ALERT_KINDS = ["budgetPercent", "absolute"];
33091
+ var projectAlertRuleSchema = external_exports.object({
33092
+ id: external_exports.string().min(1).max(64),
33093
+ kind: external_exports.enum(PROJECT_ALERT_KINDS),
33094
+ metric: external_exports.enum([UsageMetric.AI_TOKENS, UsageMetric.COMMANDS]),
33095
+ threshold: external_exports.number().int().min(1),
33096
+ email: external_exports.union([external_exports.string().email(), external_exports.literal("")]).optional(),
33097
+ notifyUserId: external_exports.union([external_exports.string().uuid(), external_exports.literal("")]).optional(),
33098
+ enabled: external_exports.boolean().default(true)
33099
+ });
33100
+ var projectAlertsSchema = external_exports.object({
33101
+ rules: external_exports.array(projectAlertRuleSchema).max(20).default([])
33102
+ });
33090
33103
  var DEFAULT_OUTBOUND_RULES = {
33091
33104
  secret: [
33092
33105
  String.raw`/\b(api[_-]?key|secret|password|passwd|access[_-]?token|bearer)\b\s*[:=]\s*\S+/i`,
@@ -33729,7 +33742,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
33729
33742
  }
33730
33743
 
33731
33744
  // src/core/ws-client.ts
33732
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, rmSync as rmSync5 } from "fs";
33745
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, rmSync as rmSync4 } from "fs";
33733
33746
  import { randomUUID as randomCommandId } from "crypto";
33734
33747
 
33735
33748
  // src/core/update-scheduler.ts
@@ -35492,28 +35505,87 @@ async function removePackage(root, slug) {
35492
35505
  }
35493
35506
  }
35494
35507
 
35495
- // src/core/sandbox.ts
35496
- import { rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
35497
- import { join as join16 } from "path";
35498
- var EGRESS_ALLOWLIST_FILE = "sandbox-egress.txt";
35499
- function writeEgressAllowlist(profileDir2, policy) {
35500
- const path = join16(profileDir2, EGRESS_ALLOWLIST_FILE);
35508
+ // src/core/network-probe.ts
35509
+ import { existsSync as existsSync9, readFileSync as readFileSync14 } from "fs";
35510
+ import { connect } from "net";
35511
+ var CANARY_TARGETS = [
35512
+ { host: "1.1.1.1", port: 443 },
35513
+ { host: "8.8.8.8", port: 443 }
35514
+ ];
35515
+ var PROBE_TIMEOUT_MS = 2500;
35516
+ function detectContainerized() {
35501
35517
  try {
35502
- if (policy && policy.network === "allowlist") {
35503
- const body = ["# 4PM egress allowlist (ADR-0192) \u2014 generated from the project sandbox policy"].concat(policy.allowedDomains).join("\n");
35504
- writeFileSync6(path, `${body}
35505
- `, "utf8");
35506
- } else {
35507
- rmSync4(path, { force: true });
35518
+ if (existsSync9("/.dockerenv")) return true;
35519
+ const cgroup = readFileSync14("/proc/1/cgroup", "utf8");
35520
+ return /docker|containerd|kubepods|libpod/.test(cgroup);
35521
+ } catch {
35522
+ return false;
35523
+ }
35524
+ }
35525
+ function canReach(host, port) {
35526
+ return new Promise((resolve8) => {
35527
+ const socket = connect({ host, port });
35528
+ let done = false;
35529
+ const finish = (ok) => {
35530
+ if (done) return;
35531
+ done = true;
35532
+ socket.destroy();
35533
+ resolve8(ok);
35534
+ };
35535
+ socket.setTimeout(PROBE_TIMEOUT_MS);
35536
+ socket.once("connect", () => finish(true));
35537
+ socket.once("timeout", () => finish(false));
35538
+ socket.once("error", () => finish(false));
35539
+ });
35540
+ }
35541
+ async function probeOutbound() {
35542
+ try {
35543
+ const results = await Promise.all(CANARY_TARGETS.map((t) => canReach(t.host, t.port)));
35544
+ return results.some(Boolean) ? "open" : "restricted";
35545
+ } catch {
35546
+ return "restricted";
35547
+ }
35548
+ }
35549
+ function probeInbound() {
35550
+ const isExposed = (path, loopbackHexPrefixes) => {
35551
+ let body;
35552
+ try {
35553
+ body = readFileSync14(path, "utf8");
35554
+ } catch {
35555
+ return false;
35556
+ }
35557
+ for (const line of body.split("\n").slice(1)) {
35558
+ const cols = line.trim().split(/\s+/);
35559
+ const local = cols[1];
35560
+ const st = cols[3];
35561
+ if (!local || st !== "0A") continue;
35562
+ const addrHex = local.split(":")[0]?.toUpperCase() ?? "";
35563
+ if (loopbackHexPrefixes.some((p) => addrHex === p)) continue;
35564
+ return true;
35508
35565
  }
35566
+ return false;
35567
+ };
35568
+ try {
35569
+ const v4 = isExposed("/proc/net/tcp", ["0100007F"]);
35570
+ const v6 = isExposed("/proc/net/tcp6", ["00000000000000000000000001000000"]);
35571
+ return v4 || v6 ? "exposed" : "isolated";
35509
35572
  } catch {
35573
+ return "isolated";
35510
35574
  }
35511
35575
  }
35576
+ async function probeNetwork() {
35577
+ return {
35578
+ outbound: await probeOutbound(),
35579
+ inbound: probeInbound(),
35580
+ containerized: detectContainerized(),
35581
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
35582
+ };
35583
+ }
35512
35584
 
35513
35585
  // src/core/git-auth.ts
35514
- import { writeFileSync as writeFileSync7 } from "fs";
35586
+ import { writeFileSync as writeFileSync6 } from "fs";
35515
35587
  import { homedir as homedir4 } from "os";
35516
- import { join as join17 } from "path";
35588
+ import { join as join16 } from "path";
35517
35589
  function tokenTarget(method) {
35518
35590
  if (method === "gitlab-group-token") {
35519
35591
  return { host: process.env.FOURPM_GIT_HOST || "gitlab.com", user: "oauth2" };
@@ -35531,7 +35603,7 @@ function applyGitAuth(method) {
35531
35603
  try {
35532
35604
  const line = `https://${target.user}:${token}@${target.host}
35533
35605
  `;
35534
- writeFileSync7(join17(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
35606
+ writeFileSync6(join16(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
35535
35607
  return true;
35536
35608
  } catch {
35537
35609
  return false;
@@ -35540,7 +35612,7 @@ function applyGitAuth(method) {
35540
35612
 
35541
35613
  // src/core/secrets.ts
35542
35614
  import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
35543
- import { join as join18 } from "path";
35615
+ import { join as join17 } from "path";
35544
35616
  var SECURITY_REL = "AI_SECURITY.md";
35545
35617
  var PLACEHOLDER_REL = "AI_PLACEHOLDER.md";
35546
35618
  var SECRETS_REL = "project.secrets.json";
@@ -35553,7 +35625,7 @@ async function readText3(path, fallback = "") {
35553
35625
  }
35554
35626
  async function readSecretsObject(root) {
35555
35627
  try {
35556
- const v = JSON.parse(await readFile6(join18(root, SECRETS_REL), "utf8"));
35628
+ const v = JSON.parse(await readFile6(join17(root, SECRETS_REL), "utf8"));
35557
35629
  return v && typeof v === "object" ? v : {};
35558
35630
  } catch {
35559
35631
  return {};
@@ -35572,8 +35644,8 @@ function placeholderKeys(text) {
35572
35644
  }
35573
35645
  async function readSecrets(root) {
35574
35646
  const [security, placeholder, secrets] = await Promise.all([
35575
- readText3(join18(root, SECURITY_REL)),
35576
- readText3(join18(root, PLACEHOLDER_REL)),
35647
+ readText3(join17(root, SECURITY_REL)),
35648
+ readText3(join17(root, PLACEHOLDER_REL)),
35577
35649
  readSecretsObject(root)
35578
35650
  ]);
35579
35651
  const names = /* @__PURE__ */ new Set([...placeholderKeys(placeholder), ...Object.keys(secrets)]);
@@ -35586,14 +35658,14 @@ async function readSecrets(root) {
35586
35658
  async function writeSecrets(root, req) {
35587
35659
  try {
35588
35660
  if (req.kind === "security") {
35589
- await writeFile5(join18(root, SECURITY_REL), req.content, "utf8");
35661
+ await writeFile5(join17(root, SECURITY_REL), req.content, "utf8");
35590
35662
  } else if (req.kind === "placeholder") {
35591
- await writeFile5(join18(root, PLACEHOLDER_REL), req.content, "utf8");
35663
+ await writeFile5(join17(root, PLACEHOLDER_REL), req.content, "utf8");
35592
35664
  } else {
35593
35665
  const secrets = await readSecretsObject(root);
35594
35666
  if (req.kind === "secret") secrets[req.key] = req.value;
35595
35667
  else delete secrets[req.key];
35596
- await writeFile5(join18(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
35668
+ await writeFile5(join17(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
35597
35669
  }
35598
35670
  return { ok: true };
35599
35671
  } catch (err) {
@@ -35603,13 +35675,13 @@ async function writeSecrets(root, req) {
35603
35675
 
35604
35676
  // src/core/agent-tools.ts
35605
35677
  import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
35606
- import { dirname as dirname5, join as join19 } from "path";
35678
+ import { dirname as dirname5, join as join18 } from "path";
35607
35679
  var SHARED_REL = ".claude/settings.json";
35608
35680
  var LOCAL_REL = ".claude/settings.local.json";
35609
35681
  var SECRETS_DENY = "Read(./project.secrets.json)";
35610
35682
  var MODES = ["default", "acceptEdits", "plan", "bypassPermissions"];
35611
35683
  function fileFor(root, scope) {
35612
- return join19(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
35684
+ return join18(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
35613
35685
  }
35614
35686
  async function readSettingsObject(path) {
35615
35687
  try {
@@ -35646,7 +35718,7 @@ async function writeAgentTools(root, scope, permissions) {
35646
35718
 
35647
35719
  // src/core/graph.ts
35648
35720
  import { readFile as readFile8, readdir as readdir5 } from "fs/promises";
35649
- import { join as join20, relative as relative2, dirname as dirname6, resolve as resolve4, extname, basename as basename2 } from "path";
35721
+ import { join as join19, relative as relative2, dirname as dirname6, resolve as resolve4, extname, basename as basename2 } from "path";
35650
35722
  var MAX_FILES = 2e3;
35651
35723
  var MAX_NODES = 3e3;
35652
35724
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
@@ -35666,7 +35738,7 @@ async function walk(root, accept) {
35666
35738
  if (e.name.startsWith(".") && e.name !== ".claude") {
35667
35739
  if (e.isDirectory()) continue;
35668
35740
  }
35669
- const full = join20(dir2, e.name);
35741
+ const full = join19(dir2, e.name);
35670
35742
  if (e.isDirectory()) {
35671
35743
  if (!SKIP_DIRS.has(e.name)) stack.push(full);
35672
35744
  } else if (accept(full)) {
@@ -35786,10 +35858,10 @@ async function buildGraph(root, mode) {
35786
35858
 
35787
35859
  // src/core/rag.ts
35788
35860
  import { readFile as readFile9, writeFile as writeFile7, statfs as statfs2, mkdir as mkdir6 } from "fs/promises";
35789
- import { existsSync as existsSync9 } from "fs";
35861
+ import { existsSync as existsSync10 } from "fs";
35790
35862
  import { execFile as execFile4, spawn as spawn7 } from "child_process";
35791
35863
  import { totalmem as totalmem2, cpus as cpus2 } from "os";
35792
- import { join as join21 } from "path";
35864
+ import { join as join20 } from "path";
35793
35865
  import { promisify as promisify4 } from "util";
35794
35866
  var run3 = promisify4(execFile4);
35795
35867
  var RAG_REL = ".claude/rag";
@@ -35810,7 +35882,7 @@ async function detectPython() {
35810
35882
  }
35811
35883
  async function installLog(root) {
35812
35884
  try {
35813
- const text = await readFile9(join21(root, RAG_REL, "install.log"), "utf8");
35885
+ const text = await readFile9(join20(root, RAG_REL, "install.log"), "utf8");
35814
35886
  return text.split("\n").filter((l) => l.length > 0).slice(-200);
35815
35887
  } catch {
35816
35888
  return [];
@@ -35872,14 +35944,14 @@ async function ragStatus(root) {
35872
35944
  };
35873
35945
  let modelName = "";
35874
35946
  try {
35875
- modelName = (await readFile9(join21(root, RAG_REL, ".installed"), "utf8")).trim();
35947
+ modelName = (await readFile9(join20(root, RAG_REL, ".installed"), "utf8")).trim();
35876
35948
  } catch {
35877
35949
  modelName = "";
35878
35950
  }
35879
35951
  const modelPresent = modelName.length > 0;
35880
35952
  return {
35881
35953
  installed: fastembed && sqliteVec && modelPresent,
35882
- installing: existsSync9(join21(root, RAG_REL, ".installing")),
35954
+ installing: existsSync10(join20(root, RAG_REL, ".installing")),
35883
35955
  python: { found: py.found, version: py.version },
35884
35956
  deps: { pip, fastembed, sqliteVec },
35885
35957
  model: { present: modelPresent, name: modelName },
@@ -35890,9 +35962,9 @@ async function ragStatus(root) {
35890
35962
  };
35891
35963
  }
35892
35964
  async function readIndexMeta(root) {
35893
- const indexing = existsSync9(join21(root, RAG_REL, ".indexing"));
35965
+ const indexing = existsSync10(join20(root, RAG_REL, ".indexing"));
35894
35966
  try {
35895
- const m = JSON.parse(await readFile9(join21(root, RAG_REL, ".index.json"), "utf8"));
35967
+ const m = JSON.parse(await readFile9(join20(root, RAG_REL, ".index.json"), "utf8"));
35896
35968
  return {
35897
35969
  present: true,
35898
35970
  indexing,
@@ -35959,14 +36031,14 @@ if __name__ == "__main__":
35959
36031
  elif cmd == "query": query(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 8)
35960
36032
  `;
35961
36033
  async function ensureEngine(root) {
35962
- await mkdir6(join21(root, RAG_REL), { recursive: true }).catch(() => void 0);
35963
- await writeFile7(join21(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
36034
+ await mkdir6(join20(root, RAG_REL), { recursive: true }).catch(() => void 0);
36035
+ await writeFile7(join20(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
35964
36036
  }
35965
36037
  async function ragReindex(root) {
35966
36038
  const py = await detectPython();
35967
36039
  if (!py.found) return { ok: false, started: false, error: "python3 not found" };
35968
- if (!existsSync9(join21(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
35969
- if (existsSync9(join21(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
36040
+ if (!existsSync10(join20(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
36041
+ if (existsSync10(join20(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
35970
36042
  await ensureEngine(root);
35971
36043
  const script = `
35972
36044
  set +e
@@ -35986,12 +36058,12 @@ rm -f "${RAG_REL}/.indexing"
35986
36058
  async function ragQuery(root, queryText, k = 8) {
35987
36059
  const py = await detectPython();
35988
36060
  if (!py.found) return { results: [], error: "python3 not found" };
35989
- if (!existsSync9(join21(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
36061
+ if (!existsSync10(join20(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
35990
36062
  await ensureEngine(root);
35991
36063
  try {
35992
36064
  const { stdout } = await run3(
35993
36065
  py.bin,
35994
- [join21(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k, 1), 20))],
36066
+ [join20(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k, 1), 20))],
35995
36067
  { cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
35996
36068
  );
35997
36069
  const parsed = JSON.parse(stdout.trim() || '{"results":[]}');
@@ -36003,9 +36075,9 @@ async function ragQuery(root, queryText, k = 8) {
36003
36075
  async function ragInstall(root, model) {
36004
36076
  const py = await detectPython();
36005
36077
  if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
36006
- if (existsSync9(join21(root, RAG_REL, ".installing"))) return { ok: true, started: true };
36078
+ if (existsSync10(join20(root, RAG_REL, ".installing"))) return { ok: true, started: true };
36007
36079
  try {
36008
- await mkdir6(join21(root, RAG_REL), { recursive: true });
36080
+ await mkdir6(join20(root, RAG_REL), { recursive: true });
36009
36081
  } catch {
36010
36082
  }
36011
36083
  const script = `
@@ -36093,12 +36165,12 @@ ${stderr}`;
36093
36165
  // src/core/git-ssh-key.ts
36094
36166
  import { execFile as execFile7 } from "child_process";
36095
36167
  import { readFile as readFile11, rm as rm3, mkdir as mkdir7 } from "fs/promises";
36096
- import { join as join22 } from "path";
36168
+ import { join as join21 } from "path";
36097
36169
  import { promisify as promisify7 } from "util";
36098
36170
  var run6 = promisify7(execFile7);
36099
36171
  function keyPaths(profileDir2) {
36100
- const dir2 = join22(profileDir2, ".ssh");
36101
- const key = join22(dir2, "id_4pm");
36172
+ const dir2 = join21(profileDir2, ".ssh");
36173
+ const key = join21(dir2, "id_4pm");
36102
36174
  return { dir: dir2, key, pub: `${key}.pub` };
36103
36175
  }
36104
36176
  async function readPublicKey(pub) {
@@ -36290,7 +36362,7 @@ async function setCommitAuthor(cwd2, machineUsername) {
36290
36362
  // src/core/scaffold.ts
36291
36363
  import { cp, mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
36292
36364
  import { execFile as execFile10 } from "child_process";
36293
- import { dirname as dirname8, join as join23, resolve as resolve7 } from "path";
36365
+ import { dirname as dirname8, join as join22, resolve as resolve7 } from "path";
36294
36366
  import { fileURLToPath as fileURLToPath3 } from "url";
36295
36367
  import { promisify as promisify10 } from "util";
36296
36368
  var run9 = promisify10(execFile10);
@@ -36319,7 +36391,7 @@ async function provisionRepos(target, repos, emit, opts = {}) {
36319
36391
  }
36320
36392
  continue;
36321
36393
  }
36322
- const dir2 = join23(target, subDirName(repo));
36394
+ const dir2 = join22(target, subDirName(repo));
36323
36395
  if (repo.url) {
36324
36396
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
36325
36397
  await run9("git", ["clone", repo.url, dir2], { timeout: 12e4 });
@@ -36339,14 +36411,14 @@ async function scaffoldProject(payload, profileDir2, onProgress) {
36339
36411
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
36340
36412
  try {
36341
36413
  const root = process.env.SCAFFOLD_ROOT ? resolve7(process.env.SCAFFOLD_ROOT) : resolve7(profileDir2);
36342
- const target = join23(root, payload.projectName);
36414
+ const target = join22(root, payload.projectName);
36343
36415
  await mkdir8(target, { recursive: true });
36344
36416
  emit("copy", "Copying the sample template\u2026");
36345
36417
  await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
36346
36418
  if (payload.spec) {
36347
36419
  emit("spec", "Writing project.spec.json\u2026");
36348
36420
  await writeFile8(
36349
- join23(target, "project.spec.json"),
36421
+ join22(target, "project.spec.json"),
36350
36422
  JSON.stringify(payload.spec, null, 2),
36351
36423
  "utf8"
36352
36424
  );
@@ -36364,7 +36436,7 @@ async function aiInit(target, spec, emit) {
36364
36436
  emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
36365
36437
  const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
36366
36438
  if (subagents.length > 0) {
36367
- await mkdir8(join23(target, ".claude", "agents"), { recursive: true });
36439
+ await mkdir8(join22(target, ".claude", "agents"), { recursive: true });
36368
36440
  for (const sa of subagents) {
36369
36441
  const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
36370
36442
  if (!name) continue;
@@ -36374,17 +36446,17 @@ name: ${name}
36374
36446
 
36375
36447
  ${(sa.description || "").trim()}
36376
36448
  `;
36377
- await writeFile8(join23(target, ".claude", "agents", `${name}.md`), body, "utf8");
36449
+ await writeFile8(join22(target, ".claude", "agents", `${name}.md`), body, "utf8");
36378
36450
  }
36379
36451
  }
36380
36452
  const specJson = JSON.stringify(spec);
36381
36453
  await generateFile(
36382
- join23(target, "README.md"),
36454
+ join22(target, "README.md"),
36383
36455
  `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
36384
36456
  ${specJson}`
36385
36457
  );
36386
36458
  await generateFile(
36387
- join23(target, "CLAUDE.md"),
36459
+ join22(target, "CLAUDE.md"),
36388
36460
  `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
36389
36461
  ${specJson}`
36390
36462
  );
@@ -36400,7 +36472,7 @@ async function addProject(payload, profileDir2, onProgress) {
36400
36472
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
36401
36473
  try {
36402
36474
  const root = process.env.SCAFFOLD_ROOT ? resolve7(process.env.SCAFFOLD_ROOT) : resolve7(profileDir2);
36403
- const target = join23(root, payload.projectName);
36475
+ const target = join22(root, payload.projectName);
36404
36476
  await mkdir8(target, { recursive: true });
36405
36477
  const repos = payload.repos ?? [];
36406
36478
  if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
@@ -36412,14 +36484,14 @@ async function addProject(payload, profileDir2, onProgress) {
36412
36484
  }
36413
36485
 
36414
36486
  // src/core/config-sync.ts
36415
- import { existsSync as existsSync10, readFileSync as readFileSync14 } from "fs";
36416
- import { join as join24 } from "path";
36487
+ import { existsSync as existsSync11, readFileSync as readFileSync15 } from "fs";
36488
+ import { join as join23 } from "path";
36417
36489
  var SERVER_MANAGED_KEYS = ["physicPath", "sessionSwitchPct", "perPromptTokenLimit"];
36418
36490
  function readConfigText(profileDir2) {
36419
- const path = join24(profileDir2, "config.json");
36420
- if (existsSync10(path)) {
36491
+ const path = join23(profileDir2, "config.json");
36492
+ if (existsSync11(path)) {
36421
36493
  try {
36422
- return readFileSync14(path, "utf8");
36494
+ return readFileSync15(path, "utf8");
36423
36495
  } catch {
36424
36496
  }
36425
36497
  }
@@ -36844,7 +36916,6 @@ var WsClient = class {
36844
36916
  });
36845
36917
  this.outboundReviewEnabled = token.outboundReview?.enabled === true;
36846
36918
  this.restrictToFolder = token.aiRestrictToFolder === true;
36847
- writeEgressAllowlist(this.context.profileDir, token.aiSandbox ?? null);
36848
36919
  applyGitAuth(token.gitAuth ?? null);
36849
36920
  this.machineUsername = token.machineUsername ?? "";
36850
36921
  if (token.scope && token.scope !== this.credential.scope) {
@@ -36992,8 +37063,8 @@ var WsClient = class {
36992
37063
  const oldRoot = this.physicFolderPath(sync.oldName);
36993
37064
  if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
36994
37065
  }
36995
- if (sync.oldName) rmSync5(join25(this.context.profileDir, sync.oldName), { recursive: true, force: true });
36996
- mkdirSync5(join25(this.context.profileDir, sync.newName), { recursive: true });
37066
+ if (sync.oldName) rmSync4(join24(this.context.profileDir, sync.oldName), { recursive: true, force: true });
37067
+ mkdirSync5(join24(this.context.profileDir, sync.newName), { recursive: true });
36997
37068
  this.physicRoot = this.physicFolderPath(sync.newName);
36998
37069
  this.bus.setProject(sync.newName);
36999
37070
  this.bus.log(`Physic project folder synced \u2192 ${sync.newName}`);
@@ -37004,7 +37075,7 @@ var WsClient = class {
37004
37075
  if (del.name) {
37005
37076
  const delRoot = this.physicFolderPath(del.name);
37006
37077
  if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
37007
- rmSync5(join25(this.context.profileDir, del.name), { recursive: true, force: true });
37078
+ rmSync4(join24(this.context.profileDir, del.name), { recursive: true, force: true });
37008
37079
  this.bus.log(`Physic project folder deleted (project removed) \u2192 ${del.name}`);
37009
37080
  }
37010
37081
  void manageSshKey("delete", this.context.profileDir).catch(() => void 0);
@@ -37376,7 +37447,7 @@ var WsClient = class {
37376
37447
  }
37377
37448
  case WsChannels.LOG_READ: {
37378
37449
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
37379
- const lines = readRecentLogLines(join25(this.context.profileDir, "logs"), limit);
37450
+ const lines = readRecentLogLines(join24(this.context.profileDir, "logs"), limit);
37380
37451
  this.send(WsChannels.LOG_READ, { lines }, message.id);
37381
37452
  break;
37382
37453
  }
@@ -37962,7 +38033,7 @@ var WsClient = class {
37962
38033
  /** Create the physic folder at an absolute path if missing (best-effort). */
37963
38034
  ensurePhysicFolderPath(folder) {
37964
38035
  try {
37965
- if (!existsSync11(folder)) {
38036
+ if (!existsSync12(folder)) {
37966
38037
  mkdirSync5(folder, { recursive: true });
37967
38038
  this.bus.log(`Created physic project folder: ${folder}`);
37968
38039
  }
@@ -37977,7 +38048,7 @@ var WsClient = class {
37977
38048
  */
37978
38049
  physicFolderPath(projectName) {
37979
38050
  const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
37980
- return safe ? join25(this.context.profileDir, safe) : null;
38051
+ return safe ? join24(this.context.profileDir, safe) : null;
37981
38052
  }
37982
38053
  /**
37983
38054
  * Report machine.status: version + the state of the physic project being served
@@ -37991,7 +38062,7 @@ var WsClient = class {
37991
38062
  projects: [],
37992
38063
  version: CLI_VERSION,
37993
38064
  physicPath,
37994
- physicPathExists: physicPath ? existsSync11(physicPath) : void 0,
38065
+ physicPathExists: physicPath ? existsSync12(physicPath) : void 0,
37995
38066
  // Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
37996
38067
  autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false
37997
38068
  });
@@ -38039,6 +38110,7 @@ var WsClient = class {
38039
38110
  };
38040
38111
  snapshot.aiCli = aiCli2;
38041
38112
  snapshot.profile = profile;
38113
+ snapshot.network = await probeNetwork();
38042
38114
  this.usageSnapshot = snapshot;
38043
38115
  this.bus.setUsage?.(snapshot);
38044
38116
  this.send(WsChannels.MACHINE_USAGE, snapshot);
@@ -38080,7 +38152,7 @@ var WsClient = class {
38080
38152
  */
38081
38153
  uploadLog() {
38082
38154
  try {
38083
- const upload = readLogUpload(join25(this.context.profileDir, "logs"));
38155
+ const upload = readLogUpload(join24(this.context.profileDir, "logs"));
38084
38156
  if (!upload) return;
38085
38157
  const sig = `${upload.fileName}:${upload.totalBytes}`;
38086
38158
  if (sig === this.lastLogSig) return;
@@ -38108,10 +38180,10 @@ var WsClient = class {
38108
38180
  };
38109
38181
 
38110
38182
  // src/core/instance-lock.ts
38111
- import { readFileSync as readFileSync15, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
38112
- import { join as join26 } from "path";
38183
+ import { readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
38184
+ import { join as join25 } from "path";
38113
38185
  function lockPath(profileDir2) {
38114
- return join26(profileDir2, "start.lock");
38186
+ return join25(profileDir2, "start.lock");
38115
38187
  }
38116
38188
  function isAlive(pid) {
38117
38189
  try {
@@ -38123,7 +38195,7 @@ function isAlive(pid) {
38123
38195
  }
38124
38196
  function lockHolder(profileDir2) {
38125
38197
  try {
38126
- const pid = Number.parseInt(readFileSync15(lockPath(profileDir2), "utf8").trim(), 10);
38198
+ const pid = Number.parseInt(readFileSync16(lockPath(profileDir2), "utf8").trim(), 10);
38127
38199
  if (Number.isInteger(pid) && pid > 0 && pid !== process.pid && isAlive(pid)) return pid;
38128
38200
  } catch {
38129
38201
  }
@@ -38131,13 +38203,13 @@ function lockHolder(profileDir2) {
38131
38203
  }
38132
38204
  function acquireInstanceLock(profileDir2) {
38133
38205
  if (lockHolder(profileDir2) !== null) return false;
38134
- writeFileSync8(lockPath(profileDir2), String(process.pid), "utf8");
38206
+ writeFileSync7(lockPath(profileDir2), String(process.pid), "utf8");
38135
38207
  return true;
38136
38208
  }
38137
38209
  function releaseInstanceLock(profileDir2) {
38138
38210
  try {
38139
- const pid = Number.parseInt(readFileSync15(lockPath(profileDir2), "utf8").trim(), 10);
38140
- if (pid === process.pid) rmSync6(lockPath(profileDir2), { force: true });
38211
+ const pid = Number.parseInt(readFileSync16(lockPath(profileDir2), "utf8").trim(), 10);
38212
+ if (pid === process.pid) rmSync5(lockPath(profileDir2), { force: true });
38141
38213
  } catch {
38142
38214
  }
38143
38215
  }
@@ -38398,8 +38470,8 @@ var SessionBus = class {
38398
38470
 
38399
38471
  // src/core/control-server.ts
38400
38472
  import { createServer } from "net";
38401
- import { rmSync as rmSync7 } from "fs";
38402
- import { join as join27 } from "path";
38473
+ import { rmSync as rmSync6 } from "fs";
38474
+ import { join as join26 } from "path";
38403
38475
 
38404
38476
  // src/core/control-protocol.ts
38405
38477
  var CONTROL_SOCKET_FILE = "control.sock";
@@ -38428,9 +38500,9 @@ function createFrameParser() {
38428
38500
 
38429
38501
  // src/core/control-server.ts
38430
38502
  function startControlServer(bus, profileDir2, info) {
38431
- const socketPath = join27(profileDir2, CONTROL_SOCKET_FILE);
38503
+ const socketPath = join26(profileDir2, CONTROL_SOCKET_FILE);
38432
38504
  try {
38433
- rmSync7(socketPath, { force: true });
38505
+ rmSync6(socketPath, { force: true });
38434
38506
  } catch {
38435
38507
  }
38436
38508
  const clients = /* @__PURE__ */ new Set();
@@ -38496,7 +38568,7 @@ function startControlServer(bus, profileDir2, info) {
38496
38568
  clients.clear();
38497
38569
  server.close();
38498
38570
  try {
38499
- rmSync7(socketPath, { force: true });
38571
+ rmSync6(socketPath, { force: true });
38500
38572
  } catch {
38501
38573
  }
38502
38574
  };
@@ -40476,7 +40548,7 @@ var src_default = Yoga;
40476
40548
 
40477
40549
  // ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/reconciler.js
40478
40550
  var import_react_reconciler = __toESM(require_react_reconciler(), 1);
40479
- var import_constants6 = __toESM(require_constants2(), 1);
40551
+ var import_constants7 = __toESM(require_constants2(), 1);
40480
40552
  import process3 from "process";
40481
40553
 
40482
40554
  // ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
@@ -41718,7 +41790,7 @@ var reconciler_default = (0, import_react_reconciler.default)({
41718
41790
  scheduleTimeout: setTimeout,
41719
41791
  cancelTimeout: clearTimeout,
41720
41792
  noTimeout: -1,
41721
- getCurrentEventPriority: () => import_constants6.DefaultEventPriority,
41793
+ getCurrentEventPriority: () => import_constants7.DefaultEventPriority,
41722
41794
  beforeActiveInstanceBlur() {
41723
41795
  },
41724
41796
  afterActiveInstanceBlur() {
@@ -44160,17 +44232,17 @@ var import_react21 = __toESM(require_react(), 1);
44160
44232
  var import_react26 = __toESM(require_react(), 1);
44161
44233
 
44162
44234
  // src/core/input-history.ts
44163
- import { existsSync as existsSync13, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "fs";
44164
- import { join as join28 } from "path";
44235
+ import { existsSync as existsSync14, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
44236
+ import { join as join27 } from "path";
44165
44237
  var MAX_HISTORY = 200;
44166
44238
  function historyPath(profileDir2) {
44167
- return join28(profileDir2, "input-history.json");
44239
+ return join27(profileDir2, "input-history.json");
44168
44240
  }
44169
44241
  function loadInputHistory(profileDir2) {
44170
44242
  const path = historyPath(profileDir2);
44171
- if (!existsSync13(path)) return [];
44243
+ if (!existsSync14(path)) return [];
44172
44244
  try {
44173
- const data = JSON.parse(readFileSync17(path, "utf8"));
44245
+ const data = JSON.parse(readFileSync18(path, "utf8"));
44174
44246
  return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
44175
44247
  } catch {
44176
44248
  return [];
@@ -44181,7 +44253,7 @@ function appendInputHistory(profileDir2, line, current) {
44181
44253
  if (!trimmed || current[current.length - 1] === trimmed) return current;
44182
44254
  const next = [...current, trimmed].slice(-MAX_HISTORY);
44183
44255
  try {
44184
- writeFileSync9(historyPath(profileDir2), JSON.stringify(next), "utf8");
44256
+ writeFileSync8(historyPath(profileDir2), JSON.stringify(next), "utf8");
44185
44257
  } catch {
44186
44258
  }
44187
44259
  return next;
@@ -44430,8 +44502,8 @@ function TranscriptLine({ line }) {
44430
44502
  var import_react24 = __toESM(require_react(), 1);
44431
44503
 
44432
44504
  // src/ui/slash-commands.ts
44433
- import { existsSync as existsSync14 } from "fs";
44434
- import { join as join29 } from "path";
44505
+ import { existsSync as existsSync15 } from "fs";
44506
+ import { join as join28 } from "path";
44435
44507
  var CONFIG_KEYS = [
44436
44508
  "aiCli",
44437
44509
  "claudeHome",
@@ -44443,7 +44515,7 @@ var CONFIG_KEYS = [
44443
44515
  "aiEnv"
44444
44516
  ];
44445
44517
  function configPath(profileDir2) {
44446
- return join29(profileDir2, "config.json");
44518
+ return join28(profileDir2, "config.json");
44447
44519
  }
44448
44520
  function parseConfigAssignment(key, valueTokens) {
44449
44521
  const value = valueTokens.join(" ").trim();
@@ -44493,7 +44565,7 @@ function runConfig(ctx) {
44493
44565
  ctx.print(`\u2714 ${verb} default config: ${path}`);
44494
44566
  ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
44495
44567
  };
44496
- if (existsSync14(path)) {
44568
+ if (existsSync15(path)) {
44497
44569
  ctx.confirm(
44498
44570
  `config exists: ${path} \u2014 replace it with defaults? (y/N)`,
44499
44571
  () => writeDefaults("replaced with")
@@ -44578,7 +44650,7 @@ function runOutput(ctx) {
44578
44650
  }
44579
44651
  function runLogs(ctx) {
44580
44652
  const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
44581
- const dir2 = join29(ctx.info.profileDir, "logs");
44653
+ const dir2 = join28(ctx.info.profileDir, "logs");
44582
44654
  const lines = readRecentLogLines(dir2, n);
44583
44655
  if (lines.length === 0) {
44584
44656
  ctx.print("No log lines yet.");
@@ -45099,7 +45171,7 @@ async function runStart(profileDir2, profileName) {
45099
45171
  return;
45100
45172
  }
45101
45173
  logger.configure({
45102
- dir: join30(profileDir2, "logs"),
45174
+ dir: join29(profileDir2, "logs"),
45103
45175
  level: resolveLogLevel(),
45104
45176
  base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
45105
45177
  });
@@ -45205,13 +45277,13 @@ async function runStart(profileDir2, profileName) {
45205
45277
  }
45206
45278
 
45207
45279
  // src/commands/attach.ts
45208
- import { join as join31 } from "path";
45280
+ import { join as join30 } from "path";
45209
45281
 
45210
45282
  // src/core/control-client.ts
45211
- import { connect } from "net";
45283
+ import { connect as connect2 } from "net";
45212
45284
  function connectControl(socketPath, bus) {
45213
45285
  return new Promise((resolve8, reject) => {
45214
- const socket = connect(socketPath);
45286
+ const socket = connect2(socketPath);
45215
45287
  socket.setEncoding("utf8");
45216
45288
  let ready = false;
45217
45289
  const forwardSubmit = bus.onLocalSubmit((input) => {
@@ -45304,7 +45376,7 @@ async function runAttach(profileDir2, profileName) {
45304
45376
  process.exitCode = 1;
45305
45377
  return;
45306
45378
  }
45307
- const socketPath = join31(profileDir2, CONTROL_SOCKET_FILE);
45379
+ const socketPath = join30(profileDir2, CONTROL_SOCKET_FILE);
45308
45380
  const bus = new SessionBus();
45309
45381
  let conn;
45310
45382
  try {
@@ -45389,7 +45461,7 @@ Tip: for the credentials to survive \`docker rm\`/recreate, keep each profile pa
45389
45461
  }
45390
45462
 
45391
45463
  // src/commands/unlink.ts
45392
- import { rmSync as rmSync8 } from "fs";
45464
+ import { rmSync as rmSync7 } from "fs";
45393
45465
  async function runUnlink(profileDir2, profileName) {
45394
45466
  const cred = readCredential(profileDir2, profileName);
45395
45467
  if (!cred) {
@@ -45404,7 +45476,7 @@ async function runUnlink(profileDir2, profileName) {
45404
45476
  `\u26A0 Could not reach the server to revoke \u2014 revoke it at web /machines. (${err.message})`
45405
45477
  );
45406
45478
  }
45407
- rmSync8(profileDir2, { recursive: true, force: true });
45479
+ rmSync7(profileDir2, { recursive: true, force: true });
45408
45480
  clearDefaultProfileIf(profileName);
45409
45481
  console.log("\u2714 Deleted the profile and its physic project folder.");
45410
45482
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
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",