sonicoder / code /tools /fs.py
Z User
☁️ Integrate Hugging Face Hub Cloud Storage
817e3da
Raw
History Blame Contribute Delete
25.5 kB
"""File system tools: read, write, edit, list, glob, grep.
Enhanced with Hugging Face Hub cloud storage integration:
- All file operations work seamlessly with both local and cloud storage
- Automatic sync to HF Hub when configured
- Transparent fallback between local and remote
All tools are sandboxed to a configurable workspace root (default: ./workspace).
They return JSON-serializable dicts so they can be exposed via the API.
"""
from __future__ import annotations
import fnmatch
import logging
import os
import re
import shutil
from pathlib import Path
from typing import Any, Optional
logger = logging.getLogger(__name__)
# ─── Workspace sandbox ──────────────────────────────────────────────────
# Default workspace: ./workspace under the app root
_DEFAULT_WORKSPACE = os.environ.get(
"SONICODER_WORKSPACE",
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "workspace")),
)
# Hub storage instance (lazy initialized)
_hub_storage = None
def get_workspace_root() -> str:
"""Return the absolute path of the agent's workspace root."""
root = _DEFAULT_WORKSPACE
os.makedirs(root, exist_ok=True)
return root
def _resolve_safe(path: str) -> str:
"""Resolve a path safely within the workspace root.
Raises ValueError if the resolved path escapes the workspace.
"""
root = get_workspace_root()
if os.path.isabs(path):
full = os.path.abspath(path)
else:
full = os.path.abspath(os.path.join(root, path))
# Ensure path is within the workspace
if not (full == root or full.startswith(root + os.sep)):
raise ValueError(
f"Path '{path}' resolves outside the workspace root ({root}). "
"Agent tools are sandboxed."
)
return full
def _get_hub_storage():
"""Get the HubStorage instance if available."""
global _hub_storage
if _hub_storage is None:
try:
from code.hub_storage import get_hub_storage, is_hub_available
if is_hub_available():
_hub_storage = get_hub_storage()
logger.debug("Hub storage integration active")
else:
logger.debug("Hub storage not configured")
except ImportError:
logger.debug("huggingface_hub not installed")
_hub_storage = None
return _hub_storage
# ─── read_file ──────────────────────────────────────────────────────────
def read_file(path: str, offset: int = 0, limit: int | None = None) -> dict[str, Any]:
"""Read a text file from the workspace.
Enhanced: Falls back to Hub storage if file not found locally.
Args:
path: Relative path inside the workspace, or absolute within it.
offset: 1-indexed line to start reading from.
limit: Maximum number of lines to read.
Returns:
dict with: path, content, line_count, truncated
"""
try:
full = _resolve_safe(path)
# Try local first
if os.path.exists(full) and os.path.isfile(full):
with open(full, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
total = len(lines)
start = max(0, (offset - 1) if offset > 0 else 0)
end = (start + limit) if limit else total
selected = lines[start:end]
# Re-number for display
numbered = "".join(
f"{start + i + 1:6}\t{line}" for i, line in enumerate(selected)
)
return {
"success": True,
"path": path,
"content": numbered,
"line_count": total,
"returned_lines": len(selected),
"truncated": end < total,
"source": "local",
}
# Try Hub storage
hub = _get_hub_storage()
if hub and hub.file_exists(path):
try:
content = hub.read_file(path)
lines = content.split('\n')
total = len(lines)
start = max(0, (offset - 1) if offset > 0 else 0)
end = (start + limit) if limit else total
selected = lines[start:end]
numbered = "".join(
f"{start + i + 1:6}\t{line}" for i, line in enumerate(selected)
)
return {
"success": True,
"path": path,
"content": numbered,
"line_count": total,
"returned_lines": len(selected),
"truncated": end < total,
"source": "hub",
}
except Exception as e:
logger.warning("Failed to read %s from Hub: %s", path, e)
return {"success": False, "error": f"File not found: {path}"}
except Exception as exc:
return {"success": False, "error": str(exc)}
# ─── write_file ─────────────────────────────────────────────────────────
def write_file(path: str, content: str) -> dict[str, Any]:
"""Write content to a file, creating parent directories as needed.
Enhanced: Automatically syncs to HF Hub when configured.
Args:
path: Relative file path in workspace
content: File content string
Returns:
dict with success status and metadata
"""
try:
full = _resolve_safe(path)
os.makedirs(os.path.dirname(full), exist_ok=True)
# Write locally first
with open(full, "w", encoding="utf-8") as f:
f.write(content)
result = {
"success": True,
"path": path,
"bytes_written": len(content.encode("utf-8")),
"source": "local",
}
# Sync to Hub if configured
hub = _get_hub_storage()
if hub:
try:
metadata = hub.write_file(
path=path,
content=content,
commit_message=f"AI generated/updated: {path}"
)
result["hub_sync"] = True
result["hub_remote"] = metadata.is_remote
result["source"] = "synced"
logger.info("Synced %s to Hub", path)
except Exception as e:
logger.warning("Failed to sync %s to Hub: %s", path, e)
result["hub_sync"] = False
result["hub_error"] = str(e)
return result
except Exception as exc:
return {"success": False, "error": str(exc)}
# ─── edit_file ──────────────────────────────────────────────────────────
def edit_file(
path: str,
old_str: str,
new_str: str,
replace_all: bool = False,
) -> dict[str, Any]:
"""Replace occurrences of old_str with new_str in a file.
Enhanced: Syncs changes back to Hub after edit.
"""
try:
full = _resolve_safe(path)
# Read file (local or Hub)
hub = _get_hub_storage()
if os.path.exists(full):
with open(full, "r", encoding="utf-8") as f:
content = f.read()
elif hub and hub.file_exists(path):
content = hub.read_file(path)
else:
return {"success": False, "error": f"File not found: {path}"}
if old_str not in content:
return {
"success": False,
"error": f"old_str not found in {path}. Edit aborted.",
}
if old_str == new_str:
return {"success": False, "error": "old_str and new_str are identical."}
count = content.count(old_str) if replace_all else 1
if not replace_all and count > 1:
return {
"success": False,
"error": (
f"old_str is not unique ({count} matches) in {path}. "
"Provide more context or use replace_all=true."
),
}
new_content = content.replace(old_str, new_str) if replace_all else content.replace(
old_str, new_str, 1
)
# Write locally
os.makedirs(os.path.dirname(full), exist_ok=True)
with open(full, "w", encoding="utf-8") as f:
f.write(new_content)
result = {
"success": True,
"path": path,
"replacements": count,
}
# Sync to Hub
if hub:
try:
hub.write_file(path, new_content, commit_message=f"Edit: {path}")
result["hub_sync"] = True
except Exception as e:
logger.warning("Failed to sync edit to Hub: %s", e)
return result
except Exception as exc:
return {"success": False, "error": str(exc)}
def multi_edit(path: str, edits: list[dict[str, Any]]) -> dict[str, Any]:
"""Apply multiple edits to a file atomically (all-or-nothing).
Enhanced: Uses batch operation for Hub sync.
"""
try:
full = _resolve_safe(path)
# Read file
hub = _get_hub_storage()
if os.path.exists(full):
with open(full, "r", encoding="utf-8") as f:
content = f.read()
elif hub and hub.file_exists(path):
content = hub.read_file(path)
else:
return {"success": False, "error": f"File not found: {path}"}
applied = 0
for edit in edits:
old_str = edit.get("old_str", "")
new_str = edit.get("new_str", "")
replace_all = edit.get("replace_all", False)
if old_str not in content:
return {
"success": False,
"error": f"old_str not found in {path} for edit #{applied + 1}.",
"applied": applied,
}
if old_str == new_str:
return {
"success": False,
"error": f"old_str and new_str identical in edit #{applied + 1}.",
"applied": applied,
}
content = content.replace(old_str, new_str) if replace_all else content.replace(
old_str, new_str, 1
)
applied += 1
# Write locally
os.makedirs(os.path.dirname(full), exist_ok=True)
with open(full, "w", encoding="utf-8") as f:
f.write(content)
result = {"success": True, "path": path, "applied": applied}
# Sync to Hub
if hub:
try:
hub.write_file(path, content, commit_message=f"Multi-edit: {path}")
result["hub_sync"] = True
except Exception as e:
logger.warning("Failed to sync multi-edit to Hub: %s", e)
return result
except Exception as exc:
return {"success": False, "error": str(exc)}
# ─── list_dir ───────────────────────────────────────────────────────────
def list_dir(path: str = ".") -> dict[str, Any]:
"""List directory contents.
Enhanced: Merges local and Hub files.
"""
try:
full = _resolve_safe(path)
entries = []
seen_names = set()
# List local files
if os.path.exists(full) and os.path.isdir(full):
for name in sorted(os.listdir(full)):
entry_path = os.path.join(full, name)
stat = os.stat(entry_path)
rel_path = os.path.relpath(entry_path, get_workspace_root())
entries.append({
"name": name,
"type": "dir" if os.path.isdir(entry_path) else "file",
"size": stat.st_size,
"path": rel_path.replace(os.sep, "/"),
"source": "local",
})
seen_names.add(name)
# Merge with Hub files
hub = _get_hub_storage()
if hub:
try:
hub_files = hub.list_files(path=path, recursive=False)
for hf in hub_files:
hf_name = Path(hf["path"]).name
if hf_name not in seen_names:
entries.append({
"name": hf_name,
"type": "file",
"size": hf.get("size", 0),
"path": hf["path"],
"source": "hub",
})
seen_names.add(hf_name)
except Exception as e:
logger.warning("Failed to list Hub files: %s", e)
return {
"success": True,
"path": path,
"entries": sorted(entries, key=lambda x: x["name"]),
}
except Exception as exc:
return {"success": False, "error": str(exc)}
# ─── glob ───────────────────────────────────────────────────────────────
def glob_paths(pattern: str, path: str = ".") -> dict[str, Any]:
"""Glob file paths matching a pattern, recursively.
Enhanced: Searches both local and Hub storage.
"""
try:
full = _resolve_safe(path)
matches: list[dict[str, Any]] = []
seen_paths = set()
# Local glob
if os.path.exists(full):
for root_dir, _dirs, files in os.walk(full):
for fname in files:
if fnmatch.fnmatch(fname, pattern) or fnmatch.fnmatch(
os.path.relpath(os.path.join(root_dir, fname), full), pattern
):
rel_path = os.path.relpath(os.path.join(root_dir, fname), get_workspace_root()).replace(os.sep, "/")
if rel_path not in seen_paths:
matches.append({"path": rel_path, "source": "local"})
seen_paths.add(rel_path)
# Hub glob
hub = _get_hub_storage()
if hub:
try:
hub_files = hub.list_files(path=path, recursive=True)
for hf in hub_files:
if fnmatch.fnmatch(Path(hf["path"]).name, pattern) or fnmatch.fnmatch(hf["path"], pattern):
if hf["path"] not in seen_paths:
matches.append({"path": hf["path"], "source": "hub"})
seen_paths.add(hf["path"])
except Exception as e:
logger.warning("Failed to glob Hub files: %s", e)
matches.sort(key=lambda x: x["path"])
return {"success": True, "pattern": pattern, "matches": matches}
except Exception as exc:
return {"success": False, "error": str(exc)}
# ─── grep ───────────────────────────────────────────────────────────────
def grep_search(
pattern: str,
path: str = ".",
include: str | None = None,
ignore_case: bool = False,
max_results: int = 100,
) -> dict[str, Any]:
"""Search file contents with a regex pattern.
Enhanced: Searches both local and Hub files.
"""
try:
flags = re.IGNORECASE if ignore_case else 0
regex = re.compile(pattern, flags)
matches: list[dict[str, Any]] = []
# Search local files
full = _resolve_safe(path)
if os.path.exists(full):
for root_dir, _dirs, files in os.walk(full):
for fname in files:
if include and not fnmatch.fnmatch(fname, include):
continue
fpath = os.path.join(root_dir, fname)
try:
with open(fpath, "r", encoding="utf-8", errors="replace") as f:
for lineno, line in enumerate(f, 1):
if regex.search(line):
matches.append({
"file": os.path.relpath(fpath, get_workspace_root()).replace(os.sep, "/"),
"line": lineno,
"text": line.rstrip()[:500],
"source": "local",
})
if len(matches) >= max_results:
return {
"success": True,
"pattern": pattern,
"matches": matches,
"truncated": True,
}
except (UnicodeDecodeError, PermissionError):
continue
# Search Hub files
hub = _get_hub_storage()
if hub and len(matches) < max_results:
try:
hub_files = hub.list_files(path=path, recursive=True)
for hf in hub_files:
if include and not fnmatch.fnmatch(Path(hf["path"]).name, include):
continue
try:
content = hub.read_file(hf["path"])
for lineno, line in enumerate(content.split('\n'), 1):
if regex.search(line):
matches.append({
"file": hf["path"],
"line": lineno,
"text": line.rstrip()[:500],
"source": "hub",
})
if len(matches) >= max_results:
return {
"success": True,
"pattern": pattern,
"matches": matches,
"truncated": True,
}
except Exception:
continue
except Exception as e:
logger.warning("Failed to search Hub files: %s", e)
return {
"success": True,
"pattern": pattern,
"matches": matches,
"truncated": False,
}
except re.error as exc:
return {"success": False, "error": f"Invalid regex: {exc}"}
except Exception as exc:
return {"success": False, "error": str(exc)}
# ─── Workspace management ───────────────────────────────────────────────
def list_workspace_tree(max_depth: int = 3) -> dict[str, Any]:
"""Return a tree view of the workspace.
Enhanced: Includes Hub files.
"""
try:
root = get_workspace_root()
def _walk(path: str, depth: int) -> dict[str, Any]:
if depth > max_depth:
return {"name": os.path.basename(path), "type": "dir", "truncated": True}
entries = []
try:
for name in sorted(os.listdir(path)):
full = os.path.join(path, name)
if os.path.isdir(full):
entries.append(_walk(full, depth + 1))
else:
entries.append({
"name": name,
"type": "file",
"size": os.path.getsize(full),
})
except PermissionError:
pass
return {"name": os.path.basename(path), "type": "dir", "children": entries}
tree = _walk(root, 0)
# Add Hub info
hub = _get_hub_storage()
if hub:
stats = hub.get_storage_stats()
tree["hub_enabled"] = stats["hub_enabled"]
tree["storage_mode"] = stats["mode"]
return {"success": True, "tree": tree}
except Exception as exc:
return {"success": False, "error": str(exc)}
def reset_workspace() -> dict[str, Any]:
"""Clear all files in the workspace (used by /new command).
Note: Does NOT clear Hub storage - use reset_hub_workspace for that.
"""
try:
root = get_workspace_root()
if os.path.exists(root):
for entry in os.listdir(root):
full = os.path.join(root, entry)
if os.path.isdir(full):
shutil.rmtree(full)
else:
os.remove(full)
return {"success": True, "message": "Workspace cleared"}
except Exception as exc:
return {"success": False, "error": str(exc)}
def snapshot_workspace() -> dict[str, str]:
"""Return a dict of {relative_path: content} for all text files in the workspace.
Enhanced: Includes Hub files in snapshot.
Used to package workspace files for ZIP/HF deploy.
"""
root = get_workspace_root()
files: dict[str, str] = {}
# Local files
for dirpath, _dirs, fnames in os.walk(root):
parts = os.path.relpath(dirpath, root).split(os.sep)
if any(p.startswith(".") or p in {"node_modules", "__pycache__", ".venv", "venv"} for p in parts):
continue
for fname in fnames:
if fname.startswith("."):
continue
full = os.path.join(dirpath, fname)
try:
with open(full, "r", encoding="utf-8") as f:
files[os.path.relpath(full, root)] = f.read()
except (UnicodeDecodeError, PermissionError):
continue
# Hub files (merge, don't overwrite local)
hub = _get_hub_storage()
if hub:
try:
hub_files = hub.list_files()
for hf in hub_files:
if hf["path"] not in files:
try:
content = hub.read_file(hf["path"])
files[hf["path"]] = content
except Exception:
continue
except Exception as e:
logger.warning("Failed to snapshot Hub files: %s", e)
return files
# ─── Hub-specific operations ────────────────────────────────────────────
def sync_to_hub(path: str | None = None) -> dict[str, Any]:
"""Manually trigger sync to HF Hub.
Args:
path: Specific file/directory to sync (None = all)
Returns:
Sync result dict
"""
hub = _get_hub_storage()
if not hub:
return {"success": False, "error": "Hub storage not configured"}
try:
return hub.sync_to_hub(path=path)
except Exception as e:
return {"success": False, "error": str(e)}
def sync_from_hub(path: str | None = None) -> dict[str, Any]:
"""Manually trigger sync from HF Hub.
Args:
path: Specific file to download (None = all)
Returns:
Sync result dict
"""
hub = _get_hub_storage()
if not hub:
return {"success": False, "error": "Hub storage not configured"}
try:
return hub.sync_from_hub(path=path)
except Exception as e:
return {"success": False, "error": str(e)}
def get_hub_status() -> dict[str, Any]:
"""Get current Hub storage status and configuration."""
hub = _get_hub_storage()
if not hub:
return {
"available": False,
"configured": False,
"mode": "local_only",
}
try:
stats = hub.get_storage_stats()
return {
"available": True,
"configured": True,
**stats,
}
except Exception as e:
return {
"available": False,
"configured": True,
"error": str(e),
}
def delete_from_hub(path: str) -> dict[str, Any]:
"""Delete a file from Hub storage only (keeps local).
Args:
path: File path to delete from Hub
Returns:
Operation result
"""
hub = _get_hub_storage()
if not hub:
return {"success": False, "error": "Hub storage not configured"}
try:
success = hub.delete_file(path, commit_message=f"Delete: {path}")
return {"success": success, "path": path}
except Exception as e:
return {"success": False, "error": str(e)}