f993541e0b
This release has a bunch of minor breaking changes if you are using opencode plugins or sdk 1. storage events have been removed (we might bring this back but had some issues) 2. concept of `app` is gone - there is a new concept called `project` and endpoints to list projects and get the current project 3. plugin receives `directory` which is cwd and `worktree` which is where the root of the project is if it's a git repo 4. the session.chat function has been renamed to session.prompt in sdk. it no longer requires model to be passed in (model is now an object) 5. every endpoint takes an optional `directory` parameter to operate as though opencode is running in that directory
117 lines
3.1 KiB
TypeScript
117 lines
3.1 KiB
TypeScript
import { z } from "zod"
|
|
import { Tool } from "./tool"
|
|
import { Ripgrep } from "../file/ripgrep"
|
|
|
|
import DESCRIPTION from "./grep.txt"
|
|
import { Instance } from "../project/instance"
|
|
|
|
export const GrepTool = Tool.define("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 searchPath = params.path || Instance.directory
|
|
|
|
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 {
|
|
title: params.pattern,
|
|
metadata: { matches: 0, truncated: false },
|
|
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 [filePath, lineNumStr, ...lineTextParts] = line.split(":")
|
|
if (!filePath || !lineNumStr || lineTextParts.length === 0) continue
|
|
|
|
const lineNum = parseInt(lineNumStr, 10)
|
|
const lineText = lineTextParts.join(":")
|
|
|
|
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 {
|
|
title: params.pattern,
|
|
metadata: { matches: 0, truncated: false },
|
|
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 {
|
|
title: params.pattern,
|
|
metadata: {
|
|
matches: finalMatches.length,
|
|
truncated,
|
|
},
|
|
output: outputLines.join("\n"),
|
|
}
|
|
},
|
|
})
|