@4pm/cli 1.5.1 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +215 -39
  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()
@@ -33671,6 +33678,9 @@ var WsChannels = {
33671
33678
  TOOLS_LIST: "tools.list",
33672
33679
  TOOLS_INSTALL: "tools.install",
33673
33680
  TOOLS_UNINSTALL: "tools.uninstall",
33681
+ // update-to-latest of an npm-distributed catalog tool or an extra (ADR-0252): same streamed
33682
+ // shape as install/uninstall — the cli reinstalls the package at `@latest`.
33683
+ TOOLS_UPDATE: "tools.update",
33674
33684
  TOOLS_PROGRESS: "tools.progress",
33675
33685
  TOOLS_DONE: "tools.done",
33676
33686
  // autonomous mode control (ADR-0152): read settings+status+books+approvals, tail logs, and
@@ -34051,11 +34061,29 @@ import { spawn as spawn2 } from "child_process";
34051
34061
  var KILL_GRACE_MS = 5e3;
34052
34062
  async function runCommand(dispatch, emit, opts) {
34053
34063
  let seq = 0;
34054
- const child = spawn2(dispatch.cmd, dispatch.args ?? [], {
34055
- cwd: dispatch.path,
34056
- env: { ...process.env, ...dispatch.env },
34057
- shell: false
34064
+ let child;
34065
+ try {
34066
+ child = spawn2(dispatch.cmd, dispatch.args ?? [], {
34067
+ cwd: dispatch.path,
34068
+ env: { ...process.env, ...dispatch.env },
34069
+ shell: false
34070
+ });
34071
+ } catch (err) {
34072
+ emit({
34073
+ commandId: dispatch.commandId,
34074
+ seq: seq++,
34075
+ chunk: `spawn error: ${err.message}
34076
+ `,
34077
+ done: true,
34078
+ exitCode: -1
34079
+ });
34080
+ return;
34081
+ }
34082
+ child.stdin?.on("error", () => {
34058
34083
  });
34084
+ if (opts?.stdin != null) {
34085
+ child.stdin?.write(opts.stdin);
34086
+ }
34059
34087
  child.stdin?.end();
34060
34088
  const timeoutMs = opts?.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 0;
34061
34089
  let timedOut = false;
@@ -34255,12 +34283,16 @@ function isOutOfCredits(text) {
34255
34283
  text
34256
34284
  );
34257
34285
  }
34258
- function runAttempt(commandId, cmd, args, cwd2, env3, onChunk, timeoutMs) {
34286
+ function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs) {
34259
34287
  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);
34288
+ runCommand(
34289
+ { commandId, cmd, args, path: cwd2, env: env3 },
34290
+ (out) => {
34291
+ if (out.chunk) onChunk(out.chunk);
34292
+ if (out.done) resolve8(out.exitCode ?? -1);
34293
+ },
34294
+ { stdin, ...timeoutMs > 0 ? { timeoutMs } : {} }
34295
+ ).catch(() => resolve8(-1));
34264
34296
  });
34265
34297
  }
34266
34298
  async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
@@ -34281,7 +34313,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
34281
34313
  handlers.onChunk(text);
34282
34314
  }
34283
34315
  };
34284
- finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, cwd2, attempt.env, emit, timeoutMs);
34316
+ finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, attempt.stdin, cwd2, attempt.env, emit, timeoutMs);
34285
34317
  const tail = parser.flush();
34286
34318
  if (tail) {
34287
34319
  captured += tail;
@@ -34358,6 +34390,52 @@ function setWorkingCredential(profileDir2, key) {
34358
34390
  } catch {
34359
34391
  }
34360
34392
  }
