The framework-agnostic core of the Google TV stack — discovery, pairing, a stateful session, and a shared device store that every client (CLI, MCP, app) builds on.
- 🔍 mDNS Discovery — finds every Google TV on the local network via
dns-sd; returns pure data, no side effects
- 🔐 Dependency-injected Pairing —
onSecret is a callback you supply (terminal readline, MCP round-trip, Tauri dialog…); the library never touches I/O itself
- 📡 Stateful Session —
createSession() returns an EventEmitter that reduces the underlying protocol stream to a single observable SessionState; subscribe with .on("change", state => …)
- ⚡ One-shot Helpers —
sendKey, launchApp, connect, withRemote for scripts that don't need a long-lived connection
- 📱 App Catalog — curated
APPS list with findApp, listApps, and appLink (builds the reliable market://launch?id=<package> URI)
- 🗄️ Shared Config Store — reads and writes
~/.config/gtv/config.json; all consumers (gtv-cli, mcp-gtv, gtv-app) share one device registry
- 🔑 Full Keycode Surface —
KEYS, KEY_LABELS, and re-exported RemoteKeyCode / RemoteDirection so consumers need only depend on @kud/gtv
import { discover, pair } from "@kud/gtv"
const [tv] = await discover()
await pair({
host: tv.host,
hostname: tv.hostname,
port: tv.port,
name: tv.name,
onSecret: async () => promptUserForPin(), // PIN displayed on the TV screen
})
import { createSession, KEYS } from "@kud/gtv"
const session = createSession() // uses the currently configured device
session.on("change", (state) => console.log(state))
session.sendKey(KEYS.home)
session.typeText("interstellar")
session.launchApp("market://launch?id=com.netflix.ninja")
session.stop()
import { sendKey, launchApp, findApp, appLink, KEYS } from "@kud/gtv"
await sendKey(KEYS.mute)
const netflix = findApp("netflix")
if (netflix) await launchApp(appLink(netflix))
| Export |
Signature |
Description |
discover |
() => Promise<DiscoveredDevice[]> |
mDNS scan; returns every Google TV found on the LAN |
| Export |
Signature |
Description |
pair |
(options: PairOptions) => Promise<PairResult> |
Full pairing flow; onSecret is called with the PIN entry callback |
PairOptions:
| Field |
Type |
Required |
Description |
host |
string |
✓ |
IP address of the TV |
hostname |
string |
|
Hostname (from mDNS) |
port |
number |
|
Remote port (default 6466) |
name |
string |
|
Friendly device name |
onSecret |
() => Promise<string> |
✓ |
Resolves the PIN shown on screen |
onStatus |
(status: PairStatus) => void |
|
Progress callback |
save |
boolean |
|
Persist device to config store (default true) |
| Export |
Signature |
Description |
createSession |
(device?: Device) => Session |
Long-lived, event-driven connection |
Session extends EventEmitter:
| Member |
Type |
Description |
state |
SessionState |
Current snapshot (connected, powered, volume, currentApp, …) |
sendKey |
(keyCode: number, direction?: number) => void |
Send a remote keypress |
typeText |
(text: string) => void |
IME text input |
launchApp |
(link: string) => void |
Open an app by URI |
stop |
() => void |
Tear down the connection |
on("change", cb) |
— |
Fires on every state update |
on("error", cb) |
— |
Fires on connection errors |
| Export |
Description |
connect(device?) |
Opens a raw AndroidRemote, resolves when ready |
withRemote(fn, device?) |
Runs fn(remote) then closes the connection |
sendKey(keyCode, direction?, device?) |
One-shot key send |
launchApp(link, device?) |
One-shot app launch |
| Export |
Signature |
Description |
APPS |
AppEntry[] |
Curated list (Netflix, YouTube, Prime Video, Plex, Disney+, Spotify, …) |
listApps |
() => AppEntry[] |
Returns APPS |
findApp |
(query: string) => AppEntry | undefined |
Case-insensitive match on id or display name |
appLink |
(app: AppEntry) => string |
Builds market://launch?id=<package> URI |
Config lives at ~/.config/gtv/config.json and is shared across all consumers.
| Export |
Description |
readConfig |
Read the full config file |
listDevices |
All paired devices |
getCurrentDevice |
The active device |
findDevice(query) |
Find by host, name, or hostname |
upsertDevice(device) |
Insert or update a device entry |
setCurrentDevice(host) |
Mark a device as active |
removeDevices(hosts) |
Delete one or more devices |
readPreferences |
Read the preferences block |
writePreferences(prefs) |
Write the preferences block |
CONFIG_PATH |
Absolute path to the config file |
| Export |
Description |
KEYS |
Map of friendly names → RemoteKeyCode values |
KEY_LABELS |
Map of RemoteKeyCode values → display strings |
RemoteKeyCode |
Re-export from @kud/androidtv-remote |
RemoteDirection |
Re-export from @kud/androidtv-remote |
setDebug |
Enable protocol-level debug logging |
gtv/
├── src/
│ ├── index.ts # Public surface
│ ├── session.ts # Stateful Session (EventEmitter)
│ ├── client.ts # One-shot helpers
│ ├── discovery.ts # mDNS via dns-sd
│ ├── pairing.ts # Pairing flow
│ ├── config.ts # Device store + preferences
│ ├── apps.ts # App catalog + appLink
│ ├── keycodes.ts # KEYS / KEY_LABELS
│ └── types.ts # SessionState, VolumeState
└── dist/ # Compiled output (tsup)
git clone https://github.com/kud/gtv.git
cd gtv
npm install
npm run build
| Script |
Description |
npm run build |
Compile with tsup |
npm run build:watch |
Compile in watch mode |
npm run typecheck |
Type-check without emitting |
npm run clean |
Delete dist/ |
Node.js ≥ 22 required.
| Package |
Role |
@kud/androidtv-remote |
Protocol layer — TLS socket, protobuf codec, key/text/app-link send |
tsup |
Zero-config ESM bundler |
typescript |
Type safety across the entire public API |
MIT © kud — Made with ❤️