Natural language network orchestrator. Tell Zeph what to do — it plans a workflow using a local LLM and dispatches commands to every machine on your network simultaneously.
"open up my coding setup"
→ Ollama plans: [{ target: "10.0.0.214", action: "multi", command: "helix,kitty,lazygit" }]
→ dispatched to 10.0.0.214:5000/multi
→ done
"open youtube and my studying stuff"
→ Ollama plans: [{ target: "10.0.0.214", action: "multi", command: "librewolf,helix,nano ~/Documents/notes.md" }]
→ dispatched to 10.0.0.214:5000/multi
→ done
GX10 (server)
├── server/ FastAPI + SQLite + Ollama
├── dashboard/ React dashboard (Vite, served as static)
└── pwa/ Mobile PWA with voice input
Client machines (Arch + Hyprland)
└── client/ Flask agent on port 5000
- Server runs on your main machine. Scans the subnet, manages the device registry, calls Ollama to plan workflows, and dispatches HTTP commands to clients.
- Client agent runs on each Arch machine. Exposes
/bash,/dispatch,/notes,/multiover HTTP. Whitelisted commands only. - Dashboard is a React SPA showing live device list, command log, and system stats. Served from
/by FastAPI. - PWA is a mobile shell at
/pwawith tap-to-speak voice input via Web Speech API.
Server machine
- Python 3.10+
- Ollama running locally with
qwen3-coder:30bpulled nmap(for subnet scan, optional — ping sweep is the default)- Node.js 18+ (to build dashboard)
Client machines
- Python 3.10+
- Hyprland (for
hyprctldispatch commands) librewolf(used for all browser opens)alacritty(used to wrap TUI apps)
git clone <repo-url> zeph
cd zephcd server
python -m venv venv
source venv/bin/activate
pip install fastapi uvicorn aiohttp python-dotenv psutil ollamaCreate server/.env:
SUBNET_PREFIX=10.0.0
OLLAMA_MODEL=qwen3-coder:30b
PORT=8000Set SUBNET_PREFIX to the first three octets of your LAN (e.g. 192.168.1, 10.0.0).
Pull the LLM model:
ollama pull qwen3-coder:30bcd dashboard
npm install
npm run buildThe built files land in dashboard/dist/ and are served automatically by FastAPI at /.
cd server
source venv/bin/activate
uvicorn main:app --host 0.0.0.0 --port 8000Dashboard: http://<server-ip>:8000
PWA: http://<server-ip>:8000/pwa
cd client
python -m venv venv
source venv/bin/activate
pip install flask
python agent.pyThe agent listens on port 5000. To auto-start with Hyprland, add to ~/.config/hypr/hyprland.conf:
exec-once = /path/to/client/venv/bin/python /path/to/client/agent.py
sudo pacman -S btop lazygit yazi zellij pgcli mpv grim alacritty
yay -S lazydocker pdfpc pairdrop- Device list — shows all discovered devices. Click a row to set a friendly name and device type.
- Command input — type a natural language command and press Enter. Ollama plans the workflow; the dispatcher fires it.
- Command log — shows each dispatched command with method, endpoint, target IP, and output.
- SCAN button — manually trigger a subnet sweep.
Open http://<server-ip>:8000/pwa on your phone and add to homescreen.
- Tap the mic button to start listening.
- Tap again to stop and send.
- Type in the text field and tap Send as an alternative.
Voice input requires HTTPS on iOS Safari. On Android Chrome it works over HTTP on LAN.
The LLM understands named setups and freestyle context. Examples:
| Command | What Zeph does |
|---|---|
set up for dev |
helix + kitty + lazygit on new workspace |
set up for studying |
librewolf + helix + notes on new workspace |
morning setup |
btop + librewolf + taskwarrior |
presentation mode |
pdfpc + librewolf, switch to workspace 1 |
hackathon mode |
helix + kitty + lazygit + git pull |
show me what's running |
btop + lazydocker |
open git |
lazygit in alacritty |
docker setup |
lazydocker + kitty |
focus mode |
helix + notes |
The LLM is not limited to these — it infers intent from any natural language description.
| File | Key | Default | Description |
|---|---|---|---|
server/.env |
SUBNET_PREFIX |
192.168.1 |
First three octets of your LAN subnet |
server/.env |
OLLAMA_MODEL |
qwen3-coder:30b |
Ollama model for workflow planning |
server/.env |
PORT |
8000 |
Server port |
server/scanner.py |
SWEEP_SECS |
300 |
Subnet scan interval in seconds |
server/scanner.py |
PING_WORKERS |
50 |
Concurrent ping workers |
client/whitelist.py |
BASH_PREFIXES |
see file | Allowed bash command prefixes |
client/whitelist.py |
MULTI_ALLOWED |
see file | Allowed apps for multi-app workflows |
client/whitelist.py |
HYPRCTL_DISPATCH |
see file | Allowed hyprctl dispatch commands |
| Method | Path | Description |
|---|---|---|
POST |
/command |
Submit natural language command |
GET |
/devices |
List all devices |
PATCH |
/devices/{ip} |
Update friendly name / device type |
GET |
/logs |
Recent command logs |
POST |
/scan |
Trigger manual subnet sweep |
GET |
/stats |
CPU / RAM / GPU / uptime |
POST |
/notes/append |
Push a note to client machines |
GET |
/notes/collect |
Pull notes from all clients |
GET |
/notes/summarize |
Ollama summary of collected notes |
WS |
/ws/devices |
Live device list (3s interval) |
WS |
/ws/logs |
Live command log (2s interval) |
Client agent (port 5000 on each machine):
| Method | Path | Description |
|---|---|---|
POST |
/bash |
Run whitelisted bash command |
POST |
/dispatch |
Run whitelisted hyprctl dispatch |
POST |
/multi |
Open up to 4 apps on new workspace |
GET |
/notes |
Read notes.md |
POST |
/notes |
Append to notes.md |
GET |
/context |
Return usage tracking context |
Run server with hot reload:
cd server && uvicorn main:app --reload --port 8000Run dashboard dev server (proxies API to port 8000):
cd dashboard && npm run devDashboard dev server runs on http://localhost:5173.
-
main.py— FastAPI app, all routes, WebSocket endpoints, static mounts, CORS, lifespan -
db.py— SQLite schema (devices + logs tables), all CRUD functions, idempotent migrations -
scanner.py— async ping sweep + ARP parsing, Linux flags, subnet via .env, 5min interval -
ollama_client.py—plan_workflow()+summarize_notes(), device list injected at runtime, improved system prompt
-
App.jsx— layout, WebSocket connections, stats polling, prop routing -
StatusBar.jsx— logo, online count, clock, status pill, SCAN refresh button -
DeviceList.jsx— device rows, status badges, friendly name + device type, inline edit -
CommandLog.jsx— conversation UI, parsed Zeph responses, summary block, CLEAR button -
CommandInput.jsx— terminal-style input, POST to /command, feeds conversation UI -
SystemStats.jsx— CPU/RAM/GPU bars, uptime, animated transitions -
vite.config.js— dev proxy for API + WebSocket + /ping to port 8000 -
package.json— React 18, Vite 5
-
index.html— mobile shell, iOS homescreen meta tags, safe-area insets, apple-touch-icon -
app.js— tap-to-start/stop voice input, Web Speech API, POST to /command -
manifest.json— start_url /pwa, icons array with 192x192 + 512x512 -
icon.png+icon-192.png— generated via Pillow
- Subnet moved to
server/.envvia python-dotenv - Scanner fixed for Linux ping flags + ARP parsing
- Scanner interval changed to 5 minutes
- Manual scan trigger
POST /scanendpoint added - Device familiars — friendly name + device type, stored in SQLite
-
get_named_devices()— only dispatches to devices with friendly name set - Logs table updated — method, endpoint, details columns
- Async HTTP dispatcher using
aiohttp -
dispatch_workflow()— parallel dispatch viaasyncio.gather() - Target resolution — IP, hostname, "all" (named devices), "server"
-
action: "bash"→ POST /bash on client -
action: "hyprctl"→ POST /dispatch on client -
action: "notes"→ POST /notes on client -
action: "summarize"→ GET /notes/summarize on server -
action: "multi"→ POST /multi on client -
action: "lights"→ stub result - 10s timeout per request, broad exception handling
- Log each dispatch result with method, endpoint, details
- Flask HTTP server on port 5000
-
POST /bash— allowed bash commands, --new-window injected, xdg-open replaced, quotes stripped -
POST /dispatch— whitelisted hyprctl dispatch commands -
POST /notes— appends note with timestamp + machine name -
POST /multi— opens up to 4 apps on new workspace -
GET /notes— returns full notes.md -
GET /context— returns usage tracking context
-
HYPRCTL_DISPATCH— workspace, exec, movetoworkspace, togglefloating, fullscreen -
BASH_PREFIXES— librewolf, xdg-open, git, cd -
MULTI_ALLOWED— librewolf, kitty, alacritty, nano, code-oss, helix, btop, lazygit, lazydocker, yazi, zellij, pgcli, taskwarrior, mpv, pdfpc, grim, pairdrop, opencode, xdg-open
- Workspace-first logic — switches to empty workspace before launching app
-
get_best_workspace()— prefers empty, falls back to least occupied -
open_multi()— opens up to 4 apps on same workspace, most important first -
tile_apps()— tiling layout for 2/3/4 apps -
ALACRITTY_WRAPset — all TUI apps wrapped in alacritty automatically
-
usage.json— local usage tracking - App count, last opened, workspace history
-
GET /contextexposes to GX10
-
~/Documents/notes.md— auto-created with header -
append_note()— timestamp + machine name + separator -
read_notes()— returns full contents
-
POST /notes/append— pushes note to named clients -
GET /notes/collect— pulls from all online named clients -
GET /notes/summarize— Ollama summarizes, shown in conversation UI
- Part 7 tested end-to-end via server
- 2/3/4 app combos verified
- App order importance rule added to Ollama system prompt
- Preset workflows: studying, dev, morning, presentation, hackathon, focus
- Freestyle LLM rules — infers intent beyond explicit examples
OpenDrop not viable — AWDL/pkg_resources dependency issues, too fragile for hackathon demo.
- Build
whisper_input.py— local Whisper STT - Capture mic, transcribe, POST to
/command - Wire into
main.pylifespan startup
- Better UX — human readable responses not raw JSON
- Tap-to-start/stop voice button — done
- Generate self-signed cert
- Run uvicorn with SSL
- Add GX10 cert to iPhone trusted certs
- Update PWA manifest + fetch/WebSocket to https/wss
-
.env— SUBNET_PREFIX, OLLAMA_MODEL, PORT -
startup.sh— single boot script for GX10 - systemd service for FastAPI
-
README.md— full setup guide
- End-to-end demo script
- Dashboard on external monitor
- iPhone PWA on homescreen
- Rehearse — target under 90 seconds
- Dashboard sometimes doesn't dispatch until hard refresh — stale WS after server restart. Fix: Ctrl+Shift+R.
- PWA response still raw JSON
- iOS voice requires HTTPS — use Android or dashboard for demo