kimaki 0.4.46 → 0.4.48

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 (85) hide show
  1. package/dist/cli.js +69 -21
  2. package/dist/commands/abort.js +4 -2
  3. package/dist/commands/add-project.js +2 -2
  4. package/dist/commands/agent.js +4 -4
  5. package/dist/commands/ask-question.js +9 -8
  6. package/dist/commands/compact.js +126 -0
  7. package/dist/commands/create-new-project.js +60 -30
  8. package/dist/commands/fork.js +3 -3
  9. package/dist/commands/merge-worktree.js +23 -10
  10. package/dist/commands/model.js +5 -5
  11. package/dist/commands/permissions.js +5 -3
  12. package/dist/commands/queue.js +2 -2
  13. package/dist/commands/remove-project.js +2 -2
  14. package/dist/commands/resume.js +2 -2
  15. package/dist/commands/session.js +6 -3
  16. package/dist/commands/share.js +2 -2
  17. package/dist/commands/undo-redo.js +2 -2
  18. package/dist/commands/user-command.js +2 -2
  19. package/dist/commands/verbosity.js +5 -5
  20. package/dist/commands/worktree-settings.js +2 -2
  21. package/dist/commands/worktree.js +18 -8
  22. package/dist/config.js +7 -0
  23. package/dist/database.js +10 -7
  24. package/dist/discord-bot.js +30 -12
  25. package/dist/discord-utils.js +2 -2
  26. package/dist/genai-worker-wrapper.js +3 -3
  27. package/dist/genai-worker.js +2 -2
  28. package/dist/genai.js +2 -2
  29. package/dist/interaction-handler.js +6 -2
  30. package/dist/logger.js +57 -9
  31. package/dist/markdown.js +2 -2
  32. package/dist/message-formatting.js +91 -6
  33. package/dist/openai-realtime.js +2 -2
  34. package/dist/opencode.js +19 -25
  35. package/dist/session-handler.js +89 -29
  36. package/dist/system-message.js +11 -9
  37. package/dist/tools.js +3 -2
  38. package/dist/utils.js +1 -0
  39. package/dist/voice-handler.js +2 -2
  40. package/dist/voice.js +2 -2
  41. package/dist/worktree-utils.js +91 -7
  42. package/dist/xml.js +2 -2
  43. package/package.json +3 -3
  44. package/src/cli.ts +108 -21
  45. package/src/commands/abort.ts +4 -2
  46. package/src/commands/add-project.ts +2 -2
  47. package/src/commands/agent.ts +4 -4
  48. package/src/commands/ask-question.ts +9 -8
  49. package/src/commands/compact.ts +148 -0
  50. package/src/commands/create-new-project.ts +87 -36
  51. package/src/commands/fork.ts +3 -3
  52. package/src/commands/merge-worktree.ts +47 -10
  53. package/src/commands/model.ts +5 -5
  54. package/src/commands/permissions.ts +6 -2
  55. package/src/commands/queue.ts +2 -2
  56. package/src/commands/remove-project.ts +2 -2
  57. package/src/commands/resume.ts +2 -2
  58. package/src/commands/session.ts +6 -3
  59. package/src/commands/share.ts +2 -2
  60. package/src/commands/undo-redo.ts +2 -2
  61. package/src/commands/user-command.ts +2 -2
  62. package/src/commands/verbosity.ts +5 -5
  63. package/src/commands/worktree-settings.ts +2 -2
  64. package/src/commands/worktree.ts +20 -7
  65. package/src/config.ts +14 -0
  66. package/src/database.ts +13 -7
  67. package/src/discord-bot.ts +45 -12
  68. package/src/discord-utils.ts +2 -2
  69. package/src/genai-worker-wrapper.ts +3 -3
  70. package/src/genai-worker.ts +2 -2
  71. package/src/genai.ts +2 -2
  72. package/src/interaction-handler.ts +7 -2
  73. package/src/logger.ts +64 -10
  74. package/src/markdown.ts +2 -2
  75. package/src/message-formatting.ts +100 -6
  76. package/src/openai-realtime.ts +2 -2
  77. package/src/opencode.ts +19 -26
  78. package/src/session-handler.ts +102 -29
  79. package/src/system-message.ts +11 -9
  80. package/src/tools.ts +3 -2
  81. package/src/utils.ts +1 -0
  82. package/src/voice-handler.ts +2 -2
  83. package/src/voice.ts +2 -2
  84. package/src/worktree-utils.ts +111 -7
  85. package/src/xml.ts +2 -2
