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/dist/commands/session.js
CHANGED
|
@@ -9,10 +9,11 @@ import { extractTagsArrays } from '../xml.js';
|
|
|
9
9
|
import { handleOpencodeSession } from '../session-handler.js';
|
|
10
10
|
import { createLogger } from '../logger.js';
|
|
11
11
|
const logger = createLogger('SESSION');
|
|
12
|
-
export async function handleSessionCommand({ command, appId
|
|
12
|
+
export async function handleSessionCommand({ command, appId }) {
|
|
13
13
|
await command.deferReply({ ephemeral: false });
|
|
14
14
|
const prompt = command.options.getString('prompt', true);
|
|
15
15
|
const filesString = command.options.getString('files') || '';
|
|
16
|
+
const agent = command.options.getString('agent') || undefined;
|
|
16
17
|
const channel = command.channel;
|
|
17
18
|
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
18
19
|
await command.editReply('This command can only be used in text channels');
|
|
@@ -66,6 +67,7 @@ export async function handleSessionCommand({ command, appId, }) {
|
|
|
66
67
|
thread,
|
|
67
68
|
projectDirectory,
|
|
68
69
|
channelId: textChannel.id,
|
|
70
|
+
agent,
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
catch (error) {
|
|
@@ -73,8 +75,61 @@ export async function handleSessionCommand({ command, appId, }) {
|
|
|
73
75
|
await command.editReply(`Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
74
76
|
}
|
|
75
77
|
}
|
|
78
|
+
async function handleAgentAutocomplete({ interaction, appId }) {
|
|
79
|
+
const focusedValue = interaction.options.getFocused();
|
|
80
|
+
let projectDirectory;
|
|
81
|
+
if (interaction.channel) {
|
|
82
|
+
const channel = interaction.channel;
|
|
83
|
+
if (channel.type === ChannelType.GuildText) {
|
|
84
|
+
const textChannel = channel;
|
|
85
|
+
if (textChannel.topic) {
|
|
86
|
+
const extracted = extractTagsArrays({
|
|
87
|
+
xml: textChannel.topic,
|
|
88
|
+
tags: ['kimaki.directory', 'kimaki.app'],
|
|
89
|
+
});
|
|
90
|
+
const channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
91
|
+
if (channelAppId && channelAppId !== appId) {
|
|
92
|
+
await interaction.respond([]);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!projectDirectory) {
|
|
100
|
+
await interaction.respond([]);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
105
|
+
const agentsResponse = await getClient().app.agents({
|
|
106
|
+
query: { directory: projectDirectory },
|
|
107
|
+
});
|
|
108
|
+
if (!agentsResponse.data || agentsResponse.data.length === 0) {
|
|
109
|
+
await interaction.respond([]);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const agents = agentsResponse.data
|
|
113
|
+
.filter((a) => a.mode === 'primary' || a.mode === 'all')
|
|
114
|
+
.filter((a) => a.name.toLowerCase().includes(focusedValue.toLowerCase()))
|
|
115
|
+
.slice(0, 25);
|
|
116
|
+
const choices = agents.map((agent) => ({
|
|
117
|
+
name: agent.name.slice(0, 100),
|
|
118
|
+
value: agent.name,
|
|
119
|
+
}));
|
|
120
|
+
await interaction.respond(choices);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
logger.error('[AUTOCOMPLETE] Error fetching agents:', error);
|
|
124
|
+
await interaction.respond([]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
76
127
|
export async function handleSessionAutocomplete({ interaction, appId, }) {
|
|
77
128
|
const focusedOption = interaction.options.getFocused(true);
|
|
129
|
+
if (focusedOption.name === 'agent') {
|
|
130
|
+
await handleAgentAutocomplete({ interaction, appId });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
78
133
|
if (focusedOption.name !== 'files') {
|
|
79
134
|
return;
|
|
80
135
|
}
|
package/dist/commands/share.js
CHANGED
|
@@ -5,7 +5,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
6
|
import { createLogger } from '../logger.js';
|
|
7
7
|
const logger = createLogger('SHARE');
|
|
8
|
-
export async function handleShareCommand({ command
|
|
8
|
+
export async function handleShareCommand({ command }) {
|
|
9
9
|
const channel = command.channel;
|
|
10
10
|
if (!channel) {
|
|
11
11
|
await command.reply({
|
|
@@ -5,7 +5,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
6
|
import { createLogger } from '../logger.js';
|
|
7
7
|
const logger = createLogger('UNDO-REDO');
|
|
8
|
-
export async function handleUndoCommand({ command
|
|
8
|
+
export async function handleUndoCommand({ command }) {
|
|
9
9
|
const channel = command.channel;
|
|
10
10
|
if (!channel) {
|
|
11
11
|
await command.reply({
|
|
@@ -88,7 +88,7 @@ export async function handleUndoCommand({ command, }) {
|
|
|
88
88
|
await command.editReply(`Failed to undo: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
export async function handleRedoCommand({ command
|
|
91
|
+
export async function handleRedoCommand({ command }) {
|
|
92
92
|
const channel = command.channel;
|
|
93
93
|
if (!channel) {
|
|
94
94
|
await command.reply({
|
|
@@ -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');
|
|
@@ -87,8 +87,7 @@ export async function initializeOpencodeForDirectory(directory) {
|
|
|
87
87
|
throw new Error(`Directory does not exist or is not accessible: ${directory}`);
|
|
88
88
|
}
|
|
89
89
|
const port = await getOpenPort();
|
|
90
|
-
const
|
|
91
|
-
const opencodeCommand = process.env.OPENCODE_PATH || `${opencodeBinDir}/opencode`;
|
|
90
|
+
const opencodeCommand = process.env.OPENCODE_PATH || 'opencode';
|
|
92
91
|
const serverProcess = spawn(opencodeCommand, ['serve', '--port', port.toString()], {
|
|
93
92
|
stdio: 'pipe',
|
|
94
93
|
detached: false,
|
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 } 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 } });
|
|
@@ -85,7 +87,7 @@ export async function abortAndRetrySession({ sessionId, thread, projectDirectory
|
|
|
85
87
|
});
|
|
86
88
|
return true;
|
|
87
89
|
}
|
|
88
|
-
export async function handleOpencodeSession({ prompt, thread, projectDirectory, originalMessage, images = [], channelId, command, }) {
|
|
90
|
+
export async function handleOpencodeSession({ prompt, thread, projectDirectory, originalMessage, images = [], channelId, command, agent, }) {
|
|
89
91
|
voiceLogger.log(`[OPENCODE SESSION] Starting for thread ${thread.id} with prompt: "${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}"`);
|
|
90
92
|
const sessionStartTime = Date.now();
|
|
91
93
|
const directory = projectDirectory || process.cwd();
|
|
@@ -127,6 +129,11 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
127
129
|
.prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
|
|
128
130
|
.run(thread.id, session.id);
|
|
129
131
|
sessionLogger.log(`Stored session ${session.id} for thread ${thread.id}`);
|
|
132
|
+
// Store agent preference if provided
|
|
133
|
+
if (agent) {
|
|
134
|
+
setSessionAgent(session.id, agent);
|
|
135
|
+
sessionLogger.log(`Set agent preference for session ${session.id}: ${agent}`);
|
|
136
|
+
}
|
|
130
137
|
const existingController = abortControllers.get(session.id);
|
|
131
138
|
if (existingController) {
|
|
132
139
|
voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`);
|
|
@@ -162,7 +169,9 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
162
169
|
const abortController = new AbortController();
|
|
163
170
|
abortControllers.set(session.id, abortController);
|
|
164
171
|
if (existingController) {
|
|
165
|
-
await new Promise((resolve) => {
|
|
172
|
+
await new Promise((resolve) => {
|
|
173
|
+
setTimeout(resolve, 200);
|
|
174
|
+
});
|
|
166
175
|
if (abortController.signal.aborted) {
|
|
167
176
|
sessionLogger.log(`[DEBOUNCE] Request was superseded during wait, exiting`);
|
|
168
177
|
return;
|
|
@@ -186,8 +195,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
186
195
|
sessionLogger.log(`Subscribed to OpenCode events`);
|
|
187
196
|
const sentPartIds = new Set(getDatabase()
|
|
188
197
|
.prepare('SELECT part_id FROM part_messages WHERE thread_id = ?')
|
|
189
|
-
.all(thread.id)
|
|
190
|
-
.map((row) => row.part_id));
|
|
198
|
+
.all(thread.id).map((row) => row.part_id));
|
|
191
199
|
let currentParts = [];
|
|
192
200
|
let stopTyping = null;
|
|
193
201
|
let usedModel;
|
|
@@ -259,7 +267,11 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
259
267
|
continue;
|
|
260
268
|
}
|
|
261
269
|
if (msg.role === 'assistant') {
|
|
262
|
-
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;
|
|
263
275
|
if (newTokensTotal > 0) {
|
|
264
276
|
tokensUsedInSession = newTokensTotal;
|
|
265
277
|
}
|
|
@@ -270,7 +282,9 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
270
282
|
if (tokensUsedInSession > 0 && usedProviderID && usedModel) {
|
|
271
283
|
if (!modelContextLimit) {
|
|
272
284
|
try {
|
|
273
|
-
const providersResponse = await getClient().provider.list({
|
|
285
|
+
const providersResponse = await getClient().provider.list({
|
|
286
|
+
query: { directory },
|
|
287
|
+
});
|
|
274
288
|
const provider = providersResponse.data?.all?.find((p) => p.id === usedProviderID);
|
|
275
289
|
const model = provider?.models?.[usedModel];
|
|
276
290
|
if (model?.limit?.context) {
|
|
@@ -332,9 +346,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
332
346
|
const outputTokens = Math.ceil(output.length / 4);
|
|
333
347
|
const LARGE_OUTPUT_THRESHOLD = 3000;
|
|
334
348
|
if (outputTokens >= LARGE_OUTPUT_THRESHOLD) {
|
|
335
|
-
const formattedTokens = outputTokens >= 1000
|
|
336
|
-
? `${(outputTokens / 1000).toFixed(1)}k`
|
|
337
|
-
: String(outputTokens);
|
|
349
|
+
const formattedTokens = outputTokens >= 1000 ? `${(outputTokens / 1000).toFixed(1)}k` : String(outputTokens);
|
|
338
350
|
const percentageSuffix = (() => {
|
|
339
351
|
if (!modelContextLimit) {
|
|
340
352
|
return '';
|
|
@@ -494,8 +506,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
494
506
|
stopTyping();
|
|
495
507
|
stopTyping = null;
|
|
496
508
|
}
|
|
497
|
-
if (!abortController.signal.aborted ||
|
|
498
|
-
abortController.signal.reason === 'finished') {
|
|
509
|
+
if (!abortController.signal.aborted || abortController.signal.reason === 'finished') {
|
|
499
510
|
const sessionDuration = prettyMilliseconds(Date.now() - sessionStartTime);
|
|
500
511
|
const attachCommand = port ? ` ⋅ ${session.id}` : '';
|
|
501
512
|
const modelInfo = usedModel ? ` ⋅ ${usedModel}` : '';
|
|
@@ -560,7 +571,11 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
560
571
|
if (images.length === 0) {
|
|
561
572
|
return prompt;
|
|
562
573
|
}
|
|
563
|
-
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
|
+
})));
|
|
564
579
|
const imagePathsList = images.map((img) => `- ${img.filename}: ${img.url}`).join('\n');
|
|
565
580
|
return `${prompt}\n\n**attached images:**\n${imagePathsList}`;
|
|
566
581
|
})();
|
|
@@ -648,15 +663,17 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
|
|
|
648
663
|
discordLogger.log(`Could not update reaction:`, e);
|
|
649
664
|
}
|
|
650
665
|
}
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
666
|
+
const errorDisplay = (() => {
|
|
667
|
+
if (error instanceof Error) {
|
|
668
|
+
const name = error.constructor.name || 'Error';
|
|
669
|
+
return `[${name}]\n${error.stack || error.message}`;
|
|
670
|
+
}
|
|
671
|
+
if (typeof error === 'string') {
|
|
672
|
+
return error;
|
|
673
|
+
}
|
|
674
|
+
return String(error);
|
|
675
|
+
})();
|
|
676
|
+
await sendThreadMessage(thread, `✗ Unexpected bot Error: ${errorDisplay}`);
|
|
660
677
|
}
|
|
661
678
|
}
|
|
662
679
|
}
|
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.
|