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/dist/discord-bot.js
CHANGED
|
@@ -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
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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;
|
package/dist/discord-utils.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
276
|
+
if (!textChannel) {
|
|
240
277
|
return {};
|
|
241
278
|
}
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
"
|
|
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') {
|