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.
Files changed (86) hide show
  1. package/bin.js +6 -1
  2. package/dist/acp-client.test.js +149 -0
  3. package/dist/ai-tool-to-genai.js +3 -0
  4. package/dist/channel-management.js +14 -9
  5. package/dist/cli.js +148 -17
  6. package/dist/commands/abort.js +78 -0
  7. package/dist/commands/add-project.js +98 -0
  8. package/dist/commands/agent.js +152 -0
  9. package/dist/commands/ask-question.js +183 -0
  10. package/dist/commands/create-new-project.js +78 -0
  11. package/dist/commands/fork.js +186 -0
  12. package/dist/commands/model.js +313 -0
  13. package/dist/commands/permissions.js +126 -0
  14. package/dist/commands/queue.js +129 -0
  15. package/dist/commands/resume.js +145 -0
  16. package/dist/commands/session.js +142 -0
  17. package/dist/commands/share.js +80 -0
  18. package/dist/commands/types.js +2 -0
  19. package/dist/commands/undo-redo.js +161 -0
  20. package/dist/commands/user-command.js +145 -0
  21. package/dist/database.js +54 -0
  22. package/dist/discord-bot.js +35 -32
  23. package/dist/discord-utils.js +81 -15
  24. package/dist/format-tables.js +3 -0
  25. package/dist/genai-worker-wrapper.js +3 -0
  26. package/dist/genai-worker.js +3 -0
  27. package/dist/genai.js +3 -0
  28. package/dist/interaction-handler.js +89 -695
  29. package/dist/logger.js +46 -5
  30. package/dist/markdown.js +107 -0
  31. package/dist/markdown.test.js +31 -1
  32. package/dist/message-formatting.js +113 -28
  33. package/dist/message-formatting.test.js +73 -0
  34. package/dist/opencode.js +73 -16
  35. package/dist/session-handler.js +176 -63
  36. package/dist/system-message.js +7 -38
  37. package/dist/tools.js +3 -0
  38. package/dist/utils.js +3 -0
  39. package/dist/voice-handler.js +21 -8
  40. package/dist/voice.js +31 -12
  41. package/dist/worker-types.js +3 -0
  42. package/dist/xml.js +3 -0
  43. package/package.json +3 -3
  44. package/src/__snapshots__/compact-session-context-no-system.md +35 -0
  45. package/src/__snapshots__/compact-session-context.md +47 -0
  46. package/src/ai-tool-to-genai.ts +4 -0
  47. package/src/channel-management.ts +24 -8
  48. package/src/cli.ts +163 -18
  49. package/src/commands/abort.ts +94 -0
  50. package/src/commands/add-project.ts +139 -0
  51. package/src/commands/agent.ts +201 -0
  52. package/src/commands/ask-question.ts +276 -0
  53. package/src/commands/create-new-project.ts +111 -0
  54. package/src/{fork.ts → commands/fork.ts} +40 -7
  55. package/src/{model-command.ts → commands/model.ts} +31 -9
  56. package/src/commands/permissions.ts +146 -0
  57. package/src/commands/queue.ts +181 -0
  58. package/src/commands/resume.ts +230 -0
  59. package/src/commands/session.ts +184 -0
  60. package/src/commands/share.ts +96 -0
  61. package/src/commands/types.ts +25 -0
  62. package/src/commands/undo-redo.ts +213 -0
  63. package/src/commands/user-command.ts +178 -0
  64. package/src/database.ts +65 -0
  65. package/src/discord-bot.ts +40 -33
  66. package/src/discord-utils.ts +88 -14
  67. package/src/format-tables.ts +4 -0
  68. package/src/genai-worker-wrapper.ts +4 -0
  69. package/src/genai-worker.ts +4 -0
  70. package/src/genai.ts +4 -0
  71. package/src/interaction-handler.ts +111 -924
  72. package/src/logger.ts +51 -10
  73. package/src/markdown.test.ts +45 -1
  74. package/src/markdown.ts +136 -0
  75. package/src/message-formatting.test.ts +81 -0
  76. package/src/message-formatting.ts +143 -30
  77. package/src/opencode.ts +84 -21
  78. package/src/session-handler.ts +248 -91
  79. package/src/system-message.ts +8 -38
  80. package/src/tools.ts +4 -0
  81. package/src/utils.ts +4 -0
  82. package/src/voice-handler.ts +24 -9
  83. package/src/voice.ts +36 -13
  84. package/src/worker-types.ts +4 -0
  85. package/src/xml.ts +4 -0
  86. package/README.md +0 -48
