Skip to content

Commit 315c59f

Browse files
LarsEckartclaude
andcommitted
Implement readFile and editFile tools for Gemini and add build script
- Add readFile method to GeminiTools with encoding support and file size limits - Add editFile method to GeminiTools with backup creation and security validation - Update GeminiProvider to register all three tools (listFiles, readFile, editFile) - Create run_build.sh script that applies formatting before building - Update documentation to reflect full tool support for both Claude and Gemini - Update CLAUDE.md and README.md to recommend new build script 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e020b41 commit 315c59f

File tree

6 files changed

+303
-118
lines changed

6 files changed

+303
-118
lines changed

CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ It's implemented in java though.
99

1010
## AI Provider Support
1111
- **Anthropic Claude**: Full support including tool calling (file operations)
12-
- **Google Gemini**: Basic chat support only (tool calling not yet implemented)
12+
- **Google Gemini**: Full support including tool calling (file operations: list files, read file, edit file)
1313

1414

1515
## Project Structure
@@ -31,7 +31,8 @@ This is a multi-module Gradle project with Kotlin DSL:
3131
- **View web logs**: `./dev-server.sh logs`
3232

3333
### Build & Test
34-
- **Build**: `./gradlew build` (includes fatJar creation)
34+
- **Build**: `./run_build.sh` (applies formatting and builds - recommended)
35+
- **Alternative build**: `./gradlew build` (includes fatJar creation)
3536
- **Test**: `./run_tests.sh` (runs tests with formatted output)
3637
- **Single test**: `./gradlew test --tests "ClassName"`
3738
- **Clean**: `./gradlew clean`

README.md

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,35 @@ This application demonstrates clean architecture principles and provides both CL
77

88
### Project Statistics
99

10-
- **Total Commits**: 59
11-
- **AI-Assisted Commits**: 40 (67.80%)
12-
- **Total Lines Added**: 8080
13-
- **AI-Assisted Lines Added**: 6410 (79.33%)
14-
- **Total Lines Changed**: 12053
15-
- **AI-Assisted Lines Changed**: 9650 (80.06%)
10+
- **Total Commits**: 60
11+
- **AI-Assisted Commits**: 41 (68.33%)
12+
- **Total Lines Added**: 8622
13+
- **AI-Assisted Lines Added**: 6952 (80.63%)
14+
- **Total Lines Changed**: 12883
15+
- **AI-Assisted Lines Changed**: 10480 (81.35%)
1616

1717
### Breakdown by AI Assistant
1818

1919
#### Claude Code
2020

21-
- **Commits**: 31 (52.54%)
22-
- **Lines Added**: 5081
23-
- **Lines Deleted**: 2357
24-
- **Lines Changed**: 7438 (61.71%)
21+
- **Commits**: 32 (53.33%)
22+
- **Lines Added**: 5623
23+
- **Lines Deleted**: 2645
24+
- **Lines Changed**: 8268 (64.18%)
2525

2626
#### Amp
2727

28-
- **Commits**: 7 (11.86%)
28+
- **Commits**: 7 (11.67%)
2929
- **Lines Added**: 1258
3030
- **Lines Deleted**: 809
31-
- **Lines Changed**: 2067 (17.15%)
31+
- **Lines Changed**: 2067 (16.04%)
3232

3333
#### GitHub Copilot
3434

35-
- **Commits**: 2 (3.39%)
35+
- **Commits**: 2 (3.33%)
3636
- **Lines Added**: 71
3737
- **Lines Deleted**: 74
38-
- **Lines Changed**: 145 (1.20%)
38+
- **Lines Changed**: 145 (1.13%)
3939

4040

4141
*Statistics are automatically updated on each commit.*
@@ -44,7 +44,7 @@ This application demonstrates clean architecture principles and provides both CL
4444

