@wrongstack/tools 0.300.0 → 0.302.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pack.js CHANGED
@@ -848,7 +848,7 @@ import {
848
848
  } from "@wrongstack/core/observability";
849
849
  import { buildChildEnv } from "@wrongstack/core/utils";
850
850
  async function* spawnStream(opts) {
851
- const max = opts.maxBytes ?? 2e5;
851
+ const max = opts.maxBytes ?? 999999999;
852
852
  const flushAt = opts.flushBytes ?? 4 * 1024;
853
853
  const maxQueue = opts.maxQueueSize ?? 500;
854
854
  const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
@@ -5921,16 +5921,23 @@ function looksBinary(content) {
5921
5921
  }
5922
5922
  return bad / sample.length > 0.1;
5923
5923
  }
5924
- function lineColAt(content, index) {
5925
- let line = 1;
5926
- let lastNl = -1;
5927
- for (let i = 0; i < index && i < content.length; i++) {
5928
- if (content.charCodeAt(i) === 10) {
5929
- line++;
5930
- lastNl = i;
5931
- }
5924
+ function newlineOffsets2(content) {
5925
+ const offsets = [];
5926
+ for (let i = 0; i < content.length; i++) {
5927
+ if (content.charCodeAt(i) === 10) offsets.push(i);
5932
5928
  }
5933
- return { line, col: index - lastNl };
5929
+ return offsets;
5930
+ }
5931
+ function lineColAt(offsets, index) {
5932
+ let low = 0;
5933
+ let high = offsets.length;
5934
+ while (low < high) {
5935
+ const mid = low + high >>> 1;
5936
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
5937
+ else high = mid;
5938
+ }
5939
+ const lastNl = low > 0 ? offsets[low - 1] : -1;
5940
+ return { line: low + 1, col: index - lastNl };
5934
5941
  }
5935
5942
  function parseGeneric2(opts) {
5936
5943
  const { file, lang } = opts;
@@ -5943,6 +5950,7 @@ function parseGeneric2(opts) {
5943
5950
  const patterns = patternsFor(lang);
5944
5951
  const symbols = [];
5945
5952
  const seen = /* @__PURE__ */ new Set();
5953
+ const nlOffsets = newlineOffsets2(content);
5946
5954
  for (const pattern of patterns) {
5947
5955
  const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
5948
5956
  re.lastIndex = 0;
@@ -5956,7 +5964,7 @@ function parseGeneric2(opts) {
5956
5964
  if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
5957
5965
  continue;
5958
5966
  }
5959
- const { line, col } = lineColAt(content, match.index);
5967
+ const { line, col } = lineColAt(nlOffsets, match.index ?? 0);
5960
5968
  const key = `${name}\0${line}\0${pattern.kind}`;
5961
5969
  if (seen.has(key)) continue;
5962
5970
  seen.add(key);
@@ -8751,7 +8759,9 @@ async function executeSingle(call, ctx, governedExecute) {
8751
8759
  executionMs: Date.now() - start
8752
8760
  };
8753
8761
  }
8754
- const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
8762
+ const tool = (ctx.catalogTools ?? ctx.tools).find(
8763
+ (candidate) => candidate.name === call.tool
8764
+ );
8755
8765
  if (!tool) {
8756
8766
  return {
8757
8767
  tool: call.tool,
@@ -8978,7 +8988,7 @@ function parsePrivateOriginAllowlist(raw) {
8978
8988
  if (!raw?.trim()) return [];
8979
8989
  const origins = /* @__PURE__ */ new Set();
8980
8990
  for (const entry of raw.split(",")) {
8981
- const candidate = entry.trim();
8991
+ const candidate = entry.trim().replace(/^["']+|["']+$/gu, "");
8982
8992
  if (!candidate) continue;
8983
8993
  const url = parseBrowserUrl(candidate, true);
8984
8994
  if (url.pathname !== "/" || url.search || url.hash) {
@@ -10211,9 +10221,9 @@ import * as path16 from "node:path";
10211
10221
  // src/codebase-index/bm25.ts
10212
10222
  var K1 = 1.5;
10213
10223
  var B = 0.75;
10224
+ var TOKENISE_RE = new RegExp("[^\\p{L}\\p{N}$']", "gu");
10214
10225
  function tokenise(text) {
10215
- const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
10216
- return sanitised.toLowerCase().split(" ").filter(Boolean);
10226
+ return text.replace(TOKENISE_RE, " ").toLowerCase().trim().split(/\s+/).filter(Boolean);
10217
10227
  }
10218
10228
  function splitName(name) {
10219
10229
  return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{L}])(\d)/gu, "$1 $2").replace(/(\d)([\p{L}])/gu, "$1 $2").replace(/[_-]+/g, " ").trim();
@@ -12699,7 +12709,9 @@ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
12699
12709
  var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
12700
12710
  var buildIdCache;
12701
12711
  function projectIndexServerBuildId(entrypoint) {
12702
- const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path17.resolve(entrypoint);
12712
+ const href = entrypoint instanceof URL ? entrypoint.href : entrypoint;
12713
+ const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
12714
+ const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path17.resolve(cleanHref);
12703
12715
  try {
12704
12716
  const stat18 = fs12.statSync(file);
12705
12717
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat18.mtimeMs && buildIdCache.size === stat18.size) {
@@ -19637,11 +19649,8 @@ function buildArgs(input) {
19637
19649
  ...input.branch.startsWith("-") || input.branch.includes(" --") ? [] : [input.branch]
19638
19650
  ] : ["branch"];
19639
19651
  case "checkout":
19640
- return [
19641
- "checkout",
19642
- ...input.branch ? ["--", input.branch] : [],
19643
- ...files.length ? ["--", ...files] : []
19644
- ];
19652
+ if (files.length) return ["checkout", "--", ...files];
19653
+ return input.branch ? ["checkout", input.branch, "--"] : ["checkout"];
19645
19654
  case "stash":
19646
19655
  return input.message ? ["stash", "push", "-m", input.message] : ["stash", "push"];
19647
19656
  case "push":
@@ -19746,7 +19755,7 @@ async function mapWithConcurrency2(items, limit, fn) {
19746
19755
 
19747
19756
  // src/glob.ts
19748
19757
  init_util();
19749
- var DEFAULT_IGNORE2 = DEFAULT_WALK_IGNORE_DIRS2;
19758
+ var DEFAULT_IGNORE2 = new Set(DEFAULT_WALK_IGNORE_DIRS2);
19750
19759
  var WALK_CONCURRENCY = 16;
19751
19760
  var globTool = {
19752
19761
  name: "glob",
@@ -19825,7 +19834,7 @@ var globTool = {
19825
19834
  const matchedFiles = [];
19826
19835
  for (const e of entries) {
19827
19836
  const name = e.name;
19828
- if (DEFAULT_IGNORE2.includes(name)) continue;
19837
+ if (DEFAULT_IGNORE2.has(name)) continue;
19829
19838
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
19830
19839
  const full = path30.join(dir, name);
19831
19840
  if (e.isDirectory()) {
@@ -19899,6 +19908,81 @@ var DANGEROUS_PATTERNS = [
19899
19908
  // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
19900
19909
  /[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
19901
19910
  ];
19911
+ function hasAmbiguousQuantifiedAlternation(pattern) {
19912
+ for (let i = 0; i < pattern.length; i++) {
19913
+ if (pattern[i] !== "(") continue;
19914
+ if (i > 0 && pattern[i - 1] === "\\") continue;
19915
+ let depth = 0;
19916
+ let inClass = false;
19917
+ let j = i;
19918
+ for (; j < pattern.length; j++) {
19919
+ const ch = pattern[j];
19920
+ if (ch === "\\") {
19921
+ j++;
19922
+ continue;
19923
+ }
19924
+ if (inClass) {
19925
+ if (ch === "]") inClass = false;
19926
+ continue;
19927
+ }
19928
+ if (ch === "[") {
19929
+ inClass = true;
19930
+ continue;
19931
+ }
19932
+ if (ch === "(") depth++;
19933
+ else if (ch === ")") {
19934
+ depth--;
19935
+ if (depth === 0) break;
19936
+ }
19937
+ }
19938
+ if (j >= pattern.length) return false;
19939
+ const next = pattern[j + 1];
19940
+ if (next !== "+" && next !== "*" && next !== "{") continue;
19941
+ let inner = pattern.slice(i + 1, j);
19942
+ inner = inner.replace(/^\?(?::|<?[=!])/u, "");
19943
+ const branches = [];
19944
+ let current = "";
19945
+ let d = 0;
19946
+ let cls = false;
19947
+ for (let k = 0; k < inner.length; k++) {
19948
+ const ch = inner[k];
19949
+ if (ch === "\\") {
19950
+ current += ch + (inner[k + 1] ?? "");
19951
+ k++;
19952
+ continue;
19953
+ }
19954
+ if (cls) {
19955
+ if (ch === "]") cls = false;
19956
+ current += ch;
19957
+ continue;
19958
+ }
19959
+ if (ch === "[") {
19960
+ cls = true;
19961
+ current += ch;
19962
+ continue;
19963
+ }
19964
+ if (ch === "(") d++;
19965
+ if (ch === ")") d--;
19966
+ if (ch === "|" && d === 0) {
19967
+ branches.push(current);
19968
+ current = "";
19969
+ continue;
19970
+ }
19971
+ current += ch;
19972
+ }
19973
+ branches.push(current);
19974
+ if (branches.length < 2) continue;
19975
+ for (let a = 0; a < branches.length; a++) {
19976
+ for (let b = a + 1; b < branches.length; b++) {
19977
+ const x = branches[a];
19978
+ const y = branches[b];
19979
+ if (x === "" || y === "") return true;
19980
+ if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
19981
+ }
19982
+ }
19983
+ }
19984
+ return false;
19985
+ }
19902
19986
  function compileUserRegex(pattern, flags) {
19903
19987
  if (typeof pattern !== "string") {
19904
19988
  return { ok: false, reason: "pattern must be a string" };
@@ -19917,6 +20001,12 @@ function compileUserRegex(pattern, flags) {
19917
20001
  };
19918
20002
  }
19919
20003
  }
20004
+ if (hasAmbiguousQuantifiedAlternation(pattern)) {
20005
+ return {
20006
+ ok: false,
20007
+ reason: "pattern quantifies an alternation with overlapping branches \u2014 rewrite so no two branches can match the same text"
20008
+ };
20009
+ }
19920
20010
  try {
19921
20011
  return { ok: true, regex: new RegExp(pattern, flags) };
19922
20012
  } catch (err) {
@@ -19933,7 +20023,7 @@ function capSubject(line) {
19933
20023
 
19934
20024
  // src/grep.ts
19935
20025
  init_util();
19936
- var DEFAULT_IGNORE3 = DEFAULT_WALK_IGNORE_DIRS3;
20026
+ var DEFAULT_IGNORE3 = new Set(DEFAULT_WALK_IGNORE_DIRS3);
19937
20027
  var NATIVE_SCAN_CONCURRENCY = 32;
19938
20028
  var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
19939
20029
  var NATIVE_MAX_FILE_BYTES = 1e6;
@@ -20006,7 +20096,7 @@ var grepTool = {
20006
20096
  field: "pattern"
20007
20097
  });
20008
20098
  }
20009
- const base = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
20099
+ const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
20010
20100
  const mode = input.output_mode ?? "content";
20011
20101
  const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
20012
20102
  const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
@@ -20331,7 +20421,7 @@ async function runNative(input, base, mode, limit, signal) {
20331
20421
  const subdirs = [];
20332
20422
  for (const e of entries) {
20333
20423
  if (stopped) return;
20334
- if (DEFAULT_IGNORE3.includes(e.name)) continue;
20424
+ if (DEFAULT_IGNORE3.has(e.name)) continue;
20335
20425
  if (e.isSymbolicLink()) continue;
20336
20426
  const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
20337
20427
  const full = path31.join(dir, e.name);
@@ -23392,12 +23482,12 @@ import { spawn as spawn13 } from "node:child_process";
23392
23482
  import * as fs27 from "node:fs/promises";
23393
23483
  import * as os9 from "node:os";
23394
23484
  import * as path32 from "node:path";
23395
- import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core/utils";
23485
+ import { buildChildEnv as buildChildEnv8, toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
23396
23486
  var patchTool = {
23397
23487
  name: "patch",
23398
23488
  category: "Filesystem",
23399
23489
  description: "Apply a unified diff (patch) to the project. This is the correct tool when you have a diff that needs to be applied precisely, including handling of rejects.",
23400
- usageHint: "Best used when you already have a diff (from generation, external source, or previous step).\n- Use `dry_run: true` to see what would happen without modifying files.\n- On failure it creates .rej and .orig files for manual review.\nOften cleaner than many small `edit` operations for larger changes.",
23490
+ usageHint: "Best used when you already have a diff (from generation, external source, or previous step).\n- Use `dry_run: true` to see what would happen without modifying files.\n- Applied with `--merge`: a conflicting hunk writes git-style conflict\n markers (<<<<<<< / ======= / >>>>>>>) INTO the file and reports failure.\n It does NOT create .rej/.orig files. `files` lists what changed on disk\n even when the patch failed, so read those back before retrying.\nOften cleaner than many small `edit` operations for larger changes.",
23401
23491
  selection: {
23402
23492
  doNotUseWhen: "you do not already have a unified diff or only need one precise replacement.",
23403
23493
  useInstead: ["edit"]
@@ -23423,31 +23513,50 @@ var patchTool = {
23423
23513
  },
23424
23514
  async execute(input, ctx, opts) {
23425
23515
  if (!input?.patch) throw new Error("patch: patch content is required");
23426
- const dir = input.directory ? safeResolve(input.directory, ctx) : ctx.cwd;
23427
23516
  const strip = Math.max(1, input.strip ?? 1);
23428
23517
  const dryRun = input.dry_run ?? false;
23518
+ const refuse = (message) => ({
23519
+ applied: 0,
23520
+ rejected: 1,
23521
+ files: [],
23522
+ dry_run: dryRun,
23523
+ message
23524
+ });
23525
+ let dir;
23526
+ try {
23527
+ dir = input.directory ? await safeResolveReal(input.directory, ctx) : ctx.cwd;
23528
+ } catch (err) {
23529
+ return refuse(`patch refused: ${toErrorMessage4(err)}`);
23530
+ }
23531
+ const realRoot = await fs27.realpath(ctx.projectRoot).catch(() => path32.resolve(ctx.projectRoot));
23429
23532
  const targets = extractDiffTargets(input.patch);
23430
23533
  const resolvedTargets = [];
23431
23534
  for (const t of targets) {
23432
- const stripped = stripPathComponents(t, strip);
23535
+ const stripped = stripPathComponents(t.raw, strip);
23433
23536
  if (!stripped) continue;
23537
+ if (path32.isAbsolute(stripped)) {
23538
+ return refuse(`patch refused: target "${t.raw}" strips to absolute path`);
23539
+ }
23434
23540
  const candidate = path32.resolve(dir, stripped);
23435
- const rel = path32.relative(ctx.projectRoot, candidate);
23541
+ let real;
23542
+ try {
23543
+ real = await resolveRealInsideRoot(candidate, ctx);
23544
+ } catch (err) {
23545
+ return refuse(`patch refused: target "${t.raw}" ${toErrorMessage4(err)}`);
23546
+ }
23547
+ const rel = path32.relative(realRoot, real);
23436
23548
  if (rel.startsWith("..") || path32.isAbsolute(rel)) {
23437
- return {
23438
- applied: 0,
23439
- rejected: 1,
23440
- files: [],
23441
- dry_run: dryRun,
23442
- message: `patch refused: target "${t}" resolves outside project root`
23443
- };
23549
+ return refuse(`patch refused: target "${t.raw}" resolves outside project root`);
23444
23550
  }
23445
- resolvedTargets.push(candidate);
23551
+ resolvedTargets.push({ raw: t.raw, deleted: t.deleted, abs: real });
23446
23552
  }
23447
23553
  const beforeContents = /* @__PURE__ */ new Map();
23554
+ const beforeExisted = /* @__PURE__ */ new Set();
23448
23555
  if (!dryRun) {
23449
23556
  for (const target of resolvedTargets) {
23450
- beforeContents.set(target, await readTextForTracking(target));
23557
+ const existed = (await fs27.stat(target.abs).catch(() => null))?.isFile() ?? false;
23558
+ if (existed) beforeExisted.add(target.abs);
23559
+ beforeContents.set(target.abs, await readTextForTracking(target.abs));
23451
23560
  }
23452
23561
  }
23453
23562
  const tmpDir = await fs27.mkdtemp(path32.join(os9.tmpdir(), ".wstack_patch_"));
@@ -23457,32 +23566,70 @@ var patchTool = {
23457
23566
  const patchFile = path32.join(tmpDir, "in.diff");
23458
23567
  await fs27.writeFile(patchFile, input.patch, { mode: 384 });
23459
23568
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
23460
- const result = await runPatch(args, dir, opts.signal);
23461
- if (result.exitCode !== 0 && !dryRun) {
23462
- return {
23463
- applied: 0,
23464
- rejected: 1,
23465
- files: [],
23466
- dry_run: dryRun,
23467
- message: `patch failed: ${result.stderr || result.stdout}`
23468
- };
23469
- }
23470
- const patched = extractPatchedFiles(result.stdout);
23569
+ const result = await runPatch(args, dir, opts.signal, {
23570
+ patchFile,
23571
+ strip,
23572
+ dryRun
23573
+ });
23574
+ const touched = [];
23471
23575
  if (!dryRun) {
23472
23576
  for (const target of resolvedTargets) {
23473
- const before = beforeContents.get(target) ?? null;
23474
- const after = await readTextForTracking(target);
23577
+ const abs = target.abs;
23578
+ const before = beforeContents.get(abs) ?? null;
23579
+ const stat18 = await fs27.stat(abs).catch(() => null);
23580
+ if (!stat18?.isFile()) {
23581
+ if (beforeExisted.has(abs)) {
23582
+ touched.push(abs);
23583
+ ctx.session?.recordFileChange?.({
23584
+ path: abs,
23585
+ action: "deleted",
23586
+ before,
23587
+ after: null
23588
+ });
23589
+ }
23590
+ continue;
23591
+ }
23592
+ const after = await readTextForTracking(abs);
23475
23593
  if (after === null || after === before) continue;
23476
- const stat18 = await fs27.stat(target).catch(() => null);
23477
- if (stat18) ctx.recordRead?.(target, stat18.mtimeMs, "write", sha256hex(after));
23594
+ touched.push(abs);
23595
+ ctx.recordRead?.(abs, stat18.mtimeMs, "write", sha256hex(after));
23478
23596
  ctx.session?.recordFileChange?.({
23479
- path: target,
23597
+ path: abs,
23480
23598
  action: before === null ? "created" : "modified",
23481
23599
  before,
23482
23600
  after
23483
23601
  });
23484
23602
  }
23485
23603
  }
23604
+ if (result.exitCode !== 0) {
23605
+ if (!dryRun) {
23606
+ const partial = touched.length > 0 ? ` ${touched.length} file(s) were still modified on disk and have been recorded for rewind: ${touched.map((p) => path32.relative(realRoot, p) || p).join(", ")}.` : "";
23607
+ return {
23608
+ applied: touched.length,
23609
+ rejected: 1,
23610
+ // Normalize to relative-to-realRoot for API consistency with the
23611
+ // success path (which returns GNU patch's dir-relative names).
23612
+ // `touched` entries are realpaths from resolveRealInsideRoot, and
23613
+ // realRoot is also a realpath, so path.relative is like-for-like.
23614
+ files: touched.map((p) => path32.relative(realRoot, p) || p),
23615
+ dry_run: dryRun,
23616
+ message: `patch failed: ${result.stderr || result.stdout}${partial}`
23617
+ };
23618
+ }
23619
+ const wouldPatch = extractPatchedFiles(result.stdout);
23620
+ return {
23621
+ applied: wouldPatch.length,
23622
+ rejected: 1,
23623
+ files: wouldPatch,
23624
+ dry_run: dryRun,
23625
+ message: `patch preview: would conflict \u2014 ${result.stderr || result.stdout}`
23626
+ };
23627
+ }
23628
+ const patched = result.engine === "git" ? [
23629
+ ...new Set(
23630
+ resolvedTargets.map((target) => path32.relative(dir, target.abs) || target.abs)
23631
+ )
23632
+ ] : extractPatchedFiles(result.stdout);
23486
23633
  return {
23487
23634
  applied: patched.length,
23488
23635
  rejected: 0,
@@ -23510,27 +23657,86 @@ async function readTextForTracking(absPath) {
23510
23657
  }
23511
23658
  function extractDiffTargets(patch) {
23512
23659
  const out = [];
23513
- const re = /^\+\+\+\s+([^\t\r\n]+)/gm;
23514
- for (const m of patch.matchAll(re)) {
23515
- const raw = m[1];
23516
- if (!raw) continue;
23517
- const target = raw.length > 4096 ? raw.slice(0, 4096).trim() : raw.trim();
23518
- if (!target || target === "/dev/null") continue;
23519
- out.push(target);
23660
+ const clean = (raw) => {
23661
+ if (!raw) return "";
23662
+ return (raw.length > 4096 ? raw.slice(0, 4096) : raw).trim();
23663
+ };
23664
+ let lastOld;
23665
+ let inHunk = false;
23666
+ let oldLinesLeft = 0;
23667
+ let newLinesLeft = 0;
23668
+ for (const line of patch.split(/\r?\n/)) {
23669
+ const hunkMatch = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/.exec(line);
23670
+ if (hunkMatch) {
23671
+ inHunk = true;
23672
+ oldLinesLeft = hunkMatch[1] ? Number(hunkMatch[1]) : 1;
23673
+ newLinesLeft = hunkMatch[2] ? Number(hunkMatch[2]) : 1;
23674
+ lastOld = void 0;
23675
+ continue;
23676
+ }
23677
+ if (inHunk) {
23678
+ const ch = line[0];
23679
+ if (ch === "-") oldLinesLeft--;
23680
+ else if (ch === "+") newLinesLeft--;
23681
+ else if (ch === " " || ch === void 0) {
23682
+ oldLinesLeft--;
23683
+ newLinesLeft--;
23684
+ }
23685
+ if (oldLinesLeft <= 0 && newLinesLeft <= 0) inHunk = false;
23686
+ continue;
23687
+ }
23688
+ const oldMatch = /^---\s+([^\t\r\n]+)/.exec(line);
23689
+ if (oldMatch) {
23690
+ lastOld = clean(oldMatch[1]);
23691
+ continue;
23692
+ }
23693
+ const newMatch = /^\+\+\+\s+([^\t\r\n]+)/.exec(line);
23694
+ if (!newMatch) continue;
23695
+ const newTarget = clean(newMatch[1]);
23696
+ if (newTarget && newTarget !== "/dev/null") {
23697
+ out.push({ raw: newTarget, deleted: false });
23698
+ } else if (lastOld && lastOld !== "/dev/null") {
23699
+ out.push({ raw: lastOld, deleted: true });
23700
+ }
23701
+ lastOld = void 0;
23520
23702
  }
23521
23703
  return out;
23522
23704
  }
23523
23705
  function stripPathComponents(p, strip) {
23524
- const parts = p.replace(/\\/g, "/").split("/").filter((s) => s !== "" && s !== ".");
23525
- if (parts.length <= strip) return void 0;
23526
- return parts.slice(strip).join("/");
23706
+ const s = p.replace(/\\/g, "/");
23707
+ let idx = 0;
23708
+ for (let i = 0; i < strip; i++) {
23709
+ while (idx < s.length && s[idx] !== "/") idx++;
23710
+ let hadSlash = false;
23711
+ while (idx < s.length && s[idx] === "/") {
23712
+ idx++;
23713
+ hadSlash = true;
23714
+ }
23715
+ if (!hadSlash) return void 0;
23716
+ }
23717
+ return s.slice(idx) || void 0;
23718
+ }
23719
+ function runPatch(args, cwd, signal, fallback) {
23720
+ return runPatchProcess("patch", args, cwd, signal).then(async (result) => {
23721
+ if (!result.unavailable) return { ...result, engine: "patch" };
23722
+ const gitArgs = [
23723
+ "apply",
23724
+ "--unsafe-paths",
23725
+ `-p${fallback.strip}`,
23726
+ "--verbose",
23727
+ ...fallback.dryRun ? ["--check"] : [],
23728
+ fallback.patchFile
23729
+ ];
23730
+ const gitResult = await runPatchProcess("git", gitArgs, cwd, signal);
23731
+ return { ...gitResult, engine: "git" };
23732
+ });
23527
23733
  }
23528
- function runPatch(args, cwd, signal) {
23734
+ function runPatchProcess(command, args, cwd, signal) {
23529
23735
  return new Promise((resolve16) => {
23530
23736
  let stdout = "";
23531
23737
  let stderr = "";
23532
23738
  const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
23533
- const child = spawn13("patch", args, {
23739
+ const child = spawn13(command, args, {
23534
23740
  cwd,
23535
23741
  signal,
23536
23742
  env,
@@ -23543,13 +23749,24 @@ function runPatch(args, cwd, signal) {
23543
23749
  child.stderr?.on("data", (c) => {
23544
23750
  stderr += c.toString();
23545
23751
  });
23546
- child.on("close", (code) => resolve16({ exitCode: code ?? 1, stdout, stderr }));
23547
- child.on("error", (e) => resolve16({ exitCode: 1, stdout: "", stderr: e.message }));
23752
+ child.on(
23753
+ "close",
23754
+ (code) => resolve16({ exitCode: code ?? 1, stdout, stderr, unavailable: false })
23755
+ );
23756
+ child.on(
23757
+ "error",
23758
+ (e) => resolve16({
23759
+ exitCode: 1,
23760
+ stdout: "",
23761
+ stderr: e.message,
23762
+ unavailable: e.code === "ENOENT"
23763
+ })
23764
+ );
23548
23765
  });
23549
23766
  }
23550
23767
  function extractPatchedFiles(output) {
23551
23768
  const files = [];
23552
- const re = /patching file (.+)/gi;
23769
+ const re = /(?:patching|checking) file (.+)/gi;
23553
23770
  for (const m of output.matchAll(re)) {
23554
23771
  if (m[1]) files.push(m[1]);
23555
23772
  }
@@ -23858,7 +24075,7 @@ function mkResult(plan, ok, message, todos) {
23858
24075
  init_util();
23859
24076
  import * as fs28 from "node:fs/promises";
23860
24077
  import { FsError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
23861
- import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
24078
+ import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
23862
24079
  var ADVANCED_MODE_META_KEY = "tools.read.advancedMode";
23863
24080
  var MAX_BYTES2 = 5 * 1024 * 1024;
23864
24081
  var readTool = {
@@ -23926,7 +24143,7 @@ var readTool = {
23926
24143
  });
23927
24144
  }
23928
24145
  throw new FsError({
23929
- message: `read: failed to stat "${input.path}": ${toErrorMessage4(err)}`,
24146
+ message: `read: failed to stat "${input.path}": ${toErrorMessage5(err)}`,
23930
24147
  code: "FS_READ_FAILED",
23931
24148
  path: absPath,
23932
24149
  context: { errno: code },
@@ -24585,7 +24802,7 @@ function substituteVars(content, name, vars) {
24585
24802
  // src/search.ts
24586
24803
  import { FetchError as FetchError3, ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
24587
24804
  import { expectDefined as expectDefined9 } from "@wrongstack/core/utils";
24588
- import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
24805
+ import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
24589
24806
  var DEFAULT_NUM = 10;
24590
24807
  var MAX_RESULTS = 50;
24591
24808
  var TIMEOUT_MS3 = 15e3;
@@ -24789,7 +25006,7 @@ async function duckduckgoSearch(query, num, signal) {
24789
25006
  return parseDuckDuckGo(html, num);
24790
25007
  } catch (err) {
24791
25008
  console.log(
24792
- JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage5(err) })
25009
+ JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage6(err) })
24793
25010
  );
24794
25011
  return [{ title: "Search unavailable", url: "https://duckduckgo.com/unavailable", snippet: "Could not reach DuckDuckGo", score: 0 }];
24795
25012
  }
@@ -24973,7 +25190,7 @@ function decodeHtmlEntities(text) {
24973
25190
 
24974
25191
  // src/set-working-dir.ts
24975
25192
  import * as fs31 from "node:fs/promises";
24976
- import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
25193
+ import { toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
24977
25194
  var setWorkingDirTool = {
24978
25195
  name: "set_working_dir",
24979
25196
  category: "Context",
@@ -25007,7 +25224,7 @@ var setWorkingDirTool = {
25007
25224
  } catch (err) {
25008
25225
  return {
25009
25226
  current: ctx.workingDir,
25010
- error: toErrorMessage6(err)
25227
+ error: toErrorMessage7(err)
25011
25228
  };
25012
25229
  }
25013
25230
  try {
@@ -25742,7 +25959,7 @@ var toolHelpTool = {
25742
25959
  const format = input.format ?? "short";
25743
25960
  const includeExamples = input.include_examples ?? false;
25744
25961
  if (input.tool) {
25745
- const tool = ctx.tools.find((t) => t.name === input.tool);
25962
+ const tool = (ctx.catalogTools ?? ctx.tools).find((t) => t.name === input.tool);
25746
25963
  if (!tool) {
25747
25964
  return {
25748
25965
  tool: input.tool,
@@ -25767,7 +25984,7 @@ var toolHelpTool = {
25767
25984
  total: 1
25768
25985
  };
25769
25986
  }
25770
- const allTools = ctx.tools.map((t) => ({
25987
+ const allTools = (ctx.catalogTools ?? ctx.tools).map((t) => ({
25771
25988
  name: t.name,
25772
25989
  description: t.description,
25773
25990
  usageHint: t.usageHint ?? "",
@@ -25875,7 +26092,7 @@ var toolSearchTool = {
25875
26092
  },
25876
26093
  async execute(input, ctx) {
25877
26094
  const limit = Math.min(input.limit ?? 20, 100);
25878
- const tools = ctx.tools;
26095
+ const tools = ctx.catalogTools ?? ctx.tools;
25879
26096
  const query = input.query?.toLowerCase() ?? "";
25880
26097
  const filtered = tools.filter((t) => {
25881
26098
  if (query && !t.name.toLowerCase().includes(query) && !t.description.toLowerCase().includes(query)) {
@@ -25955,7 +26172,7 @@ var toolUseTool = {
25955
26172
  executionMs: 0
25956
26173
  };
25957
26174
  }
25958
- const tool = ctx.tools.find((t) => t.name === input.tool);
26175
+ const tool = (ctx.catalogTools ?? ctx.tools).find((t) => t.name === input.tool);
25959
26176
  if (!tool) {
25960
26177
  return {
25961
26178
  tool: input.tool,
@@ -26013,7 +26230,13 @@ init_util();
26013
26230
  import * as fs32 from "node:fs/promises";
26014
26231
  import * as path36 from "node:path";
26015
26232
  import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
26016
- var DEFAULT_IGNORE5 = [...DEFAULT_WALK_IGNORE_DIRS4, ".wrongstack", ".ssh", ".gnupg", ".aws"];
26233
+ var DEFAULT_IGNORE5 = /* @__PURE__ */ new Set([
26234
+ ...DEFAULT_WALK_IGNORE_DIRS4,
26235
+ ".wrongstack",
26236
+ ".ssh",
26237
+ ".gnupg",
26238
+ ".aws"
26239
+ ]);
26017
26240
  var DEFAULT_MAX_ENTRIES2 = 5e3;
26018
26241
  var MAX_TREE_OUTPUT_BYTES = 256 * 1024;
26019
26242
  var treeTool = {
@@ -26174,8 +26397,12 @@ async function walkDir(dir, depth, opts) {
26174
26397
  return true;
26175
26398
  });
26176
26399
  if (depth > 0) {
26177
- const dirCount = filtered.filter((e) => e.isDirectory()).length;
26178
- const fileCount = filtered.filter((e) => e.isFile()).length;
26400
+ let dirCount = 0;
26401
+ let fileCount = 0;
26402
+ for (const e of filtered) {
26403
+ if (e.isDirectory()) dirCount++;
26404
+ else if (e.isFile()) fileCount++;
26405
+ }
26179
26406
  opts.totalDirs.value += dirCount;
26180
26407
  opts.totalFiles.value += fileCount;
26181
26408
  opts.onProgress?.();