@4pm/cli 1.5.2 → 1.5.11-b

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 +322 -181
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28834,18 +28834,21 @@ function isValidRoleSet(roles) {
28834
28834
 
28835
28835
  // vendor/@4pm/constants/worker-tools.ts
28836
28836
  var WORKER_TOOL_CATALOG = [
28837
- { id: "git", label: "Git", category: "vcs", versionArg: "--version", installPackage: null, installable: false },
28838
- { id: "node", label: "Node.js", category: "runtime", versionArg: "--version", installPackage: null, installable: false },
28839
- { id: "npm", label: "npm", category: "runtime", versionArg: "--version", installPackage: null, installable: false },
28840
- { id: "pnpm", label: "pnpm", category: "runtime", versionArg: "--version", installPackage: "pnpm", installable: false },
28841
- { id: "claude", label: "Claude Code", category: "ai-cli", versionArg: "--version", installPackage: "@anthropic-ai/claude-code", installable: false },
28842
- { id: "codex", label: "Codex", category: "ai-cli", versionArg: "--version", installPackage: "@openai/codex", installable: false },
28843
- { id: "gh", label: "GitHub CLI", category: "vcs", versionArg: "--version", installPackage: null, installable: false },
28844
- { id: "glab", label: "GitLab CLI", category: "vcs", versionArg: "--version", installPackage: null, installable: false }
28837
+ { id: "git", label: "Git", category: "vcs", versionArg: "--version", installPackage: null, installable: false, updatable: false },
28838
+ { id: "node", label: "Node.js", category: "runtime", versionArg: "--version", installPackage: null, installable: false, updatable: false },
28839
+ { id: "npm", label: "npm", category: "runtime", versionArg: "--version", installPackage: null, installable: false, updatable: false },
28840
+ { id: "pnpm", label: "pnpm", category: "runtime", versionArg: "--version", installPackage: "pnpm", installable: false, updatable: true },
28841
+ { id: "claude", label: "Claude Code", category: "ai-cli", versionArg: "--version", installPackage: "@anthropic-ai/claude-code", installable: false, updatable: true },
28842
+ { id: "codex", label: "Codex", category: "ai-cli", versionArg: "--version", installPackage: "@openai/codex", installable: false, updatable: true },
28843
+ { id: "gh", label: "GitHub CLI", category: "vcs", versionArg: "--version", installPackage: null, installable: false, updatable: false },
28844
+ { id: "glab", label: "GitLab CLI", category: "vcs", versionArg: "--version", installPackage: null, installable: false, updatable: false }
28845
28845
  ];
28846
28846
  var WORKER_TOOL_PREREQUISITE_IDS = WORKER_TOOL_CATALOG.filter(
28847
28847
  (t) => !t.installable
28848
28848
  ).map((t) => t.id);
28849
+ var WORKER_TOOL_UPDATABLE_IDS = WORKER_TOOL_CATALOG.filter(
28850
+ (t) => t.updatable
28851
+ ).map((t) => t.id);
28849
28852
  var WORKER_TOOL_BUNDLED_GLOBALS = ["npm", "corepack"];
28850
28853
 
28851
28854
  // vendor/@4pm/constants/ws.ts
@@ -32909,7 +32912,7 @@ var deletedFilterSchema = external_exports.enum(["true", "false"]).transform((v)
32909
32912
 
32910
32913
  // vendor/@4pm/dto/command.ts
32911
32914
  var COMMAND_MAX_LEN = 8e3;
32912
- var AI_PROMPT_MAX_LEN = 2e5;
32915
+ var AI_PROMPT_MAX_LEN = 5e5;
32913
32916
  var dispatchCommandRequestSchema = external_exports.object({
32914
32917
  projectId: external_exports.string().uuid(),
32915
32918
  // Target cli. Required unless `pick` is set — with `pick:"idle"` the server chooses the
@@ -33065,12 +33068,24 @@ var listMachineLinksQuerySchema = baseRequestSchema.extend({
33065
33068
  var updateMachineLinkRequestSchema = external_exports.object({
33066
33069
  hashcode3ExpiresAt: external_exports.string().datetime().nullable()
33067
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
+ });
33068
33077
  var workerToolInstallSchema = external_exports.object({
33069
33078
  /** Catalog id or npm package name (validated npm-name shape on the cli). */
33070
33079
  name: external_exports.string().trim().min(1).max(214),
33071
33080
  /** Package manager to run the global install with. */
33072
33081
  manager: external_exports.enum(["npm", "pnpm"])
33073
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
+ });
33074
33089
  var configWriteRequestSchema = external_exports.object({
33075
33090
  config: external_exports.string().max(256 * 1024)
33076
33091
  });
@@ -33675,8 +33690,27 @@ var WsChannels = {
33675
33690
  TOOLS_LIST: "tools.list",
33676
33691
  TOOLS_INSTALL: "tools.install",
33677
33692
  TOOLS_UNINSTALL: "tools.uninstall",
33693
+ // update-to-latest of an npm-distributed catalog tool or an extra (ADR-0252): same streamed
33694
+ // shape as install/uninstall — the cli reinstalls the package at `@latest`.
33695
+ TOOLS_UPDATE: "tools.update",
33678
33696
  TOOLS_PROGRESS: "tools.progress",
33679
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",
33680
33714
  // autonomous mode control (ADR-0152): read settings+status+books+approvals, tail logs, and
33681
33715
  // write {settings|approvals|userTodo|cron} — the dashboard drives the worker's autonomous engine.
33682
33716
  AUTONOMOUS_READ: "autonomous.read",
@@ -33837,7 +33871,7 @@ import { existsSync as existsSync13, mkdirSync as mkdirSync5, rmSync as rmSync4
33837
33871
  import { randomUUID as randomCommandId } from "crypto";
33838
33872
 
33839
33873
  // src/core/update-scheduler.ts
33840
- import { spawn } from "child_process";
33874
+ import { spawn as spawn2 } from "child_process";
33841
33875
 
33842
33876
  // src/common/logger/logger.ts
33843
33877
  import { appendFile, mkdir, readdir, unlink } from "fs/promises";
@@ -33942,17 +33976,191 @@ ${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
33942
33976
  }
33943
33977
  }
33944
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
+
33945
34151
  // src/core/update-scheduler.ts
33946
34152
  var TICK_MS = 6e4;
33947
34153
  var UpdateScheduler = class {
33948
- constructor(serverUrl, profileDir2, bus) {
34154
+ constructor(serverUrl, profileDir2, bus, onToolsChanged) {
33949
34155
  this.serverUrl = serverUrl;
33950
34156
  this.profileDir = profileDir2;
33951
34157
  this.bus = bus;
34158
+ this.onToolsChanged = onToolsChanged;
33952
34159
  }
33953
34160
  serverUrl;
33954
34161
  profileDir;
33955
34162
  bus;
34163
+ onToolsChanged;
33956
34164
  policy = null;
33957
34165
  timer = null;
33958
34166
  /** Date key (YYYY-MM-DD in org tz) whose update already ran — enforces once per day. */
@@ -34024,6 +34232,7 @@ var UpdateScheduler = class {
34024
34232
  async runUpdate() {
34025
34233
  this.updating = true;
34026
34234
  try {
34235
+ await this.updateFlaggedTools();
34027
34236
  this.bus.log("Scheduled auto-update: checking for a new version\u2026");
34028
34237
  const result = await updateToLatest(this.serverUrl);
34029
34238
  if (result.action === "already-latest") {
@@ -34037,7 +34246,7 @@ var UpdateScheduler = class {
34037
34246
  }
34038
34247
  logger.info("update.scheduled.updated", { version: result.version });
34039
34248
  this.bus.log(`Updated to ${result.version} \u2014 restarting to apply\u2026`);
34040
- const child = spawn(process.execPath, process.argv.slice(1), {
34249
+ const child = spawn2(process.execPath, process.argv.slice(1), {
34041
34250
  stdio: "inherit",
34042
34251
  env: { ...process.env, FOURPM_NO_UPDATE: "1" }
34043
34252
  });
@@ -34048,16 +34257,26 @@ var UpdateScheduler = class {
34048
34257
  this.updating = false;
34049
34258
  }
34050
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
+ }
34051
34270
  };
34052
34271
 
34053
34272
  // src/core/executor.ts
34054
- import { spawn as spawn2 } from "child_process";
34273
+ import { spawn as spawn3 } from "child_process";
34055
34274
  var KILL_GRACE_MS = 5e3;
34056
34275
  async function runCommand(dispatch, emit, opts) {
34057
34276
  let seq = 0;
34058
34277
  let child;
34059
34278
  try {
34060
- child = spawn2(dispatch.cmd, dispatch.args ?? [], {
34279
+ child = spawn3(dispatch.cmd, dispatch.args ?? [], {
34061
34280
  cwd: dispatch.path,
34062
34281
  env: { ...process.env, ...dispatch.env },
34063
34282
  shell: false
@@ -34651,7 +34870,7 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot) {
34651
34870
  }
34652
34871
 
34653
34872
  // src/core/claude-usage.ts
34654
- import { spawn as spawn3 } from "child_process";
34873
+ import { spawn as spawn4 } from "child_process";
34655
34874
  import { readFileSync as readFileSync9 } from "fs";
34656
34875
  import { join as join9 } from "path";
34657
34876
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
@@ -34698,7 +34917,7 @@ function runUsageCli(dir2, aiCli2) {
34698
34917
  }, 3e4);
34699
34918
  timer.unref?.();
34700
34919
  try {
34701
- child = spawn3(aiCli2, ["/usage"], {
34920
+ child = spawn4(aiCli2, ["/usage"], {
34702
34921
  env: { ...process.env, CLAUDE_CONFIG_DIR: dir2 },
34703
34922
  stdio: ["ignore", "pipe", "ignore"]
34704
34923
  });
@@ -34768,7 +34987,7 @@ async function checkClaudeUsage(dirs, aiCli2 = "claude") {
34768
34987
  // src/core/ai-assist.ts
34769
34988
  import { execFile } from "child_process";
34770
34989
  import { promisify } from "util";
34771
- var run = promisify(execFile);
34990
+ var run2 = promisify(execFile);
34772
34991
  function aiCli() {
34773
34992
  return process.env.AI_CLI || "claude";
34774
34993
  }
@@ -34811,7 +35030,7 @@ function parseAi(cli, stdout, prompt) {
34811
35030
  }
34812
35031
  async function runAi(prompt) {
34813
35032
  const cli = aiCli();
34814
- const { stdout } = await run(cli, aiArgs(cli, prompt), {
35033
+ const { stdout } = await run2(cli, aiArgs(cli, prompt), {
34815
35034
  timeout: 12e4,
34816
35035
  maxBuffer: 8 * 1024 * 1024
34817
35036
  });
@@ -35154,7 +35373,7 @@ async function writeWorkerFile(root, relPath, content) {
35154
35373
  }
35155
35374
 
35156
35375
  // src/core/support-answer.ts
35157
- import { spawn as spawn4 } from "child_process";
35376
+ import { spawn as spawn5 } from "child_process";
35158
35377
  import { execFile as execFile2 } from "child_process";
35159
35378
  import { createHash as createHash3 } from "crypto";
35160
35379
  import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
@@ -35289,7 +35508,7 @@ function runClaudeOnce(cmd, profile, prompt, extraEnv) {
35289
35508
  ...extraEnv,
35290
35509
  ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
35291
35510
  };
35292
- const child = spawn4(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
35511
+ const child = spawn5(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
35293
35512
  const parser = createAiStreamParser(cmd);
35294
35513
  let out = "";
35295
35514
  let err = "";
@@ -35359,7 +35578,7 @@ async function runSupportAnswer(req, ai, profileDir2) {
35359
35578
  }
35360
35579
 
35361
35580
  // src/core/knowledge-compose.ts
35362
- import { spawn as spawn5 } from "child_process";
35581
+ import { spawn as spawn6 } from "child_process";
35363
35582
  var COMPOSE_TIMEOUT_MS = 18e4;
35364
35583
  function buildPrompt2(req) {
35365
35584
  const focus = req.prompt ? `
@@ -35385,7 +35604,7 @@ function runOnce(cmd, profile, prompt, cwd2, extraEnv) {
35385
35604
  ...extraEnv,
35386
35605
  ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
35387
35606
  };
35388
- 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 });
35389
35608
  let out = "";
35390
35609
  let err = "";
35391
35610
  const timer = setTimeout(() => {
@@ -35433,10 +35652,10 @@ async function runKnowledgeCompose(req, ai, physicRoot) {
35433
35652
  // src/core/autonomous.ts
35434
35653
  import { readFile as readFile3, writeFile as writeFile2, chmod } from "fs/promises";
35435
35654
  import { readFileSync as readFileSync13 } from "fs";
35436
- import { execFile as execFile3, spawn as spawn6 } from "child_process";
35655
+ import { execFile as execFile3, spawn as spawn7 } from "child_process";
35437
35656
  import { join as join13 } from "path";
35438
35657
  import { promisify as promisify3 } from "util";
35439
- var run2 = promisify3(execFile3);
35658
+ var run3 = promisify3(execFile3);
35440
35659
  var SETTINGS_REL = ".claude/.autonomous.settings.json";
35441
35660
  var HISTORIES_REL = ".claude/.autonomous.histories.json";
35442
35661
  var APPROVALS_REL = ".claude/.autonomous.approvals.json";
@@ -35470,7 +35689,7 @@ function tickScript(root) {
35470
35689
  }
35471
35690
  async function crontabList() {
35472
35691
  try {
35473
- const { stdout } = await run2("crontab", ["-l"], { timeout: 1e4 });
35692
+ const { stdout } = await run3("crontab", ["-l"], { timeout: 1e4 });
35474
35693
  return stdout;
35475
35694
  } catch {
35476
35695
  return "";
@@ -35478,7 +35697,7 @@ async function crontabList() {
35478
35697
  }
35479
35698
  async function crontabSet(content) {
35480
35699
  return new Promise((resolve8) => {
35481
- const child = spawn6("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
35700
+ const child = spawn7("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
35482
35701
  child.on("error", () => resolve8(false));
35483
35702
  child.on("close", (code) => resolve8(code === 0));
35484
35703
  child.stdin.end(content.endsWith("\n") ? content : content + "\n");
@@ -36209,15 +36428,15 @@ async function buildGraph(root, mode) {
36209
36428
  // src/core/rag.ts
36210
36429
  import { readFile as readFile9, writeFile as writeFile7, statfs as statfs2, mkdir as mkdir6 } from "fs/promises";
36211
36430
  import { existsSync as existsSync10 } from "fs";
36212
- import { execFile as execFile4, spawn as spawn7 } from "child_process";
36431
+ import { execFile as execFile4, spawn as spawn8 } from "child_process";
36213
36432
  import { totalmem as totalmem2, cpus as cpus2 } from "os";
36214
36433
  import { join as join20 } from "path";
36215
36434
  import { promisify as promisify4 } from "util";
36216
- var run3 = promisify4(execFile4);
36435
+ var run4 = promisify4(execFile4);
36217
36436
  var RAG_REL = ".claude/rag";
36218
36437
  async function tryRun(cmd, args) {
36219
36438
  try {
36220
- const { stdout } = await run3(cmd, args, { timeout: 8e3 });
36439
+ const { stdout } = await run4(cmd, args, { timeout: 8e3 });
36221
36440
  return { ok: true, out: stdout.trim() };
36222
36441
  } catch {
36223
36442
  return { ok: false, out: "" };
@@ -36396,7 +36615,7 @@ touch "${RAG_REL}/.indexing"
36396
36615
  { echo "[reindex] $(date)"; "$RAG_PY" "${RAG_REL}/rag_engine.py" reindex 2>&1; } >> "${RAG_REL}/index.log" 2>&1
36397
36616
  rm -f "${RAG_REL}/.indexing"
36398
36617
  `;
36399
- const child = spawn7("bash", ["-c", script], {
36618
+ const child = spawn8("bash", ["-c", script], {
36400
36619
  cwd: root,
36401
36620
  env: { ...process.env, RAG_PY: py.bin },
36402
36621
  detached: true,
@@ -36411,7 +36630,7 @@ async function ragQuery(root, queryText, k = 8) {
36411
36630
  if (!existsSync10(join20(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
36412
36631
  await ensureEngine(root);
36413
36632
  try {
36414
- const { stdout } = await run3(
36633
+ const { stdout } = await run4(
36415
36634
  py.bin,
36416
36635
  [join20(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k, 1), 20))],
36417
36636
  { cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
@@ -36446,7 +36665,7 @@ rm -f "${RAG_REL}/.installed"
36446
36665
  } >> "${RAG_REL}/install.log" 2>&1
36447
36666
  rm -f "${RAG_REL}/.installing"
36448
36667
  `;
36449
- const child = spawn7("bash", ["-c", script], {
36668
+ const child = spawn8("bash", ["-c", script], {
36450
36669
  cwd: root,
36451
36670
  env: { ...process.env, RAG_PY: py.bin, RAG_MODEL: model },
36452
36671
  detached: true,
@@ -36461,7 +36680,7 @@ import { readFile as readFile10 } from "fs/promises";
36461
36680
  import { execFile as execFile5 } from "child_process";
36462
36681
  import { basename as basename3, dirname as dirname7, resolve as resolve5 } from "path";
36463
36682
  import { promisify as promisify5 } from "util";
36464
- var run4 = promisify5(execFile5);
36683
+ var run5 = promisify5(execFile5);
36465
36684
  async function gitDiff(path) {
36466
36685
  const target = resolve5(path);
36467
36686
  const dir2 = dirname7(target);
@@ -36473,7 +36692,7 @@ async function gitDiff(path) {
36473
36692
  }
36474
36693
  let oldContent = "";
36475
36694
  try {
36476
- const { stdout } = await run4("git", ["show", `HEAD:./${base}`], {
36695
+ const { stdout } = await run5("git", ["show", `HEAD:./${base}`], {
36477
36696
  cwd: dir2,
36478
36697
  timeout: 15e3,
36479
36698
  maxBuffer: 4 * 1024 * 1024
@@ -36487,10 +36706,10 @@ async function gitDiff(path) {
36487
36706
  // src/core/git-env.ts
36488
36707
  import { execFile as execFile6 } from "child_process";
36489
36708
  import { promisify as promisify6 } from "util";
36490
- var run5 = promisify6(execFile6);
36709
+ var run6 = promisify6(execFile6);
36491
36710
  async function hasBinary(bin, args = ["--version"]) {
36492
36711
  try {
36493
- await run5(bin, args, { timeout: 5e3 });
36712
+ await run6(bin, args, { timeout: 5e3 });
36494
36713
  return true;
36495
36714
  } catch {
36496
36715
  return false;
@@ -36503,7 +36722,7 @@ async function checkGitEnv(provider) {
36503
36722
  let account = null;
36504
36723
  if (installed) {
36505
36724
  try {
36506
- const { stdout, stderr } = await run5(bin, ["auth", "status"], { timeout: 8e3 });
36725
+ const { stdout, stderr } = await run6(bin, ["auth", "status"], { timeout: 8e3 });
36507
36726
  const out = `${stdout}
36508
36727
  ${stderr}`;
36509
36728
  authenticated = /logged in|logged into|✓/i.test(out);
@@ -36520,7 +36739,7 @@ import { execFile as execFile7 } from "child_process";
36520
36739
  import { readFile as readFile11, rm as rm3, mkdir as mkdir7 } from "fs/promises";
36521
36740
  import { join as join21 } from "path";
36522
36741
  import { promisify as promisify7 } from "util";
36523
- var run6 = promisify7(execFile7);
36742
+ var run7 = promisify7(execFile7);
36524
36743
  function keyPaths(profileDir2) {
36525
36744
  const dir2 = join21(profileDir2, ".ssh");
36526
36745
  const key = join21(dir2, "id_4pm");
@@ -36535,14 +36754,14 @@ async function readPublicKey(pub) {
36535
36754
  }
36536
36755
  async function fingerprintOf(pub) {
36537
36756
  try {
36538
- const { stdout } = await run6("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
36757
+ const { stdout } = await run7("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
36539
36758
  return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
36540
36759
  } catch {
36541
36760
  return null;
36542
36761
  }
36543
36762
  }
36544
36763
  async function configureGit(key) {
36545
- await run6(
36764
+ await run7(
36546
36765
  "git",
36547
36766
  ["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
36548
36767
  { timeout: 5e3 }
@@ -36553,7 +36772,7 @@ async function generate(profileDir2) {
36553
36772
  await mkdir7(dir2, { recursive: true });
36554
36773
  await rm3(key, { force: true });
36555
36774
  await rm3(pub, { force: true });
36556
- 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"], {
36557
36776
  timeout: 15e3
36558
36777
  });
36559
36778
  await configureGit(key);
@@ -36563,7 +36782,7 @@ async function remove(profileDir2) {
36563
36782
  const { key, pub } = keyPaths(profileDir2);
36564
36783
  await rm3(key, { force: true });
36565
36784
  await rm3(pub, { force: true });
36566
- await run6("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
36785
+ await run7("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
36567
36786
  () => void 0
36568
36787
  );
36569
36788
  return { publicKey: null, fingerprint: null };
@@ -36588,7 +36807,7 @@ import { readdir as readdir6 } from "fs/promises";
36588
36807
  import { execFile as execFile8 } from "child_process";
36589
36808
  import { resolve as resolve6, sep as sep4 } from "path";
36590
36809
  import { promisify as promisify8 } from "util";
36591
- var run7 = promisify8(execFile8);
36810
+ var run8 = promisify8(execFile8);
36592
36811
  var FS = "";
36593
36812
  var RS = "";
36594
36813
  function resolveRepo(root, repo) {
@@ -36599,7 +36818,7 @@ function resolveRepo(root, repo) {
36599
36818
  }
36600
36819
  async function git(cwd2, args) {
36601
36820
  try {
36602
- const { stdout } = await run7("git", args, {
36821
+ const { stdout } = await run8("git", args, {
36603
36822
  cwd: cwd2,
36604
36823
  timeout: 15e3,
36605
36824
  maxBuffer: 8 * 1024 * 1024
@@ -36703,11 +36922,11 @@ async function gitCommitDiff(root, repo, hash, path) {
36703
36922
  // src/core/git-commit-identity.ts
36704
36923
  import { execFile as execFile9 } from "child_process";
36705
36924
  import { promisify as promisify9 } from "util";
36706
- var run8 = promisify9(execFile9);
36925
+ var run9 = promisify9(execFile9);
36707
36926
  async function setCommitAuthor(cwd2, machineUsername) {
36708
36927
  if (!machineUsername) return;
36709
36928
  try {
36710
- 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 });
36711
36930
  } catch {
36712
36931
  }
36713
36932
  }
@@ -36718,7 +36937,7 @@ import { execFile as execFile10 } from "child_process";
36718
36937
  import { dirname as dirname8, join as join22, resolve as resolve7 } from "path";
36719
36938
  import { fileURLToPath as fileURLToPath3 } from "url";
36720
36939
  import { promisify as promisify10 } from "util";
36721
- var run9 = promisify10(execFile10);
36940
+ var run10 = promisify10(execFile10);
36722
36941
  function reposOf(spec) {
36723
36942
  const repos = spec?.repos;
36724
36943
  return Array.isArray(repos) ? repos : [];
@@ -36732,13 +36951,13 @@ async function provisionRepos(target, repos, emit, opts = {}) {
36732
36951
  if (repo.primary) {
36733
36952
  if (opts.clonePrimary && repo.url) {
36734
36953
  emit("git", `Cloning ${repo.url}\u2026`);
36735
- await run9("git", ["clone", repo.url, target], { timeout: 12e4 });
36954
+ await run10("git", ["clone", repo.url, target], { timeout: 12e4 });
36736
36955
  continue;
36737
36956
  }
36738
36957
  emit("git", "Initializing the primary repo\u2026");
36739
- await run9("git", ["init"], { cwd: target, timeout: 2e4 });
36958
+ await run10("git", ["init"], { cwd: target, timeout: 2e4 });
36740
36959
  if (repo.url) {
36741
- 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(
36742
36961
  () => void 0
36743
36962
  );
36744
36963
  }
@@ -36747,11 +36966,11 @@ async function provisionRepos(target, repos, emit, opts = {}) {
36747
36966
  const dir2 = join22(target, subDirName(repo));
36748
36967
  if (repo.url) {
36749
36968
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
36750
- await run9("git", ["clone", repo.url, dir2], { timeout: 12e4 });
36969
+ await run10("git", ["clone", repo.url, dir2], { timeout: 12e4 });
36751
36970
  } else {
36752
36971
  emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
36753
36972
  await mkdir8(dir2, { recursive: true });
36754
- await run9("git", ["init"], { cwd: dir2, timeout: 2e4 });
36973
+ await run10("git", ["init"], { cwd: dir2, timeout: 2e4 });
36755
36974
  }
36756
36975
  }
36757
36976
  }
@@ -37229,7 +37448,7 @@ function runSlashCommand(line, base) {
37229
37448
  }
37230
37449
 
37231
37450
  // src/core/memory-compact.ts
37232
- import { spawn as spawn8 } from "child_process";
37451
+ import { spawn as spawn9 } from "child_process";
37233
37452
  var COMPACT_TIMEOUT_MS = 6e4;
37234
37453
  function buildPrompt3(input) {
37235
37454
  return [
@@ -37257,7 +37476,7 @@ function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
37257
37476
  ...extraEnv,
37258
37477
  ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
37259
37478
  };
37260
- 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 });
37261
37480
  let out = "";
37262
37481
  let err = "";
37263
37482
  const timer = setTimeout(() => {
@@ -37345,125 +37564,6 @@ function applyConfigText(profileDir2, text) {
37345
37564
  return { ok: true };
37346
37565
  }
37347
37566
 
37348
- // src/core/worker-tools.ts
37349
- import { spawn as spawn9 } from "child_process";
37350
- var NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
37351
- function run10(cmd, args, onLine, timeoutMs = 18e4) {
37352
- return new Promise((resolve8) => {
37353
- let out = "";
37354
- let buf = "";
37355
- let child = null;
37356
- const done = (code) => {
37357
- clearTimeout(timer);
37358
- if (onLine && buf.trim()) onLine(buf.trim());
37359
- resolve8({ code, out });
37360
- };
37361
- const timer = setTimeout(() => {
37362
- child?.kill();
37363
- if (onLine) onLine(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
37364
- done(124);
37365
- }, timeoutMs);
37366
- timer.unref?.();
37367
- const feed = (d) => {
37368
- const text = d.toString("utf8");
37369
- out += text;
37370
- if (!onLine) return;
37371
- buf += text;
37372
- let nl;
37373
- while ((nl = buf.indexOf("\n")) >= 0) {
37374
- const line = buf.slice(0, nl).replace(/\r$/, "");
37375
- buf = buf.slice(nl + 1);
37376
- onLine(line);
37377
- }
37378
- };
37379
- try {
37380
- child = spawn9(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
37381
- child.stdout?.on("data", feed);
37382
- child.stderr?.on("data", feed);
37383
- child.on("exit", (code) => done(code ?? 0));
37384
- child.on("error", () => done(127));
37385
- } catch {
37386
- done(127);
37387
- }
37388
- });
37389
- }
37390
- async function detectOne(cmd, versionArg) {
37391
- const { code, out } = await run10(cmd, [versionArg], void 0, 15e3);
37392
- if (code !== 0) return null;
37393
- const first = out.split("\n").map((l) => l.trim()).find(Boolean);
37394
- return first ?? "";
37395
- }
37396
- async function listExtras() {
37397
- const { out } = await run10("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
37398
- if (!out.trim()) return [];
37399
- let deps = {};
37400
- try {
37401
- deps = JSON.parse(out).dependencies ?? {};
37402
- } catch {
37403
- return [];
37404
- }
37405
- const catalogPkgs = new Set(WORKER_TOOL_BUNDLED_GLOBALS);
37406
- for (const t of WORKER_TOOL_CATALOG) {
37407
- catalogPkgs.add(t.id);
37408
- if (t.installPackage) catalogPkgs.add(t.installPackage);
37409
- }
37410
- return Object.entries(deps).filter(([name]) => !catalogPkgs.has(name)).map(([name, info]) => ({
37411
- id: name,
37412
- label: name,
37413
- category: "runtime",
37414
- installed: true,
37415
- version: info.version ?? null,
37416
- installable: true
37417
- }));
37418
- }
37419
- async function detectWorkerTools() {
37420
- const catalog = await Promise.all(
37421
- WORKER_TOOL_CATALOG.map(async (t) => {
37422
- const version = await detectOne(t.id, t.versionArg);
37423
- return {
37424
- id: t.id,
37425
- label: t.label,
37426
- category: t.category,
37427
- installed: version !== null,
37428
- version,
37429
- installable: t.installable
37430
- };
37431
- })
37432
- );
37433
- const extras = await listExtras().catch(() => []);
37434
- return { catalog, extras };
37435
- }
37436
- function resolvePackage(name, op) {
37437
- const entry = WORKER_TOOL_CATALOG.find((t) => t.id === name);
37438
- if (entry) {
37439
- if (!entry.installable) {
37440
- return { error: `"${name}" is a prerequisite and cannot be ${op}ed from here.` };
37441
- }
37442
- return { pkg: entry.installPackage ?? name };
37443
- }
37444
- if (WORKER_TOOL_PREREQUISITE_IDS.includes(name)) {
37445
- return { error: `"${name}" is a prerequisite and cannot be ${op}ed from here.` };
37446
- }
37447
- if (WORKER_TOOL_BUNDLED_GLOBALS.includes(name)) {
37448
- return { error: `"${name}" ships with Node and cannot be ${op}ed from here.` };
37449
- }
37450
- if (!NPM_NAME.test(name)) return { error: `Invalid package name: "${name}".` };
37451
- return { pkg: name };
37452
- }
37453
- function opArgs(op, manager, pkg) {
37454
- if (manager === "pnpm") return [op === "install" ? "add" : "remove", "-g", pkg];
37455
- return [op === "install" ? "install" : "uninstall", "-g", pkg];
37456
- }
37457
- async function runWorkerToolOp(op, name, manager, onLine) {
37458
- const resolved = resolvePackage(name, op);
37459
- if (resolved.error || !resolved.pkg) {
37460
- return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
37461
- }
37462
- onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
37463
- const { code } = await run10(manager, opArgs(op, manager, resolved.pkg), onLine);
37464
- return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
37465
- }
37466
-
37467
37567
  // src/core/ws-client.ts
37468
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:";
37469
37569
  var HEARTBEAT_MS = 3e4;
@@ -37505,7 +37605,9 @@ var WsClient = class {
37505
37605
  this.updateScheduler = new UpdateScheduler(
37506
37606
  context.credential.serverUrl,
37507
37607
  context.profileDir,
37508
- context.bus
37608
+ context.bus,
37609
+ // After the idle daily tick updates flagged tools, report the fresh snapshot (ADR-0254).
37610
+ () => void this.reportWorkerTools()
37509
37611
  );
37510
37612
  context.bus.onLocalSubmit((input) => void this.runLocalCommand(input));
37511
37613
  context.bus.onReconnect(() => this.reconnectNow());
@@ -37622,6 +37724,20 @@ var WsClient = class {
37622
37724
  get isStopped() {
37623
37725
  return this.stopped;
37624
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
+ }
37625
37741
  /**
37626
37742
  * Force an immediate reconnect: wake the backoff sleep if waiting, else drop the live
37627
37743
  * socket so the run() loop reconnects (the next backoff is skipped). No-op if stopped.
@@ -37772,7 +37888,10 @@ var WsClient = class {
37772
37888
  projectAutoClearIdleMinutes: token.projectTokens?.autoClearIdleMinutes ?? 0,
37773
37889
  // Project shared-AI-memory override mirrors (ADR-0245).
37774
37890
  projectAiMemoryMode: token.aiMemory?.mode ?? "inherit",
37775
- 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 ?? []
37776
37895
  });
37777
37896
  if (token.aiMemory && !this.aiMemory) this.aiMemory = token.aiMemory.text;
37778
37897
  this.outboundReviewEnabled = token.outboundReview?.enabled === true;
@@ -38250,27 +38369,49 @@ var WsClient = class {
38250
38369
  break;
38251
38370
  }
38252
38371
  case WsChannels.TOOLS_LIST:
38253
- void detectWorkerTools().then(
38372
+ void detectWorkerTools(readProfileConfig(this.context.profileDir).autoUpdateTools ?? []).then(
38254
38373
  (reply) => this.send(WsChannels.TOOLS_LIST, reply, message.id)
38255
38374
  );
38256
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
+ }
38257
38382
  case WsChannels.TOOLS_INSTALL:
38258
- case WsChannels.TOOLS_UNINSTALL: {
38383
+ case WsChannels.TOOLS_UNINSTALL:
38384
+ case WsChannels.TOOLS_UPDATE: {
38259
38385
  const req = payload;
38260
- const op = message.channel === WsChannels.TOOLS_INSTALL ? "install" : "uninstall";
38386
+ const op = message.channel === WsChannels.TOOLS_INSTALL ? "install" : message.channel === WsChannels.TOOLS_UPDATE ? "update" : "uninstall";
38261
38387
  this.send(message.channel, { started: true }, message.id);
38262
38388
  void runWorkerToolOp(
38263
38389
  op,
38264
38390
  req.name,
38265
38391
  req.manager,
38266
38392
  (line) => this.send(WsChannels.TOOLS_PROGRESS, { opId: req.opId, line })
38267
- ).then(
38268
- (res) => this.send(WsChannels.TOOLS_DONE, {
38393
+ ).then((res) => {
38394
+ this.send(WsChannels.TOOLS_DONE, {
38269
38395
  opId: req.opId,
38270
38396
  ok: res.ok,
38271
38397
  exitCode: res.exitCode,
38272
38398
  error: res.error
38273
- })
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
+ )
38274
38415
  );
38275
38416
  break;
38276
38417
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.5.2",
3
+ "version": "1.5.11b",
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",