@cortexkit/aft-opencode 0.48.1 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9783,6 +9783,23 @@ async function renameIfPresent(from, to) {
9783
9783
  throw error2;
9784
9784
  }
9785
9785
  }
9786
+ // ../aft-bridge/dist/error-contract.js
9787
+ class AftToolError extends Error {
9788
+ code;
9789
+ response;
9790
+ constructor(message, code, response) {
9791
+ const cause = { code, message, response };
9792
+ super(message, { cause });
9793
+ this.name = "AftToolError";
9794
+ this.code = code;
9795
+ this.response = response;
9796
+ }
9797
+ }
9798
+ function toolErrorFromResponse(command, response) {
9799
+ const code = typeof response.code === "string" && response.code.length > 0 ? response.code : "unknown_error";
9800
+ const message = typeof response.message === "string" && response.message.length > 0 ? response.message : `${command} failed`;
9801
+ return new AftToolError(message, code, response);
9802
+ }
9786
9803
  // ../aft-bridge/dist/jsonc.js
9787
9804
  function stripJsoncSymbols(value) {
9788
9805
  if (Array.isArray(value)) {
@@ -11404,6 +11421,420 @@ function isProcessAlive(pid) {
11404
11421
  return true;
11405
11422
  }
11406
11423
  }
11424
+ // ../aft-bridge/dist/path-aliases.js
11425
+ class InvalidRequestError extends AftToolError {
11426
+ constructor(message) {
11427
+ super(message, "invalid_request", {
11428
+ success: false,
11429
+ code: "invalid_request",
11430
+ message
11431
+ });
11432
+ this.name = "InvalidRequestError";
11433
+ }
11434
+ }
11435
+ function isWellFormedUnicodeString(value) {
11436
+ for (let index = 0;index < value.length; index++) {
11437
+ const codeUnit = value.charCodeAt(index);
11438
+ if (codeUnit >= 55296 && codeUnit <= 56319) {
11439
+ const next = value.charCodeAt(index + 1);
11440
+ if (next < 56320 || next > 57343 || Number.isNaN(next))
11441
+ return false;
11442
+ index++;
11443
+ } else if (codeUnit >= 56320 && codeUnit <= 57343) {
11444
+ return false;
11445
+ }
11446
+ }
11447
+ return true;
11448
+ }
11449
+ function hasOwn(record, key) {
11450
+ return Object.hasOwn(record, key);
11451
+ }
11452
+ function invalidPathValue(property) {
11453
+ throw new InvalidRequestError(`'${property}' must be a non-empty well-formed Unicode string`);
11454
+ }
11455
+ function pathValue(record, property) {
11456
+ const value = record[property];
11457
+ if (typeof value !== "string" || value.length === 0 || !isWellFormedUnicodeString(value)) {
11458
+ invalidPathValue(property);
11459
+ }
11460
+ return value;
11461
+ }
11462
+ function normalizeAliasPair(record, canonical, legacy, required) {
11463
+ const hasCanonical = hasOwn(record, canonical);
11464
+ const hasLegacy = hasOwn(record, legacy);
11465
+ if (!hasCanonical && !hasLegacy) {
11466
+ if (required) {
11467
+ throw new InvalidRequestError(`'${canonical}' is required`);
11468
+ }
11469
+ return;
11470
+ }
11471
+ if (hasCanonical && hasLegacy) {
11472
+ let canonicalValue;
11473
+ let legacyValue;
11474
+ try {
11475
+ canonicalValue = pathValue(record, canonical);
11476
+ legacyValue = pathValue(record, legacy);
11477
+ } catch {
11478
+ throw new InvalidRequestError(`Invalid request: '${canonical}' and '${legacy}' must both be non-empty well-formed Unicode strings`);
11479
+ }
11480
+ if (canonicalValue !== legacyValue) {
11481
+ throw new InvalidRequestError(`Invalid request: '${canonical}' and '${legacy}' must contain equal decoded strings`);
11482
+ }
11483
+ delete record[legacy];
11484
+ return;
11485
+ }
11486
+ if (hasCanonical) {
11487
+ pathValue(record, canonical);
11488
+ return;
11489
+ }
11490
+ record[canonical] = pathValue(record, legacy);
11491
+ delete record[legacy];
11492
+ }
11493
+ function validateOptionalCanonicalPath(record, property) {
11494
+ if (hasOwn(record, property))
11495
+ pathValue(record, property);
11496
+ }
11497
+ function normalizeZoomTargets(record) {
11498
+ if (!hasOwn(record, "targets"))
11499
+ return;
11500
+ const targets = record.targets;
11501
+ const normalizeTarget = (target, index) => {
11502
+ if (!target || typeof target !== "object" || Array.isArray(target)) {
11503
+ throw new InvalidRequestError(`'targets[${index}].path' must be a non-empty string`);
11504
+ }
11505
+ const source = target;
11506
+ const emptyTarget = source.symbol === "" && (hasOwn(source, "path") && source.path === "" || hasOwn(source, "filePath") && source.filePath === "");
11507
+ if (emptyTarget)
11508
+ return { ...source };
11509
+ const normalized = { ...source };
11510
+ try {
11511
+ normalizeAliasPair(normalized, "path", "filePath", true);
11512
+ } catch (error2) {
11513
+ if (error2 instanceof InvalidRequestError) {
11514
+ throw new InvalidRequestError(error2.message.replace("'filePath'", `'targets[${index}].filePath'`).replace("'path'", `'targets[${index}].path'`));
11515
+ }
11516
+ throw error2;
11517
+ }
11518
+ return normalized;
11519
+ };
11520
+ if (Array.isArray(targets)) {
11521
+ if (targets.length === 0)
11522
+ return;
11523
+ record.targets = targets.map(normalizeTarget);
11524
+ return;
11525
+ }
11526
+ if (targets && typeof targets === "object") {
11527
+ record.targets = normalizeTarget(targets, 0);
11528
+ }
11529
+ }
11530
+ function bareToolName(toolName) {
11531
+ const bare = toolName.startsWith("aft_") ? toolName.slice(4) : toolName;
11532
+ 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") {
11533
+ return bare;
11534
+ }
11535
+ return;
11536
+ }
11537
+ function prepareCanonicalPathArguments(toolName, rawArguments) {
11538
+ if (!rawArguments || typeof rawArguments !== "object" || Array.isArray(rawArguments)) {
11539
+ throw new InvalidRequestError("tool arguments must be an object");
11540
+ }
11541
+ const tool = bareToolName(toolName);
11542
+ const record = { ...rawArguments };
11543
+ if (!tool)
11544
+ return record;
11545
+ switch (tool) {
11546
+ case "read":
11547
+ case "write":
11548
+ case "edit":
11549
+ case "move":
11550
+ case "import":
11551
+ case "refactor":
11552
+ normalizeAliasPair(record, "path", "filePath", true);
11553
+ break;
11554
+ case "zoom":
11555
+ normalizeAliasPair(record, "path", "filePath", false);
11556
+ normalizeZoomTargets(record);
11557
+ break;
11558
+ case "callgraph":
11559
+ normalizeAliasPair(record, "path", "filePath", true);
11560
+ normalizeAliasPair(record, "toPath", "toFile", false);
11561
+ break;
11562
+ case "safety":
11563
+ normalizeAliasPair(record, "path", "filePath", false);
11564
+ break;
11565
+ case "grep":
11566
+ case "search":
11567
+ case "conflicts":
11568
+ validateOptionalCanonicalPath(record, "path");
11569
+ break;
11570
+ }
11571
+ return record;
11572
+ }
11573
+ var EDIT_ROOT_COMPATIBILITY_KEYS = new Set([
11574
+ "oldString",
11575
+ "newString",
11576
+ "replaceAll",
11577
+ "occurrence"
11578
+ ]);
11579
+ var EDIT_ROOT_CANONICAL_KEYS = new Set(["path", "appendContent", "edits", "symbol", "content"]);
11580
+ var EDIT_ITEM_KEYS = new Set([
11581
+ "oldString",
11582
+ "newString",
11583
+ "replaceAll",
11584
+ "occurrence",
11585
+ "startLine",
11586
+ "endLine",
11587
+ "content"
11588
+ ]);
11589
+ var ASCII_WHITESPACE = /^[\t\n\v\f\r ]+$/;
11590
+ var ASCII_TRIM = /^[\t\n\v\f\r ]+|[\t\n\v\f\r ]+$/g;
11591
+ var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
11592
+ function prepareCanonicalEditArguments(toolName, rawArguments) {
11593
+ if (!rawArguments || typeof rawArguments !== "object" || Array.isArray(rawArguments)) {
11594
+ throw new InvalidRequestError("tool arguments must be an object");
11595
+ }
11596
+ const raw = rawArguments;
11597
+ const record = copyOwnProperties(raw);
11598
+ normalizeEditPathAlias(record);
11599
+ const suppliedLineFields = ["startLine", "endLine"].filter((key) => hasOwn(record, key));
11600
+ if (suppliedLineFields.length > 0) {
11601
+ 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 }].");
11602
+ }
11603
+ const isOpenCodeRetiredBoundary = toolName === "aft_edit";
11604
+ const retiredFields = ["file", "mode"].filter((key) => hasOwn(record, key));
11605
+ if (retiredFields.length > 0 && isOpenCodeRetiredBoundary) {
11606
+ 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`.");
11607
+ }
11608
+ const unknownRootKeys = Object.getOwnPropertyNames(record).filter((key) => !EDIT_ROOT_CANONICAL_KEYS.has(key) && !EDIT_ROOT_COMPATIBILITY_KEYS.has(key) && key !== "filePath").sort();
11609
+ if (unknownRootKeys.length > 0) {
11610
+ throw new InvalidRequestError(formatUnknownKeys(unknownRootKeys));
11611
+ }
11612
+ const modes = editModesPresent(record);
11613
+ if (modes.length > 1) {
11614
+ throw new InvalidRequestError(`edit: conflicting modes: ${modes.join(", ")}`);
11615
+ }
11616
+ if (modes.length === 0) {
11617
+ throw new InvalidRequestError("edit: exactly one of `appendContent`, `edits`, or `symbol` plus `content` is required");
11618
+ }
11619
+ const mode = modes[0];
11620
+ if (mode === "appendContent") {
11621
+ if (typeof record.appendContent !== "string") {
11622
+ throw new InvalidRequestError("edit: 'appendContent' must be a string");
11623
+ }
11624
+ } else if (mode === "edits") {
11625
+ const parsedEdits = parseEditArray(record.edits);
11626
+ record.edits = parsedEdits.map((item, index) => normalizeEditItem(item, index));
11627
+ } else if (mode === "symbol/content") {
11628
+ if (!hasOwn(record, "symbol") || typeof record.symbol !== "string") {
11629
+ throw new InvalidRequestError("edit: 'symbol' must be a string when symbol mode is selected");
11630
+ }
11631
+ if (!hasOwn(record, "content") || typeof record.content !== "string") {
11632
+ throw new InvalidRequestError("edit: symbol mode requires both 'symbol' and 'content' string properties");
11633
+ }
11634
+ } else {
11635
+ const item = {};
11636
+ for (const key of EDIT_ROOT_COMPATIBILITY_KEYS) {
11637
+ if (hasOwn(record, key))
11638
+ item[key] = record[key];
11639
+ }
11640
+ record.edits = [normalizeEditItem(item, 0)];
11641
+ for (const key of EDIT_ROOT_COMPATIBILITY_KEYS)
11642
+ delete record[key];
11643
+ }
11644
+ validateEditPath(record);
11645
+ return record;
11646
+ }
11647
+ function normalizeEditPathAlias(record) {
11648
+ const hasCanonical = hasOwn(record, "path");
11649
+ const hasLegacy = hasOwn(record, "filePath");
11650
+ if (!hasCanonical && !hasLegacy)
11651
+ return;
11652
+ if (hasCanonical && hasLegacy) {
11653
+ let canonical;
11654
+ let legacy;
11655
+ try {
11656
+ canonical = pathValue(record, "path");
11657
+ legacy = pathValue(record, "filePath");
11658
+ } catch {
11659
+ throw new InvalidRequestError("Invalid request: 'path' and 'filePath' must both be non-empty well-formed Unicode strings");
11660
+ }
11661
+ if (canonical !== legacy) {
11662
+ throw new InvalidRequestError("Invalid request: 'path' and 'filePath' must contain equal decoded strings");
11663
+ }
11664
+ delete record.filePath;
11665
+ return;
11666
+ }
11667
+ if (!hasCanonical) {
11668
+ record.path = pathValue(record, "filePath");
11669
+ delete record.filePath;
11670
+ }
11671
+ }
11672
+ function validateEditPath(record) {
11673
+ if (!hasOwn(record, "path")) {
11674
+ throw new InvalidRequestError("'path' is required");
11675
+ }
11676
+ pathValue(record, "path");
11677
+ }
11678
+ function formatUnknownKeys(keys) {
11679
+ return `Unrecognized keys: ${keys.map((key) => `"${key}"`).join(", ")}`;
11680
+ }
11681
+ function editModesPresent(record) {
11682
+ const modes = [];
11683
+ if (hasOwn(record, "appendContent"))
11684
+ modes.push("appendContent");
11685
+ if (hasOwn(record, "edits"))
11686
+ modes.push("edits");
11687
+ if (hasOwn(record, "symbol") || hasOwn(record, "content"))
11688
+ modes.push("symbol/content");
11689
+ if (["oldString", "newString", "replaceAll", "occurrence"].some((key) => hasOwn(record, key))) {
11690
+ modes.push("oldString/newString");
11691
+ }
11692
+ return modes;
11693
+ }
11694
+ function parseEditArray(value) {
11695
+ if (typeof value === "string") {
11696
+ let parsed;
11697
+ try {
11698
+ parsed = JSON.parse(value);
11699
+ } catch {
11700
+ throw new InvalidRequestError("edit: 'edits' must contain valid JSON representing an array");
11701
+ }
11702
+ if (!Array.isArray(parsed)) {
11703
+ throw new InvalidRequestError("edit: 'edits' JSON must have an array root");
11704
+ }
11705
+ if (parsed.length === 0) {
11706
+ throw new InvalidRequestError("edit: 'edits' array must not be empty");
11707
+ }
11708
+ return parsed;
11709
+ }
11710
+ if (!Array.isArray(value)) {
11711
+ throw new InvalidRequestError("edit: 'edits' must be a non-empty array");
11712
+ }
11713
+ if (value.length === 0) {
11714
+ throw new InvalidRequestError("edit: 'edits' array must not be empty");
11715
+ }
11716
+ return value;
11717
+ }
11718
+ function normalizeEditItem(value, index) {
11719
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
11720
+ throw new InvalidRequestError(`edit: edits[${index}] must be an object`);
11721
+ }
11722
+ const source = value;
11723
+ const item = copyOwnProperties(source);
11724
+ normalizeItemAlias(item, "oldString", "oldText");
11725
+ normalizeItemAlias(item, "newString", "newText");
11726
+ const hasFindField = ["oldString", "newString", "replaceAll", "occurrence"].some((key) => hasOwn(item, key));
11727
+ const hasRangeField = ["startLine", "endLine", "content"].some((key) => hasOwn(item, key));
11728
+ if (hasFindField && hasRangeField) {
11729
+ throw new InvalidRequestError(`edit: edits[${index}] mixes find/replace and line-range fields`);
11730
+ }
11731
+ if (hasFindField) {
11732
+ if (!hasOwn(item, "oldString") || typeof item.oldString !== "string") {
11733
+ throw new InvalidRequestError(`edit: edits[${index}] requires string 'oldString'`);
11734
+ }
11735
+ if (hasOwn(item, "newString") && typeof item.newString !== "string") {
11736
+ throw new InvalidRequestError(`edit: edits[${index}].newString must be a string`);
11737
+ }
11738
+ coerceEditScalars(item, index);
11739
+ validateEditItemKeys(item, index);
11740
+ return item;
11741
+ }
11742
+ if (hasRangeField) {
11743
+ for (const key of ["startLine", "endLine"]) {
11744
+ const value2 = item[key];
11745
+ if (typeof value2 === "string" && /^[0-9]+$/.test(value2.trim())) {
11746
+ item[key] = Number(value2.trim());
11747
+ }
11748
+ if (!hasOwn(item, key) || !isPositiveSafeInteger(item[key])) {
11749
+ throw new InvalidRequestError(`edit: edits[${index}].${key} must be a positive integer`);
11750
+ }
11751
+ }
11752
+ if (item.startLine > item.endLine) {
11753
+ throw new InvalidRequestError(`edit: edits[${index}] requires startLine <= endLine`);
11754
+ }
11755
+ if (!hasOwn(item, "content") || typeof item.content !== "string") {
11756
+ throw new InvalidRequestError(`edit: edits[${index}] requires string 'content'`);
11757
+ }
11758
+ validateEditItemKeys(item, index);
11759
+ return item;
11760
+ }
11761
+ throw new InvalidRequestError(`edit: edits[${index}] must be a find/replace or line-range item`);
11762
+ }
11763
+ function normalizeItemAlias(item, canonical, legacy) {
11764
+ if (hasOwn(item, legacy)) {
11765
+ if (!hasOwn(item, canonical))
11766
+ item[canonical] = item[legacy];
11767
+ delete item[legacy];
11768
+ }
11769
+ }
11770
+ function validateEditItemKeys(item, index) {
11771
+ const unknown = Object.getOwnPropertyNames(item).filter((key) => !EDIT_ITEM_KEYS.has(key)).sort();
11772
+ if (unknown.length > 0) {
11773
+ throw new InvalidRequestError(`edit: edits[${index}] contains ${formatUnknownKeys(unknown)}`);
11774
+ }
11775
+ }
11776
+ function coerceEditScalars(item, index) {
11777
+ if (hasOwn(item, "replaceAll") && hasOwn(item, "occurrence")) {
11778
+ throw new InvalidRequestError(`edit: edits[${index}] cannot contain both 'replaceAll' and 'occurrence'`);
11779
+ }
11780
+ if (hasOwn(item, "replaceAll"))
11781
+ item.replaceAll = coerceEditBoolean(item.replaceAll, index);
11782
+ if (hasOwn(item, "occurrence")) {
11783
+ const occurrence = coerceEditOccurrence(item.occurrence, index);
11784
+ if (occurrence === undefined)
11785
+ delete item.occurrence;
11786
+ else
11787
+ item.occurrence = occurrence;
11788
+ }
11789
+ }
11790
+ function coerceEditBoolean(value, index) {
11791
+ if (typeof value === "boolean")
11792
+ return value;
11793
+ if (typeof value === "number" && Number.isFinite(value) && (value === 0 || value === 1)) {
11794
+ return value === 1;
11795
+ }
11796
+ if (typeof value === "string") {
11797
+ if (value === "1")
11798
+ return true;
11799
+ if (value === "0")
11800
+ return false;
11801
+ if (/^(?:true|false)$/i.test(value))
11802
+ return value.toLowerCase() === "true";
11803
+ }
11804
+ throw new InvalidRequestError(`edit: edits[${index}].replaceAll must be a boolean, true/false string, or 0/1`);
11805
+ }
11806
+ function coerceEditOccurrence(value, index) {
11807
+ if (value === null)
11808
+ return;
11809
+ if (typeof value === "string") {
11810
+ const trimmed = value.replace(ASCII_TRIM, "");
11811
+ if (trimmed.length === 0 || ASCII_WHITESPACE.test(trimmed))
11812
+ return;
11813
+ if (!/^[+]?[0-9]+$/.test(trimmed)) {
11814
+ throw new InvalidRequestError(`edit: edits[${index}].occurrence must be a positive integer`);
11815
+ }
11816
+ try {
11817
+ const parsed = BigInt(trimmed);
11818
+ if (parsed < 1n || parsed > BigInt(MAX_SAFE_INTEGER))
11819
+ throw new Error("out of range");
11820
+ return Number(parsed);
11821
+ } catch {
11822
+ throw new InvalidRequestError(`edit: edits[${index}].occurrence must be a positive integer`);
11823
+ }
11824
+ }
11825
+ if (typeof value === "number" && isPositiveSafeInteger(value))
11826
+ return value;
11827
+ throw new InvalidRequestError(`edit: edits[${index}].occurrence must be a positive integer`);
11828
+ }
11829
+ function isPositiveSafeInteger(value) {
11830
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
11831
+ }
11832
+ function copyOwnProperties(source) {
11833
+ const copy = Object.create(null);
11834
+ for (const key of Object.getOwnPropertyNames(source))
11835
+ copy[key] = source[key];
11836
+ return copy;
11837
+ }
11407
11838
  // ../aft-bridge/dist/project-identity.js
11408
11839
  import { createHash as createHash4 } from "node:crypto";
11409
11840
  import { realpathSync as realpathSync2 } from "node:fs";
@@ -31564,8 +31995,8 @@ function restoreAutoUpdateSnapshot(snapshot) {
31564
31995
  rmSync4(snapshot.tempDir, { recursive: true, force: true });
31565
31996
  }
31566
31997
  }
31567
- function stripPackageNameFromPath(pathValue, packageName) {
31568
- let current = pathValue;
31998
+ function stripPackageNameFromPath(pathValue2, packageName) {
31999
+ let current = pathValue2;
31569
32000
  for (const segment of [...packageName.split("/")].reverse()) {
31570
32001
  if (basename3(current) !== segment)
31571
32002
  return null;
@@ -32707,7 +33138,7 @@ async function ensureServerInstalled(spec, config2, fetchImpl, signal) {
32707
33138
  return null;
32708
33139
  });
32709
33140
  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.`);
