kimaki 0.4.23 → 0.4.25
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/LICENSE +21 -0
- package/bin.js +6 -1
- package/dist/ai-tool-to-genai.js +3 -0
- package/dist/channel-management.js +3 -0
- package/dist/cli.js +93 -14
- package/dist/commands/abort.js +78 -0
- package/dist/commands/add-project.js +97 -0
- package/dist/commands/create-new-project.js +78 -0
- package/dist/commands/fork.js +186 -0
- package/dist/commands/model.js +294 -0
- package/dist/commands/permissions.js +126 -0
- package/dist/commands/queue.js +129 -0
- package/dist/commands/resume.js +145 -0
- package/dist/commands/session.js +144 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +161 -0
- package/dist/database.js +3 -0
- package/dist/discord-bot.js +3 -0
- package/dist/discord-utils.js +10 -1
- package/dist/format-tables.js +3 -0
- package/dist/genai-worker-wrapper.js +3 -0
- package/dist/genai-worker.js +3 -0
- package/dist/genai.js +3 -0
- package/dist/interaction-handler.js +71 -697
- package/dist/logger.js +3 -0
- package/dist/markdown.js +3 -0
- package/dist/message-formatting.js +41 -6
- package/dist/opencode.js +3 -0
- package/dist/session-handler.js +47 -3
- package/dist/system-message.js +16 -0
- package/dist/tools.js +3 -0
- package/dist/utils.js +3 -0
- package/dist/voice-handler.js +3 -0
- package/dist/voice.js +3 -0
- package/dist/worker-types.js +3 -0
- package/dist/xml.js +3 -0
- package/package.json +11 -12
- package/src/ai-tool-to-genai.ts +4 -0
- package/src/channel-management.ts +4 -0
- package/src/cli.ts +93 -14
- package/src/commands/abort.ts +94 -0
- package/src/commands/add-project.ts +138 -0
- package/src/commands/create-new-project.ts +111 -0
- package/src/{fork.ts → commands/fork.ts} +39 -5
- package/src/{model-command.ts → commands/model.ts} +7 -5
- package/src/commands/permissions.ts +146 -0
- package/src/commands/queue.ts +181 -0
- package/src/commands/resume.ts +230 -0
- package/src/commands/session.ts +186 -0
- package/src/commands/share.ts +96 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +213 -0
- package/src/database.ts +4 -0
- package/src/discord-bot.ts +4 -0
- package/src/discord-utils.ts +12 -0
- package/src/format-tables.ts +4 -0
- package/src/genai-worker-wrapper.ts +4 -0
- package/src/genai-worker.ts +4 -0
- package/src/genai.ts +4 -0
- package/src/interaction-handler.ts +81 -919
- package/src/logger.ts +4 -0
- package/src/markdown.ts +4 -0
- package/src/message-formatting.ts +52 -7
- package/src/opencode.ts +4 -0
- package/src/session-handler.ts +70 -3
- package/src/system-message.ts +17 -0
- package/src/tools.ts +4 -0
- package/src/utils.ts +4 -0
- package/src/voice-handler.ts +4 -0
- package/src/voice.ts +4 -0
- package/src/worker-types.ts +4 -0
- package/src/xml.ts +4 -0
- package/README.md +0 -48
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Undo/Redo commands - /undo, /redo
|
|
2
|
+
|
|
3
|
+
import { ChannelType, type ThreadChannel } from 'discord.js'
|
|
4
|
+
import type { CommandContext } from './types.js'
|
|
5
|
+
import { getDatabase } from '../database.js'
|
|
6
|
+
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
7
|
+
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
8
|
+
import { createLogger } from '../logger.js'
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('UNDO-REDO')
|
|
11
|
+
|
|
12
|
+
export async function handleUndoCommand({
|
|
13
|
+
command,
|
|
14
|
+
}: CommandContext): Promise<void> {
|
|
15
|
+
const channel = command.channel
|
|
16
|
+
|
|
17
|
+
if (!channel) {
|
|
18
|
+
await command.reply({
|
|
19
|
+
content: 'This command can only be used in a channel',
|
|
20
|
+
ephemeral: true,
|
|
21
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
22
|
+
})
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const isThread = [
|
|
27
|
+
ChannelType.PublicThread,
|
|
28
|
+
ChannelType.PrivateThread,
|
|
29
|
+
ChannelType.AnnouncementThread,
|
|
30
|
+
].includes(channel.type)
|
|
31
|
+
|
|
32
|
+
if (!isThread) {
|
|
33
|
+
await command.reply({
|
|
34
|
+
content: 'This command can only be used in a thread with an active session',
|
|
35
|
+
ephemeral: true,
|
|
36
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
37
|
+
})
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const textChannel = await resolveTextChannel(channel as ThreadChannel)
|
|
42
|
+
const { projectDirectory: directory } = getKimakiMetadata(textChannel)
|
|
43
|
+
|
|
44
|
+
if (!directory) {
|
|
45
|
+
await command.reply({
|
|
46
|
+
content: 'Could not determine project directory for this channel',
|
|
47
|
+
ephemeral: true,
|
|
48
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
49
|
+
})
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const row = getDatabase()
|
|
54
|
+
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
55
|
+
.get(channel.id) as { session_id: string } | undefined
|
|
56
|
+
|
|
57
|
+
if (!row?.session_id) {
|
|
58
|
+
await command.reply({
|
|
59
|
+
content: 'No active session in this thread',
|
|
60
|
+
ephemeral: true,
|
|
61
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
62
|
+
})
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sessionId = row.session_id
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
70
|
+
|
|
71
|
+
const getClient = await initializeOpencodeForDirectory(directory)
|
|
72
|
+
|
|
73
|
+
// Fetch messages to find the last assistant message
|
|
74
|
+
const messagesResponse = await getClient().session.messages({
|
|
75
|
+
path: { id: sessionId },
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (!messagesResponse.data || messagesResponse.data.length === 0) {
|
|
79
|
+
await command.editReply('No messages to undo')
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Find the last assistant message
|
|
84
|
+
const lastAssistantMessage = [...messagesResponse.data]
|
|
85
|
+
.reverse()
|
|
86
|
+
.find((m) => m.info.role === 'assistant')
|
|
87
|
+
|
|
88
|
+
if (!lastAssistantMessage) {
|
|
89
|
+
await command.editReply('No assistant message to undo')
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const response = await getClient().session.revert({
|
|
94
|
+
path: { id: sessionId },
|
|
95
|
+
body: { messageID: lastAssistantMessage.info.id },
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
if (response.error) {
|
|
99
|
+
await command.editReply(
|
|
100
|
+
`Failed to undo: ${JSON.stringify(response.error)}`,
|
|
101
|
+
)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const diffInfo = response.data?.revert?.diff
|
|
106
|
+
? `\n\`\`\`diff\n${response.data.revert.diff.slice(0, 1500)}\n\`\`\``
|
|
107
|
+
: ''
|
|
108
|
+
|
|
109
|
+
await command.editReply(
|
|
110
|
+
`⏪ **Undone** - reverted last assistant message${diffInfo}`,
|
|
111
|
+
)
|
|
112
|
+
logger.log(
|
|
113
|
+
`Session ${sessionId} reverted message ${lastAssistantMessage.info.id}`,
|
|
114
|
+
)
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger.error('[UNDO] Error:', error)
|
|
117
|
+
await command.editReply(
|
|
118
|
+
`Failed to undo: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function handleRedoCommand({
|
|
124
|
+
command,
|
|
125
|
+
}: CommandContext): Promise<void> {
|
|
126
|
+
const channel = command.channel
|
|
127
|
+
|
|
128
|
+
if (!channel) {
|
|
129
|
+
await command.reply({
|
|
130
|
+
content: 'This command can only be used in a channel',
|
|
131
|
+
ephemeral: true,
|
|
132
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
133
|
+
})
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const isThread = [
|
|
138
|
+
ChannelType.PublicThread,
|
|
139
|
+
ChannelType.PrivateThread,
|
|
140
|
+
ChannelType.AnnouncementThread,
|
|
141
|
+
].includes(channel.type)
|
|
142
|
+
|
|
143
|
+
if (!isThread) {
|
|
144
|
+
await command.reply({
|
|
145
|
+
content: 'This command can only be used in a thread with an active session',
|
|
146
|
+
ephemeral: true,
|
|
147
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
148
|
+
})
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const textChannel = await resolveTextChannel(channel as ThreadChannel)
|
|
153
|
+
const { projectDirectory: directory } = getKimakiMetadata(textChannel)
|
|
154
|
+
|
|
155
|
+
if (!directory) {
|
|
156
|
+
await command.reply({
|
|
157
|
+
content: 'Could not determine project directory for this channel',
|
|
158
|
+
ephemeral: true,
|
|
159
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
160
|
+
})
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const row = getDatabase()
|
|
165
|
+
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
166
|
+
.get(channel.id) as { session_id: string } | undefined
|
|
167
|
+
|
|
168
|
+
if (!row?.session_id) {
|
|
169
|
+
await command.reply({
|
|
170
|
+
content: 'No active session in this thread',
|
|
171
|
+
ephemeral: true,
|
|
172
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
173
|
+
})
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const sessionId = row.session_id
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
181
|
+
|
|
182
|
+
const getClient = await initializeOpencodeForDirectory(directory)
|
|
183
|
+
|
|
184
|
+
// Check if session has reverted state
|
|
185
|
+
const sessionResponse = await getClient().session.get({
|
|
186
|
+
path: { id: sessionId },
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
if (!sessionResponse.data?.revert) {
|
|
190
|
+
await command.editReply('Nothing to redo - no previous undo found')
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const response = await getClient().session.unrevert({
|
|
195
|
+
path: { id: sessionId },
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
if (response.error) {
|
|
199
|
+
await command.editReply(
|
|
200
|
+
`Failed to redo: ${JSON.stringify(response.error)}`,
|
|
201
|
+
)
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await command.editReply(`⏩ **Restored** - session back to previous state`)
|
|
206
|
+
logger.log(`Session ${sessionId} unrevert completed`)
|
|
207
|
+
} catch (error) {
|
|
208
|
+
logger.error('[REDO] Error:', error)
|
|
209
|
+
await command.editReply(
|
|
210
|
+
`Failed to redo: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|
package/src/database.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SQLite database manager for persistent bot state.
|
|
2
|
+
// Stores thread-session mappings, bot tokens, channel directories,
|
|
3
|
+
// API keys, and model preferences in ~/.kimaki/discord-sessions.db.
|
|
4
|
+
|
|
1
5
|
import Database from 'better-sqlite3'
|
|
2
6
|
import fs from 'node:fs'
|
|
3
7
|
import os from 'node:os'
|
package/src/discord-bot.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Core Discord bot module that handles message events and bot lifecycle.
|
|
2
|
+
// Bridges Discord messages to OpenCode sessions, manages voice connections,
|
|
3
|
+
// and orchestrates the main event loop for the Kimaki bot.
|
|
4
|
+
|
|
1
5
|
import { getDatabase, closeDatabase } from './database.js'
|
|
2
6
|
import { initializeOpencodeForDirectory, getOpencodeServers } from './opencode.js'
|
|
3
7
|
import {
|
package/src/discord-utils.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Discord-specific utility functions.
|
|
2
|
+
// Handles markdown splitting for Discord's 2000-char limit, code block escaping,
|
|
3
|
+
// thread message sending, and channel metadata extraction from topic tags.
|
|
4
|
+
|
|
1
5
|
import {
|
|
2
6
|
ChannelType,
|
|
3
7
|
type Message,
|
|
@@ -12,6 +16,8 @@ import { createLogger } from './logger.js'
|
|
|
12
16
|
const discordLogger = createLogger('DISCORD')
|
|
13
17
|
|
|
14
18
|
export const SILENT_MESSAGE_FLAGS = 4 | 4096
|
|
19
|
+
// Same as SILENT but without SuppressNotifications - triggers badge/notification
|
|
20
|
+
export const NOTIFY_MESSAGE_FLAGS = 4
|
|
15
21
|
|
|
16
22
|
export function escapeBackticksInCodeBlocks(markdown: string): string {
|
|
17
23
|
const lexer = new Lexer()
|
|
@@ -125,12 +131,18 @@ export function splitMarkdownForDiscord({
|
|
|
125
131
|
export async function sendThreadMessage(
|
|
126
132
|
thread: ThreadChannel,
|
|
127
133
|
content: string,
|
|
134
|
+
options?: { flags?: number }
|
|
128
135
|
): Promise<Message> {
|
|
129
136
|
const MAX_LENGTH = 2000
|
|
130
137
|
|
|
131
138
|
content = formatMarkdownTables(content)
|
|
132
139
|
content = escapeBackticksInCodeBlocks(content)
|
|
133
140
|
|
|
141
|
+
// If custom flags provided, send as single message (no chunking)
|
|
142
|
+
if (options?.flags !== undefined) {
|
|
143
|
+
return thread.send({ content, flags: options.flags })
|
|
144
|
+
}
|
|
145
|
+
|
|
134
146
|
const chunks = splitMarkdownForDiscord({ content, maxLength: MAX_LENGTH })
|
|
135
147
|
|
|
136
148
|
if (chunks.length > 1) {
|
package/src/format-tables.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Markdown table to code block converter.
|
|
2
|
+
// Discord doesn't render GFM tables, so this converts them to
|
|
3
|
+
// space-aligned code blocks for proper monospace display.
|
|
4
|
+
|
|
1
5
|
import { Lexer, type Token, type Tokens } from 'marked'
|
|
2
6
|
|
|
3
7
|
export function formatMarkdownTables(markdown: string): string {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Main thread interface for the GenAI worker.
|
|
2
|
+
// Spawns and manages the worker thread, handling message passing for
|
|
3
|
+
// audio input/output, tool call completions, and graceful shutdown.
|
|
4
|
+
|
|
1
5
|
import { Worker } from 'node:worker_threads'
|
|
2
6
|
import type { WorkerInMessage, WorkerOutMessage } from './worker-types.js'
|
|
3
7
|
import type { Tool as AITool } from 'ai'
|
package/src/genai-worker.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Worker thread for GenAI voice processing.
|
|
2
|
+
// Runs in a separate thread to handle audio encoding/decoding without blocking.
|
|
3
|
+
// Resamples 24kHz GenAI output to 48kHz stereo Opus packets for Discord.
|
|
4
|
+
|
|
1
5
|
import { parentPort, threadId } from 'node:worker_threads'
|
|
2
6
|
import { createWriteStream, type WriteStream } from 'node:fs'
|
|
3
7
|
import { mkdir } from 'node:fs/promises'
|
package/src/genai.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Google GenAI Live session manager for real-time voice interactions.
|
|
2
|
+
// Establishes bidirectional audio streaming with Gemini, handles tool calls,
|
|
3
|
+
// and manages the assistant's audio output for Discord voice channels.
|
|
4
|
+
|
|
1
5
|
import {
|
|
2
6
|
GoogleGenAI,
|
|
3
7
|
LiveServerMessage,
|