| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137 |
- export * as GrepTool from "./grep"
- import { ToolFailure } from "@kirincode-ai/llm"
- import { Effect, Layer, Schema } from "effect"
- import path from "path"
- import { makeLocationNode } from "../effect/app-node"
- import { FileSystem } from "../filesystem"
- import { FSUtil } from "../fs-util"
- import { Location } from "../location"
- import { PermissionV2 } from "../permission"
- import { Ripgrep } from "../ripgrep"
- import { RelativePath } from "../schema"
- import { ToolRegistry } from "./registry"
- import { Tool } from "./tool"
- import { Tools } from "./tools"
- export const name = "grep"
- export const Input = Schema.Struct({
- pattern: FileSystem.GrepInput.fields.pattern.annotate({
- description: "Regex pattern to search for in file contents",
- }),
- path: RelativePath.pipe(Schema.optional).annotate({
- description: "Relative directory to search. Defaults to the active Location.",
- }),
- include: FileSystem.GrepInput.fields.include.annotate({
- description: 'File glob to include in the search (for example, "*.js" or "*.{ts,tsx}")',
- }),
- limit: FileSystem.GrepInput.fields.limit.annotate({
- description: "Maximum matches to return",
- }),
- })
- export const Output = Schema.Array(FileSystem.Match)
- type ModelOutput = typeof Output.Encoded
- /** Format raw search matches into the familiar concise model output. */
- export const toModelOutput = (output: ModelOutput) => {
- const lines = output.length === 0 ? ["No files found"] : [`Found ${output.length} matches`]
- let current = ""
- for (const match of output) {
- if (current !== match.entry.path) {
- if (current) lines.push("")
- current = match.entry.path
- lines.push(`${match.entry.path}:`)
- }
- lines.push(` Line ${match.line}: ${match.text}`)
- }
- return lines.join("\n")
- }
- /** Grep leaf that defaults its filesystem root to the active Location. */
- const layer = Layer.effectDiscard(
- Effect.gen(function* () {
- const tools = yield* Tools.Service
- const fs = yield* FSUtil.Service
- const ripgrep = yield* Ripgrep.Service
- const location = yield* Location.Service
- const permission = yield* PermissionV2.Service
- yield* tools
- .register({
- [name]: Tool.make({
- description:
- "Search file contents by regular expression within the active Location or an absolute managed tool-output file. Use a path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise file resources, line numbers, and bounded line previews.",
- input: Input,
- output: Output,
- toModelOutput: ({ output }) => [
- {
- type: "text",
- text: toModelOutput(
- output.map((match) => ({
- ...match,
- entry: { ...match.entry, path: path.resolve(location.directory, match.entry.path) },
- })),
- ),
- },
- ],
- execute: (input, context) =>
- Effect.gen(function* () {
- yield* permission.assert({
- action: name,
- resources: [input.pattern],
- save: ["*"],
- metadata: {
- root: ".",
- path: input.path,
- include: input.include,
- limit: input.limit,
- },
- sessionID: context.sessionID,
- agent: context.agent,
- source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
- })
- const target = path.resolve(location.directory, input.path ?? ".")
- const info = yield* fs.stat(target).pipe(Effect.catch(() => Effect.succeed(undefined)))
- return yield* ripgrep
- .grep({
- cwd: info?.type === "Directory" ? target : path.dirname(target),
- pattern: input.pattern,
- file: info?.type === "File" ? path.basename(target) : undefined,
- include: input.include,
- limit: input.limit ?? Number.MAX_SAFE_INTEGER,
- })
- .pipe(
- Effect.map((result) =>
- result.map((match) =>
- FileSystem.Match.make({
- ...match,
- entry: FileSystem.Entry.make({
- ...match.entry,
- path: RelativePath.make(
- path.relative(
- location.directory,
- path.resolve(
- info?.type === "Directory" ? target : path.dirname(target),
- match.entry.path,
- ),
- ),
- ),
- }),
- }),
- ),
- ),
- )
- }).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to grep for ${input.pattern}` }))),
- }),
- })
- .pipe(Effect.orDie)
- }),
- )
- export const node = makeLocationNode({
- name: "tool/grep",
- layer,
- deps: [ToolRegistry.node, FSUtil.node, Ripgrep.node, Location.node, PermissionV2.node],
- })
|