feat: add gemini tools (#120)
This commit is contained in:
30
src/tools/impl/GlobGemini.ts
Normal file
30
src/tools/impl/GlobGemini.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Gemini CLI glob tool - wrapper around Letta Code's Glob tool
|
||||
* Uses Gemini's exact schema and description
|
||||
*/
|
||||
|
||||
import { glob as lettaGlob } from "./Glob";
|
||||
|
||||
interface GlobGeminiArgs {
|
||||
pattern: string;
|
||||
dir_path?: string;
|
||||
case_sensitive?: boolean;
|
||||
respect_git_ignore?: boolean;
|
||||
respect_gemini_ignore?: boolean;
|
||||
}
|
||||
|
||||
export async function glob_gemini(
|
||||
args: GlobGeminiArgs,
|
||||
): Promise<{ message: string }> {
|
||||
// Adapt Gemini params to Letta Code's Glob tool
|
||||
const lettaArgs = {
|
||||
pattern: args.pattern,
|
||||
path: args.dir_path,
|
||||
};
|
||||
|
||||
const result = await lettaGlob(lettaArgs);
|
||||
|
||||
// Glob returns { files: string[], truncated?, totalFiles? }
|
||||
const message = result.files.join("\n");
|
||||
return { message };
|
||||
}
|
||||
32
src/tools/impl/ListDirectoryGemini.ts
Normal file
32
src/tools/impl/ListDirectoryGemini.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Gemini CLI list_directory tool - wrapper around Letta Code's LS tool
|
||||
* Uses Gemini's exact schema and description
|
||||
*/
|
||||
|
||||
import { ls } from "./LS";
|
||||
|
||||
interface ListDirectoryGeminiArgs {
|
||||
dir_path: string;
|
||||
ignore?: string[];
|
||||
file_filtering_options?: {
|
||||
respect_git_ignore?: boolean;
|
||||
respect_gemini_ignore?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export async function list_directory(
|
||||
args: ListDirectoryGeminiArgs,
|
||||
): Promise<{ message: string }> {
|
||||
// Adapt Gemini params to Letta Code's LS tool
|
||||
const lettaArgs = {
|
||||
path: args.dir_path,
|
||||
ignore: args.ignore,
|
||||
};
|
||||
|
||||
const result = await ls(lettaArgs);
|
||||
|
||||
// LS returns { content: Array<{ type: string, text: string }> }
|
||||
// Convert to string message
|
||||
const message = result.content.map((item) => item.text).join("\n");
|
||||
return { message };
|
||||
}
|
||||
29
src/tools/impl/ReadFileGemini.ts
Normal file
29
src/tools/impl/ReadFileGemini.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Gemini CLI read_file tool - wrapper around Letta Code's Read tool
|
||||
* Uses Gemini's exact schema and description
|
||||
*/
|
||||
|
||||
import { read } from "./Read";
|
||||
|
||||
interface ReadFileGeminiArgs {
|
||||
file_path: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export async function read_file_gemini(
|
||||
args: ReadFileGeminiArgs,
|
||||
): Promise<{ message: string }> {
|
||||
// Adapt Gemini params to Letta Code's Read tool
|
||||
// Gemini uses 0-based offset, Letta Code uses 1-based
|
||||
const lettaArgs = {
|
||||
file_path: args.file_path,
|
||||
offset: args.offset !== undefined ? args.offset + 1 : undefined,
|
||||
limit: args.limit,
|
||||
};
|
||||
|
||||
const result = await read(lettaArgs);
|
||||
|
||||
// Read returns { content: string }
|
||||
return { message: result.content };
|
||||
}
|
||||
113
src/tools/impl/ReadManyFilesGemini.ts
Normal file
113
src/tools/impl/ReadManyFilesGemini.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Gemini CLI read_many_files tool - new implementation for Letta Code
|
||||
* Uses Gemini's exact schema and description
|
||||
*/
|
||||
|
||||
import path from "node:path";
|
||||
import { glob as globFn } from "glob";
|
||||
import { read } from "./Read";
|
||||
|
||||
interface ReadManyFilesGeminiArgs {
|
||||
include: string[];
|
||||
exclude?: string[];
|
||||
recursive?: boolean;
|
||||
useDefaultExcludes?: boolean;
|
||||
file_filtering_options?: {
|
||||
respect_git_ignore?: boolean;
|
||||
respect_gemini_ignore?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_EXCLUDES = [
|
||||
"**/node_modules/**",
|
||||
"**/.git/**",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/.next/**",
|
||||
"**/coverage/**",
|
||||
"**/*.min.js",
|
||||
"**/*.bundle.js",
|
||||
];
|
||||
|
||||
export async function read_many_files(
|
||||
args: ReadManyFilesGeminiArgs,
|
||||
): Promise<{ message: string }> {
|
||||
const { include, exclude = [], useDefaultExcludes = true } = args;
|
||||
|
||||
if (!Array.isArray(include) || include.length === 0) {
|
||||
throw new Error("include must be a non-empty array of glob patterns");
|
||||
}
|
||||
|
||||
// Build ignore patterns
|
||||
const ignorePatterns = useDefaultExcludes
|
||||
? [...DEFAULT_EXCLUDES, ...exclude]
|
||||
: exclude;
|
||||
|
||||
const cwd = process.cwd();
|
||||
const allFiles = new Set<string>();
|
||||
|
||||
// Process each include pattern
|
||||
for (const pattern of include) {
|
||||
const files = await globFn(pattern, {
|
||||
cwd,
|
||||
ignore: ignorePatterns,
|
||||
nodir: true,
|
||||
dot: true,
|
||||
absolute: true,
|
||||
});
|
||||
for (const f of files) {
|
||||
allFiles.add(f);
|
||||
}
|
||||
}
|
||||
|
||||
const sortedFiles = Array.from(allFiles).sort();
|
||||
|
||||
if (sortedFiles.length === 0) {
|
||||
return {
|
||||
message: `No files matching the criteria were found or all were skipped.`,
|
||||
};
|
||||
}
|
||||
|
||||
// Read all files and concatenate
|
||||
const contentParts: string[] = [];
|
||||
const skippedFiles: Array<{ path: string; reason: string }> = [];
|
||||
|
||||
for (const filePath of sortedFiles) {
|
||||
try {
|
||||
const _relativePath = path.relative(cwd, filePath);
|
||||
const separator = `--- ${filePath} ---`;
|
||||
|
||||
// Use our Read tool to read the file
|
||||
const result = await read({ file_path: filePath });
|
||||
const content = result.content;
|
||||
|
||||
contentParts.push(`${separator}\n\n${content}\n\n`);
|
||||
} catch (error) {
|
||||
const relativePath = path.relative(cwd, filePath);
|
||||
skippedFiles.push({
|
||||
path: relativePath,
|
||||
reason:
|
||||
error instanceof Error ? error.message : "Unknown error reading file",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
contentParts.push("--- End of content ---");
|
||||
|
||||
const processedCount = sortedFiles.length - skippedFiles.length;
|
||||
let _displayMessage = `Successfully read and concatenated content from **${processedCount} file(s)**.`;
|
||||
|
||||
if (skippedFiles.length > 0) {
|
||||
_displayMessage += `\n\n**Skipped ${skippedFiles.length} file(s):**`;
|
||||
skippedFiles.slice(0, 5).forEach((f) => {
|
||||
_displayMessage += `\n- \`${f.path}\` (${f.reason})`;
|
||||
});
|
||||
if (skippedFiles.length > 5) {
|
||||
_displayMessage += `\n- ...and ${skippedFiles.length - 5} more`;
|
||||
}
|
||||
}
|
||||
|
||||
const message = contentParts.join("");
|
||||
|
||||
return { message };
|
||||
}
|
||||
33
src/tools/impl/ReplaceGemini.ts
Normal file
33
src/tools/impl/ReplaceGemini.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Gemini CLI replace tool - wrapper around Letta Code's Edit tool
|
||||
* Uses Gemini's exact schema and description
|
||||
*/
|
||||
|
||||
import { edit } from "./Edit";
|
||||
|
||||
interface ReplaceGeminiArgs {
|
||||
file_path: string;
|
||||
old_string: string;
|
||||
new_string: string;
|
||||
expected_replacements?: number;
|
||||
}
|
||||
|
||||
export async function replace(
|
||||
args: ReplaceGeminiArgs,
|
||||
): Promise<{ message: string }> {
|
||||
// Adapt Gemini params to Letta Code's Edit tool
|
||||
// Gemini uses expected_replacements, Letta Code uses replace_all
|
||||
const lettaArgs = {
|
||||
file_path: args.file_path,
|
||||
old_string: args.old_string,
|
||||
new_string: args.new_string,
|
||||
replace_all: !!(
|
||||
args.expected_replacements && args.expected_replacements > 1
|
||||
),
|
||||
};
|
||||
|
||||
const result = await edit(lettaArgs);
|
||||
|
||||
// Edit returns { message: string, replacements: number }
|
||||
return { message: result.message };
|
||||
}
|
||||
28
src/tools/impl/RunShellCommandGemini.ts
Normal file
28
src/tools/impl/RunShellCommandGemini.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Gemini CLI run_shell_command tool - wrapper around Letta Code's Bash tool
|
||||
* Uses Gemini's exact schema and description
|
||||
*/
|
||||
|
||||
import { bash } from "./Bash";
|
||||
|
||||
interface RunShellCommandGeminiArgs {
|
||||
command: string;
|
||||
description?: string;
|
||||
dir_path?: string;
|
||||
}
|
||||
|
||||
export async function run_shell_command(
|
||||
args: RunShellCommandGeminiArgs,
|
||||
): Promise<{ message: string }> {
|
||||
// Adapt Gemini params to Letta Code's Bash tool
|
||||
const lettaArgs = {
|
||||
command: args.command,
|
||||
description: args.description,
|
||||
};
|
||||
|
||||
const result = await bash(lettaArgs);
|
||||
|
||||
// Bash returns { content: Array<{ type: string, text: string }>, status: string }
|
||||
const message = result.content.map((item) => item.text).join("\n");
|
||||
return { message };
|
||||
}
|
||||
29
src/tools/impl/SearchFileContentGemini.ts
Normal file
29
src/tools/impl/SearchFileContentGemini.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Gemini CLI search_file_content tool - wrapper around Letta Code's Grep tool
|
||||
* Uses Gemini's exact schema and description
|
||||
*/
|
||||
|
||||
import { grep } from "./Grep";
|
||||
|
||||
interface SearchFileContentGeminiArgs {
|
||||
pattern: string;
|
||||
dir_path?: string;
|
||||
include?: string;
|
||||
}
|
||||
|
||||
export async function search_file_content(
|
||||
args: SearchFileContentGeminiArgs,
|
||||
): Promise<{ message: string }> {
|
||||
// Adapt Gemini params to Letta Code's Grep tool
|
||||
const lettaArgs = {
|
||||
pattern: args.pattern,
|
||||
path: args.dir_path,
|
||||
glob: args.include,
|
||||
output_mode: "content" as const, // Return actual matching lines, not just file paths
|
||||
};
|
||||
|
||||
const result = await grep(lettaArgs);
|
||||
|
||||
// Grep returns { output: string, matches?, files? }
|
||||
return { message: result.output };
|
||||
}
|
||||
21
src/tools/impl/WriteFileGemini.ts
Normal file
21
src/tools/impl/WriteFileGemini.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Gemini CLI write_file tool - wrapper around Letta Code's Write tool
|
||||
* Uses Gemini's exact schema and description
|
||||
*/
|
||||
|
||||
import { write } from "./Write";
|
||||
|
||||
interface WriteFileGeminiArgs {
|
||||
file_path: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export async function write_file_gemini(
|
||||
args: WriteFileGeminiArgs,
|
||||
): Promise<{ message: string }> {
|
||||
// Direct mapping - parameters match exactly
|
||||
const result = await write(args);
|
||||
|
||||
// Write returns { message: string }
|
||||
return result;
|
||||
}
|
||||
60
src/tools/impl/WriteTodosGemini.ts
Normal file
60
src/tools/impl/WriteTodosGemini.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Gemini CLI write_todos tool - adapter for Letta Code's todo_write
|
||||
* Uses Gemini's exact schema and description but adapts the params
|
||||
*/
|
||||
|
||||
interface WriteTodosGeminiArgs {
|
||||
todos: Array<{
|
||||
description: string;
|
||||
status: "pending" | "in_progress" | "completed" | "cancelled";
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function write_todos(
|
||||
args: WriteTodosGeminiArgs,
|
||||
): Promise<{ message: string; todos: typeof args.todos }> {
|
||||
// Gemini uses "description" field, Letta Code uses "content" field
|
||||
// Convert to Letta format and validate
|
||||
if (!Array.isArray(args.todos)) {
|
||||
throw new Error("todos must be an array");
|
||||
}
|
||||
|
||||
for (const todo of args.todos) {
|
||||
if (!todo.description || typeof todo.description !== "string") {
|
||||
throw new Error("Each todo must have a description string");
|
||||
}
|
||||
if (
|
||||
!todo.status ||
|
||||
!["pending", "in_progress", "completed", "cancelled"].includes(
|
||||
todo.status,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
"Each todo must have a valid status (pending, in_progress, completed, or cancelled)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate only one in_progress
|
||||
const inProgressCount = args.todos.filter(
|
||||
(t) => t.status === "in_progress",
|
||||
).length;
|
||||
if (inProgressCount > 1) {
|
||||
throw new Error("Only one task can be 'in_progress' at a time.");
|
||||
}
|
||||
|
||||
const todoListString = args.todos
|
||||
.map((todo, index) => `${index + 1}. [${todo.status}] ${todo.description}`)
|
||||
.join("\n");
|
||||
|
||||
const message =
|
||||
args.todos.length > 0
|
||||
? `Successfully updated the todo list. The current list is now:\n${todoListString}`
|
||||
: "Successfully cleared the todo list.";
|
||||
|
||||
// Return with both message and todos for UI rendering
|
||||
return {
|
||||
message,
|
||||
todos: args.todos,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user