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.
Files changed (47) hide show
  1. package/dist/channel-management.js +6 -15
  2. package/dist/cli.js +210 -32
  3. package/dist/commands/merge-worktree.js +152 -0
  4. package/dist/commands/permissions.js +21 -5
  5. package/dist/commands/queue.js +5 -1
  6. package/dist/commands/resume.js +8 -16
  7. package/dist/commands/session.js +18 -42
  8. package/dist/commands/user-command.js +8 -17
  9. package/dist/commands/verbosity.js +53 -0
  10. package/dist/commands/worktree-settings.js +88 -0
  11. package/dist/commands/worktree.js +146 -50
  12. package/dist/database.js +85 -0
  13. package/dist/discord-bot.js +97 -55
  14. package/dist/discord-utils.js +51 -13
  15. package/dist/discord-utils.test.js +20 -0
  16. package/dist/escape-backticks.test.js +14 -3
  17. package/dist/interaction-handler.js +15 -0
  18. package/dist/session-handler.js +549 -412
  19. package/dist/system-message.js +25 -1
  20. package/dist/worktree-utils.js +50 -0
  21. package/package.json +1 -1
  22. package/src/__snapshots__/first-session-no-info.md +1344 -0
  23. package/src/__snapshots__/first-session-with-info.md +1350 -0
  24. package/src/__snapshots__/session-1.md +1344 -0
  25. package/src/__snapshots__/session-2.md +291 -0
  26. package/src/__snapshots__/session-3.md +20324 -0
  27. package/src/__snapshots__/session-with-tools.md +1344 -0
  28. package/src/channel-management.ts +6 -17
  29. package/src/cli.ts +250 -35
  30. package/src/commands/merge-worktree.ts +186 -0
  31. package/src/commands/permissions.ts +31 -5
  32. package/src/commands/queue.ts +5 -1
  33. package/src/commands/resume.ts +8 -18
  34. package/src/commands/session.ts +18 -44
  35. package/src/commands/user-command.ts +8 -19
  36. package/src/commands/verbosity.ts +71 -0
  37. package/src/commands/worktree-settings.ts +122 -0
  38. package/src/commands/worktree.ts +174 -55
  39. package/src/database.ts +108 -0
  40. package/src/discord-bot.ts +119 -63
  41. package/src/discord-utils.test.ts +23 -0
  42. package/src/discord-utils.ts +52 -13
  43. package/src/escape-backticks.test.ts +14 -3
  44. package/src/interaction-handler.ts +22 -0
  45. package/src/session-handler.ts +681 -436
  46. package/src/system-message.ts +37 -0
  47. package/src/worktree-utils.ts +78 -0
@@ -1,8 +1,11 @@
1
1
  // Core Discord bot module that handles message events and bot lifecycle.
2
2
  // Bridges Discord messages to OpenCode sessions, manages voice connections,
3
3
  // and orchestrates the main event loop for the Kimaki bot.
4
- import { getDatabase, closeDatabase, getThreadWorktree } from './database.js';
5
- import { initializeOpencodeForDirectory, getOpencodeServers } from './opencode.js';
4
+ import { getDatabase, closeDatabase, getThreadWorktree, createPendingWorktree, setWorktreeReady, setWorktreeError, getChannelWorktreesEnabled, getChannelDirectory, } from './database.js';
5
+ import { initializeOpencodeForDirectory, getOpencodeServers, getOpencodeClientV2 } from './opencode.js';
6
+ import { formatWorktreeName } from './commands/worktree.js';
7
+ import { WORKTREE_PREFIX } from './commands/merge-worktree.js';
8
+ import { createWorktreeWithSubmodules } from './worktree-utils.js';
6
9
  import { escapeBackticksInCodeBlocks, splitMarkdownForDiscord, SILENT_MESSAGE_FLAGS, } from './discord-utils.js';
7
10
  import { getOpencodeSystemMessage } from './system-message.js';
