kimaki 0.4.35 → 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 +135 -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 +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/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 +1 -1
- package/dist/session-handler.js +25 -15
- package/dist/system-message.js +5 -3
- 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 +14 -25
- package/src/cli.ts +282 -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 +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 +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 +24 -34
- package/src/session-handler.ts +91 -61
- package/src/system-message.ts +13 -3
- 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
|
@@ -8,7 +8,7 @@ import { createLogger } from '../logger.js';
|
|
|
8
8
|
import { getDatabase } from '../database.js';
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
const userCommandLogger = createLogger('USER_CMD');
|
|
11
|
-
export const handleUserCommand = async ({ command, appId
|
|
11
|
+
export const handleUserCommand = async ({ command, appId }) => {
|
|
12
12
|
const discordCommandName = command.commandName;
|
|
13
13
|
// Strip the -cmd suffix to get the actual OpenCode command name
|
|
14
14
|
const commandName = discordCommandName.replace(/-cmd$/, '');
|
|
@@ -16,11 +16,8 @@ export const handleUserCommand = async ({ command, appId, }) => {
|
|
|
16
16
|
userCommandLogger.log(`Executing /${commandName} (from /${discordCommandName}) with args: ${args}`);
|
|
17
17
|
const channel = command.channel;
|
|
18
18
|
userCommandLogger.log(`Channel info: type=${channel?.type}, id=${channel?.id}, isNull=${channel === null}`);
|
|
19
|
-
const isThread = channel &&
|
|
20
|
-
ChannelType.PublicThread,
|
|
21
|
-
ChannelType.PrivateThread,
|
|
22
|
-
ChannelType.AnnouncementThread,
|
|
23
|
-
].includes(channel.type);
|
|
19
|
+
const isThread = channel &&
|
|
20
|
+
[ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.AnnouncementThread].includes(channel.type);
|
|
24
21
|
const isTextChannel = channel?.type === ChannelType.GuildText;
|
|
25
22
|
if (!channel || (!isTextChannel && !isThread)) {
|
|
26
23
|
await command.reply({
|
package/dist/config.js
CHANGED
|
@@ -51,7 +51,7 @@ export function getLockPort() {
|
|
|
51
51
|
let hash = 0;
|
|
52
52
|
for (let i = 0; i < dir.length; i++) {
|
|
53
53
|
const char = dir.charCodeAt(i);
|
|
54
|
-
hash = (
|
|
54
|
+
hash = (hash << 5) - hash + char;
|
|
55
55
|
hash = hash & hash; // Convert to 32bit integer
|
|
56
56
|
}
|
|
57
57
|
// Map to port range 30000-39999
|
package/dist/discord-bot.js
CHANGED
|
@@ -8,14 +8,14 @@ import { getOpencodeSystemMessage } from './system-message.js';
|
|
|
8
8
|
import { getFileAttachments, getTextAttachments } from './message-formatting.js';
|
|
9
9
|
import { ensureKimakiCategory, ensureKimakiAudioCategory, createProjectChannels, getChannelsWithDescriptions, } from './channel-management.js';
|
|
10
10
|
import { voiceConnections, cleanupVoiceConnection, processVoiceAttachment, registerVoiceStateHandler, } from './voice-handler.js';
|
|
11
|
-
import { getCompactSessionContext, getLastSessionId
|
|
11
|
+
import { getCompactSessionContext, getLastSessionId } from './markdown.js';
|
|
12
12
|
import { handleOpencodeSession } from './session-handler.js';
|
|
13
13
|
import { registerInteractionHandler } from './interaction-handler.js';
|
|
14
14
|
export { getDatabase, closeDatabase } from './database.js';
|
|
15
15
|
export { initializeOpencodeForDirectory } from './opencode.js';
|
|
16
16
|
export { escapeBackticksInCodeBlocks, splitMarkdownForDiscord } from './discord-utils.js';
|
|
17
17
|
export { getOpencodeSystemMessage } from './system-message.js';
|
|
18
|
-
export { ensureKimakiCategory, ensureKimakiAudioCategory, createProjectChannels, getChannelsWithDescriptions } from './channel-management.js';
|
|
18
|
+
export { ensureKimakiCategory, ensureKimakiAudioCategory, createProjectChannels, getChannelsWithDescriptions, } from './channel-management.js';
|
|
19
19
|
import { ChannelType, Client, Events, GatewayIntentBits, Partials, PermissionsBitField, ThreadAutoArchiveDuration, } from 'discord.js';
|
|
20
20
|
import fs from 'node:fs';
|
|
21
21
|
import { extractTagsArrays } from './xml.js';
|
|
@@ -32,12 +32,7 @@ export async function createDiscordClient() {
|
|
|
32
32
|
GatewayIntentBits.MessageContent,
|
|
33
33
|
GatewayIntentBits.GuildVoiceStates,
|
|
34
34
|
],
|
|
35
|
-
partials: [
|
|
36
|
-
Partials.Channel,
|
|
37
|
-
Partials.Message,
|
|
38
|
-
Partials.User,
|
|
39
|
-
Partials.ThreadMember,
|
|
40
|
-
],
|
|
35
|
+
partials: [Partials.Channel, Partials.Message, Partials.User, Partials.ThreadMember],
|
|
41
36
|
});
|
|
42
37
|
}
|
|
43
38
|
export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
@@ -64,8 +59,7 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
64
59
|
for (const guild of c.guilds.cache.values()) {
|
|
65
60
|
discordLogger.log(`${guild.name} (${guild.id})`);
|
|
66
61
|
const channels = await getChannelsWithDescriptions(guild);
|
|
67
|
-
const kimakiChannels = channels.filter((ch) => ch.kimakiDirectory &&
|
|
68
|
-
(!ch.kimakiApp || ch.kimakiApp === currentAppId));
|
|
62
|
+
const kimakiChannels = channels.filter((ch) => ch.kimakiDirectory && (!ch.kimakiApp || ch.kimakiApp === currentAppId));
|
|
69
63
|
if (kimakiChannels.length > 0) {
|
|
70
64
|
discordLogger.log(` Found ${kimakiChannels.length} channel(s) for this bot:`);
|
|
71
65
|
for (const channel of kimakiChannels) {
|
package/dist/discord-utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Discord-specific utility functions.
|
|
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
|
-
import { ChannelType
|
|
4
|
+
import { ChannelType } from 'discord.js';
|
|
5
5
|
import { Lexer } from 'marked';
|
|
6
6
|
import { extractTagsArrays } from './xml.js';
|
|
7
7
|
import { formatMarkdownTables } from './format-tables.js';
|
|
@@ -37,12 +37,30 @@ export function splitMarkdownForDiscord({ content, maxLength, }) {
|
|
|
37
37
|
for (const token of tokens) {
|
|
38
38
|
if (token.type === 'code') {
|
|
39
39
|
const lang = token.lang || '';
|
|
40
|
-
lines.push({
|
|
40
|
+
lines.push({
|
|
41
|
+
text: '```' + lang + '\n',
|
|
42
|
+
inCodeBlock: false,
|
|
43
|
+
lang,
|
|
44
|
+
isOpeningFence: true,
|
|
45
|
+
isClosingFence: false,
|
|
46
|
+
});
|
|
41
47
|
const codeLines = token.text.split('\n');
|
|
42
48
|
for (const codeLine of codeLines) {
|
|
43
|
-
lines.push({
|
|
49
|
+
lines.push({
|
|
50
|
+
text: codeLine + '\n',
|
|
51
|
+
inCodeBlock: true,
|
|
52
|
+
lang,
|
|
53
|
+
isOpeningFence: false,
|
|
54
|
+
isClosingFence: false,
|
|
55
|
+
});
|
|
44
56
|
}
|
|
45
|
-
lines.push({
|
|
57
|
+
lines.push({
|
|
58
|
+
text: '```\n',
|
|
59
|
+
inCodeBlock: false,
|
|
60
|
+
lang: '',
|
|
61
|
+
isOpeningFence: false,
|
|
62
|
+
isClosingFence: true,
|
|
63
|
+
});
|
|
46
64
|
}
|
|
47
65
|
else {
|
|
48
66
|
const rawLines = token.raw.split('\n');
|
|
@@ -50,7 +68,13 @@ export function splitMarkdownForDiscord({ content, maxLength, }) {
|
|
|
50
68
|
const isLast = i === rawLines.length - 1;
|
|
51
69
|
const text = isLast ? rawLines[i] : rawLines[i] + '\n';
|
|
52
70
|
if (text) {
|
|
53
|
-
lines.push({
|
|
71
|
+
lines.push({
|
|
72
|
+
text,
|
|
73
|
+
inCodeBlock: false,
|
|
74
|
+
lang: '',
|
|
75
|
+
isOpeningFence: false,
|
|
76
|
+
isClosingFence: false,
|
|
77
|
+
});
|
|
54
78
|
}
|
|
55
79
|
}
|
|
56
80
|
}
|
|
@@ -93,7 +117,9 @@ export function splitMarkdownForDiscord({ content, maxLength, }) {
|
|
|
93
117
|
currentChunk = '';
|
|
94
118
|
}
|
|
95
119
|
// calculate overhead for code block markers
|
|
96
|
-
const codeBlockOverhead = line.inCodeBlock
|
|
120
|
+
const codeBlockOverhead = line.inCodeBlock
|
|
121
|
+
? ('```' + line.lang + '\n').length + '```\n'.length
|
|
122
|
+
: 0;
|
|
97
123
|
// ensure at least 10 chars available, even if maxLength is very small
|
|
98
124
|
const availablePerChunk = Math.max(10, maxLength - codeBlockOverhead - 50);
|
|
99
125
|
const pieces = splitLongLine(line.text, availablePerChunk, line.inCodeBlock);
|
|
@@ -204,9 +230,7 @@ export async function resolveTextChannel(channel) {
|
|
|
204
230
|
return null;
|
|
205
231
|
}
|
|
206
232
|
export function escapeDiscordFormatting(text) {
|
|
207
|
-
return text
|
|
208
|
-
.replace(/```/g, '\\`\\`\\`')
|
|
209
|
-
.replace(/````/g, '\\`\\`\\`\\`');
|
|
233
|
+
return text.replace(/```/g, '\\`\\`\\`').replace(/````/g, '\\`\\`\\`\\`');
|
|
210
234
|
}
|
|
211
235
|
export function getKimakiMetadata(textChannel) {
|
|
212
236
|
if (!textChannel?.topic) {
|
package/dist/genai.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Google GenAI Live session manager for real-time voice interactions.
|
|
2
2
|
// Establishes bidirectional audio streaming with Gemini, handles tool calls,
|
|
3
3
|
// and manages the assistant's audio output for Discord voice channels.
|
|
4
|
-
import { GoogleGenAI, LiveServerMessage, MediaResolution, Modality, Session
|
|
4
|
+
import { GoogleGenAI, LiveServerMessage, MediaResolution, Modality, Session } from '@google/genai';
|
|
5
5
|
import { writeFile } from 'fs';
|
|
6
6
|
import { createLogger } from './logger.js';
|
|
7
7
|
import { aiToolToCallableTool } from './ai-tool-to-genai.js';
|
|
@@ -65,7 +65,7 @@ function createWavHeader(dataLength, options) {
|
|
|
65
65
|
buffer.writeUInt32LE(dataLength, 40); // Subchunk2Size
|
|
66
66
|
return buffer;
|
|
67
67
|
}
|
|
68
|
-
function defaultAudioChunkHandler({ data, mimeType
|
|
68
|
+
function defaultAudioChunkHandler({ data, mimeType }) {
|
|
69
69
|
audioParts.push(data);
|
|
70
70
|
const fileName = 'audio.wav';
|
|
71
71
|
const buffer = convertToWav(audioParts, mimeType);
|
|
@@ -103,8 +103,7 @@ export async function startGenAiSession({ onAssistantAudioChunk, onAssistantStar
|
|
|
103
103
|
}));
|
|
104
104
|
if (functionResponses.length > 0 && session) {
|
|
105
105
|
session.sendToolResponse({ functionResponses });
|
|
106
|
-
genaiLogger.log('client-toolResponse: ' +
|
|
107
|
-
JSON.stringify({ functionResponses }));
|
|
106
|
+
genaiLogger.log('client-toolResponse: ' + JSON.stringify({ functionResponses }));
|
|
108
107
|
}
|
|
109
108
|
})
|
|
110
109
|
.catch((error) => {
|
|
@@ -120,8 +119,7 @@ export async function startGenAiSession({ onAssistantAudioChunk, onAssistantStar
|
|
|
120
119
|
}
|
|
121
120
|
if (part?.inlineData) {
|
|
122
121
|
const inlineData = part.inlineData;
|
|
123
|
-
if (!inlineData.mimeType ||
|
|
124
|
-
!inlineData.mimeType.startsWith('audio/')) {
|
|
122
|
+
if (!inlineData.mimeType || !inlineData.mimeType.startsWith('audio/')) {
|
|
125
123
|
genaiLogger.log('Skipping non-audio inlineData:', inlineData.mimeType);
|
|
126
124
|
continue;
|
|
127
125
|
}
|
|
@@ -5,12 +5,13 @@ import { Events } from 'discord.js';
|
|
|
5
5
|
import { handleSessionCommand, handleSessionAutocomplete } from './commands/session.js';
|
|
6
6
|
import { handleResumeCommand, handleResumeAutocomplete } from './commands/resume.js';
|
|
7
7
|
import { handleAddProjectCommand, handleAddProjectAutocomplete } from './commands/add-project.js';
|
|
8
|
+
import { handleRemoveProjectCommand, handleRemoveProjectAutocomplete, } from './commands/remove-project.js';
|
|
8
9
|
import { handleCreateNewProjectCommand } from './commands/create-new-project.js';
|
|
9
10
|
import { handlePermissionSelectMenu } from './commands/permissions.js';
|
|
10
11
|
import { handleAbortCommand } from './commands/abort.js';
|
|
11
12
|
import { handleShareCommand } from './commands/share.js';
|
|
12
13
|
import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js';
|
|
13
|
-
import { handleModelCommand, handleProviderSelectMenu, handleModelSelectMenu } from './commands/model.js';
|
|
14
|
+
import { handleModelCommand, handleProviderSelectMenu, handleModelSelectMenu, } from './commands/model.js';
|
|
14
15
|
import { handleAgentCommand, handleAgentSelectMenu } from './commands/agent.js';
|
|
15
16
|
import { handleAskQuestionSelectMenu } from './commands/ask-question.js';
|
|
16
17
|
import { handleQueueCommand, handleClearQueueCommand } from './commands/queue.js';
|
|
@@ -38,6 +39,9 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
38
39
|
case 'add-project':
|
|
39
40
|
await handleAddProjectAutocomplete({ interaction, appId });
|
|
40
41
|
return;
|
|
42
|
+
case 'remove-project':
|
|
43
|
+
await handleRemoveProjectAutocomplete({ interaction, appId });
|
|
44
|
+
return;
|
|
41
45
|
default:
|
|
42
46
|
await interaction.respond([]);
|
|
43
47
|
return;
|
|
@@ -55,6 +59,9 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
55
59
|
case 'add-project':
|
|
56
60
|
await handleAddProjectCommand({ command: interaction, appId });
|
|
57
61
|
return;
|
|
62
|
+
case 'remove-project':
|
|
63
|
+
await handleRemoveProjectCommand({ command: interaction, appId });
|
|
64
|
+
return;
|
|
58
65
|
case 'create-new-project':
|
|
59
66
|
await handleCreateNewProjectCommand({ command: interaction, appId });
|
|
60
67
|
return;
|
package/dist/markdown.js
CHANGED
|
@@ -262,9 +262,7 @@ export async function getCompactSessionContext({ client, sessionId, includeSyste
|
|
|
262
262
|
lines.push('');
|
|
263
263
|
}
|
|
264
264
|
// Get tool calls in compact form (name + params only)
|
|
265
|
-
const toolParts = (msg.parts || []).filter((p) => p.type === 'tool' &&
|
|
266
|
-
'state' in p &&
|
|
267
|
-
p.state?.status === 'completed');
|
|
265
|
+
const toolParts = (msg.parts || []).filter((p) => p.type === 'tool' && 'state' in p && p.state?.status === 'completed');
|
|
268
266
|
for (const part of toolParts) {
|
|
269
267
|
if (part.type === 'tool' && 'tool' in part && 'state' in part) {
|
|
270
268
|
const toolName = part.tool;
|
|
@@ -74,7 +74,7 @@ export async function getTextAttachments(message) {
|
|
|
74
74
|
export async function getFileAttachments(message) {
|
|
75
75
|
const fileAttachments = Array.from(message.attachments.values()).filter((attachment) => {
|
|
76
76
|
const contentType = attachment.contentType || '';
|
|
77
|
-
return
|
|
77
|
+
return contentType.startsWith('image/') || contentType === 'application/pdf';
|
|
78
78
|
});
|
|
79
79
|
if (fileAttachments.length === 0) {
|
|
80
80
|
return [];
|
|
@@ -118,14 +118,18 @@ export function getToolSummaryText(part) {
|
|
|
118
118
|
const added = newString.split('\n').length;
|
|
119
119
|
const removed = oldString.split('\n').length;
|
|
120
120
|
const fileName = filePath.split('/').pop() || '';
|
|
121
|
-
return fileName
|
|
121
|
+
return fileName
|
|
122
|
+
? `*${escapeInlineMarkdown(fileName)}* (+${added}-${removed})`
|
|
123
|
+
: `(+${added}-${removed})`;
|
|
122
124
|
}
|
|
123
125
|
if (part.tool === 'write') {
|
|
124
126
|
const filePath = part.state.input?.filePath || '';
|
|
125
127
|
const content = part.state.input?.content || '';
|
|
126
128
|
const lines = content.split('\n').length;
|
|
127
129
|
const fileName = filePath.split('/').pop() || '';
|
|
128
|
-
return fileName
|
|
130
|
+
return fileName
|
|
131
|
+
? `*${escapeInlineMarkdown(fileName)}* (${lines} line${lines === 1 ? '' : 's'})`
|
|
132
|
+
: `(${lines} line${lines === 1 ? '' : 's'})`;
|
|
129
133
|
}
|
|
130
134
|
if (part.tool === 'webfetch') {
|
|
131
135
|
const url = part.state.input?.url || '';
|
package/dist/openai-realtime.js
CHANGED
|
@@ -64,7 +64,7 @@ function createWavHeader(dataLength, options) {
|
|
|
64
64
|
buffer.writeUInt32LE(dataLength, 40); // Subchunk2Size
|
|
65
65
|
return buffer;
|
|
66
66
|
}
|
|
67
|
-
function defaultAudioChunkHandler({ data, mimeType
|
|
67
|
+
function defaultAudioChunkHandler({ data, mimeType }) {
|
|
68
68
|
audioParts.push(data);
|
|
69
69
|
const fileName = 'audio.wav';
|
|
70
70
|
const buffer = convertToWav(audioParts, mimeType);
|
|
@@ -140,9 +140,7 @@ export async function startGenAiSession({ onAssistantAudioChunk, onAssistantStar
|
|
|
140
140
|
}
|
|
141
141
|
// Set up event handlers
|
|
142
142
|
client.on('conversation.item.created', ({ item }) => {
|
|
143
|
-
if ('role' in item &&
|
|
144
|
-
item.role === 'assistant' &&
|
|
145
|
-
item.type === 'message') {
|
|
143
|
+
if ('role' in item && item.role === 'assistant' && item.type === 'message') {
|
|
146
144
|
// Check if this is the first audio content
|
|
147
145
|
const hasAudio = 'content' in item &&
|
|
148
146
|
Array.isArray(item.content) &&
|
|
@@ -153,7 +151,7 @@ export async function startGenAiSession({ onAssistantAudioChunk, onAssistantStar
|
|
|
153
151
|
}
|
|
154
152
|
}
|
|
155
153
|
});
|
|
156
|
-
client.on('conversation.updated', ({ item, delta
|
|
154
|
+
client.on('conversation.updated', ({ item, delta }) => {
|
|
157
155
|
// Handle audio chunks
|
|
158
156
|
if (delta?.audio && 'role' in item && item.role === 'assistant') {
|
|
159
157
|
if (!isAssistantSpeaking && onAssistantStartSpeaking) {
|
package/dist/opencode.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import net from 'node:net';
|
|
7
|
-
import { createOpencodeClient
|
|
7
|
+
import { createOpencodeClient } from '@opencode-ai/sdk';
|
|
8
8
|
import { createOpencodeClient as createOpencodeClientV2, } from '@opencode-ai/sdk/v2';
|
|
9
9
|
import { createLogger } from './logger.js';
|
|
10
10
|
const opencodeLogger = createLogger('OPENCODE');
|
package/dist/session-handler.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// Creates, maintains, and sends prompts to OpenCode sessions from Discord threads.
|
|
3
3
|
// Handles streaming events, permissions, abort signals, and message queuing.
|
|
4
4
|
import prettyMilliseconds from 'pretty-ms';
|
|
5
|
-
import { getDatabase, getSessionModel, getChannelModel, getSessionAgent, getChannelAgent, setSessionAgent } from './database.js';
|
|
6
|
-
import { initializeOpencodeForDirectory, getOpencodeServers, getOpencodeClientV2 } from './opencode.js';
|
|
5
|
+
import { getDatabase, getSessionModel, getChannelModel, getSessionAgent, getChannelAgent, setSessionAgent, } from './database.js';
|
|
6
|
+
import { initializeOpencodeForDirectory, getOpencodeServers, getOpencodeClientV2, } from './opencode.js';
|
|
7
7
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from './discord-utils.js';
|
|
8
8
|
import { formatPart } from './message-formatting.js';
|
|
9
9
|
import { getOpencodeSystemMessage } from './system-message.js';
|
|
10
10
|
import { createLogger } from './logger.js';
|
|
11
11
|
import { isAbortError } from './utils.js';
|
|
12
|
-
import { showAskUserQuestionDropdowns, cancelPendingQuestion, pendingQuestionContexts } from './commands/ask-question.js';
|
|
12
|
+
import { showAskUserQuestionDropdowns, cancelPendingQuestion, pendingQuestionContexts, } from './commands/ask-question.js';
|
|
13
13
|
import { showPermissionDropdown, cleanupPermissionContext } from './commands/permissions.js';
|
|
14
14
|
const sessionLogger = createLogger('SESSION');
|
|
15
15
|
const voiceLogger = createLogger('VOICE');
|
|
@@ -55,7 +55,9 @@ export async function abortAndRetrySession({ sessionId, thread, projectDirectory
|
|
|
55
55
|
sessionLogger.log(`[ABORT+RETRY] API abort call failed (may already be done):`, e);
|
|
56
56
|
}
|
|
57
57
|
// Small delay to let the abort propagate
|
|
58
|
-
await new Promise((resolve) => {
|
|
58
|
+
await new Promise((resolve) => {
|
|
59
|
+
setTimeout(resolve, 300);
|
|
60
|
+
});
|
|
59
61
|
// Fetch last user message from API
|
|
60
62
|
sessionLogger.log(`[ABORT+RETRY] Fetching last user message for session ${sessionId}`);
|
|
61
63
|
const messagesResponse = await getClient().session.messages({ path: { id: sessionId } });
|
|
@@ -167,7 +169,9 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
167
169
|
const abortController = new AbortController();
|
|
168
170
|
abortControllers.set(session.id, abortController);
|
|
169
171
|
if (existingController) {
|
|
170
|
-
await new Promise((resolve) => {
|
|
172
|
+
await new Promise((resolve) => {
|
|
173
|
+
setTimeout(resolve, 200);
|
|
174
|
+
});
|
|
171
175
|
if (abortController.signal.aborted) {
|
|
172
176
|
sessionLogger.log(`[DEBOUNCE] Request was superseded during wait, exiting`);
|
|
173
177
|
return;
|
|
@@ -191,8 +195,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
191
195
|
sessionLogger.log(`Subscribed to OpenCode events`);
|
|
192
196
|
const sentPartIds = new Set(getDatabase()
|
|
193
197
|
.prepare('SELECT part_id FROM part_messages WHERE thread_id = ?')
|
|
194
|
-
.all(thread.id)
|
|
195
|
-
.map((row) => row.part_id));
|
|
198
|
+
.all(thread.id).map((row) => row.part_id));
|
|
196
199
|
let currentParts = [];
|
|
197
200
|
let stopTyping = null;
|
|
198
201
|
let usedModel;
|
|
@@ -264,7 +267,11 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
264
267
|
continue;
|
|
265
268
|
}
|
|
266
269
|
if (msg.role === 'assistant') {
|
|
267
|
-
const newTokensTotal = msg.tokens.input +
|
|
270
|
+
const newTokensTotal = msg.tokens.input +
|
|
271
|
+
msg.tokens.output +
|
|
272
|
+
msg.tokens.reasoning +
|
|
273
|
+
msg.tokens.cache.read +
|
|
274
|
+
msg.tokens.cache.write;
|
|
268
275
|
if (newTokensTotal > 0) {
|
|
269
276
|
tokensUsedInSession = newTokensTotal;
|
|
270
277
|
}
|
|
@@ -275,7 +282,9 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
275
282
|
if (tokensUsedInSession > 0 && usedProviderID && usedModel) {
|
|
276
283
|
if (!modelContextLimit) {
|
|
277
284
|
try {
|
|
278
|
-
const providersResponse = await getClient().provider.list({
|
|
285
|
+
const providersResponse = await getClient().provider.list({
|
|
286
|
+
query: { directory },
|
|
287
|
+
});
|
|
279
288
|
const provider = providersResponse.data?.all?.find((p) => p.id === usedProviderID);
|
|
280
289
|
const model = provider?.models?.[usedModel];
|
|
281
290
|
if (model?.limit?.context) {
|
|
@@ -337,9 +346,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
337
346
|
const outputTokens = Math.ceil(output.length / 4);
|
|
338
347
|
const LARGE_OUTPUT_THRESHOLD = 3000;
|
|
339
348
|
if (outputTokens >= LARGE_OUTPUT_THRESHOLD) {
|
|
340
|
-
const formattedTokens = outputTokens >= 1000
|
|
341
|
-
? `${(outputTokens / 1000).toFixed(1)}k`
|
|
342
|
-
: String(outputTokens);
|
|
349
|
+
const formattedTokens = outputTokens >= 1000 ? `${(outputTokens / 1000).toFixed(1)}k` : String(outputTokens);
|
|
343
350
|
const percentageSuffix = (() => {
|
|
344
351
|
if (!modelContextLimit) {
|
|
345
352
|
return '';
|
|
@@ -499,8 +506,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
499
506
|
stopTyping();
|
|
500
507
|
stopTyping = null;
|
|
501
508
|
}
|
|
502
|
-
if (!abortController.signal.aborted ||
|
|
503
|
-
abortController.signal.reason === 'finished') {
|
|
509
|
+
if (!abortController.signal.aborted || abortController.signal.reason === 'finished') {
|
|
504
510
|
const sessionDuration = prettyMilliseconds(Date.now() - sessionStartTime);
|
|
505
511
|
const attachCommand = port ? ` ⋅ ${session.id}` : '';
|
|
506
512
|
const modelInfo = usedModel ? ` ⋅ ${usedModel}` : '';
|
|
@@ -565,7 +571,11 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
565
571
|
if (images.length === 0) {
|
|
566
572
|
return prompt;
|
|
567
573
|
}
|
|
568
|
-
sessionLogger.log(`[PROMPT] Sending ${images.length} image(s):`, images.map((img) => ({
|
|
574
|
+
sessionLogger.log(`[PROMPT] Sending ${images.length} image(s):`, images.map((img) => ({
|
|
575
|
+
mime: img.mime,
|
|
576
|
+
filename: img.filename,
|
|
577
|
+
url: img.url.slice(0, 100),
|
|
578
|
+
})));
|
|
569
579
|
const imagePathsList = images.map((img) => `- ${img.filename}: ${img.url}`).join('\n');
|
|
570
580
|
return `${prompt}\n\n**attached images:**\n${imagePathsList}`;
|
|
571
581
|
})();
|
package/dist/system-message.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// OpenCode system prompt generator.
|
|
2
2
|
// Creates the system message injected into every OpenCode session,
|
|
3
3
|
// including Discord-specific formatting rules, diff commands, and permissions info.
|
|
4
|
-
export function getOpencodeSystemMessage({ sessionId, channelId }) {
|
|
4
|
+
export function getOpencodeSystemMessage({ sessionId, channelId, }) {
|
|
5
5
|
return `
|
|
6
6
|
The user is reading your messages from inside Discord, via kimaki.xyz
|
|
7
7
|
|
|
@@ -22,7 +22,8 @@ Only users with these Discord permissions can send messages to the bot:
|
|
|
22
22
|
To upload files to the Discord thread (images, screenshots, long files that would clutter the chat), run:
|
|
23
23
|
|
|
24
24
|
npx -y kimaki upload-to-discord --session ${sessionId} <file1> [file2] ...
|
|
25
|
-
${channelId
|
|
25
|
+
${channelId
|
|
26
|
+
? `
|
|
26
27
|
## starting new sessions from CLI
|
|
27
28
|
|
|
28
29
|
To start a new thread/session in this channel programmatically, run:
|
|
@@ -30,7 +31,8 @@ To start a new thread/session in this channel programmatically, run:
|
|
|
30
31
|
npx -y kimaki start-session --channel ${channelId} --prompt "your prompt here"
|
|
31
32
|
|
|
32
33
|
This is useful for automation (cron jobs, GitHub webhooks, n8n, etc.)
|
|
33
|
-
`
|
|
34
|
+
`
|
|
35
|
+
: ''}
|
|
34
36
|
## showing diffs
|
|
35
37
|
|
|
36
38
|
IMPORTANT: After editing any files, you MUST execute the critique command using the Bash tool to get a diff URL, then share that URL with the user.
|
package/dist/tools.js
CHANGED
|
@@ -11,7 +11,7 @@ const toolsLogger = createLogger('TOOLS');
|
|
|
11
11
|
import { ShareMarkdown } from './markdown.js';
|
|
12
12
|
import { formatDistanceToNow } from './utils.js';
|
|
13
13
|
import pc from 'picocolors';
|
|
14
|
-
import { initializeOpencodeForDirectory, getOpencodeSystemMessage
|
|
14
|
+
import { initializeOpencodeForDirectory, getOpencodeSystemMessage } from './discord-bot.js';
|
|
15
15
|
export async function getTools({ onMessageCompleted, directory, }) {
|
|
16
16
|
const getClient = await initializeOpencodeForDirectory(directory);
|
|
17
17
|
const client = getClient();
|
|
@@ -83,23 +83,17 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
83
83
|
createNewChat: tool({
|
|
84
84
|
description: 'Start a new chat session with an initial message. Does not wait for the message to complete',
|
|
85
85
|
inputSchema: z.object({
|
|
86
|
-
message: z
|
|
87
|
-
.string()
|
|
88
|
-
.describe('The initial message to start the chat with'),
|
|
86
|
+
message: z.string().describe('The initial message to start the chat with'),
|
|
89
87
|
title: z.string().optional().describe('Optional title for the session'),
|
|
90
88
|
model: z
|
|
91
89
|
.object({
|
|
92
|
-
providerId: z
|
|
93
|
-
|
|
94
|
-
.describe('The provider ID (e.g., "anthropic", "openai")'),
|
|
95
|
-
modelId: z
|
|
96
|
-
.string()
|
|
97
|
-
.describe('The model ID (e.g., "claude-opus-4-20250514", "gpt-5")'),
|
|
90
|
+
providerId: z.string().describe('The provider ID (e.g., "anthropic", "openai")'),
|
|
91
|
+
modelId: z.string().describe('The model ID (e.g., "claude-opus-4-20250514", "gpt-5")'),
|
|
98
92
|
})
|
|
99
93
|
.optional()
|
|
100
94
|
.describe('Optional model to use for this session'),
|
|
101
95
|
}),
|
|
102
|
-
execute: async ({ message, title
|
|
96
|
+
execute: async ({ message, title }) => {
|
|
103
97
|
if (!message.trim()) {
|
|
104
98
|
throw new Error(`message must be a non empty string`);
|
|
105
99
|
}
|
|
@@ -149,9 +143,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
149
143
|
catch (error) {
|
|
150
144
|
return {
|
|
151
145
|
success: false,
|
|
152
|
-
error: error instanceof Error
|
|
153
|
-
? error.message
|
|
154
|
-
: 'Failed to create chat session',
|
|
146
|
+
error: error instanceof Error ? error.message : 'Failed to create chat session',
|
|
155
147
|
};
|
|
156
148
|
}
|
|
157
149
|
},
|
|
@@ -180,8 +172,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
180
172
|
});
|
|
181
173
|
const messages = messagesResponse.data || [];
|
|
182
174
|
const lastMessage = messages[messages.length - 1];
|
|
183
|
-
if (lastMessage?.info.role === 'assistant' &&
|
|
184
|
-
!lastMessage.info.time.completed) {
|
|
175
|
+
if (lastMessage?.info.role === 'assistant' && !lastMessage.info.time.completed) {
|
|
185
176
|
return 'in_progress';
|
|
186
177
|
}
|
|
187
178
|
return 'finished';
|
|
@@ -228,10 +219,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
228
219
|
description: 'Read messages from a chat session',
|
|
229
220
|
inputSchema: z.object({
|
|
230
221
|
sessionId: z.string().describe('The session ID to read messages from'),
|
|
231
|
-
lastAssistantOnly: z
|
|
232
|
-
.boolean()
|
|
233
|
-
.optional()
|
|
234
|
-
.describe('Only read the last assistant message'),
|
|
222
|
+
lastAssistantOnly: z.boolean().optional().describe('Only read the last assistant message'),
|
|
235
223
|
}),
|
|
236
224
|
execute: async ({ sessionId, lastAssistantOnly = false }) => {
|
|
237
225
|
if (lastAssistantOnly) {
|
|
@@ -249,8 +237,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
249
237
|
};
|
|
250
238
|
}
|
|
251
239
|
const lastMessage = assistantMessages[assistantMessages.length - 1];
|
|
252
|
-
const status = 'completed' in lastMessage.info.time &&
|
|
253
|
-
lastMessage.info.time.completed
|
|
240
|
+
const status = 'completed' in lastMessage.info.time && lastMessage.info.time.completed
|
|
254
241
|
? 'completed'
|
|
255
242
|
: 'in_progress';
|
|
256
243
|
const markdown = await markdownRenderer.generate({
|
package/dist/voice-handler.js
CHANGED
|
@@ -13,7 +13,7 @@ import dedent from 'string-dedent';
|
|
|
13
13
|
import { PermissionsBitField, Events, } from 'discord.js';
|
|
14
14
|
import { createGenAIWorker } from './genai-worker-wrapper.js';
|
|
15
15
|
import { getDatabase } from './database.js';
|
|
16
|
-
import { sendThreadMessage, escapeDiscordFormatting, SILENT_MESSAGE_FLAGS } from './discord-utils.js';
|
|
16
|
+
import { sendThreadMessage, escapeDiscordFormatting, SILENT_MESSAGE_FLAGS, } from './discord-utils.js';
|
|
17
17
|
import { transcribeAudio } from './voice.js';
|
|
18
18
|
import { createLogger } from './logger.js';
|
|
19
19
|
const voiceLogger = createLogger('VOICE');
|
|
@@ -189,7 +189,10 @@ export async function setupVoiceHandling({ connection, guildId, channelId, appId
|
|
|
189
189
|
try {
|
|
190
190
|
const textChannel = await discordClient.channels.fetch(textChannelRow.channel_id);
|
|
191
191
|
if (textChannel?.isTextBased() && 'send' in textChannel) {
|
|
192
|
-
await textChannel.send({
|
|
192
|
+
await textChannel.send({
|
|
193
|
+
content: `⚠️ Voice session error: ${error}`,
|
|
194
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
195
|
+
});
|
|
193
196
|
}
|
|
194
197
|
}
|
|
195
198
|
catch (e) {
|
|
@@ -235,10 +238,7 @@ export async function setupVoiceHandling({ connection, guildId, channelId, appId
|
|
|
235
238
|
},
|
|
236
239
|
});
|
|
237
240
|
const framer = frameMono16khz();
|
|
238
|
-
const pipeline = audioStream
|
|
239
|
-
.pipe(decoder)
|
|
240
|
-
.pipe(downsampleTransform)
|
|
241
|
-
.pipe(framer);
|
|
241
|
+
const pipeline = audioStream.pipe(decoder).pipe(downsampleTransform).pipe(framer);
|
|
242
242
|
pipeline
|
|
243
243
|
.on('data', (frame) => {
|
|
244
244
|
if (currentSessionCount !== speakingSessionCount) {
|
|
@@ -404,8 +404,7 @@ export function registerVoiceStateHandler({ discordClient, appId, }) {
|
|
|
404
404
|
voiceLogger.log(`Admin user ${member.user.tag} left voice channel: ${oldState.channel?.name}`);
|
|
405
405
|
const guildId = guild.id;
|
|
406
406
|
const voiceData = voiceConnections.get(guildId);
|
|
407
|
-
if (voiceData &&
|
|
408
|
-
voiceData.connection.joinConfig.channelId === oldState.channelId) {
|
|
407
|
+
if (voiceData && voiceData.connection.joinConfig.channelId === oldState.channelId) {
|
|
409
408
|
const voiceChannel = oldState.channel;
|
|
410
409
|
if (!voiceChannel)
|
|
411
410
|
return;
|
|
@@ -433,8 +432,7 @@ export function registerVoiceStateHandler({ discordClient, appId, }) {
|
|
|
433
432
|
voiceLogger.log(`Admin user ${member.user.tag} moved from ${oldState.channel?.name} to ${newState.channel?.name}`);
|
|
434
433
|
const guildId = guild.id;
|
|
435
434
|
const voiceData = voiceConnections.get(guildId);
|
|
436
|
-
if (voiceData &&
|
|
437
|
-
voiceData.connection.joinConfig.channelId === oldState.channelId) {
|
|
435
|
+
if (voiceData && voiceData.connection.joinConfig.channelId === oldState.channelId) {
|
|
438
436
|
const oldVoiceChannel = oldState.channel;
|
|
439
437
|
if (oldVoiceChannel) {
|
|
440
438
|
const hasOtherAdmins = oldVoiceChannel.members.some((m) => {
|
|
@@ -472,8 +470,7 @@ export function registerVoiceStateHandler({ discordClient, appId, }) {
|
|
|
472
470
|
return;
|
|
473
471
|
const existingVoiceData = voiceConnections.get(newState.guild.id);
|
|
474
472
|
if (existingVoiceData &&
|
|
475
|
-
existingVoiceData.connection.state.status !==
|
|
476
|
-
VoiceConnectionStatus.Destroyed) {
|
|
473
|
+
existingVoiceData.connection.state.status !== VoiceConnectionStatus.Destroyed) {
|
|
477
474
|
voiceLogger.log(`Bot already connected to a voice channel in guild ${newState.guild.name}`);
|
|
478
475
|
if (existingVoiceData.connection.joinConfig.channelId !== voiceChannel.id) {
|
|
479
476
|
voiceLogger.log(`Moving bot from channel ${existingVoiceData.connection.joinConfig.channelId} to ${voiceChannel.id}`);
|
package/dist/voice.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Audio transcription service using Google Gemini.
|
|
2
2
|
// Transcribes voice messages with code-aware context, using grep/glob tools
|
|
3
3
|
// to verify technical terms, filenames, and function names in the codebase.
|
|
4
|
-
import { GoogleGenAI, Type
|
|
4
|
+
import { GoogleGenAI, Type } from '@google/genai';
|
|
5
5
|
import { createLogger } from './logger.js';
|
|
6
6
|
import { glob } from 'glob';
|
|
7
7
|
import { ripGrep } from 'ripgrep-js';
|
|
@@ -87,7 +87,7 @@ const transcriptionResultToolDeclaration = {
|
|
|
87
87
|
required: ['transcription'],
|
|
88
88
|
},
|
|
89
89
|
};
|
|
90
|
-
function createToolRunner({ directory
|
|
90
|
+
function createToolRunner({ directory }) {
|
|
91
91
|
const hasDirectory = directory && directory.trim().length > 0;
|
|
92
92
|
return async ({ name, args }) => {
|
|
93
93
|
if (name === 'transcriptionResult') {
|
|
@@ -200,7 +200,9 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
|
|
|
200
200
|
thinkingConfig: {
|
|
201
201
|
thinkingBudget: 512,
|
|
202
202
|
},
|
|
203
|
-
tools: stepsRemaining <= 0
|
|
203
|
+
tools: stepsRemaining <= 0
|
|
204
|
+
? [{ functionDeclarations: [transcriptionResultToolDeclaration] }]
|
|
205
|
+
: tools,
|
|
204
206
|
},
|
|
205
207
|
});
|
|
206
208
|
}
|
package/dist/xml.js
CHANGED
|
@@ -27,8 +27,7 @@ export function extractTagsArrays({ xml, tags, }) {
|
|
|
27
27
|
// Extract content using original string positions
|
|
28
28
|
const extractContent = () => {
|
|
29
29
|
// Use element's own indices but exclude the tags
|
|
30
|
-
if (element.startIndex !== null &&
|
|
31
|
-
element.endIndex !== null) {
|
|
30
|
+
if (element.startIndex !== null && element.endIndex !== null) {
|
|
32
31
|
// Extract the full element including tags
|
|
33
32
|
const fullElement = xml.substring(element.startIndex, element.endIndex + 1);
|
|
34
33
|
// Find where content starts (after opening tag)
|
|
@@ -57,8 +56,7 @@ export function extractTagsArrays({ xml, tags, }) {
|
|
|
57
56
|
findTags(element.children, currentPath);
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
|
-
else if (node.type === ElementType.Text &&
|
|
61
|
-
node.parent?.type === ElementType.Root) {
|
|
59
|
+
else if (node.type === ElementType.Text && node.parent?.type === ElementType.Root) {
|
|
62
60
|
const textNode = node;
|
|
63
61
|
if (textNode.data.trim()) {
|
|
64
62
|
// console.log('node.parent',node.parent)
|