@wrongstack/tools 0.66.13 → 0.73.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -144,4 +144,52 @@ interface CodebaseStatsOutput {
144
144
  version: number;
145
145
  }
146
146
 
147
- export { type FileMeta as F, type IndexResult as I, type Ref as R, SCHEMA_VERSION as S, type FileSymbols as a, type IndexStats as b, type SearchResult as c, type Symbol as d, type SymbolKind as e, type SymbolLang as f, codebaseIndexTool as g, codebaseSearchTool as h, codebaseStatsTool as i };
147
+ /**
148
+ * Background indexing coordinator.
149
+ *
150
+ * Wraps {@link runIndexer} with two concerns the agent loop and the CLI wiring
151
+ * both need but neither should own:
152
+ *
153
+ * 1. **Serialization** — every reindex (startup full scan, per-edit incremental,
154
+ * external file-watch) goes through one process-wide promise-chain mutex.
155
+ * `writer.ts` opens a synchronous `node:sqlite` `DatabaseSync` connection per
156
+ * `IndexStore`; two concurrent `runIndexer` runs on the same `index.db` would
157
+ * race the writer and risk `SQLITE_BUSY`. The mutex makes them queue instead.
158
+ *
159
+ * 2. **Debounce** — rapid successive edits to the same file (editor autosave,
160
+ * multi-edit) coalesce into a single reindex, keyed per `(indexDir, file)`.
161
+ *
162
+ * `runIndexer` only reads `opts` (and ignores its `_ctx` parameter), so callers
163
+ * outside the agent loop pass a minimal stub cast to the expected shape — no
164
+ * live agent `Context` is required.
165
+ */
166
+
167
+ /** True when the file's extension maps to a language the indexer can parse. */
168
+ declare function isIndexableFile(filePath: string): boolean;
169
+ /**
170
+ * Run a full-project scan and await it. Used at session start and by the manual
171
+ * `/codebase-reindex` command. Incremental by default (unchanged files skipped
172
+ * via mtime, so repeat runs are cheap); pass `force` to clear and rebuild.
173
+ */
174
+ declare function runStartupIndex(opts: {
175
+ projectRoot: string;
176
+ indexDir?: string;
177
+ force?: boolean;
178
+ }): Promise<IndexResult>;
179
+ /**
180
+ * Debounced, fire-and-forget incremental reindex of specific files. Used by the
181
+ * per-edit toolCall middleware and the external file watcher. Non-indexable
182
+ * paths are dropped. Errors are reported via the optional `onError` callback and
183
+ * never thrown to the caller (background work must not crash a turn).
184
+ */
185
+ declare function enqueueReindex(opts: {
186
+ projectRoot: string;
187
+ files: string[];
188
+ indexDir?: string;
189
+ debounceMs?: number;
190
+ onError?: (err: unknown) => void;
191
+ }): void;
192
+ /** Cancel all pending debounced reindexes. For teardown / tests. */
193
+ declare function cancelPendingReindexes(): void;
194
+
195
+ export { type FileMeta as F, type IndexResult as I, type Ref as R, SCHEMA_VERSION as S, type FileSymbols as a, type IndexStats as b, type SearchResult as c, type Symbol as d, type SymbolKind as e, type SymbolLang as f, cancelPendingReindexes as g, codebaseIndexTool as h, codebaseSearchTool as i, codebaseStatsTool as j, enqueueReindex as k, isIndexableFile as l, runStartupIndex as r };
package/dist/builtin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
2
2
  import * as Core from '@wrongstack/core';
3
3
  import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan, resolveWstackPaths } from '@wrongstack/core';
4
- import * as fs11 from 'node:fs/promises';
4
+ import * as fs12 from 'node:fs/promises';
5
5
  import * as path from 'node:path';
6
6
  import { resolve, sep, dirname } from 'node:path';
7
7
  import * as os from 'node:os';
@@ -127,12 +127,12 @@ function safeResolve(input, ctx) {
127
127
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
128
128
  }
