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:
2026-01-27 23:33:06 -05:00
commit 0062e5d9d9
521 changed files with 66418 additions and 0 deletions

View 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
View 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
View 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");