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 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
- 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.
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
- This README describes the intended public contract; release status is tracked in
24
- the repository's `local/release/1.0.0/checklist.md`.
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 candidates for `1.0.0`. The paid STT, LLM,
140
- and TTS adapters are currently `experimental`—their deterministic protocol
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 the
210
- first release. This lets a developer start with `createVoiceAgent` and later own
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.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.