| export * as GlobTool from "./glob" |
|
|
| import { ToolFailure } from "@opencode-ai/llm" |
| import { Effect, Layer, Schema } from "effect" |
| import path from "path" |
| import { makeLocationNode } from "../effect/app-node" |
| import { FileSystem } from "../filesystem" |
| import { Location } from "../location" |
| import { Ripgrep } from "../ripgrep" |
| import { RelativePath } from "../schema" |
| import { PermissionV2 } from "../permission" |
| import { ToolRegistry } from "./registry" |
| import { Tool } from "./tool" |
| import { Tools } from "./tools" |
|
|
| export const name = "glob" |
|
|
| export const Input = Schema.Struct({ |
| pattern: FileSystem.GlobInput.fields.pattern.annotate({ description: "Glob pattern to match files against" }), |
| path: RelativePath.pipe(Schema.optional).annotate({ |
| description: "Relative directory to search. Defaults to the active Location.", |
| }), |
| limit: FileSystem.GlobInput.fields.limit.annotate({ |
| description: "Maximum results to return", |
| }), |
| }) |
|
|
| export const Output = Schema.Array(FileSystem.Entry) |
| type ModelOutput = typeof Output.Encoded |
|
|
| |
| export const toModelOutput = (output: ModelOutput) => { |
| const lines = output.length === 0 ? ["No files found"] : output.map((item) => item.path) |
| return lines.join("\n") |
| } |
|
|
| |
| const layer = Layer.effectDiscard( |
| Effect.gen(function* () { |
| const tools = yield* Tools.Service |
| const ripgrep = yield* Ripgrep.Service |
| const location = yield* Location.Service |
| const permission = yield* PermissionV2.Service |
|
|
| yield* tools |
| .register({ |
| [name]: Tool.make({ |
| description: |
| "Find files by glob pattern within the active Location. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.", |
| input: Input, |
| output: Output, |
| toModelOutput: ({ output }) => [ |
| { |
| type: "text", |
| text: toModelOutput( |
| output.map((entry) => ({ ...entry, path: path.resolve(location.directory, entry.path) })), |
| ), |
| }, |
| ], |
| execute: (input, context) => |
| Effect.gen(function* () { |
| yield* permission.assert({ |
| action: name, |
| resources: [input.pattern], |
| save: ["*"], |
| metadata: { |
| root: input.path ?? ".", |
| path: input.path, |
| limit: input.limit, |
| }, |
| sessionID: context.sessionID, |
| agent: context.agent, |
| source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID }, |
| }) |
| const cwd = path.resolve(location.directory, input.path ?? ".") |
| return yield* ripgrep |
| .glob({ |
| cwd, |
| pattern: input.pattern, |
| limit: input.limit ?? Number.MAX_SAFE_INTEGER, |
| }) |
| .pipe( |
| Effect.map((result) => |
| result.map((entry) => |
| FileSystem.Entry.make({ |
| ...entry, |
| path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, entry.path))), |
| }), |
| ), |
| ), |
| ) |
| }).pipe( |
| Effect.mapError(() => new ToolFailure({ message: `Unable to find files matching ${input.pattern}` })), |
| ), |
| }), |
| }) |
| .pipe(Effect.orDie) |
| }), |
| ) |
|
|
| export const node = makeLocationNode({ |
| name: "tool/glob", |
| layer, |
| deps: [ToolRegistry.node, Ripgrep.node, Location.node, PermissionV2.node], |
| }) |
|
|