@4pm/cli 1.5.0 → 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 +1181 -740
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28316,6 +28316,11 @@ function defaultProfileConfig() {
28316
28316
  // Terminate a single AI run after 5 min by default (ADR-0243) so a hung/looping AI CLI
28317
28317
  // (e.g. a heavy spec review/compose) can't spin the dispatcher forever; 0 = no limit.
28318
28318
  aiRunTimeoutSec: 300,
28319
+ // Web-Console blocked slash commands (ADR-0249): block the two that tamper with / kill the
28320
+ // worker by default — `/quit` (stops the cli — DoS) and `/config` (writes config.json). Every
28321
+ // other command (incl. /reconnect, /logs — non-destructive) is allowed; operators adjust this
28322
+ // list in the Worker config. Empty ⇒ allow all.
28323
+ webBlockedCommands: ["quit", "config"],
28319
28324
  // Shared AI memory (ADR-0245) — off by default (opt-in; costs a compaction call per turn).
28320
28325
  aiMemoryEnabled: false,
28321
28326
  aiMemoryBudgetChars: 6e3
@@ -28332,6 +28337,11 @@ function resolveIdleAutoClearMinutes(config) {
28332
28337
  const project = config.projectAutoClearIdleMinutes ?? 0;
28333
28338
  return project > 0 ? project : config.autoClearIdleMinutes ?? 10;
28334
28339
  }
28340
+ var DEFAULT_WEB_BLOCKED_COMMANDS = ["quit", "config"];
28341
+ function resolveWebBlockedCommands(config) {
28342
+ const list = Array.isArray(config.webBlockedCommands) ? config.webBlockedCommands : DEFAULT_WEB_BLOCKED_COMMANDS;
28343
+ return new Set(list.map((c) => String(c).trim().toLowerCase()).filter(Boolean));
28344
+ }
28335
28345
  function ensureProfileConfig(dir2) {
28336
28346
  if (existsSync(join(dir2, "config.json"))) return false;
28337
28347
  writeProfileConfig(dir2, defaultProfileConfig());
@@ -28765,7 +28775,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
28765
28775
  // src/core/ws-client.ts
28766
28776
  init_wrapper();
28767
28777
  import { randomUUID } from "crypto";
28768
- import { basename as basename4, join as join24 } from "path";
28778
+ import { basename as basename4, join as join25 } from "path";
28769
28779
 
28770
28780
  // vendor/@4pm/utils/helpers.ts
28771
28781
  function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
@@ -28824,18 +28834,21 @@ function isValidRoleSet(roles) {
28824
28834
 
28825
28835
  // vendor/@4pm/constants/worker-tools.ts
28826
28836
  var WORKER_TOOL_CATALOG = [
28827
- { id: "git", label: "Git", category: "vcs", versionArg: "--version", installPackage: null, installable: false },
28828
- { id: "node", label: "Node.js", category: "runtime", versionArg: "--version", installPackage: null, installable: false },
28829
- { id: "npm", label: "npm", category: "runtime", versionArg: "--version", installPackage: null, installable: false },
28830
- { id: "pnpm", label: "pnpm", category: "runtime", versionArg: "--version", installPackage: "pnpm", installable: false },
28831
- { id: "claude", label: "Claude Code", category: "ai-cli", versionArg: "--version", installPackage: "@anthropic-ai/claude-code", installable: false },
28832
- { id: "codex", label: "Codex", category: "ai-cli", versionArg: "--version", installPackage: "@openai/codex", installable: false },
28833
- { id: "gh", label: "GitHub CLI", category: "vcs", versionArg: "--version", installPackage: null, installable: false },
28834
- { 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 }
28835
28845
  ];
28836
28846
  var WORKER_TOOL_PREREQUISITE_IDS = WORKER_TOOL_CATALOG.filter(
28837
28847
  (t) => !t.installable
28838
28848
  ).map((t) => t.id);
28849
+ var WORKER_TOOL_UPDATABLE_IDS = WORKER_TOOL_CATALOG.filter(
28850
+ (t) => t.updatable
28851
+ ).map((t) => t.id);
28839
28852
  var WORKER_TOOL_BUNDLED_GLOBALS = ["npm", "corepack"];
28840
28853
 
28841
28854
  // vendor/@4pm/constants/ws.ts
@@ -32932,7 +32945,15 @@ var dispatchCommandRequestSchema = external_exports.object({
32932
32945
  * `project.git_write` (`write`) instead of `command.execute`, and validates `command`
32933
32946
  * against the git allowlist (`isGitCommandAllowed`). Absent ⇒ normal console dispatch.
32934
32947
  */
32935
- gitOp: external_exports.enum(["read", "write"]).optional()
32948
+ gitOp: external_exports.enum(["read", "write"]).optional(),
32949
+ /**
32950
+ * One-shot AI mode (ADR-0249): the prompt is a text-in → text-out task (spec review /
32951
+ * compose / suggest / generators) that must NOT trigger the AI CLI's agentic tool loop.
32952
+ * The cli caps such a run (`--max-turns 1` + disallowed agentic tools) so it can't wander
32953
+ * the repo / edit files / loop forever. Only meaningful with `ai:true`; absent ⇒ a full
32954
+ * agent run (the Console tab + Git merge, which legitimately use tools).
32955
+ */
32956
+ aiOneShot: external_exports.boolean().optional()
32936
32957
  }).superRefine((v, ctx) => {
32937
32958
  if (v.machineLinkId == null === (v.pick == null)) {
32938
32959
  ctx.addIssue({
@@ -32955,10 +32976,30 @@ var dispatchCommandRequestSchema = external_exports.object({
32955
32976
  var listCommandsQuerySchema = baseRequestSchema.extend({
32956
32977
  projectId: external_exports.string().uuid(),
32957
32978
  // Optional search filters (command-0005): `search` (from BaseRequest) matches the command
32958
- // text/status; `from`/`to` are an inclusive `YYYY-MM-DD` date range over `startedAt`.
32979
+ // text/status; `from`/`to` are an inclusive `YYYY-MM-DD` date range over `startedAt`;
32980
+ // `machineLinkId` (ADR-0249) filters to one machine-user's commands (absent ⇒ all).
32959
32981
  from: external_exports.string().optional(),
32960
- to: external_exports.string().optional()
32982
+ to: external_exports.string().optional(),
32983
+ machineLinkId: external_exports.string().uuid().optional()
32961
32984
  });
32985
+ var CLI_SLASH_COMMANDS = [
32986
+ { name: "help", usage: "/help", description: "List slash commands" },
32987
+ { name: "version", usage: "/version", description: "Show the cli version" },
32988
+ { name: "status", usage: "/status", description: "Show connection + profile + session status" },
32989
+ { name: "history", usage: "/history [N]", description: "List the last N executed commands (+ time)" },
32990
+ { name: "output", usage: "/output <n>", description: "Replay a command's output (n from /history)" },
32991
+ { name: "logs", usage: "/logs [N]", description: "Tail the profile's structured log" },
32992
+ { name: "expand", usage: "/expand [N]", description: "Expand a collapsed \u25B8[N] block" },
32993
+ { name: "collapse", usage: "/collapse [N]", description: "Collapse an expanded \u25B8[N] block" },
32994
+ { name: "reconnect", usage: "/reconnect", description: "Reconnect to the server now (skip the backoff)" },
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" },
32997
+ { name: "config", usage: "/config [show|init|set <k> <v>|delete <k>]", description: "View / init / update / delete profile config" },
32998
+ { name: "claude-cmd", usage: "/claude-cmd /context", description: "Run an AI-CLI slash command (e.g. /context, /usage)" },
32999
+ { name: "clear", usage: "/clear", description: "Clear the transcript" },
33000
+ { name: "quit", usage: "/quit", description: "Quit the cli (also /exit)" }
33001
+ ];
33002
+ var CLI_SLASH_COMMAND_NAMES = CLI_SLASH_COMMANDS.map((c) => c.name);
32962
33003
 
32963
33004
  // vendor/@4pm/validation/schemas.ts
32964
33005
  var passwordSchema = external_exports.string().min(8, "password: at least 8 characters").max(32, "password: at most 32 characters").regex(/[a-zA-Z]/, "password: needs at least 1 letter").regex(/[0-9]/, "password: needs at least 1 digit");
@@ -33001,7 +33042,10 @@ var listWorkersQuerySchema = baseRequestSchema.extend({
33001
33042
  /** Only workers whose live CPU/memory/disk usage % ≥ the given threshold (0–100). */
33002
33043
  minCpuPct: external_exports.coerce.number().min(0).max(100).optional(),
33003
33044
  minMemPct: external_exports.coerce.number().min(0).max(100).optional(),
33004
- 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()
33005
33049
  });
33006
33050
  var updateWorkerRequestSchema = external_exports.object({
33007
33051
  name: external_exports.string().min(1).max(100).optional()
@@ -33030,6 +33074,12 @@ var workerToolInstallSchema = external_exports.object({
33030
33074
  /** Package manager to run the global install with. */
33031
33075
  manager: external_exports.enum(["npm", "pnpm"])
33032
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
+ });
33033
33083
  var configWriteRequestSchema = external_exports.object({
33034
33084
  config: external_exports.string().max(256 * 1024)
33035
33085
  });
@@ -33634,8 +33684,14 @@ var WsChannels = {
33634
33684
  TOOLS_LIST: "tools.list",
33635
33685
  TOOLS_INSTALL: "tools.install",
33636
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",
33637
33690
  TOOLS_PROGRESS: "tools.progress",
33638
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",
33639
33695
  // autonomous mode control (ADR-0152): read settings+status+books+approvals, tail logs, and
33640
33696
  // write {settings|approvals|userTodo|cron} — the dashboard drives the worker's autonomous engine.
33641
33697
  AUTONOMOUS_READ: "autonomous.read",
@@ -33792,11 +33848,11 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
33792
33848
  }
33793
33849
 
33794
33850
  // src/core/ws-client.ts
33795
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, rmSync as rmSync4 } from "fs";
33851
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, rmSync as rmSync4 } from "fs";
33796
33852
  import { randomUUID as randomCommandId } from "crypto";
33797
33853
 
33798
33854
  // src/core/update-scheduler.ts
33799
- import { spawn } from "child_process";
33855
+ import { spawn as spawn2 } from "child_process";
33800
33856
 
33801
33857
  // src/common/logger/logger.ts
33802
33858
  import { appendFile, mkdir, readdir, unlink } from "fs/promises";
@@ -33901,6 +33957,153 @@ ${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
33901
33957
  }
33902
33958
  }
33903
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
+
33904
34107
  // src/core/update-scheduler.ts
33905
34108
  var TICK_MS = 6e4;
33906
34109
  var UpdateScheduler = class {
@@ -33983,6 +34186,7 @@ var UpdateScheduler = class {
33983
34186
  async runUpdate() {
33984
34187
  this.updating = true;
33985
34188
  try {
34189
+ await this.updateFlaggedTools();
33986
34190
  this.bus.log("Scheduled auto-update: checking for a new version\u2026");
33987
34191
  const result = await updateToLatest(this.serverUrl);
33988
34192
  if (result.action === "already-latest") {
@@ -33996,7 +34200,7 @@ var UpdateScheduler = class {
33996
34200
  }
33997
34201
  logger.info("update.scheduled.updated", { version: result.version });
33998
34202
  this.bus.log(`Updated to ${result.version} \u2014 restarting to apply\u2026`);
33999
- const child = spawn(process.execPath, process.argv.slice(1), {
34203
+ const child = spawn2(process.execPath, process.argv.slice(1), {
34000
34204
  stdio: "inherit",
34001
34205
  env: { ...process.env, FOURPM_NO_UPDATE: "1" }
34002
34206
  });
@@ -34007,18 +34211,45 @@ var UpdateScheduler = class {
34007
34211
  this.updating = false;
34008
34212
  }
34009
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
+ }
34010
34223
  };
34011
34224
 
34012
34225
  // src/core/executor.ts
34013
- import { spawn as spawn2 } from "child_process";
34226
+ import { spawn as spawn3 } from "child_process";
34014
34227
  var KILL_GRACE_MS = 5e3;
34015
34228
  async function runCommand(dispatch, emit, opts) {
34016
34229
  let seq = 0;
34017
- const child = spawn2(dispatch.cmd, dispatch.args ?? [], {
34018
- cwd: dispatch.path,
34019
- env: { ...process.env, ...dispatch.env },
34020
- 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", () => {
34021
34249
  });
34250
+ if (opts?.stdin != null) {
34251
+ child.stdin?.write(opts.stdin);
34252
+ }
34022
34253
  child.stdin?.end();
34023
34254
  const timeoutMs = opts?.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 0;
34024
34255
  let timedOut = false;
@@ -34117,15 +34348,24 @@ function createAiStreamParser(cli) {
34117
34348
  let buffer = "";
34118
34349
  const acc = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
34119
34350
  let sessionId = "";
34351
+ let apiError = { isError: false, status: null };
34120
34352
  function handleClaude(ev) {
34121
34353
  if (typeof ev.session_id === "string" && ev.session_id) sessionId = ev.session_id;
34122
- if (ev.type === "result" && ev.usage) {
34123
- acc.input = ev.usage.input_tokens ?? 0;
34124
- acc.output = ev.usage.output_tokens ?? 0;
34125
- acc.cacheRead = ev.usage.cache_read_input_tokens ?? 0;
34126
- acc.cacheCreation = ev.usage.cache_creation_input_tokens ?? 0;
34127
- acc.tokens = acc.input + acc.output;
34128
- if (typeof ev.total_cost_usd === "number") acc.costUsd = ev.total_cost_usd;
34354
+ if (ev.type === "result") {
34355
+ if (ev.is_error === true) {
34356
+ apiError = {
34357
+ isError: true,
34358
+ status: typeof ev.api_error_status === "number" ? ev.api_error_status : null
34359
+ };
34360
+ }
34361
+ if (ev.usage) {
34362
+ acc.input = ev.usage.input_tokens ?? 0;
34363
+ acc.output = ev.usage.output_tokens ?? 0;
34364
+ acc.cacheRead = ev.usage.cache_read_input_tokens ?? 0;
34365
+ acc.cacheCreation = ev.usage.cache_creation_input_tokens ?? 0;
34366
+ acc.tokens = acc.input + acc.output;
34367
+ if (typeof ev.total_cost_usd === "number") acc.costUsd = ev.total_cost_usd;
34368
+ }
34129
34369
  return "";
34130
34370
  }
34131
34371
  if (ev.type === "assistant") {
@@ -34185,7 +34425,8 @@ function createAiStreamParser(cli) {
34185
34425
  return handled === null ? line : handled;
34186
34426
  },
34187
34427
  usage: () => ({ ...acc }),
34188
- sessionId: () => sessionId
34428
+ sessionId: () => sessionId,
34429
+ apiError: () => ({ ...apiError })
34189
34430
  };
34190
34431
  }
34191
34432
  function estimateTokens(text) {
@@ -34203,12 +34444,21 @@ function isSessionLimit(text) {
34203
34444
  text
34204
34445
  );
34205
34446
  }
34206
- function runAttempt(commandId, cmd, args, cwd2, env3, onChunk, timeoutMs) {
34447
+ function isOutOfCredits(text) {
34448
+ return /out of (?:usage )?credits|credits[_ ]required|out[_ ]of[_ ]credits|insufficient credits/i.test(
34449
+ text
34450
+ );
34451
+ }
34452
+ function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs) {
34207
34453
  return new Promise((resolve8) => {
34208
- void runCommand({ commandId, cmd, args, path: cwd2, env: env3 }, (out) => {
34209
- if (out.chunk) onChunk(out.chunk);
34210
- if (out.done) resolve8(out.exitCode ?? -1);
34211
- }, 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));
34212
34462
  });
34213
34463
  }
34214
34464
  async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
@@ -34229,14 +34479,15 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
34229
34479
  handlers.onChunk(text);
34230
34480
  }
34231
34481
  };
34232
- 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);
34233
34483
  const tail = parser.flush();
34234
34484
  if (tail) {
34235
34485
  captured += tail;
34236
34486
  handlers.onChunk(tail);
34237
34487
  }
34238
34488
  lastCaptured = captured;
34239
- if (finalExit === 0) {
34489
+ const apiErr = parser.apiError();
34490
+ if (finalExit === 0 && !apiErr.isError) {
34240
34491
  const usage = parser.usage();
34241
34492
  if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
34242
34493
  reportToolResult(attempt.cmd, true);
@@ -34249,7 +34500,8 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
34249
34500
  sessionId: parser.sessionId()
34250
34501
  };
34251
34502
  }
34252
- const reason = isAuthFailure(captured) ? "auth" : isSessionLimit(captured) ? "limit" : "other";
34503
+ if (finalExit === 0 && apiErr.isError) finalExit = apiErr.status && apiErr.status > 0 ? apiErr.status : 1;
34504
+ const reason = isAuthFailure(captured) ? "auth" : apiErr.status === 429 || isOutOfCredits(captured) ? "credits" : isSessionLimit(captured) ? "limit" : "other";
34253
34505
  if (i === total - 1) break;
34254
34506
  handlers.onAttemptFail(attempt.label, reason);
34255
34507
  }
@@ -34258,6 +34510,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
34258
34510
  }
34259
34511
  function summarizeAiFailure(captured, exitCode) {
34260
34512
  if (isAuthFailure(captured)) return "Not logged in";
34513
+ if (isOutOfCredits(captured)) return "Out of usage credits";
34261
34514
  if (isSessionLimit(captured)) return "Usage limit reached";
34262
34515
  const firstLine = captured.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
34263
34516
  return firstLine || `exited ${exitCode}`;
@@ -34303,6 +34556,52 @@ function setWorkingCredential(profileDir2, key) {
34303
34556
  } catch {
34304
34557
  }
34305
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
+ }
34306
34605
 
34307
34606
  // src/utils/ai-cli.ts
34308
34607
  import { readFileSync as readFileSync8 } from "fs";
@@ -34313,6 +34612,42 @@ var REQUIRED_AI_ARGS = {
34313
34612
  codex: ["exec", "--json"],
34314
34613
  antigravity: []
34315
34614
  };
34615
+ var ONE_SHOT_DISALLOWED_CLAUDE_TOOLS = [
34616
+ "Bash",
34617
+ "Edit",
34618
+ "Write",
34619
+ "Read",
34620
+ "Glob",
34621
+ "Grep",
34622
+ "NotebookEdit",
34623
+ "Task",
34624
+ "WebSearch",
34625
+ "WebFetch",
34626
+ "Skill",
34627
+ "SlashCommand",
34628
+ "ToolSearch",
34629
+ "TaskCreate",
34630
+ "TaskGet",
34631
+ "TaskList",
34632
+ "TaskOutput",
34633
+ "TaskStop",
34634
+ "TaskUpdate",
34635
+ "Monitor",
34636
+ "DesignSync",
34637
+ "CronCreate",
34638
+ "CronDelete",
34639
+ "CronList",
34640
+ "EnterWorktree",
34641
+ "ExitWorktree",
34642
+ "RemoteTrigger",
34643
+ "ScheduleWakeup",
34644
+ "SendMessage",
34645
+ "PushNotification"
34646
+ ].join(",");
34647
+ function oneShotArgs(cmd) {
34648
+ if (!cmd.includes("claude")) return [];
34649
+ return ["--max-turns=1", "--disallowedTools", ONE_SHOT_DISALLOWED_CLAUDE_TOOLS, "--permission-mode", "default"];
34650
+ }
34316
34651
  function requiredArgs(cmd) {
34317
34652
  if (cmd.includes("claude")) return REQUIRED_AI_ARGS.claude;
34318
34653
  if (cmd.includes("codex")) return REQUIRED_AI_ARGS.codex;
@@ -34372,12 +34707,15 @@ function folderScopeGuard(folder, prompt) {
34372
34707
  prompt
34373
34708
  ].join("\n");
34374
34709
  }
34375
- function buildRunArgs(profile, cmd, prompt, resumeId) {
34710
+ function buildRunArgs(profile, cmd, resumeId, oneShot = false) {
34376
34711
  const extras = profile.args ?? [];
34377
34712
  const model = profile.model?.trim();
34378
34713
  const modelArgs = model ? ["--model", model] : [];
34379
34714
  const resumeArgs = resumeId && cmd.includes("claude") ? ["--resume", resumeId] : [];
34380
- return [...requiredArgs(cmd), ...resumeArgs, ...extras, ...modelArgs, prompt];
34715
+ const turns = profile.maxTurns;
34716
+ const perProfileTurns = !oneShot && cmd.includes("claude") && typeof turns === "number" && turns > 0 ? [`--max-turns=${Math.floor(turns)}`] : [];
34717
+ const oneShotExtra = oneShot ? oneShotArgs(cmd) : perProfileTurns;
34718
+ return [...requiredArgs(cmd), ...resumeArgs, ...extras, ...modelArgs, ...oneShotExtra];
34381
34719
  }
34382
34720
  function claudeHomeDirs(config) {
34383
34721
  const dirs = profileDirs(claudeProfilesOf(config));
@@ -34416,10 +34754,10 @@ function resolveClaudeProfiles(config, workingDir) {
34416
34754
  if (idx > 0) profiles.unshift(profiles.splice(idx, 1)[0]);
34417
34755
  return profiles;
34418
34756
  }
34419
- function planAiRun(prompt, config, hint = {}, resume = /* @__PURE__ */ new Map()) {
34420
- return isUnifiedConfig(config) ? planUnifiedRun(prompt, config, hint.credential ?? null, resume) : planLegacyRun(prompt, config, hint.dir ?? null, resume);
34757
+ function planAiRun(prompt, config, hint = {}, resume = /* @__PURE__ */ new Map(), oneShot = false) {
34758
+ return isUnifiedConfig(config) ? planUnifiedRun(prompt, config, hint.credential ?? null, resume, oneShot) : planLegacyRun(prompt, config, hint.dir ?? null, resume, oneShot);
34421
34759
  }
34422
- function planUnifiedRun(prompt, config, workingCredential, resume) {
34760
+ function planUnifiedRun(prompt, config, workingCredential, resume, oneShot) {
34423
34761
  const baseEnv = config.aiEnv ?? {};
34424
34762
  const active = activeProvider(config);
34425
34763
  const usable = config.aiProfiles.filter(isUsableCredential);
@@ -34428,7 +34766,7 @@ function planUnifiedRun(prompt, config, workingCredential, resume) {
34428
34766
  return {
34429
34767
  cmd: active,
34430
34768
  attempts: [
34431
- { cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, prompt), env: { ...baseEnv } }
34769
+ { cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, void 0, oneShot), stdin: prompt, env: { ...baseEnv } }
34432
34770
  ]
34433
34771
  };
34434
34772
  }
@@ -34446,19 +34784,20 @@ function planUnifiedRun(prompt, config, workingCredential, resume) {
34446
34784
  label: cred.label?.trim() || profileDisplayLabel(dir2),
34447
34785
  dir: dir2,
34448
34786
  key,
34449
- args: buildRunArgs(cred, cmd, prompt, resume.get(key)),
34787
+ args: buildRunArgs(cred, cmd, resume.get(key), oneShot),
34788
+ stdin: prompt,
34450
34789
  env: envVar ? { ...baseEnv, [envVar]: dir2 } : { ...baseEnv }
34451
34790
  }));
34452
34791
  return { cmd: attempts[0]?.cmd ?? DEFAULT_AI_CLI, attempts };
34453
34792
  }
34454
- function planLegacyRun(prompt, config, workingDir, resume) {
34793
+ function planLegacyRun(prompt, config, workingDir, resume, oneShot) {
34455
34794
  const cmd = config.aiCli || DEFAULT_AI_CLI;
34456
34795
  const baseEnv = config.aiEnv ?? {};
34457
34796
  const envVar = profileEnvVar(cmd);
34458
34797
  const defaultPlan = {
34459
34798
  cmd,
34460
34799
  attempts: [
34461
- { cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, prompt), env: { ...baseEnv } }
34800
+ { cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, void 0, oneShot), stdin: prompt, env: { ...baseEnv } }
34462
34801
  ]
34463
34802
  };
34464
34803
  if (!envVar) return defaultPlan;
@@ -34475,7 +34814,8 @@ function planLegacyRun(prompt, config, workingDir, resume) {
34475
34814
  label: profileDisplayLabel(dir2),
34476
34815
  dir: dir2,
34477
34816
  key,
34478
- args: buildRunArgs(profile, cmd, prompt, resume.get(key)),
34817
+ args: buildRunArgs(profile, cmd, resume.get(key), oneShot),
34818
+ stdin: prompt,
34479
34819
  env: { ...baseEnv, [envVar]: dir2 }
34480
34820
  };
34481
34821
  });
@@ -34483,7 +34823,7 @@ function planLegacyRun(prompt, config, workingDir, resume) {
34483
34823
  }
34484
34824
 
34485
34825
  // src/core/claude-usage.ts
34486
- import { spawn as spawn3 } from "child_process";
34826
+ import { spawn as spawn4 } from "child_process";
34487
34827
  import { readFileSync as readFileSync9 } from "fs";
34488
34828
  import { join as join9 } from "path";
34489
34829
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
@@ -34530,7 +34870,7 @@ function runUsageCli(dir2, aiCli2) {
34530
34870
  }, 3e4);
34531
34871
  timer.unref?.();
34532
34872
  try {
34533
- child = spawn3(aiCli2, ["/usage"], {
34873
+ child = spawn4(aiCli2, ["/usage"], {
34534
34874
  env: { ...process.env, CLAUDE_CONFIG_DIR: dir2 },
34535
34875
  stdio: ["ignore", "pipe", "ignore"]
34536
34876
  });
@@ -34600,7 +34940,7 @@ async function checkClaudeUsage(dirs, aiCli2 = "claude") {
34600
34940
  // src/core/ai-assist.ts
34601
34941
  import { execFile } from "child_process";
34602
34942
  import { promisify } from "util";
34603
- var run = promisify(execFile);
34943
+ var run2 = promisify(execFile);
34604
34944
  function aiCli() {
34605
34945
  return process.env.AI_CLI || "claude";
34606
34946
  }
@@ -34643,7 +34983,7 @@ function parseAi(cli, stdout, prompt) {
34643
34983
  }
34644
34984
  async function runAi(prompt) {
34645
34985
  const cli = aiCli();
34646
- const { stdout } = await run(cli, aiArgs(cli, prompt), {
34986
+ const { stdout } = await run2(cli, aiArgs(cli, prompt), {
34647
34987
  timeout: 12e4,
34648
34988
  maxBuffer: 8 * 1024 * 1024
34649
34989
  });
@@ -34986,7 +35326,7 @@ async function writeWorkerFile(root, relPath, content) {
34986
35326
  }
34987
35327
 
34988
35328
  // src/core/support-answer.ts
34989
- import { spawn as spawn4 } from "child_process";
35329
+ import { spawn as spawn5 } from "child_process";
34990
35330
  import { execFile as execFile2 } from "child_process";
34991
35331
  import { createHash as createHash3 } from "crypto";
34992
35332
  import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
@@ -35121,7 +35461,7 @@ function runClaudeOnce(cmd, profile, prompt, extraEnv) {
35121
35461
  ...extraEnv,
35122
35462
  ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
35123
35463
  };
35124
- const child = spawn4(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
35464
+ const child = spawn5(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
35125
35465
  const parser = createAiStreamParser(cmd);
35126
35466
  let out = "";
35127
35467
  let err = "";
@@ -35191,7 +35531,7 @@ async function runSupportAnswer(req, ai, profileDir2) {
35191
35531
  }
35192
35532
 
35193
35533
  // src/core/knowledge-compose.ts
35194
- import { spawn as spawn5 } from "child_process";
35534
+ import { spawn as spawn6 } from "child_process";
35195
35535
  var COMPOSE_TIMEOUT_MS = 18e4;
35196
35536
  function buildPrompt2(req) {
35197
35537
  const focus = req.prompt ? `
@@ -35217,7 +35557,7 @@ function runOnce(cmd, profile, prompt, cwd2, extraEnv) {
35217
35557
  ...extraEnv,
35218
35558
  ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
35219
35559
  };
35220
- 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 });
35221
35561
  let out = "";
35222
35562
  let err = "";
35223
35563
  const timer = setTimeout(() => {
@@ -35265,10 +35605,10 @@ async function runKnowledgeCompose(req, ai, physicRoot) {
35265
35605
  // src/core/autonomous.ts
35266
35606
  import { readFile as readFile3, writeFile as writeFile2, chmod } from "fs/promises";
35267
35607
  import { readFileSync as readFileSync13 } from "fs";
35268
- import { execFile as execFile3, spawn as spawn6 } from "child_process";
35608
+ import { execFile as execFile3, spawn as spawn7 } from "child_process";
35269
35609
  import { join as join13 } from "path";
35270
35610
  import { promisify as promisify3 } from "util";
35271
- var run2 = promisify3(execFile3);
35611
+ var run3 = promisify3(execFile3);
35272
35612
  var SETTINGS_REL = ".claude/.autonomous.settings.json";
35273
35613
  var HISTORIES_REL = ".claude/.autonomous.histories.json";
35274
35614
  var APPROVALS_REL = ".claude/.autonomous.approvals.json";
@@ -35302,7 +35642,7 @@ function tickScript(root) {
35302
35642
  }
35303
35643
  async function crontabList() {
35304
35644
  try {
35305
- const { stdout } = await run2("crontab", ["-l"], { timeout: 1e4 });
35645
+ const { stdout } = await run3("crontab", ["-l"], { timeout: 1e4 });
35306
35646
  return stdout;
35307
35647
  } catch {
35308
35648
  return "";
@@ -35310,7 +35650,7 @@ async function crontabList() {
35310
35650
  }
35311
35651
  async function crontabSet(content) {
35312
35652
  return new Promise((resolve8) => {
35313
- const child = spawn6("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
35653
+ const child = spawn7("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
35314
35654
  child.on("error", () => resolve8(false));
35315
35655
  child.on("close", (code) => resolve8(code === 0));
35316
35656
  child.stdin.end(content.endsWith("\n") ? content : content + "\n");
@@ -36041,15 +36381,15 @@ async function buildGraph(root, mode) {
36041
36381
  // src/core/rag.ts
36042
36382
  import { readFile as readFile9, writeFile as writeFile7, statfs as statfs2, mkdir as mkdir6 } from "fs/promises";
36043
36383
  import { existsSync as existsSync10 } from "fs";
36044
- import { execFile as execFile4, spawn as spawn7 } from "child_process";
36384
+ import { execFile as execFile4, spawn as spawn8 } from "child_process";
36045
36385
  import { totalmem as totalmem2, cpus as cpus2 } from "os";
36046
36386
  import { join as join20 } from "path";
36047
36387
  import { promisify as promisify4 } from "util";
36048
- var run3 = promisify4(execFile4);
36388
+ var run4 = promisify4(execFile4);
36049
36389
  var RAG_REL = ".claude/rag";
36050
36390
  async function tryRun(cmd, args) {
36051
36391
  try {
36052
- const { stdout } = await run3(cmd, args, { timeout: 8e3 });
36392
+ const { stdout } = await run4(cmd, args, { timeout: 8e3 });
36053
36393
  return { ok: true, out: stdout.trim() };
36054
36394
  } catch {
36055
36395
  return { ok: false, out: "" };
@@ -36228,7 +36568,7 @@ touch "${RAG_REL}/.indexing"
36228
36568
  { echo "[reindex] $(date)"; "$RAG_PY" "${RAG_REL}/rag_engine.py" reindex 2>&1; } >> "${RAG_REL}/index.log" 2>&1
36229
36569
  rm -f "${RAG_REL}/.indexing"
36230
36570
  `;
36231
- const child = spawn7("bash", ["-c", script], {
36571
+ const child = spawn8("bash", ["-c", script], {
36232
36572
  cwd: root,
36233
36573
  env: { ...process.env, RAG_PY: py.bin },
36234
36574
  detached: true,
@@ -36243,7 +36583,7 @@ async function ragQuery(root, queryText, k = 8) {
36243
36583
  if (!existsSync10(join20(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
36244
36584
  await ensureEngine(root);
36245
36585
  try {
36246
- const { stdout } = await run3(
36586
+ const { stdout } = await run4(
36247
36587
  py.bin,
36248
36588
  [join20(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k, 1), 20))],
36249
36589
  { cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
@@ -36278,7 +36618,7 @@ rm -f "${RAG_REL}/.installed"
36278
36618
  } >> "${RAG_REL}/install.log" 2>&1
36279
36619
  rm -f "${RAG_REL}/.installing"
36280
36620
  `;
36281
- const child = spawn7("bash", ["-c", script], {
36621
+ const child = spawn8("bash", ["-c", script], {
36282
36622
  cwd: root,
36283
36623
  env: { ...process.env, RAG_PY: py.bin, RAG_MODEL: model },
36284
36624
  detached: true,
@@ -36293,7 +36633,7 @@ import { readFile as readFile10 } from "fs/promises";
36293
36633
  import { execFile as execFile5 } from "child_process";
36294
36634
  import { basename as basename3, dirname as dirname7, resolve as resolve5 } from "path";
36295
36635
  import { promisify as promisify5 } from "util";
36296
- var run4 = promisify5(execFile5);
36636
+ var run5 = promisify5(execFile5);
36297
36637
  async function gitDiff(path) {
36298
36638
  const target = resolve5(path);
36299
36639
  const dir2 = dirname7(target);
@@ -36305,7 +36645,7 @@ async function gitDiff(path) {
36305
36645
  }
36306
36646
  let oldContent = "";
36307
36647
  try {
36308
- const { stdout } = await run4("git", ["show", `HEAD:./${base}`], {
36648
+ const { stdout } = await run5("git", ["show", `HEAD:./${base}`], {
36309
36649
  cwd: dir2,
36310
36650
  timeout: 15e3,
36311
36651
  maxBuffer: 4 * 1024 * 1024
@@ -36319,10 +36659,10 @@ async function gitDiff(path) {
36319
36659
  // src/core/git-env.ts
36320
36660
  import { execFile as execFile6 } from "child_process";
36321
36661
  import { promisify as promisify6 } from "util";
36322
- var run5 = promisify6(execFile6);
36662
+ var run6 = promisify6(execFile6);
36323
36663
  async function hasBinary(bin, args = ["--version"]) {
36324
36664
  try {
36325
- await run5(bin, args, { timeout: 5e3 });
36665
+ await run6(bin, args, { timeout: 5e3 });
36326
36666
  return true;
36327
36667
  } catch {
36328
36668
  return false;
@@ -36335,7 +36675,7 @@ async function checkGitEnv(provider) {
36335
36675
  let account = null;
36336
36676
  if (installed) {
36337
36677
  try {
36338
- const { stdout, stderr } = await run5(bin, ["auth", "status"], { timeout: 8e3 });
36678
+ const { stdout, stderr } = await run6(bin, ["auth", "status"], { timeout: 8e3 });
36339
36679
  const out = `${stdout}
36340
36680
  ${stderr}`;
36341
36681
  authenticated = /logged in|logged into|✓/i.test(out);
@@ -36352,7 +36692,7 @@ import { execFile as execFile7 } from "child_process";
36352
36692
  import { readFile as readFile11, rm as rm3, mkdir as mkdir7 } from "fs/promises";
36353
36693
  import { join as join21 } from "path";
36354
36694
  import { promisify as promisify7 } from "util";
36355
- var run6 = promisify7(execFile7);
36695
+ var run7 = promisify7(execFile7);
36356
36696
  function keyPaths(profileDir2) {
36357
36697
  const dir2 = join21(profileDir2, ".ssh");
36358
36698
  const key = join21(dir2, "id_4pm");
@@ -36367,14 +36707,14 @@ async function readPublicKey(pub) {
36367
36707
  }
36368
36708
  async function fingerprintOf(pub) {
36369
36709
  try {
36370
- const { stdout } = await run6("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
36710
+ const { stdout } = await run7("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
36371
36711
  return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
36372
36712
  } catch {
36373
36713
  return null;
36374
36714
  }
36375
36715
  }
36376
36716
  async function configureGit(key) {
36377
- await run6(
36717
+ await run7(
36378
36718
  "git",
36379
36719
  ["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
36380
36720
  { timeout: 5e3 }
@@ -36385,7 +36725,7 @@ async function generate(profileDir2) {
36385
36725
  await mkdir7(dir2, { recursive: true });
36386
36726
  await rm3(key, { force: true });
36387
36727
  await rm3(pub, { force: true });
36388
- 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"], {
36389
36729
  timeout: 15e3
36390
36730
  });
36391
36731
  await configureGit(key);
@@ -36395,7 +36735,7 @@ async function remove(profileDir2) {
36395
36735
  const { key, pub } = keyPaths(profileDir2);
36396
36736
  await rm3(key, { force: true });
36397
36737
  await rm3(pub, { force: true });
36398
- await run6("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
36738
+ await run7("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
36399
36739
  () => void 0
36400
36740
  );
36401
36741
  return { publicKey: null, fingerprint: null };
@@ -36420,7 +36760,7 @@ import { readdir as readdir6 } from "fs/promises";
36420
36760
  import { execFile as execFile8 } from "child_process";
36421
36761
  import { resolve as resolve6, sep as sep4 } from "path";
36422
36762
  import { promisify as promisify8 } from "util";
36423
- var run7 = promisify8(execFile8);
36763
+ var run8 = promisify8(execFile8);
36424
36764
  var FS = "";
36425
36765
  var RS = "";
36426
36766
  function resolveRepo(root, repo) {
@@ -36431,7 +36771,7 @@ function resolveRepo(root, repo) {
36431
36771
  }
36432
36772
  async function git(cwd2, args) {
36433
36773
  try {
36434
- const { stdout } = await run7("git", args, {
36774
+ const { stdout } = await run8("git", args, {
36435
36775
  cwd: cwd2,
36436
36776
  timeout: 15e3,
36437
36777
  maxBuffer: 8 * 1024 * 1024
@@ -36535,11 +36875,11 @@ async function gitCommitDiff(root, repo, hash, path) {
36535
36875
  // src/core/git-commit-identity.ts
36536
36876
  import { execFile as execFile9 } from "child_process";
36537
36877
  import { promisify as promisify9 } from "util";
36538
- var run8 = promisify9(execFile9);
36878
+ var run9 = promisify9(execFile9);
36539
36879
  async function setCommitAuthor(cwd2, machineUsername) {
36540
36880
  if (!machineUsername) return;
36541
36881
  try {
36542
- 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 });
36543
36883
  } catch {
36544
36884
  }
36545
36885
  }
@@ -36550,7 +36890,7 @@ import { execFile as execFile10 } from "child_process";
36550
36890
  import { dirname as dirname8, join as join22, resolve as resolve7 } from "path";
36551
36891
  import { fileURLToPath as fileURLToPath3 } from "url";
36552
36892
  import { promisify as promisify10 } from "util";
36553
- var run9 = promisify10(execFile10);
36893
+ var run10 = promisify10(execFile10);
36554
36894
  function reposOf(spec) {
36555
36895
  const repos = spec?.repos;
36556
36896
  return Array.isArray(repos) ? repos : [];
@@ -36564,13 +36904,13 @@ async function provisionRepos(target, repos, emit, opts = {}) {
36564
36904
  if (repo.primary) {
36565
36905
  if (opts.clonePrimary && repo.url) {
36566
36906
  emit("git", `Cloning ${repo.url}\u2026`);
36567
- await run9("git", ["clone", repo.url, target], { timeout: 12e4 });
36907
+ await run10("git", ["clone", repo.url, target], { timeout: 12e4 });
36568
36908
  continue;
36569
36909
  }
36570
36910
  emit("git", "Initializing the primary repo\u2026");
36571
- await run9("git", ["init"], { cwd: target, timeout: 2e4 });
36911
+ await run10("git", ["init"], { cwd: target, timeout: 2e4 });
36572
36912
  if (repo.url) {
36573
- 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(
36574
36914
  () => void 0
36575
36915
  );
36576
36916
  }
@@ -36579,11 +36919,11 @@ async function provisionRepos(target, repos, emit, opts = {}) {
36579
36919
  const dir2 = join22(target, subDirName(repo));
36580
36920
  if (repo.url) {
36581
36921
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
36582
- await run9("git", ["clone", repo.url, dir2], { timeout: 12e4 });
36922
+ await run10("git", ["clone", repo.url, dir2], { timeout: 12e4 });
36583
36923
  } else {
36584
36924
  emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
36585
36925
  await mkdir8(dir2, { recursive: true });
36586
- await run9("git", ["init"], { cwd: dir2, timeout: 2e4 });
36926
+ await run10("git", ["init"], { cwd: dir2, timeout: 2e4 });
36587
36927
  }
36588
36928
  }
36589
36929
  }
@@ -36668,240 +37008,513 @@ async function addProject(payload, profileDir2, onProgress) {
36668
37008
  }
36669
37009
  }
36670
37010
 
36671
- // src/core/memory-compact.ts
36672
- import { spawn as spawn8 } from "child_process";
36673
- var COMPACT_TIMEOUT_MS = 6e4;
36674
- function buildPrompt3(input) {
36675
- return [
36676
- "You maintain a COMPACT running memory of an ongoing assistant conversation so it can continue",
36677
- "across sessions and accounts. Merge the latest exchange into the existing memory and return the",
36678
- `UPDATED memory. Keep it UNDER ${input.budgetChars} characters. Use terse Markdown bullets grouped`,
36679
- "as: Decisions, Constraints, Open tasks, Glossary. Keep durable facts; drop chit-chat and anything",
36680
- "now obsolete. Output ONLY the updated memory text \u2014 no preamble, no code fences.",
36681
- "",
36682
- "=== EXISTING MEMORY ===",
36683
- input.oldMemory || "(empty)",
36684
- "",
36685
- "=== LATEST USER PROMPT ===",
36686
- input.prompt,
36687
- "",
36688
- "=== LATEST ASSISTANT ANSWER ===",
36689
- input.answer
36690
- ].join("\n");
36691
- }
36692
- function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
36693
- return new Promise((resolve8, reject) => {
36694
- const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
36695
- const env3 = {
36696
- ...process.env,
36697
- ...extraEnv,
36698
- ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
36699
- };
36700
- const child = spawn8(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
36701
- let out = "";
36702
- let err = "";
36703
- const timer = setTimeout(() => {
36704
- child.kill("SIGKILL");
36705
- reject(new Error("memory compaction timed out"));
36706
- }, COMPACT_TIMEOUT_MS);
36707
- timer.unref();
36708
- child.stdout.on("data", (d) => out += d.toString());
36709
- child.stderr.on("data", (d) => err += d.toString());
36710
- child.on("error", (e) => {
36711
- clearTimeout(timer);
36712
- reject(e);
36713
- });
36714
- child.on("close", (code) => {
36715
- clearTimeout(timer);
36716
- resolve8({ code: code ?? -1, out, err });
36717
- });
36718
- child.stdin.write(prompt);
36719
- child.stdin.end();
36720
- });
36721
- }
36722
- async function runMemoryCompaction(ai, cwd2, input) {
36723
- const prompt = buildPrompt3(input);
36724
- const attempts = ai.profiles.length > 0 ? ai.profiles : [null];
36725
- for (let i = 0; i < attempts.length; i++) {
36726
- try {
36727
- const { code, out } = await runOnce2(ai.cmd, attempts[i], prompt, cwd2, ai.env);
36728
- if (code === 0 && out.trim()) {
36729
- reportToolResult(ai.cmd, true);
36730
- return out.trim().slice(0, input.budgetChars);
36731
- }
36732
- if (i === attempts.length - 1) break;
36733
- } catch {
36734
- break;
36735
- }
36736
- }
36737
- return "";
36738
- }
36739
-
36740
- // src/core/config-sync.ts
36741
- import { existsSync as existsSync11, readFileSync as readFileSync15 } from "fs";
37011
+ // src/ui/slash-commands.ts
37012
+ import { existsSync as existsSync11 } from "fs";
36742
37013
  import { join as join23 } from "path";
36743
- var SERVER_MANAGED_KEYS = [
37014
+ var CONFIG_KEYS = [
37015
+ "aiCli",
37016
+ "claudeHome",
37017
+ "codexHome",
37018
+ "antigravityHome",
36744
37019
  "physicPath",
36745
- "sessionSwitchPct",
36746
- "perPromptTokenLimit",
36747
- // Project AI-run timeout override mirror (ADR-0243) — the ws_token refreshes it; a Worker-config
36748
- // edit must not fight it. NOTE: the machine-user's own `aiRunTimeoutSec` is NOT here (operator-editable).
36749
- "projectAiRunTimeoutSec",
36750
- // Project idle auto-clear override mirror (ADR-0244) — same rule; the machine-user's own
36751
- // `autoClearIdleMinutes` is operator-editable and NOT listed here.
36752
- "projectAutoClearIdleMinutes",
36753
- // Project shared-AI-memory override mirrors (ADR-0245) — the machine-user's own `aiMemoryEnabled`
36754
- // / `aiMemoryBudgetChars` are operator-editable and NOT listed here.
36755
- "projectAiMemoryMode",
36756
- "projectAiMemoryBudgetChars"
36757
- ];
36758
- function readConfigText(profileDir2) {
36759
- const path = join23(profileDir2, "config.json");
36760
- if (existsSync11(path)) {
36761
- try {
36762
- return readFileSync15(path, "utf8");
36763
- } catch {
37020
+ "commandHistoryUploadMinutes",
37021
+ "autoUpdate",
37022
+ "aiEnv"
37023
+ ];
37024
+ function configPath(profileDir2) {
37025
+ return join23(profileDir2, "config.json");
37026
+ }
37027
+ function parseConfigAssignment(key, valueTokens) {
37028
+ const value = valueTokens.join(" ").trim();
37029
+ switch (key) {
37030
+ case "aiCli":
37031
+ case "physicPath":
37032
+ if (!value) return { error: `Missing value for "${key}".` };
37033
+ return { patch: { [key]: value } };
37034
+ case "claudeHome":
37035
+ case "codexHome":
37036
+ case "antigravityHome": {
37037
+ const dirs = valueTokens.filter((t) => t.trim());
37038
+ if (dirs.length === 0) return { error: `Missing value for "${key}".` };
37039
+ return { patch: { [key]: dirs.map((profile) => ({ profile, model: "" })) } };
37040
+ }
37041
+ case "commandHistoryUploadMinutes": {
37042
+ const n = Number(value);
37043
+ if (!Number.isFinite(n) || n <= 0) return { error: `"${value}" is not a positive number.` };
37044
+ return { patch: { commandHistoryUploadMinutes: n } };
36764
37045
  }
37046
+ case "autoUpdate":
37047
+ if (value !== "true" && value !== "false") return { error: `Use true|false, not "${value}".` };
37048
+ return { patch: { autoUpdate: value === "true" } };
37049
+ case "aiEnv":
37050
+ try {
37051
+ return { patch: { aiEnv: JSON.parse(value) } };
37052
+ } catch {
37053
+ return { error: `aiEnv must be JSON, e.g. {"KEY":"val"}.` };
37054
+ }
37055
+ default:
37056
+ return { error: `Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.` };
36765
37057
  }
36766
- return JSON.stringify(defaultProfileConfig(), null, 2);
36767
37058
  }
36768
- function applyConfigText(profileDir2, text) {
36769
- let parsed;
36770
- try {
36771
- parsed = JSON.parse(text);
36772
- } catch (err) {
36773
- return { ok: false, error: `Invalid JSON: ${err.message}` };
37059
+ function runConfig(ctx) {
37060
+ const [sub, ...rest] = ctx.args;
37061
+ const dir2 = ctx.info.profileDir;
37062
+ const path = configPath(dir2);
37063
+ if (!sub || sub === "show") {
37064
+ ctx.print(`config: ${path}`);
37065
+ const json = JSON.stringify(readProfileConfig(dir2), null, 2);
37066
+ for (const line of json.split("\n")) ctx.print(line);
37067
+ return;
36774
37068
  }
36775
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
36776
- return { ok: false, error: "config.json must be a JSON object." };
37069
+ if (sub === "init") {
37070
+ const writeDefaults = (verb) => {
37071
+ overwriteProfileConfig(dir2, defaultProfileConfig());
37072
+ ctx.print(`\u2714 ${verb} default config: ${path}`);
37073
+ ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
37074
+ };
37075
+ if (existsSync11(path)) {
37076
+ ctx.confirm(
37077
+ `config exists: ${path} \u2014 replace it with defaults? (y/N)`,
37078
+ () => writeDefaults("replaced with")
37079
+ );
37080
+ return;
37081
+ }
37082
+ writeDefaults("wrote");
37083
+ return;
36777
37084
  }
36778
- const next = { ...parsed };
36779
- const current = readProfileConfig(profileDir2);
36780
- for (const key of SERVER_MANAGED_KEYS) {
36781
- if (current[key] !== void 0) next[key] = current[key];
36782
- else delete next[key];
37085
+ if (sub === "set") {
37086
+ const [key, ...valueTokens] = rest;
37087
+ if (!key) {
37088
+ ctx.print("usage: /config set <key> <value>", "error");
37089
+ return;
37090
+ }
37091
+ const result = parseConfigAssignment(key, valueTokens);
37092
+ if ("error" in result) {
37093
+ ctx.print(result.error, "error");
37094
+ return;
37095
+ }
37096
+ const merged = writeProfileConfig(dir2, result.patch);
37097
+ ctx.print(`\u2714 ${key} = ${JSON.stringify(merged[key])} (applies to the next prompt)`);
37098
+ return;
36783
37099
  }
36784
- overwriteProfileConfig(profileDir2, next);
36785
- return { ok: true };
37100
+ if (sub === "delete" || sub === "unset") {
37101
+ const key = rest[0];
37102
+ if (!key) {
37103
+ ctx.print("usage: /config delete <key>", "error");
37104
+ return;
37105
+ }
37106
+ if (!CONFIG_KEYS.includes(key)) {
37107
+ ctx.print(`Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.`, "error");
37108
+ return;
37109
+ }
37110
+ deleteProfileConfigKey(dir2, key);
37111
+ ctx.print(`\u2714 deleted ${key}`);
37112
+ return;
37113
+ }
37114
+ ctx.print(`Unknown /config subcommand "${sub}". Use: show | init | set | delete.`, "error");
36786
37115
  }
36787
-
36788
- // src/core/worker-tools.ts
36789
- import { spawn as spawn9 } from "child_process";
36790
- var NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
36791
- function run10(cmd, args, onLine, timeoutMs = 18e4) {
36792
- return new Promise((resolve8) => {
36793
- let out = "";
36794
- let buf = "";
36795
- let child = null;
36796
- const done = (code) => {
36797
- clearTimeout(timer);
36798
- if (onLine && buf.trim()) onLine(buf.trim());
36799
- resolve8({ code, out });
36800
- };
36801
- const timer = setTimeout(() => {
36802
- child?.kill();
36803
- if (onLine) onLine(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
36804
- done(124);
36805
- }, timeoutMs);
36806
- timer.unref?.();
36807
- const feed = (d) => {
36808
- const text = d.toString("utf8");
36809
- out += text;
36810
- if (!onLine) return;
36811
- buf += text;
36812
- let nl;
36813
- while ((nl = buf.indexOf("\n")) >= 0) {
36814
- const line = buf.slice(0, nl).replace(/\r$/, "");
36815
- buf = buf.slice(nl + 1);
36816
- onLine(line);
36817
- }
36818
- };
37116
+ function fmtDuration(totalSec) {
37117
+ const h = Math.floor(totalSec / 3600);
37118
+ const m = Math.floor(totalSec % 3600 / 60);
37119
+ const s = totalSec % 60;
37120
+ const pad = (n) => String(n).padStart(2, "0");
37121
+ return h > 0 ? `${h}h ${pad(m)}m ${pad(s)}s` : `${m}m ${pad(s)}s`;
37122
+ }
37123
+ function commandLine(cmd, args) {
37124
+ return `${cmd} ${args.join(" ")}`.trim();
37125
+ }
37126
+ function runHistory(ctx) {
37127
+ const n = Math.max(1, Math.min(50, Number(ctx.args[0]) || 15));
37128
+ const all = getCommandHistory();
37129
+ if (all.length === 0) {
37130
+ ctx.print("No commands run yet.");
37131
+ return;
37132
+ }
37133
+ const recent = all.slice(-n).reverse();
37134
+ ctx.print(`Last ${recent.length} of ${all.length} commands (newest first) \xB7 /output <n> to view:`);
37135
+ recent.forEach((e, i) => {
37136
+ const time = new Date(e.startedAt).toLocaleTimeString();
37137
+ const state = e.status === "done" ? "\u2713" : e.status === "failed" ? `\u2717 exit ${e.exitCode}` : "\xB7 running";
37138
+ ctx.print(` [${i + 1}] ${time} ${commandLine(e.cmd, e.args)} \xB7 ${state}`);
37139
+ });
37140
+ }
37141
+ function runOutput(ctx) {
37142
+ const all = getCommandHistory();
37143
+ const n = Number(ctx.args[0]);
37144
+ if (!Number.isInteger(n) || n < 1 || n > all.length) {
37145
+ ctx.print("usage: /output <n> (n = index from /history)", "error");
37146
+ return;
37147
+ }
37148
+ const entry = all.slice().reverse()[n - 1];
37149
+ const state = entry.exitCode != null ? ` (exit ${entry.exitCode})` : "";
37150
+ ctx.print(`\u2500\u2500 output [${n}] ${commandLine(entry.cmd, entry.args)} \xB7 ${entry.status}${state} \u2500\u2500`);
37151
+ const out = readCommandOutput(entry.commandId);
37152
+ if (out == null) {
37153
+ ctx.print("(no output stored \u2014 it ran in a previous session or produced none)", "warn");
37154
+ return;
37155
+ }
37156
+ for (const line of out.replace(/\n$/, "").split("\n")) ctx.print(line);
37157
+ }
37158
+ function runLogs(ctx) {
37159
+ const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
37160
+ const dir2 = join23(ctx.info.profileDir, "logs");
37161
+ const lines = readRecentLogLines(dir2, n);
37162
+ if (lines.length === 0) {
37163
+ ctx.print("No log lines yet.");
37164
+ return;
37165
+ }
37166
+ ctx.print(`Last ${lines.length} log lines (${dir2}):`);
37167
+ for (const raw of lines) {
36819
37168
  try {
36820
- child = spawn9(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
36821
- child.stdout?.on("data", feed);
36822
- child.stderr?.on("data", feed);
36823
- child.on("exit", (code) => done(code ?? 0));
36824
- child.on("error", () => done(127));
37169
+ const j = JSON.parse(raw);
37170
+ const time = String(j.timestamp ?? "").slice(11, 19);
37171
+ const level = String(j.level ?? "").padEnd(5);
37172
+ const { timestamp, level: _l, version, profile, scope, event, ...fields } = j;
37173
+ void timestamp;
37174
+ void _l;
37175
+ void version;
37176
+ void profile;
37177
+ void scope;
37178
+ const extra = Object.keys(fields).length ? " " + JSON.stringify(fields) : "";
37179
+ ctx.print(` ${time} ${level} ${String(event ?? "")}${extra}`);
36825
37180
  } catch {
36826
- done(127);
37181
+ ctx.print(` ${raw}`);
37182
+ }
37183
+ }
37184
+ }
37185
+ function runWhoami(ctx) {
37186
+ ctx.print("whoami: fetching from the server\u2026");
37187
+ void ctx.info.whoami().then((w) => {
37188
+ if (!w) {
37189
+ ctx.print("whoami: unavailable (server unreachable or link invalid)", "warn");
37190
+ return;
36827
37191
  }
37192
+ ctx.print(`account: ${w.username} \xB7 scope: ${w.scope} \xB7 roles: ${w.roles.join(", ") || "\u2014"}`);
37193
+ ctx.print(`teams: ${w.teams.length ? w.teams.map((t) => t.name).join(", ") : "(none)"}`);
37194
+ ctx.print(`projects: ${w.projects.length ? w.projects.map((p) => p.name).join(", ") : "(none)"}`);
36828
37195
  });
36829
37196
  }
36830
- async function detectOne(cmd, versionArg) {
36831
- const { code, out } = await run10(cmd, [versionArg], void 0, 15e3);
36832
- if (code !== 0) return null;
36833
- const first = out.split("\n").map((l) => l.trim()).find(Boolean);
36834
- return first ?? "";
37197
+ function aiProfileLabel(c) {
37198
+ return c.label?.trim() || profileDisplayLabel(resolveHomePath(c.profile));
36835
37199
  }
36836
- async function listExtras() {
36837
- const { out } = await run10("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
36838
- if (!out.trim()) return [];
36839
- let deps = {};
36840
- try {
36841
- deps = JSON.parse(out).dependencies ?? {};
36842
- } catch {
36843
- return [];
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;
36844
37231
  }
36845
- const catalogPkgs = new Set(WORKER_TOOL_BUNDLED_GLOBALS);
36846
- for (const t of WORKER_TOOL_CATALOG) {
36847
- catalogPkgs.add(t.id);
36848
- if (t.installPackage) catalogPkgs.add(t.installPackage);
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;
36849
37238
  }
36850
- return Object.entries(deps).filter(([name]) => !catalogPkgs.has(name)).map(([name, info]) => ({
36851
- id: name,
36852
- label: name,
36853
- category: "runtime",
36854
- installed: true,
36855
- version: info.version ?? null,
36856
- installable: true
36857
- }));
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");
36858
37260
  }
36859
- async function detectWorkerTools() {
36860
- const catalog = await Promise.all(
36861
- WORKER_TOOL_CATALOG.map(async (t) => {
36862
- const version = await detectOne(t.id, t.versionArg);
36863
- return {
36864
- id: t.id,
36865
- label: t.label,
36866
- category: t.category,
36867
- installed: version !== null,
36868
- version,
36869
- installable: t.installable
36870
- };
36871
- })
36872
- );
36873
- const extras = await listExtras().catch(() => []);
36874
- return { catalog, extras };
37261
+ function runFold(ctx, action) {
37262
+ if (ctx.maxBlock === 0) {
37263
+ ctx.print("No collapsible blocks on screen.", "warn");
37264
+ return;
37265
+ }
37266
+ const n = ctx.args[0] != null ? Number(ctx.args[0]) : ctx.maxBlock;
37267
+ if (!Number.isInteger(n) || n < 1 || n > ctx.maxBlock) {
37268
+ ctx.print(`usage: /${action} [1-${ctx.maxBlock}] (no number \u21D2 the newest block)`, "error");
37269
+ return;
37270
+ }
37271
+ if (action === "expand") ctx.expand(n);
37272
+ else ctx.collapse(n);
36875
37273
  }
36876
- function resolvePackage(name, op) {
36877
- const entry = WORKER_TOOL_CATALOG.find((t) => t.id === name);
36878
- if (entry) {
36879
- if (!entry.installable) {
36880
- return { error: `"${name}" is a prerequisite and cannot be ${op}ed from here.` };
37274
+ var COMMANDS = [
37275
+ {
37276
+ name: "help",
37277
+ usage: "/help",
37278
+ description: "List slash commands",
37279
+ run: (ctx) => {
37280
+ ctx.print("Slash commands:");
37281
+ for (const cmd of COMMANDS) ctx.print(` ${cmd.usage.padEnd(28)} ${cmd.description}`);
36881
37282
  }
36882
- return { pkg: entry.installPackage ?? name };
37283
+ },
37284
+ {
37285
+ name: "version",
37286
+ usage: "/version",
37287
+ description: "Show the cli version",
37288
+ run: (ctx) => ctx.print(`4PM CLI v${ctx.info.version} \xB7 profile ${ctx.info.profile}`)
37289
+ },
37290
+ {
37291
+ name: "status",
37292
+ usage: "/status",
37293
+ description: "Show connection + profile + session status",
37294
+ run: (ctx) => {
37295
+ const s = ctx.live;
37296
+ const uptime = fmtDuration(Math.max(0, Math.floor((Date.now() - s.startedAt) / 1e3)));
37297
+ ctx.print(`connection: ${s.status}`);
37298
+ ctx.print(`scope: ${s.scope}`);
37299
+ ctx.print(`worker: ${s.worker ?? "(unknown)"}`);
37300
+ if (s.project) ctx.print(`project: ${s.project}`);
37301
+ ctx.print(`profile: ${ctx.info.profile}`);
37302
+ ctx.print(`server: ${ctx.info.serverUrl}`);
37303
+ ctx.print(`aiCli: ${ctx.info.aiCli}${s.activeProfile ? ` \xB7 profile ${s.activeProfile}` : ""}`);
37304
+ ctx.print(`serving: ${ctx.info.physicPath ?? "(none)"}`);
37305
+ ctx.print(`uptime: ${uptime} \xB7 commands: ${getCommandHistory().length}`);
37306
+ }
37307
+ },
37308
+ {
37309
+ name: "history",
37310
+ usage: "/history [N]",
37311
+ description: "List the last N executed commands (+ time)",
37312
+ run: runHistory
37313
+ },
37314
+ {
37315
+ name: "output",
37316
+ usage: "/output <n>",
37317
+ description: "Replay a command's output (n from /history)",
37318
+ run: runOutput
37319
+ },
37320
+ {
37321
+ name: "logs",
37322
+ usage: "/logs [N]",
37323
+ description: "Tail the profile's structured log",
37324
+ run: runLogs
37325
+ },
37326
+ {
37327
+ name: "expand",
37328
+ usage: "/expand [N]",
37329
+ description: "Expand a collapsed \u25B8[N] block (default: newest); pretty-prints json/code",
37330
+ run: (ctx) => runFold(ctx, "expand")
37331
+ },
37332
+ {
37333
+ name: "collapse",
37334
+ usage: "/collapse [N]",
37335
+ description: "Collapse an expanded \u25B8[N] block (default: newest)",
37336
+ run: (ctx) => runFold(ctx, "collapse")
37337
+ },
37338
+ {
37339
+ name: "reconnect",
37340
+ usage: "/reconnect",
37341
+ description: "Reconnect to the server now (skip the backoff)",
37342
+ run: (ctx) => ctx.reconnect()
37343
+ },
37344
+ {
37345
+ name: "whoami",
37346
+ usage: "/whoami",
37347
+ description: "Show this machine account + its teams & projects",
37348
+ run: runWhoami
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
+ },
37356
+ {
37357
+ name: "config",
37358
+ usage: "/config [show|init|set <k> <v>|delete <k>]",
37359
+ description: "View / init / update / delete profile config",
37360
+ run: runConfig
37361
+ },
37362
+ {
37363
+ name: "claude-cmd",
37364
+ usage: "/claude-cmd /context",
37365
+ description: "Run an AI-CLI slash command (e.g. /context, /usage, /status)",
37366
+ run: (ctx) => {
37367
+ const rest = ctx.args.join(" ").trim();
37368
+ if (!rest) {
37369
+ ctx.print("usage: /claude-cmd /context (forwards to the AI CLI)", "error");
37370
+ return;
37371
+ }
37372
+ ctx.submitAi(rest);
37373
+ }
37374
+ },
37375
+ {
37376
+ name: "clear",
37377
+ usage: "/clear",
37378
+ description: "Clear the transcript",
37379
+ run: (ctx) => ctx.clear()
37380
+ },
37381
+ {
37382
+ name: "quit",
37383
+ usage: "/quit",
37384
+ description: "Quit the cli (also /exit, Ctrl+C)",
37385
+ run: (ctx) => ctx.quit()
36883
37386
  }
36884
- if (WORKER_TOOL_PREREQUISITE_IDS.includes(name)) {
36885
- return { error: `"${name}" is a prerequisite and cannot be ${op}ed from here.` };
37387
+ ];
37388
+ var ALIASES = { exit: "quit" };
37389
+ var SLASH_COMMANDS = COMMANDS.map((c) => ({ name: c.name, usage: c.usage, description: c.description }));
37390
+ function runSlashCommand(line, base) {
37391
+ const tokens = line.replace(/^\//, "").trim().split(/\s+/);
37392
+ const rawName = tokens[0] ?? "";
37393
+ const name = ALIASES[rawName] ?? rawName;
37394
+ const command = COMMANDS.find((c) => c.name === name);
37395
+ const ctx = { ...base, args: tokens.slice(1) };
37396
+ if (!command) {
37397
+ ctx.print(`Unknown command: /${rawName} \u2014 try /help`, "error");
37398
+ return;
36886
37399
  }
36887
- if (WORKER_TOOL_BUNDLED_GLOBALS.includes(name)) {
36888
- return { error: `"${name}" ships with Node and cannot be ${op}ed from here.` };
37400
+ command.run(ctx);
37401
+ }
37402
+
37403
+ // src/core/memory-compact.ts
37404
+ import { spawn as spawn9 } from "child_process";
37405
+ var COMPACT_TIMEOUT_MS = 6e4;
37406
+ function buildPrompt3(input) {
37407
+ return [
37408
+ "You maintain a COMPACT running memory of an ongoing assistant conversation so it can continue",
37409
+ "across sessions and accounts. Merge the latest exchange into the existing memory and return the",
37410
+ `UPDATED memory. Keep it UNDER ${input.budgetChars} characters. Use terse Markdown bullets grouped`,
37411
+ "as: Decisions, Constraints, Open tasks, Glossary. Keep durable facts; drop chit-chat and anything",
37412
+ "now obsolete. Output ONLY the updated memory text \u2014 no preamble, no code fences.",
37413
+ "",
37414
+ "=== EXISTING MEMORY ===",
37415
+ input.oldMemory || "(empty)",
37416
+ "",
37417
+ "=== LATEST USER PROMPT ===",
37418
+ input.prompt,
37419
+ "",
37420
+ "=== LATEST ASSISTANT ANSWER ===",
37421
+ input.answer
37422
+ ].join("\n");
37423
+ }
37424
+ function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
37425
+ return new Promise((resolve8, reject) => {
37426
+ const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
37427
+ const env3 = {
37428
+ ...process.env,
37429
+ ...extraEnv,
37430
+ ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
37431
+ };
37432
+ const child = spawn9(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
37433
+ let out = "";
37434
+ let err = "";
37435
+ const timer = setTimeout(() => {
37436
+ child.kill("SIGKILL");
37437
+ reject(new Error("memory compaction timed out"));
37438
+ }, COMPACT_TIMEOUT_MS);
37439
+ timer.unref();
37440
+ child.stdout.on("data", (d) => out += d.toString());
37441
+ child.stderr.on("data", (d) => err += d.toString());
37442
+ child.on("error", (e) => {
37443
+ clearTimeout(timer);
37444
+ reject(e);
37445
+ });
37446
+ child.on("close", (code) => {
37447
+ clearTimeout(timer);
37448
+ resolve8({ code: code ?? -1, out, err });
37449
+ });
37450
+ child.stdin.write(prompt);
37451
+ child.stdin.end();
37452
+ });
37453
+ }
37454
+ async function runMemoryCompaction(ai, cwd2, input) {
37455
+ const prompt = buildPrompt3(input);
37456
+ const attempts = ai.profiles.length > 0 ? ai.profiles : [null];
37457
+ for (let i = 0; i < attempts.length; i++) {
37458
+ try {
37459
+ const { code, out } = await runOnce2(ai.cmd, attempts[i], prompt, cwd2, ai.env);
37460
+ if (code === 0 && out.trim()) {
37461
+ reportToolResult(ai.cmd, true);
37462
+ return out.trim().slice(0, input.budgetChars);
37463
+ }
37464
+ if (i === attempts.length - 1) break;
37465
+ } catch {
37466
+ break;
37467
+ }
36889
37468
  }
36890
- if (!NPM_NAME.test(name)) return { error: `Invalid package name: "${name}".` };
36891
- return { pkg: name };
37469
+ return "";
36892
37470
  }
36893
- function opArgs(op, manager, pkg) {
36894
- if (manager === "pnpm") return [op === "install" ? "add" : "remove", "-g", pkg];
36895
- return [op === "install" ? "install" : "uninstall", "-g", pkg];
37471
+
37472
+ // src/core/config-sync.ts
37473
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
37474
+ import { join as join24 } from "path";
37475
+ var SERVER_MANAGED_KEYS = [
37476
+ "physicPath",
37477
+ "sessionSwitchPct",
37478
+ "perPromptTokenLimit",
37479
+ // Project AI-run timeout override mirror (ADR-0243) — the ws_token refreshes it; a Worker-config
37480
+ // edit must not fight it. NOTE: the machine-user's own `aiRunTimeoutSec` is NOT here (operator-editable).
37481
+ "projectAiRunTimeoutSec",
37482
+ // Project idle auto-clear override mirror (ADR-0244) — same rule; the machine-user's own
37483
+ // `autoClearIdleMinutes` is operator-editable and NOT listed here.
37484
+ "projectAutoClearIdleMinutes",
37485
+ // Project shared-AI-memory override mirrors (ADR-0245) — the machine-user's own `aiMemoryEnabled`
37486
+ // / `aiMemoryBudgetChars` are operator-editable and NOT listed here.
37487
+ "projectAiMemoryMode",
37488
+ "projectAiMemoryBudgetChars"
37489
+ ];
37490
+ function readConfigText(profileDir2) {
37491
+ const path = join24(profileDir2, "config.json");
37492
+ if (existsSync12(path)) {
37493
+ try {
37494
+ return readFileSync15(path, "utf8");
37495
+ } catch {
37496
+ }
37497
+ }
37498
+ return JSON.stringify(defaultProfileConfig(), null, 2);
36896
37499
  }
36897
- async function runWorkerToolOp(op, name, manager, onLine) {
36898
- const resolved = resolvePackage(name, op);
36899
- if (resolved.error || !resolved.pkg) {
36900
- return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
37500
+ function applyConfigText(profileDir2, text) {
37501
+ let parsed;
37502
+ try {
37503
+ parsed = JSON.parse(text);
37504
+ } catch (err) {
37505
+ return { ok: false, error: `Invalid JSON: ${err.message}` };
36901
37506
  }
36902
- onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
36903
- const { code } = await run10(manager, opArgs(op, manager, resolved.pkg), onLine);
36904
- return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
37507
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
37508
+ return { ok: false, error: "config.json must be a JSON object." };
37509
+ }
37510
+ const next = { ...parsed };
37511
+ const current = readProfileConfig(profileDir2);
37512
+ for (const key of SERVER_MANAGED_KEYS) {
37513
+ if (current[key] !== void 0) next[key] = current[key];
37514
+ else delete next[key];
37515
+ }
37516
+ overwriteProfileConfig(profileDir2, next);
37517
+ return { ok: true };
36905
37518
  }
36906
37519
 
36907
37520
  // src/core/ws-client.ts
@@ -36927,7 +37540,16 @@ var CONSOLE_WATCH_LEASE_MS = 45e3;
36927
37540
  var METRICS_SAMPLE_INTERVAL_MS = 5e3;
36928
37541
  var METRICS_WATCH_LEASE_MS = 45e3;
36929
37542
  function toDtoEntry(e) {
36930
- return { id: e.id, source: e.source, kind: e.kind, text: e.text, level: e.level, resultKind: e.resultKind };
37543
+ return {
37544
+ id: e.id,
37545
+ source: e.source,
37546
+ kind: e.kind,
37547
+ text: e.text,
37548
+ level: e.level,
37549
+ resultKind: e.resultKind,
37550
+ // Processing time on a terminal `exit` entry (ADR-0249) — the web renders it inline.
37551
+ ...e.durationMs != null ? { durationMs: e.durationMs } : {}
37552
+ };
36931
37553
  }
36932
37554
  var WsClient = class {
36933
37555
  constructor(context) {
@@ -36992,8 +37614,13 @@ var WsClient = class {
36992
37614
  reconnectEntryId = null;
36993
37615
  /** Org-configured reconnect cap (seconds) from the last ws_token — overrides env. */
36994
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";
36995
37620
  /** Env override for the reconnect cap (seconds); resolved once at construction. */
36996
37621
  envReconnectMaxSec = envReconnectMaxSec();
37622
+ /** Process start (ms) — uptime for a web-dispatched `/status` (ADR-0249). */
37623
+ startedAtMs = Date.now();
36997
37624
  credential;
36998
37625
  /** The physic project folder root this cli serves (null = no project) — fs.list is
36999
37626
  * scoped to it so the web FsPicker can only browse inward, never out. */
@@ -37174,6 +37801,9 @@ var WsClient = class {
37174
37801
  if (typeof token.reconnectMaxBackoffSec === "number") {
37175
37802
  this.orgReconnectMaxSec = token.reconnectMaxBackoffSec;
37176
37803
  }
37804
+ if (typeof token.timezone === "string" && token.timezone) {
37805
+ this.orgTimezone = token.timezone;
37806
+ }
37177
37807
  if (typeof token.autoUpdateDaily === "boolean") {
37178
37808
  this.updateScheduler.configure({
37179
37809
  autoUpdateDaily: token.autoUpdateDaily,
@@ -37351,8 +37981,8 @@ var WsClient = class {
37351
37981
  const oldRoot = this.physicFolderPath(sync.oldName);
37352
37982
  if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
37353
37983
  }
37354
- if (sync.oldName) rmSync4(join24(this.context.profileDir, sync.oldName), { recursive: true, force: true });
37355
- mkdirSync5(join24(this.context.profileDir, sync.newName), { recursive: true });
37984
+ if (sync.oldName) rmSync4(join25(this.context.profileDir, sync.oldName), { recursive: true, force: true });
37985
+ mkdirSync5(join25(this.context.profileDir, sync.newName), { recursive: true });
37356
37986
  this.physicRoot = this.physicFolderPath(sync.newName);
37357
37987
  this.bus.setProject(sync.newName);
37358
37988
  this.bus.log(`Physic project folder synced \u2192 ${sync.newName}`);
@@ -37363,7 +37993,7 @@ var WsClient = class {
37363
37993
  if (del.name) {
37364
37994
  const delRoot = this.physicFolderPath(del.name);
37365
37995
  if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
37366
- rmSync4(join24(this.context.profileDir, del.name), { recursive: true, force: true });
37996
+ rmSync4(join25(this.context.profileDir, del.name), { recursive: true, force: true });
37367
37997
  this.bus.log(`Physic project folder deleted (project removed) \u2192 ${del.name}`);
37368
37998
  }
37369
37999
  void manageSshKey("delete", this.context.profileDir).catch(() => void 0);
@@ -37384,7 +38014,11 @@ var WsClient = class {
37384
38014
  ai: dispatch.ai ?? false
37385
38015
  });
37386
38016
  if (dispatch.ai) {
37387
- void this.runAiPrompt(dispatch.cmd, dispatch.commandId, "server");
38017
+ if (dispatch.cmd.trimStart().startsWith("/")) {
38018
+ void this.runWebSlashCommand(dispatch.cmd, dispatch.commandId);
38019
+ } else {
38020
+ void this.runAiPrompt(dispatch.cmd, dispatch.commandId, "server", dispatch.aiOneShot ?? false);
38021
+ }
37388
38022
  break;
37389
38023
  }
37390
38024
  recordCommand({
@@ -37399,6 +38033,7 @@ var WsClient = class {
37399
38033
  text: `$ ${dispatch.cmd} ${dispatch.args.join(" ")}`.trimEnd()
37400
38034
  });
37401
38035
  this.bus.startBusy(dispatch.cmd);
38036
+ const cmdStartMs = Date.now();
37402
38037
  void runCommand(dispatch, (out) => {
37403
38038
  if (out.chunk) {
37404
38039
  this.bus.push({ source: "server", kind: "out", text: out.chunk });
@@ -37413,7 +38048,8 @@ var WsClient = class {
37413
38048
  source: "server",
37414
38049
  kind: "exit",
37415
38050
  text: code === 0 ? "\u2713 done" : `\u2717 failed (exit ${code})`,
37416
- level: code === 0 ? "info" : "error"
38051
+ level: code === 0 ? "info" : "error",
38052
+ durationMs: Date.now() - cmdStartMs
37417
38053
  });
37418
38054
  }
37419
38055
  });
@@ -37667,14 +38303,21 @@ var WsClient = class {
37667
38303
  break;
37668
38304
  }
37669
38305
  case WsChannels.TOOLS_LIST:
37670
- void detectWorkerTools().then(
38306
+ void detectWorkerTools(readProfileConfig(this.context.profileDir).autoUpdateTools ?? []).then(
37671
38307
  (reply) => this.send(WsChannels.TOOLS_LIST, reply, message.id)
37672
38308
  );
37673
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
+ }
37674
38316
  case WsChannels.TOOLS_INSTALL:
37675
- case WsChannels.TOOLS_UNINSTALL: {
38317
+ case WsChannels.TOOLS_UNINSTALL:
38318
+ case WsChannels.TOOLS_UPDATE: {
37676
38319
  const req = payload;
37677
- 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";
37678
38321
  this.send(message.channel, { started: true }, message.id);
37679
38322
  void runWorkerToolOp(
37680
38323
  op,
@@ -37736,7 +38379,7 @@ var WsClient = class {
37736
38379
  }
37737
38380
  case WsChannels.LOG_READ: {
37738
38381
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
37739
- const lines = readRecentLogLines(join24(this.context.profileDir, "logs"), limit);
38382
+ const lines = readRecentLogLines(join25(this.context.profileDir, "logs"), limit);
37740
38383
  this.send(WsChannels.LOG_READ, { lines }, message.id);
37741
38384
  break;
37742
38385
  }
@@ -37815,7 +38458,7 @@ var WsClient = class {
37815
38458
  * exactly like typing the prompt in the cli (tries profiles, meters real tokens) instead
37816
38459
  * of spawning the text as a raw command. Streams output to the transcript AND the server.
37817
38460
  */
37818
- async runAiPrompt(prompt, commandId, origin) {
38461
+ async runAiPrompt(prompt, commandId, origin, oneShot = false) {
37819
38462
  const config = readProfileConfig(this.context.profileDir);
37820
38463
  const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
37821
38464
  const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
@@ -37855,9 +38498,10 @@ var WsClient = class {
37855
38498
  const unified = isUnifiedConfig(config);
37856
38499
  const mode = config.aiFailoverMode ?? "remember";
37857
38500
  const workingDir = unified ? null : getWorkingProfile(this.context.profileDir, cmd);
37858
- 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;
37859
38503
  const startDir = unified ? null : this.profileUnderSessionPressure(config, cmd, workingDir);
37860
- const startCred = unified ? this.credentialUnderSessionPressure(config, workingCred) : null;
38504
+ const startCred = unified && !pinnedCred ? this.credentialUnderSessionPressure(config, workingCred) : null;
37861
38505
  if (startDir && startDir !== workingDir) {
37862
38506
  this.bus.push({
37863
38507
  source: origin,
@@ -37887,9 +38531,10 @@ ${this.aiMemory}
37887
38531
 
37888
38532
  ${guardedPrompt}`;
37889
38533
  }
37890
- const plan = planAiRun(effectivePrompt, config, hint, resume);
38534
+ const plan = planAiRun(effectivePrompt, config, hint, resume, oneShot);
37891
38535
  const markerArgs = plan.attempts[0]?.args ?? [];
37892
- 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}` });
37893
38538
  logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
37894
38539
  if (origin === "local") {
37895
38540
  this.send(WsChannels.COMMAND_ANNOUNCE, {
@@ -37903,6 +38548,7 @@ ${guardedPrompt}`;
37903
38548
  }
37904
38549
  recordCommand({ commandId, cmd: plan.cmd, args: markerArgs });
37905
38550
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
38551
+ const aiStartedMs = Date.now();
37906
38552
  this.bus.startBusy(plan.cmd);
37907
38553
  let seq = 0;
37908
38554
  let responseHeaderShown = false;
@@ -37946,7 +38592,7 @@ ${guardedPrompt}`;
37946
38592
  `, log: true });
37947
38593
  },
37948
38594
  onAttemptFail: (label, reason) => {
37949
- const text = reason === "limit" ? `profile "${label}" hit its session limit \u2014 trying next` : `profile "${label}" failed to authenticate \u2014 trying next`;
38595
+ const text = reason === "limit" ? `profile "${label}" hit its session limit \u2014 trying next` : reason === "credits" ? `profile "${label}" is out of usage credits \u2014 trying next` : `profile "${label}" failed to authenticate \u2014 trying next`;
37950
38596
  this.bus.push({ source: origin, kind: "log", text, level: "warn" });
37951
38597
  this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
37952
38598
  `, log: true });
@@ -38019,7 +38665,8 @@ ${guardedPrompt}`;
38019
38665
  source: origin,
38020
38666
  kind: "exit",
38021
38667
  text: result.exitCode === 0 ? "\u2713 ready \u2014 enter your next prompt" : `\u2717 ${plan.cmd} failed (exit ${result.exitCode}) \u2014 see the error above`,
38022
- level: result.exitCode === 0 ? "info" : "error"
38668
+ level: result.exitCode === 0 ? "info" : "error",
38669
+ durationMs: Date.now() - aiStartedMs
38023
38670
  });
38024
38671
  if (memCfg.enabled && result.exitCode === 0) {
38025
38672
  if (result.workedKey && result.sessionId) {
@@ -38029,6 +38676,116 @@ ${guardedPrompt}`;
38029
38676
  });
38030
38677
  }
38031
38678
  }
38679
+ /**
38680
+ * Run a `/…` 4pm-cli slash command dispatched from the web Console (ADR-0249). Mirrors the TUI's
38681
+ * `runSlashCommand` with a **server-origin** context that streams `print` output back over
38682
+ * command.output + console.sync, so a web user gets the same commands as an operator at the
38683
+ * machine — EXCEPT any the operator disabled via `webBlockedCommands`. `/claude-cmd <x>` forwards
38684
+ * `x` to the AI CLI (a real AI run). Fold ops (`/expand`/`/collapse`) are TUI-only — the web has
38685
+ * its own fold viewer — so they just print a hint. Interactive `confirm` (e.g. `/config init`) is
38686
+ * not supported from the web.
38687
+ */
38688
+ async runWebSlashCommand(line, commandId) {
38689
+ const origin = "server";
38690
+ const config = readProfileConfig(this.context.profileDir);
38691
+ const trimmed = line.trim();
38692
+ const startedMs = Date.now();
38693
+ this.bus.push({ source: origin, kind: "cmd", text: trimmed });
38694
+ recordCommand({ commandId, cmd: trimmed, args: [] });
38695
+ let seq = 0;
38696
+ const emit = (text) => {
38697
+ appendCommandOutput(commandId, `${text}
38698
+ `);
38699
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
38700
+ ` });
38701
+ };
38702
+ const settle = (exitCode) => {
38703
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: "", done: true, exitCode });
38704
+ finishCommand(commandId, exitCode);
38705
+ this.send(WsChannels.COMMAND_HISTORY, {
38706
+ commandId,
38707
+ cmd: trimmed,
38708
+ args: [],
38709
+ status: exitCode === 0 ? "done" : "failed",
38710
+ exitCode
38711
+ });
38712
+ this.bus.push({
38713
+ source: origin,
38714
+ kind: "exit",
38715
+ text: exitCode === 0 ? "\u2713 done" : `\u2717 failed (exit ${exitCode})`,
38716
+ level: exitCode === 0 ? "info" : "error",
38717
+ durationMs: Date.now() - startedMs
38718
+ });
38719
+ };
38720
+ const rawName = trimmed.replace(/^\//, "").trim().split(/\s+/)[0]?.toLowerCase() ?? "";
38721
+ const name = rawName === "exit" ? "quit" : rawName;
38722
+ const blocked = resolveWebBlockedCommands(config);
38723
+ if (blocked.has(name) || blocked.has(rawName)) {
38724
+ const msg = `Command /${rawName} is disabled on this worker (blocked by the machine config).`;
38725
+ this.bus.push({ source: origin, kind: "log", text: msg, level: "error" });
38726
+ emit(msg);
38727
+ settle(1);
38728
+ return;
38729
+ }
38730
+ if (name === "claude-cmd") {
38731
+ const rest = trimmed.replace(/^\/claude-cmd\s*/i, "").trim();
38732
+ if (!rest) {
38733
+ emit("usage: /claude-cmd /context (forwards to the AI CLI)");
38734
+ settle(1);
38735
+ return;
38736
+ }
38737
+ await this.runAiPrompt(rest, commandId, origin, false);
38738
+ return;
38739
+ }
38740
+ const info = {
38741
+ version: CLI_VERSION,
38742
+ scope: this.bus.scope ?? "project",
38743
+ profile: basename4(this.context.profileDir),
38744
+ profileDir: this.context.profileDir,
38745
+ serverUrl: this.context.credential.serverUrl,
38746
+ physicPath: this.physicRoot ?? config.physicPath ?? null,
38747
+ aiCli: config.aiCli || "claude",
38748
+ whoami: async () => null
38749
+ };
38750
+ runSlashCommand(trimmed, {
38751
+ info,
38752
+ print: (text, level) => {
38753
+ this.bus.push({ source: origin, kind: "log", text, level });
38754
+ emit(text);
38755
+ },
38756
+ clear: () => {
38757
+ this.bus.clear();
38758
+ this.bus.clearSession();
38759
+ },
38760
+ quit: () => {
38761
+ emit("Quit requested from the web console \u2014 the worker cli is shutting down.");
38762
+ setTimeout(() => process.exit(0), 200);
38763
+ },
38764
+ confirm: (prompt) => {
38765
+ const note = `${prompt} \u2014 interactive prompts aren't supported from the web console; run it on the machine.`;
38766
+ this.bus.push({ source: origin, kind: "log", text: note, level: "warn" });
38767
+ emit(note);
38768
+ },
38769
+ submitAi: () => {
38770
+ },
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),
38774
+ reconnect: () => this.reconnectNow(),
38775
+ expand: () => emit("Use the web console's fold viewer to expand blocks."),
38776
+ collapse: () => emit("Use the web console's fold viewer to collapse blocks."),
38777
+ maxBlock: 0,
38778
+ live: {
38779
+ status: this.bus.status,
38780
+ scope: this.bus.scope ?? "",
38781
+ worker: this.bus.worker,
38782
+ project: this.bus.project,
38783
+ activeProfile: this.bus.activeProfile,
38784
+ startedAt: this.startedAtMs
38785
+ }
38786
+ });
38787
+ settle(0);
38788
+ }
38032
38789
  /**
38033
38790
  * Fold the latest exchange into the shared AI memory (ADR-0245): a background claude compaction to a
38034
38791
  * budget-bounded summary, cached locally + written back to the server (`memory.update`). Best-effort —
@@ -38380,7 +39137,7 @@ ${guardedPrompt}`;
38380
39137
  /** Create the physic folder at an absolute path if missing (best-effort). */
38381
39138
  ensurePhysicFolderPath(folder) {
38382
39139
  try {
38383
- if (!existsSync12(folder)) {
39140
+ if (!existsSync13(folder)) {
38384
39141
  mkdirSync5(folder, { recursive: true });
38385
39142
  this.bus.log(`Created physic project folder: ${folder}`);
38386
39143
  }
@@ -38395,7 +39152,7 @@ ${guardedPrompt}`;
38395
39152
  */
38396
39153
  physicFolderPath(projectName) {
38397
39154
  const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
38398
- return safe ? join24(this.context.profileDir, safe) : null;
39155
+ return safe ? join25(this.context.profileDir, safe) : null;
38399
39156
  }
38400
39157
  /**
38401
39158
  * Report machine.status: version + the state of the physic project being served
@@ -38409,7 +39166,7 @@ ${guardedPrompt}`;
38409
39166
  projects: [],
38410
39167
  version: CLI_VERSION,
38411
39168
  physicPath,
38412
- physicPathExists: physicPath ? existsSync12(physicPath) : void 0,
39169
+ physicPathExists: physicPath ? existsSync13(physicPath) : void 0,
38413
39170
  // Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
38414
39171
  autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false
38415
39172
  });
@@ -38499,7 +39256,7 @@ ${guardedPrompt}`;
38499
39256
  */
38500
39257
  uploadLog() {
38501
39258
  try {
38502
- const upload = readLogUpload(join24(this.context.profileDir, "logs"));
39259
+ const upload = readLogUpload(join25(this.context.profileDir, "logs"));
38503
39260
  if (!upload) return;
38504
39261
  const sig = `${upload.fileName}:${upload.totalBytes}`;
38505
39262
  if (sig === this.lastLogSig) return;
@@ -38528,9 +39285,9 @@ ${guardedPrompt}`;
38528
39285
 
38529
39286
  // src/core/instance-lock.ts
38530
39287
  import { readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
38531
- import { join as join25 } from "path";
39288
+ import { join as join26 } from "path";
38532
39289
  function lockPath(profileDir2) {
38533
- return join25(profileDir2, "start.lock");
39290
+ return join26(profileDir2, "start.lock");
38534
39291
  }
38535
39292
  function isAlive(pid) {
38536
39293
  try {
@@ -38830,7 +39587,7 @@ var SessionBus = class {
38830
39587
  // src/core/control-server.ts
38831
39588
  import { createServer } from "net";
38832
39589
  import { rmSync as rmSync6 } from "fs";
38833
- import { join as join26 } from "path";
39590
+ import { join as join27 } from "path";
38834
39591
 
38835
39592
  // src/core/control-protocol.ts
38836
39593
  var CONTROL_SOCKET_FILE = "control.sock";
@@ -38859,7 +39616,7 @@ function createFrameParser() {
38859
39616
 
38860
39617
  // src/core/control-server.ts
38861
39618
  function startControlServer(bus, profileDir2, info) {
38862
- const socketPath = join26(profileDir2, CONTROL_SOCKET_FILE);
39619
+ const socketPath = join27(profileDir2, CONTROL_SOCKET_FILE);
38863
39620
  try {
38864
39621
  rmSync6(socketPath, { force: true });
38865
39622
  } catch {
@@ -44590,15 +45347,15 @@ var import_react21 = __toESM(require_react(), 1);
44590
45347
  var import_react26 = __toESM(require_react(), 1);
44591
45348
 
44592
45349
  // src/core/input-history.ts
44593
- import { existsSync as existsSync14, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
44594
- import { join as join27 } from "path";
45350
+ import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
45351
+ import { join as join28 } from "path";
44595
45352
  var MAX_HISTORY = 200;
44596
45353
  function historyPath(profileDir2) {
44597
- return join27(profileDir2, "input-history.json");
45354
+ return join28(profileDir2, "input-history.json");
44598
45355
  }
44599
45356
  function loadInputHistory(profileDir2) {
44600
45357
  const path = historyPath(profileDir2);
44601
- if (!existsSync14(path)) return [];
45358
+ if (!existsSync15(path)) return [];
44602
45359
  try {
44603
45360
  const data = JSON.parse(readFileSync18(path, "utf8"));
44604
45361
  return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
@@ -44720,12 +45477,17 @@ function textColor(line) {
44720
45477
  if (line.kind === "exit") return "green";
44721
45478
  return void 0;
44722
45479
  }
45480
+ var START_STAMP_RE = /^(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2} )/;
44723
45481
  function AiMarkerLine({ line }) {
44724
45482
  const arrow = line.kind === "aireq" ? "\u2039" : "\u203A";
44725
45483
  const idx = line.text.indexOf(arrow);
44726
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;
44727
45488
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Text, { children: [
44728
- /* @__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 }),
44729
45491
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: arrow }),
44730
45492
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text.slice(idx + 1) })
44731
45493
  ] });
@@ -44750,438 +45512,114 @@ function wrapToWidth(text, width) {
44750
45512
  line = word.trimStart();
44751
45513
  } else {
44752
45514
  line += word;
44753
- }
44754
- }
44755
- if (line) rows.push(line.trimEnd());
44756
- return rows.length > 0 ? rows : [""];
44757
- }
44758
- var DETAIL_RE = /<detail\s+title="([^"]*)">([\s\S]*?)<\/detail>/g;
44759
- function toSegments(raw) {
44760
- if (!raw.includes("<detail")) {
44761
- return raw.split("\n").map((text) => ({ text, collapsed: false }));
44762
- }
44763
- const out = [];
44764
- const re = new RegExp(DETAIL_RE);
44765
- let last = 0;
44766
- let m;
44767
- while ((m = re.exec(raw)) !== null) {
44768
- const before = raw.slice(last, m.index).replace(/\n$/, "");
44769
- if (before.length > 0) for (const p of before.split("\n")) out.push({ text: p, collapsed: false });
44770
- out.push({ text: m[1] || "details", collapsed: true, body: m[2] ?? "" });
44771
- last = m.index + m[0].length;
44772
- if (raw[last] === "\n") last += 1;
44773
- }
44774
- const rest = raw.slice(last);
44775
- if (rest.length > 0) for (const p of rest.split("\n")) out.push({ text: p, collapsed: false });
44776
- if (out.length === 0) out.push({ text: "", collapsed: false });
44777
- return out;
44778
- }
44779
- function pushCollapsible(lines, entry, opts) {
44780
- const { blockNo, title, body, expanded, firstRow, textWidth, keyBase } = opts;
44781
- const base = { source: entry.source, level: entry.level, kind: entry.kind };
44782
- lines.push({
44783
- ...base,
44784
- key: `${keyBase}:m`,
44785
- text: `${expanded ? "\u25BE" : "\u25B8"}[${blockNo}] ${title}`,
44786
- showTag: firstRow,
44787
- collapsed: true,
44788
- spacer: false,
44789
- blockIndex: blockNo
44790
- });
44791
- if (!expanded) return;
44792
- prettyResult(body).split("\n").forEach(
44793
- (seg, i) => wrapToWidth(seg, textWidth).forEach(
44794
- (row, j) => lines.push({ ...base, key: `${keyBase}:b:${i}:${j}`, text: row, showTag: false, collapsed: false, spacer: false })
44795
- )
44796
- );
44797
- }
44798
- function flattenEntries(entries2, textWidth, expanded = /* @__PURE__ */ new Set()) {
44799
- const lines = [];
44800
- let prevKey = null;
44801
- let blockNo = 0;
44802
- for (const entry of entries2) {
44803
- const blockKey = `${entry.source}|${entry.kind}`;
44804
- const newBlock = blockKey !== prevKey;
44805
- if (newBlock && lines.length > 0) {
44806
- lines.push({
44807
- key: `sp:${entry.id}`,
44808
- source: entry.source,
44809
- level: entry.level,
44810
- kind: entry.kind,
44811
- text: "",
44812
- showTag: false,
44813
- collapsed: false,
44814
- spacer: true
44815
- });
44816
- }
44817
- prevKey = blockKey;
44818
- let firstRow = newBlock;
44819
- if (entry.kind === "result") {
44820
- blockNo += 1;
44821
- const count = prettyResult(entry.text).split("\n").length;
44822
- const title = `result \xB7 ${entry.resultKind ?? "json"} \xB7 ${count} ${count === 1 ? "line" : "lines"}`;
44823
- pushCollapsible(lines, entry, { blockNo, title, body: entry.text, expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: entry.id });
44824
- continue;
44825
- }
44826
- const raw = entry.kind === "out" ? entry.text.replace(/\n$/, "") : entry.text;
44827
- toSegments(raw).forEach((seg, i) => {
44828
- if (seg.collapsed) {
44829
- blockNo += 1;
44830
- pushCollapsible(lines, entry, { blockNo, title: seg.text, body: seg.body ?? "", expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: `${entry.id}:${i}` });
44831
- firstRow = false;
44832
- return;
44833
- }
44834
- wrapToWidth(seg.text, textWidth).forEach((row, j) => {
44835
- lines.push({
44836
- key: `${entry.id}:${i}:${j}`,
44837
- source: entry.source,
44838
- level: entry.level,
44839
- kind: entry.kind,
44840
- text: row,
44841
- showTag: firstRow,
44842
- collapsed: false,
44843
- spacer: false
44844
- });
44845
- firstRow = false;
44846
- });
44847
- });
44848
- }
44849
- return lines;
44850
- }
44851
- function TranscriptLine({ line }) {
44852
- const tag = sourceTag(line.source);
44853
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Box_default, { flexDirection: "row", children: [
44854
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { width: 7, marginRight: 1, children: line.showTag ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: tag.color, dimColor: line.source === "system", children: tag.text }) : null }),
44855
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { flexGrow: 1, children: line.spacer ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: " " }) : line.collapsed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, wrap: "truncate-end", children: line.text || " " }) : line.kind === "aireq" || line.kind === "aires" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AiMarkerLine, { line }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: textColor(line), wrap: "truncate-end", children: line.text || " " }) })
44856
- ] });
44857
- }
44858
-
44859
- // src/ui/command-input.tsx
44860
- var import_react24 = __toESM(require_react(), 1);
44861
-
44862
- // src/ui/slash-commands.ts
44863
- import { existsSync as existsSync15 } from "fs";
44864
- import { join as join28 } from "path";
44865
- var CONFIG_KEYS = [
44866
- "aiCli",
44867
- "claudeHome",
44868
- "codexHome",
44869
- "antigravityHome",
44870
- "physicPath",
44871
- "commandHistoryUploadMinutes",
44872
- "autoUpdate",
44873
- "aiEnv"
44874
- ];
44875
- function configPath(profileDir2) {
44876
- return join28(profileDir2, "config.json");
44877
- }
44878
- function parseConfigAssignment(key, valueTokens) {
44879
- const value = valueTokens.join(" ").trim();
44880
- switch (key) {
44881
- case "aiCli":
44882
- case "physicPath":
44883
- if (!value) return { error: `Missing value for "${key}".` };
44884
- return { patch: { [key]: value } };
44885
- case "claudeHome":
44886
- case "codexHome":
44887
- case "antigravityHome": {
44888
- const dirs = valueTokens.filter((t) => t.trim());
44889
- if (dirs.length === 0) return { error: `Missing value for "${key}".` };
44890
- return { patch: { [key]: dirs.map((profile) => ({ profile, model: "" })) } };
44891
- }
44892
- case "commandHistoryUploadMinutes": {
44893
- const n = Number(value);
44894
- if (!Number.isFinite(n) || n <= 0) return { error: `"${value}" is not a positive number.` };
44895
- return { patch: { commandHistoryUploadMinutes: n } };
44896
- }
44897
- case "autoUpdate":
44898
- if (value !== "true" && value !== "false") return { error: `Use true|false, not "${value}".` };
44899
- return { patch: { autoUpdate: value === "true" } };
44900
- case "aiEnv":
44901
- try {
44902
- return { patch: { aiEnv: JSON.parse(value) } };
44903
- } catch {
44904
- return { error: `aiEnv must be JSON, e.g. {"KEY":"val"}.` };
44905
- }
44906
- default:
44907
- return { error: `Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.` };
44908
- }
44909
- }
44910
- function runConfig(ctx) {
44911
- const [sub, ...rest] = ctx.args;
44912
- const dir2 = ctx.info.profileDir;
44913
- const path = configPath(dir2);
44914
- if (!sub || sub === "show") {
44915
- ctx.print(`config: ${path}`);
44916
- const json = JSON.stringify(readProfileConfig(dir2), null, 2);
44917
- for (const line of json.split("\n")) ctx.print(line);
44918
- return;
44919
- }
44920
- if (sub === "init") {
44921
- const writeDefaults = (verb) => {
44922
- overwriteProfileConfig(dir2, defaultProfileConfig());
44923
- ctx.print(`\u2714 ${verb} default config: ${path}`);
44924
- ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
44925
- };
44926
- if (existsSync15(path)) {
44927
- ctx.confirm(
44928
- `config exists: ${path} \u2014 replace it with defaults? (y/N)`,
44929
- () => writeDefaults("replaced with")
44930
- );
44931
- return;
44932
- }
44933
- writeDefaults("wrote");
44934
- return;
44935
- }
44936
- if (sub === "set") {
44937
- const [key, ...valueTokens] = rest;
44938
- if (!key) {
44939
- ctx.print("usage: /config set <key> <value>", "error");
44940
- return;
44941
- }
44942
- const result = parseConfigAssignment(key, valueTokens);
44943
- if ("error" in result) {
44944
- ctx.print(result.error, "error");
44945
- return;
44946
- }
44947
- const merged = writeProfileConfig(dir2, result.patch);
44948
- ctx.print(`\u2714 ${key} = ${JSON.stringify(merged[key])} (applies to the next prompt)`);
44949
- return;
44950
- }
44951
- if (sub === "delete" || sub === "unset") {
44952
- const key = rest[0];
44953
- if (!key) {
44954
- ctx.print("usage: /config delete <key>", "error");
44955
- return;
44956
- }
44957
- if (!CONFIG_KEYS.includes(key)) {
44958
- ctx.print(`Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.`, "error");
44959
- return;
44960
- }
44961
- deleteProfileConfigKey(dir2, key);
44962
- ctx.print(`\u2714 deleted ${key}`);
44963
- return;
44964
- }
44965
- ctx.print(`Unknown /config subcommand "${sub}". Use: show | init | set | delete.`, "error");
44966
- }
44967
- function fmtDuration(totalSec) {
44968
- const h = Math.floor(totalSec / 3600);
44969
- const m = Math.floor(totalSec % 3600 / 60);
44970
- const s = totalSec % 60;
44971
- const pad = (n) => String(n).padStart(2, "0");
44972
- return h > 0 ? `${h}h ${pad(m)}m ${pad(s)}s` : `${m}m ${pad(s)}s`;
44973
- }
44974
- function commandLine(cmd, args) {
44975
- return `${cmd} ${args.join(" ")}`.trim();
44976
- }
44977
- function runHistory(ctx) {
44978
- const n = Math.max(1, Math.min(50, Number(ctx.args[0]) || 15));
44979
- const all = getCommandHistory();
44980
- if (all.length === 0) {
44981
- ctx.print("No commands run yet.");
44982
- return;
44983
- }
44984
- const recent = all.slice(-n).reverse();
44985
- ctx.print(`Last ${recent.length} of ${all.length} commands (newest first) \xB7 /output <n> to view:`);
44986
- recent.forEach((e, i) => {
44987
- const time = new Date(e.startedAt).toLocaleTimeString();
44988
- const state = e.status === "done" ? "\u2713" : e.status === "failed" ? `\u2717 exit ${e.exitCode}` : "\xB7 running";
44989
- ctx.print(` [${i + 1}] ${time} ${commandLine(e.cmd, e.args)} \xB7 ${state}`);
44990
- });
44991
- }
44992
- function runOutput(ctx) {
44993
- const all = getCommandHistory();
44994
- const n = Number(ctx.args[0]);
44995
- if (!Number.isInteger(n) || n < 1 || n > all.length) {
44996
- ctx.print("usage: /output <n> (n = index from /history)", "error");
44997
- return;
44998
- }
44999
- const entry = all.slice().reverse()[n - 1];
45000
- const state = entry.exitCode != null ? ` (exit ${entry.exitCode})` : "";
45001
- ctx.print(`\u2500\u2500 output [${n}] ${commandLine(entry.cmd, entry.args)} \xB7 ${entry.status}${state} \u2500\u2500`);
45002
- const out = readCommandOutput(entry.commandId);
45003
- if (out == null) {
45004
- ctx.print("(no output stored \u2014 it ran in a previous session or produced none)", "warn");
45005
- return;
45515
+ }
45006
45516
  }
45007
- for (const line of out.replace(/\n$/, "").split("\n")) ctx.print(line);
45517
+ if (line) rows.push(line.trimEnd());
45518
+ return rows.length > 0 ? rows : [""];
45008
45519
  }
45009
- function runLogs(ctx) {
45010
- const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
45011
- const dir2 = join28(ctx.info.profileDir, "logs");
45012
- const lines = readRecentLogLines(dir2, n);
45013
- if (lines.length === 0) {
45014
- ctx.print("No log lines yet.");
45015
- return;
45520
+ var DETAIL_RE = /<detail\s+title="([^"]*)">([\s\S]*?)<\/detail>/g;
45521
+ function toSegments(raw) {
45522
+ if (!raw.includes("<detail")) {
45523
+ return raw.split("\n").map((text) => ({ text, collapsed: false }));
45016
45524
  }
45017
- ctx.print(`Last ${lines.length} log lines (${dir2}):`);
45018
- for (const raw of lines) {
45019
- try {
45020
- const j = JSON.parse(raw);
45021
- const time = String(j.timestamp ?? "").slice(11, 19);
45022
- const level = String(j.level ?? "").padEnd(5);
45023
- const { timestamp, level: _l, version, profile, scope, event, ...fields } = j;
45024
- void timestamp;
45025
- void _l;
45026
- void version;
45027
- void profile;
45028
- void scope;
45029
- const extra = Object.keys(fields).length ? " " + JSON.stringify(fields) : "";
45030
- ctx.print(` ${time} ${level} ${String(event ?? "")}${extra}`);
45031
- } catch {
45032
- ctx.print(` ${raw}`);
45033
- }
45525
+ const out = [];
45526
+ const re = new RegExp(DETAIL_RE);
45527
+ let last = 0;
45528
+ let m;
45529
+ while ((m = re.exec(raw)) !== null) {
45530
+ const before = raw.slice(last, m.index).replace(/\n$/, "");
45531
+ if (before.length > 0) for (const p of before.split("\n")) out.push({ text: p, collapsed: false });
45532
+ out.push({ text: m[1] || "details", collapsed: true, body: m[2] ?? "" });
45533
+ last = m.index + m[0].length;
45534
+ if (raw[last] === "\n") last += 1;
45034
45535
  }
45536
+ const rest = raw.slice(last);
45537
+ if (rest.length > 0) for (const p of rest.split("\n")) out.push({ text: p, collapsed: false });
45538
+ if (out.length === 0) out.push({ text: "", collapsed: false });
45539
+ return out;
45035
45540
  }
45036
- function runWhoami(ctx) {
45037
- ctx.print("whoami: fetching from the server\u2026");
45038
- void ctx.info.whoami().then((w) => {
45039
- if (!w) {
45040
- ctx.print("whoami: unavailable (server unreachable or link invalid)", "warn");
45041
- return;
45042
- }
45043
- ctx.print(`account: ${w.username} \xB7 scope: ${w.scope} \xB7 roles: ${w.roles.join(", ") || "\u2014"}`);
45044
- ctx.print(`teams: ${w.teams.length ? w.teams.map((t) => t.name).join(", ") : "(none)"}`);
45045
- ctx.print(`projects: ${w.projects.length ? w.projects.map((p) => p.name).join(", ") : "(none)"}`);
45541
+ function pushCollapsible(lines, entry, opts) {
45542
+ const { blockNo, title, body, expanded, firstRow, textWidth, keyBase } = opts;
45543
+ const base = { source: entry.source, level: entry.level, kind: entry.kind };
45544
+ lines.push({
45545
+ ...base,
45546
+ key: `${keyBase}:m`,
45547
+ text: `${expanded ? "\u25BE" : "\u25B8"}[${blockNo}] ${title}`,
45548
+ showTag: firstRow,
45549
+ collapsed: true,
45550
+ spacer: false,
45551
+ blockIndex: blockNo
45046
45552
  });
45553
+ if (!expanded) return;
45554
+ prettyResult(body).split("\n").forEach(
45555
+ (seg, i) => wrapToWidth(seg, textWidth).forEach(
45556
+ (row, j) => lines.push({ ...base, key: `${keyBase}:b:${i}:${j}`, text: row, showTag: false, collapsed: false, spacer: false })
45557
+ )
45558
+ );
45047
45559
  }
45048
- function runFold(ctx, action) {
45049
- if (ctx.maxBlock === 0) {
45050
- ctx.print("No collapsible blocks on screen.", "warn");
45051
- return;
45052
- }
45053
- const n = ctx.args[0] != null ? Number(ctx.args[0]) : ctx.maxBlock;
45054
- if (!Number.isInteger(n) || n < 1 || n > ctx.maxBlock) {
45055
- ctx.print(`usage: /${action} [1-${ctx.maxBlock}] (no number \u21D2 the newest block)`, "error");
45056
- return;
45057
- }
45058
- if (action === "expand") ctx.expand(n);
45059
- else ctx.collapse(n);
45060
- }
45061
- var COMMANDS = [
45062
- {
45063
- name: "help",
45064
- usage: "/help",
45065
- description: "List slash commands",
45066
- run: (ctx) => {
45067
- ctx.print("Slash commands:");
45068
- for (const cmd of COMMANDS) ctx.print(` ${cmd.usage.padEnd(28)} ${cmd.description}`);
45560
+ function flattenEntries(entries2, textWidth, expanded = /* @__PURE__ */ new Set()) {
45561
+ const lines = [];
45562
+ let prevKey = null;
45563
+ let blockNo = 0;
45564
+ for (const entry of entries2) {
45565
+ const blockKey = `${entry.source}|${entry.kind}`;
45566
+ const newBlock = blockKey !== prevKey;
45567
+ if (newBlock && lines.length > 0) {
45568
+ lines.push({
45569
+ key: `sp:${entry.id}`,
45570
+ source: entry.source,
45571
+ level: entry.level,
45572
+ kind: entry.kind,
45573
+ text: "",
45574
+ showTag: false,
45575
+ collapsed: false,
45576
+ spacer: true
45577
+ });
45069
45578
  }
45070
- },
45071
- {
45072
- name: "version",
45073
- usage: "/version",
45074
- description: "Show the cli version",
45075
- run: (ctx) => ctx.print(`4PM CLI v${ctx.info.version} \xB7 profile ${ctx.info.profile}`)
45076
- },
45077
- {
45078
- name: "status",
45079
- usage: "/status",
45080
- description: "Show connection + profile + session status",
45081
- run: (ctx) => {
45082
- const s = ctx.live;
45083
- const uptime = fmtDuration(Math.max(0, Math.floor((Date.now() - s.startedAt) / 1e3)));
45084
- ctx.print(`connection: ${s.status}`);
45085
- ctx.print(`scope: ${s.scope}`);
45086
- ctx.print(`worker: ${s.worker ?? "(unknown)"}`);
45087
- if (s.project) ctx.print(`project: ${s.project}`);
45088
- ctx.print(`profile: ${ctx.info.profile}`);
45089
- ctx.print(`server: ${ctx.info.serverUrl}`);
45090
- ctx.print(`aiCli: ${ctx.info.aiCli}${s.activeProfile ? ` \xB7 profile ${s.activeProfile}` : ""}`);
45091
- ctx.print(`serving: ${ctx.info.physicPath ?? "(none)"}`);
45092
- ctx.print(`uptime: ${uptime} \xB7 commands: ${getCommandHistory().length}`);
45579
+ prevKey = blockKey;
45580
+ let firstRow = newBlock;
45581
+ if (entry.kind === "result") {
45582
+ blockNo += 1;
45583
+ const count = prettyResult(entry.text).split("\n").length;
45584
+ const title = `result \xB7 ${entry.resultKind ?? "json"} \xB7 ${count} ${count === 1 ? "line" : "lines"}`;
45585
+ pushCollapsible(lines, entry, { blockNo, title, body: entry.text, expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: entry.id });
45586
+ continue;
45093
45587
  }
45094
- },
45095
- {
45096
- name: "history",
45097
- usage: "/history [N]",
45098
- description: "List the last N executed commands (+ time)",
45099
- run: runHistory
45100
- },
45101
- {
45102
- name: "output",
45103
- usage: "/output <n>",
45104
- description: "Replay a command's output (n from /history)",
45105
- run: runOutput
45106
- },
45107
- {
45108
- name: "logs",
45109
- usage: "/logs [N]",
45110
- description: "Tail the profile's structured log",
45111
- run: runLogs
45112
- },
45113
- {
45114
- name: "expand",
45115
- usage: "/expand [N]",
45116
- description: "Expand a collapsed \u25B8[N] block (default: newest); pretty-prints json/code",
45117
- run: (ctx) => runFold(ctx, "expand")
45118
- },
45119
- {
45120
- name: "collapse",
45121
- usage: "/collapse [N]",
45122
- description: "Collapse an expanded \u25B8[N] block (default: newest)",
45123
- run: (ctx) => runFold(ctx, "collapse")
45124
- },
45125
- {
45126
- name: "reconnect",
45127
- usage: "/reconnect",
45128
- description: "Reconnect to the server now (skip the backoff)",
45129
- run: (ctx) => ctx.reconnect()
45130
- },
45131
- {
45132
- name: "whoami",
45133
- usage: "/whoami",
45134
- description: "Show this machine account + its teams & projects",
45135
- run: runWhoami
45136
- },
45137
- {
45138
- name: "config",
45139
- usage: "/config [show|init|set <k> <v>|delete <k>]",
45140
- description: "View / init / update / delete profile config",
45141
- run: runConfig
45142
- },
45143
- {
45144
- name: "claude-cmd",
45145
- usage: "/claude-cmd /context",
45146
- description: "Run an AI-CLI slash command (e.g. /context, /usage, /status)",
45147
- run: (ctx) => {
45148
- const rest = ctx.args.join(" ").trim();
45149
- if (!rest) {
45150
- ctx.print("usage: /claude-cmd /context (forwards to the AI CLI)", "error");
45588
+ const raw = entry.kind === "out" ? entry.text.replace(/\n$/, "") : entry.text;
45589
+ toSegments(raw).forEach((seg, i) => {
45590
+ if (seg.collapsed) {
45591
+ blockNo += 1;
45592
+ pushCollapsible(lines, entry, { blockNo, title: seg.text, body: seg.body ?? "", expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: `${entry.id}:${i}` });
45593
+ firstRow = false;
45151
45594
  return;
45152
45595
  }
45153
- ctx.submitAi(rest);
45154
- }
45155
- },
45156
- {
45157
- name: "clear",
45158
- usage: "/clear",
45159
- description: "Clear the transcript",
45160
- run: (ctx) => ctx.clear()
45161
- },
45162
- {
45163
- name: "quit",
45164
- usage: "/quit",
45165
- description: "Quit the cli (also /exit, Ctrl+C)",
45166
- run: (ctx) => ctx.quit()
45167
- }
45168
- ];
45169
- var ALIASES = { exit: "quit" };
45170
- var SLASH_COMMANDS = COMMANDS.map((c) => ({ name: c.name, usage: c.usage, description: c.description }));
45171
- function runSlashCommand(line, base) {
45172
- const tokens = line.replace(/^\//, "").trim().split(/\s+/);
45173
- const rawName = tokens[0] ?? "";
45174
- const name = ALIASES[rawName] ?? rawName;
45175
- const command = COMMANDS.find((c) => c.name === name);
45176
- const ctx = { ...base, args: tokens.slice(1) };
45177
- if (!command) {
45178
- ctx.print(`Unknown command: /${rawName} \u2014 try /help`, "error");
45179
- return;
45596
+ wrapToWidth(seg.text, textWidth).forEach((row, j) => {
45597
+ lines.push({
45598
+ key: `${entry.id}:${i}:${j}`,
45599
+ source: entry.source,
45600
+ level: entry.level,
45601
+ kind: entry.kind,
45602
+ text: row,
45603
+ showTag: firstRow,
45604
+ collapsed: false,
45605
+ spacer: false
45606
+ });
45607
+ firstRow = false;
45608
+ });
45609
+ });
45180
45610
  }
45181
- command.run(ctx);
45611
+ return lines;
45612
+ }
45613
+ function TranscriptLine({ line }) {
45614
+ const tag = sourceTag(line.source);
45615
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Box_default, { flexDirection: "row", children: [
45616
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { width: 7, marginRight: 1, children: line.showTag ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: tag.color, dimColor: line.source === "system", children: tag.text }) : null }),
45617
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { flexGrow: 1, children: line.spacer ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: " " }) : line.collapsed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, wrap: "truncate-end", children: line.text || " " }) : line.kind === "aireq" || line.kind === "aires" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AiMarkerLine, { line }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: textColor(line), wrap: "truncate-end", children: line.text || " " }) })
45618
+ ] });
45182
45619
  }
45183
45620
 
45184
45621
  // src/ui/command-input.tsx
45622
+ var import_react24 = __toESM(require_react(), 1);
45185
45623
  var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
45186
45624
  function Hint() {
45187
45625
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: " /help \xB7 Tab complete \xB7 \u2191\u2193 history \xB7 PgUp/PgDn scroll \xB7 Ctrl+C quit" });
@@ -45466,6 +45904,9 @@ function App2({
45466
45904
  setPendingConfirm({ prompt, onYes });
45467
45905
  },
45468
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),
45469
45910
  reconnect: () => bus.requestReconnect(),
45470
45911
  expand: expandFold,
45471
45912
  collapse: collapseFold,