/**
* Local Tools — tool registry, definitions, and dispatcher
*
* This is a thin facade: tool implementations live in `./tools/`.
* All consumers import from this file for backward compatibility.
*/
// Re-export task persistence API (used by ChatApp, agent-loop)
export { loadTodos, setTodoSessionId, getTodoState } from "./tools/task-manager.js";
// Import tool implementations from extracted modules
import { readFile, writeFile, editFile, multiEdit, notebookEdit, listDirectory, searchFiles, searchContent, resolvePath } from "./tools/file-ops.js";
import { readFileSync, existsSync, statSync } from "fs";
import { resolve } from "path";
import { globSearch, grepSearch } from "./tools/search-tools.js";
import { runCommand, bashOutput, killShell, listShellsFn } from "./tools/shell-exec.js";
import { webFetch, webSearch } from "./tools/web-tools.js";
import { tasksTool } from "./tools/task-manager.js";
import { taskTool, teamCreateTool, taskOutput, taskStop, configTool, askUser, lspTool, skillTool } from "./tools/agent-tools.js";
// ============================================================================
// TYPES
// ============================================================================
export interface LocalToolDefinition {
name: string;
description: string;
input_schema: {
type: "object";
properties: Record<string, unknown>;
required: string[];
};
}
import { ToolResult } from "../../shared/types.js";
export type { ToolResult };
// ============================================================================
// TOOL NAMES
// ============================================================================
export const LOCAL_TOOL_NAMES = new Set([
// Original
"read_file",
"read_many_files",
"write_file",
"edit_file",
"list_directory",
"search_files",
"search_content",
"run_command",
// New (Claude Code parity)
"glob",
"grep",
"notebook_edit",
"web_fetch",
"tasks", // Replaces todo_write — action-based CRUD with IDs, deps
"multi_edit", // Multi-edit tool
"task", // Subagent tool
"team_create", // Agent team tool
// Background process tools
"bash_output",
"kill_shell",
"list_shells",
// Background task tools (shells + agents)
"task_output",
"task_stop",
// Web search
"web_search",
// Claude Code parity — consolidated tools
"config", // Settings + plan mode
"ask_user", // Structured questions
// Code intelligence
"lsp",
// Skills
"skill",
]);
export function isLocalTool(name: string): boolean {
return LOCAL_TOOL_NAMES.has(name);
}
// ============================================================================
// TOOL DEFINITIONS (for Anthropic API)
// ============================================================================
export const LOCAL_TOOL_DEFINITIONS: LocalToolDefinition[] = [
// ------------------------------------------------------------------
// ENHANCED ORIGINALS
// ------------------------------------------------------------------
{
name: "read_file",
description: "Read file contents. Supports line-based pagination for large files. Reads images (png/jpg/gif/webp) as visual content. Reads audio files (mp3/wav/aiff/aac/ogg/flac/m4a) as audio content. Extracts text from PDFs. For multiple files, emit all read_file calls in one response — they execute in parallel.",
input_schema: {
type: "object",
properties: {
path: { type: "string", description: "Absolute or relative path to the file" },
offset: { type: "number", description: "Line number to start reading from (1-based). Omit to read from start." },
limit: { type: "number", description: "Max number of lines to read. Omit to read all." },
pages: { type: "string", description: "Page range for PDFs (e.g. '1-5', '3', '10-20'). Only for .pdf files." },
},
required: ["path"],
},
},
{
name: "read_many_files",
description: "Read multiple files matching glob patterns in a single call. More efficient than multiple read_file calls. Returns concatenated content with file headers. Files >500 lines are truncated to first 100 lines.",
input_schema: {
type: "object",
properties: {
pattern: { type: "string", description: "Glob pattern like 'src/**/*.ts' or '*.json'" },
path: { type: "string", description: "Base directory to search in (defaults to cwd)" },
limit: { type: "number", description: "Max files to read (default 20, max 50)" },
},
required: ["pattern"],
},
},
{
name: "write_file",
description: "Write content to a file, creating it and parent directories if needed. For multiple independent files, emit all write_file calls in one response.",
input_schema: {
type: "object",
properties: {
path: { type: "string", description: "Absolute or relative path to the file" },
content: { type: "string", description: "Content to write to the file" },
},
required: ["path", "content"],
},
},
{
name: "edit_file",
description: "Edit a file by replacing an exact string match. Supports replacing all occurrences.",
input_schema: {
type: "object",
properties: {
path: { type: "string", description: "Absolute or relative path to the file" },
old_string: { type: "string", description: "Exact text to find in the file" },
new_string: { type: "string", description: "Text to replace old_string with" },
replace_all: { type: "boolean", description: "Replace ALL occurrences (default false — replaces first only)" },
},
required: ["path", "old_string", "new_string"],
},
},
{
name: "list_directory",
description: "List files and directories at the specified path",
input_schema: {
type: "object",
properties: {
path: { type: "string", description: "Absolute or relative path to the directory" },
recursive: { type: "boolean", description: "List recursively (default false, max 200 entries)" },
},
required: ["path"],
},
},
{
name: "search_files",
description: "Search for files matching a name pattern using find",
input_schema: {
type: "object",
properties: {
pattern: { type: "string", description: "File name pattern (e.g. *.ts, *.swift)" },
path: { type: "string", description: "Directory to search in" },
},
required: ["pattern", "path"],
},
},
{
name: "search_content",
description: "Search for text content in files (grep-like). For advanced search, use the 'grep' tool.",
input_schema: {
type: "object",
properties: {
query: { type: "string", description: "Text or regex pattern to search for" },
path: { type: "string", description: "Directory to search in" },
file_pattern: { type: "string", description: "Optional file glob filter (e.g. *.ts)" },
},
required: ["query", "path"],
},
},
{
name: "run_command",
description: "Execute a shell command. Output streams live. Use run_in_background:true for dev servers/watchers — after starting, use bash_output to verify. On macOS use python3 not python.",
input_schema: {
type: "object",
properties: {
command: { type: "string", description: "Shell command to execute" },
working_directory: { type: "string", description: "Working directory for the command" },
timeout: { type: "number", description: "Timeout in milliseconds (default 30000, max 300000)" },
description: { type: "string", description: "Short description of what this command does" },
run_in_background: { type: "boolean", description: "Run in background (for dev servers, watchers). Returns process ID immediately." },
},
required: ["command"],
},
},
// ------------------------------------------------------------------
// NEW: GLOB — pattern-based file finder
// ------------------------------------------------------------------
{
name: "glob",
description: "Fast file pattern matching. Use glob patterns like '**/*.ts' or 'src/**/*.tsx'. Returns matching file paths. For multiple patterns, emit all glob calls in one response.",
input_schema: {
type: "object",
properties: {
pattern: { type: "string", description: "Glob pattern (e.g. '**/*.ts', 'src/**/*.tsx', '*.json')" },
path: { type: "string", description: "Base directory to search in (defaults to cwd)" },
},
required: ["pattern"],
},
},
// ------------------------------------------------------------------
// NEW: GREP — advanced content search
// ------------------------------------------------------------------
{
name: "grep",
description: "Search file contents with regex, context lines, and multiple output modes. More powerful than search_content. For multiple patterns, emit all grep calls in one response.",
input_schema: {
type: "object",
properties: {
pattern: { type: "string", description: "Regex pattern to search for" },
path: { type: "string", description: "File or directory to search in (defaults to cwd)" },
glob: { type: "string", description: "Glob pattern to filter files (e.g. '*.ts', '*.{ts,tsx}')" },
output_mode: {
type: "string",
enum: ["content", "files_with_matches", "count"],
description: "Output mode: 'content' shows matching lines, 'files_with_matches' shows file paths (default), 'count' shows match counts",
},
context: { type: "number", description: "Lines of context before and after each match" },
before: { type: "number", description: "Lines to show before each match (-B)" },
after: { type: "number", description: "Lines to show after each match (-A)" },
case_insensitive: { type: "boolean", description: "Case insensitive search (default false)" },
type: { type: "string", description: "File type shorthand: js, ts, py, go, rust, java, etc." },
head_limit: { type: "number", description: "Max results to return (default 200)" },
offset: { type: "number", description: "Skip first N entries before applying head_limit (default 0)" },
multiline: { type: "boolean", description: "Enable multiline mode where . matches newlines (requires rg)" },
},
required: ["pattern"],
},
},
// ------------------------------------------------------------------
// NEW: NOTEBOOK_EDIT — Jupyter notebook cell editing
// ------------------------------------------------------------------
{
name: "notebook_edit",
description: "Edit Jupyter notebook (.ipynb) cells: replace, insert, or delete cells.",
input_schema: {
type: "object",
properties: {
notebook_path: { type: "string", description: "Path to the .ipynb file" },
cell_id: { type: "string", description: "Cell ID or 0-based index. For insert, new cell goes after this." },
new_source: { type: "string", description: "New source code/markdown for the cell" },
cell_type: { type: "string", enum: ["code", "markdown"], description: "Cell type (required for insert)" },
edit_mode: { type: "string", enum: ["replace", "insert", "delete"], description: "Edit mode (default: replace)" },
},
required: ["notebook_path", "new_source"],
},
},
// ------------------------------------------------------------------
// NEW: WEB_FETCH — fetch URL content
// ------------------------------------------------------------------
{
name: "web_fetch",
description: "Fetch content from a URL and return as cleaned text/markdown. Strips HTML, scripts, styles.",
input_schema: {
type: "object",
properties: {
url: { type: "string", description: "URL to fetch" },
prompt: { type: "string", description: "What to extract from the page (used for context)" },
},
required: ["url"],
},
},
// ------------------------------------------------------------------
// NEW: WEB_SEARCH — search the web via Exa API
// ------------------------------------------------------------------
{
name: "web_search",
description: "Search the web for current information. Returns titles, URLs, and snippets.",
input_schema: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
allowed_domains: { type: "array", items: { type: "string" }, description: "Only include results from these domains" },
blocked_domains: { type: "array", items: { type: "string" }, description: "Exclude results from these domains" },
},
required: ["query"],
},
},
// ------------------------------------------------------------------
// TASKS — action-based CRUD for structured task tracking
// ------------------------------------------------------------------
{
name: "tasks",
description: "Track tasks for the current session. Actions: create (returns ID), update (status/deps), list (summary), get (full details). Supports dependencies via blocks/blockedBy.",
input_schema: {
type: "object",
properties: {
action: {
type: "string",
enum: ["create", "update", "list", "get"],
description: "Action to perform",
},
// For create:
subject: { type: "string", description: "Brief title in imperative form (create)" },
description: { type: "string", description: "Detailed description (create/update)" },
activeForm: { type: "string", description: "Present continuous spinner text (create/update)" },
metadata: { type: "object", description: "Arbitrary metadata (create/update)" },
// For update/get:
taskId: { type: "string", description: "Task ID (update/get)" },
status: { type: "string", enum: ["pending", "in_progress", "completed", "deleted"], description: "New status (update)" },
subject_update: { type: "string", description: "New subject (update)" },
addBlocks: { type: "array", items: { type: "string" }, description: "Task IDs this task blocks (update)" },
addBlockedBy: { type: "array", items: { type: "string" }, description: "Task IDs that block this task (update)" },
owner: { type: "string", description: "Task owner (update)" },
},
required: ["action"],
},
},
// ------------------------------------------------------------------
// NEW: MULTI_EDIT — multiple edits to one file in a single call
// ------------------------------------------------------------------
{
name: "multi_edit",
description: "Apply multiple edits to one file in a single call. Edits applied sequentially. Fails if any old_string not found.",
input_schema: {
type: "object",
properties: {
file_path: { type: "string", description: "Absolute or relative path to the file" },
edits: {
type: "array",
description: "Array of edits to apply sequentially",
items: {
type: "object",
properties: {
old_string: { type: "string", description: "Exact text to find" },
new_string: { type: "string", description: "Text to replace with" },
},
required: ["old_string", "new_string"],
},
},
},
required: ["file_path", "edits"],
},
},
// ------------------------------------------------------------------
// TASK — subagent for discrete tasks
// ------------------------------------------------------------------
{
name: "task",
description: "Launch a subagent that runs in isolated context and returns a summary when done. Use for discrete tasks completable in 2-6 turns. Use run_in_background for long tasks.",
input_schema: {
type: "object",
properties: {
prompt: { type: "string", description: "Specific task with clear completion criteria" },
subagent_type: {
type: "string",
enum: ["explore", "plan", "general-purpose", "research"],
description: "Agent type: explore=find, plan=design, general-purpose=do, research=lookup",
},
model: {
type: "string",
enum: ["sonnet", "opus", "haiku"],
description: "Haiku for quick tasks, Sonnet (default) for most, Opus for complex",
},
run_in_background: {
type: "boolean",
description: "Run agent in background. Returns output_file path to check progress via task_output.",
},
max_turns: {
type: "number",
description: "Max agentic turns (1-50). Default 8.",
},
name: {
type: "string",
description: "Display name for the agent.",
},
description: {
type: "string",
description: "Short 3-5 word description of the task.",
},
team_name: {
type: "string",
description: "Team name for spawning. Uses current team context if omitted.",
},
mode: {
type: "string",
enum: ["default", "plan", "yolo"],
description: "Permission mode for spawned agent (default inherits parent).",
},
},
required: ["prompt", "subagent_type"],
},
},
// ------------------------------------------------------------------
// TEAM — parallel agent team for large tasks
// ------------------------------------------------------------------
{
name: "team_create",
description: "Create and run an Agent Team — multiple Claude instances working in parallel. Each teammate runs in separate context, claims tasks from a shared list, and has full tool access. Size tasks for 5-6 items per teammate. Include file lists to prevent conflicts.",
input_schema: {
type: "object",
properties: {
name: {
type: "string",
description: "Team name (e.g., 'Feature Implementation Team')",
},
teammate_count: {
type: "number",
description: "Number of teammates to spawn (2-5 recommended)",
},
model: {
type: "string",
enum: ["sonnet", "opus", "haiku"],
description: "Model for all teammates (default: sonnet)",
},
tasks: {
type: "array",
description: "Tasks for the team to complete",
items: {
type: "object",
properties: {
description: {
type: "string",
description: "Clear task description with completion criteria",
},
files: {
type: "array",
items: { type: "string" },
description: "Files this task will modify (for conflict prevention)",
},
dependencies: {
type: "array",
items: { type: "string" },
description: "Task descriptions that must complete first",
},
},
required: ["description"],
},
},
},
required: ["name", "teammate_count", "tasks"],
},
},
// ------------------------------------------------------------------
// BACKGROUND PROCESS TOOLS
// ------------------------------------------------------------------
{
name: "bash_output",
description: "Read output from a running or completed background shell process. Returns only NEW output since the last read.",
input_schema: {
type: "object",
properties: {
bash_id: { type: "string", description: "The process ID returned when starting the background process" },
filter: { type: "string", description: "Optional regex to filter output lines" },
},
required: ["bash_id"],
},
},
{
name: "kill_shell",
description: "Terminate a running background shell process",
input_schema: {
type: "object",
properties: {
shell_id: { type: "string", description: "The process ID to kill" },
},
required: ["shell_id"],
},
},
{
name: "list_shells",
description: "List all background shell processes (running and recent completed)",
input_schema: {
type: "object",
properties: {},
required: [],
},
},
// ------------------------------------------------------------------
// TASK OUTPUT / TASK STOP — unified background task management
// ------------------------------------------------------------------
{
name: "task_output",
description: "Get output from a background task (shell or agent). Returns status and output content.",
input_schema: {
type: "object",
properties: {
task_id: { type: "string", description: "The task/agent ID (e.g. shell-xxx or agent-xxx)" },
block: { type: "boolean", description: "Wait for completion (default: true)" },
timeout: { type: "number", description: "Max wait time in ms (default: 30000)" },
},
required: ["task_id"],
},
},
{
name: "task_stop",
description: "Stop a running background task (shell or agent) by ID.",
input_schema: {
type: "object",
properties: {
task_id: { type: "string", description: "The task ID to stop" },
},
required: ["task_id"],
},
},
// ------------------------------------------------------------------
// CONFIG — runtime settings + mode control (consolidated)
// ------------------------------------------------------------------
{
name: "config",
description: "Read or write CLI settings. Omit value to read. Keys: model (sonnet/opus/haiku), mode (default/plan/yolo — plan restricts to read-only tools), memory. Use mode=plan before non-trivial tasks to explore first, mode=default to resume full access.",
input_schema: {
type: "object",
properties: {
setting: { type: "string", description: "Setting key: 'model', 'mode', 'memory'" },
value: { type: "string", description: "New value. Omit to read current value." },
},
required: ["setting"],
},
},
// ------------------------------------------------------------------
// LSP — Language Server Protocol code intelligence
// ------------------------------------------------------------------
{
name: "lsp",
description: "Code intelligence via Language Server Protocol. Supports: goToDefinition, findReferences, hover, documentSymbol, workspaceSymbol, goToImplementation, prepareCallHierarchy, incomingCalls, outgoingCalls. Requires a language server installed for the file type.",
input_schema: {
type: "object",
properties: {
operation: {
type: "string",
enum: [
"goToDefinition",
"findReferences",
"hover",
"documentSymbol",
"workspaceSymbol",
"goToImplementation",
"prepareCallHierarchy",
"incomingCalls",
"outgoingCalls",
],
description: "LSP operation to perform",
},
filePath: { type: "string", description: "Absolute or relative path to the file" },
line: { type: "number", description: "Line number (1-based, as shown in editors)" },
character: { type: "number", description: "Character offset (1-based, as shown in editors)" },
query: { type: "string", description: "Search query for workspaceSymbol operation (optional, defaults to all symbols)" },
},
required: ["operation", "filePath", "line", "character"],
},
},
// ------------------------------------------------------------------
// ASK_USER — structured multi-choice question
// ------------------------------------------------------------------
{
name: "ask_user",
description: "Ask the user a structured question with predefined options. Use to gather preferences, clarify requirements, or get decisions during execution. The user can always type a custom answer.",
input_schema: {
type: "object",
properties: {
question: { type: "string", description: "The question to ask" },
options: {
type: "array",
description: "2-4 options for the user to choose from",
items: {
type: "object",
properties: {
label: { type: "string", description: "Short option label (1-5 words)" },
description: { type: "string", description: "Explanation of what this option means" },
},
required: ["label", "description"],
},
},
},
required: ["question", "options"],
},
},
// ------------------------------------------------------------------
// SKILL — invoke named skills (model-callable)
// ------------------------------------------------------------------
{
name: "skill",
description: "Invoke a named skill. Skills provide specialized workflows like committing code, reviewing PRs, etc. Built-in skills: commit, review, review-pr. Custom skills from .whale/commands/ and ~/.swagmanager/commands/.",
input_schema: {
type: "object",
properties: {
skill: { type: "string", description: "Skill name (e.g., 'commit', 'review-pr')" },
args: { type: "string", description: "Optional arguments for the skill" },
},
required: ["skill"],
},
},
];
// ============================================================================
// READ MANY FILES — reads multiple files matching glob patterns
// ============================================================================
const BINARY_EXTENSIONS = new Set([
"png", "jpg", "jpeg", "gif", "webp", "bmp", "ico", "svg",
"mp3", "wav", "aiff", "aac", "ogg", "flac", "m4a",
"mp4", "mov", "avi", "mkv", "webm",
"zip", "tar", "gz", "bz2", "7z", "rar",
"exe", "dll", "so", "dylib", "o", "a",
"woff", "woff2", "ttf", "otf", "eot",
"pdf",
]);
const READ_MANY_TRUNCATE_LINES = 500;
const READ_MANY_SHOW_LINES = 100;
async function readManyFiles(input: Record<string, unknown>): Promise<ToolResult> {
const pattern = input.pattern as string;
const basePath = (input.path as string) || process.cwd();
const limit = Math.min(Math.max((input.limit as number) || 20, 1), 50);
// Use globSearch to find matching files
const globResult = globSearch({ pattern, path: basePath });
if (!globResult.success) {
return { success: false, output: globResult.output };
}
// Parse file paths from globSearch output (format: "N files:\npath1\npath2\n...")
const outputLines = globResult.output.split("\n");
if (outputLines[0] === "No files found") {
return { success: true, output: "No files matched the pattern." };
}
// First line is "N files:", rest are paths
const allFiles = outputLines.slice(1).filter(Boolean);
const totalFound = allFiles.length;
const filesToRead = allFiles.slice(0, limit);
const parts: string[] = [];
let filesRead = 0;
let skipped = 0;
for (const filePath of filesToRead) {
const ext = filePath.split(".").pop()?.toLowerCase() || "";
// Skip binary files (images, audio, video, archives, etc.)
if (BINARY_EXTENSIONS.has(ext)) {
parts.push(`=== ${filePath} ===\n[Binary file — ${ext}]\n`);
skipped++;
continue;
}
const absPath = resolve(resolvePath(basePath), resolvePath(filePath));
const targetPath = existsSync(absPath) ? absPath : filePath;
if (!existsSync(targetPath)) {
parts.push(`=== ${filePath} ===\n[File not found]\n`);
skipped++;
continue;
}
try {
const stat = statSync(targetPath);
// Skip files > 1MB
if (stat.size > 1_000_000) {
parts.push(`=== ${filePath} ===\n[File too large: ${(stat.size / 1024).toFixed(0)}KB]\n`);
skipped++;
continue;
}
const content = readFileSync(targetPath, "utf-8");
const lines = content.split("\n");
if (lines.length > READ_MANY_TRUNCATE_LINES) {
const truncated = lines.slice(0, READ_MANY_SHOW_LINES).join("\n");
parts.push(`=== ${filePath} ===\n${truncated}\n[... truncated — ${lines.length} total lines, showing first ${READ_MANY_SHOW_LINES}]\n`);
} else {
parts.push(`=== ${filePath} ===\n${content}\n`);
}
filesRead++;
} catch (err) {
parts.push(`=== ${filePath} ===\n[Error reading file: ${err}]\n`);
skipped++;
}
}
const summary = `Read ${filesRead} files, skipped ${skipped}${totalFound > limit ? `, ${totalFound - limit} more matched but not read (limit: ${limit})` : ""} — ${totalFound} total matches`;
parts.push(summary);
return { success: true, output: parts.join("\n") };
}
// ============================================================================
// EXECUTOR — dispatches to tool implementations in ./tools/
// ============================================================================
export async function executeLocalTool(
name: string,
input: Record<string, unknown>
): Promise<ToolResult> {
try {
switch (name) {
// File operations (tools/file-ops.ts)
case "read_file": return await readFile(input);
case "read_many_files": return await readManyFiles(input);
case "write_file": return writeFile(input);
case "edit_file": return editFile(input);
case "list_directory": return listDirectory(input);
case "search_files": return searchFiles(input);
case "search_content": return searchContent(input);
case "multi_edit": return multiEdit(input);
case "notebook_edit": return notebookEdit(input);
// Search (tools/search-tools.ts)
case "glob": return globSearch(input);
case "grep": return grepSearch(input);
// Shell execution (tools/shell-exec.ts)
case "run_command": return await runCommand(input);
case "bash_output": return bashOutput(input);
case "kill_shell": return killShell(input);
case "list_shells": return listShellsFn();
// Web (tools/web-tools.ts)
case "web_fetch": return await webFetch(input);
case "web_search": return await webSearch(input);
// Tasks (tools/task-manager.ts)
case "tasks": return tasksTool(input);
// Agent tools (tools/agent-tools.ts)
case "task": return await taskTool(input);
case "team_create": return await teamCreateTool(input);
case "task_output": return await taskOutput(input);
case "task_stop": return taskStop(input);
case "config": return configTool(input);
case "ask_user": return askUser(input);
case "lsp": return await lspTool(input);
case "skill": return skillTool(input);
default: return { success: false, output: `Unknown local tool: ${name}` };
}
} catch (err) {
return { success: false, output: `Error: ${err}` };
}
}