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.
Files changed (75) hide show
  1. package/dist/ai-tool-to-genai.js +1 -3
  2. package/dist/channel-management.js +1 -1
  3. package/dist/cli.js +135 -39
  4. package/dist/commands/abort.js +1 -1
  5. package/dist/commands/add-project.js +1 -1
  6. package/dist/commands/agent.js +6 -2
  7. package/dist/commands/ask-question.js +2 -1
  8. package/dist/commands/fork.js +7 -7
  9. package/dist/commands/queue.js +2 -2
  10. package/dist/commands/remove-project.js +109 -0
  11. package/dist/commands/resume.js +3 -5
  12. package/dist/commands/session.js +2 -2
  13. package/dist/commands/share.js +1 -1
  14. package/dist/commands/undo-redo.js +2 -2
  15. package/dist/commands/user-command.js +3 -6
  16. package/dist/config.js +1 -1
  17. package/dist/discord-bot.js +4 -10
  18. package/dist/discord-utils.js +33 -9
  19. package/dist/genai.js +4 -6
  20. package/dist/interaction-handler.js +8 -1
  21. package/dist/markdown.js +1 -3
  22. package/dist/message-formatting.js +7 -3
  23. package/dist/openai-realtime.js +3 -5
  24. package/dist/opencode.js +1 -1
  25. package/dist/session-handler.js +25 -15
  26. package/dist/system-message.js +5 -3
  27. package/dist/tools.js +9 -22
  28. package/dist/voice-handler.js +9 -12
  29. package/dist/voice.js +5 -3
  30. package/dist/xml.js +2 -4
  31. package/package.json +3 -2
  32. package/src/__snapshots__/compact-session-context-no-system.md +24 -24
  33. package/src/__snapshots__/compact-session-context.md +31 -31
  34. package/src/ai-tool-to-genai.ts +3 -11
  35. package/src/channel-management.ts +14 -25
  36. package/src/cli.ts +282 -195
  37. package/src/commands/abort.ts +1 -3
  38. package/src/commands/add-project.ts +8 -14
  39. package/src/commands/agent.ts +16 -9
  40. package/src/commands/ask-question.ts +8 -7
  41. package/src/commands/create-new-project.ts +8 -14
  42. package/src/commands/fork.ts +23 -27
  43. package/src/commands/model.ts +14 -11
  44. package/src/commands/permissions.ts +1 -1
  45. package/src/commands/queue.ts +6 -19
  46. package/src/commands/remove-project.ts +136 -0
  47. package/src/commands/resume.ts +11 -30
  48. package/src/commands/session.ts +4 -13
  49. package/src/commands/share.ts +1 -3
  50. package/src/commands/types.ts +1 -3
  51. package/src/commands/undo-redo.ts +6 -18
  52. package/src/commands/user-command.ts +8 -10
  53. package/src/config.ts +5 -5
  54. package/src/database.ts +10 -8
  55. package/src/discord-bot.ts +22 -46
  56. package/src/discord-utils.ts +35 -18
  57. package/src/escape-backticks.test.ts +0 -2
  58. package/src/format-tables.ts +1 -4
  59. package/src/genai-worker-wrapper.ts +3 -9
  60. package/src/genai-worker.ts +4 -19
  61. package/src/genai.ts +10 -42
  62. package/src/interaction-handler.ts +133 -121
  63. package/src/markdown.test.ts +10 -32
  64. package/src/markdown.ts +6 -14
  65. package/src/message-formatting.ts +13 -14
  66. package/src/openai-realtime.ts +25 -47
  67. package/src/opencode.ts +24 -34
  68. package/src/session-handler.ts +91 -61
  69. package/src/system-message.ts +13 -3
  70. package/src/tools.ts +13 -39
  71. package/src/utils.ts +1 -4
  72. package/src/voice-handler.ts +34 -78
  73. package/src/voice.ts +11 -19
  74. package/src/xml.test.ts +1 -1
  75. 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 = ((hash << 5) - hash) + char;
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
@@ -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, } from './markdown.js';
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) {
@@ -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, } from 'discord.js';
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({ text: '```' + lang + '\n', inCodeBlock: false, lang, isOpeningFence: true, isClosingFence: false });
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({ text: codeLine + '\n', inCodeBlock: true, lang, isOpeningFence: false, isClosingFence: false });
49
+ lines.push({
50
+ text: codeLine + '\n',
51
+ inCodeBlock: true,
52
+ lang,
53
+ isOpeningFence: false,
54
+ isClosingFence: false,
55
+ });
44
56
  }
45
- lines.push({ text: '```\n', inCodeBlock: false, lang: '', isOpeningFence: false, isClosingFence: true });
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({ text, inCodeBlock: false, lang: '', isOpeningFence: false, isClosingFence: false });
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 ? ('```' + line.lang + '\n').length + '```\n'.length : 0;
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, } from '@google/genai';
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 (contentType.startsWith('image/') || contentType === 'application/pdf');
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 ? `*${escapeInlineMarkdown(fileName)}* (+${added}-${removed})` : `(+${added}-${removed})`;
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 ? `*${escapeInlineMarkdown(fileName)}* (${lines} line${lines === 1 ? '' : 's'})` : `(${lines} line${lines === 1 ? '' : 's'})`;
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 || '';
@@ -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, } from '@opencode-ai/sdk';
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');
@@ -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) => { setTimeout(resolve, 300); });
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) => { setTimeout(resolve, 200); });
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 + msg.tokens.output + msg.tokens.reasoning + msg.tokens.cache.read + msg.tokens.cache.write;
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({ query: { directory } });
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) => ({ mime: img.mime, filename: img.filename, url: img.url.slice(0, 100) })));
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
  })();
@@ -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, } from './discord-bot.js';
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
- .string()
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({
@@ -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({ content: `⚠️ Voice session error: ${error}`, flags: SILENT_MESSAGE_FLAGS });
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, } from '@google/genai';
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 ? [{ functionDeclarations: [transcriptionResultToolDeclaration] }] : tools,
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)