@wrongstack/tools 0.275.1 → 0.276.3

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.
Files changed (47) hide show
  1. package/dist/{background-indexer-BoTUw0EM.d.ts → background-indexer-BeDBxfSh.d.ts} +6 -0
  2. package/dist/builtin.js +1056 -246
  3. package/dist/builtin.js.map +1 -1
  4. package/dist/codebase-index/index.d.ts +30 -2
  5. package/dist/codebase-index/index.js +201 -24
  6. package/dist/codebase-index/index.js.map +1 -1
  7. package/dist/codebase-index/worker.js +196 -23
  8. package/dist/codebase-index/worker.js.map +1 -1
  9. package/dist/document.js +2 -2
  10. package/dist/document.js.map +1 -1
  11. package/dist/edit.js +52 -15
  12. package/dist/edit.js.map +1 -1
  13. package/dist/fetch.js +89 -18
  14. package/dist/fetch.js.map +1 -1
  15. package/dist/glob.js +35 -1
  16. package/dist/glob.js.map +1 -1
  17. package/dist/grep.js +15 -4
  18. package/dist/grep.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1090 -255
  21. package/dist/index.js.map +1 -1
  22. package/dist/install.d.ts +7 -0
  23. package/dist/install.js +6 -0
  24. package/dist/install.js.map +1 -1
  25. package/dist/json.d.ts +26 -1
  26. package/dist/json.js +453 -46
  27. package/dist/json.js.map +1 -1
  28. package/dist/memory.js +26 -4
  29. package/dist/memory.js.map +1 -1
  30. package/dist/outdated.js +2 -2
  31. package/dist/outdated.js.map +1 -1
  32. package/dist/pack.js +1056 -246
  33. package/dist/pack.js.map +1 -1
  34. package/dist/read.js +36 -6
  35. package/dist/read.js.map +1 -1
  36. package/dist/replace.js +27 -9
  37. package/dist/replace.js.map +1 -1
  38. package/dist/search.d.ts +5 -1
  39. package/dist/search.js +179 -62
  40. package/dist/search.js.map +1 -1
  41. package/dist/tool-help.js +2 -2
  42. package/dist/tool-help.js.map +1 -1
  43. package/dist/tool-search.js +2 -2
  44. package/dist/tool-search.js.map +1 -1
  45. package/dist/write.js +13 -3
  46. package/dist/write.js.map +1 -1
  47. package/package.json +2 -2
package/dist/pack.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { spawn, execFileSync } from 'node:child_process';
2
2
  import * as Core from '@wrongstack/core';
