97 lines
3.4 KiB
TypeScript
97 lines
3.4 KiB
TypeScript
import z from "zod"
|
|
import path from "path"
|
|
import { Effect, Option } from "effect"
|
|
import * as Stream from "effect/Stream"
|
|
import { Tool } from "./tool"
|
|
import DESCRIPTION from "./glob.txt"
|
|
import { Ripgrep } from "../file/ripgrep"
|
|
import { Instance } from "../project/instance"
|
|
import { assertExternalDirectoryEffect } from "./external-directory"
|
|
import { AppFileSystem } from "../filesystem"
|
|
|
|
export const GlobTool = Tool.defineEffect(
|
|
"glob",
|
|
Effect.gen(function* () {
|
|
const rg = yield* Ripgrep.Service
|
|
const fs = yield* AppFileSystem.Service
|
|
|
|
return {
|
|
description: DESCRIPTION,
|
|
parameters: z.object({
|
|
pattern: z.string().describe("The glob pattern to match files against"),
|
|
path: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
`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.`,
|
|
),
|
|
}),
|
|
execute: (params: { pattern: string; path?: string }, ctx: Tool.Context) =>
|
|
Effect.gen(function* () {
|
|
yield* Effect.promise(() =>
|
|
ctx.ask({
|
|
permission: "glob",
|
|
patterns: [params.pattern],
|
|
always: ["*"],
|
|
metadata: {
|
|
pattern: params.pattern,
|
|
path: params.path,
|
|
},
|
|
}),
|
|
)
|
|
|
|
let search = params.path ?? Instance.directory
|
|
search = path.isAbsolute(search) ? search : path.resolve(Instance.directory, search)
|
|
yield* assertExternalDirectoryEffect(ctx, search, { kind: "directory" })
|
|
|
|
const limit = 100
|
|
let truncated = false
|
|
const files = yield* rg.files({ cwd: search, glob: [params.pattern] }).pipe(
|
|
Stream.mapEffect((file) =>
|
|
Effect.gen(function* () {
|
|
const full = path.resolve(search, file)
|
|
const info = yield* fs.stat(full).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
const mtime =
|
|
info?.mtime.pipe(
|
|
Option.map((d) => d.getTime()),
|
|
Option.getOrElse(() => 0),
|
|
) ?? 0
|
|
return { path: full, mtime }
|
|
}),
|
|
),
|
|
Stream.take(limit + 1),
|
|
Stream.runCollect,
|
|
Effect.map((chunk) => [...chunk]),
|
|
)
|
|
|
|
if (files.length > limit) {
|
|
truncated = true
|
|
files.length = limit
|
|
}
|
|
files.sort((a, b) => b.mtime - a.mtime)
|
|
|
|
const output = []
|
|
if (files.length === 0) output.push("No files found")
|
|
if (files.length > 0) {
|
|
output.push(...files.map((f) => f.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(Instance.worktree, search),
|
|
metadata: {
|
|
count: files.length,
|
|
truncated,
|
|
},
|
|
output: output.join("\n"),
|
|
}
|
|
}).pipe(Effect.orDie, Effect.runPromise),
|
|
}
|
|
}),
|
|
)
|