# PreviewAPK — full documentation (llms-full.txt) The complete text of the PreviewAPK documentation and agent guide, concatenated for agents and LLM tools that prefer one file. For the short index version, see https://previewapk.siteport.site/llms.txt Contents: 1. Getting started — https://previewapk.siteport.site/docs/ 2. API reference — https://previewapk.siteport.site/docs/api/ 3. CLI reference — https://previewapk.siteport.site/docs/cli/ 4. MCP server — https://previewapk.siteport.site/docs/mcp/ 5. CI recipes — https://previewapk.siteport.site/docs/ci/ 6. Developer & agent guide — https://previewapk.siteport.site/agents 7. Setup (agent-led onboarding) — https://previewapk.siteport.site/setup.md 8. Prompt library — https://previewapk.siteport.site/docs/prompts/ The agent skill (SKILL.md) lives at https://previewapk.siteport.site/skills/previewapk/SKILL.md and the OpenAPI 3.1 spec at https://previewapk.siteport.site/openapi.yaml. ==================================================================== 1. GETTING STARTED Source: https://previewapk.siteport.site/docs/ ==================================================================== PreviewAPK is the build-to-preview loop for Android: change the app, build, and the build runs on a cloud phone in seconds — no Android Studio, no local SDK, no Play Store upload just to test a feature. You watch and drive it from a browser; your scripts and AI agents drive it through an API, with eyes (screenshots, UI hierarchy) and ears (the app's logs) inside the app they're building — so they verify their own work instead of building blind. This page is the mental model; the rest of the docs are the mechanics. ## The model in four ideas ### 1. Device slots — you pay per device, not per app Your subscription is a set of device slots ($19/month each). A slot is a designated device space on PreviewAPK's infrastructure — Pixel 9 Pro or Galaxy S24 profile, Android 10–15, chosen in the device's settings. Attach an APK to a slot, run it, stop it, attach a different one — every session starts from a fresh phone, and pushing a different app replaces the previous one; nothing carries over. ### 2. Sessions Running a device is a session. It boots in typically 30–60 seconds, streams the real Android screen to a browser, and auto-stops after 15 minutes idle (any input resets the timer). Starting again re-boots the same device with the same app attached. While a session runs, the app's debug log is captured continuously. Watch it live in the portal's Session logs panel under the phone (with an errors-only filter and a download button), pull it from the CLI (`logs`), or let an agent read it via the MCP tool `previewapk_get_logs` — a crash's exact reason is in there. The last chunk stays readable after the session ends. Lines from the phone's own stack (graphics probing, the software renderer, runtime startup notices) are flagged as emulator noise: dimmed in the portal, folded when they repeat, and left out of every errors-only view (portal filter, CLI `--errors`, MCP `errors_only`) — while copies and downloads always carry the complete raw log. ### 3. Driver modes — exactly one driver Every session declares who drives: Mode | Who can tap, swipe, type | Typical use user | You, in the portal | Presenting, manual testing, demos agent | The API (your key): scripts, CI, AI agents | Automated checks, agent self-verification `` `` The other side's input is refused while a session runs — with a plain-language error — and control can be handed over mid-session (handoff) without a reboot. The owner can always stop a session. Links never drive. Every link PreviewAPK issues is view-only. Driving requires your portal login or your API key. A forwarded link can never hand a stranger control of your app — this is a design guarantee, not a setting. ### 4. One kind of link Link | Shape | Lifetime | Use it for Session link | /v/ | Dies when the session ends | Live shares: "watch this run, right now" `` The session link is the only link PreviewAPK issues. It is view-only, and the page it opens carries exactly one QR code — of the link itself, so a viewer can scan it and keep watching on their phone. Nothing on a share page can be downloaded. (An earlier permanent per-device link was removed: a link that never expires is a leak waiting for a timestamp.) Owners do have one file path, and it lives in the portal: Install on my phone mints a 15-minute link (`/i/`) whose QR installs the build on the owner's own phone. It exists because the owner asked, inside their account — never on a share page. ## Get your API key Agent-led (fastest): hand your agent /setup.md — or just have it run `previewapk setup`. It prints a short code; you approve the code on the portal grant page while signed in; the portal mints a normal API key and setup saves it on the agent's machine. The key never passes through a chat. Manual: - Log in to the portal. - Open the Developers — API, CLI & agents card. Your account API key is there, with copy buttons. - Treat it like a password: it can push builds and drive your devices. Send it to tools via the `X-Api-Key` header or the `PREVIEWAPK_API_KEY` environment variable — never in a URL, never in a link you share. ## Push your first build (60 seconds) curl -o previewapk https://previewapk.siteport.site/cli/previewapk.py export PREVIEWAPK_API_KEY= python3 previewapk push app.apk --run # attaches the APK and starts a session python3 previewapk status # poll until "live" python3 previewapk links # the share link for the live session How agents should look: two senses, two jobs. The UI hierarchy (API · CLI `tree` · MCP) gives the screen as elements with labels, bounds, and state — navigate and assert with it. Screenshots give pixels — verify visual correctness with them. Agents that navigate by tree and verify by screenshot are faster, cheaper, and land their taps. Open the session link and the app is running. Full surface: CLI reference, API reference, MCP server, CI recipes. Building with an AI agent? Hand it the agent guide (or /llms.txt) — it's written to be read by one. ==================================================================== 2. API REFERENCE Source: https://previewapk.siteport.site/docs/api/ ==================================================================== Base URL: `https://previewapk.siteport.site`. Everything the portal does, the API does. All responses are JSON unless noted; errors are `{"error": "…"}` with the HTTP status carrying the meaning. ## Authentication Send your account key on every call: X-Api-Key: Calls with the key act as the agent caller. (The portal's browser session cookie acts as the human caller on the same endpoints.) The distinction matters for exactly one thing: driver gating on input. A missing or wrong key returns `401`. ### Key scopes and expiry Every key carries a scope, chosen when the key is created (in the portal, or via the keys API with an admin key): Scope | Can do agent (default) | Everything a key could do before scopes existed: observe, drive, upload, manage devices. readonly | Observe only: state, hierarchy / observe, screenshots, logs, session links, recording status + download, file list / pull. Every driving or mutating call is refused with 403 and "code": "key_readonly". admin | Everything agent can, plus the keys API (create / revoke keys) — the only key-authorized management surface. The portal UI itself stays cookie-only. `` `````` ```` Keys can also expire: never, or after 30 / 90 / 365 days, chosen at creation. An expired key returns `401` with `"code": "key_expired"`. Keys created before scopes existed are `agent` scope with no expiry. The account's key list (portal, and `state` for human callers) shows each key's scope, expiry, and last-used time. ### Activity log Every API-key call is recorded in the account's activity log: time, key, action, device, HTTP result, request id — action names and metadata only, never request bodies or typed text. Entries are kept for 30 days. Human callers see the latest 50 in `GET /portal/api/state` (`activity`) and in the portal's Activity card. ## State ### `GET /portal/api/state` Everything about your account: devices, recent uploads, and (for human callers only) the API key. { "devices": [ { …device object… } ], "recents": [ {"fid": "…", "label": "InkSync", "size": 62914560, …} ] } ### The device object Field | Meaning id, name | Device id (e.g. dev1) and its display name profile | pixel (Pixel 9 Pro, 712×1600) or galaxy (Galaxy S24, 736×1600) android | Android version string, "10"–"15" status | off · starting · live · error driver | user or agent — who holds the wheel in the current (or next) session apk | null, or {label, package, version_name, version_code, sha256, size, filename, icon, ts} for the attached app — a fingerprint of the uploaded bytes — identical bytes, identical fingerprint driver_changed_at, driver_note | When the wheel last changed hands, and why — e.g. "the user took the wheel in the portal at 3:42 PM ET". How an agent learns it was interrupted, instead of guessing. handoff | null, or {pending: true, reason, requested_at} while the driving agent is waiting for a human step (see request-human below). Cleared by the handback or the session's end. finish_report | null, or the agent's filed wrap-up for the live session: {summary, verdict, counts: {duration_s, actions, errors}, recording, sid, ts} — the counts counted by the server from the session's own record. Also sealed into the session archive when the session ends. handback_note, resumed | Agent callers only, delivered exactly once: on the first state read (or input result) after the human hands the wheel back, the device carries the human's handback note and resumed: true — then both clear. Human callers never see or consume them. view_url | Session link while live, else null watching | Current viewer count on the session link idle_left | Seconds until idle auto-stop while live, else null error | Last error string, if any `````` `````` `````` `````````` `````` `````` ```` ```````` `````` `````` `````` `` ```` `` ## Upload a build ### `POST /portal/api/upload` Multipart form: file field `apk`, required text field `device` (device id or name). Each upload is identified as it lands: the package, `versionCode`, and `versionName` are read from the APK's manifest. Uploading bytes the account already has stored reuses the stored copy instead of storing it twice (`"deduped": true`). Uploads attach to the named device, replacing its current app. If the device is running and the caller holds the wheel, the new build is hot-swapped in — installed and relaunched inside the live session, no reboot — and the response gains `"swapped": true`. While the device is starting, the call returns `409`. There is deliberately no default device: a call that doesn't name its device fails (`400`) — agents list devices first, then name one, so a push can never land on the wrong device by assumption. curl -H "X-Api-Key: $KEY" -F apk=@app.apk -F device=dev1 \ https://previewapk.siteport.site/portal/api/upload → {"ok": true, "device_id": "dev1", "deduped": false, "app": {"label": "InkSync", "package": "com.InkSync", "version_name": "1.0.1", "version_code": 101}} ### Install verification & the downgrade rule After any install into a live session (a hot swap, or the install that starts a session), the engine reads the installed package back with `dumpsys` and compares its `versionCode` with the uploaded APK's. A mismatch fails the call with `"code": "install_version_mismatch"` and the expected/installed values in `details` — a swap never silently lands a different build than the one you uploaded. Installing an older `versionCode` over the app already in a live session is refused with `409` and `"code": "version_downgrade"` — Android would refuse it too, but later and less clearly. Re-send with `replace=true` (multipart field on upload/attach, `{"replace": true}` on attach-recent, `--replace` on the CLI, `replace` on the MCP push tool) to uninstall the installed app first and then install the older build; the response says plainly that the uninstalled app's data was deleted. First installs in a fresh session are unaffected. ## Device actions All actions are `POST /portal/api/device//` with a JSON body (or empty). Unknown device → `404`; unknown action → `404`. Action | Body | Does run | {"driver": "agent"|"user"} (optional) | Starts a session. Default driver: agent for API callers, user for the portal. Returns {"ok": true, "status": "starting", "driver": …}. Poll state until live (~30–60s). 409 if already running; 400 if no app is attached. stop | — | Stops the session. The session link dies immediately; the driver resets to user. input | see below | Sends input to the live session. Driver-gated (below). 409 if not running. handoff | {"driver": "agent"|"user", "note": "…"} | Passes the wheel mid-session. 409 if not running; 400 for an unknown driver value. When the human hands the wheel back to the agent, an optional note (max ~300 chars) rides along: it is delivered to the agent exactly once — on its next state read or input result, as handback_note with resumed: true — and the handback clears any pending request-human. request-human | {"reason": "…"} | The driving agent asks the human for a step only a human should do (a password or 2FA code typed on the phone, a payment approval, a CAPTCHA). Records the request on the session — the portal's device card shows "Your agent needs you: " — and answers {ok, handoff, portal_url, message_for_human}: a ready-to-send message for the human, which the agent relays verbatim. A second request updates the pending one's reason. 409 if no session is live; if the human already holds the wheel, the answer is 200 with already_user: true instead of an error. Driver-class: readonly keys are refused. finish | {"summary": "…", "verdict": "passed"|"failed"|"mixed", "end_session": false} | The driving side files the session wrap-up. The answer composes the human's message from the server's own counts (session duration, actions taken, error count — errors are counted by the engine whenever a driving call returns an error; act batches count their failed actions), the app identity, the verdict, the agent's summary, and recording availability, ending with the portal link. The report is stored on the session (finish_report in state) and sealed into the session archive. end_session defaults to false — finishing does not stop the phone; with true the session stops after composing and the wording switches to the session-record form. 409 if no session is live; 400 for a missing summary or unknown verdict. Driver-class: readonly keys are refused, and only the side holding the wheel files it. links | — | Returns {"ok": true, "status", "driver", "driver_changed_at", "driver_note", "handoff", "finish_report", "session_url"}. session_url is null unless live. attach | multipart, file field apk | Same as upload, addressed to this device. On a running device (driver-gated) the app is hot-swapped in live; 409 while starting. attach-recent | {"fid": "…"} | Re-attaches a build from your recents (fids come from state). On a running device (driver-gated) the build is hot-swapped in live. 404 for an unknown fid. remove-apk | — | Detaches the app. Device must be off. settings | see below | Name, model/version. ```````````````` ```` ```` ```````````````` ```````````` ```````````````` ```````` `````` `````` `` `` ### Settings body Key | Effect name | Rename the device (max 40 chars) profile | pixel | galaxy — device must be off (409); unknown value → 400 android | "10"–"15" — device must be off (409); unknown value → 400 `` `````````` `````````` ## Input `POST /portal/api/device//input`. Coordinates are fractions of the screen (0–1), so they survive both device profiles. Values outside 0–1 — pixel coordinates are the classic case — are rejected with a 400 that explains the system; they are never silently clamped into a corner. Body | Does {"action": "tap", "x": 0.5, "y": 0.5} | Tap {"action": "swipe", "x1": …, "y1": …, "x2": …, "y2": …, "dur": 300} | Swipe (dur = duration in ms, optional) {"action": "key", "key": 66} | Android keyevent code (3 = Home, 4 = Back, 66 = Enter, 67 = Delete) {"action": "text", "text": "hello"} | Types text into the focused field (max 500 chars). Fails with 400 when no input is focused — nothing is typed into the void. {"action": "launch"} | Bring the app back to the foreground — after Back exits to the launcher, or the app crashed or was closed. About 2 seconds; no reboot. {"action": "observe"} | A compact numbered snapshot of the screen: snapshot_id, foreground app, keyboard/focus state, screen size, and the useful elements (ref, type, text, desc, id, bounds, center, state flags) in reading order — built for a model's context window, much cheaper than the full tree. {"action": "tap_ref", "snapshot_id": "…", "ref": 3} | Tap the element a snapshot numbered. The server re-finds the element on the current screen first; if it moved or changed, the call fails 409 with "code": "stale_snapshot" instead of tapping whatever sits there now. Observe again and retry with the new ref. {"action": "wait_for", "text": "Welcome", "present": true, "timeout_s": 30} | Poll until an element (text, desc, or id) is present — or gone, with "present": false. Answers {"found": bool, "waited_ms": n}; found: false at the timeout is a result, not an error. {"action": "act", "actions": […], "observe": "ui"} | Run a batch of up to 20 actions in order: tap (fractions, or target by text/id/desc or snapshot ref), swipe, long_press, double_tap, type (clear, submit), key, open_app, close_app, open_url, notifications, wait, wait_for. Stops at the first failure (earlier actions stay done), returns per-action results with timing and error envelopes, and never auto-replays. observe: none | ui | screenshot | both attaches the screen after the batch. {"action": "record_start", "name": "…"} | Start recording the screen (Android screenrecord; one recording caps at 180s). Answers {"ok": true, "rec_id": "rec_…", "started_at": …}; 409 already_recording if one is already running. {"action": "record_stop"} | Stop the recording and pull the MP4 to the server. Answers the recording entry: {ok, rec_id, size, duration_s, started_at, stopped_at}. Download it via GET …/recording/ (below). A recording in progress is stopped and saved automatically when the session ends or the phone reboots. {"action": "record_status"} | {ok, recording: bool, rec_id?, elapsed_s?, last: }. If screenrecord ended on its own (the 180s cap), status finalizes and indexes it first. {"action": "device", "op": "…", …} | Device controls — see the table under Device controls below. Driver-gated like all input. `` ```` `` ```` `` ```` `````` `````````````` `````````````````````````````````````````` ```````` `````` ```` `` ### Device controls (`{"action": "device", "op": …}`) Op | Body | Does timezone | {"tz": "America/New_York"} | Set the device timezone (IANA name, validated). The answer is verified against the device — the reply carries the timezone the device actually reports. locale | {"locale": "de-DE", "package": "…"} | Set an app's language (BCP 47 tag) — the Android per-app locale. package defaults to the session's app. Verified on the device. The app may need a relaunch to pick it up. There is no system-wide locale switch in this API. location | {"lat": 40.7128, "lon": -74.0060} | GPS reports this fix (a mock test provider). Stays until location_clear or the session ends. Android keeps the last-known cached fix readable after a clear, until a real fix replaces it. location_clear | — | Remove the mock GPS provider. file_push | {"path": "Download/fixture.json", "data_b64": "…"} | Write a file into shared storage (max 10 MB). Paths are confined to /sdcard — .. and absolute paths outside it are refused. file_list | {"path": "Download"} | List a folder in shared storage: {entries: [{name, dir, size}]}. file_delete | {"path": "Download/fixture.json"} | Delete a file from shared storage (verified gone). reboot | — | Restart the phone. Same phone, same data — app, files, and settings survive; the app is relaunched to the foreground. Takes under a minute; the stream takes a few seconds to come back. (Restart only through this op — it's the safe restart; the phone comes back with everything in place.) ```` `````` `````` `` ```````` `````` ```` `` No clipboard control: not available on these phones. ### Driver gating Input is accepted only from the side that holds the wheel: an API caller needs `driver == "agent"`; the portal needs `driver == "user"`. The refusal is a `403` that says exactly what's wrong. When the owner takes the wheel in the portal, the refusal carries `"code": "DRIVER_TAKEN_BY_USER"` plus the `driver_note` (when it happened) — so an interrupted agent can report exactly where its run stopped instead of failing mysteriously. Any input also resets the idle timer. ## Recordings & file downloads ### `GET /portal/api/device//recording/` Download a finished recording's MP4 (`video/mp4`). The recording's session must be this device's current session or one of its archived sessions — so recordings stay downloadable after the session ends. Supports `Range` requests (206 + `Content-Range`), so a cut transfer resumes where it stopped; the CLI's `record pull` and the MCP download tool use exactly that. Unknown recording → `404`; file rotated off the server → `410`. ### `GET /portal/api/device//file?path=Download/fixture.json` Pull a file (max 10 MB) from the device's shared storage. Follows the driver gate (it reads device storage, not just the screen). `Range` requests resume like the recording download. Missing file → `404 not_found`; over the cap → `413 file_too_large`. ## Screenshots ### `GET /portal/api/device//screenshot` Returns the current screen as `image/png` (same auth) — the device's real screen (a PNG capture), so the picture shows exactly what the hierarchy describes. `409` if the device isn't running. Capture is retried internally; a `503` (with `"retryable": true`) means the frame is genuinely unavailable right now — retry after a few seconds. The `X-Captured-At` header carries the capture time. (`…/frame` is the same image, used by the portal.) ## UI hierarchy ### `GET /portal/api/device//hierarchy` The current screen as a pruned element tree instead of pixels — from Android's own view hierarchy. Each node carries what an agent needs to act without guessing: `class`, `text`, `desc` (content-description), `id` (resource id), pixel `bounds`, fractional `center`, and state flags (`click`, `scroll`, `edit`, `check`/`checked`, `selected`, `focused`, `enabled`). Pure layout containers are spliced out; the tree caps at 500 elements (`truncated` says if it hit the cap). {"ok": true, "width": 736, "height": 1600, "package": "com.InkSync", "tree": {"class": "FrameLayout", "children": [ {"class": "Button", "desc": "Sign up later", "bounds": [24, 1362, 712, 1436], "center": [0.5, 0.874], "click": true}]}} Like a screenshot, the hierarchy is observation: either caller may fetch it regardless of driver mode. Two rules of thumb: navigate and assert with the tree (fast, cheap, exact), verify visual correctness — clipping, overlap, images — with a screenshot. On Flutter and other semantics-driven UIs, labels arrive in `desc` rather than `text`; selector matching in the CLI and MCP searches both. ## Debug logs ### `GET /portal/api/device//logs` The attached app's log stream (Android logcat) for this session — its prints, warnings, errors, and crash traces, captured from session start and filtered to the app's process (plus the system crash buffer when the app dies). Query parameters: `lines` (default 200, max 2000), `since` (seconds back), `level` (minimum level V/D/I/W/E/F — `E` means errors and crashes only), `q` (substring), `after` (a line `seq` — for polling/following). Response: `{ok, lines:[{seq, t, raw}], latest, count}`. After the session ends, the last 500 lines stay available from the saved tail (the response gains `"ended": true`) until the next session replaces them. Observation, like screenshots: either caller, any driver mode. ### `GET /portal/api/device//logs/sessions` The device's archived session logs: `{ok, sessions: [{sid, started, ended, duration_seconds, action_count, end_reason, app_label, package, driver, lines, live}]}` — one entry per past session, so a crash from an earlier run is still reachable after a new session starts. Ended entries carry the session's `duration_seconds`, the number of input actions executed (`action_count` — individual actions plus each action inside a batch), and how it ended (`end_reason`: `stop`, `idle`, `orphan`, `error`, or `unknown` where the end wasn't observed). ### `GET /portal/api/device//logs/session/` One session's log lines (the live session included). Same query parameters and line shape as `…/logs`; poll the live session with `after=` to follow it. ## Public link endpoints (no auth — the token is the credential) Endpoint | Behavior GET /v/ | The view-only session page. Valid only while that session lives. GET /v//status | {"live": bool}. GET /i/ | An install link minted from the owner's portal ("Install on my phone"): serves the APK to the owner's own phone. Valid 15 minutes from minting. Share pages never serve files. `` ```` `` ## Account, keys & setup ### `POST /portal/api/devices` Create a device in a free slot (either caller). Body: `{"name": "…", "profile": "pixel"|"galaxy", "android": "15"}` — name optional (defaults to the profile's name), profile and Android default to `pixel` / `"15"`. Unknown profile or version → `400`; no free slot → `403`. Answers `{"ok": true, "device": {…}}`. ### `POST /portal/api/keys` · `POST /portal/api/keys/revoke` API-key management — portal session, or an `admin`-scope API key (an `agent` or `readonly` key cannot create or revoke keys: `403`). Create takes `{"name": "…", "scope": "agent"|"readonly"|"admin", "expires_days": 30|90|365}` — scope defaults to `agent`, expiry to never — and returns the key, which is also saved to the account's key list in the portal (viewable there any time). Revoke takes `{"id": }`; unknown or already-revoked id → `404`. Keys are listed in `GET /portal/api/state` for human callers, each with its scope, expiry, and last-used time. ### `POST /portal/api/privacy` Set the account's diagnostic-sharing preference — portal session only. Body: `{"share_diagnostics": true|false}`. When off, the feedback endpoint refuses reports with `403` and clients stop inviting them. ### `POST /portal/api/device//install-link` Mint a 15-minute "Install on my phone" link — portal session only. Body: `{"src": "dev"|"fid"}` (the device's attached app, or a recent upload by fid). Answers the token, the `/i/` URL, and a QR SVG. This is the only file-serving path in the product, and it exists for the owner's own phone — share pages never serve files. ### Agent setup grants — `POST /portal/api/setup/start` · `POST /portal/api/setup/token` The device-code style onboarding behind `previewapk setup` (full walkthrough: /setup.md). `start` (no auth; optional `{"label": "…"}`) creates a grant and returns `{"code": "XXXX-XXXX", "url": "…/portal/agent-grant", "expires_in": 600}` — rate-limited per IP. A signed-in human approves the code on the grant page; approval mints a normal account API key (named `setup — `). `token` polls by code and answers only `{"status": "pending"|"denied"|"expired"|"claimed"}` — except the first poll after approval, which returns the key exactly once. Grants require an existing account: no signup bypass, no Terms bypass. ## Errors Every error is the human `error` string plus the envelope fields where the endpoint provides them: a stable `code` (branch on it — messages may change), `retryable`, a concrete `next` step, and a `request_id` that also arrives as the `X-Request-Id` response header. Ambiguous element selectors return their `candidates` instead of guessing. An error whose outcome is unknown is never marked retryable. Status | Means | What to do 400 | Bad request — not an APK, unknown profile/version/driver, unknown input action | Fix the request; the error field says which 401 | Missing or wrong API key | Check the X-Api-Key header 403 | Driver gating — the other side holds the wheel | handoff, or start the session in your mode 401 key_expired | The API key passed its expiry | Create a new key (portal, or an admin key) 403 key_readonly | A readonly key tried to drive or change something | Observe with this key; drive with an agent-scope key 409 version_downgrade | The upload is older than the app in the live session | Upload a newer build, or re-send with replace=true (uninstalls first; its data is deleted) 502 install_version_mismatch | The installed package's versionCode isn't the uploaded APK's | Check the build you uploaded; the envelope's details carry both values 404 | Unknown device, action, fid, or link token | Re-read state; re-check the URL 409 | State conflict — already running, not running, or device busy for a change that needs it off | Poll state; stop first if needed 502/503 | Device engine hiccup or a screenshot grabbed mid-warm-up | Retry with a few seconds' backoff ```` ```` ```` ```` ```` `````` `````` `` `` ```` Polling: after `run`, poll `GET /portal/api/state` every 3–5 seconds until the device is `live` (typically 30–60s). The API is built for one account's devices, not bulk traffic — a poll every few seconds is plenty; there is no benefit to hammering it. ==================================================================== 3. CLI REFERENCE Source: https://previewapk.siteport.site/docs/cli/ ==================================================================== One stdlib-only Python file. No dependencies, no build step — if a machine has `python3`, the CLI works on it, which makes it a good fit for agent shells and CI jobs. ## Install curl -o previewapk https://previewapk.siteport.site/cli/previewapk.py chmod +x previewapk export PREVIEWAPK_API_KEY= ## Configuration Env var | Meaning PREVIEWAPK_API_KEY | Your account key — or run previewapk setup once and the CLI reads the key it saved at ~/.previewapk/credentials.json (the env var wins when both exist) PREVIEWAPK_DEVICE | Default device id, so you can omit --device PREVIEWAPK_HOST | Base URL override (default https://previewapk.siteport.site) `````` ```` ```` Global flags work in any position: `previewapk --device dev1 status --json` and `previewapk status --device dev1 --json` are the same call. `--json` prints the raw API response — use it in scripts and let agents parse it. ## Commands Command | Does push [--run] [--driver agent|user] | Uploads and attaches the APK. On a running device (you hold the wheel) the build is hot-swapped in live — installed and relaunched, no reboot. With --run, starts a session immediately (default driver agent). Prints the app label/version; with --run, where to fetch the session's share link. start [--driver agent|user] | Starts a session on the device's current app. stop | Stops the session; the session link dies. devices | List the account's devices — name, id, profile, Android version, status, attached app. The starting point on multi-device accounts. use | Select the working device (the cd): later commands talk to it without --device. use alone shows the current selection; start and push also select the device they name. Without a selection, commands refuse — nothing is guessed. status | One line: name, status, attached app — plus the session's share link while live. links | The session link (view-only — the one link there is), the session status, and the current driver. look | A compact numbered snapshot of the screen: snapshot id, foreground app, keyboard state, and the useful elements (ref, type, text, id, flags, center) in reading order. The cheap default way to read the screen — tap an element from it with tap --ref N --snapshot (the server re-checks the element first; stale_snapshot if the screen moved on). wait --text "…" [--gone] [--timeout 30] | Wait until an element (--text, --desc, or --id) appears — or disappears, with --gone. Exit code 3 on timeout. Use after anything that loads instead of sleeping blind. wait --for driver [--timeout 300] | Wait until the human hands the wheel back to the agent (after request-human or any takeover). Exit 0 on handback — and prints the human's handback note if they left one; exit 3 on timeout. request-human "reason" | Ask the human for a step only a human should do (password, 2FA, payment, CAPTCHA): records the request in the portal (the device card shows "Your agent needs you") and prints the exact message to relay to them. Then wait --for driver. finish --summary "…" [--verdict passed|failed|mixed] [--end-session] | File the session wrap-up and print the exact message to relay to the human. The counts in it (duration, actions, errors) are composed by the server from the session's record — never from memory. Default: the session keeps running; --end-session stops it after filing. act | Run a batch of up to 20 actions in order from a JSON file (or stdin): {"actions": […], "observe": "none|ui|screenshot|both"}. Stops at the first failure, prints per-action results, and is never auto-replayed — a dropped connection exits 2 (outcome unknown; look, then resubmit from the first unconfirmed action). record start [--name "…"] · record stop · record status · record pull [file.mp4] [--id rec_…] | Record the screen to an MP4 (screenrecord; one recording caps at 180s). stop pulls the MP4 to the server and reports its size and duration; pull downloads it (resumable — a cut transfer picks up where it stopped). Recordings stay downloadable after the session ends. A recording in progress when the session stops or the phone reboots is saved automatically. device timezone | Set the device timezone (e.g. America/New_York) — the reply is the timezone the device itself reports after the set. device locale [--package …] | Set the session app's language (BCP 47, e.g. de-DE) — the Android per-app locale. Relaunch the app if it doesn't switch live. device location · device location-clear | Make GPS report a mock fix — for testing location-aware flows — and clear it again. The fix lasts until cleared or the session ends. device push [remote] · device pull [local] · device ls [path] · device rm | Move files in the device's shared storage (/sdcard): seed fixtures, pull back what the app produced. 10 MB cap per file; paths can't escape shared storage. device reboot | Restart the phone — same phone, same data (app, files, settings survive), app relaunched to the foreground. Under a minute. The way to test cold-start behavior mid-session. setup [--no-wait] [--label "…"] | Agent-led setup: prints a short code and the portal grant URL; the human approves while signed in, the portal mints a normal API key, and setup saves it to ~/.previewapk/credentials.json (0600) — never printed. --no-wait prints one JSON line ({"step": "sign_in", …}) and exits; re-running setup polls until ready / denied / expired. Full walkthrough: /setup.md. feedback --worked "…" --broke "…" --wish "…" [--rating 1-5] | File a field report — what worked, what broke, what you wish it had, what you worked around. Read and tallied; field reports set the roadmap. Refused (honestly) when the account turned diagnostic sharing off in the portal's Privacy settings. tap | Tap at fractional coordinates (0–1). Out-of-range values (pixels) are refused with an explanation — never silently clamped. tap --text "Log in" | Tap an element by its label — resolved from the UI hierarchy (searches text, then desc). --desc and --id select on those fields; --index N picks among matches. Prefer selectors over coordinates whenever the target has a label. When nothing matches, the error lists the labels that are on screen. tap --field N taps the Nth input field — the reliable way into form fields whose hint text the hierarchy cannot see. logs | The app's debug log for this session. --lines N, --since SEC, --errors, --grep TEXT, and --follow to stream live. After the session ends it prints the saved tail. tree | Print the screen's UI hierarchy as an indented outline (elements, labels, centers, state flags). --json prints the raw tree. swipe [ms] | Swipe between fractional points. key | Android keyevent (3 Home, 4 Back, 66 Enter, 67 Delete). text "" | Type into the focused field. Fails honestly when nothing is focused — tap the field first. shot [file.png] | Save a screenshot of the current screen — the real screen, so it matches the hierarchy (default previewapk-shot.png). handoff agent|user | Pass the wheel mid-session. (Handing back from the portal can carry a note for the agent — the CLI prints it on the next status/wait.) relaunch | Bring the app back to the foreground — after Back exits to the launcher, or the app closed. About 2 seconds, no reboot. ```````` `` `` `` ```````````` `` `` `````` `````````` ```` ```` ```` ```` `````````````` ```` ```` ```` `````````` `` `````````````` `` `` `````````````` ```````````` ```` `` `` `` ```` `` `` ## The agent loop, in shell python3 previewapk push app.apk --run for i in $(seq 1 20); do python3 previewapk status --json | grep -q '"live"' && break sleep 5 done python3 previewapk tap 0.5 0.62 python3 previewapk text "demo@example.com" python3 previewapk shot after-login.png python3 previewapk handoff user # hand the human the wheel python3 previewapk links # paste the session link in the PR/chat python3 previewapk finish --summary "login flow verified end to end" --verdict passed # file the wrap-up; relay its message verbatim Exit codes are honest: a refused tap (driver gating) or a stopped device exits non-zero and prints the API's error message, so scripts can branch on failure instead of guessing. ==================================================================== 4. MCP SERVER Source: https://previewapk.siteport.site/docs/mcp/ ==================================================================== The same 27 tools, two ways in: a hosted MCP endpoint (URL + your API key, nothing to install or keep current) and a local stdio server in one stdlib-only Python file. Agents that speak the Model Context Protocol get PreviewAPK as native tools — including screenshots that return as images inside the conversation. ## Hosted MCP — no install Point your MCP client at `https://previewapk.siteport.site/mcp` with your account API key as a Bearer token. The server speaks MCP Streamable HTTP (2024-11-05 and 2025-03-26) and answers with the same tools, the same receipts, and the same error envelopes as the local server — your key's scope is enforced identically (a read-only key observes here and cannot drive). ### Claude Code claude mcp add --transport http previewapk https://previewapk.siteport.site/mcp \ --header "Authorization: Bearer " ### Cursor / generic remote-MCP clients { "mcpServers": { "previewapk": { "url": "https://previewapk.siteport.site/mcp", "headers": { "Authorization": "Bearer " } } } } ### Codex # ~/.codex/config.toml — with PREVIEWAPK_API_KEY set in the environment [mcp_servers.previewapk] url = "https://previewapk.siteport.site/mcp" bearer_token_env_var = "PREVIEWAPK_API_KEY" How selection works here: at connect, the server issues a session id (`Mcp-Session-Id`) and your working-device selection rides on it — select once with `previewapk_use_device` (or start a device), then drive, exactly like the local server. Two connections never share a selection, even on the same key. Selections live in the server's memory: they survive about two idle hours and are forgotten on a server restart (select again — nothing else is lost). Calls that name their `device` also work with no session at all. Hosted differences — the server cannot read your disk: `previewapk_push_apk` takes `apk_b64` (the file as base64) instead of `path`; `previewapk_device` `push_file` takes `data_b64` instead of a local path, and `pull_file` returns the content as `data_b64`; `previewapk_record_download` returns the MP4 as `data_b64` instead of writing a file. Request bodies cap at 25 MB and inline downloads at ~25 MB — for very large APKs or recordings, the CLI's resumable transfers are the right tool. ## Local install (stdio) curl -o previewapk_mcp.py https://previewapk.siteport.site/mcp/previewapk_mcp.py ### Claude Code claude mcp add previewapk \ --env PREVIEWAPK_API_KEY= \ -- python3 /path/to/previewapk_mcp.py ### Cursor / generic MCP clients { "mcpServers": { "previewapk": { "command": "python3", "args": ["/path/to/previewapk_mcp.py"], "env": { "PREVIEWAPK_API_KEY": "" } } } } ### Google Antigravity PreviewAPK is not in Antigravity's built-in MCP Store — Google curates that shelf itself, and there is no submission path. The raw-config route below takes about two minutes and works the same in the Antigravity IDE, App, and CLI, which all share the one config file (`~/.gemini/config/mcp_config.json`). In the IDE: open the agent panel's … menu → MCP Servers → Manage MCP Servers → View raw config, and add the entry: { "mcpServers": { "previewapk": { "command": "python3", "args": ["/path/to/previewapk_mcp.py"], "env": { "PREVIEWAPK_API_KEY": "" } } } } Save, then click Refresh in the MCP servers panel. Antigravity's agent gets the same 27 tools listed below, screenshots included. The working-device model: an agent session has a working device, like a working directory. `previewapk_use_device` selects it (or `previewapk_start`, which requires naming the device and selects it too); every other tool then talks to the working device without repeating its name, and a per-call `device` argument overrides for one call. With nothing selected, action tools refuse — there is no default device to guess. Each agent session holds its own selection, so two agents on one account never inherit each other's place. ## Tools Tool | Arguments | Returns previewapk_list_devices | — | Every device on the account — name, id, profile, Android, status, attached app, and which one is your working device previewapk_use_device | device | Select the working device (the cd) — later tools talk to it without naming it previewapk_status | device? | Device state (the API's device object) — including the handoff block while the agent waits for a human step, the filed finish_report, and — exactly once after a handback — handback_note + resumed previewapk_push_apk | path, device?, run?, driver? | Attach result + app label/version. On a running device (you hold the wheel) the build is hot-swapped in live — no reboot previewapk_start | device (name or id), driver? (agent default) | Session starting confirmation previewapk_stop | device? | Stopped confirmation previewapk_get_links | device? | {status, driver, driver_changed_at, driver_note, handoff, finish_report, session_url} previewapk_tap | x, y (0–1 fractions), device? | OK / driver-gating error previewapk_swipe | x1, y1, x2, y2, ms?, device? | OK previewapk_press_key | code (Android keyevent), device? | OK previewapk_type_text | text, device? | OK — fails honestly when no field is focused previewapk_relaunch | device? | Bring the app back to the foreground (about 2s, no reboot) previewapk_screenshot | device? | An image of the current screen (PNG content block, the real screen) — the agent can look at it previewapk_get_logs | since_seconds?, lines?, errors_only?, contains?, device? | The app's logcat for this session (or the saved tail after it ends) — check after actions; a crash's exact reason lives here previewapk_hierarchy | device? | The screen as an element tree (text outline) — navigate and assert without image tokens previewapk_tap_text | text, index?, device? | Tap the element with that label (text or content-desc), resolved by the hierarchy previewapk_handoff | driver (agent|user), device? | New driver confirmation previewapk_observe | screenshot?, scale?, device? | A compact numbered snapshot of the screen (snapshot_id, foreground app, keyboard state, elements with refs) — the cheap default read. With screenshot: true, the frame comes back as an image too previewapk_act | actions (max 20), observe?, device? | Run a batch of actions in order; stops at the first failure and returns per-action results. Never replay a batch whose outcome you don't know previewapk_wait_for | condition? (element|driver), text?, desc?, id?, present?, timeout_s?, device? | Wait until an element is present (or gone). {found, waited_ms} — found: false at the timeout is a result, not an error. With condition: "driver": wait until the human hands the wheel back (default timeout 300s) — the result carries the handback note previewapk_request_human | reason, device? | Ask the human for a step only a human should do (password, 2FA, payment, CAPTCHA). Returns message_for_human — relay it verbatim, then wait with previewapk_wait_for condition driver previewapk_finish | summary, verdict? (passed|failed|mixed), end_session? (default false), device? | File the session wrap-up; returns message_for_human with the server's own counts (duration, actions, errors) — relay it verbatim. The report is stored on the session and its archive previewapk_feedback | worked?, broke?, wish?, workaround?, rating?, device? | File a field report — read and tallied, and they set the roadmap. Refused when the account opted out of diagnostic sharing previewapk_record_start | name?, device? | Start recording the screen (MP4; one recording caps at 180s) previewapk_record_stop | device? | Stop the recording; the MP4 is pulled to the server and indexed (rec_id, size, duration) previewapk_record_download | path (local file), rec_id?, device? | Download a finished recording to a local file — resumable; recordings stay downloadable after the session ends. Hosted MCP: returns data_b64 instead of writing a file previewapk_device | action: set_timezone (tz) · set_locale (locale, package?) · set_location (lat, lon) · clear_location · push_file / pull_file (local_path, remote_path; 10 MB cap) · list_files · delete_file · reboot | Device controls for realistic QA: timezone, per-app language, mock GPS fix, fixture files in shared storage, and a data-preserving phone restart. No clipboard action — not available on these phones `` `````` ```````````` `````````` ```````` ```` `````` ```````` `````````````` `````` `````` ```` ```` ```````````` ```` ```````` `````````` `````````` ```````` `````````````````````````` ```````````` `````````````````` `````````````` `````` `````` `````````` ```````````````````````````````````` The workflow that makes this worthwhile: push → start (agent-driven) → screenshot → fix what looks wrong → rebuild → handoff to `user` → present the session link from `previewapk_get_links`. The agent verifies its own UI work against real pixels before a human ever sees it. The agent guide has the full briefing to give your agent. ==================================================================== 5. CI RECIPES Source: https://previewapk.siteport.site/docs/ci/ ==================================================================== The pattern is always the same three steps: build the APK, push it to your device, publish the link. Each run mints a fresh session link, so step three fetches the link of the run you just started — the PR gets the link that belongs to its build, and the link dies when the session ends. ## Setup (once) - Add your API key to your CI secrets as `PREVIEWAPK_API_KEY`. - Know your device id (run `previewapk status` locally, or read `GET /portal/api/state`). ## GitHub Actions name: Android preview on: pull_request: jobs: preview: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: { distribution: temurin, java-version: "17" } - name: Build debug APK run: ./gradlew assembleDebug - name: Push to PreviewAPK env: PREVIEWAPK_API_KEY: ${{ secrets.PREVIEWAPK_API_KEY }} run: | curl -sO https://previewapk.siteport.site/cli/previewapk.py python3 previewapk.py --device dev1 \ push app/build/outputs/apk/debug/app-debug.apk --run --driver user - name: Comment the session link env: PREVIEWAPK_API_KEY: GH_TOKEN: run: | for i in $(seq 1 30); do LINK=$(python3 previewapk.py --device dev1 links --json | python3 -c "import sys,json;print(json.load(sys.stdin).get('session_url') or '')") [ -n "$LINK" ] && break sleep 5 done gh pr comment ${{ github.event.pull_request.number }} --body "📱 **Live preview of this PR's build:** $LINK — view-only, live while the session runs." Notes: `--driver user` boots the session for the human reviewer (view-only link + owner's portal driving). Omit it — or pass `--driver agent` — in workflows where a bot should drive (next recipe). The session link exists only while the session is live; `previewapk links` returns it as `session_url`. ## Agent-in-CI (screenshot gate) An agent job can verify the build before the link is posted: start agent-driven, walk the changed flow, and fail the job if a screenshot check fails. python3 previewapk.py --device dev1 push app.apk --run --driver agent python3 previewapk.py --device dev1 status --json # poll until live python3 previewapk.py --device dev1 shot pr-screen.png # …your agent/assertion step reads pr-screen.png… python3 previewapk.py --device dev1 handoff user # leave it ready for the reviewer ## Any other CI (GitLab, Bitrise, Jenkins…) The CLI step is provider-agnostic — anywhere a shell runs, this runs: export PREVIEWAPK_API_KEY=$YOUR_SECRET curl -sO https://previewapk.siteport.site/cli/previewapk.py python3 previewapk.py push path/to/app.apk --run --driver user No shell? The same push is one multipart `POST /portal/api/upload` — see the API reference. ## Etiquette Pushes to a device are serialized by its live session. If two PR jobs push concurrently, the second push while a session is live gets a `409` — serialize preview jobs per device (e.g. a concurrency group in Actions), or give parallel pipelines separate slots. ==================================================================== 6. DEVELOPER & AGENT GUIDE Source: https://previewapk.siteport.site/agents ==================================================================== PreviewAPK for developers & AI agents — CLI, API, MCP PREVIEWAPK Your agent wrote the code and compiled the APK. Now what? It can't run an emulator, and you don't want to install Android Studio just to look at a button. PreviewAPK gives your agent eyes and ears inside the app it builds — it pushes the build, the app runs on a cloud phone in seconds, and the agent drives it, sees its screen, reads its UI, and hears its logs. It stops building blind: it verifies its own work, and you see the feature the moment it's proven — live, in your browser. ## The loop - The agent builds `app.apk`. - It pushes the build to your device (`previewapk push app.apk --run`). - It starts the session agent-driven, navigates by the UI hierarchy (elements, not pixels), checks screenshots to verify the change visually, and reads the app's logs — a crash arrives with its reason attached. - It reports back: “Built it, tested it — take a look.” You're watching the same live session in your portal; take control any time and tap around yourself. - Anyone else need eyes on it? The session's view-only link is a byproduct of the loop — send it to whoever. Links never drive. ## Two kinds of links Session link | /v/… — one per session, view-only. Minted when a session starts; dies when it ends. Get it from previewapk links (or the links action) once the session is live, and give it to the human: "here's the live app." There is no permanent link — a link that never expires is a leak waiting for a timestamp. ```` Links never drive. Driving needs credentials — you in the portal, or your API key. A session is driven by exactly one side at a time (see driver modes below). ## CLI curl -o previewapk https://previewapk.siteport.site/cli/previewapk.py && chmod +x previewapk ./previewapk setup # approve a short code in the portal; the key is minted # there and saved on this machine — never pasted into a chat # (manual alternative: export PREVIEWAPK_API_KEY= — the env var wins when set) ./previewapk push app.apk --run # upload + start, agent-driven ./previewapk look # compact numbered snapshot of the screen (refs you can tap) ./previewapk shot screen.png # save a screenshot of the live screen (PNG) ./previewapk tap 0.5 0.6 # tap (0..1 screen fractions) ./previewapk text "hello" # type into the focused field ./previewapk handoff user # give the human the wheel ./previewapk links # print the share link for the live session ./previewapk stop Every command supports `--json` for machine-readable output. Coordinates are resolution-independent fractions, so the same script drives any device profile. ## MCP server Give your agent native tools instead of a shell: push, start, tap, type, screenshot (returned as an image the agent can actually see), handoff, stop. curl -o previewapk_mcp.py https://previewapk.siteport.site/mcp/previewapk_mcp.py # Claude Code: claude mcp add previewapk --env PREVIEWAPK_API_KEY= -- python3 /path/to/previewapk_mcp.py 27 tools: `previewapk_list_devices`, `previewapk_use_device`, `previewapk_status`, `previewapk_push_apk`, `previewapk_start`, `previewapk_stop`, `previewapk_get_links`, `previewapk_tap`, `previewapk_swipe`, `previewapk_press_key`, `previewapk_type_text`, `previewapk_relaunch`, `previewapk_screenshot`, `previewapk_handoff`, `previewapk_get_logs`, `previewapk_feedback`, `previewapk_hierarchy`, `previewapk_tap_text`, `previewapk_observe`, `previewapk_act`, `previewapk_wait_for`, `previewapk_request_human`, `previewapk_finish`, `previewapk_record_start`, `previewapk_record_stop`, `previewapk_record_download`, `previewapk_device`. With `PREVIEWAPK_API_KEY` unset, the MCP server reads the key `previewapk setup` saved at `~/.previewapk/credentials.json` — one setup covers both clients. ## When the network lies to you Sandboxed agents sit behind proxies that cut connections mid-stream. The CLI and MCP are built for that: screenshot downloads resume where they were cut (add `--scale 0.5` on the CLI for a quarter-size transfer), reads retry, and when an input's response dies in transit the tool checks the device before reporting — you get landed, not landed, or an honest unknown (CLI exit code 2), never a bare failure for something that worked. Engine inputs are serialized per session, so a retry can't interleave with the original. Navigate by the hierarchy (`tree` / `previewapk_hierarchy`) — it is the most reliable channel there is — and use screenshots for visual proof. ## Field reports — tell us what to build You are one of the few who actually use this tool, and you use it for things we haven't imagined. Before you stop a session, file a field report: what broke, what worked, what you wish it had, and what you had to work around. One call — `previewapk feedback --broke "..." --wish "..."` or the `previewapk_feedback` tool. Reports are read and tallied by the team; the wishes of working agents are the roadmap. ## REST API Everything the CLI and MCP do is a plain HTTPS call with your key in the `X-Api-Key` header: Call | What it does GET /portal/api/state | Devices, attached apps, links, session state POST /portal/api/upload | Multipart: field apk, required field device (there is no default device — a call that doesn't name one fails). Replaces the device's app — on a running device (caller must hold the wheel) the new build is hot-swapped in live, no reboot POST /portal/api/device//run | {"driver":"agent"|"user"} — start the session POST /portal/api/device//input | {"action":"tap","x":.5,"y":.4} · swipe · key (Android keycode) · text GET /portal/api/device//screenshot | Current screen as PNG GET /portal/api/device//hierarchy | Current screen as a UI element tree (labels, bounds, state) — navigate by element, not pixels POST /portal/api/device//handoff | {"driver":"user"|"agent"} — switch who drives, mid-session POST /portal/api/device//stop | End the session `` `````` ```` `````````` `` `` ```` `` ## Driver modes driver=agent — the API caller drives; you watch in the portal, with a Take Control button if you want the wheel. driver=user — you drive in the portal; API input is refused. Sessions auto-stop after 15 idle minutes; any input resets the timer. ## The device model One slot ($19/mo) = one device. Every run starts on a clean phone with exactly your build on it — pushing a different app replaces the previous one and nothing carries over — which keeps previews fast, honest, and free of interference from other apps. Open your portal Back to the homepage Full documentation: getting started · API reference · CLI · MCP · CI recipes · Prompt library Agent-readable version of this page: /llms.txt · Full text: /llms-full.txt · Agent skill: /skills/previewapk/SKILL.md · Setup: /setup.md · API spec: /openapi.yaml ==================================================================== 7. SETUP — AGENT-LED ONBOARDING Source: https://previewapk.siteport.site/setup.md ==================================================================== # Set up PreviewAPK You are an AI coding agent. Your human asked you to set them up with PreviewAPK — a cloud Android phone you can drive (push an APK, start a session, read the screen, tap and type, read the app's logs) so you can run and verify Android apps instead of building blind. Follow these steps in order. The human's only jobs are to approve one browser prompt and to answer one short question at the end. **The human never pastes an API key into this chat** — the key is minted by the portal and saved directly on this machine by the setup command. Reference: the full agent skill lives at https://previewapk.siteport.site/skills/previewapk/SKILL.md — read it before driving a phone. ## Step 0 — Check before installing anything Setup is safe to re-run and resumes where it left off. First check whether this machine is already set up: - Is the PreviewAPK CLI present (`previewapk` on PATH, or a downloaded `previewapk.py`)? - Is `PREVIEWAPK_API_KEY` set, or does `~/.previewapk/credentials.json` exist? Run `previewapk status` (or `previewapk devices`). If it answers, setup is already done — skip to Step 4. Never reinstall over a working setup, and never mint a second key over a working one: `previewapk setup` itself checks first and reports `{"step": "ready", …}` without minting anything. ## Step 1 — Install the CLI (only if missing) Download the single-file, stdlib-only CLI: ``` curl -o previewapk https://previewapk.siteport.site/cli/previewapk.py chmod +x previewapk ``` No dependencies, no build step — any `python3` runs it. ## Step 2 — Start setup and show the human the code Run: ``` previewapk setup --no-wait ``` It prints exactly one JSON line: ```json {"step": "sign_in", "code": "K7QX-4M2P", "url": "https://previewapk.siteport.site/portal/agent-grant", "expires_in": 600} ``` Show the human the URL and the code, in plain words: > Open https://previewapk.siteport.site/portal/agent-grant and sign in > (or create your account). Check the page shows the code **K7QX-4M2P**, > then click **Approve**. Rules while you wait: - Do not open the grant page yourself, and do not ask the human to paste a key — approval in the browser is the whole flow. - The code is single-use and expires in 10 minutes. It is shown identically here and on the page; that comparison is the defense against phishing, so quote it exactly. - A label helps the owner recognize the key later: `previewapk setup --label "Claude Code on laptop"` — the label is shown on the approval page. **Accounts note:** approval requires an existing PreviewAPK account — the grant flow creates no side door around signup or the Terms. While PreviewAPK is in private preview, signup is limited; if the human has no account yet, the grant page leads them to sign-in / early access first, and setup completes once they have one and approve a code. ## Step 3 — Poll until it resolves Re-run setup (no flags). It resumes the pending code and polls the token endpoint every few seconds: ``` previewapk setup ``` It prints the `sign_in` line again, then exactly one final step: - `{"step": "ready", "account": "…", "key_saved": "~/.previewapk/credentials.json", "devices": ["Device 1"]}` — the key was delivered **once** by the portal and saved to `~/.previewapk/credentials.json` (file permissions 0600). Never print, echo, or read back that file's contents. - `{"step": "no_slots", …}` — signed in fine, but the account has no device slot yet (early-access / paywall state). Tell the human plainly and point them at the portal; do not treat it as an error and do not retry. - `{"step": "denied", …}` — the human declined. Stop. Explain that setup needs their approval, and mention the manual route (copy a key from the portal's Developers card into `PREVIEWAPK_API_KEY`) without pushing. - `{"step": "expired", …}` — the code aged out (10 minutes). Run `previewapk setup --no-wait` again for a fresh code and repeat Step 2. From now on every CLI command uses the saved key; the MCP server reads the same file when `PREVIEWAPK_API_KEY` is unset, so one setup covers both clients. The environment variable still wins when set. **Zero-install route:** the same key also works with the hosted MCP endpoint — no client file at all. Register `https://previewapk.siteport.site/mcp` as a streamable-http MCP server with header `Authorization: Bearer ` (Claude Code: `claude mcp add --transport http previewapk https://previewapk.siteport.site/mcp --header "Authorization: Bearer "`). The hosted server issues a session id at connect and your working-device selection rides on it, exactly like the local server's. Prefer the hosted route when the agent's MCP client supports remote servers; keep the local file for stdio-only clients. Over hosted MCP, push_apk takes the APK as base64 (`apk_b64`) instead of a file path. The key is a normal account key — the owner can view or revoke it in the portal under API keys; revoking it unpairs this machine, and setup handles a revoked saved key by returning to `sign_in`. ## Step 4 — Ready behavior With `step: ready`: 1. Run `previewapk devices` and report, in one line, what the account has (device names, profiles, attached apps). 2. Ask **one short question**: what do you want to use the phone for? 3. Then help with exactly that, under the SKILL.md rules: select the working device, push → start → observe → drive → verify → report → stop. Links never drive; one driver per session; a fresh phone every session; file a field report at the end unless the account turned diagnostic sharing off. ## The endpoints (if you are integrating instead of using the CLI) - `POST /portal/api/setup/start` — body `{"label": "…"}` (optional). Returns `{code, url, expires_in}`. Rate-limited per IP. - `POST /portal/api/setup/token` — body `{"code": "…"}`. Returns `{"status": "pending" | "denied" | "expired" | "claimed"}`, or — exactly once, on the first poll after approval — `{"status": "approved", "key": "…", "email": "…", "devices": […]}`. An unknown code returns HTTP 404 with the standard error envelope (`code: "unknown_code"`). - `GET|POST /portal/agent-grant` — the human approval page. Requires a signed-in portal session. ==================================================================== 8. PROMPT LIBRARY Source: https://previewapk.siteport.site/docs/prompts/ ==================================================================== Copy-paste prompts for the jobs agents most often run on PreviewAPK. Replace the bracketed bits (`[path]`, `[device name]`, `[ticket]`) before pasting. Ground rules every prompt assumes (they're in the agent skill): select the working device first and never guess one; read the screen with `look` / the hierarchy and verify visuals by screenshot; use `wait` / `wait_for` after anything that loads instead of sleeping; read the logs after anything unexpected; never blindly repeat an action whose outcome came back unknown; when a step needs the human (a credential, a 2FA code, a payment approval), ask with `request-human` / `previewapk_request_human` and wait for the handback — never guess credentials and never ask for one in chat; file the wrap-up (`finish` / `previewapk_finish`) at the end of every job and relay its message verbatim; file the field report at the end too (unless the account's diagnostics sharing is off — then don't, and don't try to route around it); stop the session when finished. ## 1. Smoke check on a new build Smoke-check the build I just made, on PreviewAPK. 1. List the devices and select [device name]. Push [path/to/app.apk] and start a session with driver=agent. Poll status until the device is live. 2. Read the screen (look / hierarchy). Confirm the app's first screen appears and name the element that proves it. Take a screenshot and confirm the screen renders correctly (no clipping, overlap, or blank areas). 3. Walk the main path: tap through the primary flow a first user would take — [describe it in one line, e.g. "sign up later → create a note → save"]. Before each tap, read the screen; after each tap, read it again to confirm the screen changed the way it should. Tap fields with --field N / by label, and type only after the field is focused. 4. Read the session logs with errors only. If there is a crash or an error, quote the exact lines — don't paraphrase them. 5. Report: PASS or FAIL, the screens you visited, anything that looked wrong in a screenshot, and any log errors verbatim. File a PreviewAPK field report, then stop the session. A good result looks like: a one-paragraph verdict with screen names, one screenshot per key screen, and either "no errors in the log" or the exact error lines. ## 2. Exploratory session Explore my app on PreviewAPK like a curious first-time user. Push [path/to/app.apk] to [device name], start agent-driven, and wait until live. Then explore for up to [10] minutes of session time: - Start from the first screen. Systematically visit every reachable screen: bottom tabs, menus, settings, dialogs. Use the screen snapshots as your map — note screens you reach and elements you used. - On each screen, try the obvious interactions: open and dismiss, scroll lists to the bottom, submit an empty form once — just what a user would poke at. - After anything surprising, read the logs (errors only) and take a screenshot of the surprising state before moving on. - If you reach a sign-in, payment, or consent step that needs my real credentials, do NOT invent credentials and do NOT ask me to paste a password or code into this chat: ask me with request-human "the app is asking for [the step]" (MCP: previewapk_request_human), relay its message_for_human to me word for word, then wait for the handback (wait --for driver) and continue from the handback note. - Dead ends count as findings: buttons that do nothing, back behavior that exits the app (note it — relaunch recovers), labels that are missing or confusing. Report: a map of the screens you found, the issues ranked most-severe first (each with the screen, what you did, what happened, and the log lines if any), and what you could not reach. File a field report, then stop. ## 3. Reproduce a bug report Reproduce this bug report on PreviewAPK and tell me if it's real: [paste the ticket: expected vs actual, steps if known] 1. Push the build named in the ticket (or [path/to/app.apk] if it names none) to [device name], start agent-driven, wait for live. 2. Follow the ticket's steps literally, in order. Read the screen before and after each step, and quote the screen text you actually saw at each step — not what the ticket assumed. 3. The moment the reported behavior appears — or clearly diverges from the ticket — take a screenshot of that exact state and read the logs (errors only, around that moment). Quote crash/error lines verbatim. 4. If a step can't be performed as written (missing element, different label, extra dialog), say which step and what the screen showed instead; then take the closest reasonable path and flag that you deviated. Report one of: REPRODUCED (with the exact steps as performed, the screenshot, and the log evidence), DOES NOT REPRODUCE (with what happened instead at the diverging step), or BLOCKED (what's missing). File a field report, then stop. ## 4. Accessibility pass Run an accessibility pass over my app on PreviewAPK. Push [path/to/app.apk] to [device name], start agent-driven, and visit the main screens: [list them, or "every tab and the primary flow"]. Work from the UI hierarchy — it is your evidence: - Every tappable element (click flag) should carry a label: a text or a desc (content-description). List every clickable element that has NEITHER, with its class, bounds, and screen. (On Flutter/semantics UIs, labels live in desc — check there before calling an element unlabeled.) - Icon-only buttons are the usual suspects: check each one for a meaningful desc, and quote the vague ones ("button", "image", a bare filename) verbatim. - Check form fields: each input should be reachable and tappable by its position (--field N order), and its purpose should be inferable from a nearby label in the tree. List fields whose purpose you cannot determine from the hierarchy. - Take a screenshot of each main screen and flag text that is visually clipped, overlapping, or rendered too small to read at a glance. Be honest about the limits: this pass audits the labels and structure a screen reader consumes; it is not a TalkBack walkthrough and it doesn't test dynamic font scaling. Say so in the report. Report: findings per screen, worst first, each with the element's class/bounds and the exact missing or vague label. File a field report, then stop. ## 5. Before-a-release checklist Run the pre-release check on this build on PreviewAPK. Build: [path/to/app.apk] · Device: [device name] · The change in this release: [one line]. 1. Install & first launch. Push and start agent-driven. The app reaches its first screen with no crash; read the hierarchy to confirm, screenshot to confirm it renders. 2. The changed flow. Drive the flow named above end-to-end. Verify each step on screen, screenshot the result screen. 3. Core regression sweep. Re-run the basics: [list your 3–5 core flows, e.g. "open an existing item, create a new one, search, open Settings and back"]. Note any step that behaves differently from the obvious expectation. 4. Back behavior. Press Back (key 4) from the deepest screen, one step at a time, until you reach the first screen. Note any step where Back exits the app unexpectedly (recover with relaunch) or lands somewhere surprising. 5. Logs. Read the full session log (not just errors). Quote any stack traces verbatim; summarize warnings in one line each. Ignore lines flagged as emulator noise. 6. Fresh-start honesty. This phone starts fresh every session — nothing here proves upgrade behavior from the previous version's data. If the release needs an upgrade-path check, say that this run does not cover it. 7. No-Play-services note. If any flow touches Google sign-in, Maps, or push, note that it cannot be verified on these phones. Report: a checklist table — item, PASS/FAIL, evidence (screen name, screenshot, or log line). End with a ship / don't-ship recommendation and the top reason. Hand the wheel to me (handoff user), give me the session link so I can look myself, file a field report — and leave the session running for my look unless I told you to stop it. ## 6. A flow with a human step — and the wrap-up Test the [flow name] flow on PreviewAPK, on [device name] with [path/to/app.apk]. The flow includes a step only I can do: [describe it, e.g. "approving the test payment" / "entering the 2FA code from my authenticator"]. Drive the flow up to that step. When you reach it: 1. Ask me for the step — previewapk request-human "the app is asking for [the step]" (MCP: previewapk_request_human) — and relay the message_for_human it returns to me word for word. Do not ask me for any password or code in this chat; I will type it on the phone myself. 2. Wait for the handback (previewapk wait --for driver / MCP previewapk_wait_for with condition driver). Read my handback note when it comes back, then continue the flow to the end and verify the result on screen. When the job is done, wrap up: previewapk finish --summary "[what you tested and what you found]" --verdict [passed|failed|mixed] (MCP: previewapk_finish) and relay its message_for_human to me word for word — the counts in it come from the server, not from your memory. Leave the session running (finish does not stop it) unless I told you to stop it. File a field report too.