@cortexkit/aft-pi 0.20.1 → 0.22.0

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.
package/dist/index.js CHANGED
@@ -18761,7 +18761,7 @@ var require_fetch = __commonJS((exports, module) => {
18761
18761
  request.cache = "no-store";
18762
18762
  }
18763
18763
  const newConnection = forceNewConnection ? "yes" : "no";
18764
- if (request.mode === "websocket") {} else {}
18764
+ if (request.mode === "websocket") {}
18765
18765
  let requestBody = null;
18766
18766
  if (request.body == null && fetchParams.processRequestEndOfBody) {
18767
18767
  queueMicrotask(() => fetchParams.processRequestEndOfBody());
@@ -30476,17 +30476,24 @@ var require_src2 = __commonJS((exports, module) => {
30476
30476
  // src/index.ts
30477
30477
  import { createRequire as createRequire3 } from "node:module";
30478
30478
  import { homedir as homedir9 } from "node:os";
30479
- import { join as join12 } from "node:path";
30479
+ import { join as join13 } from "node:path";
30480
30480
 
30481
30481
  // ../aft-bridge/dist/active-logger.js
30482
- var active;
30482
+ var ACTIVE_LOGGER_SYMBOL = Symbol.for("aft-bridge-active-logger");
30483
+ function loggerGlobal() {
30484
+ return globalThis;
30485
+ }
30483
30486
  function setActiveLogger(logger) {
30484
- active = logger;
30487
+ loggerGlobal()[ACTIVE_LOGGER_SYMBOL] = logger;
30488
+ }
30489
+ function getActiveLogger() {
30490
+ return loggerGlobal()[ACTIVE_LOGGER_SYMBOL];
30485
30491
  }
30486
30492
  function getLogFilePath() {
30487
- return active?.getLogFilePath?.();
30493
+ return getActiveLogger()?.getLogFilePath?.();
30488
30494
  }
30489
30495
  function log(message, meta) {
30496
+ const active = getActiveLogger();
30490
30497
  if (active) {
30491
30498
  active.log(message, meta);
30492
30499
  } else {
@@ -30494,6 +30501,7 @@ function log(message, meta) {
30494
30501
  }
30495
30502
  }
30496
30503
  function warn(message, meta) {
30504
+ const active = getActiveLogger();
30497
30505
  if (active) {
30498
30506
  active.warn(message, meta);
30499
30507
  } else {
@@ -30501,6 +30509,7 @@ function warn(message, meta) {
30501
30509
  }
30502
30510
  }
30503
30511
  function error(message, meta) {
30512
+ const active = getActiveLogger();
30504
30513
  if (active) {
30505
30514
  active.error(message, meta);
30506
30515
  } else {
@@ -30520,6 +30529,7 @@ function sessionError(sessionId, message) {
30520
30529
  import { spawn } from "node:child_process";
30521
30530
  import { homedir } from "node:os";
30522
30531
  import { join } from "node:path";
30532
+ import { StringDecoder } from "node:string_decoder";
30523
30533
  var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
30524
30534
  var SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS = 5000;
30525
30535
  var MAX_STDOUT_BUFFER = 64 * 1024 * 1024;
@@ -30616,6 +30626,7 @@ class BinaryBridge {
30616
30626
  configureWarningClients = new Map;
30617
30627
  restartResetTimer = null;
30618
30628
  errorPrefix;
30629
+ logger;
30619
30630
  constructor(binaryPath, cwd, options, configOverrides) {
30620
30631
  this.binaryPath = binaryPath;
30621
30632
  this.cwd = cwd;
@@ -30628,6 +30639,28 @@ class BinaryBridge {
30628
30639
  this.onBashCompletion = options?.onBashCompletion;
30629
30640
  this.onBashLongRunning = options?.onBashLongRunning;
30630
30641
  this.errorPrefix = options?.errorPrefix ?? "[aft-bridge]";
30642
+ this.logger = options?.logger;
30643
+ }
30644
+ logVia(message, meta) {
30645
+ const logger = this.logger ?? getActiveLogger();
30646
+ if (logger)
30647
+ logger.log(message, meta);
30648
+ else
30649
+ log(message, meta);
30650
+ }
30651
+ warnVia(message, meta) {
30652
+ const logger = this.logger ?? getActiveLogger();
30653
+ if (logger)
30654
+ logger.warn(message, meta);
30655
+ else
30656
+ warn(message, meta);
30657
+ }
30658
+ errorVia(message, meta) {
30659
+ const logger = this.logger ?? getActiveLogger();
30660
+ if (logger)
30661
+ logger.error(message, meta);
30662
+ else
30663
+ error(message, meta);
30631
30664
  }
30632
30665
  get restartCount() {
30633
30666
  return this._restartCount;
@@ -30736,8 +30769,8 @@ class BinaryBridge {
30736
30769
  return;
30737
30770
  if (configResult.warnings.length === 0)
30738
30771
  return;
30772
+ const sessionId = typeof params.session_id === "string" ? params.session_id : undefined;
30739
30773
  try {
30740
- const sessionId = typeof params.session_id === "string" ? params.session_id : undefined;
30741
30774
  await this.onConfigureWarnings({
30742
30775
  projectRoot: this.cwd,
30743
30776
  sessionId,
@@ -30746,6 +30779,10 @@ class BinaryBridge {
30746
30779
  });
30747
30780
  } catch (err) {
30748
30781
  warn(`configure warning delivery failed: ${err instanceof Error ? err.message : String(err)}`);
30782
+ } finally {
30783
+ if (sessionId) {
30784
+ this.configureWarningClients.delete(sessionId);
30785
+ }
30749
30786
  }
30750
30787
  }
30751
30788
  async handleConfigureWarningsFrame(frame) {
@@ -30757,16 +30794,23 @@ class BinaryBridge {
30757
30794
  const projectRoot = typeof frame.project_root === "string" ? frame.project_root : this.cwd;
30758
30795
  const rawSessionId = frame.session_id;
30759
30796
  const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : null;
30760
- await this.onConfigureWarnings({
30761
- projectRoot,
30762
- sessionId,
30763
- client: sessionId ? this.configureWarningClients.get(sessionId) : undefined,
30764
- warnings
30765
- });
30797
+ try {
30798
+ await this.onConfigureWarnings({
30799
+ projectRoot,
30800
+ sessionId,
30801
+ client: sessionId ? this.configureWarningClients.get(sessionId) : undefined,
30802
+ warnings
30803
+ });
30804
+ } finally {
30805
+ if (sessionId) {
30806
+ this.configureWarningClients.delete(sessionId);
30807
+ }
30808
+ }
30766
30809
  }
30767
30810
  async shutdown() {
30768
30811
  this._shuttingDown = true;
30769
30812
  this.clearRestartResetTimer();
30813
+ this.configureWarningClients.clear();
30770
30814
  this.rejectAllPending(new Error(`${this.errorPrefix} Bridge shutting down`));
30771
30815
  if (this.process) {
30772
30816
  const proc = this.process;
@@ -30790,10 +30834,12 @@ class BinaryBridge {
30790
30834
  return;
30791
30835
  try {
30792
30836
  const resp = await this.send("version");
30837
+ if (resp.success === false) {
30838
+ throw new Error(`Binary version check failed: ${String(resp.code ?? "unknown")} — likely too old`);
30839
+ }
30793
30840
  const binaryVersion = resp.version;
30794
- if (!binaryVersion) {
30795
- log("Binary did not report a version — skipping version check");
30796
- return;
30841
+ if (typeof binaryVersion !== "string") {
30842
+ throw new Error(`Binary did not report a version — likely too old (minVersion: ${this.minVersion})`);
30797
30843
  }
30798
30844
  log(`Binary version: ${binaryVersion}`);
30799
30845
  if (compareSemver(binaryVersion, this.minVersion) < 0) {
@@ -30802,6 +30848,7 @@ class BinaryBridge {
30802
30848
  }
30803
30849
  } catch (err) {
30804
30850
  warn(`Version check failed: ${err.message}`);
30851
+ throw err;
30805
30852
  }
30806
30853
  }
30807
30854
  ensureSpawned(triggeringSessionId) {
@@ -30843,19 +30890,23 @@ class BinaryBridge {
30843
30890
  env
30844
30891
  });
30845
30892
  const currentChild = child;
30893
+ const stdoutDecoder = new StringDecoder("utf8");
30846
30894
  child.stdout?.on("data", (chunk) => {
30847
- this.onStdoutData(chunk.toString("utf-8"));
30895
+ this.onStdoutData(stdoutDecoder.write(chunk));
30848
30896
  });
30897
+ child.stdout?.on("end", () => {
30898
+ const remaining = stdoutDecoder.end();
30899
+ if (remaining)
30900
+ this.onStdoutData(remaining);
30901
+ });
30902
+ const stderrDecoder = new StringDecoder("utf8");
30849
30903
  child.stderr?.on("data", (chunk) => {
30850
- const lines = chunk.toString("utf-8").trimEnd().split(`
30851
- `);
30852
- for (const line of lines) {
30853
- if (!line)
30854
- continue;
30855
- const tagged = tagStderrLine(line);
30856
- log(tagged);
30857
- this.pushStderrLine(tagged);
30858
- }
30904
+ this.onStderrData(stderrDecoder.write(chunk));
30905
+ });
30906
+ child.stderr?.on("end", () => {
30907
+ const remaining = stderrDecoder.end();
30908
+ if (remaining)
30909
+ this.onStderrData(remaining);
30859
30910
  });
30860
30911
  child.on("error", (err) => {
30861
30912
  if (this.process !== currentChild)
@@ -30888,6 +30939,17 @@ class BinaryBridge {
30888
30939
  this.stderrTail.shift();
30889
30940
  }
30890
30941
  }
30942
+ onStderrData(data) {
30943
+ const lines = data.trimEnd().split(`
30944
+ `);
30945
+ for (const line of lines) {
30946
+ if (!line)
30947
+ continue;
30948
+ const tagged = tagStderrLine(line);
30949
+ log(tagged);
30950
+ this.pushStderrLine(tagged);
30951
+ }
30952
+ }
30891
30953
  formatStderrTail() {
30892
30954
  if (this.stderrTail.length === 0)
30893
30955
  return "";
@@ -30967,6 +31029,7 @@ class BinaryBridge {
30967
31029
  }
30968
31030
  }
30969
31031
  handleTimeout(triggeringSessionId) {
31032
+ this.rejectAllPending(new Error(`${this.errorPrefix} bridge killed during sibling timeout — request aborted`));
30970
31033
  if (this.process) {
30971
31034
  this.process.kill("SIGKILL");
30972
31035
  this.process = null;
@@ -31292,23 +31355,23 @@ async function ensureOnnxRuntime(storageDir) {
31292
31355
  const onnxBaseDir = join3(storageDir, "onnxruntime");
31293
31356
  mkdirSync2(onnxBaseDir, { recursive: true });
31294
31357
  const lockPath = join3(onnxBaseDir, ONNX_LOCK_FILE);
31295
- cleanupAbandonedOnnxAttempts(onnxBaseDir, ortDir);
31358
+ cleanupAbandonedStagingDirs(onnxBaseDir);
31296
31359
  if (!acquireLock(lockPath)) {
31297
31360
  warn(`ONNX Runtime install already in progress in another process (lock: ${lockPath}). Skipping.`);
31298
31361
  return null;
31299
31362
  }
31300
31363
  try {
31364
+ cleanupIncompleteTargetIfUnowned(ortDir);
31301
31365
  return await downloadOnnxRuntime(info, ortDir);
31302
31366
  } finally {
31303
31367
  releaseLock(lockPath);
31304
31368
  }
31305
31369
  }
31306
- function cleanupAbandonedOnnxAttempts(onnxBaseDir, ortDir) {
31370
+ function cleanupAbandonedStagingDirs(onnxBaseDir) {
31307
31371
  try {
31308
31372
  const entries = readdirSync(onnxBaseDir);
31309
- const ortDirBaseName = ortDir.slice(onnxBaseDir.length + 1);
31310
31373
  for (const entry of entries) {
31311
- if (!entry.startsWith(`${ortDirBaseName}.tmp.`))
31374
+ if (!entry.startsWith(`${ORT_VERSION}.tmp.`))
31312
31375
  continue;
31313
31376
  const stagingDir = join3(onnxBaseDir, entry);
31314
31377
  const parts = entry.split(".");
@@ -31339,6 +31402,8 @@ function cleanupAbandonedOnnxAttempts(onnxBaseDir, ortDir) {
31339
31402
  }
31340
31403
  }
31341
31404
  } catch {}
31405
+ }
31406
+ function cleanupIncompleteTargetIfUnowned(ortDir) {
31342
31407
  try {
31343
31408
  if (existsSync2(ortDir) && !existsSync2(join3(ortDir, ONNX_INSTALLED_META_FILE))) {
31344
31409
  log(`[onnx] removing half-populated install dir ${ortDir} (no meta file)`);
@@ -31529,25 +31594,7 @@ async function downloadOnnxRuntime(info, targetDir) {
31529
31594
  realFiles.push(libFile);
31530
31595
  }
31531
31596
  }
31532
- for (const libFile of realFiles) {
31533
- const src = join3(extractedDir, libFile);
31534
- const dst = join3(targetDir, libFile);
31535
- try {
31536
- copyFileSync(src, dst);
31537
- if (process.platform !== "win32") {
31538
- chmodSync2(dst, 493);
31539
- }
31540
- } catch (copyErr) {
31541
- log(`ORT extract: failed to copy ${libFile}: ${copyErr}`);
31542
- }
31543
- }
31544
- for (const link of symlinks) {
31545
- const dst = join3(targetDir, link.name);
31546
- try {
31547
- unlinkSync2(dst);
31548
- } catch {}
31549
- symlinkSync(link.target, dst);
31550
- }
31597
+ copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks);
31551
31598
  const libPath = join3(targetDir, info.libName);
31552
31599
  let libHash = null;
31553
31600
  try {
@@ -31570,6 +31617,45 @@ async function downloadOnnxRuntime(info, targetDir) {
31570
31617
  return null;
31571
31618
  }
31572
31619
  }
31620
+ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, copyFile = copyFileSync) {
31621
+ const requiredLibs = new Set([info.libName]);
31622
+ for (const libFile of realFiles) {
31623
+ const src = join3(extractedDir, libFile);
31624
+ const dst = join3(targetDir, libFile);
31625
+ try {
31626
+ copyFile(src, dst);
31627
+ if (process.platform !== "win32") {
31628
+ chmodSync2(dst, 493);
31629
+ }
31630
+ } catch (copyErr) {
31631
+ if (requiredLibs.has(libFile)) {
31632
+ rmSync(targetDir, { recursive: true, force: true });
31633
+ throw copyErr;
31634
+ }
31635
+ log(`ORT extract: failed to copy optional ${libFile}: ${copyErr}`);
31636
+ }
31637
+ }
31638
+ for (const link of symlinks) {
31639
+ const dst = join3(targetDir, link.name);
31640
+ try {
31641
+ unlinkSync2(dst);
31642
+ } catch {}
31643
+ try {
31644
+ symlinkSync(link.target, dst);
31645
+ } catch (symlinkErr) {
31646
+ if (requiredLibs.has(link.name)) {
31647
+ rmSync(targetDir, { recursive: true, force: true });
31648
+ throw symlinkErr;
31649
+ }
31650
+ log(`ORT extract: failed to symlink optional ${link.name}: ${symlinkErr}`);
31651
+ }
31652
+ }
31653
+ const requiredPath = join3(targetDir, info.libName);
31654
+ if (!existsSync2(requiredPath)) {
31655
+ rmSync(targetDir, { recursive: true, force: true });
31656
+ throw new Error(`Required ONNX Runtime library missing after install: ${requiredPath}`);
31657
+ }
31658
+ }
31573
31659
  async function extractZipArchive(archivePath, destinationDir) {
31574
31660
  if (process.platform === "win32") {
31575
31661
  execFileSync("tar.exe", ["-xf", archivePath, "-C", destinationDir], {
@@ -31753,11 +31839,13 @@ class BridgePool {
31753
31839
  idleTimeoutMs;
31754
31840
  bridgeOptions;
31755
31841
  configOverrides;
31842
+ logger;
31756
31843
  cleanupTimer = null;
31757
31844
  constructor(binaryPath, options = {}, configOverrides = {}) {
31758
31845
  this.binaryPath = binaryPath;
31759
31846
  this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
31760
31847
  this.idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
31848
+ this.logger = options.logger;
31761
31849
  this.bridgeOptions = {
31762
31850
  timeoutMs: options.timeoutMs,
31763
31851
  maxRestarts: options.maxRestarts,
@@ -31801,8 +31889,10 @@ class BridgePool {
31801
31889
  cleanup() {
31802
31890
  const now = Date.now();
31803
31891
  for (const [dir, entry] of this.bridges) {
31892
+ if (entry.bridge.hasPendingRequests())
31893
+ continue;
31804
31894
  if (now - entry.lastUsed > this.idleTimeoutMs) {
31805
- entry.bridge.shutdown().catch((err) => error("cleanup shutdown failed:", err));
31895
+ entry.bridge.shutdown().catch((err) => this.error("cleanup shutdown failed:", err));
31806
31896
  this.bridges.delete(dir);
31807
31897
  }
31808
31898
  }
@@ -31811,6 +31901,8 @@ class BridgePool {
31811
31901
  let oldestDir = null;
31812
31902
  let oldestTime = Infinity;
31813
31903
  for (const [dir, entry] of this.bridges) {
31904
+ if (entry.bridge.hasPendingRequests())
31905
+ continue;
31814
31906
  if (entry.lastUsed < oldestTime) {
31815
31907
  oldestTime = entry.lastUsed;
31816
31908
  oldestDir = dir;
@@ -31818,7 +31910,7 @@ class BridgePool {
31818
31910
  }
31819
31911
  if (oldestDir) {
31820
31912
  const entry = this.bridges.get(oldestDir);
31821
- entry?.bridge.shutdown().catch((err) => error("eviction shutdown failed:", err));
31913
+ entry?.bridge.shutdown().catch((err) => this.error("eviction shutdown failed:", err));
31822
31914
  this.bridges.delete(oldestDir);
31823
31915
  }
31824
31916
  }
@@ -31836,7 +31928,21 @@ class BridgePool {
31836
31928
  const shutdowns = Array.from(this.bridges.values()).map((entry) => entry.bridge.shutdown());
31837
31929
  this.bridges.clear();
31838
31930
  await Promise.allSettled(shutdowns);
31839
- log(`Binary path updated to ${newPath}. All bridges cleared — next calls will use the new binary.`);
31931
+ this.log(`Binary path updated to ${newPath}. All bridges cleared — next calls will use the new binary.`);
31932
+ }
31933
+ log(message, meta) {
31934
+ const logger = this.logger ?? getActiveLogger();
31935
+ if (logger)
31936
+ logger.log(message, meta);
31937
+ else
31938
+ log(message, meta);
31939
+ }
31940
+ error(message, meta) {
31941
+ const logger = this.logger ?? getActiveLogger();
31942
+ if (logger)
31943
+ logger.error(message, meta);
31944
+ else
31945
+ error(message, meta);
31840
31946
  }
31841
31947
  setConfigureOverride(key, value) {
31842
31948
  if (value === undefined) {
@@ -31956,7 +32062,7 @@ async function findBinary(expectedVersion) {
31956
32062
  return syncResult;
31957
32063
  }
31958
32064
  log("Binary not found locally, attempting auto-download...");
31959
- const downloaded = await ensureBinary();
32065
+ const downloaded = await ensureBinary(expectedVersion);
31960
32066
  if (downloaded)
31961
32067
  return downloaded;
31962
32068
  throw new Error([
@@ -32616,6 +32722,13 @@ async function appendToolResultBgCompletions(drainContext, content) {
32616
32722
  const reminder = formatCombinedSystemReminder(state.pendingCompletions, state.pendingLongRunning);
32617
32723
  state.pendingCompletions = [];
32618
32724
  state.pendingLongRunning = [];
32725
+ if (state.debounceTimer) {
32726
+ clearTimeout(state.debounceTimer);
32727
+ state.debounceTimer = null;
32728
+ state.firstCompletionAt = null;
32729
+ state.scheduledFireAt = null;
32730
+ state.scheduledCompletionCount = 0;
32731
+ }
32619
32732
  return [...content, { type: "text", text: reminder }];
32620
32733
  }
32621
32734
  async function handleTurnEndBgCompletions(drainContext) {
@@ -32623,8 +32736,6 @@ async function handleTurnEndBgCompletions(drainContext) {
32623
32736
  }
32624
32737
  async function triggerWakeIfPending(drainContext, skipDrain) {
32625
32738
  const state = stateFor(drainContext.sessionID);
32626
- if (state.wakeFiredThisIdle)
32627
- return;
32628
32739
  if (drainContext.isActive?.())
32629
32740
  return;
32630
32741
  if (!skipDrain && state.outstandingTaskIds.size > 0) {
@@ -32638,9 +32749,6 @@ async function triggerWakeIfPending(drainContext, skipDrain) {
32638
32749
  sessionWarn2(drainContext.sessionID ?? "", `${LOG_PREFIX} wake send failed: ${err instanceof Error ? err.message : String(err)}`);
32639
32750
  });
32640
32751
  }
32641
- function resetBgWake(sessionID) {
32642
- stateFor(sessionID).wakeFiredThisIdle = false;
32643
- }
32644
32752
  function formatSystemReminder(completions) {
32645
32753
  const bullets = completions.map((completion) => formatCompletion(completion)).join(`
32646
32754
  `);
@@ -32705,16 +32813,17 @@ function scheduleWake(state, sendWake, onSendFailure) {
32705
32813
  state.debounceTimer = setTimeout(() => {
32706
32814
  const pending = state.pendingCompletions;
32707
32815
  const pendingLongRunning = state.pendingLongRunning;
32708
- const reminder = formatCombinedSystemReminder(pending, pendingLongRunning);
32709
- state.pendingCompletions = [];
32710
- state.pendingLongRunning = [];
32711
32816
  state.debounceTimer = null;
32712
32817
  state.firstCompletionAt = null;
32713
32818
  state.scheduledFireAt = null;
32714
32819
  state.scheduledCompletionCount = 0;
32820
+ if (pending.length === 0 && pendingLongRunning.length === 0)
32821
+ return;
32822
+ const reminder = formatCombinedSystemReminder(pending, pendingLongRunning);
32823
+ state.pendingCompletions = [];
32824
+ state.pendingLongRunning = [];
32715
32825
  sendWake(reminder).then(() => {
32716
32826
  state.retryDelayMs = null;
32717
- state.wakeFiredThisIdle = true;
32718
32827
  }).catch((err) => {
32719
32828
  state.pendingCompletions = [...pending, ...state.pendingCompletions];
32720
32829
  state.pendingLongRunning = [...pendingLongRunning, ...state.pendingLongRunning];
@@ -32736,7 +32845,6 @@ function stateFor(sessionID) {
32736
32845
  pendingCompletions: [],
32737
32846
  pendingLongRunning: [],
32738
32847
  debounceTimer: null,
32739
- wakeFiredThisIdle: false,
32740
32848
  firstCompletionAt: null,
32741
32849
  scheduledFireAt: null,
32742
32850
  scheduledCompletionCount: 0,
@@ -43989,7 +44097,7 @@ function finalize(ctx, schema) {
43989
44097
  result.$schema = "http://json-schema.org/draft-07/schema#";
43990
44098
  } else if (ctx.target === "draft-04") {
43991
44099
  result.$schema = "http://json-schema.org/draft-04/schema#";
43992
- } else if (ctx.target === "openapi-3.0") {} else {}
44100
+ } else if (ctx.target === "openapi-3.0") {}
43993
44101
  if (ctx.external?.uri) {
43994
44102
  const id = ctx.external.registry.get(schema)?.id;
43995
44103
  if (!id)
@@ -44237,7 +44345,7 @@ var literalProcessor = (schema, ctx, json, _params) => {
44237
44345
  if (val === undefined) {
44238
44346
  if (ctx.unrepresentable === "throw") {
44239
44347
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
44240
- } else {}
44348
+ }
44241
44349
  } else if (typeof val === "bigint") {
44242
44350
  if (ctx.unrepresentable === "throw") {
44243
44351
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -47037,7 +47145,8 @@ function loadAftConfig(projectDirectory) {
47037
47145
  // src/lsp-auto-install.ts
47038
47146
  import { spawn as spawn2 } from "node:child_process";
47039
47147
  import { createHash as createHash3 } from "node:crypto";
47040
- import { createReadStream, statSync as statSync3 } from "node:fs";
47148
+ import { createReadStream, mkdirSync as mkdirSync6, readFileSync as readFileSync5, renameSync as renameSync3, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
47149
+ import { join as join10 } from "node:path";
47041
47150
 
47042
47151
  // src/lsp-cache.ts
47043
47152
  import {
@@ -47721,6 +47830,50 @@ function hashInstalledBinary(spec) {
47721
47830
  stream.on("end", () => resolve2(hash2.digest("hex")));
47722
47831
  });
47723
47832
  }
47833
+ function installedBinaryPath(spec) {
47834
+ const candidates = process.platform === "win32" ? [
47835
+ lspBinaryPath(spec.npm, spec.binary),
47836
+ lspBinaryPath(spec.npm, `${spec.binary}.cmd`),
47837
+ lspBinaryPath(spec.npm, `${spec.binary}.exe`),
47838
+ lspBinaryPath(spec.npm, `${spec.binary}.bat`)
47839
+ ] : [lspBinaryPath(spec.npm, spec.binary)];
47840
+ for (const candidate of candidates) {
47841
+ try {
47842
+ if (statSync3(candidate).isFile())
47843
+ return candidate;
47844
+ } catch {}
47845
+ }
47846
+ return null;
47847
+ }
47848
+ function sha256OfFileSync(path2) {
47849
+ return createHash3("sha256").update(readFileSync5(path2)).digest("hex");
47850
+ }
47851
+ function quarantineCachedNpmInstall(spec, reason) {
47852
+ const packageDir = lspPackageDir(spec.npm);
47853
+ const dest = join10(packageDir, "..", ".quarantine", encodeURIComponent(spec.npm), `${Date.now()}`);
47854
+ warn2(`[lsp] tofu_mismatch ${spec.npm}: ${reason}; quarantining ${packageDir} -> ${dest}`);
47855
+ try {
47856
+ mkdirSync6(join10(dest, ".."), { recursive: true });
47857
+ rmSync2(dest, { recursive: true, force: true });
47858
+ renameSync3(packageDir, dest);
47859
+ } catch (err) {
47860
+ warn2(`[lsp] tofu_mismatch ${spec.npm}: failed to quarantine cache entry: ${err}`);
47861
+ }
47862
+ }
47863
+ function validateCachedNpmInstall(spec) {
47864
+ const meta3 = readInstalledMeta(spec.npm);
47865
+ const binaryPath = installedBinaryPath(spec);
47866
+ if (!meta3?.sha256 || !meta3.version || !isSafeVersion(meta3.version) || !binaryPath) {
47867
+ quarantineCachedNpmInstall(spec, "missing/unsafe metadata or binary");
47868
+ return false;
47869
+ }
47870
+ const currentHash = sha256OfFileSync(binaryPath);
47871
+ if (currentHash !== meta3.sha256) {
47872
+ quarantineCachedNpmInstall(spec, `recorded ${meta3.sha256}, current ${currentHash}`);
47873
+ return false;
47874
+ }
47875
+ return true;
47876
+ }
47724
47877
  function runAutoInstall(projectRoot, config2, fetchImpl2 = fetch) {
47725
47878
  const cachedBinDirs = [];
47726
47879
  const skipped = [];
@@ -47733,7 +47886,7 @@ function runAutoInstall(projectRoot, config2, fetchImpl2 = fetch) {
47733
47886
  return projectExtensions;
47734
47887
  };
47735
47888
  for (const spec of NPM_LSP_TABLE) {
47736
- if (isInstalled(spec.npm, spec.binary)) {
47889
+ if (isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)) {
47737
47890
  cachedBinDirs.push(lspBinDir(spec.npm));
47738
47891
  }
47739
47892
  if (config2.disabled.has(spec.id)) {
@@ -47785,16 +47938,17 @@ import {
47785
47938
  createWriteStream as createWriteStream2,
47786
47939
  existsSync as existsSync7,
47787
47940
  lstatSync as lstatSync2,
47788
- mkdirSync as mkdirSync6,
47941
+ mkdirSync as mkdirSync7,
47789
47942
  readdirSync as readdirSync4,
47943
+ readFileSync as readFileSync6,
47790
47944
  readlinkSync as readlinkSync2,
47791
47945
  realpathSync as realpathSync3,
47792
- renameSync as renameSync3,
47793
- rmSync as rmSync2,
47946
+ renameSync as renameSync4,
47947
+ rmSync as rmSync3,
47794
47948
  statSync as statSync4,
47795
47949
  unlinkSync as unlinkSync6
47796
47950
  } from "node:fs";
47797
- import { dirname as dirname2, join as join10, relative as relative2, resolve as resolve2 } from "node:path";
47951
+ import { dirname as dirname2, join as join11, relative as relative2, resolve as resolve2 } from "node:path";
47798
47952
  import { Readable as Readable2 } from "node:stream";
47799
47953
  import { pipeline as pipeline2 } from "node:stream/promises";
47800
47954
 
@@ -47884,25 +48038,25 @@ function detectHostPlatform() {
47884
48038
 
47885
48039
  // src/lsp-github-install.ts
47886
48040
  function ghCacheRoot() {
47887
- return join10(aftCacheBase(), "lsp-binaries");
48041
+ return join11(aftCacheBase(), "lsp-binaries");
47888
48042
  }
47889
48043
  function ghPackageDir(spec) {
47890
- return join10(ghCacheRoot(), spec.id);
48044
+ return join11(ghCacheRoot(), spec.id);
47891
48045
  }
47892
48046
  function ghBinDir(spec) {
47893
- return join10(ghPackageDir(spec), "bin");
48047
+ return join11(ghPackageDir(spec), "bin");
47894
48048
  }
47895
48049
  function ghExtractDir(spec) {
47896
- return join10(ghPackageDir(spec), "extracted");
48050
+ return join11(ghPackageDir(spec), "extracted");
47897
48051
  }
47898
48052
  function ghBinaryPath(spec, platform) {
47899
48053
  const ext = platform === "win32" ? ".exe" : "";
47900
- return join10(ghBinDir(spec), `${spec.binary}${ext}`);
48054
+ return join11(ghBinDir(spec), `${spec.binary}${ext}`);
47901
48055
  }
47902
48056
  function isGithubInstalled(spec, platform) {
47903
48057
  for (const candidate of ghBinaryCandidates(spec, platform)) {
47904
48058
  try {
47905
- if (statSync4(join10(ghBinDir(spec), candidate)).isFile())
48059
+ if (statSync4(join11(ghBinDir(spec), candidate)).isFile())
47906
48060
  return true;
47907
48061
  } catch {}
47908
48062
  }
@@ -47924,6 +48078,9 @@ function sha256OfFile(path2) {
47924
48078
  stream.on("end", () => resolve3(hash2.digest("hex")));
47925
48079
  });
47926
48080
  }
48081
+ function sha256OfFileSync2(path2) {
48082
+ return createHash4("sha256").update(readFileSync6(path2)).digest("hex");
48083
+ }
47927
48084
  async function fetchReleaseByTag(githubRepo, tag, fetchImpl2, signal) {
47928
48085
  const candidates = [];
47929
48086
  candidates.push(tag);
@@ -47944,11 +48101,7 @@ async function fetchReleaseByTag(githubRepo, tag, fetchImpl2, signal) {
47944
48101
  const url2 = `https://api.github.com/repos/${githubRepo}/releases/tags/${encodeURIComponent(candidate)}`;
47945
48102
  const timeout = controlledTimeoutSignal(15000, signal);
47946
48103
  try {
47947
- const res = await fetchImpl2(url2, {
47948
- headers,
47949
- redirect: "follow",
47950
- signal: timeout.signal
47951
- });
48104
+ const res = await fetchJsonFollowingRedirects(url2, headers, fetchImpl2, timeout.signal);
47952
48105
  if (res.status === 404)
47953
48106
  continue;
47954
48107
  if (!res.ok) {
@@ -47978,6 +48131,23 @@ async function fetchReleaseByTag(githubRepo, tag, fetchImpl2, signal) {
47978
48131
  }
47979
48132
  return null;
47980
48133
  }
48134
+ async function fetchJsonFollowingRedirects(url2, headers, fetchImpl2, signal) {
48135
+ const maxRedirects = 5;
48136
+ let currentUrl = url2;
48137
+ for (let i = 0;i <= maxRedirects; i += 1) {
48138
+ assertAllowedDownloadUrl(currentUrl);
48139
+ const res = await fetchImpl2(currentUrl, { headers, redirect: "manual", signal });
48140
+ if (res.status >= 300 && res.status < 400) {
48141
+ const location = res.headers.get("location");
48142
+ if (!location)
48143
+ throw new Error(`redirect status ${res.status} without Location`);
48144
+ currentUrl = new URL(location, currentUrl).toString();
48145
+ continue;
48146
+ }
48147
+ return res;
48148
+ }
48149
+ throw new Error(`too many redirects (>${maxRedirects})`);
48150
+ }
47981
48151
  async function resolveTargetTag(spec, config2, fetchImpl2, signal) {
47982
48152
  const pinned = config2.versions[spec.githubRepo];
47983
48153
  if (pinned) {
@@ -48072,17 +48242,12 @@ function assertAllowedDownloadUrl(rawUrl) {
48072
48242
  return parsed;
48073
48243
  }
48074
48244
  async function downloadFile(url2, destPath, fetchImpl2, assetSize, signal) {
48075
- assertAllowedDownloadUrl(url2);
48076
48245
  if (assetSize !== undefined && assetSize > MAX_DOWNLOAD_BYTES2) {
48077
48246
  throw new Error(`asset size ${assetSize} exceeds max ${MAX_DOWNLOAD_BYTES2} (set lsp.versions to pin a smaller release if this is wrong)`);
48078
48247
  }
48079
48248
  const timeout = controlledTimeoutSignal(120000, signal);
48080
48249
  try {
48081
- const res = await fetchImpl2(url2, {
48082
- headers: { accept: "application/octet-stream" },
48083
- redirect: "follow",
48084
- signal: timeout.signal
48085
- });
48250
+ const res = await fetchFollowingRedirects(url2, fetchImpl2, timeout.signal);
48086
48251
  if (!res.ok || !res.body) {
48087
48252
  throw new Error(`download failed (${res.status})`);
48088
48253
  }
@@ -48090,7 +48255,7 @@ async function downloadFile(url2, destPath, fetchImpl2, assetSize, signal) {
48090
48255
  if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES2) {
48091
48256
  throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES2}`);
48092
48257
  }
48093
- mkdirSync6(dirname2(destPath), { recursive: true });
48258
+ mkdirSync7(dirname2(destPath), { recursive: true });
48094
48259
  let bytesWritten = 0;
48095
48260
  const guard = new TransformStream({
48096
48261
  transform(chunk, controller) {
@@ -48114,6 +48279,27 @@ async function downloadFile(url2, destPath, fetchImpl2, assetSize, signal) {
48114
48279
  timeout.cleanup();
48115
48280
  }
48116
48281
  }
48282
+ async function fetchFollowingRedirects(url2, fetchImpl2, signal) {
48283
+ const maxRedirects = 5;
48284
+ let currentUrl = url2;
48285
+ for (let i = 0;i <= maxRedirects; i += 1) {
48286
+ assertAllowedDownloadUrl(currentUrl);
48287
+ const res = await fetchImpl2(currentUrl, {
48288
+ headers: { accept: "application/octet-stream" },
48289
+ redirect: "manual",
48290
+ signal
48291
+ });
48292
+ if (res.status >= 300 && res.status < 400) {
48293
+ const location = res.headers.get("location");
48294
+ if (!location)
48295
+ throw new Error(`redirect status ${res.status} without Location`);
48296
+ currentUrl = new URL(location, currentUrl).toString();
48297
+ continue;
48298
+ }
48299
+ return res;
48300
+ }
48301
+ throw new Error(`too many redirects (>${maxRedirects})`);
48302
+ }
48117
48303
  function validateExtraction(stagingRoot) {
48118
48304
  const realStagingRoot = realpathSync3(stagingRoot);
48119
48305
  let totalBytes = 0;
@@ -48125,7 +48311,7 @@ function validateExtraction(stagingRoot) {
48125
48311
  throw new Error(`failed to read staging dir ${dir}: ${err}`);
48126
48312
  }
48127
48313
  for (const entry of entries) {
48128
- const full = join10(dir, entry);
48314
+ const full = join11(dir, entry);
48129
48315
  let lst;
48130
48316
  try {
48131
48317
  lst = lstatSync2(full);
@@ -48163,27 +48349,83 @@ function validateExtraction(stagingRoot) {
48163
48349
  };
48164
48350
  walk(realStagingRoot);
48165
48351
  }
48352
+ function precheckArchiveSize(archivePath, archiveType) {
48353
+ let totalBytes = 0;
48354
+ if (archiveType === "zip") {
48355
+ const out = execFileSync2("unzip", ["-l", archivePath], { encoding: "utf8" });
48356
+ const match = out.match(/^\s*(\d+)\s+\d+\s+files?\s*$/m);
48357
+ if (match)
48358
+ totalBytes = Number.parseInt(match[1] ?? "0", 10);
48359
+ } else {
48360
+ const out = execFileSync2("tar", ["-tvf", archivePath], { encoding: "utf8" });
48361
+ for (const line of out.split(`
48362
+ `)) {
48363
+ const parts = line.trim().split(/\s+/);
48364
+ if (parts.length >= 6) {
48365
+ const numeric = parts.map((part) => Number.parseInt(part, 10)).filter((value) => Number.isFinite(value) && value >= 0);
48366
+ if (numeric.length > 0)
48367
+ totalBytes += Math.max(...numeric);
48368
+ }
48369
+ }
48370
+ }
48371
+ if (totalBytes > MAX_EXTRACT_BYTES2) {
48372
+ throw new Error(`archive uncompressed size ${totalBytes} exceeds ${MAX_EXTRACT_BYTES2}`);
48373
+ }
48374
+ }
48166
48375
  function extractArchiveSafely(archivePath, destDir, archiveType) {
48167
48376
  const suffix = randomBytes(8).toString("hex");
48168
48377
  const stagingDir = `${destDir}.staging-${suffix}`;
48169
48378
  try {
48170
- rmSync2(stagingDir, { recursive: true, force: true });
48379
+ rmSync3(stagingDir, { recursive: true, force: true });
48171
48380
  } catch {}
48172
- mkdirSync6(stagingDir, { recursive: true });
48381
+ mkdirSync7(stagingDir, { recursive: true });
48173
48382
  try {
48383
+ precheckArchiveSize(archivePath, archiveType);
48174
48384
  runPlatformExtractor(archivePath, stagingDir, archiveType);
48175
48385
  validateExtraction(stagingDir);
48176
48386
  try {
48177
- rmSync2(destDir, { recursive: true, force: true });
48387
+ rmSync3(destDir, { recursive: true, force: true });
48178
48388
  } catch {}
48179
- renameSync3(stagingDir, destDir);
48389
+ renameSync4(stagingDir, destDir);
48180
48390
  } catch (err) {
48181
48391
  try {
48182
- rmSync2(stagingDir, { recursive: true, force: true });
48392
+ rmSync3(stagingDir, { recursive: true, force: true });
48183
48393
  } catch {}
48184
48394
  throw err;
48185
48395
  }
48186
48396
  }
48397
+ function quarantineCachedGithubInstall(spec, reason) {
48398
+ const packageDir = ghPackageDir(spec);
48399
+ const dest = join11(ghCacheRoot(), ".quarantine", encodeURIComponent(spec.id), `${Date.now()}`);
48400
+ warn2(`[lsp] tofu_mismatch ${spec.id}: ${reason}; quarantining ${packageDir} -> ${dest}`);
48401
+ try {
48402
+ mkdirSync7(dirname2(dest), { recursive: true });
48403
+ rmSync3(dest, { recursive: true, force: true });
48404
+ renameSync4(packageDir, dest);
48405
+ } catch (err) {
48406
+ warn2(`[lsp] tofu_mismatch ${spec.id}: failed to quarantine cache entry: ${err}`);
48407
+ }
48408
+ }
48409
+ function validateCachedGithubInstall(spec, platform) {
48410
+ const meta3 = readInstalledMetaIn(ghPackageDir(spec));
48411
+ const binaryPath = ghBinaryCandidates(spec, platform).map((candidate) => join11(ghBinDir(spec), candidate)).find((candidate) => {
48412
+ try {
48413
+ return statSync4(candidate).isFile();
48414
+ } catch {
48415
+ return false;
48416
+ }
48417
+ });
48418
+ if (!meta3?.sha256 || !meta3.version || !isSafeVersion(meta3.version) || !binaryPath) {
48419
+ quarantineCachedGithubInstall(spec, "missing/unsafe metadata or binary");
48420
+ return false;
48421
+ }
48422
+ const currentHash = sha256OfFileSync2(binaryPath);
48423
+ if (currentHash !== meta3.sha256) {
48424
+ quarantineCachedGithubInstall(spec, `recorded ${meta3.sha256}, current ${currentHash}`);
48425
+ return false;
48426
+ }
48427
+ return true;
48428
+ }
48187
48429
  function runPlatformExtractor(archivePath, destDir, archiveType) {
48188
48430
  if (archiveType === "zip") {
48189
48431
  if (process.platform === "win32") {
@@ -48229,7 +48471,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl2,
48229
48471
  }
48230
48472
  const pkgDir = ghPackageDir(spec);
48231
48473
  const extractDir = ghExtractDir(spec);
48232
- const archivePath = join10(pkgDir, expected.name);
48474
+ const archivePath = join11(pkgDir, expected.name);
48233
48475
  log2(`[lsp] downloading ${spec.id} ${tag} → ${matchingAsset.url}`);
48234
48476
  try {
48235
48477
  await downloadFile(matchingAsset.url, archivePath, fetchImpl2, matchingAsset.size, signal);
@@ -48268,13 +48510,13 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl2,
48268
48510
  unlinkSync6(archivePath);
48269
48511
  } catch {}
48270
48512
  }
48271
- const innerBinaryPath = join10(extractDir, spec.binaryPathInArchive(platform, arch, version2));
48513
+ const innerBinaryPath = join11(extractDir, spec.binaryPathInArchive(platform, arch, version2));
48272
48514
  if (!existsSync7(innerBinaryPath)) {
48273
48515
  error2(`[lsp] ${spec.id}: extracted binary not found at ${innerBinaryPath}`);
48274
48516
  return null;
48275
48517
  }
48276
48518
  const targetBinary = ghBinaryPath(spec, platform);
48277
- mkdirSync6(dirname2(targetBinary), { recursive: true });
48519
+ mkdirSync7(dirname2(targetBinary), { recursive: true });
48278
48520
  try {
48279
48521
  copyFileSync3(innerBinaryPath, targetBinary);
48280
48522
  if (platform !== "win32") {
@@ -48365,7 +48607,7 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl2 = fetch) {
48365
48607
  };
48366
48608
  }
48367
48609
  for (const spec of GITHUB_LSP_TABLE) {
48368
- if (isGithubInstalled(spec, host.platform)) {
48610
+ if (isGithubInstalled(spec, host.platform) && validateCachedGithubInstall(spec, host.platform)) {
48369
48611
  cachedBinDirs.push(ghBinDir(spec));
48370
48612
  }
48371
48613
  if (config2.disabled.has(spec.id)) {
@@ -48449,8 +48691,8 @@ function discoverRelevantGithubServers(projectRoot) {
48449
48691
  }
48450
48692
 
48451
48693
  // src/notifications.ts
48452
- import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
48453
- import { join as join11 } from "node:path";
48694
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync7, renameSync as renameSync5, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
48695
+ import { join as join12 } from "node:path";
48454
48696
  var WARNING_MARKER = "\uD83D\uDD27 AFT: ⚠️";
48455
48697
  var FEATURE_MARKER = "\uD83D\uDD27 AFT: ✨";
48456
48698
  var WARNED_TOOLS_FILE = "warned_tools.json";
@@ -48468,10 +48710,10 @@ function sendIgnoredMessage(client, sessionId, text) {
48468
48710
  }
48469
48711
  function readWarnedTools(storageDir) {
48470
48712
  try {
48471
- const warnedToolsPath = join11(storageDir, WARNED_TOOLS_FILE);
48713
+ const warnedToolsPath = join12(storageDir, WARNED_TOOLS_FILE);
48472
48714
  if (!existsSync8(warnedToolsPath))
48473
48715
  return {};
48474
- const parsed = JSON.parse(readFileSync5(warnedToolsPath, "utf-8"));
48716
+ const parsed = JSON.parse(readFileSync7(warnedToolsPath, "utf-8"));
48475
48717
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
48476
48718
  return {};
48477
48719
  const warned = {};
@@ -48487,12 +48729,34 @@ function readWarnedTools(storageDir) {
48487
48729
  }
48488
48730
  function writeWarnedTools(storageDir, warned) {
48489
48731
  try {
48490
- mkdirSync7(storageDir, { recursive: true });
48491
- const warnedToolsPath = join11(storageDir, WARNED_TOOLS_FILE);
48492
- writeFileSync5(warnedToolsPath, `${JSON.stringify(warned, null, 2)}
48732
+ mkdirSync8(storageDir, { recursive: true });
48733
+ const warnedToolsPath = join12(storageDir, WARNED_TOOLS_FILE);
48734
+ const tmpPath = join12(storageDir, `${WARNED_TOOLS_FILE}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
48735
+ writeFileSync5(tmpPath, `${JSON.stringify(warned, null, 2)}
48493
48736
  `);
48737
+ renameSync5(tmpPath, warnedToolsPath);
48494
48738
  } catch {}
48495
48739
  }
48740
+ async function withWarnedToolsLock(storageDir, fn) {
48741
+ const lockDir = join12(storageDir, "warned_tools.lock");
48742
+ for (let attempt = 0;attempt < 5; attempt++) {
48743
+ try {
48744
+ mkdirSync8(storageDir, { recursive: true });
48745
+ mkdirSync8(lockDir, { mode: 448 });
48746
+ try {
48747
+ return await fn();
48748
+ } finally {
48749
+ rmSync4(lockDir, { recursive: true, force: true });
48750
+ }
48751
+ } catch (err) {
48752
+ const code = err.code;
48753
+ if (code !== "EEXIST")
48754
+ return null;
48755
+ await new Promise((resolve3) => setTimeout(resolve3, 10 * (attempt + 1)));
48756
+ }
48757
+ }
48758
+ return null;
48759
+ }
48496
48760
  function warningKey(warning, projectRoot) {
48497
48761
  const scope = warning.kind === "lsp_binary_missing" ? "_" : projectRoot ?? "_";
48498
48762
  return [
@@ -48531,27 +48795,36 @@ ${warning.hint}`;
48531
48795
  async function deliverConfigureWarnings(opts, warnings) {
48532
48796
  if (warnings.length === 0)
48533
48797
  return;
48534
- const warned = readWarnedTools(opts.storageDir);
48535
- let changed = false;
48536
- for (const warning of warnings) {
48537
- const key = warningKey(warning, opts.projectRoot);
48538
- if (Object.hasOwn(warned, key))
48539
- continue;
48540
- if (!sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning))) {
48541
- continue;
48798
+ const deliveredWithLock = await withWarnedToolsLock(opts.storageDir, async () => {
48799
+ const warned = readWarnedTools(opts.storageDir);
48800
+ let changed = false;
48801
+ for (const warning of warnings) {
48802
+ const key = warningKey(warning, opts.projectRoot);
48803
+ if (Object.hasOwn(warned, key))
48804
+ continue;
48805
+ if (!sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning))) {
48806
+ continue;
48807
+ }
48808
+ warned[key] = opts.pluginVersion;
48809
+ changed = true;
48542
48810
  }
48543
- warned[key] = opts.pluginVersion;
48544
- changed = true;
48545
- }
48546
- if (changed) {
48547
- writeWarnedTools(opts.storageDir, warned);
48811
+ if (changed) {
48812
+ const merged = { ...readWarnedTools(opts.storageDir), ...warned };
48813
+ writeWarnedTools(opts.storageDir, merged);
48814
+ }
48815
+ return true;
48816
+ });
48817
+ if (deliveredWithLock)
48818
+ return;
48819
+ for (const warning of warnings) {
48820
+ sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning));
48548
48821
  }
48549
48822
  }
48550
48823
  function sendFeatureAnnouncement(version2, features, storageDir) {
48551
- const versionFile = join11(storageDir, "last_announced_version");
48824
+ const versionFile = join12(storageDir, "last_announced_version");
48552
48825
  try {
48553
48826
  if (existsSync8(versionFile)) {
48554
- const lastVersion = readFileSync5(versionFile, "utf-8").trim();
48827
+ const lastVersion = readFileSync7(versionFile, "utf-8").trim();
48555
48828
  if (lastVersion === version2)
48556
48829
  return;
48557
48830
  }
@@ -48559,7 +48832,7 @@ function sendFeatureAnnouncement(version2, features, storageDir) {
48559
48832
  log2([`${FEATURE_MARKER} v${version2}:`, ...features.map((feature) => ` • ${feature}`)].join(`
48560
48833
  `));
48561
48834
  try {
48562
- mkdirSync7(storageDir, { recursive: true });
48835
+ mkdirSync8(storageDir, { recursive: true });
48563
48836
  writeFileSync5(versionFile, version2);
48564
48837
  } catch {}
48565
48838
  }
@@ -50002,7 +50275,6 @@ var ImportParams = Type6.Object({
50002
50275
  defaultImport: Type6.Optional(Type6.String({ description: "Default import name (e.g. 'React')" })),
50003
50276
  removeName: Type6.Optional(Type6.String({ description: "Named import to remove; omit to remove entire import" })),
50004
50277
  typeOnly: Type6.Optional(Type6.Boolean({ description: "Type-only import (TS only)" })),
50005
- dryRun: Type6.Optional(Type6.Boolean({ description: "Preview without writing" })),
50006
50278
  validate: Type6.Optional(StringEnum2(["syntax", "full"], {
50007
50279
  description: "Post-edit validation level (default: syntax)"
50008
50280
  }))
@@ -50011,12 +50283,6 @@ function buildImportSections(args, payload, theme) {
50011
50283
  const response = asRecord2(payload);
50012
50284
  if (!response)
50013
50285
  return [theme.fg("muted", "No import result.")];
50014
- if (response.dry_run === true) {
50015
- return [
50016
- theme.fg("warning", `[dry run] ${args.op}`),
50017
- asString(response.diff) || theme.fg("muted", "No diff available.")
50018
- ];
50019
- }
50020
50286
  if (args.op === "organize") {
50021
50287
  const groups = asRecords(response.groups);
50022
50288
  const groupText = groups.length > 0 ? groups.map((group) => `${asString(group.name) ?? "unknown"}: ${asNumber(group.count) ?? 0}`).join(" · ") : "No imports found";
@@ -50059,7 +50325,7 @@ function registerImportTools(pi, ctx) {
50059
50325
  pi.registerTool({
50060
50326
  name: "aft_import",
50061
50327
  label: "import",
50062
- description: "Language-aware import management. Supports TS, JS, TSX, Python, Rust, Go. Ops: `add` (auto-groups stdlib/external/internal, deduplicates), `remove` (pass `removeName` for single name or omit to remove entire import), `organize` (re-sort + deduplicate).",
50328
+ description: "Language-aware import management. Supports TS, JS, TSX, Python, Rust, Go. Ops: `add`, `remove`, `organize`. Use aft_safety checkpoint/undo before broad cleanup.",
50063
50329
  parameters: ImportParams,
50064
50330
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
50065
50331
  if ((params.op === "add" || params.op === "remove") && !params.module) {
@@ -50082,8 +50348,6 @@ function registerImportTools(pi, ctx) {
50082
50348
  req.name = params.removeName;
50083
50349
  if (params.typeOnly !== undefined)
50084
50350
  req.type_only = params.typeOnly;
50085
- if (params.dryRun !== undefined)
50086
- req.dry_run = params.dryRun;
50087
50351
  if (params.validate !== undefined)
50088
50352
  req.validate = params.validate;
50089
50353
  const response = await callBridge(bridge, commandMap[params.op], req, extCtx);
@@ -50564,7 +50828,7 @@ Pass a single \`target\`:
50564
50828
  if (response.success === false) {
50565
50829
  throw new Error(response.message || "zoom failed");
50566
50830
  }
50567
- return textResult(formatZoomText(targetLabel, response));
50831
+ return textResult(formatZoomText(targetLabel, response), response);
50568
50832
  },
50569
50833
  renderCall(args, theme, context) {
50570
50834
  return renderZoomCall(args, theme, context);
@@ -50627,28 +50891,12 @@ var RefactorParams = Type10.Object({
50627
50891
  name: Type10.Optional(Type10.String({ description: "New function name (for extract)" })),
50628
50892
  startLine: Type10.Optional(Type10.Number({ description: "1-based start line (for extract)" })),
50629
50893
  endLine: Type10.Optional(Type10.Number({ description: "1-based end line, inclusive (for extract)" })),
50630
- callSiteLine: Type10.Optional(Type10.Number({ description: "1-based call site line (for inline)" })),
50631
- dryRun: Type10.Optional(Type10.Boolean({ description: "Preview as diff" }))
50894
+ callSiteLine: Type10.Optional(Type10.Number({ description: "1-based call site line (for inline)" }))
50632
50895
  });
50633
50896
  function buildRefactorSections(args, payload, theme) {
50634
50897
  const response = asRecord2(payload);
50635
50898
  if (!response)
50636
50899
  return [theme.fg("muted", "No refactor result.")];
50637
- if (response.dry_run === true) {
50638
- const diffs = asRecords(response.diffs);
50639
- const sections = [theme.fg("warning", `[dry run] ${args.op}`)];
50640
- if (diffs.length === 0) {
50641
- sections.push(theme.fg("muted", "No diff available."));
50642
- return sections;
50643
- }
50644
- diffs.forEach((diff) => {
50645
- const file2 = shortenPath(asString(diff.file) ?? "(unknown file)");
50646
- const rendered = renderUnifiedDiff(asString(diff.diff) ?? "") || theme.fg("muted", "No diff available.");
50647
- sections.push(`${theme.fg("accent", file2)}
50648
- ${rendered}`);
50649
- });
50650
- return sections;
50651
- }
50652
50900
  if (args.op === "move") {
50653
50901
  const results = asRecords(response.results);
50654
50902
  return [
@@ -50691,7 +50939,7 @@ function registerRefactorTool(pi, ctx) {
50691
50939
  pi.registerTool({
50692
50940
  name: "aft_refactor",
50693
50941
  label: "refactor",
50694
- description: "Workspace-wide refactoring that updates imports and references across files. `move` relocates a top-level symbol (only top-level exports); `extract` pulls a line range into a new function; `inline` replaces a call site with the function body.",
50942
+ description: "Workspace-wide refactoring that updates imports and references across files. `move` relocates a top-level symbol; `extract` pulls a line range into a new function; `inline` replaces a call site. Use aft_safety checkpoint/undo before risky refactors.",
50695
50943
  parameters: RefactorParams,
50696
50944
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
50697
50945
  const bridge = bridgeFor(ctx, extCtx.cwd);
@@ -50716,8 +50964,6 @@ function registerRefactorTool(pi, ctx) {
50716
50964
  }
50717
50965
  if (params.callSiteLine !== undefined)
50718
50966
  req.call_site_line = params.callSiteLine;
50719
- if (params.dryRun !== undefined)
50720
- req.dry_run = params.dryRun;
50721
50967
  const response = await callBridge(bridge, commandMap[params.op], req, extCtx);
50722
50968
  return textResult(JSON.stringify(response, null, 2));
50723
50969
  },
@@ -50976,7 +51222,6 @@ var TransformParams = Type13.Object({
50976
51222
  position: Type13.Optional(Type13.String({
50977
51223
  description: "Position hint: 'first', 'last' (default), 'before:name', 'after:name' for add_member"
50978
51224
  })),
50979
- dryRun: Type13.Optional(Type13.Boolean({ description: "Preview without modifying" })),
50980
51225
  validate: Type13.Optional(StringEnum7(["syntax", "full"], {
50981
51226
  description: "Validation level (default: syntax)"
50982
51227
  }))
@@ -50985,12 +51230,6 @@ function buildTransformSections(args, payload, theme) {
50985
51230
  const response = asRecord2(payload);
50986
51231
  if (!response)
50987
51232
  return [theme.fg("muted", "No transform result.")];
50988
- if (response.dry_run === true) {
50989
- return [
50990
- theme.fg("warning", `[dry run] ${args.op}`),
50991
- asString(response.diff) ?? theme.fg("muted", "No diff available.")
50992
- ];
50993
- }
50994
51233
  const target = asString(response.target) ?? asString(response.scope) ?? args.target ?? args.container ?? args.field ?? args.filePath;
50995
51234
  return [
50996
51235
  `${theme.fg("success", "transformed")} ${theme.fg("accent", args.op)}`,
@@ -51016,7 +51255,7 @@ function registerStructureTool(pi, ctx) {
51016
51255
  pi.registerTool({
51017
51256
  name: "aft_transform",
51018
51257
  label: "transform",
51019
- description: "Scope-aware structural code transformations with correct indentation. See parameter descriptions for per-op requirements.",
51258
+ description: "Scope-aware structural code transformations with correct indentation. Use aft_safety checkpoint/undo before risky transforms.",
51020
51259
  parameters: TransformParams,
51021
51260
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
51022
51261
  validateTransformParams(params);
@@ -51042,8 +51281,6 @@ function registerStructureTool(pi, ctx) {
51042
51281
  req.value = params.value;
51043
51282
  if (params.position !== undefined)
51044
51283
  req.position = params.position;
51045
- if (params.dryRun !== undefined)
51046
- req.dry_run = params.dryRun;
51047
51284
  if (params.validate !== undefined)
51048
51285
  req.validate = params.validate;
51049
51286
  const response = await callBridge(bridge, params.op, req, extCtx);
@@ -51195,6 +51432,7 @@ var ALL_ONLY_TOOLS = new Set([
51195
51432
  "aft_transform",
51196
51433
  "aft_refactor"
51197
51434
  ]);
51435
+ var pendingEagerWarnings = new Map;
51198
51436
  function isConfigureWarning(value) {
51199
51437
  if (!value || typeof value !== "object" || Array.isArray(value))
51200
51438
  return false;
@@ -51204,17 +51442,29 @@ function isConfigureWarning(value) {
51204
51442
  function coerceConfigureWarnings(warnings) {
51205
51443
  return warnings.filter(isConfigureWarning);
51206
51444
  }
51445
+ function drainPendingEagerWarnings(projectRoot) {
51446
+ const pending = pendingEagerWarnings.get(projectRoot) ?? [];
51447
+ pendingEagerWarnings.delete(projectRoot);
51448
+ return pending;
51449
+ }
51207
51450
  async function handleConfigureWarningsForSession(context) {
51451
+ const validWarnings = coerceConfigureWarnings(context.warnings);
51208
51452
  if (!context.sessionId) {
51209
- warn2(`[configure] deferred warnings for ${context.projectRoot} arrived without session_id; skipping notification`);
51453
+ if (validWarnings.length === 0)
51454
+ return;
51455
+ const pending = pendingEagerWarnings.get(context.projectRoot) ?? [];
51456
+ pending.push(...validWarnings);
51457
+ pendingEagerWarnings.set(context.projectRoot, pending);
51458
+ warn2(`[configure] deferred warnings for ${context.projectRoot} arrived without session_id; buffering until first session-bound call`);
51210
51459
  return;
51211
51460
  }
51212
51461
  if (!context.client) {
51213
51462
  warn2(`[configure] deferred warnings for session ${context.sessionId} arrived without notification client; skipping notification`);
51214
51463
  return;
51215
51464
  }
51216
- const validWarnings = coerceConfigureWarnings(context.warnings);
51217
- if (validWarnings.length === 0)
51465
+ const pendingWarnings = drainPendingEagerWarnings(context.projectRoot);
51466
+ const combinedWarnings = [...pendingWarnings, ...validWarnings];
51467
+ if (combinedWarnings.length === 0)
51218
51468
  return;
51219
51469
  await deliverConfigureWarnings({
51220
51470
  client: context.client,
@@ -51222,10 +51472,10 @@ async function handleConfigureWarningsForSession(context) {
51222
51472
  storageDir: context.storageDir,
51223
51473
  pluginVersion: context.pluginVersion,
51224
51474
  projectRoot: context.projectRoot
51225
- }, validWarnings);
51475
+ }, combinedWarnings);
51226
51476
  }
51227
51477
  function resolveStorageDir() {
51228
- return join12(homedir9(), ".pi", "agent", "aft");
51478
+ return join13(homedir9(), ".pi", "agent", "aft");
51229
51479
  }
51230
51480
  function resolveToolSurface(config2) {
51231
51481
  const surface = config2.tool_surface ?? "recommended";
@@ -51398,11 +51648,12 @@ ${lines}
51398
51648
  errorPrefix: "[aft-pi]",
51399
51649
  minVersion: PLUGIN_VERSION,
51400
51650
  onConfigureWarnings: async ({ projectRoot, sessionId, client, warnings }) => {
51651
+ const pendingWarnings = sessionId ? drainPendingEagerWarnings(projectRoot) : [];
51401
51652
  await handleConfigureWarningsForSession({
51402
51653
  projectRoot,
51403
51654
  sessionId,
51404
51655
  client,
51405
- warnings,
51656
+ warnings: [...pendingWarnings, ...warnings],
51406
51657
  storageDir,
51407
51658
  pluginVersion: PLUGIN_VERSION
51408
51659
  });
@@ -51511,10 +51762,6 @@ ${lines}
51511
51762
  runtime: pi
51512
51763
  });
51513
51764
  });
51514
- pi.on("input", (_event, extCtx) => {
51515
- resetBgWake(resolveSessionId(extCtx));
51516
- return { action: "continue" };
51517
- });
51518
51765
  pi.on("session_shutdown", async () => {
51519
51766
  try {
51520
51767
  await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);