@4pm/cli 1.5.3 → 1.5.11

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 +305 -178
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -32912,7 +32912,7 @@ var deletedFilterSchema = external_exports.enum(["true", "false"]).transform((v)
32912
32912
 
32913
32913
  // vendor/@4pm/dto/command.ts
32914
32914
  var COMMAND_MAX_LEN = 8e3;
32915
- var AI_PROMPT_MAX_LEN = 2e5;
32915
+ var AI_PROMPT_MAX_LEN = 5e5;
32916
32916
  var dispatchCommandRequestSchema = external_exports.object({
32917
32917
  projectId: external_exports.string().uuid(),
32918
32918
  // Target cli. Required unless `pick` is set — with `pick:"idle"` the server chooses the
@@ -33068,12 +33068,24 @@ var listMachineLinksQuerySchema = baseRequestSchema.extend({
33068
33068
  var updateMachineLinkRequestSchema = external_exports.object({
33069
33069
  hashcode3ExpiresAt: external_exports.string().datetime().nullable()
33070
33070
  });
33071
+ var workerToolCopySchema = external_exports.object({
33072
+ /** Source machine-link id whose `toolSnapshot` supplies the manifest. */
33073
+ sourceLinkId: external_exports.string().uuid(),
33074
+ /** Target machine-link ids to queue the manifest for (pushed on their next connect). */
33075
+ targetLinkIds: external_exports.array(external_exports.string().uuid()).min(1).max(200)
33076
+ });
33071
33077
  var workerToolInstallSchema = external_exports.object({
33072
33078
  /** Catalog id or npm package name (validated npm-name shape on the cli). */
33073
33079
  name: external_exports.string().trim().min(1).max(214),
33074
33080
  /** Package manager to run the global install with. */
33075
33081
  manager: external_exports.enum(["npm", "pnpm"])
33076
33082
  });
33083
+ var workerToolAutoUpdateSchema = external_exports.object({
33084
+ /** Catalog id or installed extra's npm package name. */
33085
+ name: external_exports.string().trim().min(1).max(214),
33086
+ /** true = flag this tool for auto-update to @latest; false = clear it. */
33087
+ enabled: external_exports.boolean()
33088
+ });
33077
33089
  var configWriteRequestSchema = external_exports.object({
33078
33090
  config: external_exports.string().max(256 * 1024)
33079
33091
  });
@@ -33683,6 +33695,22 @@ var WsChannels = {
33683
33695
  TOOLS_UPDATE: "tools.update",
33684
33696
  TOOLS_PROGRESS: "tools.progress",
33685
33697
  TOOLS_DONE: "tools.done",
33698
+ // toggle per-tool auto-update (ADR-0253): request/reply — the cli persists the flag in
33699
+ // config.json (`autoUpdateTools`); the ADR-0074 idle daily tick later runs the flagged tools.
33700
+ // (ADR-0254: the flag is now persisted to the DB `toolSnapshot`; this channel is repurposed —
33701
+ // machine-0056 forwards it to an ONLINE worker only, to update its local config.json mirror
33702
+ // immediately instead of waiting for the next ws_token seed.)
33703
+ TOOLS_AUTOUPDATE: "tools.autoUpdate",
33704
+ // cli → server one-way (ADR-0254): report the worker's detected tool snapshot — the server
33705
+ // persists it to `MachineLink.toolSnapshot` for the DB-backed Tools panel + restore-on-boot.
33706
+ // Sent on the ADR-0074 daily idle tick, after each install/uninstall/update op, and on boot
33707
+ // AFTER restore completes. The read/restore direction rides `ws_token` (seeds the cli).
33708
+ TOOLS_REPORT: "tools.report",
33709
+ // server → cli (reply, ADR-0254): push a tool manifest to reconcile NOW — used by "copy tools"
33710
+ // to apply immediately to an online target (the offline path rides `ws_token.toolRestore` on the
33711
+ // next connect). The cli installs each missing/mismatched `name@version`, then reports; the reply
33712
+ // says whether it applied so the server can clear the pending manifest.
33713
+ TOOLS_RESTORE: "tools.restore",
33686
33714
  // autonomous mode control (ADR-0152): read settings+status+books+approvals, tail logs, and
33687
33715
  // write {settings|approvals|userTodo|cron} — the dashboard drives the worker's autonomous engine.
33688
33716
  AUTONOMOUS_READ: "autonomous.read",
@@ -33843,7 +33871,7 @@ import { existsSync as existsSync13, mkdirSync as mkdirSync5, rmSync as rmSync4
33843
33871
  import { randomUUID as randomCommandId } from "crypto";
33844
33872
 
33845
33873
  // src/core/update-scheduler.ts
33846
- import { spawn } from "child_process";
33874
+ import { spawn as spawn2 } from "child_process";
33847
33875
 
33848
33876
  // src/common/logger/logger.ts
33849
33877
  import { appendFile, mkdir, readdir, unlink } from "fs/promises";
@@ -33948,17 +33976,191 @@ ${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
33948
33976
  }
33949
33977
  }
33950
33978
 
33979
+ // src/core/worker-tools.ts
33980
+ import { spawn } from "child_process";
33981
+ var NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
33982
+ function run(cmd, args, onLine, timeoutMs = 18e4) {
33983
+ return new Promise((resolve8) => {
33984
+ let out = "";
33985
+ let buf = "";
33986
+ let child = null;
33987
+ const done = (code) => {
33988
+ clearTimeout(timer);
33989
+ if (onLine && buf.trim()) onLine(buf.trim());
33990
+ resolve8({ code, out });
33991
+ };
33992
+ const timer = setTimeout(() => {
33993
+ child?.kill();
33994
+ if (onLine) onLine(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
33995
+ done(124);
33996
+ }, timeoutMs);
33997
+ timer.unref?.();
33998
+ const feed = (d) => {
33999
+ const text = d.toString("utf8");
34000
+ out += text;
34001
+ if (!onLine) return;
34002
+ buf += text;
34003
+ let nl;
34004
+ while ((nl = buf.indexOf("\n")) >= 0) {
34005
+ const line = buf.slice(0, nl).replace(/\r$/, "");
34006
+ buf = buf.slice(nl + 1);
34007
+ onLine(line);
34008
+ }
34009
+ };
34010
+ try {
34011
+ child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
34012
+ child.stdout?.on("data", feed);
34013
+ child.stderr?.on("data", feed);
34014
+ child.on("exit", (code) => done(code ?? 0));
34015
+ child.on("error", () => done(127));
34016
+ } catch {
34017
+ done(127);
34018
+ }
34019
+ });
34020
+ }
34021
+ async function detectOne(cmd, versionArg) {
34022
+ const { code, out } = await run(cmd, [versionArg], void 0, 15e3);
34023
+ if (code !== 0) return null;
34024
+ const first = out.split("\n").map((l) => l.trim()).find(Boolean);
34025
+ return first ?? "";
34026
+ }
34027
+ async function listExtras(autoUpdateSet = /* @__PURE__ */ new Set()) {
34028
+ const { out } = await run("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
34029
+ if (!out.trim()) return [];
34030
+ let deps = {};
34031
+ try {
34032
+ deps = JSON.parse(out).dependencies ?? {};
34033
+ } catch {
34034
+ return [];
34035
+ }
34036
+ const catalogPkgs = new Set(WORKER_TOOL_BUNDLED_GLOBALS);
34037
+ for (const t of WORKER_TOOL_CATALOG) {
34038
+ catalogPkgs.add(t.id);
34039
+ if (t.installPackage) catalogPkgs.add(t.installPackage);
34040
+ }
34041
+ return Object.entries(deps).filter(([name]) => !catalogPkgs.has(name)).map(([name, info]) => ({
34042
+ id: name,
34043
+ label: name,
34044
+ category: "runtime",
34045
+ installed: true,
34046
+ version: info.version ?? null,
34047
+ installable: true,
34048
+ updatable: true,
34049
+ autoUpdate: autoUpdateSet.has(name)
34050
+ }));
34051
+ }
34052
+ async function detectWorkerTools(autoUpdateTools = []) {
34053
+ const autoUpdateSet = new Set(autoUpdateTools);
34054
+ const catalog = await Promise.all(
34055
+ WORKER_TOOL_CATALOG.map(async (t) => {
34056
+ const version = await detectOne(t.id, t.versionArg);
34057
+ return {
34058
+ id: t.id,
34059
+ label: t.label,
34060
+ category: t.category,
34061
+ installed: version !== null,
34062
+ version,
34063
+ installable: t.installable,
34064
+ updatable: t.updatable,
34065
+ autoUpdate: autoUpdateSet.has(t.id)
34066
+ };
34067
+ })
34068
+ );
34069
+ const extras = await listExtras(autoUpdateSet).catch(() => []);
34070
+ return { catalog, extras };
34071
+ }
34072
+ function opPast(op) {
34073
+ return op === "install" ? "installed" : op === "uninstall" ? "uninstalled" : "updated";
34074
+ }
34075
+ function resolvePackage(name, op) {
34076
+ const entry = WORKER_TOOL_CATALOG.find((t) => t.id === name);
34077
+ if (entry) {
34078
+ const allowed = op === "update" ? entry.updatable : entry.installable;
34079
+ if (!allowed) {
34080
+ return { error: `"${name}" is a prerequisite and cannot be ${opPast(op)} from here.` };
34081
+ }
34082
+ return { pkg: entry.installPackage ?? name };
34083
+ }
34084
+ if (WORKER_TOOL_PREREQUISITE_IDS.includes(name)) {
34085
+ return { error: `"${name}" is a prerequisite and cannot be ${opPast(op)} from here.` };
34086
+ }
34087
+ if (WORKER_TOOL_BUNDLED_GLOBALS.includes(name)) {
34088
+ return { error: `"${name}" ships with Node and cannot be ${opPast(op)} from here.` };
34089
+ }
34090
+ if (!NPM_NAME.test(name)) return { error: `Invalid package name: "${name}".` };
34091
+ return { pkg: name };
34092
+ }
34093
+ function opArgs(op, manager, pkg) {
34094
+ if (op === "uninstall") return manager === "pnpm" ? ["remove", "-g", pkg] : ["uninstall", "-g", pkg];
34095
+ const target = op === "update" ? `${pkg}@latest` : pkg;
34096
+ return manager === "pnpm" ? ["add", "-g", target] : ["install", "-g", target];
34097
+ }
34098
+ async function runWorkerToolOp(op, name, manager, onLine) {
34099
+ const resolved = resolvePackage(name, op);
34100
+ if (resolved.error || !resolved.pkg) {
34101
+ return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
34102
+ }
34103
+ onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
34104
+ const { code } = await run(manager, opArgs(op, manager, resolved.pkg), onLine);
34105
+ return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
34106
+ }
34107
+ function setToolAutoUpdate(profileDir2, name, enabled) {
34108
+ const resolved = resolvePackage(name, "update");
34109
+ if (resolved.error) return { ok: false, error: resolved.error };
34110
+ const current = readProfileConfig(profileDir2).autoUpdateTools ?? [];
34111
+ const set = new Set(current);
34112
+ if (enabled) set.add(name);
34113
+ else set.delete(name);
34114
+ writeProfileConfig(profileDir2, { autoUpdateTools: [...set] });
34115
+ return { ok: true };
34116
+ }
34117
+ async function autoUpdateFlaggedTools(tools, onLine) {
34118
+ for (const name of tools) {
34119
+ const res = await runWorkerToolOp("update", name, "npm", onLine).catch(
34120
+ (err) => ({ ok: false, exitCode: 1, error: String(err) })
34121
+ );
34122
+ onLine(res.ok ? `\u2713 ${name} updated` : `\u2717 ${name}: ${res.error ?? "failed"}`);
34123
+ }
34124
+ }
34125
+ async function installPinned(name, version, manager, onLine) {
34126
+ const resolved = resolvePackage(name, "update");
34127
+ if (resolved.error || !resolved.pkg) return { ok: false, error: resolved.error ?? "Invalid package." };
34128
+ const target = `${resolved.pkg}@${version}`;
34129
+ const args = manager === "pnpm" ? ["add", "-g", target] : ["install", "-g", target];
34130
+ onLine(`$ ${manager} ${args.join(" ")}`);
34131
+ const { code } = await run(manager, args, onLine);
34132
+ return { ok: code === 0, error: code === 0 ? void 0 : `${manager} exited ${code}` };
34133
+ }
34134
+ async function reconcileTools(manifest, onLine) {
34135
+ if (manifest.length === 0) return;
34136
+ const { catalog, extras } = await detectWorkerTools();
34137
+ const versionByName = new Map(
34138
+ [...catalog, ...extras].filter((r) => r.installed && r.version).map((r) => [r.id, r.version])
34139
+ );
34140
+ for (const entry of manifest) {
34141
+ const current = versionByName.get(entry.name);
34142
+ if (current && current.includes(entry.version)) continue;
34143
+ onLine(`Restoring ${entry.name}@${entry.version}\u2026`);
34144
+ const res = await installPinned(entry.name, entry.version, entry.manager, onLine).catch(
34145
+ (err) => ({ ok: false, error: String(err) })
34146
+ );
34147
+ onLine(res.ok ? `\u2713 ${entry.name}@${entry.version}` : `\u2717 ${entry.name}: ${res.error ?? "failed"}`);
34148
+ }
34149
+ }
34150
+
33951
34151
  // src/core/update-scheduler.ts
33952
34152
  var TICK_MS = 6e4;
33953
34153
  var UpdateScheduler = class {
33954
- constructor(serverUrl, profileDir2, bus) {
34154
+ constructor(serverUrl, profileDir2, bus, onToolsChanged) {
33955
34155
  this.serverUrl = serverUrl;
33956
34156
  this.profileDir = profileDir2;
33957
34157
  this.bus = bus;
34158
+ this.onToolsChanged = onToolsChanged;
33958
34159
  }
33959
34160
  serverUrl;
33960
34161
  profileDir;
33961
34162
  bus;
34163
+ onToolsChanged;
33962
34164
  policy = null;
33963
34165
  timer = null;
33964
34166
  /** Date key (YYYY-MM-DD in org tz) whose update already ran — enforces once per day. */
@@ -34030,6 +34232,7 @@ var UpdateScheduler = class {
34030
34232
  async runUpdate() {
34031
34233
  this.updating = true;
34032
34234
  try {
34235
+ await this.updateFlaggedTools();
34033
34236
  this.bus.log("Scheduled auto-update: checking for a new version\u2026");
34034
34237
  const result = await updateToLatest(this.serverUrl);
34035
34238
  if (result.action === "already-latest") {
@@ -34043,7 +34246,7 @@ var UpdateScheduler = class {
34043
34246
  }
34044
34247
  logger.info("update.scheduled.updated", { version: result.version });
34045
34248
  this.bus.log(`Updated to ${result.version} \u2014 restarting to apply\u2026`);
34046
- const child = spawn(process.execPath, process.argv.slice(1), {
34249
+ const child = spawn2(process.execPath, process.argv.slice(1), {
34047
34250
  stdio: "inherit",
34048
34251
  env: { ...process.env, FOURPM_NO_UPDATE: "1" }
34049
34252
  });
@@ -34054,16 +34257,26 @@ var UpdateScheduler = class {
34054
34257
  this.updating = false;
34055
34258
  }
34056
34259
  }
34260
+ /** Update each tool flagged for auto-update in config.json to @latest (ADR-0253) — best-effort. */
34261
+ async updateFlaggedTools() {
34262
+ const tools = readProfileConfig(this.profileDir).autoUpdateTools ?? [];
34263
+ if (tools.length === 0) return;
34264
+ this.bus.log(`Scheduled tool auto-update: ${tools.join(", ")}\u2026`);
34265
+ await autoUpdateFlaggedTools(tools, (line) => logger.info("update.tool.line", { line })).catch(
34266
+ (err) => logger.warn("update.tool.error", { error: String(err) })
34267
+ );
34268
+ this.onToolsChanged?.();
34269
+ }
34057
34270
  };
34058
34271
 
34059
34272
  // src/core/executor.ts
34060
- import { spawn as spawn2 } from "child_process";
34273
+ import { spawn as spawn3 } from "child_process";
34061
34274
  var KILL_GRACE_MS = 5e3;
34062
34275
  async function runCommand(dispatch, emit, opts) {
34063
34276
  let seq = 0;
34064
34277
  let child;
34065
34278
  try {
34066
- child = spawn2(dispatch.cmd, dispatch.args ?? [], {
34279
+ child = spawn3(dispatch.cmd, dispatch.args ?? [], {
34067
34280
  cwd: dispatch.path,
34068
34281
  env: { ...process.env, ...dispatch.env },
34069
34282
  shell: false
@@ -34657,7 +34870,7 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot) {
34657
34870
  }
34658
34871
 
34659
34872
  // src/core/claude-usage.ts
34660
- import { spawn as spawn3 } from "child_process";
34873
+ import { spawn as spawn4 } from "child_process";
34661
34874
  import { readFileSync as readFileSync9 } from "fs";
34662
34875
  import { join as join9 } from "path";
34663
34876
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
@@ -34704,7 +34917,7 @@ function runUsageCli(dir2, aiCli2) {
34704
34917
  }, 3e4);
34705
34918
  timer.unref?.();
34706
34919
  try {
34707
- child = spawn3(aiCli2, ["/usage"], {
34920
+ child = spawn4(aiCli2, ["/usage"], {
34708
34921
  env: { ...process.env, CLAUDE_CONFIG_DIR: dir2 },
34709
34922
  stdio: ["ignore", "pipe", "ignore"]
34710
34923
  });
@@ -34774,7 +34987,7 @@ async function checkClaudeUsage(dirs, aiCli2 = "claude") {
34774
34987
  // src/core/ai-assist.ts
34775
34988
  import { execFile } from "child_process";
34776
34989
  import { promisify } from "util";
34777
- var run = promisify(execFile);
34990
+ var run2 = promisify(execFile);
34778
34991
  function aiCli() {
34779
34992
  return process.env.AI_CLI || "claude";
34780
34993
  }
@@ -34817,7 +35030,7 @@ function parseAi(cli, stdout, prompt) {
34817
35030
  }
34818
35031
  async function runAi(prompt) {
34819
35032
  const cli = aiCli();
34820
- const { stdout } = await run(cli, aiArgs(cli, prompt), {
35033
+ const { stdout } = await run2(cli, aiArgs(cli, prompt), {
34821
35034
  timeout: 12e4,
34822
35035
  maxBuffer: 8 * 1024 * 1024
34823
35036
  });
@@ -35160,7 +35373,7 @@ async function writeWorkerFile(root, relPath, content) {
35160
35373
  }
35161
35374
 
35162
35375
  // src/core/support-answer.ts
35163
- import { spawn as spawn4 } from "child_process";
35376
+ import { spawn as spawn5 } from "child_process";
35164
35377
  import { execFile as execFile2 } from "child_process";
35165
35378
  import { createHash as createHash3 } from "crypto";
35166
35379
  import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
@@ -35295,7 +35508,7 @@ function runClaudeOnce(cmd, profile, prompt, extraEnv) {
35295
35508
  ...extraEnv,
35296
35509
  ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
35297
35510
  };
35298
- const child = spawn4(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
35511
+ const child = spawn5(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
35299
35512
  const parser = createAiStreamParser(cmd);
35300
35513
  let out = "";
35301
35514
  let err = "";
@@ -35365,7 +35578,7 @@ async function runSupportAnswer(req, ai, profileDir2) {
35365
35578
  }
35366
35579
 
35367
35580
  // src/core/knowledge-compose.ts
35368
- import { spawn as spawn5 } from "child_process";
35581
+ import { spawn as spawn6 } from "child_process";
35369
35582
  var COMPOSE_TIMEOUT_MS = 18e4;
35370
35583
  function buildPrompt2(req) {
35371
35584
  const focus = req.prompt ? `
@@ -35391,7 +35604,7 @@ function runOnce(cmd, profile, prompt, cwd2, extraEnv) {
35391
35604
  ...extraEnv,
35392
35605
  ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
35393
35606
  };
35394
- const child = spawn5(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
35607
+ const child = spawn6(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
35395
35608
  let out = "";
35396
35609
  let err = "";
35397
35610
  const timer = setTimeout(() => {
@@ -35439,10 +35652,10 @@ async function runKnowledgeCompose(req, ai, physicRoot) {
35439
35652
  // src/core/autonomous.ts
35440
35653
  import { readFile as readFile3, writeFile as writeFile2, chmod } from "fs/promises";
35441
35654
  import { readFileSync as readFileSync13 } from "fs";
35442
- import { execFile as execFile3, spawn as spawn6 } from "child_process";
35655
+ import { execFile as execFile3, spawn as spawn7 } from "child_process";
35443
35656
  import { join as join13 } from "path";
35444
35657
  import { promisify as promisify3 } from "util";
35445
- var run2 = promisify3(execFile3);
35658
+ var run3 = promisify3(execFile3);
35446
35659
  var SETTINGS_REL = ".claude/.autonomous.settings.json";
35447
35660
  var HISTORIES_REL = ".claude/.autonomous.histories.json";
35448
35661
  var APPROVALS_REL = ".claude/.autonomous.approvals.json";
@@ -35476,7 +35689,7 @@ function tickScript(root) {
35476
35689
  }
35477
35690
  async function crontabList() {
35478
35691
  try {
35479
- const { stdout } = await run2("crontab", ["-l"], { timeout: 1e4 });
35692
+ const { stdout } = await run3("crontab", ["-l"], { timeout: 1e4 });
35480
35693
  return stdout;
35481
35694
  } catch {
35482
35695
  return "";
@@ -35484,7 +35697,7 @@ async function crontabList() {
35484
35697
  }
35485
35698
  async function crontabSet(content) {
35486
35699
  return new Promise((resolve8) => {
35487
- const child = spawn6("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
35700
+ const child = spawn7("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
35488
35701
  child.on("error", () => resolve8(false));
35489
35702
  child.on("close", (code) => resolve8(code === 0));
35490
35703
  child.stdin.end(content.endsWith("\n") ? content : content + "\n");
@@ -36215,15 +36428,15 @@ async function buildGraph(root, mode) {
36215
36428
  // src/core/rag.ts
36216
36429
  import { readFile as readFile9, writeFile as writeFile7, statfs as statfs2, mkdir as mkdir6 } from "fs/promises";
36217
36430
  import { existsSync as existsSync10 } from "fs";
36218
- import { execFile as execFile4, spawn as spawn7 } from "child_process";
36431
+ import { execFile as execFile4, spawn as spawn8 } from "child_process";
36219
36432
  import { totalmem as totalmem2, cpus as cpus2 } from "os";
36220
36433
  import { join as join20 } from "path";
36221
36434
  import { promisify as promisify4 } from "util";
36222
- var run3 = promisify4(execFile4);
36435
+ var run4 = promisify4(execFile4);
36223
36436
  var RAG_REL = ".claude/rag";
36224
36437
  async function tryRun(cmd, args) {
36225
36438
  try {
36226
- const { stdout } = await run3(cmd, args, { timeout: 8e3 });
36439
+ const { stdout } = await run4(cmd, args, { timeout: 8e3 });
36227
36440
  return { ok: true, out: stdout.trim() };
36228
36441
  } catch {
36229
36442
  return { ok: false, out: "" };
@@ -36402,7 +36615,7 @@ touch "${RAG_REL}/.indexing"
36402
36615
  { echo "[reindex] $(date)"; "$RAG_PY" "${RAG_REL}/rag_engine.py" reindex 2>&1; } >> "${RAG_REL}/index.log" 2>&1
36403
36616
  rm -f "${RAG_REL}/.indexing"
36404
36617
  `;
36405
- const child = spawn7("bash", ["-c", script], {
36618
+ const child = spawn8("bash", ["-c", script], {
36406
36619
  cwd: root,
36407
36620
  env: { ...process.env, RAG_PY: py.bin },
36408
36621
  detached: true,
@@ -36417,7 +36630,7 @@ async function ragQuery(root, queryText, k = 8) {
36417
36630
  if (!existsSync10(join20(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
36418
36631
  await ensureEngine(root);
36419
36632
  try {
36420
- const { stdout } = await run3(
36633
+ const { stdout } = await run4(
36421
36634
  py.bin,
36422
36635
  [join20(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k, 1), 20))],
36423
36636
  { cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
@@ -36452,7 +36665,7 @@ rm -f "${RAG_REL}/.installed"
36452
36665
  } >> "${RAG_REL}/install.log" 2>&1
36453
36666
  rm -f "${RAG_REL}/.installing"
36454
36667
  `;
36455
- const child = spawn7("bash", ["-c", script], {
36668
+ const child = spawn8("bash", ["-c", script], {
36456
36669
  cwd: root,
36457
36670
  env: { ...process.env, RAG_PY: py.bin, RAG_MODEL: model },
36458
36671
  detached: true,
@@ -36467,7 +36680,7 @@ import { readFile as readFile10 } from "fs/promises";
36467
36680
  import { execFile as execFile5 } from "child_process";
36468
36681
  import { basename as basename3, dirname as dirname7, resolve as resolve5 } from "path";
36469
36682
  import { promisify as promisify5 } from "util";
36470
- var run4 = promisify5(execFile5);
36683
+ var run5 = promisify5(execFile5);
36471
36684
  async function gitDiff(path) {
36472
36685
  const target = resolve5(path);
36473
36686
  const dir2 = dirname7(target);
@@ -36479,7 +36692,7 @@ async function gitDiff(path) {
36479
36692
  }
36480
36693
  let oldContent = "";
36481
36694
  try {
36482
- const { stdout } = await run4("git", ["show", `HEAD:./${base}`], {
36695
+ const { stdout } = await run5("git", ["show", `HEAD:./${base}`], {
36483
36696
  cwd: dir2,
36484
36697
  timeout: 15e3,
36485
36698
  maxBuffer: 4 * 1024 * 1024
@@ -36493,10 +36706,10 @@ async function gitDiff(path) {
36493
36706
  // src/core/git-env.ts
36494
36707
  import { execFile as execFile6 } from "child_process";
36495
36708
  import { promisify as promisify6 } from "util";
36496
- var run5 = promisify6(execFile6);
36709
+ var run6 = promisify6(execFile6);
36497
36710
  async function hasBinary(bin, args = ["--version"]) {
36498
36711
  try {
36499
- await run5(bin, args, { timeout: 5e3 });
36712
+ await run6(bin, args, { timeout: 5e3 });
36500
36713
  return true;
36501
36714
  } catch {
36502
36715
  return false;
@@ -36509,7 +36722,7 @@ async function checkGitEnv(provider) {
36509
36722
  let account = null;
36510
36723
  if (installed) {
36511
36724
  try {
36512
- const { stdout, stderr } = await run5(bin, ["auth", "status"], { timeout: 8e3 });
36725
+ const { stdout, stderr } = await run6(bin, ["auth", "status"], { timeout: 8e3 });
36513
36726
  const out = `${stdout}
36514
36727
  ${stderr}`;
36515
36728
  authenticated = /logged in|logged into|✓/i.test(out);
@@ -36526,7 +36739,7 @@ import { execFile as execFile7 } from "child_process";
36526
36739
  import { readFile as readFile11, rm as rm3, mkdir as mkdir7 } from "fs/promises";
36527
36740
  import { join as join21 } from "path";
36528
36741
  import { promisify as promisify7 } from "util";
36529
- var run6 = promisify7(execFile7);
36742
+ var run7 = promisify7(execFile7);
36530
36743
  function keyPaths(profileDir2) {
36531
36744
  const dir2 = join21(profileDir2, ".ssh");
36532
36745
  const key = join21(dir2, "id_4pm");
@@ -36541,14 +36754,14 @@ async function readPublicKey(pub) {
36541
36754
  }
36542
36755
  async function fingerprintOf(pub) {
36543
36756
  try {
36544
- const { stdout } = await run6("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
36757
+ const { stdout } = await run7("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
36545
36758
  return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
36546
36759
  } catch {
36547
36760
  return null;
36548
36761
  }
36549
36762
  }
36550
36763
  async function configureGit(key) {
36551
- await run6(
36764
+ await run7(
36552
36765
  "git",
36553
36766
  ["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
36554
36767
  { timeout: 5e3 }
@@ -36559,7 +36772,7 @@ async function generate(profileDir2) {
36559
36772
  await mkdir7(dir2, { recursive: true });
36560
36773
  await rm3(key, { force: true });
36561
36774
  await rm3(pub, { force: true });
36562
- await run6("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
36775
+ await run7("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
36563
36776
  timeout: 15e3
36564
36777
  });
36565
36778
  await configureGit(key);
@@ -36569,7 +36782,7 @@ async function remove(profileDir2) {
36569
36782
  const { key, pub } = keyPaths(profileDir2);
36570
36783
  await rm3(key, { force: true });
36571
36784
  await rm3(pub, { force: true });
36572
- await run6("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
36785
+ await run7("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
36573
36786
  () => void 0
36574
36787
  );
36575
36788
  return { publicKey: null, fingerprint: null };
@@ -36594,7 +36807,7 @@ import { readdir as readdir6 } from "fs/promises";
36594
36807
  import { execFile as execFile8 } from "child_process";
36595
36808
  import { resolve as resolve6, sep as sep4 } from "path";
36596
36809
  import { promisify as promisify8 } from "util";
36597
- var run7 = promisify8(execFile8);
36810
+ var run8 = promisify8(execFile8);
36598
36811
  var FS = "";
36599
36812
  var RS = "";
36600
36813
  function resolveRepo(root, repo) {
@@ -36605,7 +36818,7 @@ function resolveRepo(root, repo) {
36605
36818
  }
36606
36819
  async function git(cwd2, args) {
36607
36820
  try {
36608
- const { stdout } = await run7("git", args, {
36821
+ const { stdout } = await run8("git", args, {
36609
36822
  cwd: cwd2,
36610
36823
  timeout: 15e3,
36611
36824
  maxBuffer: 8 * 1024 * 1024
@@ -36709,11 +36922,11 @@ async function gitCommitDiff(root, repo, hash, path) {
36709
36922
  // src/core/git-commit-identity.ts
36710
36923
  import { execFile as execFile9 } from "child_process";
36711
36924
  import { promisify as promisify9 } from "util";
36712
- var run8 = promisify9(execFile9);
36925
+ var run9 = promisify9(execFile9);
36713
36926
  async function setCommitAuthor(cwd2, machineUsername) {
36714
36927
  if (!machineUsername) return;
36715
36928
  try {
36716
- await run8("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
36929
+ await run9("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
36717
36930
  } catch {
36718
36931
  }
36719
36932
  }
@@ -36724,7 +36937,7 @@ import { execFile as execFile10 } from "child_process";
36724
36937
  import { dirname as dirname8, join as join22, resolve as resolve7 } from "path";
36725
36938
  import { fileURLToPath as fileURLToPath3 } from "url";
36726
36939
  import { promisify as promisify10 } from "util";
36727
- var run9 = promisify10(execFile10);
36940
+ var run10 = promisify10(execFile10);
36728
36941
  function reposOf(spec) {
36729
36942
  const repos = spec?.repos;
36730
36943
  return Array.isArray(repos) ? repos : [];
@@ -36738,13 +36951,13 @@ async function provisionRepos(target, repos, emit, opts = {}) {
36738
36951
  if (repo.primary) {
36739
36952
  if (opts.clonePrimary && repo.url) {
36740
36953
  emit("git", `Cloning ${repo.url}\u2026`);
36741
- await run9("git", ["clone", repo.url, target], { timeout: 12e4 });
36954
+ await run10("git", ["clone", repo.url, target], { timeout: 12e4 });
36742
36955
  continue;
36743
36956
  }
36744
36957
  emit("git", "Initializing the primary repo\u2026");
36745
- await run9("git", ["init"], { cwd: target, timeout: 2e4 });
36958
+ await run10("git", ["init"], { cwd: target, timeout: 2e4 });
36746
36959
  if (repo.url) {
36747
- await run9("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
36960
+ await run10("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
36748
36961
  () => void 0
36749
36962
  );
36750
36963
  }
@@ -36753,11 +36966,11 @@ async function provisionRepos(target, repos, emit, opts = {}) {
36753
36966
  const dir2 = join22(target, subDirName(repo));
36754
36967
  if (repo.url) {
36755
36968
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
36756
- await run9("git", ["clone", repo.url, dir2], { timeout: 12e4 });
36969
+ await run10("git", ["clone", repo.url, dir2], { timeout: 12e4 });
36757
36970
  } else {
36758
36971
  emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
36759
36972
  await mkdir8(dir2, { recursive: true });
36760
- await run9("git", ["init"], { cwd: dir2, timeout: 2e4 });
36973
+ await run10("git", ["init"], { cwd: dir2, timeout: 2e4 });
36761
36974
  }
36762
36975
  }
36763
36976
  }
@@ -37235,7 +37448,7 @@ function runSlashCommand(line, base) {
37235
37448
  }
37236
37449
 
37237
37450
  // src/core/memory-compact.ts
37238
- import { spawn as spawn8 } from "child_process";
37451
+ import { spawn as spawn9 } from "child_process";
37239
37452
  var COMPACT_TIMEOUT_MS = 6e4;
37240
37453
  function buildPrompt3(input) {
37241
37454
  return [
@@ -37263,7 +37476,7 @@ function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
37263
37476
  ...extraEnv,
37264
37477
  ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
37265
37478
  };
37266
- const child = spawn8(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
37479
+ const child = spawn9(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
37267
37480
  let out = "";
37268
37481
  let err = "";
37269
37482
  const timer = setTimeout(() => {
@@ -37351,132 +37564,6 @@ function applyConfigText(profileDir2, text) {
37351
37564
  return { ok: true };
37352
37565
  }
37353
37566
 
37354
- // src/core/worker-tools.ts
37355
- import { spawn as spawn9 } from "child_process";
37356
- var NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
37357
- function run10(cmd, args, onLine, timeoutMs = 18e4) {
37358
- return new Promise((resolve8) => {
37359
- let out = "";
37360
- let buf = "";
37361
- let child = null;
37362
- const done = (code) => {
37363
- clearTimeout(timer);
37364
- if (onLine && buf.trim()) onLine(buf.trim());
37365
- resolve8({ code, out });
37366
- };
37367
- const timer = setTimeout(() => {
37368
- child?.kill();
37369
- if (onLine) onLine(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
37370
- done(124);
37371
- }, timeoutMs);
37372
- timer.unref?.();
37373
- const feed = (d) => {
37374
- const text = d.toString("utf8");
37375
- out += text;
37376
- if (!onLine) return;
37377
- buf += text;
37378
- let nl;
37379
- while ((nl = buf.indexOf("\n")) >= 0) {
37380
- const line = buf.slice(0, nl).replace(/\r$/, "");
37381
- buf = buf.slice(nl + 1);
37382
- onLine(line);
37383
- }
37384
- };
37385
- try {
37386
- child = spawn9(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
37387
- child.stdout?.on("data", feed);
37388
- child.stderr?.on("data", feed);
37389
- child.on("exit", (code) => done(code ?? 0));
37390
- child.on("error", () => done(127));
37391
- } catch {
37392
- done(127);
37393
- }
37394
- });
37395
- }
37396
- async function detectOne(cmd, versionArg) {
37397
- const { code, out } = await run10(cmd, [versionArg], void 0, 15e3);
37398
- if (code !== 0) return null;
37399
- const first = out.split("\n").map((l) => l.trim()).find(Boolean);
37400
- return first ?? "";
37401
- }
37402
- async function listExtras() {
37403
- const { out } = await run10("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
37404
- if (!out.trim()) return [];
37405
- let deps = {};
37406
- try {
37407
- deps = JSON.parse(out).dependencies ?? {};
37408
- } catch {
37409
- return [];
37410
- }
37411
- const catalogPkgs = new Set(WORKER_TOOL_BUNDLED_GLOBALS);
37412
- for (const t of WORKER_TOOL_CATALOG) {
37413
- catalogPkgs.add(t.id);
37414
- if (t.installPackage) catalogPkgs.add(t.installPackage);
37415
- }
37416
- return Object.entries(deps).filter(([name]) => !catalogPkgs.has(name)).map(([name, info]) => ({
37417
- id: name,
37418
- label: name,
37419
- category: "runtime",
37420
- installed: true,
37421
- version: info.version ?? null,
37422
- installable: true,
37423
- updatable: true
37424
- }));
37425
- }
37426
- async function detectWorkerTools() {
37427
- const catalog = await Promise.all(
37428
- WORKER_TOOL_CATALOG.map(async (t) => {
37429
- const version = await detectOne(t.id, t.versionArg);
37430
- return {
37431
- id: t.id,
37432
- label: t.label,
37433
- category: t.category,
37434
- installed: version !== null,
37435
- version,
37436
- installable: t.installable,
37437
- updatable: t.updatable
37438
- };
37439
- })
37440
- );
37441
- const extras = await listExtras().catch(() => []);
37442
- return { catalog, extras };
37443
- }
37444
- function opPast(op) {
37445
- return op === "install" ? "installed" : op === "uninstall" ? "uninstalled" : "updated";
37446
- }
37447
- function resolvePackage(name, op) {
37448
- const entry = WORKER_TOOL_CATALOG.find((t) => t.id === name);
37449
- if (entry) {
37450
- const allowed = op === "update" ? entry.updatable : entry.installable;
37451
- if (!allowed) {
37452
- return { error: `"${name}" is a prerequisite and cannot be ${opPast(op)} from here.` };
37453
- }
37454
- return { pkg: entry.installPackage ?? name };
37455
- }
37456
- if (WORKER_TOOL_PREREQUISITE_IDS.includes(name)) {
37457
- return { error: `"${name}" is a prerequisite and cannot be ${opPast(op)} from here.` };
37458
- }
37459
- if (WORKER_TOOL_BUNDLED_GLOBALS.includes(name)) {
37460
- return { error: `"${name}" ships with Node and cannot be ${opPast(op)} from here.` };
37461
- }
37462
- if (!NPM_NAME.test(name)) return { error: `Invalid package name: "${name}".` };
37463
- return { pkg: name };
37464
- }
37465
- function opArgs(op, manager, pkg) {
37466
- if (op === "uninstall") return manager === "pnpm" ? ["remove", "-g", pkg] : ["uninstall", "-g", pkg];
37467
- const target = op === "update" ? `${pkg}@latest` : pkg;
37468
- return manager === "pnpm" ? ["add", "-g", target] : ["install", "-g", target];
37469
- }
37470
- async function runWorkerToolOp(op, name, manager, onLine) {
37471
- const resolved = resolvePackage(name, op);
37472
- if (resolved.error || !resolved.pkg) {
37473
- return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
37474
- }
37475
- onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
37476
- const { code } = await run10(manager, opArgs(op, manager, resolved.pkg), onLine);
37477
- return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
37478
- }
37479
-
37480
37567
  // src/core/ws-client.ts
37481
37568
  var MEMORY_SEED_HEADER = "CONTEXT MEMORY from earlier in this conversation (may span prior sessions/accounts). Use it as background; do not repeat it back unless relevant:";
37482
37569
  var HEARTBEAT_MS = 3e4;
@@ -37518,7 +37605,9 @@ var WsClient = class {
37518
37605
  this.updateScheduler = new UpdateScheduler(
37519
37606
  context.credential.serverUrl,
37520
37607
  context.profileDir,
37521
- context.bus
37608
+ context.bus,
37609
+ // After the idle daily tick updates flagged tools, report the fresh snapshot (ADR-0254).
37610
+ () => void this.reportWorkerTools()
37522
37611
  );
37523
37612
  context.bus.onLocalSubmit((input) => void this.runLocalCommand(input));
37524
37613
  context.bus.onReconnect(() => this.reconnectNow());
@@ -37635,6 +37724,20 @@ var WsClient = class {
37635
37724
  get isStopped() {
37636
37725
  return this.stopped;
37637
37726
  }
37727
+ /**
37728
+ * Detect the worker's tools and report the snapshot to the server (ADR-0254, `tools.report`) so the
37729
+ * DB-backed Tools panel + restore target stay current. One-way, best-effort — a detect/send failure
37730
+ * never disrupts the session. Called after each tool op, after the daily tick, and after a restore.
37731
+ */
37732
+ async reportWorkerTools() {
37733
+ try {
37734
+ const autoUpdate = readProfileConfig(this.context.profileDir).autoUpdateTools ?? [];
37735
+ const { catalog, extras } = await detectWorkerTools(autoUpdate);
37736
+ this.send(WsChannels.TOOLS_REPORT, { catalog, extras });
37737
+ } catch (err) {
37738
+ logger.warn("tools.report.error", { error: String(err) });
37739
+ }
37740
+ }
37638
37741
  /**
37639
37742
  * Force an immediate reconnect: wake the backoff sleep if waiting, else drop the live
37640
37743
  * socket so the run() loop reconnects (the next backoff is skipped). No-op if stopped.
@@ -37785,7 +37888,10 @@ var WsClient = class {
37785
37888
  projectAutoClearIdleMinutes: token.projectTokens?.autoClearIdleMinutes ?? 0,
37786
37889
  // Project shared-AI-memory override mirrors (ADR-0245).
37787
37890
  projectAiMemoryMode: token.aiMemory?.mode ?? "inherit",
37788
- projectAiMemoryBudgetChars: token.aiMemory?.budgetChars ?? 0
37891
+ projectAiMemoryBudgetChars: token.aiMemory?.budgetChars ?? 0,
37892
+ // Per-tool auto-update flags (ADR-0253) are now DB-owned (ADR-0254) — mirror the server's
37893
+ // list locally so the idle daily tick (UpdateScheduler) can read it without a round-trip.
37894
+ autoUpdateTools: token.toolRestore?.autoUpdate ?? []
37789
37895
  });
37790
37896
  if (token.aiMemory && !this.aiMemory) this.aiMemory = token.aiMemory.text;
37791
37897
  this.outboundReviewEnabled = token.outboundReview?.enabled === true;
@@ -38263,10 +38369,16 @@ var WsClient = class {
38263
38369
  break;
38264
38370
  }
38265
38371
  case WsChannels.TOOLS_LIST:
38266
- void detectWorkerTools().then(
38372
+ void detectWorkerTools(readProfileConfig(this.context.profileDir).autoUpdateTools ?? []).then(
38267
38373
  (reply) => this.send(WsChannels.TOOLS_LIST, reply, message.id)
38268
38374
  );
38269
38375
  break;
38376
+ case WsChannels.TOOLS_AUTOUPDATE: {
38377
+ const req = payload;
38378
+ const res = setToolAutoUpdate(this.context.profileDir, req.name, req.enabled);
38379
+ this.send(WsChannels.TOOLS_AUTOUPDATE, res, message.id);
38380
+ break;
38381
+ }
38270
38382
  case WsChannels.TOOLS_INSTALL:
38271
38383
  case WsChannels.TOOLS_UNINSTALL:
38272
38384
  case WsChannels.TOOLS_UPDATE: {
@@ -38278,13 +38390,28 @@ var WsClient = class {
38278
38390
  req.name,
38279
38391
  req.manager,
38280
38392
  (line) => this.send(WsChannels.TOOLS_PROGRESS, { opId: req.opId, line })
38281
- ).then(
38282
- (res) => this.send(WsChannels.TOOLS_DONE, {
38393
+ ).then((res) => {
38394
+ this.send(WsChannels.TOOLS_DONE, {
38283
38395
  opId: req.opId,
38284
38396
  ok: res.ok,
38285
38397
  exitCode: res.exitCode,
38286
38398
  error: res.error
38287
- })
38399
+ });
38400
+ void this.reportWorkerTools();
38401
+ });
38402
+ break;
38403
+ }
38404
+ case WsChannels.TOOLS_RESTORE: {
38405
+ const req = payload;
38406
+ void reconcileTools(
38407
+ req.manifest.map((m) => ({ name: m.name, version: m.version, manager: m.manager })),
38408
+ (line) => this.bus.log(line)
38409
+ ).then(() => this.reportWorkerTools()).then(() => this.send(WsChannels.TOOLS_RESTORE, { ok: true }, message.id)).catch(
38410
+ (err) => this.send(
38411
+ WsChannels.TOOLS_RESTORE,
38412
+ { ok: false, error: String(err) },
38413
+ message.id
38414
+ )
38288
38415
  );
38289
38416
  break;
38290
38417
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.5.3",
3
+ "version": "1.5.11",
4
4
  "private": false,
5
5
  "description": "4PM CLI — drives Claude/Codex CLIs on AI worker machines (Node + TypeScript)",
6
6
  "license": "LicenseRef-4PM-Source-Available",