@@ -0,0 +1,148 @@
1
+ // /compact command - Trigger context compaction (summarization) for the current session.
2
+
3
+ import { ChannelType, type ThreadChannel } from 'discord.js'
4
+ import type { CommandContext } from './types.js'
5
+ import { getDatabase } from '../database.js'
6
+ import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js'
7
+ import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
+
10
+ const logger = createLogger(LogPrefix.COMPACT)
11
+
12
+ export async function handleCompactCommand({ command }: CommandContext): Promise<void> {
13
+ const channel = command.channel
14
+
15
+ if (!channel) {
16
+ await command.reply({
17
+ content: 'This command can only be used in a channel',
18
+ ephemeral: true,
19
+ flags: SILENT_MESSAGE_FLAGS,
20
+ })
21
+ return
22
+ }
23
+
24
+ const isThread = [
25
+ ChannelType.PublicThread,
26
+ ChannelType.PrivateThread,
27
+ ChannelType.AnnouncementThread,
28
+ ].includes(channel.type)
29
+
30
+ if (!isThread) {
31
+ await command.reply({
32
+ content: 'This command can only be used in a thread with an active session',
33
+ ephemeral: true,
34
+ flags: SILENT_MESSAGE_FLAGS,
35
+ })
36
+ return
37
+ }
38
+
39
+ const textChannel = await resolveTextChannel(channel as ThreadChannel)
40
+ const { projectDirectory: directory } = getKimakiMetadata(textChannel)
41
+
42
+ if (!directory) {
43
+ await command.reply({
44
+ content: 'Could not determine project directory for this channel',
45
+ ephemeral: true,
46
+ flags: SILENT_MESSAGE_FLAGS,
47
+ })
48
+ return
49
+ }
50
+
51
+ const row = getDatabase()
52
+ .prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
53
+ .get(channel.id) as { session_id: string } | undefined
54
+
55
+ if (!row?.session_id) {
56
+ await command.reply({
57
+ content: 'No active session in this thread',
58
+ ephemeral: true,
59
+ flags: SILENT_MESSAGE_FLAGS,
60
+ })
61
+ return
62
+ }
63
+
64
+ const sessionId = row.session_id
65
+
66
+ // Ensure server is running for this directory
67
+ const getClient = await initializeOpencodeForDirectory(directory)
68
+ if (getClient instanceof Error) {
69
+ await command.reply({
70
+ content: `Failed to compact: ${getClient.message}`,
71
+ ephemeral: true,
72
+ flags: SILENT_MESSAGE_FLAGS,
73
+ })
74
+ return
75
+ }
76
+
77
+ const clientV2 = getOpencodeClientV2(directory)
78
+ if (!clientV2) {
79
+ await command.reply({
80
+ content: 'Failed to get OpenCode client',
81
+ ephemeral: true,
82
+ flags: SILENT_MESSAGE_FLAGS,
83
+ })
84
+ return
85
+ }
86
+
87
+ // Defer reply since compaction may take a moment
88
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
89
+
90
+ try {
91
+ // Get session messages to find the model from the last user message
92
+ const messagesResult = await clientV2.session.messages({
93
+ sessionID: sessionId,
94
+ directory,
95
+ })
96
+
97
+ if (messagesResult.error || !messagesResult.data) {
98
+ logger.error('[COMPACT] Failed to get messages:', messagesResult.error)
99
+ await command.editReply({
100
+ content: 'Failed to compact: Could not retrieve session messages',
101
+ })
102
+ return
103
+ }
104
+
105
+ // Find the last user message to get the model
106
+ const lastUserMessage = [...messagesResult.data]
107
+ .reverse()
108
+ .find((msg) => msg.info.role === 'user')
109
+
110
+ if (!lastUserMessage || lastUserMessage.info.role !== 'user') {
111
+ await command.editReply({
112
+ content: 'Failed to compact: No user message found in session',
113
+ })
114
+ return
115
+ }
116
+
117
+ const { providerID, modelID } = lastUserMessage.info.model
118
+
119
+ const result = await clientV2.session.summarize({
120
+ sessionID: sessionId,
121
+ directory,
122
+ providerID,
123
+ modelID,
124
+ auto: false,
125
+ })
126
+
127
+ if (result.error) {
128
+ logger.error('[COMPACT] Error:', result.error)
129
+ const errorMessage = 'data' in result.error && result.error.data
130
+ ? (result.error.data as { message?: string }).message || 'Unknown error'
131
+ : 'Unknown error'
132
+ await command.editReply({
133
+ content: `Failed to compact: ${errorMessage}`,
134
+ })
135
+ return
136
+ }
137
+
138
+ await command.editReply({
139
+ content: `📦 Session **compacted** successfully`,
140
+ })
141
+ logger.log(`Session ${sessionId} compacted by user`)
142
+ } catch (error) {
143
+ logger.error('[COMPACT] Error:', error)
144
+ await command.editReply({
145
+ content: `Failed to compact: ${error instanceof Error ? error.message : 'Unknown error'}`,
146
+ })
147
+ }
148
+ }
@@ -1,16 +1,79 @@
1
1
  // /create-new-project command - Create a new project folder, initialize git, and start a session.
