@4pm/cli 1.5.0 → 1.5.2

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 +983 -602
  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) {
@@ -32932,7 +32942,15 @@ var dispatchCommandRequestSchema = external_exports.object({
32932
32942
  * `project.git_write` (`write`) instead of `command.execute`, and validates `command`
32933
32943
  * against the git allowlist (`isGitCommandAllowed`). Absent ⇒ normal console dispatch.
32934
32944
  */
32935
- gitOp: external_exports.enum(["read", "write"]).optional()
32945
+ gitOp: external_exports.enum(["read", "write"]).optional(),
32946
+ /**
32947
+ * One-shot AI mode (ADR-0249): the prompt is a text-in → text-out task (spec review /
32948
+ * compose / suggest / generators) that must NOT trigger the AI CLI's agentic tool loop.
32949
+ * The cli caps such a run (`--max-turns 1` + disallowed agentic tools) so it can't wander
32950
+ * the repo / edit files / loop forever. Only meaningful with `ai:true`; absent ⇒ a full
32951
+ * agent run (the Console tab + Git merge, which legitimately use tools).
32952
+ */
32953
+ aiOneShot: external_exports.boolean().optional()
32936
32954
  }).superRefine((v, ctx) => {
32937
32955
  if (v.machineLinkId == null === (v.pick == null)) {
32938
32956
  ctx.addIssue({
@@ -32955,10 +32973,30 @@ var dispatchCommandRequestSchema = external_exports.object({
32955
32973
  var listCommandsQuerySchema = baseRequestSchema.extend({
32956
32974
  projectId: external_exports.string().uuid(),
32957
32975
  // 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`.
32976
+ // text/status; `from`/`to` are an inclusive `YYYY-MM-DD` date range over `startedAt`;
32977
+ // `machineLinkId` (ADR-0249) filters to one machine-user's commands (absent ⇒ all).
32959
32978
  from: external_exports.string().optional(),
32960
- to: external_exports.string().optional()
32979
+ to: external_exports.string().optional(),
32980
+ machineLinkId: external_exports.string().uuid().optional()
32961
32981
  });
32982
+ var CLI_SLASH_COMMANDS = [
32983
+ { name: "help", usage: "/help", description: "List slash commands" },
32984
+ { name: "version", usage: "/version", description: "Show the cli version" },
32985
+ { name: "status", usage: "/status", description: "Show connection + profile + session status" },
32986
+ { name: "history", usage: "/history [N]", description: "List the last N executed commands (+ time)" },
32987
+ { name: "output", usage: "/output <n>", description: "Replay a command's output (n from /history)" },
32988
+ { name: "logs", usage: "/logs [N]", description: "Tail the profile's structured log" },
32989
+ { name: "expand", usage: "/expand [N]", description: "Expand a collapsed \u25B8[N] block" },
32990
+ { name: "collapse", usage: "/collapse [N]", description: "Collapse an expanded \u25B8[N] block" },
32991
+ { name: "reconnect", usage: "/reconnect", description: "Reconnect to the server now (skip the backoff)" },
32992
+ { name: "whoami", usage: "/whoami", description: "Show this machine account + its teams & projects" },
32993
+ { name: "ai-profile", usage: "/ai-profile [list|use <n>|reset]", description: "List AI profiles / switch which one runs" },
32994
+ { name: "config", usage: "/config [show|init|set <k> <v>|delete <k>]", description: "View / init / update / delete profile config" },
32995
+ { name: "claude-cmd", usage: "/claude-cmd /context", description: "Run an AI-CLI slash command (e.g. /context, /usage)" },
32996
+ { name: "clear", usage: "/clear", description: "Clear the transcript" },
32997
+ { name: "quit", usage: "/quit", description: "Quit the cli (also /exit)" }
32998
+ ];
32999
+ var CLI_SLASH_COMMAND_NAMES = CLI_SLASH_COMMANDS.map((c) => c.name);
32962
33000
 
32963
33001
  // vendor/@4pm/validation/schemas.ts
32964
33002
  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 +33039,10 @@ var listWorkersQuerySchema = baseRequestSchema.extend({
33001
33039
  /** Only workers whose live CPU/memory/disk usage % ≥ the given threshold (0–100). */
33002
33040
  minCpuPct: external_exports.coerce.number().min(0).max(100).optional(),
33003
33041
  minMemPct: external_exports.coerce.number().min(0).max(100).optional(),
33004
- minDiskPct: external_exports.coerce.number().min(0).max(100).optional()
33042
+ minDiskPct: external_exports.coerce.number().min(0).max(100).optional(),
33043
+ /** Hide workers with no cli attached (clis = 0). The web defaults this on; unchecking the
33044
+ * "show empty CLIs" box drops it so empty workers are listed too. */
33045
+ hideEmptyClis: external_exports.enum(["true", "false"]).transform((v) => v === "true").optional()
33005
33046
  });
33006
33047
  var updateWorkerRequestSchema = external_exports.object({
33007
33048
  name: external_exports.string().min(1).max(100).optional()
@@ -33792,7 +33833,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
33792
33833
  }
33793
33834
 
33794
33835
  // src/core/ws-client.ts
33795
- import { existsSync as existsSync12, mkdirSync as mkdirSync5, rmSync as rmSync4 } from "fs";
33836
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, rmSync as rmSync4 } from "fs";
33796
33837
  import { randomUUID as randomCommandId } from "crypto";
33797
33838
 
33798
33839
  // src/core/update-scheduler.ts
@@ -34014,11 +34055,29 @@ import { spawn as spawn2 } from "child_process";
34014
34055
  var KILL_GRACE_MS = 5e3;
34015
34056
  async function runCommand(dispatch, emit, opts) {
34016
34057
  let seq = 0;
34017
- const child = spawn2(dispatch.cmd, dispatch.args ?? [], {
34018
- cwd: dispatch.path,
34019
- env: { ...process.env, ...dispatch.env },
34020
- shell: false
34058
+ let child;
34059
+ try {
34060
+ child = spawn2(dispatch.cmd, dispatch.args ?? [], {
34061
+ cwd: dispatch.path,
34062
+ env: { ...process.env, ...dispatch.env },
34063
+ shell: false
34064
+ });
34065
+ } catch (err) {
34066
+ emit({
34067
+ commandId: dispatch.commandId,
34068
+ seq: seq++,
34069
+ chunk: `spawn error: ${err.message}
34070
+ `,
34071
+ done: true,
34072
+ exitCode: -1
34073
+ });
34074
+ return;
34075
+ }
34076
+ child.stdin?.on("error", () => {
34021
34077
  });
34078
+ if (opts?.stdin != null) {
34079
+ child.stdin?.write(opts.stdin);
34080
+ }
34022
34081
  child.stdin?.end();
34023
34082
  const timeoutMs = opts?.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 0;
34024
34083
  let timedOut = false;
@@ -34117,15 +34176,24 @@ function createAiStreamParser(cli) {
34117
34176
  let buffer = "";
34118
34177
  const acc = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
34119
34178
  let sessionId = "";
34179
+ let apiError = { isError: false, status: null };
34120
34180
  function handleClaude(ev) {
34121
34181
  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;
34182
+ if (ev.type === "result") {
34183
+ if (ev.is_error === true) {
34184
+ apiError = {
34185
+ isError: true,
34186
+ status: typeof ev.api_error_status === "number" ? ev.api_error_status : null
34187
+ };
34188
+ }
34189
+ if (ev.usage) {
34190
+ acc.input = ev.usage.input_tokens ?? 0;
34191
+ acc.output = ev.usage.output_tokens ?? 0;
34192
+ acc.cacheRead = ev.usage.cache_read_input_tokens ?? 0;
34193
+ acc.cacheCreation = ev.usage.cache_creation_input_tokens ?? 0;
34194
+ acc.tokens = acc.input + acc.output;
34195
+ if (typeof ev.total_cost_usd === "number") acc.costUsd = ev.total_cost_usd;
34196
+ }
34129
34197
  return "";
34130
34198
  }
34131
34199
  if (ev.type === "assistant") {
@@ -34185,7 +34253,8 @@ function createAiStreamParser(cli) {
34185
34253
  return handled === null ? line : handled;
34186
34254
  },
34187
34255
  usage: () => ({ ...acc }),
34188
- sessionId: () => sessionId
34256
+ sessionId: () => sessionId,
34257
+ apiError: () => ({ ...apiError })
34189
34258
  };
34190
34259
  }
34191
34260
  function estimateTokens(text) {
@@ -34203,12 +34272,21 @@ function isSessionLimit(text) {
34203
34272
  text
34204
34273
  );
34205
34274
  }
34206
- function runAttempt(commandId, cmd, args, cwd2, env3, onChunk, timeoutMs) {
34275
+ function isOutOfCredits(text) {
34276
+ return /out of (?:usage )?credits|credits[_ ]required|out[_ ]of[_ ]credits|insufficient credits/i.test(
34277
+ text
34278
+ );
34279
+ }
34280
+ function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs) {
34207
34281
  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);
34282
+ runCommand(
34283
+ { commandId, cmd, args, path: cwd2, env: env3 },
34284
+ (out) => {
34285
+ if (out.chunk) onChunk(out.chunk);
34286
+ if (out.done) resolve8(out.exitCode ?? -1);
34287
+ },
34288
+ { stdin, ...timeoutMs > 0 ? { timeoutMs } : {} }
34289
+ ).catch(() => resolve8(-1));
34212
34290
  });
34213
34291
  }
34214
34292
  async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
@@ -34229,14 +34307,15 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
34229
34307
  handlers.onChunk(text);
34230
34308
  }
34231
34309
  };
34232
- finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, cwd2, attempt.env, emit, timeoutMs);
34310
+ finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, attempt.stdin, cwd2, attempt.env, emit, timeoutMs);
34233
34311
  const tail = parser.flush();
34234
34312
  if (tail) {
34235
34313
  captured += tail;
34236
34314
  handlers.onChunk(tail);
34237
34315
  }
34238
34316
  lastCaptured = captured;
34239
- if (finalExit === 0) {
34317
+ const apiErr = parser.apiError();
34318
+ if (finalExit === 0 && !apiErr.isError) {
34240
34319
  const usage = parser.usage();
34241
34320
  if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
34242
34321
  reportToolResult(attempt.cmd, true);
@@ -34249,7 +34328,8 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
34249
34328
  sessionId: parser.sessionId()
34250
34329
  };
34251
34330
  }
34252
- const reason = isAuthFailure(captured) ? "auth" : isSessionLimit(captured) ? "limit" : "other";
34331
+ if (finalExit === 0 && apiErr.isError) finalExit = apiErr.status && apiErr.status > 0 ? apiErr.status : 1;
34332
+ const reason = isAuthFailure(captured) ? "auth" : apiErr.status === 429 || isOutOfCredits(captured) ? "credits" : isSessionLimit(captured) ? "limit" : "other";
34253
34333
  if (i === total - 1) break;
34254
34334
  handlers.onAttemptFail(attempt.label, reason);
34255
34335
  }
@@ -34258,6 +34338,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
34258
34338
  }
34259
34339
  function summarizeAiFailure(captured, exitCode) {
34260
34340
  if (isAuthFailure(captured)) return "Not logged in";
34341
+ if (isOutOfCredits(captured)) return "Out of usage credits";
34261
34342
  if (isSessionLimit(captured)) return "Usage limit reached";
34262
34343
  const firstLine = captured.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
34263
34344
  return firstLine || `exited ${exitCode}`;
@@ -34303,6 +34384,52 @@ function setWorkingCredential(profileDir2, key) {
34303
34384
  } catch {
34304
34385
  }
34305
34386
  }
34387
+ var PINNED_CREDENTIAL_KEY = "__pinned__";
34388
+ function getPinnedCredential(profileDir2) {
34389
+ return readState(profileDir2)[PINNED_CREDENTIAL_KEY] ?? null;
34390
+ }
34391
+ function setPinnedCredential(profileDir2, key) {
34392
+ const state = readState(profileDir2);
34393
+ if (state[PINNED_CREDENTIAL_KEY] === key) return;
34394
+ state[PINNED_CREDENTIAL_KEY] = key;
34395
+ try {
34396
+ writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
34397
+ } catch {
34398
+ }
34399
+ }
34400
+ function clearPinnedCredential(profileDir2) {
34401
+ const state = readState(profileDir2);
34402
+ if (!(PINNED_CREDENTIAL_KEY in state)) return;
34403
+ delete state[PINNED_CREDENTIAL_KEY];
34404
+ try {
34405
+ writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
34406
+ } catch {
34407
+ }
34408
+ }
34409
+
34410
+ // src/utils/time.ts
34411
+ function formatTimestampInZone(date, timeZone) {
34412
+ const build = (tz) => {
34413
+ const parts = new Intl.DateTimeFormat("en-CA", {
34414
+ ...tz ? { timeZone: tz } : {},
34415
+ hour12: false,
34416
+ year: "numeric",
34417
+ month: "2-digit",
34418
+ day: "2-digit",
34419
+ hour: "2-digit",
34420
+ minute: "2-digit",
34421
+ second: "2-digit"
34422
+ }).formatToParts(date);
34423
+ const get2 = (type) => parts.find((p) => p.type === type)?.value ?? "";
34424
+ const hour = get2("hour") === "24" ? "00" : get2("hour");
34425
+ return `${get2("year")}/${get2("month")}/${get2("day")} ${hour}:${get2("minute")}:${get2("second")}`;
34426
+ };
34427
+ try {
34428
+ return build(timeZone);
34429
+ } catch {
34430
+ return build();
34431
+ }
34432
+ }
34306
34433
 
34307
34434
  // src/utils/ai-cli.ts
34308
34435
  import { readFileSync as readFileSync8 } from "fs";
@@ -34313,6 +34440,42 @@ var REQUIRED_AI_ARGS = {
34313
34440
  codex: ["exec", "--json"],
34314
34441
  antigravity: []
34315
34442
  };
34443
+ var ONE_SHOT_DISALLOWED_CLAUDE_TOOLS = [
34444
+ "Bash",
34445
+ "Edit",
34446
+ "Write",
34447
+ "Read",
34448
+ "Glob",
34449
+ "Grep",
34450
+ "NotebookEdit",
34451
+ "Task",
34452
+ "WebSearch",
34453
+ "WebFetch",
34454
+ "Skill",
34455
+ "SlashCommand",
34456
+ "ToolSearch",
34457
+ "TaskCreate",
34458
+ "TaskGet",
34459
+ "TaskList",
34460
+ "TaskOutput",
34461
+ "TaskStop",
34462
+ "TaskUpdate",
34463
+ "Monitor",
34464
+ "DesignSync",
34465
+ "CronCreate",
34466
+ "CronDelete",
34467
+ "CronList",
34468
+ "EnterWorktree",
34469
+ "ExitWorktree",
34470
+ "RemoteTrigger",
34471
+ "ScheduleWakeup",
34472
+ "SendMessage",
34473
+ "PushNotification"
34474
+ ].join(",");
34475
+ function oneShotArgs(cmd) {
34476
+ if (!cmd.includes("claude")) return [];
34477
+ return ["--max-turns=1", "--disallowedTools", ONE_SHOT_DISALLOWED_CLAUDE_TOOLS, "--permission-mode", "default"];
34478
+ }
34316
34479
  function requiredArgs(cmd) {
34317
34480
  if (cmd.includes("claude")) return REQUIRED_AI_ARGS.claude;
34318
34481
  if (cmd.includes("codex")) return REQUIRED_AI_ARGS.codex;
@@ -34372,12 +34535,15 @@ function folderScopeGuard(folder, prompt) {
34372
34535
  prompt
34373
34536
  ].join("\n");
34374
34537
  }
34375
- function buildRunArgs(profile, cmd, prompt, resumeId) {
34538
+ function buildRunArgs(profile, cmd, resumeId, oneShot = false) {
34376
34539
  const extras = profile.args ?? [];
34377
34540
  const model = profile.model?.trim();
34378
34541
  const modelArgs = model ? ["--model", model] : [];
34379
34542
  const resumeArgs = resumeId && cmd.includes("claude") ? ["--resume", resumeId] : [];
34380
- return [...requiredArgs(cmd), ...resumeArgs, ...extras, ...modelArgs, prompt];
34543
+ const turns = profile.maxTurns;
34544
+ const perProfileTurns = !oneShot && cmd.includes("claude") && typeof turns === "number" && turns > 0 ? [`--max-turns=${Math.floor(turns)}`] : [];
34545
+ const oneShotExtra = oneShot ? oneShotArgs(cmd) : perProfileTurns;
34546
+ return [...requiredArgs(cmd), ...resumeArgs, ...extras, ...modelArgs, ...oneShotExtra];
34381
34547
  }
34382
34548
  function claudeHomeDirs(config) {
34383
34549
  const dirs = profileDirs(claudeProfilesOf(config));
@@ -34416,10 +34582,10 @@ function resolveClaudeProfiles(config, workingDir) {
34416
34582
  if (idx > 0) profiles.unshift(profiles.splice(idx, 1)[0]);
34417
34583
  return profiles;
34418
34584
  }
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);
34585
+ function planAiRun(prompt, config, hint = {}, resume = /* @__PURE__ */ new Map(), oneShot = false) {
34586
+ return isUnifiedConfig(config) ? planUnifiedRun(prompt, config, hint.credential ?? null, resume, oneShot) : planLegacyRun(prompt, config, hint.dir ?? null, resume, oneShot);
34421
34587
  }
34422
- function planUnifiedRun(prompt, config, workingCredential, resume) {
34588
+ function planUnifiedRun(prompt, config, workingCredential, resume, oneShot) {
34423
34589
  const baseEnv = config.aiEnv ?? {};
34424
34590
  const active = activeProvider(config);
34425
34591
  const usable = config.aiProfiles.filter(isUsableCredential);
@@ -34428,7 +34594,7 @@ function planUnifiedRun(prompt, config, workingCredential, resume) {
34428
34594
  return {
34429
34595
  cmd: active,
34430
34596
  attempts: [
34431
- { cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, prompt), env: { ...baseEnv } }
34597
+ { cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, void 0, oneShot), stdin: prompt, env: { ...baseEnv } }
34432
34598
  ]
34433
34599
  };
34434
34600
  }
@@ -34446,19 +34612,20 @@ function planUnifiedRun(prompt, config, workingCredential, resume) {
34446
34612
  label: cred.label?.trim() || profileDisplayLabel(dir2),
34447
34613
  dir: dir2,
34448
34614
  key,
34449
- args: buildRunArgs(cred, cmd, prompt, resume.get(key)),
34615
+ args: buildRunArgs(cred, cmd, resume.get(key), oneShot),
34616
+ stdin: prompt,
34450
34617
  env: envVar ? { ...baseEnv, [envVar]: dir2 } : { ...baseEnv }
34451
34618
  }));
34452
34619
  return { cmd: attempts[0]?.cmd ?? DEFAULT_AI_CLI, attempts };
34453
34620
  }
34454
- function planLegacyRun(prompt, config, workingDir, resume) {
34621
+ function planLegacyRun(prompt, config, workingDir, resume, oneShot) {
34455
34622
  const cmd = config.aiCli || DEFAULT_AI_CLI;
34456
34623
  const baseEnv = config.aiEnv ?? {};
34457
34624
  const envVar = profileEnvVar(cmd);
34458
34625
  const defaultPlan = {
34459
34626
  cmd,
34460
34627
  attempts: [
34461
- { cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, prompt), env: { ...baseEnv } }
34628
+ { cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, void 0, oneShot), stdin: prompt, env: { ...baseEnv } }
34462
34629
  ]
34463
34630
  };
34464
34631
  if (!envVar) return defaultPlan;
@@ -34475,7 +34642,8 @@ function planLegacyRun(prompt, config, workingDir, resume) {
34475
34642
  label: profileDisplayLabel(dir2),
34476
34643
  dir: dir2,
34477
34644
  key,
34478
- args: buildRunArgs(profile, cmd, prompt, resume.get(key)),
34645
+ args: buildRunArgs(profile, cmd, resume.get(key), oneShot),
34646
+ stdin: prompt,
34479
34647
  env: { ...baseEnv, [envVar]: dir2 }
34480
34648
  };
34481
34649
  });
