feat: add gemini tools (#120)

This commit is contained in:
Charles Packer
2025-11-24 10:50:31 -08:00
committed by GitHub
parent 43813383ac
commit f2ed25bfeb
51 changed files with 1639 additions and 64 deletions

View 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 };
}

View 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 };
}

View 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 };
}

View 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 };
}

View 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 };
}

View 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 };
}

View 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 };
}

View 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;
}

View 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,
};
}