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
package/src/discord-bot.ts
CHANGED
|
@@ -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 {
|
|
6
|
-
|
|
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
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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 (!
|
|
377
|
-
voiceLogger.log(`[IGNORED] Channel #${textChannel.name} has no
|
|
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
|
|
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
|
|
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
|
-
//
|
|
498
|
-
|
|
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
|
-
|
|
509
|
-
|
|
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
|
+
})
|
package/src/discord-utils.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
332
|
+
if (!textChannel) {
|
|
295
333
|
return {}
|
|
296
334
|
}
|
|
297
335
|
|
|
298
|
-
const
|
|
299
|
-
xml: textChannel.topic,
|
|
300
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
301
|
-
})
|
|
336
|
+
const channelConfig = getChannelDirectory(textChannel.id)
|
|
302
337
|
|
|
303
|
-
|
|
304
|
-
|
|
338
|
+
if (!channelConfig) {
|
|
339
|
+
return {}
|
|
340
|
+
}
|
|
305
341
|
|
|
306
|
-
return {
|
|
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
|
-
"
|
|
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)
|