kimaki 0.4.35 → 0.4.37
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 +5 -5
- package/dist/cli.js +182 -46
- 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/database.js +7 -0
- package/dist/discord-bot.js +37 -20
- 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 +10 -4
- 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 +18 -29
- package/src/cli.ts +334 -205
- 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 +17 -8
- package/src/discord-bot.ts +60 -58
- 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 +18 -4
- 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/commands/session.ts
CHANGED
|
@@ -13,10 +13,7 @@ import { createLogger } from '../logger.js'
|
|
|
13
13
|
|
|
14
14
|
const logger = createLogger('SESSION')
|
|
15
15
|
|
|
16
|
-
export async function handleSessionCommand({
|
|
17
|
-
command,
|
|
18
|
-
appId,
|
|
19
|
-
}: CommandContext): Promise<void> {
|
|
16
|
+
export async function handleSessionCommand({ command, appId }: CommandContext): Promise<void> {
|
|
20
17
|
await command.deferReply({ ephemeral: false })
|
|
21
18
|
|
|
22
19
|
const prompt = command.options.getString('prompt', true)
|
|
@@ -50,9 +47,7 @@ export async function handleSessionCommand({
|
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
if (!projectDirectory) {
|
|
53
|
-
await command.editReply(
|
|
54
|
-
'This channel is not configured with a project directory',
|
|
55
|
-
)
|
|
50
|
+
await command.editReply('This channel is not configured with a project directory')
|
|
56
51
|
return
|
|
57
52
|
}
|
|
58
53
|
|
|
@@ -102,10 +97,7 @@ export async function handleSessionCommand({
|
|
|
102
97
|
}
|
|
103
98
|
}
|
|
104
99
|
|
|
105
|
-
async function handleAgentAutocomplete({
|
|
106
|
-
interaction,
|
|
107
|
-
appId,
|
|
108
|
-
}: AutocompleteContext): Promise<void> {
|
|
100
|
+
async function handleAgentAutocomplete({ interaction, appId }: AutocompleteContext): Promise<void> {
|
|
109
101
|
const focusedValue = interaction.options.getFocused()
|
|
110
102
|
|
|
111
103
|
let projectDirectory: string | undefined
|
|
@@ -224,8 +216,7 @@ export async function handleSessionAutocomplete({
|
|
|
224
216
|
|
|
225
217
|
const files = response.data || []
|
|
226
218
|
|
|
227
|
-
const prefix =
|
|
228
|
-
previousFiles.length > 0 ? previousFiles.join(', ') + ', ' : ''
|
|
219
|
+
const prefix = previousFiles.length > 0 ? previousFiles.join(', ') + ', ' : ''
|
|
229
220
|
|
|
230
221
|
const choices = files
|
|
231
222
|
.map((file: string) => {
|
package/src/commands/share.ts
CHANGED
|
@@ -9,9 +9,7 @@ import { createLogger } from '../logger.js'
|
|
|
9
9
|
|
|
10
10
|
const logger = createLogger('SHARE')
|
|
11
11
|
|
|
12
|
-
export async function handleShareCommand({
|
|
13
|
-
command,
|
|
14
|
-
}: CommandContext): Promise<void> {
|
|
12
|
+
export async function handleShareCommand({ command }: CommandContext): Promise<void> {
|
|
15
13
|
const channel = command.channel
|
|
16
14
|
|
|
17
15
|
if (!channel) {
|
package/src/commands/types.ts
CHANGED
|
@@ -20,6 +20,4 @@ export type AutocompleteContext = {
|
|
|
20
20
|
|
|
21
21
|
export type AutocompleteHandler = (ctx: AutocompleteContext) => Promise<void>
|
|
22
22
|
|
|
23
|
-
export type SelectMenuHandler = (
|
|
24
|
-
interaction: StringSelectMenuInteraction,
|
|
25
|
-
) => Promise<void>
|
|
23
|
+
export type SelectMenuHandler = (interaction: StringSelectMenuInteraction) => Promise<void>
|
|
@@ -9,9 +9,7 @@ import { createLogger } from '../logger.js'
|
|
|
9
9
|
|
|
10
10
|
const logger = createLogger('UNDO-REDO')
|
|
11
11
|
|
|
12
|
-
export async function handleUndoCommand({
|
|
13
|
-
command,
|
|
14
|
-
}: CommandContext): Promise<void> {
|
|
12
|
+
export async function handleUndoCommand({ command }: CommandContext): Promise<void> {
|
|
15
13
|
const channel = command.channel
|
|
16
14
|
|
|
17
15
|
if (!channel) {
|
|
@@ -96,9 +94,7 @@ export async function handleUndoCommand({
|
|
|
96
94
|
})
|
|
97
95
|
|
|
98
96
|
if (response.error) {
|
|
99
|
-
await command.editReply(
|
|
100
|
-
`Failed to undo: ${JSON.stringify(response.error)}`,
|
|
101
|
-
)
|
|
97
|
+
await command.editReply(`Failed to undo: ${JSON.stringify(response.error)}`)
|
|
102
98
|
return
|
|
103
99
|
}
|
|
104
100
|
|
|
@@ -106,12 +102,8 @@ export async function handleUndoCommand({
|
|
|
106
102
|
? `\n\`\`\`diff\n${response.data.revert.diff.slice(0, 1500)}\n\`\`\``
|
|
107
103
|
: ''
|
|
108
104
|
|
|
109
|
-
await command.editReply(
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
logger.log(
|
|
113
|
-
`Session ${sessionId} reverted message ${lastAssistantMessage.info.id}`,
|
|
114
|
-
)
|
|
105
|
+
await command.editReply(`⏪ **Undone** - reverted last assistant message${diffInfo}`)
|
|
106
|
+
logger.log(`Session ${sessionId} reverted message ${lastAssistantMessage.info.id}`)
|
|
115
107
|
} catch (error) {
|
|
116
108
|
logger.error('[UNDO] Error:', error)
|
|
117
109
|
await command.editReply(
|
|
@@ -120,9 +112,7 @@ export async function handleUndoCommand({
|
|
|
120
112
|
}
|
|
121
113
|
}
|
|
122
114
|
|
|
123
|
-
export async function handleRedoCommand({
|
|
124
|
-
command,
|
|
125
|
-
}: CommandContext): Promise<void> {
|
|
115
|
+
export async function handleRedoCommand({ command }: CommandContext): Promise<void> {
|
|
126
116
|
const channel = command.channel
|
|
127
117
|
|
|
128
118
|
if (!channel) {
|
|
@@ -196,9 +186,7 @@ export async function handleRedoCommand({
|
|
|
196
186
|
})
|
|
197
187
|
|
|
198
188
|
if (response.error) {
|
|
199
|
-
await command.editReply(
|
|
200
|
-
`Failed to redo: ${JSON.stringify(response.error)}`,
|
|
201
|
-
)
|
|
189
|
+
await command.editReply(`Failed to redo: ${JSON.stringify(response.error)}`)
|
|
202
190
|
return
|
|
203
191
|
}
|
|
204
192
|
|
|
@@ -12,10 +12,7 @@ import fs from 'node:fs'
|
|
|
12
12
|
|
|
13
13
|
const userCommandLogger = createLogger('USER_CMD')
|
|
14
14
|
|
|
15
|
-
export const handleUserCommand: CommandHandler = async ({
|
|
16
|
-
command,
|
|
17
|
-
appId,
|
|
18
|
-
}: CommandContext) => {
|
|
15
|
+
export const handleUserCommand: CommandHandler = async ({ command, appId }: CommandContext) => {
|
|
19
16
|
const discordCommandName = command.commandName
|
|
20
17
|
// Strip the -cmd suffix to get the actual OpenCode command name
|
|
21
18
|
const commandName = discordCommandName.replace(/-cmd$/, '')
|
|
@@ -31,11 +28,11 @@ export const handleUserCommand: CommandHandler = async ({
|
|
|
31
28
|
`Channel info: type=${channel?.type}, id=${channel?.id}, isNull=${channel === null}`,
|
|
32
29
|
)
|
|
33
30
|
|
|
34
|
-
const isThread =
|
|
35
|
-
|
|
36
|
-
ChannelType.PrivateThread,
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
const isThread =
|
|
32
|
+
channel &&
|
|
33
|
+
[ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.AnnouncementThread].includes(
|
|
34
|
+
channel.type,
|
|
35
|
+
)
|
|
39
36
|
|
|
40
37
|
const isTextChannel = channel?.type === ChannelType.GuildText
|
|
41
38
|
|
|
@@ -64,7 +61,8 @@ export const handleUserCommand: CommandHandler = async ({
|
|
|
64
61
|
|
|
65
62
|
if (!row) {
|
|
66
63
|
await command.reply({
|
|
67
|
-
content:
|
|
64
|
+
content:
|
|
65
|
+
'This thread does not have an active session. Use this command in a project channel to create a new thread.',
|
|
68
66
|
ephemeral: true,
|
|
69
67
|
})
|
|
70
68
|
return
|
package/src/config.ts
CHANGED
|
@@ -28,11 +28,11 @@ export function getDataDir(): string {
|
|
|
28
28
|
*/
|
|
29
29
|
export function setDataDir(dir: string): void {
|
|
30
30
|
const resolvedDir = path.resolve(dir)
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
if (!fs.existsSync(resolvedDir)) {
|
|
33
33
|
fs.mkdirSync(resolvedDir, { recursive: true })
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
dataDir = resolvedDir
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -53,17 +53,17 @@ const DEFAULT_LOCK_PORT = 29988
|
|
|
53
53
|
*/
|
|
54
54
|
export function getLockPort(): number {
|
|
55
55
|
const dir = getDataDir()
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
// Use original port for default data dir (backwards compatible)
|
|
58
58
|
if (dir === DEFAULT_DATA_DIR) {
|
|
59
59
|
return DEFAULT_LOCK_PORT
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
// Hash-based port for custom data dirs
|
|
63
63
|
let hash = 0
|
|
64
64
|
for (let i = 0; i < dir.length; i++) {
|
|
65
65
|
const char = dir.charCodeAt(i)
|
|
66
|
-
hash = (
|
|
66
|
+
hash = (hash << 5) - hash + char
|
|
67
67
|
hash = hash & hash // Convert to 32bit integer
|
|
68
68
|
}
|
|
69
69
|
// Map to port range 30000-39999
|
package/src/database.ts
CHANGED
|
@@ -61,6 +61,13 @@ 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
|
+
|
|
64
71
|
db.exec(`
|
|
65
72
|
CREATE TABLE IF NOT EXISTS bot_api_keys (
|
|
66
73
|
app_id TEXT PRIMARY KEY,
|
|
@@ -141,7 +148,7 @@ export function setChannelModel(channelId: string, modelId: string): void {
|
|
|
141
148
|
db.prepare(
|
|
142
149
|
`INSERT INTO channel_models (channel_id, model_id, updated_at)
|
|
143
150
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
144
|
-
ON CONFLICT(channel_id) DO UPDATE SET model_id = ?, updated_at = CURRENT_TIMESTAMP
|
|
151
|
+
ON CONFLICT(channel_id) DO UPDATE SET model_id = ?, updated_at = CURRENT_TIMESTAMP`,
|
|
145
152
|
).run(channelId, modelId, modelId)
|
|
146
153
|
}
|
|
147
154
|
|
|
@@ -163,9 +170,10 @@ export function getSessionModel(sessionId: string): string | undefined {
|
|
|
163
170
|
*/
|
|
164
171
|
export function setSessionModel(sessionId: string, modelId: string): void {
|
|
165
172
|
const db = getDatabase()
|
|
166
|
-
db.prepare(
|
|
167
|
-
|
|
168
|
-
|
|
173
|
+
db.prepare(`INSERT OR REPLACE INTO session_models (session_id, model_id) VALUES (?, ?)`).run(
|
|
174
|
+
sessionId,
|
|
175
|
+
modelId,
|
|
176
|
+
)
|
|
169
177
|
}
|
|
170
178
|
|
|
171
179
|
/**
|
|
@@ -187,7 +195,7 @@ export function setChannelAgent(channelId: string, agentName: string): void {
|
|
|
187
195
|
db.prepare(
|
|
188
196
|
`INSERT INTO channel_agents (channel_id, agent_name, updated_at)
|
|
189
197
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
190
|
-
ON CONFLICT(channel_id) DO UPDATE SET agent_name = ?, updated_at = CURRENT_TIMESTAMP
|
|
198
|
+
ON CONFLICT(channel_id) DO UPDATE SET agent_name = ?, updated_at = CURRENT_TIMESTAMP`,
|
|
191
199
|
).run(channelId, agentName, agentName)
|
|
192
200
|
}
|
|
193
201
|
|
|
@@ -207,9 +215,10 @@ export function getSessionAgent(sessionId: string): string | undefined {
|
|
|
207
215
|
*/
|
|
208
216
|
export function setSessionAgent(sessionId: string, agentName: string): void {
|
|
209
217
|
const db = getDatabase()
|
|
210
|
-
db.prepare(
|
|
211
|
-
|
|
212
|
-
|
|
218
|
+
db.prepare(`INSERT OR REPLACE INTO session_agents (session_id, agent_name) VALUES (?, ?)`).run(
|
|
219
|
+
sessionId,
|
|
220
|
+
agentName,
|
|
221
|
+
)
|
|
213
222
|
}
|
|
214
223
|
|
|
215
224
|
export function closeDatabase(): void {
|
package/src/discord-bot.ts
CHANGED
|
@@ -24,10 +24,7 @@ import {
|
|
|
24
24
|
processVoiceAttachment,
|
|
25
25
|
registerVoiceStateHandler,
|
|
26
26
|
} from './voice-handler.js'
|
|
27
|
-
import {
|
|
28
|
-
getCompactSessionContext,
|
|
29
|
-
getLastSessionId,
|
|
30
|
-
} from './markdown.js'
|
|
27
|
+
import { getCompactSessionContext, getLastSessionId } from './markdown.js'
|
|
31
28
|
import { handleOpencodeSession } from './session-handler.js'
|
|
32
29
|
import { registerInteractionHandler } from './interaction-handler.js'
|
|
33
30
|
|
|
@@ -35,7 +32,12 @@ export { getDatabase, closeDatabase } from './database.js'
|
|
|
35
32
|
export { initializeOpencodeForDirectory } from './opencode.js'
|
|
36
33
|
export { escapeBackticksInCodeBlocks, splitMarkdownForDiscord } from './discord-utils.js'
|
|
37
34
|
export { getOpencodeSystemMessage } from './system-message.js'
|
|
38
|
-
export {
|
|
35
|
+
export {
|
|
36
|
+
ensureKimakiCategory,
|
|
37
|
+
ensureKimakiAudioCategory,
|
|
38
|
+
createProjectChannels,
|
|
39
|
+
getChannelsWithDescriptions,
|
|
40
|
+
} from './channel-management.js'
|
|
39
41
|
export type { ChannelWithTags } from './channel-management.js'
|
|
40
42
|
|
|
41
43
|
import {
|
|
@@ -73,12 +75,7 @@ export async function createDiscordClient() {
|
|
|
73
75
|
GatewayIntentBits.MessageContent,
|
|
74
76
|
GatewayIntentBits.GuildVoiceStates,
|
|
75
77
|
],
|
|
76
|
-
partials: [
|
|
77
|
-
Partials.Channel,
|
|
78
|
-
Partials.Message,
|
|
79
|
-
Partials.User,
|
|
80
|
-
Partials.ThreadMember,
|
|
81
|
-
],
|
|
78
|
+
partials: [Partials.Channel, Partials.Message, Partials.User, Partials.ThreadMember],
|
|
82
79
|
})
|
|
83
80
|
}
|
|
84
81
|
|
|
@@ -116,15 +113,11 @@ export async function startDiscordBot({
|
|
|
116
113
|
|
|
117
114
|
const channels = await getChannelsWithDescriptions(guild)
|
|
118
115
|
const kimakiChannels = channels.filter(
|
|
119
|
-
(ch) =>
|
|
120
|
-
ch.kimakiDirectory &&
|
|
121
|
-
(!ch.kimakiApp || ch.kimakiApp === currentAppId),
|
|
116
|
+
(ch) => ch.kimakiDirectory && (!ch.kimakiApp || ch.kimakiApp === currentAppId),
|
|
122
117
|
)
|
|
123
118
|
|
|
124
119
|
if (kimakiChannels.length > 0) {
|
|
125
|
-
discordLogger.log(
|
|
126
|
-
` Found ${kimakiChannels.length} channel(s) for this bot:`,
|
|
127
|
-
)
|
|
120
|
+
discordLogger.log(` Found ${kimakiChannels.length} channel(s) for this bot:`)
|
|
128
121
|
for (const channel of kimakiChannels) {
|
|
129
122
|
discordLogger.log(` - #${channel.name}: ${channel.kimakiDirectory}`)
|
|
130
123
|
}
|
|
@@ -159,19 +152,14 @@ export async function startDiscordBot({
|
|
|
159
152
|
try {
|
|
160
153
|
await message.fetch()
|
|
161
154
|
} catch (error) {
|
|
162
|
-
discordLogger.log(
|
|
163
|
-
`Failed to fetch partial message ${message.id}:`,
|
|
164
|
-
error,
|
|
165
|
-
)
|
|
155
|
+
discordLogger.log(`Failed to fetch partial message ${message.id}:`, error)
|
|
166
156
|
return
|
|
167
157
|
}
|
|
168
158
|
}
|
|
169
159
|
|
|
170
160
|
if (message.guild && message.member) {
|
|
171
161
|
const isOwner = message.member.id === message.guild.ownerId
|
|
172
|
-
const isAdmin = message.member.permissions.has(
|
|
173
|
-
PermissionsBitField.Flags.Administrator,
|
|
174
|
-
)
|
|
162
|
+
const isAdmin = message.member.permissions.has(PermissionsBitField.Flags.Administrator)
|
|
175
163
|
const canManageServer = message.member.permissions.has(
|
|
176
164
|
PermissionsBitField.Flags.ManageGuild,
|
|
177
165
|
)
|
|
@@ -199,19 +187,6 @@ export async function startDiscordBot({
|
|
|
199
187
|
const thread = channel as ThreadChannel
|
|
200
188
|
discordLogger.log(`Message in thread ${thread.name} (${thread.id})`)
|
|
201
189
|
|
|
202
|
-
const row = getDatabase()
|
|
203
|
-
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
204
|
-
.get(thread.id) as { session_id: string } | undefined
|
|
205
|
-
|
|
206
|
-
if (!row) {
|
|
207
|
-
discordLogger.log(`No session found for thread ${thread.id}`)
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
voiceLogger.log(
|
|
212
|
-
`[SESSION] Found session ${row.session_id} for thread ${thread.id}`,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
190
|
const parent = thread.parent as TextChannel | null
|
|
216
191
|
let projectDirectory: string | undefined
|
|
217
192
|
let channelAppId: string | undefined
|
|
@@ -242,6 +217,43 @@ export async function startDiscordBot({
|
|
|
242
217
|
return
|
|
243
218
|
}
|
|
244
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 (notification) as context for the session
|
|
234
|
+
let prompt = message.content
|
|
235
|
+
const starterMessage = await thread.fetchStarterMessage().catch(() => null)
|
|
236
|
+
if (starterMessage?.content) {
|
|
237
|
+
// Strip notification prefix if present
|
|
238
|
+
const notificationContent = starterMessage.content
|
|
239
|
+
.replace(/^📢 \*\*Notification\*\*\n?/, '')
|
|
240
|
+
.trim()
|
|
241
|
+
if (notificationContent) {
|
|
242
|
+
prompt = `Context from notification:\n${notificationContent}\n\nUser request:\n${message.content}`
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await handleOpencodeSession({
|
|
247
|
+
prompt,
|
|
248
|
+
thread,
|
|
249
|
+
projectDirectory,
|
|
250
|
+
channelId: parent?.id || '',
|
|
251
|
+
})
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
voiceLogger.log(`[SESSION] Found session ${row.session_id} for thread ${thread.id}`)
|
|
256
|
+
|
|
245
257
|
let messageContent = message.content || ''
|
|
246
258
|
|
|
247
259
|
let currentSessionContext: string | undefined
|
|
@@ -315,9 +327,7 @@ export async function startDiscordBot({
|
|
|
315
327
|
)
|
|
316
328
|
|
|
317
329
|
if (!textChannel.topic) {
|
|
318
|
-
voiceLogger.log(
|
|
319
|
-
`[IGNORED] Channel #${textChannel.name} has no description`,
|
|
320
|
-
)
|
|
330
|
+
voiceLogger.log(`[IGNORED] Channel #${textChannel.name} has no description`)
|
|
321
331
|
return
|
|
322
332
|
}
|
|
323
333
|
|
|
@@ -330,9 +340,7 @@ export async function startDiscordBot({
|
|
|
330
340
|
const channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
331
341
|
|
|
332
342
|
if (!projectDirectory) {
|
|
333
|
-
voiceLogger.log(
|
|
334
|
-
`[IGNORED] Channel #${textChannel.name} has no kimaki.directory tag`,
|
|
335
|
-
)
|
|
343
|
+
voiceLogger.log(`[IGNORED] Channel #${textChannel.name} has no kimaki.directory tag`)
|
|
336
344
|
return
|
|
337
345
|
}
|
|
338
346
|
|
|
@@ -343,9 +351,7 @@ export async function startDiscordBot({
|
|
|
343
351
|
return
|
|
344
352
|
}
|
|
345
353
|
|
|
346
|
-
discordLogger.log(
|
|
347
|
-
`DIRECTORY: Found kimaki.directory: ${projectDirectory}`,
|
|
348
|
-
)
|
|
354
|
+
discordLogger.log(`DIRECTORY: Found kimaki.directory: ${projectDirectory}`)
|
|
349
355
|
if (channelAppId) {
|
|
350
356
|
discordLogger.log(`APP: Channel app ID: ${channelAppId}`)
|
|
351
357
|
}
|
|
@@ -359,9 +365,7 @@ export async function startDiscordBot({
|
|
|
359
365
|
return
|
|
360
366
|
}
|
|
361
367
|
|
|
362
|
-
const hasVoice = message.attachments.some((a) =>
|
|
363
|
-
a.contentType?.startsWith('audio/'),
|
|
364
|
-
)
|
|
368
|
+
const hasVoice = message.attachments.some((a) => a.contentType?.startsWith('audio/'))
|
|
365
369
|
|
|
366
370
|
const threadName = hasVoice
|
|
367
371
|
? 'Voice Message'
|
|
@@ -415,10 +419,10 @@ export async function startDiscordBot({
|
|
|
415
419
|
}
|
|
416
420
|
})
|
|
417
421
|
|
|
418
|
-
// Magic prefix used by `kimaki
|
|
422
|
+
// Magic prefix used by `kimaki send` CLI command to initiate sessions
|
|
419
423
|
const BOT_SESSION_PREFIX = '🤖 **Bot-initiated session**'
|
|
420
424
|
|
|
421
|
-
// Handle bot-initiated threads created by `kimaki
|
|
425
|
+
// Handle bot-initiated threads created by `kimaki send`
|
|
422
426
|
discordClient.on(Events.ThreadCreate, async (thread, newlyCreated) => {
|
|
423
427
|
try {
|
|
424
428
|
if (!newlyCreated) {
|
|
@@ -489,7 +493,9 @@ export async function startDiscordBot({
|
|
|
489
493
|
return
|
|
490
494
|
}
|
|
491
495
|
|
|
492
|
-
discordLogger.log(
|
|
496
|
+
discordLogger.log(
|
|
497
|
+
`[BOT_SESSION] Starting session for thread ${thread.id} with prompt: "${prompt.slice(0, 50)}..."`,
|
|
498
|
+
)
|
|
493
499
|
|
|
494
500
|
await handleOpencodeSession({
|
|
495
501
|
prompt,
|
|
@@ -522,9 +528,7 @@ export async function startDiscordBot({
|
|
|
522
528
|
try {
|
|
523
529
|
const cleanupPromises: Promise<void>[] = []
|
|
524
530
|
for (const [guildId] of voiceConnections) {
|
|
525
|
-
voiceLogger.log(
|
|
526
|
-
`[SHUTDOWN] Cleaning up voice connection for guild ${guildId}`,
|
|
527
|
-
)
|
|
531
|
+
voiceLogger.log(`[SHUTDOWN] Cleaning up voice connection for guild ${guildId}`)
|
|
528
532
|
cleanupPromises.push(cleanupVoiceConnection(guildId))
|
|
529
533
|
}
|
|
530
534
|
|
|
@@ -538,9 +542,7 @@ export async function startDiscordBot({
|
|
|
538
542
|
|
|
539
543
|
for (const [dir, server] of getOpencodeServers()) {
|
|
540
544
|
if (!server.process.killed) {
|
|
541
|
-
voiceLogger.log(
|
|
542
|
-
`[SHUTDOWN] Stopping OpenCode server on port ${server.port} for ${dir}`,
|
|
543
|
-
)
|
|
545
|
+
voiceLogger.log(`[SHUTDOWN] Stopping OpenCode server on port ${server.port} for ${dir}`)
|
|
544
546
|
server.process.kill('SIGTERM')
|
|
545
547
|
}
|
|
546
548
|
}
|
package/src/discord-utils.ts
CHANGED
|
@@ -2,12 +2,7 @@
|
|
|
2
2
|
// Handles markdown splitting for Discord's 2000-char limit, code block escaping,
|
|
3
3
|
// thread message sending, and channel metadata extraction from topic tags.
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
ChannelType,
|
|
7
|
-
type Message,
|
|
8
|
-
type TextChannel,
|
|
9
|
-
type ThreadChannel,
|
|
10
|
-
} from 'discord.js'
|
|
5
|
+
import { ChannelType, type Message, type TextChannel, type ThreadChannel } from 'discord.js'
|
|
11
6
|
import { Lexer } from 'marked'
|
|
12
7
|
import { extractTagsArrays } from './xml.js'
|
|
13
8
|
import { formatMarkdownTables } from './format-tables.js'
|
|
@@ -65,19 +60,43 @@ export function splitMarkdownForDiscord({
|
|
|
65
60
|
for (const token of tokens) {
|
|
66
61
|
if (token.type === 'code') {
|
|
67
62
|
const lang = token.lang || ''
|
|
68
|
-
lines.push({
|
|
63
|
+
lines.push({
|
|
64
|
+
text: '```' + lang + '\n',
|
|
65
|
+
inCodeBlock: false,
|
|
66
|
+
lang,
|
|
67
|
+
isOpeningFence: true,
|
|
68
|
+
isClosingFence: false,
|
|
69
|
+
})
|
|
69
70
|
const codeLines = token.text.split('\n')
|
|
70
71
|
for (const codeLine of codeLines) {
|
|
71
|
-
lines.push({
|
|
72
|
+
lines.push({
|
|
73
|
+
text: codeLine + '\n',
|
|
74
|
+
inCodeBlock: true,
|
|
75
|
+
lang,
|
|
76
|
+
isOpeningFence: false,
|
|
77
|
+
isClosingFence: false,
|
|
78
|
+
})
|
|
72
79
|
}
|
|
73
|
-
lines.push({
|
|
80
|
+
lines.push({
|
|
81
|
+
text: '```\n',
|
|
82
|
+
inCodeBlock: false,
|
|
83
|
+
lang: '',
|
|
84
|
+
isOpeningFence: false,
|
|
85
|
+
isClosingFence: true,
|
|
86
|
+
})
|
|
74
87
|
} else {
|
|
75
88
|
const rawLines = token.raw.split('\n')
|
|
76
89
|
for (let i = 0; i < rawLines.length; i++) {
|
|
77
90
|
const isLast = i === rawLines.length - 1
|
|
78
91
|
const text = isLast ? rawLines[i]! : rawLines[i]! + '\n'
|
|
79
92
|
if (text) {
|
|
80
|
-
lines.push({
|
|
93
|
+
lines.push({
|
|
94
|
+
text,
|
|
95
|
+
inCodeBlock: false,
|
|
96
|
+
lang: '',
|
|
97
|
+
isOpeningFence: false,
|
|
98
|
+
isClosingFence: false,
|
|
99
|
+
})
|
|
81
100
|
}
|
|
82
101
|
}
|
|
83
102
|
}
|
|
@@ -126,7 +145,9 @@ export function splitMarkdownForDiscord({
|
|
|
126
145
|
}
|
|
127
146
|
|
|
128
147
|
// calculate overhead for code block markers
|
|
129
|
-
const codeBlockOverhead = line.inCodeBlock
|
|
148
|
+
const codeBlockOverhead = line.inCodeBlock
|
|
149
|
+
? ('```' + line.lang + '\n').length + '```\n'.length
|
|
150
|
+
: 0
|
|
130
151
|
// ensure at least 10 chars available, even if maxLength is very small
|
|
131
152
|
const availablePerChunk = Math.max(10, maxLength - codeBlockOverhead - 50)
|
|
132
153
|
|
|
@@ -196,7 +217,7 @@ export function splitMarkdownForDiscord({
|
|
|
196
217
|
export async function sendThreadMessage(
|
|
197
218
|
thread: ThreadChannel,
|
|
198
219
|
content: string,
|
|
199
|
-
options?: { flags?: number }
|
|
220
|
+
options?: { flags?: number },
|
|
200
221
|
): Promise<Message> {
|
|
201
222
|
const MAX_LENGTH = 2000
|
|
202
223
|
|
|
@@ -213,9 +234,7 @@ export async function sendThreadMessage(
|
|
|
213
234
|
const chunks = splitMarkdownForDiscord({ content, maxLength: MAX_LENGTH })
|
|
214
235
|
|
|
215
236
|
if (chunks.length > 1) {
|
|
216
|
-
discordLogger.log(
|
|
217
|
-
`MESSAGE: Splitting ${content.length} chars into ${chunks.length} messages`,
|
|
218
|
-
)
|
|
237
|
+
discordLogger.log(`MESSAGE: Splitting ${content.length} chars into ${chunks.length} messages`)
|
|
219
238
|
}
|
|
220
239
|
|
|
221
240
|
let firstMessage: Message | undefined
|
|
@@ -262,9 +281,7 @@ export async function resolveTextChannel(
|
|
|
262
281
|
}
|
|
263
282
|
|
|
264
283
|
export function escapeDiscordFormatting(text: string): string {
|
|
265
|
-
return text
|
|
266
|
-
.replace(/```/g, '\\`\\`\\`')
|
|
267
|
-
.replace(/````/g, '\\`\\`\\`\\`')
|
|
284
|
+
return text.replace(/```/g, '\\`\\`\\`').replace(/````/g, '\\`\\`\\`\\`')
|
|
268
285
|
}
|
|
269
286
|
|
|
270
287
|
export function getKimakiMetadata(textChannel: TextChannel | null): {
|
|
@@ -2,8 +2,6 @@ import { test, expect } from 'vitest'
|
|
|
2
2
|
import { Lexer } from 'marked'
|
|
3
3
|
import { escapeBackticksInCodeBlocks, splitMarkdownForDiscord } from './discord-utils.js'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
5
|
test('escapes single backticks in code blocks', () => {
|
|
8
6
|
const input = '```js\nconst x = `hello`\n```'
|
|
9
7
|
const result = escapeBackticksInCodeBlocks(input)
|
package/src/format-tables.ts
CHANGED
|
@@ -78,10 +78,7 @@ function extractTokenText(token: Token): string {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function calculateColumnWidths(
|
|
82
|
-
headers: string[],
|
|
83
|
-
rows: string[][],
|
|
84
|
-
): number[] {
|
|
81
|
+
function calculateColumnWidths(headers: string[], rows: string[][]): number[] {
|
|
85
82
|
const widths = headers.map((h) => {
|
|
86
83
|
return h.length
|
|
87
84
|
})
|
|
@@ -41,13 +41,9 @@ export interface GenAIWorker {
|
|
|
41
41
|
stop(): Promise<void>
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export function createGenAIWorker(
|
|
45
|
-
options: GenAIWorkerOptions,
|
|
46
|
-
): Promise<GenAIWorker> {
|
|
44
|
+
export function createGenAIWorker(options: GenAIWorkerOptions): Promise<GenAIWorker> {
|
|
47
45
|
return new Promise((resolve, reject) => {
|
|
48
|
-
const worker = new Worker(
|
|
49
|
-
new URL('../dist/genai-worker.js', import.meta.url),
|
|
50
|
-
)
|
|
46
|
+
const worker = new Worker(new URL('../dist/genai-worker.js', import.meta.url))
|
|
51
47
|
|
|
52
48
|
// Handle messages from worker
|
|
53
49
|
worker.on('message', (message: WorkerOutMessage) => {
|
|
@@ -106,9 +102,7 @@ export function createGenAIWorker(
|
|
|
106
102
|
worker.once('exit', (code) => {
|
|
107
103
|
if (!resolved) {
|
|
108
104
|
resolved = true
|
|
109
|
-
genaiWrapperLogger.log(
|
|
110
|
-
`[GENAI WORKER WRAPPER] Worker exited with code ${code}`,
|
|
111
|
-
)
|
|
105
|
+
genaiWrapperLogger.log(`[GENAI WORKER WRAPPER] Worker exited with code ${code}`)
|
|
112
106
|
resolve()
|
|
113
107
|
}
|
|
114
108
|
})
|