"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fsRipgrepSearchTool = exports.ripgrepSearchTool = exports.fsRipgrepFixedTool = exports.ripgrepFixedTool = exports.fsRipgrepSnippetsTool = exports.ripgrepSnippetsTool = exports.fsRipgrepCountTool = exports.ripgrepCountTool = exports.fsRipgrepFilesTool = exports.ripgrepFilesTool = void 0;
/**
 * Module: tools/ripgrep
 *
 * rtws (runtime workspace) search tools backed by `rg` (ripgrep), with low-noise YAML output.
 */
const child_process_1 = require("child_process");
const promises_1 = __importDefault(require("fs/promises"));
const readline = __importStar(require("node:readline"));
const path_1 = __importDefault(require("path"));
const rtws_1 = require("../rtws");
const tool_1 = require("../tool");
function yamlQuote(value) {
    return `'${value.replace(/'/g, "''")}'`;
}
function formatYamlCodeBlock(yaml) {
    return `\`\`\`yaml\n${yaml}\n\`\`\``;
}
function okYaml(yaml) {
    return (0, tool_1.toolSuccess)(formatYamlCodeBlock(yaml));
}
function failYaml(yaml) {
    return (0, tool_1.toolFailure)(formatYamlCodeBlock(yaml));
}
function yamlFlowStringArray(values) {
    if (values.length === 0)
        return '[]';
    return `[${values.map(yamlQuote).join(', ')}]`;
}
function yamlFlowNumberArray(values) {
    if (values.length === 0)
        return '[]';
    return `[${values.join(', ')}]`;
}
function requireNonEmptyStringArg(args, key) {
    const value = args[key];
    if (typeof value !== 'string' || value.trim() === '') {
        throw new Error(`Invalid arguments: \`${key}\` must be a non-empty string`);
    }
    return value;
}
function optionalStringArg(args, key) {
    const value = args[key];
    if (value === undefined)
        return undefined;
    if (typeof value !== 'string')
        throw new Error(`Invalid arguments: \`${key}\` must be a string`);
    const trimmed = value.trim();
    if (trimmed === '')
        return undefined;
    return trimmed;
}
function optionalBooleanArg(args, key) {
    const value = args[key];
    if (value === undefined)
        return undefined;
    if (typeof value !== 'boolean')
        throw new Error(`Invalid arguments: \`${key}\` must be a boolean`);
    return value;
}
function optionalPositiveIntegerArg(args, key) {
    const value = args[key];
    if (value === undefined)
        return undefined;
    if (typeof value !== 'number' || !Number.isInteger(value)) {
        throw new Error(`Invalid arguments: \`${key}\` must be an integer`);
    }
    if (value <= 0)
        throw new Error(`Invalid arguments: \`${key}\` must be a positive integer`);
    return value;
}
function optionalNonNegativeIntegerArg(args, key) {
    const value = args[key];
    if (value === undefined)
        return undefined;
    if (typeof value !== 'number' || !Number.isInteger(value)) {
        throw new Error(`Invalid arguments: \`${key}\` must be an integer`);
    }
    if (value < 0)
        throw new Error(`Invalid arguments: \`${key}\` must be >= 0`);
    return value;
}
function optionalStringArrayArg(args, key) {
    const value = args[key];
    if (value === undefined)
        return undefined;
    if (!Array.isArray(value) || !value.every((v) => typeof v === 'string')) {
        throw new Error(`Invalid arguments: \`${key}\` must be an array of strings`);
    }
    return value;
}
function optionalStringListArg(args, key) {
    const value = args[key];
    if (value === undefined)
        return undefined;
    if (typeof value === 'string')
        return [value];
    if (!Array.isArray(value) || !value.every((v) => typeof v === 'string')) {
        throw new Error(`Invalid arguments: \`${key}\` must be a string or an array of strings`);
    }
    return value;
}
function parseRipgrepGlobsArg(args) {
    const globs = optionalStringArrayArg(args, 'globs') ?? [];
    const include = optionalStringListArg(args, 'include') ?? [];
    return [...globs, ...include];
}
function dirPatternToRipgrepGlob(pattern) {
    const normalized = pattern.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '') || '.';
    if (normalized === '.' || normalized === '')
        return undefined;
    if (normalized.includes('*'))
        return normalized;
    if (normalized.endsWith('/**'))
        return normalized;
    return `${normalized}/**`;
}
function buildAccessControlGlobs(member) {
    const include = [];
    const exclude = [];
    const whitelist = member.read_dirs ?? [];
    for (const pat of whitelist) {
        const glob = dirPatternToRipgrepGlob(pat);
        if (glob && glob !== '**' && glob !== '**/**')
            include.push(glob);
    }
    const blacklist = member.no_read_dirs ?? [];
    for (const pat of blacklist) {
        const glob = dirPatternToRipgrepGlob(pat);
        if (glob)
            exclude.push(glob);
    }
    // Taskdocs are encapsulated and forbidden to all general file tools.
    exclude.push('**/*.tsk');
    exclude.push('**/*.tsk/*');
    exclude.push('**/*.tsk/**');
    // `.dialogs/**` at rtws root is reserved runtime persistence state and hard-denied.
    exclude.push('.dialogs');
    exclude.push('.dialogs/**');
    // `.minds/**` is reserved rtws state and hard-denied for general file tools.
    // Dedicated `.minds/`-scoped tools (team_mgmt) may bypass this with internal_allow_minds=true.
    if (member.internal_allow_minds !== true) {
        exclude.push('.minds');
        exclude.push('.minds/**');
    }
    return { include, exclude };
}
function normalizeRelFromRtwsRoot(relPath) {
    return relPath.replace(/\\/g, '/').replace(/^\/+/, '');
}
function isEncapsulatedTaskPath(relFromRoot) {
    const normalized = normalizeRelFromRtwsRoot(relFromRoot);
    return /(^|\/)[^/]+\.tsk(\/|$)/.test(normalized);
}
function detectRootReservedPath(relFromRoot) {
    const normalized = normalizeRelFromRtwsRoot(relFromRoot);
    if (normalized === '.minds' || normalized.startsWith('.minds/'))
        return '.minds';
    if (normalized === '.dialogs' || normalized.startsWith('.dialogs/'))
        return '.dialogs';
    return null;
}
function detectForbiddenRipgrepSearchPath(member, searchPath) {
    const cwd = (0, rtws_1.domindsRtwsRootAbs)();
    const abs = path_1.default.resolve(cwd, searchPath);
    const relFromRoot = path_1.default.relative(cwd, abs);
    if (isEncapsulatedTaskPath(relFromRoot))
        return '*.tsk/';
    const reserved = detectRootReservedPath(relFromRoot);
    if (!reserved)
        return null;
    if (reserved === '.minds' && member.internal_allow_minds === true)
        return null;
    return reserved;
}
function normalizeGlobToken(glob) {
    return glob
        .trim()
        .replace(/\\/g, '/')
        .replace(/^!+/, '')
        .replace(/^\.\/+/, '')
        .replace(/^\/+/, '');
}
function detectForbiddenPathFromGlob(member, glob) {
    const normalized = normalizeGlobToken(glob);
    if (normalized === '')
        return null;
    if (/(^|\/)[^/]*\.tsk(\/|$)/.test(normalized))
        return '*.tsk/';
    if (normalized === '.dialogs' || normalized.startsWith('.dialogs/'))
        return '.dialogs';
    if ((normalized === '.minds' || normalized.startsWith('.minds/')) &&
        member.internal_allow_minds !== true) {
        return '.minds';
    }
    return null;
}
function detectForbiddenRipgrepGlobs(member, globs) {
    for (const glob of globs) {
        const forbidden = detectForbiddenPathFromGlob(member, glob);
        if (forbidden)
            return forbidden;
    }
    return null;
}
function detectForbiddenRipgrepRawArgs(member, rawRgArgs) {
    for (let i = 0; i < rawRgArgs.length; i++) {
        const tok = rawRgArgs[i] ?? '';
        let globPattern = null;
        if (tok === '--glob' || tok === '--iglob' || tok === '-g') {
            const next = rawRgArgs[i + 1];
            if (typeof next === 'string') {
                globPattern = next;
                i += 1;
            }
        }
        else if (tok.startsWith('--glob=')) {
            globPattern = tok.slice('--glob='.length);
        }
        else if (tok.startsWith('--iglob=')) {
            globPattern = tok.slice('--iglob='.length);
        }
        else if (tok.startsWith('-g') && tok.length > 2) {
            globPattern = tok.slice(2);
        }
        if (!globPattern)
            continue;
        const forbidden = detectForbiddenPathFromGlob(member, globPattern);
        if (forbidden)
            return forbidden;
    }
    return null;
}
function formatForbiddenSearchPathAccessDeniedYaml(mode, pattern, searchPath, forbiddenPath) {
    const summary = forbiddenPath === '.minds'
        ? 'ACCESS_DENIED: `.minds/**` is reserved rtws state for team config/memory/assets. Use `team_mgmt_ripgrep_*` (or other `team_mgmt_*`) for `.minds/**`.'
        : forbiddenPath === '.dialogs'
            ? 'ACCESS_DENIED: `.dialogs/**` at rtws root is reserved dialog/runtime persistence and is not searchable by general `ripgrep_*` tools. For Dominds debugging, reproduce under a nested rtws (e.g. `ux-rtws/.dialogs/**`).'
            : 'ACCESS_DENIED: `*.tsk/` is an encapsulated Taskdoc path and is hard-denied for general `ripgrep_*` tools.';
    const yaml = [
        `status: error`,
        `pattern: ${yamlQuote(pattern)}`,
        `mode: ${mode}`,
        `path: ${yamlQuote(searchPath)}`,
        `error: ACCESS_DENIED`,
        `reserved_path: ${yamlQuote(forbiddenPath)}`,
        `summary: ${yamlQuote(summary)}`,
    ].join('\n');
    return failYaml(yaml);
}
const RIPGREP_SNIPPET_TEXT_CHAR_LIMIT = 400;
const RIPGREP_SNIPPET_OUTPUT_CHAR_LIMIT = 200_000;
const RIPGREP_SNIPPET_RESULTS_CHAR_BUDGET = RIPGREP_SNIPPET_OUTPUT_CHAR_LIMIT - 2048;
const RIPGREP_STDERR_CHAR_LIMIT = 16_000;
const RIPGREP_PARTIAL_PATH_FAILURE_SUMMARY = 'Search hit a path error (exit code 2), but returned matches are shown.';
const RIPGREP_PARTIAL_PATH_FAILURE_NO_MATCH_SUMMARY = 'Search hit a path error (exit code 2); no matches were returned.';
function truncateRipgrepSnippetText(text, maxChars = RIPGREP_SNIPPET_TEXT_CHAR_LIMIT) {
    if (text.length <= maxChars)
        return { text, truncated: false, originalChars: text.length };
    const suffix = `...[truncated ${text.length - maxChars} chars]`;
    if (suffix.length >= maxChars) {
        return { text: suffix.slice(0, maxChars), truncated: true, originalChars: text.length };
    }
    const keepChars = maxChars - suffix.length;
    return {
        text: `${text.slice(0, keepChars)}${suffix}`,
        truncated: true,
        originalChars: text.length,
    };
}
function sumNumbers(values) {
    let total = 0;
    for (const value of values)
        total += value;
    return total;
}
function renderRipgrepSnippetResult(result) {
    const match = truncateRipgrepSnippetText(result.match);
    const renderedBefore = (result.before ?? []).map((line) => truncateRipgrepSnippetText(line));
    const renderedAfter = (result.after ?? []).map((line) => truncateRipgrepSnippetText(line));
    const beforeOriginalChars = renderedBefore.map((line) => line.originalChars);
    const afterOriginalChars = renderedAfter.map((line) => line.originalChars);
    const textTruncated = match.truncated ||
        renderedBefore.some((line) => line.truncated) ||
        renderedAfter.some((line) => line.truncated);
    const reasons = textTruncated ? ['per_text_char_limit'] : [];
    const originalTextChars = match.originalChars + sumNumbers(beforeOriginalChars) + sumNumbers(afterOriginalChars);
    return {
        yaml: [
            `  - path: ${yamlQuote(result.path)}`,
            `    line: ${result.line}`,
            `    col: ${result.col}`,
            `    text_truncated: ${textTruncated}`,
            `    match: ${yamlQuote(match.text)}`,
            ...(result.before
                ? [`    before: ${yamlFlowStringArray(renderedBefore.map((line) => line.text))}`]
                : []),
            ...(result.after
                ? [`    after: ${yamlFlowStringArray(renderedAfter.map((line) => line.text))}`]
                : []),
            `    truncation:`,
            `      applied: ${textTruncated}`,
            `      reasons: ${yamlFlowStringArray(reasons)}`,
            `      match_original_chars: ${match.originalChars}`,
            `      before_original_chars: ${yamlFlowNumberArray(beforeOriginalChars)}`,
            `      after_original_chars: ${yamlFlowNumberArray(afterOriginalChars)}`,
        ].join('\n'),
        textTruncated,
        originalTextChars,
    };
}
function fitRipgrepSnippetResults(results) {
    const rendered = [];
    let usedChars = 0;
    let textTruncated = false;
    let outputCharsTruncated = false;
    let textTrimmedResultCount = 0;
    let shownOriginalTextChars = 0;
    let omittedOriginalTextChars = 0;
    let omittedResultsByOutputBudget = 0;
    for (let idx = 0; idx < results.length; idx += 1) {
        const result = results[idx];
        const next = renderRipgrepSnippetResult(result);
        textTruncated = textTruncated || next.textTruncated;
        if (next.textTruncated)
            textTrimmedResultCount += 1;
        const nextChars = next.yaml.length + (rendered.length > 0 ? 1 : 0);
        if (usedChars + nextChars > RIPGREP_SNIPPET_RESULTS_CHAR_BUDGET) {
            outputCharsTruncated = true;
            omittedResultsByOutputBudget = results.length - idx;
            omittedOriginalTextChars += next.originalTextChars;
            for (let rest = idx + 1; rest < results.length; rest += 1) {
                omittedOriginalTextChars += renderRipgrepSnippetResult(results[rest]).originalTextChars;
            }
            break;
        }
        rendered.push(next.yaml);
        usedChars += nextChars;
        shownOriginalTextChars += next.originalTextChars;
    }
    if (rendered.length < results.length && !outputCharsTruncated) {
        outputCharsTruncated = true;
        omittedResultsByOutputBudget = results.length - rendered.length;
    }
    return {
        rendered,
        textTruncated,
        outputCharsTruncated,
        textTrimmedResultCount,
        shownOriginalTextChars,
        omittedOriginalTextChars,
        omittedResultsByOutputBudget,
    };
}
function buildRipgrepSnippetSummary(input) {
    const partialSuffix = input.partialPathFailure ? ` ${RIPGREP_PARTIAL_PATH_FAILURE_SUMMARY}` : '';
    if (input.totalMatches === 0)
        return formatRipgrepNoMatchesSummary(input.partialPathFailure);
    if (input.truncatedByMatchLimit && input.truncatedByOutputChars) {
        return `Found ${input.totalMatches} matches; showing ${input.shownResults} snippets within max_results/output limits (truncated=true).${partialSuffix}`;
    }
    if (input.truncatedByOutputChars) {
        return `Found ${input.totalMatches} matches; showing ${input.shownResults} snippets within output size limit (truncated=true).${partialSuffix}`;
    }
    if (input.truncatedByMatchLimit) {
        return `Showing first ${input.maxResults} of ${input.totalMatches} matches (truncated=true).${partialSuffix}`;
    }
    if (input.textTruncated) {
        return `Found ${input.totalMatches} matches; long snippet lines were trimmed for readability.${partialSuffix}`;
    }
    return `Found ${input.totalMatches} matches.${partialSuffix}`;
}
function formatRipgrepSnippetYaml(input) {
    const fitted = fitRipgrepSnippetResults(input.results);
    const truncatedByMatchLimit = input.totalMatches > input.maxResults;
    const truncated = truncatedByMatchLimit || fitted.outputCharsTruncated || fitted.textTruncated;
    const truncationReasons = [];
    if (truncatedByMatchLimit)
        truncationReasons.push('max_results');
    if (fitted.outputCharsTruncated)
        truncationReasons.push('output_char_budget');
    if (fitted.textTruncated)
        truncationReasons.push('per_text_char_limit');
    const summary = buildRipgrepSnippetSummary({
        totalMatches: input.totalMatches,
        shownResults: fitted.rendered.length,
        maxResults: input.maxResults,
        truncatedByMatchLimit,
        truncatedByOutputChars: fitted.outputCharsTruncated,
        textTruncated: fitted.textTruncated,
        partialPathFailure: input.partialPathFailure,
    });
    const yaml = [
        `status: ${input.partialPathFailure ? 'partial_failure' : 'ok'}`,
        `pattern: ${yamlQuote(input.pattern)}`,
        `mode: snippets`,
        `path: ${yamlQuote(input.searchPath)}`,
        ...(input.globs.length > 0 ? [`globs: ${yamlFlowStringArray(input.globs)}`] : []),
        `case: ${input.caseMode}`,
        `fixed_strings: ${input.fixedStrings}`,
        `regex: ${!input.fixedStrings}`,
        `truncated: ${truncated}`,
        `truncated_by_output_chars: ${fitted.outputCharsTruncated}`,
        `snippet_text_truncated: ${fitted.textTruncated}`,
        `limits:`,
        `  max_results: ${input.maxResults}`,
        `  max_output_chars: ${RIPGREP_SNIPPET_OUTPUT_CHAR_LIMIT}`,
        `  per_text_char_limit: ${RIPGREP_SNIPPET_TEXT_CHAR_LIMIT}`,
        `totals:`,
        `  files_matched: ${input.fileCount}`,
        `  matches: ${input.totalMatches}`,
        `  shown_results: ${fitted.rendered.length}`,
        `diagnostics:`,
        `  partial_path_failure: ${input.partialPathFailure}`,
        `  messages: ${yamlFlowStringArray(input.diagnostics)}`,
        `truncation:`,
        `  applied: ${truncated}`,
        `  reasons: ${yamlFlowStringArray(truncationReasons)}`,
        `  text_trimmed_result_count: ${fitted.textTrimmedResultCount}`,
        `  omitted_results_by_output_budget: ${fitted.omittedResultsByOutputBudget}`,
        `  shown_original_text_chars: ${fitted.shownOriginalTextChars}`,
        `  omitted_original_text_chars: ${fitted.omittedOriginalTextChars}`,
        `summary: ${yamlQuote(summary)}`,
        `results:`,
        ...fitted.rendered,
    ].join('\n');
    return input.partialPathFailure ? (0, tool_1.toolPartialFailure)(formatYamlCodeBlock(yaml)) : okYaml(yaml);
}
function defaultBaseOptions() {
    return {
        globs: [],
        caseMode: 'smart',
        fixedStrings: false,
        includeHidden: false,
        followSymlinks: false,
    };
}
function parseRipgrepCaseModeArg(args, defaultValue) {
    const raw = optionalStringArg(args, 'case');
    if (raw === undefined)
        return defaultValue;
    if (raw === 'smart' || raw === 'sensitive' || raw === 'insensitive')
        return raw;
    throw new Error("Invalid arguments: `case` must be one of: 'smart', 'sensitive', 'insensitive'");
}
const RIPGREP_EXTENSION_TYPE_BY_EXT = new Map([
    ['cjs', 'js'],
    ['cts', 'ts'],
    ['css', 'css'],
    ['ejs', 'html'],
    ['htm', 'html'],
    ['html', 'html'],
    ['js', 'js'],
    ['json', 'json'],
    ['jsx', 'js'],
    ['markdown', 'md'],
    ['md', 'md'],
    ['mdown', 'md'],
    ['mdwn', 'md'],
    ['mdx', 'md'],
    ['mjs', 'js'],
    ['mkd', 'md'],
    ['mkdn', 'md'],
    ['mts', 'ts'],
    ['scss', 'css'],
    ['svelte', 'svelte'],
    ['ts', 'ts'],
    ['tsx', 'ts'],
    ['vue', 'vue'],
    ['yaml', 'yaml'],
    ['yml', 'yaml'],
]);
function normalizeRipgrepSearchPath(searchPath) {
    if (!searchPath.includes('\\'))
        return searchPath;
    if (process.platform !== 'win32' && !searchPath.startsWith('.\\'))
        return searchPath;
    return searchPath.replace(/\\/g, '/');
}
function parseSimpleExtensionGlob(glob) {
    const normalized = glob.trim().replace(/\\/g, '/').replace(/^\.\//, '');
    if (normalized.startsWith('!'))
        return null;
    const simpleMatch = /^(?:\*\*\/)?\*\.([A-Za-z0-9_+-]+)$/.exec(normalized);
    if (simpleMatch)
        return simpleMatch[1]?.toLowerCase() ?? null;
    const braceMatch = /^(?:\*\*\/)?\*\.\{([A-Za-z0-9_+,\-\s]+)\}$/.exec(normalized);
    if (!braceMatch)
        return null;
    const exts = braceMatch[1]
        ?.split(',')
        .map((ext) => ext.trim().toLowerCase())
        .filter((ext) => ext !== '');
    if (!exts || exts.length === 0)
        return null;
    return exts.join(',');
}
function extractSimpleExtensionGlobs(globs) {
    const typeNames = new Set();
    const extensionFilters = new Set();
    const passthroughGlobs = [];
    let hasComplexPositiveGlob = false;
    for (const glob of globs) {
        const extSpec = parseSimpleExtensionGlob(glob);
        if (!extSpec) {
            passthroughGlobs.push(glob);
            if (!glob.trim().startsWith('!'))
                hasComplexPositiveGlob = true;
            continue;
        }
        const exts = extSpec.split(',');
        let allKnown = true;
        const nextTypes = [];
        for (const ext of exts) {
            const typeName = RIPGREP_EXTENSION_TYPE_BY_EXT.get(ext);
            if (!typeName) {
                allKnown = false;
                break;
            }
            nextTypes.push(typeName);
        }
        if (!allKnown) {
            passthroughGlobs.push(glob);
            hasComplexPositiveGlob = true;
            continue;
        }
        for (const typeName of nextTypes)
            typeNames.add(typeName);
        for (const ext of exts)
            extensionFilters.add(ext);
    }
    if (hasComplexPositiveGlob) {
        return {
            typeArgs: [],
            extensionFilters: [],
            passthroughGlobs: globs,
        };
    }
    return {
        typeArgs: [...typeNames].flatMap((typeName) => ['--type', typeName]),
        extensionFilters: [...extensionFilters],
        passthroughGlobs,
    };
}
function buildFilterPlan(options, member) {
    const args = [];
    const access = buildAccessControlGlobs(member);
    for (const inc of access.include) {
        args.push('--glob', inc);
    }
    for (const exc of access.exclude) {
        args.push('--glob', `!${exc}`);
    }
    const simple = process.platform === 'win32'
        ? extractSimpleExtensionGlobs(options.globs)
        : { typeArgs: [], extensionFilters: [], passthroughGlobs: options.globs };
    args.push(...simple.typeArgs);
    for (const glob of simple.passthroughGlobs) {
        args.push('--glob', glob);
    }
    const globCompatibilityWarnings = process.platform === 'win32' && simple.passthroughGlobs.length > 0
        ? [
            'Some complex globs were passed through to rg --glob. On Windows, simple extension filters are translated to --type, but complex globs may still be affected by ripgrep glob/path compatibility.',
        ]
        : [];
    return {
        args,
        postFilter: (relPath) => simple.extensionFilters.length === 0 ||
            simple.extensionFilters.some((ext) => relPath.toLowerCase().endsWith(`.${ext}`)),
        globCompatibilityWarnings,
    };
}
function baseRgArgs(options, member) {
    const args = ['--no-messages', '--color=never'];
    if (options.includeHidden)
        args.push('--hidden');
    if (options.followSymlinks)
        args.push('--follow');
    if (options.caseMode === 'smart')
        args.push('--smart-case');
    if (options.caseMode === 'sensitive')
        args.push('--case-sensitive');
    if (options.caseMode === 'insensitive')
        args.push('--ignore-case');
    if (options.fixedStrings)
        args.push('--fixed-strings');
    const filters = buildFilterPlan(options, member);
    args.push(...filters.args);
    return {
        args,
        postFilter: filters.postFilter,
        globCompatibilityWarnings: filters.globCompatibilityWarnings,
    };
}
function fsBaseRgArgs(options) {
    const args = ['--no-messages', '--color=never'];
    if (options.includeHidden)
        args.push('--hidden');
    if (options.followSymlinks)
        args.push('--follow');
    if (options.caseMode === 'smart')
        args.push('--smart-case');
    if (options.caseMode === 'sensitive')
        args.push('--case-sensitive');
    if (options.caseMode === 'insensitive')
        args.push('--ignore-case');
    if (options.fixedStrings)
        args.push('--fixed-strings');
    const simple = process.platform === 'win32'
        ? extractSimpleExtensionGlobs(options.globs)
        : { typeArgs: [], extensionFilters: [], passthroughGlobs: options.globs };
    args.push(...simple.typeArgs);
    for (const glob of simple.passthroughGlobs) {
        args.push('--glob', glob);
    }
    const globCompatibilityWarnings = process.platform === 'win32' && simple.passthroughGlobs.length > 0
        ? [
            'Some complex globs were passed through to rg --glob. On Windows, simple extension filters are translated to --type, but complex globs may still be affected by ripgrep glob/path compatibility.',
        ]
        : [];
    return {
        args,
        postFilter: (relPath) => simple.extensionFilters.length === 0 ||
            simple.extensionFilters.some((ext) => relPath.toLowerCase().endsWith(`.${ext}`)),
        globCompatibilityWarnings,
    };
}
function buildRipgrepDiagnostics(run, filterPlan, returnedMatches) {
    const diagnostics = [];
    if (run.partialPathFailure) {
        diagnostics.push(returnedMatches
            ? RIPGREP_PARTIAL_PATH_FAILURE_SUMMARY
            : RIPGREP_PARTIAL_PATH_FAILURE_NO_MATCH_SUMMARY);
    }
    diagnostics.push(...filterPlan.globCompatibilityWarnings);
    return diagnostics;
}
function formatRipgrepNoMatchesSummary(partialPathFailure) {
    return partialPathFailure ? RIPGREP_PARTIAL_PATH_FAILURE_NO_MATCH_SUMMARY : 'No matches.';
}
function buildRawRipgrepArgsPlan(rawArgs) {
    if (process.platform !== 'win32') {
        return {
            args: [...rawArgs],
            postFilter: () => true,
            globCompatibilityWarnings: [],
        };
    }
    const parsedArgs = [];
    const globsForTypeConversion = [];
    let hasComplexPositiveGlob = false;
    for (let i = 0; i < rawArgs.length; i += 1) {
        const tok = rawArgs[i] ?? '';
        if (tok === '--glob' || tok === '--iglob' || tok === '-g') {
            const next = rawArgs[i + 1];
            if (typeof next === 'string' && parseSimpleExtensionGlob(next) !== null) {
                globsForTypeConversion.push(next);
                parsedArgs.push({ kind: 'glob_pair', flag: tok, glob: next });
                i += 1;
                continue;
            }
            if (typeof next === 'string') {
                if (!next.trim().startsWith('!'))
                    hasComplexPositiveGlob = true;
                parsedArgs.push({ kind: 'glob_pair', flag: tok, glob: next });
                i += 1;
            }
            else {
                parsedArgs.push({ kind: 'other', value: tok });
            }
            continue;
        }
        if (tok.startsWith('--glob=')) {
            const glob = tok.slice('--glob='.length);
            if (parseSimpleExtensionGlob(glob) !== null) {
                globsForTypeConversion.push(glob);
                parsedArgs.push({ kind: 'glob_equals', prefix: '--glob=', glob });
                continue;
            }
            if (!glob.trim().startsWith('!'))
                hasComplexPositiveGlob = true;
            parsedArgs.push({ kind: 'glob_equals', prefix: '--glob=', glob });
            continue;
        }
        if (tok.startsWith('--iglob=')) {
            const glob = tok.slice('--iglob='.length);
            if (parseSimpleExtensionGlob(glob) !== null) {
                globsForTypeConversion.push(glob);
                parsedArgs.push({ kind: 'glob_equals', prefix: '--iglob=', glob });
                continue;
            }
            if (!glob.trim().startsWith('!'))
                hasComplexPositiveGlob = true;
            parsedArgs.push({ kind: 'glob_equals', prefix: '--iglob=', glob });
            continue;
        }
        if (tok.startsWith('-g') && tok.length > 2) {
            const glob = tok.slice(2);
            if (parseSimpleExtensionGlob(glob) !== null) {
                globsForTypeConversion.push(glob);
                parsedArgs.push({ kind: 'glob_equals', prefix: '-g', glob });
                continue;
            }
            if (!glob.trim().startsWith('!'))
                hasComplexPositiveGlob = true;
            parsedArgs.push({ kind: 'glob_equals', prefix: '-g', glob });
            continue;
        }
        parsedArgs.push({ kind: 'other', value: tok });
    }
    const renderParsedArgs = (args) => args.flatMap((arg) => {
        switch (arg.kind) {
            case 'glob_pair':
                return [arg.flag, arg.glob];
            case 'glob_equals':
                return [`${arg.prefix}${arg.glob}`];
            case 'other':
                return [arg.value];
        }
    });
    if (hasComplexPositiveGlob) {
        return {
            args: renderParsedArgs(parsedArgs),
            postFilter: () => true,
            globCompatibilityWarnings: process.platform === 'win32'
                ? [
                    'rg_args contains complex globs, so simple extension globs were left as --glob to preserve ripgrep ordering semantics.',
                ]
                : [],
        };
    }
    const simple = extractSimpleExtensionGlobs(globsForTypeConversion);
    const convertedArgs = renderParsedArgs(parsedArgs.filter((arg) => {
        if (arg.kind === 'other')
            return true;
        return parseSimpleExtensionGlob(arg.glob) === null;
    }));
    convertedArgs.push(...simple.typeArgs);
    convertedArgs.push(...simple.passthroughGlobs.flatMap((glob) => ['--glob', glob]));
    const globCompatibilityWarnings = process.platform === 'win32' && simple.passthroughGlobs.length > 0
        ? [
            'Some rg_args globs were passed through to rg --glob because they are not simple extension filters.',
        ]
        : [];
    return {
        args: convertedArgs,
        postFilter: (relPath) => simple.extensionFilters.length === 0 ||
            simple.extensionFilters.some((ext) => relPath.toLowerCase().endsWith(`.${ext}`)),
        globCompatibilityWarnings,
    };
}
function appendTruncatedText(state, chunk, maxChars) {
    if (state.text.length >= maxChars) {
        state.omittedChars += chunk.length;
        return;
    }
    const remainingChars = maxChars - state.text.length;
    if (chunk.length <= remainingChars) {
        state.text += chunk;
        return;
    }
    state.text += chunk.slice(0, remainingChars);
    state.omittedChars += chunk.length - remainingChars;
}
function formatTruncatedText(state) {
    if (state.omittedChars === 0)
        return state.text;
    const suffix = `\n...[truncated ${state.omittedChars} chars]`;
    return `${state.text}${suffix}`;
}
function isRipgrepPartialPathFailure(stderrText) {
    const lower = stderrText.toLowerCase();
    if (lower.includes('unrecognized flag'))
        return false;
    if (lower.includes('unknown option'))
        return false;
    if (lower.includes('invalid option'))
        return false;
    return (lower.includes('os error') ||
        lower.includes('no such file or directory') ||
        lower.includes('the system cannot find the path specified') ||
        lower.includes('系统找不到指定的路径'));
}
async function runRgLines(args, onLine) {
    return await new Promise((resolve, reject) => {
        const child = (0, child_process_1.spawn)('rg', args, { cwd: process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] });
        if (!child.stdout || !child.stderr) {
            reject(new Error('rg failed to create stdio pipes'));
            return;
        }
        const stderrState = { text: '', omittedChars: 0 };
        const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
        let processClosed = false;
        let processCloseCode = null;
        let processCloseSignal = null;
        let stdoutConsumed = false;
        let settled = false;
        const rejectOnce = (error) => {
            if (settled)
                return;
            settled = true;
            rl.close();
            if (!child.killed)
                child.kill();
            reject(error);
        };
        const resolveOnce = () => {
            if (settled || !processClosed || !stdoutConsumed)
                return;
            settled = true;
            const stderrText = formatTruncatedText(stderrState).trim();
            if (processCloseSignal) {
                reject(new Error(`rg terminated by signal ${processCloseSignal}`));
                return;
            }
            if (processCloseCode !== null && processCloseCode !== 0 && processCloseCode !== 1) {
                if (processCloseCode === 2 && isRipgrepPartialPathFailure(stderrText)) {
                    resolve({
                        partialPathFailure: true,
                        stderrText,
                    });
                    return;
                }
                const summary = stderrText === '' ? `rg exited with code ${processCloseCode}` : stderrText;
                reject(new Error(summary));
                return;
            }
            resolve({ partialPathFailure: false, stderrText });
        };
        child.on('error', (err) => rejectOnce(err));
        child.stdout.on('error', (err) => rejectOnce(err));
        child.stderr.on('error', (err) => rejectOnce(err));
        child.stderr.on('data', (chunk) => {
            appendTruncatedText(stderrState, typeof chunk === 'string' ? chunk : chunk.toString('utf8'), RIPGREP_STDERR_CHAR_LIMIT);
        });
        child.on('close', (code, signal) => {
            processClosed = true;
            processCloseCode = code;
            processCloseSignal = signal;
            resolveOnce();
        });
        void (async () => {
            try {
                for await (const line of rl) {
                    if (line === '')
                        continue;
                    await onLine(line);
                }
                stdoutConsumed = true;
                resolveOnce();
            }
            catch (error) {
                rejectOnce(error);
            }
        })();
    });
}
async function loadFileLines(relPath) {
    const abs = path_1.default.resolve(process.cwd(), relPath);
    const text = await promises_1.default.readFile(abs, 'utf8');
    const parts = text.split('\n');
    if (parts.length > 0 && parts[parts.length - 1] === '')
        parts.pop();
    return parts;
}
async function runRipgrepFiles(caller, pattern, searchPath, options) {
    const forbiddenPath = detectForbiddenRipgrepSearchPath(caller, searchPath);
    if (forbiddenPath) {
        return formatForbiddenSearchPathAccessDeniedYaml('files', pattern, searchPath, forbiddenPath);
    }
    const forbiddenGlob = detectForbiddenRipgrepGlobs(caller, options.globs);
    if (forbiddenGlob) {
        return formatForbiddenSearchPathAccessDeniedYaml('files', pattern, searchPath, forbiddenGlob);
    }
    const filterPlan = baseRgArgs(options, caller);
    const args = [
        ...filterPlan.args,
        '--files-with-matches',
        '--',
        pattern,
        normalizeRipgrepSearchPath(searchPath),
    ];
    try {
        let totalFiles = 0;
        const results = [];
        const run = await runRgLines(args, (line) => {
            if (!filterPlan.postFilter(line))
                return;
            totalFiles++;
            if (results.length < options.maxFiles)
                results.push({ path: line });
        });
        const diagnostics = buildRipgrepDiagnostics(run, filterPlan, totalFiles > 0);
        const truncated = totalFiles > options.maxFiles;
        const summary = totalFiles === 0
            ? formatRipgrepNoMatchesSummary(run.partialPathFailure)
            : truncated
                ? `Found ${totalFiles} files; showing first ${options.maxFiles} (truncated=true).${run.partialPathFailure ? ` ${RIPGREP_PARTIAL_PATH_FAILURE_SUMMARY}` : ''}`
                : `Found ${totalFiles} files.${run.partialPathFailure ? ` ${RIPGREP_PARTIAL_PATH_FAILURE_SUMMARY}` : ''}`;
        const yaml = [
            `status: ${run.partialPathFailure ? 'partial_failure' : 'ok'}`,
            `pattern: ${yamlQuote(pattern)}`,
            `mode: files`,
            `path: ${yamlQuote(searchPath)}`,
            ...(options.globs.length > 0 ? [`globs: ${yamlFlowStringArray(options.globs)}`] : []),
            `case: ${options.caseMode}`,
            `fixed_strings: ${options.fixedStrings}`,
            `regex: ${!options.fixedStrings}`,
            `truncated: ${truncated}`,
            `limits:`,
            `  max_files: ${options.maxFiles}`,
            `totals:`,
            `  files_matched: ${totalFiles}`,
            `diagnostics:`,
            `  partial_path_failure: ${run.partialPathFailure}`,
            `  messages: ${yamlFlowStringArray(diagnostics)}`,
            `summary: ${yamlQuote(summary)}`,
            `results:`,
            ...results.map((r) => `  - path: ${yamlQuote(r.path)}`),
        ].join('\n');
        return run.partialPathFailure ? (0, tool_1.toolPartialFailure)(formatYamlCodeBlock(yaml)) : okYaml(yaml);
    }
    catch (error) {
        return failYaml([
            `status: error`,
            `pattern: ${yamlQuote(pattern)}`,
            `mode: files`,
            `path: ${yamlQuote(searchPath)}`,
            `error: FAILED`,
            `summary: ${yamlQuote(error instanceof Error ? error.message : String(error))}`,
        ].join('\n'));
    }
}
exports.ripgrepFilesTool = {
    type: 'func',
    name: 'ripgrep_files',
    description: 'Search file paths containing a pattern (rg-backed).',
    descriptionI18n: {
        en: 'Search file paths containing a pattern (rg-backed).',
        zh: '搜索包含匹配内容的文件路径（rg 驱动）。',
    },
    parameters: {
        type: 'object',
        additionalProperties: false,
        properties: {
            pattern: { type: 'string' },
            path: { type: 'string' },
            globs: { type: 'array', items: { type: 'string' } },
            include: { type: ['string', 'array'], items: { type: 'string' } },
            case: { type: 'string' },
            fixed_strings: { type: 'boolean' },
            max_files: { type: 'integer' },
            include_hidden: { type: 'boolean' },
            follow_symlinks: { type: 'boolean' },
        },
        required: ['pattern'],
    },
    argsValidation: 'dominds',
    call: async (_dlg, caller, args) => {
        const pattern = requireNonEmptyStringArg(args, 'pattern');
        const searchPath = optionalStringArg(args, 'path') ?? '.';
        const globs = parseRipgrepGlobsArg(args);
        const caseMode = parseRipgrepCaseModeArg(args, 'smart');
        const fixedStrings = optionalBooleanArg(args, 'fixed_strings') ?? false;
        const includeHidden = optionalBooleanArg(args, 'include_hidden') ?? false;
        const followSymlinks = optionalBooleanArg(args, 'follow_symlinks') ?? false;
        const maxFiles = optionalPositiveIntegerArg(args, 'max_files') ?? 50;
        const options = {
            globs,
            caseMode,
            fixedStrings,
            includeHidden,
            followSymlinks,
            maxFiles,
        };
        return await runRipgrepFiles(caller, pattern, searchPath, options);
    },
};
async function runFsRipgrepFiles(pattern, searchPath, options) {
    const filterPlan = fsBaseRgArgs(options);
    const args = [
        ...filterPlan.args,
        '--files-with-matches',
        '--',
        pattern,
        normalizeRipgrepSearchPath(searchPath),
    ];
    try {
        let totalFiles = 0;
        const results = [];
        const run = await runRgLines(args, (line) => {
            if (!filterPlan.postFilter(line))
                return;
            totalFiles++;
            if (results.length < options.maxFiles)
                results.push({ path: line });
        });
        const diagnostics = buildRipgrepDiagnostics(run, filterPlan, totalFiles > 0);
        const truncated = totalFiles > options.maxFiles;
        const summary = totalFiles === 0
            ? formatRipgrepNoMatchesSummary(run.partialPathFailure)
            : truncated
                ? `Found ${totalFiles} files; showing first ${options.maxFiles} (truncated=true).${run.partialPathFailure ? ` ${RIPGREP_PARTIAL_PATH_FAILURE_SUMMARY}` : ''}`
                : `Found ${totalFiles} files.${run.partialPathFailure ? ` ${RIPGREP_PARTIAL_PATH_FAILURE_SUMMARY}` : ''}`;
        const yaml = [
            `status: ${run.partialPathFailure ? 'partial_failure' : 'ok'}`,
            `pattern: ${yamlQuote(pattern)}`,
            `mode: fs_files`,
            `path: ${yamlQuote(searchPath)}`,
            ...(options.globs.length > 0 ? [`globs: ${yamlFlowStringArray(options.globs)}`] : []),
            `case: ${options.caseMode}`,
            `fixed_strings: ${options.fixedStrings}`,
            `regex: ${!options.fixedStrings}`,
            `truncated: ${truncated}`,
            `limits:`,
            `  max_files: ${options.maxFiles}`,
            `totals:`,
            `  files_matched: ${totalFiles}`,
            `diagnostics:`,
            `  partial_path_failure: ${run.partialPathFailure}`,
            `  messages: ${yamlFlowStringArray(diagnostics)}`,
            `summary: ${yamlQuote(summary)}`,
            `results:`,
            ...results.map((r) => `  - path: ${yamlQuote(r.path)}`),
        ].join('\n');
        return run.partialPathFailure ? (0, tool_1.toolPartialFailure)(formatYamlCodeBlock(yaml)) : okYaml(yaml);
    }
    catch (error) {
        return failYaml([
            `status: error`,
            `pattern: ${yamlQuote(pattern)}`,
            `mode: fs_files`,
            `path: ${yamlQuote(searchPath)}`,
            `error: FAILED`,
            `summary: ${yamlQuote(error instanceof Error ? error.message : String(error))}`,
        ].join('\n'));
    }
}
exports.fsRipgrepFilesTool = {
    type: 'func',
    name: 'fs_ripgrep_files',
    description: 'Search local filesystem file paths containing a pattern without restricting paths to rtws (rg-backed).',
    descriptionI18n: {
        en: 'Search local filesystem file paths containing a pattern without restricting paths to rtws (rg-backed).',
        zh: '搜索本机文件系统中包含匹配内容的文件路径，不限制路径必须位于 rtws 内（rg 驱动）。',
    },
    parameters: exports.ripgrepFilesTool.parameters,
    argsValidation: 'dominds',
    call: async (_dlg, _caller, args) => {
        const pattern = requireNonEmptyStringArg(args, 'pattern');
        const searchPath = optionalStringArg(args, 'path') ?? '.';
        const globs = parseRipgrepGlobsArg(args);
        const caseMode = parseRipgrepCaseModeArg(args, 'smart');
        const fixedStrings = optionalBooleanArg(args, 'fixed_strings') ?? false;
        const includeHidden = optionalBooleanArg(args, 'include_hidden') ?? false;
        const followSymlinks = optionalBooleanArg(args, 'follow_symlinks') ?? false;
        const maxFiles = optionalPositiveIntegerArg(args, 'max_files') ?? 50;
        return await runFsRipgrepFiles(pattern, searchPath, {
            globs,
            caseMode,
            fixedStrings,
            includeHidden,
            followSymlinks,
            maxFiles,
        });
    },
};
async function runRipgrepCount(caller, pattern, searchPath, options) {
    const forbiddenPath = detectForbiddenRipgrepSearchPath(caller, searchPath);
    if (forbiddenPath) {
        return formatForbiddenSearchPathAccessDeniedYaml('count', pattern, searchPath, forbiddenPath);
    }
    const forbiddenGlob = detectForbiddenRipgrepGlobs(caller, options.globs);
    if (forbiddenGlob) {
        return formatForbiddenSearchPathAccessDeniedYaml('count', pattern, searchPath, forbiddenGlob);
    }
    const filterPlan = baseRgArgs(options, caller);
    const args = [
        ...filterPlan.args,
        '--count-matches',
        '--',
        pattern,
        normalizeRipgrepSearchPath(searchPath),
    ];
    try {
        let totalFiles = 0;
        let totalMatches = 0;
        const results = [];
        const run = await runRgLines(args, (line) => {
            const idx = line.lastIndexOf(':');
            if (idx <= 0)
                return;
            const p = line.slice(0, idx);
            if (!filterPlan.postFilter(p))
                return;
            const rawCount = line.slice(idx + 1);
            const count = Number.parseInt(rawCount, 10);
            if (!Number.isFinite(count))
                return;
            totalFiles++;
            totalMatches += count;
            if (results.length < options.maxFiles)
                results.push({ path: p, count });
        });
        const diagnostics = buildRipgrepDiagnostics(run, filterPlan, totalMatches > 0);
        const truncated = totalFiles > options.maxFiles;
        const summary = totalMatches === 0
            ? formatRipgrepNoMatchesSummary(run.partialPathFailure)
            : truncated
                ? `Counted ${totalMatches} matches in ${totalFiles} files; showing first ${options.maxFiles} files (truncated=true).${run.partialPathFailure ? ` ${RIPGREP_PARTIAL_PATH_FAILURE_SUMMARY}` : ''}`
                : `Counted ${totalMatches} matches in ${totalFiles} files.${run.partialPathFailure ? ` ${RIPGREP_PARTIAL_PATH_FAILURE_SUMMARY}` : ''}`;
        const yaml = [
            `status: ${run.partialPathFailure ? 'partial_failure' : 'ok'}`,
            `pattern: ${yamlQuote(pattern)}`,
            `mode: count`,
            `path: ${yamlQuote(searchPath)}`,
            ...(options.globs.length > 0 ? [`globs: ${yamlFlowStringArray(options.globs)}`] : []),
            `case: ${options.caseMode}`,
            `fixed_strings: ${options.fixedStrings}`,
            `regex: ${!options.fixedStrings}`,
            `truncated: ${truncated}`,
            `limits:`,
            `  max_files: ${options.maxFiles}`,
            `totals:`,
            `  files_matched: ${totalFiles}`,
            `  matches: ${totalMatches}`,
            `diagnostics:`,
            `  partial_path_failure: ${run.partialPathFailure}`,
            `  messages: ${yamlFlowStringArray(diagnostics)}`,
            `summary: ${yamlQuote(summary)}`,
            `results:`,
            ...results.map((r) => `  - path: ${yamlQuote(r.path)}\n    count: ${r.count}`),
        ].join('\n');
        return run.partialPathFailure ? (0, tool_1.toolPartialFailure)(formatYamlCodeBlock(yaml)) : okYaml(yaml);
    }
    catch (error) {
        return failYaml([
            `status: error`,
            `pattern: ${yamlQuote(pattern)}`,
            `mode: count`,
            `path: ${yamlQuote(searchPath)}`,
            `error: FAILED`,
            `summary: ${yamlQuote(error instanceof Error ? error.message : String(error))}`,
        ].join('\n'));
    }
}
exports.ripgrepCountTool = {
    type: 'func',
    name: 'ripgrep_count',
    description: 'Count matches per file (rg-backed).',
    descriptionI18n: {
        en: 'Count matches per file (rg-backed).',
        zh: '统计每个文件的匹配数量（rg 驱动）。',
    },
    parameters: {
        type: 'object',
        additionalProperties: false,
        properties: {
            pattern: { type: 'string' },
            path: { type: 'string' },
            globs: { type: 'array', items: { type: 'string' } },
            include: { type: ['string', 'array'], items: { type: 'string' } },
            case: { type: 'string' },
            fixed_strings: { type: 'boolean' },
            max_files: { type: 'integer' },
            include_hidden: { type: 'boolean' },
            follow_symlinks: { type: 'boolean' },
        },
        required: ['pattern'],
    },
    argsValidation: 'dominds',
    call: async (_dlg, caller, args) => {
        const pattern = requireNonEmptyStringArg(args, 'pattern');
        const searchPath = optionalStringArg(args, 'path') ?? '.';
        const globs = parseRipgrepGlobsArg(args);
        const caseMode = parseRipgrepCaseModeArg(args, 'smart');
        const fixedStrings = optionalBooleanArg(args, 'fixed_strings') ?? false;
        const includeHidden = optionalBooleanArg(args, 'include_hidden') ?? false;
        const followSymlinks = optionalBooleanArg(args, 'follow_symlinks') ?? false;
        const maxFiles = optionalPositiveIntegerArg(args, 'max_files') ?? 200;
        const options = {
            globs,
            caseMode,
            fixedStrings,
            includeHidden,
            followSymlinks,
            maxFiles,
        };
        return await runRipgrepCount(caller, pattern, searchPath, options);
    },
};
async function runFsRipgrepCount(pattern, searchPath, options) {
    const filterPlan = fsBaseRgArgs(options);
    const args = [
        ...filterPlan.args,
        '--count-matches',
        '--',
        pattern,
        normalizeRipgrepSearchPath(searchPath),
    ];
    try {
        let totalFiles = 0;
        let totalMatches = 0;
        const results = [];
        const run = await runRgLines(args, (line) => {
            const idx = line.lastIndexOf(':');
            if (idx <= 0)
                return;
            const p = line.slice(0, idx);
            if (!filterPlan.postFilter(p))
                return;
            const rawCount = line.slice(idx + 1);
            const count = Number.parseInt(rawCount, 10);
            if (!Number.isFinite(count))
                return;
            totalFiles++;
            totalMatches += count;
            if (results.length < options.maxFiles)
                results.push({ path: p, count });
        });
        const diagnostics = buildRipgrepDiagnostics(run, filterPlan, totalMatches > 0);
        const truncated = totalFiles > options.maxFiles;
        const summary = totalMatches === 0
            ? formatRipgrepNoMatchesSummary(run.partialPathFailure)
            : truncated
                ? `Counted ${totalMatches} matches in ${totalFiles} files; showing first ${options.maxFiles} files (truncated=true).${run.partialPathFailure ? ` ${RIPGREP_PARTIAL_PATH_FAILURE_SUMMARY}` : ''}`
                : `Counted ${totalMatches} matches in ${totalFiles} files.${run.partialPathFailure ? ` ${RIPGREP_PARTIAL_PATH_FAILURE_SUMMARY}` : ''}`;
        const yaml = [
            `status: ${run.partialPathFailure ? 'partial_failure' : 'ok'}`,
            `pattern: ${yamlQuote(pattern)}`,
            `mode: fs_count`,
            `path: ${yamlQuote(searchPath)}`,
            ...(options.globs.length > 0 ? [`globs: ${yamlFlowStringArray(options.globs)}`] : []),
            `case: ${options.caseMode}`,
            `fixed_strings: ${options.fixedStrings}`,
            `regex: ${!options.fixedStrings}`,
            `truncated: ${truncated}`,
            `limits:`,
            `  max_files: ${options.maxFiles}`,
            `totals:`,
            `  files_matched: ${totalFiles}`,
            `  matches: ${totalMatches}`,
            `diagnostics:`,
            `  partial_path_failure: ${run.partialPathFailure}`,
            `  messages: ${yamlFlowStringArray(diagnostics)}`,
            `summary: ${yamlQuote(summary)}`,
            `results:`,
            ...results.map((r) => `  - path: ${yamlQuote(r.path)}\n    count: ${r.count}`),
        ].join('\n');
        return run.partialPathFailure ? (0, tool_1.toolPartialFailure)(formatYamlCodeBlock(yaml)) : okYaml(yaml);
    }
    catch (error) {
        return failYaml([
            `status: error`,
            `pattern: ${yamlQuote(pattern)}`,
            `mode: fs_count`,
            `path: ${yamlQuote(searchPath)}`,
            `error: FAILED`,
            `summary: ${yamlQuote(error instanceof Error ? error.message : String(error))}`,
        ].join('\n'));
    }
}
exports.fsRipgrepCountTool = {
    type: 'func',
    name: 'fs_ripgrep_count',
    description: 'Count local filesystem matches per file without restricting paths to rtws (rg-backed).',
    descriptionI18n: {
        en: 'Count local filesystem matches per file without restricting paths to rtws (rg-backed).',
        zh: '统计本机文件系统中每个文件的匹配数量，不限制路径必须位于 rtws 内（rg 驱动）。',
    },
    parameters: exports.ripgrepCountTool.parameters,
    argsValidation: 'dominds',
    call: async (_dlg, _caller, args) => {
        const pattern = requireNonEmptyStringArg(args, 'pattern');
        const searchPath = optionalStringArg(args, 'path') ?? '.';
        const globs = parseRipgrepGlobsArg(args);
        const caseMode = parseRipgrepCaseModeArg(args, 'smart');
        const fixedStrings = optionalBooleanArg(args, 'fixed_strings') ?? false;
        const includeHidden = optionalBooleanArg(args, 'include_hidden') ?? false;
        const followSymlinks = optionalBooleanArg(args, 'follow_symlinks') ?? false;
        const maxFiles = optionalPositiveIntegerArg(args, 'max_files') ?? 200;
        return await runFsRipgrepCount(pattern, searchPath, {
            globs,
            caseMode,
            fixedStrings,
            includeHidden,
            followSymlinks,
            maxFiles,
        });
    },
};
async function runRipgrepSnippets(caller, pattern, searchPath, options) {
    const forbiddenPath = detectForbiddenRipgrepSearchPath(caller, searchPath);
    if (forbiddenPath) {
        return formatForbiddenSearchPathAccessDeniedYaml('snippets', pattern, searchPath, forbiddenPath);
    }
    const forbiddenGlob = detectForbiddenRipgrepGlobs(caller, options.globs);
    if (forbiddenGlob) {
        return formatForbiddenSearchPathAccessDeniedYaml('snippets', pattern, searchPath, forbiddenGlob);
    }
    const filterPlan = baseRgArgs(options, caller);
    const args = [
        ...filterPlan.args,
        '--vimgrep',
        '--',
        pattern,
        normalizeRipgrepSearchPath(searchPath),
    ];
    try {
        let totalMatches = 0;
        const fileSet = new Set();
        const results = [];
        const fileCache = new Map();
        const run = await runRgLines(args, async (line) => {
            const first = line.indexOf(':');
            if (first <= 0)
                return;
            const second = line.indexOf(':', first + 1);
            if (second <= first)
                return;
            const third = line.indexOf(':', second + 1);
            if (third <= second)
                return;
            const filePath = line.slice(0, first);
            const lineStr = line.slice(first + 1, second);
            const colStr = line.slice(second + 1, third);
            const text = line.slice(third + 1);
            const ln = Number.parseInt(lineStr, 10);
            const col = Number.parseInt(colStr, 10);
            if (!Number.isFinite(ln) || !Number.isFinite(col))
                return;
            if (!filterPlan.postFilter(filePath))
                return;
            totalMatches++;
            fileSet.add(filePath);
            if (results.length >= options.maxResults)
                return;
            let lines = fileCache.get(filePath);
            if (!lines) {
                lines = await loadFileLines(filePath).catch(() => []);
                fileCache.set(filePath, lines);
            }
            const idx0 = ln - 1;
            const before = lines.slice(Math.max(0, idx0 - options.contextBefore), idx0);
            const after = lines.slice(idx0 + 1, idx0 + 1 + options.contextAfter);
            results.push({ path: filePath, line: ln, col, match: text, before, after });
        });
        const diagnostics = buildRipgrepDiagnostics(run, filterPlan, totalMatches > 0);
        return formatRipgrepSnippetYaml({
            pattern,
            searchPath,
            globs: options.globs,
            caseMode: options.caseMode,
            fixedStrings: options.fixedStrings,
            maxResults: options.maxResults,
            fileCount: fileSet.size,
            totalMatches,
            partialPathFailure: run.partialPathFailure,
            diagnostics,
            results,
        });
    }
    catch (error) {
        return failYaml([
            `status: error`,
            `pattern: ${yamlQuote(pattern)}`,
            `mode: snippets`,
            `path: ${yamlQuote(searchPath)}`,
            `error: FAILED`,
            `summary: ${yamlQuote(error instanceof Error ? error.message : String(error))}`,
        ].join('\n'));
    }
}
exports.ripgrepSnippetsTool = {
    type: 'func',
    name: 'ripgrep_snippets',
    description: 'Search snippets with line/col (rg-backed).',
    descriptionI18n: {
        en: 'Search snippets with line/col (rg-backed).',
        zh: '搜索并显示带行列的匹配片段（rg 驱动）。',
    },
    parameters: {
        type: 'object',
        additionalProperties: false,
        properties: {
            pattern: { type: 'string' },
            path: { type: 'string' },
            globs: { type: 'array', items: { type: 'string' } },
            include: { type: ['string', 'array'], items: { type: 'string' } },
            case: { type: 'string' },
            fixed_strings: { type: 'boolean' },
            context_before: { type: 'integer' },
            context_after: { type: 'integer' },
            max_results: { type: 'integer' },
            include_hidden: { type: 'boolean' },
            follow_symlinks: { type: 'boolean' },
        },
        required: ['pattern'],
    },
    argsValidation: 'dominds',
    call: async (_dlg, caller, args) => {
        const pattern = requireNonEmptyStringArg(args, 'pattern');
        const searchPath = optionalStringArg(args, 'path') ?? '.';
        const globs = parseRipgrepGlobsArg(args);
        const caseMode = parseRipgrepCaseModeArg(args, 'smart');
        const fixedStrings = optionalBooleanArg(args, 'fixed_strings') ?? false;
        const includeHidden = optionalBooleanArg(args, 'include_hidden') ?? false;
        const followSymlinks = optionalBooleanArg(args, 'follow_symlinks') ?? false;
        const maxResults = optionalPositiveIntegerArg(args, 'max_results') ?? 50;
        const contextBefore = optionalNonNegativeIntegerArg(args, 'context_before') ?? 1;
        const contextAfter = optionalNonNegativeIntegerArg(args, 'context_after') ?? 1;
        const options = {
            globs,
            caseMode,
            fixedStrings,
            includeHidden,
            followSymlinks,
            maxResults,
            contextBefore,
            contextAfter,
        };
        return await runRipgrepSnippets(caller, pattern, searchPath, options);
    },
};
async function runFsRipgrepSnippets(pattern, searchPath, options) {
    const filterPlan = fsBaseRgArgs(options);
    const args = [
        ...filterPlan.args,
        '--vimgrep',
        '--',
        pattern,
        normalizeRipgrepSearchPath(searchPath),
    ];
    try {
        let totalMatches = 0;
        const fileSet = new Set();
        const results = [];
        const fileCache = new Map();
        const run = await runRgLines(args, async (line) => {
            const first = line.indexOf(':');
            if (first <= 0)
                return;
            const second = line.indexOf(':', first + 1);
            if (second <= first)
                return;
            const third = line.indexOf(':', second + 1);
            if (third <= second)
                return;
            const filePath = line.slice(0, first);
            const lineStr = line.slice(first + 1, second);
            const colStr = line.slice(second + 1, third);
            const text = line.slice(third + 1);
            const ln = Number.parseInt(lineStr, 10);
            const col = Number.parseInt(colStr, 10);
            if (!Number.isFinite(ln) || !Number.isFinite(col))
                return;
            if (!filterPlan.postFilter(filePath))
                return;
            totalMatches++;
            fileSet.add(filePath);
            if (results.length >= options.maxResults)
                return;
            let lines = fileCache.get(filePath);
            if (!lines) {
                lines = await loadFileLines(filePath).catch(() => []);
                fileCache.set(filePath, lines);
            }
            const idx0 = ln - 1;
            const before = lines.slice(Math.max(0, idx0 - options.contextBefore), idx0);
            const after = lines.slice(idx0 + 1, idx0 + 1 + options.contextAfter);
            results.push({ path: filePath, line: ln, col, match: text, before, after });
        });
        const diagnostics = buildRipgrepDiagnostics(run, filterPlan, totalMatches > 0);
        return formatRipgrepSnippetYaml({
            pattern,
            searchPath,
            globs: options.globs,
            caseMode: options.caseMode,
            fixedStrings: options.fixedStrings,
            maxResults: options.maxResults,
            fileCount: fileSet.size,
            totalMatches,
            partialPathFailure: run.partialPathFailure,
            diagnostics,
            results,
        });
    }
    catch (error) {
        return failYaml([
            `status: error`,
            `pattern: ${yamlQuote(pattern)}`,
            `mode: fs_snippets`,
            `path: ${yamlQuote(searchPath)}`,
            `error: FAILED`,
            `summary: ${yamlQuote(error instanceof Error ? error.message : String(error))}`,
        ].join('\n'));
    }
}
exports.fsRipgrepSnippetsTool = {
    type: 'func',
    name: 'fs_ripgrep_snippets',
    description: 'Search local filesystem snippets with line/col without restricting paths to rtws (rg-backed).',
    descriptionI18n: {
        en: 'Search local filesystem snippets with line/col without restricting paths to rtws (rg-backed).',
        zh: '搜索本机文件系统并显示带行列的匹配片段，不限制路径必须位于 rtws 内（rg 驱动）。',
    },
    parameters: exports.ripgrepSnippetsTool.parameters,
    argsValidation: 'dominds',
    call: async (_dlg, _caller, args) => {
        const pattern = requireNonEmptyStringArg(args, 'pattern');
        const searchPath = optionalStringArg(args, 'path') ?? '.';
        const globs = parseRipgrepGlobsArg(args);
        const caseMode = parseRipgrepCaseModeArg(args, 'smart');
        const fixedStrings = optionalBooleanArg(args, 'fixed_strings') ?? false;
        const includeHidden = optionalBooleanArg(args, 'include_hidden') ?? false;
        const followSymlinks = optionalBooleanArg(args, 'follow_symlinks') ?? false;
        const maxResults = optionalPositiveIntegerArg(args, 'max_results') ?? 50;
        const contextBefore = optionalNonNegativeIntegerArg(args, 'context_before') ?? 1;
        const contextAfter = optionalNonNegativeIntegerArg(args, 'context_after') ?? 1;
        return await runFsRipgrepSnippets(pattern, searchPath, {
            globs,
            caseMode,
            fixedStrings,
            includeHidden,
            followSymlinks,
            maxResults,
            contextBefore,
            contextAfter,
        });
    },
};
exports.ripgrepFixedTool = {
    type: 'func',
    name: 'ripgrep_fixed',
    description: 'Fixed-string ripgrep convenience tool (routes to ripgrep_files/ripgrep_snippets/ripgrep_count with fixed_strings=true).',
    descriptionI18n: {
        en: 'Fixed-string ripgrep convenience tool (routes to ripgrep_files/ripgrep_snippets/ripgrep_count with fixed_strings=true).',
        zh: '固定字符串 ripgrep 便捷工具（等同于对 ripgrep_files/ripgrep_snippets/ripgrep_count 使用 fixed_strings=true）。',
    },
    parameters: {
        type: 'object',
        additionalProperties: false,
        properties: {
            literal: { type: 'string' },
            path: { type: 'string' },
            mode: { type: 'string' },
            globs: { type: 'array', items: { type: 'string' } },
            include: { type: ['string', 'array'], items: { type: 'string' } },
            case: { type: 'string' },
            max_files: { type: 'integer' },
            max_results: { type: 'integer' },
            context_before: { type: 'integer' },
            context_after: { type: 'integer' },
            include_hidden: { type: 'boolean' },
            follow_symlinks: { type: 'boolean' },
        },
        required: ['literal'],
    },
    argsValidation: 'dominds',
    call: async (_dlg, caller, args) => {
        const literal = requireNonEmptyStringArg(args, 'literal');
        const searchPath = optionalStringArg(args, 'path') ?? '.';
        const modeRaw = optionalStringArg(args, 'mode') ?? 'snippets';
        if (modeRaw !== 'files' && modeRaw !== 'snippets' && modeRaw !== 'count') {
            throw new Error("Invalid arguments: `mode` must be one of: 'files', 'snippets', 'count'");
        }
        const globs = parseRipgrepGlobsArg(args);
        const caseMode = parseRipgrepCaseModeArg(args, 'smart');
        const includeHidden = optionalBooleanArg(args, 'include_hidden') ?? false;
        const followSymlinks = optionalBooleanArg(args, 'follow_symlinks') ?? false;
        const base = {
            globs,
            caseMode,
            fixedStrings: true,
            includeHidden,
            followSymlinks,
        };
        if (modeRaw === 'files') {
            const maxFiles = optionalPositiveIntegerArg(args, 'max_files') ?? 50;
            const options = { ...base, maxFiles };
            return await runRipgrepFiles(caller, literal, searchPath, options);
        }
        if (modeRaw === 'count') {
            const maxFiles = optionalPositiveIntegerArg(args, 'max_files') ?? 200;
            const options = { ...base, maxFiles };
            return await runRipgrepCount(caller, literal, searchPath, options);
        }
        const maxResults = optionalPositiveIntegerArg(args, 'max_results') ?? 50;
        const contextBefore = optionalNonNegativeIntegerArg(args, 'context_before') ?? 1;
        const contextAfter = optionalNonNegativeIntegerArg(args, 'context_after') ?? 1;
        const options = { ...base, maxResults, contextBefore, contextAfter };
        return await runRipgrepSnippets(caller, literal, searchPath, options);
    },
};
exports.fsRipgrepFixedTool = {
    type: 'func',
    name: 'fs_ripgrep_fixed',
    description: 'Fixed-string ripgrep convenience tool for local filesystem search without restricting paths to rtws.',
    descriptionI18n: {
        en: 'Fixed-string ripgrep convenience tool for local filesystem search without restricting paths to rtws.',
        zh: '本机文件系统固定字符串 ripgrep 便捷工具，不限制路径必须位于 rtws 内。',
    },
    parameters: exports.ripgrepFixedTool.parameters,
    argsValidation: 'dominds',
    call: async (_dlg, _caller, args) => {
        const literal = requireNonEmptyStringArg(args, 'literal');
        const searchPath = optionalStringArg(args, 'path') ?? '.';
        const modeRaw = optionalStringArg(args, 'mode') ?? 'snippets';
        if (modeRaw !== 'files' && modeRaw !== 'snippets' && modeRaw !== 'count') {
            throw new Error("Invalid arguments: `mode` must be one of: 'files', 'snippets', 'count'");
        }
        const globs = parseRipgrepGlobsArg(args);
        const caseMode = parseRipgrepCaseModeArg(args, 'smart');
        const includeHidden = optionalBooleanArg(args, 'include_hidden') ?? false;
        const followSymlinks = optionalBooleanArg(args, 'follow_symlinks') ?? false;
        const base = {
            globs,
            caseMode,
            fixedStrings: true,
            includeHidden,
            followSymlinks,
        };
        if (modeRaw === 'files') {
            const maxFiles = optionalPositiveIntegerArg(args, 'max_files') ?? 50;
            return await runFsRipgrepFiles(literal, searchPath, { ...base, maxFiles });
        }
        if (modeRaw === 'count') {
            const maxFiles = optionalPositiveIntegerArg(args, 'max_files') ?? 200;
            return await runFsRipgrepCount(literal, searchPath, { ...base, maxFiles });
        }
        const maxResults = optionalPositiveIntegerArg(args, 'max_results') ?? 50;
        const contextBefore = optionalNonNegativeIntegerArg(args, 'context_before') ?? 1;
        const contextAfter = optionalNonNegativeIntegerArg(args, 'context_after') ?? 1;
        return await runFsRipgrepSnippets(literal, searchPath, {
            ...base,
            maxResults,
            contextBefore,
            contextAfter,
        });
    },
};
const DISALLOWED_RG_ARGS = new Set(['--pre', '--pre-glob']);
async function runRipgrepSearch(caller, pattern, searchPath, rawRgArgs) {
    const forbiddenPath = detectForbiddenRipgrepSearchPath(caller, searchPath);
    if (forbiddenPath) {
        return formatForbiddenSearchPathAccessDeniedYaml('snippets', pattern, searchPath, forbiddenPath);
    }
    const forbiddenInArgs = detectForbiddenRipgrepRawArgs(caller, rawRgArgs);
    if (forbiddenInArgs) {
        return formatForbiddenSearchPathAccessDeniedYaml('snippets', pattern, searchPath, forbiddenInArgs);
    }
    for (const tok of rawRgArgs) {
        if (DISALLOWED_RG_ARGS.has(tok)) {
            return failYaml(`status: error\nerror: DISALLOWED_ARG\nsummary: ${yamlQuote(`Disallowed rg arg: ${tok}`)}`);
        }
        if (tok === '--json' ||
            tok === '--count' ||
            tok === '--count-matches' ||
            tok === '--files-with-matches' ||
            tok === '--files') {
            return failYaml(`status: error\nerror: DISALLOWED_ARG\nsummary: ${yamlQuote(`Disallowed rg output arg: ${tok}`)}`);
        }
    }
    const options = {
        ...defaultBaseOptions(),
        fixedStrings: false,
        maxResults: 50,
        contextBefore: 1,
        contextAfter: 1,
    };
    const filterPlan = baseRgArgs(options, caller);
    const rawArgsPlan = buildRawRipgrepArgsPlan(rawRgArgs);
    const rgArgs = [
        ...filterPlan.args,
        ...rawArgsPlan.args,
        '--vimgrep',
        '--',
        pattern,
        normalizeRipgrepSearchPath(searchPath),
    ];
    try {
        let totalMatches = 0;
        const fileSet = new Set();
        const results = [];
        const run = await runRgLines(rgArgs, (line) => {
            const first = line.indexOf(':');
            if (first <= 0)
                return;
            const secondColon = line.indexOf(':', first + 1);
            if (secondColon <= first)
                return;
            const thirdColon = line.indexOf(':', secondColon + 1);
            if (thirdColon <= secondColon)
                return;
            const filePath = line.slice(0, first);
            const lineStr = line.slice(first + 1, secondColon);
            const colStr = line.slice(secondColon + 1, thirdColon);
            const text = line.slice(thirdColon + 1);
            const ln = Number.parseInt(lineStr, 10);
            const col = Number.parseInt(colStr, 10);
            if (!Number.isFinite(ln) || !Number.isFinite(col))
                return;
            if (!rawArgsPlan.postFilter(filePath))
                return;
            totalMatches++;
            fileSet.add(filePath);
            if (results.length < options.maxResults)
                results.push({ path: filePath, line: ln, col, match: text });
        });
        const diagnostics = [
            ...buildRipgrepDiagnostics(run, filterPlan, totalMatches > 0),
            ...rawArgsPlan.globCompatibilityWarnings,
        ];
        return formatRipgrepSnippetYaml({
            pattern,
            searchPath,
            globs: [],
            caseMode: 'smart',
            fixedStrings: false,
            maxResults: options.maxResults,
            fileCount: fileSet.size,
            totalMatches,
            partialPathFailure: run.partialPathFailure,
            diagnostics,
            results,
        });
    }
    catch (error) {
        return failYaml([
            `status: error`,
            `pattern: ${yamlQuote(pattern)}`,
            `mode: snippets`,
            `path: ${yamlQuote(searchPath)}`,
            `error: FAILED`,
            `summary: ${yamlQuote(error instanceof Error ? error.message : String(error))}`,
        ].join('\n'));
    }
}
exports.ripgrepSearchTool = {
    type: 'func',
    name: 'ripgrep_search',
    description: 'Escape hatch: run rg-style search (snippets output, with a limited allowlist). Output is normalized to YAML snippets mode.',
    descriptionI18n: {
        en: 'Escape hatch: run rg-style search (snippets output, with a limited allowlist). Output is normalized to YAML snippets mode.',
        zh: '逃生舱：运行 rg 风格搜索（片段输出，参数白名单）。输出归一为 YAML snippets 模式。',
    },
    parameters: {
        type: 'object',
        additionalProperties: false,
        properties: {
            pattern: { type: 'string' },
            path: { type: 'string' },
            rg_args: { type: 'array', items: { type: 'string' } },
        },
        required: ['pattern'],
    },
    argsValidation: 'dominds',
    call: async (_dlg, caller, args) => {
        const pattern = requireNonEmptyStringArg(args, 'pattern');
        const searchPath = optionalStringArg(args, 'path') ?? '.';
        const rgArgs = optionalStringArrayArg(args, 'rg_args') ?? [];
        return await runRipgrepSearch(caller, pattern, searchPath, rgArgs);
    },
};
async function runFsRipgrepSearch(pattern, searchPath, rawRgArgs) {
    for (const tok of rawRgArgs) {
        if (DISALLOWED_RG_ARGS.has(tok)) {
            return failYaml(`status: error\nerror: DISALLOWED_ARG\nsummary: ${yamlQuote(`Disallowed rg arg: ${tok}`)}`);
        }
        if (tok === '--json' ||
            tok === '--count' ||
            tok === '--count-matches' ||
            tok === '--files-with-matches' ||
            tok === '--files') {
            return failYaml(`status: error\nerror: DISALLOWED_ARG\nsummary: ${yamlQuote(`Disallowed rg output arg: ${tok}`)}`);
        }
    }
    const options = {
        ...defaultBaseOptions(),
        fixedStrings: false,
        maxResults: 50,
        contextBefore: 1,
        contextAfter: 1,
    };
    const filterPlan = fsBaseRgArgs(options);
    const rawArgsPlan = buildRawRipgrepArgsPlan(rawRgArgs);
    const rgArgs = [
        ...filterPlan.args,
        ...rawArgsPlan.args,
        '--vimgrep',
        '--',
        pattern,
        normalizeRipgrepSearchPath(searchPath),
    ];
    try {
        let totalMatches = 0;
        const fileSet = new Set();
        const results = [];
        const run = await runRgLines(rgArgs, (line) => {
            const first = line.indexOf(':');
            if (first <= 0)
                return;
            const secondColon = line.indexOf(':', first + 1);
            if (secondColon <= first)
                return;
            const thirdColon = line.indexOf(':', secondColon + 1);
            if (thirdColon <= secondColon)
                return;
            const filePath = line.slice(0, first);
            const lineStr = line.slice(first + 1, secondColon);
            const colStr = line.slice(secondColon + 1, thirdColon);
            const text = line.slice(thirdColon + 1);
            const ln = Number.parseInt(lineStr, 10);
            const col = Number.parseInt(colStr, 10);
            if (!Number.isFinite(ln) || !Number.isFinite(col))
                return;
            if (!rawArgsPlan.postFilter(filePath))
                return;
            totalMatches++;
            fileSet.add(filePath);
            if (results.length < options.maxResults)
                results.push({ path: filePath, line: ln, col, match: text });
        });
        const diagnostics = [
            ...buildRipgrepDiagnostics(run, filterPlan, totalMatches > 0),
            ...rawArgsPlan.globCompatibilityWarnings,
        ];
        return formatRipgrepSnippetYaml({
            pattern,
            searchPath,
            globs: [],
            caseMode: 'smart',
            fixedStrings: false,
            maxResults: options.maxResults,
            fileCount: fileSet.size,
            totalMatches,
            partialPathFailure: run.partialPathFailure,
            diagnostics,
            results,
        });
    }
    catch (error) {
        return failYaml([
            `status: error`,
            `pattern: ${yamlQuote(pattern)}`,
            `mode: fs_snippets`,
            `path: ${yamlQuote(searchPath)}`,
            `error: FAILED`,
            `summary: ${yamlQuote(error instanceof Error ? error.message : String(error))}`,
        ].join('\n'));
    }
}
exports.fsRipgrepSearchTool = {
    type: 'func',
    name: 'fs_ripgrep_search',
    description: 'Escape hatch: run rg-style local filesystem search without restricting paths to rtws. Output is normalized to YAML snippets mode.',
    descriptionI18n: {
        en: 'Escape hatch: run rg-style local filesystem search without restricting paths to rtws. Output is normalized to YAML snippets mode.',
        zh: '逃生舱：运行本机文件系统 rg 风格搜索，不限制路径必须位于 rtws 内。输出归一为 YAML snippets 模式。',
    },
    parameters: exports.ripgrepSearchTool.parameters,
    argsValidation: 'dominds',
    call: async (_dlg, _caller, args) => {
        const pattern = requireNonEmptyStringArg(args, 'pattern');
        const searchPath = optionalStringArg(args, 'path') ?? '.';
        const rgArgs = optionalStringArrayArg(args, 'rg_args') ?? [];
        return await runFsRipgrepSearch(pattern, searchPath, rgArgs);
    },
};
