kimaki 0.4.25 → 0.4.27

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 (52) hide show
  1. package/dist/acp-client.test.js +149 -0
  2. package/dist/channel-management.js +11 -9
  3. package/dist/cli.js +58 -18
  4. package/dist/commands/add-project.js +1 -0
  5. package/dist/commands/agent.js +152 -0
  6. package/dist/commands/ask-question.js +184 -0
  7. package/dist/commands/model.js +23 -4
  8. package/dist/commands/permissions.js +101 -105
  9. package/dist/commands/session.js +1 -3
  10. package/dist/commands/user-command.js +145 -0
  11. package/dist/database.js +51 -0
  12. package/dist/discord-bot.js +32 -32
  13. package/dist/discord-utils.js +71 -14
  14. package/dist/interaction-handler.js +25 -8
  15. package/dist/logger.js +43 -5
  16. package/dist/markdown.js +104 -0
  17. package/dist/markdown.test.js +31 -1
  18. package/dist/message-formatting.js +72 -22
  19. package/dist/message-formatting.test.js +73 -0
  20. package/dist/opencode.js +70 -16
  21. package/dist/session-handler.js +142 -66
  22. package/dist/system-message.js +4 -51
  23. package/dist/voice-handler.js +18 -8
  24. package/dist/voice.js +28 -12
  25. package/package.json +14 -13
  26. package/src/__snapshots__/compact-session-context-no-system.md +35 -0
  27. package/src/__snapshots__/compact-session-context.md +47 -0
  28. package/src/channel-management.ts +20 -8
  29. package/src/cli.ts +73 -19
  30. package/src/commands/add-project.ts +1 -0
  31. package/src/commands/agent.ts +201 -0
  32. package/src/commands/ask-question.ts +277 -0
  33. package/src/commands/fork.ts +1 -2
  34. package/src/commands/model.ts +24 -4
  35. package/src/commands/permissions.ts +139 -114
  36. package/src/commands/session.ts +1 -3
  37. package/src/commands/user-command.ts +178 -0
  38. package/src/database.ts +61 -0
  39. package/src/discord-bot.ts +36 -33
  40. package/src/discord-utils.ts +76 -14
  41. package/src/interaction-handler.ts +31 -10
  42. package/src/logger.ts +47 -10
  43. package/src/markdown.test.ts +45 -1
  44. package/src/markdown.ts +132 -0
  45. package/src/message-formatting.test.ts +81 -0
  46. package/src/message-formatting.ts +93 -25
  47. package/src/opencode.ts +80 -21
  48. package/src/session-handler.ts +190 -97
  49. package/src/system-message.ts +4 -51
  50. package/src/voice-handler.ts +20 -9
  51. package/src/voice.ts +32 -13
  52. package/LICENSE +0 -21
