Files
timmy-home/timmy-local/evennia/commands/tools.py

548 lines
15 KiB
Python

#!/usr/bin/env python3
"""
Timmy Tool Commands
Issue #84 — Bridge Tools into Evennia
Converts Timmy's tool library into Evennia Command objects
so they can be invoked within the world.
"""
from evennia import Command
from evennia.utils import evtable
from typing import Optional, List
import json
import os
class CmdRead(Command):
"""
Read a file from the system.
Usage:
read <path>
Example:
read ~/.timmy/config.yaml
read /opt/timmy/logs/latest.log
"""
key = "read"
aliases = ["cat", "show"]
help_category = "Tools"
def func(self):
if not self.args:
self.caller.msg("Usage: read <path>")
return
path = self.args.strip()
path = os.path.expanduser(path)
try:
with open(path, 'r') as f:
content = f.read()
# Store for later use
self.caller.db.last_read_file = path
self.caller.db.last_read_content = content
# Limit display if too long
lines = content.split('\n')
if len(lines) > 50:
display = '\n'.join(lines[:50])
self.caller.msg(f"|w{path}|n (showing first 50 lines of {len(lines)}):")
self.caller.msg(display)
self.caller.msg(f"\n|y... {len(lines) - 50} more lines|n")
else:
self.caller.msg(f"|w{path}|n:")
self.caller.msg(content)
# Record in metrics
if hasattr(self.caller, 'update_metrics'):
self.caller.update_metrics(files_read=1)
except FileNotFoundError:
self.caller.msg(f"|rFile not found:|n {path}")
except PermissionError:
self.caller.msg(f"|rPermission denied:|n {path}")
except Exception as e:
self.caller.msg(f"|rError reading file:|n {e}")
class CmdWrite(Command):
"""
Write content to a file.
Usage:
write <path> = <content>
Example:
write ~/.timmy/notes.txt = This is a note
"""
key = "write"
aliases = ["save"]
help_category = "Tools"
def func(self):
if not self.args or "=" not in self.args:
self.caller.msg("Usage: write <path> = <content>")
return
path, content = self.args.split("=", 1)
path = path.strip()
content = content.strip()
path = os.path.expanduser(path)
try:
# Create directory if needed
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as f:
f.write(content)
self.caller.msg(f"|gWritten:|n {path}")
# Update metrics
if hasattr(self.caller, 'update_metrics'):
self.caller.update_metrics(files_modified=1, lines_written=content.count('\n'))
except PermissionError:
self.caller.msg(f"|rPermission denied:|n {path}")
except Exception as e:
self.caller.msg(f"|rError writing file:|n {e}")
class CmdSearch(Command):
"""
Search file contents for a pattern.
Usage:
search <pattern> [in <path>]
Example:
search "def main" in ~/code/
search "TODO"
"""
key = "search"
aliases = ["grep", "find"]
help_category = "Tools"
def func(self):
if not self.args:
self.caller.msg("Usage: search <pattern> [in <path>]")
return
args = self.args.strip()
# Parse path if specified
if " in " in args:
pattern, path = args.split(" in ", 1)
pattern = pattern.strip()
path = path.strip()
else:
pattern = args
path = "."
path = os.path.expanduser(path)
try:
import subprocess
result = subprocess.run(
["grep", "-r", "-n", pattern, path],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
lines = result.stdout.strip().split('\n')
self.caller.msg(f"|gFound {len(lines)} matches for '|n{pattern}|g':|n")
for line in lines[:20]: # Limit output
self.caller.msg(f" {line}")
if len(lines) > 20:
self.caller.msg(f"\n|y... and {len(lines) - 20} more|n")
else:
self.caller.msg(f"|yNo matches found for '|n{pattern}|y'|n")
except subprocess.TimeoutExpired:
self.caller.msg("|rSearch timed out|n")
except Exception as e:
self.caller.msg(f"|rError searching:|n {e}")
class CmdGitStatus(Command):
"""
Check git status of a repository.
Usage:
git status [path]
Example:
git status
git status ~/projects/timmy
"""
key = "git_status"
aliases = ["git status"]
help_category = "Git"
def func(self):
path = self.args.strip() if self.args else "."
path = os.path.expanduser(path)
try:
import subprocess
result = subprocess.run(
["git", "-C", path, "status", "-sb"],
capture_output=True,
text=True
)
if result.returncode == 0:
self.caller.msg(f"|wGit status ({path}):|n")
self.caller.msg(result.stdout)
else:
self.caller.msg(f"|rNot a git repository:|n {path}")
except Exception as e:
self.caller.msg(f"|rError:|n {e}")
class CmdGitLog(Command):
"""
Show git commit history.
Usage:
git log [n] [path]
Example:
git log
git log 10
git log 5 ~/projects/timmy
"""
key = "git_log"
aliases = ["git log"]
help_category = "Git"
def func(self):
args = self.args.strip().split() if self.args else []
# Parse args
path = "."
n = 10
for arg in args:
if arg.isdigit():
n = int(arg)
else:
path = arg
path = os.path.expanduser(path)
try:
import subprocess
result = subprocess.run(
["git", "-C", path, "log", f"--oneline", f"-{n}"],
capture_output=True,
text=True
)
if result.returncode == 0:
self.caller.msg(f"|wRecent commits ({path}):|n")
self.caller.msg(result.stdout)
else:
self.caller.msg(f"|rNot a git repository:|n {path}")
except Exception as e:
self.caller.msg(f"|rError:|n {e}")
class CmdGitPull(Command):
"""
Pull latest changes from git remote.
Usage:
git pull [path]
"""
key = "git_pull"
aliases = ["git pull"]
help_category = "Git"
def func(self):
path = self.args.strip() if self.args else "."
path = os.path.expanduser(path)
try:
import subprocess
result = subprocess.run(
["git", "-C", path, "pull"],
capture_output=True,
text=True
)
if result.returncode == 0:
self.caller.msg(f"|gPulled ({path}):|n")
self.caller.msg(result.stdout)
else:
self.caller.msg(f"|rPull failed:|n {result.stderr}")
except Exception as e:
self.caller.msg(f"|rError:|n {e}")
class CmdSysInfo(Command):
"""
Display system information.
Usage:
sysinfo
"""
key = "sysinfo"
aliases = ["system_info", "status"]
help_category = "System"
def func(self):
import platform
import psutil
# Gather info
info = {
"Platform": platform.platform(),
"CPU": f"{psutil.cpu_count()} cores, {psutil.cpu_percent()}% used",
"Memory": f"{psutil.virtual_memory().percent}% used "
f"({psutil.virtual_memory().used // (1024**3)}GB / "
f"{psutil.virtual_memory().total // (1024**3)}GB)",
"Disk": f"{psutil.disk_usage('/').percent}% used "
f"({psutil.disk_usage('/').free // (1024**3)}GB free)",
"Uptime": f"{psutil.boot_time()}" # Simplified
}
self.caller.msg("|wSystem Information:|n")
for key, value in info.items():
self.caller.msg(f" |c{key}|n: {value}")
class CmdHealth(Command):
"""
Check health of Timmy services.
Usage:
health
"""
key = "health"
aliases = ["check"]
help_category = "System"
def func(self):
import subprocess
services = [
"timmy-overnight-loop",
"timmy-health",
"llama-server",
"gitea"
]
self.caller.msg("|wService Health:|n")
for service in services:
try:
result = subprocess.run(
["systemctl", "is-active", service],
capture_output=True,
text=True
)
status = result.stdout.strip()
icon = "|g●|n" if status == "active" else "|r●|n"
self.caller.msg(f" {icon} {service}: {status}")
except:
self.caller.msg(f" |y?|n {service}: unknown")
class CmdThink(Command):
"""
Send a prompt to the local LLM and return the response.
Usage:
think <prompt>
Example:
think What should I focus on today?
think Summarize the last git commit
"""
key = "think"
aliases = ["reason", "ponder"]
help_category = "Inference"
def func(self):
if not self.args:
self.caller.msg("Usage: think <prompt>")
return
prompt = self.args.strip()
self.caller.msg(f"|wThinking about:|n {prompt[:50]}...")
try:
import requests
response = requests.post(
"http://localhost:8080/v1/chat/completions",
json={
"model": "hermes4",
"messages": [
{"role": "user", "content": prompt}
],
"max_tokens": 500
},
timeout=60
)
if response.status_code == 200:
result = response.json()
content = result["choices"][0]["message"]["content"]
self.caller.msg(f"\n|cResponse:|n\n{content}")
else:
self.caller.msg(f"|rError:|n HTTP {response.status_code}")
except requests.exceptions.ConnectionError:
self.caller.msg("|rError:|n llama-server not running on localhost:8080")
except Exception as e:
self.caller.msg(f"|rError:|n {e}")
class CmdGiteaIssues(Command):
"""
List open issues from Gitea.
Usage:
gitea issues
gitea issues --limit 5
"""
key = "gitea_issues"
aliases = ["issues"]
help_category = "Gitea"
def func(self):
args = self.args.strip().split() if self.args else []
limit = 10
for i, arg in enumerate(args):
if arg == "--limit" and i + 1 < len(args):
limit = int(args[i + 1])
try:
import requests
# Get issues from Gitea API
response = requests.get(
"http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/timmy-home/issues",
params={"state": "open", "limit": limit},
timeout=10
)
if response.status_code == 200:
issues = response.json()
self.caller.msg(f"|wOpen Issues ({len(issues)}):|n\n")
for issue in issues:
num = issue["number"]
title = issue["title"][:60]
assignee = issue.get("assignee", {}).get("login", "unassigned")
self.caller.msg(f" |y#{num}|n: {title} (|c{assignee}|n)")
else:
self.caller.msg(f"|rError:|n HTTP {response.status_code}")
except Exception as e:
self.caller.msg(f"|rError:|n {e}")
class CmdWorkshop(Command):
"""
Enter the Workshop room.
Usage:
workshop
"""
key = "workshop"
help_category = "Navigation"
def func(self):
# Find workshop
workshop = self.caller.search("Workshop", global_search=True)
if workshop:
self.caller.move_to(workshop)
class CmdLibrary(Command):
"""
Enter the Library room.
Usage:
library
"""
key = "library"
help_category = "Navigation"
def func(self):
library = self.caller.search("Library", global_search=True)
if library:
self.caller.move_to(library)
class CmdObservatory(Command):
"""
Enter the Observatory room.
Usage:
observatory
"""
key = "observatory"
help_category = "Navigation"
def func(self):
obs = self.caller.search("Observatory", global_search=True)
if obs:
self.caller.move_to(obs)
class CmdStatus(Command):
"""
Show Timmy's current status.
Usage:
status
"""
key = "status"
help_category = "Info"
def func(self):
if hasattr(self.caller, 'get_status'):
status = self.caller.get_status()
self.caller.msg("|wTimmy Status:|n\n")
if status.get('current_task'):
self.caller.msg(f"|yCurrent Task:|n {status['current_task']['description']}")
else:
self.caller.msg("|gNo active task|n")
self.caller.msg(f"Tasks Completed: {status['tasks_completed']}")
self.caller.msg(f"Knowledge Items: {status['knowledge_items']}")
self.caller.msg(f"Tools Available: {status['tools_available']}")
self.caller.msg(f"Location: {status['location']}")
else:
self.caller.msg("Status not available.")