@lawaty/lacode 1.1.0 → 1.2.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/tui.js ADDED
@@ -0,0 +1,1282 @@
1
+ // src/voice/recorder.ts
2
+ import { spawn } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ function createRecorder(deps) {
8
+ const spawnImpl = deps?.spawnImpl ?? spawn;
9
+ const tmpdirVal = deps?.tmpdir ?? tmpdir();
10
+ const maxSeconds = deps?.maxSeconds ?? 120;
11
+ const exists = deps?.exists ?? ((p) => {
12
+ try {
13
+ return existsSync(p);
14
+ } catch {
15
+ return false;
16
+ }
17
+ });
18
+ const readFile = deps?.readFile ?? ((p) => readFileSync(p));
19
+ const unlink = deps?.unlink ?? ((p) => {
20
+ unlinkSync(p);
21
+ });
22
+ const now = deps?.now ?? Date.now;
23
+ let chosenBin = deps?.bin;
24
+ let active = false;
25
+ let childHandle;
26
+ let currentPath;
27
+ let exited = false;
28
+ let resolveWait = null;
29
+ const recorder2 = {
30
+ get active() {
31
+ return active;
32
+ },
33
+ get path() {
34
+ return currentPath;
35
+ },
36
+ start() {
37
+ if (active) return;
38
+ if (chosenBin === void 0) {
39
+ if (exists("/usr/bin/arecord") || exists("/usr/local/bin/arecord") || exists("arecord")) {
40
+ chosenBin = "arecord";
41
+ } else {
42
+ chosenBin = "ffmpeg";
43
+ }
44
+ }
45
+ const bin = chosenBin;
46
+ currentPath = join(tmpdirVal, `lacode-voice-${process.pid}-${now()}-${randomUUID()}.wav`);
47
+ const path = currentPath;
48
+ const args = bin === "arecord" ? ["-q", "-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "wav", "-d", String(maxSeconds), path] : ["-hide_banner", "-loglevel", "error", "-f", "pulse", "-i", "default", "-ar", "16000", "-ac", "1", "-t", String(maxSeconds), "-y", path];
49
+ exited = false;
50
+ let child;
51
+ try {
52
+ child = spawnImpl(bin, args, { stdio: "ignore" });
53
+ } catch {
54
+ active = false;
55
+ currentPath = void 0;
56
+ return;
57
+ }
58
+ childHandle = child;
59
+ active = true;
60
+ const onClose = () => {
61
+ exited = true;
62
+ if (resolveWait) {
63
+ const r = resolveWait;
64
+ resolveWait = null;
65
+ r();
66
+ }
67
+ };
68
+ try {
69
+ child.on("close", onClose);
70
+ child.on("exit", onClose);
71
+ child.on("error", (err) => {
72
+ exited = true;
73
+ if (resolveWait) {
74
+ const r = resolveWait;
75
+ resolveWait = null;
76
+ r();
77
+ }
78
+ active = false;
79
+ });
80
+ } catch {
81
+ }
82
+ },
83
+ async stop() {
84
+ if (!active || !childHandle) return void 0;
85
+ const child = childHandle;
86
+ const filePath = currentPath;
87
+ if (!exited) {
88
+ await new Promise((resolve) => {
89
+ let done = false;
90
+ let timer;
91
+ const finish = () => {
92
+ if (done) return;
93
+ done = true;
94
+ if (timer) clearTimeout(timer);
95
+ if (resolveWait === resolve) resolveWait = null;
96
+ resolve();
97
+ };
98
+ resolveWait = resolve;
99
+ timer = setTimeout(() => {
100
+ try {
101
+ child.kill("SIGKILL");
102
+ } catch {
103
+ }
104
+ finish();
105
+ }, 3e3);
106
+ try {
107
+ child.kill("SIGINT");
108
+ } catch {
109
+ finish();
110
+ }
111
+ if (exited) finish();
112
+ });
113
+ }
114
+ let result;
115
+ try {
116
+ if (filePath) {
117
+ const data = readFile(filePath);
118
+ if (data && data.length > 0) result = data;
119
+ }
120
+ } catch {
121
+ result = void 0;
122
+ } finally {
123
+ if (filePath) {
124
+ try {
125
+ unlink(filePath);
126
+ } catch {
127
+ }
128
+ }
129
+ }
130
+ childHandle = void 0;
131
+ currentPath = void 0;
132
+ active = false;
133
+ return result;
134
+ },
135
+ abort() {
136
+ const child = childHandle;
137
+ const filePath = currentPath;
138
+ if (child) {
139
+ try {
140
+ child.kill("SIGKILL");
141
+ } catch {
142
+ }
143
+ }
144
+ if (filePath) {
145
+ try {
146
+ unlink(filePath);
147
+ } catch {
148
+ }
149
+ }
150
+ childHandle = void 0;
151
+ currentPath = void 0;
152
+ active = false;
153
+ exited = true;
154
+ if (resolveWait) {
155
+ const r = resolveWait;
156
+ resolveWait = null;
157
+ r();
158
+ }
159
+ }
160
+ };
161
+ return recorder2;
162
+ }
163
+
164
+ // src/voice/groq.ts
165
+ import { readFileSync as readFileSync2 } from "node:fs";
166
+ import { homedir } from "node:os";
167
+ import { join as join2 } from "node:path";
168
+ var DEFAULT_READ_FILE = (p) => {
169
+ try {
170
+ return readFileSync2(p, "utf8");
171
+ } catch {
172
+ return null;
173
+ }
174
+ };
175
+ function resolveGroqKey(deps) {
176
+ const env = deps?.env ?? process.env;
177
+ const home = deps?.home ?? homedir();
178
+ const readFile = deps?.readFile ?? DEFAULT_READ_FILE;
179
+ const safeRead = (p) => {
180
+ try {
181
+ return readFile(p);
182
+ } catch {
183
+ return null;
184
+ }
185
+ };
186
+ const envVal = env?.GROQ_KEY;
187
+ if (typeof envVal === "string") {
188
+ const trimmed = envVal.trim();
189
+ if (trimmed !== "") return { key: trimmed, source: "env" };
190
+ }
191
+ const filePath = join2(home, ".config", "lacode", "voice.json");
192
+ const fileContent = safeRead(filePath);
193
+ if (typeof fileContent === "string") {
194
+ let parsed;
195
+ try {
196
+ parsed = JSON.parse(fileContent);
197
+ } catch {
198
+ parsed = void 0;
199
+ }
200
+ if (parsed && typeof parsed === "object") {
201
+ const obj = parsed;
202
+ const keys = ["groqKey", "GROQ_KEY", "apiKey", "groq_key"];
203
+ for (const k of keys) {
204
+ const v = obj[k];
205
+ if (typeof v === "string") {
206
+ const t = v.trim();
207
+ if (t !== "") return { key: t, source: "file" };
208
+ }
209
+ }
210
+ }
211
+ }
212
+ const bashFiles = [join2(home, ".bashrc"), join2(home, ".bash_profile"), join2(home, ".profile")];
213
+ const skipPatterns = ["$(", "`", "$GROQ", "$HOME"];
214
+ const regex = /^\s*(?:export\s+)?GROQ_KEY\s*=\s*(.+?)\s*$/;
215
+ for (const f of bashFiles) {
216
+ const content = safeRead(f);
217
+ if (typeof content !== "string") continue;
218
+ const lines = content.split(/\r?\n/);
219
+ for (const line of lines) {
220
+ const m = regex.exec(line);
221
+ if (!m) continue;
222
+ let value = m[1];
223
+ const isSingle = value.startsWith("'") && value.endsWith("'") && value.length >= 2;
224
+ const isDouble = value.startsWith('"') && value.endsWith('"') && value.length >= 2;
225
+ if (isSingle || isDouble) {
226
+ value = value.slice(1, -1);
227
+ } else {
228
+ value = value.replace(/\s+#.*$/, "");
229
+ value = value.trim();
230
+ }
231
+ let skip = false;
232
+ for (const p of skipPatterns) {
233
+ if (value.includes(p)) {
234
+ skip = true;
235
+ break;
236
+ }
237
+ }
238
+ if (skip) continue;
239
+ if (value === "") continue;
240
+ return { key: value, source: "bashrc" };
241
+ }
242
+ }
243
+ return void 0;
244
+ }
245
+ async function transcribe(input) {
246
+ if (!input.audio || input.audio.length === 0) {
247
+ throw new Error("no audio captured");
248
+ }
249
+ const filename = input.filename ?? "audio.wav";
250
+ const model = input.model ?? "whisper-large-v3-turbo";
251
+ const language = input.language ?? "en";
252
+ const baseUrl = input.baseUrl ?? "https://api.groq.com/openai/v1";
253
+ const timeoutMs = input.timeoutMs ?? 3e4;
254
+ const fetchImpl = input.fetchImpl ?? fetch;
255
+ const form = new FormData();
256
+ const blob = new Blob([input.audio], { type: "audio/wav" });
257
+ form.append("file", blob, filename);
258
+ form.append("model", model);
259
+ form.append("language", language);
260
+ form.append("temperature", "0");
261
+ form.append("response_format", "json");
262
+ if (input.prompt) form.append("prompt", input.prompt);
263
+ const url = `${baseUrl}/audio/transcriptions`;
264
+ const res = await fetchImpl(url, {
265
+ method: "POST",
266
+ headers: { Authorization: `Bearer ${input.key}` },
267
+ body: form,
268
+ signal: AbortSignal.timeout(timeoutMs)
269
+ });
270
+ if (!res.ok) {
271
+ try {
272
+ await res.text();
273
+ } catch {
274
+ }
275
+ throw new Error(`groq transcription failed (HTTP ${res.status})`);
276
+ }
277
+ let json;
278
+ try {
279
+ json = await res.json();
280
+ } catch {
281
+ return "";
282
+ }
283
+ const text = json?.text;
284
+ if (typeof text !== "string" || text.trim() === "") return "";
285
+ return text.trim();
286
+ }
287
+
288
+ // src/voice/parse.ts
289
+ var NUMBER_WORDS = {
290
+ zero: 0,
291
+ one: 1,
292
+ two: 2,
293
+ three: 3,
294
+ four: 4,
295
+ five: 5,
296
+ six: 6,
297
+ seven: 7,
298
+ eight: 8,
299
+ nine: 9,
300
+ ten: 10,
301
+ first: 1,
302
+ second: 2,
303
+ third: 3,
304
+ fourth: 4,
305
+ fifth: 5,
306
+ sixth: 6,
307
+ seventh: 7,
308
+ eighth: 8,
309
+ ninth: 9,
310
+ tenth: 10
311
+ };
312
+ var STOPWORDS = /* @__PURE__ */ new Set(["the", "a", "an", "of", "to", "for", "and", "or", "option", "choice"]);
313
+ function normalise(utterance) {
314
+ let s = utterance.toLowerCase();
315
+ s = s.replace(/&/g, " and ").replace(/\//g, " and ");
316
+ s = s.replace(/^[^\w\s]+|[^\w\s]+$/g, "");
317
+ s = s.replace(/\s+/g, " ").trim();
318
+ return s;
319
+ }
320
+ function stripPunct(w) {
321
+ return w.replace(/^[^\w']+|[^\w']+$/g, "");
322
+ }
323
+ function extractNumericTokens(utterance) {
324
+ const norm = normalise(utterance);
325
+ const words = norm.split(/[\s,]+/);
326
+ const tokens = [];
327
+ for (const raw of words) {
328
+ const w = stripPunct(raw);
329
+ if (w === "") continue;
330
+ if (/^\d+$/.test(w)) tokens.push(w);
331
+ else if (Object.prototype.hasOwnProperty.call(NUMBER_WORDS, w)) tokens.push(w);
332
+ else if (w === "last") tokens.push(w);
333
+ }
334
+ return tokens;
335
+ }
336
+ function matchLabel(norm, question) {
337
+ if (norm === "") return null;
338
+ const opts = question.options;
339
+ const lower = (s) => s.toLowerCase().trim();
340
+ for (let i = 0; i < opts.length; i++) {
341
+ if (lower(opts[i].label) === norm) return i;
342
+ }
343
+ for (let i = 0; i < opts.length; i++) {
344
+ const lab = lower(opts[i].label);
345
+ if (lab.length === 0) continue;
346
+ if (norm.startsWith(lab) || lab.startsWith(norm)) return i;
347
+ }
348
+ const utteranceWords = new Set(
349
+ norm.split(/[\s,]+/).map((w) => stripPunct(w)).filter((w) => w.length > 0)
350
+ );
351
+ let best = -1;
352
+ let bestCount = 0;
353
+ for (let i = 0; i < opts.length; i++) {
354
+ const sig = lower(opts[i].label).split(/[\s\W]+/).filter((w) => w.length > 2 && !STOPWORDS.has(w));
355
+ const unique = [...new Set(sig)];
356
+ if (unique.length === 0) continue;
357
+ if (unique.every((w) => utteranceWords.has(w))) {
358
+ if (unique.length > bestCount) {
359
+ bestCount = unique.length;
360
+ best = i;
361
+ }
362
+ }
363
+ }
364
+ return best === -1 ? null : best;
365
+ }
366
+ function parseSelection(utterance, question) {
367
+ const norm = normalise(utterance);
368
+ const options = question.options;
369
+ const n = options.length;
370
+ const words = norm.split(/[\s,]+/);
371
+ let numericFound = false;
372
+ const rawIndices = [];
373
+ for (const raw of words) {
374
+ const w = stripPunct(raw);
375
+ if (w === "") continue;
376
+ if (/^\d+$/.test(w)) {
377
+ numericFound = true;
378
+ rawIndices.push(parseInt(w, 10) - 1);
379
+ } else if (Object.prototype.hasOwnProperty.call(NUMBER_WORDS, w)) {
380
+ numericFound = true;
381
+ rawIndices.push(NUMBER_WORDS[w] - 1);
382
+ } else if (w === "last") {
383
+ numericFound = true;
384
+ rawIndices.push(n - 1);
385
+ }
386
+ }
387
+ let matched = false;
388
+ const indices = [];
389
+ const seen = /* @__PURE__ */ new Set();
390
+ if (numericFound) {
391
+ matched = true;
392
+ for (const idx of rawIndices) {
393
+ if (idx >= 0 && idx < n && !seen.has(idx)) {
394
+ seen.add(idx);
395
+ indices.push(idx);
396
+ }
397
+ }
398
+ } else if (/\b(all|everything|both|any)\b/.test(norm)) {
399
+ matched = true;
400
+ if (question.multiple) {
401
+ for (let i = 0; i < n; i++) {
402
+ if (!seen.has(i)) {
403
+ seen.add(i);
404
+ indices.push(i);
405
+ }
406
+ }
407
+ } else if (n > 0) {
408
+ seen.add(0);
409
+ indices.push(0);
410
+ }
411
+ } else if (/\b(none|nothing|skip|neither)\b/.test(norm)) {
412
+ matched = true;
413
+ } else {
414
+ const m = matchLabel(norm, question);
415
+ if (m !== null) {
416
+ matched = true;
417
+ if (m >= 0 && m < n && !seen.has(m)) {
418
+ seen.add(m);
419
+ indices.push(m);
420
+ }
421
+ }
422
+ }
423
+ let result = indices;
424
+ if (!question.multiple && result.length > 1) {
425
+ result = result.length > 0 ? [result[0]] : [];
426
+ }
427
+ if (result.length === 0 && !matched && question.custom === true) {
428
+ return [-1];
429
+ }
430
+ return result;
431
+ }
432
+ function mapSelection(sel, question, utterance) {
433
+ const out = [];
434
+ for (const idx of sel) {
435
+ if (idx === -1) out.push(utterance.trim());
436
+ else if (idx >= 0 && idx < question.options.length) out.push(question.options[idx].label);
437
+ }
438
+ return out;
439
+ }
440
+ function parseQuestionAnswers(utterance, questions) {
441
+ if (questions.length === 0) return [];
442
+ if (questions.length === 1) {
443
+ const sel2 = parseSelection(utterance, questions[0]);
444
+ return [mapSelection(sel2, questions[0], utterance)];
445
+ }
446
+ const tokens = extractNumericTokens(utterance);
447
+ if (tokens.length === questions.length) {
448
+ const res2 = [];
449
+ for (let i = 0; i < questions.length; i++) {
450
+ const sel2 = parseSelection(tokens[i], questions[i]);
451
+ res2.push(mapSelection(sel2, questions[i], utterance));
452
+ }
453
+ return res2;
454
+ }
455
+ const sel = parseSelection(utterance, questions[0]);
456
+ const res = [mapSelection(sel, questions[0], utterance)];
457
+ for (let i = 1; i < questions.length; i++) res.push([]);
458
+ return res;
459
+ }
460
+ function parsePermissionReply(utterance) {
461
+ const norm = normalise(utterance);
462
+ const padded = ` ${norm} `;
463
+ const alwaysPhrases = [
464
+ "always",
465
+ "always allow",
466
+ "allow always",
467
+ "never ask",
468
+ "dont ask again",
469
+ "don't ask again",
470
+ "permanent",
471
+ "permanently"
472
+ ];
473
+ const oncePhrases = [
474
+ "yes",
475
+ "yeah",
476
+ "yep",
477
+ "ok",
478
+ "okay",
479
+ "allow",
480
+ "approve",
481
+ "go",
482
+ "go ahead",
483
+ "proceed",
484
+ "do it",
485
+ "sure",
486
+ "fine",
487
+ "accept"
488
+ ];
489
+ const rejectPhrases = [
490
+ "no",
491
+ "nope",
492
+ "nah",
493
+ "deny",
494
+ "reject",
495
+ "refuse",
496
+ "cancel",
497
+ "stop",
498
+ "abort",
499
+ "decline",
500
+ "don't",
501
+ "do not"
502
+ ];
503
+ for (const p of alwaysPhrases) {
504
+ if (padded.includes(` ${p} `)) return "always";
505
+ }
506
+ for (const p of oncePhrases) {
507
+ if (padded.includes(` ${p} `)) return "once";
508
+ }
509
+ for (const p of rejectPhrases) {
510
+ if (padded.includes(` ${p} `)) return "reject";
511
+ }
512
+ return void 0;
513
+ }
514
+ var cleanTranscript = (text) => /[a-z0-9]/i.test(text) ? text.trim() : "";
515
+
516
+ // src/voice/audio.ts
517
+ function readU16(bytes, off) {
518
+ return bytes[off] | bytes[off + 1] << 8;
519
+ }
520
+ function readU32(bytes, off) {
521
+ return (bytes[off] | bytes[off + 1] << 8 | bytes[off + 2] << 16 | bytes[off + 3] << 24) >>> 0;
522
+ }
523
+ function pcmStats(pcm, sampleRate = 16e3) {
524
+ const samples = pcm ? pcm.length : 0;
525
+ let sumSq = 0;
526
+ let peak = 0;
527
+ if (pcm) {
528
+ for (let i = 0; i < pcm.length; i++) {
529
+ const v = pcm[i];
530
+ sumSq += v * v;
531
+ const a = v < 0 ? -v : v;
532
+ if (a > peak) peak = a;
533
+ }
534
+ }
535
+ const rms = samples === 0 ? 0 : Math.sqrt(sumSq / samples);
536
+ const durationMs = sampleRate > 0 ? samples / sampleRate * 1e3 : 0;
537
+ return { samples, durationMs, rms, peak };
538
+ }
539
+ function hasSpeech(pcm, options) {
540
+ const sampleRate = options?.sampleRate ?? 16e3;
541
+ const minDurationMs = options?.minDurationMs ?? 250;
542
+ const minRms = options?.minRms ?? 400;
543
+ const minPeak = options?.minPeak ?? 1500;
544
+ const stats = pcmStats(pcm, sampleRate);
545
+ return stats.durationMs >= minDurationMs && stats.rms >= minRms && stats.peak >= minPeak;
546
+ }
547
+ var SILENCE_HALLUCINATIONS = /* @__PURE__ */ new Set([
548
+ "thank you",
549
+ "thanks",
550
+ "thanks for watching",
551
+ "thank you for watching",
552
+ "thank you for watching!",
553
+ "you",
554
+ "bye",
555
+ "bye bye",
556
+ "please subscribe",
557
+ "subscribe",
558
+ "amara.org",
559
+ "subtitles by",
560
+ "the end",
561
+ "okay",
562
+ "ok"
563
+ ]);
564
+ function isSilenceHallucination(text, durationMs) {
565
+ const normalised = String(text).toLowerCase().replace(/[^a-z0-9.\s]/g, "").replace(/\s+/g, " ").trim().replace(/\.$/, "");
566
+ return durationMs < 3e3 && SILENCE_HALLUCINATIONS.has(normalised);
567
+ }
568
+ function decodeWavPcm(bytes) {
569
+ if (!bytes || bytes.length < 12) return void 0;
570
+ if (bytes[0] !== 82 || bytes[1] !== 73 || bytes[2] !== 70 || bytes[3] !== 70) return void 0;
571
+ if (bytes[8] !== 87 || bytes[9] !== 65 || bytes[10] !== 86 || bytes[11] !== 69) return void 0;
572
+ let off = 12;
573
+ const total = bytes.length;
574
+ let fmtFound = false;
575
+ let audioFormat = 0;
576
+ let channels = 0;
577
+ let sampleRate = 0;
578
+ let bitsPerSample = 0;
579
+ let dataOffset = -1;
580
+ let dataSize = 0;
581
+ let dataFound = false;
582
+ while (off + 8 <= total) {
583
+ const id = String.fromCharCode(bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]);
584
+ const size = readU32(bytes, off + 4);
585
+ const chunkStart = off + 8;
586
+ if (chunkStart > total) return void 0;
587
+ if (size > total - chunkStart) return void 0;
588
+ if (id === "fmt ") {
589
+ if (size < 16) return void 0;
590
+ if (chunkStart + 16 > total) return void 0;
591
+ audioFormat = readU16(bytes, chunkStart);
592
+ channels = readU16(bytes, chunkStart + 2);
593
+ sampleRate = readU32(bytes, chunkStart + 4);
594
+ bitsPerSample = readU16(bytes, chunkStart + 14);
595
+ fmtFound = true;
596
+ } else if (id === "data" && !dataFound) {
597
+ dataOffset = chunkStart;
598
+ dataSize = size;
599
+ dataFound = true;
600
+ }
601
+ let advance = 8 + size;
602
+ if (size % 2 === 1) advance += 1;
603
+ if (advance < 8) return void 0;
604
+ off += advance;
605
+ if (off > total) return void 0;
606
+ }
607
+ if (!fmtFound) return void 0;
608
+ if (!dataFound) return void 0;
609
+ if (audioFormat !== 1) return void 0;
610
+ if (bitsPerSample !== 16) return void 0;
611
+ if (channels < 1 || channels > 2) return void 0;
612
+ if (sampleRate <= 0) return void 0;
613
+ if (dataOffset < 0) return void 0;
614
+ if (dataOffset + dataSize > total) return void 0;
615
+ const sampleCount = Math.floor(dataSize / 2);
616
+ const pcm = new Int16Array(sampleCount);
617
+ const view = new DataView(bytes.buffer, bytes.byteOffset + dataOffset, sampleCount * 2);
618
+ for (let i = 0; i < sampleCount; i++) {
619
+ pcm[i] = view.getInt16(i * 2, true);
620
+ }
621
+ return { pcm, sampleRate, channels };
622
+ }
623
+
624
+ // src/voice/transport.ts
625
+ import * as nodeNet from "node:net";
626
+
627
+ // src/voice/protocol.ts
628
+ var VOICE_PROTOCOL_VERSION = 1;
629
+ function isRecord(value) {
630
+ return typeof value === "object" && value !== null && !Array.isArray(value);
631
+ }
632
+ function toText(line) {
633
+ return typeof line === "string" ? line : line.toString("utf8");
634
+ }
635
+ function normalizeLine(line) {
636
+ let s = toText(line);
637
+ if (s.endsWith("\n")) s = s.slice(0, -1);
638
+ if (s.endsWith("\r")) s = s.slice(0, -1);
639
+ return s;
640
+ }
641
+ function encodeMessage(message) {
642
+ return JSON.stringify(message) + "\n";
643
+ }
644
+ function decodeDaemonMessage(line) {
645
+ const text = normalizeLine(line);
646
+ if (text.trim() === "") return void 0;
647
+ let parsed;
648
+ try {
649
+ parsed = JSON.parse(text);
650
+ } catch {
651
+ return void 0;
652
+ }
653
+ if (!isRecord(parsed)) return void 0;
654
+ const t = parsed.t;
655
+ switch (t) {
656
+ case "welcome": {
657
+ if (typeof parsed.v !== "number") return void 0;
658
+ if (typeof parsed.armed !== "boolean") return void 0;
659
+ if (typeof parsed.wake !== "boolean") return void 0;
660
+ return { t: "welcome", v: parsed.v, armed: parsed.armed, wake: parsed.wake };
661
+ }
662
+ case "state": {
663
+ if (typeof parsed.armed !== "boolean") return void 0;
664
+ if (parsed.phase !== "idle" && parsed.phase !== "recording" && parsed.phase !== "transcribing") return void 0;
665
+ return { t: "state", armed: parsed.armed, phase: parsed.phase };
666
+ }
667
+ case "transcript": {
668
+ if (typeof parsed.text !== "string") return void 0;
669
+ if (parsed.source !== "wake" && parsed.source !== "manual") return void 0;
670
+ return { t: "transcript", text: parsed.text, source: parsed.source };
671
+ }
672
+ case "error": {
673
+ if (typeof parsed.message !== "string") return void 0;
674
+ return { t: "error", message: parsed.message };
675
+ }
676
+ case "pong":
677
+ return { t: "pong" };
678
+ default:
679
+ return void 0;
680
+ }
681
+ }
682
+ function createLineDecoder(decode) {
683
+ const MAX_LINE = 64 * 1024;
684
+ let buffer = "";
685
+ return (chunk) => {
686
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
687
+ buffer += text;
688
+ const out = [];
689
+ while (true) {
690
+ const idx = buffer.indexOf("\n");
691
+ if (idx === -1) break;
692
+ const line = buffer.slice(0, idx);
693
+ buffer = buffer.slice(idx + 1);
694
+ if (line.length > MAX_LINE) {
695
+ buffer = "";
696
+ break;
697
+ }
698
+ const msg = decode(line);
699
+ if (msg !== void 0) out.push(msg);
700
+ }
701
+ if (buffer.length > MAX_LINE) {
702
+ buffer = "";
703
+ }
704
+ return out;
705
+ };
706
+ }
707
+
708
+ // src/voice/transport.ts
709
+ function createVoiceClient(options, deps) {
710
+ const port = options.port ?? 8765;
711
+ const host = options.host ?? "127.0.0.1";
712
+ const onMessage = options.onMessage;
713
+ const onConnectionChange = options.onConnectionChange;
714
+ const reconnectMs = options.reconnectMs ?? 2e3;
715
+ const netMod = deps && deps.net ? deps.net : nodeNet;
716
+ let socket;
717
+ let connected = false;
718
+ let stopped = false;
719
+ let timer;
720
+ let decoder = createLineDecoder(decodeDaemonMessage);
721
+ const setConnected = (value) => {
722
+ if (connected === value) return;
723
+ connected = value;
724
+ try {
725
+ onConnectionChange?.(value);
726
+ } catch {
727
+ }
728
+ };
729
+ const scheduleReconnect = () => {
730
+ if (stopped) return;
731
+ if (timer) return;
732
+ timer = setTimeout(() => {
733
+ timer = void 0;
734
+ connect();
735
+ }, reconnectMs);
736
+ try {
737
+ timer.unref?.();
738
+ } catch {
739
+ }
740
+ };
741
+ const connect = () => {
742
+ if (stopped) return;
743
+ if (socket && !socket.destroyed) return;
744
+ decoder = createLineDecoder(decodeDaemonMessage);
745
+ let sock;
746
+ try {
747
+ sock = netMod.createConnection({ port, host });
748
+ } catch {
749
+ socket = void 0;
750
+ scheduleReconnect();
751
+ return;
752
+ }
753
+ socket = sock;
754
+ sock.on("connect", () => {
755
+ setConnected(true);
756
+ });
757
+ sock.on("data", (chunk) => {
758
+ try {
759
+ const msgs = decoder(chunk);
760
+ for (const m of msgs) {
761
+ try {
762
+ onMessage?.(m);
763
+ } catch {
764
+ }
765
+ }
766
+ } catch {
767
+ }
768
+ });
769
+ sock.on("close", () => {
770
+ socket = void 0;
771
+ setConnected(false);
772
+ scheduleReconnect();
773
+ });
774
+ sock.on("error", () => {
775
+ setConnected(false);
776
+ try {
777
+ sock.destroy();
778
+ } catch {
779
+ }
780
+ socket = void 0;
781
+ scheduleReconnect();
782
+ });
783
+ };
784
+ const client = {
785
+ get connected() {
786
+ return connected;
787
+ },
788
+ start() {
789
+ stopped = false;
790
+ connect();
791
+ },
792
+ stop() {
793
+ if (stopped && !socket && !timer) {
794
+ }
795
+ stopped = true;
796
+ if (timer) {
797
+ clearTimeout(timer);
798
+ timer = void 0;
799
+ }
800
+ if (socket) {
801
+ try {
802
+ socket.destroy();
803
+ } catch {
804
+ }
805
+ socket = void 0;
806
+ }
807
+ setConnected(false);
808
+ },
809
+ send(message) {
810
+ try {
811
+ if (stopped) return false;
812
+ if (!socket || socket.destroyed || !socket.writable) return false;
813
+ if (!connected) return false;
814
+ socket.write(encodeMessage(message));
815
+ return true;
816
+ } catch {
817
+ return false;
818
+ }
819
+ }
820
+ };
821
+ return client;
822
+ }
823
+
824
+ // src/tui/index.ts
825
+ var DEFAULT_MODEL = "whisper-large-v3-turbo";
826
+ var DEFAULT_LANGUAGE = "en";
827
+ var DEFAULT_MAX_SECONDS = 120;
828
+ var DEFAULT_DICTATE_KEY = "<leader>v";
829
+ var DEFAULT_ANSWER_KEY = "<leader>a";
830
+ var DEFAULT_DAEMON_PORT = 8765;
831
+ var DEFAULT_DAEMON_HOST = "127.0.0.1";
832
+ var SETUP_HINT = 'Set GROQ_KEY or create ~/.config/lacode/voice.json with {"groqKey":"..."}';
833
+ var daemonConnected = false;
834
+ var recordingPulse;
835
+ var recorder;
836
+ var phase = "idle";
837
+ var activeMode;
838
+ var activeRequest;
839
+ function resetState() {
840
+ phase = "idle";
841
+ activeMode = void 0;
842
+ activeRequest = void 0;
843
+ }
844
+ function resolveOptions(options) {
845
+ const o = options ?? {};
846
+ const intent = o.intent === "insert" || o.intent === "answer" || o.intent === "auto" ? o.intent : "auto";
847
+ return {
848
+ enabled: o.enabled !== false,
849
+ dictation: o.dictation !== false,
850
+ answers: o.answers !== false,
851
+ submit: o.submit === true,
852
+ model: typeof o.model === "string" && o.model.length > 0 ? o.model : DEFAULT_MODEL,
853
+ language: typeof o.language === "string" && o.language.length > 0 ? o.language : DEFAULT_LANGUAGE,
854
+ vocabulary: typeof o.vocabulary === "string" && o.vocabulary.length > 0 ? o.vocabulary : void 0,
855
+ maxSeconds: typeof o.maxSeconds === "number" && Number.isFinite(o.maxSeconds) && o.maxSeconds > 0 ? o.maxSeconds : DEFAULT_MAX_SECONDS,
856
+ dictateKey: typeof o.dictateKey === "string" && o.dictateKey.length > 0 ? o.dictateKey : DEFAULT_DICTATE_KEY,
857
+ answerKey: typeof o.answerKey === "string" && o.answerKey.length > 0 ? o.answerKey : DEFAULT_ANSWER_KEY,
858
+ useDaemon: o.useDaemon !== false,
859
+ daemonPort: typeof o.daemonPort === "number" && Number.isFinite(o.daemonPort) && o.daemonPort > 0 ? o.daemonPort : DEFAULT_DAEMON_PORT,
860
+ daemonHost: typeof o.daemonHost === "string" && o.daemonHost.length > 0 ? o.daemonHost : DEFAULT_DAEMON_HOST,
861
+ intent
862
+ };
863
+ }
864
+ function toast(api, input) {
865
+ api.ui.toast(input);
866
+ }
867
+ var stopRecordingIndicator = () => {
868
+ if (!recordingPulse) return;
869
+ clearInterval(recordingPulse);
870
+ recordingPulse = void 0;
871
+ };
872
+ var startRecordingIndicator = (api, message = "\u25CF Recording\u2026") => {
873
+ stopRecordingIndicator();
874
+ const show = () => {
875
+ try {
876
+ toast(api, { variant: "info", title: "lacode voice", message, duration: 5e3 });
877
+ } catch {
878
+ }
879
+ };
880
+ show();
881
+ recordingPulse = setInterval(show, 4e3);
882
+ recordingPulse.unref?.();
883
+ };
884
+ function readableError(err) {
885
+ if (err instanceof Error) return err.message;
886
+ if (typeof err === "string") return err;
887
+ if (err && typeof err === "object") {
888
+ const message = err.message;
889
+ if (typeof message === "string" && message.length > 0) return message;
890
+ try {
891
+ return JSON.stringify(err);
892
+ } catch {
893
+ return String(err);
894
+ }
895
+ }
896
+ return String(err);
897
+ }
898
+ function unwrapList(result) {
899
+ if (Array.isArray(result)) return result;
900
+ if (result && typeof result === "object") {
901
+ const envelope = result;
902
+ if (envelope.error !== void 0 && envelope.error !== null) {
903
+ throw new Error(readableError(envelope.error));
904
+ }
905
+ if (Array.isArray(envelope.data)) return envelope.data;
906
+ }
907
+ return [];
908
+ }
909
+ function ensureOk(result) {
910
+ if (!result || typeof result !== "object" || !("error" in result)) return;
911
+ const error = result.error;
912
+ if (error !== void 0 && error !== null) throw new Error(readableError(error));
913
+ }
914
+ async function fetchPending(api) {
915
+ const [questions, permissions] = await Promise.all([
916
+ api.client.question.list(),
917
+ api.client.permission.list()
918
+ ]);
919
+ const pending = [];
920
+ for (const request of unwrapList(questions)) {
921
+ pending.push({ kind: "question", request });
922
+ }
923
+ for (const request of unwrapList(permissions)) {
924
+ pending.push({ kind: "permission", request });
925
+ }
926
+ return pending;
927
+ }
928
+ async function stillPending(api, request) {
929
+ const pending = await fetchPending(api);
930
+ return pending.some((entry) => entry.request.id === request.request.id);
931
+ }
932
+ function toParsedQuestions(request) {
933
+ return (request.questions ?? []).map((question) => ({
934
+ options: (question.options ?? []).map((option) => ({ label: option.label })),
935
+ multiple: question.multiple,
936
+ custom: question.custom
937
+ }));
938
+ }
939
+ async function stopAndComplete(api, opts) {
940
+ const mode = activeMode;
941
+ const request = activeRequest;
942
+ stopRecordingIndicator();
943
+ toast(api, { variant: "info", title: "lacode voice", message: "Transcribing\u2026", duration: 1500 });
944
+ try {
945
+ const bytes = await recorder.stop();
946
+ if (mode === "dictate") {
947
+ await finishDictate(api, opts, bytes);
948
+ } else if (mode === "answer" && request) {
949
+ await finishAnswer(api, opts, bytes, request);
950
+ }
951
+ } finally {
952
+ stopRecordingIndicator();
953
+ resetState();
954
+ }
955
+ }
956
+ async function finishDictate(api, opts, bytes) {
957
+ if (!bytes || bytes.length === 0) {
958
+ toast(api, { variant: "warning", message: "No audio captured" });
959
+ return;
960
+ }
961
+ const decoded = decodeWavPcm(bytes);
962
+ if (!decoded || !hasSpeech(decoded.pcm, { sampleRate: decoded.sampleRate })) {
963
+ api.ui.toast({ variant: "warning", title: "lacode voice", message: "No speech detected" });
964
+ return;
965
+ }
966
+ const resolved = resolveGroqKey();
967
+ if (!resolved) {
968
+ toast(api, { variant: "warning", message: SETUP_HINT });
969
+ return;
970
+ }
971
+ const text = cleanTranscript(await transcribe({
972
+ audio: bytes,
973
+ key: resolved.key,
974
+ model: opts.model,
975
+ language: opts.language,
976
+ prompt: opts.vocabulary
977
+ }));
978
+ if (isSilenceHallucination(text, decoded.pcm.length / decoded.sampleRate * 1e3)) {
979
+ api.ui.toast({ variant: "warning", title: "lacode voice", message: "No speech detected" });
980
+ return;
981
+ }
982
+ if (text.trim().length === 0) {
983
+ toast(api, { variant: "warning", message: "Nothing transcribed" });
984
+ return;
985
+ }
986
+ await api.client.tui.appendPrompt({ text });
987
+ if (opts.submit) await api.client.tui.submitPrompt();
988
+ toast(api, { variant: "success", message: text.slice(0, 60) });
989
+ }
990
+ async function finishAnswer(api, opts, bytes, request) {
991
+ if (!bytes || bytes.length === 0) {
992
+ toast(api, { variant: "warning", message: "No audio captured" });
993
+ return;
994
+ }
995
+ const decoded = decodeWavPcm(bytes);
996
+ if (!decoded || !hasSpeech(decoded.pcm, { sampleRate: decoded.sampleRate })) {
997
+ api.ui.toast({ variant: "warning", title: "lacode voice", message: "No speech detected" });
998
+ return;
999
+ }
1000
+ const resolved = resolveGroqKey();
1001
+ if (!resolved) {
1002
+ toast(api, { variant: "warning", message: SETUP_HINT });
1003
+ return;
1004
+ }
1005
+ const text = cleanTranscript(await transcribe({
1006
+ audio: bytes,
1007
+ key: resolved.key,
1008
+ model: opts.model,
1009
+ language: opts.language,
1010
+ prompt: opts.vocabulary
1011
+ }));
1012
+ if (isSilenceHallucination(text, decoded.pcm.length / decoded.sampleRate * 1e3)) {
1013
+ api.ui.toast({ variant: "warning", title: "lacode voice", message: "No speech detected" });
1014
+ return;
1015
+ }
1016
+ await answerWithText(api, opts, text, request);
1017
+ }
1018
+ async function insertText(api, opts, text) {
1019
+ await api.client.tui.appendPrompt({ text });
1020
+ if (opts.submit) await api.client.tui.submitPrompt();
1021
+ toast(api, { variant: "success", message: text.slice(0, 60) });
1022
+ }
1023
+ async function answerWithText(api, opts, text, request) {
1024
+ if (request.kind === "permission") {
1025
+ const reply = parsePermissionReply(text);
1026
+ if (!reply) {
1027
+ toast(api, { variant: "warning", message: "Couldn't match yes / always / no" });
1028
+ return;
1029
+ }
1030
+ if (!await stillPending(api, request)) {
1031
+ toast(api, { variant: "warning", message: "Request is no longer pending" });
1032
+ return;
1033
+ }
1034
+ ensureOk(await api.client.permission.reply({ requestID: request.request.id, reply }));
1035
+ toast(api, { variant: "success", message: `Permission: ${reply}` });
1036
+ return;
1037
+ }
1038
+ const answers = parseQuestionAnswers(text, toParsedQuestions(request.request));
1039
+ const allEmpty = answers.every((answer) => !answer || answer.length === 0);
1040
+ if (allEmpty) {
1041
+ toast(api, { variant: "warning", message: "Couldn't match an option" });
1042
+ return;
1043
+ }
1044
+ if (!await stillPending(api, request)) {
1045
+ toast(api, { variant: "warning", message: "Request is no longer pending" });
1046
+ return;
1047
+ }
1048
+ ensureOk(await api.client.question.reply({ requestID: request.request.id, answers }));
1049
+ const labels = answers.map((answer) => answer.join(", ")).filter((answer) => answer.length > 0).join("; ");
1050
+ toast(api, { variant: "success", message: labels });
1051
+ }
1052
+ var tui = async (api, options) => {
1053
+ const opts = resolveOptions(options);
1054
+ if (!opts.enabled) return;
1055
+ daemonConnected = false;
1056
+ resetState();
1057
+ const rawOptions = options ?? {};
1058
+ let activeRecorder;
1059
+ if (typeof rawOptions.recorder === "function") {
1060
+ activeRecorder = rawOptions.recorder();
1061
+ } else if (rawOptions.recorder && typeof rawOptions.recorder.start === "function") {
1062
+ activeRecorder = rawOptions.recorder;
1063
+ } else if (typeof rawOptions.createRecorder === "function") {
1064
+ activeRecorder = rawOptions.createRecorder({ maxSeconds: opts.maxSeconds });
1065
+ } else {
1066
+ activeRecorder = createRecorder({ maxSeconds: opts.maxSeconds });
1067
+ }
1068
+ recorder = activeRecorder;
1069
+ api.lifecycle.onDispose(() => activeRecorder.abort());
1070
+ api.lifecycle.onDispose(() => stopRecordingIndicator());
1071
+ let forcedIntent;
1072
+ const handleTranscript = async (text, source) => {
1073
+ const cleaned = cleanTranscript(text);
1074
+ if (cleaned.length === 0) return;
1075
+ let intent;
1076
+ if (forcedIntent) {
1077
+ intent = forcedIntent;
1078
+ forcedIntent = void 0;
1079
+ } else if (opts.intent === "insert" || opts.intent === "answer") {
1080
+ intent = opts.intent;
1081
+ } else {
1082
+ intent = "auto";
1083
+ }
1084
+ try {
1085
+ const pending = await fetchPending(api);
1086
+ if (intent === "answer") {
1087
+ if (pending.length === 0) {
1088
+ await insertText(api, opts, cleaned);
1089
+ return;
1090
+ }
1091
+ if (pending.length > 1) {
1092
+ toast(api, {
1093
+ variant: "warning",
1094
+ message: "Several requests are pending \u2014 answer with the keyboard"
1095
+ });
1096
+ return;
1097
+ }
1098
+ await answerWithText(api, opts, cleaned, pending[0]);
1099
+ return;
1100
+ }
1101
+ if (intent === "auto") {
1102
+ if (pending.length === 1) {
1103
+ await answerWithText(api, opts, cleaned, pending[0]);
1104
+ return;
1105
+ }
1106
+ await insertText(api, opts, cleaned);
1107
+ return;
1108
+ }
1109
+ await insertText(api, opts, cleaned);
1110
+ } catch (err) {
1111
+ toast(api, { variant: "error", title: "lacode voice", message: readableError(err) });
1112
+ }
1113
+ };
1114
+ let daemon;
1115
+ if (opts.useDaemon) {
1116
+ daemon = createVoiceClient({
1117
+ port: opts.daemonPort,
1118
+ host: opts.daemonHost,
1119
+ onMessage: (message) => {
1120
+ try {
1121
+ if (message.t === "state") {
1122
+ if (message.phase === "recording") {
1123
+ startRecordingIndicator(api);
1124
+ } else if (message.phase === "transcribing") {
1125
+ stopRecordingIndicator();
1126
+ toast(api, {
1127
+ variant: "info",
1128
+ title: "lacode voice",
1129
+ message: "Transcribing\u2026",
1130
+ duration: 1500
1131
+ });
1132
+ } else if (message.phase === "idle") {
1133
+ stopRecordingIndicator();
1134
+ }
1135
+ } else if (message.t === "transcript") {
1136
+ void handleTranscript(message.text, message.source);
1137
+ } else if (message.t === "error") {
1138
+ stopRecordingIndicator();
1139
+ toast(api, { variant: "error", title: "lacode voice", message: message.message });
1140
+ }
1141
+ } catch (err) {
1142
+ stopRecordingIndicator();
1143
+ toast(api, { variant: "error", title: "lacode voice", message: readableError(err) });
1144
+ }
1145
+ },
1146
+ onConnectionChange: (connected) => {
1147
+ daemonConnected = connected;
1148
+ if (connected && daemon) {
1149
+ daemon.send({ t: "hello", v: VOICE_PROTOCOL_VERSION, client: "tui" });
1150
+ }
1151
+ }
1152
+ });
1153
+ daemon.start();
1154
+ api.lifecycle.onDispose(() => daemon.stop());
1155
+ }
1156
+ const runDictate = async () => {
1157
+ try {
1158
+ if (!opts.dictation) {
1159
+ toast(api, { variant: "warning", message: "Voice dictation is disabled" });
1160
+ return;
1161
+ }
1162
+ if (daemonConnected && daemon) {
1163
+ forcedIntent = "insert";
1164
+ daemon.send({ t: "toggle" });
1165
+ return;
1166
+ }
1167
+ if (phase === "recording") {
1168
+ await stopAndComplete(api, opts);
1169
+ return;
1170
+ }
1171
+ const resolved = resolveGroqKey();
1172
+ if (!resolved) {
1173
+ toast(api, { variant: "warning", message: SETUP_HINT });
1174
+ return;
1175
+ }
1176
+ recorder.start();
1177
+ phase = "recording";
1178
+ activeMode = "dictate";
1179
+ activeRequest = void 0;
1180
+ startRecordingIndicator(api, "\u25CF Recording\u2026 press the key again to stop");
1181
+ } catch (err) {
1182
+ stopRecordingIndicator();
1183
+ resetState();
1184
+ toast(api, { variant: "error", title: "lacode voice", message: readableError(err) });
1185
+ }
1186
+ };
1187
+ const runAnswer = async () => {
1188
+ try {
1189
+ if (!opts.answers) {
1190
+ toast(api, { variant: "warning", message: "Voice answers are disabled" });
1191
+ return;
1192
+ }
1193
+ if (daemonConnected && daemon) {
1194
+ const pending2 = await fetchPending(api);
1195
+ if (pending2.length === 0) {
1196
+ toast(api, { variant: "warning", message: "No pending question or permission" });
1197
+ return;
1198
+ }
1199
+ forcedIntent = "answer";
1200
+ daemon.send({ t: "toggle" });
1201
+ return;
1202
+ }
1203
+ if (phase === "recording") {
1204
+ await stopAndComplete(api, opts);
1205
+ return;
1206
+ }
1207
+ const pending = await fetchPending(api);
1208
+ if (pending.length === 0) {
1209
+ toast(api, { variant: "warning", message: "No pending question or permission" });
1210
+ return;
1211
+ }
1212
+ if (pending.length > 1) {
1213
+ toast(api, {
1214
+ variant: "warning",
1215
+ message: "Several requests are pending \u2014 answer with the keyboard"
1216
+ });
1217
+ return;
1218
+ }
1219
+ activeRequest = pending[0];
1220
+ recorder.start();
1221
+ phase = "recording";
1222
+ activeMode = "answer";
1223
+ startRecordingIndicator(api, "\u25CF Recording\u2026 press the key again to stop");
1224
+ } catch (err) {
1225
+ stopRecordingIndicator();
1226
+ resetState();
1227
+ toast(api, { variant: "error", title: "lacode voice", message: readableError(err) });
1228
+ }
1229
+ };
1230
+ const runCheck = async () => {
1231
+ try {
1232
+ const resolved = resolveGroqKey();
1233
+ const daemonStatus = daemonConnected ? `daemon: connected (${opts.daemonPort})` : `daemon: not running (${opts.daemonPort})`;
1234
+ if (resolved) {
1235
+ toast(api, { variant: "success", message: `Groq key found (${resolved.source}); ${daemonStatus}` });
1236
+ return;
1237
+ }
1238
+ toast(api, { variant: "warning", message: `${SETUP_HINT}; ${daemonStatus}` });
1239
+ } catch (err) {
1240
+ toast(api, { variant: "error", title: "lacode voice", message: readableError(err) });
1241
+ }
1242
+ };
1243
+ const commands = [
1244
+ {
1245
+ name: "lacode.voice.dictate",
1246
+ title: "Voice: dictate",
1247
+ category: "Voice",
1248
+ namespace: "palette",
1249
+ run: async () => runDictate()
1250
+ },
1251
+ {
1252
+ name: "lacode.voice.answer",
1253
+ title: "Voice: answer question or permission",
1254
+ category: "Voice",
1255
+ namespace: "palette",
1256
+ run: async () => runAnswer()
1257
+ },
1258
+ {
1259
+ name: "lacode.voice.check",
1260
+ title: "Voice: check setup",
1261
+ category: "Voice",
1262
+ namespace: "palette",
1263
+ run: async () => runCheck()
1264
+ }
1265
+ ];
1266
+ const bindings = [];
1267
+ if (opts.dictation) {
1268
+ bindings.push({ key: opts.dictateKey, cmd: "lacode.voice.dictate", desc: "Voice dictate" });
1269
+ }
1270
+ if (opts.answers) {
1271
+ bindings.push({ key: opts.answerKey, cmd: "lacode.voice.answer", desc: "Voice answer" });
1272
+ }
1273
+ api.keymap.registerLayer({ mode: "base", commands, bindings });
1274
+ };
1275
+ var mod = {
1276
+ id: "lacode.voice",
1277
+ tui
1278
+ };
1279
+ var index_default = mod;
1280
+ export {
1281
+ index_default as default
1282
+ };