kimaki 0.4.37 → 0.4.39
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/channel-management.js +6 -2
- package/dist/cli.js +41 -15
- package/dist/commands/abort.js +15 -6
- package/dist/commands/add-project.js +9 -0
- package/dist/commands/agent.js +114 -20
- package/dist/commands/fork.js +13 -2
- package/dist/commands/model.js +12 -0
- package/dist/commands/remove-project.js +26 -16
- package/dist/commands/resume.js +9 -0
- package/dist/commands/session.js +13 -0
- package/dist/commands/share.js +10 -1
- package/dist/commands/undo-redo.js +13 -4
- package/dist/database.js +24 -5
- package/dist/discord-bot.js +38 -31
- package/dist/errors.js +110 -0
- package/dist/genai-worker.js +18 -16
- package/dist/interaction-handler.js +6 -1
- package/dist/markdown.js +96 -85
- package/dist/markdown.test.js +10 -3
- package/dist/message-formatting.js +50 -37
- package/dist/opencode.js +43 -46
- package/dist/session-handler.js +136 -8
- package/dist/system-message.js +2 -0
- package/dist/tools.js +18 -8
- package/dist/voice-handler.js +48 -25
- package/dist/voice.js +159 -131
- package/package.json +2 -1
- package/src/channel-management.ts +6 -2
- package/src/cli.ts +67 -19
- package/src/commands/abort.ts +17 -7
- package/src/commands/add-project.ts +9 -0
- package/src/commands/agent.ts +160 -25
- package/src/commands/fork.ts +18 -7
- package/src/commands/model.ts +12 -0
- package/src/commands/remove-project.ts +28 -16
- package/src/commands/resume.ts +9 -0
- package/src/commands/session.ts +13 -0
- package/src/commands/share.ts +11 -1
- package/src/commands/undo-redo.ts +15 -6
- package/src/database.ts +26 -4
- package/src/discord-bot.ts +42 -34
- package/src/errors.ts +208 -0
- package/src/genai-worker.ts +20 -17
- package/src/interaction-handler.ts +7 -1
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +111 -95
- package/src/message-formatting.ts +55 -38
- package/src/opencode.ts +52 -49
- package/src/session-handler.ts +164 -11
- package/src/system-message.ts +2 -0
- package/src/tools.ts +18 -8
- package/src/voice-handler.ts +48 -23
- package/src/voice.ts +195 -148
package/src/commands/agent.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// /agent command - Set the preferred agent for this channel or session.
|
|
2
|
+
// Also provides quick agent commands like /plan-agent, /build-agent that switch instantly.
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
ChatInputCommandInteraction,
|
|
@@ -10,10 +11,11 @@ import {
|
|
|
10
11
|
type TextChannel,
|
|
11
12
|
} from 'discord.js'
|
|
12
13
|
import crypto from 'node:crypto'
|
|
13
|
-
import { getDatabase, setChannelAgent, setSessionAgent, runModelMigrations } from '../database.js'
|
|
14
|
+
import { getDatabase, setChannelAgent, setSessionAgent, clearSessionModel, runModelMigrations } from '../database.js'
|
|
14
15
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
15
16
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
|
|
16
17
|
import { createLogger } from '../logger.js'
|
|
18
|
+
import * as errore from 'errore'
|
|
17
19
|
|
|
18
20
|
const agentLogger = createLogger('AGENT')
|
|
19
21
|
|
|
@@ -27,22 +29,40 @@ const pendingAgentContexts = new Map<
|
|
|
27
29
|
}
|
|
28
30
|
>()
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Context for agent commands, containing channel/session info.
|
|
34
|
+
*/
|
|
35
|
+
export type AgentCommandContext = {
|
|
36
|
+
dir: string
|
|
37
|
+
channelId: string
|
|
38
|
+
sessionId?: string
|
|
39
|
+
isThread: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sanitize an agent name to be a valid Discord command name component.
|
|
44
|
+
* Lowercase, alphanumeric and hyphens only.
|
|
45
|
+
*/
|
|
46
|
+
export function sanitizeAgentName(name: string): string {
|
|
47
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the context for an agent command (directory, channel, session).
|
|
52
|
+
* Returns null if the command cannot be executed in this context.
|
|
53
|
+
*/
|
|
54
|
+
export async function resolveAgentCommandContext({
|
|
31
55
|
interaction,
|
|
32
56
|
appId,
|
|
33
57
|
}: {
|
|
34
58
|
interaction: ChatInputCommandInteraction
|
|
35
59
|
appId: string
|
|
36
|
-
}): Promise<
|
|
37
|
-
await interaction.deferReply({ ephemeral: true })
|
|
38
|
-
|
|
39
|
-
runModelMigrations()
|
|
40
|
-
|
|
60
|
+
}): Promise<AgentCommandContext | null> {
|
|
41
61
|
const channel = interaction.channel
|
|
42
62
|
|
|
43
63
|
if (!channel) {
|
|
44
64
|
await interaction.editReply({ content: 'This command can only be used in a channel' })
|
|
45
|
-
return
|
|
65
|
+
return null
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
const isThread = [
|
|
@@ -78,26 +98,77 @@ export async function handleAgentCommand({
|
|
|
78
98
|
await interaction.editReply({
|
|
79
99
|
content: 'This command can only be used in text channels or threads',
|
|
80
100
|
})
|
|
81
|
-
return
|
|
101
|
+
return null
|
|
82
102
|
}
|
|
83
103
|
|
|
84
104
|
if (channelAppId && channelAppId !== appId) {
|
|
85
105
|
await interaction.editReply({ content: 'This channel is not configured for this bot' })
|
|
86
|
-
return
|
|
106
|
+
return null
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
if (!projectDirectory) {
|
|
90
110
|
await interaction.editReply({
|
|
91
111
|
content: 'This channel is not configured with a project directory',
|
|
92
112
|
})
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
dir: projectDirectory,
|
|
118
|
+
channelId: targetChannelId,
|
|
119
|
+
sessionId,
|
|
120
|
+
isThread,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Set the agent preference for a context (session or channel).
|
|
126
|
+
* When switching agents for a session, also clears the session model preference
|
|
127
|
+
* so the new agent's model takes effect.
|
|
128
|
+
*/
|
|
129
|
+
export function setAgentForContext({
|
|
130
|
+
context,
|
|
131
|
+
agentName,
|
|
132
|
+
}: {
|
|
133
|
+
context: AgentCommandContext
|
|
134
|
+
agentName: string
|
|
135
|
+
}): void {
|
|
136
|
+
if (context.isThread && context.sessionId) {
|
|
137
|
+
setSessionAgent(context.sessionId, agentName)
|
|
138
|
+
// Clear session model so the new agent's model takes effect
|
|
139
|
+
clearSessionModel(context.sessionId)
|
|
140
|
+
agentLogger.log(`Set agent ${agentName} for session ${context.sessionId} (cleared model preference)`)
|
|
141
|
+
} else {
|
|
142
|
+
setChannelAgent(context.channelId, agentName)
|
|
143
|
+
agentLogger.log(`Set agent ${agentName} for channel ${context.channelId}`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function handleAgentCommand({
|
|
148
|
+
interaction,
|
|
149
|
+
appId,
|
|
150
|
+
}: {
|
|
151
|
+
interaction: ChatInputCommandInteraction
|
|
152
|
+
appId: string
|
|
153
|
+
}): Promise<void> {
|
|
154
|
+
await interaction.deferReply({ ephemeral: true })
|
|
155
|
+
|
|
156
|
+
runModelMigrations()
|
|
157
|
+
|
|
158
|
+
const context = await resolveAgentCommandContext({ interaction, appId })
|
|
159
|
+
if (!context) {
|
|
93
160
|
return
|
|
94
161
|
}
|
|
95
162
|
|
|
96
163
|
try {
|
|
97
|
-
const getClient = await initializeOpencodeForDirectory(
|
|
164
|
+
const getClient = await initializeOpencodeForDirectory(context.dir)
|
|
165
|
+
if (errore.isError(getClient)) {
|
|
166
|
+
await interaction.editReply({ content: getClient.message })
|
|
167
|
+
return
|
|
168
|
+
}
|
|
98
169
|
|
|
99
170
|
const agentsResponse = await getClient().app.agents({
|
|
100
|
-
query: { directory:
|
|
171
|
+
query: { directory: context.dir },
|
|
101
172
|
})
|
|
102
173
|
|
|
103
174
|
if (!agentsResponse.data || agentsResponse.data.length === 0) {
|
|
@@ -106,7 +177,10 @@ export async function handleAgentCommand({
|
|
|
106
177
|
}
|
|
107
178
|
|
|
108
179
|
const agents = agentsResponse.data
|
|
109
|
-
.filter((
|
|
180
|
+
.filter((agent) => {
|
|
181
|
+
const hidden = (agent as { hidden?: boolean }).hidden
|
|
182
|
+
return (agent.mode === 'primary' || agent.mode === 'all') && !hidden
|
|
183
|
+
})
|
|
110
184
|
.slice(0, 25)
|
|
111
185
|
|
|
112
186
|
if (agents.length === 0) {
|
|
@@ -115,12 +189,7 @@ export async function handleAgentCommand({
|
|
|
115
189
|
}
|
|
116
190
|
|
|
117
191
|
const contextHash = crypto.randomBytes(8).toString('hex')
|
|
118
|
-
pendingAgentContexts.set(contextHash,
|
|
119
|
-
dir: projectDirectory,
|
|
120
|
-
channelId: targetChannelId,
|
|
121
|
-
sessionId,
|
|
122
|
-
isThread,
|
|
123
|
-
})
|
|
192
|
+
pendingAgentContexts.set(contextHash, context)
|
|
124
193
|
|
|
125
194
|
const options = agents.map((agent) => ({
|
|
126
195
|
label: agent.name.slice(0, 100),
|
|
@@ -179,18 +248,14 @@ export async function handleAgentSelectMenu(
|
|
|
179
248
|
}
|
|
180
249
|
|
|
181
250
|
try {
|
|
182
|
-
|
|
183
|
-
setSessionAgent(context.sessionId, selectedAgent)
|
|
184
|
-
agentLogger.log(`Set agent ${selectedAgent} for session ${context.sessionId}`)
|
|
251
|
+
setAgentForContext({ context, agentName: selectedAgent })
|
|
185
252
|
|
|
253
|
+
if (context.isThread && context.sessionId) {
|
|
186
254
|
await interaction.editReply({
|
|
187
255
|
content: `Agent preference set for this session: **${selectedAgent}**`,
|
|
188
256
|
components: [],
|
|
189
257
|
})
|
|
190
258
|
} else {
|
|
191
|
-
setChannelAgent(context.channelId, selectedAgent)
|
|
192
|
-
agentLogger.log(`Set agent ${selectedAgent} for channel ${context.channelId}`)
|
|
193
|
-
|
|
194
259
|
await interaction.editReply({
|
|
195
260
|
content: `Agent preference set for this channel: **${selectedAgent}**\n\nAll new sessions in this channel will use this agent.`,
|
|
196
261
|
components: [],
|
|
@@ -206,3 +271,73 @@ export async function handleAgentSelectMenu(
|
|
|
206
271
|
})
|
|
207
272
|
}
|
|
208
273
|
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Handle quick agent commands like /plan-agent, /build-agent.
|
|
277
|
+
* These instantly switch to the specified agent without showing a dropdown.
|
|
278
|
+
*/
|
|
279
|
+
export async function handleQuickAgentCommand({
|
|
280
|
+
command,
|
|
281
|
+
appId,
|
|
282
|
+
}: {
|
|
283
|
+
command: ChatInputCommandInteraction
|
|
284
|
+
appId: string
|
|
285
|
+
}): Promise<void> {
|
|
286
|
+
await command.deferReply({ ephemeral: true })
|
|
287
|
+
|
|
288
|
+
runModelMigrations()
|
|
289
|
+
|
|
290
|
+
// Extract agent name from command: "plan-agent" → "plan"
|
|
291
|
+
const sanitizedAgentName = command.commandName.replace(/-agent$/, '')
|
|
292
|
+
|
|
293
|
+
const context = await resolveAgentCommandContext({ interaction: command, appId })
|
|
294
|
+
if (!context) {
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const getClient = await initializeOpencodeForDirectory(context.dir)
|
|
300
|
+
if (errore.isError(getClient)) {
|
|
301
|
+
await command.editReply({ content: getClient.message })
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const agentsResponse = await getClient().app.agents({
|
|
306
|
+
query: { directory: context.dir },
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
if (!agentsResponse.data || agentsResponse.data.length === 0) {
|
|
310
|
+
await command.editReply({ content: 'No agents available in this project' })
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Find the agent matching the sanitized command name
|
|
315
|
+
const matchingAgent = agentsResponse.data.find(
|
|
316
|
+
(a) => sanitizeAgentName(a.name) === sanitizedAgentName
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if (!matchingAgent) {
|
|
320
|
+
await command.editReply({
|
|
321
|
+
content: `Agent not found. Available agents: ${agentsResponse.data.map((a) => a.name).join(', ')}`,
|
|
322
|
+
})
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
setAgentForContext({ context, agentName: matchingAgent.name })
|
|
327
|
+
|
|
328
|
+
if (context.isThread && context.sessionId) {
|
|
329
|
+
await command.editReply({
|
|
330
|
+
content: `Switched to **${matchingAgent.name}** agent for this session`,
|
|
331
|
+
})
|
|
332
|
+
} else {
|
|
333
|
+
await command.editReply({
|
|
334
|
+
content: `Switched to **${matchingAgent.name}** agent for this channel\n\nAll new sessions will use this agent.`,
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
agentLogger.error('Error in quick agent command:', error)
|
|
339
|
+
await command.editReply({
|
|
340
|
+
content: `Failed to switch agent: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
341
|
+
})
|
|
342
|
+
}
|
|
343
|
+
}
|
package/src/commands/fork.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
|
14
14
|
import { resolveTextChannel, getKimakiMetadata, sendThreadMessage } from '../discord-utils.js'
|
|
15
15
|
import { collectLastAssistantParts } from '../message-formatting.js'
|
|
16
16
|
import { createLogger } from '../logger.js'
|
|
17
|
+
import * as errore from 'errore'
|
|
17
18
|
|
|
18
19
|
const sessionLogger = createLogger('SESSION')
|
|
19
20
|
const forkLogger = createLogger('FORK')
|
|
@@ -71,9 +72,15 @@ export async function handleForkCommand(interaction: ChatInputCommandInteraction
|
|
|
71
72
|
|
|
72
73
|
const sessionId = row.session_id
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
const getClient = await initializeOpencodeForDirectory(directory)
|
|
76
|
+
if (errore.isError(getClient)) {
|
|
77
|
+
await interaction.editReply({
|
|
78
|
+
content: `Failed to load messages: ${getClient.message}`,
|
|
79
|
+
})
|
|
80
|
+
return
|
|
81
|
+
}
|
|
76
82
|
|
|
83
|
+
try {
|
|
77
84
|
const messagesResponse = await getClient().session.messages({
|
|
78
85
|
path: { id: sessionId },
|
|
79
86
|
})
|
|
@@ -85,7 +92,7 @@ export async function handleForkCommand(interaction: ChatInputCommandInteraction
|
|
|
85
92
|
return
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
const userMessages = messagesResponse.data.filter((m) => m.info.role === 'user')
|
|
95
|
+
const userMessages = messagesResponse.data.filter((m: { info: { role: string } }) => m.info.role === 'user')
|
|
89
96
|
|
|
90
97
|
if (userMessages.length === 0) {
|
|
91
98
|
await interaction.editReply({
|
|
@@ -96,8 +103,8 @@ export async function handleForkCommand(interaction: ChatInputCommandInteraction
|
|
|
96
103
|
|
|
97
104
|
const recentMessages = userMessages.slice(-25)
|
|
98
105
|
|
|
99
|
-
const options = recentMessages.map((m, index) => {
|
|
100
|
-
const textPart = m.parts.find((p) => p.type === 'text') as
|
|
106
|
+
const options = recentMessages.map((m: { parts: Array<{ type: string; text?: string }>; info: { id: string; time: { created: number } } }, index: number) => {
|
|
107
|
+
const textPart = m.parts.find((p: { type: string }) => p.type === 'text') as
|
|
101
108
|
| { type: 'text'; text: string }
|
|
102
109
|
| undefined
|
|
103
110
|
const preview = textPart?.text?.slice(0, 80) || '(no text)'
|
|
@@ -163,9 +170,13 @@ export async function handleForkSelectMenu(
|
|
|
163
170
|
|
|
164
171
|
await interaction.deferReply({ ephemeral: false })
|
|
165
172
|
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
const getClient = await initializeOpencodeForDirectory(directory)
|
|
174
|
+
if (errore.isError(getClient)) {
|
|
175
|
+
await interaction.editReply(`Failed to fork session: ${getClient.message}`)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
168
178
|
|
|
179
|
+
try {
|
|
169
180
|
const forkResponse = await getClient().session.fork({
|
|
170
181
|
path: { id: sessionId },
|
|
171
182
|
body: { messageID: selectedMessageId },
|
package/src/commands/model.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
|
15
15
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
|
|
16
16
|
import { abortAndRetrySession } from '../session-handler.js'
|
|
17
17
|
import { createLogger } from '../logger.js'
|
|
18
|
+
import * as errore from 'errore'
|
|
18
19
|
|
|
19
20
|
const modelLogger = createLogger('MODEL')
|
|
20
21
|
|
|
@@ -128,6 +129,10 @@ export async function handleModelCommand({
|
|
|
128
129
|
|
|
129
130
|
try {
|
|
130
131
|
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
132
|
+
if (errore.isError(getClient)) {
|
|
133
|
+
await interaction.editReply({ content: getClient.message })
|
|
134
|
+
return
|
|
135
|
+
}
|
|
131
136
|
|
|
132
137
|
const providersResponse = await getClient().provider.list({
|
|
133
138
|
query: { directory: projectDirectory },
|
|
@@ -232,6 +237,13 @@ export async function handleProviderSelectMenu(
|
|
|
232
237
|
|
|
233
238
|
try {
|
|
234
239
|
const getClient = await initializeOpencodeForDirectory(context.dir)
|
|
240
|
+
if (errore.isError(getClient)) {
|
|
241
|
+
await interaction.editReply({
|
|
242
|
+
content: getClient.message,
|
|
243
|
+
components: [],
|
|
244
|
+
})
|
|
245
|
+
return
|
|
246
|
+
}
|
|
235
247
|
|
|
236
248
|
const providersResponse = await getClient().provider.list({
|
|
237
249
|
query: { directory: context.dir },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// /remove-project command - Remove Discord channels for a project.
|
|
2
2
|
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import * as errore from 'errore'
|
|
4
5
|
import type { CommandContext, AutocompleteContext } from './types.js'
|
|
5
6
|
import { getDatabase } from '../database.js'
|
|
6
7
|
import { createLogger } from '../logger.js'
|
|
@@ -36,19 +37,27 @@ export async function handleRemoveProjectCommand({ command, appId }: CommandCont
|
|
|
36
37
|
const failedChannels: string[] = []
|
|
37
38
|
|
|
38
39
|
for (const { channel_id, channel_type } of channels) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const channel = await errore.tryAsync({
|
|
41
|
+
try: () => guild.channels.fetch(channel_id),
|
|
42
|
+
catch: (e) => e as Error,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if (errore.isError(channel)) {
|
|
46
|
+
logger.error(`Failed to fetch channel ${channel_id}:`, channel)
|
|
47
|
+
failedChannels.push(`${channel_type}: ${channel_id}`)
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (channel) {
|
|
52
|
+
try {
|
|
43
53
|
await channel.delete(`Removed by /remove-project command`)
|
|
44
54
|
deletedChannels.push(`${channel_type}: ${channel_id}`)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.error(`Failed to delete channel ${channel_id}:`, error)
|
|
57
|
+
failedChannels.push(`${channel_type}: ${channel_id}`)
|
|
48
58
|
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
failedChannels.push(`${channel_type}: ${channel_id}`)
|
|
59
|
+
} else {
|
|
60
|
+
deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`)
|
|
52
61
|
}
|
|
53
62
|
}
|
|
54
63
|
|
|
@@ -103,13 +112,16 @@ export async function handleRemoveProjectAutocomplete({
|
|
|
103
112
|
const projectsInGuild: { directory: string; channelId: string }[] = []
|
|
104
113
|
|
|
105
114
|
for (const { directory, channel_id } of allChannels) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
} catch {
|
|
115
|
+
const channel = await errore.tryAsync({
|
|
116
|
+
try: () => guild.channels.fetch(channel_id),
|
|
117
|
+
catch: (e) => e as Error,
|
|
118
|
+
})
|
|
119
|
+
if (errore.isError(channel)) {
|
|
112
120
|
// Channel not in this guild, skip
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
if (channel) {
|
|
124
|
+
projectsInGuild.push({ directory, channelId: channel_id })
|
|
113
125
|
}
|
|
114
126
|
}
|
|
115
127
|
|
package/src/commands/resume.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { sendThreadMessage, resolveTextChannel, getKimakiMetadata } from '../dis
|
|
|
14
14
|
import { extractTagsArrays } from '../xml.js'
|
|
15
15
|
import { collectLastAssistantParts } from '../message-formatting.js'
|
|
16
16
|
import { createLogger } from '../logger.js'
|
|
17
|
+
import * as errore from 'errore'
|
|
17
18
|
|
|
18
19
|
const logger = createLogger('RESUME')
|
|
19
20
|
|
|
@@ -60,6 +61,10 @@ export async function handleResumeCommand({ command, appId }: CommandContext): P
|
|
|
60
61
|
|
|
61
62
|
try {
|
|
62
63
|
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
64
|
+
if (errore.isError(getClient)) {
|
|
65
|
+
await command.editReply(getClient.message)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
63
68
|
|
|
64
69
|
const sessionResponse = await getClient().session.get({
|
|
65
70
|
path: { id: sessionId },
|
|
@@ -168,6 +173,10 @@ export async function handleResumeAutocomplete({
|
|
|
168
173
|
|
|
169
174
|
try {
|
|
170
175
|
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
176
|
+
if (errore.isError(getClient)) {
|
|
177
|
+
await interaction.respond([])
|
|
178
|
+
return
|
|
179
|
+
}
|
|
171
180
|
|
|
172
181
|
const sessionsResponse = await getClient().session.list()
|
|
173
182
|
if (!sessionsResponse.data) {
|
package/src/commands/session.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
|
10
10
|
import { extractTagsArrays } from '../xml.js'
|
|
11
11
|
import { handleOpencodeSession } from '../session-handler.js'
|
|
12
12
|
import { createLogger } from '../logger.js'
|
|
13
|
+
import * as errore from 'errore'
|
|
13
14
|
|
|
14
15
|
const logger = createLogger('SESSION')
|
|
15
16
|
|
|
@@ -58,6 +59,10 @@ export async function handleSessionCommand({ command, appId }: CommandContext):
|
|
|
58
59
|
|
|
59
60
|
try {
|
|
60
61
|
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
62
|
+
if (errore.isError(getClient)) {
|
|
63
|
+
await command.editReply(getClient.message)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
61
66
|
|
|
62
67
|
const files = filesString
|
|
63
68
|
.split(',')
|
|
@@ -128,6 +133,10 @@ async function handleAgentAutocomplete({ interaction, appId }: AutocompleteConte
|
|
|
128
133
|
|
|
129
134
|
try {
|
|
130
135
|
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
136
|
+
if (errore.isError(getClient)) {
|
|
137
|
+
await interaction.respond([])
|
|
138
|
+
return
|
|
139
|
+
}
|
|
131
140
|
|
|
132
141
|
const agentsResponse = await getClient().app.agents({
|
|
133
142
|
query: { directory: projectDirectory },
|
|
@@ -207,6 +216,10 @@ export async function handleSessionAutocomplete({
|
|
|
207
216
|
|
|
208
217
|
try {
|
|
209
218
|
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
219
|
+
if (errore.isError(getClient)) {
|
|
220
|
+
await interaction.respond([])
|
|
221
|
+
return
|
|
222
|
+
}
|
|
210
223
|
|
|
211
224
|
const response = await getClient().find.files({
|
|
212
225
|
query: {
|
package/src/commands/share.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { getDatabase } from '../database.js'
|
|
|
6
6
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
7
7
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
8
8
|
import { createLogger } from '../logger.js'
|
|
9
|
+
import * as errore from 'errore'
|
|
9
10
|
|
|
10
11
|
const logger = createLogger('SHARE')
|
|
11
12
|
|
|
@@ -63,8 +64,17 @@ export async function handleShareCommand({ command }: CommandContext): Promise<v
|
|
|
63
64
|
|
|
64
65
|
const sessionId = row.session_id
|
|
65
66
|
|
|
67
|
+
const getClient = await initializeOpencodeForDirectory(directory)
|
|
68
|
+
if (errore.isError(getClient)) {
|
|
69
|
+
await command.reply({
|
|
70
|
+
content: `Failed to share session: ${getClient.message}`,
|
|
71
|
+
ephemeral: true,
|
|
72
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
73
|
+
})
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
try {
|
|
67
|
-
const getClient = await initializeOpencodeForDirectory(directory)
|
|
68
78
|
const response = await getClient().session.share({
|
|
69
79
|
path: { id: sessionId },
|
|
70
80
|
})
|
|
@@ -6,6 +6,7 @@ import { getDatabase } from '../database.js'
|
|
|
6
6
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
7
7
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
8
8
|
import { createLogger } from '../logger.js'
|
|
9
|
+
import * as errore from 'errore'
|
|
9
10
|
|
|
10
11
|
const logger = createLogger('UNDO-REDO')
|
|
11
12
|
|
|
@@ -63,11 +64,15 @@ export async function handleUndoCommand({ command }: CommandContext): Promise<vo
|
|
|
63
64
|
|
|
64
65
|
const sessionId = row.session_id
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
67
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
const getClient = await initializeOpencodeForDirectory(directory)
|
|
70
|
+
if (errore.isError(getClient)) {
|
|
71
|
+
await command.editReply(`Failed to undo: ${getClient.message}`)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
70
74
|
|
|
75
|
+
try {
|
|
71
76
|
// Fetch messages to find the last assistant message
|
|
72
77
|
const messagesResponse = await getClient().session.messages({
|
|
73
78
|
path: { id: sessionId },
|
|
@@ -166,11 +171,15 @@ export async function handleRedoCommand({ command }: CommandContext): Promise<vo
|
|
|
166
171
|
|
|
167
172
|
const sessionId = row.session_id
|
|
168
173
|
|
|
169
|
-
|
|
170
|
-
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
174
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
171
175
|
|
|
172
|
-
|
|
176
|
+
const getClient = await initializeOpencodeForDirectory(directory)
|
|
177
|
+
if (errore.isError(getClient)) {
|
|
178
|
+
await command.editReply(`Failed to redo: ${getClient.message}`)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
173
181
|
|
|
182
|
+
try {
|
|
174
183
|
// Check if session has reverted state
|
|
175
184
|
const sessionResponse = await getClient().session.get({
|
|
176
185
|
path: { id: sessionId },
|
package/src/database.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import Database from 'better-sqlite3'
|
|
6
6
|
import fs from 'node:fs'
|
|
7
7
|
import path from 'node:path'
|
|
8
|
+
import * as errore from 'errore'
|
|
8
9
|
import { createLogger } from './logger.js'
|
|
9
10
|
import { getDataDir } from './config.js'
|
|
10
11
|
|
|
@@ -16,10 +17,14 @@ export function getDatabase(): Database.Database {
|
|
|
16
17
|
if (!db) {
|
|
17
18
|
const dataDir = getDataDir()
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const mkdirError = errore.tryFn({
|
|
21
|
+
try: () => {
|
|
22
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
23
|
+
},
|
|
24
|
+
catch: (e) => e as Error,
|
|
25
|
+
})
|
|
26
|
+
if (errore.isError(mkdirError)) {
|
|
27
|
+
dbLogger.error(`Failed to create data directory ${dataDir}:`, mkdirError.message)
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
const dbPath = path.join(dataDir, 'discord-sessions.db')
|
|
@@ -68,6 +73,14 @@ export function getDatabase(): Database.Database {
|
|
|
68
73
|
// Column already exists, ignore
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
// Table for threads that should auto-start a session (created by CLI without --notify-only)
|
|
77
|
+
db.exec(`
|
|
78
|
+
CREATE TABLE IF NOT EXISTS pending_auto_start (
|
|
79
|
+
thread_id TEXT PRIMARY KEY,
|
|
80
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
81
|
+
)
|
|
82
|
+
`)
|
|
83
|
+
|
|
71
84
|
db.exec(`
|
|
72
85
|
CREATE TABLE IF NOT EXISTS bot_api_keys (
|
|
73
86
|
app_id TEXT PRIMARY KEY,
|
|
@@ -176,6 +189,15 @@ export function setSessionModel(sessionId: string, modelId: string): void {
|
|
|
176
189
|
)
|
|
177
190
|
}
|
|
178
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Clear the model preference for a session.
|
|
194
|
+
* Used when switching agents so the agent's model takes effect.
|
|
195
|
+
*/
|
|
196
|
+
export function clearSessionModel(sessionId: string): void {
|
|
197
|
+
const db = getDatabase()
|
|
198
|
+
db.prepare('DELETE FROM session_models WHERE session_id = ?').run(sessionId)
|
|
199
|
+
}
|
|
200
|
+
|
|
179
201
|
/**
|
|
180
202
|
* Get the agent preference for a channel.
|
|
181
203
|
*/
|