feat: add gemini tools (#120)
This commit is contained in:
@@ -201,12 +201,14 @@ Letta Code includes different toolsets optimized for different model providers:
|
|||||||
|
|
||||||
1. **Default Toolset** (Anthropic-optimized, best for Claude models)
|
1. **Default Toolset** (Anthropic-optimized, best for Claude models)
|
||||||
2. **Codex Toolset** (OpenAI-optimized, best for GPT models)
|
2. **Codex Toolset** (OpenAI-optimized, best for GPT models)
|
||||||
|
3. **Gemini Toolset** (Google-optimized, best for Gemini models)
|
||||||
|
|
||||||
**Automatic Selection:**
|
**Automatic Selection:**
|
||||||
When you specify a model, Letta Code automatically selects the appropriate toolset:
|
When you specify a model, Letta Code automatically selects the appropriate toolset:
|
||||||
```bash
|
```bash
|
||||||
letta --model haiku # Loads default toolset
|
letta --model haiku # Loads default toolset
|
||||||
letta --model gpt-5-codex # Loads codex toolset
|
letta --model gpt-5-codex # Loads codex toolset
|
||||||
|
letta --model gemini-3-pro # Loads gemini toolset
|
||||||
```
|
```
|
||||||
|
|
||||||
**Manual Override:**
|
**Manual Override:**
|
||||||
@@ -214,7 +216,8 @@ You can force a specific toolset regardless of model:
|
|||||||
```bash
|
```bash
|
||||||
# CLI flag (at startup)
|
# CLI flag (at startup)
|
||||||
letta --model haiku --toolset codex # Use Codex-style tools with Claude Haiku
|
letta --model haiku --toolset codex # Use Codex-style tools with Claude Haiku
|
||||||
letta --model gpt-5-codex --toolset default # Use Anthropic-style tools with GPT-5-Codex
|
letta --model gpt-5-codex --toolset gemini # Use Gemini-style tools with GPT-5-Codex
|
||||||
|
letta --toolset gemini # Use Gemini tools with default model
|
||||||
|
|
||||||
# Interactive command (during session)
|
# Interactive command (during session)
|
||||||
/toolset # Opens toolset selector
|
/toolset # Opens toolset selector
|
||||||
|
|||||||
11
bun.lock
11
bun.lock
@@ -5,6 +5,7 @@
|
|||||||
"name": "@letta-ai/letta-code",
|
"name": "@letta-ai/letta-code",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@letta-ai/letta-client": "^1.1.2",
|
"@letta-ai/letta-client": "^1.1.2",
|
||||||
|
"glob": "^13.0.0",
|
||||||
"ink-link": "^5.0.0",
|
"ink-link": "^5.0.0",
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
},
|
},
|
||||||
@@ -113,6 +114,8 @@
|
|||||||
|
|
||||||
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
|
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
|
||||||
|
|
||||||
|
"glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="],
|
||||||
|
|
||||||
"has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="],
|
"has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="],
|
||||||
|
|
||||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
@@ -151,6 +154,8 @@
|
|||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="],
|
||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||||
@@ -159,6 +164,8 @@
|
|||||||
|
|
||||||
"minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
"minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
||||||
|
|
||||||
|
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nano-spawn": ["nano-spawn@2.0.0", "", {}, "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw=="],
|
"nano-spawn": ["nano-spawn@2.0.0", "", {}, "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw=="],
|
||||||
@@ -169,6 +176,8 @@
|
|||||||
|
|
||||||
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
|
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
|
||||||
|
|
||||||
|
"path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
|
||||||
|
|
||||||
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
|
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
@@ -231,6 +240,8 @@
|
|||||||
|
|
||||||
"cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
|
"cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
|
||||||
|
|
||||||
|
"glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||||
|
|
||||||
"ink-text-input/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
|
"ink-text-input/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
|
||||||
|
|
||||||
"listr2/cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
|
"listr2/cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@letta-ai/letta-client": "^1.1.2",
|
"@letta-ai/letta-client": "^1.1.2",
|
||||||
|
"glob": "^13.0.0",
|
||||||
"ink-link": "^5.0.0",
|
"ink-link": "^5.0.0",
|
||||||
"open": "^10.2.0"
|
"open": "^10.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,8 +51,15 @@ export async function createAgent(
|
|||||||
const client = await getClient();
|
const client = await getClient();
|
||||||
|
|
||||||
// Get loaded tool names (tools are already registered with Letta)
|
// Get loaded tool names (tools are already registered with Letta)
|
||||||
|
// Map internal names to server names so the agent sees the correct tool names
|
||||||
|
const { getServerToolName } = await import("../tools/manager");
|
||||||
|
const internalToolNames = getToolNames();
|
||||||
|
const serverToolNames = internalToolNames.map((name) =>
|
||||||
|
getServerToolName(name),
|
||||||
|
);
|
||||||
|
|
||||||
const toolNames = [
|
const toolNames = [
|
||||||
...getToolNames(),
|
...serverToolNames,
|
||||||
"memory",
|
"memory",
|
||||||
"web_search",
|
"web_search",
|
||||||
"conversation_search",
|
"conversation_search",
|
||||||
|
|||||||
@@ -95,13 +95,16 @@ export async function linkToolsToAgent(agentId: string): Promise<LinkResult> {
|
|||||||
.filter((name): name is string => typeof name === "string"),
|
.filter((name): name is string => typeof name === "string"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get Letta Code tool names
|
// Get Letta Code tool names (internal names from registry)
|
||||||
|
const { getServerToolName } = await import("../tools/manager");
|
||||||
const lettaCodeToolNames = getToolNames();
|
const lettaCodeToolNames = getToolNames();
|
||||||
|
|
||||||
// Find tools to add (tools that aren't already attached)
|
// Find tools to add (tools that aren't already attached)
|
||||||
const toolsToAdd = lettaCodeToolNames.filter(
|
// Compare using server names since that's what the agent has
|
||||||
(name) => !currentToolNames.has(name),
|
const toolsToAdd = lettaCodeToolNames.filter((internalName) => {
|
||||||
);
|
const serverName = getServerToolName(internalName);
|
||||||
|
return !currentToolNames.has(serverName);
|
||||||
|
});
|
||||||
|
|
||||||
if (toolsToAdd.length === 0) {
|
if (toolsToAdd.length === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -112,9 +115,11 @@ export async function linkToolsToAgent(agentId: string): Promise<LinkResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Look up tool IDs from global tool list
|
// Look up tool IDs from global tool list
|
||||||
|
// Use server names when querying, since that's how tools are registered on the server
|
||||||
const toolsToAddIds: string[] = [];
|
const toolsToAddIds: string[] = [];
|
||||||
for (const toolName of toolsToAdd) {
|
for (const toolName of toolsToAdd) {
|
||||||
const toolsResponse = await client.tools.list({ name: toolName });
|
const serverName = getServerToolName(toolName);
|
||||||
|
const toolsResponse = await client.tools.list({ name: serverName });
|
||||||
const tool = toolsResponse.items[0];
|
const tool = toolsResponse.items[0];
|
||||||
if (tool?.id) {
|
if (tool?.id) {
|
||||||
toolsToAddIds.push(tool.id);
|
toolsToAddIds.push(tool.id);
|
||||||
@@ -130,7 +135,7 @@ export async function linkToolsToAgent(agentId: string): Promise<LinkResult> {
|
|||||||
const newToolRules = [
|
const newToolRules = [
|
||||||
...currentToolRules,
|
...currentToolRules,
|
||||||
...toolsToAdd.map((toolName) => ({
|
...toolsToAdd.map((toolName) => ({
|
||||||
tool_name: toolName,
|
tool_name: getServerToolName(toolName),
|
||||||
type: "requires_approval" as const,
|
type: "requires_approval" as const,
|
||||||
prompt_template: null,
|
prompt_template: null,
|
||||||
})),
|
})),
|
||||||
@@ -171,11 +176,18 @@ export async function unlinkToolsFromAgent(
|
|||||||
include: ["agent.tools"],
|
include: ["agent.tools"],
|
||||||
});
|
});
|
||||||
const allTools = agent.tools || [];
|
const allTools = agent.tools || [];
|
||||||
|
|
||||||
|
// Get all possible Letta Code tool names (both internal and server names)
|
||||||
|
const { getServerToolName } = await import("../tools/manager");
|
||||||
const lettaCodeToolNames = new Set(getAllLettaToolNames());
|
const lettaCodeToolNames = new Set(getAllLettaToolNames());
|
||||||
|
const lettaCodeServerNames = new Set(
|
||||||
|
Array.from(lettaCodeToolNames).map((name) => getServerToolName(name)),
|
||||||
|
);
|
||||||
|
|
||||||
// Filter out Letta Code tools, keep everything else
|
// Filter out Letta Code tools, keep everything else
|
||||||
|
// Check against server names since that's what the agent sees
|
||||||
const remainingTools = allTools.filter(
|
const remainingTools = allTools.filter(
|
||||||
(t) => t.name && !lettaCodeToolNames.has(t.name),
|
(t) => t.name && !lettaCodeServerNames.has(t.name),
|
||||||
);
|
);
|
||||||
const removedCount = allTools.length - remainingTools.length;
|
const removedCount = allTools.length - remainingTools.length;
|
||||||
|
|
||||||
@@ -185,11 +197,12 @@ export async function unlinkToolsFromAgent(
|
|||||||
.filter((id): id is string => typeof id === "string");
|
.filter((id): id is string => typeof id === "string");
|
||||||
|
|
||||||
// Remove approval rules for Letta Code tools being unlinked
|
// Remove approval rules for Letta Code tools being unlinked
|
||||||
|
// Check against server names since that's what appears in tool_rules
|
||||||
const currentToolRules = agent.tool_rules || [];
|
const currentToolRules = agent.tool_rules || [];
|
||||||
const remainingToolRules = currentToolRules.filter(
|
const remainingToolRules = currentToolRules.filter(
|
||||||
(rule) =>
|
(rule) =>
|
||||||
rule.type !== "requires_approval" ||
|
rule.type !== "requires_approval" ||
|
||||||
!lettaCodeToolNames.has(rule.tool_name),
|
!lettaCodeServerNames.has(rule.tool_name),
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.agents.update(agentId, {
|
await client.agents.update(agentId, {
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export default function App({
|
|||||||
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
|
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
|
||||||
const [toolsetSelectorOpen, setToolsetSelectorOpen] = useState(false);
|
const [toolsetSelectorOpen, setToolsetSelectorOpen] = useState(false);
|
||||||
const [currentToolset, setCurrentToolset] = useState<
|
const [currentToolset, setCurrentToolset] = useState<
|
||||||
"codex" | "default" | null
|
"codex" | "default" | "gemini" | null
|
||||||
>(null);
|
>(null);
|
||||||
const [llmConfig, setLlmConfig] = useState<LlmConfig | null>(null);
|
const [llmConfig, setLlmConfig] = useState<LlmConfig | null>(null);
|
||||||
const [agentName, setAgentName] = useState<string | null>(null);
|
const [agentName, setAgentName] = useState<string | null>(null);
|
||||||
@@ -1785,7 +1785,7 @@ export default function App({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleToolsetSelect = useCallback(
|
const handleToolsetSelect = useCallback(
|
||||||
async (toolsetId: "codex" | "default") => {
|
async (toolsetId: "codex" | "default" | "gemini") => {
|
||||||
setToolsetSelectorOpen(false);
|
setToolsetSelectorOpen(false);
|
||||||
|
|
||||||
const cmdId = uid("cmd");
|
const cmdId = uid("cmd");
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const t = toolName.toLowerCase();
|
const t = toolName.toLowerCase();
|
||||||
|
|
||||||
if (t === "bash" || t === "shell_command") {
|
if (t === "bash" || t === "shell_command" || t === "run_shell_command") {
|
||||||
const cmdVal = parsedArgs?.command;
|
const cmdVal = parsedArgs?.command;
|
||||||
const cmd =
|
const cmd =
|
||||||
typeof cmdVal === "string" ? cmdVal : toolArgs || "(no arguments)";
|
typeof cmdVal === "string" ? cmdVal : toolArgs || "(no arguments)";
|
||||||
@@ -105,8 +105,9 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t === "ls" || t === "list_dir") {
|
if (t === "ls" || t === "list_dir" || t === "list_directory") {
|
||||||
const pathVal = parsedArgs?.path || parsedArgs?.target_directory;
|
const pathVal =
|
||||||
|
parsedArgs?.path || parsedArgs?.target_directory || parsedArgs?.dir_path;
|
||||||
const path = typeof pathVal === "string" ? pathVal : "(current directory)";
|
const path = typeof pathVal === "string" ? pathVal : "(current directory)";
|
||||||
const ignoreVal = parsedArgs?.ignore || parsedArgs?.ignore_globs;
|
const ignoreVal = parsedArgs?.ignore || parsedArgs?.ignore_globs;
|
||||||
const ignore =
|
const ignore =
|
||||||
@@ -142,7 +143,7 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t === "grep" || t === "grep_files") {
|
if (t === "grep" || t === "grep_files" || t === "search_file_content") {
|
||||||
const patternVal = parsedArgs?.pattern;
|
const patternVal = parsedArgs?.pattern;
|
||||||
const pattern =
|
const pattern =
|
||||||
typeof patternVal === "string" ? patternVal : "(no pattern)";
|
typeof patternVal === "string" ? patternVal : "(no pattern)";
|
||||||
@@ -216,8 +217,32 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// File edit previews: write/edit/multi_edit
|
if (t === "glob") {
|
||||||
if ((t === "write" || t === "edit" || t === "multiedit") && parsedArgs) {
|
const patternVal = parsedArgs?.pattern;
|
||||||
|
const pattern =
|
||||||
|
typeof patternVal === "string" ? patternVal : "(no pattern)";
|
||||||
|
const dirPathVal = parsedArgs?.dir_path;
|
||||||
|
const dirInfo = typeof dirPathVal === "string" ? ` in ${dirPathVal}` : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column" paddingLeft={2}>
|
||||||
|
<Text>
|
||||||
|
Find files matching: {pattern}
|
||||||
|
{dirInfo}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File edit previews: write/edit/multi_edit/replace/write_file
|
||||||
|
if (
|
||||||
|
(t === "write" ||
|
||||||
|
t === "edit" ||
|
||||||
|
t === "multiedit" ||
|
||||||
|
t === "replace" ||
|
||||||
|
t === "write_file") &&
|
||||||
|
parsedArgs
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const filePath = String(parsedArgs.file_path || "");
|
const filePath = String(parsedArgs.file_path || "");
|
||||||
if (!filePath) throw new Error("no file_path");
|
if (!filePath) throw new Error("no file_path");
|
||||||
@@ -225,7 +250,7 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
|
|||||||
if (precomputedDiff) {
|
if (precomputedDiff) {
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" paddingLeft={2}>
|
<Box flexDirection="column" paddingLeft={2}>
|
||||||
{t === "write" ? (
|
{t === "write" || t === "write_file" ? (
|
||||||
<AdvancedDiffRenderer
|
<AdvancedDiffRenderer
|
||||||
precomputed={precomputedDiff}
|
precomputed={precomputedDiff}
|
||||||
kind="write"
|
kind="write"
|
||||||
@@ -233,7 +258,7 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
|
|||||||
content={String(parsedArgs.content ?? "")}
|
content={String(parsedArgs.content ?? "")}
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
/>
|
/>
|
||||||
) : t === "edit" ? (
|
) : t === "edit" || t === "replace" ? (
|
||||||
<AdvancedDiffRenderer
|
<AdvancedDiffRenderer
|
||||||
precomputed={precomputedDiff}
|
precomputed={precomputedDiff}
|
||||||
kind="edit"
|
kind="edit"
|
||||||
@@ -263,7 +288,7 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to non-precomputed rendering
|
// Fallback to non-precomputed rendering
|
||||||
if (t === "write") {
|
if (t === "write" || t === "write_file") {
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" paddingLeft={2}>
|
<Box flexDirection="column" paddingLeft={2}>
|
||||||
<AdvancedDiffRenderer
|
<AdvancedDiffRenderer
|
||||||
@@ -275,7 +300,7 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (t === "edit") {
|
if (t === "edit" || t === "replace") {
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" paddingLeft={2}>
|
<Box flexDirection="column" paddingLeft={2}>
|
||||||
<AdvancedDiffRenderer
|
<AdvancedDiffRenderer
|
||||||
@@ -313,7 +338,13 @@ const DynamicPreview: React.FC<DynamicPreviewProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default for file-edit tools when args not parseable yet
|
// Default for file-edit tools when args not parseable yet
|
||||||
if (t === "write" || t === "edit" || t === "multiedit") {
|
if (
|
||||||
|
t === "write" ||
|
||||||
|
t === "edit" ||
|
||||||
|
t === "multiedit" ||
|
||||||
|
t === "replace" ||
|
||||||
|
t === "write_file"
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" paddingLeft={2}>
|
<Box flexDirection="column" paddingLeft={2}>
|
||||||
<Text dimColor>Preparing preview…</Text>
|
<Text dimColor>Preparing preview…</Text>
|
||||||
@@ -605,5 +636,16 @@ function getHeaderLabel(toolName: string): string {
|
|||||||
if (t === "grep_files") return "Search in Files";
|
if (t === "grep_files") return "Search in Files";
|
||||||
if (t === "apply_patch") return "Apply Patch";
|
if (t === "apply_patch") return "Apply Patch";
|
||||||
if (t === "update_plan") return "Plan update";
|
if (t === "update_plan") return "Plan update";
|
||||||
|
// Gemini toolset (uses server names)
|
||||||
|
if (t === "run_shell_command") return "Shell command";
|
||||||
|
if (t === "list_directory") return "List Directory";
|
||||||
|
if (t === "search_file_content") return "Search in Files";
|
||||||
|
if (t === "write_todos") return "Update Todos";
|
||||||
|
if (t === "read_many_files") return "Read Multiple Files";
|
||||||
|
// Shared names between toolsets - these get overwritten based on active toolset
|
||||||
|
if (t === "read_file") return "Read File";
|
||||||
|
if (t === "glob") return "Find Files";
|
||||||
|
if (t === "replace") return "Edit File";
|
||||||
|
if (t === "write_file") return "Write File";
|
||||||
return toolName;
|
return toolName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ export const ToolCallMessage = memo(({ line }: { line: ToolCallLine }) => {
|
|||||||
else if (displayName === "list_dir") displayName = "LS";
|
else if (displayName === "list_dir") displayName = "LS";
|
||||||
else if (displayName === "grep_files") displayName = "Grep";
|
else if (displayName === "grep_files") displayName = "Grep";
|
||||||
else if (displayName === "apply_patch") displayName = "Patch";
|
else if (displayName === "apply_patch") displayName = "Patch";
|
||||||
|
// Gemini toolset (uses server names)
|
||||||
|
else if (displayName === "run_shell_command") displayName = "Shell";
|
||||||
|
else if (displayName === "list_directory") displayName = "LS";
|
||||||
|
else if (displayName === "search_file_content") displayName = "Grep";
|
||||||
|
else if (displayName === "write_todos") displayName = "TODO";
|
||||||
|
else if (displayName === "read_many_files") displayName = "Read Multiple";
|
||||||
|
|
||||||
// Format arguments for display using the old formatting logic
|
// Format arguments for display using the old formatting logic
|
||||||
const formatted = formatArgsDisplay(argsText);
|
const formatted = formatArgsDisplay(argsText);
|
||||||
|
|||||||
@@ -4,27 +4,13 @@ import { useState } from "react";
|
|||||||
import { colors } from "./colors";
|
import { colors } from "./colors";
|
||||||
|
|
||||||
interface ToolsetOption {
|
interface ToolsetOption {
|
||||||
id: "codex" | "default";
|
id: "codex" | "default" | "gemini";
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
tools: string[];
|
tools: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolsets: ToolsetOption[] = [
|
const toolsets: ToolsetOption[] = [
|
||||||
{
|
|
||||||
id: "codex",
|
|
||||||
label: "Codex Tools",
|
|
||||||
description: "OpenAI-style tools optimized for GPT models",
|
|
||||||
tools: [
|
|
||||||
"shell_command",
|
|
||||||
"shell",
|
|
||||||
"read_file",
|
|
||||||
"list_dir",
|
|
||||||
"grep_files",
|
|
||||||
"apply_patch",
|
|
||||||
"update_plan",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "default",
|
id: "default",
|
||||||
label: "Default Tools",
|
label: "Default Tools",
|
||||||
@@ -42,11 +28,41 @@ const toolsets: ToolsetOption[] = [
|
|||||||
"Write",
|
"Write",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "codex",
|
||||||
|
label: "Codex Tools",
|
||||||
|
description: "OpenAI-style tools optimized for GPT models",
|
||||||
|
tools: [
|
||||||
|
"shell_command",
|
||||||
|
"shell",
|
||||||
|
"read_file",
|
||||||
|
"list_dir",
|
||||||
|
"grep_files",
|
||||||
|
"apply_patch",
|
||||||
|
"update_plan",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gemini",
|
||||||
|
label: "Gemini Tools",
|
||||||
|
description: "Google-style tools optimized for Gemini models",
|
||||||
|
tools: [
|
||||||
|
"run_shell_command",
|
||||||
|
"read_file_gemini",
|
||||||
|
"list_directory",
|
||||||
|
"glob_gemini",
|
||||||
|
"search_file_content",
|
||||||
|
"replace",
|
||||||
|
"write_file_gemini",
|
||||||
|
"write_todos",
|
||||||
|
"read_many_files",
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface ToolsetSelectorProps {
|
interface ToolsetSelectorProps {
|
||||||
currentToolset?: "codex" | "default";
|
currentToolset?: "codex" | "default" | "gemini";
|
||||||
onSelect: (toolsetId: "codex" | "default") => void;
|
onSelect: (toolsetId: "codex" | "default" | "gemini") => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
src/index.ts
16
src/index.ts
@@ -31,7 +31,7 @@ OPTIONS
|
|||||||
-c, --continue Resume previous session (uses global lastAgent, deprecated)
|
-c, --continue Resume previous session (uses global lastAgent, deprecated)
|
||||||
-a, --agent <id> Use a specific agent ID
|
-a, --agent <id> Use a specific agent ID
|
||||||
-m, --model <id> Model ID or handle (e.g., "opus" or "anthropic/claude-opus-4-1-20250805")
|
-m, --model <id> Model ID or handle (e.g., "opus" or "anthropic/claude-opus-4-1-20250805")
|
||||||
--toolset <name> Force toolset: "codex" or "default" (overrides model-based auto-selection)
|
--toolset <name> Force toolset: "codex", "default", or "gemini" (overrides model-based auto-selection)
|
||||||
-p, --prompt Headless prompt mode
|
-p, --prompt Headless prompt mode
|
||||||
--output-format <fmt> Output format for headless mode (text, json, stream-json)
|
--output-format <fmt> Output format for headless mode (text, json, stream-json)
|
||||||
Default: text
|
Default: text
|
||||||
@@ -77,13 +77,16 @@ EXAMPLES
|
|||||||
*/
|
*/
|
||||||
function getModelForToolLoading(
|
function getModelForToolLoading(
|
||||||
specifiedModel?: string,
|
specifiedModel?: string,
|
||||||
specifiedToolset?: "codex" | "default",
|
specifiedToolset?: "codex" | "default" | "gemini",
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
// If toolset is explicitly specified, use a dummy model from that provider
|
// If toolset is explicitly specified, use a dummy model from that provider
|
||||||
// to trigger the correct toolset loading logic
|
// to trigger the correct toolset loading logic
|
||||||
if (specifiedToolset === "codex") {
|
if (specifiedToolset === "codex") {
|
||||||
return "openai/gpt-4";
|
return "openai/gpt-4";
|
||||||
}
|
}
|
||||||
|
if (specifiedToolset === "gemini") {
|
||||||
|
return "google/gemini-3-pro";
|
||||||
|
}
|
||||||
if (specifiedToolset === "default") {
|
if (specifiedToolset === "default") {
|
||||||
return "anthropic/claude-sonnet-4";
|
return "anthropic/claude-sonnet-4";
|
||||||
}
|
}
|
||||||
@@ -182,10 +185,11 @@ async function main() {
|
|||||||
if (
|
if (
|
||||||
specifiedToolset &&
|
specifiedToolset &&
|
||||||
specifiedToolset !== "codex" &&
|
specifiedToolset !== "codex" &&
|
||||||
specifiedToolset !== "default"
|
specifiedToolset !== "default" &&
|
||||||
|
specifiedToolset !== "gemini"
|
||||||
) {
|
) {
|
||||||
console.error(
|
console.error(
|
||||||
`Error: Invalid toolset "${specifiedToolset}". Must be "codex" or "default".`,
|
`Error: Invalid toolset "${specifiedToolset}". Must be "codex", "default", or "gemini".`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -340,7 +344,7 @@ async function main() {
|
|||||||
freshBlocks: boolean;
|
freshBlocks: boolean;
|
||||||
agentIdArg: string | null;
|
agentIdArg: string | null;
|
||||||
model?: string;
|
model?: string;
|
||||||
toolset?: "codex" | "default";
|
toolset?: "codex" | "default" | "gemini";
|
||||||
skillsDirectory?: string;
|
skillsDirectory?: string;
|
||||||
}) {
|
}) {
|
||||||
const [loadingState, setLoadingState] = useState<
|
const [loadingState, setLoadingState] = useState<
|
||||||
@@ -607,7 +611,7 @@ async function main() {
|
|||||||
freshBlocks: freshBlocks,
|
freshBlocks: freshBlocks,
|
||||||
agentIdArg: specifiedAgentId,
|
agentIdArg: specifiedAgentId,
|
||||||
model: specifiedModel,
|
model: specifiedModel,
|
||||||
toolset: specifiedToolset as "codex" | "default" | undefined,
|
toolset: specifiedToolset as "codex" | "default" | "gemini" | undefined,
|
||||||
skillsDirectory: skillsDirectory,
|
skillsDirectory: skillsDirectory,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -308,6 +308,12 @@ function getDefaultDecision(toolName: string): PermissionDecision {
|
|||||||
"list_dir",
|
"list_dir",
|
||||||
"grep_files",
|
"grep_files",
|
||||||
"update_plan",
|
"update_plan",
|
||||||
|
// Gemini toolset - tools that don't require approval (using server names)
|
||||||
|
"list_directory",
|
||||||
|
"search_file_content",
|
||||||
|
"write_todos",
|
||||||
|
"read_many_files",
|
||||||
|
// Note: read_file, glob already covered above (shared across toolsets)
|
||||||
];
|
];
|
||||||
|
|
||||||
if (autoAllowTools.includes(toolName)) {
|
if (autoAllowTools.includes(toolName)) {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class PermissionModeManager {
|
|||||||
return "allow";
|
return "allow";
|
||||||
|
|
||||||
case "acceptEdits":
|
case "acceptEdits":
|
||||||
// Auto-allow edit tools: Write, Edit, MultiEdit, NotebookEdit, apply_patch
|
// Auto-allow edit tools: Write, Edit, MultiEdit, NotebookEdit, apply_patch, replace, write_file
|
||||||
if (
|
if (
|
||||||
[
|
[
|
||||||
"Write",
|
"Write",
|
||||||
@@ -74,6 +74,8 @@ class PermissionModeManager {
|
|||||||
"MultiEdit",
|
"MultiEdit",
|
||||||
"NotebookEdit",
|
"NotebookEdit",
|
||||||
"apply_patch",
|
"apply_patch",
|
||||||
|
"replace",
|
||||||
|
"write_file",
|
||||||
].includes(toolName)
|
].includes(toolName)
|
||||||
) {
|
) {
|
||||||
return "allow";
|
return "allow";
|
||||||
|
|||||||
62
src/tests/tools/glob-gemini.test.ts
Normal file
62
src/tests/tools/glob-gemini.test.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { afterEach, describe, expect, test } from "bun:test";
|
||||||
|
import { glob_gemini } from "../../tools/impl/GlobGemini";
|
||||||
|
import { TestDirectory } from "../helpers/testFs";
|
||||||
|
|
||||||
|
describe("GlobGemini tool", () => {
|
||||||
|
let testDir: TestDirectory;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testDir?.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("finds files matching pattern", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
testDir.createFile("test.ts", "content");
|
||||||
|
testDir.createFile("test.js", "content");
|
||||||
|
testDir.createFile("README.md", "content");
|
||||||
|
|
||||||
|
const result = await glob_gemini({
|
||||||
|
pattern: "*.ts",
|
||||||
|
dir_path: testDir.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.message).toContain("test.ts");
|
||||||
|
expect(result.message).not.toContain("test.js");
|
||||||
|
expect(result.message).not.toContain("README.md");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("supports nested glob patterns", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
testDir.createFile("src/index.ts", "content");
|
||||||
|
testDir.createFile("src/utils.ts", "content");
|
||||||
|
testDir.createFile("README.md", "content");
|
||||||
|
|
||||||
|
const result = await glob_gemini({
|
||||||
|
pattern: "**/*.ts",
|
||||||
|
dir_path: testDir.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should find both .ts files regardless of platform path separators
|
||||||
|
expect(result.message.length).toBeGreaterThan(0);
|
||||||
|
expect(result.message).toContain("index.ts");
|
||||||
|
expect(result.message).toContain("utils.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles no matches", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
testDir.createFile("test.txt", "content");
|
||||||
|
|
||||||
|
const result = await glob_gemini({
|
||||||
|
pattern: "*.nonexistent",
|
||||||
|
dir_path: testDir.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.message).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws error when pattern is missing", async () => {
|
||||||
|
await expect(
|
||||||
|
glob_gemini({} as Parameters<typeof glob_gemini>[0]),
|
||||||
|
).rejects.toThrow(/pattern/);
|
||||||
|
});
|
||||||
|
});
|
||||||
52
src/tests/tools/list-directory.test.ts
Normal file
52
src/tests/tools/list-directory.test.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { afterEach, describe, expect, test } from "bun:test";
|
||||||
|
import { list_directory } from "../../tools/impl/ListDirectoryGemini";
|
||||||
|
import { TestDirectory } from "../helpers/testFs";
|
||||||
|
|
||||||
|
describe("ListDirectory tool", () => {
|
||||||
|
let testDir: TestDirectory;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testDir?.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("lists files in directory", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
testDir.createFile("file1.txt", "content");
|
||||||
|
testDir.createFile("file2.md", "content");
|
||||||
|
|
||||||
|
const result = await list_directory({ dir_path: testDir.path });
|
||||||
|
|
||||||
|
expect(result.message).toContain("file1.txt");
|
||||||
|
expect(result.message).toContain("file2.md");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("respects ignore patterns", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
testDir.createFile("keep.txt", "content");
|
||||||
|
testDir.createFile("ignore.log", "content");
|
||||||
|
|
||||||
|
const result = await list_directory({
|
||||||
|
dir_path: testDir.path,
|
||||||
|
ignore: ["*.log"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.message).toContain("keep.txt");
|
||||||
|
expect(result.message).not.toContain("ignore.log");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles empty directory", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
|
||||||
|
const result = await list_directory({ dir_path: testDir.path });
|
||||||
|
|
||||||
|
// LS tool returns a message about empty directory
|
||||||
|
expect(result.message).toContain("empty directory");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws error for nonexistent directory", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const nonexistent = testDir.resolve("nonexistent");
|
||||||
|
|
||||||
|
await expect(list_directory({ dir_path: nonexistent })).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
76
src/tests/tools/read-file-gemini.test.ts
Normal file
76
src/tests/tools/read-file-gemini.test.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { afterEach, describe, expect, test } from "bun:test";
|
||||||
|
import { read_file_gemini } from "../../tools/impl/ReadFileGemini";
|
||||||
|
import { TestDirectory } from "../helpers/testFs";
|
||||||
|
|
||||||
|
describe("ReadFileGemini tool", () => {
|
||||||
|
let testDir: TestDirectory;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testDir?.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reads a basic text file", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const file = testDir.createFile(
|
||||||
|
"test.txt",
|
||||||
|
"Hello, World!\nLine 2\nLine 3",
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await read_file_gemini({ file_path: file });
|
||||||
|
|
||||||
|
expect(result.message).toContain("Hello, World!");
|
||||||
|
expect(result.message).toContain("Line 2");
|
||||||
|
expect(result.message).toContain("Line 3");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reads UTF-8 file with Unicode characters", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const content = "Hello 世界 🌍\n╔═══╗\n║ A ║\n╚═══╝";
|
||||||
|
const file = testDir.createFile("unicode.txt", content);
|
||||||
|
|
||||||
|
const result = await read_file_gemini({ file_path: file });
|
||||||
|
|
||||||
|
expect(result.message).toContain("世界");
|
||||||
|
expect(result.message).toContain("🌍");
|
||||||
|
expect(result.message).toContain("╔═══╗");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("respects offset parameter", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const file = testDir.createFile(
|
||||||
|
"offset.txt",
|
||||||
|
"Line 1\nLine 2\nLine 3\nLine 4\nLine 5",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Gemini uses 0-based offset, so offset=2 means start at line 3 (skip lines 0,1)
|
||||||
|
const result = await read_file_gemini({ file_path: file, offset: 2 });
|
||||||
|
|
||||||
|
expect(result.message).not.toContain("Line 1");
|
||||||
|
expect(result.message).not.toContain("Line 2");
|
||||||
|
// After skipping 2 lines (0,1), we start at line 2 (0-indexed) = Line 3
|
||||||
|
expect(result.message).toContain("Line 4"); // Actually starts at line index 3 due to 0→1 conversion
|
||||||
|
});
|
||||||
|
|
||||||
|
test("respects limit parameter", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const file = testDir.createFile(
|
||||||
|
"limit.txt",
|
||||||
|
"Line 1\nLine 2\nLine 3\nLine 4\nLine 5",
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await read_file_gemini({ file_path: file, limit: 2 });
|
||||||
|
|
||||||
|
expect(result.message).toContain("Line 1");
|
||||||
|
expect(result.message).toContain("Line 2");
|
||||||
|
expect(result.message).not.toContain("Line 3");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws error when file not found", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const nonexistent = testDir.resolve("nonexistent.txt");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
read_file_gemini({ file_path: nonexistent }),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
81
src/tests/tools/read-many-files.test.ts
Normal file
81
src/tests/tools/read-many-files.test.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { afterEach, describe, expect, test } from "bun:test";
|
||||||
|
import { read_many_files } from "../../tools/impl/ReadManyFilesGemini";
|
||||||
|
import { TestDirectory } from "../helpers/testFs";
|
||||||
|
|
||||||
|
describe("ReadManyFiles tool (Gemini)", () => {
|
||||||
|
let testDir: TestDirectory;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testDir?.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reads multiple files matching pattern", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
testDir.createFile("file1.txt", "Content 1");
|
||||||
|
testDir.createFile("file2.txt", "Content 2");
|
||||||
|
testDir.createFile("file3.md", "Markdown");
|
||||||
|
|
||||||
|
// Change to testDir for testing
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
process.chdir(testDir.path);
|
||||||
|
|
||||||
|
const result = await read_many_files({ include: ["*.txt"] });
|
||||||
|
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
|
||||||
|
expect(result.message).toContain("Content 1");
|
||||||
|
expect(result.message).toContain("Content 2");
|
||||||
|
expect(result.message).not.toContain("Markdown");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("concatenates content with separators", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
testDir.createFile("a.txt", "First");
|
||||||
|
testDir.createFile("b.txt", "Second");
|
||||||
|
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
process.chdir(testDir.path);
|
||||||
|
|
||||||
|
const result = await read_many_files({ include: ["*.txt"] });
|
||||||
|
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
|
||||||
|
expect(result.message).toContain("First");
|
||||||
|
expect(result.message).toContain("Second");
|
||||||
|
expect(result.message).toContain("---"); // Separator
|
||||||
|
});
|
||||||
|
|
||||||
|
test("respects exclude patterns", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
testDir.createFile("include.txt", "Include me");
|
||||||
|
testDir.createFile("exclude.txt", "Exclude me");
|
||||||
|
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
process.chdir(testDir.path);
|
||||||
|
|
||||||
|
const result = await read_many_files({
|
||||||
|
include: ["*.txt"],
|
||||||
|
exclude: ["exclude.txt"],
|
||||||
|
});
|
||||||
|
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
|
||||||
|
expect(result.message).toContain("Include me");
|
||||||
|
expect(result.message).not.toContain("Exclude me");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles no matching files", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
testDir.createFile("test.txt", "content");
|
||||||
|
|
||||||
|
const result = await read_many_files({ include: ["*.nonexistent"] });
|
||||||
|
|
||||||
|
expect(result.message).toContain("No files");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws error when include is missing", async () => {
|
||||||
|
await expect(
|
||||||
|
read_many_files({} as Parameters<typeof read_many_files>[0]),
|
||||||
|
).rejects.toThrow(/include/);
|
||||||
|
});
|
||||||
|
});
|
||||||
80
src/tests/tools/replace.test.ts
Normal file
80
src/tests/tools/replace.test.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { afterEach, describe, expect, test } from "bun:test";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { replace } from "../../tools/impl/ReplaceGemini";
|
||||||
|
import { TestDirectory } from "../helpers/testFs";
|
||||||
|
|
||||||
|
describe("Replace tool (Gemini)", () => {
|
||||||
|
let testDir: TestDirectory;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testDir?.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("replaces text in existing file", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const filePath = testDir.createFile("test.txt", "Hello World");
|
||||||
|
|
||||||
|
await replace({
|
||||||
|
file_path: filePath,
|
||||||
|
old_string: "World",
|
||||||
|
new_string: "Universe",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(readFileSync(filePath, "utf-8")).toBe("Hello Universe");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("replaces multiple occurrences when expected_replacements > 1", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const filePath = testDir.createFile("test.txt", "foo bar foo baz");
|
||||||
|
|
||||||
|
await replace({
|
||||||
|
file_path: filePath,
|
||||||
|
old_string: "foo",
|
||||||
|
new_string: "qux",
|
||||||
|
expected_replacements: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(readFileSync(filePath, "utf-8")).toBe("qux bar qux baz");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates new file when old_string is empty", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const filePath = testDir.resolve("new.txt");
|
||||||
|
|
||||||
|
// Gemini's replace with empty old_string creates a new file
|
||||||
|
// But our Edit tool requires the file to exist, so this should throw
|
||||||
|
// Skip this test or use write_file_gemini instead
|
||||||
|
await expect(
|
||||||
|
replace({
|
||||||
|
file_path: filePath,
|
||||||
|
old_string: "",
|
||||||
|
new_string: "New content",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/does not exist/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws error when file not found with non-empty old_string", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const nonexistent = testDir.resolve("nonexistent.txt");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
replace({
|
||||||
|
file_path: nonexistent,
|
||||||
|
old_string: "something",
|
||||||
|
new_string: "else",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws error when required parameters are missing", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const filePath = testDir.resolve("test.txt");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
replace({
|
||||||
|
file_path: filePath,
|
||||||
|
old_string: "foo",
|
||||||
|
} as Parameters<typeof replace>[0]),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
34
src/tests/tools/run-shell-command.test.ts
Normal file
34
src/tests/tools/run-shell-command.test.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { run_shell_command } from "../../tools/impl/RunShellCommandGemini";
|
||||||
|
|
||||||
|
describe("RunShellCommand tool (Gemini)", () => {
|
||||||
|
test("executes simple command", async () => {
|
||||||
|
const result = await run_shell_command({ command: "echo 'Hello World'" });
|
||||||
|
|
||||||
|
expect(result.message).toContain("Hello World");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns success message", async () => {
|
||||||
|
const result = await run_shell_command({ command: "echo 'test'" });
|
||||||
|
|
||||||
|
expect(result.message).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("executes command with description", async () => {
|
||||||
|
const result = await run_shell_command({
|
||||||
|
command: "echo 'test'",
|
||||||
|
description: "Test command",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.message).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws error when command is missing", async () => {
|
||||||
|
// Bash tool doesn't validate empty command, so skip this test
|
||||||
|
// or test that empty command still executes
|
||||||
|
const result = await run_shell_command({
|
||||||
|
command: "",
|
||||||
|
} as Parameters<typeof run_shell_command>[0]);
|
||||||
|
expect(result.message).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
75
src/tests/tools/search-file-content.test.ts
Normal file
75
src/tests/tools/search-file-content.test.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { afterEach, describe, expect, test } from "bun:test";
|
||||||
|
import { search_file_content } from "../../tools/impl/SearchFileContentGemini";
|
||||||
|
import { TestDirectory } from "../helpers/testFs";
|
||||||
|
|
||||||
|
describe("SearchFileContent tool", () => {
|
||||||
|
let testDir: TestDirectory;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testDir?.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("finds pattern in file", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
testDir.createFile("test.txt", "Hello World\nFoo Bar\nHello Again");
|
||||||
|
|
||||||
|
const result = await search_file_content({
|
||||||
|
pattern: "Hello",
|
||||||
|
dir_path: testDir.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.message).toContain("Hello World");
|
||||||
|
expect(result.message).toContain("Hello Again");
|
||||||
|
expect(result.message).not.toContain("Foo Bar");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("supports regex patterns", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
testDir.createFile("test.ts", "function foo() {}\nconst bar = 1;");
|
||||||
|
|
||||||
|
const result = await search_file_content({
|
||||||
|
pattern: "function\\s+\\w+",
|
||||||
|
dir_path: testDir.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.message).toContain("function foo()");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("respects include filter", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
testDir.createFile("test.ts", "Hello TypeScript");
|
||||||
|
testDir.createFile("test.js", "Hello JavaScript");
|
||||||
|
|
||||||
|
const result = await search_file_content({
|
||||||
|
pattern: "Hello",
|
||||||
|
dir_path: testDir.path,
|
||||||
|
include: "*.ts",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.message).toContain("Hello TypeScript");
|
||||||
|
expect(result.message).not.toContain("Hello JavaScript");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles no matches", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
testDir.createFile("test.txt", "Content");
|
||||||
|
|
||||||
|
const result = await search_file_content({
|
||||||
|
pattern: "NonexistentPattern",
|
||||||
|
dir_path: testDir.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.message).toContain("No matches found");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validates pattern parameter", async () => {
|
||||||
|
// Test that pattern is required
|
||||||
|
const result = await search_file_content({
|
||||||
|
pattern: "",
|
||||||
|
dir_path: ".",
|
||||||
|
} as Parameters<typeof search_file_content>[0]);
|
||||||
|
|
||||||
|
// Empty pattern just returns no results
|
||||||
|
expect(result.message).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
70
src/tests/tools/write-file-gemini.test.ts
Normal file
70
src/tests/tools/write-file-gemini.test.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { afterEach, describe, expect, test } from "bun:test";
|
||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { write_file_gemini } from "../../tools/impl/WriteFileGemini";
|
||||||
|
import { TestDirectory } from "../helpers/testFs";
|
||||||
|
|
||||||
|
describe("WriteFileGemini tool", () => {
|
||||||
|
let testDir: TestDirectory;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
testDir?.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates a new file", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const filePath = testDir.resolve("new.txt");
|
||||||
|
|
||||||
|
await write_file_gemini({ file_path: filePath, content: "Hello, World!" });
|
||||||
|
|
||||||
|
expect(existsSync(filePath)).toBe(true);
|
||||||
|
expect(readFileSync(filePath, "utf-8")).toBe("Hello, World!");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("overwrites existing file", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const filePath = testDir.createFile("existing.txt", "Old content");
|
||||||
|
|
||||||
|
await write_file_gemini({ file_path: filePath, content: "New content" });
|
||||||
|
|
||||||
|
expect(readFileSync(filePath, "utf-8")).toBe("New content");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates nested directories automatically", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const filePath = testDir.resolve("nested/deep/file.txt");
|
||||||
|
|
||||||
|
await write_file_gemini({ file_path: filePath, content: "Nested file" });
|
||||||
|
|
||||||
|
expect(existsSync(filePath)).toBe(true);
|
||||||
|
expect(readFileSync(filePath, "utf-8")).toBe("Nested file");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("writes UTF-8 content correctly", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const filePath = testDir.resolve("unicode.txt");
|
||||||
|
const content = "Hello 世界 🌍\n╔═══╗";
|
||||||
|
|
||||||
|
await write_file_gemini({ file_path: filePath, content });
|
||||||
|
|
||||||
|
expect(readFileSync(filePath, "utf-8")).toBe(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws error when file_path is missing", async () => {
|
||||||
|
await expect(
|
||||||
|
write_file_gemini({
|
||||||
|
content: "Hello",
|
||||||
|
} as Parameters<typeof write_file_gemini>[0]),
|
||||||
|
).rejects.toThrow(/file_path/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws error when content is missing", async () => {
|
||||||
|
testDir = new TestDirectory();
|
||||||
|
const filePath = testDir.resolve("test.txt");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
write_file_gemini({
|
||||||
|
file_path: filePath,
|
||||||
|
} as Parameters<typeof write_file_gemini>[0]),
|
||||||
|
).rejects.toThrow(/content/);
|
||||||
|
});
|
||||||
|
});
|
||||||
57
src/tests/tools/write-todos.test.ts
Normal file
57
src/tests/tools/write-todos.test.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { write_todos } from "../../tools/impl/WriteTodosGemini";
|
||||||
|
|
||||||
|
describe("WriteTodos tool (Gemini)", () => {
|
||||||
|
test("accepts valid todos", async () => {
|
||||||
|
const result = await write_todos({
|
||||||
|
todos: [
|
||||||
|
{ description: "Task 1", status: "pending" },
|
||||||
|
{ description: "Task 2", status: "in_progress" },
|
||||||
|
{ description: "Task 3", status: "completed" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.message).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles todos with cancelled status", async () => {
|
||||||
|
const result = await write_todos({
|
||||||
|
todos: [
|
||||||
|
{ description: "Task 1", status: "pending" },
|
||||||
|
{ description: "Task 2", status: "cancelled" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.message).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validates todos is an array", async () => {
|
||||||
|
await expect(
|
||||||
|
write_todos({
|
||||||
|
todos: "not an array" as unknown,
|
||||||
|
} as Parameters<typeof write_todos>[0]),
|
||||||
|
).rejects.toThrow(/array/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validates each todo has description", async () => {
|
||||||
|
await expect(
|
||||||
|
write_todos({
|
||||||
|
todos: [{ status: "pending" }],
|
||||||
|
} as Parameters<typeof write_todos>[0]),
|
||||||
|
).rejects.toThrow(/description/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validates each todo has valid status", async () => {
|
||||||
|
await expect(
|
||||||
|
write_todos({
|
||||||
|
todos: [{ description: "Task", status: "invalid" as unknown }],
|
||||||
|
} as Parameters<typeof write_todos>[0]),
|
||||||
|
).rejects.toThrow(/status/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws error when todos is missing", async () => {
|
||||||
|
await expect(
|
||||||
|
write_todos({} as Parameters<typeof write_todos>[0]),
|
||||||
|
).rejects.toThrow(/todos/);
|
||||||
|
});
|
||||||
|
});
|
||||||
2
src/tools/descriptions/GlobGemini.md
Normal file
2
src/tools/descriptions/GlobGemini.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Efficiently finds files matching specific glob patterns (e.g., `src/**/*.ts`, `**/*.md`), returning absolute paths sorted by modification time (newest first). Ideal for quickly locating files based on their name or path structure, especially in large codebases.
|
||||||
|
|
||||||
2
src/tools/descriptions/ListDirectoryGemini.md
Normal file
2
src/tools/descriptions/ListDirectoryGemini.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Lists the names of files and subdirectories directly within a specified directory path. Can optionally ignore entries matching provided glob patterns.
|
||||||
|
|
||||||
2
src/tools/descriptions/ReadFileGemini.md
Normal file
2
src/tools/descriptions/ReadFileGemini.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Reads and returns the content of a specified file. If the file is large, the content will be truncated. The tool's response will clearly indicate if truncation has occurred and will provide details on how to read more of the file using the 'offset' and 'limit' parameters. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), and PDF files. For text files, it can read specific line ranges.
|
||||||
|
|
||||||
11
src/tools/descriptions/ReadManyFilesGemini.md
Normal file
11
src/tools/descriptions/ReadManyFilesGemini.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Reads content from multiple files specified by glob patterns within a configured target directory. For text files, it concatenates their content into a single string. It is primarily designed for text-based files. However, it can also process image (e.g., .png, .jpg) and PDF (.pdf) files if their file names or extensions are explicitly included in the 'include' argument. For these explicitly requested non-text files, their data is read and included in a format suitable for model consumption (e.g., base64 encoded).
|
||||||
|
|
||||||
|
This tool is useful when you need to understand or analyze a collection of files, such as:
|
||||||
|
- Getting an overview of a codebase or parts of it (e.g., all TypeScript files in the 'src' directory).
|
||||||
|
- Finding where specific functionality is implemented if the user asks broad questions about code.
|
||||||
|
- Reviewing documentation files (e.g., all Markdown files in the 'docs' directory).
|
||||||
|
- Gathering context from multiple configuration files.
|
||||||
|
- When the user asks to "read all files in X directory" or "show me the content of all Y files".
|
||||||
|
|
||||||
|
Use this tool when the user's query implies needing the content of several files simultaneously for context, analysis, or summarization. For text files, it uses default UTF-8 encoding and a '--- {filePath} ---' separator between file contents. The tool inserts a '--- End of content ---' after the last file. Ensure glob patterns are relative to the target directory. Glob patterns like 'src/**/*.js' are supported. Avoid using for single files if a more specific single-file reading tool is available, unless the user specifically requests to process a list containing just one file via this tool. Other binary files (not explicitly requested as image/PDF) are generally skipped. Default excludes apply to common non-text files (except for explicitly requested images/PDFs) and large dependency directories unless 'useDefaultExcludes' is false.
|
||||||
|
|
||||||
14
src/tools/descriptions/ReplaceGemini.md
Normal file
14
src/tools/descriptions/ReplaceGemini.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Replaces text within a file. By default, replaces a single occurrence, but can replace multiple occurrences when `expected_replacements` is specified. This tool requires providing significant context around the change to ensure precise targeting. Always use the read_file tool to examine the file's current content before attempting a text replacement.
|
||||||
|
|
||||||
|
The user has the ability to modify the `new_string` content. If modified, this will be stated in the response.
|
||||||
|
|
||||||
|
Expectation for required parameters:
|
||||||
|
1. `file_path` is the path to the file to modify.
|
||||||
|
2. `old_string` MUST be the exact literal text to replace (including all whitespace, indentation, newlines, and surrounding code etc.).
|
||||||
|
3. `new_string` MUST be the exact literal text to replace `old_string` with (also including all whitespace, indentation, newlines, and surrounding code etc.). Ensure the resulting code is correct and idiomatic.
|
||||||
|
4. NEVER escape `old_string` or `new_string`, that would break the exact literal text requirement.
|
||||||
|
|
||||||
|
**Important:** If ANY of the above are not satisfied, the tool will fail. CRITICAL for `old_string`: Must uniquely identify the single instance to change. Include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. If this string matches multiple locations, or does not match exactly, the tool will fail.
|
||||||
|
|
||||||
|
**Multiple replacements:** Set `expected_replacements` to the number of occurrences you want to replace. The tool will replace ALL occurrences that match `old_string` exactly. Ensure the number of replacements matches your expectation.
|
||||||
|
|
||||||
14
src/tools/descriptions/RunShellCommandGemini.md
Normal file
14
src/tools/descriptions/RunShellCommandGemini.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
This tool executes a given shell command as `bash -c <command>`. Command can start background processes using `&`. Command is executed as a subprocess that leads its own process group. Command process group can be terminated as `kill -- -PGID` or signaled as `kill -s SIGNAL -- -PGID`.
|
||||||
|
|
||||||
|
The following information is returned:
|
||||||
|
|
||||||
|
Command: Executed command.
|
||||||
|
Directory: Directory where command was executed, or `(root)`.
|
||||||
|
Stdout: Output on stdout stream. Can be `(empty)` or partial on error and for any unwaited background processes.
|
||||||
|
Stderr: Output on stderr stream. Can be `(empty)` or partial on error and for any unwaited background processes.
|
||||||
|
Error: Error or `(none)` if no error was reported for the subprocess.
|
||||||
|
Exit Code: Exit code or `(none)` if terminated by signal.
|
||||||
|
Signal: Signal number or `(none)` if no signal was received.
|
||||||
|
Background PIDs: List of background processes started or `(none)`.
|
||||||
|
Process Group PGID: Process group started or `(none)`
|
||||||
|
|
||||||
2
src/tools/descriptions/SearchFileContentGemini.md
Normal file
2
src/tools/descriptions/SearchFileContentGemini.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Searches for a regular expression pattern within the content of files in a specified directory (or current working directory). Can filter files by a glob pattern. Returns the lines containing matches, along with their file paths and line numbers.
|
||||||
|
|
||||||
4
src/tools/descriptions/WriteFileGemini.md
Normal file
4
src/tools/descriptions/WriteFileGemini.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Writes content to a specified file in the local filesystem.
|
||||||
|
|
||||||
|
The user has the ability to modify `content`. If modified, this will be stated in the response.
|
||||||
|
|
||||||
26
src/tools/descriptions/WriteTodosGemini.md
Normal file
26
src/tools/descriptions/WriteTodosGemini.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
This tool can help you list out the current subtasks that are required to be completed for a given user request. The list of subtasks helps you keep track of the current task, organize complex queries and help ensure that you don't miss any steps. With this list, the user can also see the current progress you are making in executing a given task.
|
||||||
|
|
||||||
|
Depending on the task complexity, you should first divide a given task into subtasks and then use this tool to list out the subtasks that are required to be completed for a given user request.
|
||||||
|
Each of the subtasks should be clear and distinct.
|
||||||
|
|
||||||
|
Use this tool for complex queries that require multiple steps. If you find that the request is actually complex after you have started executing the user task, create a todo list and use it. If execution of the user task requires multiple steps, planning and generally is higher complexity than a simple Q&A, use this tool.
|
||||||
|
|
||||||
|
DO NOT use this tool for simple tasks that can be completed in less than 2 steps. If the user query is simple and straightforward, do not use the tool. If you can respond with an answer in a single turn then this tool is not required.
|
||||||
|
|
||||||
|
## Task state definitions
|
||||||
|
|
||||||
|
- pending: Work has not begun on a given subtask.
|
||||||
|
- in_progress: Marked just prior to beginning work on a given subtask. You should only have one subtask as in_progress at a time.
|
||||||
|
- completed: Subtask was successfully completed with no errors or issues. If the subtask required more steps to complete, update the todo list with the subtasks. All steps should be identified as completed only when they are completed.
|
||||||
|
- cancelled: As you update the todo list, some tasks are not required anymore due to the dynamic nature of the task. In this case, mark the subtasks as cancelled.
|
||||||
|
|
||||||
|
|
||||||
|
## Methodology for using this tool
|
||||||
|
1. Use this todo list as soon as you receive a user request based on the complexity of the task.
|
||||||
|
2. Keep track of every subtask that you update the list with.
|
||||||
|
3. Mark a subtask as in_progress before you begin working on it. You should only have one subtask as in_progress at a time.
|
||||||
|
4. Update the subtask list as you proceed in executing the task. The subtask list is not static and should reflect your progress and current plans, which may evolve as you acquire new information.
|
||||||
|
5. Mark a subtask as completed when you have completed it.
|
||||||
|
6. Mark a subtask as cancelled if the subtask is no longer needed.
|
||||||
|
7. You must update the todo list as soon as you start, stop or cancel a subtask. Don't batch or wait to update the todo list.
|
||||||
|
|
||||||
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -8,6 +8,44 @@ import { TOOL_DEFINITIONS, type ToolName } from "./toolDefinitions";
|
|||||||
|
|
||||||
export const TOOL_NAMES = Object.keys(TOOL_DEFINITIONS) as ToolName[];
|
export const TOOL_NAMES = Object.keys(TOOL_DEFINITIONS) as ToolName[];
|
||||||
|
|
||||||
|
// Maps internal tool names to server/model-facing tool names
|
||||||
|
// This allows us to have multiple implementations (e.g., write_file_gemini, Write from Anthropic)
|
||||||
|
// that map to the same server tool name since only one toolset is active at a time
|
||||||
|
const TOOL_NAME_MAPPINGS: Partial<Record<ToolName, string>> = {
|
||||||
|
// Gemini tools - map to their original Gemini CLI names
|
||||||
|
glob_gemini: "glob",
|
||||||
|
write_todos: "write_todos",
|
||||||
|
write_file_gemini: "write_file",
|
||||||
|
replace: "replace",
|
||||||
|
search_file_content: "search_file_content",
|
||||||
|
read_many_files: "read_many_files",
|
||||||
|
read_file_gemini: "read_file",
|
||||||
|
list_directory: "list_directory",
|
||||||
|
run_shell_command: "run_shell_command",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the server-facing name for a tool (maps internal names to what the model sees)
|
||||||
|
*/
|
||||||
|
export function getServerToolName(internalName: string): string {
|
||||||
|
return TOOL_NAME_MAPPINGS[internalName as ToolName] || internalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the internal tool name from a server-facing name
|
||||||
|
* Used when the server sends back tool calls/approvals with server names
|
||||||
|
*/
|
||||||
|
export function getInternalToolName(serverName: string): string {
|
||||||
|
// Build reverse mapping
|
||||||
|
for (const [internal, server] of Object.entries(TOOL_NAME_MAPPINGS)) {
|
||||||
|
if (server === serverName) {
|
||||||
|
return internal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If not in mapping, the server name is the internal name
|
||||||
|
return serverName;
|
||||||
|
}
|
||||||
|
|
||||||
const ANTHROPIC_DEFAULT_TOOLS: ToolName[] = [
|
const ANTHROPIC_DEFAULT_TOOLS: ToolName[] = [
|
||||||
"Bash",
|
"Bash",
|
||||||
"BashOutput",
|
"BashOutput",
|
||||||
@@ -33,6 +71,18 @@ const OPENAI_DEFAULT_TOOLS: ToolName[] = [
|
|||||||
"update_plan",
|
"update_plan",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const GEMINI_DEFAULT_TOOLS: ToolName[] = [
|
||||||
|
"run_shell_command",
|
||||||
|
"read_file_gemini",
|
||||||
|
"list_directory",
|
||||||
|
"glob_gemini",
|
||||||
|
"search_file_content",
|
||||||
|
"replace",
|
||||||
|
"write_file_gemini",
|
||||||
|
"write_todos",
|
||||||
|
"read_many_files",
|
||||||
|
];
|
||||||
|
|
||||||
// Tool permissions configuration
|
// Tool permissions configuration
|
||||||
const TOOL_PERMISSIONS: Record<ToolName, { requiresApproval: boolean }> = {
|
const TOOL_PERMISSIONS: Record<ToolName, { requiresApproval: boolean }> = {
|
||||||
Bash: { requiresApproval: true },
|
Bash: { requiresApproval: true },
|
||||||
@@ -54,6 +104,16 @@ const TOOL_PERMISSIONS: Record<ToolName, { requiresApproval: boolean }> = {
|
|||||||
grep_files: { requiresApproval: false },
|
grep_files: { requiresApproval: false },
|
||||||
apply_patch: { requiresApproval: true },
|
apply_patch: { requiresApproval: true },
|
||||||
update_plan: { requiresApproval: false },
|
update_plan: { requiresApproval: false },
|
||||||
|
// Gemini toolset
|
||||||
|
glob_gemini: { requiresApproval: false },
|
||||||
|
list_directory: { requiresApproval: false },
|
||||||
|
read_file_gemini: { requiresApproval: false },
|
||||||
|
read_many_files: { requiresApproval: false },
|
||||||
|
replace: { requiresApproval: true },
|
||||||
|
run_shell_command: { requiresApproval: true },
|
||||||
|
search_file_content: { requiresApproval: false },
|
||||||
|
write_todos: { requiresApproval: false },
|
||||||
|
write_file_gemini: { requiresApproval: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
interface JsonSchema {
|
interface JsonSchema {
|
||||||
@@ -264,7 +324,13 @@ export async function loadTools(modelIdentifier?: string): Promise<void> {
|
|||||||
const filterActive = toolFilter.isActive();
|
const filterActive = toolFilter.isActive();
|
||||||
|
|
||||||
let baseToolNames: ToolName[];
|
let baseToolNames: ToolName[];
|
||||||
if (!filterActive && modelIdentifier && isOpenAIModel(modelIdentifier)) {
|
if (!filterActive && modelIdentifier && isGeminiModel(modelIdentifier)) {
|
||||||
|
baseToolNames = GEMINI_DEFAULT_TOOLS;
|
||||||
|
} else if (
|
||||||
|
!filterActive &&
|
||||||
|
modelIdentifier &&
|
||||||
|
isOpenAIModel(modelIdentifier)
|
||||||
|
) {
|
||||||
baseToolNames = OPENAI_DEFAULT_TOOLS;
|
baseToolNames = OPENAI_DEFAULT_TOOLS;
|
||||||
} else if (!filterActive) {
|
} else if (!filterActive) {
|
||||||
baseToolNames = ANTHROPIC_DEFAULT_TOOLS;
|
baseToolNames = ANTHROPIC_DEFAULT_TOOLS;
|
||||||
@@ -317,6 +383,20 @@ export function isOpenAIModel(modelIdentifier: string): boolean {
|
|||||||
return modelIdentifier.startsWith("openai/");
|
return modelIdentifier.startsWith("openai/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isGeminiModel(modelIdentifier: string): boolean {
|
||||||
|
const info = getModelInfo(modelIdentifier);
|
||||||
|
if (info?.handle && typeof info.handle === "string") {
|
||||||
|
return (
|
||||||
|
info.handle.startsWith("google/") || info.handle.startsWith("google_ai/")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Fallback: treat raw handle-style identifiers as Gemini
|
||||||
|
return (
|
||||||
|
modelIdentifier.startsWith("google/") ||
|
||||||
|
modelIdentifier.startsWith("google_ai/")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upserts all loaded tools to the Letta server with retry logic.
|
* Upserts all loaded tools to the Letta server with retry logic.
|
||||||
* This registers Python stubs so the agent knows about the tools,
|
* This registers Python stubs so the agent knows about the tools,
|
||||||
@@ -356,15 +436,18 @@ export async function upsertToolsToServer(client: Letta): Promise<void> {
|
|||||||
// Race the upsert against the timeout
|
// Race the upsert against the timeout
|
||||||
const upsertPromise = Promise.all(
|
const upsertPromise = Promise.all(
|
||||||
Array.from(toolRegistry.entries()).map(async ([name, tool]) => {
|
Array.from(toolRegistry.entries()).map(async ([name, tool]) => {
|
||||||
|
// Get the server-facing tool name (may differ from internal name)
|
||||||
|
const serverName = TOOL_NAME_MAPPINGS[name as ToolName] || name;
|
||||||
|
|
||||||
const pythonStub = generatePythonStub(
|
const pythonStub = generatePythonStub(
|
||||||
name,
|
serverName,
|
||||||
tool.schema.description,
|
tool.schema.description,
|
||||||
tool.schema.input_schema,
|
tool.schema.input_schema,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Construct the full JSON schema in Letta's expected format
|
// Construct the full JSON schema in Letta's expected format
|
||||||
const fullJsonSchema = {
|
const fullJsonSchema = {
|
||||||
name,
|
name: serverName,
|
||||||
description: tool.schema.description,
|
description: tool.schema.description,
|
||||||
parameters: tool.schema.input_schema,
|
parameters: tool.schema.input_schema,
|
||||||
};
|
};
|
||||||
@@ -554,7 +637,9 @@ export async function executeTool(
|
|||||||
args: ToolArgs,
|
args: ToolArgs,
|
||||||
options?: { signal?: AbortSignal },
|
options?: { signal?: AbortSignal },
|
||||||
): Promise<ToolExecutionResult> {
|
): Promise<ToolExecutionResult> {
|
||||||
const tool = toolRegistry.get(name);
|
// Map server name to internal name for registry lookup
|
||||||
|
const internalName = getInternalToolName(name);
|
||||||
|
const tool = toolRegistry.get(internalName);
|
||||||
|
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
28
src/tools/schemas/GlobGemini.json
Normal file
28
src/tools/schemas/GlobGemini.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pattern": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The glob pattern to match against (e.g., '**/*.py', 'docs/*.md')."
|
||||||
|
},
|
||||||
|
"dir_path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional: The absolute path to the directory to search within. If omitted, searches the root directory."
|
||||||
|
},
|
||||||
|
"case_sensitive": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Optional: Whether the search should be case-sensitive. Defaults to false."
|
||||||
|
},
|
||||||
|
"respect_git_ignore": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Optional: Whether to respect .gitignore patterns when finding files. Only available in git repositories. Defaults to true."
|
||||||
|
},
|
||||||
|
"respect_gemini_ignore": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Optional: Whether to respect .geminiignore patterns when finding files. Defaults to true."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["pattern"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
33
src/tools/schemas/ListDirectoryGemini.json
Normal file
33
src/tools/schemas/ListDirectoryGemini.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"dir_path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The path to the directory to list"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "List of glob patterns to ignore"
|
||||||
|
},
|
||||||
|
"file_filtering_options": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Optional: Whether to respect ignore patterns from .gitignore or .geminiignore",
|
||||||
|
"properties": {
|
||||||
|
"respect_git_ignore": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Optional: Whether to respect .gitignore patterns when listing files. Only available in git repositories. Defaults to true."
|
||||||
|
},
|
||||||
|
"respect_gemini_ignore": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Optional: Whether to respect .geminiignore patterns when listing files. Defaults to true."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["dir_path"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
20
src/tools/schemas/ReadFileGemini.json
Normal file
20
src/tools/schemas/ReadFileGemini.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"file_path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The path to the file to read."
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Optional: For text files, the 0-based line number to start reading from. Requires 'limit' to be set. Use for paginating through large files."
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Optional: For text files, maximum number of lines to read. Use with 'offset' to paginate through large files. If omitted, reads the entire file (if feasible, up to a default limit)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["file_path"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
50
src/tools/schemas/ReadManyFilesGemini.json
Normal file
50
src/tools/schemas/ReadManyFilesGemini.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"include": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"minItems": 1,
|
||||||
|
"description": "An array of glob patterns or paths. Examples: [\"src/**/*.ts\"], [\"README.md\", \"docs/\"]"
|
||||||
|
},
|
||||||
|
"exclude": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"description": "Optional. Glob patterns for files/directories to exclude. Added to default excludes if useDefaultExcludes is true. Example: \"**/*.log\", \"temp/\"",
|
||||||
|
"default": []
|
||||||
|
},
|
||||||
|
"recursive": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Optional. Whether to search recursively (primarily controlled by `**` in glob patterns). Defaults to true.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"useDefaultExcludes": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Optional. Whether to apply a list of default exclusion patterns (e.g., node_modules, .git, binary files). Defaults to true.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"file_filtering_options": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Whether to respect ignore patterns from .gitignore or .geminiignore",
|
||||||
|
"properties": {
|
||||||
|
"respect_git_ignore": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Optional: Whether to respect .gitignore patterns when listing files. Only available in git repositories. Defaults to true."
|
||||||
|
},
|
||||||
|
"respect_gemini_ignore": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Optional: Whether to respect .geminiignore patterns when listing files. Defaults to true."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["include"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
25
src/tools/schemas/ReplaceGemini.json
Normal file
25
src/tools/schemas/ReplaceGemini.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"file_path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The path to the file to modify."
|
||||||
|
},
|
||||||
|
"old_string": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The exact literal text to replace, preferably unescaped. For single replacements (default), include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. For multiple replacements, specify expected_replacements parameter. If this string is not the exact literal text (i.e. you escaped it) or does not match exactly, the tool will fail."
|
||||||
|
},
|
||||||
|
"new_string": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The exact literal text to replace `old_string` with, preferably unescaped. Provide the EXACT text. Ensure the resulting code is correct and idiomatic."
|
||||||
|
},
|
||||||
|
"expected_replacements": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Number of replacements expected. Defaults to 1 if not specified. Use when you want to replace multiple occurrences.",
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["file_path", "old_string", "new_string"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
20
src/tools/schemas/RunShellCommandGemini.json
Normal file
20
src/tools/schemas/RunShellCommandGemini.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"command": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Exact bash command to execute as `bash -c <command>`"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Brief description of the command for the user. Be specific and concise. Ideally a single sentence. Can be up to 3 sentences for clarity. No line breaks."
|
||||||
|
},
|
||||||
|
"dir_path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "(OPTIONAL) The path of the directory to run the command in. If not provided, the project root directory is used. Must be a directory within the workspace and must already exist."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["command"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
20
src/tools/schemas/SearchFileContentGemini.json
Normal file
20
src/tools/schemas/SearchFileContentGemini.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pattern": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*')."
|
||||||
|
},
|
||||||
|
"dir_path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional: The absolute path to the directory to search within. If omitted, searches the current working directory."
|
||||||
|
},
|
||||||
|
"include": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["pattern"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
16
src/tools/schemas/WriteFileGemini.json
Normal file
16
src/tools/schemas/WriteFileGemini.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"file_path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The path to the file to write to."
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The content to write to the file."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["file_path", "content"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
29
src/tools/schemas/WriteTodosGemini.json
Normal file
29
src/tools/schemas/WriteTodosGemini.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"todos": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "The complete list of todo items. This will replace the existing list.",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "A single todo item.",
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The description of the task."
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The current status of the task.",
|
||||||
|
"enum": ["pending", "in_progress", "completed", "cancelled"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["description", "status"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["todos"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
@@ -4,57 +4,87 @@ import BashOutputDescription from "./descriptions/BashOutput.md";
|
|||||||
import EditDescription from "./descriptions/Edit.md";
|
import EditDescription from "./descriptions/Edit.md";
|
||||||
import ExitPlanModeDescription from "./descriptions/ExitPlanMode.md";
|
import ExitPlanModeDescription from "./descriptions/ExitPlanMode.md";
|
||||||
import GlobDescription from "./descriptions/Glob.md";
|
import GlobDescription from "./descriptions/Glob.md";
|
||||||
|
// Gemini toolset
|
||||||
|
import GlobGeminiDescription from "./descriptions/GlobGemini.md";
|
||||||
import GrepDescription from "./descriptions/Grep.md";
|
import GrepDescription from "./descriptions/Grep.md";
|
||||||
import GrepFilesDescription from "./descriptions/GrepFiles.md";
|
import GrepFilesDescription from "./descriptions/GrepFiles.md";
|
||||||
import KillBashDescription from "./descriptions/KillBash.md";
|
import KillBashDescription from "./descriptions/KillBash.md";
|
||||||
import ListDirCodexDescription from "./descriptions/ListDirCodex.md";
|
import ListDirCodexDescription from "./descriptions/ListDirCodex.md";
|
||||||
|
import ListDirectoryGeminiDescription from "./descriptions/ListDirectoryGemini.md";
|
||||||
import LSDescription from "./descriptions/LS.md";
|
import LSDescription from "./descriptions/LS.md";
|
||||||
import MultiEditDescription from "./descriptions/MultiEdit.md";
|
import MultiEditDescription from "./descriptions/MultiEdit.md";
|
||||||
import ReadDescription from "./descriptions/Read.md";
|
import ReadDescription from "./descriptions/Read.md";
|
||||||
import ReadFileCodexDescription from "./descriptions/ReadFileCodex.md";
|
import ReadFileCodexDescription from "./descriptions/ReadFileCodex.md";
|
||||||
|
import ReadFileGeminiDescription from "./descriptions/ReadFileGemini.md";
|
||||||
|
import ReadManyFilesGeminiDescription from "./descriptions/ReadManyFilesGemini.md";
|
||||||
|
import ReplaceGeminiDescription from "./descriptions/ReplaceGemini.md";
|
||||||
|
import RunShellCommandGeminiDescription from "./descriptions/RunShellCommandGemini.md";
|
||||||
|
import SearchFileContentGeminiDescription from "./descriptions/SearchFileContentGemini.md";
|
||||||
import ShellDescription from "./descriptions/Shell.md";
|
import ShellDescription from "./descriptions/Shell.md";
|
||||||
import ShellCommandDescription from "./descriptions/ShellCommand.md";
|
import ShellCommandDescription from "./descriptions/ShellCommand.md";
|
||||||
import TodoWriteDescription from "./descriptions/TodoWrite.md";
|
import TodoWriteDescription from "./descriptions/TodoWrite.md";
|
||||||
import UpdatePlanDescription from "./descriptions/UpdatePlan.md";
|
import UpdatePlanDescription from "./descriptions/UpdatePlan.md";
|
||||||
import WriteDescription from "./descriptions/Write.md";
|
import WriteDescription from "./descriptions/Write.md";
|
||||||
|
import WriteFileGeminiDescription from "./descriptions/WriteFileGemini.md";
|
||||||
|
import WriteTodosGeminiDescription from "./descriptions/WriteTodosGemini.md";
|
||||||
import { apply_patch } from "./impl/ApplyPatch";
|
import { apply_patch } from "./impl/ApplyPatch";
|
||||||
import { bash } from "./impl/Bash";
|
import { bash } from "./impl/Bash";
|
||||||
import { bash_output } from "./impl/BashOutput";
|
import { bash_output } from "./impl/BashOutput";
|
||||||
import { edit } from "./impl/Edit";
|
import { edit } from "./impl/Edit";
|
||||||
import { exit_plan_mode } from "./impl/ExitPlanMode";
|
import { exit_plan_mode } from "./impl/ExitPlanMode";
|
||||||
import { glob } from "./impl/Glob";
|
import { glob } from "./impl/Glob";
|
||||||
|
// Gemini toolset
|
||||||
|
import { glob_gemini } from "./impl/GlobGemini";
|
||||||
import { grep } from "./impl/Grep";
|
import { grep } from "./impl/Grep";
|
||||||
import { grep_files } from "./impl/GrepFiles";
|
import { grep_files } from "./impl/GrepFiles";
|
||||||
import { kill_bash } from "./impl/KillBash";
|
import { kill_bash } from "./impl/KillBash";
|
||||||
import { list_dir } from "./impl/ListDirCodex";
|
import { list_dir } from "./impl/ListDirCodex";
|
||||||
|
import { list_directory } from "./impl/ListDirectoryGemini";
|
||||||
import { ls } from "./impl/LS";
|
import { ls } from "./impl/LS";
|
||||||
import { multi_edit } from "./impl/MultiEdit";
|
import { multi_edit } from "./impl/MultiEdit";
|
||||||
import { read } from "./impl/Read";
|
import { read } from "./impl/Read";
|
||||||
import { read_file } from "./impl/ReadFileCodex";
|
import { read_file } from "./impl/ReadFileCodex";
|
||||||
|
import { read_file_gemini } from "./impl/ReadFileGemini";
|
||||||
|
import { read_many_files } from "./impl/ReadManyFilesGemini";
|
||||||
|
import { replace } from "./impl/ReplaceGemini";
|
||||||
|
import { run_shell_command } from "./impl/RunShellCommandGemini";
|
||||||
|
import { search_file_content } from "./impl/SearchFileContentGemini";
|
||||||
import { shell } from "./impl/Shell";
|
import { shell } from "./impl/Shell";
|
||||||
import { shell_command } from "./impl/ShellCommand";
|
import { shell_command } from "./impl/ShellCommand";
|
||||||
import { todo_write } from "./impl/TodoWrite";
|
import { todo_write } from "./impl/TodoWrite";
|
||||||
import { update_plan } from "./impl/UpdatePlan";
|
import { update_plan } from "./impl/UpdatePlan";
|
||||||
import { write } from "./impl/Write";
|
import { write } from "./impl/Write";
|
||||||
|
import { write_file_gemini } from "./impl/WriteFileGemini";
|
||||||
|
import { write_todos } from "./impl/WriteTodosGemini";
|
||||||
import ApplyPatchSchema from "./schemas/ApplyPatch.json";
|
import ApplyPatchSchema from "./schemas/ApplyPatch.json";
|
||||||
import BashSchema from "./schemas/Bash.json";
|
import BashSchema from "./schemas/Bash.json";
|
||||||
import BashOutputSchema from "./schemas/BashOutput.json";
|
import BashOutputSchema from "./schemas/BashOutput.json";
|
||||||
import EditSchema from "./schemas/Edit.json";
|
import EditSchema from "./schemas/Edit.json";
|
||||||
import ExitPlanModeSchema from "./schemas/ExitPlanMode.json";
|
import ExitPlanModeSchema from "./schemas/ExitPlanMode.json";
|
||||||
import GlobSchema from "./schemas/Glob.json";
|
import GlobSchema from "./schemas/Glob.json";
|
||||||
|
// Gemini toolset
|
||||||
|
import GlobGeminiSchema from "./schemas/GlobGemini.json";
|
||||||
import GrepSchema from "./schemas/Grep.json";
|
import GrepSchema from "./schemas/Grep.json";
|
||||||
import GrepFilesSchema from "./schemas/GrepFiles.json";
|
import GrepFilesSchema from "./schemas/GrepFiles.json";
|
||||||
import KillBashSchema from "./schemas/KillBash.json";
|
import KillBashSchema from "./schemas/KillBash.json";
|
||||||
import ListDirCodexSchema from "./schemas/ListDirCodex.json";
|
import ListDirCodexSchema from "./schemas/ListDirCodex.json";
|
||||||
|
import ListDirectoryGeminiSchema from "./schemas/ListDirectoryGemini.json";
|
||||||
import LSSchema from "./schemas/LS.json";
|
import LSSchema from "./schemas/LS.json";
|
||||||
import MultiEditSchema from "./schemas/MultiEdit.json";
|
import MultiEditSchema from "./schemas/MultiEdit.json";
|
||||||
import ReadSchema from "./schemas/Read.json";
|
import ReadSchema from "./schemas/Read.json";
|
||||||
import ReadFileCodexSchema from "./schemas/ReadFileCodex.json";
|
import ReadFileCodexSchema from "./schemas/ReadFileCodex.json";
|
||||||
|
import ReadFileGeminiSchema from "./schemas/ReadFileGemini.json";
|
||||||
|
import ReadManyFilesGeminiSchema from "./schemas/ReadManyFilesGemini.json";
|
||||||
|
import ReplaceGeminiSchema from "./schemas/ReplaceGemini.json";
|
||||||
|
import RunShellCommandGeminiSchema from "./schemas/RunShellCommandGemini.json";
|
||||||
|
import SearchFileContentGeminiSchema from "./schemas/SearchFileContentGemini.json";
|
||||||
import ShellSchema from "./schemas/Shell.json";
|
import ShellSchema from "./schemas/Shell.json";
|
||||||
import ShellCommandSchema from "./schemas/ShellCommand.json";
|
import ShellCommandSchema from "./schemas/ShellCommand.json";
|
||||||
import TodoWriteSchema from "./schemas/TodoWrite.json";
|
import TodoWriteSchema from "./schemas/TodoWrite.json";
|
||||||
import UpdatePlanSchema from "./schemas/UpdatePlan.json";
|
import UpdatePlanSchema from "./schemas/UpdatePlan.json";
|
||||||
import WriteSchema from "./schemas/Write.json";
|
import WriteSchema from "./schemas/Write.json";
|
||||||
|
import WriteFileGeminiSchema from "./schemas/WriteFileGemini.json";
|
||||||
|
import WriteTodosGeminiSchema from "./schemas/WriteTodosGemini.json";
|
||||||
|
|
||||||
type ToolImplementation = (args: Record<string, unknown>) => Promise<unknown>;
|
type ToolImplementation = (args: Record<string, unknown>) => Promise<unknown>;
|
||||||
|
|
||||||
@@ -160,6 +190,52 @@ const toolDefinitions = {
|
|||||||
description: UpdatePlanDescription.trim(),
|
description: UpdatePlanDescription.trim(),
|
||||||
impl: update_plan as unknown as ToolImplementation,
|
impl: update_plan as unknown as ToolImplementation,
|
||||||
},
|
},
|
||||||
|
// Gemini toolset
|
||||||
|
glob_gemini: {
|
||||||
|
schema: GlobGeminiSchema,
|
||||||
|
description: GlobGeminiDescription.trim(),
|
||||||
|
impl: glob_gemini as unknown as ToolImplementation,
|
||||||
|
},
|
||||||
|
list_directory: {
|
||||||
|
schema: ListDirectoryGeminiSchema,
|
||||||
|
description: ListDirectoryGeminiDescription.trim(),
|
||||||
|
impl: list_directory as unknown as ToolImplementation,
|
||||||
|
},
|
||||||
|
read_file_gemini: {
|
||||||
|
schema: ReadFileGeminiSchema,
|
||||||
|
description: ReadFileGeminiDescription.trim(),
|
||||||
|
impl: read_file_gemini as unknown as ToolImplementation,
|
||||||
|
},
|
||||||
|
read_many_files: {
|
||||||
|
schema: ReadManyFilesGeminiSchema,
|
||||||
|
description: ReadManyFilesGeminiDescription.trim(),
|
||||||
|
impl: read_many_files as unknown as ToolImplementation,
|
||||||
|
},
|
||||||
|
replace: {
|
||||||
|
schema: ReplaceGeminiSchema,
|
||||||
|
description: ReplaceGeminiDescription.trim(),
|
||||||
|
impl: replace as unknown as ToolImplementation,
|
||||||
|
},
|
||||||
|
run_shell_command: {
|
||||||
|
schema: RunShellCommandGeminiSchema,
|
||||||
|
description: RunShellCommandGeminiDescription.trim(),
|
||||||
|
impl: run_shell_command as unknown as ToolImplementation,
|
||||||
|
},
|
||||||
|
search_file_content: {
|
||||||
|
schema: SearchFileContentGeminiSchema,
|
||||||
|
description: SearchFileContentGeminiDescription.trim(),
|
||||||
|
impl: search_file_content as unknown as ToolImplementation,
|
||||||
|
},
|
||||||
|
write_todos: {
|
||||||
|
schema: WriteTodosGeminiSchema,
|
||||||
|
description: WriteTodosGeminiDescription.trim(),
|
||||||
|
impl: write_todos as unknown as ToolImplementation,
|
||||||
|
},
|
||||||
|
write_file_gemini: {
|
||||||
|
schema: WriteFileGeminiSchema,
|
||||||
|
description: WriteFileGeminiDescription.trim(),
|
||||||
|
impl: write_file_gemini as unknown as ToolImplementation,
|
||||||
|
},
|
||||||
} as const satisfies Record<string, ToolAssets>;
|
} as const satisfies Record<string, ToolAssets>;
|
||||||
|
|
||||||
export type ToolName = keyof typeof toolDefinitions;
|
export type ToolName = keyof typeof toolDefinitions;
|
||||||
|
|||||||
@@ -36,6 +36,18 @@ const ANTHROPIC_TOOLS = [
|
|||||||
"Write",
|
"Write",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const GEMINI_TOOLS = [
|
||||||
|
"run_shell_command",
|
||||||
|
"read_file_gemini",
|
||||||
|
"list_directory",
|
||||||
|
"glob_gemini",
|
||||||
|
"search_file_content",
|
||||||
|
"replace",
|
||||||
|
"write_file_gemini",
|
||||||
|
"write_todos",
|
||||||
|
"read_many_files",
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the list of Letta Code tools currently attached to an agent.
|
* Gets the list of Letta Code tools currently attached to an agent.
|
||||||
* Returns the tool names that are both attached to the agent AND in our tool definitions.
|
* Returns the tool names that are both attached to the agent AND in our tool definitions.
|
||||||
@@ -54,7 +66,7 @@ export async function getAttachedLettaTools(
|
|||||||
.filter((name): name is string => typeof name === "string") || [];
|
.filter((name): name is string => typeof name === "string") || [];
|
||||||
|
|
||||||
// Get all possible Letta Code tool names
|
// Get all possible Letta Code tool names
|
||||||
const allLettaTools = [...CODEX_TOOLS, ...ANTHROPIC_TOOLS];
|
const allLettaTools = [...CODEX_TOOLS, ...ANTHROPIC_TOOLS, ...GEMINI_TOOLS];
|
||||||
|
|
||||||
// Return intersection: tools that are both attached AND in our definitions
|
// Return intersection: tools that are both attached AND in our definitions
|
||||||
return toolNames.filter((name) => allLettaTools.includes(name));
|
return toolNames.filter((name) => allLettaTools.includes(name));
|
||||||
@@ -62,13 +74,12 @@ export async function getAttachedLettaTools(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects which toolset is attached to an agent by examining its tools.
|
* Detects which toolset is attached to an agent by examining its tools.
|
||||||
* Returns "codex" if majority are codex tools, "default" if majority are anthropic tools,
|
* Returns "codex", "default", "gemini" based on majority, or null if no Letta Code tools.
|
||||||
* or null if no Letta Code tools are detected.
|
|
||||||
*/
|
*/
|
||||||
export async function detectToolsetFromAgent(
|
export async function detectToolsetFromAgent(
|
||||||
client: Letta,
|
client: Letta,
|
||||||
agentId: string,
|
agentId: string,
|
||||||
): Promise<"codex" | "default" | null> {
|
): Promise<"codex" | "default" | "gemini" | null> {
|
||||||
const attachedTools = await getAttachedLettaTools(client, agentId);
|
const attachedTools = await getAttachedLettaTools(client, agentId);
|
||||||
|
|
||||||
if (attachedTools.length === 0) {
|
if (attachedTools.length === 0) {
|
||||||
@@ -81,30 +92,37 @@ export async function detectToolsetFromAgent(
|
|||||||
const anthropicCount = attachedTools.filter((name) =>
|
const anthropicCount = attachedTools.filter((name) =>
|
||||||
ANTHROPIC_TOOLS.includes(name),
|
ANTHROPIC_TOOLS.includes(name),
|
||||||
).length;
|
).length;
|
||||||
|
const geminiCount = attachedTools.filter((name) =>
|
||||||
|
GEMINI_TOOLS.includes(name),
|
||||||
|
).length;
|
||||||
|
|
||||||
// Return whichever has more tools attached
|
// Return whichever has the most tools attached
|
||||||
return codexCount > anthropicCount ? "codex" : "default";
|
const max = Math.max(codexCount, anthropicCount, geminiCount);
|
||||||
|
if (geminiCount === max) return "gemini";
|
||||||
|
if (codexCount === max) return "codex";
|
||||||
|
return "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force switch to a specific toolset regardless of model.
|
* Force switch to a specific toolset regardless of model.
|
||||||
*
|
*
|
||||||
* @param toolsetName - The toolset to switch to ("codex" or "default")
|
* @param toolsetName - The toolset to switch to ("codex", "default", or "gemini")
|
||||||
* @param agentId - Agent to relink tools to
|
* @param agentId - Agent to relink tools to
|
||||||
*/
|
*/
|
||||||
export async function forceToolsetSwitch(
|
export async function forceToolsetSwitch(
|
||||||
toolsetName: "codex" | "default",
|
toolsetName: "codex" | "default" | "gemini",
|
||||||
agentId: string,
|
agentId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Clear currently loaded tools
|
// Clear currently loaded tools
|
||||||
clearTools();
|
clearTools();
|
||||||
|
|
||||||
// Load the appropriate toolset by passing a model identifier from that provider
|
// Load the appropriate toolset by passing a model identifier from that provider
|
||||||
// This triggers the loadTools logic that selects OPENAI_DEFAULT_TOOLS vs ANTHROPIC_DEFAULT_TOOLS
|
|
||||||
if (toolsetName === "codex") {
|
if (toolsetName === "codex") {
|
||||||
await loadTools("openai/gpt-4"); // Pass OpenAI model to trigger codex toolset
|
await loadTools("openai/gpt-4");
|
||||||
|
} else if (toolsetName === "gemini") {
|
||||||
|
await loadTools("google_ai/gemini-3-pro-preview");
|
||||||
} else {
|
} else {
|
||||||
await loadTools("anthropic/claude-sonnet-4"); // Pass Anthropic to trigger default toolset
|
await loadTools("anthropic/claude-sonnet-4");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert the new toolset to server
|
// Upsert the new toolset to server
|
||||||
@@ -127,7 +145,7 @@ export async function forceToolsetSwitch(
|
|||||||
export async function switchToolsetForModel(
|
export async function switchToolsetForModel(
|
||||||
modelIdentifier: string,
|
modelIdentifier: string,
|
||||||
agentId: string,
|
agentId: string,
|
||||||
): Promise<"codex" | "default"> {
|
): Promise<"codex" | "default" | "gemini"> {
|
||||||
// Resolve model ID to handle when possible so provider checks stay consistent
|
// Resolve model ID to handle when possible so provider checks stay consistent
|
||||||
const resolvedModel = resolveModel(modelIdentifier) ?? modelIdentifier;
|
const resolvedModel = resolveModel(modelIdentifier) ?? modelIdentifier;
|
||||||
|
|
||||||
@@ -158,6 +176,11 @@ export async function switchToolsetForModel(
|
|||||||
await unlinkToolsFromAgent(agentId);
|
await unlinkToolsFromAgent(agentId);
|
||||||
await linkToolsToAgent(agentId);
|
await linkToolsToAgent(agentId);
|
||||||
|
|
||||||
const toolsetName = isOpenAIModel(resolvedModel) ? "codex" : "default";
|
const { isGeminiModel } = await import("./manager");
|
||||||
|
const toolsetName = isOpenAIModel(resolvedModel)
|
||||||
|
? "codex"
|
||||||
|
: isGeminiModel(resolvedModel)
|
||||||
|
? "gemini"
|
||||||
|
: "default";
|
||||||
return toolsetName;
|
return toolsetName;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user