voice-runtime 1.0.0 → 1.0.1
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 +34 -11
- package/bin/voice-runtime.mjs +248 -0
- package/package.json +6 -1
- package/skills/tvic/SKILL.md +79 -0
package/README.md
CHANGED
|
@@ -10,18 +10,21 @@ voice agents with bring-your-own telephony, speech, model, and synthesis provide
|
|
|
10
10
|
|
|
11
11
|
## Status
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
`voice-runtime@1.0.0` is the public Node.js entry point for the TVIC runtime. It
|
|
14
|
+
declares Node.js 20 or newer, ships both ESM and CommonJS entry points, and is
|
|
15
|
+
server-side only. The main CI compatibility matrix exercises Node 22, 24, and 26;
|
|
16
|
+
Node 20 remains the package engine floor.
|
|
17
|
+
|
|
18
|
+
Browser audio connects to a Node server through the browser-audio transport
|
|
19
|
+
adapter. Browser code does not import this package directly.
|
|
18
20
|
|
|
19
21
|
The stable managed API assembles the STT → LLM → TTS pipeline from a prompt and
|
|
20
22
|
provider configuration. The host application still owns its HTTP server,
|
|
21
23
|
webhook authentication, browser-session authentication, and domain tools.
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
the
|
|
25
|
+
If you are new to voice systems, read the [beginner guide on GitHub](https://github.com/Kkartik14/TVIC/blob/main/docs/start-here.md)
|
|
26
|
+
before using the lower-level examples. The [provider guide](https://github.com/Kkartik14/TVIC/blob/main/docs/providers.md)
|
|
27
|
+
explains credentials, model selection, and maturity labels.
|
|
25
28
|
|
|
26
29
|
## Install
|
|
27
30
|
|
|
@@ -32,8 +35,28 @@ npm install voice-runtime
|
|
|
32
35
|
The package has no provider SDK lock-in. You may use the built-in adapters, pass
|
|
33
36
|
your own provider implementations, or mix both approaches.
|
|
34
37
|
|
|
38
|
+
## Optional AI agent guidance
|
|
39
|
+
|
|
40
|
+
To give Claude Code, Codex, or another compatible coding agent project-scoped
|
|
41
|
+
TVIC guidance, run this explicitly from your project directory:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npx voice-runtime skills install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The command asks for `y` before writing `.claude/skills/tvic/SKILL.md` for Claude
|
|
48
|
+
Code and `.agents/skills/tvic/SKILL.md` for Codex and OpenRouter-compatible
|
|
49
|
+
agents. It does not run automatically during `npm install`, write to a home
|
|
50
|
+
directory, or overwrite an existing skill without `--force`. Use
|
|
51
|
+
`npx voice-runtime skills install --yes` in automation. See the [AI agent skills
|
|
52
|
+
guide](https://github.com/Kkartik14/TVIC/blob/main/docs/agent-skills.md).
|
|
53
|
+
|
|
35
54
|
## Prompt-first agent
|
|
36
55
|
|
|
56
|
+
The prompt-first API assembles the STT → LLM → TTS pipeline. It still needs an
|
|
57
|
+
authenticated transport `CallHandle`; `npm install` does not create a public
|
|
58
|
+
audio endpoint or a phone call for your application.
|
|
59
|
+
|
|
37
60
|
```ts
|
|
38
61
|
import { createVoiceAgent } from "voice-runtime";
|
|
39
62
|
|
|
@@ -136,8 +159,8 @@ Each stage is configured independently. Built-in options currently include:
|
|
|
136
159
|
- Transport: browser audio and inbound Twilio Media Streams.
|
|
137
160
|
|
|
138
161
|
Maturity is intentionally explicit: Web Client Audio and inbound Twilio Media
|
|
139
|
-
Streams are the stable transport
|
|
140
|
-
and TTS adapters are currently `experimental
|
|
162
|
+
Streams are the stable transport paths in `1.0.0`. The paid STT, LLM,
|
|
163
|
+
and TTS adapters are currently `experimental`. Their deterministic protocol
|
|
141
164
|
coverage passes, but each still needs a credential-gated live-service check
|
|
142
165
|
before release notes can call it stable. The root export `PROVIDER_STABILITY`
|
|
143
166
|
contains the machine-readable labels.
|
|
@@ -206,8 +229,8 @@ application's responsibility.
|
|
|
206
229
|
## Composable API
|
|
207
230
|
|
|
208
231
|
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
|
-
|
|
232
|
+
are exported from the package root. There are no advanced subpath imports in
|
|
233
|
+
1.0.0. This lets a developer start with `createVoiceAgent` and later own
|
|
211
234
|
the runtime/session/pipeline boundaries without changing package names.
|
|
212
235
|
|
|
213
236
|
The host remains responsible for creating authenticated transport connections,
|
|
@@ -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
|
+
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voice-runtime",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Local-first, provider-neutral runtime for realtime voice agents, powered by TVIC.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"voice-runtime": "./bin/voice-runtime.mjs"
|
|
8
|
+
},
|
|
6
9
|
"main": "./dist/index.cjs",
|
|
7
10
|
"module": "./dist/index.js",
|
|
8
11
|
"types": "./dist/index.d.ts",
|
|
@@ -16,6 +19,8 @@
|
|
|
16
19
|
},
|
|
17
20
|
"files": [
|
|
18
21
|
"dist",
|
|
22
|
+
"bin",
|
|
23
|
+
"skills",
|
|
19
24
|
"README.md",
|
|
20
25
|
"LICENSE"
|
|
21
26
|
],
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tvic
|
|
3
|
+
description: Use when building, configuring, testing, or debugging a voice agent with voice-runtime or TVIC. Covers the prompt-first API, composable runtime, providers, transports, events, persistence, and security boundaries.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TVIC development guidance
|
|
7
|
+
|
|
8
|
+
Use this skill when the project uses `voice-runtime` or the TVIC runtime. It is
|
|
9
|
+
guidance for the coding agent, not a replacement for the host application's
|
|
10
|
+
authorization or business rules.
|
|
11
|
+
|
|
12
|
+
## Start with the installed package
|
|
13
|
+
|
|
14
|
+
Before suggesting code, inspect the installed `voice-runtime` version and read
|
|
15
|
+
`node_modules/voice-runtime/README.md` when it exists. Prefer APIs exported from
|
|
16
|
+
the package root. Do not invent exports or copy repository-only imports into a
|
|
17
|
+
consumer application.
|
|
18
|
+
|
|
19
|
+
## Core model
|
|
20
|
+
|
|
21
|
+
TVIC runs a server-side cascaded voice pipeline:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
transport -> speech to text -> turn handling -> language model and tools -> text to speech -> transport
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The host application supplies an authenticated `CallHandle`, chooses providers,
|
|
28
|
+
and owns the HTTP or WebSocket lifecycle. A prompt configures behavior, but a
|
|
29
|
+
prompt does not create a phone call, public endpoint, user authentication, or
|
|
30
|
+
permission to change business data.
|
|
31
|
+
|
|
32
|
+
## Managed API
|
|
33
|
+
|
|
34
|
+
Use `createVoiceAgent` when the application wants TVIC to assemble the standard
|
|
35
|
+
speech to text, language model, and text to speech pipeline. Configure every
|
|
36
|
+
required provider explicitly. Start a session only after the host has
|
|
37
|
+
authenticated the connection and created or received its `CallHandle`.
|
|
38
|
+
|
|
39
|
+
The managed run is both an event stream and an awaitable final result. Consume
|
|
40
|
+
events for live UI, transcripts, errors, and audio state, then await the same run
|
|
41
|
+
for the final outcome. Do not assume that audio sent to a socket was heard. Use
|
|
42
|
+
the transport's playout acknowledgement when deciding whether a response was
|
|
43
|
+
delivered.
|
|
44
|
+
|
|
45
|
+
## Composable API
|
|
46
|
+
|
|
47
|
+
Use the lower-level root exports when the application needs to own part of the
|
|
48
|
+
pipeline or provide a custom STT, language model, text to speech, telephony,
|
|
49
|
+
memory, or durable-store implementation. Preserve TVIC's normalized contracts,
|
|
50
|
+
capability declarations, cancellation behavior, and event semantics.
|
|
51
|
+
|
|
52
|
+
## Providers and models
|
|
53
|
+
|
|
54
|
+
Use built-in adapters only with their documented provider names, credentials,
|
|
55
|
+
models, and maturity labels. Do not describe an experimental adapter as stable.
|
|
56
|
+
If a provider or model is not in the built-in catalog, prefer a constructed
|
|
57
|
+
custom provider or the explicit unknown-model option rather than silently
|
|
58
|
+
changing the user's selection.
|
|
59
|
+
|
|
60
|
+
## Tools and security
|
|
61
|
+
|
|
62
|
+
Tools belong to the host application. Validate their inputs and outputs, enforce
|
|
63
|
+
authorization and confirmation for irreversible actions, and make retries safe
|
|
64
|
+
when an action can be performed twice. Keep provider keys, signing secrets,
|
|
65
|
+
database URLs, transcripts, and recordings on the server. Treat caller speech,
|
|
66
|
+
transcripts, prompts, tool arguments, and external controller data as untrusted.
|
|
67
|
+
|
|
68
|
+
## Testing and troubleshooting
|
|
69
|
+
|
|
70
|
+
For a consumer project, test the exact transport and provider configuration being
|
|
71
|
+
used. Mock providers are useful for deterministic tests, but they do not prove a
|
|
72
|
+
live provider works. When debugging a missing response, trace the complete path:
|
|
73
|
+
authenticated transport, inbound media, transcript finality, completed turn,
|
|
74
|
+
model or tool result, synthesized audio, transport receipt, and playout evidence.
|
|
75
|
+
|
|
76
|
+
When changing a public contract, add a test through the public package entry
|
|
77
|
+
point and update the relevant documentation. Report uncertainty instead of
|
|
78
|
+
claiming delivery, cancellation, provider stability, or authorization behavior
|
|
79
|
+
that the underlying integration does not prove.
|