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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/bin.js +6 -1
  3. package/dist/ai-tool-to-genai.js +3 -0
  4. package/dist/channel-management.js +3 -0
  5. package/dist/cli.js +93 -14
  6. package/dist/commands/abort.js +78 -0
  7. package/dist/commands/add-project.js +97 -0
  8. package/dist/commands/create-new-project.js +78 -0
  9. package/dist/commands/fork.js +186 -0
  10. package/dist/commands/model.js +294 -0
  11. package/dist/commands/permissions.js +126 -0
  12. package/dist/commands/queue.js +129 -0
  13. package/dist/commands/resume.js +145 -0
  14. package/dist/commands/session.js +144 -0
  15. package/dist/commands/share.js +80 -0
  16. package/dist/commands/types.js +2 -0
  17. package/dist/commands/undo-redo.js +161 -0
  18. package/dist/database.js +3 -0
  19. package/dist/discord-bot.js +3 -0
  20. package/dist/discord-utils.js +10 -1
  21. package/dist/format-tables.js +3 -0
  22. package/dist/genai-worker-wrapper.js +3 -0
  23. package/dist/genai-worker.js +3 -0
  24. package/dist/genai.js +3 -0
  25. package/dist/interaction-handler.js +71 -697
  26. package/dist/logger.js +3 -0
  27. package/dist/markdown.js +3 -0
  28. package/dist/message-formatting.js +41 -6
  29. package/dist/opencode.js +3 -0
  30. package/dist/session-handler.js +47 -3
  31. package/dist/system-message.js +16 -0
  32. package/dist/tools.js +3 -0
  33. package/dist/utils.js +3 -0
  34. package/dist/voice-handler.js +3 -0
  35. package/dist/voice.js +3 -0
  36. package/dist/worker-types.js +3 -0
  37. package/dist/xml.js +3 -0
  38. package/package.json +11 -12
  39. package/src/ai-tool-to-genai.ts +4 -0
  40. package/src/channel-management.ts +4 -0
  41. package/src/cli.ts +93 -14
  42. package/src/commands/abort.ts +94 -0
  43. package/src/commands/add-project.ts +138 -0
  44. package/src/commands/create-new-project.ts +111 -0
  45. package/src/{fork.ts → commands/fork.ts} +39 -5
  46. package/src/{model-command.ts → commands/model.ts} +7 -5
  47. package/src/commands/permissions.ts +146 -0
  48. package/src/commands/queue.ts +181 -0
  49. package/src/commands/resume.ts +230 -0
  50. package/src/commands/session.ts +186 -0
  51. package/src/commands/share.ts +96 -0
  52. package/src/commands/types.ts +25 -0
  53. package/src/commands/undo-redo.ts +213 -0
  54. package/src/database.ts +4 -0
  55. package/src/discord-bot.ts +4 -0
  56. package/src/discord-utils.ts +12 -0
  57. package/src/format-tables.ts +4 -0
  58. package/src/genai-worker-wrapper.ts +4 -0
  59. package/src/genai-worker.ts +4 -0
  60. package/src/genai.ts +4 -0
  61. package/src/interaction-handler.ts +81 -919
  62. package/src/logger.ts +4 -0
  63. package/src/markdown.ts +4 -0
  64. package/src/message-formatting.ts +52 -7
  65. package/src/opencode.ts +4 -0
  66. package/src/session-handler.ts +70 -3
  67. package/src/system-message.ts +17 -0
  68. package/src/tools.ts +4 -0
  69. package/src/utils.ts +4 -0
  70. package/src/voice-handler.ts +4 -0
  71. package/src/voice.ts +4 -0
  72. package/src/worker-types.ts +4 -0
  73. package/src/xml.ts +4 -0
  74. 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'
@@ -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 {
@@ -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) {
@@ -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'
@@ -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,