@cortexkit/aft-opencode 0.20.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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));
8178
8226
  });
8227
+ child.stdout?.on("end", () => {
8228
+ const remaining = stdoutDecoder.end();
8229
+ if (remaining)
8230
+ this.onStdoutData(remaining);
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;
@@ -9932,6 +10028,13 @@ async function appendInTurnBgCompletions(drainContext, output) {
9932
10028
  output.output = appendReminder(output.output ?? "", reminder);
9933
10029
  state.pendingCompletions = [];
9934
10030
  state.pendingLongRunning = [];
10031
+ if (state.debounceTimer) {
10032
+ clearTimeout(state.debounceTimer);
10033
+ state.debounceTimer = null;
10034
+ state.firstCompletionAt = null;
10035
+ state.scheduledFireAt = null;
10036
+ state.scheduledCompletionCount = 0;
10037
+ }
9935
10038
  }
9936
10039
  async function handleIdleBgCompletions(drainContext) {
9937
10040
  await triggerWakeIfPending(drainContext, false);
@@ -9973,9 +10076,6 @@ async function triggerWakeIfPending(drainContext, skipDrain) {
9973
10076
  sessionWarn2(drainContext.sessionID, `${LOG_PREFIX} wake send failed: ${err instanceof Error ? err.message : String(err)}`);
9974
10077
  });
9975
10078
  }
9976
- function resetBgWake(sessionID) {
9977
- stateFor(sessionID).wakeFiredThisIdle = false;
9978
- }
9979
10079
  function formatSystemReminder(completions) {
9980
10080
  const bullets = completions.map((completion) => formatCompletion(completion)).join(`
9981
10081
  `);
@@ -10056,16 +10156,17 @@ function scheduleWake(state, sendWake, onSendFailure) {
10056
10156
  state.debounceTimer = setTimeout(() => {
10057
10157
  const pending = state.pendingCompletions;
10058
10158
  const pendingLongRunning = state.pendingLongRunning;
10059
- const reminder = formatCombinedSystemReminder(pending, pendingLongRunning);
10060
- state.pendingCompletions = [];
10061
- state.pendingLongRunning = [];
10062
10159
  state.debounceTimer = null;
10063
10160
  state.firstCompletionAt = null;
10064
10161
  state.scheduledFireAt = null;
10065
10162
  state.scheduledCompletionCount = 0;
10163
+ if (pending.length === 0 && pendingLongRunning.length === 0)
10164
+ return;
10165
+ const reminder = formatCombinedSystemReminder(pending, pendingLongRunning);
10166
+ state.pendingCompletions = [];
10167
+ state.pendingLongRunning = [];
10066
10168
  sendWake(reminder).then(() => {
10067
10169
  state.retryDelayMs = null;
10068
- state.wakeFiredThisIdle = true;
10069
10170
  }).catch((err) => {
10070
10171
  state.pendingCompletions = [...pending, ...state.pendingCompletions];
10071
10172
  state.pendingLongRunning = [...pendingLongRunning, ...state.pendingLongRunning];
@@ -10087,7 +10188,6 @@ function stateFor(sessionID) {
10087
10188
  pendingCompletions: [],
10088
10189
  pendingLongRunning: [],
10089
10190
  debounceTimer: null,
10090
- wakeFiredThisIdle: false,
10091
10191
  firstCompletionAt: null,
10092
10192
  scheduledFireAt: null,
10093
10193
  scheduledCompletionCount: 0,
@@ -21134,7 +21234,7 @@ function finalize(ctx, schema) {
21134
21234
  result.$schema = "http://json-schema.org/draft-07/schema#";
21135
21235
  } else if (ctx.target === "draft-04") {
21136
21236
  result.$schema = "http://json-schema.org/draft-04/schema#";
21137
- } else if (ctx.target === "openapi-3.0") {} else {}
21237
+ } else if (ctx.target === "openapi-3.0") {}
21138
21238
  if (ctx.external?.uri) {
21139
21239
  const id = ctx.external.registry.get(schema)?.id;
21140
21240
  if (!id)
@@ -21382,7 +21482,7 @@ var literalProcessor = (schema, ctx, json, _params) => {
21382
21482
  if (val === undefined) {
21383
21483
  if (ctx.unrepresentable === "throw") {
21384
21484
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
21385
- } else {}
21485
+ }
21386
21486
  } else if (typeof val === "bigint") {
21387
21487
  if (ctx.unrepresentable === "throw") {
21388
21488
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -24211,7 +24311,8 @@ import { dirname as dirname4, join as join11 } from "path";
24211
24311
  // src/hooks/auto-update-checker/cache.ts
24212
24312
  var import_comment_json3 = __toESM(require_src2(), 1);
24213
24313
  import { spawn as spawn2 } from "child_process";
24214
- 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";
24215
24316
  import { basename, dirname as dirname3, join as join10 } from "path";
24216
24317
 
24217
24318
  // src/hooks/auto-update-checker/checker.ts
@@ -24462,6 +24563,42 @@ async function getLatestVersion(channel = "latest", options = {}) {
24462
24563
  }
24463
24564
 
24464
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
+ }
24465
24602
  function stripPackageNameFromPath(pathValue, packageName) {
24466
24603
  let current = pathValue;
24467
24604
  for (const segment of [...packageName.split("/")].reverse()) {
@@ -24553,8 +24690,13 @@ function preparePackageUpdate(version2, packageName = PACKAGE_NAME, runtimePacka
24553
24690
  warn2("[auto-update-checker] No install context found for auto-update");
24554
24691
  return null;
24555
24692
  }
24556
- 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);
24557
24698
  return null;
24699
+ }
24558
24700
  const packageRemoved = removeInstalledPackage(installContext.installDir, packageName);
24559
24701
  const lockRemoved = removeFromPackageLock(installContext.installDir, packageName);
24560
24702
  if (!packageRemoved && !lockRemoved) {
@@ -24573,7 +24715,7 @@ async function runNpmInstallSafe(installDir, options = {}) {
24573
24715
  return false;
24574
24716
  const proc = spawn2("npm", ["install", "--no-audit", "--no-fund", "--no-progress"], {
24575
24717
  cwd: installDir,
24576
- stdio: "pipe"
24718
+ stdio: "ignore"
24577
24719
  });
24578
24720
  const abortProcess = () => {
24579
24721
  try {
@@ -24592,10 +24734,27 @@ async function runNpmInstallSafe(installDir, options = {}) {
24592
24734
  options.signal?.removeEventListener("abort", abortProcess);
24593
24735
  if (result === "timeout" || options.signal?.aborted) {
24594
24736
  abortProcess();
24737
+ const snapshot2 = pendingSnapshots.get(installDir);
24738
+ if (snapshot2) {
24739
+ pendingSnapshots.delete(installDir);
24740
+ restoreAutoUpdateSnapshot(snapshot2);
24741
+ }
24595
24742
  return false;
24596
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
+ }
24597
24751
  return result;
24598
24752
  } catch (err) {
24753
+ const snapshot = pendingSnapshots.get(installDir);
24754
+ if (snapshot) {
24755
+ pendingSnapshots.delete(installDir);
24756
+ restoreAutoUpdateSnapshot(snapshot);
24757
+ }
24599
24758
  warn2(`[auto-update-checker] npm install error: ${String(err)}`);
24600
24759
  return false;
24601
24760
  } finally {
@@ -24759,7 +24918,8 @@ function showToast(ctx, title, message, variant = "info", duration3 = 3000) {
24759
24918
  // src/lsp-auto-install.ts
24760
24919
  import { spawn as spawn3 } from "child_process";
24761
24920
  import { createHash as createHash3 } from "crypto";
24762
- 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";
24763
24923
 
24764
24924
  // src/lsp-cache.ts
24765
24925
  import {
@@ -25443,6 +25603,50 @@ function hashInstalledBinary(spec) {
25443
25603
  stream.on("end", () => resolve3(hash2.digest("hex")));
25444
25604
  });
25445
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
+ }
25446
25650
  function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
25447
25651
  const cachedBinDirs = [];
25448
25652
  const skipped = [];
@@ -25455,7 +25659,7 @@ function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
25455
25659
  return projectExtensions;
25456
25660
  };
25457
25661
  for (const spec of NPM_LSP_TABLE) {
25458
- if (isInstalled(spec.npm, spec.binary)) {
25662
+ if (isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)) {
25459
25663
  cachedBinDirs.push(lspBinDir(spec.npm));
25460
25664
  }
25461
25665
  if (config2.disabled.has(spec.id)) {
@@ -25507,16 +25711,17 @@ import {
25507
25711
  createWriteStream as createWriteStream2,
25508
25712
  existsSync as existsSync10,
25509
25713
  lstatSync as lstatSync2,
25510
- mkdirSync as mkdirSync7,
25714
+ mkdirSync as mkdirSync8,
25511
25715
  readdirSync as readdirSync4,
25716
+ readFileSync as readFileSync9,
25512
25717
  readlinkSync as readlinkSync2,
25513
25718
  realpathSync as realpathSync3,
25514
- renameSync as renameSync4,
25515
- rmSync as rmSync3,
25719
+ renameSync as renameSync5,
25720
+ rmSync as rmSync4,
25516
25721
  statSync as statSync5,
25517
25722
  unlinkSync as unlinkSync6
25518
25723
  } from "fs";
25519
- 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";
25520
25725
  import { Readable as Readable2 } from "stream";
25521
25726
  import { pipeline as pipeline2 } from "stream/promises";
25522
25727
 
@@ -25606,25 +25811,25 @@ function detectHostPlatform() {
25606
25811
 
25607
25812
  // src/lsp-github-install.ts
25608
25813
  function ghCacheRoot() {
25609
- return join14(aftCacheBase(), "lsp-binaries");
25814
+ return join15(aftCacheBase(), "lsp-binaries");
25610
25815
  }
25611
25816
  function ghPackageDir(spec) {
25612
- return join14(ghCacheRoot(), spec.id);
25817
+ return join15(ghCacheRoot(), spec.id);
25613
25818
  }
25614
25819
  function ghBinDir(spec) {
25615
- return join14(ghPackageDir(spec), "bin");
25820
+ return join15(ghPackageDir(spec), "bin");
25616
25821
  }
25617
25822
  function ghExtractDir(spec) {
25618
- return join14(ghPackageDir(spec), "extracted");
25823
+ return join15(ghPackageDir(spec), "extracted");
25619
25824
  }
25620
25825
  function ghBinaryPath(spec, platform2) {
25621
25826
  const ext = platform2 === "win32" ? ".exe" : "";
25622
- return join14(ghBinDir(spec), `${spec.binary}${ext}`);
25827
+ return join15(ghBinDir(spec), `${spec.binary}${ext}`);
25623
25828
  }
25624
25829
  function isGithubInstalled(spec, platform2) {
25625
25830
  for (const candidate of ghBinaryCandidates(spec, platform2)) {
25626
25831
  try {
25627
- if (statSync5(join14(ghBinDir(spec), candidate)).isFile())
25832
+ if (statSync5(join15(ghBinDir(spec), candidate)).isFile())
25628
25833
  return true;
25629
25834
  } catch {}
25630
25835
  }
@@ -25646,6 +25851,9 @@ function sha256OfFile(path2) {
25646
25851
  stream.on("end", () => resolve4(hash2.digest("hex")));
25647
25852
  });
25648
25853
  }
25854
+ function sha256OfFileSync2(path2) {
25855
+ return createHash4("sha256").update(readFileSync9(path2)).digest("hex");
25856
+ }
25649
25857
  async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
25650
25858
  const candidates = [];
25651
25859
  candidates.push(tag);
@@ -25666,11 +25874,7 @@ async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
25666
25874
  const url2 = `https://api.github.com/repos/${githubRepo}/releases/tags/${encodeURIComponent(candidate)}`;
25667
25875
  const timeout = controlledTimeoutSignal(15000, signal);
25668
25876
  try {
25669
- const res = await fetchImpl(url2, {
25670
- headers,
25671
- redirect: "follow",
25672
- signal: timeout.signal
25673
- });
25877
+ const res = await fetchJsonFollowingRedirects(url2, headers, fetchImpl, timeout.signal);
25674
25878
  if (res.status === 404)
25675
25879
  continue;
25676
25880
  if (!res.ok) {
@@ -25700,6 +25904,23 @@ async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
25700
25904
  }
25701
25905
  return null;
25702
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
+ }
25703
25924
  async function resolveTargetTag(spec, config2, fetchImpl, signal) {
25704
25925
  const pinned = config2.versions[spec.githubRepo];
25705
25926
  if (pinned) {
@@ -25794,17 +26015,12 @@ function assertAllowedDownloadUrl(rawUrl) {
25794
26015
  return parsed;
25795
26016
  }
25796
26017
  async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
25797
- assertAllowedDownloadUrl(url2);
25798
26018
  if (assetSize !== undefined && assetSize > MAX_DOWNLOAD_BYTES2) {
25799
26019
  throw new Error(`asset size ${assetSize} exceeds max ${MAX_DOWNLOAD_BYTES2} (set lsp.versions to pin a smaller release if this is wrong)`);
25800
26020
  }
25801
26021
  const timeout = controlledTimeoutSignal(120000, signal);
25802
26022
  try {
25803
- const res = await fetchImpl(url2, {
25804
- headers: { accept: "application/octet-stream" },
25805
- redirect: "follow",
25806
- signal: timeout.signal
25807
- });
26023
+ const res = await fetchFollowingRedirects(url2, fetchImpl, timeout.signal);
25808
26024
  if (!res.ok || !res.body) {
25809
26025
  throw new Error(`download failed (${res.status})`);
25810
26026
  }
@@ -25812,7 +26028,7 @@ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
25812
26028
  if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES2) {
25813
26029
  throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES2}`);
25814
26030
  }
25815
- mkdirSync7(dirname5(destPath), { recursive: true });
26031
+ mkdirSync8(dirname5(destPath), { recursive: true });
25816
26032
  let bytesWritten = 0;
25817
26033
  const guard = new TransformStream({
25818
26034
  transform(chunk, controller) {
@@ -25836,6 +26052,27 @@ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
25836
26052
  timeout.cleanup();
25837
26053
  }
25838
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
+ }
25839
26076
  function validateExtraction(stagingRoot) {
25840
26077
  const realStagingRoot = realpathSync3(stagingRoot);
25841
26078
  let totalBytes = 0;
@@ -25847,7 +26084,7 @@ function validateExtraction(stagingRoot) {
25847
26084
  throw new Error(`failed to read staging dir ${dir}: ${err}`);
25848
26085
  }
25849
26086
  for (const entry of entries) {
25850
- const full = join14(dir, entry);
26087
+ const full = join15(dir, entry);
25851
26088
  let lst;
25852
26089
  try {
25853
26090
  lst = lstatSync2(full);
@@ -25885,27 +26122,83 @@ function validateExtraction(stagingRoot) {
25885
26122
  };
25886
26123
  walk(realStagingRoot);
25887
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
+ }
25888
26148
  function extractArchiveSafely(archivePath, destDir, archiveType) {
25889
26149
  const suffix = randomBytes(8).toString("hex");
25890
26150
  const stagingDir = `${destDir}.staging-${suffix}`;
25891
26151
  try {
25892
- rmSync3(stagingDir, { recursive: true, force: true });
26152
+ rmSync4(stagingDir, { recursive: true, force: true });
25893
26153
  } catch {}
25894
- mkdirSync7(stagingDir, { recursive: true });
26154
+ mkdirSync8(stagingDir, { recursive: true });
25895
26155
  try {
26156
+ precheckArchiveSize(archivePath, archiveType);
25896
26157
  runPlatformExtractor(archivePath, stagingDir, archiveType);
25897
26158
  validateExtraction(stagingDir);
25898
26159
  try {
25899
- rmSync3(destDir, { recursive: true, force: true });
26160
+ rmSync4(destDir, { recursive: true, force: true });
25900
26161
  } catch {}
25901
- renameSync4(stagingDir, destDir);
26162
+ renameSync5(stagingDir, destDir);
25902
26163
  } catch (err) {
25903
26164
  try {
25904
- rmSync3(stagingDir, { recursive: true, force: true });
26165
+ rmSync4(stagingDir, { recursive: true, force: true });
25905
26166
  } catch {}
25906
26167
  throw err;
25907
26168
  }
25908
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
+ }
25909
26202
  function runPlatformExtractor(archivePath, destDir, archiveType) {
25910
26203
  if (archiveType === "zip") {
25911
26204
  if (process.platform === "win32") {
@@ -25951,7 +26244,7 @@ async function downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl,
25951
26244
  }
25952
26245
  const pkgDir = ghPackageDir(spec);
25953
26246
  const extractDir = ghExtractDir(spec);
25954
- const archivePath = join14(pkgDir, expected.name);
26247
+ const archivePath = join15(pkgDir, expected.name);
25955
26248
  log2(`[lsp] downloading ${spec.id} ${tag} \u2192 ${matchingAsset.url}`);
25956
26249
  try {
25957
26250
  await downloadFile(matchingAsset.url, archivePath, fetchImpl, matchingAsset.size, signal);
@@ -25990,13 +26283,13 @@ async function downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl,
25990
26283
  unlinkSync6(archivePath);
25991
26284
  } catch {}
25992
26285
  }
25993
- const innerBinaryPath = join14(extractDir, spec.binaryPathInArchive(platform2, arch, version2));
26286
+ const innerBinaryPath = join15(extractDir, spec.binaryPathInArchive(platform2, arch, version2));
25994
26287
  if (!existsSync10(innerBinaryPath)) {
25995
26288
  error2(`[lsp] ${spec.id}: extracted binary not found at ${innerBinaryPath}`);
25996
26289
  return null;
25997
26290
  }
25998
26291
  const targetBinary = ghBinaryPath(spec, platform2);
25999
- mkdirSync7(dirname5(targetBinary), { recursive: true });
26292
+ mkdirSync8(dirname5(targetBinary), { recursive: true });
26000
26293
  try {
26001
26294
  copyFileSync3(innerBinaryPath, targetBinary);
26002
26295
  if (platform2 !== "win32") {
@@ -26087,7 +26380,7 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
26087
26380
  };
26088
26381
  }
26089
26382
  for (const spec of GITHUB_LSP_TABLE) {
26090
- if (isGithubInstalled(spec, host.platform)) {
26383
+ if (isGithubInstalled(spec, host.platform) && validateCachedGithubInstall(spec, host.platform)) {
26091
26384
  cachedBinDirs.push(ghBinDir(spec));
26092
26385
  }
26093
26386
  if (config2.disabled.has(spec.id)) {
@@ -26236,9 +26529,9 @@ function normalizeToolMap(tools) {
26236
26529
  }
26237
26530
 
26238
26531
  // src/notifications.ts
26239
- 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";
26240
26533
  import { homedir as homedir9, platform as platform2 } from "os";
26241
- import { join as join15 } from "path";
26534
+ import { join as join16 } from "path";
26242
26535
  function isTuiMode() {
26243
26536
  return process.env.OPENCODE_CLIENT === "cli";
26244
26537
  }
@@ -26262,15 +26555,15 @@ function getDesktopStatePath() {
26262
26555
  const os2 = platform2();
26263
26556
  const home = homedir9();
26264
26557
  if (os2 === "darwin") {
26265
- 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");
26266
26559
  }
26267
26560
  if (os2 === "linux") {
26268
- const xdgConfig = process.env.XDG_CONFIG_HOME || join15(home, ".config");
26269
- 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");
26270
26563
  }
26271
26564
  if (os2 === "win32") {
26272
- const appData = process.env.APPDATA || join15(home, "AppData", "Roaming");
26273
- 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");
26274
26567
  }
26275
26568
  return null;
26276
26569
  }
@@ -26280,7 +26573,7 @@ function readDesktopState(directory) {
26280
26573
  return { sessionId: null, serverUrl: null };
26281
26574
  }
26282
26575
  try {
26283
- const raw = readFileSync8(statePath, "utf-8");
26576
+ const raw = readFileSync10(statePath, "utf-8");
26284
26577
  const state = JSON.parse(raw);
26285
26578
  let serverUrl = null;
26286
26579
  const serverStr = state.server;
@@ -26374,10 +26667,10 @@ async function sendWarning(opts, message) {
26374
26667
  }
26375
26668
  async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
26376
26669
  if (storageDir) {
26377
- const versionFile = join15(storageDir, "last_announced_version");
26670
+ const versionFile = join16(storageDir, "last_announced_version");
26378
26671
  try {
26379
26672
  if (existsSync11(versionFile)) {
26380
- const lastVersion = readFileSync8(versionFile, "utf-8").trim();
26673
+ const lastVersion = readFileSync10(versionFile, "utf-8").trim();
26381
26674
  if (lastVersion === version2)
26382
26675
  return;
26383
26676
  }
@@ -26397,17 +26690,17 @@ async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
26397
26690
  }
26398
26691
  if (storageDir) {
26399
26692
  try {
26400
- mkdirSync8(storageDir, { recursive: true });
26401
- writeFileSync8(join15(storageDir, "last_announced_version"), version2);
26693
+ mkdirSync9(storageDir, { recursive: true });
26694
+ writeFileSync8(join16(storageDir, "last_announced_version"), version2);
26402
26695
  } catch {}
26403
26696
  }
26404
26697
  }
26405
26698
  function readWarnedTools(storageDir) {
26406
26699
  try {
26407
- const warnedToolsPath = join15(storageDir, WARNED_TOOLS_FILE);
26700
+ const warnedToolsPath = join16(storageDir, WARNED_TOOLS_FILE);
26408
26701
  if (!existsSync11(warnedToolsPath))
26409
26702
  return {};
26410
- const parsed = JSON.parse(readFileSync8(warnedToolsPath, "utf-8"));
26703
+ const parsed = JSON.parse(readFileSync10(warnedToolsPath, "utf-8"));
26411
26704
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
26412
26705
  return {};
26413
26706
  const warned = {};
@@ -26423,12 +26716,34 @@ function readWarnedTools(storageDir) {
26423
26716
  }
26424
26717
  function writeWarnedTools(storageDir, warned) {
26425
26718
  try {
26426
- mkdirSync8(storageDir, { recursive: true });
26427
- const warnedToolsPath = join15(storageDir, WARNED_TOOLS_FILE);
26428
- 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)}
26429
26723
  `);
26724
+ renameSync6(tmpPath, warnedToolsPath);
26430
26725
  } catch {}
26431
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
+ }
26432
26747
  function warningKey(warning, projectRoot) {
26433
26748
  const scope = warning.kind === "lsp_binary_missing" ? "_" : projectRoot ?? "_";
26434
26749
  return [
@@ -26467,20 +26782,29 @@ ${warning.hint}`;
26467
26782
  async function deliverConfigureWarnings(opts, warnings) {
26468
26783
  if (warnings.length === 0)
26469
26784
  return;
26470
- const warned = readWarnedTools(opts.storageDir);
26471
- 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;
26472
26806
  for (const warning of warnings) {
26473
- const key = warningKey(warning, opts.projectRoot);
26474
- if (Object.hasOwn(warned, key))
26475
- continue;
26476
- const delivered = await sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning));
26477
- if (!delivered)
26478
- continue;
26479
- warned[key] = opts.pluginVersion;
26480
- changed = true;
26481
- }
26482
- if (changed) {
26483
- writeWarnedTools(opts.storageDir, warned);
26807
+ await sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning));
26484
26808
  }
26485
26809
  }
26486
26810
  async function cleanupWarnings(opts) {
@@ -26518,20 +26842,20 @@ async function cleanupWarnings(opts) {
26518
26842
 
26519
26843
  // src/shared/rpc-server.ts
26520
26844
  import { randomBytes as randomBytes2 } from "crypto";
26521
- 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";
26522
26846
  import { createServer } from "http";
26523
26847
  import { dirname as dirname6 } from "path";
26524
26848
 
26525
26849
  // src/shared/rpc-utils.ts
26526
26850
  import { createHash as createHash5 } from "crypto";
26527
- import { join as join16 } from "path";
26851
+ import { join as join17 } from "path";
26528
26852
  function projectHash(directory) {
26529
26853
  const normalized = directory.replace(/\/+$/, "");
26530
26854
  return createHash5("sha256").update(normalized).digest("hex").slice(0, 16);
26531
26855
  }
26532
26856
  function rpcPortFilePath(storageDir, directory) {
26533
26857
  const hash2 = projectHash(directory);
26534
- return join16(storageDir, "rpc", hash2, "port");
26858
+ return join17(storageDir, "rpc", hash2, "port");
26535
26859
  }
26536
26860
 
26537
26861
  // src/shared/rpc-server.ts
@@ -26565,10 +26889,13 @@ class AftRpcServer {
26565
26889
  this.server = server;
26566
26890
  try {
26567
26891
  const dir = dirname6(this.portFilePath);
26568
- mkdirSync9(dir, { recursive: true });
26892
+ mkdirSync10(dir, { recursive: true, mode: 448 });
26569
26893
  const tmpPath = `${this.portFilePath}.tmp`;
26570
- writeFileSync9(tmpPath, JSON.stringify({ port: this.port, token: this.token }), "utf-8");
26571
- 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);
26572
26899
  log2(`RPC server listening on 127.0.0.1:${this.port}`);
26573
26900
  } catch (err) {
26574
26901
  warn2(`Failed to write RPC port file: ${err}`);
@@ -26645,18 +26972,6 @@ class AftRpcServer {
26645
26972
  }
26646
26973
  }
26647
26974
 
26648
- // src/shared/runtime.ts
26649
- var GLOBAL_KEY = "__AFT_SHARED_BRIDGE_POOL__";
26650
- function getGlobalState() {
26651
- return globalThis;
26652
- }
26653
- function setSharedBridgePool(pool) {
26654
- getGlobalState()[GLOBAL_KEY] = pool;
26655
- }
26656
- function clearSharedBridgePool() {
26657
- getGlobalState()[GLOBAL_KEY] = null;
26658
- }
26659
-
26660
26975
  // src/shared/session-directory.ts
26661
26976
  var CACHE_MAX_ENTRIES = 200;
26662
26977
  var cache = new Map;
@@ -26863,21 +27178,21 @@ function formatStatusMarkdown(status) {
26863
27178
 
26864
27179
  // src/shared/tui-config.ts
26865
27180
  var import_comment_json4 = __toESM(require_src2(), 1);
26866
- import { existsSync as existsSync12, mkdirSync as mkdirSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync10 } from "fs";
26867
- 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";
26868
27183
 
26869
27184
  // src/shared/opencode-config-dir.ts
26870
27185
  import { homedir as homedir10 } from "os";
26871
- import { join as join17, resolve as resolve4 } from "path";
27186
+ import { join as join18, resolve as resolve4 } from "path";
26872
27187
  function getCliConfigDir() {
26873
27188
  const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
26874
27189
  if (envConfigDir) {
26875
27190
  return resolve4(envConfigDir);
26876
27191
  }
26877
27192
  if (process.platform === "win32") {
26878
- return join17(homedir10(), ".config", "opencode");
27193
+ return join18(homedir10(), ".config", "opencode");
26879
27194
  }
26880
- return join17(process.env.XDG_CONFIG_HOME || join17(homedir10(), ".config"), "opencode");
27195
+ return join18(process.env.XDG_CONFIG_HOME || join18(homedir10(), ".config"), "opencode");
26881
27196
  }
26882
27197
  function getOpenCodeConfigDir2(_options) {
26883
27198
  return getCliConfigDir();
@@ -26886,10 +27201,10 @@ function getOpenCodeConfigPaths(options) {
26886
27201
  const configDir = getOpenCodeConfigDir2(options);
26887
27202
  return {
26888
27203
  configDir,
26889
- configJson: join17(configDir, "opencode.json"),
26890
- configJsonc: join17(configDir, "opencode.jsonc"),
26891
- packageJson: join17(configDir, "package.json"),
26892
- 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")
26893
27208
  };
26894
27209
  }
26895
27210
 
@@ -26898,8 +27213,8 @@ var PLUGIN_NAME = "@cortexkit/aft-opencode";
26898
27213
  var PLUGIN_ENTRY = `${PLUGIN_NAME}@latest`;
26899
27214
  function resolveTuiConfigPath() {
26900
27215
  const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
26901
- const jsoncPath = join18(configDir, "tui.jsonc");
26902
- const jsonPath = join18(configDir, "tui.json");
27216
+ const jsoncPath = join19(configDir, "tui.jsonc");
27217
+ const jsonPath = join19(configDir, "tui.json");
26903
27218
  if (existsSync12(jsoncPath))
26904
27219
  return jsoncPath;
26905
27220
  if (existsSync12(jsonPath))
@@ -26911,7 +27226,7 @@ function ensureTuiPluginEntry() {
26911
27226
  const configPath = resolveTuiConfigPath();
26912
27227
  let config2 = {};
26913
27228
  if (existsSync12(configPath)) {
26914
- config2 = import_comment_json4.parse(readFileSync9(configPath, "utf-8")) ?? {};
27229
+ config2 = import_comment_json4.parse(readFileSync11(configPath, "utf-8")) ?? {};
26915
27230
  }
26916
27231
  const plugins = Array.isArray(config2.plugin) ? config2.plugin.filter((value) => typeof value === "string") : [];
26917
27232
  if (plugins.some((plugin) => plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`) || plugin.includes("opencode-plugin") || plugin.includes("aft-opencode"))) {
@@ -26919,7 +27234,7 @@ function ensureTuiPluginEntry() {
26919
27234
  }
26920
27235
  plugins.push(PLUGIN_ENTRY);
26921
27236
  config2.plugin = plugins;
26922
- mkdirSync10(dirname7(configPath), { recursive: true });
27237
+ mkdirSync11(dirname7(configPath), { recursive: true });
26923
27238
  writeFileSync10(configPath, `${import_comment_json4.stringify(config2, null, 2)}
26924
27239
  `);
26925
27240
  log2(`[aft-plugin] added TUI plugin entry to ${configPath}`);
@@ -26931,13 +27246,13 @@ function ensureTuiPluginEntry() {
26931
27246
  }
26932
27247
 
26933
27248
  // src/shutdown-hooks.ts
26934
- var GLOBAL_KEY2 = "__aftShutdownHooks__";
27249
+ var GLOBAL_KEY = "__aftShutdownHooks__";
26935
27250
  function getState() {
26936
27251
  const g = globalThis;
26937
- if (!g[GLOBAL_KEY2]) {
26938
- g[GLOBAL_KEY2] = { cleanups: new Set, installed: false };
27252
+ if (!g[GLOBAL_KEY]) {
27253
+ g[GLOBAL_KEY] = { cleanups: new Set, installed: false };
26939
27254
  }
26940
- return g[GLOBAL_KEY2];
27255
+ return g[GLOBAL_KEY];
26941
27256
  }
26942
27257
  var shuttingDown = false;
26943
27258
  async function runCleanups(reason) {
@@ -27039,8 +27354,10 @@ async function callBridge(ctx, runtime, command, params = {}, options) {
27039
27354
  }
27040
27355
 
27041
27356
  // src/tools/permissions.ts
27357
+ import * as fs3 from "fs";
27042
27358
  import * as path3 from "path";
27043
27359
  import { Effect } from "effect";
27360
+ var UNSUPPORTED_ASK_HOST = "AFT requires OpenCode 1.14.39 or newer for permission asks; please upgrade OpenCode";
27044
27361
  async function runAsk(maybe) {
27045
27362
  await Effect.runPromise(maybe);
27046
27363
  }
@@ -27068,6 +27385,8 @@ function workspacePattern(_context) {
27068
27385
  return ".";
27069
27386
  }
27070
27387
  async function askEditPermission(context, patterns, metadata = {}) {
27388
+ if (typeof context.ask !== "function")
27389
+ return UNSUPPORTED_ASK_HOST;
27071
27390
  try {
27072
27391
  await runAsk(context.ask({
27073
27392
  permission: "edit",
@@ -27083,6 +27402,110 @@ async function askEditPermission(context, patterns, metadata = {}) {
27083
27402
  return "Permission denied.";
27084
27403
  }
27085
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
+ }
27086
27509
  function permissionDeniedResponse(message) {
27087
27510
  return JSON.stringify({
27088
27511
  success: false,
@@ -27102,6 +27525,17 @@ function extractHint(response) {
27102
27525
  const hint = response.hint;
27103
27526
  return typeof hint === "string" && hint.length > 0 ? hint : null;
27104
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
+ }
27105
27539
  var SUPPORTED_LANGS = ["typescript", "tsx", "javascript", "python", "rust", "go"];
27106
27540
  function astTools(ctx) {
27107
27541
  const searchTool = {
@@ -27122,6 +27556,9 @@ function astTools(ctx) {
27122
27556
  contextLines: z2.number().optional().describe("Number of context lines to show around each match")
27123
27557
  },
27124
27558
  execute: async (args, context) => {
27559
+ const externalDenied = await checkAstPathsPermission(context, args.paths);
27560
+ if (externalDenied)
27561
+ return permissionDeniedResponse(externalDenied);
27125
27562
  const params = {
27126
27563
  pattern: args.pattern,
27127
27564
  lang: args.lang
@@ -27215,16 +27652,32 @@ ${hint}`;
27215
27652
  },
27216
27653
  execute: async (args, context) => {
27217
27654
  const isDryRun = args.dryRun === true;
27655
+ const externalDenied = await checkAstPathsPermission(context, args.paths);
27656
+ if (externalDenied)
27657
+ return permissionDeniedResponse(externalDenied);
27218
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
+ }
27219
27674
  const explicitPaths = Array.isArray(args.paths) ? resolveRelativePatterns(context, args.paths) : [];
27220
27675
  const positiveGlobs = Array.isArray(args.globs) ? args.globs.filter((glob) => !glob.startsWith("!")) : [];
27221
27676
  const patterns = [...explicitPaths, ...positiveGlobs];
27222
27677
  const metadata = explicitPaths.length === 1 && positiveGlobs.length === 0 && Array.isArray(args.paths) ? { filepath: resolveAbsolutePath(context, args.paths[0]) } : {};
27223
27678
  const permissionError = await askEditPermission(context, patterns.length > 0 ? patterns : [workspacePattern(context)], metadata);
27224
27679
  if (permissionError) {
27225
- const output2 = `Permission denied: ${permissionError}`;
27226
- showOutputToUser(context, output2);
27227
- return output2;
27680
+ return permissionDeniedResponse(permissionError);
27228
27681
  }
27229
27682
  }
27230
27683
  const params = {
@@ -27358,7 +27811,7 @@ function conflictTools(ctx) {
27358
27811
  }
27359
27812
 
27360
27813
  // src/tools/hoisted.ts
27361
- import * as fs3 from "fs";
27814
+ import * as fs4 from "fs";
27362
27815
  import * as path4 from "path";
27363
27816
  import { tool as tool4 } from "@opencode-ai/plugin";
27364
27817
 
@@ -27655,11 +28108,65 @@ ${chunk.old_lines.join(`
27655
28108
 
27656
28109
  // src/tools/bash.ts
27657
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
+ sessionLog2(sessionId, `[subagent-detect] cache hit: isSubagent=${cached2.isSubagent}`);
28125
+ return cached2.isSubagent;
28126
+ }
28127
+ const c = client;
28128
+ const sessionApi = c?.session;
28129
+ if (!sessionApi || typeof sessionApi.get !== "function") {
28130
+ sessionLog2(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) \u2192 caching as primary`);
28131
+ setCache2(sessionId, false);
28132
+ return false;
28133
+ }
28134
+ sessionLog2(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
28135
+ let isSubagent = false;
28136
+ let parentIdRaw;
28137
+ try {
28138
+ const result = await sessionApi.get({
28139
+ path: { id: sessionId }
28140
+ });
28141
+ const session = result?.data ?? result;
28142
+ parentIdRaw = session?.parentID;
28143
+ isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
28144
+ sessionLog2(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} \u2192 isSubagent=${isSubagent}`);
28145
+ } catch (err) {
28146
+ sessionWarn2(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
28147
+ return false;
28148
+ }
28149
+ setCache2(sessionId, isSubagent);
28150
+ return isSubagent;
28151
+ }
28152
+ function setCache2(sessionId, isSubagent) {
28153
+ if (cache2.has(sessionId))
28154
+ cache2.delete(sessionId);
28155
+ cache2.set(sessionId, { isSubagent, recordedAt: Date.now() });
28156
+ if (cache2.size > CACHE_MAX_ENTRIES2) {
28157
+ const oldest = cache2.keys().next().value;
28158
+ if (oldest !== undefined)
28159
+ cache2.delete(oldest);
28160
+ }
28161
+ }
28162
+
28163
+ // src/tools/bash.ts
27658
28164
  var z3 = tool3.schema;
27659
28165
  var METADATA_PREVIEW_LIMIT = 30 * 1024;
27660
28166
  var FOREGROUND_WAIT_WINDOW_MS = 5000;
27661
28167
  var FOREGROUND_POLL_INTERVAL_MS = 100;
27662
28168
  var BASH_TRANSPORT_TIMEOUT_MS = 30000;
28169
+ var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
27663
28170
  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.`;
27664
28171
  async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
27665
28172
  const first = await bridgeCall(ctx, runtime, "bash", params, options);
@@ -27700,6 +28207,13 @@ function createBashTool(ctx) {
27700
28207
  const metadata = context.metadata;
27701
28208
  const command = args.command;
27702
28209
  const cwd = args.workdir ?? context.directory;
28210
+ const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
28211
+ const requestedBackground = args.background === true;
28212
+ const effectiveBackground = isSubagent ? false : requestedBackground;
28213
+ sessionLog2(context.sessionID, `[bash] subagent gate: isSubagent=${isSubagent}, requestedBackground=${requestedBackground}, effectiveBackground=${effectiveBackground}, hasTimeout=${typeof args.timeout === "number"}`);
28214
+ if (isSubagent && requestedBackground) {
28215
+ sessionLog2(context.sessionID, "[bash] subagent + background:true \u2192 converting to foreground (subagent would lose task_id)");
28216
+ }
27703
28217
  const shellEnv = await ctx.plugin?.trigger?.("shell.env", { cwd, sessionID: context.sessionID, callID: getCallID(context) }, { env: {} });
27704
28218
  const data = await withPermissionLoop(ctx, context, {
27705
28219
  command,
@@ -27707,8 +28221,8 @@ function createBashTool(ctx) {
27707
28221
  workdir: args.workdir,
27708
28222
  env: shellEnv?.env ?? {},
27709
28223
  description,
27710
- background: args.background,
27711
- notify_on_completion: args.background === true,
28224
+ background: effectiveBackground,
28225
+ notify_on_completion: effectiveBackground,
27712
28226
  compressed: args.compressed,
27713
28227
  permissions_requested: true
27714
28228
  }, callBridge, {
@@ -27725,7 +28239,7 @@ function createBashTool(ctx) {
27725
28239
  if (data.status === "running" && typeof data.task_id === "string") {
27726
28240
  const callID2 = getCallID(context);
27727
28241
  const taskId = data.task_id;
27728
- if (args.background === true) {
28242
+ if (effectiveBackground) {
27729
28243
  trackBgTask(context.sessionID, taskId);
27730
28244
  const startedLine = formatBackgroundLaunch(taskId);
27731
28245
  const metadataPayload2 = { description, output: startedLine, status: "running", taskId };
@@ -27739,7 +28253,7 @@ function createBashTool(ctx) {
27739
28253
  return startedLine;
27740
28254
  }
27741
28255
  const argTimeout = args.timeout;
27742
- const waitTimeoutMs = argTimeout !== undefined ? Math.min(argTimeout, FOREGROUND_WAIT_WINDOW_MS) : FOREGROUND_WAIT_WINDOW_MS;
28256
+ const waitTimeoutMs = isSubagent ? argTimeout ?? DEFAULT_HARD_TIMEOUT_MS : argTimeout !== undefined ? Math.min(argTimeout, FOREGROUND_WAIT_WINDOW_MS) : FOREGROUND_WAIT_WINDOW_MS;
27743
28257
  const startedAt = Date.now();
27744
28258
  while (true) {
27745
28259
  const status = await callBridge(ctx, context, "bash_status", { task_id: taskId });
@@ -27820,7 +28334,7 @@ function createBashStatusTool(ctx) {
27820
28334
  return {
27821
28335
  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.",
27822
28336
  args: {
27823
- taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bgb-6b454047.")
28337
+ taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047.")
27824
28338
  },
27825
28339
  execute: async (args, context) => {
27826
28340
  const data = await callBridge(ctx, context, "bash_status", {
@@ -27850,7 +28364,7 @@ function createBashKillTool(ctx) {
27850
28364
  return {
27851
28365
  description: "Terminate a running background bash task spawned with bash({ background: true }). Returns confirmation of kill or an error if the task already finished.",
27852
28366
  args: {
27853
- taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bgb-6b454047.")
28367
+ taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047.")
27854
28368
  },
27855
28369
  execute: async (args, context) => {
27856
28370
  const data = await callBridge(ctx, context, "bash_kill", {
@@ -28161,12 +28675,23 @@ function createReadTool(ctx) {
28161
28675
  execute: async (args, context) => {
28162
28676
  const file2 = args.filePath;
28163
28677
  const filePath = path4.isAbsolute(file2) ? file2 : path4.resolve(context.directory, file2);
28164
- await runAsk(context.ask({
28165
- permission: "read",
28166
- patterns: [filePath],
28167
- always: ["*"],
28168
- metadata: {}
28169
- }));
28678
+ {
28679
+ const denial = await assertExternalDirectoryPermission(context, filePath);
28680
+ if (denial)
28681
+ return permissionDeniedResponse(denial);
28682
+ }
28683
+ try {
28684
+ await runAsk(context.ask({
28685
+ permission: "read",
28686
+ patterns: [filePath],
28687
+ always: ["*"],
28688
+ metadata: {}
28689
+ }));
28690
+ } catch (error50) {
28691
+ if (error50 instanceof Error && error50.message)
28692
+ return permissionDeniedResponse(error50.message);
28693
+ return permissionDeniedResponse("Permission denied.");
28694
+ }
28170
28695
  const ext = path4.extname(filePath).toLowerCase();
28171
28696
  const mimeMap = {
28172
28697
  ".png": "image/png",
@@ -28189,7 +28714,7 @@ function createReadTool(ctx) {
28189
28714
  const label = isImage ? "Image" : "PDF";
28190
28715
  let fileSize = 0;
28191
28716
  try {
28192
- const stat = await import("fs/promises").then((fs4) => fs4.stat(filePath));
28717
+ const stat = await import("fs/promises").then((fs5) => fs5.stat(filePath));
28193
28718
  fileSize = stat.size;
28194
28719
  } catch {}
28195
28720
  const sizeStr = fileSize > 1048576 ? `${(fileSize / 1048576).toFixed(1)}MB` : fileSize > 1024 ? `${(fileSize / 1024).toFixed(0)}KB` : `${fileSize} bytes`;
@@ -28285,6 +28810,11 @@ function createWriteTool(ctx, editToolName = "edit") {
28285
28810
  const content = args.content;
28286
28811
  const filePath = path4.isAbsolute(file2) ? file2 : path4.resolve(context.directory, file2);
28287
28812
  const relPath = path4.relative(context.worktree, filePath);
28813
+ {
28814
+ const denial = await assertExternalDirectoryPermission(context, filePath);
28815
+ if (denial)
28816
+ return permissionDeniedResponse(denial);
28817
+ }
28288
28818
  await runAsk(context.ask({
28289
28819
  permission: "edit",
28290
28820
  patterns: [relPath],
@@ -28405,9 +28935,9 @@ Note: Modes 6 and 7 are options on mode 5 (find/replace) \u2014 they require \`o
28405
28935
  - Auto-formats using project formatter if configured
28406
28936
  - Tree-sitter syntax validation on all edits
28407
28937
  - Symbol replace includes decorators, attributes, and doc comments in range
28408
- - LSP error-level diagnostics are returned automatically after non-dry-run edits
28938
+ - LSP error-level diagnostics are returned automatically after edits
28409
28939
 
28410
- Returns: JSON string for the selected edit mode. Dry runs return diff data; non-dry-run edits may append inline LSP error lines.
28940
+ Returns: JSON string for the selected edit mode. Edits may append inline LSP error lines.
28411
28941
 
28412
28942
  Common response fields: success (boolean), diff (object with before/after), backup_id (string), syntax_valid (boolean). Exact fields vary by mode.`;
28413
28943
  }
@@ -28424,8 +28954,7 @@ function createEditTool(ctx, writeToolName = "write") {
28424
28954
  content: z4.string().optional().describe("New content for symbol replace or file write"),
28425
28955
  appendContent: z4.string().optional().describe("Text to append to the end of filePath; creates the file if needed"),
28426
28956
  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 }"),
28427
- 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)"),
28428
- dryRun: z4.boolean().optional().describe("Preview changes without applying (returns diff, default: false)")
28957
+ 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)")
28429
28958
  },
28430
28959
  execute: async (args, context) => {
28431
28960
  const argsRecord = args;
@@ -28435,6 +28964,18 @@ function createEditTool(ctx, writeToolName = "write") {
28435
28964
  if (Array.isArray(args.operations)) {
28436
28965
  const ops = args.operations;
28437
28966
  const files = ops.map((op) => op.file).filter(Boolean);
28967
+ {
28968
+ const asked = new Set;
28969
+ for (const file3 of files) {
28970
+ const absPath = path4.isAbsolute(file3) ? file3 : path4.resolve(context.directory, file3);
28971
+ if (asked.has(absPath))
28972
+ continue;
28973
+ asked.add(absPath);
28974
+ const denial = await assertExternalDirectoryPermission(context, absPath);
28975
+ if (denial)
28976
+ return permissionDeniedResponse(denial);
28977
+ }
28978
+ }
28438
28979
  await runAsk(context.ask({
28439
28980
  permission: "edit",
28440
28981
  patterns: files.map((f) => path4.relative(context.worktree, path4.resolve(context.directory, f))),
@@ -28445,9 +28986,7 @@ function createEditTool(ctx, writeToolName = "write") {
28445
28986
  ...op,
28446
28987
  file: path4.isAbsolute(op.file) ? op.file : path4.resolve(context.directory, op.file)
28447
28988
  }));
28448
- const params2 = { operations: resolvedOps };
28449
- params2.dry_run = args.dryRun === true;
28450
- const data2 = await callBridge(ctx, context, "transaction", params2);
28989
+ const data2 = await callBridge(ctx, context, "transaction", { operations: resolvedOps });
28451
28990
  return JSON.stringify(data2);
28452
28991
  }
28453
28992
  const file2 = args.filePath;
@@ -28455,6 +28994,11 @@ function createEditTool(ctx, writeToolName = "write") {
28455
28994
  throw new Error("'filePath' parameter is required");
28456
28995
  const filePath = path4.isAbsolute(file2) ? file2 : path4.resolve(context.directory, file2);
28457
28996
  const relPath = path4.relative(context.worktree, filePath);
28997
+ {
28998
+ const denial = await assertExternalDirectoryPermission(context, filePath);
28999
+ if (denial)
29000
+ return permissionDeniedResponse(denial);
29001
+ }
28458
29002
  await runAsk(context.ask({
28459
29003
  permission: "edit",
28460
29004
  patterns: [relPath],
@@ -28503,14 +29047,10 @@ function createEditTool(ctx, writeToolName = "write") {
28503
29047
  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.";
28504
29048
  throw new Error(`edit: no edit mode resolved from arguments.${hint}`);
28505
29049
  }
28506
- if (args.dryRun)
28507
- params.dry_run = true;
28508
- if (!args.dryRun)
28509
- params.diagnostics = true;
28510
- if (!args.dryRun)
28511
- params.include_diff = true;
29050
+ params.diagnostics = true;
29051
+ params.include_diff = true;
28512
29052
  const data = await callBridge(ctx, context, command, params);
28513
- if (!args.dryRun && data.success && data.diff) {
29053
+ if (data.success && data.diff) {
28514
29054
  const diff = data.diff;
28515
29055
  const callID = getCallID2(context);
28516
29056
  if (callID) {
@@ -28539,31 +29079,29 @@ function createEditTool(ctx, writeToolName = "write") {
28539
29079
  result += `
28540
29080
 
28541
29081
  ${globSkipNote}`;
28542
- if (!args.dryRun) {
28543
- const diags = data.lsp_diagnostics;
28544
- if (diags && diags.length > 0) {
28545
- const errors3 = diags.filter((d) => d.severity === "error");
28546
- if (errors3.length > 0) {
28547
- const diagLines = errors3.map((d) => ` Line ${d.line}: ${d.message}`).join(`
29082
+ const diags = data.lsp_diagnostics;
29083
+ if (diags && diags.length > 0) {
29084
+ const errors3 = diags.filter((d) => d.severity === "error");
29085
+ if (errors3.length > 0) {
29086
+ const diagLines = errors3.map((d) => ` Line ${d.line}: ${d.message}`).join(`
28548
29087
  `);
28549
- result += `
29088
+ result += `
28550
29089
 
28551
29090
  LSP errors detected, please fix:
28552
29091
  ${diagLines}`;
28553
- }
28554
29092
  }
28555
- const pendingServers = data.lsp_pending_servers;
28556
- const exitedServers = data.lsp_exited_servers;
28557
- if (pendingServers && pendingServers.length > 0) {
28558
- result += `
29093
+ }
29094
+ const pendingServers = data.lsp_pending_servers;
29095
+ const exitedServers = data.lsp_exited_servers;
29096
+ if (pendingServers && pendingServers.length > 0) {
29097
+ result += `
28559
29098
 
28560
29099
  Note: LSP server(s) did not respond in time: ${pendingServers.join(", ")}. Diagnostics may be incomplete; rerun lsp_diagnostics later for a fresh check.`;
28561
- }
28562
- if (exitedServers && exitedServers.length > 0) {
28563
- result += `
29100
+ }
29101
+ if (exitedServers && exitedServers.length > 0) {
29102
+ result += `
28564
29103
 
28565
29104
  Note: LSP server(s) exited during this edit: ${exitedServers.join(", ")}. Their diagnostics could not be collected.`;
28566
- }
28567
29105
  }
28568
29106
  return result;
28569
29107
  }
@@ -28650,13 +29188,24 @@ function createApplyPatchTool(ctx) {
28650
29188
  if (h.type === "update" && h.move_path) {
28651
29189
  const dstAbs = path4.resolve(context.directory, h.move_path);
28652
29190
  affectedAbs.add(dstAbs);
28653
- if (!fs3.existsSync(dstAbs)) {
29191
+ if (!fs4.existsSync(dstAbs)) {
28654
29192
  newlyCreatedAbs.add(dstAbs);
28655
29193
  }
28656
29194
  }
28657
29195
  }
28658
29196
  const relPaths = Array.from(affectedAbs).map((abs) => path4.relative(context.worktree, abs));
28659
29197
  const multiFileWritePaths = Array.from(affectedAbs);
29198
+ {
29199
+ const asked = new Set;
29200
+ for (const filePath of multiFileWritePaths) {
29201
+ if (asked.has(filePath))
29202
+ continue;
29203
+ asked.add(filePath);
29204
+ const denial = await assertExternalDirectoryPermission(context, filePath);
29205
+ if (denial)
29206
+ return permissionDeniedResponse(denial);
29207
+ }
29208
+ }
28660
29209
  await runAsk(context.ask({
28661
29210
  permission: "edit",
28662
29211
  patterns: relPaths,
@@ -28682,7 +29231,7 @@ function createApplyPatchTool(ctx) {
28682
29231
  const filePath = path4.resolve(context.directory, hunk.path);
28683
29232
  switch (hunk.type) {
28684
29233
  case "add": {
28685
- if (fs3.existsSync(filePath)) {
29234
+ if (fs4.existsSync(filePath)) {
28686
29235
  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.`;
28687
29236
  results.push(msg);
28688
29237
  failures.push(hunk.path);
@@ -28714,7 +29263,7 @@ function createApplyPatchTool(ctx) {
28714
29263
  results.push(msg);
28715
29264
  failures.push(hunk.path);
28716
29265
  const filePath2 = path4.resolve(context.directory, hunk.path);
28717
- if (fs3.existsSync(filePath2)) {
29266
+ if (fs4.existsSync(filePath2)) {
28718
29267
  try {
28719
29268
  await callBridge(ctx, context, "delete_file", { file: filePath2 });
28720
29269
  } catch {}
@@ -28724,7 +29273,7 @@ function createApplyPatchTool(ctx) {
28724
29273
  }
28725
29274
  case "delete": {
28726
29275
  try {
28727
- const before = await fs3.promises.readFile(filePath, "utf-8").catch(() => "");
29276
+ const before = await fs4.promises.readFile(filePath, "utf-8").catch(() => "");
28728
29277
  await callBridge(ctx, context, "delete_file", { file: filePath });
28729
29278
  perFileDiffs.push({
28730
29279
  filePath,
@@ -28742,7 +29291,7 @@ function createApplyPatchTool(ctx) {
28742
29291
  }
28743
29292
  case "update": {
28744
29293
  try {
28745
- const original = await fs3.promises.readFile(filePath, "utf-8");
29294
+ const original = await fs4.promises.readFile(filePath, "utf-8");
28746
29295
  const newContent = applyUpdateChunks(original, filePath, hunk.chunks);
28747
29296
  const targetPath = hunk.move_path ? path4.resolve(context.directory, hunk.move_path) : filePath;
28748
29297
  const writeResult = await callBridge(ctx, context, "write", {
@@ -28797,7 +29346,7 @@ ${diagLines}`);
28797
29346
  if (rollbackResult.success === false) {
28798
29347
  throw new Error(rollbackResult.message ?? "checkpoint restore failed");
28799
29348
  }
28800
- if (newlyCreatedAbs.has(targetPath) && fs3.existsSync(targetPath)) {
29349
+ if (newlyCreatedAbs.has(targetPath) && fs4.existsSync(targetPath)) {
28801
29350
  const cleanupResult = await callBridge(ctx, context, "delete_file", {
28802
29351
  file: targetPath
28803
29352
  });
@@ -28892,6 +29441,17 @@ function createDeleteTool(ctx) {
28892
29441
  execute: async (args, context) => {
28893
29442
  const inputs = args.files;
28894
29443
  const absolutePaths = inputs.map((f) => path4.isAbsolute(f) ? f : path4.resolve(context.directory, f));
29444
+ {
29445
+ const asked = new Set;
29446
+ for (const filePath of absolutePaths) {
29447
+ if (asked.has(filePath))
29448
+ continue;
29449
+ asked.add(filePath);
29450
+ const denial = await assertExternalDirectoryPermission(context, filePath);
29451
+ if (denial)
29452
+ return permissionDeniedResponse(denial);
29453
+ }
29454
+ }
28895
29455
  await runAsk(context.ask({
28896
29456
  permission: "edit",
28897
29457
  patterns: absolutePaths,
@@ -28937,6 +29497,18 @@ function createMoveTool(ctx) {
28937
29497
  execute: async (args, context) => {
28938
29498
  const filePath = path4.isAbsolute(args.filePath) ? args.filePath : path4.resolve(context.directory, args.filePath);
28939
29499
  const destPath = path4.isAbsolute(args.destination) ? args.destination : path4.resolve(context.directory, args.destination);
29500
+ {
29501
+ const sourceDenial = await assertExternalDirectoryPermission(context, filePath, {
29502
+ kind: "file"
29503
+ });
29504
+ if (sourceDenial)
29505
+ return permissionDeniedResponse(sourceDenial);
29506
+ if (destPath !== filePath) {
29507
+ const destDenial = await assertExternalDirectoryPermission(context, destPath);
29508
+ if (destDenial)
29509
+ return permissionDeniedResponse(destDenial);
29510
+ }
29511
+ }
28940
29512
  await runAsk(context.ask({
28941
29513
  permission: "edit",
28942
29514
  patterns: [filePath, destPath],
@@ -28991,6 +29563,11 @@ function aftPrefixedTools(ctx) {
28991
29563
  const file2 = normalizedArgs.filePath;
28992
29564
  const filePath = path4.isAbsolute(file2) ? file2 : path4.resolve(context.directory, file2);
28993
29565
  const relPath = path4.relative(context.worktree, filePath);
29566
+ {
29567
+ const denial = await assertExternalDirectoryPermission(context, filePath);
29568
+ if (denial)
29569
+ return permissionDeniedResponse(denial);
29570
+ }
28994
29571
  await runAsk(context.ask({
28995
29572
  permission: "edit",
28996
29573
  patterns: [relPath],
@@ -29003,9 +29580,6 @@ function aftPrefixedTools(ctx) {
29003
29580
  create_dirs: normalizedArgs.create_dirs !== false,
29004
29581
  diagnostics: true
29005
29582
  };
29006
- if (normalizedArgs.dryRun === true || normalizedArgs.dry_run === true) {
29007
- writeParams.dry_run = true;
29008
- }
29009
29583
  const data = await callBridge(ctx, context, "write", writeParams);
29010
29584
  return JSON.stringify(data);
29011
29585
  }
@@ -29029,10 +29603,9 @@ function importTools(ctx) {
29029
29603
  ` + `Ops:
29030
29604
  ` + `- 'add': Add an import. Auto-detects group (stdlib/external/internal), deduplicates. Requires 'module'. Optional 'names', 'defaultImport', 'typeOnly'.
29031
29605
  ` + `- '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.
29032
- ` + `- 'organize': Re-sort and re-group all imports by language convention, deduplicate. Requires only 'filePath'.
29606
+ ` + `- '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.
29033
29607
 
29034
29608
  ` + `Returns:
29035
- ` + `- Dry run (any op): { ok, dry_run, diff, syntax_valid? }
29036
29609
  ` + `- add: { file, added, module, group?, already_present?, formatted?, syntax_valid?, format_skipped_reason?, validation_errors?, validate_skipped_reason?, backup_id?, lsp_diagnostics? }
29037
29610
  ` + `- remove: { file, removed, module, name?, formatted, syntax_valid?, format_skipped_reason?, validation_errors?, validate_skipped_reason?, backup_id?, lsp_diagnostics? }
29038
29611
  ` + "- organize: { file, groups: [{ name, count }], removed_duplicates, formatted?, syntax_valid?, format_skipped_reason?, validation_errors?, validate_skipped_reason?, backup_id?, lsp_diagnostics? }",
@@ -29044,21 +29617,22 @@ function importTools(ctx) {
29044
29617
  defaultImport: z5.string().optional().describe("Default import name (e.g. 'React')"),
29045
29618
  typeOnly: z5.boolean().optional().describe("Type-only import (TS only, default: false)"),
29046
29619
  removeName: z5.string().optional().describe("Named import to remove for 'remove' op; omit to remove entire import"),
29047
- 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)"),
29048
- dryRun: z5.boolean().optional().describe("Preview without modifying the file (default: false)")
29620
+ 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)")
29049
29621
  },
29050
29622
  execute: async (args, context) => {
29051
29623
  const op = args.op;
29052
- const isDryRun = args.dryRun === true;
29053
29624
  if ((op === "add" || op === "remove") && typeof args.module !== "string") {
29054
29625
  throw new Error(`'module' is required for '${op}' op`);
29055
29626
  }
29056
- if (!isDryRun) {
29057
- const filePath = resolveAbsolutePath(context, args.filePath);
29058
- const permissionError = await askEditPermission(context, [resolveRelativePattern(context, args.filePath)], { filepath: filePath });
29059
- if (permissionError)
29060
- return permissionDeniedResponse(permissionError);
29627
+ const filePath = resolveAbsolutePath(context, args.filePath);
29628
+ {
29629
+ const denial = await assertExternalDirectoryPermission(context, filePath);
29630
+ if (denial)
29631
+ return permissionDeniedResponse(denial);
29061
29632
  }
29633
+ const permissionError = await askEditPermission(context, [resolveRelativePattern(context, args.filePath)], { filepath: filePath });
29634
+ if (permissionError)
29635
+ return permissionDeniedResponse(permissionError);
29062
29636
  const commandMap = {
29063
29637
  add: "add_import",
29064
29638
  remove: "remove_import",
@@ -29077,8 +29651,6 @@ function importTools(ctx) {
29077
29651
  params.name = args.removeName;
29078
29652
  if (args.validate !== undefined)
29079
29653
  params.validate = args.validate;
29080
- if (args.dryRun !== undefined)
29081
- params.dry_run = args.dryRun;
29082
29654
  const response = await callBridge(ctx, context, commandMap[op], params);
29083
29655
  if (response.success === false) {
29084
29656
  throw new Error(response.message || `${op} failed`);
@@ -29421,9 +29993,9 @@ function refactoringTools(ctx) {
29421
29993
 
29422
29994
  ` + `Each op requires specific parameters \u2014 see parameter descriptions for requirements.
29423
29995
 
29424
- ` + `All ops need 'filePath'. Use dryRun to preview before applying.
29996
+ ` + `All ops need 'filePath'. Use aft_safety checkpoint/undo before risky refactors.
29425
29997
 
29426
- ` + "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, ... }.",
29998
+ ` + "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, ... }.",
29427
29999
  args: {
29428
30000
  op: z9.enum(["move", "extract", "inline"]).describe("Refactoring operation"),
29429
30001
  filePath: z9.string().describe("Path to the source file (absolute or relative to project root)"),
@@ -29433,8 +30005,7 @@ function refactoringTools(ctx) {
29433
30005
  name: z9.string().optional().describe("New function name \u2014 required for 'extract' op"),
29434
30006
  startLine: z9.number().optional().describe("1-based start line \u2014 required for 'extract' op"),
29435
30007
  endLine: z9.number().optional().describe("1-based end line (inclusive) \u2014 required for 'extract' op"),
29436
- callSiteLine: z9.number().optional().describe("1-based call site line \u2014 required for 'inline' op"),
29437
- dryRun: z9.boolean().optional().describe("Preview changes as diff without modifying files (default: false)")
30008
+ callSiteLine: z9.number().optional().describe("1-based call site line \u2014 required for 'inline' op")
29438
30009
  },
29439
30010
  execute: async (args, context) => {
29440
30011
  const op = args.op;
@@ -29462,6 +30033,18 @@ function refactoringTools(ctx) {
29462
30033
  ...typeof args.destination === "string" ? [args.destination] : []
29463
30034
  ]) : [resolveRelativePattern(context, args.filePath)];
29464
30035
  const metadata = patterns.length === 1 ? { filepath: filePath } : {};
30036
+ {
30037
+ const affectedPaths = op === "move" && typeof args.destination === "string" ? [filePath, resolveAbsolutePath(context, args.destination)] : [filePath];
30038
+ const asked = new Set;
30039
+ for (const affectedPath of affectedPaths) {
30040
+ if (asked.has(affectedPath))
30041
+ continue;
30042
+ asked.add(affectedPath);
30043
+ const denial = await assertExternalDirectoryPermission(context, affectedPath);
30044
+ if (denial)
30045
+ return permissionDeniedResponse(denial);
30046
+ }
30047
+ }
29465
30048
  const permissionError = await askEditPermission(context, patterns, metadata);
29466
30049
  if (permissionError)
29467
30050
  return permissionDeniedResponse(permissionError);
@@ -29471,8 +30054,6 @@ function refactoringTools(ctx) {
29471
30054
  inline: "inline_symbol"
29472
30055
  };
29473
30056
  const params = { file: args.filePath };
29474
- if (args.dryRun !== undefined)
29475
- params.dry_run = args.dryRun;
29476
30057
  switch (op) {
29477
30058
  case "move":
29478
30059
  params.symbol = args.symbol;
@@ -29504,6 +30085,7 @@ function refactoringTools(ctx) {
29504
30085
  }
29505
30086
 
29506
30087
  // src/tools/safety.ts
30088
+ import * as path5 from "path";
29507
30089
  import { tool as tool10 } from "@opencode-ai/plugin";
29508
30090
  var z10 = tool10.schema;
29509
30091
  function safetyTools(ctx) {
@@ -29541,10 +30123,30 @@ function safetyTools(ctx) {
29541
30123
  }
29542
30124
  if (op === "undo" && typeof args.filePath === "string") {
29543
30125
  const filePath = resolveAbsolutePath(context, args.filePath);
30126
+ {
30127
+ const denial = await assertExternalDirectoryPermission(context, filePath);
30128
+ if (denial)
30129
+ return permissionDeniedResponse(denial);
30130
+ }
29544
30131
  const permissionError = await askEditPermission(context, [resolveRelativePattern(context, args.filePath)], { filepath: filePath });
29545
30132
  if (permissionError)
29546
30133
  return permissionDeniedResponse(permissionError);
29547
30134
  }
30135
+ if (op === "checkpoint" && Array.isArray(args.files)) {
30136
+ const uniqueParents = new Set;
30137
+ for (const file2 of args.files) {
30138
+ if (typeof file2 !== "string")
30139
+ continue;
30140
+ const abs = path5.isAbsolute(file2) ? file2 : path5.resolve(context.directory, file2);
30141
+ const parent = path5.dirname(abs);
30142
+ if (uniqueParents.has(parent))
30143
+ continue;
30144
+ uniqueParents.add(parent);
30145
+ const denial = await assertExternalDirectoryPermission(context, file2, { kind: "file" });
30146
+ if (denial)
30147
+ return permissionDeniedResponse(denial);
30148
+ }
30149
+ }
29548
30150
  if (op === "restore") {
29549
30151
  const permissionError = await askEditPermission(context, [workspacePattern(context)], {
29550
30152
  checkpoint: args.name
@@ -29585,6 +30187,8 @@ function safetyTools(ctx) {
29585
30187
  }
29586
30188
 
29587
30189
  // src/tools/search.ts
30190
+ import * as fs5 from "fs";
30191
+ import * as path6 from "path";
29588
30192
  function arg(schema) {
29589
30193
  return schema;
29590
30194
  }
@@ -29654,11 +30258,33 @@ function searchTools(ctx) {
29654
30258
  path: arg(exports_external.string().optional().describe("Directory to search in, relative to project root"))
29655
30259
  },
29656
30260
  execute: async (args, context) => {
30261
+ const pattern = String(args.pattern);
30262
+ const includeArg = args.include ? String(args.include) : undefined;
30263
+ const pathArg = args.path ? String(args.path) : undefined;
30264
+ const grepDenied = await askGrepPermission(context, pattern, {
30265
+ path: pathArg,
30266
+ include: includeArg
30267
+ });
30268
+ if (grepDenied)
30269
+ return permissionDeniedResponse(grepDenied);
30270
+ if (pathArg) {
30271
+ let kind = "file";
30272
+ try {
30273
+ const abs = path6.isAbsolute(pathArg) ? pathArg : path6.resolve(context.directory, pathArg);
30274
+ if (fs5.lstatSync(abs).isDirectory())
30275
+ kind = "directory";
30276
+ } catch {}
30277
+ const externalDenied = await assertExternalDirectoryPermission(context, pathArg, {
30278
+ kind
30279
+ });
30280
+ if (externalDenied)
30281
+ return permissionDeniedResponse(externalDenied);
30282
+ }
29657
30283
  const response = await callBridge(ctx, context, "grep", {
29658
- pattern: args.pattern,
30284
+ pattern,
29659
30285
  case_sensitive: true,
29660
- include: args.include ? splitIncludeArg(String(args.include)).map(normalizeGlob).filter(Boolean) : undefined,
29661
- path: args.path,
30286
+ include: includeArg ? splitIncludeArg(includeArg).map(normalizeGlob).filter(Boolean) : undefined,
30287
+ path: pathArg,
29662
30288
  max_results: 100
29663
30289
  });
29664
30290
  if (response.success === false) {
@@ -29686,6 +30312,22 @@ function searchTools(ctx) {
29686
30312
  }
29687
30313
  }
29688
30314
  }
30315
+ const globDenied = await askGlobPermission(context, globPattern, { path: globPath });
30316
+ if (globDenied)
30317
+ return permissionDeniedResponse(globDenied);
30318
+ if (globPath) {
30319
+ let kind = "directory";
30320
+ try {
30321
+ const abs = path6.isAbsolute(globPath) ? globPath : path6.resolve(context.directory, globPath);
30322
+ if (fs5.lstatSync(abs).isFile())
30323
+ kind = "file";
30324
+ } catch {}
30325
+ const externalDenied = await assertExternalDirectoryPermission(context, globPath, {
30326
+ kind
30327
+ });
30328
+ if (externalDenied)
30329
+ return permissionDeniedResponse(externalDenied);
30330
+ }
29689
30331
  const response = await callBridge(ctx, context, "glob", {
29690
30332
  pattern: globPattern,
29691
30333
  path: globPath
@@ -29777,10 +30419,9 @@ function structureTools(ctx) {
29777
30419
  ` + `- 'add_decorator': Add Python decorator to function/class. Requires 'target' and 'decorator' (without @). Optional 'position'.
29778
30420
  ` + `- 'add_struct_tags': Add/update Go struct field tags. Requires 'target' (struct name), 'field', 'tag', 'value'.
29779
30421
 
29780
- ` + `Each op requires specific parameters \u2014 see parameter descriptions for requirements.
30422
+ ` + `Each op requires specific parameters \u2014 see parameter descriptions for requirements. Use aft_safety checkpoint/undo before risky transforms.
29781
30423
 
29782
30424
  ` + `Returns:
29783
- ` + `- Dry run (any op): { ok, dry_run, diff, syntax_valid? }
29784
30425
  ` + `- add_member: { file, scope, position, formatted, syntax_valid?, format_skipped_reason?, validation_errors?, validate_skipped_reason?, backup_id?, lsp_diagnostics? }
29785
30426
  ` + `- add_derive: { file, target, derives, formatted, syntax_valid?, format_skipped_reason?, validation_errors?, validate_skipped_reason?, backup_id?, lsp_diagnostics? }
29786
30427
  ` + `- wrap_try_catch: { file, target, formatted, syntax_valid?, format_skipped_reason?, validation_errors?, validate_skipped_reason?, backup_id?, lsp_diagnostics? }
@@ -29799,8 +30440,7 @@ function structureTools(ctx) {
29799
30440
  field: z11.string().optional().describe("Struct field name (add_struct_tags)"),
29800
30441
  tag: z11.string().optional().describe("Tag key (add_struct_tags \u2014 e.g. 'json')"),
29801
30442
  value: z11.string().optional().describe("Tag value (add_struct_tags \u2014 e.g. 'user_name,omitempty')"),
29802
- 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)"),
29803
- dryRun: z11.boolean().optional().describe("Preview without modifying the file (default: false)")
30443
+ 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)")
29804
30444
  },
29805
30445
  execute: async (args, context) => {
29806
30446
  const op = args.op;
@@ -29829,14 +30469,17 @@ function structureTools(ctx) {
29829
30469
  throw new Error("'value' is required for 'add_struct_tags' op");
29830
30470
  }
29831
30471
  const filePath = resolveAbsolutePath(context, args.filePath);
30472
+ {
30473
+ const denial = await assertExternalDirectoryPermission(context, filePath);
30474
+ if (denial)
30475
+ return permissionDeniedResponse(denial);
30476
+ }
29832
30477
  const permissionError = await askEditPermission(context, [resolveRelativePattern(context, args.filePath)], { filepath: filePath });
29833
30478
  if (permissionError)
29834
30479
  return permissionDeniedResponse(permissionError);
29835
30480
  const params = { file: args.filePath };
29836
30481
  if (args.validate !== undefined)
29837
30482
  params.validate = args.validate;
29838
- if (args.dryRun !== undefined)
29839
- params.dry_run = args.dryRun;
29840
30483
  switch (op) {
29841
30484
  case "add_member":
29842
30485
  params.scope = args.container;
@@ -29930,6 +30573,7 @@ function buildHintsFromConfig(config2, disabledTools) {
29930
30573
  }
29931
30574
 
29932
30575
  // src/configure-warnings.ts
30576
+ var pendingEagerWarnings = new Map;
29933
30577
  function isConfigureWarning(value) {
29934
30578
  if (!value || typeof value !== "object" || Array.isArray(value))
29935
30579
  return false;
@@ -29939,13 +30583,25 @@ function isConfigureWarning(value) {
29939
30583
  function coerceConfigureWarnings(warnings) {
29940
30584
  return warnings.filter(isConfigureWarning);
29941
30585
  }
30586
+ function drainPendingEagerWarnings(projectRoot) {
30587
+ const pending = pendingEagerWarnings.get(projectRoot) ?? [];
30588
+ pendingEagerWarnings.delete(projectRoot);
30589
+ return pending;
30590
+ }
29942
30591
  async function handleConfigureWarningsForSession(context) {
30592
+ const validWarnings = coerceConfigureWarnings(context.warnings);
29943
30593
  if (!context.sessionId) {
29944
- warn2(`[configure] deferred warnings for ${context.projectRoot} arrived without session_id; skipping notification`);
30594
+ if (validWarnings.length === 0)
30595
+ return;
30596
+ const pending = pendingEagerWarnings.get(context.projectRoot) ?? [];
30597
+ pending.push(...validWarnings);
30598
+ pendingEagerWarnings.set(context.projectRoot, pending);
30599
+ warn2(`[configure] deferred warnings for ${context.projectRoot} arrived without session_id; buffering until first session-bound call`);
29945
30600
  return;
29946
30601
  }
29947
- const validWarnings = coerceConfigureWarnings(context.warnings);
29948
- if (validWarnings.length === 0)
30602
+ const pendingWarnings = drainPendingEagerWarnings(context.projectRoot);
30603
+ const combinedWarnings = [...pendingWarnings, ...validWarnings];
30604
+ if (combinedWarnings.length === 0)
29949
30605
  return;
29950
30606
  await deliverConfigureWarnings({
29951
30607
  client: context.client ?? context.fallbackClient,
@@ -29953,7 +30609,7 @@ async function handleConfigureWarningsForSession(context) {
29953
30609
  storageDir: context.storageDir,
29954
30610
  pluginVersion: context.pluginVersion,
29955
30611
  projectRoot: context.projectRoot
29956
- }, validWarnings);
30612
+ }, combinedWarnings);
29957
30613
  }
29958
30614
 
29959
30615
  // src/index.ts
@@ -29968,16 +30624,10 @@ function throwSentinel(command) {
29968
30624
  }
29969
30625
  async function sendIgnoredMessage2(client, sessionID, text) {
29970
30626
  const typedClient = client;
29971
- const lastModel = await getLastAssistantModel(client, sessionID);
29972
30627
  const body = {
29973
30628
  noReply: true,
29974
30629
  parts: [{ type: "text", text, ignored: true }]
29975
30630
  };
29976
- if (lastModel) {
29977
- body.model = { providerID: lastModel.providerID, modelID: lastModel.modelID };
29978
- if (lastModel.variant)
29979
- body.variant = lastModel.variant;
29980
- }
29981
30631
  const promptInput = { path: { id: sessionID }, body };
29982
30632
  if (typeof typedClient.session?.prompt === "function") {
29983
30633
  await Promise.resolve(typedClient.session.prompt(promptInput));
@@ -30007,7 +30657,8 @@ var ANNOUNCEMENT_FEATURES = [
30007
30657
  "Trigram grep/glob and semantic search (aft_search) graduated out of experimental.",
30008
30658
  "Lots of bugfixes and new end-to-end test coverage."
30009
30659
  ];
30010
- var plugin = async (input) => {
30660
+ var plugin = async (input) => initializePluginForDirectory(input);
30661
+ async function initializePluginForDirectory(input) {
30011
30662
  const binaryPath = await findBinary();
30012
30663
  const aftConfig = loadAftConfig(input.directory);
30013
30664
  const autoUpdateAbort = new AbortController;
@@ -30035,8 +30686,8 @@ var plugin = async (input) => {
30035
30686
  if (aftConfig.max_callgraph_files !== undefined)
30036
30687
  configOverrides.max_callgraph_files = aftConfig.max_callgraph_files;
30037
30688
  const isFastembedSemanticBackend = (aftConfig.semantic?.backend ?? "fastembed") === "fastembed";
30038
- const dataHome = process.env.XDG_DATA_HOME || join19(homedir11(), ".local", "share");
30039
- configOverrides.storage_dir = join19(dataHome, "opencode", "storage", "plugin", "aft");
30689
+ const dataHome = process.env.XDG_DATA_HOME || join21(homedir11(), ".local", "share");
30690
+ configOverrides.storage_dir = join21(dataHome, "opencode", "storage", "plugin", "aft");
30040
30691
  let onnxRuntimePromise = null;
30041
30692
  if (aftConfig.semantic_search && isFastembedSemanticBackend) {
30042
30693
  const storageDir2 = configOverrides.storage_dir;
@@ -30122,10 +30773,10 @@ ${lines}
30122
30773
  }
30123
30774
  versionUpgradeAttempted = binaryVersion;
30124
30775
  warn2(`WARNING: aft binary v${binaryVersion} is older than plugin v${minVersion}. ` + "Some features may not work. Attempting to download a compatible binary...");
30125
- ensureBinary(`v${minVersion}`).then((path5) => {
30126
- if (path5) {
30127
- log2(`Found/downloaded compatible binary at ${path5}. Replacing running bridges...`);
30128
- pool.replaceBinary(path5).then(() => {
30776
+ ensureBinary(`v${minVersion}`).then((path7) => {
30777
+ if (path7) {
30778
+ log2(`Found/downloaded compatible binary at ${path7}. Replacing running bridges...`);
30779
+ pool.replaceBinary(path7).then(() => {
30129
30780
  log2("Binary replaced successfully. New bridges will use the updated binary.");
30130
30781
  }, (err) => error2("Failed to replace binary:", err));
30131
30782
  } else {
@@ -30136,11 +30787,12 @@ ${lines}
30136
30787
  });
30137
30788
  },
30138
30789
  onConfigureWarnings: async ({ projectRoot, sessionId, client, warnings }) => {
30790
+ const pendingWarnings = sessionId ? drainPendingEagerWarnings(projectRoot) : [];
30139
30791
  await handleConfigureWarningsForSession({
30140
30792
  projectRoot,
30141
30793
  sessionId,
30142
30794
  client,
30143
- warnings,
30795
+ warnings: [...pendingWarnings, ...warnings],
30144
30796
  fallbackClient: input.client,
30145
30797
  storageDir: configOverrides.storage_dir,
30146
30798
  pluginVersion: PLUGIN_VERSION
@@ -30175,7 +30827,6 @@ ${lines}
30175
30827
  config: aftConfig,
30176
30828
  storageDir: configOverrides.storage_dir
30177
30829
  };
30178
- setSharedBridgePool(pool);
30179
30830
  if (onnxRuntimePromise) {
30180
30831
  onnxRuntimePromise.then((ortDylibDir) => {
30181
30832
  if (ortDylibDir) {
@@ -30197,7 +30848,7 @@ ${lines}
30197
30848
  if (onnxRuntimePromise) {
30198
30849
  await Promise.race([
30199
30850
  onnxRuntimePromise,
30200
- new Promise((resolve9) => setTimeout(() => resolve9(null), 60000))
30851
+ new Promise((resolve11) => setTimeout(() => resolve11(null), 60000))
30201
30852
  ]);
30202
30853
  }
30203
30854
  const bridge = pool.getBridge(input.directory);
@@ -30243,10 +30894,10 @@ ${lines}
30243
30894
  return { show: false };
30244
30895
  }
30245
30896
  if (storageDir) {
30246
- const versionFile = join19(storageDir, "last_announced_version");
30897
+ const versionFile = join21(storageDir, "last_announced_version");
30247
30898
  try {
30248
30899
  if (existsSync14(versionFile)) {
30249
- const lastVersion = readFileSync10(versionFile, "utf-8").trim();
30900
+ const lastVersion = readFileSync12(versionFile, "utf-8").trim();
30250
30901
  if (lastVersion === ANNOUNCEMENT_VERSION)
30251
30902
  return { show: false };
30252
30903
  }
@@ -30257,8 +30908,8 @@ ${lines}
30257
30908
  rpcServer.handle("mark-announced", async () => {
30258
30909
  if (storageDir && ANNOUNCEMENT_VERSION) {
30259
30910
  try {
30260
- mkdirSync11(storageDir, { recursive: true });
30261
- writeFileSync11(join19(storageDir, "last_announced_version"), ANNOUNCEMENT_VERSION);
30911
+ mkdirSync12(storageDir, { recursive: true });
30912
+ writeFileSync11(join21(storageDir, "last_announced_version"), ANNOUNCEMENT_VERSION);
30262
30913
  } catch {}
30263
30914
  }
30264
30915
  return { success: true };
@@ -30390,7 +31041,6 @@ Install: ${getManualInstallHint()}`).catch(() => {});
30390
31041
  },
30391
31042
  "chat.message": async (messageInput) => {
30392
31043
  const sid = messageInput.sessionID ?? messageInput.sessionId ?? messageInput.id;
30393
- resetBgWake(sid);
30394
31044
  warmSessionDirectory(input.client, sid, input.directory);
30395
31045
  },
30396
31046
  "command.execute.before": async (commandInput, _output) => {
@@ -30450,11 +31100,10 @@ Install: ${getManualInstallHint()}`).catch(() => {});
30450
31100
  unregisterShutdown();
30451
31101
  await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
30452
31102
  rpcServer.stop();
30453
- clearSharedBridgePool();
30454
31103
  await pool.shutdown();
30455
31104
  }
30456
31105
  };
30457
- };
31106
+ }
30458
31107
  var src_default = plugin;
30459
31108
  export {
30460
31109
  src_default as default