34393
+ var PINNED_CREDENTIAL_KEY = "__pinned__";
34394
+ function getPinnedCredential(profileDir2) {
34395
+ return readState(profileDir2)[PINNED_CREDENTIAL_KEY] ?? null;
34396
+ }
34397
+ function setPinnedCredential(profileDir2, key) {
34398
+ const state = readState(profileDir2);
34399
+ if (state[PINNED_CREDENTIAL_KEY] === key) return;
34400
+ state[PINNED_CREDENTIAL_KEY] = key;
34401
+ try {
34402
+ writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
34403
+ } catch {
34404
+ }
34405
+ }
34406
+ function clearPinnedCredential(profileDir2) {
34407
+ const state = readState(profileDir2);
34408
+ if (!(PINNED_CREDENTIAL_KEY in state)) return;
34409
+ delete state[PINNED_CREDENTIAL_KEY];
34410
+ try {
34411
+ writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
34412
+ } catch {
34413
+ }
34414
+ }
34415
+
34416
+ // src/utils/time.ts
34417
+ function formatTimestampInZone(date, timeZone) {
34418
+ const build = (tz) => {
34419
+ const parts = new Intl.DateTimeFormat("en-CA", {
34420
+ ...tz ? { timeZone: tz } : {},
34421
+ hour12: false,
34422
+ year: "numeric",
34423
+ month: "2-digit",
34424
+ day: "2-digit",
34425
+ hour: "2-digit",
34426
+ minute: "2-digit",
34427
+ second: "2-digit"
34428
+ }).formatToParts(date);
34429
+ const get2 = (type) => parts.find((p) => p.type === type)?.value ?? "";
34430
+ const hour = get2("hour") === "24" ? "00" : get2("hour");
34431
+ return `${get2("year")}/${get2("month")}/${get2("day")} ${hour}:${get2("minute")}:${get2("second")}`;
34432
+ };
34433
+ try {
34434
+ return build(timeZone);
34435
+ } catch {
34436
+ return build();
34437
+ }
34438
+ }
34361
34439
 
34362
34440
  // src/utils/ai-cli.ts
34363
34441
  import { readFileSync as readFileSync8 } from "fs";
@@ -34463,7 +34541,7 @@ function folderScopeGuard(folder, prompt) {
34463
34541
  prompt
34464
34542
  ].join("\n");
34465
34543
  }
34466
- function buildRunArgs(profile, cmd, prompt, resumeId, oneShot = false) {
34544
+ function buildRunArgs(profile, cmd, resumeId, oneShot = false) {
34467
34545
  const extras = profile.args ?? [];
34468
34546
  const model = profile.model?.trim();
34469
34547
  const modelArgs = model ? ["--model", model] : [];
@@ -34471,7 +34549,7 @@ function buildRunArgs(profile, cmd, prompt, resumeId, oneShot = false) {
34471
34549
  const turns = profile.maxTurns;
34472
34550
  const perProfileTurns = !oneShot && cmd.includes("claude") && typeof turns === "number" && turns > 0 ? [`--max-turns=${Math.floor(turns)}`] : [];
34473
34551
  const oneShotExtra = oneShot ? oneShotArgs(cmd) : perProfileTurns;
34474
- return [...requiredArgs(cmd), ...resumeArgs, ...extras, ...modelArgs, ...oneShotExtra, prompt];
34552
+ return [...requiredArgs(cmd), ...resumeArgs, ...extras, ...modelArgs, ...oneShotExtra];
34475
34553
  }
34476
34554
  function claudeHomeDirs(config) {
34477
34555
  const dirs = profileDirs(claudeProfilesOf(config));
@@ -34522,7 +34600,7 @@ function planUnifiedRun(prompt, config, workingCredential, resume, oneShot) {
34522
34600
  return {
34523
34601
  cmd: active,
34524
34602
  attempts: [
34525
- { cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, prompt, void 0, oneShot), env: { ...baseEnv } }
34603
+ { cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, void 0, oneShot), stdin: prompt, env: { ...baseEnv } }
34526
34604
  ]
34527
34605
  };
34528
34606
  }
@@ -34540,7 +34618,8 @@ function planUnifiedRun(prompt, config, workingCredential, resume, oneShot) {
34540
34618
  label: cred.label?.trim() || profileDisplayLabel(dir2),
34541
34619
  dir: dir2,
34542
34620
  key,
34543
- args: buildRunArgs(cred, cmd, prompt, resume.get(key), oneShot),
34621
+ args: buildRunArgs(cred, cmd, resume.get(key), oneShot),
34622
+ stdin: prompt,
34544
34623
  env: envVar ? { ...baseEnv, [envVar]: dir2 } : { ...baseEnv }
34545
34624
  }));
