refactor: enforce device labels, unify screenshot, remove deprecated commands, session-id-less design
- Device labels: lowercase, no whitespace, only a-z 0-9 - _ (enforced at config time) - Session IDs removed: device label is the sole identifier - Routes changed: /sessions/:id → /devices/:label - Removed commands: click, type, find-window, wait-for-window, label, old version, server-version - Renamed: status → version (compares relay/remote.py/client commits) - Unified screenshot: takes 'screen' or a window label as argument - Windows listed with human-readable labels (same format as device labels) - Single instance enforcement via PID lock file - Removed input.rs (click/type functionality) - All docs and code in English - Protocol: Hello.label is now required (String, not Option<String>) - Client auto-migrates invalid labels on startup
This commit is contained in:
parent
5fd01a423d
commit
0b4a6de8ae
14 changed files with 736 additions and 1180 deletions
535
remote.py
535
remote.py
|
|
@ -1,12 +1,15 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
helios-remote - CLI to control PCs connected to the Helios Remote Relay Server.
|
||||
Config is loaded from config.env in the same directory as this script.
|
||||
helios-remote CLI - Control devices connected to the Helios Remote Relay Server.
|
||||
|
||||
Device labels are the sole identifiers. No session UUIDs.
|
||||
Labels must be lowercase, no whitespace, only a-z 0-9 - _
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -32,6 +35,18 @@ except ImportError:
|
|||
sys.exit("[helios-remote] ERROR: 'requests' not installed. Run: pip install requests")
|
||||
|
||||
|
||||
LABEL_RE = re.compile(r'^[a-z0-9][a-z0-9_-]*$')
|
||||
|
||||
def validate_label(label: str) -> str:
|
||||
"""Validate a device label."""
|
||||
if not LABEL_RE.match(label):
|
||||
sys.exit(
|
||||
f"[helios-remote] Invalid label '{label}'. "
|
||||
f"Must be lowercase, no whitespace, only a-z 0-9 - _"
|
||||
)
|
||||
return label
|
||||
|
||||
|
||||
# ── HTTP helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _headers() -> dict:
|
||||
|
|
@ -58,111 +73,85 @@ def _req(method: str, path: str, timeout: int = 30, **kwargs):
|
|||
return resp
|
||||
|
||||
|
||||
# ── Subcommands ──────────────────────────────────────────────────────────────
|
||||
# ── Window resolution ────────────────────────────────────────────────────────
|
||||
|
||||
HEADERS = _headers # alias for lazy evaluation
|
||||
|
||||
|
||||
def resolve_session(session_id: str) -> str:
|
||||
"""If session_id looks like a label (not a UUID), resolve it to an actual UUID."""
|
||||
import re
|
||||
uuid_pattern = re.compile(
|
||||
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', re.I
|
||||
)
|
||||
if uuid_pattern.match(session_id):
|
||||
return session_id
|
||||
# Look up by label
|
||||
resp = _req("GET", "/sessions")
|
||||
sessions = resp.json().get("sessions", [])
|
||||
for s in sessions:
|
||||
if s.get("label", "").lower() == session_id.lower():
|
||||
return s["id"]
|
||||
raise SystemExit(f"[helios-remote] No session found with label '{session_id}'")
|
||||
|
||||
|
||||
def resolve_window(sid: str, window_id_or_name: str) -> int:
|
||||
"""If window_id_or_name is a number, return it. Otherwise search by title substring."""
|
||||
if window_id_or_name.lstrip('-').isdigit():
|
||||
return int(window_id_or_name)
|
||||
# Search by title
|
||||
resp = _req("GET", f"/sessions/{sid}/windows")
|
||||
def resolve_window(device: str, window_label: str) -> int:
|
||||
"""Resolve a window label to its HWND. Searches by label field."""
|
||||
resp = _req("GET", f"/devices/{device}/windows")
|
||||
windows = resp.json().get("windows", [])
|
||||
query = window_id_or_name.lower()
|
||||
matches = [w for w in windows if w.get("visible") and query in w.get("title", "").lower()]
|
||||
query = window_label.lower()
|
||||
# Exact label match first
|
||||
for w in windows:
|
||||
if w.get("visible") and w.get("label", "") == query:
|
||||
return int(w["id"])
|
||||
# Substring match on label
|
||||
matches = [w for w in windows if w.get("visible") and query in w.get("label", "")]
|
||||
if not matches:
|
||||
raise SystemExit(f"[helios-remote] No visible window matching '{window_id_or_name}'")
|
||||
# Fallback: substring match on title
|
||||
matches = [w for w in windows if w.get("visible") and query in w.get("title", "").lower()]
|
||||
if not matches:
|
||||
raise SystemExit(f"[helios-remote] No visible window matching '{window_label}'")
|
||||
if len(matches) > 1:
|
||||
print(f"[helios-remote] Multiple matches for '{window_id_or_name}', using first:")
|
||||
print(f"[helios-remote] Multiple matches for '{window_label}', using first:")
|
||||
for w in matches:
|
||||
print(f" {w['id']} {w['title']}")
|
||||
print(f" {w.get('label', '?'):<30} {w['title']}")
|
||||
return int(matches[0]["id"])
|
||||
|
||||
|
||||
def cmd_sessions(_args):
|
||||
"""List all connected sessions."""
|
||||
resp = _req("GET", "/sessions")
|
||||
# ── Commands ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def cmd_devices(_args):
|
||||
"""List all connected devices."""
|
||||
resp = _req("GET", "/devices")
|
||||
data = resp.json()
|
||||
sessions = data if isinstance(data, list) else data.get("sessions", [])
|
||||
|
||||
if not sessions:
|
||||
print("No sessions connected.")
|
||||
devices = data.get("devices", [])
|
||||
if not devices:
|
||||
print("No devices connected.")
|
||||
return
|
||||
|
||||
print(f"{'ID':<36} {'Label':<20} Info")
|
||||
print("-" * 70)
|
||||
for s in sessions:
|
||||
sid = str(s.get("id") or s.get("session_id") or "?")
|
||||
label = str(s.get("label") or s.get("name") or "")
|
||||
info = str(s.get("os") or s.get("platform") or s.get("info") or "")
|
||||
print(f"{sid:<36} {label:<20} {info}")
|
||||
|
||||
|
||||
def cmd_label(args):
|
||||
"""Assign a human-readable name to a session."""
|
||||
sid = resolve_session(args.session_id)
|
||||
_req("POST", f"/sessions/{sid}/label", json={"label": args.name})
|
||||
print(f"Session {sid!r} labeled as {args.name!r}.")
|
||||
print(f"{'Device':<30}")
|
||||
print("-" * 30)
|
||||
for d in devices:
|
||||
print(d.get("label", "?"))
|
||||
|
||||
|
||||
def cmd_screenshot(args):
|
||||
"""Capture a screenshot → /tmp/helios-remote-screenshot.png"""
|
||||
sid = resolve_session(args.session_id)
|
||||
resp = _req("POST", f"/sessions/{sid}/screenshot")
|
||||
"""Capture a screenshot. Argument is 'screen' for full screen or a window label."""
|
||||
device = validate_label(args.device)
|
||||
target = args.target
|
||||
out_path = Path("/tmp/helios-remote-screenshot.png")
|
||||
|
||||
content_type = resp.headers.get("Content-Type", "")
|
||||
if "image" in content_type:
|
||||
out_path.write_bytes(resp.content)
|
||||
if target == "screen":
|
||||
resp = _req("POST", f"/devices/{device}/screenshot")
|
||||
else:
|
||||
data = resp.json()
|
||||
b64 = (
|
||||
data.get("screenshot")
|
||||
or data.get("image")
|
||||
or data.get("image_base64")
|
||||
or data.get("data")
|
||||
or data.get("png")
|
||||
)
|
||||
if not b64:
|
||||
sys.exit(
|
||||
f"[helios-remote] ERROR: Screenshot response has no image field.\n"
|
||||
f" Content-Type : {content_type}\n"
|
||||
f" Keys present : {list(data.keys()) if isinstance(data, dict) else type(data).__name__}"
|
||||
)
|
||||
if "," in b64: # strip data-URI prefix (data:image/png;base64,...)
|
||||
b64 = b64.split(",", 1)[1]
|
||||
out_path.write_bytes(base64.b64decode(b64))
|
||||
# Resolve window label to HWND
|
||||
wid = resolve_window(device, target)
|
||||
resp = _req("POST", f"/devices/{device}/windows/{wid}/screenshot")
|
||||
|
||||
data = resp.json()
|
||||
b64 = (
|
||||
data.get("image_base64")
|
||||
or data.get("screenshot")
|
||||
or data.get("image")
|
||||
or data.get("data")
|
||||
or data.get("png")
|
||||
)
|
||||
if not b64:
|
||||
sys.exit(f"[helios-remote] ERROR: No image in response. Keys: {list(data.keys())}")
|
||||
if "," in b64:
|
||||
b64 = b64.split(",", 1)[1]
|
||||
out_path.write_bytes(base64.b64decode(b64))
|
||||
print(str(out_path))
|
||||
|
||||
|
||||
def cmd_exec(args):
|
||||
"""Run a shell command on the remote session."""
|
||||
sid = resolve_session(args.session_id)
|
||||
"""Run a shell command on the remote device."""
|
||||
device = validate_label(args.device)
|
||||
command = " ".join(args.parts) if isinstance(args.parts, list) else args.parts
|
||||
body = {"command": command}
|
||||
if args.timeout:
|
||||
body["timeout_ms"] = args.timeout * 1000 # seconds → ms
|
||||
resp = _req("POST", f"/sessions/{sid}/exec", json=body, timeout=max(35, (args.timeout or 30) + 5))
|
||||
body["timeout_ms"] = args.timeout * 1000
|
||||
resp = _req("POST", f"/devices/{device}/exec", json=body,
|
||||
timeout=max(35, (args.timeout or 30) + 5))
|
||||
data = resp.json()
|
||||
|
||||
stdout = data.get("stdout") or data.get("output") or ""
|
||||
|
|
@ -178,127 +167,78 @@ def cmd_exec(args):
|
|||
sys.exit(int(exit_code))
|
||||
|
||||
|
||||
def cmd_click(args):
|
||||
"""Send a mouse click to the remote session."""
|
||||
sid = resolve_session(args.session_id)
|
||||
_req("POST", f"/sessions/{sid}/click", json={"x": args.x, "y": args.y})
|
||||
print(f"Clicked ({args.x}, {args.y}) on session {sid!r}.")
|
||||
|
||||
|
||||
def cmd_type(args):
|
||||
"""Send keyboard input to the remote session."""
|
||||
sid = resolve_session(args.session_id)
|
||||
_req("POST", f"/sessions/{sid}/type", json={"text": args.text})
|
||||
print(f"Typed {len(args.text)} chars on session {sid!r}.")
|
||||
|
||||
|
||||
def cmd_windows(args):
|
||||
"""List windows on the remote session (visible only by default)."""
|
||||
sid = resolve_session(args.session_id)
|
||||
resp = _req("GET", f"/sessions/{sid}/windows")
|
||||
"""List windows on the remote device (visible only)."""
|
||||
device = validate_label(args.device)
|
||||
resp = _req("GET", f"/devices/{device}/windows")
|
||||
data = resp.json()
|
||||
windows = data.get("windows", [])
|
||||
# Filter to visible only unless --all is passed
|
||||
all_windows = getattr(args, "all", False)
|
||||
if not all_windows:
|
||||
windows = [w for w in windows if w.get("visible")]
|
||||
windows = [w for w in windows if w.get("visible")]
|
||||
if not windows:
|
||||
print("No windows returned.")
|
||||
return
|
||||
print(f"{'ID':<20} Title")
|
||||
print(f"{'Label':<30} Title")
|
||||
print("-" * 70)
|
||||
for w in windows:
|
||||
wid = str(w.get("id", "?"))
|
||||
label = w.get("label", "?")
|
||||
title = w.get("title", "")
|
||||
print(f"{wid:<20} {title}")
|
||||
print(f"{label:<30} {title}")
|
||||
|
||||
|
||||
def cmd_minimize_all(args):
|
||||
"""Minimize all windows on the remote session."""
|
||||
sid = resolve_session(args.session_id)
|
||||
_req("POST", f"/sessions/{sid}/windows/minimize-all")
|
||||
print(f"All windows minimized on session {sid!r}.")
|
||||
"""Minimize all windows on the remote device."""
|
||||
device = validate_label(args.device)
|
||||
_req("POST", f"/devices/{device}/windows/minimize-all")
|
||||
print(f"All windows minimized on {device}.")
|
||||
|
||||
|
||||
def cmd_focus(args):
|
||||
"""Bring a window to the foreground (by ID or title substring)."""
|
||||
sid = resolve_session(args.session_id)
|
||||
wid = resolve_window(sid, args.window_id)
|
||||
_req("POST", f"/sessions/{sid}/windows/{wid}/focus")
|
||||
print(f"Window {wid} focused on session {sid!r}.")
|
||||
"""Bring a window to the foreground (by label)."""
|
||||
device = validate_label(args.device)
|
||||
wid = resolve_window(device, args.window)
|
||||
_req("POST", f"/devices/{device}/windows/{wid}/focus")
|
||||
print(f"Window '{args.window}' focused on {device}.")
|
||||
|
||||
|
||||
def cmd_maximize(args):
|
||||
"""Maximize and focus a window (by ID or title substring)."""
|
||||
sid = resolve_session(args.session_id)
|
||||
wid = resolve_window(sid, args.window_id)
|
||||
_req("POST", f"/sessions/{sid}/windows/{wid}/maximize")
|
||||
print(f"Window {wid} maximized on session {sid!r}.")
|
||||
"""Maximize and focus a window (by label)."""
|
||||
device = validate_label(args.device)
|
||||
wid = resolve_window(device, args.window)
|
||||
_req("POST", f"/devices/{device}/windows/{wid}/maximize")
|
||||
print(f"Window '{args.window}' maximized on {device}.")
|
||||
|
||||
|
||||
def cmd_logs(args):
|
||||
"""Fetch the last N lines of the client log file."""
|
||||
sid = resolve_session(args.session_id)
|
||||
resp = _req("GET", f"/sessions/{sid}/logs", params={"lines": args.lines})
|
||||
data = resp.json()
|
||||
if "error" in data:
|
||||
sys.exit(f"[helios-remote] {data['error']}")
|
||||
print(f"# {data.get('log_path', '?')} (last {args.lines} lines)")
|
||||
print(data.get("content", ""))
|
||||
def cmd_version(args):
|
||||
"""Show relay, remote.py, and client commit — check if all are in sync."""
|
||||
device = validate_label(args.device)
|
||||
|
||||
# 1. Relay (public, no auth)
|
||||
try:
|
||||
r = requests.get(f"{BASE_URL}/version", timeout=10)
|
||||
relay = r.json() if r.ok else {"commit": f"HTTP {r.status_code}"}
|
||||
except Exception as e:
|
||||
relay = {"commit": str(e)}
|
||||
|
||||
def cmd_screenshot_window(args):
|
||||
"""Capture a specific window by ID or title substring → /tmp/helios-remote-screenshot.png"""
|
||||
sid = resolve_session(args.session_id)
|
||||
wid = resolve_window(sid, args.window_id)
|
||||
resp = _req("POST", f"/sessions/{sid}/windows/{wid}/screenshot")
|
||||
data = resp.json()
|
||||
if "error" in data:
|
||||
sys.exit(f"[helios-remote] {data['error']}")
|
||||
import base64, os
|
||||
out_path = args.output or "/tmp/helios-remote-screenshot.png"
|
||||
img_bytes = base64.b64decode(data["image_base64"])
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(img_bytes)
|
||||
print(out_path)
|
||||
return out_path
|
||||
|
||||
|
||||
def _script_commit() -> str:
|
||||
"""Return the git commit hash of remote.py itself."""
|
||||
import subprocess, os
|
||||
# 2. remote.py commit
|
||||
import subprocess
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["git", "log", "-1", "--format=%h", "--", __file__],
|
||||
capture_output=True, text=True,
|
||||
cwd=os.path.dirname(os.path.abspath(__file__))
|
||||
)
|
||||
return r.stdout.strip() or "unknown"
|
||||
script_commit = r.stdout.strip() or "unknown"
|
||||
except Exception:
|
||||
return "unknown"
|
||||
script_commit = "unknown"
|
||||
|
||||
|
||||
def cmd_status(args):
|
||||
"""Show relay, remote.py, and client commit in one call."""
|
||||
import requests as _requests
|
||||
|
||||
# 1. Relay (public endpoint, no auth)
|
||||
# 3. Client
|
||||
try:
|
||||
r = _requests.get(f"{BASE_URL}/version", timeout=10)
|
||||
relay = r.json() if r.ok else {"commit": f"HTTP {r.status_code}"}
|
||||
except Exception as e:
|
||||
relay = {"commit": str(e)}
|
||||
|
||||
# 2. Client (via session)
|
||||
sid = resolve_session(args.session_id)
|
||||
try:
|
||||
r2 = _req("GET", f"/sessions/{sid}/version")
|
||||
r2 = _req("GET", f"/devices/{device}/version")
|
||||
client = r2.json()
|
||||
except Exception as e:
|
||||
client = {"commit": str(e)}
|
||||
except SystemExit:
|
||||
client = {"commit": "unreachable"}
|
||||
|
||||
relay_commit = relay.get("commit", "?")
|
||||
script_commit = _script_commit()
|
||||
client_commit = client.get("commit", "?")
|
||||
all_same = relay_commit == script_commit == client_commit
|
||||
|
||||
|
|
@ -308,53 +248,41 @@ def cmd_status(args):
|
|||
print(f" {'✅ all in sync' if all_same else '⚠️ OUT OF SYNC'}")
|
||||
|
||||
|
||||
def cmd_server_version(_args):
|
||||
"""Get server version (no auth required)."""
|
||||
import requests as _requests
|
||||
try:
|
||||
resp = _requests.get(f"{BASE_URL}/version", timeout=10)
|
||||
except Exception as exc:
|
||||
sys.exit(f"[helios-remote] CONNECTION ERROR: {exc}")
|
||||
if not resp.ok:
|
||||
sys.exit(f"[helios-remote] HTTP {resp.status_code}: {resp.text[:500]}")
|
||||
def cmd_logs(args):
|
||||
"""Fetch the last N lines of the client log file."""
|
||||
device = validate_label(args.device)
|
||||
resp = _req("GET", f"/devices/{device}/logs", params={"lines": args.lines})
|
||||
data = resp.json()
|
||||
print(f"Server version : {data.get('version', '?')}")
|
||||
print(f"Commit : {data.get('commit', '?')}")
|
||||
|
||||
|
||||
def cmd_version(args):
|
||||
"""Get the client version for a session."""
|
||||
sid = resolve_session(args.session_id)
|
||||
resp = _req("GET", f"/sessions/{sid}/version")
|
||||
data = resp.json()
|
||||
print(f"Client version : {data.get('version', '?')}")
|
||||
print(f"Commit : {data.get('commit', '?')}")
|
||||
if "error" in data:
|
||||
sys.exit(f"[helios-remote] {data['error']}")
|
||||
print(f"# {data.get('log_path', '?')} (last {args.lines} lines)")
|
||||
print(data.get("content", ""))
|
||||
|
||||
|
||||
def cmd_upload(args):
|
||||
"""Upload a local file to the remote session."""
|
||||
sid = resolve_session(args.session_id)
|
||||
"""Upload a local file to the remote device."""
|
||||
device = validate_label(args.device)
|
||||
local_path = Path(args.local_path)
|
||||
if not local_path.exists():
|
||||
sys.exit(f"[helios-remote] ERROR: Local file not found: {local_path}")
|
||||
content_base64 = base64.b64encode(local_path.read_bytes()).decode()
|
||||
_req("POST", f"/sessions/{sid}/upload", json={
|
||||
_req("POST", f"/devices/{device}/upload", json={
|
||||
"path": args.remote_path,
|
||||
"content_base64": content_base64,
|
||||
})
|
||||
print(f"Uploaded {local_path} → {args.remote_path} on session {sid!r}.")
|
||||
print(f"Uploaded {local_path} → {args.remote_path} on {device}.")
|
||||
|
||||
|
||||
def cmd_download(args):
|
||||
"""Download a file from the remote session to a local path."""
|
||||
sid = resolve_session(args.session_id)
|
||||
"""Download a file from the remote device to a local path."""
|
||||
device = validate_label(args.device)
|
||||
from urllib.parse import quote
|
||||
encoded_path = quote(args.remote_path, safe="")
|
||||
resp = _req("GET", f"/sessions/{sid}/download?path={encoded_path}")
|
||||
resp = _req("GET", f"/devices/{device}/download?path={encoded_path}")
|
||||
data = resp.json()
|
||||
b64 = data.get("content_base64", "")
|
||||
if not b64:
|
||||
sys.exit(f"[helios-remote] ERROR: No content in download response. Keys: {list(data.keys())}")
|
||||
sys.exit(f"[helios-remote] ERROR: No content in download response.")
|
||||
local_path = Path(args.local_path)
|
||||
local_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
local_path.write_bytes(base64.b64decode(b64))
|
||||
|
|
@ -362,84 +290,41 @@ def cmd_download(args):
|
|||
print(f"Downloaded {args.remote_path} → {local_path} ({size} bytes).")
|
||||
|
||||
|
||||
def cmd_find_window(args):
|
||||
"""Find visible windows by title substring on the remote session."""
|
||||
sid = resolve_session(args.session_id)
|
||||
resp = _req("GET", f"/sessions/{sid}/windows")
|
||||
data = resp.json()
|
||||
windows = data.get("windows", [])
|
||||
query = args.title.lower()
|
||||
# Only visible windows
|
||||
matches = [w for w in windows if w.get("visible") and query in w.get("title", "").lower()]
|
||||
if not matches:
|
||||
print(f"No visible windows matching '{args.title}'.")
|
||||
return
|
||||
print(f"{'ID':<20} Title")
|
||||
print("-" * 70)
|
||||
for w in matches:
|
||||
wid = str(w.get("id", "?"))
|
||||
title = w.get("title", "")
|
||||
print(f"{wid:<20} {title}")
|
||||
|
||||
|
||||
def cmd_prompt(args):
|
||||
"""Show a MessageBox on the remote PC asking the user to do something.
|
||||
Blocks until the user clicks OK — use this when the AI needs the user
|
||||
to perform a manual action (e.g. click a button, confirm a dialog)."""
|
||||
sid = resolve_session(args.session_id)
|
||||
"""Show a MessageBox on the remote PC asking the user to do something."""
|
||||
device = validate_label(args.device)
|
||||
body = {"message": args.message}
|
||||
if args.title:
|
||||
body["title"] = args.title
|
||||
resp = _req("POST", f"/sessions/{sid}/prompt", json=body)
|
||||
answer = resp.get("answer", "")
|
||||
resp = _req("POST", f"/devices/{device}/prompt", json=body)
|
||||
data = resp.json()
|
||||
answer = data.get("answer", "")
|
||||
if answer:
|
||||
print(answer)
|
||||
else:
|
||||
print(f"Prompt confirmed (no answer returned).")
|
||||
|
||||
|
||||
def cmd_wait_for_window(args):
|
||||
"""Poll until a visible window with the given title appears (or timeout)."""
|
||||
import time
|
||||
sid = resolve_session(args.session_id)
|
||||
query = args.title.lower()
|
||||
deadline = time.time() + args.timeout
|
||||
interval = 1.0
|
||||
print(f"Waiting for window matching '{args.title}' (timeout: {args.timeout}s)...")
|
||||
while time.time() < deadline:
|
||||
resp = _req("GET", f"/sessions/{sid}/windows")
|
||||
windows = resp.json().get("windows", [])
|
||||
matches = [w for w in windows if w.get("visible") and query in w.get("title", "").lower()]
|
||||
if matches:
|
||||
print(f"{'ID':<20} Title")
|
||||
print("-" * 70)
|
||||
for w in matches:
|
||||
print(f"{str(w.get('id','?')):<20} {w.get('title','')}")
|
||||
return
|
||||
time.sleep(interval)
|
||||
sys.exit(f"[helios-remote] Timeout: no window matching '{args.title}' appeared within {args.timeout}s")
|
||||
print("Prompt confirmed.")
|
||||
|
||||
|
||||
def cmd_run(args):
|
||||
"""Launch a program on the remote session (fire-and-forget)."""
|
||||
sid = resolve_session(args.session_id)
|
||||
_req("POST", f"/sessions/{sid}/run", json={"program": args.program, "args": args.args})
|
||||
print(f"Started {args.program!r} on session {sid!r}.")
|
||||
"""Launch a program on the remote device (fire-and-forget)."""
|
||||
device = validate_label(args.device)
|
||||
_req("POST", f"/devices/{device}/run", json={"program": args.program, "args": args.args})
|
||||
print(f"Started {args.program!r} on {device}.")
|
||||
|
||||
|
||||
def cmd_clipboard_get(args):
|
||||
"""Get the clipboard contents from the remote session."""
|
||||
sid = resolve_session(args.session_id)
|
||||
resp = _req("GET", f"/sessions/{sid}/clipboard")
|
||||
"""Get the clipboard contents from the remote device."""
|
||||
device = validate_label(args.device)
|
||||
resp = _req("GET", f"/devices/{device}/clipboard")
|
||||
data = resp.json()
|
||||
print(data.get("text", ""))
|
||||
|
||||
|
||||
def cmd_clipboard_set(args):
|
||||
"""Set the clipboard contents on the remote session."""
|
||||
sid = resolve_session(args.session_id)
|
||||
_req("POST", f"/sessions/{sid}/clipboard", json={"text": args.text})
|
||||
print(f"Clipboard set ({len(args.text)} chars) on session {sid!r}.")
|
||||
"""Set the clipboard contents on the remote device."""
|
||||
device = validate_label(args.device)
|
||||
_req("POST", f"/devices/{device}/clipboard", json={"text": args.text})
|
||||
print(f"Clipboard set ({len(args.text)} chars) on {device}.")
|
||||
|
||||
|
||||
# ── CLI wiring ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -447,103 +332,69 @@ def cmd_clipboard_set(args):
|
|||
def build_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(
|
||||
prog="remote.py",
|
||||
description="Control PCs connected to the Helios Remote Relay Server.",
|
||||
description="Control devices connected to the Helios Remote Relay Server.",
|
||||
)
|
||||
sub = p.add_subparsers(dest="subcmd", required=True)
|
||||
|
||||
sub.add_parser("sessions", help="List all connected sessions")
|
||||
sub.add_parser("devices", help="List all connected devices")
|
||||
|
||||
lp = sub.add_parser("label", help="Assign a name to a session")
|
||||
lp.add_argument("session_id")
|
||||
lp.add_argument("name")
|
||||
sp = sub.add_parser("screenshot", help="Capture screenshot (screen or window label)")
|
||||
sp.add_argument("device", help="Device label")
|
||||
sp.add_argument("target", help="'screen' for full screen, or a window label")
|
||||
|
||||
sp = sub.add_parser("screenshot", help="Capture screenshot → /tmp/helios-remote-screenshot.png")
|
||||
sp.add_argument("session_id")
|
||||
|
||||
swp = sub.add_parser("screenshot-window", help="Capture a specific window (by ID or title)")
|
||||
swp.add_argument("session_id")
|
||||
swp.add_argument("window_id", help="Window ID (number) or title substring")
|
||||
swp.add_argument("--output", default=None, help="Output path (default: /tmp/helios-remote-screenshot.png)")
|
||||
swp.set_defaults(func=cmd_screenshot_window)
|
||||
|
||||
ep = sub.add_parser("exec", help="Run a shell command on the remote session")
|
||||
ep.add_argument("session_id")
|
||||
ep = sub.add_parser("exec", help="Run a shell command on the remote device")
|
||||
ep.add_argument("device", help="Device label")
|
||||
ep.add_argument("parts", nargs=argparse.REMAINDER, metavar="command",
|
||||
help="Command (and arguments) to execute")
|
||||
ep.add_argument("--timeout", type=int, default=None, metavar="SECONDS",
|
||||
help="Timeout in seconds (default: 30). Use higher for long downloads etc.")
|
||||
help="Timeout in seconds (default: 30)")
|
||||
|
||||
cp = sub.add_parser("click", help="Send a mouse click")
|
||||
cp.add_argument("session_id")
|
||||
cp.add_argument("x", type=int)
|
||||
cp.add_argument("y", type=int)
|
||||
|
||||
tp = sub.add_parser("type", help="Send keyboard input")
|
||||
tp.add_argument("session_id")
|
||||
tp.add_argument("text")
|
||||
|
||||
wp = sub.add_parser("windows", help="List all windows on the remote session")
|
||||
wp.add_argument("session_id")
|
||||
wp = sub.add_parser("windows", help="List all visible windows on the remote device")
|
||||
wp.add_argument("device", help="Device label")
|
||||
|
||||
mp = sub.add_parser("minimize-all", help="Minimize all windows")
|
||||
mp.add_argument("session_id")
|
||||
mp.add_argument("device", help="Device label")
|
||||
|
||||
fp = sub.add_parser("focus", help="Bring a window to the foreground")
|
||||
fp.add_argument("session_id")
|
||||
fp.add_argument("window_id")
|
||||
fp.add_argument("device", help="Device label")
|
||||
fp.add_argument("window", help="Window label")
|
||||
|
||||
xp = sub.add_parser("maximize", help="Maximize and focus a window")
|
||||
xp.add_argument("session_id")
|
||||
xp.add_argument("window_id")
|
||||
xp.add_argument("device", help="Device label")
|
||||
xp.add_argument("window", help="Window label")
|
||||
|
||||
stp = sub.add_parser("status", help="Show relay + client commit and sync status")
|
||||
stp.add_argument("session_id")
|
||||
vp = sub.add_parser("version", help="Compare relay, remote.py, and client commits")
|
||||
vp.add_argument("device", help="Device label")
|
||||
|
||||
lgp = sub.add_parser("logs", help="Fetch last N lines of client log file")
|
||||
lgp.add_argument("session_id")
|
||||
lgp.add_argument("--lines", type=int, default=100, metavar="N", help="Number of lines (default: 100)")
|
||||
lgp.add_argument("device", help="Device label")
|
||||
lgp.add_argument("--lines", type=int, default=100, metavar="N")
|
||||
|
||||
sub.add_parser("server-version", help="Get server version (no auth required)")
|
||||
|
||||
vp = sub.add_parser("version", help="Get client version for a session")
|
||||
vp.add_argument("session_id")
|
||||
|
||||
up = sub.add_parser("upload", help="Upload a local file to the remote session")
|
||||
up.add_argument("session_id")
|
||||
up = sub.add_parser("upload", help="Upload a local file to the remote device")
|
||||
up.add_argument("device", help="Device label")
|
||||
up.add_argument("local_path")
|
||||
up.add_argument("remote_path")
|
||||
|
||||
dp = sub.add_parser("download", help="Download a file from the remote session")
|
||||
dp.add_argument("session_id")
|
||||
dp = sub.add_parser("download", help="Download a file from the remote device")
|
||||
dp.add_argument("device", help="Device label")
|
||||
dp.add_argument("remote_path")
|
||||
dp.add_argument("local_path")
|
||||
|
||||
fwp = sub.add_parser("find-window", help="Find windows by title substring")
|
||||
fwp.add_argument("session_id")
|
||||
fwp.add_argument("title", help="Substring to search for (case-insensitive)")
|
||||
pp = sub.add_parser("prompt", help="Show a MessageBox asking the user to do something")
|
||||
pp.add_argument("device", help="Device label")
|
||||
pp.add_argument("message")
|
||||
pp.add_argument("--title", default=None)
|
||||
|
||||
wfwp = sub.add_parser("wait-for-window", help="Poll until a window with given title appears")
|
||||
wfwp.add_argument("session_id")
|
||||
wfwp.add_argument("title", help="Substring to wait for (case-insensitive)")
|
||||
wfwp.add_argument("--timeout", type=int, default=30, metavar="SECONDS", help="Max wait time (default: 30s)")
|
||||
wfwp.set_defaults(func=cmd_wait_for_window)
|
||||
rp = sub.add_parser("run", help="Launch a program (fire-and-forget)")
|
||||
rp.add_argument("device", help="Device label")
|
||||
rp.add_argument("program")
|
||||
rp.add_argument("args", nargs=argparse.REMAINDER)
|
||||
|
||||
pp = sub.add_parser("prompt", help="Show a MessageBox asking the user to do something manually")
|
||||
pp.add_argument("session_id")
|
||||
pp.add_argument("message", help="What to ask the user (e.g. 'Please click Save, then OK')")
|
||||
pp.add_argument("--title", default=None, help="Dialog title (default: Helios Remote)")
|
||||
pp.set_defaults(func=cmd_prompt)
|
||||
cgp = sub.add_parser("clipboard-get", help="Get clipboard contents")
|
||||
cgp.add_argument("device", help="Device label")
|
||||
|
||||
rp = sub.add_parser("run", help="Launch a program on the remote session (fire-and-forget)")
|
||||
rp.add_argument("session_id")
|
||||
rp.add_argument("program", help="Program to launch (e.g. notepad.exe)")
|
||||
rp.add_argument("args", nargs=argparse.REMAINDER, help="Optional arguments")
|
||||
|
||||
cgp = sub.add_parser("clipboard-get", help="Get clipboard contents from the remote session")
|
||||
cgp.add_argument("session_id")
|
||||
|
||||
csp = sub.add_parser("clipboard-set", help="Set clipboard contents on the remote session")
|
||||
csp.add_argument("session_id")
|
||||
csp = sub.add_parser("clipboard-set", help="Set clipboard contents")
|
||||
csp.add_argument("device", help="Device label")
|
||||
csp.add_argument("text")
|
||||
|
||||
return p
|
||||
|
|
@ -554,29 +405,21 @@ def main():
|
|||
args = parser.parse_args()
|
||||
|
||||
{
|
||||
"sessions": cmd_sessions,
|
||||
"label": cmd_label,
|
||||
"screenshot": cmd_screenshot,
|
||||
"exec": cmd_exec,
|
||||
"click": cmd_click,
|
||||
"type": cmd_type,
|
||||
"windows": cmd_windows,
|
||||
"minimize-all": cmd_minimize_all,
|
||||
"focus": cmd_focus,
|
||||
"devices": cmd_devices,
|
||||
"screenshot": cmd_screenshot,
|
||||
"exec": cmd_exec,
|
||||
"windows": cmd_windows,
|
||||
"minimize-all": cmd_minimize_all,
|
||||
"focus": cmd_focus,
|
||||
"maximize": cmd_maximize,
|
||||
"server-version": cmd_server_version,
|
||||
"version": cmd_version,
|
||||
"logs": cmd_logs,
|
||||
"upload": cmd_upload,
|
||||
"download": cmd_download,
|
||||
"status": cmd_status,
|
||||
"logs": cmd_logs,
|
||||
"screenshot-window": cmd_screenshot_window,
|
||||
"find-window": cmd_find_window,
|
||||
"wait-for-window": cmd_wait_for_window,
|
||||
"run": cmd_run,
|
||||
"prompt": cmd_prompt,
|
||||
"clipboard-get": cmd_clipboard_get,
|
||||
"clipboard-set": cmd_clipboard_set,
|
||||
"prompt": cmd_prompt,
|
||||
"run": cmd_run,
|
||||
"clipboard-get": cmd_clipboard_get,
|
||||
"clipboard-set": cmd_clipboard_set,
|
||||
}[args.subcmd](args)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue