publish / publish (push) Has been cancelled
all old sessions and share links. we'll be more backwards compatible in the future once we're more stable.
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: "grep",
|
|
description: DESCRIPTION,
|
|
parameters: z.object({
|
|
pattern: z
|
|
.string()
|
|
.describe("The regex pattern to search for in file contents"),
|
|
path: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"The directory to search in. Defaults to the current working directory.",
|
|
),
|
|
include: z
|
|
.string()
|
|
.optional()
|
|
.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"),
|
|
}
|
|
},
|
|
})
|