@@ -36668,138 +36836,530 @@ async function addProject(payload, profileDir2, onProgress) {
36668
36836
  }
36669
36837
  }
36670
36838
 
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";
36839
+ // src/ui/slash-commands.ts
36840
+ import { existsSync as existsSync11 } from "fs";
36742
36841
  import { join as join23 } from "path";
36743
- var SERVER_MANAGED_KEYS = [
36842
+ var CONFIG_KEYS = [
36843
+ "aiCli",
36844
+ "claudeHome",
36845
+ "codexHome",
36846
+ "antigravityHome",
36744
36847
  "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"
36848
+ "commandHistoryUploadMinutes",
36849
+ "autoUpdate",
36850
+ "aiEnv"
36757
36851
  ];
36758
- function readConfigText(profileDir2) {
36759
- const path = join23(profileDir2, "config.json");
36760
- if (existsSync11(path)) {
36761
- try {
36762
- return readFileSync15(path, "utf8");
36763
- } catch {
36852
+ function configPath(profileDir2) {
36853
+ return join23(profileDir2, "config.json");
36854
+ }
36855
+ function parseConfigAssignment(key, valueTokens) {
36856
+ const value = valueTokens.join(" ").trim();
36857
+ switch (key) {
36858
+ case "aiCli":
36859
+ case "physicPath":
36860
+ if (!value) return { error: `Missing value for "${key}".` };
36861
+ return { patch: { [key]: value } };
36862
+ case "claudeHome":
36863
+ case "codexHome":
36864
+ case "antigravityHome": {
36865
+ const dirs = valueTokens.filter((t) => t.trim());
36866
+ if (dirs.length === 0) return { error: `Missing value for "${key}".` };
36867
+ return { patch: { [key]: dirs.map((profile) => ({ profile, model: "" })) } };
36868
+ }
36869
+ case "commandHistoryUploadMinutes": {
36870
+ const n = Number(value);
36871
+ if (!Number.isFinite(n) || n <= 0) return { error: `"${value}" is not a positive number.` };
36872
+ return { patch: { commandHistoryUploadMinutes: n } };
36764
36873
  }
36874
+ case "autoUpdate":
36875
+ if (value !== "true" && value !== "false") return { error: `Use true|false, not "${value}".` };
36876
+ return { patch: { autoUpdate: value === "true" } };
36877
+ case "aiEnv":
36878
+ try {
36879
+ return { patch: { aiEnv: JSON.parse(value) } };
36880
+ } catch {
36881
+ return { error: `aiEnv must be JSON, e.g. {"KEY":"val"}.` };
36882
+ }
36883
+ default:
36884
+ return { error: `Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.` };
36765
36885
  }
36766
- return JSON.stringify(defaultProfileConfig(), null, 2);
36767
36886
  }
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}` };
36887
+ function runConfig(ctx) {
36888
+ const [sub, ...rest] = ctx.args;
36889
+ const dir2 = ctx.info.profileDir;
36890
+ const path = configPath(dir2);
36891
+ if (!sub || sub === "show") {
36892
+ ctx.print(`config: ${path}`);
36893
+ const json = JSON.stringify(readProfileConfig(dir2), null, 2);
36894
+ for (const line of json.split("\n")) ctx.print(line);
36895
+ return;
36774
36896
  }
36775
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
36776
- return { ok: false, error: "config.json must be a JSON object." };
36897
+ if (sub === "init") {
36898
+ const writeDefaults = (verb) => {
36899
+ overwriteProfileConfig(dir2, defaultProfileConfig());
36900
+ ctx.print(`\u2714 ${verb} default config: ${path}`);
36901
+ ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
36902
+ };
36903
+ if (existsSync11(path)) {
36904
+ ctx.confirm(
36905
+ `config exists: ${path} \u2014 replace it with defaults? (y/N)`,
36906
+ () => writeDefaults("replaced with")
36907
+ );
36908
+ return;
36909
+ }
36910
+ writeDefaults("wrote");
36911
+ return;
36777
36912
  }
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];
36913
+ if (sub === "set") {
36914
+ const [key, ...valueTokens] = rest;
36915
+ if (!key) {
36916
+ ctx.print("usage: /config set <key> <value>", "error");
36917
+ return;
36918
+ }
36919
+ const result = parseConfigAssignment(key, valueTokens);
36920
+ if ("error" in result) {
36921
+ ctx.print(result.error, "error");
36922
+ return;
36923
+ }
36924
+ const merged = writeProfileConfig(dir2, result.patch);
36925
+ ctx.print(`\u2714 ${key} = ${JSON.stringify(merged[key])} (applies to the next prompt)`);
36926
+ return;
36783
36927
  }
36784
- overwriteProfileConfig(profileDir2, next);
36785
- return { ok: true };
36928
+ if (sub === "delete" || sub === "unset") {
36929
+ const key = rest[0];
36930
+ if (!key) {
36931
+ ctx.print("usage: /config delete <key>", "error");
36932
+ return;
36933
+ }
36934
+ if (!CONFIG_KEYS.includes(key)) {
36935
+ ctx.print(`Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.`, "error");
36936
+ return;
36937
+ }
36938
+ deleteProfileConfigKey(dir2, key);
36939
+ ctx.print(`\u2714 deleted ${key}`);
36940
+ return;
36941
+ }
36942
+ ctx.print(`Unknown /config subcommand "${sub}". Use: show | init | set | delete.`, "error");
36786
36943
  }
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();
36944
+ function fmtDuration(totalSec) {
36945
+ const h = Math.floor(totalSec / 3600);
36946
+ const m = Math.floor(totalSec % 3600 / 60);
36947
+ const s = totalSec % 60;
36948
+ const pad = (n) => String(n).padStart(2, "0");
36949
+ return h > 0 ? `${h}h ${pad(m)}m ${pad(s)}s` : `${m}m ${pad(s)}s`;
36950
+ }
36951
+ function commandLine(cmd, args) {
36952
+ return `${cmd} ${args.join(" ")}`.trim();
36953
+ }
36954
+ function runHistory(ctx) {
36955
+ const n = Math.max(1, Math.min(50, Number(ctx.args[0]) || 15));
36956
+ const all = getCommandHistory();
36957
+ if (all.length === 0) {
36958
+ ctx.print("No commands run yet.");
36959
+ return;
36960
+ }
36961
+ const recent = all.slice(-n).reverse();
36962
+ ctx.print(`Last ${recent.length} of ${all.length} commands (newest first) \xB7 /output <n> to view:`);
36963
+ recent.forEach((e, i) => {
36964
+ const time = new Date(e.startedAt).toLocaleTimeString();
36965
+ const state = e.status === "done" ? "\u2713" : e.status === "failed" ? `\u2717 exit ${e.exitCode}` : "\xB7 running";
36966
+ ctx.print(` [${i + 1}] ${time} ${commandLine(e.cmd, e.args)} \xB7 ${state}`);
36967
+ });
36968
+ }
36969
+ function runOutput(ctx) {
36970
+ const all = getCommandHistory();
36971
+ const n = Number(ctx.args[0]);
36972
+ if (!Number.isInteger(n) || n < 1 || n > all.length) {
36973
+ ctx.print("usage: /output <n> (n = index from /history)", "error");
36974
+ return;
36975
+ }
36976
+ const entry = all.slice().reverse()[n - 1];
36977
+ const state = entry.exitCode != null ? ` (exit ${entry.exitCode})` : "";
36978
+ ctx.print(`\u2500\u2500 output [${n}] ${commandLine(entry.cmd, entry.args)} \xB7 ${entry.status}${state} \u2500\u2500`);
36979
+ const out = readCommandOutput(entry.commandId);
36980
+ if (out == null) {
36981
+ ctx.print("(no output stored \u2014 it ran in a previous session or produced none)", "warn");
36982
+ return;
36983
+ }
36984
+ for (const line of out.replace(/\n$/, "").split("\n")) ctx.print(line);
36985
+ }
36986
+ function runLogs(ctx) {
36987
+ const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
36988
+ const dir2 = join23(ctx.info.profileDir, "logs");
36989
+ const lines = readRecentLogLines(dir2, n);
36990
+ if (lines.length === 0) {
36991
+ ctx.print("No log lines yet.");
36992
+ return;
36993
+ }
36994
+ ctx.print(`Last ${lines.length} log lines (${dir2}):`);
36995
+ for (const raw of lines) {
36996
+ try {
36997
+ const j = JSON.parse(raw);
36998
+ const time = String(j.timestamp ?? "").slice(11, 19);
36999
+ const level = String(j.level ?? "").padEnd(5);
37000
+ const { timestamp, level: _l, version, profile, scope, event, ...fields } = j;
37001
+ void timestamp;
37002
+ void _l;
37003
+ void version;
37004
+ void profile;
37005
+ void scope;
37006
+ const extra = Object.keys(fields).length ? " " + JSON.stringify(fields) : "";
37007
+ ctx.print(` ${time} ${level} ${String(event ?? "")}${extra}`);
37008
+ } catch {
37009
+ ctx.print(` ${raw}`);
37010
+ }
37011
+ }
37012
+ }
37013
+ function runWhoami(ctx) {
37014
+ ctx.print("whoami: fetching from the server\u2026");
37015
+ void ctx.info.whoami().then((w) => {
37016
+ if (!w) {
37017
+ ctx.print("whoami: unavailable (server unreachable or link invalid)", "warn");
37018
+ return;
37019
+ }
37020
+ ctx.print(`account: ${w.username} \xB7 scope: ${w.scope} \xB7 roles: ${w.roles.join(", ") || "\u2014"}`);
37021
+ ctx.print(`teams: ${w.teams.length ? w.teams.map((t) => t.name).join(", ") : "(none)"}`);
37022
+ ctx.print(`projects: ${w.projects.length ? w.projects.map((p) => p.name).join(", ") : "(none)"}`);
37023
+ });
37024
+ }
37025
+ function aiProfileLabel(c) {
37026
+ return c.label?.trim() || profileDisplayLabel(resolveHomePath(c.profile));
37027
+ }
37028
+ function runAiProfile(ctx) {
37029
+ const dir2 = ctx.info.profileDir;
37030
+ const config = readProfileConfig(dir2);
37031
+ const list = Array.isArray(config.aiProfiles) ? config.aiProfiles : [];
37032
+ const [sub, ...rest] = ctx.args;
37033
+ const action = (sub ?? "list").toLowerCase();
37034
+ if (action === "list") {
37035
+ if (list.length === 0) {
37036
+ ctx.print("No AI profiles configured (config.aiProfiles is empty).", "warn");
37037
+ ctx.print("This worker may use a legacy per-provider config \u2014 /config show to inspect.");
37038
+ return;
37039
+ }
37040
+ const pinned = getPinnedCredential(dir2);
37041
+ const working = getWorkingCredential(dir2);
37042
+ ctx.print("AI profiles (config order = failover priority) \xB7 /ai-profile use <n> to switch:");
37043
+ list.forEach((c, i) => {
37044
+ const usable = isUsableCredential(c);
37045
+ const key = usable ? credentialKey(c.provider, resolveHomePath(c.profile)) : null;
37046
+ const flags = [];
37047
+ if (key && key === pinned) flags.push("pinned");
37048
+ else if (key && !pinned && key === working) flags.push("active");
37049
+ if (c.enabled === false) flags.push("disabled");
37050
+ const model = c.model?.trim() ? ` \xB7 model=${c.model.trim()}` : "";
37051
+ const flagStr = flags.length ? ` (${flags.join(", ")})` : "";
37052
+ const marker = key && key === pinned ? "*" : " ";
37053
+ ctx.print(` ${marker}[${i + 1}] ${c.provider.padEnd(11)} ${aiProfileLabel(c)}${model}${flagStr}`);
37054
+ });
37055
+ if (pinned) {
37056
+ ctx.print("A profile is pinned \u2014 tried first every prompt; /ai-profile reset returns to auto.");
37057
+ }
37058
+ return;
37059
+ }
37060
+ if (action === "reset" || action === "auto" || action === "clear") {
37061
+ clearPinnedCredential(dir2);
37062
+ ctx.print("\u2714 AI profile pin cleared \u2014 failover is back to automatic (config order / remembered).");
37063
+ const working = getWorkingCredential(dir2);
37064
+ ctx.setActiveProfile(working ? labelFromCredentialKey(working) : null);
37065
+ return;
37066
+ }
37067
+ if (action === "use" || action === "switch") {
37068
+ const n = Number(rest[0]);
37069
+ if (!Number.isInteger(n) || n < 1 || n > list.length) {
37070
+ ctx.print(`usage: /ai-profile use <n> (n = 1-${list.length || "?"} from /ai-profile list)`, "error");
37071
+ return;
37072
+ }
37073
+ const cred = list[n - 1];
37074
+ if (!isUsableCredential(cred)) {
37075
+ const why = cred.enabled === false ? "it is disabled" : "it has no usable profile dir / provider";
37076
+ ctx.print(`Profile [${n}] "${aiProfileLabel(cred)}" can't be used \u2014 ${why}.`, "error");
37077
+ return;
37078
+ }
37079
+ const credDir = resolveHomePath(cred.profile);
37080
+ setPinnedCredential(dir2, credentialKey(cred.provider, credDir));
37081
+ ctx.setActiveProfile(profileDisplayLabel(credDir));
37082
+ ctx.print(
37083
+ `\u2714 switched to ${cred.provider} profile "${aiProfileLabel(cred)}" \u2014 tried first from the next prompt (failover keeps the rest as backup).`
37084
+ );
37085
+ return;
37086
+ }
37087
+ ctx.print(`Unknown /ai-profile subcommand "${sub}". Use: list | use <n> | reset.`, "error");
37088
+ }
37089
+ function runFold(ctx, action) {
37090
+ if (ctx.maxBlock === 0) {
37091
+ ctx.print("No collapsible blocks on screen.", "warn");
37092
+ return;
37093
+ }
37094
+ const n = ctx.args[0] != null ? Number(ctx.args[0]) : ctx.maxBlock;
37095
+ if (!Number.isInteger(n) || n < 1 || n > ctx.maxBlock) {
37096
+ ctx.print(`usage: /${action} [1-${ctx.maxBlock}] (no number \u21D2 the newest block)`, "error");
37097
+ return;
37098
+ }
37099
+ if (action === "expand") ctx.expand(n);
37100
+ else ctx.collapse(n);
37101
+ }
37102
+ var COMMANDS = [
37103
+ {
37104
+ name: "help",
37105
+ usage: "/help",
37106
+ description: "List slash commands",
37107
+ run: (ctx) => {
37108
+ ctx.print("Slash commands:");
37109
+ for (const cmd of COMMANDS) ctx.print(` ${cmd.usage.padEnd(28)} ${cmd.description}`);
37110
+ }
37111
+ },
37112
+ {
37113
+ name: "version",
37114
+ usage: "/version",
37115
+ description: "Show the cli version",
37116
+ run: (ctx) => ctx.print(`4PM CLI v${ctx.info.version} \xB7 profile ${ctx.info.profile}`)
37117
+ },
37118
+ {
37119
+ name: "status",
37120
+ usage: "/status",
37121
+ description: "Show connection + profile + session status",
37122
+ run: (ctx) => {
37123
+ const s = ctx.live;
37124
+ const uptime = fmtDuration(Math.max(0, Math.floor((Date.now() - s.startedAt) / 1e3)));
37125
+ ctx.print(`connection: ${s.status}`);
37126
+ ctx.print(`scope: ${s.scope}`);
37127
+ ctx.print(`worker: ${s.worker ?? "(unknown)"}`);
37128
+ if (s.project) ctx.print(`project: ${s.project}`);
37129
+ ctx.print(`profile: ${ctx.info.profile}`);
37130
+ ctx.print(`server: ${ctx.info.serverUrl}`);
37131
+ ctx.print(`aiCli: ${ctx.info.aiCli}${s.activeProfile ? ` \xB7 profile ${s.activeProfile}` : ""}`);
37132
+ ctx.print(`serving: ${ctx.info.physicPath ?? "(none)"}`);
37133
+ ctx.print(`uptime: ${uptime} \xB7 commands: ${getCommandHistory().length}`);
37134
+ }
37135
+ },
37136
+ {
37137
+ name: "history",
37138
+ usage: "/history [N]",
37139
+ description: "List the last N executed commands (+ time)",
37140
+ run: runHistory
37141
+ },
37142
+ {
37143
+ name: "output",
37144
+ usage: "/output <n>",
37145
+ description: "Replay a command's output (n from /history)",
37146
+ run: runOutput
37147
+ },
37148
+ {
37149
+ name: "logs",
37150
+ usage: "/logs [N]",
37151
+ description: "Tail the profile's structured log",
37152
+ run: runLogs
37153
+ },
37154
+ {
37155
+ name: "expand",
37156
+ usage: "/expand [N]",
37157
+ description: "Expand a collapsed \u25B8[N] block (default: newest); pretty-prints json/code",
37158
+ run: (ctx) => runFold(ctx, "expand")
37159
+ },
37160
+ {
37161
+ name: "collapse",
37162
+ usage: "/collapse [N]",
37163
+ description: "Collapse an expanded \u25B8[N] block (default: newest)",
37164
+ run: (ctx) => runFold(ctx, "collapse")
37165
+ },
37166
+ {
37167
+ name: "reconnect",
37168
+ usage: "/reconnect",
37169
+ description: "Reconnect to the server now (skip the backoff)",
37170
+ run: (ctx) => ctx.reconnect()
37171
+ },
37172
+ {
37173
+ name: "whoami",
37174
+ usage: "/whoami",
37175
+ description: "Show this machine account + its teams & projects",
37176
+ run: runWhoami
37177
+ },
37178
+ {
37179
+ name: "ai-profile",
37180
+ usage: "/ai-profile [list|use <n>|reset]",
37181
+ description: "List AI profiles / switch which one runs (ADR-0250)",
37182
+ run: runAiProfile
37183
+ },
37184
+ {
37185
+ name: "config",
37186
+ usage: "/config [show|init|set <k> <v>|delete <k>]",
37187
+ description: "View / init / update / delete profile config",
37188
+ run: runConfig
37189
+ },
37190
+ {
37191
+ name: "claude-cmd",
37192
+ usage: "/claude-cmd /context",
37193
+ description: "Run an AI-CLI slash command (e.g. /context, /usage, /status)",
37194
+ run: (ctx) => {
37195
+ const rest = ctx.args.join(" ").trim();
37196
+ if (!rest) {
37197
+ ctx.print("usage: /claude-cmd /context (forwards to the AI CLI)", "error");
37198
+ return;
37199
+ }
37200
+ ctx.submitAi(rest);
37201
+ }
37202
+ },
37203
+ {
37204
+ name: "clear",
37205
+ usage: "/clear",
37206
+ description: "Clear the transcript",
37207
+ run: (ctx) => ctx.clear()
37208
+ },
37209
+ {
37210
+ name: "quit",
37211
+ usage: "/quit",
37212
+ description: "Quit the cli (also /exit, Ctrl+C)",
37213
+ run: (ctx) => ctx.quit()
37214
+ }
37215
+ ];
37216
+ var ALIASES = { exit: "quit" };
37217
+ var SLASH_COMMANDS = COMMANDS.map((c) => ({ name: c.name, usage: c.usage, description: c.description }));
37218
+ function runSlashCommand(line, base) {
37219
+ const tokens = line.replace(/^\//, "").trim().split(/\s+/);
37220
+ const rawName = tokens[0] ?? "";
37221
+ const name = ALIASES[rawName] ?? rawName;
37222
+ const command = COMMANDS.find((c) => c.name === name);
37223
+ const ctx = { ...base, args: tokens.slice(1) };
37224
+ if (!command) {
37225
+ ctx.print(`Unknown command: /${rawName} \u2014 try /help`, "error");
37226
+ return;
37227
+ }
37228
+ command.run(ctx);
37229
+ }
37230
+
37231
+ // src/core/memory-compact.ts
37232
+ import { spawn as spawn8 } from "child_process";
37233
+ var COMPACT_TIMEOUT_MS = 6e4;
37234
+ function buildPrompt3(input) {
37235
+ return [
37236
+ "You maintain a COMPACT running memory of an ongoing assistant conversation so it can continue",
37237
+ "across sessions and accounts. Merge the latest exchange into the existing memory and return the",
37238
+ `UPDATED memory. Keep it UNDER ${input.budgetChars} characters. Use terse Markdown bullets grouped`,
37239
+ "as: Decisions, Constraints, Open tasks, Glossary. Keep durable facts; drop chit-chat and anything",
37240
+ "now obsolete. Output ONLY the updated memory text \u2014 no preamble, no code fences.",
37241
+ "",
37242
+ "=== EXISTING MEMORY ===",
37243
+ input.oldMemory || "(empty)",
37244
+ "",
37245
+ "=== LATEST USER PROMPT ===",
37246
+ input.prompt,
37247
+ "",
37248
+ "=== LATEST ASSISTANT ANSWER ===",
37249
+ input.answer
37250
+ ].join("\n");
37251
+ }
37252
+ function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
37253
+ return new Promise((resolve8, reject) => {
37254
+ const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
37255
+ const env3 = {
37256
+ ...process.env,
37257
+ ...extraEnv,
37258
+ ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
37259
+ };
37260
+ const child = spawn8(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
37261
+ let out = "";
37262
+ let err = "";
37263
+ const timer = setTimeout(() => {
37264
+ child.kill("SIGKILL");
37265
+ reject(new Error("memory compaction timed out"));
37266
+ }, COMPACT_TIMEOUT_MS);
37267
+ timer.unref();
37268
+ child.stdout.on("data", (d) => out += d.toString());
37269
+ child.stderr.on("data", (d) => err += d.toString());
37270
+ child.on("error", (e) => {
37271
+ clearTimeout(timer);
37272
+ reject(e);
37273
+ });
37274
+ child.on("close", (code) => {
37275
+ clearTimeout(timer);
37276
+ resolve8({ code: code ?? -1, out, err });
37277
+ });
37278
+ child.stdin.write(prompt);
37279
+ child.stdin.end();
37280
+ });
37281
+ }
37282
+ async function runMemoryCompaction(ai, cwd2, input) {
37283
+ const prompt = buildPrompt3(input);
37284
+ const attempts = ai.profiles.length > 0 ? ai.profiles : [null];
37285
+ for (let i = 0; i < attempts.length; i++) {
37286
+ try {
37287
+ const { code, out } = await runOnce2(ai.cmd, attempts[i], prompt, cwd2, ai.env);
37288
+ if (code === 0 && out.trim()) {
37289
+ reportToolResult(ai.cmd, true);
37290
+ return out.trim().slice(0, input.budgetChars);
37291
+ }
37292
+ if (i === attempts.length - 1) break;
37293
+ } catch {
37294
+ break;
37295
+ }
37296
+ }
37297
+ return "";
37298
+ }
37299
+
37300
+ // src/core/config-sync.ts
37301
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
37302
+ import { join as join24 } from "path";
37303
+ var SERVER_MANAGED_KEYS = [
37304
+ "physicPath",
37305
+ "sessionSwitchPct",
37306
+ "perPromptTokenLimit",
37307
+ // Project AI-run timeout override mirror (ADR-0243) — the ws_token refreshes it; a Worker-config
37308
+ // edit must not fight it. NOTE: the machine-user's own `aiRunTimeoutSec` is NOT here (operator-editable).
37309
+ "projectAiRunTimeoutSec",
37310
+ // Project idle auto-clear override mirror (ADR-0244) — same rule; the machine-user's own
37311
+ // `autoClearIdleMinutes` is operator-editable and NOT listed here.
37312
+ "projectAutoClearIdleMinutes",
37313
+ // Project shared-AI-memory override mirrors (ADR-0245) — the machine-user's own `aiMemoryEnabled`
37314
+ // / `aiMemoryBudgetChars` are operator-editable and NOT listed here.
37315
+ "projectAiMemoryMode",
37316
+ "projectAiMemoryBudgetChars"
37317
+ ];
37318
+ function readConfigText(profileDir2) {
37319
+ const path = join24(profileDir2, "config.json");
37320
+ if (existsSync12(path)) {
37321
+ try {
37322
+ return readFileSync15(path, "utf8");
37323
+ } catch {
37324
+ }
37325
+ }
37326
+ return JSON.stringify(defaultProfileConfig(), null, 2);
37327
+ }
37328
+ function applyConfigText(profileDir2, text) {
37329
+ let parsed;
37330
+ try {
37331
+ parsed = JSON.parse(text);
37332
+ } catch (err) {
37333
+ return { ok: false, error: `Invalid JSON: ${err.message}` };
37334
+ }
37335
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
37336
+ return { ok: false, error: "config.json must be a JSON object." };
37337
+ }
37338
+ const next = { ...parsed };
37339
+ const current = readProfileConfig(profileDir2);
37340
+ for (const key of SERVER_MANAGED_KEYS) {
37341
+ if (current[key] !== void 0) next[key] = current[key];
37342
+ else delete next[key];
37343
+ }
37344
+ overwriteProfileConfig(profileDir2, next);
37345
+ return { ok: true };
37346
+ }
37347
+
37348
+ // src/core/worker-tools.ts
37349
+ import { spawn as spawn9 } from "child_process";
37350
+ var NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
37351
+ function run10(cmd, args, onLine, timeoutMs = 18e4) {
37352
+ return new Promise((resolve8) => {
37353
+ let out = "";
37354
+ let buf = "";
37355
+ let child = null;
37356
+ const done = (code) => {
37357
+ clearTimeout(timer);
37358
+ if (onLine && buf.trim()) onLine(buf.trim());
37359
+ resolve8({ code, out });
37360
+ };
37361
+ const timer = setTimeout(() => {
37362
+ child?.kill();
36803
37363
  if (onLine) onLine(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
36804
37364
  done(124);
36805
37365
  }, timeoutMs);
@@ -36927,7 +37487,16 @@ var CONSOLE_WATCH_LEASE_MS = 45e3;
36927
37487
  var METRICS_SAMPLE_INTERVAL_MS = 5e3;
36928
37488
  var METRICS_WATCH_LEASE_MS = 45e3;
36929
37489
  function toDtoEntry(e) {
36930
- return { id: e.id, source: e.source, kind: e.kind, text: e.text, level: e.level, resultKind: e.resultKind };
37490
+ return {
37491
+ id: e.id,
37492
+ source: e.source,
37493
+ kind: e.kind,
37494
+ text: e.text,
37495
+ level: e.level,
37496
+ resultKind: e.resultKind,
37497
+ // Processing time on a terminal `exit` entry (ADR-0249) — the web renders it inline.
37498
+ ...e.durationMs != null ? { durationMs: e.durationMs } : {}
37499
+ };
36931
37500
  }
36932
37501
  var WsClient = class {
36933
37502
  constructor(context) {
@@ -36992,8 +37561,13 @@ var WsClient = class {
36992
37561
  reconnectEntryId = null;
36993
37562
  /** Org-configured reconnect cap (seconds) from the last ws_token — overrides env. */
36994
37563
  orgReconnectMaxSec = null;
37564
+ /** Org timezone (IANA) from the last ws_token (ADR-0132) — a rented worker follows its renter's
37565
+ * org; used to stamp the AI-run start time on the `aireq` transcript marker (ADR-0249). */
37566
+ orgTimezone = "UTC";
36995
37567
  /** Env override for the reconnect cap (seconds); resolved once at construction. */
36996
37568
  envReconnectMaxSec = envReconnectMaxSec();
37569
+ /** Process start (ms) — uptime for a web-dispatched `/status` (ADR-0249). */
37570
+ startedAtMs = Date.now();
36997
37571
  credential;
36998
37572
  /** The physic project folder root this cli serves (null = no project) — fs.list is
36999
37573
  * scoped to it so the web FsPicker can only browse inward, never out. */
@@ -37174,6 +37748,9 @@ var WsClient = class {
37174
37748
  if (typeof token.reconnectMaxBackoffSec === "number") {
37175
37749
  this.orgReconnectMaxSec = token.reconnectMaxBackoffSec;
37176
37750
  }
37751
+ if (typeof token.timezone === "string" && token.timezone) {
37752
+ this.orgTimezone = token.timezone;
37753
+ }
37177
37754
  if (typeof token.autoUpdateDaily === "boolean") {
37178
37755
  this.updateScheduler.configure({
37179
37756
  autoUpdateDaily: token.autoUpdateDaily,
@@ -37351,8 +37928,8 @@ var WsClient = class {
37351
37928
  const oldRoot = this.physicFolderPath(sync.oldName);
37352
37929
  if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
37353
37930
  }
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 });
37931
+ if (sync.oldName) rmSync4(join25(this.context.profileDir, sync.oldName), { recursive: true, force: true });
37932
+ mkdirSync5(join25(this.context.profileDir, sync.newName), { recursive: true });
37356
37933
  this.physicRoot = this.physicFolderPath(sync.newName);
37357
37934
  this.bus.setProject(sync.newName);
37358
37935
  this.bus.log(`Physic project folder synced \u2192 ${sync.newName}`);
@@ -37363,7 +37940,7 @@ var WsClient = class {
37363
37940
  if (del.name) {
37364
37941
  const delRoot = this.physicFolderPath(del.name);
37365
37942
  if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
37366
- rmSync4(join24(this.context.profileDir, del.name), { recursive: true, force: true });
37943
+ rmSync4(join25(this.context.profileDir, del.name), { recursive: true, force: true });
37367
37944
  this.bus.log(`Physic project folder deleted (project removed) \u2192 ${del.name}`);
37368
37945
  }
37369
37946
  void manageSshKey("delete", this.context.profileDir).catch(() => void 0);
@@ -37384,7 +37961,11 @@ var WsClient = class {
37384
37961
  ai: dispatch.ai ?? false
37385
37962
  });
37386
37963
  if (dispatch.ai) {
37387
- void this.runAiPrompt(dispatch.cmd, dispatch.commandId, "server");
37964
+ if (dispatch.cmd.trimStart().startsWith("/")) {
37965
+ void this.runWebSlashCommand(dispatch.cmd, dispatch.commandId);
37966
+ } else {
37967
+ void this.runAiPrompt(dispatch.cmd, dispatch.commandId, "server", dispatch.aiOneShot ?? false);
37968
+ }
37388
37969
  break;
37389
37970
  }
37390
37971
  recordCommand({
@@ -37399,6 +37980,7 @@ var WsClient = class {
37399
37980
  text: `$ ${dispatch.cmd} ${dispatch.args.join(" ")}`.trimEnd()
37400
37981
  });
37401
37982
  this.bus.startBusy(dispatch.cmd);
37983
+ const cmdStartMs = Date.now();
37402
37984
  void runCommand(dispatch, (out) => {
37403
37985
  if (out.chunk) {
37404
37986
  this.bus.push({ source: "server", kind: "out", text: out.chunk });
@@ -37413,7 +37995,8 @@ var WsClient = class {
37413
37995
  source: "server",
37414
37996
  kind: "exit",
37415
37997
  text: code === 0 ? "\u2713 done" : `\u2717 failed (exit ${code})`,
37416
- level: code === 0 ? "info" : "error"
37998
+ level: code === 0 ? "info" : "error",
37999
+ durationMs: Date.now() - cmdStartMs
37417
38000
  });
37418
38001
  }
37419
38002
  });
@@ -37736,7 +38319,7 @@ var WsClient = class {
37736
38319
  }
37737
38320
  case WsChannels.LOG_READ: {
37738
38321
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
37739
- const lines = readRecentLogLines(join24(this.context.profileDir, "logs"), limit);
38322
+ const lines = readRecentLogLines(join25(this.context.profileDir, "logs"), limit);
37740
38323
  this.send(WsChannels.LOG_READ, { lines }, message.id);
37741
38324
  break;
37742
38325
  }
@@ -37815,7 +38398,7 @@ var WsClient = class {
37815
38398
  * exactly like typing the prompt in the cli (tries profiles, meters real tokens) instead
37816
38399
  * of spawning the text as a raw command. Streams output to the transcript AND the server.
37817
38400
  */
37818
- async runAiPrompt(prompt, commandId, origin) {
38401
+ async runAiPrompt(prompt, commandId, origin, oneShot = false) {
37819
38402
  const config = readProfileConfig(this.context.profileDir);
37820
38403
  const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
37821
38404
  const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
@@ -37855,9 +38438,10 @@ var WsClient = class {
37855
38438
  const unified = isUnifiedConfig(config);
37856
38439
  const mode = config.aiFailoverMode ?? "remember";
37857
38440
  const workingDir = unified ? null : getWorkingProfile(this.context.profileDir, cmd);
37858
- const workingCred = unified && mode === "remember" ? getWorkingCredential(this.context.profileDir) : null;
38441
+ const pinnedCred = unified ? getPinnedCredential(this.context.profileDir) : null;
38442
+ const workingCred = unified ? pinnedCred ?? (mode === "remember" ? getWorkingCredential(this.context.profileDir) : null) : null;
37859
38443
  const startDir = unified ? null : this.profileUnderSessionPressure(config, cmd, workingDir);
37860
- const startCred = unified ? this.credentialUnderSessionPressure(config, workingCred) : null;
38444
+ const startCred = unified && !pinnedCred ? this.credentialUnderSessionPressure(config, workingCred) : null;
37861
38445
  if (startDir && startDir !== workingDir) {
37862
38446
  this.bus.push({
37863
38447
  source: origin,
@@ -37887,9 +38471,10 @@ ${this.aiMemory}
37887
38471
 
37888
38472
  ${guardedPrompt}`;
37889
38473
  }
37890
- const plan = planAiRun(effectivePrompt, config, hint, resume);
38474
+ const plan = planAiRun(effectivePrompt, config, hint, resume, oneShot);
37891
38475
  const markerArgs = plan.attempts[0]?.args ?? [];
37892
- this.bus.push({ source: origin, kind: "aireq", text: `${plan.cmd} \u2039 ${prompt}` });
38476
+ const startStamp = formatTimestampInZone(/* @__PURE__ */ new Date(), this.orgTimezone);
38477
+ this.bus.push({ source: origin, kind: "aireq", text: `${startStamp} ${plan.cmd} \u2039 ${prompt}` });
37893
38478
  logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
37894
38479
  if (origin === "local") {
37895
38480
  this.send(WsChannels.COMMAND_ANNOUNCE, {
@@ -37903,6 +38488,7 @@ ${guardedPrompt}`;
37903
38488
  }
37904
38489
  recordCommand({ commandId, cmd: plan.cmd, args: markerArgs });
37905
38490
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
38491
+ const aiStartedMs = Date.now();
37906
38492
  this.bus.startBusy(plan.cmd);
37907
38493
  let seq = 0;
37908
38494
  let responseHeaderShown = false;
@@ -37946,7 +38532,7 @@ ${guardedPrompt}`;
37946
38532
  `, log: true });
37947
38533
  },
37948
38534
  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`;
38535
+ 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
38536
  this.bus.push({ source: origin, kind: "log", text, level: "warn" });
37951
38537
  this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
37952
38538
  `, log: true });
@@ -38019,7 +38605,8 @@ ${guardedPrompt}`;
38019
38605
  source: origin,
38020
38606
  kind: "exit",
38021
38607
  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"
38608
+ level: result.exitCode === 0 ? "info" : "error",
38609
+ durationMs: Date.now() - aiStartedMs
38023
38610
  });
38024
38611
  if (memCfg.enabled && result.exitCode === 0) {
38025
38612
  if (result.workedKey && result.sessionId) {
@@ -38029,6 +38616,116 @@ ${guardedPrompt}`;
38029
38616
  });
38030
38617
  }
38031
38618
  }
38619
+ /**
38620
+ * Run a `/…` 4pm-cli slash command dispatched from the web Console (ADR-0249). Mirrors the TUI's
38621
+ * `runSlashCommand` with a **server-origin** context that streams `print` output back over
38622
+ * command.output + console.sync, so a web user gets the same commands as an operator at the
38623
+ * machine — EXCEPT any the operator disabled via `webBlockedCommands`. `/claude-cmd <x>` forwards
38624
+ * `x` to the AI CLI (a real AI run). Fold ops (`/expand`/`/collapse`) are TUI-only — the web has
38625
+ * its own fold viewer — so they just print a hint. Interactive `confirm` (e.g. `/config init`) is
38626
+ * not supported from the web.
38627
+ */
38628
+ async runWebSlashCommand(line, commandId) {
38629
+ const origin = "server";
38630
+ const config = readProfileConfig(this.context.profileDir);
38631
+ const trimmed = line.trim();
38632
+ const startedMs = Date.now();
38633
+ this.bus.push({ source: origin, kind: "cmd", text: trimmed });
38634
+ recordCommand({ commandId, cmd: trimmed, args: [] });
38635
+ let seq = 0;
38636
+ const emit = (text) => {
38637
+ appendCommandOutput(commandId, `${text}
38638
+ `);
38639
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
38640
+ ` });
38641
+ };
38642
+ const settle = (exitCode) => {
38643
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: "", done: true, exitCode });
38644
+ finishCommand(commandId, exitCode);
38645
+ this.send(WsChannels.COMMAND_HISTORY, {
38646
+ commandId,
38647
+ cmd: trimmed,
38648
+ args: [],
38649
+ status: exitCode === 0 ? "done" : "failed",
38650
+ exitCode
38651
+ });
38652
+ this.bus.push({
38653
+ source: origin,
38654
+ kind: "exit",
38655
+ text: exitCode === 0 ? "\u2713 done" : `\u2717 failed (exit ${exitCode})`,
38656
+ level: exitCode === 0 ? "info" : "error",
38657
+ durationMs: Date.now() - startedMs
38658
+ });
38659
+ };
38660
+ const rawName = trimmed.replace(/^\//, "").trim().split(/\s+/)[0]?.toLowerCase() ?? "";
38661
+ const name = rawName === "exit" ? "quit" : rawName;
38662
+ const blocked = resolveWebBlockedCommands(config);
38663
+ if (blocked.has(name) || blocked.has(rawName)) {
38664
+ const msg = `Command /${rawName} is disabled on this worker (blocked by the machine config).`;
38665
+ this.bus.push({ source: origin, kind: "log", text: msg, level: "error" });
38666
+ emit(msg);
38667
+ settle(1);
38668
+ return;
38669
+ }
38670
+ if (name === "claude-cmd") {
38671
+ const rest = trimmed.replace(/^\/claude-cmd\s*/i, "").trim();
38672
+ if (!rest) {
38673
+ emit("usage: /claude-cmd /context (forwards to the AI CLI)");
38674
+ settle(1);
38675
+ return;
38676
+ }
38677
+ await this.runAiPrompt(rest, commandId, origin, false);
38678
+ return;
38679
+ }
38680
+ const info = {
38681
+ version: CLI_VERSION,
38682
+ scope: this.bus.scope ?? "project",
38683
+ profile: basename4(this.context.profileDir),
38684
+ profileDir: this.context.profileDir,
38685
+ serverUrl: this.context.credential.serverUrl,
38686
+ physicPath: this.physicRoot ?? config.physicPath ?? null,
38687
+ aiCli: config.aiCli || "claude",
38688
+ whoami: async () => null
38689
+ };
38690
+ runSlashCommand(trimmed, {
38691
+ info,
38692
+ print: (text, level) => {
38693
+ this.bus.push({ source: origin, kind: "log", text, level });
38694
+ emit(text);
38695
+ },
38696
+ clear: () => {
38697
+ this.bus.clear();
38698
+ this.bus.clearSession();
38699
+ },
38700
+ quit: () => {
38701
+ emit("Quit requested from the web console \u2014 the worker cli is shutting down.");
38702
+ setTimeout(() => process.exit(0), 200);
38703
+ },
38704
+ confirm: (prompt) => {
38705
+ const note = `${prompt} \u2014 interactive prompts aren't supported from the web console; run it on the machine.`;
38706
+ this.bus.push({ source: origin, kind: "log", text: note, level: "warn" });
38707
+ emit(note);
38708
+ },
38709
+ submitAi: () => {
38710
+ },
38711
+ // only /claude-cmd uses this, and it is special-cased above
38712
+ // /ai-profile use/reset from the web Console updates the shared header too (ADR-0250).
38713
+ setActiveProfile: (label) => this.bus.setActiveProfile(label),
38714
+ reconnect: () => this.reconnectNow(),
38715
+ expand: () => emit("Use the web console's fold viewer to expand blocks."),
38716
+ collapse: () => emit("Use the web console's fold viewer to collapse blocks."),
38717
+ maxBlock: 0,
38718
+ live: {
38719
+ status: this.bus.status,
38720
+ scope: this.bus.scope ?? "",
38721
+ worker: this.bus.worker,
38722
+ project: this.bus.project,
38723
+ activeProfile: this.bus.activeProfile,
38724
+ startedAt: this.startedAtMs
38725
+ }
38726
+ });
38727
+ settle(0);
38728
+ }
38032
38729
  /**
38033
38730
  * Fold the latest exchange into the shared AI memory (ADR-0245): a background claude compaction to a
38034
38731
  * budget-bounded summary, cached locally + written back to the server (`memory.update`). Best-effort —
@@ -38380,7 +39077,7 @@ ${guardedPrompt}`;
38380
39077
  /** Create the physic folder at an absolute path if missing (best-effort). */
38381
39078
  ensurePhysicFolderPath(folder) {
38382
39079
  try {
38383
- if (!existsSync12(folder)) {
39080
+ if (!existsSync13(folder)) {
38384
39081
  mkdirSync5(folder, { recursive: true });
38385
39082
  this.bus.log(`Created physic project folder: ${folder}`);
38386
39083
  }
@@ -38395,7 +39092,7 @@ ${guardedPrompt}`;
38395
39092
  */
38396
39093
  physicFolderPath(projectName) {
38397
39094
  const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
38398
- return safe ? join24(this.context.profileDir, safe) : null;
39095
+ return safe ? join25(this.context.profileDir, safe) : null;
38399
39096
  }
38400
39097
  /**
38401
39098
  * Report machine.status: version + the state of the physic project being served
@@ -38409,7 +39106,7 @@ ${guardedPrompt}`;
38409
39106
  projects: [],
38410
39107
  version: CLI_VERSION,
38411
39108
  physicPath,
38412
- physicPathExists: physicPath ? existsSync12(physicPath) : void 0,
39109
+ physicPathExists: physicPath ? existsSync13(physicPath) : void 0,
38413
39110
  // Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
38414
39111
  autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false
38415
39112
  });
@@ -38499,7 +39196,7 @@ ${guardedPrompt}`;
38499
39196
  */
38500
39197
  uploadLog() {
38501
39198
  try {
38502
- const upload = readLogUpload(join24(this.context.profileDir, "logs"));
39199
+ const upload = readLogUpload(join25(this.context.profileDir, "logs"));
38503
39200
  if (!upload) return;
38504
39201
  const sig = `${upload.fileName}:${upload.totalBytes}`;
38505
39202
  if (sig === this.lastLogSig) return;
@@ -38528,9 +39225,9 @@ ${guardedPrompt}`;
38528
39225
 
38529
39226
  // src/core/instance-lock.ts
38530
39227
  import { readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
38531
- import { join as join25 } from "path";
39228
+ import { join as join26 } from "path";
38532
39229
  function lockPath(profileDir2) {
38533
- return join25(profileDir2, "start.lock");
39230
+ return join26(profileDir2, "start.lock");
38534
39231
  }
38535
39232
  function isAlive(pid) {
38536
39233
  try {
@@ -38830,7 +39527,7 @@ var SessionBus = class {
38830
39527
  // src/core/control-server.ts
38831
39528
  import { createServer } from "net";
38832
39529
  import { rmSync as rmSync6 } from "fs";
38833
- import { join as join26 } from "path";
39530
+ import { join as join27 } from "path";
38834
39531
 
38835
39532
  // src/core/control-protocol.ts
38836
39533
  var CONTROL_SOCKET_FILE = "control.sock";
@@ -38859,7 +39556,7 @@ function createFrameParser() {
38859
39556
 
38860
39557
  // src/core/control-server.ts
38861
39558
  function startControlServer(bus, profileDir2, info) {
38862
- const socketPath = join26(profileDir2, CONTROL_SOCKET_FILE);
39559
+ const socketPath = join27(profileDir2, CONTROL_SOCKET_FILE);
38863
39560
  try {
38864
39561
  rmSync6(socketPath, { force: true });
38865
39562
  } catch {
@@ -44590,15 +45287,15 @@ var import_react21 = __toESM(require_react(), 1);
44590
45287
  var import_react26 = __toESM(require_react(), 1);
44591
45288
 
44592
45289
  // 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";
45290
+ import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
45291
+ import { join as join28 } from "path";
44595
45292
  var MAX_HISTORY = 200;
44596
45293
  function historyPath(profileDir2) {
44597
- return join27(profileDir2, "input-history.json");
45294
+ return join28(profileDir2, "input-history.json");
44598
45295
  }
44599
45296
  function loadInputHistory(profileDir2) {
44600
45297
  const path = historyPath(profileDir2);
44601
- if (!existsSync14(path)) return [];
45298
+ if (!existsSync15(path)) return [];
44602
45299
  try {
44603
45300
  const data = JSON.parse(readFileSync18(path, "utf8"));
44604
45301
  return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
@@ -44720,12 +45417,17 @@ function textColor(line) {
44720
45417
  if (line.kind === "exit") return "green";
44721
45418
  return void 0;
44722
45419
  }
45420
+ var START_STAMP_RE = /^(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2} )/;
44723
45421
  function AiMarkerLine({ line }) {
44724
45422
  const arrow = line.kind === "aireq" ? "\u2039" : "\u203A";
44725
45423
  const idx = line.text.indexOf(arrow);
44726
45424
  if (idx < 0) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text || " " });
45425
+ const before = line.text.slice(0, idx);
45426
+ const stamp = line.kind === "aireq" ? START_STAMP_RE.exec(before)?.[1] ?? "" : "";
45427
+ const name = stamp ? before.slice(stamp.length) : before;
44727
45428
  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) }),
45429
+ stamp ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: stamp }) : null,
45430
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "cyan", bold: true, children: name }),
44729
45431
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: arrow }),
44730
45432
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text.slice(idx + 1) })
44731
45433
  ] });
