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.
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/PKG-INFO +30 -10
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/README.md +22 -9
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/pyproject.toml +11 -2
- qoder_autopilot-0.3.0/src/qoder_autopilot/browser/window_tiler.py +246 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/cli.py +50 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/config.py +13 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/ninerouter.py +17 -0
- qoder_autopilot-0.3.0/src/qoder_autopilot/relay.py +416 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/user_config.py +10 -0
- qoder_autopilot-0.3.0/tests/test_relay.py +196 -0
- qoder_autopilot-0.2.2/src/qoder_autopilot/browser/window_tiler.py +0 -117
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/.gitignore +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/LICENSE +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/docs/architecture.md +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/examples/basic_usage.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/examples/parallel_mode.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/__init__.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/__main__.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/browser/__init__.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/browser/camoufox.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/captcha/__init__.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/captcha/ai_vision.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/captcha/manual.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/captcha/opencv_detect.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/captcha/slider.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/captcha/solver.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/credentials.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/deploy.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/errors.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/first_run.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/identity.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/logger.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/oauth.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/otp.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/register.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/temp_mail.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/package.json +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/schema.sql +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/scripts/setup.sh +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/src/config.js +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/src/handlers/api.js +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/src/handlers/email.js +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/src/index.js +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/src/utils.js +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/src/qoder_autopilot/worker_template/wrangler.toml.example +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/tests/conftest.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/tests/test_config.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/tests/test_errors.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/tests/test_identity.py +0 -0
- {qoder_autopilot-0.2.2 → qoder_autopilot-0.3.0}/tests/test_oauth.py +0 -0
- {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.
|
|
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
|
[](https://www.python.org/downloads/)
|
|
65
|
+
[](https://pypi.org/project/qoder-autopilot/)
|
|
58
66
|
[](https://opensource.org/licenses/MIT)
|
|
59
67
|
[](https://camoufox.com/)
|
|
68
|
+
[](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
|
-
###
|
|
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
|
|
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
|
[](https://www.python.org/downloads/)
|
|
8
|
+
[](https://pypi.org/project/qoder-autopilot/)
|
|
8
9
|
[](https://opensource.org/licenses/MIT)
|
|
9
10
|
[](https://camoufox.com/)
|
|
11
|
+
[](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
|
-
###
|
|
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
|
|
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.
|
|
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
|
|