Dax Raad f6ed59bf45 Refactor external tools organization and add file search API endpoint
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-11 23:59:51 -04:00

132 lines
3.2 KiB
TypeScript

import { z } from "zod"
import { Tool } from "./tool"
import { App } from "../app/app"
import { Ripgrep } from "../external/ripgrep"
import DESCRIPTION from "./grep.txt"
export const GrepTool = Tool.define({
id: "opencode.grep",
description: DESCRIPTION,
parameters: z.object({
pattern: z
.string()
.describe("The regex pattern to search for in file contents"),
path: z
.string()
.nullable()
.describe(
"The directory to search in. Defaults to the current working directory.",
),
include: z
.string()
.nullable()
.describe(
'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
),
}),
async execute(params) {
if (!params.pattern) {
throw new Error("pattern is required")
}
const app = App.info()
const searchPath = params.path || app.path.cwd
const rgPath = await Ripgrep.filepath()
const args = ["-n", params.pattern]
if (params.include) {
args.push("--glob", params.include)
}
args.push(searchPath)
const proc = Bun.spawn([rgPath, ...args], {
stdout: "pipe",
stderr: "pipe",
})
const output = await new Response(proc.stdout).text()
const errorOutput = await new Response(proc.stderr).text()
const exitCode = await proc.exited
if (exitCode === 1) {
return {
metadata: { matches: 0, truncated: false, title: params.pattern },
output: "No files found",
}
}
if (exitCode !== 0) {
throw new Error(`ripgrep failed: ${errorOutput}`)
}
const lines = output.trim().split("\n")
const matches = []
for (const line of lines) {
if (!line) continue
const parts = line.split(":", 3)
if (parts.length < 3) continue
const filePath = parts[0]
const lineNum = parseInt(parts[1], 10)
const lineText = parts[2]
const file = Bun.file(filePath)
const stats = await file.stat().catch(() => null)
if (!stats) continue
matches.push({
path: filePath,
modTime: stats.mtime.getTime(),
lineNum,
lineText,
})
}
matches.sort((a, b) => b.modTime - a.modTime)
const limit = 100
const truncated = matches.length > limit
const finalMatches = truncated ? matches.slice(0, limit) : matches
if (finalMatches.length === 0) {
return {
metadata: { matches: 0, truncated: false, title: params.pattern },
output: "No files found",
}
}
const outputLines = [`Found ${finalMatches.length} matches`]
let currentFile = ""
for (const match of finalMatches) {
if (currentFile !== match.path) {
if (currentFile !== "") {
outputLines.push("")
}
currentFile = match.path
outputLines.push(`${match.path}:`)
}
outputLines.push(` Line ${match.lineNum}: ${match.lineText}`)
}
if (truncated) {
outputLines.push("")
outputLines.push(
"(Results are truncated. Consider using a more specific path or pattern.)",
)
}
return {
metadata: {
matches: finalMatches.length,
truncated,
title: params.pattern,
},
output: outputLines.join("\n"),
}
},
})