package tools import ( "context" "encoding/json" "fmt" "os" "path/filepath" "regexp" "sort" "strings" ) const maxGlobResults = 500 type globTool struct{ root string } func (t *globTool) Name() string { return "glob" } func (t *globTool) Description() string { return "Find workspace files matching a glob pattern (** crosses directories, * within one segment). Returns up to 500 relative paths." } func (t *globTool) Parameters() json.RawMessage { return json.RawMessage(`{"type":"object","required":["pattern"],"properties":{"pattern":{"type":"string","description":"glob such as **/*.go or docs/*.md"}}}`) } func (t *globTool) Execute(_ context.Context, args json.RawMessage) (Result, error) { rel, err := readArg(args, "pattern") if err != nil { return Result{}, err } if strings.TrimSpace(rel) == "" { return Result{Content: "pattern is empty", IsError: true}, nil } re, err := globRegexp(rel) if err != nil { return Result{Content: err.Error(), IsError: true}, nil } var matches []string capped := false err = filepath.WalkDir(t.root, func(path string, d os.DirEntry, walkErr error) error { if walkErr != nil { return nil } r, rerr := filepath.Rel(t.root, path) if rerr != nil { return nil } r = filepath.ToSlash(r) if r == "." { return nil } if d.IsDir() { // 目录命中以 / 结尾,供模型分辨文件与目录。 if re.MatchString(r + "/") { if len(matches) < maxGlobResults { matches = append(matches, r+"/") } else { capped = true } } return nil } if re.MatchString(r) { if len(matches) < maxGlobResults { matches = append(matches, r) } else { capped = true } } return nil }) if err != nil { return Result{Content: err.Error(), IsError: true}, nil } if len(matches) == 0 { return Result{Content: "no matches"}, nil } sort.Strings(matches) out := strings.Join(matches, "\n") if capped { out += fmt.Sprintf("\n(showing first %d matches)", maxGlobResults) } return Result{Content: out}, nil } // globRegexp 把受限 glob(** 跨目录、* 单段、? 单字符,其余按字面)编译为正则。 // 尾段单独的 ** 匹配其下全部内容(文件与目录);中段的 ** 匹配零或多层目录。 // 不支持字符类与分组,避免与正则语义混淆。 func globRegexp(pattern string) (*regexp.Regexp, error) { var b strings.Builder b.WriteString("^") segs := strings.Split(filepath.ToSlash(pattern), "/") prevStarStar := false for i, seg := range segs { // 前一段是独立的 **(非尾段)时,其 (?:[^/]+/)* 已含目录斜杠, // 零层目录的场景不需要再补一个分隔符。 if i > 0 && !prevStarStar { b.WriteString("/") } prevStarStar = false if seg == "**" { if i == len(segs)-1 { // 尾段 **:匹配其余全部路径(文件与目录)。 b.WriteString(".+") } else { b.WriteString("(?:[^/]+/)*") prevStarStar = true } continue } for _, part := range splitDoubleStar(seg) { if part == "**" { b.WriteString("[^/]*") continue } for _, ch := range part { switch ch { case '*': b.WriteString("[^/]*") case '?': b.WriteString("[^/]") default: b.WriteString(regexp.QuoteMeta(string(ch))) } } } } b.WriteString("$") return regexp.Compile(b.String()) } // splitDoubleStar 把段内混用的 ** 与字面部分切开(如 "a**b")。 func splitDoubleStar(seg string) []string { var out []string cur := strings.Builder{} for i := 0; i < len(seg); i++ { if i+1 < len(seg) && seg[i] == '*' && seg[i+1] == '*' { if cur.Len() > 0 { out = append(out, cur.String()) cur.Reset() } out = append(out, "**") i++ continue } cur.WriteByte(seg[i]) } if cur.Len() > 0 { out = append(out, cur.String()) } return out } // globMatcher 是文件名级 glob(filepath.Match 语义),供 grep include 过滤用。 type globMatcher struct{ pattern string } func newGlobMatcher(pattern string) (*globMatcher, error) { if _, err := filepath.Match(pattern, "probe"); err != nil { return nil, fmt.Errorf("invalid include pattern %q: %w", pattern, err) } return &globMatcher{pattern: pattern}, nil } func (m *globMatcher) Match(name string) bool { ok, _ := filepath.Match(m.pattern, name) return ok }