@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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as fs7 from 'node:fs/promises';
2
2
  import * as Core from '@wrongstack/core';
3
- import { toErrorMessage, atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, assessCommitSafety, recordPackageAction, detectPackageEcosystem, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
3
+ import { ToolValidationError, FsError, toErrorMessage, atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, ToolError, FetchError, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, assessCommitSafety, deepMerge, recordPackageAction, detectPackageEcosystem, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
4
4
  import * as path from 'node:path';
5
5
  import { resolve, sep, dirname, join } from 'node:path';
6
6
  import { spawn, execFileSync } from 'node:child_process';
@@ -207,19 +207,49 @@ var readTool = {
207
207
  required: ["path"]
208
208
  },
209
209
  async execute(input, ctx) {
210
- if (!input?.path) throw new Error("read: path is required");
210
+ if (!input?.path) {
211
+ throw new ToolValidationError({
212
+ message: "read: path is required",
213
+ field: "path"
214
+ });
215
+ }
211
216
  const absPath = await safeResolveReal(input.path, ctx);
212
217
  let stat11;
213
218
  try {
214
219
  stat11 = await fs7.stat(absPath);
215
220
  } catch (err) {
216
221
  const code = err.code;
217
- if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
218
- throw new Error(`read: failed to stat "${input.path}": ${toErrorMessage(err)}`);
222
+ if (code === "ENOENT") {
223
+ throw new FsError({
224
+ message: `read: file not found "${input.path}"`,
225
+ code: "FS_READ_FAILED",
226
+ path: absPath,
227
+ context: { errno: "ENOENT" }
228
+ });
229
+ }
230
+ throw new FsError({
231
+ message: `read: failed to stat "${input.path}": ${toErrorMessage(err)}`,
232
+ code: "FS_READ_FAILED",
233
+ path: absPath,
234
+ context: { errno: code },
235
+ cause: err
236
+ });
237
+ }
238
+ if (!stat11.isFile()) {
239
+ throw new FsError({
240
+ message: `read: "${input.path}" is not a regular file`,
241
+ code: "FS_READ_FAILED",
242
+ path: absPath,
243
+ context: { reason: "not-a-regular-file" }
244
+ });
219
245
  }
220
- if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
221
246
  if (stat11.size > MAX_BYTES) {
222
- throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES})`);
247
+ throw new FsError({
248
+ message: `read: file too large (${stat11.size} bytes, limit ${MAX_BYTES})`,
249
+ code: "FS_READ_FAILED",
250
+ path: absPath,
251
+ context: { size: stat11.size, limit: MAX_BYTES, reason: "too-large" }
252
+ });
223
253
  }
224
254
  const offset = Math.max(1, input.offset ?? 1);
225
255
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
@@ -364,8 +394,18 @@ var writeTool = {
364
394
  required: ["path", "content"]
365
395
  },
366
396
  async execute(input, ctx) {
367
- if (!input?.path) throw new Error("write: path is required");
368
- if (input.content === void 0) throw new Error("write: content is required");
397
+ if (!input?.path) {
398
+ throw new ToolValidationError({
399
+ message: "write: path is required",
400
+ field: "path"
401
+ });
402
+ }
403
+ if (input.content === void 0) {
404
+ throw new ToolValidationError({
405
+ message: "write: content is required",
406
+ field: "content"
407
+ });
408
+ }
369
409
  const absPath = await safeResolveReal(input.path, ctx);
370
410
  let existed = false;
371
411
  let prev = "";
@@ -425,28 +465,62 @@ var editTool = {
425
465
  required: ["path", "old_string", "new_string"]
426
466
  },
427
467
  async execute(input, ctx) {
428
- if (!input?.path) throw new Error("edit: path is required");
429
- if (input.old_string === void 0) throw new Error("edit: old_string is required");
430
- if (input.new_string === void 0) throw new Error("edit: new_string is required");
431
- if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
468
+ if (!input?.path) {
469
+ throw new ToolValidationError({ message: "edit: path is required", field: "path" });
470
+ }
471
+ if (input.old_string === void 0) {
472
+ throw new ToolValidationError({
473
+ message: "edit: old_string is required",
474
+ field: "old_string"
475
+ });
476
+ }
477
+ if (input.new_string === void 0) {
478
+ throw new ToolValidationError({
479
+ message: "edit: new_string is required",
480
+ field: "new_string"
481
+ });
482
+ }
483
+ if (input.old_string === "") {
484
+ throw new ToolValidationError({
485
+ message: "edit: old_string cannot be empty",
486
+ field: "old_string"
487
+ });
488
+ }
432
489
  const absPath = await safeResolveReal(input.path, ctx);
433
490
  const stat11 = await fs7.stat(absPath).catch((err) => {
434
491
  if (err.code === "ENOENT") {
435
- throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
492
+ throw new ToolValidationError({
493
+ message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
494
+ field: "path",
495
+ context: { exists: false }
496
+ });
436
497
  }
437
498
  throw err;
438
499
  });
439
- if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
500
+ if (!stat11.isFile()) {
501
+ throw new ToolValidationError({
502
+ message: `edit: "${input.path}" is not a regular file`,
503
+ field: "path"
504
+ });
505
+ }
440
506
  const autoRead = !ctx.hasRead(absPath);
441
507
  const original = await fs7.readFile(absPath, "utf8");
442
508
  const updated = await fs7.stat(absPath);
443
509
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
444
510
  const lastReadMtime = ctx.lastReadMtime(absPath);
445
511
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
446
- throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
512
+ throw new ToolValidationError({
513
+ message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
514
+ field: "path",
515
+ context: { reason: "external_modification" }
516
+ });
447
517
  }
448
518
  if (autoRead && updated.mtimeMs > stat11.mtimeMs + mtimeTolerance) {
449
- throw new Error(`edit: file "${input.path}" changed while being auto-read. Retry the edit.`);
519
+ throw new ToolValidationError({
520
+ message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
521
+ field: "path",
522
+ context: { reason: "auto_read_race" }
523
+ });
450
524
  }
451
525
  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;
452
526
  const style = detectNewlineStyle(original);
@@ -472,15 +546,18 @@ var editTool = {
472
546
  }
473
547
  if (count === 0) {
474
548
  const hint = findSimilarity(fileLf, oldLf);
475
- throw new Error(
476
- `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}.` : ""}`
477
- );
549
+ throw new ToolValidationError({
550
+ message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}.` : ""}`,
551
+ field: "old_string"
552
+ });
478
553
  }
