4c4efc416a
The auto-download mechanism for ripgrep existed but was never called. When 'rg' wasn't in PATH, the grep tool silently fell back to GNU grep, which wastes ~10% token budget due to noisy results. Changes: 1. Wired up resolveGrepCliWithAutoInstall() in the CLI resolution path 2. When 'rg' is not found in PATH, auto-downloads ripgrep v14.1.1 3. Caches the downloaded binary in OpenCode data directory 4. Falls back to GNU grep only if auto-download fails (with warning) Fixes #3003
76 lines
3.0 KiB
TypeScript
76 lines
3.0 KiB
TypeScript
import { resolve } from "node:path"
|
|
import type { PluginInput } from "@opencode-ai/plugin"
|
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
|
import { runRg, runRgCount } from "./cli"
|
|
import { resolveGrepCliWithAutoInstall } from "./constants"
|
|
import { formatGrepResult, formatCountResult } from "./result-formatter"
|
|
|
|
export function createGrepTools(ctx: PluginInput): Record<string, ToolDefinition> {
|
|
const grep: ToolDefinition = tool({
|
|
description:
|
|
"Fast content search tool with safety limits (60s timeout, 256KB output). " +
|
|
"Searches file contents using regular expressions. " +
|
|
"Supports full regex syntax (eg. \"log.*Error\", \"function\\s+\\w+\", etc.). " +
|
|
"Filter files by pattern with the include parameter (eg. \"*.js\", \"*.{ts,tsx}\"). " +
|
|
"Output modes: \"content\" shows matching lines, \"files_with_matches\" shows only file paths (default), \"count\" shows match counts per file.",
|
|
args: {
|
|
pattern: tool.schema.string().describe("The regex pattern to search for in file contents"),
|
|
include: tool.schema
|
|
.string()
|
|
.optional()
|
|
.describe("File pattern to include in the search (e.g. \"*.js\", \"*.{ts,tsx}\")"),
|
|
path: tool.schema
|
|
.string()
|
|
.optional()
|
|
.describe("The directory to search in. Defaults to the current working directory."),
|
|
output_mode: tool.schema
|
|
.enum(["content", "files_with_matches", "count"])
|
|
.optional()
|
|
.describe(
|
|
"Output mode: \"content\" shows matching lines, \"files_with_matches\" shows only file paths (default), \"count\" shows match counts per file."
|
|
),
|
|
head_limit: tool.schema
|
|
.number()
|
|
.optional()
|
|
.describe("Limit output to first N entries. 0 or omitted means no limit."),
|
|
},
|
|
execute: async (args, context) => {
|
|
try {
|
|
const globs = args.include ? [args.include] : undefined
|
|
const runtimeCtx = context as Record<string, unknown>
|
|
const dir = typeof runtimeCtx.directory === "string" ? runtimeCtx.directory : ctx.directory
|
|
const searchPath = args.path ? resolve(dir, args.path) : dir
|
|
const paths = [searchPath]
|
|
const outputMode = args.output_mode ?? "files_with_matches"
|
|
const headLimit = args.head_limit ?? 0
|
|
const cli = await resolveGrepCliWithAutoInstall()
|
|
|
|
if (outputMode === "count") {
|
|
const results = await runRgCount({
|
|
pattern: args.pattern,
|
|
paths,
|
|
globs,
|
|
}, cli)
|
|
const limited = headLimit > 0 ? results.slice(0, headLimit) : results
|
|
return formatCountResult(limited)
|
|
}
|
|
|
|
const result = await runRg({
|
|
pattern: args.pattern,
|
|
paths,
|
|
globs,
|
|
context: 0,
|
|
outputMode,
|
|
headLimit,
|
|
}, cli)
|
|
|
|
return formatGrepResult(result)
|
|
} catch (e) {
|
|
return `Error: ${e instanceof Error ? e.message : String(e)}`
|
|
}
|
|
},
|
|
})
|
|
|
|
return { grep }
|
|
}
|