Add Discord-native hybrid front-end for Jarvis (bot + bridge)
Some checks failed
Release / semantic-release (push) Successful in 59s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 13m45s
Release / build-linux (push) Failing after 7m47s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
Some checks failed
Release / semantic-release (push) Successful in 59s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 13m45s
Release / build-linux (push) Failing after 7m47s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
Transform isair/jarvis into a Discord-controlled voice assistant running on the Ubuntu VNC desktop, keeping the mature ~39k-line Python brain intact. - bot/ (Node + bun, discord.js): /자비스 slash commands (ephemeral), voice channel join + voice receive/playback, pluggable VNC screen broadcast (selfbot live / noVNC / screenshot) - bridge/ (Python, Flask): wraps jarvis STT + run_reply_engine + Piper TTS behind a thin localhost HTTP API - .env.example, scripts/ (start_bridge/start_bot/dev), README rewrite, docs/language-comparison.md and docs/vnc-xfce-setup.md Language decision: hybrid (Python brain + Node/bun Discord layer) because Discord blocks bot video; native screen broadcast only works via a Node selfbot library.
This commit is contained in:
155
src/jarvis/tools/builtin/local_files.py
Normal file
155
src/jarvis/tools/builtin/local_files.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""Local files tool implementation for safe file operations."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
from ..base import Tool, ToolContext
|
||||
from ..types import ToolExecutionResult
|
||||
|
||||
|
||||
class LocalFilesTool(Tool):
|
||||
"""Tool for safe local file operations within user's home directory."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "localFiles"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "Safely read, write, list, append, or delete files within your home directory."
|
||||
|
||||
@property
|
||||
def inputSchema(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {"type": "string", "description": "Operation to perform: list, read, write, append, delete"},
|
||||
"path": {"type": "string", "description": "File or directory path (relative to home directory)"},
|
||||
"content": {"type": "string", "description": "Content to write/append (for write/append operations)"},
|
||||
"glob": {"type": "string", "description": "Glob pattern for listing (default: *)"},
|
||||
"recursive": {"type": "boolean", "description": "Whether to search recursively (for list operation)"}
|
||||
},
|
||||
"required": ["operation", "path"]
|
||||
}
|
||||
|
||||
def run(self, args: Optional[Dict[str, Any]], context: ToolContext) -> ToolExecutionResult:
|
||||
"""Execute the local files tool."""
|
||||
try:
|
||||
# Safety: restrict to user's home directory by default
|
||||
home_root = Path(os.path.expanduser("~")).resolve()
|
||||
|
||||
def _expand_user_path(p: str) -> str:
|
||||
if not isinstance(p, str):
|
||||
return str(p)
|
||||
if p == "~":
|
||||
return os.path.expanduser("~")
|
||||
if p.startswith("~/") or p.startswith("~\\"):
|
||||
return os.path.join(os.path.expanduser("~"), p[2:])
|
||||
return os.path.expanduser(p)
|
||||
|
||||
def _resolve_safe(p: str) -> Path:
|
||||
resolved = Path(_expand_user_path(p)).resolve()
|
||||
try:
|
||||
# Allow exactly the home root or its descendants
|
||||
if resolved == home_root or str(resolved).startswith(str(home_root) + os.sep):
|
||||
return resolved
|
||||
except Exception:
|
||||
pass
|
||||
raise PermissionError(f"Path not allowed: {resolved}")
|
||||
|
||||
if not (args and isinstance(args, dict)):
|
||||
return ToolExecutionResult(success=False, reply_text="localFiles requires a JSON object with at least 'operation' and 'path'.")
|
||||
|
||||
operation = str(args.get("operation") or "").strip().lower()
|
||||
path_arg = args.get("path")
|
||||
if not operation or not path_arg:
|
||||
return ToolExecutionResult(success=False, reply_text="localFiles requires 'operation' and 'path'.")
|
||||
|
||||
target = _resolve_safe(str(path_arg))
|
||||
|
||||
# list
|
||||
if operation == "list":
|
||||
if not target.exists():
|
||||
return ToolExecutionResult(success=False, reply_text=f"Path not found: {target}")
|
||||
if target.is_file():
|
||||
return ToolExecutionResult(success=True, reply_text=f"File: {target.name}")
|
||||
|
||||
glob_pattern = args.get("glob", "*")
|
||||
recursive = bool(args.get("recursive", False))
|
||||
|
||||
try:
|
||||
if recursive:
|
||||
files = list(target.rglob(glob_pattern))
|
||||
else:
|
||||
files = list(target.glob(glob_pattern))
|
||||
|
||||
if not files:
|
||||
return ToolExecutionResult(success=True, reply_text=f"No files found matching '{glob_pattern}' in {target}")
|
||||
|
||||
file_list = []
|
||||
for f in sorted(files)[:50]: # Limit to 50 files
|
||||
relative_path = f.relative_to(target)
|
||||
file_type = "DIR" if f.is_dir() else "FILE"
|
||||
file_list.append(f" {file_type}: {relative_path}")
|
||||
|
||||
result = f"Contents of {target}:\n" + "\n".join(file_list)
|
||||
if len(files) > 50:
|
||||
result += f"\n... and {len(files) - 50} more files"
|
||||
|
||||
return ToolExecutionResult(success=True, reply_text=result)
|
||||
except Exception as e:
|
||||
return ToolExecutionResult(success=False, reply_text=f"List failed: {e}")
|
||||
|
||||
# read
|
||||
if operation == "read":
|
||||
if not target.exists() or not target.is_file():
|
||||
return ToolExecutionResult(success=False, reply_text=f"File not found: {target}")
|
||||
try:
|
||||
data = target.read_text(encoding="utf-8", errors="replace")
|
||||
max_chars = 10000
|
||||
if len(data) > max_chars:
|
||||
data = data[:max_chars] + f"\n... (truncated, showing first {max_chars} chars)"
|
||||
return ToolExecutionResult(success=True, reply_text=data)
|
||||
except Exception as e:
|
||||
return ToolExecutionResult(success=False, reply_text=f"Read failed: {e}")
|
||||
|
||||
# write
|
||||
if operation == "write":
|
||||
content = args.get("content")
|
||||
if not isinstance(content, str):
|
||||
return ToolExecutionResult(success=False, reply_text="Write requires string 'content'.")
|
||||
try:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(content, encoding="utf-8")
|
||||
return ToolExecutionResult(success=True, reply_text=f"Wrote {len(content)} characters to {target}")
|
||||
except Exception as e:
|
||||
return ToolExecutionResult(success=False, reply_text=f"Write failed: {e}")
|
||||
|
||||
# append
|
||||
if operation == "append":
|
||||
content = args.get("content")
|
||||
if not isinstance(content, str):
|
||||
return ToolExecutionResult(success=False, reply_text="Append requires string 'content'.")
|
||||
try:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with target.open("a", encoding="utf-8", errors="replace") as f:
|
||||
f.write(content)
|
||||
return ToolExecutionResult(success=True, reply_text=f"Appended {len(content)} characters to {target}")
|
||||
except Exception as e:
|
||||
return ToolExecutionResult(success=False, reply_text=f"Append failed: {e}")
|
||||
|
||||
# delete
|
||||
if operation == "delete":
|
||||
try:
|
||||
if target.exists() and target.is_file():
|
||||
target.unlink()
|
||||
return ToolExecutionResult(success=True, reply_text=f"Deleted file: {target}")
|
||||
return ToolExecutionResult(success=False, reply_text=f"File not found: {target}")
|
||||
except Exception as e:
|
||||
return ToolExecutionResult(success=False, reply_text=f"Delete failed: {e}")
|
||||
|
||||
return ToolExecutionResult(success=False, reply_text=f"Unknown localFiles operation: {operation}")
|
||||
except PermissionError as pe:
|
||||
return ToolExecutionResult(success=False, reply_text=f"Permission error: {pe}")
|
||||
except Exception as e:
|
||||
return ToolExecutionResult(success=False, reply_text=f"localFiles error: {e}")
|
||||
Reference in New Issue
Block a user