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,402 @@
|
|
|
1
|
+
// /model command - Set the preferred model for this channel or session.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ChatInputCommandInteraction,
|
|
5
|
+
StringSelectMenuInteraction,
|
|
6
|
+
StringSelectMenuBuilder,
|
|
7
|
+
ActionRowBuilder,
|
|
8
|
+
ChannelType,
|
|
9
|
+
type ThreadChannel,
|
|
10
|
+
type TextChannel,
|
|
11
|
+
} from 'discord.js'
|
|
12
|
+
import crypto from 'node:crypto'
|
|
13
|
+
import { getDatabase, setChannelModel, setSessionModel, runModelMigrations } from '../database.js'
|
|
14
|
+
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
15
|
+
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
|
|
16
|
+
import { abortAndRetrySession } from '../session-handler.js'
|
|
17
|
+
import { createLogger } from '../logger.js'
|
|
18
|
+
|
|
19
|
+
const modelLogger = createLogger('MODEL')
|
|
20
|
+
|
|
21
|
+
// Store context by hash to avoid customId length limits (Discord max: 100 chars)
|
|
22
|
+
const pendingModelContexts = new Map<string, {
|
|
23
|
+
dir: string
|
|
24
|
+
channelId: string
|
|
25
|
+
sessionId?: string
|
|
26
|
+
isThread: boolean
|
|
27
|
+
providerId?: string
|
|
28
|
+
providerName?: string
|
|
29
|
+
thread?: ThreadChannel
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
export type ProviderInfo = {
|
|
33
|
+
id: string
|
|
34
|
+
name: string
|
|
35
|
+
models: Record<
|
|
36
|
+
string,
|
|
37
|
+
{
|
|
38
|
+
id: string
|
|
39
|
+
name: string
|
|
40
|
+
release_date: string
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Handle the /model slash command.
|
|
47
|
+
* Shows a select menu with available providers.
|
|
48
|
+
*/
|
|
49
|
+
export async function handleModelCommand({
|
|
50
|
+
interaction,
|
|
51
|
+
appId,
|
|
52
|
+
}: {
|
|
53
|
+
interaction: ChatInputCommandInteraction
|
|
54
|
+
appId: string
|
|
55
|
+
}): Promise<void> {
|
|
56
|
+
modelLogger.log('[MODEL] handleModelCommand called')
|
|
57
|
+
|
|
58
|
+
// Defer reply immediately to avoid 3-second timeout
|
|
59
|
+
await interaction.deferReply({ ephemeral: true })
|
|
60
|
+
modelLogger.log('[MODEL] Deferred reply')
|
|
61
|
+
|
|
62
|
+
// Ensure migrations are run
|
|
63
|
+
runModelMigrations()
|
|
64
|
+
|
|
65
|
+
const channel = interaction.channel
|
|
66
|
+
|
|
67
|
+
if (!channel) {
|
|
68
|
+
await interaction.editReply({
|
|
69
|
+
content: 'This command can only be used in a channel',
|
|
70
|
+
})
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Determine if we're in a thread or text channel
|
|
75
|
+
const isThread = [
|
|
76
|
+
ChannelType.PublicThread,
|
|
77
|
+
ChannelType.PrivateThread,
|
|
78
|
+
ChannelType.AnnouncementThread,
|
|
79
|
+
].includes(channel.type)
|
|
80
|
+
|
|
81
|
+
let projectDirectory: string | undefined
|
|
82
|
+
let channelAppId: string | undefined
|
|
83
|
+
let targetChannelId: string
|
|
84
|
+
let sessionId: string | undefined
|
|
85
|
+
|
|
86
|
+
if (isThread) {
|
|
87
|
+
const thread = channel as ThreadChannel
|
|
88
|
+
const textChannel = await resolveTextChannel(thread)
|
|
89
|
+
const metadata = getKimakiMetadata(textChannel)
|
|
90
|
+
projectDirectory = metadata.projectDirectory
|
|
91
|
+
channelAppId = metadata.channelAppId
|
|
92
|
+
targetChannelId = textChannel?.id || channel.id
|
|
93
|
+
|
|
94
|
+
// Get session ID for this thread
|
|
95
|
+
const row = getDatabase()
|
|
96
|
+
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
97
|
+
.get(thread.id) as { session_id: string } | undefined
|
|
98
|
+
sessionId = row?.session_id
|
|
99
|
+
} else if (channel.type === ChannelType.GuildText) {
|
|
100
|
+
const textChannel = channel as TextChannel
|
|
101
|
+
const metadata = getKimakiMetadata(textChannel)
|
|
102
|
+
projectDirectory = metadata.projectDirectory
|
|
103
|
+
channelAppId = metadata.channelAppId
|
|
104
|
+
targetChannelId = channel.id
|
|
105
|
+
} else {
|
|
106
|
+
await interaction.editReply({
|
|
107
|
+
content: 'This command can only be used in text channels or threads',
|
|
108
|
+
})
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (channelAppId && channelAppId !== appId) {
|
|
113
|
+
await interaction.editReply({
|
|
114
|
+
content: 'This channel is not configured for this bot',
|
|
115
|
+
})
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!projectDirectory) {
|
|
120
|
+
await interaction.editReply({
|
|
121
|
+
content: 'This channel is not configured with a project directory',
|
|
122
|
+
})
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
128
|
+
|
|
129
|
+
const providersResponse = await getClient().provider.list({
|
|
130
|
+
query: { directory: projectDirectory },
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
if (!providersResponse.data) {
|
|
134
|
+
await interaction.editReply({
|
|
135
|
+
content: 'Failed to fetch providers',
|
|
136
|
+
})
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { all: allProviders, connected } = providersResponse.data
|
|
141
|
+
|
|
142
|
+
// Filter to only connected providers (have credentials)
|
|
143
|
+
const availableProviders = allProviders.filter((p) => {
|
|
144
|
+
return connected.includes(p.id)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
if (availableProviders.length === 0) {
|
|
148
|
+
await interaction.editReply({
|
|
149
|
+
content:
|
|
150
|
+
'No providers with credentials found. Use `/connect` in OpenCode TUI to add provider credentials.',
|
|
151
|
+
})
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Store context with a short hash key to avoid customId length limits
|
|
156
|
+
const context = {
|
|
157
|
+
dir: projectDirectory,
|
|
158
|
+
channelId: targetChannelId,
|
|
159
|
+
sessionId: sessionId,
|
|
160
|
+
isThread: isThread,
|
|
161
|
+
thread: isThread ? (channel as ThreadChannel) : undefined,
|
|
162
|
+
}
|
|
163
|
+
const contextHash = crypto.randomBytes(8).toString('hex')
|
|
164
|
+
pendingModelContexts.set(contextHash, context)
|
|
165
|
+
|
|
166
|
+
const options = availableProviders.slice(0, 25).map((provider) => {
|
|
167
|
+
const modelCount = Object.keys(provider.models || {}).length
|
|
168
|
+
return {
|
|
169
|
+
label: provider.name.slice(0, 100),
|
|
170
|
+
value: provider.id,
|
|
171
|
+
description: `${modelCount} model${modelCount !== 1 ? 's' : ''} available`.slice(0, 100),
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
176
|
+
.setCustomId(`model_provider:${contextHash}`)
|
|
177
|
+
.setPlaceholder('Select a provider')
|
|
178
|
+
.addOptions(options)
|
|
179
|
+
|
|
180
|
+
const actionRow = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
|
|
181
|
+
|
|
182
|
+
await interaction.editReply({
|
|
183
|
+
content: '**Set Model Preference**\nSelect a provider:',
|
|
184
|
+
components: [actionRow],
|
|
185
|
+
})
|
|
186
|
+
} catch (error) {
|
|
187
|
+
modelLogger.error('Error loading providers:', error)
|
|
188
|
+
await interaction.editReply({
|
|
189
|
+
content: `Failed to load providers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Handle the provider select menu interaction.
|
|
196
|
+
* Shows a second select menu with models for the chosen provider.
|
|
197
|
+
*/
|
|
198
|
+
export async function handleProviderSelectMenu(
|
|
199
|
+
interaction: StringSelectMenuInteraction
|
|
200
|
+
): Promise<void> {
|
|
201
|
+
const customId = interaction.customId
|
|
202
|
+
|
|
203
|
+
if (!customId.startsWith('model_provider:')) {
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Defer update immediately to avoid timeout
|
|
208
|
+
await interaction.deferUpdate()
|
|
209
|
+
|
|
210
|
+
const contextHash = customId.replace('model_provider:', '')
|
|
211
|
+
const context = pendingModelContexts.get(contextHash)
|
|
212
|
+
|
|
213
|
+
if (!context) {
|
|
214
|
+
await interaction.editReply({
|
|
215
|
+
content: 'Selection expired. Please run /model again.',
|
|
216
|
+
components: [],
|
|
217
|
+
})
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const selectedProviderId = interaction.values[0]
|
|
222
|
+
if (!selectedProviderId) {
|
|
223
|
+
await interaction.editReply({
|
|
224
|
+
content: 'No provider selected',
|
|
225
|
+
components: [],
|
|
226
|
+
})
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const getClient = await initializeOpencodeForDirectory(context.dir)
|
|
232
|
+
|
|
233
|
+
const providersResponse = await getClient().provider.list({
|
|
234
|
+
query: { directory: context.dir },
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
if (!providersResponse.data) {
|
|
238
|
+
await interaction.editReply({
|
|
239
|
+
content: 'Failed to fetch providers',
|
|
240
|
+
components: [],
|
|
241
|
+
})
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const provider = providersResponse.data.all.find((p) => p.id === selectedProviderId)
|
|
246
|
+
|
|
247
|
+
if (!provider) {
|
|
248
|
+
await interaction.editReply({
|
|
249
|
+
content: 'Provider not found',
|
|
250
|
+
components: [],
|
|
251
|
+
})
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const models = Object.entries(provider.models || {})
|
|
256
|
+
.map(([modelId, model]) => ({
|
|
257
|
+
id: modelId,
|
|
258
|
+
name: model.name,
|
|
259
|
+
releaseDate: model.release_date,
|
|
260
|
+
}))
|
|
261
|
+
// Sort by release date descending (most recent first)
|
|
262
|
+
.sort((a, b) => {
|
|
263
|
+
const dateA = a.releaseDate ? new Date(a.releaseDate).getTime() : 0
|
|
264
|
+
const dateB = b.releaseDate ? new Date(b.releaseDate).getTime() : 0
|
|
265
|
+
return dateB - dateA
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
if (models.length === 0) {
|
|
269
|
+
await interaction.editReply({
|
|
270
|
+
content: `No models available for ${provider.name}`,
|
|
271
|
+
components: [],
|
|
272
|
+
})
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Take first 25 models (most recent since sorted descending)
|
|
277
|
+
const recentModels = models.slice(0, 25)
|
|
278
|
+
|
|
279
|
+
// Update context with provider info and reuse the same hash
|
|
280
|
+
context.providerId = selectedProviderId
|
|
281
|
+
context.providerName = provider.name
|
|
282
|
+
pendingModelContexts.set(contextHash, context)
|
|
283
|
+
|
|
284
|
+
const options = recentModels.map((model) => {
|
|
285
|
+
const dateStr = model.releaseDate
|
|
286
|
+
? new Date(model.releaseDate).toLocaleDateString()
|
|
287
|
+
: 'Unknown date'
|
|
288
|
+
return {
|
|
289
|
+
label: model.name.slice(0, 100),
|
|
290
|
+
value: model.id,
|
|
291
|
+
description: dateStr.slice(0, 100),
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
296
|
+
.setCustomId(`model_select:${contextHash}`)
|
|
297
|
+
.setPlaceholder('Select a model')
|
|
298
|
+
.addOptions(options)
|
|
299
|
+
|
|
300
|
+
const actionRow = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
|
|
301
|
+
|
|
302
|
+
await interaction.editReply({
|
|
303
|
+
content: `**Set Model Preference**\nProvider: **${provider.name}**\nSelect a model:`,
|
|
304
|
+
components: [actionRow],
|
|
305
|
+
})
|
|
306
|
+
} catch (error) {
|
|
307
|
+
modelLogger.error('Error loading models:', error)
|
|
308
|
+
await interaction.editReply({
|
|
309
|
+
content: `Failed to load models: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
310
|
+
components: [],
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Handle the model select menu interaction.
|
|
317
|
+
* Stores the model preference in the database.
|
|
318
|
+
*/
|
|
319
|
+
export async function handleModelSelectMenu(
|
|
320
|
+
interaction: StringSelectMenuInteraction
|
|
321
|
+
): Promise<void> {
|
|
322
|
+
const customId = interaction.customId
|
|
323
|
+
|
|
324
|
+
if (!customId.startsWith('model_select:')) {
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Defer update immediately
|
|
329
|
+
await interaction.deferUpdate()
|
|
330
|
+
|
|
331
|
+
const contextHash = customId.replace('model_select:', '')
|
|
332
|
+
const context = pendingModelContexts.get(contextHash)
|
|
333
|
+
|
|
334
|
+
if (!context || !context.providerId || !context.providerName) {
|
|
335
|
+
await interaction.editReply({
|
|
336
|
+
content: 'Selection expired. Please run /model again.',
|
|
337
|
+
components: [],
|
|
338
|
+
})
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const selectedModelId = interaction.values[0]
|
|
343
|
+
if (!selectedModelId) {
|
|
344
|
+
await interaction.editReply({
|
|
345
|
+
content: 'No model selected',
|
|
346
|
+
components: [],
|
|
347
|
+
})
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Build full model ID: provider_id/model_id
|
|
352
|
+
const fullModelId = `${context.providerId}/${selectedModelId}`
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
// Store in appropriate table based on context
|
|
356
|
+
if (context.isThread && context.sessionId) {
|
|
357
|
+
// Store for session
|
|
358
|
+
setSessionModel(context.sessionId, fullModelId)
|
|
359
|
+
modelLogger.log(`Set model ${fullModelId} for session ${context.sessionId}`)
|
|
360
|
+
|
|
361
|
+
// Check if there's a running request and abort+retry with new model
|
|
362
|
+
let retried = false
|
|
363
|
+
if (context.thread) {
|
|
364
|
+
retried = await abortAndRetrySession({
|
|
365
|
+
sessionId: context.sessionId,
|
|
366
|
+
thread: context.thread,
|
|
367
|
+
projectDirectory: context.dir,
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (retried) {
|
|
372
|
+
await interaction.editReply({
|
|
373
|
+
content: `Model changed for this session:\n**${context.providerName}** / **${selectedModelId}**\n\n\`${fullModelId}\`\n\n_Retrying current request with new model..._`,
|
|
374
|
+
components: [],
|
|
375
|
+
})
|
|
376
|
+
} else {
|
|
377
|
+
await interaction.editReply({
|
|
378
|
+
content: `Model preference set for this session:\n**${context.providerName}** / **${selectedModelId}**\n\n\`${fullModelId}\``,
|
|
379
|
+
components: [],
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
// Store for channel
|
|
384
|
+
setChannelModel(context.channelId, fullModelId)
|
|
385
|
+
modelLogger.log(`Set model ${fullModelId} for channel ${context.channelId}`)
|
|
386
|
+
|
|
387
|
+
await interaction.editReply({
|
|
388
|
+
content: `Model preference set for this channel:\n**${context.providerName}** / **${selectedModelId}**\n\n\`${fullModelId}\`\n\nAll new sessions in this channel will use this model.`,
|
|
389
|
+
components: [],
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Clean up the context from memory
|
|
394
|
+
pendingModelContexts.delete(contextHash)
|
|
395
|
+
} catch (error) {
|
|
396
|
+
modelLogger.error('Error saving model preference:', error)
|
|
397
|
+
await interaction.editReply({
|
|
398
|
+
content: `Failed to save model preference: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
399
|
+
components: [],
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Permission commands - /accept, /accept-always, /reject
|
|
2
|
+
|
|
3
|
+
import { ChannelType } from 'discord.js'
|
|
4
|
+
import type { CommandContext } from './types.js'
|
|
5
|
+
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
6
|
+
import { pendingPermissions } from '../session-handler.js'
|
|
7
|
+
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
8
|
+
import { createLogger } from '../logger.js'
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('PERMISSIONS')
|
|
11
|
+
|
|
12
|
+
export async function handleAcceptCommand({
|
|
13
|
+
command,
|
|
14
|
+
}: CommandContext): Promise<void> {
|
|
15
|
+
const scope = command.commandName === 'accept-always' ? 'always' : 'once'
|
|
16
|
+
const channel = command.channel
|
|
17
|
+
|
|
18
|
+
if (!channel) {
|
|
19
|
+
await command.reply({
|
|
20
|
+
content: 'This command can only be used in a channel',
|
|
21
|
+
ephemeral: true,
|
|
22
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
23
|
+
})
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const isThread = [
|
|
28
|
+
ChannelType.PublicThread,
|
|
29
|
+
ChannelType.PrivateThread,
|
|
30
|
+
ChannelType.AnnouncementThread,
|
|
31
|
+
].includes(channel.type)
|
|
32
|
+
|
|
33
|
+
if (!isThread) {
|
|
34
|
+
await command.reply({
|
|
35
|
+
content: 'This command can only be used in a thread with an active session',
|
|
36
|
+
ephemeral: true,
|
|
37
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
38
|
+
})
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const pending = pendingPermissions.get(channel.id)
|
|
43
|
+
if (!pending) {
|
|
44
|
+
await command.reply({
|
|
45
|
+
content: 'No pending permission request in this thread',
|
|
46
|
+
ephemeral: true,
|
|
47
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
48
|
+
})
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const getClient = await initializeOpencodeForDirectory(pending.directory)
|
|
54
|
+
await getClient().postSessionIdPermissionsPermissionId({
|
|
55
|
+
path: {
|
|
56
|
+
id: pending.permission.sessionID,
|
|
57
|
+
permissionID: pending.permission.id,
|
|
58
|
+
},
|
|
59
|
+
body: {
|
|
60
|
+
response: scope,
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
pendingPermissions.delete(channel.id)
|
|
65
|
+
const msg =
|
|
66
|
+
scope === 'always'
|
|
67
|
+
? `✅ Permission **accepted** (auto-approve similar requests)`
|
|
68
|
+
: `✅ Permission **accepted**`
|
|
69
|
+
await command.reply({ content: msg, flags: SILENT_MESSAGE_FLAGS })
|
|
70
|
+
logger.log(`Permission ${pending.permission.id} accepted with scope: ${scope}`)
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logger.error('[ACCEPT] Error:', error)
|
|
73
|
+
await command.reply({
|
|
74
|
+
content: `Failed to accept permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
75
|
+
ephemeral: true,
|
|
76
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function handleRejectCommand({
|
|
82
|
+
command,
|
|
83
|
+
}: CommandContext): Promise<void> {
|
|
84
|
+
const channel = command.channel
|
|
85
|
+
|
|
86
|
+
if (!channel) {
|
|
87
|
+
await command.reply({
|
|
88
|
+
content: 'This command can only be used in a channel',
|
|
89
|
+
ephemeral: true,
|
|
90
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
91
|
+
})
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const isThread = [
|
|
96
|
+
ChannelType.PublicThread,
|
|
97
|
+
ChannelType.PrivateThread,
|
|
98
|
+
ChannelType.AnnouncementThread,
|
|
99
|
+
].includes(channel.type)
|
|
100
|
+
|
|
101
|
+
if (!isThread) {
|
|
102
|
+
await command.reply({
|
|
103
|
+
content: 'This command can only be used in a thread with an active session',
|
|
104
|
+
ephemeral: true,
|
|
105
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
106
|
+
})
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const pending = pendingPermissions.get(channel.id)
|
|
111
|
+
if (!pending) {
|
|
112
|
+
await command.reply({
|
|
113
|
+
content: 'No pending permission request in this thread',
|
|
114
|
+
ephemeral: true,
|
|
115
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
116
|
+
})
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const getClient = await initializeOpencodeForDirectory(pending.directory)
|
|
122
|
+
await getClient().postSessionIdPermissionsPermissionId({
|
|
123
|
+
path: {
|
|
124
|
+
id: pending.permission.sessionID,
|
|
125
|
+
permissionID: pending.permission.id,
|
|
126
|
+
},
|
|
127
|
+
body: {
|
|
128
|
+
response: 'reject',
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
pendingPermissions.delete(channel.id)
|
|
133
|
+
await command.reply({
|
|
134
|
+
content: `❌ Permission **rejected**`,
|
|
135
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
136
|
+
})
|
|
137
|
+
logger.log(`Permission ${pending.permission.id} rejected`)
|
|
138
|
+
} catch (error) {
|
|
139
|
+
logger.error('[REJECT] Error:', error)
|
|
140
|
+
await command.reply({
|
|
141
|
+
content: `Failed to reject permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
142
|
+
ephemeral: true,
|
|
143
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|