kimaki 0.4.10 → 0.4.13
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/discordBot.js +204 -106
- package/dist/escape-backticks.test.js +286 -1
- package/dist/tools.js +3 -2
- package/package.json +1 -1
- package/src/discordBot.ts +249 -130
- package/src/escape-backticks.test.ts +302 -1
- package/src/tools.ts +6 -3
package/dist/tools.js
CHANGED
|
@@ -8,7 +8,7 @@ const toolsLogger = createLogger('TOOLS');
|
|
|
8
8
|
import { formatDistanceToNow } from 'date-fns';
|
|
9
9
|
import { ShareMarkdown } from './markdown.js';
|
|
10
10
|
import pc from 'picocolors';
|
|
11
|
-
import { initializeOpencodeForDirectory } from './discordBot.js';
|
|
11
|
+
import { initializeOpencodeForDirectory, OPENCODE_SYSTEM_MESSAGE, } from './discordBot.js';
|
|
12
12
|
export async function getTools({ onMessageCompleted, directory, }) {
|
|
13
13
|
const getClient = await initializeOpencodeForDirectory(directory);
|
|
14
14
|
const client = getClient();
|
|
@@ -48,6 +48,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
48
48
|
body: {
|
|
49
49
|
parts: [{ type: 'text', text: message }],
|
|
50
50
|
model: sessionModel,
|
|
51
|
+
system: OPENCODE_SYSTEM_MESSAGE,
|
|
51
52
|
},
|
|
52
53
|
})
|
|
53
54
|
.then(async (response) => {
|
|
@@ -114,7 +115,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
114
115
|
path: { id: session.data.id },
|
|
115
116
|
body: {
|
|
116
117
|
parts: [{ type: 'text', text: message }],
|
|
117
|
-
|
|
118
|
+
system: OPENCODE_SYSTEM_MESSAGE,
|
|
118
119
|
},
|
|
119
120
|
})
|
|
120
121
|
.then(async (response) => {
|
package/package.json
CHANGED
package/src/discordBot.ts
CHANGED
|
@@ -54,6 +54,51 @@ import { setGlobalDispatcher, Agent } from 'undici'
|
|
|
54
54
|
// disables the automatic 5 minutes abort after no body
|
|
55
55
|
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 }))
|
|
56
56
|
|
|
57
|
+
export const OPENCODE_SYSTEM_MESSAGE = `
|
|
58
|
+
The user is reading your messages from inside Discord, via kimaki.xyz
|
|
59
|
+
|
|
60
|
+
After each message, if you implemented changes, you can show the user a diff via an url running the command, to show the changes in working directory:
|
|
61
|
+
|
|
62
|
+
bunx critique web
|
|
63
|
+
|
|
64
|
+
you can also show latest commit changes using
|
|
65
|
+
|
|
66
|
+
bunx critique web HEAD~1
|
|
67
|
+
|
|
68
|
+
do this in case you committed the changes yourself (only if the user asks so, never commit otherwise).
|
|
69
|
+
|
|
70
|
+
## markdown
|
|
71
|
+
|
|
72
|
+
discord does support basic markdown features like code blocks, code blocks languages, inline code, bold, italic, quotes, etc.
|
|
73
|
+
|
|
74
|
+
the max heading level is 3, so do not use ####
|
|
75
|
+
|
|
76
|
+
headings are discouraged anyway. instead try to use bold text for titles which renders more nicely in Discord
|
|
77
|
+
|
|
78
|
+
## tables
|
|
79
|
+
|
|
80
|
+
discord does NOT support markdown gfm tables.
|
|
81
|
+
|
|
82
|
+
so instead of using full markdown tables ALWAYS show code snippets with space aligned cells:
|
|
83
|
+
|
|
84
|
+
\`\`\`
|
|
85
|
+
Item Qty Price
|
|
86
|
+
---------- --- -----
|
|
87
|
+
Apples 10 $5
|
|
88
|
+
Oranges 3 $2
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
Using code blocks will make the content use monospaced font so that space will be aligned correctly
|
|
92
|
+
|
|
93
|
+
IMPORTANT: add enough space characters to align the table! otherwise the content will not look good and will be difficult to understand for the user
|
|
94
|
+
|
|
95
|
+
code blocks for tables and diagrams MUST have Max length of 85 characters. otherwise the content will wrap
|
|
96
|
+
|
|
97
|
+
## diagrams
|
|
98
|
+
|
|
99
|
+
you can create diagrams wrapping them in code blocks too.
|
|
100
|
+
`
|
|
101
|
+
|
|
57
102
|
const discordLogger = createLogger('DISCORD')
|
|
58
103
|
const voiceLogger = createLogger('VOICE')
|
|
59
104
|
const opencodeLogger = createLogger('OPENCODE')
|
|
@@ -168,11 +213,13 @@ async function setupVoiceHandling({
|
|
|
168
213
|
guildId,
|
|
169
214
|
channelId,
|
|
170
215
|
appId,
|
|
216
|
+
discordClient,
|
|
171
217
|
}: {
|
|
172
218
|
connection: VoiceConnection
|
|
173
219
|
guildId: string
|
|
174
220
|
channelId: string
|
|
175
221
|
appId: string
|
|
222
|
+
discordClient: Client
|
|
176
223
|
}) {
|
|
177
224
|
voiceLogger.log(
|
|
178
225
|
`Setting up voice handling for guild ${guildId}, channel ${channelId}`,
|
|
@@ -285,8 +332,28 @@ async function setupVoiceHandling({
|
|
|
285
332
|
|
|
286
333
|
genAiWorker.sendTextInput(text)
|
|
287
334
|
},
|
|
288
|
-
onError(error) {
|
|
335
|
+
async onError(error) {
|
|
289
336
|
voiceLogger.error('GenAI worker error:', error)
|
|
337
|
+
const textChannelRow = getDatabase()
|
|
338
|
+
.prepare(
|
|
339
|
+
`SELECT cd2.channel_id FROM channel_directories cd1
|
|
340
|
+
JOIN channel_directories cd2 ON cd1.directory = cd2.directory
|
|
341
|
+
WHERE cd1.channel_id = ? AND cd1.channel_type = 'voice' AND cd2.channel_type = 'text'`,
|
|
342
|
+
)
|
|
343
|
+
.get(channelId) as { channel_id: string } | undefined
|
|
344
|
+
|
|
345
|
+
if (textChannelRow) {
|
|
346
|
+
try {
|
|
347
|
+
const textChannel = await discordClient.channels.fetch(
|
|
348
|
+
textChannelRow.channel_id,
|
|
349
|
+
)
|
|
350
|
+
if (textChannel?.isTextBased() && 'send' in textChannel) {
|
|
351
|
+
await textChannel.send(`⚠️ Voice session error: ${error}`)
|
|
352
|
+
}
|
|
353
|
+
} catch (e) {
|
|
354
|
+
voiceLogger.error('Failed to send error to text channel:', e)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
290
357
|
},
|
|
291
358
|
})
|
|
292
359
|
|
|
@@ -629,69 +696,24 @@ async function sendThreadMessage(
|
|
|
629
696
|
|
|
630
697
|
content = escapeBackticksInCodeBlocks(content)
|
|
631
698
|
|
|
632
|
-
|
|
633
|
-
if (content.length <= MAX_LENGTH) {
|
|
634
|
-
return await thread.send(content)
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Use marked's lexer to tokenize markdown content
|
|
638
|
-
const lexer = new Lexer()
|
|
639
|
-
const tokens = lexer.lex(content)
|
|
640
|
-
|
|
641
|
-
const chunks: string[] = []
|
|
642
|
-
let currentChunk = ''
|
|
643
|
-
|
|
644
|
-
// Process each token and add to chunks
|
|
645
|
-
for (const token of tokens) {
|
|
646
|
-
const tokenText = token.raw || ''
|
|
699
|
+
const chunks = splitMarkdownForDiscord({ content, maxLength: MAX_LENGTH })
|
|
647
700
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
chunks.
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// If this single token is longer than MAX_LENGTH, split it
|
|
655
|
-
if (tokenText.length > MAX_LENGTH) {
|
|
656
|
-
if (currentChunk) {
|
|
657
|
-
chunks.push(currentChunk)
|
|
658
|
-
currentChunk = ''
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
let remainingText = tokenText
|
|
662
|
-
while (remainingText.length > MAX_LENGTH) {
|
|
663
|
-
// Try to split at a newline if possible
|
|
664
|
-
let splitIndex = MAX_LENGTH
|
|
665
|
-
const newlineIndex = remainingText.lastIndexOf('\n', MAX_LENGTH - 1)
|
|
666
|
-
if (newlineIndex > MAX_LENGTH * 0.7) {
|
|
667
|
-
splitIndex = newlineIndex + 1
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
chunks.push(remainingText.slice(0, splitIndex))
|
|
671
|
-
remainingText = remainingText.slice(splitIndex)
|
|
672
|
-
}
|
|
673
|
-
currentChunk = remainingText
|
|
674
|
-
} else {
|
|
675
|
-
currentChunk += tokenText
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Add any remaining content
|
|
680
|
-
if (currentChunk) {
|
|
681
|
-
chunks.push(currentChunk)
|
|
701
|
+
if (chunks.length > 1) {
|
|
702
|
+
discordLogger.log(
|
|
703
|
+
`MESSAGE: Splitting ${content.length} chars into ${chunks.length} messages`,
|
|
704
|
+
)
|
|
682
705
|
}
|
|
683
706
|
|
|
684
|
-
// Send all chunks
|
|
685
|
-
discordLogger.log(
|
|
686
|
-
`MESSAGE: Splitting ${content.length} chars into ${chunks.length} messages`,
|
|
687
|
-
)
|
|
688
|
-
|
|
689
707
|
let firstMessage: Message | undefined
|
|
690
708
|
for (let i = 0; i < chunks.length; i++) {
|
|
691
709
|
const chunk = chunks[i]
|
|
692
|
-
if (!chunk)
|
|
710
|
+
if (!chunk) {
|
|
711
|
+
continue
|
|
712
|
+
}
|
|
693
713
|
const message = await thread.send(chunk)
|
|
694
|
-
if (i === 0)
|
|
714
|
+
if (i === 0) {
|
|
715
|
+
firstMessage = message
|
|
716
|
+
}
|
|
695
717
|
}
|
|
696
718
|
|
|
697
719
|
return firstMessage!
|
|
@@ -852,6 +874,97 @@ export function escapeBackticksInCodeBlocks(markdown: string): string {
|
|
|
852
874
|
return result
|
|
853
875
|
}
|
|
854
876
|
|
|
877
|
+
type LineInfo = {
|
|
878
|
+
text: string
|
|
879
|
+
inCodeBlock: boolean
|
|
880
|
+
lang: string
|
|
881
|
+
isOpeningFence: boolean
|
|
882
|
+
isClosingFence: boolean
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export function splitMarkdownForDiscord({
|
|
886
|
+
content,
|
|
887
|
+
maxLength,
|
|
888
|
+
}: {
|
|
889
|
+
content: string
|
|
890
|
+
maxLength: number
|
|
891
|
+
}): string[] {
|
|
892
|
+
if (content.length <= maxLength) {
|
|
893
|
+
return [content]
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const lexer = new Lexer()
|
|
897
|
+
const tokens = lexer.lex(content)
|
|
898
|
+
|
|
899
|
+
const lines: LineInfo[] = []
|
|
900
|
+
for (const token of tokens) {
|
|
901
|
+
if (token.type === 'code') {
|
|
902
|
+
const lang = token.lang || ''
|
|
903
|
+
lines.push({ text: '```' + lang + '\n', inCodeBlock: false, lang, isOpeningFence: true, isClosingFence: false })
|
|
904
|
+
const codeLines = token.text.split('\n')
|
|
905
|
+
for (const codeLine of codeLines) {
|
|
906
|
+
lines.push({ text: codeLine + '\n', inCodeBlock: true, lang, isOpeningFence: false, isClosingFence: false })
|
|
907
|
+
}
|
|
908
|
+
lines.push({ text: '```\n', inCodeBlock: false, lang: '', isOpeningFence: false, isClosingFence: true })
|
|
909
|
+
} else {
|
|
910
|
+
const rawLines = token.raw.split('\n')
|
|
911
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
912
|
+
const isLast = i === rawLines.length - 1
|
|
913
|
+
const text = isLast ? rawLines[i]! : rawLines[i]! + '\n'
|
|
914
|
+
if (text) {
|
|
915
|
+
lines.push({ text, inCodeBlock: false, lang: '', isOpeningFence: false, isClosingFence: false })
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const chunks: string[] = []
|
|
922
|
+
let currentChunk = ''
|
|
923
|
+
let currentLang: string | null = null
|
|
924
|
+
|
|
925
|
+
for (const line of lines) {
|
|
926
|
+
const wouldExceed = currentChunk.length + line.text.length > maxLength
|
|
927
|
+
|
|
928
|
+
if (wouldExceed && currentChunk) {
|
|
929
|
+
if (currentLang !== null) {
|
|
930
|
+
currentChunk += '```\n'
|
|
931
|
+
}
|
|
932
|
+
chunks.push(currentChunk)
|
|
933
|
+
|
|
934
|
+
if (line.isClosingFence && currentLang !== null) {
|
|
935
|
+
currentChunk = ''
|
|
936
|
+
currentLang = null
|
|
937
|
+
continue
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (line.inCodeBlock || line.isOpeningFence) {
|
|
941
|
+
const lang = line.lang
|
|
942
|
+
currentChunk = '```' + lang + '\n'
|
|
943
|
+
if (!line.isOpeningFence) {
|
|
944
|
+
currentChunk += line.text
|
|
945
|
+
}
|
|
946
|
+
currentLang = lang
|
|
947
|
+
} else {
|
|
948
|
+
currentChunk = line.text
|
|
949
|
+
currentLang = null
|
|
950
|
+
}
|
|
951
|
+
} else {
|
|
952
|
+
currentChunk += line.text
|
|
953
|
+
if (line.inCodeBlock || line.isOpeningFence) {
|
|
954
|
+
currentLang = line.lang
|
|
955
|
+
} else if (line.isClosingFence) {
|
|
956
|
+
currentLang = null
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (currentChunk) {
|
|
962
|
+
chunks.push(currentChunk)
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
return chunks
|
|
966
|
+
}
|
|
967
|
+
|
|
855
968
|
/**
|
|
856
969
|
* Escape Discord formatting characters to prevent breaking code blocks and inline code
|
|
857
970
|
*/
|
|
@@ -1121,30 +1234,31 @@ function getToolOutputToDisplay(part: Part): string {
|
|
|
1121
1234
|
return part.state.error || 'Unknown error'
|
|
1122
1235
|
}
|
|
1123
1236
|
|
|
1124
|
-
if (part.tool === 'todowrite') {
|
|
1125
|
-
const todos =
|
|
1126
|
-
(part.state.input?.todos as {
|
|
1127
|
-
content: string
|
|
1128
|
-
status: 'pending' | 'in_progress' | 'completed' | 'cancelled'
|
|
1129
|
-
}[]) || []
|
|
1130
|
-
return todos
|
|
1131
|
-
.map((todo) => {
|
|
1132
|
-
let statusIcon = '▢'
|
|
1133
|
-
if (todo.status === 'in_progress') {
|
|
1134
|
-
statusIcon = '●'
|
|
1135
|
-
}
|
|
1136
|
-
if (todo.status === 'completed' || todo.status === 'cancelled') {
|
|
1137
|
-
statusIcon = '■'
|
|
1138
|
-
}
|
|
1139
|
-
return `\`${statusIcon}\` ${todo.content}`
|
|
1140
|
-
})
|
|
1141
|
-
.filter(Boolean)
|
|
1142
|
-
.join('\n')
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
1237
|
return ''
|
|
1146
1238
|
}
|
|
1147
1239
|
|
|
1240
|
+
function formatTodoList(part: Part): string {
|
|
1241
|
+
if (part.type !== 'tool' || part.tool !== 'todowrite') return ''
|
|
1242
|
+
const todos =
|
|
1243
|
+
(part.state.input?.todos as {
|
|
1244
|
+
content: string
|
|
1245
|
+
status: 'pending' | 'in_progress' | 'completed' | 'cancelled'
|
|
1246
|
+
}[]) || []
|
|
1247
|
+
if (todos.length === 0) return ''
|
|
1248
|
+
return todos
|
|
1249
|
+
.map((todo, i) => {
|
|
1250
|
+
const num = `${i + 1}.`
|
|
1251
|
+
if (todo.status === 'in_progress') {
|
|
1252
|
+
return `${num} **${todo.content}**`
|
|
1253
|
+
}
|
|
1254
|
+
if (todo.status === 'completed' || todo.status === 'cancelled') {
|
|
1255
|
+
return `${num} ~~${todo.content}~~`
|
|
1256
|
+
}
|
|
1257
|
+
return `${num} ${todo.content}`
|
|
1258
|
+
})
|
|
1259
|
+
.join('\n')
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1148
1262
|
function formatPart(part: Part): string {
|
|
1149
1263
|
if (part.type === 'text') {
|
|
1150
1264
|
return part.text || ''
|
|
@@ -1172,6 +1286,10 @@ function formatPart(part: Part): string {
|
|
|
1172
1286
|
}
|
|
1173
1287
|
|
|
1174
1288
|
if (part.type === 'tool') {
|
|
1289
|
+
if (part.tool === 'todowrite') {
|
|
1290
|
+
return formatTodoList(part)
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1175
1293
|
if (part.state.status !== 'completed' && part.state.status !== 'error') {
|
|
1176
1294
|
return ''
|
|
1177
1295
|
}
|
|
@@ -1179,9 +1297,20 @@ function formatPart(part: Part): string {
|
|
|
1179
1297
|
const summaryText = getToolSummaryText(part)
|
|
1180
1298
|
const outputToDisplay = getToolOutputToDisplay(part)
|
|
1181
1299
|
|
|
1182
|
-
let toolTitle =
|
|
1183
|
-
if (
|
|
1184
|
-
toolTitle =
|
|
1300
|
+
let toolTitle = ''
|
|
1301
|
+
if (part.state.status === 'error') {
|
|
1302
|
+
toolTitle = 'error'
|
|
1303
|
+
} else if (part.tool === 'bash') {
|
|
1304
|
+
const command = (part.state.input?.command as string) || ''
|
|
1305
|
+
const isSingleLine = !command.includes('\n')
|
|
1306
|
+
const hasBackticks = command.includes('`')
|
|
1307
|
+
if (isSingleLine && command.length <= 120 && !hasBackticks) {
|
|
1308
|
+
toolTitle = `\`${command}\``
|
|
1309
|
+
} else {
|
|
1310
|
+
toolTitle = part.state.title ? `*${part.state.title}*` : ''
|
|
1311
|
+
}
|
|
1312
|
+
} else if (part.state.title) {
|
|
1313
|
+
toolTitle = `*${part.state.title}*`
|
|
1185
1314
|
}
|
|
1186
1315
|
|
|
1187
1316
|
const icon = part.state.status === 'completed' ? '◼︎' : part.state.status === 'error' ? '⨯' : ''
|
|
@@ -1687,6 +1816,7 @@ async function handleOpencodeSession({
|
|
|
1687
1816
|
path: { id: session.id },
|
|
1688
1817
|
body: {
|
|
1689
1818
|
parts,
|
|
1819
|
+
system: OPENCODE_SYSTEM_MESSAGE,
|
|
1690
1820
|
},
|
|
1691
1821
|
signal: abortController.signal,
|
|
1692
1822
|
})
|
|
@@ -2508,68 +2638,56 @@ export async function startDiscordBot({
|
|
|
2508
2638
|
`📂 **Resumed session:** ${sessionTitle}\n📅 **Created:** ${new Date(sessionResponse.data.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`,
|
|
2509
2639
|
)
|
|
2510
2640
|
|
|
2511
|
-
//
|
|
2512
|
-
|
|
2641
|
+
// Collect all assistant parts first, then only render the last 30
|
|
2642
|
+
const allAssistantParts: { id: string; content: string }[] = []
|
|
2513
2643
|
for (const message of messages) {
|
|
2514
|
-
if (message.info.role === '
|
|
2515
|
-
// Render user messages
|
|
2516
|
-
const userParts = message.parts.filter(
|
|
2517
|
-
(p) => p.type === 'text' && !p.synthetic,
|
|
2518
|
-
)
|
|
2519
|
-
const userTexts = userParts
|
|
2520
|
-
.map((p) => {
|
|
2521
|
-
if (p.type === 'text') {
|
|
2522
|
-
return p.text
|
|
2523
|
-
}
|
|
2524
|
-
return ''
|
|
2525
|
-
})
|
|
2526
|
-
.filter((t) => t.trim())
|
|
2527
|
-
|
|
2528
|
-
const userText = userTexts.join('\n\n')
|
|
2529
|
-
if (userText) {
|
|
2530
|
-
// Escape backticks in user messages to prevent formatting issues
|
|
2531
|
-
const escapedText = escapeDiscordFormatting(userText)
|
|
2532
|
-
await sendThreadMessage(thread, `**User:**\n${escapedText}`)
|
|
2533
|
-
}
|
|
2534
|
-
} else if (message.info.role === 'assistant') {
|
|
2535
|
-
// Render assistant parts
|
|
2536
|
-
const partsToRender: { id: string; content: string }[] = []
|
|
2537
|
-
|
|
2644
|
+
if (message.info.role === 'assistant') {
|
|
2538
2645
|
for (const part of message.parts) {
|
|
2539
2646
|
const content = formatPart(part)
|
|
2540
2647
|
if (content.trim()) {
|
|
2541
|
-
|
|
2648
|
+
allAssistantParts.push({ id: part.id, content })
|
|
2542
2649
|
}
|
|
2543
2650
|
}
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2544
2653
|
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
.map((p) => p.content)
|
|
2548
|
-
.join('\n\n')
|
|
2654
|
+
const partsToRender = allAssistantParts.slice(-30)
|
|
2655
|
+
const skippedCount = allAssistantParts.length - partsToRender.length
|
|
2549
2656
|
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2657
|
+
if (skippedCount > 0) {
|
|
2658
|
+
await sendThreadMessage(
|
|
2659
|
+
thread,
|
|
2660
|
+
`*Skipped ${skippedCount} older assistant parts...*`,
|
|
2661
|
+
)
|
|
2662
|
+
}
|
|
2554
2663
|
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2664
|
+
if (partsToRender.length > 0) {
|
|
2665
|
+
const combinedContent = partsToRender
|
|
2666
|
+
.map((p) => p.content)
|
|
2667
|
+
.join('\n\n')
|
|
2558
2668
|
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
}
|
|
2564
|
-
},
|
|
2565
|
-
)
|
|
2669
|
+
const discordMessage = await sendThreadMessage(
|
|
2670
|
+
thread,
|
|
2671
|
+
combinedContent,
|
|
2672
|
+
)
|
|
2566
2673
|
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2674
|
+
const stmt = getDatabase().prepare(
|
|
2675
|
+
'INSERT OR REPLACE INTO part_messages (part_id, message_id, thread_id) VALUES (?, ?, ?)',
|
|
2676
|
+
)
|
|
2677
|
+
|
|
2678
|
+
const transaction = getDatabase().transaction(
|
|
2679
|
+
(parts: { id: string }[]) => {
|
|
2680
|
+
for (const part of parts) {
|
|
2681
|
+
stmt.run(part.id, discordMessage.id, thread.id)
|
|
2682
|
+
}
|
|
2683
|
+
},
|
|
2684
|
+
)
|
|
2685
|
+
|
|
2686
|
+
transaction(partsToRender)
|
|
2571
2687
|
}
|
|
2572
2688
|
|
|
2689
|
+
const messageCount = messages.length
|
|
2690
|
+
|
|
2573
2691
|
await sendThreadMessage(
|
|
2574
2692
|
thread,
|
|
2575
2693
|
`✅ **Session resumed!** Loaded ${messageCount} messages.\n\nYou can now continue the conversation by sending messages in this thread.`,
|
|
@@ -3009,6 +3127,7 @@ export async function startDiscordBot({
|
|
|
3009
3127
|
guildId: newState.guild.id,
|
|
3010
3128
|
channelId: voiceChannel.id,
|
|
3011
3129
|
appId: currentAppId!,
|
|
3130
|
+
discordClient,
|
|
3012
3131
|
})
|
|
3013
3132
|
|
|
3014
3133
|
// Handle connection state changes
|