@wrongstack/tools 0.1.4 → 0.1.8
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/README.md +127 -0
- package/dist/audit.js.map +1 -1
- package/dist/bash.js +103 -5
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +550 -258
- package/dist/builtin.js.map +1 -1
- package/dist/diff.js +5 -9
- package/dist/diff.js.map +1 -1
- package/dist/document.js +0 -1
- package/dist/document.js.map +1 -1
- package/dist/edit.js +2 -2
- package/dist/edit.js.map +1 -1
- package/dist/exec.d.ts +0 -1
- package/dist/exec.js +105 -44
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js +110 -25
- package/dist/fetch.js.map +1 -1
- package/dist/format.js.map +1 -1
- package/dist/git.d.ts +0 -1
- package/dist/git.js +9 -9
- package/dist/git.js.map +1 -1
- package/dist/glob.js +0 -1
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +58 -3
- package/dist/grep.js.map +1 -1
- package/dist/index.js +549 -257
- package/dist/index.js.map +1 -1
- package/dist/install.js.map +1 -1
- package/dist/lint.js.map +1 -1
- package/dist/logs.js +61 -6
- package/dist/logs.js.map +1 -1
- package/dist/outdated.js.map +1 -1
- package/dist/patch.js +68 -29
- package/dist/patch.js.map +1 -1
- package/dist/read.js +0 -1
- package/dist/read.js.map +1 -1
- package/dist/replace.js +59 -9
- package/dist/replace.js.map +1 -1
- package/dist/scaffold.js +5 -6
- package/dist/scaffold.js.map +1 -1
- package/dist/test.js.map +1 -1
- package/dist/todo.js +1 -1
- package/dist/todo.js.map +1 -1
- package/dist/tool-use.js +0 -8
- package/dist/tool-use.js.map +1 -1
- package/dist/tree.js +9 -5
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js.map +1 -1
- package/dist/write.js +0 -1
- package/dist/write.js.map +1 -1
- package/package.json +7 -4
package/dist/grep.js
CHANGED
|
@@ -27,6 +27,46 @@ function isBinaryBuffer(buf) {
|
|
|
27
27
|
return false;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// src/_regex.ts
|
|
31
|
+
var MAX_PATTERN_LEN = 512;
|
|
32
|
+
var DANGEROUS_PATTERNS = [
|
|
33
|
+
/(\([^)]*[+*][^)]*\))[+*]/,
|
|
34
|
+
// (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
|
|
35
|
+
/(\(\?:[^)]*[+*][^)]*\))[+*]/
|
|
36
|
+
// same, with non-capturing group
|
|
37
|
+
];
|
|
38
|
+
function compileUserRegex(pattern, flags) {
|
|
39
|
+
if (typeof pattern !== "string") {
|
|
40
|
+
return { ok: false, reason: "pattern must be a string" };
|
|
41
|
+
}
|
|
42
|
+
if (pattern.length === 0) {
|
|
43
|
+
return { ok: false, reason: "pattern is empty" };
|
|
44
|
+
}
|
|
45
|
+
if (pattern.length > MAX_PATTERN_LEN) {
|
|
46
|
+
return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
|
|
47
|
+
}
|
|
48
|
+
for (const rx of DANGEROUS_PATTERNS) {
|
|
49
|
+
if (rx.test(pattern)) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return { ok: true, regex: new RegExp(pattern, flags) };
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
reason: err instanceof Error ? err.message : "invalid regex"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
var MAX_SUBJECT_LEN = 64 * 1024;
|
|
66
|
+
function capSubject(line) {
|
|
67
|
+
return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;
|
|
68
|
+
}
|
|
69
|
+
|
|
30
70
|
// src/grep.ts
|
|
31
71
|
var DEFAULT_IGNORE = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
32
72
|
var grepTool = {
|
|
@@ -103,6 +143,8 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
103
143
|
let totalLines = 0;
|
|
104
144
|
let batchSinceFlush = 0;
|
|
105
145
|
const FLUSH_AT = 16;
|
|
146
|
+
const MAX_BUF_BYTES = 1e6;
|
|
147
|
+
let bufOverflow = false;
|
|
106
148
|
const child = spawn("rg", args, { signal, stdio: ["ignore", "pipe", "pipe"] });
|
|
107
149
|
const queue = [];
|
|
108
150
|
let waiter;
|
|
@@ -140,6 +182,14 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
140
182
|
}
|
|
141
183
|
if (c.kind === "close") break;
|
|
142
184
|
buf += c.data;
|
|
185
|
+
if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
|
|
186
|
+
bufOverflow = true;
|
|
187
|
+
buf = buf.slice(-MAX_BUF_BYTES);
|
|
188
|
+
try {
|
|
189
|
+
child.kill("SIGTERM");
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
143
193
|
const idx = buf.lastIndexOf("\n");
|
|
144
194
|
if (idx === -1) continue;
|
|
145
195
|
const ready = buf.slice(0, idx);
|
|
@@ -186,14 +236,18 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
186
236
|
output: {
|
|
187
237
|
matches,
|
|
188
238
|
count: totalLines,
|
|
189
|
-
truncated: totalLines > limit,
|
|
239
|
+
truncated: totalLines > limit || bufOverflow,
|
|
190
240
|
used: "rg"
|
|
191
241
|
}
|
|
192
242
|
};
|
|
193
243
|
}
|
|
194
244
|
async function runNative(input, base, mode, limit, signal) {
|
|
195
245
|
const flags = input.case_insensitive ? "i" : "";
|
|
196
|
-
const
|
|
246
|
+
const compiled = compileUserRegex(input.pattern, flags);
|
|
247
|
+
if (!compiled.ok) {
|
|
248
|
+
throw new Error(`grep: ${compiled.reason}`);
|
|
249
|
+
}
|
|
250
|
+
const re = compiled.regex;
|
|
197
251
|
const globRe = input.glob ? compileGlob(input.glob) : null;
|
|
198
252
|
const matches = [];
|
|
199
253
|
const fileMatches = /* @__PURE__ */ new Map();
|
|
@@ -210,6 +264,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
210
264
|
for (const e of entries) {
|
|
211
265
|
if (stopped) return;
|
|
212
266
|
if (DEFAULT_IGNORE.includes(e.name)) continue;
|
|
267
|
+
if (e.isSymbolicLink()) continue;
|
|
213
268
|
const full = path.join(dir, e.name);
|
|
214
269
|
if (e.isDirectory()) {
|
|
215
270
|
await walk(full);
|
|
@@ -225,7 +280,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
225
280
|
const lines = text.split(/\r?\n/);
|
|
226
281
|
let fileHits = 0;
|
|
227
282
|
for (let i = 0; i < lines.length; i++) {
|
|
228
|
-
const ln = lines[i] ?? "";
|
|
283
|
+
const ln = capSubject(lines[i] ?? "");
|
|
229
284
|
re.lastIndex = 0;
|
|
230
285
|
if (re.test(ln)) {
|
|
231
286
|
fileHits++;
|
package/dist/grep.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/_util.ts","../src/grep.ts"],"names":["resolve","spawn","path2","stat"],"mappings":"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","file":"grep.js","sourcesContent":["import * as path from 'node:path';\r\nimport { spawn } from 'node:child_process';\r\nimport type { Context, ToolProgressEvent } from '@wrongstack/core';\r\n\r\nexport function resolvePath(input: string, ctx: Context): string {\r\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\r\n}\r\n\r\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(absPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\r\n }\r\n return target;\r\n}\r\n\r\nexport function safeResolve(input: string, ctx: Context): string {\r\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\r\n}\r\n\r\nexport function truncateMiddle(s: string, max: number): string {\r\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\r\n const half = Math.floor(max / 2);\r\n return (\r\n s.slice(0, half) +\r\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\r\n s.slice(-half)\r\n );\r\n}\r\n\r\nexport function isBinaryBuffer(buf: Buffer): boolean {\r\n const len = Math.min(buf.length, 8192);\r\n for (let i = 0; i < len; i++) {\r\n if (buf[i] === 0) return true;\r\n }\r\n return false;\r\n}\r\n\r\nexport interface SpawnStreamResult {\r\n stdout: string;\r\n stderr: string;\r\n exitCode: number;\r\n truncated: boolean;\r\n error?: string;\r\n}\r\n\r\nexport interface SpawnStreamOptions {\r\n cmd: string;\r\n args: string[];\r\n cwd: string;\r\n signal: AbortSignal;\r\n maxBytes?: number;\r\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\r\n flushBytes?: number;\r\n}\r\n\r\n/**\r\n * Spawn a child process and yield `partial_output` progress events as\r\n * stdout/stderr arrive (batched by byte threshold), then return the full\r\n * buffered result. Shared between install/lint/format/typecheck/test/audit\r\n * so the TUI live tail sees consistent progress regardless of which tool\r\n * is running.\r\n */\r\nexport async function* spawnStream(\r\n opts: SpawnStreamOptions,\r\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\r\n const max = opts.maxBytes ?? 200_000;\r\n const flushAt = opts.flushBytes ?? 4 * 1024;\r\n let stdout = '';\r\n let stderr = '';\r\n let pending = '';\r\n let error: string | undefined;\r\n\r\n const child = spawn(opts.cmd, opts.args, {\r\n cwd: opts.cwd,\r\n signal: opts.signal,\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n });\r\n\r\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number };\r\n const queue: Chunk[] = [];\r\n let waiter: (() => void) | undefined;\r\n const wake = () => {\r\n if (waiter) {\r\n const w = waiter;\r\n waiter = undefined;\r\n w();\r\n }\r\n };\r\n\r\n child.stdout?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stdout.length < max) stdout += s;\r\n queue.push({ kind: 'out', data: s });\r\n wake();\r\n });\r\n child.stderr?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stderr.length < max) stderr += s;\r\n queue.push({ kind: 'err', data: s });\r\n wake();\r\n });\r\n child.on('error', (e) => {\r\n error = e.message;\r\n queue.push({ kind: 'error', data: e.message });\r\n wake();\r\n });\r\n child.on('close', (code) => {\r\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\r\n wake();\r\n });\r\n\r\n let exitCode = 0;\r\n let spawnFailed = false;\r\n for (;;) {\r\n while (queue.length === 0) {\r\n await new Promise<void>((resolve) => {\r\n waiter = resolve;\r\n });\r\n }\r\n const chunk = queue.shift()!;\r\n if (chunk.kind === 'close') {\r\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\r\n // rather than the negative platform code Node fabricates.\r\n if (!spawnFailed) exitCode = chunk.code ?? 0;\r\n break;\r\n }\r\n if (chunk.kind === 'error') {\r\n spawnFailed = true;\r\n exitCode = 1;\r\n // close usually follows\r\n continue;\r\n }\r\n pending += chunk.data;\r\n if (pending.length >= flushAt) {\r\n yield { type: 'partial_output', text: pending };\r\n pending = '';\r\n }\r\n }\r\n if (pending.length > 0) {\r\n yield { type: 'partial_output', text: pending };\r\n }\r\n\r\n return {\r\n stdout,\r\n stderr,\r\n exitCode,\r\n truncated: stdout.length >= max || stderr.length >= max,\r\n error,\r\n };\r\n}\r\n","import * as fs from 'node:fs/promises';\r\nimport * as path from 'node:path';\r\nimport { spawn } from 'node:child_process';\r\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\r\nimport { compileGlob } from '@wrongstack/core';\r\nimport { isBinaryBuffer, safeResolve } from './_util.js';\r\n\r\ninterface GrepInput {\r\n pattern: string;\r\n path?: string;\r\n glob?: string;\r\n output_mode?: 'content' | 'files_with_matches' | 'count';\r\n context_lines?: number;\r\n case_insensitive?: boolean;\r\n limit?: number;\r\n}\r\n\r\ninterface GrepOutput {\r\n matches: string[];\r\n count: number;\r\n truncated: boolean;\r\n used: 'rg' | 'native';\r\n}\r\n\r\nconst DEFAULT_IGNORE = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];\r\n\r\nexport const grepTool: Tool<GrepInput, GrepOutput> = {\r\n name: 'grep',\r\n description: 'Search file contents with a regex. Uses ripgrep when available.',\r\n usageHint:\r\n 'Pattern is regex. Use `output_mode: \"content\"` for matched lines, `\"files_with_matches\"` for paths, `\"count\"` for tallies. `glob` filters files (e.g. `*.ts`).',\r\n permission: 'auto',\r\n mutating: false,\r\n maxOutputBytes: 131_072,\r\n timeoutMs: 10_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n pattern: { type: 'string' },\r\n path: { type: 'string' },\r\n glob: { type: 'string' },\r\n output_mode: { type: 'string', enum: ['content', 'files_with_matches', 'count'] },\r\n context_lines: { type: 'integer' },\r\n case_insensitive: { type: 'boolean' },\r\n limit: { type: 'integer' },\r\n },\r\n required: ['pattern'],\r\n },\r\n async execute(input, ctx, opts) {\r\n let final: GrepOutput | undefined;\r\n for await (const ev of grepTool.executeStream!(input, ctx, opts)) {\r\n if (ev.type === 'final') final = ev.output;\r\n }\r\n if (!final) throw new Error('grep: stream ended without final event');\r\n return final;\r\n },\r\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<GrepOutput>> {\r\n if (!input?.pattern) throw new Error('grep: pattern is required');\r\n const base = input.path ? safeResolve(input.path, ctx) : ctx.cwd;\r\n const mode = input.output_mode ?? 'content';\r\n const limit = Math.max(1, Math.min(input.limit ?? 200, 2000));\r\n\r\n const rgAvailable = await detectRg(opts.signal);\r\n if (rgAvailable) {\r\n try {\r\n yield* runRgStream(input, base, mode, limit, opts.signal);\r\n return;\r\n } catch {\r\n // fall through to native\r\n }\r\n }\r\n yield { type: 'log', text: 'Falling back to native grep…' };\r\n const out = await runNative(input, base, mode, limit, opts.signal);\r\n yield { type: 'final', output: out };\r\n },\r\n};\r\n\r\nasync function detectRg(signal: AbortSignal): Promise<boolean> {\r\n return new Promise((resolve) => {\r\n try {\r\n const p = spawn('rg', ['--version'], { stdio: 'ignore', signal });\r\n p.on('error', () => resolve(false));\r\n p.on('close', (code) => resolve(code === 0));\r\n } catch {\r\n resolve(false);\r\n }\r\n });\r\n}\r\n\r\nasync function* runRgStream(\r\n input: GrepInput,\r\n base: string,\r\n mode: 'content' | 'files_with_matches' | 'count',\r\n limit: number,\r\n signal: AbortSignal,\r\n): AsyncGenerator<ToolStreamEvent<GrepOutput>> {\r\n const args: string[] = ['--no-heading'];\r\n if (input.case_insensitive) args.push('-i');\r\n if (mode === 'files_with_matches') args.push('-l');\r\n if (mode === 'count') args.push('-c');\r\n if (mode === 'content') {\r\n args.push('-n');\r\n if (input.context_lines) args.push('-C', String(input.context_lines));\r\n }\r\n if (input.glob) args.push('--glob', input.glob);\r\n args.push('--', input.pattern, base);\r\n\r\n const matches: string[] = [];\r\n let buf = '';\r\n let totalLines = 0;\r\n let batchSinceFlush = 0;\r\n const FLUSH_AT = 16; // yield a partial_output every 16 matches\r\n\r\n const child = spawn('rg', args, { signal, stdio: ['ignore', 'pipe', 'pipe'] });\r\n\r\n type Chunk = { kind: 'out' | 'close' | 'error'; data: string };\r\n const queue: Chunk[] = [];\r\n let waiter: (() => void) | undefined;\r\n const wake = () => {\r\n if (waiter) {\r\n const w = waiter;\r\n waiter = undefined;\r\n w();\r\n }\r\n };\r\n child.stdout?.on('data', (c) => {\r\n queue.push({ kind: 'out', data: c.toString() });\r\n wake();\r\n });\r\n child.on('error', (e) => {\r\n queue.push({ kind: 'error', data: e.message });\r\n wake();\r\n });\r\n child.on('close', () => {\r\n queue.push({ kind: 'close', data: '' });\r\n wake();\r\n });\r\n\r\n let pendingBatch: string[] = [];\r\n let errored = false;\r\n for (;;) {\r\n while (queue.length === 0) {\r\n await new Promise<void>((r) => {\r\n waiter = r;\r\n });\r\n }\r\n const c = queue.shift()!;\r\n if (c.kind === 'error') {\r\n errored = true;\r\n continue;\r\n }\r\n if (c.kind === 'close') break;\r\n buf += c.data;\r\n const idx = buf.lastIndexOf('\\n');\r\n if (idx === -1) continue;\r\n const ready = buf.slice(0, idx);\r\n buf = buf.slice(idx + 1);\r\n for (const line of ready.split('\\n')) {\r\n if (!line) continue;\r\n totalLines++;\r\n if (matches.length < limit) {\r\n matches.push(line);\r\n pendingBatch.push(line);\r\n batchSinceFlush++;\r\n }\r\n }\r\n if (batchSinceFlush >= FLUSH_AT) {\r\n yield {\r\n type: 'partial_output',\r\n text: pendingBatch.join('\\n'),\r\n data: { matches_so_far: matches.length },\r\n };\r\n pendingBatch = [];\r\n batchSinceFlush = 0;\r\n }\r\n }\r\n\r\n if (buf.trim()) {\r\n for (const line of buf.split('\\n')) {\r\n if (!line) continue;\r\n totalLines++;\r\n if (matches.length < limit) {\r\n matches.push(line);\r\n pendingBatch.push(line);\r\n }\r\n }\r\n }\r\n if (pendingBatch.length > 0) {\r\n yield {\r\n type: 'partial_output',\r\n text: pendingBatch.join('\\n'),\r\n data: { matches_so_far: matches.length },\r\n };\r\n }\r\n if (errored) throw new Error('rg: spawn error');\r\n\r\n yield {\r\n type: 'final',\r\n output: {\r\n matches,\r\n count: totalLines,\r\n truncated: totalLines > limit,\r\n used: 'rg',\r\n },\r\n };\r\n}\r\n\r\nasync function runNative(\r\n input: GrepInput,\r\n base: string,\r\n mode: 'content' | 'files_with_matches' | 'count',\r\n limit: number,\r\n signal: AbortSignal,\r\n): Promise<GrepOutput> {\r\n const flags = input.case_insensitive ? 'i' : '';\r\n const re = new RegExp(input.pattern, flags);\r\n const globRe = input.glob ? compileGlob(input.glob) : null;\r\n const matches: string[] = [];\r\n const fileMatches = new Map<string, number>();\r\n let total = 0;\r\n let stopped = false;\r\n\r\n const walk = async (dir: string): Promise<void> => {\r\n if (stopped || signal.aborted) return;\r\n let entries: import('node:fs').Dirent[];\r\n try {\r\n entries = await fs.readdir(dir, { withFileTypes: true });\r\n } catch {\r\n return;\r\n }\r\n for (const e of entries) {\r\n if (stopped) return;\r\n if (DEFAULT_IGNORE.includes(e.name)) continue;\r\n const full = path.join(dir, e.name);\r\n if (e.isDirectory()) {\r\n await walk(full);\r\n } else if (e.isFile()) {\r\n if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;\r\n if (globRe) globRe.lastIndex = 0;\r\n try {\r\n const stat = await fs.stat(full);\r\n if (stat.size > 1_000_000) continue;\r\n const head = await fs.readFile(full);\r\n if (isBinaryBuffer(head)) continue;\r\n const text = head.toString('utf8');\r\n const lines = text.split(/\\r?\\n/);\r\n let fileHits = 0;\r\n for (let i = 0; i < lines.length; i++) {\r\n const ln = lines[i] ?? '';\r\n re.lastIndex = 0;\r\n if (re.test(ln)) {\r\n fileHits++;\r\n total++;\r\n if (mode === 'content' && matches.length < limit) {\r\n matches.push(`${full}:${i + 1}:${ln}`);\r\n }\r\n }\r\n }\r\n if (fileHits > 0) {\r\n fileMatches.set(full, fileHits);\r\n if (mode === 'files_with_matches' && matches.length < limit) {\r\n matches.push(full);\r\n }\r\n if (mode === 'count' && matches.length < limit) {\r\n matches.push(`${full}:${fileHits}`);\r\n }\r\n }\r\n if (matches.length >= limit) stopped = true;\r\n } catch {\r\n // skip read errors\r\n }\r\n }\r\n }\r\n };\r\n await walk(base);\r\n\r\n return {\r\n matches,\r\n count: total,\r\n truncated: stopped,\r\n used: 'native',\r\n };\r\n}\r\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/_util.ts","../src/_regex.ts","../src/grep.ts"],"names":["resolve","path2","stat"],"mappings":";;;;;;AAGO,SAAS,WAAA,CAAY,OAAe,GAAA,EAAsB;AAC/D,EAAA,OAAY,IAAA,CAAA,UAAA,CAAW,KAAK,CAAA,GAAS,IAAA,CAAA,SAAA,CAAU,KAAK,CAAA,GAAS,IAAA,CAAA,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AACrF;AAEO,SAAS,gBAAA,CAAiB,SAAiB,GAAA,EAAsB;AACtE,EAAA,MAAM,IAAA,GAAY,IAAA,CAAA,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA;AACzC,EAAA,MAAM,MAAA,GAAc,aAAQ,OAAO,CAAA;AACnC,EAAA,MAAM,GAAA,GAAW,IAAA,CAAA,QAAA,CAAS,IAAA,EAAM,MAAM,CAAA;AACtC,EAAA,IAAI,IAAI,UAAA,CAAW,IAAI,CAAA,IAAU,IAAA,CAAA,UAAA,CAAW,GAAG,CAAA,EAAG;AAChD,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,MAAA,EAAS,OAAO,CAAA,2BAAA,EAA8B,IAAI,CAAA,CAAA,CAAG,CAAA;AAAA,EACvE;AACA,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,WAAA,CAAY,OAAe,GAAA,EAAsB;AAC/D,EAAA,OAAO,gBAAA,CAAiB,WAAA,CAAY,KAAA,EAAO,GAAG,GAAG,GAAG,CAAA;AACtD;AAYO,SAAS,eAAe,GAAA,EAAsB;AACnD,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,QAAQ,IAAI,CAAA;AACrC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,EAAK,CAAA,EAAA,EAAK;AAC5B,IAAA,IAAI,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,EAAG,OAAO,IAAA;AAAA,EAC3B;AACA,EAAA,OAAO,KAAA;AACT;;;ACdA,IAAM,eAAA,GAAkB,GAAA;AAIxB,IAAM,kBAAA,GAA4C;AAAA,EAChD,0BAAA;AAAA;AAAA,EACA;AAAA;AACF,CAAA;AAYO,SAAS,gBAAA,CACd,SACA,KAAA,EAC6B;AAC7B,EAAA,IAAI,OAAO,YAAY,QAAA,EAAU;AAC/B,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,0BAAA,EAA2B;AAAA,EACzD;AACA,EAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AACxB,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,kBAAA,EAAmB;AAAA,EACjD;AACA,EAAA,IAAI,OAAA,CAAQ,SAAS,eAAA,EAAiB;AACpC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,CAAA,gBAAA,EAAmB,eAAe,CAAA,WAAA,CAAA,EAAc;AAAA,EAC9E;AACA,EAAA,KAAA,MAAW,MAAM,kBAAA,EAAoB;AACnC,IAAA,IAAI,EAAA,CAAG,IAAA,CAAK,OAAO,CAAA,EAAG;AACpB,MAAA,OAAO;AAAA,QACL,EAAA,EAAI,KAAA;AAAA,QACJ,MAAA,EAAQ;AAAA,OACV;AAAA,IACF;AAAA,EACF;AACA,EAAA,IAAI;AACF,IAAA,OAAO,EAAE,IAAI,IAAA,EAAM,KAAA,EAAO,IAAI,MAAA,CAAO,OAAA,EAAS,KAAK,CAAA,EAAE;AAAA,EACvD,SAAS,GAAA,EAAK;AACZ,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU;AAAA,KAC/C;AAAA,EACF;AACF;AAOO,IAAM,kBAAkB,EAAA,GAAK,IAAA;AAE7B,SAAS,WAAW,IAAA,EAAsB;AAC/C,EAAA,OAAO,KAAK,MAAA,GAAS,eAAA,GAAkB,KAAK,KAAA,CAAM,CAAA,EAAG,eAAe,CAAA,GAAI,IAAA;AAC1E;;;ACzDA,IAAM,iBAAiB,CAAC,cAAA,EAAgB,QAAQ,MAAA,EAAQ,OAAA,EAAS,SAAS,UAAU,CAAA;AAE7E,IAAM,QAAA,GAAwC;AAAA,EACnD,IAAA,EAAM,MAAA;AAAA,EACN,WAAA,EAAa,iEAAA;AAAA,EACb,SAAA,EACE,gKAAA;AAAA,EACF,UAAA,EAAY,MAAA;AAAA,EACZ,QAAA,EAAU,KAAA;AAAA,EACV,cAAA,EAAgB,MAAA;AAAA,EAChB,SAAA,EAAW,GAAA;AAAA,EACX,WAAA,EAAa;AAAA,IACX,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY;AAAA,MACV,OAAA,EAAS,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,MAC1B,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,MACvB,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,MACvB,WAAA,EAAa,EAAE,IAAA,EAAM,QAAA,EAAU,MAAM,CAAC,SAAA,EAAW,oBAAA,EAAsB,OAAO,CAAA,EAAE;AAAA,MAChF,aAAA,EAAe,EAAE,IAAA,EAAM,SAAA,EAAU;AAAA,MACjC,gBAAA,EAAkB,EAAE,IAAA,EAAM,SAAA,EAAU;AAAA,MACpC,KAAA,EAAO,EAAE,IAAA,EAAM,SAAA;AAAU,KAC3B;AAAA,IACA,QAAA,EAAU,CAAC,SAAS;AAAA,GACtB;AAAA,EACA,MAAM,OAAA,CAAQ,KAAA,EAAO,GAAA,EAAK,IAAA,EAAM;AAC9B,IAAA,IAAI,KAAA;AACJ,IAAA,WAAA,MAAiB,MAAM,QAAA,CAAS,aAAA,CAAe,KAAA,EAAO,GAAA,EAAK,IAAI,CAAA,EAAG;AAChE,MAAA,IAAI,EAAA,CAAG,IAAA,KAAS,OAAA,EAAS,KAAA,GAAQ,EAAA,CAAG,MAAA;AAAA,IACtC;AACA,IAAA,IAAI,CAAC,KAAA,EAAO,MAAM,IAAI,MAAM,wCAAwC,CAAA;AACpE,IAAA,OAAO,KAAA;AAAA,EACT,CAAA;AAAA,EACA,OAAO,aAAA,CAAc,KAAA,EAAO,GAAA,EAAK,IAAA,EAAmD;AAClF,IAAA,IAAI,CAAC,KAAA,EAAO,OAAA,EAAS,MAAM,IAAI,MAAM,2BAA2B,CAAA;AAChE,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,GAAO,WAAA,CAAY,MAAM,IAAA,EAAM,GAAG,IAAI,GAAA,CAAI,GAAA;AAC7D,IAAA,MAAM,IAAA,GAAO,MAAM,WAAA,IAAe,SAAA;AAClC,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,IAAI,KAAA,CAAM,KAAA,IAAS,GAAA,EAAK,GAAI,CAAC,CAAA;AAE5D,IAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,IAAA,CAAK,MAAM,CAAA;AAC9C,IAAA,IAAI,WAAA,EAAa;AACf,MAAA,IAAI;AACF,QAAA,OAAO,YAAY,KAAA,EAAO,IAAA,EAAM,IAAA,EAAM,KAAA,EAAO,KAAK,MAAM,CAAA;AACxD,QAAA;AAAA,MACF,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AACA,IAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAO,IAAA,EAAM,mCAAA,EAA+B;AAC1D,IAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,KAAA,EAAO,MAAM,IAAA,EAAM,KAAA,EAAO,KAAK,MAAM,CAAA;AACjE,IAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAS,MAAA,EAAQ,GAAA,EAAI;AAAA,EACrC;AACF;AAEA,eAAe,SAAS,MAAA,EAAuC;AAC7D,EAAA,OAAO,IAAI,OAAA,CAAQ,CAACA,QAAAA,KAAY;AAC9B,IAAA,IAAI;AACF,MAAA,MAAM,CAAA,GAAI,KAAA,CAAM,IAAA,EAAM,CAAC,WAAW,GAAG,EAAE,KAAA,EAAO,QAAA,EAAU,MAAA,EAAQ,CAAA;AAChE,MAAA,CAAA,CAAE,EAAA,CAAG,OAAA,EAAS,MAAMA,QAAAA,CAAQ,KAAK,CAAC,CAAA;AAClC,MAAA,CAAA,CAAE,GAAG,OAAA,EAAS,CAAC,SAASA,QAAAA,CAAQ,IAAA,KAAS,CAAC,CAAC,CAAA;AAAA,IAC7C,CAAA,CAAA,MAAQ;AACN,MAAAA,SAAQ,KAAK,CAAA;AAAA,IACf;AAAA,EACF,CAAC,CAAA;AACH;AAEA,gBAAgB,WAAA,CACd,KAAA,EACA,IAAA,EACA,IAAA,EACA,OACA,MAAA,EAC6C;AAC7C,EAAA,MAAM,IAAA,GAAiB,CAAC,cAAc,CAAA;AACtC,EAAA,IAAI,KAAA,CAAM,gBAAA,EAAkB,IAAA,CAAK,IAAA,CAAK,IAAI,CAAA;AAC1C,EAAA,IAAI,IAAA,KAAS,oBAAA,EAAsB,IAAA,CAAK,IAAA,CAAK,IAAI,CAAA;AACjD,EAAA,IAAI,IAAA,KAAS,OAAA,EAAS,IAAA,CAAK,IAAA,CAAK,IAAI,CAAA;AACpC,EAAA,IAAI,SAAS,SAAA,EAAW;AACtB,IAAA,IAAA,CAAK,KAAK,IAAI,CAAA;AACd,IAAA,IAAI,KAAA,CAAM,eAAe,IAAA,CAAK,IAAA,CAAK,MAAM,MAAA,CAAO,KAAA,CAAM,aAAa,CAAC,CAAA;AAAA,EACtE;AACA,EAAA,IAAI,MAAM,IAAA,EAAM,IAAA,CAAK,IAAA,CAAK,QAAA,EAAU,MAAM,IAAI,CAAA;AAC9C,EAAA,IAAA,CAAK,IAAA,CAAK,IAAA,EAAM,KAAA,CAAM,OAAA,EAAS,IAAI,CAAA;AAEnC,EAAA,MAAM,UAAoB,EAAC;AAC3B,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,IAAI,UAAA,GAAa,CAAA;AACjB,EAAA,IAAI,eAAA,GAAkB,CAAA;AACtB,EAAA,MAAM,QAAA,GAAW,EAAA;AAKjB,EAAA,MAAM,aAAA,GAAgB,GAAA;AACtB,EAAA,IAAI,WAAA,GAAc,KAAA;AAElB,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,EAAE,MAAA,EAAQ,KAAA,EAAO,CAAC,QAAA,EAAU,MAAA,EAAQ,MAAM,CAAA,EAAG,CAAA;AAG7E,EAAA,MAAM,QAAiB,EAAC;AACxB,EAAA,IAAI,MAAA;AACJ,EAAA,MAAM,OAAO,MAAM;AACjB,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,MAAM,CAAA,GAAI,MAAA;AACV,MAAA,MAAA,GAAS,MAAA;AACT,MAAA,CAAA,EAAE;AAAA,IACJ;AAAA,EACF,CAAA;AACA,EAAA,KAAA,CAAM,MAAA,EAAQ,EAAA,CAAG,MAAA,EAAQ,CAAC,CAAA,KAAM;AAC9B,IAAA,KAAA,CAAM,IAAA,CAAK,EAAE,IAAA,EAAM,KAAA,EAAO,MAAM,CAAA,CAAE,QAAA,IAAY,CAAA;AAC9C,IAAA,IAAA,EAAK;AAAA,EACP,CAAC,CAAA;AACD,EAAA,KAAA,CAAM,EAAA,CAAG,OAAA,EAAS,CAAC,CAAA,KAAM;AACvB,IAAA,KAAA,CAAM,KAAK,EAAE,IAAA,EAAM,SAAS,IAAA,EAAM,CAAA,CAAE,SAAS,CAAA;AAC7C,IAAA,IAAA,EAAK;AAAA,EACP,CAAC,CAAA;AACD,EAAA,KAAA,CAAM,EAAA,CAAG,SAAS,MAAM;AACtB,IAAA,KAAA,CAAM,KAAK,EAAE,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,IAAI,CAAA;AACtC,IAAA,IAAA,EAAK;AAAA,EACP,CAAC,CAAA;AAED,EAAA,IAAI,eAAyB,EAAC;AAC9B,EAAA,IAAI,OAAA,GAAU,KAAA;AACd,EAAA,WAAS;AACP,IAAA,OAAO,KAAA,CAAM,WAAW,CAAA,EAAG;AACzB,MAAA,MAAM,IAAI,OAAA,CAAc,CAAC,CAAA,KAAM;AAC7B,QAAA,MAAA,GAAS,CAAA;AAAA,MACX,CAAC,CAAA;AAAA,IACH;AACA,IAAA,MAAM,CAAA,GAAI,MAAM,KAAA,EAAM;AACtB,IAAA,IAAI,CAAA,CAAE,SAAS,OAAA,EAAS;AACtB,MAAA,OAAA,GAAU,IAAA;AACV,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAA,CAAE,SAAS,OAAA,EAAS;AACxB,IAAA,GAAA,IAAO,CAAA,CAAE,IAAA;AAIT,IAAA,IAAI,GAAA,CAAI,MAAA,GAAS,aAAA,IAAiB,CAAC,WAAA,EAAa;AAC9C,MAAA,WAAA,GAAc,IAAA;AACd,MAAA,GAAA,GAAM,GAAA,CAAI,KAAA,CAAM,CAAC,aAAa,CAAA;AAC9B,MAAA,IAAI;AAAE,QAAA,KAAA,CAAM,KAAK,SAAS,CAAA;AAAA,MAAG,CAAA,CAAA,MAAQ;AAAA,MAAe;AAAA,IACtD;AACA,IAAA,MAAM,GAAA,GAAM,GAAA,CAAI,WAAA,CAAY,IAAI,CAAA;AAChC,IAAA,IAAI,QAAQ,EAAA,EAAI;AAChB,IAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA;AAC9B,IAAA,GAAA,GAAM,GAAA,CAAI,KAAA,CAAM,GAAA,GAAM,CAAC,CAAA;AACvB,IAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA,EAAG;AACpC,MAAA,IAAI,CAAC,IAAA,EAAM;AACX,MAAA,UAAA,EAAA;AACA,MAAA,IAAI,OAAA,CAAQ,SAAS,KAAA,EAAO;AAC1B,QAAA,OAAA,CAAQ,KAAK,IAAI,CAAA;AACjB,QAAA,YAAA,CAAa,KAAK,IAAI,CAAA;AACtB,QAAA,eAAA,EAAA;AAAA,MACF;AAAA,IACF;AACA,IAAA,IAAI,mBAAmB,QAAA,EAAU;AAC/B,MAAA,MAAM;AAAA,QACJ,IAAA,EAAM,gBAAA;AAAA,QACN,IAAA,EAAM,YAAA,CAAa,IAAA,CAAK,IAAI,CAAA;AAAA,QAC5B,IAAA,EAAM,EAAE,cAAA,EAAgB,OAAA,CAAQ,MAAA;AAAO,OACzC;AACA,MAAA,YAAA,GAAe,EAAC;AAChB,MAAA,eAAA,GAAkB,CAAA;AAAA,IACpB;AAAA,EACF;AAEA,EAAA,IAAI,GAAA,CAAI,MAAK,EAAG;AACd,IAAA,KAAA,MAAW,IAAA,IAAQ,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA,EAAG;AAClC,MAAA,IAAI,CAAC,IAAA,EAAM;AACX,MAAA,UAAA,EAAA;AACA,MAAA,IAAI,OAAA,CAAQ,SAAS,KAAA,EAAO;AAC1B,QAAA,OAAA,CAAQ,KAAK,IAAI,CAAA;AACjB,QAAA,YAAA,CAAa,KAAK,IAAI,CAAA;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AACA,EAAA,IAAI,YAAA,CAAa,SAAS,CAAA,EAAG;AAC3B,IAAA,MAAM;AAAA,MACJ,IAAA,EAAM,gBAAA;AAAA,MACN,IAAA,EAAM,YAAA,CAAa,IAAA,CAAK,IAAI,CAAA;AAAA,MAC5B,IAAA,EAAM,EAAE,cAAA,EAAgB,OAAA,CAAQ,MAAA;AAAO,KACzC;AAAA,EACF;AACA,EAAA,IAAI,OAAA,EAAS,MAAM,IAAI,KAAA,CAAM,iBAAiB,CAAA;AAE9C,EAAA,MAAM;AAAA,IACJ,IAAA,EAAM,OAAA;AAAA,IACN,MAAA,EAAQ;AAAA,MACN,OAAA;AAAA,MACA,KAAA,EAAO,UAAA;AAAA,MACP,SAAA,EAAW,aAAa,KAAA,IAAS,WAAA;AAAA,MACjC,IAAA,EAAM;AAAA;AACR,GACF;AACF;AAEA,eAAe,SAAA,CACb,KAAA,EACA,IAAA,EACA,IAAA,EACA,OACA,MAAA,EACqB;AACrB,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,gBAAA,GAAmB,GAAA,GAAM,EAAA;AAC7C,EAAA,MAAM,QAAA,GAAW,gBAAA,CAAiB,KAAA,CAAM,OAAA,EAAS,KAAK,CAAA;AACtD,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,MAAA,EAAS,QAAA,CAAS,MAAM,CAAA,CAAE,CAAA;AAAA,EAC5C;AACA,EAAA,MAAM,KAAK,QAAA,CAAS,KAAA;AACpB,EAAA,MAAM,SAAS,KAAA,CAAM,IAAA,GAAO,WAAA,CAAY,KAAA,CAAM,IAAI,CAAA,GAAI,IAAA;AACtD,EAAA,MAAM,UAAoB,EAAC;AAC3B,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAAoB;AAC5C,EAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,EAAA,IAAI,OAAA,GAAU,KAAA;AAEd,EAAA,MAAM,IAAA,GAAO,OAAO,GAAA,KAA+B;AACjD,IAAA,IAAI,OAAA,IAAW,OAAO,OAAA,EAAS;AAC/B,IAAA,IAAI,OAAA;AACJ,IAAA,IAAI;AACF,MAAA,OAAA,GAAU,MAAS,EAAA,CAAA,OAAA,CAAQ,GAAA,EAAK,EAAE,aAAA,EAAe,MAAM,CAAA;AAAA,IACzD,CAAA,CAAA,MAAQ;AACN,MAAA;AAAA,IACF;AACA,IAAA,KAAA,MAAW,KAAK,OAAA,EAAS;AACvB,MAAA,IAAI,OAAA,EAAS;AACb,MAAA,IAAI,cAAA,CAAe,QAAA,CAAS,CAAA,CAAE,IAAI,CAAA,EAAG;AAKrC,MAAA,IAAI,CAAA,CAAE,gBAAe,EAAG;AACxB,MAAA,MAAM,IAAA,GAAYC,IAAA,CAAA,IAAA,CAAK,GAAA,EAAK,CAAA,CAAE,IAAI,CAAA;AAClC,MAAA,IAAI,CAAA,CAAE,aAAY,EAAG;AACnB,QAAA,MAAM,KAAK,IAAI,CAAA;AAAA,MACjB,CAAA,MAAA,IAAW,CAAA,CAAE,MAAA,EAAO,EAAG;AACrB,QAAA,IAAI,MAAA,IAAU,CAAC,MAAA,CAAO,IAAA,CAAK,CAAA,CAAE,IAAI,CAAA,IAAK,CAAC,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EAAG;AAC1D,QAAA,IAAI,MAAA,SAAe,SAAA,GAAY,CAAA;AAC/B,QAAA,IAAI;AACF,UAAA,MAAMC,KAAAA,GAAO,MAAS,EAAA,CAAA,IAAA,CAAK,IAAI,CAAA;AAC/B,UAAA,IAAIA,KAAAA,CAAK,OAAO,GAAA,EAAW;AAC3B,UAAA,MAAM,IAAA,GAAO,MAAS,EAAA,CAAA,QAAA,CAAS,IAAI,CAAA;AACnC,UAAA,IAAI,cAAA,CAAe,IAAI,CAAA,EAAG;AAC1B,UAAA,MAAM,IAAA,GAAO,IAAA,CAAK,QAAA,CAAS,MAAM,CAAA;AACjC,UAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AAChC,UAAA,IAAI,QAAA,GAAW,CAAA;AACf,UAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,YAAA,MAAM,EAAA,GAAK,UAAA,CAAW,KAAA,CAAM,CAAC,KAAK,EAAE,CAAA;AACpC,YAAA,EAAA,CAAG,SAAA,GAAY,CAAA;AACf,YAAA,IAAI,EAAA,CAAG,IAAA,CAAK,EAAE,CAAA,EAAG;AACf,cAAA,QAAA,EAAA;AACA,cAAA,KAAA,EAAA;AACA,cAAA,IAAI,IAAA,KAAS,SAAA,IAAa,OAAA,CAAQ,MAAA,GAAS,KAAA,EAAO;AAChD,gBAAA,OAAA,CAAQ,IAAA,CAAK,GAAG,IAAI,CAAA,CAAA,EAAI,IAAI,CAAC,CAAA,CAAA,EAAI,EAAE,CAAA,CAAE,CAAA;AAAA,cACvC;AAAA,YACF;AAAA,UACF;AACA,UAAA,IAAI,WAAW,CAAA,EAAG;AAChB,YAAA,WAAA,CAAY,GAAA,CAAI,MAAM,QAAQ,CAAA;AAC9B,YAAA,IAAI,IAAA,KAAS,oBAAA,IAAwB,OAAA,CAAQ,MAAA,GAAS,KAAA,EAAO;AAC3D,cAAA,OAAA,CAAQ,KAAK,IAAI,CAAA;AAAA,YACnB;AACA,YAAA,IAAI,IAAA,KAAS,OAAA,IAAW,OAAA,CAAQ,MAAA,GAAS,KAAA,EAAO;AAC9C,cAAA,OAAA,CAAQ,IAAA,CAAK,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,QAAQ,CAAA,CAAE,CAAA;AAAA,YACpC;AAAA,UACF;AACA,UAAA,IAAI,OAAA,CAAQ,MAAA,IAAU,KAAA,EAAO,OAAA,GAAU,IAAA;AAAA,QACzC,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAA;AACA,EAAA,MAAM,KAAK,IAAI,CAAA;AAEf,EAAA,OAAO;AAAA,IACL,OAAA;AAAA,IACA,KAAA,EAAO,KAAA;AAAA,IACP,SAAA,EAAW,OAAA;AAAA,IACX,IAAA,EAAM;AAAA,GACR;AACF","file":"grep.js","sourcesContent":["import * as path from 'node:path';\nimport type { Context } from '@wrongstack/core';\n\r\nexport function resolvePath(input: string, ctx: Context): string {\r\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\r\n}\r\n\r\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(absPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\r\n }\r\n return target;\r\n}\r\n\r\nexport function safeResolve(input: string, ctx: Context): string {\r\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\r\n}\r\n\r\nexport function truncateMiddle(s: string, max: number): string {\r\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\r\n const half = Math.floor(max / 2);\r\n return (\r\n s.slice(0, half) +\r\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\r\n s.slice(-half)\r\n );\r\n}\r\n\r\nexport function isBinaryBuffer(buf: Buffer): boolean {\r\n const len = Math.min(buf.length, 8192);\r\n for (let i = 0; i < len; i++) {\r\n if (buf[i] === 0) return true;\r\n }\r\n return false;\r\n}\r\n\r\n","/**\n * Compile a user-supplied regex with conservative bounds against ReDoS.\n *\n * Node's regex engine (V8) is backtracking-based and cannot interrupt a\n * synchronous match — a pattern like `(a+)+$` against a sufficiently long\n * line will pin a worker for seconds. The executor's outer `timeoutMs` only\n * fires between async boundaries, so a long regex eval inside a sync loop\n * is uninterruptible.\n *\n * We can't fully prevent ReDoS without an alternative engine (re2-wasm), but\n * we can sharply limit the blast radius:\n *\n * 1. Cap pattern length — practically all legitimate user patterns are\n * under 256 characters. A 4 KB pattern is almost certainly malicious\n * or a copy-paste accident.\n * 2. Reject patterns containing the most obvious super-linear structures.\n * This is a coarse filter (false-positives are likely; we accept that\n * for hostile-input contexts).\n *\n * Callers should additionally bound the *subject* length (e.g. by capping\n * line size before matching).\n */\n\nconst MAX_PATTERN_LEN = 512;\n\n// Heuristics for catastrophic-backtracking constructs. Not exhaustive; bias\n// toward false-positives in tools that accept LLM-generated input.\nconst DANGEROUS_PATTERNS: ReadonlyArray<RegExp> = [\n /(\\([^)]*[+*][^)]*\\))[+*]/, // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier\n /(\\(\\?:[^)]*[+*][^)]*\\))[+*]/, // same, with non-capturing group\n];\n\nexport interface CompileResult {\n ok: true;\n regex: RegExp;\n}\n\nexport interface CompileFail {\n ok: false;\n reason: string;\n}\n\nexport function compileUserRegex(\n pattern: string,\n flags: string,\n): CompileResult | CompileFail {\n if (typeof pattern !== 'string') {\n return { ok: false, reason: 'pattern must be a string' };\n }\n if (pattern.length === 0) {\n return { ok: false, reason: 'pattern is empty' };\n }\n if (pattern.length > MAX_PATTERN_LEN) {\n return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };\n }\n for (const rx of DANGEROUS_PATTERNS) {\n if (rx.test(pattern)) {\n return {\n ok: false,\n reason: 'pattern looks vulnerable to catastrophic backtracking — rewrite without nested quantifiers',\n };\n }\n }\n try {\n return { ok: true, regex: new RegExp(pattern, flags) };\n } catch (err) {\n return {\n ok: false,\n reason: err instanceof Error ? err.message : 'invalid regex',\n };\n }\n}\n\n/**\n * Truncate a subject line to a safe length for synchronous regex eval.\n * The cap is conservative; tools that need exact-line matching against very\n * long lines should use ripgrep externally rather than the native walker.\n */\nexport const MAX_SUBJECT_LEN = 64 * 1024;\n\nexport function capSubject(line: string): string {\n return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;\n}\n","import * as fs from 'node:fs/promises';\r\nimport * as path from 'node:path';\r\nimport { spawn } from 'node:child_process';\r\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\r\nimport { compileGlob } from '@wrongstack/core';\r\nimport { isBinaryBuffer, safeResolve } from './_util.js';\r\nimport { compileUserRegex, capSubject } from './_regex.js';\r\n\r\ninterface GrepInput {\r\n pattern: string;\r\n path?: string;\r\n glob?: string;\r\n output_mode?: 'content' | 'files_with_matches' | 'count';\r\n context_lines?: number;\r\n case_insensitive?: boolean;\r\n limit?: number;\r\n}\r\n\r\ninterface GrepOutput {\r\n matches: string[];\r\n count: number;\r\n truncated: boolean;\r\n used: 'rg' | 'native';\r\n}\r\n\r\nconst DEFAULT_IGNORE = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];\r\n\r\nexport const grepTool: Tool<GrepInput, GrepOutput> = {\r\n name: 'grep',\r\n description: 'Search file contents with a regex. Uses ripgrep when available.',\r\n usageHint:\r\n 'Pattern is regex. Use `output_mode: \"content\"` for matched lines, `\"files_with_matches\"` for paths, `\"count\"` for tallies. `glob` filters files (e.g. `*.ts`).',\r\n permission: 'auto',\r\n mutating: false,\r\n maxOutputBytes: 131_072,\r\n timeoutMs: 10_000,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n pattern: { type: 'string' },\r\n path: { type: 'string' },\r\n glob: { type: 'string' },\r\n output_mode: { type: 'string', enum: ['content', 'files_with_matches', 'count'] },\r\n context_lines: { type: 'integer' },\r\n case_insensitive: { type: 'boolean' },\r\n limit: { type: 'integer' },\r\n },\r\n required: ['pattern'],\r\n },\r\n async execute(input, ctx, opts) {\r\n let final: GrepOutput | undefined;\r\n for await (const ev of grepTool.executeStream!(input, ctx, opts)) {\r\n if (ev.type === 'final') final = ev.output;\r\n }\r\n if (!final) throw new Error('grep: stream ended without final event');\r\n return final;\r\n },\r\n async *executeStream(input, ctx, opts): AsyncGenerator<ToolStreamEvent<GrepOutput>> {\r\n if (!input?.pattern) throw new Error('grep: pattern is required');\r\n const base = input.path ? safeResolve(input.path, ctx) : ctx.cwd;\r\n const mode = input.output_mode ?? 'content';\r\n const limit = Math.max(1, Math.min(input.limit ?? 200, 2000));\r\n\r\n const rgAvailable = await detectRg(opts.signal);\r\n if (rgAvailable) {\r\n try {\r\n yield* runRgStream(input, base, mode, limit, opts.signal);\r\n return;\r\n } catch {\r\n // fall through to native\r\n }\r\n }\r\n yield { type: 'log', text: 'Falling back to native grep…' };\r\n const out = await runNative(input, base, mode, limit, opts.signal);\r\n yield { type: 'final', output: out };\r\n },\r\n};\r\n\r\nasync function detectRg(signal: AbortSignal): Promise<boolean> {\r\n return new Promise((resolve) => {\r\n try {\r\n const p = spawn('rg', ['--version'], { stdio: 'ignore', signal });\r\n p.on('error', () => resolve(false));\r\n p.on('close', (code) => resolve(code === 0));\r\n } catch {\r\n resolve(false);\r\n }\r\n });\r\n}\r\n\r\nasync function* runRgStream(\r\n input: GrepInput,\r\n base: string,\r\n mode: 'content' | 'files_with_matches' | 'count',\r\n limit: number,\r\n signal: AbortSignal,\r\n): AsyncGenerator<ToolStreamEvent<GrepOutput>> {\r\n const args: string[] = ['--no-heading'];\r\n if (input.case_insensitive) args.push('-i');\r\n if (mode === 'files_with_matches') args.push('-l');\r\n if (mode === 'count') args.push('-c');\r\n if (mode === 'content') {\r\n args.push('-n');\r\n if (input.context_lines) args.push('-C', String(input.context_lines));\r\n }\r\n if (input.glob) args.push('--glob', input.glob);\r\n args.push('--', input.pattern, base);\r\n\r\n const matches: string[] = [];\r\n let buf = '';\r\n let totalLines = 0;\r\n let batchSinceFlush = 0;\r\n const FLUSH_AT = 16; // yield a partial_output every 16 matches\r\n // Cap on the in-progress line buffer. Without this, a single huge \"line\"\r\n // (e.g. a file with no newlines under a symlink) plus a fast producer\r\n // would let `buf` grow unbounded. 1 MB comfortably holds any realistic\r\n // grep hit; beyond that we kill the child and surface a truncation.\r\n const MAX_BUF_BYTES = 1_000_000;\r\n let bufOverflow = false;\r\n\r\n const child = spawn('rg', args, { signal, stdio: ['ignore', 'pipe', 'pipe'] });\r\n\r\n type Chunk = { kind: 'out' | 'close' | 'error'; data: string };\r\n const queue: Chunk[] = [];\r\n let waiter: (() => void) | undefined;\r\n const wake = () => {\r\n if (waiter) {\r\n const w = waiter;\r\n waiter = undefined;\r\n w();\r\n }\r\n };\r\n child.stdout?.on('data', (c) => {\r\n queue.push({ kind: 'out', data: c.toString() });\r\n wake();\r\n });\r\n child.on('error', (e) => {\r\n queue.push({ kind: 'error', data: e.message });\r\n wake();\r\n });\r\n child.on('close', () => {\r\n queue.push({ kind: 'close', data: '' });\r\n wake();\r\n });\r\n\r\n let pendingBatch: string[] = [];\r\n let errored = false;\r\n for (;;) {\r\n while (queue.length === 0) {\r\n await new Promise<void>((r) => {\r\n waiter = r;\r\n });\r\n }\r\n const c = queue.shift()!;\r\n if (c.kind === 'error') {\r\n errored = true;\r\n continue;\r\n }\r\n if (c.kind === 'close') break;\r\n buf += c.data;\r\n // Guard against a pathological producer (e.g. matching a huge binary\r\n // without newlines) pinning memory. Kill the child and mark the result\r\n // truncated; whatever we already captured stays intact.\r\n if (buf.length > MAX_BUF_BYTES && !bufOverflow) {\r\n bufOverflow = true;\r\n buf = buf.slice(-MAX_BUF_BYTES);\r\n try { child.kill('SIGTERM'); } catch { /* ignore */ }\r\n }\r\n const idx = buf.lastIndexOf('\\n');\r\n if (idx === -1) continue;\r\n const ready = buf.slice(0, idx);\r\n buf = buf.slice(idx + 1);\r\n for (const line of ready.split('\\n')) {\r\n if (!line) continue;\r\n totalLines++;\r\n if (matches.length < limit) {\r\n matches.push(line);\r\n pendingBatch.push(line);\r\n batchSinceFlush++;\r\n }\r\n }\r\n if (batchSinceFlush >= FLUSH_AT) {\r\n yield {\r\n type: 'partial_output',\r\n text: pendingBatch.join('\\n'),\r\n data: { matches_so_far: matches.length },\r\n };\r\n pendingBatch = [];\r\n batchSinceFlush = 0;\r\n }\r\n }\r\n\r\n if (buf.trim()) {\r\n for (const line of buf.split('\\n')) {\r\n if (!line) continue;\r\n totalLines++;\r\n if (matches.length < limit) {\r\n matches.push(line);\r\n pendingBatch.push(line);\r\n }\r\n }\r\n }\r\n if (pendingBatch.length > 0) {\r\n yield {\r\n type: 'partial_output',\r\n text: pendingBatch.join('\\n'),\r\n data: { matches_so_far: matches.length },\r\n };\r\n }\r\n if (errored) throw new Error('rg: spawn error');\r\n\r\n yield {\r\n type: 'final',\r\n output: {\r\n matches,\r\n count: totalLines,\r\n truncated: totalLines > limit || bufOverflow,\r\n used: 'rg',\r\n },\r\n };\r\n}\r\n\r\nasync function runNative(\r\n input: GrepInput,\r\n base: string,\r\n mode: 'content' | 'files_with_matches' | 'count',\r\n limit: number,\r\n signal: AbortSignal,\r\n): Promise<GrepOutput> {\r\n const flags = input.case_insensitive ? 'i' : '';\r\n const compiled = compileUserRegex(input.pattern, flags);\r\n if (!compiled.ok) {\r\n throw new Error(`grep: ${compiled.reason}`);\r\n }\r\n const re = compiled.regex;\r\n const globRe = input.glob ? compileGlob(input.glob) : null;\r\n const matches: string[] = [];\r\n const fileMatches = new Map<string, number>();\r\n let total = 0;\r\n let stopped = false;\r\n\r\n const walk = async (dir: string): Promise<void> => {\r\n if (stopped || signal.aborted) return;\r\n let entries: import('node:fs').Dirent[];\r\n try {\r\n entries = await fs.readdir(dir, { withFileTypes: true });\r\n } catch {\r\n return;\r\n }\r\n for (const e of entries) {\r\n if (stopped) return;\r\n if (DEFAULT_IGNORE.includes(e.name)) continue;\r\n // Skip symlinks entirely. fs.Dirent.isDirectory/isFile return the\r\n // symlink's TYPE without resolving, but following the link into\r\n // arbitrary places (e.g. ~/.ssh) is the security concern. Tools\r\n // that genuinely need to traverse symlinks should opt in explicitly.\r\n if (e.isSymbolicLink()) continue;\r\n const full = path.join(dir, e.name);\r\n if (e.isDirectory()) {\r\n await walk(full);\r\n } else if (e.isFile()) {\r\n if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;\r\n if (globRe) globRe.lastIndex = 0;\r\n try {\r\n const stat = await fs.stat(full);\r\n if (stat.size > 1_000_000) continue;\r\n const head = await fs.readFile(full);\r\n if (isBinaryBuffer(head)) continue;\r\n const text = head.toString('utf8');\r\n const lines = text.split(/\\r?\\n/);\r\n let fileHits = 0;\r\n for (let i = 0; i < lines.length; i++) {\r\n const ln = capSubject(lines[i] ?? '');\r\n re.lastIndex = 0;\r\n if (re.test(ln)) {\r\n fileHits++;\r\n total++;\r\n if (mode === 'content' && matches.length < limit) {\r\n matches.push(`${full}:${i + 1}:${ln}`);\r\n }\r\n }\r\n }\r\n if (fileHits > 0) {\r\n fileMatches.set(full, fileHits);\r\n if (mode === 'files_with_matches' && matches.length < limit) {\r\n matches.push(full);\r\n }\r\n if (mode === 'count' && matches.length < limit) {\r\n matches.push(`${full}:${fileHits}`);\r\n }\r\n }\r\n if (matches.length >= limit) stopped = true;\r\n } catch {\r\n // skip read errors\r\n }\r\n }\r\n }\r\n };\r\n await walk(base);\r\n\r\n return {\r\n matches,\r\n count: total,\r\n truncated: stopped,\r\n used: 'native',\r\n };\r\n}\r\n"]}
|