kimaki 0.4.34 → 0.4.36
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/dist/ai-tool-to-genai.js +1 -3
- package/dist/channel-management.js +1 -1
- package/dist/cli.js +142 -39
- package/dist/commands/abort.js +1 -1
- package/dist/commands/add-project.js +1 -1
- package/dist/commands/agent.js +6 -2
- package/dist/commands/ask-question.js +2 -1
- package/dist/commands/fork.js +7 -7
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +109 -0
- package/dist/commands/resume.js +3 -5
- package/dist/commands/session.js +56 -1
- package/dist/commands/share.js +1 -1
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +3 -6
- package/dist/config.js +1 -1
- package/dist/discord-bot.js +4 -10
- package/dist/discord-utils.js +33 -9
- package/dist/genai.js +4 -6
- package/dist/interaction-handler.js +8 -1
- package/dist/markdown.js +1 -3
- package/dist/message-formatting.js +7 -3
- package/dist/openai-realtime.js +3 -5
- package/dist/opencode.js +2 -3
- package/dist/session-handler.js +42 -25
- package/dist/system-message.js +5 -3
- package/dist/tools.js +9 -22
- package/dist/unnest-code-blocks.js +4 -2
- package/dist/unnest-code-blocks.test.js +40 -15
- package/dist/voice-handler.js +9 -12
- package/dist/voice.js +5 -3
- package/dist/xml.js +2 -4
- package/package.json +3 -2
- package/src/__snapshots__/compact-session-context-no-system.md +24 -24
- package/src/__snapshots__/compact-session-context.md +31 -31
- package/src/ai-tool-to-genai.ts +3 -11
- package/src/channel-management.ts +14 -25
- package/src/cli.ts +290 -195
- package/src/commands/abort.ts +1 -3
- package/src/commands/add-project.ts +8 -14
- package/src/commands/agent.ts +16 -9
- package/src/commands/ask-question.ts +8 -7
- package/src/commands/create-new-project.ts +8 -14
- package/src/commands/fork.ts +23 -27
- package/src/commands/model.ts +14 -11
- package/src/commands/permissions.ts +1 -1
- package/src/commands/queue.ts +6 -19
- package/src/commands/remove-project.ts +136 -0
- package/src/commands/resume.ts +11 -30
- package/src/commands/session.ts +68 -9
- package/src/commands/share.ts +1 -3
- package/src/commands/types.ts +1 -3
- package/src/commands/undo-redo.ts +6 -18
- package/src/commands/user-command.ts +8 -10
- package/src/config.ts +5 -5
- package/src/database.ts +10 -8
- package/src/discord-bot.ts +22 -46
- package/src/discord-utils.ts +35 -18
- package/src/escape-backticks.test.ts +0 -2
- package/src/format-tables.ts +1 -4
- package/src/genai-worker-wrapper.ts +3 -9
- package/src/genai-worker.ts +4 -19
- package/src/genai.ts +10 -42
- package/src/interaction-handler.ts +133 -121
- package/src/markdown.test.ts +10 -32
- package/src/markdown.ts +6 -14
- package/src/message-formatting.ts +13 -14
- package/src/openai-realtime.ts +25 -47
- package/src/opencode.ts +26 -37
- package/src/session-handler.ts +111 -75
- package/src/system-message.ts +13 -3
- package/src/tools.ts +13 -39
- package/src/unnest-code-blocks.test.ts +42 -15
- package/src/unnest-code-blocks.ts +4 -2
- package/src/utils.ts +1 -4
- package/src/voice-handler.ts +34 -78
- package/src/voice.ts +11 -19
- package/src/xml.test.ts +1 -1
- package/src/xml.ts +3 -12
package/src/commands/abort.ts
CHANGED
|
@@ -10,9 +10,7 @@ import { createLogger } from '../logger.js'
|
|
|
10
10
|
|
|
11
11
|
const logger = createLogger('ABORT')
|
|
12
12
|
|
|
13
|
-
export async function handleAbortCommand({
|
|
14
|
-
command,
|
|
15
|
-
}: CommandContext): Promise<void> {
|
|
13
|
+
export async function handleAbortCommand({ command }: CommandContext): Promise<void> {
|
|
16
14
|
const channel = command.channel
|
|
17
15
|
|
|
18
16
|
if (!channel) {
|
|
@@ -11,10 +11,7 @@ import { abbreviatePath } from '../utils.js'
|
|
|
11
11
|
|
|
12
12
|
const logger = createLogger('ADD-PROJECT')
|
|
13
13
|
|
|
14
|
-
export async function handleAddProjectCommand({
|
|
15
|
-
command,
|
|
16
|
-
appId,
|
|
17
|
-
}: CommandContext): Promise<void> {
|
|
14
|
+
export async function handleAddProjectCommand({ command, appId }: CommandContext): Promise<void> {
|
|
18
15
|
await command.deferReply({ ephemeral: false })
|
|
19
16
|
|
|
20
17
|
const projectId = command.options.getString('project', true)
|
|
@@ -63,13 +60,12 @@ export async function handleAddProjectCommand({
|
|
|
63
60
|
return
|
|
64
61
|
}
|
|
65
62
|
|
|
66
|
-
const { textChannelId, voiceChannelId, channelName } =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
})
|
|
63
|
+
const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
|
|
64
|
+
guild,
|
|
65
|
+
projectDirectory: directory,
|
|
66
|
+
appId,
|
|
67
|
+
botName: command.client.user?.username,
|
|
68
|
+
})
|
|
73
69
|
|
|
74
70
|
await command.editReply(
|
|
75
71
|
`✅ Created channels for project:\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n📁 Directory: \`${directory}\``,
|
|
@@ -102,9 +98,7 @@ export async function handleAddProjectAutocomplete({
|
|
|
102
98
|
|
|
103
99
|
const db = getDatabase()
|
|
104
100
|
const existingDirs = db
|
|
105
|
-
.prepare(
|
|
106
|
-
'SELECT DISTINCT directory FROM channel_directories WHERE channel_type = ?',
|
|
107
|
-
)
|
|
101
|
+
.prepare('SELECT DISTINCT directory FROM channel_directories WHERE channel_type = ?')
|
|
108
102
|
.all('text') as { directory: string }[]
|
|
109
103
|
const existingDirSet = new Set(existingDirs.map((row) => row.directory))
|
|
110
104
|
|
package/src/commands/agent.ts
CHANGED
|
@@ -17,12 +17,15 @@ import { createLogger } from '../logger.js'
|
|
|
17
17
|
|
|
18
18
|
const agentLogger = createLogger('AGENT')
|
|
19
19
|
|
|
20
|
-
const pendingAgentContexts = new Map<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
const pendingAgentContexts = new Map<
|
|
21
|
+
string,
|
|
22
|
+
{
|
|
23
|
+
dir: string
|
|
24
|
+
channelId: string
|
|
25
|
+
sessionId?: string
|
|
26
|
+
isThread: boolean
|
|
27
|
+
}
|
|
28
|
+
>()
|
|
26
29
|
|
|
27
30
|
export async function handleAgentCommand({
|
|
28
31
|
interaction,
|
|
@@ -72,7 +75,9 @@ export async function handleAgentCommand({
|
|
|
72
75
|
channelAppId = metadata.channelAppId
|
|
73
76
|
targetChannelId = channel.id
|
|
74
77
|
} else {
|
|
75
|
-
await interaction.editReply({
|
|
78
|
+
await interaction.editReply({
|
|
79
|
+
content: 'This command can only be used in text channels or threads',
|
|
80
|
+
})
|
|
76
81
|
return
|
|
77
82
|
}
|
|
78
83
|
|
|
@@ -82,7 +87,9 @@ export async function handleAgentCommand({
|
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
if (!projectDirectory) {
|
|
85
|
-
await interaction.editReply({
|
|
90
|
+
await interaction.editReply({
|
|
91
|
+
content: 'This channel is not configured with a project directory',
|
|
92
|
+
})
|
|
86
93
|
return
|
|
87
94
|
}
|
|
88
95
|
|
|
@@ -141,7 +148,7 @@ export async function handleAgentCommand({
|
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
export async function handleAgentSelectMenu(
|
|
144
|
-
interaction: StringSelectMenuInteraction
|
|
151
|
+
interaction: StringSelectMenuInteraction,
|
|
145
152
|
): Promise<void> {
|
|
146
153
|
const customId = interaction.customId
|
|
147
154
|
|
|
@@ -95,9 +95,10 @@ export async function showAskUserQuestionDropdowns({
|
|
|
95
95
|
},
|
|
96
96
|
]
|
|
97
97
|
|
|
98
|
+
const placeholder = options.find((x) => x.label)?.label || 'Select an option'
|
|
98
99
|
const selectMenu = new StringSelectMenuBuilder()
|
|
99
100
|
.setCustomId(`ask_question:${contextHash}:${i}`)
|
|
100
|
-
.setPlaceholder(
|
|
101
|
+
.setPlaceholder(placeholder)
|
|
101
102
|
.addOptions(options)
|
|
102
103
|
|
|
103
104
|
// Enable multi-select if the question supports it
|
|
@@ -122,7 +123,7 @@ export async function showAskUserQuestionDropdowns({
|
|
|
122
123
|
* Handle dropdown selection for AskUserQuestion.
|
|
123
124
|
*/
|
|
124
125
|
export async function handleAskQuestionSelectMenu(
|
|
125
|
-
interaction: StringSelectMenuInteraction
|
|
126
|
+
interaction: StringSelectMenuInteraction,
|
|
126
127
|
): Promise<void> {
|
|
127
128
|
const customId = interaction.customId
|
|
128
129
|
|
|
@@ -196,9 +197,7 @@ export async function handleAskQuestionSelectMenu(
|
|
|
196
197
|
* Submit all collected answers back to the OpenCode session.
|
|
197
198
|
* Uses the question.reply API to provide answers to the waiting tool.
|
|
198
199
|
*/
|
|
199
|
-
async function submitQuestionAnswers(
|
|
200
|
-
context: PendingQuestionContext
|
|
201
|
-
): Promise<void> {
|
|
200
|
+
async function submitQuestionAnswers(context: PendingQuestionContext): Promise<void> {
|
|
202
201
|
try {
|
|
203
202
|
const clientV2 = getOpencodeClientV2(context.directory)
|
|
204
203
|
if (!clientV2) {
|
|
@@ -215,12 +214,14 @@ async function submitQuestionAnswers(
|
|
|
215
214
|
answers,
|
|
216
215
|
})
|
|
217
216
|
|
|
218
|
-
logger.log(
|
|
217
|
+
logger.log(
|
|
218
|
+
`Submitted answers for question ${context.requestId} in session ${context.sessionId}`,
|
|
219
|
+
)
|
|
219
220
|
} catch (error) {
|
|
220
221
|
logger.error('Failed to submit answers:', error)
|
|
221
222
|
await sendThreadMessage(
|
|
222
223
|
context.thread,
|
|
223
|
-
`✗ Failed to submit answers: ${error instanceof Error ? error.message : 'Unknown error'}
|
|
224
|
+
`✗ Failed to submit answers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
224
225
|
)
|
|
225
226
|
}
|
|
226
227
|
}
|
|
@@ -54,9 +54,7 @@ export async function handleCreateNewProjectCommand({
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
if (fs.existsSync(projectDirectory)) {
|
|
57
|
-
await command.editReply(
|
|
58
|
-
`Project directory already exists: ${projectDirectory}`,
|
|
59
|
-
)
|
|
57
|
+
await command.editReply(`Project directory already exists: ${projectDirectory}`)
|
|
60
58
|
return
|
|
61
59
|
}
|
|
62
60
|
|
|
@@ -67,16 +65,13 @@ export async function handleCreateNewProjectCommand({
|
|
|
67
65
|
execSync('git init', { cwd: projectDirectory, stdio: 'pipe' })
|
|
68
66
|
logger.log(`Initialized git in: ${projectDirectory}`)
|
|
69
67
|
|
|
70
|
-
const { textChannelId, voiceChannelId, channelName } =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
})
|
|
68
|
+
const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
|
|
69
|
+
guild,
|
|
70
|
+
projectDirectory,
|
|
71
|
+
appId,
|
|
72
|
+
})
|
|
76
73
|
|
|
77
|
-
const textChannel = (await guild.channels.fetch(
|
|
78
|
-
textChannelId,
|
|
79
|
-
)) as TextChannel
|
|
74
|
+
const textChannel = (await guild.channels.fetch(textChannelId)) as TextChannel
|
|
80
75
|
|
|
81
76
|
await command.editReply(
|
|
82
77
|
`✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n\n_Starting session..._`,
|
|
@@ -94,8 +89,7 @@ export async function handleCreateNewProjectCommand({
|
|
|
94
89
|
})
|
|
95
90
|
|
|
96
91
|
await handleOpencodeSession({
|
|
97
|
-
prompt:
|
|
98
|
-
'The project was just initialized. Say hi and ask what the user wants to build.',
|
|
92
|
+
prompt: 'The project was just initialized. Say hi and ask what the user wants to build.',
|
|
99
93
|
thread,
|
|
100
94
|
projectDirectory,
|
|
101
95
|
channelId: textChannel.id,
|
package/src/commands/fork.ts
CHANGED
|
@@ -85,9 +85,7 @@ export async function handleForkCommand(interaction: ChatInputCommandInteraction
|
|
|
85
85
|
return
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
const userMessages = messagesResponse.data.filter(
|
|
89
|
-
(m) => m.info.role === 'user'
|
|
90
|
-
)
|
|
88
|
+
const userMessages = messagesResponse.data.filter((m) => m.info.role === 'user')
|
|
91
89
|
|
|
92
90
|
if (userMessages.length === 0) {
|
|
93
91
|
await interaction.editReply({
|
|
@@ -99,7 +97,9 @@ export async function handleForkCommand(interaction: ChatInputCommandInteraction
|
|
|
99
97
|
const recentMessages = userMessages.slice(-25)
|
|
100
98
|
|
|
101
99
|
const options = recentMessages.map((m, index) => {
|
|
102
|
-
const textPart = m.parts.find((p) => p.type === 'text') as
|
|
100
|
+
const textPart = m.parts.find((p) => p.type === 'text') as
|
|
101
|
+
| { type: 'text'; text: string }
|
|
102
|
+
| undefined
|
|
103
103
|
const preview = textPart?.text?.slice(0, 80) || '(no text)'
|
|
104
104
|
const label = `${index + 1}. ${preview}${preview.length >= 80 ? '...' : ''}`
|
|
105
105
|
|
|
@@ -117,11 +117,11 @@ export async function handleForkCommand(interaction: ChatInputCommandInteraction
|
|
|
117
117
|
.setPlaceholder('Select a message to fork from')
|
|
118
118
|
.addOptions(options)
|
|
119
119
|
|
|
120
|
-
const actionRow = new ActionRowBuilder<StringSelectMenuBuilder>()
|
|
121
|
-
.addComponents(selectMenu)
|
|
120
|
+
const actionRow = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
|
|
122
121
|
|
|
123
122
|
await interaction.editReply({
|
|
124
|
-
content:
|
|
123
|
+
content:
|
|
124
|
+
'**Fork Session**\nSelect the user message to fork from. The forked session will continue as if you had not sent that message:',
|
|
125
125
|
components: [actionRow],
|
|
126
126
|
})
|
|
127
127
|
} catch (error) {
|
|
@@ -132,7 +132,9 @@ export async function handleForkCommand(interaction: ChatInputCommandInteraction
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
export async function handleForkSelectMenu(
|
|
135
|
+
export async function handleForkSelectMenu(
|
|
136
|
+
interaction: StringSelectMenuInteraction,
|
|
137
|
+
): Promise<void> {
|
|
136
138
|
const customId = interaction.customId
|
|
137
139
|
|
|
138
140
|
if (!customId.startsWith('fork_select:')) {
|
|
@@ -177,11 +179,14 @@ export async function handleForkSelectMenu(interaction: StringSelectMenuInteract
|
|
|
177
179
|
const forkedSession = forkResponse.data
|
|
178
180
|
const parentChannel = interaction.channel
|
|
179
181
|
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
if (
|
|
183
|
+
!parentChannel ||
|
|
184
|
+
![
|
|
185
|
+
ChannelType.PublicThread,
|
|
186
|
+
ChannelType.PrivateThread,
|
|
187
|
+
ChannelType.AnnouncementThread,
|
|
188
|
+
].includes(parentChannel.type)
|
|
189
|
+
) {
|
|
185
190
|
await interaction.editReply('Could not access parent channel')
|
|
186
191
|
return
|
|
187
192
|
}
|
|
@@ -200,14 +205,10 @@ export async function handleForkSelectMenu(interaction: StringSelectMenuInteract
|
|
|
200
205
|
})
|
|
201
206
|
|
|
202
207
|
getDatabase()
|
|
203
|
-
.prepare(
|
|
204
|
-
'INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)'
|
|
205
|
-
)
|
|
208
|
+
.prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
|
|
206
209
|
.run(thread.id, forkedSession.id)
|
|
207
210
|
|
|
208
|
-
sessionLogger.log(
|
|
209
|
-
`Created forked session ${forkedSession.id} in thread ${thread.id}`
|
|
210
|
-
)
|
|
211
|
+
sessionLogger.log(`Created forked session ${forkedSession.id} in thread ${thread.id}`)
|
|
211
212
|
|
|
212
213
|
await sendThreadMessage(
|
|
213
214
|
thread,
|
|
@@ -240,18 +241,13 @@ export async function handleForkSelectMenu(interaction: StringSelectMenuInteract
|
|
|
240
241
|
}
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
await sendThreadMessage(
|
|
244
|
-
thread,
|
|
245
|
-
`You can now continue the conversation from this point.`,
|
|
246
|
-
)
|
|
244
|
+
await sendThreadMessage(thread, `You can now continue the conversation from this point.`)
|
|
247
245
|
|
|
248
|
-
await interaction.editReply(
|
|
249
|
-
`Session forked! Continue in ${thread.toString()}`
|
|
250
|
-
)
|
|
246
|
+
await interaction.editReply(`Session forked! Continue in ${thread.toString()}`)
|
|
251
247
|
} catch (error) {
|
|
252
248
|
forkLogger.error('Error forking session:', error)
|
|
253
249
|
await interaction.editReply(
|
|
254
|
-
`Failed to fork session: ${error instanceof Error ? error.message : 'Unknown error'}
|
|
250
|
+
`Failed to fork session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
255
251
|
)
|
|
256
252
|
}
|
|
257
253
|
}
|
package/src/commands/model.ts
CHANGED
|
@@ -19,15 +19,18 @@ import { createLogger } from '../logger.js'
|
|
|
19
19
|
const modelLogger = createLogger('MODEL')
|
|
20
20
|
|
|
21
21
|
// Store context by hash to avoid customId length limits (Discord max: 100 chars)
|
|
22
|
-
const pendingModelContexts = new Map<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
const pendingModelContexts = new Map<
|
|
23
|
+
string,
|
|
24
|
+
{
|
|
25
|
+
dir: string
|
|
26
|
+
channelId: string
|
|
27
|
+
sessionId?: string
|
|
28
|
+
isThread: boolean
|
|
29
|
+
providerId?: string
|
|
30
|
+
providerName?: string
|
|
31
|
+
thread?: ThreadChannel
|
|
32
|
+
}
|
|
33
|
+
>()
|
|
31
34
|
|
|
32
35
|
export type ProviderInfo = {
|
|
33
36
|
id: string
|
|
@@ -196,7 +199,7 @@ export async function handleModelCommand({
|
|
|
196
199
|
* Shows a second select menu with models for the chosen provider.
|
|
197
200
|
*/
|
|
198
201
|
export async function handleProviderSelectMenu(
|
|
199
|
-
interaction: StringSelectMenuInteraction
|
|
202
|
+
interaction: StringSelectMenuInteraction,
|
|
200
203
|
): Promise<void> {
|
|
201
204
|
const customId = interaction.customId
|
|
202
205
|
|
|
@@ -317,7 +320,7 @@ export async function handleProviderSelectMenu(
|
|
|
317
320
|
* Stores the model preference in the database.
|
|
318
321
|
*/
|
|
319
322
|
export async function handleModelSelectMenu(
|
|
320
|
-
interaction: StringSelectMenuInteraction
|
|
323
|
+
interaction: StringSelectMenuInteraction,
|
|
321
324
|
): Promise<void> {
|
|
322
325
|
const customId = interaction.customId
|
|
323
326
|
|
|
@@ -96,7 +96,7 @@ export async function showPermissionDropdown({
|
|
|
96
96
|
* Handle dropdown selection for permission.
|
|
97
97
|
*/
|
|
98
98
|
export async function handlePermissionSelectMenu(
|
|
99
|
-
interaction: StringSelectMenuInteraction
|
|
99
|
+
interaction: StringSelectMenuInteraction,
|
|
100
100
|
): Promise<void> {
|
|
101
101
|
const customId = interaction.customId
|
|
102
102
|
|
package/src/commands/queue.ts
CHANGED
|
@@ -20,9 +20,7 @@ import { createLogger } from '../logger.js'
|
|
|
20
20
|
|
|
21
21
|
const logger = createLogger('QUEUE')
|
|
22
22
|
|
|
23
|
-
export async function handleQueueCommand({
|
|
24
|
-
command,
|
|
25
|
-
}: CommandContext): Promise<void> {
|
|
23
|
+
export async function handleQueueCommand({ command }: CommandContext): Promise<void> {
|
|
26
24
|
const message = command.options.getString('message', true)
|
|
27
25
|
const channel = command.channel
|
|
28
26
|
|
|
@@ -85,9 +83,7 @@ export async function handleQueueCommand({
|
|
|
85
83
|
flags: SILENT_MESSAGE_FLAGS,
|
|
86
84
|
})
|
|
87
85
|
|
|
88
|
-
logger.log(
|
|
89
|
-
`[QUEUE] No active request, sending immediately in thread ${channel.id}`,
|
|
90
|
-
)
|
|
86
|
+
logger.log(`[QUEUE] No active request, sending immediately in thread ${channel.id}`)
|
|
91
87
|
|
|
92
88
|
handleOpencodeSession({
|
|
93
89
|
prompt: message,
|
|
@@ -97,10 +93,7 @@ export async function handleQueueCommand({
|
|
|
97
93
|
}).catch(async (e) => {
|
|
98
94
|
logger.error(`[QUEUE] Failed to send message:`, e)
|
|
99
95
|
const errorMsg = e instanceof Error ? e.message : String(e)
|
|
100
|
-
await sendThreadMessage(
|
|
101
|
-
channel as ThreadChannel,
|
|
102
|
-
`✗ Failed: ${errorMsg.slice(0, 200)}`,
|
|
103
|
-
)
|
|
96
|
+
await sendThreadMessage(channel as ThreadChannel, `✗ Failed: ${errorMsg.slice(0, 200)}`)
|
|
104
97
|
})
|
|
105
98
|
|
|
106
99
|
return
|
|
@@ -123,14 +116,10 @@ export async function handleQueueCommand({
|
|
|
123
116
|
flags: SILENT_MESSAGE_FLAGS,
|
|
124
117
|
})
|
|
125
118
|
|
|
126
|
-
logger.log(
|
|
127
|
-
`[QUEUE] User ${command.user.displayName} queued message in thread ${channel.id}`,
|
|
128
|
-
)
|
|
119
|
+
logger.log(`[QUEUE] User ${command.user.displayName} queued message in thread ${channel.id}`)
|
|
129
120
|
}
|
|
130
121
|
|
|
131
|
-
export async function handleClearQueueCommand({
|
|
132
|
-
command,
|
|
133
|
-
}: CommandContext): Promise<void> {
|
|
122
|
+
export async function handleClearQueueCommand({ command }: CommandContext): Promise<void> {
|
|
134
123
|
const channel = command.channel
|
|
135
124
|
|
|
136
125
|
if (!channel) {
|
|
@@ -175,7 +164,5 @@ export async function handleClearQueueCommand({
|
|
|
175
164
|
flags: SILENT_MESSAGE_FLAGS,
|
|
176
165
|
})
|
|
177
166
|
|
|
178
|
-
logger.log(
|
|
179
|
-
`[QUEUE] User ${command.user.displayName} cleared queue in thread ${channel.id}`,
|
|
180
|
-
)
|
|
167
|
+
logger.log(`[QUEUE] User ${command.user.displayName} cleared queue in thread ${channel.id}`)
|
|
181
168
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// /remove-project command - Remove Discord channels for a project.
|
|
2
|
+
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import type { CommandContext, AutocompleteContext } from './types.js'
|
|
5
|
+
import { getDatabase } from '../database.js'
|
|
6
|
+
import { createLogger } from '../logger.js'
|
|
7
|
+
import { abbreviatePath } from '../utils.js'
|
|
8
|
+
|
|
9
|
+
const logger = createLogger('REMOVE-PROJECT')
|
|
10
|
+
|
|
11
|
+
export async function handleRemoveProjectCommand({ command, appId }: CommandContext): Promise<void> {
|
|
12
|
+
await command.deferReply({ ephemeral: false })
|
|
13
|
+
|
|
14
|
+
const directory = command.options.getString('project', true)
|
|
15
|
+
const guild = command.guild
|
|
16
|
+
|
|
17
|
+
if (!guild) {
|
|
18
|
+
await command.editReply('This command can only be used in a guild')
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const db = getDatabase()
|
|
24
|
+
|
|
25
|
+
// Get channel IDs for this directory
|
|
26
|
+
const channels = db
|
|
27
|
+
.prepare('SELECT channel_id, channel_type FROM channel_directories WHERE directory = ?')
|
|
28
|
+
.all(directory) as { channel_id: string; channel_type: string }[]
|
|
29
|
+
|
|
30
|
+
if (channels.length === 0) {
|
|
31
|
+
await command.editReply(`No channels found for directory: \`${directory}\``)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const deletedChannels: string[] = []
|
|
36
|
+
const failedChannels: string[] = []
|
|
37
|
+
|
|
38
|
+
for (const { channel_id, channel_type } of channels) {
|
|
39
|
+
try {
|
|
40
|
+
const channel = await guild.channels.fetch(channel_id).catch(() => null)
|
|
41
|
+
|
|
42
|
+
if (channel) {
|
|
43
|
+
await channel.delete(`Removed by /remove-project command`)
|
|
44
|
+
deletedChannels.push(`${channel_type}: ${channel_id}`)
|
|
45
|
+
} else {
|
|
46
|
+
// Channel doesn't exist in this guild or was already deleted
|
|
47
|
+
deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`)
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger.error(`Failed to delete channel ${channel_id}:`, error)
|
|
51
|
+
failedChannels.push(`${channel_type}: ${channel_id}`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Remove from database
|
|
56
|
+
db.prepare('DELETE FROM channel_directories WHERE directory = ?').run(directory)
|
|
57
|
+
|
|
58
|
+
const projectName = path.basename(directory)
|
|
59
|
+
let message = `Removed project **${projectName}**\n`
|
|
60
|
+
message += `Directory: \`${directory}\`\n\n`
|
|
61
|
+
|
|
62
|
+
if (deletedChannels.length > 0) {
|
|
63
|
+
message += `Deleted channels:\n${deletedChannels.map((c) => `- ${c}`).join('\n')}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (failedChannels.length > 0) {
|
|
67
|
+
message += `\n\nFailed to delete (may be in another server):\n${failedChannels.map((c) => `- ${c}`).join('\n')}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await command.editReply(message)
|
|
71
|
+
logger.log(`Removed project ${projectName} at ${directory}`)
|
|
72
|
+
} catch (error) {
|
|
73
|
+
logger.error('[REMOVE-PROJECT] Error:', error)
|
|
74
|
+
await command.editReply(
|
|
75
|
+
`Failed to remove project: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function handleRemoveProjectAutocomplete({
|
|
81
|
+
interaction,
|
|
82
|
+
appId,
|
|
83
|
+
}: AutocompleteContext): Promise<void> {
|
|
84
|
+
const focusedValue = interaction.options.getFocused()
|
|
85
|
+
const guild = interaction.guild
|
|
86
|
+
|
|
87
|
+
if (!guild) {
|
|
88
|
+
await interaction.respond([])
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const db = getDatabase()
|
|
94
|
+
|
|
95
|
+
// Get all directories with channels
|
|
96
|
+
const allChannels = db
|
|
97
|
+
.prepare(
|
|
98
|
+
'SELECT DISTINCT directory, channel_id FROM channel_directories WHERE channel_type = ?',
|
|
99
|
+
)
|
|
100
|
+
.all('text') as { directory: string; channel_id: string }[]
|
|
101
|
+
|
|
102
|
+
// Filter to only channels that exist in this guild
|
|
103
|
+
const projectsInGuild: { directory: string; channelId: string }[] = []
|
|
104
|
+
|
|
105
|
+
for (const { directory, channel_id } of allChannels) {
|
|
106
|
+
try {
|
|
107
|
+
const channel = await guild.channels.fetch(channel_id).catch(() => null)
|
|
108
|
+
if (channel) {
|
|
109
|
+
projectsInGuild.push({ directory, channelId: channel_id })
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Channel not in this guild, skip
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const projects = projectsInGuild
|
|
117
|
+
.filter(({ directory }) => {
|
|
118
|
+
const baseName = path.basename(directory)
|
|
119
|
+
const searchText = `${baseName} ${directory}`.toLowerCase()
|
|
120
|
+
return searchText.includes(focusedValue.toLowerCase())
|
|
121
|
+
})
|
|
122
|
+
.slice(0, 25)
|
|
123
|
+
.map(({ directory }) => {
|
|
124
|
+
const name = `${path.basename(directory)} (${abbreviatePath(directory)})`
|
|
125
|
+
return {
|
|
126
|
+
name: name.length > 100 ? name.slice(0, 99) + '...' : name,
|
|
127
|
+
value: directory,
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
await interaction.respond(projects)
|
|
132
|
+
} catch (error) {
|
|
133
|
+
logger.error('[AUTOCOMPLETE] Error fetching projects:', error)
|
|
134
|
+
await interaction.respond([])
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/commands/resume.ts
CHANGED
|
@@ -10,21 +10,14 @@ import fs from 'node:fs'
|
|
|
10
10
|
import type { CommandContext, AutocompleteContext } from './types.js'
|
|
11
11
|
import { getDatabase } from '../database.js'
|
|
12
12
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
13
|
-
import {
|
|
14
|
-
sendThreadMessage,
|
|
15
|
-
resolveTextChannel,
|
|
16
|
-
getKimakiMetadata,
|
|
17
|
-
} from '../discord-utils.js'
|
|
13
|
+
import { sendThreadMessage, resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
|
|
18
14
|
import { extractTagsArrays } from '../xml.js'
|
|
19
15
|
import { collectLastAssistantParts } from '../message-formatting.js'
|
|
20
16
|
import { createLogger } from '../logger.js'
|
|
21
17
|
|
|
22
18
|
const logger = createLogger('RESUME')
|
|
23
19
|
|
|
24
|
-
export async function handleResumeCommand({
|
|
25
|
-
command,
|
|
26
|
-
appId,
|
|
27
|
-
}: CommandContext): Promise<void> {
|
|
20
|
+
export async function handleResumeCommand({ command, appId }: CommandContext): Promise<void> {
|
|
28
21
|
await command.deferReply({ ephemeral: false })
|
|
29
22
|
|
|
30
23
|
const sessionId = command.options.getString('session', true)
|
|
@@ -56,9 +49,7 @@ export async function handleResumeCommand({
|
|
|
56
49
|
}
|
|
57
50
|
|
|
58
51
|
if (!projectDirectory) {
|
|
59
|
-
await command.editReply(
|
|
60
|
-
'This channel is not configured with a project directory',
|
|
61
|
-
)
|
|
52
|
+
await command.editReply('This channel is not configured with a project directory')
|
|
62
53
|
return
|
|
63
54
|
}
|
|
64
55
|
|
|
@@ -88,9 +79,7 @@ export async function handleResumeCommand({
|
|
|
88
79
|
})
|
|
89
80
|
|
|
90
81
|
getDatabase()
|
|
91
|
-
.prepare(
|
|
92
|
-
'INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)',
|
|
93
|
-
)
|
|
82
|
+
.prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
|
|
94
83
|
.run(thread.id, sessionId)
|
|
95
84
|
|
|
96
85
|
logger.log(`[RESUME] Created thread ${thread.id} for session ${sessionId}`)
|
|
@@ -105,9 +94,7 @@ export async function handleResumeCommand({
|
|
|
105
94
|
|
|
106
95
|
const messages = messagesResponse.data
|
|
107
96
|
|
|
108
|
-
await command.editReply(
|
|
109
|
-
`Resumed session "${sessionTitle}" in ${thread.toString()}`,
|
|
110
|
-
)
|
|
97
|
+
await command.editReply(`Resumed session "${sessionTitle}" in ${thread.toString()}`)
|
|
111
98
|
|
|
112
99
|
await sendThreadMessage(
|
|
113
100
|
thread,
|
|
@@ -119,10 +106,7 @@ export async function handleResumeCommand({
|
|
|
119
106
|
})
|
|
120
107
|
|
|
121
108
|
if (skippedCount > 0) {
|
|
122
|
-
await sendThreadMessage(
|
|
123
|
-
thread,
|
|
124
|
-
`*Skipped ${skippedCount} older assistant parts...*`,
|
|
125
|
-
)
|
|
109
|
+
await sendThreadMessage(thread, `*Skipped ${skippedCount} older assistant parts...*`)
|
|
126
110
|
}
|
|
127
111
|
|
|
128
112
|
if (content.trim()) {
|
|
@@ -168,8 +152,7 @@ export async function handleResumeAutocomplete({
|
|
|
168
152
|
interaction.channel as TextChannel | ThreadChannel | null,
|
|
169
153
|
)
|
|
170
154
|
if (textChannel) {
|
|
171
|
-
const { projectDirectory: directory, channelAppId } =
|
|
172
|
-
getKimakiMetadata(textChannel)
|
|
155
|
+
const { projectDirectory: directory, channelAppId } = getKimakiMetadata(textChannel)
|
|
173
156
|
if (channelAppId && channelAppId !== appId) {
|
|
174
157
|
await interaction.respond([])
|
|
175
158
|
return
|
|
@@ -194,17 +177,15 @@ export async function handleResumeAutocomplete({
|
|
|
194
177
|
|
|
195
178
|
const existingSessionIds = new Set(
|
|
196
179
|
(
|
|
197
|
-
getDatabase()
|
|
198
|
-
|
|
199
|
-
|
|
180
|
+
getDatabase().prepare('SELECT session_id FROM thread_sessions').all() as {
|
|
181
|
+
session_id: string
|
|
182
|
+
}[]
|
|
200
183
|
).map((row) => row.session_id),
|
|
201
184
|
)
|
|
202
185
|
|
|
203
186
|
const sessions = sessionsResponse.data
|
|
204
187
|
.filter((session) => !existingSessionIds.has(session.id))
|
|
205
|
-
.filter((session) =>
|
|
206
|
-
session.title.toLowerCase().includes(focusedValue.toLowerCase()),
|
|
207
|
-
)
|
|
188
|
+
.filter((session) => session.title.toLowerCase().includes(focusedValue.toLowerCase()))
|
|
208
189
|
.slice(0, 25)
|
|
209
190
|
.map((session) => {
|
|
210
191
|
const dateStr = new Date(session.time.updated).toLocaleString()
|