@@ -0,0 +1,213 @@
1
+ // Undo/Redo commands - /undo, /redo
2
+
3
+ import { ChannelType, type ThreadChannel } from 'discord.js'
4
+ import type { CommandContext } from './types.js'
5
+ import { getDatabase } from '../database.js'
6
+ import { initializeOpencodeForDirectory } from '../opencode.js'
7
+ import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
+ import { createLogger } from '../logger.js'
9
+
10
+ const logger = createLogger('UNDO-REDO')
11
+
12
+ export async function handleUndoCommand({
13
+ command,
14
+ }: CommandContext): Promise<void> {
15
+ const channel = command.channel
16
+
17
+ if (!channel) {
18
+ await command.reply({
19
+ content: 'This command can only be used in a channel',
20
+ ephemeral: true,
21
+ flags: SILENT_MESSAGE_FLAGS,
22
+ })
23
+ return
24
+ }
25
+
26
+ const isThread = [
27
+ ChannelType.PublicThread,
28
+ ChannelType.PrivateThread,
29
+ ChannelType.AnnouncementThread,
30
+ ].includes(channel.type)
31
+
32
+ if (!isThread) {
33
+ await command.reply({
34
+ content: 'This command can only be used in a thread with an active session',
35
+ ephemeral: true,
36
+ flags: SILENT_MESSAGE_FLAGS,
37
+ })
38
+ return
39
+ }
40
+
41
+ const textChannel = await resolveTextChannel(channel as ThreadChannel)
42
+ const { projectDirectory: directory } = getKimakiMetadata(textChannel)
43
+
44
+ if (!directory) {
45
+ await command.reply({
46
+ content: 'Could not determine project directory for this channel',
47
+ ephemeral: true,
48
+ flags: SILENT_MESSAGE_FLAGS,
49
+ })
50
+ return
51
+ }
52
+
53
+ const row = getDatabase()
54
+ .prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
55
+ .get(channel.id) as { session_id: string } | undefined
56
+
57
+ if (!row?.session_id) {
58
+ await command.reply({
59
+ content: 'No active session in this thread',
60
+ ephemeral: true,
61
+ flags: SILENT_MESSAGE_FLAGS,
62
+ })
63
+ return
64
+ }
65
+
66
+ const sessionId = row.session_id
67
+
68
+ try {
69
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
70
+
71
+ const getClient = await initializeOpencodeForDirectory(directory)
72
+
73
+ // Fetch messages to find the last assistant message
74
+ const messagesResponse = await getClient().session.messages({
75
+ path: { id: sessionId },
76
+ })
77
+
78
+ if (!messagesResponse.data || messagesResponse.data.length === 0) {
79
+ await command.editReply('No messages to undo')
80
+ return
81
+ }
82
+
83
+ // Find the last assistant message
84
+ const lastAssistantMessage = [...messagesResponse.data]
85
+ .reverse()
86
+ .find((m) => m.info.role === 'assistant')
87
+
88
+ if (!lastAssistantMessage) {
89
+ await command.editReply('No assistant message to undo')
90
+ return
91
+ }
92
+
93
+ const response = await getClient().session.revert({
94
+ path: { id: sessionId },
95
+ body: { messageID: lastAssistantMessage.info.id },
96
+ })
97
+
98
+ if (response.error) {
99
+ await command.editReply(
100
+ `Failed to undo: ${JSON.stringify(response.error)}`,
101
+ )
102
+ return
103
+ }
104
+
105
+ const diffInfo = response.data?.revert?.diff
106
+ ? `\n\`\`\`diff\n${response.data.revert.diff.slice(0, 1500)}\n\`\`\``
107
+ : ''
108
+
109
+ await command.editReply(
110
+ `⏪ **Undone** - reverted last assistant message${diffInfo}`,
111
+ )
112
+ logger.log(
113
+ `Session ${sessionId} reverted message ${lastAssistantMessage.info.id}`,
114
+ )
115
+ } catch (error) {
116
+ logger.error('[UNDO] Error:', error)
117
+ await command.editReply(
118
+ `Failed to undo: ${error instanceof Error ? error.message : 'Unknown error'}`,
119
+ )
120
+ }
121
+ }
122
+
123
+ export async function handleRedoCommand({
124
+ command,
125
+ }: CommandContext): Promise<void> {
126
+ const channel = command.channel
127
+
128
+ if (!channel) {
129
+ await command.reply({
130
+ content: 'This command can only be used in a channel',
131
+ ephemeral: true,
132
+ flags: SILENT_MESSAGE_FLAGS,
133
+ })
134
+ return
135
+ }
136
+
137
+ const isThread = [
138
+ ChannelType.PublicThread,
139
+ ChannelType.PrivateThread,
140
+ ChannelType.AnnouncementThread,
141
+ ].includes(channel.type)
142
+
143
+ if (!isThread) {
144
+ await command.reply({
145
+ content: 'This command can only be used in a thread with an active session',
146
+ ephemeral: true,
147
+ flags: SILENT_MESSAGE_FLAGS,
148
+ })
149
+ return
150
+ }
151
+
152
+ const textChannel = await resolveTextChannel(channel as ThreadChannel)
153
+ const { projectDirectory: directory } = getKimakiMetadata(textChannel)
154
+
155
+ if (!directory) {
156
+ await command.reply({
157
+ content: 'Could not determine project directory for this channel',
158
+ ephemeral: true,
159
+ flags: SILENT_MESSAGE_FLAGS,
160
+ })
161
+ return
162
+ }
163
+
164
+ const row = getDatabase()
165
+ .prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
166
+ .get(channel.id) as { session_id: string } | undefined
167
+
168
+ if (!row?.session_id) {
169
+ await command.reply({
170
+ content: 'No active session in this thread',
171
+ ephemeral: true,
172
+ flags: SILENT_MESSAGE_FLAGS,
173
+ })
174
+ return
175
+ }
176
+
177
+ const sessionId = row.session_id
178
+
179
+ try {
180
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
181
+
182
+ const getClient = await initializeOpencodeForDirectory(directory)
183
+
184
+ // Check if session has reverted state
185
+ const sessionResponse = await getClient().session.get({
186
+ path: { id: sessionId },
187
+ })
188
+
189
+ if (!sessionResponse.data?.revert) {
190
+ await command.editReply('Nothing to redo - no previous undo found')
191
+ return
192
+ }
193
+
194
+ const response = await getClient().session.unrevert({
195
+ path: { id: sessionId },
196
+ })
197
+
198
+ if (response.error) {
199
+ await command.editReply(
200
+ `Failed to redo: ${JSON.stringify(response.error)}`,
201
+ )
202
+ return
203
+ }
204
+
205
+ await command.editReply(`⏩ **Restored** - session back to previous state`)
206
+ logger.log(`Session ${sessionId} unrevert completed`)
207
+ } catch (error) {
208
+ logger.error('[REDO] Error:', error)
209
+ await command.editReply(
210
+ `Failed to redo: ${error instanceof Error ? error.message : 'Unknown error'}`,
211
+ )
212
+ }
213
+ }
@@ -0,0 +1,178 @@
1
+ // User-defined OpenCode command handler.
2
+ // Handles slash commands that map to user-configured commands in opencode.json.
3
+
4
+ import type { CommandContext, CommandHandler } from './types.js'
5
+ import { ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
6
+ import { extractTagsArrays } from '../xml.js'
7
+ import { handleOpencodeSession } from '../session-handler.js'
8
+ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
9
+ import { createLogger } from '../logger.js'
10
+ import { getDatabase } from '../database.js'
11
+ import fs from 'node:fs'
12
+
13
+ const userCommandLogger = createLogger('USER_CMD')
14
+
15
+ export const handleUserCommand: CommandHandler = async ({
16
+ command,
17
+ appId,
18
+ }: CommandContext) => {
19
+ const discordCommandName = command.commandName
20
+ // Strip the -cmd suffix to get the actual OpenCode command name
21
+ const commandName = discordCommandName.replace(/-cmd$/, '')
22
+ const args = command.options.getString('arguments') || ''
23
+
24
+ userCommandLogger.log(
25
+ `Executing /${commandName} (from /${discordCommandName}) with args: ${args}`,
26
+ )
27
+
28
+ const channel = command.channel
29
+
30
+ userCommandLogger.log(
31
+ `Channel info: type=${channel?.type}, id=${channel?.id}, isNull=${channel === null}`,
32
+ )
33
+
34
+ const isThread = channel && [
35
+ ChannelType.PublicThread,
36
+ ChannelType.PrivateThread,
37
+ ChannelType.AnnouncementThread,
38
+ ].includes(channel.type)
39
+
40
+ const isTextChannel = channel?.type === ChannelType.GuildText
41
+
42
+ if (!channel || (!isTextChannel && !isThread)) {
43
+ await command.reply({
44
+ content: 'This command can only be used in text channels or threads',
45
+ ephemeral: true,
46
+ })
47
+ return
48
+ }
49
+
50
+ let projectDirectory: string | undefined
51
+ let channelAppId: string | undefined
52
+ let textChannel: TextChannel | null = null
53
+ let thread: ThreadChannel | null = null
54
+
55
+ if (isThread) {
56
+ // Running in an existing thread - get project directory from parent channel
57
+ thread = channel as ThreadChannel
58
+ textChannel = thread.parent as TextChannel | null
59
+
60
+ // Verify this thread has an existing session
61
+ const row = getDatabase()
62
+ .prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
63
+ .get(thread.id) as { session_id: string } | undefined
64
+
65
+ if (!row) {
66
+ await command.reply({
67
+ content: 'This thread does not have an active session. Use this command in a project channel to create a new thread.',
68
+ ephemeral: true,
69
+ })
70
+ return
71
+ }
72
+
73
+ if (textChannel?.topic) {
74
+ const extracted = extractTagsArrays({
75
+ xml: textChannel.topic,
76
+ tags: ['kimaki.directory', 'kimaki.app'],
77
+ })
78
+
79
+ projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
80
+ channelAppId = extracted['kimaki.app']?.[0]?.trim()
81
+ }
82
+ } else {
83
+ // Running in a text channel - will create a new thread
84
+ textChannel = channel as TextChannel
85
+
86
+ if (textChannel.topic) {
87
+ const extracted = extractTagsArrays({
88
+ xml: textChannel.topic,
89
+ tags: ['kimaki.directory', 'kimaki.app'],
90
+ })
91
+
92
+ projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
93
+ channelAppId = extracted['kimaki.app']?.[0]?.trim()
94
+ }
95
+ }
96
+
97
+ if (channelAppId && channelAppId !== appId) {
98
+ await command.reply({
99
+ content: 'This channel is not configured for this bot',
100
+ ephemeral: true,
101
+ })
102
+ return
103
+ }
104
+
105
+ if (!projectDirectory) {
106
+ await command.reply({
107
+ content: 'This channel is not configured with a project directory',
108
+ ephemeral: true,
109
+ })
110
+ return
111
+ }
112
+
113
+ if (!fs.existsSync(projectDirectory)) {
114
+ await command.reply({
115
+ content: `Directory does not exist: ${projectDirectory}`,
116
+ ephemeral: true,
117
+ })
118
+ return
119
+ }
120
+
121
+ await command.deferReply({ ephemeral: false })
122
+
123
+ try {
124
+ // Use the dedicated session.command API instead of formatting as text prompt
125
+ const commandPayload = { name: commandName, arguments: args }
126
+
127
+ if (isThread && thread) {
128
+ // Running in existing thread - just send the command
129
+ await command.editReply(`Running /${commandName}...`)
130
+
131
+ await handleOpencodeSession({
132
+ prompt: '', // Not used when command is set
133
+ thread,
134
+ projectDirectory,
135
+ channelId: textChannel?.id,
136
+ command: commandPayload,
137
+ })
138
+ } else if (textChannel) {
139
+ // Running in text channel - create a new thread
140
+ const starterMessage = await textChannel.send({
141
+ content: `**/${commandName}**${args ? ` ${args.slice(0, 200)}${args.length > 200 ? '…' : ''}` : ''}`,
142
+ flags: SILENT_MESSAGE_FLAGS,
143
+ })
144
+
145
+ const threadName = `/${commandName} ${args.slice(0, 80)}${args.length > 80 ? '…' : ''}`
146
+ const newThread = await starterMessage.startThread({
147
+ name: threadName.slice(0, 100),
148
+ autoArchiveDuration: 1440,
149
+ reason: `OpenCode command: ${commandName}`,
150
+ })
151
+
152
+ await command.editReply(`Started /${commandName} in ${newThread.toString()}`)
153
+
154
+ await handleOpencodeSession({
155
+ prompt: '', // Not used when command is set
156
+ thread: newThread,
157
+ projectDirectory,
158
+ channelId: textChannel.id,
159
+ command: commandPayload,
160
+ })
161
+ }
162
+ } catch (error) {
163
+ userCommandLogger.error(`Error executing /${commandName}:`, error)
164
+
165
+ const errorMessage = error instanceof Error ? error.message : String(error)
166
+
167
+ if (command.deferred) {
168
+ await command.editReply({
169
+ content: `Failed to execute /${commandName}: ${errorMessage}`,
170
+ })
171
+ } else {
172
+ await command.reply({
173
+ content: `Failed to execute /${commandName}: ${errorMessage}`,
174
+ ephemeral: true,
175
+ })
176
+ }
177
+ }
178
+ }
package/src/database.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // SQLite database manager for persistent bot state.
2
+ // Stores thread-session mappings, bot tokens, channel directories,
3
+ // API keys, and model preferences in ~/.kimaki/discord-sessions.db.
4
+
1
5
  import Database from 'better-sqlite3'
2
6
  import fs from 'node:fs'
3
7
  import os from 'node:os'
@@ -96,6 +100,23 @@ export function runModelMigrations(database?: Database.Database): void {
96
100
  )
97
101
  `)
98
102
 
103
+ targetDb.exec(`
104
+ CREATE TABLE IF NOT EXISTS channel_agents (
105
+ channel_id TEXT PRIMARY KEY,
106
+ agent_name TEXT NOT NULL,
107
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
108
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
109
+ )
110
+ `)
111
+
112
+ targetDb.exec(`
113
+ CREATE TABLE IF NOT EXISTS session_agents (
114
+ session_id TEXT PRIMARY KEY,
115
+ agent_name TEXT NOT NULL,
116
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
117
+ )
118
+ `)
119
+
99
120
  dbLogger.log('Model preferences migrations complete')
100
121
  }
101
122
 
@@ -147,6 +168,50 @@ export function setSessionModel(sessionId: string, modelId: string): void {
147
168
  ).run(sessionId, modelId)
148
169
  }
149
170
 
171
+ /**
172
+ * Get the agent preference for a channel.
173
+ */
174
+ export function getChannelAgent(channelId: string): string | undefined {
175
+ const db = getDatabase()
176
+ const row = db
177
+ .prepare('SELECT agent_name FROM channel_agents WHERE channel_id = ?')
178
+ .get(channelId) as { agent_name: string } | undefined
179
+ return row?.agent_name
180
+ }
181
+
182
+ /**
183
+ * Set the agent preference for a channel.
184
+ */
185
+ export function setChannelAgent(channelId: string, agentName: string): void {
186
+ const db = getDatabase()
187
+ db.prepare(
188
+ `INSERT INTO channel_agents (channel_id, agent_name, updated_at)
189
+ VALUES (?, ?, CURRENT_TIMESTAMP)
190
+ ON CONFLICT(channel_id) DO UPDATE SET agent_name = ?, updated_at = CURRENT_TIMESTAMP`
191
+ ).run(channelId, agentName, agentName)
192
+ }
193
+
194
+ /**
195
+ * Get the agent preference for a session.
196
+ */
197
+ export function getSessionAgent(sessionId: string): string | undefined {
198
+ const db = getDatabase()
199
+ const row = db
200
+ .prepare('SELECT agent_name FROM session_agents WHERE session_id = ?')
201
+ .get(sessionId) as { agent_name: string } | undefined
202
+ return row?.agent_name
203
+ }
204
+
205
+ /**
206
+ * Set the agent preference for a session.
207
+ */
208
+ export function setSessionAgent(sessionId: string, agentName: string): void {
209
+ const db = getDatabase()
210
+ db.prepare(
211
+ `INSERT OR REPLACE INTO session_agents (session_id, agent_name) VALUES (?, ?)`
212
+ ).run(sessionId, agentName)
213
+ }
214
+
150
215
  export function closeDatabase(): void {
151
216
  if (db) {
152
217
  db.close()
@@ -1,3 +1,7 @@
1
+ // Core Discord bot module that handles message events and bot lifecycle.
2
+ // Bridges Discord messages to OpenCode sessions, manages voice connections,
3
+ // and orchestrates the main event loop for the Kimaki bot.
4
+
1
5
  import { getDatabase, closeDatabase } from './database.js'
2
6
  import { initializeOpencodeForDirectory, getOpencodeServers } from './opencode.js'
3
7
  import {
@@ -21,9 +25,10 @@ import {
21
25
  registerVoiceStateHandler,
22
26
  } from './voice-handler.js'
23
27
  import {
24
- handleOpencodeSession,
25
- parseSlashCommand,
26
- } from './session-handler.js'
28
+ getCompactSessionContext,
29
+ getLastSessionId,
30
+ } from './markdown.js'
31
+ import { handleOpencodeSession } from './session-handler.js'
27
32
  import { registerInteractionHandler } from './interaction-handler.js'
28
33
 
29
34
  export { getDatabase, closeDatabase } from './database.js'
@@ -236,34 +241,39 @@ export async function startDiscordBot({
236
241
 
237
242
  let messageContent = message.content || ''
238
243
 
239
- let sessionMessagesText: string | undefined
240
- if (projectDirectory && row.session_id) {
244
+ let currentSessionContext: string | undefined
245
+ let lastSessionContext: string | undefined
246
+
247
+ if (projectDirectory) {
241
248
  try {
242
249
  const getClient = await initializeOpencodeForDirectory(projectDirectory)
243
- const messagesResponse = await getClient().session.messages({
244
- path: { id: row.session_id },
250
+ const client = getClient()
251
+
252
+ // get current session context (without system prompt, it would be duplicated)
253
+ if (row.session_id) {
254
+ currentSessionContext = await getCompactSessionContext({
255
+ client,
256
+ sessionId: row.session_id,
257
+ includeSystemPrompt: false,
258
+ maxMessages: 15,
259
+ })
260
+ }
261
+
262
+ // get last session context (with system prompt for project context)
263
+ const lastSessionId = await getLastSessionId({
264
+ client,
265
+ excludeSessionId: row.session_id,
245
266
  })
246
- const messages = messagesResponse.data || []
247
- const recentMessages = messages.slice(-10)
248
- sessionMessagesText = recentMessages
249
- .map((m) => {
250
- const role = m.info.role === 'user' ? 'User' : 'Assistant'
251
- const text = (() => {
252
- if (m.info.role === 'user') {
253
- const textParts = (m.parts || []).filter((p) => p.type === 'text')
254
- return textParts
255
- .map((p) => ('text' in p ? p.text : ''))
256
- .filter(Boolean)
257
- .join('\n')
258
- }
259
- const assistantInfo = m.info as { text?: string }
260
- return assistantInfo.text?.slice(0, 500)
261
- })()
262
- return `[${role}]: ${text || '(no text)'}`
267
+ if (lastSessionId) {
268
+ lastSessionContext = await getCompactSessionContext({
269
+ client,
270
+ sessionId: lastSessionId,
271
+ includeSystemPrompt: true,
272
+ maxMessages: 10,
263
273
  })
264
- .join('\n\n')
274
+ }
265
275
  } catch (e) {
266
- voiceLogger.log(`Could not get session messages:`, e)
276
+ voiceLogger.error(`Could not get session context:`, e)
267
277
  }
268
278
  }
269
279
 
@@ -272,25 +282,24 @@ export async function startDiscordBot({
272
282
  thread,
273
283
  projectDirectory,
274
284
  appId: currentAppId,
275
- sessionMessages: sessionMessagesText,
285
+ currentSessionContext,
286
+ lastSessionContext,
276
287
  })
277
288
  if (transcription) {
278
289
  messageContent = transcription
279
290
  }
280
291
 
281
- const fileAttachments = getFileAttachments(message)
292
+ const fileAttachments = await getFileAttachments(message)
282
293
  const textAttachmentsContent = await getTextAttachments(message)
283
294
  const promptWithAttachments = textAttachmentsContent
284
295
  ? `${messageContent}\n\n${textAttachmentsContent}`
285
296
  : messageContent
286
- const parsedCommand = parseSlashCommand(messageContent)
287
297
  await handleOpencodeSession({
288
298
  prompt: promptWithAttachments,
289
299
  thread,
290
300
  projectDirectory,
291
301
  originalMessage: message,
292
302
  images: fileAttachments,
293
- parsedCommand,
294
303
  channelId: parent?.id,
295
304
  })
296
305
  return
@@ -376,19 +385,17 @@ export async function startDiscordBot({
376
385
  messageContent = transcription
377
386
  }
378
387
 
379
- const fileAttachments = getFileAttachments(message)
388
+ const fileAttachments = await getFileAttachments(message)
380
389
  const textAttachmentsContent = await getTextAttachments(message)
381
390
  const promptWithAttachments = textAttachmentsContent
382
391
  ? `${messageContent}\n\n${textAttachmentsContent}`
383
392
  : messageContent
384
- const parsedCommand = parseSlashCommand(messageContent)
385
393
  await handleOpencodeSession({
386
394
  prompt: promptWithAttachments,
387
395
  thread,
388
396
  projectDirectory,
389
397
  originalMessage: message,
390
398
  images: fileAttachments,
391
- parsedCommand,
392
399
  channelId: textChannel.id,
393
400
  })
394
401
  } else {