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.
@@ -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
+ )