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 +59 -18
- package/bin/voice-runtime.mjs +248 -0
- package/dist/index.cjs +7353 -2064
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +372 -158
- package/dist/index.js +7342 -2059
- package/dist/index.js.map +4 -4
- package/package.json +7 -2
- package/skills/tvic/SKILL.md +79 -0
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
24
|
-
the
|
|
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: "
|
|
47
|
-
apiKey: process.env.
|
|
48
|
-
model: "gpt-
|
|
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
|
|
140
|
-
and TTS adapters are currently `experimental
|
|
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
|
|
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: "
|
|
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
|
|
210
|
-
|
|
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
|
+
});
|