npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@byokit/realtime

v0.3.3

Published

TypeScript kits that let people sign in to your app with the AI plans they already pay for, on web, phone and desktop.

Readme

@byokit/realtime

Realtime voice sessions with injected audio and provider adapters in a child process. First consumer: muxr.

import { realtimeClient, type AudioPorts, type RealtimeHostFrame, type RealtimeStream } from '@byokit/realtime';
import { realtimeEngine, toolBridge } from '@byokit/realtime/node';

// The app supplies its sign-in, carrier, audio implementation and authorized planner.
declare const accounts: { access(member: number, signal?: AbortSignal): Promise<{ access: string; accountId: string }> };
declare const emit: (frame: RealtimeHostFrame) => void;
declare const runPlanner: (request: string, signal: AbortSignal) => Promise<string>;
declare const openVoiceStream: () => Promise<RealtimeStream>;
declare const audio: AudioPorts;
const member = 1;
const tools = [{ name: 'delegate', description: 'Handle a spoken request', parameters: { type: 'object', properties: { request: { type: 'string' } } } }];
const bridge = toolBridge({
  tools, handlers: { delegate: async (args, { signal }) => runPlanner(String(args.request), signal) },
  emit, failure: () => 'That request could not be completed.',
});
const engine = realtimeEngine({
  engine: 'chatgpt', auth: { kind: 'plan', access: signal => accounts.access(member, signal) },
  tools, bridge, instructions: 'Answer briefly.', emit,
});
// Forward device frames to engine.receive(frame); engine.close() ends the child.
const client = realtimeClient({
  open: openVoiceStream, audio,
  onStatus: status => console.log(status),
  onTurn: (role, text) => console.log(role, text),
});

For ChatGPT voice register a delegate tool handler receiving { request } and supply the app's planner. API routes use auth: { kind: 'key', key }, with billing per use. The subscription route uses the person's ChatGPT sign-in from @byokit/accounts; it never reads Codex credentials or starts the Codex CLI. Backend refusal is reported as not-included.

Structured delegation and named targets

delegationHandler dispatches a JSON request of the form { name, arguments } directly through an app-supplied toolBridge. For ordinary spoken requests it uses the optional app planner. Invalid structured requests never enter the planner. Keep the action bridge separate from the outer delegate bridge:

import { delegationHandler, toolBridge } from '@byokit/realtime/node';
import type { ToolHandler } from '@byokit/realtime/node';
import type { RealtimeHostFrame } from '@byokit/realtime';

declare const emit: (frame: RealtimeHostFrame) => void;
declare const sendAuthorizedMessage: ToolHandler;
declare const plan: (request: string, context: Parameters<ToolHandler>[1]) => Promise<string>;
const actionTools = [{ name: 'message', description: 'Message a named agent', parameters: {
  type: 'object', properties: { agent: { type: 'string' }, text: { type: 'string' } },
  required: ['agent', 'text'], additionalProperties: false,
} }];
const actions = toolBridge({ tools: actionTools, handlers: { message: sendAuthorizedMessage },
  emit, failure: () => 'The message could not be confirmed.', timeoutFor: () => 75000 });
const delegate = delegationHandler({ bridge: actions, plan });
const tools = [{ name: 'delegate', description: 'Delegate a spoken request', parameters: { type: 'object' } }];
const bridge = toolBridge({ tools, handlers: { delegate }, emit,
  timeoutFor: () => 340000, failure: () => 'The request could not be confirmed.' });
// Pass the outer bridge and its delegate tool to realtimeEngine.
// A structured request can be:
// {"name":"message","arguments":{"agent":"Avery","text":"Please report progress"}}
// Close actions as well as the outer bridge when the session ends.

The kit preserves arguments and operation ids; retries with the same call id share a result. Named targets, target lookup, ambiguity checks, authorization, argument validation and mutation receipts belong to the app's handlers. Tool JSON schemas describe the provider catalog; they are not runtime authorization. The app planner owns its model, conversation memory, tool budget and deadline; the kit does not add a planner model or any agent runtime dependency. Aborted calls must stop in the app too; completed side effects are never rolled back. The host bridge owns every tool watchdog and failure message; the child has no second deadline. A 340-second planner or a 290-second watch can therefore use the app's declared budget. An ordinary ChatGPT user turn preserves unfinished delegations and their retransmission cache. Explicit interrupt or close aborts them; a new turn id still identifies a new request.

Advisory auth check

Pass a read-only saved-sign-in lookup as authCheck on the engine. It starts independently, is never awaited by startup, and cannot close the session. It does not call auth.access, refresh a token, run a CLI or initiate login. The app's peek callback must also never refresh, prompt or start login.

