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%
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