129
129
  async function assertRealInsideRoot(absPath, ctx) {
130
- const realRoot = await fs11.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));
130
+ const realRoot = await fs12.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));
131
131
  let probe = absPath;
132
132
  for (; ; ) {
133
133
  let real;
134
134
  try {
135
- real = await fs11.realpath(probe);
135
+ real = await fs12.realpath(probe);
136
136
  } catch (err) {
137
137
  if (err.code === "ENOENT") {
138
138
  const parent = path.dirname(probe);
@@ -1296,6 +1296,17 @@ var IndexStore = class {
1296
1296
  ({ id, text }) => ({ id, text })
1297
1297
  );
1298
1298
  }
1299
+ /**
1300
+ * Largest symbol id currently in the table (0 when empty). New ids must be
1301
+ * allocated from this, NOT from `COUNT(*)`: incremental reindexes delete a
1302
+ * changed file's rows, so the row count drops below the max id and a
1303
+ * count-based id would collide with a surviving row (UNIQUE constraint on
1304
+ * `symbols.id`). Ids may have gaps — that is fine.
1305
+ */
1306
+ getMaxSymbolId() {
1307
+ const rows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
1308
+ return rows[0]?.m ?? 0;
1309
+ }
1299
1310
  // ─── Stats ───────────────────────────────────────────────────────────────────
1300
1311
  getStats() {
1301
1312
  const sizeBytes = this.sizeBytes();
@@ -2614,6 +2625,56 @@ function makeSymbol2(opts) {
2614
2625
  text: `${opts.name} ${opts.signature}`.trim()
2615
2626
  };
2616
2627
  }
2628
+ function globBody(glob) {
2629
+ return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
2630
+ }
2631
+ function compileGitignore(lines) {
2632
+ const rules = [];
2633
+ for (const raw of lines) {
2634
+ let line = raw.replace(/\r$/, "");
2635
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
2636
+ line = line.trim();
2637
+ let negated = false;
2638
+ if (line.startsWith("!")) {
2639
+ negated = true;
2640
+ line = line.slice(1);
2641
+ }
2642
+ let dirOnly = false;
2643
+ if (line.endsWith("/")) {
2644
+ dirOnly = true;
2645
+ line = line.slice(0, -1);
2646
+ }
2647
+ if (!line) continue;
2648
+ const anchored = line.startsWith("/") || line.includes("/");
2649
+ if (line.startsWith("/")) line = line.slice(1);
2650
+ const body = globBody(line);
2651
+ const prefix = anchored ? "^" : "(?:^|.*/)";
2652
+ rules.push({
2653
+ eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
2654
+ under: new RegExp(`${prefix}${body}/.*$`),
2655
+ negated,
2656
+ dirOnly
2657
+ });
2658
+ }
2659
+ return (relPath, isDir) => {
2660
+ const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
2661
+ let ignored = false;
2662
+ for (const r of rules) {
2663
+ const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
2664
+ if (re.test(p)) ignored = !r.negated;
2665
+ }
2666
+ return ignored;
2667
+ };
2668
+ }
2669
+ async function loadGitignoreMatcher(projectRoot) {
2670
+ let lines = [];
2671
+ try {
2672
+ const raw = await fs12.readFile(path.join(projectRoot, ".gitignore"), "utf8");
2673
+ lines = raw.split("\n");
2674
+ } catch {
2675
+ }
2676
+ return compileGitignore(lines);
2677
+ }
2617
2678
 
2618
2679
  // src/codebase-index/indexer.ts
2619
2680
  var DEFAULT_IGNORE = [
@@ -2627,7 +2688,7 @@ var DEFAULT_IGNORE = [
2627
2688
  "__snapshots__",
2628
2689
  ".nyc_output"
2629
2690
  ];
2630
- async function findSourceFiles(projectRoot, ignore) {
2691
+ async function findSourceFiles(projectRoot, ignore, isGitIgnored) {
2631
2692
  const results = [];
2632
2693
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
2633
2694
  const globs = [
@@ -2645,17 +2706,19 @@ async function findSourceFiles(projectRoot, ignore) {
2645
2706
  const walk = async (dir) => {
2646
2707
  let entries;
2647
2708
  try {
2648
- entries = await fs11.readdir(dir, { withFileTypes: true });
2709
+ entries = await fs12.readdir(dir, { withFileTypes: true });
2649
2710
  } catch {
2650
2711
  return;
2651
2712
  }
2652
2713
  for (const e of entries) {
2653
2714
  if (ignoreSet.has(e.name)) continue;
2654
2715
  const full = path.join(dir, e.name);
2716
+ const rel = path.relative(projectRoot, full).replace(/\\/g, "/");
2655
2717
  if (e.isDirectory()) {
2718
+ if (isGitIgnored(rel, true)) continue;
2656
2719
  await walk(full);
2657
2720
  } else if (e.isFile()) {
2658
- const rel = path.relative(projectRoot, full).replace(/\\/g, "/");
2721
+ if (isGitIgnored(rel, false)) continue;
2659
2722
  const ext = path.extname(e.name);
2660
2723
  for (const { ext: extName, pat } of globs) {
2661
2724
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
@@ -2698,11 +2761,12 @@ async function runIndexer(_ctx, opts) {
2698
2761
  const langStats = {};
2699
2762
  let filesIndexed = 0;
2700
2763
  let symbolsIndexed = 0;
2764
+ const isGitIgnored = await loadGitignoreMatcher(projectRoot);
2701
2765
  let files;
2702
2766
  if (opts.files && opts.files.length > 0) {
2703
- files = opts.files.map((f) => path.resolve(projectRoot, f));
2767
+ files = opts.files.map((f) => path.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path.relative(projectRoot, f).replace(/\\/g, "/"), false));
2704
2768
  } else {
2705
- files = await findSourceFiles(projectRoot, ignore);
2769
+ files = await findSourceFiles(projectRoot, ignore, isGitIgnored);
2706
2770
  }
2707
2771
  if (langs && langs.length > 0) {
2708
2772
  const langSet = new Set(langs);
@@ -2719,7 +2783,7 @@ async function runIndexer(_ctx, opts) {
2719
2783
  for (const file of files) {
2720
2784
  let stat10;
2721
2785
  try {
2722
- stat10 = await fs11.stat(file);
2786
+ stat10 = await fs12.stat(file);
2723
2787
  } catch {
2724
2788
  store.deleteFile(file);
2725
2789
  continue;
@@ -2734,11 +2798,11 @@ async function runIndexer(_ctx, opts) {
2734
2798
  filesIndexed++;
2735
2799
  continue;
2736
2800
  }
2737
- store.deleteSymbolsForFile(file);
2738
2801
  store.deleteRefsForFile(file);
2802
+ store.deleteSymbolsForFile(file);
2739
2803
  let content;
2740
2804
  try {
2741
- content = await fs11.readFile(file, "utf8");
2805
+ content = await fs12.readFile(file, "utf8");
2742
2806
  } catch (e) {
2743
2807
  errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
2744
2808
  continue;
@@ -2761,7 +2825,7 @@ async function runIndexer(_ctx, opts) {
2761
2825
  filesIndexed++;
2762
2826
  continue;
2763
2827
  }
2764
- const nextId = store.getStats().totalSymbols + 1;
2828
+ const nextId = store.getMaxSymbolId() + 1;
2765
2829
  const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
2766
2830
  store.insertSymbols(symbolsWithIds, nextId);
2767
2831
  const count = symbolsWithIds.length;
@@ -2788,7 +2852,7 @@ async function runIndexer(_ctx, opts) {
2788
2852
  }
2789
2853
  for (const [file_] of existingMeta) {
2790
2854
  try {
2791
- await fs11.stat(file_);
2855
+ await fs12.stat(file_);
2792
2856
  } catch {
2793
2857
  store.deleteFile(file_);
2794
2858
  }
@@ -3171,9 +3235,9 @@ async function fileDiff(input, ctx, _signal) {
3171
3235
  const results = [];
3172
3236
  for (const file of files) {
3173
3237
  const absPath = safeResolve(file, ctx);
3174
- const stat10 = await fs11.stat(absPath).catch(() => null);
3238
+ const stat10 = await fs12.stat(absPath).catch(() => null);
3175
3239
  if (!stat10?.isFile()) continue;
3176
- const content = await fs11.readFile(absPath, "utf8");
3240
+ const content = await fs12.readFile(absPath, "utf8");
3177
3241
  const lines = content.split(/\r?\n/);
3178
3242
  results.push(formatWithLineNumbers(file, lines));
3179
3243
  }
@@ -3235,7 +3299,7 @@ var documentTool = {
3235
3299
  const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
3236
3300
  for (const absPath of fileList) {
3237
3301
  try {
3238
- const content = await fs11.readFile(absPath, "utf8");
3302
+ const content = await fs12.readFile(absPath, "utf8");
3239
3303
  filesProcessed++;
3240
3304
  const processed = processFile(
3241
3305
  content,
@@ -3271,7 +3335,7 @@ async function resolveFiles(filesInput, cwd) {
3271
3335
  for (const f of files) {
3272
3336
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
3273
3337
  try {
3274
- const stat10 = await fs11.stat(absPath);
3338
+ const stat10 = await fs12.stat(absPath);
3275
3339
  if (stat10.isFile()) resolved.push(absPath);
3276
3340
  } catch {
3277
3341
  }
@@ -3363,7 +3427,7 @@ var editTool = {
3363
3427
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
3364
3428
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
3365
3429
  const absPath = await safeResolveReal(input.path, ctx);
3366
- const stat10 = await fs11.stat(absPath).catch((err) => {
3430
+ const stat10 = await fs12.stat(absPath).catch((err) => {
3367
3431
  if (err.code === "ENOENT") {
3368
3432
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
3369
3433
  }
@@ -3373,8 +3437,8 @@ var editTool = {
3373
3437
  if (!ctx.hasRead(absPath)) {
3374
3438
  throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
3375
3439
  }
3376
- const original = await fs11.readFile(absPath, "utf8");
3377
- const updated = await fs11.stat(absPath);
3440
+ const original = await fs12.readFile(absPath, "utf8");
3441
+ const updated = await fs12.stat(absPath);
3378
3442
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
3379
3443
  const lastReadMtime = ctx.lastReadMtime(absPath);
3380
3444
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
@@ -3414,7 +3478,7 @@ var editTool = {
3414
3478
  const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
3415
3479
  const newFile = toStyle(newFileLf, style);
3416
3480
  await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
3417
- const written = await fs11.stat(absPath);
3481
+ const written = await fs12.stat(absPath);
3418
3482
  ctx.recordRead(absPath, written.mtimeMs);
3419
3483
  ctx.session.recordFileChange({
3420
3484
  path: absPath,
@@ -4446,7 +4510,7 @@ var globTool = {
4446
4510
  }
4447
4511
  let entries;
4448
4512
  try {
4449
- entries = await fs11.readdir(dir, { withFileTypes: true });
4513
+ entries = await fs12.readdir(dir, { withFileTypes: true });
4450
4514
  } catch {
4451
4515
  return;
4452
4516
  }
@@ -4462,7 +4526,7 @@ var globTool = {
4462
4526
  } else if (e.isFile()) {
4463
4527
  if (re.test(rel) || re.test(name)) {
4464
4528
  try {
4465
- const st = await fs11.stat(full);
4529
+ const st = await fs12.stat(full);
4466
4530
  results.push({ rel: full, mtime: st.mtimeMs });
4467
4531
  if (results.length >= limit) {
4468
4532
  truncated = true;
@@ -4481,7 +4545,7 @@ var globTool = {
4481
4545
  };
4482
4546
  async function readGitignore(dir) {
4483
4547
  try {
4484
- const raw = await fs11.readFile(path.join(dir, ".gitignore"), "utf8");
4548
+ const raw = await fs12.readFile(path.join(dir, ".gitignore"), "utf8");
4485
4549
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
4486
4550
  } catch {
4487
4551
  return [];
@@ -4763,7 +4827,7 @@ async function runNative(input, base, mode, limit, signal) {
4763
4827
  if (stopped || signal.aborted) return;
4764
4828
  let entries;
4765
4829
  try {
4766
- entries = await fs11.readdir(dir, { withFileTypes: true });
4830
+ entries = await fs12.readdir(dir, { withFileTypes: true });
4767
4831
  } catch {
4768
4832
  return;
4769
4833
  }
@@ -4778,9 +4842,9 @@ async function runNative(input, base, mode, limit, signal) {
4778
4842
  if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
4779
4843
  if (globRe) globRe.lastIndex = 0;
4780
4844
  try {
4781
- const stat10 = await fs11.stat(full);
4845
+ const stat10 = await fs12.stat(full);
4782
4846
  if (stat10.size > 1e6) continue;
4783
- const head = await fs11.readFile(full);
4847
+ const head = await fs12.readFile(full);
4784
4848
  if (isBinaryBuffer(head)) continue;
4785
4849
  const text = head.toString("utf8");
4786
4850
  const lines = text.split(/\r?\n/);
@@ -4959,7 +5023,7 @@ var jsonTool = {
4959
5023
  let raw;
4960
5024
  if (input.file) {
4961
5025
  try {
4962
- raw = await fs11.readFile(input.file, "utf8");
5026
+ raw = await fs12.readFile(input.file, "utf8");
4963
5027
  } catch {
4964
5028
  return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
4965
5029
  }
@@ -4997,8 +5061,8 @@ var jsonTool = {
4997
5061
  };
4998
5062
  }
4999
5063
  };
5000
- function query(data, path18) {
5001
- const parts = path18.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
5064
+ function query(data, path19) {
5065
+ const parts = path19.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
5002
5066
  let current = data;
5003
5067
  for (const part of parts) {
5004
5068
  if (current === null || current === void 0) return void 0;
@@ -5267,7 +5331,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
5267
5331
  }
5268
5332
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
5269
5333
  var MAX_TAIL_LINES = 1e5;
5270
- async function fileLogs(path18, lines, filterRe, stream) {
5334
+ async function fileLogs(path19, lines, filterRe, stream) {
5271
5335
  const { createInterface } = await import('node:readline');
5272
5336
  const { createReadStream } = await import('node:fs');
5273
5337
  const entries = [];
@@ -5276,7 +5340,7 @@ async function fileLogs(path18, lines, filterRe, stream) {
5276
5340
  let writeIdx = 0;
5277
5341
  let totalLines = 0;
5278
5342
  const rl = createInterface({
5279
- input: createReadStream(path18),
5343
+ input: createReadStream(path19),
5280
5344
  crlfDelay: Number.POSITIVE_INFINITY
5281
5345
  });
5282
5346
  for await (const line of rl) {
@@ -5297,7 +5361,7 @@ async function fileLogs(path18, lines, filterRe, stream) {
5297
5361
  if (parsed) entries.push(parsed);
5298
5362
  }
5299
5363
  return {
5300
- source: path18,
5364
+ source: path19,
5301
5365
  entries,
5302
5366
  total: entries.length,
5303
5367
  truncated: totalLines > effLines,
@@ -5476,12 +5540,12 @@ var patchTool = {
5476
5540
  };
5477
5541
  }
5478
5542
  }
5479
- const tmpDir = await fs11.mkdtemp(path.join(os.tmpdir(), ".wstack_patch_"));
5543
+ const tmpDir = await fs12.mkdtemp(path.join(os.tmpdir(), ".wstack_patch_"));
5480
5544
  try {
5481
- await fs11.chmod(tmpDir, 448).catch(() => {
5545
+ await fs12.chmod(tmpDir, 448).catch(() => {
5482
5546
  });
5483
5547
  const patchFile = path.join(tmpDir, "in.diff");
5484
- await fs11.writeFile(patchFile, input.patch, { mode: 384 });
5548
+ await fs12.writeFile(patchFile, input.patch, { mode: 384 });
5485
5549
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
5486
5550
  const result = await runPatch(args, dir, opts.signal);
5487
5551
  if (result.exitCode !== 0 && !dryRun) {
@@ -5502,7 +5566,7 @@ var patchTool = {
5502
5566
  message: result.stdout || "patch applied"
5503
5567
  };
5504
5568
  } finally {
5505
- await fs11.rm(tmpDir, { recursive: true, force: true }).catch(() => {
5569
+ await fs12.rm(tmpDir, { recursive: true, force: true }).catch(() => {
5506
5570
  });
5507
5571
  }
5508
5572
  }
@@ -5744,7 +5808,7 @@ var readTool = {
5744
5808
  const absPath = await safeResolveReal(input.path, ctx);
5745
5809
  let stat10;
5746
5810
  try {
5747
- stat10 = await fs11.stat(absPath);
5811
+ stat10 = await fs12.stat(absPath);
5748
5812
  } catch (err) {
5749
5813
  const code = err.code;
5750
5814
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
@@ -5756,7 +5820,7 @@ var readTool = {
5756
5820
  if (stat10.size > MAX_BYTES2) {
5757
5821
  throw new Error(`read: file too large (${stat10.size} bytes, limit ${MAX_BYTES2})`);
5758
5822
  }
5759
- const buf = await fs11.readFile(absPath);
5823
+ const buf = await fs12.readFile(absPath);
5760
5824
  if (isBinaryBuffer(buf)) {
5761
5825
  throw new Error(`read: "${input.path}" appears to be binary`);
5762
5826
  }
@@ -5824,11 +5888,11 @@ var replaceTool = {
5824
5888
  const dryRun = input.dry_run ?? false;
5825
5889
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
5826
5890
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
5827
- const realRoot = await fs11.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
5891
+ const realRoot = await fs12.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
5828
5892
  const results = [];
5829
5893
  let totalReplacements = 0;
5830
5894
  for (const absPath of fileList) {
5831
- const lstat2 = await fs11.lstat(absPath).catch((err) => {
5895
+ const lstat2 = await fs12.lstat(absPath).catch((err) => {
5832
5896
  if (err.code === "ENOENT") return null;
5833
5897
  throw err;
5834
5898
  });
@@ -5836,17 +5900,17 @@ var replaceTool = {
5836
5900
  if (lstat2.isSymbolicLink()) continue;
5837
5901
  let realPath;
5838
5902
  try {
5839
- realPath = await fs11.realpath(absPath);
5903
+ realPath = await fs12.realpath(absPath);
5840
5904
  } catch {
5841
5905
  continue;
5842
5906
  }
5843
5907
  const rel = path.relative(realRoot, realPath);
5844
5908
  if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
5845
- const stat10 = await fs11.stat(realPath).catch(() => null);
5909
+ const stat10 = await fs12.stat(realPath).catch(() => null);
5846
5910
  if (!stat10 || !stat10.isFile()) continue;
5847
5911
  let content;
5848
5912
  try {
5849
- const buf = await fs11.readFile(realPath);
5913
+ const buf = await fs12.readFile(realPath);
5850
5914
  if (isBinaryBuffer(buf)) continue;
5851
5915
  content = buf.toString("utf8");
5852
5916
  } catch {
@@ -5898,7 +5962,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
5898
5962
  const resolved = [];
5899
5963
  for (const p of parts) {
5900
5964
  const absPath = safeResolve(p, ctx);
5901
- const stat10 = await fs11.stat(absPath).catch(() => null);
5965
+ const stat10 = await fs12.stat(absPath).catch(() => null);
5902
5966
  if (stat10?.isFile()) {
5903
5967
  resolved.push(absPath);
5904
5968
  }
@@ -5949,7 +6013,7 @@ async function globNative(pattern, base, extraGlob) {
5949
6013
  const walk = async (dir) => {
5950
6014
  let entries;
5951
6015
  try {
5952
- entries = await fs11.readdir(dir, { withFileTypes: true });
6016
+ entries = await fs12.readdir(dir, { withFileTypes: true });
5953
6017
  } catch {
5954
6018
  return;
5955
6019
  }
@@ -5957,7 +6021,7 @@ async function globNative(pattern, base, extraGlob) {
5957
6021
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
5958
6022
  const full = path.join(dir, e.name);
5959
6023
  try {
5960
- const stat10 = await fs11.lstat(full);
6024
+ const stat10 = await fs12.lstat(full);
5961
6025
  if (stat10.isSymbolicLink()) continue;
5962
6026
  } catch {
5963
6027
  continue;
@@ -6134,7 +6198,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
6134
6198
  }
6135
6199
  const fullPath = target;
6136
6200
  if (!dryRun) {
6137
- await fs11.mkdir(path.dirname(fullPath), { recursive: true });
6201
+ await fs12.mkdir(path.dirname(fullPath), { recursive: true });
6138
6202
  await atomicWrite(fullPath, substituteVars(content, name, vars));
6139
6203
  }
6140
6204
  files.push(resolvedPath);
@@ -6773,10 +6837,14 @@ var toolSearchTool = {
6773
6837
  permission: t.permission,
6774
6838
  mutating: t.mutating
6775
6839
  }));
6840
+ const totalAvailable = tools.length;
6841
+ const hint = results.length === 0 && query2 ? `No tools matched "${input.query}". Use tool-help (without arguments) to see all ${totalAvailable} available tools.` : void 0;
6776
6842
  return {
6777
6843
  tools: results,
6778
6844
  total: filtered.length,
6779
- truncated: filtered.length > limit
6845
+ truncated: filtered.length > limit,
6846
+ ...hint ? { hint } : {},
6847
+ _available: totalAvailable
6780
6848
  };
6781
6849
  }
6782
6850
  };
@@ -6986,7 +7054,7 @@ var treeTool = {
6986
7054
  }
6987
7055
  };
6988
7056
  async function walkDir(dir, depth, opts) {
6989
- const entries = await fs11.readdir(dir, { withFileTypes: true }).catch(() => []);
7057
+ const entries = await fs12.readdir(dir, { withFileTypes: true }).catch(() => []);
6990
7058
  const filtered = entries.filter((e) => {
6991
7059
  if (!opts.showHidden && e.name.startsWith(".")) return false;
6992
7060
  if (opts.exclude.has(e.name)) return false;
@@ -7134,14 +7202,14 @@ var writeTool = {
7134
7202
  let existed = false;
7135
7203
  let prev = "";
7136
7204
  try {
7137
- const stat11 = await fs11.stat(absPath);
7205
+ const stat11 = await fs12.stat(absPath);
7138
7206
  existed = stat11.isFile();
7139
7207
  if (existed) {
7140
7208
  if (!ctx.hasRead(absPath)) {
7141
- prev = await fs11.readFile(absPath, "utf8");
7209
+ prev = await fs12.readFile(absPath, "utf8");
7142
7210
  ctx.recordRead(absPath, stat11.mtimeMs);
7143
7211
  } else {
7144
- prev = await fs11.readFile(absPath, "utf8");
7212
+ prev = await fs12.readFile(absPath, "utf8");
7145
7213
  }
7146
7214
  }
7147
7215
  } catch (err) {
@@ -7152,7 +7220,7 @@ var writeTool = {
7152
7220
  await atomicWrite(absPath, input.content);
7153
7221
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
7154
7222
  + (new file, ${input.content.split("\n").length} lines)`;
7155
- const stat10 = await fs11.stat(absPath);
7223
+ const stat10 = await fs12.stat(absPath);
7156
7224
  ctx.recordRead(absPath, stat10.mtimeMs);
7157
7225
  ctx.session.recordFileChange({
7158
7226
  path: absPath,