kimaki 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/bin.sh +28 -0
- package/dist/ai-tool-to-genai.js +207 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/cli.js +357 -0
- package/dist/directVoiceStreaming.js +102 -0
- package/dist/discordBot.js +1740 -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 +199 -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 +352 -0
- package/dist/utils.js +52 -0
- package/dist/voice.js +28 -0
- package/dist/worker-types.js +1 -0
- package/dist/xml.js +85 -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 +551 -0
- package/src/discordBot.ts +2350 -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 +225 -0
- package/src/openai-realtime.ts +363 -0
- package/src/tools.ts +421 -0
- package/src/utils.ts +73 -0
- package/src/voice.ts +42 -0
- package/src/worker-types.ts +60 -0
- package/src/xml.ts +112 -0
- package/bin.js +0 -3
- 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,421 @@
|
|
|
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?: any
|
|
30
|
+
markdown?: string
|
|
31
|
+
}) => void
|
|
32
|
+
}) {
|
|
33
|
+
const client = await initializeOpencodeForDirectory(directory)
|
|
34
|
+
|
|
35
|
+
const markdownRenderer = new ShareMarkdown(client)
|
|
36
|
+
|
|
37
|
+
const providersResponse = await client.config.providers({})
|
|
38
|
+
const providers: Provider[] = providersResponse.data?.providers || []
|
|
39
|
+
|
|
40
|
+
// Helper: get last assistant model for a session (non-summary)
|
|
41
|
+
const getSessionModel = async (
|
|
42
|
+
sessionId: string,
|
|
43
|
+
): Promise<{ providerID: string; modelID: string } | undefined> => {
|
|
44
|
+
const res = await client.session.messages({ path: { id: sessionId } })
|
|
45
|
+
const data = res.data
|
|
46
|
+
if (!data || data.length === 0) return undefined
|
|
47
|
+
for (let i = data.length - 1; i >= 0; i--) {
|
|
48
|
+
const info = data?.[i]?.info
|
|
49
|
+
if (info?.role === 'assistant') {
|
|
50
|
+
const ai = info as AssistantMessage
|
|
51
|
+
if (!ai.summary && ai.providerID && ai.modelID) {
|
|
52
|
+
return { providerID: ai.providerID, modelID: ai.modelID }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tools = {
|
|
60
|
+
submitMessage: tool({
|
|
61
|
+
description:
|
|
62
|
+
'Submit a message to an existing chat session. Does not wait for the message to complete',
|
|
63
|
+
inputSchema: z.object({
|
|
64
|
+
sessionId: z.string().describe('The session ID to send message to'),
|
|
65
|
+
message: z.string().describe('The message text to send'),
|
|
66
|
+
}),
|
|
67
|
+
execute: async ({ sessionId, message }) => {
|
|
68
|
+
const sessionModel = await getSessionModel(sessionId)
|
|
69
|
+
|
|
70
|
+
// do not await
|
|
71
|
+
client.session
|
|
72
|
+
.prompt({
|
|
73
|
+
path: { id: sessionId },
|
|
74
|
+
|
|
75
|
+
body: {
|
|
76
|
+
parts: [{ type: 'text', text: message }],
|
|
77
|
+
model: sessionModel,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
.then(async (response) => {
|
|
81
|
+
const markdown = await markdownRenderer.generate({
|
|
82
|
+
sessionID: sessionId,
|
|
83
|
+
lastAssistantOnly: true,
|
|
84
|
+
})
|
|
85
|
+
onMessageCompleted?.({
|
|
86
|
+
sessionId,
|
|
87
|
+
messageId: '',
|
|
88
|
+
data: response.data,
|
|
89
|
+
markdown,
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
.catch((error) => {
|
|
93
|
+
onMessageCompleted?.({
|
|
94
|
+
sessionId,
|
|
95
|
+
messageId: '',
|
|
96
|
+
error,
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
sessionId,
|
|
102
|
+
directive: 'Tell user that message has been sent successfully',
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
|
|
107
|
+
createNewChat: tool({
|
|
108
|
+
description:
|
|
109
|
+
'Start a new chat session with an initial message. Does not wait for the message to complete',
|
|
110
|
+
inputSchema: z.object({
|
|
111
|
+
message: z
|
|
112
|
+
.string()
|
|
113
|
+
.describe('The initial message to start the chat with'),
|
|
114
|
+
title: z.string().optional().describe('Optional title for the session'),
|
|
115
|
+
model: z
|
|
116
|
+
.object({
|
|
117
|
+
providerId: z
|
|
118
|
+
.string()
|
|
119
|
+
.describe('The provider ID (e.g., "anthropic", "openai")'),
|
|
120
|
+
modelId: z
|
|
121
|
+
.string()
|
|
122
|
+
.describe(
|
|
123
|
+
'The model ID (e.g., "claude-opus-4-20250514", "gpt-5")',
|
|
124
|
+
),
|
|
125
|
+
})
|
|
126
|
+
.optional()
|
|
127
|
+
.describe('Optional model to use for this session'),
|
|
128
|
+
}),
|
|
129
|
+
execute: async ({ message, title, model }) => {
|
|
130
|
+
if (!message.trim()) {
|
|
131
|
+
throw new Error(`message must be a non empty string`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const session = await client.session.create({
|
|
136
|
+
body: {
|
|
137
|
+
title: title || message.slice(0, 50),
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (!session.data) {
|
|
142
|
+
throw new Error('Failed to create session')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// do not await
|
|
146
|
+
client.session
|
|
147
|
+
.prompt({
|
|
148
|
+
path: { id: session.data.id },
|
|
149
|
+
body: {
|
|
150
|
+
parts: [{ type: 'text', text: message }],
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
.then(async (response) => {
|
|
154
|
+
const markdown = await markdownRenderer.generate({
|
|
155
|
+
sessionID: session.data.id,
|
|
156
|
+
lastAssistantOnly: true,
|
|
157
|
+
})
|
|
158
|
+
onMessageCompleted?.({
|
|
159
|
+
sessionId: session.data.id,
|
|
160
|
+
messageId: '',
|
|
161
|
+
data: response.data,
|
|
162
|
+
markdown,
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
.catch((error) => {
|
|
166
|
+
onMessageCompleted?.({
|
|
167
|
+
sessionId: session.data.id,
|
|
168
|
+
messageId: '',
|
|
169
|
+
error,
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
sessionId: session.data.id,
|
|
176
|
+
title: session.data.title,
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return {
|
|
180
|
+
success: false,
|
|
181
|
+
error:
|
|
182
|
+
error instanceof Error
|
|
183
|
+
? error.message
|
|
184
|
+
: 'Failed to create chat session',
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
|
|
190
|
+
listChats: tool({
|
|
191
|
+
description:
|
|
192
|
+
'Get a list of available chat sessions sorted by most recent',
|
|
193
|
+
inputSchema: z.object({}),
|
|
194
|
+
execute: async () => {
|
|
195
|
+
toolsLogger.log(`Listing opencode sessions`)
|
|
196
|
+
const sessions = await client.session.list()
|
|
197
|
+
|
|
198
|
+
if (!sessions.data) {
|
|
199
|
+
return { success: false, error: 'No sessions found' }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const sortedSessions = [...sessions.data]
|
|
203
|
+
.sort((a, b) => {
|
|
204
|
+
return b.time.updated - a.time.updated
|
|
205
|
+
})
|
|
206
|
+
.slice(0, 20)
|
|
207
|
+
|
|
208
|
+
const sessionList = sortedSessions.map(async (session) => {
|
|
209
|
+
const finishedAt = session.time.updated
|
|
210
|
+
const status = await (async () => {
|
|
211
|
+
if (session.revert) return 'error'
|
|
212
|
+
const messagesResponse = await client.session.messages({
|
|
213
|
+
path: { id: session.id },
|
|
214
|
+
})
|
|
215
|
+
const messages = messagesResponse.data || []
|
|
216
|
+
const lastMessage = messages[messages.length - 1]
|
|
217
|
+
if (
|
|
218
|
+
lastMessage?.info.role === 'assistant' &&
|
|
219
|
+
!lastMessage.info.time.completed
|
|
220
|
+
) {
|
|
221
|
+
return 'in_progress'
|
|
222
|
+
}
|
|
223
|
+
return 'finished'
|
|
224
|
+
})()
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
id: session.id,
|
|
228
|
+
folder: session.directory,
|
|
229
|
+
status,
|
|
230
|
+
finishedAt: formatDistanceToNow(new Date(finishedAt), {
|
|
231
|
+
addSuffix: true,
|
|
232
|
+
}),
|
|
233
|
+
title: session.title,
|
|
234
|
+
prompt: session.title,
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const resolvedList = await Promise.all(sessionList)
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
success: true,
|
|
242
|
+
sessions: resolvedList,
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
}),
|
|
246
|
+
|
|
247
|
+
searchFiles: tool({
|
|
248
|
+
description: 'Search for files in a folder',
|
|
249
|
+
inputSchema: z.object({
|
|
250
|
+
folder: z
|
|
251
|
+
.string()
|
|
252
|
+
.optional()
|
|
253
|
+
.describe(
|
|
254
|
+
'The folder path to search in, optional. only use if user specifically asks for it',
|
|
255
|
+
),
|
|
256
|
+
query: z.string().describe('The search query for files'),
|
|
257
|
+
}),
|
|
258
|
+
execute: async ({ folder, query }) => {
|
|
259
|
+
const results = await client.find.files({
|
|
260
|
+
query: {
|
|
261
|
+
query,
|
|
262
|
+
directory: folder,
|
|
263
|
+
},
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
success: true,
|
|
268
|
+
files: results.data || [],
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
|
|
273
|
+
readSessionMessages: tool({
|
|
274
|
+
description: 'Read messages from a chat session',
|
|
275
|
+
inputSchema: z.object({
|
|
276
|
+
sessionId: z.string().describe('The session ID to read messages from'),
|
|
277
|
+
lastAssistantOnly: z
|
|
278
|
+
.boolean()
|
|
279
|
+
.optional()
|
|
280
|
+
.describe('Only read the last assistant message'),
|
|
281
|
+
}),
|
|
282
|
+
execute: async ({ sessionId, lastAssistantOnly = false }) => {
|
|
283
|
+
if (lastAssistantOnly) {
|
|
284
|
+
const messages = await client.session.messages({
|
|
285
|
+
path: { id: sessionId },
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
if (!messages.data) {
|
|
289
|
+
return { success: false, error: 'No messages found' }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const assistantMessages = messages.data.filter(
|
|
293
|
+
(m) => m.info.role === 'assistant',
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if (assistantMessages.length === 0) {
|
|
297
|
+
return {
|
|
298
|
+
success: false,
|
|
299
|
+
error: 'No assistant messages found',
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const lastMessage = assistantMessages[assistantMessages.length - 1]
|
|
304
|
+
const status =
|
|
305
|
+
'completed' in lastMessage!.info.time &&
|
|
306
|
+
lastMessage!.info.time.completed
|
|
307
|
+
? 'completed'
|
|
308
|
+
: 'in_progress'
|
|
309
|
+
|
|
310
|
+
const markdown = await markdownRenderer.generate({
|
|
311
|
+
sessionID: sessionId,
|
|
312
|
+
lastAssistantOnly: true,
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
success: true,
|
|
317
|
+
markdown,
|
|
318
|
+
status,
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
const markdown = await markdownRenderer.generate({
|
|
322
|
+
sessionID: sessionId,
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
const messages = await client.session.messages({
|
|
326
|
+
path: { id: sessionId },
|
|
327
|
+
})
|
|
328
|
+
const lastMessage = messages.data?.[messages.data.length - 1]
|
|
329
|
+
const status =
|
|
330
|
+
lastMessage?.info.role === 'assistant' &&
|
|
331
|
+
lastMessage?.info.time &&
|
|
332
|
+
'completed' in lastMessage.info.time &&
|
|
333
|
+
!lastMessage.info.time.completed
|
|
334
|
+
? 'in_progress'
|
|
335
|
+
: 'completed'
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
markdown,
|
|
340
|
+
status,
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
}),
|
|
345
|
+
|
|
346
|
+
abortChat: tool({
|
|
347
|
+
description: 'Abort/stop an in-progress chat session',
|
|
348
|
+
inputSchema: z.object({
|
|
349
|
+
sessionId: z.string().describe('The session ID to abort'),
|
|
350
|
+
}),
|
|
351
|
+
execute: async ({ sessionId }) => {
|
|
352
|
+
try {
|
|
353
|
+
const result = await client.session.abort({
|
|
354
|
+
path: { id: sessionId },
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
if (!result.data) {
|
|
358
|
+
return {
|
|
359
|
+
success: false,
|
|
360
|
+
error: 'Failed to abort session',
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
success: true,
|
|
366
|
+
sessionId,
|
|
367
|
+
message: 'Session aborted successfully',
|
|
368
|
+
}
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
error:
|
|
373
|
+
error instanceof Error ? error.message : 'Unknown error occurred',
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
}),
|
|
378
|
+
|
|
379
|
+
getModels: tool({
|
|
380
|
+
description: 'Get all available AI models from all providers',
|
|
381
|
+
inputSchema: z.object({}),
|
|
382
|
+
execute: async () => {
|
|
383
|
+
try {
|
|
384
|
+
const providersResponse = await client.config.providers({})
|
|
385
|
+
const providers: Provider[] = providersResponse.data?.providers || []
|
|
386
|
+
|
|
387
|
+
const models: Array<{ providerId: string; modelId: string }> = []
|
|
388
|
+
|
|
389
|
+
providers.forEach((provider) => {
|
|
390
|
+
if (provider.models && typeof provider.models === 'object') {
|
|
391
|
+
Object.entries(provider.models).forEach(([modelId, model]) => {
|
|
392
|
+
models.push({
|
|
393
|
+
providerId: provider.id,
|
|
394
|
+
modelId: modelId,
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
success: true,
|
|
402
|
+
models,
|
|
403
|
+
totalCount: models.length,
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return {
|
|
407
|
+
success: false,
|
|
408
|
+
error:
|
|
409
|
+
error instanceof Error ? error.message : 'Failed to fetch models',
|
|
410
|
+
models: [],
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
}),
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
tools,
|
|
419
|
+
providers,
|
|
420
|
+
}
|
|
421
|
+
}
|
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.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { DomHandler, Parser, ElementType } from 'htmlparser2'
|
|
2
|
+
import type { ChildNode, Element, Text } from 'domhandler'
|
|
3
|
+
import { createLogger } from './logger.js'
|
|
4
|
+
|
|
5
|
+
const xmlLogger = createLogger('XML')
|
|
6
|
+
|
|
7
|
+
export function extractTagsArrays<T extends string>({
|
|
8
|
+
xml,
|
|
9
|
+
tags,
|
|
10
|
+
}: {
|
|
11
|
+
xml: string
|
|
12
|
+
tags: T[]
|
|
13
|
+
}): Record<T, string[]> & { others: string[] } {
|
|
14
|
+
const result: Record<string, string[]> = {
|
|
15
|
+
others: [],
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Initialize arrays for each tag
|
|
19
|
+
tags.forEach((tag) => {
|
|
20
|
+
result[tag] = []
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const handler = new DomHandler(
|
|
25
|
+
(error, dom) => {
|
|
26
|
+
if (error) {
|
|
27
|
+
xmlLogger.error('Error parsing XML:', error)
|
|
28
|
+
} else {
|
|
29
|
+
const findTags = (nodes: ChildNode[], path: string[] = []) => {
|
|
30
|
+
nodes.forEach((node) => {
|
|
31
|
+
if (node.type === ElementType.Tag) {
|
|
32
|
+
const element = node as Element
|
|
33
|
+
const currentPath = [...path, element.name]
|
|
34
|
+
const pathString = currentPath.join('.')
|
|
35
|
+
|
|
36
|
+
// Extract content using original string positions
|
|
37
|
+
const extractContent = (): string => {
|
|
38
|
+
// Use element's own indices but exclude the tags
|
|
39
|
+
if (
|
|
40
|
+
element.startIndex !== null &&
|
|
41
|
+
element.endIndex !== null
|
|
42
|
+
) {
|
|
43
|
+
// Extract the full element including tags
|
|
44
|
+
const fullElement = xml.substring(
|
|
45
|
+
element.startIndex,
|
|
46
|
+
element.endIndex + 1,
|
|
47
|
+
)
|
|
48
|
+
// Find where content starts (after opening tag)
|
|
49
|
+
const contentStart = fullElement.indexOf('>') + 1
|
|
50
|
+
// Find where content ends (before this element's closing tag)
|
|
51
|
+
const closingTag = `</${element.name}>`
|
|
52
|
+
const contentEnd = fullElement.lastIndexOf(closingTag)
|
|
53
|
+
|
|
54
|
+
if (contentStart > 0 && contentEnd > contentStart) {
|
|
55
|
+
return fullElement.substring(contentStart, contentEnd)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return ''
|
|
59
|
+
}
|
|
60
|
+
return ''
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check both single tag names and nested paths
|
|
64
|
+
if (tags.includes(element.name as T)) {
|
|
65
|
+
const content = extractContent()
|
|
66
|
+
result[element.name as T]?.push(content)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check for nested path matches
|
|
70
|
+
if (tags.includes(pathString as T)) {
|
|
71
|
+
const content = extractContent()
|
|
72
|
+
result[pathString as T]?.push(content)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (element.children) {
|
|
76
|
+
findTags(element.children, currentPath)
|
|
77
|
+
}
|
|
78
|
+
} else if (
|
|
79
|
+
node.type === ElementType.Text &&
|
|
80
|
+
node.parent?.type === ElementType.Root
|
|
81
|
+
) {
|
|
82
|
+
const textNode = node as Text
|
|
83
|
+
if (textNode.data.trim()) {
|
|
84
|
+
// console.log('node.parent',node.parent)
|
|
85
|
+
result.others?.push(textNode.data.trim())
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
findTags(dom)
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
withStartIndices: true,
|
|
96
|
+
withEndIndices: true,
|
|
97
|
+
xmlMode: true,
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const parser = new Parser(handler, {
|
|
102
|
+
xmlMode: true,
|
|
103
|
+
decodeEntities: false,
|
|
104
|
+
})
|
|
105
|
+
parser.write(xml)
|
|
106
|
+
parser.end()
|
|
107
|
+
} catch (error) {
|
|
108
|
+
xmlLogger.error('Unexpected error in extractTags:', error)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return result as Record<T, string[]> & { others: string[] }
|
|
112
|
+
}
|
package/bin.js
DELETED