forked from anomalyco/opencode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgrep.ts
More file actions
131 lines (110 loc) · 3.19 KB
/
grep.ts
File metadata and controls
131 lines (110 loc) · 3.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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"),
}
},
})