kimaki 0.4.24 → 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,144 @@
|
|
|
1
|
+
// /session command - Start a new OpenCode session.
|
|
2
|
+
import { ChannelType } from 'discord.js';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { getDatabase } from '../database.js';
|
|
6
|
+
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
7
|
+
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
8
|
+
import { extractTagsArrays } from '../xml.js';
|
|
9
|
+
import { handleOpencodeSession, parseSlashCommand } from '../session-handler.js';
|
|
10
|
+
import { createLogger } from '../logger.js';
|
|
11
|
+
const logger = createLogger('SESSION');
|
|
12
|
+
export async function handleSessionCommand({ command, appId, }) {
|
|
13
|
+
await command.deferReply({ ephemeral: false });
|
|
14
|
+
const prompt = command.options.getString('prompt', true);
|
|
15
|
+
const filesString = command.options.getString('files') || '';
|
|
16
|
+
const channel = command.channel;
|
|
17
|
+
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
18
|
+
await command.editReply('This command can only be used in text channels');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const textChannel = channel;
|
|
22
|
+
let projectDirectory;
|
|
23
|
+
let channelAppId;
|
|
24
|
+
if (textChannel.topic) {
|
|
25
|
+
const extracted = extractTagsArrays({
|
|
26
|
+
xml: textChannel.topic,
|
|
27
|
+
tags: ['kimaki.directory', 'kimaki.app'],
|
|
28
|
+
});
|
|
29
|
+
projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
30
|
+
channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
31
|
+
}
|
|
32
|
+
if (channelAppId && channelAppId !== appId) {
|
|
33
|
+
await command.editReply('This channel is not configured for this bot');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!projectDirectory) {
|
|
37
|
+
await command.editReply('This channel is not configured with a project directory');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!fs.existsSync(projectDirectory)) {
|
|
41
|
+
await command.editReply(`Directory does not exist: ${projectDirectory}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
46
|
+
const files = filesString
|
|
47
|
+
.split(',')
|
|
48
|
+
.map((f) => f.trim())
|
|
49
|
+
.filter((f) => f);
|
|
50
|
+
let fullPrompt = prompt;
|
|
51
|
+
if (files.length > 0) {
|
|
52
|
+
fullPrompt = `${prompt}\n\n@${files.join(' @')}`;
|
|
53
|
+
}
|
|
54
|
+
const starterMessage = await textChannel.send({
|
|
55
|
+
content: `🚀 **Starting OpenCode session**\n📝 ${prompt.slice(0, 200)}${prompt.length > 200 ? '…' : ''}${files.length > 0 ? `\n📎 Files: ${files.join(', ')}` : ''}`,
|
|
56
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
57
|
+
});
|
|
58
|
+
const thread = await starterMessage.startThread({
|
|
59
|
+
name: prompt.slice(0, 100),
|
|
60
|
+
autoArchiveDuration: 1440,
|
|
61
|
+
reason: 'OpenCode session',
|
|
62
|
+
});
|
|
63
|
+
await command.editReply(`Created new session in ${thread.toString()}`);
|
|
64
|
+
const parsedCommand = parseSlashCommand(fullPrompt);
|
|
65
|
+
await handleOpencodeSession({
|
|
66
|
+
prompt: fullPrompt,
|
|
67
|
+
thread,
|
|
68
|
+
projectDirectory,
|
|
69
|
+
parsedCommand,
|
|
70
|
+
channelId: textChannel.id,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger.error('[SESSION] Error:', error);
|
|
75
|
+
await command.editReply(`Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function handleSessionAutocomplete({ interaction, appId, }) {
|
|
79
|
+
const focusedOption = interaction.options.getFocused(true);
|
|
80
|
+
if (focusedOption.name !== 'files') {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const focusedValue = focusedOption.value;
|
|
84
|
+
const parts = focusedValue.split(',');
|
|
85
|
+
const previousFiles = parts
|
|
86
|
+
.slice(0, -1)
|
|
87
|
+
.map((f) => f.trim())
|
|
88
|
+
.filter((f) => f);
|
|
89
|
+
const currentQuery = (parts[parts.length - 1] || '').trim();
|
|
90
|
+
let projectDirectory;
|
|
91
|
+
if (interaction.channel) {
|
|
92
|
+
const channel = interaction.channel;
|
|
93
|
+
if (channel.type === ChannelType.GuildText) {
|
|
94
|
+
const textChannel = channel;
|
|
95
|
+
if (textChannel.topic) {
|
|
96
|
+
const extracted = extractTagsArrays({
|
|
97
|
+
xml: textChannel.topic,
|
|
98
|
+
tags: ['kimaki.directory', 'kimaki.app'],
|
|
99
|
+
});
|
|
100
|
+
const channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
101
|
+
if (channelAppId && channelAppId !== appId) {
|
|
102
|
+
await interaction.respond([]);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!projectDirectory) {
|
|
110
|
+
await interaction.respond([]);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
115
|
+
const response = await getClient().find.files({
|
|
116
|
+
query: {
|
|
117
|
+
query: currentQuery || '',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
const files = response.data || [];
|
|
121
|
+
const prefix = previousFiles.length > 0 ? previousFiles.join(', ') + ', ' : '';
|
|
122
|
+
const choices = files
|
|
123
|
+
.map((file) => {
|
|
124
|
+
const fullValue = prefix + file;
|
|
125
|
+
const allFiles = [...previousFiles, file];
|
|
126
|
+
const allBasenames = allFiles.map((f) => f.split('/').pop() || f);
|
|
127
|
+
let displayName = allBasenames.join(', ');
|
|
128
|
+
if (displayName.length > 100) {
|
|
129
|
+
displayName = '…' + displayName.slice(-97);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
name: displayName,
|
|
133
|
+
value: fullValue,
|
|
134
|
+
};
|
|
135
|
+
})
|
|
136
|
+
.filter((choice) => choice.value.length <= 100)
|
|
137
|
+
.slice(0, 25);
|
|
138
|
+
await interaction.respond(choices);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
logger.error('[AUTOCOMPLETE] Error fetching files:', error);
|
|
142
|
+
await interaction.respond([]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// /share command - Share the current session as a public URL.
|
|
2
|
+
import { ChannelType } from 'discord.js';
|
|
3
|
+
import { getDatabase } from '../database.js';
|
|
4
|
+
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
|
+
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
|
+
import { createLogger } from '../logger.js';
|
|
7
|
+
const logger = createLogger('SHARE');
|
|
8
|
+
export async function handleShareCommand({ command, }) {
|
|
9
|
+
const channel = command.channel;
|
|
10
|
+
if (!channel) {
|
|
11
|
+
await command.reply({
|
|
12
|
+
content: 'This command can only be used in a channel',
|
|
13
|
+
ephemeral: true,
|
|
14
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const isThread = [
|
|
19
|
+
ChannelType.PublicThread,
|
|
20
|
+
ChannelType.PrivateThread,
|
|
21
|
+
ChannelType.AnnouncementThread,
|
|
22
|
+
].includes(channel.type);
|
|
23
|
+
if (!isThread) {
|
|
24
|
+
await command.reply({
|
|
25
|
+
content: 'This command can only be used in a thread with an active session',
|
|
26
|
+
ephemeral: true,
|
|
27
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const textChannel = await resolveTextChannel(channel);
|
|
32
|
+
const { projectDirectory: directory } = getKimakiMetadata(textChannel);
|
|
33
|
+
if (!directory) {
|
|
34
|
+
await command.reply({
|
|
35
|
+
content: 'Could not determine project directory for this channel',
|
|
36
|
+
ephemeral: true,
|
|
37
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const row = getDatabase()
|
|
42
|
+
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
43
|
+
.get(channel.id);
|
|
44
|
+
if (!row?.session_id) {
|
|
45
|
+
await command.reply({
|
|
46
|
+
content: 'No active session in this thread',
|
|
47
|
+
ephemeral: true,
|
|
48
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const sessionId = row.session_id;
|
|
53
|
+
try {
|
|
54
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
|
+
const response = await getClient().session.share({
|
|
56
|
+
path: { id: sessionId },
|
|
57
|
+
});
|
|
58
|
+
if (!response.data?.share?.url) {
|
|
59
|
+
await command.reply({
|
|
60
|
+
content: 'Failed to generate share URL',
|
|
61
|
+
ephemeral: true,
|
|
62
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await command.reply({
|
|
67
|
+
content: `🔗 **Session shared:** ${response.data.share.url}`,
|
|
68
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
69
|
+
});
|
|
70
|
+
logger.log(`Session ${sessionId} shared: ${response.data.share.url}`);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
logger.error('[SHARE] Error:', error);
|
|
74
|
+
await command.reply({
|
|
75
|
+
content: `Failed to share session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
76
|
+
ephemeral: true,
|
|
77
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Undo/Redo commands - /undo, /redo
|
|
2
|
+
import { ChannelType } from 'discord.js';
|
|
3
|
+
import { getDatabase } from '../database.js';
|
|
4
|
+
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
|
+
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
|
+
import { createLogger } from '../logger.js';
|
|
7
|
+
const logger = createLogger('UNDO-REDO');
|
|
8
|
+
export async function handleUndoCommand({ command, }) {
|
|
9
|
+
const channel = command.channel;
|
|
10
|
+
if (!channel) {
|
|
11
|
+
await command.reply({
|
|
12
|
+
content: 'This command can only be used in a channel',
|
|
13
|
+
ephemeral: true,
|
|
14
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const isThread = [
|
|
19
|
+
ChannelType.PublicThread,
|
|
20
|
+
ChannelType.PrivateThread,
|
|
21
|
+
ChannelType.AnnouncementThread,
|
|
22
|
+
].includes(channel.type);
|
|
23
|
+
if (!isThread) {
|
|
24
|
+
await command.reply({
|
|
25
|
+
content: 'This command can only be used in a thread with an active session',
|
|
26
|
+
ephemeral: true,
|
|
27
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const textChannel = await resolveTextChannel(channel);
|
|
32
|
+
const { projectDirectory: directory } = getKimakiMetadata(textChannel);
|
|
33
|
+
if (!directory) {
|
|
34
|
+
await command.reply({
|
|
35
|
+
content: 'Could not determine project directory for this channel',
|
|
36
|
+
ephemeral: true,
|
|
37
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const row = getDatabase()
|
|
42
|
+
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
43
|
+
.get(channel.id);
|
|
44
|
+
if (!row?.session_id) {
|
|
45
|
+
await command.reply({
|
|
46
|
+
content: 'No active session in this thread',
|
|
47
|
+
ephemeral: true,
|
|
48
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const sessionId = row.session_id;
|
|
53
|
+
try {
|
|
54
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
55
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
56
|
+
// Fetch messages to find the last assistant message
|
|
57
|
+
const messagesResponse = await getClient().session.messages({
|
|
58
|
+
path: { id: sessionId },
|
|
59
|
+
});
|
|
60
|
+
if (!messagesResponse.data || messagesResponse.data.length === 0) {
|
|
61
|
+
await command.editReply('No messages to undo');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Find the last assistant message
|
|
65
|
+
const lastAssistantMessage = [...messagesResponse.data]
|
|
66
|
+
.reverse()
|
|
67
|
+
.find((m) => m.info.role === 'assistant');
|
|
68
|
+
if (!lastAssistantMessage) {
|
|
69
|
+
await command.editReply('No assistant message to undo');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const response = await getClient().session.revert({
|
|
73
|
+
path: { id: sessionId },
|
|
74
|
+
body: { messageID: lastAssistantMessage.info.id },
|
|
75
|
+
});
|
|
76
|
+
if (response.error) {
|
|
77
|
+
await command.editReply(`Failed to undo: ${JSON.stringify(response.error)}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const diffInfo = response.data?.revert?.diff
|
|
81
|
+
? `\n\`\`\`diff\n${response.data.revert.diff.slice(0, 1500)}\n\`\`\``
|
|
82
|
+
: '';
|
|
83
|
+
await command.editReply(`⏪ **Undone** - reverted last assistant message${diffInfo}`);
|
|
84
|
+
logger.log(`Session ${sessionId} reverted message ${lastAssistantMessage.info.id}`);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
logger.error('[UNDO] Error:', error);
|
|
88
|
+
await command.editReply(`Failed to undo: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function handleRedoCommand({ command, }) {
|
|
92
|
+
const channel = command.channel;
|
|
93
|
+
if (!channel) {
|
|
94
|
+
await command.reply({
|
|
95
|
+
content: 'This command can only be used in a channel',
|
|
96
|
+
ephemeral: true,
|
|
97
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const isThread = [
|
|
102
|
+
ChannelType.PublicThread,
|
|
103
|
+
ChannelType.PrivateThread,
|
|
104
|
+
ChannelType.AnnouncementThread,
|
|
105
|
+
].includes(channel.type);
|
|
106
|
+
if (!isThread) {
|
|
107
|
+
await command.reply({
|
|
108
|
+
content: 'This command can only be used in a thread with an active session',
|
|
109
|
+
ephemeral: true,
|
|
110
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const textChannel = await resolveTextChannel(channel);
|
|
115
|
+
const { projectDirectory: directory } = getKimakiMetadata(textChannel);
|
|
116
|
+
if (!directory) {
|
|
117
|
+
await command.reply({
|
|
118
|
+
content: 'Could not determine project directory for this channel',
|
|
119
|
+
ephemeral: true,
|
|
120
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const row = getDatabase()
|
|
125
|
+
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
126
|
+
.get(channel.id);
|
|
127
|
+
if (!row?.session_id) {
|
|
128
|
+
await command.reply({
|
|
129
|
+
content: 'No active session in this thread',
|
|
130
|
+
ephemeral: true,
|
|
131
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const sessionId = row.session_id;
|
|
136
|
+
try {
|
|
137
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
138
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
139
|
+
// Check if session has reverted state
|
|
140
|
+
const sessionResponse = await getClient().session.get({
|
|
141
|
+
path: { id: sessionId },
|
|
142
|
+
});
|
|
143
|
+
if (!sessionResponse.data?.revert) {
|
|
144
|
+
await command.editReply('Nothing to redo - no previous undo found');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const response = await getClient().session.unrevert({
|
|
148
|
+
path: { id: sessionId },
|
|
149
|
+
});
|
|
150
|
+
if (response.error) {
|
|
151
|
+
await command.editReply(`Failed to redo: ${JSON.stringify(response.error)}`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
await command.editReply(`⏩ **Restored** - session back to previous state`);
|
|
155
|
+
logger.log(`Session ${sessionId} unrevert completed`);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
logger.error('[REDO] Error:', error);
|
|
159
|
+
await command.editReply(`Failed to redo: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
package/dist/database.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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.
|
|
1
4
|
import Database from 'better-sqlite3';
|
|
2
5
|
import fs from 'node:fs';
|
|
3
6
|
import os from 'node:os';
|
package/dist/discord-bot.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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.
|
|
1
4
|
import { getDatabase, closeDatabase } from './database.js';
|
|
2
5
|
import { initializeOpencodeForDirectory, getOpencodeServers } from './opencode.js';
|
|
3
6
|
import { escapeBackticksInCodeBlocks, splitMarkdownForDiscord, SILENT_MESSAGE_FLAGS, } from './discord-utils.js';
|
package/dist/discord-utils.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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.
|
|
1
4
|
import { ChannelType, } from 'discord.js';
|
|
2
5
|
import { Lexer } from 'marked';
|
|
3
6
|
import { extractTagsArrays } from './xml.js';
|
|
@@ -5,6 +8,8 @@ import { formatMarkdownTables } from './format-tables.js';
|
|
|
5
8
|
import { createLogger } from './logger.js';
|
|
6
9
|
const discordLogger = createLogger('DISCORD');
|
|
7
10
|
export const SILENT_MESSAGE_FLAGS = 4 | 4096;
|
|
11
|
+
// Same as SILENT but without SuppressNotifications - triggers badge/notification
|
|
12
|
+
export const NOTIFY_MESSAGE_FLAGS = 4;
|
|
8
13
|
export function escapeBackticksInCodeBlocks(markdown) {
|
|
9
14
|
const lexer = new Lexer();
|
|
10
15
|
const tokens = lexer.lex(markdown);
|
|
@@ -91,10 +96,14 @@ export function splitMarkdownForDiscord({ content, maxLength, }) {
|
|
|
91
96
|
}
|
|
92
97
|
return chunks;
|
|
93
98
|
}
|
|
94
|
-
export async function sendThreadMessage(thread, content) {
|
|
99
|
+
export async function sendThreadMessage(thread, content, options) {
|
|
95
100
|
const MAX_LENGTH = 2000;
|
|
96
101
|
content = formatMarkdownTables(content);
|
|
97
102
|
content = escapeBackticksInCodeBlocks(content);
|
|
103
|
+
// If custom flags provided, send as single message (no chunking)
|
|
104
|
+
if (options?.flags !== undefined) {
|
|
105
|
+
return thread.send({ content, flags: options.flags });
|
|
106
|
+
}
|
|
98
107
|
const chunks = splitMarkdownForDiscord({ content, maxLength: MAX_LENGTH });
|
|
99
108
|
if (chunks.length > 1) {
|
|
100
109
|
discordLogger.log(`MESSAGE: Splitting ${content.length} chars into ${chunks.length} messages`);
|
package/dist/format-tables.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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.
|
|
1
4
|
import { Lexer } from 'marked';
|
|
2
5
|
export function formatMarkdownTables(markdown) {
|
|
3
6
|
const lexer = new Lexer();
|
|
@@ -1,3 +1,6 @@
|
|
|
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.
|
|
1
4
|
import { Worker } from 'node:worker_threads';
|
|
2
5
|
import { createLogger } from './logger.js';
|
|
3
6
|
const genaiWorkerLogger = createLogger('GENAI WORKER');
|
package/dist/genai-worker.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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.
|
|
1
4
|
import { parentPort, threadId } from 'node:worker_threads';
|
|
2
5
|
import { createWriteStream } from 'node:fs';
|
|
3
6
|
import { mkdir } from 'node:fs/promises';
|
package/dist/genai.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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.
|
|
1
4
|
import { GoogleGenAI, LiveServerMessage, MediaResolution, Modality, Session, } from '@google/genai';
|
|
2
5
|
import { writeFile } from 'fs';
|
|
3
6
|
import { createLogger } from './logger.js';
|