34546
34625
  return { cmd: attempts[0]?.cmd ?? DEFAULT_AI_CLI, attempts };
@@ -34552,7 +34631,7 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot) {
34552
34631
  const defaultPlan = {
34553
34632
  cmd,
34554
34633
  attempts: [
34555
- { cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, prompt, void 0, oneShot), env: { ...baseEnv } }
34634
+ { cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, void 0, oneShot), stdin: prompt, env: { ...baseEnv } }
34556
34635
  ]
34557
34636
  };
34558
34637
  if (!envVar) return defaultPlan;
@@ -34569,7 +34648,8 @@ function planLegacyRun(prompt, config, workingDir, resume, oneShot) {
34569
34648
  label: profileDisplayLabel(dir2),
34570
34649
  dir: dir2,
34571
34650
  key,
34572
- args: buildRunArgs(profile, cmd, prompt, resume.get(key), oneShot),
34651
+ args: buildRunArgs(profile, cmd, resume.get(key), oneShot),
34652
+ stdin: prompt,
34573
34653
  env: { ...baseEnv, [envVar]: dir2 }
34574
34654
  };
34575
34655
  });
@@ -36948,6 +37028,70 @@ function runWhoami(ctx) {
36948
37028
  ctx.print(`projects: ${w.projects.length ? w.projects.map((p) => p.name).join(", ") : "(none)"}`);
36949
37029
  });
36950
37030
  }
