From 51a84bd0ad26357fdc9edd89f03d239903282cc8 Mon Sep 17 00:00:00 2001 From: thzxx Date: Sat, 18 Apr 2026 12:09:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v5.1.0=20-=20=E8=AE=B0=E5=BF=86replace/?= =?UTF-8?q?remove=E3=80=81Skill=E6=B8=90=E8=BF=9B=E5=BC=8F=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E3=80=81=E5=AE=89=E5=85=A8=E6=89=AB=E6=8F=8F=E3=80=81?= =?UTF-8?q?=E9=A2=84=E7=AE=97=E5=8F=AF=E9=85=8D=E7=BD=AE=E3=80=81=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=88=AA=E6=96=AD=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- docs/BUILD.md | 10 ++- docs/CHANGELOG.md | 29 +++++- docs/DEVELOPMENT.md | 2 +- package-lock.json | 4 +- package.json | 2 +- src/renderer/index.html | 24 ++++- src/renderer/services/agent-engine.ts | 41 +++++++-- src/renderer/services/memory-manager.ts | 113 ++++++++++++++++++++++++ src/renderer/services/skill-manager.ts | 53 +++++++++++ src/renderer/services/tool-registry.ts | 95 +++++++++++++++++++- 11 files changed, 355 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 3d9dab9..80b2165 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 全离线运行 · 数据本地存储 · 零外部依赖 -[![版本](https://img.shields.io/badge/version-5.0.1-brightgreen?style=flat-square)](https://gitee.com/thzxx/metona-ollama-desktop/releases) +[![版本](https://img.shields.io/badge/version-5.1.0-brightgreen?style=flat-square)](https://gitee.com/thzxx/metona-ollama-desktop/releases) [![平台](https://img.shields.io/badge/platform-Windows%20x64-blue?style=flat-square)](https://gitee.com/thzxx/metona-ollama-desktop/releases) [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178c6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![Electron](https://img.shields.io/badge/Electron-33-47848f?style=flat-square&logo=electron&logoColor=white)](https://www.electronjs.org/) @@ -31,7 +31,7 @@ --- -### 🔧 Tool Calling — 34 个内置工具 +### 🔧 Tool Calling — 38 个内置工具 AI 在对话中**主动调用**本地工具完成任务,所有操作在用户可视化监督下执行。采用 **ReAct Agent Loop**(Thought → Action → Observation → Reflection),最大 15 轮,10 分钟超时。 @@ -294,7 +294,7 @@ src/ | 文件 | 类型 | |------|------| -| `Metona Ollama Setup 5.0.1.exe` | NSIS 安装包(可选目录、创建快捷方式) | +| `Metona Ollama Setup 5.1.0.exe` | NSIS 安装包(可选目录、创建快捷方式) | > ⚠️ 未签名版本,首次运行 Windows 可能弹出安全警告,点击「仍要运行」即可。 diff --git a/docs/BUILD.md b/docs/BUILD.md index 148099d..a1aaf0d 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -14,7 +14,7 @@ # 1. 克隆并安装依赖 git clone https://gitee.com/thzxx/metona-ollama-desktop.git cd metona-ollama-desktop -git checkout metona-ollama-desktop-v5.0.1 +git checkout metona-ollama-desktop-v5.1.0 npm config set registry https://registry.npmmirror.com npm install @@ -176,6 +176,14 @@ electron-builder 会自动处理所有依赖下载(Electron、NSIS、winCodeSi ## 构建日志 +### v5.1.0(2026-04-18) — metona-ollama-desktop 仓库 + +- ✨ 新增 4 个工具:memory_replace、memory_remove、skill_list、skill_view(总数 38 个) +- ✨ 记忆安全扫描(prompt injection / 敏感信息 / 不可见字符检测) +- 🔧 Agent Loop 迭代预算可配置化(maxTurns) +- 🔧 工具结果截断按工具独立配置 +- 📝 版本号更新至 5.1.0 + ### v5.0.1(2026-04-18) — metona-ollama-desktop 仓库 - 🔧 修复 DuckDuckGo 搜索超时过长(15s → 8s),添加 Google 作为第三搜索引擎兜底 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bf79d34..0deb407 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,7 +3,7 @@ ## 分支结构 **桌面版主线**(`master`): -- `metona-ollama-desktop-v3.2.6` → `metona-ollama-desktop-v3.3.4` → `metona-ollama-desktop-v4.0.0` → `metona-ollama-desktop-v5.0.0` → `metona-ollama-desktop-v5.0.1`(当前发布分支) +- `metona-ollama-desktop-v3.2.6` → `metona-ollama-desktop-v3.3.4` → `metona-ollama-desktop-v4.0.0` → `metona-ollama-desktop-v5.0.0` → `metona-ollama-desktop-v5.0.1` → `metona-ollama-desktop-v5.1.0`(当前发布分支) **Web 版历史**已独立为 [metona-ollama-web](https://gitee.com/thzxx/metona-ollama-web) 仓库(v1.1.0 ~ v4.0.3)。 @@ -11,6 +11,33 @@ ## 桌面版(Desktop) +### Desktop v5.1.0 — Hermes 借鉴 & 记忆/Skill 系统增强 + +- ✨ **记忆 replace/remove 操作**(借鉴 Hermes Agent) + - 新增 `memory_replace` 工具:子串匹配 old_text,替换为 new_content + - 新增 `memory_remove` 工具:子串匹配 old_text,删除对应记忆 + - 两种目标:memory(Agent 笔记)和 user(用户画像) + - 唯一匹配约束:匹配多条时返回错误,要求更精确的子串 +- ✨ **Skill 渐进式加载**(借鉴 Hermes Agent) + - 新增 `skill_list` 工具(Level 0):返回技能名称、描述、成功率、工具链预览 + - 新增 `skill_view` 工具(Level 1):返回完整工具链、参数提示、使用统计 + - Agent 先看列表,需要时再加载详情,节省 token +- ✨ **记忆安全扫描** + - 记忆写入前自动检查 prompt injection 模式(ignore previous / you are now a 等) + - 检测敏感信息(SSH 私钥、API Key、信用卡号) + - 检测不可见 Unicode 字符 + - 拦截时返回明确错误信息 +- 🔧 **迭代预算可配置化** + - Agent Loop 最大轮数从硬编码 15 改为 state 读取(`maxTurns`,默认 15) + - 用户可通过设置面板调整 +- 🔧 **工具结果截断按工具配置** + - 新增 `TOOL_MAX_RESULT_SIZE` 常量,每个工具独立配置最大返回字符数 + - web_fetch: 20000, web_search: 3000, read_file: 15000, run_command: 10000 等 +- 🔧 **更新 AGENT_SYSTEM_PROMPT** + - 工具数量 34 → 38 + - TOOL_USAGE_GUIDE 新增记忆管理和技能查看规则 +- 📝 **版本号更新至 5.1.0**(7 个文件同步更新) + ### Desktop v5.0.1 — 搜索引擎优化 & Agent 去重修复 - 🔧 **修复 DuckDuckGo 搜索超时过长** diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index d4dc86c..2a6e441 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,6 +1,6 @@ # Metona Ollama Desktop — 开发规范 -> 版本: 5.0.1 | 更新: 2026-04-18 | 维护: 项目团队 +> 版本: 5.1.0 | 更新: 2026-04-18 | 维护: 项目团队 --- diff --git a/package-lock.json b/package-lock.json index a985471..5defa32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "metona-ollama-desktop", - "version": "5.0.1", + "version": "5.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "metona-ollama-desktop", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "dependencies": { "sql.js": "^1.11.0" diff --git a/package.json b/package.json index f12f5a7..c97bab1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metona-ollama-desktop", - "version": "5.0.1", + "version": "5.1.0", "description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端", "main": "dist/main/main.js", "author": "thzxx", diff --git a/src/renderer/index.html b/src/renderer/index.html index bfe1110..b108967 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -27,7 +27,7 @@
Metona Ollama - v5.0.1 + v5.1.0
+
+
🔄memory_replace自动
+
替换已有记忆内容(子串匹配 old_text,替换为 new_content)
+
+
+
🗑️memory_remove自动
+
删除记忆条目(子串匹配 old_text)
+
📋session_list自动
列出历史会话,支持按标题搜索
@@ -560,6 +568,14 @@
🤖spawn_task自动
生成子代理独立执行任务,完成后回传结果
+
+
📚skill_list自动
+
列出所有自动生成的技能(名称、描述、成功率)
+
+
+
📖skill_view自动
+
查看技能详情(完整工具链、参数提示、使用统计)
+
🌐browser_open自动
在 Agent 浏览器中打开 URL
diff --git a/src/renderer/services/agent-engine.ts b/src/renderer/services/agent-engine.ts index 458d871..04a41d5 100644 --- a/src/renderer/services/agent-engine.ts +++ b/src/renderer/services/agent-engine.ts @@ -28,16 +28,30 @@ import type { TraceEntry } from '../types.js'; -const MAX_LOOPS = 15; const MAX_LOOP_TIME = 600000; // 10 分钟总超时 const TOOL_EXEC_TIMEOUT = 30000; // 工具执行超时 30 秒 const STREAM_TIMEOUT = 120000; // 单次流式调用超时 2 分钟 const MAX_RETRIES = 2; // 工具错误自动重试次数 +/** 每个工具返回给模型的最大字符数 */ +const TOOL_MAX_RESULT_SIZE: Record = { + web_fetch: 20000, // 网页内容通常较长 + web_search: 3000, // 搜索结果已精简 + read_file: 15000, // 文件内容 + read_multiple_files: 10000, + list_directory: 5000, + search_files: 5000, + run_command: 10000, // 命令输出 + git: 5000, + session_read: 15000, + browser_extract: 10000, + browser_evaluate: 8000, +}; + /** v4.1: 工具并行执行 — 依赖检测用 */ const TOOLS_WITH_DATA_DEPS = new Set(['web_fetch', 'edit_file', 'append_file', 'move_file', 'delete_file']); -const AGENT_SYSTEM_PROMPT = `你是一个具备工具调用能力的 AI 助手。你有 34 个工具可以调用,包括文件操作、联网搜索、网页抓取、命令执行、浏览器控制、记忆管理、MCP 工具等。 +const AGENT_SYSTEM_PROMPT = `你是一个具备工具调用能力的 AI 助手。你有 38 个工具可以调用,包括文件操作、联网搜索、网页抓取、命令执行、浏览器控制、记忆管理、MCP 工具等。 ## 核心规则 @@ -73,7 +87,9 @@ const TOOL_USAGE_GUIDE = `【工具链规则(强制执行)】 7. 不要重复调用:已经成功调用过的工具+参数组合不要再调用。 8. 记忆工具:可以用 memory_search 搜索过去记忆,用 memory_add 添加重要信息。 9. 会话工具:可以用 session_list 列出历史会话,用 session_read 读取会话内容。 -10. 并行调用:当多个工具调用互相独立时,可以在同一次回复中同时调用它们。`; +10. 并行调用:当多个工具调用互相独立时,可以在同一次回复中同时调用它们。 +11. 管理记忆:可以用 memory_replace 更新已有记忆(子串匹配 old_text),用 memory_remove 删除不再需要的记忆。 +12. 查看技能:可以用 skill_list 列出所有可用技能,用 skill_view 查看特定技能的详细工具链。`; /** 工具名白名单:用于文本解析兜底时过滤非法工具名 */ const VALID_TOOL_NAMES = new Set([ @@ -81,7 +97,8 @@ const VALID_TOOL_NAMES = new Set([ 'delete_file', 'run_command', 'move_file', 'copy_file', 'web_fetch', 'web_search', 'append_file', 'edit_file', 'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files', 'read_multiple_files', 'git', 'compress', - 'memory_search', 'memory_add', 'session_list', 'session_read' + 'memory_search', 'memory_add', 'memory_replace', 'memory_remove', 'session_list', 'session_read', + 'skill_list', 'skill_view' ]); /** @@ -234,7 +251,8 @@ function formatToolResultForModel(toolName: string, result: ToolResult): string case 'web_fetch': { let content = (result.content as string) || ''; // 截断过长内容,避免撑爆上下文 - if (content.length > 15000) content = content.slice(0, 15000) + '\n... (已截断)'; + const webFetchMax = TOOL_MAX_RESULT_SIZE['web_fetch'] || 20000; + if (content.length > webFetchMax) content = content.slice(0, webFetchMax) + '\n... (已截断)'; return JSON.stringify({ success: true, url: result.url, content }); } @@ -315,7 +333,11 @@ function formatToolResultForModel(toolName: string, result: ToolResult): string k === 'status' || k === 'length' || k === 'isDirectory') continue; clean[k] = v; } - return JSON.stringify(clean); + let json = JSON.stringify(clean); + // 按工具配置截断 + const maxLen = TOOL_MAX_RESULT_SIZE[toolName] || 15000; + if (json.length > maxLen) json = json.slice(0, maxLen) + '\n... (已截断)'; + return json; } } } @@ -466,6 +488,9 @@ export async function runAgentLoop( logInfo(`ReAct Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}, tokens≈${estimateTokens(messages.map(m => m.content || '').join(''))}`); + // 迭代预算:从 state 读取,默认 15 + const maxLoops = state.get('maxTurns', 15); + let loopCount = 0; const allToolRecords: ToolCallRecord[] = []; const loopStartTime = Date.now(); @@ -481,7 +506,7 @@ export async function runAgentLoop( return { eval_count: totalEvalCount || undefined, total_duration: totalDuration }; }; - while (loopCount < MAX_LOOPS) { + while (loopCount < maxLoops) { loopCount++; // 全局超时检查 @@ -498,7 +523,7 @@ export async function runAgentLoop( return; } - logAgentLoop(loopCount, MAX_LOOPS); + logAgentLoop(loopCount, maxLoops); let thinking = ''; content = ''; diff --git a/src/renderer/services/memory-manager.ts b/src/renderer/services/memory-manager.ts index 9f00791..cbd5d7c 100644 --- a/src/renderer/services/memory-manager.ts +++ b/src/renderer/services/memory-manager.ts @@ -248,6 +248,13 @@ export async function addMemory(data: { }): Promise { const db = state.get(KEYS.DB); + // 安全扫描:检测 prompt injection 和敏感信息 + const securityCheck = scanMemorySecurity(data.content); + if (!securityCheck.safe) { + logWarn('记忆安全扫描拦截', securityCheck.reason); + throw new Error(`记忆内容被安全规则拦截: ${securityCheck.reason}`); + } + // 检查重复(内容相似度 > 80% 则跳过) const existing = memoryCache.find(e => { if (e.type !== data.type) return false; @@ -293,6 +300,112 @@ export async function addMemory(data: { return entry; } +// ── 安全扫描 ── + +/** 记忆内容安全扫描 */ +function scanMemorySecurity(content: string): { safe: boolean; reason: string } { + // Prompt injection 模式 + const injectionPatterns = [ + /ignore\s+(all\s+)?previous/i, + /forget\s+(all\s+)?instructions/i, + /you\s+are\s+now\s+a/i, + /new\s+system\s*prompt/i, + /override\s+(your|the)\s+/i, + /disregard\s+(all|any|previous)/i, + ]; + for (const p of injectionPatterns) { + if (p.test(content)) return { safe: false, reason: '疑似 prompt injection 攻击' }; + } + + // 敏感信息模式 + const secretPatterns = [ + /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/, + /sk-[a-zA-Z0-9]{20,}/, + /ghp_[a-zA-Z0-9]{36}/, + /AKIA[A-Z0-9]{16}/, + /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/, // 信用卡号 + ]; + for (const p of secretPatterns) { + if (p.test(content)) return { safe: false, reason: '疑似包含敏感信息(密钥/密码/信用卡号)' }; + } + + // 不可见字符 + if (/[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/.test(content)) { + return { safe: false, reason: '包含不可见 Unicode 字符' }; + } + + return { safe: true, reason: '' }; +} + +// ── 记忆替换(根据 old_text 子串匹配)── + +export async function replaceMemoryByContent( + target: 'memory' | 'user', + oldText: string, + newContent: string +): Promise<{ success: boolean; message: string }> { + if (!oldText || oldText.length < 2) { + return { success: false, message: 'old_text 不能为空且至少 2 个字符' }; + } + if (!newContent || newContent.length < 2) { + return { success: false, message: 'new_content 不能为空且至少 2 个字符' }; + } + + // 安全扫描 + const securityCheck = scanMemorySecurity(newContent); + if (!securityCheck.safe) { + return { success: false, message: `新内容被安全规则拦截: ${securityCheck.reason}` }; + } + + // 匹配 + const matches = memoryCache.filter(e => { + if (target === 'user') { + return (e.type === 'preference' || e.type === 'fact') && e.content.includes(oldText); + } + return e.content.includes(oldText); + }); + + if (matches.length === 0) { + return { success: false, message: `未找到包含 "${oldText.slice(0, 50)}" 的记忆` }; + } + if (matches.length > 1) { + return { success: false, message: `匹配到 ${matches.length} 条记忆,请使用更精确的 old_text` }; + } + + await updateMemory(matches[0].id, { content: newContent.trim() }); + logMemory('替换记忆', `${matches[0].id}: ${oldText.slice(0, 30)} → ${newContent.slice(0, 30)}`); + return { success: true, message: `已替换记忆: ${matches[0].id}` }; +} + +// ── 记忆删除(根据 old_text 子串匹配)── + +export async function removeMemoryByContent( + target: 'memory' | 'user', + oldText: string +): Promise<{ success: boolean; message: string }> { + if (!oldText || oldText.length < 2) { + return { success: false, message: 'old_text 不能为空且至少 2 个字符' }; + } + + const matches = memoryCache.filter(e => { + if (target === 'user') { + return (e.type === 'preference' || e.type === 'fact') && e.content.includes(oldText); + } + return e.content.includes(oldText); + }); + + if (matches.length === 0) { + return { success: false, message: `未找到包含 "${oldText.slice(0, 50)}" 的记忆` }; + } + if (matches.length > 1) { + return { success: false, message: `匹配到 ${matches.length} 条记忆,请使用更精确的 old_text` }; + } + + await deleteMemory(matches[0].id); + logMemory('删除记忆', `${matches[0].id}: ${oldText.slice(0, 50)}`); + return { success: true, message: `已删除记忆: ${matches[0].id}` }; +} + // ── 记忆更新 ── export async function updateMemory(id: string, updates: Partial): Promise { diff --git a/src/renderer/services/skill-manager.ts b/src/renderer/services/skill-manager.ts index a842228..6e71e3f 100644 --- a/src/renderer/services/skill-manager.ts +++ b/src/renderer/services/skill-manager.ts @@ -268,3 +268,56 @@ function generateSkillDescription(chain: ToolChainStep[], sessionTitle: string): const steps = chain.map((s, i) => `${i + 1}. ${s.description}`).join(';'); return `从会话「${sessionTitle}」中提取。步骤:${steps}`; } + +// ── 渐进式技能加载 ── + +/** 列出所有技能(渐进式 Level 0:只返回名称、描述、成功率) */ +export async function listSkills(): Promise> { + const bridge = (window as any).metonaDesktop; + if (!bridge?.db?.getAllSkills) return []; + const allSkills: Skill[] = await bridge.db.getAllSkills(); + return allSkills.map(skill => { + const total = skill.success_count + skill.fail_count; + const rate = total > 0 ? Math.round(skill.success_count / total * 100) : 100; + let chainPreview = ''; + try { + const chain: ToolChainStep[] = JSON.parse(skill.tool_chain); + chainPreview = chain.map(s => s.name).join(' → '); + } catch { /* */ } + return { + name: skill.name, + description: skill.description, + success_rate: `${rate}%`, + chain_preview: chainPreview + }; + }); +} + +/** 查看技能详情(渐进式 Level 1:返回完整工具链和参数提示) */ +export async function viewSkill(skillName: string): Promise<{ + success: boolean; + skill?: Skill & { chain: ToolChainStep[]; success_rate: string }; + error?: string; +}> { + const bridge = (window as any).metonaDesktop; + if (!bridge?.db?.getAllSkills) return { success: false, error: '数据库不可用' }; + const allSkills: Skill[] = await bridge.db.getAllSkills(); + const skill = allSkills.find(s => + s.name.toLowerCase() === skillName.toLowerCase() || + s.name.toLowerCase().includes(skillName.toLowerCase()) + ); + if (!skill) return { success: false, error: `未找到技能: ${skillName}` }; + let chain: ToolChainStep[] = []; + try { chain = JSON.parse(skill.tool_chain); } catch { /* */ } + const total = skill.success_count + skill.fail_count; + const rate = total > 0 ? Math.round(skill.success_count / total * 100) : 100; + return { + success: true, + skill: { ...skill, chain, success_rate: `${rate}%` } + }; +} diff --git a/src/renderer/services/tool-registry.ts b/src/renderer/services/tool-registry.ts index 43a745d..b0ec629 100644 --- a/src/renderer/services/tool-registry.ts +++ b/src/renderer/services/tool-registry.ts @@ -388,6 +388,37 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ } } }, + { + type: 'function', + function: { + name: 'memory_replace', + description: 'Replace an existing memory entry with updated content. Uses substring matching to find the entry to replace.', + parameters: { + type: 'object', + required: ['target', 'old_text', 'new_content'], + properties: { + target: { type: 'string', enum: ['memory', 'user'], description: 'Target store: memory (agent notes) or user (user profile).' }, + old_text: { type: 'string', description: 'Unique substring that identifies the entry to replace.' }, + new_content: { type: 'string', description: 'The new content to replace the old entry with.' } + } + } + } + }, + { + type: 'function', + function: { + name: 'memory_remove', + description: 'Remove a memory entry. Uses substring matching to find the entry to remove.', + parameters: { + type: 'object', + required: ['target', 'old_text'], + properties: { + target: { type: 'string', enum: ['memory', 'user'], description: 'Target store: memory (agent notes) or user (user profile).' }, + old_text: { type: 'string', description: 'Unique substring that identifies the entry to remove.' } + } + } + } + }, { type: 'function', function: { @@ -417,6 +448,31 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [ } } }, + { + type: 'function', + function: { + name: 'skill_list', + description: 'List all available auto-generated skills. Shows name, description, and success rate. Use skill_view to see full details of a specific skill.', + parameters: { + type: 'object', + properties: {} + } + } + }, + { + type: 'function', + function: { + name: 'skill_view', + description: 'View full details of a specific skill including its tool chain, parameter hints, and usage statistics.', + parameters: { + type: 'object', + required: ['name'], + properties: { + name: { type: 'string', description: 'The skill name (or partial name) to view.' } + } + } + } + }, // ══════════════════════════════════════════════ // v4.3 新增工具:子代理委派 // ══════════════════════════════════════════════ @@ -562,7 +618,8 @@ let enabledTools: Set = new Set([ 'move_file', 'copy_file', 'web_fetch', 'web_search', 'append_file', 'edit_file', 'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files', 'read_multiple_files', 'git', 'compress', - 'memory_search', 'memory_add', 'session_list', 'session_read', 'spawn_task', + 'memory_search', 'memory_add', 'memory_replace', 'memory_remove', + 'session_list', 'session_read', 'skill_list', 'skill_view', 'spawn_task', 'browser_open', 'browser_screenshot', 'browser_evaluate', 'browser_extract', 'browser_click', 'browser_type', 'browser_scroll', 'browser_close' ]); @@ -637,6 +694,23 @@ export async function executeTool(toolName: string, args: Record