2
+ // Also exports createNewProject() for reuse during onboarding (welcome channel creation).
2
3
 
3
- import { ChannelType, type TextChannel } from 'discord.js'
4
+ import { ChannelType, type Guild, type TextChannel } from 'discord.js'
4
5
  import fs from 'node:fs'
5
6
  import path from 'node:path'
7
+ import { execSync } from 'node:child_process'
6
8
  import type { CommandContext } from './types.js'
7
9
  import { getProjectsDir } from '../config.js'
8
10
  import { createProjectChannels } from '../channel-management.js'
9
11
  import { handleOpencodeSession } from '../session-handler.js'
10
12
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
11
- import { createLogger } from '../logger.js'
13
+ import { createLogger, LogPrefix } from '../logger.js'
14
+
15
+ const logger = createLogger(LogPrefix.CREATE_PROJECT)
16
+
17
+ /**
18
+ * Core project creation logic: creates directory, inits git, creates Discord channels.
19
+ * Reused by the slash command handler and by onboarding (welcome channel).
20
+ * Returns null if the project directory already exists.
21
+ */
22
+ export async function createNewProject({
23
+ guild,
24
+ projectName,
25
+ appId,
26
+ botName,
27
+ }: {
28
+ guild: Guild
29
+ projectName: string
30
+ appId: string
31
+ botName?: string
32
+ }): Promise<{
33
+ textChannelId: string
34
+ voiceChannelId: string
35
+ channelName: string
36
+ projectDirectory: string
37
+ sanitizedName: string
38
+ } | null> {
39
+ const sanitizedName = projectName
40
+ .toLowerCase()
41
+ .replace(/[^a-z0-9-]/g, '-')
42
+ .replace(/-+/g, '-')
43
+ .replace(/^-|-$/g, '')
44
+ .slice(0, 100)
45
+
46
+ if (!sanitizedName) {
47
+ return null
48
+ }
12
49
 
13
- const logger = createLogger('CREATE-NEW-PROJECT')
50
+ const projectsDir = getProjectsDir()
51
+ const projectDirectory = path.join(projectsDir, sanitizedName)
52
+
53
+ if (!fs.existsSync(projectsDir)) {
54
+ fs.mkdirSync(projectsDir, { recursive: true })
55
+ logger.log(`Created projects directory: ${projectsDir}`)
56
+ }
57
+
58
+ if (fs.existsSync(projectDirectory)) {
59
+ return null
60
+ }
61
+
62
+ fs.mkdirSync(projectDirectory, { recursive: true })
63
+ logger.log(`Created project directory: ${projectDirectory}`)
64
+
65
+ execSync('git init', { cwd: projectDirectory, stdio: 'pipe' })
66
+ logger.log(`Initialized git in: ${projectDirectory}`)
67
+
68
+ const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
69
+ guild,
70
+ projectDirectory,
71
+ appId,
72
+ botName,
73
+ })
74
+
75
+ return { textChannelId, voiceChannelId, channelName, projectDirectory, sanitizedName }
76
+ }
14
77
 
