feat: 增强编码能力 - 新增代码编辑器、代码搜索、Git集成

- 新增 plugins/code_editor.py: 精准代码编辑(搜索替换、批量编辑、diff对比)
- 新增 plugins/code_search.py: 代码搜索与理解(跨文件搜索、符号提取、依赖分析)
- 新增 plugins/git_tools.py: Git 版本控制(status/diff/log/commit/branch)
- 新增 core/ 模块: 配置管理、引擎管理、对话处理、显示输出、代码沙箱
- 修复 requirements.txt: 使用正确的 pip 包名(Pillow、opencv-python 等)
- 更新 unified_tool_manager.py: 兼容 Plugin 和 Liugin 两种类名
- 更新系统提示词: 增强编程工作流指导

本次更新由 mimo 自动化执行,与开发者无关。
This commit is contained in:
Xiaoli Dev committed 2026-04-25 10:55:27 +08:00
commit e22652fbbb
53 files changed
+20067

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
"""
Agent Skills 协议实现
支持两种格式:
1. 标准开源格式: SKILL.md (YAML frontmatter + Markdown)
2. Python 类格式: Skill 子类 (用于需要代码执行的技能)
"""
from .base import (
# 基础类
Skill,
SkillResult,
SkillParameter,
ParameterType,
SkillLoader,
# Markdown 技能
MarkdownSkill,
)
__all__ = [
'Skill',
'SkillResult',
'SkillParameter',
'ParameterType',
'SkillLoader',
'MarkdownSkill',
]
__version__ = '1.0.0'
+396
View File
@@ -0,0 +1,396 @@
"""
Agent Skills 协议基类
支持两种格式:
1. 标准开源格式: SKILL.md (YAML frontmatter + Markdown)
2. Python 类格式: Skill 子类 (用于需要代码执行的技能)
"""
import os
import re
import json
import threading
import importlib
import inspect
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, List, Dict, Any, Union
class ParameterType(Enum):
"""参数类型枚举 - 映射到 JSON Schema"""
STRING = "string"
INTEGER = "integer"
NUMBER = "number"
BOOLEAN = "boolean"
ARRAY = "array"
OBJECT = "object"
@dataclass
class SkillParameter:
"""技能参数定义"""
name: str
param_type: ParameterType
description: str
required: bool = True
default: Any = None
enum: Optional[List[Any]] = None
def to_json_schema(self) -> Dict[str, Any]:
"""转换为 JSON Schema 格式"""
schema = {
"type": self.param_type.value,
"description": self.description
}
if self.enum:
schema["enum"] = self.enum
return schema
@dataclass
class SkillResult:
"""技能执行结果"""
success: bool
data: Any = None
message: str = ""
error: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
def __str__(self) -> str:
if self.success:
return self.message or str(self.data)
return f"Error: {self.error}"
def to_dict(self) -> Dict[str, Any]:
return {
"success": self.success,
"data": self.data,
"message": self.message,
"error": self.error,
"metadata": self.metadata
}
class Skill(ABC):
"""技能基类 - 用于 Python 实现的技能"""
def __init__(self):
self.name: str = ""
self.description: str = ""
self.version: str = "1.0.0"
self.author: str = ""
self.tags: List[str] = []
self._cli = None
@property
@abstractmethod
def parameters(self) -> List[SkillParameter]:
pass
@abstractmethod
def execute(self, **kwargs) -> SkillResult:
pass
def set_cli(self, cli) -> None:
self._cli = cli
def get_tool_definition(self) -> Dict[str, Any]:
"""获取 OpenAI 格式的工具定义"""
properties = {}
required = []
for param in self.parameters:
properties[param.name] = param.to_json_schema()
if param.required:
required.append(param.name)
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": properties,
"required": required
}
}
}
def get_anthropic_tool_definition(self) -> Dict[str, Any]:
"""获取 Anthropic 格式的工具定义"""
properties = {}
required = []
for param in self.parameters:
properties[param.name] = param.to_json_schema()
if param.required:
required.append(param.name)
return {
"name": self.name,
"description": self.description,
"input_schema": {
"type": "object",
"properties": properties,
"required": required
}
}
def run(self, **kwargs) -> SkillResult:
"""执行技能(带参数验证)"""
error = self._validate_parameters(**kwargs)
if error:
return SkillResult(success=False, error=error)
return self.execute(**kwargs)
def _validate_parameters(self, **kwargs) -> Optional[str]:
"""验证参数"""
for param in self.parameters:
if param.required and param.name not in kwargs:
return f"缺少必需参数: {param.name}"
return None
@dataclass
class MarkdownSkill:
"""Markdown 格式的技能 (标准 Agent Skills 格式)"""
name: str
description: str
version: str = "1.0.0"
author: str = ""
license: str = "MIT"
tools: List[str] = field(default_factory=list)
instructions: str = ""
file_path: str = ""
scripts_dir: str = ""
def get_tool_definition(self) -> Dict[str, Any]:
"""获取 OpenAI 格式的工具定义"""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["list", "read", "write", "delete", "copy", "move", "info", "search"],
"description": "要执行的文件操作类型"
},
"path": {
"type": "string",
"description": "目标文件或目录路径"
},
"content": {
"type": "string",
"description": "写入内容(write/append 操作时使用)"
},
"destination": {
"type": "string",
"description": "目标路径(copy/move 操作时使用)"
}
},
"required": ["operation"]
}
}
}
def get_anthropic_tool_definition(self) -> Dict[str, Any]:
"""获取 Anthropic 格式的工具定义"""
return {
"name": self.name,
"description": self.description,
"input_schema": {
"type": "object",
"properties": {
"operation": {"type": "string"},
"path": {"type": "string"},
"content": {"type": "string"},
"destination": {"type": "string"}
},
"required": ["operation"]
}
}
class SkillLoader:
"""技能加载器 - 支持标准 SKILL.md 和 Python 类两种格式"""
def __init__(self, skills_dir: str = None):
self.skills_dir = skills_dir
self._skills: Dict[str, Union[Skill, MarkdownSkill]] = {}
self._lock = threading.RLock()
@property
def skills(self) -> Dict[str, Union[Skill, MarkdownSkill]]:
return self._skills.copy()
def discover_skills(self, skills_dir: str = None) -> List[Dict[str, str]]:
"""发现所有技能"""
search_dir = skills_dir or self.skills_dir
if not search_dir or not os.path.isdir(search_dir):
return []
discovered = []
for item in os.listdir(search_dir):
item_path = os.path.join(search_dir, item)
# 跳过特殊文件
if item in ('__init__.py', 'base.py') or item.startswith('_') or item.startswith('.'):
continue
# SKILL.md 格式 (目录)
skill_md = os.path.join(item_path, "SKILL.md")
if os.path.isdir(item_path) and os.path.isfile(skill_md):
discovered.append({"name": item, "type": "markdown", "path": skill_md})
# Python 单文件格式
elif item.endswith('.py') and os.path.isfile(item_path):
discovered.append({"name": item[:-3], "type": "python", "path": item_path})
return discovered
def parse_skill_md(self, file_path: str) -> Optional[MarkdownSkill]:
"""解析 SKILL.md 文件"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 解析 YAML frontmatter
if not content.startswith('---'):
return None
end_idx = content.find('---', 3)
if end_idx == -1:
return None
frontmatter = content[3:end_idx].strip()
instructions = content[end_idx + 3:].strip()
# 解析 YAML (简化版)
metadata = {}
current_list = None
current_key = None
for line in frontmatter.split('\n'):
stripped = line.strip()
if not stripped:
continue
# 列表项
if stripped.startswith('- '):
if current_key and current_key not in metadata:
metadata[current_key] = []
if current_key and isinstance(metadata.get(current_key), list):
metadata[current_key].append(stripped[2:])
continue
# 键值对
if ':' in line:
current_key = None
colon_idx = line.index(':')
key = line[:colon_idx].strip()
value = line[colon_idx + 1:].strip()
if value:
# 处理列表格式 [a, b, c]
if value.startswith('[') and value.endswith(']'):
value = [v.strip() for v in value[1:-1].split(',') if v.strip()]
metadata[key] = value
else:
current_key = key
current_list = []
skill_dir = os.path.dirname(file_path)
scripts_dir = os.path.join(skill_dir, "scripts")
# 解析参数定义 (从 frontmatter 中的 parameters 字段)
parameters = {}
if "parameters" in metadata and isinstance(metadata["parameters"], dict):
parameters = metadata["parameters"]
elif "params" in metadata and isinstance(metadata["params"], dict):
parameters = metadata["params"]
return MarkdownSkill(
name=metadata.get('name', os.path.basename(skill_dir)),
description=metadata.get('description', ''),
version=metadata.get('version', '1.0.0'),
author=metadata.get('author', ''),
license=metadata.get('license', 'MIT'),
tools=metadata.get('tools', []) if isinstance(metadata.get('tools'), list) else [],
instructions=instructions,
file_path=file_path,
scripts_dir=scripts_dir if os.path.isdir(scripts_dir) else ""
)
except Exception as e:
print(f"解析 SKILL.md 失败 ({file_path}): {e}")
return None
def load_skill(self, skill_info: Dict[str, str]) -> Optional[Union[Skill, MarkdownSkill]]:
"""加载单个技能"""
skill_type = skill_info.get('type')
skill_path = skill_info.get('path')
skill_name = skill_info.get('name')
if skill_type == "markdown":
skill = self.parse_skill_md(skill_path)
if skill:
self._skills[skill.name] = skill
return skill
elif skill_type == "python":
try:
# 动态导入 Python 模块
spec = importlib.util.spec_from_file_location(skill_name, skill_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# 查找 Skill 子类
for name, obj in inspect.getmembers(module, inspect.isclass):
if issubclass(obj, Skill) and obj is not Skill:
skill = obj()
self._skills[skill.name] = skill
return skill
except Exception as e:
print(f"加载 Python 技能 '{skill_name}' 失败: {e}")
return None
def load_all_skills(self, skills_dir: str = None) -> Dict[str, Union[Skill, MarkdownSkill]]:
"""加载所有技能"""
discovered = self.discover_skills(skills_dir)
for skill_info in discovered:
self.load_skill(skill_info)
return self.skills
def get_skill(self, name: str) -> Optional[Union[Skill, MarkdownSkill]]:
"""获取已加载的技能"""
return self._skills.get(name)
def get_all_tool_definitions(self, format: str = "openai") -> List[Dict]:
"""获取所有技能的工具定义"""
definitions = []
for skill in self._skills.values():
if format == "anthropic":
definitions.append(skill.get_anthropic_tool_definition())
else:
definitions.append(skill.get_tool_definition())
return definitions
def get_skill(self, name: str) -> Optional[Union[Skill, MarkdownSkill]]:
"""获取已加载的技能"""
return self._skills.get(name)
def get_all_tool_definitions(self, format: str = "openai") -> List[Dict]:
"""获取所有技能的工具定义"""
definitions = []
for skill in self._skills.values():
if format == "anthropic":
definitions.append(skill.get_anthropic_tool_definition())
else:
definitions.append(skill.get_tool_definition())
return definitions
+305
View File
@@ -0,0 +1,305 @@
---
name: file-manager
description: File and directory management tool for creating, reading, writing, deleting, moving, copying, and searching files. Use this skill when the user needs to perform file operations, list directories, search by pattern or content, or get file metadata like size and modification time.
license: MIT
version: 1.0.0
author: Xiaoli Team (based on openakita/openakita)
tools:
- read_file
- write_file
- bash
---
# File Manager Skill
A comprehensive file and directory management toolset for AI agents.
## Capabilities
- **Create**: Files and directories
- **Read**: File contents with encoding support
- **Write**: Create or overwrite files
- **Append**: Add content to existing files
- **Delete**: Remove files or directories
- **Move**: Move or rename files and directories
- **Copy**: Duplicate files or directories
- **Search**: Find files by pattern or content
- **List**: Browse directory contents
- **Info**: Get file metadata (size, dates, permissions)
## Script Usage
This skill includes a Python script for structured JSON output:
```bash
python scripts/file_ops.py list <path> [--recursive] [--pattern "*.py"]
python scripts/file_ops.py read <file_path> [--encoding utf-8] [--lines N]
python scripts/file_ops.py write <file_path> --content "内容" [--append]
python scripts/file_ops.py copy <source> <destination>
python scripts/file_ops.py move <source> <destination>
python scripts/file_ops.py delete <path> [--recursive]
python scripts/file_ops.py search <directory> --pattern "*.py" [--content "搜索词"]
python scripts/file_ops.py info <path>
```
## Direct Shell Commands
### List Directory Contents
When asked to show files in a directory:
```bash
# List current directory
ls -la
# List specific directory
ls -la /path/to/directory
# Recursive listing (Unix)
find /path -type f -name "*.py"
# Windows
dir /s /b C:\path\*.py
```
### Read File
When asked to read a file:
```bash
# Read entire file
cat filename.txt
# Read with line numbers
cat -n filename.txt
# Read first N lines
head -n 50 filename.txt
# Read last N lines
tail -n 50 filename.txt
```
### Write File
When asked to create or write a file:
```bash
# Write content to file (overwrites)
echo "content" > filename.txt
# Write multiple lines
cat > filename.txt << 'EOF'
Line 1
Line 2
Line 3
EOF
# Windows PowerShell
Set-Content -Path filename.txt -Value "content"
```
### Append to File
When asked to add content to an existing file:
```bash
# Append to file
echo "new line" >> filename.txt
# Append multiple lines
cat >> filename.txt << 'EOF'
Additional line 1
Additional line 2
EOF
```
### Create Directory
When asked to create a folder:
```bash
# Create single directory
mkdir directory_name
# Create nested directories
mkdir -p path/to/nested/directories
# Windows
mkdir path\to\nested\directories
```
### Delete File or Directory
When asked to delete:
```bash
# Delete a file
rm filename.txt
# Delete empty directory
rmdir directory_name
# Delete directory and contents
rm -rf directory_name
# Windows
del filename.txt
rmdir /s /q directory_name
```
⚠️ **Warning**: Deletion is permanent. Always confirm before deleting.
### Copy File or Directory
When asked to copy:
```bash
# Copy file
cp source.txt destination.txt
# Copy directory recursively
cp -r source_dir/ destination_dir/
# Windows
copy source.txt destination.txt
xcopy /E /I source_dir destination_dir
```
### Move or Rename
When asked to move or rename:
```bash
# Move file
mv source.txt /new/location/source.txt
# Rename file
mv oldname.txt newname.txt
# Windows
move source.txt destination.txt
ren oldname.txt newname.txt
```
### Search Files
When asked to find files:
```bash
# Find by name pattern
find /path -name "*.py"
find /path -iname "*.PY" # case-insensitive
# Find by content (grep)
grep -r "search_term" /path
grep -rl "search_term" /path # filenames only
# Windows PowerShell
Get-ChildItem -Path C:\path -Filter *.py -Recurse
Select-String -Path C:\path\*.py -Pattern "search_term"
```
### Get File Information
When asked about file details:
```bash
# File metadata (Unix)
ls -la filename.txt
stat filename.txt
# File size only
du -h filename.txt
# Windows
dir filename.txt
```
## JSON Output Format
All operations should return structured results:
```json
{
"success": true,
"operation": "list",
"path": "/home/user/project",
"data": {
"files": ["main.py", "config.json", "README.md"],
"directories": ["src", "tests", "docs"],
"total_count": 6
},
"timestamp": "2026-03-21T10:30:00Z"
}
```
## Safety Rules
1. **Always validate paths**: Reject paths containing `..` that escape the working directory
2. **Confirm destructive operations**: Ask before delete, move, or overwrite
3. **Handle encoding**: Default to UTF-8, detect and handle other encodings
4. **Large file warning**: Alert for files > 10MB
5. **Binary file detection**: Cannot read binary files as text
## Protected Directories
Never allow operations on:
- System directories: `/etc`, `/root`, `C:\Windows`, `C:\Program Files`
- Hidden files starting with `.` (unless explicitly requested)
- Files with sensitive names: `.env`, `credentials`, `secrets`
## Error Handling
| Error | Response |
|-------|----------|
| File not found | Suggest similar filenames or check path |
| Permission denied | Explain required permissions |
| Disk full | Warn user and suggest cleanup |
| Invalid path | Show correct path format |
## Examples
### Example 1: List Project Files
User: "Show me the project structure"
```bash
find . -type f -name "*.py" | head -20
tree -L 2 -I '__pycache__|*.pyc'
```
### Example 2: Search and Replace
User: "Replace 'old_func' with 'new_func' in all Python files"
```bash
# Find and preview
grep -r "old_func" --include="*.py" .
# Replace (with backup)
find . -name "*.py" -exec sed -i.bak 's/old_func/new_func/g' {} \;
```
### Example 3: Safe Delete
User: "Delete the temp folder"
Before executing:
1. Check if path exists
2. List contents to show what will be deleted
3. Ask for confirmation
4. Execute if confirmed
## Cross-Platform Notes
| Operation | Unix | Windows |
|-----------|------|---------|
| List | `ls -la` | `dir` |
| Copy | `cp` | `copy` |
| Move | `mv` | `move` |
| Delete | `rm -rf` | `rmdir /s /q` |
| Find | `find` | `Get-ChildItem` |
| Grep | `grep` | `Select-String` |
Always detect the OS and use appropriate commands.
+670
View File
@@ -0,0 +1,670 @@
#!/usr/bin/env python3
"""
File Operations Script for file-manager skill
Provides file system operations as JSON output
"""
import os
import sys
import json
import shutil
import argparse
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any, List
def json_response(success: bool, operation: str, data: Any = None, error: str = None) -> str:
"""Generate JSON response"""
return json.dumps({
"success": success,
"operation": operation,
"data": data,
"error": error,
"timestamp": datetime.now().isoformat()
}, ensure_ascii=False, indent=2)
def list_directory(path: str, recursive: bool = False, pattern: str = None) -> str:
"""List directory contents"""
try:
path = os.path.abspath(path)
if not os.path.exists(path):
return json_response(False, "list", error=f"Path not found: {path}")
if not os.path.isdir(path):
return json_response(False, "list", error=f"Not a directory: {path}")
files = []
directories = []
if recursive:
for root, dirs, filenames in os.walk(path):
for d in dirs:
rel_path = os.path.relpath(os.path.join(root, d), path)
if pattern is None or Path(d).match(pattern):
directories.append(rel_path)
for f in filenames:
rel_path = os.path.relpath(os.path.join(root, f), path)
if pattern is None or Path(f).match(pattern):
files.append(rel_path)
else:
for item in os.listdir(path):
full_path = os.path.join(path, item)
if os.path.isdir(full_path):
if pattern is None or Path(item).match(pattern):
directories.append(item)
else:
if pattern is None or Path(item).match(pattern):
files.append(item)
return json_response(True, "list", {
"path": path,
"files": sorted(files),
"directories": sorted(directories),
"file_count": len(files),
"dir_count": len(directories),
"total_count": len(files) + len(directories)
})
except Exception as e:
return json_response(False, "list", error=str(e))
def read_file(file_path: str, encoding: str = "utf-8", lines: int = None) -> str:
"""Read file content"""
try:
file_path = os.path.abspath(file_path)
if not os.path.exists(file_path):
return json_response(False, "read", error=f"File not found: {file_path}")
if not os.path.isfile(file_path):
return json_response(False, "read", error=f"Not a file: {file_path}")
file_size = os.path.getsize(file_path)
with open(file_path, 'r', encoding=encoding) as f:
if lines:
content_lines = []
for i, line in enumerate(f):
if i >= lines:
break
content_lines.append(line.rstrip('\n\r'))
content = '\n'.join(content_lines)
total_lines = sum(1 for _ in open(file_path, 'r', encoding=encoding))
else:
content = f.read()
total_lines = content.count('\n') + 1
return json_response(True, "read", {
"path": file_path,
"content": content,
"size": file_size,
"lines": total_lines,
"displayed_lines": lines if lines else total_lines
})
except UnicodeDecodeError:
return json_response(False, "read", error="Cannot decode file (binary or wrong encoding)")
except Exception as e:
return json_response(False, "read", error=str(e))
def write_file(file_path: str, content: str, append: bool = False) -> str:
"""Write or append to file"""
try:
file_path = os.path.abspath(file_path)
# Create parent directory if needed
parent = os.path.dirname(file_path)
if parent and not os.path.exists(parent):
os.makedirs(parent, exist_ok=True)
mode = 'a' if append else 'w'
with open(file_path, mode, encoding='utf-8') as f:
f.write(content)
return json_response(True, "write" if not append else "append", {
"path": file_path,
"size": len(content.encode('utf-8')),
"appended": append
})
except Exception as e:
return json_response(False, "write", error=str(e))
def copy_path(source: str, destination: str) -> str:
"""Copy file or directory"""
try:
source = os.path.abspath(source)
destination = os.path.abspath(destination)
if not os.path.exists(source):
return json_response(False, "copy", error=f"Source not found: {source}")
# Create parent directory for destination
parent = os.path.dirname(destination)
if parent and not os.path.exists(parent):
os.makedirs(parent, exist_ok=True)
if os.path.isfile(source):
shutil.copy2(source, destination)
else:
shutil.copytree(source, destination, dirs_exist_ok=True)
return json_response(True, "copy", {
"source": source,
"destination": destination,
"type": "file" if os.path.isfile(source) else "directory"
})
except Exception as e:
return json_response(False, "copy", error=str(e))
def move_path(source: str, destination: str) -> str:
"""Move or rename file or directory"""
try:
source = os.path.abspath(source)
destination = os.path.abspath(destination)
if not os.path.exists(source):
return json_response(False, "move", error=f"Source not found: {source}")
# Create parent directory for destination
parent = os.path.dirname(destination)
if parent and not os.path.exists(parent):
os.makedirs(parent, exist_ok=True)
shutil.move(source, destination)
return json_response(True, "move", {
"source": source,
"destination": destination
})
except Exception as e:
return json_response(False, "move", error=str(e))
def delete_path(path: str, recursive: bool = False) -> str:
"""Delete file or directory"""
try:
path = os.path.abspath(path)
if not os.path.exists(path):
return json_response(False, "delete", error=f"Path not found: {path}")
if os.path.isfile(path):
os.remove(path)
item_type = "file"
elif os.path.isdir(path):
if recursive:
shutil.rmtree(path)
else:
os.rmdir(path) # Only works for empty directories
item_type = "directory"
return json_response(True, "delete", {
"path": path,
"type": item_type
})
except OSError as e:
if "not empty" in str(e).lower():
return json_response(False, "delete", error="Directory not empty. Use --recursive to delete.")
return json_response(False, "delete", error=str(e))
except Exception as e:
return json_response(False, "delete", error=str(e))
def search_files(directory: str, pattern: str = None, content: str = None) -> str:
"""Search files by name pattern or content"""
try:
directory = os.path.abspath(directory)
if not os.path.exists(directory):
return json_response(False, "search", error=f"Directory not found: {directory}")
results = []
for root, dirs, files in os.walk(directory):
for filename in files:
file_path = os.path.join(root, filename)
rel_path = os.path.relpath(file_path, directory)
# Pattern match
if pattern and not Path(filename).match(pattern):
continue
# Content search
if content:
try:
with open(file_path, 'r', encoding='utf-8') as f:
file_content = f.read()
if content not in file_content:
continue
# Find matching lines
matches = []
for i, line in enumerate(file_content.split('\n'), 1):
if content in line:
matches.append({"line": i, "text": line.strip()[:100]})
if matches:
results.append({
"path": rel_path,
"matches": matches[:5] # Limit matches per file
})
except (UnicodeDecodeError, IOError):
continue
else:
results.append({"path": rel_path})
return json_response(True, "search", {
"directory": directory,
"pattern": pattern,
"content_search": content,
"results": results[:100], # Limit results
"total": len(results)
})
except Exception as e:
return json_response(False, "search", error=str(e))
def get_info(path: str) -> str:
"""Get file/directory information"""
try:
path = os.path.abspath(path)
if not os.path.exists(path):
return json_response(False, "info", error=f"Path not found: {path}")
stat_info = os.stat(path)
info = {
"path": path,
"name": os.path.basename(path),
"type": "directory" if os.path.isdir(path) else "file",
"size": stat_info.st_size,
"created": datetime.fromtimestamp(stat_info.st_ctime).isoformat(),
"modified": datetime.fromtimestamp(stat_info.st_mtime).isoformat(),
"accessed": datetime.fromtimestamp(stat_info.st_atime).isoformat(),
"permissions": oct(stat_info.st_mode)[-3:]
}
if os.path.isfile(path):
info["extension"] = os.path.splitext(path)[1]
# Try to detect if text file
try:
with open(path, 'r', encoding='utf-8') as f:
f.read(1024)
info["encoding"] = "utf-8"
except UnicodeDecodeError:
info["encoding"] = "binary"
return json_response(True, "info", info)
except Exception as e:
return json_response(False, "info", error=str(e))
def modify_line(file_path: str, line_number: int, new_content: str) -> str:
"""Modify a specific line in a file"""
try:
file_path = os.path.abspath(file_path)
if not os.path.exists(file_path):
return json_response(False, "modify_line", error=f"File not found: {file_path}")
if not os.path.isfile(file_path):
return json_response(False, "modify_line", error=f"Not a file: {file_path}")
if line_number < 1:
return json_response(False, "modify_line", error="Line number must be >= 1")
# Read all lines
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total_lines = len(lines)
if line_number > total_lines:
return json_response(False, "modify_line",
error=f"File has only {total_lines} lines, cannot modify line {line_number}")
# Get old content for reporting
old_content = lines[line_number - 1].rstrip('\n\r')
# Modify the line (preserve newline if existed)
has_newline = lines[line_number - 1].endswith('\n')
lines[line_number - 1] = new_content + ('\n' if has_newline else '')
# Write back
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return json_response(True, "modify_line", {
"path": file_path,
"line_number": line_number,
"old_content": old_content,
"new_content": new_content,
"total_lines": total_lines
})
except UnicodeDecodeError:
return json_response(False, "modify_line", error="Cannot modify binary file")
except Exception as e:
return json_response(False, "modify_line", error=str(e))
def insert_line(file_path: str, line_number: int, content: str) -> str:
"""Insert a new line at specified position"""
try:
file_path = os.path.abspath(file_path)
if not os.path.exists(file_path):
return json_response(False, "insert_line", error=f"File not found: {file_path}")
if not os.path.isfile(file_path):
return json_response(False, "insert_line", error=f"Not a file: {file_path}")
if line_number < 1:
return json_response(False, "insert_line", error="Line number must be >= 1")
# Read all lines
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total_lines = len(lines)
# Insert at position (can be after last line)
insert_pos = min(line_number - 1, total_lines)
lines.insert(insert_pos, content + '\n')
# Write back
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return json_response(True, "insert_line", {
"path": file_path,
"line_number": line_number,
"inserted_content": content,
"total_lines": len(lines)
})
except Exception as e:
return json_response(False, "insert_line", error=str(e))
def delete_line(file_path: str, line_number: int) -> str:
"""Delete a specific line from a file"""
try:
file_path = os.path.abspath(file_path)
if not os.path.exists(file_path):
return json_response(False, "delete_line", error=f"File not found: {file_path}")
if not os.path.isfile(file_path):
return json_response(False, "delete_line", error=f"Not a file: {file_path}")
if line_number < 1:
return json_response(False, "delete_line", error="Line number must be >= 1")
# Read all lines
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total_lines = len(lines)
if line_number > total_lines:
return json_response(False, "delete_line",
error=f"File has only {total_lines} lines, cannot delete line {line_number}")
# Get deleted content
deleted_content = lines[line_number - 1].rstrip('\n\r')
# Remove the line
del lines[line_number - 1]
# Write back
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return json_response(True, "delete_line", {
"path": file_path,
"line_number": line_number,
"deleted_content": deleted_content,
"total_lines": len(lines)
})
except Exception as e:
return json_response(False, "delete_line", error=str(e))
def modify_lines(file_path: str, modifications: Dict[int, str]) -> str:
"""
Modify multiple lines at once
Args:
file_path: Path to the file
modifications: Dict mapping line numbers (1-based) to new content
Example: {2: "new line 2", 5: "new line 5"}
"""
try:
file_path = os.path.abspath(file_path)
if not os.path.exists(file_path):
return json_response(False, "modify_lines", error=f"File not found: {file_path}")
if not os.path.isfile(file_path):
return json_response(False, "modify_lines", error=f"Not a file: {file_path}")
# Read all lines
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total_lines = len(lines)
results = []
# Sort modifications by line number (descending to avoid index shift issues)
for line_number in sorted(modifications.keys()):
if line_number < 1:
results.append({"line": line_number, "success": False, "error": "Line number must be >= 1"})
continue
if line_number > total_lines:
results.append({"line": line_number, "success": False,
"error": f"File has only {total_lines} lines"})
continue
# Get old content
old_content = lines[line_number - 1].rstrip('\n\r')
new_content = modifications[line_number]
# Modify the line
has_newline = lines[line_number - 1].endswith('\n')
lines[line_number - 1] = new_content + ('\n' if has_newline else '')
results.append({
"line": line_number,
"success": True,
"old_content": old_content,
"new_content": new_content
})
# Write back
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return json_response(True, "modify_lines", {
"path": file_path,
"total_lines": total_lines,
"modifications_count": len(modifications),
"results": results
})
except Exception as e:
return json_response(False, "modify_lines", error=str(e))
def replace_text(file_path: str, old_text: str, new_text: str, start_line: int = None, end_line: int = None) -> str:
"""
Replace all occurrences of text in a file (optionally within a line range)
Args:
file_path: Path to the file
old_text: Text to find and replace
new_text: Replacement text
start_line: Optional start line (1-based, inclusive)
end_line: Optional end line (1-based, inclusive)
"""
try:
file_path = os.path.abspath(file_path)
if not os.path.exists(file_path):
return json_response(False, "replace_text", error=f"File not found: {file_path}")
if not os.path.isfile(file_path):
return json_response(False, "replace_text", error=f"Not a file: {file_path}")
# Read all lines
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
total_lines = len(lines)
replacements = []
# Determine range
start = max(0, (start_line or 1) - 1)
end = min(total_lines, end_line or total_lines)
for i in range(start, end):
line = lines[i]
if old_text in line:
count = line.count(old_text)
lines[i] = line.replace(old_text, new_text)
replacements.append({
"line": i + 1,
"count": count,
"old_line": line.rstrip('\n\r'),
"new_line": lines[i].rstrip('\n\r')
})
# Write back
with open(file_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
return json_response(True, "replace_text", {
"path": file_path,
"old_text": old_text,
"new_text": new_text,
"line_range": [start + 1, end] if start_line or end_line else None,
"total_replacements": sum(r["count"] for r in replacements),
"lines_modified": len(replacements),
"details": replacements
})
except Exception as e:
return json_response(False, "replace_text", error=str(e))
def main():
parser = argparse.ArgumentParser(description="File operations for file-manager skill")
subparsers = parser.add_subparsers(dest="operation", help="Operation to perform")
# List
list_parser = subparsers.add_parser("list", help="List directory contents")
list_parser.add_argument("path", help="Directory path")
list_parser.add_argument("-r", "--recursive", action="store_true", help="Recursive listing")
list_parser.add_argument("-p", "--pattern", help="File pattern (e.g., *.py)")
# Read
read_parser = subparsers.add_parser("read", help="Read file content")
read_parser.add_argument("path", help="File path")
read_parser.add_argument("-e", "--encoding", default="utf-8", help="File encoding")
read_parser.add_argument("-n", "--lines", type=int, help="Number of lines to read")
# Write
write_parser = subparsers.add_parser("write", help="Write to file")
write_parser.add_argument("path", help="File path")
write_parser.add_argument("-c", "--content", required=True, help="Content to write")
write_parser.add_argument("-a", "--append", action="store_true", help="Append to file")
# Copy
copy_parser = subparsers.add_parser("copy", help="Copy file or directory")
copy_parser.add_argument("source", help="Source path")
copy_parser.add_argument("destination", help="Destination path")
# Move
move_parser = subparsers.add_parser("move", help="Move file or directory")
move_parser.add_argument("source", help="Source path")
move_parser.add_argument("destination", help="Destination path")
# Delete
delete_parser = subparsers.add_parser("delete", help="Delete file or directory")
delete_parser.add_argument("path", help="Path to delete")
delete_parser.add_argument("-r", "--recursive", action="store_true", help="Recursive delete")
# Search
search_parser = subparsers.add_parser("search", help="Search files")
search_parser.add_argument("directory", help="Directory to search")
search_parser.add_argument("-p", "--pattern", help="File pattern")
search_parser.add_argument("-c", "--content", help="Content to search for")
# Info
info_parser = subparsers.add_parser("info", help="Get file info")
info_parser.add_argument("path", help="File or directory path")
# Modify line
modify_parser = subparsers.add_parser("modify-line", help="Modify a specific line")
modify_parser.add_argument("path", help="File path")
modify_parser.add_argument("line", type=int, help="Line number (1-based)")
modify_parser.add_argument("content", help="New line content")
# Insert line
insert_parser = subparsers.add_parser("insert-line", help="Insert a new line")
insert_parser.add_argument("path", help="File path")
insert_parser.add_argument("line", type=int, help="Line number to insert at (1-based)")
insert_parser.add_argument("content", help="Line content to insert")
# Delete line
del_line_parser = subparsers.add_parser("delete-line", help="Delete a specific line")
del_line_parser.add_argument("path", help="File path")
del_line_parser.add_argument("line", type=int, help="Line number to delete (1-based)")
# Modify multiple lines (JSON format)
multi_parser = subparsers.add_parser("modify-lines", help="Modify multiple lines at once")
multi_parser.add_argument("path", help="File path")
multi_parser.add_argument("-m", "--modifications", required=True,
help='JSON dict of line numbers to content, e.g. \'{"2": "new line 2", "5": "new line 5"}\'')
# Replace text
replace_parser = subparsers.add_parser("replace", help="Replace text in file")
replace_parser.add_argument("path", help="File path")
replace_parser.add_argument("old", help="Text to find and replace")
replace_parser.add_argument("new", help="Replacement text")
replace_parser.add_argument("--start", type=int, help="Start line (optional)")
replace_parser.add_argument("--end", type=int, help="End line (optional)")
args = parser.parse_args()
if args.operation == "list":
print(list_directory(args.path, args.recursive, args.pattern))
elif args.operation == "read":
print(read_file(args.path, args.encoding, args.lines))
elif args.operation == "write":
print(write_file(args.path, args.content, args.append))
elif args.operation == "copy":
print(copy_path(args.source, args.destination))
elif args.operation == "move":
print(move_path(args.source, args.destination))
elif args.operation == "delete":
print(delete_path(args.path, args.recursive))
elif args.operation == "search":
print(search_files(args.directory, args.pattern, args.content))
elif args.operation == "info":
print(get_info(args.path))
elif args.operation == "modify-line":
print(modify_line(args.path, args.line, args.content))
elif args.operation == "insert-line":
print(insert_line(args.path, args.line, args.content))
elif args.operation == "delete-line":
print(delete_line(args.path, args.line))
elif args.operation == "modify-lines":
mods = json.loads(args.modifications)
mods_int = {int(k): v for k, v in mods.items()}
print(modify_lines(args.path, mods_int))
elif args.operation == "replace":
print(replace_text(args.path, args.old, args.new, args.start, args.end))
else:
parser.print_help()
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
Line 1
MODIFIED LINE
Line 3
@@ -0,0 +1,5 @@
Line 1
REPLACED LINE 2
Line 3
REPLACED LINE 4
Line 5