voice-runtime 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,20 +8,29 @@
8
8
  [TVIC](https://github.com/Kkartik14/TVIC), a provider-neutral runtime for realtime
9
9
  voice agents with bring-your-own telephony, speech, model, and synthesis providers.
10
10
 
11
+ New to the package? Start with the [beginner's guide](https://github.com/Kkartik14/TVIC/blob/main/docs/getting-started.md).
12
+ Use the [voice-agent guide](https://github.com/Kkartik14/TVIC/blob/main/docs/building-a-voice-agent.md)
13
+ for application design, [providers](https://github.com/Kkartik14/TVIC/blob/main/docs/providers.md)
14
+ for model setup, and [transports](https://github.com/Kkartik14/TVIC/blob/main/docs/transports.md)
15
+ for browser and phone connections.
16
+
11
17
  ## Status
12
18
 
13
- The package is the public Node.js entry point for the TVIC runtime. The first
14
- release targets Node 20 through 26 and ships both ESM and CommonJS entry points.
15
- It is server-side only: browser audio connects to a Node server through the
16
- browser-audio transport adapter, while browser imports from this package remain
17
- out of scope for the first release.
19
+ `voice-runtime@1.0.1` is the public Node.js entry point for the TVIC runtime. It
20
+ supports Node.js 22, 24, and 26, ships both ESM and CommonJS entry points, and is
21
+ server-side only. The main CI compatibility matrix exercises all three supported
22
+ lines.
23
+
24
+ Browser audio connects to a Node server through the browser-audio transport
25
+ adapter. Browser code does not import this package directly.
18
26
 
19
- The stable managed API assembles the STT → LLM → TTS pipeline from a prompt and
27
+ The stable managed API assembles the STT -> LLM -> TTS pipeline from a prompt and
20
28
  provider configuration. The host application still owns its HTTP server,
21
29
  webhook authentication, browser-session authentication, and domain tools.
22
30
 
23
- This README describes the intended public contract; release status is tracked in
24
- the repository's `local/release/1.0.0/checklist.md`.
31
+ For a system overview, read [How it works](https://github.com/Kkartik14/TVIC/blob/main/docs/how-it-works.md).
32
+ For testing and deployment, read the [testing guide](https://github.com/Kkartik14/TVIC/blob/main/docs/testing.md)
33
+ and [deployment guide](https://github.com/Kkartik14/TVIC/blob/main/docs/deploying.md).
25
34
 
26
35
  ## Install
27
36
 
@@ -32,8 +41,29 @@ npm install voice-runtime
32
41
  The package has no provider SDK lock-in. You may use the built-in adapters, pass
33
42
  your own provider implementations, or mix both approaches.
34
43
 
44
+ ## Optional AI agent guidance
45
+
46
+ To give Claude Code, Codex, or another compatible coding agent project-scoped
47
+ TVIC guidance, run this explicitly from your project directory:
48
+
49
+ ```sh
50
+ npx voice-runtime skills install
51
+ ```
52
+
53
+ The command asks for `y` before writing `.claude/skills/tvic/SKILL.md` for Claude
54
+ Code and `.agents/skills/tvic/SKILL.md` for Codex and OpenRouter-compatible
55
+ agents. It does not run automatically during `npm install`, write to a home
56
+ directory, or overwrite an existing skill without `--force`. Use
57
+ `npx voice-runtime skills install --yes` in automation. See the [AI coding-agent
58
+ guide](https://github.com/Kkartik14/TVIC/blob/main/docs/ai-coding-agents.md) and
59
+ [skills reference](https://github.com/Kkartik14/TVIC/blob/main/docs/agent-skills.md).
60
+
35
61
  ## Prompt-first agent
36
62
 
63
+ The prompt-first API assembles the STT -> LLM -> TTS pipeline. It still needs an
64
+ authenticated transport `CallHandle`; `npm install` does not create a public
65
+ audio endpoint or a phone call for your application.
66
+
37
67
  ```ts
38
68
  import { createVoiceAgent } from "voice-runtime";
39
69
 
@@ -43,9 +73,9 @@ const agent = createVoiceAgent({
43
73
  telephony: { provider: "web-client-audio" },
44
74
  stt: { provider: "deepgram", apiKey: process.env.DEEPGRAM_API_KEY },
45
75
  llm: {
46
- provider: "openai",
47
- apiKey: process.env.OPENAI_API_KEY,
48
- model: "gpt-4.1-mini",
76
+ provider: "groq",
77
+ apiKey: process.env.GROQ_API_KEY,
78
+ model: "openai/gpt-oss-20b",
49
79
  },
50
80
  tts: {
51
81
  provider: "cartesia",
@@ -130,14 +160,17 @@ outside the realtime critical path.
130
160
 
131
161
  Each stage is configured independently. Built-in options currently include:
132
162
 
163
+ The complete matrix, catalog dates, official provider links, and live-test
164
+ guidance are in the [provider guide](https://github.com/Kkartik14/TVIC/blob/main/docs/providers.md).
165
+
133
166
  - STT: Deepgram, Sarvam, ElevenLabs Scribe, AssemblyAI, and Soniox.
134
- - LLM: OpenAI Responses.
167
+ - LLM: Groq Chat Completions (the reference path) and an optional OpenAI Responses adapter.
135
168
  - TTS: Cartesia and ElevenLabs.
136
169
  - Transport: browser audio and inbound Twilio Media Streams.
137
170
 
138
171
  Maturity is intentionally explicit: Web Client Audio and inbound Twilio Media
139
- Streams are the stable transport candidates for `1.0.0`. The paid STT, LLM,
140
- and TTS adapters are currently `experimental`—their deterministic protocol
172
+ Streams are the stable transport paths in `1.0.1`. The paid STT, LLM,
173
+ and TTS adapters are currently `experimental`. Their deterministic protocol
141
174
  coverage passes, but each still needs a credential-gated live-service check
142
175
  before release notes can call it stable. The root export `PROVIDER_STABILITY`
143
176
  contains the machine-readable labels.
@@ -153,6 +186,7 @@ created.
153
186
  | ElevenLabs | `ELEVENLABS_API_KEY` |
154
187
  | AssemblyAI | `ASSEMBLYAI_API_KEY` |
155
188
  | Soniox | `SONIOX_API_KEY` |
189
+ | Groq | `GROQ_API_KEY` |
156
190
  | OpenAI | `OPENAI_API_KEY` |
157
191
  | Cartesia | `CARTESIA_API_KEY` |
158
192
 
@@ -160,7 +194,7 @@ STT, LLM, and TTS models are independent settings. TTS voice selection is also
160
194
  independent. Built-in TTS can read `CARTESIA_VOICE_ID` or
161
195
  `ELEVENLABS_VOICE_ID` for its voice. For complete provider-specific control,
162
196
  pass a constructed provider instance instead of a built-in configuration object.
163
- Built-in STT and OpenAI-compatible LLM configurations reject models outside the
197
+ Built-in STT and LLM configurations reject models outside the
164
198
  dated TVIC catalog unless `allowUnknownModel: true` is explicitly set.
165
199
 
166
200
  Tools are application-owned functions. Define their schema and executor once,
@@ -184,7 +218,7 @@ const bookAppointment = defineTool<{ date: string }, { booked: boolean }>({
184
218
  const providers = {
185
219
  telephony: { provider: "web-client-audio" as const },
186
220
  stt: { provider: "deepgram" as const, apiKey: process.env.DEEPGRAM_API_KEY },
187
- llm: { provider: "openai" as const, apiKey: process.env.OPENAI_API_KEY },
221
+ llm: { provider: "groq" as const, apiKey: process.env.GROQ_API_KEY },
188
222
  tts: {
189
223
  provider: "cartesia" as const,
190
224
  apiKey: process.env.CARTESIA_API_KEY,
@@ -206,10 +240,14 @@ application's responsibility.
206
240
  ## Composable API
207
241
 
208
242
  Advanced runtime, media, provider, normalized-error, and durable-adapter APIs
209
- are exported from the package root. There are no advanced subpath imports in the
210
- first release. This lets a developer start with `createVoiceAgent` and later own
243
+ are exported from the package root. There are no advanced subpath imports in
244
+ 1.0.1. This lets a developer start with `createVoiceAgent` and later own
211
245
  the runtime/session/pipeline boundaries without changing package names.
212
246
 
247
+ Read the [voice-agent guide](https://github.com/Kkartik14/TVIC/blob/main/docs/building-a-voice-agent.md)
248
+ for the managed to composable migration path and the [API reference](https://github.com/Kkartik14/TVIC/blob/main/docs/api-reference.md)
249
+ for the root exports.
250
+
213
251
  The host remains responsible for creating authenticated transport connections,
214
252
  owning HTTP/webhook lifecycle, and implementing application-domain tools. TVIC
215
253
  does not automatically create public unauthenticated endpoints or choose a
@@ -221,6 +259,9 @@ The root also exposes the PostgreSQL and Redis durability building blocks. The
221
259
  database drivers are intentionally injected, so install and configure `pg` and
222
260
  `redis` in your application:
223
261
 
262
+ The [persistence guide](https://github.com/Kkartik14/TVIC/blob/main/docs/persistence.md)
263
+ covers migrations, memory scopes, Docker, ownership, and production retention.
264
+
224
265
  ```ts
225
266
  import {
226
267
  createPostgresMemory,
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import readline from "node:readline/promises";
7
+
8
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ const skillSource = path.join(packageRoot, "skills", "tvic", "SKILL.md");
10
+
11
+ const AGENT_TARGETS = Object.freeze({
12
+ claude: Object.freeze({
13
+ label: "Claude Code",
14
+ relativePath: path.join(".claude", "skills", "tvic", "SKILL.md"),
15
+ }),
16
+ codex: Object.freeze({
17
+ label: "Codex",
18
+ relativePath: path.join(".agents", "skills", "tvic", "SKILL.md"),
19
+ }),
20
+ openrouter: Object.freeze({
21
+ label: "OpenRouter-compatible agents",
22
+ relativePath: path.join(".agents", "skills", "tvic", "SKILL.md"),
23
+ }),
24
+ });
25
+
26
+ const DEFAULT_AGENTS = Object.freeze(["claude", "codex", "openrouter"]);
27
+
28
+ function printHelp() {
29
+ process.stdout.write("voice-runtime\n\n");
30
+ process.stdout.write("Install the optional TVIC guidance skill in the current project.\n\n");
31
+ process.stdout.write("Usage:\n");
32
+ process.stdout.write(" voice-runtime skills install [options]\n\n");
33
+ process.stdout.write("Options:\n");
34
+ process.stdout.write(" --agent <name> Target claude, codex, or openrouter. Repeatable.\n");
35
+ process.stdout.write(" --yes Confirm installation without a prompt.\n");
36
+ process.stdout.write(" --dry-run Show planned files without changing anything.\n");
37
+ process.stdout.write(" --force Replace an existing TVIC skill file.\n");
38
+ process.stdout.write(" --help Show this help.\n\n");
39
+ process.stdout.write("Project paths:\n");
40
+ process.stdout.write(" Claude Code: .claude/skills/tvic/SKILL.md\n");
41
+ process.stdout.write(" Codex/OpenRouter: .agents/skills/tvic/SKILL.md\n");
42
+ }
43
+
44
+ function parseArgs(args) {
45
+ const options = {
46
+ agents: [],
47
+ dryRun: false,
48
+ force: false,
49
+ help: false,
50
+ yes: false,
51
+ };
52
+
53
+ for (let index = 0; index < args.length; index += 1) {
54
+ const argument = args[index];
55
+ if (argument === "--help" || argument === "-h") {
56
+ options.help = true;
57
+ continue;
58
+ }
59
+ if (argument === "--yes" || argument === "-y") {
60
+ options.yes = true;
61
+ continue;
62
+ }
63
+ if (argument === "--dry-run") {
64
+ options.dryRun = true;
65
+ continue;
66
+ }
67
+ if (argument === "--force" || argument === "-f") {
68
+ options.force = true;
69
+ continue;
70
+ }
71
+
72
+ if (argument === "--agent") {
73
+ const value = args[index + 1];
74
+ if (!value) throw new Error("--agent requires a value");
75
+ options.agents.push(...value.split(","));
76
+ index += 1;
77
+ continue;
78
+ }
79
+
80
+ if (argument.startsWith("--agent=")) {
81
+ options.agents.push(...argument.slice("--agent=".length).split(","));
82
+ continue;
83
+ }
84
+
85
+ throw new Error(`Unknown option: ${argument}`);
86
+ }
87
+
88
+ return options;
89
+ }
90
+
91
+ function selectedAgents(requestedAgents) {
92
+ const names = requestedAgents.length > 0 ? requestedAgents : DEFAULT_AGENTS;
93
+ const selected = new Set();
94
+
95
+ for (const requested of names) {
96
+ const name = requested.trim().toLowerCase();
97
+ if (name === "all") {
98
+ for (const agent of DEFAULT_AGENTS) selected.add(agent);
99
+ continue;
100
+ }
101
+ if (!Object.hasOwn(AGENT_TARGETS, name)) {
102
+ throw new Error(`Unknown agent "${requested}". Choose claude, codex, or openrouter.`);
103
+ }
104
+ selected.add(name);
105
+ }
106
+
107
+ return [...selected];
108
+ }
109
+
110
+ function plannedFiles(agentNames, projectDirectory) {
111
+ const plans = new Map();
112
+
113
+ for (const agentName of agentNames) {
114
+ const target = AGENT_TARGETS[agentName];
115
+ const absolutePath = path.resolve(projectDirectory, target.relativePath);
116
+ const existing = plans.get(absolutePath);
117
+ if (existing) {
118
+ existing.agentNames.push(agentName);
119
+ } else {
120
+ plans.set(absolutePath, {
121
+ agentNames: [agentName],
122
+ absolutePath,
123
+ relativePath: path.relative(projectDirectory, absolutePath),
124
+ });
125
+ }
126
+ }
127
+
128
+ return [...plans.values()];
129
+ }
130
+
131
+ async function inspectPlans(plans, source, force) {
132
+ const inspected = [];
133
+
134
+ for (const plan of plans) {
135
+ let current;
136
+ try {
137
+ current = await readFile(plan.absolutePath, "utf8");
138
+ } catch (error) {
139
+ if (error?.code !== "ENOENT") throw error;
140
+ }
141
+
142
+ let action = "create";
143
+ if (current !== undefined) {
144
+ action = current === source ? "unchanged" : force ? "replace" : "conflict";
145
+ }
146
+ inspected.push({ ...plan, action });
147
+ }
148
+
149
+ return inspected;
150
+ }
151
+
152
+ async function confirmInstallation(projectDirectory, plans) {
153
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
154
+ throw new Error(
155
+ "No interactive terminal is available. Re-run with --yes to confirm installation.",
156
+ );
157
+ }
158
+
159
+ const labels = [...new Set(plans.flatMap((plan) => plan.agentNames))]
160
+ .map((agentName) => AGENT_TARGETS[agentName].label)
161
+ .join(", ");
162
+ const prompt = `Install TVIC guidance for ${labels} in ${projectDirectory}? (y/N) `;
163
+ const terminal = readline.createInterface({ input: process.stdin, output: process.stdout });
164
+ try {
165
+ const answer = await terminal.question(prompt);
166
+ return answer.trim().toLowerCase() === "y";
167
+ } finally {
168
+ terminal.close();
169
+ }
170
+ }
171
+
172
+ async function installPlans(plans, source) {
173
+ for (const plan of plans) {
174
+ if (plan.action === "unchanged") {
175
+ process.stdout.write(`Already installed: ${plan.relativePath}\n`);
176
+ continue;
177
+ }
178
+ await mkdir(path.dirname(plan.absolutePath), { recursive: true });
179
+ await writeFile(plan.absolutePath, source, "utf8");
180
+ process.stdout.write(
181
+ `${plan.action === "replace" ? "Updated" : "Installed"}: ${plan.relativePath}\n`,
182
+ );
183
+ }
184
+ }
185
+
186
+ async function installSkills(options) {
187
+ const projectDirectory = process.cwd();
188
+ const agents = selectedAgents(options.agents);
189
+ const source = await readFile(skillSource, "utf8");
190
+ const plans = await inspectPlans(plannedFiles(agents, projectDirectory), source, options.force);
191
+
192
+ process.stdout.write(`Project: ${projectDirectory}\n`);
193
+ for (const plan of plans) {
194
+ const agentsForPath = [...new Set(plan.agentNames)]
195
+ .map((agentName) => AGENT_TARGETS[agentName].label)
196
+ .join(" and ");
197
+ process.stdout.write(` ${plan.relativePath} (${agentsForPath})\n`);
198
+ }
199
+
200
+ if (options.dryRun) {
201
+ process.stdout.write("Dry run: no files changed.\n");
202
+ return;
203
+ }
204
+
205
+ const conflicts = plans.filter((plan) => plan.action === "conflict");
206
+ if (conflicts.length > 0) {
207
+ throw new Error(
208
+ `Existing TVIC skill files differ: ${conflicts
209
+ .map((plan) => plan.relativePath)
210
+ .join(", ")}. Use --force to replace them.`,
211
+ );
212
+ }
213
+
214
+ if (!options.yes && !(await confirmInstallation(projectDirectory, plans))) {
215
+ process.stdout.write("No files changed.\n");
216
+ return;
217
+ }
218
+
219
+ await installPlans(plans, source);
220
+ process.stdout.write(
221
+ "TVIC agent guidance is ready. Restart the agent if it does not appear automatically.\n",
222
+ );
223
+ }
224
+
225
+ async function main() {
226
+ const [command, subcommand, ...args] = process.argv.slice(2);
227
+ if (command === "--help" || command === "-h" || !command) {
228
+ printHelp();
229
+ return;
230
+ }
231
+ if (command !== "skills" || subcommand !== "install") {
232
+ throw new Error("Use: voice-runtime skills install [options]");
233
+ }
234
+
235
+ const options = parseArgs(args);
236
+ if (options.help) {
237
+ printHelp();
238
+ return;
239
+ }
240
+ await installSkills(options);
241
+ }
242
+
243
+ main().catch((error) => {
244
+ process.stderr.write(
245
+ `voice-runtime: ${error instanceof Error ? error.message : String(error)}\n`,
246
+ );
247
+ process.exitCode = 1;
248
+ });