@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.
@@ -995,16 +995,23 @@ function looksBinary(content) {
995
995
  }
996
996
  return bad / sample.length > 0.1;
997
997
  }
998
- function lineColAt(content, index) {
999
- let line = 1;
1000
- let lastNl = -1;
1001
- for (let i = 0; i < index && i < content.length; i++) {
1002
- if (content.charCodeAt(i) === 10) {
1003
- line++;
1004
- lastNl = i;
1005
- }
998
+ function newlineOffsets2(content) {
999
+ const offsets = [];
1000
+ for (let i = 0; i < content.length; i++) {
1001
+ if (content.charCodeAt(i) === 10) offsets.push(i);
1002
+ }
1003
+ return offsets;
1004
+ }
1005
+ function lineColAt(offsets, index) {
1006
+ let low = 0;
1007
+ let high = offsets.length;
1008
+ while (low < high) {
1009
+ const mid = low + high >>> 1;
1010
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
1011
+ else high = mid;
1006
1012
  }
1007
- return { line, col: index - lastNl };
1013
+ const lastNl = low > 0 ? offsets[low - 1] : -1;
1014
+ return { line: low + 1, col: index - lastNl };
1008
1015
  }
1009
1016
  function parseGeneric(opts) {
1010
1017
  const { file, lang } = opts;
@@ -1017,6 +1024,7 @@ function parseGeneric(opts) {
1017
1024
  const patterns = patternsFor(lang);
1018
1025
  const symbols = [];
1019
1026
  const seen = /* @__PURE__ */ new Set();
1027
+ const nlOffsets = newlineOffsets2(content);
1020
1028
  for (const pattern of patterns) {
1021
1029
  const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
1022
1030
  re.lastIndex = 0;
@@ -1030,7 +1038,7 @@ function parseGeneric(opts) {
1030
1038
  if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
1031
1039
  continue;
1032
1040
  }
1033
- const { line, col } = lineColAt(content, match.index);
1041
+ const { line, col } = lineColAt(nlOffsets, match.index ?? 0);
1034
1042
  const key = `${name}\0${line}\0${pattern.kind}`;
1035
1043
  if (seen.has(key)) continue;
1036
1044
  seen.add(key);
@@ -2228,9 +2236,9 @@ import * as path4 from "node:path";
2228
2236
  // src/codebase-index/bm25.ts
2229
2237
  var K1 = 1.5;
2230
2238
  var B = 0.75;
2239
+ var TOKENISE_RE = new RegExp("[^\\p{L}\\p{N}$']", "gu");
2231
2240
  function tokenise(text) {
2232
- const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
2233
- return sanitised.toLowerCase().split(" ").filter(Boolean);
2241
+ return text.replace(TOKENISE_RE, " ").toLowerCase().trim().split(/\s+/).filter(Boolean);
2234
2242
  }
2235
2243
  function splitName(name) {
2236
2244
  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();
@@ -4745,7 +4753,9 @@ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
4745
4753
  var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
4746
4754
  var buildIdCache;
4747
4755
  function projectIndexServerBuildId(entrypoint) {
4748
- const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path5.resolve(entrypoint);
4756
+ const href = entrypoint instanceof URL ? entrypoint.href : entrypoint;
4757
+ const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
4758
+ const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path5.resolve(cleanHref);
4749
4759
  try {
4750
4760
  const stat2 = fs4.statSync(file);
4751
4761
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat2.mtimeMs && buildIdCache.size === stat2.size) {
@@ -996,16 +996,23 @@ function looksBinary(content) {
996
996
  }
997
997
  return bad / sample.length > 0.1;
998
998
  }
999
- function lineColAt(content, index) {
1000
- let line = 1;
1001
- let lastNl = -1;
1002
- for (let i = 0; i < index && i < content.length; i++) {
1003
- if (content.charCodeAt(i) === 10) {
1004
- line++;
1005
- lastNl = i;
1006
- }
999
+ function newlineOffsets2(content) {
1000
+ const offsets = [];
1001
+ for (let i = 0; i < content.length; i++) {
1002
+ if (content.charCodeAt(i) === 10) offsets.push(i);
1003
+ }
1004
+ return offsets;
1005
+ }
1006
+ function lineColAt(offsets, index) {
1007
+ let low = 0;
1008
+ let high = offsets.length;
1009
+ while (low < high) {
1010
+ const mid = low + high >>> 1;
1011
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
1012
+ else high = mid;
1007
1013
  }
1008
- return { line, col: index - lastNl };
1014
+ const lastNl = low > 0 ? offsets[low - 1] : -1;
1015
+ return { line: low + 1, col: index - lastNl };
1009
1016
  }
1010
1017
  function parseGeneric(opts) {
1011
1018
  const { file, lang } = opts;
@@ -1018,6 +1025,7 @@ function parseGeneric(opts) {
1018
1025
  const patterns = patternsFor(lang);
1019
1026
  const symbols = [];
1020
1027
  const seen = /* @__PURE__ */ new Set();
1028
+ const nlOffsets = newlineOffsets2(content);
1021
1029
  for (const pattern of patterns) {
1022
1030
  const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
1023
1031
  re.lastIndex = 0;
@@ -1031,7 +1039,7 @@ function parseGeneric(opts) {
1031
1039
  if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
1032
1040
  continue;
1033
1041
  }
1034
- const { line, col } = lineColAt(content, match.index);
1042
+ const { line, col } = lineColAt(nlOffsets, match.index ?? 0);
1035
1043
  const key = `${name}\0${line}\0${pattern.kind}`;
1036
1044
  if (seen.has(key)) continue;
1037
1045
  seen.add(key);
@@ -2987,9 +2995,9 @@ import * as path10 from "node:path";
2987
2995
  // src/codebase-index/bm25.ts
2988
2996
  var K1 = 1.5;
2989
2997
  var B = 0.75;
2998
+ var TOKENISE_RE = new RegExp("[^\\p{L}\\p{N}$']", "gu");
2990
2999
  function tokenise(text) {
2991
- const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
2992
- return sanitised.toLowerCase().split(" ").filter(Boolean);
3000
+ return text.replace(TOKENISE_RE, " ").toLowerCase().trim().split(/\s+/).filter(Boolean);
2993
3001
  }
2994
3002
  function splitName(name) {
2995
3003
  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();
@@ -5837,7 +5845,9 @@ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
5837
5845
  var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
5838
5846
  var buildIdCache;
5839
5847
  function projectIndexServerBuildId(entrypoint) {
5840
- const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path12.resolve(entrypoint);
5848
+ const href = entrypoint instanceof URL ? entrypoint.href : entrypoint;
5849
+ const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
5850
+ const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path12.resolve(cleanHref);
5841
5851
  try {
5842
5852
  const stat2 = fs9.statSync(file);
5843
5853
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat2.mtimeMs && buildIdCache.size === stat2.size) {
@@ -992,16 +992,23 @@ function looksBinary(content) {
992
992
  }
993
993
  return bad / sample.length > 0.1;
994
994
  }
995
- function lineColAt(content, index) {
996
- let line = 1;
997
- let lastNl = -1;
998
- for (let i = 0; i < index && i < content.length; i++) {
999
- if (content.charCodeAt(i) === 10) {
1000
- line++;
1001
- lastNl = i;
1002
- }
995
+ function newlineOffsets2(content) {
996
+ const offsets = [];
997
+ for (let i = 0; i < content.length; i++) {
998
+ if (content.charCodeAt(i) === 10) offsets.push(i);
999
+ }
1000
+ return offsets;
1001
+ }
1002
+ function lineColAt(offsets, index) {
1003
+ let low = 0;
1004
+ let high = offsets.length;
1005
+ while (low < high) {
1006
+ const mid = low + high >>> 1;
1007
+ if ((offsets[mid] ?? 0) < index) low = mid + 1;
1008
+ else high = mid;
1003
1009
  }
1004
- return { line, col: index - lastNl };
1010
+ const lastNl = low > 0 ? offsets[low - 1] : -1;
1011
+ return { line: low + 1, col: index - lastNl };
1005
1012
  }
1006
1013
  function parseGeneric(opts) {
1007
1014
  const { file, lang } = opts;
@@ -1014,6 +1021,7 @@ function parseGeneric(opts) {
1014
1021
  const patterns = patternsFor(lang);
1015
1022
  const symbols = [];
1016
1023
  const seen = /* @__PURE__ */ new Set();
1024
+ const nlOffsets = newlineOffsets2(content);
1017
1025
  for (const pattern of patterns) {
1018
1026
  const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
1019
1027
  re.lastIndex = 0;
@@ -1027,7 +1035,7 @@ function parseGeneric(opts) {
1027
1035
  if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
1028
1036
  continue;
1029
1037
  }
1030
- const { line, col } = lineColAt(content, match.index);
1038
+ const { line, col } = lineColAt(nlOffsets, match.index ?? 0);
1031
1039
  const key = `${name}\0${line}\0${pattern.kind}`;
1032
1040
  if (seen.has(key)) continue;
1033
1041
  seen.add(key);
@@ -2974,9 +2982,9 @@ import * as path10 from "node:path";
2974
2982
  // src/codebase-index/bm25.ts
2975
2983
  var K1 = 1.5;
2976
2984
  var B = 0.75;
2985
+ var TOKENISE_RE = new RegExp("[^\\p{L}\\p{N}$']", "gu");
2977
2986
  function tokenise(text) {
2978
- const sanitised = text.replace(/[^\p{L}\p{N}$'_]/gu, " ").replace(/_/g, " ");
2979
- return sanitised.toLowerCase().split(" ").filter(Boolean);
2987
+ return text.replace(TOKENISE_RE, " ").toLowerCase().trim().split(/\s+/).filter(Boolean);
2980
2988
  }
2981
2989
  function splitName(name) {
2982
2990
  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();
package/dist/format.js CHANGED
@@ -792,7 +792,7 @@ function getProcessRegistry() {
792
792
  // src/_spawn-stream.ts
793
793
  var isWin = process.platform === "win32";
794
794
  async function* spawnStream(opts) {
795
- const max = opts.maxBytes ?? 2e5;
795
+ const max = opts.maxBytes ?? 999999999;
796
796
  const flushAt = opts.flushBytes ?? 4 * 1024;
797
797
  const maxQueue = opts.maxQueueSize ?? 500;
798
798
  const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
package/dist/git.js CHANGED
@@ -291,11 +291,8 @@ function buildArgs(input) {
291
291
  ...input.branch.startsWith("-") || input.branch.includes(" --") ? [] : [input.branch]
292
292
  ] : ["branch"];
293
293
  case "checkout":
294
- return [
295
- "checkout",
296
- ...input.branch ? ["--", input.branch] : [],
297
- ...files.length ? ["--", ...files] : []
298
- ];
294
+ if (files.length) return ["checkout", "--", ...files];
295
+ return input.branch ? ["checkout", input.branch, "--"] : ["checkout"];
299
296
  case "stash":
300
297
  return input.message ? ["stash", "push", "-m", input.message] : ["stash", "push"];
301
298
  case "push":
package/dist/glob.js CHANGED
@@ -138,7 +138,7 @@ async function safeResolveReal(input, ctx) {
138
138
  }
139
139
 
140
140
  // src/glob.ts
141
- var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
141
+ var DEFAULT_IGNORE = new Set(DEFAULT_WALK_IGNORE_DIRS);
142
142
  var WALK_CONCURRENCY = 16;
143
143
  var globTool = {
144
144
  name: "glob",
@@ -217,7 +217,7 @@ var globTool = {
217
217
  const matchedFiles = [];
218
218
  for (const e of entries) {
219
219
  const name = e.name;
220
- if (DEFAULT_IGNORE.includes(name)) continue;
220
+ if (DEFAULT_IGNORE.has(name)) continue;
221
221
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
222
222
  const full = path3.join(dir, name);
223
223
  if (e.isDirectory()) {
package/dist/grep.js CHANGED
@@ -40,6 +40,81 @@ var DANGEROUS_PATTERNS = [
40
40
  // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
41
41
  /[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
42
42
  ];
43
+ function hasAmbiguousQuantifiedAlternation(pattern) {
44
+ for (let i = 0; i < pattern.length; i++) {
45
+ if (pattern[i] !== "(") continue;
46
+ if (i > 0 && pattern[i - 1] === "\\") continue;
47
+ let depth = 0;
48
+ let inClass = false;
49
+ let j = i;
50
+ for (; j < pattern.length; j++) {
51
+ const ch = pattern[j];
52
+ if (ch === "\\") {
53
+ j++;
54
+ continue;
55
+ }
56
+ if (inClass) {
57
+ if (ch === "]") inClass = false;
58
+ continue;
59
+ }
60
+ if (ch === "[") {
61
+ inClass = true;
62
+ continue;
63
+ }
64
+ if (ch === "(") depth++;
65
+ else if (ch === ")") {
66
+ depth--;
67
+ if (depth === 0) break;
68
+ }
69
+ }
70
+ if (j >= pattern.length) return false;
71
+ const next = pattern[j + 1];
72
+ if (next !== "+" && next !== "*" && next !== "{") continue;
73
+ let inner = pattern.slice(i + 1, j);
74
+ inner = inner.replace(/^\?(?::|<?[=!])/u, "");
75
+ const branches = [];
76
+ let current = "";
77
+ let d = 0;
78
+ let cls = false;
79
+ for (let k = 0; k < inner.length; k++) {
80
+ const ch = inner[k];
81
+ if (ch === "\\") {
82
+ current += ch + (inner[k + 1] ?? "");
83
+ k++;
84
+ continue;
85
+ }
86
+ if (cls) {
87
+ if (ch === "]") cls = false;
88
+ current += ch;
89
+ continue;
90
+ }
91
+ if (ch === "[") {
92
+ cls = true;
93
+ current += ch;
94
+ continue;
95
+ }
96
+ if (ch === "(") d++;
97
+ if (ch === ")") d--;
98
+ if (ch === "|" && d === 0) {
99
+ branches.push(current);
100
+ current = "";
101
+ continue;
102
+ }
103
+ current += ch;
104
+ }
105
+ branches.push(current);
106
+ if (branches.length < 2) continue;
107
+ for (let a = 0; a < branches.length; a++) {
108
+ for (let b = a + 1; b < branches.length; b++) {
109
+ const x = branches[a];
110
+ const y = branches[b];
111
+ if (x === "" || y === "") return true;
112
+ if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
113
+ }
114
+ }
115
+ }
116
+ return false;
117
+ }
43
118
  function compileUserRegex(pattern, flags) {
44
119
  if (typeof pattern !== "string") {
45
120
  return { ok: false, reason: "pattern must be a string" };
@@ -58,6 +133,12 @@ function compileUserRegex(pattern, flags) {
58
133
  };
59
134
  }
60
135
  }
136
+ if (hasAmbiguousQuantifiedAlternation(pattern)) {
137
+ return {
138
+ ok: false,
139
+ reason: "pattern quantifies an alternation with overlapping branches \u2014 rewrite so no two branches can match the same text"
140
+ };
141
+ }
61
142
  try {
62
143
  return { ok: true, regex: new RegExp(pattern, flags) };
63
144
  } catch (err) {
@@ -73,6 +154,7 @@ function capSubject(line) {
73
154
  }
74
155
 
75
156
  // src/_util.ts
157
+ import * as fsp from "node:fs/promises";
76
158
  import * as path from "node:path";
77
159
  import * as Core from "@wrongstack/core/utils";
78
160
  function resolvePath(input, ctx) {
@@ -96,6 +178,39 @@ function ensureInsideRoot(absPath, ctx) {
96
178
  function safeResolve(input, ctx) {
97
179
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
98
180
  }
181
+ async function resolveRealInsideRoot(absPath, ctx) {
182
+ if (ctx.allowOutsideProjectRoot) return absPath;
183
+ const realRoots = await Promise.all(
184
+ allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r)))
185
+ );
186
+ let probe = absPath;
187
+ const pendingTail = [];
188
+ for (; ; ) {
189
+ let real;
190
+ try {
191
+ real = await fsp.realpath(probe);
192
+ } catch (err) {
193
+ if (err.code === "ENOENT") {
194
+ const parent = path.dirname(probe);
195
+ if (parent === probe) return absPath;
196
+ pendingTail.unshift(path.basename(probe));
197
+ probe = parent;
198
+ continue;
199
+ }
200
+ throw err;
201
+ }
202
+ if (isInsideAny(real, realRoots)) {
203
+ return pendingTail.length > 0 ? path.join(real, ...pendingTail) : real;
204
+ }
205
+ throw new Error(
206
+ `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
207
+ );
208
+ }
209
+ }
210
+ async function safeResolveReal(input, ctx) {
211
+ const abs = safeResolve(input, ctx);
212
+ return await resolveRealInsideRoot(abs, ctx);
213
+ }
99
214
  function isBinaryBuffer(buf) {
100
215
  const len = Math.min(buf.length, 8192);
101
216
  for (let i = 0; i < len; i++) {
@@ -160,7 +275,7 @@ async function loadGitignoreMatcher(projectRoot) {
160
275
  }
161
276
 
162
277
  // src/grep.ts
163
- var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
278
+ var DEFAULT_IGNORE = new Set(DEFAULT_WALK_IGNORE_DIRS);
164
279
  var NATIVE_SCAN_CONCURRENCY = 32;
165
280
  var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
166
281
  var NATIVE_MAX_FILE_BYTES = 1e6;
@@ -233,7 +348,7 @@ var grepTool = {
233
348
  field: "pattern"
234
349
  });
235
350
  }
236
- const base = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
351
+ const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
237
352
  const mode = input.output_mode ?? "content";
238
353
  const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
239
354
  const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
@@ -558,7 +673,7 @@ async function runNative(input, base, mode, limit, signal) {
558
673
  const subdirs = [];
559
674
  for (const e of entries) {
560
675
  if (stopped) return;
561
- if (DEFAULT_IGNORE.includes(e.name)) continue;
676
+ if (DEFAULT_IGNORE.has(e.name)) continue;
562
677
  if (e.isSymbolicLink()) continue;
563
678
  const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
564
679
  const full = path3.join(dir, e.name);
package/dist/index.d.ts CHANGED
@@ -30,6 +30,7 @@ export { lintTool } from './lint.js';
30
30
  export { logsTool } from './logs.js';
31
31
  export { forgetTool, relatedMemoryTool, rememberTool, searchMemoryTool } from './memory.js';
32
32
  export { createModeTool } from './mode.js';
33
+ export { nextStepsTool } from './next-steps-tool.js';
33
34
  export { outdatedTool } from './outdated.js';
34
35
  export { builtinToolsPack } from './pack.js';
35
36
  export { patchTool } from './patch.js';