mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-30 13:54:01 +00:00
🤖 Generated with [OpenCode](https://opencode.ai) Co-Authored-By: OpenCode <noreply@opencode.ai>
132 lines
3.2 KiB
TypeScript
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"),
|
|
}
|
|
},
|
|
})
|