37031
+ function aiProfileLabel(c) {
37032
+ return c.label?.trim() || profileDisplayLabel(resolveHomePath(c.profile));
37033
+ }
37034
+ function runAiProfile(ctx) {
37035
+ const dir2 = ctx.info.profileDir;
37036
+ const config = readProfileConfig(dir2);
37037
+ const list = Array.isArray(config.aiProfiles) ? config.aiProfiles : [];
37038
+ const [sub, ...rest] = ctx.args;
37039
+ const action = (sub ?? "list").toLowerCase();
37040
+ if (action === "list") {
37041
+ if (list.length === 0) {
37042
+ ctx.print("No AI profiles configured (config.aiProfiles is empty).", "warn");
37043
+ ctx.print("This worker may use a legacy per-provider config \u2014 /config show to inspect.");
37044
+ return;
37045
+ }
37046
+ const pinned = getPinnedCredential(dir2);
37047
+ const working = getWorkingCredential(dir2);
37048
+ ctx.print("AI profiles (config order = failover priority) \xB7 /ai-profile use <n> to switch:");
37049
+ list.forEach((c, i) => {
37050
+ const usable = isUsableCredential(c);
37051
+ const key = usable ? credentialKey(c.provider, resolveHomePath(c.profile)) : null;
37052
+ const flags = [];
37053
+ if (key && key === pinned) flags.push("pinned");
37054
+ else if (key && !pinned && key === working) flags.push("active");
37055
+ if (c.enabled === false) flags.push("disabled");
37056
+ const model = c.model?.trim() ? ` \xB7 model=${c.model.trim()}` : "";
37057
+ const flagStr = flags.length ? ` (${flags.join(", ")})` : "";
37058
+ const marker = key && key === pinned ? "*" : " ";
37059
+ ctx.print(` ${marker}[${i + 1}] ${c.provider.padEnd(11)} ${aiProfileLabel(c)}${model}${flagStr}`);
37060
+ });
37061
+ if (pinned) {
37062
+ ctx.print("A profile is pinned \u2014 tried first every prompt; /ai-profile reset returns to auto.");
37063
+ }
37064
+ return;
37065
+ }
37066
+ if (action === "reset" || action === "auto" || action === "clear") {
37067
+ clearPinnedCredential(dir2);
37068
+ ctx.print("\u2714 AI profile pin cleared \u2014 failover is back to automatic (config order / remembered).");
37069
+ const working = getWorkingCredential(dir2);
37070
+ ctx.setActiveProfile(working ? labelFromCredentialKey(working) : null);
37071
+ return;
37072
+ }
37073
+ if (action === "use" || action === "switch") {
37074
+ const n = Number(rest[0]);
37075
+ if (!Number.isInteger(n) || n < 1 || n > list.length) {
37076
+ ctx.print(`usage: /ai-profile use <n> (n = 1-${list.length || "?"} from /ai-profile list)`, "error");
37077
+ return;
37078
+ }
37079
+ const cred = list[n - 1];
37080
+ if (!isUsableCredential(cred)) {
37081
+ const why = cred.enabled === false ? "it is disabled" : "it has no usable profile dir / provider";
37082
+ ctx.print(`Profile [${n}] "${aiProfileLabel(cred)}" can't be used \u2014 ${why}.`, "error");
37083
+ return;
37084
+ }
37085
+ const credDir = resolveHomePath(cred.profile);
37086
+ setPinnedCredential(dir2, credentialKey(cred.provider, credDir));
37087
+ ctx.setActiveProfile(profileDisplayLabel(credDir));
37088
+ ctx.print(
37089
+ `\u2714 switched to ${cred.provider} profile "${aiProfileLabel(cred)}" \u2014 tried first from the next prompt (failover keeps the rest as backup).`
37090
+ );
37091
+ return;
37092
+ }
37093
+ ctx.print(`Unknown /ai-profile subcommand "${sub}". Use: list | use <n> | reset.`, "error");
37094
+ }
36951
37095
  function runFold(ctx, action) {
36952
37096
  if (ctx.maxBlock === 0) {
36953
37097
  ctx.print("No collapsible blocks on screen.", "warn");
@@ -37037,6 +37181,12 @@ var COMMANDS = [
37037
37181
  description: "Show this machine account + its teams & projects",
37038
37182
  run: runWhoami
37039
37183
  },
37184
+ {
37185
+ name: "ai-profile",
37186
+ usage: "/ai-profile [list|use <n>|reset]",
37187
+ description: "List AI profiles / switch which one runs (ADR-0250)",
37188
+ run: runAiProfile
37189
+ },
37040
37190
  {
37041
37191
  name: "config",
37042
37192
  usage: "/config [show|init|set <k> <v>|delete <k>]",
@@ -37269,7 +37419,8 @@ async function listExtras() {
37269
37419
  category: "runtime",
37270
37420
  installed: true,
37271
37421
  version: info.version ?? null,
37272
- installable: true
37422
+ installable: true,
37423
+ updatable: true
37273
37424
  }));
37274
37425
  }
37275
37426
  async function detectWorkerTools() {
@@ -37282,33 +37433,39 @@ async function detectWorkerTools() {
37282
37433
  category: t.category,
37283
37434
  installed: version !== null,
37284
37435
  version,
37285
- installable: t.installable
37436
+ installable: t.installable,
37437
+ updatable: t.updatable
37286
37438
  };
37287
37439
  })
37288
37440
  );
37289
37441
  const extras = await listExtras().catch(() => []);
37290
37442
  return { catalog, extras };
37291
37443
  }
