@cortexkit/aft-pi 0.21.0 → 0.22.1

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));
30896
+ });
30897
+ child.stdout?.on("end", () => {
30898
+ const remaining = stdoutDecoder.end();
30899
+ if (remaining)
30900
+ this.onStdoutData(remaining);
30848
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([
@@ -32630,8 +32736,6 @@ async function handleTurnEndBgCompletions(drainContext) {
32630
32736
  }
32631
32737
  async function triggerWakeIfPending(drainContext, skipDrain) {
32632
32738
  const state = stateFor(drainContext.sessionID);
32633
- if (state.wakeFiredThisIdle)
32634
- return;
32635
32739
  if (drainContext.isActive?.())
32636
32740
  return;
32637
32741
  if (!skipDrain && state.outstandingTaskIds.size > 0) {
@@ -32645,9 +32749,6 @@ async function triggerWakeIfPending(drainContext, skipDrain) {
32645
32749
  sessionWarn2(drainContext.sessionID ?? "", `${LOG_PREFIX} wake send failed: ${err instanceof Error ? err.message : String(err)}`);
32646
32750
  });
32647
32751
  }
32648
- function resetBgWake(sessionID) {
32649
- stateFor(sessionID).wakeFiredThisIdle = false;
32650
- }
32651
32752
  function formatSystemReminder(completions) {
32652
32753
  const bullets = completions.map((completion) => formatCompletion(completion)).join(`
32653
32754
  `);
@@ -32723,7 +32824,6 @@ function scheduleWake(state, sendWake, onSendFailure) {
32723
32824
  state.pendingLongRunning = [];
32724
32825
  sendWake(reminder).then(() => {
32725
32826
  state.retryDelayMs = null;
32726
- state.wakeFiredThisIdle = true;
32727
32827
  }).catch((err) => {
32728
32828
  state.pendingCompletions = [...pending, ...state.pendingCompletions];
32729
32829
  state.pendingLongRunning = [...pendingLongRunning, ...state.pendingLongRunning];
@@ -32745,7 +32845,6 @@ function stateFor(sessionID) {
32745
32845
  pendingCompletions: [],
32746
32846
  pendingLongRunning: [],
32747
32847
  debounceTimer: null,
32748
- wakeFiredThisIdle: false,
32749
32848
  firstCompletionAt: null,
32750
32849
  scheduledFireAt: null,
32751
32850
  scheduledCompletionCount: 0,
@@ -43998,7 +44097,7 @@ function finalize(ctx, schema) {
43998
44097
  result.$schema = "http://json-schema.org/draft-07/schema#";
43999
44098
  } else if (ctx.target === "draft-04") {
44000
44099
  result.$schema = "http://json-schema.org/draft-04/schema#";
44001
- } else if (ctx.target === "openapi-3.0") {} else {}
44100
+ } else if (ctx.target === "openapi-3.0") {}
44002
44101
  if (ctx.external?.uri) {
44003
44102
  const id = ctx.external.registry.get(schema)?.id;
44004
44103
  if (!id)
@@ -44246,7 +44345,7 @@ var literalProcessor = (schema, ctx, json, _params) => {
44246
44345
  if (val === undefined) {
44247
44346
  if (ctx.unrepresentable === "throw") {
44248
44347
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
44249
- } else {}
44348
+ }
44250
44349
  } else if (typeof val === "bigint") {
44251
44350
  if (ctx.unrepresentable === "throw") {
44252
44351
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -47046,7 +47145,8 @@ function loadAftConfig(projectDirectory) {
47046
47145
  // src/lsp-auto-install.ts
47047
47146
  import { spawn as spawn2 } from "node:child_process";
47048
47147
  import { createHash as createHash3 } from "node:crypto";
47049
- 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";
47050
47150
 
47051
47151
  // src/lsp-cache.ts
47052
47152
  import {
@@ -47730,6 +47830,50 @@ function hashInstalledBinary(spec) {
47730
47830
  stream.on("end", () => resolve2(hash2.digest("hex")));
47731
47831
  });
47732
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
+ }
47733
47877
  function runAutoInstall(projectRoot, config2, fetchImpl2 = fetch) {
47734
47878
  const cachedBinDirs = [];
47735
47879
  const skipped = [];
@@ -47742,7 +47886,7 @@ function runAutoInstall(projectRoot, config2, fetchImpl2 = fetch) {
47742
47886
  return projectExtensions;
47743
47887
  };
47744
47888
  for (const spec of NPM_LSP_TABLE) {
47745
- if (isInstalled(spec.npm, spec.binary)) {
47889
+ if (isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)) {
47746
47890
  cachedBinDirs.push(lspBinDir(spec.npm));
47747
47891
  }
47748
47892
  if (config2.disabled.has(spec.id)) {
@@ -47794,16 +47938,17 @@ import {
47794
47938
  createWriteStream as createWriteStream2,
47795
47939
  existsSync as existsSync7,
47796
47940
  lstatSync as lstatSync2,
47797
- mkdirSync as mkdirSync6,
47941
+ mkdirSync as mkdirSync7,
47798
47942
  readdirSync as readdirSync4,
47943
+ readFileSync as readFileSync6,
47799
47944
  readlinkSync as readlinkSync2,
47800
47945
  realpathSync as realpathSync3,
47801
- renameSync as renameSync3,
47802
- rmSync as rmSync2,
47946
+ renameSync as renameSync4,
47947
+ rmSync as rmSync3,
47803
47948
  statSync as statSync4,
47804
47949
  unlinkSync as unlinkSync6
47805
47950
  } from "node:fs";
47806
- 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";
47807
47952
  import { Readable as Readable2 } from "node:stream";
47808
47953
  import { pipeline as pipeline2 } from "node:stream/promises";
47809
47954
 
@@ -47893,25 +48038,25 @@ function detectHostPlatform() {
47893
48038
 
47894
48039
  // src/lsp-github-install.ts
47895
48040
  function ghCacheRoot() {
47896
- return join10(aftCacheBase(), "lsp-binaries");
48041
+ return join11(aftCacheBase(), "lsp-binaries");
47897
48042
  }
47898
48043
  function ghPackageDir(spec) {
47899
- return join10(ghCacheRoot(), spec.id);
48044
+ return join11(ghCacheRoot(), spec.id);
47900
48045
  }
47901
48046
  function ghBinDir(spec) {
47902
- return join10(ghPackageDir(spec), "bin");
48047
+ return join11(ghPackageDir(spec), "bin");
47903
48048
  }
47904
48049
  function ghExtractDir(spec) {
47905
- return join10(ghPackageDir(spec), "extracted");
48050
+ return join11(ghPackageDir(spec), "extracted");
47906
48051
  }
47907
48052
  function ghBinaryPath(spec, platform) {
47908
48053
  const ext = platform === "win32" ? ".exe" : "";
47909
- return join10(ghBinDir(spec), `${spec.binary}${ext}`);
48054
+ return join11(ghBinDir(spec), `${spec.binary}${ext}`);
47910
48055
  }
47911
48056
  function isGithubInstalled(spec, platform) {
47912
48057
  for (const candidate of ghBinaryCandidates(spec, platform)) {
47913
48058
  try {
47914
- if (statSync4(join10(ghBinDir(spec), candidate)).isFile())
48059
+ if (statSync4(join11(ghBinDir(spec), candidate)).isFile())
47915
48060
  return true;
47916
48061
  } catch {}
47917
48062
  }
@@ -47933,6 +48078,9 @@ function sha256OfFile(path2) {
47933
48078
  stream.on("end", () => resolve3(hash2.digest("hex")));
47934
48079
  });
47935
48080
  }
48081
+ function sha256OfFileSync2(path2) {
48082
+ return createHash4("sha256").update(readFileSync6(path2)).digest("hex");
48083
+ }
47936
48084
  async function fetchReleaseByTag(githubRepo, tag, fetchImpl2, signal) {
47937
48085
  const candidates = [];
47938
48086
  candidates.push(tag);
@@ -47953,11 +48101,7 @@ async function fetchReleaseByTag(githubRepo, tag, fetchImpl2, signal) {
47953
48101
  const url2 = `https://api.github.com/repos/${githubRepo}/releases/tags/${encodeURIComponent(candidate)}`;
47954
48102
  const timeout = controlledTimeoutSignal(15000, signal);
47955
48103
  try {
47956
- const res = await fetchImpl2(url2, {
47957
- headers,
47958
- redirect: "follow",
47959
- signal: timeout.signal
47960
- });
48104
+ const res = await fetchJsonFollowingRedirects(url2, headers, fetchImpl2, timeout.signal);
47961
48105
  if (res.status === 404)
47962
48106
  continue;
47963
48107
  if (!res.ok) {
@@ -47987,6 +48131,23 @@ async function fetchReleaseByTag(githubRepo, tag, fetchImpl2, signal) {
47987
48131
  }
47988
48132
  return null;
47989
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
+ }
47990
48151
  async function resolveTargetTag(spec, config2, fetchImpl2, signal) {
47991
48152
  const pinned = config2.versions[spec.githubRepo];
47992
48153
  if (pinned) {
@@ -48081,17 +48242,12 @@ function assertAllowedDownloadUrl(rawUrl) {
48081
48242
  return parsed;
48082
48243
  }
48083
48244
  async function downloadFile(url2, destPath, fetchImpl2, assetSize, signal) {
48084
- assertAllowedDownloadUrl(url2);
48085
48245
  if (assetSize !== undefined && assetSize > MAX_DOWNLOAD_BYTES2) {
48086
48246
  throw new Error(`asset size ${assetSize} exceeds max ${MAX_DOWNLOAD_BYTES2} (set lsp.versions to pin a smaller release if this is wrong)`);
48087
48247
  }
48088
48248
  const timeout = controlledTimeoutSignal(120000, signal);
48089
48249
  try {
48090
- const res = await fetchImpl2(url2, {
48091
- headers: { accept: "application/octet-stream" },
48092
- redirect: "follow",
48093
- signal: timeout.signal
48094
- });
48250
+ const res = await fetchFollowingRedirects(url2, fetchImpl2, timeout.signal);
48095
48251
  if (!res.ok || !res.body) {
48096
48252
  throw new Error(`download failed (${res.status})`);
48097
48253
  }
@@ -48099,7 +48255,7 @@ async function downloadFile(url2, destPath, fetchImpl2, assetSize, signal) {
48099
48255
  if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES2) {
48100
48256
  throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES2}`);
48101
48257
  }
48102
- mkdirSync6(dirname2(destPath), { recursive: true });
48258
+ mkdirSync7(dirname2(destPath), { recursive: true });
48103
48259
  let bytesWritten = 0;
48104
48260
  const guard = new TransformStream({
48105
48261
  transform(chunk, controller) {
@@ -48123,6 +48279,27 @@ async function downloadFile(url2, destPath, fetchImpl2, assetSize, signal) {
48123
48279
  timeout.cleanup();
48124
48280
  }
48125
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
+ }
48126
48303
  function validateExtraction(stagingRoot) {
48127
48304
  const realStagingRoot = realpathSync3(stagingRoot);
48128
48305
  let totalBytes = 0;
@@ -48134,7 +48311,7 @@ function validateExtraction(stagingRoot) {
48134
48311
  throw new Error(`failed to read staging dir ${dir}: ${err}`);
48135
48312
  }
48136
48313
  for (const entry of entries) {
48137
- const full = join10(dir, entry);
48314
+ const full = join11(dir, entry);
48138
48315
  let lst;
48139
48316
  try {
48140
48317
  lst = lstatSync2(full);
@@ -48172,27 +48349,83 @@ function validateExtraction(stagingRoot) {
48172
48349
  };
48173
48350
  walk(realStagingRoot);
48174
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
+ }
48175
48375
  function extractArchiveSafely(archivePath, destDir, archiveType) {
48176
48376
  const suffix = randomBytes(8).toString("hex");
48177
48377
  const stagingDir = `${destDir}.staging-${suffix}`;
48178
48378
  try {
48179
- rmSync2(stagingDir, { recursive: true, force: true });
48379
+ rmSync3(stagingDir, { recursive: true, force: true });
48180
48380
  } catch {}
48181
- mkdirSync6(stagingDir, { recursive: true });
48381
+ mkdirSync7(stagingDir, { recursive: true });
48182
48382
  try {
48383
+ precheckArchiveSize(archivePath, archiveType);
48183
48384
  runPlatformExtractor(archivePath, stagingDir, archiveType);
48184
48385
  validateExtraction(stagingDir);
48185
48386
  try {
48186
- rmSync2(destDir, { recursive: true, force: true });
48387
+ rmSync3(destDir, { recursive: true, force: true });
48187
48388
  } catch {}
48188
- renameSync3(stagingDir, destDir);
48389
+ renameSync4(stagingDir, destDir);
48189
48390
  } catch (err) {
48190
48391
  try {
48191
- rmSync2(stagingDir, { recursive: true, force: true });
48392
+ rmSync3(stagingDir, { recursive: true, force: true });
48192
48393
  } catch {}
48193
48394
  throw err;
48194
48395
  }
48195
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
+ }
48196
48429
  function runPlatformExtractor(archivePath, destDir, archiveType) {
48197
48430
  if (archiveType === "zip") {
48198
48431
  if (process.platform === "win32") {
@@ -48238,7 +48471,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl2,
48238
48471
  }
48239
48472
  const pkgDir = ghPackageDir(spec);
48240
48473
  const extractDir = ghExtractDir(spec);
48241
- const archivePath = join10(pkgDir, expected.name);
48474
+ const archivePath = join11(pkgDir, expected.name);
48242
48475
  log2(`[lsp] downloading ${spec.id} ${tag} → ${matchingAsset.url}`);
48243
48476
  try {
48244
48477
  await downloadFile(matchingAsset.url, archivePath, fetchImpl2, matchingAsset.size, signal);
@@ -48277,13 +48510,13 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl2,
48277
48510
  unlinkSync6(archivePath);
48278
48511
  } catch {}
48279
48512
  }
48280
- const innerBinaryPath = join10(extractDir, spec.binaryPathInArchive(platform, arch, version2));
48513
+ const innerBinaryPath = join11(extractDir, spec.binaryPathInArchive(platform, arch, version2));
48281
48514
  if (!existsSync7(innerBinaryPath)) {
48282
48515
  error2(`[lsp] ${spec.id}: extracted binary not found at ${innerBinaryPath}`);
48283
48516
  return null;
48284
48517
  }
48285
48518
  const targetBinary = ghBinaryPath(spec, platform);
48286
- mkdirSync6(dirname2(targetBinary), { recursive: true });
48519
+ mkdirSync7(dirname2(targetBinary), { recursive: true });
48287
48520
  try {
48288
48521
  copyFileSync3(innerBinaryPath, targetBinary);
48289
48522
  if (platform !== "win32") {
@@ -48374,7 +48607,7 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl2 = fetch) {
48374
48607
  };
48375
48608
  }
48376
48609
  for (const spec of GITHUB_LSP_TABLE) {
48377
- if (isGithubInstalled(spec, host.platform)) {
48610
+ if (isGithubInstalled(spec, host.platform) && validateCachedGithubInstall(spec, host.platform)) {
48378
48611
  cachedBinDirs.push(ghBinDir(spec));
48379
48612
  }
48380
48613
  if (config2.disabled.has(spec.id)) {
@@ -48458,8 +48691,8 @@ function discoverRelevantGithubServers(projectRoot) {
48458
48691
  }
48459
48692
 
48460
48693
  // src/notifications.ts
48461
- import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
48462
- 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";
48463
48696
  var WARNING_MARKER = "\uD83D\uDD27 AFT: ⚠️";
48464
48697
  var FEATURE_MARKER = "\uD83D\uDD27 AFT: ✨";
48465
48698
  var WARNED_TOOLS_FILE = "warned_tools.json";
@@ -48477,10 +48710,10 @@ function sendIgnoredMessage(client, sessionId, text) {
48477
48710
  }
48478
48711
  function readWarnedTools(storageDir) {
48479
48712
  try {
48480
- const warnedToolsPath = join11(storageDir, WARNED_TOOLS_FILE);
48713
+ const warnedToolsPath = join12(storageDir, WARNED_TOOLS_FILE);
48481
48714
  if (!existsSync8(warnedToolsPath))
48482
48715
  return {};
48483
- const parsed = JSON.parse(readFileSync5(warnedToolsPath, "utf-8"));
48716
+ const parsed = JSON.parse(readFileSync7(warnedToolsPath, "utf-8"));
48484
48717
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
48485
48718
  return {};
48486
48719
  const warned = {};
@@ -48496,12 +48729,34 @@ function readWarnedTools(storageDir) {
48496
48729
  }
48497
48730
  function writeWarnedTools(storageDir, warned) {
48498
48731
  try {
48499
- mkdirSync7(storageDir, { recursive: true });
48500
- const warnedToolsPath = join11(storageDir, WARNED_TOOLS_FILE);
48501
- 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)}
48502
48736
  `);
48737
+ renameSync5(tmpPath, warnedToolsPath);
48503
48738
  } catch {}
48504
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
+ }
48505
48760
  function warningKey(warning, projectRoot) {
48506
48761
  const scope = warning.kind === "lsp_binary_missing" ? "_" : projectRoot ?? "_";
48507
48762
  return [
@@ -48540,27 +48795,36 @@ ${warning.hint}`;
48540
48795
  async function deliverConfigureWarnings(opts, warnings) {
48541
48796
  if (warnings.length === 0)
48542
48797
  return;
48543
- const warned = readWarnedTools(opts.storageDir);
48544
- let changed = false;
48545
- for (const warning of warnings) {
48546
- const key = warningKey(warning, opts.projectRoot);
48547
- if (Object.hasOwn(warned, key))
48548
- continue;
48549
- if (!sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning))) {
48550
- 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;
48551
48810
  }
48552
- warned[key] = opts.pluginVersion;
48553
- changed = true;
48554
- }
48555
- if (changed) {
48556
- 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));
48557
48821
  }
48558
48822
  }
48559
48823
  function sendFeatureAnnouncement(version2, features, storageDir) {
48560
- const versionFile = join11(storageDir, "last_announced_version");
48824
+ const versionFile = join12(storageDir, "last_announced_version");
48561
48825
  try {
48562
48826
  if (existsSync8(versionFile)) {
48563
- const lastVersion = readFileSync5(versionFile, "utf-8").trim();
48827
+ const lastVersion = readFileSync7(versionFile, "utf-8").trim();
48564
48828
  if (lastVersion === version2)
48565
48829
  return;
48566
48830
  }
@@ -48568,7 +48832,7 @@ function sendFeatureAnnouncement(version2, features, storageDir) {
48568
48832
  log2([`${FEATURE_MARKER} v${version2}:`, ...features.map((feature) => ` • ${feature}`)].join(`
48569
48833
  `));
48570
48834
  try {
48571
- mkdirSync7(storageDir, { recursive: true });
48835
+ mkdirSync8(storageDir, { recursive: true });
48572
48836
  writeFileSync5(versionFile, version2);
48573
48837
  } catch {}
48574
48838
  }
@@ -50011,7 +50275,6 @@ var ImportParams = Type6.Object({
50011
50275
  defaultImport: Type6.Optional(Type6.String({ description: "Default import name (e.g. 'React')" })),
50012
50276
  removeName: Type6.Optional(Type6.String({ description: "Named import to remove; omit to remove entire import" })),
50013
50277
  typeOnly: Type6.Optional(Type6.Boolean({ description: "Type-only import (TS only)" })),
50014
- dryRun: Type6.Optional(Type6.Boolean({ description: "Preview without writing" })),
50015
50278
  validate: Type6.Optional(StringEnum2(["syntax", "full"], {
50016
50279
  description: "Post-edit validation level (default: syntax)"
50017
50280
  }))
@@ -50020,12 +50283,6 @@ function buildImportSections(args, payload, theme) {
50020
50283
  const response = asRecord2(payload);
50021
50284
  if (!response)
50022
50285
  return [theme.fg("muted", "No import result.")];
50023
- if (response.dry_run === true) {
50024
- return [
50025
- theme.fg("warning", `[dry run] ${args.op}`),
50026
- asString(response.diff) || theme.fg("muted", "No diff available.")
50027
- ];
50028
- }
50029
50286
  if (args.op === "organize") {
50030
50287
  const groups = asRecords(response.groups);
50031
50288
  const groupText = groups.length > 0 ? groups.map((group) => `${asString(group.name) ?? "unknown"}: ${asNumber(group.count) ?? 0}`).join(" · ") : "No imports found";
@@ -50068,7 +50325,7 @@ function registerImportTools(pi, ctx) {
50068
50325
  pi.registerTool({
50069
50326
  name: "aft_import",
50070
50327
  label: "import",
50071
- 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.",
50072
50329
  parameters: ImportParams,
50073
50330
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
50074
50331
  if ((params.op === "add" || params.op === "remove") && !params.module) {
@@ -50091,8 +50348,6 @@ function registerImportTools(pi, ctx) {
50091
50348
  req.name = params.removeName;
50092
50349
  if (params.typeOnly !== undefined)
50093
50350
  req.type_only = params.typeOnly;
50094
- if (params.dryRun !== undefined)
50095
- req.dry_run = params.dryRun;
50096
50351
  if (params.validate !== undefined)
50097
50352
  req.validate = params.validate;
50098
50353
  const response = await callBridge(bridge, commandMap[params.op], req, extCtx);
@@ -50573,7 +50828,7 @@ Pass a single \`target\`:
50573
50828
  if (response.success === false) {
50574
50829
  throw new Error(response.message || "zoom failed");
50575
50830
  }
50576
- return textResult(formatZoomText(targetLabel, response));
50831
+ return textResult(formatZoomText(targetLabel, response), response);
50577
50832
  },
50578
50833
  renderCall(args, theme, context) {
50579
50834
  return renderZoomCall(args, theme, context);
@@ -50636,28 +50891,12 @@ var RefactorParams = Type10.Object({
50636
50891
  name: Type10.Optional(Type10.String({ description: "New function name (for extract)" })),
50637
50892
  startLine: Type10.Optional(Type10.Number({ description: "1-based start line (for extract)" })),
50638
50893
  endLine: Type10.Optional(Type10.Number({ description: "1-based end line, inclusive (for extract)" })),
50639
- callSiteLine: Type10.Optional(Type10.Number({ description: "1-based call site line (for inline)" })),
50640
- dryRun: Type10.Optional(Type10.Boolean({ description: "Preview as diff" }))
50894
+ callSiteLine: Type10.Optional(Type10.Number({ description: "1-based call site line (for inline)" }))
50641
50895
  });
50642
50896
  function buildRefactorSections(args, payload, theme) {
50643
50897
  const response = asRecord2(payload);
50644
50898
  if (!response)
50645
50899
  return [theme.fg("muted", "No refactor result.")];
50646
- if (response.dry_run === true) {
50647
- const diffs = asRecords(response.diffs);
50648
- const sections = [theme.fg("warning", `[dry run] ${args.op}`)];
50649
- if (diffs.length === 0) {
50650
- sections.push(theme.fg("muted", "No diff available."));
50651
- return sections;
50652
- }
50653
- diffs.forEach((diff) => {
50654
- const file2 = shortenPath(asString(diff.file) ?? "(unknown file)");
50655
- const rendered = renderUnifiedDiff(asString(diff.diff) ?? "") || theme.fg("muted", "No diff available.");
50656
- sections.push(`${theme.fg("accent", file2)}
50657
- ${rendered}`);
50658
- });
50659
- return sections;
50660
- }
50661
50900
  if (args.op === "move") {
50662
50901
  const results = asRecords(response.results);
50663
50902
  return [
@@ -50700,7 +50939,7 @@ function registerRefactorTool(pi, ctx) {
50700
50939
  pi.registerTool({
50701
50940
  name: "aft_refactor",
50702
50941
  label: "refactor",
50703
- 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.",
50704
50943
  parameters: RefactorParams,
50705
50944
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
50706
50945
  const bridge = bridgeFor(ctx, extCtx.cwd);
@@ -50725,8 +50964,6 @@ function registerRefactorTool(pi, ctx) {
50725
50964
  }
50726
50965
  if (params.callSiteLine !== undefined)
50727
50966
  req.call_site_line = params.callSiteLine;
50728
- if (params.dryRun !== undefined)
50729
- req.dry_run = params.dryRun;
50730
50967
  const response = await callBridge(bridge, commandMap[params.op], req, extCtx);
50731
50968
  return textResult(JSON.stringify(response, null, 2));
50732
50969
  },
@@ -50985,7 +51222,6 @@ var TransformParams = Type13.Object({
50985
51222
  position: Type13.Optional(Type13.String({
50986
51223
  description: "Position hint: 'first', 'last' (default), 'before:name', 'after:name' for add_member"
50987
51224
  })),
50988
- dryRun: Type13.Optional(Type13.Boolean({ description: "Preview without modifying" })),
50989
51225
  validate: Type13.Optional(StringEnum7(["syntax", "full"], {
50990
51226
  description: "Validation level (default: syntax)"
50991
51227
  }))
@@ -50994,12 +51230,6 @@ function buildTransformSections(args, payload, theme) {
50994
51230
  const response = asRecord2(payload);
50995
51231
  if (!response)
50996
51232
  return [theme.fg("muted", "No transform result.")];
50997
- if (response.dry_run === true) {
50998
- return [
50999
- theme.fg("warning", `[dry run] ${args.op}`),
51000
- asString(response.diff) ?? theme.fg("muted", "No diff available.")
51001
- ];
51002
- }
51003
51233
  const target = asString(response.target) ?? asString(response.scope) ?? args.target ?? args.container ?? args.field ?? args.filePath;
51004
51234
  return [
51005
51235
  `${theme.fg("success", "transformed")} ${theme.fg("accent", args.op)}`,
@@ -51025,7 +51255,7 @@ function registerStructureTool(pi, ctx) {
51025
51255
  pi.registerTool({
51026
51256
  name: "aft_transform",
51027
51257
  label: "transform",
51028
- 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.",
51029
51259
  parameters: TransformParams,
51030
51260
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
51031
51261
  validateTransformParams(params);
@@ -51051,8 +51281,6 @@ function registerStructureTool(pi, ctx) {
51051
51281
  req.value = params.value;
51052
51282
  if (params.position !== undefined)
51053
51283
  req.position = params.position;
51054
- if (params.dryRun !== undefined)
51055
- req.dry_run = params.dryRun;
51056
51284
  if (params.validate !== undefined)
51057
51285
  req.validate = params.validate;
51058
51286
  const response = await callBridge(bridge, params.op, req, extCtx);
@@ -51204,6 +51432,7 @@ var ALL_ONLY_TOOLS = new Set([
51204
51432
  "aft_transform",
51205
51433
  "aft_refactor"
51206
51434
  ]);
51435
+ var pendingEagerWarnings = new Map;
51207
51436
  function isConfigureWarning(value) {
51208
51437
  if (!value || typeof value !== "object" || Array.isArray(value))
51209
51438
  return false;
@@ -51213,17 +51442,33 @@ function isConfigureWarning(value) {
51213
51442
  function coerceConfigureWarnings(warnings) {
51214
51443
  return warnings.filter(isConfigureWarning);
51215
51444
  }
51445
+ function drainPendingEagerWarnings(projectRoot) {
51446
+ const pending = pendingEagerWarnings.get(projectRoot) ?? [];
51447
+ pendingEagerWarnings.delete(projectRoot);
51448
+ return pending;
51449
+ }
51450
+ function shouldPrepareOnnxRuntime(config2) {
51451
+ const isFastembedSemanticBackend = (config2.semantic?.backend ?? "fastembed") === "fastembed";
51452
+ return config2.semantic_search === true && isFastembedSemanticBackend;
51453
+ }
51216
51454
  async function handleConfigureWarningsForSession(context) {
51455
+ const validWarnings = coerceConfigureWarnings(context.warnings);
51217
51456
  if (!context.sessionId) {
51218
- warn2(`[configure] deferred warnings for ${context.projectRoot} arrived without session_id; skipping notification`);
51457
+ if (validWarnings.length === 0)
51458
+ return;
51459
+ const pending = pendingEagerWarnings.get(context.projectRoot) ?? [];
51460
+ pending.push(...validWarnings);
51461
+ pendingEagerWarnings.set(context.projectRoot, pending);
51462
+ warn2(`[configure] deferred warnings for ${context.projectRoot} arrived without session_id; buffering until first session-bound call`);
51219
51463
  return;
51220
51464
  }
51221
51465
  if (!context.client) {
51222
51466
  warn2(`[configure] deferred warnings for session ${context.sessionId} arrived without notification client; skipping notification`);
51223
51467
  return;
51224
51468
  }
51225
- const validWarnings = coerceConfigureWarnings(context.warnings);
51226
- if (validWarnings.length === 0)
51469
+ const pendingWarnings = drainPendingEagerWarnings(context.projectRoot);
51470
+ const combinedWarnings = [...pendingWarnings, ...validWarnings];
51471
+ if (combinedWarnings.length === 0)
51227
51472
  return;
51228
51473
  await deliverConfigureWarnings({
51229
51474
  client: context.client,
@@ -51231,10 +51476,10 @@ async function handleConfigureWarningsForSession(context) {
51231
51476
  storageDir: context.storageDir,
51232
51477
  pluginVersion: context.pluginVersion,
51233
51478
  projectRoot: context.projectRoot
51234
- }, validWarnings);
51479
+ }, combinedWarnings);
51235
51480
  }
51236
51481
  function resolveStorageDir() {
51237
- return join12(homedir9(), ".pi", "agent", "aft");
51482
+ return join13(homedir9(), ".pi", "agent", "aft");
51238
51483
  }
51239
51484
  function resolveToolSurface(config2) {
51240
51485
  const surface = config2.tool_surface ?? "recommended";
@@ -51309,7 +51554,7 @@ async function src_default(pi) {
51309
51554
  const config2 = loadAftConfig(process.cwd());
51310
51555
  const storageDir = resolveStorageDir();
51311
51556
  let onnxRuntimePromise = null;
51312
- if (config2.semantic_search) {
51557
+ if (shouldPrepareOnnxRuntime(config2)) {
51313
51558
  onnxRuntimePromise = ensureOnnxRuntime(storageDir).catch((err) => {
51314
51559
  warn2(`Failed to prepare ONNX Runtime: ${err instanceof Error ? err.message : String(err)}`);
51315
51560
  return null;
@@ -51407,11 +51652,12 @@ ${lines}
51407
51652
  errorPrefix: "[aft-pi]",
51408
51653
  minVersion: PLUGIN_VERSION,
51409
51654
  onConfigureWarnings: async ({ projectRoot, sessionId, client, warnings }) => {
51655
+ const pendingWarnings = sessionId ? drainPendingEagerWarnings(projectRoot) : [];
51410
51656
  await handleConfigureWarningsForSession({
51411
51657
  projectRoot,
51412
51658
  sessionId,
51413
51659
  client,
51414
- warnings,
51660
+ warnings: [...pendingWarnings, ...warnings],
51415
51661
  storageDir,
51416
51662
  pluginVersion: PLUGIN_VERSION
51417
51663
  });
@@ -51520,10 +51766,6 @@ ${lines}
51520
51766
  runtime: pi
51521
51767
  });
51522
51768
  });
51523
- pi.on("input", (_event, extCtx) => {
51524
- resetBgWake(resolveSessionId(extCtx));
51525
- return { action: "continue" };
51526
- });
51527
51769
  pi.on("session_shutdown", async () => {
51528
51770
  try {
51529
51771
  await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
@@ -51543,7 +51785,11 @@ ${lines}
51543
51785
  });
51544
51786
  log2(`AFT extension ready (surface=${config2.tool_surface ?? "recommended"})`);
51545
51787
  }
51546
- var __test__2 = { resolveToolSurface, handleConfigureWarningsForSession };
51788
+ var __test__2 = {
51789
+ resolveToolSurface,
51790
+ handleConfigureWarningsForSession,
51791
+ shouldPrepareOnnxRuntime
51792
+ };
51547
51793
  export {
51548
51794
  src_default as default,
51549
51795
  __test__2 as __test__