@cortexkit/aft-opencode 0.48.1 → 0.49.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7871,6 +7871,7 @@ function sleep(ms) {
7871
7871
  return new Promise((resolve) => setTimeout(resolve, ms));
7872
7872
  }
7873
7873
  // ../aft-bridge/dist/bash-hints.js
7874
+ import * as fs from "node:fs";
7874
7875
  import * as os from "node:os";
7875
7876
  import * as path from "node:path";
7876
7877
  var CONFLICT_HINT = `
@@ -7879,6 +7880,7 @@ var CONFLICT_HINT = `
7879
7880
  var GREP_SEARCH_AFT_SEARCH_HINT = "DO NOT search code by running grep/rg in bash — it is unindexed, unranked, and serial. Use the `aft_search` tool instead (it auto-routes concepts, identifiers, regex, and literals).";
7880
7881
  var GREP_SEARCH_GREP_HINT = "DO NOT search code by running grep/rg in bash — it is unindexed, unranked, and serial. Use the `grep` tool instead (indexed and ranked).";
7881
7882
  var GREP_SEARCH_HINT_PREFIX = "DO NOT search code by running grep/rg in bash —";
7883
+ var GREP_SEARCH_FRESHNESS_WINDOW_MS = 60000;
7882
7884
  function maybeAppendConflictsHint(output) {
7883
7885
  if (!output.includes("Automatic merge failed; fix conflicts"))
7884
7886
  return output;
@@ -7950,12 +7952,19 @@ function shouldSuppressGrepSearchHint(command, projectRoot) {
7950
7952
  const operands = collectPathOperands(firstStage, firstToken.end);
7951
7953
  if (operands.length === 0)
7952
7954
  return false;
7955
+ let sawInProjectOperand = false;
7953
7956
  for (const operand of operands) {
7954
7957
  if (isDynamicPathOperand(operand))
7955
7958
  continue;
7956
- if (isPathInsideProject(resolvedRoot, effectiveCwd, operand))
7957
- return false;
7959
+ const resolvedOperand = resolvePathOperand(effectiveCwd, operand);
7960
+ if (!isPathInsideProject(resolvedRoot, effectiveCwd, operand))
7961
+ continue;
7962
+ sawInProjectOperand = true;
7963
+ if (shouldSuppressResolvedPath(resolvedOperand))
7964
+ return true;
7958
7965
  }
7966
+ if (sawInProjectOperand)
7967
+ return false;
7959
7968
  }
7960
7969
  return sawCodeSearchStatement;
7961
7970
  }
@@ -8054,6 +8063,17 @@ function isPathInsideProject(resolvedRoot, baseCwd, operand) {
8054
8063
  const rel = path.relative(resolvedRoot, resolved);
8055
8064
  return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
8056
8065
  }