8
11
  import { getFileAttachments, getTextAttachments } from './message-formatting.js';
@@ -11,7 +14,7 @@ import { voiceConnections, cleanupVoiceConnection, processVoiceAttachment, regis
11
14
  import { getCompactSessionContext, getLastSessionId } from './markdown.js';
12
15
  import { handleOpencodeSession } from './session-handler.js';
13
16
  import { registerInteractionHandler } from './interaction-handler.js';
14
- export { getDatabase, closeDatabase } from './database.js';
17
+ export { getDatabase, closeDatabase, getChannelDirectory } from './database.js';
15
18
  export { initializeOpencodeForDirectory } from './opencode.js';
16
19
  export { escapeBackticksInCodeBlocks, splitMarkdownForDiscord } from './discord-utils.js';
17
20
  export { getOpencodeSystemMessage } from './system-message.js';
@@ -19,7 +22,6 @@ export { ensureKimakiCategory, ensureKimakiAudioCategory, createProjectChannels,
19
22
  import { ChannelType, Client, Events, GatewayIntentBits, Partials, PermissionsBitField, ThreadAutoArchiveDuration, } from 'discord.js';
20
23
  import fs from 'node:fs';
21
24
  import * as errore from 'errore';
22
- import { extractTagsArrays } from './xml.js';
23
25
  import { createLogger } from './logger.js';
24
26
  import { setGlobalDispatcher, Agent } from 'undici';
25
27
  // Increase connection pool to prevent deadlock when multiple sessions have open SSE streams.
@@ -39,7 +41,7 @@ export async function createDiscordClient() {
39
41
  partials: [Partials.Channel, Partials.Message, Partials.User, Partials.ThreadMember],
40
42
  });
41
43
  }
42
- export async function startDiscordBot({ token, appId, discordClient, }) {
44
+ export async function startDiscordBot({ token, appId, discordClient, useWorktrees, }) {
43
45
  if (!discordClient) {
44
46
  discordClient = await createDiscordClient();
45
47
  }
@@ -127,13 +129,12 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
127
129
  const parent = thread.parent;
128
130
  let projectDirectory;
129
131
  let channelAppId;
130
- if (parent?.topic) {
131
- const extracted = extractTagsArrays({
132
- xml: parent.topic,
133
- tags: ['kimaki.directory', 'kimaki.app'],
134
- });
135
- projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
136
- channelAppId = extracted['kimaki.app']?.[0]?.trim();
132
+ if (parent) {
133
+ const channelConfig = getChannelDirectory(parent.id);
134
+ if (channelConfig) {
135
+ projectDirectory = channelConfig.directory;
136
+ channelAppId = channelConfig.appId || undefined;
137
+ }
137
138
  }
138
139
  // Check if this thread is a worktree thread
139
140
  const worktreeInfo = getThreadWorktree(thread.id);
@@ -152,9 +153,11 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
152
153
  });
153
154
  return;
154
155
  }
155
- if (worktreeInfo.worktree_directory) {
156
- projectDirectory = worktreeInfo.worktree_directory;
157
- discordLogger.log(`Using worktree directory: ${projectDirectory}`);
156
+ // Use original project directory for OpenCode server (session lives there)
157
+ // The worktree directory is passed via query.directory in prompt/command calls
158
+ if (worktreeInfo.project_directory) {
159
+ projectDirectory = worktreeInfo.project_directory;
160
+ discordLogger.log(`Using project directory: ${projectDirectory} (worktree: ${worktreeInfo.worktree_directory})`);
158
161
  }
159
162
  }
