@4pm/cli 1.5.14-hotfix → 1.5.15

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 +422 -197
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28599,7 +28599,7 @@ async function runLink(serverUrl, explicitProfile, token = null) {
28599
28599
 
28600
28600
  // src/commands/start.ts
28601
28601
  import { spawn as spawn10 } from "child_process";
28602
- import { join as join29 } from "path";
28602
+ import { join as join30 } from "path";
28603
28603
 
28604
28604
  // src/core/update.ts
28605
28605
  import { execSync as execSync2 } from "child_process";
@@ -28775,7 +28775,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
28775
28775
  // src/core/ws-client.ts
28776
28776
  init_wrapper();
28777
28777
  import { randomUUID } from "crypto";
28778
- import { basename as basename4, join as join25 } from "path";
28778
+ import { basename as basename4, join as join26 } from "path";
28779
28779
 
28780
28780
  // vendor/@4pm/utils/helpers.ts
28781
28781
  function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
@@ -32913,6 +32913,29 @@ var deletedFilterSchema = external_exports.enum(["true", "false"]).transform((v)
32913
32913
  // vendor/@4pm/dto/command.ts
32914
32914
  var COMMAND_MAX_LEN = 8e3;
32915
32915
  var AI_PROMPT_MAX_LEN = 5e5;
32916
+ var COMMAND_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
32917
+ var COMMAND_IMAGE_MAX_COUNT = 10;
32918
+ var COMMAND_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
32919
+ function commandImageExt(mime) {
32920
+ switch (mime) {
32921
+ case "image/png":
32922
+ return "png";
32923
+ case "image/jpeg":
32924
+ return "jpg";
32925
+ case "image/webp":
32926
+ return "webp";
32927
+ case "image/gif":
32928
+ return "gif";
32929
+ default:
32930
+ return "bin";
32931
+ }
32932
+ }
32933
+ var commandImageRefSchema = external_exports.object({
32934
+ id: external_exports.string().min(1).max(200),
32935
+ placeholder: external_exports.string().min(1).max(40),
32936
+ name: external_exports.string().max(255),
32937
+ mime: external_exports.enum(COMMAND_IMAGE_MIME_TYPES)
32938
+ });
32916
32939
  var dispatchCommandRequestSchema = external_exports.object({
32917
32940
  projectId: external_exports.string().uuid(),
32918
32941
  // Target cli. Required unless `pick` is set — with `pick:"idle"` the server chooses the
@@ -32946,6 +32969,13 @@ var dispatchCommandRequestSchema = external_exports.object({
32946
32969
  * against the git allowlist (`isGitCommandAllowed`). Absent ⇒ normal console dispatch.
32947
32970
  */
32948
32971
  gitOp: external_exports.enum(["read", "write"]).optional(),
32972
+ /**
32973
+ * Console image attachments (ADR-0257): up to `COMMAND_IMAGE_MAX_COUNT` uploaded images the
32974
+ * prompt references by `[Image#N]` placeholders. Only meaningful with `ai:true` on a full agent
32975
+ * run (one-shot spec-assist disallows `Read`). Rejected with `IMAGE_UPLOAD_BLOCKED` when the
32976
+ * project has `outboundReview.blockImages`.
32977
+ */
32978
+ images: external_exports.array(commandImageRefSchema).max(COMMAND_IMAGE_MAX_COUNT).optional(),
32949
32979
  /**
32950
32980
  * One-shot AI mode (ADR-0249): the prompt is a text-in → text-out task (spec review /
32951
32981
  * compose / suggest / generators) that must NOT trigger the AI CLI's agentic tool loop.
@@ -33802,6 +33832,9 @@ var WsChannels = {
33802
33832
  // the worker uploads its pending log tail (+ any queued history) then acks, so the release
33803
33833
  // sweep can finalise without waiting the full 5-minute timeout when the worker is online.
33804
33834
  RENTAL_FLUSH: "rental.flush",
33835
+ // cli → server (reply): fetch a Console prompt image blob to materialize on the worker before
33836
+ // spawning the AI CLI (ADR-0257) — the server reads it from @4pm/storage (org-scoped by link).
33837
+ IMAGE_FETCH: "image.fetch",
33805
33838
  // outbound review (ADR-0082): machine→server request, server→outbound evaluate, result back
33806
33839
  REVIEW_REQUEST: "review.request",
33807
33840
  REVIEW_EVALUATE: "review.evaluate",
@@ -33871,7 +33904,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
33871
33904
  }
33872
33905
 
33873
33906
  // src/core/ws-client.ts
33874
- import { existsSync as existsSync13, mkdirSync as mkdirSync5, rmSync as rmSync4 } from "fs";
33907
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, rmSync as rmSync5 } from "fs";
33875
33908
  import { randomUUID as randomCommandId } from "crypto";
33876
33909
 
33877
33910
  // src/core/update-scheduler.ts
@@ -33983,6 +34016,35 @@ ${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
33983
34016
  // src/core/worker-tools.ts
33984
34017
  import { spawn } from "child_process";
33985
34018
  var NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
34019
+ var TOOL_INSTALL_TIMEOUT_DEFAULT_MS = 3e5;
34020
+ var TOOL_INSTALL_MAX_ATTEMPTS = 3;
34021
+ var TOOL_RETRY_BASE_MS = 5e3;
34022
+ function resolveInstallTimeoutMs(sec) {
34023
+ return sec && sec > 0 ? sec * 1e3 : TOOL_INSTALL_TIMEOUT_DEFAULT_MS;
34024
+ }
34025
+ function sleep(ms) {
34026
+ return new Promise((resolve8) => {
34027
+ const t = setTimeout(resolve8, ms);
34028
+ t.unref?.();
34029
+ });
34030
+ }
34031
+ function classifyToolFailure(code, out) {
34032
+ if (code === 124) return { reason: "timeout", retryable: true };
34033
+ const t = out.toLowerCase();
34034
+ if (/e404|not found|no such package|is not in this registry|404 not found/.test(t)) {
34035
+ return { reason: "not-found", retryable: false };
34036
+ }
34037
+ if (/etarget|no matching version|notarget/.test(t)) return { reason: "not-found", retryable: false };
34038
+ if (/unsupported_engine|npm error engine|err_pnpm_unsupported_engine/.test(t)) {
34039
+ return { reason: "engine", retryable: false };
34040
+ }
34041
+ if (/econnreset|etimedout|eai_again|enotfound|econnrefused|socket hang up|err_socket|network|network_request_failed|registry.*(50\d|timeout)|request to https?:\/\/.*failed/.test(
34042
+ t
34043
+ )) {
34044
+ return { reason: "network", retryable: true };
34045
+ }
34046
+ return { reason: "other", retryable: false };
34047
+ }
33986
34048
  function run(cmd, args, onLine, timeoutMs = 18e4) {
33987
34049
  return new Promise((resolve8) => {
33988
34050
  let out = "";
@@ -34099,13 +34161,13 @@ function opArgs(op, manager, pkg) {
34099
34161
  const target = op === "update" ? `${pkg}@latest` : pkg;
34100
34162
  return manager === "pnpm" ? ["add", "-g", target] : ["install", "-g", target];
34101
34163
  }
34102
- async function runWorkerToolOp(op, name, manager, onLine) {
34164
+ async function runWorkerToolOp(op, name, manager, onLine, timeoutMs = TOOL_INSTALL_TIMEOUT_DEFAULT_MS) {
34103
34165
  const resolved = resolvePackage(name, op);
34104
34166
  if (resolved.error || !resolved.pkg) {
34105
34167
  return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
34106
34168
  }
34107
34169
  onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
34108
- const { code } = await run(manager, opArgs(op, manager, resolved.pkg), onLine);
34170
+ const { code } = await run(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
34109
34171
  return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
34110
34172
  }
34111
34173
  function setToolAutoUpdate(profileDir2, name, enabled) {
@@ -34118,25 +34180,40 @@ function setToolAutoUpdate(profileDir2, name, enabled) {
34118
34180
  writeProfileConfig(profileDir2, { autoUpdateTools: [...set] });
34119
34181
  return { ok: true };
34120
34182
  }
34121
- async function autoUpdateFlaggedTools(tools, onLine) {
34183
+ async function autoUpdateFlaggedTools(tools, onLine, timeoutMs = TOOL_INSTALL_TIMEOUT_DEFAULT_MS) {
34122
34184
  for (const name of tools) {
34123
- const res = await runWorkerToolOp("update", name, "npm", onLine).catch(
34185
+ const res = await runWorkerToolOp("update", name, "npm", onLine, timeoutMs).catch(
34124
34186
  (err) => ({ ok: false, exitCode: 1, error: String(err) })
34125
34187
  );
34126
34188
  onLine(res.ok ? `\u2713 ${name} updated` : `\u2717 ${name}: ${res.error ?? "failed"}`);
34127
34189
  }
34128
34190
  }
34129
- async function installPinned(name, version, manager, onLine) {
34191
+ async function installPinned(name, version, manager, onLine, timeoutMs = TOOL_INSTALL_TIMEOUT_DEFAULT_MS) {
34130
34192
  const resolved = resolvePackage(name, "update");
34131
- if (resolved.error || !resolved.pkg) return { ok: false, error: resolved.error ?? "Invalid package." };
34193
+ if (resolved.error || !resolved.pkg) {
34194
+ return { ok: false, error: resolved.error ?? "Invalid package.", reason: "other", retryable: false };
34195
+ }
34132
34196
  const target = `${resolved.pkg}@${version}`;
34133
34197
  const args = manager === "pnpm" ? ["add", "-g", target] : ["install", "-g", target];
34134
- onLine(`$ ${manager} ${args.join(" ")}`);
34135
- const { code } = await run(manager, args, onLine);
34136
- return { ok: code === 0, error: code === 0 ? void 0 : `${manager} exited ${code}` };
34137
- }
34138
- async function reconcileTools(manifest, onLine) {
34139
- if (manifest.length === 0) return;
34198
+ for (let attempt = 1; attempt <= TOOL_INSTALL_MAX_ATTEMPTS; attempt++) {
34199
+ const suffix = attempt > 1 ? ` (attempt ${attempt}/${TOOL_INSTALL_MAX_ATTEMPTS})` : "";
34200
+ onLine(`$ ${manager} ${args.join(" ")}${suffix}`);
34201
+ const { code, out } = await run(manager, args, onLine, timeoutMs);
34202
+ if (code === 0) return { ok: true };
34203
+ const { reason, retryable } = classifyToolFailure(code, out);
34204
+ if (!retryable || attempt === TOOL_INSTALL_MAX_ATTEMPTS) {
34205
+ return { ok: false, error: `${manager} exited ${code}`, reason, retryable };
34206
+ }
34207
+ const base = TOOL_RETRY_BASE_MS * 3 ** (attempt - 1);
34208
+ const wait = Math.round(base * (0.8 + Math.random() * 0.4));
34209
+ onLine(`retrying in ${Math.round(wait / 1e3)}s (${reason})\u2026`);
34210
+ await sleep(wait);
34211
+ }
34212
+ return { ok: false, error: `${manager} failed`, reason: "other", retryable: false };
34213
+ }
34214
+ async function reconcileTools(manifest, onLine, timeoutMs = TOOL_INSTALL_TIMEOUT_DEFAULT_MS) {
34215
+ const failed = [];
34216
+ if (manifest.length === 0) return failed;
34140
34217
  const { catalog, extras } = await detectWorkerTools();
34141
34218
  const versionByName = new Map(
34142
34219
  [...catalog, ...extras].filter((r) => r.installed && r.version).map((r) => [r.id, r.version])
@@ -34145,11 +34222,27 @@ async function reconcileTools(manifest, onLine) {
34145
34222
  const current = versionByName.get(entry.name);
34146
34223
  if (current && current.includes(entry.version)) continue;
34147
34224
  onLine(`Restoring ${entry.name}@${entry.version}\u2026`);
34148
- const res = await installPinned(entry.name, entry.version, entry.manager, onLine).catch(
34149
- (err) => ({ ok: false, error: String(err) })
34225
+ const res = await installPinned(entry.name, entry.version, entry.manager, onLine, timeoutMs).catch(
34226
+ (err) => ({
34227
+ ok: false,
34228
+ error: String(err),
34229
+ reason: "other",
34230
+ retryable: false
34231
+ })
34150
34232
  );
34151
- onLine(res.ok ? `\u2713 ${entry.name}@${entry.version}` : `\u2717 ${entry.name}: ${res.error ?? "failed"}`);
34233
+ if (res.ok) {
34234
+ onLine(`\u2713 ${entry.name}@${entry.version}`);
34235
+ } else {
34236
+ onLine(`\u2717 ${entry.name}: ${res.error ?? "failed"}`);
34237
+ failed.push({
34238
+ name: entry.name,
34239
+ version: entry.version,
34240
+ reason: res.reason ?? "other",
34241
+ retryable: res.retryable ?? false
34242
+ });
34243
+ }
34152
34244
  }
34245
+ return failed;
34153
34246
  }
34154
34247
 
34155
34248
  // src/core/update-scheduler.ts
@@ -34263,10 +34356,12 @@ var UpdateScheduler = class {
34263
34356
  }
34264
34357
  /** Update each tool flagged for auto-update in config.json to @latest (ADR-0253) — best-effort. */
34265
34358
  async updateFlaggedTools() {
34266
- const tools = readProfileConfig(this.profileDir).autoUpdateTools ?? [];
34359
+ const config = readProfileConfig(this.profileDir);
34360
+ const tools = config.autoUpdateTools ?? [];
34267
34361
  if (tools.length === 0) return;
34268
34362
  this.bus.log(`Scheduled tool auto-update: ${tools.join(", ")}\u2026`);
34269
- await autoUpdateFlaggedTools(tools, (line) => logger.info("update.tool.line", { line })).catch(
34363
+ const timeoutMs = resolveInstallTimeoutMs(config.toolInstallTimeoutSec);
34364
+ await autoUpdateFlaggedTools(tools, (line) => logger.info("update.tool.line", { line }), timeoutMs).catch(
34270
34365
  (err) => logger.warn("update.tool.error", { error: String(err) })
34271
34366
  );
34272
34367
  this.onToolsChanged?.();
@@ -35251,23 +35346,86 @@ function finishCommand(commandId, exitCode, usage) {
35251
35346
  persist();
35252
35347
  }
35253
35348
 
35349
+ // src/core/command-images.ts
35350
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
35351
+ import { join as join11 } from "path";
35352
+ function attachmentsBaseDir(physicRoot) {
35353
+ return join11(physicRoot, ".4pm", "attachments");
35354
+ }
35355
+ var ATTACHMENT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
35356
+ async function materializeImages(physicRoot, commandId, images, fetch2) {
35357
+ const map = /* @__PURE__ */ new Map();
35358
+ if (!images.length) return map;
35359
+ const dir2 = join11(attachmentsBaseDir(physicRoot), commandId);
35360
+ mkdirSync3(dir2, { recursive: true });
35361
+ let n = 0;
35362
+ for (const img of images) {
35363
+ n += 1;
35364
+ try {
35365
+ const reply = await fetch2(img.id);
35366
+ if (!reply.dataBase64) {
35367
+ logger.warn("command.image.fetch.empty", { commandId, imageId: img.id, error: reply.error });
35368
+ continue;
35369
+ }
35370
+ const ext = commandImageExt(reply.mime ?? img.mime);
35371
+ const file = join11(dir2, `img-${n}.${ext}`);
35372
+ writeFileSync6(file, Buffer.from(reply.dataBase64, "base64"));
35373
+ map.set(img.placeholder, file);
35374
+ } catch (err) {
35375
+ logger.warn("command.image.fetch.failed", {
35376
+ commandId,
35377
+ imageId: img.id,
35378
+ error: err instanceof Error ? err.message : String(err)
35379
+ });
35380
+ }
35381
+ }
35382
+ return map;
35383
+ }
35384
+ function rewriteImagePlaceholders(prompt, paths) {
35385
+ let out = prompt;
35386
+ for (const [placeholder, file] of paths) {
35387
+ out = out.split(placeholder).join(`\`${file}\``);
35388
+ }
35389
+ return out;
35390
+ }
35391
+ function sweepOldAttachments(physicRoot, maxAgeMs = ATTACHMENT_MAX_AGE_MS) {
35392
+ const base = attachmentsBaseDir(physicRoot);
35393
+ if (!existsSync7(base)) return 0;
35394
+ const cutoff = Date.now() - maxAgeMs;
35395
+ let removed = 0;
35396
+ try {
35397
+ for (const entry of readdirSync3(base)) {
35398
+ const dir2 = join11(base, entry);
35399
+ try {
35400
+ if (statSync2(dir2).mtimeMs < cutoff) {
35401
+ rmSync3(dir2, { recursive: true, force: true });
35402
+ removed += 1;
35403
+ }
35404
+ } catch {
35405
+ }
35406
+ }
35407
+ } catch {
35408
+ }
35409
+ return removed;
35410
+ }
35411
+
35254
35412
  // src/core/command-output-store.ts
35255
35413
  import {
35256
35414
  appendFileSync,
35257
- existsSync as existsSync7,
35258
- mkdirSync as mkdirSync3,
35415
+ existsSync as existsSync8,
35416
+ mkdirSync as mkdirSync4,
35259
35417
  readFileSync as readFileSync11,
35260
- readdirSync as readdirSync3,
35261
- rmSync as rmSync3,
35262
- statSync as statSync2
35418
+ readdirSync as readdirSync4,
35419
+ rmSync as rmSync4,
35420
+ statSync as statSync3
35263
35421
  } from "fs";
35264
- import { join as join11 } from "path";
35422
+ import { join as join12 } from "path";
35265
35423
  var KEEP_FILES = 200;
35266
35424
  var dir = null;
35267
35425
  function initCommandOutput(profileDir2) {
35268
- dir = join11(profileDir2, "command-output");
35426
+ dir = join12(profileDir2, "command-output");
35269
35427
  try {
35270
- mkdirSync3(dir, { recursive: true });
35428
+ mkdirSync4(dir, { recursive: true });
35271
35429
  prune();
35272
35430
  } catch {
35273
35431
  }
@@ -35275,14 +35433,14 @@ function initCommandOutput(profileDir2) {
35275
35433
  function appendCommandOutput(commandId, text) {
35276
35434
  if (!dir || !text) return;
35277
35435
  try {
35278
- appendFileSync(join11(dir, `${commandId}.log`), text, "utf8");
35436
+ appendFileSync(join12(dir, `${commandId}.log`), text, "utf8");
35279
35437
  } catch {
35280
35438
  }
35281
35439
  }
35282
35440
  function readCommandOutput(commandId) {
35283
35441
  if (!dir) return null;
35284
- const path = join11(dir, `${commandId}.log`);
35285
- if (!existsSync7(path)) return null;
35442
+ const path = join12(dir, `${commandId}.log`);
35443
+ if (!existsSync8(path)) return null;
35286
35444
  try {
35287
35445
  return readFileSync11(path, "utf8");
35288
35446
  } catch {
@@ -35292,8 +35450,8 @@ function readCommandOutput(commandId) {
35292
35450
  function prune() {
35293
35451
  if (!dir) return;
35294
35452
  try {
35295
- const files = readdirSync3(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, t: statSync2(join11(dir, f)).mtimeMs })).sort((a, b) => b.t - a.t);
35296
- for (const { f } of files.slice(KEEP_FILES)) rmSync3(join11(dir, f), { force: true });
35453
+ const files = readdirSync4(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, t: statSync3(join12(dir, f)).mtimeMs })).sort((a, b) => b.t - a.t);
35454
+ for (const { f } of files.slice(KEEP_FILES)) rmSync4(join12(dir, f), { force: true });
35297
35455
  } catch {
35298
35456
  }
35299
35457
  }
@@ -35301,9 +35459,9 @@ function pruneCommandOutputByAge(days) {
35301
35459
  if (!dir || days <= 0) return;
35302
35460
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
35303
35461
  try {
35304
- for (const f of readdirSync3(dir).filter((n) => n.endsWith(".log"))) {
35305
- const p = join11(dir, f);
35306
- if (statSync2(p).mtimeMs < cutoff) rmSync3(p, { force: true });
35462
+ for (const f of readdirSync4(dir).filter((n) => n.endsWith(".log"))) {
35463
+ const p = join12(dir, f);
35464
+ if (statSync3(p).mtimeMs < cutoff) rmSync4(p, { force: true });
35307
35465
  }
35308
35466
  } catch {
35309
35467
  }
@@ -35380,8 +35538,8 @@ async function writeWorkerFile(root, relPath, content) {
35380
35538
  import { spawn as spawn5 } from "child_process";
35381
35539
  import { execFile as execFile2 } from "child_process";
35382
35540
  import { createHash as createHash3 } from "crypto";
35383
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
35384
- import { join as join12 } from "path";
35541
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
35542
+ import { join as join13 } from "path";
35385
35543
  import { promisify as promisify2 } from "util";
35386
35544
  var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
35387
35545
  var execFileAsync = promisify2(execFile2);
@@ -35389,11 +35547,11 @@ var MAX_CONTEXT_CHARS = 12e4;
35389
35547
  var ANSWER_TIMEOUT_MS = 12e4;
35390
35548
  var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
35391
35549
  function kbRoot(profileDir2) {
35392
- return join12(profileDir2, "support-kb");
35550
+ return join13(profileDir2, "support-kb");
35393
35551
  }
35394
35552
  function repoDir(profileDir2, url, branch) {
35395
35553
  const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
35396
- return join12(kbRoot(profileDir2), key);
35554
+ return join13(kbRoot(profileDir2), key);
35397
35555
  }
35398
35556
  function authedUrl(url, token) {
35399
35557
  if (!token) return url;
@@ -35402,10 +35560,10 @@ function authedUrl(url, token) {
35402
35560
  }
35403
35561
  async function ensureRepo(profileDir2, repo) {
35404
35562
  const dir2 = repoDir(profileDir2, repo.url, repo.branch);
35405
- if (existsSync8(join12(dir2, ".git"))) return dir2;
35563
+ if (existsSync9(join13(dir2, ".git"))) return dir2;
35406
35564
  const url = authedUrl(repo.url, repo.token);
35407
35565
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
35408
- mkdirSync4(kbRoot(profileDir2), { recursive: true });
35566
+ mkdirSync5(kbRoot(profileDir2), { recursive: true });
35409
35567
  await execFileAsync("git", ["clone", "--depth", "1", "--branch", repo.branch, url, dir2], {
35410
35568
  env: gitEnv
35411
35569
  });
@@ -35413,11 +35571,11 @@ async function ensureRepo(profileDir2, repo) {
35413
35571
  }
35414
35572
  async function refreshSupportKb(profileDir2) {
35415
35573
  const root = kbRoot(profileDir2);
35416
- if (!existsSync8(root)) return;
35574
+ if (!existsSync9(root)) return;
35417
35575
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
35418
- for (const entry of readdirSync4(root)) {
35419
- const dir2 = join12(root, entry);
35420
- if (!existsSync8(join12(dir2, ".git"))) continue;
35576
+ for (const entry of readdirSync5(root)) {
35577
+ const dir2 = join13(root, entry);
35578
+ if (!existsSync9(join13(dir2, ".git"))) continue;
35421
35579
  try {
35422
35580
  await execFileAsync("git", ["-C", dir2, "pull", "--ff-only"], { env: gitEnv });
35423
35581
  } catch (err) {
@@ -35430,10 +35588,10 @@ function collectDocs(dir2) {
35430
35588
  let total = 0;
35431
35589
  const walk2 = (current) => {
35432
35590
  if (total >= MAX_CONTEXT_CHARS) return;
35433
- for (const entry of readdirSync4(current)) {
35591
+ for (const entry of readdirSync5(current)) {
35434
35592
  if (entry === ".git" || entry === "node_modules") continue;
35435
- const full = join12(current, entry);
35436
- const st = statSync3(full);
35593
+ const full = join13(current, entry);
35594
+ const st = statSync4(full);
35437
35595
  if (st.isDirectory()) {
35438
35596
  walk2(full);
35439
35597
  } else if (DOC_EXTENSIONS.some((ext) => entry.toLowerCase().endsWith(ext))) {
@@ -35657,7 +35815,7 @@ async function runKnowledgeCompose(req, ai, physicRoot) {
35657
35815
  import { readFile as readFile3, writeFile as writeFile2, chmod } from "fs/promises";
35658
35816
  import { readFileSync as readFileSync13 } from "fs";
35659
35817
  import { execFile as execFile3, spawn as spawn7 } from "child_process";
35660
- import { join as join13 } from "path";
35818
+ import { join as join14 } from "path";
35661
35819
  import { promisify as promisify3 } from "util";
35662
35820
  var run3 = promisify3(execFile3);
35663
35821
  var SETTINGS_REL = ".claude/.autonomous.settings.json";
@@ -35689,7 +35847,7 @@ function parseJson(text) {
35689
35847
  }
35690
35848
  }
35691
35849
  function tickScript(root) {
35692
- return join13(root, TICK_REL);
35850
+ return join14(root, TICK_REL);
35693
35851
  }
35694
35852
  async function crontabList() {
35695
35853
  try {
@@ -35712,8 +35870,8 @@ async function cronInstalled(root) {
35712
35870
  return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
35713
35871
  }
35714
35872
  async function getAutonomousStatus(root) {
35715
- const settings = parseJson(await readText(join13(root, SETTINGS_REL))) ?? {};
35716
- const hist = parseJson(await readText(join13(root, HISTORIES_REL))) ?? {};
35873
+ const settings = parseJson(await readText(join14(root, SETTINGS_REL))) ?? {};
35874
+ const hist = parseJson(await readText(join14(root, HISTORIES_REL))) ?? {};
35717
35875
  const records = Array.isArray(hist.records) ? hist.records : [];
35718
35876
  const last = records[records.length - 1];
35719
35877
  const ticks = hist.ticks ?? {};
@@ -35730,9 +35888,9 @@ async function getAutonomousStatus(root) {
35730
35888
  }
35731
35889
  function isAutonomousRunning(root) {
35732
35890
  try {
35733
- const s = JSON.parse(readFileSync13(join13(root, SETTINGS_REL), "utf8"));
35891
+ const s = JSON.parse(readFileSync13(join14(root, SETTINGS_REL), "utf8"));
35734
35892
  if (s.paused === true) return false;
35735
- const h = JSON.parse(readFileSync13(join13(root, HISTORIES_REL), "utf8"));
35893
+ const h = JSON.parse(readFileSync13(join14(root, HISTORIES_REL), "utf8"));
35736
35894
  const ts = h.records?.[h.records.length - 1]?.ts;
35737
35895
  if (!ts) return false;
35738
35896
  const t = Date.parse(ts.replace(" ", "T"));
@@ -35743,10 +35901,10 @@ function isAutonomousRunning(root) {
35743
35901
  }
35744
35902
  async function readAutonomous(root) {
35745
35903
  const [settings, approvals, status, ...books] = await Promise.all([
35746
- readText(join13(root, SETTINGS_REL)),
35747
- readText(join13(root, APPROVALS_REL), "{}"),
35904
+ readText(join14(root, SETTINGS_REL)),
35905
+ readText(join14(root, APPROVALS_REL), "{}"),
35748
35906
  getAutonomousStatus(root),
35749
- ...Object.values(BOOK_FILES).map((f) => readText(join13(root, f)))
35907
+ ...Object.values(BOOK_FILES).map((f) => readText(join14(root, f)))
35750
35908
  ]);
35751
35909
  const keys = Object.keys(BOOK_FILES);
35752
35910
  const bookMap = {};
@@ -35755,7 +35913,7 @@ async function readAutonomous(root) {
35755
35913
  }
35756
35914
  async function readAutonomousLogs(root, date) {
35757
35915
  const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
35758
- const text = await readText(join13(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
35916
+ const text = await readText(join14(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
35759
35917
  const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
35760
35918
  return { date: day, lines };
35761
35919
  }
@@ -35779,24 +35937,24 @@ async function writeAutonomous(root, req, by) {
35779
35937
  switch (req.kind) {
35780
35938
  case "settings": {
35781
35939
  if (!parseJson(req.settings)) return { ok: false, error: "settings is not valid JSON" };
35782
- await writeFile2(join13(root, SETTINGS_REL), req.settings, "utf8");
35940
+ await writeFile2(join14(root, SETTINGS_REL), req.settings, "utf8");
35783
35941
  break;
35784
35942
  }
35785
35943
  case "approvals": {
35786
- const map = parseJson(await readText(join13(root, APPROVALS_REL), "{}")) ?? {};
35944
+ const map = parseJson(await readText(join14(root, APPROVALS_REL), "{}")) ?? {};
35787
35945
  if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
35788
35946
  else delete map[req.taskId];
35789
- await writeFile2(join13(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
35947
+ await writeFile2(join14(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
35790
35948
  break;
35791
35949
  }
35792
35950
  case "userTodo": {
35793
- const cur = await readText(join13(root, BOOK_FILES.userTodo));
35951
+ const cur = await readText(join14(root, BOOK_FILES.userTodo));
35794
35952
  const block = `
35795
35953
 
35796
35954
  > posted by ${by} at ${(/* @__PURE__ */ new Date()).toISOString()}
35797
35955
  ${req.content.trim()}
35798
35956
  `;
35799
- await writeFile2(join13(root, BOOK_FILES.userTodo), cur.trimEnd() + block, "utf8");
35957
+ await writeFile2(join14(root, BOOK_FILES.userTodo), cur.trimEnd() + block, "utf8");
35800
35958
  break;
35801
35959
  }
35802
35960
  case "cron": {
@@ -35813,7 +35971,7 @@ ${req.content.trim()}
35813
35971
 
35814
35972
  // src/core/agents.ts
35815
35973
  import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3, readdir as readdir3, rm } from "fs/promises";
35816
- import { join as join14 } from "path";
35974
+ import { join as join15 } from "path";
35817
35975
  var AGENTS_REL = ".claude/agents";
35818
35976
  var SKILLS_REL = ".claude/skills";
35819
35977
  var MEMORY_DIRS = /* @__PURE__ */ new Set(["short-memory", "long-memory"]);
@@ -35837,17 +35995,17 @@ async function listAgents(root) {
35837
35995
  const subagents = [];
35838
35996
  const skills = [];
35839
35997
  try {
35840
- for (const e of await readdir3(join14(root, AGENTS_REL), { withFileTypes: true })) {
35998
+ for (const e of await readdir3(join15(root, AGENTS_REL), { withFileTypes: true })) {
35841
35999
  if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
35842
- const fm = frontmatter(await readText2(join14(root, AGENTS_REL, e.name)));
36000
+ const fm = frontmatter(await readText2(join15(root, AGENTS_REL, e.name)));
35843
36001
  subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
35844
36002
  }
35845
36003
  } catch {
35846
36004
  }
35847
36005
  try {
35848
- for (const e of await readdir3(join14(root, SKILLS_REL), { withFileTypes: true })) {
36006
+ for (const e of await readdir3(join15(root, SKILLS_REL), { withFileTypes: true })) {
35849
36007
  if (!e.isDirectory()) continue;
35850
- const fm = frontmatter(await readText2(join14(root, SKILLS_REL, e.name, "SKILL.md")));
36008
+ const fm = frontmatter(await readText2(join15(root, SKILLS_REL, e.name, "SKILL.md")));
35851
36009
  skills.push({ name: e.name, description: fm.description });
35852
36010
  }
35853
36011
  } catch {
@@ -35858,15 +36016,15 @@ async function listAgents(root) {
35858
36016
  }
35859
36017
  function itemPath(root, kind, name) {
35860
36018
  const n = safeName(name);
35861
- return kind === "subagent" ? join14(root, AGENTS_REL, `${n}.md`) : join14(root, SKILLS_REL, n, "SKILL.md");
36019
+ return kind === "subagent" ? join15(root, AGENTS_REL, `${n}.md`) : join15(root, SKILLS_REL, n, "SKILL.md");
35862
36020
  }
35863
36021
  async function readAgent(root, kind, name) {
35864
36022
  const content = await readText2(itemPath(root, kind, name));
35865
36023
  if (kind !== "subagent") return { content };
35866
36024
  const n = safeName(name);
35867
36025
  const [short, long] = await Promise.all([
35868
- readText2(join14(root, AGENTS_REL, "short-memory", `${n}.md`)),
35869
- readText2(join14(root, AGENTS_REL, "long-memory", `${n}.md`))
36026
+ readText2(join15(root, AGENTS_REL, "short-memory", `${n}.md`)),
36027
+ readText2(join15(root, AGENTS_REL, "long-memory", `${n}.md`))
35870
36028
  ]);
35871
36029
  return { content, memory: { short, long } };
35872
36030
  }
@@ -35876,12 +36034,12 @@ async function writeAgent(root, req) {
35876
36034
  try {
35877
36035
  if (req.action === "delete") {
35878
36036
  if (req.kind === "subagent") await rm(itemPath(root, "subagent", n), { force: true });
35879
- else await rm(join14(root, SKILLS_REL, n), { recursive: true, force: true });
36037
+ else await rm(join15(root, SKILLS_REL, n), { recursive: true, force: true });
35880
36038
  return { ok: true };
35881
36039
  }
35882
36040
  if (req.content === void 0) return { ok: false, error: "content is required" };
35883
36041
  const path = itemPath(root, req.kind, n);
35884
- await mkdir3(join14(path, ".."), { recursive: true });
36042
+ await mkdir3(join15(path, ".."), { recursive: true });
35885
36043
  await writeFile3(path, req.content, "utf8");
35886
36044
  return { ok: true };
35887
36045
  } catch (err) {
@@ -35892,7 +36050,7 @@ async function writeAgent(root, req) {
35892
36050
  // src/core/packages.ts
35893
36051
  import { createHash as createHash4 } from "crypto";
35894
36052
  import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir4, readdir as readdir4, rm as rm2, stat } from "fs/promises";
35895
- import { dirname as dirname4, join as join15, relative } from "path";
36053
+ import { dirname as dirname4, join as join16, relative } from "path";
35896
36054
  var AGENTS_REL2 = ".claude/agents";
35897
36055
  var SKILLS_REL2 = ".claude/skills";
35898
36056
  var MANIFEST_REL = ".claude/.4pm-packages.json";
@@ -35916,7 +36074,7 @@ async function listFilesRel(dir2) {
35916
36074
  return;
35917
36075
  }
35918
36076
  for (const e of entries2) {
35919
- const full = join15(d, e.name);
36077
+ const full = join16(d, e.name);
35920
36078
  if (e.isDirectory()) await walk2(full);
35921
36079
  else if (e.isFile()) out.push(relative(dir2, full));
35922
36080
  }
@@ -35927,16 +36085,16 @@ async function listFilesRel(dir2) {
35927
36085
  async function readArtifactFiles(root, kind, slug) {
35928
36086
  const files = [];
35929
36087
  if (kind === "subagent") {
35930
- const agent = await readMaybe(join15(root, AGENTS_REL2, `${slug}.md`));
36088
+ const agent = await readMaybe(join16(root, AGENTS_REL2, `${slug}.md`));
35931
36089
  if (agent) files.push({ path: "agent.md", contentBase64: agent.toString("base64") });
35932
- const short = await readMaybe(join15(root, AGENTS_REL2, "short-memory", `${slug}.md`));
36090
+ const short = await readMaybe(join16(root, AGENTS_REL2, "short-memory", `${slug}.md`));
35933
36091
  if (short) files.push({ path: "short-memory.md", contentBase64: short.toString("base64") });
35934
- const long = await readMaybe(join15(root, AGENTS_REL2, "long-memory", `${slug}.md`));
36092
+ const long = await readMaybe(join16(root, AGENTS_REL2, "long-memory", `${slug}.md`));
35935
36093
  if (long) files.push({ path: "long-memory.md", contentBase64: long.toString("base64") });
35936
36094
  } else {
35937
- const base = join15(root, SKILLS_REL2, slug);
36095
+ const base = join16(root, SKILLS_REL2, slug);
35938
36096
  for (const rel of await listFilesRel(base)) {
35939
- const buf = await readMaybe(join15(base, rel));
36097
+ const buf = await readMaybe(join16(base, rel));
35940
36098
  if (buf) files.push({ path: rel.split("\\").join("/"), contentBase64: buf.toString("base64") });
35941
36099
  }
35942
36100
  }
@@ -35954,7 +36112,7 @@ async function artifactSha256(root, kind, slug) {
35954
36112
  return h.digest("hex");
35955
36113
  }
35956
36114
  async function artifactExists(root, kind, slug) {
35957
- const target = kind === "subagent" ? join15(root, AGENTS_REL2, `${slug}.md`) : join15(root, SKILLS_REL2, slug);
36115
+ const target = kind === "subagent" ? join16(root, AGENTS_REL2, `${slug}.md`) : join16(root, SKILLS_REL2, slug);
35958
36116
  try {
35959
36117
  await stat(target);
35960
36118
  return true;
@@ -35963,7 +36121,7 @@ async function artifactExists(root, kind, slug) {
35963
36121
  }
35964
36122
  }
35965
36123
  async function readManifest(root) {
35966
- const buf = await readMaybe(join15(root, MANIFEST_REL));
36124
+ const buf = await readMaybe(join16(root, MANIFEST_REL));
35967
36125
  if (!buf) return {};
35968
36126
  try {
35969
36127
  return JSON.parse(buf.toString("utf8"));
@@ -35972,7 +36130,7 @@ async function readManifest(root) {
35972
36130
  }
35973
36131
  }
35974
36132
  async function writeManifest(root, manifest) {
35975
- const path = join15(root, MANIFEST_REL);
36133
+ const path = join16(root, MANIFEST_REL);
35976
36134
  await mkdir4(dirname4(path), { recursive: true });
35977
36135
  await writeFile4(path, JSON.stringify(manifest, null, 2), "utf8");
35978
36136
  }
@@ -35981,24 +36139,24 @@ async function writeArtifactFiles(root, kind, slug, files) {
35981
36139
  if (kind === "subagent") {
35982
36140
  const agent = byPath.get("agent.md");
35983
36141
  if (agent) {
35984
- await mkdir4(join15(root, AGENTS_REL2), { recursive: true });
35985
- await writeFile4(join15(root, AGENTS_REL2, `${slug}.md`), agent);
36142
+ await mkdir4(join16(root, AGENTS_REL2), { recursive: true });
36143
+ await writeFile4(join16(root, AGENTS_REL2, `${slug}.md`), agent);
35986
36144
  }
35987
36145
  const short = byPath.get("short-memory.md");
35988
36146
  if (short) {
35989
- await mkdir4(join15(root, AGENTS_REL2, "short-memory"), { recursive: true });
35990
- await writeFile4(join15(root, AGENTS_REL2, "short-memory", `${slug}.md`), short);
36147
+ await mkdir4(join16(root, AGENTS_REL2, "short-memory"), { recursive: true });
36148
+ await writeFile4(join16(root, AGENTS_REL2, "short-memory", `${slug}.md`), short);
35991
36149
  }
35992
36150
  const long = byPath.get("long-memory.md");
35993
36151
  if (long) {
35994
- await mkdir4(join15(root, AGENTS_REL2, "long-memory"), { recursive: true });
35995
- await writeFile4(join15(root, AGENTS_REL2, "long-memory", `${slug}.md`), long);
36152
+ await mkdir4(join16(root, AGENTS_REL2, "long-memory"), { recursive: true });
36153
+ await writeFile4(join16(root, AGENTS_REL2, "long-memory", `${slug}.md`), long);
35996
36154
  }
35997
36155
  } else {
35998
- const base = join15(root, SKILLS_REL2, slug);
36156
+ const base = join16(root, SKILLS_REL2, slug);
35999
36157
  for (const [rel, buf] of byPath) {
36000
36158
  if (rel.includes("..") || rel.startsWith("/")) continue;
36001
- const dest = join15(base, rel);
36159
+ const dest = join16(base, rel);
36002
36160
  await mkdir4(dirname4(dest), { recursive: true });
36003
36161
  await writeFile4(dest, buf);
36004
36162
  }
@@ -36064,11 +36222,11 @@ async function removePackage(root, slug) {
36064
36222
  const entry = manifest[n];
36065
36223
  if (!entry) return { ok: false, error: "not installed" };
36066
36224
  if (entry.kind === "subagent") {
36067
- await rm2(join15(root, AGENTS_REL2, `${n}.md`), { force: true });
36068
- await rm2(join15(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
36069
- await rm2(join15(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
36225
+ await rm2(join16(root, AGENTS_REL2, `${n}.md`), { force: true });
36226
+ await rm2(join16(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
36227
+ await rm2(join16(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
36070
36228
  } else {
36071
- await rm2(join15(root, SKILLS_REL2, n), { recursive: true, force: true });
36229
+ await rm2(join16(root, SKILLS_REL2, n), { recursive: true, force: true });
36072
36230
  }
36073
36231
  delete manifest[n];
36074
36232
  await writeManifest(root, manifest);
@@ -36079,7 +36237,7 @@ async function removePackage(root, slug) {
36079
36237
  }
36080
36238
 
36081
36239
  // src/core/network-probe.ts
36082
- import { existsSync as existsSync9, readFileSync as readFileSync14 } from "fs";
36240
+ import { existsSync as existsSync10, readFileSync as readFileSync14 } from "fs";
36083
36241
  import { connect } from "net";
36084
36242
  var CANARY_TARGETS = [
36085
36243
  { host: "1.1.1.1", port: 443 },
@@ -36088,7 +36246,7 @@ var CANARY_TARGETS = [
36088
36246
  var PROBE_TIMEOUT_MS = 2500;
36089
36247
  function detectContainerized() {
36090
36248
  try {
36091
- if (existsSync9("/.dockerenv")) return true;
36249
+ if (existsSync10("/.dockerenv")) return true;
36092
36250
  const cgroup = readFileSync14("/proc/1/cgroup", "utf8");
36093
36251
  return /docker|containerd|kubepods|libpod/.test(cgroup);
36094
36252
  } catch {
@@ -36156,9 +36314,9 @@ async function probeNetwork() {
36156
36314
  }
36157
36315
 
36158
36316
  // src/core/git-auth.ts
36159
- import { writeFileSync as writeFileSync6 } from "fs";
36317
+ import { writeFileSync as writeFileSync7 } from "fs";
36160
36318
  import { homedir as homedir4 } from "os";
36161
- import { join as join16 } from "path";
36319
+ import { join as join17 } from "path";
36162
36320
  function tokenTarget(method) {
36163
36321
  if (method === "gitlab-group-token") {
36164
36322
  return { host: process.env.FOURPM_GIT_HOST || "gitlab.com", user: "oauth2" };
@@ -36176,7 +36334,7 @@ function applyGitAuth(method) {
36176
36334
  try {
36177
36335
  const line = `https://${target.user}:${token}@${target.host}
36178
36336
  `;
36179
- writeFileSync6(join16(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
36337
+ writeFileSync7(join17(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
36180
36338
  return true;
36181
36339
  } catch {
36182
36340
  return false;
@@ -36185,7 +36343,7 @@ function applyGitAuth(method) {
36185
36343
 
36186
36344
  // src/core/secrets.ts
36187
36345
  import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
36188
- import { join as join17 } from "path";
36346
+ import { join as join18 } from "path";
36189
36347
  var SECURITY_REL = "AI_SECURITY.md";
36190
36348
  var PLACEHOLDER_REL = "AI_PLACEHOLDER.md";
36191
36349
  var SECRETS_REL = "project.secrets.json";
@@ -36198,7 +36356,7 @@ async function readText3(path, fallback = "") {
36198
36356
  }
36199
36357
  async function readSecretsObject(root) {
36200
36358
  try {
36201
- const v = JSON.parse(await readFile6(join17(root, SECRETS_REL), "utf8"));
36359
+ const v = JSON.parse(await readFile6(join18(root, SECRETS_REL), "utf8"));
36202
36360
  return v && typeof v === "object" ? v : {};
36203
36361
  } catch {
36204
36362
  return {};
@@ -36217,8 +36375,8 @@ function placeholderKeys(text) {
36217
36375
  }
36218
36376
  async function readSecrets(root) {
36219
36377
  const [security, placeholder, secrets] = await Promise.all([
36220
- readText3(join17(root, SECURITY_REL)),
36221
- readText3(join17(root, PLACEHOLDER_REL)),
36378
+ readText3(join18(root, SECURITY_REL)),
36379
+ readText3(join18(root, PLACEHOLDER_REL)),
36222
36380
  readSecretsObject(root)
36223
36381
  ]);
36224
36382
  const names = /* @__PURE__ */ new Set([...placeholderKeys(placeholder), ...Object.keys(secrets)]);
@@ -36231,14 +36389,14 @@ async function readSecrets(root) {
36231
36389
  async function writeSecrets(root, req) {
36232
36390
  try {
36233
36391
  if (req.kind === "security") {
36234
- await writeFile5(join17(root, SECURITY_REL), req.content, "utf8");
36392
+ await writeFile5(join18(root, SECURITY_REL), req.content, "utf8");
36235
36393
  } else if (req.kind === "placeholder") {
36236
- await writeFile5(join17(root, PLACEHOLDER_REL), req.content, "utf8");
36394
+ await writeFile5(join18(root, PLACEHOLDER_REL), req.content, "utf8");
36237
36395
  } else {
36238
36396
  const secrets = await readSecretsObject(root);
36239
36397
  if (req.kind === "secret") secrets[req.key] = req.value;
36240
36398
  else delete secrets[req.key];
36241
- await writeFile5(join17(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
36399
+ await writeFile5(join18(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
36242
36400
  }
36243
36401
  return { ok: true };
36244
36402
  } catch (err) {
@@ -36248,13 +36406,13 @@ async function writeSecrets(root, req) {
36248
36406
 
36249
36407
  // src/core/agent-tools.ts
36250
36408
  import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
36251
- import { dirname as dirname5, join as join18 } from "path";
36409
+ import { dirname as dirname5, join as join19 } from "path";
36252
36410
  var SHARED_REL = ".claude/settings.json";
36253
36411
  var LOCAL_REL = ".claude/settings.local.json";
36254
36412
  var SECRETS_DENY = "Read(./project.secrets.json)";
36255
36413
  var MODES = ["default", "acceptEdits", "plan", "bypassPermissions"];
36256
36414
  function fileFor(root, scope) {
36257
- return join18(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
36415
+ return join19(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
36258
36416
  }
36259
36417
  async function readSettingsObject(path) {
36260
36418
  try {
@@ -36291,7 +36449,7 @@ async function writeAgentTools(root, scope, permissions) {
36291
36449
 
36292
36450
  // src/core/graph.ts
36293
36451
  import { readFile as readFile8, readdir as readdir5 } from "fs/promises";
36294
- import { join as join19, relative as relative2, dirname as dirname6, resolve as resolve4, extname, basename as basename2 } from "path";
36452
+ import { join as join20, relative as relative2, dirname as dirname6, resolve as resolve4, extname, basename as basename2 } from "path";
36295
36453
  var MAX_FILES = 2e3;
36296
36454
  var MAX_NODES = 3e3;
36297
36455
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
@@ -36311,7 +36469,7 @@ async function walk(root, accept) {
36311
36469
  if (e.name.startsWith(".") && e.name !== ".claude") {
36312
36470
  if (e.isDirectory()) continue;
36313
36471
  }
36314
- const full = join19(dir2, e.name);
36472
+ const full = join20(dir2, e.name);
36315
36473
  if (e.isDirectory()) {
36316
36474
  if (!SKIP_DIRS.has(e.name)) stack.push(full);
36317
36475
  } else if (accept(full)) {
@@ -36431,10 +36589,10 @@ async function buildGraph(root, mode) {
36431
36589
 
36432
36590
  // src/core/rag.ts
36433
36591
  import { readFile as readFile9, writeFile as writeFile7, statfs as statfs2, mkdir as mkdir6 } from "fs/promises";
36434
- import { existsSync as existsSync10 } from "fs";
36592
+ import { existsSync as existsSync11 } from "fs";
36435
36593
  import { execFile as execFile4, spawn as spawn8 } from "child_process";
36436
36594
  import { totalmem as totalmem2, cpus as cpus2 } from "os";
36437
- import { join as join20 } from "path";
36595
+ import { join as join21 } from "path";
36438
36596
  import { promisify as promisify4 } from "util";
36439
36597
  var run4 = promisify4(execFile4);
36440
36598
  var RAG_REL = ".claude/rag";
@@ -36455,7 +36613,7 @@ async function detectPython() {
36455
36613
  }
36456
36614
  async function installLog(root) {
36457
36615
  try {
36458
- const text = await readFile9(join20(root, RAG_REL, "install.log"), "utf8");
36616
+ const text = await readFile9(join21(root, RAG_REL, "install.log"), "utf8");
36459
36617
  return text.split("\n").filter((l) => l.length > 0).slice(-200);
36460
36618
  } catch {
36461
36619
  return [];
@@ -36517,14 +36675,14 @@ async function ragStatus(root) {
36517
36675
  };
36518
36676
  let modelName = "";
36519
36677
  try {
36520
- modelName = (await readFile9(join20(root, RAG_REL, ".installed"), "utf8")).trim();
36678
+ modelName = (await readFile9(join21(root, RAG_REL, ".installed"), "utf8")).trim();
36521
36679
  } catch {
36522
36680
  modelName = "";
36523
36681
  }
36524
36682
  const modelPresent = modelName.length > 0;
36525
36683
  return {
36526
36684
  installed: fastembed && sqliteVec && modelPresent,
36527
- installing: existsSync10(join20(root, RAG_REL, ".installing")),
36685
+ installing: existsSync11(join21(root, RAG_REL, ".installing")),
36528
36686
  python: { found: py.found, version: py.version },
36529
36687
  deps: { pip, fastembed, sqliteVec },
36530
36688
  model: { present: modelPresent, name: modelName },
@@ -36535,9 +36693,9 @@ async function ragStatus(root) {
36535
36693
  };
36536
36694
  }
36537
36695
  async function readIndexMeta(root) {
36538
- const indexing = existsSync10(join20(root, RAG_REL, ".indexing"));
36696
+ const indexing = existsSync11(join21(root, RAG_REL, ".indexing"));
36539
36697
  try {
36540
- const m = JSON.parse(await readFile9(join20(root, RAG_REL, ".index.json"), "utf8"));
36698
+ const m = JSON.parse(await readFile9(join21(root, RAG_REL, ".index.json"), "utf8"));
36541
36699
  return {
36542
36700
  present: true,
36543
36701
  indexing,
@@ -36604,14 +36762,14 @@ if __name__ == "__main__":
36604
36762
  elif cmd == "query": query(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 8)
36605
36763
  `;
36606
36764
  async function ensureEngine(root) {
36607
- await mkdir6(join20(root, RAG_REL), { recursive: true }).catch(() => void 0);
36608
- await writeFile7(join20(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
36765
+ await mkdir6(join21(root, RAG_REL), { recursive: true }).catch(() => void 0);
36766
+ await writeFile7(join21(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
36609
36767
  }
36610
36768
  async function ragReindex(root) {
36611
36769
  const py = await detectPython();
36612
36770
  if (!py.found) return { ok: false, started: false, error: "python3 not found" };
36613
- if (!existsSync10(join20(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
36614
- if (existsSync10(join20(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
36771
+ if (!existsSync11(join21(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
36772
+ if (existsSync11(join21(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
36615
36773
  await ensureEngine(root);
36616
36774
  const script = `
36617
36775
  set +e
@@ -36631,12 +36789,12 @@ rm -f "${RAG_REL}/.indexing"
36631
36789
  async function ragQuery(root, queryText, k = 8) {
36632
36790
  const py = await detectPython();
36633
36791
  if (!py.found) return { results: [], error: "python3 not found" };
36634
- if (!existsSync10(join20(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
36792
+ if (!existsSync11(join21(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
36635
36793
  await ensureEngine(root);
36636
36794
  try {
36637
36795
  const { stdout } = await run4(
36638
36796
  py.bin,
36639
- [join20(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k, 1), 20))],
36797
+ [join21(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k, 1), 20))],
36640
36798
  { cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
36641
36799
  );
36642
36800
  const parsed = JSON.parse(stdout.trim() || '{"results":[]}');
@@ -36651,9 +36809,9 @@ async function ragInstall(root, model) {
36651
36809
  }
36652
36810
  const py = await detectPython();
36653
36811
  if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
36654
- if (existsSync10(join20(root, RAG_REL, ".installing"))) return { ok: true, started: true };
36812
+ if (existsSync11(join21(root, RAG_REL, ".installing"))) return { ok: true, started: true };
36655
36813
  try {
36656
- await mkdir6(join20(root, RAG_REL), { recursive: true });
36814
+ await mkdir6(join21(root, RAG_REL), { recursive: true });
36657
36815
  } catch {
36658
36816
  }
36659
36817
  const script = `
@@ -36741,12 +36899,12 @@ ${stderr}`;
36741
36899
  // src/core/git-ssh-key.ts
36742
36900
  import { execFile as execFile7 } from "child_process";
36743
36901
  import { readFile as readFile11, rm as rm3, mkdir as mkdir7 } from "fs/promises";
36744
- import { join as join21 } from "path";
36902
+ import { join as join22 } from "path";
36745
36903
  import { promisify as promisify7 } from "util";
36746
36904
  var run7 = promisify7(execFile7);
36747
36905
  function keyPaths(profileDir2) {
36748
- const dir2 = join21(profileDir2, ".ssh");
36749
- const key = join21(dir2, "id_4pm");
36906
+ const dir2 = join22(profileDir2, ".ssh");
36907
+ const key = join22(dir2, "id_4pm");
36750
36908
  return { dir: dir2, key, pub: `${key}.pub` };
36751
36909
  }
36752
36910
  async function readPublicKey(pub) {
@@ -36938,7 +37096,7 @@ async function setCommitAuthor(cwd2, machineUsername) {
36938
37096
  // src/core/scaffold.ts
36939
37097
  import { cp, mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
36940
37098
  import { execFile as execFile10 } from "child_process";
36941
- import { dirname as dirname8, join as join22, resolve as resolve7 } from "path";
37099
+ import { dirname as dirname8, join as join23, resolve as resolve7 } from "path";
36942
37100
  import { fileURLToPath as fileURLToPath3 } from "url";
36943
37101
  import { promisify as promisify10 } from "util";
36944
37102
  var run10 = promisify10(execFile10);
@@ -36967,7 +37125,7 @@ async function provisionRepos(target, repos, emit, opts = {}) {
36967
37125
  }
36968
37126
  continue;
36969
37127
  }
36970
- const dir2 = join22(target, subDirName(repo));
37128
+ const dir2 = join23(target, subDirName(repo));
36971
37129
  if (repo.url) {
36972
37130
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
36973
37131
  await run10("git", ["clone", repo.url, dir2], { timeout: 12e4 });
@@ -36987,14 +37145,14 @@ async function scaffoldProject(payload, profileDir2, onProgress) {
36987
37145
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
36988
37146
  try {
36989
37147
  const root = process.env.SCAFFOLD_ROOT ? resolve7(process.env.SCAFFOLD_ROOT) : resolve7(profileDir2);
36990
- const target = join22(root, payload.projectName);
37148
+ const target = join23(root, payload.projectName);
36991
37149
  await mkdir8(target, { recursive: true });
36992
37150
  emit("copy", "Copying the sample template\u2026");
36993
37151
  await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
36994
37152
  if (payload.spec) {
36995
37153
  emit("spec", "Writing project.spec.json\u2026");
36996
37154
  await writeFile8(
36997
- join22(target, "project.spec.json"),
37155
+ join23(target, "project.spec.json"),
36998
37156
  JSON.stringify(payload.spec, null, 2),
36999
37157
  "utf8"
37000
37158
  );
@@ -37012,7 +37170,7 @@ async function aiInit(target, spec, emit) {
37012
37170
  emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
37013
37171
  const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
37014
37172
  if (subagents.length > 0) {
37015
- await mkdir8(join22(target, ".claude", "agents"), { recursive: true });
37173
+ await mkdir8(join23(target, ".claude", "agents"), { recursive: true });
37016
37174
  for (const sa of subagents) {
37017
37175
  const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
37018
37176
  if (!name) continue;
@@ -37022,17 +37180,17 @@ name: ${name}
37022
37180
 
37023
37181
  ${(sa.description || "").trim()}
37024
37182
  `;
37025
- await writeFile8(join22(target, ".claude", "agents", `${name}.md`), body, "utf8");
37183
+ await writeFile8(join23(target, ".claude", "agents", `${name}.md`), body, "utf8");
37026
37184
  }
37027
37185
  }
37028
37186
  const specJson = JSON.stringify(spec);
37029
37187
  await generateFile(
37030
- join22(target, "README.md"),
37188
+ join23(target, "README.md"),
37031
37189
  `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
37032
37190
  ${specJson}`
37033
37191
  );
37034
37192
  await generateFile(
37035
- join22(target, "CLAUDE.md"),
37193
+ join23(target, "CLAUDE.md"),
37036
37194
  `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
37037
37195
  ${specJson}`
37038
37196
  );
@@ -37048,7 +37206,7 @@ async function addProject(payload, profileDir2, onProgress) {
37048
37206
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
37049
37207
  try {
37050
37208
  const root = process.env.SCAFFOLD_ROOT ? resolve7(process.env.SCAFFOLD_ROOT) : resolve7(profileDir2);
37051
- const target = join22(root, payload.projectName);
37209
+ const target = join23(root, payload.projectName);
37052
37210
  await mkdir8(target, { recursive: true });
37053
37211
  const repos = payload.repos ?? [];
37054
37212
  if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
@@ -37060,8 +37218,8 @@ async function addProject(payload, profileDir2, onProgress) {
37060
37218
  }
37061
37219
 
37062
37220
  // src/ui/slash-commands.ts
37063
- import { existsSync as existsSync11 } from "fs";
37064
- import { join as join23 } from "path";
37221
+ import { existsSync as existsSync12 } from "fs";
37222
+ import { join as join24 } from "path";
37065
37223
  var CONFIG_KEYS = [
37066
37224
  "aiCli",
37067
37225
  "claudeHome",
@@ -37073,7 +37231,7 @@ var CONFIG_KEYS = [
37073
37231
  "aiEnv"
37074
37232
  ];
37075
37233
  function configPath(profileDir2) {
37076
- return join23(profileDir2, "config.json");
37234
+ return join24(profileDir2, "config.json");
37077
37235
  }
37078
37236
  function parseConfigAssignment(key, valueTokens) {
37079
37237
  const value = valueTokens.join(" ").trim();
@@ -37123,7 +37281,7 @@ function runConfig(ctx) {
37123
37281
  ctx.print(`\u2714 ${verb} default config: ${path}`);
37124
37282
  ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
37125
37283
  };
37126
- if (existsSync11(path)) {
37284
+ if (existsSync12(path)) {
37127
37285
  ctx.confirm(
37128
37286
  `config exists: ${path} \u2014 replace it with defaults? (y/N)`,
37129
37287
  () => writeDefaults("replaced with")
@@ -37208,7 +37366,7 @@ function runOutput(ctx) {
37208
37366
  }
37209
37367
  function runLogs(ctx) {
37210
37368
  const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
37211
- const dir2 = join23(ctx.info.profileDir, "logs");
37369
+ const dir2 = join24(ctx.info.profileDir, "logs");
37212
37370
  const lines = readRecentLogLines(dir2, n);
37213
37371
  if (lines.length === 0) {
37214
37372
  ctx.print("No log lines yet.");
@@ -37521,8 +37679,8 @@ async function runMemoryCompaction(ai, cwd2, input) {
37521
37679
  }
37522
37680
 
37523
37681
  // src/core/config-sync.ts
37524
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
37525
- import { join as join24 } from "path";
37682
+ import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
37683
+ import { join as join25 } from "path";
37526
37684
  var SERVER_MANAGED_KEYS = [
37527
37685
  "physicPath",
37528
37686
  "sessionSwitchPct",
@@ -37539,8 +37697,8 @@ var SERVER_MANAGED_KEYS = [
37539
37697
  "projectAiMemoryBudgetChars"
37540
37698
  ];
37541
37699
  function readConfigText(profileDir2) {
37542
- const path = join24(profileDir2, "config.json");
37543
- if (existsSync12(path)) {
37700
+ const path = join25(profileDir2, "config.json");
37701
+ if (existsSync13(path)) {
37544
37702
  try {
37545
37703
  return readFileSync15(path, "utf8");
37546
37704
  } catch {
@@ -37610,8 +37768,9 @@ var WsClient = class {
37610
37768
  context.credential.serverUrl,
37611
37769
  context.profileDir,
37612
37770
  context.bus,
37613
- // After the idle daily tick updates flagged tools, report the fresh snapshot (ADR-0254).
37614
- () => void this.reportWorkerTools()
37771
+ // After the idle daily tick updates flagged tools, report the fresh snapshot (ADR-0254). The
37772
+ // `daily` trigger lets the server re-drive a still-failing restore on this spaced tick (ADR-0258).
37773
+ () => void this.reportWorkerTools("daily")
37615
37774
  );
37616
37775
  context.bus.onLocalSubmit((input) => void this.runLocalCommand(input));
37617
37776
  context.bus.onReconnect(() => this.reconnectNow());
@@ -37731,13 +37890,21 @@ var WsClient = class {
37731
37890
  /**
37732
37891
  * Detect the worker's tools and report the snapshot to the server (ADR-0254, `tools.report`) so the
37733
37892
  * DB-backed Tools panel + restore target stay current. One-way, best-effort — a detect/send failure
37734
- * never disrupts the session. Called after each tool op, after the daily tick, and after a restore.
37893
+ * never disrupts the session. Called after each tool op (`op`), after the daily tick (`daily`), and
37894
+ * after a restore reconcile (`boot`/`manual`). `trigger` gates the server re-drive (ADR-0258: only
37895
+ * `daily` re-drives a still-failing restore); `restoreFailed` is passed only after a reconcile — a
37896
+ * plain `op`/`daily` report omits it so the server preserves the last known failed set.
37735
37897
  */
37736
- async reportWorkerTools() {
37898
+ async reportWorkerTools(trigger = "op", restoreFailed) {
37737
37899
  try {
37738
37900
  const autoUpdate = readProfileConfig(this.context.profileDir).autoUpdateTools ?? [];
37739
37901
  const { catalog, extras } = await detectWorkerTools(autoUpdate);
37740
- this.send(WsChannels.TOOLS_REPORT, { catalog, extras });
37902
+ this.send(WsChannels.TOOLS_REPORT, {
37903
+ catalog,
37904
+ extras,
37905
+ trigger,
37906
+ ...restoreFailed !== void 0 ? { restoreFailed } : {}
37907
+ });
37741
37908
  } catch (err) {
37742
37909
  logger.warn("tools.report.error", { error: String(err) });
37743
37910
  }
@@ -37883,6 +38050,7 @@ var WsClient = class {
37883
38050
  pruneCommandHistoryByAge(token.cliRetentionDays);
37884
38051
  pruneCommandOutputByAge(token.cliRetentionDays);
37885
38052
  }
38053
+ if (this.physicRoot) sweepOldAttachments(this.physicRoot);
37886
38054
  writeProfileConfig(this.context.profileDir, {
37887
38055
  sessionSwitchPct: token.projectTokens?.sessionSwitchPct ?? 0,
37888
38056
  perPromptTokenLimit: token.projectTokens?.perPromptTokenLimit ?? 0,
@@ -38051,8 +38219,8 @@ var WsClient = class {
38051
38219
  const oldRoot = this.physicFolderPath(sync.oldName);
38052
38220
  if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
38053
38221
  }
38054
- if (sync.oldName) rmSync4(join25(this.context.profileDir, sync.oldName), { recursive: true, force: true });
38055
- mkdirSync5(join25(this.context.profileDir, sync.newName), { recursive: true });
38222
+ if (sync.oldName) rmSync5(join26(this.context.profileDir, sync.oldName), { recursive: true, force: true });
38223
+ mkdirSync6(join26(this.context.profileDir, sync.newName), { recursive: true });
38056
38224
  this.physicRoot = this.physicFolderPath(sync.newName);
38057
38225
  this.bus.setProject(sync.newName);
38058
38226
  this.bus.log(`Physic project folder synced \u2192 ${sync.newName}`);
@@ -38063,7 +38231,7 @@ var WsClient = class {
38063
38231
  if (del.name) {
38064
38232
  const delRoot = this.physicFolderPath(del.name);
38065
38233
  if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
38066
- rmSync4(join25(this.context.profileDir, del.name), { recursive: true, force: true });
38234
+ rmSync5(join26(this.context.profileDir, del.name), { recursive: true, force: true });
38067
38235
  this.bus.log(`Physic project folder deleted (project removed) \u2192 ${del.name}`);
38068
38236
  }
38069
38237
  void manageSshKey("delete", this.context.profileDir).catch(() => void 0);
@@ -38104,7 +38272,13 @@ var WsClient = class {
38104
38272
  if (dispatch.cmd.trimStart().startsWith("/")) {
38105
38273
  void this.runWebSlashCommand(dispatch.cmd, dispatch.commandId);
38106
38274
  } else {
38107
- void this.runAiPrompt(dispatch.cmd, dispatch.commandId, "server", dispatch.aiOneShot ?? false);
38275
+ void this.runAiPrompt(
38276
+ dispatch.cmd,
38277
+ dispatch.commandId,
38278
+ "server",
38279
+ dispatch.aiOneShot ?? false,
38280
+ dispatch.images
38281
+ );
38108
38282
  }
38109
38283
  break;
38110
38284
  }
@@ -38406,11 +38580,15 @@ var WsClient = class {
38406
38580
  const req = payload;
38407
38581
  const op = message.channel === WsChannels.TOOLS_INSTALL ? "install" : message.channel === WsChannels.TOOLS_UPDATE ? "update" : "uninstall";
38408
38582
  this.send(message.channel, { started: true }, message.id);
38583
+ const opTimeoutMs = resolveInstallTimeoutMs(
38584
+ readProfileConfig(this.context.profileDir).toolInstallTimeoutSec
38585
+ );
38409
38586
  void runWorkerToolOp(
38410
38587
  op,
38411
38588
  req.name,
38412
38589
  req.manager,
38413
- (line) => this.send(WsChannels.TOOLS_PROGRESS, { opId: req.opId, line })
38590
+ (line) => this.send(WsChannels.TOOLS_PROGRESS, { opId: req.opId, line }),
38591
+ opTimeoutMs
38414
38592
  ).then((res) => {
38415
38593
  this.send(WsChannels.TOOLS_DONE, {
38416
38594
  opId: req.opId,
@@ -38424,16 +38602,45 @@ var WsClient = class {
38424
38602
  }
38425
38603
  case WsChannels.TOOLS_RESTORE: {
38426
38604
  const req = payload;
38427
- void reconcileTools(
38428
- req.manifest.map((m) => ({ name: m.name, version: m.version, manager: m.manager })),
38429
- (line) => this.bus.log(line)
38430
- ).then(() => this.reportWorkerTools()).then(() => this.send(WsChannels.TOOLS_RESTORE, { ok: true }, message.id)).catch(
38431
- (err) => this.send(
38432
- WsChannels.TOOLS_RESTORE,
38433
- { ok: false, error: String(err) },
38434
- message.id
38435
- )
38605
+ const restoreTrigger = req.trigger === "manual" ? "manual" : "boot";
38606
+ const installTimeoutMs = resolveInstallTimeoutMs(
38607
+ readProfileConfig(this.context.profileDir).toolInstallTimeoutSec
38436
38608
  );
38609
+ const manifest = req.manifest.map((m) => ({ name: m.name, version: m.version, manager: m.manager }));
38610
+ const opId = req.opId;
38611
+ if (opId) {
38612
+ this.send(WsChannels.TOOLS_RESTORE, { ok: true }, message.id);
38613
+ void reconcileTools(
38614
+ manifest,
38615
+ (line) => {
38616
+ this.bus.log(line);
38617
+ this.send(WsChannels.TOOLS_PROGRESS, { opId, line });
38618
+ },
38619
+ installTimeoutMs
38620
+ ).then(async (restoreFailed) => {
38621
+ this.send(WsChannels.TOOLS_DONE, {
38622
+ opId,
38623
+ ok: restoreFailed.length === 0,
38624
+ exitCode: restoreFailed.length === 0 ? 0 : 1
38625
+ });
38626
+ await this.reportWorkerTools(restoreTrigger, restoreFailed);
38627
+ }).catch(
38628
+ (err) => this.send(WsChannels.TOOLS_DONE, {
38629
+ opId,
38630
+ ok: false,
38631
+ exitCode: 1,
38632
+ error: String(err)
38633
+ })
38634
+ );
38635
+ } else {
38636
+ void reconcileTools(manifest, (line) => this.bus.log(line), installTimeoutMs).then((restoreFailed) => this.reportWorkerTools(restoreTrigger, restoreFailed)).then(() => this.send(WsChannels.TOOLS_RESTORE, { ok: true }, message.id)).catch(
38637
+ (err) => this.send(
38638
+ WsChannels.TOOLS_RESTORE,
38639
+ { ok: false, error: String(err) },
38640
+ message.id
38641
+ )
38642
+ );
38643
+ }
38437
38644
  break;
38438
38645
  }
38439
38646
  case WsChannels.GIT_DIFF:
@@ -38481,7 +38688,7 @@ var WsClient = class {
38481
38688
  }
38482
38689
  case WsChannels.LOG_READ: {
38483
38690
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
38484
- const lines = readRecentLogLines(join25(this.context.profileDir, "logs"), limit);
38691
+ const lines = readRecentLogLines(join26(this.context.profileDir, "logs"), limit);
38485
38692
  this.send(WsChannels.LOG_READ, { lines }, message.id);
38486
38693
  break;
38487
38694
  }
@@ -38560,7 +38767,7 @@ var WsClient = class {
38560
38767
  * exactly like typing the prompt in the cli (tries profiles, meters real tokens) instead
38561
38768
  * of spawning the text as a raw command. Streams output to the transcript AND the server.
38562
38769
  */
38563
- async runAiPrompt(prompt, commandId, origin, oneShot = false) {
38770
+ async runAiPrompt(prompt, commandId, origin, oneShot = false, images) {
38564
38771
  const config = readProfileConfig(this.context.profileDir);
38565
38772
  const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
38566
38773
  const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
@@ -38618,7 +38825,18 @@ var WsClient = class {
38618
38825
  text: `session ${this.usageSnapshot?.session.utilizationPct ?? 0}% \u2265 ${config.sessionSwitchPct}% \u2014 switching to Claude profile "${labelFromCredentialKey(startCred)}"`
38619
38826
  });
38620
38827
  }
38621
- const guardedPrompt = this.restrictToFolder && this.physicRoot ? folderScopeGuard(this.physicRoot, prompt) : prompt;
38828
+ if (this.physicRoot) sweepOldAttachments(this.physicRoot);
38829
+ let aiBodyPrompt = prompt;
38830
+ if (images?.length && this.physicRoot && !oneShot) {
38831
+ const paths = await materializeImages(
38832
+ this.physicRoot,
38833
+ commandId,
38834
+ images,
38835
+ (imageId) => this.imageFetch(commandId, imageId)
38836
+ );
38837
+ if (paths.size) aiBodyPrompt = rewriteImagePlaceholders(prompt, paths);
38838
+ }
38839
+ const guardedPrompt = this.restrictToFolder && this.physicRoot ? folderScopeGuard(this.physicRoot, aiBodyPrompt) : aiBodyPrompt;
38622
38840
  const hint = unified ? { credential: startCred ?? workingCred } : { dir: startDir ?? workingDir };
38623
38841
  const memCfg = resolveMemoryConfig(config);
38624
38842
  const firstAttempt = planAiRun(guardedPrompt, config, hint).attempts[0];
@@ -39202,6 +39420,13 @@ ${guardedPrompt}`;
39202
39420
  checkQuota(metric, amount = 1) {
39203
39421
  return this.request(WsChannels.QUOTA_CHECK, { metric, amount });
39204
39422
  }
39423
+ /** Fetch a Console prompt image blob from the server to materialize on the worker (ADR-0257). */
39424
+ imageFetch(commandId, imageId) {
39425
+ return this.request(WsChannels.IMAGE_FETCH, {
39426
+ commandId,
39427
+ imageId
39428
+ });
39429
+ }
39205
39430
  /** usage.report batched to the server (ADR-0020); tags each event with the AI profile. */
39206
39431
  reportUsage(events, profile) {
39207
39432
  const tagged = profile ? events.map((e) => ({ ...e, profile })) : events;
@@ -39239,8 +39464,8 @@ ${guardedPrompt}`;
39239
39464
  /** Create the physic folder at an absolute path if missing (best-effort). */
39240
39465
  ensurePhysicFolderPath(folder) {
39241
39466
  try {
39242
- if (!existsSync13(folder)) {
39243
- mkdirSync5(folder, { recursive: true });
39467
+ if (!existsSync14(folder)) {
39468
+ mkdirSync6(folder, { recursive: true });
39244
39469
  this.bus.log(`Created physic project folder: ${folder}`);
39245
39470
  }
39246
39471
  } catch (err) {
@@ -39254,7 +39479,7 @@ ${guardedPrompt}`;
39254
39479
  */
39255
39480
  physicFolderPath(projectName) {
39256
39481
  const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
39257
- return safe ? join25(this.context.profileDir, safe) : null;
39482
+ return safe ? join26(this.context.profileDir, safe) : null;
39258
39483
  }
39259
39484
  /**
39260
39485
  * Report machine.status: version + the state of the physic project being served
@@ -39269,7 +39494,7 @@ ${guardedPrompt}`;
39269
39494
  projects: [],
39270
39495
  version: CLI_VERSION,
39271
39496
  physicPath,
39272
- physicPathExists: physicPath ? existsSync13(physicPath) : void 0,
39497
+ physicPathExists: physicPath ? existsSync14(physicPath) : void 0,
39273
39498
  // Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
39274
39499
  autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false,
39275
39500
  // The machine-user AI-run limit (0 = unlimited) + profile count so a dispatch can resolve the
@@ -39363,7 +39588,7 @@ ${guardedPrompt}`;
39363
39588
  */
39364
39589
  uploadLog() {
39365
39590
  try {
39366
- const upload = readLogUpload(join25(this.context.profileDir, "logs"));
39591
+ const upload = readLogUpload(join26(this.context.profileDir, "logs"));
39367
39592
  if (!upload) return;
39368
39593
  const sig = `${upload.fileName}:${upload.totalBytes}`;
39369
39594
  if (sig === this.lastLogSig) return;
@@ -39391,10 +39616,10 @@ ${guardedPrompt}`;
39391
39616
  };
39392
39617
 
39393
39618
  // src/core/instance-lock.ts
39394
- import { readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
39395
- import { join as join26 } from "path";
39619
+ import { readFileSync as readFileSync16, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
39620
+ import { join as join27 } from "path";
39396
39621
  function lockPath(profileDir2) {
39397
- return join26(profileDir2, "start.lock");
39622
+ return join27(profileDir2, "start.lock");
39398
39623
  }
39399
39624
  function isAlive(pid) {
39400
39625
  try {
@@ -39414,13 +39639,13 @@ function lockHolder(profileDir2) {
39414
39639
  }
39415
39640
  function acquireInstanceLock(profileDir2) {
39416
39641
  if (lockHolder(profileDir2) !== null) return false;
39417
- writeFileSync7(lockPath(profileDir2), String(process.pid), "utf8");
39642
+ writeFileSync8(lockPath(profileDir2), String(process.pid), "utf8");
39418
39643
  return true;
39419
39644
  }
39420
39645
  function releaseInstanceLock(profileDir2) {
39421
39646
  try {
39422
39647
  const pid = Number.parseInt(readFileSync16(lockPath(profileDir2), "utf8").trim(), 10);
39423
- if (pid === process.pid) rmSync5(lockPath(profileDir2), { force: true });
39648
+ if (pid === process.pid) rmSync6(lockPath(profileDir2), { force: true });
39424
39649
  } catch {
39425
39650
  }
39426
39651
  }
@@ -39693,8 +39918,8 @@ var SessionBus = class {
39693
39918
 
39694
39919
  // src/core/control-server.ts
39695
39920
  import { createServer } from "net";
39696
- import { rmSync as rmSync6 } from "fs";
39697
- import { join as join27 } from "path";
39921
+ import { rmSync as rmSync7 } from "fs";
39922
+ import { join as join28 } from "path";
39698
39923
 
39699
39924
  // src/core/control-protocol.ts
39700
39925
  var CONTROL_SOCKET_FILE = "control.sock";
@@ -39723,9 +39948,9 @@ function createFrameParser() {
39723
39948
 
39724
39949
  // src/core/control-server.ts
39725
39950
  function startControlServer(bus, profileDir2, info) {
39726
- const socketPath = join27(profileDir2, CONTROL_SOCKET_FILE);
39951
+ const socketPath = join28(profileDir2, CONTROL_SOCKET_FILE);
39727
39952
  try {
39728
- rmSync6(socketPath, { force: true });
39953
+ rmSync7(socketPath, { force: true });
39729
39954
  } catch {
39730
39955
  }
39731
39956
  const clients = /* @__PURE__ */ new Set();
@@ -39791,7 +40016,7 @@ function startControlServer(bus, profileDir2, info) {
39791
40016
  clients.clear();
39792
40017
  server.close();
39793
40018
  try {
39794
- rmSync6(socketPath, { force: true });
40019
+ rmSync7(socketPath, { force: true });
39795
40020
  } catch {
39796
40021
  }
39797
40022
  };
@@ -45454,15 +45679,15 @@ var import_react21 = __toESM(require_react(), 1);
45454
45679
  var import_react26 = __toESM(require_react(), 1);
45455
45680
 
45456
45681
  // src/core/input-history.ts
45457
- import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
45458
- import { join as join28 } from "path";
45682
+ import { existsSync as existsSync16, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
45683
+ import { join as join29 } from "path";
45459
45684
  var MAX_HISTORY = 200;
45460
45685
  function historyPath(profileDir2) {
45461
- return join28(profileDir2, "input-history.json");
45686
+ return join29(profileDir2, "input-history.json");
45462
45687
  }
45463
45688
  function loadInputHistory(profileDir2) {
45464
45689
  const path = historyPath(profileDir2);
45465
- if (!existsSync15(path)) return [];
45690
+ if (!existsSync16(path)) return [];
45466
45691
  try {
45467
45692
  const data = JSON.parse(readFileSync18(path, "utf8"));
45468
45693
  return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
@@ -45475,7 +45700,7 @@ function appendInputHistory(profileDir2, line, current) {
45475
45700
  if (!trimmed || current[current.length - 1] === trimmed) return current;
45476
45701
  const next = [...current, trimmed].slice(-MAX_HISTORY);
45477
45702
  try {
45478
- writeFileSync8(historyPath(profileDir2), JSON.stringify(next), "utf8");
45703
+ writeFileSync9(historyPath(profileDir2), JSON.stringify(next), "utf8");
45479
45704
  } catch {
45480
45705
  }
45481
45706
  return next;
@@ -46079,7 +46304,7 @@ async function runStart(profileDir2, profileName) {
46079
46304
  return;
46080
46305
  }
46081
46306
  logger.configure({
46082
- dir: join29(profileDir2, "logs"),
46307
+ dir: join30(profileDir2, "logs"),
46083
46308
  level: resolveLogLevel(),
46084
46309
  base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
46085
46310
  });
@@ -46189,7 +46414,7 @@ async function runStart(profileDir2, profileName) {
46189
46414
  }
46190
46415
 
46191
46416
  // src/commands/attach.ts
46192
- import { join as join30 } from "path";
46417
+ import { join as join31 } from "path";
46193
46418
 
46194
46419
  // src/core/control-client.ts
46195
46420
  import { connect as connect2 } from "net";
@@ -46288,7 +46513,7 @@ async function runAttach(profileDir2, profileName) {
46288
46513
  process.exitCode = 1;
46289
46514
  return;
46290
46515
  }
46291
- const socketPath = join30(profileDir2, CONTROL_SOCKET_FILE);
46516
+ const socketPath = join31(profileDir2, CONTROL_SOCKET_FILE);
46292
46517
  const bus = new SessionBus();
46293
46518
  let conn;
46294
46519
  try {
@@ -46321,7 +46546,7 @@ async function runAttach(profileDir2, profileName) {
46321
46546
 
46322
46547
  // src/commands/ai-login.ts
46323
46548
  import { spawn as spawn11 } from "child_process";
46324
- import { mkdirSync as mkdirSync6 } from "fs";
46549
+ import { mkdirSync as mkdirSync7 } from "fs";
46325
46550
  import { basename as basename5 } from "path";
46326
46551
  function runProviderLogin(cmd, envVar, dir2) {
46327
46552
  return new Promise((resolve8) => {
@@ -46359,7 +46584,7 @@ async function runAiLogin(profileDir2, profileName, aiFilter) {
46359
46584
  continue;
46360
46585
  }
46361
46586
  const dir2 = resolveHomePath(cred.profile);
46362
- mkdirSync6(dir2, { recursive: true });
46587
+ mkdirSync7(dir2, { recursive: true });
46363
46588
  console.log(`
46364
46589
  \u25B6 Logging in ${cred.provider} \xB7 ${cred.profile} (${envVar}=${dir2})`);
46365
46590
  console.log(` Complete the provider's own sign-in (claude: type \`/login\`; codex: follow its prompt), then exit.`);
@@ -46373,7 +46598,7 @@ Tip: for the credentials to survive \`docker rm\`/recreate, keep each profile pa
46373
46598
  }
46374
46599
 
46375
46600
  // src/commands/unlink.ts
46376
- import { rmSync as rmSync7 } from "fs";
46601
+ import { rmSync as rmSync8 } from "fs";
46377
46602
  async function runUnlink(profileDir2, profileName) {
46378
46603
  const cred = readCredential(profileDir2, profileName);
46379
46604
  if (!cred) {
@@ -46388,7 +46613,7 @@ async function runUnlink(profileDir2, profileName) {
46388
46613
  `\u26A0 Could not reach the server to revoke \u2014 revoke it at web /machines. (${err.message})`
46389
46614
  );
46390
46615
  }
46391
- rmSync7(profileDir2, { recursive: true, force: true });
46616
+ rmSync8(profileDir2, { recursive: true, force: true });
46392
46617
  clearDefaultProfileIf(profileName);
46393
46618
  console.log("\u2714 Deleted the profile and its physic project folder.");
46394
46619
  }