kimaki 0.4.21 → 0.4.22

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/cli.js CHANGED
@@ -87,7 +87,7 @@ async function registerCommands(token, appId) {
87
87
  })
88
88
  .toJSON(),
89
89
  new SlashCommandBuilder()
90
- .setName('add-new-project')
90
+ .setName('create-new-project')
91
91
  .setDescription('Create a new project folder, initialize git, and start a session')
92
92
  .addStringOption((option) => {
93
93
  option
@@ -16,6 +16,7 @@ import * as prism from 'prism-media';
16
16
  import dedent from 'string-dedent';
17
17
  import { transcribeAudio } from './voice.js';
18
18
  import { extractTagsArrays, extractNonXmlContent } from './xml.js';
19
+ import { formatMarkdownTables } from './format-tables.js';
19
20
  import prettyMilliseconds from 'pretty-ms';
20
21
  import { createLogger } from './logger.js';
21
22
  import { isAbortError } from './utils.js';
@@ -564,6 +565,7 @@ async function getOpenPort() {
564
565
  */
565
566
  async function sendThreadMessage(thread, content) {
566
567
  const MAX_LENGTH = 2000;
568
+ content = formatMarkdownTables(content);
567
569
  content = escapeBackticksInCodeBlocks(content);
568
570
  const chunks = splitMarkdownForDiscord({ content, maxLength: MAX_LENGTH });
569
571
  if (chunks.length > 1) {
@@ -818,7 +820,7 @@ function escapeInlineCode(text) {
818
820
  .replace(/(?<!\\)`(?!`)/g, '\\`') // Single backticks (not already escaped or part of double/triple)
819
821
  .replace(/\|\|/g, '\\|\\|'); // Double pipes (spoiler syntax)
820
822
  }
821
- function resolveTextChannel(channel) {
823
+ async function resolveTextChannel(channel) {
822
824
  if (!channel) {
823
825
  return null;
824
826
  }
@@ -828,9 +830,12 @@ function resolveTextChannel(channel) {
828
830
  if (channel.type === ChannelType.PublicThread ||
829
831
  channel.type === ChannelType.PrivateThread ||
830
832
  channel.type === ChannelType.AnnouncementThread) {
831
- const parent = channel.parent;
832
- if (parent?.type === ChannelType.GuildText) {
833
- return parent;
833
+ const parentId = channel.parentId;
834
+ if (parentId) {
835
+ const parent = await channel.guild.channels.fetch(parentId);
836
+ if (parent?.type === ChannelType.GuildText) {
837
+ return parent;
838
+ }
834
839
  }
835
840
  }
836
841
  return null;
@@ -1003,9 +1008,17 @@ function getToolSummaryText(part) {
1003
1008
  const pattern = part.state.input?.pattern || '';
1004
1009
  return pattern ? `*${pattern}*` : '';
1005
1010
  }
1006
- if (part.tool === 'bash' || part.tool === 'task' || part.tool === 'todoread' || part.tool === 'todowrite') {
1011
+ if (part.tool === 'bash' || part.tool === 'todoread' || part.tool === 'todowrite') {
1007
1012
  return '';
1008
1013
  }
1014
+ if (part.tool === 'task') {
1015
+ const description = part.state.input?.description || '';
1016
+ return description ? `_${description}_` : '';
1017
+ }
1018
+ if (part.tool === 'skill') {
1019
+ const name = part.state.input?.name || '';
1020
+ return name ? `_${name}_` : '';
1021
+ }
1009
1022
  if (!part.state.input)
1010
1023
  return '';
1011
1024
  const inputFields = Object.entries(part.state.input)
@@ -1025,20 +1038,13 @@ function formatTodoList(part) {
1025
1038
  if (part.type !== 'tool' || part.tool !== 'todowrite')
1026
1039
  return '';
1027
1040
  const todos = part.state.input?.todos || [];
1028
- if (todos.length === 0)
1041
+ const activeIndex = todos.findIndex((todo) => {
1042
+ return todo.status === 'in_progress';
1043
+ });
1044
+ const activeTodo = todos[activeIndex];
1045
+ if (activeIndex === -1 || !activeTodo)
1029
1046
  return '';
1030
- return todos
1031
- .map((todo, i) => {
1032
- const num = `${i + 1}.`;
1033
- if (todo.status === 'in_progress') {
1034
- return `${num} **${todo.content}**`;
1035
- }
1036
- if (todo.status === 'completed' || todo.status === 'cancelled') {
1037
- return `${num} ~~${todo.content}~~`;
1038
- }
1039
- return `${num} ${todo.content}`;
1040
- })
1041
- .join('\n');
1047
+ return `${activeIndex + 1}. **${activeTodo.content}**`;
1042
1048
  }
1043
1049
  function formatPart(part) {
1044
1050
  if (part.type === 'text') {
@@ -1078,9 +1084,9 @@ function formatPart(part) {
1078
1084
  const command = part.state.input?.command || '';
1079
1085
  const description = part.state.input?.description || '';
1080
1086
  const isSingleLine = !command.includes('\n');
1081
- const hasBackticks = command.includes('`');
1082
- if (isSingleLine && !hasBackticks && command.length <= 50) {
1083
- toolTitle = `\`${command}\``;
1087
+ const hasUnderscores = command.includes('_');
1088
+ if (isSingleLine && !hasUnderscores && command.length <= 50) {
1089
+ toolTitle = `_${command}_`;
1084
1090
  }
1085
1091
  else if (description) {
1086
1092
  toolTitle = `_${description}_`;
@@ -1199,6 +1205,8 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
1199
1205
  let usedModel;
1200
1206
  let usedProviderID;
1201
1207
  let tokensUsedInSession = 0;
1208
+ let lastDisplayedContextPercentage = 0;
1209
+ let modelContextLimit;
1202
1210
  let typingInterval = null;
1203
1211
  function startTyping() {
1204
1212
  if (abortController.signal.aborted) {
@@ -1272,6 +1280,29 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
1272
1280
  assistantMessageId = msg.id;
1273
1281
  usedModel = msg.modelID;
1274
1282
  usedProviderID = msg.providerID;
1283
+ if (tokensUsedInSession > 0 && usedProviderID && usedModel) {
1284
+ if (!modelContextLimit) {
1285
+ try {
1286
+ const providersResponse = await getClient().provider.list({ query: { directory } });
1287
+ const provider = providersResponse.data?.all?.find((p) => p.id === usedProviderID);
1288
+ const model = provider?.models?.[usedModel];
1289
+ if (model?.limit?.context) {
1290
+ modelContextLimit = model.limit.context;
1291
+ }
1292
+ }
1293
+ catch (e) {
1294
+ sessionLogger.error('Failed to fetch provider info for context limit:', e);
1295
+ }
1296
+ }
1297
+ if (modelContextLimit) {
1298
+ const currentPercentage = Math.floor((tokensUsedInSession / modelContextLimit) * 100);
1299
+ const thresholdCrossed = Math.floor(currentPercentage / 10) * 10;
1300
+ if (thresholdCrossed > lastDisplayedContextPercentage && thresholdCrossed >= 10) {
1301
+ lastDisplayedContextPercentage = thresholdCrossed;
1302
+ await sendThreadMessage(thread, `◼︎ context usage ${currentPercentage}%`);
1303
+ }
1304
+ }
1305
+ }
1275
1306
  }
1276
1307
  }
1277
1308
  else if (event.type === 'message.part.updated') {
@@ -1774,9 +1805,8 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
1774
1805
  const focusedValue = interaction.options.getFocused();
1775
1806
  // Get the channel's project directory from its topic
1776
1807
  let projectDirectory;
1777
- if (interaction.channel &&
1778
- interaction.channel.type === ChannelType.GuildText) {
1779
- const textChannel = resolveTextChannel(interaction.channel);
1808
+ if (interaction.channel) {
1809
+ const textChannel = await resolveTextChannel(interaction.channel);
1780
1810
  if (textChannel) {
1781
1811
  const { projectDirectory: directory, channelAppId } = getKimakiMetadata(textChannel);
1782
1812
  if (channelAppId && channelAppId !== currentAppId) {
@@ -1839,9 +1869,8 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
1839
1869
  const currentQuery = (parts[parts.length - 1] || '').trim();
1840
1870
  // Get the channel's project directory from its topic
1841
1871
  let projectDirectory;
1842
- if (interaction.channel &&
1843
- interaction.channel.type === ChannelType.GuildText) {
1844
- const textChannel = resolveTextChannel(interaction.channel);
1872
+ if (interaction.channel) {
1873
+ const textChannel = await resolveTextChannel(interaction.channel);
1845
1874
  if (textChannel) {
1846
1875
  const { projectDirectory: directory, channelAppId } = getKimakiMetadata(textChannel);
1847
1876
  if (channelAppId && channelAppId !== currentAppId) {
@@ -2103,7 +2132,7 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
2103
2132
  if (partsToRender.length > 0) {
2104
2133
  const combinedContent = partsToRender
2105
2134
  .map((p) => p.content)
2106
- .join('\n\n');
2135
+ .join('\n');
2107
2136
  const discordMessage = await sendThreadMessage(thread, combinedContent);
2108
2137
  const stmt = getDatabase().prepare('INSERT OR REPLACE INTO part_messages (part_id, message_id, thread_id) VALUES (?, ?, ?)');
2109
2138
  const transaction = getDatabase().transaction((parts) => {
@@ -2168,7 +2197,7 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
2168
2197
  await command.editReply(`Failed to create channels: ${error instanceof Error ? error.message : 'Unknown error'}`);
2169
2198
  }
2170
2199
  }
2171
- else if (command.commandName === 'add-new-project') {
2200
+ else if (command.commandName === 'create-new-project') {
2172
2201
  await command.deferReply({ ephemeral: false });
2173
2202
  const projectName = command.options.getString('name', true);
2174
2203
  const guild = command.guild;
@@ -2364,7 +2393,7 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
2364
2393
  });
2365
2394
  return;
2366
2395
  }
2367
- const textChannel = resolveTextChannel(channel);
2396
+ const textChannel = await resolveTextChannel(channel);
2368
2397
  const { projectDirectory: directory } = getKimakiMetadata(textChannel);
2369
2398
  if (!directory) {
2370
2399
  await command.reply({
@@ -2426,7 +2455,7 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
2426
2455
  });
2427
2456
  return;
2428
2457
  }
2429
- const textChannel = resolveTextChannel(channel);
2458
+ const textChannel = await resolveTextChannel(channel);
2430
2459
  const { projectDirectory: directory } = getKimakiMetadata(textChannel);
2431
2460
  if (!directory) {
2432
2461
  await command.reply({
@@ -0,0 +1,93 @@
1
+ import { Lexer } from 'marked';
2
+ export function formatMarkdownTables(markdown) {
3
+ const lexer = new Lexer();
4
+ const tokens = lexer.lex(markdown);
5
+ let result = '';
6
+ for (const token of tokens) {
7
+ if (token.type === 'table') {
8
+ result += formatTableToken(token);
9
+ }
10
+ else {
11
+ result += token.raw;
12
+ }
13
+ }
14
+ return result;
15
+ }
16
+ function formatTableToken(table) {
17
+ const headers = table.header.map((cell) => {
18
+ return extractCellText(cell.tokens);
19
+ });
20
+ const rows = table.rows.map((row) => {
21
+ return row.map((cell) => {
22
+ return extractCellText(cell.tokens);
23
+ });
24
+ });
25
+ const columnWidths = calculateColumnWidths(headers, rows);
26
+ const lines = [];
27
+ lines.push(formatRow(headers, columnWidths));
28
+ lines.push(formatSeparator(columnWidths));
29
+ for (const row of rows) {
30
+ lines.push(formatRow(row, columnWidths));
31
+ }
32
+ return '```\n' + lines.join('\n') + '\n```\n';
33
+ }
34
+ function extractCellText(tokens) {
35
+ const parts = [];
36
+ for (const token of tokens) {
37
+ parts.push(extractTokenText(token));
38
+ }
39
+ return parts.join('').trim();
40
+ }
41
+ function extractTokenText(token) {
42
+ switch (token.type) {
43
+ case 'text':
44
+ case 'codespan':
45
+ case 'escape':
46
+ return token.text;
47
+ case 'link':
48
+ return token.href;
49
+ case 'image':
50
+ return token.href;
51
+ case 'strong':
52
+ case 'em':
53
+ case 'del':
54
+ return token.tokens ? extractCellText(token.tokens) : token.text;
55
+ case 'br':
56
+ return ' ';
57
+ default: {
58
+ const tokenAny = token;
59
+ if (tokenAny.tokens && Array.isArray(tokenAny.tokens)) {
60
+ return extractCellText(tokenAny.tokens);
61
+ }
62
+ if (typeof tokenAny.text === 'string') {
63
+ return tokenAny.text;
64
+ }
65
+ return '';
66
+ }
67
+ }
68
+ }
69
+ function calculateColumnWidths(headers, rows) {
70
+ const widths = headers.map((h) => {
71
+ return h.length;
72
+ });
73
+ for (const row of rows) {
74
+ for (let i = 0; i < row.length; i++) {
75
+ const cell = row[i] ?? '';
76
+ widths[i] = Math.max(widths[i] ?? 0, cell.length);
77
+ }
78
+ }
79
+ return widths;
80
+ }
81
+ function formatRow(cells, widths) {
82
+ const paddedCells = cells.map((cell, i) => {
83
+ return cell.padEnd(widths[i] ?? 0);
84
+ });
85
+ return paddedCells.join(' ');
86
+ }
87
+ function formatSeparator(widths) {
88
+ return widths
89
+ .map((w) => {
90
+ return '-'.repeat(w);
91
+ })
92
+ .join(' ');
93
+ }