33141
+ 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
33142
  return {
32712
33143
  started: false,
32713
33144
  reason: `TOFU sha256 mismatch on ${spec.npm}@${version2} — see plugin log`
@@ -33500,7 +33931,7 @@ async function downloadAndInstall(spec, tag, assets, platform3, arch, fetchImpl,
33500
33931
  const previousArchiveSha256 = previousMeta?.archiveSha256 ?? (previousMeta?.binarySha256 ? undefined : previousMeta?.sha256);
33501
33932
  if (previousMeta && previousMeta.version === tag && previousArchiveSha256) {
33502
33933
  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.`);
33934
+ 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
33935
  try {
33505
33936
  unlinkSync7(archivePath);
33506
33937
  } catch {}
@@ -33732,12 +34163,43 @@ function normalizeToolArgSchemas(toolDefinition) {
33732
34163
  }
33733
34164
  return toolDefinition;
33734
34165
  }
33735
- function normalizeToolMap(tools) {
33736
- for (const def of Object.values(tools)) {
33737
- normalizeToolArgSchemas(def);
34166
+ function bareToolName2(toolName) {
34167
+ return toolName.startsWith("aft_") ? toolName.slice(4) : toolName;
34168
+ }
34169
+ function prepareOpenCodeArguments(toolName, rawArguments) {
34170
+ const bare = bareToolName2(toolName);
34171
+ if (bare === "edit") {
34172
+ return prepareCanonicalEditArguments(toolName, rawArguments);
34173
+ }
34174
+ return prepareCanonicalPathArguments(toolName, rawArguments);
34175
+ }
34176
+ var DISPLAY_FILE_PATH_TOOLS = new Set(["read", "write", "edit"]);
34177
+ function preserveDisplayFilePathAlias(toolName, rawArguments, prepared) {
34178
+ if (!DISPLAY_FILE_PATH_TOOLS.has(toolName))
34179
+ return;
34180
+ if (!rawArguments || typeof rawArguments !== "object" || Array.isArray(rawArguments))
34181
+ return;
34182
+ const raw = rawArguments;
34183
+ if (typeof prepared.path === "string" && !Object.hasOwn(raw, "filePath")) {
34184
+ raw.filePath = prepared.path;
34185
+ }
34186
+ }
34187
+ function prepareToolMap(tools) {
34188
+ for (const [toolName, def] of Object.entries(tools)) {
34189
+ const execute = def.execute;
34190
+ def.execute = async (args, context) => {
34191
+ const prepared = prepareOpenCodeArguments(toolName, args);
34192
+ preserveDisplayFilePathAlias(toolName, args, prepared);
34193
+ return execute(prepared, context);
34194
+ };
33738
34195
  }
33739
34196
  return tools;
33740
34197
  }
34198
+ function normalizeToolMap(tools) {
34199
+ for (const def of Object.values(tools))
34200
+ normalizeToolArgSchemas(def);
34201
+ return prepareToolMap(tools);
34202
+ }
33741
34203
  // src/shared/ignored-message.ts
33742
34204
  async function sendIgnoredMessage2(client, sessionID, text) {
33743
34205
  const typedClient = client;
@@ -35039,8 +35501,45 @@ function astTools(ctx) {
35039
35501
  };
35040
35502
  }
35041
35503
 
35042
- // src/tools/bash.ts
35504
+ // src/tools/conflicts.ts
35043
35505
  import { tool as tool4 } from "@opencode-ai/plugin";
35506
+ var z4 = tool4.schema;
35507
+ function conflictTools(ctx) {
35508
+ return {
35509
+ aft_conflicts: {
35510
+ 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).",
35511
+ args: {
35512
+ 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()
35513
+ },
35514
+ execute: async (args, context) => {
35515
+ const rawArgs = {};
35516
+ if (!isEmptyParam(args?.path)) {
35517
+ const expanded = expandTilde2(String(args.path));
35518
+ const projectRoot = await resolveProjectRoot(ctx, context);
35519
+ const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
35520
+ const denied = await assertExternalDirectoryPermission(ctx, context, resolved, {
35521
+ kind: "directory"
35522
+ });
35523
+ if (denied)
35524
+ return permissionDeniedResponse(denied);
35525
+ rawArgs.path = resolved;
35526
+ }
35527
+ const response = await callToolCall(ctx, context, "conflicts", rawArgs);
35528
+ if (response.success === false) {
35529
+ throw new Error(response.message || "git_conflicts failed");
35530
+ }
35531
+ return response.text;
35532
+ }
35533
+ }
35534
+ };
35535
+ }
35536
+
35537
+ // src/tools/hoisted.ts
35538
+ import * as path4 from "node:path";
35539
+ import { tool as tool8 } from "@opencode-ai/plugin";
35540
+
35541
+ // src/tools/bash.ts
35542
+ import { tool as tool5 } from "@opencode-ai/plugin";
35044
35543
 
35045
35544
  // src/shared/subagent-detect.ts
35046
35545
  var CACHE_MAX_ENTRIES2 = 200;
@@ -35093,7 +35592,7 @@ function setCache2(sessionId, isSubagent) {
35093
35592
  }
35094
35593
 
35095
35594
  // src/tools/bash.ts
35096
- var z4 = tool4.schema;
35595
+ var z5 = tool5.schema;
35097
35596
  var METADATA_PREVIEW_LIMIT = 30 * 1024;
35098
35597
  var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
35099
35598
  var BASH_TRANSPORT_MARGIN_MS = 1e4;
@@ -35193,22 +35692,22 @@ async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
35193
35692
  function createBashTool(ctx, aftSearchRegisteredOverride) {
35194
35693
  const initialBashCfg = resolveBashConfig(ctx.config);
35195
35694
  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.")
35695
+ 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
35696
  } : {};
35198
35697
  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.'),
35698
+ 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
35699
  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
35700
  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
35701
  } : {};
35203
35702
  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."),
35703
+ command: z5.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
35704
+ 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."),
35705
+ 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."),
35706
+ description: z5.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
35707
+ 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."),
35708
+ 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
35709
  ...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."),
35710
+ 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
35711
  ...ptyArgs
35213
35712
  };
35214
35713
  return {
@@ -35307,8 +35806,8 @@ function createBashStatusTool(ctx) {
35307
35806
  return {
35308
35807
  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
35808
  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.")
35809
+ taskId: z5.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded."),
35810
+ 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
35811
  },
35313
35812
  execute: async (args, context) => {
35314
35813
  const taskId = args.taskId;
@@ -35322,7 +35821,7 @@ function createBashKillTool(ctx) {
35322
35821
  return {
35323
35822
  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
35823
  args: {
35325
- taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded.")
35824
+ taskId: z5.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded.")
35326
35825
  },
35327
35826
  execute: async (args, context) => {
35328
35827
  const data = await callBashBridge(ctx, context, "bash_kill", {
@@ -35414,44 +35913,6 @@ function shortenCommand(command) {
35414
35913
  return collapsed.length <= 80 ? collapsed : `${collapsed.slice(0, 77)}...`;
35415
35914
  }
35416
35915
 
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
35916
  // src/tools/bash_watch.ts
35456
35917
  import { tool as tool6 } from "@opencode-ai/plugin";
35457
35918
  var z6 = tool6.schema;
@@ -35569,7 +36030,7 @@ Waited ${waited.elapsed_ms}ms; matched ${JSON.stringify(waited.match ?? "")}${st
35569
36030
  Waited ${waited.elapsed_ms}ms; timeout reached without match.`;
35570
36031
  } else if (waited.reason === "unavailable") {
35571
36032
  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 }).`;
36033
+ 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
36034
  } else {
35574
36035
  const stat2 = String(data.status ?? "unknown");
35575
36036
  const e = typeof data.exit_code === "number" ? `, exit ${data.exit_code}` : "";
@@ -35831,6 +36292,12 @@ function relativeToWorktree(fp, worktree) {
35831
36292
  function readAttachments(data) {
35832
36293
  return Array.isArray(data.attachments) ? data.attachments : [];
35833
36294
  }
36295
+ function persistFilePathAlias(args, context) {
36296
+ if (typeof args.path === "string" && !Object.hasOwn(args, "filePath")) {
36297
+ args.filePath = args.path;
36298
+ }
36299
+ context.metadata({ metadata: {} });
36300
+ }
35834
36301
  function buildUnifiedDiff(fp, before, after) {
35835
36302
  const beforeLines = before.split(`
35836
36303
  `);
@@ -35994,19 +36461,6 @@ function inferBeforeStart(ops, from, beforeLen) {
35994
36461
  return beforeLen;
35995
36462
  }
35996
36463
  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
36464
  var READ_DESCRIPTION = `Read file contents or list directory entries.
36011
36465
 
36012
36466
  Use either startLine/endLine OR offset/limit to read a section of a file.
@@ -36020,96 +36474,99 @@ Behavior:
36020
36474
  - Directories return sorted entries with trailing / for subdirectories
36021
36475
 
36022
36476
  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/" }
36477
+ Read full file: { "path": "src/app.ts" }
36478
+ Read lines 50-100: { "path": "src/app.ts", "startLine": 50, "endLine": 100 }
36479
+ Read 30 lines from line 200: { "path": "src/app.ts", "offset": 200, "limit": 30 }
36480
+ List directory: { "path": "src/" }
36027
36481
  `;
36028
36482
  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,
36483
+ return prepareToolMap({
36484
+ read: {
36485
+ description: READ_DESCRIPTION,
36486
+ args: {
36487
+ filePath: z8.string().describe("Path to file or directory (absolute or relative to project root)"),
36488
+ startLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line to start reading from"),
36489
+ endLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line to stop reading at (inclusive)"),
36490
+ limit: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Max lines to return (default: 2000)"),
36491
+ offset: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line number to start reading from (use with limit). Ignored if startLine is provided")
36492
+ },
36493
+ execute: async (args, context) => {
36494
+ const file2 = args.path;
36495
+ const projectRoot = await resolveProjectRoot(ctx, context);
36496
+ const filePath = resolvePathFromProjectRoot(projectRoot, file2);
36497
+ persistFilePathAlias(args, context);
36498
+ {
36499
+ const denial = await assertExternalDirectoryPermission(ctx, context, filePath, {
36500
+ serverValidatedRead: true
36501
+ });
36502
+ if (denial)
36503
+ return permissionDeniedResponse(denial);
36504
+ }
36505
+ try {
36506
+ await runAsk(context.ask({
36507
+ permission: "read",
36508
+ patterns: [filePath],
36509
+ always: ["*"],
36510
+ metadata: {}
36511
+ }));
36512
+ } catch (error53) {
36513
+ if (error53 instanceof Error && error53.message)
36514
+ return permissionDeniedResponse(error53.message);
36515
+ return permissionDeniedResponse("Permission denied.");
36516
+ }
36517
+ const rawStartLine = coerceOptionalInt(args.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
36518
+ const rawEndLine = coerceOptionalInt(args.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
36519
+ const rawLimit = coerceOptionalInt(args.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
36520
+ const rawOffset = coerceOptionalInt(args.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
36521
+ let startLine = rawStartLine;
36522
+ let endLine = rawEndLine;
36523
+ if (startLine === undefined && rawOffset !== undefined) {
36524
+ startLine = rawOffset;
36525
+ if (rawLimit !== undefined) {
36526
+ endLine = rawOffset + rawLimit - 1;
36527
+ }
36528
+ }
36529
+ const rawArgs = { filePath: file2 };
36530
+ if (startLine !== undefined)
36531
+ rawArgs.startLine = startLine;
36532
+ if (endLine !== undefined)
36533
+ rawArgs.endLine = endLine;
36534
+ if (rawLimit !== undefined && rawOffset === undefined)
36535
+ rawArgs.limit = rawLimit;
36536
+ const response = await callToolCall(ctx, context, "read", rawArgs);
36537
+ if (response.success === false) {
36538
+ throw new Error(response.message || "read failed");
36539
+ }
36540
+ const dp = relativeToWorktree(filePath, projectRoot) || file2;
36541
+ const output = response.text;
36542
+ const attachments = readAttachments(response);
36543
+ if (attachments.length > 0) {
36544
+ const toolAttachments = attachments.filter((attachment) => typeof attachment.mime === "string" && typeof attachment.data === "string").map((attachment) => ({
36545
+ type: "file",
36546
+ mime: attachment.mime,
36547
+ url: `data:${attachment.mime};base64,${attachment.data}`
36548
+ }));
36549
+ if (toolAttachments.length > 0) {
36550
+ const first = attachments[0];
36551
+ const firstMime = typeof first.mime === "string" ? first.mime : "";
36552
+ return {
36553
+ output,
36103
36554
  title: dp,
36104
- isImage: first.kind === "image" || firstMime.startsWith("image/"),
36105
- isPdf: first.kind === "pdf" || firstMime === "application/pdf"
36106
- }
36107
- };
36555
+ attachments: toolAttachments,
36556
+ metadata: {
36557
+ preview: output,
36558
+ filepath: filePath,
36559
+ title: dp,
36560
+ isImage: first.kind === "image" || firstMime.startsWith("image/"),
36561
+ isPdf: first.kind === "pdf" || firstMime === "application/pdf"
36562
+ }
36563
+ };
36564
+ }
36108
36565
  }
36566
+ return { output, title: dp, metadata: { title: dp } };
36109
36567
  }
36110
- return { output, title: dp, metadata: { title: dp } };
36111
36568
  }
36112
- };
36569
+ }).read;
36113
36570
  }
36114
36571
  function getWriteDescription(ctx, editToolName) {
36115
36572
  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,10 +36580,12 @@ function createWriteTool(ctx, editToolName = "edit") {
36123
36580
  content: z8.string().describe("The full content to write to the file")
36124
36581
  },
36125
36582
  execute: async (args, context) => {
36126
- const file2 = args.filePath;
36583
+ const argsRecord = args;
36584
+ const file2 = args.path;
36127
36585
  const content = args.content;
36128
36586
  const projectRoot = await resolveProjectRoot(ctx, context);
36129
36587
  const filePath = resolvePathFromProjectRoot(projectRoot, file2);
36588
+ persistFilePathAlias(argsRecord, context);
36130
36589
  const relPath = path4.relative(projectRoot, filePath);
36131
36590
  {
36132
36591
  const denial2 = await assertExternalDirectoryPermission(ctx, context, filePath);
@@ -36136,7 +36595,7 @@ function createWriteTool(ctx, editToolName = "edit") {
36136
36595
  const rawArgs = { filePath: file2, content };
36137
36596
  const preview2 = await callToolCall(ctx, context, "write", rawArgs, { preview: true });
36138
36597
  if (preview2.success === false) {
36139
- throw new Error(preview2.message || "write preview failed");
36598
+ throw toolErrorFromResponse("write", preview2);
36140
36599
  }
36141
36600
  const denial = await askEditPermission(context, [relPath], {
36142
36601
  filepath: filePath,
@@ -36146,7 +36605,7 @@ function createWriteTool(ctx, editToolName = "edit") {
36146
36605
  return permissionDeniedResponse(denial);
36147
36606
  const data = await callToolCall(ctx, context, "write", rawArgs);
36148
36607
  if (data.success === false) {
36149
- throw new Error(data.message || "write failed");
36608
+ throw toolErrorFromResponse("write", data);
36150
36609
  }
36151
36610
  const output = data.text;
36152
36611
  const diff = data.diff;
@@ -36156,20 +36615,20 @@ function createWriteTool(ctx, editToolName = "edit") {
36156
36615
  const dp = relativeToWorktree(filePath, projectRoot);
36157
36616
  const beforeContent = diff.before ?? "";
36158
36617
  const afterContent = diff.after ?? content;
36618
+ const patch = truncated ? typeof preview2.preview_diff === "string" ? preview2.preview_diff : "" : buildUnifiedDiff(filePath, beforeContent, afterContent);
36159
36619
  return {
36160
36620
  output,
36161
36621
  title: dp,
36162
36622
  metadata: {
36163
- diff: truncated ? typeof preview2.preview_diff === "string" ? preview2.preview_diff : "" : buildUnifiedDiff(filePath, beforeContent, afterContent),
36164
- ...truncated ? {} : {
36623
+ diff: patch,
36624
+ ...patch ? {
36165
36625
  filediff: {
36166
36626
  file: filePath,
36167
- before: beforeContent,
36168
- after: afterContent,
36169
- additions: diff?.additions ?? 0,
36170
- deletions: diff?.deletions ?? 0
36627
+ patch,
36628
+ additions: diff.additions ?? 0,
36629
+ deletions: diff.deletions ?? 0
36171
36630
  }
36172
- },
36631
+ } : {},
36173
36632
  diagnostics: {}
36174
36633
  }
36175
36634
  };
@@ -36182,40 +36641,33 @@ function getEditDescription(ctx, writeToolName) {
36182
36641
 
36183
36642
  **Modes** (determined by which parameters you provide):
36184
36643
 
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.
36644
+ 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
36645
 
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" }\`
36646
+ 1. **Append** — pass \`path\` + \`appendContent\`
36647
+ Appends text to the end of a file, creating it if it does not exist.
36648
+ Example: \`{ "path": "notes.txt", "appendContent": "new line\\n" }\`
36190
36649
 
36191
- 2. **Batch edits** — pass \`filePath\` + \`edits\` array
36650
+ 2. **Batch edits** — pass \`path\` + \`edits\` array
36192
36651
  Multiple edits in one file atomically. Each edit is either:
36193
36652
  - \`{ "oldString": "old", "newString": "new" }\` — find/replace
36194
36653
  - \`{ "oldString": "old", "newString": "new", "replaceAll": true }\` — replace every match
36195
36654
  - \`{ "startLine": 5, "endLine": 7, "content": "new lines" }\` — replace line range (1-based, both inclusive)
36196
36655
  Set content to empty string to delete lines.
36197
36656
 
36198
- 3. **Symbol replace** — pass \`filePath\` + \`symbol\` + \`content\`
36199
- Replaces an entire named symbol (function, class, type) with new content.
36657
+ 3. **Symbol replace** — pass \`path\` + \`symbol\` + \`content\`
36658
+ Replaces an entire named symbol (function, class, type).
36200
36659
  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() { ... }" }\`
36660
+ Example: \`{ "path": "src/app.ts", "symbol": "handleRequest", "content": "function handleRequest() { ... }" }\`
36203
36661
 
36204
- 4. **Find and replace** — pass \`filePath\` + \`oldString\` + \`newString\`
36662
+ 4. **Find and replace** — put \`oldString\` and optional \`newString\` in an item of \`edits[]\`
36205
36663
  Finds the exact text in \`oldString\` and replaces it with \`newString\`.
36206
36664
  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" }\`
36665
+ If multiple matches exist, specify \`occurrence\` or set \`replaceAll: true\` in that item.
36209
36666
 
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 }\`
36667
+ 5. **Replace all occurrences** — add \`replaceAll: true\` to a find/replace item.
36213
36668
 
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\`.
36669
+ 6. **Select specific occurrence** — add \`occurrence: N\` to a find/replace item (1-based).
36670
+ When multiple matches exist, select the Nth one (1 = first, 2 = second, etc.).
36219
36671
 
36220
36672
  **Behavior:**
36221
36673
  ${backupBehavior}
@@ -36228,64 +36680,45 @@ function createEditTool(ctx, writeToolName = "write") {
36228
36680
  return {
36229
36681
  description: getEditDescription(ctx, writeToolName),
36230
36682
  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"),
36683
+ filePath: z8.string().describe("Path to the file to edit (absolute or relative to project root)"),
36236
36684
  symbol: z8.string().optional().describe("Named symbol to replace (function, class, type)"),
36237
36685
  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"),
36686
+ appendContent: z8.string().optional().describe("Text to append to the end of path; creates the file if needed"),
36239
36687
  edits: z8.array(z8.object({
36240
36688
  oldString: z8.string().optional().describe("Text to find for a batch find/replace edit"),
36241
36689
  newString: z8.string().optional().describe("Replacement text for a batch find/replace edit"),
36242
36690
  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"),
36691
+ occurrence: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based occurrence for this batch item (1 = first match)"),
36244
36692
  startLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based start line for a batch line-range edit"),
36245
36693
  endLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based end line for a batch line-range edit"),
36246
36694
  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")
36695
+ })).min(1).optional().describe("Batch edits — non-empty array of { oldString, newString }, { oldString, newString, replaceAll: true }, or { startLine, endLine, content } objects")
36248
36696
  },
36249
36697
  execute: async (args, context) => {
36250
36698
  const argsRecord = args;
36251
36699
  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.");
36700
+ 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
36701
  }
36254
- const file2 = args.filePath;
36702
+ const file2 = args.path;
36255
36703
  if (!file2)
36256
- throw new Error("'filePath' parameter is required");
36704
+ throw new Error("'path' parameter is required");
36257
36705
  const projectRoot = await resolveProjectRoot(ctx, context);
36258
36706
  const filePath = resolvePathFromProjectRoot(projectRoot, file2);
36707
+ persistFilePathAlias(argsRecord, context);
36259
36708
  const relPath = path4.relative(projectRoot, filePath);
36260
36709
  {
36261
36710
  const denial2 = await assertExternalDirectoryPermission(ctx, context, filePath);
36262
36711
  if (denial2)
36263
36712
  return permissionDeniedResponse(denial2);
36264
36713
  }
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"]) {
36714
+ const rawArgs = { path: file2 };
36715
+ for (const key of ["appendContent", "symbol", "content", "edits"]) {
36268
36716
  if (argsRecord[key] !== undefined)
36269
36717
  rawArgs[key] = argsRecord[key];
36270
36718
  }
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
36719
  const preview2 = await callToolCall(ctx, context, "edit", rawArgs, { preview: true });
36287
36720
  if (preview2.success === false) {
36288
- throw new Error(preview2.message || "edit preview failed");
36721
+ throw toolErrorFromResponse("edit", preview2);
36289
36722
  }
36290
36723
  const denial = await askEditPermission(context, [relPath], {
36291
36724
  filepath: filePath,
@@ -36295,7 +36728,7 @@ function createEditTool(ctx, writeToolName = "write") {
36295
36728
  return permissionDeniedResponse(denial);
36296
36729
  const data = await callToolCall(ctx, context, "edit", rawArgs);
36297
36730
  if (data.success === false) {
36298
- throw new Error(data.message || "edit failed");
36731
+ throw toolErrorFromResponse("edit", data);
36299
36732
  }
36300
36733
  const output = data.text;
36301
36734
  const diff = data.diff;
@@ -36304,17 +36737,17 @@ function createEditTool(ctx, writeToolName = "write") {
36304
36737
  const truncated = diff.truncated === true;
36305
36738
  const beforeContent = diff.before ?? "";
36306
36739
  const afterContent = diff.after ?? "";
36740
+ const patch = truncated ? typeof preview2.preview_diff === "string" ? preview2.preview_diff : "" : buildUnifiedDiff(filePath, beforeContent, afterContent);
36307
36741
  const uiMeta = {
36308
- diff: truncated ? typeof preview2.preview_diff === "string" ? preview2.preview_diff : "" : buildUnifiedDiff(filePath, beforeContent, afterContent),
36309
- ...truncated ? {} : {
36742
+ diff: patch,
36743
+ ...patch ? {
36310
36744
  filediff: {
36311
36745
  file: filePath,
36312
- before: beforeContent,
36313
- after: afterContent,
36746
+ patch,
36314
36747
  additions: diff.additions ?? 0,
36315
36748
  deletions: diff.deletions ?? 0
36316
36749
  }
36317
- },
36750
+ } : {},
36318
36751
  diagnostics: {}
36319
36752
  };
36320
36753
  return { output, title: relativeToWorktree(filePath, projectRoot), metadata: uiMeta };
@@ -36494,12 +36927,12 @@ function createMoveTool(ctx) {
36494
36927
  return {
36495
36928
  description: moveDescription(ctx),
36496
36929
  args: {
36497
- filePath: z8.string().describe("Source file path to move (absolute or relative to project root)"),
36930
+ path: z8.string().describe("Source file path to move (absolute or relative to project root)"),
36498
36931
  destination: z8.string().describe("Destination file path (absolute or relative to project root)")
36499
36932
  },
36500
36933
  execute: async (args, context) => {
36501
36934
  const projectRoot = await resolveProjectRoot(ctx, context);
36502
- const filePath = resolvePathFromProjectRoot(projectRoot, args.filePath);
36935
+ const filePath = resolvePathFromProjectRoot(projectRoot, args.path);
36503
36936
  const destPath = resolvePathFromProjectRoot(projectRoot, args.destination);
36504
36937
  {
36505
36938
  const sourceDenial = await assertExternalDirectoryPermission(ctx, context, filePath, {
@@ -36520,7 +36953,7 @@ function createMoveTool(ctx) {
36520
36953
  metadata: { action: "move" }
36521
36954
  }));
36522
36955
  const result = await callToolCall(ctx, context, "move", {
36523
- filePath: args.filePath,
36956
+ filePath: args.path,
36524
36957
  destination: args.destination
36525
36958
  });
36526
36959
  if (result.success === false) {
@@ -36549,51 +36982,14 @@ function hoistedTools(ctx) {
36549
36982
  tools.bash_kill = createBashKillTool(ctx);
36550
36983
  }
36551
36984
  }
36552
- return tools;
36985
+ return prepareToolMap(tools);
36553
36986
  }
36554
36987
  function aftPrefixedTools(ctx) {
36555
36988
  const aftEditTool = createEditTool(ctx, "aft_write");
36556
36989
  const tools = {
36557
36990
  aft_read: createReadTool(ctx),
36558
36991
  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
- },
36992
+ aft_edit: aftEditTool,
36597
36993
  aft_apply_patch: createApplyPatchTool(ctx),
36598
36994
  aft_delete: createDeleteTool(ctx),
36599
36995
  aft_move: createMoveTool(ctx)
@@ -36608,7 +37004,7 @@ function aftPrefixedTools(ctx) {
36608
37004
  tools.bash_kill = createBashKillTool(ctx);
36609
37005
  }
36610
37006
  }
36611
- return tools;
37007
+ return prepareToolMap(tools);
36612
37008
  }
36613
37009
 
36614
37010
  // src/tools/imports.ts
@@ -36616,17 +37012,17 @@ import { tool as tool9 } from "@opencode-ai/plugin";
36616
37012
  var z9 = tool9.schema;
36617
37013
  function importTools(ctx) {
36618
37014
  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 {
37015
+ return prepareToolMap({
36620
37016
  aft_import: {
36621
37017
  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
37018
 
36623
37019
  ` + `Ops:
36624
37020
  ` + `- 'add': Add an import. Auto-detects group (stdlib/external/internal), deduplicates. Requires 'module'. Optional 'names', 'defaultImport', 'typeOnly'.
36625
37021
  ` + `- '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}`,
37022
+ ` + `- 'organize': Re-sort and re-group all imports by language convention, deduplicate. Requires only 'path'. ${organizeRecovery}`,
36627
37023
  args: {
36628
37024
  op: z9.enum(["add", "remove", "organize"]).describe("Import operation"),
36629
- filePath: z9.string().describe("Path to the file (absolute or relative to project root)"),
37025
+ path: z9.string().describe("Path to the file (absolute or relative to project root)"),
36630
37026
  module: z9.string().optional().describe("Module path (required for add, remove — e.g. 'react', './utils', 'std::fmt')"),
36631
37027
  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
37028
  defaultImport: z9.string().optional().describe("Default import name, ES only (e.g. 'React')"),
@@ -36643,7 +37039,7 @@ function importTools(ctx) {
36643
37039
  if ((op === "add" || op === "remove") && isEmptyParam(args.module)) {
36644
37040
  throw new Error(`'module' is required for '${op}' op`);
36645
37041
  }
36646
- const filePath = await resolvePathArg(ctx, context, args.filePath);
37042
+ const filePath = await resolvePathArg(ctx, context, args.path);
36647
37043
  {
36648
37044
  const denial = await assertExternalDirectoryPermission(ctx, context, filePath);
36649
37045
  if (denial)
@@ -36680,7 +37076,7 @@ function importTools(ctx) {
36680
37076
  return response.text;
36681
37077
  }
36682
37078
  }
36683
- };
37079
+ });
36684
37080
  }
36685
37081
 
36686
37082
  // src/tools/inspect.ts
@@ -36787,7 +37183,7 @@ import { tool as tool11 } from "@opencode-ai/plugin";
36787
37183
  var z11 = tool11.schema;
36788
37184
  var CALLGRAPH_SOFT_CODES = new Set(["symbol_not_found", "callgraph_building"]);
36789
37185
  function navigationTools(ctx) {
36790
- return {
37186
+ return prepareToolMap({
36791
37187
  aft_callgraph: {
36792
37188
  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
37189
 
@@ -36796,27 +37192,27 @@ function navigationTools(ctx) {
36796
37192
  ` + `- 'impact': What breaks if a symbol changes — affected callers with signatures and entry-point status (blast radius). Use before a risky edit.
36797
37193
  ` + `- 'call_tree': What a function calls (forward traversal). Use to understand a function's dependencies before modifying it.
36798
37194
  ` + `- '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.
37195
+ ` + `- '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
37196
  ` + `- 'trace_data': Follow a value through variable assignments and function parameters across files. Requires 'symbol' (scope to trace from) and 'expression'.
36801
37197
 
36802
- ` + `All ops require both 'filePath' and 'symbol'. 'expression' is additionally required for trace_data; 'toSymbol' for trace_to_symbol.
37198
+ ` + `All ops require both 'path' and 'symbol'. 'expression' is additionally required for trace_data; 'toSymbol' for trace_to_symbol.
36803
37199
 
36804
37200
  ` + `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
37201
  `,
36806
37202
  args: {
36807
37203
  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)"),
37204
+ path: z11.string().describe("Path to the source file containing the symbol (absolute or relative to project root)"),
36809
37205
  symbol: z11.string().describe("Name of the symbol to analyze"),
36810
37206
  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
37207
  expression: z11.string().optional().describe("Expression to track through data flow (required for trace_data op)"),
36812
37208
  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"),
37209
+ toPath: z11.string().optional().describe("Optional target file for trace_to_symbol; required when toSymbol exists in multiple files"),
36814
37210
  includeTests: z11.boolean().optional().describe("Include test files in callers/paths. Defaults to false; tests are hidden."),
36815
37211
  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
37212
  },
