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:
139
src/tools/read/execute.ts
Normal file
139
src/tools/read/execute.ts
Normal 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
45
src/tools/read/format.ts
Normal 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
23
src/tools/read/params.ts
Normal 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;
|
||||
Reference in New Issue
Block a user