@@ -44750,438 +45452,114 @@ function wrapToWidth(text, width) {
44750
45452
  line = word.trimStart();
44751
45453
  } else {
44752
45454
  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;
45455
+ }
45006
45456
  }
45007
- for (const line of out.replace(/\n$/, "").split("\n")) ctx.print(line);
45457
+ if (line) rows.push(line.trimEnd());
45458
+ return rows.length > 0 ? rows : [""];
45008
45459
  }
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;
45460
+ var DETAIL_RE = /<detail\s+title="([^"]*)">([\s\S]*?)<\/detail>/g;
45461
+ function toSegments(raw) {
45462
+ if (!raw.includes("<detail")) {
45463
+ return raw.split("\n").map((text) => ({ text, collapsed: false }));
45016
45464
  }
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
- }
45465
+ const out = [];
45466
+ const re = new RegExp(DETAIL_RE);
45467
+ let last = 0;
45468
+ let m;
45469
+ while ((m = re.exec(raw)) !== null) {
45470
+ const before = raw.slice(last, m.index).replace(/\n$/, "");
45471
+ if (before.length > 0) for (const p of before.split("\n")) out.push({ text: p, collapsed: false });
45472
+ out.push({ text: m[1] || "details", collapsed: true, body: m[2] ?? "" });
45473
+ last = m.index + m[0].length;
45474
+ if (raw[last] === "\n") last += 1;
45034
45475
  }
