kimaki 0.0.3 → 0.1.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/README.md +7 -0
- package/bin.js +63 -1
- package/dist/ai-tool-to-genai.js +207 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/cli.js +348 -0
- package/dist/directVoiceStreaming.js +102 -0
- package/dist/discordBot.js +1760 -0
- package/dist/genai-worker-wrapper.js +104 -0
- package/dist/genai-worker.js +293 -0
- package/dist/genai.js +224 -0
- package/dist/logger.js +10 -0
- package/dist/markdown.js +203 -0
- package/dist/markdown.test.js +232 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/plugin.js +1414 -0
- package/dist/tools.js +353 -0
- package/dist/utils.js +52 -0
- package/dist/voice.js +28 -0
- package/dist/worker-types.js +1 -0
- package/dist/xml.js +89 -0
- package/dist/xml.test.js +32 -0
- package/package.json +37 -56
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +251 -0
- package/src/cli.ts +539 -0
- package/src/discordBot.ts +2356 -0
- package/src/genai-worker-wrapper.ts +152 -0
- package/src/genai-worker.ts +361 -0
- package/src/genai.ts +308 -0
- package/src/logger.ts +16 -0
- package/src/markdown.test.ts +314 -0
- package/src/markdown.ts +229 -0
- package/src/openai-realtime.ts +363 -0
- package/src/tools.ts +422 -0
- package/src/utils.ts +73 -0
- package/src/voice.ts +42 -0
- package/src/worker-types.ts +60 -0
- package/src/xml.test.ts +37 -0
- package/src/xml.ts +117 -0
- package/dist/bin.d.ts +0 -3
- package/dist/bin.d.ts.map +0 -1
- package/dist/bin.js +0 -4
- package/dist/bin.js.map +0 -1
- package/dist/bundle.js +0 -3124
- package/dist/cli.d.ts.map +0 -1
package/src/tools.ts
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { tool } from 'ai'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
4
|
+
import net from 'node:net'
|
|
5
|
+
import {
|
|
6
|
+
createOpencodeClient,
|
|
7
|
+
type OpencodeClient,
|
|
8
|
+
type AssistantMessage,
|
|
9
|
+
type Provider,
|
|
10
|
+
} from '@opencode-ai/sdk'
|
|
11
|
+
import { createLogger } from './logger.js'
|
|
12
|
+
|
|
13
|
+
const toolsLogger = createLogger('TOOLS')
|
|
14
|
+
import { formatDistanceToNow } from 'date-fns'
|
|
15
|
+
|
|
16
|
+
import { ShareMarkdown } from './markdown.js'
|
|
17
|
+
import pc from 'picocolors'
|
|
18
|
+
import { initializeOpencodeForDirectory } from './discordBot.js'
|
|
19
|
+
|
|
20
|
+
export async function getTools({
|
|
21
|
+
onMessageCompleted,
|
|
22
|
+
directory,
|
|
23
|
+
}: {
|
|
24
|
+
directory: string
|
|
25
|
+
onMessageCompleted?: (params: {
|
|
26
|
+
sessionId: string
|
|
27
|
+
messageId: string
|
|
28
|
+
data?: { info: AssistantMessage }
|
|
29
|
+
error?: unknown
|
|
30
|
+
markdown?: string
|
|
31
|
+
}) => void
|
|
32
|
+
}) {
|
|
33
|
+
const getClient = await initializeOpencodeForDirectory(directory)
|
|
34
|
+
const client = getClient()
|
|
35
|
+
|
|
36
|
+
const markdownRenderer = new ShareMarkdown(client)
|
|
37
|
+
|
|
38
|
+
const providersResponse = await client.config.providers({})
|
|
39
|
+
const providers: Provider[] = providersResponse.data?.providers || []
|
|
40
|
+
|
|
41
|
+
// Helper: get last assistant model for a session (non-summary)
|
|
42
|
+
const getSessionModel = async (
|
|
43
|
+
sessionId: string,
|
|
44
|
+
): Promise<{ providerID: string; modelID: string } | undefined> => {
|
|
45
|
+
const res = await getClient().session.messages({ path: { id: sessionId } })
|
|
46
|
+
const data = res.data
|
|
47
|
+
if (!data || data.length === 0) return undefined
|
|
48
|
+
for (let i = data.length - 1; i >= 0; i--) {
|
|
49
|
+
const info = data?.[i]?.info
|
|
50
|
+
if (info?.role === 'assistant') {
|
|
51
|
+
const ai = info as AssistantMessage
|
|
52
|
+
if (!ai.summary && ai.providerID && ai.modelID) {
|
|
53
|
+
return { providerID: ai.providerID, modelID: ai.modelID }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const tools = {
|
|
61
|
+
submitMessage: tool({
|
|
62
|
+
description:
|
|
63
|
+
'Submit a message to an existing chat session. Does not wait for the message to complete',
|
|
64
|
+
inputSchema: z.object({
|
|
65
|
+
sessionId: z.string().describe('The session ID to send message to'),
|
|
66
|
+
message: z.string().describe('The message text to send'),
|
|
67
|
+
}),
|
|
68
|
+
execute: async ({ sessionId, message }) => {
|
|
69
|
+
const sessionModel = await getSessionModel(sessionId)
|
|
70
|
+
|
|
71
|
+
// do not await
|
|
72
|
+
getClient()
|
|
73
|
+
.session.prompt({
|
|
74
|
+
path: { id: sessionId },
|
|
75
|
+
|
|
76
|
+
body: {
|
|
77
|
+
parts: [{ type: 'text', text: message }],
|
|
78
|
+
model: sessionModel,
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
.then(async (response) => {
|
|
82
|
+
const markdown = await markdownRenderer.generate({
|
|
83
|
+
sessionID: sessionId,
|
|
84
|
+
lastAssistantOnly: true,
|
|
85
|
+
})
|
|
86
|
+
onMessageCompleted?.({
|
|
87
|
+
sessionId,
|
|
88
|
+
messageId: '',
|
|
89
|
+
data: response.data,
|
|
90
|
+
markdown,
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
.catch((error) => {
|
|
94
|
+
onMessageCompleted?.({
|
|
95
|
+
sessionId,
|
|
96
|
+
messageId: '',
|
|
97
|
+
error,
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
sessionId,
|
|
103
|
+
directive: 'Tell user that message has been sent successfully',
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
|
|
108
|
+
createNewChat: tool({
|
|
109
|
+
description:
|
|
110
|
+
'Start a new chat session with an initial message. Does not wait for the message to complete',
|
|
111
|
+
inputSchema: z.object({
|
|
112
|
+
message: z
|
|
113
|
+
.string()
|
|
114
|
+
.describe('The initial message to start the chat with'),
|
|
115
|
+
title: z.string().optional().describe('Optional title for the session'),
|
|
116
|
+
model: z
|
|
117
|
+
.object({
|
|
118
|
+
providerId: z
|
|
119
|
+
.string()
|
|
120
|
+
.describe('The provider ID (e.g., "anthropic", "openai")'),
|
|
121
|
+
modelId: z
|
|
122
|
+
.string()
|
|
123
|
+
.describe(
|
|
124
|
+
'The model ID (e.g., "claude-opus-4-20250514", "gpt-5")',
|
|
125
|
+
),
|
|
126
|
+
})
|
|
127
|
+
.optional()
|
|
128
|
+
.describe('Optional model to use for this session'),
|
|
129
|
+
}),
|
|
130
|
+
execute: async ({ message, title, model }) => {
|
|
131
|
+
if (!message.trim()) {
|
|
132
|
+
throw new Error(`message must be a non empty string`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const session = await getClient().session.create({
|
|
137
|
+
body: {
|
|
138
|
+
title: title || message.slice(0, 50),
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
if (!session.data) {
|
|
143
|
+
throw new Error('Failed to create session')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// do not await
|
|
147
|
+
getClient()
|
|
148
|
+
.session.prompt({
|
|
149
|
+
path: { id: session.data.id },
|
|
150
|
+
body: {
|
|
151
|
+
parts: [{ type: 'text', text: message }],
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
.then(async (response) => {
|
|
155
|
+
const markdown = await markdownRenderer.generate({
|
|
156
|
+
sessionID: session.data.id,
|
|
157
|
+
lastAssistantOnly: true,
|
|
158
|
+
})
|
|
159
|
+
onMessageCompleted?.({
|
|
160
|
+
sessionId: session.data.id,
|
|
161
|
+
messageId: '',
|
|
162
|
+
data: response.data,
|
|
163
|
+
markdown,
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
.catch((error) => {
|
|
167
|
+
onMessageCompleted?.({
|
|
168
|
+
sessionId: session.data.id,
|
|
169
|
+
messageId: '',
|
|
170
|
+
error,
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
success: true,
|
|
176
|
+
sessionId: session.data.id,
|
|
177
|
+
title: session.data.title,
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
error:
|
|
183
|
+
error instanceof Error
|
|
184
|
+
? error.message
|
|
185
|
+
: 'Failed to create chat session',
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
|
|
191
|
+
listChats: tool({
|
|
192
|
+
description:
|
|
193
|
+
'Get a list of available chat sessions sorted by most recent',
|
|
194
|
+
inputSchema: z.object({}),
|
|
195
|
+
execute: async () => {
|
|
196
|
+
toolsLogger.log(`Listing opencode sessions`)
|
|
197
|
+
const sessions = await getClient().session.list()
|
|
198
|
+
|
|
199
|
+
if (!sessions.data) {
|
|
200
|
+
return { success: false, error: 'No sessions found' }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const sortedSessions = [...sessions.data]
|
|
204
|
+
.sort((a, b) => {
|
|
205
|
+
return b.time.updated - a.time.updated
|
|
206
|
+
})
|
|
207
|
+
.slice(0, 20)
|
|
208
|
+
|
|
209
|
+
const sessionList = sortedSessions.map(async (session) => {
|
|
210
|
+
const finishedAt = session.time.updated
|
|
211
|
+
const status = await (async () => {
|
|
212
|
+
if (session.revert) return 'error'
|
|
213
|
+
const messagesResponse = await getClient().session.messages({
|
|
214
|
+
path: { id: session.id },
|
|
215
|
+
})
|
|
216
|
+
const messages = messagesResponse.data || []
|
|
217
|
+
const lastMessage = messages[messages.length - 1]
|
|
218
|
+
if (
|
|
219
|
+
lastMessage?.info.role === 'assistant' &&
|
|
220
|
+
!lastMessage.info.time.completed
|
|
221
|
+
) {
|
|
222
|
+
return 'in_progress'
|
|
223
|
+
}
|
|
224
|
+
return 'finished'
|
|
225
|
+
})()
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
id: session.id,
|
|
229
|
+
folder: session.directory,
|
|
230
|
+
status,
|
|
231
|
+
finishedAt: formatDistanceToNow(new Date(finishedAt), {
|
|
232
|
+
addSuffix: true,
|
|
233
|
+
}),
|
|
234
|
+
title: session.title,
|
|
235
|
+
prompt: session.title,
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const resolvedList = await Promise.all(sessionList)
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
success: true,
|
|
243
|
+
sessions: resolvedList,
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
|
|
248
|
+
searchFiles: tool({
|
|
249
|
+
description: 'Search for files in a folder',
|
|
250
|
+
inputSchema: z.object({
|
|
251
|
+
folder: z
|
|
252
|
+
.string()
|
|
253
|
+
.optional()
|
|
254
|
+
.describe(
|
|
255
|
+
'The folder path to search in, optional. only use if user specifically asks for it',
|
|
256
|
+
),
|
|
257
|
+
query: z.string().describe('The search query for files'),
|
|
258
|
+
}),
|
|
259
|
+
execute: async ({ folder, query }) => {
|
|
260
|
+
const results = await getClient().find.files({
|
|
261
|
+
query: {
|
|
262
|
+
query,
|
|
263
|
+
directory: folder,
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
success: true,
|
|
269
|
+
files: results.data || [],
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
}),
|
|
273
|
+
|
|
274
|
+
readSessionMessages: tool({
|
|
275
|
+
description: 'Read messages from a chat session',
|
|
276
|
+
inputSchema: z.object({
|
|
277
|
+
sessionId: z.string().describe('The session ID to read messages from'),
|
|
278
|
+
lastAssistantOnly: z
|
|
279
|
+
.boolean()
|
|
280
|
+
.optional()
|
|
281
|
+
.describe('Only read the last assistant message'),
|
|
282
|
+
}),
|
|
283
|
+
execute: async ({ sessionId, lastAssistantOnly = false }) => {
|
|
284
|
+
if (lastAssistantOnly) {
|
|
285
|
+
const messages = await getClient().session.messages({
|
|
286
|
+
path: { id: sessionId },
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
if (!messages.data) {
|
|
290
|
+
return { success: false, error: 'No messages found' }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const assistantMessages = messages.data.filter(
|
|
294
|
+
(m) => m.info.role === 'assistant',
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if (assistantMessages.length === 0) {
|
|
298
|
+
return {
|
|
299
|
+
success: false,
|
|
300
|
+
error: 'No assistant messages found',
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const lastMessage = assistantMessages[assistantMessages.length - 1]
|
|
305
|
+
const status =
|
|
306
|
+
'completed' in lastMessage!.info.time &&
|
|
307
|
+
lastMessage!.info.time.completed
|
|
308
|
+
? 'completed'
|
|
309
|
+
: 'in_progress'
|
|
310
|
+
|
|
311
|
+
const markdown = await markdownRenderer.generate({
|
|
312
|
+
sessionID: sessionId,
|
|
313
|
+
lastAssistantOnly: true,
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
success: true,
|
|
318
|
+
markdown,
|
|
319
|
+
status,
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
const markdown = await markdownRenderer.generate({
|
|
323
|
+
sessionID: sessionId,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const messages = await getClient().session.messages({
|
|
327
|
+
path: { id: sessionId },
|
|
328
|
+
})
|
|
329
|
+
const lastMessage = messages.data?.[messages.data.length - 1]
|
|
330
|
+
const status =
|
|
331
|
+
lastMessage?.info.role === 'assistant' &&
|
|
332
|
+
lastMessage?.info.time &&
|
|
333
|
+
'completed' in lastMessage.info.time &&
|
|
334
|
+
!lastMessage.info.time.completed
|
|
335
|
+
? 'in_progress'
|
|
336
|
+
: 'completed'
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
success: true,
|
|
340
|
+
markdown,
|
|
341
|
+
status,
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
}),
|
|
346
|
+
|
|
347
|
+
abortChat: tool({
|
|
348
|
+
description: 'Abort/stop an in-progress chat session',
|
|
349
|
+
inputSchema: z.object({
|
|
350
|
+
sessionId: z.string().describe('The session ID to abort'),
|
|
351
|
+
}),
|
|
352
|
+
execute: async ({ sessionId }) => {
|
|
353
|
+
try {
|
|
354
|
+
const result = await getClient().session.abort({
|
|
355
|
+
path: { id: sessionId },
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
if (!result.data) {
|
|
359
|
+
return {
|
|
360
|
+
success: false,
|
|
361
|
+
error: 'Failed to abort session',
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
success: true,
|
|
367
|
+
sessionId,
|
|
368
|
+
message: 'Session aborted successfully',
|
|
369
|
+
}
|
|
370
|
+
} catch (error) {
|
|
371
|
+
return {
|
|
372
|
+
success: false,
|
|
373
|
+
error:
|
|
374
|
+
error instanceof Error ? error.message : 'Unknown error occurred',
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
}),
|
|
379
|
+
|
|
380
|
+
getModels: tool({
|
|
381
|
+
description: 'Get all available AI models from all providers',
|
|
382
|
+
inputSchema: z.object({}),
|
|
383
|
+
execute: async () => {
|
|
384
|
+
try {
|
|
385
|
+
const providersResponse = await getClient().config.providers({})
|
|
386
|
+
const providers: Provider[] = providersResponse.data?.providers || []
|
|
387
|
+
|
|
388
|
+
const models: Array<{ providerId: string; modelId: string }> = []
|
|
389
|
+
|
|
390
|
+
providers.forEach((provider) => {
|
|
391
|
+
if (provider.models && typeof provider.models === 'object') {
|
|
392
|
+
Object.entries(provider.models).forEach(([modelId, model]) => {
|
|
393
|
+
models.push({
|
|
394
|
+
providerId: provider.id,
|
|
395
|
+
modelId: modelId,
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
success: true,
|
|
403
|
+
models,
|
|
404
|
+
totalCount: models.length,
|
|
405
|
+
}
|
|
406
|
+
} catch (error) {
|
|
407
|
+
return {
|
|
408
|
+
success: false,
|
|
409
|
+
error:
|
|
410
|
+
error instanceof Error ? error.message : 'Failed to fetch models',
|
|
411
|
+
models: [],
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
}),
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
tools,
|
|
420
|
+
providers,
|
|
421
|
+
}
|
|
422
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { PermissionsBitField } from 'discord.js'
|
|
2
|
+
|
|
3
|
+
type GenerateInstallUrlOptions = {
|
|
4
|
+
clientId: string
|
|
5
|
+
permissions?: bigint[]
|
|
6
|
+
scopes?: string[]
|
|
7
|
+
guildId?: string
|
|
8
|
+
disableGuildSelect?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function generateBotInstallUrl({
|
|
12
|
+
clientId,
|
|
13
|
+
permissions = [
|
|
14
|
+
PermissionsBitField.Flags.ViewChannel,
|
|
15
|
+
PermissionsBitField.Flags.ManageChannels,
|
|
16
|
+
PermissionsBitField.Flags.SendMessages,
|
|
17
|
+
PermissionsBitField.Flags.SendMessagesInThreads,
|
|
18
|
+
PermissionsBitField.Flags.CreatePublicThreads,
|
|
19
|
+
PermissionsBitField.Flags.ManageThreads,
|
|
20
|
+
PermissionsBitField.Flags.ReadMessageHistory,
|
|
21
|
+
PermissionsBitField.Flags.AddReactions,
|
|
22
|
+
PermissionsBitField.Flags.ManageMessages,
|
|
23
|
+
PermissionsBitField.Flags.UseExternalEmojis,
|
|
24
|
+
PermissionsBitField.Flags.AttachFiles,
|
|
25
|
+
PermissionsBitField.Flags.Connect,
|
|
26
|
+
PermissionsBitField.Flags.Speak,
|
|
27
|
+
],
|
|
28
|
+
scopes = ['bot'],
|
|
29
|
+
guildId,
|
|
30
|
+
disableGuildSelect = false,
|
|
31
|
+
}: GenerateInstallUrlOptions): string {
|
|
32
|
+
const permissionsBitField = new PermissionsBitField(permissions)
|
|
33
|
+
const permissionsValue = permissionsBitField.bitfield.toString()
|
|
34
|
+
|
|
35
|
+
const url = new URL('https://discord.com/api/oauth2/authorize')
|
|
36
|
+
url.searchParams.set('client_id', clientId)
|
|
37
|
+
url.searchParams.set('permissions', permissionsValue)
|
|
38
|
+
url.searchParams.set('scope', scopes.join(' '))
|
|
39
|
+
|
|
40
|
+
if (guildId) {
|
|
41
|
+
url.searchParams.set('guild_id', guildId)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (disableGuildSelect) {
|
|
45
|
+
url.searchParams.set('disable_guild_select', 'true')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return url.toString()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getRequiredBotPermissions(): bigint[] {
|
|
52
|
+
return [
|
|
53
|
+
PermissionsBitField.Flags.ViewChannel,
|
|
54
|
+
PermissionsBitField.Flags.ManageChannels,
|
|
55
|
+
PermissionsBitField.Flags.SendMessages,
|
|
56
|
+
PermissionsBitField.Flags.SendMessagesInThreads,
|
|
57
|
+
PermissionsBitField.Flags.CreatePublicThreads,
|
|
58
|
+
PermissionsBitField.Flags.ManageThreads,
|
|
59
|
+
PermissionsBitField.Flags.ReadMessageHistory,
|
|
60
|
+
PermissionsBitField.Flags.AddReactions,
|
|
61
|
+
PermissionsBitField.Flags.ManageMessages,
|
|
62
|
+
PermissionsBitField.Flags.UseExternalEmojis,
|
|
63
|
+
PermissionsBitField.Flags.AttachFiles,
|
|
64
|
+
PermissionsBitField.Flags.Connect,
|
|
65
|
+
PermissionsBitField.Flags.Speak,
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getPermissionNames(): string[] {
|
|
70
|
+
const permissions = getRequiredBotPermissions()
|
|
71
|
+
const permissionsBitField = new PermissionsBitField(permissions)
|
|
72
|
+
return permissionsBitField.toArray()
|
|
73
|
+
}
|
package/src/voice.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { openai } from '@ai-sdk/openai'
|
|
2
|
+
import { experimental_transcribe as transcribe } from 'ai'
|
|
3
|
+
import { createLogger } from './logger.js'
|
|
4
|
+
|
|
5
|
+
const voiceLogger = createLogger('VOICE')
|
|
6
|
+
|
|
7
|
+
export async function transcribeAudio({
|
|
8
|
+
audio,
|
|
9
|
+
prompt,
|
|
10
|
+
language,
|
|
11
|
+
temperature,
|
|
12
|
+
}: {
|
|
13
|
+
audio: Buffer | Uint8Array | ArrayBuffer | string
|
|
14
|
+
prompt?: string
|
|
15
|
+
language?: string
|
|
16
|
+
temperature?: number
|
|
17
|
+
}): Promise<string> {
|
|
18
|
+
try {
|
|
19
|
+
const result = await transcribe({
|
|
20
|
+
model: openai.transcription('whisper-1'),
|
|
21
|
+
audio,
|
|
22
|
+
...(prompt || language || temperature !== undefined
|
|
23
|
+
? {
|
|
24
|
+
providerOptions: {
|
|
25
|
+
openai: {
|
|
26
|
+
...(prompt && { prompt }),
|
|
27
|
+
...(language && { language }),
|
|
28
|
+
...(temperature !== undefined && { temperature }),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
: {}),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return result.text
|
|
36
|
+
} catch (error) {
|
|
37
|
+
voiceLogger.error('Failed to transcribe audio:', error)
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Audio transcription failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Tool as AITool } from 'ai'
|
|
2
|
+
|
|
3
|
+
// Messages sent from main thread to worker
|
|
4
|
+
export type WorkerInMessage =
|
|
5
|
+
| {
|
|
6
|
+
type: 'init'
|
|
7
|
+
directory: string // Project directory for tools
|
|
8
|
+
systemMessage?: string
|
|
9
|
+
guildId: string
|
|
10
|
+
channelId: string
|
|
11
|
+
}
|
|
12
|
+
| {
|
|
13
|
+
type: 'sendRealtimeInput'
|
|
14
|
+
audio?: {
|
|
15
|
+
mimeType: string
|
|
16
|
+
data: string // base64
|
|
17
|
+
}
|
|
18
|
+
audioStreamEnd?: boolean
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
type: 'sendTextInput'
|
|
22
|
+
text: string
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
type: 'interrupt'
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
type: 'stop'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Messages sent from worker to main thread via parentPort
|
|
32
|
+
export type WorkerOutMessage =
|
|
33
|
+
| {
|
|
34
|
+
type: 'assistantOpusPacket'
|
|
35
|
+
packet: ArrayBuffer // Opus encoded audio packet
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
type: 'assistantStartSpeaking'
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
type: 'assistantStopSpeaking'
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
type: 'assistantInterruptSpeaking'
|
|
45
|
+
}
|
|
46
|
+
| {
|
|
47
|
+
type: 'toolCallCompleted'
|
|
48
|
+
sessionId: string
|
|
49
|
+
messageId: string
|
|
50
|
+
data?: any
|
|
51
|
+
error?: any
|
|
52
|
+
markdown?: string
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
type: 'error'
|
|
56
|
+
error: string
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
type: 'ready'
|
|
60
|
+
}
|
package/src/xml.test.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest'
|
|
2
|
+
import { extractNonXmlContent } from './xml.js'
|
|
3
|
+
|
|
4
|
+
describe('extractNonXmlContent', () => {
|
|
5
|
+
test('removes xml tags and returns only text content', () => {
|
|
6
|
+
const xml = 'Hello <tag>content</tag> world <nested><inner>deep</inner></nested> end'
|
|
7
|
+
expect(extractNonXmlContent(xml)).toMatchInlineSnapshot(`
|
|
8
|
+
"Hello
|
|
9
|
+
world
|
|
10
|
+
end"
|
|
11
|
+
`)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('handles multiple text segments', () => {
|
|
15
|
+
const xml = 'Start <a>tag1</a> middle <b>tag2</b> finish'
|
|
16
|
+
expect(extractNonXmlContent(xml)).toMatchInlineSnapshot(`
|
|
17
|
+
"Start
|
|
18
|
+
middle
|
|
19
|
+
finish"
|
|
20
|
+
`)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('handles only xml without text', () => {
|
|
24
|
+
const xml = '<root><child>content</child></root>'
|
|
25
|
+
expect(extractNonXmlContent(xml)).toMatchInlineSnapshot(`""`)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('handles only text without xml', () => {
|
|
29
|
+
const xml = 'Just plain text'
|
|
30
|
+
expect(extractNonXmlContent(xml)).toMatchInlineSnapshot(`"Just plain text"`)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('handles empty string', () => {
|
|
34
|
+
const xml = ''
|
|
35
|
+
expect(extractNonXmlContent(xml)).toMatchInlineSnapshot(`""`)
|
|
36
|
+
})
|
|
37
|
+
})
|