Unofficial Android client for running Pi Coding Agent through a small self-hosted REST bridge.
The Android app does not talk to Pi directly. Instead, it connects to server/pi_bridge.py, which runs on a machine where the pi CLI is installed and authenticated.
- English-only Android UI.
- Server URL and API key settings.
- Fetches available Pi models from the bridge.
- Sends prompts to Pi with the selected provider/model.
- Reads Pi session history from local Pi JSONL session files.
- Can open an existing session and continue it.
- Includes an adaptive/vector Android app icon.
Android app
-> HTTPS + Bearer token
-> pi_bridge.py
-> local pi CLI
-> ~/.pi/agent/sessions/*.jsonl
The bridge wraps a few Pi CLI/data operations:
pi --list-modelsfor available models.pi --print --session-id <id> --provider <provider> --model <model> <prompt>for chat.~/.pi/agent/sessionsfor session history.
- Android SDK / Gradle wrapper for building from source.
- Android device or emulator.
- A bridge URL and API key.
- Python 3.10+.
- Pi CLI installed and authenticated.
- Access to the Pi session directory, usually
~/.pi/agent/sessions. - Optional but recommended: HTTPS reverse proxy such as Caddy, Nginx, Cloudflare Tunnel, or ngrok.
For full server configuration steps - Pi CLI setup, bridge API key, local verification, HTTPS reverse proxy, persistence, Android app settings, and troubleshooting - see docs/SERVER_SETUP.md.
On the bridge host:
mkdir -p ~/.config/pi-android-bridge
python3 - <<'PY'
import secrets, pathlib
path = pathlib.Path.home() / '.config/pi-android-bridge/api_key'
path.write_text(secrets.token_urlsafe(32) + '\n')
path.chmod(0o600)
print(path)
PYpython3 server/pi_bridge.pyBy default, the bridge listens on 127.0.0.1:8766.
For local testing, use an SSH tunnel, ngrok, Cloudflare Tunnel, or put an HTTPS reverse proxy in front of the bridge.
Example Caddy reverse proxy:
pi.example.com {
reverse_proxy 127.0.0.1:8766
}Use the resulting base URL in the Android app, for example:
https://pi.example.com
Open the app settings and enter:
- Server URL: your bridge base URL, for example
https://pi.example.com - API key: the value from
~/.config/pi-android-bridge/api_key
Do not commit the real API key or bake it into the APK.
All /api/* endpoints require a bearer token. Curl examples below use --oauth2-bearer "$API_TOKEN", which sends the standard bearer-token authorization header.
GET /health is public and does not require authentication.
If authentication fails, the bridge returns HTTP 401:
{
"error": "unauthorized"
}Set these shell variables for the examples:
BASE_URL=https://pi.example.com
API_TOKEN=replace-with-your-api-keyPublic health check.
Request:
curl "$BASE_URL/health"Response 200:
{
"ok": true,
"service": "pi-android-bridge",
"time": "2026-06-14T22:30:26Z"
}Returns the available provider/model list parsed from pi --list-models.
Request:
curl \
--oauth2-bearer "$API_TOKEN" \
-H "User-Agent: PiAndroidClient/1.0" \
"$BASE_URL/api/models"Response 200:
{
"models": [
{
"provider": "anthropic",
"id": "claude-3-5-haiku-20241022",
"context": "200K",
"max_output": "8.2K",
"thinking": "no",
"images": "yes"
}
]
}Possible errors:
401- missing or wrong bearer key.500-pi --list-modelsfailed; response body contains{ "error": "..." }.
Returns up to 100 most recently modified Pi sessions.
Request:
curl \
--oauth2-bearer "$API_TOKEN" \
-H "User-Agent: PiAndroidClient/1.0" \
"$BASE_URL/api/sessions"Response 200:
{
"sessions": [
{
"id": "019ec6e7-5885-748b-81f3-2a9c09d4d5c0",
"title": "Example prompt title",
"updated_at": "2026-06-14T16:11:50.789Z",
"message_count": 2,
"provider": "anthropic",
"model": "claude-opus-4-8"
}
]
}Notes:
titleis derived from the first user message when possible.updated_atis the session timestamp from the JSONL file when available.providerandmodelare the lastmodel_changevalues seen in the session file.
Possible errors:
401- missing or wrong bearer key.
Returns a single Pi session with messages.
Request:
curl \
--oauth2-bearer "$API_TOKEN" \
-H "User-Agent: PiAndroidClient/1.0" \
"$BASE_URL/api/sessions/019ec6e7-5885-748b-81f3-2a9c09d4d5c0"Response 200:
{
"id": "019ec6e7-5885-748b-81f3-2a9c09d4d5c0",
"title": "Example prompt title",
"updated_at": "2026-06-14T16:11:50.789Z",
"message_count": 2,
"provider": "anthropic",
"model": "claude-opus-4-8",
"messages": [
{
"role": "user",
"text": "Example prompt",
"timestamp": "2026-06-14T16:16:38.284Z"
},
{
"role": "assistant",
"text": "Example response",
"timestamp": "2026-06-14T16:16:47.456Z"
}
]
}Possible errors:
401- missing or wrong bearer key.404- session was not found:
{
"error": "session not found"
}Runs Pi in non-interactive print mode with the selected provider/model.
Request:
curl \
-X POST \
--oauth2-bearer "$API_TOKEN" \
-H "User-Agent: PiAndroidClient/1.0" \
-H "Content-Type: application/json" \
-d '{
"provider": "github-copilot",
"model": "gpt-5.4-mini",
"session_id": "android-smoke-test",
"message": "Respond with exactly: PONG"
}' \
"$BASE_URL/api/chat"Request body:
{
"provider": "github-copilot",
"model": "gpt-5.4-mini",
"session_id": "optional-existing-or-new-session-id",
"message": "User prompt text"
}Fields:
message- required, non-empty prompt text.model- required Pi model id.provider- optional but recommended; when present the bridge passes--provider <provider>topi.session_id- optional. If empty or missing, the bridge generates a UUID and returns it.
Response 200:
{
"ok": true,
"session_id": "android-smoke-test",
"answer": "PONG"
}Possible errors:
400-messageormodelmissing:
{
"error": "message and model are required"
}401- missing or wrong bearer key.502-piexited non-zero:
{
"error": "pi failed",
"exit_code": 1,
"stderr": "...last 4000 chars...",
"stdout": "...last 4000 chars..."
}504-pidid not finish within 600 seconds:
{
"error": "pi timed out"
}500- unexpected bridge exception:
{
"error": "..."
}Environment variables supported by server/pi_bridge.py:
PI_BRIDGE_HOST default: 127.0.0.1
PI_BRIDGE_PORT default: 8766
PI_BRIDGE_KEY_FILE default: ~/.config/pi-android-bridge/api_key
PI_SESSION_ROOT default: ~/.pi/agent/sessions
PI_BIN default: pi
Example:
PI_BRIDGE_HOST=127.0.0.1 \
PI_BRIDGE_PORT=8766 \
PI_BRIDGE_KEY_FILE="$HOME/.config/pi-android-bridge/api_key" \
PI_SESSION_ROOT="$HOME/.pi/agent/sessions" \
python3 server/pi_bridge.pyANDROID_HOME=$HOME/Android/Sdk ./gradlew :app:assembleDebug --console=plain --no-daemonDebug APK output:
app/build/outputs/apk/debug/app-debug.apk
- Treat the bridge API key as a secret.
- Do not expose the bridge directly to the public internet without HTTPS and a strong API key.
- Prefer binding the bridge to
127.0.0.1and exposing it through a reverse proxy or tunnel. - The Android app stores the server URL and API key locally on the device.
- The bridge can run local Pi commands, so anyone with the API key can use your Pi CLI account on that host.
This is a small utility client and bridge, not an official Pi product.