15
78
  export async function handleCreateNewProjectCommand({
16
79
  command,
@@ -32,49 +95,37 @@ export async function handleCreateNewProjectCommand({
32
95
  return
33
96
  }
34
97
 
35
- const sanitizedName = projectName
36
- .toLowerCase()
37
- .replace(/[^a-z0-9-]/g, '-')
38
- .replace(/-+/g, '-')
39
- .replace(/^-|-$/g, '')
40
- .slice(0, 100)
41
-
42
- if (!sanitizedName) {
43
- await command.editReply('Invalid project name')
44
- return
45
- }
98
+ try {
99
+ const result = await createNewProject({
100
+ guild,
101
+ projectName,
102
+ appId,
103
+ botName: command.client.user?.username,
104
+ })
46
105
 
47
- const projectsDir = getProjectsDir()
48
- const projectDirectory = path.join(projectsDir, sanitizedName)
106
+ if (!result) {
107
+ const sanitizedName = projectName
108
+ .toLowerCase()
109
+ .replace(/[^a-z0-9-]/g, '-')
110
+ .replace(/-+/g, '-')
111
+ .replace(/^-|-$/g, '')
112
+ .slice(0, 100)
49
113
 
50
- try {
51
- if (!fs.existsSync(projectsDir)) {
52
- fs.mkdirSync(projectsDir, { recursive: true })
53
- logger.log(`Created projects directory: ${projectsDir}`)
54
- }
114
+ if (!sanitizedName) {
115
+ await command.editReply('Invalid project name')
116
+ return
117
+ }
55
118
 
56
- if (fs.existsSync(projectDirectory)) {
119
+ const projectDirectory = path.join(getProjectsDir(), sanitizedName)
57
120
  await command.editReply(`Project directory already exists: ${projectDirectory}`)
58
121
  return
59
122
  }
60
123
 
61
- fs.mkdirSync(projectDirectory, { recursive: true })
62
- logger.log(`Created project directory: ${projectDirectory}`)
63
-
64
- const { execSync } = await import('node:child_process')
65
- execSync('git init', { cwd: projectDirectory, stdio: 'pipe' })
66
- logger.log(`Initialized git in: ${projectDirectory}`)
67
-
68
- const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
69
- guild,
70
- projectDirectory,
71
- appId,
72
- })
73
-
124
+ const { textChannelId, voiceChannelId, channelName, projectDirectory, sanitizedName } = result
74
125
  const textChannel = (await guild.channels.fetch(textChannelId)) as TextChannel
75
126
 
76
127
  await command.editReply(
77
- `✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n\n_Starting session..._`,
128
+ `✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n_Starting session..._`,
78
129
  )
79
130
 
80
131
  const starterMessage = await textChannel.send({
@@ -13,11 +13,11 @@ import { getDatabase } from '../database.js'
13
13
  import { initializeOpencodeForDirectory } from '../opencode.js'
14
14
  import { resolveTextChannel, getKimakiMetadata, sendThreadMessage } from '../discord-utils.js'
15
15
  import { collectLastAssistantParts } from '../message-formatting.js'
16
- import { createLogger } from '../logger.js'
16
+ import { createLogger, LogPrefix } from '../logger.js'
17
17
  import * as errore from 'errore'
18
18
 
19
- const sessionLogger = createLogger('SESSION')
20
- const forkLogger = createLogger('FORK')
19
+ const sessionLogger = createLogger(LogPrefix.SESSION)
20
+ const forkLogger = createLogger(LogPrefix.FORK)
21
21
 
22
22
  export async function handleForkCommand(interaction: ChatInputCommandInteraction): Promise<void> {
23
23
  const channel = interaction.channel
@@ -5,10 +5,10 @@
5
5
  import { type ThreadChannel } from 'discord.js'
6
6
  import type { CommandContext } from './types.js'
7
7
  import { getThreadWorktree } from '../database.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
  import { execAsync } from '../worktree-utils.js'
10
10
 
11
- const logger = createLogger('MERGE-WORKTREE')
11
+ const logger = createLogger(LogPrefix.WORKTREE)
12
12
 
13
13
  /** Worktree thread title prefix - indicates unmerged worktree */
14
14
  export const WORKTREE_PREFIX = '⬦ '
@@ -47,7 +47,11 @@ async function isDetachedHead(worktreeDir: string): Promise<boolean> {
47
47
  try {
48
48
  await execAsync(`git -C "${worktreeDir}" symbolic-ref HEAD`)
49
49
  return false
50
- } catch {
50
+ } catch (error) {
51
+ logger.debug(
52
+ `Failed to resolve HEAD for ${worktreeDir}:`,
53
+ error instanceof Error ? error.message : String(error),
54
+ )
51
55
  return true
52
56
  }
53
57
  }
@@ -59,7 +63,11 @@ async function getCurrentBranch(worktreeDir: string): Promise<string | null> {
59
63
  try {
60
64
  const { stdout } = await execAsync(`git -C "${worktreeDir}" symbolic-ref --short HEAD`)
61
65
  return stdout.trim() || null
62
- } catch {
66
+ } catch (error) {
67
+ logger.debug(
68
+ `Failed to get current branch for ${worktreeDir}:`,
69
+ error instanceof Error ? error.message : String(error),
70
+ )
63
71
  return null
64
72
  }
65
73
  }
@@ -113,7 +121,11 @@ export async function handleMergeWorktreeCommand({ command, appId }: CommandCont
113
121
  `git -C "${mainRepoDir}" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@'`,
114
122
  )
115
123
  defaultBranch = stdout.trim() || 'main'
116
- } catch {
124
+ } catch (error) {
125
+ logger.warn(
126
+ `Failed to detect default branch for ${mainRepoDir}, falling back to main:`,
127
+ error instanceof Error ? error.message : String(error),
128
+ )
117
129
  defaultBranch = 'main'
118
130
  }
119
131
 
@@ -141,11 +153,26 @@ export async function handleMergeWorktreeCommand({ command, appId }: CommandCont
141
153
  await execAsync(`git -C "${worktreeDir}" merge ${defaultBranch} --no-edit`)
142
154
  } catch (e) {
143
155
  // If merge fails (conflicts), abort and report
144
- await execAsync(`git -C "${worktreeDir}" merge --abort`).catch(() => {})
156
+ await execAsync(`git -C "${worktreeDir}" merge --abort`).catch((error) => {
157
+ logger.warn(
158
+ `Failed to abort merge in ${worktreeDir}:`,
159
+ error instanceof Error ? error.message : String(error),
160
+ )
161
+ })
145
162
  // Clean up temp branch if we created one
146
163
  if (tempBranch) {
147
- await execAsync(`git -C "${worktreeDir}" checkout --detach`).catch(() => {})
148
- await execAsync(`git -C "${worktreeDir}" branch -D ${tempBranch}`).catch(() => {})
164
+ await execAsync(`git -C "${worktreeDir}" checkout --detach`).catch((error) => {
165
+ logger.warn(
166
+ `Failed to detach HEAD after merge conflict in ${worktreeDir}:`,
167
+ error instanceof Error ? error.message : String(error),
168
+ )
169
+ })
170
+ await execAsync(`git -C "${worktreeDir}" branch -D ${tempBranch}`).catch((error) => {
171
+ logger.warn(
172
+ `Failed to delete temp branch ${tempBranch} in ${worktreeDir}:`,
173
+ error instanceof Error ? error.message : String(error),
174
+ )
175
+ })
149
176
  }
150
177
  throw new Error(`Merge conflict - resolve manually in worktree then retry`)
151
178
  }
@@ -162,11 +189,21 @@ export async function handleMergeWorktreeCommand({ command, appId }: CommandCont
162
189
 
163
190
  // 7. Delete the merged branch (temp or original)
164
191
  logger.log(`Deleting merged branch ${branchToMerge}`)
165
- await execAsync(`git -C "${worktreeDir}" branch -D ${branchToMerge}`).catch(() => {})
192
+ await execAsync(`git -C "${worktreeDir}" branch -D ${branchToMerge}`).catch((error) => {
193
+ logger.warn(
194
+ `Failed to delete merged branch ${branchToMerge} in ${worktreeDir}:`,
195
+ error instanceof Error ? error.message : String(error),
196
+ )
197
+ })
166
198
 
167
199
  // Also delete the original worktree branch if different from what we merged
168
200
  if (!isDetached && branchToMerge !== worktreeInfo.worktree_name) {
169
- await execAsync(`git -C "${worktreeDir}" branch -D ${worktreeInfo.worktree_name}`).catch(() => {})
201
+ await execAsync(`git -C "${worktreeDir}" branch -D ${worktreeInfo.worktree_name}`).catch((error) => {
202
+ logger.warn(
203
+ `Failed to delete worktree branch ${worktreeInfo.worktree_name} in ${worktreeDir}:`,
204
+ error instanceof Error ? error.message : String(error),
205
+ )
206
+ })
170
207
  }
171
208
 
172
209
  // 8. Remove worktree prefix from thread title (fire and forget with timeout)
@@ -14,10 +14,10 @@ import { getDatabase, setChannelModel, setSessionModel, runModelMigrations } fro
14
14
  import { initializeOpencodeForDirectory } from '../opencode.js'
15
15
  import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
16
16
  import { abortAndRetrySession } from '../session-handler.js'
17
- import { createLogger } from '../logger.js'
17
+ import { createLogger, LogPrefix } from '../logger.js'
18
18
  import * as errore from 'errore'
19
19
 
20
- const modelLogger = createLogger('MODEL')
20
+ const modelLogger = createLogger(LogPrefix.MODEL)
21
21
 
22
22
  // Store context by hash to avoid customId length limits (Discord max: 100 chars)
23
23
  const pendingModelContexts = new Map<
@@ -385,12 +385,12 @@ export async function handleModelSelectMenu(
385
385
 
386
386
  if (retried) {
387
387
  await interaction.editReply({
388
- content: `Model changed for this session:\n**${context.providerName}** / **${selectedModelId}**\n\n\`${fullModelId}\`\n\n_Retrying current request with new model..._`,
388
+ content: `Model changed for this session:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\n_Retrying current request with new model..._`,
389
389
  components: [],
390
390
  })
391
391
  } else {
392
392
  await interaction.editReply({
393
- content: `Model preference set for this session:\n**${context.providerName}** / **${selectedModelId}**\n\n\`${fullModelId}\``,
393
+ content: `Model preference set for this session:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\``,
394
394
  components: [],
395
395
  })
396
396
  }
@@ -400,7 +400,7 @@ export async function handleModelSelectMenu(
400
400
  modelLogger.log(`Set model ${fullModelId} for channel ${context.channelId}`)
401
401
 
402
402
  await interaction.editReply({
403
- content: `Model preference set for this channel:\n**${context.providerName}** / **${selectedModelId}**\n\n\`${fullModelId}\`\n\nAll new sessions in this channel will use this model.`,
403
+ content: `Model preference set for this channel:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\nAll new sessions in this channel will use this model.`,
404
404
  components: [],
405
405
  })
406
406
  }
@@ -12,9 +12,9 @@ import crypto from 'node:crypto'
12
12
  import type { PermissionRequest } from '@opencode-ai/sdk/v2'
13
13
  import { getOpencodeClientV2 } from '../opencode.js'
14
14
  import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js'
15
- import { createLogger } from '../logger.js'
15
+ import { createLogger, LogPrefix } from '../logger.js'
16
16
 
17
- const logger = createLogger('PERMISSIONS')
17
+ const logger = createLogger(LogPrefix.PERMISSIONS)
18
18
 
19
19
  type PendingPermissionContext = {
20
20
  permission: PermissionRequest
@@ -35,10 +35,12 @@ export async function showPermissionDropdown({
35
35
  thread,
36
36
  permission,
37
37
  directory,
38
+ subtaskLabel,
38
39
  }: {
39
40
  thread: ThreadChannel
40
41
  permission: PermissionRequest
41
42
  directory: string
43
+ subtaskLabel?: string
42
44
  }): Promise<{ messageId: string; contextHash: string }> {
43
45
  const contextHash = crypto.randomBytes(8).toString('hex')
44
46
 
@@ -80,9 +82,11 @@ export async function showPermissionDropdown({
80
82
 
81
83
  const actionRow = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
82
84
 
85
+ const subtaskLine = subtaskLabel ? `**From:** \`${subtaskLabel}\`\n` : ''
83
86
  const permissionMessage = await thread.send({
84
87
  content:
85
88
  `⚠️ **Permission Required**\n\n` +
89
+ subtaskLine +
86
90
  `**Type:** \`${permission.permission}\`\n` +
87
91
  (patternStr ? `**Pattern:** \`${patternStr}\`` : ''),
88
92
  components: [actionRow],
@@ -16,9 +16,9 @@ import {
16
16
  getQueueLength,
17
17
  clearQueue,
18
18
  } from '../session-handler.js'
19
- import { createLogger } from '../logger.js'
19
+ import { createLogger, LogPrefix } from '../logger.js'
20
20
 
21
- const logger = createLogger('QUEUE')
21
+ const logger = createLogger(LogPrefix.QUEUE)
22
22
 
23
23
  export async function handleQueueCommand({ command }: CommandContext): Promise<void> {
24
24
  const message = command.options.getString('message', true)
@@ -4,10 +4,10 @@ import path from 'node:path'
4
4
  import * as errore from 'errore'
5
5
  import type { CommandContext, AutocompleteContext } from './types.js'
6
6
  import { getDatabase } from '../database.js'
7
- import { createLogger } from '../logger.js'
7
+ import { createLogger, LogPrefix } from '../logger.js'
8
8
  import { abbreviatePath } from '../utils.js'
9
9
 
10
- const logger = createLogger('REMOVE-PROJECT')
10
+ const logger = createLogger(LogPrefix.REMOVE_PROJECT)
11
11
 
12
12
  export async function handleRemoveProjectCommand({ command, appId }: CommandContext): Promise<void> {
13
13
  await command.deferReply({ ephemeral: false })
@@ -12,10 +12,10 @@ import { getDatabase, getChannelDirectory } from '../database.js'
12
12
  import { initializeOpencodeForDirectory } from '../opencode.js'
13
13
  import { sendThreadMessage, resolveTextChannel } from '../discord-utils.js'
14
14
  import { collectLastAssistantParts } from '../message-formatting.js'
15
- import { createLogger } from '../logger.js'
15
+ import { createLogger, LogPrefix } from '../logger.js'
16
16
  import * as errore from 'errore'
17
17
 
18
- const logger = createLogger('RESUME')
18
+ const logger = createLogger(LogPrefix.RESUME)
19
19
 
20
20
  export async function handleResumeCommand({ command, appId }: CommandContext): Promise<void> {
21
21
  await command.deferReply({ ephemeral: false })
@@ -8,10 +8,10 @@ import { getDatabase, getChannelDirectory } from '../database.js'
8
8
  import { initializeOpencodeForDirectory } from '../opencode.js'
9
9
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
10
10
  import { handleOpencodeSession } from '../session-handler.js'
11
- import { createLogger } from '../logger.js'
11
+ import { createLogger, LogPrefix } from '../logger.js'
12
12
  import * as errore from 'errore'
13
13
 
14
- const logger = createLogger('SESSION')
14
+ const logger = createLogger(LogPrefix.SESSION)
15
15
 
16
16
  export async function handleSessionCommand({ command, appId }: CommandContext): Promise<void> {
17
17
  await command.deferReply({ ephemeral: false })
@@ -133,7 +133,10 @@ async function handleAgentAutocomplete({ interaction, appId }: AutocompleteConte
133
133
  }
134
134
 
135
135
  const agents = agentsResponse.data
136
- .filter((a) => a.mode === 'primary' || a.mode === 'all')
136
+ .filter((a) => {
137
+ const hidden = (a as { hidden?: boolean }).hidden
138
+ return (a.mode === 'primary' || a.mode === 'all') && !hidden
139
+ })
137
140
  .filter((a) => a.name.toLowerCase().includes(focusedValue.toLowerCase()))
138
141
  .slice(0, 25)
139
142
 
@@ -5,10 +5,10 @@ import type { CommandContext } from './types.js'
5
5
  import { getDatabase } from '../database.js'
6
6
  import { initializeOpencodeForDirectory } from '../opencode.js'
7
7
  import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
  import * as errore from 'errore'
10
10
 
11
- const logger = createLogger('SHARE')
11
+ const logger = createLogger(LogPrefix.SHARE)
12
12
 
13
13
  export async function handleShareCommand({ command }: CommandContext): Promise<void> {
14
14
  const channel = command.channel
@@ -5,10 +5,10 @@ import type { CommandContext } from './types.js'
5
5
  import { getDatabase } from '../database.js'
6
6
  import { initializeOpencodeForDirectory } from '../opencode.js'
7
7
  import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
  import * as errore from 'errore'
10
10
 
11
- const logger = createLogger('UNDO-REDO')
11
+ const logger = createLogger(LogPrefix.UNDO_REDO)
12
12
 
13
13
  export async function handleUndoCommand({ command }: CommandContext): Promise<void> {
14
14
  const channel = command.channel
@@ -5,11 +5,11 @@ import type { CommandContext, CommandHandler } from './types.js'
5
5
  import { ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
6
6
  import { handleOpencodeSession } from '../session-handler.js'
7
7
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
  import { getDatabase, getChannelDirectory } from '../database.js'
10
10
  import fs from 'node:fs'
11
11
 
12
- const userCommandLogger = createLogger('USER_CMD')
12
+ const userCommandLogger = createLogger(LogPrefix.USER_CMD)
13
13
 
14
14
  export const handleUserCommand: CommandHandler = async ({ command, appId }: CommandContext) => {
15
15
  const discordCommandName = command.commandName
@@ -5,13 +5,13 @@
5
5
 
6
6
  import { ChatInputCommandInteraction, ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
7
7
  import { getChannelVerbosity, setChannelVerbosity, type VerbosityLevel } from '../database.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
 
10
- const verbosityLogger = createLogger('VERBOSITY')
10
+ const verbosityLogger = createLogger(LogPrefix.VERBOSITY)
11
11
 
12
12
  /**
13
13
  * Handle the /verbosity slash command.
14
- * Sets output verbosity for the channel (applies to new sessions).
14
+ * Sets output verbosity for the channel (applies immediately, even mid-session).
15
15
  */
16
16
  export async function handleVerbosityCommand({
17
17
  command,
@@ -51,7 +51,7 @@ export async function handleVerbosityCommand({
51
51
 
52
52
  if (currentLevel === level) {
53
53
  await command.reply({
54
- content: `Verbosity is already set to **${level}**.`,
54
+ content: `Verbosity is already set to **${level}** for this channel.`,
55
55
  ephemeral: true,
56
56
  })
57
57
  return
@@ -65,7 +65,7 @@ export async function handleVerbosityCommand({
65
65
  : 'All output will be shown, including tool executions and status messages.'
66
66
 
67
67
  await command.reply({
68
- content: `Verbosity set to **${level}**.\n\n${description}\n\nThis applies to all new sessions in this channel.`,
68
+ content: `Verbosity set to **${level}** for this channel.\n${description}\nThis is a per-channel setting and applies immediately, including any active sessions.`,
69
69
  ephemeral: true,
70
70
  })
71
71
  }
@@ -5,9 +5,9 @@
5
5
  import { ChatInputCommandInteraction, ChannelType, type TextChannel } from 'discord.js'
6
6
  import { getChannelWorktreesEnabled, setChannelWorktreesEnabled } from '../database.js'
7
7
  import { getKimakiMetadata } from '../discord-utils.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
 
10
- const worktreeSettingsLogger = createLogger('WORKTREE_SETTINGS')
10
+ const worktreeSettingsLogger = createLogger(LogPrefix.WORKTREE)
11
11
 
12
12
  /**
13
13
  * Handle the /enable-worktrees slash command.