kimaki 0.4.44 → 0.4.45

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 (41) hide show
  1. package/dist/channel-management.js +6 -15
  2. package/dist/cli.js +54 -37
  3. package/dist/commands/permissions.js +21 -5
  4. package/dist/commands/queue.js +5 -1
  5. package/dist/commands/resume.js +8 -16
  6. package/dist/commands/session.js +18 -42
  7. package/dist/commands/user-command.js +8 -17
  8. package/dist/commands/verbosity.js +53 -0
  9. package/dist/commands/worktree-settings.js +2 -2
  10. package/dist/commands/worktree.js +132 -25
  11. package/dist/database.js +49 -0
  12. package/dist/discord-bot.js +24 -38
  13. package/dist/discord-utils.js +51 -13
  14. package/dist/discord-utils.test.js +20 -0
  15. package/dist/escape-backticks.test.js +14 -3
  16. package/dist/interaction-handler.js +4 -0
  17. package/dist/session-handler.js +541 -413
  18. package/package.json +1 -1
  19. package/src/__snapshots__/first-session-no-info.md +1344 -0
  20. package/src/__snapshots__/first-session-with-info.md +1350 -0
  21. package/src/__snapshots__/session-1.md +1344 -0
  22. package/src/__snapshots__/session-2.md +291 -0
  23. package/src/__snapshots__/session-3.md +20324 -0
  24. package/src/__snapshots__/session-with-tools.md +1344 -0
  25. package/src/channel-management.ts +6 -17
  26. package/src/cli.ts +63 -45
  27. package/src/commands/permissions.ts +31 -5
  28. package/src/commands/queue.ts +5 -1
  29. package/src/commands/resume.ts +8 -18
  30. package/src/commands/session.ts +18 -44
  31. package/src/commands/user-command.ts +8 -19
  32. package/src/commands/verbosity.ts +71 -0
  33. package/src/commands/worktree-settings.ts +2 -2
  34. package/src/commands/worktree.ts +160 -27
  35. package/src/database.ts +65 -0
  36. package/src/discord-bot.ts +26 -42
  37. package/src/discord-utils.test.ts +23 -0
  38. package/src/discord-utils.ts +52 -13
  39. package/src/escape-backticks.test.ts +14 -3
  40. package/src/interaction-handler.ts +5 -0
  41. package/src/session-handler.ts +669 -436
@@ -4,8 +4,7 @@
4
4
 
5
5
  import { ChannelType, type CategoryChannel, type Guild, type TextChannel } from 'discord.js'
6
6
  import path from 'node:path'
7
- import { getDatabase } from './database.js'
8
- import { extractTagsArrays } from './xml.js'
7
+ import { getDatabase, getChannelDirectory } from './database.js'
9
8
 