import { realtimeEngine, toolBridge } from '@byokit/realtime/node';
import type { RealtimeAuth, RealtimeHostFrame, RealtimeTool, RealtimeAuthPeekResult, RealtimeAuthResult } from '@byokit/realtime';

declare const auth: Extract<RealtimeAuth, { kind: 'plan' }>;
declare const tools: RealtimeTool[];
declare const bridge: ReturnType<typeof toolBridge>;
declare const emit: (frame: RealtimeHostFrame) => void;
declare const peekSavedSignIn: (signal: AbortSignal) => Promise<RealtimeAuthPeekResult>;
declare const updateSettingsBadge: (status: 'ready' | 'signed-out' | 'unknown') => void;
declare const showSignInMessage: (result: RealtimeAuthResult) => void;
const engine = realtimeEngine({
  engine: 'chatgpt', auth,
  tools, bridge, emit,
  authCheck: { peek: peekSavedSignIn, onStatus: status => updateSettingsBadge(status), onResult: showSignInMessage },
});

ready means the app reports a saved sign-in, not provider entitlement. signed-out means the lookup returned false or found an unusable saved sign-in. Unrecognized errors or a timeout produce unknown; the default timeout is one second, capped at ten seconds. Close cancels pending checks and suppresses late notifications. Normal session authentication still needs the credential from auth.access. For a standalone settings lookup, realtimeAuthCheck({ peek }) is also exported from the portable entry and returns { result, details, close }. result still resolves to the status string; details resolves to { state, reason?, message? }, and engine onResult receives the same diagnostic object. A boolean or { state } peek keeps the previous behavior without adding a reason.

To report an expired saved sign-in, the read-only peek can return { state: 'signed-out', reason: 'login-expired' }. Built-in reasons are credential-permissions, login-expired, missing and unknown; apps can supply additional reason strings. Messages use fixed plain wording and never copy exception text or credential paths. Missing-file errors map to missing, permission errors to credential-permissions, and expired-login errors to login-expired; unrecognized errors and timeouts report unknown. Do not await this check before opening a voice session or show a login prompt from its result.

Lifecycle announcements and semantic app tools

Feed app-confirmed lifecycle reports into the existing speech path. The kit does not watch agents or infer their outcome:

import type { realtimeClient } from '@byokit/realtime';
import type { realtimeEngine } from '@byokit/realtime/node';
declare const client: ReturnType<typeof realtimeClient>;
declare const engine: ReturnType<typeof realtimeEngine>;

// Called by the app's lifecycle subscription; use bounded, sanitized app wording.
client.speak('Avery has finished the task.');
// A host can inject the same report directly:
engine.receive({ type: 'realtime.say', text: 'Avery has finished the task.' });

speak queues at most sixteen reports until the call is ready, waits for local playback drain and does not replay sent speech on reconnect. onTurn observes transcripts and onStatus observes playback states; a transcript alone does not prove the report was audible. The app owns push delivery, deduplication, waking a sleeping call and stopping it after reports. When player.finish(callback) returns false, the client reports connected immediately. True means the player will call back after draining. Clearing audio also rechecks queued speech, including while the microphone is muted.

Compose appBridge with toolBridge to round-trip semantic phone requests:

import { appBridge, toolBridge } from '@byokit/realtime/node';
import type { RealtimeHostFrame } from '@byokit/realtime';
declare const emit: (frame: RealtimeHostFrame) => void;
const app = appBridge(emit);
const navigation = [{ name: 'navigate', description: 'Open an app destination',
  parameters: { type: 'object', properties: { target: { type: 'string' } }, required: ['target'] } }];
const bridge = toolBridge({ tools: navigation, app, emit,
  handlers: { navigate: (args, { signal }) => app.run('navigate', String(args.target), signal) },
  failure: () => 'The app could not complete that request.',
});
// client.onAppRequest handles view/navigate/activate; forward all client frames
// to engine.receive, which routes app results back to this app bridge.
// Closing the engine also cancels pending app requests.

The main entry is portable for web and React Native and loads no native code. ./webrtc supplies webRtcPeer for browsers and selects its native implementation under the react-native condition; install the optional react-native-webrtc 124.0.8 peer in the RN app. All media APIs can be injected for tests. ./node requires Node 22.18+ and is for the credential host. It starts its own adapter child with an empty environment; credentials travel over private stdin. No provider key or plan token is sent to the device.

AudioPorts requires microphone acquisition/release, PCM capture, playback admission/drain and audio routing. The app owns permissions and its microphone service. Acquire must resolve only after that service is ready. The client awaits it before capture. Each client owns its ports; the app must arbitrate shared microphones.

Preconnect without microphone capture

