@wrongstack/tools 0.299.0 → 0.301.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.
Files changed (53) hide show
  1. package/dist/audit.js +6 -2
  2. package/dist/bash.js +6 -2
  3. package/dist/batch-tool-use.js +3 -1
  4. package/dist/browser/index.js +1 -1
  5. package/dist/builtin.d.ts +3 -2
  6. package/dist/builtin.js +1781 -376
  7. package/dist/codebase-index/bm25.d.ts +7 -1
  8. package/dist/codebase-index/import-extractor.d.ts +39 -0
  9. package/dist/codebase-index/index.js +1418 -267
  10. package/dist/codebase-index/languages.d.ts +24 -0
  11. package/dist/codebase-index/module-resolver.d.ts +78 -0
  12. package/dist/codebase-index/module-roots.d.ts +81 -0
  13. package/dist/codebase-index/parser-output.d.ts +29 -0
  14. package/dist/codebase-index/project-server.js +1402 -249
  15. package/dist/codebase-index/rs-parser.d.ts +22 -0
  16. package/dist/codebase-index/schema.d.ts +24 -1
  17. package/dist/codebase-index/worker.js +1401 -250
  18. package/dist/codebase-index/writer-graph-helpers.d.ts +17 -5
  19. package/dist/codebase-index/writer-ref-mapper.d.ts +3 -0
  20. package/dist/codebase-index/writer-schema.d.ts +15 -3
  21. package/dist/codebase-index/writer.d.ts +76 -3
  22. package/dist/exec.js +35 -2
  23. package/dist/format.js +6 -2
  24. package/dist/git.js +2 -5
  25. package/dist/glob.js +2 -2
  26. package/dist/grep.js +118 -3
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1925 -415
  29. package/dist/install.js +6 -2
  30. package/dist/json.js +132 -2
  31. package/dist/languages/index.js +6 -2
  32. package/dist/lint.js +6 -2
  33. package/dist/logs.js +81 -0
  34. package/dist/next-steps-tool.d.ts +26 -0
  35. package/dist/outdated.js +6 -2
  36. package/dist/pack.js +1781 -376
  37. package/dist/patch.js +206 -45
  38. package/dist/process-registry.d.ts +6 -0
  39. package/dist/process-registry.js +6 -2
  40. package/dist/ps-slash.js +6 -2
  41. package/dist/read.js +1410 -257
  42. package/dist/replace.js +81 -0
  43. package/dist/skill.js +51 -2
  44. package/dist/test.js +6 -2
  45. package/dist/tool-help.js +2 -2
  46. package/dist/tool-search.js +1 -1
  47. package/dist/tool-tier.d.ts +1 -1
  48. package/dist/tool-tier.js +1786 -397
  49. package/dist/tool-use.js +1 -1
  50. package/dist/tree.js +13 -3
  51. package/dist/typecheck.js +6 -2
  52. package/package.json +3 -3
  53. package/dist/codebase-index/refs-extractor.d.ts +0 -11
@@ -2,15 +2,28 @@ import type { GraphEdge, GraphNode, SymbolLang } from './schema.js';
2
2
  /**
3
3
  * Derive a monorepo package name from an absolute file path.
4
4
  * Handles both `packages/<name>/...` and `apps/<name>/...` layouts.
5
+ *
6
+ * This is the fallback only. The authoritative grouping is computed at index
7
+ * time from each ecosystem's own manifests and stored on `files.package` —
8
+ * see {@link createPackageLabeller}. A path-shape guess is all that is left
9
+ * for a repo with no manifest at all.
5
10
  */
6
11
  export declare function derivePackage(filePath: string): string | undefined;
7
- export declare function packageFromImport(moduleName: string): string | undefined;
12
+ /**
13
+ * Build the `file → package` lookup the graph readers group by.
14
+ *
15
+ * `stored` comes from `files.package`, which the indexer filled from `go.mod`,
16
+ * `Cargo.toml`, `package.json`, `pom.xml` and friends. Files missing a stored
17
+ * label (indexed by an older run, or outside any manifest) fall back to the
18
+ * path-shape heuristic and finally to `(root)`.
19
+ */
20
+ export declare function createPackageLabeller(stored: ReadonlyMap<string, string>): (file: string) => string;
8
21
  export declare function buildPackageGraphNodes(fileCounts: Array<{
9
22
  file: string;
10
23
  n: number;
11
24
  }>, files: Array<{
12
25
  file: string;
13
- }>): {
26
+ }>, packageOf: (file: string) => string): {
14
27
  pkgNodes: Map<string, GraphNode>;
15
28
  fileToPkg: Map<string, string>;
16
29
  };
