shuvmaki 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 +70 -0
- package/dist/ai-tool-to-genai.js +210 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/channel-management.js +97 -0
- package/dist/cli.js +709 -0
- 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 +184 -0
- package/dist/discord-bot.js +384 -0
- package/dist/discord-utils.js +217 -0
- package/dist/escape-backticks.test.js +410 -0
- package/dist/format-tables.js +96 -0
- package/dist/format-tables.test.js +418 -0
- package/dist/genai-worker-wrapper.js +109 -0
- package/dist/genai-worker.js +297 -0
- package/dist/genai.js +232 -0
- package/dist/interaction-handler.js +144 -0
- package/dist/logger.js +51 -0
- package/dist/markdown.js +310 -0
- package/dist/markdown.test.js +262 -0
- package/dist/message-formatting.js +273 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode.js +216 -0
- package/dist/session-handler.js +580 -0
- package/dist/system-message.js +61 -0
- package/dist/tools.js +356 -0
- package/dist/utils.js +85 -0
- package/dist/voice-handler.js +541 -0
- package/dist/voice.js +314 -0
- package/dist/worker-types.js +4 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +60 -0
- 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.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +255 -0
- package/src/channel-management.ts +161 -0
- package/src/cli.ts +1010 -0
- 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/commands/fork.ts +257 -0
- package/src/commands/model.ts +402 -0
- 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 +220 -0
- package/src/discord-bot.ts +513 -0
- package/src/discord-utils.ts +282 -0
- package/src/escape-backticks.test.ts +447 -0
- package/src/format-tables.test.ts +440 -0
- package/src/format-tables.ts +110 -0
- package/src/genai-worker-wrapper.ts +160 -0
- package/src/genai-worker.ts +366 -0
- package/src/genai.ts +321 -0
- package/src/interaction-handler.ts +187 -0
- package/src/logger.ts +57 -0
- package/src/markdown.test.ts +358 -0
- package/src/markdown.ts +365 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +340 -0
- package/src/openai-realtime.ts +363 -0
- package/src/opencode.ts +277 -0
- package/src/session-handler.ts +758 -0
- package/src/system-message.ts +62 -0
- package/src/tools.ts +428 -0
- package/src/utils.ts +118 -0
- package/src/voice-handler.ts +760 -0
- package/src/voice.ts +432 -0
- package/src/worker-types.ts +66 -0
- package/src/xml.test.ts +37 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// OpenCode system prompt generator.
|
|
2
|
+
// Creates the system message injected into every OpenCode session,
|
|
3
|
+
// including Discord-specific formatting rules, diff commands, and permissions info.
|
|
4
|
+
|
|
5
|
+
export function getOpencodeSystemMessage({ sessionId }: { sessionId: string }) {
|
|
6
|
+
return `
|
|
7
|
+
The user is reading your messages from inside Discord, via kimaki.xyz
|
|
8
|
+
|
|
9
|
+
The user cannot see bash tool outputs. If there is important information in bash output, include it in your text response.
|
|
10
|
+
|
|
11
|
+
Your current OpenCode session ID is: ${sessionId}
|
|
12
|
+
|
|
13
|
+
## permissions
|
|
14
|
+
|
|
15
|
+
Only users with these Discord permissions can send messages to the bot:
|
|
16
|
+
- Server Owner
|
|
17
|
+
- Administrator permission
|
|
18
|
+
- Manage Server permission
|
|
19
|
+
- "Kimaki" role (case-insensitive)
|
|
20
|
+
|
|
21
|
+
## uploading files to discord
|
|
22
|
+
|
|
23
|
+
To upload files to the Discord thread (images, screenshots, long files that would clutter the chat), run:
|
|
24
|
+
|
|
25
|
+
npx -y kimaki upload-to-discord --session ${sessionId} <file1> [file2] ...
|
|
26
|
+
|
|
27
|
+
## showing diffs
|
|
28
|
+
|
|
29
|
+
IMPORTANT: After editing any files, you MUST execute the critique command using the Bash tool to get a diff URL, then share that URL with the user.
|
|
30
|
+
|
|
31
|
+
Execute this after making changes:
|
|
32
|
+
|
|
33
|
+
bunx critique web
|
|
34
|
+
|
|
35
|
+
If there are other unrelated changes in the working directory, filter to only show the files you edited:
|
|
36
|
+
|
|
37
|
+
bunx critique web -- path/to/file1.ts path/to/file2.ts
|
|
38
|
+
|
|
39
|
+
You can also show latest commit changes using:
|
|
40
|
+
|
|
41
|
+
bunx critique web HEAD
|
|
42
|
+
|
|
43
|
+
bunx critique web HEAD~1 to get the one before last
|
|
44
|
+
|
|
45
|
+
Do this in case you committed the changes yourself (only if the user asks so, never commit otherwise).
|
|
46
|
+
|
|
47
|
+
The command outputs a URL - share that URL with the user so they can see the diff.
|
|
48
|
+
|
|
49
|
+
## markdown
|
|
50
|
+
|
|
51
|
+
discord does support basic markdown features like code blocks, code blocks languages, inline code, bold, italic, quotes, etc.
|
|
52
|
+
|
|
53
|
+
the max heading level is 3, so do not use ####
|
|
54
|
+
|
|
55
|
+
headings are discouraged anyway. instead try to use bold text for titles which renders more nicely in Discord
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## diagrams
|
|
59
|
+
|
|
60
|
+
you can create diagrams wrapping them in code blocks.
|
|
61
|
+
`
|
|
62
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
// Voice assistant tool definitions for the GenAI worker.
|
|
2
|
+
// Provides tools for managing OpenCode sessions (create, submit, abort),
|
|
3
|
+
// listing chats, searching files, and reading session messages.
|
|
4
|
+
|
|
5
|
+
import { tool } from 'ai'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
8
|
+
import net from 'node:net'
|
|
9
|
+
import {
|
|
10
|
+
createOpencodeClient,
|
|
11
|
+
type OpencodeClient,
|
|
12
|
+
type AssistantMessage,
|
|
13
|
+
type Provider,
|
|
14
|
+
} from '@opencode-ai/sdk'
|
|
15
|
+
import { createLogger } from './logger.js'
|
|
16
|
+
|
|
17
|
+
const toolsLogger = createLogger('TOOLS')
|
|
18
|
+
|
|
19
|
+
import { ShareMarkdown } from './markdown.js'
|
|
20
|
+
import { formatDistanceToNow } from './utils.js'
|
|
21
|
+
import pc from 'picocolors'
|
|
22
|
+
import {
|
|
23
|
+
initializeOpencodeForDirectory,
|
|
24
|
+
getOpencodeSystemMessage,
|
|
25
|
+
} from './discord-bot.js'
|
|
26
|
+
|
|
27
|
+
export async function getTools({
|
|
28
|
+
onMessageCompleted,
|
|
29
|
+
directory,
|
|
30
|
+
}: {
|
|
31
|
+
directory: string
|
|
32
|
+
onMessageCompleted?: (params: {
|
|
33
|
+
sessionId: string
|
|
34
|
+
messageId: string
|
|
35
|
+
data?: { info: AssistantMessage }
|
|
36
|
+
error?: unknown
|
|
37
|
+
markdown?: string
|
|
38
|
+
}) => void
|
|
39
|
+
}) {
|
|
40
|
+
const getClient = await initializeOpencodeForDirectory(directory)
|
|
41
|
+
const client = getClient()
|
|
42
|
+
|
|
43
|
+
const markdownRenderer = new ShareMarkdown(client)
|
|
44
|
+
|
|
45
|
+
const providersResponse = await client.config.providers({})
|
|
46
|
+
const providers: Provider[] = providersResponse.data?.providers || []
|
|
47
|
+
|
|
48
|
+
// Helper: get last assistant model for a session (non-summary)
|
|
49
|
+
const getSessionModel = async (
|
|
50
|
+
sessionId: string,
|
|
51
|
+
): Promise<{ providerID: string; modelID: string } | undefined> => {
|
|
52
|
+
const res = await getClient().session.messages({ path: { id: sessionId } })
|
|
53
|
+
const data = res.data
|
|
54
|
+
if (!data || data.length === 0) return undefined
|
|
55
|
+
for (let i = data.length - 1; i >= 0; i--) {
|
|
56
|
+
const info = data?.[i]?.info
|
|
57
|
+
if (info?.role === 'assistant') {
|
|
58
|
+
const ai = info as AssistantMessage
|
|
59
|
+
if (!ai.summary && ai.providerID && ai.modelID) {
|
|
60
|
+
return { providerID: ai.providerID, modelID: ai.modelID }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const tools = {
|
|
68
|
+
submitMessage: tool({
|
|
69
|
+
description:
|
|
70
|
+
'Submit a message to an existing chat session. Does not wait for the message to complete',
|
|
71
|
+
inputSchema: z.object({
|
|
72
|
+
sessionId: z.string().describe('The session ID to send message to'),
|
|
73
|
+
message: z.string().describe('The message text to send'),
|
|
74
|
+
}),
|
|
75
|
+
execute: async ({ sessionId, message }) => {
|
|
76
|
+
const sessionModel = await getSessionModel(sessionId)
|
|
77
|
+
|
|
78
|
+
// do not await
|
|
79
|
+
getClient()
|
|
80
|
+
.session.prompt({
|
|
81
|
+
path: { id: sessionId },
|
|
82
|
+
body: {
|
|
83
|
+
parts: [{ type: 'text', text: message }],
|
|
84
|
+
model: sessionModel,
|
|
85
|
+
system: getOpencodeSystemMessage({ sessionId }),
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
.then(async (response) => {
|
|
89
|
+
const markdown = await markdownRenderer.generate({
|
|
90
|
+
sessionID: sessionId,
|
|
91
|
+
lastAssistantOnly: true,
|
|
92
|
+
})
|
|
93
|
+
onMessageCompleted?.({
|
|
94
|
+
sessionId,
|
|
95
|
+
messageId: '',
|
|
96
|
+
data: response.data,
|
|
97
|
+
markdown,
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
.catch((error) => {
|
|
101
|
+
onMessageCompleted?.({
|
|
102
|
+
sessionId,
|
|
103
|
+
messageId: '',
|
|
104
|
+
error,
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
sessionId,
|
|
110
|
+
directive: 'Tell user that message has been sent successfully',
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
|
|
115
|
+
createNewChat: tool({
|
|
116
|
+
description:
|
|
117
|
+
'Start a new chat session with an initial message. Does not wait for the message to complete',
|
|
118
|
+
inputSchema: z.object({
|
|
119
|
+
message: z
|
|
120
|
+
.string()
|
|
121
|
+
.describe('The initial message to start the chat with'),
|
|
122
|
+
title: z.string().optional().describe('Optional title for the session'),
|
|
123
|
+
model: z
|
|
124
|
+
.object({
|
|
125
|
+
providerId: z
|
|
126
|
+
.string()
|
|
127
|
+
.describe('The provider ID (e.g., "anthropic", "openai")'),
|
|
128
|
+
modelId: z
|
|
129
|
+
.string()
|
|
130
|
+
.describe(
|
|
131
|
+
'The model ID (e.g., "claude-opus-4-20250514", "gpt-5")',
|
|
132
|
+
),
|
|
133
|
+
})
|
|
134
|
+
.optional()
|
|
135
|
+
.describe('Optional model to use for this session'),
|
|
136
|
+
}),
|
|
137
|
+
execute: async ({ message, title, }) => {
|
|
138
|
+
if (!message.trim()) {
|
|
139
|
+
throw new Error(`message must be a non empty string`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const session = await getClient().session.create({
|
|
144
|
+
body: {
|
|
145
|
+
title: title || message.slice(0, 50),
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
if (!session.data) {
|
|
150
|
+
throw new Error('Failed to create session')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// do not await
|
|
154
|
+
getClient()
|
|
155
|
+
.session.prompt({
|
|
156
|
+
path: { id: session.data.id },
|
|
157
|
+
body: {
|
|
158
|
+
parts: [{ type: 'text', text: message }],
|
|
159
|
+
system: getOpencodeSystemMessage({ sessionId: session.data.id }),
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
.then(async (response) => {
|
|
163
|
+
const markdown = await markdownRenderer.generate({
|
|
164
|
+
sessionID: session.data.id,
|
|
165
|
+
lastAssistantOnly: true,
|
|
166
|
+
})
|
|
167
|
+
onMessageCompleted?.({
|
|
168
|
+
sessionId: session.data.id,
|
|
169
|
+
messageId: '',
|
|
170
|
+
data: response.data,
|
|
171
|
+
markdown,
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
.catch((error) => {
|
|
175
|
+
onMessageCompleted?.({
|
|
176
|
+
sessionId: session.data.id,
|
|
177
|
+
messageId: '',
|
|
178
|
+
error,
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
sessionId: session.data.id,
|
|
185
|
+
title: session.data.title,
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return {
|
|
189
|
+
success: false,
|
|
190
|
+
error:
|
|
191
|
+
error instanceof Error
|
|
192
|
+
? error.message
|
|
193
|
+
: 'Failed to create chat session',
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
|
|
199
|
+
listChats: tool({
|
|
200
|
+
description:
|
|
201
|
+
'Get a list of available chat sessions sorted by most recent',
|
|
202
|
+
inputSchema: z.object({}),
|
|
203
|
+
execute: async () => {
|
|
204
|
+
toolsLogger.log(`Listing opencode sessions`)
|
|
205
|
+
const sessions = await getClient().session.list()
|
|
206
|
+
|
|
207
|
+
if (!sessions.data) {
|
|
208
|
+
return { success: false, error: 'No sessions found' }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const sortedSessions = [...sessions.data]
|
|
212
|
+
.sort((a, b) => {
|
|
213
|
+
return b.time.updated - a.time.updated
|
|
214
|
+
})
|
|
215
|
+
.slice(0, 20)
|
|
216
|
+
|
|
217
|
+
const sessionList = sortedSessions.map(async (session) => {
|
|
218
|
+
const finishedAt = session.time.updated
|
|
219
|
+
const status = await (async () => {
|
|
220
|
+
if (session.revert) return 'error'
|
|
221
|
+
const messagesResponse = await getClient().session.messages({
|
|
222
|
+
path: { id: session.id },
|
|
223
|
+
})
|
|
224
|
+
const messages = messagesResponse.data || []
|
|
225
|
+
const lastMessage = messages[messages.length - 1]
|
|
226
|
+
if (
|
|
227
|
+
lastMessage?.info.role === 'assistant' &&
|
|
228
|
+
!lastMessage.info.time.completed
|
|
229
|
+
) {
|
|
230
|
+
return 'in_progress'
|
|
231
|
+
}
|
|
232
|
+
return 'finished'
|
|
233
|
+
})()
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
id: session.id,
|
|
237
|
+
folder: session.directory,
|
|
238
|
+
status,
|
|
239
|
+
finishedAt: formatDistanceToNow(new Date(finishedAt)),
|
|
240
|
+
title: session.title,
|
|
241
|
+
prompt: session.title,
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const resolvedList = await Promise.all(sessionList)
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
success: true,
|
|
249
|
+
sessions: resolvedList,
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
}),
|
|
253
|
+
|
|
254
|
+
searchFiles: tool({
|
|
255
|
+
description: 'Search for files in a folder',
|
|
256
|
+
inputSchema: z.object({
|
|
257
|
+
folder: z
|
|
258
|
+
.string()
|
|
259
|
+
.optional()
|
|
260
|
+
.describe(
|
|
261
|
+
'The folder path to search in, optional. only use if user specifically asks for it',
|
|
262
|
+
),
|
|
263
|
+
query: z.string().describe('The search query for files'),
|
|
264
|
+
}),
|
|
265
|
+
execute: async ({ folder, query }) => {
|
|
266
|
+
const results = await getClient().find.files({
|
|
267
|
+
query: {
|
|
268
|
+
query,
|
|
269
|
+
directory: folder,
|
|
270
|
+
},
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
success: true,
|
|
275
|
+
files: results.data || [],
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
|
|
280
|
+
readSessionMessages: tool({
|
|
281
|
+
description: 'Read messages from a chat session',
|
|
282
|
+
inputSchema: z.object({
|
|
283
|
+
sessionId: z.string().describe('The session ID to read messages from'),
|
|
284
|
+
lastAssistantOnly: z
|
|
285
|
+
.boolean()
|
|
286
|
+
.optional()
|
|
287
|
+
.describe('Only read the last assistant message'),
|
|
288
|
+
}),
|
|
289
|
+
execute: async ({ sessionId, lastAssistantOnly = false }) => {
|
|
290
|
+
if (lastAssistantOnly) {
|
|
291
|
+
const messages = await getClient().session.messages({
|
|
292
|
+
path: { id: sessionId },
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
if (!messages.data) {
|
|
296
|
+
return { success: false, error: 'No messages found' }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const assistantMessages = messages.data.filter(
|
|
300
|
+
(m) => m.info.role === 'assistant',
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if (assistantMessages.length === 0) {
|
|
304
|
+
return {
|
|
305
|
+
success: false,
|
|
306
|
+
error: 'No assistant messages found',
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const lastMessage = assistantMessages[assistantMessages.length - 1]
|
|
311
|
+
const status =
|
|
312
|
+
'completed' in lastMessage!.info.time &&
|
|
313
|
+
lastMessage!.info.time.completed
|
|
314
|
+
? 'completed'
|
|
315
|
+
: 'in_progress'
|
|
316
|
+
|
|
317
|
+
const markdown = await markdownRenderer.generate({
|
|
318
|
+
sessionID: sessionId,
|
|
319
|
+
lastAssistantOnly: true,
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
success: true,
|
|
324
|
+
markdown,
|
|
325
|
+
status,
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
const markdown = await markdownRenderer.generate({
|
|
329
|
+
sessionID: sessionId,
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
const messages = await getClient().session.messages({
|
|
333
|
+
path: { id: sessionId },
|
|
334
|
+
})
|
|
335
|
+
const lastMessage = messages.data?.[messages.data.length - 1]
|
|
336
|
+
const status =
|
|
337
|
+
lastMessage?.info.role === 'assistant' &&
|
|
338
|
+
lastMessage?.info.time &&
|
|
339
|
+
'completed' in lastMessage.info.time &&
|
|
340
|
+
!lastMessage.info.time.completed
|
|
341
|
+
? 'in_progress'
|
|
342
|
+
: 'completed'
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
success: true,
|
|
346
|
+
markdown,
|
|
347
|
+
status,
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
}),
|
|
352
|
+
|
|
353
|
+
abortChat: tool({
|
|
354
|
+
description: 'Abort/stop an in-progress chat session',
|
|
355
|
+
inputSchema: z.object({
|
|
356
|
+
sessionId: z.string().describe('The session ID to abort'),
|
|
357
|
+
}),
|
|
358
|
+
execute: async ({ sessionId }) => {
|
|
359
|
+
try {
|
|
360
|
+
const result = await getClient().session.abort({
|
|
361
|
+
path: { id: sessionId },
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
if (!result.data) {
|
|
365
|
+
return {
|
|
366
|
+
success: false,
|
|
367
|
+
error: 'Failed to abort session',
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
success: true,
|
|
373
|
+
sessionId,
|
|
374
|
+
message: 'Session aborted successfully',
|
|
375
|
+
}
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return {
|
|
378
|
+
success: false,
|
|
379
|
+
error:
|
|
380
|
+
error instanceof Error ? error.message : 'Unknown error occurred',
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
}),
|
|
385
|
+
|
|
386
|
+
getModels: tool({
|
|
387
|
+
description: 'Get all available AI models from all providers',
|
|
388
|
+
inputSchema: z.object({}),
|
|
389
|
+
execute: async () => {
|
|
390
|
+
try {
|
|
391
|
+
const providersResponse = await getClient().config.providers({})
|
|
392
|
+
const providers: Provider[] = providersResponse.data?.providers || []
|
|
393
|
+
|
|
394
|
+
const models: Array<{ providerId: string; modelId: string }> = []
|
|
395
|
+
|
|
396
|
+
providers.forEach((provider) => {
|
|
397
|
+
if (provider.models && typeof provider.models === 'object') {
|
|
398
|
+
Object.entries(provider.models).forEach(([modelId, model]) => {
|
|
399
|
+
models.push({
|
|
400
|
+
providerId: provider.id,
|
|
401
|
+
modelId: modelId,
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
success: true,
|
|
409
|
+
models,
|
|
410
|
+
totalCount: models.length,
|
|
411
|
+
}
|
|
412
|
+
} catch (error) {
|
|
413
|
+
return {
|
|
414
|
+
success: false,
|
|
415
|
+
error:
|
|
416
|
+
error instanceof Error ? error.message : 'Failed to fetch models',
|
|
417
|
+
models: [],
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
}),
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
tools,
|
|
426
|
+
providers,
|
|
427
|
+
}
|
|
428
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// General utility functions for the bot.
|
|
2
|
+
// Includes Discord OAuth URL generation, array deduplication,
|
|
3
|
+
// abort error detection, and date/time formatting helpers.
|
|
4
|
+
|
|
5
|
+
import { PermissionsBitField } from 'discord.js'
|
|
6
|
+
|
|
7
|
+
type GenerateInstallUrlOptions = {
|
|
8
|
+
clientId: string
|
|
9
|
+
permissions?: bigint[]
|
|
10
|
+
scopes?: string[]
|
|
11
|
+
guildId?: string
|
|
12
|
+
disableGuildSelect?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function generateBotInstallUrl({
|
|
16
|
+
clientId,
|
|
17
|
+
permissions = [
|
|
18
|
+
PermissionsBitField.Flags.ViewChannel,
|
|
19
|
+
PermissionsBitField.Flags.ManageChannels,
|
|
20
|
+
PermissionsBitField.Flags.SendMessages,
|
|
21
|
+
PermissionsBitField.Flags.SendMessagesInThreads,
|
|
22
|
+
PermissionsBitField.Flags.CreatePublicThreads,
|
|
23
|
+
PermissionsBitField.Flags.ManageThreads,
|
|
24
|
+
PermissionsBitField.Flags.ReadMessageHistory,
|
|
25
|
+
PermissionsBitField.Flags.AddReactions,
|
|
26
|
+
PermissionsBitField.Flags.ManageMessages,
|
|
27
|
+
PermissionsBitField.Flags.UseExternalEmojis,
|
|
28
|
+
PermissionsBitField.Flags.AttachFiles,
|
|
29
|
+
PermissionsBitField.Flags.Connect,
|
|
30
|
+
PermissionsBitField.Flags.Speak,
|
|
31
|
+
],
|
|
32
|
+
scopes = ['bot'],
|
|
33
|
+
guildId,
|
|
34
|
+
disableGuildSelect = false,
|
|
35
|
+
}: GenerateInstallUrlOptions): string {
|
|
36
|
+
const permissionsBitField = new PermissionsBitField(permissions)
|
|
37
|
+
const permissionsValue = permissionsBitField.bitfield.toString()
|
|
38
|
+
|
|
39
|
+
const url = new URL('https://discord.com/api/oauth2/authorize')
|
|
40
|
+
url.searchParams.set('client_id', clientId)
|
|
41
|
+
url.searchParams.set('permissions', permissionsValue)
|
|
42
|
+
url.searchParams.set('scope', scopes.join(' '))
|
|
43
|
+
|
|
44
|
+
if (guildId) {
|
|
45
|
+
url.searchParams.set('guild_id', guildId)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (disableGuildSelect) {
|
|
49
|
+
url.searchParams.set('disable_guild_select', 'true')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return url.toString()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function deduplicateByKey<T, K>(arr: T[], keyFn: (item: T) => K): T[] {
|
|
56
|
+
const seen = new Set<K>()
|
|
57
|
+
return arr.filter((item) => {
|
|
58
|
+
const key = keyFn(item)
|
|
59
|
+
if (seen.has(key)) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
seen.add(key)
|
|
63
|
+
return true
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isAbortError(
|
|
68
|
+
error: unknown,
|
|
69
|
+
signal?: AbortSignal,
|
|
70
|
+
): error is Error {
|
|
71
|
+
return (
|
|
72
|
+
(error instanceof Error &&
|
|
73
|
+
(error.name === 'AbortError' ||
|
|
74
|
+
error.name === 'Aborterror' ||
|
|
75
|
+
error.name === 'aborterror' ||
|
|
76
|
+
error.name.toLowerCase() === 'aborterror' ||
|
|
77
|
+
error.message?.includes('aborted') ||
|
|
78
|
+
(signal?.aborted ?? false))) ||
|
|
79
|
+
(error instanceof DOMException && error.name === 'AbortError')
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
|
|
84
|
+
|
|
85
|
+
const TIME_DIVISIONS: Array<{ amount: number; name: Intl.RelativeTimeFormatUnit }> = [
|
|
86
|
+
{ amount: 60, name: 'seconds' },
|
|
87
|
+
{ amount: 60, name: 'minutes' },
|
|
88
|
+
{ amount: 24, name: 'hours' },
|
|
89
|
+
{ amount: 7, name: 'days' },
|
|
90
|
+
{ amount: 4.34524, name: 'weeks' },
|
|
91
|
+
{ amount: 12, name: 'months' },
|
|
92
|
+
{ amount: Number.POSITIVE_INFINITY, name: 'years' },
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
export function formatDistanceToNow(date: Date): string {
|
|
96
|
+
let duration = (date.getTime() - Date.now()) / 1000
|
|
97
|
+
|
|
98
|
+
for (const division of TIME_DIVISIONS) {
|
|
99
|
+
if (Math.abs(duration) < division.amount) {
|
|
100
|
+
return rtf.format(Math.round(duration), division.name)
|
|
101
|
+
}
|
|
102
|
+
duration /= division.amount
|
|
103
|
+
}
|
|
104
|
+
return rtf.format(Math.round(duration), 'years')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
108
|
+
month: 'short',
|
|
109
|
+
day: 'numeric',
|
|
110
|
+
year: 'numeric',
|
|
111
|
+
hour: 'numeric',
|
|
112
|
+
minute: '2-digit',
|
|
113
|
+
hour12: true,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
export function formatDateTime(date: Date): string {
|
|
117
|
+
return dtf.format(date)
|
|
118
|
+
}
|