app
Adom Desktop - RustDesk Bridge
Public Made by Adomby adom
Drive RustDesk remote-desktop sessions from cloud Claude via Adom Desktop — connect out to any host by ID or IP (with a password), and list/close sessions. Works where RDP can't (Windows Home).
← Commit history
Publish 0.1.1
11 files changed
+381
BRIDGE_VERSIONadded+1@@ -0,0 +1 @@+0.1.1
SKILL.mdadded+27@@ -0,0 +1,27 @@+---+name: adom-desktop-rustdesk-bridge+description: Drive RustDesk remote-desktop sessions via Adom Desktop. Use when the user wants to remote into / control / screen-share / view a host that RDP can't reach (Windows Home, no RDP server), or says rustdesk, connect via rustdesk, remote desktop, remote into my machine, screen share my other pc, rdp alternative, vnc alternative, watch the install on my test laptop.+---++# Adom Desktop - RustDesk Bridge++Use this when the user wants to **control or view a remote machine's screen** and RDP isn't an option (Windows **Home** can't host RDP; or the host has no RDP server). It's the client side — it drives the controlling machine out to a host.++## Activate (first use, idempotent)++1. `adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-desktop-rustdesk-bridge/files/rustdesk-bridge-manifest.json"}'`+2. `adom-desktop rustdesk_ensure_client` — downloads the portable RustDesk client if missing+3. then call the verbs below++## Verbs++- `rustdesk_connect {to, password?}` — `to` is a RustDesk ID or IP. Pass `password` to auto-authenticate (no typing).+- `rustdesk_client_status` — installed path + active sessions.+- `rustdesk_disconnect {to?}` — close one session (or all).+- `rustdesk_ensure_client` — ensure/download the client.++## Notes++- Multi-machine: address with `adom-desktop --target <name> rustdesk_connect ...`.+- RustDesk portable uses a rotating one-time password; a permanent password needs the installed service (host-side, Phase 2).+- On the same LAN/VPN, RustDesk upgrades to a direct P2P connection automatically.
bridge.jsonadded+33@@ -0,0 +1,33 @@+{+ "manifest_version": 1,+ "name": "rustdesk",+ "displayName": "RustDesk",+ "version": "0.1.1",+ "description": "Drive RustDesk remote-desktop connections (client side): connect out to an ID/IP, list and close sessions.",+ "author": "adom",+ "license": "MIT",+ "spawn": {+ "kind": "python",+ "entrypoint": "server.py",+ "port": 0,+ "healthEndpoint": "/status",+ "stopMethod": "kill",+ "killImageName": "python.exe"+ },+ "verbPrefixes": ["rustdesk_"],+ "verbs": [+ "rustdesk_connect",+ "rustdesk_disconnect",+ "rustdesk_client_status",+ "rustdesk_ensure_client"+ ],+ "statusVerb": "rustdesk_client_status",+ "dependencies": { "python": ">=3.8" },+ "category": "Remote Access",+ "tags": ["rustdesk", "remote desktop", "remote access", "screen share", "vnc", "rdp alternative"],+ "platforms": {+ "windows": { "supported": true },+ "macos": { "supported": false, "reason": "Untested; exe resolution + tasklist/taskkill are Windows-specific in this version." },+ "linux": { "supported": false, "reason": "Untested; Windows-specific process/window handling so far." }+ }+}
docs/HERO.mdadded+1@@ -0,0 +1 @@+Put a 1200x630 `hero.png` in this folder. It is the billboard reused on the page header, the homepage, and the screensaver. Until it exists, `adom-wiki pkg lint` reports the hero as the one outstanding item.
docs/hero.pngadded⋯ 1 unchanged line ⋯
install.shadded+7@@ -0,0 +1,7 @@+#!/bin/sh+set -e++# Symlink your install targets back into the module dir so edits+# propagate and reinstalls do not clobber, e.g.:+# ln -sfn "$PWD/bin/run" "$HOME/.local/bin/run"+echo "installed"
package.jsonadded+48@@ -0,0 +1,48 @@+{+ "slug": "adom-desktop-rustdesk-bridge",+ "title": "Adom Desktop - RustDesk Bridge",+ "type": "app",+ "version": "0.1.1",+ "description": "Drive RustDesk remote-desktop sessions from cloud Claude via Adom Desktop — connect out to any host by ID or IP (with a password), and list/close sessions. Works where RDP can't (Windows Home). The client-side sibling of the RDP bridge.",+ "tags": ["bridge", "rustdesk", "remote-desktop", "remote-access", "screen-share", "adom-desktop"],+ "dependencies": {},+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ },+ "discovery_triggers": [+ "rustdesk",+ "connect via rustdesk",+ "remote desktop",+ "remote into my machine",+ "control a remote host",+ "screen share my other machine",+ "view my other pc",+ "remote desktop to a windows home pc",+ "rdp alternative",+ "vnc alternative",+ "watch the install on my test laptop"+ ],+ "discovery_pitch": "Drive a RustDesk session from cloud Claude — connect out to any host by ID or IP with a password and manage the session. Works where RDP can't (Windows Home).",+ "sample_prompts": [+ { "label": "Connect", "prompt": "Connect to host 48174343 via rustdesk" },+ { "label": "Watch install", "prompt": "Open a rustdesk session to my test laptop and watch the HD install" },+ { "label": "List sessions", "prompt": "What rustdesk sessions are open?" },+ { "label": "Disconnect", "prompt": "Disconnect the rustdesk session to 48174343" }+ ],+ "metadata": {+ "activate": {+ "kind": "adom-desktop-bridge",+ "skill": "adom/adom-desktop-rustdesk-bridge",+ "bridgeManifestUrl": "https://wiki.adom.inc/api/v1/pages/adom-desktop-rustdesk-bridge/files/rustdesk-bridge-manifest.json",+ "postInstall": ["rustdesk_ensure_client"],+ "requires": ["adom-desktop"],+ "verbPrefixes": ["rustdesk_"]+ }+ },+ "hero": {+ "headline": "RustDesk Bridge",+ "subhead": "Drive a remote desktop from cloud Claude — even on Windows Home.",+ "screenshot": "docs/hero.png"+ }+}
rustdesk-bridge-manifest.jsonadded+8@@ -0,0 +1,8 @@+{+ "manifest_version": 1,+ "version": "0.1.1",+ "url": "https://wiki.adom.inc/api/v1/pages/adom-desktop-rustdesk-bridge/files/rustdesk-bridge-v0.1.1.zip",+ "sha256": "38fe41b35590a367e106d5c7f70f7214a515fd0814e4340bb1a77b31e39ac22d",+ "size": 4434,+ "released_at": "2026-06-30T02:22:26Z"+}
rustdesk-bridge-v0.1.1.zipadded⋯ 1 unchanged line ⋯
server.pyadded+251@@ -0,0 +1,251 @@+#!/usr/bin/env python3+"""Adom Desktop - RustDesk Bridge (Phase 1: client side).++Drives the CONTROLLING side of RustDesk from cloud Claude via adom-desktop:+launch a connection out to a remote ID/IP, list active sessions, close them,+and ensure the client binary is present.++HTTP contract (adom-desktop bridge SDK):+ GET /status -> {ok, bridge, version, uptimeSec, led, summary, tooltip}+ POST /command {command,args} -> {success, output|error, ...}++The runtime spawns this as: python server.py --port <ephemeral>+"""+import csv+import glob+import io+import json+import os+import re+import subprocess+import sys+import time+import urllib.request+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer++BRIDGE_NAME = "rustdesk"+BRIDGE_VERSION = "0.1.1"+DEFAULT_PORT = 8810+RUSTDESK_DL_URL = "https://github.com/rustdesk/rustdesk/releases/download/1.4.8/rustdesk-1.4.8-x86_64.exe"++# DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP (Windows: don't tie the GUI to the bridge)+_CREATE_FLAGS = 0x00000008 | 0x00000200+_NO_WINDOW = 0x08000000 # CREATE_NO_WINDOW for the helper subprocesses (tasklist/taskkill)+_START = time.time()+++# ---------------------------------------------------------------- helpers+def _candidate_exes():+ la = os.environ.get("LOCALAPPDATA", "")+ pf = os.environ.get("ProgramFiles", r"C:\Program Files")+ up = os.environ.get("USERPROFILE", "")+ out = [+ os.path.join(la, "rustdesk", "rustdesk.exe"),+ os.path.join(pf, "RustDesk", "rustdesk.exe"),+ ]+ if up:+ out += sorted(glob.glob(os.path.join(up, "Downloads", "rustdesk-*.exe")), reverse=True)+ return out+++def find_rustdesk():+ for p in _candidate_exes():+ if p and os.path.isfile(p):+ return p+ from shutil import which+ return which("rustdesk")+++def _tasklist_rustdesk():+ """[{pid, title}] for every running rustdesk.exe (window title via /V)."""+ try:+ out = subprocess.run(+ ["tasklist", "/FI", "IMAGENAME eq rustdesk.exe", "/V", "/FO", "CSV", "/NH"],+ capture_output=True, text=True, timeout=10, creationflags=_NO_WINDOW,+ ).stdout+ except Exception:+ return []+ rows = []+ for r in csv.reader(io.StringIO(out)):+ if len(r) >= 9 and r[0].lower().startswith("rustdesk"):+ try:+ pid = int(r[1])+ except ValueError:+ continue+ rows.append({"pid": pid, "title": r[-1]})+ return rows+++_SESSION_RE = re.compile(r"^\s*(\S.*?)\s*-\s*Remote Desktop\s*-\s*RustDesk", re.I)+++def _sessions():+ """Active outbound control sessions, parsed from '<peer> - Remote Desktop - RustDesk' titles."""+ out = []+ for p in _tasklist_rustdesk():+ m = _SESSION_RE.match(p.get("title") or "")+ if m:+ out.append({"pid": p["pid"], "peer": m.group(1).strip(), "title": p["title"]})+ return out+++def _norm(s):+ return re.sub(r"\s+", "", s or "")+++# ---------------------------------------------------------------- verbs+def v_connect(args):+ to = (args.get("to") or args.get("id") or args.get("ip") or "").strip()+ if not to:+ return {"success": False, "error": "missing 'to' (RustDesk ID or IP)"}+ exe = find_rustdesk()+ if not exe:+ return {"success": False, "error": "rustdesk client not found - run rustdesk_ensure_client first"}+ cmd = [exe, "--connect", to]+ pw = args.get("password")+ if pw:+ cmd += ["--password", str(pw)]+ try:+ p = subprocess.Popen(cmd, creationflags=_CREATE_FLAGS,+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)+ except Exception as e:+ return {"success": False, "error": "launch failed: %s" % e}+ return {"success": True, "output": "connecting to %s" % to,+ "to": to, "pid": p.pid, "withPassword": bool(pw), "exe": exe,+ "_hint": "Session window opens as '<id> - Remote Desktop - RustDesk'. "+ "Use rustdesk_client_status to confirm, rustdesk_disconnect to close."}+++def v_disconnect(args):+ to = (args.get("to") or args.get("id") or "").strip()+ killed = []+ for s in _sessions():+ if not to or _norm(s["peer"]) == _norm(to):+ try:+ subprocess.run(["taskkill", "/PID", str(s["pid"]), "/F"],+ capture_output=True, timeout=10, creationflags=_NO_WINDOW)+ killed.append(s["peer"])+ except Exception:+ pass+ return {"success": True, "output": "disconnected %d session(s)" % len(killed),+ "disconnected": killed, "count": len(killed)}+++def v_client_status(args):+ exe = find_rustdesk()+ sess = _sessions()+ return {"success": True,+ "output": "%d active session(s)" % len(sess),+ "installed": bool(exe), "path": exe,+ "sessions": sess, "bridgeVersion": BRIDGE_VERSION}+++def v_ensure_client(args):+ exe = find_rustdesk()+ if exe:+ return {"success": True, "output": "client present", "present": True, "path": exe}+ up = os.environ.get("USERPROFILE", "")+ dest = os.path.join(up, "Downloads", "rustdesk-1.4.8-x86_64.exe")+ try:+ urllib.request.urlretrieve(RUSTDESK_DL_URL, dest)+ except Exception as e:+ return {"success": False, "error": "download failed: %s" % e}+ return {"success": True, "output": "downloaded portable client",+ "present": True, "path": dest, "downloaded": True}+++VERBS = {+ "rustdesk_connect": v_connect,+ "rustdesk_disconnect": v_disconnect,+ "rustdesk_client_status": v_client_status,+ "rustdesk_ensure_client": v_ensure_client,+}+++# ---------------------------------------------------------------- status chip+def status_payload():+ exe = find_rustdesk()+ sess = _sessions()+ if not exe:+ led, summary = "yellow", "client missing"+ elif sess:+ led, summary = "green", "%d session%s" % (len(sess), "" if len(sess) == 1 else "s")+ else:+ led, summary = "green", "ready"+ tooltip = "RustDesk client bridge v%s\n%s" % (+ BRIDGE_VERSION,+ ("Sessions: " + ", ".join(s["peer"] for s in sess)) if sess else "No active sessions",+ )+ return {"ok": True, "bridge": BRIDGE_NAME, "version": BRIDGE_VERSION,+ "uptimeSec": int(time.time() - _START),+ "led": led, "summary": summary, "tooltip": tooltip}+++# ---------------------------------------------------------------- http+class Handler(BaseHTTPRequestHandler):+ def _send(self, code, obj):+ body = json.dumps(obj).encode()+ self.send_response(code)+ self.send_header("Content-Type", "application/json")+ self.send_header("Content-Length", str(len(body)))+ self.end_headers()+ self.wfile.write(body)++ def log_message(self, *a): # keep stdout/stderr clean+ pass++ def do_GET(self):+ if self.path.rstrip("/") in ("/status", "/health", ""):+ self._send(200, status_payload())+ else:+ self._send(404, {"ok": False, "error": "not found"})++ def do_POST(self):+ if self.path.rstrip("/") != "/command":+ self._send(404, {"success": False, "error": "not found"})+ return+ try:+ n = int(self.headers.get("Content-Length", 0) or 0)+ body = json.loads(self.rfile.read(n) or b"{}")+ except Exception as e:+ self._send(400, {"success": False, "error": "bad json: %s" % e})+ return+ verb = body.get("command") or body.get("verb") or ""+ args = body.get("args") or {}+ fn = VERBS.get(verb)+ if not fn:+ self._send(404, {"success": False, "error": "unknown verb: %s" % verb,+ "known": list(VERBS)})+ return+ try:+ self._send(200, fn(args))+ except Exception as e:+ self._send(200, {"success": False, "error": "%s: %s" % (type(e).__name__, e)})+++def _parse_port(argv):+ for i, a in enumerate(argv):+ if a == "--port" and i + 1 < len(argv):+ try:+ return int(argv[i + 1])+ except ValueError:+ pass+ if a.startswith("--port="):+ try:+ return int(a.split("=", 1)[1])+ except ValueError:+ pass+ env = os.environ.get("BRIDGE_PORT") or os.environ.get("ADOM_BRIDGE_PORT")+ return int(env) if env else DEFAULT_PORT+++def main():+ port = _parse_port(sys.argv[1:])+ srv = ThreadingHTTPServer(("127.0.0.1", port), Handler)+ sys.stderr.write("rustdesk bridge v%s listening on 127.0.0.1:%d\n" % (BRIDGE_VERSION, port))+ sys.stderr.flush()+ srv.serve_forever()+++if __name__ == "__main__":+ main()
uninstall.shadded+5@@ -0,0 +1,5 @@+#!/bin/sh+set -e++# Undo what install.sh did (remove the symlinks you created).+echo "uninstalled"