Files
autofirmer-expanded/clicker/clicker.py
T
Brandon LiandClaude Opus 5 54221bbc0c Add autobuyer page capture, browser extension, and desktop clicker
Builds the pipeline the autobuyer needs: see the page, find an element,
click it.

extension/ — MV3 Chromium extension. Polls /api/autobuyer/status and,
while on, scrapes the target tab's HTML and posts it back. Also serves
locate requests: focuses the window, scrolls the element into view, and
reports its position. host_permissions is scoped to tradeify plus
localhost so it cannot read other sites — an empty target pattern would
otherwise capture whatever tab happened to be active, including banking
or mail.

app/api/autobuyer/ — status toggle, capture store, and the locate request
queue. CORS is open because the extension's origin changes every time an
unpacked extension is reloaded.

app/autobuyer/page.tsx — ON switch, source view (default) and a rendered
view. The render uses sandbox="allow-scripts" without allow-same-origin:
the page's own JS is needed because sites ship content at opacity:0 and
fade it in, but the frame must not reach the dashboard's same-origin API
routes, which serve firm credentials.

clicker/ — Python CLI. Asks the extension where a selector is, adds the
element rect to the window's screen position and the browser chrome
height to get desktop coordinates, then clicks with a human motion model
(curved path, eased velocity, occasional overshoot, dwell before press).
Raises the browser application first, since macOS consumes a click on an
unfocused window rather than delivering it.

Refuses to click when the element is covered by an overlay, when the
coordinates fall off-screen, or when the browser cannot be confirmed
frontmost.

Verified: API round-trips, capture pruning, locate claim-once semantics,
motion geometry and timing, and focus activation — the last two against
stubs, since pyautogui and pyobjc are not installed here. NOT verified
end to end: Chrome is still running a stale build of the extension, so a
locate request has never completed against a real page and no real click
has been sent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 16:15:18 -05:00

224 lines
9.3 KiB
Python

