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
static/pcm-worklet.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pcm-worklet.js
|
|
3
|
+
* AudioWorkletProcessor that downsamples mic audio to 16 kHz mono Int16 PCM
|
|
4
|
+
* and posts ArrayBuffers to the main thread for WebSocket transmission.
|
|
5
|
+
*
|
|
6
|
+
* The AudioContext sample rate (available as global `sampleRate`) is typically
|
|
7
|
+
* 44100 or 48000 Hz. We downsample to 16000 Hz by accumulating samples and
|
|
8
|
+
* stepping through them with a fixed ratio.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const TARGET_SAMPLE_RATE = 16000;
|
|
12
|
+
|
|
13
|
+
class PCMProcessor extends AudioWorkletProcessor {
|
|
14
|
+
constructor(options) {
|
|
15
|
+
super(options);
|
|
16
|
+
// Accumulation buffer for input samples before downsampling
|
|
17
|
+
this._accumulator = [];
|
|
18
|
+
// Fractional position in the input for the downsampling cursor
|
|
19
|
+
this._phase = 0;
|
|
20
|
+
// Ratio: how many input samples per one output sample
|
|
21
|
+
this._ratio = sampleRate / TARGET_SAMPLE_RATE;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
process(inputs) {
|
|
25
|
+
const input = inputs[0];
|
|
26
|
+
if (!input || !input[0] || input[0].length === 0) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const channelData = input[0]; // Float32Array, mono
|
|
31
|
+
|
|
32
|
+
// Downsample via nearest-neighbour stepping.
|
|
33
|
+
// For each output sample we need, advance the phase by ratio through the input.
|
|
34
|
+
const outputSamples = [];
|
|
35
|
+
for (let i = 0; i < channelData.length; i++) {
|
|
36
|
+
this._accumulator.push(channelData[i]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Process accumulated samples
|
|
40
|
+
while (this._phase < this._accumulator.length) {
|
|
41
|
+
const idx = Math.floor(this._phase);
|
|
42
|
+
// Linear interpolation between adjacent samples for slightly better quality
|
|
43
|
+
const next = Math.min(idx + 1, this._accumulator.length - 1);
|
|
44
|
+
const frac = this._phase - idx;
|
|
45
|
+
const sample = this._accumulator[idx] * (1 - frac) + this._accumulator[next] * frac;
|
|
46
|
+
// Clamp and convert Float32 [-1, 1] to Int16 [-32767, 32767]
|
|
47
|
+
const clamped = Math.max(-1, Math.min(1, sample));
|
|
48
|
+
outputSamples.push(Math.round(clamped * 32767));
|
|
49
|
+
this._phase += this._ratio;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Discard consumed input, keep the fractional remainder
|
|
53
|
+
const consumed = Math.floor(this._phase);
|
|
54
|
+
this._accumulator = this._accumulator.slice(consumed);
|
|
55
|
+
this._phase -= consumed;
|
|
56
|
+
|
|
57
|
+
if (outputSamples.length === 0) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Pack output samples into an Int16 little-endian ArrayBuffer and transfer it
|
|
62
|
+
const int16Buffer = new Int16Array(outputSamples);
|
|
63
|
+
this.port.postMessage(int16Buffer.buffer, [int16Buffer.buffer]);
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
registerProcessor('pcm-processor', PCMProcessor);
|
static/pro.html
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en"><head><meta charset="utf-8"><title>Voxa — professional</title>
|
|
3
|
+
<style>
|
|
4
|
+
body{margin:0;font-family:'Inter',system-ui,sans-serif;background:#f6f6f7;color:#0a0a0b}
|
|
5
|
+
h1{padding:28px 36px 2px;font-weight:600;letter-spacing:-.5px}
|
|
6
|
+
p.sub{padding:0 36px;color:#71717a;margin:0 0 8px}
|
|
7
|
+
.grid{display:flex;flex-wrap:wrap;gap:30px;padding:26px 36px;align-items:flex-end}
|
|
8
|
+
.col{display:flex;flex-direction:column;gap:12px;align-items:center}
|
|
9
|
+
.name{font-size:13px;color:#52525b}
|
|
10
|
+
.card{width:200px;height:200px;border-radius:30px;display:flex;align-items:center;justify-content:center}
|
|
11
|
+
.card img{width:170px;height:170px}
|
|
12
|
+
.sm{width:84px;height:84px;border-radius:18px}.sm img{width:84px;height:84px}
|
|
13
|
+
.fav{width:40px;height:40px;border-radius:10px}.fav img{width:40px;height:40px}
|
|
14
|
+
.wmcard{background:#fff;border:1px solid #ececee;border-radius:20px;padding:34px 40px}
|
|
15
|
+
.wmcard img{width:330px}
|
|
16
|
+
</style></head><body>
|
|
17
|
+
<h1>Voxa — minimal · monochrome</h1>
|
|
18
|
+
<p class="sub">No gradient. Ink on white / white on ink. Voice‑ring mark.</p>
|
|
19
|
+
<div class="grid">
|
|
20
|
+
<div class="col"><div class="name">app icon · light</div><div class="card"><img src="/static/voxa-pro-light.svg"></div></div>
|
|
21
|
+
<div class="col"><div class="name">app icon · dark</div><div class="card"><img src="/static/voxa-pro-dark.svg"></div></div>
|
|
22
|
+
<div class="col"><div class="name">small</div>
|
|
23
|
+
<div class="card sm"><img src="/static/voxa-pro-light.svg"></div>
|
|
24
|
+
<div class="card fav"><img src="/static/voxa-pro-dark.svg"></div></div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="grid">
|
|
27
|
+
<div class="col"><div class="name">wordmark</div><div class="wmcard"><img src="/static/voxa-pro-wordmark.svg"></div></div>
|
|
28
|
+
</div>
|
|
29
|
+
</body></html>
|
static/pro2.html
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en"><head><meta charset="utf-8"><title>Voxa pro</title>
|
|
3
|
+
<style>
|
|
4
|
+
body{margin:0;font-family:'Inter',system-ui,sans-serif;background:#f6f6f7;color:#0a0a0b}
|
|
5
|
+
h1{padding:26px 36px 2px;font-weight:600;letter-spacing:-.5px}
|
|
6
|
+
.row{display:flex;flex-wrap:wrap;gap:26px;padding:14px 36px 4px;align-items:center}
|
|
7
|
+
.opt{font-size:13px;color:#52525b;padding:18px 36px 0;font-weight:600}
|
|
8
|
+
.card{width:180px;height:180px;border-radius:28px;display:flex;align-items:center;justify-content:center}
|
|
9
|
+
.card img{width:150px;height:150px}
|
|
10
|
+
.fav{width:44px;height:44px;border-radius:11px}.fav img{width:44px;height:44px}
|
|
11
|
+
.wmcard{background:#fff;border:1px solid #ececee;border-radius:18px;padding:28px 36px}
|
|
12
|
+
.wmcard img{width:300px}
|
|
13
|
+
.lbl{font-size:12px;color:#71717a;text-align:center;margin-top:6px}
|
|
14
|
+
.col{display:flex;flex-direction:column}
|
|
15
|
+
</style></head><body>
|
|
16
|
+
<h1>Voxa — minimal · professional</h1>
|
|
17
|
+
|
|
18
|
+
<div class="opt">Option A · Voice ring (refined, smooth)</div>
|
|
19
|
+
<div class="row">
|
|
20
|
+
<div class="col"><div class="card"><img src="/static/voxa-ring-light.svg"></div><div class="lbl">light</div></div>
|
|
21
|
+
<div class="col"><div class="card"><img src="/static/voxa-ring-dark.svg"></div><div class="lbl">dark</div></div>
|
|
22
|
+
<div class="col"><div class="card fav"><img src="/static/voxa-ring-dark.svg"></div><div class="lbl">favicon</div></div>
|
|
23
|
+
<div class="col"><div class="wmcard"><img src="/static/voxa-ring-wordmark.svg"></div><div class="lbl">wordmark</div></div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="opt">Option B · V with voice dot (ultra‑minimal)</div>
|
|
27
|
+
<div class="row">
|
|
28
|
+
<div class="col"><div class="card"><img src="/static/voxa-vdot-light.svg"></div><div class="lbl">light</div></div>
|
|
29
|
+
<div class="col"><div class="card"><img src="/static/voxa-vdot-dark.svg"></div><div class="lbl">dark</div></div>
|
|
30
|
+
<div class="col"><div class="card fav"><img src="/static/voxa-vdot-dark.svg"></div><div class="lbl">favicon</div></div>
|
|
31
|
+
<div class="col"><div class="wmcard"><img src="/static/voxa-vdot-wordmark.svg"></div><div class="lbl">wordmark</div></div>
|
|
32
|
+
</div>
|
|
33
|
+
</body></html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><g fill="#FFFFFF"><rect x="250.0" y="107.8" width="12" height="50.2" rx="6.0" transform="rotate(0.00 256 256)"/><rect x="250.0" y="86.5" width="12" height="71.5" rx="6.0" transform="rotate(12.00 256 256)"/><rect x="250.0" y="76.4" width="12" height="81.6" rx="6.0" transform="rotate(24.00 256 256)"/><rect x="250.0" y="80.1" width="12" height="77.9" rx="6.0" transform="rotate(36.00 256 256)"/><rect x="250.0" y="96.6" width="12" height="61.4" rx="6.0" transform="rotate(48.00 256 256)"/><rect x="250.0" y="121.6" width="12" height="36.4" rx="6.0" transform="rotate(60.00 256 256)"/><rect x="250.0" y="107.8" width="12" height="50.2" rx="6.0" transform="rotate(72.00 256 256)"/><rect x="250.0" y="86.5" width="12" height="71.5" rx="6.0" transform="rotate(84.00 256 256)"/><rect x="250.0" y="76.4" width="12" height="81.6" rx="6.0" transform="rotate(96.00 256 256)"/><rect x="250.0" y="80.1" width="12" height="77.9" rx="6.0" transform="rotate(108.00 256 256)"/><rect x="250.0" y="96.6" width="12" height="61.4" rx="6.0" transform="rotate(120.00 256 256)"/><rect x="250.0" y="121.6" width="12" height="36.4" rx="6.0" transform="rotate(132.00 256 256)"/><rect x="250.0" y="107.8" width="12" height="50.2" rx="6.0" transform="rotate(144.00 256 256)"/><rect x="250.0" y="86.5" width="12" height="71.5" rx="6.0" transform="rotate(156.00 256 256)"/><rect x="250.0" y="76.4" width="12" height="81.6" rx="6.0" transform="rotate(168.00 256 256)"/><rect x="250.0" y="80.1" width="12" height="77.9" rx="6.0" transform="rotate(180.00 256 256)"/><rect x="250.0" y="96.6" width="12" height="61.4" rx="6.0" transform="rotate(192.00 256 256)"/><rect x="250.0" y="121.6" width="12" height="36.4" rx="6.0" transform="rotate(204.00 256 256)"/><rect x="250.0" y="107.8" width="12" height="50.2" rx="6.0" transform="rotate(216.00 256 256)"/><rect x="250.0" y="86.5" width="12" height="71.5" rx="6.0" transform="rotate(228.00 256 256)"/><rect x="250.0" y="76.4" width="12" height="81.6" rx="6.0" transform="rotate(240.00 256 256)"/><rect x="250.0" y="80.1" width="12" height="77.9" rx="6.0" transform="rotate(252.00 256 256)"/><rect x="250.0" y="96.6" width="12" height="61.4" rx="6.0" transform="rotate(264.00 256 256)"/><rect x="250.0" y="121.6" width="12" height="36.4" rx="6.0" transform="rotate(276.00 256 256)"/><rect x="250.0" y="107.8" width="12" height="50.2" rx="6.0" transform="rotate(288.00 256 256)"/><rect x="250.0" y="86.5" width="12" height="71.5" rx="6.0" transform="rotate(300.00 256 256)"/><rect x="250.0" y="76.4" width="12" height="81.6" rx="6.0" transform="rotate(312.00 256 256)"/><rect x="250.0" y="80.1" width="12" height="77.9" rx="6.0" transform="rotate(324.00 256 256)"/><rect x="250.0" y="96.6" width="12" height="61.4" rx="6.0" transform="rotate(336.00 256 256)"/><rect x="250.0" y="121.6" width="12" height="36.4" rx="6.0" transform="rotate(348.00 256 256)"/><circle cx="256" cy="256" r="22"/></g></svg>
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: voxa-code
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hands-free voice operator for Claude Code
|
|
5
|
+
Author-email: Ti <voxa@voxa.space>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://voxa.space
|
|
8
|
+
Project-URL: Repository, https://github.com/Ti-03/voxa
|
|
9
|
+
Project-URL: Issues, https://github.com/Ti-03/voxa/issues
|
|
10
|
+
Keywords: claude,claude-code,voice,cli,agent,gemini
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: fastapi>=0.110
|
|
26
|
+
Requires-Dist: uvicorn[standard]>=0.29
|
|
27
|
+
Requires-Dist: websockets>=12
|
|
28
|
+
Requires-Dist: google-genai>=0.3
|
|
29
|
+
Requires-Dist: claude-agent-sdk>=0.1
|
|
30
|
+
Requires-Dist: python-dotenv>=1.0
|
|
31
|
+
Requires-Dist: pyjwt[crypto]>=2.8
|
|
32
|
+
Requires-Dist: httpx[http2]>=0.27
|
|
33
|
+
Requires-Dist: qrcode>=7.4
|
|
34
|
+
Requires-Dist: cbor2>=5.6
|
|
35
|
+
Requires-Dist: pyobjc-framework-Quartz>=10; sys_platform == "darwin"
|
|
36
|
+
Requires-Dist: pyobjc-framework-ApplicationServices>=10; sys_platform == "darwin"
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
40
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# Voxa
|
|
44
|
+
|
|
45
|
+
Voxa lets you call into your laptop from a phone browser, talk to a Gemini Live
|
|
46
|
+
"operator," and have it drive Claude Code by voice.
|
|
47
|
+
|
|
48
|
+
**MVP scope (drive mode only):** pick a working directory by voice, send spoken
|
|
49
|
+
instructions, hear Claude's final result read back. Attach mode, voice
|
|
50
|
+
folder-browsing, and barge-in interruption are V2 backlog items (see
|
|
51
|
+
`docs/superpowers/specs/2026-06-27-loop-design.md` and
|
|
52
|
+
`docs/superpowers/plans/2026-06-27-loop-mvp.md`).
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Prerequisites
|
|
57
|
+
|
|
58
|
+
- **Python 3.11+** on the laptop.
|
|
59
|
+
- **Tailscale** installed and logged in on both the laptop and the phone (free
|
|
60
|
+
personal plan is fine). The phone must be on the same tailnet as the laptop,
|
|
61
|
+
or MagicDNS must be enabled.
|
|
62
|
+
- **A Gemini API key** from [Google AI Studio](https://aistudio.google.com/live)
|
|
63
|
+
with Gemini Live access.
|
|
64
|
+
- **Claude Code logged in** on the laptop (`claude` CLI authenticated). The
|
|
65
|
+
agent SDK reuses your existing Claude Code credentials; no separate
|
|
66
|
+
`ANTHROPIC_API_KEY` is needed unless you prefer to supply one.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Quickstart
|
|
71
|
+
|
|
72
|
+
Install Voxa on the laptop you want to control with one command.
|
|
73
|
+
|
|
74
|
+
**macOS / Linux:**
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
curl -fsSL https://voxa.space/install.sh | sh
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Windows (PowerShell):**
|
|
81
|
+
|
|
82
|
+
```powershell
|
|
83
|
+
irm https://voxa.space/install.ps1 | iex
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Prefer a package runner? These work on any OS:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx voxa-code # Node users
|
|
90
|
+
uvx voxa-code # Python users (or: pipx install voxa-code)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Then start it:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
voxa
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Voxa is zero-config by default: it uses the hosted relay, so there are no API
|
|
100
|
+
keys to set up. `voxa` starts the server and prints a pairing QR code. Scan it
|
|
101
|
+
with the Voxa phone app (or open the printed URL in your phone browser) to
|
|
102
|
+
connect.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Develop from source
|
|
107
|
+
|
|
108
|
+
Contributors who want to hack on Voxa can run it from a checkout instead of the
|
|
109
|
+
published package.
|
|
110
|
+
|
|
111
|
+
### 1. Create and activate the virtual environment
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python3 -m venv .venv
|
|
115
|
+
.venv/bin/pip install -e ".[dev]"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 2. Configure secrets
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
cp .env.example .env
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Open `.env` and fill in:
|
|
125
|
+
|
|
126
|
+
| Key | Value |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `GEMINI_API_KEY` | Your Google AI Studio key |
|
|
129
|
+
| `VOXA_AUTH_TOKEN` | Any random secret string (protects the WebSocket endpoint on your tailnet) |
|
|
130
|
+
|
|
131
|
+
`GEMINI_LIVE_MODEL`, `VOXA_HOST`, and `VOXA_PORT` have sensible defaults and
|
|
132
|
+
can be left as-is.
|
|
133
|
+
|
|
134
|
+
### 3. Start the server
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
bash scripts/serve.sh
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The script:
|
|
141
|
+
1. Starts the Voxa FastAPI server on `127.0.0.1:8787` (or `$VOXA_PORT`).
|
|
142
|
+
2. Calls `tailscale serve` to expose it over HTTPS on your tailnet (required
|
|
143
|
+
because the phone browser needs a secure context for microphone access).
|
|
144
|
+
3. Prints the full HTTPS URL including your auth token.
|
|
145
|
+
|
|
146
|
+
### 4. Connect from the phone
|
|
147
|
+
|
|
148
|
+
Open the printed URL on your phone browser. Tap **Connect**, then speak.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Architecture (brief)
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
Phone browser (static/)
|
|
156
|
+
| HTTPS WebSocket (auth token required)
|
|
157
|
+
v
|
|
158
|
+
FastAPI server (server/app.py)
|
|
159
|
+
| audio bytes (16 kHz PCM)
|
|
160
|
+
v
|
|
161
|
+
GeminiOperator (server/gemini_operator.py) <--> Gemini Live API
|
|
162
|
+
| tool calls (start_claude_session, send_to_claude, …)
|
|
163
|
+
v
|
|
164
|
+
Orchestrator (server/orchestrator.py)
|
|
165
|
+
|
|
|
166
|
+
v
|
|
167
|
+
ClaudeController (server/claude_controller.py) --> Claude Code (agent SDK, bypassPermissions)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Config is loaded from `.env` via `server/config.py`.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Running the test suite
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
.venv/bin/python -m pytest -v
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Expected: 22 tests pass, no warnings.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Manual end-to-end smoke test
|
|
185
|
+
|
|
186
|
+
The smoke test requires a real phone, real Tailscale connectivity, and real API
|
|
187
|
+
keys. Run it against a scratch directory, not a real project.
|
|
188
|
+
|
|
189
|
+
**Before you start:**
|
|
190
|
+
- `.env` is fully filled in (real `GEMINI_API_KEY` and `VOXA_AUTH_TOKEN`).
|
|
191
|
+
- Tailscale is running on both the laptop and the phone.
|
|
192
|
+
- Claude Code is logged in on the laptop.
|
|
193
|
+
|
|
194
|
+
**Procedure:**
|
|
195
|
+
|
|
196
|
+
1. Open a terminal on the laptop and run:
|
|
197
|
+
```bash
|
|
198
|
+
bash scripts/serve.sh
|
|
199
|
+
```
|
|
200
|
+
Wait for the line `Voxa is live. On your phone open: https://...`
|
|
201
|
+
|
|
202
|
+
2. Copy the printed HTTPS URL (it already includes `?token=...`).
|
|
203
|
+
|
|
204
|
+
3. On the phone, open the URL in Safari or Chrome. You should see the Voxa
|
|
205
|
+
interface. Grant microphone permission when prompted.
|
|
206
|
+
|
|
207
|
+
4. Tap **Connect**. The button should change state to indicate an active
|
|
208
|
+
session.
|
|
209
|
+
|
|
210
|
+
5. Speak: "Start a session in `/tmp/loop-smoke` and create a file called
|
|
211
|
+
`hello.txt` that says hi."
|
|
212
|
+
|
|
213
|
+
6. **Verify:**
|
|
214
|
+
- Gemini acknowledges the instruction verbally (you hear a response through
|
|
215
|
+
the phone speaker).
|
|
216
|
+
- On the laptop terminal you see Claude Code start with `bypassPermissions`
|
|
217
|
+
active (no permission prompts appear).
|
|
218
|
+
- After Claude finishes, `/tmp/loop-smoke/hello.txt` exists on the laptop
|
|
219
|
+
and contains `hi`.
|
|
220
|
+
- Gemini speaks the final result back to you.
|
|
221
|
+
|
|
222
|
+
7. To stop: press Ctrl-C in the laptop terminal. The `trap` in `serve.sh` will
|
|
223
|
+
kill the server and tear down `tailscale serve`.
|
|
224
|
+
|
|
225
|
+
**Warning:** Use a throwaway scratch directory (like `/tmp/loop-smoke`) for
|
|
226
|
+
your first smoke test. Claude Code runs with `bypassPermissions`, so it will
|
|
227
|
+
write files without asking.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
server/apns.py,sha256=204SSCPWAKfhIYPNlv4c2Ysj4TutB7kJt7Vj6hoNhlk,3618
|
|
3
|
+
server/app.py,sha256=Qjm6XhI3_Vfz79B2z8L-jD6z8Qw2G0XwksTeA9WXVjo,28556
|
|
4
|
+
server/appattest.py,sha256=BOAd4ynkHLBhtUgvEdTjmpHUZNCdOWqmuxYP7tn9qUA,11806
|
|
5
|
+
server/appstore.py,sha256=SJacKxCD8PubOqZorYW1Gb_CvkAaI3m6ilK9iTxGfI4,5649
|
|
6
|
+
server/attested_store.py,sha256=_nwHV8PZzn8Wc7hXCS7iRe12IoflyNgLWYQxpd4Hraw,2032
|
|
7
|
+
server/auth.py,sha256=v9tCHA9FLtF9BPrXP66-VKcVZedLln4xrWSD-jzVeRk,3004
|
|
8
|
+
server/ax_controller.py,sha256=0cl_gJmLdZOI6Gmua9IzMi-Cm2K9dxaF5SMed8skWfI,7331
|
|
9
|
+
server/billing.py,sha256=WXo8-es8So0ZwG8tzFu3J-FnjskkT73ipS5Wp4dwFIU,7255
|
|
10
|
+
server/call_manager.py,sha256=8PaIAPRBVoqSmY8SKO-0IwRbVMhZeYW9oYUrgin5Rps,3690
|
|
11
|
+
server/claude_controller.py,sha256=lgQ3PaBEP1Y8FTLlsd1skMa9jinXhBJ5jN7KJGvpqjY,5860
|
|
12
|
+
server/cli.py,sha256=KR1jcQ2LPqTak9IbuflxmtI0BxYKp-uaETk2mExL0rE,13399
|
|
13
|
+
server/cloud_app.py,sha256=ZK9BPoCAYLoh2EGgqgIptrIoy_sq87sc4Oy_Zm2Vn1w,16285
|
|
14
|
+
server/config.py,sha256=hVgMFeS6wO59gDXbaBjAyQ_xjiUaUnFD5XNIU6pNS7g,2077
|
|
15
|
+
server/device_registry.py,sha256=SLsNUT1hijilhWdqCsn7tzTfHiKbrRWFp8HNNO-L3TA,1808
|
|
16
|
+
server/gemini_operator.py,sha256=vbUFJhIjJs8ivlV8o646KiwZ22hQ8KWfaOLyvGoLZ3g,36864
|
|
17
|
+
server/hooks.py,sha256=6bHj6kpYzTO4GygL-8mN2M1lmVoHAVe9yywpWq-q1Ow,8073
|
|
18
|
+
server/orchestrator.py,sha256=oWM6GCsSdg-SEqrgXqvTyJ5VaBmbEbfQ3i74EerJNVM,14178
|
|
19
|
+
server/push_routes.py,sha256=_t7YaIXcNFt-KEzJV5xuzLOHgiRrzwXuBOhm26Fxnb0,2103
|
|
20
|
+
server/ratelimit.py,sha256=1JX0Z3arGuB04blaJZZsecoeFiKRcQP6bgm9xFb7PJ0,1609
|
|
21
|
+
server/relay.py,sha256=LkVBdErPiQaBP-Iky5XrVk6jZVtI5Xq8-cGecCizSFI,6084
|
|
22
|
+
server/relay_client.py,sha256=NGNVS_S2bMQJmfhEr2Z6v7bKpOkSlBM2FvxTGg4vBkc,3127
|
|
23
|
+
server/remote_operator.py,sha256=iIm4OwR1O43M9XEU_xXWqEbvaI6AuARUbiAAa5yeMtM,5434
|
|
24
|
+
server/session_hub.py,sha256=U85vu94SlmNjrStT1tS5Ihkgh2yh2UGR9SqLT5mDNf4,1011
|
|
25
|
+
server/terminal_watcher.py,sha256=WGQpvK0P1ggXAVIMTzfbT0OU0MpNwWjnzTv35WG3Fi4,10310
|
|
26
|
+
server/terminals.py,sha256=9xjDUo7jyXoAV3P719oJxRQhuBJztgesHOokNW514UM,17961
|
|
27
|
+
server/tmux_controller.py,sha256=HzmN32ke-IyMAwToME6JxEjTlO-WSUIHkf8MY5Rnc7Y,25453
|
|
28
|
+
server/transcript_monitor.py,sha256=xBjFEcEUcf4Sny5fLMnF0vv8Rb1iMuFgzPr3i6QECFo,4736
|
|
29
|
+
server/transcripts.py,sha256=Ko59HzAu0NSp9Wyr4e-4kMvogH_HQ0emcgacSRnppAE,5214
|
|
30
|
+
server/users.py,sha256=vq518cryA3NeG05BTh6h7YZ2aVHsIkl_BbrWwCeNAGo,3187
|
|
31
|
+
server/voxa_cloud.py,sha256=wiAik-HtQxlT6vqQbqELAvPqUOfBp9vPivp_iXLu4ZU,5588
|
|
32
|
+
server/waitlist.py,sha256=S2w9iD6aBkQ35OTuFmZQCPSO-rdgIV4sOClP13SLGQ8,4750
|
|
33
|
+
server/certs/AppleRootCA-G3.pem,sha256=mjDmYheJjLw__yN1aAO7E1xIa2K10QLokzjzmjBDxU8,847
|
|
34
|
+
server/certs/Apple_App_Attestation_Root_CA.pem,sha256=x3jQmsNB9_2fjzsZ4rgVr2rtStRJDh6SwFyzVSEqUBM,798
|
|
35
|
+
static/app.js,sha256=XUYg19dNspMIsN7RGWEZmCI9xAMdRJT0HzzCd8T_-as,11480
|
|
36
|
+
static/favicon.svg,sha256=JdCUQcrLtuaDdSL5PwaN8x_Q4T_lo5JfJ0hs7--qMAE,3056
|
|
37
|
+
static/index.html,sha256=4y0uGoZ4ZmBtVzO5c0M7ZsiLWXwDTb8-Z6KEXq71cU0,6452
|
|
38
|
+
static/pcm-worklet.js,sha256=RlGxUIVMGSgKJH8qkAGWg2pPAFh3SF9ZcldCjOXrzLE,2440
|
|
39
|
+
static/pro.html,sha256=fxXlopJq4oc9j98KxtUEeRwxnPK9iqRlqXqMY-P6bOg,1699
|
|
40
|
+
static/pro2.html,sha256=fpkaZw5PwP3arncFArh44JVxbU9tGuJQXCCNGyx5QsE,2068
|
|
41
|
+
static/voxa-mark-white.svg,sha256=zJsGK3rnUeRcaMPJavi2KR5k6o6Phpazjwf2ERMxgxA,3000
|
|
42
|
+
voxa_code-0.1.0.dist-info/licenses/LICENSE,sha256=sVPQ_LaBfD73Rag4j42VENF87in8pjUYIauORl6lmUo,1066
|
|
43
|
+
voxa_code-0.1.0.dist-info/METADATA,sha256=sow1S5j23oZ9kslim07kBLQ1Gvk5G_CPNtHU0rofM64,6666
|
|
44
|
+
voxa_code-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
45
|
+
voxa_code-0.1.0.dist-info/entry_points.txt,sha256=BAhKDED9DiBogHH1cwnQoVdfKrdjW4t_KYWSlULoAMo,41
|
|
46
|
+
voxa_code-0.1.0.dist-info/top_level.txt,sha256=6THs7DkS2J_BX0FjF2W-iDl103KUE1k5yy4K_lCH6zc,14
|
|
47
|
+
voxa_code-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Voxa (Ti)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|