479
554
  if (count > 1 && !input.replace_all) {
480
555
  const lines = lineNumbersFor(fileLf, matches);
481
- throw new Error(
482
- `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")}). Add more context to make it unique, or set replace_all: true.`
483
- );
556
+ throw new ToolValidationError({
557
+ 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.`,
558
+ field: "old_string",
559
+ context: { occurrences: count }
560
+ });
484
561
  }
485
562
  const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
486
563
  const newFile = toStyle(newFileLf, style);
@@ -580,8 +657,8 @@ var DEFAULT_IGNORE = ["node_modules", ".git", "dist", "build", ".next", "coverag
580
657
  var replaceTool = {
581
658
  name: "replace",
582
659
  category: "Transform",
583
- 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.",
584
- 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.",
660
+ 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.",
661
+ 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.",
585
662
  permission: "confirm",
586
663
  mutating: true,
587
664
  capabilities: ["fs.write"],
@@ -601,22 +678,40 @@ var replaceTool = {
601
678
  type: "boolean",
602
679
  description: "Replace all occurrences in each file (default: true)"
603
680
  },
604
- dry_run: { type: "boolean", description: "Preview changes without writing" }
681
+ dry_run: { type: "boolean", description: "Preview changes without writing (default: true)" }
605
682
  },
606
683
  required: ["pattern", "replacement", "files"]
607
684
  },
608
685
  async execute(input, ctx) {
609
- if (!input?.pattern) throw new Error("replace: pattern is required");
610
- if (input.replacement === void 0) throw new Error("replace: replacement is required");
611
- if (!input?.files) throw new Error("replace: files is required");
686
+ if (!input?.pattern) {
687
+ throw new ToolValidationError({
688
+ message: "replace: pattern is required",
689
+ field: "pattern"
690
+ });
691
+ }
692
+ if (input.replacement === void 0) {
693
+ throw new ToolValidationError({
694
+ message: "replace: replacement is required",
695
+ field: "replacement"
696
+ });
697
+ }
698
+ if (!input?.files) {
699
+ throw new ToolValidationError({
700
+ message: "replace: files is required",
701
+ field: "files"
702
+ });
703
+ }
612
704
  const replaceAll = input.replace_all ?? true;
613
705
  const compiled = compileUserRegex(input.pattern, "g");
614
706
  if (!compiled.ok) {
615
- throw new Error(`replace: ${compiled.reason}`);
707
+ throw new ToolValidationError({
708
+ message: `replace: ${compiled.reason}`,
709
+ field: "pattern"
710
+ });
616
711
  }
617
712
  const re = compiled.regex;
618
713
  const globRe = input.glob ? compileGlob(input.glob) : null;
619
- const dryRun = input.dry_run ?? false;
714
+ const dryRun = input.dry_run ?? true;
620
715
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
621
716
  const fileList = await resolveFiles(filesInput, ctx, globRe);
622
717
  const realRoot = await fs7.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
@@ -712,13 +807,13 @@ async function globFiles(pattern, base, extraGlob) {
712
807
  return await globNative(pattern, base, extraGlob);
713
808
  }
714
809
  function checkRg() {
715
- return new Promise((resolve6) => {
810
+ return new Promise((resolve7) => {
716
811
  try {
717
812
  const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
718
- p.on("error", () => resolve6(false));
719
- p.on("close", (code) => resolve6(code === 0));
813
+ p.on("error", () => resolve7(false));
814
+ p.on("close", (code) => resolve7(code === 0));
720
815
  } catch {
721
- resolve6(false);
816
+ resolve7(false);
722
817
  }
723
818
  });
724
819
  }
@@ -735,10 +830,10 @@ function spawnRgFind(pattern, base) {
735
830
  buf += chunk.toString();
736
831
  });
737
832
  return {
738
- promise: new Promise((resolve6, reject) => {
833
+ promise: new Promise((resolve7, reject) => {
739
834
  child.on("error", reject);
740
835
  child.on("close", () => {
741
- resolve6(buf.split("\n").filter(Boolean));
836
+ resolve7(buf.split("\n").filter(Boolean));
742
837
  });
743
838
  })
744
839
  };
@@ -830,7 +925,7 @@ var globTool = {
830
925
  },
831
926
  async execute(input, ctx) {
832
927
  if (!input?.pattern) throw new Error("glob: pattern is required");
833
- const base = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
928
+ const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
834
929
  const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
835
930
  const ignored = await readGitignore(base);
836
931
  const re = compileGlob(input.pattern);
@@ -885,8 +980,12 @@ var globTool = {
885
980
  try {
886
981
  const st = await fs7.stat(full);
887
982
  if (st.isDirectory()) {
983
+ const real = await fs7.realpath(full);
984
+ await assertRealInsideRoot(real, ctx);
888
985
  subdirs.push({ full, rel });
889
986
  } else if (st.isFile()) {
987
+ const real = await fs7.realpath(full);
988
+ await assertRealInsideRoot(real, ctx);
890
989
  re.lastIndex = 0;
891
990
  const relMatch = re.test(rel);
892
991
  re.lastIndex = 0;
@@ -977,13 +1076,21 @@ var grepTool = {
977
1076
  return final;
978
1077
  },
979
1078
  async *executeStream(input, ctx, opts) {
980
- if (!input?.pattern) throw new Error("grep: pattern is required");
1079
+ if (!input?.pattern) {
1080
+ throw new ToolValidationError({
1081
+ message: "grep: pattern is required",
1082
+ field: "pattern"
1083
+ });
1084
+ }
981
1085
  const base = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
982
1086
  const mode = input.output_mode ?? "content";
983
1087
  const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
984
1088
  const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
985
1089
  if (!validation.ok) {
986
- throw new Error(`grep: ${validation.reason}`);
1090
+ throw new ToolValidationError({
1091
+ message: `grep: ${validation.reason}`,
1092
+ field: "pattern"
1093
+ });
987
1094
  }
988
1095
  const rgAvailable = await detectRg(opts.signal);
989
1096
  if (rgAvailable) {
@@ -999,13 +1106,13 @@ var grepTool = {
999
1106
  }
1000
1107
  };
1001
1108
  async function detectRg(signal) {
1002
- return new Promise((resolve6) => {
1109
+ return new Promise((resolve7) => {
1003
1110
  try {
1004
1111
  const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal, windowsHide: true });
1005
- p.on("error", () => resolve6(false));
1006
- p.on("close", (code) => resolve6(code === 0));
1112
+ p.on("error", () => resolve7(false));
1113
+ p.on("close", (code) => resolve7(code === 0));
1007
1114
  } catch {
1008
- resolve6(false);
1115
+ resolve7(false);
1009
1116
  }
1010
1117
  });
1011
1118
  }
@@ -1139,7 +1246,10 @@ async function runNative(input, base, mode, limit, signal) {
1139
1246
  const flags = input.case_insensitive ? "i" : "";
1140
1247
  const compiled = compileUserRegex(input.pattern, flags);
1141
1248
  if (!compiled.ok) {
1142
- throw new Error(`grep: ${compiled.reason}`);
1249
+ throw new ToolValidationError({
1250
+ message: `grep: ${compiled.reason}`,
1251
+ field: "pattern"
1252
+ });
1143
1253
  }
1144
1254
  const re = compiled.regex;
1145
1255
  const globRe = input.glob ? compileGlob(input.glob) : null;
@@ -2971,10 +3081,10 @@ var bashTool = {
2971
3081
  queue.push(c);
2972
3082
  }
2973
3083
  };
2974
- const next = () => new Promise((resolve6) => {
3084
+ const next = () => new Promise((resolve7) => {
2975
3085
  const c = queue.shift();
2976
- if (c) resolve6(c);
2977
- else resolveNext = resolve6;
3086
+ if (c) resolve7(c);
3087
+ else resolveNext = resolve7;
2978
3088
  });
2979
3089
  let lastFlush = Date.now();
2980
3090
  const flush = () => {
@@ -3435,7 +3545,7 @@ var execTool = {
3435
3545
  }
3436
3546
  };
3437
3547
  function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3438
- return new Promise((resolve6) => {
3548
+ return new Promise((resolve7) => {
3439
3549
  let stdout = "";
3440
3550
  let stderr = "";
3441
3551
  let killed = false;
@@ -3443,7 +3553,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3443
3553
  const finish = (result) => {
3444
3554
  if (resolvedOnce.value) return;
3445
3555
  resolvedOnce.value = true;
3446
- resolve6(result);
3556
+ resolve7(result);
3447
3557
  };
3448
3558
  const startedAt = Date.now();
3449
3559
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
@@ -3620,10 +3730,16 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
3620
3730
  for (; ; ) {
3621
3731
  const parsed = new URL(currentUrl);
3622
3732
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
3623
- throw new Error(`fetch: redirect to unsupported protocol "${parsed.protocol}"`);
3733
+ throw new ToolValidationError({
3734
+ message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
3735
+ field: "url"
3736
+ });
3624
3737
  }
3625
3738
  if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
3626
- throw new Error("fetch: redirect to http:// blocked (HTTPS required by default)");
3739
+ throw new ToolValidationError({
3740
+ message: "fetch: redirect to http:// blocked (HTTPS required by default)",
3741
+ field: "url"
3742
+ });
3627
3743
  }
3628
3744
  await assertNotPrivate(parsed.hostname);
3629
3745
  const init = {
@@ -3638,11 +3754,19 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
3638
3754
  }
3639
3755
  redirectCount++;
3640
3756
  if (redirectCount > maxRedirects) {
3641
- throw new Error(`fetch: exceeded ${maxRedirects} redirects`);
3757
+ throw new FetchError({
3758
+ message: `fetch: exceeded ${maxRedirects} redirects`,
3759
+ status: res.status,
3760
+ context: { url: currentUrl, maxRedirects, redirectCount }
3761
+ });
3642
3762
  }
3643
3763
  const location = res.headers.get("location");
3644
3764
  if (!location) {
3645
- throw new Error("fetch: redirect status with no location header");
3765
+ throw new FetchError({
3766
+ message: "fetch: redirect status with no location header",
3767
+ status: res.status,
3768
+ context: { url: currentUrl, redirectCount }
3769
+ });
3646
3770
  }
3647
3771
  currentUrl = new URL(location, currentUrl).toString();
3648
3772
  }
@@ -3681,26 +3805,53 @@ var fetchTool = {
3681
3805
  async execute(input, ctx, opts) {
3682
3806
  let final;
3683
3807
  const executeStream = fetchTool.executeStream;
3684
- if (!executeStream) throw new Error("fetchTool: stream execution unavailable");
3808
+ if (!executeStream) {
3809
+ throw new ToolError({
3810
+ message: "fetchTool: stream execution unavailable",
3811
+ code: "TOOL_EXECUTION_FAILED",
3812
+ toolName: "fetch"
3813
+ });
3814
+ }
3685
3815
  for await (const ev of executeStream(input, ctx, opts)) {
3686
3816
  if (ev.type === "final") final = ev.output;
3687
3817
  }
3688
- if (!final) throw new Error("fetch: stream ended without final event");
3818
+ if (!final) {
3819
+ throw new ToolError({
3820
+ message: "fetch: stream ended without final event",
3821
+ code: "TOOL_EXECUTION_FAILED",
3822
+ toolName: "fetch"
3823
+ });
3824
+ }
3689
3825
  return final;
3690
3826
  },
3691
3827
  async *executeStream(input, ctx, opts) {
3692
- if (!input?.url) throw new Error("fetch: url is required");
3828
+ if (!input?.url) {
3829
+ throw new ToolValidationError({
3830
+ message: "fetch: url is required",
3831
+ field: "url"
3832
+ });
3833
+ }
3693
3834
  const u = new URL(input.url);
3694
3835
  if (u.protocol !== "https:" && u.protocol !== "http:") {
3695
- throw new Error(`fetch: unsupported protocol "${u.protocol}"`);
3836
+ throw new ToolValidationError({
3837
+ message: `fetch: unsupported protocol "${u.protocol}"`,
3838
+ field: "url"
3839
+ });
3696
3840
  }
3697
3841
  if (u.protocol === "http:" && !ALLOW_PRIVATE) {
3698
- throw new Error("fetch: http:// blocked (HTTPS required by default)");
3842
+ throw new ToolValidationError({
3843
+ message: "fetch: http:// blocked (HTTPS required by default)",
3844
+ field: "url"
3845
+ });
3699
3846
  }
3700
3847
  await assertNotPrivate(u.hostname);
3701
3848
  yield { type: "log", text: `GET ${input.url}` };
3702
3849
  const ctrl = new AbortController();
3703
- const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS);
3850
+ const timer = setTimeout(() => ctrl.abort(new ToolError({
3851
+ message: "fetch timeout",
3852
+ code: "TOOL_TIMEOUT",
3853
+ toolName: "fetch"
3854
+ })), TIMEOUT_MS);
3704
3855
  const combined = combineSignals([opts.signal, ctrl.signal]);
3705
3856
  try {
3706
3857
  let res;
@@ -3712,7 +3863,11 @@ var fetchTool = {
3712
3863
  }
3713
3864
  const ct = res.headers.get("content-type") ?? "application/octet-stream";
3714
3865
  if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
3715
- throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
3866
+ throw new FetchError({
3867
+ message: `fetch: refusing to read binary content-type "${ct}"`,
3868
+ status: res.status,
3869
+ context: { url: res.url, contentType: ct }
3870
+ });
3716
3871
  }
3717
3872
  yield {
3718
3873
  type: "log",
@@ -3777,16 +3932,25 @@ async function assertNotPrivate(hostname4) {
3777
3932
  if (ALLOW_PRIVATE) return;
3778
3933
  const host = hostname4.startsWith("[") && hostname4.endsWith("]") ? hostname4.slice(1, -1) : hostname4;
3779
3934
  if (host === "localhost" || host.endsWith(".localhost")) {
3780
- throw new Error("fetch: blocked localhost target");
3935
+ throw new ToolValidationError({
3936
+ message: "fetch: blocked localhost target",
3937
+ field: "url"
3938
+ });
3781
3939
  }
3782
3940
  const ipVersion = net.isIP(host);
3783
3941
  if (ipVersion === 4) {
3784
3942
  if (isPrivateIPv4(host)) {
3785
- throw new Error(`fetch: blocked private/loopback address "${host}"`);
3943
+ throw new ToolValidationError({
3944
+ message: `fetch: blocked private/loopback address "${host}"`,
3945
+ field: "url"
3946
+ });
3786
3947
  }
3787
3948
  } else if (ipVersion === 6) {
3788
3949
  if (isPrivateIPv6(host)) {
3789
- throw new Error(`fetch: blocked private/loopback address "${host}"`);
3950
+ throw new ToolValidationError({
3951
+ message: `fetch: blocked private/loopback address "${host}"`,
3952
+ field: "url"
3953
+ });
3790
3954
  }
3791
3955
  } else {
3792
3956
  try {
@@ -3794,7 +3958,10 @@ async function assertNotPrivate(hostname4) {
3794
3958
  for (const r of records) {
3795
3959
  const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
3796
3960
  if (bad) {
3797
- throw new Error(`fetch: resolved to private address ${r.address}`);
3961
+ throw new ToolValidationError({
3962
+ message: `fetch: resolved to private address ${r.address}`,
3963
+ field: "url"
3964
+ });
3798
3965
  }
3799
3966
  }
3800
3967
  } catch (err) {
@@ -3804,7 +3971,13 @@ async function assertNotPrivate(hostname4) {
3804
3971
  }
3805
3972
  function describeFetchError(err, url, timedOut) {
3806
3973
  if (timedOut) {
3807
- return new Error(`fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`);
3974
+ return new ToolError({
3975
+ message: `fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`,
3976
+ code: "TOOL_TIMEOUT",
3977
+ toolName: "fetch",
3978
+ context: { url, timedOut: true, timeoutMs: TIMEOUT_MS },
3979
+ cause: err
3980
+ });
3808
3981
  }
3809
3982
  const parts = [];
3810
3983
  const seen = /* @__PURE__ */ new Set();
@@ -3817,7 +3990,15 @@ function describeFetchError(err, url, timedOut) {
3817
3990
  cur = cur.cause;
3818
3991
  }
3819
3992
  const detail = parts.length > 0 ? parts.join(" \u2192 ") : "fetch failed";
3820
- return new Error(`fetch: GET ${url} failed \u2014 ${detail}`);
3993
+ return new FetchError({
3994
+ message: `fetch: GET ${url} failed \u2014 ${detail}`,
3995
+ status: 502,
3996
+ context: { url, timedOut: false, transportErrors: parts },
3997
+ // Preserve the original undici / DNS / TLS chain so callers can inspect
3998
+ // it via `err.cause` and structured `instanceof` checks. The flattened
3999
+ // text version stays in the message for human readability.
4000
+ cause: err
4001
+ });
3821
4002
  }
3822
4003
  function prettyJson(s) {
3823
4004
  try {
@@ -3829,11 +4010,13 @@ function prettyJson(s) {
3829
4010
  var DEFAULT_NUM = 10;
3830
4011
  var MAX_RESULTS = 50;
3831
4012
  var TIMEOUT_MS2 = 15e3;
4013
+ var CACHE_TTL_MS = 3e5;
4014
+ var cache = /* @__PURE__ */ new Map();
3832
4015
  var searchTool = {
3833
4016
  name: "search",
3834
4017
  category: "Search",
3835
- 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.",
3836
- 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.",
4018
+ 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.",
4019
+ 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.",
3837
4020
  permission: "confirm",
3838
4021
  mutating: false,
3839
4022
  capabilities: ["net.outbound"],
@@ -3853,6 +4036,10 @@ var searchTool = {
3853
4036
  type: "string",
3854
4037
  enum: ["duckduckgo", "google", "bing"],
3855
4038
  description: "Search engine to use (default: duckduckgo)"
4039
+ },
4040
+ skip_cache: {
4041
+ type: "boolean",
4042
+ description: "Skip the in-memory cache and force a fresh search (default: false)"
3856
4043
  }
3857
4044
  },
3858
4045
  required: ["query"]
@@ -3868,57 +4055,135 @@ var searchTool = {
3868
4055
  return final;
3869
4056
  },
3870
4057
  async *executeStream(input, _ctx, opts) {
3871
- if (!input?.query) throw new Error("search: query is required");
4058
+ if (!input?.query || input.query.trim() === "") {
4059
+ throw new ToolValidationError({
4060
+ message: "search: query is required and must be a non-empty string",
4061
+ field: "query"
4062
+ });
4063
+ }
3872
4064
  const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));
3873
4065
  const source = input.source ?? "duckduckgo";
4066
+ const skipCache = input.skip_cache ?? false;
4067
+ const cacheKey = `${source}:${input.query}`;
4068
+ if (!skipCache) {
4069
+ const entry = cache.get(cacheKey);
4070
+ if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {
4071
+ const results = entry.results.map((r) => ({
4072
+ title: r.title,
4073
+ url: r.url,
4074
+ snippet: r.snippet
4075
+ }));
4076
+ yield {
4077
+ type: "log",
4078
+ text: `Cache hit for "${input.query}" (${source})`,
4079
+ data: { source, query: input.query, cached: true }
4080
+ };
4081
+ yield {
4082
+ type: "partial_output",
4083
+ text: `${results.length} cached results from ${source}`,
4084
+ data: { count: results.length, cached: true }
4085
+ };
4086
+ yield {
4087
+ type: "final",
4088
+ output: {
4089
+ query: input.query,
4090
+ results: results.slice(0, num),
4091
+ source,
4092
+ truncated: results.length >= num,
4093
+ cached: true
4094
+ }
4095
+ };
4096
+ return;
4097
+ }
4098
+ }
3874
4099
  yield {
3875
4100
  type: "log",
3876
4101
  text: `Querying ${source} for "${input.query}"\u2026`,
3877
- data: { source, query: input.query }
4102
+ data: { source, query: input.query, cached: false }
3878
4103
  };
3879
- let output;
4104
+ let rawResults;
3880
4105
  switch (source) {
3881
4106
  case "duckduckgo":
3882
- output = await duckduckgoSearch(input.query, num, opts.signal);
4107
+ rawResults = await duckduckgoSearch(input.query, num, opts.signal);
3883
4108
  break;
3884
4109
  case "google":
3885
- output = await googleSearch(input.query, num, opts.signal);
4110
+ rawResults = await googleSearch(input.query, num, opts.signal);
3886
4111
  break;
3887
4112
  case "bing":
3888
- output = await bingSearch(input.query, num, opts.signal);
4113
+ rawResults = await bingSearch(input.query, num, opts.signal);
3889
4114
  break;
3890
4115
  default:
3891
- throw new Error(`search: unknown source "${source}"`);
4116
+ throw new ToolValidationError({
4117
+ message: `search: unknown source "${source}"`,
4118
+ field: "source"
4119
+ });
3892
4120
  }
4121
+ const seenUrls = /* @__PURE__ */ new Set();
4122
+ const deduped = [];
4123
+ for (const r of rawResults) {
4124
+ const noQuery = r.url.split("?")[0] ?? r.url;
4125
+ const normalized = noQuery.split("#")[0] ?? r.url;
4126
+ if (!seenUrls.has(normalized) && r.url.startsWith("http")) {
4127
+ seenUrls.add(normalized);
4128
+ deduped.push(r);
4129
+ }
4130
+ }
4131
+ const ranked = scoreResults(deduped, input.query);
4132
+ const finalResults = ranked.slice(0, num);
4133
+ cache.set(cacheKey, { results: ranked, timestamp: Date.now() });
4134
+ pruneStaleCacheEntries();
3893
4135
  yield {
3894
4136
  type: "partial_output",
3895
- text: `${output.results.length} results from ${output.source}`,
3896
- data: { count: output.results.length }
4137
+ text: `${finalResults.length} results from ${source}`,
4138
+ data: { count: finalResults.length, cached: false }
4139
+ };
4140
+ yield {
4141
+ type: "final",
4142
+ output: {
4143
+ query: input.query,
4144
+ results: finalResults.map((r) => ({
4145
+ title: r.title,
4146
+ url: r.url,
4147
+ snippet: r.snippet
4148
+ })),
4149
+ source,
4150
+ truncated: finalResults.length >= num,
4151
+ cached: false
4152
+ }
3897
4153
  };
3898
- yield { type: "final", output };
3899
4154
  }
3900
4155
  };
3901
- async function duckduckgoSearch(query2, num, signal) {
3902
- const encoded = encodeURIComponent(query2);
4156
+ function pruneStaleCacheEntries() {
4157
+ const cutoff = Date.now() - CACHE_TTL_MS * 2;
4158
+ for (const [key, entry] of cache.entries()) {
4159
+ if (entry.timestamp < cutoff) cache.delete(key);
4160
+ }
4161
+ }
4162
+ function scoreResults(results, query) {
4163
+ const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
4164
+ return results.map((r) => {
4165
+ const titleLower = r.title.toLowerCase();
4166
+ const snippetLower = r.snippet.toLowerCase();
4167
+ let score = r.score;
4168
+ for (const term of terms) {
4169
+ if (titleLower.includes(term)) score += 2;
4170
+ if (snippetLower.includes(term)) score += 1;
4171
+ }
4172
+ return { ...r, score };
4173
+ }).sort((a, b) => b.score - a.score);
4174
+ }
4175
+ async function duckduckgoSearch(query, num, signal) {
4176
+ const encoded = encodeURIComponent(query);
3903
4177
  const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
3904
4178
  try {
3905
4179
  const response = await fetchWithTimeout(url, signal, TIMEOUT_MS2);
3906
4180
  const html = await response.text();
3907
- const results = parseDuckDuckGo(html, num);
3908
- return {
3909
- query: query2,
3910
- results,
3911
- source: "duckduckgo",
3912
- truncated: results.length >= num
3913
- };
4181
+ return parseDuckDuckGo(html, num);
3914
4182
  } catch (err) {
3915
- console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage$2(err) }));
3916
- return {
3917
- query: query2,
3918
- results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
3919
- source: "duckduckgo",
3920
- truncated: false
3921
- };
4183
+ console.log(
4184
+ JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage$2(err) })
4185
+ );
4186
+ return [{ title: "Search unavailable", url: "https://duckduckgo.com/unavailable", snippet: "Could not reach DuckDuckGo", score: 0 }];
3922
4187
  }
3923
4188
  }
3924
4189
  function takeFrom(iter, max) {
@@ -3943,25 +4208,22 @@ function parseDuckDuckGo(html, num) {
3943
4208
  );
3944
4209
  for (let i = 0; i < linkMatches.length && i < num; i++) {
3945
4210
  const entry = linkMatches[i];
3946
- results.push({
3947
- title: entry?.title ?? "",
3948
- url: entry?.url ?? "",
3949
- snippet: snippetMatches[i] ?? ""
3950
- });
4211
+ if (entry) {
4212
+ results.push({
4213
+ title: entry.title ?? "",
4214
+ url: entry.url ?? "",
4215
+ snippet: snippetMatches[i] ?? "",
4216
+ score: 1
4217
+ });
4218
+ }
3951
4219
  }
3952
4220
  return results;
3953
4221
  }
3954
- async function googleSearch(query2, num, signal) {
3955
- const encoded = encodeURIComponent(query2);
4222
+ async function googleSearch(query, num, signal) {
4223
+ const encoded = encodeURIComponent(query);
3956
4224
  const url = `https://www.google.com/search?q=${encoded}&hl=en`;
3957
4225
  const html = await fetchWithTimeout(url, signal, TIMEOUT_MS2).then((r) => r.text()).catch(() => "");
3958
- const results = parseGoogleResults(html, num);
3959
- return {
3960
- query: query2,
3961
- results,
3962
- source: "google",
3963
- truncated: results.length >= num
3964
- };
4226
+ return parseGoogleResults(html, num);
3965
4227
  }
3966
4228
  function parseGoogleResults(html, num) {
3967
4229
  const results = [];
@@ -3984,22 +4246,17 @@ function parseGoogleResults(html, num) {
3984
4246
  results.push({
3985
4247
  title: titles[i] ?? "",
3986
4248
  url: urls[i] ?? "",
3987
- snippet: snippets[i] ?? ""
4249
+ snippet: snippets[i] ?? "",
4250
+ score: 1
3988
4251
  });
3989
4252
  }
3990
4253
  return results;
3991
4254
  }
3992
- async function bingSearch(query2, num, signal) {
3993
- const encoded = encodeURIComponent(query2);
4255
+ async function bingSearch(query, num, signal) {
4256
+ const encoded = encodeURIComponent(query);
3994
4257
  const url = `https://www.bing.com/search?q=${encoded}`;
3995
4258
  const html = await fetchWithTimeout(url, signal, TIMEOUT_MS2).then((r) => r.text()).catch(() => "");
3996
- const results = parseBingResults(html, num);
3997
- return {
3998
- query: query2,
3999
- results,
4000
- source: "bing",
4001
- truncated: results.length >= num
4002
- };
4259
+ return parseBingResults(html, num);
4003
4260
  }
4004
4261
  function parseBingResults(html, num) {
4005
4262
  const results = [];
@@ -4014,11 +4271,15 @@ function parseBingResults(html, num) {
4014
4271
  num
4015
4272
  );
4016
4273
  for (let i = 0; i < entries.length; i++) {
4017
- results.push({
4018
- title: entries[i]?.title ?? "",
4019
- url: entries[i]?.url ?? "",
4020
- snippet: snippets[i] ?? ""
4021
- });
4274
+ const entry = entries[i];
4275
+ if (entry) {
4276
+ results.push({
4277
+ title: entry.title ?? "",
4278
+ url: entry.url ?? "",
4279
+ snippet: snippets[i] ?? "",
4280
+ score: 1
4281
+ });
4282
+ }
4022
4283
  }
4023
4284
  return results;
4024
4285
  }
@@ -4034,7 +4295,15 @@ async function fetchWithTimeout(url, signal, timeoutMs) {
4034
4295
  return res;
4035
4296
  } catch (e) {
4036
4297
  clearTimeout(timer);
4037
- throw e;
4298
+ if (e instanceof FetchError) {
4299
+ throw e;
4300
+ }
4301
+ throw new FetchError({
4302
+ message: `search: failed to fetch ${url}`,
4303
+ status: 0,
4304
+ context: { url },
4305
+ cause: e
4306
+ });
4038
4307
  }
4039
4308
  }
4040
4309
  function anySignal(...signals) {
@@ -4647,7 +4916,7 @@ function buildArgs(input) {
4647
4916
  }
4648
4917
  }
4649
4918
  function runGit(args, cwd, signal) {
4650
- return new Promise((resolve6) => {
4919
+ return new Promise((resolve7) => {
4651
4920
  let stdout = "";
4652
4921
  let stderr = "";
4653
4922
  const child = spawn("git", args, {
@@ -4668,7 +4937,7 @@ function runGit(args, cwd, signal) {
4668
4937
  }
4669
4938
  });
4670
4939
  child.on("error", (err) => {
4671
- resolve6({
4940
+ resolve7({
4672
4941
  command: args[0],
4673
4942
  stdout: normalizeCommandOutput(stdout),
4674
4943
  stderr: err.message,
@@ -4677,7 +4946,7 @@ function runGit(args, cwd, signal) {
4677
4946
  });
4678
4947
  });
4679
4948
  child.on("close", (code) => {
4680
- resolve6({
4949
+ resolve7({
4681
4950
  command: args[0],
4682
4951
  stdout: normalizeCommandOutput(stdout),
4683
4952
  stderr: normalizeCommandOutput(stderr),
@@ -4777,7 +5046,7 @@ function stripPathComponents(p, strip) {
4777
5046
  return parts.slice(strip).join("/");
4778
5047
  }
4779
5048
  function runPatch(args, cwd, signal) {
4780
- return new Promise((resolve6) => {
5049
+ return new Promise((resolve7) => {
4781
5050
  let stdout = "";
4782
5051
  let stderr = "";
4783
5052
  const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
@@ -4788,8 +5057,8 @@ function runPatch(args, cwd, signal) {
4788
5057
  child.stderr?.on("data", (c) => {
4789
5058
  stderr += c.toString();
4790
5059
  });
4791
- child.on("close", (code) => resolve6({ exitCode: code ?? 1, stdout, stderr }));
4792
- child.on("error", (e) => resolve6({ exitCode: 1, stdout: "", stderr: e.message }));
5060
+ child.on("close", (code) => resolve7({ exitCode: code ?? 1, stdout, stderr }));
5061
+ child.on("error", (e) => resolve7({ exitCode: 1, stdout: "", stderr: e.message }));
4793
5062
  });
4794
5063
  }
4795
5064
  function extractPatchedFiles(output) {
@@ -4803,8 +5072,8 @@ function extractPatchedFiles(output) {
4803
5072
  var jsonTool = {
4804
5073
  name: "json",
4805
5074
  category: "Data",
4806
- description: "Parse, pretty-print, query, and convert between JSON, JSON5, and YAML. Supports simple path-based queries.",
4807
- 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.",
5075
+ 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.",
5076
+ 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.',
4808
5077
  permission: "auto",
4809
5078
  mutating: false,
4810
5079
  timeoutMs: 5e3,
@@ -4813,69 +5082,420 @@ var jsonTool = {
4813
5082
  inputSchema: {
4814
5083
  type: "object",
4815
5084
  properties: {
4816
- file: { type: "string", description: "Path to JSON/JSON5/YAML file" },
4817
- data: { type: "string", description: "JSON/JSON5/YAML string (alternative to file)" },
4818
- query: {
5085
+ action: {
4819
5086
  type: "string",
4820
- description: 'JMESPath-like query (e.g. "a.b[0].c" or "a[*].name")'
5087
+ enum: ["parse", "query", "validate", "transform", "merge"],
5088
+ description: "Operation (default: parse). parse=read/pretty-print, query=JMESPath, validate=schema, transform=chained queries, merge=deep merge."
4821
5089
  },
5090
+ file: { type: "string", description: "Path to JSON/JSON5/YAML file (parse/query/validate)" },
5091
+ data: { type: "string", description: "JSON/JSON5/YAML string (parse/query/validate, alternative to file)" },
4822
5092
  format: {
4823
5093
  type: "string",
4824
5094
  enum: ["json", "json5", "yaml"],
4825
- description: "Output format (default: json)"
5095
+ description: "Output format for parse/query/transform (default: json)"
5096
+ },
5097
+ query: {
5098
+ type: "string",
5099
+ description: "JMESPath-like query expression (query action)"
5100
+ },
5101
+ transforms: {
5102
+ type: "array",
5103
+ items: { type: "string" },
5104
+ description: "Ordered JMESPath query strings (transform action)"
5105
+ },
5106
+ schema: {
5107
+ type: "object",
5108
+ description: "JSON Schema to validate against (validate action)"
5109
+ },
5110
+ base: { description: "Base JSON object (merge action)" },
5111
+ patch: { description: "Patch JSON object to merge in (merge action)" },
5112
+ conflictResolution: {
5113
+ type: "string",
5114
+ enum: ["prefer-base", "prefer-patch"],
5115
+ description: "Merge conflict resolution (default: prefer-patch)"
4826
5116
  },
4827
5117
  validate: {
4828
5118
  type: "boolean",
4829
- description: "Validate syntax only, no output (default: false)"
5119
+ description: "Validate syntax only, no output (parse action, default: false)"
4830
5120
  }
4831
5121
  }
4832
5122
  },
4833
- async execute(input) {
4834
- const format = input.format ?? "json";
4835
- let parsed;
4836
- let raw;
4837
- if (input.file) {
4838
- try {
4839
- raw = await fs7.readFile(input.file, "utf8");
4840
- } catch {
4841
- return { data: null, formatted: "", type: "unknown", error: `Could not read file` };
4842
- }
4843
- } else if (input.data) {
4844
- raw = input.data;
4845
- } else {
4846
- return { data: null, formatted: "", type: "unknown", error: "Provide file or data" };
5123
+ async execute(input, ctx) {
5124
+ const action = input.action ?? "parse";
5125
+ switch (action) {
5126
+ case "query":
5127
+ return executeQuery(input, ctx);
5128
+ case "validate":
5129
+ return executeValidate(input, ctx);
5130
+ case "transform":
5131
+ return executeTransform(input, ctx);
5132
+ case "merge":
5133
+ return executeMerge(input);
5134
+ case "parse":
5135
+ default:
5136
+ return executeParse(input, ctx);
4847
5137
  }
5138
+ }
5139
+ };
5140
+ async function executeParse(input, ctx) {
5141
+ const format = input.format ?? "json";
5142
+ let parsed;
5143
+ let raw;
5144
+ if (input.file) {
4848
5145
  try {
4849
- parsed = JSON.parse(raw);
4850
- } catch (e) {
4851
- return {
4852
- data: null,
4853
- formatted: "",
4854
- type: "unknown",
4855
- /* v8 ignore next -- JSON.parse only throws SyntaxError (an Error); the String(e) side is defensive. */
4856
- error: `Parse failed: ${e instanceof Error ? e.message : String(e)}`
4857
- };
4858
- }
4859
- if (input.validate) {
4860
- return {
4861
- data: parsed,
4862
- formatted: "valid",
4863
- type: Array.isArray(parsed) ? "array" : typeof parsed,
4864
- keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
4865
- };
5146
+ raw = await fs7.readFile(await safeResolveReal(input.file, ctx), "utf8");
5147
+ } catch {
5148
+ return { data: null, formatted: "", type: "unknown", action: "parse", error: "Could not read file" };
4866
5149
  }
4867
- const queryResult = input.query ? query(parsed, input.query) : void 0;
4868
- const formatted = formatOutput(queryResult ?? parsed, format);
5150
+ } else if (input.data) {
5151
+ raw = input.data;
5152
+ } else {
5153
+ return { data: null, formatted: "", type: "unknown", action: "parse", error: "Provide file or data" };
5154
+ }
5155
+ try {
5156
+ parsed = JSON.parse(raw);
5157
+ } catch (e) {
5158
+ return {
5159
+ data: null,
5160
+ formatted: "",
5161
+ type: "unknown",
5162
+ action: "parse",
5163
+ /* v8 ignore next -- JSON.parse only throws SyntaxError (an Error); the String(e) side is defensive. */
5164
+ error: `Parse failed: ${e instanceof Error ? e.message : String(e)}`
5165
+ };
5166
+ }
5167
+ if (input.validate) {
4869
5168
  return {
4870
5169
  data: parsed,
4871
- formatted,
5170
+ formatted: "valid",
4872
5171
  type: Array.isArray(parsed) ? "array" : typeof parsed,
5172
+ action: "parse",
5173
+ keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
5174
+ };
5175
+ }
5176
+ if (input.query) {
5177
+ const queryResult = simpleQuery(parsed, input.query);
5178
+ const formatted2 = formatOutput(queryResult, format);
5179
+ return {
5180
+ data: parsed,
5181
+ formatted: formatted2,
5182
+ type: Array.isArray(parsed) ? "array" : typeof parsed,
5183
+ action: "parse",
4873
5184
  keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0,
4874
5185
  query_result: queryResult
4875
5186
  };
4876
5187
  }
4877
- };
4878
- function query(data, path22) {
5188
+ const formatted = formatOutput(parsed, format);
5189
+ return {
5190
+ data: parsed,
5191
+ formatted,
5192
+ type: Array.isArray(parsed) ? "array" : typeof parsed,
5193
+ action: "parse",
5194
+ keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
5195
+ };
5196
+ }
5197
+ async function executeQuery(input, ctx) {
5198
+ if (!input.query) {
5199
+ return { data: null, formatted: "", type: "unknown", action: "query", error: "query is required for action: query" };
5200
+ }
5201
+ let parsed;
5202
+ if (input.file) {
5203
+ try {
5204
+ const raw = await fs7.readFile(await safeResolveReal(input.file, ctx), "utf8");
5205
+ parsed = JSON.parse(raw);
5206
+ } catch {
5207
+ return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not read/parse file" };
5208
+ }
5209
+ } else if (input.data) {
5210
+ try {
5211
+ parsed = JSON.parse(input.data);
5212
+ } catch {
5213
+ return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not parse data string" };
5214
+ }
5215
+ } else {
5216
+ return { data: null, formatted: "", type: "unknown", action: "query", error: "Provide file or data" };
5217
+ }
5218
+ try {
5219
+ const result = jmespathSearch(parsed, input.query);
5220
+ const format = input.format ?? "json";
5221
+ return {
5222
+ data: parsed,
5223
+ formatted: formatOutput(result, format),
5224
+ type: result === null ? "null" : Array.isArray(result) ? "array" : typeof result,
5225
+ action: "query",
5226
+ query_result: result
5227
+ };
5228
+ } catch (e) {
5229
+ return {
5230
+ data: null,
5231
+ formatted: "",
5232
+ type: "unknown",
5233
+ action: "query",
5234
+ /* v8 ignore next -- defensive String(e) */
5235
+ error: `Query failed: ${e instanceof Error ? e.message : String(e)}`
5236
+ };
5237
+ }
5238
+ }
5239
+ async function executeValidate(input, ctx) {
5240
+ if (!input.schema) {
5241
+ return { data: null, formatted: "", type: "unknown", action: "validate", error: "schema is required for action: validate" };
5242
+ }
5243
+ let parsed;
5244
+ if (input.file) {
5245
+ try {
5246
+ const raw = await fs7.readFile(await safeResolveReal(input.file, ctx), "utf8");
5247
+ parsed = JSON.parse(raw);
5248
+ } catch {
5249
+ return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not read/parse file" };
5250
+ }
5251
+ } else if (input.data) {
5252
+ try {
5253
+ parsed = JSON.parse(input.data);
5254
+ } catch {
5255
+ return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not parse data string" };
5256
+ }
5257
+ } else {
5258
+ return { data: null, formatted: "", type: "unknown", action: "validate", error: "Provide file or data" };
5259
+ }
5260
+ try {
5261
+ const { valid, errors } = validateJsonSchema(parsed, input.schema);
5262
+ return {
5263
+ data: parsed,
5264
+ formatted: valid ? "valid" : "invalid",
5265
+ type: Array.isArray(parsed) ? "array" : typeof parsed,
5266
+ action: "validate",
5267
+ valid,
5268
+ errors
5269
+ };
5270
+ } catch (e) {
5271
+ return {
5272
+ data: null,
5273
+ formatted: "",
5274
+ type: "unknown",
5275
+ action: "validate",
5276
+ /* v8 ignore next -- defensive String(e) */
5277
+ error: `Validation failed: ${e instanceof Error ? e.message : String(e)}`
5278
+ };
5279
+ }
5280
+ }
5281
+ async function executeTransform(input, ctx) {
5282
+ if (!input.transforms || input.transforms.length === 0) {
5283
+ return { data: null, formatted: "", type: "unknown", action: "transform", error: "transforms array is required for action: transform" };
5284
+ }
5285
+ let parsed;
5286
+ if (input.file) {
5287
+ try {
5288
+ const raw = await fs7.readFile(await safeResolveReal(input.file, ctx), "utf8");
5289
+ parsed = JSON.parse(raw);
5290
+ } catch {
5291
+ return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not read/parse file" };
5292
+ }
5293
+ } else if (input.data) {
5294
+ try {
5295
+ parsed = JSON.parse(input.data);
5296
+ } catch {
5297
+ return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not parse data string" };
5298
+ }
5299
+ } else {
5300
+ return { data: null, formatted: "", type: "unknown", action: "transform", error: "Provide file or data" };
5301
+ }
5302
+ try {
5303
+ let current = parsed;
5304
+ const steps = [];
5305
+ for (const t of input.transforms) {
5306
+ current = jmespathSearch(current, t);
5307
+ steps.push({ transform: t, result: current });
5308
+ }
5309
+ const format = input.format ?? "json";
5310
+ return {
5311
+ data: parsed,
5312
+ formatted: formatOutput(current, format),
5313
+ type: current === null ? "null" : Array.isArray(current) ? "array" : typeof current,
5314
+ action: "transform",
5315
+ result: current,
5316
+ steps
5317
+ };
5318
+ } catch (e) {
5319
+ return {
5320
+ data: null,
5321
+ formatted: "",
5322
+ type: "unknown",
5323
+ action: "transform",
5324
+ /* v8 ignore next -- defensive String(e) */
5325
+ error: `Transform failed: ${e instanceof Error ? e.message : String(e)}`
5326
+ };
5327
+ }
5328
+ }
5329
+ async function executeMerge(input) {
5330
+ if (input.base === void 0 || input.patch === void 0) {
5331
+ return { data: null, formatted: "", type: "unknown", action: "merge", error: "base and patch are required for action: merge" };
5332
+ }
5333
+ const conflictResolution = input.conflictResolution ?? "prefer-patch";
5334
+ try {
5335
+ const result = deepMerge(input.base, input.patch, { conflictResolution });
5336
+ const format = input.format ?? "json";
5337
+ return {
5338
+ data: result,
5339
+ formatted: formatOutput(result, format),
5340
+ type: result === null ? "null" : Array.isArray(result) ? "array" : typeof result,
5341
+ action: "merge",
5342
+ result
5343
+ };
5344
+ } catch (e) {
5345
+ return {
5346
+ data: null,
5347
+ formatted: "",
5348
+ type: "unknown",
5349
+ action: "merge",
5350
+ /* v8 ignore next -- defensive String(e) */
5351
+ error: `Merge failed: ${e instanceof Error ? e.message : String(e)}`
5352
+ };
5353
+ }
5354
+ }
5355
+ function jmespathSearch(data, query) {
5356
+ if (!query || query === "@") return data;
5357
+ if (query === "$") return data;
5358
+ const dotMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(?:\.(.+))?$/);
5359
+ if (dotMatch) {
5360
+ const key = dotMatch[1];
5361
+ const rest = dotMatch[2];
5362
+ const val = data?.[key];
5363
+ if (rest === void 0) return val;
5364
+ return jmespathSearch(val, rest);
5365
+ }
5366
+ const arrMatch = query.match(/^\[(\d+)\](?:\.(.+))?$/);
5367
+ if (arrMatch) {
5368
+ const idx = Number.parseInt(arrMatch[1], 10);
5369
+ const rest = arrMatch[2];
5370
+ const arr = data;
5371
+ const val = arr?.[idx];
5372
+ if (rest === void 0) return val;
5373
+ return jmespathSearch(val, rest);
5374
+ }
5375
+ if (query === "[*]") {
5376
+ if (Array.isArray(data)) {
5377
+ return data;
5378
+ }
5379
+ return data;
5380
+ }
5381
+ const multiMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\[\*\](?:\.(.+))?$/);
5382
+ if (multiMatch) {
5383
+ const key = multiMatch[1];
5384
+ const rest = multiMatch[2];
5385
+ const arr = data?.[key];
5386
+ if (!Array.isArray(arr)) return [];
5387
+ if (rest === void 0) return arr;
5388
+ return arr.map((item) => jmespathSearch(item, rest));
5389
+ }
5390
+ const filterMatch = query.match(/^\[\\?([a-zA-Z_][a-zA-Z0-9_]*)(==|!=|<|>|<=|>=)(`[^`]+`|'[^']*')\](?:\.(.+))?$/);
5391
+ if (filterMatch) {
5392
+ const field = filterMatch[1];
5393
+ const op = filterMatch[2];
5394
+ const rawVal = filterMatch[3];
5395
+ const rest = filterMatch[4];
5396
+ const cmpVal = JSON.parse(rawVal.slice(1, -1));
5397
+ const arr = data;
5398
+ if (!Array.isArray(arr)) return [];
5399
+ const filtered = arr.filter((item) => {
5400
+ const itemVal = item[field];
5401
+ switch (op) {
5402
+ case "==":
5403
+ return itemVal === cmpVal;
5404
+ case "!=":
5405
+ return itemVal !== cmpVal;
5406
+ case ">":
5407
+ return Number(itemVal) > Number(cmpVal);
5408
+ case "<":
5409
+ return Number(itemVal) < Number(cmpVal);
5410
+ case ">=":
5411
+ return Number(itemVal) >= Number(cmpVal);
5412
+ case "<=":
5413
+ return Number(itemVal) <= Number(cmpVal);
5414
+ /* v8 ignore next -- op is constrained to the six operators by the filter regex; default is unreachable. */
5415
+ default:
5416
+ return true;
5417
+ }
5418
+ });
5419
+ if (rest === void 0) return filtered;
5420
+ return filtered.map((item) => jmespathSearch(item, rest));
5421
+ }
5422
+ const fnMatch = query.match(/^(length|keys|values|type)\(@\)$/);
5423
+ if (fnMatch) {
5424
+ const fn = fnMatch[1];
5425
+ switch (fn) {
5426
+ case "length":
5427
+ if (Array.isArray(data)) return data.length;
5428
+ if (typeof data === "string") return data.length;
5429
+ if (typeof data === "object" && data !== null) return Object.keys(data).length;
5430
+ return 0;
5431
+ case "keys":
5432
+ if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.keys(data);
5433
+ return [];
5434
+ case "values":
5435
+ if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.values(data);
5436
+ return [];
5437
+ case "type":
5438
+ if (data === null) return "null";
5439
+ if (Array.isArray(data)) return "array";
5440
+ return typeof data;
5441
+ /* v8 ignore next 2 -- fn is constrained to the four names by the function regex; default is unreachable. */
5442
+ default:
5443
+ return null;
5444
+ }
5445
+ }
5446
+ return null;
5447
+ }
5448
+ function validateJsonSchema(data, schema) {
5449
+ const errors = [];
5450
+ function check(value, s, path22) {
5451
+ if (s["type"]) {
5452
+ const expectedType = s["type"];
5453
+ const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
5454
+ if (expectedType === "integer") {
5455
+ if (!Number.isInteger(value)) errors.push(`${path22}: expected integer, got ${actualType}`);
5456
+ } else if (expectedType !== actualType) {
5457
+ errors.push(`${path22}: expected ${expectedType}, got ${actualType}`);
5458
+ }
5459
+ }
5460
+ if (typeof value === "string" && s["format"] === "uri" && value) {
5461
+ try {
5462
+ new URL(value);
5463
+ } catch {
5464
+ errors.push(`${path22}: not a valid URI`);
5465
+ }
5466
+ }
5467
+ if (typeof value === "string" && s["pattern"]) {
5468
+ const re = new RegExp(s["pattern"]);
5469
+ if (!re.test(value)) errors.push(`${path22}: does not match pattern ${s["pattern"]}`);
5470
+ }
5471
+ if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
5472
+ errors.push(`${path22}: string too short (min ${s["minLength"]})`);
5473
+ }
5474
+ if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
5475
+ errors.push(`${path22}: string too long (max ${s["maxLength"]})`);
5476
+ }
5477
+ if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
5478
+ errors.push(`${path22}: below minimum ${s["minimum"]}`);
5479
+ }
5480
+ if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
5481
+ errors.push(`${path22}: above maximum ${s["maximum"]}`);
5482
+ }
5483
+ if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
5484
+ for (let i = 0; i < value.length; i++) {
5485
+ check(value[i], s["items"], `${path22}[${i}]`);
5486
+ }
5487
+ }
5488
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
5489
+ const props = s["properties"];
5490
+ for (const [k, propSchema] of Object.entries(props)) {
5491
+ check(value[k], propSchema, `${path22}.${k}`);
5492
+ }
5493
+ }
5494
+ }
5495
+ check(data, schema, "$");
5496
+ return { valid: errors.length === 0, errors };
5497
+ }
5498
+ function simpleQuery(data, path22) {
4879
5499
  const parts = path22.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
4880
5500
  let current = data;
4881
5501
  for (const part of parts) {
@@ -5017,7 +5637,7 @@ function findGitDir2(cwd) {
5017
5637
  return null;
5018
5638
  }
5019
5639
  function runGit2(args, cwd, signal) {
5020
- return new Promise((resolve6) => {
5640
+ return new Promise((resolve7) => {
5021
5641
  let stdout = "";
5022
5642
  let stderr = "";
5023
5643
  const child = spawn("git", args, {
@@ -5033,8 +5653,8 @@ function runGit2(args, cwd, signal) {
5033
5653
  child.stderr?.on("data", (c) => {
5034
5654
  stderr += c.toString();
5035
5655
  });
5036
- child.on("close", (code) => resolve6({ stdout, stderr, exitCode: code ?? 0 }));
5037
- child.on("error", (e) => resolve6({ stdout: "", stderr: e.message, exitCode: 1 }));
5656
+ child.on("close", (code) => resolve7({ stdout, stderr, exitCode: code ?? 0 }));
5657
+ child.on("error", (e) => resolve7({ stdout: "", stderr: e.message, exitCode: 1 }));
5038
5658
  });
5039
5659
  }
5040
5660
  async function fileDiff(input, ctx, _signal) {
@@ -5355,8 +5975,8 @@ async function* spawnStream(opts) {
5355
5975
  try {
5356
5976
  for (; ; ) {
5357
5977
  while (queue.length === 0) {
5358
- await new Promise((resolve6) => {
5359
- waiter = resolve6;
5978
+ await new Promise((resolve7) => {
5979
+ waiter = resolve7;
5360
5980
  });
5361
5981
  }
5362
5982
  const chunk = queue.shift();
@@ -5889,6 +6509,10 @@ var installTool = {
5889
6509
  global: {
5890
6510
  type: "boolean",
5891
6511
  description: "Whether to perform a global install (use with caution)."
6512
+ },
6513
+ lifecycleScripts: {
6514
+ type: "boolean",
6515
+ 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."
5892
6516
  }
5893
6517
  }
5894
6518
  },
@@ -5908,8 +6532,10 @@ var installTool = {
5908
6532
  yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
5909
6533
  const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
5910
6534
  const globalFlag = input.global ? ["-g"] : [];
6535
+ const ignoreScripts = input.lifecycleScripts !== true;
5911
6536
  const args = [];
5912
6537
  if (input.dry_run) args.push("--dry-run");
6538
+ if (ignoreScripts) args.push("--ignore-scripts");
5913
6539
  if (pkgManager === "pnpm") {
5914
6540
  if (save) args.push(save);
5915
6541
  args.push("add", ...globalFlag);
@@ -6114,8 +6740,8 @@ var outdatedTool = {
6114
6740
  // read-only, but `outdated` makes outbound HTTP calls to the
6115
6741
  // registry. The 'confirm' permission routes the call through the
6116
6742
  // tool.confirm_needed flow on every invocation. M-1 originally
6117
- // fixed four sibling tools (mcp_control, shellcheck, shellcheck_scan,
6118
- // web_search) but missed this one; applying the same contract here.
6743
+ // fixed four sibling tools (mcp_control, shellcheck, shellcheck (scan mode),
6744
+ // search) but missed this one; applying the same contract here.
6119
6745
  mutating: true,
6120
6746
  // Capability is outbound network — the tool only hits the package
6121
6747
  // registry over HTTP, never touches the filesystem or runs shell.
@@ -6156,7 +6782,7 @@ var outdatedTool = {
6156
6782
  }
6157
6783
  };
6158
6784
  function runOutdated(manager, args, cwd, signal) {
6159
- return new Promise((resolve6) => {
6785
+ return new Promise((resolve7) => {
6160
6786
  let stdout = "";
6161
6787
  let stderr = "";
6162
6788
  const MAX = 1e5;
@@ -6173,10 +6799,10 @@ function runOutdated(manager, args, cwd, signal) {
6173
6799
  });
6174
6800
  child.on("close", (code) => {
6175
6801
  const result = parseOutdatedOutput(stdout, code ?? 0);
6176
- resolve6(result);
6802
+ resolve7(result);
6177
6803
  });
6178
6804
  child.on("error", (e) => {
6179
- resolve6({
6805
+ resolve7({
6180
6806
  exit_code: 1,
6181
6807
  packages: [],
6182
6808
  total: 0,
@@ -6302,7 +6928,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
6302
6928
  };
6303
6929
  }
6304
6930
  args.push("--timestamps", service);
6305
- return new Promise((resolve6) => {
6931
+ return new Promise((resolve7) => {
6306
6932
  let stdout = "";
6307
6933
  let stderr = "";
6308
6934
  const MAX = 2e5;
@@ -6318,7 +6944,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
6318
6944
  if (settled) return;
6319
6945
  settled = true;
6320
6946
  clearTimeout(timer);
6321
- resolve6(result);
6947
+ resolve7(result);
6322
6948
  };
6323
6949
  const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
6324
6950
  const timer = setTimeout(() => {
@@ -6428,8 +7054,8 @@ function parseLine(line) {
6428
7054
  var documentTool = {
6429
7055
  name: "document",
6430
7056
  category: "Project",
6431
- 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.",
6432
- 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.",
7057
+ 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.",
7058
+ 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.",
6433
7059
  permission: "auto",
6434
7060
  mutating: false,
6435
7061
  timeoutMs: 3e4,
@@ -6912,7 +7538,14 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
6912
7538
  kitId: active.kit,
6913
7539
  outPath: input.out
6914
7540
  });
6915
- const abs = path.join(ctx.projectRoot, result.path);
7541
+ const root = path.resolve(ctx.projectRoot);
7542
+ const abs = path.resolve(path.join(ctx.projectRoot, result.path));
7543
+ const rel = path.relative(root, abs);
7544
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
7545
+ throw new Error(
7546
+ `design: materialize path "${result.path}" would escape the project root`
7547
+ );
7548
+ }
6916
7549
  let exists = false;
6917
7550
  try {
6918
7551
  await fs7.access(abs);
@@ -6983,8 +7616,8 @@ Replace off-palette colors with kit tokens (or the materialized CSS vars / token
6983
7616
  var toolSearchTool = {
6984
7617
  name: "tool_search",
6985
7618
  category: "Meta",
6986
- description: "Search the catalog of available tools. Very useful when you are unsure which tool to use for a task.",
6987
- 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.",
7619
+ 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.",
7620
+ 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.",
6988
7621
  permission: "auto",
6989
7622
  mutating: false,
6990
7623
  timeoutMs: 1e3,
@@ -7022,9 +7655,9 @@ var toolSearchTool = {
7022
7655
  async execute(input, ctx) {
7023
7656
  const limit = Math.min(input.limit ?? 20, 100);
7024
7657
  const tools = ctx.tools;
7025
- const query2 = input.query?.toLowerCase() ?? "";
7658
+ const query = input.query?.toLowerCase() ?? "";
7026
7659
  const filtered = tools.filter((t) => {
7027
- if (query2 && !t.name.toLowerCase().includes(query2) && !t.description.toLowerCase().includes(query2)) {
7660
+ if (query && !t.name.toLowerCase().includes(query) && !t.description.toLowerCase().includes(query)) {
7028
7661
  return false;
7029
7662
  }
7030
7663
  if (input.tags && input.tags.length > 0) {
@@ -7048,7 +7681,7 @@ var toolSearchTool = {
7048
7681
  mutating: t.mutating
7049
7682
  }));
7050
7683
  const totalAvailable = tools.length;
7051
- const hint = results.length === 0 && query2 ? `No tools matched "${input.query}". Use tool-help (without arguments) to see all ${totalAvailable} available tools.` : void 0;
7684
+ const hint = results.length === 0 && query ? `No tools matched "${input.query}". Use tool-help (without arguments) to see all ${totalAvailable} available tools.` : void 0;
7052
7685
  return {
7053
7686
  tools: results,
7054
7687
  total: filtered.length,
@@ -7240,8 +7873,8 @@ async function executeSingle(call, ctx, opts) {
7240
7873
  var toolHelpTool = {
7241
7874
  name: "tool_help",
7242
7875
  category: "Meta",
7243
- 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.",
7244
- 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.",
7876
+ 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.",
7877
+ 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.",
7245
7878
  permission: "auto",
7246
7879
  mutating: false,
7247
7880
  timeoutMs: 5e3,
@@ -7359,8 +7992,6 @@ function formatAllToolsMarkdown(tools) {
7359
7992
  }
7360
7993
  return lines.join("\n");
7361
7994
  }
7362
-
7363
- // src/memory.ts
7364
7995
  function rememberTool(memory) {
7365
7996
  return {
7366
7997
  name: "remember",
@@ -7403,7 +8034,12 @@ function rememberTool(memory) {
7403
8034
  required: ["text"]
7404
8035
  },
7405
8036
  async execute(input) {
7406
- if (!input?.text) throw new Error("remember: text is required");
8037
+ if (!input?.text) {
8038
+ throw new ToolValidationError({
8039
+ message: "remember: text is required",
8040
+ field: "text"
8041
+ });
8042
+ }
7407
8043
  const scope = input.scope ?? "project-memory";
7408
8044
  await memory.remember(input.text, scope, {
7409
8045
  type: input.type,
@@ -7433,7 +8069,12 @@ function forgetTool(memory) {
7433
8069
  required: ["query"]
7434
8070
  },
7435
8071
  async execute(input) {
7436
- if (!input?.query) throw new Error("forget: query is required");
8072
+ if (!input?.query) {
8073
+ throw new ToolValidationError({
8074
+ message: "forget: query is required",
8075
+ field: "query"
8076
+ });
8077
+ }
7437
8078
  const scope = input.scope ?? "project-memory";
7438
8079
  const removed = await memory.forget(input.query, scope);
7439
8080
  return { removed, scope };
@@ -7470,7 +8111,12 @@ function searchMemoryTool(memory) {
7470
8111
  required: ["query"]
7471
8112
  },
7472
8113
  async execute(input) {
7473
- if (!input?.query) throw new Error("search_memory: query is required");
8114
+ if (!input?.query) {
8115
+ throw new ToolValidationError({
8116
+ message: "search_memory: query is required",
8117
+ field: "query"
8118
+ });
8119
+ }
7474
8120
  const scope = input.scope ?? "project-memory";
7475
8121
  const limit = Math.min(input.limit ?? 5, 20);
7476
8122
  const entries = await memory.search(input.query, scope, limit);
@@ -7517,7 +8163,12 @@ function relatedMemoryTool(memory) {
7517
8163
  required: ["text"]
7518
8164
  },
7519
8165
  async execute(input) {
7520
- if (!input?.text) throw new Error("find_related_memories: text is required");
8166
+ if (!input?.text) {
8167
+ throw new ToolValidationError({
8168
+ message: "find_related_memories: text is required",
8169
+ field: "text"
8170
+ });
8171
+ }
7521
8172
  const scope = input.scope ?? "project-memory";
7522
8173
  const limit = Math.min(input.limit ?? 5, 20);
7523
8174
  const entries = memory.findRelated ? await memory.findRelated(input.text, scope, limit) : await memory.search(input.text, scope, limit);
@@ -7758,16 +8409,23 @@ var ProcessGuardian = class {
7758
8409
  event: "process_guardian.uncaught_exception",
7759
8410
  error: err.message,
7760
8411
  stack: err.stack,
7761
- instanceId: this.instanceId
8412
+ instanceId: this.instanceId,
8413
+ fatal: true
7762
8414
  }));
8415
+ this.stop();
8416
+ process.exit(1);
7763
8417
  });
7764
8418
  process.on("unhandledRejection", (reason) => {
8419
+ const err = reason instanceof Error ? { message: reason.message, stack: reason.stack } : { value: String(reason) };
7765
8420
  console.error(JSON.stringify({
7766
8421
  level: "error",
7767
8422
  event: "process_guardian.unhandled_rejection",
7768
- reason: String(reason),
7769
- instanceId: this.instanceId
8423
+ ...err,
8424
+ instanceId: this.instanceId,
8425
+ fatal: true
7770
8426
  }));
8427
+ this.stop();
8428
+ process.exit(1);
7771
8429
  });
7772
8430
  process.on("SIGTERM", (origin) => {
7773
8431
  console.log(JSON.stringify({
@@ -8308,8 +8966,8 @@ var Bm25Index = class {
8308
8966
  df;
8309
8967
  N;
8310
8968
  safeAvgLen;
8311
- score(query2, filter) {
8312
- const qTokens = tokenise(query2);
8969
+ score(query, filter) {
8970
+ const qTokens = tokenise(query);
8313
8971
  if (qTokens.length === 0) return [];
8314
8972
  const results = [];
8315
8973
  for (const doc of this.documents) {
@@ -8632,7 +9290,7 @@ var IndexStore = class {
8632
9290
  );
8633
9291
  }
8634
9292
  // ─── Search ──────────────────────────────────────────────────────────────────
8635
- search(query2, filter) {
9293
+ search(query, filter) {
8636
9294
  const conditions = [];
8637
9295
  const values = [];
8638
9296
  let effectiveKind = filter?.kind;
@@ -8656,8 +9314,8 @@ var IndexStore = class {
8656
9314
  conditions.push("file LIKE ?");
8657
9315
  values.push(`%${filter.file}%`);
8658
9316
  }
8659
- if (query2.trim()) {
8660
- const tokens = query2.toLowerCase().split(/\s+/).filter(Boolean);
9317
+ if (query.trim()) {
9318
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
8661
9319
  const tokenConds = tokens.map(() => "text LIKE ?");
8662
9320
  conditions.push(`(${tokenConds.join(" OR ")})`);
8663
9321
  for (const t of tokens) values.push(`%${t}%`);
@@ -8691,10 +9349,10 @@ var IndexStore = class {
8691
9349
  * `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
8692
9350
  * "users", camelCase-split text makes "complex" find "complexOperation").
8693
9351
  */
8694
- searchRanked(query2, filter, limit) {
8695
- const tokens = tokenise(query2);
9352
+ searchRanked(query, filter, limit) {
9353
+ const tokens = tokenise(query);
8696
9354
  if (tokens.length === 0 || !this.ftsAvailable) {
8697
- return this.searchRankedFallback(query2, filter, limit);
9355
+ return this.searchRankedFallback(query, filter, limit);
8698
9356
  }
8699
9357
  let effectiveKind = filter?.kind;
8700
9358
  if (filter?.lspKind !== void 0) {
@@ -8751,19 +9409,19 @@ var IndexStore = class {
8751
9409
  };
8752
9410
  }
8753
9411
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
8754
- searchRankedFallback(query2, filter, limit) {
8755
- const candidates = this.search(query2, filter);
9412
+ searchRankedFallback(query, filter, limit) {
9413
+ const candidates = this.search(query, filter);
8756
9414
  if (candidates.length === 0) return { results: [], total: 0 };
8757
- if (!query2.trim()) {
9415
+ if (!query.trim()) {
8758
9416
  return { results: candidates.slice(0, limit), total: candidates.length };
8759
9417
  }
8760
9418
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
8761
9419
  const bm25 = buildBm25Index(
8762
9420
  candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
8763
9421
  );
8764
- const scored = bm25.score(query2, (id) => candidateById.has(id));
9422
+ const scored = bm25.score(query, (id) => candidateById.has(id));
8765
9423
  scored.sort((a, b) => b.score - a.score);
8766
- const qTokens = tokenise(query2);
9424
+ const qTokens = tokenise(query);
8767
9425
  const results = scored.slice(0, limit).map(({ id, score }) => {
8768
9426
  const c = expectDefined(candidateById.get(id));
8769
9427
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
@@ -8874,6 +9532,104 @@ var IndexStore = class {
8874
9532
  }
8875
9533
  });
8876
9534
  }
9535
+ /**
9536
+ * Commit a batch of file-level symbol/refs/upserts in a single transaction.
9537
+ *
9538
+ * Used by the indexer to amortize SQLite commit overhead across many files.
9539
+ * Before this, the indexer issued one transaction per file (BEGIN IMMEDIATE
9540
+ * for symbols, plus per-file deletes and an upsertFile call), so a 20-file
9541
+ * parallel batch cost ~5+ transactions × 20 files = 100+ commits. With
9542
+ * this entry point we do exactly one BEGIN/COMMIT per parallel batch.
9543
+ *
9544
+ * Each entry must already be a fully-parsed FileSymbols (symbols + refs).
9545
+ * The caller is responsible for the per-file prefix accounting
9546
+ * (refsByLine → flat list with `fromId` populated). `deleteForFiles` lets
9547
+ * the caller clear stale symbols/refs for any files being re-indexed before
9548
+ * the inserts run (required to keep refs → symbols FK invariants).
9549
+ *
9550
+ * Returns the symbols back with their assigned `id` (same shape as
9551
+ * {@link insertSymbols}) so callers can build final per-file results.
9552
+ */
9553
+ commitBatch(entries, options = {}) {
9554
+ if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
9555
+ return [];
9556
+ }
9557
+ return this.runWithRetry(() => {
9558
+ this.db.exec("BEGIN IMMEDIATE");
9559
+ try {
9560
+ if (options.deleteForFiles && options.deleteForFiles.length > 0) {
9561
+ const placeholders = options.deleteForFiles.map(() => "?").join(",");
9562
+ if (this.ftsAvailable) {
9563
+ this.db.prepare(
9564
+ `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
9565
+ ).run(...options.deleteForFiles);
9566
+ }
9567
+ this.db.prepare(
9568
+ `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
9569
+ ).run(...options.deleteForFiles);
9570
+ this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
9571
+ }
9572
+ const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
9573
+ let nextId = (maxRows[0]?.m ?? 0) + 1;
9574
+ const symStmt = this.db.prepare(
9575
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
9576
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
9577
+ );
9578
+ const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
9579
+ const allInserted = [];
9580
+ const refsToInsert = [];
9581
+ for (const entry of entries) {
9582
+ for (const s of entry.symbols) {
9583
+ const id = nextId++;
9584
+ symStmt.run(
9585
+ id,
9586
+ s.lang,
9587
+ s.kind,
9588
+ s.name,
9589
+ s.file,
9590
+ s.line,
9591
+ s.col,
9592
+ s.signature,
9593
+ s.docComment,
9594
+ s.scope,
9595
+ s.text,
9596
+ s.file
9597
+ );
9598
+ ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
9599
+ allInserted.push({ ...s, id });
9600
+ }
9601
+ for (const r of entry.refs) refsToInsert.push(r);
9602
+ }
9603
+ if (refsToInsert.length > 0) {
9604
+ const refStmt = this.db.prepare(
9605
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
9606
+ VALUES (?, ?, ?, ?, ?)`
9607
+ );
9608
+ for (const ref of refsToInsert) {
9609
+ refStmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
9610
+ }
9611
+ }
9612
+ const upsertStmt = this.db.prepare(
9613
+ `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
9614
+ VALUES (?, ?, ?, ?, ?)
9615
+ ON CONFLICT(file) DO UPDATE SET
9616
+ lang = excluded.lang,
9617
+ mtime_ms = excluded.mtime_ms,
9618
+ symbol_count = excluded.symbol_count,
9619
+ last_indexed = excluded.last_indexed`
9620
+ );
9621
+ const now2 = Date.now();
9622
+ for (const entry of entries) {
9623
+ upsertStmt.run(entry.file, entry.lang, entry.mtimeMs, entry.symbolCount, now2);
9624
+ }
9625
+ this.db.exec("COMMIT");
9626
+ return allInserted;
9627
+ } catch (err) {
9628
+ this.db.exec("ROLLBACK");
9629
+ throw err;
9630
+ }
9631
+ });
9632
+ }
8877
9633
  /**
8878
9634
  * Delete all refs whose source symbols are in a given file.
8879
9635
  * Used when re-indexing a file to clear stale refs.
@@ -9390,8 +10146,8 @@ async function syncGoParse(filePath, content, lang) {
9390
10146
  proc.stdin?.write(content);
9391
10147
  proc.stdin?.end();
9392
10148
  const { code } = await Promise.race([
9393
- new Promise((resolve6) => {
9394
- proc.on("close", (c) => resolve6({ code: c }));
10149
+ new Promise((resolve7) => {
10150
+ proc.on("close", (c) => resolve7({ code: c }));
9395
10151
  }),
9396
10152
  new Promise(
9397
10153
  (_, reject) => setTimeout(() => {
@@ -9653,8 +10409,8 @@ async function syncPyParse(filePath, content, lang) {
9653
10409
  stdout += chunk.toString();
9654
10410
  });
9655
10411
  const { code } = await Promise.race([
9656
- new Promise((resolve6) => {
9657
- proc.on("close", (c) => resolve6({ code: c }));
10412
+ new Promise((resolve7) => {
10413
+ proc.on("close", (c) => resolve7({ code: c }));
9658
10414
  }),
9659
10415
  new Promise(
9660
10416
  (_, reject) => setTimeout(() => {
@@ -9739,8 +10495,8 @@ async function tryNativeParse(file, content) {
9739
10495
  stdout += chunk.toString();
9740
10496
  });
9741
10497
  const { code } = await Promise.race([
9742
- new Promise((resolve6) => {
9743
- proc.on("close", (c) => resolve6({ code: c }));
10498
+ new Promise((resolve7) => {
10499
+ proc.on("close", (c) => resolve7({ code: c }));
9744
10500
  }),
9745
10501
  new Promise(
9746
10502
  (_, reject) => setTimeout(() => {
@@ -10230,7 +10986,7 @@ async function loadGitignoreMatcher(projectRoot) {
10230
10986
  var YIELD_EVERY_N = 50;
10231
10987
  var PARALLEL_BATCH = 20;
10232
10988
  function yieldEventLoop() {
10233
- return new Promise((resolve6) => setImmediate(resolve6));
10989
+ return new Promise((resolve7) => setImmediate(resolve7));
10234
10990
  }
10235
10991
  function throwIfAborted(signal) {
10236
10992
  if (!signal?.aborted) return;
@@ -10405,6 +11161,8 @@ async function runIndexerWithStore(store, opts) {
10405
11161
  return { file, stat: stat11, lang, parsed, content };
10406
11162
  })
10407
11163
  );
11164
+ const batchEntries = [];
11165
+ const deleteForFiles = [];
10408
11166
  for (let fi = 0; fi < statReadParse.length; fi++) {
10409
11167
  const settled = statReadParse[fi];
10410
11168
  const file = expectDefined(batchFiles[fi]);
@@ -10429,43 +11187,116 @@ async function runIndexerWithStore(store, opts) {
10429
11187
  }
10430
11188
  if (!lang || !parsed) {
10431
11189
  if (lang) {
10432
- store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: 0, lastIndexed: Date.now() });
11190
+ store.upsertFile({
11191
+ file,
11192
+ lang,
11193
+ mtimeMs: Math.floor(stat11.mtimeMs),
11194
+ symbolCount: 0,
11195
+ lastIndexed: Date.now()
11196
+ });
10433
11197
  filesIndexed++;
10434
11198
  }
10435
11199
  continue;
10436
11200
  }
10437
- store.deleteRefsForFile(file);
10438
- store.deleteSymbolsForFile(file);
10439
11201
  if (parsed.symbols.length === 0) {
10440
- store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: 0, lastIndexed: Date.now() });
11202
+ store.upsertFile({
11203
+ file,
11204
+ lang,
11205
+ mtimeMs: Math.floor(stat11.mtimeMs),
11206
+ symbolCount: 0,
11207
+ lastIndexed: Date.now()
11208
+ });
10441
11209
  filesIndexed++;
10442
11210
  continue;
10443
11211
  }
10444
- const symbolsWithIds = store.insertSymbols(parsed.symbols);
10445
- const count = symbolsWithIds.length;
10446
- symbolsIndexed += count;
10447
- langStats[lang] = (langStats[lang] ?? 0) + count;
11212
+ const refs = [];
10448
11213
  if (parsed.refs && parsed.refs.length > 0) {
10449
- const refsByLine = /* @__PURE__ */ new Map();
10450
- for (const r of parsed.refs) {
10451
- let arr = refsByLine.get(r.line);
10452
- if (!arr) {
10453
- arr = [];
10454
- refsByLine.set(r.line, arr);
11214
+ for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
11215
+ }
11216
+ batchEntries.push({
11217
+ file,
11218
+ lang,
11219
+ symbols: parsed.symbols,
11220
+ refs,
11221
+ mtimeMs: Math.floor(stat11.mtimeMs),
11222
+ symbolCount: parsed.symbols.length
11223
+ });
11224
+ deleteForFiles.push(file);
11225
+ }
11226
+ if (batchEntries.length > 0) {
11227
+ try {
11228
+ const inserted = store.commitBatch(batchEntries, { deleteForFiles });
11229
+ let cursor = 0;
11230
+ for (const entry of batchEntries) {
11231
+ const count = entry.symbols.length;
11232
+ const symbolsWithIds = inserted.slice(cursor, cursor + count);
11233
+ cursor += count;
11234
+ symbolsIndexed += count;
11235
+ langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
11236
+ filesIndexed++;
11237
+ if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
11238
+ const refsByLine = /* @__PURE__ */ new Map();
11239
+ for (let i = 0; i < symbolsWithIds.length; i++) {
11240
+ const sym = symbolsWithIds[i];
11241
+ let arr = refsByLine.get(sym.line);
11242
+ if (!arr) {
11243
+ arr = [];
11244
+ refsByLine.set(sym.line, arr);
11245
+ }
11246
+ arr.push(i);
11247
+ }
11248
+ for (const ref of entry.refs) {
11249
+ const indices = refsByLine.get(ref.line);
11250
+ if (indices && indices.length > 0) {
11251
+ const idx = indices.shift();
11252
+ ref.fromId = symbolsWithIds[idx].id;
11253
+ }
11254
+ }
10455
11255
  }
10456
- arr.push(r);
10457
11256
  }
10458
- const batch = [];
10459
- for (const sym of symbolsWithIds) {
10460
- const symRefs = refsByLine.get(sym.line);
10461
- if (symRefs) {
10462
- for (const r of symRefs) batch.push({ ...r, fromId: sym.id });
11257
+ } catch (err) {
11258
+ const message = err instanceof Error ? err.message : String(err);
11259
+ errors.push(`commitBatch failed: ${message} \u2014 falling back to per-file writes`);
11260
+ for (const entry of batchEntries) {
11261
+ try {
11262
+ store.deleteRefsForFile(entry.file);
11263
+ store.deleteSymbolsForFile(entry.file);
11264
+ const symbolsWithIds = store.insertSymbols(entry.symbols);
11265
+ symbolsIndexed += symbolsWithIds.length;
11266
+ langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
11267
+ filesIndexed++;
11268
+ if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
11269
+ const refsByLine = /* @__PURE__ */ new Map();
11270
+ for (const sym of symbolsWithIds) {
11271
+ let arr = refsByLine.get(sym.line);
11272
+ if (!arr) {
11273
+ arr = [];
11274
+ refsByLine.set(sym.line, arr);
11275
+ }
11276
+ arr.push(sym);
11277
+ }
11278
+ const fallbackBatch = [];
11279
+ for (const ref of entry.refs) {
11280
+ const syms = refsByLine.get(ref.line);
11281
+ if (syms && syms.length > 0) {
11282
+ const sym = syms.shift();
11283
+ fallbackBatch.push({ ...ref, fromId: sym.id });
11284
+ }
11285
+ }
11286
+ if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
11287
+ }
11288
+ store.upsertFile({
11289
+ file: entry.file,
11290
+ lang: entry.lang,
11291
+ mtimeMs: entry.mtimeMs,
11292
+ symbolCount: entry.symbolCount,
11293
+ lastIndexed: Date.now()
11294
+ });
11295
+ } catch (innerErr) {
11296
+ errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
10463
11297
  }
10464
11298
  }
10465
- if (batch.length > 0) store.insertRefsBatch(batch);
10466
11299
  }
10467
- store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: count, lastIndexed: Date.now() });
10468
- filesIndexed++;
10469
11300
  }
10470
11301
  }
10471
11302
  if (discoveredFiles) {
@@ -10648,7 +11479,7 @@ function shutdownCodebaseIndexHost() {
10648
11479
  function callIndexOp(op, args, opts) {
10649
11480
  const w = ensureWorker();
10650
11481
  if (!w) return callInline(op, args, opts);
10651
- return new Promise((resolve6, reject) => {
11482
+ return new Promise((resolve7, reject) => {
10652
11483
  const id = nextRpcId++;
10653
11484
  const timer = setTimeout(() => {
10654
11485
  pending.delete(id);
@@ -10671,7 +11502,7 @@ function callIndexOp(op, args, opts) {
10671
11502
  pending.set(id, {
10672
11503
  resolve: (v) => {
10673
11504
  cleanup();
10674
- resolve6(v);
11505
+ resolve7(v);
10675
11506
  },
10676
11507
  reject: (e) => {
10677
11508
  cleanup();
@@ -10907,7 +11738,7 @@ var codebaseSearchTool = {
10907
11738
  name: "codebase-search",
10908
11739
  category: "Project",
10909
11740
  icon: "index",
10910
- 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.",
11741
+ 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).",
10911
11742
  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.",
10912
11743
  permission: "auto",
10913
11744
  mutating: false,
@@ -10941,6 +11772,10 @@ var codebaseSearchTool = {
10941
11772
  description: "Maximum results to return (default 20, max 100)",
10942
11773
  minimum: 1,
10943
11774
  maximum: 100
11775
+ },
11776
+ preferLsp: {
11777
+ type: "boolean",
11778
+ 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."
10944
11779
  }
10945
11780
  },
10946
11781
  required: ["query"]
@@ -11103,15 +11938,15 @@ var setWorkingDirTool = {
11103
11938
  };
11104
11939
  }
11105
11940
  };
11106
- function findTaskIndex(tasks, query2) {
11107
- const asNum = Number.parseInt(query2, 10);
11941
+ function findTaskIndex(tasks, query) {
11942
+ const asNum = Number.parseInt(query, 10);
11108
11943
  if (!Number.isNaN(asNum)) {
11109
11944
  const idx = asNum - 1;
11110
11945
  if (tasks[idx]) return idx;
11111
11946
  }
11112
- const byId = tasks.findIndex((t) => t.id === query2);
11947
+ const byId = tasks.findIndex((t) => t.id === query);
11113
11948
  if (byId >= 0) return byId;
11114
- const lower = query2.toLowerCase();
11949
+ const lower = query.toLowerCase();
11115
11950
  return tasks.findIndex((t) => t.title.toLowerCase().includes(lower));
11116
11951
  }
11117
11952
  var taskTool = {