4545
- **Multi-Provider Support**: Choose between Anthropic Claude and Google Gemini
4646
- **Dual Interface Support**: Run as a command-line application or web server
47-
- **File Operations**: Built-in tools for reading, editing, and listing files (CLI mode only, Anthropic Claude only)
47+
- **File Operations**: Built-in tools for reading, editing, and listing files (supports both Claude and Gemini)
4848
- **Clean Architecture**: Hexagonal architecture with clear separation of concerns
4949
- **Hot Reloading**: Development server with automatic restart and live reload
5050
- **Separate Logging**: Mode-specific log files (CLI: `logs/application-cli.log`, Web: `logs/application-web.log`)
@@ -76,14 +76,12 @@ export GOOGLE_API_KEY="your-api-key-here"
7676
export AI_PROVIDER=gemini
7777
```
7878

79-
> **Note**: Tool support (file operations) is currently only available with Anthropic Claude. Gemini provider supports basic chat functionality only.
80-
8179
### 2. Clone and Build
8280

8381
```bash
8482
git clone <repository-url>
8583
cd code-editing-agent-java
86-
./gradlew build
84+
./run_build.sh
8785
```
8886

8987
### 3. Install Git Hooks (Optional)
@@ -138,7 +136,7 @@ For active development with hot reloading:
138136
./gradlew test --tests "ConversationServiceTest"
139137

140138
# Build and run tests
141-
./gradlew build
139+
./run_build.sh
142140
```
143141

144142
### Project Commands
@@ -153,7 +151,8 @@ For active development with hot reloading:
153151
- **View web logs**: `./dev-server.sh logs`
154152

155153
#### Build & Test
156-
- **Build**: `./gradlew build` (includes fatJar creation)
154+
- **Build**: `./run_build.sh` (applies formatting and builds - recommended)
155+
- **Alternative build**: `./gradlew build` (includes fatJar creation)
157156
- **Test**: `./run_tests.sh` (runs tests with formatted output)
158157
- **Clean**: `./gradlew clean`
159158
- **Fat JAR**: Created automatically during build in `app/build/libs/`
@@ -176,13 +175,13 @@ This application follows hexagonal architecture principles:
176175

177176
### Built-in Tools
178177

179-
The agent comes with file operation tools (CLI mode only, Anthropic Claude only):
178+
The agent comes with file operation tools:
180179

181180
- **ReadFileTool**: Read file contents
182-
- **EditFileTool**: Modify existing files
181+
- **EditFileTool**: Modify existing files
183182
- **ListFilesTool**: Browse directory contents
184183

185-
> **Note**: Tool support is currently only available when using Anthropic Claude as the AI provider. Google Gemini provider does not yet support function calling/tools.
184+
> **Note**: Tool support is available for both Anthropic Claude and Google Gemini providers.
186185
187186
> **Security Note**: These file tools are designed exclusively for CLI mode where they operate on the user's local file system. In web mode, these tools would pose serious security risks by allowing web users to access the server's file system. A proper web implementation would require either disabling file tools entirely or implementing sandboxed alternatives.
188187

app/src/main/java/com/larseckart/adapters/ai/GeminiProvider.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,14 @@ public AIResponse sendMessage(AIRequest request) {
5757
log.info("Attempting to register Gemini tools");
5858
Method listFilesMethod =
5959
GeminiTools.class.getDeclaredMethod("listFiles", String.class, Boolean.class);
60-
configBuilder.tools(Tool.builder().functions(listFilesMethod));
61-
log.info("Successfully registered listFiles tool for Gemini");
60+
Method readFileMethod =
61+
GeminiTools.class.getDeclaredMethod("readFile", String.class, String.class);
62+
Method editFileMethod =
63+
GeminiTools.class.getDeclaredMethod(
64+
"editFile", String.class, String.class, String.class);
65+
configBuilder.tools(
66+
Tool.builder().functions(listFilesMethod, readFileMethod, editFileMethod));
67+
log.info("Successfully registered listFiles, readFile, and editFile tools for Gemini");
6268
} catch (NoSuchMethodException e) {
6369
log.error("Failed to register Gemini tools", e);
6470
}

