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
156 lines (140 loc) · 5.68 KB
/
grep.ts
File metadata and controls
156 lines (140 loc) · 5.68 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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import path from "path"
import { Schema } from "effect"
import { Effect, Option } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Ripgrep } from "../file/ripgrep"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./grep.txt"
import * as Tool from "./tool"
import { Reference } from "@/reference/reference"
const MAX_LINE_LENGTH = 2000
export const Parameters = Schema.Struct({
pattern: Schema.String.annotate({ description: "The regex pattern to search for in file contents" }),
path: Schema.optional(Schema.String).annotate({
description: "The directory to search in. Defaults to the current working directory.",
}),
include: Schema.optional(Schema.String).annotate({
description: 'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
}),
})
export const GrepTool = Tool.define(
"grep",
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const rg = yield* Ripgrep.Service
const reference = yield* Reference.Service
return {
description: DESCRIPTION,
parameters: Parameters,
execute: (params: { pattern: string; path?: string; include?: string }, ctx: Tool.Context) =>
Effect.gen(function* () {
const empty = {
title: params.pattern,
metadata: { matches: 0, truncated: false },
output: "No files found",
}
if (!params.pattern) {
throw new Error("pattern is required")
}
yield* ctx.ask({
permission: "grep",
patterns: [params.pattern],
always: ["*"],
metadata: {
pattern: params.pattern,
path: params.path,
include: params.include,
},
})
const ins = yield* InstanceState.context
const requested = path.isAbsolute(params.path ?? ins.directory)
? (params.path ?? ins.directory)
: path.join(ins.directory, params.path ?? ".")
yield* reference.ensure(requested)
const requestedInfo = yield* fs.stat(requested).pipe(Effect.catch(() => Effect.succeed(undefined)))
yield* assertExternalDirectoryEffect(ctx, requested, {
bypass: yield* reference.contains(requested),
kind: requestedInfo?.type === "Directory" ? "directory" : "file",
})
const search = AppFileSystem.resolve(requested)
const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined)))
const cwd = info?.type === "Directory" ? search : path.dirname(search)
const file = info?.type === "Directory" ? undefined : [path.relative(cwd, search)]
const result = yield* rg.search({
cwd,
pattern: params.pattern,
glob: params.include ? [params.include] : undefined,
file,
signal: ctx.abort,
})
if (result.items.length === 0) return empty
const rows = result.items.map((item) => ({
path: AppFileSystem.resolve(
path.isAbsolute(item.path.text) ? item.path.text : path.join(cwd, item.path.text),
),
line: item.line_number,
text: item.lines.text,
}))
const times = new Map(
(yield* Effect.forEach(
[...new Set(rows.map((row) => row.path))],
Effect.fnUntraced(function* (file) {
const info = yield* fs.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!info || info.type === "Directory") return undefined
return [
file,
info.mtime.pipe(
Option.map((time) => time.getTime()),
Option.getOrElse(() => 0),
) ?? 0,
] as const
}),
{ concurrency: 16 },
)).filter((entry): entry is readonly [string, number] => Boolean(entry)),
)
const matches = rows.flatMap((row) => {
const mtime = times.get(row.path)
if (mtime === undefined) return []
return [{ ...row, mtime }]
})
matches.sort((a, b) => b.mtime - a.mtime)
const limit = 100
const truncated = matches.length > limit
const final = truncated ? matches.slice(0, limit) : matches
if (final.length === 0) return empty
const total = matches.length
const output = [`Found ${total} matches${truncated ? ` (showing first ${limit})` : ""}`]
let current = ""
for (const match of final) {
if (current !== match.path) {
if (current !== "") output.push("")
current = match.path
output.push(`${match.path}:`)
}
const text =
match.text.length > MAX_LINE_LENGTH ? match.text.substring(0, MAX_LINE_LENGTH) + "..." : match.text
output.push(` Line ${match.line}: ${text}`)
}
if (truncated) {
output.push("")
output.push(
`(Results truncated: showing ${limit} of ${total} matches (${total - limit} hidden). Consider using a more specific path or pattern.)`,
)
}
if (result.partial) {
output.push("")
output.push("(Some paths were inaccessible and skipped)")
}
return {
title: params.pattern,
metadata: {
matches: total,
truncated,
},
output: output.join("\n"),
}
}).pipe(Effect.orDie),
}
}),
)