kimaki 0.4.35 → 0.4.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-tool-to-genai.js +1 -3
- package/dist/channel-management.js +5 -5
- package/dist/cli.js +182 -46
- package/dist/commands/abort.js +1 -1
- package/dist/commands/add-project.js +1 -1
- package/dist/commands/agent.js +6 -2
- package/dist/commands/ask-question.js +2 -1
- package/dist/commands/fork.js +7 -7
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +109 -0
- package/dist/commands/resume.js +3 -5
- package/dist/commands/session.js +2 -2
- package/dist/commands/share.js +1 -1
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +3 -6
- package/dist/config.js +1 -1
- package/dist/database.js +7 -0
- package/dist/discord-bot.js +37 -20
- package/dist/discord-utils.js +33 -9
- package/dist/genai.js +4 -6
- package/dist/interaction-handler.js +8 -1
- package/dist/markdown.js +1 -3
- package/dist/message-formatting.js +7 -3
- package/dist/openai-realtime.js +3 -5
- package/dist/opencode.js +1 -1
- package/dist/session-handler.js +25 -15
- package/dist/system-message.js +10 -4
- package/dist/tools.js +9 -22
- package/dist/voice-handler.js +9 -12
- package/dist/voice.js +5 -3
- package/dist/xml.js +2 -4
- package/package.json +3 -2
- package/src/__snapshots__/compact-session-context-no-system.md +24 -24
- package/src/__snapshots__/compact-session-context.md +31 -31
- package/src/ai-tool-to-genai.ts +3 -11
- package/src/channel-management.ts +18 -29
- package/src/cli.ts +334 -205
- package/src/commands/abort.ts +1 -3
- package/src/commands/add-project.ts +8 -14
- package/src/commands/agent.ts +16 -9
- package/src/commands/ask-question.ts +8 -7
- package/src/commands/create-new-project.ts +8 -14
- package/src/commands/fork.ts +23 -27
- package/src/commands/model.ts +14 -11
- package/src/commands/permissions.ts +1 -1
- package/src/commands/queue.ts +6 -19
- package/src/commands/remove-project.ts +136 -0
- package/src/commands/resume.ts +11 -30
- package/src/commands/session.ts +4 -13
- package/src/commands/share.ts +1 -3
- package/src/commands/types.ts +1 -3
- package/src/commands/undo-redo.ts +6 -18
- package/src/commands/user-command.ts +8 -10
- package/src/config.ts +5 -5
- package/src/database.ts +17 -8
- package/src/discord-bot.ts +60 -58
- package/src/discord-utils.ts +35 -18
- package/src/escape-backticks.test.ts +0 -2
- package/src/format-tables.ts +1 -4
- package/src/genai-worker-wrapper.ts +3 -9
- package/src/genai-worker.ts +4 -19
- package/src/genai.ts +10 -42
- package/src/interaction-handler.ts +133 -121
- package/src/markdown.test.ts +10 -32
- package/src/markdown.ts +6 -14
- package/src/message-formatting.ts +13 -14
- package/src/openai-realtime.ts +25 -47
- package/src/opencode.ts +24 -34
- package/src/session-handler.ts +91 -61
- package/src/system-message.ts +18 -4
- package/src/tools.ts +13 -39
- package/src/utils.ts +1 -4
- package/src/voice-handler.ts +34 -78
- package/src/voice.ts +11 -19
- package/src/xml.test.ts +1 -1
- package/src/xml.ts +3 -12
package/dist/commands/resume.js
CHANGED
|
@@ -3,12 +3,12 @@ import { ChannelType, ThreadAutoArchiveDuration, } from 'discord.js';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { getDatabase } from '../database.js';
|
|
5
5
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
|
-
import { sendThreadMessage, resolveTextChannel, getKimakiMetadata
|
|
6
|
+
import { sendThreadMessage, resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
7
7
|
import { extractTagsArrays } from '../xml.js';
|
|
8
8
|
import { collectLastAssistantParts } from '../message-formatting.js';
|
|
9
9
|
import { createLogger } from '../logger.js';
|
|
10
10
|
const logger = createLogger('RESUME');
|
|
11
|
-
export async function handleResumeCommand({ command, appId
|
|
11
|
+
export async function handleResumeCommand({ command, appId }) {
|
|
12
12
|
await command.deferReply({ ephemeral: false });
|
|
13
13
|
const sessionId = command.options.getString('session', true);
|
|
14
14
|
const channel = command.channel;
|
|
@@ -116,9 +116,7 @@ export async function handleResumeAutocomplete({ interaction, appId, }) {
|
|
|
116
116
|
await interaction.respond([]);
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
|
-
const existingSessionIds = new Set(getDatabase()
|
|
120
|
-
.prepare('SELECT session_id FROM thread_sessions')
|
|
121
|
-
.all().map((row) => row.session_id));
|
|
119
|
+
const existingSessionIds = new Set(getDatabase().prepare('SELECT session_id FROM thread_sessions').all().map((row) => row.session_id));
|
|
122
120
|
const sessions = sessionsResponse.data
|
|
123
121
|
.filter((session) => !existingSessionIds.has(session.id))
|
|
124
122
|
.filter((session) => session.title.toLowerCase().includes(focusedValue.toLowerCase()))
|
package/dist/commands/session.js
CHANGED
|
@@ -9,7 +9,7 @@ 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') || '';
|
|
@@ -75,7 +75,7 @@ export async function handleSessionCommand({ command, appId, }) {
|
|
|
75
75
|
await command.editReply(`Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
async function handleAgentAutocomplete({ interaction, appId
|
|
78
|
+
async function handleAgentAutocomplete({ interaction, appId }) {
|
|
79
79
|
const focusedValue = interaction.options.getFocused();
|
|
80
80
|
let projectDirectory;
|
|
81
81
|
if (interaction.channel) {
|
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/database.js
CHANGED
|
@@ -50,6 +50,13 @@ export function getDatabase() {
|
|
|
50
50
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
51
51
|
)
|
|
52
52
|
`);
|
|
53
|
+
// Migration: add app_id column to channel_directories for multi-bot support
|
|
54
|
+
try {
|
|
55
|
+
db.exec(`ALTER TABLE channel_directories ADD COLUMN app_id TEXT`);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Column already exists, ignore
|
|
59
|
+
}
|
|
53
60
|
db.exec(`
|
|
54
61
|
CREATE TABLE IF NOT EXISTS bot_api_keys (
|
|
55
62
|
app_id TEXT PRIMARY KEY,
|
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) {
|
|
@@ -125,14 +119,6 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
125
119
|
if (isThread) {
|
|
126
120
|
const thread = channel;
|
|
127
121
|
discordLogger.log(`Message in thread ${thread.name} (${thread.id})`);
|
|
128
|
-
const row = getDatabase()
|
|
129
|
-
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
130
|
-
.get(thread.id);
|
|
131
|
-
if (!row) {
|
|
132
|
-
discordLogger.log(`No session found for thread ${thread.id}`);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
voiceLogger.log(`[SESSION] Found session ${row.session_id} for thread ${thread.id}`);
|
|
136
122
|
const parent = thread.parent;
|
|
137
123
|
let projectDirectory;
|
|
138
124
|
let channelAppId;
|
|
@@ -156,6 +142,37 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
156
142
|
});
|
|
157
143
|
return;
|
|
158
144
|
}
|
|
145
|
+
const row = getDatabase()
|
|
146
|
+
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
147
|
+
.get(thread.id);
|
|
148
|
+
// No existing session - start a new one (e.g., replying to a notification thread)
|
|
149
|
+
if (!row) {
|
|
150
|
+
discordLogger.log(`No session for thread ${thread.id}, starting new session`);
|
|
151
|
+
if (!projectDirectory) {
|
|
152
|
+
discordLogger.log(`Cannot start session: no project directory for thread ${thread.id}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Include starter message (notification) as context for the session
|
|
156
|
+
let prompt = message.content;
|
|
157
|
+
const starterMessage = await thread.fetchStarterMessage().catch(() => null);
|
|
158
|
+
if (starterMessage?.content) {
|
|
159
|
+
// Strip notification prefix if present
|
|
160
|
+
const notificationContent = starterMessage.content
|
|
161
|
+
.replace(/^📢 \*\*Notification\*\*\n?/, '')
|
|
162
|
+
.trim();
|
|
163
|
+
if (notificationContent) {
|
|
164
|
+
prompt = `Context from notification:\n${notificationContent}\n\nUser request:\n${message.content}`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
await handleOpencodeSession({
|
|
168
|
+
prompt,
|
|
169
|
+
thread,
|
|
170
|
+
projectDirectory,
|
|
171
|
+
channelId: parent?.id || '',
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
voiceLogger.log(`[SESSION] Found session ${row.session_id} for thread ${thread.id}`);
|
|
159
176
|
let messageContent = message.content || '';
|
|
160
177
|
let currentSessionContext;
|
|
161
178
|
let lastSessionContext;
|
|
@@ -299,9 +316,9 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
299
316
|
}
|
|
300
317
|
}
|
|
301
318
|
});
|
|
302
|
-
// Magic prefix used by `kimaki
|
|
319
|
+
// Magic prefix used by `kimaki send` CLI command to initiate sessions
|
|
303
320
|
const BOT_SESSION_PREFIX = '🤖 **Bot-initiated session**';
|
|
304
|
-
// Handle bot-initiated threads created by `kimaki
|
|
321
|
+
// Handle bot-initiated threads created by `kimaki send`
|
|
305
322
|
discordClient.on(Events.ThreadCreate, async (thread, newlyCreated) => {
|
|
306
323
|
try {
|
|
307
324
|
if (!newlyCreated) {
|
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,15 +22,21 @@ 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:
|
|
29
30
|
|
|
30
|
-
npx -y kimaki
|
|
31
|
+
npx -y kimaki send --channel ${channelId} --prompt "your prompt here"
|
|
32
|
+
|
|
33
|
+
Use --notify-only to create a notification thread without starting an AI session:
|
|
34
|
+
|
|
35
|
+
npx -y kimaki send --channel ${channelId} --prompt "User cancelled subscription" --notify-only
|
|
31
36
|
|
|
32
37
|
This is useful for automation (cron jobs, GitHub webhooks, n8n, etc.)
|
|
33
|
-
`
|
|
38
|
+
`
|
|
39
|
+
: ''}
|
|
34
40
|
## showing diffs
|
|
35
41
|
|
|
36
42
|
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.
|