Terminal-based AI coding agent with interactive TUI for autonomous code generation.
Features: - Interactive TUI with React/Ink - Autonomous agent with tool calls (bash, read, write, edit, glob, grep) - Permission system with pattern-based rules - Session management with auto-compaction - Dual providers: GitHub Copilot and Ollama - MCP server integration - Todo panel and theme system - Streaming responses - GitHub-compatible project context
This commit is contained in:
58
src/tools/grep/definition.ts
Normal file
58
src/tools/grep/definition.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Grep Tool Definition - Search file contents
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { executeRipgrep } from "@tools/grep/execute";
|
||||
import type { ToolDefinition, ToolContext, ToolResult } from "@/types/tools";
|
||||
|
||||
export const grepParams = z.object({
|
||||
pattern: z
|
||||
.string()
|
||||
.describe("The regular expression pattern to search for in file contents"),
|
||||
path: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("File or directory to search in. Defaults to current working directory."),
|
||||
glob: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Glob pattern to filter files (e.g., '*.ts', '**/*.tsx')"),
|
||||
case_insensitive: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Case insensitive search"),
|
||||
context_lines: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Number of context lines to show before and after each match"),
|
||||
});
|
||||
|
||||
type GrepParams = z.infer<typeof grepParams>;
|
||||
|
||||
const executeGrepTool = async (
|
||||
args: GrepParams,
|
||||
ctx: ToolContext,
|
||||
): Promise<ToolResult> => {
|
||||
// Execute ripgrep search
|
||||
const directory = args.path || ctx.workingDir;
|
||||
const result = await executeRipgrep(args.pattern, directory);
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
title: result.title,
|
||||
output: result.output,
|
||||
error: result.error,
|
||||
};
|
||||
};
|
||||
|
||||
export const grepToolDefinition: ToolDefinition<typeof grepParams> = {
|
||||
name: "grep",
|
||||
description: `A powerful search tool built on ripgrep for searching file contents.
|
||||
- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
|
||||
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx")
|
||||
- Use this when you need to find code patterns, function definitions, or specific text
|
||||
- For finding files by name, use glob instead`,
|
||||
parameters: grepParams,
|
||||
execute: executeGrepTool,
|
||||
};
|
||||
117
src/tools/grep/execute.ts
Normal file
117
src/tools/grep/execute.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Grep tool execution (functional)
|
||||
*/
|
||||
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import fg from "fast-glob";
|
||||
import fs from "fs/promises";
|
||||
|
||||
import {
|
||||
GREP_DEFAULTS,
|
||||
GREP_IGNORE_PATTERNS,
|
||||
GREP_MESSAGES,
|
||||
GREP_COMMANDS,
|
||||
} from "@constants/grep";
|
||||
import { searchLines, formatMatches } from "@tools/grep/search";
|
||||
import type { GrepMatch, GrepOptions, GrepResult } from "@/types/tools";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const createSuccessResult = (output: string, files: string[]): GrepResult => ({
|
||||
success: true,
|
||||
title: "Grep",
|
||||
output: output || GREP_MESSAGES.NO_MATCHES,
|
||||
files,
|
||||
});
|
||||
|
||||
const createErrorResult = (error: unknown): GrepResult => ({
|
||||
success: false,
|
||||
title: "Grep failed",
|
||||
output: "",
|
||||
error: GREP_MESSAGES.SEARCH_FAILED(error),
|
||||
});
|
||||
|
||||
const searchFile = async (
|
||||
file: string,
|
||||
pattern: string,
|
||||
options?: GrepOptions,
|
||||
): Promise<GrepMatch[]> => {
|
||||
try {
|
||||
const content = await fs.readFile(file, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
return searchLines(lines, pattern, file, options);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const executeGrep = async (
|
||||
pattern: string,
|
||||
files: string[] = [GREP_DEFAULTS.DEFAULT_PATTERN],
|
||||
options?: GrepOptions,
|
||||
): Promise<GrepResult> => {
|
||||
try {
|
||||
const fileList = await fg(files, {
|
||||
ignore: [...GREP_IGNORE_PATTERNS],
|
||||
});
|
||||
|
||||
const matches: GrepMatch[] = [];
|
||||
const maxResults = options?.maxResults ?? GREP_DEFAULTS.MAX_RESULTS;
|
||||
|
||||
for (const file of fileList) {
|
||||
if (matches.length >= maxResults) break;
|
||||
|
||||
const fileMatches = await searchFile(file, pattern, options);
|
||||
const remaining = maxResults - matches.length;
|
||||
matches.push(...fileMatches.slice(0, remaining));
|
||||
}
|
||||
|
||||
const output = formatMatches(matches);
|
||||
const uniqueFiles = [...new Set(matches.map((m) => m.file))];
|
||||
|
||||
return createSuccessResult(output, uniqueFiles);
|
||||
} catch (error) {
|
||||
return createErrorResult(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const searchInFile = async (
|
||||
filePath: string,
|
||||
pattern: string,
|
||||
options?: GrepOptions,
|
||||
): Promise<GrepMatch[]> => searchFile(filePath, pattern, options);
|
||||
|
||||
export const executeRipgrep = async (
|
||||
pattern: string,
|
||||
directory: string = ".",
|
||||
): Promise<GrepResult> => {
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
GREP_COMMANDS.RIPGREP(pattern, directory),
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
title: "Ripgrep",
|
||||
output: stdout || GREP_MESSAGES.NO_MATCHES,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const execError = error as { code?: number; message?: string };
|
||||
|
||||
if (execError.code === GREP_DEFAULTS.NO_MATCHES_EXIT_CODE) {
|
||||
return {
|
||||
success: true,
|
||||
title: "Ripgrep",
|
||||
output: GREP_MESSAGES.NO_MATCHES,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
title: "Ripgrep failed",
|
||||
output: "",
|
||||
error: GREP_MESSAGES.RIPGREP_FAILED(execError.message ?? "Unknown error"),
|
||||
};
|
||||
}
|
||||
};
|
||||
52
src/tools/grep/search.ts
Normal file
52
src/tools/grep/search.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Grep search utilities
|
||||
*/
|
||||
|
||||
import type { GrepMatch, GrepOptions } from "@/types/tools";
|
||||
|
||||
const normalizeForSearch = (text: string, ignoreCase: boolean): string =>
|
||||
ignoreCase ? text.toLowerCase() : text;
|
||||
|
||||
const matchesPattern = (
|
||||
line: string,
|
||||
pattern: string,
|
||||
options?: GrepOptions,
|
||||
): boolean => {
|
||||
if (options?.regex) {
|
||||
const regex = new RegExp(pattern, options.ignoreCase ? "i" : "");
|
||||
return regex.test(line);
|
||||
}
|
||||
|
||||
const searchPattern = normalizeForSearch(
|
||||
pattern,
|
||||
options?.ignoreCase ?? false,
|
||||
);
|
||||
const searchLine = normalizeForSearch(line, options?.ignoreCase ?? false);
|
||||
return searchLine.includes(searchPattern);
|
||||
};
|
||||
|
||||
export const searchLines = (
|
||||
lines: string[],
|
||||
pattern: string,
|
||||
file: string,
|
||||
options?: GrepOptions,
|
||||
): GrepMatch[] => {
|
||||
const matches: GrepMatch[] = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
if (matchesPattern(line, pattern, options)) {
|
||||
matches.push({
|
||||
file,
|
||||
line: i + 1,
|
||||
content: line.trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
};
|
||||
|
||||
export const formatMatches = (matches: GrepMatch[]): string =>
|
||||
matches.map((m) => `${m.file}:${m.line}: ${m.content}`).join("\n");
|
||||
Reference in New Issue
Block a user