import { EventStore } from "./store.js"; import { TesterRegistry } from "./testers.js"; import { AnnotationStore } from "./annotations.js"; import { type Dispatcher } from "./injectors.js"; import type { DiagEvent } from "@vincentt-xr/harness/events"; export interface RelayOptions { port?: number; /** WS path the client connects to (default matches HarnessProvider). */ path?: string; capacity?: number; onLog?: (msg: string) => void; /** Bind the reverse channel to this project dir (enables annotation routes). */ projectDir?: string; /** Injected for tests; else built from projectDir. */ annotationStore?: AnnotationStore; /** Injected for tests; else a Claude-Code-passive → paste chain. */ dispatcher?: Dispatcher; /** Injected for tests. */ makeId?: () => string; /** * Live tap for the CLI's terminal stream. It fires per batch as events land, * so the creator sees a line the moment the phone produces it rather than on * a poll. The relay still buffers everything — this is an addition, not a * replacement, and a missing handler changes nothing. */ onEvents?: (events: DiagEvent[], tags: EventTags) => void; /** * A connection resolved its tester — on the `hello` at WS-open, before any * batch. This is what lets the CLI print `tester-N connected` on connect, so a * SILENT app (no events at all) and an app that crashes during mount are both * attributed. Fires ONCE per connection, the moment the tester is known; a * connection whose key misses the grammar resolves to no tester and never * fires this. The events path still carries `tester` for un-helloed clients * (an old harness), so attribution is not lost if the hello never arrives. */ onAttribute?: (tags: EventTags) => void; /** * Fires ONCE per refused session when the tester cap is reached, so the CLI * can print the one warning line. Once per session rather than once per batch: * the registry holds a tombstone precisely so a chatty refused client cannot * repaint the warning on every flush. */ onCapReached?: () => void; /** Injected for tests, so cap behavior is drivable without 64 real clients. */ testerRegistry?: TesterRegistry; } /** Who produced a batch: the connection's viewer label and its resolved tester. */ export interface EventTags { viewer?: string; tester?: string; } export interface RelayHandle { store: EventStore; annotations?: AnnotationStore; /** The run's tester registry — read-only to callers; the relay owns writes. */ testers: TesterRegistry; close: () => Promise; } export declare function startRelay(opts?: RelayOptions): RelayHandle; /** * readViewerLabel accepts ONLY `^viewer-[0-9]{1,4}$` and returns undefined for * everything else — an array-valued header (a duplicate), a forged class, an * ANSI escape, a newline. There is no "sanitize and keep" branch: a label that * does not match is not a label, and the events simply carry no viewer. */ export declare function readViewerLabel(raw: string | string[] | undefined): string | undefined;