opencode/packages/core/src/tool/glob.ts

81 lines
3.2 KiB
TypeScript

export * as GlobTool from "./glob"
import { ToolFailure, toolText } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import { FileSystem } from "../filesystem"
import { LocationSearch } from "../location-search"
import { PermissionV2 } from "../permission"
import { Tool } from "./tool"
import { Tools } from "./tools"
export const name = "glob"
export const Input = Schema.Struct({
pattern: LocationSearch.FilesInput.fields.pattern.annotate({ description: "Glob pattern to match files against" }),
path: LocationSearch.FilesInput.fields.path.annotate({
description: "Relative directory to search. Defaults to the active Location.",
}),
limit: LocationSearch.FilesInput.fields.limit.annotate({
description: `Maximum results to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})`,
}),
})
type ModelOutput = typeof LocationSearch.FilesResult.Encoded
/** Format raw Location search results into the concise line-oriented output models expect. */
export const toModelOutput = (output: ModelOutput) => {
const lines = output.items.length === 0 ? ["No files found"] : output.items.map((item) => item.resource)
if (output.truncated) {
lines.push(
"",
`(Results are truncated: showing first ${output.items.length} results. Consider using a more specific path or pattern.)`,
)
}
if (output.partial) lines.push("", "(Results may be incomplete because some discovered files could not be read.)")
return lines.join("\n")
}
/**
* Location-scoped glob leaf. FileSystem supplies canonical permission metadata;
* LocationSearch resolves the current root and owns containment and traversal.
*/
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const filesystem = yield* FileSystem.Service
const search = yield* LocationSearch.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: LocationSearch.FilesResult,
toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })],
execute: (input, context) =>
Effect.gen(function* () {
const root = yield* filesystem.resolveRoot({ path: input.path })
yield* permission.assert({
action: name,
resources: [input.pattern],
save: ["*"],
metadata: {
root: root.resource,
path: input.path,
limit: input.limit,
},
sessionID: context.sessionID,
agent: context.agent,
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
})
return yield* search.files(input)
}).pipe(
Effect.mapError(() => new ToolFailure({ message: `Unable to find files matching ${input.pattern}` })),
),
}),
})
.pipe(Effect.orDie)
}),
)