voxa-code 0.1.0__py3-none-any.whl
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.
- server/__init__.py +0 -0
- server/apns.py +89 -0
- server/app.py +589 -0
- server/appattest.py +310 -0
- server/appstore.py +141 -0
- server/attested_store.py +60 -0
- server/auth.py +70 -0
- server/ax_controller.py +202 -0
- server/billing.py +177 -0
- server/call_manager.py +91 -0
- server/certs/AppleRootCA-G3.pem +15 -0
- server/certs/Apple_App_Attestation_Root_CA.pem +14 -0
- server/claude_controller.py +156 -0
- server/cli.py +365 -0
- server/cloud_app.py +345 -0
- server/config.py +56 -0
- server/device_registry.py +52 -0
- server/gemini_operator.py +677 -0
- server/hooks.py +202 -0
- server/orchestrator.py +315 -0
- server/push_routes.py +50 -0
- server/ratelimit.py +41 -0
- server/relay.py +157 -0
- server/relay_client.py +89 -0
- server/remote_operator.py +128 -0
- server/session_hub.py +33 -0
- server/terminal_watcher.py +241 -0
- server/terminals.py +510 -0
- server/tmux_controller.py +580 -0
- server/transcript_monitor.py +134 -0
- server/transcripts.py +143 -0
- server/users.py +90 -0
- server/voxa_cloud.py +132 -0
- server/waitlist.py +130 -0
- static/app.js +388 -0
- static/favicon.svg +1 -0
- static/index.html +253 -0
- static/pcm-worklet.js +69 -0
- static/pro.html +29 -0
- static/pro2.html +33 -0
- static/voxa-mark-white.svg +1 -0
- voxa_code-0.1.0.dist-info/METADATA +227 -0
- voxa_code-0.1.0.dist-info/RECORD +47 -0
- voxa_code-0.1.0.dist-info/WHEEL +5 -0
- voxa_code-0.1.0.dist-info/entry_points.txt +2 -0
- voxa_code-0.1.0.dist-info/licenses/LICENSE +21 -0
- voxa_code-0.1.0.dist-info/top_level.txt +2 -0
server/cli.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""`voxa` launcher: start the server + a public tunnel, then print a scannable QR.
|
|
2
|
+
|
|
3
|
+
Run it as `python -m server.cli` (or, once installed, just `voxa`). It:
|
|
4
|
+
1. starts the FastAPI server (uvicorn),
|
|
5
|
+
2. opens a Cloudflare quick tunnel and captures its public URL,
|
|
6
|
+
3. prints a scannable QR + the pairing URL right in the terminal,
|
|
7
|
+
4. cleans both up on Ctrl-C.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import atexit
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import signal
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import threading
|
|
19
|
+
|
|
20
|
+
_CF_URL = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
|
|
21
|
+
|
|
22
|
+
# The hosted Voxa Cloud (relay + metered Gemini + push). Non-secret defaults so
|
|
23
|
+
# `voxa` / `npx voxa` runs with ZERO configuration: no key, no token, no .env.
|
|
24
|
+
# The user's keys never touch the laptop; auth is by pairing with the phone app.
|
|
25
|
+
VOXA_DEFAULT_RELAY = "https://api.voxa.space"
|
|
26
|
+
VOXA_DEFAULT_LIVE_PROXY = "wss://api.voxa.space/live"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _voxa_dir() -> str:
|
|
30
|
+
d = os.path.expanduser("~/.voxa")
|
|
31
|
+
os.makedirs(d, exist_ok=True)
|
|
32
|
+
return d
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _stable_secret(name: str, nbytes: int = 16) -> str:
|
|
36
|
+
"""A value that's STABLE across `voxa` runs (persisted in ~/.voxa/<name>).
|
|
37
|
+
|
|
38
|
+
The pairing code and auth token must not change between launches, otherwise a
|
|
39
|
+
phone that paired once would be orphaned every time the laptop restarts."""
|
|
40
|
+
path = os.path.join(_voxa_dir(), name)
|
|
41
|
+
try:
|
|
42
|
+
with open(path) as f:
|
|
43
|
+
val = f.read().strip()
|
|
44
|
+
if val:
|
|
45
|
+
return val
|
|
46
|
+
except OSError:
|
|
47
|
+
pass
|
|
48
|
+
import secrets
|
|
49
|
+
val = secrets.token_hex(nbytes)
|
|
50
|
+
try:
|
|
51
|
+
with open(path, "w") as f:
|
|
52
|
+
f.write(val)
|
|
53
|
+
os.chmod(path, 0o600)
|
|
54
|
+
except OSError:
|
|
55
|
+
pass
|
|
56
|
+
return val
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _apply_zero_config_defaults() -> None:
|
|
60
|
+
"""Fill env so the laptop client needs nothing typed. Metered mode means the
|
|
61
|
+
cloud holds the Gemini key; a random per-machine auth token gates only the
|
|
62
|
+
laptop's own loopback /ws (the phone reaches us through the relay by code).
|
|
63
|
+
|
|
64
|
+
The auth token and relay code are PERSISTED so a previously-paired phone keeps
|
|
65
|
+
working across laptop restarts (the saved QR stays valid)."""
|
|
66
|
+
env = os.environ
|
|
67
|
+
# Prefer a DIRECT Tailscale link (lowest latency: no relay hop) when it's
|
|
68
|
+
# available and the user hasn't pinned their own relay. Only fall back to the
|
|
69
|
+
# hosted relay default when Tailscale isn't an option (or VOXA_FORCE_RELAY is set).
|
|
70
|
+
# Note: switching transports changes the QR, so a phone paired on the relay must
|
|
71
|
+
# re-scan the Tailscale QR (and vice-versa).
|
|
72
|
+
force_relay = env.get("VOXA_FORCE_RELAY", "").strip().lower() not in ("", "0", "false", "no")
|
|
73
|
+
user_set_relay = bool(env.get("VOXA_RELAY_URL", "").strip())
|
|
74
|
+
if user_set_relay or force_relay or not _tailscale_available():
|
|
75
|
+
env.setdefault("VOXA_RELAY_URL", VOXA_DEFAULT_RELAY)
|
|
76
|
+
# else: leave VOXA_RELAY_URL unset so main() takes the Tailscale direct path.
|
|
77
|
+
env.setdefault("VOXA_LIVE_PROXY", VOXA_DEFAULT_LIVE_PROXY)
|
|
78
|
+
if not env.get("VOXA_AUTH_TOKEN", "").strip():
|
|
79
|
+
env["VOXA_AUTH_TOKEN"] = _stable_secret("auth_token")
|
|
80
|
+
if not env.get("VOXA_RELAY_CODE", "").strip():
|
|
81
|
+
# 16 bytes (128-bit) so the pairing code, which bridges a phone to this
|
|
82
|
+
# laptop, is not brute-forceable. (Persisted, so already-paired phones keep
|
|
83
|
+
# their existing code; only fresh installs get the longer one.)
|
|
84
|
+
env["VOXA_RELAY_CODE"] = _stable_secret("relay_code", nbytes=16)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _print_qr(url: str) -> None:
|
|
88
|
+
import qrcode
|
|
89
|
+
qr = qrcode.QRCode(border=1)
|
|
90
|
+
qr.add_data(url)
|
|
91
|
+
qr.make(fit=True)
|
|
92
|
+
qr.print_ascii(invert=True)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _banner(pairing_url: str) -> None:
|
|
96
|
+
line = "─" * 56
|
|
97
|
+
print(f"\n{line}")
|
|
98
|
+
print(" Voxa is live. Scan this with the Voxa app (or your phone camera):\n")
|
|
99
|
+
_print_qr(pairing_url)
|
|
100
|
+
print(f"\n {pairing_url}")
|
|
101
|
+
print("\n Phone browser works too, just open that URL.")
|
|
102
|
+
print(f" Press Ctrl-C to stop.\n{line}\n", flush=True)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> int:
|
|
106
|
+
from dotenv import load_dotenv
|
|
107
|
+
from server.config import load_config
|
|
108
|
+
|
|
109
|
+
load_dotenv() # a .env in the current directory
|
|
110
|
+
home_env = os.path.expanduser("~/.voxa/.env")
|
|
111
|
+
if os.path.exists(home_env):
|
|
112
|
+
load_dotenv(home_env) # optional overrides for self-hosters
|
|
113
|
+
# Zero-config: hosted relay + metered cloud Gemini + a random local token.
|
|
114
|
+
_apply_zero_config_defaults()
|
|
115
|
+
try:
|
|
116
|
+
cfg = load_config()
|
|
117
|
+
except ValueError as e:
|
|
118
|
+
print(
|
|
119
|
+
f"\nVoxa needs configuration: {e}\n"
|
|
120
|
+
"To point at your own server instead, set VOXA_RELAY_URL / VOXA_LIVE_PROXY\n"
|
|
121
|
+
"in ~/.voxa/.env. The default talks to the hosted Voxa Cloud.\n",
|
|
122
|
+
file=sys.stderr,
|
|
123
|
+
)
|
|
124
|
+
return 1
|
|
125
|
+
port = cfg.port
|
|
126
|
+
relay_url = os.environ.get("VOXA_RELAY_URL", "").strip()
|
|
127
|
+
|
|
128
|
+
if not relay_url and not _which("cloudflared"):
|
|
129
|
+
print(
|
|
130
|
+
"cloudflared is not installed. Install it with:\n"
|
|
131
|
+
" brew install cloudflared\n"
|
|
132
|
+
"(or set VOXA_RELAY_URL to your hosted relay).",
|
|
133
|
+
file=sys.stderr,
|
|
134
|
+
)
|
|
135
|
+
return 1
|
|
136
|
+
|
|
137
|
+
if _port_in_use(port):
|
|
138
|
+
print(
|
|
139
|
+
f"\nPort {port} is already in use, another Voxa may be running.\n"
|
|
140
|
+
f"Stop it first (e.g. `pkill -f 'server.cli'`) or set VOXA_PORT to a free port.\n",
|
|
141
|
+
file=sys.stderr,
|
|
142
|
+
)
|
|
143
|
+
return 1
|
|
144
|
+
|
|
145
|
+
env = {**os.environ, "VOXA_MODE": os.environ.get("VOXA_MODE", "attach")}
|
|
146
|
+
server = subprocess.Popen(
|
|
147
|
+
[sys.executable, "-m", "uvicorn", "server.app:create_app",
|
|
148
|
+
"--factory", "--host", "127.0.0.1", "--port", str(port)],
|
|
149
|
+
env=env,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
active = {"tunnel": None}
|
|
153
|
+
|
|
154
|
+
def cleanup(*_):
|
|
155
|
+
for p in (active["tunnel"], server):
|
|
156
|
+
if p:
|
|
157
|
+
try:
|
|
158
|
+
p.terminate()
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
atexit.register(cleanup)
|
|
163
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
164
|
+
signal.signal(sig, lambda *a: (cleanup(), sys.exit(0)))
|
|
165
|
+
|
|
166
|
+
if not _wait_healthy(port):
|
|
167
|
+
print("server did not come up; not showing a QR.", file=sys.stderr)
|
|
168
|
+
cleanup()
|
|
169
|
+
return 1
|
|
170
|
+
|
|
171
|
+
# Install the GLOBAL Claude Code hook so EVERY Claude session on this machine
|
|
172
|
+
# rings the phone when it finishes or needs input (reliable + terminal-agnostic,
|
|
173
|
+
# unlike screen-scraping). Idempotent; set VOXA_INSTALL_HOOK=0 to skip.
|
|
174
|
+
if os.environ.get("VOXA_INSTALL_HOOK", "1").strip() not in ("0", "false", ""):
|
|
175
|
+
try:
|
|
176
|
+
from server.hooks import install_claude_hook, default_settings_path, hook_url
|
|
177
|
+
install_claude_hook(default_settings_path(),
|
|
178
|
+
hook_url("127.0.0.1", port, cfg.auth_token))
|
|
179
|
+
print(" ✓ Claude hook installed — calls you when any Claude session finishes.")
|
|
180
|
+
except Exception as e:
|
|
181
|
+
print(f" (could not install Claude hook: {e})", file=sys.stderr)
|
|
182
|
+
|
|
183
|
+
# Self-hosted relay mode: dial OUT to your cloud relay (no tunnel/Tailscale).
|
|
184
|
+
if relay_url:
|
|
185
|
+
import asyncio
|
|
186
|
+
import uuid
|
|
187
|
+
from server.relay_client import run_bridge
|
|
188
|
+
https = relay_url.rstrip("/")
|
|
189
|
+
host = https.split("://", 1)[-1]
|
|
190
|
+
ws = ("wss://" if https.startswith("https") else "ws://") + host
|
|
191
|
+
# Stable across restarts (persisted in ~/.voxa/relay_code) so a paired phone
|
|
192
|
+
# keeps working; falls back to a random code only if persistence failed.
|
|
193
|
+
code = os.environ.get("VOXA_RELAY_CODE", "").strip() or uuid.uuid4().hex[:10]
|
|
194
|
+
local_ws = f"ws://127.0.0.1:{port}/ws?token={cfg.auth_token}"
|
|
195
|
+
_banner(f"{https}/?code={code}&token={cfg.auth_token}")
|
|
196
|
+
try:
|
|
197
|
+
asyncio.run(run_bridge(ws, code, local_ws,
|
|
198
|
+
os.environ.get("VOXA_RELAY_TOKEN", "").strip()))
|
|
199
|
+
except KeyboardInterrupt:
|
|
200
|
+
pass
|
|
201
|
+
finally:
|
|
202
|
+
cleanup()
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
# Prefer Tailscale (stable, private, permanent URL — no flaky quick tunnels).
|
|
206
|
+
public_url = _tailscale_url(port)
|
|
207
|
+
if public_url:
|
|
208
|
+
print("Using your Tailscale network (stable, private link).", flush=True)
|
|
209
|
+
atexit.register(lambda: subprocess.run(
|
|
210
|
+
["tailscale", "serve", "--https=443", "off"], capture_output=True))
|
|
211
|
+
if not _wait_public_healthy(public_url, tries=20):
|
|
212
|
+
print("Tailscale URL not reachable; falling back to a tunnel…", file=sys.stderr)
|
|
213
|
+
public_url = None
|
|
214
|
+
|
|
215
|
+
# Otherwise a cloudflare quick tunnel. These are per-instance flaky, so retry
|
|
216
|
+
# until one is actually reachable (and re-run if all fail).
|
|
217
|
+
if not public_url:
|
|
218
|
+
print("Opening secure tunnel…", flush=True)
|
|
219
|
+
for attempt in range(1, 4) if not public_url else []:
|
|
220
|
+
tunnel = subprocess.Popen(
|
|
221
|
+
# 127.0.0.1 (not localhost): localhost may resolve to IPv6 ::1 while
|
|
222
|
+
# uvicorn binds IPv4, which makes the tunnel 502.
|
|
223
|
+
["cloudflared", "tunnel", "--url", f"http://127.0.0.1:{port}"],
|
|
224
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
|
225
|
+
)
|
|
226
|
+
active["tunnel"] = tunnel
|
|
227
|
+
url = _read_tunnel_url(tunnel, timeout=20)
|
|
228
|
+
if url:
|
|
229
|
+
threading.Thread(
|
|
230
|
+
target=lambda t=tunnel: [None for _ in t.stdout], daemon=True
|
|
231
|
+
).start()
|
|
232
|
+
if _wait_public_healthy(url, tries=40):
|
|
233
|
+
public_url = url
|
|
234
|
+
break
|
|
235
|
+
try:
|
|
236
|
+
tunnel.terminate()
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
if attempt < 3:
|
|
240
|
+
print(f" tunnel attempt {attempt} didn't connect, retrying…", flush=True)
|
|
241
|
+
|
|
242
|
+
if not public_url:
|
|
243
|
+
print(
|
|
244
|
+
"\nCouldn't get a working tunnel after 3 tries (cloudflare quick tunnels\n"
|
|
245
|
+
"can be flaky). Re-run `npx voxa`, or set up Tailscale for a permanent,\n"
|
|
246
|
+
"reliable link.\n",
|
|
247
|
+
file=sys.stderr,
|
|
248
|
+
)
|
|
249
|
+
cleanup()
|
|
250
|
+
return 1
|
|
251
|
+
|
|
252
|
+
pairing_url = f"{public_url}/?token={cfg.auth_token}"
|
|
253
|
+
_banner(pairing_url)
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
server.wait()
|
|
257
|
+
except KeyboardInterrupt:
|
|
258
|
+
pass
|
|
259
|
+
finally:
|
|
260
|
+
cleanup()
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _which(name: str) -> bool:
|
|
265
|
+
from shutil import which
|
|
266
|
+
return which(name) is not None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _tailscale_available() -> bool:
|
|
270
|
+
"""True if Tailscale is installed and logged in (has a tailnet DNS name). A cheap
|
|
271
|
+
status-only check (no `serve`), used to decide whether to skip the relay default."""
|
|
272
|
+
if not _which("tailscale"):
|
|
273
|
+
return False
|
|
274
|
+
import json
|
|
275
|
+
try:
|
|
276
|
+
st = subprocess.run(["tailscale", "status", "--json"],
|
|
277
|
+
capture_output=True, text=True, timeout=5)
|
|
278
|
+
if st.returncode != 0:
|
|
279
|
+
return False
|
|
280
|
+
dns = (json.loads(st.stdout).get("Self") or {}).get("DNSName", "").rstrip(".")
|
|
281
|
+
return bool(dns)
|
|
282
|
+
except Exception:
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _tailscale_url(port: int):
|
|
287
|
+
"""If Tailscale is installed and logged in, serve the port over HTTPS on the
|
|
288
|
+
tailnet and return its stable https URL. Returns None to fall back to a tunnel."""
|
|
289
|
+
if not _which("tailscale"):
|
|
290
|
+
return None
|
|
291
|
+
import json
|
|
292
|
+
try:
|
|
293
|
+
st = subprocess.run(["tailscale", "status", "--json"],
|
|
294
|
+
capture_output=True, text=True, timeout=5)
|
|
295
|
+
if st.returncode != 0:
|
|
296
|
+
return None
|
|
297
|
+
dns = (json.loads(st.stdout).get("Self") or {}).get("DNSName", "").rstrip(".")
|
|
298
|
+
if not dns:
|
|
299
|
+
return None
|
|
300
|
+
r = subprocess.run(
|
|
301
|
+
["tailscale", "serve", "--bg", "--https=443", f"http://127.0.0.1:{port}"],
|
|
302
|
+
capture_output=True, text=True, timeout=10,
|
|
303
|
+
)
|
|
304
|
+
if r.returncode != 0:
|
|
305
|
+
return None
|
|
306
|
+
return f"https://{dns}"
|
|
307
|
+
except Exception:
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _read_tunnel_url(proc, timeout: int = 20):
|
|
312
|
+
"""Read cloudflared output (in a thread) until its public URL appears, or give up."""
|
|
313
|
+
result = {"url": None}
|
|
314
|
+
|
|
315
|
+
def reader():
|
|
316
|
+
for line in proc.stdout:
|
|
317
|
+
m = _CF_URL.search(line)
|
|
318
|
+
if m:
|
|
319
|
+
result["url"] = m.group(0)
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
t = threading.Thread(target=reader, daemon=True)
|
|
323
|
+
t.start()
|
|
324
|
+
t.join(timeout)
|
|
325
|
+
return result["url"]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _port_in_use(port: int) -> bool:
|
|
329
|
+
import socket
|
|
330
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
331
|
+
return s.connect_ex(("127.0.0.1", port)) == 0
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _wait_healthy(port: int, tries: int = 25) -> bool:
|
|
335
|
+
import time
|
|
336
|
+
import urllib.request
|
|
337
|
+
for _ in range(tries):
|
|
338
|
+
try:
|
|
339
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{port}/healthz", timeout=1) as r:
|
|
340
|
+
if r.status == 200:
|
|
341
|
+
return True
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
time.sleep(0.3)
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _wait_public_healthy(public_url: str, tries: int = 70) -> bool:
|
|
349
|
+
"""Poll the PUBLIC tunnel URL until it actually responds (the tunnel can take
|
|
350
|
+
several seconds to connect to Cloudflare's edge after printing its URL)."""
|
|
351
|
+
import time
|
|
352
|
+
import urllib.request
|
|
353
|
+
for _ in range(tries):
|
|
354
|
+
try:
|
|
355
|
+
with urllib.request.urlopen(f"{public_url}/healthz", timeout=3) as r:
|
|
356
|
+
if r.status == 200:
|
|
357
|
+
return True
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
time.sleep(0.5)
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
if __name__ == "__main__":
|
|
365
|
+
raise SystemExit(main())
|