@4pm/cli 1.8.4 → 1.9.0-c

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 +564 -425
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -29506,17 +29506,38 @@ MCowBQYDK2VwAyEARlfzjaFhe5bhRrMp/ZuH6sVqg/S7f8B1dliuF21iDTw=
29506
29506
  `;
29507
29507
 
29508
29508
  // src/version.ts
29509
+ import { execFileSync } from "child_process";
29509
29510
  import { readFileSync as readFileSync4 } from "fs";
29510
29511
  import { dirname, join as join4 } from "path";
29511
29512
  import { fileURLToPath } from "url";
29513
+ function gitDescribeVersion(cwd2) {
29514
+ try {
29515
+ const out = execFileSync("git", ["describe", "--tags", "--match", "cli-v*"], {
29516
+ cwd: cwd2,
29517
+ timeout: 3e3,
29518
+ stdio: ["ignore", "pipe", "ignore"]
29519
+ }).toString().trim();
29520
+ const stripped = out.replace(/^cli-v/, "");
29521
+ return stripped || null;
29522
+ } catch {
29523
+ return null;
29524
+ }
29525
+ }
29512
29526
  function readCliVersion() {
29527
+ const envVersion = process.env.FOURPM_CLI_VERSION?.trim();
29528
+ if (envVersion) return envVersion.replace(/^cli-v/, "");
29529
+ let version = "0.0.0";
29513
29530
  try {
29514
29531
  const here = dirname(fileURLToPath(import.meta.url));
29515
29532
  const pkg = JSON.parse(readFileSync4(join4(here, "..", "package.json"), "utf8"));
29516
- return pkg.version ?? "0.0.0";
29533
+ version = pkg.version ?? "0.0.0";
29534
+ if (version === "0.0.0") {
29535
+ const dev = gitDescribeVersion(here);
29536
+ if (dev) version = dev;
29537
+ }
29517
29538
  } catch {
29518
- return "0.0.0";
29519
29539
  }
29540
+ return version;
29520
29541
  }
29521
29542
  var CLI_VERSION = readCliVersion();
29522
29543
 
@@ -33965,6 +33986,14 @@ var issueProvisioningTokenRequestSchema = external_exports.object({
33965
33986
  /** TTL seconds (0/absent ⇒ a default short TTL applied server-side). */
33966
33987
  ttlSec: external_exports.number().int().min(0).max(2592e3).optional()
33967
33988
  });
33989
+ var provisionRequestSchema = external_exports.object({
33990
+ mode: external_exports.enum(["sync", "force"]).optional().default("sync")
33991
+ });
33992
+ var provisionForUserRequestSchema = external_exports.object({
33993
+ userId: external_exports.string().uuid(),
33994
+ projectId: external_exports.string().uuid(),
33995
+ mode: external_exports.enum(["sync", "force"]).optional().default("sync")
33996
+ });
33968
33997
  var listWorkersQuerySchema = baseRequestSchema.extend({
33969
33998
  status: external_exports.enum(["online", "offline"]).optional(),
33970
33999
  /** Only workers whose live CPU/memory/disk usage % ≥ the given threshold (0–100). */
@@ -34240,7 +34269,13 @@ var repoSpecSchema = external_exports.object({
34240
34269
  /** Clone url — `https` or `ssh` (required; ADR-0172). */
34241
34270
  url: external_exports.string().max(500).regex(GIT_URL_RE, "must be an https or ssh git url"),
34242
34271
  /** Sub-repo subfolder under the target root (empty ⇒ derived from role/name). */
34243
- subdir: external_exports.string().max(120).optional().default("")
34272
+ subdir: external_exports.string().max(120).optional().default(""),
34273
+ /**
34274
+ * Primary branch to clone / check out (ADR-0292). Empty ⇒ the repo's default branch. The
34275
+ * worker clones with `-b <branch>` and the on-demand "update repos" action checks it out +
34276
+ * fast-forward pulls it.
34277
+ */
34278
+ branch: external_exports.string().max(200).optional().default("")
34244
34279
  });
34245
34280
  function repoName(url) {
34246
34281
  const last = (url || "").trim().replace(/\/+$/, "").split(/[/:]/).pop() ?? "";
@@ -34367,9 +34402,282 @@ var activityHeartbeatSchema = external_exports.object({
34367
34402
  projectId: external_exports.string().uuid().optional()
34368
34403
  });
34369
34404
 
34405
+ // src/core/scaffold.ts
34406
+ import { cp, mkdir, readdir, rename, rm, writeFile } from "fs/promises";
34407
+ import { existsSync as existsSync4 } from "fs";
34408
+ import { execFile as execFile2 } from "child_process";
34409
+ import { dirname as dirname3, join as join6, resolve } from "path";
34410
+ import { fileURLToPath as fileURLToPath3 } from "url";
34411
+ import { promisify as promisify2 } from "util";
34412
+
34413
+ // src/core/ai-assist.ts
34414
+ import { execFile } from "child_process";
34415
+ import { promisify } from "util";
34416
+ var run = promisify(execFile);
34417
+ function aiCli() {
34418
+ return process.env.AI_CLI || "claude";
34419
+ }
34420
+ function aiArgs(cli, prompt) {
34421
+ if (cli === "claude") return ["-p", prompt, "--output-format", "json"];
34422
+ if (cli === "codex") return ["exec", "--json", prompt];
34423
+ return ["-p", prompt];
34424
+ }
34425
+ function estimateTokens(prompt, output) {
34426
+ return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
34427
+ }
34428
+ function parseAi(cli, stdout, prompt) {
34429
+ if (cli === "claude") {
34430
+ try {
34431
+ const j = JSON.parse(stdout);
34432
+ const text2 = (j.result ?? "").trim();
34433
+ const tokens = (j.usage?.input_tokens ?? 0) + (j.usage?.output_tokens ?? 0);
34434
+ if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens(prompt, text2) };
34435
+ } catch {
34436
+ }
34437
+ }
34438
+ if (cli === "codex") {
34439
+ try {
34440
+ let text2 = "";
34441
+ let tokens = 0;
34442
+ for (const line of stdout.split("\n").filter(Boolean)) {
34443
+ const obj = JSON.parse(line);
34444
+ if (obj.text) text2 = obj.text;
34445
+ else if (obj.message) text2 = obj.message;
34446
+ if (obj.usage) {
34447
+ tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
34448
+ }
34449
+ }
34450
+ if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens(prompt, text2) };
34451
+ } catch {
34452
+ }
34453
+ }
34454
+ const text = stdout.trim();
34455
+ return { text, tokens: estimateTokens(prompt, text) };
34456
+ }
34457
+ async function runAi(prompt) {
34458
+ const cli = aiCli();
34459
+ const { stdout } = await run(cli, aiArgs(cli, prompt), {
34460
+ timeout: 12e4,
34461
+ maxBuffer: 8 * 1024 * 1024
34462
+ });
34463
+ return parseAi(cli, stdout, prompt);
34464
+ }
34465
+ async function aiGenerate(prompt) {
34466
+ const { text } = await runAi(prompt);
34467
+ return text;
34468
+ }
34469
+ async function aiOutboundReview(input, allowedRepos, projectPath) {
34470
+ const prompt = `You are a security reviewer for an autonomous coding agent. Decide if the following INPUT is SAFE to run. It is UNSAFE if it: (1) asks the AI using secret VALUES (api keys, env values, passwords); (2) would affect other environments (only the sandbox project is allowed, no ssh/deploy/prod); (3) edits outside the project folder "${projectPath}"; (4) commits/pushes to a repo not in this allowlist: ${allowedRepos.length ? allowedRepos.join(", ") : "(none declared)"}. Reply with ONLY JSON: {"ok": <true|false>, "reasons": ["secret"|"environment"|"path"|"repo", ...]}. INPUT:
34471
+ ${input}`;
34472
+ const { text, tokens } = await runAi(prompt);
34473
+ const start = text.indexOf("{");
34474
+ const end = text.lastIndexOf("}");
34475
+ if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
34476
+ try {
34477
+ const j = JSON.parse(text.slice(start, end + 1));
34478
+ const reasons = Array.isArray(j.reasons) ? j.reasons.filter((r) => typeof r === "string") : [];
34479
+ return { ok: j.ok === true && reasons.length === 0, reasons, tokens };
34480
+ } catch {
34481
+ return { ok: false, reasons: ["ai_parse"], tokens };
34482
+ }
34483
+ }
34484
+
34485
+ // src/core/scaffold.ts
34486
+ var run2 = promisify2(execFile2);
34487
+ function reposOf(spec) {
34488
+ const repos = spec?.repos;
34489
+ return Array.isArray(repos) ? repos : [];
34490
+ }
34491
+ function subDirName(repo) {
34492
+ const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
34493
+ return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
34494
+ }
34495
+ function cloneArgs(url, dest, branch) {
34496
+ const b = (branch ?? "").trim();
34497
+ return b ? ["clone", "-b", b, url, dest] : ["clone", url, dest];
34498
+ }
34499
+ async function updateRepo(dir2, label, branch, emit) {
34500
+ const b = (branch ?? "").trim();
34501
+ emit("git", `Updating ${label} (fetch + ${b ? `checkout ${b} + ` : ""}fast-forward pull)\u2026`);
34502
+ await run2("git", ["fetch", "origin", "--prune"], { cwd: dir2, timeout: 12e4 });
34503
+ if (b) await run2("git", ["checkout", b], { cwd: dir2, timeout: 6e4 });
34504
+ await run2("git", ["pull", "--ff-only"], { cwd: dir2, timeout: 12e4 });
34505
+ }
34506
+ async function provisionRepos(target, repos, emit, opts = {}) {
34507
+ const mode = opts.mode ?? "clone";
34508
+ const subDirsToKeep = new Set(repos.filter((r) => !r.primary && r.url).map((r) => subDirName(r)));
34509
+ for (const repo of repos) {
34510
+ if (repo.primary) {
34511
+ if (opts.clonePrimary && repo.url) {
34512
+ if (existsSync4(join6(target, ".git"))) {
34513
+ if (mode === "sync") {
34514
+ await updateRepo(target, "primary repo", repo.branch, emit);
34515
+ } else if (mode === "force") {
34516
+ emit("git", `Re-cloning ${repo.url} (force)\u2026`);
34517
+ const tmp = `${target}.4pm-reclone`;
34518
+ await rm(tmp, { recursive: true, force: true });
34519
+ await run2("git", cloneArgs(repo.url, tmp, repo.branch), { timeout: 12e4 });
34520
+ for (const entry of await readdir(target)) {
34521
+ if (subDirsToKeep.has(entry)) continue;
34522
+ await rm(join6(target, entry), { recursive: true, force: true });
34523
+ }
34524
+ for (const entry of await readdir(tmp)) {
34525
+ if (subDirsToKeep.has(entry)) continue;
34526
+ await rename(join6(tmp, entry), join6(target, entry));
34527
+ }
34528
+ await rm(tmp, { recursive: true, force: true });
34529
+ } else {
34530
+ emit("git", `Primary repo already present \u2014 skipping clone.`);
34531
+ }
34532
+ continue;
34533
+ }
34534
+ emit("git", `Cloning ${repo.url}\u2026`);
34535
+ await run2("git", cloneArgs(repo.url, target, repo.branch), { timeout: 12e4 });
34536
+ continue;
34537
+ }
34538
+ emit("git", "Initializing the primary repo\u2026");
34539
+ await run2("git", ["init"], { cwd: target, timeout: 2e4 });
34540
+ if (repo.url) {
34541
+ await run2("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
34542
+ () => void 0
34543
+ );
34544
+ }
34545
+ continue;
34546
+ }
34547
+ const dir2 = join6(target, subDirName(repo));
34548
+ if (repo.url) {
34549
+ if (existsSync4(join6(dir2, ".git"))) {
34550
+ if (mode === "sync") {
34551
+ await updateRepo(dir2, `sub-repo ${subDirName(repo)}`, repo.branch, emit);
34552
+ } else if (mode === "force") {
34553
+ emit("git", `Re-cloning sub-repo ${repo.url} (force)\u2026`);
34554
+ await rm(dir2, { recursive: true, force: true });
34555
+ await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
34556
+ } else {
34557
+ emit("git", `Sub-repo ${subDirName(repo)} already present \u2014 skipping clone.`);
34558
+ }
34559
+ continue;
34560
+ }
34561
+ emit("git", `Cloning sub-repo ${repo.url}\u2026`);
34562
+ await run2("git", cloneArgs(repo.url, dir2, repo.branch), { timeout: 12e4 });
34563
+ } else {
34564
+ emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
34565
+ await mkdir(dir2, { recursive: true });
34566
+ await run2("git", ["init"], { cwd: dir2, timeout: 2e4 });
34567
+ }
34568
+ }
34569
+ }
34570
+ async function ensureReposCloned(physicRoot, repos, emit) {
34571
+ if (repos.length === 0) return;
34572
+ await provisionRepos(physicRoot, repos, emit ?? (() => void 0), { clonePrimary: true });
34573
+ }
34574
+ function sampleDir() {
34575
+ if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
34576
+ const here = dirname3(fileURLToPath3(import.meta.url));
34577
+ const bundled = resolve(here, "project-sample");
34578
+ for (const candidate of [bundled, resolve(here, "../project-sample"), resolve(here, "../../project-sample")]) {
34579
+ if (existsSync4(candidate)) return candidate;
34580
+ }
34581
+ return bundled;
34582
+ }
34583
+ async function scaffoldProject(payload, profileDir2, onProgress) {
34584
+ let lastStep = "start";
34585
+ const emit = (step, message) => {
34586
+ lastStep = step;
34587
+ onProgress?.({ projectId: payload.projectId, step, message });
34588
+ };
34589
+ try {
34590
+ const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
34591
+ const target = join6(root, payload.projectName);
34592
+ await mkdir(target, { recursive: true });
34593
+ emit("copy", "Copying the sample template\u2026");
34594
+ await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
34595
+ if (payload.spec) {
34596
+ emit("spec", "Writing project.spec.json\u2026");
34597
+ await writeFile(
34598
+ join6(target, "project.spec.json"),
34599
+ JSON.stringify(payload.spec, null, 2),
34600
+ "utf8"
34601
+ );
34602
+ }
34603
+ const repos = reposOf(payload.spec);
34604
+ if (repos.length > 0) await provisionRepos(target, repos, emit);
34605
+ if (payload.spec) await aiInit(target, payload.spec, emit);
34606
+ emit("version", "Writing .4pm/.4pm.json\u2026");
34607
+ await writeTemplateMarker(target);
34608
+ onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
34609
+ return { ok: true, path: target };
34610
+ } catch (err) {
34611
+ return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
34612
+ }
34613
+ }
34614
+ async function writeTemplateMarker(target) {
34615
+ const dir2 = join6(target, ".4pm");
34616
+ await mkdir(dir2, { recursive: true });
34617
+ const marker = { templateVersion: PROJECT_TEMPLATE.version, scaffoldedAt: (/* @__PURE__ */ new Date()).toISOString() };
34618
+ await writeFile(join6(dir2, ".4pm.json"), JSON.stringify(marker, null, 2) + "\n", "utf8");
34619
+ }
34620
+ async function aiInit(target, spec, emit) {
34621
+ emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
34622
+ const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
34623
+ if (subagents.length > 0) {
34624
+ await mkdir(join6(target, ".claude", "agents"), { recursive: true });
34625
+ for (const sa of subagents) {
34626
+ const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
34627
+ if (!name) continue;
34628
+ const body = `---
34629
+ name: ${name}
34630
+ ---
34631
+
34632
+ ${(sa.description || "").trim()}
34633
+ `;
34634
+ await writeFile(join6(target, ".claude", "agents", `${name}.md`), body, "utf8");
34635
+ }
34636
+ }
34637
+ const specJson = JSON.stringify(spec);
34638
+ await generateFile(
34639
+ join6(target, "README.md"),
34640
+ `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
34641
+ ${specJson}`
34642
+ );
34643
+ await generateFile(
34644
+ join6(target, "CLAUDE.md"),
34645
+ `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
34646
+ ${specJson}`
34647
+ );
34648
+ }
34649
+ async function generateFile(path, prompt) {
34650
+ try {
34651
+ const text = await aiGenerate(prompt);
34652
+ if (text.trim()) await writeFile(path, text.trim() + "\n", "utf8");
34653
+ } catch {
34654
+ }
34655
+ }
34656
+ async function addProject(payload, profileDir2, onProgress) {
34657
+ let lastStep = "start";
34658
+ const emit = (step, message) => {
34659
+ lastStep = step;
34660
+ onProgress?.({ projectId: payload.projectId, step, message });
34661
+ };
34662
+ try {
34663
+ const root = process.env.SCAFFOLD_ROOT ? resolve(process.env.SCAFFOLD_ROOT) : resolve(profileDir2);
34664
+ const target = join6(root, payload.projectName);
34665
+ await mkdir(target, { recursive: true });
34666
+ const repos = payload.repos ?? [];
34667
+ const mode = payload.mode ?? "clone";
34668
+ if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true, mode });
34669
+ onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
34670
+ return { ok: true, path: target };
34671
+ } catch (err) {
34672
+ const message = err instanceof Error ? err.message : String(err);
34673
+ onProgress?.({ projectId: payload.projectId, step: "error", message: `Failed: ${message}`, done: true });
34674
+ return { ok: false, error: message, step: lastStep };
34675
+ }
34676
+ }
34677
+
34370
34678
  // src/core/worker-metrics.ts
34371
34679
  import { statfs, readFile } from "fs/promises";
34372
- import { existsSync as existsSync4 } from "fs";
34680
+ import { existsSync as existsSync5 } from "fs";
34373
34681
  import { totalmem, freemem, cpus, platform, arch } from "os";
34374
34682
  var CGROUP = "/sys/fs/cgroup";
34375
34683
  var MB = 1024 * 1024;
@@ -34419,7 +34727,7 @@ function parseMaybeMax(text) {
34419
34727
  return Number.isFinite(n) ? n : null;
34420
34728
  }
34421
34729
  function createWorkerMetricsSampler(diskPath) {
34422
- const cgroupV2 = existsSync4(`${CGROUP}/cpu.stat`) && existsSync4(`${CGROUP}/memory.current`);
34730
+ const cgroupV2 = existsSync5(`${CGROUP}/cpu.stat`) && existsSync5(`${CGROUP}/memory.current`);
34423
34731
  let cgPrev = null;
34424
34732
  let osPrev = null;
34425
34733
  async function cgroupCpuCount() {
@@ -34734,6 +35042,10 @@ var WsChannels = {
34734
35042
  // serving cli so a change (e.g. aiRunTimeoutSec) applies on the next run, not only after a
34735
35043
  // reconnect. `ws_token` seeds these on connect; this channel only carries updates.
34736
35044
  PROJECT_TOKENS: "project.tokens",
35045
+ // server → cli (ADR-0289): update the cli to the latest version now (idle-aware self-update +
35046
+ // re-exec). Sent by the manual operator "Update" action and the update-on-connect hook when the
35047
+ // connecting cli is outdated. No payload; honours the machine's local autoUpdate opt-out.
35048
+ CLI_UPDATE: "cli.update",
34737
35049
  // command lifecycle
34738
35050
  COMMAND_ANNOUNCE: "command.announce",
34739
35051
  // console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
@@ -34847,9 +35159,9 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
34847
35159
  import { spawn as spawn2 } from "child_process";
34848
35160
 
34849
35161
  // src/common/logger/logger.ts
34850
- import { appendFile, mkdir, readdir, unlink } from "fs/promises";
35162
+ import { appendFile, mkdir as mkdir2, readdir as readdir2, unlink } from "fs/promises";
34851
35163
  import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
34852
- import { join as join6 } from "path";
35164
+ import { join as join7 } from "path";
34853
35165
  var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
34854
35166
  var DAY_MS = 24 * 60 * 60 * 1e3;
34855
35167
  var DEFAULT_RETENTION_DAYS = 14;
@@ -34898,8 +35210,8 @@ var CliLogger = class {
34898
35210
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
34899
35211
  const entry = { timestamp, level, ...this.base, event, ...fields };
34900
35212
  const line = JSON.stringify(entry) + "\n";
34901
- const path = join6(dir2, `cli-${timestamp.slice(0, 10)}.jsonl`);
34902
- this.queue = this.queue.then(() => mkdir(dir2, { recursive: true })).then(() => appendFile(path, line, "utf8")).catch((err) => {
35213
+ const path = join7(dir2, `cli-${timestamp.slice(0, 10)}.jsonl`);
35214
+ this.queue = this.queue.then(() => mkdir2(dir2, { recursive: true })).then(() => appendFile(path, line, "utf8")).catch((err) => {
34903
35215
  console.warn(`Failed to write cli log: ${err}`);
34904
35216
  });
34905
35217
  }
@@ -34907,13 +35219,13 @@ var CliLogger = class {
34907
35219
  async cleanupOldLogs() {
34908
35220
  const dir2 = this.dir;
34909
35221
  if (!dir2) return;
34910
- const files = await readdir(dir2).catch(() => []);
35222
+ const files = await readdir2(dir2).catch(() => []);
34911
35223
  const cutoff = Date.now() - this.retentionDays * DAY_MS;
34912
35224
  for (const file of files) {
34913
35225
  const match = /^cli-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(file);
34914
35226
  if (!match) continue;
34915
35227
  if ((/* @__PURE__ */ new Date(`${match[1]}T00:00:00Z`)).getTime() < cutoff) {
34916
- await unlink(join6(dir2, file)).catch(() => void 0);
35228
+ await unlink(join7(dir2, file)).catch(() => void 0);
34917
35229
  }
34918
35230
  }
34919
35231
  }
@@ -34924,7 +35236,7 @@ function readRecentLogLines(dir2, limit) {
34924
35236
  const files = readdirSync2(dir2).filter((f) => /^cli-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)).sort();
34925
35237
  const latest = files[files.length - 1];
34926
35238
  if (!latest) return [];
34927
- const lines = readFileSync6(join6(dir2, latest), "utf8").split("\n").filter(Boolean);
35239
+ const lines = readFileSync6(join7(dir2, latest), "utf8").split("\n").filter(Boolean);
34928
35240
  return lines.slice(-limit);
34929
35241
  } catch {
34930
35242
  return [];
@@ -34937,8 +35249,8 @@ function readLogUpload(dir2) {
34937
35249
  const latest = files[files.length - 1];
34938
35250
  if (!latest) return null;
34939
35251
  let totalBytes = 0;
34940
- for (const f of files) totalBytes += statSync(join6(dir2, f)).size;
34941
- let content = readFileSync6(join6(dir2, latest), "utf8");
35252
+ for (const f of files) totalBytes += statSync(join7(dir2, f)).size;
35253
+ let content = readFileSync6(join7(dir2, latest), "utf8");
34942
35254
  if (content.length > LOG_UPLOAD_CONTENT_MAX) {
34943
35255
  content = `\u2026[truncated]
