kimaki 0.4.43 → 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.
- package/dist/channel-management.js +6 -15
- package/dist/cli.js +210 -32
- package/dist/commands/merge-worktree.js +152 -0
- package/dist/commands/permissions.js +21 -5
- package/dist/commands/queue.js +5 -1
- package/dist/commands/resume.js +8 -16
- package/dist/commands/session.js +18 -42
- package/dist/commands/user-command.js +8 -17
- package/dist/commands/verbosity.js +53 -0
- package/dist/commands/worktree-settings.js +88 -0
- package/dist/commands/worktree.js +146 -50
- package/dist/database.js +85 -0
- package/dist/discord-bot.js +97 -55
- package/dist/discord-utils.js +51 -13
- package/dist/discord-utils.test.js +20 -0
- package/dist/escape-backticks.test.js +14 -3
- package/dist/interaction-handler.js +15 -0
- package/dist/session-handler.js +549 -412
- package/dist/system-message.js +25 -1
- package/dist/worktree-utils.js +50 -0
- package/package.json +1 -1
- package/src/__snapshots__/first-session-no-info.md +1344 -0
- package/src/__snapshots__/first-session-with-info.md +1350 -0
- package/src/__snapshots__/session-1.md +1344 -0
- package/src/__snapshots__/session-2.md +291 -0
- package/src/__snapshots__/session-3.md +20324 -0
- package/src/__snapshots__/session-with-tools.md +1344 -0
- package/src/channel-management.ts +6 -17
- package/src/cli.ts +250 -35
- package/src/commands/merge-worktree.ts +186 -0
- package/src/commands/permissions.ts +31 -5
- package/src/commands/queue.ts +5 -1
- package/src/commands/resume.ts +8 -18
- package/src/commands/session.ts +18 -44
- package/src/commands/user-command.ts +8 -19
- package/src/commands/verbosity.ts +71 -0
- package/src/commands/worktree-settings.ts +122 -0
- package/src/commands/worktree.ts +174 -55
- package/src/database.ts +108 -0
- package/src/discord-bot.ts +119 -63
- package/src/discord-utils.test.ts +23 -0
- package/src/discord-utils.ts +52 -13
- package/src/escape-backticks.test.ts +14 -3
- package/src/interaction-handler.ts +22 -0
- package/src/session-handler.ts +681 -436
- package/src/system-message.ts +37 -0
- package/src/worktree-utils.ts +78 -0
|
@@ -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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
*/
|
package/src/commands/queue.ts
CHANGED
|
@@ -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
|
|
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
|
package/src/commands/resume.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
161
|
-
if (
|
|
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
|
|
package/src/commands/session.ts
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
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
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// /enable-worktrees and /disable-worktrees commands.
|
|
2
|
+
// Allows per-channel opt-in for automatic worktree creation,
|
|
3
|
+
// as an alternative to the global --use-worktrees CLI flag.
|
|
4
|
+
|
|
5
|
+
import { ChatInputCommandInteraction, ChannelType, type TextChannel } from 'discord.js'
|
|
6
|
+
import { getChannelWorktreesEnabled, setChannelWorktreesEnabled } from '../database.js'
|
|
7
|
+
import { getKimakiMetadata } from '../discord-utils.js'
|
|
8
|
+
import { createLogger } from '../logger.js'
|
|
9
|
+
|
|
10
|
+
const worktreeSettingsLogger = createLogger('WORKTREE_SETTINGS')
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Handle the /enable-worktrees slash command.
|
|
14
|
+
* Enables automatic worktree creation for new sessions in this channel.
|
|
15
|
+
*/
|
|
16
|
+
export async function handleEnableWorktreesCommand({
|
|
17
|
+
command,
|
|
18
|
+
appId,
|
|
19
|
+
}: {
|
|
20
|
+
command: ChatInputCommandInteraction
|
|
21
|
+
appId: string
|
|
22
|
+
}): Promise<void> {
|
|
23
|
+
worktreeSettingsLogger.log('[ENABLE_WORKTREES] Command called')
|
|
24
|
+
|
|
25
|
+
const channel = command.channel
|
|
26
|
+
|
|
27
|
+
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
28
|
+
await command.reply({
|
|
29
|
+
content: 'This command can only be used in text channels (not threads).',
|
|
30
|
+
ephemeral: true,
|
|
31
|
+
})
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const textChannel = channel as TextChannel
|
|
36
|
+
const metadata = getKimakiMetadata(textChannel)
|
|
37
|
+
|
|
38
|
+
if (metadata.channelAppId && metadata.channelAppId !== appId) {
|
|
39
|
+
await command.reply({
|
|
40
|
+
content: 'This channel is configured for a different bot.',
|
|
41
|
+
ephemeral: true,
|
|
42
|
+
})
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!metadata.projectDirectory) {
|
|
47
|
+
await command.reply({
|
|
48
|
+
content:
|
|
49
|
+
'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
|
|
50
|
+
ephemeral: true,
|
|
51
|
+
})
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const wasEnabled = getChannelWorktreesEnabled(textChannel.id)
|
|
56
|
+
setChannelWorktreesEnabled(textChannel.id, true)
|
|
57
|
+
|
|
58
|
+
worktreeSettingsLogger.log(`[ENABLE_WORKTREES] Enabled for channel ${textChannel.id}`)
|
|
59
|
+
|
|
60
|
+
await command.reply({
|
|
61
|
+
content: wasEnabled
|
|
62
|
+
? `Worktrees are already enabled for this channel.\n\nNew sessions started from messages in **#${textChannel.name}** will automatically create git worktrees.`
|
|
63
|
+
: `Worktrees **enabled** for this channel.\n\nNew sessions started from messages in **#${textChannel.name}** will now automatically create git worktrees.\n\nUse \`/disable-worktrees\` to turn this off.`,
|
|
64
|
+
ephemeral: true,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Handle the /disable-worktrees slash command.
|
|
70
|
+
* Disables automatic worktree creation for new sessions in this channel.
|
|
71
|
+
*/
|
|
72
|
+
export async function handleDisableWorktreesCommand({
|
|
73
|
+
command,
|
|
74
|
+
appId,
|
|
75
|
+
}: {
|
|
76
|
+
command: ChatInputCommandInteraction
|
|
77
|
+
appId: string
|
|
78
|
+
}): Promise<void> {
|
|
79
|
+
worktreeSettingsLogger.log('[DISABLE_WORKTREES] Command called')
|
|
80
|
+
|
|
81
|
+
const channel = command.channel
|
|
82
|
+
|
|
83
|
+
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
84
|
+
await command.reply({
|
|
85
|
+
content: 'This command can only be used in text channels (not threads).',
|
|
86
|
+
ephemeral: true,
|
|
87
|
+
})
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const textChannel = channel as TextChannel
|
|
92
|
+
const metadata = getKimakiMetadata(textChannel)
|
|
93
|
+
|
|
94
|
+
if (metadata.channelAppId && metadata.channelAppId !== appId) {
|
|
95
|
+
await command.reply({
|
|
96
|
+
content: 'This channel is configured for a different bot.',
|
|
97
|
+
ephemeral: true,
|
|
98
|
+
})
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!metadata.projectDirectory) {
|
|
103
|
+
await command.reply({
|
|
104
|
+
content:
|
|
105
|
+
'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
|
|
106
|
+
ephemeral: true,
|
|
107
|
+
})
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const wasEnabled = getChannelWorktreesEnabled(textChannel.id)
|
|
112
|
+
setChannelWorktreesEnabled(textChannel.id, false)
|
|
113
|
+
|
|
114
|
+
worktreeSettingsLogger.log(`[DISABLE_WORKTREES] Disabled for channel ${textChannel.id}`)
|
|
115
|
+
|
|
116
|
+
await command.reply({
|
|
117
|
+
content: wasEnabled
|
|
118
|
+
? `Worktrees **disabled** for this channel.\n\nNew sessions started from messages in **#${textChannel.name}** will use the main project directory.\n\nUse \`/enable-worktrees\` to turn this back on.`
|
|
119
|
+
: `Worktrees are already disabled for this channel.\n\nNew sessions will use the main project directory.`,
|
|
120
|
+
ephemeral: true,
|
|
121
|
+
})
|
|
122
|
+
}
|