qoder-autopilot 0.2.2__tar.gz → 0.3.0__tar.gz

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.
Files changed (51) hide show
  1. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/PKG-INFO +30 -10
  2. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/README.md +22 -9
  3. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/pyproject.toml +11 -2
  4. qoder_autopilot-0.3.0/src/qoder_autopilot/browser/window_tiler.py +246 -0
  5. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/cli.py +50 -0
  6. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/config.py +13 -0
  7. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/ninerouter.py +17 -0
  8. qoder_autopilot-0.3.0/src/qoder_autopilot/relay.py +416 -0
  9. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/user_config.py +10 -0
  10. qoder_autopilot-0.3.0/tests/test_relay.py +196 -0
  11. qoder_autopilot-0.2.2/src/qoder_autopilot/browser/window_tiler.py +0 -117
  12. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/.gitignore +0 -0
  13. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/LICENSE +0 -0
  14. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/docs/architecture.md +0 -0
  15. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/examples/basic_usage.py +0 -0
  16. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/examples/parallel_mode.py +0 -0
  17. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/__init__.py +0 -0
  18. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/__main__.py +0 -0
  19. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/browser/__init__.py +0 -0
  20. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/browser/camoufox.py +0 -0
  21. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/captcha/__init__.py +0 -0
  22. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/captcha/ai_vision.py +0 -0
  23. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/captcha/manual.py +0 -0
  24. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/captcha/opencv_detect.py +0 -0
  25. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/captcha/slider.py +0 -0
  26. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/captcha/solver.py +0 -0
  27. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/credentials.py +0 -0
  28. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/deploy.py +0 -0
  29. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/errors.py +0 -0
  30. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/first_run.py +0 -0
  31. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/identity.py +0 -0
  32. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/logger.py +0 -0
  33. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/oauth.py +0 -0
  34. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/otp.py +0 -0
  35. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/register.py +0 -0
  36. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/temp_mail.py +0 -0
  37. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/package.json +0 -0
  38. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/schema.sql +0 -0
  39. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/scripts/setup.sh +0 -0
  40. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/src/config.js +0 -0
  41. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/src/handlers/api.js +0 -0
  42. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/src/handlers/email.js +0 -0
  43. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/src/index.js +0 -0
  44. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/src/utils.js +0 -0
  45. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/wrangler.toml.example +0 -0
  46. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/tests/conftest.py +0 -0
  47. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/tests/test_config.py +0 -0
  48. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/tests/test_errors.py +0 -0
  49. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/tests/test_identity.py +0 -0
  50. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/tests/test_oauth.py +0 -0
  51. {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/tests/test_otp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qoder-autopilot
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Automated Qoder account registration with anti-detect browser, captcha solving (AI + OpenCV), and 9Router OAuth integration.
5
5
  Project-URL: Homepage, https://github.com/Daivageralda/qoder-autopilot
6
6
  Project-URL: Repository, https://github.com/Daivageralda/qoder-autopilot
@@ -30,14 +30,17 @@ Requires-Dist: pydantic>=2.0
30
30
  Requires-Dist: python-dotenv>=1.0
31
31
  Requires-Dist: requests>=2.28
32
32
  Provides-Extra: all
33
+ Requires-Dist: fastapi>=0.100; extra == 'all'
33
34
  Requires-Dist: numpy>=1.24; extra == 'all'
34
35
  Requires-Dist: openai>=1.0; extra == 'all'
35
36
  Requires-Dist: opencv-python-headless>=4.8; extra == 'all'
37
+ Requires-Dist: uvicorn[standard]>=0.23; extra == 'all'
36
38
  Provides-Extra: captcha
37
39
  Requires-Dist: numpy>=1.24; extra == 'captcha'
38
40
  Requires-Dist: openai>=1.0; extra == 'captcha'
39
41
  Requires-Dist: opencv-python-headless>=4.8; extra == 'captcha'
40
42
  Provides-Extra: dev
43
+ Requires-Dist: fastapi>=0.100; extra == 'dev'
41
44
  Requires-Dist: mypy>=1.0; extra == 'dev'
42
45
  Requires-Dist: numpy>=1.24; extra == 'dev'
43
46
  Requires-Dist: openai>=1.0; extra == 'dev'
@@ -46,6 +49,10 @@ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
46
49
  Requires-Dist: pytest-cov>=4.0; extra == 'dev'
47
50
  Requires-Dist: pytest>=7.0; extra == 'dev'
48
51
  Requires-Dist: ruff>=0.4; extra == 'dev'
52
+ Requires-Dist: uvicorn[standard]>=0.23; extra == 'dev'
53
+ Provides-Extra: relay
54
+ Requires-Dist: fastapi>=0.100; extra == 'relay'
55
+ Requires-Dist: uvicorn[standard]>=0.23; extra == 'relay'
49
56
  Description-Content-Type: text/markdown
50
57
 
51
58
  # 🤖 Qoder Autopilot
@@ -55,11 +62,17 @@ Automated [Qoder](https://qoder.com) account registration with anti-detect brows
55
62
  > Register Qoder accounts → solve captchas → verify OTP → auto-connect to 9Router. All in one command.
56
63
 
57
64
  [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
65
+ [![PyPI version](https://img.shields.io/pypi/v/qoder-autopilot.svg)](https://pypi.org/project/qoder-autopilot/)
58
66
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
59
67
  [![Camoufox](https://img.shields.io/badge/browser-Camoufox-orange.svg)](https://camoufox.com/)
68
+ [![Tests](https://github.com/Daivageralda/qoder-autopilot/actions/workflows/test.yml/badge.svg)](https://github.com/Daivageralda/qoder-autopilot/actions/workflows/test.yml)
60
69
 
61
70
  ---
62
71
 
72
+ > **📦 Published on PyPI** — [pypi.org/project/qoder-autopilot](https://pypi.org/project/qoder-autopilot/)
73
+ >
74
+ > Install with `pip install qoder-autopilot` — no clone needed.
75
+
63
76
  ## ✨ Features
64
77
 
65
78
  - **🦊 Anti-detect Browser** — Uses [Camoufox](https://camoufox.com/) (stealth Firefox fork) with C++-level fingerprinting to bypass bot detection
@@ -76,13 +89,26 @@ Automated [Qoder](https://qoder.com) account registration with anti-detect brows
76
89
 
77
90
  ## 📦 Installation
78
91
 
79
- ### From source (recommended)
92
+ ### Via pip (recommended)
93
+
94
+ ```bash
95
+ # Basic install (manual captcha only)
96
+ pip install qoder-autopilot
97
+
98
+ # With AI captcha solver support
99
+ pip install qoder-autopilot[captcha]
100
+
101
+ # Full install with all extras
102
+ pip install qoder-autopilot[full]
103
+ ```
104
+
105
+ ### From source (development)
80
106
 
81
107
  ```bash
82
108
  git clone https://github.com/Daivageralda/qoder-autopilot.git
83
109
  cd qoder-autopilot
84
110
 
85
- # Basic install (manual captcha only)
111
+ # Basic install
86
112
  pip install -e .
87
113
 
88
114
  # With AI captcha solver
@@ -92,12 +118,6 @@ pip install -e ".[captcha]"
92
118
  pip install -e ".[dev]"
93
119
  ```
94
120
 
95
- ### Via pip
96
-
97
- ```bash
98
- pip install qoder-autopilot
99
- ```
100
-
101
121
  ### Post-install
102
122
 
103
123
  ```bash
@@ -213,7 +233,7 @@ qoder-autopilot config reset
213
233
  | `otp-timeout` | Max seconds to wait for OTP | `20` |
214
234
  | `captcha-timeout` | Max seconds for manual captcha | `120` |
215
235
  | `parallel-delay` | Delay between parallel accounts (sec) | `30` |
216
- | `ninerouter-db` | Path to 9Router SQLite DB | `~/.9router/db/data.sqlite` |
236
+ | `ninerouter-db` | Path to 9Router SQLite DB | OS-aware: `~/.9router/db/data.sqlite` (macOS/Linux), `%APPDATA%/9router/db/data.sqlite` (Windows) |
217
237
 
218
238
  ### Via environment variables
219
239
 
@@ -5,11 +5,17 @@ Automated [Qoder](https://qoder.com) account registration with anti-detect brows
5
5
  > Register Qoder accounts → solve captchas → verify OTP → auto-connect to 9Router. All in one command.
6
6
 
7
7
  [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
8
+ [![PyPI version](https://img.shields.io/pypi/v/qoder-autopilot.svg)](https://pypi.org/project/qoder-autopilot/)
8
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
10
  [![Camoufox](https://img.shields.io/badge/browser-Camoufox-orange.svg)](https://camoufox.com/)
11
+ [![Tests](https://github.com/Daivageralda/qoder-autopilot/actions/workflows/test.yml/badge.svg)](https://github.com/Daivageralda/qoder-autopilot/actions/workflows/test.yml)
10
12
 
11
13
  ---
12
14
 
15
+ > **📦 Published on PyPI** — [pypi.org/project/qoder-autopilot](https://pypi.org/project/qoder-autopilot/)
16
+ >
17
+ > Install with `pip install qoder-autopilot` — no clone needed.
18
+
13
19
  ## ✨ Features
14
20
 
15
21
  - **🦊 Anti-detect Browser** — Uses [Camoufox](https://camoufox.com/) (stealth Firefox fork) with C++-level fingerprinting to bypass bot detection
@@ -26,13 +32,26 @@ Automated [Qoder](https://qoder.com) account registration with anti-detect brows
26
32
 
27
33
  ## 📦 Installation
28
34
 
29
- ### From source (recommended)
35
+ ### Via pip (recommended)
36
+
37
+ ```bash
38
+ # Basic install (manual captcha only)
39
+ pip install qoder-autopilot
40
+
41
+ # With AI captcha solver support
42
+ pip install qoder-autopilot[captcha]
43
+
44
+ # Full install with all extras
45
+ pip install qoder-autopilot[full]
46
+ ```
47
+
48
+ ### From source (development)
30
49
 
31
50
  ```bash
32
51
  git clone https://github.com/Daivageralda/qoder-autopilot.git
33
52
  cd qoder-autopilot
34
53
 
35
- # Basic install (manual captcha only)
54
+ # Basic install
36
55
  pip install -e .
37
56
 
38
57
  # With AI captcha solver
@@ -42,12 +61,6 @@ pip install -e ".[captcha]"
42
61
  pip install -e ".[dev]"
43
62
  ```
44
63
 
45
- ### Via pip
46
-
47
- ```bash
48
- pip install qoder-autopilot
49
- ```
50
-
51
64
  ### Post-install
52
65
 
53
66
  ```bash
@@ -163,7 +176,7 @@ qoder-autopilot config reset
163
176
  | `otp-timeout` | Max seconds to wait for OTP | `20` |
164
177
  | `captcha-timeout` | Max seconds for manual captcha | `120` |
165
178
  | `parallel-delay` | Delay between parallel accounts (sec) | `30` |
166
- | `ninerouter-db` | Path to 9Router SQLite DB | `~/.9router/db/data.sqlite` |
179
+ | `ninerouter-db` | Path to 9Router SQLite DB | OS-aware: `~/.9router/db/data.sqlite` (macOS/Linux), `%APPDATA%/9router/db/data.sqlite` (Windows) |
167
180
 
168
181
  ### Via environment variables
169
182
 
@@ -9,7 +9,7 @@ build-backend = "hatchling.build"
9
9
  # ── Project Metadata ──────────────────────────────
10
10
  [project]
11
11
  name = "qoder-autopilot"
12
- version = "0.2.2"
12
+ version = "0.3.0"
13
13
  description = "Automated Qoder account registration with anti-detect browser, captcha solving (AI + OpenCV), and 9Router OAuth integration."
14
14
  readme = "README.md"
15
15
  license = "MIT"
@@ -53,9 +53,14 @@ captcha = [
53
53
  "numpy>=1.24",
54
54
  "openai>=1.0",
55
55
  ]
56
+ # Relay server for remote 9Router integration
57
+ relay = [
58
+ "fastapi>=0.100",
59
+ "uvicorn[standard]>=0.23",
60
+ ]
56
61
  # Install everything
57
62
  all = [
58
- "qoder-autopilot[captcha]",
63
+ "qoder-autopilot[captcha,relay]",
59
64
  ]
60
65
  # Development dependencies
61
66
  dev = [
@@ -122,6 +127,10 @@ known-first-party = ["qoder_autopilot"]
122
127
  [tool.pytest.ini_options]
123
128
  testpaths = ["tests"]
124
129
  asyncio_mode = "auto"
130
+ norecursedirs = ["src", "node_modules", ".venv", "*.egg-info"]
131
+ python_files = ["test_*.py"]
132
+ python_classes = ["Test*"]
133
+ python_functions = ["test_*"]
125
134
  markers = [
126
135
  "slow: marks tests as slow (deselect with '-m \"not slow\"')",
127
136
  "integration: marks tests that require external services",
@@ -0,0 +1,246 @@
1
+ """
2
+ Qoder Autopilot — Window Grid Tiling
3
+ ======================================
4
+ Tile Camoufox browser windows into a 2×2 grid layout.
5
+ Supports macOS (AppleScript) and Windows (Win32 API via ctypes).
6
+
7
+ Grid layout:
8
+ ┌──────────┬──────────┐
9
+ │ Slot 1 │ Slot 2 │ (top row)
10
+ ├──────────┼──────────┤
11
+ │ Slot 3 │ Slot 4 │ (bottom row)
12
+ └──────────┴──────────┘
13
+
14
+ Windows cycle through slots: 5th window → slot 1, 6th → slot 2, etc.
15
+ """
16
+
17
+ import platform
18
+ import subprocess
19
+
20
+ from ..logger import log
21
+
22
+ _screen_size_cache: tuple[int, int] | None = None
23
+
24
+
25
+ def get_screen_size() -> tuple[int, int]:
26
+ """Get main screen dimensions (cached).
27
+
28
+ macOS: Uses AppleScript (osascript).
29
+ Windows: Uses ctypes Win32 API (GetSystemMetrics).
30
+ Linux: Falls back to 1920×1080.
31
+
32
+ Returns:
33
+ Tuple of (width, height) in pixels.
34
+ """
35
+ global _screen_size_cache
36
+ if _screen_size_cache:
37
+ return _screen_size_cache
38
+
39
+ system = platform.system()
40
+
41
+ if system == "Darwin":
42
+ try:
43
+ result = subprocess.run(
44
+ [
45
+ "osascript",
46
+ "-e",
47
+ 'tell application "Finder" to get bounds of window of desktop',
48
+ ],
49
+ capture_output=True,
50
+ text=True,
51
+ timeout=5,
52
+ )
53
+ parts = [int(x.strip()) for x in result.stdout.strip().split(",")]
54
+ _screen_size_cache = (parts[2], parts[3])
55
+ except Exception:
56
+ _screen_size_cache = (1920, 1080)
57
+
58
+ elif system == "Windows":
59
+ try:
60
+ import ctypes
61
+
62
+ user32 = ctypes.windll.user32 # type: ignore[attr-defined]
63
+ SM_CXSCREEN = 0 # noqa: N806
64
+ SM_CYSCREEN = 1 # noqa: N806
65
+ w = user32.GetSystemMetrics(SM_CXSCREEN)
66
+ h = user32.GetSystemMetrics(SM_CYSCREEN)
67
+ _screen_size_cache = (w, h)
68
+ except Exception:
69
+ _screen_size_cache = (1920, 1080)
70
+
71
+ else:
72
+ # Linux / other — fallback
73
+ _screen_size_cache = (1920, 1080)
74
+
75
+ return _screen_size_cache
76
+
77
+
78
+ def _tile_macos(sw: int, sh: int) -> None:
79
+ """Tile Camoufox windows on macOS using AppleScript."""
80
+ half_w = sw // 2
81
+ half_h = sh // 2
82
+ menubar = 25
83
+
84
+ # Grid positions: (left, top, right, bottom)
85
+ g = [
86
+ (0, menubar, half_w, half_h + menubar),
87
+ (half_w, menubar, sw, half_h + menubar),
88
+ (0, half_h + menubar, half_w, sh),
89
+ (half_w, half_h + menubar, sw, sh),
90
+ ]
91
+
92
+ grid_str = (
93
+ "{"
94
+ + "{"
95
+ + f"{g[0][0]}, {g[0][1]}, {g[0][2]}, {g[0][3]}"
96
+ + "}, "
97
+ + "{"
98
+ + f"{g[1][0]}, {g[1][1]}, {g[1][2]}, {g[1][3]}"
99
+ + "}, "
100
+ + "{"
101
+ + f"{g[2][0]}, {g[2][1]}, {g[2][2]}, {g[2][3]}"
102
+ + "}, "
103
+ + "{"
104
+ + f"{g[3][0]}, {g[3][1]}, {g[3][2]}, {g[3][3]}"
105
+ + "}"
106
+ + "}"
107
+ )
108
+
109
+ script = f"""
110
+ tell application "camoufox"
111
+ set winCount to count of windows
112
+ set grid to {grid_str}
113
+ repeat with i from 1 to winCount
114
+ set posIdx to ((i - 1) mod 4) + 1
115
+ set bounds of window i to item posIdx of grid
116
+ end repeat
117
+ return winCount
118
+ end tell
119
+ """
120
+
121
+ try:
122
+ result = subprocess.run(
123
+ ["osascript", "-e", script],
124
+ capture_output=True,
125
+ text=True,
126
+ timeout=10,
127
+ )
128
+ count = result.stdout.strip()
129
+ log(f" 🪟 Tiled {count} Camoufox windows into 2×2 grid")
130
+ except Exception as e:
131
+ log(f" ⚠️ Grid tiling failed: {e}", "WARN")
132
+
133
+
134
+ def _tile_windows(sw: int, sh: int) -> None:
135
+ """Tile Camoufox windows on Windows using Win32 API via ctypes.
136
+
137
+ Enumerates all top-level windows, filters by title containing 'camoufox'
138
+ or 'firefox' (Camoufox is Firefox-based), then positions them in a 2×2 grid.
139
+ """
140
+ import ctypes
141
+ from ctypes import wintypes
142
+
143
+ user32 = ctypes.windll.user32 # type: ignore[attr-defined]
144
+
145
+ # Win32 constants
146
+ SWP_NOZORDER = 0x0004 # noqa: N806
147
+ SWP_NOACTIVATE = 0x0010 # noqa: N806
148
+ GW_OWNER = 4 # noqa: N806
149
+ WS_VISIBLE = 0x10000000 # noqa: N806
150
+
151
+ # Grid positions
152
+ half_w = sw // 2
153
+ half_h = sh // 2
154
+ taskbar_h = 48 # approximate Windows taskbar height
155
+
156
+ grid = [
157
+ (0, 0, half_w, half_h), # top-left
158
+ (half_w, 0, half_w, half_h), # top-right
159
+ (0, half_h, half_w, half_h - taskbar_h), # bottom-left
160
+ (half_w, half_h, half_w, half_h - taskbar_h), # bottom-right
161
+ ]
162
+
163
+ # Callback type for EnumWindows
164
+ WNDENUMPROC = ctypes.WINFUNCTYPE( # type: ignore[attr-defined] # noqa: N806
165
+ wintypes.BOOL,
166
+ wintypes.HWND,
167
+ wintypes.LPARAM,
168
+ )
169
+
170
+ found_windows: list[int] = []
171
+
172
+ def enum_callback(hwnd: int, _lparam: int) -> bool:
173
+ """Collect visible top-level windows with Camoufox/Firefox in title."""
174
+ # Skip invisible windows
175
+ style = user32.GetWindowLongW(hwnd, -16) # GWL_STYLE
176
+ if not (style & WS_VISIBLE):
177
+ return True
178
+
179
+ # Skip child/owned windows (only want top-level)
180
+ owner = user32.GetWindow(hwnd, GW_OWNER)
181
+ if owner:
182
+ return True
183
+
184
+ # Get window title
185
+ length = user32.GetWindowTextLengthW(hwnd)
186
+ if length == 0:
187
+ return True
188
+ buf = ctypes.create_unicode_buffer(length + 1)
189
+ user32.GetWindowTextW(hwnd, buf, length + 1)
190
+ title = buf.value.lower()
191
+
192
+ # Match Camoufox windows (title contains 'camoufox' or 'firefox')
193
+ if "camoufox" in title or "firefox" in title:
194
+ found_windows.append(hwnd)
195
+
196
+ return True
197
+
198
+ try:
199
+ # Enumerate all windows
200
+ user32.EnumWindows(WNDENUMPROC(enum_callback), 0)
201
+
202
+ if not found_windows:
203
+ log(" ⚠️ No Camoufox windows found for tiling", "WARN")
204
+ return
205
+
206
+ # Position each window in the grid
207
+ count = 0
208
+ for i, hwnd in enumerate(found_windows):
209
+ slot = i % 4
210
+ x, y, w, h = grid[slot]
211
+
212
+ # SetWindowPos: move and resize
213
+ user32.SetWindowPos(
214
+ hwnd,
215
+ None,
216
+ x,
217
+ y,
218
+ w,
219
+ h,
220
+ SWP_NOZORDER | SWP_NOACTIVATE,
221
+ )
222
+ count += 1
223
+
224
+ log(f" 🪟 Tiled {count} Camoufox windows into 2×2 grid (Windows)")
225
+
226
+ except Exception as e:
227
+ log(f" ⚠️ Windows grid tiling failed: {e}", "WARN")
228
+
229
+
230
+ def tile_all_camoufox_windows() -> None:
231
+ """Tile ALL open Camoufox windows into a 2×2 grid.
232
+
233
+ Call this after all browsers have been launched.
234
+ Supports macOS (AppleScript) and Windows (Win32 API).
235
+ Linux is not supported (falls back to no-op).
236
+ """
237
+ system = platform.system()
238
+ if system not in ("Darwin", "Windows"):
239
+ return
240
+
241
+ sw, sh = get_screen_size()
242
+
243
+ if system == "Darwin":
244
+ _tile_macos(sw, sh)
245
+ elif system == "Windows":
246
+ _tile_windows(sw, sh)
@@ -266,6 +266,10 @@ def main() -> None:
266
266
  deploy_worker()
267
267
  return
268
268
 
269
+ if sub == "relay":
270
+ _handle_relay_command(sys.argv[2:])
271
+ return
272
+
269
273
  # ── First-run wizard ──
270
274
  from .first_run import is_first_run, run_first_run_wizard
271
275
 
@@ -429,3 +433,49 @@ def _handle_config_command(argv: list[str]) -> None:
429
433
  print(f"❌ Unknown command: {cmd}")
430
434
  print("Run 'qoder-autopilot config --help' for usage")
431
435
  sys.exit(1)
436
+
437
+
438
+ def _handle_relay_command(argv: list[str]) -> None:
439
+ """Handle 'qoder-autopilot relay' subcommand."""
440
+ import argparse
441
+
442
+ parser = argparse.ArgumentParser(
443
+ prog="qoder-autopilot relay",
444
+ description="Start relay server for remote 9Router integration",
445
+ )
446
+ parser.add_argument(
447
+ "--host",
448
+ default="0.0.0.0",
449
+ help="Bind host (default: 0.0.0.0)",
450
+ )
451
+ parser.add_argument(
452
+ "--port",
453
+ type=int,
454
+ default=8765,
455
+ help="Bind port (default: 8765)",
456
+ )
457
+ parser.add_argument(
458
+ "--token",
459
+ default=None,
460
+ help="Custom auth token (default: auto-generate)",
461
+ )
462
+ parser.add_argument(
463
+ "--db",
464
+ default=None,
465
+ help="Custom 9Router DB path (default: auto-detect)",
466
+ )
467
+
468
+ if argv and argv[0] in ("-h", "--help"):
469
+ parser.print_help()
470
+ return
471
+
472
+ args = parser.parse_args(argv)
473
+
474
+ from .relay import start_relay
475
+
476
+ start_relay(
477
+ host=args.host,
478
+ port=args.port,
479
+ custom_token=args.token,
480
+ custom_db_path=args.db,
481
+ )
@@ -150,6 +150,14 @@ class Settings(BaseSettings):
150
150
  default_factory=_default_ninerouter_db,
151
151
  description="Path to 9Router SQLite database (OS-aware default)",
152
152
  )
153
+ ninerouter_relay_url: str = Field(
154
+ default="",
155
+ description="Remote relay server URL (e.g., http://myvps:8765)",
156
+ )
157
+ ninerouter_relay_token: str = Field(
158
+ default="",
159
+ description="Auth token for relay server",
160
+ )
153
161
 
154
162
  # ── AI Captcha (optional) ─────────────────────────────────────────────
155
163
  ai_api_key: str = Field(
@@ -203,6 +211,11 @@ class Settings(BaseSettings):
203
211
  db_path = os.path.expanduser(self.ninerouter_db)
204
212
  return os.path.exists(db_path)
205
213
 
214
+ @property
215
+ def has_relay(self) -> bool:
216
+ """Check if remote relay is configured."""
217
+ return bool(self.ninerouter_relay_url and self.ninerouter_relay_token)
218
+
206
219
  @property
207
220
  def ninerouter_db_path(self) -> str:
208
221
  """Expanded path to 9Router SQLite database."""
@@ -31,6 +31,9 @@ def add_to_9router_device(
31
31
  ) -> bool:
32
32
  """Add a Qoder connection to 9Router DB using device token response.
33
33
 
34
+ If a relay URL is configured, sends the token to the remote relay server.
35
+ Otherwise, inserts directly into the local SQLite database.
36
+
34
37
  Args:
35
38
  email: The Qoder account email.
36
39
  display_name: Display name for the connection.
@@ -42,6 +45,20 @@ def add_to_9router_device(
42
45
  Returns:
43
46
  True if successfully inserted, False otherwise.
44
47
  """
48
+ # Check if relay is configured
49
+ if config.settings.has_relay:
50
+ from .relay import send_to_relay
51
+
52
+ return send_to_relay(
53
+ relay_url=config.settings.ninerouter_relay_url,
54
+ relay_token=config.settings.ninerouter_relay_token,
55
+ email=email,
56
+ display_name=display_name,
57
+ device_token_body=device_token_body,
58
+ machine_id=machine_id,
59
+ )
60
+
61
+ # Local insert (existing logic)
45
62
  db = db_path or config.NINEROUTER_DB
46
63
  log("💾 Adding to 9Router DB (device token flow)...")
47
64