10
9
  export async function ensureKimakiCategory(
11
10
  guild: Guild,
@@ -83,7 +82,7 @@ export async function createProjectChannels({
83
82
  name: channelName,
84
83
  type: ChannelType.GuildText,
85
84
  parent: kimakiCategory,
86
- topic: `<kimaki><directory>${projectDirectory}</directory><app>${appId}</app></kimaki>`,
85
+ // Channel configuration is stored in SQLite, not in the topic
87
86
  })
88
87
 
89
88
  const voiceChannel = await guild.channels.create({
@@ -128,25 +127,15 @@ export async function getChannelsWithDescriptions(guild: Guild): Promise<Channel
128
127
  const textChannel = channel as TextChannel
129
128
  const description = textChannel.topic || null
130
129
 
131
- let kimakiDirectory: string | undefined
132
- let kimakiApp: string | undefined
133
-
134
- if (description) {
135
- const extracted = extractTagsArrays({
136
- xml: description,
137
- tags: ['kimaki.directory', 'kimaki.app'],
138
- })
139
-
140
- kimakiDirectory = extracted['kimaki.directory']?.[0]?.trim()
141
- kimakiApp = extracted['kimaki.app']?.[0]?.trim()
142
- }
130
+ // Get channel config from database instead of parsing XML from topic
131
+ const channelConfig = getChannelDirectory(textChannel.id)
143
132
 
144
133
  channels.push({
145
134
  id: textChannel.id,
146
135
  name: textChannel.name,
147
136
  description,
148
- kimakiDirectory,
149
- kimakiApp,
137
+ kimakiDirectory: channelConfig?.directory,
138
+ kimakiApp: channelConfig?.appId || undefined,
150
139
  })
151
140
  })
152
141
 
package/src/cli.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  getChannelsWithDescriptions,
22
22
  createDiscordClient,
23
23
  getDatabase,
24
+ getChannelDirectory,
24
25
  startDiscordBot,
25
26
  initializeOpencodeForDirectory,
26
27
  ensureKimakiCategory,
@@ -47,7 +48,6 @@ import { uploadFilesToDiscord } from './discord-utils.js'
47
48
  import { spawn, spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
48
49
  import http from 'node:http'
49
50
  import { setDataDir, getDataDir, getLockPort } from './config.js'
50
- import { extractTagsArrays } from './xml.js'
51
51
  import { sanitizeAgentName } from './commands/agent.js'
52
52
 
53
53
  const cliLogger = createLogger('CLI')
@@ -240,12 +240,12 @@ async function registerCommands({
240
240
  .toJSON(),
241
241
  new SlashCommandBuilder()
242
242
  .setName('new-worktree')
243
- .setDescription('Create a new git worktree and start a session thread')
243
+ .setDescription('Create a new git worktree (in thread: uses thread name if no name given)')
244
244
  .addStringOption((option) => {
245
245
  option
246
246
  .setName('name')
247
- .setDescription('Name for the worktree (will be formatted: lowercase, spaces to dashes)')
248
- .setRequired(true)
247
+ .setDescription('Name for worktree (optional in threads - uses thread name)')
248
+ .setRequired(false)
249
249
 
250
250
  return option
251
251
  })
@@ -342,6 +342,21 @@ async function registerCommands({
342
342
  .setName('redo')
343
343
  .setDescription('Redo previously undone changes')
344
344
  .toJSON(),
345
+ new SlashCommandBuilder()
346
+ .setName('verbosity')
347
+ .setDescription('Set output verbosity for new sessions in this channel')
348
+ .addStringOption((option) => {
349
+ option
350
+ .setName('level')
351
+ .setDescription('Verbosity level')
352
+ .setRequired(true)
353
+ .addChoices(
354
+ { name: 'tools-and-text (default)', value: 'tools-and-text' },
355
+ { name: 'text-only', value: 'text-only' },
356
+ )
357
+ return option
358
+ })
359
+ .toJSON(),
345
360
  ]
346
361
 
347
362
  // Add user-defined commands with -cmd suffix
@@ -1367,25 +1382,17 @@ cli
1367
1382
  guild_id: string
1368
1383
  }
1369
1384
 
1370
- if (!channelData.topic) {
1371
- s.stop('Channel has no topic')
1385
+ const channelConfig = getChannelDirectory(channelData.id)
1386
+
1387
+ if (!channelConfig) {
1388
+ s.stop('Channel not configured')
1372
1389
  throw new Error(
1373
- `Channel #${channelData.name} has no topic. It must have a <kimaki.directory> tag.`,
1390
+ `Channel #${channelData.name} is not configured with a project directory. Run the bot first to sync channel data.`,
1374
1391
  )
1375
1392
  }
1376
1393
 
1377
- const extracted = extractTagsArrays({
1378
- xml: channelData.topic,
1379
- tags: ['kimaki.directory', 'kimaki.app'],
1380
- })
1381
-
1382
- const projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
1383
- const channelAppId = extracted['kimaki.app']?.[0]?.trim()
1384
-
1385
- if (!projectDirectory) {
1386
- s.stop('No kimaki.directory tag found')
1387
- throw new Error(`Channel #${channelData.name} has no <kimaki.directory> tag in topic.`)
1388
- }
1394
+ const projectDirectory = channelConfig.directory
1395
+ const channelAppId = channelConfig.appId || undefined
1389
1396
 
1390
1397
  // Verify app ID matches if both are present
1391
1398
  if (channelAppId && appId && channelAppId !== appId) {
@@ -1599,30 +1606,7 @@ cli
1599
1606
  }
1600
1607
 
1601
1608
  const s = spinner()
1602
- s.start('Checking for existing channel...')
1603
-
1604
- // Check if channel already exists
1605
- try {
1606
- const db = getDatabase()
1607
- const existingChannel = db
1608
- .prepare(
1609
- 'SELECT channel_id FROM channel_directories WHERE directory = ? AND channel_type = ? AND app_id = ?',
1610
- )
1611
- .get(absolutePath, 'text', appId) as { channel_id: string } | undefined
1612
-
1613
- if (existingChannel) {
1614
- s.stop('Channel already exists')
1615
- note(
1616
- `Channel already exists for this directory.\n\nChannel: <#${existingChannel.channel_id}>\nDirectory: ${absolutePath}`,
1617
- '⚠️ Already Exists',
1618
- )
1619
- process.exit(0)
1620
- }
1621
- } catch {
1622
- // Database might not exist, continue to create
1623
- }
1624
-
1625
- s.message('Connecting to Discord...')
1609
+ s.start('Connecting to Discord...')
1626
1610
  const client = await createDiscordClient()
1627
1611
 
1628
1612
  await new Promise<void>((resolve, reject) => {
@@ -1638,10 +1622,14 @@ cli
1638
1622
  // Find guild
1639
1623
  let guild: Guild
1640
1624
  if (options.guild) {
1641
- const foundGuild = client.guilds.cache.get(options.guild)
1625
+ // Get raw guild ID from argv to avoid cac's number coercion losing precision on large IDs
1626
+ const guildArgIndex = process.argv.findIndex((arg) => arg === '-g' || arg === '--guild')
1627
+ const rawGuildArg = guildArgIndex >= 0 ? process.argv[guildArgIndex + 1] : undefined
1628
+ const guildId = rawGuildArg || String(options.guild)
1629
+ const foundGuild = client.guilds.cache.get(guildId)
1642
1630
  if (!foundGuild) {
1643
1631
  s.stop('Guild not found')
1644
- cliLogger.error(`Guild not found: ${options.guild}`)
1632
+ cliLogger.error(`Guild not found: ${guildId}`)
1645
1633
  client.destroy()
1646
1634
  process.exit(EXIT_NO_RESTART)
1647
1635
  }
@@ -1686,6 +1674,36 @@ cli
1686
1674
  }
1687
1675
  }
1688
1676
 
1677
+ // Check if channel already exists in this guild
1678
+ s.message('Checking for existing channel...')
1679
+ try {
1680
+ const db = getDatabase()
1681
+ const existingChannels = db
1682
+ .prepare(
1683
+ 'SELECT channel_id FROM channel_directories WHERE directory = ? AND channel_type = ? AND app_id = ?',
1684
+ )
1685
+ .all(absolutePath, 'text', appId) as { channel_id: string }[]
1686
+
1687
+ for (const existingChannel of existingChannels) {
1688
+ try {
1689
+ const ch = await client.channels.fetch(existingChannel.channel_id)
1690
+ if (ch && 'guild' in ch && ch.guild?.id === guild.id) {
1691
+ s.stop('Channel already exists')
1692
+ note(
1693
+ `Channel already exists for this directory in ${guild.name}.\n\nChannel: <#${existingChannel.channel_id}>\nDirectory: ${absolutePath}`,
1694
+ '⚠️ Already Exists',
1695
+ )
1696
+ client.destroy()
1697
+ process.exit(0)
1698
+ }
1699
+ } catch {
1700
+ // Channel might be deleted, continue checking
1701
+ }
1702
+ }
1703
+ } catch {
1704
+ // Database might not exist, continue to create
1705
+ }
1706
+
1689
1707
  s.message(`Creating channels in ${guild.name}...`)
1690
1708
 
1691
1709
  const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
@@ -18,6 +18,7 @@ const logger = createLogger('PERMISSIONS')
18
18
 
19
19
  type PendingPermissionContext = {
20
20
  permission: PermissionRequest
21
+ requestIds: string[]
21
22
  directory: string
22
23
  thread: ThreadChannel
23
24
  contextHash: string
@@ -43,6 +44,7 @@ export async function showPermissionDropdown({
43
44
 
44
45
  const context: PendingPermissionContext = {
45
46
  permission,
47
+ requestIds: [permission.id],
46
48
  directory,
47
49
  thread,
48
50
  contextHash,
@@ -124,10 +126,15 @@ export async function handlePermissionSelectMenu(
124
126
  if (!clientV2) {
125
127
  throw new Error('OpenCode server not found for directory')
126
128
  }
127
- await clientV2.permission.reply({
128
- requestID: context.permission.id,
129
- reply: response,
130
- })
129
+ const requestIds = context.requestIds.length > 0 ? context.requestIds : [context.permission.id]
130
+ await Promise.all(
131
+ requestIds.map((requestId) => {
132
+ return clientV2.permission.reply({
133
+ requestID: requestId,
134
+ reply: response,
135
+ })
136
+ }),
137
+ )
131
138
 
132
139
  pendingPermissionContexts.delete(contextHash)
133
140
 
@@ -153,7 +160,7 @@ export async function handlePermissionSelectMenu(
153
160
  components: [], // Remove the dropdown
154
161
  })
155
162
 
156
- logger.log(`Permission ${context.permission.id} ${response}`)
163
+ logger.log(`Permission ${context.permission.id} ${response} (${requestIds.length} request(s))`)
157
164
  } catch (error) {
158
165
  logger.error('Error handling permission:', error)
159
166
  await interaction.editReply({
@@ -163,6 +170,25 @@ export async function handlePermissionSelectMenu(
163
170
  }
164
171
  }
165
172
 
173
+ export function addPermissionRequestToContext({
174
+ contextHash,
175
+ requestId,
176
+ }: {
177
+ contextHash: string
178
+ requestId: string
179
+ }): boolean {
180
+ const context = pendingPermissionContexts.get(contextHash)
181
+ if (!context) {
182
+ return false
183
+ }
184
+ if (context.requestIds.includes(requestId)) {
185
+ return false
186
+ }
187
+ context.requestIds = [...context.requestIds, requestId]
188
+ pendingPermissionContexts.set(contextHash, context)
189
+ return true
190
+ }
191
+
166
192
  /**
167
193
  * Clean up a pending permission context (e.g., on auto-reject).
168
194
  */
@@ -62,7 +62,11 @@ export async function handleQueueCommand({ command }: CommandContext): Promise<v
62
62
  }
63
63
 
64
64
  // Check if there's an active request running
65
- const hasActiveRequest = abortControllers.has(row.session_id)
65
+ const existingController = abortControllers.get(row.session_id)
66
+ const hasActiveRequest = Boolean(existingController && !existingController.signal.aborted)
67
+ if (existingController && existingController.signal.aborted) {
68
+ abortControllers.delete(row.session_id)
69
+ }
66
70
 
67
71
  if (!hasActiveRequest) {
68
72
  // No active request, send immediately
@@ -8,10 +8,9 @@ import {
8
8
  } from 'discord.js'
9
9
  import fs from 'node:fs'
10
10
  import type { CommandContext, AutocompleteContext } from './types.js'
11
- import { getDatabase } from '../database.js'
11
+ import { getDatabase, getChannelDirectory } from '../database.js'
12
12
  import { initializeOpencodeForDirectory } from '../opencode.js'
13
- import { sendThreadMessage, resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
14
- import { extractTagsArrays } from '../xml.js'
13
+ import { sendThreadMessage, resolveTextChannel } from '../discord-utils.js'
15
14
  import { collectLastAssistantParts } from '../message-formatting.js'
16
15
  import { createLogger } from '../logger.js'
17
16
  import * as errore from 'errore'
@@ -31,18 +30,9 @@ export async function handleResumeCommand({ command, appId }: CommandContext): P
31
30
 
32
31
  const textChannel = channel as TextChannel
33
32
 
34
- let projectDirectory: string | undefined
35
- let channelAppId: string | undefined
36
-
37
- if (textChannel.topic) {
38
- const extracted = extractTagsArrays({
39
- xml: textChannel.topic,
40
- tags: ['kimaki.directory', 'kimaki.app'],
41
- })
42
-
43
- projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
44
- channelAppId = extracted['kimaki.app']?.[0]?.trim()
45
- }
33
+ const channelConfig = getChannelDirectory(textChannel.id)
34
+ const projectDirectory = channelConfig?.directory
35
+ const channelAppId = channelConfig?.appId || undefined
46
36
 
47
37
  if (channelAppId && channelAppId !== appId) {
48
38
  await command.editReply('This channel is not configured for this bot')
@@ -157,12 +147,12 @@ export async function handleResumeAutocomplete({
157
147
  interaction.channel as TextChannel | ThreadChannel | null,
158
148
  )
159
149
  if (textChannel) {
160
- const { projectDirectory: directory, channelAppId } = getKimakiMetadata(textChannel)
161
- if (channelAppId && channelAppId !== appId) {
150
+ const channelConfig = getChannelDirectory(textChannel.id)
151
+ if (channelConfig?.appId && channelConfig.appId !== appId) {
162
152
  await interaction.respond([])
163
153
  return
164
154
  }
165
- projectDirectory = directory
155
+ projectDirectory = channelConfig?.directory
166
156
  }
167
157
  }
168
158
 
@@ -4,10 +4,9 @@ import { ChannelType, type TextChannel } from 'discord.js'
4
4
  import fs from 'node:fs'
5
5
  import path from 'node:path'
6
6
  import type { CommandContext, AutocompleteContext } from './types.js'
7
- import { getDatabase } from '../database.js'
7
+ import { getDatabase, getChannelDirectory } from '../database.js'
8
8
  import { initializeOpencodeForDirectory } from '../opencode.js'
9
9
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
10
- import { extractTagsArrays } from '../xml.js'
11
10
  import { handleOpencodeSession } from '../session-handler.js'
12
11
  import { createLogger } from '../logger.js'
13
12
  import * as errore from 'errore'
@@ -29,18 +28,9 @@ export async function handleSessionCommand({ command, appId }: CommandContext):
29
28
 
30
29
  const textChannel = channel as TextChannel
31
30
 
32
- let projectDirectory: string | undefined
33
- let channelAppId: string | undefined
34
-
35
- if (textChannel.topic) {
36
- const extracted = extractTagsArrays({
37
- xml: textChannel.topic,
38
- tags: ['kimaki.directory', 'kimaki.app'],
39
- })
40
-
41
- projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
42
- channelAppId = extracted['kimaki.app']?.[0]?.trim()
43
- }
31
+ const channelConfig = getChannelDirectory(textChannel.id)
32
+ const projectDirectory = channelConfig?.directory
33
+ const channelAppId = channelConfig?.appId || undefined
44
34
 
45
35
  if (channelAppId && channelAppId !== appId) {
46
36
  await command.editReply('This channel is not configured for this bot')
@@ -107,22 +97,14 @@ async function handleAgentAutocomplete({ interaction, appId }: AutocompleteConte
107
97
 
108
98
  let projectDirectory: string | undefined
109
99
 
110
- if (interaction.channel) {
111
- const channel = interaction.channel
112
- if (channel.type === ChannelType.GuildText) {
113
- const textChannel = channel as TextChannel
114
- if (textChannel.topic) {
115
- const extracted = extractTagsArrays({
116
- xml: textChannel.topic,
117
- tags: ['kimaki.directory', 'kimaki.app'],
118
- })
119
- const channelAppId = extracted['kimaki.app']?.[0]?.trim()
120
- if (channelAppId && channelAppId !== appId) {
121
- await interaction.respond([])
122
- return
123
- }
124
- projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
100
+ if (interaction.channel && interaction.channel.type === ChannelType.GuildText) {
101
+ const channelConfig = getChannelDirectory(interaction.channel.id)
102
+ if (channelConfig) {
103
+ if (channelConfig.appId && channelConfig.appId !== appId) {
104
+ await interaction.respond([])
105
+ return
125
106
  }
107
+ projectDirectory = channelConfig.directory
126
108
  }
127
109
  }
128
110
 
@@ -190,22 +172,14 @@ export async function handleSessionAutocomplete({
190
172
 
191
173
  let projectDirectory: string | undefined
192
174
 
193
- if (interaction.channel) {
194
- const channel = interaction.channel
195
- if (channel.type === ChannelType.GuildText) {
196
- const textChannel = channel as TextChannel
197
- if (textChannel.topic) {
198
- const extracted = extractTagsArrays({
199
- xml: textChannel.topic,
200
- tags: ['kimaki.directory', 'kimaki.app'],
201
- })
202
- const channelAppId = extracted['kimaki.app']?.[0]?.trim()
203
- if (channelAppId && channelAppId !== appId) {
204
- await interaction.respond([])
205
- return
206
- }
207
- projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
175
+ if (interaction.channel && interaction.channel.type === ChannelType.GuildText) {
176
+ const channelConfig = getChannelDirectory(interaction.channel.id)
177
+ if (channelConfig) {
178
+ if (channelConfig.appId && channelConfig.appId !== appId) {
179
+ await interaction.respond([])
180
+ return
208
181
  }
182
+ projectDirectory = channelConfig.directory
209
183
  }
210
184
  }
211
185
 
@@ -3,11 +3,10 @@
3
3
 
4
4
  import type { CommandContext, CommandHandler } from './types.js'
5
5
  import { ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
6
- import { extractTagsArrays } from '../xml.js'
7
6
  import { handleOpencodeSession } from '../session-handler.js'
8
7
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
9
8
  import { createLogger } from '../logger.js'
10
- import { getDatabase } from '../database.js'
9
+ import { getDatabase, getChannelDirectory } from '../database.js'
11
10
  import fs from 'node:fs'
12
11
 
13
12
  const userCommandLogger = createLogger('USER_CMD')
@@ -68,28 +67,18 @@ export const handleUserCommand: CommandHandler = async ({ command, appId }: Comm
68
67
  return
69
68
  }
70
69
 
71
- if (textChannel?.topic) {
72
- const extracted = extractTagsArrays({
73
- xml: textChannel.topic,
74
- tags: ['kimaki.directory', 'kimaki.app'],
75
- })
76
-
77
- projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
78
- channelAppId = extracted['kimaki.app']?.[0]?.trim()
70
+ if (textChannel) {
71
+ const channelConfig = getChannelDirectory(textChannel.id)
72
+ projectDirectory = channelConfig?.directory
73
+ channelAppId = channelConfig?.appId || undefined
79
74
  }
80
75
  } else {
81
76
  // Running in a text channel - will create a new thread
82
77
  textChannel = channel as TextChannel
83
78
 
84
- if (textChannel.topic) {
85
- const extracted = extractTagsArrays({
86
- xml: textChannel.topic,
87
- tags: ['kimaki.directory', 'kimaki.app'],
88
- })
89
-
90
- projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
91
- channelAppId = extracted['kimaki.app']?.[0]?.trim()
92
- }
79
+ const channelConfig = getChannelDirectory(textChannel.id)
80
+ projectDirectory = channelConfig?.directory
81
+ channelAppId = channelConfig?.appId || undefined
93
82
  }
94
83
 
95
84
  if (channelAppId && channelAppId !== appId) {
@@ -0,0 +1,71 @@
1
+ // /verbosity command.
2
+ // Sets the output verbosity level for sessions in a channel.
3
+ // 'tools-and-text' (default): shows all output including tool executions
4
+ // 'text-only': only shows text responses (⬥ diamond parts)
5
+
6
+ import { ChatInputCommandInteraction, ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
7
+ import { getChannelVerbosity, setChannelVerbosity, type VerbosityLevel } from '../database.js'
8
+ import { createLogger } from '../logger.js'
9
+
10
+ const verbosityLogger = createLogger('VERBOSITY')
11
+
12
+ /**
13
+ * Handle the /verbosity slash command.
14
+ * Sets output verbosity for the channel (applies to new sessions).
15
+ */
16
+ export async function handleVerbosityCommand({
17
+ command,
18
+ appId,
19
+ }: {
20
+ command: ChatInputCommandInteraction
21
+ appId: string
22
+ }): Promise<void> {
23
+ verbosityLogger.log('[VERBOSITY] Command called')
24
+
25
+ const channel = command.channel
26
+ if (!channel) {
27
+ await command.reply({
28
+ content: 'Could not determine channel.',
29
+ ephemeral: true,
30
+ })
31
+ return
32
+ }
33
+
34
+ // Get the parent channel ID (for threads, use parent; for text channels, use self)
35
+ const channelId = (() => {
36
+ if (channel.type === ChannelType.GuildText) {
37
+ return channel.id
38
+ }
39
+ if (
40
+ channel.type === ChannelType.PublicThread ||
41
+ channel.type === ChannelType.PrivateThread ||
42
+ channel.type === ChannelType.AnnouncementThread
43
+ ) {
44
+ return (channel as ThreadChannel).parentId || channel.id
45
+ }
46
+ return channel.id
47
+ })()
48
+
49
+ const level = command.options.getString('level', true) as VerbosityLevel
50
+ const currentLevel = getChannelVerbosity(channelId)
51
+
52
+ if (currentLevel === level) {
53
+ await command.reply({
54
+ content: `Verbosity is already set to **${level}**.`,
55
+ ephemeral: true,
56
+ })
57
+ return
58
+ }
59
+
60
+ setChannelVerbosity(channelId, level)
61
+ verbosityLogger.log(`[VERBOSITY] Set channel ${channelId} to ${level}`)
62
+
63
+ const description = level === 'text-only'
64
+ ? 'Only text responses will be shown. Tool executions, status messages, and thinking will be hidden.'
65
+ : 'All output will be shown, including tool executions and status messages.'
66
+
67
+ await command.reply({
68
+ content: `Verbosity set to **${level}**.\n\n${description}\n\nThis applies to all new sessions in this channel.`,
69
+ ephemeral: true,
70
+ })
71
+ }
@@ -46,7 +46,7 @@ export async function handleEnableWorktreesCommand({
46
46
  if (!metadata.projectDirectory) {
47
47
  await command.reply({
48
48
  content:
49
- 'This channel is not configured with a project directory.\nAdd a `<kimaki.directory>/path/to/project</kimaki.directory>` tag to the channel description.',
49
+ 'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
50
50
  ephemeral: true,
51
51
  })
52
52
  return
@@ -102,7 +102,7 @@ export async function handleDisableWorktreesCommand({
102
102
  if (!metadata.projectDirectory) {
103
103
  await command.reply({
104
104
  content:
105
- 'This channel is not configured with a project directory.\nAdd a `<kimaki.directory>/path/to/project</kimaki.directory>` tag to the channel description.',
105
+ 'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
106
106
  ephemeral: true,
107
107
  })
108
108
  return