kimaki 0.0.0 → 0.0.2

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/bundle.js ADDED
@@ -0,0 +1,3019 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true,
8
+ configurable: true,
9
+ set: (newValue) => all[name] = () => newValue
10
+ });
11
+ };
12
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
13
+
14
+ // src/video-to-ascii.ts
15
+ var exports_video_to_ascii = {};
16
+ __export(exports_video_to_ascii, {
17
+ extractFrames: () => extractFrames,
18
+ convertImageToAscii: () => convertImageToAscii
19
+ });
20
+ import fs2 from "node:fs";
21
+ import sharp from "sharp";
22
+ import { exec } from "node:child_process";
23
+ import { promisify } from "node:util";
24
+ function stripAnsi(str) {
25
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
26
+ }
27
+ async function extractFrames({
28
+ videoPath,
29
+ outputDir,
30
+ fps
31
+ }) {
32
+ if (!fs2.existsSync(outputDir)) {
33
+ fs2.mkdirSync(outputDir, { recursive: true });
34
+ }
35
+ console.log(`Extracting frames at ${fps} fps...`);
36
+ const command = `ffmpeg -i "${videoPath}" -vf fps=${fps} "${outputDir}/frame_%04d.png" -y`;
37
+ try {
38
+ const { stdout, stderr } = await execAsync(command);
39
+ console.log("Frames extracted successfully");
40
+ } catch (error) {
41
+ console.error("Error extracting frames:", error.message);
42
+ throw error;
43
+ }
44
+ }
45
+ async function convertImageToAscii({
46
+ imagePath,
47
+ cols,
48
+ rows,
49
+ keepAspectRatio = true,
50
+ colored = false
51
+ }) {
52
+ try {
53
+ const metadata = await sharp(imagePath).metadata();
54
+ const originalWidth = metadata.width || 100;
55
+ const originalHeight = metadata.height || 100;
56
+ let finalCols = cols;
57
+ let finalRows = rows;
58
+ let resizeOptions = {};
59
+ if (keepAspectRatio) {
60
+ const aspectRatio = originalWidth / originalHeight;
61
+ const targetAspectRatio = cols * 2 / rows;
62
+ if (aspectRatio > targetAspectRatio) {
63
+ finalRows = Math.floor(cols / aspectRatio * 0.5);
64
+ } else {
65
+ finalCols = Math.floor(rows * aspectRatio * 2);
66
+ }
67
+ resizeOptions = { fit: "fill" };
68
+ } else {
69
+ const aspectRatio = originalWidth / originalHeight;
70
+ const targetAspectRatio = cols * 2 / rows;
71
+ if (aspectRatio > targetAspectRatio) {
72
+ const zoomHeight = Math.ceil(cols / aspectRatio * 0.5);
73
+ resizeOptions = {
74
+ width: cols,
75
+ height: zoomHeight,
76
+ fit: "cover",
77
+ position: "center"
78
+ };
79
+ } else {
80
+ const zoomWidth = Math.ceil(rows * aspectRatio * 2);
81
+ resizeOptions = {
82
+ width: zoomWidth,
83
+ height: rows,
84
+ fit: "cover",
85
+ position: "center"
86
+ };
87
+ }
88
+ finalCols = cols;
89
+ finalRows = rows;
90
+ }
91
+ if (colored) {
92
+ const { data, info } = await sharp(imagePath).resize(finalCols, finalRows, resizeOptions).raw().toBuffer({ resolveWithObject: true });
93
+ const asciiArt = [];
94
+ const channels = info.channels;
95
+ const actualCols = info.width;
96
+ const actualRows = info.height;
97
+ for (let y = 0;y < actualRows; y++) {
98
+ let row = "";
99
+ for (let x = 0;x < actualCols; x++) {
100
+ const pixelIndex = (y * actualCols + x) * channels;
101
+ const r = data[pixelIndex] || 0;
102
+ const g = data[pixelIndex + 1] || 0;
103
+ const b = data[pixelIndex + 2] || 0;
104
+ const brightness = r * 0.299 + g * 0.587 + b * 0.114;
105
+ const charIndex = Math.floor(brightness / 255 * (ASCII_CHARS.length - 1));
106
+ const char = ASCII_CHARS[charIndex] || " ";
107
+ const brightnessFactor = brightness / 255;
108
+ const adjustedR = Math.floor(r * brightnessFactor * 0.8);
109
+ const adjustedG = Math.floor(g * brightnessFactor * 0.8);
110
+ const adjustedB = Math.floor(b * brightnessFactor * 0.8);
111
+ row += `\x1B[38;2;${adjustedR};${adjustedG};${adjustedB}m${char}\x1B[0m`;
112
+ }
113
+ if (!keepAspectRatio) {
114
+ const visibleLength = stripAnsi(row).length;
115
+ if (visibleLength < cols) {
116
+ row += " ".repeat(cols - visibleLength);
117
+ }
118
+ }
119
+ asciiArt.push(row);
120
+ }
121
+ if (!keepAspectRatio) {
122
+ while (asciiArt.length < rows) {
123
+ asciiArt.push(" ".repeat(cols));
124
+ }
125
+ }
126
+ return asciiArt.join(`
127
+ `);
128
+ } else {
129
+ const { data: resized, info } = await sharp(imagePath).resize(finalCols, finalRows, resizeOptions).greyscale().raw().toBuffer({ resolveWithObject: true });
130
+ const actualCols = info.width;
131
+ const actualRows = info.height;
132
+ const asciiArt = [];
133
+ for (let y = 0;y < actualRows; y++) {
134
+ let row = "";
135
+ for (let x = 0;x < actualCols; x++) {
136
+ const pixelIndex = y * actualCols + x;
137
+ const brightness = resized[pixelIndex] || 0;
138
+ const charIndex = Math.floor(brightness / 255 * (ASCII_CHARS.length - 1));
139
+ row += ASCII_CHARS[charIndex] || " ";
140
+ }
141
+ if (!keepAspectRatio) {
142
+ const visibleLength = stripAnsi(row).length;
143
+ if (visibleLength < cols) {
144
+ row += " ".repeat(cols - visibleLength);
145
+ }
146
+ }
147
+ asciiArt.push(row);
148
+ }
149
+ if (!keepAspectRatio) {
150
+ while (asciiArt.length < rows) {
151
+ asciiArt.push(" ".repeat(cols));
152
+ }
153
+ }
154
+ return asciiArt.join(`
155
+ `);
156
+ }
157
+ } catch (error) {
158
+ console.error(`Error converting ${imagePath}:`, error.message);
159
+ throw error;
160
+ }
161
+ }
162
+ var execAsync, ASCII_CHARS = " .'`^\",:;Il!i><~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$";
163
+ var init_video_to_ascii = __esm(() => {
164
+ execAsync = promisify(exec);
165
+ });
166
+
167
+ // ../liveapi/src/utils.ts
168
+ function base64ToArrayBuffer(base64) {
169
+ var binaryString = atob(base64);
170
+ var bytes = new Uint8Array(binaryString.length);
171
+ for (let i = 0;i < binaryString.length; i++) {
172
+ bytes[i] = binaryString.charCodeAt(i);
173
+ }
174
+ return bytes.buffer;
175
+ }
176
+ var map, audioContext;
177
+ var init_utils = __esm(() => {
178
+ map = new Map;
179
+ audioContext = (() => {
180
+ const didInteract = new Promise((res) => {
181
+ if (typeof window === "undefined") {
182
+ res(true);
183
+ }
184
+ addEventListener("pointerdown", res, { once: true });
185
+ addEventListener("keydown", res, { once: true });
186
+ });
187
+ return async (options) => {
188
+ try {
189
+ const a = new Audio;
190
+ a.src = "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA";
191
+ await a.play();
192
+ if (options?.id && map.has(options.id)) {
193
+ const ctx2 = map.get(options.id);
194
+ if (ctx2) {
195
+ return ctx2;
196
+ }
197
+ }
198
+ const ctx = new AudioContext(options);
199
+ if (options?.id) {
200
+ map.set(options.id, ctx);
201
+ }
202
+ return ctx;
203
+ } catch (e) {
204
+ await didInteract;
205
+ if (options?.id && map.has(options.id)) {
206
+ const ctx2 = map.get(options.id);
207
+ if (ctx2) {
208
+ return ctx2;
209
+ }
210
+ }
211
+ const ctx = new AudioContext(options);
212
+ if (options?.id) {
213
+ map.set(options.id, ctx);
214
+ }
215
+ return ctx;
216
+ }
217
+ };
218
+ })();
219
+ });
220
+
221
+ // ../liveapi/src/worklets/audio-processing.ts
222
+ var AudioRecordingWorklet = `
223
+ class AudioProcessingWorklet extends AudioWorkletProcessor {
224
+
225
+ // send and clear buffer every 2048 samples,
226
+ // which at 16khz is about 8 times a second
227
+ buffer = new Int16Array(2048);
228
+
229
+ // current write index
230
+ bufferWriteIndex = 0;
231
+
232
+ constructor() {
233
+ super();
234
+ this.hasAudio = false;
235
+ }
236
+
237
+ /**
238
+ * @param inputs Float32Array[][] [input#][channel#][sample#] so to access first inputs 1st channel inputs[0][0]
239
+ * @param outputs Float32Array[][]
240
+ */
241
+ process(inputs) {
242
+ if (inputs[0].length) {
243
+ const channel0 = inputs[0][0];
244
+ this.processChunk(channel0);
245
+ }
246
+ return true;
247
+ }
248
+
249
+ sendAndClearBuffer(){
250
+ this.port.postMessage({
251
+ event: "chunk",
252
+ data: {
253
+ int16arrayBuffer: this.buffer.slice(0, this.bufferWriteIndex).buffer,
254
+ },
255
+ });
256
+ this.bufferWriteIndex = 0;
257
+ }
258
+
259
+ processChunk(float32Array) {
260
+ const l = float32Array.length;
261
+
262
+ for (let i = 0; i < l; i++) {
263
+ // convert float32 -1 to 1 to int16 -32768 to 32767
264
+ const int16Value = float32Array[i] * 32768;
265
+ this.buffer[this.bufferWriteIndex++] = int16Value;
266
+ if(this.bufferWriteIndex >= this.buffer.length) {
267
+ this.sendAndClearBuffer();
268
+ }
269
+ }
270
+
271
+ if(this.bufferWriteIndex >= this.buffer.length) {
272
+ this.sendAndClearBuffer();
273
+ }
274
+ }
275
+ }
276
+ `, audio_processing_default;
277
+ var init_audio_processing = __esm(() => {
278
+ audio_processing_default = AudioRecordingWorklet;
279
+ });
280
+
281
+ // ../liveapi/src/worklets/vol-meter.ts
282
+ var VolMeterWorket = `
283
+ class VolMeter extends AudioWorkletProcessor {
284
+ volume
285
+ updateIntervalInMS
286
+ nextUpdateFrame
287
+
288
+ constructor() {
289
+ super()
290
+ this.volume = 0
291
+ this.updateIntervalInMS = 25
292
+ this.nextUpdateFrame = this.updateIntervalInMS
293
+ this.port.onmessage = event => {
294
+ if (event.data.updateIntervalInMS) {
295
+ this.updateIntervalInMS = event.data.updateIntervalInMS
296
+ }
297
+ }
298
+ }
299
+
300
+ get intervalInFrames() {
301
+ return (this.updateIntervalInMS / 1000) * sampleRate
302
+ }
303
+
304
+ process(inputs) {
305
+ const input = inputs[0]
306
+
307
+ if (input.length > 0) {
308
+ const samples = input[0]
309
+ let sum = 0
310
+ let rms = 0
311
+
312
+ for (let i = 0; i < samples.length; ++i) {
313
+ sum += samples[i] * samples[i]
314
+ }
315
+
316
+ rms = Math.sqrt(sum / samples.length)
317
+ this.volume = Math.max(rms, this.volume * 0.7)
318
+
319
+ this.nextUpdateFrame -= samples.length
320
+ if (this.nextUpdateFrame < 0) {
321
+ this.nextUpdateFrame += this.intervalInFrames
322
+ this.port.postMessage({volume: this.volume})
323
+ }
324
+ }
325
+
326
+ return true
327
+ }
328
+ }`, vol_meter_default;
329
+ var init_vol_meter = __esm(() => {
330
+ vol_meter_default = VolMeterWorket;
331
+ });
332
+
333
+ // ../liveapi/src/audio-resampler.ts
334
+ function downSampleAudioBuffer(buffer, inputSampleRate, outputSampleRate) {
335
+ if (outputSampleRate >= inputSampleRate) {
336
+ throw new Error("Output sample rate must be less than input sample rate.");
337
+ }
338
+ const sampleRateRatio = inputSampleRate / outputSampleRate;
339
+ const newLength = Math.round(buffer.length / sampleRateRatio);
340
+ const result = new Float32Array(newLength);
341
+ let offsetResult = 0;
342
+ let offsetBuffer = 0;
343
+ while (offsetResult < result.length) {
344
+ const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
345
+ let accum = 0;
346
+ let count = 0;
347
+ for (let i = offsetBuffer;i < nextOffsetBuffer && i < buffer.length; i++) {
348
+ accum += buffer[i];
349
+ count++;
350
+ }
351
+ result[offsetResult] = accum / count;
352
+ offsetResult++;
353
+ offsetBuffer = nextOffsetBuffer;
354
+ }
355
+ return result;
356
+ }
357
+ function downSampleInt16Buffer(buffer, inputSampleRate, outputSampleRate) {
358
+ if (outputSampleRate >= inputSampleRate) {
359
+ throw new Error("Output sample rate must be less than input sample rate.");
360
+ }
361
+ const int16Array = new Int16Array(buffer);
362
+ const float32Array = new Float32Array(int16Array.length);
363
+ for (let i = 0;i < int16Array.length; i++) {
364
+ float32Array[i] = int16Array[i] / 32768;
365
+ }
366
+ const downsampled = downSampleAudioBuffer(float32Array, inputSampleRate, outputSampleRate);
367
+ const downsampledInt16 = new Int16Array(downsampled.length);
368
+ for (let i = 0;i < downsampled.length; i++) {
369
+ downsampledInt16[i] = Math.max(-32768, Math.min(32767, Math.floor(downsampled[i] * 32768)));
370
+ }
371
+ return downsampledInt16.buffer;
372
+ }
373
+
374
+ // ../liveapi/src/audioworklet-registry.ts
375
+ var registeredWorklets, createWorketFromSrc = (workletName, workletSrc) => {
376
+ const script = new Blob([`registerProcessor("${workletName}", ${workletSrc})`], {
377
+ type: "application/javascript"
378
+ });
379
+ return URL.createObjectURL(script);
380
+ };
381
+ var init_audioworklet_registry = __esm(() => {
382
+ registeredWorklets = new Map;
383
+ });
384
+
385
+ // ../liveapi/src/audio-recorder.ts
386
+ import EventEmitter from "eventemitter3";
387
+ var AudioRecorder;
388
+ var init_audio_recorder = __esm(() => {
389
+ init_utils();
390
+ init_audio_processing();
391
+ init_vol_meter();
392
+ init_audioworklet_registry();
393
+ AudioRecorder = class AudioRecorder extends EventEmitter.EventEmitter {
394
+ sampleRate;
395
+ stream;
396
+ audioContext;
397
+ source;
398
+ recording = false;
399
+ recordingWorklet;
400
+ vuWorklet;
401
+ starting = null;
402
+ constructor(sampleRate = 16000) {
403
+ super();
404
+ this.sampleRate = sampleRate;
405
+ }
406
+ async start() {
407
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
408
+ throw new Error("Could not request user media");
409
+ }
410
+ this.starting = new Promise(async (resolve, reject) => {
411
+ this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
412
+ this.audioContext = await audioContext({ sampleRate: this.sampleRate });
413
+ this.source = this.audioContext.createMediaStreamSource(this.stream);
414
+ const workletName = "audio-recorder-worklet";
415
+ const src = createWorketFromSrc(workletName, audio_processing_default);
416
+ await this.audioContext.audioWorklet.addModule(src);
417
+ this.recordingWorklet = new AudioWorkletNode(this.audioContext, workletName);
418
+ this.recordingWorklet.port.onmessage = async (ev) => {
419
+ const arrayBuffer = ev.data.data.int16arrayBuffer;
420
+ if (arrayBuffer) {
421
+ let outputBuffer = arrayBuffer;
422
+ if (this.sampleRate > 16000) {
423
+ outputBuffer = downSampleInt16Buffer(arrayBuffer, this.sampleRate, 16000);
424
+ }
425
+ this.emit("data", outputBuffer);
426
+ }
427
+ };
428
+ this.source.connect(this.recordingWorklet);
429
+ const vuWorkletName = "vu-meter";
430
+ await this.audioContext.audioWorklet.addModule(createWorketFromSrc(vuWorkletName, vol_meter_default));
431
+ this.vuWorklet = new AudioWorkletNode(this.audioContext, vuWorkletName);
432
+ this.vuWorklet.port.onmessage = (ev) => {
433
+ this.emit("volume", ev.data.volume);
434
+ };
435
+ this.source.connect(this.vuWorklet);
436
+ this.recording = true;
437
+ resolve();
438
+ this.starting = null;
439
+ });
440
+ }
441
+ stop() {
442
+ const handleStop = () => {
443
+ this.source?.disconnect();
444
+ this.stream?.getTracks?.()?.forEach((track) => track.stop());
445
+ this.stream = undefined;
446
+ this.recordingWorklet = undefined;
447
+ this.vuWorklet = undefined;
448
+ };
449
+ if (this.starting) {
450
+ this.starting.then(handleStop);
451
+ return;
452
+ }
453
+ handleStop();
454
+ }
455
+ };
456
+ });
457
+
458
+ // ../liveapi/src/audio-streamer.ts
459
+ class AudioStreamer {
460
+ context;
461
+ sampleRate = 24000;
462
+ bufferSize = 7680;
463
+ audioQueue = [];
464
+ isPlaying = false;
465
+ isStreamComplete = false;
466
+ checkInterval = null;
467
+ scheduledTime = 0;
468
+ initialBufferTime = 0.1;
469
+ gainNode;
470
+ source;
471
+ endOfQueueAudioSource = null;
472
+ onComplete = () => {};
473
+ constructor(context) {
474
+ this.context = context;
475
+ this.gainNode = this.context.createGain();
476
+ this.source = this.context.createBufferSource();
477
+ this.gainNode.connect(this.context.destination);
478
+ this.addPCM16 = this.addPCM16.bind(this);
479
+ }
480
+ async addWorklet(workletName, workletSrc, handler) {
481
+ let workletsRecord = registeredWorklets.get(this.context);
482
+ if (workletsRecord && workletsRecord[workletName]) {
483
+ workletsRecord[workletName].handlers.push(handler);
484
+ return Promise.resolve(this);
485
+ }
486
+ if (!workletsRecord) {
487
+ registeredWorklets.set(this.context, {});
488
+ workletsRecord = registeredWorklets.get(this.context);
489
+ }
490
+ workletsRecord[workletName] = { handlers: [handler] };
491
+ const src = createWorketFromSrc(workletName, workletSrc);
492
+ await this.context.audioWorklet.addModule(src);
493
+ const worklet = new AudioWorkletNode(this.context, workletName);
494
+ workletsRecord[workletName].node = worklet;
495
+ return this;
496
+ }
497
+ _processPCM16Chunk(chunk) {
498
+ const float32Array = new Float32Array(chunk.length / 2);
499
+ const dataView = new DataView(chunk.buffer);
500
+ for (let i = 0;i < chunk.length / 2; i++) {
501
+ try {
502
+ const int16 = dataView.getInt16(i * 2, true);
503
+ float32Array[i] = int16 / 32768;
504
+ } catch (e) {
505
+ console.error(e);
506
+ }
507
+ }
508
+ return float32Array;
509
+ }
510
+ addPCM16(chunk) {
511
+ this.isStreamComplete = false;
512
+ let processingBuffer = this._processPCM16Chunk(chunk);
513
+ while (processingBuffer.length >= this.bufferSize) {
514
+ const buffer = processingBuffer.slice(0, this.bufferSize);
515
+ this.audioQueue.push(buffer);
516
+ processingBuffer = processingBuffer.slice(this.bufferSize);
517
+ }
518
+ if (processingBuffer.length > 0) {
519
+ this.audioQueue.push(processingBuffer);
520
+ }
521
+ if (!this.isPlaying) {
522
+ this.isPlaying = true;
523
+ this.scheduledTime = this.context.currentTime + this.initialBufferTime;
524
+ this.scheduleNextBuffer();
525
+ }
526
+ }
527
+ createAudioBuffer(audioData) {
528
+ const audioBuffer = this.context.createBuffer(1, audioData.length, this.sampleRate);
529
+ audioBuffer.getChannelData(0).set(audioData);
530
+ return audioBuffer;
531
+ }
532
+ scheduleNextBuffer() {
533
+ const SCHEDULE_AHEAD_TIME = 0.2;
534
+ while (this.audioQueue.length > 0 && this.scheduledTime < this.context.currentTime + SCHEDULE_AHEAD_TIME) {
535
+ const audioData = this.audioQueue.shift();
536
+ const audioBuffer = this.createAudioBuffer(audioData);
537
+ const source = this.context.createBufferSource();
538
+ if (this.audioQueue.length === 0) {
539
+ if (this.endOfQueueAudioSource) {
540
+ this.endOfQueueAudioSource.onended = null;
541
+ }
542
+ this.endOfQueueAudioSource = source;
543
+ source.onended = () => {
544
+ if (!this.audioQueue.length && this.endOfQueueAudioSource === source) {
545
+ this.endOfQueueAudioSource = null;
546
+ this.onComplete();
547
+ }
548
+ };
549
+ }
550
+ source.buffer = audioBuffer;
551
+ source.connect(this.gainNode);
552
+ const worklets = registeredWorklets.get(this.context);
553
+ if (worklets) {
554
+ Object.entries(worklets).forEach(([workletName, graph]) => {
555
+ const { node, handlers } = graph;
556
+ if (node) {
557
+ source.connect(node);
558
+ node.port.onmessage = function(ev) {
559
+ handlers.forEach((handler) => {
560
+ handler.call(node.port, ev);
561
+ });
562
+ };
563
+ node.connect(this.context.destination);
564
+ }
565
+ });
566
+ }
567
+ const startTime = Math.max(this.scheduledTime, this.context.currentTime);
568
+ source.start(startTime);
569
+ this.scheduledTime = startTime + audioBuffer.duration;
570
+ }
571
+ if (this.audioQueue.length === 0) {
572
+ if (this.isStreamComplete) {
573
+ this.isPlaying = false;
574
+ if (this.checkInterval) {
575
+ clearInterval(this.checkInterval);
576
+ this.checkInterval = null;
577
+ }
578
+ } else {
579
+ if (!this.checkInterval) {
580
+ this.checkInterval = setInterval(() => {
581
+ if (this.audioQueue.length > 0) {
582
+ this.scheduleNextBuffer();
583
+ }
584
+ }, 100);
585
+ }
586
+ }
587
+ } else {
588
+ const nextCheckTime = (this.scheduledTime - this.context.currentTime) * 1000;
589
+ setTimeout(() => this.scheduleNextBuffer(), Math.max(0, nextCheckTime - 50));
590
+ }
591
+ }
592
+ stop() {
593
+ this.isPlaying = false;
594
+ this.isStreamComplete = true;
595
+ this.audioQueue = [];
596
+ this.scheduledTime = this.context.currentTime;
597
+ if (this.checkInterval) {
598
+ clearInterval(this.checkInterval);
599
+ this.checkInterval = null;
600
+ }
601
+ this.gainNode.gain.linearRampToValueAtTime(0, this.context.currentTime + 0.1);
602
+ setTimeout(() => {
603
+ this.gainNode.disconnect();
604
+ this.gainNode = this.context.createGain();
605
+ this.gainNode.connect(this.context.destination);
606
+ }, 200);
607
+ }
608
+ async resume() {
609
+ if (this.context.state === "suspended") {
610
+ await this.context.resume();
611
+ }
612
+ this.isStreamComplete = false;
613
+ this.scheduledTime = this.context.currentTime + this.initialBufferTime;
614
+ this.gainNode.gain.setValueAtTime(1, this.context.currentTime);
615
+ }
616
+ complete() {
617
+ this.isStreamComplete = true;
618
+ this.onComplete();
619
+ }
620
+ }
621
+ var init_audio_streamer = __esm(() => {
622
+ init_audioworklet_registry();
623
+ });
624
+
625
+ // ../liveapi/src/live-api-client.ts
626
+ import {
627
+ GoogleGenAI,
628
+ MediaResolution
629
+ } from "@google/genai";
630
+
631
+ class LiveAPIClient {
632
+ client;
633
+ session = null;
634
+ audioStreamer = null;
635
+ audioRecorder = null;
636
+ model;
637
+ onStateChange;
638
+ onMessage;
639
+ state = {
640
+ connected: false,
641
+ muted: false,
642
+ inVolume: 0,
643
+ outVolume: 0,
644
+ logs: [],
645
+ config: {
646
+ inputAudioTranscription: {},
647
+ outputAudioTranscription: {},
648
+ mediaResolution: MediaResolution.MEDIA_RESOLUTION_MEDIUM,
649
+ contextWindowCompression: {
650
+ triggerTokens: "25600",
651
+ slidingWindow: { targetTokens: "12800" }
652
+ }
653
+ },
654
+ isAssistantSpeaking: false
655
+ };
656
+ onUserAudioChunk;
657
+ tools = [];
658
+ sessionHandle = null;
659
+ isExplicitDisconnect = false;
660
+ reconnectAttempts = 0;
661
+ maxReconnectAttempts;
662
+ reconnectTimeout = null;
663
+ autoReconnect;
664
+ autoMuteOnAssistantSpeaking;
665
+ userMutedState = false;
666
+ constructor(options) {
667
+ const {
668
+ model,
669
+ onStateChange,
670
+ onMessage,
671
+ onUserAudioChunk,
672
+ apiKey,
673
+ enableGoogleSearch,
674
+ recordingSampleRate = 16000,
675
+ config,
676
+ autoReconnect = true,
677
+ maxReconnectAttempts = 5,
678
+ autoMuteOnAssistantSpeaking = true
679
+ } = options;
680
+ if (!apiKey) {
681
+ throw new Error("API key is required");
682
+ }
683
+ this.client = new GoogleGenAI({ apiKey });
684
+ this.model = model || "models/gemini-2.0-flash-exp";
685
+ this.onStateChange = onStateChange;
686
+ this.onMessage = onMessage;
687
+ this.onUserAudioChunk = onUserAudioChunk;
688
+ this.autoReconnect = autoReconnect;
689
+ this.maxReconnectAttempts = maxReconnectAttempts;
690
+ this.autoMuteOnAssistantSpeaking = autoMuteOnAssistantSpeaking;
691
+ this.tools = config?.tools || [];
692
+ if (enableGoogleSearch) {
693
+ this.tools.push({ googleSearch: {} });
694
+ }
695
+ if (config) {
696
+ this.state.config = { ...this.state.config, ...config };
697
+ }
698
+ this.audioRecorder = new AudioRecorder(recordingSampleRate);
699
+ this.setupAudioRecorder();
700
+ }
701
+ updateState(updates) {
702
+ this.state = { ...this.state, ...updates };
703
+ this.onStateChange?.(this.state);
704
+ }
705
+ getState() {
706
+ return { ...this.state };
707
+ }
708
+ async initAudioStreamer() {
709
+ if (!this.audioStreamer) {
710
+ const audioCtx = await audioContext({ id: "audio-out" });
711
+ this.audioStreamer = new AudioStreamer(audioCtx);
712
+ await this.audioStreamer.addWorklet("vumeter-out", vol_meter_default, (ev) => {
713
+ this.updateState({ outVolume: ev.data.volume });
714
+ });
715
+ this.audioStreamer.onComplete = () => {
716
+ this.onAssistantStopSpeaking();
717
+ };
718
+ }
719
+ }
720
+ setupAudioRecorder() {
721
+ if (!this.audioRecorder)
722
+ return;
723
+ this.audioRecorder.on("data", (arrayBuffer) => {
724
+ if (this.state.connected && !this.state.muted) {
725
+ const binary = String.fromCharCode(...new Uint8Array(arrayBuffer));
726
+ const base64 = btoa(binary);
727
+ this.sendRealtimeInput([
728
+ {
729
+ mimeType: "audio/pcm;rate=16000",
730
+ data: base64
731
+ }
732
+ ]);
733
+ if (this.onUserAudioChunk) {
734
+ this.onUserAudioChunk(arrayBuffer.slice(0));
735
+ }
736
+ }
737
+ });
738
+ this.audioRecorder.on("volume", (volume) => {
739
+ this.updateState({ inVolume: volume });
740
+ });
741
+ }
742
+ log(message) {
743
+ console.log(message);
744
+ const newEvents = [...this.state.logs, message];
745
+ if (newEvents.length > 200) {
746
+ this.updateState({ logs: newEvents.slice(-150) });
747
+ } else {
748
+ this.updateState({ logs: newEvents });
749
+ }
750
+ }
751
+ async connect() {
752
+ if (this.state.connected) {
753
+ return false;
754
+ }
755
+ this.isExplicitDisconnect = false;
756
+ this.reconnectAttempts = 0;
757
+ if (this.reconnectTimeout) {
758
+ clearTimeout(this.reconnectTimeout);
759
+ this.reconnectTimeout = null;
760
+ }
761
+ await this.initAudioStreamer();
762
+ const callbacks = {
763
+ onopen: this.onOpen.bind(this),
764
+ onmessage: this.handleMessage.bind(this),
765
+ onerror: this.onError.bind(this),
766
+ onclose: this.onClose.bind(this)
767
+ };
768
+ const connectConfig = { ...this.state.config };
769
+ if (this.sessionHandle && connectConfig.sessionResumption) {
770
+ connectConfig.sessionResumption = {
771
+ ...connectConfig.sessionResumption,
772
+ handle: this.sessionHandle
773
+ };
774
+ this.log("connect: Attempting to resume session with handle");
775
+ }
776
+ try {
777
+ this.session = await this.client.live.connect({
778
+ model: this.model,
779
+ config: connectConfig,
780
+ callbacks
781
+ });
782
+ } catch (e) {
783
+ console.error("Error connecting to GenAI Live:", e);
784
+ this.log("error: " + JSON.stringify({ message: "Failed to connect", error: e }));
785
+ return false;
786
+ }
787
+ return true;
788
+ }
789
+ disconnect() {
790
+ if (!this.session) {
791
+ return false;
792
+ }
793
+ this.isExplicitDisconnect = true;
794
+ if (this.reconnectTimeout) {
795
+ clearTimeout(this.reconnectTimeout);
796
+ this.reconnectTimeout = null;
797
+ }
798
+ this.session?.close();
799
+ this.session = null;
800
+ this.setConnected(false);
801
+ this.audioRecorder?.stop();
802
+ this.audioStreamer?.stop();
803
+ this.onAssistantStopSpeaking();
804
+ this.sessionHandle = null;
805
+ this.reconnectAttempts = 0;
806
+ this.log("close: " + JSON.stringify({ reason: "User disconnected" }));
807
+ return true;
808
+ }
809
+ setConnected(value) {
810
+ this.updateState({ connected: value });
811
+ if (value && !this.state.muted) {
812
+ this.audioRecorder?.start();
813
+ } else {
814
+ this.audioRecorder?.stop();
815
+ }
816
+ }
817
+ onOpen() {
818
+ this.setConnected(true);
819
+ this.log("open");
820
+ }
821
+ onError(e) {
822
+ this.log("error: " + JSON.stringify({ message: e.message, error: e }));
823
+ }
824
+ async onClose(e) {
825
+ this.setConnected(false);
826
+ this.log("close: " + JSON.stringify({ reason: e.reason, code: e.code }));
827
+ if (this.autoReconnect && !this.isExplicitDisconnect && this.sessionHandle) {
828
+ await this.attemptReconnect();
829
+ }
830
+ }
831
+ async attemptReconnect() {
832
+ console.log(`attempting to reconnect`);
833
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
834
+ this.log("error: Max reconnection attempts reached, giving up");
835
+ this.sessionHandle = null;
836
+ this.reconnectAttempts = 0;
837
+ return;
838
+ }
839
+ this.reconnectAttempts++;
840
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 1e4);
841
+ this.log(`reconnect: Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`);
842
+ this.reconnectTimeout = setTimeout(async () => {
843
+ this.reconnectTimeout = null;
844
+ try {
845
+ const success = await this.connect();
846
+ if (success) {
847
+ this.log("reconnect: Successfully reconnected to session");
848
+ this.reconnectAttempts = 0;
849
+ } else {
850
+ this.log("reconnect: Failed to reconnect, will retry");
851
+ }
852
+ } catch (error) {
853
+ this.log("reconnect: Error during reconnection: " + JSON.stringify(error));
854
+ }
855
+ }, delay);
856
+ }
857
+ async handleMessage(message) {
858
+ const clonedMessage = JSON.parse(JSON.stringify(message));
859
+ this.onMessage?.(clonedMessage);
860
+ if (message.setupComplete) {
861
+ this.log("setupcomplete");
862
+ return;
863
+ }
864
+ if (message.sessionResumptionUpdate) {
865
+ const update = message.sessionResumptionUpdate;
866
+ if (update.resumable && update.newHandle) {
867
+ this.sessionHandle = update.newHandle;
868
+ this.log("sessionResumption: Updated handle for resumption " + update.newHandle);
869
+ } else if (!update.resumable) {
870
+ this.sessionHandle = null;
871
+ this.log("sessionResumption: Session not resumable at this point");
872
+ }
873
+ return;
874
+ }
875
+ if (message.goAway) {
876
+ this.log("goAway: Server will disconnect, preparing for reconnection");
877
+ return;
878
+ }
879
+ if (message.toolCall) {
880
+ this.log("toolcall: " + JSON.stringify(message.toolCall));
881
+ if (message.toolCall.functionCalls) {
882
+ await this.handleToolCalls(message.toolCall.functionCalls);
883
+ }
884
+ return;
885
+ }
886
+ if (message.toolCallCancellation) {
887
+ this.log("toolcallcancellation: " + JSON.stringify(message.toolCallCancellation));
888
+ return;
889
+ }
890
+ if (message.serverContent) {
891
+ const { serverContent } = message;
892
+ if (serverContent.interrupted) {
893
+ this.log("interrupted");
894
+ this.audioStreamer?.stop();
895
+ this.onAssistantStopSpeaking();
896
+ return;
897
+ }
898
+ if (serverContent.turnComplete) {
899
+ this.log("turncomplete");
900
+ }
901
+ if (serverContent.modelTurn) {
902
+ let parts = serverContent.modelTurn?.parts || [];
903
+ const [audioParts, otherParts] = partition(parts, (p) => p.inlineData && p.inlineData.mimeType?.startsWith("audio/pcm"));
904
+ const base64s = audioParts.map((p) => p.inlineData?.data);
905
+ base64s.forEach((b64, index) => {
906
+ if (b64) {
907
+ if (index === 0 && !this.state.isAssistantSpeaking) {
908
+ this.onAssistantStartSpeaking();
909
+ }
910
+ const data = base64ToArrayBuffer(b64);
911
+ this.audioStreamer?.addPCM16(new Uint8Array(data));
912
+ }
913
+ });
914
+ if (otherParts.length) {
915
+ this.log("content: " + JSON.stringify({ modelTurn: { parts: otherParts } }));
916
+ otherParts.forEach((part) => {
917
+ if (part.executableCode && part.executableCode.language === "PYTHON" && part.executableCode.code) {
918
+ const preview = part.executableCode.code;
919
+ this.log(`python executableCode:
920
+ ` + preview);
921
+ }
922
+ });
923
+ }
924
+ }
925
+ }
926
+ }
927
+ setMuted(muted) {
928
+ this.userMutedState = muted;
929
+ this.updateState({ muted });
930
+ if (this.state.connected) {
931
+ if (muted) {
932
+ this.audioRecorder?.stop();
933
+ } else {
934
+ this.audioRecorder?.start();
935
+ }
936
+ }
937
+ }
938
+ onAssistantStartSpeaking() {
939
+ this.updateState({ isAssistantSpeaking: true });
940
+ if (this.autoMuteOnAssistantSpeaking && !this.userMutedState && !this.state.muted) {
941
+ this.updateState({ muted: true });
942
+ if (this.state.connected) {
943
+ this.audioRecorder?.stop();
944
+ }
945
+ }
946
+ }
947
+ onAssistantStopSpeaking() {
948
+ this.updateState({ isAssistantSpeaking: false });
949
+ if (this.autoMuteOnAssistantSpeaking && !this.userMutedState && this.state.muted) {
950
+ this.updateState({ muted: false });
951
+ if (this.state.connected) {
952
+ this.audioRecorder?.start();
953
+ }
954
+ }
955
+ }
956
+ sendText(text, turnComplete = true) {
957
+ if (!this.session)
958
+ return;
959
+ const parts = [{ text }];
960
+ this.session.sendClientContent({ turns: parts, turnComplete });
961
+ this.log("client-send: " + JSON.stringify({ turns: parts, turnComplete }));
962
+ }
963
+ sendRealtimeInput(chunks) {
964
+ if (!this.session)
965
+ return;
966
+ let hasAudio = false;
967
+ let hasVideo = false;
968
+ for (const ch of chunks) {
969
+ this.session.sendRealtimeInput({ media: ch });
970
+ if (ch.mimeType.includes("audio")) {
971
+ hasAudio = true;
972
+ }
973
+ if (ch.mimeType.includes("image")) {
974
+ hasVideo = true;
975
+ }
976
+ }
977
+ const mediaType = (() => {
978
+ if (hasAudio && hasVideo)
979
+ return "audio+video";
980
+ if (hasAudio)
981
+ return "audio";
982
+ if (hasVideo)
983
+ return "video";
984
+ return "unknown";
985
+ })();
986
+ }
987
+ async handleToolCalls(functionCalls) {
988
+ if (!this.session) {
989
+ console.log("No session active in handleToolCalls");
990
+ return;
991
+ }
992
+ if (!this.tools.length) {
993
+ console.log("No tools registered in handleToolCalls");
994
+ return;
995
+ }
996
+ try {
997
+ for (const tool2 of this.tools) {
998
+ if (!functionCalls.some((x) => x.name === tool2.name)) {
999
+ continue;
1000
+ }
1001
+ const parts = await tool2.callTool(functionCalls);
1002
+ const functionResponses = parts.filter((part) => part.functionResponse).map((part) => ({
1003
+ response: part.functionResponse.response,
1004
+ id: part.functionResponse.id,
1005
+ name: part.functionResponse.name
1006
+ }));
1007
+ if (functionResponses.length > 0) {
1008
+ this.session.sendToolResponse({ functionResponses });
1009
+ this.log("client-toolResponse: " + JSON.stringify({ functionResponses }));
1010
+ } else {
1011
+ console.log(`no tool call response!`);
1012
+ }
1013
+ }
1014
+ } catch (error) {
1015
+ console.error("Error handling tool calls:", error);
1016
+ this.log("error: " + JSON.stringify({ message: "Tool call failed", error }));
1017
+ }
1018
+ }
1019
+ sendToolResponse(response) {
1020
+ if (!this.session) {
1021
+ console.error("Cannot send tool response: session not connected");
1022
+ return;
1023
+ }
1024
+ this.session.sendToolResponse(response);
1025
+ this.log("client-toolResponse: " + JSON.stringify(response));
1026
+ }
1027
+ setConfig(config) {
1028
+ if (config.tools) {
1029
+ this.tools = config.tools;
1030
+ }
1031
+ this.updateState({ config });
1032
+ }
1033
+ getConfig() {
1034
+ return { ...this.state.config };
1035
+ }
1036
+ destroy() {
1037
+ console.log(`calling destroy()`);
1038
+ this.isExplicitDisconnect = true;
1039
+ if (this.reconnectTimeout) {
1040
+ clearTimeout(this.reconnectTimeout);
1041
+ this.reconnectTimeout = null;
1042
+ }
1043
+ this.disconnect();
1044
+ this.audioRecorder?.stop();
1045
+ this.audioStreamer?.stop();
1046
+ this.onAssistantStopSpeaking();
1047
+ this.tools = [];
1048
+ this.sessionHandle = null;
1049
+ this.reconnectAttempts = 0;
1050
+ }
1051
+ }
1052
+ function partition(arr, predicate) {
1053
+ const truthy = [];
1054
+ const falsy = [];
1055
+ arr.forEach((item, index) => {
1056
+ if (predicate(item, index, arr)) {
1057
+ truthy.push(item);
1058
+ } else {
1059
+ falsy.push(item);
1060
+ }
1061
+ });
1062
+ return [truthy, falsy];
1063
+ }
1064
+ var init_live_api_client = __esm(() => {
1065
+ init_audio_recorder();
1066
+ init_audio_streamer();
1067
+ init_utils();
1068
+ init_vol_meter();
1069
+ });
1070
+
1071
+ // ../liveapi/src/ai-tool-to-genai.ts
1072
+ import { Type } from "@google/genai";
1073
+ import { toJSONSchema } from "zod";
1074
+ function jsonSchemaToGenAISchema(jsonSchema) {
1075
+ const schema = {};
1076
+ if (jsonSchema.type) {
1077
+ switch (jsonSchema.type) {
1078
+ case "string":
1079
+ schema.type = Type.STRING;
1080
+ break;
1081
+ case "number":
1082
+ schema.type = Type.NUMBER;
1083
+ schema.format = jsonSchema.format || "float";
1084
+ break;
1085
+ case "integer":
1086
+ schema.type = Type.INTEGER;
1087
+ schema.format = jsonSchema.format || "int32";
1088
+ break;
1089
+ case "boolean":
1090
+ schema.type = Type.BOOLEAN;
1091
+ break;
1092
+ case "array":
1093
+ schema.type = Type.ARRAY;
1094
+ if (jsonSchema.items) {
1095
+ schema.items = jsonSchemaToGenAISchema(jsonSchema.items);
1096
+ }
1097
+ if (jsonSchema.minItems !== undefined) {
1098
+ schema.minItems = jsonSchema.minItems;
1099
+ }
1100
+ if (jsonSchema.maxItems !== undefined) {
1101
+ schema.maxItems = jsonSchema.maxItems;
1102
+ }
1103
+ break;
1104
+ case "object":
1105
+ schema.type = Type.OBJECT;
1106
+ if (jsonSchema.properties) {
1107
+ schema.properties = {};
1108
+ for (const [key, value] of Object.entries(jsonSchema.properties)) {
1109
+ schema.properties[key] = jsonSchemaToGenAISchema(value);
1110
+ }
1111
+ }
1112
+ if (jsonSchema.required) {
1113
+ schema.required = jsonSchema.required;
1114
+ }
1115
+ break;
1116
+ default:
1117
+ schema.type = jsonSchema.type;
1118
+ }
1119
+ }
1120
+ if (jsonSchema.description) {
1121
+ schema.description = jsonSchema.description;
1122
+ }
1123
+ if (jsonSchema.enum) {
1124
+ schema.enum = jsonSchema.enum.map(String);
1125
+ }
1126
+ if (jsonSchema.default !== undefined) {
1127
+ schema.default = jsonSchema.default;
1128
+ }
1129
+ if (jsonSchema.example !== undefined) {
1130
+ schema.example = jsonSchema.example;
1131
+ }
1132
+ if (jsonSchema.nullable) {
1133
+ schema.nullable = true;
1134
+ }
1135
+ if (jsonSchema.anyOf) {
1136
+ schema.anyOf = jsonSchema.anyOf.map((s) => jsonSchemaToGenAISchema(s));
1137
+ } else if (jsonSchema.oneOf) {
1138
+ schema.anyOf = jsonSchema.oneOf.map((s) => jsonSchemaToGenAISchema(s));
1139
+ }
1140
+ if (jsonSchema.minimum !== undefined) {
1141
+ schema.minimum = jsonSchema.minimum;
1142
+ }
1143
+ if (jsonSchema.maximum !== undefined) {
1144
+ schema.maximum = jsonSchema.maximum;
1145
+ }
1146
+ if (jsonSchema.minLength !== undefined) {
1147
+ schema.minLength = jsonSchema.minLength;
1148
+ }
1149
+ if (jsonSchema.maxLength !== undefined) {
1150
+ schema.maxLength = jsonSchema.maxLength;
1151
+ }
1152
+ if (jsonSchema.pattern) {
1153
+ schema.pattern = jsonSchema.pattern;
1154
+ }
1155
+ return schema;
1156
+ }
1157
+ function aiToolToGenAIFunction(tool2) {
1158
+ const inputSchema = tool2.inputSchema;
1159
+ let toolName = "tool";
1160
+ let jsonSchema = {};
1161
+ if (inputSchema) {
1162
+ jsonSchema = toJSONSchema(inputSchema);
1163
+ const description = inputSchema.description;
1164
+ if (description) {
1165
+ const nameMatch = description.match(/name:\s*(\w+)/);
1166
+ if (nameMatch) {
1167
+ toolName = nameMatch[1];
1168
+ }
1169
+ }
1170
+ }
1171
+ const genAISchema = jsonSchemaToGenAISchema(jsonSchema);
1172
+ const functionDeclaration = {
1173
+ name: toolName,
1174
+ description: tool2.description || jsonSchema.description || "Tool function",
1175
+ parameters: genAISchema
1176
+ };
1177
+ return functionDeclaration;
1178
+ }
1179
+ function aiToolToCallableTool(tool2, name) {
1180
+ const toolName = name || "tool";
1181
+ return {
1182
+ name,
1183
+ async tool() {
1184
+ const functionDeclaration = aiToolToGenAIFunction(tool2);
1185
+ if (name) {
1186
+ functionDeclaration.name = name;
1187
+ }
1188
+ return {
1189
+ functionDeclarations: [functionDeclaration]
1190
+ };
1191
+ },
1192
+ async callTool(functionCalls) {
1193
+ const parts = [];
1194
+ for (const functionCall of functionCalls) {
1195
+ if (functionCall.name !== toolName && name && functionCall.name !== name) {
1196
+ continue;
1197
+ }
1198
+ if (tool2.execute) {
1199
+ try {
1200
+ const result = await tool2.execute(functionCall.args || {}, {
1201
+ toolCallId: functionCall.id || "",
1202
+ messages: []
1203
+ });
1204
+ parts.push({
1205
+ functionResponse: {
1206
+ id: functionCall.id,
1207
+ name: functionCall.name || toolName,
1208
+ response: {
1209
+ output: result
1210
+ }
1211
+ }
1212
+ });
1213
+ } catch (error) {
1214
+ parts.push({
1215
+ functionResponse: {
1216
+ id: functionCall.id,
1217
+ name: functionCall.name || toolName,
1218
+ response: {
1219
+ error: error instanceof Error ? error.message : String(error)
1220
+ }
1221
+ }
1222
+ });
1223
+ }
1224
+ }
1225
+ }
1226
+ return parts;
1227
+ }
1228
+ };
1229
+ }
1230
+ function extractSchemaFromTool(tool2) {
1231
+ const inputSchema = tool2.inputSchema;
1232
+ if (!inputSchema) {
1233
+ return {};
1234
+ }
1235
+ return toJSONSchema(inputSchema);
1236
+ }
1237
+ function callableToolsFromObject(tools) {
1238
+ return Object.entries(tools).map(([name, tool2]) => aiToolToCallableTool(tool2, name));
1239
+ }
1240
+ var init_ai_tool_to_genai = () => {};
1241
+
1242
+ // ../liveapi/src/genai-to-ui-message.ts
1243
+ import { createIdGenerator } from "ai";
1244
+
1245
+ class LiveMessageAssembler {
1246
+ currentUserParts = [];
1247
+ currentAssistantParts = [];
1248
+ allMessages = [];
1249
+ idGenerator;
1250
+ constructor(options = {}) {
1251
+ this.idGenerator = options.idGenerator ?? createIdGenerator({
1252
+ prefix: "msg",
1253
+ size: 24
1254
+ });
1255
+ }
1256
+ processMessage(message) {
1257
+ const updates = this.processServerMessage(message);
1258
+ const newMessages = this.addParts(updates);
1259
+ if (newMessages.length > 0) {
1260
+ this.allMessages.push(...newMessages);
1261
+ }
1262
+ if (message.serverContent?.interrupted) {
1263
+ const flushedMessages = this.flushPending();
1264
+ this.allMessages.push(...flushedMessages);
1265
+ }
1266
+ const allMessages = [...this.allMessages];
1267
+ if (this.currentUserParts.length > 0) {
1268
+ allMessages.push(this.createMessage("user", this.currentUserParts));
1269
+ }
1270
+ if (this.currentAssistantParts.length > 0) {
1271
+ allMessages.push(this.createMessage("assistant", this.currentAssistantParts));
1272
+ }
1273
+ return allMessages.map(mergeConsecutiveTextParts);
1274
+ }
1275
+ clear() {
1276
+ this.currentUserParts = [];
1277
+ this.currentAssistantParts = [];
1278
+ this.allMessages = [];
1279
+ }
1280
+ getAllMessages() {
1281
+ return [...this.allMessages];
1282
+ }
1283
+ processServerMessage(message) {
1284
+ const updates = [];
1285
+ if (message.serverContent?.modelTurn) {
1286
+ const parts = this.extractPartsFromContent(message.serverContent.modelTurn);
1287
+ for (const part of parts) {
1288
+ updates.push({
1289
+ part,
1290
+ role: "assistant",
1291
+ isFinal: false
1292
+ });
1293
+ }
1294
+ if (message.serverContent.turnComplete) {
1295
+ updates.push({
1296
+ part: { type: "text", text: "" },
1297
+ role: "assistant",
1298
+ isFinal: true
1299
+ });
1300
+ }
1301
+ }
1302
+ if (message.serverContent?.inputTranscription?.text) {
1303
+ updates.push({
1304
+ part: {
1305
+ type: "text",
1306
+ text: message.serverContent.inputTranscription.text
1307
+ },
1308
+ role: "user",
1309
+ isFinal: message.serverContent.inputTranscription.finished || false
1310
+ });
1311
+ }
1312
+ if (message.serverContent?.outputTranscription?.text) {
1313
+ updates.push({
1314
+ part: {
1315
+ type: "text",
1316
+ text: message.serverContent.outputTranscription.text
1317
+ },
1318
+ role: "assistant",
1319
+ isFinal: message.serverContent.outputTranscription.finished || false
1320
+ });
1321
+ }
1322
+ if (message.toolCall?.functionCalls) {
1323
+ for (const functionCall of message.toolCall.functionCalls) {
1324
+ updates.push({
1325
+ part: this.functionCallToToolPart(functionCall),
1326
+ role: "assistant",
1327
+ isFinal: false
1328
+ });
1329
+ }
1330
+ }
1331
+ return updates;
1332
+ }
1333
+ processClientMessage(message) {
1334
+ const updates = [];
1335
+ if (message.clientContent?.turns) {
1336
+ for (const turn of message.clientContent.turns) {
1337
+ const role = this.determineRole(turn.role);
1338
+ const parts = this.extractPartsFromContent(turn);
1339
+ for (const part of parts) {
1340
+ updates.push({
1341
+ part,
1342
+ role,
1343
+ isFinal: false
1344
+ });
1345
+ }
1346
+ }
1347
+ if (message.clientContent.turnComplete) {
1348
+ const lastTurn = message.clientContent.turns[message.clientContent.turns.length - 1];
1349
+ const role = this.determineRole(lastTurn?.role);
1350
+ updates.push({
1351
+ part: { type: "text", text: "" },
1352
+ role,
1353
+ isFinal: true
1354
+ });
1355
+ }
1356
+ }
1357
+ if (message.realtimeInput) {
1358
+ const parts = this.processRealtimeInput(message.realtimeInput);
1359
+ const isActivityEnd = !!message.realtimeInput.activityEnd;
1360
+ for (const part of parts) {
1361
+ updates.push({
1362
+ part,
1363
+ role: "user",
1364
+ isFinal: isActivityEnd
1365
+ });
1366
+ }
1367
+ }
1368
+ if (message.toolResponse?.functionResponses) {
1369
+ for (const response of message.toolResponse.functionResponses) {
1370
+ updates.push({
1371
+ part: this.functionResponseToToolPart(response),
1372
+ role: "user",
1373
+ isFinal: false
1374
+ });
1375
+ }
1376
+ }
1377
+ return updates;
1378
+ }
1379
+ addParts(updates) {
1380
+ const completedMessages = [];
1381
+ for (const update of updates) {
1382
+ if (update.part.type === "text" && update.part.text === "" && update.isFinal) {
1383
+ if (update.role === "user" && this.currentUserParts.length > 0) {
1384
+ completedMessages.push(this.createMessage("user", this.currentUserParts));
1385
+ this.currentUserParts = [];
1386
+ } else if (update.role === "assistant" && this.currentAssistantParts.length > 0) {
1387
+ completedMessages.push(this.createMessage("assistant", this.currentAssistantParts));
1388
+ this.currentAssistantParts = [];
1389
+ }
1390
+ } else {
1391
+ if (update.role === "user") {
1392
+ this.currentUserParts.push(update.part);
1393
+ } else if (update.role === "assistant") {
1394
+ this.currentAssistantParts.push(update.part);
1395
+ }
1396
+ if (update.isFinal) {
1397
+ if (update.role === "user" && this.currentUserParts.length > 0) {
1398
+ completedMessages.push(this.createMessage("user", this.currentUserParts));
1399
+ this.currentUserParts = [];
1400
+ } else if (update.role === "assistant" && this.currentAssistantParts.length > 0) {
1401
+ completedMessages.push(this.createMessage("assistant", this.currentAssistantParts));
1402
+ this.currentAssistantParts = [];
1403
+ }
1404
+ }
1405
+ }
1406
+ }
1407
+ return completedMessages;
1408
+ }
1409
+ getCurrentParts(role) {
1410
+ return role === "user" ? [...this.currentUserParts] : [...this.currentAssistantParts];
1411
+ }
1412
+ flushPending() {
1413
+ const messages = [];
1414
+ if (this.currentUserParts.length > 0) {
1415
+ messages.push(this.createMessage("user", this.currentUserParts));
1416
+ this.currentUserParts = [];
1417
+ }
1418
+ if (this.currentAssistantParts.length > 0) {
1419
+ messages.push(this.createMessage("assistant", this.currentAssistantParts));
1420
+ this.currentAssistantParts = [];
1421
+ }
1422
+ return messages;
1423
+ }
1424
+ flush() {
1425
+ const messages = this.flushPending();
1426
+ this.allMessages.push(...messages);
1427
+ return [...this.allMessages];
1428
+ }
1429
+ extractPartsFromContent(content) {
1430
+ if (!content.parts)
1431
+ return [];
1432
+ return content.parts.map((part) => this.convertPartToUIPart(part)).filter((p) => p !== null);
1433
+ }
1434
+ processRealtimeInput(input) {
1435
+ const parts = [];
1436
+ if (input.text) {
1437
+ parts.push({
1438
+ type: "text",
1439
+ text: input.text,
1440
+ state: input.activityEnd ? "done" : "streaming"
1441
+ });
1442
+ }
1443
+ if (input.mediaChunks) {
1444
+ for (const chunk of input.mediaChunks) {
1445
+ if (chunk && typeof chunk === "object" && chunk.mimeType) {
1446
+ if (chunk.mimeType?.startsWith("audio/") || chunk.mimeType?.startsWith("video/")) {
1447
+ continue;
1448
+ }
1449
+ parts.push({
1450
+ type: "data-url",
1451
+ data: {
1452
+ mimeType: chunk.mimeType || "application/octet-stream",
1453
+ url: `data:${chunk.mimeType};base64,${chunk.data}`
1454
+ }
1455
+ });
1456
+ }
1457
+ }
1458
+ }
1459
+ return parts;
1460
+ }
1461
+ convertPartToUIPart(part) {
1462
+ if (typeof part === "string") {
1463
+ return {
1464
+ type: "text",
1465
+ text: part
1466
+ };
1467
+ }
1468
+ if (part.text) {
1469
+ return {
1470
+ type: "text",
1471
+ text: part.text,
1472
+ providerMetadata: part.thought ? { thought: { value: true } } : undefined
1473
+ };
1474
+ }
1475
+ if (part.inlineData) {
1476
+ if (part.inlineData.mimeType?.startsWith("audio/") || part.inlineData.mimeType?.startsWith("video/")) {
1477
+ return null;
1478
+ }
1479
+ const mimeType = part.inlineData.mimeType || "application/octet-stream";
1480
+ return {
1481
+ type: "data-url",
1482
+ data: {
1483
+ mimeType,
1484
+ url: `data:${mimeType};base64,${part.inlineData.data}`
1485
+ }
1486
+ };
1487
+ }
1488
+ if (part.fileData) {
1489
+ return {
1490
+ type: "file",
1491
+ name: part.fileData.fileUri || "file",
1492
+ url: part.fileData.fileUri,
1493
+ mediaType: part.fileData.mimeType || "application/octet-stream"
1494
+ };
1495
+ }
1496
+ if (part.functionCall) {
1497
+ return this.functionCallToToolPart(part.functionCall);
1498
+ }
1499
+ if (part.functionResponse) {
1500
+ return this.functionResponseToToolPart(part.functionResponse);
1501
+ }
1502
+ if (part.codeExecutionResult) {
1503
+ return {
1504
+ type: "tool-result",
1505
+ toolCallId: this.idGenerator(),
1506
+ toolName: "executableCode",
1507
+ state: part.codeExecutionResult.outcome === "OUTCOME_OK" ? "output-available" : "output-error",
1508
+ input: {},
1509
+ output: part.codeExecutionResult.output || ""
1510
+ };
1511
+ }
1512
+ if (part.executableCode) {
1513
+ return {
1514
+ type: "tool-call",
1515
+ toolCallId: this.idGenerator(),
1516
+ toolName: "executableCode",
1517
+ state: "input-available",
1518
+ input: {
1519
+ language: part.executableCode.language || "unknown",
1520
+ code: part.executableCode.code || ""
1521
+ }
1522
+ };
1523
+ }
1524
+ return null;
1525
+ }
1526
+ functionCallToToolPart(functionCall) {
1527
+ return {
1528
+ type: "tool-call",
1529
+ toolCallId: functionCall.id || this.idGenerator(),
1530
+ toolName: functionCall.name,
1531
+ state: "input-available",
1532
+ input: functionCall.args || {}
1533
+ };
1534
+ }
1535
+ functionResponseToToolPart(response) {
1536
+ const responseData = response.response || {};
1537
+ const isError = responseData.error !== undefined;
1538
+ if (isError) {
1539
+ return {
1540
+ type: "tool-result",
1541
+ toolCallId: response.id || this.idGenerator(),
1542
+ toolName: response.name,
1543
+ state: "output-error",
1544
+ input: {},
1545
+ errorText: JSON.stringify(responseData.error)
1546
+ };
1547
+ }
1548
+ return {
1549
+ type: "tool-result",
1550
+ toolCallId: response.id || this.idGenerator(),
1551
+ toolName: response.name,
1552
+ state: "output-available",
1553
+ input: {},
1554
+ output: responseData.output || responseData
1555
+ };
1556
+ }
1557
+ createMessage(role, parts) {
1558
+ return {
1559
+ id: this.idGenerator(),
1560
+ role,
1561
+ parts: [...parts]
1562
+ };
1563
+ }
1564
+ determineRole(role) {
1565
+ if (!role)
1566
+ return "user";
1567
+ if (role === "model")
1568
+ return "assistant";
1569
+ if (role === "system" || role === "user" || role === "assistant")
1570
+ return role;
1571
+ return "user";
1572
+ }
1573
+ }
1574
+ function mergeConsecutiveTextParts(message) {
1575
+ const mergedParts = message.parts.reduce((acc, part) => {
1576
+ const lastPart = acc[acc.length - 1];
1577
+ if (part.type === "text" && lastPart?.type === "text") {
1578
+ const mergedTextPart = {
1579
+ type: "text",
1580
+ text: lastPart.text + part.text
1581
+ };
1582
+ const currentTextPart = part;
1583
+ if (currentTextPart.state) {
1584
+ mergedTextPart.state = currentTextPart.state;
1585
+ } else if (lastPart.state) {
1586
+ mergedTextPart.state = lastPart.state;
1587
+ }
1588
+ const lastTextPart = lastPart;
1589
+ if (lastTextPart.providerMetadata || currentTextPart.providerMetadata) {
1590
+ mergedTextPart.providerMetadata = {
1591
+ ...lastTextPart.providerMetadata,
1592
+ ...currentTextPart.providerMetadata
1593
+ };
1594
+ }
1595
+ acc[acc.length - 1] = mergedTextPart;
1596
+ } else {
1597
+ acc.push(part);
1598
+ }
1599
+ return acc;
1600
+ }, []);
1601
+ return {
1602
+ ...message,
1603
+ parts: mergedParts
1604
+ };
1605
+ }
1606
+ function uiMessageToClientMessage(message, turnComplete = true) {
1607
+ const toolResponses = message.parts.filter((part) => part.type === "tool-result").map((part) => {
1608
+ const toolPart = part;
1609
+ return {
1610
+ id: toolPart.toolCallId,
1611
+ name: "",
1612
+ response: toolPart.state === "output-error" ? { error: toolPart.errorText } : toolPart.output || {}
1613
+ };
1614
+ });
1615
+ if (toolResponses.length > 0) {
1616
+ return {
1617
+ toolResponse: {
1618
+ functionResponses: toolResponses
1619
+ }
1620
+ };
1621
+ }
1622
+ const parts = message.parts.map((part) => uiPartToGenAIPart(part)).filter((p) => p !== null);
1623
+ return {
1624
+ clientContent: {
1625
+ turns: [
1626
+ {
1627
+ role: message.role,
1628
+ parts
1629
+ }
1630
+ ],
1631
+ turnComplete
1632
+ }
1633
+ };
1634
+ }
1635
+ function uiPartToGenAIPart(part) {
1636
+ switch (part.type) {
1637
+ case "text":
1638
+ return {
1639
+ text: part.text
1640
+ };
1641
+ case "file":
1642
+ const filePart = part;
1643
+ return {
1644
+ fileData: {
1645
+ fileUri: filePart.url,
1646
+ mimeType: filePart.mediaType
1647
+ }
1648
+ };
1649
+ case "tool-call":
1650
+ const toolCall = part;
1651
+ return {
1652
+ functionCall: {
1653
+ id: toolCall.toolCallId,
1654
+ name: "",
1655
+ args: toolCall.input
1656
+ }
1657
+ };
1658
+ default:
1659
+ if (part.type.startsWith("data-")) {
1660
+ const dataPart = part;
1661
+ if (dataPart.data && typeof dataPart.data === "object" && "url" in dataPart.data && dataPart.data.url) {
1662
+ const dataUrl = dataPart.data.url;
1663
+ const base64Match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
1664
+ if (base64Match) {
1665
+ const mimeType = base64Match[1] || "application/octet-stream";
1666
+ return {
1667
+ inlineData: {
1668
+ mimeType,
1669
+ data: base64Match[2]
1670
+ }
1671
+ };
1672
+ }
1673
+ }
1674
+ }
1675
+ return null;
1676
+ }
1677
+ }
1678
+ var init_genai_to_ui_message = () => {};
1679
+
1680
+ // ../liveapi/src/api.ts
1681
+ import { GoogleGenAI as GoogleGenAI2, Modality } from "@google/genai";
1682
+ function createAudioChatAPI(options) {
1683
+ const { geminiApiKey, tools, onRequest } = options;
1684
+ const ai = new GoogleGenAI2({
1685
+ apiKey: geminiApiKey,
1686
+ apiVersion: "v1alpha"
1687
+ });
1688
+ return async function handler(request) {
1689
+ try {
1690
+ if (onRequest) {
1691
+ await onRequest({ request });
1692
+ }
1693
+ const method = request.method.toUpperCase();
1694
+ if (method === "GET") {
1695
+ const url = new URL(request.url);
1696
+ const model = url.searchParams.get("model") || "gemini-2.0-flash-live-001";
1697
+ let token;
1698
+ try {
1699
+ const authToken = await ai.authTokens.create({
1700
+ config: {
1701
+ uses: 1,
1702
+ expireTime: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
1703
+ newSessionExpireTime: new Date(Date.now() + 60 * 1000).toISOString(),
1704
+ liveConnectConstraints: {
1705
+ model,
1706
+ config: {
1707
+ responseModalities: [Modality.AUDIO, Modality.TEXT],
1708
+ sessionResumption: {},
1709
+ temperature: 0.7
1710
+ }
1711
+ }
1712
+ }
1713
+ });
1714
+ token = authToken.name || "";
1715
+ } catch (error) {
1716
+ console.error("Error generating token:", error);
1717
+ return new Response(JSON.stringify({ error: "Failed to generate token" }), {
1718
+ status: 500,
1719
+ headers: {
1720
+ "Content-Type": "application/json"
1721
+ }
1722
+ });
1723
+ }
1724
+ const toolDefinitions = Object.entries(tools).map(([name, tool2]) => ({
1725
+ name,
1726
+ description: tool2.description || `Tool: ${name}`,
1727
+ inputSchema: extractSchemaFromTool(tool2)
1728
+ }));
1729
+ const response = {
1730
+ token,
1731
+ tools: toolDefinitions
1732
+ };
1733
+ return new Response(JSON.stringify(response), {
1734
+ status: 200,
1735
+ headers: {
1736
+ "Content-Type": "application/json"
1737
+ }
1738
+ });
1739
+ }
1740
+ if (method === "POST") {
1741
+ const body = await request.json();
1742
+ const toolRequest = body;
1743
+ const { tool: toolName, input } = toolRequest;
1744
+ if (!toolName) {
1745
+ return new Response(JSON.stringify({ error: "Missing tool name" }), {
1746
+ status: 400,
1747
+ headers: {
1748
+ "Content-Type": "application/json"
1749
+ }
1750
+ });
1751
+ }
1752
+ const tool2 = tools[toolName];
1753
+ if (!tool2) {
1754
+ return new Response(JSON.stringify({ error: `Tool not found: ${toolName}` }), {
1755
+ status: 404,
1756
+ headers: {
1757
+ "Content-Type": "application/json"
1758
+ }
1759
+ });
1760
+ }
1761
+ if (!tool2.execute) {
1762
+ return new Response(JSON.stringify({
1763
+ error: `Tool ${toolName} has no execute function`
1764
+ }), {
1765
+ status: 400,
1766
+ headers: {
1767
+ "Content-Type": "application/json"
1768
+ }
1769
+ });
1770
+ }
1771
+ try {
1772
+ const result = await tool2.execute(input || {}, {
1773
+ toolCallId: crypto.randomUUID(),
1774
+ messages: []
1775
+ });
1776
+ return new Response(JSON.stringify(result), {
1777
+ status: 200,
1778
+ headers: {
1779
+ "Content-Type": "application/json"
1780
+ }
1781
+ });
1782
+ } catch (error) {
1783
+ console.error(`Error executing tool ${toolName}:`, error);
1784
+ return new Response(JSON.stringify({
1785
+ error: error instanceof Error ? error.message : "Tool execution failed"
1786
+ }), {
1787
+ status: 500,
1788
+ headers: {
1789
+ "Content-Type": "application/json"
1790
+ }
1791
+ });
1792
+ }
1793
+ }
1794
+ return new Response(JSON.stringify({ error: "Method not allowed" }), {
1795
+ status: 405,
1796
+ headers: {
1797
+ "Content-Type": "application/json"
1798
+ }
1799
+ });
1800
+ } catch (error) {
1801
+ console.error("API handler error:", error);
1802
+ if (error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
1803
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
1804
+ status: 401,
1805
+ headers: {
1806
+ "Content-Type": "application/json"
1807
+ }
1808
+ });
1809
+ }
1810
+ return new Response(JSON.stringify({
1811
+ error: error instanceof Error ? error.message : "Internal server error"
1812
+ }), {
1813
+ status: 500,
1814
+ headers: {
1815
+ "Content-Type": "application/json"
1816
+ }
1817
+ });
1818
+ }
1819
+ };
1820
+ }
1821
+ var init_api = __esm(() => {
1822
+ init_ai_tool_to_genai();
1823
+ });
1824
+
1825
+ // ../liveapi/src/index.ts
1826
+ var exports_src = {};
1827
+ __export(exports_src, {
1828
+ uiMessageToClientMessage: () => uiMessageToClientMessage,
1829
+ mergeConsecutiveTextParts: () => mergeConsecutiveTextParts,
1830
+ extractSchemaFromTool: () => extractSchemaFromTool,
1831
+ downSampleInt16Buffer: () => downSampleInt16Buffer,
1832
+ downSampleAudioBuffer: () => downSampleAudioBuffer,
1833
+ createAudioChatAPI: () => createAudioChatAPI,
1834
+ callableToolsFromObject: () => callableToolsFromObject,
1835
+ aiToolToGenAIFunction: () => aiToolToGenAIFunction,
1836
+ aiToolToCallableTool: () => aiToolToCallableTool,
1837
+ LiveMessageAssembler: () => LiveMessageAssembler,
1838
+ LiveAPIClient: () => LiveAPIClient
1839
+ });
1840
+ var init_src = __esm(() => {
1841
+ init_live_api_client();
1842
+ init_ai_tool_to_genai();
1843
+ init_genai_to_ui_message();
1844
+ init_api();
1845
+ });
1846
+
1847
+ // src/cli.tsx
1848
+ import { cac } from "cac";
1849
+
1850
+ // ../node_modules/.pnpm/string-dedent@3.0.2/node_modules/string-dedent/dist/dedent.mjs
1851
+ var cache = new WeakMap;
1852
+ var newline = /(\n|\r\n?|\u2028|\u2029)/g;
1853
+ var leadingWhitespace = /^\s*/;
1854
+ var nonWhitespace = /\S/;
1855
+ var slice = Array.prototype.slice;
1856
+ var zero = 48;
1857
+ var nine = 57;
1858
+ var lowerA = 97;
1859
+ var lowerF = 102;
1860
+ var upperA = 65;
1861
+ var upperF = 70;
1862
+ function dedent(arg) {
1863
+ if (typeof arg === "string") {
1864
+ return process2([arg])[0];
1865
+ }
1866
+ if (typeof arg === "function") {
1867
+ return function() {
1868
+ const args = slice.call(arguments);
1869
+ args[0] = processTemplateStringsArray(args[0]);
1870
+ return arg.apply(this, args);
1871
+ };
1872
+ }
1873
+ const strings = processTemplateStringsArray(arg);
1874
+ let s = getCooked(strings, 0);
1875
+ for (let i = 1;i < strings.length; i++) {
1876
+ s += arguments[i] + getCooked(strings, i);
1877
+ }
1878
+ return s;
1879
+ }
1880
+ function getCooked(strings, index) {
1881
+ const str = strings[index];
1882
+ if (str === undefined)
1883
+ throw new TypeError(`invalid cooked string at index ${index}`);
1884
+ return str;
1885
+ }
1886
+ function processTemplateStringsArray(strings) {
1887
+ const cached = cache.get(strings);
1888
+ if (cached)
1889
+ return cached;
1890
+ const raw = process2(strings.raw);
1891
+ const cooked = raw.map(cook);
1892
+ Object.defineProperty(cooked, "raw", {
1893
+ value: Object.freeze(raw)
1894
+ });
1895
+ Object.freeze(cooked);
1896
+ cache.set(strings, cooked);
1897
+ return cooked;
1898
+ }
1899
+ function process2(strings) {
1900
+ const splitQuasis = strings.map((quasi) => quasi.split(newline));
1901
+ let common;
1902
+ for (let i = 0;i < splitQuasis.length; i++) {
1903
+ const lines = splitQuasis[i];
1904
+ const firstSplit = i === 0;
1905
+ const lastSplit = i + 1 === splitQuasis.length;
1906
+ if (firstSplit) {
1907
+ if (lines.length === 1 || lines[0].length > 0) {
1908
+ throw new Error("invalid content on opening line");
1909
+ }
1910
+ lines[1] = "";
1911
+ }
1912
+ if (lastSplit) {
1913
+ if (lines.length === 1 || nonWhitespace.test(lines[lines.length - 1])) {
1914
+ throw new Error("invalid content on closing line");
1915
+ }
1916
+ lines[lines.length - 2] = "";
1917
+ lines[lines.length - 1] = "";
1918
+ }
1919
+ for (let j = 2;j < lines.length; j += 2) {
1920
+ const text = lines[j];
1921
+ const lineContainsTemplateExpression = j + 1 === lines.length && !lastSplit;
1922
+ const leading = leadingWhitespace.exec(text)[0];
1923
+ if (!lineContainsTemplateExpression && leading.length === text.length) {
1924
+ lines[j] = "";
1925
+ continue;
1926
+ }
1927
+ common = commonStart(leading, common);
1928
+ }
1929
+ }
1930
+ const min = common ? common.length : 0;
1931
+ return splitQuasis.map((lines) => {
1932
+ let quasi = lines[0];
1933
+ for (let i = 1;i < lines.length; i += 2) {
1934
+ const newline2 = lines[i];
1935
+ const text = lines[i + 1];
1936
+ quasi += newline2 + text.slice(min);
1937
+ }
1938
+ return quasi;
1939
+ });
1940
+ }
1941
+ function commonStart(a, b) {
1942
+ if (b === undefined || a === b)
1943
+ return a;
1944
+ let i = 0;
1945
+ for (const len = Math.min(a.length, b.length);i < len; i++) {
1946
+ if (a[i] !== b[i])
1947
+ break;
1948
+ }
1949
+ return a.slice(0, i);
1950
+ }
1951
+ function cook(raw) {
1952
+ let out = "";
1953
+ let start = 0;
1954
+ let i = 0;
1955
+ while ((i = raw.indexOf("\\", i)) > -1) {
1956
+ out += raw.slice(start, i);
1957
+ if (++i === raw.length)
1958
+ return;
1959
+ const next = raw[i++];
1960
+ switch (next) {
1961
+ case "b":
1962
+ out += "\b";
1963
+ break;
1964
+ case "t":
1965
+ out += "\t";
1966
+ break;
1967
+ case "n":
1968
+ out += `
1969
+ `;
1970
+ break;
1971
+ case "v":
1972
+ out += "\v";
1973
+ break;
1974
+ case "f":
1975
+ out += "\f";
1976
+ break;
1977
+ case "r":
1978
+ out += "\r";
1979
+ break;
1980
+ case "\r":
1981
+ if (i < raw.length && raw[i] === `
1982
+ `)
1983
+ ++i;
1984
+ case `
1985
+ `:
1986
+ case "\u2028":
1987
+ case "\u2029":
1988
+ break;
1989
+ case "0":
1990
+ if (isDigit(raw, i))
1991
+ return;
1992
+ out += "\x00";
1993
+ break;
1994
+ case "x": {
1995
+ const n = parseHex(raw, i, i + 2);
1996
+ if (n === -1)
1997
+ return;
1998
+ i += 2;
1999
+ out += String.fromCharCode(n);
2000
+ break;
2001
+ }
2002
+ case "u": {
2003
+ let n;
2004
+ if (i < raw.length && raw[i] === "{") {
2005
+ const end = raw.indexOf("}", ++i);
2006
+ if (end === -1)
2007
+ return;
2008
+ n = parseHex(raw, i, end);
2009
+ i = end + 1;
2010
+ } else {
2011
+ n = parseHex(raw, i, i + 4);
2012
+ i += 4;
2013
+ }
2014
+ if (n === -1 || n > 1114111)
2015
+ return;
2016
+ out += String.fromCodePoint(n);
2017
+ break;
2018
+ }
2019
+ default:
2020
+ if (isDigit(next, 0))
2021
+ return;
2022
+ out += next;
2023
+ }
2024
+ start = i;
2025
+ }
2026
+ return out + raw.slice(start);
2027
+ }
2028
+ function isDigit(str, index) {
2029
+ const c = str.charCodeAt(index);
2030
+ return c >= zero && c <= nine;
2031
+ }
2032
+ function parseHex(str, index, end) {
2033
+ if (end >= str.length)
2034
+ return -1;
2035
+ let n = 0;
2036
+ for (;index < end; index++) {
2037
+ const c = hexToInt(str.charCodeAt(index));
2038
+ if (c === -1)
2039
+ return -1;
2040
+ n = n * 16 + c;
2041
+ }
2042
+ return n;
2043
+ }
2044
+ function hexToInt(c) {
2045
+ if (c >= zero && c <= nine)
2046
+ return c - zero;
2047
+ if (c >= lowerA && c <= lowerF)
2048
+ return c - lowerA + 10;
2049
+ if (c >= upperA && c <= upperF)
2050
+ return c - upperA + 10;
2051
+ return -1;
2052
+ }
2053
+
2054
+ // src/cli.tsx
2055
+ import { mediaDevices } from "node-web-audio-api";
2056
+ import { MediaResolution as MediaResolution2, Modality as Modality2 } from "@google/genai";
2057
+ import * as webAudioApi from "node-web-audio-api";
2058
+ import fs3 from "node:fs";
2059
+ import path2 from "node:path";
2060
+ import WaveFile from "wavefile";
2061
+ import pc2 from "picocolors";
2062
+
2063
+ // src/tools.ts
2064
+ import { tool } from "ai";
2065
+ import { z as z2 } from "zod";
2066
+ import { spawn } from "node:child_process";
2067
+ import net from "node:net";
2068
+ import {
2069
+ createOpencodeClient
2070
+ } from "@opencode-ai/sdk";
2071
+ import { formatDistanceToNow } from "date-fns";
2072
+
2073
+ // src/config.ts
2074
+ import fs from "node:fs";
2075
+ import os from "node:os";
2076
+ import path from "node:path";
2077
+ import { z } from "zod";
2078
+ var PreferredModelSchema = z.object({
2079
+ providerId: z.string(),
2080
+ modelId: z.string()
2081
+ });
2082
+ var KimakiConfigSchema = z.object({
2083
+ preferredModel: PreferredModelSchema.optional()
2084
+ });
2085
+ var configDir = path.join(os.homedir(), ".kimaki");
2086
+ var configPath = path.join(configDir, "kimaki.json");
2087
+ async function readConfig() {
2088
+ try {
2089
+ const configData = await fs.promises.readFile(configPath, "utf-8");
2090
+ const config = JSON.parse(configData);
2091
+ return KimakiConfigSchema.parse(config);
2092
+ } catch (error) {
2093
+ return {};
2094
+ }
2095
+ }
2096
+ async function writeConfig(config) {
2097
+ await fs.promises.mkdir(configDir, { recursive: true });
2098
+ await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));
2099
+ }
2100
+ async function updateConfig(updates) {
2101
+ const config = await readConfig();
2102
+ const updatedConfig = { ...config, ...updates };
2103
+ await writeConfig(updatedConfig);
2104
+ }
2105
+
2106
+ // src/markdown.ts
2107
+ import { DateTime } from "luxon";
2108
+ import * as yaml from "js-yaml";
2109
+
2110
+ class ShareMarkdown {
2111
+ client;
2112
+ constructor(client) {
2113
+ this.client = client;
2114
+ }
2115
+ async generate(options) {
2116
+ const { sessionID, includeSystemInfo, lastAssistantOnly } = options;
2117
+ const sessionResponse = await this.client.session.get({
2118
+ path: { id: sessionID }
2119
+ });
2120
+ if (!sessionResponse.data) {
2121
+ throw new Error(`Session ${sessionID} not found`);
2122
+ }
2123
+ const session = sessionResponse.data;
2124
+ const messagesResponse = await this.client.session.messages({
2125
+ path: { id: sessionID }
2126
+ });
2127
+ if (!messagesResponse.data) {
2128
+ throw new Error(`No messages found for session ${sessionID}`);
2129
+ }
2130
+ const messages = messagesResponse.data;
2131
+ const messagesToRender = lastAssistantOnly ? (() => {
2132
+ const assistantMessages = messages.filter((m) => m.info.role === "assistant");
2133
+ return assistantMessages.length > 0 ? [assistantMessages[assistantMessages.length - 1]] : [];
2134
+ })() : messages;
2135
+ const lines = [];
2136
+ if (!lastAssistantOnly) {
2137
+ lines.push(`# ${session.title || "Untitled Session"}`);
2138
+ lines.push("");
2139
+ if (includeSystemInfo === true) {
2140
+ lines.push("## Session Information");
2141
+ lines.push("");
2142
+ lines.push(`- **Created**: ${DateTime.fromMillis(session.time.created).toLocaleString(DateTime.DATETIME_MED)}`);
2143
+ lines.push(`- **Updated**: ${DateTime.fromMillis(session.time.updated).toLocaleString(DateTime.DATETIME_MED)}`);
2144
+ if (session.version) {
2145
+ lines.push(`- **OpenCode Version**: v${session.version}`);
2146
+ }
2147
+ lines.push("");
2148
+ }
2149
+ lines.push("## Conversation");
2150
+ lines.push("");
2151
+ }
2152
+ for (const message of messagesToRender) {
2153
+ const messageLines = this.renderMessage(message.info, message.parts);
2154
+ lines.push(...messageLines);
2155
+ lines.push("");
2156
+ }
2157
+ return lines.join(`
2158
+ `);
2159
+ }
2160
+ renderMessage(message, parts) {
2161
+ const lines = [];
2162
+ if (message.role === "user") {
2163
+ lines.push("### \uD83D\uDC64 User");
2164
+ lines.push("");
2165
+ for (const part of parts) {
2166
+ if (part.type === "text" && part.text) {
2167
+ lines.push(part.text);
2168
+ lines.push("");
2169
+ } else if (part.type === "file") {
2170
+ lines.push(`\uD83D\uDCCE **Attachment**: ${part.filename || "unnamed file"}`);
2171
+ if (part.url) {
2172
+ lines.push(` - URL: ${part.url}`);
2173
+ }
2174
+ lines.push("");
2175
+ }
2176
+ }
2177
+ } else if (message.role === "assistant") {
2178
+ lines.push(`### \uD83E\uDD16 Assistant (${message.modelID || "unknown model"})`);
2179
+ lines.push("");
2180
+ const filteredParts = parts.filter((part) => {
2181
+ if (part.type === "step-start" && parts.indexOf(part) > 0)
2182
+ return false;
2183
+ if (part.type === "snapshot")
2184
+ return false;
2185
+ if (part.type === "patch")
2186
+ return false;
2187
+ if (part.type === "step-finish")
2188
+ return false;
2189
+ if (part.type === "text" && part.synthetic === true)
2190
+ return false;
2191
+ if (part.type === "tool" && part.tool === "todoread")
2192
+ return false;
2193
+ if (part.type === "text" && !part.text)
2194
+ return false;
2195
+ if (part.type === "tool" && (part.state.status === "pending" || part.state.status === "running"))
2196
+ return false;
2197
+ return true;
2198
+ });
2199
+ for (const part of filteredParts) {
2200
+ const partLines = this.renderPart(part, message);
2201
+ lines.push(...partLines);
2202
+ }
2203
+ if (message.time?.completed) {
2204
+ const duration = message.time.completed - message.time.created;
2205
+ lines.push("");
2206
+ lines.push(`*Completed in ${this.formatDuration(duration)}*`);
2207
+ }
2208
+ }
2209
+ return lines;
2210
+ }
2211
+ renderPart(part, message) {
2212
+ const lines = [];
2213
+ switch (part.type) {
2214
+ case "text":
2215
+ if (part.text) {
2216
+ lines.push(part.text);
2217
+ lines.push("");
2218
+ }
2219
+ break;
2220
+ case "reasoning":
2221
+ if (part.text) {
2222
+ lines.push("<details>");
2223
+ lines.push("<summary>\uD83D\uDCAD Thinking</summary>");
2224
+ lines.push("");
2225
+ lines.push(part.text);
2226
+ lines.push("");
2227
+ lines.push("</details>");
2228
+ lines.push("");
2229
+ }
2230
+ break;
2231
+ case "tool":
2232
+ if (part.state.status === "completed") {
2233
+ lines.push(`#### \uD83D\uDEE0️ Tool: ${part.tool}`);
2234
+ lines.push("");
2235
+ if (part.state.input && Object.keys(part.state.input).length > 0) {
2236
+ lines.push("**Input:**");
2237
+ lines.push("```yaml");
2238
+ lines.push(yaml.dump(part.state.input, { lineWidth: -1 }));
2239
+ lines.push("```");
2240
+ lines.push("");
2241
+ }
2242
+ if (part.state.output) {
2243
+ lines.push("**Output:**");
2244
+ lines.push("```");
2245
+ lines.push(part.state.output);
2246
+ lines.push("```");
2247
+ lines.push("");
2248
+ }
2249
+ if (part.state.time?.start && part.state.time?.end) {
2250
+ const duration = part.state.time.end - part.state.time.start;
2251
+ if (duration > 2000) {
2252
+ lines.push(`*Duration: ${this.formatDuration(duration)}*`);
2253
+ lines.push("");
2254
+ }
2255
+ }
2256
+ } else if (part.state.status === "error") {
2257
+ lines.push(`#### ❌ Tool Error: ${part.tool}`);
2258
+ lines.push("");
2259
+ lines.push("```");
2260
+ lines.push(part.state.error || "Unknown error");
2261
+ lines.push("```");
2262
+ lines.push("");
2263
+ }
2264
+ break;
2265
+ case "step-start":
2266
+ lines.push(`**Started using ${message.providerID}/${message.modelID}**`);
2267
+ lines.push("");
2268
+ break;
2269
+ }
2270
+ return lines;
2271
+ }
2272
+ formatDuration(ms) {
2273
+ if (ms < 1000)
2274
+ return `${ms}ms`;
2275
+ if (ms < 60000)
2276
+ return `${(ms / 1000).toFixed(1)}s`;
2277
+ const minutes = Math.floor(ms / 60000);
2278
+ const seconds = Math.floor(ms % 60000 / 1000);
2279
+ return `${minutes}m ${seconds}s`;
2280
+ }
2281
+ }
2282
+
2283
+ // src/tools.ts
2284
+ import pc from "picocolors";
2285
+ async function getOpenPort() {
2286
+ return new Promise((resolve, reject) => {
2287
+ const server = net.createServer();
2288
+ server.listen(0, () => {
2289
+ const address = server.address();
2290
+ if (address && typeof address === "object") {
2291
+ const port = address.port;
2292
+ server.close(() => {
2293
+ resolve(port);
2294
+ });
2295
+ } else {
2296
+ reject(new Error("Failed to get port"));
2297
+ }
2298
+ });
2299
+ server.on("error", reject);
2300
+ });
2301
+ }
2302
+ async function waitForServer(port, maxAttempts = 30) {
2303
+ for (let i = 0;i < maxAttempts; i++) {
2304
+ try {
2305
+ const endpoints = [
2306
+ `http://localhost:${port}/api/health`,
2307
+ `http://localhost:${port}/`,
2308
+ `http://localhost:${port}/api`
2309
+ ];
2310
+ for (const endpoint of endpoints) {
2311
+ try {
2312
+ const response = await fetch(endpoint);
2313
+ if (response.status < 500) {
2314
+ console.log(pc.green(`OpenCode server ready on port ${port}`));
2315
+ return true;
2316
+ }
2317
+ } catch (e) {}
2318
+ }
2319
+ } catch (e) {}
2320
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2321
+ }
2322
+ throw new Error(`Server did not start on port ${port} after ${maxAttempts} seconds`);
2323
+ }
2324
+ async function startOpencodeServer(port) {
2325
+ console.log(pc.cyan(`Starting OpenCode server on port ${port}...`));
2326
+ const serverProcess = spawn("opencode", ["serve", "--port", port.toString()], {
2327
+ stdio: "pipe",
2328
+ detached: false,
2329
+ env: {
2330
+ ...process.env,
2331
+ OPENCODE_PORT: port.toString()
2332
+ }
2333
+ });
2334
+ serverProcess.stdout?.on("data", (data) => {
2335
+ console.log(pc.gray(`[OpenCode] ${data.toString().trim()}`));
2336
+ });
2337
+ serverProcess.stderr?.on("data", (data) => {
2338
+ console.error(pc.yellow(`[OpenCode Error] ${data.toString().trim()}`));
2339
+ });
2340
+ serverProcess.on("error", (error) => {
2341
+ console.error(pc.red("Failed to start OpenCode server:"), error);
2342
+ });
2343
+ await waitForServer(port);
2344
+ return serverProcess;
2345
+ }
2346
+ async function selectModelProvider(providedModel) {
2347
+ if (providedModel) {
2348
+ return {
2349
+ providerID: providedModel.providerId,
2350
+ modelID: providedModel.modelId
2351
+ };
2352
+ }
2353
+ const config = await readConfig();
2354
+ if (config.preferredModel) {
2355
+ return {
2356
+ providerID: config.preferredModel.providerId,
2357
+ modelID: config.preferredModel.modelId
2358
+ };
2359
+ }
2360
+ throw new Error("No model specified and no preferred model set. Please call getModels to see available models and ask user to choose one. Do not spell model names exactly, give rough names like Sonnet or GPT5. Prefer latest versions");
2361
+ }
2362
+ async function getTools({
2363
+ onMessageCompleted
2364
+ } = {}) {
2365
+ const port = await getOpenPort();
2366
+ const serverProcess = await startOpencodeServer(port);
2367
+ const client = createOpencodeClient({ baseUrl: `http://localhost:${port}` });
2368
+ const markdownRenderer = new ShareMarkdown(client);
2369
+ const providersResponse = await client.config.providers({});
2370
+ const providers = providersResponse.data?.providers || [];
2371
+ const config = await readConfig();
2372
+ const preferredModel = config.preferredModel;
2373
+ process.on("exit", () => {
2374
+ if (serverProcess && !serverProcess.killed) {
2375
+ serverProcess.kill("SIGTERM");
2376
+ }
2377
+ });
2378
+ process.on("SIGINT", () => {
2379
+ if (serverProcess && !serverProcess.killed) {
2380
+ serverProcess.kill("SIGTERM");
2381
+ }
2382
+ process.exit(0);
2383
+ });
2384
+ process.on("SIGTERM", () => {
2385
+ if (serverProcess && !serverProcess.killed) {
2386
+ serverProcess.kill("SIGTERM");
2387
+ }
2388
+ process.exit(0);
2389
+ });
2390
+ const getSessionModel = async (sessionId) => {
2391
+ const res = await client.session.messages({ path: { id: sessionId } });
2392
+ const data = res.data;
2393
+ if (!data || data.length === 0)
2394
+ return;
2395
+ for (let i = data.length - 1;i >= 0; i--) {
2396
+ const info = data[i].info;
2397
+ if (info.role === "assistant") {
2398
+ const ai = info;
2399
+ if (!ai.summary && ai.providerID && ai.modelID) {
2400
+ return { providerID: ai.providerID, modelID: ai.modelID };
2401
+ }
2402
+ }
2403
+ }
2404
+ return;
2405
+ };
2406
+ const tools = {
2407
+ submitMessage: tool({
2408
+ description: "Submit a message to an existing chat session. Does not wait for the message to complete",
2409
+ inputSchema: z2.object({
2410
+ sessionId: z2.string().describe("The session ID to send message to"),
2411
+ message: z2.string().describe("The message text to send")
2412
+ }),
2413
+ execute: async ({ sessionId, message }) => {
2414
+ const sessionModel = await getSessionModel(sessionId);
2415
+ client.session.prompt({
2416
+ path: { id: sessionId },
2417
+ body: {
2418
+ parts: [{ type: "text", text: message }],
2419
+ model: sessionModel
2420
+ }
2421
+ }).then(async (response) => {
2422
+ const markdown = await markdownRenderer.generate({
2423
+ sessionID: sessionId,
2424
+ lastAssistantOnly: true
2425
+ });
2426
+ onMessageCompleted?.({
2427
+ sessionId,
2428
+ messageId: "",
2429
+ data: response.data,
2430
+ markdown
2431
+ });
2432
+ }).catch((error) => {
2433
+ onMessageCompleted?.({
2434
+ sessionId,
2435
+ messageId: "",
2436
+ error
2437
+ });
2438
+ });
2439
+ return {
2440
+ success: true,
2441
+ sessionId,
2442
+ directive: "Tell user that message has been sent successfully"
2443
+ };
2444
+ }
2445
+ }),
2446
+ createNewChat: tool({
2447
+ description: "Start a new chat session with an initial message. Does not wait for the message to complete",
2448
+ inputSchema: z2.object({
2449
+ message: z2.string().describe("The initial message to start the chat with"),
2450
+ title: z2.string().optional().describe("Optional title for the session"),
2451
+ model: z2.object({
2452
+ providerId: z2.string().describe('The provider ID (e.g., "anthropic", "openai")'),
2453
+ modelId: z2.string().describe('The model ID (e.g., "claude-opus-4-20250514", "gpt-5")')
2454
+ }).optional().describe("Optional model to use for this session")
2455
+ }),
2456
+ execute: async ({ message, title, model }) => {
2457
+ if (!message.trim()) {
2458
+ throw new Error(`message must be a non empty string`);
2459
+ }
2460
+ try {
2461
+ const { providerID, modelID } = await selectModelProvider(model);
2462
+ if (model) {
2463
+ await updateConfig({
2464
+ preferredModel: {
2465
+ providerId: model.providerId,
2466
+ modelId: model.modelId
2467
+ }
2468
+ });
2469
+ }
2470
+ const session = await client.session.create({
2471
+ body: {
2472
+ title: title || message.slice(0, 50)
2473
+ }
2474
+ });
2475
+ if (!session.data) {
2476
+ throw new Error("Failed to create session");
2477
+ }
2478
+ client.session.prompt({
2479
+ path: { id: session.data.id },
2480
+ body: {
2481
+ parts: [{ type: "text", text: message }],
2482
+ model: modelID && providerID ? { modelID, providerID } : undefined
2483
+ }
2484
+ }).then(async (response) => {
2485
+ const markdown = await markdownRenderer.generate({
2486
+ sessionID: session.data.id,
2487
+ lastAssistantOnly: true
2488
+ });
2489
+ onMessageCompleted?.({
2490
+ sessionId: session.data.id,
2491
+ messageId: "",
2492
+ data: response.data,
2493
+ markdown
2494
+ });
2495
+ }).catch((error) => {
2496
+ onMessageCompleted?.({
2497
+ sessionId: session.data.id,
2498
+ messageId: "",
2499
+ error
2500
+ });
2501
+ });
2502
+ return {
2503
+ success: true,
2504
+ sessionId: session.data.id,
2505
+ title: session.data.title
2506
+ };
2507
+ } catch (error) {
2508
+ return {
2509
+ success: false,
2510
+ error: error instanceof Error ? error.message : "Failed to create chat session"
2511
+ };
2512
+ }
2513
+ }
2514
+ }),
2515
+ listChats: tool({
2516
+ description: "Get a list of available chat sessions sorted by most recent",
2517
+ inputSchema: z2.object({}),
2518
+ execute: async () => {
2519
+ console.log(`listing opencode sessions`);
2520
+ const sessions = await client.session.list();
2521
+ if (!sessions.data) {
2522
+ return { success: false, error: "No sessions found" };
2523
+ }
2524
+ const sortedSessions = [...sessions.data].sort((a, b) => {
2525
+ return b.time.updated - a.time.updated;
2526
+ }).slice(0, 20);
2527
+ const sessionList = sortedSessions.map(async (session) => {
2528
+ const finishedAt = session.time.updated;
2529
+ const status = await (async () => {
2530
+ if (session.revert)
2531
+ return "error";
2532
+ const messagesResponse = await client.session.messages({
2533
+ path: { id: session.id }
2534
+ });
2535
+ const messages = messagesResponse.data || [];
2536
+ const lastMessage = messages[messages.length - 1];
2537
+ if (lastMessage?.info.role === "assistant" && !lastMessage.info.time.completed) {
2538
+ return "in_progress";
2539
+ }
2540
+ return "finished";
2541
+ })();
2542
+ return {
2543
+ id: session.id,
2544
+ folder: session.directory,
2545
+ status,
2546
+ finishedAt: formatDistanceToNow(new Date(finishedAt), {
2547
+ addSuffix: true
2548
+ }),
2549
+ title: session.title,
2550
+ prompt: session.title
2551
+ };
2552
+ });
2553
+ const resolvedList = await Promise.all(sessionList);
2554
+ return {
2555
+ success: true,
2556
+ sessions: resolvedList
2557
+ };
2558
+ }
2559
+ }),
2560
+ searchFiles: tool({
2561
+ description: "Search for files in a folder",
2562
+ inputSchema: z2.object({
2563
+ folder: z2.string().optional().describe("The folder path to search in, optional. only use if user specifically asks for it"),
2564
+ query: z2.string().describe("The search query for files")
2565
+ }),
2566
+ execute: async ({ folder, query }) => {
2567
+ const results = await client.find.files({
2568
+ query: {
2569
+ query,
2570
+ directory: folder
2571
+ }
2572
+ });
2573
+ return {
2574
+ success: true,
2575
+ files: results.data || []
2576
+ };
2577
+ }
2578
+ }),
2579
+ readSessionMessages: tool({
2580
+ description: "Read messages from a chat session",
2581
+ inputSchema: z2.object({
2582
+ sessionId: z2.string().describe("The session ID to read messages from"),
2583
+ lastAssistantOnly: z2.boolean().optional().describe("Only read the last assistant message")
2584
+ }),
2585
+ execute: async ({ sessionId, lastAssistantOnly = false }) => {
2586
+ if (lastAssistantOnly) {
2587
+ const messages = await client.session.messages({
2588
+ path: { id: sessionId }
2589
+ });
2590
+ if (!messages.data) {
2591
+ return { success: false, error: "No messages found" };
2592
+ }
2593
+ const assistantMessages = messages.data.filter((m) => m.info.role === "assistant");
2594
+ if (assistantMessages.length === 0) {
2595
+ return {
2596
+ success: false,
2597
+ error: "No assistant messages found"
2598
+ };
2599
+ }
2600
+ const lastMessage = assistantMessages[assistantMessages.length - 1];
2601
+ const status = "completed" in lastMessage.info.time && lastMessage.info.time.completed ? "completed" : "in_progress";
2602
+ const markdown = await markdownRenderer.generate({
2603
+ sessionID: sessionId,
2604
+ lastAssistantOnly: true
2605
+ });
2606
+ return {
2607
+ success: true,
2608
+ markdown,
2609
+ status
2610
+ };
2611
+ } else {
2612
+ const markdown = await markdownRenderer.generate({
2613
+ sessionID: sessionId
2614
+ });
2615
+ const messages = await client.session.messages({
2616
+ path: { id: sessionId }
2617
+ });
2618
+ const lastMessage = messages.data?.[messages.data.length - 1];
2619
+ const status = lastMessage?.info.role === "assistant" && lastMessage?.info.time && "completed" in lastMessage.info.time && !lastMessage.info.time.completed ? "in_progress" : "completed";
2620
+ return {
2621
+ success: true,
2622
+ markdown,
2623
+ status
2624
+ };
2625
+ }
2626
+ }
2627
+ }),
2628
+ abortChat: tool({
2629
+ description: "Abort/stop an in-progress chat session",
2630
+ inputSchema: z2.object({
2631
+ sessionId: z2.string().describe("The session ID to abort")
2632
+ }),
2633
+ execute: async ({ sessionId }) => {
2634
+ try {
2635
+ const result = await client.session.abort({
2636
+ path: { id: sessionId }
2637
+ });
2638
+ if (!result.data) {
2639
+ return {
2640
+ success: false,
2641
+ error: "Failed to abort session"
2642
+ };
2643
+ }
2644
+ return {
2645
+ success: true,
2646
+ sessionId,
2647
+ message: "Session aborted successfully"
2648
+ };
2649
+ } catch (error) {
2650
+ return {
2651
+ success: false,
2652
+ error: error instanceof Error ? error.message : "Unknown error occurred"
2653
+ };
2654
+ }
2655
+ }
2656
+ }),
2657
+ getModels: tool({
2658
+ description: "Get all available AI models from all providers",
2659
+ inputSchema: z2.object({}),
2660
+ execute: async () => {
2661
+ try {
2662
+ const providersResponse2 = await client.config.providers({});
2663
+ const providers2 = providersResponse2.data?.providers || [];
2664
+ const models = [];
2665
+ providers2.forEach((provider) => {
2666
+ if (provider.models && typeof provider.models === "object") {
2667
+ Object.entries(provider.models).forEach(([modelId, model]) => {
2668
+ models.push({
2669
+ providerId: provider.id,
2670
+ modelId
2671
+ });
2672
+ });
2673
+ }
2674
+ });
2675
+ return {
2676
+ success: true,
2677
+ models,
2678
+ totalCount: models.length
2679
+ };
2680
+ } catch (error) {
2681
+ return {
2682
+ success: false,
2683
+ error: error instanceof Error ? error.message : "Failed to fetch models",
2684
+ models: []
2685
+ };
2686
+ }
2687
+ }
2688
+ })
2689
+ };
2690
+ return {
2691
+ tools,
2692
+ providers,
2693
+ preferredModel
2694
+ };
2695
+ }
2696
+
2697
+ // src/cli.tsx
2698
+ import { render, Box, Text, useStdout } from "ink";
2699
+ import { useState, useEffect } from "react";
2700
+ import { jsxDEV } from "react/jsx-dev-runtime";
2701
+ var cli = cac("kimaki");
2702
+ cli.help();
2703
+ function AsciiVideoPlayer() {
2704
+ const [frameIndex, setFrameIndex] = useState(0);
2705
+ const [asciiFrame, setAsciiFrame] = useState("Loading frames...");
2706
+ const [frames, setFrames] = useState([]);
2707
+ const { stdout } = useStdout();
2708
+ useEffect(() => {
2709
+ const framesDir = path2.join(path2.dirname(new URL(import.meta.url).pathname), "../assets/frames");
2710
+ try {
2711
+ const files = fs3.readdirSync(framesDir).filter((f) => f.endsWith(".png")).sort();
2712
+ setFrames(files.map((f) => path2.join(framesDir, f)));
2713
+ } catch (error) {
2714
+ setAsciiFrame("Error: Could not find frames directory");
2715
+ }
2716
+ }, []);
2717
+ useEffect(() => {
2718
+ if (frames.length === 0)
2719
+ return;
2720
+ const loadFrame = async () => {
2721
+ const { convertImageToAscii: convertImageToAscii2 } = await Promise.resolve().then(() => (init_video_to_ascii(), exports_video_to_ascii));
2722
+ const ascii = await convertImageToAscii2({
2723
+ imagePath: frames[frameIndex],
2724
+ cols: 80,
2725
+ rows: 22,
2726
+ colored: true,
2727
+ keepAspectRatio: false
2728
+ });
2729
+ setAsciiFrame(ascii);
2730
+ };
2731
+ loadFrame();
2732
+ const timer = setTimeout(() => {
2733
+ setFrameIndex((prev) => (prev + 1) % frames.length);
2734
+ }, 50);
2735
+ return () => clearTimeout(timer);
2736
+ }, [frameIndex, frames]);
2737
+ return /* @__PURE__ */ jsxDEV(Box, {
2738
+ flexDirection: "column",
2739
+ children: [
2740
+ /* @__PURE__ */ jsxDEV(Text, {
2741
+ children: asciiFrame
2742
+ }, undefined, false, undefined, this),
2743
+ /* @__PURE__ */ jsxDEV(Text, {
2744
+ dimColor: true,
2745
+ children: [
2746
+ "Press Ctrl+C to exit • Frame ",
2747
+ frameIndex + 1,
2748
+ "/",
2749
+ frames.length
2750
+ ]
2751
+ }, undefined, true, undefined, this)
2752
+ ]
2753
+ }, undefined, true, undefined, this);
2754
+ }
2755
+ function KimakiTUI({
2756
+ messages,
2757
+ isConnected
2758
+ }) {
2759
+ const formatMessage = (msg) => {
2760
+ const data = msg.data;
2761
+ if (data.serverContent?.modelTurn?.parts?.[0]?.text) {
2762
+ return `\uD83E\uDD16 ${data.serverContent.modelTurn.parts[0].text.slice(0, 80)}...`;
2763
+ }
2764
+ if (data.serverContent?.turnComplete) {
2765
+ return "✅ Turn completed";
2766
+ }
2767
+ if (data.toolCall) {
2768
+ return `\uD83D\uDD27 Tool: ${data.toolCall.name}`;
2769
+ }
2770
+ if (data.toolResponse) {
2771
+ return `\uD83D\uDCE6 Tool response received`;
2772
+ }
2773
+ if (data.interrupted) {
2774
+ return "⚠️ Interrupted";
2775
+ }
2776
+ if (data.setupComplete) {
2777
+ return "\uD83D\uDE80 Setup complete";
2778
+ }
2779
+ return JSON.stringify(data).slice(0, 80);
2780
+ };
2781
+ return /* @__PURE__ */ jsxDEV(Box, {
2782
+ flexDirection: "column",
2783
+ paddingX: 1,
2784
+ children: [
2785
+ /* @__PURE__ */ jsxDEV(Box, {
2786
+ borderStyle: "single",
2787
+ paddingX: 1,
2788
+ marginBottom: 1,
2789
+ children: /* @__PURE__ */ jsxDEV(Text, {
2790
+ color: isConnected ? "green" : "yellow",
2791
+ children: [
2792
+ "Kimaki Voice Assistant -",
2793
+ " ",
2794
+ isConnected ? "● Connected to Gemini Live API" : "⏳ Connecting..."
2795
+ ]
2796
+ }, undefined, true, undefined, this)
2797
+ }, undefined, false, undefined, this),
2798
+ /* @__PURE__ */ jsxDEV(Box, {
2799
+ borderStyle: "single",
2800
+ paddingX: 1,
2801
+ flexDirection: "column",
2802
+ children: [
2803
+ /* @__PURE__ */ jsxDEV(Text, {
2804
+ color: "white",
2805
+ bold: true,
2806
+ children: "Debug Messages:"
2807
+ }, undefined, false, undefined, this),
2808
+ /* @__PURE__ */ jsxDEV(Box, {
2809
+ flexDirection: "column",
2810
+ marginTop: 1,
2811
+ children: messages.length === 0 ? /* @__PURE__ */ jsxDEV(Text, {
2812
+ dimColor: true,
2813
+ children: "No messages yet..."
2814
+ }, undefined, false, undefined, this) : messages.slice(-15).map((msg, index) => /* @__PURE__ */ jsxDEV(Box, {
2815
+ marginBottom: 0,
2816
+ children: [
2817
+ /* @__PURE__ */ jsxDEV(Text, {
2818
+ color: "cyan",
2819
+ children: [
2820
+ "[",
2821
+ new Date(msg.timestamp).toLocaleTimeString(),
2822
+ "]",
2823
+ " "
2824
+ ]
2825
+ }, undefined, true, undefined, this),
2826
+ /* @__PURE__ */ jsxDEV(Text, {
2827
+ color: "white",
2828
+ children: formatMessage(msg)
2829
+ }, undefined, false, undefined, this)
2830
+ ]
2831
+ }, index, true, undefined, this))
2832
+ }, undefined, false, undefined, this)
2833
+ ]
2834
+ }, undefined, true, undefined, this)
2835
+ ]
2836
+ }, undefined, true, undefined, this);
2837
+ }
2838
+ cli.command("", "Spawn Kimaki to orchestrate code agents").action(async (options) => {
2839
+ try {
2840
+ const token = process.env.GEMINI_API_KEY;
2841
+ Object.assign(globalThis, webAudioApi);
2842
+ navigator.mediaDevices = mediaDevices;
2843
+ const { LiveAPIClient: LiveAPIClient2, callableToolsFromObject: callableToolsFromObject2 } = await Promise.resolve().then(() => (init_src(), exports_src));
2844
+ let liveApiClient = null;
2845
+ let audioChunks = [];
2846
+ let isModelSpeaking = false;
2847
+ let debugMessages = [];
2848
+ let isConnected = false;
2849
+ let updateUI = null;
2850
+ const { tools, providers, preferredModel } = await getTools({
2851
+ onMessageCompleted: (params) => {
2852
+ if (!liveApiClient)
2853
+ return;
2854
+ const text = params.error ? `<systemMessage>
2855
+ Chat message failed for session ${params.sessionId}. Error: ${params.error?.message || String(params.error)}
2856
+ </systemMessage>` : `<systemMessage>
2857
+ Chat message completed for session ${params.sessionId}.
2858
+
2859
+ Assistant response:
2860
+ ${params.markdown}
2861
+ </systemMessage>`;
2862
+ liveApiClient.sendText(text);
2863
+ }
2864
+ });
2865
+ const saveUserAudio = async () => {
2866
+ console.log("saveUserAudio", audioChunks.length);
2867
+ if (audioChunks.length === 0)
2868
+ return;
2869
+ try {
2870
+ const timestamp = Date.now();
2871
+ const dir = "useraudio";
2872
+ if (!fs3.existsSync(dir)) {
2873
+ fs3.mkdirSync(dir, { recursive: true });
2874
+ }
2875
+ const totalLength = audioChunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
2876
+ const combinedBuffer = new ArrayBuffer(totalLength);
2877
+ const combinedView = new Int16Array(combinedBuffer);
2878
+ let offset = 0;
2879
+ for (const chunk of audioChunks) {
2880
+ const chunkView = new Int16Array(chunk);
2881
+ combinedView.set(chunkView, offset);
2882
+ offset += chunkView.length;
2883
+ }
2884
+ const wav = new WaveFile.WaveFile;
2885
+ wav.fromScratch(1, 16000, "16", Array.from(combinedView));
2886
+ const filename = path2.join(dir, `${timestamp}.wav`);
2887
+ await fs3.promises.writeFile(filename, Buffer.from(wav.toBuffer()));
2888
+ console.log(`Saved user audio to ${filename} (${audioChunks.length} chunks)`);
2889
+ audioChunks = [];
2890
+ } catch (error) {
2891
+ console.error("Failed to save audio file:", error);
2892
+ }
2893
+ };
2894
+ const model = "models/gemini-2.5-flash-live-preview";
2895
+ const newClient = new LiveAPIClient2({
2896
+ apiKey: token,
2897
+ model,
2898
+ recordingSampleRate: 44100,
2899
+ autoMuteOnAssistantSpeaking: false,
2900
+ onUserAudioChunk: (chunk) => {
2901
+ if (!isModelSpeaking) {
2902
+ audioChunks.push(chunk);
2903
+ }
2904
+ },
2905
+ onMessage: (message) => {
2906
+ debugMessages.push({
2907
+ timestamp: Date.now(),
2908
+ data: message
2909
+ });
2910
+ if (debugMessages.length > 50) {
2911
+ debugMessages = debugMessages.slice(-50);
2912
+ }
2913
+ if (updateUI) {
2914
+ updateUI([...debugMessages], isConnected);
2915
+ }
2916
+ process.env.DEBUG && console.log(message);
2917
+ if (message.serverContent?.turnComplete && audioChunks.length > 0) {
2918
+ isModelSpeaking = true;
2919
+ process.env.DEBUG && saveUserAudio();
2920
+ }
2921
+ if (message.serverContent?.turnComplete) {
2922
+ isModelSpeaking = false;
2923
+ }
2924
+ },
2925
+ config: {
2926
+ tools: callableToolsFromObject2(tools),
2927
+ responseModalities: [Modality2.AUDIO],
2928
+ speechConfig: {
2929
+ voiceConfig: {
2930
+ prebuiltVoiceConfig: {
2931
+ voiceName: "Charon"
2932
+ }
2933
+ }
2934
+ },
2935
+ mediaResolution: MediaResolution2.MEDIA_RESOLUTION_MEDIUM,
2936
+ contextWindowCompression: {
2937
+ triggerTokens: "25600",
2938
+ slidingWindow: { targetTokens: "12800" }
2939
+ },
2940
+ systemInstruction: {
2941
+ parts: [
2942
+ {
2943
+ text: dedent`
2944
+ You are Kimaki, an AI similar to Jarvis: you help your user (an engineer) controlling his coding agent, just like Jarvis controls Ironman armor and machines. Speak fast.
2945
+
2946
+ You should talk like Jarvis, British accent, satirical, joking and calm. Be short and concise. Speak fast.
2947
+
2948
+ After tool calls give a super short summary of the assistant message, you should say what the assistant message writes.
2949
+
2950
+ Before starting a new session ask for confirmation if it is not clear if the user finished describing it. ask "message ready, send?"
2951
+
2952
+ NEVER repeat the whole tool call parameters or message.
2953
+
2954
+ Your job is to manage many opencode agent chat instances. Opencode is the agent used to write the code, it is similar to Claude Code.
2955
+
2956
+ For everything the user asks it is implicit that the user is asking for you to proxy the requests to opencode sessions.
2957
+
2958
+ You can
2959
+ - start new chats on a given project
2960
+ - read the chats to report progress to the user
2961
+ - submit messages to the chat
2962
+ - list files for a given projects, so you can translate imprecise user prompts to precise messages that mention filename paths using @
2963
+
2964
+ Common patterns
2965
+ - to get the last session use the listChats tool
2966
+ - when user asks you to do something you submit a new session to do it. it's implicit that you proxy requests to the agents chat!
2967
+ - when you submit a session assume the session will take a minute or 2 to complete the task
2968
+
2969
+ Rules
2970
+ - never spell files by mentioning dots, letters, etc. instead give a brief description of the filename
2971
+ - NEVER spell hashes or IDs
2972
+ - never read session ids or other ids
2973
+
2974
+ Your voice is calm and monotone, NEVER excited and goofy. But you speak without jargon or bs and do veiled short jokes.
2975
+ You speak like you knew something other don't. You are cool and cold.
2976
+ `
2977
+ }
2978
+ ]
2979
+ }
2980
+ },
2981
+ onStateChange: (state) => {}
2982
+ });
2983
+ liveApiClient = newClient;
2984
+ const connected = await newClient.connect();
2985
+ isConnected = true;
2986
+ await new Promise((res) => setTimeout(res, 500));
2987
+ const App = () => {
2988
+ const [messages, setMessages] = useState([]);
2989
+ const [connectionStatus, setConnectionStatus] = useState(isConnected);
2990
+ useEffect(() => {
2991
+ updateUI = (msgs, connected2) => {
2992
+ setMessages(msgs);
2993
+ setConnectionStatus(connected2);
2994
+ };
2995
+ setMessages([...debugMessages]);
2996
+ setConnectionStatus(isConnected);
2997
+ return () => {
2998
+ updateUI = null;
2999
+ };
3000
+ }, []);
3001
+ return /* @__PURE__ */ jsxDEV(KimakiTUI, {
3002
+ messages,
3003
+ isConnected: connectionStatus
3004
+ }, undefined, false, undefined, this);
3005
+ };
3006
+ render(/* @__PURE__ */ jsxDEV(App, {}, undefined, false, undefined, this));
3007
+ } catch (error) {
3008
+ console.error(pc2.red(`
3009
+ Error initializing project:`));
3010
+ console.error(pc2.red(error));
3011
+ process.exit(1);
3012
+ }
3013
+ });
3014
+ cli.command("ascii", "Play ASCII video").action(async () => {
3015
+ render(/* @__PURE__ */ jsxDEV(AsciiVideoPlayer, {}, undefined, false, undefined, this));
3016
+ });
3017
+
3018
+ // src/bin.ts
3019
+ cli.parse();