37444
+ function opPast(op) {
37445
+ return op === "install" ? "installed" : op === "uninstall" ? "uninstalled" : "updated";
37446
+ }
37292
37447
  function resolvePackage(name, op) {
37293
37448
  const entry = WORKER_TOOL_CATALOG.find((t) => t.id === name);
37294
37449
  if (entry) {
37295
- if (!entry.installable) {
37296
- return { error: `"${name}" is a prerequisite and cannot be ${op}ed from here.` };
37450
+ const allowed = op === "update" ? entry.updatable : entry.installable;
37451
+ if (!allowed) {
37452
+ return { error: `"${name}" is a prerequisite and cannot be ${opPast(op)} from here.` };
37297
37453
  }
37298
37454
  return { pkg: entry.installPackage ?? name };
37299
37455
  }
37300
37456
  if (WORKER_TOOL_PREREQUISITE_IDS.includes(name)) {
37301
- return { error: `"${name}" is a prerequisite and cannot be ${op}ed from here.` };
37457
+ return { error: `"${name}" is a prerequisite and cannot be ${opPast(op)} from here.` };
37302
37458
  }
37303
37459
  if (WORKER_TOOL_BUNDLED_GLOBALS.includes(name)) {
37304
- return { error: `"${name}" ships with Node and cannot be ${op}ed from here.` };
37460
+ return { error: `"${name}" ships with Node and cannot be ${opPast(op)} from here.` };
37305
37461
  }
37306
37462
  if (!NPM_NAME.test(name)) return { error: `Invalid package name: "${name}".` };
37307
37463
  return { pkg: name };
37308
37464
  }
37309
37465
  function opArgs(op, manager, pkg) {
37310
- if (manager === "pnpm") return [op === "install" ? "add" : "remove", "-g", pkg];
37311
- return [op === "install" ? "install" : "uninstall", "-g", pkg];
37466
+ if (op === "uninstall") return manager === "pnpm" ? ["remove", "-g", pkg] : ["uninstall", "-g", pkg];
37467
+ const target = op === "update" ? `${pkg}@latest` : pkg;
37468
+ return manager === "pnpm" ? ["add", "-g", target] : ["install", "-g", target];
37312
37469
  }
37313
37470
  async function runWorkerToolOp(op, name, manager, onLine) {
37314
37471
  const resolved = resolvePackage(name, op);
@@ -37417,6 +37574,9 @@ var WsClient = class {
37417
37574
  reconnectEntryId = null;
37418
37575
  /** Org-configured reconnect cap (seconds) from the last ws_token — overrides env. */
37419
37576
  orgReconnectMaxSec = null;
37577
+ /** Org timezone (IANA) from the last ws_token (ADR-0132) — a rented worker follows its renter's
37578
+ * org; used to stamp the AI-run start time on the `aireq` transcript marker (ADR-0249). */
37579
+ orgTimezone = "UTC";
37420
37580
  /** Env override for the reconnect cap (seconds); resolved once at construction. */
37421
37581
  envReconnectMaxSec = envReconnectMaxSec();
37422
37582
  /** Process start (ms) — uptime for a web-dispatched `/status` (ADR-0249). */
@@ -37601,6 +37761,9 @@ var WsClient = class {
37601
37761
  if (typeof token.reconnectMaxBackoffSec === "number") {
37602
37762
  this.orgReconnectMaxSec = token.reconnectMaxBackoffSec;
37603
37763
  }
37764
+ if (typeof token.timezone === "string" && token.timezone) {
37765
+ this.orgTimezone = token.timezone;
37766
+ }
37604
37767
  if (typeof token.autoUpdateDaily === "boolean") {
37605
37768
  this.updateScheduler.configure({
37606
37769
  autoUpdateDaily: token.autoUpdateDaily,
@@ -38105,9 +38268,10 @@ var WsClient = class {
38105
38268
  );
38106
38269
  break;
38107
38270
  case WsChannels.TOOLS_INSTALL:
38108
- case WsChannels.TOOLS_UNINSTALL: {
38271
+ case WsChannels.TOOLS_UNINSTALL:
38272
+ case WsChannels.TOOLS_UPDATE: {
38109
38273
  const req = payload;
38110
- const op = message.channel === WsChannels.TOOLS_INSTALL ? "install" : "uninstall";
38274
+ const op = message.channel === WsChannels.TOOLS_INSTALL ? "install" : message.channel === WsChannels.TOOLS_UPDATE ? "update" : "uninstall";
38111
38275
  this.send(message.channel, { started: true }, message.id);
38112
38276
  void runWorkerToolOp(
38113
38277
  op,
@@ -38288,9 +38452,10 @@ var WsClient = class {
38288
38452
  const unified = isUnifiedConfig(config);
38289
38453
  const mode = config.aiFailoverMode ?? "remember";
38290
38454
  const workingDir = unified ? null : getWorkingProfile(this.context.profileDir, cmd);
38291
- const workingCred = unified && mode === "remember" ? getWorkingCredential(this.context.profileDir) : null;
38455
+ const pinnedCred = unified ? getPinnedCredential(this.context.profileDir) : null;
38456
+ const workingCred = unified ? pinnedCred ?? (mode === "remember" ? getWorkingCredential(this.context.profileDir) : null) : null;
38292
38457
  const startDir = unified ? null : this.profileUnderSessionPressure(config, cmd, workingDir);
38293
- const startCred = unified ? this.credentialUnderSessionPressure(config, workingCred) : null;
38458
+ const startCred = unified && !pinnedCred ? this.credentialUnderSessionPressure(config, workingCred) : null;
38294
38459
  if (startDir && startDir !== workingDir) {
38295
38460
  this.bus.push({
38296
38461
  source: origin,
@@ -38322,7 +38487,8 @@ ${guardedPrompt}`;
38322
38487
  }
38323
38488
  const plan = planAiRun(effectivePrompt, config, hint, resume, oneShot);
38324
38489
  const markerArgs = plan.attempts[0]?.args ?? [];
38325
- this.bus.push({ source: origin, kind: "aireq", text: `${plan.cmd} \u2039 ${prompt}` });
38490
+ const startStamp = formatTimestampInZone(/* @__PURE__ */ new Date(), this.orgTimezone);
38491
+ this.bus.push({ source: origin, kind: "aireq", text: `${startStamp} ${plan.cmd} \u2039 ${prompt}` });
38326
38492
  logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
38327
38493
  if (origin === "local") {
38328
38494
  this.send(WsChannels.COMMAND_ANNOUNCE, {
@@ -38557,6 +38723,8 @@ ${guardedPrompt}`;
38557
38723
  submitAi: () => {
38558
38724
  },
38559
38725
  // only /claude-cmd uses this, and it is special-cased above
38726
+ // /ai-profile use/reset from the web Console updates the shared header too (ADR-0250).
38727
+ setActiveProfile: (label) => this.bus.setActiveProfile(label),
38560
38728
  reconnect: () => this.reconnectNow(),
38561
38729
  expand: () => emit("Use the web console's fold viewer to expand blocks."),
38562
38730
  collapse: () => emit("Use the web console's fold viewer to collapse blocks."),
@@ -45263,12 +45431,17 @@ function textColor(line) {
45263
45431
  if (line.kind === "exit") return "green";
45264
45432
  return void 0;
45265
45433
  }
45434
+ var START_STAMP_RE = /^(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2} )/;
45266
45435
  function AiMarkerLine({ line }) {
45267
45436
  const arrow = line.kind === "aireq" ? "\u2039" : "\u203A";
45268
45437
  const idx = line.text.indexOf(arrow);
45269
45438
  if (idx < 0) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text || " " });
45439
+ const before = line.text.slice(0, idx);
45440
+ const stamp = line.kind === "aireq" ? START_STAMP_RE.exec(before)?.[1] ?? "" : "";
45441
+ const name = stamp ? before.slice(stamp.length) : before;
45270
45442
  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) }),
45443
+ stamp ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: stamp }) : null,
45444
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "cyan", bold: true, children: name }),
45272
45445
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: arrow }),
45273
45446
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text.slice(idx + 1) })
45274
45447
  ] });
@@ -45685,6 +45858,9 @@ function App2({
45685
45858
  setPendingConfirm({ prompt, onYes });
45686
45859
  },
45687
45860
  submitAi: (aiInput) => bus.submitLocal(aiInput),
45861
+ // /ai-profile use/reset updates the header's active-profile label at once (ADR-0250);
45862
+ // the bus event feeds the React state (see the onActiveProfile subscription above).
45863
+ setActiveProfile: (label) => bus.setActiveProfile(label),
45688
45864
  reconnect: () => bus.requestReconnect(),
45689
45865
  expand: expandFold,
45690
45866
  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.3",
4
4
  "private": false,
5
5
  "description": "4PM CLI — drives Claude/Codex CLIs on AI worker machines (Node + TypeScript)",
6
6
  "license": "LicenseRef-4PM-Source-Available",