36817
37213
  execute: async (args, context) => {
36818
- if (isEmptyParam(args.filePath)) {
36819
- throw new Error("'filePath' is required");
37214
+ if (isEmptyParam(args.path)) {
37215
+ throw new Error("'path' is required");
36820
37216
  }
36821
37217
  if (isEmptyParam(args.symbol)) {
36822
37218
  throw new Error("'symbol' is required");
@@ -36827,8 +37223,8 @@ function navigationTools(ctx) {
36827
37223
  if (args.op === "trace_to_symbol" && isEmptyParam(args.toSymbol)) {
36828
37224
  throw new Error("'toSymbol' is required for 'trace_to_symbol' op");
36829
37225
  }
36830
- const filePath = await resolvePathArg(ctx, context, args.filePath);
36831
- const toFile = !isEmptyParam(args.toFile) ? await resolvePathArg(ctx, context, args.toFile) : undefined;
37226
+ const filePath = await resolvePathArg(ctx, context, args.path);
37227
+ const toFile = !isEmptyParam(args.toPath) ? await resolvePathArg(ctx, context, args.toPath) : undefined;
36832
37228
  const checked = new Set;
36833
37229
  for (const target of [filePath, ...toFile !== undefined ? [toFile] : []]) {
36834
37230
  if (checked.has(target))
@@ -36840,7 +37236,7 @@ function navigationTools(ctx) {
36840
37236
  }
36841
37237
  const rawArgs = {
36842
37238
  op: args.op,
36843
- filePath: args.filePath,
37239
+ filePath: args.path,
36844
37240
  symbol: args.symbol
36845
37241
  };
36846
37242
  const depth = coerceOptionalInt(args.depth, "depth", 1, Number.MAX_SAFE_INTEGER);
@@ -36850,8 +37246,8 @@ function navigationTools(ctx) {
36850
37246
  rawArgs.expression = args.expression;
36851
37247
  if (!isEmptyParam(args.toSymbol))
36852
37248
  rawArgs.toSymbol = args.toSymbol;
36853
- if (!isEmptyParam(args.toFile))
36854
- rawArgs.toFile = args.toFile;
37249
+ if (!isEmptyParam(args.toPath))
37250
+ rawArgs.toFile = args.toPath;
36855
37251
  if (!isEmptyParam(args.includeTests))
36856
37252
  rawArgs.includeTests = coerceBoolean(args.includeTests);
36857
37253
  if (!isEmptyParam(args.includeUnresolved))
@@ -36867,23 +37263,26 @@ function navigationTools(ctx) {
36867
37263
  return response.text;
36868
37264
  }
36869
37265
  }
36870
- };
37266
+ });
36871
37267
  }
36872
37268
 
36873
37269
  // src/tools/reading.ts
36874
37270
  import { tool as tool12 } from "@opencode-ai/plugin";
36875
37271
  var z12 = tool12.schema;
36876
37272
  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}`;
37273
+ const targets = args.targets;
37274
+ if (!isEmptyParam(targets)) {
37275
+ if (Array.isArray(targets)) {
37276
+ if (targets.length === 1) {
37277
+ return `${targets[0].path}#${targets[0].symbol}`;
36881
37278
  }