3
- import { buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
3
+ import { buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, ToolValidationError, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, ToolError, FetchError, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, deepMerge, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, FsError, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
4
4
  import * as fs from 'node:fs';
5
5
  import { statSync, mkdirSync, createWriteStream } from 'node:fs';
6
6
  import * as fs2 from 'node:fs/promises';
@@ -833,8 +833,8 @@ async function* spawnStream(opts) {
833
833
  try {
834
834
  for (; ; ) {
835
835
  while (queue.length === 0) {
836
- await new Promise((resolve6) => {
837
- waiter = resolve6;
836
+ await new Promise((resolve7) => {
837
+ waiter = resolve7;
838
838
  });
839
839
  }
840
840
  const chunk = queue.shift();
@@ -2133,10 +2133,10 @@ var bashTool = {
2133
2133
  queue.push(c);
2134
2134
  }
2135
2135
  };
2136
- const next = () => new Promise((resolve6) => {
2136
+ const next = () => new Promise((resolve7) => {
2137
2137
  const c = queue.shift();
2138
- if (c) resolve6(c);
2139
- else resolveNext = resolve6;
2138
+ if (c) resolve7(c);
2139
+ else resolveNext = resolve7;
2140
2140
  });
2141
2141
  let lastFlush = Date.now();
2142
2142
  const flush = () => {
@@ -2536,8 +2536,8 @@ var Bm25Index = class {
2536
2536
  df;
2537
2537
  N;
2538
2538
  safeAvgLen;
2539
- score(query2, filter) {
2540
- const qTokens = tokenise(query2);
2539
+ score(query, filter) {
2540
+ const qTokens = tokenise(query);
2541
2541
  if (qTokens.length === 0) return [];
2542
2542
  const results = [];
2543
2543
  for (const doc of this.documents) {
@@ -2860,7 +2860,7 @@ var IndexStore = class {
2860
2860
  );
2861
2861
  }
2862
2862
  // ─── Search ──────────────────────────────────────────────────────────────────
2863
- search(query2, filter) {
2863
+ search(query, filter) {
2864
2864
  const conditions = [];
2865
2865
  const values = [];
2866
2866
  let effectiveKind = filter?.kind;
@@ -2884,8 +2884,8 @@ var IndexStore = class {
2884
2884
  conditions.push("file LIKE ?");
2885
2885
  values.push(`%${filter.file}%`);
2886
2886
  }
2887
- if (query2.trim()) {
2888
- const tokens = query2.toLowerCase().split(/\s+/).filter(Boolean);
2887
+ if (query.trim()) {
2888
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
2889
2889
  const tokenConds = tokens.map(() => "text LIKE ?");
2890
2890
  conditions.push(`(${tokenConds.join(" OR ")})`);
2891
2891
  for (const t of tokens) values.push(`%${t}%`);
@@ -2919,10 +2919,10 @@ var IndexStore = class {
2919
2919
  * `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
2920
2920
  * "users", camelCase-split text makes "complex" find "complexOperation").
2921
2921
  */
2922
- searchRanked(query2, filter, limit) {
2923
- const tokens = tokenise(query2);
2922
+ searchRanked(query, filter, limit) {
2923
+ const tokens = tokenise(query);
2924
2924
  if (tokens.length === 0 || !this.ftsAvailable) {
2925
- return this.searchRankedFallback(query2, filter, limit);
2925
+ return this.searchRankedFallback(query, filter, limit);
2926
2926
  }
2927
2927
  let effectiveKind = filter?.kind;
2928
2928
  if (filter?.lspKind !== void 0) {
@@ -2979,19 +2979,19 @@ var IndexStore = class {
2979
2979
  };
2980
2980
  }
2981
2981
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
2982
- searchRankedFallback(query2, filter, limit) {
2983
- const candidates = this.search(query2, filter);
2982
+ searchRankedFallback(query, filter, limit) {
2983
+ const candidates = this.search(query, filter);
2984
2984
  if (candidates.length === 0) return { results: [], total: 0 };
2985
- if (!query2.trim()) {
2985
+ if (!query.trim()) {
2986
2986
  return { results: candidates.slice(0, limit), total: candidates.length };
2987
2987
  }
2988
2988
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
2989
2989
  const bm25 = buildBm25Index(
2990
2990
  candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
2991
2991
  );
2992
- const scored = bm25.score(query2, (id) => candidateById.has(id));
2992
+ const scored = bm25.score(query, (id) => candidateById.has(id));
2993
2993
  scored.sort((a, b) => b.score - a.score);
2994
- const qTokens = tokenise(query2);
2994
+ const qTokens = tokenise(query);
2995
2995
  const results = scored.slice(0, limit).map(({ id, score }) => {
2996
2996
  const c = expectDefined(candidateById.get(id));
2997
2997
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
@@ -3102,6 +3102,104 @@ var IndexStore = class {
3102
3102
  }
3103
3103
  });
3104
3104
  }
3105
+ /**
3106
+ * Commit a batch of file-level symbol/refs/upserts in a single transaction.
3107
+ *
3108
+ * Used by the indexer to amortize SQLite commit overhead across many files.
3109
+ * Before this, the indexer issued one transaction per file (BEGIN IMMEDIATE
3110
+ * for symbols, plus per-file deletes and an upsertFile call), so a 20-file
3111
+ * parallel batch cost ~5+ transactions × 20 files = 100+ commits. With
3112
+ * this entry point we do exactly one BEGIN/COMMIT per parallel batch.
3113
+ *
3114
+ * Each entry must already be a fully-parsed FileSymbols (symbols + refs).
3115
+ * The caller is responsible for the per-file prefix accounting
3116
+ * (refsByLine → flat list with `fromId` populated). `deleteForFiles` lets
3117
+ * the caller clear stale symbols/refs for any files being re-indexed before
3118
+ * the inserts run (required to keep refs → symbols FK invariants).
3119
+ *
3120
+ * Returns the symbols back with their assigned `id` (same shape as
3121
+ * {@link insertSymbols}) so callers can build final per-file results.
3122
+ */
3123
+ commitBatch(entries, options = {}) {
3124
+ if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
3125
+ return [];
3126
+ }
3127
+ return this.runWithRetry(() => {
3128
+ this.db.exec("BEGIN IMMEDIATE");
3129
+ try {
3130
+ if (options.deleteForFiles && options.deleteForFiles.length > 0) {
3131
+ const placeholders = options.deleteForFiles.map(() => "?").join(",");
3132
+ if (this.ftsAvailable) {
3133
+ this.db.prepare(
3134
+ `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
3135
+ ).run(...options.deleteForFiles);
3136
+ }
3137
+ this.db.prepare(
3138
+ `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
3139
+ ).run(...options.deleteForFiles);
3140
+ this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
3141
+ }
3142
+ const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
3143
+ let nextId = (maxRows[0]?.m ?? 0) + 1;
3144
+ const symStmt = this.db.prepare(
3145
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
3146
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
3147
+ );
3148
+ const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
3149
+ const allInserted = [];
3150
+ const refsToInsert = [];
3151
+ for (const entry of entries) {
3152
+ for (const s of entry.symbols) {
3153
+ const id = nextId++;
3154
+ symStmt.run(
3155
+ id,
3156
+ s.lang,
3157
+ s.kind,
3158
+ s.name,
3159
+ s.file,
3160
+ s.line,
3161
+ s.col,
3162
+ s.signature,
3163
+ s.docComment,
3164
+ s.scope,
3165
+ s.text,
3166
+ s.file
3167
+ );
3168
+ ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
3169
+ allInserted.push({ ...s, id });
3170
+ }
3171
+ for (const r of entry.refs) refsToInsert.push(r);
3172
+ }
3173
+ if (refsToInsert.length > 0) {
3174
+ const refStmt = this.db.prepare(
3175
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
3176
+ VALUES (?, ?, ?, ?, ?)`
3177
+ );
3178
+ for (const ref of refsToInsert) {
3179
+ refStmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
3180
+ }
3181
+ }
3182
+ const upsertStmt = this.db.prepare(
3183
+ `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
3184
+ VALUES (?, ?, ?, ?, ?)
3185
+ ON CONFLICT(file) DO UPDATE SET
3186
+ lang = excluded.lang,
3187
+ mtime_ms = excluded.mtime_ms,
3188
+ symbol_count = excluded.symbol_count,
3189
+ last_indexed = excluded.last_indexed`
3190
+ );
3191
+ const now = Date.now();
3192
+ for (const entry of entries) {
3193
+ upsertStmt.run(entry.file, entry.lang, entry.mtimeMs, entry.symbolCount, now);
3194
+ }
3195
+ this.db.exec("COMMIT");
3196
+ return allInserted;
3197
+ } catch (err) {
3198
+ this.db.exec("ROLLBACK");
3199
+ throw err;
3200
+ }
3201
+ });
3202
+ }
3105
3203
  /**
3106
3204
  * Delete all refs whose source symbols are in a given file.
3107
3205
  * Used when re-indexing a file to clear stale refs.
@@ -3618,8 +3716,8 @@ async function syncGoParse(filePath, content, lang) {
3618
3716
  proc.stdin?.write(content);
3619
3717
  proc.stdin?.end();
3620
3718
  const { code } = await Promise.race([
3621
- new Promise((resolve6) => {
3622
- proc.on("close", (c) => resolve6({ code: c }));
3719
+ new Promise((resolve7) => {
3720
+ proc.on("close", (c) => resolve7({ code: c }));
3623
3721
  }),
3624
3722
  new Promise(
3625
3723
  (_, reject) => setTimeout(() => {
@@ -3881,8 +3979,8 @@ async function syncPyParse(filePath, content, lang) {
3881
3979
  stdout += chunk.toString();
3882
3980
  });
3883
3981
  const { code } = await Promise.race([
3884
- new Promise((resolve6) => {
3885
- proc.on("close", (c) => resolve6({ code: c }));
3982
+ new Promise((resolve7) => {
3983
+ proc.on("close", (c) => resolve7({ code: c }));
3886
3984
  }),
3887
3985
  new Promise(
3888
3986
  (_, reject) => setTimeout(() => {
@@ -3967,8 +4065,8 @@ async function tryNativeParse(file, content) {
3967
4065
  stdout += chunk.toString();
3968
4066
  });
3969
4067
  const { code } = await Promise.race([
3970
- new Promise((resolve6) => {
3971
- proc.on("close", (c) => resolve6({ code: c }));
4068
+ new Promise((resolve7) => {
4069
+ proc.on("close", (c) => resolve7({ code: c }));
3972
4070
  }),
3973
4071
  new Promise(
3974
4072
  (_, reject) => setTimeout(() => {
@@ -4458,7 +4556,7 @@ async function loadGitignoreMatcher(projectRoot) {
4458
4556
  var YIELD_EVERY_N = 50;
4459
4557
  var PARALLEL_BATCH = 20;
4460
4558
  function yieldEventLoop() {
4461
- return new Promise((resolve6) => setImmediate(resolve6));
4559
+ return new Promise((resolve7) => setImmediate(resolve7));
4462
4560
  }
4463
4561
  function throwIfAborted(signal) {
4464
4562
  if (!signal?.aborted) return;
@@ -4633,6 +4731,8 @@ async function runIndexerWithStore(store, opts) {
4633
4731
  return { file, stat: stat11, lang, parsed, content };
4634
4732
  })
4635
4733
  );
4734
+ const batchEntries = [];
4735
+ const deleteForFiles = [];
4636
4736
  for (let fi = 0; fi < statReadParse.length; fi++) {
4637
4737
  const settled = statReadParse[fi];
4638
4738
  const file = expectDefined(batchFiles[fi]);
@@ -4657,43 +4757,116 @@ async function runIndexerWithStore(store, opts) {
4657
4757
  }
4658
4758
  if (!lang || !parsed) {
4659
4759
  if (lang) {
4660
- store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: 0, lastIndexed: Date.now() });
4760
+ store.upsertFile({
4761
+ file,
4762
+ lang,
4763
+ mtimeMs: Math.floor(stat11.mtimeMs),
4764
+ symbolCount: 0,
4765
+ lastIndexed: Date.now()
4766
+ });
4661
4767
  filesIndexed++;
4662
4768
  }
4663
4769
  continue;
4664
4770
  }
4665
- store.deleteRefsForFile(file);
4666
- store.deleteSymbolsForFile(file);
4667
4771
  if (parsed.symbols.length === 0) {
4668
- store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: 0, lastIndexed: Date.now() });
4772
+ store.upsertFile({
4773
+ file,
4774
+ lang,
4775
+ mtimeMs: Math.floor(stat11.mtimeMs),
4776
+ symbolCount: 0,
4777
+ lastIndexed: Date.now()
4778
+ });
4669
4779
  filesIndexed++;
4670
4780
  continue;
4671
4781
  }
4672
- const symbolsWithIds = store.insertSymbols(parsed.symbols);
4673
- const count = symbolsWithIds.length;
4674
- symbolsIndexed += count;
4675
- langStats[lang] = (langStats[lang] ?? 0) + count;
4782
+ const refs = [];
4676
4783
  if (parsed.refs && parsed.refs.length > 0) {
4677
- const refsByLine = /* @__PURE__ */ new Map();
4678
- for (const r of parsed.refs) {
4679
- let arr = refsByLine.get(r.line);
4680
- if (!arr) {
4681
- arr = [];
4682
- refsByLine.set(r.line, arr);
4784
+ for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
4785
+ }
4786
+ batchEntries.push({
4787
+ file,
4788
+ lang,
4789
+ symbols: parsed.symbols,
4790
+ refs,
4791
+ mtimeMs: Math.floor(stat11.mtimeMs),
4792
+ symbolCount: parsed.symbols.length
4793
+ });
4794
+ deleteForFiles.push(file);
4795
+ }
4796
+ if (batchEntries.length > 0) {
4797
+ try {
4798
+ const inserted = store.commitBatch(batchEntries, { deleteForFiles });
4799
+ let cursor = 0;
4800
+ for (const entry of batchEntries) {
4801
+ const count = entry.symbols.length;
4802
+ const symbolsWithIds = inserted.slice(cursor, cursor + count);
4803
+ cursor += count;
4804
+ symbolsIndexed += count;
4805
+ langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
4806
+ filesIndexed++;
4807
+ if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
4808
+ const refsByLine = /* @__PURE__ */ new Map();
4809
+ for (let i = 0; i < symbolsWithIds.length; i++) {
4810
+ const sym = symbolsWithIds[i];
4811
+ let arr = refsByLine.get(sym.line);
4812
+ if (!arr) {
4813
+ arr = [];
4814
+ refsByLine.set(sym.line, arr);
4815
+ }
4816
+ arr.push(i);
4817
+ }
4818
+ for (const ref of entry.refs) {
4819
+ const indices = refsByLine.get(ref.line);
4820
+ if (indices && indices.length > 0) {
4821
+ const idx = indices.shift();
4822
+ ref.fromId = symbolsWithIds[idx].id;
4823
+ }
4824
+ }
4683
4825
  }
4684
- arr.push(r);
4685
4826
  }
4686
- const batch = [];
4687
- for (const sym of symbolsWithIds) {
4688
- const symRefs = refsByLine.get(sym.line);
4689
- if (symRefs) {
4690
- for (const r of symRefs) batch.push({ ...r, fromId: sym.id });
4827
+ } catch (err) {
4828
+ const message = err instanceof Error ? err.message : String(err);
4829
+ errors.push(`commitBatch failed: ${message} \u2014 falling back to per-file writes`);
4830
+ for (const entry of batchEntries) {
4831
+ try {
4832
+ store.deleteRefsForFile(entry.file);
4833
+ store.deleteSymbolsForFile(entry.file);
4834
+ const symbolsWithIds = store.insertSymbols(entry.symbols);
4835
+ symbolsIndexed += symbolsWithIds.length;
4836
+ langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
4837
+ filesIndexed++;
4838
+ if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
4839
+ const refsByLine = /* @__PURE__ */ new Map();
4840
+ for (const sym of symbolsWithIds) {
4841
+ let arr = refsByLine.get(sym.line);
4842
+ if (!arr) {
4843
+ arr = [];
4844
+ refsByLine.set(sym.line, arr);
4845
+ }
4846
+ arr.push(sym);
4847
+ }
4848
+ const fallbackBatch = [];
4849
+ for (const ref of entry.refs) {
4850
+ const syms = refsByLine.get(ref.line);
4851
+ if (syms && syms.length > 0) {
4852
+ const sym = syms.shift();
4853
+ fallbackBatch.push({ ...ref, fromId: sym.id });
4854
+ }
4855
+ }
4856
+ if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
4857
+ }
4858
+ store.upsertFile({
4859
+ file: entry.file,
4860
+ lang: entry.lang,
4861
+ mtimeMs: entry.mtimeMs,
4862
+ symbolCount: entry.symbolCount,
4863
+ lastIndexed: Date.now()
4864
+ });
4865
+ } catch (innerErr) {
4866
+ errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
4691
4867
  }
4692
4868
  }
4693
- if (batch.length > 0) store.insertRefsBatch(batch);
4694
4869
  }
4695
- store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: count, lastIndexed: Date.now() });
4696
- filesIndexed++;
4697
4870
  }
4698
4871
  }
4699
4872
  if (discoveredFiles) {
@@ -4861,7 +5034,7 @@ function terminateWorker(reason) {
4861
5034
  function callIndexOp(op, args, opts) {
4862
5035
  const w = ensureWorker();
4863
5036
  if (!w) return callInline(op, args, opts);
4864
- return new Promise((resolve6, reject) => {
5037
+ return new Promise((resolve7, reject) => {
4865
5038
  const id = nextRpcId++;
4866
5039
  const timer = setTimeout(() => {
4867
5040
  pending.delete(id);
@@ -4884,7 +5057,7 @@ function callIndexOp(op, args, opts) {
4884
5057
  pending.set(id, {
4885
5058
  resolve: (v) => {
4886
5059
  cleanup();
4887
- resolve6(v);
5060
+ resolve7(v);
4888
5061
  },
4889
5062
  reject: (e) => {
4890
5063
  cleanup();
@@ -5076,7 +5249,7 @@ var codebaseSearchTool = {
5076
5249
  name: "codebase-search",
5077
5250
  category: "Project",
5078
5251
  icon: "index",
5079
- description: "Semantic/keyword search over the indexed codebase symbols (functions, classes, interfaces, etc.). Uses BM25 ranking. Much more powerful and structured than raw `grep` for finding code by name or concept.",
5252
+ description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Much more powerful and structured than raw `grep` for finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
5080
5253
  usageHint: "PREFERRED FOR CODE UNDERSTANDING:\n\n- Use when you need to find where something is defined or used by name.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\nThis is generally better than `grep` when you are looking for symbols rather than arbitrary text patterns.",
5081
5254
  permission: "auto",
5082
5255
  mutating: false,
@@ -5110,6 +5283,10 @@ var codebaseSearchTool = {
5110
5283
  description: "Maximum results to return (default 20, max 100)",
5111
5284
  minimum: 1,
5112
5285
  maximum: 100
5286
+ },
5287
+ preferLsp: {
5288
+ type: "boolean",
5289
+ description: "Prefer live LSP results over the index. Index-only when the LSP plugin is not active. When the LSP plugin is active and this is true, results come from live workspaceSymbol queries."
5113
5290
  }
5114
5291
  },
5115
5292
  required: ["query"]
@@ -5374,7 +5551,14 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
5374
5551
  kitId: active.kit,
5375
5552
  outPath: input.out
5376
5553
  });
5377
- const abs = path3.join(ctx.projectRoot, result.path);
5554
+ const root = path3.resolve(ctx.projectRoot);
5555
+ const abs = path3.resolve(path3.join(ctx.projectRoot, result.path));
5556
+ const rel = path3.relative(root, abs);
5557
+ if (rel.startsWith("..") || path3.isAbsolute(rel)) {
5558
+ throw new Error(
5559
+ `design: materialize path "${result.path}" would escape the project root`
5560
+ );
5561
+ }
5378
5562
  let exists = false;
5379
5563
  try {
5380
5564
  await fs2.access(abs);
@@ -5533,7 +5717,7 @@ function findGitDir(cwd) {
5533
5717
  return null;
5534
5718
  }
5535
5719
  function runGit(args, cwd, signal) {
5536
- return new Promise((resolve6) => {
5720
+ return new Promise((resolve7) => {
5537
5721
  let stdout = "";
5538
5722
  let stderr = "";
5539
5723
  const child = spawn("git", args, {
@@ -5549,8 +5733,8 @@ function runGit(args, cwd, signal) {
5549
5733
  child.stderr?.on("data", (c) => {
5550
5734
  stderr += c.toString();
5551
5735
  });
5552
- child.on("close", (code) => resolve6({ stdout, stderr, exitCode: code ?? 0 }));
5553
- child.on("error", (e) => resolve6({ stdout: "", stderr: e.message, exitCode: 1 }));
5736
+ child.on("close", (code) => resolve7({ stdout, stderr, exitCode: code ?? 0 }));
5737
+ child.on("error", (e) => resolve7({ stdout: "", stderr: e.message, exitCode: 1 }));
5554
5738
  });
5555
5739
  }
5556
5740
  async function fileDiff(input, ctx, _signal) {
@@ -5589,8 +5773,8 @@ ${numbered}`;
5589
5773
  var documentTool = {
5590
5774
  name: "document",
5591
5775
  category: "Project",
5592
- description: "Preview documentation comments (JSDoc/TSDoc style) that would be generated for code symbols. Returns a list of candidates with status `skipped` \u2014 the tool is currently a read-only preview and does NOT write to files.",
5593
- usageHint: "USE FOR IMPROVING CODE DOCUMENTATION:\n\n- Good for adding missing docs to public APIs or complex functions.\n- Currently this is a PREVIEW-ONLY tool: it does not modify files.\n- Use the output to decide which symbols to document manually, or pass the candidates to `edit` / `patch`.\n- `overwrite`, `style`, and `target` parameters are accepted for future expansion but are ignored today.\nAlways review the proposed documentation before applying it \u2014 the model can hallucinate details.",
5776
+ description: "DEPRECATED \u2014 use the `auto_doc` tool with `dryRun: true` instead. This tool is a read-only preview stub that returns `skipped` candidates without generating real docstrings.",
5777
+ usageHint: "Deprecated: prefer `auto_doc` with `dryRun: true` for previewing, or `auto_doc` without dryRun for writing. This tool only lists undocumented symbols with placeholder comments \u2014 it does not generate real JSDoc/TSDoc.",
5594
5778
  permission: "auto",
5595
5779
  mutating: false,
5596
5780
  timeoutMs: 3e4,
@@ -5753,28 +5937,62 @@ var editTool = {
5753
5937
  required: ["path", "old_string", "new_string"]
5754
5938
  },
5755
5939
  async execute(input, ctx) {
5756
- if (!input?.path) throw new Error("edit: path is required");
5757
- if (input.old_string === void 0) throw new Error("edit: old_string is required");
5758
- if (input.new_string === void 0) throw new Error("edit: new_string is required");
5759
- if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
5940
+ if (!input?.path) {
5941
+ throw new ToolValidationError({ message: "edit: path is required", field: "path" });
5942
+ }
5943
+ if (input.old_string === void 0) {
5944
+ throw new ToolValidationError({
5945
+ message: "edit: old_string is required",
5946
+ field: "old_string"
5947
+ });
5948
+ }
5949
+ if (input.new_string === void 0) {
5950
+ throw new ToolValidationError({
5951
+ message: "edit: new_string is required",
5952
+ field: "new_string"
5953
+ });
5954
+ }
5955
+ if (input.old_string === "") {
5956
+ throw new ToolValidationError({
5957
+ message: "edit: old_string cannot be empty",
5958
+ field: "old_string"
5959
+ });
5960
+ }
5760
5961
  const absPath = await safeResolveReal(input.path, ctx);
5761
5962
  const stat11 = await fs2.stat(absPath).catch((err) => {
5762
5963
  if (err.code === "ENOENT") {
5763
- throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
5964
+ throw new ToolValidationError({
5965
+ message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
5966
+ field: "path",
5967
+ context: { exists: false }
5968
+ });
5764
5969
  }
5765
5970
  throw err;
5766
5971
  });
5767
- if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
5972
+ if (!stat11.isFile()) {
5973
+ throw new ToolValidationError({
5974
+ message: `edit: "${input.path}" is not a regular file`,
5975
+ field: "path"
5976
+ });
5977
+ }
5768
5978
  const autoRead = !ctx.hasRead(absPath);
5769
5979
  const original = await fs2.readFile(absPath, "utf8");
5770
5980
  const updated = await fs2.stat(absPath);
5771
5981
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
5772
5982
  const lastReadMtime = ctx.lastReadMtime(absPath);
5773
5983
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
5774
- throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
5984
+ throw new ToolValidationError({
5985
+ message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
5986
+ field: "path",
5987
+ context: { reason: "external_modification" }
5988
+ });
5775
5989
  }
5776
5990
  if (autoRead && updated.mtimeMs > stat11.mtimeMs + mtimeTolerance) {
5777
- throw new Error(`edit: file "${input.path}" changed while being auto-read. Retry the edit.`);
5991
+ throw new ToolValidationError({
5992
+ message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
5993
+ field: "path",
5994
+ context: { reason: "auto_read_race" }
5995
+ });
5778
5996
  }
5779
5997
  const autoReadNote = autoRead ? `No prior read was recorded for "${input.path}"; edit auto-read the current file and applied the replacement only after the ambiguity checks passed.` : void 0;
5780
5998
  const style = detectNewlineStyle(original);
@@ -5800,15 +6018,18 @@ var editTool = {
5800
6018
  }
5801
6019
  if (count === 0) {
5802
6020
  const hint = findSimilarity(fileLf, oldLf);
5803
- throw new Error(
5804
- `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}.` : ""}`
5805
- );
6021
+ throw new ToolValidationError({
6022
+ message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}.` : ""}`,
6023
+ field: "old_string"
6024
+ });
5806
6025
  }
5807
6026
  if (count > 1 && !input.replace_all) {
5808
6027
  const lines = lineNumbersFor(fileLf, matches);
5809
- throw new Error(
5810
- `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")}). Add more context to make it unique, or set replace_all: true.`
5811
- );
6028
+ throw new ToolValidationError({
6029
+ message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")}). Add more context to make it unique, or set replace_all: true.`,
6030
+ field: "old_string",
6031
+ context: { occurrences: count }
6032
+ });
5812
6033
  }
5813
6034
  const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
5814
6035
  const newFile = toStyle(newFileLf, style);
@@ -6142,7 +6363,7 @@ var execTool = {
6142
6363
  }
6143
6364
  };
6144
6365
  function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
6145
- return new Promise((resolve6) => {
6366
+ return new Promise((resolve7) => {
6146
6367
  let stdout = "";
6147
6368
  let stderr = "";
6148
6369
  let killed = false;
@@ -6150,7 +6371,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
6150
6371
  const finish = (result) => {
6151
6372
  if (resolvedOnce.value) return;
6152
6373
  resolvedOnce.value = true;
6153
- resolve6(result);
6374
+ resolve7(result);
6154
6375
  };
6155
6376
  const startedAt = Date.now();
6156
6377
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
@@ -6327,10 +6548,16 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
6327
6548
  for (; ; ) {
6328
6549
  const parsed = new URL(currentUrl);
6329
6550
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
6330
- throw new Error(`fetch: redirect to unsupported protocol "${parsed.protocol}"`);
6551
+ throw new ToolValidationError({
6552
+ message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
6553
+ field: "url"
6554
+ });
6331
6555
  }
6332
6556
  if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
6333
- throw new Error("fetch: redirect to http:// blocked (HTTPS required by default)");
6557
+ throw new ToolValidationError({
6558
+ message: "fetch: redirect to http:// blocked (HTTPS required by default)",
6559
+ field: "url"
6560
+ });
6334
6561
  }
6335
6562
  await assertNotPrivate(parsed.hostname);
6336
6563
  const init = {
@@ -6345,11 +6572,19 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
6345
6572
  }
6346
6573
  redirectCount++;
6347
6574
  if (redirectCount > maxRedirects) {
6348
- throw new Error(`fetch: exceeded ${maxRedirects} redirects`);
6575
+ throw new FetchError({
6576
+ message: `fetch: exceeded ${maxRedirects} redirects`,
6577
+ status: res.status,
6578
+ context: { url: currentUrl, maxRedirects, redirectCount }
6579
+ });
6349
6580
  }
6350
6581
  const location = res.headers.get("location");
6351
6582
  if (!location) {
6352
- throw new Error("fetch: redirect status with no location header");
6583
+ throw new FetchError({
6584
+ message: "fetch: redirect status with no location header",
6585
+ status: res.status,
6586
+ context: { url: currentUrl, redirectCount }
6587
+ });
6353
6588
  }
6354
6589
  currentUrl = new URL(location, currentUrl).toString();
6355
6590
  }
@@ -6388,26 +6623,53 @@ var fetchTool = {
6388
6623
  async execute(input, ctx, opts) {
6389
6624
  let final;
6390
6625
  const executeStream = fetchTool.executeStream;
6391
- if (!executeStream) throw new Error("fetchTool: stream execution unavailable");
6626
+ if (!executeStream) {
6627
+ throw new ToolError({
6628
+ message: "fetchTool: stream execution unavailable",
6629
+ code: "TOOL_EXECUTION_FAILED",
6630
+ toolName: "fetch"
6631
+ });
6632
+ }
6392
6633
  for await (const ev of executeStream(input, ctx, opts)) {
6393
6634
  if (ev.type === "final") final = ev.output;
6394
6635
  }
6395
- if (!final) throw new Error("fetch: stream ended without final event");
6636
+ if (!final) {
6637
+ throw new ToolError({
6638
+ message: "fetch: stream ended without final event",
6639
+ code: "TOOL_EXECUTION_FAILED",
6640
+ toolName: "fetch"
6641
+ });
6642
+ }
6396
6643
  return final;
6397
6644
  },
6398
6645
  async *executeStream(input, ctx, opts) {
6399
- if (!input?.url) throw new Error("fetch: url is required");
6646
+ if (!input?.url) {
6647
+ throw new ToolValidationError({
6648
+ message: "fetch: url is required",
6649
+ field: "url"
6650
+ });
6651
+ }
6400
6652
  const u = new URL(input.url);
6401
6653
  if (u.protocol !== "https:" && u.protocol !== "http:") {
6402
- throw new Error(`fetch: unsupported protocol "${u.protocol}"`);
6654
+ throw new ToolValidationError({
6655
+ message: `fetch: unsupported protocol "${u.protocol}"`,
6656
+ field: "url"
6657
+ });
6403
6658
  }
6404
6659
  if (u.protocol === "http:" && !ALLOW_PRIVATE) {
6405
- throw new Error("fetch: http:// blocked (HTTPS required by default)");
6660
+ throw new ToolValidationError({
6661
+ message: "fetch: http:// blocked (HTTPS required by default)",
6662
+ field: "url"
6663
+ });
6406
6664
  }
6407
6665
  await assertNotPrivate(u.hostname);
6408
6666
  yield { type: "log", text: `GET ${input.url}` };
6409
6667
  const ctrl = new AbortController();
6410
- const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS);
6668
+ const timer = setTimeout(() => ctrl.abort(new ToolError({
6669
+ message: "fetch timeout",
6670
+ code: "TOOL_TIMEOUT",
6671
+ toolName: "fetch"
6672
+ })), TIMEOUT_MS);
6411
6673
  const combined = combineSignals([opts.signal, ctrl.signal]);
6412
6674
  try {
6413
6675
  let res;
@@ -6419,7 +6681,11 @@ var fetchTool = {
6419
6681
  }
6420
6682
  const ct = res.headers.get("content-type") ?? "application/octet-stream";
6421
6683
  if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
6422
- throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
6684
+ throw new FetchError({
6685
+ message: `fetch: refusing to read binary content-type "${ct}"`,
6686
+ status: res.status,
6687
+ context: { url: res.url, contentType: ct }
6688
+ });
6423
6689
  }
6424
6690
  yield {
6425
6691
  type: "log",
@@ -6484,16 +6750,25 @@ async function assertNotPrivate(hostname2) {
6484
6750
  if (ALLOW_PRIVATE) return;
6485
6751
  const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
6486
6752
  if (host === "localhost" || host.endsWith(".localhost")) {
6487
- throw new Error("fetch: blocked localhost target");
6753
+ throw new ToolValidationError({
6754
+ message: "fetch: blocked localhost target",
6755
+ field: "url"
6756
+ });
6488
6757
  }
6489
6758
  const ipVersion = net.isIP(host);
6490
6759
  if (ipVersion === 4) {
6491
6760
  if (isPrivateIPv4(host)) {
6492
- throw new Error(`fetch: blocked private/loopback address "${host}"`);
6761
+ throw new ToolValidationError({
6762
+ message: `fetch: blocked private/loopback address "${host}"`,
6763
+ field: "url"
6764
+ });
6493
6765
  }
6494
6766
  } else if (ipVersion === 6) {
6495
6767
  if (isPrivateIPv6(host)) {
6496
- throw new Error(`fetch: blocked private/loopback address "${host}"`);
6768
+ throw new ToolValidationError({
6769
+ message: `fetch: blocked private/loopback address "${host}"`,
6770
+ field: "url"
6771
+ });
6497
6772
  }
6498
6773
  } else {
6499
6774
  try {
@@ -6501,7 +6776,10 @@ async function assertNotPrivate(hostname2) {
6501
6776
  for (const r of records) {
6502
6777
  const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
6503
6778
  if (bad) {
6504
- throw new Error(`fetch: resolved to private address ${r.address}`);
6779
+ throw new ToolValidationError({
6780
+ message: `fetch: resolved to private address ${r.address}`,
6781
+ field: "url"
6782
+ });
6505
6783
  }
6506
6784
  }
6507
6785
  } catch (err) {
@@ -6511,7 +6789,13 @@ async function assertNotPrivate(hostname2) {
6511
6789
  }
6512
6790
  function describeFetchError(err, url, timedOut) {
6513
6791
  if (timedOut) {
6514
- return new Error(`fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`);
6792
+ return new ToolError({
6793
+ message: `fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`,
6794
+ code: "TOOL_TIMEOUT",
6795
+ toolName: "fetch",
6796
+ context: { url, timedOut: true, timeoutMs: TIMEOUT_MS },
6797
+ cause: err
6798
+ });
6515
6799
  }
6516
6800
  const parts = [];
6517
6801
  const seen = /* @__PURE__ */ new Set();
@@ -6524,7 +6808,15 @@ function describeFetchError(err, url, timedOut) {
6524
6808
  cur = cur.cause;
6525
6809
  }
6526
6810
  const detail = parts.length > 0 ? parts.join(" \u2192 ") : "fetch failed";
6527
- return new Error(`fetch: GET ${url} failed \u2014 ${detail}`);
6811
+ return new FetchError({
6812
+ message: `fetch: GET ${url} failed \u2014 ${detail}`,
6813
+ status: 502,
6814
+ context: { url, timedOut: false, transportErrors: parts },
6815
+ // Preserve the original undici / DNS / TLS chain so callers can inspect
6816
+ // it via `err.cause` and structured `instanceof` checks. The flattened
6817
+ // text version stays in the message for human readability.
6818
+ cause: err
6819
+ });
6528
6820
  }
6529
6821
  function prettyJson(s) {
6530
6822
  try {
@@ -6872,7 +7164,7 @@ function buildArgs(input) {
6872
7164
  }
6873
7165
  }
6874
7166
  function runGit2(args, cwd, signal) {
6875
- return new Promise((resolve6) => {
7167
+ return new Promise((resolve7) => {
6876
7168
  let stdout = "";
6877
7169
  let stderr = "";
6878
7170
  const child = spawn("git", args, {
@@ -6893,7 +7185,7 @@ function runGit2(args, cwd, signal) {
6893
7185
  }
6894
7186
  });
6895
7187
  child.on("error", (err) => {
6896
- resolve6({
7188
+ resolve7({
6897
7189
  command: args[0],
6898
7190
  stdout: normalizeCommandOutput(stdout),
6899
7191
  stderr: err.message,
@@ -6902,7 +7194,7 @@ function runGit2(args, cwd, signal) {
6902
7194
  });
6903
7195
  });
6904
7196
  child.on("close", (code) => {
6905
- resolve6({
7197
+ resolve7({
6906
7198
  command: args[0],
6907
7199
  stdout: normalizeCommandOutput(stdout),
6908
7200
  stderr: normalizeCommandOutput(stderr),
@@ -6964,7 +7256,7 @@ var globTool = {
6964
7256
  },
6965
7257
  async execute(input, ctx) {
6966
7258
  if (!input?.pattern) throw new Error("glob: pattern is required");
6967
- const base = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
7259
+ const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
6968
7260
  const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
6969
7261
  const ignored = await readGitignore(base);
6970
7262
  const re = compileGlob(input.pattern);
@@ -7019,8 +7311,12 @@ var globTool = {
7019
7311
  try {
7020
7312
  const st = await fs2.stat(full);
7021
7313
  if (st.isDirectory()) {
7314
+ const real = await fs2.realpath(full);
7315
+ await assertRealInsideRoot(real, ctx);
7022
7316
  subdirs.push({ full, rel });
7023
7317
  } else if (st.isFile()) {
7318
+ const real = await fs2.realpath(full);
7319
+ await assertRealInsideRoot(real, ctx);
7024
7320
  re.lastIndex = 0;
7025
7321
  const relMatch = re.test(rel);
7026
7322
  re.lastIndex = 0;
@@ -7158,13 +7454,21 @@ var grepTool = {
7158
7454
  return final;
7159
7455
  },
7160
7456
  async *executeStream(input, ctx, opts) {
7161
- if (!input?.pattern) throw new Error("grep: pattern is required");
7457
+ if (!input?.pattern) {
7458
+ throw new ToolValidationError({
7459
+ message: "grep: pattern is required",
7460
+ field: "pattern"
7461
+ });
7462
+ }
7162
7463
  const base = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
7163
7464
  const mode = input.output_mode ?? "content";
7164
7465
  const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
7165
7466
  const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
7166
7467
  if (!validation.ok) {
7167
- throw new Error(`grep: ${validation.reason}`);
7468
+ throw new ToolValidationError({
7469
+ message: `grep: ${validation.reason}`,
7470
+ field: "pattern"
7471
+ });
7168
7472
  }
7169
7473
  const rgAvailable = await detectRg(opts.signal);
7170
7474
  if (rgAvailable) {
@@ -7180,13 +7484,13 @@ var grepTool = {
7180
7484
  }
7181
7485
  };
7182
7486
  async function detectRg(signal) {
7183
- return new Promise((resolve6) => {
7487
+ return new Promise((resolve7) => {
7184
7488
  try {
7185
7489
  const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal, windowsHide: true });
7186
- p.on("error", () => resolve6(false));
7187
- p.on("close", (code) => resolve6(code === 0));
7490
+ p.on("error", () => resolve7(false));
7491
+ p.on("close", (code) => resolve7(code === 0));
7188
7492
  } catch {
7189
- resolve6(false);
7493
+ resolve7(false);
7190
7494
  }
7191
7495
  });
7192
7496
  }
@@ -7320,7 +7624,10 @@ async function runNative(input, base, mode, limit, signal) {
7320
7624
  const flags = input.case_insensitive ? "i" : "";
7321
7625
  const compiled = compileUserRegex(input.pattern, flags);
7322
7626
  if (!compiled.ok) {
7323
- throw new Error(`grep: ${compiled.reason}`);
7627
+ throw new ToolValidationError({
7628
+ message: `grep: ${compiled.reason}`,
7629
+ field: "pattern"
7630
+ });
7324
7631
  }
7325
7632
  const re = compiled.regex;
7326
7633
  const globRe = input.glob ? compileGlob(input.glob) : null;
@@ -7477,6 +7784,10 @@ var installTool = {
7477
7784
  global: {
7478
7785
  type: "boolean",
7479
7786
  description: "Whether to perform a global install (use with caution)."
7787
+ },
7788
+ lifecycleScripts: {
7789
+ type: "boolean",
7790
+ description: "Opt in to running package lifecycle scripts (preinstall / install / postinstall / prepare / \u2026). Default: false \u2014 installs pass --ignore-scripts so a malicious package cannot execute arbitrary code at install time. Set true to opt back in to the legacy npm/pnpm/yarn default."
7480
7791
  }
7481
7792
  }
7482
7793
  },
@@ -7496,8 +7807,10 @@ var installTool = {
7496
7807
  yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
7497
7808
  const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
7498
7809
  const globalFlag = input.global ? ["-g"] : [];
7810
+ const ignoreScripts = input.lifecycleScripts !== true;
7499
7811
  const args = [];
7500
7812
  if (input.dry_run) args.push("--dry-run");
7813
+ if (ignoreScripts) args.push("--ignore-scripts");
7501
7814
  if (pkgManager === "pnpm") {
7502
7815
  if (save) args.push(save);
7503
7816
  args.push("add", ...globalFlag);
@@ -7592,8 +7905,8 @@ function resolveManifestPath(cwd, pkgManager) {
7592
7905
  var jsonTool = {
7593
7906
  name: "json",
7594
7907
  category: "Data",
7595
- description: "Parse, pretty-print, query, and convert between JSON, JSON5, and YAML. Supports simple path-based queries.",
7596
- usageHint: "VERY USEFUL FOR DATA INSPECTION:\n\n- Use on package.json, tsconfig, config files, or any structured data.\n- `query` lets you extract specific values without reading the whole file.\n- Great for validating that a file has the expected structure.\nPrefer this over raw `read` + manual parsing when dealing with configuration or data files.",
7908
+ description: "Parse, pretty-print, query, validate, transform, and merge JSON/JSON5/YAML. Use `action` to select the operation: parse (default), query, validate, transform, or merge.",
7909
+ usageHint: 'VERY USEFUL FOR DATA INSPECTION:\n\n- `action: "parse"` (default): read/pretty-print/convert JSON, JSON5, or YAML from `file` or `data`.\n- `action: "query"`: JMESPath-like query (`a.b[0].c`, `items[*].name`, filters, functions).\n- `action: "validate"`: validate data against a JSON Schema (`schema` param).\n- `action: "transform"`: chain multiple JMESPath transforms (`transforms` param).\n- `action: "merge"`: deep merge `base` and `patch` objects (`conflictResolution` param).\nPrefer this over raw `read` + manual parsing when dealing with configuration or data files.',
7597
7910
  permission: "auto",
7598
7911
  mutating: false,
7599
7912
  timeoutMs: 5e3,
@@ -7602,69 +7915,420 @@ var jsonTool = {
7602
7915
  inputSchema: {
7603
7916
  type: "object",
7604
7917
  properties: {
7605
- file: { type: "string", description: "Path to JSON/JSON5/YAML file" },
7606
- data: { type: "string", description: "JSON/JSON5/YAML string (alternative to file)" },
7607
- query: {
7918
+ action: {
7608
7919
  type: "string",
7609
- description: 'JMESPath-like query (e.g. "a.b[0].c" or "a[*].name")'
7920
+ enum: ["parse", "query", "validate", "transform", "merge"],
7921
+ description: "Operation (default: parse). parse=read/pretty-print, query=JMESPath, validate=schema, transform=chained queries, merge=deep merge."
7610
7922
  },
7923
+ file: { type: "string", description: "Path to JSON/JSON5/YAML file (parse/query/validate)" },
7924
+ data: { type: "string", description: "JSON/JSON5/YAML string (parse/query/validate, alternative to file)" },
7611
7925
  format: {
7612
7926
  type: "string",
7613
7927
  enum: ["json", "json5", "yaml"],
7614
- description: "Output format (default: json)"
7928
+ description: "Output format for parse/query/transform (default: json)"
7929
+ },
7930
+ query: {
7931
+ type: "string",
7932
+ description: "JMESPath-like query expression (query action)"
7933
+ },
7934
+ transforms: {
7935
+ type: "array",
7936
+ items: { type: "string" },
7937
+ description: "Ordered JMESPath query strings (transform action)"
7938
+ },
7939
+ schema: {
7940
+ type: "object",
7941
+ description: "JSON Schema to validate against (validate action)"
7942
+ },
7943
+ base: { description: "Base JSON object (merge action)" },
7944
+ patch: { description: "Patch JSON object to merge in (merge action)" },
7945
+ conflictResolution: {
7946
+ type: "string",
7947
+ enum: ["prefer-base", "prefer-patch"],
7948
+ description: "Merge conflict resolution (default: prefer-patch)"
7615
7949
  },
7616
7950
  validate: {
7617
7951
  type: "boolean",
7618
- description: "Validate syntax only, no output (default: false)"
7952
+ description: "Validate syntax only, no output (parse action, default: false)"
7619
7953
  }
7620
7954
  }
7621
7955
  },
7622
- async execute(input) {
7623
- const format = input.format ?? "json";
7624
- let parsed;
7625
- let raw;
7626
- if (input.file) {
7627
- try {
7628
- raw = await fs2.readFile(input.file, "utf8");
7629
- } catch {
7630
- return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
7631
- }
7632
- } else if (input.data) {
7633
- raw = input.data;
7634
- } else {
7635
- return { data: null, formatted: "", type: "unknown", error: "Provide file or data" };
7956
+ async execute(input, ctx) {
7957
+ const action = input.action ?? "parse";
7958
+ switch (action) {
7959
+ case "query":
7960
+ return executeQuery(input, ctx);
7961
+ case "validate":
7962
+ return executeValidate(input, ctx);
7963
+ case "transform":
7964
+ return executeTransform(input, ctx);
7965
+ case "merge":
7966
+ return executeMerge(input);
7967
+ case "parse":
7968
+ default:
7969
+ return executeParse(input, ctx);
7636
7970
  }
7971
+ }
7972
+ };
7973
+ async function executeParse(input, ctx) {
7974
+ const format = input.format ?? "json";
7975
+ let parsed;
7976
+ let raw;
7977
+ if (input.file) {
7637
7978
  try {
7638
- parsed = JSON.parse(raw);
7639
- } catch (e) {
7640
- return {
7641
- data: null,
7642
- formatted: "",
7643
- type: "unknown",
7644
- /* v8 ignore next -- JSON.parse only throws SyntaxError (an Error); the String(e) side is defensive. */
7645
- error: `Parse failed: ${e instanceof Error ? e.message : String(e)}`
7646
- };
7647
- }
7648
- if (input.validate) {
7649
- return {
7650
- data: parsed,
7651
- formatted: "valid",
7652
- type: Array.isArray(parsed) ? "array" : typeof parsed,
7653
- keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
7654
- };
7979
+ raw = await fs2.readFile(await safeResolveReal(input.file, ctx), "utf8");
7980
+ } catch {
7981
+ return { data: null, formatted: "", type: "unknown", action: "parse", error: "Could not read file" };
7655
7982
  }
7656
- const queryResult = input.query ? query(parsed, input.query) : void 0;
7657
- const formatted = formatOutput(queryResult ?? parsed, format);
7983
+ } else if (input.data) {
7984
+ raw = input.data;
7985
+ } else {
7986
+ return { data: null, formatted: "", type: "unknown", action: "parse", error: "Provide file or data" };
7987
+ }
7988
+ try {
7989
+ parsed = JSON.parse(raw);
7990
+ } catch (e) {
7991
+ return {
7992
+ data: null,
7993
+ formatted: "",
7994
+ type: "unknown",
7995
+ action: "parse",
7996
+ /* v8 ignore next -- JSON.parse only throws SyntaxError (an Error); the String(e) side is defensive. */
7997
+ error: `Parse failed: ${e instanceof Error ? e.message : String(e)}`
7998
+ };
7999
+ }
8000
+ if (input.validate) {
7658
8001
  return {
7659
8002
  data: parsed,
7660
- formatted,
8003
+ formatted: "valid",
7661
8004
  type: Array.isArray(parsed) ? "array" : typeof parsed,
8005
+ action: "parse",
8006
+ keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
8007
+ };
8008
+ }
8009
+ if (input.query) {
8010
+ const queryResult = simpleQuery(parsed, input.query);
8011
+ const formatted2 = formatOutput(queryResult, format);
8012
+ return {
8013
+ data: parsed,
8014
+ formatted: formatted2,
8015
+ type: Array.isArray(parsed) ? "array" : typeof parsed,
8016
+ action: "parse",
7662
8017
  keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0,
7663
8018
  query_result: queryResult
7664
8019
  };
7665
8020
  }
7666
- };
7667
- function query(data, path22) {
8021
+ const formatted = formatOutput(parsed, format);
8022
+ return {
8023
+ data: parsed,
8024
+ formatted,
8025
+ type: Array.isArray(parsed) ? "array" : typeof parsed,
8026
+ action: "parse",
8027
+ keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
8028
+ };
8029
+ }
8030
+ async function executeQuery(input, ctx) {
8031
+ if (!input.query) {
8032
+ return { data: null, formatted: "", type: "unknown", action: "query", error: "query is required for action: query" };
8033
+ }
8034
+ let parsed;
8035
+ if (input.file) {
8036
+ try {
8037
+ const raw = await fs2.readFile(await safeResolveReal(input.file, ctx), "utf8");
8038
+ parsed = JSON.parse(raw);
8039
+ } catch {
8040
+ return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not read/parse file" };
8041
+ }
8042
+ } else if (input.data) {
8043
+ try {
8044
+ parsed = JSON.parse(input.data);
8045
+ } catch {
8046
+ return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not parse data string" };
8047
+ }
8048
+ } else {
8049
+ return { data: null, formatted: "", type: "unknown", action: "query", error: "Provide file or data" };
8050
+ }
8051
+ try {
8052
+ const result = jmespathSearch(parsed, input.query);
8053
+ const format = input.format ?? "json";
8054
+ return {
8055
+ data: parsed,
8056
+ formatted: formatOutput(result, format),
8057
+ type: result === null ? "null" : Array.isArray(result) ? "array" : typeof result,
8058
+ action: "query",
8059
+ query_result: result
8060
+ };
8061
+ } catch (e) {
8062
+ return {
8063
+ data: null,
8064
+ formatted: "",
8065
+ type: "unknown",
8066
+ action: "query",
8067
+ /* v8 ignore next -- defensive String(e) */
8068
+ error: `Query failed: ${e instanceof Error ? e.message : String(e)}`
8069
+ };
8070
+ }
8071
+ }
8072
+ async function executeValidate(input, ctx) {
8073
+ if (!input.schema) {
8074
+ return { data: null, formatted: "", type: "unknown", action: "validate", error: "schema is required for action: validate" };
8075
+ }
8076
+ let parsed;
8077
+ if (input.file) {
8078
+ try {
8079
+ const raw = await fs2.readFile(await safeResolveReal(input.file, ctx), "utf8");
8080
+ parsed = JSON.parse(raw);
8081
+ } catch {
8082
+ return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not read/parse file" };
8083
+ }
8084
+ } else if (input.data) {
8085
+ try {
8086
+ parsed = JSON.parse(input.data);
8087
+ } catch {
8088
+ return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not parse data string" };
8089
+ }
8090
+ } else {
8091
+ return { data: null, formatted: "", type: "unknown", action: "validate", error: "Provide file or data" };
8092
+ }
8093
+ try {
8094
+ const { valid, errors } = validateJsonSchema(parsed, input.schema);
8095
+ return {
8096
+ data: parsed,
8097
+ formatted: valid ? "valid" : "invalid",
8098
+ type: Array.isArray(parsed) ? "array" : typeof parsed,
8099
+ action: "validate",
8100
+ valid,
8101
+ errors
8102
+ };
8103
+ } catch (e) {
8104
+ return {
8105
+ data: null,
8106
+ formatted: "",
8107
+ type: "unknown",
8108
+ action: "validate",
8109
+ /* v8 ignore next -- defensive String(e) */
8110
+ error: `Validation failed: ${e instanceof Error ? e.message : String(e)}`
8111
+ };
8112
+ }
8113
+ }
8114
+ async function executeTransform(input, ctx) {
8115
+ if (!input.transforms || input.transforms.length === 0) {
8116
+ return { data: null, formatted: "", type: "unknown", action: "transform", error: "transforms array is required for action: transform" };
8117
+ }
8118
+ let parsed;
8119
+ if (input.file) {
8120
+ try {
8121
+ const raw = await fs2.readFile(await safeResolveReal(input.file, ctx), "utf8");
8122
+ parsed = JSON.parse(raw);
8123
+ } catch {
8124
+ return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not read/parse file" };
8125
+ }
8126
+ } else if (input.data) {
8127
+ try {
8128
+ parsed = JSON.parse(input.data);
8129
+ } catch {
8130
+ return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not parse data string" };
8131
+ }
8132
+ } else {
8133
+ return { data: null, formatted: "", type: "unknown", action: "transform", error: "Provide file or data" };
8134
+ }
8135
+ try {
8136
+ let current = parsed;
8137
+ const steps = [];
8138
+ for (const t of input.transforms) {
8139
+ current = jmespathSearch(current, t);
8140
+ steps.push({ transform: t, result: current });
8141
+ }
8142
+ const format = input.format ?? "json";
8143
+ return {
8144
+ data: parsed,
8145
+ formatted: formatOutput(current, format),
8146
+ type: current === null ? "null" : Array.isArray(current) ? "array" : typeof current,
8147
+ action: "transform",
8148
+ result: current,
8149
+ steps
8150
+ };
8151
+ } catch (e) {
8152
+ return {
8153
+ data: null,
8154
+ formatted: "",
8155
+ type: "unknown",
8156
+ action: "transform",
8157
+ /* v8 ignore next -- defensive String(e) */
8158
+ error: `Transform failed: ${e instanceof Error ? e.message : String(e)}`
8159
+ };
8160
+ }
8161
+ }
8162
+ async function executeMerge(input) {
8163
+ if (input.base === void 0 || input.patch === void 0) {
8164
+ return { data: null, formatted: "", type: "unknown", action: "merge", error: "base and patch are required for action: merge" };
8165
+ }
8166
+ const conflictResolution = input.conflictResolution ?? "prefer-patch";
8167
+ try {
8168
+ const result = deepMerge(input.base, input.patch, { conflictResolution });
8169
+ const format = input.format ?? "json";
8170
+ return {
8171
+ data: result,
8172
+ formatted: formatOutput(result, format),
8173
+ type: result === null ? "null" : Array.isArray(result) ? "array" : typeof result,
8174
+ action: "merge",
8175
+ result
8176
+ };
8177
+ } catch (e) {
8178
+ return {
8179
+ data: null,
8180
+ formatted: "",
8181
+ type: "unknown",
8182
+ action: "merge",
8183
+ /* v8 ignore next -- defensive String(e) */
8184
+ error: `Merge failed: ${e instanceof Error ? e.message : String(e)}`
8185
+ };
8186
+ }
8187
+ }
8188
+ function jmespathSearch(data, query) {
8189
+ if (!query || query === "@") return data;
8190
+ if (query === "$") return data;
8191
+ const dotMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(?:\.(.+))?$/);
8192
+ if (dotMatch) {
8193
+ const key = dotMatch[1];
8194
+ const rest = dotMatch[2];
8195
+ const val = data?.[key];
8196
+ if (rest === void 0) return val;
8197
+ return jmespathSearch(val, rest);
8198
+ }
8199
+ const arrMatch = query.match(/^\[(\d+)\](?:\.(.+))?$/);
8200
+ if (arrMatch) {
8201
+ const idx = Number.parseInt(arrMatch[1], 10);
8202
+ const rest = arrMatch[2];
8203
+ const arr = data;
8204
+ const val = arr?.[idx];
8205
+ if (rest === void 0) return val;
8206
+ return jmespathSearch(val, rest);
8207
+ }
8208
+ if (query === "[*]") {
8209
+ if (Array.isArray(data)) {
8210
+ return data;
8211
+ }
8212
+ return data;
8213
+ }
8214
+ const multiMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\[\*\](?:\.(.+))?$/);
8215
+ if (multiMatch) {
8216
+ const key = multiMatch[1];
8217
+ const rest = multiMatch[2];
8218
+ const arr = data?.[key];
8219
+ if (!Array.isArray(arr)) return [];
8220
+ if (rest === void 0) return arr;
8221
+ return arr.map((item) => jmespathSearch(item, rest));
8222
+ }
8223
+ const filterMatch = query.match(/^\[\\?([a-zA-Z_][a-zA-Z0-9_]*)(==|!=|<|>|<=|>=)(`[^`]+`|'[^']*')\](?:\.(.+))?$/);
8224
+ if (filterMatch) {
8225
+ const field = filterMatch[1];
8226
+ const op = filterMatch[2];
8227
+ const rawVal = filterMatch[3];
8228
+ const rest = filterMatch[4];
8229
+ const cmpVal = JSON.parse(rawVal.slice(1, -1));
8230
+ const arr = data;
8231
+ if (!Array.isArray(arr)) return [];
8232
+ const filtered = arr.filter((item) => {
8233
+ const itemVal = item[field];
8234
+ switch (op) {
8235
+ case "==":
8236
+ return itemVal === cmpVal;
8237
+ case "!=":
8238
+ return itemVal !== cmpVal;
8239
+ case ">":
8240
+ return Number(itemVal) > Number(cmpVal);
8241
+ case "<":
8242
+ return Number(itemVal) < Number(cmpVal);
8243
+ case ">=":
8244
+ return Number(itemVal) >= Number(cmpVal);
8245
+ case "<=":
8246
+ return Number(itemVal) <= Number(cmpVal);
8247
+ /* v8 ignore next -- op is constrained to the six operators by the filter regex; default is unreachable. */
8248
+ default:
8249
+ return true;
8250
+ }
8251
+ });
8252
+ if (rest === void 0) return filtered;
8253
+ return filtered.map((item) => jmespathSearch(item, rest));
8254
+ }
8255
+ const fnMatch = query.match(/^(length|keys|values|type)\(@\)$/);
8256
+ if (fnMatch) {
8257
+ const fn = fnMatch[1];
8258
+ switch (fn) {
8259
+ case "length":
8260
+ if (Array.isArray(data)) return data.length;
8261
+ if (typeof data === "string") return data.length;
8262
+ if (typeof data === "object" && data !== null) return Object.keys(data).length;
8263
+ return 0;
8264
+ case "keys":
8265
+ if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.keys(data);
8266
+ return [];
8267
+ case "values":
8268
+ if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.values(data);
8269
+ return [];
8270
+ case "type":
8271
+ if (data === null) return "null";
8272
+ if (Array.isArray(data)) return "array";
8273
+ return typeof data;
8274
+ /* v8 ignore next 2 -- fn is constrained to the four names by the function regex; default is unreachable. */
8275
+ default:
8276
+ return null;
8277
+ }
8278
+ }
8279
+ return null;
8280
+ }
8281
+ function validateJsonSchema(data, schema) {
8282
+ const errors = [];
8283
+ function check(value, s, path22) {
8284
+ if (s["type"]) {
8285
+ const expectedType = s["type"];
8286
+ const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
8287
+ if (expectedType === "integer") {
8288
+ if (!Number.isInteger(value)) errors.push(`${path22}: expected integer, got ${actualType}`);
8289
+ } else if (expectedType !== actualType) {
8290
+ errors.push(`${path22}: expected ${expectedType}, got ${actualType}`);
8291
+ }
8292
+ }
8293
+ if (typeof value === "string" && s["format"] === "uri" && value) {
8294
+ try {
8295
+ new URL(value);
8296
+ } catch {
8297
+ errors.push(`${path22}: not a valid URI`);
8298
+ }
8299
+ }
8300
+ if (typeof value === "string" && s["pattern"]) {
8301
+ const re = new RegExp(s["pattern"]);
8302
+ if (!re.test(value)) errors.push(`${path22}: does not match pattern ${s["pattern"]}`);
8303
+ }
8304
+ if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
8305
+ errors.push(`${path22}: string too short (min ${s["minLength"]})`);
8306
+ }
8307
+ if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
8308
+ errors.push(`${path22}: string too long (max ${s["maxLength"]})`);
8309
+ }
8310
+ if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
8311
+ errors.push(`${path22}: below minimum ${s["minimum"]}`);
8312
+ }
8313
+ if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
8314
+ errors.push(`${path22}: above maximum ${s["maximum"]}`);
8315
+ }
8316
+ if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
8317
+ for (let i = 0; i < value.length; i++) {
8318
+ check(value[i], s["items"], `${path22}[${i}]`);
8319
+ }
8320
+ }
8321
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
8322
+ const props = s["properties"];
8323
+ for (const [k, propSchema] of Object.entries(props)) {
8324
+ check(value[k], propSchema, `${path22}.${k}`);
8325
+ }
8326
+ }
8327
+ }
8328
+ check(data, schema, "$");
8329
+ return { valid: errors.length === 0, errors };
8330
+ }
8331
+ function simpleQuery(data, path22) {
7668
8332
  const parts = path22.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
7669
8333
  let current = data;
7670
8334
  for (const part of parts) {
@@ -7891,7 +8555,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
7891
8555
  };
7892
8556
  }
7893
8557
  args.push("--timestamps", service);
7894
- return new Promise((resolve6) => {
8558
+ return new Promise((resolve7) => {
7895
8559
  let stdout = "";
7896
8560
  let stderr = "";
7897
8561
  const MAX = 2e5;
@@ -7907,7 +8571,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
7907
8571
  if (settled) return;
7908
8572
  settled = true;
7909
8573
  clearTimeout(timer);
7910
- resolve6(result);
8574
+ resolve7(result);
7911
8575
  };
7912
8576
  const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
7913
8577
  const timer = setTimeout(() => {
@@ -8027,8 +8691,8 @@ var outdatedTool = {
8027
8691
  // read-only, but `outdated` makes outbound HTTP calls to the
8028
8692
  // registry. The 'confirm' permission routes the call through the
8029
8693
  // tool.confirm_needed flow on every invocation. M-1 originally
8030
- // fixed four sibling tools (mcp_control, shellcheck, shellcheck_scan,
8031
- // web_search) but missed this one; applying the same contract here.
8694
+ // fixed four sibling tools (mcp_control, shellcheck, shellcheck (scan mode),
8695
+ // search) but missed this one; applying the same contract here.
8032
8696
  mutating: true,
8033
8697
  // Capability is outbound network — the tool only hits the package
8034
8698
  // registry over HTTP, never touches the filesystem or runs shell.
@@ -8069,7 +8733,7 @@ var outdatedTool = {
8069
8733
  }
8070
8734
  };
8071
8735
  function runOutdated(manager, args, cwd, signal) {
8072
- return new Promise((resolve6) => {
8736
+ return new Promise((resolve7) => {
8073
8737
  let stdout = "";
8074
8738
  let stderr = "";
8075
8739
  const MAX = 1e5;
@@ -8086,10 +8750,10 @@ function runOutdated(manager, args, cwd, signal) {
8086
8750
  });
8087
8751
  child.on("close", (code) => {
8088
8752
  const result = parseOutdatedOutput(stdout, code ?? 0);
8089
- resolve6(result);
8753
+ resolve7(result);
8090
8754
  });
8091
8755
  child.on("error", (e) => {
8092
- resolve6({
8756
+ resolve7({
8093
8757
  exit_code: 1,
8094
8758
  packages: [],
8095
8759
  total: 0,
@@ -8223,7 +8887,7 @@ function stripPathComponents(p, strip) {
8223
8887
  return parts.slice(strip).join("/");
8224
8888
  }
8225
8889
  function runPatch(args, cwd, signal) {
8226
- return new Promise((resolve6) => {
8890
+ return new Promise((resolve7) => {
8227
8891
  let stdout = "";
8228
8892
  let stderr = "";
8229
8893
  const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
@@ -8234,8 +8898,8 @@ function runPatch(args, cwd, signal) {
8234
8898
  child.stderr?.on("data", (c) => {
8235
8899
  stderr += c.toString();
8236
8900
  });
8237
- child.on("close", (code) => resolve6({ exitCode: code ?? 1, stdout, stderr }));
8238
- child.on("error", (e) => resolve6({ exitCode: 1, stdout: "", stderr: e.message }));
8901
+ child.on("close", (code) => resolve7({ exitCode: code ?? 1, stdout, stderr }));
8902
+ child.on("error", (e) => resolve7({ exitCode: 1, stdout: "", stderr: e.message }));
8239
8903
  });
8240
8904
  }
8241
8905
  function extractPatchedFiles(output) {
@@ -8542,19 +9206,49 @@ var readTool = {
8542
9206
  required: ["path"]
8543
9207
  },
8544
9208
  async execute(input, ctx) {
8545
- if (!input?.path) throw new Error("read: path is required");
9209
+ if (!input?.path) {
9210
+ throw new ToolValidationError({
9211
+ message: "read: path is required",
9212
+ field: "path"
9213
+ });
9214
+ }
8546
9215
  const absPath = await safeResolveReal(input.path, ctx);
8547
9216
  let stat11;
8548
9217
  try {
8549
9218
  stat11 = await fs2.stat(absPath);
8550
9219
  } catch (err) {
8551
9220
  const code = err.code;
8552
- if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
8553
- throw new Error(`read: failed to stat "${input.path}": ${toErrorMessage$2(err)}`);
9221
+ if (code === "ENOENT") {
9222
+ throw new FsError({
9223
+ message: `read: file not found "${input.path}"`,
9224
+ code: "FS_READ_FAILED",
9225
+ path: absPath,
9226
+ context: { errno: "ENOENT" }
9227
+ });
9228
+ }
9229
+ throw new FsError({
9230
+ message: `read: failed to stat "${input.path}": ${toErrorMessage$2(err)}`,
9231
+ code: "FS_READ_FAILED",
9232
+ path: absPath,
9233
+ context: { errno: code },
9234
+ cause: err
9235
+ });
9236
+ }
9237
+ if (!stat11.isFile()) {
9238
+ throw new FsError({
9239
+ message: `read: "${input.path}" is not a regular file`,
9240
+ code: "FS_READ_FAILED",
9241
+ path: absPath,
9242
+ context: { reason: "not-a-regular-file" }
9243
+ });
8554
9244
  }
8555
- if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
8556
9245
  if (stat11.size > MAX_BYTES2) {
8557
- throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES2})`);
9246
+ throw new FsError({
9247
+ message: `read: file too large (${stat11.size} bytes, limit ${MAX_BYTES2})`,
9248
+ code: "FS_READ_FAILED",
9249
+ path: absPath,
9250
+ context: { size: stat11.size, limit: MAX_BYTES2, reason: "too-large" }
9251
+ });
8558
9252
  }
8559
9253
  const offset = Math.max(1, input.offset ?? 1);
8560
9254
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
@@ -8678,8 +9372,8 @@ var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "covera
8678
9372
  var replaceTool = {
8679
9373
  name: "replace",
8680
9374
  category: "Transform",
8681
- description: "Perform a search-and-replace across multiple files using a regex pattern. This is a powerful bulk transformation tool. Always use `dry_run: true` first on anything non-trivial.",
8682
- usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Start with `dry_run: true` to see exactly what would change.\n2. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n3. `replace_all` controls whether only the first match per file or all matches are replaced.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
9375
+ description: "Perform a search-and-replace across multiple files using a regex pattern. This is a powerful bulk transformation tool. Dry-run is ON by default \u2014 set `dry_run: false` to apply changes.",
9376
+ usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Run without `dry_run: false` first to see exactly what would change (dry-run is the default).\n2. Review the diff output, then re-run with `dry_run: false` to apply.\n3. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n4. `replace_all` controls whether only the first match per file or all matches are replaced.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
8683
9377
  permission: "confirm",
8684
9378
  mutating: true,
8685
9379
  capabilities: ["fs.write"],
@@ -8699,22 +9393,40 @@ var replaceTool = {
8699
9393
  type: "boolean",
8700
9394
  description: "Replace all occurrences in each file (default: true)"
8701
9395
  },
8702
- dry_run: { type: "boolean", description: "Preview changes without writing" }
9396
+ dry_run: { type: "boolean", description: "Preview changes without writing (default: true)" }
8703
9397
  },
8704
9398
  required: ["pattern", "replacement", "files"]
8705
9399
  },
8706
9400
  async execute(input, ctx) {
8707
- if (!input?.pattern) throw new Error("replace: pattern is required");
8708
- if (input.replacement === void 0) throw new Error("replace: replacement is required");
8709
- if (!input?.files) throw new Error("replace: files is required");
9401
+ if (!input?.pattern) {
9402
+ throw new ToolValidationError({
9403
+ message: "replace: pattern is required",
9404
+ field: "pattern"
9405
+ });
9406
+ }
9407
+ if (input.replacement === void 0) {
9408
+ throw new ToolValidationError({
9409
+ message: "replace: replacement is required",
9410
+ field: "replacement"
9411
+ });
9412
+ }
9413
+ if (!input?.files) {
9414
+ throw new ToolValidationError({
9415
+ message: "replace: files is required",
9416
+ field: "files"
9417
+ });
9418
+ }
8710
9419
  const replaceAll = input.replace_all ?? true;
8711
9420
  const compiled = compileUserRegex(input.pattern, "g");
8712
9421
  if (!compiled.ok) {
8713
- throw new Error(`replace: ${compiled.reason}`);
9422
+ throw new ToolValidationError({
9423
+ message: `replace: ${compiled.reason}`,
9424
+ field: "pattern"
9425
+ });
8714
9426
  }
8715
9427
  const re = compiled.regex;
8716
9428
  const globRe = input.glob ? compileGlob(input.glob) : null;
8717
- const dryRun = input.dry_run ?? false;
9429
+ const dryRun = input.dry_run ?? true;
8718
9430
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
8719
9431
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
8720
9432
  const realRoot = await fs2.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
@@ -8810,13 +9522,13 @@ async function globFiles(pattern, base, extraGlob) {
8810
9522
  return await globNative(pattern, base, extraGlob);
8811
9523
  }
8812
9524
  function checkRg() {
8813
- return new Promise((resolve6) => {
9525
+ return new Promise((resolve7) => {
8814
9526
  try {
8815
9527
  const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
8816
- p.on("error", () => resolve6(false));
8817
- p.on("close", (code) => resolve6(code === 0));
9528
+ p.on("error", () => resolve7(false));
9529
+ p.on("close", (code) => resolve7(code === 0));
8818
9530
  } catch {
8819
- resolve6(false);
9531
+ resolve7(false);
8820
9532
  }
8821
9533
  });
8822
9534
  }
@@ -8833,10 +9545,10 @@ function spawnRgFind(pattern, base) {
8833
9545
  buf += chunk.toString();
8834
9546
  });
8835
9547
  return {
8836
- promise: new Promise((resolve6, reject) => {
9548
+ promise: new Promise((resolve7, reject) => {
8837
9549
  child.on("error", reject);
8838
9550
  child.on("close", () => {
8839
- resolve6(buf.split("\n").filter(Boolean));
9551
+ resolve7(buf.split("\n").filter(Boolean));
8840
9552
  });
8841
9553
  })
8842
9554
  };
@@ -9063,11 +9775,13 @@ function substituteVars(content, name, vars) {
9063
9775
  var DEFAULT_NUM = 10;
9064
9776
  var MAX_RESULTS = 50;
9065
9777
  var TIMEOUT_MS3 = 15e3;
9778
+ var CACHE_TTL_MS = 3e5;
9779
+ var cache = /* @__PURE__ */ new Map();
9066
9780
  var searchTool = {
9067
9781
  name: "search",
9068
9782
  category: "Search",
9069
- description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase.",
9070
- usageHint: "Good for: API documentation, error messages, library usage examples, current best practices.\n\n- Prefer specific queries over very broad ones.\n- Results go through the guarded fetch system (same protections as the `fetch` tool).\n- This is often better than the model trying to recall outdated knowledge.",
9783
+ description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL.",
9784
+ usageHint: "Good for: API documentation, error messages, library usage examples, current best practices.\n\n- Prefer specific queries over very broad ones.\n- Results go through the guarded fetch system (same protections as the `fetch` tool).\n- Supports duckduckgo (default), google, and bing sources.\n- Set `skip_cache: true` to force a fresh search.\n- This is often better than the model trying to recall outdated knowledge.",
9071
9785
  permission: "confirm",
9072
9786
  mutating: false,
9073
9787
  capabilities: ["net.outbound"],
@@ -9087,6 +9801,10 @@ var searchTool = {
9087
9801
  type: "string",
9088
9802
  enum: ["duckduckgo", "google", "bing"],
9089
9803
  description: "Search engine to use (default: duckduckgo)"
9804
+ },
9805
+ skip_cache: {
9806
+ type: "boolean",
9807
+ description: "Skip the in-memory cache and force a fresh search (default: false)"
9090
9808
  }
9091
9809
  },
9092
9810
  required: ["query"]
@@ -9102,57 +9820,135 @@ var searchTool = {
9102
9820
  return final;
9103
9821
  },
9104
9822
  async *executeStream(input, _ctx, opts) {
9105
- if (!input?.query) throw new Error("search: query is required");
9823
+ if (!input?.query || input.query.trim() === "") {
9824
+ throw new ToolValidationError({
9825
+ message: "search: query is required and must be a non-empty string",
9826
+ field: "query"
9827
+ });
9828
+ }
9106
9829
  const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));
9107
9830
  const source = input.source ?? "duckduckgo";
9831
+ const skipCache = input.skip_cache ?? false;
9832
+ const cacheKey = `${source}:${input.query}`;
9833
+ if (!skipCache) {
9834
+ const entry = cache.get(cacheKey);
9835
+ if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {
9836
+ const results = entry.results.map((r) => ({
9837
+ title: r.title,
9838
+ url: r.url,
9839
+ snippet: r.snippet
9840
+ }));
9841
+ yield {
9842
+ type: "log",
9843
+ text: `Cache hit for "${input.query}" (${source})`,
9844
+ data: { source, query: input.query, cached: true }
9845
+ };
9846
+ yield {
9847
+ type: "partial_output",
9848
+ text: `${results.length} cached results from ${source}`,
9849
+ data: { count: results.length, cached: true }
9850
+ };
9851
+ yield {
9852
+ type: "final",
9853
+ output: {
9854
+ query: input.query,
9855
+ results: results.slice(0, num),
9856
+ source,
9857
+ truncated: results.length >= num,
9858
+ cached: true
9859
+ }
9860
+ };
9861
+ return;
9862
+ }
9863
+ }
9108
9864
  yield {
9109
9865
  type: "log",
9110
9866
  text: `Querying ${source} for "${input.query}"\u2026`,
9111
- data: { source, query: input.query }
9867
+ data: { source, query: input.query, cached: false }
9112
9868
  };
9113
- let output;
9869
+ let rawResults;
9114
9870
  switch (source) {
9115
9871
  case "duckduckgo":
9116
- output = await duckduckgoSearch(input.query, num, opts.signal);
9872
+ rawResults = await duckduckgoSearch(input.query, num, opts.signal);
9117
9873
  break;
9118
9874
  case "google":
9119
- output = await googleSearch(input.query, num, opts.signal);
9875
+ rawResults = await googleSearch(input.query, num, opts.signal);
9120
9876
  break;
9121
9877
  case "bing":
9122
- output = await bingSearch(input.query, num, opts.signal);
9878
+ rawResults = await bingSearch(input.query, num, opts.signal);
9123
9879
  break;
9124
9880
  default:
9125
- throw new Error(`search: unknown source "${source}"`);
9881
+ throw new ToolValidationError({
9882
+ message: `search: unknown source "${source}"`,
9883
+ field: "source"
9884
+ });
9126
9885
  }
9886
+ const seenUrls = /* @__PURE__ */ new Set();
9887
+ const deduped = [];
9888
+ for (const r of rawResults) {
9889
+ const noQuery = r.url.split("?")[0] ?? r.url;
9890
+ const normalized = noQuery.split("#")[0] ?? r.url;
9891
+ if (!seenUrls.has(normalized) && r.url.startsWith("http")) {
9892
+ seenUrls.add(normalized);
9893
+ deduped.push(r);
9894
+ }
9895
+ }
9896
+ const ranked = scoreResults(deduped, input.query);
9897
+ const finalResults = ranked.slice(0, num);
9898
+ cache.set(cacheKey, { results: ranked, timestamp: Date.now() });
9899
+ pruneStaleCacheEntries();
9127
9900
  yield {
9128
9901
  type: "partial_output",
9129
- text: `${output.results.length} results from ${output.source}`,
9130
- data: { count: output.results.length }
9902
+ text: `${finalResults.length} results from ${source}`,
9903
+ data: { count: finalResults.length, cached: false }
9904
+ };
9905
+ yield {
9906
+ type: "final",
9907
+ output: {
9908
+ query: input.query,
9909
+ results: finalResults.map((r) => ({
9910
+ title: r.title,
9911
+ url: r.url,
9912
+ snippet: r.snippet
9913
+ })),
9914
+ source,
9915
+ truncated: finalResults.length >= num,
9916
+ cached: false
9917
+ }
9131
9918
  };
9132
- yield { type: "final", output };
9133
9919
  }
9134
9920
  };
9135
- async function duckduckgoSearch(query2, num, signal) {
9136
- const encoded = encodeURIComponent(query2);
9921
+ function pruneStaleCacheEntries() {
9922
+ const cutoff = Date.now() - CACHE_TTL_MS * 2;
9923
+ for (const [key, entry] of cache.entries()) {
9924
+ if (entry.timestamp < cutoff) cache.delete(key);
9925
+ }
9926
+ }
9927
+ function scoreResults(results, query) {
9928
+ const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
9929
+ return results.map((r) => {
9930
+ const titleLower = r.title.toLowerCase();
9931
+ const snippetLower = r.snippet.toLowerCase();
9932
+ let score = r.score;
9933
+ for (const term of terms) {
9934
+ if (titleLower.includes(term)) score += 2;
9935
+ if (snippetLower.includes(term)) score += 1;
9936
+ }
9937
+ return { ...r, score };
9938
+ }).sort((a, b) => b.score - a.score);
9939
+ }
9940
+ async function duckduckgoSearch(query, num, signal) {
9941
+ const encoded = encodeURIComponent(query);
9137
9942
  const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
9138
9943
  try {
9139
9944
  const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
9140
9945
  const html = await response.text();
9141
- const results = parseDuckDuckGo(html, num);
9142
- return {
9143
- query: query2,
9144
- results,
9145
- source: "duckduckgo",
9146
- truncated: results.length >= num
9147
- };
9946
+ return parseDuckDuckGo(html, num);
9148
9947
  } catch (err) {
9149
- console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$3(err) }));
9150
- return {
9151
- query: query2,
9152
- results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
9153
- source: "duckduckgo",
9154
- truncated: false
9155
- };
9948
+ console.log(
9949
+ JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage$3(err) })
9950
+ );
9951
+ return [{ title: "Search unavailable", url: "https://duckduckgo.com/unavailable", snippet: "Could not reach DuckDuckGo", score: 0 }];
9156
9952
  }
9157
9953
  }
9158
9954
  function takeFrom(iter, max) {
@@ -9177,25 +9973,22 @@ function parseDuckDuckGo(html, num) {
9177
9973
  );
9178
9974
  for (let i = 0; i < linkMatches.length && i < num; i++) {
9179
9975
  const entry = linkMatches[i];
9180
- results.push({
9181
- title: entry?.title ?? "",
9182
- url: entry?.url ?? "",
9183
- snippet: snippetMatches[i] ?? ""
9184
- });
9976
+ if (entry) {
9977
+ results.push({
9978
+ title: entry.title ?? "",
9979
+ url: entry.url ?? "",
9980
+ snippet: snippetMatches[i] ?? "",
9981
+ score: 1
9982
+ });
9983
+ }
9185
9984
  }
9186
9985
  return results;
9187
9986
  }
9188
- async function googleSearch(query2, num, signal) {
9189
- const encoded = encodeURIComponent(query2);
9987
+ async function googleSearch(query, num, signal) {
9988
+ const encoded = encodeURIComponent(query);
9190
9989
  const url = `https://www.google.com/search?q=${encoded}&hl=en`;
9191
9990
  const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text()).catch(() => "");
9192
- const results = parseGoogleResults(html, num);
9193
- return {
9194
- query: query2,
9195
- results,
9196
- source: "google",
9197
- truncated: results.length >= num
9198
- };
9991
+ return parseGoogleResults(html, num);
9199
9992
  }
9200
9993
  function parseGoogleResults(html, num) {
9201
9994
  const results = [];
@@ -9218,22 +10011,17 @@ function parseGoogleResults(html, num) {
9218
10011
  results.push({
9219
10012
  title: titles[i] ?? "",
9220
10013
  url: urls[i] ?? "",
9221
- snippet: snippets[i] ?? ""
10014
+ snippet: snippets[i] ?? "",
10015
+ score: 1
9222
10016
  });
9223
10017
  }
9224
10018
  return results;
9225
10019
  }
9226
- async function bingSearch(query2, num, signal) {
9227
- const encoded = encodeURIComponent(query2);
10020
+ async function bingSearch(query, num, signal) {
10021
+ const encoded = encodeURIComponent(query);
9228
10022
  const url = `https://www.bing.com/search?q=${encoded}`;
9229
10023
  const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text()).catch(() => "");
9230
- const results = parseBingResults(html, num);
9231
- return {
9232
- query: query2,
9233
- results,
9234
- source: "bing",
9235
- truncated: results.length >= num
9236
- };
10024
+ return parseBingResults(html, num);
9237
10025
  }
9238
10026
  function parseBingResults(html, num) {
9239
10027
  const results = [];
@@ -9248,11 +10036,15 @@ function parseBingResults(html, num) {
9248
10036
  num
9249
10037
  );
9250
10038
  for (let i = 0; i < entries.length; i++) {
9251
- results.push({
9252
- title: entries[i]?.title ?? "",
9253
- url: entries[i]?.url ?? "",
9254
- snippet: snippets[i] ?? ""
9255
- });
10039
+ const entry = entries[i];
10040
+ if (entry) {
10041
+ results.push({
10042
+ title: entry.title ?? "",
10043
+ url: entry.url ?? "",
10044
+ snippet: snippets[i] ?? "",
10045
+ score: 1
10046
+ });
10047
+ }
9256
10048
  }
9257
10049
  return results;
9258
10050
  }
@@ -9268,7 +10060,15 @@ async function fetchWithTimeout(url, signal, timeoutMs) {
9268
10060
  return res;
9269
10061
  } catch (e) {
9270
10062
  clearTimeout(timer);
9271
- throw e;
10063
+ if (e instanceof FetchError) {
10064
+ throw e;
10065
+ }
10066
+ throw new FetchError({
10067
+ message: `search: failed to fetch ${url}`,
10068
+ status: 0,
10069
+ context: { url },
10070
+ cause: e
10071
+ });
9272
10072
  }
9273
10073
  }
9274
10074
  function anySignal(...signals) {
@@ -9332,15 +10132,15 @@ var setWorkingDirTool = {
9332
10132
  };
9333
10133
  }
9334
10134
  };
9335
- function findTaskIndex(tasks, query2) {
9336
- const asNum = Number.parseInt(query2, 10);
10135
+ function findTaskIndex(tasks, query) {
10136
+ const asNum = Number.parseInt(query, 10);
9337
10137
  if (!Number.isNaN(asNum)) {
9338
10138
  const idx = asNum - 1;
9339
10139
  if (tasks[idx]) return idx;
9340
10140
  }
9341
- const byId = tasks.findIndex((t) => t.id === query2);
10141
+ const byId = tasks.findIndex((t) => t.id === query);
9342
10142
  if (byId >= 0) return byId;
9343
- const lower = query2.toLowerCase();
10143
+ const lower = query.toLowerCase();
9344
10144
  return tasks.findIndex((t) => t.title.toLowerCase().includes(lower));
9345
10145
  }
9346
10146
  var taskTool = {
@@ -9964,8 +10764,8 @@ var todoTool = {
9964
10764
  var toolHelpTool = {
9965
10765
  name: "tool_help",
9966
10766
  category: "Meta",
9967
- description: "Get detailed help for one or more tools, including their full schema and usage guidance. This is the best way to understand exactly how to call a specific tool.",
9968
- usageHint: "USE WHEN YOU NEED PRECISE TOOL INFORMATION:\n\n- Call with a specific `tool` name when you want the full schema and current usageHint.\n- Omit `tool` (or use a broad query) to get an overview of available tools.\n- Different `format` options give you different levels of detail.\nThis tool is extremely valuable for self-correction when you are unsure about a tool's interface.",
10767
+ description: "Get detailed help for a specific tool, including its full input schema and usage guidance. If you do not know which tool to use, search with `tool_search` first, then call this with the tool name.",
10768
+ usageHint: "USE WHEN YOU NEED PRECISE TOOL INFORMATION:\n\n- Call with a specific `tool` name when you want the full schema and current usageHint.\n- Omit `tool` to get an overview of all available tools.\n- Different `format` options give you different levels of detail.\n- Tip: use `tool_search` to find the right tool name, then `tool_help` for the full schema.\nThis tool is extremely valuable for self-correction when you are unsure about a tool's interface.",
9969
10769
  permission: "auto",
9970
10770
  mutating: false,
9971
10771
  timeoutMs: 5e3,
@@ -10088,8 +10888,8 @@ function formatAllToolsMarkdown(tools) {
10088
10888
  var toolSearchTool = {
10089
10889
  name: "tool_search",
10090
10890
  category: "Meta",
10091
- description: "Search the catalog of available tools. Very useful when you are unsure which tool to use for a task.",
10092
- usageHint: "SELF-DISCOVERY TOOL:\n\n- Use when you need to find the right tool for a job.\n- `query` searches names and descriptions.\n- You can filter by `tags` (category), `permission`, or `mutating`.\nCall this before guessing tool names. It helps you discover the best tool for the current situation.",
10891
+ description: "Search the catalog of available tools by name or description. Use this to discover which tool to use for a task. For the full schema and usage details of a specific tool, use `tool_help` instead.",
10892
+ usageHint: "SELF-DISCOVERY TOOL:\n\n- Use when you need to find the right tool for a job.\n- `query` searches names and descriptions.\n- You can filter by `tags` (category), `permission`, or `mutating`.\n- Once you find the right tool name, use `tool_help` with that name for full schema details.\nCall this before guessing tool names. It helps you discover the best tool for the current situation.",
10093
10893
  permission: "auto",
10094
10894
  mutating: false,
10095
10895
  timeoutMs: 1e3,
@@ -10127,9 +10927,9 @@ var toolSearchTool = {
10127
10927
  async execute(input, ctx) {
10128
10928
  const limit = Math.min(input.limit ?? 20, 100);
10129
10929
  const tools = ctx.tools;
10130
- const query2 = input.query?.toLowerCase() ?? "";
10930
+ const query = input.query?.toLowerCase() ?? "";
10131
10931
  const filtered = tools.filter((t) => {
10132
- if (query2 && !t.name.toLowerCase().includes(query2) && !t.description.toLowerCase().includes(query2)) {
10932
+ if (query && !t.name.toLowerCase().includes(query) && !t.description.toLowerCase().includes(query)) {
10133
10933
  return false;
10134
10934
  }
10135
10935
  if (input.tags && input.tags.length > 0) {
@@ -10153,7 +10953,7 @@ var toolSearchTool = {
10153
10953
  mutating: t.mutating
10154
10954
  }));
10155
10955
  const totalAvailable = tools.length;
10156
- const hint = results.length === 0 && query2 ? `No tools matched "${input.query}". Use tool-help (without arguments) to see all ${totalAvailable} available tools.` : void 0;
10956
+ const hint = results.length === 0 && query ? `No tools matched "${input.query}". Use tool-help (without arguments) to see all ${totalAvailable} available tools.` : void 0;
10157
10957
  return {
10158
10958
  tools: results,
10159
10959
  total: filtered.length,
@@ -10526,8 +11326,18 @@ var writeTool = {
10526
11326
  required: ["path", "content"]
10527
11327
  },
10528
11328
  async execute(input, ctx) {
10529
- if (!input?.path) throw new Error("write: path is required");
10530
- if (input.content === void 0) throw new Error("write: content is required");
11329
+ if (!input?.path) {
11330
+ throw new ToolValidationError({
11331
+ message: "write: path is required",
11332
+ field: "path"
11333
+ });
11334
+ }
11335
+ if (input.content === void 0) {
11336
+ throw new ToolValidationError({
11337
+ message: "write: content is required",
11338
+ field: "content"
11339
+ });
11340
+ }
10531
11341
  const absPath = await safeResolveReal(input.path, ctx);
10532
11342
  let existed = false;
10533
11343
  let prev = "";