@4pm/cli 1.5.11 → 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 +451 -200
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28323,14 +28323,14 @@ function defaultProfileConfig() {
28323
28323
  webBlockedCommands: ["quit", "config"],
28324
28324
  // Shared AI memory (ADR-0245) — off by default (opt-in; costs a compaction call per turn).
28325
28325
  aiMemoryEnabled: false,
28326
- aiMemoryBudgetChars: 6e3
28326
+ aiMemoryBudgetChars: 1e3
28327
28327
  };
28328
28328
  }
28329
28329
  function resolveMemoryConfig(config) {
28330
28330
  const mode = config.projectAiMemoryMode ?? "inherit";
28331
28331
  const enabled = mode === "on" ? true : mode === "off" ? false : config.aiMemoryEnabled ?? false;
28332
28332
  const projectBudget = config.projectAiMemoryBudgetChars ?? 0;
28333
- const budgetChars = projectBudget > 0 ? projectBudget : config.aiMemoryBudgetChars ?? 6e3;
28333
+ const budgetChars = projectBudget > 0 ? projectBudget : config.aiMemoryBudgetChars ?? 1e3;
28334
28334
  return { enabled, budgetChars };
28335
28335
  }
28336
28336
  function resolveIdleAutoClearMinutes(config) {
@@ -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.
@@ -33760,6 +33790,10 @@ var WsChannels = {
33760
33790
  // server → cli: the project was deleted ⇒ delete the physic folder; the cli keeps
33761
33791
  // its pairing and goes idle (ADR-0068).
33762
33792
  PHYSIC_DELETE: "physic.delete",
33793
+ // server → cli (ADR-0256): a project's token settings were saved ⇒ push the fresh knobs to every
33794
+ // serving cli so a change (e.g. aiRunTimeoutSec) applies on the next run, not only after a
33795
+ // reconnect. `ws_token` seeds these on connect; this channel only carries updates.
33796
+ PROJECT_TOKENS: "project.tokens",
33763
33797
  // command lifecycle
33764
33798
  COMMAND_ANNOUNCE: "command.announce",
33765
33799
  // console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
@@ -33798,6 +33832,9 @@ var WsChannels = {
33798
33832
  // the worker uploads its pending log tail (+ any queued history) then acks, so the release
33799
33833
  // sweep can finalise without waiting the full 5-minute timeout when the worker is online.
33800
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",
33801
33838
  // outbound review (ADR-0082): machine→server request, server→outbound evaluate, result back
33802
33839
  REVIEW_REQUEST: "review.request",
33803
33840
  REVIEW_EVALUATE: "review.evaluate",
@@ -33867,7 +33904,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
33867
33904
  }
33868
33905
 
33869
33906
  // src/core/ws-client.ts
33870
- 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";
33871
33908
  import { randomUUID as randomCommandId } from "crypto";
33872
33909
 
33873
33910
  // src/core/update-scheduler.ts
@@ -33979,6 +34016,35 @@ ${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
33979
34016
  // src/core/worker-tools.ts
33980
34017
  import { spawn } from "child_process";
33981
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
+ }
33982
34048
  function run(cmd, args, onLine, timeoutMs = 18e4) {
33983
34049
  return new Promise((resolve8) => {
33984
34050
  let out = "";
@@ -34095,13 +34161,13 @@ function opArgs(op, manager, pkg) {
34095
34161
  const target = op === "update" ? `${pkg}@latest` : pkg;
34096
34162
  return manager === "pnpm" ? ["add", "-g", target] : ["install", "-g", target];
34097
34163
  }
34098
- async function runWorkerToolOp(op, name, manager, onLine) {
34164
+ async function runWorkerToolOp(op, name, manager, onLine, timeoutMs = TOOL_INSTALL_TIMEOUT_DEFAULT_MS) {
34099
34165
  const resolved = resolvePackage(name, op);
34100
34166
  if (resolved.error || !resolved.pkg) {
34101
34167
  return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
34102
34168
  }
34103
34169
  onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
34104
- const { code } = await run(manager, opArgs(op, manager, resolved.pkg), onLine);
34170
+ const { code } = await run(manager, opArgs(op, manager, resolved.pkg), onLine, timeoutMs);
34105
34171
  return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
34106
34172
  }
34107
34173
  function setToolAutoUpdate(profileDir2, name, enabled) {
@@ -34114,25 +34180,40 @@ function setToolAutoUpdate(profileDir2, name, enabled) {
34114
34180
  writeProfileConfig(profileDir2, { autoUpdateTools: [...set] });
34115
34181
  return { ok: true };
34116
34182
  }
34117
- async function autoUpdateFlaggedTools(tools, onLine) {
34183
+ async function autoUpdateFlaggedTools(tools, onLine, timeoutMs = TOOL_INSTALL_TIMEOUT_DEFAULT_MS) {
34118
34184
  for (const name of tools) {
34119
- const res = await runWorkerToolOp("update", name, "npm", onLine).catch(
34185
+ const res = await runWorkerToolOp("update", name, "npm", onLine, timeoutMs).catch(
34120
34186
  (err) => ({ ok: false, exitCode: 1, error: String(err) })
34121
34187
  );
34122
34188
  onLine(res.ok ? `\u2713 ${name} updated` : `\u2717 ${name}: ${res.error ?? "failed"}`);
34123
34189
  }
34124
34190
  }
34125
- async function installPinned(name, version, manager, onLine) {
34191
+ async function installPinned(name, version, manager, onLine, timeoutMs = TOOL_INSTALL_TIMEOUT_DEFAULT_MS) {
34126
34192
  const resolved = resolvePackage(name, "update");
34127
- 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
+ }
34128
34196
  const target = `${resolved.pkg}@${version}`;
34129
34197
  const args = manager === "pnpm" ? ["add", "-g", target] : ["install", "-g", target];
34130
- onLine(`$ ${manager} ${args.join(" ")}`);
34131
- const { code } = await run(manager, args, onLine);
34132
- return { ok: code === 0, error: code === 0 ? void 0 : `${manager} exited ${code}` };
34133
- }
34134
- async function reconcileTools(manifest, onLine) {
34135
- 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;
34136
34217
  const { catalog, extras } = await detectWorkerTools();
34137
34218
  const versionByName = new Map(
34138
34219
  [...catalog, ...extras].filter((r) => r.installed && r.version).map((r) => [r.id, r.version])
@@ -34141,11 +34222,27 @@ async function reconcileTools(manifest, onLine) {
34141
34222
  const current = versionByName.get(entry.name);
34142
34223
  if (current && current.includes(entry.version)) continue;
34143
34224
  onLine(`Restoring ${entry.name}@${entry.version}\u2026`);
34144
- const res = await installPinned(entry.name, entry.version, entry.manager, onLine).catch(
34145
- (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
+ })
34146
34232
  );
34147
- 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
+ }
34148
34244
  }
34245
+ return failed;
34149
34246
  }
34150
34247
 
34151
34248
  // src/core/update-scheduler.ts
@@ -34259,10 +34356,12 @@ var UpdateScheduler = class {
34259
34356
  }
34260
34357
  /** Update each tool flagged for auto-update in config.json to @latest (ADR-0253) — best-effort. */
34261
34358
  async updateFlaggedTools() {
34262
- const tools = readProfileConfig(this.profileDir).autoUpdateTools ?? [];
34359
+ const config = readProfileConfig(this.profileDir);
34360
+ const tools = config.autoUpdateTools ?? [];
34263
34361
  if (tools.length === 0) return;
34264
34362
  this.bus.log(`Scheduled tool auto-update: ${tools.join(", ")}\u2026`);
34265
- 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(
34266
34365
  (err) => logger.warn("update.tool.error", { error: String(err) })
34267
34366
  );
34268
34367
  this.onToolsChanged?.();
@@ -35247,23 +35346,86 @@ function finishCommand(commandId, exitCode, usage) {
35247
35346
  persist();
35248
35347
  }
35249
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
+
35250
35412
  // src/core/command-output-store.ts
35251
35413
  import {
35252
35414
  appendFileSync,
35253
- existsSync as existsSync7,
35254
- mkdirSync as mkdirSync3,
35415
+ existsSync as existsSync8,
35416
+ mkdirSync as mkdirSync4,
35255
35417
  readFileSync as readFileSync11,
35256
- readdirSync as readdirSync3,
35257
- rmSync as rmSync3,
35258
- statSync as statSync2
35418
+ readdirSync as readdirSync4,
35419
+ rmSync as rmSync4,
35420
+ statSync as statSync3
35259
35421
  } from "fs";
35260
- import { join as join11 } from "path";
35422
+ import { join as join12 } from "path";
35261
35423
  var KEEP_FILES = 200;
35262
35424
  var dir = null;
35263
35425
  function initCommandOutput(profileDir2) {
35264
- dir = join11(profileDir2, "command-output");
35426
+ dir = join12(profileDir2, "command-output");
35265
35427
  try {
35266
- mkdirSync3(dir, { recursive: true });
35428
+ mkdirSync4(dir, { recursive: true });
35267
35429
  prune();
35268
35430
  } catch {
35269
35431
  }
@@ -35271,14 +35433,14 @@ function initCommandOutput(profileDir2) {
35271
35433
  function appendCommandOutput(commandId, text) {
35272
35434
  if (!dir || !text) return;
35273
35435
  try {
35274
- appendFileSync(join11(dir, `${commandId}.log`), text, "utf8");
35436
+ appendFileSync(join12(dir, `${commandId}.log`), text, "utf8");
35275
35437
  } catch {
35276
35438
  }
35277
35439
  }
35278
35440
  function readCommandOutput(commandId) {
35279
35441
  if (!dir) return null;
35280
- const path = join11(dir, `${commandId}.log`);
35281
- if (!existsSync7(path)) return null;
35442
+ const path = join12(dir, `${commandId}.log`);
35443
+ if (!existsSync8(path)) return null;
35282
35444
  try {
35283
35445
  return readFileSync11(path, "utf8");
35284
35446
  } catch {
@@ -35288,8 +35450,8 @@ function readCommandOutput(commandId) {
35288
35450
  function prune() {
35289
35451
  if (!dir) return;
35290
35452
  try {
35291
- 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);
35292
- 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 });
35293
35455
  } catch {
35294
35456
  }
35295
35457
  }
@@ -35297,9 +35459,9 @@ function pruneCommandOutputByAge(days) {
35297
35459
  if (!dir || days <= 0) return;
35298
35460
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
35299
35461
  try {
35300
- for (const f of readdirSync3(dir).filter((n) => n.endsWith(".log"))) {
35301
- const p = join11(dir, f);
35302
- 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 });
35303
35465
  }
35304
35466
  } catch {
35305
35467
  }
@@ -35376,8 +35538,8 @@ async function writeWorkerFile(root, relPath, content) {
35376
35538
  import { spawn as spawn5 } from "child_process";
35377
35539
  import { execFile as execFile2 } from "child_process";
35378
35540
  import { createHash as createHash3 } from "crypto";
35379
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
35380
- 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";
35381
35543
  import { promisify as promisify2 } from "util";
35382
35544
  var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
35383
35545
  var execFileAsync = promisify2(execFile2);
@@ -35385,11 +35547,11 @@ var MAX_CONTEXT_CHARS = 12e4;
35385
35547
  var ANSWER_TIMEOUT_MS = 12e4;
35386
35548
  var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
35387
35549
  function kbRoot(profileDir2) {
35388
- return join12(profileDir2, "support-kb");
35550
+ return join13(profileDir2, "support-kb");
35389
35551
  }
35390
35552
  function repoDir(profileDir2, url, branch) {
35391
35553
  const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
35392
- return join12(kbRoot(profileDir2), key);
35554
+ return join13(kbRoot(profileDir2), key);
35393
35555
  }
35394
35556
  function authedUrl(url, token) {
35395
35557
  if (!token) return url;
@@ -35398,10 +35560,10 @@ function authedUrl(url, token) {
35398
35560
  }
35399
35561
  async function ensureRepo(profileDir2, repo) {
35400
35562
  const dir2 = repoDir(profileDir2, repo.url, repo.branch);
35401
- if (existsSync8(join12(dir2, ".git"))) return dir2;
35563
+ if (existsSync9(join13(dir2, ".git"))) return dir2;
35402
35564
  const url = authedUrl(repo.url, repo.token);
35403
35565
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
35404
- mkdirSync4(kbRoot(profileDir2), { recursive: true });
35566
+ mkdirSync5(kbRoot(profileDir2), { recursive: true });
35405
35567
  await execFileAsync("git", ["clone", "--depth", "1", "--branch", repo.branch, url, dir2], {
35406
35568
  env: gitEnv
35407
35569
  });
@@ -35409,11 +35571,11 @@ async function ensureRepo(profileDir2, repo) {
35409
35571
  }
35410
35572
  async function refreshSupportKb(profileDir2) {
35411
35573
  const root = kbRoot(profileDir2);
35412
- if (!existsSync8(root)) return;
35574
+ if (!existsSync9(root)) return;
35413
35575
  const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
35414
- for (const entry of readdirSync4(root)) {
35415
- const dir2 = join12(root, entry);
35416
- 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;
35417
35579
  try {
35418
35580
  await execFileAsync("git", ["-C", dir2, "pull", "--ff-only"], { env: gitEnv });
35419
35581
  } catch (err) {
@@ -35426,10 +35588,10 @@ function collectDocs(dir2) {
35426
35588
  let total = 0;
35427
35589
  const walk2 = (current) => {
35428
35590
  if (total >= MAX_CONTEXT_CHARS) return;
35429
- for (const entry of readdirSync4(current)) {
35591
+ for (const entry of readdirSync5(current)) {
35430
35592
  if (entry === ".git" || entry === "node_modules") continue;
35431
- const full = join12(current, entry);
35432
- const st = statSync3(full);
35593
+ const full = join13(current, entry);
35594
+ const st = statSync4(full);
35433
35595
  if (st.isDirectory()) {
35434
35596
  walk2(full);
35435
35597
  } else if (DOC_EXTENSIONS.some((ext) => entry.toLowerCase().endsWith(ext))) {
@@ -35653,7 +35815,7 @@ async function runKnowledgeCompose(req, ai, physicRoot) {
35653
35815
  import { readFile as readFile3, writeFile as writeFile2, chmod } from "fs/promises";
35654
35816
  import { readFileSync as readFileSync13 } from "fs";
35655
35817
  import { execFile as execFile3, spawn as spawn7 } from "child_process";
35656
- import { join as join13 } from "path";
35818
+ import { join as join14 } from "path";
35657
35819
  import { promisify as promisify3 } from "util";
35658
35820
  var run3 = promisify3(execFile3);
35659
35821
  var SETTINGS_REL = ".claude/.autonomous.settings.json";
@@ -35685,7 +35847,7 @@ function parseJson(text) {
35685
35847
  }
35686
35848
  }
35687
35849
  function tickScript(root) {
35688
- return join13(root, TICK_REL);
35850
+ return join14(root, TICK_REL);
35689
35851
  }
35690
35852
  async function crontabList() {
35691
35853
  try {
@@ -35708,8 +35870,8 @@ async function cronInstalled(root) {
35708
35870
  return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
35709
35871
  }
35710
35872
  async function getAutonomousStatus(root) {
35711
- const settings = parseJson(await readText(join13(root, SETTINGS_REL))) ?? {};
35712
- 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))) ?? {};
35713
35875
  const records = Array.isArray(hist.records) ? hist.records : [];
35714
35876
  const last = records[records.length - 1];
35715
35877
  const ticks = hist.ticks ?? {};
@@ -35726,9 +35888,9 @@ async function getAutonomousStatus(root) {
35726
35888
  }
35727
35889
  function isAutonomousRunning(root) {
35728
35890
  try {
35729
- const s = JSON.parse(readFileSync13(join13(root, SETTINGS_REL), "utf8"));
35891
+ const s = JSON.parse(readFileSync13(join14(root, SETTINGS_REL), "utf8"));
35730
35892
  if (s.paused === true) return false;
35731
- const h = JSON.parse(readFileSync13(join13(root, HISTORIES_REL), "utf8"));
35893
+ const h = JSON.parse(readFileSync13(join14(root, HISTORIES_REL), "utf8"));
35732
35894
  const ts = h.records?.[h.records.length - 1]?.ts;
35733
35895
  if (!ts) return false;
35734
35896
  const t = Date.parse(ts.replace(" ", "T"));
@@ -35739,10 +35901,10 @@ function isAutonomousRunning(root) {
35739
35901
  }
35740
35902
  async function readAutonomous(root) {
35741
35903
  const [settings, approvals, status, ...books] = await Promise.all([
35742
- readText(join13(root, SETTINGS_REL)),
35743
- readText(join13(root, APPROVALS_REL), "{}"),
35904
+ readText(join14(root, SETTINGS_REL)),
35905
+ readText(join14(root, APPROVALS_REL), "{}"),
35744
35906
  getAutonomousStatus(root),
35745
- ...Object.values(BOOK_FILES).map((f) => readText(join13(root, f)))
35907
+ ...Object.values(BOOK_FILES).map((f) => readText(join14(root, f)))
35746
35908
  ]);
35747
35909
  const keys = Object.keys(BOOK_FILES);
35748
35910
  const bookMap = {};
@@ -35751,7 +35913,7 @@ async function readAutonomous(root) {
35751
35913
  }
35752
35914
  async function readAutonomousLogs(root, date) {
35753
35915
  const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
35754
- 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`));
35755
35917
  const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
35756
35918
  return { date: day, lines };
35757
35919
  }
@@ -35775,24 +35937,24 @@ async function writeAutonomous(root, req, by) {
35775
35937
  switch (req.kind) {
35776
35938
  case "settings": {
35777
35939
  if (!parseJson(req.settings)) return { ok: false, error: "settings is not valid JSON" };
35778
- await writeFile2(join13(root, SETTINGS_REL), req.settings, "utf8");
35940
+ await writeFile2(join14(root, SETTINGS_REL), req.settings, "utf8");
35779
35941
  break;
35780
35942
  }
35781
35943
  case "approvals": {
35782
- const map = parseJson(await readText(join13(root, APPROVALS_REL), "{}")) ?? {};
35944
+ const map = parseJson(await readText(join14(root, APPROVALS_REL), "{}")) ?? {};
35783
35945
  if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
35784
35946
  else delete map[req.taskId];
35785
- 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");
35786
35948
  break;
35787
35949
  }
35788
35950
  case "userTodo": {
35789
- const cur = await readText(join13(root, BOOK_FILES.userTodo));
35951
+ const cur = await readText(join14(root, BOOK_FILES.userTodo));
35790
35952
  const block = `
35791
35953
 
35792
35954
  > posted by ${by} at ${(/* @__PURE__ */ new Date()).toISOString()}
35793
35955
  ${req.content.trim()}
35794
35956
  `;
35795
- await writeFile2(join13(root, BOOK_FILES.userTodo), cur.trimEnd() + block, "utf8");
35957
+ await writeFile2(join14(root, BOOK_FILES.userTodo), cur.trimEnd() + block, "utf8");
35796
35958
  break;
35797
35959
  }
35798
35960
  case "cron": {
@@ -35809,7 +35971,7 @@ ${req.content.trim()}
35809
35971
 
35810
35972
  // src/core/agents.ts
35811
35973
  import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3, readdir as readdir3, rm } from "fs/promises";
35812
- import { join as join14 } from "path";
35974
+ import { join as join15 } from "path";
35813
35975
  var AGENTS_REL = ".claude/agents";
35814
35976
  var SKILLS_REL = ".claude/skills";
35815
35977
  var MEMORY_DIRS = /* @__PURE__ */ new Set(["short-memory", "long-memory"]);
@@ -35833,17 +35995,17 @@ async function listAgents(root) {
35833
35995
  const subagents = [];
35834
35996
  const skills = [];
35835
35997
  try {
35836
- 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 })) {
35837
35999
  if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
35838
- const fm = frontmatter(await readText2(join14(root, AGENTS_REL, e.name)));
36000
+ const fm = frontmatter(await readText2(join15(root, AGENTS_REL, e.name)));
35839
36001
  subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
35840
36002
  }
35841
36003
  } catch {
35842
36004
  }
35843
36005
  try {
35844
- 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 })) {
35845
36007
  if (!e.isDirectory()) continue;
35846
- 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")));
35847
36009
  skills.push({ name: e.name, description: fm.description });
35848
36010
  }
35849
36011
  } catch {
@@ -35854,15 +36016,15 @@ async function listAgents(root) {
35854
36016
  }
35855
36017
  function itemPath(root, kind, name) {
35856
36018
  const n = safeName(name);
35857
- 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");
35858
36020
  }
35859
36021
  async function readAgent(root, kind, name) {
35860
36022
  const content = await readText2(itemPath(root, kind, name));
35861
36023
  if (kind !== "subagent") return { content };
35862
36024
  const n = safeName(name);
35863
36025
  const [short, long] = await Promise.all([
35864
- readText2(join14(root, AGENTS_REL, "short-memory", `${n}.md`)),
35865
- 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`))
35866
36028
  ]);
35867
36029
  return { content, memory: { short, long } };
35868
36030
  }
@@ -35872,12 +36034,12 @@ async function writeAgent(root, req) {
35872
36034
  try {
35873
36035
  if (req.action === "delete") {
35874
36036
  if (req.kind === "subagent") await rm(itemPath(root, "subagent", n), { force: true });
35875
- 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 });
35876
36038
  return { ok: true };
35877
36039
  }
35878
36040
  if (req.content === void 0) return { ok: false, error: "content is required" };
35879
36041
  const path = itemPath(root, req.kind, n);
35880
- await mkdir3(join14(path, ".."), { recursive: true });
36042
+ await mkdir3(join15(path, ".."), { recursive: true });
35881
36043
  await writeFile3(path, req.content, "utf8");
35882
36044
  return { ok: true };
35883
36045
  } catch (err) {
@@ -35888,7 +36050,7 @@ async function writeAgent(root, req) {
35888
36050
  // src/core/packages.ts
35889
36051
  import { createHash as createHash4 } from "crypto";
35890
36052
  import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir4, readdir as readdir4, rm as rm2, stat } from "fs/promises";
35891
- import { dirname as dirname4, join as join15, relative } from "path";
36053
+ import { dirname as dirname4, join as join16, relative } from "path";
35892
36054
  var AGENTS_REL2 = ".claude/agents";
35893
36055
  var SKILLS_REL2 = ".claude/skills";
35894
36056
  var MANIFEST_REL = ".claude/.4pm-packages.json";
@@ -35912,7 +36074,7 @@ async function listFilesRel(dir2) {
35912
36074
  return;
35913
36075
  }
35914
36076
  for (const e of entries2) {
35915
- const full = join15(d, e.name);
36077
+ const full = join16(d, e.name);
35916
36078
  if (e.isDirectory()) await walk2(full);
35917
36079
  else if (e.isFile()) out.push(relative(dir2, full));
35918
36080
  }
@@ -35923,16 +36085,16 @@ async function listFilesRel(dir2) {
35923
36085
  async function readArtifactFiles(root, kind, slug) {
35924
36086
  const files = [];
35925
36087
  if (kind === "subagent") {
35926
- const agent = await readMaybe(join15(root, AGENTS_REL2, `${slug}.md`));
36088
+ const agent = await readMaybe(join16(root, AGENTS_REL2, `${slug}.md`));
35927
36089
  if (agent) files.push({ path: "agent.md", contentBase64: agent.toString("base64") });
35928
- 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`));
35929
36091
  if (short) files.push({ path: "short-memory.md", contentBase64: short.toString("base64") });
35930
- 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`));
35931
36093
  if (long) files.push({ path: "long-memory.md", contentBase64: long.toString("base64") });
35932
36094
  } else {
35933
- const base = join15(root, SKILLS_REL2, slug);
36095
+ const base = join16(root, SKILLS_REL2, slug);
35934
36096
  for (const rel of await listFilesRel(base)) {
35935
- const buf = await readMaybe(join15(base, rel));
36097
+ const buf = await readMaybe(join16(base, rel));
35936
36098
  if (buf) files.push({ path: rel.split("\\").join("/"), contentBase64: buf.toString("base64") });
35937
36099
  }
35938
36100
  }
@@ -35950,7 +36112,7 @@ async function artifactSha256(root, kind, slug) {
35950
36112
  return h.digest("hex");
35951
36113
  }
35952
36114
  async function artifactExists(root, kind, slug) {
35953
- 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);
35954
36116
  try {
35955
36117
  await stat(target);
35956
36118
  return true;
@@ -35959,7 +36121,7 @@ async function artifactExists(root, kind, slug) {
35959
36121
  }
35960
36122
  }
35961
36123
  async function readManifest(root) {
35962
- const buf = await readMaybe(join15(root, MANIFEST_REL));
36124
+ const buf = await readMaybe(join16(root, MANIFEST_REL));
35963
36125
  if (!buf) return {};
35964
36126
  try {
35965
36127
  return JSON.parse(buf.toString("utf8"));
@@ -35968,7 +36130,7 @@ async function readManifest(root) {
35968
36130
  }
35969
36131
  }
35970
36132
  async function writeManifest(root, manifest) {
35971
- const path = join15(root, MANIFEST_REL);
36133
+ const path = join16(root, MANIFEST_REL);
35972
36134
  await mkdir4(dirname4(path), { recursive: true });
35973
36135
  await writeFile4(path, JSON.stringify(manifest, null, 2), "utf8");
35974
36136
  }
@@ -35977,24 +36139,24 @@ async function writeArtifactFiles(root, kind, slug, files) {
35977
36139
  if (kind === "subagent") {
35978
36140
  const agent = byPath.get("agent.md");
35979
36141
  if (agent) {
35980
- await mkdir4(join15(root, AGENTS_REL2), { recursive: true });
35981
- 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);
35982
36144
  }
35983
36145
  const short = byPath.get("short-memory.md");
35984
36146
  if (short) {
35985
- await mkdir4(join15(root, AGENTS_REL2, "short-memory"), { recursive: true });
35986
- 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);
35987
36149
  }
35988
36150
  const long = byPath.get("long-memory.md");
35989
36151
  if (long) {
35990
- await mkdir4(join15(root, AGENTS_REL2, "long-memory"), { recursive: true });
35991
- 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);
35992
36154
  }
35993
36155
  } else {
35994
- const base = join15(root, SKILLS_REL2, slug);
36156
+ const base = join16(root, SKILLS_REL2, slug);
35995
36157
  for (const [rel, buf] of byPath) {
35996
36158
  if (rel.includes("..") || rel.startsWith("/")) continue;
35997
- const dest = join15(base, rel);
36159
+ const dest = join16(base, rel);
35998
36160
  await mkdir4(dirname4(dest), { recursive: true });
35999
36161
  await writeFile4(dest, buf);
36000
36162
  }
@@ -36060,11 +36222,11 @@ async function removePackage(root, slug) {
36060
36222
  const entry = manifest[n];
36061
36223
  if (!entry) return { ok: false, error: "not installed" };
36062
36224
  if (entry.kind === "subagent") {
36063
- await rm2(join15(root, AGENTS_REL2, `${n}.md`), { force: true });
36064
- await rm2(join15(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
36065
- 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 });
36066
36228
  } else {
36067
- await rm2(join15(root, SKILLS_REL2, n), { recursive: true, force: true });
36229
+ await rm2(join16(root, SKILLS_REL2, n), { recursive: true, force: true });
36068
36230
  }
36069
36231
  delete manifest[n];
36070
36232
  await writeManifest(root, manifest);
@@ -36075,7 +36237,7 @@ async function removePackage(root, slug) {
36075
36237
  }
36076
36238
 
36077
36239
  // src/core/network-probe.ts
36078
- import { existsSync as existsSync9, readFileSync as readFileSync14 } from "fs";
36240
+ import { existsSync as existsSync10, readFileSync as readFileSync14 } from "fs";
36079
36241
  import { connect } from "net";
36080
36242
  var CANARY_TARGETS = [
36081
36243
  { host: "1.1.1.1", port: 443 },
@@ -36084,7 +36246,7 @@ var CANARY_TARGETS = [
36084
36246
  var PROBE_TIMEOUT_MS = 2500;
36085
36247
  function detectContainerized() {
36086
36248
  try {
36087
- if (existsSync9("/.dockerenv")) return true;
36249
+ if (existsSync10("/.dockerenv")) return true;
36088
36250
  const cgroup = readFileSync14("/proc/1/cgroup", "utf8");
36089
36251
  return /docker|containerd|kubepods|libpod/.test(cgroup);
36090
36252
  } catch {
@@ -36152,9 +36314,9 @@ async function probeNetwork() {
36152
36314
  }
36153
36315
 
36154
36316
  // src/core/git-auth.ts
36155
- import { writeFileSync as writeFileSync6 } from "fs";
36317
+ import { writeFileSync as writeFileSync7 } from "fs";
36156
36318
  import { homedir as homedir4 } from "os";
36157
- import { join as join16 } from "path";
36319
+ import { join as join17 } from "path";
36158
36320
  function tokenTarget(method) {
36159
36321
  if (method === "gitlab-group-token") {
36160
36322
  return { host: process.env.FOURPM_GIT_HOST || "gitlab.com", user: "oauth2" };
@@ -36172,7 +36334,7 @@ function applyGitAuth(method) {
36172
36334
  try {
36173
36335
  const line = `https://${target.user}:${token}@${target.host}
36174
36336
  `;
36175
- writeFileSync6(join16(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
36337
+ writeFileSync7(join17(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
36176
36338
  return true;
36177
36339
  } catch {
36178
36340
  return false;
@@ -36181,7 +36343,7 @@ function applyGitAuth(method) {
36181
36343
 
36182
36344
  // src/core/secrets.ts
36183
36345
  import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
36184
- import { join as join17 } from "path";
36346
+ import { join as join18 } from "path";
36185
36347
  var SECURITY_REL = "AI_SECURITY.md";
36186
36348
  var PLACEHOLDER_REL = "AI_PLACEHOLDER.md";
36187
36349
  var SECRETS_REL = "project.secrets.json";
@@ -36194,7 +36356,7 @@ async function readText3(path, fallback = "") {
36194
36356
  }
36195
36357
  async function readSecretsObject(root) {
36196
36358
  try {
36197
- const v = JSON.parse(await readFile6(join17(root, SECRETS_REL), "utf8"));
36359
+ const v = JSON.parse(await readFile6(join18(root, SECRETS_REL), "utf8"));
36198
36360
  return v && typeof v === "object" ? v : {};
36199
36361
  } catch {
36200
36362
  return {};
@@ -36213,8 +36375,8 @@ function placeholderKeys(text) {
36213
36375
  }
36214
36376
  async function readSecrets(root) {
36215
36377
  const [security, placeholder, secrets] = await Promise.all([
36216
- readText3(join17(root, SECURITY_REL)),
36217
- readText3(join17(root, PLACEHOLDER_REL)),
36378
+ readText3(join18(root, SECURITY_REL)),
36379
+ readText3(join18(root, PLACEHOLDER_REL)),
36218
36380
  readSecretsObject(root)
36219
36381
  ]);
36220
36382
  const names = /* @__PURE__ */ new Set([...placeholderKeys(placeholder), ...Object.keys(secrets)]);
@@ -36227,14 +36389,14 @@ async function readSecrets(root) {
36227
36389
  async function writeSecrets(root, req) {
36228
36390
  try {
36229
36391
  if (req.kind === "security") {
36230
- await writeFile5(join17(root, SECURITY_REL), req.content, "utf8");
36392
+ await writeFile5(join18(root, SECURITY_REL), req.content, "utf8");
36231
36393
  } else if (req.kind === "placeholder") {
36232
- await writeFile5(join17(root, PLACEHOLDER_REL), req.content, "utf8");
36394
+ await writeFile5(join18(root, PLACEHOLDER_REL), req.content, "utf8");
36233
36395
  } else {
36234
36396
  const secrets = await readSecretsObject(root);
36235
36397
  if (req.kind === "secret") secrets[req.key] = req.value;
36236
36398
  else delete secrets[req.key];
36237
- 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");
36238
36400
  }
36239
36401
  return { ok: true };
36240
36402
  } catch (err) {
@@ -36244,13 +36406,13 @@ async function writeSecrets(root, req) {
36244
36406
 
36245
36407
  // src/core/agent-tools.ts
36246
36408
  import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
36247
- import { dirname as dirname5, join as join18 } from "path";
36409
+ import { dirname as dirname5, join as join19 } from "path";
36248
36410
  var SHARED_REL = ".claude/settings.json";
36249
36411
  var LOCAL_REL = ".claude/settings.local.json";
36250
36412
  var SECRETS_DENY = "Read(./project.secrets.json)";
36251
36413
  var MODES = ["default", "acceptEdits", "plan", "bypassPermissions"];
36252
36414
  function fileFor(root, scope) {
36253
- return join18(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
36415
+ return join19(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
36254
36416
  }
36255
36417
  async function readSettingsObject(path) {
36256
36418
  try {
@@ -36287,7 +36449,7 @@ async function writeAgentTools(root, scope, permissions) {
36287
36449
 
36288
36450
  // src/core/graph.ts
36289
36451
  import { readFile as readFile8, readdir as readdir5 } from "fs/promises";
36290
- 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";
36291
36453
  var MAX_FILES = 2e3;
36292
36454
  var MAX_NODES = 3e3;
36293
36455
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
@@ -36307,7 +36469,7 @@ async function walk(root, accept) {
36307
36469
  if (e.name.startsWith(".") && e.name !== ".claude") {
36308
36470
  if (e.isDirectory()) continue;
36309
36471
  }
36310
- const full = join19(dir2, e.name);
36472
+ const full = join20(dir2, e.name);
36311
36473
  if (e.isDirectory()) {
36312
36474
  if (!SKIP_DIRS.has(e.name)) stack.push(full);
36313
36475
  } else if (accept(full)) {
@@ -36427,10 +36589,10 @@ async function buildGraph(root, mode) {
36427
36589
 
36428
36590
  // src/core/rag.ts
36429
36591
  import { readFile as readFile9, writeFile as writeFile7, statfs as statfs2, mkdir as mkdir6 } from "fs/promises";
36430
- import { existsSync as existsSync10 } from "fs";
36592
+ import { existsSync as existsSync11 } from "fs";
36431
36593
  import { execFile as execFile4, spawn as spawn8 } from "child_process";
36432
36594
  import { totalmem as totalmem2, cpus as cpus2 } from "os";
36433
- import { join as join20 } from "path";
36595
+ import { join as join21 } from "path";
36434
36596
  import { promisify as promisify4 } from "util";
36435
36597
  var run4 = promisify4(execFile4);
36436
36598
  var RAG_REL = ".claude/rag";
@@ -36451,7 +36613,7 @@ async function detectPython() {
36451
36613
  }
36452
36614
  async function installLog(root) {
36453
36615
  try {
36454
- const text = await readFile9(join20(root, RAG_REL, "install.log"), "utf8");
36616
+ const text = await readFile9(join21(root, RAG_REL, "install.log"), "utf8");
36455
36617
  return text.split("\n").filter((l) => l.length > 0).slice(-200);
36456
36618
  } catch {
36457
36619
  return [];
@@ -36513,14 +36675,14 @@ async function ragStatus(root) {
36513
36675
  };
36514
36676
  let modelName = "";
36515
36677
  try {
36516
- modelName = (await readFile9(join20(root, RAG_REL, ".installed"), "utf8")).trim();
36678
+ modelName = (await readFile9(join21(root, RAG_REL, ".installed"), "utf8")).trim();
36517
36679
  } catch {
36518
36680
  modelName = "";
36519
36681
  }
36520
36682
  const modelPresent = modelName.length > 0;
36521
36683
  return {
36522
36684
  installed: fastembed && sqliteVec && modelPresent,
36523
- installing: existsSync10(join20(root, RAG_REL, ".installing")),
36685
+ installing: existsSync11(join21(root, RAG_REL, ".installing")),
36524
36686
  python: { found: py.found, version: py.version },
36525
36687
  deps: { pip, fastembed, sqliteVec },
36526
36688
  model: { present: modelPresent, name: modelName },
@@ -36531,9 +36693,9 @@ async function ragStatus(root) {
36531
36693
  };
36532
36694
  }
36533
36695
  async function readIndexMeta(root) {
36534
- const indexing = existsSync10(join20(root, RAG_REL, ".indexing"));
36696
+ const indexing = existsSync11(join21(root, RAG_REL, ".indexing"));
36535
36697
  try {
36536
- 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"));
36537
36699
  return {
36538
36700
  present: true,
36539
36701
  indexing,
@@ -36600,14 +36762,14 @@ if __name__ == "__main__":
36600
36762
  elif cmd == "query": query(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 8)
36601
36763
  `;
36602
36764
  async function ensureEngine(root) {
36603
- await mkdir6(join20(root, RAG_REL), { recursive: true }).catch(() => void 0);
36604
- 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");
36605
36767
  }
36606
36768
  async function ragReindex(root) {
36607
36769
  const py = await detectPython();
36608
36770
  if (!py.found) return { ok: false, started: false, error: "python3 not found" };
36609
- if (!existsSync10(join20(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
36610
- 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 };
36611
36773
  await ensureEngine(root);
36612
36774
  const script = `
36613
36775
  set +e
@@ -36627,12 +36789,12 @@ rm -f "${RAG_REL}/.indexing"
36627
36789
  async function ragQuery(root, queryText, k = 8) {
36628
36790
  const py = await detectPython();
36629
36791
  if (!py.found) return { results: [], error: "python3 not found" };
36630
- 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" };
36631
36793
  await ensureEngine(root);
36632
36794
  try {
36633
36795
  const { stdout } = await run4(
36634
36796
  py.bin,
36635
- [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))],
36636
36798
  { cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
36637
36799
  );
36638
36800
  const parsed = JSON.parse(stdout.trim() || '{"results":[]}');
@@ -36647,9 +36809,9 @@ async function ragInstall(root, model) {
36647
36809
  }
36648
36810
  const py = await detectPython();
36649
36811
  if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
36650
- 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 };
36651
36813
  try {
36652
- await mkdir6(join20(root, RAG_REL), { recursive: true });
36814
+ await mkdir6(join21(root, RAG_REL), { recursive: true });
36653
36815
  } catch {
36654
36816
  }
36655
36817
  const script = `
@@ -36737,12 +36899,12 @@ ${stderr}`;
36737
36899
  // src/core/git-ssh-key.ts
36738
36900
  import { execFile as execFile7 } from "child_process";
36739
36901
  import { readFile as readFile11, rm as rm3, mkdir as mkdir7 } from "fs/promises";
36740
- import { join as join21 } from "path";
36902
+ import { join as join22 } from "path";
36741
36903
  import { promisify as promisify7 } from "util";
36742
36904
  var run7 = promisify7(execFile7);
36743
36905
  function keyPaths(profileDir2) {
36744
- const dir2 = join21(profileDir2, ".ssh");
36745
- const key = join21(dir2, "id_4pm");
36906
+ const dir2 = join22(profileDir2, ".ssh");
36907
+ const key = join22(dir2, "id_4pm");
36746
36908
  return { dir: dir2, key, pub: `${key}.pub` };
36747
36909
  }
36748
36910
  async function readPublicKey(pub) {
@@ -36934,7 +37096,7 @@ async function setCommitAuthor(cwd2, machineUsername) {
36934
37096
  // src/core/scaffold.ts
36935
37097
  import { cp, mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
36936
37098
  import { execFile as execFile10 } from "child_process";
36937
- 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";
36938
37100
  import { fileURLToPath as fileURLToPath3 } from "url";
36939
37101
  import { promisify as promisify10 } from "util";
36940
37102
  var run10 = promisify10(execFile10);
@@ -36963,7 +37125,7 @@ async function provisionRepos(target, repos, emit, opts = {}) {
36963
37125
  }
36964
37126
  continue;
36965
37127
  }
36966
- const dir2 = join22(target, subDirName(repo));
37128
+ const dir2 = join23(target, subDirName(repo));
36967
37129
  if (repo.url) {
36968
37130
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
36969
37131
  await run10("git", ["clone", repo.url, dir2], { timeout: 12e4 });
@@ -36983,14 +37145,14 @@ async function scaffoldProject(payload, profileDir2, onProgress) {
36983
37145
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
36984
37146
  try {
36985
37147
  const root = process.env.SCAFFOLD_ROOT ? resolve7(process.env.SCAFFOLD_ROOT) : resolve7(profileDir2);
36986
- const target = join22(root, payload.projectName);
37148
+ const target = join23(root, payload.projectName);
36987
37149
  await mkdir8(target, { recursive: true });
36988
37150
  emit("copy", "Copying the sample template\u2026");
36989
37151
  await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
36990
37152
  if (payload.spec) {
36991
37153
  emit("spec", "Writing project.spec.json\u2026");
36992
37154
  await writeFile8(
36993
- join22(target, "project.spec.json"),
37155
+ join23(target, "project.spec.json"),
36994
37156
  JSON.stringify(payload.spec, null, 2),
36995
37157
  "utf8"
36996
37158
  );
@@ -37008,7 +37170,7 @@ async function aiInit(target, spec, emit) {
37008
37170
  emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
37009
37171
  const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
37010
37172
  if (subagents.length > 0) {
37011
- await mkdir8(join22(target, ".claude", "agents"), { recursive: true });
37173
+ await mkdir8(join23(target, ".claude", "agents"), { recursive: true });
37012
37174
  for (const sa of subagents) {
37013
37175
  const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
37014
37176
  if (!name) continue;
@@ -37018,17 +37180,17 @@ name: ${name}
37018
37180
 
37019
37181
  ${(sa.description || "").trim()}
37020
37182
  `;
37021
- await writeFile8(join22(target, ".claude", "agents", `${name}.md`), body, "utf8");
37183
+ await writeFile8(join23(target, ".claude", "agents", `${name}.md`), body, "utf8");
37022
37184
  }
37023
37185
  }
37024
37186
  const specJson = JSON.stringify(spec);
37025
37187
  await generateFile(
37026
- join22(target, "README.md"),
37188
+ join23(target, "README.md"),
37027
37189
  `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
37028
37190
  ${specJson}`
37029
37191
  );
37030
37192
  await generateFile(
37031
- join22(target, "CLAUDE.md"),
37193
+ join23(target, "CLAUDE.md"),
37032
37194
  `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
37033
37195
  ${specJson}`
37034
37196
  );
@@ -37044,7 +37206,7 @@ async function addProject(payload, profileDir2, onProgress) {
37044
37206
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
37045
37207
  try {
37046
37208
  const root = process.env.SCAFFOLD_ROOT ? resolve7(process.env.SCAFFOLD_ROOT) : resolve7(profileDir2);
37047
- const target = join22(root, payload.projectName);
37209
+ const target = join23(root, payload.projectName);
37048
37210
  await mkdir8(target, { recursive: true });
37049
37211
  const repos = payload.repos ?? [];
37050
37212
  if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
@@ -37056,8 +37218,8 @@ async function addProject(payload, profileDir2, onProgress) {
37056
37218
  }
37057
37219
 
37058
37220
  // src/ui/slash-commands.ts
37059
- import { existsSync as existsSync11 } from "fs";
37060
- import { join as join23 } from "path";
37221
+ import { existsSync as existsSync12 } from "fs";
37222
+ import { join as join24 } from "path";
37061
37223
  var CONFIG_KEYS = [
37062
37224
  "aiCli",
37063
37225
  "claudeHome",
@@ -37069,7 +37231,7 @@ var CONFIG_KEYS = [
37069
37231
  "aiEnv"
37070
37232
  ];
37071
37233
  function configPath(profileDir2) {
37072
- return join23(profileDir2, "config.json");
37234
+ return join24(profileDir2, "config.json");
37073
37235
  }
37074
37236
  function parseConfigAssignment(key, valueTokens) {
37075
37237
  const value = valueTokens.join(" ").trim();
@@ -37119,7 +37281,7 @@ function runConfig(ctx) {
37119
37281
  ctx.print(`\u2714 ${verb} default config: ${path}`);
37120
37282
  ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
37121
37283
  };
37122
- if (existsSync11(path)) {
37284
+ if (existsSync12(path)) {
37123
37285
  ctx.confirm(
37124
37286
  `config exists: ${path} \u2014 replace it with defaults? (y/N)`,
37125
37287
  () => writeDefaults("replaced with")
@@ -37204,7 +37366,7 @@ function runOutput(ctx) {
37204
37366
  }
37205
37367
  function runLogs(ctx) {
37206
37368
  const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
37207
- const dir2 = join23(ctx.info.profileDir, "logs");
37369
+ const dir2 = join24(ctx.info.profileDir, "logs");
37208
37370
  const lines = readRecentLogLines(dir2, n);
37209
37371
  if (lines.length === 0) {
37210
37372
  ctx.print("No log lines yet.");
@@ -37517,8 +37679,8 @@ async function runMemoryCompaction(ai, cwd2, input) {
37517
37679
  }
37518
37680
 
37519
37681
  // src/core/config-sync.ts
37520
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
37521
- import { join as join24 } from "path";
37682
+ import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
37683
+ import { join as join25 } from "path";
37522
37684
  var SERVER_MANAGED_KEYS = [
37523
37685
  "physicPath",
37524
37686
  "sessionSwitchPct",
@@ -37535,8 +37697,8 @@ var SERVER_MANAGED_KEYS = [
37535
37697
  "projectAiMemoryBudgetChars"
37536
37698
  ];
37537
37699
  function readConfigText(profileDir2) {
37538
- const path = join24(profileDir2, "config.json");
37539
- if (existsSync12(path)) {
37700
+ const path = join25(profileDir2, "config.json");
37701
+ if (existsSync13(path)) {
37540
37702
  try {
37541
37703
  return readFileSync15(path, "utf8");
37542
37704
  } catch {
@@ -37606,8 +37768,9 @@ var WsClient = class {
37606
37768
  context.credential.serverUrl,
37607
37769
  context.profileDir,
37608
37770
  context.bus,
37609
- // After the idle daily tick updates flagged tools, report the fresh snapshot (ADR-0254).
37610
- () => 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")
37611
37774
  );
37612
37775
  context.bus.onLocalSubmit((input) => void this.runLocalCommand(input));
37613
37776
  context.bus.onReconnect(() => this.reconnectNow());
@@ -37727,13 +37890,21 @@ var WsClient = class {
37727
37890
  /**
37728
37891
  * Detect the worker's tools and report the snapshot to the server (ADR-0254, `tools.report`) so the
37729
37892
  * DB-backed Tools panel + restore target stay current. One-way, best-effort — a detect/send failure
37730
- * 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.
37731
37897
  */
37732
- async reportWorkerTools() {
37898
+ async reportWorkerTools(trigger = "op", restoreFailed) {
37733
37899
  try {
37734
37900
  const autoUpdate = readProfileConfig(this.context.profileDir).autoUpdateTools ?? [];
37735
37901
  const { catalog, extras } = await detectWorkerTools(autoUpdate);
37736
- 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
+ });
37737
37908
  } catch (err) {
37738
37909
  logger.warn("tools.report.error", { error: String(err) });
37739
37910
  }
@@ -37879,6 +38050,7 @@ var WsClient = class {
37879
38050
  pruneCommandHistoryByAge(token.cliRetentionDays);
37880
38051
  pruneCommandOutputByAge(token.cliRetentionDays);
37881
38052
  }
38053
+ if (this.physicRoot) sweepOldAttachments(this.physicRoot);
37882
38054
  writeProfileConfig(this.context.profileDir, {
37883
38055
  sessionSwitchPct: token.projectTokens?.sessionSwitchPct ?? 0,
37884
38056
  perPromptTokenLimit: token.projectTokens?.perPromptTokenLimit ?? 0,
@@ -38047,8 +38219,8 @@ var WsClient = class {
38047
38219
  const oldRoot = this.physicFolderPath(sync.oldName);
38048
38220
  if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
38049
38221
  }
38050
- if (sync.oldName) rmSync4(join25(this.context.profileDir, sync.oldName), { recursive: true, force: true });
38051
- 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 });
38052
38224
  this.physicRoot = this.physicFolderPath(sync.newName);
38053
38225
  this.bus.setProject(sync.newName);
38054
38226
  this.bus.log(`Physic project folder synced \u2192 ${sync.newName}`);
@@ -38059,7 +38231,7 @@ var WsClient = class {
38059
38231
  if (del.name) {
38060
38232
  const delRoot = this.physicFolderPath(del.name);
38061
38233
  if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
38062
- rmSync4(join25(this.context.profileDir, del.name), { recursive: true, force: true });
38234
+ rmSync5(join26(this.context.profileDir, del.name), { recursive: true, force: true });
38063
38235
  this.bus.log(`Physic project folder deleted (project removed) \u2192 ${del.name}`);
38064
38236
  }
38065
38237
  void manageSshKey("delete", this.context.profileDir).catch(() => void 0);
@@ -38067,6 +38239,23 @@ var WsClient = class {
38067
38239
  this.physicRoot = null;
38068
38240
  break;
38069
38241
  }
38242
+ case WsChannels.PROJECT_TOKENS: {
38243
+ const tokens = payload;
38244
+ writeProfileConfig(this.context.profileDir, {
38245
+ sessionSwitchPct: tokens.sessionSwitchPct ?? 0,
38246
+ perPromptTokenLimit: tokens.perPromptTokenLimit ?? 0,
38247
+ projectAiRunTimeoutSec: tokens.aiRunTimeoutSec ?? 0,
38248
+ projectAutoClearIdleMinutes: tokens.autoClearIdleMinutes ?? 0,
38249
+ projectAiMemoryMode: tokens.memory?.mode ?? "inherit",
38250
+ projectAiMemoryBudgetChars: tokens.memory?.budgetChars ?? 0
38251
+ });
38252
+ this.restrictToFolder = tokens.restrictToFolder === true;
38253
+ logger.info("project.tokens.applied", {
38254
+ aiRunTimeoutSec: tokens.aiRunTimeoutSec ?? 0,
38255
+ autoClearIdleMinutes: tokens.autoClearIdleMinutes ?? 0
38256
+ });
38257
+ break;
38258
+ }
38070
38259
  case WsChannels.COMMAND_DISPATCH: {
38071
38260
  const dispatch = payload;
38072
38261
  if (this.handledCommands.has(dispatch.commandId)) {
@@ -38083,7 +38272,13 @@ var WsClient = class {
38083
38272
  if (dispatch.cmd.trimStart().startsWith("/")) {
38084
38273
  void this.runWebSlashCommand(dispatch.cmd, dispatch.commandId);
38085
38274
  } else {
38086
- 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
+ );
38087
38282
  }
38088
38283
  break;
38089
38284
  }
@@ -38385,11 +38580,15 @@ var WsClient = class {
38385
38580
  const req = payload;
38386
38581
  const op = message.channel === WsChannels.TOOLS_INSTALL ? "install" : message.channel === WsChannels.TOOLS_UPDATE ? "update" : "uninstall";
38387
38582
  this.send(message.channel, { started: true }, message.id);
38583
+ const opTimeoutMs = resolveInstallTimeoutMs(
38584
+ readProfileConfig(this.context.profileDir).toolInstallTimeoutSec
38585
+ );
38388
38586
  void runWorkerToolOp(
38389
38587
  op,
38390
38588
  req.name,
38391
38589
  req.manager,
38392
- (line) => this.send(WsChannels.TOOLS_PROGRESS, { opId: req.opId, line })
38590
+ (line) => this.send(WsChannels.TOOLS_PROGRESS, { opId: req.opId, line }),
38591
+ opTimeoutMs
38393
38592
  ).then((res) => {
38394
38593
  this.send(WsChannels.TOOLS_DONE, {
38395
38594
  opId: req.opId,
@@ -38403,16 +38602,45 @@ var WsClient = class {
38403
38602
  }
38404
38603
  case WsChannels.TOOLS_RESTORE: {
38405
38604
  const req = payload;
38406
- void reconcileTools(
38407
- req.manifest.map((m) => ({ name: m.name, version: m.version, manager: m.manager })),
38408
- (line) => this.bus.log(line)
38409
- ).then(() => this.reportWorkerTools()).then(() => this.send(WsChannels.TOOLS_RESTORE, { ok: true }, message.id)).catch(
38410
- (err) => this.send(
38411
- WsChannels.TOOLS_RESTORE,
38412
- { ok: false, error: String(err) },
38413
- message.id
38414
- )
38605
+ const restoreTrigger = req.trigger === "manual" ? "manual" : "boot";
38606
+ const installTimeoutMs = resolveInstallTimeoutMs(
38607
+ readProfileConfig(this.context.profileDir).toolInstallTimeoutSec
38415
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
+ }
38416
38644
  break;
38417
38645
  }
38418
38646
  case WsChannels.GIT_DIFF:
@@ -38460,7 +38688,7 @@ var WsClient = class {
38460
38688
  }
38461
38689
  case WsChannels.LOG_READ: {
38462
38690
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
38463
- const lines = readRecentLogLines(join25(this.context.profileDir, "logs"), limit);
38691
+ const lines = readRecentLogLines(join26(this.context.profileDir, "logs"), limit);
38464
38692
  this.send(WsChannels.LOG_READ, { lines }, message.id);
38465
38693
  break;
38466
38694
  }
@@ -38539,7 +38767,7 @@ var WsClient = class {
38539
38767
  * exactly like typing the prompt in the cli (tries profiles, meters real tokens) instead
38540
38768
  * of spawning the text as a raw command. Streams output to the transcript AND the server.
38541
38769
  */
38542
- async runAiPrompt(prompt, commandId, origin, oneShot = false) {
38770
+ async runAiPrompt(prompt, commandId, origin, oneShot = false, images) {
38543
38771
  const config = readProfileConfig(this.context.profileDir);
38544
38772
  const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
38545
38773
  const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
@@ -38597,7 +38825,18 @@ var WsClient = class {
38597
38825
  text: `session ${this.usageSnapshot?.session.utilizationPct ?? 0}% \u2265 ${config.sessionSwitchPct}% \u2014 switching to Claude profile "${labelFromCredentialKey(startCred)}"`
38598
38826
  });
38599
38827
  }
38600
- 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;
38601
38840
  const hint = unified ? { credential: startCred ?? workingCred } : { dir: startDir ?? workingDir };
38602
38841
  const memCfg = resolveMemoryConfig(config);
38603
38842
  const firstAttempt = planAiRun(guardedPrompt, config, hint).attempts[0];
@@ -39181,6 +39420,13 @@ ${guardedPrompt}`;
39181
39420
  checkQuota(metric, amount = 1) {
39182
39421
  return this.request(WsChannels.QUOTA_CHECK, { metric, amount });
39183
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
+ }
39184
39430
  /** usage.report batched to the server (ADR-0020); tags each event with the AI profile. */
39185
39431
  reportUsage(events, profile) {
39186
39432
  const tagged = profile ? events.map((e) => ({ ...e, profile })) : events;
@@ -39218,8 +39464,8 @@ ${guardedPrompt}`;
39218
39464
  /** Create the physic folder at an absolute path if missing (best-effort). */
39219
39465
  ensurePhysicFolderPath(folder) {
39220
39466
  try {
39221
- if (!existsSync13(folder)) {
39222
- mkdirSync5(folder, { recursive: true });
39467
+ if (!existsSync14(folder)) {
39468
+ mkdirSync6(folder, { recursive: true });
39223
39469
  this.bus.log(`Created physic project folder: ${folder}`);
39224
39470
  }
39225
39471
  } catch (err) {
@@ -39233,7 +39479,7 @@ ${guardedPrompt}`;
39233
39479
  */
39234
39480
  physicFolderPath(projectName) {
39235
39481
  const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
39236
- return safe ? join25(this.context.profileDir, safe) : null;
39482
+ return safe ? join26(this.context.profileDir, safe) : null;
39237
39483
  }
39238
39484
  /**
39239
39485
  * Report machine.status: version + the state of the physic project being served
@@ -39242,14 +39488,19 @@ ${guardedPrompt}`;
39242
39488
  sendMachineStatus() {
39243
39489
  const config = readProfileConfig(this.context.profileDir);
39244
39490
  const physicPath = config.physicPath ?? null;
39491
+ const aiProfileCount = Array.isArray(config.aiProfiles) ? config.aiProfiles.length : (config.claudeHome?.length ?? 0) + (config.codexHome?.length ?? 0) + (config.antigravityHome?.length ?? 0);
39245
39492
  this.send(WsChannels.MACHINE_STATUS, {
39246
39493
  status: "online",
39247
39494
  projects: [],
39248
39495
  version: CLI_VERSION,
39249
39496
  physicPath,
39250
- physicPathExists: physicPath ? existsSync13(physicPath) : void 0,
39497
+ physicPathExists: physicPath ? existsSync14(physicPath) : void 0,
39251
39498
  // Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
39252
- autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false
39499
+ autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false,
39500
+ // The machine-user AI-run limit (0 = unlimited) + profile count so a dispatch can resolve the
39501
+ // effective timeout and derive the SSE reply windows server-side (ADR-0256).
39502
+ aiRunTimeoutSec: config.aiRunTimeoutSec ?? 0,
39503
+ aiProfileCount
39253
39504
  });
39254
39505
  }
39255
39506
  /** Heartbeat every 30s: ping + periodically re-report machine.status. */
@@ -39337,7 +39588,7 @@ ${guardedPrompt}`;
39337
39588
  */
39338
39589
  uploadLog() {
39339
39590
  try {
39340
- const upload = readLogUpload(join25(this.context.profileDir, "logs"));
39591
+ const upload = readLogUpload(join26(this.context.profileDir, "logs"));
39341
39592
  if (!upload) return;
39342
39593
  const sig = `${upload.fileName}:${upload.totalBytes}`;
39343
39594
  if (sig === this.lastLogSig) return;
@@ -39365,10 +39616,10 @@ ${guardedPrompt}`;
39365
39616
  };
39366
39617
 
39367
39618
  // src/core/instance-lock.ts
39368
- import { readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
39369
- 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";
39370
39621
  function lockPath(profileDir2) {
39371
- return join26(profileDir2, "start.lock");
39622
+ return join27(profileDir2, "start.lock");
39372
39623
  }
39373
39624
  function isAlive(pid) {
39374
39625
  try {
@@ -39388,13 +39639,13 @@ function lockHolder(profileDir2) {
39388
39639
  }
39389
39640
  function acquireInstanceLock(profileDir2) {
39390
39641
  if (lockHolder(profileDir2) !== null) return false;
39391
- writeFileSync7(lockPath(profileDir2), String(process.pid), "utf8");
39642
+ writeFileSync8(lockPath(profileDir2), String(process.pid), "utf8");
39392
39643
  return true;
39393
39644
  }
39394
39645
  function releaseInstanceLock(profileDir2) {
39395
39646
  try {
39396
39647
  const pid = Number.parseInt(readFileSync16(lockPath(profileDir2), "utf8").trim(), 10);
39397
- if (pid === process.pid) rmSync5(lockPath(profileDir2), { force: true });
39648
+ if (pid === process.pid) rmSync6(lockPath(profileDir2), { force: true });
39398
39649
  } catch {
39399
39650
  }
39400
39651
  }
@@ -39667,8 +39918,8 @@ var SessionBus = class {
39667
39918
 
39668
39919
  // src/core/control-server.ts
39669
39920
  import { createServer } from "net";
39670
- import { rmSync as rmSync6 } from "fs";
39671
- import { join as join27 } from "path";
39921
+ import { rmSync as rmSync7 } from "fs";
39922
+ import { join as join28 } from "path";
39672
39923
 
39673
39924
  // src/core/control-protocol.ts
39674
39925
  var CONTROL_SOCKET_FILE = "control.sock";
@@ -39697,9 +39948,9 @@ function createFrameParser() {
39697
39948
 
39698
39949
  // src/core/control-server.ts
39699
39950
  function startControlServer(bus, profileDir2, info) {
39700
- const socketPath = join27(profileDir2, CONTROL_SOCKET_FILE);
39951
+ const socketPath = join28(profileDir2, CONTROL_SOCKET_FILE);
39701
39952
  try {
39702
- rmSync6(socketPath, { force: true });
39953
+ rmSync7(socketPath, { force: true });
39703
39954
  } catch {
39704
39955
  }
39705
39956
  const clients = /* @__PURE__ */ new Set();
@@ -39765,7 +40016,7 @@ function startControlServer(bus, profileDir2, info) {
39765
40016
  clients.clear();
39766
40017
  server.close();
39767
40018
  try {
39768
- rmSync6(socketPath, { force: true });
40019
+ rmSync7(socketPath, { force: true });
39769
40020
  } catch {
39770
40021
  }
39771
40022
  };
@@ -45428,15 +45679,15 @@ var import_react21 = __toESM(require_react(), 1);
45428
45679
  var import_react26 = __toESM(require_react(), 1);
45429
45680
 
45430
45681
  // src/core/input-history.ts
45431
- import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
45432
- 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";
45433
45684
  var MAX_HISTORY = 200;
45434
45685
  function historyPath(profileDir2) {
45435
- return join28(profileDir2, "input-history.json");
45686
+ return join29(profileDir2, "input-history.json");
45436
45687
  }
45437
45688
  function loadInputHistory(profileDir2) {
45438
45689
  const path = historyPath(profileDir2);
45439
- if (!existsSync15(path)) return [];
45690
+ if (!existsSync16(path)) return [];
45440
45691
  try {
45441
45692
  const data = JSON.parse(readFileSync18(path, "utf8"));
45442
45693
  return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
@@ -45449,7 +45700,7 @@ function appendInputHistory(profileDir2, line, current) {
45449
45700
  if (!trimmed || current[current.length - 1] === trimmed) return current;
45450
45701
  const next = [...current, trimmed].slice(-MAX_HISTORY);
45451
45702
  try {
45452
- writeFileSync8(historyPath(profileDir2), JSON.stringify(next), "utf8");
45703
+ writeFileSync9(historyPath(profileDir2), JSON.stringify(next), "utf8");
45453
45704
  } catch {
45454
45705
  }
45455
45706
  return next;
@@ -46053,7 +46304,7 @@ async function runStart(profileDir2, profileName) {
46053
46304
  return;
46054
46305
  }
46055
46306
  logger.configure({
46056
- dir: join29(profileDir2, "logs"),
46307
+ dir: join30(profileDir2, "logs"),
46057
46308
  level: resolveLogLevel(),
46058
46309
  base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
46059
46310
  });
@@ -46163,7 +46414,7 @@ async function runStart(profileDir2, profileName) {
46163
46414
  }
46164
46415
 
46165
46416
  // src/commands/attach.ts
46166
- import { join as join30 } from "path";
46417
+ import { join as join31 } from "path";
46167
46418
 
46168
46419
  // src/core/control-client.ts
46169
46420
  import { connect as connect2 } from "net";
@@ -46262,7 +46513,7 @@ async function runAttach(profileDir2, profileName) {
46262
46513
  process.exitCode = 1;
46263
46514
  return;
46264
46515
  }
46265
- const socketPath = join30(profileDir2, CONTROL_SOCKET_FILE);
46516
+ const socketPath = join31(profileDir2, CONTROL_SOCKET_FILE);
46266
46517
  const bus = new SessionBus();
46267
46518
  let conn;
46268
46519
  try {
@@ -46295,7 +46546,7 @@ async function runAttach(profileDir2, profileName) {
46295
46546
 
46296
46547
  // src/commands/ai-login.ts
46297
46548
  import { spawn as spawn11 } from "child_process";
46298
- import { mkdirSync as mkdirSync6 } from "fs";
46549
+ import { mkdirSync as mkdirSync7 } from "fs";
46299
46550
  import { basename as basename5 } from "path";
46300
46551
  function runProviderLogin(cmd, envVar, dir2) {
46301
46552
  return new Promise((resolve8) => {
@@ -46333,7 +46584,7 @@ async function runAiLogin(profileDir2, profileName, aiFilter) {
46333
46584
  continue;
46334
46585
  }
46335
46586
  const dir2 = resolveHomePath(cred.profile);
46336
- mkdirSync6(dir2, { recursive: true });
46587
+ mkdirSync7(dir2, { recursive: true });
46337
46588
  console.log(`
46338
46589
  \u25B6 Logging in ${cred.provider} \xB7 ${cred.profile} (${envVar}=${dir2})`);
46339
46590
  console.log(` Complete the provider's own sign-in (claude: type \`/login\`; codex: follow its prompt), then exit.`);
@@ -46347,7 +46598,7 @@ Tip: for the credentials to survive \`docker rm\`/recreate, keep each profile pa
46347
46598
  }
46348
46599
 
46349
46600
  // src/commands/unlink.ts
46350
- import { rmSync as rmSync7 } from "fs";
46601
+ import { rmSync as rmSync8 } from "fs";
46351
46602
  async function runUnlink(profileDir2, profileName) {
46352
46603
  const cred = readCredential(profileDir2, profileName);
46353
46604
  if (!cred) {
@@ -46362,7 +46613,7 @@ async function runUnlink(profileDir2, profileName) {
46362
46613
  `\u26A0 Could not reach the server to revoke \u2014 revoke it at web /machines. (${err.message})`
46363
46614
  );
46364
46615
  }
46365
- rmSync7(profileDir2, { recursive: true, force: true });
46616
+ rmSync8(profileDir2, { recursive: true, force: true });
46366
46617
  clearDefaultProfileIf(profileName);
46367
46618
  console.log("\u2714 Deleted the profile and its physic project folder.");
46368
46619
  }