@wrongstack/tools 0.295.0 → 0.295.1
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/auto-proceed-loop-guard.d.ts +31 -0
- package/dist/auto-proceed-loop-guard.d.ts.map +1 -1
- package/dist/auto-proceed-loop-guard.js +24 -1
- package/dist/auto-proceed-loop-guard.js.map +2 -2
- package/dist/bash.js +39 -23
- package/dist/bash.js.map +2 -2
- package/dist/builtin.js +278 -109
- package/dist/builtin.js.map +4 -4
- package/dist/codebase-index/generic-parser.d.ts.map +1 -1
- package/dist/codebase-index/index.js +48 -47
- package/dist/codebase-index/index.js.map +2 -2
- package/dist/codebase-index/indexer.d.ts.map +1 -1
- package/dist/codebase-index/worker.js +48 -47
- package/dist/codebase-index/worker.js.map +2 -2
- package/dist/codebase-index/writer.d.ts.map +1 -1
- package/dist/exec.js +39 -23
- package/dist/exec.js.map +2 -2
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +80 -30
- package/dist/glob.js.map +4 -4
- package/dist/grep.d.ts.map +1 -1
- package/dist/grep.js +87 -20
- package/dist/grep.js.map +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +281 -109
- package/dist/index.js.map +4 -4
- package/dist/kanban-evidence-bridge.d.ts +21 -0
- package/dist/kanban-evidence-bridge.d.ts.map +1 -0
- package/dist/kanban.d.ts +17 -1
- package/dist/kanban.d.ts.map +1 -1
- package/dist/kanban.js +164 -7
- package/dist/kanban.js.map +3 -3
- package/dist/pack.js +278 -109
- package/dist/pack.js.map +4 -4
- package/dist/process-registry-persistent.d.ts.map +1 -1
- package/dist/ps-slash.js +39 -23
- package/dist/ps-slash.js.map +2 -2
- package/dist/tool-tier.js +278 -109
- package/dist/tool-tier.js.map +4 -4
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +2 -14
- package/dist/tree.js.map +2 -2
- package/dist/win32.d.ts +10 -0
- package/dist/win32.d.ts.map +1 -0
- package/dist/win32.js +67 -0
- package/dist/win32.js.map +7 -0
- package/package.json +7 -3
package/dist/glob.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAKnD,UAAU,SAAS;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;CACpB;AAKD,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CA+JhD,CAAC"}
|
package/dist/glob.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/glob.ts
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
4
|
-
import { compileGlob } from "@wrongstack/core/utils";
|
|
2
|
+
import * as fs2 from "node:fs/promises";
|
|
3
|
+
import * as path3 from "node:path";
|
|
4
|
+
import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS } from "@wrongstack/core/utils";
|
|
5
5
|
|
|
6
6
|
// src/_concurrency.ts
|
|
7
7
|
async function mapWithConcurrency(items, limit, fn) {
|
|
@@ -20,27 +20,82 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
20
20
|
return results;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// src/codebase-index/gitignore.ts
|
|
24
|
+
import * as fs from "node:fs/promises";
|
|
25
|
+
import * as path from "node:path";
|
|
26
|
+
import { compileGlob } from "@wrongstack/core/utils";
|
|
27
|
+
function globBody(glob) {
|
|
28
|
+
return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
|
|
29
|
+
}
|
|
30
|
+
function compileGitignore(lines) {
|
|
31
|
+
const rules = [];
|
|
32
|
+
for (const raw of lines) {
|
|
33
|
+
let line = raw.replace(/\r$/, "");
|
|
34
|
+
if (!line.trim() || line.trimStart().startsWith("#")) continue;
|
|
35
|
+
line = line.trim();
|
|
36
|
+
let negated = false;
|
|
37
|
+
if (line.startsWith("!")) {
|
|
38
|
+
negated = true;
|
|
39
|
+
line = line.slice(1);
|
|
40
|
+
}
|
|
41
|
+
let dirOnly = false;
|
|
42
|
+
if (line.endsWith("/")) {
|
|
43
|
+
dirOnly = true;
|
|
44
|
+
line = line.slice(0, -1);
|
|
45
|
+
}
|
|
46
|
+
if (!line) continue;
|
|
47
|
+
const anchored = line.startsWith("/") || line.includes("/");
|
|
48
|
+
if (line.startsWith("/")) line = line.slice(1);
|
|
49
|
+
const body = globBody(line);
|
|
50
|
+
const prefix = anchored ? "^" : "(?:^|.*/)";
|
|
51
|
+
rules.push({
|
|
52
|
+
eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
|
|
53
|
+
under: new RegExp(`${prefix}${body}/.*$`),
|
|
54
|
+
negated,
|
|
55
|
+
dirOnly
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return (relPath, isDir) => {
|
|
59
|
+
const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
60
|
+
let ignored = false;
|
|
61
|
+
for (const r of rules) {
|
|
62
|
+
const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
|
|
63
|
+
if (re.test(p)) ignored = !r.negated;
|
|
64
|
+
}
|
|
65
|
+
return ignored;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function loadGitignoreMatcher(projectRoot) {
|
|
69
|
+
let lines = [];
|
|
70
|
+
try {
|
|
71
|
+
const raw = await fs.readFile(path.join(projectRoot, ".gitignore"), "utf8");
|
|
72
|
+
lines = raw.split("\n");
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
return compileGitignore(lines);
|
|
76
|
+
}
|
|
77
|
+
|
|
23
78
|
// src/_util.ts
|
|
24
79
|
import * as fsp from "node:fs/promises";
|
|
25
|
-
import * as
|
|
80
|
+
import * as path2 from "node:path";
|
|
26
81
|
import * as Core from "@wrongstack/core/utils";
|
|
27
82
|
function resolvePath(input, ctx) {
|
|
28
|
-
return
|
|
83
|
+
return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
29
84
|
}
|
|
30
85
|
function allowedRoots(ctx) {
|
|
31
|
-
return [
|
|
86
|
+
return [path2.resolve(ctx.projectRoot), path2.resolve(Core.wstackGlobalRoot())];
|
|
32
87
|
}
|
|
33
88
|
function isInsideAny(target, roots) {
|
|
34
89
|
return roots.some((root) => {
|
|
35
|
-
const rel =
|
|
36
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
90
|
+
const rel = path2.relative(root, target);
|
|
91
|
+
return rel === "" || !rel.startsWith("..") && !path2.isAbsolute(rel);
|
|
37
92
|
});
|
|
38
93
|
}
|
|
39
94
|
function ensureInsideRoot(absPath, ctx) {
|
|
40
|
-
const target =
|
|
95
|
+
const target = path2.resolve(absPath);
|
|
41
96
|
if (ctx.allowOutsideProjectRoot) return target;
|
|
42
97
|
if (isInsideAny(target, allowedRoots(ctx))) return target;
|
|
43
|
-
throw new Error(`Path "${absPath}" is outside project root "${
|
|
98
|
+
throw new Error(`Path "${absPath}" is outside project root "${path2.resolve(ctx.projectRoot)}"`);
|
|
44
99
|
}
|
|
45
100
|
function safeResolve(input, ctx) {
|
|
46
101
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
@@ -48,7 +103,7 @@ function safeResolve(input, ctx) {
|
|
|
48
103
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
49
104
|
if (ctx.allowOutsideProjectRoot) return;
|
|
50
105
|
const realRoots = await Promise.all(
|
|
51
|
-
allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() =>
|
|
106
|
+
allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path2.resolve(r)))
|
|
52
107
|
);
|
|
53
108
|
let probe = absPath;
|
|
54
109
|
for (; ; ) {
|
|
@@ -57,7 +112,7 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
57
112
|
real = await fsp.realpath(probe);
|
|
58
113
|
} catch (err) {
|
|
59
114
|
if (err.code === "ENOENT") {
|
|
60
|
-
const parent =
|
|
115
|
+
const parent = path2.dirname(probe);
|
|
61
116
|
if (parent === probe) return;
|
|
62
117
|
probe = parent;
|
|
63
118
|
continue;
|
|
@@ -77,7 +132,7 @@ async function safeResolveReal(input, ctx) {
|
|
|
77
132
|
}
|
|
78
133
|
|
|
79
134
|
// src/glob.ts
|
|
80
|
-
var DEFAULT_IGNORE =
|
|
135
|
+
var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
|
|
81
136
|
var WALK_CONCURRENCY = 16;
|
|
82
137
|
var globTool = {
|
|
83
138
|
name: "glob",
|
|
@@ -117,8 +172,8 @@ var globTool = {
|
|
|
117
172
|
const signal = opts?.signal;
|
|
118
173
|
const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
|
|
119
174
|
const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
|
|
120
|
-
const
|
|
121
|
-
const re =
|
|
175
|
+
const isGitIgnored = await loadGitignoreMatcher(base);
|
|
176
|
+
const re = compileGlob2(input.pattern);
|
|
122
177
|
const results = [];
|
|
123
178
|
let truncated = false;
|
|
124
179
|
const pushResult = async (full) => {
|
|
@@ -127,7 +182,7 @@ var globTool = {
|
|
|
127
182
|
return;
|
|
128
183
|
}
|
|
129
184
|
try {
|
|
130
|
-
const st = await
|
|
185
|
+
const st = await fs2.stat(full);
|
|
131
186
|
if (truncated || results.length >= limit) {
|
|
132
187
|
truncated = true;
|
|
133
188
|
return;
|
|
@@ -148,7 +203,7 @@ var globTool = {
|
|
|
148
203
|
}
|
|
149
204
|
let entries;
|
|
150
205
|
try {
|
|
151
|
-
entries = await
|
|
206
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
152
207
|
} catch {
|
|
153
208
|
return;
|
|
154
209
|
}
|
|
@@ -157,12 +212,13 @@ var globTool = {
|
|
|
157
212
|
for (const e of entries) {
|
|
158
213
|
const name = e.name;
|
|
159
214
|
if (DEFAULT_IGNORE.includes(name)) continue;
|
|
160
|
-
if (ignored.includes(name)) continue;
|
|
161
215
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
162
|
-
const full =
|
|
216
|
+
const full = path3.join(dir, name);
|
|
163
217
|
if (e.isDirectory()) {
|
|
218
|
+
if (isGitIgnored(rel, true)) continue;
|
|
164
219
|
subdirs.push({ full, rel });
|
|
165
220
|
} else if (e.isFile()) {
|
|
221
|
+
if (isGitIgnored(rel, false)) continue;
|
|
166
222
|
re.lastIndex = 0;
|
|
167
223
|
const relMatch = re.test(rel);
|
|
168
224
|
re.lastIndex = 0;
|
|
@@ -172,13 +228,15 @@ var globTool = {
|
|
|
172
228
|
}
|
|
173
229
|
} else if (e.isSymbolicLink()) {
|
|
174
230
|
try {
|
|
175
|
-
const st = await
|
|
231
|
+
const st = await fs2.stat(full);
|
|
176
232
|
if (st.isDirectory()) {
|
|
177
|
-
|
|
233
|
+
if (isGitIgnored(rel, true)) continue;
|
|
234
|
+
const real = await fs2.realpath(full);
|
|
178
235
|
await assertRealInsideRoot(real, ctx);
|
|
179
236
|
subdirs.push({ full, rel });
|
|
180
237
|
} else if (st.isFile()) {
|
|
181
|
-
|
|
238
|
+
if (isGitIgnored(rel, false)) continue;
|
|
239
|
+
const real = await fs2.realpath(full);
|
|
182
240
|
await assertRealInsideRoot(real, ctx);
|
|
183
241
|
re.lastIndex = 0;
|
|
184
242
|
const relMatch = re.test(rel);
|
|
@@ -201,14 +259,6 @@ var globTool = {
|
|
|
201
259
|
return { files: results.map((r) => r.rel), truncated };
|
|
202
260
|
}
|
|
203
261
|
};
|
|
204
|
-
async function readGitignore(dir) {
|
|
205
|
-
try {
|
|
206
|
-
const raw = await fs.readFile(path2.join(dir, ".gitignore"), "utf8");
|
|
207
|
-
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
208
|
-
} catch {
|
|
209
|
-
return [];
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
262
|
export {
|
|
213
263
|
globTool
|
|
214
264
|
};
|
package/dist/glob.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/glob.ts", "../src/_concurrency.ts", "../src/_util.ts"],
|
|
4
|
-
"sourcesContent": ["import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { compileGlob } from '@wrongstack/core/utils';\nimport type { Tool } from '@wrongstack/core/types';\nimport { mapWithConcurrency } from './_concurrency.js';\nimport { assertRealInsideRoot, safeResolveReal } from './_util.js';\n\ninterface GlobInput {\n pattern: string;\n path?: string | undefined;\n limit?: number | undefined;\n}\n\ninterface GlobOutput {\n files: string[];\n truncated: boolean;\n}\n\nconst DEFAULT_IGNORE = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.turbo'];\nconst WALK_CONCURRENCY = 16;\n\nexport const globTool: Tool<GlobInput, GlobOutput> = {\n name: 'glob',\n category: 'Filesystem',\n description:\n 'Find files by path pattern. Use index-backed `codebase-search` first for code symbols or concepts when it is live.',\n usageHint:\n 'PATH DISCOVERY AND SEARCH SCOPING:\\n\\n' +\n '- When `codebase-search` is live, use it first for code concepts; use `glob` for filenames, path patterns, and non-indexed files.\\n' +\n '- Combine with `path` and `limit`.\\n' +\n '- Default ignores common build/dependency directories.\\n' +\n 'Much more efficient than shell `find` for most use cases inside the agent.',\n selection: {\n doNotUseWhen: 'you need to search inside file contents.',\n useInstead: ['grep'],\n },\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'folder',\n maxOutputBytes: 65_536,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n pattern: {\n type: 'string',\n description: 'Glob pattern to match (e.g. \"**/*.ts\", \"src/**\").',\n },\n path: {\n type: 'string',\n description: 'Base directory to search from (defaults to project root).',\n },\n limit: {\n type: 'integer',\n description: 'Maximum number of results to return (default 1000, max 5000).',\n },\n },\n required: ['pattern'],\n },\n async execute(input, ctx, opts) {\n if (!input?.pattern) throw new Error('glob: pattern is required');\n const signal = opts?.signal;\n // `safeResolveReal` validates that the input base \u2014 even if symlinked \u2014\n // resolves to a real path inside the project root (or `~/.wrongstack`).\n // Throws on escape, matching how single-file tools (`read`, `edit`,\n // `write`) reject out-of-root paths: the caller named the base explicitly.\n const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;\n const limit = Math.max(1, Math.min(input.limit ?? 1000, 5000));\n\n const ignored = await readGitignore(base);\n const re = compileGlob(input.pattern);\n\n const results: { rel: string; mtime: number }[] = [];\n let truncated = false;\n const pushResult = async (full: string): Promise<void> => {\n // Bail before stat if a concurrent worker has already filled the budget \u2014\n // the limit is a global cap across all parallel walkers, not per-worker.\n if (truncated || results.length >= limit) {\n truncated = true;\n return;\n }\n try {\n const st = await fs.stat(full);\n // Re-check after the await: another worker may have filled the budget\n // while we were waiting on fs.stat.\n if (truncated || results.length >= limit) {\n truncated = true;\n return;\n }\n results.push({ rel: full, mtime: st.mtimeMs });\n if (results.length >= limit) truncated = true;\n } catch {\n // skip stat error\n }\n };\n const walk = async (dir: string, relPrefix: string): Promise<void> => {\n // Abort check per directory: a huge tree walk must stop promptly on\n // Ctrl+C instead of running to completion with a discarded result.\n if (signal?.aborted) {\n truncated = true;\n return;\n }\n /* v8 ignore start -- the inner limit guards (file push + post-recursion return) always stop first; this re-entry guard is defensive. */\n if (results.length >= limit) {\n truncated = true;\n return;\n }\n /* v8 ignore stop */\n let entries: import('node:fs').Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n const subdirs: Array<{ full: string; rel: string }> = [];\n const matchedFiles: string[] = [];\n for (const e of entries) {\n const name = e.name;\n if (DEFAULT_IGNORE.includes(name)) continue;\n if (ignored.includes(name)) continue;\n const rel = relPrefix ? `${relPrefix}/${name}` : name;\n const full = path.join(dir, name);\n if (e.isDirectory()) {\n subdirs.push({ full, rel });\n } else if (e.isFile()) {\n re.lastIndex = 0;\n const relMatch = re.test(rel);\n re.lastIndex = 0;\n const nameMatch = re.test(name);\n if (relMatch || nameMatch) {\n matchedFiles.push(full);\n }\n } else if (e.isSymbolicLink()) {\n try {\n const st = await fs.stat(full);\n if (st.isDirectory()) {\n // CWE-59 containment: a symlink inside the workspace can point\n // outside it. Before recursing into it (or including the\n // resolved file in results), realpath the symlink and verify\n // its target is still inside the project root. Skip silently\n // on escape so a single bad symlink doesn't poison the whole\n // walk.\n const real = await fs.realpath(full);\n await assertRealInsideRoot(real, ctx);\n subdirs.push({ full, rel });\n } else if (st.isFile()) {\n const real = await fs.realpath(full);\n await assertRealInsideRoot(real, ctx);\n re.lastIndex = 0;\n const relMatch = re.test(rel);\n re.lastIndex = 0;\n const nameMatch = re.test(name);\n if (relMatch || nameMatch) matchedFiles.push(full);\n }\n } catch {\n // Skip broken symlink, stat error, OR out-of-root target. All\n // three should fail the walk without aborting the whole search.\n }\n }\n if (truncated) return;\n }\n await mapWithConcurrency(matchedFiles, WALK_CONCURRENCY, pushResult);\n if (truncated) return;\n // Subdir walks: each one re-checks the limit at entry (re-entry guard),\n // but we also stop dispatching new walks once truncated, so siblings of\n // a hit-limit subdir don't keep adding results.\n const remainingSubdirs = truncated ? [] : subdirs;\n await mapWithConcurrency(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk(full, rel));\n };\n await walk(base, '');\n results.sort((a, b) => b.mtime - a.mtime);\n return { files: results.map((r) => r.rel), truncated };\n },\n};\n\nasync function readGitignore(dir: string): Promise<string[]> {\n try {\n const raw = await fs.readFile(path.join(dir, '.gitignore'), 'utf8');\n return raw\n .split('\\n')\n .map((l) => l.trim())\n .filter((l) => l && !l.startsWith('#'));\n } catch {\n return [];\n }\n}\n", "/**\n * Bounded-concurrency async map.\n *\n * Runs `fn` over `items` with at most `limit` in-flight promises. Order of\n * results matches input order. Errors reject the returned promise on the first\n * failure \u2014 the remaining in-flight work is not awaited; this matches\n * `Promise.all` semantics for callers that want fail-fast behavior.\n *\n * Lives here (rather than imported from `@wrongstack/bench`) because:\n * 1. tools sits below bench in the dependency graph \u2014 bench is a consumer of\n * tools, never the other way around.\n * 2. The semantics used by grep/glob are deliberately simple (fail-fast,\n * no cancellation, no progress) and don't need bench's richer options.\n */\nexport async function mapWithConcurrency<T, R>(\n items: readonly T[],\n limit: number,\n fn: (item: T) => Promise<R>,\n): Promise<R[]> {\n if (items.length === 0) return [];\n const effectiveLimit = Math.max(1, Math.min(limit | 0, items.length));\n const results: R[] = new Array(items.length);\n let nextIndex = 0;\n\n const worker = async (): Promise<void> => {\n while (true) {\n const i = nextIndex++;\n if (i >= items.length) return;\n results[i] = await fn(items[i] as T);\n }\n };\n\n await Promise.all(Array.from({ length: effectiveLimit }, worker));\n return results;\n}\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core/utils';\nimport type { Context } from '@wrongstack/core/agent';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
|
|
5
|
-
"mappings": ";AAAA,
|
|
6
|
-
"names": ["path"]
|
|
3
|
+
"sources": ["../src/glob.ts", "../src/_concurrency.ts", "../src/codebase-index/gitignore.ts", "../src/_util.ts"],
|
|
4
|
+
"sourcesContent": ["import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { compileGlob, DEFAULT_WALK_IGNORE_DIRS } from '@wrongstack/core/utils';\nimport type { Tool } from '@wrongstack/core/types';\nimport { mapWithConcurrency } from './_concurrency.js';\nimport { loadGitignoreMatcher } from './codebase-index/gitignore.js';\nimport { assertRealInsideRoot, safeResolveReal } from './_util.js';\n\ninterface GlobInput {\n pattern: string;\n path?: string | undefined;\n limit?: number | undefined;\n}\n\ninterface GlobOutput {\n files: string[];\n truncated: boolean;\n}\n\nconst DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;\nconst WALK_CONCURRENCY = 16;\n\nexport const globTool: Tool<GlobInput, GlobOutput> = {\n name: 'glob',\n category: 'Filesystem',\n description:\n 'Find files by path pattern. Use index-backed `codebase-search` first for code symbols or concepts when it is live.',\n usageHint:\n 'PATH DISCOVERY AND SEARCH SCOPING:\\n\\n' +\n '- When `codebase-search` is live, use it first for code concepts; use `glob` for filenames, path patterns, and non-indexed files.\\n' +\n '- Combine with `path` and `limit`.\\n' +\n '- Default ignores common build/dependency directories.\\n' +\n 'Much more efficient than shell `find` for most use cases inside the agent.',\n selection: {\n doNotUseWhen: 'you need to search inside file contents.',\n useInstead: ['grep'],\n },\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'folder',\n maxOutputBytes: 65_536,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n pattern: {\n type: 'string',\n description: 'Glob pattern to match (e.g. \"**/*.ts\", \"src/**\").',\n },\n path: {\n type: 'string',\n description: 'Base directory to search from (defaults to project root).',\n },\n limit: {\n type: 'integer',\n description: 'Maximum number of results to return (default 1000, max 5000).',\n },\n },\n required: ['pattern'],\n },\n async execute(input, ctx, opts) {\n if (!input?.pattern) throw new Error('glob: pattern is required');\n const signal = opts?.signal;\n // `safeResolveReal` validates that the input base \u2014 even if symlinked \u2014\n // resolves to a real path inside the project root (or `~/.wrongstack`).\n // Throws on escape, matching how single-file tools (`read`, `edit`,\n // `write`) reject out-of-root paths: the caller named the base explicitly.\n const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;\n const limit = Math.max(1, Math.min(input.limit ?? 1000, 5000));\n\n // Full gitignore semantics (globs, anchors, negation, dir-only rules)\n // rooted at the walk base \u2014 a project whose build output isn't in the\n // static DEFAULT_IGNORE list would otherwise be walked in full.\n const isGitIgnored = await loadGitignoreMatcher(base);\n const re = compileGlob(input.pattern);\n\n const results: { rel: string; mtime: number }[] = [];\n let truncated = false;\n const pushResult = async (full: string): Promise<void> => {\n // Bail before stat if a concurrent worker has already filled the budget \u2014\n // the limit is a global cap across all parallel walkers, not per-worker.\n if (truncated || results.length >= limit) {\n truncated = true;\n return;\n }\n try {\n const st = await fs.stat(full);\n // Re-check after the await: another worker may have filled the budget\n // while we were waiting on fs.stat.\n if (truncated || results.length >= limit) {\n truncated = true;\n return;\n }\n results.push({ rel: full, mtime: st.mtimeMs });\n if (results.length >= limit) truncated = true;\n } catch {\n // skip stat error\n }\n };\n const walk = async (dir: string, relPrefix: string): Promise<void> => {\n // Abort check per directory: a huge tree walk must stop promptly on\n // Ctrl+C instead of running to completion with a discarded result.\n if (signal?.aborted) {\n truncated = true;\n return;\n }\n /* v8 ignore start -- the inner limit guards (file push + post-recursion return) always stop first; this re-entry guard is defensive. */\n if (results.length >= limit) {\n truncated = true;\n return;\n }\n /* v8 ignore stop */\n let entries: import('node:fs').Dirent[];\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n const subdirs: Array<{ full: string; rel: string }> = [];\n const matchedFiles: string[] = [];\n for (const e of entries) {\n const name = e.name;\n if (DEFAULT_IGNORE.includes(name)) continue;\n const rel = relPrefix ? `${relPrefix}/${name}` : name;\n const full = path.join(dir, name);\n if (e.isDirectory()) {\n if (isGitIgnored(rel, true)) continue;\n subdirs.push({ full, rel });\n } else if (e.isFile()) {\n if (isGitIgnored(rel, false)) continue;\n re.lastIndex = 0;\n const relMatch = re.test(rel);\n re.lastIndex = 0;\n const nameMatch = re.test(name);\n if (relMatch || nameMatch) {\n matchedFiles.push(full);\n }\n } else if (e.isSymbolicLink()) {\n try {\n const st = await fs.stat(full);\n if (st.isDirectory()) {\n if (isGitIgnored(rel, true)) continue;\n // CWE-59 containment: a symlink inside the workspace can point\n // outside it. Before recursing into it (or including the\n // resolved file in results), realpath the symlink and verify\n // its target is still inside the project root. Skip silently\n // on escape so a single bad symlink doesn't poison the whole\n // walk.\n const real = await fs.realpath(full);\n await assertRealInsideRoot(real, ctx);\n subdirs.push({ full, rel });\n } else if (st.isFile()) {\n if (isGitIgnored(rel, false)) continue;\n const real = await fs.realpath(full);\n await assertRealInsideRoot(real, ctx);\n re.lastIndex = 0;\n const relMatch = re.test(rel);\n re.lastIndex = 0;\n const nameMatch = re.test(name);\n if (relMatch || nameMatch) matchedFiles.push(full);\n }\n } catch {\n // Skip broken symlink, stat error, OR out-of-root target. All\n // three should fail the walk without aborting the whole search.\n }\n }\n if (truncated) return;\n }\n await mapWithConcurrency(matchedFiles, WALK_CONCURRENCY, pushResult);\n if (truncated) return;\n // Subdir walks: each one re-checks the limit at entry (re-entry guard),\n // but we also stop dispatching new walks once truncated, so siblings of\n // a hit-limit subdir don't keep adding results.\n const remainingSubdirs = truncated ? [] : subdirs;\n await mapWithConcurrency(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk(full, rel));\n };\n await walk(base, '');\n results.sort((a, b) => b.mtime - a.mtime);\n return { files: results.map((r) => r.rel), truncated };\n },\n};\n", "/**\n * Bounded-concurrency async map.\n *\n * Runs `fn` over `items` with at most `limit` in-flight promises. Order of\n * results matches input order. Errors reject the returned promise on the first\n * failure \u2014 the remaining in-flight work is not awaited; this matches\n * `Promise.all` semantics for callers that want fail-fast behavior.\n *\n * Lives here (rather than imported from `@wrongstack/bench`) because:\n * 1. tools sits below bench in the dependency graph \u2014 bench is a consumer of\n * tools, never the other way around.\n * 2. The semantics used by grep/glob are deliberately simple (fail-fast,\n * no cancellation, no progress) and don't need bench's richer options.\n */\nexport async function mapWithConcurrency<T, R>(\n items: readonly T[],\n limit: number,\n fn: (item: T) => Promise<R>,\n): Promise<R[]> {\n if (items.length === 0) return [];\n const effectiveLimit = Math.max(1, Math.min(limit | 0, items.length));\n const results: R[] = new Array(items.length);\n let nextIndex = 0;\n\n const worker = async (): Promise<void> => {\n while (true) {\n const i = nextIndex++;\n if (i >= items.length) return;\n results[i] = await fn(items[i] as T);\n }\n };\n\n await Promise.all(Array.from({ length: effectiveLimit }, worker));\n return results;\n}\n", "/**\n * Minimal but faithful `.gitignore` matcher for the indexer.\n *\n * Supports the parts of the gitignore spec that matter for skipping source\n * files: comments / blanks, `!` negation (last match wins), trailing-slash\n * directory-only rules, leading-slash / embedded-slash anchoring, and the\n * `*` / `**` / `?` / `[...]` globs (via core's {@link compileGlob}).\n *\n * Only the project-root `.gitignore` is read. Nested `.gitignore` files are not\n * walked \u2014 the common build/dependency dirs that would live deeper are already\n * covered by the indexer's always-on `DEFAULT_IGNORE`.\n *\n * Known limitation: a `!negated` file inside an ignored directory will not be\n * re-included, because the indexer prunes ignored directories before descending\n * (a large performance win). This matches most lightweight implementations.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { compileGlob } from '@wrongstack/core/utils';\n\nexport type IgnoreMatcher = (relPath: string, isDir: boolean) => boolean;\n\ninterface Rule {\n /** Matches the entry itself or anything under it (for dirs / plain names). */\n eqOrUnder: RegExp;\n /** Matches only entries strictly under it (for dir-only rules on files). */\n under: RegExp;\n negated: boolean;\n dirOnly: boolean;\n}\n\n/** Strip the `^`/`$` anchors compileGlob adds so we can re-anchor ourselves. */\nfunction globBody(glob: string): string {\n return compileGlob(glob).source.replace(/^\\^/, '').replace(/\\$$/, '');\n}\n\n/** Compile a list of raw `.gitignore` lines into a matcher. */\nexport function compileGitignore(lines: string[]): IgnoreMatcher {\n const rules: Rule[] = [];\n\n for (const raw of lines) {\n let line = raw.replace(/\\r$/, '');\n if (!line.trim() || line.trimStart().startsWith('#')) continue;\n line = line.trim();\n\n let negated = false;\n if (line.startsWith('!')) {\n negated = true;\n line = line.slice(1);\n }\n\n let dirOnly = false;\n if (line.endsWith('/')) {\n dirOnly = true;\n line = line.slice(0, -1);\n }\n if (!line) continue;\n\n // A slash anywhere (after the trailing slash is stripped) anchors the\n // pattern to the gitignore's directory (the project root here). A bare name\n // matches at any depth.\n const anchored = line.startsWith('/') || line.includes('/');\n if (line.startsWith('/')) line = line.slice(1);\n\n const body = globBody(line);\n const prefix = anchored ? '^' : '(?:^|.*/)';\n rules.push({\n eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),\n under: new RegExp(`${prefix}${body}/.*$`),\n negated,\n dirOnly,\n });\n }\n\n return (relPath: string, isDir: boolean): boolean => {\n const p = relPath.replace(/\\\\/g, '/').replace(/^\\/+/, '');\n let ignored = false;\n for (const r of rules) {\n // A directory-only rule never matches a file by its own name; it only\n // matches files that live strictly beneath the named directory.\n const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;\n if (re.test(p)) ignored = !r.negated;\n }\n return ignored;\n };\n}\n\n/** Read `<projectRoot>/.gitignore` and compile it. Missing file \u2192 matches nothing. */\nexport async function loadGitignoreMatcher(projectRoot: string): Promise<IgnoreMatcher> {\n let lines: string[] = [];\n try {\n const raw = await fs.readFile(path.join(projectRoot, '.gitignore'), 'utf8');\n lines = raw.split('\\n');\n } catch {\n // No .gitignore \u2014 nothing extra to ignore beyond the indexer defaults.\n }\n return compileGitignore(lines);\n}\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core/utils';\nimport type { Context } from '@wrongstack/core/agent';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["fs", "path", "compileGlob", "path", "compileGlob"]
|
|
7
7
|
}
|
package/dist/grep.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../src/grep.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,IAAI,EAAmB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../src/grep.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,IAAI,EAAmB,MAAM,wBAAwB,CAAC;AAOpE,UAAU,SAAS;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,WAAW,CAAC,EAAE,SAAS,GAAG,oBAAoB,GAAG,OAAO,GAAG,SAAS,CAAC;IACrE,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,IAAI,GAAG,QAAQ,CAAC;CACvB;AAOD,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAqGhD,CAAC"}
|
package/dist/grep.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/grep.ts
|
|
2
2
|
import { expectDefined } from "@wrongstack/core/utils";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
4
|
+
import * as fs2 from "node:fs/promises";
|
|
5
|
+
import * as path3 from "node:path";
|
|
6
6
|
import { ToolValidationError } from "@wrongstack/core/types";
|
|
7
|
-
import { buildChildEnv, compileGlob } from "@wrongstack/core/utils";
|
|
7
|
+
import { buildChildEnv, compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS } from "@wrongstack/core/utils";
|
|
8
8
|
|
|
9
9
|
// src/_concurrency.ts
|
|
10
10
|
async function mapWithConcurrency(items, limit, fn) {
|
|
@@ -68,26 +68,81 @@ function capSubject(line) {
|
|
|
68
68
|
return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// src/
|
|
71
|
+
// src/codebase-index/gitignore.ts
|
|
72
|
+
import * as fs from "node:fs/promises";
|
|
72
73
|
import * as path from "node:path";
|
|
74
|
+
import { compileGlob } from "@wrongstack/core/utils";
|
|
75
|
+
function globBody(glob) {
|
|
76
|
+
return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
|
|
77
|
+
}
|
|
78
|
+
function compileGitignore(lines) {
|
|
79
|
+
const rules = [];
|
|
80
|
+
for (const raw of lines) {
|
|
81
|
+
let line = raw.replace(/\r$/, "");
|
|
82
|
+
if (!line.trim() || line.trimStart().startsWith("#")) continue;
|
|
83
|
+
line = line.trim();
|
|
84
|
+
let negated = false;
|
|
85
|
+
if (line.startsWith("!")) {
|
|
86
|
+
negated = true;
|
|
87
|
+
line = line.slice(1);
|
|
88
|
+
}
|
|
89
|
+
let dirOnly = false;
|
|
90
|
+
if (line.endsWith("/")) {
|
|
91
|
+
dirOnly = true;
|
|
92
|
+
line = line.slice(0, -1);
|
|
93
|
+
}
|
|
94
|
+
if (!line) continue;
|
|
95
|
+
const anchored = line.startsWith("/") || line.includes("/");
|
|
96
|
+
if (line.startsWith("/")) line = line.slice(1);
|
|
97
|
+
const body = globBody(line);
|
|
98
|
+
const prefix = anchored ? "^" : "(?:^|.*/)";
|
|
99
|
+
rules.push({
|
|
100
|
+
eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
|
|
101
|
+
under: new RegExp(`${prefix}${body}/.*$`),
|
|
102
|
+
negated,
|
|
103
|
+
dirOnly
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return (relPath, isDir) => {
|
|
107
|
+
const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
108
|
+
let ignored = false;
|
|
109
|
+
for (const r of rules) {
|
|
110
|
+
const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
|
|
111
|
+
if (re.test(p)) ignored = !r.negated;
|
|
112
|
+
}
|
|
113
|
+
return ignored;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function loadGitignoreMatcher(projectRoot) {
|
|
117
|
+
let lines = [];
|
|
118
|
+
try {
|
|
119
|
+
const raw = await fs.readFile(path.join(projectRoot, ".gitignore"), "utf8");
|
|
120
|
+
lines = raw.split("\n");
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
return compileGitignore(lines);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/_util.ts
|
|
127
|
+
import * as path2 from "node:path";
|
|
73
128
|
import * as Core from "@wrongstack/core/utils";
|
|
74
129
|
function resolvePath(input, ctx) {
|
|
75
|
-
return
|
|
130
|
+
return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
76
131
|
}
|
|
77
132
|
function allowedRoots(ctx) {
|
|
78
|
-
return [
|
|
133
|
+
return [path2.resolve(ctx.projectRoot), path2.resolve(Core.wstackGlobalRoot())];
|
|
79
134
|
}
|
|
80
135
|
function isInsideAny(target, roots) {
|
|
81
136
|
return roots.some((root) => {
|
|
82
|
-
const rel =
|
|
83
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
137
|
+
const rel = path2.relative(root, target);
|
|
138
|
+
return rel === "" || !rel.startsWith("..") && !path2.isAbsolute(rel);
|
|
84
139
|
});
|
|
85
140
|
}
|
|
86
141
|
function ensureInsideRoot(absPath, ctx) {
|
|
87
|
-
const target =
|
|
142
|
+
const target = path2.resolve(absPath);
|
|
88
143
|
if (ctx.allowOutsideProjectRoot) return target;
|
|
89
144
|
if (isInsideAny(target, allowedRoots(ctx))) return target;
|
|
90
|
-
throw new Error(`Path "${absPath}" is outside project root "${
|
|
145
|
+
throw new Error(`Path "${absPath}" is outside project root "${path2.resolve(ctx.projectRoot)}"`);
|
|
91
146
|
}
|
|
92
147
|
function safeResolve(input, ctx) {
|
|
93
148
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
@@ -101,7 +156,7 @@ function isBinaryBuffer(buf) {
|
|
|
101
156
|
}
|
|
102
157
|
|
|
103
158
|
// src/grep.ts
|
|
104
|
-
var DEFAULT_IGNORE =
|
|
159
|
+
var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
|
|
105
160
|
var NATIVE_SCAN_CONCURRENCY = 32;
|
|
106
161
|
var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
|
|
107
162
|
var NATIVE_MAX_FILE_BYTES = 1e6;
|
|
@@ -218,6 +273,10 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
218
273
|
for (const ignored of DEFAULT_IGNORE) {
|
|
219
274
|
args.push("--glob", `!${ignored}/**`, "--glob", `!**/${ignored}/**`);
|
|
220
275
|
}
|
|
276
|
+
const gitignorePath = path3.join(base, ".gitignore");
|
|
277
|
+
if (await fs2.access(gitignorePath).then(() => true, () => false)) {
|
|
278
|
+
args.push("--ignore-file", gitignorePath);
|
|
279
|
+
}
|
|
221
280
|
if (input.glob) args.push("--glob", input.glob);
|
|
222
281
|
args.push("--", input.pattern, base);
|
|
223
282
|
const matches = [];
|
|
@@ -342,7 +401,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
342
401
|
});
|
|
343
402
|
}
|
|
344
403
|
const re = compiled.regex;
|
|
345
|
-
const globRe = input.glob ?
|
|
404
|
+
const globRe = input.glob ? compileGlob2(input.glob) : null;
|
|
405
|
+
const isGitIgnored = await loadGitignoreMatcher(base);
|
|
346
406
|
const matches = [];
|
|
347
407
|
const countOnlyFirstHit = mode === "count" && limit === 1;
|
|
348
408
|
const maxBytes = mode === "content" ? NATIVE_MAX_FILE_BYTES : Math.min(NATIVE_MAX_FILE_BYTES, 256 * 1024);
|
|
@@ -353,9 +413,9 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
353
413
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
354
414
|
if (globRe) globRe.lastIndex = 0;
|
|
355
415
|
try {
|
|
356
|
-
const stat2 = await
|
|
416
|
+
const stat2 = await fs2.stat(full);
|
|
357
417
|
if (!stat2.isFile() || stat2.size > maxBytes || stopped || signal.aborted) return;
|
|
358
|
-
const file = await
|
|
418
|
+
const file = await fs2.open(full, "r");
|
|
359
419
|
try {
|
|
360
420
|
let bytesReadTotal = 0;
|
|
361
421
|
let lineNumber = 0;
|
|
@@ -430,11 +490,11 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
430
490
|
} catch {
|
|
431
491
|
}
|
|
432
492
|
};
|
|
433
|
-
const walk = async (dir) => {
|
|
493
|
+
const walk = async (dir, relPrefix) => {
|
|
434
494
|
if (stopped || signal.aborted) return;
|
|
435
495
|
let entries;
|
|
436
496
|
try {
|
|
437
|
-
entries = await
|
|
497
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
438
498
|
} catch {
|
|
439
499
|
return;
|
|
440
500
|
}
|
|
@@ -444,17 +504,24 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
444
504
|
if (stopped) return;
|
|
445
505
|
if (DEFAULT_IGNORE.includes(e.name)) continue;
|
|
446
506
|
if (e.isSymbolicLink()) continue;
|
|
447
|
-
const
|
|
507
|
+
const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
|
|
508
|
+
const full = path3.join(dir, e.name);
|
|
448
509
|
if (e.isDirectory()) {
|
|
449
|
-
|
|
510
|
+
if (isGitIgnored(rel, true)) continue;
|
|
511
|
+
subdirs.push({ full, rel });
|
|
450
512
|
} else if (e.isFile()) {
|
|
513
|
+
if (isGitIgnored(rel, false)) continue;
|
|
451
514
|
files.push({ full, name: e.name });
|
|
452
515
|
}
|
|
453
516
|
}
|
|
454
517
|
await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
|
|
455
|
-
await mapWithConcurrency(
|
|
518
|
+
await mapWithConcurrency(
|
|
519
|
+
subdirs,
|
|
520
|
+
Math.min(16, NATIVE_SCAN_CONCURRENCY),
|
|
521
|
+
({ full, rel }) => walk(full, rel)
|
|
522
|
+
);
|
|
456
523
|
};
|
|
457
|
-
await walk(base);
|
|
524
|
+
await walk(base, "");
|
|
458
525
|
return {
|
|
459
526
|
matches,
|
|
460
527
|
count: total,
|