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

@stackbilt/cf-voice

v0.1.0

Published

Cloudflare Workers AI voice primitives — STT, TTS, DO base class, and voice UI renderer

Readme

@stackbilt/cf-voice

Cloudflare Workers AI voice primitives. Drop a real-time voice interface into any Worker in ~30 lines.

What's included:

  • BaseVoiceAgent — Durable Object base class with Deepgram Flux STT + Workers AI TTS pre-wired
  • emailSummaryAction — detect "email me that summary" intent and trigger a side effect
  • extractIds — pull session/canonical identity out of the opaque agents SDK connection context
  • renderVoiceUI — parameterized orb UI (idle/listening/thinking/speaking/error states, transcript feed, toast notifications)
  • SQL migrations for voice session history and email event audit log
  • Wrangler config guide

Stack: @cloudflare/voice · agents SDK · Workers AI (no external API keys)


Installation

npm install @stackbilt/cf-voice

Peer dependencies:

npm install @cloudflare/voice agents

Quick start

1. Create your Durable Object

import { BaseVoiceAgent } from '@stackbilt/cf-voice';
import type { VoiceTurnContext } from '@stackbilt/cf-voice';

interface Env {
  AI: Ai;
}

export class MyVoiceDO extends BaseVoiceAgent<Env> {
  async handleTurn(transcript: string, ctx: VoiceTurnContext): Promise<string> {
    // Return whatever text the agent should speak.
    return `You said: ${transcript}`;
  }
}

2. Wire the route in your Worker

import { renderVoiceUI } from '@stackbilt/cf-voice';
import { routeAgentRequest } from 'agents';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (new URL(request.url).pathname === '/voice') {
      return new Response(renderVoiceUI({ productName: 'My App' }), {
        headers: { 'Content-Type': 'text/html' },
      });
    }
    return (await routeAgentRequest(request, env)) ?? new Response('Not found', { status: 404 });
  },
};

export { MyVoiceDO };

3. Add bindings to wrangler.toml

See wrangler-snippet.md for the full config block. The essentials:

[ai]
binding = "AI"

[durable_objects]
bindings = [{ name = "MyVoiceDO", class_name = "MyVoiceDO" }]

[[migrations]]
tag = "v1"
new_sqlite_classes = ["MyVoiceDO"]

That's it. Open /voice in a browser, tap the orb.


API

BaseVoiceAgent<TEnv>

Abstract Durable Object class. Extend it and implement handleTurn.

abstract class BaseVoiceAgent<TEnv extends VoiceEnv> {
  // Override to change the TTS voice character. Default: 'asteria'
  protected readonly speaker: string;

  // Wire up VoiceActions to handle intents before handleTurn is called.
  protected readonly actions: VoiceAction<TEnv>[];

  // Last user utterance — available to actions and subclass methods.
  lastQuestion: string;
  // Last agent response — available to actions and subclass methods.
  lastAnswer: string;

  // Extract session + canonical IDs from a turn context.
  ids(ctx: VoiceTurnContext): SessionIds;

  // Implement this. Return the text the agent will speak.
  abstract handleTurn(transcript: string, ctx: VoiceTurnContext): Promise<string>;
}

VoiceEnv minimum:

interface VoiceEnv {
  AI: { run(model: string, input: Record<string, unknown>): Promise<unknown> };
}

emailSummaryAction(opts)

Built-in VoiceAction that detects "email me a summary of that" intent and calls your send function.

import { emailSummaryAction } from '@stackbilt/cf-voice';

export class MyVoiceDO extends BaseVoiceAgent<MyEnv> {
  protected readonly actions = [
    emailSummaryAction<MyEnv>({
      send: async (env, { question, answer, sessionId, canonicalId }) => {
        const ok = await myEmailService.send({ question, answer });
        return ok ? { ok: true, sent: 1 } : { ok: false, reason: 'send failed' };
      },
    }),
  ];

  async handleTurn(transcript, ctx) { /* ... */ }
}

The action fires when the transcript matches phrases like "email me that", "send me a summary", "email this". It reads lastQuestion/lastAnswer from the agent instance and passes them to your send function. If nothing has been discussed yet, it speaks a prompt asking the user to ask a question first.

Options:

| Field | Type | Description | |---|---|---| | send | (env, payload) => Promise<{ ok: true; sent: number } \| { ok: false; reason: string }> | Called when intent is detected. | | nothingYetMessage | string? | Spoken when lastQuestion is empty. |


renderVoiceUI(opts?)

Returns a complete HTML page as a string. Mount it on any route.

Key options:

| Option | Default | Description | |---|---|---| | agentDOName | 'VoiceSessionDO' | DO class name registered in wrangler.toml | | productName | 'Voice' | Shown in the header | | heading | 'Ask anything.' | Large heading on the page | | subheading | 'Tap the circle to start talking.' | Subtext under the heading | | examples | [] | Chips that send pre-set text: [{ label: 'Missed calls?', text: 'How many missed calls this week?' }] | | accentColor | '#d97706' | Amber accent used for the orb, chips, and agent response bubbles | | backHref / backLabel | '/' / '← Back' | Header back link | | identityParam | 'canonical_id' | URL query param forwarded to the agent as identity context | | showSignalChip | false | Fetches /signals/active and shows open issue count in the header | | footerText | 'Powered by Stackbilt LLC' | Footer attribution |

The client loads VoiceClient from esm.sh/@cloudflare/[email protected]/client — no bundler needed.


extractIds(ctx)

Pulls sessionId and canonicalId from a VoiceTurnContext. The agents SDK doesn't expose these in a typed way; this function probes the known locations across runtime versions.

import { extractIds } from '@stackbilt/cf-voice';

const { sessionId, canonicalId } = extractIds(ctx);

isEmailSummaryIntent(transcript)

The raw intent detector used by emailSummaryAction. Export it if you want to build custom action logic around the same heuristic.

import { isEmailSummaryIntent } from '@stackbilt/cf-voice';

isEmailSummaryIntent('can you email me a summary of that'); // true

Voice actions

A VoiceAction is checked before handleTurn. The first action whose detect() returns true handles the turn.

interface VoiceAction<TEnv> {
  detect(transcript: string): boolean;
  handle(
    transcript: string,
    ctx: VoiceTurnContext,
    env: TEnv,
    agent: { lastQuestion: string; lastAnswer: string; ids(ctx): SessionIds },
  ): Promise<string>;
}

You can build your own and pass them in protected readonly actions = [...].


Database migrations

Optional. Copy from migrations/ if you want D1 audit records:

  • 0001_voice_sessions.sql — one row per call (identity_id, started_at, ended_at, transcript)
  • 0002_voice_email_events.sql — one row per email send attempt with status, recipients, and provider message IDs

These are generic — identity_id is whatever identifier makes sense in your system (canonical_id, user_id, etc).


Reference consumer

See example-consumer.ts for how opsbrain's original 70-line VoiceSessionDO reduces to ~25 lines using this package.


Requirements

  • Cloudflare Workers with nodejs_compat flag
  • Workers AI binding ([ai])
  • VoiceSessionDO registered as new_sqlite_classes (required by the agents SDK — see wrangler-snippet.md)
  • @cloudflare/voice ^0.1.3 and agents ^0.12.0 as peer dependencies