@4pm/cli 1.5.1 → 1.5.2-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 +416 -194
  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
@@ -32990,6 +32993,7 @@ var CLI_SLASH_COMMANDS = [
32990
32993
  { name: "collapse", usage: "/collapse [N]", description: "Collapse an expanded \u25B8[N] block" },
32991
32994
  { name: "reconnect", usage: "/reconnect", description: "Reconnect to the server now (skip the backoff)" },
32992
32995
  { name: "whoami", usage: "/whoami", description: "Show this machine account + its teams & projects" },
32996
+ { name: "ai-profile", usage: "/ai-profile [list|use <n>|reset]", description: "List AI profiles / switch which one runs" },
32993
32997
  { name: "config", usage: "/config [show|init|set <k> <v>|delete <k>]", description: "View / init / update / delete profile config" },
32994
32998
  { name: "claude-cmd", usage: "/claude-cmd /context", description: "Run an AI-CLI slash command (e.g. /context, /usage)" },
32995
32999
  { name: "clear", usage: "/clear", description: "Clear the transcript" },
@@ -33038,7 +33042,10 @@ var listWorkersQuerySchema = baseRequestSchema.extend({
33038
33042
  /** Only workers whose live CPU/memory/disk usage % ≥ the given threshold (0–100). */
33039
33043
  minCpuPct: external_exports.coerce.number().min(0).max(100).optional(),
33040
33044
  minMemPct: external_exports.coerce.number().min(0).max(100).optional(),
33041
- minDiskPct: external_exports.coerce.number().min(0).max(100).optional()
33045
+ minDiskPct: external_exports.coerce.number().min(0).max(100).optional(),
33046
+ /** Hide workers with no cli attached (clis = 0). The web defaults this on; unchecking the
33047
+ * "show empty CLIs" box drops it so empty workers are listed too. */
33048
+ hideEmptyClis: external_exports.enum(["true", "false"]).transform((v) => v === "true").optional()
33042
33049
  });
33043
33050
  var updateWorkerRequestSchema = external_exports.object({
33044
33051
  name: external_exports.string().min(1).max(100).optional()
@@ -33067,6 +33074,12 @@ var workerToolInstallSchema = external_exports.object({
33067
33074
  /** Package manager to run the global install with. */
33068
33075
  manager: external_exports.enum(["npm", "pnpm"])
33069
33076
  });
33077
+ var workerToolAutoUpdateSchema = external_exports.object({
33078
+ /** Catalog id or installed extra's npm package name. */
33079
+ name: external_exports.string().trim().min(1).max(214),
33080
+ /** true = flag this tool for auto-update to @latest; false = clear it. */
33081
+ enabled: external_exports.boolean()
33082
+ });
33070
33083
  var configWriteRequestSchema = external_exports.object({
33071
33084
  config: external_exports.string().max(256 * 1024)
33072
33085
  });
@@ -33671,8 +33684,14 @@ var WsChannels = {
33671
33684
  TOOLS_LIST: "tools.list",
33672
33685
  TOOLS_INSTALL: "tools.install",
33673
33686
  TOOLS_UNINSTALL: "tools.uninstall",
33687
+ // update-to-latest of an npm-distributed catalog tool or an extra (ADR-0252): same streamed
33688
+ // shape as install/uninstall — the cli reinstalls the package at `@latest`.
33689
+ TOOLS_UPDATE: "tools.update",
33674
33690
  TOOLS_PROGRESS: "tools.progress",
33675
33691
  TOOLS_DONE: "tools.done",
33692
+ // toggle per-tool auto-update (ADR-0253): request/reply — the cli persists the flag in
33693
+ // config.json (`autoUpdateTools`); the ADR-0074 idle daily tick later runs the flagged tools.
33694
+ TOOLS_AUTOUPDATE: "tools.autoUpdate",
33676
33695
  // autonomous mode control (ADR-0152): read settings+status+books+approvals, tail logs, and
33677
33696
  // write {settings|approvals|userTodo|cron} — the dashboard drives the worker's autonomous engine.
33678
33697
  AUTONOMOUS_READ: "autonomous.read",
@@ -33833,7 +33852,7 @@ import { existsSync as existsSync13, mkdirSync as mkdirSync5, rmSync as rmSync4
33833
33852
  import { randomUUID as randomCommandId } from "crypto";
33834
33853
 
33835
33854
  // src/core/update-scheduler.ts
33836
- import { spawn } from "child_process";
33855
+ import { spawn as spawn2 } from "child_process";
33837
33856
 
33838
33857
  // src/common/logger/logger.ts
33839
33858
  import { appendFile, mkdir, readdir, unlink } from "fs/promises";
@@ -33938,6 +33957,153 @@ ${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
33938
33957
  }
33939
33958
  }
33940
33959
 
33960
+ // src/core/worker-tools.ts
33961
+ import { spawn } from "child_process";
33962
+ var NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
33963
+ function run(cmd, args, onLine, timeoutMs = 18e4) {
33964
+ return new Promise((resolve8) => {
33965
+ let out = "";
33966
+ let buf = "";
33967
+ let child = null;
33968
+ const done = (code) => {
33969
+ clearTimeout(timer);
33970
+ if (onLine && buf.trim()) onLine(buf.trim());
33971
+ resolve8({ code, out });
33972
+ };
33973
+ const timer = setTimeout(() => {
33974
+ child?.kill();
33975
+ if (onLine) onLine(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
33976
+ done(124);
33977
+ }, timeoutMs);
33978
+ timer.unref?.();
33979
+ const feed = (d) => {
33980
+ const text = d.toString("utf8");
33981
+ out += text;
33982
+ if (!onLine) return;
33983
+ buf += text;
33984
+ let nl;
33985
+ while ((nl = buf.indexOf("\n")) >= 0) {
33986
+ const line = buf.slice(0, nl).replace(/\r$/, "");
33987
+ buf = buf.slice(nl + 1);
33988
+ onLine(line);
33989
+ }
33990
+ };
33991
+ try {
33992
+ child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
33993
+ child.stdout?.on("data", feed);
33994
+ child.stderr?.on("data", feed);
33995
+ child.on("exit", (code) => done(code ?? 0));
33996
+ child.on("error", () => done(127));
33997
+ } catch {
33998
+ done(127);
33999
+ }
34000
+ });
34001
+ }
34002
+ async function detectOne(cmd, versionArg) {
34003
+ const { code, out } = await run(cmd, [versionArg], void 0, 15e3);
34004
+ if (code !== 0) return null;
34005
+ const first = out.split("\n").map((l) => l.trim()).find(Boolean);
34006
+ return first ?? "";
34007
+ }
34008
+ async function listExtras(autoUpdateSet = /* @__PURE__ */ new Set()) {
34009
+ const { out } = await run("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
34010
+ if (!out.trim()) return [];
34011
+ let deps = {};
34012
+ try {
34013
+ deps = JSON.parse(out).dependencies ?? {};
34014
+ } catch {
34015
+ return [];
34016
+ }
34017
+ const catalogPkgs = new Set(WORKER_TOOL_BUNDLED_GLOBALS);
34018
+ for (const t of WORKER_TOOL_CATALOG) {
34019
+ catalogPkgs.add(t.id);
34020
+ if (t.installPackage) catalogPkgs.add(t.installPackage);
34021
+ }
34022
+ return Object.entries(deps).filter(([name]) => !catalogPkgs.has(name)).map(([name, info]) => ({
34023
+ id: name,
34024
+ label: name,
34025
+ category: "runtime",
34026
+ installed: true,
34027
+ version: info.version ?? null,
34028
+ installable: true,
34029
+ updatable: true,
34030
+ autoUpdate: autoUpdateSet.has(name)
34031
+ }));
34032
+ }
34033
+ async function detectWorkerTools(autoUpdateTools = []) {
34034
+ const autoUpdateSet = new Set(autoUpdateTools);
34035
+ const catalog = await Promise.all(
34036
+ WORKER_TOOL_CATALOG.map(async (t) => {
34037
+ const version = await detectOne(t.id, t.versionArg);
34038
+ return {
34039
+ id: t.id,
34040
+ label: t.label,
34041
+ category: t.category,
34042
+ installed: version !== null,
34043
+ version,
34044
+ installable: t.installable,
34045
+ updatable: t.updatable,
34046
+ autoUpdate: autoUpdateSet.has(t.id)
34047
+ };
34048
+ })
34049
+ );
34050
+ const extras = await listExtras(autoUpdateSet).catch(() => []);
34051
+ return { catalog, extras };
34052
+ }
34053
+ function opPast(op) {
34054
+ return op === "install" ? "installed" : op === "uninstall" ? "uninstalled" : "updated";
34055
+ }
34056
+ function resolvePackage(name, op) {
34057
+ const entry = WORKER_TOOL_CATALOG.find((t) => t.id === name);
34058
+ if (entry) {
34059
+ const allowed = op === "update" ? entry.updatable : entry.installable;
34060
+ if (!allowed) {
34061
+ return { error: `"${name}" is a prerequisite and cannot be ${opPast(op)} from here.` };
34062
+ }
34063
+ return { pkg: entry.installPackage ?? name };
34064
+ }
34065
+ if (WORKER_TOOL_PREREQUISITE_IDS.includes(name)) {
34066
+ return { error: `"${name}" is a prerequisite and cannot be ${opPast(op)} from here.` };
34067
+ }
34068
+ if (WORKER_TOOL_BUNDLED_GLOBALS.includes(name)) {
34069
+ return { error: `"${name}" ships with Node and cannot be ${opPast(op)} from here.` };
34070
+ }
34071
+ if (!NPM_NAME.test(name)) return { error: `Invalid package name: "${name}".` };
34072
+ return { pkg: name };
34073
+ }
34074
+ function opArgs(op, manager, pkg) {
34075
+ if (op === "uninstall") return manager === "pnpm" ? ["remove", "-g", pkg] : ["uninstall", "-g", pkg];
34076
+ const target = op === "update" ? `${pkg}@latest` : pkg;
34077
+ return manager === "pnpm" ? ["add", "-g", target] : ["install", "-g", target];
34078
+ }
34079
+ async function runWorkerToolOp(op, name, manager, onLine) {
34080
+ const resolved = resolvePackage(name, op);
34081
+ if (resolved.error || !resolved.pkg) {
34082
+ return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
34083
+ }
34084
+ onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
34085
+ const { code } = await run(manager, opArgs(op, manager, resolved.pkg), onLine);
34086
+ return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
34087
+ }
34088
+ function setToolAutoUpdate(profileDir2, name, enabled) {
34089
+ const resolved = resolvePackage(name, "update");
34090
+ if (resolved.error) return { ok: false, error: resolved.error };
34091
+ const current = readProfileConfig(profileDir2).autoUpdateTools ?? [];
34092
+ const set = new Set(current);
34093
+ if (enabled) set.add(name);
34094
+ else set.delete(name);
34095
+ writeProfileConfig(profileDir2, { autoUpdateTools: [...set] });
34096
+ return { ok: true };
34097
+ }
34098
+ async function autoUpdateFlaggedTools(tools, onLine) {
34099
+ for (const name of tools) {
34100
+ const res = await runWorkerToolOp("update", name, "npm", onLine).catch(
34101
+ (err) => ({ ok: false, exitCode: 1, error: String(err) })
34102
+ );
34103
+ onLine(res.ok ? `\u2713 ${name} updated` : `\u2717 ${name}: ${res.error ?? "failed"}`);
34104
+ }
34105
+ }
34106
+
33941
34107
  // src/core/update-scheduler.ts
33942
34108
  var TICK_MS = 6e4;
33943
34109
  var UpdateScheduler = class {
@@ -34020,6 +34186,7 @@ var UpdateScheduler = class {
34020
34186
  async runUpdate() {
34021
34187
  this.updating = true;
34022
34188
  try {
34189
+ await this.updateFlaggedTools();
34023
34190
  this.bus.log("Scheduled auto-update: checking for a new version\u2026");
34024
34191
  const result = await updateToLatest(this.serverUrl);
34025
34192
  if (result.action === "already-latest") {
@@ -34033,7 +34200,7 @@ var UpdateScheduler = class {
34033
34200
  }
34034
34201
  logger.info("update.scheduled.updated", { version: result.version });
34035
34202
  this.bus.log(`Updated to ${result.version} \u2014 restarting to apply\u2026`);
34036
- const child = spawn(process.execPath, process.argv.slice(1), {
34203
+ const child = spawn2(process.execPath, process.argv.slice(1), {
34037
34204
  stdio: "inherit",
34038
34205
  env: { ...process.env, FOURPM_NO_UPDATE: "1" }
34039
34206
  });
@@ -34044,18 +34211,45 @@ var UpdateScheduler = class {
34044
34211
  this.updating = false;
34045
34212
  }
34046
34213
  }
34214
+ /** Update each tool flagged for auto-update in config.json to @latest (ADR-0253) — best-effort. */
34215
+ async updateFlaggedTools() {
34216
+ const tools = readProfileConfig(this.profileDir).autoUpdateTools ?? [];
34217
+ if (tools.length === 0) return;
34218
+ this.bus.log(`Scheduled tool auto-update: ${tools.join(", ")}\u2026`);
34219
+ await autoUpdateFlaggedTools(tools, (line) => logger.info("update.tool.line", { line })).catch(
34220
+ (err) => logger.warn("update.tool.error", { error: String(err) })
34221
+ );
34222
+ }
34047
34223
  };
34048
34224
 
34049
34225
  // src/core/executor.ts
34050
- import { spawn as spawn2 } from "child_process";
34226
+ import { spawn as spawn3 } from "child_process";
34051
34227
  var KILL_GRACE_MS = 5e3;
34052
34228
  async function runCommand(dispatch, emit, opts) {
34053
34229
  let seq = 0;
34054
- const child = spawn2(dispatch.cmd, dispatch.args ?? [], {
34055
- cwd: dispatch.path,
34056
- env: { ...process.env, ...dispatch.env },
34057
- shell: false
34230
+ let child;
34231
+ try {
34232
+ child = spawn3(dispatch.cmd, dispatch.args ?? [], {
34233
+ cwd: dispatch.path,
34234
+ env: { ...process.env, ...dispatch.env },
34235
+ shell: false
34236
+ });
34237
+ } catch (err) {
34238
+ emit({
34239
+ commandId: dispatch.commandId,
34240
+ seq: seq++,
34241
+ chunk: `spawn error: ${err.message}
34242
+ `,
34243
+ done: true,
34244
+ exitCode: -1
34245
+ });
34246
+ return;
34247
+ }
34248
+ child.stdin?.on("error", () => {
34058
34249
  });
34250
+ if (opts?.stdin != null) {
34251
+ child.stdin?.write(opts.stdin);
34252
+ }
34059
34253
  child.stdin?.end();
34060
34254
  const timeoutMs = opts?.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 0;
34061
34255
  let timedOut = false;
@@ -34255,12 +34449,16 @@ function isOutOfCredits(text) {
34255
34449
  text
34256
34450
  );
34257
34451
  }
34258
- function runAttempt(commandId, cmd, args, cwd2, env3, onChunk, timeoutMs) {
34452
+ function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs) {
34259
34453
  return new Promise((resolve8) => {
34260
- void runCommand({ commandId, cmd, args, path: cwd2, env: env3 }, (out) => {
34261
- if (out.chunk) onChunk(out.chunk);
34262
- if (out.done) resolve8(out.exitCode ?? -1);
34263
- }, timeoutMs > 0 ? { timeoutMs } : void 0);
34454
+ runCommand(
34455
+ { commandId, cmd, args, path: cwd2, env: env3 },
34456
+ (out) => {
34457
+ if (out.chunk) onChunk(out.chunk);
34458
+ if (out.done) resolve8(out.exitCode ?? -1);
34459
+ },
34460
+ { stdin, ...timeoutMs > 0 ? { timeoutMs } : {} }
34461
+ ).catch(() => resolve8(-1));
34264
34462
  });
34265
34463
  }
34266
34464
  async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
@@ -34281,7 +34479,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
34281
34479
  handlers.onChunk(text);
34282
34480
  }
34283
34481
  };
34284
- finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, cwd2, attempt.env, emit, timeoutMs);
34482
+ finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, attempt.stdin, cwd2, attempt.env, emit, timeoutMs);
34285
34483
  const tail = parser.flush();
34286
34484
  if (tail) {
34287
34485
  captured += tail;
@@ -34358,6 +34556,52 @@ function setWorkingCredential(profileDir2, key) {
34358
34556
  } catch {
34359
34557
  }
34360
34558
  }
34559
+ var PINNED_CREDENTIAL_KEY = "__pinned__";
34560
+ function getPinnedCredential(profileDir2) {
34561
+ return readState(profileDir2)[PINNED_CREDENTIAL_KEY] ?? null;
34562
+ }
34563
+ function setPinnedCredential(profileDir2, key) {
34564
+ const state = readState(profileDir2);
34565
+ if (state[PINNED_CREDENTIAL_KEY] === key) return;
34566
+ state[PINNED_CREDENTIAL_KEY] = key;
34567
+ try {
34568
+ writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
34569
+ } catch {
34570
+ }
34571
+ }
34572
+ function clearPinnedCredential(profileDir2) {
34573
+ const state = readState(profileDir2);
34574
+ if (!(PINNED_CREDENTIAL_KEY in state)) return;
34575
+ delete state[PINNED_CREDENTIAL_KEY];
34576
+ try {
34577
+ writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
34578
+ } catch {
34579
+ }
34580
+ }
34581
+
34582
+ // src/utils/time.ts
34583
+ function formatTimestampInZone(date, timeZone) {
34584
+ const build = (tz) => {
34585
+ const parts = new Intl.DateTimeFormat("en-CA", {
34586
+ ...tz ? { timeZone: tz } : {},
34587
+ hour12: false,
34588
+ year: "numeric",
34589
+ month: "2-digit",
34590
+ day: "2-digit",
34591
+ hour: "2-digit",
34592
+ minute: "2-digit",
34593
+ second: "2-digit"
34594
+ }).formatToParts(date);
34595
+ const get2 = (type) => parts.find((p) => p.type === type)?.value ?? "";
34596
+ const hour = get2("hour") === "24" ? "00" : get2("hour");
34597
+ return `${get2("year")}/${get2("month")}/${get2("day")} ${hour}:${get2("minute")}:${get2("second")}`;
34598
+ };
34599
+ try {
34600
+ return build(timeZone);
34601
+ } catch {
34602
+ return build();
34603
+ }
34604
+ }
34361
34605
 
34362
34606
  // src/utils/ai-cli.ts
34363
34607
  import { readFileSync as readFileSync8 } from "fs";
@@ -34463,7 +34707,7 @@ function folderScopeGuard(folder, prompt) {
34463
34707
  prompt
34464
34708
  ].join("\n");
34465
34709
  }
34466
- function buildRunArgs(profile, cmd, prompt, resumeId, oneShot = false) {
34710
+ function buildRunArgs(profile, cmd, resumeId, oneShot = false) {
34467
34711
  const extras = profile.args ?? [];
34468
34712
  const model = profile.model?.trim();
34469
34713
  const modelArgs = model ? ["--model", model] : [];
@@ -34471,7 +34715,7 @@ function buildRunArgs(profile, cmd, prompt, resumeId, oneShot = false) {
34471
34715
  const turns = profile.maxTurns;
34472
34716
  const perProfileTurns = !oneShot && cmd.includes("claude") && typeof turns === "number" && turns > 0 ? [`--max-turns=${Math.floor(turns)}`] : [];
34473
34717
  const oneShotExtra = oneShot ? oneShotArgs(cmd) : perProfileTurns;
34474
- return [...requiredArgs(cmd), ...resumeArgs, ...extras, ...modelArgs, ...oneShotExtra, prompt];
34718
+ return [...requiredArgs(cmd), ...resumeArgs, ...extras, ...modelArgs, ...oneShotExtra];
34475
34719
  }
34476
34720
  function claudeHomeDirs(config) {
34477
34721
  const dirs = profileDirs(claudeProfilesOf(config));
@@ -34522,7 +34766,7 @@ function planUnifiedRun(prompt, config, workingCredential, resume, oneShot) {
34522
34766
  return {
34523
34767
  cmd: active,
34524
34768
  attempts: [
34525
- { cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, prompt, void 0, oneShot), env: { ...baseEnv } }
34769
+ { cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, void 0, oneShot), stdin: prompt, env: { ...baseEnv } }
34526
34770
  ]
34527
34771
  };
34528
34772
  }
@@ -34540,7 +34784,8 @@ function planUnifiedRun(prompt, config, workingCredential, resume, oneShot) {
34540
34784
  label: cred.label?.trim() || profileDisplayLabel(dir2),
34541
34785
  dir: dir2,
34542
34786
  key,
34543
- args: buildRunArgs(cred, cmd, prompt, resume.get(key), oneShot),
34787
+ args: buildRunArgs(cred, cmd, resume.get(key), oneShot),
34788
+ stdin: prompt,
34544
34789
  env: envVar ? { ...baseEnv, [envVar]: dir2 } : { ...baseEnv }
34545
34790
  }));
34546
34791
  return { cmd: attempts[0]?.cmd ?? DEFAULT_AI_CLI, attempts };
@@ -34552,7 +34797,7 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot) {
34552
34797
  const defaultPlan = {
34553
34798
  cmd,
34554
34799
  attempts: [
34555
- { cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, prompt, void 0, oneShot), env: { ...baseEnv } }
34800
+ { cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, void 0, oneShot), stdin: prompt, env: { ...baseEnv } }
34556
34801
  ]
34557
34802
  };
34558
34803
  if (!envVar) return defaultPlan;
@@ -34569,7 +34814,8 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot) {
34569
34814
  label: profileDisplayLabel(dir2),
34570
34815
  dir: dir2,
34571
34816
  key,
34572
- args: buildRunArgs(profile, cmd, prompt, resume.get(key), oneShot),
34817
+ args: buildRunArgs(profile, cmd, resume.get(key), oneShot),
34818
+ stdin: prompt,
34573
34819
  env: { ...baseEnv, [envVar]: dir2 }
34574
34820
  };
34575
34821
  });
@@ -34577,7 +34823,7 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot) {
34577
34823
  }
34578
34824
 
34579
34825
  // src/core/claude-usage.ts
34580
- import { spawn as spawn3 } from "child_process";
34826
+ import { spawn as spawn4 } from "child_process";
34581
34827
  import { readFileSync as readFileSync9 } from "fs";
34582
34828
  import { join as join9 } from "path";
34583
34829
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
@@ -34624,7 +34870,7 @@ function runUsageCli(dir2, aiCli2) {
34624
34870
  }, 3e4);
