kimaki 0.4.24 → 0.4.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin.js +6 -1
- package/dist/acp-client.test.js +149 -0
- package/dist/ai-tool-to-genai.js +3 -0
- package/dist/channel-management.js +14 -9
- package/dist/cli.js +148 -17
- package/dist/commands/abort.js +78 -0
- package/dist/commands/add-project.js +98 -0
- package/dist/commands/agent.js +152 -0
- package/dist/commands/ask-question.js +183 -0
- package/dist/commands/create-new-project.js +78 -0
- package/dist/commands/fork.js +186 -0
- package/dist/commands/model.js +313 -0
- package/dist/commands/permissions.js +126 -0
- package/dist/commands/queue.js +129 -0
- package/dist/commands/resume.js +145 -0
- package/dist/commands/session.js +142 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +161 -0
- package/dist/commands/user-command.js +145 -0
- package/dist/database.js +54 -0
- package/dist/discord-bot.js +35 -32
- package/dist/discord-utils.js +81 -15
- package/dist/format-tables.js +3 -0
- package/dist/genai-worker-wrapper.js +3 -0
- package/dist/genai-worker.js +3 -0
- package/dist/genai.js +3 -0
- package/dist/interaction-handler.js +89 -695
- package/dist/logger.js +46 -5
- package/dist/markdown.js +107 -0
- package/dist/markdown.test.js +31 -1
- package/dist/message-formatting.js +113 -28
- package/dist/message-formatting.test.js +73 -0
- package/dist/opencode.js +73 -16
- package/dist/session-handler.js +176 -63
- package/dist/system-message.js +7 -38
- package/dist/tools.js +3 -0
- package/dist/utils.js +3 -0
- package/dist/voice-handler.js +21 -8
- package/dist/voice.js +31 -12
- package/dist/worker-types.js +3 -0
- package/dist/xml.js +3 -0
- package/package.json +3 -3
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +47 -0
- package/src/ai-tool-to-genai.ts +4 -0
- package/src/channel-management.ts +24 -8
- package/src/cli.ts +163 -18
- package/src/commands/abort.ts +94 -0
- package/src/commands/add-project.ts +139 -0
- package/src/commands/agent.ts +201 -0
- package/src/commands/ask-question.ts +276 -0
- package/src/commands/create-new-project.ts +111 -0
- package/src/{fork.ts → commands/fork.ts} +40 -7
- package/src/{model-command.ts → commands/model.ts} +31 -9
- package/src/commands/permissions.ts +146 -0
- package/src/commands/queue.ts +181 -0
- package/src/commands/resume.ts +230 -0
- package/src/commands/session.ts +184 -0
- package/src/commands/share.ts +96 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +213 -0
- package/src/commands/user-command.ts +178 -0
- package/src/database.ts +65 -0
- package/src/discord-bot.ts +40 -33
- package/src/discord-utils.ts +88 -14
- package/src/format-tables.ts +4 -0
- package/src/genai-worker-wrapper.ts +4 -0
- package/src/genai-worker.ts +4 -0
- package/src/genai.ts +4 -0
- package/src/interaction-handler.ts +111 -924
- package/src/logger.ts +51 -10
- package/src/markdown.test.ts +45 -1
- package/src/markdown.ts +136 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +143 -30
- package/src/opencode.ts +84 -21
- package/src/session-handler.ts +248 -91
- package/src/system-message.ts +8 -38
- package/src/tools.ts +4 -0
- package/src/utils.ts +4 -0
- package/src/voice-handler.ts +24 -9
- package/src/voice.ts +36 -13
- package/src/worker-types.ts +4 -0
- package/src/xml.ts +4 -0
- package/README.md +0 -48
|
@@ -1,43 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from '
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from './discord-utils.js'
|
|
21
|
-
import { handleForkCommand, handleForkSelectMenu } from './fork.js'
|
|
22
|
-
import {
|
|
23
|
-
handleModelCommand,
|
|
24
|
-
handleProviderSelectMenu,
|
|
25
|
-
handleModelSelectMenu,
|
|
26
|
-
} from './model-command.js'
|
|
27
|
-
import { formatPart } from './message-formatting.js'
|
|
28
|
-
import { createProjectChannels } from './channel-management.js'
|
|
29
|
-
import {
|
|
30
|
-
handleOpencodeSession,
|
|
31
|
-
parseSlashCommand,
|
|
32
|
-
abortControllers,
|
|
33
|
-
pendingPermissions,
|
|
34
|
-
} from './session-handler.js'
|
|
35
|
-
import { extractTagsArrays } from './xml.js'
|
|
1
|
+
// Discord slash command and interaction handler.
|
|
2
|
+
// Processes all slash commands (/session, /resume, /fork, /model, /abort, etc.)
|
|
3
|
+
// and manages autocomplete, select menu interactions for the bot.
|
|
4
|
+
|
|
5
|
+
import { Events, type Client, type Interaction } from 'discord.js'
|
|
6
|
+
import { handleSessionCommand, handleSessionAutocomplete } from './commands/session.js'
|
|
7
|
+
import { handleResumeCommand, handleResumeAutocomplete } from './commands/resume.js'
|
|
8
|
+
import { handleAddProjectCommand, handleAddProjectAutocomplete } from './commands/add-project.js'
|
|
9
|
+
import { handleCreateNewProjectCommand } from './commands/create-new-project.js'
|
|
10
|
+
import { handleAcceptCommand, handleRejectCommand } from './commands/permissions.js'
|
|
11
|
+
import { handleAbortCommand } from './commands/abort.js'
|
|
12
|
+
import { handleShareCommand } from './commands/share.js'
|
|
13
|
+
import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js'
|
|
14
|
+
import { handleModelCommand, handleProviderSelectMenu, handleModelSelectMenu } from './commands/model.js'
|
|
15
|
+
import { handleAgentCommand, handleAgentSelectMenu } from './commands/agent.js'
|
|
16
|
+
import { handleAskQuestionSelectMenu } from './commands/ask-question.js'
|
|
17
|
+
import { handleQueueCommand, handleClearQueueCommand } from './commands/queue.js'
|
|
18
|
+
import { handleUndoCommand, handleRedoCommand } from './commands/undo-redo.js'
|
|
19
|
+
import { handleUserCommand } from './commands/user-command.js'
|
|
36
20
|
import { createLogger } from './logger.js'
|
|
37
21
|
|
|
38
|
-
const discordLogger = createLogger('DISCORD')
|
|
39
22
|
const interactionLogger = createLogger('INTERACTION')
|
|
40
23
|
|
|
24
|
+
|
|
41
25
|
export function registerInteractionHandler({
|
|
42
26
|
discordClient,
|
|
43
27
|
appId,
|
|
@@ -51,939 +35,142 @@ export function registerInteractionHandler({
|
|
|
51
35
|
Events.InteractionCreate,
|
|
52
36
|
async (interaction: Interaction) => {
|
|
53
37
|
try {
|
|
54
|
-
interactionLogger.log(
|
|
38
|
+
interactionLogger.log(
|
|
39
|
+
`[INTERACTION] Received: ${interaction.type} - ${
|
|
40
|
+
interaction.isChatInputCommand()
|
|
41
|
+
? interaction.commandName
|
|
42
|
+
: interaction.isAutocomplete()
|
|
43
|
+
? `autocomplete:${interaction.commandName}`
|
|
44
|
+
: 'other'
|
|
45
|
+
}`,
|
|
46
|
+
)
|
|
55
47
|
|
|
56
48
|
if (interaction.isAutocomplete()) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
let projectDirectory: string | undefined
|
|
61
|
-
if (interaction.channel) {
|
|
62
|
-
const textChannel = await resolveTextChannel(
|
|
63
|
-
interaction.channel as TextChannel | ThreadChannel | null,
|
|
64
|
-
)
|
|
65
|
-
if (textChannel) {
|
|
66
|
-
const { projectDirectory: directory, channelAppId } =
|
|
67
|
-
getKimakiMetadata(textChannel)
|
|
68
|
-
if (channelAppId && channelAppId !== appId) {
|
|
69
|
-
await interaction.respond([])
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
projectDirectory = directory
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!projectDirectory) {
|
|
77
|
-
await interaction.respond([])
|
|
49
|
+
switch (interaction.commandName) {
|
|
50
|
+
case 'session':
|
|
51
|
+
await handleSessionAutocomplete({ interaction, appId })
|
|
78
52
|
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const existingSessionIds = new Set(
|
|
92
|
-
(
|
|
93
|
-
getDatabase()
|
|
94
|
-
.prepare('SELECT session_id FROM thread_sessions')
|
|
95
|
-
.all() as { session_id: string }[]
|
|
96
|
-
).map((row) => row.session_id),
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
const sessions = sessionsResponse.data
|
|
100
|
-
.filter((session) => !existingSessionIds.has(session.id))
|
|
101
|
-
.filter((session) =>
|
|
102
|
-
session.title
|
|
103
|
-
.toLowerCase()
|
|
104
|
-
.includes(focusedValue.toLowerCase()),
|
|
105
|
-
)
|
|
106
|
-
.slice(0, 25)
|
|
107
|
-
.map((session) => {
|
|
108
|
-
const dateStr = new Date(
|
|
109
|
-
session.time.updated,
|
|
110
|
-
).toLocaleString()
|
|
111
|
-
const suffix = ` (${dateStr})`
|
|
112
|
-
const maxTitleLength = 100 - suffix.length
|
|
113
|
-
|
|
114
|
-
let title = session.title
|
|
115
|
-
if (title.length > maxTitleLength) {
|
|
116
|
-
title = title.slice(0, Math.max(0, maxTitleLength - 1)) + '…'
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
name: `${title}${suffix}`,
|
|
121
|
-
value: session.id,
|
|
122
|
-
}
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
await interaction.respond(sessions)
|
|
126
|
-
} catch (error) {
|
|
127
|
-
interactionLogger.error(
|
|
128
|
-
'[AUTOCOMPLETE] Error fetching sessions:',
|
|
129
|
-
error,
|
|
130
|
-
)
|
|
131
|
-
await interaction.respond([])
|
|
132
|
-
}
|
|
133
|
-
} else if (interaction.commandName === 'session') {
|
|
134
|
-
const focusedOption = interaction.options.getFocused(true)
|
|
135
|
-
|
|
136
|
-
if (focusedOption.name === 'files') {
|
|
137
|
-
const focusedValue = focusedOption.value
|
|
138
|
-
|
|
139
|
-
const parts = focusedValue.split(',')
|
|
140
|
-
const previousFiles = parts
|
|
141
|
-
.slice(0, -1)
|
|
142
|
-
.map((f) => f.trim())
|
|
143
|
-
.filter((f) => f)
|
|
144
|
-
const currentQuery = (parts[parts.length - 1] || '').trim()
|
|
145
|
-
|
|
146
|
-
let projectDirectory: string | undefined
|
|
147
|
-
if (interaction.channel) {
|
|
148
|
-
const textChannel = await resolveTextChannel(
|
|
149
|
-
interaction.channel as TextChannel | ThreadChannel | null,
|
|
150
|
-
)
|
|
151
|
-
if (textChannel) {
|
|
152
|
-
const { projectDirectory: directory, channelAppId } =
|
|
153
|
-
getKimakiMetadata(textChannel)
|
|
154
|
-
if (channelAppId && channelAppId !== appId) {
|
|
155
|
-
await interaction.respond([])
|
|
156
|
-
return
|
|
157
|
-
}
|
|
158
|
-
projectDirectory = directory
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (!projectDirectory) {
|
|
163
|
-
await interaction.respond([])
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
const getClient =
|
|
169
|
-
await initializeOpencodeForDirectory(projectDirectory)
|
|
170
|
-
|
|
171
|
-
const response = await getClient().find.files({
|
|
172
|
-
query: {
|
|
173
|
-
query: currentQuery || '',
|
|
174
|
-
},
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
const files = response.data || []
|
|
178
|
-
|
|
179
|
-
const prefix =
|
|
180
|
-
previousFiles.length > 0
|
|
181
|
-
? previousFiles.join(', ') + ', '
|
|
182
|
-
: ''
|
|
183
|
-
|
|
184
|
-
const choices = files
|
|
185
|
-
.map((file: string) => {
|
|
186
|
-
const fullValue = prefix + file
|
|
187
|
-
const allFiles = [...previousFiles, file]
|
|
188
|
-
const allBasenames = allFiles.map(
|
|
189
|
-
(f) => f.split('/').pop() || f,
|
|
190
|
-
)
|
|
191
|
-
let displayName = allBasenames.join(', ')
|
|
192
|
-
if (displayName.length > 100) {
|
|
193
|
-
displayName = '…' + displayName.slice(-97)
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
name: displayName,
|
|
197
|
-
value: fullValue,
|
|
198
|
-
}
|
|
199
|
-
})
|
|
200
|
-
.filter((choice) => choice.value.length <= 100)
|
|
201
|
-
.slice(0, 25)
|
|
202
|
-
|
|
203
|
-
await interaction.respond(choices)
|
|
204
|
-
} catch (error) {
|
|
205
|
-
interactionLogger.error('[AUTOCOMPLETE] Error fetching files:', error)
|
|
206
|
-
await interaction.respond([])
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
} else if (interaction.commandName === 'add-project') {
|
|
210
|
-
const focusedValue = interaction.options.getFocused()
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
const currentDir = process.cwd()
|
|
214
|
-
const getClient = await initializeOpencodeForDirectory(currentDir)
|
|
215
|
-
|
|
216
|
-
const projectsResponse = await getClient().project.list({})
|
|
217
|
-
if (!projectsResponse.data) {
|
|
218
|
-
await interaction.respond([])
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const db = getDatabase()
|
|
223
|
-
const existingDirs = db
|
|
224
|
-
.prepare(
|
|
225
|
-
'SELECT DISTINCT directory FROM channel_directories WHERE channel_type = ?',
|
|
226
|
-
)
|
|
227
|
-
.all('text') as { directory: string }[]
|
|
228
|
-
const existingDirSet = new Set(
|
|
229
|
-
existingDirs.map((row) => row.directory),
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
const availableProjects = projectsResponse.data.filter(
|
|
233
|
-
(project) => !existingDirSet.has(project.worktree),
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
const projects = availableProjects
|
|
237
|
-
.filter((project) => {
|
|
238
|
-
const baseName = path.basename(project.worktree)
|
|
239
|
-
const searchText = `${baseName} ${project.worktree}`.toLowerCase()
|
|
240
|
-
return searchText.includes(focusedValue.toLowerCase())
|
|
241
|
-
})
|
|
242
|
-
.sort((a, b) => {
|
|
243
|
-
const aTime = a.time.initialized || a.time.created
|
|
244
|
-
const bTime = b.time.initialized || b.time.created
|
|
245
|
-
return bTime - aTime
|
|
246
|
-
})
|
|
247
|
-
.slice(0, 25)
|
|
248
|
-
.map((project) => {
|
|
249
|
-
const name = `${path.basename(project.worktree)} (${project.worktree})`
|
|
250
|
-
return {
|
|
251
|
-
name: name.length > 100 ? name.slice(0, 99) + '…' : name,
|
|
252
|
-
value: project.id,
|
|
253
|
-
}
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
await interaction.respond(projects)
|
|
257
|
-
} catch (error) {
|
|
258
|
-
interactionLogger.error(
|
|
259
|
-
'[AUTOCOMPLETE] Error fetching projects:',
|
|
260
|
-
error,
|
|
261
|
-
)
|
|
53
|
+
|
|
54
|
+
case 'resume':
|
|
55
|
+
await handleResumeAutocomplete({ interaction, appId })
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
case 'add-project':
|
|
59
|
+
await handleAddProjectAutocomplete({ interaction, appId })
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
default:
|
|
262
63
|
await interaction.respond([])
|
|
263
|
-
|
|
64
|
+
return
|
|
264
65
|
}
|
|
265
66
|
}
|
|
266
67
|
|
|
267
68
|
if (interaction.isChatInputCommand()) {
|
|
268
|
-
|
|
269
|
-
interactionLogger.log(`[COMMAND] Processing: ${command.commandName}`)
|
|
69
|
+
interactionLogger.log(`[COMMAND] Processing: ${interaction.commandName}`)
|
|
270
70
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const prompt = command.options.getString('prompt', true)
|
|
275
|
-
const filesString = command.options.getString('files') || ''
|
|
276
|
-
const channel = command.channel
|
|
277
|
-
|
|
278
|
-
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
279
|
-
await command.editReply(
|
|
280
|
-
'This command can only be used in text channels',
|
|
281
|
-
)
|
|
71
|
+
switch (interaction.commandName) {
|
|
72
|
+
case 'session':
|
|
73
|
+
await handleSessionCommand({ command: interaction, appId })
|
|
282
74
|
return
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const textChannel = channel as TextChannel
|
|
286
75
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (textChannel.topic) {
|
|
291
|
-
const extracted = extractTagsArrays({
|
|
292
|
-
xml: textChannel.topic,
|
|
293
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
297
|
-
channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (channelAppId && channelAppId !== appId) {
|
|
301
|
-
await command.editReply(
|
|
302
|
-
'This channel is not configured for this bot',
|
|
303
|
-
)
|
|
76
|
+
case 'resume':
|
|
77
|
+
await handleResumeCommand({ command: interaction, appId })
|
|
304
78
|
return
|
|
305
|
-
}
|
|
306
79
|
|
|
307
|
-
|
|
308
|
-
await command
|
|
309
|
-
'This channel is not configured with a project directory',
|
|
310
|
-
)
|
|
80
|
+
case 'add-project':
|
|
81
|
+
await handleAddProjectCommand({ command: interaction, appId })
|
|
311
82
|
return
|
|
312
|
-
}
|
|
313
83
|
|
|
314
|
-
|
|
315
|
-
await command
|
|
316
|
-
`Directory does not exist: ${projectDirectory}`,
|
|
317
|
-
)
|
|
84
|
+
case 'create-new-project':
|
|
85
|
+
await handleCreateNewProjectCommand({ command: interaction, appId })
|
|
318
86
|
return
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
try {
|
|
322
|
-
const getClient =
|
|
323
|
-
await initializeOpencodeForDirectory(projectDirectory)
|
|
324
|
-
|
|
325
|
-
const files = filesString
|
|
326
|
-
.split(',')
|
|
327
|
-
.map((f) => f.trim())
|
|
328
|
-
.filter((f) => f)
|
|
329
|
-
|
|
330
|
-
let fullPrompt = prompt
|
|
331
|
-
if (files.length > 0) {
|
|
332
|
-
fullPrompt = `${prompt}\n\n@${files.join(' @')}`
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const starterMessage = await textChannel.send({
|
|
336
|
-
content: `🚀 **Starting OpenCode session**\n📝 ${prompt.slice(0, 200)}${prompt.length > 200 ? '…' : ''}${files.length > 0 ? `\n📎 Files: ${files.join(', ')}` : ''}`,
|
|
337
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
const thread = await starterMessage.startThread({
|
|
341
|
-
name: prompt.slice(0, 100),
|
|
342
|
-
autoArchiveDuration: 1440,
|
|
343
|
-
reason: 'OpenCode session',
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
await command.editReply(
|
|
347
|
-
`Created new session in ${thread.toString()}`,
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
const parsedCommand = parseSlashCommand(fullPrompt)
|
|
351
|
-
await handleOpencodeSession({
|
|
352
|
-
prompt: fullPrompt,
|
|
353
|
-
thread,
|
|
354
|
-
projectDirectory,
|
|
355
|
-
parsedCommand,
|
|
356
|
-
channelId: textChannel.id,
|
|
357
|
-
})
|
|
358
|
-
} catch (error) {
|
|
359
|
-
interactionLogger.error('[SESSION] Error:', error)
|
|
360
|
-
await command.editReply(
|
|
361
|
-
`Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
362
|
-
)
|
|
363
|
-
}
|
|
364
|
-
} else if (command.commandName === 'resume') {
|
|
365
|
-
await command.deferReply({ ephemeral: false })
|
|
366
|
-
|
|
367
|
-
const sessionId = command.options.getString('session', true)
|
|
368
|
-
const channel = command.channel
|
|
369
|
-
|
|
370
|
-
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
371
|
-
await command.editReply(
|
|
372
|
-
'This command can only be used in text channels',
|
|
373
|
-
)
|
|
374
|
-
return
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const textChannel = channel as TextChannel
|
|
378
|
-
|
|
379
|
-
let projectDirectory: string | undefined
|
|
380
|
-
let channelAppId: string | undefined
|
|
381
87
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
389
|
-
channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (channelAppId && channelAppId !== appId) {
|
|
393
|
-
await command.editReply(
|
|
394
|
-
'This channel is not configured for this bot',
|
|
395
|
-
)
|
|
88
|
+
case 'accept':
|
|
89
|
+
case 'accept-always':
|
|
90
|
+
await handleAcceptCommand({ command: interaction, appId })
|
|
396
91
|
return
|
|
397
|
-
}
|
|
398
92
|
|
|
399
|
-
|
|
400
|
-
await command
|
|
401
|
-
'This channel is not configured with a project directory',
|
|
402
|
-
)
|
|
93
|
+
case 'reject':
|
|
94
|
+
await handleRejectCommand({ command: interaction, appId })
|
|
403
95
|
return
|
|
404
|
-
}
|
|
405
96
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
)
|
|
410
|
-
return
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
try {
|
|
414
|
-
const getClient =
|
|
415
|
-
await initializeOpencodeForDirectory(projectDirectory)
|
|
416
|
-
|
|
417
|
-
const sessionResponse = await getClient().session.get({
|
|
418
|
-
path: { id: sessionId },
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
if (!sessionResponse.data) {
|
|
422
|
-
await command.editReply('Session not found')
|
|
423
|
-
return
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const sessionTitle = sessionResponse.data.title
|
|
427
|
-
|
|
428
|
-
const thread = await textChannel.threads.create({
|
|
429
|
-
name: `Resume: ${sessionTitle}`.slice(0, 100),
|
|
430
|
-
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
431
|
-
reason: `Resuming session ${sessionId}`,
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
getDatabase()
|
|
435
|
-
.prepare(
|
|
436
|
-
'INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)',
|
|
437
|
-
)
|
|
438
|
-
.run(thread.id, sessionId)
|
|
439
|
-
|
|
440
|
-
interactionLogger.log(
|
|
441
|
-
`[RESUME] Created thread ${thread.id} for session ${sessionId}`,
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
const messagesResponse = await getClient().session.messages({
|
|
445
|
-
path: { id: sessionId },
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
if (!messagesResponse.data) {
|
|
449
|
-
throw new Error('Failed to fetch session messages')
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const messages = messagesResponse.data
|
|
453
|
-
|
|
454
|
-
await command.editReply(
|
|
455
|
-
`Resumed session "${sessionTitle}" in ${thread.toString()}`,
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
await sendThreadMessage(
|
|
459
|
-
thread,
|
|
460
|
-
`📂 **Resumed session:** ${sessionTitle}\n📅 **Created:** ${new Date(sessionResponse.data.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`,
|
|
461
|
-
)
|
|
462
|
-
|
|
463
|
-
const allAssistantParts: { id: string; content: string }[] = []
|
|
464
|
-
for (const message of messages) {
|
|
465
|
-
if (message.info.role === 'assistant') {
|
|
466
|
-
for (const part of message.parts) {
|
|
467
|
-
const content = formatPart(part)
|
|
468
|
-
if (content.trim()) {
|
|
469
|
-
allAssistantParts.push({ id: part.id, content })
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const partsToRender = allAssistantParts.slice(-30)
|
|
476
|
-
const skippedCount = allAssistantParts.length - partsToRender.length
|
|
477
|
-
|
|
478
|
-
if (skippedCount > 0) {
|
|
479
|
-
await sendThreadMessage(
|
|
480
|
-
thread,
|
|
481
|
-
`*Skipped ${skippedCount} older assistant parts...*`,
|
|
482
|
-
)
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (partsToRender.length > 0) {
|
|
486
|
-
const combinedContent = partsToRender
|
|
487
|
-
.map((p) => p.content)
|
|
488
|
-
.join('\n')
|
|
489
|
-
|
|
490
|
-
const discordMessage = await sendThreadMessage(
|
|
491
|
-
thread,
|
|
492
|
-
combinedContent,
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
const stmt = getDatabase().prepare(
|
|
496
|
-
'INSERT OR REPLACE INTO part_messages (part_id, message_id, thread_id) VALUES (?, ?, ?)',
|
|
497
|
-
)
|
|
498
|
-
|
|
499
|
-
const transaction = getDatabase().transaction(
|
|
500
|
-
(parts: { id: string }[]) => {
|
|
501
|
-
for (const part of parts) {
|
|
502
|
-
stmt.run(part.id, discordMessage.id, thread.id)
|
|
503
|
-
}
|
|
504
|
-
},
|
|
505
|
-
)
|
|
506
|
-
|
|
507
|
-
transaction(partsToRender)
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
const messageCount = messages.length
|
|
511
|
-
|
|
512
|
-
await sendThreadMessage(
|
|
513
|
-
thread,
|
|
514
|
-
`✅ **Session resumed!** Loaded ${messageCount} messages.\n\nYou can now continue the conversation by sending messages in this thread.`,
|
|
515
|
-
)
|
|
516
|
-
} catch (error) {
|
|
517
|
-
interactionLogger.error('[RESUME] Error:', error)
|
|
518
|
-
await command.editReply(
|
|
519
|
-
`Failed to resume session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
520
|
-
)
|
|
521
|
-
}
|
|
522
|
-
} else if (command.commandName === 'add-project') {
|
|
523
|
-
await command.deferReply({ ephemeral: false })
|
|
524
|
-
|
|
525
|
-
const projectId = command.options.getString('project', true)
|
|
526
|
-
const guild = command.guild
|
|
527
|
-
|
|
528
|
-
if (!guild) {
|
|
529
|
-
await command.editReply('This command can only be used in a guild')
|
|
97
|
+
case 'abort':
|
|
98
|
+
case 'stop':
|
|
99
|
+
await handleAbortCommand({ command: interaction, appId })
|
|
530
100
|
return
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
try {
|
|
534
|
-
const currentDir = process.cwd()
|
|
535
|
-
const getClient = await initializeOpencodeForDirectory(currentDir)
|
|
536
|
-
|
|
537
|
-
const projectsResponse = await getClient().project.list({})
|
|
538
|
-
if (!projectsResponse.data) {
|
|
539
|
-
await command.editReply('Failed to fetch projects')
|
|
540
|
-
return
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
const project = projectsResponse.data.find(
|
|
544
|
-
(p) => p.id === projectId,
|
|
545
|
-
)
|
|
546
|
-
|
|
547
|
-
if (!project) {
|
|
548
|
-
await command.editReply('Project not found')
|
|
549
|
-
return
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const directory = project.worktree
|
|
553
|
-
|
|
554
|
-
if (!fs.existsSync(directory)) {
|
|
555
|
-
await command.editReply(`Directory does not exist: ${directory}`)
|
|
556
|
-
return
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const db = getDatabase()
|
|
560
|
-
const existingChannel = db
|
|
561
|
-
.prepare(
|
|
562
|
-
'SELECT channel_id FROM channel_directories WHERE directory = ? AND channel_type = ?',
|
|
563
|
-
)
|
|
564
|
-
.get(directory, 'text') as { channel_id: string } | undefined
|
|
565
|
-
|
|
566
|
-
if (existingChannel) {
|
|
567
|
-
await command.editReply(
|
|
568
|
-
`A channel already exists for this directory: <#${existingChannel.channel_id}>`,
|
|
569
|
-
)
|
|
570
|
-
return
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const { textChannelId, voiceChannelId, channelName } =
|
|
574
|
-
await createProjectChannels({
|
|
575
|
-
guild,
|
|
576
|
-
projectDirectory: directory,
|
|
577
|
-
appId,
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
await command.editReply(
|
|
581
|
-
`✅ Created channels for project:\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n📁 Directory: \`${directory}\``,
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
discordLogger.log(
|
|
585
|
-
`Created channels for project ${channelName} at ${directory}`,
|
|
586
|
-
)
|
|
587
|
-
} catch (error) {
|
|
588
|
-
interactionLogger.error('[ADD-PROJECT] Error:', error)
|
|
589
|
-
await command.editReply(
|
|
590
|
-
`Failed to create channels: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
591
|
-
)
|
|
592
|
-
}
|
|
593
|
-
} else if (command.commandName === 'create-new-project') {
|
|
594
|
-
await command.deferReply({ ephemeral: false })
|
|
595
|
-
|
|
596
|
-
const projectName = command.options.getString('name', true)
|
|
597
|
-
const guild = command.guild
|
|
598
|
-
const channel = command.channel
|
|
599
|
-
|
|
600
|
-
if (!guild) {
|
|
601
|
-
await command.editReply('This command can only be used in a guild')
|
|
602
|
-
return
|
|
603
|
-
}
|
|
604
101
|
|
|
605
|
-
|
|
606
|
-
await
|
|
102
|
+
case 'share':
|
|
103
|
+
await handleShareCommand({ command: interaction, appId })
|
|
607
104
|
return
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const sanitizedName = projectName
|
|
611
|
-
.toLowerCase()
|
|
612
|
-
.replace(/[^a-z0-9-]/g, '-')
|
|
613
|
-
.replace(/-+/g, '-')
|
|
614
|
-
.replace(/^-|-$/g, '')
|
|
615
|
-
.slice(0, 100)
|
|
616
105
|
|
|
617
|
-
|
|
618
|
-
await
|
|
619
|
-
return
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const kimakiDir = path.join(os.homedir(), 'kimaki')
|
|
623
|
-
const projectDirectory = path.join(kimakiDir, sanitizedName)
|
|
624
|
-
|
|
625
|
-
try {
|
|
626
|
-
if (!fs.existsSync(kimakiDir)) {
|
|
627
|
-
fs.mkdirSync(kimakiDir, { recursive: true })
|
|
628
|
-
discordLogger.log(`Created kimaki directory: ${kimakiDir}`)
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
if (fs.existsSync(projectDirectory)) {
|
|
632
|
-
await command.editReply(`Project directory already exists: ${projectDirectory}`)
|
|
633
|
-
return
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
fs.mkdirSync(projectDirectory, { recursive: true })
|
|
637
|
-
discordLogger.log(`Created project directory: ${projectDirectory}`)
|
|
638
|
-
|
|
639
|
-
const { execSync } = await import('node:child_process')
|
|
640
|
-
execSync('git init', { cwd: projectDirectory, stdio: 'pipe' })
|
|
641
|
-
discordLogger.log(`Initialized git in: ${projectDirectory}`)
|
|
642
|
-
|
|
643
|
-
const { textChannelId, voiceChannelId, channelName } =
|
|
644
|
-
await createProjectChannels({
|
|
645
|
-
guild,
|
|
646
|
-
projectDirectory,
|
|
647
|
-
appId,
|
|
648
|
-
})
|
|
649
|
-
|
|
650
|
-
const textChannel = await guild.channels.fetch(textChannelId) as TextChannel
|
|
651
|
-
|
|
652
|
-
await command.editReply(
|
|
653
|
-
`✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n\n_Starting session..._`,
|
|
654
|
-
)
|
|
655
|
-
|
|
656
|
-
const starterMessage = await textChannel.send({
|
|
657
|
-
content: `🚀 **New project initialized**\n📁 \`${projectDirectory}\``,
|
|
658
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
659
|
-
})
|
|
660
|
-
|
|
661
|
-
const thread = await starterMessage.startThread({
|
|
662
|
-
name: `Init: ${sanitizedName}`,
|
|
663
|
-
autoArchiveDuration: 1440,
|
|
664
|
-
reason: 'New project session',
|
|
665
|
-
})
|
|
666
|
-
|
|
667
|
-
await handleOpencodeSession({
|
|
668
|
-
prompt: 'The project was just initialized. Say hi and ask what the user wants to build.',
|
|
669
|
-
thread,
|
|
670
|
-
projectDirectory,
|
|
671
|
-
channelId: textChannel.id,
|
|
672
|
-
})
|
|
673
|
-
|
|
674
|
-
discordLogger.log(
|
|
675
|
-
`Created new project ${channelName} at ${projectDirectory}`,
|
|
676
|
-
)
|
|
677
|
-
} catch (error) {
|
|
678
|
-
interactionLogger.error('[ADD-NEW-PROJECT] Error:', error)
|
|
679
|
-
await command.editReply(
|
|
680
|
-
`Failed to create new project: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
681
|
-
)
|
|
682
|
-
}
|
|
683
|
-
} else if (
|
|
684
|
-
command.commandName === 'accept' ||
|
|
685
|
-
command.commandName === 'accept-always'
|
|
686
|
-
) {
|
|
687
|
-
const scope = command.commandName === 'accept-always' ? 'always' : 'once'
|
|
688
|
-
const channel = command.channel
|
|
689
|
-
|
|
690
|
-
if (!channel) {
|
|
691
|
-
await command.reply({
|
|
692
|
-
content: 'This command can only be used in a channel',
|
|
693
|
-
ephemeral: true,
|
|
694
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
695
|
-
})
|
|
106
|
+
case 'fork':
|
|
107
|
+
await handleForkCommand(interaction)
|
|
696
108
|
return
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
const isThread = [
|
|
700
|
-
ChannelType.PublicThread,
|
|
701
|
-
ChannelType.PrivateThread,
|
|
702
|
-
ChannelType.AnnouncementThread,
|
|
703
|
-
].includes(channel.type)
|
|
704
|
-
|
|
705
|
-
if (!isThread) {
|
|
706
|
-
await command.reply({
|
|
707
|
-
content: 'This command can only be used in a thread with an active session',
|
|
708
|
-
ephemeral: true,
|
|
709
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
710
|
-
})
|
|
711
|
-
return
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
const pending = pendingPermissions.get(channel.id)
|
|
715
|
-
if (!pending) {
|
|
716
|
-
await command.reply({
|
|
717
|
-
content: 'No pending permission request in this thread',
|
|
718
|
-
ephemeral: true,
|
|
719
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
720
|
-
})
|
|
721
|
-
return
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
try {
|
|
725
|
-
const getClient = await initializeOpencodeForDirectory(pending.directory)
|
|
726
|
-
await getClient().postSessionIdPermissionsPermissionId({
|
|
727
|
-
path: {
|
|
728
|
-
id: pending.permission.sessionID,
|
|
729
|
-
permissionID: pending.permission.id,
|
|
730
|
-
},
|
|
731
|
-
body: {
|
|
732
|
-
response: scope,
|
|
733
|
-
},
|
|
734
|
-
})
|
|
735
|
-
|
|
736
|
-
pendingPermissions.delete(channel.id)
|
|
737
|
-
const msg =
|
|
738
|
-
scope === 'always'
|
|
739
|
-
? `✅ Permission **accepted** (auto-approve similar requests)`
|
|
740
|
-
: `✅ Permission **accepted**`
|
|
741
|
-
await command.reply({ content: msg, flags: SILENT_MESSAGE_FLAGS })
|
|
742
|
-
discordLogger.log(
|
|
743
|
-
`Permission ${pending.permission.id} accepted with scope: ${scope}`,
|
|
744
|
-
)
|
|
745
|
-
} catch (error) {
|
|
746
|
-
interactionLogger.error('[ACCEPT] Error:', error)
|
|
747
|
-
await command.reply({
|
|
748
|
-
content: `Failed to accept permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
749
|
-
ephemeral: true,
|
|
750
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
751
|
-
})
|
|
752
|
-
}
|
|
753
|
-
} else if (command.commandName === 'reject') {
|
|
754
|
-
const channel = command.channel
|
|
755
|
-
|
|
756
|
-
if (!channel) {
|
|
757
|
-
await command.reply({
|
|
758
|
-
content: 'This command can only be used in a channel',
|
|
759
|
-
ephemeral: true,
|
|
760
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
761
|
-
})
|
|
762
|
-
return
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const isThread = [
|
|
766
|
-
ChannelType.PublicThread,
|
|
767
|
-
ChannelType.PrivateThread,
|
|
768
|
-
ChannelType.AnnouncementThread,
|
|
769
|
-
].includes(channel.type)
|
|
770
|
-
|
|
771
|
-
if (!isThread) {
|
|
772
|
-
await command.reply({
|
|
773
|
-
content: 'This command can only be used in a thread with an active session',
|
|
774
|
-
ephemeral: true,
|
|
775
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
776
|
-
})
|
|
777
|
-
return
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
const pending = pendingPermissions.get(channel.id)
|
|
781
|
-
if (!pending) {
|
|
782
|
-
await command.reply({
|
|
783
|
-
content: 'No pending permission request in this thread',
|
|
784
|
-
ephemeral: true,
|
|
785
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
786
|
-
})
|
|
787
|
-
return
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
try {
|
|
791
|
-
const getClient = await initializeOpencodeForDirectory(pending.directory)
|
|
792
|
-
await getClient().postSessionIdPermissionsPermissionId({
|
|
793
|
-
path: {
|
|
794
|
-
id: pending.permission.sessionID,
|
|
795
|
-
permissionID: pending.permission.id,
|
|
796
|
-
},
|
|
797
|
-
body: {
|
|
798
|
-
response: 'reject',
|
|
799
|
-
},
|
|
800
|
-
})
|
|
801
|
-
|
|
802
|
-
pendingPermissions.delete(channel.id)
|
|
803
|
-
await command.reply({ content: `❌ Permission **rejected**`, flags: SILENT_MESSAGE_FLAGS })
|
|
804
|
-
discordLogger.log(`Permission ${pending.permission.id} rejected`)
|
|
805
|
-
} catch (error) {
|
|
806
|
-
interactionLogger.error('[REJECT] Error:', error)
|
|
807
|
-
await command.reply({
|
|
808
|
-
content: `Failed to reject permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
809
|
-
ephemeral: true,
|
|
810
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
811
|
-
})
|
|
812
|
-
}
|
|
813
|
-
} else if (command.commandName === 'abort') {
|
|
814
|
-
const channel = command.channel
|
|
815
|
-
|
|
816
|
-
if (!channel) {
|
|
817
|
-
await command.reply({
|
|
818
|
-
content: 'This command can only be used in a channel',
|
|
819
|
-
ephemeral: true,
|
|
820
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
821
|
-
})
|
|
822
|
-
return
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
const isThread = [
|
|
826
|
-
ChannelType.PublicThread,
|
|
827
|
-
ChannelType.PrivateThread,
|
|
828
|
-
ChannelType.AnnouncementThread,
|
|
829
|
-
].includes(channel.type)
|
|
830
|
-
|
|
831
|
-
if (!isThread) {
|
|
832
|
-
await command.reply({
|
|
833
|
-
content: 'This command can only be used in a thread with an active session',
|
|
834
|
-
ephemeral: true,
|
|
835
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
836
|
-
})
|
|
837
|
-
return
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
const textChannel = await resolveTextChannel(channel as ThreadChannel)
|
|
841
|
-
const { projectDirectory: directory } = getKimakiMetadata(textChannel)
|
|
842
109
|
|
|
843
|
-
|
|
844
|
-
await
|
|
845
|
-
content: 'Could not determine project directory for this channel',
|
|
846
|
-
ephemeral: true,
|
|
847
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
848
|
-
})
|
|
849
|
-
return
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
const row = getDatabase()
|
|
853
|
-
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
854
|
-
.get(channel.id) as { session_id: string } | undefined
|
|
855
|
-
|
|
856
|
-
if (!row?.session_id) {
|
|
857
|
-
await command.reply({
|
|
858
|
-
content: 'No active session in this thread',
|
|
859
|
-
ephemeral: true,
|
|
860
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
861
|
-
})
|
|
110
|
+
case 'model':
|
|
111
|
+
await handleModelCommand({ interaction, appId })
|
|
862
112
|
return
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
try {
|
|
868
|
-
const existingController = abortControllers.get(sessionId)
|
|
869
|
-
if (existingController) {
|
|
870
|
-
existingController.abort(new Error('User requested abort'))
|
|
871
|
-
abortControllers.delete(sessionId)
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
const getClient = await initializeOpencodeForDirectory(directory)
|
|
875
|
-
await getClient().session.abort({
|
|
876
|
-
path: { id: sessionId },
|
|
877
|
-
})
|
|
878
|
-
|
|
879
|
-
await command.reply({ content: `🛑 Request **aborted**`, flags: SILENT_MESSAGE_FLAGS })
|
|
880
|
-
discordLogger.log(`Session ${sessionId} aborted by user`)
|
|
881
|
-
} catch (error) {
|
|
882
|
-
interactionLogger.error('[ABORT] Error:', error)
|
|
883
|
-
await command.reply({
|
|
884
|
-
content: `Failed to abort: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
885
|
-
ephemeral: true,
|
|
886
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
887
|
-
})
|
|
888
|
-
}
|
|
889
|
-
} else if (command.commandName === 'share') {
|
|
890
|
-
const channel = command.channel
|
|
891
|
-
|
|
892
|
-
if (!channel) {
|
|
893
|
-
await command.reply({
|
|
894
|
-
content: 'This command can only be used in a channel',
|
|
895
|
-
ephemeral: true,
|
|
896
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
897
|
-
})
|
|
113
|
+
|
|
114
|
+
case 'agent':
|
|
115
|
+
await handleAgentCommand({ interaction, appId })
|
|
898
116
|
return
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
ChannelType.PublicThread,
|
|
903
|
-
ChannelType.PrivateThread,
|
|
904
|
-
ChannelType.AnnouncementThread,
|
|
905
|
-
].includes(channel.type)
|
|
906
|
-
|
|
907
|
-
if (!isThread) {
|
|
908
|
-
await command.reply({
|
|
909
|
-
content: 'This command can only be used in a thread with an active session',
|
|
910
|
-
ephemeral: true,
|
|
911
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
912
|
-
})
|
|
117
|
+
|
|
118
|
+
case 'queue':
|
|
119
|
+
await handleQueueCommand({ command: interaction, appId })
|
|
913
120
|
return
|
|
914
|
-
}
|
|
915
121
|
|
|
916
|
-
|
|
917
|
-
|
|
122
|
+
case 'clear-queue':
|
|
123
|
+
await handleClearQueueCommand({ command: interaction, appId })
|
|
124
|
+
return
|
|
918
125
|
|
|
919
|
-
|
|
920
|
-
await
|
|
921
|
-
content: 'Could not determine project directory for this channel',
|
|
922
|
-
ephemeral: true,
|
|
923
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
924
|
-
})
|
|
126
|
+
case 'undo':
|
|
127
|
+
await handleUndoCommand({ command: interaction, appId })
|
|
925
128
|
return
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
930
|
-
.get(channel.id) as { session_id: string } | undefined
|
|
931
|
-
|
|
932
|
-
if (!row?.session_id) {
|
|
933
|
-
await command.reply({
|
|
934
|
-
content: 'No active session in this thread',
|
|
935
|
-
ephemeral: true,
|
|
936
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
937
|
-
})
|
|
129
|
+
|
|
130
|
+
case 'redo':
|
|
131
|
+
await handleRedoCommand({ command: interaction, appId })
|
|
938
132
|
return
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
const sessionId = row.session_id
|
|
942
|
-
|
|
943
|
-
try {
|
|
944
|
-
const getClient = await initializeOpencodeForDirectory(directory)
|
|
945
|
-
const response = await getClient().session.share({
|
|
946
|
-
path: { id: sessionId },
|
|
947
|
-
})
|
|
948
|
-
|
|
949
|
-
if (!response.data?.share?.url) {
|
|
950
|
-
await command.reply({
|
|
951
|
-
content: 'Failed to generate share URL',
|
|
952
|
-
ephemeral: true,
|
|
953
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
954
|
-
})
|
|
955
|
-
return
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
await command.reply({ content: `🔗 **Session shared:** ${response.data.share.url}`, flags: SILENT_MESSAGE_FLAGS })
|
|
959
|
-
discordLogger.log(`Session ${sessionId} shared: ${response.data.share.url}`)
|
|
960
|
-
} catch (error) {
|
|
961
|
-
interactionLogger.error('[SHARE] Error:', error)
|
|
962
|
-
await command.reply({
|
|
963
|
-
content: `Failed to share session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
964
|
-
ephemeral: true,
|
|
965
|
-
flags: SILENT_MESSAGE_FLAGS,
|
|
966
|
-
})
|
|
967
|
-
}
|
|
968
|
-
} else if (command.commandName === 'fork') {
|
|
969
|
-
await handleForkCommand(command)
|
|
970
|
-
} else if (command.commandName === 'model') {
|
|
971
|
-
await handleModelCommand({ interaction: command, appId })
|
|
972
133
|
}
|
|
134
|
+
|
|
135
|
+
// Handle user-defined commands (ending with -cmd suffix)
|
|
136
|
+
if (interaction.commandName.endsWith('-cmd')) {
|
|
137
|
+
await handleUserCommand({ command: interaction, appId })
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
return
|
|
973
141
|
}
|
|
974
142
|
|
|
975
143
|
if (interaction.isStringSelectMenu()) {
|
|
976
|
-
|
|
144
|
+
const customId = interaction.customId
|
|
145
|
+
|
|
146
|
+
if (customId.startsWith('fork_select:')) {
|
|
977
147
|
await handleForkSelectMenu(interaction)
|
|
978
|
-
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (customId.startsWith('model_provider:')) {
|
|
979
152
|
await handleProviderSelectMenu(interaction)
|
|
980
|
-
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (customId.startsWith('model_select:')) {
|
|
981
157
|
await handleModelSelectMenu(interaction)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (customId.startsWith('agent_select:')) {
|
|
162
|
+
await handleAgentSelectMenu(interaction)
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (customId.startsWith('ask_question:')) {
|
|
167
|
+
await handleAskQuestionSelectMenu(interaction)
|
|
168
|
+
return
|
|
982
169
|
}
|
|
170
|
+
return
|
|
983
171
|
}
|
|
984
172
|
} catch (error) {
|
|
985
173
|
interactionLogger.error('[INTERACTION] Error handling interaction:', error)
|
|
986
|
-
// Try to respond to the interaction if possible
|
|
987
174
|
try {
|
|
988
175
|
if (interaction.isRepliable() && !interaction.replied && !interaction.deferred) {
|
|
989
176
|
await interaction.reply({
|