kimaki 0.4.35 → 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 +135 -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 +2 -2
- 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 +1 -1
- package/dist/session-handler.js +25 -15
- package/dist/system-message.js +5 -3
- package/dist/tools.js +9 -22
- 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 +282 -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 +4 -13
- 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 +24 -34
- package/src/session-handler.ts +91 -61
- package/src/system-message.ts +13 -3
- package/src/tools.ts +13 -39
- 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) {
|
|
@@ -117,33 +111,31 @@ export async function initializeOpencodeForDirectory(directory: string) {
|
|
|
117
111
|
|
|
118
112
|
const opencodeCommand = process.env.OPENCODE_PATH || 'opencode'
|
|
119
113
|
|
|
120
|
-
const serverProcess = spawn(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
},
|
|
138
|
-
} satisfies Config),
|
|
139
|
-
OPENCODE_PORT: port.toString(),
|
|
140
|
-
},
|
|
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(),
|
|
141
131
|
},
|
|
142
|
-
)
|
|
132
|
+
})
|
|
143
133
|
|
|
144
134
|
// Buffer logs until we know if server started successfully
|
|
145
135
|
const logBuffer: string[] = []
|
|
146
|
-
logBuffer.push(
|
|
136
|
+
logBuffer.push(
|
|
137
|
+
`Spawned opencode serve --port ${port} in ${directory} (pid: ${serverProcess.pid})`,
|
|
138
|
+
)
|
|
147
139
|
|
|
148
140
|
serverProcess.stdout?.on('data', (data) => {
|
|
149
141
|
logBuffer.push(`[stdout] ${data.toString().trim()}`)
|
|
@@ -171,9 +163,7 @@ export async function initializeOpencodeForDirectory(directory: string) {
|
|
|
171
163
|
opencodeLogger.error(`Failed to restart opencode server:`, e)
|
|
172
164
|
})
|
|
173
165
|
} else {
|
|
174
|
-
opencodeLogger.error(
|
|
175
|
-
`Server for ${directory} crashed too many times (5), not restarting`,
|
|
176
|
-
)
|
|
166
|
+
opencodeLogger.error(`Server for ${directory} crashed too many times (5), not restarting`)
|
|
177
167
|
}
|
|
178
168
|
} else {
|
|
179
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
|
|
|
@@ -183,17 +202,13 @@ export async function handleOpencodeSession({
|
|
|
183
202
|
session = sessionResponse.data
|
|
184
203
|
sessionLogger.log(`Successfully reused session ${sessionId}`)
|
|
185
204
|
} catch (error) {
|
|
186
|
-
voiceLogger.log(
|
|
187
|
-
`[SESSION] Session ${sessionId} not found, will create new one`,
|
|
188
|
-
)
|
|
205
|
+
voiceLogger.log(`[SESSION] Session ${sessionId} not found, will create new one`)
|
|
189
206
|
}
|
|
190
207
|
}
|
|
191
208
|
|
|
192
209
|
if (!session) {
|
|
193
210
|
const sessionTitle = prompt.length > 80 ? prompt.slice(0, 77) + '...' : prompt.slice(0, 80)
|
|
194
|
-
voiceLogger.log(
|
|
195
|
-
`[SESSION] Creating new session with title: "${sessionTitle}"`,
|
|
196
|
-
)
|
|
211
|
+
voiceLogger.log(`[SESSION] Creating new session with title: "${sessionTitle}"`)
|
|
197
212
|
const sessionResponse = await getClient().session.create({
|
|
198
213
|
body: { title: sessionTitle },
|
|
199
214
|
})
|
|
@@ -206,9 +221,7 @@ export async function handleOpencodeSession({
|
|
|
206
221
|
}
|
|
207
222
|
|
|
208
223
|
getDatabase()
|
|
209
|
-
.prepare(
|
|
210
|
-
'INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)',
|
|
211
|
-
)
|
|
224
|
+
.prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
|
|
212
225
|
.run(thread.id, session.id)
|
|
213
226
|
sessionLogger.log(`Stored session ${session.id} for thread ${thread.id}`)
|
|
214
227
|
|
|
@@ -220,16 +233,16 @@ export async function handleOpencodeSession({
|
|
|
220
233
|
|
|
221
234
|
const existingController = abortControllers.get(session.id)
|
|
222
235
|
if (existingController) {
|
|
223
|
-
voiceLogger.log(
|
|
224
|
-
`[ABORT] Cancelling existing request for session: ${session.id}`,
|
|
225
|
-
)
|
|
236
|
+
voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`)
|
|
226
237
|
existingController.abort(new Error('New request started'))
|
|
227
238
|
}
|
|
228
239
|
|
|
229
240
|
const pendingPerm = pendingPermissions.get(thread.id)
|
|
230
241
|
if (pendingPerm) {
|
|
231
242
|
try {
|
|
232
|
-
sessionLogger.log(
|
|
243
|
+
sessionLogger.log(
|
|
244
|
+
`[PERMISSION] Auto-rejecting pending permission ${pendingPerm.permission.id} due to new message`,
|
|
245
|
+
)
|
|
233
246
|
const clientV2 = getOpencodeClientV2(directory)
|
|
234
247
|
if (clientV2) {
|
|
235
248
|
await clientV2.permission.reply({
|
|
@@ -240,7 +253,10 @@ export async function handleOpencodeSession({
|
|
|
240
253
|
// Clean up both the pending permission and its dropdown context
|
|
241
254
|
cleanupPermissionContext(pendingPerm.contextHash)
|
|
242
255
|
pendingPermissions.delete(thread.id)
|
|
243
|
-
await sendThreadMessage(
|
|
256
|
+
await sendThreadMessage(
|
|
257
|
+
thread,
|
|
258
|
+
`⚠️ Previous permission request auto-rejected due to new message`,
|
|
259
|
+
)
|
|
244
260
|
} catch (e) {
|
|
245
261
|
sessionLogger.log(`[PERMISSION] Failed to auto-reject permission:`, e)
|
|
246
262
|
cleanupPermissionContext(pendingPerm.contextHash)
|
|
@@ -258,7 +274,9 @@ export async function handleOpencodeSession({
|
|
|
258
274
|
abortControllers.set(session.id, abortController)
|
|
259
275
|
|
|
260
276
|
if (existingController) {
|
|
261
|
-
await new Promise((resolve) => {
|
|
277
|
+
await new Promise((resolve) => {
|
|
278
|
+
setTimeout(resolve, 200)
|
|
279
|
+
})
|
|
262
280
|
if (abortController.signal.aborted) {
|
|
263
281
|
sessionLogger.log(`[DEBOUNCE] Request was superseded during wait, exiting`)
|
|
264
282
|
return
|
|
@@ -277,7 +295,7 @@ export async function handleOpencodeSession({
|
|
|
277
295
|
}
|
|
278
296
|
const eventsResult = await clientV2.event.subscribe(
|
|
279
297
|
{ directory },
|
|
280
|
-
{ signal: abortController.signal }
|
|
298
|
+
{ signal: abortController.signal },
|
|
281
299
|
)
|
|
282
300
|
|
|
283
301
|
if (abortController.signal.aborted) {
|
|
@@ -289,10 +307,11 @@ export async function handleOpencodeSession({
|
|
|
289
307
|
sessionLogger.log(`Subscribed to OpenCode events`)
|
|
290
308
|
|
|
291
309
|
const sentPartIds = new Set<string>(
|
|
292
|
-
(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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),
|
|
296
315
|
)
|
|
297
316
|
|
|
298
317
|
let currentParts: Part[] = []
|
|
@@ -385,7 +404,12 @@ export async function handleOpencodeSession({
|
|
|
385
404
|
}
|
|
386
405
|
|
|
387
406
|
if (msg.role === 'assistant') {
|
|
388
|
-
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
|
|
389
413
|
if (newTokensTotal > 0) {
|
|
390
414
|
tokensUsedInSession = newTokensTotal
|
|
391
415
|
}
|
|
@@ -398,7 +422,9 @@ export async function handleOpencodeSession({
|
|
|
398
422
|
if (tokensUsedInSession > 0 && usedProviderID && usedModel) {
|
|
399
423
|
if (!modelContextLimit) {
|
|
400
424
|
try {
|
|
401
|
-
const providersResponse = await getClient().provider.list({
|
|
425
|
+
const providersResponse = await getClient().provider.list({
|
|
426
|
+
query: { directory },
|
|
427
|
+
})
|
|
402
428
|
const provider = providersResponse.data?.all?.find((p) => p.id === usedProviderID)
|
|
403
429
|
const model = provider?.models?.[usedModel]
|
|
404
430
|
if (model?.limit?.context) {
|
|
@@ -410,7 +436,9 @@ export async function handleOpencodeSession({
|
|
|
410
436
|
}
|
|
411
437
|
|
|
412
438
|
if (modelContextLimit) {
|
|
413
|
-
const currentPercentage = Math.floor(
|
|
439
|
+
const currentPercentage = Math.floor(
|
|
440
|
+
(tokensUsedInSession / modelContextLimit) * 100,
|
|
441
|
+
)
|
|
414
442
|
const thresholdCrossed = Math.floor(currentPercentage / 10) * 10
|
|
415
443
|
if (thresholdCrossed > lastDisplayedContextPercentage && thresholdCrossed >= 10) {
|
|
416
444
|
lastDisplayedContextPercentage = thresholdCrossed
|
|
@@ -431,9 +459,7 @@ export async function handleOpencodeSession({
|
|
|
431
459
|
continue
|
|
432
460
|
}
|
|
433
461
|
|
|
434
|
-
const existingIndex = currentParts.findIndex(
|
|
435
|
-
(p: Part) => p.id === part.id,
|
|
436
|
-
)
|
|
462
|
+
const existingIndex = currentParts.findIndex((p: Part) => p.id === part.id)
|
|
437
463
|
if (existingIndex >= 0) {
|
|
438
464
|
currentParts[existingIndex] = part
|
|
439
465
|
} else {
|
|
@@ -468,9 +494,8 @@ export async function handleOpencodeSession({
|
|
|
468
494
|
const outputTokens = Math.ceil(output.length / 4)
|
|
469
495
|
const LARGE_OUTPUT_THRESHOLD = 3000
|
|
470
496
|
if (outputTokens >= LARGE_OUTPUT_THRESHOLD) {
|
|
471
|
-
const formattedTokens =
|
|
472
|
-
? `${(outputTokens / 1000).toFixed(1)}k`
|
|
473
|
-
: String(outputTokens)
|
|
497
|
+
const formattedTokens =
|
|
498
|
+
outputTokens >= 1000 ? `${(outputTokens / 1000).toFixed(1)}k` : String(outputTokens)
|
|
474
499
|
const percentageSuffix = (() => {
|
|
475
500
|
if (!modelContextLimit) {
|
|
476
501
|
return ''
|
|
@@ -519,18 +544,13 @@ export async function handleOpencodeSession({
|
|
|
519
544
|
const errorData = event.properties.error
|
|
520
545
|
const errorMessage = errorData?.data?.message || 'Unknown error'
|
|
521
546
|
sessionLogger.error(`Sending error to thread: ${errorMessage}`)
|
|
522
|
-
await sendThreadMessage(
|
|
523
|
-
thread,
|
|
524
|
-
`✗ opencode session error: ${errorMessage}`,
|
|
525
|
-
)
|
|
547
|
+
await sendThreadMessage(thread, `✗ opencode session error: ${errorMessage}`)
|
|
526
548
|
|
|
527
549
|
if (originalMessage) {
|
|
528
550
|
try {
|
|
529
551
|
await originalMessage.reactions.removeAll()
|
|
530
552
|
await originalMessage.react('❌')
|
|
531
|
-
voiceLogger.log(
|
|
532
|
-
`[REACTION] Added error reaction due to session error`,
|
|
533
|
-
)
|
|
553
|
+
voiceLogger.log(`[REACTION] Added error reaction due to session error`)
|
|
534
554
|
} catch (e) {
|
|
535
555
|
discordLogger.log(`Could not update reaction:`, e)
|
|
536
556
|
}
|
|
@@ -579,9 +599,7 @@ export async function handleOpencodeSession({
|
|
|
579
599
|
continue
|
|
580
600
|
}
|
|
581
601
|
|
|
582
|
-
sessionLogger.log(
|
|
583
|
-
`Permission ${requestID} replied with: ${reply}`,
|
|
584
|
-
)
|
|
602
|
+
sessionLogger.log(`Permission ${requestID} replied with: ${reply}`)
|
|
585
603
|
|
|
586
604
|
const pending = pendingPermissions.get(thread.id)
|
|
587
605
|
if (pending && pending.permission.id === requestID) {
|
|
@@ -633,9 +651,7 @@ export async function handleOpencodeSession({
|
|
|
633
651
|
}
|
|
634
652
|
} catch (e) {
|
|
635
653
|
if (isAbortError(e, abortController.signal)) {
|
|
636
|
-
sessionLogger.log(
|
|
637
|
-
'AbortController aborted event handling (normal exit)',
|
|
638
|
-
)
|
|
654
|
+
sessionLogger.log('AbortController aborted event handling (normal exit)')
|
|
639
655
|
return
|
|
640
656
|
}
|
|
641
657
|
sessionLogger.error(`Unexpected error in event handling code`, e)
|
|
@@ -656,16 +672,12 @@ export async function handleOpencodeSession({
|
|
|
656
672
|
stopTyping = null
|
|
657
673
|
}
|
|
658
674
|
|
|
659
|
-
if (
|
|
660
|
-
|
|
661
|
-
abortController.signal.reason === 'finished'
|
|
662
|
-
) {
|
|
663
|
-
const sessionDuration = prettyMilliseconds(
|
|
664
|
-
Date.now() - sessionStartTime,
|
|
665
|
-
)
|
|
675
|
+
if (!abortController.signal.aborted || abortController.signal.reason === 'finished') {
|
|
676
|
+
const sessionDuration = prettyMilliseconds(Date.now() - sessionStartTime)
|
|
666
677
|
const attachCommand = port ? ` ⋅ ${session.id}` : ''
|
|
667
678
|
const modelInfo = usedModel ? ` ⋅ ${usedModel}` : ''
|
|
668
|
-
const agentInfo =
|
|
679
|
+
const agentInfo =
|
|
680
|
+
usedAgent && usedAgent.toLowerCase() !== 'build' ? ` ⋅ **${usedAgent}**` : ''
|
|
669
681
|
let contextInfo = ''
|
|
670
682
|
|
|
671
683
|
try {
|
|
@@ -680,8 +692,14 @@ export async function handleOpencodeSession({
|
|
|
680
692
|
sessionLogger.error('Failed to fetch provider info for context percentage:', e)
|
|
681
693
|
}
|
|
682
694
|
|
|
683
|
-
await sendThreadMessage(
|
|
684
|
-
|
|
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
|
+
)
|
|
685
703
|
|
|
686
704
|
// Process queued messages after completion
|
|
687
705
|
const queue = messageQueue.get(thread.id)
|
|
@@ -694,7 +712,10 @@ export async function handleOpencodeSession({
|
|
|
694
712
|
sessionLogger.log(`[QUEUE] Processing queued message from ${nextMessage.username}`)
|
|
695
713
|
|
|
696
714
|
// Show that queued message is being sent
|
|
697
|
-
await sendThreadMessage(
|
|
715
|
+
await sendThreadMessage(
|
|
716
|
+
thread,
|
|
717
|
+
`» **${nextMessage.username}:** ${nextMessage.prompt.slice(0, 150)}${nextMessage.prompt.length > 150 ? '...' : ''}`,
|
|
718
|
+
)
|
|
698
719
|
|
|
699
720
|
// Send the queued message as a new prompt (recursive call)
|
|
700
721
|
// Use setImmediate to avoid blocking and allow this finally to complete
|
|
@@ -738,7 +759,14 @@ export async function handleOpencodeSession({
|
|
|
738
759
|
if (images.length === 0) {
|
|
739
760
|
return prompt
|
|
740
761
|
}
|
|
741
|
-
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
|
+
)
|
|
742
770
|
const imagePathsList = images.map((img) => `- ${img.filename}: ${img.url}`).join('\n')
|
|
743
771
|
return `${prompt}\n\n**attached images:**\n${imagePathsList}`
|
|
744
772
|
})()
|
|
@@ -747,7 +775,8 @@ export async function handleOpencodeSession({
|
|
|
747
775
|
sessionLogger.log(`[PROMPT] Parts to send:`, parts.length)
|
|
748
776
|
|
|
749
777
|
// Get model preference: session-level overrides channel-level
|
|
750
|
-
const modelPreference =
|
|
778
|
+
const modelPreference =
|
|
779
|
+
getSessionModel(session.id) || (channelId ? getChannelModel(channelId) : undefined)
|
|
751
780
|
const modelParam = (() => {
|
|
752
781
|
if (!modelPreference) {
|
|
753
782
|
return undefined
|
|
@@ -762,7 +791,8 @@ export async function handleOpencodeSession({
|
|
|
762
791
|
})()
|
|
763
792
|
|
|
764
793
|
// Get agent preference: session-level overrides channel-level
|
|
765
|
-
const agentPreference =
|
|
794
|
+
const agentPreference =
|
|
795
|
+
getSessionAgent(session.id) || (channelId ? getChannelAgent(channelId) : undefined)
|
|
766
796
|
if (agentPreference) {
|
|
767
797
|
sessionLogger.log(`[AGENT] Using agent preference: ${agentPreference}`)
|
|
768
798
|
}
|
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.
|