8066
+ function shouldSuppressResolvedPath(resolved) {
8067
+ try {
8068
+ const stats = fs.statSync(resolved);
8069
+ if (stats.isFile())
8070
+ return true;
8071
+ const ageMs = Date.now() - stats.mtimeMs;
8072
+ return ageMs >= 0 && ageMs < GREP_SEARCH_FRESHNESS_WINDOW_MS;
8073
+ } catch {
8074
+ return false;
8075
+ }
8076
+ }
8057
8077
  function splitTopLevelStatements(command) {
8058
8078
  const statements = [];
8059
8079
  let start = 0;
@@ -9388,7 +9408,7 @@ function formatDroppedKeyWarnings(dropped) {
9388
9408
  // ../aft-bridge/dist/downloader.js
9389
9409
  import { spawnSync } from "node:child_process";
9390
9410
  import { createHash as createHash2, randomUUID } from "node:crypto";
9391
- import { chmodSync, closeSync, copyFileSync, createWriteStream, existsSync as existsSync2, mkdirSync, openSync, readFileSync as readFileSync3, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
9411
+ import { chmodSync, closeSync, copyFileSync, createWriteStream, existsSync as existsSync2, mkdirSync, openSync, readFileSync as readFileSync3, renameSync, rmSync, statSync as statSync2, unlinkSync, writeSync } from "node:fs";
9392
9412
  import { homedir as homedir3 } from "node:os";
9393
9413
  import { join as join3 } from "node:path";
9394
9414
  import { Readable } from "node:stream";
@@ -9421,7 +9441,7 @@ function readBinaryVersion(binaryPath) {
9421
9441
  const result = spawnSync(binaryPath, ["--version"], {
9422
9442
  encoding: "utf-8",
9423
9443
  stdio: ["pipe", "pipe", "pipe"],
9424
- timeout: 5000
9444
+ timeout: 60000
9425
9445
  });
9426
9446
  const stdoutVersion = result.stdout?.trim();
9427
9447
  const stderrVersion = result.stderr?.trim();
@@ -9626,7 +9646,7 @@ async function acquireDownloadLock(lockPath) {
9626
9646
  if (code !== "EEXIST")
9627
9647
  throw err;
9628
9648
  try {
9629
- const ageMs = Date.now() - statSync(lockPath).mtimeMs;
9649
+ const ageMs = Date.now() - statSync2(lockPath).mtimeMs;
9630
9650
  if (ageMs > DOWNLOAD_LOCK_STALE_MS) {
9631
9651
  rmSync(lockPath, { force: true });
9632
9652
  continue;
@@ -9783,6 +9803,23 @@ async function renameIfPresent(from, to) {
9783
9803
  throw error2;
9784
9804
  }
9785
9805
  }
9806
+ // ../aft-bridge/dist/error-contract.js
9807
+ class AftToolError extends Error {
9808
+ code;
9809
+ response;
9810
+ constructor(message, code, response) {
9811
+ const cause = { code, message, response };
9812
+ super(message, { cause });
9813
+ this.name = "AftToolError";
9814
+ this.code = code;
9815
+ this.response = response;
9816
+ }
9817
+ }
9818
+ function toolErrorFromResponse(command, response) {
9819
+ const code = typeof response.code === "string" && response.code.length > 0 ? response.code : "unknown_error";
9820
+ const message = typeof response.message === "string" && response.message.length > 0 ? response.message : `${command} failed`;
9821
+ return new AftToolError(message, code, response);
9822
+ }
9786
9823
  // ../aft-bridge/dist/jsonc.js
9787
9824
  function stripJsoncSymbols(value) {
9788
9825
  if (Array.isArray(value)) {
@@ -9799,7 +9836,7 @@ function stripJsoncSymbols(value) {
9799
9836
  }
9800
9837
  // ../aft-bridge/dist/migration.js
9801
9838
  import { spawnSync as spawnSync2 } from "node:child_process";
9802
- import { closeSync as closeSync3, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync as openSync3, readFileSync as readFileSync5, renameSync as renameSync4, rmSync as rmSync2, statSync as statSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
9839
+ import { closeSync as closeSync3, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync as openSync3, readFileSync as readFileSync5, renameSync as renameSync4, rmSync as rmSync2, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
9803
9840
  import { homedir as homedir7, tmpdir } from "node:os";
9804
9841
  import { basename, dirname as dirname3, join as join7, resolve as resolve4 } from "node:path";
9805
9842
 
@@ -10299,7 +10336,7 @@ function acquireConfigMigrationLock(lockDir) {
10299
10336
  if (code !== "EEXIST")
10300
10337
  throw err;
10301
10338
  try {
10302
- const ageMs = Date.now() - statSync2(lockDir).mtimeMs;
10339
+ const ageMs = Date.now() - statSync3(lockDir).mtimeMs;
10303
10340
  if (ageMs > 60000) {
10304
10341
  rmSync2(lockDir, { recursive: true, force: true });
10305
10342
  continue;
@@ -10585,7 +10622,7 @@ async function ensureStorageMigrated(opts) {
10585
10622
  throw new Error(`AFT storage migration failed (${detail}). ` + `Harness: ${opts.harness}. Legacy: ${legacyRoot}. Target: ${newRoot}. ` + `See log: ${logPath}. ` + `Plugin load aborted to prevent legacy/new state divergence.` + (stderrTail ? ` Stderr tail: ${stderrTail}` : "") + (stdoutTail ? ` Stdout tail: ${stdoutTail}` : ""));
10586
10623
  }
10587
10624
  // ../aft-bridge/dist/npm-resolver.js
10588
- import { readdirSync, statSync as statSync3 } from "node:fs";
10625
+ import { readdirSync, statSync as statSync4 } from "node:fs";
10589
10626
  import { homedir as homedir8 } from "node:os";
10590
10627
  import { delimiter, dirname as dirname4, isAbsolute as isAbsolute3, join as join8 } from "node:path";
10591
10628
  function defaultDeps() {
@@ -10601,7 +10638,7 @@ function npmBinaryName(platform) {
10601
10638
  }
10602
10639
  function isFile(p) {
10603
10640
  try {
10604
- return statSync3(p).isFile();
10641
+ return statSync4(p).isFile();
10605
10642
  } catch {
10606
10643
  return false;
10607
10644
  }
@@ -10704,7 +10741,7 @@ function isNpmAvailable(deps = defaultDeps()) {
10704
10741
  // ../aft-bridge/dist/onnx-runtime.js
10705
10742
  import { execFileSync } from "node:child_process";
10706
10743
  import { createHash as createHash3 } from "node:crypto";
10707
- import { chmodSync as chmodSync3, closeSync as closeSync4, copyFileSync as copyFileSync3, createWriteStream as createWriteStream2, existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync5, openSync as openSync4, readdirSync as readdirSync2, readFileSync as readFileSync6, readlinkSync, realpathSync, rmSync as rmSync3, statSync as statSync4, symlinkSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "node:fs";
10744
+ import { chmodSync as chmodSync3, closeSync as closeSync4, copyFileSync as copyFileSync3, createWriteStream as createWriteStream2, existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync5, openSync as openSync4, readdirSync as readdirSync2, readFileSync as readFileSync6, readlinkSync, realpathSync, rmSync as rmSync3, statSync as statSync5, symlinkSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "node:fs";
10708
10745
  import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute4, join as join9, relative as relative2, resolve as resolve5, win32 } from "node:path";
10709
10746
  import { Readable as Readable2 } from "node:stream";
10710
10747
  import { pipeline as pipeline2 } from "node:stream/promises";
@@ -10837,7 +10874,7 @@ function cleanupAbandonedStagingDirs(onnxBaseDir) {
10837
10874
  abandoned = true;
10838
10875
  } else {
10839
10876
  try {
10840
- const ageMs = Date.now() - statSync4(stagingDir).mtimeMs;
10877
+ const ageMs = Date.now() - statSync5(stagingDir).mtimeMs;
10841
10878
  abandoned = ageMs > STALE_LOCK_MS;
10842
10879
  } catch {
10843
10880
  abandoned = true;
@@ -11270,7 +11307,7 @@ function writeOnnxInstalledMeta(installDir, version, sha256, archiveSha256) {
11270
11307
  function readOnnxInstalledMeta(installDir) {
11271
11308
  const path2 = join9(installDir, ONNX_INSTALLED_META_FILE);
11272
11309
  try {
11273
- if (!statSync4(path2).isFile())
11310
+ if (!statSync5(path2).isFile())
11274
11311
  return null;
11275
11312
  const raw = readFileSync6(path2, "utf8");
11276
11313
  const parsed = JSON.parse(raw);
@@ -11321,7 +11358,7 @@ ${new Date().toISOString()}
11321
11358
  const parsed = Number.parseInt(firstLine, 10);
11322
11359
  if (Number.isFinite(parsed) && parsed > 0)
11323
11360
  owningPid = parsed;
11324
- lockMtimeMs = statSync4(lockPath).mtimeMs;
11361
+ lockMtimeMs = statSync5(lockPath).mtimeMs;
11325
11362
  } catch {
11326
11363
  return tryClaim();
11327
11364
  }
@@ -11404,6 +11441,488 @@ function isProcessAlive(pid) {
11404
11441
  return true;
11405
11442
  }
11406
11443
  }
11444
+ // ../aft-bridge/dist/path-aliases.js
11445
+ class InvalidRequestError extends AftToolError {
11446
+ constructor(message) {
11447
+ super(message, "invalid_request", {
11448
+ success: false,
11449
+ code: "invalid_request",
11450
+ message
11451
+ });
11452
+ this.name = "InvalidRequestError";
11453
+ }
11454
+ }
11455
+ function isWellFormedUnicodeString(value) {
11456
+ for (let index = 0;index < value.length; index++) {
11457
+ const codeUnit = value.charCodeAt(index);
11458
+ if (codeUnit >= 55296 && codeUnit <= 56319) {
11459
+ const next = value.charCodeAt(index + 1);
11460
+ if (next < 56320 || next > 57343 || Number.isNaN(next))
11461
+ return false;
11462
+ index++;
11463
+ } else if (codeUnit >= 56320 && codeUnit <= 57343) {
11464
+ return false;
11465
+ }
11466
+ }
11467
+ return true;
11468
+ }
11469
+ function hasOwn(record, key) {
11470
+ return Object.hasOwn(record, key);
11471
+ }
11472
+ function invalidPathValue(property) {
11473
+ throw new InvalidRequestError(`'${property}' must be a non-empty well-formed Unicode string`);
11474
+ }
11475
+ function pathValue(record, property) {
11476
+ const value = record[property];
11477
+ if (typeof value !== "string" || value.length === 0 || !isWellFormedUnicodeString(value)) {
11478
+ invalidPathValue(property);
11479
+ }
11480
+ return value;
11481
+ }
11482
+ function normalizeAliasPair(record, canonical, legacy, required) {
11483
+ const hasCanonical = hasOwn(record, canonical);
11484
+ const hasLegacy = hasOwn(record, legacy);
11485
+ if (!hasCanonical && !hasLegacy) {
11486
+ if (required) {
11487
+ throw new InvalidRequestError(`'${canonical}' is required`);
11488
+ }
11489
+ return;
11490
+ }
11491
+ if (hasCanonical && hasLegacy) {
11492
+ let canonicalValue;
11493
+ let legacyValue;
11494
+ try {
11495
+ canonicalValue = pathValue(record, canonical);
11496
+ legacyValue = pathValue(record, legacy);
11497
+ } catch {
11498
+ throw new InvalidRequestError(`Invalid request: '${canonical}' and '${legacy}' must both be non-empty well-formed Unicode strings`);
11499
+ }
11500
+ if (canonicalValue !== legacyValue) {
11501
+ throw new InvalidRequestError(`Invalid request: '${canonical}' and '${legacy}' must contain equal decoded strings`);
11502
+ }
11503
+ delete record[legacy];
11504
+ return;
11505
+ }
11506
+ if (hasCanonical) {
11507
+ pathValue(record, canonical);
11508
+ return;
11509
+ }
11510
+ record[canonical] = pathValue(record, legacy);
11511
+ delete record[legacy];
11512
+ }
11513
+ function validateOptionalCanonicalPath(record, property) {
11514
+ if (hasOwn(record, property))
11515
+ pathValue(record, property);
11516
+ }
11517
+ function normalizeZoomTargets(record) {
11518
+ if (!hasOwn(record, "targets"))
11519
+ return;
11520
+ const targets = record.targets;
11521
+ const normalizeTarget = (target, index) => {
11522
+ if (!target || typeof target !== "object" || Array.isArray(target)) {
11523
+ throw new InvalidRequestError(`'targets[${index}].path' must be a non-empty string`);
11524
+ }
11525
+ const source = target;
11526
+ const emptyTarget = source.symbol === "" && (hasOwn(source, "path") && source.path === "" || hasOwn(source, "filePath") && source.filePath === "");
11527
+ if (emptyTarget)
11528
+ return { ...source };
11529
+ const normalized = { ...source };
11530
+ try {
11531
+ normalizeAliasPair(normalized, "path", "filePath", true);
11532
+ } catch (error2) {
11533
+ if (error2 instanceof InvalidRequestError) {
11534
+ throw new InvalidRequestError(error2.message.replace("'filePath'", `'targets[${index}].filePath'`).replace("'path'", `'targets[${index}].path'`));
11535
+ }
11536
+ throw error2;
11537
+ }
11538
+ return normalized;
11539
+ };
11540
+ if (Array.isArray(targets)) {
11541
+ if (targets.length === 0)
11542
+ return;
11543
+ record.targets = targets.map(normalizeTarget);
11544
+ return;
11545
+ }
11546
+ if (targets && typeof targets === "object") {
11547
+ record.targets = normalizeTarget(targets, 0);
11548
+ }
11549
+ }
11550
+ function bareToolName(toolName) {
11551
+ const bare = toolName.startsWith("aft_") ? toolName.slice(4) : toolName;
11552
+ if (bare === "read" || bare === "write" || bare === "edit" || bare === "zoom" || bare === "callgraph" || bare === "safety" || bare === "move" || bare === "import" || bare === "refactor" || bare === "grep" || bare === "search" || bare === "conflicts") {
11553
+ return bare;
11554
+ }
11555
+ return;
11556
+ }
11557
+ function prepareCanonicalPathArguments(toolName, rawArguments) {
11558
+ if (!rawArguments || typeof rawArguments !== "object" || Array.isArray(rawArguments)) {
11559
+ throw new InvalidRequestError("tool arguments must be an object");
11560
+ }
11561
+ const tool = bareToolName(toolName);
11562
+ const record = { ...rawArguments };
11563
+ if (!tool)
11564
+ return record;
11565
+ switch (tool) {
11566
+ case "read":
11567
+ case "write":
11568
+ case "edit":
11569
+ case "move":
11570
+ case "import":
11571
+ case "refactor":
11572
+ normalizeAliasPair(record, "path", "filePath", true);
11573
+ break;
11574
+ case "zoom":
11575
+ normalizeAliasPair(record, "path", "filePath", false);
11576
+ normalizeZoomTargets(record);
11577
+ break;
11578
+ case "callgraph":
11579
+ normalizeAliasPair(record, "path", "filePath", true);
11580
+ normalizeAliasPair(record, "toPath", "toFile", false);
11581
+ break;
11582
+ case "safety":
11583
+ normalizeAliasPair(record, "path", "filePath", false);
11584
+ break;
11585
+ case "grep":
11586
+ case "search":
11587
+ case "conflicts":
11588
+ validateOptionalCanonicalPath(record, "path");
11589
+ break;
11590
+ }
11591
+ return record;
11592
+ }
11593
+ var EDIT_ROOT_COMPATIBILITY_KEYS = new Set([
11594
+ "oldString",
11595
+ "newString",
11596
+ "replaceAll",
11597
+ "occurrence"
11598
+ ]);
11599
+ var EDIT_ROOT_CANONICAL_KEYS = new Set(["path", "appendContent", "edits", "symbol", "content"]);
11600
+ var EDIT_ITEM_KEYS = new Set([
11601
+ "oldString",
11602
+ "newString",
11603
+ "replaceAll",
11604
+ "occurrence",
11605
+ "startLine",
11606
+ "endLine",
11607
+ "content"
11608
+ ]);
11609
+ var ASCII_WHITESPACE = /^[\t\n\v\f\r ]+$/;
11610
+ var ASCII_TRIM = /^[\t\n\v\f\r ]+|[\t\n\v\f\r ]+$/g;
11611
+ var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
11612
+ function prepareCanonicalEditArguments(toolName, rawArguments) {
11613
+ if (!rawArguments || typeof rawArguments !== "object" || Array.isArray(rawArguments)) {
11614
+ throw new InvalidRequestError("tool arguments must be an object");
11615
+ }
11616
+ const raw = rawArguments;
11617
+ const record = copyOwnProperties(raw);
11618
+ normalizeEditPathAlias(record);
11619
+ const suppliedLineFields = ["startLine", "endLine"].filter((key) => hasOwn(record, key));
11620
+ if (suppliedLineFields.length > 0) {
11621
+ throw new InvalidRequestError(`edit: top-level ${suppliedLineFields.map((key) => `'${key}'`).join(" and ")} are invalid; ` + "line-range fields are valid only inside 'edits[]'. " + "Use edits: [{ startLine, endLine, content }].");
11622
+ }
11623
+ const isOpenCodeRetiredBoundary = toolName === "aft_edit";
11624
+ const retiredFields = ["file", "mode"].filter((key) => hasOwn(record, key));
11625
+ if (retiredFields.length > 0 && isOpenCodeRetiredBoundary) {
11626
+ throw new InvalidRequestError("aft_edit: the retired `mode`/`file` edit form is no longer supported; use `path` with " + "exactly one of `appendContent`, `edits`, or `symbol` plus `content`.");
11627
+ }
11628
+ const unknownRootKeys = Object.getOwnPropertyNames(record).filter((key) => !EDIT_ROOT_CANONICAL_KEYS.has(key) && !EDIT_ROOT_COMPATIBILITY_KEYS.has(key) && key !== "filePath").sort();
11629
+ if (unknownRootKeys.length > 0) {
11630
+ throw new InvalidRequestError(formatUnknownKeys(unknownRootKeys));
11631
+ }
11632
+ const modes = editModesPresent(record);
11633
+ if (hasOrphanedSymbolContent(record)) {
11634
+ throw new InvalidRequestError("edit: 'content' requires a non-empty string 'symbol' when symbol mode is selected");
11635
+ }
11636
+ if (modes.length > 1) {
11637
+ throw new InvalidRequestError(`edit: conflicting modes: ${modes.join(", ")}. ` + OMIT_OPTIONAL_FIELDS_STEERING);
11638
+ }
11639
+ if (modes.length === 0) {
11640
+ throw new InvalidRequestError("edit: exactly one of `appendContent`, `edits`, or `symbol` plus `content` is required. " + OMIT_OPTIONAL_FIELDS_STEERING);
11641
+ }
11642
+ const mode = modes[0];
11643
+ if (mode === "appendContent") {
11644
+ if (typeof record.appendContent !== "string") {
11645
+ throw new InvalidRequestError("edit: 'appendContent' must be a string");
11646
+ }
11647
+ } else if (mode === "edits") {
11648
+ const parsedEdits = parseEditArray(record.edits);
11649
+ record.edits = parsedEdits.map((item, index) => normalizeEditItem(item, index));
11650
+ } else if (mode === "symbol/content") {
11651
+ if (!hasOwn(record, "symbol") || typeof record.symbol !== "string") {
11652
+ throw new InvalidRequestError("edit: 'symbol' must be a string when symbol mode is selected");
11653
+ }
11654
+ if (!hasOwn(record, "content") || typeof record.content !== "string") {
11655
+ throw new InvalidRequestError("edit: symbol mode requires both 'symbol' and 'content' string properties");
11656
+ }
11657
+ } else {
11658
+ const item = {};
11659
+ for (const key of EDIT_ROOT_COMPATIBILITY_KEYS) {
11660
+ if (hasOwn(record, key))
11661
+ item[key] = record[key];
11662
+ }
11663
+ record.edits = [normalizeEditItem(item, 0)];
11664
+ for (const key of EDIT_ROOT_COMPATIBILITY_KEYS)
11665
+ delete record[key];
11666
+ }
11667
+ validateEditPath(record);
11668
+ return record;
11669
+ }
11670
+ function normalizeEditPathAlias(record) {
11671
+ const hasCanonical = hasOwn(record, "path");
11672
+ const hasLegacy = hasOwn(record, "filePath");
11673
+ if (!hasCanonical && !hasLegacy)
11674
+ return;
11675
+ if (hasCanonical && hasLegacy) {
11676
+ let canonical;
11677
+ let legacy;
11678
+ try {
11679
+ canonical = pathValue(record, "path");
11680
+ legacy = pathValue(record, "filePath");
11681
+ } catch {
11682
+ throw new InvalidRequestError("Invalid request: 'path' and 'filePath' must both be non-empty well-formed Unicode strings");
11683
+ }
11684
+ if (canonical !== legacy) {
11685
+ throw new InvalidRequestError("Invalid request: 'path' and 'filePath' must contain equal decoded strings");
11686
+ }
11687
+ delete record.filePath;
11688
+ return;
11689
+ }
11690
+ if (!hasCanonical) {
11691
+ record.path = pathValue(record, "filePath");
11692
+ delete record.filePath;
11693
+ }
11694
+ }
11695
+ function validateEditPath(record) {
11696
+ if (!hasOwn(record, "path")) {
11697
+ throw new InvalidRequestError("'path' is required");
11698
+ }
11699
+ pathValue(record, "path");
11700
+ }
11701
+ function formatUnknownKeys(keys) {
11702
+ return `Unrecognized keys: ${keys.map((key) => `"${key}"`).join(", ")}`;
11703
+ }
11704
+ function editModesPresent(record) {
11705
+ const hasAppendContent = isNonEmptyString(record.appendContent);
11706
+ if (!hasAppendContent)
11707
+ delete record.appendContent;
11708
+ const hasEdits = normalizeEditArraySentinels(record);
11709
+ if (!hasEdits)
11710
+ delete record.edits;
11711
+ const hasSymbol = isNonEmptyString(record.symbol);
11712
+ if (!hasSymbol) {
11713
+ delete record.symbol;
11714
+ if (record.content === null || record.content === "")
11715
+ delete record.content;
11716
+ } else if (record.content === null) {
11717
+ delete record.content;
11718
+ }
11719
+ const hasSingleEdit = isNonEmptyString(record.oldString);
11720
+ if (!hasSingleEdit) {
11721
+ for (const key of EDIT_ROOT_COMPATIBILITY_KEYS)
11722
+ delete record[key];
11723
+ } else {
11724
+ for (const key of ["newString", "replaceAll", "occurrence"]) {
11725
+ if (record[key] === null)
11726
+ delete record[key];
11727
+ }
11728
+ }
11729
+ const modes = [];
11730
+ if (hasAppendContent)
11731
+ modes.push("appendContent");
11732
+ if (hasEdits)
11733
+ modes.push("edits");
11734
+ if (hasSymbol)
11735
+ modes.push("symbol/content");
11736
+ if (hasSingleEdit)
11737
+ modes.push("oldString/newString");
11738
+ return modes;
11739
+ }
11740
+ function isNonEmptyString(value) {
11741
+ return typeof value === "string" && value.length > 0;
11742
+ }
11743
+ var OMIT_OPTIONAL_FIELDS_STEERING = "Omit unused optional fields entirely; do not send empty strings or empty arrays for them.";
11744
+ function isEditSentinelItem(item) {
11745
+ if (!item || typeof item !== "object" || Array.isArray(item))
11746
+ return false;
11747
+ const record = item;
11748
+ if (record.oldString !== "")
11749
+ return false;
11750
+ const newStringEmpty = !hasOwn(record, "newString") || record.newString === "" || record.newString === null;
11751
+ if (!newStringEmpty)
11752
+ return false;
11753
+ return !hasOwn(record, "content") || record.content === "" || record.content === null;
11754
+ }
11755
+ function normalizeEditArraySentinels(record) {
11756
+ const value = record.edits;
11757
+ if (Array.isArray(value)) {
11758
+ const survivors = value.filter((item) => !isEditSentinelItem(item));
11759
+ if (survivors.length === 0)
11760
+ return false;
11761
+ record.edits = survivors;
11762
+ return true;
11763
+ }
11764
+ if (typeof value !== "string" || value.length === 0)
11765
+ return false;
11766
+ try {
11767
+ const parsed = JSON.parse(value);
11768
+ if (!Array.isArray(parsed))
11769
+ return true;
11770
+ const survivors = parsed.filter((item) => !isEditSentinelItem(item));
11771
+ if (survivors.length === 0)
11772
+ return false;
11773
+ record.edits = survivors;
11774
+ return true;
11775
+ } catch {
11776
+ return true;
11777
+ }
11778
+ }
11779
+ function hasOrphanedSymbolContent(record) {
11780
+ return isNonEmptyString(record.content) && !isNonEmptyString(record.symbol);
11781
+ }
11782
+ function parseEditArray(value) {
11783
+ if (typeof value === "string") {
11784
+ let parsed;
11785
+ try {
11786
+ parsed = JSON.parse(value);
11787
+ } catch {
11788
+ throw new InvalidRequestError("edit: 'edits' must contain valid JSON representing an array");
11789
+ }
11790
+ if (!Array.isArray(parsed)) {
11791
+ throw new InvalidRequestError("edit: 'edits' JSON must have an array root");
11792
+ }
11793
+ if (parsed.length === 0) {
11794
+ throw new InvalidRequestError("edit: 'edits' array must not be empty");
11795
+ }
11796
+ return parsed;
11797
+ }
11798
+ if (!Array.isArray(value)) {
11799
+ throw new InvalidRequestError("edit: 'edits' must be a non-empty array");
11800
+ }
11801
+ if (value.length === 0) {
11802
+ throw new InvalidRequestError("edit: 'edits' array must not be empty");
11803
+ }
11804
+ return value;
11805
+ }
11806
+ function normalizeEditItem(value, index) {
11807
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
11808
+ throw new InvalidRequestError(`edit: edits[${index}] must be an object`);
11809
+ }
11810
+ const source = value;
11811
+ const item = copyOwnProperties(source);
11812
+ normalizeItemAlias(item, "oldString", "oldText");
11813
+ normalizeItemAlias(item, "newString", "newText");
11814
+ const hasFindField = ["oldString", "newString", "replaceAll", "occurrence"].some((key) => hasOwn(item, key));
11815
+ const hasRangeField = ["startLine", "endLine", "content"].some((key) => hasOwn(item, key));
11816
+ if (hasFindField && hasRangeField) {
11817
+ throw new InvalidRequestError(`edit: edits[${index}] mixes find/replace and line-range fields`);
11818
+ }
11819
+ if (hasFindField) {
11820
+ if (!hasOwn(item, "oldString") || typeof item.oldString !== "string") {
11821
+ throw new InvalidRequestError(`edit: edits[${index}] requires string 'oldString'`);
11822
+ }
11823
+ if (hasOwn(item, "newString") && typeof item.newString !== "string") {
11824
+ throw new InvalidRequestError(`edit: edits[${index}].newString must be a string`);
11825
+ }
11826
+ coerceEditScalars(item, index);
11827
+ validateEditItemKeys(item, index);
11828
+ return item;
11829
+ }
11830
+ if (hasRangeField) {
11831
+ for (const key of ["startLine", "endLine"]) {
11832
+ const value2 = item[key];
11833
+ if (typeof value2 === "string" && /^[0-9]+$/.test(value2.trim())) {
11834
+ item[key] = Number(value2.trim());
11835
+ }
11836
+ if (!hasOwn(item, key) || !isPositiveSafeInteger(item[key])) {
11837
+ throw new InvalidRequestError(`edit: edits[${index}].${key} must be a positive integer`);
11838
+ }
11839
+ }
11840
+ if (item.startLine > item.endLine) {
11841
+ throw new InvalidRequestError(`edit: edits[${index}] requires startLine <= endLine`);
11842
+ }
11843
+ if (!hasOwn(item, "content") || typeof item.content !== "string") {
11844
+ throw new InvalidRequestError(`edit: edits[${index}] requires string 'content'`);
11845
+ }
11846
+ validateEditItemKeys(item, index);
11847
+ return item;
11848
+ }
11849
+ throw new InvalidRequestError(`edit: edits[${index}] must be a find/replace or line-range item`);
11850
+ }
11851
+ function normalizeItemAlias(item, canonical, legacy) {
11852
+ if (hasOwn(item, legacy)) {
11853
+ if (!hasOwn(item, canonical))
11854
+ item[canonical] = item[legacy];
11855
+ delete item[legacy];
11856
+ }
11857
+ }
11858
+ function validateEditItemKeys(item, index) {
11859
+ const unknown = Object.getOwnPropertyNames(item).filter((key) => !EDIT_ITEM_KEYS.has(key)).sort();
11860
+ if (unknown.length > 0) {
11861
+ throw new InvalidRequestError(`edit: edits[${index}] contains ${formatUnknownKeys(unknown)}`);
11862
+ }
11863
+ }
11864
+ function coerceEditScalars(item, index) {
11865
+ if (hasOwn(item, "replaceAll") && hasOwn(item, "occurrence")) {
11866
+ throw new InvalidRequestError(`edit: edits[${index}] cannot contain both 'replaceAll' and 'occurrence'`);
11867
+ }
11868
+ if (hasOwn(item, "replaceAll"))
11869
+ item.replaceAll = coerceEditBoolean(item.replaceAll, index);
11870
+ if (hasOwn(item, "occurrence")) {
11871
+ const occurrence = coerceEditOccurrence(item.occurrence, index);
11872
+ if (occurrence === undefined)
11873
+ delete item.occurrence;
11874
+ else
11875
+ item.occurrence = occurrence;
11876
+ }
11877
+ }
11878
+ function coerceEditBoolean(value, index) {
11879
+ if (typeof value === "boolean")
11880
+ return value;
11881
+ if (typeof value === "number" && Number.isFinite(value) && (value === 0 || value === 1)) {
11882
+ return value === 1;
11883
+ }
11884
+ if (typeof value === "string") {
11885
+ if (value === "1")
11886
+ return true;
11887
+ if (value === "0")
11888
+ return false;
11889
+ if (/^(?:true|false)$/i.test(value))
11890
+ return value.toLowerCase() === "true";
11891
+ }
11892
+ throw new InvalidRequestError(`edit: edits[${index}].replaceAll must be a boolean, true/false string, or 0/1`);
11893
+ }
11894
+ function coerceEditOccurrence(value, index) {
11895
+ if (value === null)
11896
+ return;
11897
+ if (typeof value === "string") {
11898
+ const trimmed = value.replace(ASCII_TRIM, "");
11899
+ if (trimmed.length === 0 || ASCII_WHITESPACE.test(trimmed))
11900
+ return;
11901
+ if (!/^[+]?[0-9]+$/.test(trimmed)) {
11902
+ throw new InvalidRequestError(`edit: edits[${index}].occurrence must be a positive integer`);
11903
+ }
11904
+ try {
11905
+ const parsed = BigInt(trimmed);
11906
+ if (parsed < 1n || parsed > BigInt(MAX_SAFE_INTEGER))
11907
+ throw new Error("out of range");
11908
+ return Number(parsed);
11909
+ } catch {
11910
+ throw new InvalidRequestError(`edit: edits[${index}].occurrence must be a positive integer`);
11911
+ }
11912
+ }
11913
+ if (typeof value === "number" && isPositiveSafeInteger(value))
11914
+ return value;
11915
+ throw new InvalidRequestError(`edit: edits[${index}].occurrence must be a positive integer`);
11916
+ }
11917
+ function isPositiveSafeInteger(value) {
11918
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
11919
+ }
11920
+ function copyOwnProperties(source) {
11921
+ const copy = Object.create(null);
11922
+ for (const key of Object.getOwnPropertyNames(source))
11923
+ copy[key] = source[key];
11924
+ return copy;
11925
+ }
11407
11926
  // ../aft-bridge/dist/project-identity.js
11408
11927
  import { createHash as createHash4 } from "node:crypto";
11409
11928
  import { realpathSync as realpathSync2 } from "node:fs";
@@ -11842,7 +12361,7 @@ class RevivableProjectTransport {
11842
12361
  }
11843
12362
  }
11844
12363
  // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/client.js
11845
- import { promises as fs2 } from "node:fs";
12364
+ import { promises as fs3 } from "node:fs";
11846
12365
  import { debuglog } from "node:util";
11847
12366
 
11848
12367
  // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/auth.js
@@ -11910,7 +12429,7 @@ async function authenticateClient(sock, conn, deadlineMs) {
11910
12429
  }
11911
12430
 
11912
12431
  // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/connection-file.js
11913
- import { promises as fs } from "node:fs";
12432
+ import { promises as fs2 } from "node:fs";
11914
12433
 
11915
12434
  // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/envelope.js
11916
12435
  var PROTOCOL_VERSION = 2;
@@ -12078,7 +12597,7 @@ function validate(info) {
12078
12597
  async function verifyOwnerOnly(path2) {
12079
12598
  if (process.platform === "win32")
12080
12599
  return;
12081
- const stat2 = await fs.stat(path2);
12600
+ const stat2 = await fs2.stat(path2);
12082
12601
  const mode = stat2.mode & 511;
12083
12602
  if ((mode & 63) !== 0) {
12084
12603
  throw new ConnectionFileError(`connection file ${path2} has insecure permissions 0o${mode.toString(8)}; expected owner-only 0600`);
@@ -12086,7 +12605,7 @@ async function verifyOwnerOnly(path2) {
12086
12605
  }
12087
12606
  async function readConnectionFile(path2) {
12088
12607
  await verifyOwnerOnly(path2);
12089
- const raw = await fs.readFile(path2, "utf8");
12608
+ const raw = await fs2.readFile(path2, "utf8");
12090
12609
  let parsed;
12091
12610
  try {
12092
12611
  parsed = JSON.parse(raw);
@@ -13149,7 +13668,7 @@ function isRetryableRouteOpenCode(code) {
13149
13668
  }
13150
13669
  async function connectionFileExists(path2) {
13151
13670
  try {
13152
- await fs2.access(path2);
13671
+ await fs3.access(path2);
13153
13672
  return true;
13154
13673
  } catch {
13155
13674
  return false;
@@ -31268,7 +31787,7 @@ import {
31268
31787
  readFileSync as readFileSync11,
31269
31788
  renameSync as renameSync6,
31270
31789
  rmSync as rmSync5,
31271
- statSync as statSync6,
31790
+ statSync as statSync7,
31272
31791
  writeFileSync as writeFileSync7
31273
31792
  } from "node:fs";
31274
31793
  import { dirname as dirname8 } from "node:path";
@@ -31282,7 +31801,7 @@ var import_comment_json3 = __toESM(require_src2(), 1);
31282
31801
 
31283
31802
  // src/hooks/auto-update-checker/checker.ts
31284
31803
  var import_comment_json2 = __toESM(require_src2(), 1);
31285
- import { existsSync as existsSync10, readFileSync as readFileSync9, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
31804
+ import { existsSync as existsSync10, readFileSync as readFileSync9, statSync as statSync6, writeFileSync as writeFileSync5 } from "node:fs";
31286
31805
  import { homedir as homedir13 } from "node:os";
31287
31806
  import { dirname as dirname6, isAbsolute as isAbsolute7, join as join13, resolve as resolve8 } from "node:path";
31288
31807
  import { fileURLToPath } from "node:url";
@@ -31409,7 +31928,7 @@ function getLocalDevPath(directory) {
31409
31928
  }
31410
31929
  function findPackageJsonUp(startPath) {
31411
31930
  try {
31412
- const stat2 = statSync5(startPath);
31931
+ const stat2 = statSync6(startPath);
31413
31932
  let dir = stat2.isDirectory() ? startPath : dirname6(startPath);
31414
31933
  for (let i = 0;i < 10; i++) {
31415
31934
  const pkgPath = join13(dir, "package.json");
@@ -31564,8 +32083,8 @@ function restoreAutoUpdateSnapshot(snapshot) {
31564
32083
  rmSync4(snapshot.tempDir, { recursive: true, force: true });
31565
32084
  }
31566
32085
  }
31567
- function stripPackageNameFromPath(pathValue, packageName) {
31568
- let current = pathValue;
32086
+ function stripPackageNameFromPath(pathValue2, packageName) {
32087
+ let current = pathValue2;
31569
32088
  for (const segment of [...packageName.split("/")].reverse()) {
31570
32089
  if (basename3(current) !== segment)
31571
32090
  return null;
@@ -31890,7 +32409,7 @@ function isOrphanedCheckLock(lockPath) {
31890
32409
  }
31891
32410
  }
31892
32411
  function readCheckLockState(lockPath) {
31893
- const fallbackStartedMs = statSync6(lockPath).mtimeMs;
32412
+ const fallbackStartedMs = statSync7(lockPath).mtimeMs;
31894
32413
  try {
31895
32414
  const parsed = JSON.parse(readFileSync11(lockPath, "utf-8"));
31896
32415
  const pid = Number.isSafeInteger(parsed.pid) && (parsed.pid ?? 0) > 0 ? parsed.pid ?? null : null;
@@ -32024,7 +32543,7 @@ import {
32024
32543
  readFileSync as readFileSync14,
32025
32544
  renameSync as renameSync7,
32026
32545
  rmSync as rmSync6,
32027
- statSync as statSync8,
32546
+ statSync as statSync9,
32028
32547
  writeFileSync as writeFileSync9
32029
32548
  } from "node:fs";
32030
32549
  import { join as join17 } from "node:path";
@@ -32035,7 +32554,7 @@ import {
32035
32554
  mkdirSync as mkdirSync7,
32036
32555
  openSync as openSync6,
32037
32556
  readFileSync as readFileSync12,
32038
- statSync as statSync7,
32557
+ statSync as statSync8,
32039
32558
  unlinkSync as unlinkSync6,
32040
32559
  writeFileSync as writeFileSync8
32041
32560
  } from "node:fs";
@@ -32068,7 +32587,7 @@ function lspBinDir(npmPackage) {
32068
32587
  function isInstalled(npmPackage, binary) {
32069
32588
  for (const candidate of lspBinaryCandidates(binary)) {
32070
32589
  try {
32071
- if (statSync7(join15(lspBinDir(npmPackage), candidate)).isFile())
32590
+ if (statSync8(join15(lspBinDir(npmPackage), candidate)).isFile())
32072
32591
  return true;
32073
32592
  } catch {}
32074
32593
  }
@@ -32096,7 +32615,7 @@ function writeInstalledMetaIn(installDir, version2, sha256) {
32096
32615
  function readInstalledMetaIn(installDir) {
32097
32616
  const path3 = join15(installDir, INSTALLED_META_FILE);
32098
32617
  try {
32099
- if (!statSync7(path3).isFile())
32618
+ if (!statSync8(path3).isFile())
32100
32619
  return null;
32101
32620
  const raw = readFileSync12(path3, "utf8");
32102
32621
  const parsed = JSON.parse(raw);
@@ -32153,7 +32672,7 @@ ${new Date().toISOString()}
32153
32672
  const parsed = Number.parseInt(firstLine, 10);
32154
32673
  if (Number.isFinite(parsed) && parsed > 0)
32155
32674
  owningPid = parsed;
32156
- lockMtimeMs = statSync7(lock).mtimeMs;
32675
+ lockMtimeMs = statSync8(lock).mtimeMs;
32157
32676
  } catch {
32158
32677
  return tryClaim();
32159
32678
  }
@@ -32707,7 +33226,7 @@ async function ensureServerInstalled(spec, config2, fetchImpl, signal) {
32707
33226
  return null;
32708
33227
  });
32709
33228
  if (currentHash && currentHash !== installedMeta.sha256) {
32710
- error2(`[lsp] ${spec.npm}@${version2}: TOFU sha256 mismatch — refusing to use ` + `tampered binary. Recorded ${installedMeta.sha256}, current ${currentHash}. ` + `Run \`aft doctor --clear\` to re-install from scratch.`);
33229
+ error2(`[lsp] ${spec.npm}@${version2}: TOFU sha256 mismatch — refusing to use ` + `tampered binary. Recorded ${installedMeta.sha256}, current ${currentHash}. ` + `Run \`npx @cortexkit/aft doctor --clear\` to re-install from scratch.`);
32711
33230
  return {
32712
33231
  started: false,
32713
33232
  reason: `TOFU sha256 mismatch on ${spec.npm}@${version2} — see plugin log`
@@ -32758,7 +33277,7 @@ function hashInstalledBinary(spec) {
32758
33277
  let pathToHash = null;
32759
33278
  for (const p of candidates) {
32760
33279
  try {
32761
- if (statSync8(p).isFile()) {
33280
+ if (statSync9(p).isFile()) {
32762
33281
  pathToHash = p;
32763
33282
  break;
32764
33283
  }
@@ -32784,7 +33303,7 @@ function installedBinaryPath(spec) {
32784
33303
  ] : [lspBinaryPath(spec.npm, spec.binary)];
32785
33304
  for (const candidate of candidates) {
32786
33305
  try {
32787
- if (statSync8(candidate).isFile())
33306
+ if (statSync9(candidate).isFile())
32788
33307
  return candidate;
32789
33308
  } catch {}
32790
33309
  }
@@ -32896,7 +33415,7 @@ import {
32896
33415
  realpathSync as realpathSync3,
32897
33416
  renameSync as renameSync8,
32898
33417
  rmSync as rmSync7,
32899
- statSync as statSync9,
33418
+ statSync as statSync10,
32900
33419
  unlinkSync as unlinkSync7,
32901
33420
  writeFileSync as writeFileSync10
32902
33421
  } from "node:fs";
@@ -33009,7 +33528,7 @@ function ghBinaryPath(spec, platform3) {
33009
33528
  function isGithubInstalled(spec, platform3) {
33010
33529
  for (const candidate of ghBinaryCandidates(spec, platform3)) {
33011
33530
  try {
33012
- if (statSync9(join18(ghBinDir(spec), candidate)).isFile())
33531
+ if (statSync10(join18(ghBinDir(spec), candidate)).isFile())
33013
33532
  return true;
33014
33533
  } catch {}
33015
33534
  }
@@ -33023,7 +33542,7 @@ function ghBinaryCandidates(spec, platform3) {
33023
33542
  function readGithubInstalledMetaIn(installDir) {
33024
33543
  try {
33025
33544
  const path3 = join18(installDir, INSTALLED_META_FILE2);
33026
- if (!statSync9(path3).isFile())
33545
+ if (!statSync10(path3).isFile())
33027
33546
  return null;
33028
33547
  const parsed = JSON.parse(readFileSync15(path3, "utf8"));
33029
33548
  if (typeof parsed.version !== "string" || parsed.version.length === 0)
@@ -33401,7 +33920,7 @@ function validateCachedGithubInstall(spec, platform3) {
33401
33920
  const meta3 = readGithubInstalledMetaIn(packageDir);
33402
33921
  const binaryPath = ghBinaryCandidates(spec, platform3).map((candidate) => join18(ghBinDir(spec), candidate)).find((candidate) => {
33403
33922
  try {
33404
- return statSync9(candidate).isFile();
33923
+ return statSync10(candidate).isFile();
33405
33924
  } catch {
33406
33925
  return false;
33407
33926
  }
@@ -33500,7 +34019,7 @@ async function downloadAndInstall(spec, tag, assets, platform3, arch, fetchImpl,
33500
34019
  const previousArchiveSha256 = previousMeta?.archiveSha256 ?? (previousMeta?.binarySha256 ? undefined : previousMeta?.sha256);
33501
34020
  if (previousMeta && previousMeta.version === tag && previousArchiveSha256) {
33502
34021
  if (previousArchiveSha256 !== archiveSha256) {
33503
- error2(`[lsp] ${spec.id} ${tag}: TOFU sha256 mismatch — refusing install. ` + `Previously installed archive sha256=${previousArchiveSha256}, downloaded sha256=${archiveSha256}. ` + `This means the published release for tag ${tag} changed. Investigate before proceeding. ` + `Run \`aft doctor --clear\` to wipe the cache and force a fresh install if you've verified the change.`);
34022
+ error2(`[lsp] ${spec.id} ${tag}: TOFU sha256 mismatch — refusing install. ` + `Previously installed archive sha256=${previousArchiveSha256}, downloaded sha256=${archiveSha256}. ` + `This means the published release for tag ${tag} changed. Investigate before proceeding. ` + `Run \`npx @cortexkit/aft doctor --clear\` to wipe the cache and force a fresh install if you've verified the change.`);
33504
34023
  try {
33505
34024
  unlinkSync7(archivePath);
33506
34025
  } catch {}
@@ -33732,12 +34251,43 @@ function normalizeToolArgSchemas(toolDefinition) {
33732
34251
  }
33733
34252
  return toolDefinition;
33734
34253
  }
33735
- function normalizeToolMap(tools) {
33736
- for (const def of Object.values(tools)) {
33737
- normalizeToolArgSchemas(def);
34254
+ function bareToolName2(toolName) {
34255
+ return toolName.startsWith("aft_") ? toolName.slice(4) : toolName;
34256
+ }
34257
+ function prepareOpenCodeArguments(toolName, rawArguments) {
34258
+ const bare = bareToolName2(toolName);
34259
+ if (bare === "edit") {
34260
+ return prepareCanonicalEditArguments(toolName, rawArguments);
34261
+ }
34262
+ return prepareCanonicalPathArguments(toolName, rawArguments);
34263
+ }
34264
+ var DISPLAY_FILE_PATH_TOOLS = new Set(["read", "write", "edit"]);
34265
+ function preserveDisplayFilePathAlias(toolName, rawArguments, prepared) {
34266
+ if (!DISPLAY_FILE_PATH_TOOLS.has(toolName))
34267
+ return;
34268
+ if (!rawArguments || typeof rawArguments !== "object" || Array.isArray(rawArguments))
34269
+ return;
34270
+ const raw = rawArguments;
34271
+ if (typeof prepared.path === "string" && !Object.hasOwn(raw, "filePath")) {
34272
+ raw.filePath = prepared.path;
34273
+ }
34274
+ }
34275
+ function prepareToolMap(tools) {
34276
+ for (const [toolName, def] of Object.entries(tools)) {
34277
+ const execute = def.execute;
34278
+ def.execute = async (args, context) => {
34279
+ const prepared = prepareOpenCodeArguments(toolName, args);
34280
+ preserveDisplayFilePathAlias(toolName, args, prepared);
34281
+ return execute(prepared, context);
34282
+ };
33738
34283
  }
33739
34284
  return tools;
33740
34285
  }
34286
+ function normalizeToolMap(tools) {
34287
+ for (const def of Object.values(tools))
34288
+ normalizeToolArgSchemas(def);
34289
+ return prepareToolMap(tools);
34290
+ }
33741
34291
  // src/shared/ignored-message.ts
33742
34292
  async function sendIgnoredMessage2(client, sessionID, text) {
33743
34293
  const typedClient = client;
@@ -34591,7 +35141,7 @@ import { tool as tool3 } from "@opencode-ai/plugin";
34591
35141
 
34592
35142
  // src/tools/permissions.ts
34593
35143
  import { execFileSync as execFileSync3 } from "node:child_process";
34594
- import * as fs3 from "node:fs";
35144
+ import * as fs4 from "node:fs";
34595
35145
  import { tmpdir as tmpdir3 } from "node:os";
34596
35146
  import * as path3 from "node:path";
34597
35147
  var UNSUPPORTED_ASK_HOST = "AFT requires OpenCode 1.15.5 or newer for permission asks; please upgrade OpenCode";
@@ -34627,11 +35177,22 @@ function resolveAbsolutePath(context, target) {
34627
35177
  const expanded = expandTilde2(target);
34628
35178
  return path3.isAbsolute(expanded) ? expanded : path3.resolve(projectRootFor(context), expanded);
34629
35179
  }
35180
+ function permissionPath(context, target) {
35181
+ const projectRoot = path3.resolve(projectRootFor(context));
35182
+ const absolutePath = path3.resolve(resolveAbsolutePath(context, target));
35183
+ if (projectRoot === path3.parse(projectRoot).root)
35184
+ return absolutePath;
35185
+ const relativePath = path3.relative(projectRoot, absolutePath);
35186
+ if (relativePath !== "" && relativePath !== ".." && !relativePath.startsWith(`..${path3.sep}`) && !path3.isAbsolute(relativePath)) {
35187
+ return relativePath;
35188
+ }
35189
+ return relativePath === "" ? "." : absolutePath;
35190
+ }
34630
35191
  function resolveRelativePattern(context, target) {
34631
- return path3.relative(projectRootFor(context), resolveAbsolutePath(context, target)) || ".";
35192
+ return permissionPath(context, target);
34632
35193
  }
34633
35194
  function resolveRelativePatternFromAbsolute(context, absolutePath) {
34634
- return path3.relative(projectRootFor(context), absolutePath) || ".";
35195
+ return permissionPath(context, absolutePath);
34635
35196
  }
34636
35197
  function resolveRelativePatterns(context, targets) {
34637
35198
  const seen = new Set;
@@ -34703,7 +35264,7 @@ function windowsPath(p) {
34703
35264
  function normalizePath(p) {
34704
35265
  const resolved = path3.resolve(windowsPath(p));
34705
35266
  try {
34706
- return fs3.realpathSync.native(resolved);
35267
+ return fs4.realpathSync.native(resolved);
34707
35268
  } catch {
34708
35269
  return normalizeNearestExistingParent(resolved);
34709
35270
  }
@@ -34713,7 +35274,7 @@ function normalizeNearestExistingParent(resolved) {
34713
35274
  let current = resolved;
34714
35275
  while (true) {
34715
35276
  try {
34716
- const realParent = fs3.realpathSync.native(current);
35277
+ const realParent = fs4.realpathSync.native(current);
34717
35278
  return missingTail.length === 0 ? realParent : path3.join(realParent, ...missingTail.reverse());
34718
35279
  } catch {
34719
35280
  const parent = path3.dirname(current);
@@ -34787,7 +35348,7 @@ function gitRootForNearestExistingParent(resolved) {
34787
35348
  const nearest = normalizeNearestExistingParent(resolved);
34788
35349
  let cwd = nearest;
34789
35350
  try {
34790
- if (fs3.statSync(nearest).isFile())
35351
+ if (fs4.statSync(nearest).isFile())
34791
35352
  cwd = path3.dirname(nearest);
34792
35353
  } catch {}
34793
35354
  try {
@@ -35039,8 +35600,45 @@ function astTools(ctx) {
35039
35600
  };
35040
35601
  }
35041
35602
 
35042
- // src/tools/bash.ts
35603
+ // src/tools/conflicts.ts
35043
35604
  import { tool as tool4 } from "@opencode-ai/plugin";
35605
+ var z4 = tool4.schema;
35606
+ function conflictTools(ctx) {
35607
+ return {
35608
+ aft_conflicts: {
35609
+ description: "Show all git merge conflicts across the repository — returns line-numbered conflict regions with context for every conflicted file in a single call. Conflicts are discovered from the git repository's top level. By default it inspects the session's project repository; pass `path` to inspect a different repository or git worktree (e.g. where a rebase/merge is running).",
35610
+ args: {
35611
+ path: z4.string().describe("Optional path inside the git repository or worktree to inspect (absolute or relative to project root). Conflicts are discovered from that repository's top level. Defaults to the session project root.").optional()
35612
+ },
35613
+ execute: async (args, context) => {
35614
+ const rawArgs = {};
35615
+ if (!isEmptyParam(args?.path)) {
35616
+ const expanded = expandTilde2(String(args.path));
35617
+ const projectRoot = await resolveProjectRoot(ctx, context);
35618
+ const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
35619
+ const denied = await assertExternalDirectoryPermission(ctx, context, resolved, {
35620
+ kind: "directory"
35621
+ });
35622
+ if (denied)
35623
+ return permissionDeniedResponse(denied);
35624
+ rawArgs.path = resolved;
35625
+ }
35626
+ const response = await callToolCall(ctx, context, "conflicts", rawArgs);
35627
+ if (response.success === false) {
35628
+ throw new Error(response.message || "git_conflicts failed");
35629
+ }
35630
+ return response.text;
35631
+ }
35632
+ }
35633
+ };
35634
+ }
35635
+
35636
+ // src/tools/hoisted.ts
35637
+ import * as path4 from "node:path";
35638
+ import { tool as tool8 } from "@opencode-ai/plugin";
35639
+
35640
+ // src/tools/bash.ts
35641
+ import { tool as tool5 } from "@opencode-ai/plugin";
35044
35642
 
35045
35643
  // src/shared/subagent-detect.ts
35046
35644
  var CACHE_MAX_ENTRIES2 = 200;
@@ -35093,7 +35691,7 @@ function setCache2(sessionId, isSubagent) {
35093
35691
  }
35094
35692
 
35095
35693
  // src/tools/bash.ts
35096
- var z4 = tool4.schema;
35694
+ var z5 = tool5.schema;
35097
35695
  var METADATA_PREVIEW_LIMIT = 30 * 1024;
35098
35696
  var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
35099
35697
  var BASH_TRANSPORT_MARGIN_MS = 1e4;
@@ -35193,22 +35791,22 @@ async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
35193
35791
  function createBashTool(ctx, aftSearchRegisteredOverride) {
35194
35792
  const initialBashCfg = resolveBashConfig(ctx.config);
35195
35793
  const backgroundFlagArg = initialBashCfg.background ? {
35196
- background: z4.boolean().optional().describe("When true, spawn the command in the background and return a taskId for bash_status/bash_kill instead of waiting for completion. Defaults to false.")
35794
+ background: z5.boolean().optional().describe("When true, spawn the command in the background and return a taskId for bash_status/bash_kill instead of waiting for completion. Defaults to false.")
35197
35795
  } : {};
35198
35796
  const ptyArgs = initialBashCfg.background ? {
35199
- pty: z4.boolean().optional().describe('When true, spawn the command in a real PTY for interactive programs (python/node/bash REPLs, vim). Implies background: true automatically. Unavailable in subagent sessions. Inspect with bash_status({ taskId, outputMode: "screen" }) and drive interactively with bash_write — its input accepts either a string OR an array like [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ] for atomic text+key sequences.'),
35797
+ pty: z5.boolean().optional().describe('When true, spawn the command in a real PTY for interactive programs (python/node/bash REPLs, vim). Implies background: true automatically. Unavailable in subagent sessions. Inspect with bash_status({ taskId, outputMode: "screen" }) and drive interactively with bash_write — its input accepts either a string OR an array like [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ] for atomic text+key sequences.'),
35200
35798
  ptyRows: optionalInt(1, 60).describe("PTY terminal height in rows — ignored when pty is false. Defaults to 24 when pty: true. Minimum 1, maximum 60."),
35201
35799
  ptyCols: optionalInt(1, 140).describe("PTY terminal width in columns — ignored when pty is false. Defaults to 80 when pty: true. Minimum 1, maximum 140.")
35202
35800
  } : {};
35203
35801
  const args = {
35204
- command: z4.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
35205
- timeout: optionalInt(1, Number.MAX_SAFE_INTEGER).describe(initialBashCfg.background ? "Hard kill cap in milliseconds (positive integer). When omitted, the task can run up to 30 minutes. Foreground bash returns inline if the command finishes within ~8s (configurable via bash.foreground_wait_window_ms); otherwise it's automatically promoted to background and a completion reminder is delivered when the task actually finishes." : "Hard kill cap in milliseconds (positive integer). When omitted, the foreground command can run up to 30 minutes and returns inline when it finishes."),
35206
- workdir: z4.string().optional().describe("Working directory for command execution. Relative paths resolve through the bridge; defaults to the current tool context/project root when omitted."),
35207
- description: z4.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
35208
- wait: z4.boolean().optional().describe("When true, run in the foreground without auto-promoting and wait until the command finishes or reaches its timeout; if you send a new message, the wait detaches to background. Use only when you know the result is required before doing anything else."),
35209
- sandbox: z4.literal("host").optional().describe("Request one-command approval to run unsandboxed on the host; use only when native sandboxing blocks required work, and note that it is a no-op when sandboxing is disabled."),
35802
+ command: z5.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
35803
+ timeout: optionalInt(1, Number.MAX_SAFE_INTEGER).describe(initialBashCfg.background ? "Hard kill cap in milliseconds (positive integer). In the default foreground mode when wait is false, a command that exceeds the configured wait window is promoted to background and gets a completion reminder when it exits; wait:true disables promotion and remains inline until completion or timeout." : "Hard kill cap in milliseconds (positive integer). When omitted, the foreground command can run up to 30 minutes and returns inline when it finishes."),
35804
+ workdir: z5.string().optional().describe("Working directory for command execution. Relative paths resolve through the bridge; defaults to the current tool context/project root when omitted."),
35805
+ description: z5.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
35806
+ wait: z5.boolean().optional().describe("When true, run in the foreground without auto-promoting and wait until the command finishes or reaches its timeout; if you send a new message, the wait detaches to background. Use only when you know the result is required before doing anything else."),
35807
+ sandbox: z5.literal("host").optional().describe("Request one-command approval to run unsandboxed on the host; use only when native sandboxing blocks required work, and note that it is a no-op when sandboxing is disabled."),
35210
35808
  ...backgroundFlagArg,
35211
- compressed: z4.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output."),
35809
+ compressed: z5.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output."),
35212
35810
  ...ptyArgs
35213
35811
  };
35214
35812
  return {
@@ -35307,8 +35905,8 @@ function createBashStatusTool(ctx) {
35307
35905
  return {
35308
35906
  description: "Read-only snapshot of a background or PTY bash task's current state and output. Returns immediately. Never waits. One look to check on a task is fine — never loop it to wait for completion. To wait, use bash_watch.",
35309
35907
  args: {
35310
- taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded."),
35311
- outputMode: z4.enum(["screen", "raw", "both"]).optional().describe("PTY output rendering mode. Defaults to screen for PTY tasks and preserves existing behavior for piped tasks when omitted.")
35908
+ taskId: z5.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded."),
35909
+ outputMode: z5.enum(["screen", "raw", "both"]).optional().describe("PTY output rendering mode. Defaults to screen for PTY tasks and preserves existing behavior for piped tasks when omitted.")
35312
35910
  },
35313
35911
  execute: async (args, context) => {
35314
35912
  const taskId = args.taskId;
@@ -35322,7 +35920,7 @@ function createBashKillTool(ctx) {
35322
35920
  return {
35323
35921
  description: "Terminate a running background bash task spawned with bash({ background: true }). Returns confirmation of kill or an error if the task already finished.",
35324
35922
  args: {
35325
- taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded.")
35923
+ taskId: z5.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded.")
35326
35924
  },
35327
35925
  execute: async (args, context) => {
35328
35926
  const data = await callBashBridge(ctx, context, "bash_kill", {
@@ -35414,44 +36012,6 @@ function shortenCommand(command) {
35414
36012
  return collapsed.length <= 80 ? collapsed : `${collapsed.slice(0, 77)}...`;
35415
36013
  }
35416
36014
 
35417
- // src/tools/conflicts.ts
35418
- import { tool as tool5 } from "@opencode-ai/plugin";
35419
- var z5 = tool5.schema;
35420
- function conflictTools(ctx) {
35421
- return {
35422
- aft_conflicts: {
35423
- description: "Show all git merge conflicts across the repository — returns line-numbered conflict regions with context for every conflicted file in a single call. Conflicts are discovered from the git repository's top level. By default it inspects the session's project repository; pass `path` to inspect a different repository or git worktree (e.g. where a rebase/merge is running).",
35424
- args: {
35425
- path: z5.string().describe("Optional path inside the git repository or worktree to inspect (absolute or relative to project root). Conflicts are discovered from that repository's top level. Defaults to the session project root.").optional()
35426
- },
35427
- execute: async (args, context) => {
35428
- const rawArgs = {};
35429
- if (!isEmptyParam(args?.path)) {
35430
- const expanded = expandTilde2(String(args.path));
35431
- const projectRoot = await resolveProjectRoot(ctx, context);
35432
- const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
35433
- const denied = await assertExternalDirectoryPermission(ctx, context, resolved, {
35434
- kind: "directory"
35435
- });
35436
- if (denied)
35437
- return permissionDeniedResponse(denied);
35438
- rawArgs.path = resolved;
35439
- }
35440
- const response = await callToolCall(ctx, context, "conflicts", rawArgs);
35441
- if (response.success === false) {
35442
- throw new Error(response.message || "git_conflicts failed");
35443
- }
35444
- return response.text;
35445
- }
35446
- }
35447
- };
35448
- }
35449
-
35450
- // src/tools/hoisted.ts
35451
- import * as fs4 from "node:fs";
35452
- import * as path4 from "node:path";
35453
- import { tool as tool8 } from "@opencode-ai/plugin";
35454
-
35455
36015
  // src/tools/bash_watch.ts
35456
36016
  import { tool as tool6 } from "@opencode-ai/plugin";
35457
36017
  var z6 = tool6.schema;
@@ -35569,7 +36129,7 @@ Waited ${waited.elapsed_ms}ms; matched ${JSON.stringify(waited.match ?? "")}${st
35569
36129
  Waited ${waited.elapsed_ms}ms; timeout reached without match.`;
35570
36130
  } else if (waited.reason === "unavailable") {
35571
36131
  text += `
35572
- Waited ${waited.elapsed_ms}ms; the bridge stayed busy and status couldn't be read. The task may still be running check with bash_status({ taskId }).`;
36132
+ Waited ${waited.elapsed_ms}ms; the bridge was busy, so task state is unknown. Do not poll; let the task's completion notification wake the session, or use one bash_status snapshot on the next normal tool call.`;
35573
36133
  } else {
35574
36134
  const stat2 = String(data.status ?? "unknown");
35575
36135
  const e = typeof data.exit_code === "number" ? `, exit ${data.exit_code}` : "";
@@ -35831,6 +36391,12 @@ function relativeToWorktree(fp, worktree) {
35831
36391
  function readAttachments(data) {
35832
36392
  return Array.isArray(data.attachments) ? data.attachments : [];
35833
36393
  }
36394
+ function persistFilePathAlias(args, context) {
36395
+ if (typeof args.path === "string" && !Object.hasOwn(args, "filePath")) {
36396
+ args.filePath = args.path;
36397
+ }
36398
+ context.metadata({ metadata: {} });
36399
+ }
35834
36400
  function buildUnifiedDiff(fp, before, after) {
35835
36401
  const beforeLines = before.split(`
35836
36402
  `);
@@ -35994,19 +36560,6 @@ function inferBeforeStart(ops, from, beforeLen) {
35994
36560
  return beforeLen;
35995
36561
  }
35996
36562
  var z8 = tool8.schema;
35997
- function diagnosticsOnEditDefault(ctx) {
35998
- return ctx.config.lsp?.diagnostics_on_edit ?? false;
35999
- }
36000
- async function readCurrentFileForPreview(filePath) {
36001
- try {
36002
- return await fs4.promises.readFile(filePath, "utf-8");
36003
- } catch (error53) {
36004
- if (error53 && typeof error53 === "object" && "code" in error53 && error53.code === "ENOENT") {
36005
- return "";
36006
- }
36007
- throw error53;
36008
- }
36009
- }
36010
36563
  var READ_DESCRIPTION = `Read file contents or list directory entries.
36011
36564
 
36012
36565
  Use either startLine/endLine OR offset/limit to read a section of a file.
@@ -36020,96 +36573,99 @@ Behavior:
36020
36573
  - Directories return sorted entries with trailing / for subdirectories
36021
36574
 
36022
36575
  Examples:
36023
- Read full file: { "filePath": "src/app.ts" }
36024
- Read lines 50-100: { "filePath": "src/app.ts", "startLine": 50, "endLine": 100 }
36025
- Read 30 lines from line 200: { "filePath": "src/app.ts", "offset": 200, "limit": 30 }
36026
- List directory: { "filePath": "src/" }
36576
+ Read full file: { "path": "src/app.ts" }
36577
+ Read lines 50-100: { "path": "src/app.ts", "startLine": 50, "endLine": 100 }
36578
+ Read 30 lines from line 200: { "path": "src/app.ts", "offset": 200, "limit": 30 }
36579
+ List directory: { "path": "src/" }
36027
36580
  `;
36028
36581
  function createReadTool(ctx) {
36029
- return {
36030
- description: READ_DESCRIPTION,
36031
- args: {
36032
- filePath: z8.string().describe("Path to file or directory (absolute or relative to project root)"),
36033
- startLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line to start reading from"),
36034
- endLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line to stop reading at (inclusive)"),
36035
- limit: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Max lines to return (default: 2000)"),
36036
- offset: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line number to start reading from (use with limit). Ignored if startLine is provided")
36037
- },
36038
- execute: async (args, context) => {
36039
- const file2 = args.filePath;
36040
- const projectRoot = await resolveProjectRoot(ctx, context);
36041
- const filePath = resolvePathFromProjectRoot(projectRoot, file2);
36042
- {
36043
- const denial = await assertExternalDirectoryPermission(ctx, context, filePath, {
36044
- serverValidatedRead: true
36045
- });
36046
- if (denial)
36047
- return permissionDeniedResponse(denial);
36048
- }
36049
- try {
36050
- await runAsk(context.ask({
36051
- permission: "read",
36052
- patterns: [filePath],
36053
- always: ["*"],
36054
- metadata: {}
36055
- }));
36056
- } catch (error53) {
36057
- if (error53 instanceof Error && error53.message)
36058
- return permissionDeniedResponse(error53.message);
36059
- return permissionDeniedResponse("Permission denied.");
36060
- }
36061
- const rawStartLine = coerceOptionalInt(args.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
36062
- const rawEndLine = coerceOptionalInt(args.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
36063
- const rawLimit = coerceOptionalInt(args.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
36064
- const rawOffset = coerceOptionalInt(args.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
36065
- let startLine = rawStartLine;
36066
- let endLine = rawEndLine;
36067
- if (startLine === undefined && rawOffset !== undefined) {
36068
- startLine = rawOffset;
36069
- if (rawLimit !== undefined) {
36070
- endLine = rawOffset + rawLimit - 1;
36071
- }
36072
- }
36073
- const rawArgs = { filePath: file2 };
36074
- if (startLine !== undefined)
36075
- rawArgs.startLine = startLine;
36076
- if (endLine !== undefined)
36077
- rawArgs.endLine = endLine;
36078
- if (rawLimit !== undefined && rawOffset === undefined)
36079
- rawArgs.limit = rawLimit;
36080
- const response = await callToolCall(ctx, context, "read", rawArgs);
36081
- if (response.success === false) {
36082
- throw new Error(response.message || "read failed");
36083
- }
36084
- const dp = relativeToWorktree(filePath, projectRoot) || file2;
36085
- const output = response.text;
36086
- const attachments = readAttachments(response);
36087
- if (attachments.length > 0) {
36088
- const toolAttachments = attachments.filter((attachment) => typeof attachment.mime === "string" && typeof attachment.data === "string").map((attachment) => ({
36089
- type: "file",
36090
- mime: attachment.mime,
36091
- url: `data:${attachment.mime};base64,${attachment.data}`
36092
- }));
36093
- if (toolAttachments.length > 0) {
36094
- const first = attachments[0];
36095
- const firstMime = typeof first.mime === "string" ? first.mime : "";
36096
- return {
36097
- output,
36098
- title: dp,
36099
- attachments: toolAttachments,
36100
- metadata: {
36101
- preview: output,
36102
- filepath: filePath,
36582
+ return prepareToolMap({
36583
+ read: {
36584
+ description: READ_DESCRIPTION,
36585
+ args: {
36586
+ filePath: z8.string().describe("Path to file or directory (absolute or relative to project root)"),
36587
+ startLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line to start reading from"),
36588
+ endLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line to stop reading at (inclusive)"),
36589
+ limit: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Max lines to return (default: 2000)"),
36590
+ offset: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line number to start reading from (use with limit). Ignored if startLine is provided")
36591
+ },
36592
+ execute: async (args, context) => {
36593
+ const file2 = args.path;
36594
+ const projectRoot = await resolveProjectRoot(ctx, context);
36595
+ const filePath = resolvePathFromProjectRoot(projectRoot, file2);
36596
+ persistFilePathAlias(args, context);
36597
+ {
36598
+ const denial = await assertExternalDirectoryPermission(ctx, context, filePath, {
36599
+ serverValidatedRead: true
36600
+ });
36601
+ if (denial)
36602
+ return permissionDeniedResponse(denial);
36603
+ }
36604
+ try {
36605
+ await runAsk(context.ask({
36606
+ permission: "read",
36607
+ patterns: [filePath],
36608
+ always: ["*"],
36609
+ metadata: {}
36610
+ }));
36611
+ } catch (error53) {
36612
+ if (error53 instanceof Error && error53.message)
36613
+ return permissionDeniedResponse(error53.message);
36614
+ return permissionDeniedResponse("Permission denied.");
36615
+ }
36616
+ const rawStartLine = coerceOptionalInt(args.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
36617
+ const rawEndLine = coerceOptionalInt(args.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
36618
+ const rawLimit = coerceOptionalInt(args.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
36619
+ const rawOffset = coerceOptionalInt(args.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
36620
+ let startLine = rawStartLine;
36621
+ let endLine = rawEndLine;
36622
+ if (startLine === undefined && rawOffset !== undefined) {
36623
+ startLine = rawOffset;
36624
+ if (rawLimit !== undefined) {
36625
+ endLine = rawOffset + rawLimit - 1;
36626
+ }
36627
+ }
36628
+ const rawArgs = { filePath: file2 };
36629
+ if (startLine !== undefined)
36630
+ rawArgs.startLine = startLine;
36631
+ if (endLine !== undefined)
36632
+ rawArgs.endLine = endLine;
36633
+ if (rawLimit !== undefined && rawOffset === undefined)
36634
+ rawArgs.limit = rawLimit;
36635
+ const response = await callToolCall(ctx, context, "read", rawArgs);
36636
+ if (response.success === false) {
36637
+ throw new Error(response.message || "read failed");
36638
+ }
36639
+ const dp = relativeToWorktree(filePath, projectRoot) || file2;
36640
+ const output = response.text;
36641
+ const attachments = readAttachments(response);
36642
+ if (attachments.length > 0) {
36643
+ const toolAttachments = attachments.filter((attachment) => typeof attachment.mime === "string" && typeof attachment.data === "string").map((attachment) => ({
36644
+ type: "file",
36645
+ mime: attachment.mime,
36646
+ url: `data:${attachment.mime};base64,${attachment.data}`
36647
+ }));
36648
+ if (toolAttachments.length > 0) {
36649
+ const first = attachments[0];
36650
+ const firstMime = typeof first.mime === "string" ? first.mime : "";
36651
+ return {
36652
+ output,
36103
36653
  title: dp,
36104
- isImage: first.kind === "image" || firstMime.startsWith("image/"),
36105
- isPdf: first.kind === "pdf" || firstMime === "application/pdf"
36106
- }
36107
- };
36654
+ attachments: toolAttachments,
36655
+ metadata: {
36656
+ preview: output,
36657
+ filepath: filePath,
36658
+ title: dp,
36659
+ isImage: first.kind === "image" || firstMime.startsWith("image/"),
36660
+ isPdf: first.kind === "pdf" || firstMime === "application/pdf"
36661
+ }
36662
+ };
36663
+ }
36108
36664
  }
36665
+ return { output, title: dp, metadata: { title: dp } };
36109
36666
  }
36110
- return { output, title: dp, metadata: { title: dp } };
36111
36667
  }
36112
- };
36668
+ }).read;
36113
36669
  }
36114
36670
  function getWriteDescription(ctx, editToolName) {
36115
36671
  const backupText = ctx.config.backup?.enabled === false ? "Backup capture is disabled by user config." : "Existing files are backed up before overwriting (undo via aft_safety).";
@@ -36123,11 +36679,13 @@ function createWriteTool(ctx, editToolName = "edit") {
36123
36679
  content: z8.string().describe("The full content to write to the file")
36124
36680
  },
36125
36681
  execute: async (args, context) => {
36126
- const file2 = args.filePath;
36682
+ const argsRecord = args;
36683
+ const file2 = args.path;
36127
36684
  const content = args.content;
36128
36685
  const projectRoot = await resolveProjectRoot(ctx, context);
36129
36686
  const filePath = resolvePathFromProjectRoot(projectRoot, file2);
36130
- const relPath = path4.relative(projectRoot, filePath);
36687
+ persistFilePathAlias(argsRecord, context);
36688
+ const permissionPattern = permissionPath(context, filePath);
36131
36689
  {
36132
36690
  const denial2 = await assertExternalDirectoryPermission(ctx, context, filePath);
36133
36691
  if (denial2)
@@ -36136,9 +36694,9 @@ function createWriteTool(ctx, editToolName = "edit") {
36136
36694
  const rawArgs = { filePath: file2, content };
36137
36695
  const preview2 = await callToolCall(ctx, context, "write", rawArgs, { preview: true });
36138
36696
  if (preview2.success === false) {
36139
- throw new Error(preview2.message || "write preview failed");
36697
+ throw toolErrorFromResponse("write", preview2);
36140
36698
  }
36141
- const denial = await askEditPermission(context, [relPath], {
36699
+ const denial = await askEditPermission(context, [permissionPattern], {
36142
36700
  filepath: filePath,
36143
36701
  diff: typeof preview2.preview_diff === "string" ? preview2.preview_diff : ""
36144
36702
  });
@@ -36146,7 +36704,7 @@ function createWriteTool(ctx, editToolName = "edit") {
36146
36704
  return permissionDeniedResponse(denial);
36147
36705
  const data = await callToolCall(ctx, context, "write", rawArgs);
36148
36706
  if (data.success === false) {
36149
- throw new Error(data.message || "write failed");
36707
+ throw toolErrorFromResponse("write", data);
36150
36708
  }
36151
36709
  const output = data.text;
36152
36710
  const diff = data.diff;
@@ -36156,20 +36714,20 @@ function createWriteTool(ctx, editToolName = "edit") {
36156
36714
  const dp = relativeToWorktree(filePath, projectRoot);
36157
36715
  const beforeContent = diff.before ?? "";
36158
36716
  const afterContent = diff.after ?? content;
36717
+ const patch = truncated ? typeof preview2.preview_diff === "string" ? preview2.preview_diff : "" : buildUnifiedDiff(filePath, beforeContent, afterContent);
36159
36718
  return {
36160
36719
  output,
36161
36720
  title: dp,
36162
36721
  metadata: {
36163
- diff: truncated ? typeof preview2.preview_diff === "string" ? preview2.preview_diff : "" : buildUnifiedDiff(filePath, beforeContent, afterContent),
36164
- ...truncated ? {} : {
36722
+ diff: patch,
36723
+ ...patch ? {
36165
36724
  filediff: {
36166
36725
  file: filePath,
36167
- before: beforeContent,
36168
- after: afterContent,
36169
- additions: diff?.additions ?? 0,
36170
- deletions: diff?.deletions ?? 0
36726
+ patch,
36727
+ additions: diff.additions ?? 0,
36728
+ deletions: diff.deletions ?? 0
36171
36729
  }
36172
- },
36730
+ } : {},
36173
36731
  diagnostics: {}
36174
36732
  }
36175
36733
  };
@@ -36182,40 +36740,33 @@ function getEditDescription(ctx, writeToolName) {
36182
36740
 
36183
36741
  **Modes** (determined by which parameters you provide):
36184
36742
 
36185
- Mode priority: appendContent > edits > symbol (without oldString) > oldString (find/replace). If none match, the call is rejected — there is no implicit "write" fallback. To edit multiple files, make parallel \`edit\` calls in one response.
36743
+ Provide exactly one mode per call: appendContent, edits[], or symbol plus content. Mixing modes or providing none is rejected — there is no implicit "write" fallback. To edit multiple files, make parallel \`edit\` calls in one response.
36186
36744
 
36187
- 1. **Append** — pass \`filePath\` + \`appendContent\`
36188
- Appends text to the end of a file, creating the file if it does not exist.
36189
- Example: \`{ "filePath": "notes.txt", "appendContent": "new line\\n" }\`
36745
+ 1. **Append** — pass \`path\` + \`appendContent\`
36746
+ Appends text to the end of a file, creating it if it does not exist.
36747
+ Example: \`{ "path": "notes.txt", "appendContent": "new line\\n" }\`
36190
36748
 
36191
- 2. **Batch edits** — pass \`filePath\` + \`edits\` array
36749
+ 2. **Batch edits** — pass \`path\` + \`edits\` array
36192
36750
  Multiple edits in one file atomically. Each edit is either:
36193
36751
  - \`{ "oldString": "old", "newString": "new" }\` — find/replace
36194
36752
  - \`{ "oldString": "old", "newString": "new", "replaceAll": true }\` — replace every match
36195
36753
  - \`{ "startLine": 5, "endLine": 7, "content": "new lines" }\` — replace line range (1-based, both inclusive)
36196
36754
  Set content to empty string to delete lines.
36197
36755
 
36198
- 3. **Symbol replace** — pass \`filePath\` + \`symbol\` + \`content\`
36199
- Replaces an entire named symbol (function, class, type) with new content.
36756
+ 3. **Symbol replace** — pass \`path\` + \`symbol\` + \`content\`
36757
+ Replaces an entire named symbol (function, class, type).
36200
36758
  Includes decorators, attributes, and doc comments in the replacement range.
36201
- **Important:** You must NOT provide \`oldString\` when using symbol mode — if present, the tool silently falls back to find/replace mode.
36202
- Example: \`{ "filePath": "src/app.ts", "symbol": "handleRequest", "content": "function handleRequest() { ... }" }\`
36759
+ Example: \`{ "path": "src/app.ts", "symbol": "handleRequest", "content": "function handleRequest() { ... }" }\`
36203
36760
 
36204
- 4. **Find and replace** — pass \`filePath\` + \`oldString\` + \`newString\`
36761
+ 4. **Find and replace** — put \`oldString\` and optional \`newString\` in an item of \`edits[]\`
36205
36762
  Finds the exact text in \`oldString\` and replaces it with \`newString\`.
36206
36763
  Supports fuzzy matching (handles whitespace differences automatically).
36207
- If multiple matches exist, specify which one with \`occurrence\` or use \`replaceAll: true\`.
36208
- Example: \`{ "filePath": "src/app.ts", "oldString": "const x = 1", "newString": "const x = 2" }\`
36764
+ If multiple matches exist, specify \`occurrence\` or set \`replaceAll: true\` in that item.
36209
36765
 
36210
- 5. **Replace all occurrences** — add \`replaceAll: true\`
36211
- Replaces every occurrence of \`oldString\` in the file.
36212
- Example: \`{ "filePath": "src/app.ts", "oldString": "oldName", "newString": "newName", "replaceAll": true }\`
36766
+ 5. **Replace all occurrences** — add \`replaceAll: true\` to a find/replace item.
36213
36767
 
36214
- 6. **Select specific occurrence** — add \`occurrence: N\` (0-indexed)
36215
- When multiple matches exist, select the Nth one (0 = first, 1 = second, etc.).
36216
- Example: \`{ "filePath": "src/app.ts", "oldString": "TODO", "newString": "DONE", "occurrence": 0 }\`
36217
-
36218
- Note: Modes 5 and 6 are options on mode 4 (find/replace) — they require \`oldString\`.
36768
+ 6. **Select specific occurrence** — add \`occurrence: N\` to a find/replace item (1-based).
36769
+ When multiple matches exist, select the Nth one (1 = first, 2 = second, etc.).
36219
36770
 
36220
36771
  **Behavior:**
36221
36772
  ${backupBehavior}
@@ -36228,66 +36779,47 @@ function createEditTool(ctx, writeToolName = "write") {
36228
36779
  return {
36229
36780
  description: getEditDescription(ctx, writeToolName),
36230
36781
  args: {
36231
- filePath: z8.string().optional().describe("Path to the file to edit (absolute or relative to project root)"),
36232
- oldString: z8.string().optional().describe("Text to find (exact match, with fuzzy fallback)"),
36233
- newString: z8.string().optional().describe("Text to replace with (omit or set to empty string to delete the matched text)"),
36234
- replaceAll: z8.boolean().optional().describe("Replace all occurrences"),
36235
- occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER).describe("0-indexed occurrence to replace when multiple matches exist"),
36782
+ filePath: z8.string().describe("Path to the file to edit (absolute or relative to project root)"),
36236
36783
  symbol: z8.string().optional().describe("Named symbol to replace (function, class, type)"),
36237
36784
  content: z8.string().optional().describe("Replacement content for symbol mode. For whole-file writes, use the `write` tool."),
36238
- appendContent: z8.string().optional().describe("Text to append to the end of filePath; creates the file if needed"),
36785
+ appendContent: z8.string().optional().describe("Text to append to the end of path; creates the file if needed"),
36239
36786
  edits: z8.array(z8.object({
36240
36787
  oldString: z8.string().optional().describe("Text to find for a batch find/replace edit"),
36241
36788
  newString: z8.string().optional().describe("Replacement text for a batch find/replace edit"),
36242
36789
  replaceAll: z8.boolean().optional().describe("Replace every occurrence for this batch item"),
36243
- occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER).describe("0-indexed occurrence for this batch item"),
36790
+ occurrence: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based occurrence for this batch item (1 = first match)"),
36244
36791
  startLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based start line for a batch line-range edit"),
36245
36792
  endLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based end line for a batch line-range edit"),
36246
36793
  content: z8.string().optional().describe("Replacement text for a batch line-range edit")
36247
- })).optional().describe("Batch edits — array of { oldString, newString }, { oldString, newString, replaceAll: true }, or { startLine, endLine, content } objects")
36794
+ })).min(1).optional().describe("Batch edits — non-empty array of { oldString, newString }, { oldString, newString, replaceAll: true }, or { startLine, endLine, content } objects")
36248
36795
  },
36249
36796
  execute: async (args, context) => {
36250
36797
  const argsRecord = args;
36251
36798
  if (argsRecord.startLine !== undefined || argsRecord.endLine !== undefined) {
36252
- throw new Error("edit: 'startLine'/'endLine' are not top-level parameters. " + "For line-range edits, nest them inside the `edits` array: " + '`edits: [{ startLine: N, endLine: M, content: "..." }]`. ' + "For find/replace, use `oldString`/`newString` instead.");
36799
+ throw new Error("edit: 'startLine'/'endLine' are not top-level parameters. " + "For line-range edits, nest them inside the `edits` array: " + '`edits: [{ startLine: N, endLine: M, content: "..." }]`. ' + "For find/replace, use an item in `edits[]` instead.");
36253
36800
  }
36254
- const file2 = args.filePath;
36801
+ const file2 = args.path;
36255
36802
  if (!file2)
36256
- throw new Error("'filePath' parameter is required");
36803
+ throw new Error("'path' parameter is required");
36257
36804
  const projectRoot = await resolveProjectRoot(ctx, context);
36258
36805
  const filePath = resolvePathFromProjectRoot(projectRoot, file2);
36259
- const relPath = path4.relative(projectRoot, filePath);
36806
+ persistFilePathAlias(argsRecord, context);
36807
+ const permissionPattern = permissionPath(context, filePath);
36260
36808
  {
36261
36809
  const denial2 = await assertExternalDirectoryPermission(ctx, context, filePath);
36262
36810
  if (denial2)
36263
36811
  return permissionDeniedResponse(denial2);
36264
36812
  }
36265
- const occurrence = coerceOptionalInt(args.occurrence, "occurrence", 0, Number.MAX_SAFE_INTEGER);
36266
- const rawArgs = { filePath: file2 };
36267
- for (const key of ["appendContent", "symbol", "content", "oldString", "newString"]) {
36813
+ const rawArgs = { path: file2 };
36814
+ for (const key of ["appendContent", "symbol", "content", "edits"]) {
36268
36815
  if (argsRecord[key] !== undefined)
36269
36816
  rawArgs[key] = argsRecord[key];
36270
36817
  }
36271
- if (Array.isArray(argsRecord.edits)) {
36272
- rawArgs.edits = argsRecord.edits.map((item) => {
36273
- if (!item || typeof item !== "object" || Array.isArray(item))
36274
- return item;
36275
- const batchItem = item;
36276
- return batchItem.replaceAll === undefined ? batchItem : { ...batchItem, replaceAll: coerceBoolean(batchItem.replaceAll) };
36277
- });
36278
- } else if (argsRecord.edits !== undefined) {
36279
- rawArgs.edits = argsRecord.edits;
36280
- }
36281
- if (argsRecord.replaceAll !== undefined) {
36282
- rawArgs.replaceAll = coerceBoolean(argsRecord.replaceAll);
36283
- }
36284
- if (occurrence !== undefined)
36285
- rawArgs.occurrence = occurrence;
36286
36818
  const preview2 = await callToolCall(ctx, context, "edit", rawArgs, { preview: true });
36287
36819
  if (preview2.success === false) {
36288
- throw new Error(preview2.message || "edit preview failed");
36820
+ throw toolErrorFromResponse("edit", preview2);
36289
36821
  }
36290
- const denial = await askEditPermission(context, [relPath], {
36822
+ const denial = await askEditPermission(context, [permissionPattern], {
36291
36823
  filepath: filePath,
36292
36824
  diff: typeof preview2.preview_diff === "string" ? preview2.preview_diff : ""
36293
36825
  });
@@ -36295,7 +36827,7 @@ function createEditTool(ctx, writeToolName = "write") {
36295
36827
  return permissionDeniedResponse(denial);
36296
36828
  const data = await callToolCall(ctx, context, "edit", rawArgs);
36297
36829
  if (data.success === false) {
36298
- throw new Error(data.message || "edit failed");
36830
+ throw toolErrorFromResponse("edit", data);
36299
36831
  }
36300
36832
  const output = data.text;
36301
36833
  const diff = data.diff;
@@ -36304,17 +36836,17 @@ function createEditTool(ctx, writeToolName = "write") {
36304
36836
  const truncated = diff.truncated === true;
36305
36837
  const beforeContent = diff.before ?? "";
36306
36838
  const afterContent = diff.after ?? "";
36839
+ const patch = truncated ? typeof preview2.preview_diff === "string" ? preview2.preview_diff : "" : buildUnifiedDiff(filePath, beforeContent, afterContent);
36307
36840
  const uiMeta = {
36308
- diff: truncated ? typeof preview2.preview_diff === "string" ? preview2.preview_diff : "" : buildUnifiedDiff(filePath, beforeContent, afterContent),
36309
- ...truncated ? {} : {
36841
+ diff: patch,
36842
+ ...patch ? {
36310
36843
  filediff: {
36311
36844
  file: filePath,
36312
- before: beforeContent,
36313
- after: afterContent,
36845
+ patch,
36314
36846
  additions: diff.additions ?? 0,
36315
36847
  deletions: diff.deletions ?? 0
36316
36848
  }
36317
- },
36849
+ } : {},
36318
36850
  diagnostics: {}
36319
36851
  };
36320
36852
  return { output, title: relativeToWorktree(filePath, projectRoot), metadata: uiMeta };
@@ -36401,9 +36933,11 @@ function createApplyPatchTool(ctx) {
36401
36933
  return permissionDeniedResponse(denial2);
36402
36934
  }
36403
36935
  const affectedRelPaths = stringArray(preview2.affected_rel_paths);
36404
- const denial = await askEditPermission(context, affectedRelPaths, {
36936
+ const affectedPaths = stringArray(preview2.affected_paths);
36937
+ const permissionPatterns = (affectedPaths.length > 0 ? affectedPaths : affectedRelPaths).map((filePath) => permissionPath(context, filePath));
36938
+ const denial = await askEditPermission(context, permissionPatterns, {
36405
36939
  diff: typeof preview2.preview_diff === "string" ? preview2.preview_diff : "",
36406
- filepath: typeof preview2.filepath === "string" ? preview2.filepath : affectedRelPaths[0]
36940
+ filepath: typeof preview2.filepath === "string" ? preview2.filepath : affectedPaths[0] ?? affectedRelPaths[0]
36407
36941
  });
36408
36942
  if (denial)
36409
36943
  return permissionDeniedResponse(denial);
@@ -36494,12 +37028,12 @@ function createMoveTool(ctx) {
36494
37028
  return {
36495
37029
  description: moveDescription(ctx),
36496
37030
  args: {
36497
- filePath: z8.string().describe("Source file path to move (absolute or relative to project root)"),
37031
+ path: z8.string().describe("Source file path to move (absolute or relative to project root)"),
36498
37032
  destination: z8.string().describe("Destination file path (absolute or relative to project root)")
36499
37033
  },
36500
37034
  execute: async (args, context) => {
36501
37035
  const projectRoot = await resolveProjectRoot(ctx, context);
36502
- const filePath = resolvePathFromProjectRoot(projectRoot, args.filePath);
37036
+ const filePath = resolvePathFromProjectRoot(projectRoot, args.path);
36503
37037
  const destPath = resolvePathFromProjectRoot(projectRoot, args.destination);
36504
37038
  {
36505
37039
  const sourceDenial = await assertExternalDirectoryPermission(ctx, context, filePath, {
@@ -36520,7 +37054,7 @@ function createMoveTool(ctx) {
36520
37054
  metadata: { action: "move" }
36521
37055
  }));
36522
37056
  const result = await callToolCall(ctx, context, "move", {
36523
- filePath: args.filePath,
37057
+ filePath: args.path,
36524
37058
  destination: args.destination
36525
37059
  });
36526
37060
  if (result.success === false) {
@@ -36549,51 +37083,14 @@ function hoistedTools(ctx) {
36549
37083
  tools.bash_kill = createBashKillTool(ctx);
36550
37084
  }
36551
37085
  }
36552
- return tools;
37086
+ return prepareToolMap(tools);
36553
37087
  }
36554
37088
  function aftPrefixedTools(ctx) {
36555
37089
  const aftEditTool = createEditTool(ctx, "aft_write");
36556
37090
  const tools = {
36557
37091
  aft_read: createReadTool(ctx),
36558
37092
  aft_write: createWriteTool(ctx, "aft_edit"),
36559
- aft_edit: {
36560
- ...aftEditTool,
36561
- execute: async (args, context) => {
36562
- const argRecord = args;
36563
- const normalizedArgs = argRecord.mode !== undefined && argRecord.filePath === undefined && typeof argRecord.file === "string" ? { ...argRecord, filePath: argRecord.file } : { ...argRecord };
36564
- if (normalizedArgs.mode === "write" && typeof normalizedArgs.filePath === "string" && typeof normalizedArgs.content === "string") {
36565
- const file2 = normalizedArgs.filePath;
36566
- const projectRoot = await resolveProjectRoot(ctx, context);
36567
- const filePath = resolvePathFromProjectRoot(projectRoot, file2);
36568
- const relPath = path4.relative(projectRoot, filePath);
36569
- {
36570
- const denial2 = await assertExternalDirectoryPermission(ctx, context, filePath);
36571
- if (denial2)
36572
- return permissionDeniedResponse(denial2);
36573
- }
36574
- const currentContent = await readCurrentFileForPreview(filePath);
36575
- const previewDiff = buildUnifiedDiff(filePath, currentContent, normalizedArgs.content);
36576
- const denial = await askEditPermission(context, [relPath], {
36577
- filepath: filePath,
36578
- diff: previewDiff
36579
- });
36580
- if (denial)
36581
- return permissionDeniedResponse(denial);
36582
- const writeParams = {
36583
- file: filePath,
36584
- content: normalizedArgs.content,
36585
- create_dirs: normalizedArgs.create_dirs !== false,
36586
- diagnostics: normalizedArgs.diagnostics ?? diagnosticsOnEditDefault(ctx)
36587
- };
36588
- const response = await callBridge(ctx, context, "write", writeParams);
36589
- if (response.success === false) {
36590
- throw new Error(response.message ?? "write failed");
36591
- }
36592
- return JSON.stringify(response);
36593
- }
36594
- return aftEditTool.execute(normalizedArgs, context);
36595
- }
36596
- },
37093
+ aft_edit: aftEditTool,
36597
37094
  aft_apply_patch: createApplyPatchTool(ctx),
36598
37095
  aft_delete: createDeleteTool(ctx),
36599
37096
  aft_move: createMoveTool(ctx)
@@ -36608,7 +37105,7 @@ function aftPrefixedTools(ctx) {
36608
37105
  tools.bash_kill = createBashKillTool(ctx);
36609
37106
  }
36610
37107
  }
36611
- return tools;
37108
+ return prepareToolMap(tools);
36612
37109
  }
36613
37110
 
36614
37111
  // src/tools/imports.ts
@@ -36616,17 +37113,17 @@ import { tool as tool9 } from "@opencode-ai/plugin";
36616
37113
  var z9 = tool9.schema;
36617
37114
  function importTools(ctx) {
36618
37115
  const organizeRecovery = ctx.config.backup?.enabled === false ? "Backup capture is disabled by user config; review broad cleanup changes before proceeding." : "Use aft_safety checkpoint/undo for recovery before broad cleanup.";
36619
- return {
37116
+ return prepareToolMap({
36620
37117
  aft_import: {
36621
37118
  description: `Language-aware import management. Supports TS, JS, TSX, Python, Rust, Go, Solidity, Java, C#, PHP, Kotlin, Scala, Swift, Ruby, Lua, C, C++, Perl, and Vue.
36622
37119
 
36623
37120
  ` + `Ops:
36624
37121
  ` + `- 'add': Add an import. Auto-detects group (stdlib/external/internal), deduplicates. Requires 'module'. Optional 'names', 'defaultImport', 'typeOnly'.
36625
37122
  ` + `- '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.
36626
- ` + `- 'organize': Re-sort and re-group all imports by language convention, deduplicate. Requires only 'filePath'. ${organizeRecovery}`,
37123
+ ` + `- 'organize': Re-sort and re-group all imports by language convention, deduplicate. Requires only 'path'. ${organizeRecovery}`,
36627
37124
  args: {
36628
37125
  op: z9.enum(["add", "remove", "organize"]).describe("Import operation"),
36629
- filePath: z9.string().describe("Path to the file (absolute or relative to project root)"),
37126
+ path: z9.string().describe("Path to the file (absolute or relative to project root)"),
36630
37127
  module: z9.string().optional().describe("Module path (required for add, remove — e.g. 'react', './utils', 'std::fmt')"),
36631
37128
  names: z9.array(z9.string()).optional().describe("Named imports to add. Each entry uses the language's native named-import text, " + "including per-name aliasing where the language uses `as` (e.g. ['useState', 'debounce as db'], " + "Solidity ['ERC20', 'IERC20 as IToken'])."),
36632
37129
  defaultImport: z9.string().optional().describe("Default import name, ES only (e.g. 'React')"),
@@ -36643,7 +37140,7 @@ function importTools(ctx) {
36643
37140
  if ((op === "add" || op === "remove") && isEmptyParam(args.module)) {
36644
37141
  throw new Error(`'module' is required for '${op}' op`);
36645
37142
  }
36646
- const filePath = await resolvePathArg(ctx, context, args.filePath);
37143
+ const filePath = await resolvePathArg(ctx, context, args.path);
36647
37144
  {
36648
37145
  const denial = await assertExternalDirectoryPermission(ctx, context, filePath);
36649
37146
  if (denial)
@@ -36680,7 +37177,7 @@ function importTools(ctx) {
36680
37177
  return response.text;
36681
37178
  }
36682
37179
  }
36683
- };
37180
+ });
36684
37181
  }
36685
37182
 
36686
37183
  // src/tools/inspect.ts
@@ -36787,7 +37284,7 @@ import { tool as tool11 } from "@opencode-ai/plugin";
36787
37284
  var z11 = tool11.schema;
36788
37285
  var CALLGRAPH_SOFT_CODES = new Set(["symbol_not_found", "callgraph_building"]);
36789
37286
  function navigationTools(ctx) {
36790
- return {
37287
+ return prepareToolMap({
36791
37288
  aft_callgraph: {
36792
37289
  description: `Answer code-relationship questions from a real call graph — instead of grep + read chains. Reach for this whenever the question is about how symbols connect: who calls X, what X calls, what breaks if X changes, how execution reaches X, or how a value flows.
36793
37290
 
@@ -36796,27 +37293,27 @@ function navigationTools(ctx) {
36796
37293
  ` + `- 'impact': What breaks if a symbol changes — affected callers with signatures and entry-point status (blast radius). Use before a risky edit.
36797
37294
  ` + `- 'call_tree': What a function calls (forward traversal). Use to understand a function's dependencies before modifying it.
36798
37295
  ` + `- 'trace_to': How execution reaches a function from entry points (routes, exports, main). Use to understand context around deeply-nested code.
36799
- ` + `- 'trace_to_symbol': Shortest call path from one symbol to another. Requires 'toSymbol'. If multiple targets match, the error returns candidate files; retry with 'toFile' to disambiguate.
37296
+ ` + `- 'trace_to_symbol': Shortest call path from one symbol to another. Requires 'toSymbol'. If multiple targets match, the error returns candidate files; retry with 'toPath' to disambiguate.
36800
37297
  ` + `- 'trace_data': Follow a value through variable assignments and function parameters across files. Requires 'symbol' (scope to trace from) and 'expression'.
36801
37298
 
36802
- ` + `All ops require both 'filePath' and 'symbol'. 'expression' is additionally required for trace_data; 'toSymbol' for trace_to_symbol.
37299
+ ` + `All ops require both 'path' and 'symbol'. 'expression' is additionally required for trace_data; 'toSymbol' for trace_to_symbol.
36803
37300
 
36804
37301
  ` + `Markers: ~ = edge resolved by name only (may point at the wrong same-named symbol); [unresolved] = callee not resolved to a definition, so the location shown is the call site. Unmarked edges are resolved exactly. By default, unresolved external/stdlib leaf calls in call_tree are collapsed into one summary per parent; pass includeUnresolved=true to show every unresolved edge individually.
36805
37302
  `,
36806
37303
  args: {
36807
37304
  op: z11.enum(["call_tree", "callers", "trace_to", "trace_to_symbol", "impact", "trace_data"]).describe("Navigation operation"),
36808
- filePath: z11.string().describe("Path to the source file containing the symbol (absolute or relative to project root)"),
37305
+ path: z11.string().describe("Path to the source file containing the symbol (absolute or relative to project root)"),
36809
37306
  symbol: z11.string().describe("Name of the symbol to analyze"),
36810
37307
  depth: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Max traversal depth (default: call_tree=5, callers=1, trace_to=10, trace_to_symbol=10 capped at 16, impact=5, trace_data=5)"),
36811
37308
  expression: z11.string().optional().describe("Expression to track through data flow (required for trace_data op)"),
36812
37309
  toSymbol: z11.string().optional().describe("Target symbol name for trace_to_symbol; the returned path ends at this symbol"),
36813
- toFile: z11.string().optional().describe("Optional target file for trace_to_symbol; required when toSymbol exists in multiple files"),
37310
+ toPath: z11.string().optional().describe("Optional target file for trace_to_symbol; required when toSymbol exists in multiple files"),
36814
37311
  includeTests: z11.boolean().optional().describe("Include test files in callers/paths. Defaults to false; tests are hidden."),
36815
37312
  includeUnresolved: z11.boolean().optional().describe("Show every unresolved external/stdlib call individually. Defaults to false; unresolved leaf calls are collapsed into one summary per parent.")
36816
37313
  },
36817
37314
  execute: async (args, context) => {
36818
- if (isEmptyParam(args.filePath)) {
36819
- throw new Error("'filePath' is required");
37315
+ if (isEmptyParam(args.path)) {
37316
+ throw new Error("'path' is required");
36820
37317
  }
36821
37318
  if (isEmptyParam(args.symbol)) {
36822
37319
  throw new Error("'symbol' is required");
@@ -36827,8 +37324,8 @@ function navigationTools(ctx) {
36827
37324
  if (args.op === "trace_to_symbol" && isEmptyParam(args.toSymbol)) {
36828
37325
  throw new Error("'toSymbol' is required for 'trace_to_symbol' op");
36829
37326
  }
36830
- const filePath = await resolvePathArg(ctx, context, args.filePath);
36831
- const toFile = !isEmptyParam(args.toFile) ? await resolvePathArg(ctx, context, args.toFile) : undefined;
37327
+ const filePath = await resolvePathArg(ctx, context, args.path);
37328
+ const toFile = !isEmptyParam(args.toPath) ? await resolvePathArg(ctx, context, args.toPath) : undefined;
36832
37329
  const checked = new Set;
36833
37330
  for (const target of [filePath, ...toFile !== undefined ? [toFile] : []]) {
36834
37331
  if (checked.has(target))
@@ -36840,7 +37337,7 @@ function navigationTools(ctx) {
36840
37337
  }
36841
37338
  const rawArgs = {
36842
37339
  op: args.op,
36843
- filePath: args.filePath,
37340
+ filePath: args.path,
36844
37341
  symbol: args.symbol
36845
37342
  };
36846
37343
  const depth = coerceOptionalInt(args.depth, "depth", 1, Number.MAX_SAFE_INTEGER);
@@ -36850,8 +37347,8 @@ function navigationTools(ctx) {
36850
37347
  rawArgs.expression = args.expression;
36851
37348
  if (!isEmptyParam(args.toSymbol))
36852
37349
  rawArgs.toSymbol = args.toSymbol;
36853
- if (!isEmptyParam(args.toFile))
36854
- rawArgs.toFile = args.toFile;
37350
+ if (!isEmptyParam(args.toPath))
37351
+ rawArgs.toFile = args.toPath;
36855
37352
  if (!isEmptyParam(args.includeTests))
36856
37353
  rawArgs.includeTests = coerceBoolean(args.includeTests);
36857
37354
  if (!isEmptyParam(args.includeUnresolved))
@@ -36867,23 +37364,26 @@ function navigationTools(ctx) {
36867
37364
  return response.text;
36868
37365
  }
36869
37366
  }
36870
- };
37367
+ });
36871
37368
  }
36872
37369
 
36873
37370
  // src/tools/reading.ts
36874
37371
  import { tool as tool12 } from "@opencode-ai/plugin";
36875
37372
  var z12 = tool12.schema;
36876
37373
  function buildZoomTitle(args) {
36877
- if (!isEmptyParam(args.targets)) {
36878
- if (Array.isArray(args.targets)) {
36879
- if (args.targets.length === 1 && args.targets[0]) {
36880
- return `${args.targets[0].filePath}#${args.targets[0].symbol}`;
37374
+ const targets = args.targets;
37375
+ if (!isEmptyParam(targets)) {
37376
+ if (Array.isArray(targets)) {
37377
+ if (targets.length === 1) {
37378
+ return `${targets[0].path}#${targets[0].symbol}`;
36881
37379
  }
36882
- return `${args.targets.length} targets across files`;
37380
+ return `${targets.length} targets across files`;
37381
+ }
37382
+ if (targets && typeof targets === "object") {
37383
+ return `${targets.path}#${targets.symbol}`;
36883
37384
  }
36884
- return `${args.targets.filePath}#${args.targets.symbol}`;
36885
37385
  }
36886
- const path5 = args.filePath ?? args.url ?? "";
37386
+ const path5 = args.path ?? args.url ?? "";
36887
37387
  if (typeof args.symbols === "string")
36888
37388
  return path5 ? `${path5}#${args.symbols}` : args.symbols;
36889
37389
  if (Array.isArray(args.symbols) && args.symbols.length > 0) {
@@ -36894,7 +37394,7 @@ function buildZoomTitle(args) {
36894
37394
  return path5 || "(no target)";
36895
37395
  }
36896
37396
  function readingTools(ctx) {
36897
- return {
37397
+ return prepareToolMap({
36898
37398
  aft_outline: {
36899
37399
  description: "Structural outline of source code, documentation files, or remote URLs. For code, returns symbols (functions, classes, types) with line ranges. For Markdown and HTML, returns heading hierarchy. Use this to explore structure before reading specific sections with aft_zoom. Set `files: true` with a directory target for a flat indexed file tree with language, symbol count, and byte metadata.\n\n" + "For understanding a specific feature, prefer aft_search + aft_zoom on named symbols; use aft_outline on a whole directory only for high-level structure mapping. aft_zoom with `callgraph:true` gives one-level forward calls-out; use aft_callgraph only for reverse callers or multi-level traces.\n\n" + "Pass a single `target`:\n" + ` • file path → outline that file (with signatures)
36900
37400
  ` + ` • directory path → outline all source files under it (recursively, up to 200 files)
@@ -36943,21 +37443,21 @@ function readingTools(ctx) {
36943
37443
  }
36944
37444
  },
36945
37445
  aft_zoom: {
36946
- description: "Inspect code symbols or documentation sections. For code, returns the full source of a symbol. Pass `callgraph: true` to also include call-graph annotations (calls-out / called-by within the same file). For Markdown and HTML, returns the section content under the given heading.\n\nUse exactly ONE mode: `{ filePath, symbols }`, `{ url, symbols }`, or `{ targets }`. `symbols` can be a string or array (one or many lookups in the same file/URL). Use `targets` for cross-file batches: `{ filePath, symbol }` or an array of them.",
37446
+ description: "Inspect code symbols or documentation sections. For code, returns the full source of a symbol. Pass `callgraph: true` to also include call-graph annotations (calls-out / called-by within the same file). For Markdown and HTML, returns the section content under the given heading.\n\nUse exactly ONE mode: `{ path, symbols }`, `{ url, symbols }`, or `{ targets }`. `symbols` can be a string or array (one or many lookups in the same file/URL). Use `targets` for cross-file batches: `{ path, symbol }` or an array of them.",
36947
37447
  args: {
36948
- filePath: z12.string().optional().describe("Path to file (absolute or relative to project root)"),
37448
+ path: z12.string().optional().describe("Path to file (absolute or relative to project root)"),
36949
37449
  url: z12.string().optional().describe("HTTP/HTTPS URL of an HTML or Markdown document to fetch and zoom into"),
36950
37450
  symbols: z12.union([z12.string(), z12.array(z12.string())]).optional().describe("Symbol name for code, or heading text for Markdown/HTML. Pass a string for one lookup or an array for batched lookups in the same file/URL."),
36951
37451
  targets: z12.union([
36952
37452
  z12.object({
36953
- filePath: z12.string().describe("Path to file (absolute or relative to project root)"),
37453
+ path: z12.string().describe("Path to file (absolute or relative to project root)"),
36954
37454
  symbol: z12.string().describe("Symbol name in that file")
36955
37455
  }),
36956
37456
  z12.array(z12.object({
36957
- filePath: z12.string().describe("Path to file (absolute or relative to project root)"),
37457
+ path: z12.string().describe("Path to file (absolute or relative to project root)"),
36958
37458
  symbol: z12.string().describe("Symbol name in that file")
36959
37459
  }))
36960
- ]).optional().describe("Cross-file batch: `{ filePath, symbol }` or an array of them. Mutually exclusive with filePath/url/symbols."),
37460
+ ]).optional().describe("Cross-file batch: `{ path, symbol }` or an array of them. Mutually exclusive with path/url/symbols."),
36961
37461
  contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Lines of context before/after the symbol (default: 3)"),
36962
37462
  callgraph: z12.boolean().optional().describe("Include call-graph annotations (calls-out / called-by within the same file). Default false; off keeps zoom output minimal.")
36963
37463
  },
@@ -36968,7 +37468,7 @@ function readingTools(ctx) {
36968
37468
  const entryEmpty = (entry) => {
36969
37469
  if (!entry || typeof entry !== "object")
36970
37470
  return true;
36971
- const fp = entry.filePath;
37471
+ const fp = entry.path;
36972
37472
  const sym = entry.symbol;
36973
37473
  const fpEmpty = typeof fp !== "string" || fp.length === 0;
36974
37474
  const symEmpty = typeof sym !== "string" || sym.length === 0;
@@ -36978,7 +37478,7 @@ function readingTools(ctx) {
36978
37478
  return !t.every(entryEmpty);
36979
37479
  return !entryEmpty(t);
36980
37480
  };
36981
- const hasFilePath = !isEmptyParam(args.filePath);
37481
+ const hasFilePath = !isEmptyParam(args.path);
36982
37482
  const hasUrl = !isEmptyParam(args.url);
36983
37483
  const hasTargets = hasTargetsProvided(args.targets);
36984
37484
  const hasSymbols = !isEmptyParam(args.symbols);
@@ -36987,7 +37487,7 @@ function readingTools(ctx) {
36987
37487
  const zoomTitle = buildZoomTitle(args);
36988
37488
  const zoomDisplay = { title: zoomTitle };
36989
37489
  if (hasFilePath)
36990
- zoomDisplay.filePath = args.filePath;
37490
+ zoomDisplay.path = args.path;
36991
37491
  if (hasUrl)
36992
37492
  zoomDisplay.url = args.url;
36993
37493
  if (hasSymbols) {
@@ -37006,25 +37506,31 @@ function readingTools(ctx) {
37006
37506
  });
37007
37507
  if (hasTargets) {
37008
37508
  if (hasFilePath || hasUrl || hasSymbols) {
37009
- throw new Error("'targets' is mutually exclusive with 'filePath', 'url', and 'symbols'");
37509
+ throw new Error("'targets' is mutually exclusive with 'path', 'url', and 'symbols'");
37010
37510
  }
37011
37511
  const targets = Array.isArray(args.targets) ? args.targets : [args.targets];
37012
37512
  if (targets.length === 0) {
37013
37513
  throw new Error("'targets' must be a non-empty object or array");
37014
37514
  }
37015
37515
  for (const [i, entry] of targets.entries()) {
37016
- if (!entry || typeof entry.filePath !== "string" || entry.filePath.length === 0) {
37017
- throw new Error(`targets[${i}].filePath must be a non-empty string`);
37516
+ const targetPath = entry?.path;
37517
+ if (typeof targetPath !== "string" || targetPath.length === 0) {
37518
+ throw new Error(`targets[${i}].path must be a non-empty string`);
37018
37519
  }
37019
37520
  if (typeof entry.symbol !== "string" || entry.symbol.length === 0) {
37020
37521
  throw new Error(`targets[${i}].symbol must be a non-empty string`);
37021
37522
  }
37022
37523
  }
37023
- const resolvedTargets = await Promise.all(targets.map((t) => resolvePathArg(ctx, context, t.filePath)));
37524
+ const resolvedTargets = await Promise.all(targets.map((t) => resolvePathArg(ctx, context, t.path)));
37024
37525
  const permissionDenied = await assertPathExternalPermissions(ctx, context, resolvedTargets);
37025
37526
  if (permissionDenied)
37026
37527
  return permissionDeniedResponse(permissionDenied);
37027
- const rawArgs2 = { targets };
37528
+ const rawArgs2 = {
37529
+ targets: targets.map((target) => ({
37530
+ filePath: target.path,
37531
+ symbol: target.symbol
37532
+ }))
37533
+ };
37028
37534
  if (contextLines !== undefined)
37029
37535
  rawArgs2.contextLines = contextLines;
37030
37536
  if (wantCallgraph)
@@ -37036,18 +37542,18 @@ function readingTools(ctx) {
37036
37542
  return withMeta(response2.text);
37037
37543
  }
37038
37544
  if (!hasFilePath && !hasUrl) {
37039
- throw new Error("Provide exactly one of 'filePath', 'url', or 'targets'");
37545
+ throw new Error("Provide exactly one of 'path', 'url', or 'targets'");
37040
37546
  }
37041
37547
  if (hasFilePath && hasUrl) {
37042
- throw new Error("Provide exactly ONE of 'filePath' or 'url' — not both");
37548
+ throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
37043
37549
  }
37044
37550
  if (!hasUrl) {
37045
- const file2 = await resolvePathArg(ctx, context, args.filePath);
37551
+ const file2 = await resolvePathArg(ctx, context, args.path);
37046
37552
  const permissionDenied = await assertPathExternalPermissions(ctx, context, file2);
37047
37553
  if (permissionDenied)
37048
37554
  return permissionDeniedResponse(permissionDenied);
37049
37555
  }
37050
- const rawArgs = hasUrl ? { url: args.url } : { filePath: args.filePath };
37556
+ const rawArgs = hasUrl ? { url: args.url } : { filePath: args.path };
37051
37557
  if (hasSymbols)
37052
37558
  rawArgs.symbols = args.symbols;
37053
37559
  if (contextLines !== undefined)
@@ -37061,7 +37567,7 @@ function readingTools(ctx) {
37061
37567
  return withMeta(response.text);
37062
37568
  }
37063
37569
  }
37064
- };
37570
+ });
37065
37571
  }
37066
37572
  async function permissionKindForPath(resolvedPath) {
37067
37573
  try {
@@ -37151,7 +37657,7 @@ async function queryLspHints(client, symbolName, directory, sessionId) {
37151
37657
  // src/tools/refactoring.ts
37152
37658
  var z13 = tool13.schema;
37153
37659
  function refactoringTools(ctx) {
37154
- return {
37660
+ return prepareToolMap({
37155
37661
  aft_refactor: {
37156
37662
  description: `Workspace-wide refactoring that updates imports and references across files.
37157
37663
 
@@ -37161,7 +37667,7 @@ function refactoringTools(ctx) {
37161
37667
  ` + "- 'inline': replace a function call with the function's body.",
37162
37668
  args: {
37163
37669
  op: z13.enum(["move", "extract", "inline"]).describe("Refactoring operation"),
37164
- filePath: z13.string().describe("Path to the source file (absolute or relative to project root)"),
37670
+ path: z13.string().describe("Path to the source file (absolute or relative to project root)"),
37165
37671
  symbol: z13.string().optional().describe("Symbol name — required for 'move' and 'inline' ops"),
37166
37672
  destination: z13.string().optional().describe("Target file path — required for 'move' op"),
37167
37673
  scope: z13.string().optional().describe("Disambiguation scope for 'move' op — when multiple top-level symbols share the same name, specify the containing scope to disambiguate (e.g. 'MyClass'). Does NOT enable access to nested symbols or class methods."),
@@ -37192,7 +37698,7 @@ function refactoringTools(ctx) {
37192
37698
  if (op === "inline" && callSiteLine === undefined) {
37193
37699
  throw new Error("'callSiteLine' is required for 'inline' op");
37194
37700
  }
37195
- const filePath = await resolvePathArg(ctx, context, args.filePath);
37701
+ const filePath = await resolvePathArg(ctx, context, args.path);
37196
37702
  const destination = op === "move" ? await resolvePathArg(ctx, context, args.destination) : undefined;
37197
37703
  const patterns = op === "move" ? resolveRelativePatterns(context, [
37198
37704
  workspacePattern(context),
@@ -37246,7 +37752,7 @@ function refactoringTools(ctx) {
37246
37752
  return response.text;
37247
37753
  }
37248
37754
  }
37249
- };
37755
+ });
37250
37756
  }
37251
37757
 
37252
37758
  // src/tools/safety.ts
@@ -37273,15 +37779,15 @@ function relativePatternsFromPaths(context, paths) {
37273
37779
  return patterns;
37274
37780
  }
37275
37781
  function safetyTools(ctx) {
37276
- return {
37782
+ return prepareToolMap({
37277
37783
  aft_safety: {
37278
37784
  description: `File safety and recovery operations.
37279
37785
 
37280
37786
  ` + `Per-file undo stack is capped at 20 entries (oldest evicted).
37281
37787
 
37282
37788
  ` + `Ops:
37283
- ` + `- 'undo': Undo the entire last tool call when 'filePath' is omitted (typical), or undo the last edit to one file when 'filePath' is provided. Note: pops from the undo stack (irreversible, no redo). Use 'history' to inspect per-file history before undoing.
37284
- ` + `- 'history': List all edit snapshots for a file. Requires 'filePath'.
37789
+ ` + `- 'undo': Undo the entire last tool call when 'path' is omitted (typical), or undo the last edit to one file when 'path' is provided. Note: pops from the undo stack (irreversible, no redo). Use 'history' to inspect per-file history before undoing.
37790
+ ` + `- 'history': List all edit snapshots for a file. Requires 'path'.
37285
37791
  ` + `- 'checkpoint': Save a named snapshot of tracked files. Requires 'name'. Optional 'files' to snapshot specific files only.
37286
37792
  ` + `- 'restore': Restore files to a previously saved checkpoint. Requires 'name'.
37287
37793
  ` + `- 'list': List all available named checkpoints. No extra params needed.
@@ -37291,22 +37797,22 @@ function safetyTools(ctx) {
37291
37797
  ` + "Use checkpoint before risky multi-file changes. Use undo for quick single-file rollback.",
37292
37798
  args: {
37293
37799
  op: z14.enum(["undo", "history", "checkpoint", "restore", "list"]).describe("Safety operation"),
37294
- filePath: z14.string().optional().describe("File path (required for history, optional for undo). Absolute or relative to project root"),
37800
+ path: z14.string().optional().describe("File path (required for history, optional for undo). Absolute or relative to project root"),
37295
37801
  name: z14.string().optional().describe("Checkpoint name (required for checkpoint, restore)"),
37296
37802
  files: z14.array(z14.string()).optional().describe("Specific files to include in checkpoint (optional, defaults to all tracked files)")
37297
37803
  },
37298
37804
  execute: async (args, context) => {
37299
37805
  const op = args.op;
37300
- if (op === "history" && typeof args.filePath !== "string") {
37301
- throw new Error(`'filePath' is required for '${op}' op`);
37806
+ if (op === "history" && typeof args.path !== "string") {
37807
+ throw new Error(`'path' is required for '${op}' op`);
37302
37808
  }
37303
37809
  if ((op === "checkpoint" || op === "restore") && typeof args.name !== "string") {
37304
37810
  throw new Error(`'name' is required for '${op}' op`);
37305
37811
  }
37306
37812
  if (op === "undo") {
37307
37813
  const previewParams = {};
37308
- if (typeof args.filePath === "string")
37309
- previewParams.file = args.filePath;
37814
+ if (typeof args.path === "string")
37815
+ previewParams.file = args.path;
37310
37816
  const preview2 = await callBridge(ctx, context, "undo_preview", previewParams);
37311
37817
  if (preview2.success === false) {
37312
37818
  throw new Error(bridgeErrorMessage(preview2, "undo preview failed"));
@@ -37317,14 +37823,14 @@ function safetyTools(ctx) {
37317
37823
  if (denial)
37318
37824
  return permissionDeniedResponse(denial);
37319
37825
  }
37320
- const filePath = typeof args.filePath === "string" ? resolveAbsolutePath(context, args.filePath) : undefined;
37826
+ const filePath = typeof args.path === "string" ? resolveAbsolutePath(context, args.path) : undefined;
37321
37827
  const permissionError = await askEditPermission(context, relativePatternsFromPaths(context, previewPaths), filePath ? { filepath: filePath } : { operation: "undo", paths: previewPaths });
37322
37828
  if (permissionError)
37323
37829
  return permissionDeniedResponse(permissionError);
37324
37830
  }
37325
37831
  if (op === "checkpoint") {
37326
37832
  const coercedFiles = coerceStringArray(args.files);
37327
- const checkpointFiles = coercedFiles.length > 0 ? coercedFiles : typeof args.filePath === "string" ? [args.filePath] : undefined;
37833
+ const checkpointFiles = coercedFiles.length > 0 ? coercedFiles : typeof args.path === "string" ? [args.path] : undefined;
37328
37834
  if (Array.isArray(checkpointFiles)) {
37329
37835
  const projectRoot = await resolveProjectRoot(ctx, context);
37330
37836
  const uniqueParents = new Set;
@@ -37365,7 +37871,7 @@ function safetyTools(ctx) {
37365
37871
  if (args.name !== undefined)
37366
37872
  rawArgs.name = args.name;
37367
37873
  const payloadFiles = coerceStringArray(args.files).map(expandTilde2);
37368
- const filePathArg = typeof args.filePath === "string" ? expandTilde2(args.filePath) : undefined;
37874
+ const filePathArg = typeof args.path === "string" ? expandTilde2(args.path) : undefined;
37369
37875
  if (filePathArg !== undefined)
37370
37876
  rawArgs.filePath = filePathArg;
37371
37877
  if (payloadFiles.length > 0)
@@ -37377,7 +37883,7 @@ function safetyTools(ctx) {
37377
37883
  return response.text;
37378
37884
  }
37379
37885
  }
37380
- };
37886
+ });
37381
37887
  }
37382
37888
 
37383
37889
  // src/tools/search.ts
@@ -37562,15 +38068,12 @@ function arg3(schema) {
37562
38068
  function semanticTools(ctx) {
37563
38069
  const searchTool = {
37564
38070
  description: [
37565
- "Search code with one tool: concepts, identifiers, error strings, regex, literals, and filenames are auto-routed to the right engine and returned ranked. For conceptual 'how does X work' queries, phrase a full natural-language sentence — the semantic lane is NL-aware and matches intent against docstrings and comments ('how does the ORM build and execute a query', 'where is rate limiting handled'), not just keywords. Exact names, strings, and regex stay terse ('^export', 'Cargo.lock').",
37566
- "",
37567
- "Set hint to 'regex', 'literal', or 'semantic' to force a lane."
38071
+ "Search code with one tool: concepts, identifiers, error strings, regex, literals, and filenames are auto-routed to the right engine and returned ranked. For conceptual 'how does X work' queries, phrase a full natural-language sentence — the semantic lane is NL-aware and matches intent against docstrings and comments ('how does the ORM build and execute a query', 'where is rate limiting handled'), not just keywords. Exact names, strings, and regex stay terse ('^export', 'Cargo.lock')."
37568
38072
  ].join(`
37569
38073
  `),
37570
38074
  args: {
37571
38075
  query: arg3(z15.string().describe("Concept, regex, literal text, filename, or capability to find. Examples: 'fuzzy match with whitespace tolerance', '^export', 'Cargo.lock'.")),
37572
38076
  topK: arg3(optionalInt(1, 100).describe("Number of results (default: 10, max: 100)")),
37573
- hint: arg3(z15.enum(["regex", "literal", "semantic", "auto"]).optional().describe("Optional routing hint. Defaults to 'auto'.")),
37574
38077
  includeTests: arg3(z15.boolean().optional().describe("Include test files (*.test.*, *_test.rs, __tests__/, …) plus test-support, fixture, mock, snapshot, and corpus files. Defaults to false.")),
37575
38078
  path: arg3(z15.string().optional().describe("Search a different project root (absolute or ~ path). Requires that project to have been indexed by AFT."))
37576
38079
  },
@@ -37579,24 +38082,19 @@ function semanticTools(ctx) {
37579
38082
  throw new Error("semantic_search: invalid params: `query` must be a non-empty string");
37580
38083
  }
37581
38084
  const query = args.query;
37582
- const hint = typeof args.hint === "string" ? args.hint : undefined;
37583
38085
  const pathArg = typeof args.path === "string" && args.path.trim() ? args.path.trim() : undefined;
37584
- if (hint !== "semantic") {
37585
- const denied = await askSearchPermission(context, query);
37586
- if (denied)
37587
- return permissionDeniedResponse(denied);
37588
- }
38086
+ const denied = await askSearchPermission(context, query);
38087
+ if (denied)
38088
+ return permissionDeniedResponse(denied);
37589
38089
  if (pathArg) {
37590
- const denied = await assertAftSearchExternalPermission(ctx, context, pathArg);
37591
- if (denied)
37592
- return permissionDeniedResponse(denied);
38090
+ const denied2 = await assertAftSearchExternalPermission(ctx, context, pathArg);
38091
+ if (denied2)
38092
+ return permissionDeniedResponse(denied2);
37593
38093
  }
37594
38094
  const rawArgs = { query };
37595
38095
  const topK = coerceOptionalInt(args.topK, "topK", 1, 100);
37596
38096
  if (topK !== undefined)
37597
38097
  rawArgs.topK = topK;
37598
- if (hint)
37599
- rawArgs.hint = hint;
37600
38098
  if (typeof args.includeTests === "boolean")
37601
38099
  rawArgs.includeTests = args.includeTests;
37602
38100
  if (pathArg)
@@ -37614,6 +38112,37 @@ function semanticTools(ctx) {
37614
38112
  };
37615
38113
  }
37616
38114
 
38115
+ // src/tool-registration.ts
38116
+ var ALL_ONLY_TOOLS = ["aft_callgraph", "aft_delete", "aft_move", "aft_refactor"];
38117
+ function buildOpenCodeToolMap(ctx, config2, onUnknownDisabled) {
38118
+ const surface = config2.tool_surface ?? "recommended";
38119
+ const allTools = normalizeToolMap({
38120
+ ...surface !== "minimal" && (config2.hoist_builtin_tools !== false ? hoistedTools(ctx) : aftPrefixedTools(ctx)),
38121
+ ...readingTools(ctx),
38122
+ ...config2.backup?.enabled === false ? {} : safetyTools(ctx),
38123
+ ...surface !== "minimal" && importTools(ctx),
38124
+ ...navigationTools(ctx),
38125
+ ...surface !== "minimal" && astTools(ctx),
38126
+ ...surface !== "minimal" && config2.semantic_search === true && semanticTools(ctx),
38127
+ ...inspectToolSurfaceEnabled(config2) && inspectTools(ctx),
38128
+ ...surface !== "minimal" && config2.search_index === true && searchTools(ctx),
38129
+ ...refactoringTools(ctx),
38130
+ ...surface !== "minimal" && conflictTools(ctx)
38131
+ });
38132
+ if (surface !== "all") {
38133
+ for (const name of ALL_ONLY_TOOLS)
38134
+ delete allTools[name];
38135
+ }
38136
+ for (const name of config2.disabled_tools ?? []) {
38137
+ if (name in allTools) {
38138
+ delete allTools[name];
38139
+ } else {
38140
+ onUnknownDisabled?.(name, Object.keys(allTools));
38141
+ }
38142
+ }
38143
+ return allTools;
38144
+ }
38145
+
37617
38146
  // src/workflow-hints.ts
37618
38147
  var HEADING = "## IMPORTANT NOTICE about your tools";
37619
38148
  function buildWorkflowHints(opts) {
@@ -37644,18 +38173,18 @@ function buildWorkflowHints(opts) {
37644
38173
  }
37645
38174
  if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
37646
38175
  const searchName = hasSearch ? "aft_search" : grepName;
37647
- const locate = hasSearch ? '`aft_search` is the primary code-search tool: one call auto-routes concepts, identifiers, regex, error strings, and literals (pass `hint: "regex"`/`"literal"`/`"semantic"` to force a lane).' : `\`${grepName}\` (the tool — indexed and ranked) locates code.`;
38176
+ const locate = hasSearch ? "`aft_search` is the primary code-search tool: one call auto-routes concepts, identifiers, regex, error strings, and literals." : `\`${grepName}\` (the tool — indexed and ranked) locates code.`;
37648
38177
  const readName = opts.hoistBuiltins ? "read" : "aft_read";
37649
38178
  sections.push([
37650
38179
  `**Code exploration**: ${locate} Then \`aft_outline\` for structure → \`aft_zoom\` for symbol(s). DO NOT run \`grep\`/\`rg\`/\`find\`/\`sed\`/\`cat\` through \`bash\` to locate or read code — the bash path is unindexed, unranked, serial, and routinely surfaces the wrong hit. Keep \`bash\` for shell facts (git state, file metadata, processes). Reflex translations:`,
37651
38180
  `- \`grep -rn "handleAuth" src/\` in bash → \`${searchName}({ query: "handleAuth" })\``,
37652
38181
  `- \`find . -name "*.ts" | xargs grep watcher\` in bash → \`${searchName}({ query: "watcher invalidation" })\` (concepts work too)`,
37653
- `- \`sed -n '100,160p' app.ts\` / \`cat app.ts\` in bash → \`${readName}({ filePath: "app.ts", startLine: 100, endLine: 160 })\``
38182
+ `- \`sed -n '100,160p' app.ts\` / \`cat app.ts\` in bash → \`${readName}({ path: "app.ts", startLine: 100, endLine: 160 })\``
37654
38183
  ].join(`
37655
38184
  `));
37656
38185
  }
37657
38186
  if (hasInspect) {
37658
- sections.push("**Codebase health & diagnostics**: AFT does not surface compile/type errors automatically after edits — pull them with `aft_inspect`. Run it after a batch of edits and before you run tests or commit, when starting in unfamiliar code, or before a refactor/review. One call summarizes diagnostics (compile/type errors), TODOs, metrics, dead code, unused exports, and duplicates; pass `sections` for focused drill-down and `scope` to actively pull diagnostics for a specific file or directory. Its diagnostics are a fast checkpoint, not the authority — a clean `tsc` / `cargo check` / `pyright` run is the real gate. Treat `stale_categories` as a genuine stale-cache signal while an async Tier 2 refresh catches up.");
38187
+ sections.push("**Codebase health & diagnostics**: AFT does not surface compile/type errors automatically after edits — pull them with `aft_inspect`. Run it after a batch of edits and before you run tests or commit, when starting in unfamiliar code, or before a refactor/review. One call summarizes diagnostics (compile/type errors), TODOs, metrics, dead code, unused exports, and duplicates; pass `sections` for focused drill-down and `scope` to actively pull diagnostics for a specific file or directory. Its diagnostics are a fast checkpoint, not the authority — a clean `tsc` / `cargo check` / `pyright` run is the real gate. Treat stale_categories/pending_categories as stale or incomplete cache state. AFT schedules a Tier-2 refresh after its next idle or inspect-triggered background run; use one later normal aft_inspect after that refresh, not a polling loop.");
37659
38188
  sections.push("**AFT status bar**: tool results may end with a one-line health bar `[AFT E<errors> W<warnings> | D<dead-code> U<unused-exports> C<clone/dup-groups> | T<todos>]` — an IDE-style glance that appears when a count changes. `E`/`W` are live LSP diagnostics for files touched this session (your universal compile-error signal across every language with an LSP). A `~` before `D` means the dead-code/unused/dup counts predate your latest edit — run `aft_inspect` for current numbers and detail. When `E>0`, you likely just introduced errors; investigate before moving on.");
37660
38189
  }
37661
38190
  if (hasNavigate) {
@@ -37700,6 +38229,18 @@ function buildHintsFromConfig(config2, disabledTools) {
37700
38229
  disabledTools
37701
38230
  });
37702
38231
  }
38232
+ function appendHintsToSystem(system, hintsBlock) {
38233
+ if (!hintsBlock)
38234
+ return;
38235
+ if (system.length > 0) {
38236
+ const last = system.length - 1;
38237
+ system[last] = `${system[last]}
38238
+
38239
+ ${hintsBlock}`;
38240
+ } else {
38241
+ system.push(hintsBlock);
38242
+ }
38243
+ }
37703
38244
 
37704
38245
  // src/index.ts
37705
38246
  setActiveLogger(bridgeLogger);
@@ -37728,11 +38269,11 @@ var PLUGIN_VERSION = (() => {
37728
38269
  return "0.0.0";
37729
38270
  }
37730
38271
  })();
37731
- var ANNOUNCEMENT_VERSION = "0.48.0";
38272
+ var ANNOUNCEMENT_VERSION = "0.49.1";
37732
38273
  var ANNOUNCEMENT_FEATURES = [
37733
- "New (beta, off by default): an optional native sandbox for hoisted bash. Confines commands to your project and blocks reads of your credentials (~/.ssh, ~/.aws, cloud configs). Enable with sandbox.enabled in aft.jsonc — macOS and Linux; see docs/config.md for the platform matrix.",
37734
- "Fixed: Windows background bash tasks could report failed with no exit code after a detach/rebind completion is now recorded reliably.",
37735
- "Fixed: search no longer rebuilds its index unnecessarily for repositories with grafted history."
38274
+ "Windows: language servers spawn again under extended-length paths, config edits reach running servers, and dead-code/diagnostics results no longer silently drop one canonical path form across the LSP and inspect surface.",
38275
+ "Faster answers under load: completed bash commands respond immediately instead of waiting behind background maintenance (thanks @hheei), and chained commands like `cargo test && ...` keep every command's output.",
38276
+ "Permission rules with absolute paths outside the project root now match (thanks @iceteaSA), and slow embedding providers get a clear timeout message naming the setting that raises the budget."
37736
38277
  ];
37737
38278
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
37738
38279
  var plugin = async (input) => initializePluginForDirectory(input);
@@ -38168,38 +38709,12 @@ Install: ${getManualInstallHint()}`).catch(() => {});
38168
38709
  } else {
38169
38710
  cleanupWarnings(notifyOpts).catch(() => {});
38170
38711
  }
38171
- const surface = aftConfig.tool_surface ?? "recommended";
38172
- const ALL_ONLY_TOOLS = new Set(["aft_callgraph", "aft_delete", "aft_move", "aft_refactor"]);
38173
- const allTools = normalizeToolMap({
38174
- ...surface !== "minimal" && (aftConfig.hoist_builtin_tools !== false ? hoistedTools(ctx) : aftPrefixedTools(ctx)),
38175
- ...readingTools(ctx),
38176
- ...aftConfig.backup?.enabled === false ? {} : safetyTools(ctx),
38177
- ...surface !== "minimal" && importTools(ctx),
38178
- ...navigationTools(ctx),
38179
- ...surface !== "minimal" && astTools(ctx),
38180
- ...surface !== "minimal" && aftConfig.semantic_search === true && semanticTools(ctx),
38181
- ...inspectToolSurfaceEnabled(aftConfig) && inspectTools(ctx),
38182
- ...surface !== "minimal" && aftConfig.search_index === true && searchTools(ctx),
38183
- ...refactoringTools(ctx),
38184
- ...surface !== "minimal" && conflictTools(ctx)
38712
+ const allTools = buildOpenCodeToolMap(ctx, aftConfig, (name, available) => {
38713
+ warn2(`disabled_tools: "${name}" not found available: ${available.join(", ")}`);
38185
38714
  });
38186
- if (surface !== "all") {
38187
- for (const name of ALL_ONLY_TOOLS) {
38188
- if (name in allTools) {
38189
- delete allTools[name];
38190
- }
38191
- }
38192
- }
38193
- const disabled = new Set(aftConfig.disabled_tools ?? []);
38194
- if (disabled.size > 0) {
38195
- for (const name of disabled) {
38196
- if (name in allTools) {
38197
- delete allTools[name];
38198
- } else {
38199
- warn2(`disabled_tools: "${name}" not found — available: ${Object.keys(allTools).join(", ")}`);
38200
- }
38201
- }
38202
- log2(`Disabled ${disabled.size} tool(s): ${[...disabled].join(", ")}`);
38715
+ const disabled = aftConfig.disabled_tools ?? [];
38716
+ if (disabled.length > 0) {
38717
+ log2(`Disabled ${disabled.length} tool(s): ${disabled.join(", ")}`);
38203
38718
  }
38204
38719
  instrumentToolMap(allTools);
38205
38720
  const autoUpdateEventHook = createAutoUpdateCheckerHook(input, {
@@ -38259,9 +38774,9 @@ Install: ${getManualInstallHint()}`).catch(() => {});
38259
38774
  return {
38260
38775
  tool: allTools,
38261
38776
  "experimental.chat.system.transform": async (_input, output) => {
38262
- if (hintsBlock) {
38263
- output.system.push(hintsBlock);
38264
- }
38777
+ if (!hintsBlock)
38778
+ return;
38779
+ appendHintsToSystem(output.system, hintsBlock);
38265
38780
  },
38266
38781
  event: async (eventInput) => {
38267
38782
  await autoUpdateEventHook(eventInput);
@@ -38314,7 +38829,8 @@ Install: ${getManualInstallHint()}`).catch(() => {});
38314
38829
  const sessionDir = getSessionDirectoryCached(sid) ?? await getSessionDirectory(input.client, sid, input.directory) ?? input.directory;
38315
38830
  signalBashWaitDetachForProject(pool, sessionDir, sid);
38316
38831
  },
38317
- "tool.execute.before": async (toolInput) => {
38832
+ "tool.execute.before": async (toolInput, output) => {
38833
+ output.args = prepareOpenCodeArguments(toolInput.tool, output.args);
38318
38834
  if (toolInput.sessionID)
38319
38835
  inspectTier2Idle.clear(toolInput.sessionID);
38320
38836
  },