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,13 @@
1
+ """Antigravity Remote - Secure remote control via Telegram."""
2
+
3
+ __version__ = "3.0.0"
4
+
5
+ from .agent import LocalAgent, run_agent
6
+ from .secrets import get_user_config, save_user_config
7
+
8
+ __all__ = [
9
+ "LocalAgent",
10
+ "run_agent",
11
+ "get_user_config",
12
+ "save_user_config",
13
+ ]
@@ -0,0 +1,112 @@
1
+ """CLI entry point for Antigravity Remote (Secure Version)."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import logging
6
+ import sys
7
+
8
+ from .agent import run_agent
9
+ from .secrets import get_user_config, save_user_config, clear_user_config, get_user_config_path
10
+
11
+
12
+ def setup_logging(verbose: bool = False) -> None:
13
+ level = logging.DEBUG if verbose else logging.INFO
14
+ logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=level)
15
+
16
+
17
+ def register_user() -> None:
18
+ """Secure user registration."""
19
+ print("� Antigravity Remote - Secure Registration")
20
+ print()
21
+ print("To get your credentials:")
22
+ print("1. Open Telegram and message @antigravityrcbot")
23
+ print("2. Send /start - you'll see your ID and Auth Token")
24
+ print()
25
+
26
+ user_id = input("Enter your Telegram User ID: ").strip()
27
+ if not user_id.isdigit():
28
+ print("❌ Invalid user ID. It should be a number.")
29
+ sys.exit(1)
30
+
31
+ auth_token = input("Enter your Auth Token: ").strip()
32
+ if len(auth_token) != 32:
33
+ print("❌ Invalid auth token. Should be 32 characters.")
34
+ sys.exit(1)
35
+
36
+ save_user_config(user_id, auth_token)
37
+ print(f"✅ Registered securely!")
38
+ print(f" Config saved to: {get_user_config_path()}")
39
+ print()
40
+ print("Now run: antigravity-remote")
41
+
42
+
43
+ def show_status() -> None:
44
+ config = get_user_config()
45
+ print("📊 Antigravity Remote - Status")
46
+ print()
47
+ if config:
48
+ print(f"✅ User ID: {config['user_id']}")
49
+ print(f"🔑 Auth Token: {config['auth_token'][:8]}...")
50
+ else:
51
+ print("❌ Not registered. Run: antigravity-remote --register")
52
+ print(f"📁 Config: {get_user_config_path()}")
53
+ print()
54
+ print("📱 Bot: @antigravityrcbot")
55
+
56
+
57
+ def main() -> None:
58
+ parser = argparse.ArgumentParser(description="Secure remote control for Antigravity AI")
59
+
60
+ parser.add_argument("--register", action="store_true", help="Register your credentials")
61
+ parser.add_argument("--status", action="store_true", help="Show registration status")
62
+ parser.add_argument("--unregister", action="store_true", help="Remove your registration")
63
+ parser.add_argument("--server", help="Custom server URL")
64
+ parser.add_argument("-v", "--verbose", action="store_true", help="Verbose logging")
65
+ parser.add_argument("--version", action="version", version="antigravity-remote 3.0.0")
66
+
67
+ args = parser.parse_args()
68
+
69
+ if args.register:
70
+ register_user()
71
+ return
72
+
73
+ if args.status:
74
+ show_status()
75
+ return
76
+
77
+ if args.unregister:
78
+ clear_user_config()
79
+ print("✅ Unregistered.")
80
+ return
81
+
82
+ setup_logging(args.verbose)
83
+
84
+ config = get_user_config()
85
+ if not config:
86
+ print("❌ Not registered!")
87
+ print()
88
+ print("Run: antigravity-remote --register")
89
+ sys.exit(1)
90
+
91
+ user_id = config["user_id"]
92
+ auth_token = config["auth_token"]
93
+
94
+ print("� Antigravity Remote Control (Secure)")
95
+ print(f" User ID: {user_id}")
96
+ print(f" Auth: {auth_token[:8]}...")
97
+ print(f" Bot: @antigravityrcbot")
98
+ print()
99
+ print("📱 Open Telegram and message @antigravityrcbot to control your PC!")
100
+ print()
101
+ print("Press Ctrl+C to stop")
102
+ print()
103
+
104
+ try:
105
+ asyncio.run(run_agent(user_id, auth_token, args.server))
106
+ except KeyboardInterrupt:
107
+ print("\n👋 Shutting down...")
108
+ sys.exit(0)
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1,382 @@
1
+ """
2
+ Local Agent for Antigravity Remote - SECURE VERSION
3
+ Handles authentication and all commands including MEDIA (Voice, Photo, Files)
4
+ """
5
+
6
+ import asyncio
7
+ import base64
8
+ import json
9
+ import logging
10
+ import os
11
+ import time
12
+ import hashlib
13
+ import re
14
+ from pathlib import Path
15
+
16
+ import websockets
17
+
18
+ from .utils import (
19
+ send_to_antigravity,
20
+ send_key_combo,
21
+ scroll_screen,
22
+ take_screenshot,
23
+ cleanup_screenshot,
24
+ focus_antigravity,
25
+ )
26
+
27
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
28
+ logger = logging.getLogger(__name__)
29
+
30
+ DEFAULT_SERVER_URL = os.environ.get("ANTIGRAVITY_SERVER", "wss://antigravity-remote.onrender.com/ws")
31
+
32
+ # OCR keywords
33
+ APPROVAL_KEYWORDS = ["run command", "accept changes", "proceed", "approve", "allow", "confirm", "y/n"]
34
+ DONE_KEYWORDS = ["anything else", "let me know", "task complete", "done!", "successfully", "finished"]
35
+ ERROR_KEYWORDS = ["error:", "failed", "exception", "traceback", "cannot", "permission denied"]
36
+
37
+
38
+ def sanitize_input(text: str, max_length: int = 4000) -> str:
39
+ """Sanitize input."""
40
+ if not text:
41
+ return ""
42
+ text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', text)
43
+ return text[:max_length]
44
+
45
+
46
+ class LocalAgent:
47
+ """Secure local agent with authentication and media support."""
48
+
49
+ def __init__(self, user_id: str, auth_token: str, server_url: str = None):
50
+ self.user_id = user_id
51
+ self.auth_token = auth_token
52
+ self.server_url = server_url or DEFAULT_SERVER_URL
53
+ self.websocket = None
54
+ self.running = False
55
+ self.watchdog_enabled = False
56
+ self.watchdog_task = None
57
+ self.last_screen_hash = None
58
+ self.idle_count = 0
59
+
60
+ # Setup download dirs
61
+ self.downloads_dir = Path.home() / "Downloads" / "AntigravityRemote"
62
+ self.downloads_dir.mkdir(parents=True, exist_ok=True)
63
+
64
+ async def connect(self):
65
+ url = f"{self.server_url}/{self.user_id}"
66
+ logger.info(f"🔌 Connecting to server...")
67
+
68
+ try:
69
+ self.websocket = await websockets.connect(url)
70
+
71
+ # Send authentication
72
+ auth_msg = json.dumps({"auth_token": self.auth_token})
73
+ await self.websocket.send(auth_msg)
74
+
75
+ # Wait for strict auth response
76
+ try:
77
+ response = await asyncio.wait_for(self.websocket.recv(), timeout=10.0)
78
+ resp = json.loads(response)
79
+ if "error" in resp:
80
+ logger.error(f"❌ Authentication failed: {resp['error']}")
81
+ return False
82
+ except asyncio.TimeoutError:
83
+ # Legacy server might not send response immediately, assume ok but warn
84
+ logger.warning("⚠️ No auth response (legacy server?), assuming connected")
85
+
86
+ logger.info("✅ Authenticated and connected!")
87
+ return True
88
+
89
+ except Exception as e:
90
+ logger.error(f"❌ Connection failed: {e}")
91
+ return False
92
+
93
+ async def send_alert(self, alert_type: str, text: str, include_screenshot: bool = False):
94
+ """Send alert to server."""
95
+ if not self.websocket:
96
+ return
97
+
98
+ alert = {"type": "alert", "alert_type": alert_type, "text": text}
99
+
100
+ if include_screenshot:
101
+ path = take_screenshot()
102
+ if path:
103
+ with open(path, "rb") as f:
104
+ alert["image"] = base64.b64encode(f.read()).decode()
105
+ cleanup_screenshot(path)
106
+
107
+ try:
108
+ await self.websocket.send(json.dumps(alert))
109
+ except Exception:
110
+ pass
111
+
112
+ async def run_watchdog(self):
113
+ """Background watchdog loop."""
114
+ logger.info("🐕 Watchdog started")
115
+ last_alert_time = 0
116
+
117
+ while self.watchdog_enabled:
118
+ await asyncio.sleep(5)
119
+
120
+ try:
121
+ path = take_screenshot()
122
+ if not path:
123
+ continue
124
+
125
+ with open(path, "rb") as f:
126
+ data = f.read()
127
+ current_hash = hashlib.md5(data[:10000]).hexdigest()
128
+
129
+ if current_hash == self.last_screen_hash:
130
+ self.idle_count += 1
131
+ else:
132
+ self.idle_count = 0
133
+ self.last_screen_hash = current_hash
134
+
135
+ # Try OCR
136
+ try:
137
+ import pytesseract
138
+ from PIL import Image
139
+ img = Image.open(path)
140
+ text = pytesseract.image_to_string(img).lower()
141
+
142
+ current_time = time.time()
143
+ if current_time - last_alert_time > 30:
144
+ for kw in APPROVAL_KEYWORDS:
145
+ if kw in text:
146
+ await self.send_alert("approval", f"🚨 *Approval needed!*\nDetected: `{kw}`", True)
147
+ last_alert_time = current_time
148
+ break
149
+
150
+ for kw in DONE_KEYWORDS:
151
+ if kw in text:
152
+ await self.send_alert("done", f"✅ *Task complete!*\nDetected: `{kw}`", True)
153
+ last_alert_time = current_time
154
+ break
155
+
156
+ for kw in ERROR_KEYWORDS:
157
+ if kw in text:
158
+ await self.send_alert("error", f"⚠️ *Error detected!*\nDetected: `{kw}`", True)
159
+ last_alert_time = current_time
160
+ break
161
+ except ImportError:
162
+ pass
163
+
164
+ cleanup_screenshot(path)
165
+
166
+ if self.idle_count >= 3 and time.time() - last_alert_time > 60:
167
+ await self.send_alert("idle", "💤 *Screen idle*", True)
168
+ last_alert_time = time.time()
169
+ self.idle_count = 0
170
+
171
+ except Exception as e:
172
+ logger.error(f"Watchdog error: {e}")
173
+
174
+ logger.info("🐕 Watchdog stopped")
175
+
176
+ def process_voice(self, audio_path: Path) -> str:
177
+ """Transcribe voice file using SpeechRecognition."""
178
+ try:
179
+ import speech_recognition as sr
180
+ from pydub import AudioSegment
181
+
182
+ # Convert OGG to WAV
183
+ wav_path = audio_path.with_suffix('.wav')
184
+ sound = AudioSegment.from_ogg(str(audio_path))
185
+ sound.export(str(wav_path), format="wav")
186
+
187
+ r = sr.Recognizer()
188
+ with sr.AudioFile(str(wav_path)) as source:
189
+ audio = r.record(source)
190
+ text = r.recognize_google(audio)
191
+ return text
192
+ except Exception as e:
193
+ logger.error(f"Transcription failed: {e}")
194
+ return ""
195
+
196
+ async def handle_command(self, command: dict) -> dict:
197
+ cmd_type = command.get("type")
198
+ message_id = command.get("message_id")
199
+ result = {"message_id": message_id, "success": False}
200
+
201
+ try:
202
+ if cmd_type == "screenshot":
203
+ path = take_screenshot()
204
+ if path:
205
+ with open(path, "rb") as f:
206
+ result["image"] = base64.b64encode(f.read()).decode()
207
+ cleanup_screenshot(path)
208
+ result["success"] = True
209
+
210
+ elif cmd_type == "relay":
211
+ text = sanitize_input(command.get("text", ""))
212
+ result["success"] = send_to_antigravity(text)
213
+
214
+ elif cmd_type == "photo":
215
+ try:
216
+ data = base64.b64decode(command.get("data", ""))
217
+ filename = f"photo_{int(time.time())}.jpg"
218
+ path = self.downloads_dir / filename
219
+ path.write_bytes(data)
220
+
221
+ # Tell Agent
222
+ send_to_antigravity(f"I uploaded a photo here: {path}")
223
+ result["success"] = True
224
+ except Exception as e:
225
+ result["error"] = str(e)
226
+
227
+ elif cmd_type == "voice":
228
+ try:
229
+ data = base64.b64decode(command.get("data", ""))
230
+ filename = f"voice_{int(time.time())}.ogg"
231
+ path = self.downloads_dir / filename
232
+ path.write_bytes(data)
233
+
234
+ # Try transcribe
235
+ text = self.process_voice(path)
236
+ if text:
237
+ send_to_antigravity(f"(Voice Command): {text}")
238
+ result["text"] = text
239
+ else:
240
+ send_to_antigravity(f"I sent a voice note here: {path}")
241
+ result["text"] = "Audio saved (transcription failed)"
242
+
243
+ result["success"] = True
244
+ except Exception as e:
245
+ result["error"] = str(e)
246
+
247
+ elif cmd_type == "file":
248
+ try:
249
+ data = base64.b64decode(command.get("data", ""))
250
+ name = sanitize_input(command.get("name", "file"), 100)
251
+ path = Path.cwd() / name # Save to CWD
252
+ path.write_bytes(data)
253
+
254
+ send_to_antigravity(f"I saved a file: {path.absolute()}")
255
+ result["path"] = str(path.absolute())
256
+ result["success"] = True
257
+ except Exception as e:
258
+ result["error"] = str(e)
259
+
260
+ elif cmd_type == "scroll":
261
+ direction = command.get("direction", "down")
262
+ clicks = {"up": 25, "down": -25, "top": 500, "bottom": -500}.get(direction, -25)
263
+ result["success"] = scroll_screen(clicks)
264
+
265
+ elif cmd_type == "key":
266
+ combo = sanitize_input(command.get("combo", ""), 50).split("+")
267
+ result["success"] = send_key_combo(combo)
268
+
269
+ elif cmd_type == "accept":
270
+ import pyautogui
271
+ focus_antigravity()
272
+ time.sleep(0.2)
273
+ pyautogui.hotkey('alt', 'enter')
274
+ result["success"] = True
275
+
276
+ elif cmd_type == "reject":
277
+ import pyautogui
278
+ focus_antigravity()
279
+ time.sleep(0.2)
280
+ pyautogui.press('escape')
281
+ result["success"] = True
282
+
283
+ elif cmd_type == "undo":
284
+ import pyautogui
285
+ focus_antigravity()
286
+ pyautogui.hotkey('ctrl', 'z')
287
+ result["success"] = True
288
+
289
+ elif cmd_type == "cancel":
290
+ import pyautogui
291
+ focus_antigravity()
292
+ pyautogui.press('escape')
293
+ result["success"] = True
294
+
295
+ elif cmd_type == "model":
296
+ import pyautogui
297
+ model = sanitize_input(command.get("model", ""), 100)
298
+ focus_antigravity()
299
+ time.sleep(0.5)
300
+
301
+ # Strategy 1: Ctrl + / (Common Cursor shortcut)
302
+ pyautogui.hotkey('ctrl', '/')
303
+ time.sleep(0.5)
304
+ pyautogui.write(model, interval=0.05)
305
+ time.sleep(0.5)
306
+ pyautogui.press('enter')
307
+
308
+ # Strategy 2: Just tell the Agent!
309
+ time.sleep(0.5)
310
+ send_to_antigravity(f"Please switch model to {model}")
311
+ result["success"] = True
312
+
313
+ elif cmd_type == "watchdog":
314
+ enabled = command.get("enabled", False)
315
+ self.watchdog_enabled = enabled
316
+ if enabled and not self.watchdog_task:
317
+ self.watchdog_task = asyncio.create_task(self.run_watchdog())
318
+ elif not enabled and self.watchdog_task:
319
+ self.watchdog_task.cancel()
320
+ self.watchdog_task = None
321
+ result["success"] = True
322
+
323
+ elif cmd_type == "sysinfo":
324
+ import psutil
325
+ cpu = psutil.cpu_percent(interval=1)
326
+ mem = psutil.virtual_memory()
327
+ result["info"] = f"CPU: {cpu}%\nRAM: {mem.percent}%"
328
+ result["success"] = True
329
+
330
+ elif cmd_type == "files":
331
+ workspace = os.getcwd()
332
+ items = os.listdir(workspace)[:20]
333
+ result["files"] = "\n".join(f"📄 {i}" for i in items)
334
+ result["success"] = True
335
+
336
+ else:
337
+ logger.warning(f"Unknown command: {cmd_type}")
338
+
339
+ except Exception as e:
340
+ logger.error(f"Command error: {e}")
341
+ result["error"] = "Command failed"
342
+
343
+ return result
344
+
345
+ async def run(self):
346
+ self.running = True
347
+ reconnect_delay = 5
348
+
349
+ while self.running:
350
+ try:
351
+ if not await self.connect():
352
+ logger.info(f"Retrying in {reconnect_delay}s...")
353
+ await asyncio.sleep(reconnect_delay)
354
+ continue
355
+
356
+ reconnect_delay = 5
357
+
358
+ async for message in self.websocket:
359
+ command = json.loads(message)
360
+ logger.info(f"📥 Received: {command.get('type')}")
361
+ result = await self.handle_command(command)
362
+ await self.websocket.send(json.dumps(result))
363
+
364
+ except websockets.exceptions.ConnectionClosed:
365
+ logger.warning("Connection closed. Reconnecting...")
366
+ await asyncio.sleep(reconnect_delay)
367
+ reconnect_delay = min(reconnect_delay * 2, 60)
368
+ except Exception as e:
369
+ logger.error(f"Error: {e}")
370
+ await asyncio.sleep(reconnect_delay)
371
+
372
+ def stop(self):
373
+ self.running = False
374
+ self.watchdog_enabled = False
375
+ if self.websocket:
376
+ asyncio.create_task(self.websocket.close())
377
+
378
+
379
+ async def run_agent(user_id: str, auth_token: str, server_url: str = None):
380
+ """Run the secure local agent."""
381
+ agent = LocalAgent(user_id, auth_token, server_url)
382
+ await agent.run()
@@ -0,0 +1,168 @@
1
+ """Main bot class for Antigravity Remote."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import sys
6
+
7
+ from telegram import Update
8
+ from telegram.ext import (
9
+ ApplicationBuilder,
10
+ CommandHandler,
11
+ CallbackQueryHandler,
12
+ MessageHandler,
13
+ filters,
14
+ )
15
+
16
+ from .config import config
17
+ from .state import state
18
+ from .handlers import (
19
+ # Control
20
+ start_command,
21
+ pause_command,
22
+ resume_command,
23
+ cancel_command,
24
+ key_command,
25
+ lock_command,
26
+ unlock_command,
27
+ # Screen
28
+ status_command,
29
+ scroll_command,
30
+ accept_command,
31
+ reject_command,
32
+ undo_command,
33
+ # Files
34
+ sysinfo_command,
35
+ files_command,
36
+ read_command,
37
+ diff_command,
38
+ log_command,
39
+ # Monitoring
40
+ heartbeat_command,
41
+ watchdog_command,
42
+ schedule_command,
43
+ # AI
44
+ model_command,
45
+ summary_command,
46
+ handle_message,
47
+ handle_model_callback,
48
+ # Quick
49
+ quick_replies_command,
50
+ handle_quick_callback,
51
+ handle_voice,
52
+ )
53
+ from .utils import take_screenshot, cleanup_screenshot
54
+ from .handlers.base import is_authorized
55
+
56
+ logger = logging.getLogger(__name__)
57
+
58
+
59
+ class AntigravityBot:
60
+ """Main Antigravity Remote Control bot."""
61
+
62
+ def __init__(self):
63
+ """Initialize the bot using embedded token and registered user."""
64
+ self.token = config.bot_token
65
+ self.user_id = config.allowed_user_id
66
+ self.application = None
67
+
68
+ def validate(self) -> bool:
69
+ """Validate configuration before starting."""
70
+ errors = config.validate()
71
+
72
+ if errors:
73
+ for error in errors:
74
+ logger.error(f"Configuration error: {error}")
75
+ return False
76
+
77
+ return True
78
+
79
+ async def button_handler(self, update: Update, context) -> None:
80
+ """Handle all callback button presses."""
81
+ query = update.callback_query
82
+ await query.answer()
83
+
84
+ if not await is_authorized(update):
85
+ return
86
+
87
+ data = query.data
88
+
89
+ if data == "screenshot":
90
+ await query.message.reply_text("📸 Capturing...")
91
+ path = await asyncio.to_thread(take_screenshot)
92
+ if path:
93
+ await context.bot.send_photo(
94
+ chat_id=update.effective_chat.id,
95
+ photo=open(path, 'rb')
96
+ )
97
+ cleanup_screenshot(path)
98
+
99
+ elif data.startswith("model_"):
100
+ model_id = data.replace("model_", "")
101
+ await handle_model_callback(query, context, model_id)
102
+
103
+ elif data.startswith("quick_"):
104
+ action = data.replace("quick_", "")
105
+ await handle_quick_callback(query, context, action)
106
+
107
+ def setup_handlers(self) -> None:
108
+ """Register all command and message handlers."""
109
+ app = self.application
110
+
111
+ # Command handlers
112
+ handlers = [
113
+ ("start", start_command),
114
+ ("status", status_command),
115
+ ("pause", pause_command),
116
+ ("resume", resume_command),
117
+ ("cancel", cancel_command),
118
+ ("scroll", scroll_command),
119
+ ("accept", accept_command),
120
+ ("reject", reject_command),
121
+ ("undo", undo_command),
122
+ ("sysinfo", sysinfo_command),
123
+ ("files", files_command),
124
+ ("read", read_command),
125
+ ("diff", diff_command),
126
+ ("log", log_command),
127
+ ("lock", lock_command),
128
+ ("unlock", unlock_command),
129
+ ("heartbeat", heartbeat_command),
130
+ ("key", key_command),
131
+ ("schedule", schedule_command),
132
+ ("watchdog", watchdog_command),
133
+ ("model", model_command),
134
+ ("quick", quick_replies_command),
135
+ ("summary", summary_command),
136
+ ]
137
+
138
+ for command, handler in handlers:
139
+ app.add_handler(CommandHandler(command, handler))
140
+
141
+ # Callback handlers
142
+ app.add_handler(CallbackQueryHandler(self.button_handler))
143
+
144
+ # Message handlers
145
+ app.add_handler(MessageHandler(filters.VOICE, handle_voice))
146
+ app.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), handle_message))
147
+
148
+ def run(self) -> None:
149
+ """Start the bot."""
150
+ if not self.validate():
151
+ logger.error("Configuration validation failed")
152
+ sys.exit(1)
153
+
154
+ self.application = ApplicationBuilder().token(self.token).build()
155
+ self.setup_handlers()
156
+
157
+ print("🚀 Antigravity Remote Control")
158
+ print(f" User: {self.user_id}")
159
+ print(f" Workspace: {config.workspace_path}")
160
+ print()
161
+
162
+ self.application.run_polling()
163
+
164
+ def stop(self) -> None:
165
+ """Stop the bot and cleanup."""
166
+ state.cancel_tasks()
167
+ if self.application:
168
+ self.application.stop()