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
@@ -13,10 +13,7 @@ import { createLogger } from '../logger.js'
13
13
 
14
14
  const logger = createLogger('SESSION')
15
15
 
16
- export async function handleSessionCommand({
17
- command,
18
- appId,
19
- }: CommandContext): Promise<void> {
16
+ export async function handleSessionCommand({ command, appId }: CommandContext): Promise<void> {
20
17
  await command.deferReply({ ephemeral: false })
21
18
 
22
19
  const prompt = command.options.getString('prompt', true)
@@ -50,9 +47,7 @@ export async function handleSessionCommand({
50
47
  }
51
48
 
52
49
  if (!projectDirectory) {
53
- await command.editReply(
54
- 'This channel is not configured with a project directory',
55
- )
50
+ await command.editReply('This channel is not configured with a project directory')
56
51
  return
57
52
  }
58
53
 
@@ -102,10 +97,7 @@ export async function handleSessionCommand({
102
97
  }
103
98
  }
104
99
 
105
- async function handleAgentAutocomplete({
106
- interaction,
107
- appId,
108
- }: AutocompleteContext): Promise<void> {
100
+ async function handleAgentAutocomplete({ interaction, appId }: AutocompleteContext): Promise<void> {
109
101
  const focusedValue = interaction.options.getFocused()
110
102
 
111
103
  let projectDirectory: string | undefined
@@ -224,8 +216,7 @@ export async function handleSessionAutocomplete({
224
216
 
225
217
  const files = response.data || []
226
218
 
227
- const prefix =
228
- previousFiles.length > 0 ? previousFiles.join(', ') + ', ' : ''
219
+ const prefix = previousFiles.length > 0 ? previousFiles.join(', ') + ', ' : ''
229
220
 
230
221
  const choices = files
231
222
  .map((file: string) => {
@@ -9,9 +9,7 @@ import { createLogger } from '../logger.js'
9
9
 
10
10
  const logger = createLogger('SHARE')
11
11
 
12
- export async function handleShareCommand({
13
- command,
14
- }: CommandContext): Promise<void> {
12
+ export async function handleShareCommand({ command }: CommandContext): Promise<void> {
15
13
  const channel = command.channel
16
14
 
17
15
  if (!channel) {
@@ -20,6 +20,4 @@ export type AutocompleteContext = {
20
20
 
21
21
  export type AutocompleteHandler = (ctx: AutocompleteContext) => Promise<void>
22
22
 
23
- export type SelectMenuHandler = (
24
- interaction: StringSelectMenuInteraction,
25
- ) => Promise<void>
23
+ export type SelectMenuHandler = (interaction: StringSelectMenuInteraction) => Promise<void>
@@ -9,9 +9,7 @@ import { createLogger } from '../logger.js'
9
9
 
10
10
  const logger = createLogger('UNDO-REDO')
11
11
 
12
- export async function handleUndoCommand({
13
- command,
14
- }: CommandContext): Promise<void> {
12
+ export async function handleUndoCommand({ command }: CommandContext): Promise<void> {
15
13
  const channel = command.channel
16
14
 
17
15
  if (!channel) {
@@ -96,9 +94,7 @@ export async function handleUndoCommand({
96
94
  })
97
95
 
98
96
  if (response.error) {
99
- await command.editReply(
100
- `Failed to undo: ${JSON.stringify(response.error)}`,
101
- )
97
+ await command.editReply(`Failed to undo: ${JSON.stringify(response.error)}`)
102
98
  return
103
99
  }
104
100
 
@@ -106,12 +102,8 @@ export async function handleUndoCommand({
106
102
  ? `\n\`\`\`diff\n${response.data.revert.diff.slice(0, 1500)}\n\`\`\``
107
103
  : ''
108
104
 
109
- await command.editReply(
110
- `⏪ **Undone** - reverted last assistant message${diffInfo}`,
111
- )
112
- logger.log(
113
- `Session ${sessionId} reverted message ${lastAssistantMessage.info.id}`,
114
- )
105
+ await command.editReply(`⏪ **Undone** - reverted last assistant message${diffInfo}`)
106
+ logger.log(`Session ${sessionId} reverted message ${lastAssistantMessage.info.id}`)
115
107
  } catch (error) {
116
108
  logger.error('[UNDO] Error:', error)
117
109
  await command.editReply(
@@ -120,9 +112,7 @@ export async function handleUndoCommand({
120
112
  }
121
113
  }
122
114
 
123
- export async function handleRedoCommand({
124
- command,
125
- }: CommandContext): Promise<void> {
115
+ export async function handleRedoCommand({ command }: CommandContext): Promise<void> {
126
116
  const channel = command.channel
127
117
 
128
118
  if (!channel) {
@@ -196,9 +186,7 @@ export async function handleRedoCommand({
196
186
  })
197
187
 
198
188
  if (response.error) {
199
- await command.editReply(
200
- `Failed to redo: ${JSON.stringify(response.error)}`,
201
- )
189
+ await command.editReply(`Failed to redo: ${JSON.stringify(response.error)}`)
202
190
  return
203
191
  }
204
192
 
@@ -12,10 +12,7 @@ import fs from 'node:fs'
12
12
 
13
13
  const userCommandLogger = createLogger('USER_CMD')
14
14
 
15
- export const handleUserCommand: CommandHandler = async ({
16
- command,
17
- appId,
18
- }: CommandContext) => {
15
+ export const handleUserCommand: CommandHandler = async ({ command, appId }: CommandContext) => {
19
16
  const discordCommandName = command.commandName
20
17
  // Strip the -cmd suffix to get the actual OpenCode command name
21
18
  const commandName = discordCommandName.replace(/-cmd$/, '')
@@ -31,11 +28,11 @@ export const handleUserCommand: CommandHandler = async ({
31
28
  `Channel info: type=${channel?.type}, id=${channel?.id}, isNull=${channel === null}`,
32
29
  )
33
30
 
34
- const isThread = channel && [
35
- ChannelType.PublicThread,
36
- ChannelType.PrivateThread,
37
- ChannelType.AnnouncementThread,
38
- ].includes(channel.type)
31
+ const isThread =
32
+ channel &&
33
+ [ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.AnnouncementThread].includes(
34
+ channel.type,
35
+ )
39
36
 
40
37
  const isTextChannel = channel?.type === ChannelType.GuildText
41
38
 
@@ -64,7 +61,8 @@ export const handleUserCommand: CommandHandler = async ({
64
61
 
65
62
  if (!row) {
66
63
  await command.reply({
67
- content: 'This thread does not have an active session. Use this command in a project channel to create a new thread.',
64
+ content:
65
+ 'This thread does not have an active session. Use this command in a project channel to create a new thread.',
68
66
  ephemeral: true,
69
67
  })
70
68
  return
package/src/config.ts CHANGED
@@ -28,11 +28,11 @@ export function getDataDir(): string {
28
28
  */
29
29
  export function setDataDir(dir: string): void {
30
30
  const resolvedDir = path.resolve(dir)
31
-
31
+
32
32
  if (!fs.existsSync(resolvedDir)) {
33
33
  fs.mkdirSync(resolvedDir, { recursive: true })
34
34
  }
35
-
35
+
36
36
  dataDir = resolvedDir
37
37
  }
38
38
 
@@ -53,17 +53,17 @@ const DEFAULT_LOCK_PORT = 29988
53
53
  */
54
54
  export function getLockPort(): number {
55
55
  const dir = getDataDir()
56
-
56
+
57
57
  // Use original port for default data dir (backwards compatible)
58
58
  if (dir === DEFAULT_DATA_DIR) {
59
59
  return DEFAULT_LOCK_PORT
60
60
  }
61
-
61
+
62
62
  // Hash-based port for custom data dirs
63
63
  let hash = 0
64
64
  for (let i = 0; i < dir.length; i++) {
65
65
  const char = dir.charCodeAt(i)
66
- hash = ((hash << 5) - hash) + char
66
+ hash = (hash << 5) - hash + char
67
67
  hash = hash & hash // Convert to 32bit integer
68
68
  }
69
69
  // Map to port range 30000-39999
package/src/database.ts CHANGED
@@ -141,7 +141,7 @@ export function setChannelModel(channelId: string, modelId: string): void {
141
141
  db.prepare(
142
142
  `INSERT INTO channel_models (channel_id, model_id, updated_at)
143
143
  VALUES (?, ?, CURRENT_TIMESTAMP)
144
- ON CONFLICT(channel_id) DO UPDATE SET model_id = ?, updated_at = CURRENT_TIMESTAMP`
144
+ ON CONFLICT(channel_id) DO UPDATE SET model_id = ?, updated_at = CURRENT_TIMESTAMP`,
145
145
  ).run(channelId, modelId, modelId)
146
146
  }
147
147
 
@@ -163,9 +163,10 @@ export function getSessionModel(sessionId: string): string | undefined {
163
163
  */
164
164
  export function setSessionModel(sessionId: string, modelId: string): void {
165
165
  const db = getDatabase()
166
- db.prepare(
167
- `INSERT OR REPLACE INTO session_models (session_id, model_id) VALUES (?, ?)`
168
- ).run(sessionId, modelId)
166
+ db.prepare(`INSERT OR REPLACE INTO session_models (session_id, model_id) VALUES (?, ?)`).run(
167
+ sessionId,
168
+ modelId,
169
+ )
169
170
  }
170
171
 
171
172
  /**
@@ -187,7 +188,7 @@ export function setChannelAgent(channelId: string, agentName: string): void {
187
188
  db.prepare(
188
189
  `INSERT INTO channel_agents (channel_id, agent_name, updated_at)
189
190
  VALUES (?, ?, CURRENT_TIMESTAMP)
190
- ON CONFLICT(channel_id) DO UPDATE SET agent_name = ?, updated_at = CURRENT_TIMESTAMP`
191
+ ON CONFLICT(channel_id) DO UPDATE SET agent_name = ?, updated_at = CURRENT_TIMESTAMP`,
191
192
  ).run(channelId, agentName, agentName)
192
193
  }
193
194
 
@@ -207,9 +208,10 @@ export function getSessionAgent(sessionId: string): string | undefined {
207
208
  */
208
209
  export function setSessionAgent(sessionId: string, agentName: string): void {
209
210
  const db = getDatabase()
210
- db.prepare(
211
- `INSERT OR REPLACE INTO session_agents (session_id, agent_name) VALUES (?, ?)`
212
- ).run(sessionId, agentName)
211
+ db.prepare(`INSERT OR REPLACE INTO session_agents (session_id, agent_name) VALUES (?, ?)`).run(
212
+ sessionId,
213
+ agentName,
214
+ )
213
215
  }
214
216
 
215
217
  export function closeDatabase(): void {
@@ -24,10 +24,7 @@ import {
24
24
  processVoiceAttachment,
25
25
  registerVoiceStateHandler,
26
26
  } from './voice-handler.js'
27
- import {
28
- getCompactSessionContext,
29
- getLastSessionId,
30
- } from './markdown.js'
27
+ import { getCompactSessionContext, getLastSessionId } from './markdown.js'
31
28
  import { handleOpencodeSession } from './session-handler.js'
32
29
  import { registerInteractionHandler } from './interaction-handler.js'
33
30
 
@@ -35,7 +32,12 @@ export { getDatabase, closeDatabase } from './database.js'
35
32
  export { initializeOpencodeForDirectory } from './opencode.js'
36
33
  export { escapeBackticksInCodeBlocks, splitMarkdownForDiscord } from './discord-utils.js'
37
34
  export { getOpencodeSystemMessage } from './system-message.js'
38
- export { ensureKimakiCategory, ensureKimakiAudioCategory, createProjectChannels, getChannelsWithDescriptions } from './channel-management.js'
35
+ export {
36
+ ensureKimakiCategory,
37
+ ensureKimakiAudioCategory,
38
+ createProjectChannels,
39
+ getChannelsWithDescriptions,
40
+ } from './channel-management.js'
39
41
  export type { ChannelWithTags } from './channel-management.js'
40
42
 
41
43
  import {
@@ -73,12 +75,7 @@ export async function createDiscordClient() {
73
75
  GatewayIntentBits.MessageContent,
74
76
  GatewayIntentBits.GuildVoiceStates,
75
77
  ],
76
- partials: [
77
- Partials.Channel,
78
- Partials.Message,
79
- Partials.User,
80
- Partials.ThreadMember,
81
- ],
78
+ partials: [Partials.Channel, Partials.Message, Partials.User, Partials.ThreadMember],
82
79
  })
83
80
  }
84
81
 
@@ -116,15 +113,11 @@ export async function startDiscordBot({
116
113
 
117
114
  const channels = await getChannelsWithDescriptions(guild)
118
115
  const kimakiChannels = channels.filter(
119
- (ch) =>
120
- ch.kimakiDirectory &&
121
- (!ch.kimakiApp || ch.kimakiApp === currentAppId),
116
+ (ch) => ch.kimakiDirectory && (!ch.kimakiApp || ch.kimakiApp === currentAppId),
122
117
  )
123
118
 
124
119
  if (kimakiChannels.length > 0) {
125
- discordLogger.log(
126
- ` Found ${kimakiChannels.length} channel(s) for this bot:`,
127
- )
120
+ discordLogger.log(` Found ${kimakiChannels.length} channel(s) for this bot:`)
128
121
  for (const channel of kimakiChannels) {
129
122
  discordLogger.log(` - #${channel.name}: ${channel.kimakiDirectory}`)
130
123
  }
@@ -159,19 +152,14 @@ export async function startDiscordBot({
159
152
  try {
160
153
  await message.fetch()
161
154
  } catch (error) {
162
- discordLogger.log(
163
- `Failed to fetch partial message ${message.id}:`,
164
- error,
165
- )
155
+ discordLogger.log(`Failed to fetch partial message ${message.id}:`, error)
166
156
  return
167
157
  }
168
158
  }
169
159
 
170
160
  if (message.guild && message.member) {
171
161
  const isOwner = message.member.id === message.guild.ownerId
172
- const isAdmin = message.member.permissions.has(
173
- PermissionsBitField.Flags.Administrator,
174
- )
162
+ const isAdmin = message.member.permissions.has(PermissionsBitField.Flags.Administrator)
175
163
  const canManageServer = message.member.permissions.has(
176
164
  PermissionsBitField.Flags.ManageGuild,
177
165
  )
@@ -208,9 +196,7 @@ export async function startDiscordBot({
208
196
  return
209
197
  }
210
198
 
211
- voiceLogger.log(
212
- `[SESSION] Found session ${row.session_id} for thread ${thread.id}`,
213
- )
199
+ voiceLogger.log(`[SESSION] Found session ${row.session_id} for thread ${thread.id}`)
214
200
 
215
201
  const parent = thread.parent as TextChannel | null
216
202
  let projectDirectory: string | undefined
@@ -315,9 +301,7 @@ export async function startDiscordBot({
315
301
  )
316
302
 
317
303
  if (!textChannel.topic) {
318
- voiceLogger.log(
319
- `[IGNORED] Channel #${textChannel.name} has no description`,
320
- )
304
+ voiceLogger.log(`[IGNORED] Channel #${textChannel.name} has no description`)
321
305
  return
322
306
  }
323
307
 
@@ -330,9 +314,7 @@ export async function startDiscordBot({
330
314
  const channelAppId = extracted['kimaki.app']?.[0]?.trim()
331
315
 
332
316
  if (!projectDirectory) {
333
- voiceLogger.log(
334
- `[IGNORED] Channel #${textChannel.name} has no kimaki.directory tag`,
335
- )
317
+ voiceLogger.log(`[IGNORED] Channel #${textChannel.name} has no kimaki.directory tag`)
336
318
  return
337
319
  }
338
320
 
@@ -343,9 +325,7 @@ export async function startDiscordBot({
343
325
  return
344
326
  }
345
327
 
346
- discordLogger.log(
347
- `DIRECTORY: Found kimaki.directory: ${projectDirectory}`,
348
- )
328
+ discordLogger.log(`DIRECTORY: Found kimaki.directory: ${projectDirectory}`)
349
329
  if (channelAppId) {
350
330
  discordLogger.log(`APP: Channel app ID: ${channelAppId}`)
351
331
  }
@@ -359,9 +339,7 @@ export async function startDiscordBot({
359
339
  return
360
340
  }
361
341
 
362
- const hasVoice = message.attachments.some((a) =>
363
- a.contentType?.startsWith('audio/'),
364
- )
342
+ const hasVoice = message.attachments.some((a) => a.contentType?.startsWith('audio/'))
365
343
 
366
344
  const threadName = hasVoice
367
345
  ? 'Voice Message'
@@ -489,7 +467,9 @@ export async function startDiscordBot({
489
467
  return
490
468
  }
491
469
 
492
- discordLogger.log(`[BOT_SESSION] Starting session for thread ${thread.id} with prompt: "${prompt.slice(0, 50)}..."`)
470
+ discordLogger.log(
471
+ `[BOT_SESSION] Starting session for thread ${thread.id} with prompt: "${prompt.slice(0, 50)}..."`,
472
+ )
493
473
 
494
474
  await handleOpencodeSession({
495
475
  prompt,
@@ -522,9 +502,7 @@ export async function startDiscordBot({
522
502
  try {
523
503
  const cleanupPromises: Promise<void>[] = []
524
504
  for (const [guildId] of voiceConnections) {
525
- voiceLogger.log(
526
- `[SHUTDOWN] Cleaning up voice connection for guild ${guildId}`,
527
- )
505
+ voiceLogger.log(`[SHUTDOWN] Cleaning up voice connection for guild ${guildId}`)
528
506
  cleanupPromises.push(cleanupVoiceConnection(guildId))
529
507
  }
530
508
 
@@ -538,9 +516,7 @@ export async function startDiscordBot({
538
516
 
539
517
  for (const [dir, server] of getOpencodeServers()) {
540
518
  if (!server.process.killed) {
541
- voiceLogger.log(
542
- `[SHUTDOWN] Stopping OpenCode server on port ${server.port} for ${dir}`,
543
- )
519
+ voiceLogger.log(`[SHUTDOWN] Stopping OpenCode server on port ${server.port} for ${dir}`)
544
520
  server.process.kill('SIGTERM')
545
521
  }
546
522
  }
@@ -2,12 +2,7 @@
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
4
 
5
- import {
6
- ChannelType,
7
- type Message,
8
- type TextChannel,
9
- type ThreadChannel,
10
- } from 'discord.js'
5
+ import { ChannelType, type Message, type TextChannel, type ThreadChannel } from 'discord.js'
11
6
  import { Lexer } from 'marked'
12
7
  import { extractTagsArrays } from './xml.js'
13
8
  import { formatMarkdownTables } from './format-tables.js'
@@ -65,19 +60,43 @@ export function splitMarkdownForDiscord({
65
60
  for (const token of tokens) {
66
61
  if (token.type === 'code') {
67
62
  const lang = token.lang || ''
68
- lines.push({ text: '```' + lang + '\n', inCodeBlock: false, lang, isOpeningFence: true, isClosingFence: false })
63
+ lines.push({
64
+ text: '```' + lang + '\n',
65
+ inCodeBlock: false,
66
+ lang,
67
+ isOpeningFence: true,
68
+ isClosingFence: false,
69
+ })
69
70
  const codeLines = token.text.split('\n')
70
71
  for (const codeLine of codeLines) {
71
- lines.push({ text: codeLine + '\n', inCodeBlock: true, lang, isOpeningFence: false, isClosingFence: false })
72
+ lines.push({
73
+ text: codeLine + '\n',
74
+ inCodeBlock: true,
75
+ lang,
76
+ isOpeningFence: false,
77
+ isClosingFence: false,
78
+ })
72
79
  }
73
- lines.push({ text: '```\n', inCodeBlock: false, lang: '', isOpeningFence: false, isClosingFence: true })
80
+ lines.push({
81
+ text: '```\n',
82
+ inCodeBlock: false,
83
+ lang: '',
84
+ isOpeningFence: false,
85
+ isClosingFence: true,
86
+ })
74
87
  } else {
75
88
  const rawLines = token.raw.split('\n')
76
89
  for (let i = 0; i < rawLines.length; i++) {
77
90
  const isLast = i === rawLines.length - 1
78
91
  const text = isLast ? rawLines[i]! : rawLines[i]! + '\n'
79
92
  if (text) {
80
- lines.push({ text, inCodeBlock: false, lang: '', isOpeningFence: false, isClosingFence: false })
93
+ lines.push({
94
+ text,
95
+ inCodeBlock: false,
96
+ lang: '',
97
+ isOpeningFence: false,
98
+ isClosingFence: false,
99
+ })
81
100
  }
82
101
  }
83
102
  }
@@ -126,7 +145,9 @@ export function splitMarkdownForDiscord({
126
145
  }
127
146
 
128
147
  // calculate overhead for code block markers
129
- const codeBlockOverhead = line.inCodeBlock ? ('```' + line.lang + '\n').length + '```\n'.length : 0
148
+ const codeBlockOverhead = line.inCodeBlock
149
+ ? ('```' + line.lang + '\n').length + '```\n'.length
150
+ : 0
130
151
  // ensure at least 10 chars available, even if maxLength is very small
131
152
  const availablePerChunk = Math.max(10, maxLength - codeBlockOverhead - 50)
132
153
 
@@ -196,7 +217,7 @@ export function splitMarkdownForDiscord({
196
217
  export async function sendThreadMessage(
197
218
  thread: ThreadChannel,
198
219
  content: string,
199
- options?: { flags?: number }
220
+ options?: { flags?: number },
200
221
  ): Promise<Message> {
201
222
  const MAX_LENGTH = 2000
202
223
 
@@ -213,9 +234,7 @@ export async function sendThreadMessage(
213
234
  const chunks = splitMarkdownForDiscord({ content, maxLength: MAX_LENGTH })
214
235
 
215
236
  if (chunks.length > 1) {
216
- discordLogger.log(
217
- `MESSAGE: Splitting ${content.length} chars into ${chunks.length} messages`,
218
- )
237
+ discordLogger.log(`MESSAGE: Splitting ${content.length} chars into ${chunks.length} messages`)
219
238
  }
220
239
 
221
240
  let firstMessage: Message | undefined
@@ -262,9 +281,7 @@ export async function resolveTextChannel(
262
281
  }
263
282
 
264
283
  export function escapeDiscordFormatting(text: string): string {
265
- return text
266
- .replace(/```/g, '\\`\\`\\`')
267
- .replace(/````/g, '\\`\\`\\`\\`')
284
+ return text.replace(/```/g, '\\`\\`\\`').replace(/````/g, '\\`\\`\\`\\`')
268
285
  }
269
286
 
270
287
  export function getKimakiMetadata(textChannel: TextChannel | null): {
@@ -2,8 +2,6 @@ import { test, expect } from 'vitest'
2
2
  import { Lexer } from 'marked'
3
3
  import { escapeBackticksInCodeBlocks, splitMarkdownForDiscord } from './discord-utils.js'
4
4
 
5
-
6
-
7
5
  test('escapes single backticks in code blocks', () => {
8
6
  const input = '```js\nconst x = `hello`\n```'
9
7
  const result = escapeBackticksInCodeBlocks(input)
@@ -78,10 +78,7 @@ function extractTokenText(token: Token): string {
78
78
  }
79
79
  }
80
80
 
81
- function calculateColumnWidths(
82
- headers: string[],
83
- rows: string[][],
84
- ): number[] {
81
+ function calculateColumnWidths(headers: string[], rows: string[][]): number[] {
85
82
  const widths = headers.map((h) => {
86
83
  return h.length
87
84
  })
@@ -41,13 +41,9 @@ export interface GenAIWorker {
41
41
  stop(): Promise<void>
42
42
  }
43
43
 
44
- export function createGenAIWorker(
45
- options: GenAIWorkerOptions,
46
- ): Promise<GenAIWorker> {
44
+ export function createGenAIWorker(options: GenAIWorkerOptions): Promise<GenAIWorker> {
47
45
  return new Promise((resolve, reject) => {
48
- const worker = new Worker(
49
- new URL('../dist/genai-worker.js', import.meta.url),
50
- )
46
+ const worker = new Worker(new URL('../dist/genai-worker.js', import.meta.url))
51
47
 
52
48
  // Handle messages from worker
53
49
  worker.on('message', (message: WorkerOutMessage) => {
@@ -106,9 +102,7 @@ export function createGenAIWorker(
106
102
  worker.once('exit', (code) => {
107
103
  if (!resolved) {
108
104
  resolved = true
109
- genaiWrapperLogger.log(
110
- `[GENAI WORKER WRAPPER] Worker exited with code ${code}`,
111
- )
105
+ genaiWrapperLogger.log(`[GENAI WORKER WRAPPER] Worker exited with code ${code}`)
112
106
  resolve()
113
107
  }
114
108
  })
@@ -40,12 +40,7 @@ process.on('uncaughtException', (error) => {
40
40
  })
41
41
 
42
42
  process.on('unhandledRejection', (reason, promise) => {
43
- workerLogger.error(
44
- 'Unhandled rejection in worker:',
45
- reason,
46
- 'at promise:',
47
- promise,
48
- )
43
+ workerLogger.error('Unhandled rejection in worker:', reason, 'at promise:', promise)
49
44
  sendError(`Worker unhandled rejection: ${reason}`)
50
45
  })
51
46
 
@@ -130,12 +125,7 @@ async function createAssistantAudioLogStream(
130
125
  if (!process.env.DEBUG) return null
131
126
 
132
127
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
133
- const audioDir = path.join(
134
- process.cwd(),
135
- 'discord-audio-logs',
136
- guildId,
137
- channelId,
138
- )
128
+ const audioDir = path.join(process.cwd(), 'discord-audio-logs', guildId, channelId)
139
129
 
140
130
  try {
141
131
  await mkdir(audioDir, { recursive: true })
@@ -252,10 +242,7 @@ parentPort.on('message', async (message: WorkerInMessage) => {
252
242
  workerLogger.log(`Initializing with directory:`, message.directory)
253
243
 
254
244
  // Create audio log stream for assistant audio
255
- audioLogStream = await createAssistantAudioLogStream(
256
- message.guildId,
257
- message.channelId,
258
- )
245
+ audioLogStream = await createAssistantAudioLogStream(message.guildId, message.channelId)
259
246
 
260
247
  // Start packet sending interval
261
248
  startPacketSending()
@@ -359,8 +346,6 @@ parentPort.on('message', async (message: WorkerInMessage) => {
359
346
  }
360
347
  } catch (error) {
361
348
  workerLogger.error(`Error handling message:`, error)
362
- sendError(
363
- error instanceof Error ? error.message : 'Unknown error in worker',
364
- )
349
+ sendError(error instanceof Error ? error.message : 'Unknown error in worker')
365
350
  }
366
351
  })