@wrongstack/tools 0.68.0 → 0.77.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/pack.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,8 +2625,92 @@ 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
+ }
2678
+
2679
+ // src/codebase-index/background-indexer.ts
2680
+ var _ready = false;
2681
+ var _indexing = false;
2682
+ var _currentFile = 0;
2683
+ var _totalFiles = 0;
2684
+ var _lastError = null;
2685
+ function setIndexReady() {
2686
+ _ready = true;
2687
+ }
2688
+ function getIndexState() {
2689
+ return {
2690
+ ready: _ready,
2691
+ indexing: _indexing,
2692
+ currentFile: _currentFile,
2693
+ totalFiles: _totalFiles,
2694
+ lastError: _lastError
2695
+ };
2696
+ }
2697
+ var _listeners = [];
2698
+ function emitState() {
2699
+ const state = getIndexState();
2700
+ for (const l of _listeners) l(state);
2701
+ }
2702
+ function _setIndexProgress(current, total) {
2703
+ _currentFile = current;
2704
+ _totalFiles = total;
2705
+ emitState();
2706
+ }
2707
+ Promise.resolve();
2617
2708
 
2618
2709
  // src/codebase-index/indexer.ts
2710
+ var YIELD_EVERY_N = 50;
2711
+ function yieldEventLoop() {
2712
+ return new Promise((resolve7) => setImmediate(resolve7));
2713
+ }
2619
2714
  var DEFAULT_IGNORE = [
2620
2715
  "node_modules",
2621
2716
  ".git",
@@ -2627,7 +2722,7 @@ var DEFAULT_IGNORE = [
2627
2722
  "__snapshots__",
2628
2723
  ".nyc_output"
2629
2724
  ];
2630
- async function findSourceFiles(projectRoot, ignore) {
2725
+ async function findSourceFiles(projectRoot, ignore, isGitIgnored) {
2631
2726
  const results = [];
2632
2727
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
2633
2728
  const globs = [
@@ -2645,17 +2740,19 @@ async function findSourceFiles(projectRoot, ignore) {
2645
2740
  const walk = async (dir) => {
2646
2741
  let entries;
2647
2742
  try {
2648
- entries = await fs11.readdir(dir, { withFileTypes: true });
2743
+ entries = await fs12.readdir(dir, { withFileTypes: true });
2649
2744
  } catch {
2650
2745
  return;
2651
2746
  }
2652
2747
  for (const e of entries) {
2653
2748
  if (ignoreSet.has(e.name)) continue;
2654
2749
  const full = path.join(dir, e.name);
2750
+ const rel = path.relative(projectRoot, full).replace(/\\/g, "/");
2655
2751
  if (e.isDirectory()) {
2752
+ if (isGitIgnored(rel, true)) continue;
2656
2753
  await walk(full);
2657
2754
  } else if (e.isFile()) {
2658
- const rel = path.relative(projectRoot, full).replace(/\\/g, "/");
2755
+ if (isGitIgnored(rel, false)) continue;
2659
2756
  const ext = path.extname(e.name);
2660
2757
  for (const { ext: extName, pat } of globs) {
2661
2758
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
@@ -2698,11 +2795,12 @@ async function runIndexer(_ctx, opts) {
2698
2795
  const langStats = {};
2699
2796
  let filesIndexed = 0;
2700
2797
  let symbolsIndexed = 0;
2798
+ const isGitIgnored = await loadGitignoreMatcher(projectRoot);
2701
2799
  let files;
2702
2800
  if (opts.files && opts.files.length > 0) {
2703
- files = opts.files.map((f) => path.resolve(projectRoot, f));
2801
+ files = opts.files.map((f) => path.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path.relative(projectRoot, f).replace(/\\/g, "/"), false));
2704
2802
  } else {
2705
- files = await findSourceFiles(projectRoot, ignore);
2803
+ files = await findSourceFiles(projectRoot, ignore, isGitIgnored);
2706
2804
  }
2707
2805
  if (langs && langs.length > 0) {
2708
2806
  const langSet = new Set(langs);
@@ -2716,10 +2814,15 @@ async function runIndexer(_ctx, opts) {
2716
2814
  if (!force) {
2717
2815
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
2718
2816
  }
2719
- for (const file of files) {
2817
+ for (let fi = 0; fi < files.length; fi++) {
2818
+ const file = files[fi];
2819
+ _setIndexProgress(fi + 1, files.length);
2820
+ if (fi > 0 && fi % YIELD_EVERY_N === 0) {
2821
+ await yieldEventLoop();
2822
+ }
2720
2823
  let stat10;
2721
2824
  try {
2722
- stat10 = await fs11.stat(file);
2825
+ stat10 = await fs12.stat(file);
2723
2826
  } catch {
2724
2827
  store.deleteFile(file);
2725
2828
  continue;
@@ -2734,11 +2837,11 @@ async function runIndexer(_ctx, opts) {
2734
2837
  filesIndexed++;
2735
2838
  continue;
2736
2839
  }
2737
- store.deleteSymbolsForFile(file);
2738
2840
  store.deleteRefsForFile(file);
2841
+ store.deleteSymbolsForFile(file);
2739
2842
  let content;
2740
2843
  try {
2741
- content = await fs11.readFile(file, "utf8");
2844
+ content = await fs12.readFile(file, "utf8");
2742
2845
  } catch (e) {
2743
2846
  errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
2744
2847
  continue;
@@ -2761,7 +2864,7 @@ async function runIndexer(_ctx, opts) {
2761
2864
  filesIndexed++;
2762
2865
  continue;
2763
2866
  }
2764
- const nextId = store.getStats().totalSymbols + 1;
2867
+ const nextId = store.getMaxSymbolId() + 1;
2765
2868
  const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
2766
2869
  store.insertSymbols(symbolsWithIds, nextId);
2767
2870
  const count = symbolsWithIds.length;
@@ -2788,7 +2891,7 @@ async function runIndexer(_ctx, opts) {
2788
2891
  }
2789
2892
  for (const [file_] of existingMeta) {
2790
2893
  try {
2791
- await fs11.stat(file_);
2894
+ await fs12.stat(file_);
2792
2895
  } catch {
2793
2896
  store.deleteFile(file_);
2794
2897
  }
@@ -2836,6 +2939,7 @@ var codebaseIndexTool = {
2836
2939
  langs: input.langs,
2837
2940
  indexDir: codebaseIndexDirOverride(ctx)
2838
2941
  });
2942
+ setIndexReady();
2839
2943
  return result;
2840
2944
  }
2841
2945
  };
@@ -2971,6 +3075,31 @@ var codebaseSearchTool = {
2971
3075
  required: ["query"]
2972
3076
  },
2973
3077
  async execute(input, ctx) {
3078
+ const state = getIndexState();
3079
+ if (!state.ready) {
3080
+ return {
3081
+ results: [],
3082
+ total: 0,
3083
+ query: input.query,
3084
+ indexStatus: state.indexing ? `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.` : "Index not yet built. The codebase is being indexed at startup \u2014 search will be available shortly."
3085
+ };
3086
+ }
3087
+ if (state.indexing) {
3088
+ return {
3089
+ results: [],
3090
+ total: 0,
3091
+ query: input.query,
3092
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
3093
+ };
3094
+ }
3095
+ if (state.lastError) {
3096
+ return {
3097
+ results: [],
3098
+ total: 0,
3099
+ query: input.query,
3100
+ indexStatus: `Index build failed: ${state.lastError}. Try /codebase-reindex.`
3101
+ };
3102
+ }
2974
3103
  const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
2975
3104
  try {
2976
3105
  const limit = Math.min(input.limit ?? 20, 100);
@@ -3028,6 +3157,32 @@ var codebaseStatsTool = {
3028
3157
  additionalProperties: false
3029
3158
  },
3030
3159
  async execute(_input, ctx) {
3160
+ const idxState = getIndexState();
3161
+ if (!idxState.ready) {
3162
+ return {
3163
+ totalSymbols: 0,
3164
+ totalFiles: 0,
3165
+ byLang: {},
3166
+ byKind: {},
3167
+ lastIndexed: null,
3168
+ sizeBytes: 0,
3169
+ indexPath: "",
3170
+ version: SCHEMA_VERSION,
3171
+ indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
3172
+ };
3173
+ }
3174
+ if (idxState.indexing) {
3175
+ const store2 = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
3176
+ try {
3177
+ const stats = store2.getStats();
3178
+ return {
3179
+ ...stats,
3180
+ indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
3181
+ };
3182
+ } finally {
3183
+ store2.close();
3184
+ }
3185
+ }
3031
3186
  const store = new IndexStore(ctx.projectRoot, { indexDir: codebaseIndexDirOverride(ctx) });
3032
3187
  try {
3033
3188
  const stats = store.getStats();
@@ -3171,9 +3326,9 @@ async function fileDiff(input, ctx, _signal) {
3171
3326
  const results = [];
3172
3327
  for (const file of files) {
3173
3328
  const absPath = safeResolve(file, ctx);
3174
- const stat10 = await fs11.stat(absPath).catch(() => null);
3329
+ const stat10 = await fs12.stat(absPath).catch(() => null);
3175
3330
  if (!stat10?.isFile()) continue;
3176
- const content = await fs11.readFile(absPath, "utf8");
3331
+ const content = await fs12.readFile(absPath, "utf8");
3177
3332
  const lines = content.split(/\r?\n/);
3178
3333
  results.push(formatWithLineNumbers(file, lines));
3179
3334
  }
@@ -3235,7 +3390,7 @@ var documentTool = {
3235
3390
  const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
3236
3391
  for (const absPath of fileList) {
3237
3392
  try {
3238
- const content = await fs11.readFile(absPath, "utf8");
3393
+ const content = await fs12.readFile(absPath, "utf8");
3239
3394
  filesProcessed++;
3240
3395
  const processed = processFile(
3241
3396
  content,
@@ -3271,7 +3426,7 @@ async function resolveFiles(filesInput, cwd) {
3271
3426
  for (const f of files) {
3272
3427
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
3273
3428
  try {
3274
- const stat10 = await fs11.stat(absPath);
3429
+ const stat10 = await fs12.stat(absPath);
3275
3430
  if (stat10.isFile()) resolved.push(absPath);
3276
3431
  } catch {
3277
3432
  }
@@ -3363,7 +3518,7 @@ var editTool = {
3363
3518
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
3364
3519
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
3365
3520
  const absPath = await safeResolveReal(input.path, ctx);
3366
- const stat10 = await fs11.stat(absPath).catch((err) => {
3521
+ const stat10 = await fs12.stat(absPath).catch((err) => {
3367
3522
  if (err.code === "ENOENT") {
3368
3523
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
3369
3524
  }
@@ -3373,8 +3528,8 @@ var editTool = {
3373
3528
  if (!ctx.hasRead(absPath)) {
3374
3529
  throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
3375
3530
  }
3376
- const original = await fs11.readFile(absPath, "utf8");
3377
- const updated = await fs11.stat(absPath);
3531
+ const original = await fs12.readFile(absPath, "utf8");
3532
+ const updated = await fs12.stat(absPath);
3378
3533
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
3379
3534
  const lastReadMtime = ctx.lastReadMtime(absPath);
3380
3535
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
@@ -3414,7 +3569,7 @@ var editTool = {
3414
3569
  const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
3415
3570
  const newFile = toStyle(newFileLf, style);
3416
3571
  await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
3417
- const written = await fs11.stat(absPath);
3572
+ const written = await fs12.stat(absPath);
3418
3573
  ctx.recordRead(absPath, written.mtimeMs);
3419
3574
  ctx.session.recordFileChange({
3420
3575
  path: absPath,
@@ -4446,7 +4601,7 @@ var globTool = {
4446
4601
  }
4447
4602
  let entries;
4448
4603
  try {
4449
- entries = await fs11.readdir(dir, { withFileTypes: true });
4604
+ entries = await fs12.readdir(dir, { withFileTypes: true });
4450
4605
  } catch {
4451
4606
  return;
4452
4607
  }
@@ -4462,7 +4617,7 @@ var globTool = {
4462
4617
  } else if (e.isFile()) {
4463
4618
  if (re.test(rel) || re.test(name)) {
4464
4619
  try {
4465
- const st = await fs11.stat(full);
4620
+ const st = await fs12.stat(full);
4466
4621
  results.push({ rel: full, mtime: st.mtimeMs });
4467
4622
  if (results.length >= limit) {
4468
4623
  truncated = true;
@@ -4481,7 +4636,7 @@ var globTool = {
4481
4636
  };
4482
4637
  async function readGitignore(dir) {
4483
4638
  try {
4484
- const raw = await fs11.readFile(path.join(dir, ".gitignore"), "utf8");
4639
+ const raw = await fs12.readFile(path.join(dir, ".gitignore"), "utf8");
4485
4640
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
4486
4641
  } catch {
4487
4642
  return [];
@@ -4763,7 +4918,7 @@ async function runNative(input, base, mode, limit, signal) {
4763
4918
  if (stopped || signal.aborted) return;
4764
4919
  let entries;
4765
4920
  try {
4766
- entries = await fs11.readdir(dir, { withFileTypes: true });
4921
+ entries = await fs12.readdir(dir, { withFileTypes: true });
4767
4922
  } catch {
4768
4923
  return;
4769
4924
  }
@@ -4778,9 +4933,9 @@ async function runNative(input, base, mode, limit, signal) {
4778
4933
  if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
4779
4934
  if (globRe) globRe.lastIndex = 0;
4780
4935
  try {
4781
- const stat10 = await fs11.stat(full);
4936
+ const stat10 = await fs12.stat(full);
4782
4937
  if (stat10.size > 1e6) continue;
4783
- const head = await fs11.readFile(full);
4938
+ const head = await fs12.readFile(full);
4784
4939
  if (isBinaryBuffer(head)) continue;
4785
4940
  const text = head.toString("utf8");
4786
4941
  const lines = text.split(/\r?\n/);
@@ -4959,7 +5114,7 @@ var jsonTool = {
4959
5114
  let raw;
4960
5115
  if (input.file) {
4961
5116
  try {
4962
- raw = await fs11.readFile(input.file, "utf8");
5117
+ raw = await fs12.readFile(input.file, "utf8");
4963
5118
  } catch {
4964
5119
  return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
4965
5120
  }
@@ -4997,8 +5152,8 @@ var jsonTool = {
4997
5152
  };
4998
5153
  }
4999
5154
  };
5000
- function query(data, path18) {
5001
- const parts = path18.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
5155
+ function query(data, path19) {
5156
+ const parts = path19.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
5002
5157
  let current = data;
5003
5158
  for (const part of parts) {
5004
5159
  if (current === null || current === void 0) return void 0;
@@ -5267,7 +5422,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
5267
5422
  }
5268
5423
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
5269
5424
  var MAX_TAIL_LINES = 1e5;
5270
- async function fileLogs(path18, lines, filterRe, stream) {
5425
+ async function fileLogs(path19, lines, filterRe, stream) {
5271
5426
  const { createInterface } = await import('node:readline');
5272
5427
  const { createReadStream } = await import('node:fs');
5273
5428
  const entries = [];
@@ -5276,7 +5431,7 @@ async function fileLogs(path18, lines, filterRe, stream) {
5276
5431
  let writeIdx = 0;
5277
5432
  let totalLines = 0;
5278
5433
  const rl = createInterface({
5279
- input: createReadStream(path18),
5434
+ input: createReadStream(path19),
5280
5435
  crlfDelay: Number.POSITIVE_INFINITY
5281
5436
  });
5282
5437
  for await (const line of rl) {
@@ -5297,7 +5452,7 @@ async function fileLogs(path18, lines, filterRe, stream) {
5297
5452
  if (parsed) entries.push(parsed);
5298
5453
  }
5299
5454
  return {
5300
- source: path18,
5455
+ source: path19,
5301
5456
  entries,
5302
5457
  total: entries.length,
5303
5458
  truncated: totalLines > effLines,
@@ -5476,12 +5631,12 @@ var patchTool = {
5476
5631
  };
5477
5632
  }
5478
5633
  }
5479
- const tmpDir = await fs11.mkdtemp(path.join(os.tmpdir(), ".wstack_patch_"));
5634
+ const tmpDir = await fs12.mkdtemp(path.join(os.tmpdir(), ".wstack_patch_"));
5480
5635
  try {
5481
- await fs11.chmod(tmpDir, 448).catch(() => {
5636
+ await fs12.chmod(tmpDir, 448).catch(() => {
5482
5637
  });
5483
5638
  const patchFile = path.join(tmpDir, "in.diff");
5484
- await fs11.writeFile(patchFile, input.patch, { mode: 384 });
5639
+ await fs12.writeFile(patchFile, input.patch, { mode: 384 });
5485
5640
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
5486
5641
  const result = await runPatch(args, dir, opts.signal);
5487
5642
  if (result.exitCode !== 0 && !dryRun) {
@@ -5502,7 +5657,7 @@ var patchTool = {
5502
5657
  message: result.stdout || "patch applied"
5503
5658
  };
5504
5659
  } finally {
5505
- await fs11.rm(tmpDir, { recursive: true, force: true }).catch(() => {
5660
+ await fs12.rm(tmpDir, { recursive: true, force: true }).catch(() => {
5506
5661
  });
5507
5662
  }
5508
5663
  }
@@ -5744,7 +5899,7 @@ var readTool = {
5744
5899
  const absPath = await safeResolveReal(input.path, ctx);
5745
5900
  let stat10;
5746
5901
  try {
5747
- stat10 = await fs11.stat(absPath);
5902
+ stat10 = await fs12.stat(absPath);
5748
5903
  } catch (err) {
5749
5904
  const code = err.code;
5750
5905
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
@@ -5756,7 +5911,7 @@ var readTool = {
5756
5911
  if (stat10.size > MAX_BYTES2) {
5757
5912
  throw new Error(`read: file too large (${stat10.size} bytes, limit ${MAX_BYTES2})`);
5758
5913
  }
5759
- const buf = await fs11.readFile(absPath);
5914
+ const buf = await fs12.readFile(absPath);
5760
5915
  if (isBinaryBuffer(buf)) {
5761
5916
  throw new Error(`read: "${input.path}" appears to be binary`);
5762
5917
  }
@@ -5824,11 +5979,11 @@ var replaceTool = {
5824
5979
  const dryRun = input.dry_run ?? false;
5825
5980
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
5826
5981
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
5827
- const realRoot = await fs11.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
5982
+ const realRoot = await fs12.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
5828
5983
  const results = [];
5829
5984
  let totalReplacements = 0;
5830
5985
  for (const absPath of fileList) {
5831
- const lstat2 = await fs11.lstat(absPath).catch((err) => {
5986
+ const lstat2 = await fs12.lstat(absPath).catch((err) => {
5832
5987
  if (err.code === "ENOENT") return null;
5833
5988
  throw err;
5834
5989
  });
@@ -5836,17 +5991,17 @@ var replaceTool = {
5836
5991
  if (lstat2.isSymbolicLink()) continue;
5837
5992
  let realPath;
5838
5993
  try {
5839
- realPath = await fs11.realpath(absPath);
5994
+ realPath = await fs12.realpath(absPath);
5840
5995
  } catch {
5841
5996
  continue;
5842
5997
  }
5843
5998
  const rel = path.relative(realRoot, realPath);
5844
5999
  if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
5845
- const stat10 = await fs11.stat(realPath).catch(() => null);
6000
+ const stat10 = await fs12.stat(realPath).catch(() => null);
5846
6001
  if (!stat10 || !stat10.isFile()) continue;
5847
6002
  let content;
5848
6003
  try {
5849
- const buf = await fs11.readFile(realPath);
6004
+ const buf = await fs12.readFile(realPath);
5850
6005
  if (isBinaryBuffer(buf)) continue;
5851
6006
  content = buf.toString("utf8");
5852
6007
  } catch {
@@ -5898,7 +6053,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
5898
6053
  const resolved = [];
5899
6054
  for (const p of parts) {
5900
6055
  const absPath = safeResolve(p, ctx);
5901
- const stat10 = await fs11.stat(absPath).catch(() => null);
6056
+ const stat10 = await fs12.stat(absPath).catch(() => null);
5902
6057
  if (stat10?.isFile()) {
5903
6058
  resolved.push(absPath);
5904
6059
  }
@@ -5949,7 +6104,7 @@ async function globNative(pattern, base, extraGlob) {
5949
6104
  const walk = async (dir) => {
5950
6105
  let entries;
5951
6106
  try {
5952
- entries = await fs11.readdir(dir, { withFileTypes: true });
6107
+ entries = await fs12.readdir(dir, { withFileTypes: true });
5953
6108
  } catch {
5954
6109
  return;
5955
6110
  }
@@ -5957,7 +6112,7 @@ async function globNative(pattern, base, extraGlob) {
5957
6112
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
5958
6113
  const full = path.join(dir, e.name);
5959
6114
  try {
5960
- const stat10 = await fs11.lstat(full);
6115
+ const stat10 = await fs12.lstat(full);
5961
6116
  if (stat10.isSymbolicLink()) continue;
5962
6117
  } catch {
5963
6118
  continue;
@@ -6134,7 +6289,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
6134
6289
  }
6135
6290
  const fullPath = target;
6136
6291
  if (!dryRun) {
6137
- await fs11.mkdir(path.dirname(fullPath), { recursive: true });
6292
+ await fs12.mkdir(path.dirname(fullPath), { recursive: true });
6138
6293
  await atomicWrite(fullPath, substituteVars(content, name, vars));
6139
6294
  }
6140
6295
  files.push(resolvedPath);
@@ -6525,9 +6680,10 @@ var todoTool = {
6525
6680
  name: "todo",
6526
6681
  category: "Session",
6527
6682
  description: "Manage the session-level todo list. This is the primary mechanism for tracking multi-step work. The list is fully replaced on every call (not appended).",
6528
- usageHint: "BEST PRACTICE for complex tasks:\n- At the beginning of a non-trivial task, create a clear todo list with specific, actionable items.\n- Only **one** item should be `in_progress` at any time.\n- Update the list frequently as work progresses (mark items done, add new ones, change status).\n- The system and user can see this list, so keep it honest and up-to-date.\nThis tool is extremely valuable for maintaining focus and giving the user visibility into your plan.",
6683
+ usageHint: "BEST PRACTICE for complex tasks:\n- At the beginning of a non-trivial task, create a clear todo list with specific, actionable items.\n- Only **one** item should be `in_progress` at any time.\n- Update the list frequently as work progresses (mark items done, add new ones, change status).\n- **Re-order items** to reflect current priorities \u2014 the full list is replaced each call, so item order is entirely under your control.\n- When all items are completed the board auto-clears \u2014 you do NOT need to send an empty list.\n- The system and user can see this list, so keep it honest and up-to-date.\nThis tool is extremely valuable for maintaining focus and giving the user visibility into your plan.",
6529
6684
  permission: "auto",
6530
6685
  mutating: false,
6686
+ // mutates only conversation state (ctx.todos), not external state — no confirmation needed
6531
6687
  timeoutMs: 1e3,
6532
6688
  inputSchema: {
6533
6689
  type: "object",
@@ -6773,10 +6929,14 @@ var toolSearchTool = {
6773
6929
  permission: t.permission,
6774
6930
  mutating: t.mutating
6775
6931
  }));
6932
+ const totalAvailable = tools.length;
6933
+ 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
6934
  return {
6777
6935
  tools: results,
6778
6936
  total: filtered.length,
6779
- truncated: filtered.length > limit
6937
+ truncated: filtered.length > limit,
6938
+ ...hint ? { hint } : {},
6939
+ _available: totalAvailable
6780
6940
  };
6781
6941
  }
6782
6942
  };
@@ -6986,7 +7146,7 @@ var treeTool = {
6986
7146
  }
6987
7147
  };
6988
7148
  async function walkDir(dir, depth, opts) {
6989
- const entries = await fs11.readdir(dir, { withFileTypes: true }).catch(() => []);
7149
+ const entries = await fs12.readdir(dir, { withFileTypes: true }).catch(() => []);
6990
7150
  const filtered = entries.filter((e) => {
6991
7151
  if (!opts.showHidden && e.name.startsWith(".")) return false;
6992
7152
  if (opts.exclude.has(e.name)) return false;
@@ -7134,14 +7294,14 @@ var writeTool = {
7134
7294
  let existed = false;
7135
7295
  let prev = "";
7136
7296
  try {
7137
- const stat11 = await fs11.stat(absPath);
7297
+ const stat11 = await fs12.stat(absPath);
7138
7298
  existed = stat11.isFile();
7139
7299
  if (existed) {
7140
7300
  if (!ctx.hasRead(absPath)) {
7141
- prev = await fs11.readFile(absPath, "utf8");
7301
+ prev = await fs12.readFile(absPath, "utf8");
7142
7302
  ctx.recordRead(absPath, stat11.mtimeMs);
7143
7303
  } else {
7144
- prev = await fs11.readFile(absPath, "utf8");
7304
+ prev = await fs12.readFile(absPath, "utf8");
7145
7305
  }
7146
7306
  }
7147
7307
  } catch (err) {
@@ -7152,7 +7312,7 @@ var writeTool = {
7152
7312
  await atomicWrite(absPath, input.content);
7153
7313
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
7154
7314
  + (new file, ${input.content.split("\n").length} lines)`;
7155
- const stat10 = await fs11.stat(absPath);
7315
+ const stat10 = await fs12.stat(absPath);
7156
7316
  ctx.recordRead(absPath, stat10.mtimeMs);
7157
7317
  ctx.session.recordFileChange({
7158
7318
  path: absPath,