kimaki 0.4.24 → 0.4.26
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/bin.js +6 -1
- package/dist/acp-client.test.js +149 -0
- package/dist/ai-tool-to-genai.js +3 -0
- package/dist/channel-management.js +14 -9
- package/dist/cli.js +148 -17
- package/dist/commands/abort.js +78 -0
- package/dist/commands/add-project.js +98 -0
- package/dist/commands/agent.js +152 -0
- package/dist/commands/ask-question.js +183 -0
- package/dist/commands/create-new-project.js +78 -0
- package/dist/commands/fork.js +186 -0
- package/dist/commands/model.js +313 -0
- package/dist/commands/permissions.js +126 -0
- package/dist/commands/queue.js +129 -0
- package/dist/commands/resume.js +145 -0
- package/dist/commands/session.js +142 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +161 -0
- package/dist/commands/user-command.js +145 -0
- package/dist/database.js +54 -0
- package/dist/discord-bot.js +35 -32
- package/dist/discord-utils.js +81 -15
- package/dist/format-tables.js +3 -0
- package/dist/genai-worker-wrapper.js +3 -0
- package/dist/genai-worker.js +3 -0
- package/dist/genai.js +3 -0
- package/dist/interaction-handler.js +89 -695
- package/dist/logger.js +46 -5
- package/dist/markdown.js +107 -0
- package/dist/markdown.test.js +31 -1
- package/dist/message-formatting.js +113 -28
- package/dist/message-formatting.test.js +73 -0
- package/dist/opencode.js +73 -16
- package/dist/session-handler.js +176 -63
- package/dist/system-message.js +7 -38
- package/dist/tools.js +3 -0
- package/dist/utils.js +3 -0
- package/dist/voice-handler.js +21 -8
- package/dist/voice.js +31 -12
- package/dist/worker-types.js +3 -0
- package/dist/xml.js +3 -0
- package/package.json +3 -3
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +47 -0
- package/src/ai-tool-to-genai.ts +4 -0
- package/src/channel-management.ts +24 -8
- package/src/cli.ts +163 -18
- package/src/commands/abort.ts +94 -0
- package/src/commands/add-project.ts +139 -0
- package/src/commands/agent.ts +201 -0
- package/src/commands/ask-question.ts +276 -0
- package/src/commands/create-new-project.ts +111 -0
- package/src/{fork.ts → commands/fork.ts} +40 -7
- package/src/{model-command.ts → commands/model.ts} +31 -9
- package/src/commands/permissions.ts +146 -0
- package/src/commands/queue.ts +181 -0
- package/src/commands/resume.ts +230 -0
- package/src/commands/session.ts +184 -0
- package/src/commands/share.ts +96 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +213 -0
- package/src/commands/user-command.ts +178 -0
- package/src/database.ts +65 -0
- package/src/discord-bot.ts +40 -33
- package/src/discord-utils.ts +88 -14
- package/src/format-tables.ts +4 -0
- package/src/genai-worker-wrapper.ts +4 -0
- package/src/genai-worker.ts +4 -0
- package/src/genai.ts +4 -0
- package/src/interaction-handler.ts +111 -924
- package/src/logger.ts +51 -10
- package/src/markdown.test.ts +45 -1
- package/src/markdown.ts +136 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +143 -30
- package/src/opencode.ts +84 -21
- package/src/session-handler.ts +248 -91
- package/src/system-message.ts +8 -38
- package/src/tools.ts +4 -0
- package/src/utils.ts +4 -0
- package/src/voice-handler.ts +24 -9
- package/src/voice.ts +36 -13
- package/src/worker-types.ts +4 -0
- package/src/xml.ts +4 -0
- package/README.md +0 -48
package/dist/database.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// SQLite database manager for persistent bot state.
|
|
2
|
+
// Stores thread-session mappings, bot tokens, channel directories,
|
|
3
|
+
// API keys, and model preferences in ~/.kimaki/discord-sessions.db.
|
|
1
4
|
import Database from 'better-sqlite3';
|
|
2
5
|
import fs from 'node:fs';
|
|
3
6
|
import os from 'node:os';
|
|
@@ -79,6 +82,21 @@ export function runModelMigrations(database) {
|
|
|
79
82
|
model_id TEXT NOT NULL,
|
|
80
83
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
81
84
|
)
|
|
85
|
+
`);
|
|
86
|
+
targetDb.exec(`
|
|
87
|
+
CREATE TABLE IF NOT EXISTS channel_agents (
|
|
88
|
+
channel_id TEXT PRIMARY KEY,
|
|
89
|
+
agent_name TEXT NOT NULL,
|
|
90
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
91
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
92
|
+
)
|
|
93
|
+
`);
|
|
94
|
+
targetDb.exec(`
|
|
95
|
+
CREATE TABLE IF NOT EXISTS session_agents (
|
|
96
|
+
session_id TEXT PRIMARY KEY,
|
|
97
|
+
agent_name TEXT NOT NULL,
|
|
98
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
99
|
+
)
|
|
82
100
|
`);
|
|
83
101
|
dbLogger.log('Model preferences migrations complete');
|
|
84
102
|
}
|
|
@@ -122,6 +140,42 @@ export function setSessionModel(sessionId, modelId) {
|
|
|
122
140
|
const db = getDatabase();
|
|
123
141
|
db.prepare(`INSERT OR REPLACE INTO session_models (session_id, model_id) VALUES (?, ?)`).run(sessionId, modelId);
|
|
124
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Get the agent preference for a channel.
|
|
145
|
+
*/
|
|
146
|
+
export function getChannelAgent(channelId) {
|
|
147
|
+
const db = getDatabase();
|
|
148
|
+
const row = db
|
|
149
|
+
.prepare('SELECT agent_name FROM channel_agents WHERE channel_id = ?')
|
|
150
|
+
.get(channelId);
|
|
151
|
+
return row?.agent_name;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Set the agent preference for a channel.
|
|
155
|
+
*/
|
|
156
|
+
export function setChannelAgent(channelId, agentName) {
|
|
157
|
+
const db = getDatabase();
|
|
158
|
+
db.prepare(`INSERT INTO channel_agents (channel_id, agent_name, updated_at)
|
|
159
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
160
|
+
ON CONFLICT(channel_id) DO UPDATE SET agent_name = ?, updated_at = CURRENT_TIMESTAMP`).run(channelId, agentName, agentName);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get the agent preference for a session.
|
|
164
|
+
*/
|
|
165
|
+
export function getSessionAgent(sessionId) {
|
|
166
|
+
const db = getDatabase();
|
|
167
|
+
const row = db
|
|
168
|
+
.prepare('SELECT agent_name FROM session_agents WHERE session_id = ?')
|
|
169
|
+
.get(sessionId);
|
|
170
|
+
return row?.agent_name;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Set the agent preference for a session.
|
|
174
|
+
*/
|
|
175
|
+
export function setSessionAgent(sessionId, agentName) {
|
|
176
|
+
const db = getDatabase();
|
|
177
|
+
db.prepare(`INSERT OR REPLACE INTO session_agents (session_id, agent_name) VALUES (?, ?)`).run(sessionId, agentName);
|
|
178
|
+
}
|
|
125
179
|
export function closeDatabase() {
|
|
126
180
|
if (db) {
|
|
127
181
|
db.close();
|
package/dist/discord-bot.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Core Discord bot module that handles message events and bot lifecycle.
|
|
2
|
+
// Bridges Discord messages to OpenCode sessions, manages voice connections,
|
|
3
|
+
// and orchestrates the main event loop for the Kimaki bot.
|
|
1
4
|
import { getDatabase, closeDatabase } from './database.js';
|
|
2
5
|
import { initializeOpencodeForDirectory, getOpencodeServers } from './opencode.js';
|
|
3
6
|
import { escapeBackticksInCodeBlocks, splitMarkdownForDiscord, SILENT_MESSAGE_FLAGS, } from './discord-utils.js';
|
|
@@ -5,7 +8,8 @@ import { getOpencodeSystemMessage } from './system-message.js';
|
|
|
5
8
|
import { getFileAttachments, getTextAttachments } from './message-formatting.js';
|
|
6
9
|
import { ensureKimakiCategory, ensureKimakiAudioCategory, createProjectChannels, getChannelsWithDescriptions, } from './channel-management.js';
|
|
7
10
|
import { voiceConnections, cleanupVoiceConnection, processVoiceAttachment, registerVoiceStateHandler, } from './voice-handler.js';
|
|
8
|
-
import {
|
|
11
|
+
import { getCompactSessionContext, getLastSessionId, } from './markdown.js';
|
|
12
|
+
import { handleOpencodeSession } from './session-handler.js';
|
|
9
13
|
import { registerInteractionHandler } from './interaction-handler.js';
|
|
10
14
|
export { getDatabase, closeDatabase } from './database.js';
|
|
11
15
|
export { initializeOpencodeForDirectory } from './opencode.js';
|
|
@@ -150,35 +154,37 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
150
154
|
return;
|
|
151
155
|
}
|
|
152
156
|
let messageContent = message.content || '';
|
|
153
|
-
let
|
|
154
|
-
|
|
157
|
+
let currentSessionContext;
|
|
158
|
+
let lastSessionContext;
|
|
159
|
+
if (projectDirectory) {
|
|
155
160
|
try {
|
|
156
161
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
157
|
-
const
|
|
158
|
-
|
|
162
|
+
const client = getClient();
|
|
163
|
+
// get current session context (without system prompt, it would be duplicated)
|
|
164
|
+
if (row.session_id) {
|
|
165
|
+
currentSessionContext = await getCompactSessionContext({
|
|
166
|
+
client,
|
|
167
|
+
sessionId: row.session_id,
|
|
168
|
+
includeSystemPrompt: false,
|
|
169
|
+
maxMessages: 15,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// get last session context (with system prompt for project context)
|
|
173
|
+
const lastSessionId = await getLastSessionId({
|
|
174
|
+
client,
|
|
175
|
+
excludeSessionId: row.session_id,
|
|
159
176
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return textParts
|
|
169
|
-
.map((p) => ('text' in p ? p.text : ''))
|
|
170
|
-
.filter(Boolean)
|
|
171
|
-
.join('\n');
|
|
172
|
-
}
|
|
173
|
-
const assistantInfo = m.info;
|
|
174
|
-
return assistantInfo.text?.slice(0, 500);
|
|
175
|
-
})();
|
|
176
|
-
return `[${role}]: ${text || '(no text)'}`;
|
|
177
|
-
})
|
|
178
|
-
.join('\n\n');
|
|
177
|
+
if (lastSessionId) {
|
|
178
|
+
lastSessionContext = await getCompactSessionContext({
|
|
179
|
+
client,
|
|
180
|
+
sessionId: lastSessionId,
|
|
181
|
+
includeSystemPrompt: true,
|
|
182
|
+
maxMessages: 10,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
179
185
|
}
|
|
180
186
|
catch (e) {
|
|
181
|
-
voiceLogger.
|
|
187
|
+
voiceLogger.error(`Could not get session context:`, e);
|
|
182
188
|
}
|
|
183
189
|
}
|
|
184
190
|
const transcription = await processVoiceAttachment({
|
|
@@ -186,24 +192,23 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
186
192
|
thread,
|
|
187
193
|
projectDirectory,
|
|
188
194
|
appId: currentAppId,
|
|
189
|
-
|
|
195
|
+
currentSessionContext,
|
|
196
|
+
lastSessionContext,
|
|
190
197
|
});
|
|
191
198
|
if (transcription) {
|
|
192
199
|
messageContent = transcription;
|
|
193
200
|
}
|
|
194
|
-
const fileAttachments = getFileAttachments(message);
|
|
201
|
+
const fileAttachments = await getFileAttachments(message);
|
|
195
202
|
const textAttachmentsContent = await getTextAttachments(message);
|
|
196
203
|
const promptWithAttachments = textAttachmentsContent
|
|
197
204
|
? `${messageContent}\n\n${textAttachmentsContent}`
|
|
198
205
|
: messageContent;
|
|
199
|
-
const parsedCommand = parseSlashCommand(messageContent);
|
|
200
206
|
await handleOpencodeSession({
|
|
201
207
|
prompt: promptWithAttachments,
|
|
202
208
|
thread,
|
|
203
209
|
projectDirectory,
|
|
204
210
|
originalMessage: message,
|
|
205
211
|
images: fileAttachments,
|
|
206
|
-
parsedCommand,
|
|
207
212
|
channelId: parent?.id,
|
|
208
213
|
});
|
|
209
214
|
return;
|
|
@@ -262,19 +267,17 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
262
267
|
if (transcription) {
|
|
263
268
|
messageContent = transcription;
|
|
264
269
|
}
|
|
265
|
-
const fileAttachments = getFileAttachments(message);
|
|
270
|
+
const fileAttachments = await getFileAttachments(message);
|
|
266
271
|
const textAttachmentsContent = await getTextAttachments(message);
|
|
267
272
|
const promptWithAttachments = textAttachmentsContent
|
|
268
273
|
? `${messageContent}\n\n${textAttachmentsContent}`
|
|
269
274
|
: messageContent;
|
|
270
|
-
const parsedCommand = parseSlashCommand(messageContent);
|
|
271
275
|
await handleOpencodeSession({
|
|
272
276
|
prompt: promptWithAttachments,
|
|
273
277
|
thread,
|
|
274
278
|
projectDirectory,
|
|
275
279
|
originalMessage: message,
|
|
276
280
|
images: fileAttachments,
|
|
277
|
-
parsedCommand,
|
|
278
281
|
channelId: textChannel.id,
|
|
279
282
|
});
|
|
280
283
|
}
|
package/dist/discord-utils.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Discord-specific utility functions.
|
|
2
|
+
// Handles markdown splitting for Discord's 2000-char limit, code block escaping,
|
|
3
|
+
// thread message sending, and channel metadata extraction from topic tags.
|
|
1
4
|
import { ChannelType, } from 'discord.js';
|
|
2
5
|
import { Lexer } from 'marked';
|
|
3
6
|
import { extractTagsArrays } from './xml.js';
|
|
@@ -5,6 +8,8 @@ import { formatMarkdownTables } from './format-tables.js';
|
|
|
5
8
|
import { createLogger } from './logger.js';
|
|
6
9
|
const discordLogger = createLogger('DISCORD');
|
|
7
10
|
export const SILENT_MESSAGE_FLAGS = 4 | 4096;
|
|
11
|
+
// Same as SILENT but without SuppressNotifications - triggers badge/notification
|
|
12
|
+
export const NOTIFY_MESSAGE_FLAGS = 4;
|
|
8
13
|
export function escapeBackticksInCodeBlocks(markdown) {
|
|
9
14
|
const lexer = new Lexer();
|
|
10
15
|
const tokens = lexer.lex(markdown);
|
|
@@ -51,29 +56,86 @@ export function splitMarkdownForDiscord({ content, maxLength, }) {
|
|
|
51
56
|
const chunks = [];
|
|
52
57
|
let currentChunk = '';
|
|
53
58
|
let currentLang = null;
|
|
59
|
+
// helper to split a long line into smaller pieces at word boundaries or hard breaks
|
|
60
|
+
const splitLongLine = (text, available, inCode) => {
|
|
61
|
+
const pieces = [];
|
|
62
|
+
let remaining = text;
|
|
63
|
+
while (remaining.length > available) {
|
|
64
|
+
let splitAt = available;
|
|
65
|
+
// for non-code, try to split at word boundary
|
|
66
|
+
if (!inCode) {
|
|
67
|
+
const lastSpace = remaining.lastIndexOf(' ', available);
|
|
68
|
+
if (lastSpace > available * 0.5) {
|
|
69
|
+
splitAt = lastSpace + 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
pieces.push(remaining.slice(0, splitAt));
|
|
73
|
+
remaining = remaining.slice(splitAt);
|
|
74
|
+
}
|
|
75
|
+
if (remaining) {
|
|
76
|
+
pieces.push(remaining);
|
|
77
|
+
}
|
|
78
|
+
return pieces;
|
|
79
|
+
};
|
|
54
80
|
for (const line of lines) {
|
|
55
81
|
const wouldExceed = currentChunk.length + line.text.length > maxLength;
|
|
56
|
-
if (wouldExceed
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
82
|
+
if (wouldExceed) {
|
|
83
|
+
// handle case where single line is longer than maxLength
|
|
84
|
+
if (line.text.length > maxLength) {
|
|
85
|
+
// first, flush current chunk if any
|
|
86
|
+
if (currentChunk) {
|
|
87
|
+
if (currentLang !== null) {
|
|
88
|
+
currentChunk += '```\n';
|
|
89
|
+
}
|
|
90
|
+
chunks.push(currentChunk);
|
|
91
|
+
currentChunk = '';
|
|
92
|
+
}
|
|
93
|
+
// calculate overhead for code block markers
|
|
94
|
+
const codeBlockOverhead = line.inCodeBlock ? ('```' + line.lang + '\n').length + '```\n'.length : 0;
|
|
95
|
+
const availablePerChunk = maxLength - codeBlockOverhead - 50; // safety margin
|
|
96
|
+
const pieces = splitLongLine(line.text, availablePerChunk, line.inCodeBlock);
|
|
97
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
98
|
+
const piece = pieces[i];
|
|
99
|
+
if (line.inCodeBlock) {
|
|
100
|
+
chunks.push('```' + line.lang + '\n' + piece + '```\n');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
chunks.push(piece);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
63
106
|
currentLang = null;
|
|
64
107
|
continue;
|
|
65
108
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
109
|
+
// normal case: line fits in a chunk but current chunk would overflow
|
|
110
|
+
if (currentChunk) {
|
|
111
|
+
if (currentLang !== null) {
|
|
112
|
+
currentChunk += '```\n';
|
|
113
|
+
}
|
|
114
|
+
chunks.push(currentChunk);
|
|
115
|
+
if (line.isClosingFence && currentLang !== null) {
|
|
116
|
+
currentChunk = '';
|
|
117
|
+
currentLang = null;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (line.inCodeBlock || line.isOpeningFence) {
|
|
121
|
+
const lang = line.lang;
|
|
122
|
+
currentChunk = '```' + lang + '\n';
|
|
123
|
+
if (!line.isOpeningFence) {
|
|
124
|
+
currentChunk += line.text;
|
|
125
|
+
}
|
|
126
|
+
currentLang = lang;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
currentChunk = line.text;
|
|
130
|
+
currentLang = null;
|
|
71
131
|
}
|
|
72
|
-
currentLang = lang;
|
|
73
132
|
}
|
|
74
133
|
else {
|
|
134
|
+
// currentChunk is empty but line still exceeds - shouldn't happen after above check
|
|
75
135
|
currentChunk = line.text;
|
|
76
|
-
|
|
136
|
+
if (line.inCodeBlock || line.isOpeningFence) {
|
|
137
|
+
currentLang = line.lang;
|
|
138
|
+
}
|
|
77
139
|
}
|
|
78
140
|
}
|
|
79
141
|
else {
|
|
@@ -91,10 +153,14 @@ export function splitMarkdownForDiscord({ content, maxLength, }) {
|
|
|
91
153
|
}
|
|
92
154
|
return chunks;
|
|
93
155
|
}
|
|
94
|
-
export async function sendThreadMessage(thread, content) {
|
|
156
|
+
export async function sendThreadMessage(thread, content, options) {
|
|
95
157
|
const MAX_LENGTH = 2000;
|
|
96
158
|
content = formatMarkdownTables(content);
|
|
97
159
|
content = escapeBackticksInCodeBlocks(content);
|
|
160
|
+
// If custom flags provided, send as single message (no chunking)
|
|
161
|
+
if (options?.flags !== undefined) {
|
|
162
|
+
return thread.send({ content, flags: options.flags });
|
|
163
|
+
}
|
|
98
164
|
const chunks = splitMarkdownForDiscord({ content, maxLength: MAX_LENGTH });
|
|
99
165
|
if (chunks.length > 1) {
|
|
100
166
|
discordLogger.log(`MESSAGE: Splitting ${content.length} chars into ${chunks.length} messages`);
|
package/dist/format-tables.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Markdown table to code block converter.
|
|
2
|
+
// Discord doesn't render GFM tables, so this converts them to
|
|
3
|
+
// space-aligned code blocks for proper monospace display.
|
|
1
4
|
import { Lexer } from 'marked';
|
|
2
5
|
export function formatMarkdownTables(markdown) {
|
|
3
6
|
const lexer = new Lexer();
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Main thread interface for the GenAI worker.
|
|
2
|
+
// Spawns and manages the worker thread, handling message passing for
|
|
3
|
+
// audio input/output, tool call completions, and graceful shutdown.
|
|
1
4
|
import { Worker } from 'node:worker_threads';
|
|
2
5
|
import { createLogger } from './logger.js';
|
|
3
6
|
const genaiWorkerLogger = createLogger('GENAI WORKER');
|
package/dist/genai-worker.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Worker thread for GenAI voice processing.
|
|
2
|
+
// Runs in a separate thread to handle audio encoding/decoding without blocking.
|
|
3
|
+
// Resamples 24kHz GenAI output to 48kHz stereo Opus packets for Discord.
|
|
1
4
|
import { parentPort, threadId } from 'node:worker_threads';
|
|
2
5
|
import { createWriteStream } from 'node:fs';
|
|
3
6
|
import { mkdir } from 'node:fs/promises';
|
package/dist/genai.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Google GenAI Live session manager for real-time voice interactions.
|
|
2
|
+
// Establishes bidirectional audio streaming with Gemini, handles tool calls,
|
|
3
|
+
// and manages the assistant's audio output for Discord voice channels.
|
|
1
4
|
import { GoogleGenAI, LiveServerMessage, MediaResolution, Modality, Session, } from '@google/genai';
|
|
2
5
|
import { writeFile } from 'fs';
|
|
3
6
|
import { createLogger } from './logger.js';
|