113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
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),
|
|
}
|
|
}),
|
|
)
|