antigravity-remote 3.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- antigravity_remote/__init__.py +13 -0
- antigravity_remote/__main__.py +112 -0
- antigravity_remote/agent.py +382 -0
- antigravity_remote/bot.py +168 -0
- antigravity_remote/config.py +73 -0
- antigravity_remote/handlers/__init__.py +77 -0
- antigravity_remote/handlers/ai.py +135 -0
- antigravity_remote/handlers/base.py +35 -0
- antigravity_remote/handlers/control.py +123 -0
- antigravity_remote/handlers/files.py +124 -0
- antigravity_remote/handlers/monitoring.py +199 -0
- antigravity_remote/handlers/quick.py +135 -0
- antigravity_remote/handlers/screen.py +108 -0
- antigravity_remote/state.py +76 -0
- antigravity_remote/utils/__init__.py +16 -0
- antigravity_remote/utils/automation.py +143 -0
- antigravity_remote/utils/ocr.py +98 -0
- antigravity_remote/utils/screenshot.py +49 -0
- antigravity_remote-3.1.0.dist-info/METADATA +114 -0
- antigravity_remote-3.1.0.dist-info/RECORD +23 -0
- antigravity_remote-3.1.0.dist-info/WHEEL +4 -0
- antigravity_remote-3.1.0.dist-info/entry_points.txt +2 -0
- antigravity_remote-3.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Configuration management for Antigravity Remote."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
load_dotenv()
|
|
11
|
+
except ImportError:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
from .secrets import get_bot_token, get_user_id
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Config:
|
|
19
|
+
"""Configuration for Antigravity Remote bot."""
|
|
20
|
+
|
|
21
|
+
# Telegram settings - token is embedded, user ID from local config
|
|
22
|
+
bot_token: str = field(default_factory=get_bot_token)
|
|
23
|
+
allowed_user_id: str = field(default_factory=lambda: get_user_id() or "")
|
|
24
|
+
|
|
25
|
+
# Workspace settings
|
|
26
|
+
workspace_path: Path = field(
|
|
27
|
+
default_factory=lambda: Path(os.getenv("WORKSPACE_PATH", os.getcwd()))
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Security
|
|
31
|
+
lock_password: str = field(default_factory=lambda: os.getenv("LOCK_PASSWORD", "unlock123"))
|
|
32
|
+
|
|
33
|
+
# OCR settings
|
|
34
|
+
tesseract_path: Optional[str] = field(
|
|
35
|
+
default_factory=lambda: os.getenv(
|
|
36
|
+
"TESSERACT_PATH",
|
|
37
|
+
r"C:\Program Files\Tesseract-OCR\tesseract.exe"
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Timing settings
|
|
42
|
+
watchdog_interval: int = field(
|
|
43
|
+
default_factory=lambda: int(os.getenv("WATCHDOG_INTERVAL", "5"))
|
|
44
|
+
)
|
|
45
|
+
alert_cooldown: int = field(
|
|
46
|
+
default_factory=lambda: int(os.getenv("ALERT_COOLDOWN", "30"))
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def validate(self) -> list[str]:
|
|
50
|
+
"""Validate configuration and return list of errors."""
|
|
51
|
+
errors = []
|
|
52
|
+
|
|
53
|
+
if not self.bot_token:
|
|
54
|
+
errors.append("Bot token not available")
|
|
55
|
+
if not self.allowed_user_id:
|
|
56
|
+
errors.append("User not registered. Run: antigravity-remote --register")
|
|
57
|
+
if not self.workspace_path.exists():
|
|
58
|
+
errors.append(f"WORKSPACE_PATH does not exist: {self.workspace_path}")
|
|
59
|
+
|
|
60
|
+
return errors
|
|
61
|
+
|
|
62
|
+
def reload_user_id(self) -> None:
|
|
63
|
+
"""Reload user ID from local config."""
|
|
64
|
+
self.allowed_user_id = get_user_id() or ""
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_env(cls) -> "Config":
|
|
68
|
+
"""Create configuration from environment variables."""
|
|
69
|
+
return cls()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Global config instance
|
|
73
|
+
config = Config.from_env()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Handler modules for Antigravity Remote."""
|
|
2
|
+
|
|
3
|
+
from .control import (
|
|
4
|
+
start_command,
|
|
5
|
+
pause_command,
|
|
6
|
+
resume_command,
|
|
7
|
+
cancel_command,
|
|
8
|
+
key_command,
|
|
9
|
+
lock_command,
|
|
10
|
+
unlock_command,
|
|
11
|
+
)
|
|
12
|
+
from .screen import (
|
|
13
|
+
status_command,
|
|
14
|
+
scroll_command,
|
|
15
|
+
accept_command,
|
|
16
|
+
reject_command,
|
|
17
|
+
undo_command,
|
|
18
|
+
)
|
|
19
|
+
from .files import (
|
|
20
|
+
sysinfo_command,
|
|
21
|
+
files_command,
|
|
22
|
+
read_command,
|
|
23
|
+
diff_command,
|
|
24
|
+
log_command,
|
|
25
|
+
)
|
|
26
|
+
from .monitoring import (
|
|
27
|
+
heartbeat_command,
|
|
28
|
+
watchdog_command,
|
|
29
|
+
schedule_command,
|
|
30
|
+
)
|
|
31
|
+
from .ai import (
|
|
32
|
+
model_command,
|
|
33
|
+
summary_command,
|
|
34
|
+
handle_message,
|
|
35
|
+
handle_model_callback,
|
|
36
|
+
)
|
|
37
|
+
from .quick import (
|
|
38
|
+
quick_replies_command,
|
|
39
|
+
handle_quick_callback,
|
|
40
|
+
handle_voice,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
# Control
|
|
45
|
+
"start_command",
|
|
46
|
+
"pause_command",
|
|
47
|
+
"resume_command",
|
|
48
|
+
"cancel_command",
|
|
49
|
+
"key_command",
|
|
50
|
+
"lock_command",
|
|
51
|
+
"unlock_command",
|
|
52
|
+
# Screen
|
|
53
|
+
"status_command",
|
|
54
|
+
"scroll_command",
|
|
55
|
+
"accept_command",
|
|
56
|
+
"reject_command",
|
|
57
|
+
"undo_command",
|
|
58
|
+
# Files
|
|
59
|
+
"sysinfo_command",
|
|
60
|
+
"files_command",
|
|
61
|
+
"read_command",
|
|
62
|
+
"diff_command",
|
|
63
|
+
"log_command",
|
|
64
|
+
# Monitoring
|
|
65
|
+
"heartbeat_command",
|
|
66
|
+
"watchdog_command",
|
|
67
|
+
"schedule_command",
|
|
68
|
+
# AI
|
|
69
|
+
"model_command",
|
|
70
|
+
"summary_command",
|
|
71
|
+
"handle_message",
|
|
72
|
+
"handle_model_callback",
|
|
73
|
+
# Quick
|
|
74
|
+
"quick_replies_command",
|
|
75
|
+
"handle_quick_callback",
|
|
76
|
+
"handle_voice",
|
|
77
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""AI command handlers for Antigravity Remote."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import pyautogui
|
|
8
|
+
import pyperclip
|
|
9
|
+
|
|
10
|
+
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
|
11
|
+
from telegram.ext import ContextTypes
|
|
12
|
+
from telegram.constants import ParseMode
|
|
13
|
+
|
|
14
|
+
from .base import is_authorized
|
|
15
|
+
from ..state import state
|
|
16
|
+
from ..utils import focus_antigravity, send_to_antigravity, take_screenshot, cleanup_screenshot
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Available models in Antigravity
|
|
21
|
+
MODELS = [
|
|
22
|
+
("Gemini 3 Pro (High)", "gemini_3_pro_high"),
|
|
23
|
+
("Gemini 3 Pro (Low)", "gemini_3_pro_low"),
|
|
24
|
+
("Gemini 3 Flash", "gemini_3_flash"),
|
|
25
|
+
("Claude Sonnet 4.5", "claude_sonnet_45"),
|
|
26
|
+
("Claude Sonnet 4.5 (Thinking)", "claude_sonnet_45_thinking"),
|
|
27
|
+
("Claude Opus 4.5 (Thinking)", "claude_opus_45_thinking"),
|
|
28
|
+
("GPT-OSS 120B (Medium)", "gpt_oss_120b"),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
MODEL_NAMES = {model_id: name for name, model_id in MODELS}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def model_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
35
|
+
"""Show model selection menu."""
|
|
36
|
+
if not await is_authorized(update):
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
keyboard = [
|
|
40
|
+
[InlineKeyboardButton(name, callback_data=f"model_{model_id}")]
|
|
41
|
+
for name, model_id in MODELS
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
await update.message.reply_text(
|
|
45
|
+
"🤖 *Select a model:*\n\n_Note: Model availability depends on your subscription_",
|
|
46
|
+
reply_markup=InlineKeyboardMarkup(keyboard),
|
|
47
|
+
parse_mode=ParseMode.MARKDOWN
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def summary_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
52
|
+
"""Ask Antigravity for a task summary."""
|
|
53
|
+
if not await is_authorized(update):
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
summary_prompt = "Please give me a brief summary of what you just did in the last task."
|
|
57
|
+
|
|
58
|
+
status_msg = await update.message.reply_text("📝 Asking for summary...")
|
|
59
|
+
success = await asyncio.to_thread(send_to_antigravity, summary_prompt)
|
|
60
|
+
|
|
61
|
+
if success:
|
|
62
|
+
keyboard = [[InlineKeyboardButton("📸 Get Summary", callback_data="screenshot")]]
|
|
63
|
+
await status_msg.edit_text(
|
|
64
|
+
"📝 *Summary requested!*\nWait a moment for the response, then tap:",
|
|
65
|
+
reply_markup=InlineKeyboardMarkup(keyboard),
|
|
66
|
+
parse_mode=ParseMode.MARKDOWN
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
await status_msg.edit_text("❌ Failed to send summary request")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
73
|
+
"""Handle text messages - relay to Antigravity."""
|
|
74
|
+
if not await is_authorized(update):
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
if state.locked:
|
|
78
|
+
await update.message.reply_text("🔒 Bot is locked. Use /unlock <password>")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
if state.paused:
|
|
82
|
+
await update.message.reply_text("⏸️ Relay is paused. Use /resume")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
user_msg = update.message.text
|
|
86
|
+
|
|
87
|
+
# Log command
|
|
88
|
+
state.log_command(user_msg)
|
|
89
|
+
|
|
90
|
+
status_msg = await update.message.reply_text("📤 Sending to Antigravity...")
|
|
91
|
+
success = await asyncio.to_thread(send_to_antigravity, user_msg)
|
|
92
|
+
|
|
93
|
+
if not success:
|
|
94
|
+
await status_msg.edit_text("❌ Failed to send. Is Antigravity app open?")
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
keyboard = [[InlineKeyboardButton("📸 Get Result", callback_data="screenshot")]]
|
|
98
|
+
await status_msg.edit_text(
|
|
99
|
+
"✅ *Sent!* Tap when ready:",
|
|
100
|
+
reply_markup=InlineKeyboardMarkup(keyboard),
|
|
101
|
+
parse_mode=ParseMode.MARKDOWN
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def handle_model_callback(
|
|
106
|
+
query,
|
|
107
|
+
context: ContextTypes.DEFAULT_TYPE,
|
|
108
|
+
model_id: str
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Handle model selection callback."""
|
|
111
|
+
model_name = MODEL_NAMES.get(model_id, model_id)
|
|
112
|
+
|
|
113
|
+
def switch_model():
|
|
114
|
+
focus_antigravity()
|
|
115
|
+
time.sleep(0.2)
|
|
116
|
+
# Open command palette
|
|
117
|
+
pyautogui.hotkey('ctrl', 'shift', 'p')
|
|
118
|
+
time.sleep(0.5)
|
|
119
|
+
# Type model switch command
|
|
120
|
+
pyperclip.copy("Switch Model")
|
|
121
|
+
pyautogui.hotkey('ctrl', 'v')
|
|
122
|
+
time.sleep(0.3)
|
|
123
|
+
pyautogui.press('enter')
|
|
124
|
+
time.sleep(0.3)
|
|
125
|
+
# Type model name
|
|
126
|
+
pyperclip.copy(model_name)
|
|
127
|
+
pyautogui.hotkey('ctrl', 'v')
|
|
128
|
+
time.sleep(0.2)
|
|
129
|
+
pyautogui.press('enter')
|
|
130
|
+
|
|
131
|
+
await asyncio.to_thread(switch_model)
|
|
132
|
+
await query.message.reply_text(
|
|
133
|
+
f"🔄 Switching to *{model_name}*...",
|
|
134
|
+
parse_mode=ParseMode.MARKDOWN
|
|
135
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Base handler functionality for Antigravity Remote."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Callable, TypeVar, ParamSpec
|
|
6
|
+
|
|
7
|
+
from telegram import Update
|
|
8
|
+
from telegram.ext import ContextTypes
|
|
9
|
+
|
|
10
|
+
from ..config import config
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
P = ParamSpec('P')
|
|
15
|
+
T = TypeVar('T')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def authorized_only(func: Callable[P, T]) -> Callable[P, T]:
|
|
19
|
+
"""Decorator to require authorization for a handler."""
|
|
20
|
+
@wraps(func)
|
|
21
|
+
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs):
|
|
22
|
+
user_id = str(update.effective_user.id)
|
|
23
|
+
|
|
24
|
+
if user_id != config.allowed_user_id:
|
|
25
|
+
logger.warning(f"Unauthorized access attempt from user {user_id}")
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
return await func(update, context, *args, **kwargs)
|
|
29
|
+
|
|
30
|
+
return wrapper
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def is_authorized(update: Update) -> bool:
|
|
34
|
+
"""Check if the update is from an authorized user."""
|
|
35
|
+
return str(update.effective_user.id) == config.allowed_user_id
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Control command handlers for Antigravity Remote."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import pyautogui
|
|
8
|
+
|
|
9
|
+
from telegram import Update
|
|
10
|
+
from telegram.ext import ContextTypes
|
|
11
|
+
from telegram.constants import ParseMode
|
|
12
|
+
|
|
13
|
+
from .base import is_authorized
|
|
14
|
+
from ..state import state
|
|
15
|
+
from ..utils import focus_antigravity, send_key_combo
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
21
|
+
"""Show help menu."""
|
|
22
|
+
if not await is_authorized(update):
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
help_text = """🔗 *Antigravity Remote Control*
|
|
26
|
+
|
|
27
|
+
*Relay:* Send any message to relay it.
|
|
28
|
+
|
|
29
|
+
*Commands:*
|
|
30
|
+
`/status` - Screenshot now
|
|
31
|
+
`/model` - Switch AI model
|
|
32
|
+
`/quick` - Quick reply buttons
|
|
33
|
+
`/summary` - Ask for task summary
|
|
34
|
+
`/watchdog` - Smart auto-alerts
|
|
35
|
+
`/pause` / `/resume` - Toggle relay
|
|
36
|
+
`/cancel` - Send Escape
|
|
37
|
+
`/scroll` - up/down/top/bottom
|
|
38
|
+
`/accept` / `/reject` - Click buttons
|
|
39
|
+
`/undo` - Ctrl+Z
|
|
40
|
+
`/key` - Send key combo
|
|
41
|
+
`/schedule` - Schedule command
|
|
42
|
+
`/sysinfo` - System stats
|
|
43
|
+
`/files` - List files
|
|
44
|
+
`/read` - Read file
|
|
45
|
+
`/diff` - Git diff
|
|
46
|
+
`/log` - Command history
|
|
47
|
+
`/lock` / `/unlock` - Security
|
|
48
|
+
`/heartbeat` - Auto screenshots
|
|
49
|
+
"""
|
|
50
|
+
await update.message.reply_text(help_text, parse_mode=ParseMode.MARKDOWN)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def pause_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
54
|
+
"""Pause message relay."""
|
|
55
|
+
if not await is_authorized(update):
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
state.paused = True
|
|
59
|
+
await update.message.reply_text("⏸️ Relay paused. Use /resume to continue.")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def resume_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
63
|
+
"""Resume message relay."""
|
|
64
|
+
if not await is_authorized(update):
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
state.paused = False
|
|
68
|
+
await update.message.reply_text("▶️ Relay resumed!")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def cancel_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
72
|
+
"""Send Escape key."""
|
|
73
|
+
if not await is_authorized(update):
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
await asyncio.to_thread(lambda: (focus_antigravity(), pyautogui.press('escape')))
|
|
77
|
+
await update.message.reply_text("❌ Sent Escape key")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def key_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
81
|
+
"""Send a key combination."""
|
|
82
|
+
if not await is_authorized(update):
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
args = context.args
|
|
86
|
+
if not args:
|
|
87
|
+
await update.message.reply_text("Usage: /key ctrl+s or /key alt+shift+tab")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
combo = args[0].lower().split('+')
|
|
91
|
+
success = await asyncio.to_thread(send_key_combo, combo)
|
|
92
|
+
|
|
93
|
+
if success:
|
|
94
|
+
await update.message.reply_text(
|
|
95
|
+
f"⌨️ Sent: `{'+'.join(combo)}`",
|
|
96
|
+
parse_mode=ParseMode.MARKDOWN
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
await update.message.reply_text("❌ Failed to send key combo")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def lock_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
103
|
+
"""Lock the bot."""
|
|
104
|
+
if not await is_authorized(update):
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
state.locked = True
|
|
108
|
+
await update.message.reply_text("🔒 Bot locked. Use /unlock <password> to unlock.")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def unlock_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
112
|
+
"""Unlock the bot."""
|
|
113
|
+
if not await is_authorized(update):
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
from ..config import config
|
|
117
|
+
|
|
118
|
+
args = context.args
|
|
119
|
+
if args and args[0] == config.lock_password:
|
|
120
|
+
state.locked = False
|
|
121
|
+
await update.message.reply_text("🔓 Bot unlocked!")
|
|
122
|
+
else:
|
|
123
|
+
await update.message.reply_text("❌ Wrong password.")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""File command handlers for Antigravity Remote."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
import psutil
|
|
8
|
+
from telegram import Update
|
|
9
|
+
from telegram.ext import ContextTypes
|
|
10
|
+
from telegram.constants import ParseMode
|
|
11
|
+
|
|
12
|
+
from .base import is_authorized
|
|
13
|
+
from ..config import config
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def sysinfo_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
19
|
+
"""Show system information."""
|
|
20
|
+
if not await is_authorized(update):
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
cpu = psutil.cpu_percent(interval=1)
|
|
24
|
+
mem = psutil.virtual_memory()
|
|
25
|
+
disk = psutil.disk_usage('C:/')
|
|
26
|
+
|
|
27
|
+
msg = f"""⚙️ *System Info*
|
|
28
|
+
CPU: `{cpu}%`
|
|
29
|
+
RAM: `{mem.percent}%` ({mem.used // (1024**3)}GB / {mem.total // (1024**3)}GB)
|
|
30
|
+
Disk C: `{disk.percent}%` ({disk.used // (1024**3)}GB / {disk.total // (1024**3)}GB)
|
|
31
|
+
"""
|
|
32
|
+
await update.message.reply_text(msg, parse_mode=ParseMode.MARKDOWN)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def files_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
36
|
+
"""List files in workspace."""
|
|
37
|
+
if not await is_authorized(update):
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
items = os.listdir(config.workspace_path)
|
|
42
|
+
files = []
|
|
43
|
+
for item in items[:30]:
|
|
44
|
+
path = config.workspace_path / item
|
|
45
|
+
icon = "📄" if path.is_file() else "📁"
|
|
46
|
+
files.append(f"{icon} {item}")
|
|
47
|
+
|
|
48
|
+
await update.message.reply_text(
|
|
49
|
+
f"📂 *Files in workspace:*\n" + "\n".join(files),
|
|
50
|
+
parse_mode=ParseMode.MARKDOWN
|
|
51
|
+
)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
await update.message.reply_text(f"Error: {e}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def read_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
57
|
+
"""Read a file's contents."""
|
|
58
|
+
if not await is_authorized(update):
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
args = context.args
|
|
62
|
+
if not args:
|
|
63
|
+
await update.message.reply_text("Usage: /read filename.txt")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
filepath = config.workspace_path / args[0]
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
70
|
+
content = f.read()[:3000]
|
|
71
|
+
|
|
72
|
+
await update.message.reply_text(
|
|
73
|
+
f"📄 *{args[0]}*:\n```\n{content}\n```",
|
|
74
|
+
parse_mode=ParseMode.MARKDOWN
|
|
75
|
+
)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
await update.message.reply_text(f"Error reading file: {e}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def diff_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
81
|
+
"""Show git diff."""
|
|
82
|
+
if not await is_authorized(update):
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
result = subprocess.run(
|
|
87
|
+
['git', 'diff', '--stat'],
|
|
88
|
+
cwd=config.workspace_path,
|
|
89
|
+
capture_output=True,
|
|
90
|
+
text=True,
|
|
91
|
+
timeout=10
|
|
92
|
+
)
|
|
93
|
+
output = result.stdout[:3000] or "No changes"
|
|
94
|
+
|
|
95
|
+
await update.message.reply_text(
|
|
96
|
+
f"📊 *Git Diff:*\n```\n{output}\n```",
|
|
97
|
+
parse_mode=ParseMode.MARKDOWN
|
|
98
|
+
)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
await update.message.reply_text(f"Error: {e}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def log_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
104
|
+
"""Show command history."""
|
|
105
|
+
if not await is_authorized(update):
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
from ..state import state
|
|
109
|
+
|
|
110
|
+
logs = state.get_recent_logs(10)
|
|
111
|
+
|
|
112
|
+
if not logs:
|
|
113
|
+
await update.message.reply_text("📋 No commands logged yet.")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
log_text = "\n".join([
|
|
117
|
+
f"`{entry.to_dict()['time']}`: {entry.message[:50]}"
|
|
118
|
+
for entry in logs
|
|
119
|
+
])
|
|
120
|
+
|
|
121
|
+
await update.message.reply_text(
|
|
122
|
+
f"📋 *Recent Commands:*\n{log_text}",
|
|
123
|
+
parse_mode=ParseMode.MARKDOWN
|
|
124
|
+
)
|