kimaki 0.4.36 โ 0.4.38
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 +10 -6
- package/dist/cli.js +85 -25
- package/dist/commands/agent.js +101 -19
- package/dist/database.js +22 -0
- package/dist/discord-bot.js +39 -22
- package/dist/interaction-handler.js +6 -1
- package/dist/session-handler.js +36 -6
- package/dist/system-message.js +5 -1
- package/package.json +1 -1
- package/src/channel-management.ts +10 -6
- package/src/cli.ts +118 -37
- package/src/commands/agent.ts +147 -24
- package/src/database.ts +24 -0
- package/src/discord-bot.ts +49 -29
- package/src/interaction-handler.ts +7 -1
- package/src/session-handler.ts +47 -9
- package/src/system-message.ts +5 -1
package/src/database.ts
CHANGED
|
@@ -61,6 +61,21 @@ export function getDatabase(): Database.Database {
|
|
|
61
61
|
)
|
|
62
62
|
`)
|
|
63
63
|
|
|
64
|
+
// Migration: add app_id column to channel_directories for multi-bot support
|
|
65
|
+
try {
|
|
66
|
+
db.exec(`ALTER TABLE channel_directories ADD COLUMN app_id TEXT`)
|
|
67
|
+
} catch {
|
|
68
|
+
// Column already exists, ignore
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Table for threads that should auto-start a session (created by CLI without --notify-only)
|
|
72
|
+
db.exec(`
|
|
73
|
+
CREATE TABLE IF NOT EXISTS pending_auto_start (
|
|
74
|
+
thread_id TEXT PRIMARY KEY,
|
|
75
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
76
|
+
)
|
|
77
|
+
`)
|
|
78
|
+
|
|
64
79
|
db.exec(`
|
|
65
80
|
CREATE TABLE IF NOT EXISTS bot_api_keys (
|
|
66
81
|
app_id TEXT PRIMARY KEY,
|
|
@@ -169,6 +184,15 @@ export function setSessionModel(sessionId: string, modelId: string): void {
|
|
|
169
184
|
)
|
|
170
185
|
}
|
|
171
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Clear the model preference for a session.
|
|
189
|
+
* Used when switching agents so the agent's model takes effect.
|
|
190
|
+
*/
|
|
191
|
+
export function clearSessionModel(sessionId: string): void {
|
|
192
|
+
const db = getDatabase()
|
|
193
|
+
db.prepare('DELETE FROM session_models WHERE session_id = ?').run(sessionId)
|
|
194
|
+
}
|
|
195
|
+
|
|
172
196
|
/**
|
|
173
197
|
* Get the agent preference for a channel.
|
|
174
198
|
*/
|
package/src/discord-bot.ts
CHANGED
|
@@ -187,17 +187,6 @@ export async function startDiscordBot({
|
|
|
187
187
|
const thread = channel as ThreadChannel
|
|
188
188
|
discordLogger.log(`Message in thread ${thread.name} (${thread.id})`)
|
|
189
189
|
|
|
190
|
-
const row = getDatabase()
|
|
191
|
-
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
192
|
-
.get(thread.id) as { session_id: string } | undefined
|
|
193
|
-
|
|
194
|
-
if (!row) {
|
|
195
|
-
discordLogger.log(`No session found for thread ${thread.id}`)
|
|
196
|
-
return
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
voiceLogger.log(`[SESSION] Found session ${row.session_id} for thread ${thread.id}`)
|
|
200
|
-
|
|
201
190
|
const parent = thread.parent as TextChannel | null
|
|
202
191
|
let projectDirectory: string | undefined
|
|
203
192
|
let channelAppId: string | undefined
|
|
@@ -228,6 +217,37 @@ export async function startDiscordBot({
|
|
|
228
217
|
return
|
|
229
218
|
}
|
|
230
219
|
|
|
220
|
+
const row = getDatabase()
|
|
221
|
+
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
222
|
+
.get(thread.id) as { session_id: string } | undefined
|
|
223
|
+
|
|
224
|
+
// No existing session - start a new one (e.g., replying to a notification thread)
|
|
225
|
+
if (!row) {
|
|
226
|
+
discordLogger.log(`No session for thread ${thread.id}, starting new session`)
|
|
227
|
+
|
|
228
|
+
if (!projectDirectory) {
|
|
229
|
+
discordLogger.log(`Cannot start session: no project directory for thread ${thread.id}`)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Include starter message as context for the session
|
|
234
|
+
let prompt = message.content
|
|
235
|
+
const starterMessage = await thread.fetchStarterMessage().catch(() => null)
|
|
236
|
+
if (starterMessage?.content && starterMessage.content !== message.content) {
|
|
237
|
+
prompt = `Context from thread:\n${starterMessage.content}\n\nUser request:\n${message.content}`
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await handleOpencodeSession({
|
|
241
|
+
prompt,
|
|
242
|
+
thread,
|
|
243
|
+
projectDirectory,
|
|
244
|
+
channelId: parent?.id || '',
|
|
245
|
+
})
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
voiceLogger.log(`[SESSION] Found session ${row.session_id} for thread ${thread.id}`)
|
|
250
|
+
|
|
231
251
|
let messageContent = message.content || ''
|
|
232
252
|
|
|
233
253
|
let currentSessionContext: string | undefined
|
|
@@ -393,42 +413,42 @@ export async function startDiscordBot({
|
|
|
393
413
|
}
|
|
394
414
|
})
|
|
395
415
|
|
|
396
|
-
//
|
|
397
|
-
const BOT_SESSION_PREFIX = '๐ค **Bot-initiated session**'
|
|
398
|
-
|
|
399
|
-
// Handle bot-initiated threads created by `kimaki start-session`
|
|
416
|
+
// Handle bot-initiated threads created by `kimaki send` (without --notify-only)
|
|
400
417
|
discordClient.on(Events.ThreadCreate, async (thread, newlyCreated) => {
|
|
401
418
|
try {
|
|
402
419
|
if (!newlyCreated) {
|
|
403
420
|
return
|
|
404
421
|
}
|
|
405
422
|
|
|
423
|
+
// Check if this thread is marked for auto-start in the database
|
|
424
|
+
const db = getDatabase()
|
|
425
|
+
const pendingRow = db
|
|
426
|
+
.prepare('SELECT thread_id FROM pending_auto_start WHERE thread_id = ?')
|
|
427
|
+
.get(thread.id) as { thread_id: string } | undefined
|
|
428
|
+
|
|
429
|
+
if (!pendingRow) {
|
|
430
|
+
return // Not a CLI-initiated auto-start thread
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Remove from pending table
|
|
434
|
+
db.prepare('DELETE FROM pending_auto_start WHERE thread_id = ?').run(thread.id)
|
|
435
|
+
|
|
436
|
+
discordLogger.log(`[BOT_SESSION] Detected bot-initiated thread: ${thread.name}`)
|
|
437
|
+
|
|
406
438
|
// Only handle threads in text channels
|
|
407
439
|
const parent = thread.parent as TextChannel | null
|
|
408
440
|
if (!parent || parent.type !== ChannelType.GuildText) {
|
|
409
441
|
return
|
|
410
442
|
}
|
|
411
443
|
|
|
412
|
-
// Get the starter message
|
|
444
|
+
// Get the starter message for the prompt
|
|
413
445
|
const starterMessage = await thread.fetchStarterMessage().catch(() => null)
|
|
414
446
|
if (!starterMessage) {
|
|
415
447
|
discordLogger.log(`[THREAD_CREATE] Could not fetch starter message for thread ${thread.id}`)
|
|
416
448
|
return
|
|
417
449
|
}
|
|
418
450
|
|
|
419
|
-
|
|
420
|
-
if (starterMessage.author.id !== discordClient.user?.id) {
|
|
421
|
-
return
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (!starterMessage.content.startsWith(BOT_SESSION_PREFIX)) {
|
|
425
|
-
return
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
discordLogger.log(`[BOT_SESSION] Detected bot-initiated thread: ${thread.name}`)
|
|
429
|
-
|
|
430
|
-
// Extract the prompt (everything after the prefix)
|
|
431
|
-
const prompt = starterMessage.content.slice(BOT_SESSION_PREFIX.length).trim()
|
|
451
|
+
const prompt = starterMessage.content.trim()
|
|
432
452
|
if (!prompt) {
|
|
433
453
|
discordLogger.log(`[BOT_SESSION] No prompt found in starter message`)
|
|
434
454
|
return
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
handleProviderSelectMenu,
|
|
21
21
|
handleModelSelectMenu,
|
|
22
22
|
} from './commands/model.js'
|
|
23
|
-
import { handleAgentCommand, handleAgentSelectMenu } from './commands/agent.js'
|
|
23
|
+
import { handleAgentCommand, handleAgentSelectMenu, handleQuickAgentCommand } from './commands/agent.js'
|
|
24
24
|
import { handleAskQuestionSelectMenu } from './commands/ask-question.js'
|
|
25
25
|
import { handleQueueCommand, handleClearQueueCommand } from './commands/queue.js'
|
|
26
26
|
import { handleUndoCommand, handleRedoCommand } from './commands/undo-redo.js'
|
|
@@ -136,6 +136,12 @@ export function registerInteractionHandler({
|
|
|
136
136
|
return
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// Handle quick agent commands (ending with -agent suffix, but not the base /agent command)
|
|
140
|
+
if (interaction.commandName.endsWith('-agent') && interaction.commandName !== 'agent') {
|
|
141
|
+
await handleQuickAgentCommand({ command: interaction, appId })
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
139
145
|
// Handle user-defined commands (ending with -cmd suffix)
|
|
140
146
|
if (interaction.commandName.endsWith('-cmd')) {
|
|
141
147
|
await handleUserCommand({ command: interaction, appId })
|
package/src/session-handler.ts
CHANGED
|
@@ -641,6 +641,39 @@ export async function handleOpencodeSession({
|
|
|
641
641
|
requestId: questionRequest.id,
|
|
642
642
|
input: { questions: questionRequest.questions },
|
|
643
643
|
})
|
|
644
|
+
|
|
645
|
+
// Process queued messages if any - queued message will cancel the pending question
|
|
646
|
+
const queue = messageQueue.get(thread.id)
|
|
647
|
+
if (queue && queue.length > 0) {
|
|
648
|
+
const nextMessage = queue.shift()!
|
|
649
|
+
if (queue.length === 0) {
|
|
650
|
+
messageQueue.delete(thread.id)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
sessionLogger.log(
|
|
654
|
+
`[QUEUE] Question shown but queue has messages, processing from ${nextMessage.username}`,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
await sendThreadMessage(
|
|
658
|
+
thread,
|
|
659
|
+
`ยป **${nextMessage.username}:** ${nextMessage.prompt.slice(0, 150)}${nextMessage.prompt.length > 150 ? '...' : ''}`,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
// handleOpencodeSession will call cancelPendingQuestion, which cancels the dropdown
|
|
663
|
+
setImmediate(() => {
|
|
664
|
+
handleOpencodeSession({
|
|
665
|
+
prompt: nextMessage.prompt,
|
|
666
|
+
thread,
|
|
667
|
+
projectDirectory: directory,
|
|
668
|
+
images: nextMessage.images,
|
|
669
|
+
channelId,
|
|
670
|
+
}).catch(async (e) => {
|
|
671
|
+
sessionLogger.error(`[QUEUE] Failed to process queued message:`, e)
|
|
672
|
+
const errorMsg = e instanceof Error ? e.message : String(e)
|
|
673
|
+
await sendThreadMessage(thread, `โ Queued message failed: ${errorMsg.slice(0, 200)}`)
|
|
674
|
+
})
|
|
675
|
+
})
|
|
676
|
+
}
|
|
644
677
|
} else if (event.type === 'session.idle') {
|
|
645
678
|
// Session is done processing - abort to signal completion
|
|
646
679
|
if (event.properties.sessionID === session.id) {
|
|
@@ -774,10 +807,23 @@ export async function handleOpencodeSession({
|
|
|
774
807
|
const parts = [{ type: 'text' as const, text: promptWithImagePaths }, ...images]
|
|
775
808
|
sessionLogger.log(`[PROMPT] Parts to send:`, parts.length)
|
|
776
809
|
|
|
810
|
+
// Get agent preference: session-level overrides channel-level
|
|
811
|
+
const agentPreference =
|
|
812
|
+
getSessionAgent(session.id) || (channelId ? getChannelAgent(channelId) : undefined)
|
|
813
|
+
if (agentPreference) {
|
|
814
|
+
sessionLogger.log(`[AGENT] Using agent preference: ${agentPreference}`)
|
|
815
|
+
}
|
|
816
|
+
|
|
777
817
|
// Get model preference: session-level overrides channel-level
|
|
818
|
+
// BUT: if an agent is set, don't pass model param so the agent's model takes effect
|
|
778
819
|
const modelPreference =
|
|
779
820
|
getSessionModel(session.id) || (channelId ? getChannelModel(channelId) : undefined)
|
|
780
821
|
const modelParam = (() => {
|
|
822
|
+
// When an agent is set, let the agent's model config take effect
|
|
823
|
+
if (agentPreference) {
|
|
824
|
+
sessionLogger.log(`[MODEL] Skipping model param, agent "${agentPreference}" controls model`)
|
|
825
|
+
return undefined
|
|
826
|
+
}
|
|
781
827
|
if (!modelPreference) {
|
|
782
828
|
return undefined
|
|
783
829
|
}
|
|
@@ -790,13 +836,6 @@ export async function handleOpencodeSession({
|
|
|
790
836
|
return { providerID, modelID }
|
|
791
837
|
})()
|
|
792
838
|
|
|
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
839
|
// Use session.command API for slash commands, session.prompt for regular messages
|
|
801
840
|
const response = command
|
|
802
841
|
? await getClient().session.command({
|
|
@@ -850,9 +889,8 @@ export async function handleOpencodeSession({
|
|
|
850
889
|
|
|
851
890
|
return { sessionID: session.id, result: response.data, port }
|
|
852
891
|
} catch (error) {
|
|
853
|
-
sessionLogger.error(`ERROR: Failed to send prompt:`, error)
|
|
854
|
-
|
|
855
892
|
if (!isAbortError(error, abortController.signal)) {
|
|
893
|
+
sessionLogger.error(`ERROR: Failed to send prompt:`, error)
|
|
856
894
|
abortController.abort('error')
|
|
857
895
|
|
|
858
896
|
if (originalMessage) {
|
package/src/system-message.ts
CHANGED
|
@@ -36,7 +36,11 @@ ${
|
|
|
36
36
|
|
|
37
37
|
To start a new thread/session in this channel programmatically, run:
|
|
38
38
|
|
|
39
|
-
npx -y kimaki
|
|
39
|
+
npx -y kimaki send --channel ${channelId} --prompt "your prompt here"
|
|
40
|
+
|
|
41
|
+
Use --notify-only to create a notification thread without starting an AI session:
|
|
42
|
+
|
|
43
|
+
npx -y kimaki send --channel ${channelId} --prompt "User cancelled subscription" --notify-only
|
|
40
44
|
|
|
41
45
|
This is useful for automation (cron jobs, GitHub webhooks, n8n, etc.)
|
|
42
46
|
`
|