34625
34871
  timer.unref?.();
34626
34872
  try {
34627
- child = spawn3(aiCli2, ["/usage"], {
34873
+ child = spawn4(aiCli2, ["/usage"], {
34628
34874
  env: { ...process.env, CLAUDE_CONFIG_DIR: dir2 },
34629
34875
  stdio: ["ignore", "pipe", "ignore"]
34630
34876
  });
@@ -34694,7 +34940,7 @@ async function checkClaudeUsage(dirs, aiCli2 = "claude") {
34694
34940
  // src/core/ai-assist.ts
34695
34941
  import { execFile } from "child_process";
34696
34942
  import { promisify } from "util";
34697
- var run = promisify(execFile);
34943
+ var run2 = promisify(execFile);
34698
34944
  function aiCli() {
34699
34945
  return process.env.AI_CLI || "claude";
34700
34946
  }
@@ -34737,7 +34983,7 @@ function parseAi(cli, stdout, prompt) {
34737
34983
  }
34738
34984
  async function runAi(prompt) {
34739
34985
  const cli = aiCli();
34740
- const { stdout } = await run(cli, aiArgs(cli, prompt), {
34986
+ const { stdout } = await run2(cli, aiArgs(cli, prompt), {
34741
34987
  timeout: 12e4,
34742
34988
  maxBuffer: 8 * 1024 * 1024
34743
34989
  });
@@ -35080,7 +35326,7 @@ async function writeWorkerFile(root, relPath, content) {
35080
35326
  }
35081
35327
 
35082
35328
  // src/core/support-answer.ts
35083
- import { spawn as spawn4 } from "child_process";
35329
+ import { spawn as spawn5 } from "child_process";
35084
35330
  import { execFile as execFile2 } from "child_process";
35085
35331
  import { createHash as createHash3 } from "crypto";
35086
35332
  import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
@@ -35215,7 +35461,7 @@ function runClaudeOnce(cmd, profile, prompt, extraEnv) {
35215
35461
  ...extraEnv,
35216
35462
  ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
35217
35463
  };
35218
- const child = spawn4(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
35464
+ const child = spawn5(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
35219
35465
  const parser = createAiStreamParser(cmd);
35220
35466
  let out = "";
35221
35467
  let err = "";
@@ -35285,7 +35531,7 @@ async function runSupportAnswer(req, ai, profileDir2) {
35285
35531
  }
35286
35532
 
35287
35533
  // src/core/knowledge-compose.ts
35288
- import { spawn as spawn5 } from "child_process";
35534
+ import { spawn as spawn6 } from "child_process";
35289
35535
  var COMPOSE_TIMEOUT_MS = 18e4;
35290
35536
  function buildPrompt2(req) {
35291
35537
  const focus = req.prompt ? `
@@ -35311,7 +35557,7 @@ function runOnce(cmd, profile, prompt, cwd2, extraEnv) {
35311
35557
  ...extraEnv,
35312
35558
  ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
35313
35559
  };
35314
- const child = spawn5(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
35560
+ const child = spawn6(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
35315
35561
  let out = "";
35316
35562
  let err = "";
35317
35563
  const timer = setTimeout(() => {
@@ -35359,10 +35605,10 @@ async function runKnowledgeCompose(req, ai, physicRoot) {
35359
35605
  // src/core/autonomous.ts
35360
35606
  import { readFile as readFile3, writeFile as writeFile2, chmod } from "fs/promises";
35361
35607
  import { readFileSync as readFileSync13 } from "fs";
35362
- import { execFile as execFile3, spawn as spawn6 } from "child_process";
35608
+ import { execFile as execFile3, spawn as spawn7 } from "child_process";
35363
35609
  import { join as join13 } from "path";
35364
35610
  import { promisify as promisify3 } from "util";
35365
- var run2 = promisify3(execFile3);
35611
+ var run3 = promisify3(execFile3);
35366
35612
  var SETTINGS_REL = ".claude/.autonomous.settings.json";
35367
35613
  var HISTORIES_REL = ".claude/.autonomous.histories.json";
35368
35614
  var APPROVALS_REL = ".claude/.autonomous.approvals.json";
@@ -35396,7 +35642,7 @@ function tickScript(root) {
35396
35642
  }
35397
35643
  async function crontabList() {
35398
35644
  try {
35399
- const { stdout } = await run2("crontab", ["-l"], { timeout: 1e4 });
35645
+ const { stdout } = await run3("crontab", ["-l"], { timeout: 1e4 });
35400
35646
  return stdout;
35401
35647
  } catch {
35402
35648
  return "";
@@ -35404,7 +35650,7 @@ async function crontabList() {
35404
35650
  }
35405
35651
  async function crontabSet(content) {
35406
35652
  return new Promise((resolve8) => {
35407
- const child = spawn6("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
35653
+ const child = spawn7("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
35408
35654
  child.on("error", () => resolve8(false));
35409
35655
  child.on("close", (code) => resolve8(code === 0));
35410
35656
  child.stdin.end(content.endsWith("\n") ? content : content + "\n");
@@ -36135,15 +36381,15 @@ async function buildGraph(root, mode) {
36135
36381
  // src/core/rag.ts
36136
36382
  import { readFile as readFile9, writeFile as writeFile7, statfs as statfs2, mkdir as mkdir6 } from "fs/promises";
36137
36383
  import { existsSync as existsSync10 } from "fs";
36138
- import { execFile as execFile4, spawn as spawn7 } from "child_process";
36384
+ import { execFile as execFile4, spawn as spawn8 } from "child_process";
36139
36385
  import { totalmem as totalmem2, cpus as cpus2 } from "os";
36140
36386
  import { join as join20 } from "path";
36141
36387
  import { promisify as promisify4 } from "util";
36142
- var run3 = promisify4(execFile4);
36388
+ var run4 = promisify4(execFile4);
36143
36389
  var RAG_REL = ".claude/rag";
36144
36390
  async function tryRun(cmd, args) {
36145
36391
  try {
36146
- const { stdout } = await run3(cmd, args, { timeout: 8e3 });
36392
+ const { stdout } = await run4(cmd, args, { timeout: 8e3 });
36147
36393
  return { ok: true, out: stdout.trim() };
36148
36394
  } catch {
36149
36395
  return { ok: false, out: "" };
@@ -36322,7 +36568,7 @@ touch "${RAG_REL}/.indexing"
36322
36568
  { echo "[reindex] $(date)"; "$RAG_PY" "${RAG_REL}/rag_engine.py" reindex 2>&1; } >> "${RAG_REL}/index.log" 2>&1
36323
36569
  rm -f "${RAG_REL}/.indexing"
36324
36570
  `;
36325
- const child = spawn7("bash", ["-c", script], {
36571
+ const child = spawn8("bash", ["-c", script], {
36326
36572
  cwd: root,
36327
36573
  env: { ...process.env, RAG_PY: py.bin },
36328
36574
  detached: true,
@@ -36337,7 +36583,7 @@ async function ragQuery(root, queryText, k = 8) {
36337
36583
  if (!existsSync10(join20(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
36338
36584
  await ensureEngine(root);
36339
36585
  try {
36340
- const { stdout } = await run3(
36586
+ const { stdout } = await run4(
36341
36587
  py.bin,
36342
36588
  [join20(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k, 1), 20))],
36343
36589
  { cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
@@ -36372,7 +36618,7 @@ rm -f "${RAG_REL}/.installed"
36372
36618
  } >> "${RAG_REL}/install.log" 2>&1
36373
36619
  rm -f "${RAG_REL}/.installing"
36374
36620
  `;
36375
- const child = spawn7("bash", ["-c", script], {
36621
+ const child = spawn8("bash", ["-c", script], {
36376
36622
  cwd: root,
36377
36623
  env: { ...process.env, RAG_PY: py.bin, RAG_MODEL: model },
36378
36624
  detached: true,
@@ -36387,7 +36633,7 @@ import { readFile as readFile10 } from "fs/promises";
36387
36633
  import { execFile as execFile5 } from "child_process";
36388
36634
  import { basename as basename3, dirname as dirname7, resolve as resolve5 } from "path";
36389
36635
  import { promisify as promisify5 } from "util";
36390
- var run4 = promisify5(execFile5);
36636
+ var run5 = promisify5(execFile5);
36391
36637
  async function gitDiff(path) {
36392
36638
  const target = resolve5(path);
36393
36639
  const dir2 = dirname7(target);
@@ -36399,7 +36645,7 @@ async function gitDiff(path) {
36399
36645
  }
36400
36646
  let oldContent = "";
36401
36647
  try {
36402
- const { stdout } = await run4("git", ["show", `HEAD:./${base}`], {
36648
+ const { stdout } = await run5("git", ["show", `HEAD:./${base}`], {
36403
36649
  cwd: dir2,
36404
36650
  timeout: 15e3,
36405
36651
  maxBuffer: 4 * 1024 * 1024
@@ -36413,10 +36659,10 @@ async function gitDiff(path) {
36413
36659
  // src/core/git-env.ts
36414
36660
  import { execFile as execFile6 } from "child_process";
36415
36661
  import { promisify as promisify6 } from "util";
36416
- var run5 = promisify6(execFile6);
36662
+ var run6 = promisify6(execFile6);
36417
36663
  async function hasBinary(bin, args = ["--version"]) {
36418
36664
  try {
36419
- await run5(bin, args, { timeout: 5e3 });
36665
+ await run6(bin, args, { timeout: 5e3 });
36420
36666
  return true;
36421
36667
  } catch {
36422
36668
  return false;
@@ -36429,7 +36675,7 @@ async function checkGitEnv(provider) {
36429
36675
  let account = null;
36430
36676
  if (installed) {
36431
36677
  try {
36432
- const { stdout, stderr } = await run5(bin, ["auth", "status"], { timeout: 8e3 });
36678
+ const { stdout, stderr } = await run6(bin, ["auth", "status"], { timeout: 8e3 });
36433
36679
  const out = `${stdout}
36434
36680
  ${stderr}`;
36435
36681
  authenticated = /logged in|logged into|✓/i.test(out);
@@ -36446,7 +36692,7 @@ import { execFile as execFile7 } from "child_process";
36446
36692
  import { readFile as readFile11, rm as rm3, mkdir as mkdir7 } from "fs/promises";
36447
36693
  import { join as join21 } from "path";
36448
36694
  import { promisify as promisify7 } from "util";
36449
- var run6 = promisify7(execFile7);
36695
+ var run7 = promisify7(execFile7);
36450
36696
  function keyPaths(profileDir2) {
36451
36697
  const dir2 = join21(profileDir2, ".ssh");
36452
36698
  const key = join21(dir2, "id_4pm");
@@ -36461,14 +36707,14 @@ async function readPublicKey(pub) {
36461
36707
  }
36462
36708
  async function fingerprintOf(pub) {
36463
36709
  try {
36464
- const { stdout } = await run6("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
36710
+ const { stdout } = await run7("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
36465
36711
  return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
36466
36712
  } catch {
36467
36713
  return null;
36468
36714
  }
36469
36715
  }
36470
36716
  async function configureGit(key) {
36471
- await run6(
36717
+ await run7(
36472
36718
  "git",
36473
36719
  ["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
36474
36720
  { timeout: 5e3 }
@@ -36479,7 +36725,7 @@ async function generate(profileDir2) {
36479
36725
  await mkdir7(dir2, { recursive: true });
36480
36726
  await rm3(key, { force: true });
36481
36727
  await rm3(pub, { force: true });
36482
- await run6("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
36728
+ await run7("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
36483
36729
  timeout: 15e3
36484
36730
  });
36485
36731
  await configureGit(key);
@@ -36489,7 +36735,7 @@ async function remove(profileDir2) {
36489
36735
  const { key, pub } = keyPaths(profileDir2);
36490
36736
  await rm3(key, { force: true });
36491
36737
  await rm3(pub, { force: true });
36492
- await run6("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
36738
+ await run7("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
36493
36739
  () => void 0
36494
36740
  );
36495
36741
  return { publicKey: null, fingerprint: null };
@@ -36514,7 +36760,7 @@ import { readdir as readdir6 } from "fs/promises";
36514
36760
  import { execFile as execFile8 } from "child_process";
36515
36761
  import { resolve as resolve6, sep as sep4 } from "path";
36516
36762
  import { promisify as promisify8 } from "util";
36517
- var run7 = promisify8(execFile8);
36763
+ var run8 = promisify8(execFile8);
36518
36764
  var FS = "";
36519
36765
  var RS = "";
36520
36766
  function resolveRepo(root, repo) {
@@ -36525,7 +36771,7 @@ function resolveRepo(root, repo) {
36525
36771
  }
36526
36772
  async function git(cwd2, args) {
36527
36773
  try {
36528
- const { stdout } = await run7("git", args, {
36774
+ const { stdout } = await run8("git", args, {
36529
36775
  cwd: cwd2,
36530
36776
  timeout: 15e3,
36531
36777
  maxBuffer: 8 * 1024 * 1024
@@ -36629,11 +36875,11 @@ async function gitCommitDiff(root, repo, hash, path) {
36629
36875
  // src/core/git-commit-identity.ts
36630
36876
  import { execFile as execFile9 } from "child_process";
36631
36877
  import { promisify as promisify9 } from "util";
36632
- var run8 = promisify9(execFile9);
36878
+ var run9 = promisify9(execFile9);
36633
36879
  async function setCommitAuthor(cwd2, machineUsername) {
36634
36880
  if (!machineUsername) return;
36635
36881
  try {
36636
- await run8("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
36882
+ await run9("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
36637
36883
  } catch {
36638
36884
  }
36639
36885
  }
@@ -36644,7 +36890,7 @@ import { execFile as execFile10 } from "child_process";
36644
36890
  import { dirname as dirname8, join as join22, resolve as resolve7 } from "path";
36645
36891
  import { fileURLToPath as fileURLToPath3 } from "url";
36646
36892
  import { promisify as promisify10 } from "util";
36647
- var run9 = promisify10(execFile10);
36893
+ var run10 = promisify10(execFile10);
36648
36894
  function reposOf(spec) {
36649
36895
  const repos = spec?.repos;
36650
36896
  return Array.isArray(repos) ? repos : [];
@@ -36658,13 +36904,13 @@ async function provisionRepos(target, repos, emit, opts = {}) {
36658
36904
  if (repo.primary) {
36659
36905
  if (opts.clonePrimary && repo.url) {
36660
36906
  emit("git", `Cloning ${repo.url}\u2026`);
36661
- await run9("git", ["clone", repo.url, target], { timeout: 12e4 });
36907
+ await run10("git", ["clone", repo.url, target], { timeout: 12e4 });
36662
36908
  continue;
36663
36909
  }
36664
36910
  emit("git", "Initializing the primary repo\u2026");
36665
- await run9("git", ["init"], { cwd: target, timeout: 2e4 });
36911
+ await run10("git", ["init"], { cwd: target, timeout: 2e4 });
36666
36912
  if (repo.url) {
36667
- await run9("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
36913
+ await run10("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
36668
36914
  () => void 0
36669
36915
  );
36670
36916
  }
@@ -36673,11 +36919,11 @@ async function provisionRepos(target, repos, emit, opts = {}) {
36673
36919
  const dir2 = join22(target, subDirName(repo));
36674
36920
  if (repo.url) {
36675
36921
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
36676
- await run9("git", ["clone", repo.url, dir2], { timeout: 12e4 });
36922
+ await run10("git", ["clone", repo.url, dir2], { timeout: 12e4 });
36677
36923
  } else {
36678
36924
  emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
36679
36925
  await mkdir8(dir2, { recursive: true });
36680
- await run9("git", ["init"], { cwd: dir2, timeout: 2e4 });
36926
+ await run10("git", ["init"], { cwd: dir2, timeout: 2e4 });
36681
36927
  }
36682
36928
  }
36683
36929
  }
@@ -36948,6 +37194,70 @@ function runWhoami(ctx) {
36948
37194
  ctx.print(`projects: ${w.projects.length ? w.projects.map((p) => p.name).join(", ") : "(none)"}`);
36949
37195
  });
36950
37196
  }
37197
+ function aiProfileLabel(c) {
37198
+ return c.label?.trim() || profileDisplayLabel(resolveHomePath(c.profile));
37199
+ }
37200
+ function runAiProfile(ctx) {
37201
+ const dir2 = ctx.info.profileDir;
37202
+ const config = readProfileConfig(dir2);
37203
+ const list = Array.isArray(config.aiProfiles) ? config.aiProfiles : [];
37204
+ const [sub, ...rest] = ctx.args;
37205
+ const action = (sub ?? "list").toLowerCase();
37206
+ if (action === "list") {
37207
+ if (list.length === 0) {
37208
+ ctx.print("No AI profiles configured (config.aiProfiles is empty).", "warn");
37209
+ ctx.print("This worker may use a legacy per-provider config \u2014 /config show to inspect.");
37210
+ return;
37211
+ }
37212
+ const pinned = getPinnedCredential(dir2);
37213
+ const working = getWorkingCredential(dir2);
37214
+ ctx.print("AI profiles (config order = failover priority) \xB7 /ai-profile use <n> to switch:");
37215
+ list.forEach((c, i) => {
37216
+ const usable = isUsableCredential(c);
37217
+ const key = usable ? credentialKey(c.provider, resolveHomePath(c.profile)) : null;
37218
+ const flags = [];
37219
+ if (key && key === pinned) flags.push("pinned");
37220
+ else if (key && !pinned && key === working) flags.push("active");
37221
+ if (c.enabled === false) flags.push("disabled");
37222
+ const model = c.model?.trim() ? ` \xB7 model=${c.model.trim()}` : "";
37223
+ const flagStr = flags.length ? ` (${flags.join(", ")})` : "";
37224
+ const marker = key && key === pinned ? "*" : " ";
37225
+ ctx.print(` ${marker}[${i + 1}] ${c.provider.padEnd(11)} ${aiProfileLabel(c)}${model}${flagStr}`);
37226
+ });
37227
+ if (pinned) {
37228
+ ctx.print("A profile is pinned \u2014 tried first every prompt; /ai-profile reset returns to auto.");
37229
+ }
37230
+ return;
37231
+ }
37232
+ if (action === "reset" || action === "auto" || action === "clear") {
37233
+ clearPinnedCredential(dir2);
37234
+ ctx.print("\u2714 AI profile pin cleared \u2014 failover is back to automatic (config order / remembered).");
37235
+ const working = getWorkingCredential(dir2);
37236
+ ctx.setActiveProfile(working ? labelFromCredentialKey(working) : null);
37237
+ return;
37238
+ }
37239
+ if (action === "use" || action === "switch") {
37240
+ const n = Number(rest[0]);
37241
+ if (!Number.isInteger(n) || n < 1 || n > list.length) {
37242
+ ctx.print(`usage: /ai-profile use <n> (n = 1-${list.length || "?"} from /ai-profile list)`, "error");
37243
+ return;
37244
+ }
37245
+ const cred = list[n - 1];
37246
+ if (!isUsableCredential(cred)) {
37247
+ const why = cred.enabled === false ? "it is disabled" : "it has no usable profile dir / provider";
37248
+ ctx.print(`Profile [${n}] "${aiProfileLabel(cred)}" can't be used \u2014 ${why}.`, "error");
37249
+ return;
37250
+ }
37251
+ const credDir = resolveHomePath(cred.profile);
37252
+ setPinnedCredential(dir2, credentialKey(cred.provider, credDir));
37253
+ ctx.setActiveProfile(profileDisplayLabel(credDir));
37254
+ ctx.print(
37255
+ `\u2714 switched to ${cred.provider} profile "${aiProfileLabel(cred)}" \u2014 tried first from the next prompt (failover keeps the rest as backup).`
37256
+ );
37257
+ return;
37258
+ }
37259
+ ctx.print(`Unknown /ai-profile subcommand "${sub}". Use: list | use <n> | reset.`, "error");
37260
+ }
36951
37261
  function runFold(ctx, action) {
36952
37262
  if (ctx.maxBlock === 0) {
36953
37263
  ctx.print("No collapsible blocks on screen.", "warn");
@@ -37037,6 +37347,12 @@ var COMMANDS = [
37037
37347
  description: "Show this machine account + its teams & projects",
37038
37348
  run: runWhoami
37039
37349
  },
37350
+ {
37351
+ name: "ai-profile",
37352
+ usage: "/ai-profile [list|use <n>|reset]",
37353
+ description: "List AI profiles / switch which one runs (ADR-0250)",
37354
+ run: runAiProfile
37355
+ },
37040
37356
  {
37041
37357
  name: "config",
37042
37358
  usage: "/config [show|init|set <k> <v>|delete <k>]",
@@ -37085,7 +37401,7 @@ function runSlashCommand(line, base) {
37085
37401
  }
37086
37402
 
37087
37403
  // src/core/memory-compact.ts
37088
- import { spawn as spawn8 } from "child_process";
37404
+ import { spawn as spawn9 } from "child_process";
37089
37405
  var COMPACT_TIMEOUT_MS = 6e4;
37090
37406
  function buildPrompt3(input) {
37091
37407
  return [
@@ -37113,7 +37429,7 @@ function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
37113
37429
  ...extraEnv,
37114
37430
  ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
37115
37431
  };
37116
- const child = spawn8(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
37432
+ const child = spawn9(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
37117
37433
  let out = "";
37118
37434
  let err = "";
37119
37435
  const timer = setTimeout(() => {
@@ -37201,125 +37517,6 @@ function applyConfigText(profileDir2, text) {
37201
37517
  return { ok: true };
37202
37518
  }
37203
37519
 
37204
- // src/core/worker-tools.ts
37205
- import { spawn as spawn9 } from "child_process";
37206
- var NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
37207
- function run10(cmd, args, onLine, timeoutMs = 18e4) {
37208
- return new Promise((resolve8) => {
37209
- let out = "";
37210
- let buf = "";
37211
- let child = null;
37212
- const done = (code) => {
37213
- clearTimeout(timer);
37214
- if (onLine && buf.trim()) onLine(buf.trim());
37215
- resolve8({ code, out });
37216
- };
37217
- const timer = setTimeout(() => {
37218
- child?.kill();
37219
- if (onLine) onLine(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
37220
- done(124);
37221
- }, timeoutMs);
37222
- timer.unref?.();
37223
- const feed = (d) => {
37224
- const text = d.toString("utf8");
37225
- out += text;
37226
- if (!onLine) return;
37227
- buf += text;
37228
- let nl;
37229
- while ((nl = buf.indexOf("\n")) >= 0) {
37230
- const line = buf.slice(0, nl).replace(/\r$/, "");
37231
- buf = buf.slice(nl + 1);
37232
- onLine(line);
37233
- }
37234
- };
37235
- try {
37236
- child = spawn9(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
37237
- child.stdout?.on("data", feed);
37238
- child.stderr?.on("data", feed);
37239
- child.on("exit", (code) => done(code ?? 0));
37240
- child.on("error", () => done(127));
37241
- } catch {
37242
- done(127);
37243
- }
37244
- });
37245
- }
37246
- async function detectOne(cmd, versionArg) {
37247
- const { code, out } = await run10(cmd, [versionArg], void 0, 15e3);
37248
- if (code !== 0) return null;
37249
- const first = out.split("\n").map((l) => l.trim()).find(Boolean);
37250
- return first ?? "";
37251
- }
37252
- async function listExtras() {
37253
- const { out } = await run10("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
37254
- if (!out.trim()) return [];
37255
- let deps = {};
37256
- try {
37257
- deps = JSON.parse(out).dependencies ?? {};
37258
- } catch {
37259
- return [];
37260
- }
37261
- const catalogPkgs = new Set(WORKER_TOOL_BUNDLED_GLOBALS);
37262
- for (const t of WORKER_TOOL_CATALOG) {
37263
- catalogPkgs.add(t.id);
37264
- if (t.installPackage) catalogPkgs.add(t.installPackage);
37265
- }
37266
- return Object.entries(deps).filter(([name]) => !catalogPkgs.has(name)).map(([name, info]) => ({
37267
- id: name,
37268
- label: name,
37269
- category: "runtime",
37270
- installed: true,
37271
- version: info.version ?? null,
37272
- installable: true
37273
- }));
37274
- }
37275
- async function detectWorkerTools() {
37276
- const catalog = await Promise.all(
37277
- WORKER_TOOL_CATALOG.map(async (t) => {
37278
- const version = await detectOne(t.id, t.versionArg);
37279
- return {
37280
- id: t.id,
37281
- label: t.label,
37282
- category: t.category,
37283
- installed: version !== null,
37284
- version,
37285
- installable: t.installable
37286
- };
37287
- })
37288
- );
37289
- const extras = await listExtras().catch(() => []);
37290
- return { catalog, extras };
37291
- }
37292
- function resolvePackage(name, op) {
37293
- const entry = WORKER_TOOL_CATALOG.find((t) => t.id === name);
37294
- if (entry) {
37295
- if (!entry.installable) {
37296
- return { error: `"${name}" is a prerequisite and cannot be ${op}ed from here.` };
37297
- }
37298
- return { pkg: entry.installPackage ?? name };
37299
- }
37300
- if (WORKER_TOOL_PREREQUISITE_IDS.includes(name)) {
37301
- return { error: `"${name}" is a prerequisite and cannot be ${op}ed from here.` };
37302
- }
37303
- if (WORKER_TOOL_BUNDLED_GLOBALS.includes(name)) {
37304
- return { error: `"${name}" ships with Node and cannot be ${op}ed from here.` };
37305
- }
37306
- if (!NPM_NAME.test(name)) return { error: `Invalid package name: "${name}".` };
37307
- return { pkg: name };
37308
- }
37309
- function opArgs(op, manager, pkg) {
37310
- if (manager === "pnpm") return [op === "install" ? "add" : "remove", "-g", pkg];
37311
- return [op === "install" ? "install" : "uninstall", "-g", pkg];
37312
- }
37313
- async function runWorkerToolOp(op, name, manager, onLine) {
37314
- const resolved = resolvePackage(name, op);
37315
- if (resolved.error || !resolved.pkg) {
37316
- return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
37317
- }
37318
- onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
37319
- const { code } = await run10(manager, opArgs(op, manager, resolved.pkg), onLine);
37320
- return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
37321
- }
37322
-
37323
37520
  // src/core/ws-client.ts
37324
37521
  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:";
37325
37522
  var HEARTBEAT_MS = 3e4;
@@ -37417,6 +37614,9 @@ var WsClient = class {
37417
37614
  reconnectEntryId = null;
37418
37615
  /** Org-configured reconnect cap (seconds) from the last ws_token — overrides env. */
37419
37616
  orgReconnectMaxSec = null;
37617
+ /** Org timezone (IANA) from the last ws_token (ADR-0132) — a rented worker follows its renter's
37618
+ * org; used to stamp the AI-run start time on the `aireq` transcript marker (ADR-0249). */
37619
+ orgTimezone = "UTC";
37420
37620
  /** Env override for the reconnect cap (seconds); resolved once at construction. */
37421
37621
  envReconnectMaxSec = envReconnectMaxSec();
37422
37622
  /** Process start (ms) — uptime for a web-dispatched `/status` (ADR-0249). */
@@ -37601,6 +37801,9 @@ var WsClient = class {
37601
37801
  if (typeof token.reconnectMaxBackoffSec === "number") {
37602
37802
  this.orgReconnectMaxSec = token.reconnectMaxBackoffSec;
37603
37803
  }
37804
+ if (typeof token.timezone === "string" && token.timezone) {
37805
+ this.orgTimezone = token.timezone;
37806
+ }
37604
37807
  if (typeof token.autoUpdateDaily === "boolean") {
37605
37808
  this.updateScheduler.configure({
37606
37809
  autoUpdateDaily: token.autoUpdateDaily,
@@ -38100,14 +38303,21 @@ var WsClient = class {
38100
38303
  break;
38101
38304
  }
38102
38305
  case WsChannels.TOOLS_LIST:
38103
- void detectWorkerTools().then(
38306
+ void detectWorkerTools(readProfileConfig(this.context.profileDir).autoUpdateTools ?? []).then(
38104
38307
  (reply) => this.send(WsChannels.TOOLS_LIST, reply, message.id)
38105
38308
  );
38106
38309
  break;
38310
+ case WsChannels.TOOLS_AUTOUPDATE: {
38311
+ const req = payload;
38312
+ const res = setToolAutoUpdate(this.context.profileDir, req.name, req.enabled);
38313
+ this.send(WsChannels.TOOLS_AUTOUPDATE, res, message.id);
38314
+ break;
38315
+ }
38107
38316
  case WsChannels.TOOLS_INSTALL:
38108
- case WsChannels.TOOLS_UNINSTALL: {
38317
+ case WsChannels.TOOLS_UNINSTALL:
38318
+ case WsChannels.TOOLS_UPDATE: {
38109
38319
  const req = payload;
38110
- const op = message.channel === WsChannels.TOOLS_INSTALL ? "install" : "uninstall";
38320
+ const op = message.channel === WsChannels.TOOLS_INSTALL ? "install" : message.channel === WsChannels.TOOLS_UPDATE ? "update" : "uninstall";
38111
38321
  this.send(message.channel, { started: true }, message.id);
38112
38322
  void runWorkerToolOp(
38113
38323
  op,
@@ -38288,9 +38498,10 @@ var WsClient = class {
38288
38498
  const unified = isUnifiedConfig(config);
38289
38499
  const mode = config.aiFailoverMode ?? "remember";
38290
38500
  const workingDir = unified ? null : getWorkingProfile(this.context.profileDir, cmd);
38291
- const workingCred = unified && mode === "remember" ? getWorkingCredential(this.context.profileDir) : null;
38501
+ const pinnedCred = unified ? getPinnedCredential(this.context.profileDir) : null;
38502
+ const workingCred = unified ? pinnedCred ?? (mode === "remember" ? getWorkingCredential(this.context.profileDir) : null) : null;
38292
38503
  const startDir = unified ? null : this.profileUnderSessionPressure(config, cmd, workingDir);
38293
- const startCred = unified ? this.credentialUnderSessionPressure(config, workingCred) : null;
38504
+ const startCred = unified && !pinnedCred ? this.credentialUnderSessionPressure(config, workingCred) : null;
38294
38505
  if (startDir && startDir !== workingDir) {
38295
38506
  this.bus.push({
38296
38507
  source: origin,
@@ -38322,7 +38533,8 @@ ${guardedPrompt}`;
38322
38533
  }
38323
38534
  const plan = planAiRun(effectivePrompt, config, hint, resume, oneShot);
38324
38535
  const markerArgs = plan.attempts[0]?.args ?? [];
38325
- this.bus.push({ source: origin, kind: "aireq", text: `${plan.cmd} \u2039 ${prompt}` });
38536
+ const startStamp = formatTimestampInZone(/* @__PURE__ */ new Date(), this.orgTimezone);
38537
+ this.bus.push({ source: origin, kind: "aireq", text: `${startStamp} ${plan.cmd} \u2039 ${prompt}` });
38326
38538
  logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
38327
38539
  if (origin === "local") {
38328
38540
  this.send(WsChannels.COMMAND_ANNOUNCE, {
@@ -38557,6 +38769,8 @@ ${guardedPrompt}`;
38557
38769
  submitAi: () => {
38558
38770
  },
38559
38771
  // only /claude-cmd uses this, and it is special-cased above
38772
+ // /ai-profile use/reset from the web Console updates the shared header too (ADR-0250).
38773
+ setActiveProfile: (label) => this.bus.setActiveProfile(label),
38560
38774
  reconnect: () => this.reconnectNow(),
38561
38775
  expand: () => emit("Use the web console's fold viewer to expand blocks."),
38562
38776
  collapse: () => emit("Use the web console's fold viewer to collapse blocks."),
@@ -45263,12 +45477,17 @@ function textColor(line) {
45263
45477
  if (line.kind === "exit") return "green";
45264
45478
  return void 0;
45265
45479
  }
45480
+ var START_STAMP_RE = /^(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2} )/;
45266
45481
  function AiMarkerLine({ line }) {
45267
45482
  const arrow = line.kind === "aireq" ? "\u2039" : "\u203A";
45268
45483
  const idx = line.text.indexOf(arrow);
45269
45484
  if (idx < 0) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text || " " });
45485
+ const before = line.text.slice(0, idx);
45486
+ const stamp = line.kind === "aireq" ? START_STAMP_RE.exec(before)?.[1] ?? "" : "";
45487
+ const name = stamp ? before.slice(stamp.length) : before;
45270
45488
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Text, { children: [
45271
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "cyan", bold: true, children: line.text.slice(0, idx) }),
45489
+ stamp ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: stamp }) : null,
45490
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "cyan", bold: true, children: name }),
45272
45491
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: arrow }),
45273
45492
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text.slice(idx + 1) })
45274
45493
  ] });
@@ -45685,6 +45904,9 @@ function App2({
45685
45904
  setPendingConfirm({ prompt, onYes });
45686
45905
  },
45687
45906
  submitAi: (aiInput) => bus.submitLocal(aiInput),
45907
+ // /ai-profile use/reset updates the header's active-profile label at once (ADR-0250);
45908
+ // the bus event feeds the React state (see the onActiveProfile subscription above).
45909
+ setActiveProfile: (label) => bus.setActiveProfile(label),
45688
45910
  reconnect: () => bus.requestReconnect(),
45689
45911
  expand: expandFold,
45690
45912
  collapse: collapseFold,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.5.1",
3
+ "version": "1.5.2b",
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",