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 "./glob.txt" import * as Tool from "./tool" export const Parameters = Schema.Struct({ pattern: Schema.String.annotate({ description: "The glob pattern to match files against" }), path: Schema.optional(Schema.String).annotate({ description: `The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`, }), }) export const GlobTool = Tool.define( "glob", Effect.gen(function* () { const fs = yield* FSUtil.Service const ripgrep = yield* Ripgrep.Service return { description: DESCRIPTION, parameters: Parameters, execute: (params: { pattern: string; path?: string }, ctx: Tool.Context) => Effect.gen(function* () { const ins = yield* InstanceState.context yield* ctx.ask({ permission: "glob", patterns: [params.pattern], always: ["*"], metadata: { pattern: params.pattern, path: params.path, }, }) let search = params.path ?? ins.directory search = path.isAbsolute(search) ? search : path.resolve(ins.directory, search) const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined))) if (info?.type === "File") { throw new Error(`glob path must be a directory: ${search}`) } yield* assertExternalDirectoryEffect(ctx, search, { bypass: false, kind: "directory", }) const limit = 100 const files = yield* ripgrep.glob({ cwd: search, pattern: params.pattern, limit }) const truncated = files.length === limit const output = [] if (files.length === 0) output.push("No files found") if (files.length > 0) { output.push(...files.map((file) => path.resolve(search, file.path))) if (truncated) { output.push("") output.push( `(Results are truncated: showing first ${limit} results. Consider using a more specific path or pattern.)`, ) } } return { title: path.relative(ins.worktree, search), metadata: { count: files.length, truncated, }, output: output.join("\n"), } }).pipe(Effect.orDie), } }), )