@@ -0,0 +1,178 @@
1
+ // User-defined OpenCode command handler.
2
+ // Handles slash commands that map to user-configured commands in opencode.json.
3
+
4
+ import type { CommandContext, CommandHandler } from './types.js'
5
+ import { ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
6
+ import { extractTagsArrays } from '../xml.js'
7
+ import { handleOpencodeSession } from '../session-handler.js'
8
+ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
9
+ import { createLogger } from '../logger.js'
10
+ import { getDatabase } from '../database.js'
11
+ import fs from 'node:fs'
12
+
13
+ const userCommandLogger = createLogger('USER_CMD')
14
+
15
+ export const handleUserCommand: CommandHandler = async ({
16
+ command,
17
+ appId,
18
+ }: CommandContext) => {
19
+ const discordCommandName = command.commandName
20
+ // Strip the -cmd suffix to get the actual OpenCode command name
21
+ const commandName = discordCommandName.replace(/-cmd$/, '')
22
+ const args = command.options.getString('arguments') || ''
23
+
24
+ userCommandLogger.log(
25
+ `Executing /${commandName} (from /${discordCommandName}) with args: ${args}`,
26
+ )
27
+
28
+ const channel = command.channel
29
+
30
+ userCommandLogger.log(
31
+ `Channel info: type=${channel?.type}, id=${channel?.id}, isNull=${channel === null}`,
32
+ )
33
+
34
+ const isThread = channel && [
35
+ ChannelType.PublicThread,
36
+ ChannelType.PrivateThread,
37
+ ChannelType.AnnouncementThread,
38
+ ].includes(channel.type)
39
+
40
+ const isTextChannel = channel?.type === ChannelType.GuildText
41
+
42
+ if (!channel || (!isTextChannel && !isThread)) {
43
+ await command.reply({
44
+ content: 'This command can only be used in text channels or threads',
45
+ ephemeral: true,
46
+ })
47
+ return
48
+ }
49
+
50
+ let projectDirectory: string | undefined
51
+ let channelAppId: string | undefined
52
+ let textChannel: TextChannel | null = null
53
+ let thread: ThreadChannel | null = null
54
+
55
+ if (isThread) {
56
+ // Running in an existing thread - get project directory from parent channel
57
+ thread = channel as ThreadChannel
58
+ textChannel = thread.parent as TextChannel | null
59
+
60
+ // Verify this thread has an existing session
61
+ const row = getDatabase()
62
+ .prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
63
+ .get(thread.id) as { session_id: string } | undefined
64
+
65
+ if (!row) {
66
+ 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.',
68
+ ephemeral: true,
69
+ })
70
+ return
71
+ }
72
+
73
+ if (textChannel?.topic) {
74
+ const extracted = extractTagsArrays({
75
+ xml: textChannel.topic,
76
+ tags: ['kimaki.directory', 'kimaki.app'],
77
+ })
78
+
79
+ projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
80
+ channelAppId = extracted['kimaki.app']?.[0]?.trim()
81
+ }
82
+ } else {
83
+ // Running in a text channel - will create a new thread
84
+ textChannel = channel as TextChannel
85
+
86
+ if (textChannel.topic) {
87
+ const extracted = extractTagsArrays({
88
+ xml: textChannel.topic,
89
+ tags: ['kimaki.directory', 'kimaki.app'],
90
+ })
91
+
92
+ projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
93
+ channelAppId = extracted['kimaki.app']?.[0]?.trim()
94
+ }
95
+ }
96
+
97
+ if (channelAppId && channelAppId !== appId) {
98
+ await command.reply({
99
+ content: 'This channel is not configured for this bot',
100
+ ephemeral: true,
101
+ })
102
+ return
103
+ }
104
+
105
+ if (!projectDirectory) {
106
+ await command.reply({
107
+ content: 'This channel is not configured with a project directory',
108
+ ephemeral: true,
109
+ })
110
+ return
111
+ }
112
+
113
+ if (!fs.existsSync(projectDirectory)) {
114
+ await command.reply({
115
+ content: `Directory does not exist: ${projectDirectory}`,
116
+ ephemeral: true,
117
+ })
118
+ return
119
+ }
120
+
121
+ await command.deferReply({ ephemeral: false })
122
+
123
+ try {
124
+ // Use the dedicated session.command API instead of formatting as text prompt
125
+ const commandPayload = { name: commandName, arguments: args }
126
+
127
+ if (isThread && thread) {
128
+ // Running in existing thread - just send the command
129
+ await command.editReply(`Running /${commandName}...`)
130
+
131
+ await handleOpencodeSession({
132
+ prompt: '', // Not used when command is set
133
+ thread,
134
+ projectDirectory,
135
+ channelId: textChannel?.id,
136
+ command: commandPayload,
137
+ })
138
+ } else if (textChannel) {
139
+ // Running in text channel - create a new thread
140
+ const starterMessage = await textChannel.send({
141
+ content: `**/${commandName}**${args ? ` ${args.slice(0, 200)}${args.length > 200 ? '…' : ''}` : ''}`,
142
+ flags: SILENT_MESSAGE_FLAGS,
143
+ })
144
+
145
+ const threadName = `/${commandName} ${args.slice(0, 80)}${args.length > 80 ? '…' : ''}`
146
+ const newThread = await starterMessage.startThread({
147
+ name: threadName.slice(0, 100),
148
+ autoArchiveDuration: 1440,
149
+ reason: `OpenCode command: ${commandName}`,
150
+ })
151
+
152
+ await command.editReply(`Started /${commandName} in ${newThread.toString()}`)
153
+
154
+ await handleOpencodeSession({
155
+ prompt: '', // Not used when command is set
156
+ thread: newThread,
157
+ projectDirectory,
158
+ channelId: textChannel.id,
159
+ command: commandPayload,
160
+ })
161
+ }
162
+ } catch (error) {
163
+ userCommandLogger.error(`Error executing /${commandName}:`, error)
164
+
165
+ const errorMessage = error instanceof Error ? error.message : String(error)
166
+
167
+ if (command.deferred) {
168
+ await command.editReply({
169
+ content: `Failed to execute /${commandName}: ${errorMessage}`,
170
+ })
171
+ } else {
172
+ await command.reply({
173
+ content: `Failed to execute /${commandName}: ${errorMessage}`,
174
+ ephemeral: true,
175
+ })
176
+ }
177
+ }
178
+ }
package/src/database.ts CHANGED
@@ -100,6 +100,23 @@ export function runModelMigrations(database?: Database.Database): void {
100
100
  )
