Improve readability issues

This is only the first change of many changes.

* Remove redundant autogenerated comments
* Use the recommended file name style
* Use camelCase for variable names
* Don't introduce submodules for relevant types
* Don't introduce constants like modules, these are implementation details
* Remove empty files
This commit is contained in:
Jaana Dogan 2025-04-17 12:03:02 -07:00 committed by N. Taylor Mullen
parent 898a83031c
commit 81ba61df7f
25 changed files with 186 additions and 240 deletions

View File

@ -1,22 +0,0 @@
import { ToolCallEvent } from "../ui/types.js";
export enum GeminiEventType {
Content,
ToolCallInfo,
}
export interface GeminiContentEvent {
type: GeminiEventType.Content;
value: string;
}
export interface GeminiToolCallInfoEvent {
type: GeminiEventType.ToolCallInfo;
value: ToolCallEvent;
}
export type GeminiEvent =
| GeminiContentEvent
| GeminiToolCallInfoEvent;
export type GeminiStream = AsyncIterable<GeminiEvent>;

View File

@ -1,4 +0,0 @@
export enum StreamingState {
Idle,
Responding,
}

View File

@ -1 +0,0 @@
export const MEMORY_FILE_NAME = 'GEMINI.md';

View File

@ -10,9 +10,9 @@ import { CoreSystemPrompt } from './prompts.js';
import { type ToolCallEvent, type ToolCallConfirmationDetails, ToolCallStatus } from '../ui/types.js'; import { type ToolCallEvent, type ToolCallConfirmationDetails, ToolCallStatus } from '../ui/types.js';
import process from 'node:process'; import process from 'node:process';
import { toolRegistry } from '../tools/tool-registry.js'; import { toolRegistry } from '../tools/tool-registry.js';
import { ToolResult } from '../tools/ToolResult.js'; import { ToolResult } from '../tools/tool.js';
import { getFolderStructure } from '../utils/getFolderStructure.js'; import { getFolderStructure } from '../utils/getFolderStructure.js';
import { GeminiEventType, GeminiStream } from './GeminiStream.js'; import { GeminiEventType, GeminiStream } from './gemini-stream.js';
type ToolExecutionOutcome = { type ToolExecutionOutcome = {
callId: string; callId: string;
@ -62,7 +62,7 @@ ${folderStructure}
try { try {
const chat = this.ai.chats.create({ const chat = this.ai.chats.create({
model: 'gemini-2.5-pro-preview-03-25',//'gemini-2.0-flash', model: 'gemini-2.0-flash',//'gemini-2.0-flash',
config: { config: {
systemInstruction: CoreSystemPrompt, systemInstruction: CoreSystemPrompt,
...this.defaultHyperParameters, ...this.defaultHyperParameters,

View File

@ -1,7 +1,33 @@
import { ToolCallEvent } from "../ui/types.js";
import { Part } from '@google/genai'; import { Part } from '@google/genai';
import { HistoryItem } from '../ui/types.js'; import { HistoryItem } from '../ui/types.js';
import { GeminiEventType, GeminiStream } from './GeminiStream.js'; import { handleToolCallChunk, addErrorMessageToHistory } from './history-updater.js';
import { handleToolCallChunk, addErrorMessageToHistory } from './historyUpdater.js';
export enum GeminiEventType {
Content,
ToolCallInfo,
}
export interface GeminiContentEvent {
type: GeminiEventType.Content;
value: string;
}
export interface GeminiToolCallInfoEvent {
type: GeminiEventType.ToolCallInfo;
value: ToolCallEvent;
}
export type GeminiEvent =
| GeminiContentEvent
| GeminiToolCallInfoEvent;
export type GeminiStream = AsyncIterable<GeminiEvent>;
export enum StreamingState {
Idle,
Responding,
}
interface StreamProcessorParams { interface StreamProcessorParams {
stream: GeminiStream; stream: GeminiStream;
@ -104,7 +130,6 @@ export const processGeminiStream = async ({ // Renamed function for clarity
clearTimeout(renderTimeoutId); clearTimeout(renderTimeoutId);
renderTimeoutId = null; renderTimeoutId = null;
} }
// Flush any text buffer content. // Flush any text buffer content.
render(textBuffer); render(textBuffer);
currentGeminiMessageId = null; // End text message context currentGeminiMessageId = null; // End text message context

View File

@ -1,7 +1,7 @@
import { Part } from "@google/genai"; import { Part } from "@google/genai";
import { toolRegistry } from "../tools/tool-registry.js"; import { toolRegistry } from "../tools/tool-registry.js";
import { HistoryItem, IndividualToolCallDisplay, ToolCallEvent, ToolCallStatus, ToolConfirmationOutcome, ToolEditConfirmationDetails, ToolExecuteConfirmationDetails } from "../ui/types.js"; import { HistoryItem, IndividualToolCallDisplay, ToolCallEvent, ToolCallStatus, ToolConfirmationOutcome, ToolEditConfirmationDetails, ToolExecuteConfirmationDetails } from "../ui/types.js";
import { ToolResultDisplay } from "../tools/ToolResult.js"; import { ToolResultDisplay } from "../tools/tool.js";
/** /**
* Processes a tool call chunk and updates the history state accordingly. * Processes a tool call chunk and updates the history state accordingly.
@ -46,13 +46,9 @@ export const handleToolCallChunk = (
if (!tool) { if (!tool) {
throw new Error(`Tool "${chunk.name}" not found or is not registered.`); throw new Error(`Tool "${chunk.name}" not found or is not registered.`);
} }
handleToolCallChunk({ ...chunk, status: ToolCallStatus.Invoked, resultDisplay: "Executing...", confirmationDetails: undefined }, setHistory, submitQuery, getNextMessageId, currentToolGroupIdRef); handleToolCallChunk({ ...chunk, status: ToolCallStatus.Invoked, resultDisplay: "Executing...", confirmationDetails: undefined }, setHistory, submitQuery, getNextMessageId, currentToolGroupIdRef);
const result = await tool.execute(chunk.args); const result = await tool.execute(chunk.args);
handleToolCallChunk({ ...chunk, status: ToolCallStatus.Invoked, resultDisplay: result.returnDisplay, confirmationDetails: undefined }, setHistory, submitQuery, getNextMessageId, currentToolGroupIdRef); handleToolCallChunk({ ...chunk, status: ToolCallStatus.Invoked, resultDisplay: result.returnDisplay, confirmationDetails: undefined }, setHistory, submitQuery, getNextMessageId, currentToolGroupIdRef);
const functionResponse: Part = { const functionResponse: Part = {
functionResponse: { functionResponse: {
name: chunk.name, name: chunk.name,
@ -60,7 +56,6 @@ export const handleToolCallChunk = (
response: { "output": result.llmContent }, response: { "output": result.llmContent },
}, },
} }
await submitQuery(functionResponse); await submitQuery(functionResponse);
} }
} }

View File

@ -1,6 +1,7 @@
import { ReadFileTool } from "../tools/read-file.tool.js"; import { ReadFileTool } from "../tools/read-file.tool.js";
import { TerminalTool } from "../tools/terminal.tool.js"; import { TerminalTool } from "../tools/terminal.tool.js";
import { MEMORY_FILE_NAME } from "./constants.js";
const MEMORY_FILE_NAME = 'GEMINI.md';
const contactEmail = 'ntaylormullen@google.com'; const contactEmail = 'ntaylormullen@google.com';
export const CoreSystemPrompt = ` export const CoreSystemPrompt = `

View File

@ -1,73 +0,0 @@
import type { FunctionDeclaration, Schema } from '@google/genai';
import { ToolResult } from './ToolResult.js';
import { Tool } from './Tool.js';
import { ToolCallConfirmationDetails } from '../ui/types.js';
/**
* Base implementation for tools with common functionality
*/
export abstract class BaseTool<TParams = unknown, TResult extends ToolResult = ToolResult> implements Tool<TParams, TResult> {
/**
* Creates a new instance of BaseTool
* @param name Internal name of the tool (used for API calls)
* @param displayName User-friendly display name of the tool
* @param description Description of what the tool does
* @param parameterSchema JSON Schema defining the parameters
*/
constructor(
public readonly name: string,
public readonly displayName: string,
public readonly description: string,
public readonly parameterSchema: Record<string, unknown>
) {}
/**
* Function declaration schema computed from name, description, and parameterSchema
*/
get schema(): FunctionDeclaration {
return {
name: this.name,
description: this.description,
parameters: this.parameterSchema as Schema
};
}
/**
* Validates the parameters for the tool
* This is a placeholder implementation and should be overridden
* @param params Parameters to validate
* @returns An error message string if invalid, null otherwise
*/
invalidParams(params: TParams): string | null {
// Implementation would typically use a JSON Schema validator
// This is a placeholder that should be implemented by derived classes
return null;
}
/**
* Gets a pre-execution description of the tool operation
* Default implementation that should be overridden by derived classes
* @param params Parameters for the tool execution
* @returns A markdown string describing what the tool will do
*/
getDescription(params: TParams): string {
return JSON.stringify(params);
}
/**
* Determines if the tool should prompt for confirmation before execution
* @param params Parameters for the tool execution
* @returns Whether or not execute should be confirmed by the user.
*/
shouldConfirmExecute(params: TParams): Promise<ToolCallConfirmationDetails | false> {
return Promise.resolve(false);
}
/**
* Abstract method to execute the tool with the given parameters
* Must be implemented by derived classes
* @param params Parameters for the tool execution
* @returns Result of the tool execution
*/
abstract execute(params: TParams): Promise<TResult>;
}

View File

@ -1,5 +1,4 @@
import { FunctionDeclaration } from "@google/genai"; import { FunctionDeclaration, Schema } from "@google/genai";
import { ToolResult } from "./ToolResult.js";
import { ToolCallConfirmationDetails } from "../ui/types.js"; import { ToolCallConfirmationDetails } from "../ui/types.js";
/** /**
@ -25,14 +24,14 @@ export interface Tool<TParams = unknown, TResult extends ToolResult = ToolResult
* Function declaration schema from @google/genai * Function declaration schema from @google/genai
*/ */
schema: FunctionDeclaration; schema: FunctionDeclaration;
/** /**
* Validates the parameters for the tool * Validates the parameters for the tool
* @param params Parameters to validate * @param params Parameters to validate
* @returns An error message string if invalid, null otherwise * @returns An error message string if invalid, null otherwise
*/ */
invalidParams(params: TParams): string | null; invalidParams(params: TParams): string | null;
/** /**
* Gets a pre-execution description of the tool operation * Gets a pre-execution description of the tool operation
* @param params Parameters for the tool execution * @param params Parameters for the tool execution
@ -47,7 +46,7 @@ export interface Tool<TParams = unknown, TResult extends ToolResult = ToolResult
* @returns Whether execute should be confirmed. * @returns Whether execute should be confirmed.
*/ */
shouldConfirmExecute(params: TParams): Promise<ToolCallConfirmationDetails | false>; shouldConfirmExecute(params: TParams): Promise<ToolCallConfirmationDetails | false>;
/** /**
* Executes the tool with the given parameters * Executes the tool with the given parameters
* @param params Parameters for the tool execution * @param params Parameters for the tool execution
@ -55,3 +54,94 @@ export interface Tool<TParams = unknown, TResult extends ToolResult = ToolResult
*/ */
execute(params: TParams): Promise<TResult>; execute(params: TParams): Promise<TResult>;
} }
/**
* Base implementation for tools with common functionality
*/
export abstract class BaseTool<TParams = unknown, TResult extends ToolResult = ToolResult> implements Tool<TParams, TResult> {
/**
* Creates a new instance of BaseTool
* @param name Internal name of the tool (used for API calls)
* @param displayName User-friendly display name of the tool
* @param description Description of what the tool does
* @param parameterSchema JSON Schema defining the parameters
*/
constructor(
public readonly name: string,
public readonly displayName: string,
public readonly description: string,
public readonly parameterSchema: Record<string, unknown>
) {}
/**
* Function declaration schema computed from name, description, and parameterSchema
*/
get schema(): FunctionDeclaration {
return {
name: this.name,
description: this.description,
parameters: this.parameterSchema as Schema
};
}
/**
* Validates the parameters for the tool
* This is a placeholder implementation and should be overridden
* @param params Parameters to validate
* @returns An error message string if invalid, null otherwise
*/
invalidParams(params: TParams): string | null {
// Implementation would typically use a JSON Schema validator
// This is a placeholder that should be implemented by derived classes
return null;
}
/**
* Gets a pre-execution description of the tool operation
* Default implementation that should be overridden by derived classes
* @param params Parameters for the tool execution
* @returns A markdown string describing what the tool will do
*/
getDescription(params: TParams): string {
return JSON.stringify(params);
}
/**
* Determines if the tool should prompt for confirmation before execution
* @param params Parameters for the tool execution
* @returns Whether or not execute should be confirmed by the user.
*/
shouldConfirmExecute(params: TParams): Promise<ToolCallConfirmationDetails | false> {
return Promise.resolve(false);
}
/**
* Abstract method to execute the tool with the given parameters
* Must be implemented by derived classes
* @param params Parameters for the tool execution
* @returns Result of the tool execution
*/
abstract execute(params: TParams): Promise<TResult>;
}
export interface ToolResult {
/**
* Content meant to be included in LLM history.
* This should represent the factual outcome of the tool execution.
*/
llmContent: string;
/**
* Markdown string for user display.
* This provides a user-friendly summary or visualization of the result.
*/
returnDisplay: ToolResultDisplay;
}
export type ToolResultDisplay = string | FileDiff;
export interface FileDiff {
fileDiff: string
}

View File

@ -1,22 +0,0 @@
/**
* Standard tool result interface that all tools should implement
*/
export interface ToolResult {
/**
* Content meant to be included in LLM history.
* This should represent the factual outcome of the tool execution.
*/
llmContent: string;
/**
* Markdown string for user display.
* This provides a user-friendly summary or visualization of the result.
*/
returnDisplay: ToolResultDisplay;
}
export type ToolResultDisplay = string | FileDiff;
export interface FileDiff {
fileDiff: string
}

View File

@ -2,8 +2,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import * as Diff from 'diff'; import * as Diff from 'diff';
import { SchemaValidator } from '../utils/schemaValidator.js'; import { SchemaValidator } from '../utils/schemaValidator.js';
import { ToolResult } from './ToolResult.js'; import { BaseTool, ToolResult } from './tool.js';
import { BaseTool } from './BaseTool.js';
import { ToolCallConfirmationDetails, ToolConfirmationOutcome, ToolEditConfirmationDetails } from '../ui/types.js'; import { ToolCallConfirmationDetails, ToolConfirmationOutcome, ToolEditConfirmationDetails } from '../ui/types.js';
import { makeRelative, shortenPath } from '../utils/paths.js'; import { makeRelative, shortenPath } from '../utils/paths.js';
import { ReadFileTool } from './read-file.tool.js'; import { ReadFileTool } from './read-file.tool.js';
@ -67,7 +66,7 @@ export class EditTool extends BaseTool<EditToolParams, EditToolResult> {
`Replaces a SINGLE, UNIQUE occurrence of text within a file. Requires providing significant context around the change to ensure uniqueness. For moving/renaming files, use the Bash tool with \`mv\`. For replacing entire file contents or creating new files use the ${WriteFileTool.Name} tool. Always use the ${ReadFileTool.Name} tool to examine the file before using this tool.`, `Replaces a SINGLE, UNIQUE occurrence of text within a file. Requires providing significant context around the change to ensure uniqueness. For moving/renaming files, use the Bash tool with \`mv\`. For replacing entire file contents or creating new files use the ${WriteFileTool.Name} tool. Always use the ${ReadFileTool.Name} tool to examine the file before using this tool.`,
{ {
properties: { properties: {
file_path: { filePath: {
description: 'The absolute path to the file to modify. Must start with /. When creating a new file, ensure the parent directory exists (use the `LS` tool to verify).', description: 'The absolute path to the file to modify. Must start with /. When creating a new file, ensure the parent directory exists (use the `LS` tool to verify).',
type: 'string' type: 'string'
}, },

View File

@ -2,8 +2,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import fg from 'fast-glob'; import fg from 'fast-glob';
import { SchemaValidator } from '../utils/schemaValidator.js'; import { SchemaValidator } from '../utils/schemaValidator.js';
import { BaseTool } from './BaseTool.js'; import { BaseTool, ToolResult } from './tool.js';
import { ToolResult } from './ToolResult.js';
import { shortenPath, makeRelative } from '../utils/paths.js'; import { shortenPath, makeRelative } from '../utils/paths.js';
/** /**

View File

@ -4,8 +4,7 @@ import path from 'path';
import { EOL } from 'os'; // Used for parsing grep output lines import { EOL } from 'os'; // Used for parsing grep output lines
import { spawn } from 'child_process'; // Used for git grep and system grep import { spawn } from 'child_process'; // Used for git grep and system grep
import fastGlob from 'fast-glob'; // Used for JS fallback file searching import fastGlob from 'fast-glob'; // Used for JS fallback file searching
import { ToolResult } from './ToolResult.js'; import { BaseTool, ToolResult } from './tool.js';
import { BaseTool } from './BaseTool.js';
import { SchemaValidator } from '../utils/schemaValidator.js'; import { SchemaValidator } from '../utils/schemaValidator.js';
import { makeRelative, shortenPath } from '../utils/paths.js'; import { makeRelative, shortenPath } from '../utils/paths.js';

View File

@ -1,8 +1,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { BaseTool } from './BaseTool.js'; import { BaseTool, ToolResult } from './tool.js';
import { SchemaValidator } from '../utils/schemaValidator.js'; import { SchemaValidator } from '../utils/schemaValidator.js';
import { ToolResult } from './ToolResult.js';
import { makeRelative, shortenPath } from '../utils/paths.js'; import { makeRelative, shortenPath } from '../utils/paths.js';
/** /**
@ -13,7 +12,7 @@ export interface LSToolParams {
* The absolute path to the directory to list * The absolute path to the directory to list
*/ */
path: string; path: string;
/** /**
* List of glob patterns to ignore * List of glob patterns to ignore
*/ */
@ -28,22 +27,22 @@ export interface FileEntry {
* Name of the file or directory * Name of the file or directory
*/ */
name: string; name: string;
/** /**
* Absolute path to the file or directory * Absolute path to the file or directory
*/ */
path: string; path: string;
/** /**
* Whether this entry is a directory * Whether this entry is a directory
*/ */
isDirectory: boolean; isDirectory: boolean;
/** /**
* Size of the file in bytes (0 for directories) * Size of the file in bytes (0 for directories)
*/ */
size: number; size: number;
/** /**
* Last modified timestamp * Last modified timestamp
*/ */
@ -58,12 +57,12 @@ export interface LSToolResult extends ToolResult {
* List of file entries * List of file entries
*/ */
entries: FileEntry[]; entries: FileEntry[];
/** /**
* The directory that was listed * The directory that was listed
*/ */
listedPath: string; listedPath: string;
/** /**
* Total number of entries found * Total number of entries found
*/ */
@ -120,15 +119,13 @@ export class LSTool extends BaseTool<LSToolParams, LSToolResult> {
private isWithinRoot(pathToCheck: string): boolean { private isWithinRoot(pathToCheck: string): boolean {
const normalizedPath = path.normalize(pathToCheck); const normalizedPath = path.normalize(pathToCheck);
const normalizedRoot = path.normalize(this.rootDirectory); const normalizedRoot = path.normalize(this.rootDirectory);
// Ensure the normalizedRoot ends with a path separator for proper path comparison // Ensure the normalizedRoot ends with a path separator for proper path comparison
const rootWithSep = normalizedRoot.endsWith(path.sep) const rootWithSep = normalizedRoot.endsWith(path.sep)
? normalizedRoot ? normalizedRoot
: normalizedRoot + path.sep; : normalizedRoot + path.sep;
return normalizedPath === normalizedRoot || normalizedPath.startsWith(rootWithSep); return normalizedPath === normalizedRoot || normalizedPath.startsWith(rootWithSep);
} }
/** /**
* Validates the parameters for the tool * Validates the parameters for the tool
* @param params Parameters to validate * @param params Parameters to validate
@ -138,7 +135,6 @@ export class LSTool extends BaseTool<LSToolParams, LSToolResult> {
if (this.schema.parameters && !SchemaValidator.validate(this.schema.parameters as Record<string, unknown>, params)) { if (this.schema.parameters && !SchemaValidator.validate(this.schema.parameters as Record<string, unknown>, params)) {
return "Parameters failed schema validation."; return "Parameters failed schema validation.";
} }
// Ensure path is absolute // Ensure path is absolute
if (!path.isAbsolute(params.path)) { if (!path.isAbsolute(params.path)) {
return `Path must be absolute: ${params.path}`; return `Path must be absolute: ${params.path}`;
@ -148,10 +144,9 @@ export class LSTool extends BaseTool<LSToolParams, LSToolResult> {
if (!this.isWithinRoot(params.path)) { if (!this.isWithinRoot(params.path)) {
return `Path must be within the root directory (${this.rootDirectory}): ${params.path}`; return `Path must be within the root directory (${this.rootDirectory}): ${params.path}`;
} }
return null; return null;
} }
/** /**
* Checks if a filename matches any of the ignore patterns * Checks if a filename matches any of the ignore patterns
* @param filename Filename to check * @param filename Filename to check
@ -162,26 +157,22 @@ export class LSTool extends BaseTool<LSToolParams, LSToolResult> {
if (!patterns || patterns.length === 0) { if (!patterns || patterns.length === 0) {
return false; return false;
} }
for (const pattern of patterns) { for (const pattern of patterns) {
// Convert glob pattern to RegExp // Convert glob pattern to RegExp
const regexPattern = pattern const regexPattern = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&') .replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*') .replace(/\*/g, '.*')
.replace(/\?/g, '.'); .replace(/\?/g, '.');
const regex = new RegExp(`^${regexPattern}$`); const regex = new RegExp(`^${regexPattern}$`);
if (regex.test(filename)) { if (regex.test(filename)) {
return true; return true;
} }
} }
return false; return false;
} }
/** /**
* Gets a description of the file reading operation * Gets a description of the file reading operation
* @param params Parameters for the file reading * @param params Parameters for the file reading
* @returns A string describing the file being read * @returns A string describing the file being read
*/ */
@ -189,7 +180,7 @@ export class LSTool extends BaseTool<LSToolParams, LSToolResult> {
const relativePath = makeRelative(params.path, this.rootDirectory); const relativePath = makeRelative(params.path, this.rootDirectory);
return shortenPath(relativePath); return shortenPath(relativePath);
} }
/** /**
* Executes the LS operation with the given parameters * Executes the LS operation with the given parameters
* @param params Parameters for the LS operation * @param params Parameters for the LS operation
@ -206,7 +197,7 @@ export class LSTool extends BaseTool<LSToolParams, LSToolResult> {
returnDisplay: "**Error:** Failed to execute tool." returnDisplay: "**Error:** Failed to execute tool."
}; };
} }
try { try {
// Check if path exists // Check if path exists
if (!fs.existsSync(params.path)) { if (!fs.existsSync(params.path)) {
@ -218,7 +209,6 @@ export class LSTool extends BaseTool<LSToolParams, LSToolResult> {
returnDisplay: `Directory does not exist` returnDisplay: `Directory does not exist`
}; };
} }
// Check if path is a directory // Check if path is a directory
const stats = fs.statSync(params.path); const stats = fs.statSync(params.path);
if (!stats.isDirectory()) { if (!stats.isDirectory()) {
@ -230,11 +220,10 @@ export class LSTool extends BaseTool<LSToolParams, LSToolResult> {
returnDisplay: `Path is not a directory` returnDisplay: `Path is not a directory`
}; };
} }
// Read directory contents // Read directory contents
const files = fs.readdirSync(params.path); const files = fs.readdirSync(params.path);
const entries: FileEntry[] = []; const entries: FileEntry[] = [];
if (files.length === 0) { if (files.length === 0) {
return { return {
entries: [], entries: [],
@ -244,20 +233,16 @@ export class LSTool extends BaseTool<LSToolParams, LSToolResult> {
returnDisplay: `Directory is empty.` returnDisplay: `Directory is empty.`
}; };
} }
// Process each entry
for (const file of files) { for (const file of files) {
// Skip if the file matches ignore patterns
if (this.shouldIgnore(file, params.ignore)) { if (this.shouldIgnore(file, params.ignore)) {
continue; continue;
} }
const fullPath = path.join(params.path, file); const fullPath = path.join(params.path, file);
try { try {
const stats = fs.statSync(fullPath); const stats = fs.statSync(fullPath);
const isDir = stats.isDirectory(); const isDir = stats.isDirectory();
entries.push({ entries.push({
name: file, name: file,
path: fullPath, path: fullPath,
@ -270,21 +255,21 @@ export class LSTool extends BaseTool<LSToolParams, LSToolResult> {
console.error(`Error accessing ${fullPath}: ${error}`); console.error(`Error accessing ${fullPath}: ${error}`);
} }
} }
// Sort entries (directories first, then alphabetically) // Sort entries (directories first, then alphabetically)
entries.sort((a, b) => { entries.sort((a, b) => {
if (a.isDirectory && !b.isDirectory) return -1; if (a.isDirectory && !b.isDirectory) return -1;
if (!a.isDirectory && b.isDirectory) return 1; if (!a.isDirectory && b.isDirectory) return 1;
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}); });
// Create formatted content for display // Create formatted content for display
const directoryContent = entries.map(entry => { const directoryContent = entries.map(entry => {
const typeIndicator = entry.isDirectory ? 'd' : '-'; const typeIndicator = entry.isDirectory ? 'd' : '-';
const sizeInfo = entry.isDirectory ? '' : ` (${entry.size} bytes)`; const sizeInfo = entry.isDirectory ? '' : ` (${entry.size} bytes)`;
return `${typeIndicator} ${entry.name}${sizeInfo}`; return `${typeIndicator} ${entry.name}${sizeInfo}`;
}).join('\n'); }).join('\n');
return { return {
entries, entries,
listedPath: params.path, listedPath: params.path,

View File

@ -1,9 +1,8 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { ToolResult } from './ToolResult.js';
import { BaseTool } from './BaseTool.js';
import { SchemaValidator } from '../utils/schemaValidator.js'; import { SchemaValidator } from '../utils/schemaValidator.js';
import { makeRelative, shortenPath } from '../utils/paths.js'; import { makeRelative, shortenPath } from '../utils/paths.js';
import { BaseTool, ToolResult } from './tool.js';
/** /**
* Parameters for the ReadFile tool * Parameters for the ReadFile tool
@ -36,7 +35,7 @@ export interface ReadFileToolResult extends ToolResult {
*/ */
export class ReadFileTool extends BaseTool<ReadFileToolParams, ReadFileToolResult> { export class ReadFileTool extends BaseTool<ReadFileToolParams, ReadFileToolResult> {
public static readonly Name: string = 'read_file'; public static readonly Name: string = 'read_file';
// Maximum number of lines to read by default // Maximum number of lines to read by default
private static readonly DEFAULT_MAX_LINES = 2000; private static readonly DEFAULT_MAX_LINES = 2000;
@ -108,26 +107,19 @@ export class ReadFileTool extends BaseTool<ReadFileToolParams, ReadFileToolResul
if (this.schema.parameters && !SchemaValidator.validate(this.schema.parameters as Record<string, unknown>, params)) { if (this.schema.parameters && !SchemaValidator.validate(this.schema.parameters as Record<string, unknown>, params)) {
return "Parameters failed schema validation."; return "Parameters failed schema validation.";
} }
const filePath = params.file_path;
// Ensure path is absolute if (!path.isAbsolute(filePath)) {
if (!path.isAbsolute(params.file_path)) { return `File path must be absolute: ${filePath}`;
return `File path must be absolute: ${params.file_path}`;
} }
if (!this.isWithinRoot(filePath)) {
// Ensure path is within the root directory return `File path must be within the root directory (${this.rootDirectory}): ${filePath}`;
if (!this.isWithinRoot(params.file_path)) {
return `File path must be within the root directory (${this.rootDirectory}): ${params.file_path}`;
} }
// Validate offset and limit if provided
if (params.offset !== undefined && params.offset < 0) { if (params.offset !== undefined && params.offset < 0) {
return 'Offset must be a non-negative number'; return 'Offset must be a non-negative number';
} }
if (params.limit !== undefined && params.limit <= 0) { if (params.limit !== undefined && params.limit <= 0) {
return 'Limit must be a positive number'; return 'Limit must be a positive number';
} }
return null; return null;
} }
@ -208,6 +200,7 @@ export class ReadFileTool extends BaseTool<ReadFileToolParams, ReadFileToolResul
*/ */
async execute(params: ReadFileToolParams): Promise<ReadFileToolResult> { async execute(params: ReadFileToolParams): Promise<ReadFileToolResult> {
const validationError = this.invalidParams(params); const validationError = this.invalidParams(params);
const filePath = params.file_path;
if (validationError) { if (validationError) {
return { return {
llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`, llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`,
@ -216,51 +209,40 @@ export class ReadFileTool extends BaseTool<ReadFileToolParams, ReadFileToolResul
} }
try { try {
// Check if file exists if (!fs.existsSync(filePath)) {
if (!fs.existsSync(params.file_path)) {
return { return {
llmContent: `File not found: ${params.file_path}`, llmContent: `File not found: ${filePath}`,
returnDisplay: `File not found.`, returnDisplay: `File not found.`,
}; };
} }
// Check if it's a directory const stats = fs.statSync(filePath);
const stats = fs.statSync(params.file_path);
if (stats.isDirectory()) { if (stats.isDirectory()) {
return { return {
llmContent: `Path is a directory, not a file: ${params.file_path}`, llmContent: `Path is a directory, not a file: ${filePath}`,
returnDisplay: `File is directory.`, returnDisplay: `File is directory.`,
}; };
} }
// Detect file type const fileType = this.detectFileType(filePath);
const fileType = this.detectFileType(params.file_path);
// Handle binary files differently
if (fileType !== 'text') { if (fileType !== 'text') {
return { return {
llmContent: `Binary file: ${params.file_path} (${fileType})`, llmContent: `Binary file: ${filePath} (${fileType})`,
returnDisplay: ``, returnDisplay: ``,
}; };
} }
// Read and process text file const content = fs.readFileSync(filePath, 'utf8');
const content = fs.readFileSync(params.file_path, 'utf8');
const lines = content.split('\n'); const lines = content.split('\n');
// Apply offset and limit
const startLine = params.offset || 0; const startLine = params.offset || 0;
// Use the default max lines if no limit is provided
const endLine = params.limit const endLine = params.limit
? startLine + params.limit ? startLine + params.limit
: Math.min(startLine + ReadFileTool.DEFAULT_MAX_LINES, lines.length); : Math.min(startLine + ReadFileTool.DEFAULT_MAX_LINES, lines.length);
const selectedLines = lines.slice(startLine, endLine); const selectedLines = lines.slice(startLine, endLine);
// Format with line numbers and handle line truncation
let truncated = false; let truncated = false;
const formattedLines = selectedLines.map((line) => { const formattedLines = selectedLines.map((line) => {
// Calculate actual line number (1-based)
// Truncate long lines
let processedLine = line; let processedLine = line;
if (line.length > ReadFileTool.MAX_LINE_LENGTH) { if (line.length > ReadFileTool.MAX_LINE_LENGTH) {
processedLine = line.substring(0, ReadFileTool.MAX_LINE_LENGTH) + '... [truncated]'; processedLine = line.substring(0, ReadFileTool.MAX_LINE_LENGTH) + '... [truncated]';
@ -270,10 +252,8 @@ export class ReadFileTool extends BaseTool<ReadFileToolParams, ReadFileToolResul
return processedLine; return processedLine;
}); });
// Check if content was truncated due to line limit or max lines limit
const contentTruncated = (endLine < lines.length) || truncated; const contentTruncated = (endLine < lines.length) || truncated;
// Create llmContent with truncation info if needed
let llmContent = ''; let llmContent = '';
if (contentTruncated) { if (contentTruncated) {
llmContent += `[File truncated: showing lines ${startLine + 1}-${endLine} of ${lines.length} total lines. Use offset parameter to view more.]\n`; llmContent += `[File truncated: showing lines ${startLine + 1}-${endLine} of ${lines.length} total lines. Use offset parameter to view more.]\n`;
@ -288,7 +268,7 @@ export class ReadFileTool extends BaseTool<ReadFileToolParams, ReadFileToolResul
const errorMsg = `Error reading file: ${error instanceof Error ? error.message : String(error)}`; const errorMsg = `Error reading file: ${error instanceof Error ? error.message : String(error)}`;
return { return {
llmContent: `Error reading file ${params.file_path}: ${errorMsg}`, llmContent: `Error reading file ${filePath}: ${errorMsg}`,
returnDisplay: `Failed to read file: ${errorMsg}`, returnDisplay: `Failed to read file: ${errorMsg}`,
}; };
} }

View File

@ -2,13 +2,10 @@ import { spawn, SpawnOptions, ChildProcessWithoutNullStreams, exec } from 'child
import path from 'path'; import path from 'path';
import os from 'os'; import os from 'os';
import crypto from 'crypto'; import crypto from 'crypto';
import { promises as fs } from 'fs'; // Added fs.promises import { promises as fs } from 'fs';
import { BaseTool } from './BaseTool.js'; // Adjust path as needed import { BaseTool, ToolResult } from './tool.js';
import { ToolResult } from './ToolResult.js'; // Adjust path as needed import { SchemaValidator } from '../utils/schemaValidator.js';
import { SchemaValidator } from '../utils/schemaValidator.js'; // Adjust path as needed
import { ToolCallConfirmationDetails, ToolConfirmationOutcome, ToolExecuteConfirmationDetails } from '../ui/types.js'; // Adjust path as needed import { ToolCallConfirmationDetails, ToolConfirmationOutcome, ToolExecuteConfirmationDetails } from '../ui/types.js'; // Adjust path as needed
import { GeminiClient } from '../core/GeminiClient.js';
import { SchemaUnion, Type } from '@google/genai';
import { BackgroundTerminalAnalyzer } from '../utils/BackgroundTerminalAnalyzer.js'; import { BackgroundTerminalAnalyzer } from '../utils/BackgroundTerminalAnalyzer.js';
// --- Interfaces --- // --- Interfaces ---

View File

@ -1,6 +1,5 @@
import { ToolListUnion, FunctionDeclaration } from '@google/genai'; import { ToolListUnion, FunctionDeclaration } from '@google/genai';
import { Tool } from './Tool.js'; import { Tool } from './tool.js';
import { ToolResult } from './ToolResult.js';
class ToolRegistry { class ToolRegistry {
private tools: Map<string, Tool> = new Map(); private tools: Map<string, Tool> = new Map();

View File

@ -1,7 +1,6 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { ToolResult } from './ToolResult.js'; import { BaseTool, ToolResult } from './tool.js';
import { BaseTool } from './BaseTool.js';
import { SchemaValidator } from '../utils/schemaValidator.js'; import { SchemaValidator } from '../utils/schemaValidator.js';
import { makeRelative, shortenPath } from '../utils/paths.js'; import { makeRelative, shortenPath } from '../utils/paths.js';
import { ToolCallConfirmationDetails, ToolConfirmationOutcome, ToolEditConfirmationDetails } from '../ui/types.js'; import { ToolCallConfirmationDetails, ToolConfirmationOutcome, ToolEditConfirmationDetails } from '../ui/types.js';
@ -52,7 +51,7 @@ export class WriteFileTool extends BaseTool<WriteFileToolParams, WriteFileToolRe
'Writes content to a specified file in the local filesystem.', 'Writes content to a specified file in the local filesystem.',
{ {
properties: { properties: {
file_path: { filePath: {
description: 'The absolute path to the file to write to (e.g., \'/home/user/project/file.txt\'). Relative paths are not supported.', description: 'The absolute path to the file to write to (e.g., \'/home/user/project/file.txt\'). Relative paths are not supported.',
type: 'string' type: 'string'
}, },
@ -61,7 +60,7 @@ export class WriteFileTool extends BaseTool<WriteFileToolParams, WriteFileToolRe
type: 'string' type: 'string'
} }
}, },
required: ['file_path', 'content'], required: ['filePath', 'content'],
type: 'object' type: 'object'
} }
); );

View File

@ -9,7 +9,7 @@ import HistoryDisplay from './components/HistoryDisplay.js';
import LoadingIndicator from './components/LoadingIndicator.js'; import LoadingIndicator from './components/LoadingIndicator.js';
import InputPrompt from './components/InputPrompt.js'; import InputPrompt from './components/InputPrompt.js';
import Footer from './components/Footer.js'; import Footer from './components/Footer.js';
import { StreamingState } from '../core/StreamingState.js'; import { StreamingState } from '../core/gemini-stream.js';
import { PartListUnion } from '@google/genai'; import { PartListUnion } from '@google/genai';
interface AppProps { interface AppProps {

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
import Spinner from 'ink-spinner'; import Spinner from 'ink-spinner';
import { ToolCallStatus } from '../../types.js'; import { ToolCallStatus } from '../../types.js';
import { ToolResultDisplay } from '../../../tools/ToolResult.js'; import { ToolResultDisplay } from '../../../tools/tool.js';
import DiffRenderer from './DiffRenderer.js'; import DiffRenderer from './DiffRenderer.js';
import { MarkdownRenderer } from '../../utils/MarkdownRenderer.js'; import { MarkdownRenderer } from '../../utils/MarkdownRenderer.js';

View File

@ -1,10 +1,10 @@
import { useState, useRef, useCallback, useEffect } from 'react'; import { useState, useRef, useCallback, useEffect } from 'react';
import { useInput } from 'ink'; import { useInput } from 'ink';
import { GeminiClient } from '../../core/GeminiClient.js'; import { GeminiClient } from '../../core/gemini-client.js';
import { type Chat, type PartListUnion } from '@google/genai'; import { type Chat, type PartListUnion } from '@google/genai';
import { HistoryItem } from '../types.js'; import { HistoryItem } from '../types.js';
import { processGeminiStream } from '../../core/geminiStreamProcessor.js'; import { processGeminiStream } from '../../core/gemini-stream.js';
import { StreamingState } from '../../core/StreamingState.js'; import { StreamingState } from '../../core/gemini-stream.js';
const addHistoryItem = ( const addHistoryItem = (
setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>, setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>,

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { WITTY_LOADING_PHRASES, PHRASE_CHANGE_INTERVAL_MS } from '../constants.js'; import { WITTY_LOADING_PHRASES, PHRASE_CHANGE_INTERVAL_MS } from '../constants.js';
import { StreamingState } from '../../core/StreamingState.js'; import { StreamingState } from '../../core/gemini-stream.js';
export const useLoadingIndicator = (streamingState: StreamingState) => { export const useLoadingIndicator = (streamingState: StreamingState) => {
const [elapsedTime, setElapsedTime] = useState(0); const [elapsedTime, setElapsedTime] = useState(0);

View File

@ -1,4 +1,4 @@
import { ToolResultDisplay } from "../tools/ToolResult.js"; import { ToolResultDisplay } from '../tools/tool.js';
export enum ToolCallStatus { export enum ToolCallStatus {
Pending, Pending,

View File

@ -1,6 +1,6 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { SchemaUnion, Type } from "@google/genai"; // Assuming these types exist import { SchemaUnion, Type } from "@google/genai"; // Assuming these types exist
import { GeminiClient } from "../core/GeminiClient.js"; // Assuming this path import { GeminiClient } from "../core/gemini-client.js"; // Assuming this path
import { exec } from 'child_process'; // Needed for Windows process check import { exec } from 'child_process'; // Needed for Windows process check
import { promisify } from 'util'; // To promisify exec import { promisify } from 'util'; // To promisify exec