app/src/main/java/com/larseckart/tools/gemini/GeminiTools.java

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,21 @@
33
import static org.slf4j.LoggerFactory.getLogger;
44

55
import java.io.IOException;
6+
import java.nio.charset.Charset;
67
import java.nio.file.Files;
8+
import java.nio.file.NoSuchFileException;
79
import java.nio.file.Path;
810
import java.nio.file.Paths;
11+
import java.nio.file.StandardCopyOption;
912
import java.util.Comparator;
1013
import java.util.stream.Stream;
1114
import org.slf4j.Logger;
1215

1316
public class GeminiTools {
1417

1518
private static final Logger log = getLogger(GeminiTools.class);
19+
private static final long MAX_FILE_SIZE = 1024 * 1024; // 1MB limit
20+
private static final String DEFAULT_ENCODING = "UTF-8";
1621

1722
/**
1823
* Lists the contents of a directory, including files and subdirectories
@@ -88,6 +93,183 @@ public static String listFiles(String path, Boolean showHidden) {
8893
}
8994
}
9095

96+
/**
97+
* Reads file contents from the filesystem. Supports both absolute and relative paths, various
98+
* encodings, and includes proper error handling and file size limits.
99+
*
100+
* @param path The file path to read (absolute or relative to current working directory)
101+
* @param encoding The character encoding to use (defaults to UTF-8 if null)
102+
* @return File contents or error message
103+
*/
104+
public static String readFile(String path, String encoding) {
105+
log.info("Executing readFile with path: {}, encoding: {}", path, encoding);
106+
107+
try {
108+
// Handle default values
109+
String pathStr = path != null ? path : ".";
110+
String fileEncoding = encoding != null ? encoding : DEFAULT_ENCODING;
111+
112+
Path filePath = resolveFilePath(pathStr);
113+
114+
// Check file size before reading
115+
if (Files.exists(filePath)) {
116+
long fileSize = Files.size(filePath);
117+
if (fileSize > MAX_FILE_SIZE) {
118+
return "Error: File is too large ("
119+
+ fileSize
120+
+ " bytes). Maximum supported file size is "
121+
+ MAX_FILE_SIZE
122+
+ " bytes.";
123+
}
124+
}
125+
126+
// Read file content with specified encoding
127+
Charset charset = Charset.forName(fileEncoding);
128+
return Files.readString(filePath, charset);
129+
130+
} catch (NoSuchFileException e) {
131+
return "Error: File not found: " + e.getFile();
132+
} catch (SecurityException e) {
133+
return "Error: Permission denied accessing file: " + path;
134+
} catch (IOException e) {
135+
return "Error: IO exception reading file: " + e.getMessage();
136+
} catch (IllegalArgumentException e) {
137+
return "Error: Invalid encoding specified: " + encoding;
138+
} catch (Exception e) {
139+
return "Error: Unexpected error reading file: " + e.getMessage();
140+
}
141+
}
142+
143+
/**
144+
* Performs simple text replacement in files. Creates a backup before editing and validates that
145+
* search text exists.
146+
*
147+
* @param path The path to the file to edit
148+
* @param searchText The text to search for and replace
149+
* @param replaceText The text to replace the search text with
150+
* @return Success message with replacement count or error message
151+
*/
152+
public static String editFile(String path, String searchText, String replaceText) {
153+
log.info(
154+
"Executing editFile with path: {}, searchText: {}, replaceText: {}",
155+
path,
156+
searchText,
157+
replaceText);
158+
159+
try {
160+
// Validate required parameters
161+
if (path == null || path.trim().isEmpty()) {
162+
return "Error: 'path' parameter is required and cannot be empty";
163+
}
164+
if (searchText == null || searchText.trim().isEmpty()) {
165+
return "Error: 'searchText' parameter is required and cannot be empty";
166+
}
167+
if (replaceText == null) {
168+
return "Error: 'replaceText' parameter is required";
169+
}
170+
171+
// Validate path for security (prevent directory traversal)
172+
if (path.contains("..")
173+
|| path.startsWith("/etc/")
174+
|| path.startsWith("/usr/")
175+
|| path.startsWith("/bin/")) {
176+
log.warn("Potentially unsafe path attempted: {}", path);
177+
return "Error: Path not allowed for security reasons";
178+
}
179+
180+
Path filePath;
181+
try {
182+
filePath = Paths.get(path);
183+
if (!filePath.isAbsolute()) {
184+
filePath = Paths.get(System.getProperty("user.dir")).resolve(path);
185+
}
186+
filePath = filePath.normalize();
187+
} catch (Exception e) {
188+
log.error("Invalid path: {}", path, e);
189+
return "Error: Invalid file path: " + path;
190+
}
191+
192+
// Check if file exists
193+
if (!Files.exists(filePath)) {
194+
log.error("File not found: {}", filePath);
195+
return "Error: File not found: " + path;
196+
}
197+
198+
// Check if it's actually a file (not a directory)
199+
if (!Files.isRegularFile(filePath)) {
200+
log.error("Path is not a regular file: {}", filePath);
201+
return "Error: Path is not a regular file: " + path;
202+
}
203+
204+
// Read current file content
205+
String content;
206+
try {
207+
content = Files.readString(filePath);
208+
} catch (IOException e) {
209+
log.error("Failed to read file: {}", filePath, e);
210+
return "Error: Failed to read file: " + e.getMessage();
211+
}
212+
213+
// Check if search text exists in the file
214+
if (!content.contains(searchText)) {
215+
log.info("Search text '{}' not found in file: {}", searchText, filePath);
216+
return "Error: Text '" + searchText + "' not found in file";
217+
}
218+
219+
// Count occurrences before replacement
220+
int occurrences = content.split(searchText, -1).length - 1;
221+
222+
// Create backup file
223+
Path backupPath = Paths.get(filePath + ".backup");
224+
try {
225+
Files.copy(filePath, backupPath, StandardCopyOption.REPLACE_EXISTING);
226+
log.info("Created backup file: {}", backupPath);
227+
} catch (IOException e) {
228+
log.error("Failed to create backup file: {}", backupPath, e);
229+
return "Error: Failed to create backup file: " + e.getMessage();
230+
}
231+
232+
// Perform replacement
233+
String newContent = content.replace(searchText, replaceText);
234+
235+
// Write updated content back to file
236+
try {
237+
Files.writeString(filePath, newContent);
238+
log.info("Successfully edited file: {} ({} occurrences replaced)", filePath, occurrences);
239+
} catch (IOException e) {
240+
log.error("Failed to write updated content to file: {}", filePath, e);
241+
return "Error: Failed to write to file: " + e.getMessage();
242+
}
243+
244+
return String.format(
245+
"File edited successfully! Replaced %d occurrences of '%s' with '%s' in %s. Backup created at %s.backup",
246+
occurrences, searchText, replaceText, path, path);
247+
248+
} catch (Exception e) {
249+
log.error("Unexpected error in editFile tool", e);
250+
return "Error: Unexpected error occurred: " + e.getMessage();
251+
}
252+
}
253+
254+
/**
255+
* Resolves the file path, handling both absolute and relative paths. Relative paths are resolved
256+
* against the current working directory.
257+
*
258+
* @param pathStr the path string to resolve
259+
* @return the resolved Path
260+
*/
261+
private static Path resolveFilePath(String pathStr) {
262+
Path path = Paths.get(pathStr);
263+
264+
if (path.isAbsolute()) {
265+
return path;
266+
} else {
267+
// Resolve relative path against current working directory
268+
Path currentDir = Paths.get(System.getProperty("user.dir"));
269+
return currentDir.resolve(path);
270+
}
271+
}
272+
91273
private static String formatFileSize(long bytes) {
92274
if (bytes < 1024) {
93275
return bytes + " bytes";

0 commit comments

Comments
 (0)