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,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()
|