36882
- return `${args.targets.length} targets across files`;
37279
+ return `${targets.length} targets across files`;
37280
+ }
37281
+ if (targets && typeof targets === "object") {
37282
+ return `${targets.path}#${targets.symbol}`;
36883
37283
  }
36884
- return `${args.targets.filePath}#${args.targets.symbol}`;
36885
37284
  }
36886
- const path5 = args.filePath ?? args.url ?? "";
37285
+ const path5 = args.path ?? args.url ?? "";
36887
37286
  if (typeof args.symbols === "string")
36888
37287
  return path5 ? `${path5}#${args.symbols}` : args.symbols;
36889
37288
  if (Array.isArray(args.symbols) && args.symbols.length > 0) {
@@ -36894,7 +37293,7 @@ function buildZoomTitle(args) {
36894
37293
  return path5 || "(no target)";
36895
37294
  }
36896
37295
  function readingTools(ctx) {
36897
- return {
37296
+ return prepareToolMap({
36898
37297
  aft_outline: {
36899
37298
  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
37299
  ` + ` • directory path → outline all source files under it (recursively, up to 200 files)
@@ -36943,21 +37342,21 @@ function readingTools(ctx) {
36943
37342
  }
36944
37343
  },
36945
37344
  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.",
37345
+ 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
37346
  args: {
36948
- filePath: z12.string().optional().describe("Path to file (absolute or relative to project root)"),
37347
+ path: z12.string().optional().describe("Path to file (absolute or relative to project root)"),
36949
37348
  url: z12.string().optional().describe("HTTP/HTTPS URL of an HTML or Markdown document to fetch and zoom into"),
36950
37349
  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
37350
  targets: z12.union([
36952
37351
  z12.object({
36953
- filePath: z12.string().describe("Path to file (absolute or relative to project root)"),
37352
+ path: z12.string().describe("Path to file (absolute or relative to project root)"),
36954
37353
  symbol: z12.string().describe("Symbol name in that file")
36955
37354
  }),
36956
37355
  z12.array(z12.object({
36957
- filePath: z12.string().describe("Path to file (absolute or relative to project root)"),
37356
+ path: z12.string().describe("Path to file (absolute or relative to project root)"),
36958
37357
  symbol: z12.string().describe("Symbol name in that file")
36959
37358
  }))
36960
- ]).optional().describe("Cross-file batch: `{ filePath, symbol }` or an array of them. Mutually exclusive with filePath/url/symbols."),
37359
+ ]).optional().describe("Cross-file batch: `{ path, symbol }` or an array of them. Mutually exclusive with path/url/symbols."),
36961
37360
  contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Lines of context before/after the symbol (default: 3)"),
36962
37361
  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
37362
  },
@@ -36968,7 +37367,7 @@ function readingTools(ctx) {
36968
37367
  const entryEmpty = (entry) => {
36969
37368
  if (!entry || typeof entry !== "object")
36970
37369
  return true;
36971
- const fp = entry.filePath;
37370
+ const fp = entry.path;
36972
37371
  const sym = entry.symbol;
36973
37372
  const fpEmpty = typeof fp !== "string" || fp.length === 0;
36974
37373
  const symEmpty = typeof sym !== "string" || sym.length === 0;
@@ -36978,7 +37377,7 @@ function readingTools(ctx) {
36978
37377
  return !t.every(entryEmpty);
36979
37378
  return !entryEmpty(t);
36980
37379
  };
36981
- const hasFilePath = !isEmptyParam(args.filePath);
37380
+ const hasFilePath = !isEmptyParam(args.path);
36982
37381
  const hasUrl = !isEmptyParam(args.url);
36983
37382
  const hasTargets = hasTargetsProvided(args.targets);
36984
37383
  const hasSymbols = !isEmptyParam(args.symbols);
@@ -36987,7 +37386,7 @@ function readingTools(ctx) {
36987
37386
  const zoomTitle = buildZoomTitle(args);
36988
37387
  const zoomDisplay = { title: zoomTitle };
36989
37388
  if (hasFilePath)
36990
- zoomDisplay.filePath = args.filePath;
37389
+ zoomDisplay.path = args.path;
36991
37390
  if (hasUrl)
36992
37391
  zoomDisplay.url = args.url;
36993
37392
  if (hasSymbols) {
@@ -37006,25 +37405,31 @@ function readingTools(ctx) {
37006
37405
  });
37007
37406
  if (hasTargets) {
37008
37407
  if (hasFilePath || hasUrl || hasSymbols) {
37009
- throw new Error("'targets' is mutually exclusive with 'filePath', 'url', and 'symbols'");
37408
+ throw new Error("'targets' is mutually exclusive with 'path', 'url', and 'symbols'");
37010
37409
  }
37011
37410
  const targets = Array.isArray(args.targets) ? args.targets : [args.targets];
37012
37411
  if (targets.length === 0) {
37013
37412
  throw new Error("'targets' must be a non-empty object or array");
37014
37413
  }
37015
37414
  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`);
37415
+ const targetPath = entry?.path;
37416
+ if (typeof targetPath !== "string" || targetPath.length === 0) {
37417
+ throw new Error(`targets[${i}].path must be a non-empty string`);
37018
37418
  }
37019
37419
  if (typeof entry.symbol !== "string" || entry.symbol.length === 0) {
37020
37420
  throw new Error(`targets[${i}].symbol must be a non-empty string`);
37021
37421
  }
37022
37422
  }
37023
- const resolvedTargets = await Promise.all(targets.map((t) => resolvePathArg(ctx, context, t.filePath)));
37423
+ const resolvedTargets = await Promise.all(targets.map((t) => resolvePathArg(ctx, context, t.path)));
37024
37424
  const permissionDenied = await assertPathExternalPermissions(ctx, context, resolvedTargets);
37025
37425
  if (permissionDenied)
37026
37426
  return permissionDeniedResponse(permissionDenied);
37027
- const rawArgs2 = { targets };
37427
+ const rawArgs2 = {
37428
+ targets: targets.map((target) => ({
37429
+ filePath: target.path,
37430
+ symbol: target.symbol
37431
+ }))
37432
+ };
37028
37433
  if (contextLines !== undefined)
37029
37434
  rawArgs2.contextLines = contextLines;
37030
37435
  if (wantCallgraph)
@@ -37036,18 +37441,18 @@ function readingTools(ctx) {
37036
37441
  return withMeta(response2.text);
37037
37442
  }
37038
37443
  if (!hasFilePath && !hasUrl) {
37039
- throw new Error("Provide exactly one of 'filePath', 'url', or 'targets'");
37444
+ throw new Error("Provide exactly one of 'path', 'url', or 'targets'");
37040
37445
  }
37041
37446
  if (hasFilePath && hasUrl) {
37042
- throw new Error("Provide exactly ONE of 'filePath' or 'url' — not both");
37447
+ throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
37043
37448
  }
37044
37449
  if (!hasUrl) {
37045
- const file2 = await resolvePathArg(ctx, context, args.filePath);
37450
+ const file2 = await resolvePathArg(ctx, context, args.path);
37046
37451
  const permissionDenied = await assertPathExternalPermissions(ctx, context, file2);
37047
37452
  if (permissionDenied)
37048
37453
  return permissionDeniedResponse(permissionDenied);
37049
37454
  }
37050
- const rawArgs = hasUrl ? { url: args.url } : { filePath: args.filePath };
37455
+ const rawArgs = hasUrl ? { url: args.url } : { filePath: args.path };
37051
37456
  if (hasSymbols)
37052
37457
  rawArgs.symbols = args.symbols;
37053
37458
  if (contextLines !== undefined)
@@ -37061,7 +37466,7 @@ function readingTools(ctx) {
37061
37466
  return withMeta(response.text);
37062
37467
  }
37063
37468
  }
37064
- };
37469
+ });
37065
37470
  }
37066
37471
  async function permissionKindForPath(resolvedPath) {
37067
37472
  try {
@@ -37151,7 +37556,7 @@ async function queryLspHints(client, symbolName, directory, sessionId) {
37151
37556
  // src/tools/refactoring.ts
37152
37557
  var z13 = tool13.schema;
37153
37558
  function refactoringTools(ctx) {
37154
- return {
37559
+ return prepareToolMap({
37155
37560
  aft_refactor: {
37156
37561
  description: `Workspace-wide refactoring that updates imports and references across files.
37157
37562
 
@@ -37161,7 +37566,7 @@ function refactoringTools(ctx) {
37161
37566
  ` + "- 'inline': replace a function call with the function's body.",
37162
37567
  args: {
37163
37568
  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)"),
37569
+ path: z13.string().describe("Path to the source file (absolute or relative to project root)"),
37165
37570
  symbol: z13.string().optional().describe("Symbol name — required for 'move' and 'inline' ops"),
37166
37571
  destination: z13.string().optional().describe("Target file path — required for 'move' op"),
37167
37572
  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 +37597,7 @@ function refactoringTools(ctx) {
37192
37597
  if (op === "inline" && callSiteLine === undefined) {
37193
37598
  throw new Error("'callSiteLine' is required for 'inline' op");
37194
37599
  }
37195
- const filePath = await resolvePathArg(ctx, context, args.filePath);
37600
+ const filePath = await resolvePathArg(ctx, context, args.path);
37196
37601
  const destination = op === "move" ? await resolvePathArg(ctx, context, args.destination) : undefined;
37197
37602
  const patterns = op === "move" ? resolveRelativePatterns(context, [
37198
37603
  workspacePattern(context),
@@ -37246,7 +37651,7 @@ function refactoringTools(ctx) {
37246
37651
  return response.text;
37247
37652
  }
37248
37653
  }
37249
- };
37654
+ });
37250
37655
  }
37251
37656
 
37252
37657
  // src/tools/safety.ts
@@ -37273,15 +37678,15 @@ function relativePatternsFromPaths(context, paths) {
37273
37678
  return patterns;
37274
37679
  }
37275
37680
  function safetyTools(ctx) {
37276
- return {
37681
+ return prepareToolMap({
37277
37682
  aft_safety: {
37278
37683
  description: `File safety and recovery operations.
37279
37684
 
37280
37685
  ` + `Per-file undo stack is capped at 20 entries (oldest evicted).
37281
37686
 
37282
37687
  ` + `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'.
37688
+ ` + `- '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.
37689
+ ` + `- 'history': List all edit snapshots for a file. Requires 'path'.
37285
37690
  ` + `- 'checkpoint': Save a named snapshot of tracked files. Requires 'name'. Optional 'files' to snapshot specific files only.
37286
37691
  ` + `- 'restore': Restore files to a previously saved checkpoint. Requires 'name'.
37287
37692
  ` + `- 'list': List all available named checkpoints. No extra params needed.
@@ -37291,22 +37696,22 @@ function safetyTools(ctx) {
37291
37696
  ` + "Use checkpoint before risky multi-file changes. Use undo for quick single-file rollback.",
37292
37697
  args: {
37293
37698
  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"),
37699
+ path: z14.string().optional().describe("File path (required for history, optional for undo). Absolute or relative to project root"),
37295
37700
  name: z14.string().optional().describe("Checkpoint name (required for checkpoint, restore)"),
37296
37701
  files: z14.array(z14.string()).optional().describe("Specific files to include in checkpoint (optional, defaults to all tracked files)")
37297
37702
  },
37298
37703
  execute: async (args, context) => {
37299
37704
  const op = args.op;
37300
- if (op === "history" && typeof args.filePath !== "string") {
37301
- throw new Error(`'filePath' is required for '${op}' op`);
37705
+ if (op === "history" && typeof args.path !== "string") {
37706
+ throw new Error(`'path' is required for '${op}' op`);
37302
37707
  }
37303
37708
  if ((op === "checkpoint" || op === "restore") && typeof args.name !== "string") {
37304
37709
  throw new Error(`'name' is required for '${op}' op`);
37305
37710
  }
37306
37711
  if (op === "undo") {
37307
37712
  const previewParams = {};
37308
- if (typeof args.filePath === "string")
37309
- previewParams.file = args.filePath;
37713
+ if (typeof args.path === "string")
37714
+ previewParams.file = args.path;
37310
37715
  const preview2 = await callBridge(ctx, context, "undo_preview", previewParams);
37311
37716
  if (preview2.success === false) {
37312
37717
  throw new Error(bridgeErrorMessage(preview2, "undo preview failed"));
@@ -37317,14 +37722,14 @@ function safetyTools(ctx) {
37317
37722
  if (denial)
37318
37723
  return permissionDeniedResponse(denial);
37319
37724
  }
37320
- const filePath = typeof args.filePath === "string" ? resolveAbsolutePath(context, args.filePath) : undefined;
37725
+ const filePath = typeof args.path === "string" ? resolveAbsolutePath(context, args.path) : undefined;
37321
37726
  const permissionError = await askEditPermission(context, relativePatternsFromPaths(context, previewPaths), filePath ? { filepath: filePath } : { operation: "undo", paths: previewPaths });
37322
37727
  if (permissionError)
37323
37728
  return permissionDeniedResponse(permissionError);
37324
37729
  }
37325
37730
  if (op === "checkpoint") {
37326
37731
  const coercedFiles = coerceStringArray(args.files);
37327
- const checkpointFiles = coercedFiles.length > 0 ? coercedFiles : typeof args.filePath === "string" ? [args.filePath] : undefined;
37732
+ const checkpointFiles = coercedFiles.length > 0 ? coercedFiles : typeof args.path === "string" ? [args.path] : undefined;
37328
37733
  if (Array.isArray(checkpointFiles)) {
37329
37734
  const projectRoot = await resolveProjectRoot(ctx, context);
37330
37735
  const uniqueParents = new Set;
@@ -37365,7 +37770,7 @@ function safetyTools(ctx) {
37365
37770
  if (args.name !== undefined)
37366
37771
  rawArgs.name = args.name;
37367
37772
  const payloadFiles = coerceStringArray(args.files).map(expandTilde2);
37368
- const filePathArg = typeof args.filePath === "string" ? expandTilde2(args.filePath) : undefined;
37773
+ const filePathArg = typeof args.path === "string" ? expandTilde2(args.path) : undefined;
37369
37774
  if (filePathArg !== undefined)
37370
37775
  rawArgs.filePath = filePathArg;
37371
37776
  if (payloadFiles.length > 0)
@@ -37377,11 +37782,11 @@ function safetyTools(ctx) {
37377
37782
  return response.text;
37378
37783
  }
37379
37784
  }
37380
- };
37785
+ });
37381
37786
  }
37382
37787
 
37383
37788
  // src/tools/search.ts
37384
- import * as fs5 from "node:fs";
37789
+ import * as fs4 from "node:fs";
37385
37790
  import * as path6 from "node:path";
37386
37791
  function arg2(schema) {
37387
37792
  return schema;
@@ -37390,7 +37795,7 @@ function absoluteSearchPath(projectRoot, target) {
37390
37795
  return resolvePathFromProjectRoot(projectRoot, expandTilde2(target));
37391
37796
  }
37392
37797
  function searchPathExists(projectRoot, target) {
37393
- return fs5.existsSync(absoluteSearchPath(projectRoot, target));
37798
+ return fs4.existsSync(absoluteSearchPath(projectRoot, target));
37394
37799
  }
37395
37800
  function splitSearchPathArg(projectRoot, raw) {
37396
37801
  if (searchPathExists(projectRoot, raw) || !/\s/.test(raw)) {
@@ -37433,7 +37838,7 @@ ${note}` : note;
37433
37838
  }
37434
37839
  function searchPathKind(projectRoot, target, defaultKind) {
37435
37840
  try {
37436
- const stat2 = fs5.lstatSync(absoluteSearchPath(projectRoot, target));
37841
+ const stat2 = fs4.lstatSync(absoluteSearchPath(projectRoot, target));
37437
37842
  if (defaultKind === "file") {
37438
37843
  return stat2.isDirectory() ? "directory" : "file";
37439
37844
  }
@@ -37562,15 +37967,12 @@ function arg3(schema) {
37562
37967
  function semanticTools(ctx) {
37563
37968
  const searchTool = {
37564
37969
  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."
37970
+ "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
37971
  ].join(`
37569
37972
  `),
37570
37973
  args: {
37571
37974
  query: arg3(z15.string().describe("Concept, regex, literal text, filename, or capability to find. Examples: 'fuzzy match with whitespace tolerance', '^export', 'Cargo.lock'.")),
37572
37975
  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
37976
  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
37977
  path: arg3(z15.string().optional().describe("Search a different project root (absolute or ~ path). Requires that project to have been indexed by AFT."))
37576
37978
  },
@@ -37579,24 +37981,19 @@ function semanticTools(ctx) {
37579
37981
  throw new Error("semantic_search: invalid params: `query` must be a non-empty string");
37580
37982
  }
37581
37983
  const query = args.query;
37582
- const hint = typeof args.hint === "string" ? args.hint : undefined;
37583
37984
  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
- }
37985
+ const denied = await askSearchPermission(context, query);
37986
+ if (denied)
37987
+ return permissionDeniedResponse(denied);
37589
37988
  if (pathArg) {
37590
- const denied = await assertAftSearchExternalPermission(ctx, context, pathArg);
37591
- if (denied)
37592
- return permissionDeniedResponse(denied);
37989
+ const denied2 = await assertAftSearchExternalPermission(ctx, context, pathArg);
37990
+ if (denied2)
37991
+ return permissionDeniedResponse(denied2);
37593
37992
  }
37594
37993
  const rawArgs = { query };
37595
37994
  const topK = coerceOptionalInt(args.topK, "topK", 1, 100);
37596
37995
  if (topK !== undefined)
37597
37996
  rawArgs.topK = topK;
37598
- if (hint)
37599
- rawArgs.hint = hint;
37600
37997
  if (typeof args.includeTests === "boolean")
37601
37998
  rawArgs.includeTests = args.includeTests;
37602
37999
  if (pathArg)
@@ -37614,6 +38011,37 @@ function semanticTools(ctx) {
37614
38011
  };
37615
38012
  }
37616
38013
 
38014
+ // src/tool-registration.ts
38015
+ var ALL_ONLY_TOOLS = ["aft_callgraph", "aft_delete", "aft_move", "aft_refactor"];
38016
+ function buildOpenCodeToolMap(ctx, config2, onUnknownDisabled) {
38017
+ const surface = config2.tool_surface ?? "recommended";
38018
+ const allTools = normalizeToolMap({
38019
+ ...surface !== "minimal" && (config2.hoist_builtin_tools !== false ? hoistedTools(ctx) : aftPrefixedTools(ctx)),
38020
+ ...readingTools(ctx),
38021
+ ...config2.backup?.enabled === false ? {} : safetyTools(ctx),
38022
+ ...surface !== "minimal" && importTools(ctx),
38023
+ ...navigationTools(ctx),
38024
+ ...surface !== "minimal" && astTools(ctx),
38025
+ ...surface !== "minimal" && config2.semantic_search === true && semanticTools(ctx),
38026
+ ...inspectToolSurfaceEnabled(config2) && inspectTools(ctx),
38027
+ ...surface !== "minimal" && config2.search_index === true && searchTools(ctx),
38028
+ ...refactoringTools(ctx),
38029
+ ...surface !== "minimal" && conflictTools(ctx)
38030
+ });
38031
+ if (surface !== "all") {
38032
+ for (const name of ALL_ONLY_TOOLS)
38033
+ delete allTools[name];
38034
+ }
38035
+ for (const name of config2.disabled_tools ?? []) {
38036
+ if (name in allTools) {
38037
+ delete allTools[name];
38038
+ } else {
38039
+ onUnknownDisabled?.(name, Object.keys(allTools));
38040
+ }
38041
+ }
38042
+ return allTools;
38043
+ }
38044
+
37617
38045
  // src/workflow-hints.ts
37618
38046
  var HEADING = "## IMPORTANT NOTICE about your tools";
37619
38047
  function buildWorkflowHints(opts) {
@@ -37644,18 +38072,18 @@ function buildWorkflowHints(opts) {
37644
38072
  }
37645
38073
  if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
37646
38074
  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.`;
38075
+ 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
38076
  const readName = opts.hoistBuiltins ? "read" : "aft_read";
37649
38077
  sections.push([
37650
38078
  `**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
38079
  `- \`grep -rn "handleAuth" src/\` in bash → \`${searchName}({ query: "handleAuth" })\``,
37652
38080
  `- \`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 })\``
38081
+ `- \`sed -n '100,160p' app.ts\` / \`cat app.ts\` in bash → \`${readName}({ path: "app.ts", startLine: 100, endLine: 160 })\``
37654
38082
  ].join(`
37655
38083
  `));
37656
38084
  }
37657
38085
  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.");
38086
+ 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
38087
  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
38088
  }
37661
38089
  if (hasNavigate) {
@@ -37728,11 +38156,11 @@ var PLUGIN_VERSION = (() => {
37728
38156
  return "0.0.0";
37729
38157
  }
37730
38158
  })();
37731
- var ANNOUNCEMENT_VERSION = "0.48.0";
38159
+ var ANNOUNCEMENT_VERSION = "0.49.0";
37732
38160
  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."
38161
+ "One unified tool surface: every tool now takes `path` (old spellings keep working), `edit` has one calling shape, and `occurrence` is 1-based. Fewer ways for models to get a call wrong; prompt caches re-warm once after this upgrade.",
38162
+ "aft_search now routes queries itself the hint parameter is gone, and searches that find no exact match are re-ranked by their terms instead of returning nothing.",
38163
+ "Corporate and custom certificate authorities now work: HTTPS requests verify through the OS trust store (Keychain, Windows certificate store, SSL_CERT_FILE on Linux)."
37736
38164
  ];
37737
38165
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
37738
38166
  var plugin = async (input) => initializePluginForDirectory(input);
@@ -38168,38 +38596,12 @@ Install: ${getManualInstallHint()}`).catch(() => {});
38168
38596
  } else {
38169
38597
  cleanupWarnings(notifyOpts).catch(() => {});
38170
38598
  }
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)
38599
+ const allTools = buildOpenCodeToolMap(ctx, aftConfig, (name, available) => {
38600
+ warn2(`disabled_tools: "${name}" not found available: ${available.join(", ")}`);
38185
38601
  });
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(", ")}`);
38602
+ const disabled = aftConfig.disabled_tools ?? [];
38603
+ if (disabled.length > 0) {
38604
+ log2(`Disabled ${disabled.length} tool(s): ${disabled.join(", ")}`);
38203
38605
  }
38204
38606
  instrumentToolMap(allTools);
38205
38607
  const autoUpdateEventHook = createAutoUpdateCheckerHook(input, {
@@ -38314,7 +38716,8 @@ Install: ${getManualInstallHint()}`).catch(() => {});
38314
38716
  const sessionDir = getSessionDirectoryCached(sid) ?? await getSessionDirectory(input.client, sid, input.directory) ?? input.directory;
38315
38717
  signalBashWaitDetachForProject(pool, sessionDir, sid);
38316
38718
  },
38317
- "tool.execute.before": async (toolInput) => {
38719
+ "tool.execute.before": async (toolInput, output) => {
38720
+ output.args = prepareOpenCodeArguments(toolInput.tool, output.args);
38318
38721
  if (toolInput.sessionID)
38319
38722
  inspectTier2Idle.clear(toolInput.sessionID);
38320
38723
  },