@@ -22,7 +35,7 @@ export type WriterFileGraphSymbolRow = {
22
35
  lang: string;
23
36
  line: number;
24
37
  };
25
- export declare function buildFileGraphNodeState(pkgSyms: WriterFileGraphSymbolRow[], localFiles: Set<string>): {
38
+ export declare function buildFileGraphNodeState(pkgSyms: WriterFileGraphSymbolRow[], localFiles: Set<string>, packageOf: (file: string) => string): {
26
39
  fileNodes: Map<string, GraphNode>;
27
40
  symToFile: Map<number, string>;
28
41
  fileStats: Map<string, {
@@ -41,8 +54,7 @@ export type WriterSymbolGraphRow = {
41
54
  signature: string;
42
55
  scope: string;
43
56
  };
44
- export declare function buildSymbolGraphNodes(symById: Map<number, WriterSymbolGraphRow>, relatedIds: Set<number>, fileFilter: string): GraphNode[];
45
- export declare function resolveRelativeImport(fromFile: string, moduleName: string, indexedFiles: Set<string>): string | undefined;
57
+ export declare function buildSymbolGraphNodes(symById: Map<number, WriterSymbolGraphRow>, relatedIds: Set<number>, fileFilter: string, packageOf: (file: string) => string): GraphNode[];
46
58
  export type WeightedEdgeAccumulator = {
47
59
  weight: number;
48
60
  types: Map<string, number>;
@@ -6,6 +6,9 @@ export type WriterRefRow = {
6
6
  to_id: number | null;
7
7
  call_type: string;
8
8
  line: number;
9
+ lang?: string | null;
10
+ module?: string | null;
11
+ to_file?: string | null;
9
12
  };
10
13
  export declare function mapWriterRefRow(row: WriterRefRow): Ref;
11
14
  //# sourceMappingURL=writer-ref-mapper.d.ts.map
@@ -1,7 +1,19 @@
1
1
  export declare const METADATA_TABLE_SQL = "\n CREATE TABLE IF NOT EXISTS metadata (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n";
2
- export declare const CORE_TABLES_SQL = "\n CREATE TABLE IF NOT EXISTS files (\n file TEXT PRIMARY KEY,\n lang TEXT NOT NULL,\n mtime_ms INTEGER NOT NULL,\n symbol_count INTEGER NOT NULL DEFAULT 0,\n last_indexed INTEGER NOT NULL\n );\n CREATE TABLE IF NOT EXISTS symbols (\n id INTEGER PRIMARY KEY,\n lang TEXT NOT NULL,\n kind TEXT NOT NULL,\n name TEXT NOT NULL,\n file TEXT NOT NULL,\n line INTEGER NOT NULL,\n col INTEGER NOT NULL,\n signature TEXT NOT NULL DEFAULT '',\n doc_comment TEXT NOT NULL DEFAULT '',\n scope TEXT NOT NULL DEFAULT '',\n text TEXT NOT NULL DEFAULT '',\n file_fk TEXT NOT NULL\n );\n";
2
+ export declare const CORE_TABLES_SQL = "\n CREATE TABLE IF NOT EXISTS files (\n file TEXT PRIMARY KEY,\n lang TEXT NOT NULL,\n mtime_ms INTEGER NOT NULL,\n symbol_count INTEGER NOT NULL DEFAULT 0,\n last_indexed INTEGER NOT NULL,\n -- Code Atlas grouping label, computed at index time from the ecosystem's\n -- own manifests (package.json, go.mod, Cargo.toml, \u2026). Stored rather than\n -- re-derived per query because the evidence lives on disk, not in the DB.\n package TEXT NOT NULL DEFAULT ''\n );\n CREATE TABLE IF NOT EXISTS symbols (\n id INTEGER PRIMARY KEY,\n lang TEXT NOT NULL,\n kind TEXT NOT NULL,\n name TEXT NOT NULL,\n file TEXT NOT NULL,\n line INTEGER NOT NULL,\n col INTEGER NOT NULL,\n signature TEXT NOT NULL DEFAULT '',\n doc_comment TEXT NOT NULL DEFAULT '',\n scope TEXT NOT NULL DEFAULT '',\n text TEXT NOT NULL DEFAULT '',\n file_fk TEXT NOT NULL\n );\n";
3
+ export declare const FILE_INDEX_SQL: readonly ['CREATE INDEX IF NOT EXISTS idx_f_package ON files(package)'];
3
4
  export declare const SYMBOL_INDEX_SQL: readonly ['CREATE INDEX IF NOT EXISTS idx_s_name ON symbols(name)', 'CREATE INDEX IF NOT EXISTS idx_s_kind ON symbols(kind)', 'CREATE INDEX IF NOT EXISTS idx_s_lang ON symbols(lang)', 'CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)', 'CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)', 'CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)', 'CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)'];
4
- export declare const REFS_TABLE_SQL = "\n CREATE TABLE IF NOT EXISTS refs (\n id INTEGER PRIMARY KEY,\n from_id INTEGER NOT NULL,\n to_name TEXT NOT NULL,\n to_id INTEGER,\n call_type TEXT NOT NULL,\n line INTEGER NOT NULL\n );\n";
5
- export declare const REFS_INDEX_SQL: readonly ['CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)', 'CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)', 'CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)', 'CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)'];
5
+ export declare const REFS_TABLE_SQL = "\n CREATE TABLE IF NOT EXISTS refs (\n id INTEGER PRIMARY KEY,\n from_id INTEGER NOT NULL,\n to_name TEXT NOT NULL,\n to_id INTEGER,\n call_type TEXT NOT NULL,\n line INTEGER NOT NULL,\n lang TEXT NOT NULL DEFAULT '',\n module TEXT,\n to_file TEXT\n );\n";
6
+ export declare const REFS_INDEX_SQL: readonly ['CREATE INDEX IF NOT EXISTS idx_r_from ON refs(from_id)', 'CREATE INDEX IF NOT EXISTS idx_r_to_id ON refs(to_id)', 'CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)', 'CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)', 'CREATE INDEX IF NOT EXISTS idx_r_to_name_lang ON refs(to_name, lang)', 'CREATE INDEX IF NOT EXISTS idx_r_module ON refs(module)', 'CREATE INDEX IF NOT EXISTS idx_r_to_file ON refs(to_file)'];
7
+ /**
8
+ * Static `lang → family` mirror of {@link LANG_FAMILY_ENTRIES}, so ref
9
+ * resolution can scope a match to one language family with a join rather than
10
+ * binding a per-family IN-list into every statement.
11
+ *
12
+ * Row `('', '*')` is the wildcard for refs written without a language (older
13
+ * rows, and tests that construct refs by hand): they keep resolving globally.
14
+ */
15
+ export declare const LANG_FAMILY_TABLE_SQL = "\n CREATE TABLE IF NOT EXISTS lang_family (\n lang TEXT PRIMARY KEY,\n family TEXT NOT NULL\n );\n";
16
+ /** Family value that matches every symbol family, used by language-less refs. */
17
+ export declare const LANG_FAMILY_WILDCARD = "*";
6
18
  export declare const SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
7
19
  //# sourceMappingURL=writer-schema.d.ts.map
@@ -56,10 +56,44 @@ export declare class IndexStore {
56
56
  indexDir?: string | undefined;
57
57
  });
58
58
  runWithRetry<T>(fn: () => T): T;
59
+ /**
60
+ * Mirror the in-process language→family map into SQLite.
61
+ *
62
+ * Rewritten on every open rather than only on schema bumps: the mapping is
63
+ * static lookup data, so a code-side change (a new language, a language
64
+ * moving families) must take effect without forcing a full reindex.
65
+ */
66
+ private seedLangFamilies;
67
+ /**
68
+ * Add any column the current schema expects but the on-disk table lacks.
69
+ *
70
+ * `CREATE TABLE IF NOT EXISTS` silently keeps an existing table's old shape,
71
+ * and the version check above only rebuilds on a version *mismatch*. That
72
+ * leaves a real gap: several wstack processes share this database, and while
73
+ * a version upgrade is rolling out one of them may still be running the
74
+ * previous build. That older process sees the newer version number, drops the
75
+ * tables, and recreates them from *its* DDL — without the newer columns —
76
+ * while the metadata row still reads the new version. Every later query for
77
+ * one of those columns then fails with `no such column`, and no amount of
78
+ * reindexing fixes it, because the version numbers already agree.
79
+ *
80
+ * Repairing column-by-column makes the schema self-healing from any of those
81
+ * states. Table and column names are compile-time literals from this module,
82
+ * never user input.
83
+ */
84
+ private repairMissingColumns;
59
85
  private initSchema;
60
86
  private static readonly NEXT_SYMBOL_ID_KEY;
61
87
  /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
62
88
  private static readonly MAX_SQL_VARS;
89
+ /**
90
+ * Correlated predicate: the ref in `refs` and the candidate symbol aliased
91
+ * `sym` belong to the same language family — or the ref carries no language,
92
+ * in which case the wildcard bind matches everything.
93
+ *
94
+ * Each textual occurrence consumes one `?` bind of {@link LANG_FAMILY_WILDCARD}.
95
+ */
96
+ private static readonly FAMILY_MATCH_SQL;
63
97
  /**
64
98
  * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
65
99
  * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
@@ -101,6 +135,42 @@ export declare class IndexStore {
101
135
  upsertFile(meta: FileMeta): void;
102
136
  getFileMeta(file: string): FileMeta | null;
103
137
  getAllFileMetas(): FileMeta[];
138
+ /** Store the Code Atlas grouping label for each indexed file. */
139
+ setFilePackages(entries: ReadonlyMap<string, string>): void;
140
+ /**
141
+ * Every indexed `namespace`/`module` declaration, for ecosystems whose import
142
+ * specifiers name a namespace rather than a path (C#, PHP, Elixir, Haskell).
143
+ * Ordered so the resolver's choice among duplicate declarations is stable.
144
+ */
145
+ getNamespaceDeclarations(): Array<{
146
+ name: string;
147
+ file: string;
148
+ }>;
149
+ /** `file → package` for every indexed file that has a label. */
150
+ getFilePackages(): Map<string, string>;
151
+ /**
152
+ * Distinct `(fromFile, lang, module)` triples needing module resolution.
153
+ *
154
+ * Distinct rather than per-ref because resolution depends only on these three
155
+ * values: a file importing the same module twenty times resolves it once.
156
+ */
157
+ getUnresolvedImports(onlyFiles?: readonly string[]): Array<{
158
+ fromFile: string;
159
+ lang: string;
160
+ module: string;
161
+ }>;
162
+ /**
163
+ * Write resolved import targets back onto `refs.to_file`.
164
+ *
165
+ * Applied through a temp table and a single UPDATE: one statement per
166
+ * resolution would mean thousands of round-trips on a first index.
167
+ */
168
+ applyImportResolutions(resolutions: ReadonlyArray<{
169
+ fromFile: string;
170
+ lang: string;
171
+ module: string;
172
+ toFile: string;
173
+ }>): number;
104
174
  search(query: string, filter?: WriterSearchFilter, opts?: {
105
175
  limit?: number | undefined;
106
176
  }): SearchResult[];
@@ -228,9 +298,12 @@ export declare class IndexStore {
228
298
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
229
299
  * Call this after all symbols have been inserted to fill in cross-references.
230
300
  *
231
- * Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
232
- * the UPDATE to refs that will actually resolve, so `.changes` counts only refs
233
- * that found a targetmatching the previous per-row loop's return value.
301
+ * A match additionally requires the referencing ref and the target symbol to
302
+ * be in the same {@link LangFamily}. Without that guard a name match is a
303
+ * cross-language accident waiting to happen `main`, `New`, `Parse` and
304
+ * `Config` are declared in most languages at once, and each collision draws a
305
+ * Code Atlas edge between files that never reference each other. Refs stored
306
+ * without a language keep the old global behaviour via the `'*'` wildcard row.
234
307
  */
235
308
  resolveRefs(): number;
236
309
  resolveRefsForNames(names: Iterable<string>): number;
package/dist/exec.js CHANGED
@@ -1131,7 +1131,7 @@ var ProcessRegistryImpl = class {
1131
1131
  const p = this.processes.get(pid);
1132
1132
  if (!p) return false;
1133
1133
  if (p.killed) return true;
1134
- if (p.protected) return false;
1134
+ if (p.protected && opts.includeProtected !== true) return false;
1135
1135
  if (opts.preserveBackground && p.background) return false;
1136
1136
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
1137
1137
  const isWin3 = os.platform() === "win32";
@@ -1182,9 +1182,13 @@ var ProcessRegistryImpl = class {
1182
1182
  killAll(opts = {}) {
1183
1183
  const pids = Array.from(this.processes.keys());
1184
1184
  const killed = [];
1185
+ const includeProtected = opts.includeProtected === true;
1185
1186
  for (const pid of pids) {
1186
1187
  const p = this.processes.get(pid);
1187
- if (p && !p.protected && this.kill(pid, opts)) killed.push(pid);
1188
+ if (!p) continue;
1189
+ if (p.protected && !includeProtected) continue;
1190
+ if (opts.preserveBackground && p.background) continue;
1191
+ if (this.kill(pid, opts)) killed.push(pid);
1188
1192
  }
1189
1193
  return killed;
1190
1194
  }
@@ -2642,6 +2646,26 @@ var BLOCKED_ARG_PATTERNS = {
2642
2646
  pnpm: [],
2643
2647
  npx: []
2644
2648
  };
2649
+ var BLOCKED_OPTION_NAMES = {
2650
+ git: /* @__PURE__ */ new Set([
2651
+ "--exec",
2652
+ "--upload-pack",
2653
+ "--receive-pack",
2654
+ "--exec-path",
2655
+ "--git-dir",
2656
+ "--work-tree",
2657
+ "--namespace",
2658
+ "-c",
2659
+ "--config",
2660
+ "--config-env",
2661
+ "-C"
2662
+ ]),
2663
+ find: /* @__PURE__ */ new Set(["-exec", "-ok", "-execdir"])
2664
+ };
2665
+ function optionName(arg) {
2666
+ const eq = arg.indexOf("=");
2667
+ return eq > 0 ? arg.slice(0, eq) : arg;
2668
+ }
2645
2669
  var BLOCKED_SUBCOMMANDS = {
2646
2670
  docker: /* @__PURE__ */ new Set(["push"]),
2647
2671
  podman: /* @__PURE__ */ new Set(["push"]),
@@ -2679,6 +2703,15 @@ function validateArgs(cmd, args) {
2679
2703
  const blocked2 = blockedSequences.find((seq) => seq.every((part, idx) => actual[idx] === part));
2680
2704
  if (blocked2) return `Blocked subcommand "${blocked2.join(" ")}" for command "${cmd}"`;
2681
2705
  }
2706
+ const blockedOptions = BLOCKED_OPTION_NAMES[cmd];
2707
+ if (blockedOptions) {
2708
+ for (const arg of args) {
2709
+ if (arg === "--") break;
2710
+ if (blockedOptions.has(optionName(arg))) {
2711
+ return `Blocked option "${optionName(arg)}" for command "${cmd}"`;
2712
+ }
2713
+ }
2714
+ }
2682
2715
  const blocked = BLOCKED_ARG_PATTERNS[cmd];
2683
2716
  if (!blocked) return null;
2684
2717
  for (const arg of args) {
package/dist/format.js CHANGED
@@ -666,7 +666,7 @@ var ProcessRegistryImpl = class {
666
666
  const p = this.processes.get(pid);
667
667
  if (!p) return false;
668
668
  if (p.killed) return true;
669
- if (p.protected) return false;
669
+ if (p.protected && opts.includeProtected !== true) return false;
670
670
  if (opts.preserveBackground && p.background) return false;
671
671
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
672
672
  const isWin2 = os.platform() === "win32";
@@ -717,9 +717,13 @@ var ProcessRegistryImpl = class {
717
717
  killAll(opts = {}) {
718
718
  const pids = Array.from(this.processes.keys());
719
719
  const killed = [];
720
+ const includeProtected = opts.includeProtected === true;
720
721
  for (const pid of pids) {
721
722
  const p = this.processes.get(pid);
722
- if (p && !p.protected && this.kill(pid, opts)) killed.push(pid);
723
+ if (!p) continue;
724
+ if (p.protected && !includeProtected) continue;
725
+ if (opts.preserveBackground && p.background) continue;
726
+ if (this.kill(pid, opts)) killed.push(pid);
723
727
  }
724
728
  return killed;
725
729
  }
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';