@profullstack/qryptchat 0.8.1 → 0.9.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.
@@ -1251,6 +1251,10 @@ Usage:
1251
1251
  qc open the chat client (signs you in first if needed)
1252
1252
  qc fonts install|status|remove our OpenEmoji colour font as the terminal's emoji font
1253
1253
  qc agent join <link> join as an AI agent from an invite (makes its own keys) [--name --username]
1254
+ qc agent call --brain "<cmd>" [--voice orion] [--post] [--no-greet]
1255
+ as an agent, join voice calls in your chats: hear people, ask <cmd> what
1256
+ to say (JSON on stdin; it prints PASS or SAY: text), speak it
1257
+ (needs: npm i -g @livekit/rtc-node)
1254
1258
  qc login [--oob] sign in through your browser; --oob to paste a code (SSH)
1255
1259
  qc logout forget this terminal's session and keys
1256
1260
  qc update [--check] update qc to the latest release (--check only says whether there is one)
@@ -1292,7 +1296,7 @@ function parseArgs2(argv) {
1292
1296
  const [k, v] = a.slice(2).split("=", 2);
1293
1297
  if (v !== undefined)
1294
1298
  args.flags[k] = v;
1295
- else if (["url", "n", "limit", "reply", "emoji", "pronouns", "website", "bio", "name", "username"].includes(k) && argv[i + 1] !== undefined && !argv[i + 1].startsWith("--"))
1299
+ else if (["url", "n", "limit", "reply", "emoji", "pronouns", "website", "bio", "name", "username", "brain", "voice"].includes(k) && argv[i + 1] !== undefined && !argv[i + 1].startsWith("--"))
1296
1300
  args.flags[k] = argv[++i];
1297
1301
  else
1298
1302
  args.flags[k] = true;
@@ -1364,8 +1368,18 @@ async function main2(argv, { version = "0.0.0" } = {}) {
1364
1368
  return runTui(c, { initialChat: rest[0] });
1365
1369
  }
1366
1370
  case "agent": {
1371
+ if (rest[0] === "call") {
1372
+ const { runVoiceAgent } = await import("./chunk-s094t64g.js");
1373
+ return runVoiceAgent(await client(flags), {
1374
+ brain: typeof flags.brain === "string" ? flags.brain : undefined,
1375
+ voice: typeof flags.voice === "string" ? flags.voice : undefined,
1376
+ post: Boolean(flags.post),
1377
+ greet: !flags["no-greet"],
1378
+ hearAgents: Boolean(flags["hear-agents"])
1379
+ });
1380
+ }
1367
1381
  if (rest[0] !== "join" || !rest[1])
1368
- throw new Error("Usage: qc agent join <invite link> [--name NAME] [--username NAME]");
1382
+ throw new Error('Usage: qc agent join <invite link> [--name NAME] [--username NAME] | qc agent call --brain "<cmd>"');
1369
1383
  const { session, conversationId, operator } = await joinAsAgent(rest[1], {
1370
1384
  name: typeof flags.name === "string" ? flags.name : undefined,
1371
1385
  username: typeof flags.username === "string" ? flags.username : undefined
@@ -1623,4 +1637,4 @@ async function voiceCommand(argv, { out, json }) {
1623
1637
  }
1624
1638
  }
1625
1639
 
1626
- export { exitWhenFlushed2, errorLine2, HELP2, parseArgs2, findChat2, main2 };
1640
+ export { CALL_TYPE, parseCall, exitWhenFlushed2, errorLine2, HELP2, parseArgs2, findChat2, main2 };
@@ -0,0 +1,440 @@
1
+ import {
2
+ CALL_TYPE,
3
+ parseCall
4
+ } from "./chunk-e39k2cyf.js";
5
+
6
+ // src/cli/voice-agent.js
7
+ import { spawn } from "node:child_process";
8
+
9
+ // src/lib/chat/voice-recorder.js
10
+ function encodeWav(samples, sampleRate) {
11
+ const buf = new ArrayBuffer(44 + samples.length * 2);
12
+ const v = new DataView(buf);
13
+ const str = (o, t) => [...t].forEach((c, i) => v.setUint8(o + i, c.charCodeAt(0)));
14
+ str(0, "RIFF");
15
+ v.setUint32(4, 36 + samples.length * 2, true);
16
+ str(8, "WAVE");
17
+ str(12, "fmt ");
18
+ v.setUint32(16, 16, true);
19
+ v.setUint16(20, 1, true);
20
+ v.setUint16(22, 1, true);
21
+ v.setUint32(24, sampleRate, true);
22
+ v.setUint32(28, sampleRate * 2, true);
23
+ v.setUint16(32, 2, true);
24
+ v.setUint16(34, 16, true);
25
+ str(36, "data");
26
+ v.setUint32(40, samples.length * 2, true);
27
+ for (let i = 0;i < samples.length; i++) {
28
+ const x = Math.max(-1, Math.min(1, samples[i]));
29
+ v.setInt16(44 + i * 2, x < 0 ? x * 32768 : x * 32767, true);
30
+ }
31
+ return new Uint8Array(buf);
32
+ }
33
+ function createEndpointer({ stepMs = 50, silenceMs = 1100, noSpeechMs = 8000, maxMs = 60000, minLevel = 0.012, maxThreshold = 0.05, speechMs = 150 } = {}) {
34
+ let t = 0;
35
+ let floor = null;
36
+ let loud = 0;
37
+ let quietFor = 0;
38
+ let spoke = false;
39
+ return {
40
+ get spoke() {
41
+ return spoke;
42
+ },
43
+ push(rms) {
44
+ t += stepMs;
45
+ if (t <= 300) {
46
+ floor = floor === null ? rms : Math.min(floor, rms);
47
+ return null;
48
+ }
49
+ const threshold = Math.max(minLevel, Math.min(maxThreshold, (floor ?? 0) * 3));
50
+ if (rms > threshold) {
51
+ loud += 1;
52
+ quietFor = 0;
53
+ if (!spoke && loud * stepMs >= speechMs) {
54
+ spoke = true;
55
+ return "speech-start";
56
+ }
57
+ } else {
58
+ loud = 0;
59
+ quietFor += stepMs;
60
+ if (!spoke)
61
+ floor = floor === null ? rms : floor * 0.95 + rms * 0.05;
62
+ }
63
+ if (spoke && quietFor >= silenceMs)
64
+ return "end";
65
+ if (!spoke && t >= noSpeechMs)
66
+ return "no-speech";
67
+ if (t >= maxMs)
68
+ return "max";
69
+ return null;
70
+ }
71
+ };
72
+ }
73
+
74
+ // src/lib/chat/voice-mode.js
75
+ var DICTATED = "\uD83D\uDDE3️";
76
+ var hostOf = (url) => {
77
+ try {
78
+ return new URL(url).hostname.replace(/^www\./, "");
79
+ } catch {
80
+ return "a link";
81
+ }
82
+ };
83
+ function speakable(text, max = 1200) {
84
+ let s = String(text ?? "");
85
+ if (s.startsWith(DICTATED))
86
+ s = s.slice(DICTATED.length);
87
+ s = s.replace(/```[\s\S]*?(```|$)/g, " (code in the chat) ").replace(/`([^`]+)`/g, "$1").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g, "$1").replace(/https?:\/\/[^\s)>\]]+/g, (u) => `link to ${hostOf(u)}`).replace(/^\s{0,3}#{1,6}\s+/gm, "").replace(/^\s*(?:[-*+]|\d+[.)])\s+/gm, "").replace(/^\s*>\s?/gm, "").replace(/^\s*\|?(?:\s*:?-{3,}:?\s*\|)+\s*$/gm, "").replace(/\|/g, ", ").replace(/(\*\*|__|~~)(.+?)\1/g, "$2").replace(/(^|\W)[*_](\S[^*_]*?)[*_](?=\W|$)/g, "$1$2").replace(/\s*\n+\s*/g, ". ").replace(/([.!?:;,])(?:\s*\.)+/g, "$1").replace(/\s{2,}/g, " ").trim();
88
+ if (s.length <= max)
89
+ return s;
90
+ const cut = s.slice(0, max);
91
+ const end = Math.max(cut.lastIndexOf(". "), cut.lastIndexOf("! "), cut.lastIndexOf("? "));
92
+ return `${end > max / 3 ? cut.slice(0, end + 1) : cut.replace(/\s+\S*$/, "…")} The rest is in the chat.`;
93
+ }
94
+
95
+ // src/cli/voice-agent.js
96
+ var HKDF = 1;
97
+ var e2eeOptions = (key) => ({
98
+ keyProviderOptions: {
99
+ sharedKey: new Uint8Array(key),
100
+ ratchetSalt: new TextEncoder().encode("LKFrameEncryptionKey"),
101
+ ratchetWindowSize: 16,
102
+ failureTolerance: -1,
103
+ keyRingSize: 16,
104
+ keyDerivationFunction: HKDF
105
+ }
106
+ });
107
+ var JOIN_RECENT_MS = 30 * 60 * 1000;
108
+ var ALONE_LEAVE_MS = 30 * 1000;
109
+ var MAX_CALL_MS = 3 * 60 * 60 * 1000;
110
+ var RATE = 16000;
111
+ var SPEAK_RATE = 24000;
112
+ var SPOKEN_PREFIX = "\uD83D\uDD0A";
113
+ var userIdOf = (identity) => String(identity || "").split(":").pop();
114
+ function splitSentences(text, max = 300) {
115
+ const out = [];
116
+ let cur = "";
117
+ for (const s of String(text).match(/[^.!?]+[.!?]+["')\]]*\s*|[^.!?]+$/g) || []) {
118
+ if (cur && (cur + s).length > max) {
119
+ out.push(cur.trim());
120
+ cur = "";
121
+ }
122
+ cur += s;
123
+ }
124
+ if (cur.trim())
125
+ out.push(cur.trim());
126
+ return out;
127
+ }
128
+ function parseBrain(out) {
129
+ const s = String(out || "");
130
+ const i = s.search(/^\s*SAY:/m);
131
+ return i < 0 ? "" : s.slice(i).replace(/^\s*SAY:\s*/, "").trim();
132
+ }
133
+ function wavSamples(bytes) {
134
+ const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
135
+ const v = new DataView(b.buffer, b.byteOffset, b.byteLength);
136
+ let o = 12;
137
+ while (o + 8 <= b.length) {
138
+ const id = String.fromCharCode(b[o], b[o + 1], b[o + 2], b[o + 3]);
139
+ let size = v.getUint32(o + 4, true);
140
+ if (id === "data") {
141
+ if (!size || size === 4294967295 || o + 8 + size > b.length)
142
+ size = b.length - o - 8;
143
+ const n = Math.floor(size / 2);
144
+ const out = new Int16Array(n);
145
+ for (let i = 0;i < n; i++)
146
+ out[i] = v.getInt16(o + 8 + i * 2, true);
147
+ return out;
148
+ }
149
+ o += 8 + size + size % 2;
150
+ }
151
+ return new Int16Array(0);
152
+ }
153
+ var rmsOf = (int16) => {
154
+ let s = 0;
155
+ for (let i = 0;i < int16.length; i++)
156
+ s += (int16[i] / 32768) ** 2;
157
+ return Math.sqrt(s / Math.max(1, int16.length));
158
+ };
159
+ function askBrain(cmd, input, { timeoutMs = 90000, env = process.env } = {}) {
160
+ return new Promise((resolve) => {
161
+ const child = spawn(cmd, { shell: true, env, stdio: ["pipe", "pipe", "pipe"] });
162
+ let out = "";
163
+ let err = "";
164
+ const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
165
+ child.stdout.on("data", (d) => out += d);
166
+ child.stderr.on("data", (d) => err += d);
167
+ child.on("error", (e) => {
168
+ clearTimeout(timer);
169
+ resolve({ say: "", error: e.message });
170
+ });
171
+ child.on("close", (code) => {
172
+ clearTimeout(timer);
173
+ resolve({ say: parseBrain(out), error: code ? `brain exited ${code}: ${err.trim().slice(-200)}` : "" });
174
+ });
175
+ child.stdin.end(JSON.stringify(input));
176
+ });
177
+ }
178
+
179
+ class CallSession {
180
+ constructor({ client, rtc, conversation, call, brain, voice, post, log, isHuman }) {
181
+ Object.assign(this, { client, rtc, conversation, call, brain, voice, post, log, isHuman });
182
+ this.transcript = [];
183
+ this.queue = Promise.resolve();
184
+ this.speakToken = 0;
185
+ this.speaking = false;
186
+ this.closed = false;
187
+ this.started = Date.now();
188
+ this.lastHuman = Date.now();
189
+ }
190
+ get id() {
191
+ return this.conversation.id;
192
+ }
193
+ async join({ greet = true } = {}) {
194
+ const { Room, RoomEvent, AudioSource, LocalAudioTrack, TrackPublishOptions, TrackSource, TrackKind } = this.rtc;
195
+ const { token, url } = await this.client.request("/api/calls/token", { method: "POST", body: { conversationId: this.id } });
196
+ const room = new Room;
197
+ this.room = room;
198
+ room.on(RoomEvent.TrackSubscribed, (track, _pub, participant) => {
199
+ if (track.kind === TrackKind.KIND_AUDIO && this.isHuman(userIdOf(participant.identity)))
200
+ this.listen(track, participant);
201
+ });
202
+ room.on(RoomEvent.Disconnected, () => this.close("disconnected"));
203
+ await room.connect(url, token, { autoSubscribe: true, e2ee: e2eeOptions(this.call.key) });
204
+ this.source = new AudioSource(SPEAK_RATE, 1);
205
+ const track = LocalAudioTrack.createAudioTrack(`${this.client.me?.username || "agent"}-voice`, this.source);
206
+ const opts = new TrackPublishOptions;
207
+ opts.source = TrackSource.SOURCE_MICROPHONE;
208
+ await room.localParticipant.publishTrack(track, opts);
209
+ this.log(`joined the call in ${this.conversation.title}`);
210
+ this.watchdog = setInterval(() => this.checkAlone(), 5000);
211
+ if (greet)
212
+ this.say(`${this.client.me?.display_name || this.client.me?.username || "Agent"} here.`);
213
+ }
214
+ humansPresent() {
215
+ return [...this.room?.remoteParticipants?.values() ?? []].some((p) => this.isHuman(userIdOf(p.identity)));
216
+ }
217
+ checkAlone() {
218
+ if (this.humansPresent())
219
+ this.lastHuman = Date.now();
220
+ if (Date.now() - this.lastHuman > ALONE_LEAVE_MS)
221
+ return this.close("everyone left");
222
+ if (Date.now() - this.started > MAX_CALL_MS)
223
+ return this.close("call too long");
224
+ }
225
+ async listen(track, participant) {
226
+ const { AudioStream } = this.rtc;
227
+ const name = participant.name || "someone";
228
+ const speakerId = userIdOf(participant.identity);
229
+ const stream = new AudioStream(track, { sampleRate: RATE, numChannels: 1 });
230
+ let ep = createEndpointer({ stepMs: 10, silenceMs: 900, noSpeechMs: Infinity, maxMs: 30000 });
231
+ let frames = [];
232
+ try {
233
+ for await (const frame of stream) {
234
+ if (this.closed)
235
+ break;
236
+ const pcm = Int16Array.from(frame.data);
237
+ frames.push(pcm);
238
+ const ev = ep.push(rmsOf(pcm));
239
+ if (!ep.spoke && frames.length > 50)
240
+ frames = frames.slice(-50);
241
+ if (ev === "speech-start")
242
+ this.bargeIn();
243
+ if (ev === "end" || ev === "max") {
244
+ const all = new Float32Array(frames.reduce((n, f) => n + f.length, 0));
245
+ let o = 0;
246
+ for (const f of frames)
247
+ for (let i = 0;i < f.length; i++)
248
+ all[o++] = f[i] / 32768;
249
+ frames = [];
250
+ ep = createEndpointer({ stepMs: 10, silenceMs: 900, noSpeechMs: Infinity, maxMs: 30000 });
251
+ this.enqueue(() => this.heard(all, name, speakerId));
252
+ }
253
+ }
254
+ } catch (e) {
255
+ if (!this.closed)
256
+ this.log(`listening to ${name} stopped: ${e.message}`);
257
+ }
258
+ }
259
+ enqueue(job) {
260
+ this.queue = this.queue.then(job).catch((e) => this.log(`error: ${e.message}`));
261
+ return this.queue;
262
+ }
263
+ async heard(samples, name, speakerId) {
264
+ const wav = encodeWav(samples, RATE);
265
+ const res = await this.client.fetch(`${this.client.base}/api/voice/transcribe`, {
266
+ method: "POST",
267
+ headers: { Authorization: `Bearer ${await this.client.token()}`, "Content-Type": "audio/wav", "X-Voice-Lang": "en", "X-Voice-Hint": encodeURIComponent(this.hints().join(",")) },
268
+ body: wav
269
+ });
270
+ const data = await res.json().catch(() => ({}));
271
+ if (!res.ok)
272
+ throw new Error(`transcribe ${res.status}: ${data.error || ""}`);
273
+ const text = String(data.text || "").trim();
274
+ if (!text)
275
+ return;
276
+ this.log(`${name}: ${text}`);
277
+ this.transcript.push({ who: name, text });
278
+ const { say, error } = await askBrain(this.brain, {
279
+ agent: this.client.me?.username,
280
+ conversationId: this.id,
281
+ title: this.conversation.title,
282
+ speaker: name,
283
+ speakerId,
284
+ text,
285
+ at: new Date().toISOString(),
286
+ transcript: this.transcript.slice(-30)
287
+ });
288
+ if (error)
289
+ this.log(error);
290
+ if (!say)
291
+ return;
292
+ this.transcript.push({ who: `${this.client.me?.username} (you)`, text: say });
293
+ if (this.post)
294
+ this.client.send(this.id, `${SPOKEN_PREFIX} ${say}`).catch((e) => this.log(`post failed: ${e.message}`));
295
+ await this.say(say);
296
+ }
297
+ hints() {
298
+ return [...new Set((this.conversation.participants || []).flatMap((p) => [p.user?.username, p.user?.display_name]).filter(Boolean))];
299
+ }
300
+ async say(text) {
301
+ const clean = speakable(text, 1500);
302
+ if (!clean || !this.source)
303
+ return;
304
+ const token = ++this.speakToken;
305
+ const sentences = splitSentences(clean, 120);
306
+ let next = this.tts(sentences[0]);
307
+ this.speaking = true;
308
+ try {
309
+ for (let i = 0;i < sentences.length; i++) {
310
+ const pcm = await next;
311
+ if (i + 1 < sentences.length)
312
+ next = this.tts(sentences[i + 1]);
313
+ if (token !== this.speakToken || this.closed)
314
+ return;
315
+ const { AudioFrame } = this.rtc;
316
+ const step = SPEAK_RATE / 100;
317
+ for (let o = 0;o + step <= pcm.length; o += step) {
318
+ if (token !== this.speakToken || this.closed)
319
+ return;
320
+ await this.source.captureFrame(new AudioFrame(pcm.slice(o, o + step), SPEAK_RATE, 1, step));
321
+ }
322
+ }
323
+ await this.source.waitForPlayout?.();
324
+ } finally {
325
+ if (token === this.speakToken)
326
+ this.speaking = false;
327
+ }
328
+ }
329
+ async tts(sentence) {
330
+ const res = await this.client.fetch(`${this.client.base}/api/voice/speak`, {
331
+ method: "POST",
332
+ headers: { Authorization: `Bearer ${await this.client.token()}`, "Content-Type": "application/json" },
333
+ body: JSON.stringify({ text: sentence, voice: this.voice })
334
+ });
335
+ if (!res.ok)
336
+ throw new Error(`speak ${res.status}: ${(await res.json().catch(() => ({}))).error || ""}`);
337
+ return wavSamples(new Uint8Array(await res.arrayBuffer()));
338
+ }
339
+ bargeIn() {
340
+ if (!this.speaking)
341
+ return;
342
+ this.speakToken++;
343
+ this.speaking = false;
344
+ this.source?.clearQueue();
345
+ }
346
+ async close(why) {
347
+ if (this.closed)
348
+ return;
349
+ this.closed = true;
350
+ clearInterval(this.watchdog);
351
+ this.speakToken++;
352
+ this.log(`left the call in ${this.conversation.title} (${why})`);
353
+ await this.room?.disconnect().catch(() => {});
354
+ this.onClose?.();
355
+ }
356
+ }
357
+ async function loadRtc() {
358
+ try {
359
+ const name = "@livekit/rtc-node";
360
+ return await import(name);
361
+ } catch {
362
+ throw new Error("qc agent call needs @livekit/rtc-node: npm i -g @livekit/rtc-node (or install it next to qc).");
363
+ }
364
+ }
365
+ async function latestCall(client, conversationId) {
366
+ const { messages = [] } = await client.request("/api/messages/load", { method: "POST", body: { conversationId, limit: 20 } });
367
+ for (const m of [...messages].reverse()) {
368
+ if (m.message_type !== CALL_TYPE || !m.encrypted_content)
369
+ continue;
370
+ const call = parseCall(await client.ring.decrypt(m.encrypted_content));
371
+ if (call)
372
+ return { ...call, messageId: m.id, at: call.at || m.created_at, senderId: m.sender_id };
373
+ }
374
+ return null;
375
+ }
376
+ async function runVoiceAgent(client, { brain, voice = "orion", post = false, greet = true, hearAgents = false, log = (s) => process.stderr.write(`qc agent call: ${s}
377
+ `), rtc, signal } = {}) {
378
+ if (!brain)
379
+ throw new Error('Usage: qc agent call --brain "<command>" [--voice orion] [--post] [--no-greet]');
380
+ rtc ??= await loadRtc();
381
+ const sessions = new Map;
382
+ let chats = new Map;
383
+ const loadChats = async () => {
384
+ chats = new Map((await client.conversations()).map((c) => [c.id, c]));
385
+ return chats;
386
+ };
387
+ const agentIds = () => new Set([...chats.values()].flatMap((c) => (c.participants || []).filter((p) => p.user?.account_type === "agent").map((p) => p.user_id ?? p.user?.id)));
388
+ const isHuman = (userId) => userId !== client.me?.id && (hearAgents || !agentIds().has(userId));
389
+ const join = async (conversation, call) => {
390
+ if (sessions.has(conversation.id) || Date.now() - Date.parse(call.at) > MAX_CALL_MS)
391
+ return;
392
+ const session = new CallSession({ client, rtc, conversation, call, brain, voice, post, log, isHuman });
393
+ sessions.set(conversation.id, session);
394
+ session.onClose = () => sessions.delete(conversation.id);
395
+ try {
396
+ await session.join({ greet });
397
+ } catch (e) {
398
+ log(`could not join the call in ${conversation.title}: ${e.message}`);
399
+ await session.close("join failed");
400
+ }
401
+ };
402
+ await loadChats();
403
+ for (const c of chats.values()) {
404
+ const call = await latestCall(client, c.id).catch(() => null);
405
+ if (call && Date.now() - Date.parse(call.at) < JOIN_RECENT_MS)
406
+ await join(c, call);
407
+ }
408
+ log(`listening for calls in ${chats.size} chat(s)`);
409
+ await client.events(async ({ type, data }) => {
410
+ const m = data?.message;
411
+ if (type !== "NEW_MESSAGE" || !m?.conversation_id)
412
+ return;
413
+ if (!chats.has(m.conversation_id))
414
+ await loadChats();
415
+ const conversation = chats.get(m.conversation_id);
416
+ if (!conversation)
417
+ return;
418
+ if (m.message_type === CALL_TYPE) {
419
+ const call = await latestCall(client, conversation.id);
420
+ if (call && call.messageId === m.id) {
421
+ await sessions.get(conversation.id)?.close("a new call started");
422
+ await join(conversation, call);
423
+ }
424
+ return;
425
+ }
426
+ const session = sessions.get(conversation.id);
427
+ if (session && m.sender_id === client.me?.id && (m.message_type ?? "text") === "text") {
428
+ const { messages = [] } = await client.request("/api/messages/load", { method: "POST", body: { conversationId: conversation.id, limit: 5 } });
429
+ const raw = messages.find((x) => x.id === m.id);
430
+ const text = raw?.encrypted_content ? await client.ring.decrypt(raw.encrypted_content) : "";
431
+ if (text && !text.startsWith(SPOKEN_PREFIX) && !text.startsWith("\uD83D\uDDE3️"))
432
+ session.enqueue(() => session.say(text));
433
+ }
434
+ }, { signal, onStatus: (s) => log(`events ${s}`) });
435
+ for (const s of sessions.values())
436
+ await s.close("stopped");
437
+ }
438
+ export {
439
+ runVoiceAgent
440
+ };
package/dist/commands.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  parseArgs2,
6
6
  findChat2,
7
7
  main2
8
- } from "./chunk-wtbsdxe7.js";
8
+ } from "./chunk-e39k2cyf.js";
9
9
  import"./chunk-8gpysrd6.js";
10
10
  export {
11
11
  HELP2 as HELP,
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import"./chunk-8gpysrd6.js";
2
2
  import {
3
3
  findChat2
4
- } from "./chunk-wtbsdxe7.js";
4
+ } from "./chunk-e39k2cyf.js";
5
5
 
6
6
  // src/cli/mcp.js
7
7
  import { createInterface } from "node:readline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/qryptchat",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "qc: qrypt.chat in your terminal. A full-screen end-to-end encrypted chat client (ML-KEM-1024), plus a scriptable CLI and an MCP server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,5 +38,13 @@
38
38
  "directory": "packages/qryptchat"
39
39
  },
40
40
  "license": "MIT",
41
- "author": "Profullstack, Inc."
41
+ "author": "Profullstack, Inc.",
42
+ "peerDependencies": {
43
+ "@livekit/rtc-node": ">=1.2.1"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "@livekit/rtc-node": {
47
+ "optional": true
48
+ }
49
+ }
42
50
  }