@cortexkit/aft-opencode 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
@@ -7803,20 +7803,27 @@ var require_src2 = __commonJS((exports, module) => {
7803
7803
  });
7804
7804
 
7805
7805
  // src/index.ts
7806
- import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync11 } from "fs";
7806
+ import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
7807
7807
  import { createRequire as createRequire2 } from "module";
7808
7808
  import { homedir as homedir11 } from "os";
7809
- import { join as join19 } from "path";
7809
+ import { join as join21 } from "path";
7810
7810
 
7811
7811
  // ../aft-bridge/dist/active-logger.js
7812
- var active;
7812
+ var ACTIVE_LOGGER_SYMBOL = Symbol.for("aft-bridge-active-logger");
7813
+ function loggerGlobal() {
7814
+ return globalThis;
7815
+ }
7813
7816
  function setActiveLogger(logger) {
7814
- active = logger;
7817
+ loggerGlobal()[ACTIVE_LOGGER_SYMBOL] = logger;
7818
+ }
7819
+ function getActiveLogger() {
7820
+ return loggerGlobal()[ACTIVE_LOGGER_SYMBOL];
7815
7821
  }
7816
7822
  function getLogFilePath() {
7817
- return active?.getLogFilePath?.();
7823
+ return getActiveLogger()?.getLogFilePath?.();
7818
7824
  }
