/** * Device registry: the single owner of {@link TwinklyClient} instances. * * Tools never construct clients or touch HTTP — they ask the manager to * resolve a `device` parameter to a client and run an operation through * {@link DeviceManager.withDevice}, which centralizes error normalization. * * Responsibilities: * - hold a name→device registry seeded from config, * - lazily create one client per device and reuse it (so the cached auth * token from `@twinklyjs/twinkly` is reused across calls), * - resolve an optional `device` argument to a concrete device, falling back * to the configured default / sole device / implicit `default`, * - run UDP discovery on demand with a short-lived cache and fold the results * into the registry. */ import { TwinklyClient } from '@twinklyjs/twinkly'; import type { Device, DiscoverOptions } from '@twinklyjs/twinkly'; import { type TwinklyMcpConfig } from '../config.js'; /** Where a registry entry came from. */ export type DeviceSource = 'config' | 'discovered'; /** Public, serializable view of a registered device. */ export interface DeviceInfo { name: string; ip: string; source: DeviceSource; /** True when this device is the one used if a tool omits `device`. */ isDefault: boolean; } /** A device resolved to a live client, ready to use. */ export interface ResolvedDevice { name: string; ip: string; client: TwinklyClient; } /** Injectable seams, all defaulted to the real implementations. */ export interface DeviceManagerDeps { /** Factory for clients (overridden in tests). */ createClient?: (ip: string) => TwinklyClient; /** UDP discovery implementation (overridden in tests). */ discover?: (options?: DiscoverOptions) => Promise; /** Clock, for the discovery cache TTL (overridden in tests). */ now?: () => number; /** How long discovery results stay fresh, in ms. Default 60s. */ discoveryTtlMs?: number; } export declare class DeviceManager { private readonly entries; private readonly defaultDevice; private readonly createClient; private readonly discoverImpl; private readonly now; private readonly discoveryTtlMs; private discoveryCache; constructor(config: Pick, deps?: DeviceManagerDeps); /** All registered devices (config + discovered), in insertion order. */ listDevices(): DeviceInfo[]; /** Whether a device with the given name is registered. */ has(name: string): boolean; /** * Resolve a `device` argument to a live client. Throws {@link TwinklyError} * (`device_not_found` / `no_device_specified`) when resolution fails. */ resolve(deviceName?: string): ResolvedDevice; /** * Run an operation against a resolved device, normalizing any failure into a * {@link TwinklyError} tagged with the device name. This is the only path * tools should use to talk to a device. */ withDevice(deviceName: string | undefined, fn: (client: TwinklyClient) => Promise): Promise; /** * Discover devices on the LAN, caching results for `discoveryTtlMs`. Newly * found devices (by IP) are added to the registry under their `deviceId`. * Pass `force` to bypass the cache. */ discoverDevices(options?: { force?: boolean; } & DiscoverOptions): Promise; private runDiscovery; private clientFor; /** Resolve a (possibly absent) device name to a concrete, existing name. */ private resolveName; /** The default device name, or `undefined` if it can't be determined. */ private tryResolveDefaultName; private deviceNamesLabel; /** Ensure a discovered device name doesn't collide with an existing entry. */ private uniqueName; }