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
112 lines (101 loc) · 3.87 KB
/
Copy pathgrep.ts
File metadata and controls
112 lines (101 loc) · 3.87 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
import path from "path"
import { Effect, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./grep.txt"
import * as Tool from "./tool"
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* FSUtil.Service
const ripgrep = yield* Ripgrep.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 ?? ".")
const requestedInfo = yield* fs.stat(requested).pipe(Effect.catch(() => Effect.succeed(undefined)))
yield* assertExternalDirectoryEffect(ctx, requested, {
bypass: false,
kind: requestedInfo?.type === "Directory" ? "directory" : "file",
})
const search = FSUtil.resolve(requested)
const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined)))
const cwd = info?.type === "Directory" ? search : path.dirname(search)
const result = yield* ripgrep.grep({
cwd,
pattern: params.pattern,
include: params.include,
limit: 100,
})
if (result.length === 0) return empty
const rows = result.map((item) => ({
path: path.resolve(cwd, item.entry.path),
line: item.line,
text: item.text,
}))
const limit = 100
const truncated = rows.length === limit
const final = rows
if (final.length === 0) return empty
const total = rows.length
const hasMore = truncated || result.length === limit
const output = [`Found ${total} matches${hasMore ? " (more matches available)" : ""}`]
let current = ""
for (const match of final) {
if (current !== match.path) {
if (current !== "") output.push("")
current = match.path
output.push(`${match.path}:`)
}
output.push(` Line ${match.line}: ${match.text}`)
}
if (truncated) {
output.push("")
output.push("(Results truncated. Consider using a more specific path or pattern.)")
}
return {
title: params.pattern,
metadata: {
matches: total,
truncated,
},
output: output.join("\n"),
}
}).pipe(Effect.orDie),
}
}),
)