UNPKG

@deep-assistant/agent

Version:

A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.

117 lines (95 loc) 3.25 kB
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 = ["-nH", "--field-match-separator=|", "--regexp", 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"), } }, })