agent-voice 0.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/dist/ask-MG4CURKM.js +93 -0
- package/dist/auth-KIIHTB6C.js +37 -0
- package/dist/chunk-4RFF3WHI.js +183 -0
- package/dist/chunk-EF2ZPBZD.js +28 -0
- package/dist/chunk-GUTKT66X.js +62 -0
- package/dist/cli.js +97 -0
- package/dist/config-6FQI6YPB.js +16 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +361 -0
- package/dist/say-KJWVP5OC.js +62 -0
- package/dist/types-UKEQRG2U.js +15 -0
- package/package.json +49 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createAudioPlayer,
|
|
4
|
+
createAudioRecorder,
|
|
5
|
+
createRealtimeSession
|
|
6
|
+
} from "./chunk-4RFF3WHI.js";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_VOICE
|
|
9
|
+
} from "./chunk-EF2ZPBZD.js";
|
|
10
|
+
|
|
11
|
+
// src/ask.ts
|
|
12
|
+
async function ask(message, options = {}) {
|
|
13
|
+
const {
|
|
14
|
+
voice = DEFAULT_VOICE,
|
|
15
|
+
timeout = 30,
|
|
16
|
+
ack = false,
|
|
17
|
+
auth,
|
|
18
|
+
createPlayer = createAudioPlayer,
|
|
19
|
+
createRecorder = createAudioRecorder
|
|
20
|
+
} = options;
|
|
21
|
+
const player = createPlayer();
|
|
22
|
+
player.start();
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
let recorder = null;
|
|
25
|
+
let transcript = "";
|
|
26
|
+
let timeoutTimer = null;
|
|
27
|
+
let speechDetected = false;
|
|
28
|
+
let cleaned = false;
|
|
29
|
+
let resolved = false;
|
|
30
|
+
async function cleanup() {
|
|
31
|
+
if (cleaned) return;
|
|
32
|
+
cleaned = true;
|
|
33
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
34
|
+
recorder?.stop();
|
|
35
|
+
recorder?.close();
|
|
36
|
+
await player.drain();
|
|
37
|
+
session.close();
|
|
38
|
+
}
|
|
39
|
+
function finish() {
|
|
40
|
+
if (resolved) return;
|
|
41
|
+
resolved = true;
|
|
42
|
+
cleanup().then(() => resolve(transcript));
|
|
43
|
+
}
|
|
44
|
+
const session = createRealtimeSession({
|
|
45
|
+
voice,
|
|
46
|
+
mode: "default",
|
|
47
|
+
ack,
|
|
48
|
+
auth,
|
|
49
|
+
onAudioDelta(pcm16) {
|
|
50
|
+
player.write(pcm16);
|
|
51
|
+
},
|
|
52
|
+
onTranscript(text) {
|
|
53
|
+
transcript = text;
|
|
54
|
+
if (!ack) finish();
|
|
55
|
+
},
|
|
56
|
+
onSpeechStarted() {
|
|
57
|
+
speechDetected = true;
|
|
58
|
+
if (timeoutTimer) {
|
|
59
|
+
clearTimeout(timeoutTimer);
|
|
60
|
+
timeoutTimer = null;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
onInitialResponseDone() {
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
recorder = createRecorder();
|
|
66
|
+
recorder.onData((pcm16) => {
|
|
67
|
+
session.sendAudio(pcm16);
|
|
68
|
+
});
|
|
69
|
+
recorder.start();
|
|
70
|
+
}, 500);
|
|
71
|
+
timeoutTimer = setTimeout(() => {
|
|
72
|
+
if (!speechDetected) {
|
|
73
|
+
cleanup();
|
|
74
|
+
reject(new Error(`No speech detected within ${timeout}s timeout`));
|
|
75
|
+
}
|
|
76
|
+
}, timeout * 1e3);
|
|
77
|
+
},
|
|
78
|
+
onDone() {
|
|
79
|
+
if (ack) finish();
|
|
80
|
+
},
|
|
81
|
+
async onError(error) {
|
|
82
|
+
await cleanup();
|
|
83
|
+
reject(new Error(error));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
session.connect().then(() => {
|
|
87
|
+
session.sendMessage(message);
|
|
88
|
+
}, reject);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
ask
|
|
93
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
writeAuthConfig
|
|
4
|
+
} from "./chunk-GUTKT66X.js";
|
|
5
|
+
import "./chunk-EF2ZPBZD.js";
|
|
6
|
+
|
|
7
|
+
// src/auth.ts
|
|
8
|
+
import { input, password } from "@inquirer/prompts";
|
|
9
|
+
import OpenAI from "openai";
|
|
10
|
+
var DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
11
|
+
async function verifyAuth(apiKey, baseURL) {
|
|
12
|
+
const client = new OpenAI({ apiKey, baseURL });
|
|
13
|
+
await client.models.list();
|
|
14
|
+
}
|
|
15
|
+
async function auth() {
|
|
16
|
+
const baseUrl = await input({
|
|
17
|
+
message: "Base URL",
|
|
18
|
+
default: DEFAULT_BASE_URL
|
|
19
|
+
});
|
|
20
|
+
const apiKey = await password({
|
|
21
|
+
message: "API key"
|
|
22
|
+
});
|
|
23
|
+
if (!apiKey) {
|
|
24
|
+
throw new Error("API key is required.");
|
|
25
|
+
}
|
|
26
|
+
process.stderr.write("Verifying...\n");
|
|
27
|
+
await verifyAuth(apiKey, baseUrl);
|
|
28
|
+
const config = { apiKey };
|
|
29
|
+
if (baseUrl !== DEFAULT_BASE_URL) {
|
|
30
|
+
config.baseUrl = baseUrl;
|
|
31
|
+
}
|
|
32
|
+
writeAuthConfig(config);
|
|
33
|
+
process.stderr.write("Auth config saved to ~/.agent-voice/config.json\n");
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
auth
|
|
37
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CHANNELS,
|
|
4
|
+
SAMPLE_RATE
|
|
5
|
+
} from "./chunk-EF2ZPBZD.js";
|
|
6
|
+
|
|
7
|
+
// src/audio.ts
|
|
8
|
+
import { AudioIO, SampleFormat16Bit } from "naudiodon2";
|
|
9
|
+
function createAudioPlayer() {
|
|
10
|
+
const stream = AudioIO({
|
|
11
|
+
outOptions: {
|
|
12
|
+
channelCount: CHANNELS,
|
|
13
|
+
sampleFormat: SampleFormat16Bit,
|
|
14
|
+
sampleRate: SAMPLE_RATE,
|
|
15
|
+
closeOnError: true
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
let closed = false;
|
|
19
|
+
return {
|
|
20
|
+
write(pcm16) {
|
|
21
|
+
return stream.write(pcm16);
|
|
22
|
+
},
|
|
23
|
+
start() {
|
|
24
|
+
stream.start();
|
|
25
|
+
},
|
|
26
|
+
drain() {
|
|
27
|
+
if (closed) return Promise.resolve();
|
|
28
|
+
closed = true;
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
stream.quit(() => resolve());
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
close() {
|
|
34
|
+
if (closed) return;
|
|
35
|
+
closed = true;
|
|
36
|
+
stream.quit();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function createAudioRecorder() {
|
|
41
|
+
const stream = AudioIO({
|
|
42
|
+
inOptions: {
|
|
43
|
+
channelCount: CHANNELS,
|
|
44
|
+
sampleFormat: SampleFormat16Bit,
|
|
45
|
+
sampleRate: SAMPLE_RATE,
|
|
46
|
+
closeOnError: true
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
let stopped = false;
|
|
50
|
+
return {
|
|
51
|
+
onData(cb) {
|
|
52
|
+
stream.on("data", cb);
|
|
53
|
+
},
|
|
54
|
+
start() {
|
|
55
|
+
stream.start();
|
|
56
|
+
},
|
|
57
|
+
stop() {
|
|
58
|
+
if (stopped) return;
|
|
59
|
+
stopped = true;
|
|
60
|
+
stream.quit();
|
|
61
|
+
},
|
|
62
|
+
close() {
|
|
63
|
+
if (stopped) return;
|
|
64
|
+
stopped = true;
|
|
65
|
+
stream.quit();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/realtime.ts
|
|
71
|
+
import { OpenAIRealtimeWS } from "openai/beta/realtime/ws";
|
|
72
|
+
var SYSTEM_INSTRUCTIONS = `
|
|
73
|
+
# Role
|
|
74
|
+
Voice relay between an AI agent and a human.
|
|
75
|
+
|
|
76
|
+
# Instructions
|
|
77
|
+
- When given a text message, read it aloud EXACTLY as written. Do not add, remove, or rephrase anything.
|
|
78
|
+
- After the human responds, acknowledge briefly \u2014 a few words only. Vary your phrasing.
|
|
79
|
+
- NEVER repeat back what the user said verbatim.
|
|
80
|
+
- NEVER ask follow-up questions.
|
|
81
|
+
- Keep every response under one sentence.
|
|
82
|
+
|
|
83
|
+
# Tone
|
|
84
|
+
- Calm, neutral, concise.
|
|
85
|
+
`.trim();
|
|
86
|
+
function createRealtimeSession(options) {
|
|
87
|
+
let rt;
|
|
88
|
+
let responseCount = 0;
|
|
89
|
+
function configureSession() {
|
|
90
|
+
const turnDetection = options.mode === "say" ? null : {
|
|
91
|
+
type: "semantic_vad",
|
|
92
|
+
eagerness: "medium",
|
|
93
|
+
create_response: options.ack,
|
|
94
|
+
interrupt_response: true
|
|
95
|
+
};
|
|
96
|
+
rt.send({
|
|
97
|
+
type: "session.update",
|
|
98
|
+
session: {
|
|
99
|
+
instructions: SYSTEM_INSTRUCTIONS,
|
|
100
|
+
voice: options.voice,
|
|
101
|
+
input_audio_format: "pcm16",
|
|
102
|
+
output_audio_format: "pcm16",
|
|
103
|
+
input_audio_transcription: { model: "gpt-4o-transcribe" },
|
|
104
|
+
turn_detection: turnDetection
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function bindEvents() {
|
|
109
|
+
rt.on("response.audio.delta", (event) => {
|
|
110
|
+
const pcm16 = Buffer.from(event.delta, "base64");
|
|
111
|
+
options.onAudioDelta(pcm16);
|
|
112
|
+
});
|
|
113
|
+
rt.on("conversation.item.input_audio_transcription.completed", (event) => {
|
|
114
|
+
options.onTranscript(event.transcript);
|
|
115
|
+
});
|
|
116
|
+
rt.on("input_audio_buffer.speech_started", () => {
|
|
117
|
+
options.onSpeechStarted();
|
|
118
|
+
});
|
|
119
|
+
rt.on("response.done", () => {
|
|
120
|
+
responseCount++;
|
|
121
|
+
if (responseCount === 1) {
|
|
122
|
+
options.onInitialResponseDone();
|
|
123
|
+
} else if (responseCount === 2) {
|
|
124
|
+
options.onDone();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
rt.on("error", (event) => {
|
|
128
|
+
options.onError(event.error?.message ?? "Unknown realtime error");
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
connect() {
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const client = options.auth ? {
|
|
135
|
+
apiKey: options.auth.apiKey,
|
|
136
|
+
baseURL: options.auth.baseUrl ?? "https://api.openai.com/v1"
|
|
137
|
+
} : void 0;
|
|
138
|
+
rt = new OpenAIRealtimeWS({ model: "gpt-4o-realtime-preview" }, client);
|
|
139
|
+
rt.socket.on("open", () => {
|
|
140
|
+
configureSession();
|
|
141
|
+
bindEvents();
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
rt.socket.on("error", (err) => {
|
|
145
|
+
reject(new Error(`WebSocket connection failed: ${err.message}`));
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
sendMessage(text) {
|
|
150
|
+
rt.send({
|
|
151
|
+
type: "conversation.item.create",
|
|
152
|
+
item: {
|
|
153
|
+
type: "message",
|
|
154
|
+
role: "user",
|
|
155
|
+
content: [
|
|
156
|
+
{
|
|
157
|
+
type: "input_text",
|
|
158
|
+
text: `Read this aloud exactly as written, word for word. Do not add, remove, or change anything:
|
|
159
|
+
|
|
160
|
+
${text}`
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
rt.send({ type: "response.create" });
|
|
166
|
+
},
|
|
167
|
+
sendAudio(pcm16) {
|
|
168
|
+
rt.send({
|
|
169
|
+
type: "input_audio_buffer.append",
|
|
170
|
+
audio: pcm16.toString("base64")
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
close() {
|
|
174
|
+
rt?.close();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export {
|
|
180
|
+
createAudioPlayer,
|
|
181
|
+
createAudioRecorder,
|
|
182
|
+
createRealtimeSession
|
|
183
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/types.ts
|
|
4
|
+
var SAMPLE_RATE = 24e3;
|
|
5
|
+
var CHANNELS = 1;
|
|
6
|
+
var BIT_DEPTH = 16;
|
|
7
|
+
var VOICES = [
|
|
8
|
+
"alloy",
|
|
9
|
+
"ash",
|
|
10
|
+
"ballad",
|
|
11
|
+
"coral",
|
|
12
|
+
"echo",
|
|
13
|
+
"fable",
|
|
14
|
+
"nova",
|
|
15
|
+
"onyx",
|
|
16
|
+
"sage",
|
|
17
|
+
"shimmer",
|
|
18
|
+
"verse"
|
|
19
|
+
];
|
|
20
|
+
var DEFAULT_VOICE = "ash";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
SAMPLE_RATE,
|
|
24
|
+
CHANNELS,
|
|
25
|
+
BIT_DEPTH,
|
|
26
|
+
VOICES,
|
|
27
|
+
DEFAULT_VOICE
|
|
28
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_VOICE
|
|
4
|
+
} from "./chunk-EF2ZPBZD.js";
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
var CONFIG_DIR = join(homedir(), ".agent-voice");
|
|
11
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
12
|
+
function readConfig() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function writeAuthConfig(auth) {
|
|
20
|
+
const config = readConfig();
|
|
21
|
+
config.auth = auth;
|
|
22
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
23
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}
|
|
24
|
+
`, {
|
|
25
|
+
mode: 384
|
|
26
|
+
});
|
|
27
|
+
chmodSync(CONFIG_PATH, 384);
|
|
28
|
+
}
|
|
29
|
+
function resolveAuth() {
|
|
30
|
+
const config = readConfig();
|
|
31
|
+
if (config.auth?.apiKey) {
|
|
32
|
+
return config.auth;
|
|
33
|
+
}
|
|
34
|
+
if (process.env.OPENAI_API_KEY) {
|
|
35
|
+
return { apiKey: process.env.OPENAI_API_KEY };
|
|
36
|
+
}
|
|
37
|
+
throw new Error(
|
|
38
|
+
"No API key found. Run `agent-voice auth` or set OPENAI_API_KEY."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function writeVoiceConfig(voice) {
|
|
42
|
+
const config = readConfig();
|
|
43
|
+
config.voice = voice;
|
|
44
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
45
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}
|
|
46
|
+
`, {
|
|
47
|
+
mode: 384
|
|
48
|
+
});
|
|
49
|
+
chmodSync(CONFIG_PATH, 384);
|
|
50
|
+
}
|
|
51
|
+
function resolveVoice() {
|
|
52
|
+
const config = readConfig();
|
|
53
|
+
return config.voice ?? DEFAULT_VOICE;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
readConfig,
|
|
58
|
+
writeAuthConfig,
|
|
59
|
+
resolveAuth,
|
|
60
|
+
writeVoiceConfig,
|
|
61
|
+
resolveVoice
|
|
62
|
+
};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { closeSync, openSync, writeSync } from "fs";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
var savedStdoutFd = openSync("/dev/fd/1", "w");
|
|
7
|
+
closeSync(1);
|
|
8
|
+
openSync("/dev/null", "w");
|
|
9
|
+
function writeResult(text) {
|
|
10
|
+
writeSync(savedStdoutFd, `${text}
|
|
11
|
+
`);
|
|
12
|
+
closeSync(savedStdoutFd);
|
|
13
|
+
}
|
|
14
|
+
var { ask } = await import("./ask-MG4CURKM.js");
|
|
15
|
+
var { say } = await import("./say-KJWVP5OC.js");
|
|
16
|
+
var { resolveAuth, resolveVoice, writeVoiceConfig } = await import("./config-6FQI6YPB.js");
|
|
17
|
+
var { VOICES } = await import("./types-UKEQRG2U.js");
|
|
18
|
+
async function readStdin() {
|
|
19
|
+
if (process.stdin.isTTY) return "";
|
|
20
|
+
const chunks = [];
|
|
21
|
+
for await (const chunk of process.stdin) {
|
|
22
|
+
chunks.push(chunk);
|
|
23
|
+
}
|
|
24
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
25
|
+
}
|
|
26
|
+
async function getMessage(flag) {
|
|
27
|
+
if (flag) return flag;
|
|
28
|
+
const stdin = await readStdin();
|
|
29
|
+
if (stdin) return stdin;
|
|
30
|
+
throw new Error("No message provided. Use -m or pipe via stdin.");
|
|
31
|
+
}
|
|
32
|
+
var program = new Command().name("agent-voice").description("AI agent voice interaction CLI");
|
|
33
|
+
program.command("auth").description("Configure API key and base URL").action(async () => {
|
|
34
|
+
try {
|
|
35
|
+
const { auth } = await import("./auth-KIIHTB6C.js");
|
|
36
|
+
await auth();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
process.stderr.write(`${err instanceof Error ? err.message : err}
|
|
40
|
+
`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
var defaultVoice = resolveVoice();
|
|
45
|
+
var voicesCmd = program.command("voices").description("List available voices");
|
|
46
|
+
voicesCmd.action(() => {
|
|
47
|
+
for (const v of VOICES) {
|
|
48
|
+
const marker = v === defaultVoice ? " (default)" : "";
|
|
49
|
+
process.stderr.write(`${v}${marker}
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
process.exit(0);
|
|
53
|
+
});
|
|
54
|
+
voicesCmd.command("set <voice>").description("Set the default voice").action((voice) => {
|
|
55
|
+
if (!VOICES.includes(voice)) {
|
|
56
|
+
process.stderr.write(
|
|
57
|
+
`Unknown voice "${voice}". Available: ${VOICES.join(", ")}
|
|
58
|
+
`
|
|
59
|
+
);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
writeVoiceConfig(voice);
|
|
63
|
+
process.stderr.write(`Default voice set to "${voice}".
|
|
64
|
+
`);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
});
|
|
67
|
+
program.command("ask").description("Speak a message and listen for a response").option("-m, --message <text>", "Text message to speak").option("--voice <name>", "OpenAI voice", defaultVoice).option("--timeout <seconds>", "Seconds to wait for user speech", "30").option("--ack", "Speak an acknowledgment after the user responds").action(async (opts) => {
|
|
68
|
+
try {
|
|
69
|
+
const auth = resolveAuth();
|
|
70
|
+
const message = await getMessage(opts.message);
|
|
71
|
+
const transcript = await ask(message, {
|
|
72
|
+
voice: opts.voice,
|
|
73
|
+
timeout: Number.parseInt(opts.timeout, 10),
|
|
74
|
+
ack: opts.ack ?? false,
|
|
75
|
+
auth
|
|
76
|
+
});
|
|
77
|
+
writeResult(transcript);
|
|
78
|
+
process.exit(0);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
process.stderr.write(`${err instanceof Error ? err.message : err}
|
|
81
|
+
`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
program.command("say").description("Speak a message without listening for a response").option("-m, --message <text>", "Text message to speak").option("--voice <name>", "OpenAI voice", defaultVoice).action(async (opts) => {
|
|
86
|
+
try {
|
|
87
|
+
const auth = resolveAuth();
|
|
88
|
+
const message = await getMessage(opts.message);
|
|
89
|
+
await say(message, { voice: opts.voice, auth });
|
|
90
|
+
process.exit(0);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
process.stderr.write(`${err instanceof Error ? err.message : err}
|
|
93
|
+
`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
program.parse();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
readConfig,
|
|
4
|
+
resolveAuth,
|
|
5
|
+
resolveVoice,
|
|
6
|
+
writeAuthConfig,
|
|
7
|
+
writeVoiceConfig
|
|
8
|
+
} from "./chunk-GUTKT66X.js";
|
|
9
|
+
import "./chunk-EF2ZPBZD.js";
|
|
10
|
+
export {
|
|
11
|
+
readConfig,
|
|
12
|
+
resolveAuth,
|
|
13
|
+
resolveVoice,
|
|
14
|
+
writeAuthConfig,
|
|
15
|
+
writeVoiceConfig
|
|
16
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
type AudioPlayer = {
|
|
2
|
+
write(pcm16: Buffer): boolean;
|
|
3
|
+
start(): void;
|
|
4
|
+
drain(): Promise<void>;
|
|
5
|
+
close(): void;
|
|
6
|
+
};
|
|
7
|
+
type AudioRecorder = {
|
|
8
|
+
onData(cb: (pcm16: Buffer) => void): void;
|
|
9
|
+
start(): void;
|
|
10
|
+
stop(): void;
|
|
11
|
+
close(): void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type AuthConfig = {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
};
|
|
18
|
+
declare function resolveAuth(): AuthConfig;
|
|
19
|
+
declare function resolveVoice(): string;
|
|
20
|
+
|
|
21
|
+
type AskOptions = {
|
|
22
|
+
voice?: string;
|
|
23
|
+
timeout?: number;
|
|
24
|
+
ack?: boolean;
|
|
25
|
+
auth?: AuthConfig;
|
|
26
|
+
createPlayer?: () => AudioPlayer;
|
|
27
|
+
createRecorder?: () => AudioRecorder;
|
|
28
|
+
};
|
|
29
|
+
declare function ask(message: string, options?: AskOptions): Promise<string>;
|
|
30
|
+
|
|
31
|
+
type SayOptions = {
|
|
32
|
+
voice?: string;
|
|
33
|
+
auth?: AuthConfig;
|
|
34
|
+
createPlayer?: () => AudioPlayer;
|
|
35
|
+
};
|
|
36
|
+
declare function say(message: string, options?: SayOptions): Promise<void>;
|
|
37
|
+
|
|
38
|
+
declare const VOICES: readonly ["alloy", "ash", "ballad", "coral", "echo", "fable", "nova", "onyx", "sage", "shimmer", "verse"];
|
|
39
|
+
type Voice = (typeof VOICES)[number];
|
|
40
|
+
declare const DEFAULT_VOICE: Voice;
|
|
41
|
+
|
|
42
|
+
export { type AskOptions, type AuthConfig, DEFAULT_VOICE, type SayOptions, VOICES, type Voice, ask, resolveAuth, resolveVoice, say };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// src/audio.ts
|
|
2
|
+
import { AudioIO, SampleFormat16Bit } from "naudiodon2";
|
|
3
|
+
|
|
4
|
+
// src/types.ts
|
|
5
|
+
var SAMPLE_RATE = 24e3;
|
|
6
|
+
var CHANNELS = 1;
|
|
7
|
+
var VOICES = [
|
|
8
|
+
"alloy",
|
|
9
|
+
"ash",
|
|
10
|
+
"ballad",
|
|
11
|
+
"coral",
|
|
12
|
+
"echo",
|
|
13
|
+
"fable",
|
|
14
|
+
"nova",
|
|
15
|
+
"onyx",
|
|
16
|
+
"sage",
|
|
17
|
+
"shimmer",
|
|
18
|
+
"verse"
|
|
19
|
+
];
|
|
20
|
+
var DEFAULT_VOICE = "ash";
|
|
21
|
+
|
|
22
|
+
// src/audio.ts
|
|
23
|
+
function createAudioPlayer() {
|
|
24
|
+
const stream = AudioIO({
|
|
25
|
+
outOptions: {
|
|
26
|
+
channelCount: CHANNELS,
|
|
27
|
+
sampleFormat: SampleFormat16Bit,
|
|
28
|
+
sampleRate: SAMPLE_RATE,
|
|
29
|
+
closeOnError: true
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
let closed = false;
|
|
33
|
+
return {
|
|
34
|
+
write(pcm16) {
|
|
35
|
+
return stream.write(pcm16);
|
|
36
|
+
},
|
|
37
|
+
start() {
|
|
38
|
+
stream.start();
|
|
39
|
+
},
|
|
40
|
+
drain() {
|
|
41
|
+
if (closed) return Promise.resolve();
|
|
42
|
+
closed = true;
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
stream.quit(() => resolve());
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
close() {
|
|
48
|
+
if (closed) return;
|
|
49
|
+
closed = true;
|
|
50
|
+
stream.quit();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function createAudioRecorder() {
|
|
55
|
+
const stream = AudioIO({
|
|
56
|
+
inOptions: {
|
|
57
|
+
channelCount: CHANNELS,
|
|
58
|
+
sampleFormat: SampleFormat16Bit,
|
|
59
|
+
sampleRate: SAMPLE_RATE,
|
|
60
|
+
closeOnError: true
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
let stopped = false;
|
|
64
|
+
return {
|
|
65
|
+
onData(cb) {
|
|
66
|
+
stream.on("data", cb);
|
|
67
|
+
},
|
|
68
|
+
start() {
|
|
69
|
+
stream.start();
|
|
70
|
+
},
|
|
71
|
+
stop() {
|
|
72
|
+
if (stopped) return;
|
|
73
|
+
stopped = true;
|
|
74
|
+
stream.quit();
|
|
75
|
+
},
|
|
76
|
+
close() {
|
|
77
|
+
if (stopped) return;
|
|
78
|
+
stopped = true;
|
|
79
|
+
stream.quit();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/realtime.ts
|
|
85
|
+
import { OpenAIRealtimeWS } from "openai/beta/realtime/ws";
|
|
86
|
+
var SYSTEM_INSTRUCTIONS = `
|
|
87
|
+
# Role
|
|
88
|
+
Voice relay between an AI agent and a human.
|
|
89
|
+
|
|
90
|
+
# Instructions
|
|
91
|
+
- When given a text message, read it aloud EXACTLY as written. Do not add, remove, or rephrase anything.
|
|
92
|
+
- After the human responds, acknowledge briefly \u2014 a few words only. Vary your phrasing.
|
|
93
|
+
- NEVER repeat back what the user said verbatim.
|
|
94
|
+
- NEVER ask follow-up questions.
|
|
95
|
+
- Keep every response under one sentence.
|
|
96
|
+
|
|
97
|
+
# Tone
|
|
98
|
+
- Calm, neutral, concise.
|
|
99
|
+
`.trim();
|
|
100
|
+
function createRealtimeSession(options) {
|
|
101
|
+
let rt;
|
|
102
|
+
let responseCount = 0;
|
|
103
|
+
function configureSession() {
|
|
104
|
+
const turnDetection = options.mode === "say" ? null : {
|
|
105
|
+
type: "semantic_vad",
|
|
106
|
+
eagerness: "medium",
|
|
107
|
+
create_response: options.ack,
|
|
108
|
+
interrupt_response: true
|
|
109
|
+
};
|
|
110
|
+
rt.send({
|
|
111
|
+
type: "session.update",
|
|
112
|
+
session: {
|
|
113
|
+
instructions: SYSTEM_INSTRUCTIONS,
|
|
114
|
+
voice: options.voice,
|
|
115
|
+
input_audio_format: "pcm16",
|
|
116
|
+
output_audio_format: "pcm16",
|
|
117
|
+
input_audio_transcription: { model: "gpt-4o-transcribe" },
|
|
118
|
+
turn_detection: turnDetection
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function bindEvents() {
|
|
123
|
+
rt.on("response.audio.delta", (event) => {
|
|
124
|
+
const pcm16 = Buffer.from(event.delta, "base64");
|
|
125
|
+
options.onAudioDelta(pcm16);
|
|
126
|
+
});
|
|
127
|
+
rt.on("conversation.item.input_audio_transcription.completed", (event) => {
|
|
128
|
+
options.onTranscript(event.transcript);
|
|
129
|
+
});
|
|
130
|
+
rt.on("input_audio_buffer.speech_started", () => {
|
|
131
|
+
options.onSpeechStarted();
|
|
132
|
+
});
|
|
133
|
+
rt.on("response.done", () => {
|
|
134
|
+
responseCount++;
|
|
135
|
+
if (responseCount === 1) {
|
|
136
|
+
options.onInitialResponseDone();
|
|
137
|
+
} else if (responseCount === 2) {
|
|
138
|
+
options.onDone();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
rt.on("error", (event) => {
|
|
142
|
+
options.onError(event.error?.message ?? "Unknown realtime error");
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
connect() {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const client = options.auth ? {
|
|
149
|
+
apiKey: options.auth.apiKey,
|
|
150
|
+
baseURL: options.auth.baseUrl ?? "https://api.openai.com/v1"
|
|
151
|
+
} : void 0;
|
|
152
|
+
rt = new OpenAIRealtimeWS({ model: "gpt-4o-realtime-preview" }, client);
|
|
153
|
+
rt.socket.on("open", () => {
|
|
154
|
+
configureSession();
|
|
155
|
+
bindEvents();
|
|
156
|
+
resolve();
|
|
157
|
+
});
|
|
158
|
+
rt.socket.on("error", (err) => {
|
|
159
|
+
reject(new Error(`WebSocket connection failed: ${err.message}`));
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
sendMessage(text) {
|
|
164
|
+
rt.send({
|
|
165
|
+
type: "conversation.item.create",
|
|
166
|
+
item: {
|
|
167
|
+
type: "message",
|
|
168
|
+
role: "user",
|
|
169
|
+
content: [
|
|
170
|
+
{
|
|
171
|
+
type: "input_text",
|
|
172
|
+
text: `Read this aloud exactly as written, word for word. Do not add, remove, or change anything:
|
|
173
|
+
|
|
174
|
+
${text}`
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
rt.send({ type: "response.create" });
|
|
180
|
+
},
|
|
181
|
+
sendAudio(pcm16) {
|
|
182
|
+
rt.send({
|
|
183
|
+
type: "input_audio_buffer.append",
|
|
184
|
+
audio: pcm16.toString("base64")
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
close() {
|
|
188
|
+
rt?.close();
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/ask.ts
|
|
194
|
+
async function ask(message, options = {}) {
|
|
195
|
+
const {
|
|
196
|
+
voice = DEFAULT_VOICE,
|
|
197
|
+
timeout = 30,
|
|
198
|
+
ack = false,
|
|
199
|
+
auth,
|
|
200
|
+
createPlayer = createAudioPlayer,
|
|
201
|
+
createRecorder = createAudioRecorder
|
|
202
|
+
} = options;
|
|
203
|
+
const player = createPlayer();
|
|
204
|
+
player.start();
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
let recorder = null;
|
|
207
|
+
let transcript = "";
|
|
208
|
+
let timeoutTimer = null;
|
|
209
|
+
let speechDetected = false;
|
|
210
|
+
let cleaned = false;
|
|
211
|
+
let resolved = false;
|
|
212
|
+
async function cleanup() {
|
|
213
|
+
if (cleaned) return;
|
|
214
|
+
cleaned = true;
|
|
215
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
216
|
+
recorder?.stop();
|
|
217
|
+
recorder?.close();
|
|
218
|
+
await player.drain();
|
|
219
|
+
session.close();
|
|
220
|
+
}
|
|
221
|
+
function finish() {
|
|
222
|
+
if (resolved) return;
|
|
223
|
+
resolved = true;
|
|
224
|
+
cleanup().then(() => resolve(transcript));
|
|
225
|
+
}
|
|
226
|
+
const session = createRealtimeSession({
|
|
227
|
+
voice,
|
|
228
|
+
mode: "default",
|
|
229
|
+
ack,
|
|
230
|
+
auth,
|
|
231
|
+
onAudioDelta(pcm16) {
|
|
232
|
+
player.write(pcm16);
|
|
233
|
+
},
|
|
234
|
+
onTranscript(text) {
|
|
235
|
+
transcript = text;
|
|
236
|
+
if (!ack) finish();
|
|
237
|
+
},
|
|
238
|
+
onSpeechStarted() {
|
|
239
|
+
speechDetected = true;
|
|
240
|
+
if (timeoutTimer) {
|
|
241
|
+
clearTimeout(timeoutTimer);
|
|
242
|
+
timeoutTimer = null;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
onInitialResponseDone() {
|
|
246
|
+
setTimeout(() => {
|
|
247
|
+
recorder = createRecorder();
|
|
248
|
+
recorder.onData((pcm16) => {
|
|
249
|
+
session.sendAudio(pcm16);
|
|
250
|
+
});
|
|
251
|
+
recorder.start();
|
|
252
|
+
}, 500);
|
|
253
|
+
timeoutTimer = setTimeout(() => {
|
|
254
|
+
if (!speechDetected) {
|
|
255
|
+
cleanup();
|
|
256
|
+
reject(new Error(`No speech detected within ${timeout}s timeout`));
|
|
257
|
+
}
|
|
258
|
+
}, timeout * 1e3);
|
|
259
|
+
},
|
|
260
|
+
onDone() {
|
|
261
|
+
if (ack) finish();
|
|
262
|
+
},
|
|
263
|
+
async onError(error) {
|
|
264
|
+
await cleanup();
|
|
265
|
+
reject(new Error(error));
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
session.connect().then(() => {
|
|
269
|
+
session.sendMessage(message);
|
|
270
|
+
}, reject);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/config.ts
|
|
275
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
276
|
+
import { homedir } from "os";
|
|
277
|
+
import { join } from "path";
|
|
278
|
+
var CONFIG_DIR = join(homedir(), ".agent-voice");
|
|
279
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
280
|
+
function readConfig() {
|
|
281
|
+
try {
|
|
282
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
283
|
+
} catch {
|
|
284
|
+
return {};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function resolveAuth() {
|
|
288
|
+
const config = readConfig();
|
|
289
|
+
if (config.auth?.apiKey) {
|
|
290
|
+
return config.auth;
|
|
291
|
+
}
|
|
292
|
+
if (process.env.OPENAI_API_KEY) {
|
|
293
|
+
return { apiKey: process.env.OPENAI_API_KEY };
|
|
294
|
+
}
|
|
295
|
+
throw new Error(
|
|
296
|
+
"No API key found. Run `agent-voice auth` or set OPENAI_API_KEY."
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
function resolveVoice() {
|
|
300
|
+
const config = readConfig();
|
|
301
|
+
return config.voice ?? DEFAULT_VOICE;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/say.ts
|
|
305
|
+
async function say(message, options = {}) {
|
|
306
|
+
const {
|
|
307
|
+
voice = DEFAULT_VOICE,
|
|
308
|
+
auth,
|
|
309
|
+
createPlayer = createAudioPlayer
|
|
310
|
+
} = options;
|
|
311
|
+
const player = createPlayer();
|
|
312
|
+
player.start();
|
|
313
|
+
return new Promise((resolve, reject) => {
|
|
314
|
+
let cleaned = false;
|
|
315
|
+
function cleanup() {
|
|
316
|
+
if (cleaned) return;
|
|
317
|
+
cleaned = true;
|
|
318
|
+
session.close();
|
|
319
|
+
}
|
|
320
|
+
const session = createRealtimeSession({
|
|
321
|
+
voice,
|
|
322
|
+
mode: "say",
|
|
323
|
+
ack: false,
|
|
324
|
+
auth,
|
|
325
|
+
onAudioDelta(pcm16) {
|
|
326
|
+
player.write(pcm16);
|
|
327
|
+
},
|
|
328
|
+
onTranscript() {
|
|
329
|
+
},
|
|
330
|
+
onSpeechStarted() {
|
|
331
|
+
},
|
|
332
|
+
async onInitialResponseDone() {
|
|
333
|
+
try {
|
|
334
|
+
await player.drain();
|
|
335
|
+
} catch {
|
|
336
|
+
player.close();
|
|
337
|
+
}
|
|
338
|
+
cleanup();
|
|
339
|
+
resolve();
|
|
340
|
+
},
|
|
341
|
+
onDone() {
|
|
342
|
+
},
|
|
343
|
+
onError(error) {
|
|
344
|
+
player.close();
|
|
345
|
+
cleanup();
|
|
346
|
+
reject(new Error(error));
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
session.connect().then(() => {
|
|
350
|
+
session.sendMessage(message);
|
|
351
|
+
}, reject);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
export {
|
|
355
|
+
DEFAULT_VOICE,
|
|
356
|
+
VOICES,
|
|
357
|
+
ask,
|
|
358
|
+
resolveAuth,
|
|
359
|
+
resolveVoice,
|
|
360
|
+
say
|
|
361
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createAudioPlayer,
|
|
4
|
+
createRealtimeSession
|
|
5
|
+
} from "./chunk-4RFF3WHI.js";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_VOICE
|
|
8
|
+
} from "./chunk-EF2ZPBZD.js";
|
|
9
|
+
|
|
10
|
+
// src/say.ts
|
|
11
|
+
async function say(message, options = {}) {
|
|
12
|
+
const {
|
|
13
|
+
voice = DEFAULT_VOICE,
|
|
14
|
+
auth,
|
|
15
|
+
createPlayer = createAudioPlayer
|
|
16
|
+
} = options;
|
|
17
|
+
const player = createPlayer();
|
|
18
|
+
player.start();
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
let cleaned = false;
|
|
21
|
+
function cleanup() {
|
|
22
|
+
if (cleaned) return;
|
|
23
|
+
cleaned = true;
|
|
24
|
+
session.close();
|
|
25
|
+
}
|
|
26
|
+
const session = createRealtimeSession({
|
|
27
|
+
voice,
|
|
28
|
+
mode: "say",
|
|
29
|
+
ack: false,
|
|
30
|
+
auth,
|
|
31
|
+
onAudioDelta(pcm16) {
|
|
32
|
+
player.write(pcm16);
|
|
33
|
+
},
|
|
34
|
+
onTranscript() {
|
|
35
|
+
},
|
|
36
|
+
onSpeechStarted() {
|
|
37
|
+
},
|
|
38
|
+
async onInitialResponseDone() {
|
|
39
|
+
try {
|
|
40
|
+
await player.drain();
|
|
41
|
+
} catch {
|
|
42
|
+
player.close();
|
|
43
|
+
}
|
|
44
|
+
cleanup();
|
|
45
|
+
resolve();
|
|
46
|
+
},
|
|
47
|
+
onDone() {
|
|
48
|
+
},
|
|
49
|
+
onError(error) {
|
|
50
|
+
player.close();
|
|
51
|
+
cleanup();
|
|
52
|
+
reject(new Error(error));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
session.connect().then(() => {
|
|
56
|
+
session.sendMessage(message);
|
|
57
|
+
}, reject);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
say
|
|
62
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-voice",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for AI agents to interact with humans via voice",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"agent-voice": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@inquirer/prompts": "^8.2.0",
|
|
22
|
+
"commander": "^13.1.0",
|
|
23
|
+
"naudiodon2": "^2.1.0",
|
|
24
|
+
"openai": "^4.96.0",
|
|
25
|
+
"ws": "^8.18.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@biomejs/biome": "^1.9.4",
|
|
29
|
+
"@changesets/cli": "^2.29.8",
|
|
30
|
+
"@types/node": "^22.12.0",
|
|
31
|
+
"@types/ws": "^8.5.14",
|
|
32
|
+
"dotenv-cli": "^11.0.0",
|
|
33
|
+
"lefthook": "^2.1.0",
|
|
34
|
+
"tsup": "^8.3.6",
|
|
35
|
+
"tsx": "^4.19.2",
|
|
36
|
+
"typescript": "^5.7.3",
|
|
37
|
+
"vitest": "^4.0.18"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"dev": "dotenv -e .env.local -- tsx src/cli.ts",
|
|
41
|
+
"agent-voice": "dotenv -e .env.local -- tsx src/cli.ts",
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"check": "biome check --write .",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"test": "dotenv -e .env.local -- vitest run",
|
|
46
|
+
"test:watch": "dotenv -e .env.local -- vitest",
|
|
47
|
+
"release": "pnpm build && changeset publish"
|
|
48
|
+
}
|
|
49
|
+
}
|