2025-06-04 17:12:13 +00:00
import path from "path"
2026-06-06 13:42:31 +00:00
import { Effect , Schema } from "effect"
2026-04-27 18:33:33 +00:00
import { InstanceState } from "@/effect/instance-state"
2026-06-02 20:09:26 +00:00
import { FSUtil } from "@opencode-ai/core/fs-util"
2026-06-10 00:38:02 +00:00
import { Ripgrep } from "@opencode-ai/core/ripgrep"
2026-04-10 17:56:42 +00:00
import { assertExternalDirectoryEffect } from "./external-directory"
2026-04-14 06:26:23 +00:00
import DESCRIPTION from "./glob.txt"
2026-04-16 03:56:54 +00:00
import * as Tool from "./tool"
2025-05-21 14:30:39 +00:00
2026-04-23 20:09:34 +00:00
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. ` ,
} ) ,
} )
2026-04-11 02:36:02 +00:00
export const GlobTool = Tool . define (
2026-04-10 17:56:42 +00:00
"glob" ,
Effect . gen ( function * ( ) {
2026-06-02 20:09:26 +00:00
const fs = yield * FSUtil . Service
2026-06-10 00:38:02 +00:00
const ripgrep = yield * Ripgrep . Service
2026-04-10 17:56:42 +00:00
return {
description : DESCRIPTION ,
2026-04-23 20:09:34 +00:00
parameters : Parameters ,
2026-04-10 17:56:42 +00:00
execute : ( params : { pattern : string ; path? : string } , ctx : Tool.Context ) = >
Effect . gen ( function * ( ) {
2026-04-14 06:26:23 +00:00
const ins = yield * InstanceState . context
2026-04-11 02:36:02 +00:00
yield * ctx . ask ( {
permission : "glob" ,
patterns : [ params . pattern ] ,
always : [ "*" ] ,
metadata : {
pattern : params.pattern ,
path : params.path ,
} ,
} )
2025-06-04 17:12:13 +00:00
2026-04-14 06:26:23 +00:00
let search = params . path ? ? ins . directory
search = path . isAbsolute ( search ) ? search : path.resolve ( ins . directory , search )
2026-04-13 23:26:50 +00:00
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 } ` )
}
2026-05-10 12:27:11 +00:00
yield * assertExternalDirectoryEffect ( ctx , search , {
2026-06-09 16:08:58 +00:00
bypass : false ,
2026-05-10 12:27:11 +00:00
kind : "directory" ,
} )
2025-05-21 14:30:39 +00:00
2026-04-10 17:56:42 +00:00
const limit = 100
2026-06-10 00:38:02 +00:00
const files = yield * ripgrep . glob ( { cwd : search , pattern : params.pattern , limit } )
const truncated = files . length === limit
2026-04-10 17:56:42 +00:00
const output = [ ]
2026-06-10 00:38:02 +00:00
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 ) {
2026-04-10 17:56:42 +00:00
output . push ( "" )
output . push (
` (Results are truncated: showing first ${ limit } results. Consider using a more specific path or pattern.) ` ,
)
}
}
return {
2026-04-14 06:26:23 +00:00
title : path.relative ( ins . worktree , search ) ,
2026-04-10 17:56:42 +00:00
metadata : {
2026-06-10 00:38:02 +00:00
count : files.length ,
truncated ,
2026-04-10 17:56:42 +00:00
} ,
output : output.join ( "\n" ) ,
}
2026-04-11 02:36:02 +00:00
} ) . pipe ( Effect . orDie ) ,
2025-05-31 18:41:00 +00:00
}
2026-04-10 17:56:42 +00:00
} ) ,
)