kimaki 0.4.12 → 0.4.13

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.
@@ -129,7 +129,7 @@ async function createUserAudioLogStream(guildId, channelId) {
129
129
  }
130
130
  }
131
131
  // Set up voice handling for a connection (called once per connection)
132
- async function setupVoiceHandling({ connection, guildId, channelId, appId, }) {
132
+ async function setupVoiceHandling({ connection, guildId, channelId, appId, discordClient, }) {
133
133
  voiceLogger.log(`Setting up voice handling for guild ${guildId}, channel ${channelId}`);
134
134
  // Check if this voice channel has an associated directory
135
135
  const channelDirRow = getDatabase()
@@ -227,8 +227,24 @@ async function setupVoiceHandling({ connection, guildId, channelId, appId, }) {
227
227
  : `<systemMessage>\nThe coding agent finished working on session ${params.sessionId}\n\nHere's what the assistant wrote:\n${params.markdown}\n</systemMessage>`;
228
228
  genAiWorker.sendTextInput(text);
229
229
  },
230
- onError(error) {
230
+ async onError(error) {
231
231
  voiceLogger.error('GenAI worker error:', error);
232
+ const textChannelRow = getDatabase()
233
+ .prepare(`SELECT cd2.channel_id FROM channel_directories cd1
234
+ JOIN channel_directories cd2 ON cd1.directory = cd2.directory
235
+ WHERE cd1.channel_id = ? AND cd1.channel_type = 'voice' AND cd2.channel_type = 'text'`)
236
+ .get(channelId);
237
+ if (textChannelRow) {
238
+ try {
239
+ const textChannel = await discordClient.channels.fetch(textChannelRow.channel_id);
240
+ if (textChannel?.isTextBased() && 'send' in textChannel) {
241
+ await textChannel.send(`⚠️ Voice session error: ${error}`);
242
+ }
243
+ }
244
+ catch (e) {
245
+ voiceLogger.error('Failed to send error to text channel:', e);
246
+ }
247
+ }
232
248
  },
233
249
  });
234
250
  // Stop any existing GenAI worker before storing new one
@@ -912,24 +928,27 @@ function getToolOutputToDisplay(part) {
912
928
  if (part.state.status === 'error') {
913
929
  return part.state.error || 'Unknown error';
914
930
  }
915
- if (part.tool === 'todowrite') {
916
- const todos = part.state.input?.todos || [];
917
- return todos
918
- .map((todo) => {
919
- let statusIcon = '▢';
920
- if (todo.status === 'in_progress') {
921
- statusIcon = '●';
922
- }
923
- if (todo.status === 'completed' || todo.status === 'cancelled') {
924
- statusIcon = '■';
925
- }
926
- return `\`${statusIcon}\` ${todo.content}`;
927
- })
928
- .filter(Boolean)
929
- .join('\n');
930
- }
931
931
  return '';
932
932
  }
933
+ function formatTodoList(part) {
934
+ if (part.type !== 'tool' || part.tool !== 'todowrite')
935
+ return '';
936
+ const todos = part.state.input?.todos || [];
937
+ if (todos.length === 0)
938
+ return '';
939
+ return todos
940
+ .map((todo, i) => {
941
+ const num = `${i + 1}.`;
942
+ if (todo.status === 'in_progress') {
943
+ return `${num} **${todo.content}**`;
944
+ }
945
+ if (todo.status === 'completed' || todo.status === 'cancelled') {
946
+ return `${num} ~~${todo.content}~~`;
947
+ }
948
+ return `${num} ${todo.content}`;
949
+ })
950
+ .join('\n');
951
+ }
933
952
  function formatPart(part) {
934
953
  if (part.type === 'text') {
935
954
  return part.text || '';
@@ -952,14 +971,31 @@ function formatPart(part) {
952
971
  return `◼︎ snapshot ${part.snapshot}`;
953
972
  }
954
973
  if (part.type === 'tool') {
974
+ if (part.tool === 'todowrite') {
975
+ return formatTodoList(part);
976
+ }
955
977
  if (part.state.status !== 'completed' && part.state.status !== 'error') {
956
978
  return '';
957
979
  }
958
980
  const summaryText = getToolSummaryText(part);
959
981
  const outputToDisplay = getToolOutputToDisplay(part);
960
- let toolTitle = part.state.status === 'completed' ? part.state.title || '' : 'error';
961
- if (toolTitle) {
962
- toolTitle = `*${toolTitle}*`;
982
+ let toolTitle = '';
983
+ if (part.state.status === 'error') {
984
+ toolTitle = 'error';
985
+ }
986
+ else if (part.tool === 'bash') {
987
+ const command = part.state.input?.command || '';
988
+ const isSingleLine = !command.includes('\n');
989
+ const hasBackticks = command.includes('`');
990
+ if (isSingleLine && command.length <= 120 && !hasBackticks) {
991
+ toolTitle = `\`${command}\``;
992
+ }
993
+ else {
994
+ toolTitle = part.state.title ? `*${part.state.title}*` : '';
995
+ }
996
+ }
997
+ else if (part.state.title) {
998
+ toolTitle = `*${part.state.title}*`;
963
999
  }
964
1000
  const icon = part.state.status === 'completed' ? '◼︎' : part.state.status === 'error' ? '⨯' : '';
965
1001
  const title = `${icon} ${part.tool} ${toolTitle} ${summaryText}`;
@@ -1937,52 +1973,37 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
1937
1973
  await command.editReply(`Resumed session "${sessionTitle}" in ${thread.toString()}`);
1938
1974
  // Send initial message to thread
1939
1975
  await sendThreadMessage(thread, `📂 **Resumed session:** ${sessionTitle}\n📅 **Created:** ${new Date(sessionResponse.data.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`);
1940
- // Render all existing messages
1941
- let messageCount = 0;
1976
+ // Collect all assistant parts first, then only render the last 30
1977
+ const allAssistantParts = [];
1942
1978
  for (const message of messages) {
1943
- if (message.info.role === 'user') {
1944
- // Render user messages
1945
- const userParts = message.parts.filter((p) => p.type === 'text' && !p.synthetic);
1946
- const userTexts = userParts
1947
- .map((p) => {
1948
- if (p.type === 'text') {
1949
- return p.text;
1950
- }
1951
- return '';
1952
- })
1953
- .filter((t) => t.trim());
1954
- const userText = userTexts.join('\n\n');
1955
- if (userText) {
1956
- // Escape backticks in user messages to prevent formatting issues
1957
- const escapedText = escapeDiscordFormatting(userText);
1958
- await sendThreadMessage(thread, `**User:**\n${escapedText}`);
1959
- }
1960
- }
1961
- else if (message.info.role === 'assistant') {
1962
- // Render assistant parts
1963
- const partsToRender = [];
1979
+ if (message.info.role === 'assistant') {
1964
1980
  for (const part of message.parts) {
1965
1981
  const content = formatPart(part);
1966
1982
  if (content.trim()) {
1967
- partsToRender.push({ id: part.id, content });
1983
+ allAssistantParts.push({ id: part.id, content });
1968
1984
  }
1969
1985
  }
1970
- if (partsToRender.length > 0) {
1971
- const combinedContent = partsToRender
1972
- .map((p) => p.content)
1973
- .join('\n\n');
1974
- const discordMessage = await sendThreadMessage(thread, combinedContent);
1975
- const stmt = getDatabase().prepare('INSERT OR REPLACE INTO part_messages (part_id, message_id, thread_id) VALUES (?, ?, ?)');
1976
- const transaction = getDatabase().transaction((parts) => {
1977
- for (const part of parts) {
1978
- stmt.run(part.id, discordMessage.id, thread.id);
1979
- }
1980
- });
1981
- transaction(partsToRender);
1982
- }
1983
1986
  }
1984
- messageCount++;
1985
1987
  }
1988
+ const partsToRender = allAssistantParts.slice(-30);
1989
+ const skippedCount = allAssistantParts.length - partsToRender.length;
1990
+ if (skippedCount > 0) {
1991
+ await sendThreadMessage(thread, `*Skipped ${skippedCount} older assistant parts...*`);
1992
+ }
1993
+ if (partsToRender.length > 0) {
1994
+ const combinedContent = partsToRender
1995
+ .map((p) => p.content)
1996
+ .join('\n\n');
1997
+ const discordMessage = await sendThreadMessage(thread, combinedContent);
1998
+ const stmt = getDatabase().prepare('INSERT OR REPLACE INTO part_messages (part_id, message_id, thread_id) VALUES (?, ?, ?)');
1999
+ const transaction = getDatabase().transaction((parts) => {
2000
+ for (const part of parts) {
2001
+ stmt.run(part.id, discordMessage.id, thread.id);
2002
+ }
2003
+ });
2004
+ transaction(partsToRender);
2005
+ }
2006
+ const messageCount = messages.length;
1986
2007
  await sendThreadMessage(thread, `✅ **Session resumed!** Loaded ${messageCount} messages.\n\nYou can now continue the conversation by sending messages in this thread.`);
1987
2008
  }
1988
2009
  catch (error) {
@@ -2321,6 +2342,7 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
2321
2342
  guildId: newState.guild.id,
2322
2343
  channelId: voiceChannel.id,
2323
2344
  appId: currentAppId,
2345
+ discordClient,
2324
2346
  });
2325
2347
  // Handle connection state changes
2326
2348
  connection.on(VoiceConnectionStatus.Disconnected, async () => {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.12",
5
+ "version": "0.4.13",
6
6
  "scripts": {
7
7
  "dev": "tsx --env-file .env src/cli.ts",
8
8
  "prepublishOnly": "pnpm tsc",
package/src/discordBot.ts CHANGED
@@ -213,11 +213,13 @@ async function setupVoiceHandling({
213
213
  guildId,
214
214
  channelId,
215
215
  appId,
216
+ discordClient,
216
217
  }: {
217
218
  connection: VoiceConnection
218
219
  guildId: string
219
220
  channelId: string
220
221
  appId: string
222
+ discordClient: Client
221
223
  }) {
222
224
  voiceLogger.log(
223
225
  `Setting up voice handling for guild ${guildId}, channel ${channelId}`,
@@ -330,8 +332,28 @@ async function setupVoiceHandling({
330
332
 
331
333
  genAiWorker.sendTextInput(text)
332
334
  },
333
- onError(error) {
335
+ async onError(error) {
334
336
  voiceLogger.error('GenAI worker error:', error)
337
+ const textChannelRow = getDatabase()
338
+ .prepare(
339
+ `SELECT cd2.channel_id FROM channel_directories cd1
340
+ JOIN channel_directories cd2 ON cd1.directory = cd2.directory
341
+ WHERE cd1.channel_id = ? AND cd1.channel_type = 'voice' AND cd2.channel_type = 'text'`,
342
+ )
343
+ .get(channelId) as { channel_id: string } | undefined
344
+
345
+ if (textChannelRow) {
346
+ try {
347
+ const textChannel = await discordClient.channels.fetch(
348
+ textChannelRow.channel_id,
349
+ )
350
+ if (textChannel?.isTextBased() && 'send' in textChannel) {
351
+ await textChannel.send(`⚠️ Voice session error: ${error}`)
352
+ }
353
+ } catch (e) {
354
+ voiceLogger.error('Failed to send error to text channel:', e)
355
+ }
356
+ }
335
357
  },
336
358
  })
337
359
 
@@ -1212,30 +1234,31 @@ function getToolOutputToDisplay(part: Part): string {
1212
1234
  return part.state.error || 'Unknown error'
1213
1235
  }
1214
1236
 
1215
- if (part.tool === 'todowrite') {
1216
- const todos =
1217
- (part.state.input?.todos as {
1218
- content: string
1219
- status: 'pending' | 'in_progress' | 'completed' | 'cancelled'
1220
- }[]) || []
1221
- return todos
1222
- .map((todo) => {
1223
- let statusIcon = '▢'
1224
- if (todo.status === 'in_progress') {
1225
- statusIcon = '●'
1226
- }
1227
- if (todo.status === 'completed' || todo.status === 'cancelled') {
1228
- statusIcon = '■'
1229
- }
1230
- return `\`${statusIcon}\` ${todo.content}`
1231
- })
1232
- .filter(Boolean)
1233
- .join('\n')
1234
- }
1235
-
1236
1237
  return ''
1237
1238
  }
1238
1239
 
1240
+ function formatTodoList(part: Part): string {
1241
+ if (part.type !== 'tool' || part.tool !== 'todowrite') return ''
1242
+ const todos =
1243
+ (part.state.input?.todos as {
1244
+ content: string
1245
+ status: 'pending' | 'in_progress' | 'completed' | 'cancelled'
1246
+ }[]) || []
1247
+ if (todos.length === 0) return ''
1248
+ return todos
1249
+ .map((todo, i) => {
1250
+ const num = `${i + 1}.`
1251
+ if (todo.status === 'in_progress') {
1252
+ return `${num} **${todo.content}**`
1253
+ }
1254
+ if (todo.status === 'completed' || todo.status === 'cancelled') {
1255
+ return `${num} ~~${todo.content}~~`
1256
+ }
1257
+ return `${num} ${todo.content}`
1258
+ })
1259
+ .join('\n')
1260
+ }
1261
+
1239
1262
  function formatPart(part: Part): string {
1240
1263
  if (part.type === 'text') {
1241
1264
  return part.text || ''
@@ -1263,6 +1286,10 @@ function formatPart(part: Part): string {
1263
1286
  }
1264
1287
 
1265
1288
  if (part.type === 'tool') {
1289
+ if (part.tool === 'todowrite') {
1290
+ return formatTodoList(part)
1291
+ }
1292
+
1266
1293
  if (part.state.status !== 'completed' && part.state.status !== 'error') {
1267
1294
  return ''
1268
1295
  }
@@ -1270,9 +1297,20 @@ function formatPart(part: Part): string {
1270
1297
  const summaryText = getToolSummaryText(part)
1271
1298
  const outputToDisplay = getToolOutputToDisplay(part)
1272
1299
 
1273
- let toolTitle = part.state.status === 'completed' ? part.state.title || '' : 'error'
1274
- if (toolTitle) {
1275
- toolTitle = `*${toolTitle}*`
1300
+ let toolTitle = ''
1301
+ if (part.state.status === 'error') {
1302
+ toolTitle = 'error'
1303
+ } else if (part.tool === 'bash') {
1304
+ const command = (part.state.input?.command as string) || ''
1305
+ const isSingleLine = !command.includes('\n')
1306
+ const hasBackticks = command.includes('`')
1307
+ if (isSingleLine && command.length <= 120 && !hasBackticks) {
1308
+ toolTitle = `\`${command}\``
1309
+ } else {
1310
+ toolTitle = part.state.title ? `*${part.state.title}*` : ''
1311
+ }
1312
+ } else if (part.state.title) {
1313
+ toolTitle = `*${part.state.title}*`
1276
1314
  }
1277
1315
 
1278
1316
  const icon = part.state.status === 'completed' ? '◼︎' : part.state.status === 'error' ? '⨯' : ''
@@ -2600,68 +2638,56 @@ export async function startDiscordBot({
2600
2638
  `📂 **Resumed session:** ${sessionTitle}\n📅 **Created:** ${new Date(sessionResponse.data.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`,
2601
2639
  )
2602
2640
 
2603
- // Render all existing messages
2604
- let messageCount = 0
2641
+ // Collect all assistant parts first, then only render the last 30
2642
+ const allAssistantParts: { id: string; content: string }[] = []
2605
2643
  for (const message of messages) {
2606
- if (message.info.role === 'user') {
2607
- // Render user messages
2608
- const userParts = message.parts.filter(
2609
- (p) => p.type === 'text' && !p.synthetic,
2610
- )
2611
- const userTexts = userParts
2612
- .map((p) => {
2613
- if (p.type === 'text') {
2614
- return p.text
2615
- }
2616
- return ''
2617
- })
2618
- .filter((t) => t.trim())
2619
-
2620
- const userText = userTexts.join('\n\n')
2621
- if (userText) {
2622
- // Escape backticks in user messages to prevent formatting issues
2623
- const escapedText = escapeDiscordFormatting(userText)
2624
- await sendThreadMessage(thread, `**User:**\n${escapedText}`)
2625
- }
2626
- } else if (message.info.role === 'assistant') {
2627
- // Render assistant parts
2628
- const partsToRender: { id: string; content: string }[] = []
2629
-
2644
+ if (message.info.role === 'assistant') {
2630
2645
  for (const part of message.parts) {
2631
2646
  const content = formatPart(part)
2632
2647
  if (content.trim()) {
2633
- partsToRender.push({ id: part.id, content })
2648
+ allAssistantParts.push({ id: part.id, content })
2634
2649
  }
2635
2650
  }
2651
+ }
2652
+ }
2636
2653
 
2637
- if (partsToRender.length > 0) {
2638
- const combinedContent = partsToRender
2639
- .map((p) => p.content)
2640
- .join('\n\n')
2654
+ const partsToRender = allAssistantParts.slice(-30)
2655
+ const skippedCount = allAssistantParts.length - partsToRender.length
2641
2656
 
2642
- const discordMessage = await sendThreadMessage(
2643
- thread,
2644
- combinedContent,
2645
- )
2657
+ if (skippedCount > 0) {
2658
+ await sendThreadMessage(
2659
+ thread,
2660
+ `*Skipped ${skippedCount} older assistant parts...*`,
2661
+ )
2662
+ }
2646
2663
 
2647
- const stmt = getDatabase().prepare(
2648
- 'INSERT OR REPLACE INTO part_messages (part_id, message_id, thread_id) VALUES (?, ?, ?)',
2649
- )
2664
+ if (partsToRender.length > 0) {
2665
+ const combinedContent = partsToRender
2666
+ .map((p) => p.content)
2667
+ .join('\n\n')
2650
2668
 
2651
- const transaction = getDatabase().transaction(
2652
- (parts: { id: string }[]) => {
2653
- for (const part of parts) {
2654
- stmt.run(part.id, discordMessage.id, thread.id)
2655
- }
2656
- },
2657
- )
2669
+ const discordMessage = await sendThreadMessage(
2670
+ thread,
2671
+ combinedContent,
2672
+ )
2658
2673
 
2659
- transaction(partsToRender)
2660
- }
2661
- }
2662
- messageCount++
2674
+ const stmt = getDatabase().prepare(
2675
+ 'INSERT OR REPLACE INTO part_messages (part_id, message_id, thread_id) VALUES (?, ?, ?)',
2676
+ )
2677
+
2678
+ const transaction = getDatabase().transaction(
2679
+ (parts: { id: string }[]) => {
2680
+ for (const part of parts) {
2681
+ stmt.run(part.id, discordMessage.id, thread.id)
2682
+ }
2683
+ },
2684
+ )
2685
+
2686
+ transaction(partsToRender)
2663
2687
  }
2664
2688
 
2689
+ const messageCount = messages.length
2690
+
2665
2691
  await sendThreadMessage(
2666
2692
  thread,
2667
2693
  `✅ **Session resumed!** Loaded ${messageCount} messages.\n\nYou can now continue the conversation by sending messages in this thread.`,
@@ -3101,6 +3127,7 @@ export async function startDiscordBot({
3101
3127
  guildId: newState.guild.id,
3102
3128
  channelId: voiceChannel.id,
3103
3129
  appId: currentAppId!,
3130
+ discordClient,
3104
3131
  })
3105
3132
 
3106
3133
  // Handle connection state changes