45476
+ const rest = raw.slice(last);
45477
+ if (rest.length > 0) for (const p of rest.split("\n")) out.push({ text: p, collapsed: false });
45478
+ if (out.length === 0) out.push({ text: "", collapsed: false });
45479
+ return out;
45035
45480
  }
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)"}`);
45481
+ function pushCollapsible(lines, entry, opts) {
45482
+ const { blockNo, title, body, expanded, firstRow, textWidth, keyBase } = opts;
45483
+ const base = { source: entry.source, level: entry.level, kind: entry.kind };
45484
+ lines.push({
45485
+ ...base,
45486
+ key: `${keyBase}:m`,
45487
+ text: `${expanded ? "\u25BE" : "\u25B8"}[${blockNo}] ${title}`,
45488
+ showTag: firstRow,
45489
+ collapsed: true,
45490
+ spacer: false,
45491
+ blockIndex: blockNo
45046
45492
  });
45493
+ if (!expanded) return;
45494
+ prettyResult(body).split("\n").forEach(
45495
+ (seg, i) => wrapToWidth(seg, textWidth).forEach(
45496
+ (row, j) => lines.push({ ...base, key: `${keyBase}:b:${i}:${j}`, text: row, showTag: false, collapsed: false, spacer: false })
45497
+ )
45498
+ );
45047
45499
  }
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}`);
45500
+ function flattenEntries(entries2, textWidth, expanded = /* @__PURE__ */ new Set()) {
45501
+ const lines = [];
45502
+ let prevKey = null;
45503
+ let blockNo = 0;
45504
+ for (const entry of entries2) {
45505
+ const blockKey = `${entry.source}|${entry.kind}`;
45506
+ const newBlock = blockKey !== prevKey;
45507
+ if (newBlock && lines.length > 0) {
45508
+ lines.push({
45509
+ key: `sp:${entry.id}`,
45510
+ source: entry.source,
45511
+ level: entry.level,
45512
+ kind: entry.kind,
45513
+ text: "",
45514
+ showTag: false,
45515
+ collapsed: false,
45516
+ spacer: true
45517
+ });
45069
45518
  }
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}`);
45519
+ prevKey = blockKey;
45520
+ let firstRow = newBlock;
45521
+ if (entry.kind === "result") {
45522
+ blockNo += 1;
45523
+ const count = prettyResult(entry.text).split("\n").length;
45524
+ const title = `result \xB7 ${entry.resultKind ?? "json"} \xB7 ${count} ${count === 1 ? "line" : "lines"}`;
45525
+ pushCollapsible(lines, entry, { blockNo, title, body: entry.text, expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: entry.id });
45526
+ continue;
45093
45527
  }
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");
45528
+ const raw = entry.kind === "out" ? entry.text.replace(/\n$/, "") : entry.text;
45529
+ toSegments(raw).forEach((seg, i) => {
45530
+ if (seg.collapsed) {
45531
+ blockNo += 1;
45532
+ pushCollapsible(lines, entry, { blockNo, title: seg.text, body: seg.body ?? "", expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: `${entry.id}:${i}` });
45533
+ firstRow = false;
45151
45534
  return;
45152
45535
  }
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;
45536
+ wrapToWidth(seg.text, textWidth).forEach((row, j) => {
45537
+ lines.push({
45538
+ key: `${entry.id}:${i}:${j}`,
45539
+ source: entry.source,
45540
+ level: entry.level,
45541
+ kind: entry.kind,
45542
+ text: row,
45543
+ showTag: firstRow,
45544
+ collapsed: false,
45545
+ spacer: false
45546
+ });
45547
+ firstRow = false;
45548
+ });
45549
+ });
45180
45550
  }
45181
- command.run(ctx);
45551
+ return lines;
45552
+ }
45553
+ function TranscriptLine({ line }) {
45554
+ const tag = sourceTag(line.source);
45555
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Box_default, { flexDirection: "row", children: [
45556
+ /* @__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 }),
45557
+ /* @__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 || " " }) })
45558
+ ] });
45182
45559
  }
45183
45560
 
45184
45561
  // src/ui/command-input.tsx
45562
+ var import_react24 = __toESM(require_react(), 1);
45185
45563
  var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
45186
45564
  function Hint() {
45187
45565
  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 +45844,9 @@ function App2({
45466
45844
  setPendingConfirm({ prompt, onYes });
45467
45845
  },
45468
45846
  submitAi: (aiInput) => bus.submitLocal(aiInput),
45847
+ // /ai-profile use/reset updates the header's active-profile label at once (ADR-0250);
45848
+ // the bus event feeds the React state (see the onActiveProfile subscription above).
45849
+ setActiveProfile: (label) => bus.setActiveProfile(label),
45469
45850
  reconnect: () => bus.requestReconnect(),
45470
45851
  expand: expandFold,
45471
45852
  collapse: collapseFold,