SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
4.8 KB · 148 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: src/tools/glob.ts4 * Description: The glob tool — file discovery by name pattern, mtime-ordered, ignore-aware (TOOL_PROTOCOL §6).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import * as path from "node:path";11import { globToRegExp, walkFiles } from "../workspace/walk.js";12import { resolveToolPath } from "./format.js";13import type { ToolDefinition } from "./registry.js";14import type { ToolContext, ToolResult } from "./types.js";1516export interface GlobInput {17  pattern: string;18  path?: string;19}2021const DESCRIPTION =22  'Find files by name pattern, e.g. "**/*.ts" or "src/**/config.*". Returns matching file paths ' +23  "ordered by most recently modified, at most 100. Respects .gitignore and .khaelorignore. Use this " +24  "to discover file layout; use grep to search file contents.";2526const MAX_RESULTS = 100;27const BUILTIN_IGNORES = ["node_modules", ".git", "dist"];2829async function executeGlob(input: GlobInput, ctx: ToolContext): Promise<ToolResult> {30  const startedAt = Date.now();31  const cwd = ctx.workspace.cwd();32  const root = input.path !== undefined ? resolveToolPath(cwd, input.path) : cwd;3334  let regex: RegExp;35  try {36    regex = globToRegExp(input.pattern);37  } catch {38    return {39      content: `Invalid glob pattern "${input.pattern}". Use patterns like "**/*.ts" or "src/**/config.*".`,40      isError: true,41      metadata: { title: `Glob ${input.pattern} · invalid`, durationMs: Date.now() - startedAt },42    };43  }4445  // Built-in noise dirs stay ignored unless the pattern explicitly targets them (§6.2).46  const builtinIgnores = BUILTIN_IGNORES.filter((dir) => !input.pattern.includes(dir));47  const files = await walkFiles(root, { builtinIgnores, signal: ctx.signal });4849  const matched = files.filter((f) =>50    regex.test(path.relative(root, f.path).split(path.sep).join("/")),51  );52  matched.sort((a, b) => b.mtimeMs - a.mtimeMs);53  const relPaths = matched.map((f) => path.relative(root, f.path).split(path.sep).join("/"));5455  if (relPaths.length === 0) {56    // Nearest-extension hint when the pattern names a literal extension (§6.3).57    let hint = "";58    const extMatch = /\.([A-Za-z0-9]+)$/.exec(input.pattern);59    if (extMatch !== null) {60      const stem = input.pattern.slice(0, -((extMatch[1] as string).length));61      const stemRegex = ((): RegExp | undefined => {62        try {63          return globToRegExp(`${stem}*`);64        } catch {65          return undefined;66        }67      })();68      if (stemRegex !== undefined) {69        const counts = new Map<string, number>();70        for (const f of files) {71          const rel = path.relative(root, f.path).split(path.sep).join("/");72          if (!stemRegex.test(rel)) continue;73          const ext = path.extname(rel);74          if (ext.length === 0) continue;75          counts.set(ext, (counts.get(ext) ?? 0) + 1);76        }77        const best = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];78        if (best !== undefined) {79          hint = ` Nearest existing extension: ${best[0]} (${best[1]} ${best[1] === 1 ? "file" : "files"}).`;80        }81      }82    }83    return {84      content: `No files match "${input.pattern}" under ${root}.${hint}`,85      metadata: {86        title: `Glob ${input.pattern} · 0 files`,87        files: 0,88        durationMs: Date.now() - startedAt,89      },90    };91  }9293  const total = relPaths.length;94  const shown = relPaths.slice(0, MAX_RESULTS);95  const lines = [96    `${total} ${total === 1 ? "file matches" : "files match"} "${input.pattern}" (newest first):`,97    ...shown,98  ];99100  let truncation;101  if (total > MAX_RESULTS) {102    const spillPath = await ctx.spill("glob", relPaths.join("\n"));103    lines.push(`[Showing ${MAX_RESULTS} of ${total}. Full list: ${spillPath} — or narrow the pattern.]`);104    truncation = {105      originalBytes: Buffer.byteLength(relPaths.join("\n"), "utf8"),106      originalLines: total,107      shownHeadLines: MAX_RESULTS,108      shownTailLines: 0,109      omittedLines: total - MAX_RESULTS,110      spillPath,111    };112  }113114  return {115    content: lines.join("\n"),116    metadata: {117      title: `Glob ${input.pattern} · ${total} ${total === 1 ? "file" : "files"}`,118      files: total,119      durationMs: Date.now() - startedAt,120      ...(truncation !== undefined ? { truncation } : {}),121    },122  };123}124125/** Create the glob tool definition. */126export function createGlobTool(): ToolDefinition<GlobInput> {127  return {128    name: "glob",129    description: DESCRIPTION,130    capability: "file.read",131    inputSchema: {132      type: "object",133      properties: {134        pattern: {135          type: "string",136          description: "Glob pattern to match file paths against.",137        },138        path: {139          type: "string",140          description: "Directory to search in. Defaults to the working directory.",141        },142      },143      required: ["pattern"],144    },145    execute: executeGlob,146  };147}148