kimaki 0.4.34 → 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 +142 -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 +56 -1
- 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 +2 -3
- package/dist/session-handler.js +42 -25
- package/dist/system-message.js +5 -3
- package/dist/tools.js +9 -22
- package/dist/unnest-code-blocks.js +4 -2
- package/dist/unnest-code-blocks.test.js +40 -15
- 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 +290 -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 +68 -9
- 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 +26 -37
- package/src/session-handler.ts +111 -75
- package/src/system-message.ts +13 -3
- package/src/tools.ts +13 -39
- package/src/unnest-code-blocks.test.ts +42 -15
- package/src/unnest-code-blocks.ts +4 -2
- 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,14 +13,12 @@ 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)
|
|
23
20
|
const filesString = command.options.getString('files') || ''
|
|
21
|
+
const agent = command.options.getString('agent') || undefined
|
|
24
22
|
const channel = command.channel
|
|
25
23
|
|
|
26
24
|
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
@@ -49,9 +47,7 @@ export async function handleSessionCommand({
|
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
if (!projectDirectory) {
|
|
52
|
-
await command.editReply(
|
|
53
|
-
'This channel is not configured with a project directory',
|
|
54
|
-
)
|
|
50
|
+
await command.editReply('This channel is not configured with a project directory')
|
|
55
51
|
return
|
|
56
52
|
}
|
|
57
53
|
|
|
@@ -91,6 +87,7 @@ export async function handleSessionCommand({
|
|
|
91
87
|
thread,
|
|
92
88
|
projectDirectory,
|
|
93
89
|
channelId: textChannel.id,
|
|
90
|
+
agent,
|
|
94
91
|
})
|
|
95
92
|
} catch (error) {
|
|
96
93
|
logger.error('[SESSION] Error:', error)
|
|
@@ -100,12 +97,75 @@ export async function handleSessionCommand({
|
|
|
100
97
|
}
|
|
101
98
|
}
|
|
102
99
|
|
|
100
|
+
async function handleAgentAutocomplete({ interaction, appId }: AutocompleteContext): Promise<void> {
|
|
101
|
+
const focusedValue = interaction.options.getFocused()
|
|
102
|
+
|
|
103
|
+
let projectDirectory: string | undefined
|
|
104
|
+
|
|
105
|
+
if (interaction.channel) {
|
|
106
|
+
const channel = interaction.channel
|
|
107
|
+
if (channel.type === ChannelType.GuildText) {
|
|
108
|
+
const textChannel = channel as TextChannel
|
|
109
|
+
if (textChannel.topic) {
|
|
110
|
+
const extracted = extractTagsArrays({
|
|
111
|
+
xml: textChannel.topic,
|
|
112
|
+
tags: ['kimaki.directory', 'kimaki.app'],
|
|
113
|
+
})
|
|
114
|
+
const channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
115
|
+
if (channelAppId && channelAppId !== appId) {
|
|
116
|
+
await interaction.respond([])
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!projectDirectory) {
|
|
125
|
+
await interaction.respond([])
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
131
|
+
|
|
132
|
+
const agentsResponse = await getClient().app.agents({
|
|
133
|
+
query: { directory: projectDirectory },
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
if (!agentsResponse.data || agentsResponse.data.length === 0) {
|
|
137
|
+
await interaction.respond([])
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const agents = agentsResponse.data
|
|
142
|
+
.filter((a) => a.mode === 'primary' || a.mode === 'all')
|
|
143
|
+
.filter((a) => a.name.toLowerCase().includes(focusedValue.toLowerCase()))
|
|
144
|
+
.slice(0, 25)
|
|
145
|
+
|
|
146
|
+
const choices = agents.map((agent) => ({
|
|
147
|
+
name: agent.name.slice(0, 100),
|
|
148
|
+
value: agent.name,
|
|
149
|
+
}))
|
|
150
|
+
|
|
151
|
+
await interaction.respond(choices)
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.error('[AUTOCOMPLETE] Error fetching agents:', error)
|
|
154
|
+
await interaction.respond([])
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
103
158
|
export async function handleSessionAutocomplete({
|
|
104
159
|
interaction,
|
|
105
160
|
appId,
|
|
106
161
|
}: AutocompleteContext): Promise<void> {
|
|
107
162
|
const focusedOption = interaction.options.getFocused(true)
|
|
108
163
|
|
|
164
|
+
if (focusedOption.name === 'agent') {
|
|
165
|
+
await handleAgentAutocomplete({ interaction, appId })
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
109
169
|
if (focusedOption.name !== 'files') {
|
|
110
170
|
return
|
|
111
171
|
}
|
|
@@ -156,8 +216,7 @@ export async function handleSessionAutocomplete({
|
|
|
156
216
|
|
|
157
217
|
const files = response.data || []
|
|
158
218
|
|
|
159
|
-
const prefix =
|
|
160
|
-
previousFiles.length > 0 ? previousFiles.join(', ') + ', ' : ''
|
|
219
|
+
const prefix = previousFiles.length > 0 ? previousFiles.join(', ') + ', ' : ''
|
|
161
220
|
|
|
162
221
|
const choices = files
|
|
163
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
|
@@ -141,7 +141,7 @@ export function setChannelModel(channelId: string, modelId: string): void {
|
|
|
141
141
|
db.prepare(
|
|
142
142
|
`INSERT INTO channel_models (channel_id, model_id, updated_at)
|
|
143
143
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
144
|
-
ON CONFLICT(channel_id) DO UPDATE SET model_id = ?, updated_at = CURRENT_TIMESTAMP
|
|
144
|
+
ON CONFLICT(channel_id) DO UPDATE SET model_id = ?, updated_at = CURRENT_TIMESTAMP`,
|
|
145
145
|
).run(channelId, modelId, modelId)
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -163,9 +163,10 @@ export function getSessionModel(sessionId: string): string | undefined {
|
|
|
163
163
|
*/
|
|
164
164
|
export function setSessionModel(sessionId: string, modelId: string): void {
|
|
165
165
|
const db = getDatabase()
|
|
166
|
-
db.prepare(
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
db.prepare(`INSERT OR REPLACE INTO session_models (session_id, model_id) VALUES (?, ?)`).run(
|
|
167
|
+
sessionId,
|
|
168
|
+
modelId,
|
|
169
|
+
)
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
/**
|
|
@@ -187,7 +188,7 @@ export function setChannelAgent(channelId: string, agentName: string): void {
|
|
|
187
188
|
db.prepare(
|
|
188
189
|
`INSERT INTO channel_agents (channel_id, agent_name, updated_at)
|
|
189
190
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
190
|
-
ON CONFLICT(channel_id) DO UPDATE SET agent_name = ?, updated_at = CURRENT_TIMESTAMP
|
|
191
|
+
ON CONFLICT(channel_id) DO UPDATE SET agent_name = ?, updated_at = CURRENT_TIMESTAMP`,
|
|
191
192
|
).run(channelId, agentName, agentName)
|
|
192
193
|
}
|
|
193
194
|
|
|
@@ -207,9 +208,10 @@ export function getSessionAgent(sessionId: string): string | undefined {
|
|
|
207
208
|
*/
|
|
208
209
|
export function setSessionAgent(sessionId: string, agentName: string): void {
|
|
209
210
|
const db = getDatabase()
|
|
210
|
-
db.prepare(
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
db.prepare(`INSERT OR REPLACE INTO session_agents (session_id, agent_name) VALUES (?, ?)`).run(
|
|
212
|
+
sessionId,
|
|
213
|
+
agentName,
|
|
214
|
+
)
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
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
|
)
|
|
@@ -208,9 +196,7 @@ export async function startDiscordBot({
|
|
|
208
196
|
return
|
|
209
197
|
}
|
|
210
198
|
|
|
211
|
-
voiceLogger.log(
|
|
212
|
-
`[SESSION] Found session ${row.session_id} for thread ${thread.id}`,
|
|
213
|
-
)
|
|
199
|
+
voiceLogger.log(`[SESSION] Found session ${row.session_id} for thread ${thread.id}`)
|
|
214
200
|
|
|
215
201
|
const parent = thread.parent as TextChannel | null
|
|
216
202
|
let projectDirectory: string | undefined
|
|
@@ -315,9 +301,7 @@ export async function startDiscordBot({
|
|
|
315
301
|
)
|
|
316
302
|
|
|
317
303
|
if (!textChannel.topic) {
|
|
318
|
-
voiceLogger.log(
|
|
319
|
-
`[IGNORED] Channel #${textChannel.name} has no description`,
|
|
320
|
-
)
|
|
304
|
+
voiceLogger.log(`[IGNORED] Channel #${textChannel.name} has no description`)
|
|
321
305
|
return
|
|
322
306
|
}
|
|
323
307
|
|
|
@@ -330,9 +314,7 @@ export async function startDiscordBot({
|
|
|
330
314
|
const channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
331
315
|
|
|
332
316
|
if (!projectDirectory) {
|
|
333
|
-
voiceLogger.log(
|
|
334
|
-
`[IGNORED] Channel #${textChannel.name} has no kimaki.directory tag`,
|
|
335
|
-
)
|
|
317
|
+
voiceLogger.log(`[IGNORED] Channel #${textChannel.name} has no kimaki.directory tag`)
|
|
336
318
|
return
|
|
337
319
|
}
|
|
338
320
|
|
|
@@ -343,9 +325,7 @@ export async function startDiscordBot({
|
|
|
343
325
|
return
|
|
344
326
|
}
|
|
345
327
|
|
|
346
|
-
discordLogger.log(
|
|
347
|
-
`DIRECTORY: Found kimaki.directory: ${projectDirectory}`,
|
|
348
|
-
)
|
|
328
|
+
discordLogger.log(`DIRECTORY: Found kimaki.directory: ${projectDirectory}`)
|
|
349
329
|
if (channelAppId) {
|
|
350
330
|
discordLogger.log(`APP: Channel app ID: ${channelAppId}`)
|
|
351
331
|
}
|
|
@@ -359,9 +339,7 @@ export async function startDiscordBot({
|
|
|
359
339
|
return
|
|
360
340
|
}
|
|
361
341
|
|
|
362
|
-
const hasVoice = message.attachments.some((a) =>
|
|
363
|
-
a.contentType?.startsWith('audio/'),
|
|
364
|
-
)
|
|
342
|
+
const hasVoice = message.attachments.some((a) => a.contentType?.startsWith('audio/'))
|
|
365
343
|
|
|
366
344
|
const threadName = hasVoice
|
|
367
345
|
? 'Voice Message'
|
|
@@ -489,7 +467,9 @@ export async function startDiscordBot({
|
|
|
489
467
|
return
|
|
490
468
|
}
|
|
491
469
|
|
|
492
|
-
discordLogger.log(
|
|
470
|
+
discordLogger.log(
|
|
471
|
+
`[BOT_SESSION] Starting session for thread ${thread.id} with prompt: "${prompt.slice(0, 50)}..."`,
|
|
472
|
+
)
|
|
493
473
|
|
|
494
474
|
await handleOpencodeSession({
|
|
495
475
|
prompt,
|
|
@@ -522,9 +502,7 @@ export async function startDiscordBot({
|
|
|
522
502
|
try {
|
|
523
503
|
const cleanupPromises: Promise<void>[] = []
|
|
524
504
|
for (const [guildId] of voiceConnections) {
|
|
525
|
-
voiceLogger.log(
|
|
526
|
-
`[SHUTDOWN] Cleaning up voice connection for guild ${guildId}`,
|
|
527
|
-
)
|
|
505
|
+
voiceLogger.log(`[SHUTDOWN] Cleaning up voice connection for guild ${guildId}`)
|
|
528
506
|
cleanupPromises.push(cleanupVoiceConnection(guildId))
|
|
529
507
|
}
|
|
530
508
|
|
|
@@ -538,9 +516,7 @@ export async function startDiscordBot({
|
|
|
538
516
|
|
|
539
517
|
for (const [dir, server] of getOpencodeServers()) {
|
|
540
518
|
if (!server.process.killed) {
|
|
541
|
-
voiceLogger.log(
|
|
542
|
-
`[SHUTDOWN] Stopping OpenCode server on port ${server.port} for ${dir}`,
|
|
543
|
-
)
|
|
519
|
+
voiceLogger.log(`[SHUTDOWN] Stopping OpenCode server on port ${server.port} for ${dir}`)
|
|
544
520
|
server.process.kill('SIGTERM')
|
|
545
521
|
}
|
|
546
522
|
}
|
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
|
})
|
package/src/genai-worker.ts
CHANGED
|
@@ -40,12 +40,7 @@ process.on('uncaughtException', (error) => {
|
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
process.on('unhandledRejection', (reason, promise) => {
|
|
43
|
-
workerLogger.error(
|
|
44
|
-
'Unhandled rejection in worker:',
|
|
45
|
-
reason,
|
|
46
|
-
'at promise:',
|
|
47
|
-
promise,
|
|
48
|
-
)
|
|
43
|
+
workerLogger.error('Unhandled rejection in worker:', reason, 'at promise:', promise)
|
|
49
44
|
sendError(`Worker unhandled rejection: ${reason}`)
|
|
50
45
|
})
|
|
51
46
|
|
|
@@ -130,12 +125,7 @@ async function createAssistantAudioLogStream(
|
|
|
130
125
|
if (!process.env.DEBUG) return null
|
|
131
126
|
|
|
132
127
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
133
|
-
const audioDir = path.join(
|
|
134
|
-
process.cwd(),
|
|
135
|
-
'discord-audio-logs',
|
|
136
|
-
guildId,
|
|
137
|
-
channelId,
|
|
138
|
-
)
|
|
128
|
+
const audioDir = path.join(process.cwd(), 'discord-audio-logs', guildId, channelId)
|
|
139
129
|
|
|
140
130
|
try {
|
|
141
131
|
await mkdir(audioDir, { recursive: true })
|
|
@@ -252,10 +242,7 @@ parentPort.on('message', async (message: WorkerInMessage) => {
|
|
|
252
242
|
workerLogger.log(`Initializing with directory:`, message.directory)
|
|
253
243
|
|
|
254
244
|
// Create audio log stream for assistant audio
|
|
255
|
-
audioLogStream = await createAssistantAudioLogStream(
|
|
256
|
-
message.guildId,
|
|
257
|
-
message.channelId,
|
|
258
|
-
)
|
|
245
|
+
audioLogStream = await createAssistantAudioLogStream(message.guildId, message.channelId)
|
|
259
246
|
|
|
260
247
|
// Start packet sending interval
|
|
261
248
|
startPacketSending()
|
|
@@ -359,8 +346,6 @@ parentPort.on('message', async (message: WorkerInMessage) => {
|
|
|
359
346
|
}
|
|
360
347
|
} catch (error) {
|
|
361
348
|
workerLogger.error(`Error handling message:`, error)
|
|
362
|
-
sendError(
|
|
363
|
-
error instanceof Error ? error.message : 'Unknown error in worker',
|
|
364
|
-
)
|
|
349
|
+
sendError(error instanceof Error ? error.message : 'Unknown error in worker')
|
|
365
350
|
}
|
|
366
351
|
})
|