feat: 工具扩展至 20 个,新增 8 个实用工具

新增工具:
- get_file_info: 获取文件详细元数据(自动)
- tree: 树形目录结构展示(自动)
- download_file: URL 下载文件到本地(需确认)
- diff_files: 两个文件对比差异(自动)
- replace_in_files: glob 批量查找替换(需确认)
- read_multiple_files: 批量读取多个文件(自动)
- git_status: Git 仓库状态查看(自动)
- compress: 创建/解压 zip/tar.gz(需确认)
This commit is contained in:
OpenClaw Agent
2026-04-16 09:50:52 +08:00
parent 8901f841da
commit 396100335a
4 changed files with 596 additions and 7 deletions
+17 -1
View File
@@ -25,7 +25,15 @@ import {
handleCopyFile,
handleWebFetch,
handleAppendFile,
handleEditFile
handleEditFile,
handleGetFileInfo,
handleTree,
handleDownloadFile,
handleDiffFiles,
handleReplaceInFiles,
handleReadMultipleFiles,
handleGitStatus,
handleCompress
} from './tool-handlers.js';
import { getAllowedDirs, getBlockedDirs, setAllowedDirs } from './tool-security.js';
import { startProcess, killProcess, getWorkspaceDir, setWorkspaceDir, listWorkspaceDir } from './workspace.js';
@@ -118,6 +126,14 @@ export function setupIPC(): void {
case 'web_fetch': return await handleWebFetch(args as { url: string; max_chars?: number; extract_mode?: string });
case 'append_file': return await handleAppendFile(args as { path: string; content: string; newline?: boolean });
case 'edit_file': return await handleEditFile(args as { path: string; old_text: string; new_text: string; all?: boolean });
case 'get_file_info': return await handleGetFileInfo(args as { path: string });
case 'tree': return await handleTree(args as { path: string; max_depth?: number; include_hidden?: boolean });
case 'download_file': return await handleDownloadFile(args as { url: string; destination: string });
case 'diff_files': return await handleDiffFiles(args as { file1: string; file2: string; context_lines?: number });
case 'replace_in_files': return await handleReplaceInFiles(args as { path: string; glob: string; old_text: string; new_text: string });
case 'read_multiple_files':return await handleReadMultipleFiles(args as { paths: string[]; max_chars_per_file?: number });
case 'git_status': return await handleGitStatus(args as { path?: string });
case 'compress': return await handleCompress(args as { action: string; path: string; destination?: string; format?: string });
default: return { success: false, error: `未知工具: ${toolName}` };
}
} catch (err) {
+399
View File
@@ -522,3 +522,402 @@ export async function handleEditFile(params: { path: string; old_text: string; n
return { success: false, error: (err as Error).message };
}
}
export async function handleGetFileInfo(params: { path: string }): Promise<ToolResult> {
try {
const filePath = resolvePath(params.path);
const allowed = checkPathAllowed(filePath, 'read');
if (!allowed.ok) return { success: false, error: allowed.reason };
const stat = await fs.stat(filePath);
return {
success: true,
path: filePath,
name: path.basename(filePath),
type: stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : 'other',
size: stat.size,
created: stat.birthtime.toISOString(),
modified: stat.mtime.toISOString(),
accessed: stat.atime.toISOString(),
permissions: (stat.mode & 0o777).toString(8)
};
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
export async function handleTree(params: { path: string; max_depth?: number; include_hidden?: boolean }): Promise<ToolResult> {
try {
const rootPath = resolvePath(params.path);
const allowed = checkPathAllowed(rootPath, 'read');
if (!allowed.ok) return { success: false, error: allowed.reason };
const maxDepth = params.max_depth || 3;
const includeHidden = params.include_hidden || false;
const lines: string[] = [];
let fileCount = 0;
let dirCount = 0;
async function walk(dir: string, prefix: string, depth: number): Promise<void> {
if (depth > maxDepth) return;
let items;
try { items = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
const filtered = items
.filter(i => includeHidden || !i.name.startsWith('.'))
.sort((a, b) => {
if (a.isDirectory() && !b.isDirectory()) return -1;
if (!a.isDirectory() && b.isDirectory()) return 1;
return a.name.localeCompare(b.name);
});
for (let i = 0; i < filtered.length; i++) {
const item = filtered[i];
const isLast = i === filtered.length - 1;
const connector = isLast ? '└── ' : '├── ';
const icon = item.isDirectory() ? '📁' : '📄';
lines.push(`${prefix}${connector}${icon} ${item.name}`);
if (item.isDirectory()) {
dirCount++;
const childPrefix = prefix + (isLast ? ' ' : '│ ');
await walk(path.join(dir, item.name), childPrefix, depth + 1);
} else {
fileCount++;
}
}
}
lines.push(`📁 ${path.basename(rootPath)}/`);
await walk(rootPath, '', 1);
return {
success: true,
path: rootPath,
tree: lines.join('\n'),
fileCount,
dirCount
};
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
export async function handleDownloadFile(params: { url: string; destination: string }): Promise<ToolResult> {
try {
const destPath = resolvePath(params.destination);
const allowed = checkPathAllowed(destPath, 'write');
if (!allowed.ok) return { success: false, error: allowed.reason };
if (!params.url.startsWith('http://') && !params.url.startsWith('https://')) {
return { success: false, error: '仅支持 http/https 协议' };
}
const resp = await fetch(params.url, {
signal: AbortSignal.timeout(60000)
});
if (!resp.ok) return { success: false, error: `HTTP ${resp.status}: ${resp.statusText}` };
const buf = Buffer.from(await resp.arrayBuffer());
if (buf.length > 50 * 1024 * 1024) {
return { success: false, error: '文件超过 50MB 限制' };
}
const destDir = path.dirname(destPath);
await fs.mkdir(destDir, { recursive: true });
await fs.writeFile(destPath, buf);
return {
success: true,
url: params.url,
destination: destPath,
size: buf.length,
content_type: resp.headers.get('content-type') || 'unknown'
};
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
export async function handleDiffFiles(params: { file1: string; file2: string; context_lines?: number }): Promise<ToolResult> {
try {
const path1 = resolvePath(params.file1);
const path2 = resolvePath(params.file2);
const check1 = checkPathAllowed(path1, 'read');
if (!check1.ok) return { success: false, error: check1.reason };
const check2 = checkPathAllowed(path2, 'read');
if (!check2.ok) return { success: false, error: check2.reason };
const content1 = await fs.readFile(path1, 'utf-8');
const content2 = await fs.readFile(path2, 'utf-8');
const lines1 = content1.split('\n');
const lines2 = content2.split('\n');
// 简单的统一 diff 实现
const diffLines: string[] = [];
const contextSize = params.context_lines || 3;
let i = 0, j = 0;
while (i < lines1.length || j < lines2.length) {
if (i < lines1.length && j < lines2.length && lines1[i] === lines2[j]) {
if (diffLines.length === 0 || diffLines[diffLines.length - 1].startsWith('-') || diffLines[diffLines.length - 1].startsWith('+')) {
// 添加上下文
const ctxStart = Math.max(0, i - contextSize);
for (let k = ctxStart; k < i; k++) {
diffLines.push(` ${lines1[k]}`);
}
}
diffLines.push(` ${lines1[i]}`);
i++; j++;
} else {
// 尝试找匹配
let found = false;
for (let look = 1; look <= 5 && i + look < lines1.length; look++) {
if (lines1[i + look] === lines2[j]) {
for (let k = 0; k < look; k++) diffLines.push(`-${lines1[i + k]}`);
i += look; found = true; break;
}
}
if (!found) {
for (let look = 1; look <= 5 && j + look < lines2.length; look++) {
if (lines1[i] === lines2[j + look]) {
for (let k = 0; k < look; k++) diffLines.push(`+${lines2[j + k]}`);
j += look; found = true; break;
}
}
}
if (!found) {
if (i < lines1.length) { diffLines.push(`-${lines1[i]}`); i++; }
if (j < lines2.length) { diffLines.push(`+${lines2[j]}`); j++; }
}
}
}
const hasChanges = diffLines.some(l => l.startsWith('-') || l.startsWith('+'));
return {
success: true,
file1: path1,
file2: path2,
diff: diffLines.join('\n'),
hasChanges,
lines1: lines1.length,
lines2: lines2.length
};
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
export async function handleReplaceInFiles(params: { path: string; glob: string; old_text: string; new_text: string }): Promise<ToolResult> {
try {
const rootPath = resolvePath(params.path);
const allowed = checkPathAllowed(rootPath, 'write');
if (!allowed.ok) return { success: false, error: allowed.reason };
// 简单 glob 匹配
const pattern = params.glob.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.');
const regex = new RegExp(`^${pattern}$`);
const results: Array<{ file: string; replacements: number }> = [];
let totalReplacements = 0;
const maxFiles = 200;
let fileCount = 0;
async function scanDir(dir: string): Promise<void> {
if (fileCount >= maxFiles) return;
let items;
try { items = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
for (const item of items) {
if (fileCount >= maxFiles) return;
if (item.name.startsWith('.')) continue;
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
await scanDir(fullPath);
} else if (regex.test(item.name)) {
fileCount++;
try {
const content = await fs.readFile(fullPath, 'utf-8');
if (content.includes(params.old_text)) {
const parts = content.split(params.old_text);
const count = parts.length - 1;
const newContent = parts.join(params.new_text);
await fs.writeFile(fullPath, newContent, 'utf-8');
results.push({ file: path.relative(rootPath, fullPath), replacements: count });
totalReplacements += count;
}
} catch { /* skip unreadable */ }
}
}
}
await scanDir(rootPath);
return {
success: true,
pattern: params.glob,
filesChanged: results.length,
totalReplacements,
results,
truncated: fileCount >= maxFiles
};
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
export async function handleReadMultipleFiles(params: { paths: string[]; max_chars_per_file?: number }): Promise<ToolResult> {
try {
const maxChars = params.max_chars_per_file || 2000;
const results: Array<{ path: string; success: boolean; content?: string; error?: string; lines?: number }> = [];
for (const p of params.paths.slice(0, 20)) {
const filePath = resolvePath(p);
const allowed = checkPathAllowed(filePath, 'read');
if (!allowed.ok) {
results.push({ path: p, success: false, error: allowed.reason });
continue;
}
try {
const stat = await fs.stat(filePath);
if (stat.size > 1024 * 1024) {
results.push({ path: p, success: false, error: '文件超过 1MB' });
continue;
}
let content = await fs.readFile(filePath, 'utf-8');
const lines = content.split('\n').length;
if (content.length > maxChars) content = content.slice(0, maxChars) + '\n... (已截断)';
results.push({ path: p, success: true, content, lines });
} catch (err) {
results.push({ path: p, success: false, error: (err as Error).message });
}
}
return { success: true, files: results, total: results.length };
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
export async function handleGitStatus(params: { path?: string }): Promise<ToolResult> {
try {
const cwd = params.path ? path.resolve(params.path) : getWorkspaceDir();
return new Promise((resolve) => {
const git = spawn('git', ['status', '--porcelain=v1', '-b'], { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
git.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
git.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
git.on('close', (code) => {
if (code !== 0) {
resolve({ success: false, error: stderr || '不是 git 仓库' });
return;
}
const lines = stdout.split('\n').filter(l => l.trim());
const branchLine = lines.find(l => l.startsWith('##')) || '';
const branch = branchLine.replace('## ', '').split('...')[0] || 'unknown';
const modified: string[] = [];
const staged: string[] = [];
const untracked: string[] = [];
for (const line of lines) {
if (line.startsWith('##')) continue;
const status = line.slice(0, 2);
const file = line.slice(3);
if (status === '??') untracked.push(file);
else {
if (status[0] !== ' ' && status[0] !== '?') staged.push(file);
if (status[1] !== ' ' && status[1] !== '?') modified.push(file);
}
}
resolve({
success: true,
branch,
modified,
staged,
untracked,
isClean: modified.length === 0 && staged.length === 0 && untracked.length === 0
});
});
git.on('error', (err) => {
resolve({ success: false, error: err.message });
});
});
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
export async function handleCompress(params: { action: string; path: string; destination?: string; format?: string }): Promise<ToolResult> {
try {
const format = params.format || 'tar.gz';
if (params.action === 'create') {
const srcPath = resolvePath(params.path);
const check = checkPathAllowed(srcPath, 'read');
if (!check.ok) return { success: false, error: check.reason };
const destPath = params.destination
? resolvePath(params.destination)
: srcPath + (format === 'zip' ? '.zip' : '.tar.gz');
const destCheck = checkPathAllowed(destPath, 'write');
if (!destCheck.ok) return { success: false, error: destCheck.reason };
return new Promise((resolve) => {
const cmd = format === 'zip'
? `zip -r "${destPath}" "${path.basename(srcPath)}"`
: `tar czf "${destPath}" "${path.basename(srcPath)}"`;
const proc = spawn('bash', ['-c', cmd], { cwd: path.dirname(srcPath), stdio: ['pipe', 'pipe', 'pipe'] });
let stderr = '';
proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
proc.on('close', (code) => {
if (code === 0) {
fs.stat(destPath).then(s => {
resolve({ success: true, action: 'create', archive: destPath, size: s.size });
}).catch(() => resolve({ success: true, action: 'create', archive: destPath }));
} else {
resolve({ success: false, error: stderr || `压缩失败 (exit ${code})` });
}
});
proc.on('error', (err) => resolve({ success: false, error: err.message }));
});
} else {
// extract
const archivePath = resolvePath(params.path);
const check = checkPathAllowed(archivePath, 'read');
if (!check.ok) return { success: false, error: check.reason };
const destDir = params.destination ? resolvePath(params.destination) : path.dirname(archivePath);
const destCheck = checkPathAllowed(destDir, 'write');
if (!destCheck.ok) return { success: false, error: destCheck.reason };
await fs.mkdir(destDir, { recursive: true });
return new Promise((resolve) => {
const isZip = archivePath.endsWith('.zip');
const cmd = isZip
? `unzip -o "${archivePath}" -d "${destDir}"`
: `tar xzf "${archivePath}" -C "${destDir}"`;
const proc = spawn('bash', ['-c', cmd], { stdio: ['pipe', 'pipe', 'pipe'] });
let stderr = '';
proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
proc.on('close', (code) => {
resolve(code === 0
? { success: true, action: 'extract', destination: destDir }
: { success: false, error: stderr || `解压失败 (exit ${code})` }
);
});
proc.on('error', (err) => resolve({ success: false, error: err.message }));
});
}
} catch (err) {
return { success: false, error: (err as Error).message };
}
}
+34 -2
View File
@@ -350,7 +350,7 @@
<div class="modal-body">
<div class="help-section"><h4>🚀 快速开始</h4><ol><li>确保 Ollama 已启动(默认 <code>http://127.0.0.1:11434</code>,可在设置中修改)</li><li>顶部模型栏选择一个模型,右侧会显示模型能力徽章(🧠 Think / 👁️ Vision / 🔧 Tools</li><li>输入消息,按 <kbd>Enter</kbd> 发送,<kbd>Shift+Enter</kbd> 换行</li></ol></div>
<div class="help-section"><h4>💬 聊天功能</h4><ul><li><strong>流式回复</strong> — 实时打字效果,随时点 ■ 停止</li><li><strong>Think 推理</strong> — 设置中开启,让模型展示深度思考过程(需模型支持,如 Qwen3)</li><li><strong>多模态</strong> — 上传图片,模型需支持 Vision 能力</li><li><strong>文件分析</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB,内容以代码块发送给模型</li><li><strong>上下文长度</strong> — 设置中调整 <code>num_ctx</code>,越大记忆越长,消耗显存越多</li></ul></div>
<div class="help-section"><h4>🔧 Tool CallingAI 自主操作)</h4><ul><li>设置中开启后,AI 可以在对话中<strong>自主调用本地工具</strong>,像一个本地 Agent</li><li><strong>12 个工具</strong><code>read_file</code> / <code>write_file</code> / <code>list_directory</code> / <code>search_files</code> / <code>create_directory</code> / <code>delete_file</code> / <code>move_file</code> / <code>copy_file</code> / <code>web_fetch</code> / <code>append_file</code> / <code>edit_file</code> / <code>run_command</code></li><li>写入/删除/命令执行等高风险操作会弹出<strong>确认对话框</strong>,你可以批准或拒绝</li><li>危险命令(<code>rm -rf</code><code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code><code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error</li><li>最多自动循环 <strong>10 轮</strong>工具调用,也可随时中断</li><li>⚠️ 需要模型支持 Tool Calling(推荐 Qwen3、Llama 3.1+、Mistral</li></ul></div>
<div class="help-section"><h4>🔧 Tool CallingAI 自主操作)</h4><ul><li>设置中开启后,AI 可以在对话中<strong>自主调用本地工具</strong>,像一个本地 Agent</li><li><strong>20 个工具</strong>文件读写、目录管理、搜索替换、网页抓取、下载、Git、压缩/解压等</li><li>写入/删除/命令执行等高风险操作会弹出<strong>确认对话框</strong>,你可以批准或拒绝</li><li>危险命令(<code>rm -rf</code><code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code><code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error</li><li>最多自动循环 <strong>10 轮</strong>工具调用,也可随时中断</li><li>⚠️ 需要模型支持 Tool Calling(推荐 Qwen3、Llama 3.1+、Mistral</li></ul></div>
<div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>设置中开启后,AI 从对话中<strong>自动提取关键信息</strong>(事实/偏好/规则),跨会话持续积累</li><li>对话满 <strong>6 条消息</strong>后自动触发记忆提取</li><li>新对话时自动检索相关记忆注入上下文,让 AI "记住"你</li><li>点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除</li><li><strong>向量语义搜索</strong>:在设置中选择嵌入模型后,支持语义级别记忆检索(更精准)</li><li>未选择嵌入模型时,使用关键词匹配检索记忆</li><li>记忆存储在本地 IndexedDB,不会上传到任何服务器</li></ul></div>
<div class="help-section"><h4>🖥️ 布局说明</h4><ul><li><strong>左侧面板</strong> — 执行日志,实时显示应用运行日志(连接、模型加载、工具调用等)</li><li><strong>中间区域</strong> — 聊天消息,顶部 Header + 模型栏,底部输入框</li><li><strong>右侧面板</strong> — 工作空间(常驻显示),包含:<ul><li><strong>💻 命令行 Tab</strong> — 终端界面,实时流式输出,支持长时间运行(无超时),多终端 Tab</li><li><strong>📁 文件 Tab</strong> — 浏览工作空间目录,点击文件预览内容</li></ul></li><li>工作空间面板宽度可通过拖拽左边缘调整(300–700px)</li><li>工作空间目录可在设置中修改</li></ul></div>
<div class="help-section"><h4>🕐 历史记录</h4><ul><li>所有会话自动保存到本地 IndexedDB</li><li>点击顶部 🕐 按钮查看、搜索、恢复历史会话</li><li>支持导出 Markdown / HTML / TXT / <code>.metona</code> 加密备份</li></ul></div>
@@ -363,7 +363,7 @@
<div class="modal-overlay" id="toolsModal" style="display:none;">
<div class="modal">
<div class="modal-header">
<h3>🔧 工具面板(12 个)</h3>
<h3>🔧 工具面板(20 个)</h3>
<button class="icon-btn" id="btnCloseTools">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
@@ -436,6 +436,38 @@
</label>
</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon"></span><span class="tool-card-name">get_file_info</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">获取文件/目录详细信息:大小、日期、权限</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🌳</span><span class="tool-card-name">tree</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">以树形结构展示目录内容</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">⬇️</span><span class="tool-card-name">download_file</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">从 URL 下载文件到本地</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🔀</span><span class="tool-card-name">diff_files</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">对比两个文件,显示差异</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🔄</span><span class="tool-card-name">replace_in_files</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">按 glob 模式批量查找替换多个文件</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">📚</span><span class="tool-card-name">read_multiple_files</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">一次读取多个文件内容</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🔖</span><span class="tool-card-name">git_status</span><span class="tool-card-badge auto">自动</span></div>
<div class="tool-card-desc">查看 Git 仓库状态:分支、修改、暂存</div>
</div>
<div class="tool-card">
<div class="tool-card-header"><span class="tool-card-icon">🗜️</span><span class="tool-card-name">compress</span><span class="tool-card-badge confirm">需确认</span></div>
<div class="tool-card-desc">创建或解压 zip/tar.gz 归档</div>
</div>
</div>
</div>
</div>
+146 -4
View File
@@ -198,10 +198,134 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
}
}
}
},
{
type: 'function',
function: {
name: 'get_file_info',
description: 'Get detailed file or directory information: size, dates, permissions, type.',
parameters: {
type: 'object',
required: ['path'],
properties: {
path: { type: 'string', description: 'File or directory path.' }
}
}
}
},
{
type: 'function',
function: {
name: 'tree',
description: 'Display directory structure as a tree. Shows nested files and folders.',
parameters: {
type: 'object',
required: ['path'],
properties: {
path: { type: 'string', description: 'Root directory path.' },
max_depth: { type: 'integer', description: 'Max depth. Default: 3.' },
include_hidden: { type: 'boolean', description: 'Include hidden files. Default: false.' }
}
}
}
},
{
type: 'function',
function: {
name: 'download_file',
description: 'Download a file from a URL to a local path. Requires user confirmation.',
parameters: {
type: 'object',
required: ['url', 'destination'],
properties: {
url: { type: 'string', description: 'URL to download (http/https).' },
destination: { type: 'string', description: 'Local path to save the file.' }
}
}
}
},
{
type: 'function',
function: {
name: 'diff_files',
description: 'Compare two files and show differences. Returns a unified diff.',
parameters: {
type: 'object',
required: ['file1', 'file2'],
properties: {
file1: { type: 'string', description: 'First file path.' },
file2: { type: 'string', description: 'Second file path.' },
context_lines: { type: 'integer', description: 'Context lines around changes. Default: 3.' }
}
}
}
},
{
type: 'function',
function: {
name: 'replace_in_files',
description: 'Find and replace text across multiple files matching a glob pattern. Requires user confirmation.',
parameters: {
type: 'object',
required: ['path', 'glob', 'old_text', 'new_text'],
properties: {
path: { type: 'string', description: 'Root directory.' },
glob: { type: 'string', description: 'File pattern, e.g. "*.ts", "**/*.js".' },
old_text: { type: 'string', description: 'Text to find.' },
new_text: { type: 'string', description: 'Replacement text.' }
}
}
}
},
{
type: 'function',
function: {
name: 'read_multiple_files',
description: 'Read multiple files at once. Returns contents of all requested files.',
parameters: {
type: 'object',
required: ['paths'],
properties: {
paths: { type: 'array', items: { type: 'string' }, description: 'Array of file paths to read.' },
max_chars_per_file: { type: 'integer', description: 'Max chars per file. Default: 2000.' }
}
}
}
},
{
type: 'function',
function: {
name: 'git_status',
description: 'Show git repository status: branch, modified/staged/untracked files.',
parameters: {
type: 'object',
required: [],
properties: {
path: { type: 'string', description: 'Repository path. Defaults to workspace.' }
}
}
}
},
{
type: 'function',
function: {
name: 'compress',
description: 'Create or extract archives (zip/tar.gz). Requires user confirmation for create.',
parameters: {
type: 'object',
required: ['action', 'path'],
properties: {
action: { type: 'string', enum: ['create', 'extract'], description: 'Create or extract archive.' },
path: { type: 'string', description: 'Source path (create) or archive path (extract).' },
destination: { type: 'string', description: 'Output archive path (create) or extract dir (extract).' },
format: { type: 'string', enum: ['zip', 'tar.gz'], description: 'Archive format. Default: tar.gz.' }
}
}
}
}
];
const CONFIRM_TOOLS = ['write_file', 'delete_file', 'run_command', 'create_directory', 'move_file', 'copy_file', 'append_file', 'edit_file'];
const CONFIRM_TOOLS = ['write_file', 'delete_file', 'run_command', 'create_directory', 'move_file', 'copy_file', 'append_file', 'edit_file', 'download_file', 'replace_in_files', 'compress'];
export function needsConfirmation(toolName: string): boolean {
return CONFIRM_TOOLS.includes(toolName);
@@ -211,7 +335,9 @@ let enabledTools: Set<string> = new Set([
'read_file', 'list_directory', 'search_files',
'write_file', 'create_directory', 'delete_file',
'run_command',
'move_file', 'copy_file', 'web_fetch', 'append_file', 'edit_file'
'move_file', 'copy_file', 'web_fetch', 'append_file', 'edit_file',
'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files',
'read_multiple_files', 'git_status', 'compress'
]);
export function setToolEnabled(toolName: string, enabled: boolean): void {
@@ -289,7 +415,15 @@ export function getToolIcon(name: string): string {
copy_file: '📋',
web_fetch: '🌐',
append_file: '',
edit_file: '✂️'
edit_file: '✂️',
get_file_info: '️',
tree: '🌳',
download_file: '⬇️',
diff_files: '🔀',
replace_in_files: '🔄',
read_multiple_files: '📚',
git_status: '🔖',
compress: '🗜️'
};
return icons[name] || '🔧';
}
@@ -307,7 +441,15 @@ export function formatToolName(name: string): string {
copy_file: '复制文件',
web_fetch: '网页抓取',
append_file: '追加文件',
edit_file: '编辑文件'
edit_file: '编辑文件',
get_file_info: '文件信息',
tree: '目录树',
download_file: '下载文件',
diff_files: '文件对比',
replace_in_files: '批量替换',
read_multiple_files: '批量读取',
git_status: 'Git 状态',
compress: '压缩/解压'
};
return names[name] || name;
}