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
@@ -2,8 +2,20 @@
2
2
  // Bridges Discord messages to OpenCode sessions, manages voice connections,
3
3
  // and orchestrates the main event loop for the Kimaki bot.
4
4
 
5
- import { getDatabase, closeDatabase, getThreadWorktree } from './database.js'
6
- import { initializeOpencodeForDirectory, getOpencodeServers } from './opencode.js'
5
+ import {
6
+ getDatabase,
7
+ closeDatabase,
8
+ getThreadWorktree,
9
+ createPendingWorktree,
10
+ setWorktreeReady,
11
+ setWorktreeError,
12
+ getChannelWorktreesEnabled,
13
+ getChannelDirectory,
14
+ } from './database.js'
15
+ import { initializeOpencodeForDirectory, getOpencodeServers, getOpencodeClientV2 } from './opencode.js'
16
+ import { formatWorktreeName } from './commands/worktree.js'
17
+ import { WORKTREE_PREFIX } from './commands/merge-worktree.js'
18
+ import { createWorktreeWithSubmodules } from './worktree-utils.js'
7
19
  import {
8
20
  escapeBackticksInCodeBlocks,
9
21
  splitMarkdownForDiscord,
@@ -28,7 +40,7 @@ import { getCompactSessionContext, getLastSessionId } from './markdown.js'
28
40
  import { handleOpencodeSession } from './session-handler.js'
29
41
  import { registerInteractionHandler } from './interaction-handler.js'
30
42
 
31
- export { getDatabase, closeDatabase } from './database.js'
43
+ export { getDatabase, closeDatabase, getChannelDirectory } from './database.js'
32
44
  export { initializeOpencodeForDirectory } from './opencode.js'
33
45
  export { escapeBackticksInCodeBlocks, splitMarkdownForDiscord } from './discord-utils.js'
34
46
  export { getOpencodeSystemMessage } from './system-message.js'
@@ -54,7 +66,6 @@ import {
54
66
  } from 'discord.js'
55
67
  import fs from 'node:fs'
56
68
  import * as errore from 'errore'
57
- import { extractTagsArrays } from './xml.js'
58
69
  import { createLogger } from './logger.js'
59
70
  import { setGlobalDispatcher, Agent } from 'undici'
60
71
 
@@ -69,6 +80,8 @@ const voiceLogger = createLogger('VOICE')
69
80
  type StartOptions = {
70
81
  token: string
71
82
  appId?: string
83
+ /** When true, all new sessions from channel messages create git worktrees */
84
+ useWorktrees?: boolean
72
85
  }
73
86
 
74
87
  export async function createDiscordClient() {
@@ -87,6 +100,7 @@ export async function startDiscordBot({
87
100
  token,
88
101
  appId,
89
102
  discordClient,
103
+ useWorktrees,
90
104
  }: StartOptions & { discordClient?: Client }) {
91
105
  if (!discordClient) {
92
106
  discordClient = await createDiscordClient()
@@ -197,14 +211,12 @@ export async function startDiscordBot({
197
211
  let projectDirectory: string | undefined
198
212
  let channelAppId: string | undefined
199
213
 
200
- if (parent?.topic) {
201
- const extracted = extractTagsArrays({
202
- xml: parent.topic,
203
- tags: ['kimaki.directory', 'kimaki.app'],
204
- })
205
-
206
- projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
207
- channelAppId = extracted['kimaki.app']?.[0]?.trim()
214
+ if (parent) {
215
+ const channelConfig = getChannelDirectory(parent.id)
216
+ if (channelConfig) {
217
+ projectDirectory = channelConfig.directory
218
+ channelAppId = channelConfig.appId || undefined
219
+ }
208
220
  }
209
221
 
210
222
  // Check if this thread is a worktree thread
@@ -224,9 +236,11 @@ export async function startDiscordBot({
224
236
  })
225
237
  return
226
238
  }
227
- if (worktreeInfo.worktree_directory) {
228
- projectDirectory = worktreeInfo.worktree_directory
229
- discordLogger.log(`Using worktree directory: ${projectDirectory}`)
239
+ // Use original project directory for OpenCode server (session lives there)
240
+ // The worktree directory is passed via query.directory in prompt/command calls
241
+ if (worktreeInfo.project_directory) {
242
+ projectDirectory = worktreeInfo.project_directory
243
+ discordLogger.log(`Using project directory: ${projectDirectory} (worktree: ${worktreeInfo.worktree_directory})`)
230
244
  }
231
245
  }
232
246
 
@@ -360,24 +374,16 @@ export async function startDiscordBot({
360
374
  `[GUILD_TEXT] Message in text channel #${textChannel.name} (${textChannel.id})`,
361
375
  )
362
376
 
363
- if (!textChannel.topic) {
364
- voiceLogger.log(`[IGNORED] Channel #${textChannel.name} has no description`)
365
- return
366
- }
367
-
368
- const extracted = extractTagsArrays({
369
- xml: textChannel.topic,
370
- tags: ['kimaki.directory', 'kimaki.app'],
371
- })
372
-
373
- const projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
374
- const channelAppId = extracted['kimaki.app']?.[0]?.trim()
377
+ const channelConfig = getChannelDirectory(textChannel.id)
375
378
 
376
- if (!projectDirectory) {
377
- voiceLogger.log(`[IGNORED] Channel #${textChannel.name} has no kimaki.directory tag`)
379
+ if (!channelConfig) {
380
+ voiceLogger.log(`[IGNORED] Channel #${textChannel.name} has no project directory configured`)
378
381
  return
379
382
  }
380
383
 
384
+ const projectDirectory = channelConfig.directory
385
+ const channelAppId = channelConfig.appId || undefined
386
+
381
387
  if (channelAppId && channelAppId !== currentAppId) {
382
388
  voiceLogger.log(
383
389
  `[IGNORED] Channel belongs to different bot app (expected: ${currentAppId}, got: ${channelAppId})`,
@@ -401,10 +407,18 @@ export async function startDiscordBot({
401
407
 
402
408
  const hasVoice = message.attachments.some((a) => a.contentType?.startsWith('audio/'))
403
409
 
404
- const threadName = hasVoice
410
+ const baseThreadName = hasVoice
405
411
  ? 'Voice Message'
406
412
  : message.content?.replace(/\s+/g, ' ').trim() || 'Claude Thread'
407
413
 
414
+ // Check if worktrees should be enabled (CLI flag OR channel setting)
415
+ const shouldUseWorktrees = useWorktrees || getChannelWorktreesEnabled(textChannel.id)
416
+
417
+ // Add worktree prefix if worktrees are enabled
418
+ const threadName = shouldUseWorktrees
419
+ ? `${WORKTREE_PREFIX}${baseThreadName}`
420
+ : baseThreadName
421
+
408
422
  const thread = await message.startThread({
409
423
  name: threadName.slice(0, 80),
410
424
  autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
@@ -413,12 +427,65 @@ export async function startDiscordBot({
413
427
 
414
428
  discordLogger.log(`Created thread "${thread.name}" (${thread.id})`)
415
429
 
430
+ // Create worktree if worktrees are enabled (CLI flag OR channel setting)
431
+ let sessionDirectory = projectDirectory
432
+ if (shouldUseWorktrees) {
433
+ const worktreeName = formatWorktreeName(
434
+ hasVoice ? `voice-${Date.now()}` : threadName.slice(0, 50),
435
+ )
436
+ discordLogger.log(`[WORKTREE] Creating worktree: ${worktreeName}`)
437
+
438
+ // Store pending worktree immediately so bot knows about it
439
+ createPendingWorktree({
440
+ threadId: thread.id,
441
+ worktreeName,
442
+ projectDirectory,
443
+ })
444
+
445
+ // Initialize OpenCode and create worktree
446
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
447
+ if (getClient instanceof Error) {
448
+ discordLogger.error(`[WORKTREE] Failed to init OpenCode: ${getClient.message}`)
449
+ setWorktreeError({ threadId: thread.id, errorMessage: getClient.message })
450
+ await thread.send({
451
+ content: `⚠️ Failed to create worktree: ${getClient.message}\nUsing main project directory instead.`,
452
+ flags: SILENT_MESSAGE_FLAGS,
453
+ })
454
+ } else {
455
+ const clientV2 = getOpencodeClientV2(projectDirectory)
456
+ if (!clientV2) {
457
+ discordLogger.error(`[WORKTREE] No v2 client for ${projectDirectory}`)
458
+ setWorktreeError({ threadId: thread.id, errorMessage: 'No OpenCode v2 client' })
459
+ } else {
460
+ const worktreeResult = await createWorktreeWithSubmodules({
461
+ clientV2,
462
+ directory: projectDirectory,
463
+ name: worktreeName,
464
+ })
465
+
466
+ if (worktreeResult instanceof Error) {
467
+ const errMsg = worktreeResult.message
468
+ discordLogger.error(`[WORKTREE] Creation failed: ${errMsg}`)
469
+ setWorktreeError({ threadId: thread.id, errorMessage: errMsg })
470
+ await thread.send({
471
+ content: `⚠️ Failed to create worktree: ${errMsg}\nUsing main project directory instead.`,
472
+ flags: SILENT_MESSAGE_FLAGS,
473
+ })
474
+ } else {
475
+ setWorktreeReady({ threadId: thread.id, worktreeDirectory: worktreeResult.directory })
476
+ sessionDirectory = worktreeResult.directory
477
+ discordLogger.log(`[WORKTREE] Created: ${worktreeResult.directory} (branch: ${worktreeResult.branch})`)
478
+ }
479
+ }
480
+ }
481
+ }
482
+
416
483
  let messageContent = message.content || ''
417
484
 
418
485
  const transcription = await processVoiceAttachment({
419
486
  message,
420
487
  thread,
421
- projectDirectory,
488
+ projectDirectory: sessionDirectory,
422
489
  isNewThread: true,
423
490
  appId: currentAppId,
424
491
  })
@@ -434,7 +501,7 @@ export async function startDiscordBot({
434
501
  await handleOpencodeSession({
435
502
  prompt: promptWithAttachments,
436
503
  thread,
437
- projectDirectory,
504
+ projectDirectory: sessionDirectory,
438
505
  originalMessage: message,
439
506
  images: fileAttachments,
440
507
  channelId: textChannel.id,
@@ -454,65 +521,54 @@ export async function startDiscordBot({
454
521
  })
455
522
 
456
523
  // Handle bot-initiated threads created by `kimaki send` (without --notify-only)
524
+ // Uses embed marker instead of database to avoid race conditions
525
+ const AUTO_START_MARKER = 'kimaki:start'
457
526
  discordClient.on(Events.ThreadCreate, async (thread, newlyCreated) => {
458
527
  try {
459
528
  if (!newlyCreated) {
460
529
  return
461
530
  }
462
531
 
463
- // Check if this thread is marked for auto-start in the database
464
- const db = getDatabase()
465
- const pendingRow = db
466
- .prepare('SELECT thread_id FROM pending_auto_start WHERE thread_id = ?')
467
- .get(thread.id) as { thread_id: string } | undefined
468
-
469
- if (!pendingRow) {
470
- return // Not a CLI-initiated auto-start thread
471
- }
472
-
473
- // Remove from pending table
474
- db.prepare('DELETE FROM pending_auto_start WHERE thread_id = ?').run(thread.id)
475
-
476
- discordLogger.log(`[BOT_SESSION] Detected bot-initiated thread: ${thread.name}`)
477
-
478
532
  // Only handle threads in text channels
479
533
  const parent = thread.parent as TextChannel | null
480
534
  if (!parent || parent.type !== ChannelType.GuildText) {
481
535
  return
482
536
  }
483
537
 
484
- // Get the starter message for the prompt
538
+ // Get the starter message to check for auto-start marker
485
539
  const starterMessage = await thread.fetchStarterMessage().catch(() => null)
486
540
  if (!starterMessage) {
487
541
  discordLogger.log(`[THREAD_CREATE] Could not fetch starter message for thread ${thread.id}`)
488
542
  return
489
543
  }
490
544
 
545
+ // Check if starter message has the auto-start embed marker
546
+ const hasAutoStartMarker = starterMessage.embeds.some(
547
+ (embed) => embed.footer?.text === AUTO_START_MARKER,
548
+ )
549
+ if (!hasAutoStartMarker) {
550
+ return // Not a CLI-initiated auto-start thread
551
+ }
552
+
553
+ discordLogger.log(`[BOT_SESSION] Detected bot-initiated thread: ${thread.name}`)
554
+
491
555
  const prompt = starterMessage.content.trim()
492
556
  if (!prompt) {
493
557
  discordLogger.log(`[BOT_SESSION] No prompt found in starter message`)
494
558
  return
495
559
  }
496
560
 
497
- // Extract directory from parent channel topic
498
- if (!parent.topic) {
499
- discordLogger.log(`[BOT_SESSION] Parent channel has no topic`)
500
- return
501
- }
502
-
503
- const extracted = extractTagsArrays({
504
- xml: parent.topic,
505
- tags: ['kimaki.directory', 'kimaki.app'],
506
- })
561
+ // Get directory from database
562
+ const channelConfig = getChannelDirectory(parent.id)
507
563
 
508
- const projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
509
- const channelAppId = extracted['kimaki.app']?.[0]?.trim()
510
-
511
- if (!projectDirectory) {
512
- discordLogger.log(`[BOT_SESSION] No kimaki.directory in parent channel topic`)
564
+ if (!channelConfig) {
565
+ discordLogger.log(`[BOT_SESSION] No project directory configured for parent channel`)
513
566
  return
514
567
  }
515
568
 
569
+ const projectDirectory = channelConfig.directory
570
+ const channelAppId = channelConfig.appId || undefined
571
+
516
572
  if (channelAppId && channelAppId !== currentAppId) {
517
573
  discordLogger.log(`[BOT_SESSION] Channel belongs to different bot app`)
518
574
  return
@@ -0,0 +1,23 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { splitMarkdownForDiscord } from './discord-utils.js'
3
+
4
+ describe('splitMarkdownForDiscord', () => {
5
+ test('never returns chunks over the max length with code fences', () => {
6
+ const maxLength = 2000
7
+ const header = '## Summary of Current Architecture\n\n'
8
+ const codeFenceStart = '```\n'
9
+ const codeFenceEnd = '\n```\n'
10
+ const codeLine = 'x'.repeat(180)
11
+ const codeBlock = Array.from({ length: 20 })
12
+ .map(() => codeLine)
13
+ .join('\n')
14
+ const markdown = `${header}${codeFenceStart}${codeBlock}${codeFenceEnd}`
15
+
16
+ const chunks = splitMarkdownForDiscord({ content: markdown, maxLength })
17
+
18
+ expect(chunks.length).toBeGreaterThan(1)
19
+ for (const chunk of chunks) {
20
+ expect(chunk.length).toBeLessThanOrEqual(maxLength)
21
+ }
22
+ })
23
+ })
@@ -4,8 +4,8 @@
4
4
 
5
5
  import { ChannelType, type Message, type TextChannel, type ThreadChannel } from 'discord.js'
6
6
  import { Lexer } from 'marked'
7
- import { extractTagsArrays } from './xml.js'
8
7
  import { formatMarkdownTables } from './format-tables.js'
8
+ import { getChannelDirectory } from './database.js'
9
9
  import { limitHeadingDepth } from './limit-heading-depth.js'
10
10
  import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js'
11
11
  import { createLogger } from './logger.js'
@@ -132,8 +132,18 @@ export function splitMarkdownForDiscord({
132
132
  return pieces
133
133
  }
134
134
 
135
+ const closingFence = '```\n'
136
+
135
137
  for (const line of lines) {
136
- const wouldExceed = currentChunk.length + line.text.length > maxLength
138
+ const openingFenceSize =
139
+ currentChunk.length === 0 && (line.inCodeBlock || line.isOpeningFence)
140
+ ? ('```' + line.lang + '\n').length
141
+ : 0
142
+ const lineLength = line.isOpeningFence ? 0 : line.text.length
143
+ const activeFenceOverhead =
144
+ currentLang !== null || openingFenceSize > 0 ? closingFence.length : 0
145
+ const wouldExceed =
146
+ currentChunk.length + openingFenceSize + lineLength + activeFenceOverhead > maxLength
137
147
 
138
148
  if (wouldExceed) {
139
149
  // handle case where single line is longer than maxLength
@@ -195,9 +205,34 @@ export function splitMarkdownForDiscord({
195
205
  }
196
206
  } else {
197
207
  // currentChunk is empty but line still exceeds - shouldn't happen after above check
198
- currentChunk = line.text
199
- if (line.inCodeBlock || line.isOpeningFence) {
200
- currentLang = line.lang
208
+ const openingFence = line.inCodeBlock || line.isOpeningFence
209
+ const openingFenceSize = openingFence ? ('```' + line.lang + '\n').length : 0
210
+ if (line.text.length + openingFenceSize + activeFenceOverhead > maxLength) {
211
+ const fencedOverhead = openingFence
212
+ ? ('```' + line.lang + '\n').length + closingFence.length
213
+ : 0
214
+ const availablePerChunk = Math.max(10, maxLength - fencedOverhead - 50)
215
+ const pieces = splitLongLine(line.text, availablePerChunk, line.inCodeBlock)
216
+ for (const piece of pieces) {
217
+ if (openingFence) {
218
+ chunks.push('```' + line.lang + '\n' + piece + closingFence)
219
+ } else {
220
+ chunks.push(piece)
221
+ }
222
+ }
223
+ currentChunk = ''
224
+ currentLang = null
225
+ } else {
226
+ if (openingFence) {
227
+ currentChunk = '```' + line.lang + '\n'
228
+ if (!line.isOpeningFence) {
229
+ currentChunk += line.text
230
+ }
231
+ currentLang = line.lang
232
+ } else {
233
+ currentChunk = line.text
234
+ currentLang = null
235
+ }
201
236
  }
202
237
  }
203
238
  } else {
@@ -211,6 +246,9 @@ export function splitMarkdownForDiscord({
211
246
  }
212
247
 
213
248
  if (currentChunk) {
249
+ if (currentLang !== null) {
250
+ currentChunk += closingFence
251
+ }
214
252
  chunks.push(currentChunk)
215
253
  }
216
254
 
@@ -291,19 +329,20 @@ export function getKimakiMetadata(textChannel: TextChannel | null): {
291
329
  projectDirectory?: string
292
330
  channelAppId?: string
293
331
  } {
294
- if (!textChannel?.topic) {
332
+ if (!textChannel) {
295
333
  return {}
296
334
  }
297
335
 
298
- const extracted = extractTagsArrays({
299
- xml: textChannel.topic,
300
- tags: ['kimaki.directory', 'kimaki.app'],
301
- })
336
+ const channelConfig = getChannelDirectory(textChannel.id)
302
337
 
303
- const projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
304
- const channelAppId = extracted['kimaki.app']?.[0]?.trim()
338
+ if (!channelConfig) {
339
+ return {}
340
+ }
305
341
 
306
- return { projectDirectory, channelAppId }
342
+ return {
343
+ projectDirectory: channelConfig.directory,
344
+ channelAppId: channelConfig.appId || undefined,
345
+ }
307
346
  }
308
347
 
309
348
  /**
@@ -194,11 +194,17 @@ test('splitMarkdownForDiscord adds closing and opening fences when splitting cod
194
194
  [
195
195
  "\`\`\`js
196
196
  line1
197
+ \`\`\`
198
+ ",
199
+ "\`\`\`js
197
200
  line2
198
201
  \`\`\`
199
202
  ",
200
203
  "\`\`\`js
201
204
  line3
205
+ \`\`\`
206
+ ",
207
+ "\`\`\`js
202
208
  line4
203
209
  \`\`\`
204
210
  ",
@@ -234,10 +240,12 @@ test('splitMarkdownForDiscord handles mixed content with code blocks', () => {
234
240
  [
235
241
  "Text before
236
242
  \`\`\`js
237
- code
238
243
  \`\`\`
239
244
  ",
240
- "Text after",
245
+ "\`\`\`js
246
+ code
247
+ \`\`\`
248
+ Text after",
241
249
  ]
242
250
  `)
243
251
  })
@@ -250,6 +258,9 @@ test('splitMarkdownForDiscord handles code block without language', () => {
250
258
  expect(result).toMatchInlineSnapshot(`
251
259
  [
252
260
  "\`\`\`
261
+ \`\`\`
262
+ ",
263
+ "\`\`\`
253
264
  line1
254
265
  \`\`\`
255
266
  ",
@@ -437,10 +448,10 @@ And here is some text after the code block.`
437
448
 
438
449
  export function formatCurrency(amount: number): string {
439
450
  return new Intl.NumberFormat('en-US', {
440
- style: 'currency',
441
451
  \`\`\`
442
452
  ",
443
453
  "\`\`\`typescript
454
+ style: 'currency',
444
455
  currency: 'USD',
445
456
  }).format(amount)
446
457
  }
@@ -5,6 +5,11 @@
5
5
  import { Events, type Client, type Interaction } from 'discord.js'
6
6
  import { handleSessionCommand, handleSessionAutocomplete } from './commands/session.js'
7
7
  import { handleNewWorktreeCommand } from './commands/worktree.js'
8
+ import { handleMergeWorktreeCommand } from './commands/merge-worktree.js'
9
+ import {
10
+ handleEnableWorktreesCommand,
11
+ handleDisableWorktreesCommand,
12
+ } from './commands/worktree-settings.js'
8
13
  import { handleResumeCommand, handleResumeAutocomplete } from './commands/resume.js'
9
14
  import { handleAddProjectCommand, handleAddProjectAutocomplete } from './commands/add-project.js'
10
15
  import {
@@ -26,6 +31,7 @@ import { handleAskQuestionSelectMenu } from './commands/ask-question.js'
26
31
  import { handleQueueCommand, handleClearQueueCommand } from './commands/queue.js'
27
32
  import { handleUndoCommand, handleRedoCommand } from './commands/undo-redo.js'
28
33
  import { handleUserCommand } from './commands/user-command.js'
34
+ import { handleVerbosityCommand } from './commands/verbosity.js'
29
35
  import { createLogger } from './logger.js'
30
36
 
31
37
  const interactionLogger = createLogger('INTERACTION')
@@ -87,6 +93,18 @@ export function registerInteractionHandler({
87
93
  await handleNewWorktreeCommand({ command: interaction, appId })
88
94
  return
89
95
 
96
+ case 'merge-worktree':
97
+ await handleMergeWorktreeCommand({ command: interaction, appId })
98
+ return
99
+
100
+ case 'enable-worktrees':
101
+ await handleEnableWorktreesCommand({ command: interaction, appId })
102
+ return
103
+
104
+ case 'disable-worktrees':
105
+ await handleDisableWorktreesCommand({ command: interaction, appId })
106
+ return
107
+
90
108
  case 'resume':
91
109
  await handleResumeCommand({ command: interaction, appId })
92
110
  return
@@ -139,6 +157,10 @@ export function registerInteractionHandler({
139
157
  case 'redo':
140
158
  await handleRedoCommand({ command: interaction, appId })
141
159
  return
160
+
161
+ case 'verbosity':
162
+ await handleVerbosityCommand({ command: interaction, appId })
163
+ return
142
164
  }
143
165
 
144
166
  // Handle quick agent commands (ending with -agent suffix, but not the base /agent command)