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,199 @@
|
|
|
1
|
+
"""Monitoring command handlers for Antigravity Remote."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
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
|
+
from ..state import state
|
|
15
|
+
from ..utils import take_screenshot, cleanup_screenshot, scan_screen, detect_keywords
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def heartbeat_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
21
|
+
"""Start/stop heartbeat screenshots."""
|
|
22
|
+
if not await is_authorized(update):
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
args = context.args
|
|
26
|
+
|
|
27
|
+
# Cancel existing heartbeat
|
|
28
|
+
if state.heartbeat_task:
|
|
29
|
+
state.heartbeat_task.cancel()
|
|
30
|
+
state.heartbeat_task = None
|
|
31
|
+
|
|
32
|
+
if not args or args[0] == "off":
|
|
33
|
+
await update.message.reply_text("💓 Heartbeat stopped.")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
minutes = int(args[0])
|
|
38
|
+
except ValueError:
|
|
39
|
+
await update.message.reply_text("Usage: /heartbeat <minutes> or /heartbeat off")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
async def heartbeat_loop():
|
|
43
|
+
while True:
|
|
44
|
+
await asyncio.sleep(minutes * 60)
|
|
45
|
+
path = await asyncio.to_thread(take_screenshot)
|
|
46
|
+
if path:
|
|
47
|
+
await context.bot.send_photo(
|
|
48
|
+
chat_id=update.effective_chat.id,
|
|
49
|
+
photo=open(path, 'rb'),
|
|
50
|
+
caption=f"💓 Heartbeat - {datetime.now().strftime('%H:%M')}"
|
|
51
|
+
)
|
|
52
|
+
cleanup_screenshot(path)
|
|
53
|
+
|
|
54
|
+
state.heartbeat_task = asyncio.create_task(heartbeat_loop())
|
|
55
|
+
await update.message.reply_text(f"💓 Heartbeat started! Screenshot every {minutes} minutes.")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def watchdog_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
59
|
+
"""Start/stop smart watchdog monitoring."""
|
|
60
|
+
if not await is_authorized(update):
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
args = context.args
|
|
64
|
+
|
|
65
|
+
# Cancel existing watchdog
|
|
66
|
+
if state.watchdog_task:
|
|
67
|
+
state.watchdog_task.cancel()
|
|
68
|
+
state.watchdog_task = None
|
|
69
|
+
|
|
70
|
+
if args and args[0] == "off":
|
|
71
|
+
await update.message.reply_text("🐕 Watchdog stopped.")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
check_interval = config.watchdog_interval
|
|
75
|
+
alert_cooldown = config.alert_cooldown
|
|
76
|
+
|
|
77
|
+
async def watchdog_loop():
|
|
78
|
+
while True:
|
|
79
|
+
await asyncio.sleep(check_interval)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
screen_text, current_hash = await asyncio.to_thread(scan_screen)
|
|
83
|
+
current_time = time.time()
|
|
84
|
+
|
|
85
|
+
# Activity monitoring
|
|
86
|
+
if current_hash == state.watchdog_last_hash:
|
|
87
|
+
state.watchdog_idle_count += 1
|
|
88
|
+
else:
|
|
89
|
+
state.watchdog_idle_count = 0
|
|
90
|
+
state.watchdog_last_hash = current_hash
|
|
91
|
+
|
|
92
|
+
# Check for keywords
|
|
93
|
+
detection = detect_keywords(screen_text)
|
|
94
|
+
|
|
95
|
+
if detection and current_time - state.watchdog_last_alert > alert_cooldown:
|
|
96
|
+
category, keyword = detection
|
|
97
|
+
state.watchdog_last_alert = current_time
|
|
98
|
+
|
|
99
|
+
path = await asyncio.to_thread(take_screenshot)
|
|
100
|
+
if path:
|
|
101
|
+
captions = {
|
|
102
|
+
'approval': f"🚨 *Approval needed!*\nDetected: `{keyword}`",
|
|
103
|
+
'done': f"✅ *Task appears complete!*\nDetected: `{keyword}`",
|
|
104
|
+
'error': f"⚠️ *Error detected!*\nDetected: `{keyword}`",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await context.bot.send_photo(
|
|
108
|
+
chat_id=update.effective_chat.id,
|
|
109
|
+
photo=open(path, 'rb'),
|
|
110
|
+
caption=captions.get(category, f"Detected: `{keyword}`"),
|
|
111
|
+
parse_mode=ParseMode.MARKDOWN
|
|
112
|
+
)
|
|
113
|
+
cleanup_screenshot(path)
|
|
114
|
+
|
|
115
|
+
# Idle detection (2+ cycles with no change)
|
|
116
|
+
if (state.watchdog_idle_count >= 2 and
|
|
117
|
+
current_time - state.watchdog_last_alert > 60):
|
|
118
|
+
|
|
119
|
+
state.watchdog_last_alert = current_time
|
|
120
|
+
state.watchdog_idle_count = 0
|
|
121
|
+
|
|
122
|
+
path = await asyncio.to_thread(take_screenshot)
|
|
123
|
+
if path:
|
|
124
|
+
await context.bot.send_photo(
|
|
125
|
+
chat_id=update.effective_chat.id,
|
|
126
|
+
photo=open(path, 'rb'),
|
|
127
|
+
caption="💤 *Screen idle* - No activity detected",
|
|
128
|
+
parse_mode=ParseMode.MARKDOWN
|
|
129
|
+
)
|
|
130
|
+
cleanup_screenshot(path)
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error(f"Watchdog error: {e}")
|
|
134
|
+
|
|
135
|
+
state.watchdog_task = asyncio.create_task(watchdog_loop())
|
|
136
|
+
|
|
137
|
+
await update.message.reply_text(
|
|
138
|
+
"🐕 *Watchdog started!*\n\n"
|
|
139
|
+
"I'll alert you when:\n"
|
|
140
|
+
"• 🚨 Approval is needed\n"
|
|
141
|
+
"• ✅ Task appears complete\n"
|
|
142
|
+
"• ⚠️ Errors are detected\n"
|
|
143
|
+
"• 💤 Screen goes idle\n\n"
|
|
144
|
+
"Use `/watchdog off` to stop.",
|
|
145
|
+
parse_mode=ParseMode.MARKDOWN
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
150
|
+
"""Schedule a command for later."""
|
|
151
|
+
if not await is_authorized(update):
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
args = context.args
|
|
155
|
+
if len(args) < 2:
|
|
156
|
+
await update.message.reply_text("Usage: /schedule 5m /status\nTime: 30s, 5m, 1h")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
time_str = args[0].lower()
|
|
160
|
+
scheduled_cmd = ' '.join(args[1:])
|
|
161
|
+
|
|
162
|
+
# Parse time
|
|
163
|
+
try:
|
|
164
|
+
if time_str.endswith('s'):
|
|
165
|
+
seconds = int(time_str[:-1])
|
|
166
|
+
elif time_str.endswith('m'):
|
|
167
|
+
seconds = int(time_str[:-1]) * 60
|
|
168
|
+
elif time_str.endswith('h'):
|
|
169
|
+
seconds = int(time_str[:-1]) * 3600
|
|
170
|
+
else:
|
|
171
|
+
seconds = int(time_str)
|
|
172
|
+
except ValueError:
|
|
173
|
+
await update.message.reply_text("Invalid time format. Use: 30s, 5m, 1h")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
await update.message.reply_text(
|
|
177
|
+
f"⏰ Scheduled `{scheduled_cmd}` in {time_str}",
|
|
178
|
+
parse_mode=ParseMode.MARKDOWN
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
async def run_scheduled():
|
|
182
|
+
await asyncio.sleep(seconds)
|
|
183
|
+
|
|
184
|
+
if 'status' in scheduled_cmd.lower() or 'screenshot' in scheduled_cmd.lower():
|
|
185
|
+
path = await asyncio.to_thread(take_screenshot)
|
|
186
|
+
if path:
|
|
187
|
+
await context.bot.send_photo(
|
|
188
|
+
chat_id=update.effective_chat.id,
|
|
189
|
+
photo=open(path, 'rb'),
|
|
190
|
+
caption="⏰ Scheduled screenshot"
|
|
191
|
+
)
|
|
192
|
+
cleanup_screenshot(path)
|
|
193
|
+
else:
|
|
194
|
+
await context.bot.send_message(
|
|
195
|
+
chat_id=update.effective_chat.id,
|
|
196
|
+
text=f"⏰ Timer complete for: {scheduled_cmd}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
asyncio.create_task(run_scheduled())
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Quick command handlers for Antigravity Remote."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
|
9
|
+
from telegram.ext import ContextTypes
|
|
10
|
+
from telegram.constants import ParseMode
|
|
11
|
+
|
|
12
|
+
from .base import is_authorized
|
|
13
|
+
from ..utils import send_to_antigravity, take_screenshot, cleanup_screenshot
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Quick reply options
|
|
18
|
+
QUICK_REPLIES = [
|
|
19
|
+
("✅ Yes", "quick_yes"),
|
|
20
|
+
("❌ No", "quick_no"),
|
|
21
|
+
("▶️ Proceed", "quick_proceed"),
|
|
22
|
+
("⏹️ Cancel", "quick_cancel"),
|
|
23
|
+
("👍 Approve", "quick_approve"),
|
|
24
|
+
("⏭️ Skip", "quick_skip"),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
QUICK_TEXTS = {
|
|
28
|
+
"quick_yes": "Yes",
|
|
29
|
+
"quick_no": "No",
|
|
30
|
+
"quick_proceed": "Proceed",
|
|
31
|
+
"quick_cancel": "Cancel",
|
|
32
|
+
"quick_approve": "Approve",
|
|
33
|
+
"quick_skip": "Skip",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def quick_replies_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
38
|
+
"""Show quick reply buttons."""
|
|
39
|
+
if not await is_authorized(update):
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
keyboard = [
|
|
43
|
+
[InlineKeyboardButton("✅ Yes", callback_data="quick_yes"),
|
|
44
|
+
InlineKeyboardButton("❌ No", callback_data="quick_no")],
|
|
45
|
+
[InlineKeyboardButton("▶️ Proceed", callback_data="quick_proceed"),
|
|
46
|
+
InlineKeyboardButton("⏹️ Cancel", callback_data="quick_cancel")],
|
|
47
|
+
[InlineKeyboardButton("👍 Approve", callback_data="quick_approve"),
|
|
48
|
+
InlineKeyboardButton("⏭️ Skip", callback_data="quick_skip")],
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
await update.message.reply_text(
|
|
52
|
+
"⚡ *Quick Replies* - Tap to send:",
|
|
53
|
+
reply_markup=InlineKeyboardMarkup(keyboard),
|
|
54
|
+
parse_mode=ParseMode.MARKDOWN
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def handle_quick_callback(
|
|
59
|
+
query,
|
|
60
|
+
context: ContextTypes.DEFAULT_TYPE,
|
|
61
|
+
action: str
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Handle quick reply callback."""
|
|
64
|
+
text = QUICK_TEXTS.get(action, action.capitalize())
|
|
65
|
+
success = await asyncio.to_thread(send_to_antigravity, text)
|
|
66
|
+
|
|
67
|
+
if success:
|
|
68
|
+
await query.message.reply_text(f"📤 Sent: *{text}*", parse_mode=ParseMode.MARKDOWN)
|
|
69
|
+
else:
|
|
70
|
+
await query.message.reply_text("❌ Failed to send")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
74
|
+
"""Handle voice messages - download, transcribe, and relay."""
|
|
75
|
+
if not await is_authorized(update):
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
voice = update.message.voice
|
|
79
|
+
status_msg = await update.message.reply_text("🎤 Processing voice message...")
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Download voice file
|
|
83
|
+
file = await context.bot.get_file(voice.file_id)
|
|
84
|
+
voice_path = os.path.join(tempfile.gettempdir(), f"voice_{voice.file_id}.ogg")
|
|
85
|
+
await file.download_to_drive(voice_path)
|
|
86
|
+
|
|
87
|
+
# Try to transcribe with speech recognition
|
|
88
|
+
try:
|
|
89
|
+
import speech_recognition as sr
|
|
90
|
+
recognizer = sr.Recognizer()
|
|
91
|
+
|
|
92
|
+
# Convert ogg to wav
|
|
93
|
+
wav_path = voice_path.replace('.ogg', '.wav')
|
|
94
|
+
os.system(f'ffmpeg -i "{voice_path}" -y "{wav_path}" 2>nul')
|
|
95
|
+
|
|
96
|
+
with sr.AudioFile(wav_path) as source:
|
|
97
|
+
audio = recognizer.record(source)
|
|
98
|
+
text = recognizer.recognize_google(audio)
|
|
99
|
+
|
|
100
|
+
# Cleanup
|
|
101
|
+
if os.path.exists(wav_path):
|
|
102
|
+
os.remove(wav_path)
|
|
103
|
+
if os.path.exists(voice_path):
|
|
104
|
+
os.remove(voice_path)
|
|
105
|
+
|
|
106
|
+
# Relay transcribed text
|
|
107
|
+
await status_msg.edit_text(
|
|
108
|
+
f"🎤 Transcribed: *{text}*\n\nSending...",
|
|
109
|
+
parse_mode=ParseMode.MARKDOWN
|
|
110
|
+
)
|
|
111
|
+
success = await asyncio.to_thread(send_to_antigravity, text)
|
|
112
|
+
|
|
113
|
+
if success:
|
|
114
|
+
keyboard = [[InlineKeyboardButton("📸 Get Result", callback_data="screenshot")]]
|
|
115
|
+
await status_msg.edit_text(
|
|
116
|
+
f"✅ *Voice relayed:*\n`{text}`",
|
|
117
|
+
reply_markup=InlineKeyboardMarkup(keyboard),
|
|
118
|
+
parse_mode=ParseMode.MARKDOWN
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
await status_msg.edit_text("❌ Failed to relay voice message")
|
|
122
|
+
|
|
123
|
+
except ImportError:
|
|
124
|
+
await status_msg.edit_text(
|
|
125
|
+
"⚠️ Voice transcription not available.\n"
|
|
126
|
+
"Install: `pip install SpeechRecognition`",
|
|
127
|
+
parse_mode=ParseMode.MARKDOWN
|
|
128
|
+
)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
await status_msg.edit_text(f"⚠️ Transcription failed: {e}")
|
|
131
|
+
if os.path.exists(voice_path):
|
|
132
|
+
os.remove(voice_path)
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
await status_msg.edit_text(f"❌ Error processing voice: {e}")
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Screen 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 ..utils import focus_antigravity, take_screenshot, cleanup_screenshot, scroll_screen
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
20
|
+
"""Take and send a screenshot."""
|
|
21
|
+
if not await is_authorized(update):
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
msg = await update.message.reply_text("📸 Capturing...")
|
|
25
|
+
path = await asyncio.to_thread(take_screenshot)
|
|
26
|
+
|
|
27
|
+
if path:
|
|
28
|
+
await context.bot.send_photo(
|
|
29
|
+
chat_id=update.effective_chat.id,
|
|
30
|
+
photo=open(path, 'rb'),
|
|
31
|
+
caption="🖥️ Current screen"
|
|
32
|
+
)
|
|
33
|
+
cleanup_screenshot(path)
|
|
34
|
+
else:
|
|
35
|
+
await update.message.reply_text("❌ Failed to capture screenshot")
|
|
36
|
+
|
|
37
|
+
await msg.delete()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def scroll_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
41
|
+
"""Scroll the screen."""
|
|
42
|
+
if not await is_authorized(update):
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
args = context.args
|
|
46
|
+
direction = "down"
|
|
47
|
+
multiplier = 1
|
|
48
|
+
|
|
49
|
+
# Parse args: /scroll up x50 or /scroll down 10 or /scroll bottom
|
|
50
|
+
for arg in args:
|
|
51
|
+
if arg == "bottom":
|
|
52
|
+
direction = "down"
|
|
53
|
+
multiplier = 100
|
|
54
|
+
elif arg == "top":
|
|
55
|
+
direction = "up"
|
|
56
|
+
multiplier = 100
|
|
57
|
+
elif arg in ["up", "down"]:
|
|
58
|
+
direction = arg
|
|
59
|
+
elif arg.startswith("x") and arg[1:].isdigit():
|
|
60
|
+
multiplier = int(arg[1:])
|
|
61
|
+
elif arg.isdigit():
|
|
62
|
+
multiplier = int(arg)
|
|
63
|
+
|
|
64
|
+
# Calculate scroll amount
|
|
65
|
+
base_clicks = 25
|
|
66
|
+
clicks = base_clicks * multiplier
|
|
67
|
+
if direction == "down":
|
|
68
|
+
clicks = -clicks
|
|
69
|
+
|
|
70
|
+
success = await asyncio.to_thread(scroll_screen, clicks)
|
|
71
|
+
|
|
72
|
+
if success:
|
|
73
|
+
await update.message.reply_text(f"📜 Scrolled {direction} x{multiplier}")
|
|
74
|
+
else:
|
|
75
|
+
await update.message.reply_text("❌ Failed to scroll")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def accept_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
79
|
+
"""Send Accept (Alt+Enter)."""
|
|
80
|
+
if not await is_authorized(update):
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
await asyncio.to_thread(
|
|
84
|
+
lambda: (focus_antigravity(), time.sleep(0.2), pyautogui.hotkey('alt', 'enter'))
|
|
85
|
+
)
|
|
86
|
+
await update.message.reply_text("✅ Sent Accept (Alt+Enter)")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def reject_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
90
|
+
"""Send Reject (Escape)."""
|
|
91
|
+
if not await is_authorized(update):
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
await asyncio.to_thread(
|
|
95
|
+
lambda: (focus_antigravity(), time.sleep(0.2), pyautogui.press('escape'))
|
|
96
|
+
)
|
|
97
|
+
await update.message.reply_text("❌ Sent Reject (Escape)")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def undo_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
101
|
+
"""Send Undo (Ctrl+Z)."""
|
|
102
|
+
if not await is_authorized(update):
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
await asyncio.to_thread(
|
|
106
|
+
lambda: (focus_antigravity(), pyautogui.hotkey('ctrl', 'z'))
|
|
107
|
+
)
|
|
108
|
+
await update.message.reply_text("↩️ Sent Undo (Ctrl+Z)")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""State management for Antigravity Remote bot."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class CommandLogEntry:
|
|
11
|
+
"""A single command log entry."""
|
|
12
|
+
timestamp: datetime
|
|
13
|
+
message: str
|
|
14
|
+
|
|
15
|
+
def to_dict(self) -> dict[str, Any]:
|
|
16
|
+
return {
|
|
17
|
+
"time": self.timestamp.strftime("%H:%M:%S"),
|
|
18
|
+
"msg": self.message
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class BotState:
|
|
24
|
+
"""Mutable state for the Antigravity Remote bot."""
|
|
25
|
+
|
|
26
|
+
# Control state
|
|
27
|
+
paused: bool = False
|
|
28
|
+
locked: bool = False
|
|
29
|
+
|
|
30
|
+
# Background tasks
|
|
31
|
+
heartbeat_task: Optional[asyncio.Task] = None
|
|
32
|
+
watchdog_task: Optional[asyncio.Task] = None
|
|
33
|
+
|
|
34
|
+
# Watchdog state
|
|
35
|
+
watchdog_last_alert: float = 0.0
|
|
36
|
+
watchdog_last_hash: Optional[int] = None
|
|
37
|
+
watchdog_idle_count: int = 0
|
|
38
|
+
|
|
39
|
+
# Command history
|
|
40
|
+
command_log: list[CommandLogEntry] = field(default_factory=list)
|
|
41
|
+
max_log_entries: int = 100
|
|
42
|
+
|
|
43
|
+
def log_command(self, message: str) -> None:
|
|
44
|
+
"""Add a command to the log."""
|
|
45
|
+
self.command_log.append(
|
|
46
|
+
CommandLogEntry(timestamp=datetime.now(), message=message)
|
|
47
|
+
)
|
|
48
|
+
# Trim log if too long
|
|
49
|
+
if len(self.command_log) > self.max_log_entries:
|
|
50
|
+
self.command_log = self.command_log[-50:]
|
|
51
|
+
|
|
52
|
+
def get_recent_logs(self, count: int = 10) -> list[CommandLogEntry]:
|
|
53
|
+
"""Get the most recent log entries."""
|
|
54
|
+
return self.command_log[-count:]
|
|
55
|
+
|
|
56
|
+
def cancel_tasks(self) -> None:
|
|
57
|
+
"""Cancel all background tasks."""
|
|
58
|
+
if self.heartbeat_task:
|
|
59
|
+
self.heartbeat_task.cancel()
|
|
60
|
+
self.heartbeat_task = None
|
|
61
|
+
if self.watchdog_task:
|
|
62
|
+
self.watchdog_task.cancel()
|
|
63
|
+
self.watchdog_task = None
|
|
64
|
+
|
|
65
|
+
def reset(self) -> None:
|
|
66
|
+
"""Reset state to defaults."""
|
|
67
|
+
self.paused = False
|
|
68
|
+
self.locked = False
|
|
69
|
+
self.cancel_tasks()
|
|
70
|
+
self.watchdog_last_alert = 0.0
|
|
71
|
+
self.watchdog_last_hash = None
|
|
72
|
+
self.watchdog_idle_count = 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Global state instance
|
|
76
|
+
state = BotState()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Utility modules for Antigravity Remote."""
|
|
2
|
+
|
|
3
|
+
from .automation import focus_antigravity, send_to_antigravity, send_key_combo, scroll_screen
|
|
4
|
+
from .screenshot import take_screenshot, cleanup_screenshot
|
|
5
|
+
from .ocr import scan_screen, detect_keywords
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"focus_antigravity",
|
|
9
|
+
"send_to_antigravity",
|
|
10
|
+
"send_key_combo",
|
|
11
|
+
"scroll_screen",
|
|
12
|
+
"take_screenshot",
|
|
13
|
+
"cleanup_screenshot",
|
|
14
|
+
"scan_screen",
|
|
15
|
+
"detect_keywords",
|
|
16
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Window automation utilities for Antigravity Remote."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import pyautogui
|
|
8
|
+
import pygetwindow as gw
|
|
9
|
+
import pyperclip
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Configure pyautogui safety settings
|
|
14
|
+
pyautogui.FAILSAFE = True
|
|
15
|
+
pyautogui.PAUSE = 0.1
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def focus_antigravity() -> bool:
|
|
19
|
+
"""
|
|
20
|
+
Focus the Antigravity/VS Code/Cursor window.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
True if window was focused, False otherwise.
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
# Try different window titles in priority order
|
|
27
|
+
window_titles = ['Antigravity IDE', 'Antigravity', 'Visual Studio Code', 'Cursor']
|
|
28
|
+
|
|
29
|
+
for title in window_titles:
|
|
30
|
+
windows = gw.getWindowsWithTitle(title)
|
|
31
|
+
if windows:
|
|
32
|
+
win = windows[0]
|
|
33
|
+
if win.isMinimized:
|
|
34
|
+
win.restore()
|
|
35
|
+
win.activate()
|
|
36
|
+
time.sleep(0.3)
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
logger.warning("No Antigravity/VS Code/Cursor window found")
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
except Exception as e:
|
|
43
|
+
logger.error(f"Error focusing window: {e}")
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def send_to_antigravity(message: str) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Send a message to the Antigravity chat input.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
message: The message to send.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
True if message was sent, False otherwise.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
if not focus_antigravity():
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
time.sleep(0.3)
|
|
62
|
+
screen_width, screen_height = pyautogui.size()
|
|
63
|
+
|
|
64
|
+
# Click in the chat input area (right side, near bottom)
|
|
65
|
+
chat_input_x = int(screen_width * 0.75)
|
|
66
|
+
chat_input_y = int(screen_height * 0.92)
|
|
67
|
+
|
|
68
|
+
pyautogui.click(chat_input_x, chat_input_y)
|
|
69
|
+
time.sleep(0.3)
|
|
70
|
+
|
|
71
|
+
# Select all and paste new message
|
|
72
|
+
pyautogui.hotkey('ctrl', 'a')
|
|
73
|
+
time.sleep(0.1)
|
|
74
|
+
|
|
75
|
+
pyperclip.copy(message)
|
|
76
|
+
pyautogui.hotkey('ctrl', 'v')
|
|
77
|
+
time.sleep(0.2)
|
|
78
|
+
|
|
79
|
+
# Send message
|
|
80
|
+
pyautogui.press('enter')
|
|
81
|
+
|
|
82
|
+
logger.info(f"Sent message to Antigravity: {message[:50]}...")
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.error(f"Error sending to Antigravity: {e}")
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def send_key_combo(keys: list[str]) -> bool:
|
|
91
|
+
"""
|
|
92
|
+
Send a keyboard shortcut.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
keys: List of keys to press together (e.g., ['ctrl', 's']).
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
True if keys were sent, False otherwise.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
if not focus_antigravity():
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
time.sleep(0.2)
|
|
105
|
+
pyautogui.hotkey(*keys)
|
|
106
|
+
logger.info(f"Sent key combo: {'+'.join(keys)}")
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Error sending key combo: {e}")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def scroll_screen(clicks: int, x_percent: float = 0.80, y_percent: float = 0.40) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Scroll the screen at specified position.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
clicks: Number of scroll clicks (positive = up, negative = down).
|
|
120
|
+
x_percent: Horizontal position as percentage of screen width.
|
|
121
|
+
y_percent: Vertical position as percentage of screen height.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True if scroll was performed, False otherwise.
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
if not focus_antigravity():
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
screen_width, screen_height = pyautogui.size()
|
|
131
|
+
x = int(screen_width * x_percent)
|
|
132
|
+
y = int(screen_height * y_percent)
|
|
133
|
+
|
|
134
|
+
pyautogui.moveTo(x, y)
|
|
135
|
+
time.sleep(0.1)
|
|
136
|
+
pyautogui.scroll(clicks)
|
|
137
|
+
|
|
138
|
+
logger.info(f"Scrolled {clicks} clicks at ({x}, {y})")
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error(f"Error scrolling: {e}")
|
|
143
|
+
return False
|