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 hoststring✓ IP address of the TV hostnamestringHostname (from mDNS) portnumberRemote port (default 6466) namestringFriendly device name onSecret() => Promise<string>✓ Resolves the PIN shown on screen onStatus(status: PairStatus) => voidProgress callback savebooleanPersist device to config store (default true)
Export Signature Description createSession(device?: Device) => SessionLong-lived, event-driven connection
Session extends EventEmitter:
Member Type Description stateSessionStateCurrent snapshot (connected, powered, volume, currentApp, …) sendKey(keyCode: number, direction?: number) => voidSend a remote keypress typeText(text: string) => voidIME text input launchApp(link: string) => voidOpen an app by URI stop() => voidTear 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 APPSAppEntry[]Curated list (Netflix, YouTube, Prime Video, Plex, Disney+, Spotify, …) listApps() => AppEntry[]Returns APPS findApp(query: string) => AppEntry | undefinedCase-insensitive match on id or display name appLink(app: AppEntry) => stringBuilds market://launch?id=<package> URI
Config lives at ~/.config/gtv/config.json and is shared across all consumers.
Export Description readConfigRead the full config file listDevicesAll paired devices getCurrentDeviceThe 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 readPreferencesRead the preferences block writePreferences(prefs)Write the preferences block CONFIG_PATHAbsolute path to the config file
Export Description KEYSMap of friendly names → RemoteKeyCode values KEY_LABELSMap of RemoteKeyCode values → display strings RemoteKeyCodeRe-export from @kud/androidtv-remote RemoteDirectionRe-export from @kud/androidtv-remote setDebugEnable 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 buildCompile with tsup npm run build:watchCompile in watch mode npm run typecheckType-check without emitting npm run cleanDelete dist/
Node.js ≥ 22 required.
Package Role @kud/androidtv-remoteProtocol layer — TLS socket, protobuf codec, key/text/app-link send tsupZero-config ESM bundler typescriptType safety across the entire public API
MIT © kud — Made with ❤️