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

139
src/tools/read/execute.ts Normal file
View File

@@ -0,0 +1,139 @@
/**
* Read tool execution
*/
import fs from "fs/promises";
import path from "path";
import {
READ_DEFAULTS,
READ_MESSAGES,
READ_TITLES,
READ_DESCRIPTION,
} from "@constants/read";
import { isFileOpAllowed, promptFilePermission } from "@services/permissions";
import { readParams } from "@tools/read/params";
import { processLines } from "@tools/read/format";
import type {
ToolDefinition,
ToolContext,
ToolResult,
ReadParams,
} from "@/types/tools";
const createDeniedResult = (filePath: string): ToolResult => ({
success: false,
title: READ_TITLES.DENIED(filePath),
output: "",
error: READ_MESSAGES.PERMISSION_DENIED,
});
const createErrorResult = (filePath: string, error: Error): ToolResult => ({
success: false,
title: READ_TITLES.FAILED(filePath),
output: "",
error: error.message,
});
const createDirectoryResult = (
filePath: string,
files: string[],
): ToolResult => ({
success: true,
title: READ_TITLES.DIRECTORY(filePath),
output: "Directory contents:\n" + files.join("\n"),
metadata: {
isDirectory: true,
fileCount: files.length,
},
});
const createFileResult = (
filePath: string,
fullPath: string,
output: string,
totalLines: number,
linesRead: number,
truncated: boolean,
offset: number,
): ToolResult => ({
success: true,
title: path.basename(filePath),
output,
metadata: {
filepath: fullPath,
totalLines,
linesRead,
truncated,
offset,
},
});
const resolvePath = (filePath: string, workingDir: string): string =>
path.isAbsolute(filePath) ? filePath : path.join(workingDir, filePath);
const checkPermission = async (
fullPath: string,
autoApprove: boolean,
): Promise<boolean> => {
if (autoApprove || isFileOpAllowed("Read", fullPath)) {
return true;
}
const { allowed } = await promptFilePermission("Read", fullPath);
return allowed;
};
const readDirectory = async (fullPath: string): Promise<string[]> =>
fs.readdir(fullPath);
const readFileContent = async (fullPath: string): Promise<string> =>
fs.readFile(fullPath, "utf-8");
export const executeRead = async (
args: ReadParams,
ctx: ToolContext,
): Promise<ToolResult> => {
const { filePath, offset = 0, limit = READ_DEFAULTS.MAX_LINES } = args;
const fullPath = resolvePath(filePath, ctx.workingDir);
const allowed = await checkPermission(fullPath, ctx.autoApprove ?? false);
if (!allowed) return createDeniedResult(filePath);
ctx.onMetadata?.({
title: READ_TITLES.READING(path.basename(filePath)),
status: "running",
});
try {
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
const files = await readDirectory(fullPath);
return createDirectoryResult(filePath, files);
}
const content = await readFileContent(fullPath);
const lines = content.split("\n");
const { output, truncated } = processLines(lines, offset, limit);
return createFileResult(
filePath,
fullPath,
output.join("\n"),
lines.length,
output.length,
truncated,
offset,
);
} catch (error) {
return createErrorResult(filePath, error as Error);
}
};
export const readTool: ToolDefinition<typeof readParams> = {
name: "read",
description: READ_DESCRIPTION,
parameters: readParams,
execute: executeRead,
};

45
src/tools/read/format.ts Normal file
View File

@@ -0,0 +1,45 @@
/**
* Read tool formatting utilities
*/
import { READ_DEFAULTS } from "@constants/read";
export const truncateLine = (line: string): string =>
line.length > READ_DEFAULTS.MAX_LINE_LENGTH
? line.substring(0, READ_DEFAULTS.MAX_LINE_LENGTH) + "..."
: line;
export const formatLineWithNumber = (
line: string,
lineNumber: number,
): string =>
String(lineNumber).padStart(READ_DEFAULTS.LINE_NUMBER_PAD, " ") + "\t" + line;
export const calculateLineSize = (line: string): number =>
Buffer.byteLength(line, "utf-8") + 1;
export const processLines = (
lines: string[],
offset: number,
limit: number,
): { output: string[]; truncated: boolean } => {
const result: string[] = [];
let bytes = 0;
const maxLine = Math.min(lines.length, offset + limit);
for (let i = offset; i < maxLine; i++) {
const truncatedLine = truncateLine(lines[i]);
const lineWithNumber = formatLineWithNumber(truncatedLine, i + 1);
const size = calculateLineSize(lineWithNumber);
if (bytes + size > READ_DEFAULTS.MAX_BYTES) break;
result.push(lineWithNumber);
bytes += size;
}
return {
output: result,
truncated: result.length < lines.length - offset,
};
};

23
src/tools/read/params.ts Normal file
View File

@@ -0,0 +1,23 @@
/**
* Read tool parameter schema
*/
import { z } from "zod";
import { READ_DEFAULTS } from "@constants/read";
export const readParams = z.object({
filePath: z.string().describe("The absolute path to the file to read"),
offset: z
.number()
.optional()
.describe("Line number to start reading from (0-indexed)"),
limit: z
.number()
.optional()
.describe(
`Maximum number of lines to read (default: ${READ_DEFAULTS.MAX_LINES})`,
),
});
export type ReadParamsSchema = typeof readParams;