#!/usr/bin/env python3
"""Desktop mouse control for the AutoFirmer autobuyer.
The browser extension can see the DOM but can't move the mouse; this script can
move the mouse but can't see inside Chrome. They meet at the dashboard API:
clicker --POST /api/autobuyer/locate--> "where is button.buy?"
extension raises the window, scrolls the
element into view, measures it
clicker --GET /api/autobuyer/locate--> desktop x,y
clicker moves the real mouse and clicks
Usage:
python clicker.py locate "button.buy" # measure only, no clicking
python clicker.py click "button.buy" # measure, then really click
python clicker.py click ".btn" --index 2 --url "https://tradeify.co/*"
python clicker.py click "button.buy" --dry-run # move the cursor, don't press
Requires the AutoBuyer page switch to be ON — that's the master arming switch.
"""
import argparse
import json
import random
import sys
import time
import urllib.error
import urllib.request
import focus
DEFAULT_API = "http://localhost:3000"
POLL_INTERVAL = 0.25
class DashboardError(RuntimeError):
pass
class Dashboard:
"""Thin client for the autobuyer endpoints."""
def __init__(self, base: str, timeout: float = 10.0):
self.base = base.rstrip("/")
self.timeout = timeout
def _request(self, path: str, method: str = "GET", payload: dict | None = None) -> dict:
data = json.dumps(payload).encode() if payload is not None else None
headers = {"Content-Type": "application/json"} if data else {}
req = urllib.request.Request(self.base + path, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as exc:
body = exc.read().decode(errors="replace")
try:
message = json.loads(body).get("error", body)
except json.JSONDecodeError:
message = body
raise DashboardError(f"{method} {path} -> HTTP {exc.code}: {message}") from None
except urllib.error.URLError as exc:
raise DashboardError(f"Cannot reach {self.base}{exc.reason}") from None
def status(self) -> dict:
return self._request("/api/autobuyer/status")
def locate(self, selector: str, index: int, url_pattern: str, timeout: float) -> dict:
"""Queue a lookup and block until the extension answers it."""
queued = self._request(
"/api/autobuyer/locate",
"POST",
{"selector": selector, "index": index, "urlPattern": url_pattern},
)
request_id = queued["id"]
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
row = self._request(f"/api/autobuyer/locate?id={request_id}")
if row["status"] == "done":
return row["result"]
if row["status"] == "error":
raise DashboardError(f"Extension could not locate it: {row['error']}")
time.sleep(POLL_INTERVAL)
raise DashboardError(
f"No answer within {timeout:g}s. Is the extension installed, is Chrome "
f"running, and is the AutoBuyer switch ON?"
)
def scale_factor(found: dict, override: float | None) -> float:
"""CSS pixels and desktop pixels are the same on macOS and on unscaled Windows,
but Windows display scaling and some Linux setups break that. Compare the
screen size the browser reports against the one the OS reports."""
if override is not None:
return override
try:
import pyautogui
except ImportError:
return 1.0 # `locate` is useful without the mouse library installed
os_width = pyautogui.size().width
css_width = (found.get("screenSize") or {}).get("width")
if not css_width:
return 1.0
ratio = os_width / css_width
# Only trust a clean-ish ratio; anything odd means a multi-monitor layout we
# shouldn't guess at, so fall back to 1:1 and let --scale override.
return ratio if 0.4 < ratio < 4.0 else 1.0
def describe(found: dict, factor: float) -> str:
x, y = found["screen"]["x"] * factor, found["screen"]["y"] * factor
lines = [
f" matched <{found['tag']}> {found['text']!r}"
+ (f" (1 of {found['matchCount']})" if found["matchCount"] > 1 else ""),
f" page {found['url']}",
f" viewport x={found['viewport']['x']:.0f} y={found['viewport']['y']:.0f}"
f" {found['viewport']['width']:.0f}x{found['viewport']['height']:.0f}",
f" desktop x={x:.0f} y={y:.0f}" + (f" (scale {factor:g})" if factor != 1 else ""),
]
if found.get("covered"):
lines.append(f" WARNING something is on top of it: {found['coveredBy']}")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("action", choices=["locate", "click"],
help="locate = measure only; click = measure then click")
parser.add_argument("selector", help="CSS selector of the target element")
parser.add_argument("--index", type=int, default=0, help="which match, if the selector hits several (default 0)")
parser.add_argument("--url", default="", help='Chrome match pattern for the tab, e.g. "https://tradeify.co/*"')
parser.add_argument("--api", default=DEFAULT_API, help=f"dashboard URL (default {DEFAULT_API})")
parser.add_argument("--timeout", type=float, default=20.0, help="seconds to wait for the extension (default 20)")
parser.add_argument("--scale", type=float, default=None, help="CSS-to-desktop pixel ratio (default: auto-detect)")
parser.add_argument("--dry-run", action="store_true", help="move the cursor to the target but do not press")
parser.add_argument("--force", action="store_true", help="click even if something is covering the element")
parser.add_argument("--robotic", action="store_true",
help="straight-line move and instant click, skipping the human motion model")
parser.add_argument("--seed", type=int, default=None,
help="seed the motion RNG so a run is reproducible (for debugging)")
parser.add_argument("--no-activate", action="store_true",
help="do not raise the browser first (the click may be eaten by window activation)")
args = parser.parse_args()
dash = Dashboard(args.api)
try:
if not dash.status().get("enabled"):
print("AutoBuyer capture is OFF — turn it on from the dashboard first.", file=sys.stderr)
return 2
print(f"Locating {args.selector!r} …", flush=True)
found = dash.locate(args.selector, args.index, args.url, args.timeout)
except DashboardError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
if args.action == "locate":
print(describe(found, scale_factor(found, args.scale)))
return 0
if found.get("covered") and not args.force:
print(describe(found, 1.0))
print("\nRefusing to click: the element is covered — the click would hit "
f"{found['coveredBy']} instead. Pass --force to click anyway.", file=sys.stderr)
return 3
try:
import pyautogui
except ImportError:
print("error: pyautogui is not installed — run: pip install -r requirements.txt", file=sys.stderr)
return 1
# Slamming the cursor into a screen corner aborts the script.
pyautogui.FAILSAFE = True
factor = scale_factor(found, args.scale)
x = found["screen"]["x"] * factor
y = found["screen"]["y"] * factor
screen_w, screen_h = pyautogui.size()
if not (0 <= x < screen_w and 0 <= y < screen_h):
print(describe(found, factor))
print(f"\nerror: target ({x:.0f}, {y:.0f}) is off-screen ({screen_w}x{screen_h}). "
f"Is the Chrome window partly off the display, or on a second monitor?", file=sys.stderr)
return 3
print(describe(found, factor))
# A click on a background window is eaten by the window manager activating it,
# so the first attempt silently does nothing. Raise the browser first, and do it
# here rather than before locating: the measurement takes seconds, and focus is
# only required at the moment of the press.
if not args.no_activate:
focused = focus.ensure_frontmost()
print(f" focus {focused.detail}")
if not focused.ok:
print("\nRefusing to click: the browser is not frontmost, so the click "
"would be consumed activating its window instead of pressing the "
"element. Pass --no-activate to override.", file=sys.stderr)
return 3
if args.robotic:
pyautogui.moveTo(x, y, duration=0.25)
if not args.dry_run:
pyautogui.click()
else:
import humanize
rng = random.Random(args.seed) if args.seed is not None else random.Random()
humanize.click(x, y, rng=rng, press=not args.dry_run)
if args.dry_run:
print("\ndry run — cursor moved, no click sent.")
else:
print("\nclicked.")
return 0
if __name__ == "__main__":
sys.exit(main())