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/session-handler.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
pendingQuestionContexts,
|
|
31
31
|
} from './commands/ask-question.js'
|
|
32
32
|
import { showPermissionDropdown, cleanupPermissionContext } from './commands/permissions.js'
|
|
33
|
+
import * as errore from 'errore'
|
|
33
34
|
|
|
34
35
|
const sessionLogger = createLogger('SESSION')
|
|
35
36
|
const voiceLogger = createLogger('VOICE')
|
|
@@ -104,6 +105,10 @@ export async function abortAndRetrySession({
|
|
|
104
105
|
|
|
105
106
|
// Also call the API abort endpoint
|
|
106
107
|
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
108
|
+
if (errore.isError(getClient)) {
|
|
109
|
+
sessionLogger.error(`[ABORT+RETRY] Failed to initialize OpenCode client:`, getClient.message)
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
107
112
|
try {
|
|
108
113
|
await getClient().session.abort({ path: { id: sessionId } })
|
|
109
114
|
} catch (e) {
|
|
@@ -183,6 +188,10 @@ export async function handleOpencodeSession({
|
|
|
183
188
|
sessionLogger.log(`Using directory: ${directory}`)
|
|
184
189
|
|
|
185
190
|
const getClient = await initializeOpencodeForDirectory(directory)
|
|
191
|
+
if (errore.isError(getClient)) {
|
|
192
|
+
await sendThreadMessage(thread, `✗ ${getClient.message}`)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
186
195
|
|
|
187
196
|
const serverEntry = getOpencodeServers().get(directory)
|
|
188
197
|
const port = serverEntry?.port
|
|
@@ -392,6 +401,11 @@ export async function handleOpencodeSession({
|
|
|
392
401
|
}
|
|
393
402
|
|
|
394
403
|
const eventHandler = async () => {
|
|
404
|
+
// Subtask tracking: child sessionId → { label, assistantMessageId }
|
|
405
|
+
const subtaskSessions = new Map<string, { label: string; assistantMessageId?: string }>()
|
|
406
|
+
// Counts spawned tasks per agent type: "explore" → 2
|
|
407
|
+
const agentSpawnCounts: Record<string, number> = {}
|
|
408
|
+
|
|
395
409
|
try {
|
|
396
410
|
let assistantMessageId: string | undefined
|
|
397
411
|
|
|
@@ -399,6 +413,12 @@ export async function handleOpencodeSession({
|
|
|
399
413
|
if (event.type === 'message.updated') {
|
|
400
414
|
const msg = event.properties.info
|
|
401
415
|
|
|
416
|
+
// Track assistant message IDs for subtask sessions
|
|
417
|
+
const subtaskInfo = subtaskSessions.get(msg.sessionID)
|
|
418
|
+
if (subtaskInfo && msg.role === 'assistant') {
|
|
419
|
+
subtaskInfo.assistantMessageId = msg.id
|
|
420
|
+
}
|
|
421
|
+
|
|
402
422
|
if (msg.sessionID !== session.id) {
|
|
403
423
|
continue
|
|
404
424
|
}
|
|
@@ -451,10 +471,55 @@ export async function handleOpencodeSession({
|
|
|
451
471
|
} else if (event.type === 'message.part.updated') {
|
|
452
472
|
const part = event.properties.part
|
|
453
473
|
|
|
454
|
-
if (
|
|
474
|
+
// Check if this is a subtask event (child session we're tracking)
|
|
475
|
+
const subtaskInfo = subtaskSessions.get(part.sessionID)
|
|
476
|
+
const isSubtaskEvent = Boolean(subtaskInfo)
|
|
477
|
+
|
|
478
|
+
// Accept events from main session OR tracked subtask sessions
|
|
479
|
+
if (part.sessionID !== session.id && !isSubtaskEvent) {
|
|
455
480
|
continue
|
|
456
481
|
}
|
|
457
482
|
|
|
483
|
+
// For subtask events, send them immediately with prefix (don't buffer in currentParts)
|
|
484
|
+
if (isSubtaskEvent && subtaskInfo) {
|
|
485
|
+
// Skip parts that aren't useful to show (step-start, step-finish, pending tools)
|
|
486
|
+
if (part.type === 'step-start' || part.type === 'step-finish') {
|
|
487
|
+
continue
|
|
488
|
+
}
|
|
489
|
+
if (part.type === 'tool' && part.state.status === 'pending') {
|
|
490
|
+
continue
|
|
491
|
+
}
|
|
492
|
+
// Skip text parts - the outer agent will report the task result anyway
|
|
493
|
+
if (part.type === 'text') {
|
|
494
|
+
continue
|
|
495
|
+
}
|
|
496
|
+
// Only show parts from assistant messages (not user prompts sent to subtask)
|
|
497
|
+
// Skip if we haven't seen an assistant message yet, or if this part is from a different message
|
|
498
|
+
if (
|
|
499
|
+
!subtaskInfo.assistantMessageId ||
|
|
500
|
+
part.messageID !== subtaskInfo.assistantMessageId
|
|
501
|
+
) {
|
|
502
|
+
continue
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const content = formatPart(part, subtaskInfo.label)
|
|
506
|
+
if (content.trim() && !sentPartIds.has(part.id)) {
|
|
507
|
+
try {
|
|
508
|
+
const msg = await sendThreadMessage(thread, content + '\n\n')
|
|
509
|
+
sentPartIds.add(part.id)
|
|
510
|
+
getDatabase()
|
|
511
|
+
.prepare(
|
|
512
|
+
'INSERT OR REPLACE INTO part_messages (part_id, message_id, thread_id) VALUES (?, ?, ?)',
|
|
513
|
+
)
|
|
514
|
+
.run(part.id, msg.id, thread.id)
|
|
515
|
+
} catch (error) {
|
|
516
|
+
discordLogger.error(`ERROR: Failed to send subtask part ${part.id}:`, error)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
continue
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Main session events: require matching assistantMessageId
|
|
458
523
|
if (part.messageID !== assistantMessageId) {
|
|
459
524
|
continue
|
|
460
525
|
}
|
|
@@ -486,6 +551,20 @@ export async function handleOpencodeSession({
|
|
|
486
551
|
}
|
|
487
552
|
}
|
|
488
553
|
await sendPartMessage(part)
|
|
554
|
+
// Track task tool and register child session when sessionId is available
|
|
555
|
+
if (part.tool === 'task' && !sentPartIds.has(part.id)) {
|
|
556
|
+
const description = (part.state.input?.description as string) || ''
|
|
557
|
+
const agent = (part.state.input?.subagent_type as string) || 'task'
|
|
558
|
+
const childSessionId = (part.state.metadata?.sessionId as string) || ''
|
|
559
|
+
if (description && childSessionId) {
|
|
560
|
+
agentSpawnCounts[agent] = (agentSpawnCounts[agent] || 0) + 1
|
|
561
|
+
const label = `${agent}-${agentSpawnCounts[agent]}`
|
|
562
|
+
subtaskSessions.set(childSessionId, { label, assistantMessageId: undefined })
|
|
563
|
+
const taskDisplay = `┣ task **${label}** _${description}_`
|
|
564
|
+
await sendThreadMessage(thread, taskDisplay + '\n\n')
|
|
565
|
+
sentPartIds.add(part.id)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
489
568
|
}
|
|
490
569
|
|
|
491
570
|
// Show token usage for completed tools with large output (>5k tokens)
|
|
@@ -538,6 +617,7 @@ export async function handleOpencodeSession({
|
|
|
538
617
|
stopTyping = startTyping()
|
|
539
618
|
}, 300)
|
|
540
619
|
}
|
|
620
|
+
|
|
541
621
|
} else if (event.type === 'session.error') {
|
|
542
622
|
sessionLogger.error(`ERROR:`, event.properties)
|
|
543
623
|
if (event.properties.sessionID === session.id) {
|
|
@@ -641,11 +721,53 @@ export async function handleOpencodeSession({
|
|
|
641
721
|
requestId: questionRequest.id,
|
|
642
722
|
input: { questions: questionRequest.questions },
|
|
643
723
|
})
|
|
724
|
+
|
|
725
|
+
// Process queued messages if any - queued message will cancel the pending question
|
|
726
|
+
const queue = messageQueue.get(thread.id)
|
|
727
|
+
if (queue && queue.length > 0) {
|
|
728
|
+
const nextMessage = queue.shift()!
|
|
729
|
+
if (queue.length === 0) {
|
|
730
|
+
messageQueue.delete(thread.id)
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
sessionLogger.log(
|
|
734
|
+
`[QUEUE] Question shown but queue has messages, processing from ${nextMessage.username}`,
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
await sendThreadMessage(
|
|
738
|
+
thread,
|
|
739
|
+
`» **${nextMessage.username}:** ${nextMessage.prompt.slice(0, 150)}${nextMessage.prompt.length > 150 ? '...' : ''}`,
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
// handleOpencodeSession will call cancelPendingQuestion, which cancels the dropdown
|
|
743
|
+
setImmediate(() => {
|
|
744
|
+
handleOpencodeSession({
|
|
745
|
+
prompt: nextMessage.prompt,
|
|
746
|
+
thread,
|
|
747
|
+
projectDirectory: directory,
|
|
748
|
+
images: nextMessage.images,
|
|
749
|
+
channelId,
|
|
750
|
+
}).catch(async (e) => {
|
|
751
|
+
sessionLogger.error(`[QUEUE] Failed to process queued message:`, e)
|
|
752
|
+
const errorMsg = e instanceof Error ? e.message : String(e)
|
|
753
|
+
await sendThreadMessage(
|
|
754
|
+
thread,
|
|
755
|
+
`✗ Queued message failed: ${errorMsg.slice(0, 200)}`,
|
|
756
|
+
)
|
|
757
|
+
})
|
|
758
|
+
})
|
|
759
|
+
}
|
|
644
760
|
} else if (event.type === 'session.idle') {
|
|
761
|
+
const idleSessionId = event.properties.sessionID
|
|
645
762
|
// Session is done processing - abort to signal completion
|
|
646
|
-
if (
|
|
763
|
+
if (idleSessionId === session.id) {
|
|
647
764
|
sessionLogger.log(`[SESSION IDLE] Session ${session.id} is idle, aborting`)
|
|
648
765
|
abortController.abort('finished')
|
|
766
|
+
} else if (subtaskSessions.has(idleSessionId)) {
|
|
767
|
+
// Child session completed - clean up tracking
|
|
768
|
+
const subtask = subtaskSessions.get(idleSessionId)
|
|
769
|
+
sessionLogger.log(`[SUBTASK IDLE] Subtask "${subtask?.label}" completed`)
|
|
770
|
+
subtaskSessions.delete(idleSessionId)
|
|
649
771
|
}
|
|
650
772
|
}
|
|
651
773
|
}
|
|
@@ -681,6 +803,32 @@ export async function handleOpencodeSession({
|
|
|
681
803
|
let contextInfo = ''
|
|
682
804
|
|
|
683
805
|
try {
|
|
806
|
+
// Fetch final token count from API since message.updated events can arrive
|
|
807
|
+
// after session.idle due to race conditions in event ordering
|
|
808
|
+
if (tokensUsedInSession === 0) {
|
|
809
|
+
const messagesResponse = await getClient().session.messages({
|
|
810
|
+
path: { id: session.id },
|
|
811
|
+
})
|
|
812
|
+
const messages = messagesResponse.data || []
|
|
813
|
+
const lastAssistant = [...messages]
|
|
814
|
+
.reverse()
|
|
815
|
+
.find((m) => m.info.role === 'assistant')
|
|
816
|
+
if (lastAssistant && 'tokens' in lastAssistant.info) {
|
|
817
|
+
const tokens = lastAssistant.info.tokens as {
|
|
818
|
+
input: number
|
|
819
|
+
output: number
|
|
820
|
+
reasoning: number
|
|
821
|
+
cache: { read: number; write: number }
|
|
822
|
+
}
|
|
823
|
+
tokensUsedInSession =
|
|
824
|
+
tokens.input +
|
|
825
|
+
tokens.output +
|
|
826
|
+
tokens.reasoning +
|
|
827
|
+
tokens.cache.read +
|
|
828
|
+
tokens.cache.write
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
684
832
|
const providersResponse = await getClient().provider.list({ query: { directory } })
|
|
685
833
|
const provider = providersResponse.data?.all?.find((p) => p.id === usedProviderID)
|
|
686
834
|
const model = provider?.models?.[usedModel || '']
|
|
@@ -774,10 +922,23 @@ export async function handleOpencodeSession({
|
|
|
774
922
|
const parts = [{ type: 'text' as const, text: promptWithImagePaths }, ...images]
|
|
775
923
|
sessionLogger.log(`[PROMPT] Parts to send:`, parts.length)
|
|
776
924
|
|
|
925
|
+
// Get agent preference: session-level overrides channel-level
|
|
926
|
+
const agentPreference =
|
|
927
|
+
getSessionAgent(session.id) || (channelId ? getChannelAgent(channelId) : undefined)
|
|
928
|
+
if (agentPreference) {
|
|
929
|
+
sessionLogger.log(`[AGENT] Using agent preference: ${agentPreference}`)
|
|
930
|
+
}
|
|
931
|
+
|
|
777
932
|
// Get model preference: session-level overrides channel-level
|
|
933
|
+
// BUT: if an agent is set, don't pass model param so the agent's model takes effect
|
|
778
934
|
const modelPreference =
|
|
779
935
|
getSessionModel(session.id) || (channelId ? getChannelModel(channelId) : undefined)
|
|
780
936
|
const modelParam = (() => {
|
|
937
|
+
// When an agent is set, let the agent's model config take effect
|
|
938
|
+
if (agentPreference) {
|
|
939
|
+
sessionLogger.log(`[MODEL] Skipping model param, agent "${agentPreference}" controls model`)
|
|
940
|
+
return undefined
|
|
941
|
+
}
|
|
781
942
|
if (!modelPreference) {
|
|
782
943
|
return undefined
|
|
783
944
|
}
|
|
@@ -790,13 +951,6 @@ export async function handleOpencodeSession({
|
|
|
790
951
|
return { providerID, modelID }
|
|
791
952
|
})()
|
|
792
953
|
|
|
793
|
-
// Get agent preference: session-level overrides channel-level
|
|
794
|
-
const agentPreference =
|
|
795
|
-
getSessionAgent(session.id) || (channelId ? getChannelAgent(channelId) : undefined)
|
|
796
|
-
if (agentPreference) {
|
|
797
|
-
sessionLogger.log(`[AGENT] Using agent preference: ${agentPreference}`)
|
|
798
|
-
}
|
|
799
|
-
|
|
800
954
|
// Use session.command API for slash commands, session.prompt for regular messages
|
|
801
955
|
const response = command
|
|
802
956
|
? await getClient().session.command({
|
|
@@ -850,9 +1004,8 @@ export async function handleOpencodeSession({
|
|
|
850
1004
|
|
|
851
1005
|
return { sessionID: session.id, result: response.data, port }
|
|
852
1006
|
} catch (error) {
|
|
853
|
-
sessionLogger.error(`ERROR: Failed to send prompt:`, error)
|
|
854
|
-
|
|
855
1007
|
if (!isAbortError(error, abortController.signal)) {
|
|
1008
|
+
sessionLogger.error(`ERROR: Failed to send prompt:`, error)
|
|
856
1009
|
abortController.abort('error')
|
|
857
1010
|
|
|
858
1011
|
if (originalMessage) {
|
package/src/system-message.ts
CHANGED
|
@@ -85,6 +85,8 @@ you can create diagrams wrapping them in code blocks.
|
|
|
85
85
|
|
|
86
86
|
IMPORTANT: At the end of each response, especially after completing a task or presenting a plan, use the question tool to offer the user clear options for what to do next.
|
|
87
87
|
|
|
88
|
+
IMPORTANT: The question tool must be called last, after all text parts. If it is called before your final text response, the user will not see the text.
|
|
89
|
+
|
|
88
90
|
Examples:
|
|
89
91
|
- After showing a plan: offer "Start implementing?" with Yes/No options
|
|
90
92
|
- After completing edits: offer "Commit changes?" with Yes/No options
|
package/src/tools.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type Provider,
|
|
14
14
|
} from '@opencode-ai/sdk'
|
|
15
15
|
import { createLogger } from './logger.js'
|
|
16
|
+
import * as errore from 'errore'
|
|
16
17
|
|
|
17
18
|
const toolsLogger = createLogger('TOOLS')
|
|
18
19
|
|
|
@@ -35,6 +36,9 @@ export async function getTools({
|
|
|
35
36
|
}) => void
|
|
36
37
|
}) {
|
|
37
38
|
const getClient = await initializeOpencodeForDirectory(directory)
|
|
39
|
+
if (errore.isError(getClient)) {
|
|
40
|
+
throw new Error(getClient.message)
|
|
41
|
+
}
|
|
38
42
|
const client = getClient()
|
|
39
43
|
|
|
40
44
|
const markdownRenderer = new ShareMarkdown(client)
|
|
@@ -83,7 +87,7 @@ export async function getTools({
|
|
|
83
87
|
},
|
|
84
88
|
})
|
|
85
89
|
.then(async (response) => {
|
|
86
|
-
const
|
|
90
|
+
const markdownResult = await markdownRenderer.generate({
|
|
87
91
|
sessionID: sessionId,
|
|
88
92
|
lastAssistantOnly: true,
|
|
89
93
|
})
|
|
@@ -91,7 +95,7 @@ export async function getTools({
|
|
|
91
95
|
sessionId,
|
|
92
96
|
messageId: '',
|
|
93
97
|
data: response.data,
|
|
94
|
-
markdown,
|
|
98
|
+
markdown: errore.unwrapOr(markdownResult, ''),
|
|
95
99
|
})
|
|
96
100
|
})
|
|
97
101
|
.catch((error) => {
|
|
@@ -149,7 +153,7 @@ export async function getTools({
|
|
|
149
153
|
},
|
|
150
154
|
})
|
|
151
155
|
.then(async (response) => {
|
|
152
|
-
const
|
|
156
|
+
const markdownResult = await markdownRenderer.generate({
|
|
153
157
|
sessionID: session.data.id,
|
|
154
158
|
lastAssistantOnly: true,
|
|
155
159
|
})
|
|
@@ -157,7 +161,7 @@ export async function getTools({
|
|
|
157
161
|
sessionId: session.data.id,
|
|
158
162
|
messageId: '',
|
|
159
163
|
data: response.data,
|
|
160
|
-
markdown,
|
|
164
|
+
markdown: errore.unwrapOr(markdownResult, ''),
|
|
161
165
|
})
|
|
162
166
|
})
|
|
163
167
|
.catch((error) => {
|
|
@@ -290,20 +294,26 @@ export async function getTools({
|
|
|
290
294
|
? 'completed'
|
|
291
295
|
: 'in_progress'
|
|
292
296
|
|
|
293
|
-
const
|
|
297
|
+
const markdownResult = await markdownRenderer.generate({
|
|
294
298
|
sessionID: sessionId,
|
|
295
299
|
lastAssistantOnly: true,
|
|
296
300
|
})
|
|
301
|
+
if (errore.isError(markdownResult)) {
|
|
302
|
+
throw new Error(markdownResult.message)
|
|
303
|
+
}
|
|
297
304
|
|
|
298
305
|
return {
|
|
299
306
|
success: true,
|
|
300
|
-
markdown,
|
|
307
|
+
markdown: markdownResult,
|
|
301
308
|
status,
|
|
302
309
|
}
|
|
303
310
|
} else {
|
|
304
|
-
const
|
|
311
|
+
const markdownResult = await markdownRenderer.generate({
|
|
305
312
|
sessionID: sessionId,
|
|
306
313
|
})
|
|
314
|
+
if (errore.isError(markdownResult)) {
|
|
315
|
+
throw new Error(markdownResult.message)
|
|
316
|
+
}
|
|
307
317
|
|
|
308
318
|
const messages = await getClient().session.messages({
|
|
309
319
|
path: { id: sessionId },
|
|
@@ -319,7 +329,7 @@ export async function getTools({
|
|
|
319
329
|
|
|
320
330
|
return {
|
|
321
331
|
success: true,
|
|
322
|
-
markdown,
|
|
332
|
+
markdown: markdownResult,
|
|
323
333
|
status,
|
|
324
334
|
}
|
|
325
335
|
}
|
package/src/voice-handler.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Discord voice channel connection and audio stream handler.
|
|
2
2
|
// Manages joining/leaving voice channels, captures user audio, resamples to 16kHz,
|
|
3
3
|
// and routes audio to the GenAI worker for real-time voice assistant interactions.
|
|
4
|
+
import * as errore from 'errore'
|
|
4
5
|
|
|
5
6
|
import {
|
|
6
7
|
VoiceConnectionStatus,
|
|
@@ -34,6 +35,8 @@ import {
|
|
|
34
35
|
SILENT_MESSAGE_FLAGS,
|
|
35
36
|
} from './discord-utils.js'
|
|
36
37
|
import { transcribeAudio } from './voice.js'
|
|
38
|
+
import { FetchError } from './errors.js'
|
|
39
|
+
|
|
37
40
|
import { createLogger } from './logger.js'
|
|
38
41
|
|
|
39
42
|
const voiceLogger = createLogger('VOICE')
|
|
@@ -443,7 +446,15 @@ export async function processVoiceAttachment({
|
|
|
443
446
|
|
|
444
447
|
await sendThreadMessage(thread, '🎤 Transcribing voice message...')
|
|
445
448
|
|
|
446
|
-
const audioResponse = await
|
|
449
|
+
const audioResponse = await errore.tryAsync({
|
|
450
|
+
try: () => fetch(audioAttachment.url),
|
|
451
|
+
catch: (e) => new FetchError({ url: audioAttachment.url, cause: e }),
|
|
452
|
+
})
|
|
453
|
+
if (errore.isError(audioResponse)) {
|
|
454
|
+
voiceLogger.error(`Failed to download audio attachment:`, audioResponse.message)
|
|
455
|
+
await sendThreadMessage(thread, `⚠️ Failed to download audio: ${audioResponse.message}`)
|
|
456
|
+
return null
|
|
457
|
+
}
|
|
447
458
|
const audioBuffer = Buffer.from(await audioResponse.arrayBuffer())
|
|
448
459
|
|
|
449
460
|
voiceLogger.log(`Downloaded ${audioBuffer.length} bytes, transcribing...`)
|
|
@@ -457,10 +468,9 @@ export async function processVoiceAttachment({
|
|
|
457
468
|
const { stdout } = await execAsync('git ls-files | tree --fromfile -a', {
|
|
458
469
|
cwd: projectDirectory,
|
|
459
470
|
})
|
|
460
|
-
const result = stdout
|
|
461
471
|
|
|
462
|
-
if (
|
|
463
|
-
transcriptionPrompt = `Discord voice message transcription. Project file structure:\n${
|
|
472
|
+
if (stdout) {
|
|
473
|
+
transcriptionPrompt = `Discord voice message transcription. Project file structure:\n${stdout}\n\nPlease transcribe file names and paths accurately based on this context.`
|
|
464
474
|
voiceLogger.log(`Added project context to transcription prompt`)
|
|
465
475
|
}
|
|
466
476
|
} catch (e) {
|
|
@@ -479,19 +489,25 @@ export async function processVoiceAttachment({
|
|
|
479
489
|
}
|
|
480
490
|
}
|
|
481
491
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
492
|
+
const transcription = await transcribeAudio({
|
|
493
|
+
audio: audioBuffer,
|
|
494
|
+
prompt: transcriptionPrompt,
|
|
495
|
+
geminiApiKey,
|
|
496
|
+
directory: projectDirectory,
|
|
497
|
+
currentSessionContext,
|
|
498
|
+
lastSessionContext,
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
if (errore.isError(transcription)) {
|
|
502
|
+
const errMsg = errore.matchError(transcription, {
|
|
503
|
+
ApiKeyMissingError: (e) => e.message,
|
|
504
|
+
InvalidAudioFormatError: (e) => e.message,
|
|
505
|
+
TranscriptionError: (e) => e.message,
|
|
506
|
+
EmptyTranscriptionError: (e) => e.message,
|
|
507
|
+
NoResponseContentError: (e) => e.message,
|
|
508
|
+
NoToolResponseError: (e) => e.message,
|
|
491
509
|
})
|
|
492
|
-
|
|
493
|
-
const errMsg = error instanceof Error ? error.message : String(error)
|
|
494
|
-
voiceLogger.error(`Transcription failed:`, error)
|
|
510
|
+
voiceLogger.error(`Transcription failed:`, transcription)
|
|
495
511
|
await sendThreadMessage(thread, `⚠️ Transcription failed: ${errMsg}`)
|
|
496
512
|
return null
|
|
497
513
|
}
|
|
@@ -503,14 +519,23 @@ export async function processVoiceAttachment({
|
|
|
503
519
|
if (isNewThread) {
|
|
504
520
|
const threadName = transcription.replace(/\s+/g, ' ').trim().slice(0, 80)
|
|
505
521
|
if (threadName) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
thread.setName(threadName),
|
|
509
|
-
|
|
510
|
-
|
|
522
|
+
const renamed = await Promise.race([
|
|
523
|
+
errore.tryAsync({
|
|
524
|
+
try: () => thread.setName(threadName),
|
|
525
|
+
catch: (e) => e as Error,
|
|
526
|
+
}),
|
|
527
|
+
new Promise<null>((resolve) => {
|
|
528
|
+
setTimeout(() => {
|
|
529
|
+
resolve(null)
|
|
530
|
+
}, 2000)
|
|
531
|
+
}),
|
|
532
|
+
])
|
|
533
|
+
if (renamed === null) {
|
|
534
|
+
voiceLogger.log(`Thread name update timed out`)
|
|
535
|
+
} else if (errore.isError(renamed)) {
|
|
536
|
+
voiceLogger.log(`Could not update thread name:`, renamed.message)
|
|
537
|
+
} else {
|
|
511
538
|
voiceLogger.log(`Updated thread name to: "${threadName}"`)
|
|
512
|
-
} catch (e) {
|
|
513
|
-
voiceLogger.log(`Could not update thread name:`, e)
|
|
514
539
|
}
|
|
515
540
|
}
|
|
516
541
|
}
|