34944
35256
  ${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
@@ -34981,7 +35293,7 @@ function classifyToolFailure(code, out) {
34981
35293
  }
34982
35294
  return { reason: "other", retryable: false };
34983
35295
  }
34984
- function run(cmd, args, onLine, timeoutMs = 18e4) {
35296
+ function run3(cmd, args, onLine, timeoutMs = 18e4) {
34985
35297
  return new Promise((resolve10) => {
34986
35298
  let out = "";
34987
35299
  let buf = "";
@@ -35021,13 +35333,13 @@ function run(cmd, args, onLine, timeoutMs = 18e4) {
35021
35333
  });
35022
35334
  }
35023
35335
  async function detectOne(cmd, versionArg) {
35024
- const { code, out } = await run(cmd, [versionArg], void 0, 15e3);
35336
+ const { code, out } = await run3(cmd, [versionArg], void 0, 15e3);
35025
35337
  if (code !== 0) return null;
35026
35338
  const first = out.split("\n").map((l) => l.trim()).find(Boolean);
35027
35339
  return first ?? "";
35028
35340
  }
35029
35341
  async function listExtras(autoUpdateSet = /* @__PURE__ */ new Set()) {
35030
- const { out } = await run("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
35342
+ const { out } = await run3("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
35031
35343
  if (!out.trim()) return [];
35032
35344
  let deps = {};
35033
35345
  try {
@@ -35103,7 +35415,7 @@ async function runWorkerToolOp(op, name, manager, onLine, timeoutMs = TOOL_INSTA
35103
35415
  return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
35104
35416
  }
35105
35417
  onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
35106
- const { code } = await run(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
35418
+ const { code } = await run3(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
35107
35419
  return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
35108
35420
  }
35109
35421
  function setToolAutoUpdate(profileDir2, name, enabled) {
@@ -35134,7 +35446,7 @@ async function installPinned(name, version, manager, onLine, timeoutMs = TOOL_IN
35134
35446
  for (let attempt = 1; attempt <= TOOL_INSTALL_MAX_ATTEMPTS; attempt++) {
35135
35447
  const suffix = attempt > 1 ? ` (attempt ${attempt}/${TOOL_INSTALL_MAX_ATTEMPTS})` : "";
35136
35448
  onLine(`$ ${manager} ${args.join(" ")}${suffix}`);
35137
- const { code, out } = await run(manager, args, onLine, timeoutMs);
35449
+ const { code, out } = await run3(manager, args, onLine, timeoutMs);
35138
35450
  if (code === 0) return { ok: true };
35139
35451
  const { reason, retryable } = classifyToolFailure(code, out);
35140
35452
  if (!retryable || attempt === TOOL_INSTALL_MAX_ATTEMPTS) {
@@ -35200,6 +35512,9 @@ var UpdateScheduler = class {
35200
35512
  lastDoneDateKey = null;
35201
35513
  /** Date key marked due but deferred because the cli was busy (stays set past the hour). */
35202
35514
  pendingDateKey = null;
35515
+ /** A push/connect-triggered update requested now (ADR-0289) — runs when idle, independent of the
35516
+ * daily policy; cleared once it runs. */
35517
+ forcedPending = false;
35203
35518
  /** Guards against a second update while one is in flight. */
35204
35519
  updating = false;
35205
35520
  /** Apply the latest policy (called on each ws_token) and start the tick timer once. */
@@ -35210,6 +35525,25 @@ var UpdateScheduler = class {
35210
35525
  this.timer.unref?.();
35211
35526
  }
35212
35527
  }
35528
+ /**
35529
+ * Request an update to latest NOW (ADR-0289) — the server's `cli.update` push or the
35530
+ * update-on-connect hook. Idle-aware like the daily tick (defers while a command runs) and
35531
+ * honours the local `autoUpdate:false` opt-out, but is independent of the daily org policy. Also
35532
+ * ensures the tick timer is running so the request is served even before the first ws_token.
35533
+ */
35534
+ updateNow() {
35535
+ if (this.optedOutLocally()) {
35536
+ logger.info("update.push.optedOut");
35537
+ return;
35538
+ }
35539
+ if (CLI_VERSION.startsWith("0.0.0")) return;
35540
+ this.forcedPending = true;
35541
+ if (!this.timer) {
35542
+ this.timer = setInterval(() => void this.tick(), TICK_MS);
35543
+ this.timer.unref?.();
35544
+ }
35545
+ void this.tick();
35546
+ }
35213
35547
  /** Stop the scheduler when the session ends (logout / replaced / stopped). */
35214
35548
  stop() {
35215
35549
  if (this.timer) {
@@ -35242,12 +35576,19 @@ var UpdateScheduler = class {
35242
35576
  return null;
35243
35577
  }
35244
35578
  }
35245
- /** One evaluation: mark the update due at the target hour, then update once idle. */
35579
+ /** One evaluation: run a forced (push/connect) update if pending, else the daily-scheduled one. */
35246
35580
  async tick() {
35247
- const p = this.policy;
35248
- if (!p || !p.autoUpdateDaily || this.updating) return;
35581
+ if (this.updating) return;
35249
35582
  if (CLI_VERSION.startsWith("0.0.0")) return;
35250
35583
  if (this.optedOutLocally()) return;
35584
+ if (this.forcedPending) {
35585
+ if (this.bus.busy !== null) return;
35586
+ this.forcedPending = false;
35587
+ await this.runUpdate();
35588
+ return;
35589
+ }
35590
+ const p = this.policy;
35591
+ if (!p || !p.autoUpdateDaily) return;
35251
35592
  const clock = this.orgClock();
35252
35593
  if (!clock) return;
35253
35594
  if (clock.dateKey === this.lastDoneDateKey) return;
@@ -35305,14 +35646,14 @@ var UpdateScheduler = class {
35305
35646
  };
35306
35647
 
35307
35648
  // src/core/ai-profile-state.ts
35308
- import { existsSync as existsSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
35309
- import { join as join7 } from "path";
35649
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
35650
+ import { join as join8 } from "path";
35310
35651
  function statePath(profileDir2) {
35311
- return join7(profileDir2, "ai-profiles.json");
35652
+ return join8(profileDir2, "ai-profiles.json");
35312
35653
  }
35313
35654
  function readState(profileDir2) {
35314
35655
  const path = statePath(profileDir2);
35315
- if (!existsSync5(path)) return {};
35656
+ if (!existsSync6(path)) return {};
35316
35657
  try {
35317
35658
  return JSON.parse(readFileSync7(path, "utf8"));
35318
35659
  } catch {
@@ -35370,7 +35711,7 @@ function clearPinnedCredential(profileDir2) {
35370
35711
  // src/utils/ai-cli.ts
35371
35712
  import { readFileSync as readFileSync8 } from "fs";
35372
35713
  import { homedir as homedir3 } from "os";
35373
- import { basename, isAbsolute, join as join8 } from "path";
35714
+ import { basename, isAbsolute, join as join9 } from "path";
35374
35715
  var REQUIRED_AI_ARGS = {
35375
35716
  claude: ["-p", "--output-format", "stream-json", "--verbose"],
35376
35717
  codex: ["exec", "--json"],
@@ -35507,9 +35848,9 @@ function claudeCredentialKeys(config) {
35507
35848
  return claudeProfilesOf(config).filter(isUsableProfile).map((p) => credentialKey("claude", resolveHomePath(p.profile)));
35508
35849
  }
35509
35850
  function resolveHomePath(value) {
35510
- if (value.startsWith("~")) return join8(homedir3(), value.slice(1).replace(/^[/\\]/, ""));
35851
+ if (value.startsWith("~")) return join9(homedir3(), value.slice(1).replace(/^[/\\]/, ""));
35511
35852
  if (isAbsolute(value)) return value;
35512
- return join8(homedir3(), value);
35853
+ return join9(homedir3(), value);
35513
35854
  }
35514
35855
  function profileDirs(profiles) {
35515
35856
  if (!profiles) return [];
@@ -35555,11 +35896,11 @@ function buildRunArgs(profile, cmd, resumeId, oneShot = false, override, readOnl
35555
35896
  }
35556
35897
  function claudeHomeDirs(config) {
35557
35898
  const dirs = profileDirs(claudeProfilesOf(config));
35558
- return dirs.length > 0 ? dirs : [join8(homedir3(), ".claude")];
35899
+ return dirs.length > 0 ? dirs : [join9(homedir3(), ".claude")];
35559
35900
  }
35560
35901
  function profileDisplayLabel(dir2) {
35561
35902
  try {
35562
- const raw = JSON.parse(readFileSync8(join8(dir2, ".claude.json"), "utf8"));
35903
+ const raw = JSON.parse(readFileSync8(join9(dir2, ".claude.json"), "utf8"));
35563
35904
  const email = raw.oauthAccount?.emailAddress;
35564
35905
  if (email && email.trim()) return email;
35565
35906
  } catch {
@@ -35661,11 +36002,11 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot, override, re
35661
36002
  // src/core/claude-usage.ts
35662
36003
  import { spawn as spawn3 } from "child_process";
35663
36004
  import { readFileSync as readFileSync9 } from "fs";
35664
- import { join as join9 } from "path";
36005
+ import { join as join10 } from "path";
35665
36006
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
35666
36007
  function readCreds(dir2) {
35667
36008
  try {
35668
- const raw = JSON.parse(readFileSync9(join9(dir2, ".credentials.json"), "utf8"));
36009
+ const raw = JSON.parse(readFileSync9(join10(dir2, ".credentials.json"), "utf8"));
35669
36010
  const oauth = raw.claudeAiOauth ?? raw;
35670
36011
  return oauth.accessToken ? oauth : null;
35671
36012
  } catch {
@@ -35774,8 +36115,8 @@ async function checkClaudeUsage(dirs, aiCli2 = "claude") {
35774
36115
  }
35775
36116
 
35776
36117
  // src/core/command-history.ts
35777
- import { existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
35778
- import { join as join10 } from "path";
36118
+ import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
36119
+ import { join as join11 } from "path";
35779
36120
  var MAX_ENTRIES = 1e3;
35780
36121
  var filePath = null;
35781
36122
  var entries = [];
@@ -35837,8 +36178,8 @@ async function uploadHistory(cfg) {
35837
36178
  }
35838
36179
  }
35839
36180
  function initCommandHistory(profileDir2, profileName, uploadMinutes = 10) {
35840
- filePath = join10(profileDir2, "command-history.json");
35841
- if (existsSync6(filePath)) {
36181
+ filePath = join11(profileDir2, "command-history.json");
36182
+ if (existsSync7(filePath)) {
35842
36183
  try {
35843
36184
  entries = JSON.parse(readFileSync10(filePath, "utf8"));
35844
36185
  } catch {
@@ -35894,16 +36235,16 @@ function finishCommand(commandId, exitCode, usage) {
35894
36235
  }
35895
36236
 
35896
36237
  // src/core/command-images.ts
35897
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
35898
- import { join as join11 } from "path";
36238
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
36239
+ import { join as join12 } from "path";
35899
36240
  function attachmentsBaseDir(physicRoot) {
35900
- return join11(physicRoot, ".4pm", "attachments");
36241
+ return join12(physicRoot, ".4pm", "attachments");
35901
36242
  }
35902
36243
  var ATTACHMENT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
35903
36244
  async function materializeImages(physicRoot, commandId, images, fetch2) {
35904
36245
  const map = /* @__PURE__ */ new Map();
35905
36246
  if (!images.length) return map;
35906
- const dir2 = join11(attachmentsBaseDir(physicRoot), commandId);
36247
+ const dir2 = join12(attachmentsBaseDir(physicRoot), commandId);
35907
36248
  mkdirSync3(dir2, { recursive: true });
35908
36249
  let n = 0;
35909
36250
  for (const img of images) {
@@ -35915,7 +36256,7 @@ async function materializeImages(physicRoot, commandId, images, fetch2) {
35915
36256
  continue;
35916
36257
  }
35917
36258
  const ext = commandImageExt(reply.mime ?? img.mime);
35918
- const file = join11(dir2, `img-${n}.${ext}`);
36259
+ const file = join12(dir2, `img-${n}.${ext}`);
35919
36260
  writeFileSync6(file, Buffer.from(reply.dataBase64, "base64"));
35920
36261
  map.set(img.placeholder, file);
35921
36262
  } catch (err) {
@@ -35937,12 +36278,12 @@ function rewriteImagePlaceholders(prompt, paths) {
35937
36278
  }
35938
36279
  function sweepOldAttachments(physicRoot, maxAgeMs = ATTACHMENT_MAX_AGE_MS) {
35939
36280
  const base = attachmentsBaseDir(physicRoot);
35940
- if (!existsSync7(base)) return 0;
36281
+ if (!existsSync8(base)) return 0;
35941
36282
  const cutoff = Date.now() - maxAgeMs;
35942
36283
  let removed = 0;
35943
36284
  try {
35944
36285
  for (const entry of readdirSync3(base)) {
35945
- const dir2 = join11(base, entry);
36286
+ const dir2 = join12(base, entry);
35946
36287
  try {
35947
36288
  if (statSync2(dir2).mtimeMs < cutoff) {
35948
36289
  rmSync3(dir2, { recursive: true, force: true });
@@ -35959,18 +36300,18 @@ function sweepOldAttachments(physicRoot, maxAgeMs = ATTACHMENT_MAX_AGE_MS) {
35959
36300
  // src/core/command-output-store.ts
35960
36301
  import {
35961
36302
  appendFileSync,
35962
- existsSync as existsSync8,
36303
+ existsSync as existsSync9,
35963
36304
  mkdirSync as mkdirSync4,
35964
36305
  readFileSync as readFileSync11,
35965
36306
  readdirSync as readdirSync4,
35966
36307
  rmSync as rmSync4,
35967
36308
  statSync as statSync3
35968
36309
  } from "fs";
35969
- import { join as join12 } from "path";
36310
+ import { join as join13 } from "path";
35970
36311
  var KEEP_FILES = 200;
35971
36312
  var dir = null;
35972
36313
  function initCommandOutput(profileDir2) {
35973
- dir = join12(profileDir2, "command-output");
36314
+ dir = join13(profileDir2, "command-output");
35974
36315
  try {
35975
36316
  mkdirSync4(dir, { recursive: true });
35976
36317
  prune();
@@ -35980,14 +36321,14 @@ function initCommandOutput(profileDir2) {
35980
36321
  function appendCommandOutput(commandId, text) {
35981
36322
  if (!dir || !text) return;
35982
36323
  try {
35983
- appendFileSync(join12(dir, `${commandId}.log`), text, "utf8");
36324
+ appendFileSync(join13(dir, `${commandId}.log`), text, "utf8");
35984
36325
  } catch {
35985
36326
  }
35986
36327
  }
35987
36328
  function readCommandOutput(commandId) {
35988
36329
  if (!dir) return null;
35989
- const path = join12(dir, `${commandId}.log`);
35990
- if (!existsSync8(path)) return null;
36330
+ const path = join13(dir, `${commandId}.log`);
36331
+ if (!existsSync9(path)) return null;
35991
36332
  try {
35992
36333
  return readFileSync11(path, "utf8");
35993
36334
  } catch {
@@ -35997,8 +36338,8 @@ function readCommandOutput(commandId) {
35997
36338
  function prune() {
35998
36339
  if (!dir) return;
35999
36340
  try {
36000
- const files = readdirSync4(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, t: statSync3(join12(dir, f)).mtimeMs })).sort((a, b) => b.t - a.t);
36001
- for (const { f } of files.slice(KEEP_FILES)) rmSync4(join12(dir, f), { force: true });
36341
+ const files = readdirSync4(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, t: statSync3(join13(dir, f)).mtimeMs })).sort((a, b) => b.t - a.t);
36342
+ for (const { f } of files.slice(KEEP_FILES)) rmSync4(join13(dir, f), { force: true });
36002
36343
  } catch {
36003
36344
  }
36004
36345
  }
@@ -36007,7 +36348,7 @@ function pruneCommandOutputByAge(days) {
36007
36348
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
36008
36349
  try {
36009
36350
  for (const f of readdirSync4(dir).filter((n) => n.endsWith(".log"))) {
36010
- const p = join12(dir, f);
36351
+ const p = join13(dir, f);
36011
36352
  if (statSync3(p).mtimeMs < cutoff) rmSync4(p, { force: true });
36012
36353
  }
36013
36354
  } catch {
@@ -36016,12 +36357,12 @@ function pruneCommandOutputByAge(days) {
36016
36357
 
36017
36358
  // src/core/support-answer.ts
36018
36359
  import { spawn as spawn4 } from "child_process";
36019
- import { execFile } from "child_process";
36360
+ import { execFile as execFile3 } from "child_process";
36020
36361
  import { createHash as createHash3 } from "crypto";
36021
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
36362
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
36022
36363
  import { randomUUID } from "crypto";
36023
- import { join as join13 } from "path";
36024
- import { promisify } from "util";
36364
+ import { join as join14 } from "path";
36365
+ import { promisify as promisify3 } from "util";
36025
36366
 
36026
36367
  // src/core/ai-stream.ts
36027
36368
  function assistantText(ev) {
@@ -36116,22 +36457,22 @@ function createAiStreamParser(cli) {
36116
36457
  apiError: () => ({ ...apiError })
36117
36458
  };
36118
36459
  }
36119
- function estimateTokens(text) {
36460
+ function estimateTokens2(text) {
36120
36461
  return Math.max(1, Math.ceil(text.length / 4));
36121
36462
  }
36122
36463
 
36123
36464
  // src/core/support-answer.ts
36124
36465
  var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
36125
- var execFileAsync = promisify(execFile);
36466
+ var execFileAsync = promisify3(execFile3);
36126
36467
  var MAX_CONTEXT_CHARS = 12e4;
36127
36468
  var ANSWER_TIMEOUT_MS = 12e4;
36128
36469
  var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
36129
36470
  function kbRoot(profileDir2) {
36130
- return join13(profileDir2, "support-kb");
36471
+ return join14(profileDir2, "support-kb");
36131
36472
  }
36132
36473
  function repoDir(profileDir2, url, branch) {
36133
36474
  const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
36134
- return join13(kbRoot(profileDir2), key);
36475
+ return join14(kbRoot(profileDir2), key);
36135
36476
  }
36136
36477
  function authedUrl(url, token) {
36137
36478
  if (!token) return url;
@@ -36140,7 +36481,7 @@ function authedUrl(url, token) {
36140
36481
  }
36141
36482
  async function ensureRepo(profileDir2, repo) {
36142
36483
  const dir2 = repoDir(profileDir2, repo.url, repo.branch);
36143
- if (existsSync9(join13(dir2, ".git"))) return dir2;
36484
+ if (existsSync10(join14(dir2, ".git"))) return dir2;
36144
36485
  const url = authedUrl(repo.url, repo.token);
36145
36486
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
36146
36487
  mkdirSync5(kbRoot(profileDir2), { recursive: true });
@@ -36151,11 +36492,11 @@ async function ensureRepo(profileDir2, repo) {
36151
36492
  }
36152
36493
  async function refreshSupportKb(profileDir2) {
36153
36494
  const root = kbRoot(profileDir2);
36154
- if (!existsSync9(root)) return;
36495
+ if (!existsSync10(root)) return;
36155
36496
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
36156
36497
  for (const entry of readdirSync5(root)) {
36157
- const dir2 = join13(root, entry);
36158
- if (!existsSync9(join13(dir2, ".git"))) continue;
36498
+ const dir2 = join14(root, entry);
36499
+ if (!existsSync10(join14(dir2, ".git"))) continue;
36159
36500
  try {
36160
36501
  await execFileAsync("git", ["-C", dir2, "pull", "--ff-only"], { env: gitEnv });
36161
36502
  } catch (err) {
@@ -36170,7 +36511,7 @@ function collectDocs(dir2) {
36170
36511
  if (total >= MAX_CONTEXT_CHARS) return;
36171
36512
  for (const entry of readdirSync5(current2)) {
36172
36513
  if (entry === ".git" || entry === "node_modules") continue;
36173
- const full = join13(current2, entry);
36514
+ const full = join14(current2, entry);
36174
36515
  const st = statSync4(full);
36175
36516
  if (st.isDirectory()) {
36176
36517
  walk2(full);
@@ -36235,11 +36576,11 @@ function imageExt(mime) {
36235
36576
  }
36236
36577
  }
36237
36578
  function materializeImages2(profileDir2, images, question) {
36238
- const dir2 = join13(profileDir2, "help-images", randomUUID());
36579
+ const dir2 = join14(profileDir2, "help-images", randomUUID());
36239
36580
  mkdirSync5(dir2, { recursive: true });
36240
36581
  let rewritten = question;
36241
36582
  images.forEach((img, i) => {
36242
- const file = join13(dir2, `image-${i + 1}.${imageExt(img.mime)}`);
36583
+ const file = join14(dir2, `image-${i + 1}.${imageExt(img.mime)}`);
36243
36584
  writeFileSync7(file, Buffer.from(img.dataBase64, "base64"));
36244
36585
  if (img.placeholder) rewritten = rewritten.split(img.placeholder).join(file);
36245
36586
  });
@@ -36334,7 +36675,7 @@ async function runSupportAnswer(req, ai, profileDir2) {
36334
36675
  if (!text) return { body: "", error: "empty answer" };
36335
36676
  const { body, moderation } = parseModeratedAnswer(text);
36336
36677
  if (!body) return { body: "", error: "empty answer" };
36337
- const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens(body) };
36678
+ const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens2(body) };
36338
36679
  return {
36339
36680
  body,
36340
36681
  ...moderation ? { moderation } : {},
@@ -36381,12 +36722,12 @@ function reportToolResult(tool, ok, message) {
36381
36722
  }
36382
36723
 
36383
36724
  // src/core/autonomous.ts
36384
- import { readFile as readFile2, writeFile, chmod } from "fs/promises";
36725
+ import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
36385
36726
  import { readFileSync as readFileSync13 } from "fs";
36386
- import { execFile as execFile2, spawn as spawn5 } from "child_process";
36387
- import { join as join14 } from "path";
36388
- import { promisify as promisify2 } from "util";
36389
- var run2 = promisify2(execFile2);
36727
+ import { execFile as execFile4, spawn as spawn5 } from "child_process";
36728
+ import { join as join15 } from "path";
36729
+ import { promisify as promisify4 } from "util";
36730
+ var run4 = promisify4(execFile4);
36390
36731
  var SETTINGS_REL = ".claude/.autonomous.settings.json";
36391
36732
  var HISTORIES_REL = ".claude/.autonomous.histories.json";
36392
36733
  var APPROVALS_REL = ".claude/.autonomous.approvals.json";
@@ -36419,11 +36760,11 @@ function tableCell(value) {
36419
36760
  return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
36420
36761
  }
36421
36762
  function tickScript(root) {
36422
- return join14(root, TICK_REL);
36763
+ return join15(root, TICK_REL);
36423
36764
  }
36424
36765
  async function crontabList() {
36425
36766
  try {
36426
- const { stdout } = await run2("crontab", ["-l"], { timeout: 1e4 });
36767
+ const { stdout } = await run4("crontab", ["-l"], { timeout: 1e4 });
36427
36768
  return stdout;
36428
36769
  } catch {
36429
36770
  return "";
@@ -36442,8 +36783,8 @@ async function cronInstalled(root) {
36442
36783
  return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
36443
36784
  }
36444
36785
  async function getAutonomousStatus(root) {
36445
- const settings = parseJson(await readText(join14(root, SETTINGS_REL))) ?? {};
36446
- const hist = parseJson(await readText(join14(root, HISTORIES_REL))) ?? {};
36786
+ const settings = parseJson(await readText(join15(root, SETTINGS_REL))) ?? {};
36787
+ const hist = parseJson(await readText(join15(root, HISTORIES_REL))) ?? {};
36447
36788
  const records = Array.isArray(hist.records) ? hist.records : [];
36448
36789
  const last = records[records.length - 1];
36449
36790
  const ticks = hist.ticks ?? {};
@@ -36460,9 +36801,9 @@ async function getAutonomousStatus(root) {
36460
36801
  }
36461
36802
  function isAutonomousRunning(root) {
36462
36803
  try {
36463
- const s = JSON.parse(readFileSync13(join14(root, SETTINGS_REL), "utf8"));
36804
+ const s = JSON.parse(readFileSync13(join15(root, SETTINGS_REL), "utf8"));
36464
36805
  if (s.paused === true) return false;
36465
- const h = JSON.parse(readFileSync13(join14(root, HISTORIES_REL), "utf8"));
36806
+ const h = JSON.parse(readFileSync13(join15(root, HISTORIES_REL), "utf8"));
36466
36807
  const ts = h.records?.[h.records.length - 1]?.ts;
36467
36808
  if (!ts) return false;
36468
36809
  const t2 = Date.parse(ts.replace(" ", "T"));
@@ -36473,10 +36814,10 @@ function isAutonomousRunning(root) {
36473
36814
  }
36474
36815
  async function readAutonomous(root) {
36475
36816
  const [settings, approvals, status, ...books] = await Promise.all([
36476
- readText(join14(root, SETTINGS_REL)),
36477
- readText(join14(root, APPROVALS_REL), "{}"),
36817
+ readText(join15(root, SETTINGS_REL)),
36818
+ readText(join15(root, APPROVALS_REL), "{}"),
36478
36819
  getAutonomousStatus(root),
36479
- ...Object.values(BOOK_FILES).map((f) => readText(join14(root, f)))
36820
+ ...Object.values(BOOK_FILES).map((f) => readText(join15(root, f)))
36480
36821
  ]);
36481
36822
  const keys = Object.keys(BOOK_FILES);
36482
36823
  const bookMap = {};
@@ -36485,7 +36826,7 @@ async function readAutonomous(root) {
36485
36826
  }
36486
36827
  async function readAutonomousLogs(root, date) {
36487
36828
  const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
36488
- const text = await readText(join14(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
36829
+ const text = await readText(join15(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
36489
36830
  const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
36490
36831
  return { date: day, lines };
36491
36832
  }
@@ -36509,22 +36850,22 @@ async function writeAutonomous(root, req, by) {
36509
36850
  switch (req.kind) {
36510
36851
  case "settings": {
36511
36852
  if (!parseJson(req.settings)) return { ok: false, error: "settings is not valid JSON" };
36512
- await writeFile(join14(root, SETTINGS_REL), req.settings, "utf8");
36853
+ await writeFile2(join15(root, SETTINGS_REL), req.settings, "utf8");
36513
36854
  break;
36514
36855
  }
36515
36856
  case "approvals": {
36516
- const map = parseJson(await readText(join14(root, APPROVALS_REL), "{}")) ?? {};
36857
+ const map = parseJson(await readText(join15(root, APPROVALS_REL), "{}")) ?? {};
36517
36858
  if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
36518
36859
  else delete map[req.taskId];
36519
- await writeFile(join14(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
36860
+ await writeFile2(join15(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
36520
36861
  break;
36521
36862
  }
36522
36863
  case "userTodo": {
36523
- const cur = await readText(join14(root, BOOK_FILES.userTodo));
36864
+ const cur = await readText(join15(root, BOOK_FILES.userTodo));
36524
36865
  const trace = `posted by ${tableCell(by)} at ${(/* @__PURE__ */ new Date()).toISOString()}`;
36525
36866
  const row = `| | ${tableCell(req.content)} | ${trace} |
36526
36867
  `;
36527
- await writeFile(join14(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
36868
+ await writeFile2(join15(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
36528
36869
  break;
36529
36870
  }
36530
36871
  case "cron": {
@@ -36540,7 +36881,7 @@ async function writeAutonomous(root, req, by) {
36540
36881
  }
36541
36882
 
36542
36883
  // src/core/network-probe.ts
36543
- import { existsSync as existsSync10, readFileSync as readFileSync14 } from "fs";
36884
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
36544
36885
  import { connect } from "net";
36545
36886
  var CANARY_TARGETS = [
36546
36887
  { host: "1.1.1.1", port: 443 },
@@ -36549,7 +36890,7 @@ var CANARY_TARGETS = [
36549
36890
  var PROBE_TIMEOUT_MS = 2500;
36550
36891
  function detectContainerized() {
36551
36892
  try {
36552
- if (existsSync10("/.dockerenv")) return true;
36893
+ if (existsSync11("/.dockerenv")) return true;
36553
36894
  const cgroup = readFileSync14("/proc/1/cgroup", "utf8");
36554
36895
  return /docker|containerd|kubepods|libpod/.test(cgroup);
36555
36896
  } catch {
@@ -36619,7 +36960,7 @@ async function probeNetwork() {
36619
36960
  // src/core/git-auth.ts
36620
36961
  import { writeFileSync as writeFileSync8 } from "fs";
36621
36962
  import { homedir as homedir4 } from "os";
36622
- import { join as join15 } from "path";
36963
+ import { join as join16 } from "path";
36623
36964
  function tokenTarget(method) {
36624
36965
  if (method === "gitlab-group-token") {
36625
36966
  return { host: process.env.FOURPM_GIT_HOST || "gitlab.com", user: "oauth2" };
@@ -36637,7 +36978,7 @@ function applyGitAuth(method) {
36637
36978
  try {
36638
36979
  const line = `https://${target.user}:${token}@${target.host}
36639
36980
  `;
36640
- writeFileSync8(join15(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
36981
+ writeFileSync8(join16(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
36641
36982
  return true;
36642
36983
  } catch {
36643
36984
  return false;
@@ -36822,7 +37163,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
36822
37163
  const apiErr = parser.apiError();
36823
37164
  if (finalExit === 0 && !apiErr.isError) {
36824
37165
  const usage = parser.usage();
36825
- if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
37166
+ if (usage.tokens === 0) usage.tokens = estimateTokens2(captured);
36826
37167
  reportToolResult(attempt.cmd, true);
36827
37168
  return {
36828
37169
  exitCode: 0,
@@ -36874,8 +37215,8 @@ function formatTimestampInZone(date, timeZone) {
36874
37215
  }
36875
37216
 
36876
37217
  // src/ui/slash-commands.ts
36877
- import { existsSync as existsSync11 } from "fs";
36878
- import { join as join16 } from "path";
37218
+ import { existsSync as existsSync12 } from "fs";
37219
+ import { join as join17 } from "path";
36879
37220
  var CONFIG_KEYS = [
36880
37221
  "aiCli",
36881
37222
  "claudeHome",
@@ -36887,7 +37228,7 @@ var CONFIG_KEYS = [
36887
37228
  "aiEnv"
36888
37229
  ];
36889
37230
  function configPath(profileDir2) {
36890
- return join16(profileDir2, "config.json");
37231
+ return join17(profileDir2, "config.json");
36891
37232
  }
36892
37233
  function parseConfigAssignment(key, valueTokens) {
36893
37234
  const value = valueTokens.join(" ").trim();
@@ -36937,7 +37278,7 @@ function runConfig(ctx) {
36937
37278
  ctx.print(t("slash.config.wroteDefault", { verb, path }));
36938
37279
  ctx.print(t("slash.config.addClaudeHint"));
36939
37280
  };
36940
- if (existsSync11(path)) {
37281
+ if (existsSync12(path)) {
36941
37282
  ctx.confirm(
36942
37283
  t("slash.config.replaceConfirm", { path }),
36943
37284
  () => writeDefaults(t("slash.config.verbReplaced"))
@@ -37022,7 +37363,7 @@ function runOutput(ctx) {
37022
37363
  }
37023
37364
  function runLogs(ctx) {
37024
37365
  const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
37025
- const dir2 = join16(ctx.info.profileDir, "logs");
37366
+ const dir2 = join17(ctx.info.profileDir, "logs");
37026
37367
  const lines = readRecentLogLines(dir2, n);
37027
37368
  if (lines.length === 0) {
37028
37369
  ctx.print(t("slash.logs.none"));
@@ -37337,13 +37678,13 @@ async function runMemoryCompaction(ai, cwd2, input) {
37337
37678
  }
37338
37679
 
37339
37680
  // src/core/git-commit-identity.ts
37340
- import { execFile as execFile3 } from "child_process";
37341
- import { promisify as promisify3 } from "util";
37342
- var run3 = promisify3(execFile3);
37681
+ import { execFile as execFile5 } from "child_process";
37682
+ import { promisify as promisify5 } from "util";
37683
+ var run5 = promisify5(execFile5);
37343
37684
  async function setCommitAuthor(cwd2, machineUsername) {
37344
37685
  if (!machineUsername) return;
37345
37686
  try {
37346
- await run3("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
37687
+ await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
37347
37688
  } catch {
37348
37689
  }
37349
37690
  }
@@ -37442,7 +37783,7 @@ async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, imag
37442
37783
  const aiRunTimeoutMs = timeoutSec > 0 ? timeoutSec * 1e3 : 0;
37443
37784
  const perPromptLimit = config.perPromptTokenLimit ?? 0;
37444
37785
  if (perPromptLimit > 0) {
37445
- const estimated = estimateTokens(prompt);
37786
+ const estimated = estimateTokens2(prompt);
37446
37787
  if (estimated > perPromptLimit) {
37447
37788
  rejectPromptOverLimit(ctx, commandId, origin, prompt, estimated, perPromptLimit);
37448
37789
  return;
@@ -37859,15 +38200,15 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
37859
38200
  }
37860
38201
 
37861
38202
  // src/core/fs-browse.ts
37862
- import { readdir as readdir2 } from "fs/promises";
37863
- import { resolve, sep as sep2 } from "path";
38203
+ import { readdir as readdir3 } from "fs/promises";
38204
+ import { resolve as resolve2, sep as sep2 } from "path";
37864
38205
  async function listDir(path, root) {
37865
38206
  if (!root) return { path: "", entries: [] };
37866
- const base = resolve(root);
37867
- const requested = resolve(base, path && path.trim() ? path : ".");
38207
+ const base = resolve2(root);
38208
+ const requested = resolve2(base, path && path.trim() ? path : ".");
37868
38209
  const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
37869
38210
  try {
37870
- const dirents = await readdir2(target, { withFileTypes: true });
38211
+ const dirents = await readdir3(target, { withFileTypes: true });
37871
38212
  const entries2 = dirents.map((d) => ({
37872
38213
  name: d.name,
37873
38214
  type: d.isDirectory() ? "dir" : "file"
@@ -37883,12 +38224,12 @@ async function listDir(path, root) {
37883
38224
 
37884
38225
  // src/core/fs-read.ts
37885
38226
  import { readFile as readFile3 } from "fs/promises";
37886
- import { resolve as resolve2, sep as sep3 } from "path";
38227
+ import { resolve as resolve3, sep as sep3 } from "path";
37887
38228
  var MAX_BYTES = 256 * 1024;
37888
38229
  function resolveInRoot(root, relPath) {
37889
38230
  if (!root) return null;
37890
- const base = resolve2(root);
37891
- const target = resolve2(base, relPath || ".");
38231
+ const base = resolve3(root);
38232
+ const target = resolve3(base, relPath || ".");
37892
38233
  return target === base || target.startsWith(base + sep3) ? target : null;
37893
38234
  }
37894
38235
  async function readWorkerFile(root, path) {
@@ -37907,13 +38248,13 @@ async function readWorkerFile(root, path) {
37907
38248
  }
37908
38249
 
37909
38250
  // src/core/fs-write.ts
37910
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
37911
- import { dirname as dirname3, resolve as resolve3, sep as sep4 } from "path";
38251
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
38252
+ import { dirname as dirname4, resolve as resolve4, sep as sep4 } from "path";
37912
38253
  var MAX_BYTES2 = 2 * 1024 * 1024;
37913
38254
  function resolveInRoot2(root, relPath) {
37914
38255
  if (!root) return null;
37915
- const base = resolve3(root);
37916
- const target = resolve3(base, relPath || ".");
38256
+ const base = resolve4(root);
38257
+ const target = resolve4(base, relPath || ".");
37917
38258
  return target === base || target.startsWith(base + sep4) ? target : null;
37918
38259
  }
37919
38260
  async function writeWorkerFile(root, relPath, content) {
@@ -37926,8 +38267,8 @@ async function writeWorkerFile(root, relPath, content) {
37926
38267
  return { ok: false, path: target, bytes: 0, error: `content too large (max ${MAX_BYTES2} bytes)` };
37927
38268
  }
37928
38269
  try {
37929
- await mkdir2(dirname3(target), { recursive: true });
37930
- await writeFile2(target, content, "utf8");
38270
+ await mkdir3(dirname4(target), { recursive: true });
38271
+ await writeFile3(target, content, "utf8");
37931
38272
  return { ok: true, path: target, bytes };
37932
38273
  } catch (err) {
37933
38274
  return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
@@ -37935,12 +38276,12 @@ async function writeWorkerFile(root, relPath, content) {
37935
38276
  }
37936
38277
 
37937
38278
  // src/core/fs-mutate.ts
37938
- import { mkdir as mkdir3, writeFile as writeFile3, rename, rm, stat } from "fs/promises";
37939
- import { basename as basename3, dirname as dirname4, resolve as resolve4, sep as sep5 } from "path";
38279
+ import { mkdir as mkdir4, writeFile as writeFile4, rename as rename2, rm as rm2, stat } from "fs/promises";
38280
+ import { basename as basename3, dirname as dirname5, resolve as resolve5, sep as sep5 } from "path";
37940
38281
  var MAX_BYTES3 = 2 * 1024 * 1024;
37941
38282
  function resolveInRoot3(root, rel) {
37942
- const base = resolve4(root);
37943
- const target = resolve4(base, rel || ".");
38283
+ const base = resolve5(root);
38284
+ const target = resolve5(base, rel || ".");
37944
38285
  return target === base || target.startsWith(base + sep5) ? target : null;
37945
38286
  }
37946
38287
  async function isDir(p) {
@@ -37965,39 +38306,39 @@ async function mutateFs(root, req) {
37965
38306
  switch (req.op) {
37966
38307
  case "mkdir": {
37967
38308
  const target = resolveInRoot3(root, req.path ?? "");
37968
- if (!target || target === resolve4(root)) return escaped(req.path ?? "");
37969
- await mkdir3(target, { recursive: true });
38309
+ if (!target || target === resolve5(root)) return escaped(req.path ?? "");
38310
+ await mkdir4(target, { recursive: true });
37970
38311
  return { ok: true, path: target };
37971
38312
  }
37972
38313
  case "create": {
37973
38314
  const target = resolveInRoot3(root, req.path ?? "");
37974
- if (!target || target === resolve4(root)) return escaped(req.path ?? "");
38315
+ if (!target || target === resolve5(root)) return escaped(req.path ?? "");
37975
38316
  if (await exists(target)) return { ok: false, path: target, error: "already exists" };
37976
38317
  const content = req.content ?? "";
37977
38318
  if (Buffer.byteLength(content, "utf8") > MAX_BYTES3) {
37978
38319
  return { ok: false, path: target, error: `content too large (max ${MAX_BYTES3} bytes)` };
37979
38320
  }
37980
- await mkdir3(dirname4(target), { recursive: true });
37981
- await writeFile3(target, content, "utf8");
38321
+ await mkdir4(dirname5(target), { recursive: true });
38322
+ await writeFile4(target, content, "utf8");
37982
38323
  return { ok: true, path: target };
37983
38324
  }
37984
38325
  case "move": {
37985
38326
  const from = resolveInRoot3(root, req.from ?? "");
37986
38327
  let to = resolveInRoot3(root, req.to ?? "");
37987
38328
  if (!from || !to) return escaped(req.from ?? req.to ?? "");
37988
- if (from === resolve4(root)) return { ok: false, path: from, error: "cannot move the project root" };
37989
- if (await isDir(to)) to = resolve4(to, basename3(from));
37990
- if (to !== resolve4(root) && !to.startsWith(resolve4(root) + sep5)) return escaped(req.to ?? "");
38329
+ if (from === resolve5(root)) return { ok: false, path: from, error: "cannot move the project root" };
38330
+ if (await isDir(to)) to = resolve5(to, basename3(from));
38331
+ if (to !== resolve5(root) && !to.startsWith(resolve5(root) + sep5)) return escaped(req.to ?? "");
37991
38332
  if (await exists(to)) return { ok: false, path: to, error: "destination already exists" };
37992
- await mkdir3(dirname4(to), { recursive: true });
37993
- await rename(from, to);
38333
+ await mkdir4(dirname5(to), { recursive: true });
38334
+ await rename2(from, to);
37994
38335
  return { ok: true, path: to };
37995
38336
  }
37996
38337
  case "delete": {
37997
38338
  const target = resolveInRoot3(root, req.path ?? "");
37998
38339
  if (!target) return escaped(req.path ?? "");
37999
- if (target === resolve4(root)) return { ok: false, path: target, error: "cannot delete the project root" };
38000
- await rm(target, { recursive: true, force: true });
38340
+ if (target === resolve5(root)) return { ok: false, path: target, error: "cannot delete the project root" };
38341
+ await rm2(target, { recursive: true, force: true });
38001
38342
  return { ok: true, path: target };
38002
38343
  }
38003
38344
  default:
@@ -38009,12 +38350,12 @@ async function mutateFs(root, req) {
38009
38350
  }
38010
38351
 
38011
38352
  // src/core/fs-transfer.ts
38012
- import { mkdir as mkdir4, readFile as readFile4, stat as stat2, writeFile as writeFile4 } from "fs/promises";
38013
- import { basename as basename4, dirname as dirname5, extname, resolve as resolve5, sep as sep6 } from "path";
38353
+ import { mkdir as mkdir5, readFile as readFile4, stat as stat2, writeFile as writeFile5 } from "fs/promises";
38354
+ import { basename as basename4, dirname as dirname6, extname, resolve as resolve6, sep as sep6 } from "path";
38014
38355
  function resolveInRoot4(root, relPath) {
38015
38356
  if (!root) return null;
38016
- const base = resolve5(root);
38017
- const target = resolve5(base, relPath || ".");
38357
+ const base = resolve6(root);
38358
+ const target = resolve6(base, relPath || ".");
38018
38359
  return target === base || target.startsWith(base + sep6) ? target : null;
38019
38360
  }
38020
38361
  var MIME_BY_EXT = {
@@ -38053,8 +38394,8 @@ async function uploadBinaryFile(root, relPath, contentBase64) {
38053
38394
  return { ok: false, path: target, bytes: 0, error: `file too large (max ${FS_TRANSFER_MAX_BYTES} bytes)` };
38054
38395
  }
38055
38396
  try {
38056
- await mkdir4(dirname5(target), { recursive: true });
38057
- await writeFile4(target, buf);
38397
+ await mkdir5(dirname6(target), { recursive: true });
38398
+ await writeFile5(target, buf);
38058
38399
  return { ok: true, path: target, bytes: buf.byteLength };
38059
38400
  } catch (err) {
38060
38401
  return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
@@ -38129,14 +38470,14 @@ function handleFsChannels(ctx, message, payload) {
38129
38470
 
38130
38471
  // src/core/git-diff.ts
38131
38472
  import { readFile as readFile5 } from "fs/promises";
38132
- import { execFile as execFile4 } from "child_process";
38133
- import { basename as basename5, dirname as dirname6, resolve as resolve6, sep as sep7 } from "path";
38134
- import { promisify as promisify4 } from "util";
38135
- var run4 = promisify4(execFile4);
38473
+ import { execFile as execFile6 } from "child_process";
38474
+ import { basename as basename5, dirname as dirname7, resolve as resolve7, sep as sep7 } from "path";
38475
+ import { promisify as promisify6 } from "util";
38476
+ var run6 = promisify6(execFile6);
38136
38477
  function resolveInRoot5(root, relPath) {
38137
38478
  if (!root) return null;
38138
- const base = resolve6(root);
38139
- const target = resolve6(base, relPath || ".");
38479
+ const base = resolve7(root);
38480
+ const target = resolve7(base, relPath || ".");
38140
38481
  return target === base || target.startsWith(base + sep7) ? target : null;
38141
38482
  }
38142
38483
  function looksBinary(buf) {
@@ -38151,7 +38492,7 @@ async function gitDiff(root, path) {
38151
38492
  if (!target) {
38152
38493
  return { path, oldContent: "", newContent: "", isBinary: false };
38153
38494
  }
38154
- const dir2 = dirname6(target);
38495
+ const dir2 = dirname7(target);
38155
38496
  const base = basename5(target);
38156
38497
  let newBuf = null;
38157
38498
  try {
@@ -38160,7 +38501,7 @@ async function gitDiff(root, path) {
38160
38501
  }
38161
38502
  let oldBuf = null;
38162
38503
  try {
38163
- const { stdout } = await run4("git", ["show", `HEAD:./${base}`], {
38504
+ const { stdout } = await run6("git", ["show", `HEAD:./${base}`], {
38164
38505
  cwd: dir2,
38165
38506
  timeout: 15e3,
38166
38507
  maxBuffer: FS_TRANSFER_MAX_BYTES + 1024,
@@ -38190,12 +38531,12 @@ async function gitDiff(root, path) {
38190
38531
  }
38191
38532
 
38192
38533
  // src/core/git-env.ts
38193
- import { execFile as execFile5 } from "child_process";
38194
- import { promisify as promisify5 } from "util";
38195
- var run5 = promisify5(execFile5);
38534
+ import { execFile as execFile7 } from "child_process";
38535
+ import { promisify as promisify7 } from "util";
38536
+ var run7 = promisify7(execFile7);
38196
38537
  async function hasBinary(bin, args = ["--version"]) {
38197
38538
  try {
38198
- await run5(bin, args, { timeout: 5e3 });
38539
+ await run7(bin, args, { timeout: 5e3 });
38199
38540
  return true;
38200
38541
  } catch {
38201
38542
  return false;
@@ -38208,7 +38549,7 @@ async function checkGitEnv(provider) {
38208
38549
  let account = null;
38209
38550
  if (installed) {
38210
38551
  try {
38211
- const { stdout, stderr } = await run5(bin, ["auth", "status"], { timeout: 8e3 });
38552
+ const { stdout, stderr } = await run7(bin, ["auth", "status"], { timeout: 8e3 });
38212
38553
  const out = `${stdout}
38213
38554
  ${stderr}`;
38214
38555
  authenticated = /logged in|logged into|✓/i.test(out);
@@ -38221,14 +38562,14 @@ ${stderr}`;
38221
38562
  }
38222
38563
 
38223
38564
  // src/core/git-ssh-key.ts
38224
- import { execFile as execFile6 } from "child_process";
38225
- import { readFile as readFile6, rm as rm2, mkdir as mkdir5 } from "fs/promises";
38226
- import { join as join17 } from "path";
38227
- import { promisify as promisify6 } from "util";
38228
- var run6 = promisify6(execFile6);
38565
+ import { execFile as execFile8 } from "child_process";
38566
+ import { readFile as readFile6, rm as rm3, mkdir as mkdir6 } from "fs/promises";
38567
+ import { join as join18 } from "path";
38568
+ import { promisify as promisify8 } from "util";
38569
+ var run8 = promisify8(execFile8);
38229
38570
  function keyPaths(profileDir2) {
38230
- const dir2 = join17(profileDir2, ".ssh");
38231
- const key = join17(dir2, "id_4pm");
38571
+ const dir2 = join18(profileDir2, ".ssh");
38572
+ const key = join18(dir2, "id_4pm");
38232
38573
  return { dir: dir2, key, pub: `${key}.pub` };
38233
38574
  }
38234
38575
  async function readPublicKey(pub) {
@@ -38240,14 +38581,14 @@ async function readPublicKey(pub) {
38240
38581
  }
38241
38582
  async function fingerprintOf(pub) {
38242
38583
  try {
38243
- const { stdout } = await run6("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
38584
+ const { stdout } = await run8("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
38244
38585
  return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
38245
38586
  } catch {
38246
38587
  return null;
38247
38588
  }
38248
38589
  }
38249
38590
  async function configureGit(key) {
38250
- await run6(
38591
+ await run8(
38251
38592
  "git",
38252
38593
  ["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
38253
38594
  { timeout: 5e3 }
@@ -38255,10 +38596,10 @@ async function configureGit(key) {
38255
38596
  }
38256
38597
  async function generate(profileDir2) {
38257
38598
  const { dir: dir2, key, pub } = keyPaths(profileDir2);
38258
- await mkdir5(dir2, { recursive: true });
38259
- await rm2(key, { force: true });
38260
- await rm2(pub, { force: true });
38261
- await run6("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
38599
+ await mkdir6(dir2, { recursive: true });
38600
+ await rm3(key, { force: true });
38601
+ await rm3(pub, { force: true });
38602
+ await run8("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
38262
38603
  timeout: 15e3
38263
38604
  });
38264
38605
  await configureGit(key);
@@ -38266,9 +38607,9 @@ async function generate(profileDir2) {
38266
38607
  }
38267
38608
  async function remove(profileDir2) {
38268
38609
  const { key, pub } = keyPaths(profileDir2);
38269
- await rm2(key, { force: true });
38270
- await rm2(pub, { force: true });
38271
- await run6("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
38610
+ await rm3(key, { force: true });
38611
+ await rm3(pub, { force: true });
38612
+ await run8("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
38272
38613
  () => void 0
38273
38614
  );
38274
38615
  return { publicKey: null, fingerprint: null };
@@ -38289,22 +38630,22 @@ async function manageSshKey(op, profileDir2) {
38289
38630
  }
38290
38631
 
38291
38632
  // src/core/git-history.ts
38292
- import { readdir as readdir3 } from "fs/promises";
38293
- import { execFile as execFile7 } from "child_process";
38294
- import { resolve as resolve7, sep as sep8 } from "path";
38295
- import { promisify as promisify7 } from "util";
38296
- var run7 = promisify7(execFile7);
38633
+ import { readdir as readdir4 } from "fs/promises";
38634
+ import { execFile as execFile9 } from "child_process";
38635
+ import { resolve as resolve8, sep as sep8 } from "path";
38636
+ import { promisify as promisify9 } from "util";
38637
+ var run9 = promisify9(execFile9);
38297
38638
  var FS = "";
38298
38639
  var RS = "";
38299
38640
  function resolveRepo(root, repo) {
38300
38641
  if (!root) return null;
38301
- const base = resolve7(root);
38302
- const requested = resolve7(base, repo && repo.trim() ? repo : ".");
38642
+ const base = resolve8(root);
38643
+ const requested = resolve8(base, repo && repo.trim() ? repo : ".");
38303
38644
  return requested === base || requested.startsWith(base + sep8) ? requested : base;
38304
38645
  }
38305
38646
  async function git(cwd2, args) {
38306
38647
  try {
38307
- const { stdout } = await run7("git", args, {
38648
+ const { stdout } = await run9("git", args, {
38308
38649
  cwd: cwd2,
38309
38650
  timeout: 15e3,
38310
38651
  maxBuffer: 8 * 1024 * 1024
@@ -38316,7 +38657,7 @@ async function git(cwd2, args) {
38316
38657
  }
38317
38658
  async function gitRepos(root) {
38318
38659
  if (!root) return { repos: [] };
38319
- const base = resolve7(root);
38660
+ const base = resolve8(root);
38320
38661
  const found = /* @__PURE__ */ new Set();
38321
38662
  const repos = [];
38322
38663
  const add = async (dir2) => {
@@ -38330,16 +38671,16 @@ async function gitRepos(root) {
38330
38671
  };
38331
38672
  await add(base);
38332
38673
  try {
38333
- const lvl1 = await readdir3(base, { withFileTypes: true });
38674
+ const lvl1 = await readdir4(base, { withFileTypes: true });
38334
38675
  for (const d of lvl1) {
38335
38676
  if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
38336
- const p1 = resolve7(base, d.name);
38677
+ const p1 = resolve8(base, d.name);
38337
38678
  await add(p1);
38338
38679
  try {
38339
- const lvl2 = await readdir3(p1, { withFileTypes: true });
38680
+ const lvl2 = await readdir4(p1, { withFileTypes: true });
38340
38681
  for (const d2 of lvl2) {
38341
38682
  if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
38342
- await add(resolve7(p1, d2.name));
38683
+ await add(resolve8(p1, d2.name));
38343
38684
  }
38344
38685
  } catch {
38345
38686
  }
@@ -38457,79 +38798,7 @@ function handleGitChannels(ctx, message, payload) {
38457
38798
  }
38458
38799
 
38459
38800
  // src/core/ws-client/handlers/misc.ts
38460
- import { join as join18 } from "path";
38461
-
38462
- // src/core/ai-assist.ts
38463
- import { execFile as execFile8 } from "child_process";
38464
- import { promisify as promisify8 } from "util";
38465
- var run8 = promisify8(execFile8);
38466
- function aiCli() {
38467
- return process.env.AI_CLI || "claude";
38468
- }
38469
- function aiArgs(cli, prompt) {
38470
- if (cli === "claude") return ["-p", prompt, "--output-format", "json"];
38471
- if (cli === "codex") return ["exec", "--json", prompt];
38472
- return ["-p", prompt];
38473
- }
38474
- function estimateTokens2(prompt, output) {
38475
- return Math.max(1, Math.ceil((prompt.length + output.length) / 4));
38476
- }
38477
- function parseAi(cli, stdout, prompt) {
38478
- if (cli === "claude") {
38479
- try {
38480
- const j = JSON.parse(stdout);
38481
- const text2 = (j.result ?? "").trim();
38482
- const tokens = (j.usage?.input_tokens ?? 0) + (j.usage?.output_tokens ?? 0);
38483
- if (text2 || tokens) return { text: text2, tokens: tokens || estimateTokens2(prompt, text2) };
38484
- } catch {
38485
- }
38486
- }
38487
- if (cli === "codex") {
38488
- try {
38489
- let text2 = "";
38490
- let tokens = 0;
38491
- for (const line of stdout.split("\n").filter(Boolean)) {
38492
- const obj = JSON.parse(line);
38493
- if (obj.text) text2 = obj.text;
38494
- else if (obj.message) text2 = obj.message;
38495
- if (obj.usage) {
38496
- tokens = obj.usage.total_tokens ?? (obj.usage.input_tokens ?? 0) + (obj.usage.output_tokens ?? 0);
38497
- }
38498
- }
38499
- if (text2 || tokens) return { text: text2.trim(), tokens: tokens || estimateTokens2(prompt, text2) };
38500
- } catch {
38501
- }
38502
- }
38503
- const text = stdout.trim();
38504
- return { text, tokens: estimateTokens2(prompt, text) };
38505
- }
38506
- async function runAi(prompt) {
38507
- const cli = aiCli();
38508
- const { stdout } = await run8(cli, aiArgs(cli, prompt), {
38509
- timeout: 12e4,
38510
- maxBuffer: 8 * 1024 * 1024
38511
- });
38512
- return parseAi(cli, stdout, prompt);
38513
- }
38514
- async function aiGenerate(prompt) {
38515
- const { text } = await runAi(prompt);
38516
- return text;
38517
- }
38518
- async function aiOutboundReview(input, allowedRepos, projectPath) {
38519
- const prompt = `You are a security reviewer for an autonomous coding agent. Decide if the following INPUT is SAFE to run. It is UNSAFE if it: (1) asks the AI using secret VALUES (api keys, env values, passwords); (2) would affect other environments (only the sandbox project is allowed, no ssh/deploy/prod); (3) edits outside the project folder "${projectPath}"; (4) commits/pushes to a repo not in this allowlist: ${allowedRepos.length ? allowedRepos.join(", ") : "(none declared)"}. Reply with ONLY JSON: {"ok": <true|false>, "reasons": ["secret"|"environment"|"path"|"repo", ...]}. INPUT:
38520
- ${input}`;
38521
- const { text, tokens } = await runAi(prompt);
38522
- const start = text.indexOf("{");
38523
- const end = text.lastIndexOf("}");
38524
- if (start < 0 || end <= start) return { ok: false, reasons: ["ai_parse"], tokens };
38525
- try {
38526
- const j = JSON.parse(text.slice(start, end + 1));
38527
- const reasons = Array.isArray(j.reasons) ? j.reasons.filter((r) => typeof r === "string") : [];
38528
- return { ok: j.ok === true && reasons.length === 0, reasons, tokens };
38529
- } catch {
38530
- return { ok: false, reasons: ["ai_parse"], tokens };
38531
- }
38532
- }
38801
+ import { join as join19 } from "path";
38533
38802
 
38534
38803
  // src/core/outbound-review.ts
38535
38804
  function compilePatterns(sources, fallback) {
@@ -38605,9 +38874,13 @@ async function evaluateReview(payload) {
38605
38874
  // src/core/ws-client/handlers/misc.ts
38606
38875
  function handleMiscChannels(ctx, message, payload) {
38607
38876
  switch (message.channel) {
38877
+ case WsChannels.CLI_UPDATE: {
38878
+ ctx.updateCliNow();
38879
+ return true;
38880
+ }
38608
38881
  case WsChannels.LOG_READ: {
38609
38882
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
38610
- const lines = readRecentLogLines(join18(ctx.profileDir, "logs"), limit);
38883
+ const lines = readRecentLogLines(join19(ctx.profileDir, "logs"), limit);
38611
38884
  ctx.send(WsChannels.LOG_READ, { lines }, message.id);
38612
38885
  return true;
38613
38886
  }
@@ -38640,161 +38913,6 @@ function handleMiscChannels(ctx, message, payload) {
38640
38913
  // src/core/ws-client/handlers/project.ts
38641
38914
  import { existsSync as existsSync13, mkdirSync as mkdirSync6, renameSync, rmSync as rmSync6 } from "fs";
38642
38915
  import { join as join20 } from "path";
38643
-
38644
- // src/core/scaffold.ts
38645
- import { cp, mkdir as mkdir6, writeFile as writeFile5 } from "fs/promises";
38646
- import { existsSync as existsSync12 } from "fs";
38647
- import { execFile as execFile9 } from "child_process";
38648
- import { dirname as dirname7, join as join19, resolve as resolve8 } from "path";
38649
- import { fileURLToPath as fileURLToPath3 } from "url";
38650
- import { promisify as promisify9 } from "util";
38651
- var run9 = promisify9(execFile9);
38652
- function reposOf(spec) {
38653
- const repos = spec?.repos;
38654
- return Array.isArray(repos) ? repos : [];
38655
- }
38656
- function subDirName(repo) {
38657
- const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
38658
- return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
38659
- }
38660
- async function provisionRepos(target, repos, emit, opts = {}) {
38661
- for (const repo of repos) {
38662
- if (repo.primary) {
38663
- if (opts.clonePrimary && repo.url) {
38664
- if (existsSync12(join19(target, ".git"))) {
38665
- emit("git", `Primary repo already present \u2014 skipping clone.`);
38666
- continue;
38667
- }
38668
- emit("git", `Cloning ${repo.url}\u2026`);
38669
- await run9("git", ["clone", repo.url, target], { timeout: 12e4 });
38670
- continue;
38671
- }
38672
- emit("git", "Initializing the primary repo\u2026");
38673
- await run9("git", ["init"], { cwd: target, timeout: 2e4 });
38674
- if (repo.url) {
38675
- await run9("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
38676
- () => void 0
38677
- );
38678
- }
38679
- continue;
38680
- }
38681
- const dir2 = join19(target, subDirName(repo));
38682
- if (repo.url) {
38683
- if (existsSync12(join19(dir2, ".git"))) {
38684
- emit("git", `Sub-repo ${subDirName(repo)} already present \u2014 skipping clone.`);
38685
- continue;
38686
- }
38687
- emit("git", `Cloning sub-repo ${repo.url}\u2026`);
38688
- await run9("git", ["clone", repo.url, dir2], { timeout: 12e4 });
38689
- } else {
38690
- emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
38691
- await mkdir6(dir2, { recursive: true });
38692
- await run9("git", ["init"], { cwd: dir2, timeout: 2e4 });
38693
- }
38694
- }
38695
- }
38696
- function sampleDir() {
38697
- if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
38698
- const here = dirname7(fileURLToPath3(import.meta.url));
38699
- const bundled = resolve8(here, "project-sample");
38700
- for (const candidate of [bundled, resolve8(here, "../project-sample"), resolve8(here, "../../project-sample")]) {
38701
- if (existsSync12(candidate)) return candidate;
38702
- }
38703
- return bundled;
38704
- }
38705
- async function scaffoldProject(payload, profileDir2, onProgress) {
38706
- let lastStep = "start";
38707
- const emit = (step, message) => {
38708
- lastStep = step;
38709
- onProgress?.({ projectId: payload.projectId, step, message });
38710
- };
38711
- try {
38712
- const root = process.env.SCAFFOLD_ROOT ? resolve8(process.env.SCAFFOLD_ROOT) : resolve8(profileDir2);
38713
- const target = join19(root, payload.projectName);
38714
- await mkdir6(target, { recursive: true });
38715
- emit("copy", "Copying the sample template\u2026");
38716
- await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
38717
- if (payload.spec) {
38718
- emit("spec", "Writing project.spec.json\u2026");
38719
- await writeFile5(
38720
- join19(target, "project.spec.json"),
38721
- JSON.stringify(payload.spec, null, 2),
38722
- "utf8"
38723
- );
38724
- }
38725
- const repos = reposOf(payload.spec);
38726
- if (repos.length > 0) await provisionRepos(target, repos, emit);
38727
- if (payload.spec) await aiInit(target, payload.spec, emit);
38728
- emit("version", "Writing .4pm/.4pm.json\u2026");
38729
- await writeTemplateMarker(target);
38730
- onProgress?.({ projectId: payload.projectId, step: "done", message: "Scaffold complete.", done: true });
38731
- return { ok: true, path: target };
38732
- } catch (err) {
38733
- return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
38734
- }
38735
- }
38736
- async function writeTemplateMarker(target) {
38737
- const dir2 = join19(target, ".4pm");
38738
- await mkdir6(dir2, { recursive: true });
38739
- const marker = { templateVersion: PROJECT_TEMPLATE.version, scaffoldedAt: (/* @__PURE__ */ new Date()).toISOString() };
38740
- await writeFile5(join19(dir2, ".4pm.json"), JSON.stringify(marker, null, 2) + "\n", "utf8");
38741
- }
38742
- async function aiInit(target, spec, emit) {
38743
- emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
38744
- const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
38745
- if (subagents.length > 0) {
38746
- await mkdir6(join19(target, ".claude", "agents"), { recursive: true });
38747
- for (const sa of subagents) {
38748
- const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
38749
- if (!name) continue;
38750
- const body = `---
38751
- name: ${name}
38752
- ---
38753
-
38754
- ${(sa.description || "").trim()}
38755
- `;
38756
- await writeFile5(join19(target, ".claude", "agents", `${name}.md`), body, "utf8");
38757
- }
38758
- }
38759
- const specJson = JSON.stringify(spec);
38760
- await generateFile(
38761
- join19(target, "README.md"),
38762
- `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
38763
- ${specJson}`
38764
- );
38765
- await generateFile(
38766
- join19(target, "CLAUDE.md"),
38767
- `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
38768
- ${specJson}`
38769
- );
38770
- }
38771
- async function generateFile(path, prompt) {
38772
- try {
38773
- const text = await aiGenerate(prompt);
38774
- if (text.trim()) await writeFile5(path, text.trim() + "\n", "utf8");
38775
- } catch {
38776
- }
38777
- }
38778
- async function addProject(payload, profileDir2, onProgress) {
38779
- let lastStep = "start";
38780
- const emit = (step, message) => {
38781
- lastStep = step;
38782
- onProgress?.({ projectId: payload.projectId, step, message });
38783
- };
38784
- try {
38785
- const root = process.env.SCAFFOLD_ROOT ? resolve8(process.env.SCAFFOLD_ROOT) : resolve8(profileDir2);
38786
- const target = join19(root, payload.projectName);
38787
- await mkdir6(target, { recursive: true });
38788
- const repos = payload.repos ?? [];
38789
- if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
38790
- onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
38791
- return { ok: true, path: target };
38792
- } catch (err) {
38793
- return { ok: false, error: err instanceof Error ? err.message : String(err), step: lastStep };
38794
- }
38795
- }
38796
-
38797
- // src/core/ws-client/handlers/project.ts
38798
38916
  function handleProjectChannels(ctx, message, payload) {
38799
38917
  switch (message.channel) {
38800
38918
  case WsChannels.PHYSIC_SYNC: {
@@ -38815,6 +38933,7 @@ function handleProjectChannels(ctx, message, payload) {
38815
38933
  ctx.physicRoot = ctx.physicFolderPath(sync.newName);
38816
38934
  ctx.bus.setProject(sync.newName);
38817
38935
  ctx.bus.log(t("project.folderSynced", { name: sync.newName }));
38936
+ void ctx.cloneServingRepos().catch(() => void 0);
38818
38937
  return true;
38819
38938
  }
38820
38939
  case WsChannels.PHYSIC_DELETE: {
@@ -39088,7 +39207,7 @@ function handleToolsChannels(ctx, message, payload) {
39088
39207
  }
39089
39208
 
39090
39209
  // src/core/agents.ts
39091
- import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as readdir4, rm as rm3 } from "fs/promises";
39210
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7, readdir as readdir5, rm as rm4 } from "fs/promises";
39092
39211
  import { join as join21 } from "path";
39093
39212
  var AGENTS_REL = ".claude/agents";
39094
39213
  var SKILLS_REL = ".claude/skills";
@@ -39113,7 +39232,7 @@ async function listAgents(root) {
39113
39232
  const subagents = [];
39114
39233
  const skills = [];
39115
39234
  try {
39116
- for (const e of await readdir4(join21(root, AGENTS_REL), { withFileTypes: true })) {
39235
+ for (const e of await readdir5(join21(root, AGENTS_REL), { withFileTypes: true })) {
39117
39236
  if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
39118
39237
  const fm = frontmatter(await readText2(join21(root, AGENTS_REL, e.name)));
39119
39238
  subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
@@ -39121,7 +39240,7 @@ async function listAgents(root) {
39121
39240
  } catch {
39122
39241
  }
39123
39242
  try {
39124
- for (const e of await readdir4(join21(root, SKILLS_REL), { withFileTypes: true })) {
39243
+ for (const e of await readdir5(join21(root, SKILLS_REL), { withFileTypes: true })) {
39125
39244
  if (!e.isDirectory()) continue;
39126
39245
  const fm = frontmatter(await readText2(join21(root, SKILLS_REL, e.name, "SKILL.md")));
39127
39246
  skills.push({ name: e.name, description: fm.description });
@@ -39151,8 +39270,8 @@ async function writeAgent(root, req) {
39151
39270
  if (!n) return { ok: false, error: "invalid name" };
39152
39271
  try {
39153
39272
  if (req.action === "delete") {
39154
- if (req.kind === "subagent") await rm3(itemPath(root, "subagent", n), { force: true });
39155
- else await rm3(join21(root, SKILLS_REL, n), { recursive: true, force: true });
39273
+ if (req.kind === "subagent") await rm4(itemPath(root, "subagent", n), { force: true });
39274
+ else await rm4(join21(root, SKILLS_REL, n), { recursive: true, force: true });
39156
39275
  return { ok: true };
39157
39276
  }
39158
39277
  if (req.content === void 0) return { ok: false, error: "content is required" };
@@ -39273,7 +39392,7 @@ async function writeAgentTools(root, scope, permissions) {
39273
39392
 
39274
39393
  // src/core/packages.ts
39275
39394
  import { createHash as createHash4 } from "crypto";
39276
- import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as readdir5, rm as rm4, stat as stat3 } from "fs/promises";
39395
+ import { readFile as readFile10, writeFile as writeFile9, mkdir as mkdir9, readdir as readdir6, rm as rm5, stat as stat3 } from "fs/promises";
39277
39396
  import { dirname as dirname9, join as join24, relative } from "path";
39278
39397
  var AGENTS_REL2 = ".claude/agents";
39279
39398
  var SKILLS_REL2 = ".claude/skills";
@@ -39293,7 +39412,7 @@ async function listFilesRel(dir2) {
39293
39412
  async function walk2(d) {
39294
39413
  let entries2;
39295
39414
  try {
39296
- entries2 = await readdir5(d, { withFileTypes: true });
39415
+ entries2 = await readdir6(d, { withFileTypes: true });
39297
39416
  } catch {
39298
39417
  return;
39299
39418
  }
@@ -39446,11 +39565,11 @@ async function removePackage(root, slug) {
39446
39565
  const entry = manifest[n];
39447
39566
  if (!entry) return { ok: false, error: "not installed" };
39448
39567
  if (entry.kind === "subagent") {
39449
- await rm4(join24(root, AGENTS_REL2, `${n}.md`), { force: true });
39450
- await rm4(join24(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
39451
- await rm4(join24(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
39568
+ await rm5(join24(root, AGENTS_REL2, `${n}.md`), { force: true });
39569
+ await rm5(join24(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
39570
+ await rm5(join24(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
39452
39571
  } else {
39453
- await rm4(join24(root, SKILLS_REL2, n), { recursive: true, force: true });
39572
+ await rm5(join24(root, SKILLS_REL2, n), { recursive: true, force: true });
39454
39573
  }
39455
39574
  delete manifest[n];
39456
39575
  await writeManifest(root, manifest);
@@ -39461,7 +39580,7 @@ async function removePackage(root, slug) {
39461
39580
  }
39462
39581
 
39463
39582
  // src/core/graph.ts
39464
- import { readFile as readFile11, readdir as readdir6 } from "fs/promises";
39583
+ import { readFile as readFile11, readdir as readdir7 } from "fs/promises";
39465
39584
  import { join as join25, relative as relative2, dirname as dirname10, resolve as resolve9, extname as extname2, basename as basename6 } from "path";
39466
39585
  var MAX_FILES = 2e3;
39467
39586
  var MAX_NODES = 3e3;
@@ -39474,7 +39593,7 @@ async function walk(root, accept) {
39474
39593
  const dir2 = stack.pop();
39475
39594
  let entries2;
39476
39595
  try {
39477
- entries2 = await readdir6(dir2, { withFileTypes: true });
39596
+ entries2 = await readdir7(dir2, { withFileTypes: true });
39478
39597
  } catch {
39479
39598
  continue;
39480
39599
  }
@@ -40170,6 +40289,9 @@ var WsClient = class {
40170
40289
  /** The physic project folder root this cli serves (null = no project) — fs.list is
40171
40290
  * scoped to it so the web FsPicker can only browse inward, never out. */
40172
40291
  physicRoot = null;
40292
+ /** Declared repos of the served project (ADR-0289), from each ws_token — cloned into the physic
40293
+ * root on connect (and after a PHYSIC_SYNC creates the folder) when any is missing its `.git`. */
40294
+ servingRepos = [];
40173
40295
  /** This machine-user's username — the git commit author for this project (ADR-0097).
40174
40296
  * Push uses the account already logged in with gh/glab on the worker. */
40175
40297
  machineUsername = "";
@@ -40287,9 +40409,24 @@ var WsClient = class {
40287
40409
  setConsoleWatching: (on) => self.setConsoleWatching(on),
40288
40410
  setMetricsWatching: (on) => self.setMetricsWatching(on),
40289
40411
  ensurePhysicFolderPath: (folder) => self.ensurePhysicFolderPath(folder),
40290
- physicFolderPath: (projectName) => self.physicFolderPath(projectName)
40412
+ physicFolderPath: (projectName) => self.physicFolderPath(projectName),
40413
+ cloneServingRepos: () => self.ensureServingReposCloned(),
40414
+ updateCliNow: () => self.updateScheduler.updateNow()
40291
40415
  };
40292
40416
  }
40417
+ /**
40418
+ * Clone any declared repo missing from the served physic root (ADR-0289) — idempotent (skips a
40419
+ * repo that already has `.git`). Best-effort: a clone failure never disrupts the session (the web
40420
+ * still surfaces "not provisioned"). No-op when idle (no physic root) or no repos declared.
40421
+ */
40422
+ async ensureServingReposCloned() {
40423
+ if (!this.physicRoot || this.servingRepos.length === 0) return;
40424
+ try {
40425
+ await ensureReposCloned(this.physicRoot, this.servingRepos, (_step, message) => this.bus.log(message));
40426
+ } catch (err) {
40427
+ logger.warn("repos.clone.failed", { error: err instanceof Error ? err.message : String(err) });
40428
+ }
40429
+ }
40293
40430
  /**
40294
40431
  * Detect the worker's tools and report the snapshot to the server (ADR-0254, `tools.report`) so the
40295
40432
  * DB-backed Tools panel + restore target stay current. One-way, best-effort — a detect/send failure
@@ -40473,6 +40610,8 @@ var WsClient = class {
40473
40610
  this.restrictToFolder = token.aiRestrictToFolder === true;
40474
40611
  applyGitAuth(token.gitAuth ?? null);
40475
40612
  this.machineUsername = token.machineUsername ?? "";
40613
+ this.servingRepos = token.repos ?? [];
40614
+ void this.ensureServingReposCloned();
40476
40615
  if (token.scope && token.scope !== this.credential.scope) {
40477
40616
  this.credential.scope = token.scope;
40478
40617
  writeCredential(this.context.profileDir, this.credential);