kimaki 0.4.34 → 0.4.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-tool-to-genai.js +1 -3
- package/dist/channel-management.js +1 -1
- package/dist/cli.js +142 -39
- package/dist/commands/abort.js +1 -1
- package/dist/commands/add-project.js +1 -1
- package/dist/commands/agent.js +6 -2
- package/dist/commands/ask-question.js +2 -1
- package/dist/commands/fork.js +7 -7
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +109 -0
- package/dist/commands/resume.js +3 -5
- package/dist/commands/session.js +56 -1
- package/dist/commands/share.js +1 -1
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +3 -6
- package/dist/config.js +1 -1
- package/dist/discord-bot.js +4 -10
- package/dist/discord-utils.js +33 -9
- package/dist/genai.js +4 -6
- package/dist/interaction-handler.js +8 -1
- package/dist/markdown.js +1 -3
- package/dist/message-formatting.js +7 -3
- package/dist/openai-realtime.js +3 -5
- package/dist/opencode.js +2 -3
- package/dist/session-handler.js +42 -25
- package/dist/system-message.js +5 -3
- package/dist/tools.js +9 -22
- package/dist/unnest-code-blocks.js +4 -2
- package/dist/unnest-code-blocks.test.js +40 -15
- package/dist/voice-handler.js +9 -12
- package/dist/voice.js +5 -3
- package/dist/xml.js +2 -4
- package/package.json +3 -2
- package/src/__snapshots__/compact-session-context-no-system.md +24 -24
- package/src/__snapshots__/compact-session-context.md +31 -31
- package/src/ai-tool-to-genai.ts +3 -11
- package/src/channel-management.ts +14 -25
- package/src/cli.ts +290 -195
- package/src/commands/abort.ts +1 -3
- package/src/commands/add-project.ts +8 -14
- package/src/commands/agent.ts +16 -9
- package/src/commands/ask-question.ts +8 -7
- package/src/commands/create-new-project.ts +8 -14
- package/src/commands/fork.ts +23 -27
- package/src/commands/model.ts +14 -11
- package/src/commands/permissions.ts +1 -1
- package/src/commands/queue.ts +6 -19
- package/src/commands/remove-project.ts +136 -0
- package/src/commands/resume.ts +11 -30
- package/src/commands/session.ts +68 -9
- package/src/commands/share.ts +1 -3
- package/src/commands/types.ts +1 -3
- package/src/commands/undo-redo.ts +6 -18
- package/src/commands/user-command.ts +8 -10
- package/src/config.ts +5 -5
- package/src/database.ts +10 -8
- package/src/discord-bot.ts +22 -46
- package/src/discord-utils.ts +35 -18
- package/src/escape-backticks.test.ts +0 -2
- package/src/format-tables.ts +1 -4
- package/src/genai-worker-wrapper.ts +3 -9
- package/src/genai-worker.ts +4 -19
- package/src/genai.ts +10 -42
- package/src/interaction-handler.ts +133 -121
- package/src/markdown.test.ts +10 -32
- package/src/markdown.ts +6 -14
- package/src/message-formatting.ts +13 -14
- package/src/openai-realtime.ts +25 -47
- package/src/opencode.ts +26 -37
- package/src/session-handler.ts +111 -75
- package/src/system-message.ts +13 -3
- package/src/tools.ts +13 -39
- package/src/unnest-code-blocks.test.ts +42 -15
- package/src/unnest-code-blocks.ts +4 -2
- package/src/utils.ts +1 -4
- package/src/voice-handler.ts +34 -78
- package/src/voice.ts +11 -19
- package/src/xml.test.ts +1 -1
- package/src/xml.ts +3 -12
package/src/openai-realtime.ts
CHANGED
|
@@ -129,13 +129,7 @@ function createWavHeader(dataLength: number, options: WavConversionOptions) {
|
|
|
129
129
|
return buffer
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
function defaultAudioChunkHandler({
|
|
133
|
-
data,
|
|
134
|
-
mimeType,
|
|
135
|
-
}: {
|
|
136
|
-
data: Buffer
|
|
137
|
-
mimeType: string
|
|
138
|
-
}) {
|
|
132
|
+
function defaultAudioChunkHandler({ data, mimeType }: { data: Buffer; mimeType: string }) {
|
|
139
133
|
audioParts.push(data)
|
|
140
134
|
const fileName = 'audio.wav'
|
|
141
135
|
const buffer = convertToWav(audioParts, mimeType)
|
|
@@ -247,36 +241,23 @@ export async function startGenAiSession({
|
|
|
247
241
|
}
|
|
248
242
|
|
|
249
243
|
// Set up event handlers
|
|
250
|
-
client.on(
|
|
251
|
-
'
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
'
|
|
255
|
-
item.
|
|
256
|
-
item.type === '
|
|
257
|
-
) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
'content' in item &&
|
|
261
|
-
Array.isArray(item.content) &&
|
|
262
|
-
item.content.some((c) => 'type' in c && c.type === 'audio')
|
|
263
|
-
if (hasAudio && !isAssistantSpeaking && onAssistantStartSpeaking) {
|
|
264
|
-
isAssistantSpeaking = true
|
|
265
|
-
onAssistantStartSpeaking()
|
|
266
|
-
}
|
|
244
|
+
client.on('conversation.item.created', ({ item }: { item: ConversationItem }) => {
|
|
245
|
+
if ('role' in item && item.role === 'assistant' && item.type === 'message') {
|
|
246
|
+
// Check if this is the first audio content
|
|
247
|
+
const hasAudio =
|
|
248
|
+
'content' in item &&
|
|
249
|
+
Array.isArray(item.content) &&
|
|
250
|
+
item.content.some((c) => 'type' in c && c.type === 'audio')
|
|
251
|
+
if (hasAudio && !isAssistantSpeaking && onAssistantStartSpeaking) {
|
|
252
|
+
isAssistantSpeaking = true
|
|
253
|
+
onAssistantStartSpeaking()
|
|
267
254
|
}
|
|
268
|
-
}
|
|
269
|
-
)
|
|
255
|
+
}
|
|
256
|
+
})
|
|
270
257
|
|
|
271
258
|
client.on(
|
|
272
259
|
'conversation.updated',
|
|
273
|
-
({
|
|
274
|
-
item,
|
|
275
|
-
delta,
|
|
276
|
-
}: {
|
|
277
|
-
item: ConversationItem
|
|
278
|
-
delta: ConversationEventDelta | null
|
|
279
|
-
}) => {
|
|
260
|
+
({ item, delta }: { item: ConversationItem; delta: ConversationEventDelta | null }) => {
|
|
280
261
|
// Handle audio chunks
|
|
281
262
|
if (delta?.audio && 'role' in item && item.role === 'assistant') {
|
|
282
263
|
if (!isAssistantSpeaking && onAssistantStartSpeaking) {
|
|
@@ -313,20 +294,17 @@ export async function startGenAiSession({
|
|
|
313
294
|
},
|
|
314
295
|
)
|
|
315
296
|
|
|
316
|
-
client.on(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
},
|
|
329
|
-
)
|
|
297
|
+
client.on('conversation.item.completed', ({ item }: { item: ConversationItem }) => {
|
|
298
|
+
if (
|
|
299
|
+
'role' in item &&
|
|
300
|
+
item.role === 'assistant' &&
|
|
301
|
+
isAssistantSpeaking &&
|
|
302
|
+
onAssistantStopSpeaking
|
|
303
|
+
) {
|
|
304
|
+
isAssistantSpeaking = false
|
|
305
|
+
onAssistantStopSpeaking()
|
|
306
|
+
}
|
|
307
|
+
})
|
|
330
308
|
|
|
331
309
|
client.on('conversation.interrupted', () => {
|
|
332
310
|
openaiLogger.log('Assistant was interrupted')
|
package/src/opencode.ts
CHANGED
|
@@ -5,11 +5,7 @@
|
|
|
5
5
|
import { spawn, type ChildProcess } from 'node:child_process'
|
|
6
6
|
import fs from 'node:fs'
|
|
7
7
|
import net from 'node:net'
|
|
8
|
-
import {
|
|
9
|
-
createOpencodeClient,
|
|
10
|
-
type OpencodeClient,
|
|
11
|
-
type Config,
|
|
12
|
-
} from '@opencode-ai/sdk'
|
|
8
|
+
import { createOpencodeClient, type OpencodeClient, type Config } from '@opencode-ai/sdk'
|
|
13
9
|
import {
|
|
14
10
|
createOpencodeClient as createOpencodeClientV2,
|
|
15
11
|
type OpencodeClient as OpencodeClientV2,
|
|
@@ -84,9 +80,7 @@ async function waitForServer(port: number, maxAttempts = 30): Promise<boolean> {
|
|
|
84
80
|
}
|
|
85
81
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
86
82
|
}
|
|
87
|
-
throw new Error(
|
|
88
|
-
`Server did not start on port ${port} after ${maxAttempts} seconds`,
|
|
89
|
-
)
|
|
83
|
+
throw new Error(`Server did not start on port ${port} after ${maxAttempts} seconds`)
|
|
90
84
|
}
|
|
91
85
|
|
|
92
86
|
export async function initializeOpencodeForDirectory(directory: string) {
|
|
@@ -115,36 +109,33 @@ export async function initializeOpencodeForDirectory(directory: string) {
|
|
|
115
109
|
|
|
116
110
|
const port = await getOpenPort()
|
|
117
111
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
{
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
webfetch: 'allow',
|
|
138
|
-
},
|
|
139
|
-
} satisfies Config),
|
|
140
|
-
OPENCODE_PORT: port.toString(),
|
|
141
|
-
},
|
|
112
|
+
const opencodeCommand = process.env.OPENCODE_PATH || 'opencode'
|
|
113
|
+
|
|
114
|
+
const serverProcess = spawn(opencodeCommand, ['serve', '--port', port.toString()], {
|
|
115
|
+
stdio: 'pipe',
|
|
116
|
+
detached: false,
|
|
117
|
+
cwd: directory,
|
|
118
|
+
env: {
|
|
119
|
+
...process.env,
|
|
120
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
121
|
+
$schema: 'https://opencode.ai/config.json',
|
|
122
|
+
lsp: false,
|
|
123
|
+
formatter: false,
|
|
124
|
+
permission: {
|
|
125
|
+
edit: 'allow',
|
|
126
|
+
bash: 'allow',
|
|
127
|
+
webfetch: 'allow',
|
|
128
|
+
},
|
|
129
|
+
} satisfies Config),
|
|
130
|
+
OPENCODE_PORT: port.toString(),
|
|
142
131
|
},
|
|
143
|
-
)
|
|
132
|
+
})
|
|
144
133
|
|
|
145
134
|
// Buffer logs until we know if server started successfully
|
|
146
135
|
const logBuffer: string[] = []
|
|
147
|
-
logBuffer.push(
|
|
136
|
+
logBuffer.push(
|
|
137
|
+
`Spawned opencode serve --port ${port} in ${directory} (pid: ${serverProcess.pid})`,
|
|
138
|
+
)
|
|
148
139
|
|
|
149
140
|
serverProcess.stdout?.on('data', (data) => {
|
|
150
141
|
logBuffer.push(`[stdout] ${data.toString().trim()}`)
|
|
@@ -172,9 +163,7 @@ export async function initializeOpencodeForDirectory(directory: string) {
|
|
|
172
163
|
opencodeLogger.error(`Failed to restart opencode server:`, e)
|
|
173
164
|
})
|
|
174
165
|
} else {
|
|
175
|
-
opencodeLogger.error(
|
|
176
|
-
`Server for ${directory} crashed too many times (5), not restarting`,
|
|
177
|
-
)
|
|
166
|
+
opencodeLogger.error(`Server for ${directory} crashed too many times (5), not restarting`)
|
|
178
167
|
}
|
|
179
168
|
} else {
|
|
180
169
|
serverRetryCount.delete(directory)
|
package/src/session-handler.ts
CHANGED
|
@@ -6,14 +6,29 @@ import type { Part, PermissionRequest } from '@opencode-ai/sdk/v2'
|
|
|
6
6
|
import type { FilePartInput } from '@opencode-ai/sdk'
|
|
7
7
|
import type { Message, ThreadChannel } from 'discord.js'
|
|
8
8
|
import prettyMilliseconds from 'pretty-ms'
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
getDatabase,
|
|
11
|
+
getSessionModel,
|
|
12
|
+
getChannelModel,
|
|
13
|
+
getSessionAgent,
|
|
14
|
+
getChannelAgent,
|
|
15
|
+
setSessionAgent,
|
|
16
|
+
} from './database.js'
|
|
17
|
+
import {
|
|
18
|
+
initializeOpencodeForDirectory,
|
|
19
|
+
getOpencodeServers,
|
|
20
|
+
getOpencodeClientV2,
|
|
21
|
+
} from './opencode.js'
|
|
11
22
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from './discord-utils.js'
|
|
12
23
|
import { formatPart } from './message-formatting.js'
|
|
13
24
|
import { getOpencodeSystemMessage } from './system-message.js'
|
|
14
25
|
import { createLogger } from './logger.js'
|
|
15
26
|
import { isAbortError } from './utils.js'
|
|
16
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
showAskUserQuestionDropdowns,
|
|
29
|
+
cancelPendingQuestion,
|
|
30
|
+
pendingQuestionContexts,
|
|
31
|
+
} from './commands/ask-question.js'
|
|
17
32
|
import { showPermissionDropdown, cleanupPermissionContext } from './commands/permissions.js'
|
|
18
33
|
|
|
19
34
|
const sessionLogger = createLogger('SESSION')
|
|
@@ -96,7 +111,9 @@ export async function abortAndRetrySession({
|
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
// Small delay to let the abort propagate
|
|
99
|
-
await new Promise((resolve) => {
|
|
114
|
+
await new Promise((resolve) => {
|
|
115
|
+
setTimeout(resolve, 300)
|
|
116
|
+
})
|
|
100
117
|
|
|
101
118
|
// Fetch last user message from API
|
|
102
119
|
sessionLogger.log(`[ABORT+RETRY] Fetching last user message for session ${sessionId}`)
|
|
@@ -110,7 +127,9 @@ export async function abortAndRetrySession({
|
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
// Extract text and images from parts
|
|
113
|
-
const textPart = lastUserMessage.parts.find((p) => p.type === 'text') as
|
|
130
|
+
const textPart = lastUserMessage.parts.find((p) => p.type === 'text') as
|
|
131
|
+
| { type: 'text'; text: string }
|
|
132
|
+
| undefined
|
|
114
133
|
const prompt = textPart?.text || ''
|
|
115
134
|
const images = lastUserMessage.parts.filter((p) => p.type === 'file') as FilePartInput[]
|
|
116
135
|
|
|
@@ -141,6 +160,7 @@ export async function handleOpencodeSession({
|
|
|
141
160
|
images = [],
|
|
142
161
|
channelId,
|
|
143
162
|
command,
|
|
163
|
+
agent,
|
|
144
164
|
}: {
|
|
145
165
|
prompt: string
|
|
146
166
|
thread: ThreadChannel
|
|
@@ -150,6 +170,8 @@ export async function handleOpencodeSession({
|
|
|
150
170
|
channelId?: string
|
|
151
171
|
/** If set, uses session.command API instead of session.prompt */
|
|
152
172
|
command?: { name: string; arguments: string }
|
|
173
|
+
/** Agent to use for this session */
|
|
174
|
+
agent?: string
|
|
153
175
|
}): Promise<{ sessionID: string; result: any; port?: number } | undefined> {
|
|
154
176
|
voiceLogger.log(
|
|
155
177
|
`[OPENCODE SESSION] Starting for thread ${thread.id} with prompt: "${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}"`,
|
|
@@ -180,17 +202,13 @@ export async function handleOpencodeSession({
|
|
|
180
202
|
session = sessionResponse.data
|
|
181
203
|
sessionLogger.log(`Successfully reused session ${sessionId}`)
|
|
182
204
|
} catch (error) {
|
|
183
|
-
voiceLogger.log(
|
|
184
|
-
`[SESSION] Session ${sessionId} not found, will create new one`,
|
|
185
|
-
)
|
|
205
|
+
voiceLogger.log(`[SESSION] Session ${sessionId} not found, will create new one`)
|
|
186
206
|
}
|
|
187
207
|
}
|
|
188
208
|
|
|
189
209
|
if (!session) {
|
|
190
210
|
const sessionTitle = prompt.length > 80 ? prompt.slice(0, 77) + '...' : prompt.slice(0, 80)
|
|
191
|
-
voiceLogger.log(
|
|
192
|
-
`[SESSION] Creating new session with title: "${sessionTitle}"`,
|
|
193
|
-
)
|
|
211
|
+
voiceLogger.log(`[SESSION] Creating new session with title: "${sessionTitle}"`)
|
|
194
212
|
const sessionResponse = await getClient().session.create({
|
|
195
213
|
body: { title: sessionTitle },
|
|
196
214
|
})
|
|
@@ -203,24 +221,28 @@ export async function handleOpencodeSession({
|
|
|
203
221
|
}
|
|
204
222
|
|
|
205
223
|
getDatabase()
|
|
206
|
-
.prepare(
|
|
207
|
-
'INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)',
|
|
208
|
-
)
|
|
224
|
+
.prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
|
|
209
225
|
.run(thread.id, session.id)
|
|
210
226
|
sessionLogger.log(`Stored session ${session.id} for thread ${thread.id}`)
|
|
211
227
|
|
|
228
|
+
// Store agent preference if provided
|
|
229
|
+
if (agent) {
|
|
230
|
+
setSessionAgent(session.id, agent)
|
|
231
|
+
sessionLogger.log(`Set agent preference for session ${session.id}: ${agent}`)
|
|
232
|
+
}
|
|
233
|
+
|
|
212
234
|
const existingController = abortControllers.get(session.id)
|
|
213
235
|
if (existingController) {
|
|
214
|
-
voiceLogger.log(
|
|
215
|
-
`[ABORT] Cancelling existing request for session: ${session.id}`,
|
|
216
|
-
)
|
|
236
|
+
voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`)
|
|
217
237
|
existingController.abort(new Error('New request started'))
|
|
218
238
|
}
|
|
219
239
|
|
|
220
240
|
const pendingPerm = pendingPermissions.get(thread.id)
|
|
221
241
|
if (pendingPerm) {
|
|
222
242
|
try {
|
|
223
|
-
sessionLogger.log(
|
|
243
|
+
sessionLogger.log(
|
|
244
|
+
`[PERMISSION] Auto-rejecting pending permission ${pendingPerm.permission.id} due to new message`,
|
|
245
|
+
)
|
|
224
246
|
const clientV2 = getOpencodeClientV2(directory)
|
|
225
247
|
if (clientV2) {
|
|
226
248
|
await clientV2.permission.reply({
|
|
@@ -231,7 +253,10 @@ export async function handleOpencodeSession({
|
|
|
231
253
|
// Clean up both the pending permission and its dropdown context
|
|
232
254
|
cleanupPermissionContext(pendingPerm.contextHash)
|
|
233
255
|
pendingPermissions.delete(thread.id)
|
|
234
|
-
await sendThreadMessage(
|
|
256
|
+
await sendThreadMessage(
|
|
257
|
+
thread,
|
|
258
|
+
`⚠️ Previous permission request auto-rejected due to new message`,
|
|
259
|
+
)
|
|
235
260
|
} catch (e) {
|
|
236
261
|
sessionLogger.log(`[PERMISSION] Failed to auto-reject permission:`, e)
|
|
237
262
|
cleanupPermissionContext(pendingPerm.contextHash)
|
|
@@ -249,7 +274,9 @@ export async function handleOpencodeSession({
|
|
|
249
274
|
abortControllers.set(session.id, abortController)
|
|
250
275
|
|
|
251
276
|
if (existingController) {
|
|
252
|
-
await new Promise((resolve) => {
|
|
277
|
+
await new Promise((resolve) => {
|
|
278
|
+
setTimeout(resolve, 200)
|
|
279
|
+
})
|
|
253
280
|
if (abortController.signal.aborted) {
|
|
254
281
|
sessionLogger.log(`[DEBOUNCE] Request was superseded during wait, exiting`)
|
|
255
282
|
return
|
|
@@ -268,7 +295,7 @@ export async function handleOpencodeSession({
|
|
|
268
295
|
}
|
|
269
296
|
const eventsResult = await clientV2.event.subscribe(
|
|
270
297
|
{ directory },
|
|
271
|
-
{ signal: abortController.signal }
|
|
298
|
+
{ signal: abortController.signal },
|
|
272
299
|
)
|
|
273
300
|
|
|
274
301
|
if (abortController.signal.aborted) {
|
|
@@ -280,10 +307,11 @@ export async function handleOpencodeSession({
|
|
|
280
307
|
sessionLogger.log(`Subscribed to OpenCode events`)
|
|
281
308
|
|
|
282
309
|
const sentPartIds = new Set<string>(
|
|
283
|
-
(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
310
|
+
(
|
|
311
|
+
getDatabase()
|
|
312
|
+
.prepare('SELECT part_id FROM part_messages WHERE thread_id = ?')
|
|
313
|
+
.all(thread.id) as { part_id: string }[]
|
|
314
|
+
).map((row) => row.part_id),
|
|
287
315
|
)
|
|
288
316
|
|
|
289
317
|
let currentParts: Part[] = []
|
|
@@ -376,7 +404,12 @@ export async function handleOpencodeSession({
|
|
|
376
404
|
}
|
|
377
405
|
|
|
378
406
|
if (msg.role === 'assistant') {
|
|
379
|
-
const newTokensTotal =
|
|
407
|
+
const newTokensTotal =
|
|
408
|
+
msg.tokens.input +
|
|
409
|
+
msg.tokens.output +
|
|
410
|
+
msg.tokens.reasoning +
|
|
411
|
+
msg.tokens.cache.read +
|
|
412
|
+
msg.tokens.cache.write
|
|
380
413
|
if (newTokensTotal > 0) {
|
|
381
414
|
tokensUsedInSession = newTokensTotal
|
|
382
415
|
}
|
|
@@ -389,7 +422,9 @@ export async function handleOpencodeSession({
|
|
|
389
422
|
if (tokensUsedInSession > 0 && usedProviderID && usedModel) {
|
|
390
423
|
if (!modelContextLimit) {
|
|
391
424
|
try {
|
|
392
|
-
const providersResponse = await getClient().provider.list({
|
|
425
|
+
const providersResponse = await getClient().provider.list({
|
|
426
|
+
query: { directory },
|
|
427
|
+
})
|
|
393
428
|
const provider = providersResponse.data?.all?.find((p) => p.id === usedProviderID)
|
|
394
429
|
const model = provider?.models?.[usedModel]
|
|
395
430
|
if (model?.limit?.context) {
|
|
@@ -401,7 +436,9 @@ export async function handleOpencodeSession({
|
|
|
401
436
|
}
|
|
402
437
|
|
|
403
438
|
if (modelContextLimit) {
|
|
404
|
-
const currentPercentage = Math.floor(
|
|
439
|
+
const currentPercentage = Math.floor(
|
|
440
|
+
(tokensUsedInSession / modelContextLimit) * 100,
|
|
441
|
+
)
|
|
405
442
|
const thresholdCrossed = Math.floor(currentPercentage / 10) * 10
|
|
406
443
|
if (thresholdCrossed > lastDisplayedContextPercentage && thresholdCrossed >= 10) {
|
|
407
444
|
lastDisplayedContextPercentage = thresholdCrossed
|
|
@@ -422,9 +459,7 @@ export async function handleOpencodeSession({
|
|
|
422
459
|
continue
|
|
423
460
|
}
|
|
424
461
|
|
|
425
|
-
const existingIndex = currentParts.findIndex(
|
|
426
|
-
(p: Part) => p.id === part.id,
|
|
427
|
-
)
|
|
462
|
+
const existingIndex = currentParts.findIndex((p: Part) => p.id === part.id)
|
|
428
463
|
if (existingIndex >= 0) {
|
|
429
464
|
currentParts[existingIndex] = part
|
|
430
465
|
} else {
|
|
@@ -459,9 +494,8 @@ export async function handleOpencodeSession({
|
|
|
459
494
|
const outputTokens = Math.ceil(output.length / 4)
|
|
460
495
|
const LARGE_OUTPUT_THRESHOLD = 3000
|
|
461
496
|
if (outputTokens >= LARGE_OUTPUT_THRESHOLD) {
|
|
462
|
-
const formattedTokens =
|
|
463
|
-
? `${(outputTokens / 1000).toFixed(1)}k`
|
|
464
|
-
: String(outputTokens)
|
|
497
|
+
const formattedTokens =
|
|
498
|
+
outputTokens >= 1000 ? `${(outputTokens / 1000).toFixed(1)}k` : String(outputTokens)
|
|
465
499
|
const percentageSuffix = (() => {
|
|
466
500
|
if (!modelContextLimit) {
|
|
467
501
|
return ''
|
|
@@ -510,18 +544,13 @@ export async function handleOpencodeSession({
|
|
|
510
544
|
const errorData = event.properties.error
|
|
511
545
|
const errorMessage = errorData?.data?.message || 'Unknown error'
|
|
512
546
|
sessionLogger.error(`Sending error to thread: ${errorMessage}`)
|
|
513
|
-
await sendThreadMessage(
|
|
514
|
-
thread,
|
|
515
|
-
`✗ opencode session error: ${errorMessage}`,
|
|
516
|
-
)
|
|
547
|
+
await sendThreadMessage(thread, `✗ opencode session error: ${errorMessage}`)
|
|
517
548
|
|
|
518
549
|
if (originalMessage) {
|
|
519
550
|
try {
|
|
520
551
|
await originalMessage.reactions.removeAll()
|
|
521
552
|
await originalMessage.react('❌')
|
|
522
|
-
voiceLogger.log(
|
|
523
|
-
`[REACTION] Added error reaction due to session error`,
|
|
524
|
-
)
|
|
553
|
+
voiceLogger.log(`[REACTION] Added error reaction due to session error`)
|
|
525
554
|
} catch (e) {
|
|
526
555
|
discordLogger.log(`Could not update reaction:`, e)
|
|
527
556
|
}
|
|
@@ -570,9 +599,7 @@ export async function handleOpencodeSession({
|
|
|
570
599
|
continue
|
|
571
600
|
}
|
|
572
601
|
|
|
573
|
-
sessionLogger.log(
|
|
574
|
-
`Permission ${requestID} replied with: ${reply}`,
|
|
575
|
-
)
|
|
602
|
+
sessionLogger.log(`Permission ${requestID} replied with: ${reply}`)
|
|
576
603
|
|
|
577
604
|
const pending = pendingPermissions.get(thread.id)
|
|
578
605
|
if (pending && pending.permission.id === requestID) {
|
|
@@ -624,9 +651,7 @@ export async function handleOpencodeSession({
|
|
|
624
651
|
}
|
|
625
652
|
} catch (e) {
|
|
626
653
|
if (isAbortError(e, abortController.signal)) {
|
|
627
|
-
sessionLogger.log(
|
|
628
|
-
'AbortController aborted event handling (normal exit)',
|
|
629
|
-
)
|
|
654
|
+
sessionLogger.log('AbortController aborted event handling (normal exit)')
|
|
630
655
|
return
|
|
631
656
|
}
|
|
632
657
|
sessionLogger.error(`Unexpected error in event handling code`, e)
|
|
@@ -647,16 +672,12 @@ export async function handleOpencodeSession({
|
|
|
647
672
|
stopTyping = null
|
|
648
673
|
}
|
|
649
674
|
|
|
650
|
-
if (
|
|
651
|
-
|
|
652
|
-
abortController.signal.reason === 'finished'
|
|
653
|
-
) {
|
|
654
|
-
const sessionDuration = prettyMilliseconds(
|
|
655
|
-
Date.now() - sessionStartTime,
|
|
656
|
-
)
|
|
675
|
+
if (!abortController.signal.aborted || abortController.signal.reason === 'finished') {
|
|
676
|
+
const sessionDuration = prettyMilliseconds(Date.now() - sessionStartTime)
|
|
657
677
|
const attachCommand = port ? ` ⋅ ${session.id}` : ''
|
|
658
678
|
const modelInfo = usedModel ? ` ⋅ ${usedModel}` : ''
|
|
659
|
-
const agentInfo =
|
|
679
|
+
const agentInfo =
|
|
680
|
+
usedAgent && usedAgent.toLowerCase() !== 'build' ? ` ⋅ **${usedAgent}**` : ''
|
|
660
681
|
let contextInfo = ''
|
|
661
682
|
|
|
662
683
|
try {
|
|
@@ -671,8 +692,14 @@ export async function handleOpencodeSession({
|
|
|
671
692
|
sessionLogger.error('Failed to fetch provider info for context percentage:', e)
|
|
672
693
|
}
|
|
673
694
|
|
|
674
|
-
await sendThreadMessage(
|
|
675
|
-
|
|
695
|
+
await sendThreadMessage(
|
|
696
|
+
thread,
|
|
697
|
+
`_Completed in ${sessionDuration}${contextInfo}_${attachCommand}${modelInfo}${agentInfo}`,
|
|
698
|
+
{ flags: NOTIFY_MESSAGE_FLAGS },
|
|
699
|
+
)
|
|
700
|
+
sessionLogger.log(
|
|
701
|
+
`DURATION: Session completed in ${sessionDuration}, port ${port}, model ${usedModel}, tokens ${tokensUsedInSession}`,
|
|
702
|
+
)
|
|
676
703
|
|
|
677
704
|
// Process queued messages after completion
|
|
678
705
|
const queue = messageQueue.get(thread.id)
|
|
@@ -685,7 +712,10 @@ export async function handleOpencodeSession({
|
|
|
685
712
|
sessionLogger.log(`[QUEUE] Processing queued message from ${nextMessage.username}`)
|
|
686
713
|
|
|
687
714
|
// Show that queued message is being sent
|
|
688
|
-
await sendThreadMessage(
|
|
715
|
+
await sendThreadMessage(
|
|
716
|
+
thread,
|
|
717
|
+
`» **${nextMessage.username}:** ${nextMessage.prompt.slice(0, 150)}${nextMessage.prompt.length > 150 ? '...' : ''}`,
|
|
718
|
+
)
|
|
689
719
|
|
|
690
720
|
// Send the queued message as a new prompt (recursive call)
|
|
691
721
|
// Use setImmediate to avoid blocking and allow this finally to complete
|
|
@@ -729,7 +759,14 @@ export async function handleOpencodeSession({
|
|
|
729
759
|
if (images.length === 0) {
|
|
730
760
|
return prompt
|
|
731
761
|
}
|
|
732
|
-
sessionLogger.log(
|
|
762
|
+
sessionLogger.log(
|
|
763
|
+
`[PROMPT] Sending ${images.length} image(s):`,
|
|
764
|
+
images.map((img) => ({
|
|
765
|
+
mime: img.mime,
|
|
766
|
+
filename: img.filename,
|
|
767
|
+
url: img.url.slice(0, 100),
|
|
768
|
+
})),
|
|
769
|
+
)
|
|
733
770
|
const imagePathsList = images.map((img) => `- ${img.filename}: ${img.url}`).join('\n')
|
|
734
771
|
return `${prompt}\n\n**attached images:**\n${imagePathsList}`
|
|
735
772
|
})()
|
|
@@ -738,7 +775,8 @@ export async function handleOpencodeSession({
|
|
|
738
775
|
sessionLogger.log(`[PROMPT] Parts to send:`, parts.length)
|
|
739
776
|
|
|
740
777
|
// Get model preference: session-level overrides channel-level
|
|
741
|
-
const modelPreference =
|
|
778
|
+
const modelPreference =
|
|
779
|
+
getSessionModel(session.id) || (channelId ? getChannelModel(channelId) : undefined)
|
|
742
780
|
const modelParam = (() => {
|
|
743
781
|
if (!modelPreference) {
|
|
744
782
|
return undefined
|
|
@@ -753,7 +791,8 @@ export async function handleOpencodeSession({
|
|
|
753
791
|
})()
|
|
754
792
|
|
|
755
793
|
// Get agent preference: session-level overrides channel-level
|
|
756
|
-
const agentPreference =
|
|
794
|
+
const agentPreference =
|
|
795
|
+
getSessionAgent(session.id) || (channelId ? getChannelAgent(channelId) : undefined)
|
|
757
796
|
if (agentPreference) {
|
|
758
797
|
sessionLogger.log(`[AGENT] Using agent preference: ${agentPreference}`)
|
|
759
798
|
}
|
|
@@ -825,20 +864,17 @@ export async function handleOpencodeSession({
|
|
|
825
864
|
discordLogger.log(`Could not update reaction:`, e)
|
|
826
865
|
}
|
|
827
866
|
}
|
|
828
|
-
const
|
|
829
|
-
error
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
typeof error
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
await sendThreadMessage(
|
|
839
|
-
thread,
|
|
840
|
-
`✗ Unexpected bot Error: [${errorName}]\n${errorMsg}`,
|
|
841
|
-
)
|
|
867
|
+
const errorDisplay = (() => {
|
|
868
|
+
if (error instanceof Error) {
|
|
869
|
+
const name = error.constructor.name || 'Error'
|
|
870
|
+
return `[${name}]\n${error.stack || error.message}`
|
|
871
|
+
}
|
|
872
|
+
if (typeof error === 'string') {
|
|
873
|
+
return error
|
|
874
|
+
}
|
|
875
|
+
return String(error)
|
|
876
|
+
})()
|
|
877
|
+
await sendThreadMessage(thread, `✗ Unexpected bot Error: ${errorDisplay}`)
|
|
842
878
|
}
|
|
843
879
|
}
|
|
844
880
|
}
|
package/src/system-message.ts
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
// Creates the system message injected into every OpenCode session,
|
|
3
3
|
// including Discord-specific formatting rules, diff commands, and permissions info.
|
|
4
4
|
|
|
5
|
-
export function getOpencodeSystemMessage({
|
|
5
|
+
export function getOpencodeSystemMessage({
|
|
6
|
+
sessionId,
|
|
7
|
+
channelId,
|
|
8
|
+
}: {
|
|
9
|
+
sessionId: string
|
|
10
|
+
channelId?: string
|
|
11
|
+
}) {
|
|
6
12
|
return `
|
|
7
13
|
The user is reading your messages from inside Discord, via kimaki.xyz
|
|
8
14
|
|
|
@@ -23,7 +29,9 @@ Only users with these Discord permissions can send messages to the bot:
|
|
|
23
29
|
To upload files to the Discord thread (images, screenshots, long files that would clutter the chat), run:
|
|
24
30
|
|
|
25
31
|
npx -y kimaki upload-to-discord --session ${sessionId} <file1> [file2] ...
|
|
26
|
-
${
|
|
32
|
+
${
|
|
33
|
+
channelId
|
|
34
|
+
? `
|
|
27
35
|
## starting new sessions from CLI
|
|
28
36
|
|
|
29
37
|
To start a new thread/session in this channel programmatically, run:
|
|
@@ -31,7 +39,9 @@ To start a new thread/session in this channel programmatically, run:
|
|
|
31
39
|
npx -y kimaki start-session --channel ${channelId} --prompt "your prompt here"
|
|
32
40
|
|
|
33
41
|
This is useful for automation (cron jobs, GitHub webhooks, n8n, etc.)
|
|
34
|
-
`
|
|
42
|
+
`
|
|
43
|
+
: ''
|
|
44
|
+
}
|
|
35
45
|
## showing diffs
|
|
36
46
|
|
|
37
47
|
IMPORTANT: After editing any files, you MUST execute the critique command using the Bash tool to get a diff URL, then share that URL with the user.
|