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:
commit
e22652fbbb
53 files changed
+20067
No files matched your search
@@ -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
@@ -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
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Reference in new issue
Block a user