Set capture: 'lazy' for a browser or React Native WebRTC session. Startup negotiates one sendrecv audio transceiver with no sender track: it calls neither audio.microphone.acquire() nor getUserMedia, so the app's microphone foreground service stays stopped. The playback route remains available.

import { realtimeClient, type AudioPorts, type RealtimeStream } from '@byokit/realtime';
import { webRtcPeer } from '@byokit/realtime/webrtc';
declare const audio: AudioPorts;
declare const openVoiceStream: () => Promise<RealtimeStream>;
const client = realtimeClient({
  open: openVoiceStream, audio, webrtc: webRtcPeer, capture: 'lazy',
  onStatus() {}, onTurn() {},
});
// Once WebRTC setup has started, await this before accepting speech:
async function beginTalking() { await client.attachMic(); }
// When idle, release hardware capture while keeping the call connected:
async function becomeIdle() { await client.releaseMic(); }
// End the call and release all media:
function endCall() { client.stop(); }

attachMic() awaits the app's microphone permission/service lease, captures an audio track and uses sender.replaceTrack(track) before enabling the sender's encoding. Lazy peers negotiate sendrecv with an inactive encoding: Android's audio device cannot start recording just because an answer is applied. releaseMic() disables that encoding before replaceTrack(null), stops capture tracks and releases the lease so the app can stop its microphone service and clear the OS indicator. Neither operation renegotiates. Repeated calls are safe; release or stop during pending capture cleans up when that capture resolves. Reconnecting starts another lazy peer; the app explicitly attaches again when it needs input.

The built-in webRtcPeer handle also exposes both methods for direct use. Custom peer factories may implement these optional methods; client attach rejects if the current peer does not support them or setup has not started. The default is eager capture, and PCM capture is unchanged. setMuted() still only toggles track delivery; use releaseMic() to relinquish capture. Mute state persists when a new track is attached. Each client owns its AudioPorts; microphone.release() must stop the app-owned foreground service.

Close the client and engine when the call ends. client.interrupt() clears playback and aborts unfinished tools; old response output is fenced. Transient provider and transport failures retry twice, after 500 and 1,000 ms, with a fresh budget after 30 healthy seconds. Sent audio, speech and tool calls are never replayed. The app can mark transport closure terminal with retryableClose. Explicit ChatGPT interruption opens a fresh call; explicit Gemini interruption rotates its provider session. Native speech interruption remains provider-driven. No STT/LLM/TTS chain is exported. See the lift contract for bounds, events and usage.

Reconnect and host policy

Set preserveMediaOnReconnect: true on the client to retain a ready PCM recorder, microphone lease, audio route and playback tail across a carrier reconnect. Audio captured while the carrier is down is discarded. Pending microphone acquisition still releases before retry; WebRTC always creates a fresh peer for a fresh call. A changed PCM format releases the old recorder before acquiring another. Stop releases retained media even during backoff. The default remains release/clear/reacquire.

The engine accepts app phrase policy and credential-error wording:

import { realtimeEngine, toolBridge } from '@byokit/realtime/node';
import type { RealtimeAuth, RealtimeHostFrame, RealtimeTool } from '@byokit/realtime';
declare const auth: Extract<RealtimeAuth, { kind: 'plan' }>;
declare const tools: RealtimeTool[];
declare const bridge: ReturnType<typeof toolBridge>;
declare const emit: (frame: RealtimeHostFrame) => void;

const engine = realtimeEngine({ engine: 'chatgpt',
  auth, tools, bridge, emit,
  hangup: text => /^goodbye[.!]?$/i.test(text.trim()),
  authFailure: () => 'Sign in again from Settings.',
  signalingIdentity: { originator: 'voice-app', userAgent: 'voice-app/1' },
});

Hangup ends after the next completed agent transcript so the provider can give a farewell. It is a transcript-level policy, not proof that playback drained. Credential errors are sanitized, bounded and emitted as the close reason; the kit initiates no login or prompt. Transcripts preserve up to 4,000 UTF-8 bytes, state details up to 500 bytes, and close reasons up to 2,048 bytes. Empty ChatGPT transcripts are ignored.

ChatGPT emits realtime.webrtc.start while the isolated child starts and access is being resolved. Device frames remain bounded and queued until the credential/config line is sent. This overlaps local setup without sending a signaling request before credentials are available. signalingIdentity lets the app name its own originator/user-agent; the default for both is byokit, and identity values must be printable ASCII of at most 160 characters. Credentials, endpoint restrictions and redirect rejection are unchanged. The identity option allows a consumer to compare signaling latency; offline tests prove startup ordering, not a reduction in the remote provider's POST time.

For an explicitly live microphone and subscription-provider proof, run the bounded voice demo. It signs in through accounts, keeping the sign-in at this computer's machine store, and records connection facts and turn counts. Offline tests do not establish a live pass.