160
163
  if (channelAppId && channelAppId !== currentAppId) {
@@ -268,20 +271,13 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
268
271
  if (channel.type === ChannelType.GuildText) {
269
272
  const textChannel = channel;
270
273
  voiceLogger.log(`[GUILD_TEXT] Message in text channel #${textChannel.name} (${textChannel.id})`);
271
- if (!textChannel.topic) {
272
- voiceLogger.log(`[IGNORED] Channel #${textChannel.name} has no description`);
273
- return;
274
- }
275
- const extracted = extractTagsArrays({
276
- xml: textChannel.topic,
277
- tags: ['kimaki.directory', 'kimaki.app'],
278
- });
279
- const projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
280
- const channelAppId = extracted['kimaki.app']?.[0]?.trim();
281
- if (!projectDirectory) {
282
- voiceLogger.log(`[IGNORED] Channel #${textChannel.name} has no kimaki.directory tag`);
274
+ const channelConfig = getChannelDirectory(textChannel.id);
275
+ if (!channelConfig) {
276
+ voiceLogger.log(`[IGNORED] Channel #${textChannel.name} has no project directory configured`);
283
277
  return;
284
278
  }
279
+ const projectDirectory = channelConfig.directory;
280
+ const channelAppId = channelConfig.appId || undefined;
285
281
  if (channelAppId && channelAppId !== currentAppId) {
286
282
  voiceLogger.log(`[IGNORED] Channel belongs to different bot app (expected: ${currentAppId}, got: ${channelAppId})`);
287
283
  return;
@@ -299,20 +295,76 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
299
295
  return;
300
296
  }
301
297
  const hasVoice = message.attachments.some((a) => a.contentType?.startsWith('audio/'));
302
- const threadName = hasVoice
298
+ const baseThreadName = hasVoice
303
299
  ? 'Voice Message'
304
300
  : message.content?.replace(/\s+/g, ' ').trim() || 'Claude Thread';
301
+ // Check if worktrees should be enabled (CLI flag OR channel setting)
302
+ const shouldUseWorktrees = useWorktrees || getChannelWorktreesEnabled(textChannel.id);
303
+ // Add worktree prefix if worktrees are enabled
304
+ const threadName = shouldUseWorktrees
305
+ ? `${WORKTREE_PREFIX}${baseThreadName}`
306
+ : baseThreadName;
305
307
  const thread = await message.startThread({
306
308
  name: threadName.slice(0, 80),
307
309
  autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
308
310
  reason: 'Start Claude session',
309
311
  });
310
312
  discordLogger.log(`Created thread "${thread.name}" (${thread.id})`);
313
+ // Create worktree if worktrees are enabled (CLI flag OR channel setting)
314
+ let sessionDirectory = projectDirectory;
315
+ if (shouldUseWorktrees) {
316
+ const worktreeName = formatWorktreeName(hasVoice ? `voice-${Date.now()}` : threadName.slice(0, 50));
317
+ discordLogger.log(`[WORKTREE] Creating worktree: ${worktreeName}`);
318
+ // Store pending worktree immediately so bot knows about it
319
+ createPendingWorktree({
320
+ threadId: thread.id,
321
+ worktreeName,
322
+ projectDirectory,
323
+ });
324
+ // Initialize OpenCode and create worktree
325
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
326
+ if (getClient instanceof Error) {
327
+ discordLogger.error(`[WORKTREE] Failed to init OpenCode: ${getClient.message}`);
328
+ setWorktreeError({ threadId: thread.id, errorMessage: getClient.message });
329
+ await thread.send({
330
+ content: `⚠️ Failed to create worktree: ${getClient.message}\nUsing main project directory instead.`,
331
+ flags: SILENT_MESSAGE_FLAGS,
332
+ });
333
+ }
334
+ else {
335
+ const clientV2 = getOpencodeClientV2(projectDirectory);
336
+ if (!clientV2) {
337
+ discordLogger.error(`[WORKTREE] No v2 client for ${projectDirectory}`);
338
+ setWorktreeError({ threadId: thread.id, errorMessage: 'No OpenCode v2 client' });
339
+ }
340
+ else {
341
+ const worktreeResult = await createWorktreeWithSubmodules({
342
+ clientV2,
343
+ directory: projectDirectory,
344
+ name: worktreeName,
345
+ });
346
+ if (worktreeResult instanceof Error) {
347
+ const errMsg = worktreeResult.message;
348
+ discordLogger.error(`[WORKTREE] Creation failed: ${errMsg}`);
349
+ setWorktreeError({ threadId: thread.id, errorMessage: errMsg });
350
+ await thread.send({
351
+ content: `⚠️ Failed to create worktree: ${errMsg}\nUsing main project directory instead.`,
352
+ flags: SILENT_MESSAGE_FLAGS,
353
+ });
354
+ }
355
+ else {
356
+ setWorktreeReady({ threadId: thread.id, worktreeDirectory: worktreeResult.directory });
357
+ sessionDirectory = worktreeResult.directory;
358
+ discordLogger.log(`[WORKTREE] Created: ${worktreeResult.directory} (branch: ${worktreeResult.branch})`);
359
+ }
360
+ }
361
+ }
362
+ }
311
363
  let messageContent = message.content || '';
312
364
  const transcription = await processVoiceAttachment({
313
365
  message,
314
366
  thread,
315
- projectDirectory,
367
+ projectDirectory: sessionDirectory,
316
368
  isNewThread: true,
317
369
  appId: currentAppId,
318
370
  });
@@ -327,7 +379,7 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
327
379
  await handleOpencodeSession({
328
380
  prompt: promptWithAttachments,
329
381
  thread,
330
- projectDirectory,
382
+ projectDirectory: sessionDirectory,
331
383
  originalMessage: message,
332
384
  images: fileAttachments,
333
385
  channelId: textChannel.id,
@@ -349,53 +401,43 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
349
401
  }
350
402
  });
351
403
  // Handle bot-initiated threads created by `kimaki send` (without --notify-only)
404
+ // Uses embed marker instead of database to avoid race conditions
405
+ const AUTO_START_MARKER = 'kimaki:start';
352
406
  discordClient.on(Events.ThreadCreate, async (thread, newlyCreated) => {
353
407
  try {
354
408
  if (!newlyCreated) {
355
409
  return;
356
410
  }
357
- // Check if this thread is marked for auto-start in the database
358
- const db = getDatabase();
359
- const pendingRow = db
360
- .prepare('SELECT thread_id FROM pending_auto_start WHERE thread_id = ?')
361
- .get(thread.id);
362
- if (!pendingRow) {
363
- return; // Not a CLI-initiated auto-start thread
364
- }
365
- // Remove from pending table
366
- db.prepare('DELETE FROM pending_auto_start WHERE thread_id = ?').run(thread.id);
367
- discordLogger.log(`[BOT_SESSION] Detected bot-initiated thread: ${thread.name}`);
368
411
  // Only handle threads in text channels
369
412
  const parent = thread.parent;
370
413
  if (!parent || parent.type !== ChannelType.GuildText) {
371
414
  return;
372
415
  }
373
- // Get the starter message for the prompt
416
+ // Get the starter message to check for auto-start marker
374
417
  const starterMessage = await thread.fetchStarterMessage().catch(() => null);
375
418
  if (!starterMessage) {
376
419
  discordLogger.log(`[THREAD_CREATE] Could not fetch starter message for thread ${thread.id}`);
377
420
  return;
378
421
  }
422
+ // Check if starter message has the auto-start embed marker
423
+ const hasAutoStartMarker = starterMessage.embeds.some((embed) => embed.footer?.text === AUTO_START_MARKER);
424
+ if (!hasAutoStartMarker) {
425
+ return; // Not a CLI-initiated auto-start thread
426
+ }
427
+ discordLogger.log(`[BOT_SESSION] Detected bot-initiated thread: ${thread.name}`);
379
428
  const prompt = starterMessage.content.trim();
380
429
  if (!prompt) {
381
430
  discordLogger.log(`[BOT_SESSION] No prompt found in starter message`);
382
431
  return;
383
432
  }
384
- // Extract directory from parent channel topic
385
- if (!parent.topic) {
386
- discordLogger.log(`[BOT_SESSION] Parent channel has no topic`);
387
- return;
388
- }
389
- const extracted = extractTagsArrays({
390
- xml: parent.topic,
391
- tags: ['kimaki.directory', 'kimaki.app'],
392
- });
393
- const projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
394
- const channelAppId = extracted['kimaki.app']?.[0]?.trim();
395
- if (!projectDirectory) {
396
- discordLogger.log(`[BOT_SESSION] No kimaki.directory in parent channel topic`);
433
+ // Get directory from database
434
+ const channelConfig = getChannelDirectory(parent.id);
435
+ if (!channelConfig) {
436
+ discordLogger.log(`[BOT_SESSION] No project directory configured for parent channel`);
397
437
  return;
398
438
  }
439
+ const projectDirectory = channelConfig.directory;
440
+ const channelAppId = channelConfig.appId || undefined;
399
441
  if (channelAppId && channelAppId !== currentAppId) {
400
442
  discordLogger.log(`[BOT_SESSION] Channel belongs to different bot app`);
401
443
  return;
@@ -3,8 +3,8 @@
3
3
  // thread message sending, and channel metadata extraction from topic tags.
4
4
  import { ChannelType } from 'discord.js';
5
5
  import { Lexer } from 'marked';
6
- import { extractTagsArrays } from './xml.js';
7
6
  import { formatMarkdownTables } from './format-tables.js';
7
+ import { getChannelDirectory } from './database.js';
8
8
  import { limitHeadingDepth } from './limit-heading-depth.js';
9
9
  import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js';
10
10
  import { createLogger } from './logger.js';
@@ -106,8 +106,14 @@ export function splitMarkdownForDiscord({ content, maxLength, }) {
106
106
  }
107
107
  return pieces;
108
108
  };
109
+ const closingFence = '```\n';
109
110
  for (const line of lines) {
110
- const wouldExceed = currentChunk.length + line.text.length > maxLength;
111
+ const openingFenceSize = currentChunk.length === 0 && (line.inCodeBlock || line.isOpeningFence)
112
+ ? ('```' + line.lang + '\n').length
113
+ : 0;
114
+ const lineLength = line.isOpeningFence ? 0 : line.text.length;
115
+ const activeFenceOverhead = currentLang !== null || openingFenceSize > 0 ? closingFence.length : 0;
116
+ const wouldExceed = currentChunk.length + openingFenceSize + lineLength + activeFenceOverhead > maxLength;
111
117
  if (wouldExceed) {
112
118
  // handle case where single line is longer than maxLength
113
119
  if (line.text.length > maxLength) {
@@ -164,9 +170,37 @@ export function splitMarkdownForDiscord({ content, maxLength, }) {
164
170
  }
165
171
  else {
166
172
  // currentChunk is empty but line still exceeds - shouldn't happen after above check
167
- currentChunk = line.text;
168
- if (line.inCodeBlock || line.isOpeningFence) {
169
- currentLang = line.lang;
173
+ const openingFence = line.inCodeBlock || line.isOpeningFence;
174
+ const openingFenceSize = openingFence ? ('```' + line.lang + '\n').length : 0;
175
+ if (line.text.length + openingFenceSize + activeFenceOverhead > maxLength) {
176
+ const fencedOverhead = openingFence
177
+ ? ('```' + line.lang + '\n').length + closingFence.length
178
+ : 0;
179
+ const availablePerChunk = Math.max(10, maxLength - fencedOverhead - 50);
180
+ const pieces = splitLongLine(line.text, availablePerChunk, line.inCodeBlock);
181
+ for (const piece of pieces) {
182
+ if (openingFence) {
183
+ chunks.push('```' + line.lang + '\n' + piece + closingFence);
184
+ }
185
+ else {
186
+ chunks.push(piece);
187
+ }
188
+ }
189
+ currentChunk = '';
190
+ currentLang = null;
191
+ }
192
+ else {
193
+ if (openingFence) {
194
+ currentChunk = '```' + line.lang + '\n';
195
+ if (!line.isOpeningFence) {
196
+ currentChunk += line.text;
197
+ }
198
+ currentLang = line.lang;
199
+ }
200
+ else {
201
+ currentChunk = line.text;
202
+ currentLang = null;
203
+ }
170
204
  }
171
205
  }
172
206
  }
@@ -181,6 +215,9 @@ export function splitMarkdownForDiscord({ content, maxLength, }) {
181
215
  }
182
216
  }
183
217
  if (currentChunk) {
218
+ if (currentLang !== null) {
219
+ currentChunk += closingFence;
220
+ }
184
221
  chunks.push(currentChunk);
185
222
  }
186
223
  return chunks;
@@ -236,16 +273,17 @@ export function escapeDiscordFormatting(text) {
236
273
  return text.replace(/```/g, '\\`\\`\\`').replace(/````/g, '\\`\\`\\`\\`');
237
274
  }
238
275
  export function getKimakiMetadata(textChannel) {
239
- if (!textChannel?.topic) {
276
+ if (!textChannel) {
240
277
  return {};
241
278
  }
242
- const extracted = extractTagsArrays({
243
- xml: textChannel.topic,
244
- tags: ['kimaki.directory', 'kimaki.app'],
245
- });
246
- const projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
247
- const channelAppId = extracted['kimaki.app']?.[0]?.trim();
248
- return { projectDirectory, channelAppId };
279
+ const channelConfig = getChannelDirectory(textChannel.id);
280
+ if (!channelConfig) {
281
+ return {};
282
+ }
283
+ return {
284
+ projectDirectory: channelConfig.directory,
285
+ channelAppId: channelConfig.appId || undefined,
286
+ };
249
287
  }
250
288
  /**
251
289
  * Upload files to a Discord thread/channel in a single message.
@@ -0,0 +1,20 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { splitMarkdownForDiscord } from './discord-utils.js';
3
+ describe('splitMarkdownForDiscord', () => {
4
+ test('never returns chunks over the max length with code fences', () => {
5
+ const maxLength = 2000;
6
+ const header = '## Summary of Current Architecture\n\n';
7
+ const codeFenceStart = '```\n';
8
+ const codeFenceEnd = '\n```\n';
9
+ const codeLine = 'x'.repeat(180);
10
+ const codeBlock = Array.from({ length: 20 })
11
+ .map(() => codeLine)
12
+ .join('\n');
13
+ const markdown = `${header}${codeFenceStart}${codeBlock}${codeFenceEnd}`;
14
+ const chunks = splitMarkdownForDiscord({ content: markdown, maxLength });
15
+ expect(chunks.length).toBeGreaterThan(1);
16
+ for (const chunk of chunks) {
17
+ expect(chunk.length).toBeLessThanOrEqual(maxLength);
18
+ }
19
+ });
20
+ });
@@ -171,11 +171,17 @@ test('splitMarkdownForDiscord adds closing and opening fences when splitting cod
171
171
  [
172
172
  "\`\`\`js
173
173
  line1
174
+ \`\`\`
175
+ ",
176
+ "\`\`\`js
174
177
  line2
175
178
  \`\`\`
176
179
  ",
177
180
  "\`\`\`js
178
181
  line3
182
+ \`\`\`
183
+ ",
184
+ "\`\`\`js
179
185
  line4
180
186
  \`\`\`
181
187
  ",
@@ -209,10 +215,12 @@ test('splitMarkdownForDiscord handles mixed content with code blocks', () => {
209
215
  [
210
216
  "Text before
211
217
  \`\`\`js
212
- code
213
218
  \`\`\`
214
219
  ",
215
- "Text after",
220
+ "\`\`\`js
221
+ code
222
+ \`\`\`
223
+ Text after",
216
224
  ]
217
225
  `);
218
226
  });
@@ -224,6 +232,9 @@ test('splitMarkdownForDiscord handles code block without language', () => {
224
232
  expect(result).toMatchInlineSnapshot(`
225
233
  [
226
234
  "\`\`\`
235
+ \`\`\`
236
+ ",
237
+ "\`\`\`
227
238
  line1
228
239
  \`\`\`
229
240
  ",
@@ -402,10 +413,10 @@ And here is some text after the code block.`;
402
413
 
403
414
  export function formatCurrency(amount: number): string {
404
415
  return new Intl.NumberFormat('en-US', {
405
- style: 'currency',
406
416
  \`\`\`
407
417
  ",
408
418
  "\`\`\`typescript
419
+ style: 'currency',
409
420
  currency: 'USD',
410
421
  }).format(amount)
411
422
  }
@@ -4,6 +4,8 @@
4
4
  import { Events } from 'discord.js';
5
5
  import { handleSessionCommand, handleSessionAutocomplete } from './commands/session.js';
6
6
  import { handleNewWorktreeCommand } from './commands/worktree.js';
7
+ import { handleMergeWorktreeCommand } from './commands/merge-worktree.js';
8
+ import { handleEnableWorktreesCommand, handleDisableWorktreesCommand, } from './commands/worktree-settings.js';
7
9
  import { handleResumeCommand, handleResumeAutocomplete } from './commands/resume.js';
8
10
  import { handleAddProjectCommand, handleAddProjectAutocomplete } from './commands/add-project.js';
9
11
  import { handleRemoveProjectCommand, handleRemoveProjectAutocomplete, } from './commands/remove-project.js';
@@ -18,6 +20,7 @@ import { handleAskQuestionSelectMenu } from './commands/ask-question.js';
18
20
  import { handleQueueCommand, handleClearQueueCommand } from './commands/queue.js';
19
21
  import { handleUndoCommand, handleRedoCommand } from './commands/undo-redo.js';
20
22
  import { handleUserCommand } from './commands/user-command.js';
23
+ import { handleVerbosityCommand } from './commands/verbosity.js';
21
24
  import { createLogger } from './logger.js';
22
25
  const interactionLogger = createLogger('INTERACTION');
23
26
  export function registerInteractionHandler({ discordClient, appId, }) {
@@ -57,6 +60,15 @@ export function registerInteractionHandler({ discordClient, appId, }) {
57
60
  case 'new-worktree':
58
61
  await handleNewWorktreeCommand({ command: interaction, appId });
59
62
  return;
63
+ case 'merge-worktree':
64
+ await handleMergeWorktreeCommand({ command: interaction, appId });
65
+ return;
66
+ case 'enable-worktrees':
67
+ await handleEnableWorktreesCommand({ command: interaction, appId });
68
+ return;
69
+ case 'disable-worktrees':
70
+ await handleDisableWorktreesCommand({ command: interaction, appId });
71
+ return;
60
72
  case 'resume':
61
73
  await handleResumeCommand({ command: interaction, appId });
62
74
  return;
@@ -97,6 +109,9 @@ export function registerInteractionHandler({ discordClient, appId, }) {
97
109
  case 'redo':
98
110
  await handleRedoCommand({ command: interaction, appId });
99
111
  return;
112
+ case 'verbosity':
113
+ await handleVerbosityCommand({ command: interaction, appId });
114
+ return;
100
115
  }
101
116
  // Handle quick agent commands (ending with -agent suffix, but not the base /agent command)
102
117
  if (interaction.commandName.endsWith('-agent') && interaction.commandName !== 'agent') {