101
101
  `)
102
102
 
103
+ targetDb.exec(`
104
+ CREATE TABLE IF NOT EXISTS channel_agents (
105
+ channel_id TEXT PRIMARY KEY,
106
+ agent_name TEXT NOT NULL,
107
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
108
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
109
+ )
110
+ `)
111
+
112
+ targetDb.exec(`
113
+ CREATE TABLE IF NOT EXISTS session_agents (
114
+ session_id TEXT PRIMARY KEY,
115
+ agent_name TEXT NOT NULL,
116
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
117
+ )
118
+ `)
119
+
103
120
  dbLogger.log('Model preferences migrations complete')
104
121
  }
105
122
 
@@ -151,6 +168,50 @@ export function setSessionModel(sessionId: string, modelId: string): void {
151
168
  ).run(sessionId, modelId)
152
169
  }
153
170
 
171
+ /**
172
+ * Get the agent preference for a channel.
173
+ */
174
+ export function getChannelAgent(channelId: string): string | undefined {
175
+ const db = getDatabase()
176
+ const row = db
177
+ .prepare('SELECT agent_name FROM channel_agents WHERE channel_id = ?')
178
+ .get(channelId) as { agent_name: string } | undefined
179
+ return row?.agent_name
180
+ }
181
+
182
+ /**
183
+ * Set the agent preference for a channel.
184
+ */
185
+ export function setChannelAgent(channelId: string, agentName: string): void {
186
+ const db = getDatabase()
187
+ db.prepare(
188
+ `INSERT INTO channel_agents (channel_id, agent_name, updated_at)
189
+ VALUES (?, ?, CURRENT_TIMESTAMP)
190
+ ON CONFLICT(channel_id) DO UPDATE SET agent_name = ?, updated_at = CURRENT_TIMESTAMP`
191
+ ).run(channelId, agentName, agentName)
192
+ }
193
+
194
+ /**
195
+ * Get the agent preference for a session.
196
+ */
197
+ export function getSessionAgent(sessionId: string): string | undefined {
198
+ const db = getDatabase()
199
+ const row = db
200
+ .prepare('SELECT agent_name FROM session_agents WHERE session_id = ?')
201
+ .get(sessionId) as { agent_name: string } | undefined
202
+ return row?.agent_name
203
+ }
204
+
205
+ /**
206
+ * Set the agent preference for a session.
207
+ */
208
+ export function setSessionAgent(sessionId: string, agentName: string): void {
209
+ const db = getDatabase()
210
+ db.prepare(
211
+ `INSERT OR REPLACE INTO session_agents (session_id, agent_name) VALUES (?, ?)`
212
+ ).run(sessionId, agentName)
213
+ }
214
+
154
215
  export function closeDatabase(): void {
155
216
  if (db) {
156
217
  db.close()
@@ -25,9 +25,10 @@ import {
25
25
  registerVoiceStateHandler,
26
26
  } from './voice-handler.js'
27
27
  import {
28
- handleOpencodeSession,
29
- parseSlashCommand,
30
- } from './session-handler.js'
28
+ getCompactSessionContext,
29
+ getLastSessionId,
30
+ } from './markdown.js'
31
+ import { handleOpencodeSession } from './session-handler.js'
31
32
  import { registerInteractionHandler } from './interaction-handler.js'
32
33
 
33
34
  export { getDatabase, closeDatabase } from './database.js'
@@ -240,34 +241,39 @@ export async function startDiscordBot({
240
241
 
241
242
  let messageContent = message.content || ''
242
243
 
243
- let sessionMessagesText: string | undefined
244
- if (projectDirectory && row.session_id) {
244
+ let currentSessionContext: string | undefined
245
+ let lastSessionContext: string | undefined
246
+
247
+ if (projectDirectory) {
245
248
  try {
246
249
  const getClient = await initializeOpencodeForDirectory(projectDirectory)
247
- const messagesResponse = await getClient().session.messages({
248
- path: { id: row.session_id },
250
+ const client = getClient()
251
+
252
+ // get current session context (without system prompt, it would be duplicated)
253
+ if (row.session_id) {
254
+ currentSessionContext = await getCompactSessionContext({
255
+ client,
256
+ sessionId: row.session_id,
257
+ includeSystemPrompt: false,
258
+ maxMessages: 15,
259
+ })
260
+ }
261
+
262
+ // get last session context (with system prompt for project context)
263
+ const lastSessionId = await getLastSessionId({
264
+ client,
265
+ excludeSessionId: row.session_id,
249
266
  })
250
- const messages = messagesResponse.data || []
251
- const recentMessages = messages.slice(-10)
252
- sessionMessagesText = recentMessages
253
- .map((m) => {
254
- const role = m.info.role === 'user' ? 'User' : 'Assistant'
255
- const text = (() => {
256
- if (m.info.role === 'user') {
257
- const textParts = (m.parts || []).filter((p) => p.type === 'text')
258
- return textParts
259
- .map((p) => ('text' in p ? p.text : ''))
260
- .filter(Boolean)
261
- .join('\n')
262
- }
263
- const assistantInfo = m.info as { text?: string }
264
- return assistantInfo.text?.slice(0, 500)
265
- })()
266
- return `[${role}]: ${text || '(no text)'}`
267
+ if (lastSessionId) {
268
+ lastSessionContext = await getCompactSessionContext({
269
+ client,
270
+ sessionId: lastSessionId,
271
+ includeSystemPrompt: true,
272
+ maxMessages: 10,
267
273
  })
268
- .join('\n\n')
274
+ }
269
275
  } catch (e) {
270
- voiceLogger.log(`Could not get session messages:`, e)
276
+ voiceLogger.error(`Could not get session context:`, e)
271
277
  }
272
278
  }
273
279
 
@@ -276,25 +282,24 @@ export async function startDiscordBot({
276
282
  thread,
277
283
  projectDirectory,
278
284
  appId: currentAppId,
279
- sessionMessages: sessionMessagesText,
285
+ currentSessionContext,
286
+ lastSessionContext,
280
287
  })
281
288
  if (transcription) {
282
289
  messageContent = transcription
283
290
  }
284
291
 
285
- const fileAttachments = getFileAttachments(message)
292
+ const fileAttachments = await getFileAttachments(message)
286
293
  const textAttachmentsContent = await getTextAttachments(message)
287
294
  const promptWithAttachments = textAttachmentsContent
288
295
  ? `${messageContent}\n\n${textAttachmentsContent}`
289
296
  : messageContent
290
- const parsedCommand = parseSlashCommand(messageContent)
291
297
  await handleOpencodeSession({
292
298
  prompt: promptWithAttachments,
293
299
  thread,
294
300
  projectDirectory,
295
301
  originalMessage: message,
296
302
  images: fileAttachments,
297
- parsedCommand,
298
303
  channelId: parent?.id,
299
304
  })
300
305
  return
@@ -380,19 +385,17 @@ export async function startDiscordBot({
380
385
  messageContent = transcription
381
386
  }
382
387
 
383
- const fileAttachments = getFileAttachments(message)
388
+ const fileAttachments = await getFileAttachments(message)
384
389
  const textAttachmentsContent = await getTextAttachments(message)
385
390
  const promptWithAttachments = textAttachmentsContent
386
391
  ? `${messageContent}\n\n${textAttachmentsContent}`
387
392
  : messageContent
388
- const parsedCommand = parseSlashCommand(messageContent)
389
393
  await handleOpencodeSession({
390
394
  prompt: promptWithAttachments,
391
395
  thread,
392
396
  projectDirectory,
393
397
  originalMessage: message,
394
398
  images: fileAttachments,
395
- parsedCommand,
396
399
  channelId: textChannel.id,
397
400
  })
398
401
  } else {
@@ -85,31 +85,93 @@ export function splitMarkdownForDiscord({
85
85
  let currentChunk = ''
86
86
  let currentLang: string | null = null
87
87
 
88
+ // helper to split a long line into smaller pieces at word boundaries or hard breaks
89
+ const splitLongLine = (text: string, available: number, inCode: boolean): string[] => {
90
+ const pieces: string[] = []
91
+ let remaining = text
92
+
93
+ while (remaining.length > available) {
94
+ let splitAt = available
95
+ // for non-code, try to split at word boundary
96
+ if (!inCode) {
97
+ const lastSpace = remaining.lastIndexOf(' ', available)
98
+ if (lastSpace > available * 0.5) {
99
+ splitAt = lastSpace + 1
100
+ }
101
+ }
102
+ pieces.push(remaining.slice(0, splitAt))
103
+ remaining = remaining.slice(splitAt)
104
+ }
105
+ if (remaining) {
106
+ pieces.push(remaining)
107
+ }
108
+ return pieces
109
+ }
110
+
88
111
  for (const line of lines) {
89
112
  const wouldExceed = currentChunk.length + line.text.length > maxLength
90
113
 
91
- if (wouldExceed && currentChunk) {
92
- if (currentLang !== null) {
93
- currentChunk += '```\n'
94
- }
95
- chunks.push(currentChunk)
114
+ if (wouldExceed) {
115
+ // handle case where single line is longer than maxLength
116
+ if (line.text.length > maxLength) {
117
+ // first, flush current chunk if any
118
+ if (currentChunk) {
119
+ if (currentLang !== null) {
120
+ currentChunk += '```\n'
121
+ }
122
+ chunks.push(currentChunk)
123
+ currentChunk = ''
124
+ }
125
+
126
+ // calculate overhead for code block markers
127
+ const codeBlockOverhead = line.inCodeBlock ? ('```' + line.lang + '\n').length + '```\n'.length : 0
128
+ const availablePerChunk = maxLength - codeBlockOverhead - 50 // safety margin
129
+
130
+ const pieces = splitLongLine(line.text, availablePerChunk, line.inCodeBlock)
131
+
132
+ for (let i = 0; i < pieces.length; i++) {
133
+ const piece = pieces[i]!
134
+ if (line.inCodeBlock) {
135
+ chunks.push('```' + line.lang + '\n' + piece + '```\n')
136
+ } else {
137
+ chunks.push(piece)
138
+ }
139
+ }
96
140
 
97
- if (line.isClosingFence && currentLang !== null) {
98
- currentChunk = ''
99
141
  currentLang = null
100
142
  continue
101
143
  }
102
144
 
103
- if (line.inCodeBlock || line.isOpeningFence) {
104
- const lang = line.lang
105
- currentChunk = '```' + lang + '\n'
106
- if (!line.isOpeningFence) {
107
- currentChunk += line.text
145
+ // normal case: line fits in a chunk but current chunk would overflow
146
+ if (currentChunk) {
147
+ if (currentLang !== null) {
148
+ currentChunk += '```\n'
149
+ }
150
+ chunks.push(currentChunk)
151
+
152
+ if (line.isClosingFence && currentLang !== null) {
153
+ currentChunk = ''
154
+ currentLang = null
155
+ continue
156
+ }
157
+
158
+ if (line.inCodeBlock || line.isOpeningFence) {
159
+ const lang = line.lang
160
+ currentChunk = '```' + lang + '\n'
161
+ if (!line.isOpeningFence) {
162
+ currentChunk += line.text
163
+ }
164
+ currentLang = lang
165
+ } else {
166
+ currentChunk = line.text
167
+ currentLang = null
108
168
  }
109
- currentLang = lang
110
169
  } else {
170
+ // currentChunk is empty but line still exceeds - shouldn't happen after above check
111
171
  currentChunk = line.text
112
- currentLang = null
172
+ if (line.inCodeBlock || line.isOpeningFence) {
173
+ currentLang = line.lang
174
+ }
113
175
  }
114
176
  } else {
115
177
  currentChunk += line.text
@@ -7,17 +7,21 @@ import { handleSessionCommand, handleSessionAutocomplete } from './commands/sess
7
7
  import { handleResumeCommand, handleResumeAutocomplete } from './commands/resume.js'
8
8
  import { handleAddProjectCommand, handleAddProjectAutocomplete } from './commands/add-project.js'
9
9
  import { handleCreateNewProjectCommand } from './commands/create-new-project.js'
10
- import { handleAcceptCommand, handleRejectCommand } from './commands/permissions.js'
10
+ import { handlePermissionSelectMenu } from './commands/permissions.js'
11
11
  import { handleAbortCommand } from './commands/abort.js'
12
12
  import { handleShareCommand } from './commands/share.js'
13
13
  import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js'
14
14
  import { handleModelCommand, handleProviderSelectMenu, handleModelSelectMenu } from './commands/model.js'
15
+ import { handleAgentCommand, handleAgentSelectMenu } from './commands/agent.js'
16
+ import { handleAskQuestionSelectMenu } from './commands/ask-question.js'
15
17
  import { handleQueueCommand, handleClearQueueCommand } from './commands/queue.js'
16
18
  import { handleUndoCommand, handleRedoCommand } from './commands/undo-redo.js'
19
+ import { handleUserCommand } from './commands/user-command.js'
17
20
  import { createLogger } from './logger.js'
18
21
 
19
22
  const interactionLogger = createLogger('INTERACTION')
20
23
 
24
+
21
25
  export function registerInteractionHandler({
22
26
  discordClient,
23
27
  appId,
@@ -81,16 +85,8 @@ export function registerInteractionHandler({
81
85
  await handleCreateNewProjectCommand({ command: interaction, appId })
82
86
  return
83
87
 
84
- case 'accept':
85
- case 'accept-always':
86
- await handleAcceptCommand({ command: interaction, appId })
87
- return
88
-
89
- case 'reject':
90
- await handleRejectCommand({ command: interaction, appId })
91
- return
92
-
93
88
  case 'abort':
89
+ case 'stop':
94
90
  await handleAbortCommand({ command: interaction, appId })
95
91
  return
96
92
 
@@ -106,6 +102,10 @@ export function registerInteractionHandler({
106
102
  await handleModelCommand({ interaction, appId })
107
103
  return
108
104
 
105
+ case 'agent':
106
+ await handleAgentCommand({ interaction, appId })
107
+ return
108
+
109
109
  case 'queue':
110
110
  await handleQueueCommand({ command: interaction, appId })
111
111
  return
@@ -122,6 +122,12 @@ export function registerInteractionHandler({
122
122
  await handleRedoCommand({ command: interaction, appId })
123
123
  return
124
124
  }
125
+
126
+ // Handle user-defined commands (ending with -cmd suffix)
127
+ if (interaction.commandName.endsWith('-cmd')) {
128
+ await handleUserCommand({ command: interaction, appId })
129
+ return
130
+ }
125
131
  return
126
132
  }
127
133
 
@@ -142,6 +148,21 @@ export function registerInteractionHandler({
142
148
  await handleModelSelectMenu(interaction)
143
149
  return
144
150
  }
151
+
152
+ if (customId.startsWith('agent_select:')) {
153
+ await handleAgentSelectMenu(interaction)
154
+ return
155
+ }
156
+
157
+ if (customId.startsWith('ask_question:')) {
158
+ await handleAskQuestionSelectMenu(interaction)
159
+ return
160
+ }
161
+
162
+ if (customId.startsWith('permission:')) {
163
+ await handlePermissionSelectMenu(interaction)
164
+ return
165
+ }
145
166
  return
146
167
  }
147
168
  } catch (error) {
package/src/logger.ts CHANGED
@@ -3,18 +3,55 @@
3
3
  // (DISCORD, VOICE, SESSION, etc.) for easier debugging.
4
4
 
5
5
  import { log } from '@clack/prompts'
6
+ import fs from 'node:fs'
7
+ import path, { dirname } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const __filename = fileURLToPath(import.meta.url)
11
+ const __dirname = dirname(__filename)
12
+ const isDev = !__dirname.includes('node_modules')
13
+
14
+ const logFilePath = path.join(__dirname, '..', 'tmp', 'kimaki.log')
15
+
16
+ // reset log file on startup in dev mode
17
+ if (isDev) {
18
+ const logDir = path.dirname(logFilePath)
19
+ if (!fs.existsSync(logDir)) {
20
+ fs.mkdirSync(logDir, { recursive: true })
21
+ }
22
+ fs.writeFileSync(logFilePath, `--- kimaki log started at ${new Date().toISOString()} ---\n`)
23
+ }
24
+
25
+ function writeToFile(level: string, prefix: string, args: any[]) {
26
+ if (!isDev) {
27
+ return
28
+ }
29
+ const timestamp = new Date().toISOString()
30
+ const message = `[${timestamp}] [${level}] [${prefix}] ${args.map((arg) => String(arg)).join(' ')}\n`
31
+ fs.appendFileSync(logFilePath, message)
32
+ }
6
33
 
7
34
  export function createLogger(prefix: string) {
8
35
  return {
9
- log: (...args: any[]) =>
10
- log.info([`[${prefix}]`, ...args.map((arg) => String(arg))].join(' ')),
11
- error: (...args: any[]) =>
12
- log.error([`[${prefix}]`, ...args.map((arg) => String(arg))].join(' ')),
13
- warn: (...args: any[]) =>
14
- log.warn([`[${prefix}]`, ...args.map((arg) => String(arg))].join(' ')),
15
- info: (...args: any[]) =>
16
- log.info([`[${prefix}]`, ...args.map((arg) => String(arg))].join(' ')),
17
- debug: (...args: any[]) =>
18
- log.info([`[${prefix}]`, ...args.map((arg) => String(arg))].join(' ')),
36
+ log: (...args: any[]) => {
37
+ writeToFile('INFO', prefix, args)
38
+ log.info([`[${prefix}]`, ...args.map((arg) => String(arg))].join(' '))
39
+ },
40
+ error: (...args: any[]) => {
41
+ writeToFile('ERROR', prefix, args)
42
+ log.error([`[${prefix}]`, ...args.map((arg) => String(arg))].join(' '))
43
+ },
44
+ warn: (...args: any[]) => {
45
+ writeToFile('WARN', prefix, args)
46
+ log.warn([`[${prefix}]`, ...args.map((arg) => String(arg))].join(' '))
47
+ },
48
+ info: (...args: any[]) => {
49
+ writeToFile('INFO', prefix, args)
50
+ log.info([`[${prefix}]`, ...args.map((arg) => String(arg))].join(' '))
51
+ },
52
+ debug: (...args: any[]) => {
53
+ writeToFile('DEBUG', prefix, args)
54
+ log.info([`[${prefix}]`, ...args.map((arg) => String(arg))].join(' '))
55
+ },
19
56
  }
20
57
  }