7819
7825
  function log(message, meta) {
7826
+ const active = getActiveLogger();
7820
7827
  if (active) {
7821
7828
  active.log(message, meta);
7822
7829
  } else {
@@ -7824,6 +7831,7 @@ function log(message, meta) {
7824
7831
  }
7825
7832
  }
7826
7833
  function warn(message, meta) {
7834
+ const active = getActiveLogger();
7827
7835
  if (active) {
7828
7836
  active.warn(message, meta);
7829
7837
  } else {
@@ -7831,6 +7839,7 @@ function warn(message, meta) {
7831
7839
  }
7832
7840
  }
7833
7841
  function error(message, meta) {
7842
+ const active = getActiveLogger();
7834
7843
  if (active) {
7835
7844
  active.error(message, meta);
7836
7845
  } else {
@@ -7850,6 +7859,7 @@ function sessionError(sessionId, message) {
7850
7859
  import { spawn } from "child_process";
7851
7860
  import { homedir } from "os";
7852
7861
  import { join } from "path";
7862
+ import { StringDecoder } from "string_decoder";
7853
7863
  var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
7854
7864
  var SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS = 5000;
7855
7865
  var MAX_STDOUT_BUFFER = 64 * 1024 * 1024;
@@ -7946,6 +7956,7 @@ class BinaryBridge {
7946
7956
  configureWarningClients = new Map;
7947
7957
  restartResetTimer = null;
7948
7958
  errorPrefix;
7959
+ logger;
7949
7960
  constructor(binaryPath, cwd, options, configOverrides) {
7950
7961
  this.binaryPath = binaryPath;
7951
7962
  this.cwd = cwd;
@@ -7958,6 +7969,28 @@ class BinaryBridge {
7958
7969
  this.onBashCompletion = options?.onBashCompletion;
7959
7970
  this.onBashLongRunning = options?.onBashLongRunning;
7960
7971
  this.errorPrefix = options?.errorPrefix ?? "[aft-bridge]";
7972
+ this.logger = options?.logger;
7973
+ }
7974
+ logVia(message, meta) {
7975
+ const logger = this.logger ?? getActiveLogger();
7976
+ if (logger)
7977
+ logger.log(message, meta);
7978
+ else
7979
+ log(message, meta);
7980
+ }
7981
+ warnVia(message, meta) {
7982
+ const logger = this.logger ?? getActiveLogger();
7983
+ if (logger)
7984
+ logger.warn(message, meta);
7985
+ else
7986
+ warn(message, meta);
7987
+ }
7988
+ errorVia(message, meta) {
7989
+ const logger = this.logger ?? getActiveLogger();
7990
+ if (logger)
7991
+ logger.error(message, meta);
7992
+ else
7993
+ error(message, meta);
7961
7994
  }
7962
7995
  get restartCount() {
7963
7996
  return this._restartCount;
@@ -8066,8 +8099,8 @@ class BinaryBridge {
8066
8099
  return;
8067
8100
  if (configResult.warnings.length === 0)
8068
8101
  return;
8102
+ const sessionId = typeof params.session_id === "string" ? params.session_id : undefined;
8069
8103
  try {
8070
- const sessionId = typeof params.session_id === "string" ? params.session_id : undefined;
8071
8104
  await this.onConfigureWarnings({
8072
8105
  projectRoot: this.cwd,
8073
8106
  sessionId,
@@ -8076,6 +8109,10 @@ class BinaryBridge {
8076
8109
  });
8077
8110
  } catch (err) {
8078
8111
  warn(`configure warning delivery failed: ${err instanceof Error ? err.message : String(err)}`);
8112
+ } finally {
8113
+ if (sessionId) {
8114
+ this.configureWarningClients.delete(sessionId);
8115
+ }
8079
8116
  }
8080
8117
  }
8081
8118
  async handleConfigureWarningsFrame(frame) {
@@ -8087,16 +8124,23 @@ class BinaryBridge {
8087
8124
  const projectRoot = typeof frame.project_root === "string" ? frame.project_root : this.cwd;
8088
8125
  const rawSessionId = frame.session_id;
8089
8126
  const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : null;
8090
- await this.onConfigureWarnings({
8091
- projectRoot,
8092
- sessionId,
8093
- client: sessionId ? this.configureWarningClients.get(sessionId) : undefined,
8094
- warnings
8095
- });
8127
+ try {
8128
+ await this.onConfigureWarnings({
8129
+ projectRoot,
8130
+ sessionId,
8131
+ client: sessionId ? this.configureWarningClients.get(sessionId) : undefined,
8132
+ warnings
8133
+ });
8134
+ } finally {
8135
+ if (sessionId) {
8136
+ this.configureWarningClients.delete(sessionId);
8137
+ }
8138
+ }
8096
8139
  }
8097
8140
  async shutdown() {
8098
8141
  this._shuttingDown = true;
8099
8142
  this.clearRestartResetTimer();
8143
+ this.configureWarningClients.clear();
8100
8144
  this.rejectAllPending(new Error(`${this.errorPrefix} Bridge shutting down`));
8101
8145
  if (this.process) {
8102
8146
  const proc = this.process;
@@ -8120,10 +8164,12 @@ class BinaryBridge {
8120
8164
  return;
8121
8165
  try {
8122
8166
  const resp = await this.send("version");
8167
+ if (resp.success === false) {
8168
+ throw new Error(`Binary version check failed: ${String(resp.code ?? "unknown")} \u2014 likely too old`);
8169
+ }
8123
8170
  const binaryVersion = resp.version;
8124
- if (!binaryVersion) {
8125
- log("Binary did not report a version \u2014 skipping version check");
8126
- return;
8171
+ if (typeof binaryVersion !== "string") {
8172
+ throw new Error(`Binary did not report a version \u2014 likely too old (minVersion: ${this.minVersion})`);
8127
8173
  }
8128
8174
  log(`Binary version: ${binaryVersion}`);
8129
8175
  if (compareSemver(binaryVersion, this.minVersion) < 0) {
@@ -8132,6 +8178,7 @@ class BinaryBridge {
8132
8178
  }
8133
8179
  } catch (err) {
8134
8180
  warn(`Version check failed: ${err.message}`);
8181
+ throw err;
8135
8182
  }
8136
8183
  }
8137
8184
  ensureSpawned(triggeringSessionId) {
@@ -8173,19 +8220,23 @@ class BinaryBridge {
8173
8220
  env
8174
8221
  });
8175
8222
  const currentChild = child;
8223
+ const stdoutDecoder = new StringDecoder("utf8");
8176
8224
  child.stdout?.on("data", (chunk) => {
8177
- this.onStdoutData(chunk.toString("utf-8"));
8225
+ this.onStdoutData(stdoutDecoder.write(chunk));
8226
+ });
8227
+ child.stdout?.on("end", () => {
8228
+ const remaining = stdoutDecoder.end();
8229
+ if (remaining)
8230
+ this.onStdoutData(remaining);
8178
8231
  });
8232
+ const stderrDecoder = new StringDecoder("utf8");
8179
8233
  child.stderr?.on("data", (chunk) => {
8180
- const lines = chunk.toString("utf-8").trimEnd().split(`
8181
- `);
8182
- for (const line of lines) {
8183
- if (!line)
8184
- continue;
8185
- const tagged = tagStderrLine(line);
8186
- log(tagged);
8187
- this.pushStderrLine(tagged);
8188
- }
8234
+ this.onStderrData(stderrDecoder.write(chunk));
8235
+ });
8236
+ child.stderr?.on("end", () => {
8237
+ const remaining = stderrDecoder.end();
8238
+ if (remaining)
8239
+ this.onStderrData(remaining);
8189
8240
  });
8190
8241
  child.on("error", (err) => {
8191
8242
  if (this.process !== currentChild)
@@ -8218,6 +8269,17 @@ class BinaryBridge {
8218
8269
  this.stderrTail.shift();
8219
8270
  }
8220
8271
  }
8272
+ onStderrData(data) {
8273
+ const lines = data.trimEnd().split(`
8274
+ `);
8275
+ for (const line of lines) {
8276
+ if (!line)
8277
+ continue;
8278
+ const tagged = tagStderrLine(line);
8279
+ log(tagged);
8280
+ this.pushStderrLine(tagged);
8281
+ }
8282
+ }
8221
8283
  formatStderrTail() {
8222
8284
  if (this.stderrTail.length === 0)
8223
8285
  return "";
@@ -8297,6 +8359,7 @@ class BinaryBridge {
8297
8359
  }
8298
8360
  }
8299
8361
  handleTimeout(triggeringSessionId) {
8362
+ this.rejectAllPending(new Error(`${this.errorPrefix} bridge killed during sibling timeout \u2014 request aborted`));
8300
8363
  if (this.process) {
8301
8364
  this.process.kill("SIGKILL");
8302
8365
  this.process = null;
@@ -8625,23 +8688,23 @@ async function ensureOnnxRuntime(storageDir) {
8625
8688
  const onnxBaseDir = join3(storageDir, "onnxruntime");
8626
8689
  mkdirSync2(onnxBaseDir, { recursive: true });
8627
8690
  const lockPath = join3(onnxBaseDir, ONNX_LOCK_FILE);
8628
- cleanupAbandonedOnnxAttempts(onnxBaseDir, ortDir);
8691
+ cleanupAbandonedStagingDirs(onnxBaseDir);
8629
8692
  if (!acquireLock(lockPath)) {
8630
8693
  warn(`ONNX Runtime install already in progress in another process (lock: ${lockPath}). Skipping.`);
8631
8694
  return null;
8632
8695
  }
8633
8696
  try {
8697
+ cleanupIncompleteTargetIfUnowned(ortDir);
8634
8698
  return await downloadOnnxRuntime(info, ortDir);
8635
8699
  } finally {
8636
8700
  releaseLock(lockPath);
8637
8701
  }
8638
8702
  }
8639
- function cleanupAbandonedOnnxAttempts(onnxBaseDir, ortDir) {
8703
+ function cleanupAbandonedStagingDirs(onnxBaseDir) {
8640
8704
  try {
8641
8705
  const entries = readdirSync(onnxBaseDir);
8642
- const ortDirBaseName = ortDir.slice(onnxBaseDir.length + 1);
8643
8706
  for (const entry of entries) {
8644
- if (!entry.startsWith(`${ortDirBaseName}.tmp.`))
8707
+ if (!entry.startsWith(`${ORT_VERSION}.tmp.`))
8645
8708
  continue;
8646
8709
  const stagingDir = join3(onnxBaseDir, entry);
8647
8710
  const parts = entry.split(".");
@@ -8672,6 +8735,8 @@ function cleanupAbandonedOnnxAttempts(onnxBaseDir, ortDir) {
8672
8735
  }
8673
8736
  }
8674
8737
  } catch {}
8738
+ }
8739
+ function cleanupIncompleteTargetIfUnowned(ortDir) {
8675
8740
  try {
8676
8741
  if (existsSync2(ortDir) && !existsSync2(join3(ortDir, ONNX_INSTALLED_META_FILE))) {
8677
8742
  log(`[onnx] removing half-populated install dir ${ortDir} (no meta file)`);
@@ -8862,25 +8927,7 @@ async function downloadOnnxRuntime(info, targetDir) {
8862
8927
  realFiles.push(libFile);
8863
8928
  }
8864
8929
  }
8865
- for (const libFile of realFiles) {
8866
- const src = join3(extractedDir, libFile);
8867
- const dst = join3(targetDir, libFile);
8868
- try {
8869
- copyFileSync(src, dst);
8870
- if (process.platform !== "win32") {
8871
- chmodSync2(dst, 493);
8872
- }
8873
- } catch (copyErr) {
8874
- log(`ORT extract: failed to copy ${libFile}: ${copyErr}`);
8875
- }
8876
- }
8877
- for (const link of symlinks) {
8878
- const dst = join3(targetDir, link.name);
8879
- try {
8880
- unlinkSync2(dst);
8881
- } catch {}
8882
- symlinkSync(link.target, dst);
8883
- }
8930
+ copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks);
8884
8931
  const libPath = join3(targetDir, info.libName);
8885
8932
  let libHash = null;
8886
8933
  try {
@@ -8903,6 +8950,45 @@ async function downloadOnnxRuntime(info, targetDir) {
8903
8950
  return null;
8904
8951
  }
8905
8952
  }
8953
+ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, copyFile = copyFileSync) {
8954
+ const requiredLibs = new Set([info.libName]);
8955
+ for (const libFile of realFiles) {
8956
+ const src = join3(extractedDir, libFile);
8957
+ const dst = join3(targetDir, libFile);
8958
+ try {
8959
+ copyFile(src, dst);
8960
+ if (process.platform !== "win32") {
8961
+ chmodSync2(dst, 493);
8962
+ }
8963
+ } catch (copyErr) {
8964
+ if (requiredLibs.has(libFile)) {
8965
+ rmSync(targetDir, { recursive: true, force: true });
8966
+ throw copyErr;
8967
+ }
8968
+ log(`ORT extract: failed to copy optional ${libFile}: ${copyErr}`);
8969
+ }
8970
+ }
8971
+ for (const link of symlinks) {
8972
+ const dst = join3(targetDir, link.name);
8973
+ try {
8974
+ unlinkSync2(dst);
8975
+ } catch {}
8976
+ try {
8977
+ symlinkSync(link.target, dst);
8978
+ } catch (symlinkErr) {
8979
+ if (requiredLibs.has(link.name)) {
8980
+ rmSync(targetDir, { recursive: true, force: true });
8981
+ throw symlinkErr;
8982
+ }
8983
+ log(`ORT extract: failed to symlink optional ${link.name}: ${symlinkErr}`);
8984
+ }
8985
+ }
8986
+ const requiredPath = join3(targetDir, info.libName);
8987
+ if (!existsSync2(requiredPath)) {
8988
+ rmSync(targetDir, { recursive: true, force: true });
8989
+ throw new Error(`Required ONNX Runtime library missing after install: ${requiredPath}`);
8990
+ }
8991
+ }
8906
8992
  async function extractZipArchive(archivePath, destinationDir) {
8907
8993
  if (process.platform === "win32") {
8908
8994
  execFileSync("tar.exe", ["-xf", archivePath, "-C", destinationDir], {
@@ -9086,11 +9172,13 @@ class BridgePool {
9086
9172
  idleTimeoutMs;
9087
9173
  bridgeOptions;
9088
9174
  configOverrides;
9175
+ logger;
9089
9176
  cleanupTimer = null;
9090
9177
  constructor(binaryPath, options = {}, configOverrides = {}) {
9091
9178
  this.binaryPath = binaryPath;
9092
9179
  this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
9093
9180
  this.idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
9181
+ this.logger = options.logger;
9094
9182
  this.bridgeOptions = {
9095
9183
  timeoutMs: options.timeoutMs,
9096
9184
  maxRestarts: options.maxRestarts,
@@ -9134,8 +9222,10 @@ class BridgePool {
9134
9222
  cleanup() {
9135
9223
  const now = Date.now();
9136
9224
  for (const [dir, entry] of this.bridges) {
9225
+ if (entry.bridge.hasPendingRequests())
9226
+ continue;
9137
9227
  if (now - entry.lastUsed > this.idleTimeoutMs) {
9138
- entry.bridge.shutdown().catch((err) => error("cleanup shutdown failed:", err));
9228
+ entry.bridge.shutdown().catch((err) => this.error("cleanup shutdown failed:", err));
9139
9229
  this.bridges.delete(dir);
9140
9230
  }
9141
9231
  }
@@ -9144,6 +9234,8 @@ class BridgePool {
9144
9234
  let oldestDir = null;
9145
9235
  let oldestTime = Infinity;
9146
9236
  for (const [dir, entry] of this.bridges) {
9237
+ if (entry.bridge.hasPendingRequests())
9238
+ continue;
9147
9239
  if (entry.lastUsed < oldestTime) {
9148
9240
  oldestTime = entry.lastUsed;
9149
9241
  oldestDir = dir;
@@ -9151,7 +9243,7 @@ class BridgePool {
9151
9243
  }
9152
9244
  if (oldestDir) {
9153
9245
  const entry = this.bridges.get(oldestDir);
9154
- entry?.bridge.shutdown().catch((err) => error("eviction shutdown failed:", err));
9246
+ entry?.bridge.shutdown().catch((err) => this.error("eviction shutdown failed:", err));
9155
9247
  this.bridges.delete(oldestDir);
9156
9248
  }
9157
9249
  }
@@ -9169,7 +9261,21 @@ class BridgePool {
9169
9261
  const shutdowns = Array.from(this.bridges.values()).map((entry) => entry.bridge.shutdown());
9170
9262
  this.bridges.clear();
9171
9263
  await Promise.allSettled(shutdowns);
9172
- log(`Binary path updated to ${newPath}. All bridges cleared \u2014 next calls will use the new binary.`);
9264
+ this.log(`Binary path updated to ${newPath}. All bridges cleared \u2014 next calls will use the new binary.`);
9265
+ }
9266
+ log(message, meta) {
9267
+ const logger = this.logger ?? getActiveLogger();
9268
+ if (logger)
9269
+ logger.log(message, meta);
9270
+ else
9271
+ log(message, meta);
9272
+ }
9273
+ error(message, meta) {
9274
+ const logger = this.logger ?? getActiveLogger();
9275
+ if (logger)
9276
+ logger.error(message, meta);
9277
+ else
9278
+ error(message, meta);
9173
9279
  }
9174
9280
  setConfigureOverride(key, value) {
9175
9281
  if (value === undefined) {
@@ -9289,7 +9395,7 @@ async function findBinary(expectedVersion) {
9289
9395
  return syncResult;
9290
9396
  }
9291
9397
  log("Binary not found locally, attempting auto-download...");
9292
- const downloaded = await ensureBinary();
9398
+ const downloaded = await ensureBinary(expectedVersion);
9293
9399
  if (downloaded)
9294
9400
  return downloaded;
9295
9401
  throw new Error([
@@ -9853,16 +9959,6 @@ async function resolvePromptContext(client, sessionId) {
9853
9959
  return null;
9854
9960
  return result;
9855
9961
  }
9856
- async function getLastAssistantModel(client, sessionId) {
9857
- const ctx = await resolvePromptContext(client, sessionId);
9858
- if (!ctx?.model)
9859
- return null;
9860
- return {
9861
- providerID: ctx.model.providerID,
9862
- modelID: ctx.model.modelID,
9863
- ...ctx.variant ? { variant: ctx.variant } : {}
9864
- };
9865
- }
9866
9962
 
9867
9963
  // src/bg-notifications.ts
9868
9964
  var sessionBgStates = new Map;
@@ -9980,9 +10076,6 @@ async function triggerWakeIfPending(drainContext, skipDrain) {
9980
10076
  sessionWarn2(drainContext.sessionID, `${LOG_PREFIX} wake send failed: ${err instanceof Error ? err.message : String(err)}`);
9981
10077
  });
9982
10078
  }
9983
- function resetBgWake(sessionID) {
9984
- stateFor(sessionID).wakeFiredThisIdle = false;
9985
- }
9986
10079
  function formatSystemReminder(completions) {
9987
10080
  const bullets = completions.map((completion) => formatCompletion(completion)).join(`
9988
10081
  `);
@@ -10074,7 +10167,6 @@ function scheduleWake(state, sendWake, onSendFailure) {
10074
10167
  state.pendingLongRunning = [];
10075
10168
  sendWake(reminder).then(() => {
10076
10169
  state.retryDelayMs = null;
10077
- state.wakeFiredThisIdle = true;
10078
10170
  }).catch((err) => {
10079
10171
  state.pendingCompletions = [...pending, ...state.pendingCompletions];
10080
10172
  state.pendingLongRunning = [...pendingLongRunning, ...state.pendingLongRunning];
@@ -10096,7 +10188,6 @@ function stateFor(sessionID) {
10096
10188
  pendingCompletions: [],
10097
10189
  pendingLongRunning: [],
10098
10190
  debounceTimer: null,
10099
- wakeFiredThisIdle: false,
10100
10191
  firstCompletionAt: null,
10101
10192
  scheduledFireAt: null,
10102
10193
  scheduledCompletionCount: 0,
@@ -21143,7 +21234,7 @@ function finalize(ctx, schema) {
21143
21234
  result.$schema = "http://json-schema.org/draft-07/schema#";
21144
21235
  } else if (ctx.target === "draft-04") {
21145
21236
  result.$schema = "http://json-schema.org/draft-04/schema#";
21146
- } else if (ctx.target === "openapi-3.0") {} else {}
21237
+ } else if (ctx.target === "openapi-3.0") {}
21147
21238
  if (ctx.external?.uri) {
21148
21239
  const id = ctx.external.registry.get(schema)?.id;
21149
21240
  if (!id)
@@ -21391,7 +21482,7 @@ var literalProcessor = (schema, ctx, json, _params) => {
21391
21482
  if (val === undefined) {
21392
21483
  if (ctx.unrepresentable === "throw") {
21393
21484
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
21394
- } else {}
21485
+ }
21395
21486
  } else if (typeof val === "bigint") {
21396
21487
  if (ctx.unrepresentable === "throw") {
21397
21488
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -24220,7 +24311,8 @@ import { dirname as dirname4, join as join11 } from "path";
24220
24311
  // src/hooks/auto-update-checker/cache.ts
24221
24312
  var import_comment_json3 = __toESM(require_src2(), 1);
24222
24313
  import { spawn as spawn2 } from "child_process";
24223
- import { existsSync as existsSync7, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
24314
+ import { cpSync, existsSync as existsSync7, mkdtempSync, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
24315
+ import { tmpdir as tmpdir2 } from "os";
24224
24316
  import { basename, dirname as dirname3, join as join10 } from "path";
24225
24317
 
24226
24318
  // src/hooks/auto-update-checker/checker.ts
@@ -24471,6 +24563,42 @@ async function getLatestVersion(channel = "latest", options = {}) {
24471
24563
  }
24472
24564
 
24473
24565
  // src/hooks/auto-update-checker/cache.ts
24566
+ var pendingSnapshots = new Map;
24567
+ function createAutoUpdateSnapshot(installDir, packageJsonPath, packageName) {
24568
+ const packageDir = join10(installDir, "node_modules", packageName);
24569
+ const lockfilePath = join10(installDir, "package-lock.json");
24570
+ const tempDir = mkdtempSync(join10(tmpdir2(), "aft-auto-update-"));
24571
+ const stagedPackageDir = existsSync7(packageDir) ? join10(tempDir, "package") : null;
24572
+ if (stagedPackageDir)
24573
+ cpSync(packageDir, stagedPackageDir, { recursive: true });
24574
+ return {
24575
+ packageJsonPath,
24576
+ packageJson: existsSync7(packageJsonPath) ? readFileSync5(packageJsonPath, "utf-8") : null,
24577
+ lockfilePath,
24578
+ lockfile: existsSync7(lockfilePath) ? readFileSync5(lockfilePath, "utf-8") : null,
24579
+ packageDir,
24580
+ stagedPackageDir,
24581
+ tempDir
24582
+ };
24583
+ }
24584
+ function restoreAutoUpdateSnapshot(snapshot) {
24585
+ try {
24586
+ if (snapshot.packageJson === null)
24587
+ rmSync2(snapshot.packageJsonPath, { force: true });
24588
+ else
24589
+ writeFileSync5(snapshot.packageJsonPath, snapshot.packageJson);
24590
+ if (snapshot.lockfile === null)
24591
+ rmSync2(snapshot.lockfilePath, { force: true });
24592
+ else
24593
+ writeFileSync5(snapshot.lockfilePath, snapshot.lockfile);
24594
+ rmSync2(snapshot.packageDir, { recursive: true, force: true });
24595
+ if (snapshot.stagedPackageDir) {
24596
+ cpSync(snapshot.stagedPackageDir, snapshot.packageDir, { recursive: true });
24597
+ }
24598
+ } finally {
24599
+ rmSync2(snapshot.tempDir, { recursive: true, force: true });
24600
+ }
24601
+ }
24474
24602
  function stripPackageNameFromPath(pathValue, packageName) {
24475
24603
  let current = pathValue;
24476
24604
  for (const segment of [...packageName.split("/")].reverse()) {
@@ -24562,8 +24690,13 @@ function preparePackageUpdate(version2, packageName = PACKAGE_NAME, runtimePacka
24562
24690
  warn2("[auto-update-checker] No install context found for auto-update");
24563
24691
  return null;
24564
24692
  }
24565
- if (!ensureDependencyVersion(installContext.packageJsonPath, packageName, version2))
24693
+ const snapshot = createAutoUpdateSnapshot(installContext.installDir, installContext.packageJsonPath, packageName);
24694
+ pendingSnapshots.set(installContext.installDir, snapshot);
24695
+ if (!ensureDependencyVersion(installContext.packageJsonPath, packageName, version2)) {
24696
+ pendingSnapshots.delete(installContext.installDir);
24697
+ restoreAutoUpdateSnapshot(snapshot);
24566
24698
  return null;
24699
+ }
24567
24700
  const packageRemoved = removeInstalledPackage(installContext.installDir, packageName);
24568
24701
  const lockRemoved = removeFromPackageLock(installContext.installDir, packageName);
24569
24702
  if (!packageRemoved && !lockRemoved) {
@@ -24582,7 +24715,7 @@ async function runNpmInstallSafe(installDir, options = {}) {
24582
24715
  return false;
24583
24716
  const proc = spawn2("npm", ["install", "--no-audit", "--no-fund", "--no-progress"], {
24584
24717
  cwd: installDir,
24585
- stdio: "pipe"
24718
+ stdio: "ignore"
24586
24719
  });
24587
24720
  const abortProcess = () => {
24588
24721
  try {
@@ -24601,10 +24734,27 @@ async function runNpmInstallSafe(installDir, options = {}) {
24601
24734
  options.signal?.removeEventListener("abort", abortProcess);
24602
24735
  if (result === "timeout" || options.signal?.aborted) {
24603
24736
  abortProcess();
24737
+ const snapshot2 = pendingSnapshots.get(installDir);
24738
+ if (snapshot2) {
24739
+ pendingSnapshots.delete(installDir);
24740
+ restoreAutoUpdateSnapshot(snapshot2);
24741
+ }
24604
24742
  return false;
24605
24743
  }
24744
+ const snapshot = pendingSnapshots.get(installDir);
24745
+ pendingSnapshots.delete(installDir);
24746
+ if (!result && snapshot) {
24747
+ restoreAutoUpdateSnapshot(snapshot);
24748
+ } else if (snapshot) {
24749
+ rmSync2(snapshot.tempDir, { recursive: true, force: true });
24750
+ }
24606
24751
  return result;
24607
24752
  } catch (err) {
24753
+ const snapshot = pendingSnapshots.get(installDir);
24754
+ if (snapshot) {
24755
+ pendingSnapshots.delete(installDir);
24756
+ restoreAutoUpdateSnapshot(snapshot);
24757
+ }
24608
24758
  warn2(`[auto-update-checker] npm install error: ${String(err)}`);
24609
24759
  return false;
24610
24760
  } finally {
@@ -24768,7 +24918,8 @@ function showToast(ctx, title, message, variant = "info", duration3 = 3000) {
24768
24918
  // src/lsp-auto-install.ts
24769
24919
  import { spawn as spawn3 } from "child_process";
24770
24920
  import { createHash as createHash3 } from "crypto";
24771
- import { createReadStream, statSync as statSync4 } from "fs";
24921
+ import { createReadStream, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync4, rmSync as rmSync3, statSync as statSync4 } from "fs";
24922
+ import { join as join14 } from "path";
24772
24923
 
24773
24924
  // src/lsp-cache.ts
24774
24925
  import {
@@ -25452,6 +25603,50 @@ function hashInstalledBinary(spec) {
25452
25603
  stream.on("end", () => resolve3(hash2.digest("hex")));
25453
25604
  });
25454
25605
  }
25606
+ function installedBinaryPath(spec) {
25607
+ const candidates = process.platform === "win32" ? [
25608
+ lspBinaryPath(spec.npm, spec.binary),
25609
+ lspBinaryPath(spec.npm, `${spec.binary}.cmd`),
25610
+ lspBinaryPath(spec.npm, `${spec.binary}.exe`),
25611
+ lspBinaryPath(spec.npm, `${spec.binary}.bat`)
25612
+ ] : [lspBinaryPath(spec.npm, spec.binary)];
25613
+ for (const candidate of candidates) {
25614
+ try {
25615
+ if (statSync4(candidate).isFile())
25616
+ return candidate;
25617
+ } catch {}
25618
+ }
25619
+ return null;
25620
+ }
25621
+ function sha256OfFileSync(path2) {
25622
+ return createHash3("sha256").update(readFileSync8(path2)).digest("hex");
25623
+ }
25624
+ function quarantineCachedNpmInstall(spec, reason) {
25625
+ const packageDir = lspPackageDir(spec.npm);
25626
+ const dest = join14(packageDir, "..", ".quarantine", encodeURIComponent(spec.npm), `${Date.now()}`);
25627
+ warn2(`[lsp] tofu_mismatch ${spec.npm}: ${reason}; quarantining ${packageDir} -> ${dest}`);
25628
+ try {
25629
+ mkdirSync7(join14(dest, ".."), { recursive: true });
25630
+ rmSync3(dest, { recursive: true, force: true });
25631
+ renameSync4(packageDir, dest);
25632
+ } catch (err) {
25633
+ warn2(`[lsp] tofu_mismatch ${spec.npm}: failed to quarantine cache entry: ${err}`);
25634
+ }
25635
+ }
25636
+ function validateCachedNpmInstall(spec) {
25637
+ const meta3 = readInstalledMeta(spec.npm);
25638
+ const binaryPath = installedBinaryPath(spec);
25639
+ if (!meta3?.sha256 || !meta3.version || !isSafeVersion(meta3.version) || !binaryPath) {
25640
+ quarantineCachedNpmInstall(spec, "missing/unsafe metadata or binary");
25641
+ return false;
25642
+ }
25643
+ const currentHash = sha256OfFileSync(binaryPath);
25644
+ if (currentHash !== meta3.sha256) {
25645
+ quarantineCachedNpmInstall(spec, `recorded ${meta3.sha256}, current ${currentHash}`);
25646
+ return false;
25647
+ }
25648
+ return true;
25649
+ }
25455
25650
  function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
25456
25651
  const cachedBinDirs = [];
25457
25652
  const skipped = [];
@@ -25464,7 +25659,7 @@ function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
25464
25659
  return projectExtensions;
25465
25660
  };
25466
25661
  for (const spec of NPM_LSP_TABLE) {
25467
- if (isInstalled(spec.npm, spec.binary)) {
25662
+ if (isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)) {
25468
25663
  cachedBinDirs.push(lspBinDir(spec.npm));
25469
25664
  }
25470
25665
  if (config2.disabled.has(spec.id)) {
@@ -25516,16 +25711,17 @@ import {
25516
25711
  createWriteStream as createWriteStream2,
25517
25712
  existsSync as existsSync10,
25518
25713
  lstatSync as lstatSync2,
25519
- mkdirSync as mkdirSync7,
25714
+ mkdirSync as mkdirSync8,
25520
25715
  readdirSync as readdirSync4,
25716
+ readFileSync as readFileSync9,
25521
25717
  readlinkSync as readlinkSync2,
25522
25718
  realpathSync as realpathSync3,
25523
- renameSync as renameSync4,
25524
- rmSync as rmSync3,
25719
+ renameSync as renameSync5,
25720
+ rmSync as rmSync4,
25525
25721
  statSync as statSync5,
25526
25722
  unlinkSync as unlinkSync6
25527
25723
  } from "fs";
25528
- import { dirname as dirname5, join as join14, relative as relative2, resolve as resolve3 } from "path";
25724
+ import { dirname as dirname5, join as join15, relative as relative2, resolve as resolve3 } from "path";
25529
25725
  import { Readable as Readable2 } from "stream";
25530
25726
  import { pipeline as pipeline2 } from "stream/promises";
25531
25727
 
@@ -25615,25 +25811,25 @@ function detectHostPlatform() {
25615
25811
 
25616
25812
  // src/lsp-github-install.ts
25617
25813
  function ghCacheRoot() {
25618
- return join14(aftCacheBase(), "lsp-binaries");
25814
+ return join15(aftCacheBase(), "lsp-binaries");
25619
25815
  }
25620
25816
  function ghPackageDir(spec) {
25621
- return join14(ghCacheRoot(), spec.id);
25817
+ return join15(ghCacheRoot(), spec.id);
25622
25818
  }
25623
25819
  function ghBinDir(spec) {
25624
- return join14(ghPackageDir(spec), "bin");
25820
+ return join15(ghPackageDir(spec), "bin");
25625
25821
  }
25626
25822
  function ghExtractDir(spec) {
25627
- return join14(ghPackageDir(spec), "extracted");
25823
+ return join15(ghPackageDir(spec), "extracted");
25628
25824
  }
25629
25825
  function ghBinaryPath(spec, platform2) {
25630
25826
  const ext = platform2 === "win32" ? ".exe" : "";
25631
- return join14(ghBinDir(spec), `${spec.binary}${ext}`);
25827
+ return join15(ghBinDir(spec), `${spec.binary}${ext}`);
25632
25828
  }
25633
25829
  function isGithubInstalled(spec, platform2) {
25634
25830
  for (const candidate of ghBinaryCandidates(spec, platform2)) {
25635
25831
  try {
25636
- if (statSync5(join14(ghBinDir(spec), candidate)).isFile())
25832
+ if (statSync5(join15(ghBinDir(spec), candidate)).isFile())
25637
25833
  return true;
25638
25834
  } catch {}
25639
25835
  }
@@ -25655,6 +25851,9 @@ function sha256OfFile(path2) {
25655
25851
  stream.on("end", () => resolve4(hash2.digest("hex")));
25656
25852
  });
25657
25853
  }
25854
+ function sha256OfFileSync2(path2) {
25855
+ return createHash4("sha256").update(readFileSync9(path2)).digest("hex");
25856
+ }
25658
25857
  async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
25659
25858
  const candidates = [];
25660
25859
  candidates.push(tag);
@@ -25675,11 +25874,7 @@ async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
25675
25874
  const url2 = `https://api.github.com/repos/${githubRepo}/releases/tags/${encodeURIComponent(candidate)}`;
25676
25875
  const timeout = controlledTimeoutSignal(15000, signal);
25677
25876
  try {
25678
- const res = await fetchImpl(url2, {
25679
- headers,
25680
- redirect: "follow",
25681
- signal: timeout.signal
25682
- });
25877
+ const res = await fetchJsonFollowingRedirects(url2, headers, fetchImpl, timeout.signal);
25683
25878
  if (res.status === 404)
25684
25879
  continue;
25685
25880
  if (!res.ok) {
@@ -25709,6 +25904,23 @@ async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
25709
25904
  }
25710
25905
  return null;
25711
25906
  }
25907
+ async function fetchJsonFollowingRedirects(url2, headers, fetchImpl, signal) {
25908
+ const maxRedirects = 5;
25909
+ let currentUrl = url2;
25910
+ for (let i = 0;i <= maxRedirects; i += 1) {
25911
+ assertAllowedDownloadUrl(currentUrl);
25912
+ const res = await fetchImpl(currentUrl, { headers, redirect: "manual", signal });
25913
+ if (res.status >= 300 && res.status < 400) {
25914
+ const location = res.headers.get("location");
25915
+ if (!location)
25916
+ throw new Error(`redirect status ${res.status} without Location`);
25917
+ currentUrl = new URL(location, currentUrl).toString();
25918
+ continue;
25919
+ }
25920
+ return res;
25921
+ }
25922
+ throw new Error(`too many redirects (>${maxRedirects})`);
25923
+ }
25712
25924
  async function resolveTargetTag(spec, config2, fetchImpl, signal) {
25713
25925
  const pinned = config2.versions[spec.githubRepo];
25714
25926
  if (pinned) {
@@ -25803,17 +26015,12 @@ function assertAllowedDownloadUrl(rawUrl) {
25803
26015
  return parsed;
25804
26016
  }
25805
26017
  async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
25806
- assertAllowedDownloadUrl(url2);
25807
26018
  if (assetSize !== undefined && assetSize > MAX_DOWNLOAD_BYTES2) {
25808
26019
  throw new Error(`asset size ${assetSize} exceeds max ${MAX_DOWNLOAD_BYTES2} (set lsp.versions to pin a smaller release if this is wrong)`);
25809
26020
  }
25810
26021
  const timeout = controlledTimeoutSignal(120000, signal);
25811
26022
  try {
25812
- const res = await fetchImpl(url2, {
25813
- headers: { accept: "application/octet-stream" },
25814
- redirect: "follow",
25815
- signal: timeout.signal
25816
- });
26023
+ const res = await fetchFollowingRedirects(url2, fetchImpl, timeout.signal);
25817
26024
  if (!res.ok || !res.body) {
25818
26025
  throw new Error(`download failed (${res.status})`);
25819
26026
  }
@@ -25821,7 +26028,7 @@ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
25821
26028
  if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES2) {
25822
26029
  throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES2}`);
25823
26030
  }
25824
- mkdirSync7(dirname5(destPath), { recursive: true });
26031
+ mkdirSync8(dirname5(destPath), { recursive: true });
25825
26032
  let bytesWritten = 0;
25826
26033
  const guard = new TransformStream({
25827
26034
  transform(chunk, controller) {
@@ -25845,6 +26052,27 @@ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
25845
26052
  timeout.cleanup();
25846
26053
  }
25847
26054
  }
26055
+ async function fetchFollowingRedirects(url2, fetchImpl, signal) {
26056
+ const maxRedirects = 5;
26057
+ let currentUrl = url2;
26058
+ for (let i = 0;i <= maxRedirects; i += 1) {
26059
+ assertAllowedDownloadUrl(currentUrl);
26060
+ const res = await fetchImpl(currentUrl, {
26061
+ headers: { accept: "application/octet-stream" },
26062
+ redirect: "manual",
26063
+ signal
26064
+ });
26065
+ if (res.status >= 300 && res.status < 400) {
26066
+ const location = res.headers.get("location");
26067
+ if (!location)
26068
+ throw new Error(`redirect status ${res.status} without Location`);
26069
+ currentUrl = new URL(location, currentUrl).toString();
26070
+ continue;
26071
+ }
26072
+ return res;
26073
+ }
26074
+ throw new Error(`too many redirects (>${maxRedirects})`);
26075
+ }
25848
26076
  function validateExtraction(stagingRoot) {
25849
26077
  const realStagingRoot = realpathSync3(stagingRoot);
25850
26078
  let totalBytes = 0;
@@ -25856,7 +26084,7 @@ function validateExtraction(stagingRoot) {
25856
26084
  throw new Error(`failed to read staging dir ${dir}: ${err}`);
25857
26085
  }
25858
26086
  for (const entry of entries) {
25859
- const full = join14(dir, entry);
26087
+ const full = join15(dir, entry);
25860
26088
  let lst;
25861
26089
  try {
25862
26090
  lst = lstatSync2(full);
@@ -25894,27 +26122,83 @@ function validateExtraction(stagingRoot) {
25894
26122
  };
25895
26123
  walk(realStagingRoot);
25896
26124
  }
26125
+ function precheckArchiveSize(archivePath, archiveType) {
26126
+ let totalBytes = 0;
26127
+ if (archiveType === "zip") {
26128
+ const out = execFileSync2("unzip", ["-l", archivePath], { encoding: "utf8" });
26129
+ const match = out.match(/^\s*(\d+)\s+\d+\s+files?\s*$/m);
26130
+ if (match)
26131
+ totalBytes = Number.parseInt(match[1] ?? "0", 10);
26132
+ } else {
26133
+ const out = execFileSync2("tar", ["-tvf", archivePath], { encoding: "utf8" });
26134
+ for (const line of out.split(`
26135
+ `)) {
26136
+ const parts = line.trim().split(/\s+/);
26137
+ if (parts.length >= 6) {
26138
+ const numeric = parts.map((part) => Number.parseInt(part, 10)).filter((value) => Number.isFinite(value) && value >= 0);
26139
+ if (numeric.length > 0)
26140
+ totalBytes += Math.max(...numeric);
26141
+ }
26142
+ }
26143
+ }
26144
+ if (totalBytes > MAX_EXTRACT_BYTES2) {
26145
+ throw new Error(`archive uncompressed size ${totalBytes} exceeds ${MAX_EXTRACT_BYTES2}`);
26146
+ }
26147
+ }
25897
26148
  function extractArchiveSafely(archivePath, destDir, archiveType) {
25898
26149
  const suffix = randomBytes(8).toString("hex");
25899
26150
  const stagingDir = `${destDir}.staging-${suffix}`;
25900
26151
  try {
25901
- rmSync3(stagingDir, { recursive: true, force: true });
26152
+ rmSync4(stagingDir, { recursive: true, force: true });
25902
26153
  } catch {}
25903
- mkdirSync7(stagingDir, { recursive: true });
26154
+ mkdirSync8(stagingDir, { recursive: true });
25904
26155
  try {
26156
+ precheckArchiveSize(archivePath, archiveType);
25905
26157
  runPlatformExtractor(archivePath, stagingDir, archiveType);
25906
26158
  validateExtraction(stagingDir);
25907
26159
  try {
25908
- rmSync3(destDir, { recursive: true, force: true });
26160
+ rmSync4(destDir, { recursive: true, force: true });
25909
26161
  } catch {}
25910
- renameSync4(stagingDir, destDir);
26162
+ renameSync5(stagingDir, destDir);
25911
26163
  } catch (err) {
25912
26164
  try {
25913
- rmSync3(stagingDir, { recursive: true, force: true });
26165
+ rmSync4(stagingDir, { recursive: true, force: true });
25914
26166
  } catch {}
25915
26167
  throw err;
25916
26168
  }
25917
26169
  }
26170
+ function quarantineCachedGithubInstall(spec, reason) {
26171
+ const packageDir = ghPackageDir(spec);
26172
+ const dest = join15(ghCacheRoot(), ".quarantine", encodeURIComponent(spec.id), `${Date.now()}`);
26173
+ warn2(`[lsp] tofu_mismatch ${spec.id}: ${reason}; quarantining ${packageDir} -> ${dest}`);
26174
+ try {
26175
+ mkdirSync8(dirname5(dest), { recursive: true });
26176
+ rmSync4(dest, { recursive: true, force: true });
26177
+ renameSync5(packageDir, dest);
26178
+ } catch (err) {
26179
+ warn2(`[lsp] tofu_mismatch ${spec.id}: failed to quarantine cache entry: ${err}`);
26180
+ }
26181
+ }
26182
+ function validateCachedGithubInstall(spec, platform2) {
26183
+ const meta3 = readInstalledMetaIn(ghPackageDir(spec));
26184
+ const binaryPath = ghBinaryCandidates(spec, platform2).map((candidate) => join15(ghBinDir(spec), candidate)).find((candidate) => {
26185
+ try {
26186
+ return statSync5(candidate).isFile();
26187
+ } catch {
26188
+ return false;
26189
+ }
26190
+ });
26191
+ if (!meta3?.sha256 || !meta3.version || !isSafeVersion(meta3.version) || !binaryPath) {
26192
+ quarantineCachedGithubInstall(spec, "missing/unsafe metadata or binary");
26193
+ return false;
26194
+ }
26195
+ const currentHash = sha256OfFileSync2(binaryPath);
26196
+ if (currentHash !== meta3.sha256) {
26197
+ quarantineCachedGithubInstall(spec, `recorded ${meta3.sha256}, current ${currentHash}`);
26198
+ return false;
26199
+ }
26200
+ return true;
26201
+ }
25918
26202
  function runPlatformExtractor(archivePath, destDir, archiveType) {
25919
26203
  if (archiveType === "zip") {
25920
26204
  if (process.platform === "win32") {
@@ -25960,7 +26244,7 @@ async function downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl,
25960
26244
  }
25961
26245
  const pkgDir = ghPackageDir(spec);
25962
26246
  const extractDir = ghExtractDir(spec);
25963
- const archivePath = join14(pkgDir, expected.name);
26247
+ const archivePath = join15(pkgDir, expected.name);
25964
26248
  log2(`[lsp] downloading ${spec.id} ${tag} \u2192 ${matchingAsset.url}`);
25965
26249
  try {
25966
26250
  await downloadFile(matchingAsset.url, archivePath, fetchImpl, matchingAsset.size, signal);
@@ -25999,13 +26283,13 @@ async function downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl,
25999
26283
  unlinkSync6(archivePath);
26000
26284
  } catch {}
26001
26285
  }
26002
- const innerBinaryPath = join14(extractDir, spec.binaryPathInArchive(platform2, arch, version2));
26286
+ const innerBinaryPath = join15(extractDir, spec.binaryPathInArchive(platform2, arch, version2));
26003
26287
  if (!existsSync10(innerBinaryPath)) {
26004
26288
  error2(`[lsp] ${spec.id}: extracted binary not found at ${innerBinaryPath}`);
26005
26289
  return null;
26006
26290
  }
26007
26291
  const targetBinary = ghBinaryPath(spec, platform2);
26008
- mkdirSync7(dirname5(targetBinary), { recursive: true });
26292
+ mkdirSync8(dirname5(targetBinary), { recursive: true });
26009
26293
  try {
26010
26294
  copyFileSync3(innerBinaryPath, targetBinary);
26011
26295
  if (platform2 !== "win32") {
@@ -26096,7 +26380,7 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
26096
26380
  };
26097
26381
  }
26098
26382
  for (const spec of GITHUB_LSP_TABLE) {
26099
- if (isGithubInstalled(spec, host.platform)) {
26383
+ if (isGithubInstalled(spec, host.platform) && validateCachedGithubInstall(spec, host.platform)) {
26100
26384
  cachedBinDirs.push(ghBinDir(spec));
26101
26385
  }
26102
26386
  if (config2.disabled.has(spec.id)) {
@@ -26245,9 +26529,9 @@ function normalizeToolMap(tools) {
26245
26529
  }
26246
26530
 
26247
26531
  // src/notifications.ts
26248
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
26532
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync10, renameSync as renameSync6, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
26249
26533
  import { homedir as homedir9, platform as platform2 } from "os";
26250
- import { join as join15 } from "path";
26534
+ import { join as join16 } from "path";
26251
26535
  function isTuiMode() {
26252
26536
  return process.env.OPENCODE_CLIENT === "cli";
26253
26537
  }
@@ -26271,15 +26555,15 @@ function getDesktopStatePath() {
26271
26555
  const os2 = platform2();
26272
26556
  const home = homedir9();
26273
26557
  if (os2 === "darwin") {
26274
- return join15(home, "Library", "Application Support", "ai.opencode.desktop", "opencode.global.dat");
26558
+ return join16(home, "Library", "Application Support", "ai.opencode.desktop", "opencode.global.dat");
26275
26559
  }
26276
26560
  if (os2 === "linux") {
26277
- const xdgConfig = process.env.XDG_CONFIG_HOME || join15(home, ".config");
26278
- return join15(xdgConfig, "ai.opencode.desktop", "opencode.global.dat");
26561
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join16(home, ".config");
26562
+ return join16(xdgConfig, "ai.opencode.desktop", "opencode.global.dat");
26279
26563
  }
26280
26564
  if (os2 === "win32") {
26281
- const appData = process.env.APPDATA || join15(home, "AppData", "Roaming");
26282
- return join15(appData, "ai.opencode.desktop", "opencode.global.dat");
26565
+ const appData = process.env.APPDATA || join16(home, "AppData", "Roaming");
26566
+ return join16(appData, "ai.opencode.desktop", "opencode.global.dat");
26283
26567
  }
26284
26568
  return null;
26285
26569
  }
@@ -26289,7 +26573,7 @@ function readDesktopState(directory) {
26289
26573
  return { sessionId: null, serverUrl: null };
26290
26574
  }
26291
26575
  try {
26292
- const raw = readFileSync8(statePath, "utf-8");
26576
+ const raw = readFileSync10(statePath, "utf-8");
26293
26577
  const state = JSON.parse(raw);
26294
26578
  let serverUrl = null;
26295
26579
  const serverStr = state.server;
@@ -26383,10 +26667,10 @@ async function sendWarning(opts, message) {
26383
26667
  }
26384
26668
  async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
26385
26669
  if (storageDir) {
26386
- const versionFile = join15(storageDir, "last_announced_version");
26670
+ const versionFile = join16(storageDir, "last_announced_version");
26387
26671
  try {
26388
26672
  if (existsSync11(versionFile)) {
26389
- const lastVersion = readFileSync8(versionFile, "utf-8").trim();
26673
+ const lastVersion = readFileSync10(versionFile, "utf-8").trim();
26390
26674
  if (lastVersion === version2)
26391
26675
  return;
26392
26676
  }
@@ -26406,17 +26690,17 @@ async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
26406
26690
  }
26407
26691
  if (storageDir) {
26408
26692
  try {
26409
- mkdirSync8(storageDir, { recursive: true });
26410
- writeFileSync8(join15(storageDir, "last_announced_version"), version2);
26693
+ mkdirSync9(storageDir, { recursive: true });
26694
+ writeFileSync8(join16(storageDir, "last_announced_version"), version2);
26411
26695
  } catch {}
26412
26696
  }
26413
26697
  }
26414
26698
  function readWarnedTools(storageDir) {
26415
26699
  try {
26416
- const warnedToolsPath = join15(storageDir, WARNED_TOOLS_FILE);
26700
+ const warnedToolsPath = join16(storageDir, WARNED_TOOLS_FILE);
26417
26701
  if (!existsSync11(warnedToolsPath))
26418
26702
  return {};
26419
- const parsed = JSON.parse(readFileSync8(warnedToolsPath, "utf-8"));
26703
+ const parsed = JSON.parse(readFileSync10(warnedToolsPath, "utf-8"));
26420
26704
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
26421
26705
  return {};
26422
26706
  const warned = {};
@@ -26432,12 +26716,34 @@ function readWarnedTools(storageDir) {
26432
26716
  }
26433
26717
  function writeWarnedTools(storageDir, warned) {
26434
26718
  try {
26435
- mkdirSync8(storageDir, { recursive: true });
26436
- const warnedToolsPath = join15(storageDir, WARNED_TOOLS_FILE);
26437
- writeFileSync8(warnedToolsPath, `${JSON.stringify(warned, null, 2)}
26719
+ mkdirSync9(storageDir, { recursive: true });
26720
+ const warnedToolsPath = join16(storageDir, WARNED_TOOLS_FILE);
26721
+ const tmpPath = join16(storageDir, `${WARNED_TOOLS_FILE}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
26722
+ writeFileSync8(tmpPath, `${JSON.stringify(warned, null, 2)}
26438
26723
  `);
26724
+ renameSync6(tmpPath, warnedToolsPath);
26439
26725
  } catch {}
26440
26726
  }
26727
+ async function withWarnedToolsLock(storageDir, fn) {
26728
+ const lockDir = join16(storageDir, "warned_tools.lock");
26729
+ for (let attempt = 0;attempt < 5; attempt++) {
26730
+ try {
26731
+ mkdirSync9(storageDir, { recursive: true });
26732
+ mkdirSync9(lockDir, { mode: 448 });
26733
+ try {
26734
+ return await fn();
26735
+ } finally {
26736
+ rmSync5(lockDir, { recursive: true, force: true });
26737
+ }
26738
+ } catch (err) {
26739
+ const code = err.code;
26740
+ if (code !== "EEXIST")
26741
+ return null;
26742
+ await new Promise((resolve4) => setTimeout(resolve4, 10 * (attempt + 1)));
26743
+ }
26744
+ }
26745
+ return null;
26746
+ }
26441
26747
  function warningKey(warning, projectRoot) {
26442
26748
  const scope = warning.kind === "lsp_binary_missing" ? "_" : projectRoot ?? "_";
26443
26749
  return [
@@ -26476,20 +26782,29 @@ ${warning.hint}`;
26476
26782
  async function deliverConfigureWarnings(opts, warnings) {
26477
26783
  if (warnings.length === 0)
26478
26784
  return;
26479
- const warned = readWarnedTools(opts.storageDir);
26480
- let changed = false;
26785
+ const deliveredWithLock = await withWarnedToolsLock(opts.storageDir, async () => {
26786
+ const warned = readWarnedTools(opts.storageDir);
26787
+ let changed = false;
26788
+ for (const warning of warnings) {
26789
+ const key = warningKey(warning, opts.projectRoot);
26790
+ if (Object.hasOwn(warned, key))
26791
+ continue;
26792
+ const delivered = await sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning));
26793
+ if (!delivered)
26794
+ continue;
26795
+ warned[key] = opts.pluginVersion;
26796
+ changed = true;
26797
+ }
26798
+ if (changed) {
26799
+ const merged = { ...readWarnedTools(opts.storageDir), ...warned };
26800
+ writeWarnedTools(opts.storageDir, merged);
26801
+ }
26802
+ return true;
26803
+ });
26804
+ if (deliveredWithLock)
26805
+ return;
26481
26806
  for (const warning of warnings) {
26482
- const key = warningKey(warning, opts.projectRoot);
26483
- if (Object.hasOwn(warned, key))
26484
- continue;
26485
- const delivered = await sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning));
26486
- if (!delivered)
26487
- continue;
26488
- warned[key] = opts.pluginVersion;
26489
- changed = true;
26490
- }
26491
- if (changed) {
26492
- writeWarnedTools(opts.storageDir, warned);
26807
+ await sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning));
26493
26808
  }
26494
26809
  }
26495
26810
  async function cleanupWarnings(opts) {
@@ -26527,20 +26842,20 @@ async function cleanupWarnings(opts) {
26527
26842
 
26528
26843
  // src/shared/rpc-server.ts
26529
26844
  import { randomBytes as randomBytes2 } from "crypto";
26530
- import { mkdirSync as mkdirSync9, renameSync as renameSync5, unlinkSync as unlinkSync7, writeFileSync as writeFileSync9 } from "fs";
26845
+ import { mkdirSync as mkdirSync10, renameSync as renameSync7, unlinkSync as unlinkSync7, writeFileSync as writeFileSync9 } from "fs";
26531
26846
  import { createServer } from "http";
26532
26847
  import { dirname as dirname6 } from "path";
26533
26848
 
26534
26849
  // src/shared/rpc-utils.ts
26535
26850
  import { createHash as createHash5 } from "crypto";
26536
- import { join as join16 } from "path";
26851
+ import { join as join17 } from "path";
26537
26852
  function projectHash(directory) {
26538
26853
  const normalized = directory.replace(/\/+$/, "");
26539
26854
  return createHash5("sha256").update(normalized).digest("hex").slice(0, 16);
26540
26855
  }
26541
26856
  function rpcPortFilePath(storageDir, directory) {
26542
26857
  const hash2 = projectHash(directory);
26543
- return join16(storageDir, "rpc", hash2, "port");
26858
+ return join17(storageDir, "rpc", hash2, "port");
26544
26859
  }
26545
26860
 
26546
26861
  // src/shared/rpc-server.ts
@@ -26574,10 +26889,13 @@ class AftRpcServer {
26574
26889
  this.server = server;
26575
26890
  try {
26576
26891
  const dir = dirname6(this.portFilePath);
26577
- mkdirSync9(dir, { recursive: true });
26892
+ mkdirSync10(dir, { recursive: true, mode: 448 });
26578
26893
  const tmpPath = `${this.portFilePath}.tmp`;
26579
- writeFileSync9(tmpPath, JSON.stringify({ port: this.port, token: this.token }), "utf-8");
26580
- renameSync5(tmpPath, this.portFilePath);
26894
+ writeFileSync9(tmpPath, JSON.stringify({ port: this.port, token: this.token }), {
26895
+ encoding: "utf-8",
26896
+ mode: 384
26897
+ });
26898
+ renameSync7(tmpPath, this.portFilePath);
26581
26899
  log2(`RPC server listening on 127.0.0.1:${this.port}`);
26582
26900
  } catch (err) {
26583
26901
  warn2(`Failed to write RPC port file: ${err}`);
@@ -26654,18 +26972,6 @@ class AftRpcServer {
26654
26972
  }
26655
26973
  }
26656
26974
 
26657
- // src/shared/runtime.ts
26658
- var GLOBAL_KEY = "__AFT_SHARED_BRIDGE_POOL__";
26659
- function getGlobalState() {
26660
- return globalThis;
26661
- }
26662
- function setSharedBridgePool(pool) {
26663
- getGlobalState()[GLOBAL_KEY] = pool;
26664
- }
26665
- function clearSharedBridgePool() {
26666
- getGlobalState()[GLOBAL_KEY] = null;
26667
- }
26668
-
26669
26975
  // src/shared/session-directory.ts
26670
26976
  var CACHE_MAX_ENTRIES = 200;
26671
26977
  var cache = new Map;
@@ -26872,21 +27178,21 @@ function formatStatusMarkdown(status) {
26872
27178
 
26873
27179
  // src/shared/tui-config.ts
26874
27180
  var import_comment_json4 = __toESM(require_src2(), 1);
26875
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync10 } from "fs";
26876
- import { dirname as dirname7, join as join18 } from "path";
27181
+ import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
27182
+ import { dirname as dirname7, join as join19 } from "path";
26877
27183
 
26878
27184
  // src/shared/opencode-config-dir.ts
26879
27185
  import { homedir as homedir10 } from "os";
26880
- import { join as join17, resolve as resolve4 } from "path";
27186
+ import { join as join18, resolve as resolve4 } from "path";
26881
27187
  function getCliConfigDir() {
26882
27188
  const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
26883
27189
  if (envConfigDir) {
26884
27190
  return resolve4(envConfigDir);
26885
27191
  }
26886
27192
  if (process.platform === "win32") {
26887
- return join17(homedir10(), ".config", "opencode");
27193
+ return join18(homedir10(), ".config", "opencode");
26888
27194
  }
26889
- return join17(process.env.XDG_CONFIG_HOME || join17(homedir10(), ".config"), "opencode");
27195
+ return join18(process.env.XDG_CONFIG_HOME || join18(homedir10(), ".config"), "opencode");
26890
27196
  }
26891
27197
  function getOpenCodeConfigDir2(_options) {
26892
27198
  return getCliConfigDir();
@@ -26895,10 +27201,10 @@ function getOpenCodeConfigPaths(options) {
26895
27201
  const configDir = getOpenCodeConfigDir2(options);
26896
27202
  return {
26897
27203
  configDir,
26898
- configJson: join17(configDir, "opencode.json"),
26899
- configJsonc: join17(configDir, "opencode.jsonc"),
26900
- packageJson: join17(configDir, "package.json"),
26901
- omoConfig: join17(configDir, "magic-context.jsonc")
27204
+ configJson: join18(configDir, "opencode.json"),
27205
+ configJsonc: join18(configDir, "opencode.jsonc"),
27206
+ packageJson: join18(configDir, "package.json"),
27207
+ omoConfig: join18(configDir, "magic-context.jsonc")
26902
27208
  };
26903
27209
  }
26904
27210
 
@@ -26907,8 +27213,8 @@ var PLUGIN_NAME = "@cortexkit/aft-opencode";
26907
27213
  var PLUGIN_ENTRY = `${PLUGIN_NAME}@latest`;
26908
27214
  function resolveTuiConfigPath() {
26909
27215
  const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
26910
- const jsoncPath = join18(configDir, "tui.jsonc");
26911
- const jsonPath = join18(configDir, "tui.json");
27216
+ const jsoncPath = join19(configDir, "tui.jsonc");
27217
+ const jsonPath = join19(configDir, "tui.json");
26912
27218
  if (existsSync12(jsoncPath))
26913
27219
  return jsoncPath;
26914
27220
  if (existsSync12(jsonPath))
@@ -26920,7 +27226,7 @@ function ensureTuiPluginEntry() {
26920
27226
  const configPath = resolveTuiConfigPath();
26921
27227
  let config2 = {};
26922
27228
  if (existsSync12(configPath)) {
26923
- config2 = import_comment_json4.parse(readFileSync9(configPath, "utf-8")) ?? {};
27229
+ config2 = import_comment_json4.parse(readFileSync11(configPath, "utf-8")) ?? {};
26924
27230
  }
26925
27231
  const plugins = Array.isArray(config2.plugin) ? config2.plugin.filter((value) => typeof value === "string") : [];
26926
27232
  if (plugins.some((plugin) => plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`) || plugin.includes("opencode-plugin") || plugin.includes("aft-opencode"))) {
@@ -26928,7 +27234,7 @@ function ensureTuiPluginEntry() {
26928
27234
  }
26929
27235
  plugins.push(PLUGIN_ENTRY);
26930
27236
  config2.plugin = plugins;
26931
- mkdirSync10(dirname7(configPath), { recursive: true });
27237
+ mkdirSync11(dirname7(configPath), { recursive: true });
26932
27238
  writeFileSync10(configPath, `${import_comment_json4.stringify(config2, null, 2)}
26933
27239
  `);
26934
27240
  log2(`[aft-plugin] added TUI plugin entry to ${configPath}`);
@@ -26940,13 +27246,13 @@ function ensureTuiPluginEntry() {
26940
27246
  }
26941
27247
 
26942
27248
  // src/shutdown-hooks.ts
26943
- var GLOBAL_KEY2 = "__aftShutdownHooks__";
27249
+ var GLOBAL_KEY = "__aftShutdownHooks__";
26944
27250
  function getState() {
26945
27251
  const g = globalThis;
26946
- if (!g[GLOBAL_KEY2]) {
26947
- g[GLOBAL_KEY2] = { cleanups: new Set, installed: false };
27252
+ if (!g[GLOBAL_KEY]) {
27253
+ g[GLOBAL_KEY] = { cleanups: new Set, installed: false };
26948
27254
  }
26949
- return g[GLOBAL_KEY2];
27255
+ return g[GLOBAL_KEY];
26950
27256
  }
26951
27257
  var shuttingDown = false;
26952
27258
  async function runCleanups(reason) {
@@ -27048,8 +27354,10 @@ async function callBridge(ctx, runtime, command, params = {}, options) {
27048
27354
  }
27049
27355
 
27050
27356
  // src/tools/permissions.ts
27357
+ import * as fs3 from "fs";
27051
27358
  import * as path3 from "path";
27052
27359
  import { Effect } from "effect";
27360
+ var UNSUPPORTED_ASK_HOST = "AFT requires OpenCode 1.14.39 or newer for permission asks; please upgrade OpenCode";
27053
27361
  async function runAsk(maybe) {
27054
27362
  await Effect.runPromise(maybe);
27055
27363
  }
@@ -27077,6 +27385,8 @@ function workspacePattern(_context) {
27077
27385
  return ".";
27078
27386
  }
27079
27387
  async function askEditPermission(context, patterns, metadata = {}) {
27388
+ if (typeof context.ask !== "function")
27389
+ return UNSUPPORTED_ASK_HOST;
27080
27390
  try {
27081
27391
  await runAsk(context.ask({
27082
27392
  permission: "edit",
@@ -27092,6 +27402,110 @@ async function askEditPermission(context, patterns, metadata = {}) {
27092
27402
  return "Permission denied.";
27093
27403
  }
27094
27404
  }
27405
+ function containsPath(parent, child) {
27406
+ if (!parent)
27407
+ return false;
27408
+ const rel = path3.relative(parent, child);
27409
+ return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
27410
+ }
27411
+ function windowsPath(p) {
27412
+ if (process.platform !== "win32")
27413
+ return p;
27414
+ return p.replace(/^\/([a-zA-Z]):(?:[\\/]|$)/, (_, drive) => `${drive.toUpperCase()}:/`).replace(/^\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`).replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`).replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`);
27415
+ }
27416
+ function normalizePath(p) {
27417
+ if (process.platform !== "win32")
27418
+ return p;
27419
+ const resolved = path3.resolve(windowsPath(p));
27420
+ try {
27421
+ return fs3.realpathSync.native(resolved);
27422
+ } catch {
27423
+ return resolved;
27424
+ }
27425
+ }
27426
+ function normalizePathPattern(p) {
27427
+ if (process.platform !== "win32")
27428
+ return p;
27429
+ if (p === "*" || p === "**")
27430
+ return p;
27431
+ const match = p.match(/^(.*)[\\/](\*{1,2})$/);
27432
+ if (!match)
27433
+ return normalizePath(p);
27434
+ const dir = /^[A-Za-z]:$/.test(match[1]) ? `${match[1]}\\` : match[1];
27435
+ return path3.join(normalizePath(dir), match[2]);
27436
+ }
27437
+ async function assertExternalDirectoryPermission(context, target, options) {
27438
+ if (!target)
27439
+ return;
27440
+ if (typeof context.ask !== "function")
27441
+ return UNSUPPORTED_ASK_HOST;
27442
+ const resolved = path3.isAbsolute(target) ? target : path3.resolve(context.directory, target);
27443
+ const absoluteTarget = normalizePath(resolved);
27444
+ const directory = context.directory ? normalizePath(context.directory) : context.directory;
27445
+ const rawWorktree = context.worktree;
27446
+ const worktree = rawWorktree && rawWorktree !== "/" ? normalizePath(rawWorktree) : rawWorktree;
27447
+ if (directory && containsPath(directory, absoluteTarget))
27448
+ return;
27449
+ if (worktree && worktree !== "/" && worktree !== directory && containsPath(worktree, absoluteTarget)) {
27450
+ return;
27451
+ }
27452
+ const kind = options?.kind ?? "file";
27453
+ const parentDir = kind === "directory" ? absoluteTarget : path3.dirname(absoluteTarget);
27454
+ const rawGlob = process.platform === "win32" ? normalizePathPattern(path3.join(parentDir, "*")) : path3.join(parentDir, "*").replaceAll("\\", "/");
27455
+ try {
27456
+ await runAsk(context.ask({
27457
+ permission: "external_directory",
27458
+ patterns: [rawGlob],
27459
+ always: [rawGlob],
27460
+ metadata: {
27461
+ filepath: absoluteTarget,
27462
+ parentDir
27463
+ }
27464
+ }));
27465
+ return;
27466
+ } catch (error50) {
27467
+ if (error50 instanceof Error && error50.message) {
27468
+ return error50.message;
27469
+ }
27470
+ return "Permission denied (external directory).";
27471
+ }
27472
+ }
27473
+ async function askGrepPermission(context, pattern, metadata = {}) {
27474
+ if (typeof context.ask !== "function")
27475
+ return UNSUPPORTED_ASK_HOST;
27476
+ try {
27477
+ await runAsk(context.ask({
27478
+ permission: "grep",
27479
+ patterns: [pattern],
27480
+ always: ["*"],
27481
+ metadata: { pattern, ...metadata }
27482
+ }));
27483
+ return;
27484
+ } catch (error50) {
27485
+ if (error50 instanceof Error && error50.message) {
27486
+ return error50.message;
27487
+ }
27488
+ return "Permission denied (grep).";
27489
+ }
27490
+ }
27491
+ async function askGlobPermission(context, pattern, metadata = {}) {
27492
+ if (typeof context.ask !== "function")
27493
+ return UNSUPPORTED_ASK_HOST;
27494
+ try {
27495
+ await runAsk(context.ask({
27496
+ permission: "glob",
27497
+ patterns: [pattern],
27498
+ always: ["*"],
27499
+ metadata: { pattern, ...metadata }
27500
+ }));
27501
+ return;
27502
+ } catch (error50) {
27503
+ if (error50 instanceof Error && error50.message) {
27504
+ return error50.message;
27505
+ }
27506
+ return "Permission denied (glob).";
27507
+ }
27508
+ }
27095
27509
  function permissionDeniedResponse(message) {
27096
27510
  return JSON.stringify({
27097
27511
  success: false,
@@ -27111,6 +27525,17 @@ function extractHint(response) {
27111
27525
  const hint = response.hint;
27112
27526
  return typeof hint === "string" && hint.length > 0 ? hint : null;
27113
27527
  }
27528
+ async function checkAstPathsPermission(context, paths) {
27529
+ if (!Array.isArray(paths))
27530
+ return;
27531
+ const uniquePaths = Array.from(new Set(paths.filter((p) => typeof p === "string" && p.length > 0)));
27532
+ for (const p of uniquePaths) {
27533
+ const denial = await assertExternalDirectoryPermission(context, p, { kind: "directory" });
27534
+ if (denial)
27535
+ return denial;
27536
+ }
27537
+ return;
27538
+ }
27114
27539
  var SUPPORTED_LANGS = ["typescript", "tsx", "javascript", "python", "rust", "go"];
27115
27540
  function astTools(ctx) {
27116
27541
  const searchTool = {
@@ -27131,6 +27556,9 @@ function astTools(ctx) {
27131
27556
  contextLines: z2.number().optional().describe("Number of context lines to show around each match")
27132
27557
  },
27133
27558
  execute: async (args, context) => {
27559
+ const externalDenied = await checkAstPathsPermission(context, args.paths);
27560
+ if (externalDenied)
27561
+ return permissionDeniedResponse(externalDenied);
27134
27562
  const params = {
27135
27563
  pattern: args.pattern,
27136
27564
  lang: args.lang
@@ -27224,16 +27652,32 @@ ${hint}`;
27224
27652
  },
27225
27653
  execute: async (args, context) => {
27226
27654
  const isDryRun = args.dryRun === true;
27655
+ const externalDenied = await checkAstPathsPermission(context, args.paths);
27656
+ if (externalDenied)
27657
+ return permissionDeniedResponse(externalDenied);
27227
27658
  if (!isDryRun) {
27659
+ const paths = Array.isArray(args.paths) ? args.paths : ["."];
27660
+ if (!Array.isArray(args.paths)) {
27661
+ const asked = new Set;
27662
+ for (const targetPath of paths) {
27663
+ const absPath = resolveAbsolutePath(context, targetPath);
27664
+ if (asked.has(absPath))
27665
+ continue;
27666
+ asked.add(absPath);
27667
+ const denial = await assertExternalDirectoryPermission(context, absPath, {
27668
+ kind: "directory"
27669
+ });
27670
+ if (denial)
27671
+ return permissionDeniedResponse(denial);
27672
+ }
27673
+ }
27228
27674
  const explicitPaths = Array.isArray(args.paths) ? resolveRelativePatterns(context, args.paths) : [];
27229
27675
  const positiveGlobs = Array.isArray(args.globs) ? args.globs.filter((glob) => !glob.startsWith("!")) : [];
27230
27676
  const patterns = [...explicitPaths, ...positiveGlobs];
27231
27677
  const metadata = explicitPaths.length === 1 && positiveGlobs.length === 0 && Array.isArray(args.paths) ? { filepath: resolveAbsolutePath(context, args.paths[0]) } : {};
27232
27678
  const permissionError = await askEditPermission(context, patterns.length > 0 ? patterns : [workspacePattern(context)], metadata);
27233
27679
  if (permissionError) {
27234
- const output2 = `Permission denied: ${permissionError}`;
27235
- showOutputToUser(context, output2);
27236
- return output2;
27680
+ return permissionDeniedResponse(permissionError);
27237
27681
  }
27238
27682
  }
27239
27683
  const params = {
@@ -27367,7 +27811,7 @@ function conflictTools(ctx) {
27367
27811
  }
27368
27812
 
27369
27813
  // src/tools/hoisted.ts
27370
- import * as fs3 from "fs";
27814
+ import * as fs4 from "fs";
27371
27815
  import * as path4 from "path";
27372
27816
  import { tool as tool4 } from "@opencode-ai/plugin";
27373
27817
 
@@ -27664,11 +28108,64 @@ ${chunk.old_lines.join(`
27664
28108
 
27665
28109
  // src/tools/bash.ts
27666
28110
  import { tool as tool3 } from "@opencode-ai/plugin";
28111
+
28112
+ // src/shared/subagent-detect.ts
28113
+ var CACHE_MAX_ENTRIES2 = 200;
28114
+ var cache2 = new Map;
28115
+ async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
28116
+ if (!sessionId) {
28117
+ sessionLog2(undefined, "[subagent-detect] no sessionId provided \u2192 primary");
28118
+ return false;
28119
+ }
28120
+ const cached2 = cache2.get(sessionId);
28121
+ if (cached2) {
28122
+ cache2.delete(sessionId);
28123
+ cache2.set(sessionId, cached2);
28124
+ return cached2.isSubagent;
28125
+ }
28126
+ const c = client;
28127
+ const sessionApi = c?.session;
28128
+ if (!sessionApi || typeof sessionApi.get !== "function") {
28129
+ sessionLog2(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) \u2192 caching as primary`);
28130
+ setCache2(sessionId, false);
28131
+ return false;
28132
+ }
28133
+ sessionLog2(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
28134
+ let isSubagent = false;
28135
+ let parentIdRaw;
28136
+ try {
28137
+ const result = await sessionApi.get({
28138
+ path: { id: sessionId }
28139
+ });
28140
+ const session = result?.data ?? result;
28141
+ parentIdRaw = session?.parentID;
28142
+ isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
28143
+ sessionLog2(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} \u2192 isSubagent=${isSubagent}`);
28144
+ } catch (err) {
28145
+ sessionWarn2(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
28146
+ return false;
28147
+ }
28148
+ setCache2(sessionId, isSubagent);
28149
+ return isSubagent;
28150
+ }
28151
+ function setCache2(sessionId, isSubagent) {
28152
+ if (cache2.has(sessionId))
28153
+ cache2.delete(sessionId);
28154
+ cache2.set(sessionId, { isSubagent, recordedAt: Date.now() });
28155
+ if (cache2.size > CACHE_MAX_ENTRIES2) {
28156
+ const oldest = cache2.keys().next().value;
28157
+ if (oldest !== undefined)
28158
+ cache2.delete(oldest);
28159
+ }
28160
+ }
28161
+
28162
+ // src/tools/bash.ts
27667
28163
  var z3 = tool3.schema;
27668
28164
  var METADATA_PREVIEW_LIMIT = 30 * 1024;
27669
28165
  var FOREGROUND_WAIT_WINDOW_MS = 5000;
27670
28166
  var FOREGROUND_POLL_INTERVAL_MS = 100;
27671
28167
  var BASH_TRANSPORT_TIMEOUT_MS = 30000;
28168
+ var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
27672
28169
  var BASH_DESCRIPTION = `Hoisted bash tool with output compression, command rewriting to AFT tools, and optional background execution. By default, output is compressed; pass compressed: false for raw output. Pass background: true to spawn in the background and get a task_id for bash_status/bash_kill.`;
27673
28170
  async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
27674
28171
  const first = await bridgeCall(ctx, runtime, "bash", params, options);
@@ -27709,6 +28206,12 @@ function createBashTool(ctx) {
27709
28206
  const metadata = context.metadata;
27710
28207
  const command = args.command;
27711
28208
  const cwd = args.workdir ?? context.directory;
28209
+ const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
28210
+ const requestedBackground = args.background === true;
28211
+ const effectiveBackground = isSubagent ? false : requestedBackground;
28212
+ if (isSubagent && requestedBackground) {
28213
+ sessionLog2(context.sessionID, "[bash] subagent + background:true \u2192 converting to foreground (subagent would lose task_id)");
28214
+ }
27712
28215
  const shellEnv = await ctx.plugin?.trigger?.("shell.env", { cwd, sessionID: context.sessionID, callID: getCallID(context) }, { env: {} });
27713
28216
  const data = await withPermissionLoop(ctx, context, {
27714
28217
  command,
@@ -27716,8 +28219,8 @@ function createBashTool(ctx) {
27716
28219
  workdir: args.workdir,
27717
28220
  env: shellEnv?.env ?? {},
27718
28221
  description,
27719
- background: args.background,
27720
- notify_on_completion: args.background === true,
28222
+ background: effectiveBackground,
28223
+ notify_on_completion: effectiveBackground,
27721
28224
  compressed: args.compressed,
27722
28225
  permissions_requested: true
27723
28226
  }, callBridge, {
@@ -27734,7 +28237,7 @@ function createBashTool(ctx) {
27734
28237
  if (data.status === "running" && typeof data.task_id === "string") {
27735
28238
  const callID2 = getCallID(context);
27736
28239
  const taskId = data.task_id;
27737
- if (args.background === true) {
28240
+ if (effectiveBackground) {
27738
28241
  trackBgTask(context.sessionID, taskId);
27739
28242
  const startedLine = formatBackgroundLaunch(taskId);
27740
28243
  const metadataPayload2 = { description, output: startedLine, status: "running", taskId };
@@ -27748,7 +28251,7 @@ function createBashTool(ctx) {
27748
28251
  return startedLine;
27749
28252
  }
27750
28253
  const argTimeout = args.timeout;
27751
- const waitTimeoutMs = argTimeout !== undefined ? Math.min(argTimeout, FOREGROUND_WAIT_WINDOW_MS) : FOREGROUND_WAIT_WINDOW_MS;
28254
+ const waitTimeoutMs = isSubagent ? argTimeout ?? DEFAULT_HARD_TIMEOUT_MS : argTimeout !== undefined ? Math.min(argTimeout, FOREGROUND_WAIT_WINDOW_MS) : FOREGROUND_WAIT_WINDOW_MS;
27752
28255
  const startedAt = Date.now();
27753
28256
  while (true) {
27754
28257
  const status = await callBridge(ctx, context, "bash_status", { task_id: taskId });
@@ -27829,7 +28332,7 @@ function createBashStatusTool(ctx) {
27829
28332
  return {
27830
28333
  description: "Check the status and captured output of a background bash task spawned with bash({ background: true }). Returns status (running | completed | failed | killed | timed_out), exit code, duration, and a preview of captured output.",
27831
28334
  args: {
27832
- taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bgb-6b454047.")
28335
+ taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047.")
27833
28336
  },
27834
28337
  execute: async (args, context) => {
27835
28338
  const data = await callBridge(ctx, context, "bash_status", {
@@ -27859,7 +28362,7 @@ function createBashKillTool(ctx) {
27859
28362
  return {
27860
28363
  description: "Terminate a running background bash task spawned with bash({ background: true }). Returns confirmation of kill or an error if the task already finished.",
27861
28364
  args: {
27862
- taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bgb-6b454047.")
28365
+ taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047.")
27863
28366
  },
27864
28367
  execute: async (args, context) => {
27865
28368
  const data = await callBridge(ctx, context, "bash_kill", {
@@ -28170,12 +28673,23 @@ function createReadTool(ctx) {
28170
28673
  execute: async (args, context) => {
28171
28674
  const file2 = args.filePath;
28172
28675
  const filePath = path4.isAbsolute(file2) ? file2 : path4.resolve(context.directory, file2);
28173
- await runAsk(context.ask({
28174
- permission: "read",
28175
- patterns: [filePath],
28176
- always: ["*"],
28177
- metadata: {}
28178
- }));
28676
+ {
28677
+ const denial = await assertExternalDirectoryPermission(context, filePath);
28678
+ if (denial)
28679
+ return permissionDeniedResponse(denial);
28680
+ }
28681
+ try {
28682
+ await runAsk(context.ask({
28683
+ permission: "read",
28684
+ patterns: [filePath],
28685
+ always: ["*"],
28686
+ metadata: {}
28687
+ }));
28688
+ } catch (error50) {
28689
+ if (error50 instanceof Error && error50.message)
28690
+ return permissionDeniedResponse(error50.message);
28691
+ return permissionDeniedResponse("Permission denied.");
28692
+ }
28179
28693
  const ext = path4.extname(filePath).toLowerCase();
28180
28694
  const mimeMap = {
28181
28695
  ".png": "image/png",
@@ -28198,7 +28712,7 @@ function createReadTool(ctx) {
28198
28712
  const label = isImage ? "Image" : "PDF";
28199
28713
  let fileSize = 0;
28200
28714
  try {
28201
- const stat = await import("fs/promises").then((fs4) => fs4.stat(filePath));
28715
+ const stat = await import("fs/promises").then((fs5) => fs5.stat(filePath));
28202
28716
  fileSize = stat.size;
28203
28717
  } catch {}
28204
28718
  const sizeStr = fileSize > 1048576 ? `${(fileSize / 1048576).toFixed(1)}MB` : fileSize > 1024 ? `${(fileSize / 1024).toFixed(0)}KB` : `${fileSize} bytes`;
@@ -28294,6 +28808,11 @@ function createWriteTool(ctx, editToolName = "edit") {
28294
28808
  const content = args.content;
28295
28809
  const filePath = path4.isAbsolute(file2) ? file2 : path4.resolve(context.directory, file2);
28296
28810
  const relPath = path4.relative(context.worktree, filePath);
28811
+ {
28812
+ const denial = await assertExternalDirectoryPermission(context, filePath);
28813
+ if (denial)
28814
+ return permissionDeniedResponse(denial);
28815
+ }
28297
28816
  await runAsk(context.ask({
28298
28817
  permission: "edit",
28299
28818
  patterns: [relPath],
@@ -28414,9 +28933,9 @@ Note: Modes 6 and 7 are options on mode 5 (find/replace) \u2014 they require \`o
28414
28933
  - Auto-formats using project formatter if configured
28415
28934
  - Tree-sitter syntax validation on all edits
28416
28935
  - Symbol replace includes decorators, attributes, and doc comments in range
28417
- - LSP error-level diagnostics are returned automatically after non-dry-run edits
28936
+ - LSP error-level diagnostics are returned automatically after edits
28418
28937
 
28419
- Returns: JSON string for the selected edit mode. Dry runs return diff data; non-dry-run edits may append inline LSP error lines.
28938
+ Returns: JSON string for the selected edit mode. Edits may append inline LSP error lines.
28420
28939
 
28421
28940
  Common response fields: success (boolean), diff (object with before/after), backup_id (string), syntax_valid (boolean). Exact fields vary by mode.`;
28422
28941
  }
@@ -28433,8 +28952,7 @@ function createEditTool(ctx, writeToolName = "write") {
28433
28952
  content: z4.string().optional().describe("New content for symbol replace or file write"),
28434
28953
  appendContent: z4.string().optional().describe("Text to append to the end of filePath; creates the file if needed"),
28435
28954
  edits: z4.array(z4.record(z4.string(), z4.unknown())).optional().describe("Batch edits \u2014 array of { oldString: string, newString: string } or { startLine: number (1-based), endLine: number (1-based, inclusive), content: string }"),
28436
- operations: z4.array(z4.record(z4.string(), z4.unknown())).optional().describe("Transaction \u2014 array of { file: string, command: 'edit_match' | 'write', match?: string, replacement?: string, content?: string } for multi-file edits with rollback. Note: uses 'file'/'match'/'replacement' (not filePath/oldString/newString)"),
28437
- dryRun: z4.boolean().optional().describe("Preview changes without applying (returns diff, default: false)")
28955
+ operations: z4.array(z4.record(z4.string(), z4.unknown())).optional().describe("Transaction \u2014 array of { file: string, command: 'edit_match' | 'write', match?: string, replacement?: string, content?: string } for multi-file edits with rollback. Note: uses 'file'/'match'/'replacement' (not filePath/oldString/newString)")
28438
28956
  },
28439
28957
  execute: async (args, context) => {
28440
28958
  const argsRecord = args;
@@ -28444,6 +28962,18 @@ function createEditTool(ctx, writeToolName = "write") {
28444
28962
  if (Array.isArray(args.operations)) {
28445
28963
  const ops = args.operations;
28446
28964
  const files = ops.map((op) => op.file).filter(Boolean);
28965
+ {
28966
+ const asked = new Set;
28967
+ for (const file3 of files) {
28968
+ const absPath = path4.isAbsolute(file3) ? file3 : path4.resolve(context.directory, file3);
28969
+ if (asked.has(absPath))
28970
+ continue;
28971
+ asked.add(absPath);
28972
+ const denial = await assertExternalDirectoryPermission(context, absPath);
28973
+ if (denial)
28974
+ return permissionDeniedResponse(denial);
28975
+ }
28976
+ }
28447
28977
  await runAsk(context.ask({
28448
28978
  permission: "edit",
28449
28979
  patterns: files.map((f) => path4.relative(context.worktree, path4.resolve(context.directory, f))),
@@ -28454,9 +28984,7 @@ function createEditTool(ctx, writeToolName = "write") {
28454
28984
  ...op,
28455
28985
  file: path4.isAbsolute(op.file) ? op.file : path4.resolve(context.directory, op.file)
28456
28986
  }));
28457
- const params2 = { operations: resolvedOps };
28458
- params2.dry_run = args.dryRun === true;
28459
- const data2 = await callBridge(ctx, context, "transaction", params2);
28987
+ const data2 = await callBridge(ctx, context, "transaction", { operations: resolvedOps });
28460
28988
  return JSON.stringify(data2);
28461
28989
  }
28462
28990
  const file2 = args.filePath;
@@ -28464,6 +28992,11 @@ function createEditTool(ctx, writeToolName = "write") {
28464
28992
  throw new Error("'filePath' parameter is required");
28465
28993
  const filePath = path4.isAbsolute(file2) ? file2 : path4.resolve(context.directory, file2);
28466
28994
  const relPath = path4.relative(context.worktree, filePath);
28995
+ {
28996
+ const denial = await assertExternalDirectoryPermission(context, filePath);
28997
+ if (denial)
28998
+ return permissionDeniedResponse(denial);
28999
+ }
28467
29000
  await runAsk(context.ask({
28468
29001
  permission: "edit",
28469
29002
  patterns: [relPath],
@@ -28512,14 +29045,10 @@ function createEditTool(ctx, writeToolName = "write") {
28512
29045
  const hint = typeof args.content === "string" ? ` To write the whole file, use the '${writeToolName}' tool. To edit existing content, provide 'oldString' (and optionally 'newString'), 'symbol' + 'content', or an 'edits' array.` : " Provide 'oldString' (+ optional 'newString'), 'symbol' + 'content', 'edits' array, or 'operations' array.";
28513
29046
  throw new Error(`edit: no edit mode resolved from arguments.${hint}`);
28514
29047
  }
28515
- if (args.dryRun)
28516
- params.dry_run = true;
28517
- if (!args.dryRun)
28518
- params.diagnostics = true;
28519
- if (!args.dryRun)
28520
- params.include_diff = true;
29048
+ params.diagnostics = true;
29049
+ params.include_diff = true;
28521
29050
  const data = await callBridge(ctx, context, command, params);
28522
- if (!args.dryRun && data.success && data.diff) {
29051
+ if (data.success && data.diff) {
28523
29052
  const diff = data.diff;
28524
29053
  const callID = getCallID2(context);
28525
29054
  if (callID) {
@@ -28548,31 +29077,29 @@ function createEditTool(ctx, writeToolName = "write") {
28548
29077
  result += `
28549
29078
 
28550
29079
  ${globSkipNote}`;
28551
- if (!args.dryRun) {
28552
- const diags = data.lsp_diagnostics;
28553
- if (diags && diags.length > 0) {
28554
- const errors3 = diags.filter((d) => d.severity === "error");
28555
- if (errors3.length > 0) {
28556
- const diagLines = errors3.map((d) => ` Line ${d.line}: ${d.message}`).join(`
29080
+ const diags = data.lsp_diagnostics;
29081
+ if (diags && diags.length > 0) {
29082
+ const errors3 = diags.filter((d) => d.severity === "error");
29083
+ if (errors3.length > 0) {
29084
+ const diagLines = errors3.map((d) => ` Line ${d.line}: ${d.message}`).join(`
28557
29085
  `);
28558
- result += `
29086
+ result += `
28559
29087
 
28560
29088
  LSP errors detected, please fix:
28561
29089
  ${diagLines}`;
28562
- }
28563
29090
  }
28564
- const pendingServers = data.lsp_pending_servers;
28565
- const exitedServers = data.lsp_exited_servers;
28566
- if (pendingServers && pendingServers.length > 0) {
28567
- result += `
29091
+ }
29092
+ const pendingServers = data.lsp_pending_servers;
29093
+ const exitedServers = data.lsp_exited_servers;
29094
+ if (pendingServers && pendingServers.length > 0) {
29095
+ result += `
28568
29096
 
28569
29097
  Note: LSP server(s) did not respond in time: ${pendingServers.join(", ")}. Diagnostics may be incomplete; rerun lsp_diagnostics later for a fresh check.`;
28570
- }
28571
- if (exitedServers && exitedServers.length > 0) {
28572
- result += `
29098
+ }
29099
+ if (exitedServers && exitedServers.length > 0) {
29100
+ result += `
28573
29101
 
28574
29102
  Note: LSP server(s) exited during this edit: ${exitedServers.join(", ")}. Their diagnostics could not be collected.`;
28575
- }
28576
29103
  }
28577
29104
  return result;
28578
29105
  }
@@ -28659,13 +29186,24 @@ function createApplyPatchTool(ctx) {
28659
29186
  if (h.type === "update" && h.move_path) {
28660
29187
  const dstAbs = path4.resolve(context.directory, h.move_path);
28661
29188
  affectedAbs.add(dstAbs);
28662
- if (!fs3.existsSync(dstAbs)) {
29189
+ if (!fs4.existsSync(dstAbs)) {
28663
29190
  newlyCreatedAbs.add(dstAbs);
28664
29191
  }
28665
29192
  }
28666
29193
  }
28667
29194
  const relPaths = Array.from(affectedAbs).map((abs) => path4.relative(context.worktree, abs));
28668
29195
  const multiFileWritePaths = Array.from(affectedAbs);
29196
+ {
29197
+ const asked = new Set;
29198
+ for (const filePath of multiFileWritePaths) {
29199
+ if (asked.has(filePath))
29200
+ continue;
29201
+ asked.add(filePath);
29202
+ const denial = await assertExternalDirectoryPermission(context, filePath);
29203
+ if (denial)
29204
+ return permissionDeniedResponse(denial);
29205
+ }
29206
+ }
28669
29207
  await runAsk(context.ask({
28670
29208
  permission: "edit",
28671
29209
  patterns: relPaths,
@@ -28691,7 +29229,7 @@ function createApplyPatchTool(ctx) {
28691
29229
  const filePath = path4.resolve(context.directory, hunk.path);
28692
29230
  switch (hunk.type) {
28693
29231
  case "add": {
28694
- if (fs3.existsSync(filePath)) {
29232
+ if (fs4.existsSync(filePath)) {
28695
29233
  const msg = `Failed to create ${hunk.path}: file already exists. Use *** Update File: to modify, or *** Delete File: first if you want to replace it entirely.`;
28696
29234
  results.push(msg);
28697
29235
  failures.push(hunk.path);
@@ -28723,7 +29261,7 @@ function createApplyPatchTool(ctx) {
28723
29261
  results.push(msg);
28724
29262
  failures.push(hunk.path);
28725
29263
  const filePath2 = path4.resolve(context.directory, hunk.path);
28726
- if (fs3.existsSync(filePath2)) {
29264
+ if (fs4.existsSync(filePath2)) {
28727
29265
  try {
28728
29266
  await callBridge(ctx, context, "delete_file", { file: filePath2 });
28729
29267
  } catch {}
@@ -28733,7 +29271,7 @@ function createApplyPatchTool(ctx) {
28733
29271
  }
28734
29272
  case "delete": {
28735
29273
  try {
28736
- const before = await fs3.promises.readFile(filePath, "utf-8").catch(() => "");
29274
+ const before = await fs4.promises.readFile(filePath, "utf-8").catch(() => "");
28737
29275
  await callBridge(ctx, context, "delete_file", { file: filePath });
28738
29276
  perFileDiffs.push({
28739
29277
  filePath,
@@ -28751,7 +29289,7 @@ function createApplyPatchTool(ctx) {
28751
29289
  }
28752
29290
  case "update": {
28753
29291
  try {
28754
- const original = await fs3.promises.readFile(filePath, "utf-8");
29292
+ const original = await fs4.promises.readFile(filePath, "utf-8");
28755
29293
  const newContent = applyUpdateChunks(original, filePath, hunk.chunks);
28756
29294
  const targetPath = hunk.move_path ? path4.resolve(context.directory, hunk.move_path) : filePath;
28757
29295
  const writeResult = await callBridge(ctx, context, "write", {
@@ -28806,7 +29344,7 @@ ${diagLines}`);
28806
29344
  if (rollbackResult.success === false) {
28807
29345
  throw new Error(rollbackResult.message ?? "checkpoint restore failed");
28808
29346
  }
28809
- if (newlyCreatedAbs.has(targetPath) && fs3.existsSync(targetPath)) {
29347
+ if (newlyCreatedAbs.has(targetPath) && fs4.existsSync(targetPath)) {
28810
29348
  const cleanupResult = await callBridge(ctx, context, "delete_file", {
28811
29349
  file: targetPath
28812
29350
  });
@@ -28901,6 +29439,17 @@ function createDeleteTool(ctx) {
28901
29439
  execute: async (args, context) => {
28902
29440
  const inputs = args.files;
28903
29441
  const absolutePaths = inputs.map((f) => path4.isAbsolute(f) ? f : path4.resolve(context.directory, f));
29442
+ {
29443
+ const asked = new Set;
29444
+ for (const filePath of absolutePaths) {
29445
+ if (asked.has(filePath))
29446
+ continue;
29447
+ asked.add(filePath);
29448
+ const denial = await assertExternalDirectoryPermission(context, filePath);
29449
+ if (denial)
29450
+ return permissionDeniedResponse(denial);
29451
+ }
29452
+ }
28904
29453
  await runAsk(context.ask({
28905
29454
  permission: "edit",
28906
29455
  patterns: absolutePaths,
@@ -28946,6 +29495,18 @@ function createMoveTool(ctx) {
28946
29495
  execute: async (args, context) => {
28947
29496
  const filePath = path4.isAbsolute(args.filePath) ? args.filePath : path4.resolve(context.directory, args.filePath);
28948
29497
  const destPath = path4.isAbsolute(args.destination) ? args.destination : path4.resolve(context.directory, args.destination);
29498
+ {
29499
+ const sourceDenial = await assertExternalDirectoryPermission(context, filePath, {
29500
+ kind: "file"
29501
+ });
29502
+ if (sourceDenial)
29503
+ return permissionDeniedResponse(sourceDenial);
29504
+ if (destPath !== filePath) {
29505
+ const destDenial = await assertExternalDirectoryPermission(context, destPath);
29506
+ if (destDenial)
29507
+ return permissionDeniedResponse(destDenial);
29508
+ }
29509
+ }
28949
29510
  await runAsk(context.ask({
28950
29511
  permission: "edit",
28951
29512
  patterns: [filePath, destPath],
@@ -29000,6 +29561,11 @@ function aftPrefixedTools(ctx) {
29000
29561
  const file2 = normalizedArgs.filePath;
29001
29562
  const filePath = path4.isAbsolute(file2) ? file2 : path4.resolve(context.directory, file2);
29002
29563
  const relPath = path4.relative(context.worktree, filePath);
29564
+ {
29565
+ const denial = await assertExternalDirectoryPermission(context, filePath);
29566
+ if (denial)
29567
+ return permissionDeniedResponse(denial);
29568
+ }
29003
29569
  await runAsk(context.ask({
29004
29570
  permission: "edit",
29005
29571
  patterns: [relPath],
@@ -29012,9 +29578,6 @@ function aftPrefixedTools(ctx) {
29012
29578
  create_dirs: normalizedArgs.create_dirs !== false,
29013
29579
  diagnostics: true
29014
29580
  };
29015
- if (normalizedArgs.dryRun === true || normalizedArgs.dry_run === true) {
29016
- writeParams.dry_run = true;
29017
- }
29018
29581
  const data = await callBridge(ctx, context, "write", writeParams);
29019
29582
  return JSON.stringify(data);
29020
29583
  }
@@ -29038,10 +29601,9 @@ function importTools(ctx) {
29038
29601
  ` + `Ops:
29039
29602
  ` + `- 'add': Add an import. Auto-detects group (stdlib/external/internal), deduplicates. Requires 'module'. Optional 'names', 'defaultImport', 'typeOnly'.
29040
29603
  ` + `- 'remove': Remove an import or a specific named import. Requires 'module'. Provide 'removeName' to remove a single named import; omit to remove the entire import.
29041
- ` + `- 'organize': Re-sort and re-group all imports by language convention, deduplicate. Requires only 'filePath'.
29604
+ ` + `- 'organize': Re-sort and re-group all imports by language convention, deduplicate. Requires only 'filePath'. Use aft_safety checkpoint/undo for recovery before broad cleanup.
29042
29605
 
29043
29606
  ` + `Returns:
29044
- ` + `- Dry run (any op): { ok, dry_run, diff, syntax_valid? }
29045
29607
  ` + `- add: { file, added, module, group?, already_present?, formatted?, syntax_valid?, format_skipped_reason?, validation_errors?, validate_skipped_reason?, backup_id?, lsp_diagnostics? }
29046
29608
  ` + `- remove: { file, removed, module, name?, formatted, syntax_valid?, format_skipped_reason?, validation_errors?, validate_skipped_reason?, backup_id?, lsp_diagnostics? }
29047
29609
  ` + "- organize: { file, groups: [{ name, count }], removed_duplicates, formatted?, syntax_valid?, format_skipped_reason?, validation_errors?, validate_skipped_reason?, backup_id?, lsp_diagnostics? }",
@@ -29053,21 +29615,22 @@ function importTools(ctx) {
29053
29615
  defaultImport: z5.string().optional().describe("Default import name (e.g. 'React')"),
29054
29616
  typeOnly: z5.boolean().optional().describe("Type-only import (TS only, default: false)"),
29055
29617
  removeName: z5.string().optional().describe("Named import to remove for 'remove' op; omit to remove entire import"),
29056
- validate: z5.enum(["syntax", "full"]).optional().describe("Validation level: 'syntax' (default) or 'full'. Syntax = tree-sitter parse check only. Full = also runs LSP type-checking (slower, catches more errors)"),
29057
- dryRun: z5.boolean().optional().describe("Preview without modifying the file (default: false)")
29618
+ validate: z5.enum(["syntax", "full"]).optional().describe("Validation level: 'syntax' (default) or 'full'. Syntax = tree-sitter parse check only. Full = also runs LSP type-checking (slower, catches more errors)")
29058
29619
  },
29059
29620
  execute: async (args, context) => {
29060
29621
  const op = args.op;
29061
- const isDryRun = args.dryRun === true;
29062
29622
  if ((op === "add" || op === "remove") && typeof args.module !== "string") {
29063
29623
  throw new Error(`'module' is required for '${op}' op`);
29064
29624
  }
29065
- if (!isDryRun) {
29066
- const filePath = resolveAbsolutePath(context, args.filePath);
29067
- const permissionError = await askEditPermission(context, [resolveRelativePattern(context, args.filePath)], { filepath: filePath });
29068
- if (permissionError)
29069
- return permissionDeniedResponse(permissionError);
29625
+ const filePath = resolveAbsolutePath(context, args.filePath);
29626
+ {
29627
+ const denial = await assertExternalDirectoryPermission(context, filePath);
29628
+ if (denial)
29629
+ return permissionDeniedResponse(denial);
29070
29630
  }
29631
+ const permissionError = await askEditPermission(context, [resolveRelativePattern(context, args.filePath)], { filepath: filePath });
29632
+ if (permissionError)
29633
+ return permissionDeniedResponse(permissionError);
29071
29634
  const commandMap = {
29072
29635
  add: "add_import",
29073
29636
  remove: "remove_import",
@@ -29086,8 +29649,6 @@ function importTools(ctx) {
29086
29649
  params.name = args.removeName;
29087
29650
  if (args.validate !== undefined)
29088
29651
  params.validate = args.validate;
29089
- if (args.dryRun !== undefined)
29090
- params.dry_run = args.dryRun;
29091
29652
  const response = await callBridge(ctx, context, commandMap[op], params);
29092
29653
  if (response.success === false) {
29093
29654
  throw new Error(response.message || `${op} failed`);
@@ -29430,9 +29991,9 @@ function refactoringTools(ctx) {
29430
29991
 
29431
29992
  ` + `Each op requires specific parameters \u2014 see parameter descriptions for requirements.
29432
29993
 
29433
- ` + `All ops need 'filePath'. Use dryRun to preview before applying.
29994
+ ` + `All ops need 'filePath'. Use aft_safety checkpoint/undo before risky refactors.
29434
29995
 
29435
- ` + "Returns: move dry-run { ok, dry_run, diffs }; move apply { ok, files_modified, consumers_updated, checkpoint_name, results }. extract returns { file, name, parameters, return_type, syntax_valid, formatted, ... }. inline returns { file, symbol, call_context, substitutions, conflicts, syntax_valid, formatted, ... }.",
29996
+ ` + "Returns: move { ok, files_modified, consumers_updated, checkpoint_name, results }. extract returns { file, name, parameters, return_type, syntax_valid, formatted, ... }. inline returns { file, symbol, call_context, substitutions, conflicts, syntax_valid, formatted, ... }.",
29436
29997
  args: {
29437
29998
  op: z9.enum(["move", "extract", "inline"]).describe("Refactoring operation"),
29438
29999
  filePath: z9.string().describe("Path to the source file (absolute or relative to project root)"),
@@ -29442,8 +30003,7 @@ function refactoringTools(ctx) {
29442
30003
  name: z9.string().optional().describe("New function name \u2014 required for 'extract' op"),
29443
30004
  startLine: z9.number().optional().describe("1-based start line \u2014 required for 'extract' op"),
29444
30005
  endLine: z9.number().optional().describe("1-based end line (inclusive) \u2014 required for 'extract' op"),
29445
- callSiteLine: z9.number().optional().describe("1-based call site line \u2014 required for 'inline' op"),
29446
- dryRun: z9.boolean().optional().describe("Preview changes as diff without modifying files (default: false)")
30006
+ callSiteLine: z9.number().optional().describe("1-based call site line \u2014 required for 'inline' op")
29447
30007
  },
29448
30008
  execute: async (args, context) => {
29449
30009
  const op = args.op;
@@ -29471,6 +30031,18 @@ function refactoringTools(ctx) {
29471
30031
  ...typeof args.destination === "string" ? [args.destination] : []
29472
30032
  ]) : [resolveRelativePattern(context, args.filePath)];
29473
30033
  const metadata = patterns.length === 1 ? { filepath: filePath } : {};
30034
+ {
30035
+ const affectedPaths = op === "move" && typeof args.destination === "string" ? [filePath, resolveAbsolutePath(context, args.destination)] : [filePath];
30036
+ const asked = new Set;
30037
+ for (const affectedPath of affectedPaths) {
30038
+ if (asked.has(affectedPath))
30039
+ continue;
30040
+ asked.add(affectedPath);
30041
+ const denial = await assertExternalDirectoryPermission(context, affectedPath);
30042
+ if (denial)
30043
+ return permissionDeniedResponse(denial);
30044
+ }
30045
+ }
29474
30046
  const permissionError = await askEditPermission(context, patterns, metadata);
29475
30047
  if (permissionError)
29476
30048
  return permissionDeniedResponse(permissionError);
@@ -29480,8 +30052,6 @@ function refactoringTools(ctx) {
29480
30052
  inline: "inline_symbol"
29481
30053
  };
29482
30054
  const params = { file: args.filePath };
29483
- if (args.dryRun !== undefined)
29484
- params.dry_run = args.dryRun;
29485
30055
  switch (op) {
29486
30056
  case "move":
29487
30057
  params.symbol = args.symbol;
@@ -29513,6 +30083,7 @@ function refactoringTools(ctx) {
29513
30083
  }
29514
30084
 
29515
30085
  // src/tools/safety.ts
30086
+ import * as path5 from "path";
29516
30087
  import { tool as tool10 } from "@opencode-ai/plugin";
29517
30088
  var z10 = tool10.schema;
29518
30089
  function safetyTools(ctx) {
@@ -29550,10 +30121,30 @@ function safetyTools(ctx) {
29550
30121
  }
29551
30122
  if (op === "undo" && typeof args.filePath === "string") {
29552
30123
  const filePath = resolveAbsolutePath(context, args.filePath);
30124
+ {
30125
+ const denial = await assertExternalDirectoryPermission(context, filePath);
30126
+ if (denial)
30127
+ return permissionDeniedResponse(denial);
30128
+ }
29553
30129
  const permissionError = await askEditPermission(context, [resolveRelativePattern(context, args.filePath)], { filepath: filePath });
29554
30130
  if (permissionError)
29555
30131
  return permissionDeniedResponse(permissionError);
29556
30132
  }
30133
+ if (op === "checkpoint" && Array.isArray(args.files)) {
30134
+ const uniqueParents = new Set;
30135
+ for (const file2 of args.files) {
30136
+ if (typeof file2 !== "string")
30137
+ continue;
30138
+ const abs = path5.isAbsolute(file2) ? file2 : path5.resolve(context.directory, file2);
30139
+ const parent = path5.dirname(abs);
30140
+ if (uniqueParents.has(parent))
30141
+ continue;
30142
+ uniqueParents.add(parent);
30143
+ const denial = await assertExternalDirectoryPermission(context, file2, { kind: "file" });
30144
+ if (denial)
30145
+ return permissionDeniedResponse(denial);
30146
+ }
30147
+ }
29557
30148
  if (op === "restore") {
29558
30149
  const permissionError = await askEditPermission(context, [workspacePattern(context)], {
29559
30150
  checkpoint: args.name
@@ -29594,6 +30185,8 @@ function safetyTools(ctx) {
29594
30185
  }
29595
30186
 
29596
30187
  // src/tools/search.ts
30188
+ import * as fs5 from "fs";
30189
+ import * as path6 from "path";
29597
30190
  function arg(schema) {
29598
30191
  return schema;
29599
30192
  }
@@ -29663,11 +30256,33 @@ function searchTools(ctx) {
29663
30256
  path: arg(exports_external.string().optional().describe("Directory to search in, relative to project root"))
29664
30257
  },
29665
30258
  execute: async (args, context) => {
30259
+ const pattern = String(args.pattern);
30260
+ const includeArg = args.include ? String(args.include) : undefined;
30261
+ const pathArg = args.path ? String(args.path) : undefined;
30262
+ const grepDenied = await askGrepPermission(context, pattern, {
30263
+ path: pathArg,
30264
+ include: includeArg
30265
+ });
30266
+ if (grepDenied)
30267
+ return permissionDeniedResponse(grepDenied);
30268
+ if (pathArg) {
30269
+ let kind = "file";
30270
+ try {
30271
+ const abs = path6.isAbsolute(pathArg) ? pathArg : path6.resolve(context.directory, pathArg);
30272
+ if (fs5.lstatSync(abs).isDirectory())
30273
+ kind = "directory";
30274
+ } catch {}
30275
+ const externalDenied = await assertExternalDirectoryPermission(context, pathArg, {
30276
+ kind
30277
+ });
30278
+ if (externalDenied)
30279
+ return permissionDeniedResponse(externalDenied);
30280
+ }
29666
30281
  const response = await callBridge(ctx, context, "grep", {
29667
- pattern: args.pattern,
30282
+ pattern,
29668
30283
  case_sensitive: true,
29669
- include: args.include ? splitIncludeArg(String(args.include)).map(normalizeGlob).filter(Boolean) : undefined,
29670
- path: args.path,
30284
+ include: includeArg ? splitIncludeArg(includeArg).map(normalizeGlob).filter(Boolean) : undefined,
30285
+ path: pathArg,
29671
30286
  max_results: 100
29672
30287
  });
29673
30288
  if (response.success === false) {
@@ -29695,6 +30310,22 @@ function searchTools(ctx) {
29695
30310
  }
29696
30311
  }
29697
30312
  }
30313
+ const globDenied = await askGlobPermission(context, globPattern, { path: globPath });
30314
+ if (globDenied)
30315
+ return permissionDeniedResponse(globDenied);
30316
+ if (globPath) {
30317
+ let kind = "directory";
30318
+ try {
30319
+ const abs = path6.isAbsolute(globPath) ? globPath : path6.resolve(context.directory, globPath);
30320
+ if (fs5.lstatSync(abs).isFile())
30321
+ kind = "file";
30322
+ } catch {}
30323
+ const externalDenied = await assertExternalDirectoryPermission(context, globPath, {
30324
+ kind
30325
+ });
30326
+ if (externalDenied)
30327
+ return permissionDeniedResponse(externalDenied);
30328
+ }
29698
30329
  const response = await callBridge(ctx, context, "glob", {
29699
30330
  pattern: globPattern,
29700
30331
  path: globPath
@@ -29786,10 +30417,9 @@ function structureTools(ctx) {
29786
30417
  ` + `- 'add_decorator': Add Python decorator to function/class. Requires 'target' and 'decorator' (without @). Optional 'position'.
29787
30418
  ` + `- 'add_struct_tags': Add/update Go struct field tags. Requires 'target' (struct name), 'field', 'tag', 'value'.
29788
30419
 
29789
- ` + `Each op requires specific parameters \u2014 see parameter descriptions for requirements.
30420
+ ` + `Each op requires specific parameters \u2014 see parameter descriptions for requirements. Use aft_safety checkpoint/undo before risky transforms.
29790
30421
 
29791
30422
  ` + `Returns:
29792
- ` + `- Dry run (any op): { ok, dry_run, diff, syntax_valid? }
29793
30423
  ` + `- add_member: { file, scope, position, formatted, syntax_valid?, format_skipped_reason?, validation_errors?, validate_skipped_reason?, backup_id?, lsp_diagnostics? }
29794
30424
  ` + `- add_derive: { file, target, derives, formatted, syntax_valid?, format_skipped_reason?, validation_errors?, validate_skipped_reason?, backup_id?, lsp_diagnostics? }
29795
30425
  ` + `- wrap_try_catch: { file, target, formatted, syntax_valid?, format_skipped_reason?, validation_errors?, validate_skipped_reason?, backup_id?, lsp_diagnostics? }
@@ -29808,8 +30438,7 @@ function structureTools(ctx) {
29808
30438
  field: z11.string().optional().describe("Struct field name (add_struct_tags)"),
29809
30439
  tag: z11.string().optional().describe("Tag key (add_struct_tags \u2014 e.g. 'json')"),
29810
30440
  value: z11.string().optional().describe("Tag value (add_struct_tags \u2014 e.g. 'user_name,omitempty')"),
29811
- validate: z11.enum(["syntax", "full"]).optional().describe("Validation level: 'syntax' (default) or 'full'. Syntax = tree-sitter parse check only. Full = also runs LSP type-checking (slower, catches more errors)"),
29812
- dryRun: z11.boolean().optional().describe("Preview without modifying the file (default: false)")
30441
+ validate: z11.enum(["syntax", "full"]).optional().describe("Validation level: 'syntax' (default) or 'full'. Syntax = tree-sitter parse check only. Full = also runs LSP type-checking (slower, catches more errors)")
29813
30442
  },
29814
30443
  execute: async (args, context) => {
29815
30444
  const op = args.op;
@@ -29838,14 +30467,17 @@ function structureTools(ctx) {
29838
30467
  throw new Error("'value' is required for 'add_struct_tags' op");
29839
30468
  }
29840
30469
  const filePath = resolveAbsolutePath(context, args.filePath);
30470
+ {
30471
+ const denial = await assertExternalDirectoryPermission(context, filePath);
30472
+ if (denial)
30473
+ return permissionDeniedResponse(denial);
30474
+ }
29841
30475
  const permissionError = await askEditPermission(context, [resolveRelativePattern(context, args.filePath)], { filepath: filePath });
29842
30476
  if (permissionError)
29843
30477
  return permissionDeniedResponse(permissionError);
29844
30478
  const params = { file: args.filePath };
29845
30479
  if (args.validate !== undefined)
29846
30480
  params.validate = args.validate;
29847
- if (args.dryRun !== undefined)
29848
- params.dry_run = args.dryRun;
29849
30481
  switch (op) {
29850
30482
  case "add_member":
29851
30483
  params.scope = args.container;
@@ -29939,6 +30571,7 @@ function buildHintsFromConfig(config2, disabledTools) {
29939
30571
  }
29940
30572
 
29941
30573
  // src/configure-warnings.ts
30574
+ var pendingEagerWarnings = new Map;
29942
30575
  function isConfigureWarning(value) {
29943
30576
  if (!value || typeof value !== "object" || Array.isArray(value))
29944
30577
  return false;
@@ -29948,13 +30581,25 @@ function isConfigureWarning(value) {
29948
30581
  function coerceConfigureWarnings(warnings) {
29949
30582
  return warnings.filter(isConfigureWarning);
29950
30583
  }
30584
+ function drainPendingEagerWarnings(projectRoot) {
30585
+ const pending = pendingEagerWarnings.get(projectRoot) ?? [];
30586
+ pendingEagerWarnings.delete(projectRoot);
30587
+ return pending;
30588
+ }
29951
30589
  async function handleConfigureWarningsForSession(context) {
30590
+ const validWarnings = coerceConfigureWarnings(context.warnings);
29952
30591
  if (!context.sessionId) {
29953
- warn2(`[configure] deferred warnings for ${context.projectRoot} arrived without session_id; skipping notification`);
30592
+ if (validWarnings.length === 0)
30593
+ return;
30594
+ const pending = pendingEagerWarnings.get(context.projectRoot) ?? [];
30595
+ pending.push(...validWarnings);
30596
+ pendingEagerWarnings.set(context.projectRoot, pending);
30597
+ warn2(`[configure] deferred warnings for ${context.projectRoot} arrived without session_id; buffering until first session-bound call`);
29954
30598
  return;
29955
30599
  }
29956
- const validWarnings = coerceConfigureWarnings(context.warnings);
29957
- if (validWarnings.length === 0)
30600
+ const pendingWarnings = drainPendingEagerWarnings(context.projectRoot);
30601
+ const combinedWarnings = [...pendingWarnings, ...validWarnings];
30602
+ if (combinedWarnings.length === 0)
29958
30603
  return;
29959
30604
  await deliverConfigureWarnings({
29960
30605
  client: context.client ?? context.fallbackClient,
@@ -29962,7 +30607,7 @@ async function handleConfigureWarningsForSession(context) {
29962
30607
  storageDir: context.storageDir,
29963
30608
  pluginVersion: context.pluginVersion,
29964
30609
  projectRoot: context.projectRoot
29965
- }, validWarnings);
30610
+ }, combinedWarnings);
29966
30611
  }
29967
30612
 
29968
30613
  // src/index.ts
@@ -29977,16 +30622,10 @@ function throwSentinel(command) {
29977
30622
  }
29978
30623
  async function sendIgnoredMessage2(client, sessionID, text) {
29979
30624
  const typedClient = client;
29980
- const lastModel = await getLastAssistantModel(client, sessionID);
29981
30625
  const body = {
29982
30626
  noReply: true,
29983
30627
  parts: [{ type: "text", text, ignored: true }]
29984
30628
  };
29985
- if (lastModel) {
29986
- body.model = { providerID: lastModel.providerID, modelID: lastModel.modelID };
29987
- if (lastModel.variant)
29988
- body.variant = lastModel.variant;
29989
- }
29990
30629
  const promptInput = { path: { id: sessionID }, body };
29991
30630
  if (typeof typedClient.session?.prompt === "function") {
29992
30631
  await Promise.resolve(typedClient.session.prompt(promptInput));
@@ -30016,7 +30655,8 @@ var ANNOUNCEMENT_FEATURES = [
30016
30655
  "Trigram grep/glob and semantic search (aft_search) graduated out of experimental.",
30017
30656
  "Lots of bugfixes and new end-to-end test coverage."
30018
30657
  ];
30019
- var plugin = async (input) => {
30658
+ var plugin = async (input) => initializePluginForDirectory(input);
30659
+ async function initializePluginForDirectory(input) {
30020
30660
  const binaryPath = await findBinary();
30021
30661
  const aftConfig = loadAftConfig(input.directory);
30022
30662
  const autoUpdateAbort = new AbortController;
@@ -30044,8 +30684,8 @@ var plugin = async (input) => {
30044
30684
  if (aftConfig.max_callgraph_files !== undefined)
30045
30685
  configOverrides.max_callgraph_files = aftConfig.max_callgraph_files;
30046
30686
  const isFastembedSemanticBackend = (aftConfig.semantic?.backend ?? "fastembed") === "fastembed";
30047
- const dataHome = process.env.XDG_DATA_HOME || join19(homedir11(), ".local", "share");
30048
- configOverrides.storage_dir = join19(dataHome, "opencode", "storage", "plugin", "aft");
30687
+ const dataHome = process.env.XDG_DATA_HOME || join21(homedir11(), ".local", "share");
30688
+ configOverrides.storage_dir = join21(dataHome, "opencode", "storage", "plugin", "aft");
30049
30689
  let onnxRuntimePromise = null;
30050
30690
  if (aftConfig.semantic_search && isFastembedSemanticBackend) {
30051
30691
  const storageDir2 = configOverrides.storage_dir;
@@ -30131,10 +30771,10 @@ ${lines}
30131
30771
  }
30132
30772
  versionUpgradeAttempted = binaryVersion;
30133
30773
  warn2(`WARNING: aft binary v${binaryVersion} is older than plugin v${minVersion}. ` + "Some features may not work. Attempting to download a compatible binary...");
30134
- ensureBinary(`v${minVersion}`).then((path5) => {
30135
- if (path5) {
30136
- log2(`Found/downloaded compatible binary at ${path5}. Replacing running bridges...`);
30137
- pool.replaceBinary(path5).then(() => {
30774
+ ensureBinary(`v${minVersion}`).then((path7) => {
30775
+ if (path7) {
30776
+ log2(`Found/downloaded compatible binary at ${path7}. Replacing running bridges...`);
30777
+ pool.replaceBinary(path7).then(() => {
30138
30778
  log2("Binary replaced successfully. New bridges will use the updated binary.");
30139
30779
  }, (err) => error2("Failed to replace binary:", err));
30140
30780
  } else {
@@ -30145,11 +30785,12 @@ ${lines}
30145
30785
  });
30146
30786
  },
30147
30787
  onConfigureWarnings: async ({ projectRoot, sessionId, client, warnings }) => {
30788
+ const pendingWarnings = sessionId ? drainPendingEagerWarnings(projectRoot) : [];
30148
30789
  await handleConfigureWarningsForSession({
30149
30790
  projectRoot,
30150
30791
  sessionId,
30151
30792
  client,
30152
- warnings,
30793
+ warnings: [...pendingWarnings, ...warnings],
30153
30794
  fallbackClient: input.client,
30154
30795
  storageDir: configOverrides.storage_dir,
30155
30796
  pluginVersion: PLUGIN_VERSION
@@ -30184,7 +30825,6 @@ ${lines}
30184
30825
  config: aftConfig,
30185
30826
  storageDir: configOverrides.storage_dir
30186
30827
  };
30187
- setSharedBridgePool(pool);
30188
30828
  if (onnxRuntimePromise) {
30189
30829
  onnxRuntimePromise.then((ortDylibDir) => {
30190
30830
  if (ortDylibDir) {
@@ -30206,7 +30846,7 @@ ${lines}
30206
30846
  if (onnxRuntimePromise) {
30207
30847
  await Promise.race([
30208
30848
  onnxRuntimePromise,
30209
- new Promise((resolve9) => setTimeout(() => resolve9(null), 60000))
30849
+ new Promise((resolve11) => setTimeout(() => resolve11(null), 60000))
30210
30850
  ]);
30211
30851
  }
30212
30852
  const bridge = pool.getBridge(input.directory);
@@ -30252,10 +30892,10 @@ ${lines}
30252
30892
  return { show: false };
30253
30893
  }
30254
30894
  if (storageDir) {
30255
- const versionFile = join19(storageDir, "last_announced_version");
30895
+ const versionFile = join21(storageDir, "last_announced_version");
30256
30896
  try {
30257
30897
  if (existsSync14(versionFile)) {
30258
- const lastVersion = readFileSync10(versionFile, "utf-8").trim();
30898
+ const lastVersion = readFileSync12(versionFile, "utf-8").trim();
30259
30899
  if (lastVersion === ANNOUNCEMENT_VERSION)
30260
30900
  return { show: false };
30261
30901
  }
@@ -30266,8 +30906,8 @@ ${lines}
30266
30906
  rpcServer.handle("mark-announced", async () => {
30267
30907
  if (storageDir && ANNOUNCEMENT_VERSION) {
30268
30908
  try {
30269
- mkdirSync11(storageDir, { recursive: true });
30270
- writeFileSync11(join19(storageDir, "last_announced_version"), ANNOUNCEMENT_VERSION);
30909
+ mkdirSync12(storageDir, { recursive: true });
30910
+ writeFileSync11(join21(storageDir, "last_announced_version"), ANNOUNCEMENT_VERSION);
30271
30911
  } catch {}
30272
30912
  }
30273
30913
  return { success: true };
@@ -30399,7 +31039,6 @@ Install: ${getManualInstallHint()}`).catch(() => {});
30399
31039
  },
30400
31040
  "chat.message": async (messageInput) => {
30401
31041
  const sid = messageInput.sessionID ?? messageInput.sessionId ?? messageInput.id;
30402
- resetBgWake(sid);
30403
31042
  warmSessionDirectory(input.client, sid, input.directory);
30404
31043
  },
30405
31044
  "command.execute.before": async (commandInput, _output) => {
@@ -30459,11 +31098,10 @@ Install: ${getManualInstallHint()}`).catch(() => {});
30459
31098
  unregisterShutdown();
30460
31099
  await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
30461
31100
  rpcServer.stop();
30462
- clearSharedBridgePool();
30463
31101
  await pool.shutdown();
30464
31102
  }
30465
31103
  };
30466
- };
31104
+ }
30467
31105
  var src_default = plugin;
30468
31106
  export {
30469
31107
  src_default as default